arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
1,055 lines ⢠43.5 kB
JavaScript
import { pathToFileURL } from "node:url";
import path from "node:path";
import { Command } from "commander";
import pc from "picocolors";
import { discoverAgents } from "./agents/discovery.js";
import { orchestrate } from "./agents/orchestrate.js";
import { getAgentCapabilities } from "./agents/dispatch.js";
import { showStatus } from "./agents/status.js";
import { initProject } from "./persona/loader.js";
import { registerMemoryCommands } from "./memory/cli.js";
import { detectBreakingChanges } from "./version/drift-detector.js";
import { createSliceVersion } from "./version/version-creator.js";
const program = new Command()
.name("arela")
.description("AI-powered CTO with multi-agent orchestration")
.version("3.10.0");
/**
* arela agents - List discovered agents
*/
program
.command("agents")
.description("Discover and list available AI agents")
.option("--verbose", "Show detailed information", false)
.action(async (opts) => {
console.log(pc.bold(pc.cyan("\nš¤ Meeting your AI agents...\n")));
const agents = await discoverAgents();
const capabilities = getAgentCapabilities();
if (agents.length === 0) {
console.log(pc.yellow("š“ No agents discovered. Let's fix that!"));
console.log(pc.gray("\nYou'll need at least one of these awesome tools:"));
console.log(pc.gray(" - š§ Codex CLI (npm install -g @openai/codex)"));
console.log(pc.gray(" - šØ Claude CLI (npm install -g @anthropic-ai/claude)"));
console.log(pc.gray(" - š® Ollama (https://ollama.ai)"));
console.log("");
return;
}
console.log(pc.bold(`Your AI team is assembled! (${agents.length} agents ready)\n`));
for (const agent of agents) {
const icon = agent.available ? "ā
" : "ā";
const status = agent.available ? pc.green("Ready to rock!") : pc.red("Taking a nap");
console.log(`${icon} ${pc.bold(agent.name)} - ${status}`);
if (opts.verbose) {
console.log(pc.gray(` Command: ${agent.command}`));
console.log(pc.gray(` Type: ${agent.type}`));
if (agent.version) {
console.log(pc.gray(` Version: ${agent.version}`));
}
// Show capabilities if we have them
const agentKey = agent.command;
const capability = capabilities[agentKey];
if (capability) {
console.log(pc.gray(` š° Cost: $${capability.costPer1kTokens}/1k tokens`));
console.log(pc.gray(` šÆ Best for: ${capability.bestFor.join(", ")}`));
}
console.log("");
}
}
if (!opts.verbose) {
console.log(pc.gray("\nš Run with --verbose to see their superpowers\n"));
}
});
/**
* arela init - Initialize project
*/
program
.command("init")
.description("Initialize Arela in current project")
.option("--cwd <dir>", "Directory to initialize", process.cwd())
.option("--preset <type>", "Rule preset: startup, enterprise, solo, all", "startup")
.option("--force", "Overwrite existing files", false)
.action(async (opts) => {
console.log(pc.bold(pc.cyan("\nšÆ Arela v3.3.0 - Your AI CTO is here to help!\n")));
console.log(pc.gray("Let's get you set up. What kind of team are you?\n"));
// Validate preset
const validPresets = ["startup", "enterprise", "solo", "all"];
if (!validPresets.includes(opts.preset)) {
console.error(pc.red(`šµāš« Invalid preset: ${opts.preset}`));
console.error(pc.gray(`Valid presets: ${validPresets.join(", ")}\n`));
process.exit(1);
}
// Show preset info
const presetDescriptions = {
startup: "š Startup - Move fast, break things (11 rules)",
enterprise: "š¢ Enterprise - Quality & compliance (23 rules)",
solo: "𦸠Solo Dev - Just the essentials (9 rules)",
all: "šÆ The Whole Enchilada - Give me everything! (29 rules)",
};
console.log(pc.gray(`> ${opts.preset} - ${presetDescriptions[opts.preset]}\n`));
console.log(pc.bold(pc.cyan(`š ${opts.preset.charAt(0).toUpperCase() + opts.preset.slice(1)} mode activated! Fast-tracking your setup...\n`)));
try {
const result = await initProject({
cwd: opts.cwd,
preset: opts.preset,
force: opts.force,
});
console.log(pc.bold(pc.green("\n⨠Boom! Your AI CTO is ready:\n")));
console.log(pc.bold("Created:"));
for (const file of result.created) {
console.log(pc.gray(` - ${file}`));
}
if (result.skipped.length > 0) {
console.log(pc.yellow("\nā ļø Skipped (already exists):"));
for (const file of result.skipped) {
console.log(pc.gray(` - ${file}`));
}
console.log(pc.gray("\nš” Use --force to overwrite"));
}
console.log(pc.bold(pc.cyan("\nš Next: Run `arela agents` to meet your AI team\n")));
}
catch (error) {
console.error(pc.red(`\nšµāš« Initialization went sideways: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela orchestrate - Run tickets
*/
program
.command("orchestrate")
.description("Run tickets with multi-agent orchestration")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--parallel", "Run tickets in parallel", false)
.option("--max-parallel <n>", "Max parallel tickets", "5")
.option("--agent <name>", "Run only tickets for specific agent")
.option("--tickets <list>", "Comma-separated list of ticket IDs (e.g., CODEX-001,CODEX-002)")
.option("--force", "Re-run completed tickets", false)
.option("--dry-run", "Show what would run without executing", false)
.action(async (opts) => {
try {
await orchestrate({
cwd: opts.cwd,
parallel: opts.parallel,
maxParallel: parseInt(opts.maxParallel, 10),
agent: opts.agent,
tickets: opts.tickets ? opts.tickets.split(',').map((t) => t.trim()) : undefined,
force: opts.force,
dryRun: opts.dryRun,
});
}
catch (error) {
console.error(pc.red(`\nšµāš« Orchestration hit a snag: ${error.message}\n`));
process.exit(1);
}
});
export class UnsupportedPlatformError extends Error {
platform;
constructor(platform) {
super(`Platform "${platform}" not supported.`);
this.platform = platform;
this.name = "UnsupportedPlatformError";
}
}
export async function handleRunCommand(platform, opts) {
if (platform === "web") {
try {
const { runWebApp } = await import("./run/web.js");
// Validate AI Pilot options
if (opts.aiPilot && !opts.goal) {
console.error(pc.red('\nšµāš« --ai-pilot requires --goal to be specified\n'));
process.exit(1);
}
await runWebApp({
url: opts.url ?? "http://localhost:3000",
flow: opts.flow ?? "default",
headless: Boolean(opts.headless),
record: Boolean(opts.record),
analyze: Boolean(opts.analyze),
aiPilot: Boolean(opts.aiPilot),
goal: opts.goal,
});
}
catch (error) {
console.error(pc.red(`\nšµāš« Web runner hit a snag: ${error.message}\n`));
throw error;
}
return;
}
if (platform === "mobile") {
try {
const { runMobileApp } = await import("./run/mobile.js");
await runMobileApp({
platform: (opts.platform ?? "ios"),
device: opts.device,
flow: opts.flow ?? "default",
app: opts.app,
webFallback: opts.webFallback,
});
}
catch (error) {
console.error(pc.red(`\nšµāš« Mobile runner hit a snag: ${error.message}\n`));
throw error;
}
return;
}
throw new UnsupportedPlatformError(platform);
}
export function buildRunCommand(programInstance, handler = handleRunCommand) {
return programInstance
.command("run")
.description("Run and test your app like a real user")
.argument("<platform>", "Platform: web or mobile")
.option("--url <url>", "URL for web apps", "http://localhost:3000")
.option("--platform <platform>", "Mobile platform: ios or android", "ios")
.option("--device <name>", "Device name (e.g., 'iPhone 15 Pro')")
.option("--app <path>", "Path to .app or .apk file (auto-detects Expo)")
.option("--flow <name>", "User flow to test", "default")
.option("--headless", "Run browser in headless mode (web only)", false)
.option("--record", "Record video of test execution", false)
.option("--analyze", "Run AI-powered analysis on screenshots (web only)", false)
.option("--ai-pilot", "Let AI figure out how to achieve goal (web only)", false)
.option("--goal <goal>", "Goal for AI Pilot to achieve (requires --ai-pilot)")
.option("--web-fallback", "Force web fallback mode with mobile viewport (mobile only)", false)
.addHelpText("after", "\nExamples:\n" +
" $ arela run web\n" +
" $ arela run web --url http://localhost:8080\n" +
" $ arela run web --flow signup --headless\n" +
" $ arela run mobile\n" +
" $ arela run mobile --platform android\n" +
" $ arela run mobile --device 'Pixel 7' --flow onboarding\n")
.action(async (platformArg, opts) => {
try {
await handler(platformArg, opts);
}
catch (error) {
if (error instanceof UnsupportedPlatformError) {
console.error(pc.red(error.message));
console.log(pc.gray("Supported platforms: web, mobile"));
process.exit(1);
}
if (error instanceof Error) {
console.error(pc.red(`\nšµāš« Run command hit a snag: ${error.message}\n`));
}
process.exit(1);
}
});
}
/**
* arela run - Execute user flows via platform runners
*/
buildRunCommand(program);
/**
* arela status - Show ticket status
*/
program
.command("status")
.description("Show ticket execution status")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--verbose", "Show detailed status", false)
.action(async (opts) => {
try {
await showStatus({
cwd: opts.cwd,
verbose: opts.verbose,
});
}
catch (error) {
console.error(pc.red(`\nšµāš« Status check went sideways: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela doctor - Validate project structure
*/
program
.command("doctor")
.description("Validate project structure and setup")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--fix", "Auto-fix issues", false)
.action(async (opts) => {
console.log(pc.bold(pc.cyan("\nš„ Arela Doctor - Let's check your pulse!\n")));
try {
const { checkStructure, fixStructure } = await import("./utils/structure-validator.js");
const issues = await checkStructure(opts.cwd);
if (issues.length === 0) {
console.log(pc.green("š Perfect health! Your project structure is awesome!\n"));
return;
}
console.log(pc.bold(pc.yellow("ā ļø Found a few oopsies:\n")));
for (const issue of issues) {
const icon = issue.type === "error" ? "ā" : "ā ļø ";
console.log(`${icon} ${issue.message}`);
if (issue.fix) {
console.log(pc.dim(` š” Quick fix: ${issue.fix}`));
}
if (issue.files && issue.files.length > 0) {
console.log(pc.dim(` š Files affected: ${issue.files.length}`));
}
console.log("");
}
if (opts.fix) {
console.log(pc.cyan("š§ Applying some magic...\n"));
await fixStructure(opts.cwd, issues);
console.log(pc.green("\nā
All fixed! Your project is feeling much better now.\n"));
}
else {
console.log(pc.gray("š” Run with --fix and I'll patch these up for you\n"));
}
}
catch (error) {
console.error(pc.red(`\nšµāš« Doctor needs a coffee: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela index - Build RAG index
*/
program
.command("index")
.description("Build semantic search index for codebase")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--model <name>", "Ollama model to use", "nomic-embed-text")
.option("--host <url>", "Ollama host URL", "http://localhost:11434")
.option("--parallel", "Index files in parallel (faster but more memory)", false)
.action(async (opts) => {
console.log(pc.bold(pc.cyan("\nš Building your RAG brain...\n")));
console.log(pc.gray("š§ I'll automatically set up Ollama and required models if needed...\n"));
try {
const { buildIndex } = await import("./rag/index.js");
const result = await buildIndex({
cwd: opts.cwd,
model: opts.model,
ollamaHost: opts.host,
parallel: opts.parallel,
progress: true, // Enable progress bar
});
console.log(pc.bold(pc.green(`\nš Nailed it! Your RAG index is built and ready\n`)));
console.log(pc.gray(`š Indexed ${result.filesIndexed} files in ${((result.timeMs / 1000).toFixed(1))}s`));
console.log(pc.gray(`š§ Your codebase is now searchable by AI\n`));
console.log(pc.bold(pc.cyan("š Go build something amazing!\n")));
}
catch (error) {
console.error(pc.red(`\nšµāš« Indexing went sideways: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela auto-index - Run incremental indexing (called by git hook)
*/
program
.command("auto-index")
.description("Incrementally update RAG index (called by git hook)")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--silent", "Run silently", true)
.option("--personality <type>", "CLI personality: professional, fun, bold", "professional")
.action(async (opts) => {
try {
const { personalities, getMessage } = await import("./utils/cli-personality.js");
const personality = personalities[opts.personality] || personalities.professional;
const { incrementalIndex } = await import("./utils/auto-index.js");
const result = await incrementalIndex({
cwd: opts.cwd,
silent: opts.silent,
});
if (!result.skipped && !opts.silent) {
const updateMsg = personality.mode === "fun"
? `š Index updated: +${result.added} added, ~${result.updated} updated, -${result.deleted} deleted`
: personality.mode === "bold"
? `Index updated: +${result.added} added, ~${result.updated} updated, -${result.deleted} deleted`
: `ā
Index updated: +${result.added} added, ~${result.updated} updated, -${result.deleted} deleted`;
console.log(pc.green(updateMsg));
}
}
catch (error) {
console.error(pc.red(`ā Auto-indexing failed: ${error.message}`));
process.exit(1);
}
});
/**
* arela install-hook - Install auto-indexing git hook
*/
program
.command("install-hook")
.description("Install post-commit hook for auto-indexing")
.option("--cwd <dir>", "Working directory", process.cwd())
.action(async (opts) => {
console.log(pc.bold(pc.cyan("\nšŖ Installing Auto-Indexing Hook...\n")));
try {
const { installAutoIndexHook } = await import("./utils/auto-index.js");
await installAutoIndexHook(opts.cwd);
console.log(pc.bold(pc.green("\n⨠Hook installed! Your git is now supercharged\n")));
console.log(pc.gray("š¤ Your RAG index will update automatically after each commit."));
console.log(pc.gray("ā” Only changed files are re-indexed (fast & efficient).\n"));
}
catch (error) {
console.error(pc.red(`\nšµāš« Installation went sideways: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela uninstall-hook - Remove auto-indexing git hook
*/
program
.command("uninstall-hook")
.description("Remove post-commit hook for auto-indexing")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--personality <type>", "CLI personality: professional, fun, bold", "professional")
.action(async (opts) => {
const { personalities, getMessage } = await import("./utils/cli-personality.js");
const personality = personalities[opts.personality] || personalities.professional;
const header = personality.mode === "fun"
? "šŖ Removing the auto-indexing magic..."
: personality.mode === "bold"
? "šŖ Uninstalling hook (why would you do this)"
: "šŖ Removing Auto-Indexing Hook";
console.log(pc.bold(pc.cyan(`\n${header}\n`)));
try {
const { uninstallAutoIndexHook } = await import("./utils/auto-index.js");
await uninstallAutoIndexHook(opts.cwd);
const doneMsg = personality.mode === "fun"
? "ā
Hook removed! Auto-indexing disabled"
: personality.mode === "bold"
? "ā
Hook removed. You'll regret this."
: "ā
Hook removed successfully";
console.log(pc.green(`${doneMsg}\n`));
}
catch (error) {
console.error(pc.red(`\nā Removal failed: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela analyze - Analyze code flows and architecture
*/
program
.command("analyze")
.description("Analyze code flows and architecture")
.argument("<type>", "Analysis type: flow, architecture, tests")
.argument("[names...]", "Names or paths to analyze")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--dir <path>", "Directory to analyze")
.option("--verbose", "Show detailed analysis", false)
.option("--json <path>", "Export results to JSON file")
.option("--output <format>", "Output format: text, json", "text")
.addHelpText("after", "\nExamples:\n" +
" $ arela analyze flow\n" +
" $ arela analyze flow main --verbose\n" +
" $ arela analyze architecture\n" +
" $ arela analyze architecture /repo/path1 /repo/path2\n" +
" $ arela analyze architecture --json report.json\n" +
" $ arela analyze tests --dir src --json test-report.json --verbose\n")
.action(async (type, names, opts) => {
if (type === "flow") {
await handleFlowAnalysis(names, opts);
}
else if (type === "architecture") {
await handleArchitectureAnalysis(names, opts);
}
else if (type === "tests") {
await handleTestAnalysis(opts);
}
else {
console.error(pc.red(`\nšµāš« Analysis type "${type}" not supported. Use: flow, architecture, tests\n`));
process.exit(1);
}
});
/**
* Handle flow analysis
*/
async function handleFlowAnalysis(names, opts) {
const name = names[0] || "main";
console.log(pc.bold(pc.cyan("\nš Analyzing Code Flow...\n")));
console.log(pc.gray(`Flow: ${name}`));
console.log(pc.gray(`Directory: ${opts.cwd}\n`));
try {
const { analyzeFlow, generateMarkdownReport } = await import("./flow/analyzer.js");
const { reportAnalysis, reportBriefSummary, exportJSON, exportMarkdown } = await import("./flow/reporter.js");
const result = await analyzeFlow({
cwd: opts.cwd,
flowName: name,
verbose: opts.verbose,
});
if (opts.verbose) {
reportAnalysis(result);
}
else {
reportBriefSummary(result);
}
// Export if requested
if (opts.json) {
exportJSON(result, opts.json);
}
console.log(pc.bold(pc.green("⨠Analysis complete!\n")));
}
catch (error) {
console.error(pc.red(`\nšµāš« Analysis failed: ${error.message}\n`));
if (opts.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
/**
* Handle architecture analysis
*/
async function handleArchitectureAnalysis(names, opts) {
console.log(pc.bold(pc.cyan("\nš Analyzing Architecture...\n")));
// Determine repository paths
let repoPaths = names.length > 0 ? names : [opts.cwd];
console.log(pc.gray(`Repositories: ${repoPaths.join(", ")}\n`));
try {
const { analyzeArchitecture } = await import("./analyze/architecture.js");
const { reportArchitecture, exportArchitectureJson } = await import("./analyze/reporter.js");
const report = await analyzeArchitecture(repoPaths, {
verbose: opts.verbose,
output: opts.output,
});
// Display report
reportArchitecture(report, opts.verbose);
// Export if requested
if (opts.json) {
exportArchitectureJson(report, opts.json);
console.log(pc.gray(`\nš Report exported to ${opts.json}`));
}
console.log(pc.bold(pc.green("\n⨠Architecture analysis complete!\n")));
}
catch (error) {
console.error(pc.red(`\nšµāš« Architecture analysis failed: ${error.message}\n`));
if (opts.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
/**
* Handle test strategy analysis
*/
async function handleTestAnalysis(opts) {
console.log(pc.bold(pc.cyan("\nš§Ŗ Test Strategy Optimizer\n")));
try {
const { analyzeTestStrategy } = await import("./analyze/tests/analyzer.js");
const { reportTestStrategy, exportTestStrategyJson, writeDefaultTestReport, } = await import("./analyze/tests/reporter.js");
const report = await analyzeTestStrategy({
cwd: opts.cwd,
dir: opts.dir,
verbose: opts.verbose,
});
reportTestStrategy(report, opts.verbose);
writeDefaultTestReport(report);
if (opts.json) {
exportTestStrategyJson(report, opts.json);
}
console.log(pc.bold(pc.green("⨠Test analysis complete!\n")));
}
catch (error) {
console.error(pc.red(`\nšµāš« Test analysis failed: ${error.message}\n`));
if (opts.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
/**
* arela ingest - Ingest codebase into graph database
*/
program
.command("ingest <command>")
.description("Ingest and analyze codebase")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--repo <path>", "Repository path (defaults to current directory)")
.option("--refresh", "Refresh existing graph", false)
.option("--analyze", "Run analysis after ingestion", false)
.option("--verbose", "Verbose output", false)
.action(async (command, opts) => {
if (command !== "codebase") {
console.error(pc.red(`\nšµāš« Unknown ingest command: ${command}`));
console.log(pc.gray("Available commands: codebase\n"));
process.exit(1);
}
const repoPath = opts.repo || opts.cwd;
console.log(pc.bold(pc.cyan("\nš§ Arela Codebase Ingestion\n")));
try {
const { ingestCodebase } = await import("./ingest/index.js");
const result = await ingestCodebase(repoPath, {
refresh: opts.refresh,
analyze: opts.analyze,
verbose: opts.verbose,
});
console.log(pc.bold(pc.cyan("\nš Ingestion Complete!\n")));
console.log(pc.gray(`Files scanned: ${result.summary.filesScanned}`));
console.log(pc.gray(`Imports found: ${result.summary.importsFound}`));
console.log(pc.gray(`Functions: ${result.summary.functionsDefined}`));
console.log(pc.gray(`API calls: ${result.summary.apiCallsFound}`));
console.log(pc.gray(`\nGraph stored at: ${result.dbPath}`));
console.log(pc.gray(`Completed in ${(result.duration / 1000).toFixed(2)}s\n`));
}
catch (error) {
console.error(pc.red(`\nšµāš« Ingestion failed: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela mcp - Start MCP server
*/
program
.command("mcp")
.description("Start MCP server for Windsurf integration")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--model <name>", "Ollama model to use", "nomic-embed-text")
.option("--host <url>", "Ollama host URL", "http://localhost:11434")
.option("--top-k <n>", "Number of results to return", "5")
.action(async (opts) => {
console.log(pc.bold(pc.cyan("\nš Starting Arela MCP Server...\n")));
console.log(pc.gray(`š Working directory: ${opts.cwd}`));
console.log(pc.gray(`š§ Model: ${opts.model}`));
console.log(pc.gray(`š Ollama host: ${opts.host}\n`));
try {
const { runArelaMcpServer } = await import("./mcp/server.js");
await runArelaMcpServer({
cwd: opts.cwd,
model: opts.model,
ollamaHost: opts.host,
defaultTopK: parseInt(opts.topK, 10),
});
// Server runs indefinitely
}
catch (error) {
console.error(pc.red(`\nšµāš« MCP server went sideways: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela mcp stats - Show search enforcement statistics
*/
program
.command("mcp-stats")
.description("Show search enforcement statistics from MCP server")
.action(async () => {
console.log(pc.bold(pc.cyan("\nš Search Enforcement Statistics\n")));
try {
const { searchEnforcer } = await import("./mcp/search-enforcer.js");
searchEnforcer.printStats();
const stats = searchEnforcer.getStats();
if (parseInt(stats.complianceRate) < 50) {
console.log(pc.yellow("ā ļø Low compliance rate! Agents are not using arela_search enough.\n"));
}
else if (parseInt(stats.complianceRate) >= 90) {
console.log(pc.green("š Excellent compliance! Agents are using arela_search properly.\n"));
}
}
catch (error) {
console.error(pc.red(`\nā Failed to get stats: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela detect slices - Detect vertical slices
*/
program
.command("detect")
.description("Detect optimal vertical slices in codebase")
.argument("<type>", "Detection type (only 'slices' supported)")
.argument("[repos...]", "Repository paths (optional)")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--json <path>", "Export results to JSON file")
.option("--verbose", "Show detailed analysis", false)
.option("--min-cohesion <n>", "Minimum cohesion percentage (0-100)", "0")
.option("--max-slices <n>", "Maximum slices to detect")
.action(async (type, repos, opts) => {
if (type !== 'slices') {
console.error(pc.red(`\nšµāš« Unknown detect type: ${type}`));
console.log(pc.gray("Available types: slices\n"));
process.exit(1);
}
console.log(pc.bold(pc.cyan("\nš Detecting Optimal Slice Boundaries...\n")));
try {
const { detectSlices } = await import("./detect/index.js");
const { formatReport: formatOutput, printVerboseInfo } = await import("./detect/reporter.js");
const options = {
verbose: opts.verbose,
json: opts.json,
minCohesion: parseInt(opts.minCohesion, 10),
maxSlices: opts.maxSlices ? parseInt(opts.maxSlices, 10) : undefined,
};
// If no repos provided, use cwd as the repo path
// Handle both array and undefined cases
const reposArray = Array.isArray(repos) ? repos : (repos ? [repos] : []);
const repoPaths = reposArray.length > 0 ? reposArray : [opts.cwd];
if (opts.verbose) {
console.log(pc.gray(`Repo paths: ${repoPaths.join(', ')}`));
console.log(pc.gray(`CWD: ${opts.cwd}\n`));
}
const report = await detectSlices(repoPaths, opts.cwd, options);
// Display results
console.log(formatOutput(report));
if (opts.verbose) {
printVerboseInfo(report);
}
console.log(pc.bold(pc.green(`⨠Done! Detected ${report.sliceCount} slices with ${report.totalFiles} files\n`)));
}
catch (error) {
console.error(pc.red(`\nšµāš« Slice detection failed: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela generate - Generate contracts, clients, and other specifications
*/
program
.command("generate")
.description("Generate API contracts, clients, and specifications")
.argument("[type]", "Type of generation (contracts, client)", "contracts")
.argument("[repos...]", "Repository paths (optional)")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--language <lang>", "Client language (typescript, python)", "typescript")
.option("--contract <path>", "Path to OpenAPI contract file")
.option("--contract-dir <path>", "Directory containing OpenAPI contracts")
.option("--base-url <url>", "Base URL for API client")
.option("--output <dir>", "Output directory")
.option("--format <type>", "Output format for contracts: yaml or json", "yaml")
.option("--per-slice", "Generate contracts per vertical slice", true)
.option("--drift-only", "Only show schema drift issues", false)
.option("--dry-run", "Show what would be generated without writing files", false)
.action(async (type, repos, opts) => {
if (type === "contracts") {
await handleContractGeneration(repos, opts);
}
else if (type === "client") {
await handleClientGeneration(opts);
}
else {
console.error(pc.red(`\nšµāš« Unknown generate command: ${type}`));
console.log(pc.gray("Available commands: contracts, client\n"));
process.exit(1);
}
});
async function handleContractGeneration(repos, opts) {
try {
const { generateContracts } = await import("./contracts/index.js");
const repoPaths = repos && repos.length > 0 ? repos : [opts.cwd];
const report = await generateContracts({
repoPaths,
perSlice: opts.perSlice !== false,
format: opts.format,
driftOnly: opts.driftOnly,
outputDir: opts.output || "openapi",
});
// Success - exit with code 0
const driftCount = report.driftIssues.length;
if (driftCount > 0) {
process.exit(0); // Exit with warning but not failure
}
}
catch (error) {
console.error(pc.red(`\nšµāš« Contract generation failed: ${error.message}\n`));
process.exit(1);
}
}
async function handleClientGeneration(opts) {
try {
const { generateClient } = await import("./generate/client/index.js");
await generateClient({
language: opts.language || "typescript",
contract: opts.contract,
contractDir: opts.contractDir,
outputDir: opts.output || "src/api",
baseURL: opts.baseUrl,
dryRun: opts.dryRun,
});
}
catch (error) {
console.error(pc.red(`\nšµāš« Client generation failed: ${error.message}\n`));
if (opts.verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
/**
* arela validate contracts - Validate API against OpenAPI contracts
*/
program
.command("validate contracts")
.description("Validate API implementation against OpenAPI contracts")
.option("--contract <path>", "Specific contract to validate")
.option("--server <url>", "API server URL", "http://localhost:3000")
.option("--start-server <cmd>", "Command to start API server")
.option("--watch", "Watch mode for development")
.option("--cwd <dir>", "Working directory", process.cwd())
.action(async (options) => {
console.log(pc.bold(pc.cyan("\nš Validating API Contracts...\n")));
try {
const { validateContracts } = await import("./validate/contract-validator.js");
const result = await validateContracts({
contractPath: options.contract,
serverUrl: options.server,
startServer: options.startServer,
watch: options.watch,
cwd: options.cwd,
});
if (result.passed) {
console.log(pc.bold(pc.green("ā
All contracts validated successfully!\n")));
console.log(pc.gray(` Total endpoints: ${result.total}`));
console.log(pc.gray(` Passed: ${result.total - result.failures}\n`));
// Show details for each contract
for (const contract of result.contracts) {
const status = contract.passed ? pc.green("ā") : pc.red("ā");
console.log(`${status} ${path.basename(contract.path)}`);
console.log(pc.gray(` Endpoints: ${contract.total}, Passes: ${contract.passes}\n`));
}
process.exit(0);
}
else {
console.log(pc.bold(pc.red("ā Contract validation failed!\n")));
console.log(pc.gray(` Total endpoints: ${result.total}`));
console.log(pc.gray(` Failed: ${result.failures}\n`));
// Show details for each contract
for (const contract of result.contracts) {
const status = contract.passed ? pc.green("ā") : pc.red("ā");
console.log(`${status} ${path.basename(contract.path)}`);
console.log(pc.gray(` Endpoints: ${contract.total}, Failures: ${contract.failures}`));
if (contract.details) {
console.log(pc.gray(` Details: ${contract.details}\n`));
}
}
process.exit(1);
}
}
catch (error) {
console.error(pc.red("\nā Contract validation error:"));
console.error(pc.red(error.message + "\n"));
process.exit(1);
}
});
/**
* arela refactor - Refactoring commands
*/
const refactorCommand = program.command("refactor");
refactorCommand
.command("extract-all-slices")
.description("Extract all detected slices into separate vertical features")
.option("--dry-run", "Preview extraction without making changes", false)
.option("--skip-tests", "Skip test verification", false)
.option("--interactive", "Ask for confirmation before extraction", false)
.option("--min-cohesion <n>", "Minimum cohesion percentage (0-100)", "70")
.option("--cwd <dir>", "Working directory", process.cwd())
.option("--verbose", "Show detailed output", false)
.action(async (options) => {
console.log(pc.bold(pc.cyan("\nš Arela v4.0.0 - Slice Extraction\n")));
try {
const { SliceExtractor } = await import("./refactor/index.js");
const extractor = new SliceExtractor();
const result = await extractor.extractAllSlices({
dryRun: options.dryRun,
skipTests: options.skipTests,
interactive: options.interactive,
minCohesion: parseInt(options.minCohesion, 10),
cwd: options.cwd,
verbose: options.verbose,
});
if (!result.success) {
console.error(pc.red(`\nā Extraction failed!\n`));
for (const error of result.errors) {
console.error(pc.red(` ⢠${error}`));
}
console.log("");
process.exit(1);
}
else {
console.log(pc.bold(pc.green(`\nā
Success!\n`)));
console.log(pc.gray(` Duration: ${(result.duration / 1000).toFixed(2)}s`));
process.exit(0);
}
}
catch (error) {
console.error(pc.red(`\nā Extraction failed: ${error.message}\n`));
process.exit(1);
}
});
refactorCommand
.command("extract-slice <name>")
.description("Extract a specific slice into a vertical feature")
.option("--dry-run", "Preview extraction without making changes", false)
.option("--skip-tests", "Skip test verification", false)
.option("--cwd <dir>", "Working directory", process.cwd())
.action(async (name, options) => {
console.log(pc.bold(pc.cyan(`\nš Extracting Slice: ${name}\n`)));
try {
const { SliceExtractor } = await import("./refactor/index.js");
const { detectSlices } = await import("./detect/index.js");
// Detect slices first
const report = await detectSlices(["."], options.cwd);
// Find the slice matching the name
const slice = report.slices.find(s => s.name.toLowerCase() === name.toLowerCase());
if (!slice) {
console.error(pc.red(`\nā Slice not found: ${name}\n`));
console.log(pc.gray("Available slices:"));
for (const s of report.slices) {
console.log(pc.gray(` ⢠${s.name} (${s.fileCount} files)`));
}
console.log("");
process.exit(1);
}
const extractor = new SliceExtractor();
const result = await extractor.extractAllSlices({
dryRun: options.dryRun,
skipTests: options.skipTests,
cwd: options.cwd,
});
if (!result.success) {
console.error(pc.red(`\nā Extraction failed!\n`));
process.exit(1);
}
console.log(pc.bold(pc.green(`\nā
Slice extracted!\n`)));
}
catch (error) {
console.error(pc.red(`\nā Failed: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela claude - Launch Claude CLI with ticket context
*/
program
.command("claude [ticket]")
.description("Launch Claude CLI (optionally with ticket path)")
.action(async (ticket) => {
const { execa } = await import('execa');
if (ticket) {
// Print the implement command for Claude
console.log(pc.bold(pc.cyan("\nš Ticket ready for Claude:\n")));
console.log(pc.gray(`implement ${ticket}\n`));
console.log(pc.gray("Paste this into Claude when it starts...\n"));
// Wait 2 seconds
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Launch Claude CLI
try {
await execa('claude', [], {
stdio: 'inherit',
cwd: process.cwd(),
});
}
catch (error) {
console.error(pc.red('\nā Failed to launch Claude CLI'));
console.error(pc.gray('Make sure Claude CLI is installed: https://claude.ai/cli\n'));
process.exit(1);
}
});
/**
* arela codex - Launch Codex CLI with ticket context
*/
program
.command("codex [ticket]")
.description("Launch Codex CLI (optionally with ticket path)")
.action(async (ticket) => {
const { execa } = await import('execa');
if (ticket) {
// Print the implement command for Codex
console.log(pc.bold(pc.cyan("\nš Ticket ready for Codex:\n")));
console.log(pc.gray(`implement ${ticket}\n`));
console.log(pc.gray("Paste this into Codex when it starts...\n"));
// Wait 2 seconds
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Launch Codex CLI
try {
await execa('codex', [], {
stdio: 'inherit',
cwd: process.cwd(),
});
}
catch (error) {
console.error(pc.red('\nā Failed to launch Codex CLI'));
console.error(pc.gray('Make sure Codex CLI is installed\n'));
process.exit(1);
}
});
const versionCommand = program
.command("version")
.description("Detect and manage API versions");
versionCommand
.command("detect-drift")
.description("Detect breaking OpenAPI changes in git history")
.option("--cwd <dir>", "Repository root", process.cwd())
.action(async (opts) => {
console.log(pc.bold(pc.cyan("\nš Checking for API drift...\n")));
try {
const drift = await detectBreakingChanges(opts.cwd);
if (drift.length === 0) {
console.log(pc.bold(pc.green("ā
No breaking changes detected\n")));
return;
}
for (const change of drift) {
const severityIcon = change.severity === "critical" ? "šØ" : "ā ļø";
console.log(`${severityIcon} ${pc.bold(change.type)} ${pc.gray(`(${change.file})`)}`);
if (change.endpoint) {
console.log(pc.gray(` Endpoint: ${change.endpoint}`));
}
if (change.method) {
console.log(pc.gray(` Method: ${change.method.toUpperCase()}`));
}
console.log(pc.gray(` Field: ${change.field}`));
console.log(pc.gray(` Old: ${change.oldValue}`));
console.log(pc.gray(` New: ${change.newValue}\n`));
}
console.log(pc.yellow("š” Consider creating a new slice version for breaking changes.\n"));
process.exit(1);
}
catch (error) {
console.error(pc.red(`\nā Drift detection failed: ${error.message}\n`));
process.exit(1);
}
});
versionCommand
.command("create <slice>")
.description("Create a new version of a slice and its OpenAPI contract")
.option("--cwd <dir>", "Repository root", process.cwd())
.option("--version <number>", "Version number to create", "2")
.action(async (slice, opts) => {
const versionNumber = parseInt(opts.version, 10);
if (Number.isNaN(versionNumber)) {
console.error(pc.red("\nā Version must be a valid number\n"));
process.exit(1);
}
console.log(pc.bold(pc.cyan(`\nš¦ Creating ${slice} v${versionNumber}...\n`)));
try {
const result = await createSliceVersion(opts.cwd, slice, versionNumber);
console.log(pc.green("ā
Slice version created!\n"));
console.log(pc.gray(` Directory: ${result.newSlicePath}`));
if (result.newSpecPath) {
console.log(pc.gray(` OpenAPI spec: ${result.newSpecPath}`));
}
else {
console.log(pc.yellow(" No OpenAPI spec found to duplicate."));
}
console.log(pc.bold(pc.cyan("\nNext steps:")));
console.log(pc.gray("1. Implement changes in the new slice"));
console.log(pc.gray("2. Update the versioned OpenAPI contract"));
console.log(pc.gray("3. Test v1 and v2 side-by-side"));
console.log(pc.gray("4. Deploy with both versions enabled\n"));
}
catch (error) {
console.error(pc.red(`\nā Failed to create version: ${error.message}\n`));
process.exit(1);
}
});
registerMemoryCommands(program);
function isDirectCliExecution() {
const entry = process.argv[1];
if (!entry) {
return true;
}
try {
return import.meta.url === pathToFileURL(entry).href;
}
catch {
return true;
}
}
if (isDirectCliExecution()) {
program.parse();
}
export { program };
//# sourceMappingURL=cli.js.map