@every-env/cli
Version:
Multi-agent orchestrator for AI-powered development workflows
204 lines • 9.51 kB
JavaScript
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