import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import { getApiKey, getProjectId, getApiUrl, setProjectId } from '../utils/config.js'; import axios from 'axios'; import fs from 'fs/promises'; import path from 'path'; export function createSyncCommand() { const sync = new Command('sync'); sync .description('Synchronize local state with Rigstate Cloud') .option('-p, --project ', 'Specify Project ID (saves to config automatically)') .action(async (options) => { const spinner = ora('Synchronizing project state...').start(); try { // 1. Authentication Check let apiKey; try { apiKey = getApiKey(); } catch (e) { spinner.fail('Not authenticated. Run "rigstate login" first.'); return; } // 2. Project Context Resolution let projectId = options.project; // Check local .rigstate manifest if (!projectId) { const { loadManifest } = await import('../utils/manifest.js'); const manifest = await loadManifest(); if (manifest?.project_id) projectId = manifest.project_id; } // Check global config if (!projectId) projectId = getProjectId(); if (options.project) { // Persistence: Save project ID for future use setProjectId(options.project); } if (!projectId) { spinner.fail('No project context found.\n Run with --project once to save context.'); return; } const apiUrl = getApiUrl(); // 3. API Execution const response = await axios.get(`${apiUrl}/api/v1/roadmap`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` } }); // Parse Standardized Response ({ success, data, ... }) if (!response.data.success) { throw new Error(response.data.error || 'Unknown API failure'); } const { roadmap, project } = response.data.data; const timestamp = response.data.timestamp; // 4. Write Artifacts const targetPath = path.join(process.cwd(), 'roadmap.json'); const fileContent = JSON.stringify({ project, last_synced: timestamp, roadmap }, null, 2); await fs.writeFile(targetPath, fileContent, 'utf-8'); // 4b. Write Context Manifest (.rigstate) - CONTEXT GUARD try { const { saveManifest } = await import('../utils/manifest.js'); await saveManifest({ project_id: projectId, linked_at: timestamp, // Using timestamp as linked_at for consistency api_url: apiUrl }); } catch (e) { // Fail silently } // 4c. Provision Agent Skills (Skills Dominion) console.log(chalk.bold('\n🧠 Agent Skills Provisioning...')); try { const { provisionSkills, generateSkillsDiscoveryBlock } = await import('../utils/skills-provisioner.js'); const skills = await provisionSkills(apiUrl, apiKey, projectId, process.cwd()); // Update .cursorrules with skills discovery block (if file exists) const cursorRulesPath = path.join(process.cwd(), '.cursorrules'); try { let rulesContent = await fs.readFile(cursorRulesPath, 'utf-8'); const skillsBlock = generateSkillsDiscoveryBlock(skills); // Replace existing skills block or insert after PROJECT CONTEXT if (rulesContent.includes('')) { rulesContent = rulesContent.replace( /[\s\S]*?<\/available_skills>/, skillsBlock ); } else if (rulesContent.includes('## 🧠 PROJECT CONTEXT')) { // Insert after PROJECT CONTEXT section const insertPoint = rulesContent.indexOf('---', rulesContent.indexOf('## 🧠 PROJECT CONTEXT')); if (insertPoint !== -1) { rulesContent = rulesContent.slice(0, insertPoint + 3) + '\n\n' + skillsBlock + '\n' + rulesContent.slice(insertPoint + 3); } } await fs.writeFile(cursorRulesPath, rulesContent, 'utf-8'); console.log(chalk.dim(` Updated .cursorrules with skills discovery block`)); } catch (e) { // .cursorrules doesn't exist or couldn't be updated } } catch (e: any) { console.log(chalk.yellow(` ⚠ Skills provisioning skipped: ${e.message}`)); } // 5. Process Execution Logs (MISSION REPORTING) try { const logPath = path.join(process.cwd(), '.rigstate', 'logs', 'last_execution.json'); try { const logContent = await fs.readFile(logPath, 'utf-8'); const logData = JSON.parse(logContent); if (logData.task_summary) { await axios.post(`${apiUrl}/api/v1/execution-logs`, { project_id: projectId, ...logData, agent_role: process.env.RIGSTATE_MODE === 'SUPERVISOR' ? 'SUPERVISOR' : 'WORKER' }, { headers: { Authorization: `Bearer ${apiKey}` } }); await fs.unlink(logPath); console.log(chalk.dim(`āœ” Mission Report uploaded.`)); } } catch (e: any) { // Ignore ENOENT (file not found), log errors if API fails if (e.code !== 'ENOENT') { // console.log(chalk.yellow('Log upload skipped: ' + e.message)); } } } catch (e) { } // 6. User Feedback spinner.succeed(chalk.green(`Synced ${roadmap.length} roadmap steps for project "${project}"`)); console.log(chalk.dim(`Local files updated: roadmap.json`)); const { runGuardianWatchdog } = await import('../utils/watchdog.js'); const settings = response.data.data.settings || {}; await runGuardianWatchdog(process.cwd(), settings, projectId); // 8. Bridge Heartbeat & Pending Tasks console.log(chalk.bold('\nšŸ“” Agent Bridge Heartbeat...')); try { const bridgeResponse = await axios.get(`${apiUrl}/api/v1/agent/bridge`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` } }); if (bridgeResponse.data.success) { const tasks = bridgeResponse.data.tasks; const pending = tasks.filter((t: any) => t.status === 'PENDING'); const approved = tasks.filter((t: any) => t.status === 'APPROVED'); if (pending.length > 0 || approved.length > 0) { console.log(chalk.yellow(`⚠ Bridge Alert: ${pending.length} pending, ${approved.length} approved tasks found.`)); console.log(chalk.dim('Run "rigstate fix" to process these tasks or ensure your IDE MCP server is active.')); } else { console.log(chalk.green('āœ” Heartbeat healthy. No pending bridge tasks.')); } // Acknowledge Pings if any const pings = pending.filter((t: any) => t.proposal?.startsWith('ping')); for (const ping of pings) { await axios.post(`${apiUrl}/api/v1/agent/bridge`, { bridge_id: ping.id, status: 'COMPLETED', summary: 'Pong! CLI Sync Heartbeat confirmed.' }, { headers: { Authorization: `Bearer ${apiKey}` } }); console.log(chalk.cyan(`šŸ“ Pong! Acknowledged heartbeat signal [${ping.id}]`)); } } } catch (e: any) { console.log(chalk.yellow(`⚠ Could not verify Bridge status: ${e.message}`)); } if (options.project) { console.log(chalk.blue(`Project context saved. Future commands will use this project.`)); } // 9. Migration Guard (The Firewall) try { const migrationDir = path.join(process.cwd(), 'supabase', 'migrations'); const files = await fs.readdir(migrationDir); const sqlFiles = files.filter(f => f.endsWith('.sql')).sort(); if (sqlFiles.length > 0) { const latestMigration = sqlFiles[sqlFiles.length - 1]; console.log(chalk.dim(`\nšŸ›” Migration Guard:`)); console.log(chalk.dim(` Latest Local: ${latestMigration}`)); console.log(chalk.yellow(` ⚠ Ensure DB schema matches this version. CLI cannot verify Remote RLS policies directly.`)); } } catch (e) { // No migrations folder, or error reading - ignore } // 10. Sovereign Foundation (Vault Sync) try { const vaultResponse = await axios.post(`${apiUrl}/api/v1/vault/sync`, { project_id: projectId }, { headers: { Authorization: `Bearer ${apiKey}` } } ); if (vaultResponse.data.success) { const vaultContent: string = vaultResponse.data.data.content || ''; const localEnvPath = path.join(process.cwd(), '.env.local'); let localContent = ''; try { localContent = await fs.readFile(localEnvPath, 'utf-8'); } catch (e) { /* File doesn't exist */ } // Normalize for comparison (trim, ignore comments?) - Simple trim for now if (vaultContent.trim() !== localContent.trim()) { console.log(chalk.bold('\nšŸ” Sovereign Foundation (Vault):')); console.log(chalk.yellow(' Status: Drift Detected / Update Available')); const { syncVault } = await import('inquirer').then(m => m.default.prompt([{ type: 'confirm', name: 'syncVault', message: 'Synchronize local .env.local with Vault secrets?', default: false }])); if (syncVault) { await fs.writeFile(localEnvPath, vaultContent, 'utf-8'); console.log(chalk.green(' āœ… .env.local synchronized with Vault.')); } else { console.log(chalk.dim(' Skipped vault sync.')); } } else { console.log(chalk.dim('\nšŸ” Sovereign Foundation: Synced.')); } } } catch (e: any) { // Fail silently or warn if vault access denied (expected for some users) // console.log(chalk.dim(` (Vault check skipped: ${e.message})`)); } // 11. System Integrity Checks (The Firewall) console.log(chalk.dim('\nšŸ›”ļø System Integrity Check...')); await checkSystemIntegrity(apiUrl, apiKey, projectId); } catch (error: any) { if (axios.isAxiosError(error)) { const message = error.response?.data?.error || error.message; spinner.fail(chalk.red(`Sync failed: ${message}`)); } else { spinner.fail(chalk.red('Sync failed: ' + (error.message || 'Unknown error'))); } } }); return sync; } /** * System Integrity Checks * Verifies Migration Sync and RLS Status via API */ async function checkSystemIntegrity(apiUrl: string, apiKey: string, projectId: string) { try { // Call System Integrity API const response = await axios.get(`${apiUrl}/api/v1/system/integrity`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` } }); if (response.data.success) { const { migrations, rls, guardian_violations } = response.data.data; // Migration Status if (migrations) { if (migrations.in_sync) { console.log(chalk.green(` āœ… Migrations synced (${migrations.count} versions)`)); } else { console.log(chalk.red(` šŸ›‘ CRITICAL: DB Schema out of sync! ${migrations.missing?.length || 0} migrations not applied.`)); if (migrations.missing?.length > 0) { console.log(chalk.dim(` Missing: ${migrations.missing.slice(0, 3).join(', ')}${migrations.missing.length > 3 ? '...' : ''}`)); } console.log(chalk.yellow(` Run 'supabase db push' or apply migrations immediately.`)); } } // RLS Status if (rls) { if (rls.all_secured) { console.log(chalk.green(` āœ… RLS Audit Passed (${rls.table_count} tables secured)`)); } else { console.log(chalk.red(` šŸ›‘ CRITICAL: Security Vulnerability! ${rls.unsecured?.length || 0} tables have RLS disabled.`)); rls.unsecured?.forEach((table: string) => { console.log(chalk.red(` - ${table}`)); }); console.log(chalk.yellow(' Enable RLS immediately: ALTER TABLE "table" ENABLE ROW LEVEL SECURITY;')); } } // Guardian Violations if (guardian_violations) { if (guardian_violations.count === 0) { console.log(chalk.green(' āœ… Guardian: No active violations')); } else { console.log(chalk.yellow(` āš ļø Guardian: ${guardian_violations.count} active violations`)); console.log(chalk.dim(' Run "rigstate check" for details.')); } } } } catch (e: any) { // API might not have this endpoint yet - fail silently console.log(chalk.dim(' (System integrity check skipped - API endpoint not available)')); } }