import { Command } from 'commander'; import chalk from 'chalk'; import fs from 'fs/promises'; import path from 'path'; import ora from 'ora'; import { execSync } from 'child_process'; import { loadManifest } from '../utils/manifest.js'; import { getApiUrl, getApiKey, setProjectId } from '../utils/config.js'; import axios from 'axios'; export function createInitCommand() { return new Command('init') .description('Initialize or link a Rigstate project (interactive mode available)') .argument('[project-id]', 'ID of the project to link (optional, prompts if not provided)') .option('-f, --force', 'Overwrite existing .cursorrules file') .option('--rules-only', 'Only regenerate .cursorrules without interactive setup') .action(async (projectIdArg, options) => { const spinner = ora('Initializing Rigstate project...').start(); let apiKey: string; try { apiKey = getApiKey(); } catch (e) { spinner.fail(chalk.red('Not authenticated. Run "rigstate login" first.')); return; } const apiUrl = getApiUrl(); let projectId = projectIdArg; try { // If --rules-only, just regenerate rules from existing manifest if (options.rulesOnly) { const manifest = await loadManifest(); if (!manifest) { spinner.fail('No .rigstate manifest found. Run "rigstate init" first.'); return; } projectId = manifest.project_id; await generateRules(apiUrl, apiKey, projectId, options.force, spinner); return; } // Interactive mode if no project ID provided if (!projectId) { spinner.stop(); // Dynamic import for inquirer const inquirer = (await import('inquirer')).default; spinner.start('Fetching your projects...'); // Fetch projects via API let projects: any[] = []; try { const projectsResponse = await axios.get(`${apiUrl}/api/v1/projects`, { headers: { Authorization: `Bearer ${apiKey}` } }); if (projectsResponse.data.success) { projects = projectsResponse.data.data.projects || []; } } catch (e: any) { // API might not exist yet - fallback to manual entry spinner.info('Projects API not available. Using manual entry mode.'); } spinner.stop(); if (projects.length === 0) { // Fallback: Ask for project ID manually const { manualProjectId } = await inquirer.prompt([ { type: 'input', name: 'manualProjectId', message: 'Enter Project ID (from Rigstate dashboard):', validate: (input: string) => input.trim() ? true : 'Project ID is required' } ]); projectId = manualProjectId; } else { // Build choices from fetched projects const choices: any[] = [ { name: '➕ Create New Project', value: 'NEW' }, new inquirer.Separator() ]; projects.forEach((p: any) => { choices.push({ name: `[${p.organization_name || 'Personal'}] ${p.name} (${p.status})`, value: p.id }); }); const { selectedId } = await inquirer.prompt([ { type: 'list', name: 'selectedId', message: 'Select a project to link:', choices: choices, pageSize: 15 } ]); if (selectedId === 'NEW') { // Create new project flow const { newName } = await inquirer.prompt([ { type: 'input', name: 'newName', message: 'Enter project name:', validate: (input: string) => input.trim() ? true : 'Name is required' } ]); // Fetch organizations (with fallback) spinner.start('Fetching organizations...'); let orgs: any[] = []; try { const orgsResponse = await axios.get(`${apiUrl}/api/v1/organizations`, { headers: { Authorization: `Bearer ${apiKey}` } }); orgs = orgsResponse.data.data?.organizations || []; } catch (e) { // API might not exist - skip org selection } spinner.stop(); let selectedOrgId = orgs[0]?.id; if (orgs.length > 1) { const { orgId } = await inquirer.prompt([ { type: 'list', name: 'orgId', message: 'Which organization does this belong to?', choices: orgs.map((org: any) => ({ name: `${org.name} (${org.role || 'member'})`, value: org.id })) } ]); selectedOrgId = orgId; } if (!selectedOrgId) { console.log(chalk.yellow('No organization available. Please create the project via the Rigstate dashboard.')); return; } // Create project via API spinner.start('Creating new project...'); try { const createResponse = await axios.post(`${apiUrl}/api/v1/projects`, { name: newName, organization_id: selectedOrgId }, { headers: { Authorization: `Bearer ${apiKey}` } }); if (!createResponse.data.success) { spinner.fail(chalk.red('Failed to create project: ' + createResponse.data.error)); return; } projectId = createResponse.data.data.project.id; spinner.succeed(chalk.green(`Created new project: ${newName}`)); } catch (e: any) { spinner.fail(chalk.red('Project creation API not available. Please create via dashboard.')); return; } } else { projectId = selectedId; } } spinner.start(`Linking to project ID: ${projectId}...`); } // Core link logic // Save project ID to config setProjectId(projectId); // Write local manifest const { saveManifest } = await import('../utils/manifest.js'); await saveManifest({ project_id: projectId, linked_at: new Date().toISOString(), api_url: apiUrl }); // Initialize git if needed try { await fs.access('.git'); } catch { spinner.text = 'Initializing git repository...'; execSync('git init', { stdio: 'ignore' }); } spinner.succeed(chalk.green(`✅ Linked to project: ${projectId}`)); // Generate rules await generateRules(apiUrl, apiKey, projectId, options.force, spinner); console.log(''); console.log(chalk.blue('Next steps:')); console.log(chalk.dim(' rigstate sync - Sync roadmap and context')); console.log(chalk.dim(' rigstate watch - Start development loop')); console.log(chalk.dim(' rigstate focus - Get current task')); } catch (e: any) { spinner.fail(chalk.red('Initialization failed: ' + e.message)); } }); } async function generateRules(apiUrl: string, apiKey: string, projectId: string, force: boolean, spinner: any) { spinner.start('Generating AI rules (MDC + AGENTS.md)...'); try { const response = await axios.post(`${apiUrl}/api/v1/rules/generate`, { project_id: projectId }, { headers: { Authorization: `Bearer ${apiKey}` } }); if (response.data.success || response.data.files) { const files = response.data.files || []; if (files.length === 0 && response.data.rules) { // Fallback to legacy mono-file if no multiple files returned const rulesPath = path.join(process.cwd(), '.cursorrules'); await fs.writeFile(rulesPath, response.data.rules, 'utf-8'); spinner.succeed(chalk.green('✔ Generated .cursorrules (legacy mode)')); return; } for (const file of files) { const targetPath = path.join(process.cwd(), file.path); const targetDir = path.dirname(targetPath); // Ensure directory exists await fs.mkdir(targetDir, { recursive: true }); // Check if exists and force try { await fs.access(targetPath); if (!force && !file.path.startsWith('.cursor/rules/')) { console.log(chalk.dim(` ${file.path} already exists. Skipping.`)); continue; } } catch { // File doesn't exist, proceed } await fs.writeFile(targetPath, file.content, 'utf-8'); } // Cleanup legacy .cursorrules if we have new files and aren't inhibited if (files.length > 0) { const legacyPath = path.join(process.cwd(), '.cursorrules'); try { const stats = await fs.stat(legacyPath); if (stats.isFile()) { await fs.rename(legacyPath, `${legacyPath}.bak`); console.log(chalk.dim(' Moved legacy .cursorrules to .cursorrules.bak')); } } catch (e) { // Ignore if legacy file doesn't exist } } spinner.succeed(chalk.green(`✔ Generated ${files.length} rule files (v${response.data.version || '3.0'})`)); } else { spinner.info(chalk.dim(' Rules generation skipped (API response invalid)')); } } catch (e: any) { spinner.info(chalk.dim(` Rules generation failed: ${e.message}`)); } }