/* eslint-disable no-console */ import { Command } from 'commander'; import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import os from 'os'; import { getApiUrl, getApiKey } from '../utils/config.js'; export function createLinkCommand() { return new Command('link') .description('Link current directory to a Rigstate project') .argument('[projectId]', 'Project ID to link') .action(async (projectId) => { // Check Global Override first try { const globalPath = path.join(os.homedir(), '.rigstate', 'config.json'); const globalData = await fs.readFile(globalPath, 'utf-8').catch(() => null); if (globalData) { const config = JSON.parse(globalData); const cwd = process.cwd(); if (config.overrides && config.overrides[cwd]) { const overrideId = config.overrides[cwd]; console.warn(chalk.yellow(`Global override detected. Enforcing project ID: ${overrideId}`)); if (!projectId) projectId = overrideId; else if (projectId !== overrideId) { console.warn(chalk.red(`Ignoring provided ID ${projectId}. Using override.`)); projectId = overrideId; } } } } catch (e) { } // Interactive Selection if no ID if (!projectId) { try { const inquirer = (await import('inquirer')).default; const { getApiKey: _getApiKey, getApiUrl: _getApiUrl } = await import('../utils/config.js'); const apiKey = getApiKey(); const apiUrl = getApiUrl(); if (!apiKey) { console.error(chalk.red('Not authenticated. Please run "rigstate login" or provide a Project ID.')); process.exit(1); } console.log(chalk.dim('Fetching your projects...')); const axios = (await import('axios')).default; const response = await axios.get(`${apiUrl}/api/v1/projects`, { headers: { Authorization: `Bearer ${apiKey}` } }); if (!response.data.success || !response.data.data.projects?.length) { console.error(chalk.yellow('No projects found. Create one at https://app.rigstate.com')); process.exit(1); } const choices = response.data.data.projects.map((p: any) => ({ name: `${p.name} (${p.id})`, value: p.id })); const answer = await inquirer.prompt([{ type: 'list', name: 'id', message: 'Select project to link:', choices }]); projectId = answer.id; } catch (e: any) { console.error(chalk.red(`Failed to fetch projects: ${e.message}`)); console.error('Please provide project ID manually: rigstate link '); process.exit(1); } } const cwd = process.cwd(); try { const { saveManifest } = await import('../utils/manifest.js'); const targetFile = await saveManifest({ project_id: projectId, linked_at: new Date().toISOString(), api_url: getApiUrl() !== 'https://app.rigstate.com' ? getApiUrl() : undefined }); console.log(chalk.green(`✔ Linked to project ID: ${projectId}`)); console.log(chalk.dim(`Created local identity manifest at ${path.relative(cwd, targetFile)}`)); // === SMART AUTOMATION === console.log(''); console.log(chalk.bold('🤖 Rigstate Automation Detected')); console.log(''); const apiKey = getApiKey(); const apiUrl = getApiUrl(); if (apiKey) { // 1. Env Sync console.log(chalk.blue('🔐 Checking Vault for secrets...')); const { syncEnv } = await import('./env.js'); await syncEnv(projectId, apiKey, apiUrl, true); // 2. Rules Sync console.log(chalk.blue('🧠 Syncing neural instructions...')); const { syncProjectRules } = await import('./sync-rules.js'); await syncProjectRules(projectId, apiKey, apiUrl); // 3. Git Hooks (Auto-Vaccine) console.log(chalk.blue('🛡️ Injecting Guardian hooks & Safety nets...')); await installHooks(process.cwd()); await hardenGitIgnore(process.cwd()); // 4. Genesis Protocol (Smart Detection) console.log(chalk.blue('🏗️ Checking Genesis status...')); const { checkGenesisStatus, triggerGenesis } = await import('./genesis.js'); const genesisStatus = await checkGenesisStatus(projectId, apiKey, apiUrl); if (!genesisStatus.complete) { // Try to trigger automatically — will fail gracefully if spec not ready const triggered = await triggerGenesis(projectId, apiKey, apiUrl, false); if (!triggered) { console.log(chalk.dim(' 💡 Run "rigstate genesis" after completing onboarding with Frank.')); } } else { console.log(chalk.green(` ✔ Genesis complete (${genesisStatus.stepCount} foundation steps ready)`)); } console.log(''); console.log(chalk.bold.green('🚀 Link Complete! Your environment is ready.')); // 5. Tactical Suggestion const { suggestNextMove } = await import('./suggest.js'); await suggestNextMove(projectId, apiKey, apiUrl); } else { console.log(''); console.log(chalk.bold.green('🚀 Link Complete!')); } } catch (error: any) { if (error.message?.includes('Not authenticated')) { console.warn(chalk.yellow('⚠️ Not authenticated. Run "rigstate login" to enable automation features.')); } else { console.error(chalk.red(`Failed to link project: ${error.message}`)); } } }); } async function hardenGitIgnore(cwd: string) { const fs = await import('fs/promises'); const path = await import('path'); const ignorePath = path.join(cwd, '.gitignore'); const REQUIRED_IGNORES = [ '# Rigstate - Runtime Artifacts (Do not commit)', '.rigstate/ACTIVE_VIOLATIONS.md', '.rigstate/CURRENT_CONTEXT.md', '.rigstate/daemon.pid', '.rigstate/daemon.state.json', '.rigstate/*.log', '.rigstate/*.bak', '# Keep identity tracked', '!.rigstate/identity.json' ]; try { let content = ''; try { content = await fs.readFile(ignorePath, 'utf-8'); } catch { // No .gitignore, start fresh content = ''; } const missing = REQUIRED_IGNORES.filter(line => !content.includes(line) && !line.startsWith('#')); if (missing.length > 0) { console.log(chalk.dim(' Configuring .gitignore for Rigstate safety...')); const toAppend = '\n\n' + REQUIRED_IGNORES.join('\n') + '\n'; await fs.writeFile(ignorePath, content + toAppend, 'utf-8'); console.log(chalk.green(' ✔ .gitignore updated (Artifacts protected)')); } else { console.log(chalk.green(' ✔ .gitignore already hardened')); } } catch (e: any) { console.warn(chalk.yellow(` Could not update .gitignore: ${e.message}`)); } } async function installHooks(cwd: string) { const fs = await import('fs/promises'); const path = await import('path'); // Check if git repo try { await fs.access(path.join(cwd, '.git')); } catch { console.log(chalk.dim(' (Not a git repository, skipping hooks)')); return; } const hooksDir = path.join(cwd, '.husky'); // Check if simple husky setup exists or just do a manual pre-commit script // For now, let's look for a basic pre-commit file in .git/hooks if husky isn't there // Actually, let's just use the `hooks` command logic if possible, or a lightweight version try { // Simpler approach: Check if pre-commit exists and install if missing const preCommitPath = path.join(cwd, '.git/hooks/pre-commit'); let shouldInstall = false; try { await fs.access(preCommitPath); const content = await fs.readFile(preCommitPath, 'utf-8'); if (content.includes('rigstate')) { console.log(chalk.green(' ✔ Git hooks already active')); } else { shouldInstall = true; } } catch { shouldInstall = true; } if (shouldInstall) { const PRE_COMMIT_SCRIPT = `#!/bin/sh # Rigstate Guardian Pre-commit Hook # Installed by: rigstate link (v0.7.25) # 1. Silent Sentinel Check if [ -f .rigstate/guardian.lock ]; then echo "🛑 INTERVENTION ACTIVE: Commit blocked by Silent Sentinel." exit 1 fi echo "🛡️ Running Guardian checks..." rigstate check --staged --strict=critical exit $? `; // Ensure hooks dir exists await fs.mkdir(path.dirname(preCommitPath), { recursive: true }); // Write hook if (await fileExists(preCommitPath)) { const existing = await fs.readFile(preCommitPath, 'utf-8'); await fs.writeFile(preCommitPath, existing + '\n\n' + PRE_COMMIT_SCRIPT.replace('#!/bin/sh\n', ''), { mode: 0o755 }); } else { await fs.writeFile(preCommitPath, PRE_COMMIT_SCRIPT, { mode: 0o755 }); } console.log(chalk.green(' ✔ Applied Guardian protection (git-hooks)')); } } catch (e: any) { // Ignore hook errors during link console.log(chalk.dim(' (Skipped hooks: ' + e.message + ')')); } } async function fileExists(path: string) { const fs = await import('fs/promises'); try { await fs.access(path); return true; } catch { return false; } }