#!/usr/bin/env node // // One-off, idempotent sync of the canonical roadmap content into a target DB. // // BIFROST_DB_PATH=/opt/fenja/data/bifrost-portal/bifrost.db node scripts/sync-roadmap.js // // What it does, in order: // 1. Takes its OWN online backup of the target DB to // .pre-roadmap-sync-.bak (safe while the app is running). // 2. Upserts the 10 canonical items MATCHED BY display_order — it UPDATEs // title / description / status / target / features in place, so row ids, // roadmap_attributions, created_at and shipped_at are all preserved. // 3. Inserts a canonical item only if no row exists at that display_order. // 4. Leaves any extra prod rows (display_order outside 1..N) untouched and // logs them — it never deletes. // // Safe to re-run. Does NOT wipe the table (unlike scripts/seed-demo.js, which // must never be run against prod). import Database from 'better-sqlite3'; import { existsSync } from 'node:fs'; // ── Canonical roadmap (content only; no demo dates/attributions) ─────────── const ROADMAP = [ { display_order: 1, title: 'MCP Usage', status: 'shipping', target: 'July 2026', description: 'Connect Fenja to external MCP servers — databases, internal tools, and other software — so it can read from and act across your systems.', features: ['Connect external MCP servers', 'Reach databases, internal tools & software', 'Per-connection credentials & scoping'] }, { display_order: 2, title: 'Extended Logging', status: 'shipping', target: 'July 2026', description: 'Granular logging and audit controls, capturing usage down to the individual user, request, and document.', features: ['Granular usage & audit logging', 'Per-user, per-request, per-document trails', 'Exportable audit records'] }, { display_order: 3, title: 'Wiki 2.0', status: 'in_beta', target: 'July 2026', description: 'A major wiki upgrade: version history with diffs between revisions, admin-locked pages, and a richer page structure.', features: ['Version history with revision diffs', 'Admin-locked pages', 'Richer page structure'] }, { display_order: 4, title: 'Interview 2.0', status: 'planned', target: 'July 2026', description: 'A major Interviews upgrade: build and manage interview scripts, send invites, and track responses in one place.', features: ['Build & manage interview scripts', 'Send invites & track responses', 'Centralised results view'] }, { display_order: 5, title: 'Fenja Analyze', status: 'planned', target: 'August 2026', description: 'The first release of Fenja Analyze: ask a question in plain language, run it as a query against your data, and get an answer with sources.', features: ['Plain-language questions → queries', 'Answers grounded in your data', 'Source citations'] }, { display_order: 6, title: 'Fenja Agentic', status: 'planned', target: 'September 2026', description: 'The first release of Agents: create and orchestrate agents from an admin panel, wiring them to the tools and data they need.', features: ['Create agents from an admin panel', 'Orchestrate multi-agent workflows', 'Wire agents to tools & data'] }, { display_order: 7, title: 'Fenja Dev', status: 'planned', target: 'September 2026', description: 'The first release of Fenja Dev: a sovereign IDE with terminal and git integration, running entirely inside your environment.', features: ['Sovereign in-environment IDE', 'Integrated terminal', 'Git integration'] }, { display_order: 8, title: 'HTML Reports', status: 'planned', target: 'September 2026', description: 'Structured reporting for Fenja Analyze: admins define report templates so generated analyses render as clean, shareable HTML.', features: ['Admin-defined report templates', 'Analyses rendered as clean HTML', 'Shareable, structured output'] }, { display_order: 9, title: 'Self-Service Agents', status: 'planned', target: 'October 2026', description: 'A major expansion of the agent experience, letting users create and run their own agents without admin involvement.', features: ['User-created agents (no admin needed)', 'Run & manage your own agents', 'Reuse across tasks'] }, { display_order: 10, title: 'Self-service Routines & Skills', status: 'exploring', target: 'October 2026', description: "Personal and domain-level routines and skills: users build reusable, tailored workflows for their own and their team's recurring tasks.", features: ['Personal & domain-level routines', 'Reusable, tailored workflows', 'Built for recurring team tasks'] }, ]; async function main() { const dbPath = process.env.BIFROST_DB_PATH; if (!dbPath) { console.error('!! BIFROST_DB_PATH is not set. Refusing to guess the DB location.'); process.exit(1); } if (!existsSync(dbPath)) { console.error(`!! No database at ${dbPath}`); process.exit(1); } const db = new Database(dbPath); db.pragma('foreign_keys = ON'); // Guard: the features column must exist (migration 0009). Deploy runs // migrate.js, so run this AFTER deploying. const cols = db.prepare('PRAGMA table_info(roadmap_items)').all().map((c) => c.name); if (!cols.includes('features')) { console.error('!! roadmap_items.features column is missing — run `node scripts/migrate.js` (deploy) first.'); process.exit(1); } // 1. Online backup (safe while the app is running). Timestamp from the OS. const ts = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = `${dbPath}.pre-roadmap-sync-${ts}.bak`; console.log(`==> Backing up current DB -> ${backupPath}`); await db.backup(backupPath); // Snapshot current titles for the diff log. const before = db.prepare('SELECT display_order, title FROM roadmap_items ORDER BY display_order').all(); console.log(`==> Current roadmap (${before.length} rows):`); before.forEach((r) => console.log(` ${r.display_order}. ${r.title}`)); // 2/3. Upsert by display_order, inside a transaction. const findByOrder = db.prepare('SELECT id FROM roadmap_items WHERE display_order = ?'); const update = db.prepare(` UPDATE roadmap_items SET title = @title, description = @description, status = @status, target = @target, features = @features, updated_at = datetime('now') WHERE id = @id `); const insert = db.prepare(` INSERT INTO roadmap_items (title, description, status, target, display_order, shipped_at, metadata_text, features) VALUES (@title, @description, @status, @target, @display_order, NULL, NULL, @features) `); let updated = 0; let inserted = 0; const run = db.transaction(() => { for (const item of ROADMAP) { const row = findByOrder.get(item.display_order); const params = { title: item.title, description: item.description, status: item.status, target: item.target, features: JSON.stringify(item.features), display_order: item.display_order, }; if (row) { update.run({ ...params, id: row.id }); updated += 1; } else { insert.run(params); inserted += 1; } } }); run(); // 4. Report extras that were left untouched (never deleted). const maxOrder = ROADMAP.length; const extras = db .prepare('SELECT display_order, title FROM roadmap_items WHERE display_order > ? ORDER BY display_order') .all(maxOrder); console.log(`==> Synced: ${updated} updated in place, ${inserted} inserted.`); if (extras.length > 0) { console.log(`==> Left untouched (display_order > ${maxOrder}) — review in /admin if these should go:`); extras.forEach((r) => console.log(` ${r.display_order}. ${r.title}`)); } const after = db.prepare('SELECT display_order, title, status, target FROM roadmap_items ORDER BY display_order').all(); console.log('==> Roadmap now:'); after.forEach((r) => console.log(` ${r.display_order}. ${r.title} [${r.status} · ${r.target}]`)); db.close(); console.log(`==> Done. Backup kept at ${backupPath}`); } main().catch((err) => { console.error(err); process.exit(1); });