UNPKG

@every-env/cli

Version:

Multi-agent orchestrator for AI-powered development workflows

204 lines 9.51 kB
import React from "react"; import { render } from "ink"; import { Command } from "commander"; import { detectProjectType, getProjectTypeDisplayName } from "../utils/project-detect.js"; import { readRuntimeConfigIfExists, writeRuntimeConfigAtomic, deepMergeRuntimeConfig, validateRuntimeConfig, validateEnvironmentNonBlocking, getDefaultRuntimeConfigPath, } from "../utils/runtime-config.js"; import { AVAILABLE_AGENTS, DEFAULT_RUNTIME_CONFIG } from "../types/config.js"; import { InitPrompts } from "../interactive/init/InitPrompts.js"; import { copyClaudeFiles } from "../utils/copy-claude-files.js"; import chalk from "chalk"; import { existsSync, rmSync } from "fs"; import { dirname } from "path"; import readline from "readline"; export function createInitCommand() { return new Command("init") .description("Initialize a new every-env project") .option("--force", "Overwrite existing configuration") .option("--yes", "Use default values without prompting") .option("--default-agent <agent>", "Set default agent (claude, amp, codex, copy-clipboard)") .option("--project-type <type>", "Override detected project type") .option("--enable-agent <agent>", "Enable additional agents", (value, previous) => [ ...(previous || []), value, ]) .option("--cli <cli>", "Set CLI command for agents") .action(async (options) => { await initCommand(options); }); } async function promptForConfirmation(message) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(`${message} (y/N) `, (answer) => { rl.close(); resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); }); }); } export async function initCommand(options) { const configPath = getDefaultRuntimeConfigPath(); const everyEnvDir = dirname(configPath); // Get .every-env directory path const dirExists = existsSync(everyEnvDir); // Check if .every-env directory exists and not forcing if (dirExists && !options.force) { if (options.yes) { // In non-interactive mode with --yes, skip the prompt and proceed console.log(chalk.yellow("⚠ .every-env directory exists. Proceeding with --yes flag...")); } else if (process.stdout.isTTY) { // Interactive mode - prompt for confirmation console.log(chalk.yellow("\n⚠ Warning: .every-env directory already exists!")); console.log(chalk.yellow("Running 'every init' will delete everything in .every-env and start fresh.")); console.log(chalk.yellow("This includes all configuration, state, and migration files.\n")); const confirmed = await promptForConfirmation("Are you sure you want to continue?"); if (!confirmed) { console.log(chalk.gray("\nInitialization cancelled.")); return; } // Delete the directory console.log(chalk.gray("\nDeleting existing .every-env directory...")); rmSync(everyEnvDir, { recursive: true, force: true }); } else { // Non-interactive mode without --yes console.log(chalk.red("Error: .every-env directory already exists.")); console.log(chalk.gray("Use --force or --yes to overwrite, or run interactively.")); return; } } else if (dirExists && options.force) { // Force flag is set, delete without asking console.log(chalk.gray("Deleting existing .every-env directory (--force)...")); rmSync(everyEnvDir, { recursive: true, force: true }); } const existingConfig = await readRuntimeConfigIfExists(configPath); // Detect project type const detectedProject = await detectProjectType(); if (options.yes || !process.stdout.isTTY) { // Non-interactive mode await handleNonInteractiveInit(options, detectedProject, existingConfig, configPath); } else { // Interactive mode await handleInteractiveInit(detectedProject, existingConfig, configPath); } } async function handleNonInteractiveInit(options, detectedProject, existingConfig, configPath) { try { // Build config from options and defaults const defaultAgent = options.defaultAgent || "claude"; const projectType = options.projectType || detectedProject.projectType; const enabledAgents = options.enableAgent || []; // Build agents config with only runtime-relevant fields (non-destructive) const agents = {}; const toPersistedAgent = (agentName) => { const meta = AVAILABLE_AGENTS[agentName]; const out = {}; if (meta?.defaultCli || options.cli) out.cli = options.cli ?? meta?.defaultCli; // Get default args from the runtime config defaults const defaultArgs = DEFAULT_RUNTIME_CONFIG.agents?.[agentName]?.args || []; out.args = defaultArgs; return out; }; // Set default agent agents[defaultAgent] = toPersistedAgent(defaultAgent); // Set additional enabled agents (do not touch unspecified agents) for (const agentName of enabledAgents) { if (agentName !== defaultAgent && AVAILABLE_AGENTS[agentName]) { agents[agentName] = toPersistedAgent(agentName); } } const newConfig = { projectType, defaultAgent, agents, signals: detectedProject.signals, }; // Merge with existing config const finalConfig = deepMergeRuntimeConfig(existingConfig, newConfig); const validatedConfig = validateRuntimeConfig(finalConfig); // Write config await writeRuntimeConfigAtomic(configPath, validatedConfig); // Validate environment and add warnings const warnings = await validateEnvironmentNonBlocking(validatedConfig); if (warnings.length > 0) { const configWithWarnings = { ...validatedConfig, validationWarnings: warnings }; await writeRuntimeConfigAtomic(configPath, configWithWarnings); } // Copy .claude files (commands, agents, etc.) try { await copyClaudeFiles({ force: options.force || false, silent: false }); } catch (error) { console.log(chalk.yellow(`\n⚠ Could not copy .claude files: ${error instanceof Error ? error.message : String(error)}`)); console.log(chalk.gray('You can copy them later with: every copy-commands')); } // Print summary console.log("\n✓ Configuration saved successfully!"); console.log(`Project Type: ${getProjectTypeDisplayName(projectType)}`); console.log(`Default Agent: ${AVAILABLE_AGENTS[defaultAgent].displayName}`); if (enabledAgents.length > 0) { console.log(`Additional Agents: ${enabledAgents.map((name) => AVAILABLE_AGENTS[name]?.displayName || name).join(", ")}`); } if (warnings.length > 0) { console.log("\nWarnings:"); warnings.forEach((warning) => console.log(` - ${warning}`)); } } catch (error) { console.error("Failed to initialize configuration:", error instanceof Error ? error.message : String(error)); process.exit(1); } } async function handleInteractiveInit(detectedProject, existingConfig, configPath) { let completed = false; const handleComplete = async (config) => { try { // Merge with existing config const finalConfig = deepMergeRuntimeConfig(existingConfig, config); const validatedConfig = validateRuntimeConfig(finalConfig); // Write config await writeRuntimeConfigAtomic(configPath, validatedConfig); // Validate environment and add warnings const warnings = await validateEnvironmentNonBlocking(validatedConfig); if (warnings.length > 0) { const configWithWarnings = { ...validatedConfig, validationWarnings: warnings }; await writeRuntimeConfigAtomic(configPath, configWithWarnings); } // Copy .claude files (commands, agents, etc.) try { await copyClaudeFiles({ force: existingConfig !== null, silent: false }); } catch (error) { console.log(chalk.yellow(`\n⚠ Could not copy .claude files: ${error instanceof Error ? error.message : String(error)}`)); console.log(chalk.gray('You can copy them later with: every copy-commands')); } completed = true; } catch (error) { console.error("Failed to save configuration:", error instanceof Error ? error.message : String(error)); process.exit(1); } }; const handleCancel = () => { console.log("Configuration cancelled."); process.exit(0); }; const { waitUntilExit } = render(React.createElement(InitPrompts, { detectedProject, existingConfig, onComplete: (config) => { void handleComplete(config); }, onCancel: handleCancel, })); await waitUntilExit(); if (!completed) { process.exit(0); } } //# sourceMappingURL=init.js.map