arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
1,172 lines (1,170 loc) โข 63.7 kB
JavaScript
#!/usr/bin/env node
import { config } from "dotenv";
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";
import { getStalenessChecker } from "./utils/staleness-checker.js";
import { checkForUpdatesAsync } from "./utils/update-checker.js";
// Load .env file if it exists
config();
const VERSION = "4.1.0";
const program = new Command()
.name("arela")
.description("AI-powered CTO with multi-agent orchestration")
.version(VERSION);
// Auto-check for updates (non-blocking, async, cached for 24h)
checkForUpdatesAsync(VERSION);
// Auto-check memory staleness before every command
program.hook("preAction", async () => {
const checker = getStalenessChecker();
await checker.checkAndUpdate({ silent: true }); // Silent to avoid spam
});
/**
* 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"));
}
// Create .env files
console.log(pc.bold(pc.cyan("\n๐ Setting up environment...\n")));
const { setupKeys } = await import("./setup/keys.js");
// Create .env and .env.example if they don't exist
const fs = await import("fs");
const VERSION = "4.0.2";
const envPath = path.join(opts.cwd, ".env");
const envExamplePath = path.join(opts.cwd, ".env.example");
const envTemplate = `# Arela Configuration
# OpenAI API Key (optional but recommended for fast classification)
# Get yours at: https://platform.openai.com/api-keys
# Cost: ~$0.0001 per query (1 cent per 100 queries)
# Speed: ~200ms vs 1.5s with Ollama
OPENAI_API_KEY=
# Anthropic API Key (optional, for future features)
ANTHROPIC_API_KEY=
`;
if (!fs.existsSync(envExamplePath)) {
fs.writeFileSync(envExamplePath, envTemplate);
console.log(pc.gray(" โ
Created .env.example"));
}
if (!fs.existsSync(envPath)) {
fs.writeFileSync(envPath, envTemplate);
console.log(pc.gray(" โ
Created .env"));
}
console.log(pc.bold(pc.cyan("\n๐ Next steps:\n")));
console.log(pc.gray(" 1. Run `arela setup` to configure API keys (optional)"));
console.log(pc.gray(" 2. Run `arela agents` to meet your AI team"));
console.log(pc.gray(" 3. Run `arela index` to build your RAG brain\n"));
}
catch (error) {
console.error(pc.red(`\n๐ตโ๐ซ Initialization went sideways: ${error.message}\n`));
process.exit(1);
}
});
/**
* arela setup - Interactive setup wizard
*/
program
.command("setup")
.description("Interactive setup wizard for API keys and configuration")
.action(async () => {
const { setupKeys } = await import("./setup/keys.js");
await setupKeys();
});
/**
* 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 route - Test Meta-RAG context routing
*/
program
.command("route")
.description("Test Meta-RAG context routing")
.argument("<query>", "Query to route")
.option("--verbose", "Show detailed routing info")
.option("--multi-hop", "Enable multi-hop reasoning for complex queries")
.action(async (query, opts) => {
try {
const { ContextRouter } = await import("./context-router.js");
const { QueryClassifier } = await import("./meta-rag/classifier.js");
const { MemoryRouter } = await import("./meta-rag/router.js");
const { FusionEngine } = await import("./fusion/index.js");
const { HexiMemory } = await import("./memory/hexi-memory.js");
const heximemory = new HexiMemory();
await heximemory.init(process.cwd());
const classifier = new QueryClassifier();
await classifier.init();
const memoryRouter = new MemoryRouter({
heximemory,
classifier,
});
const fusion = new FusionEngine();
const router = new ContextRouter({
heximemory,
classifier,
router: memoryRouter,
fusion,
debug: opts.verbose,
});
await router.init();
// Multi-hop reasoning path
if (opts.multiHop) {
const { QueryDecomposer } = await import("./reasoning/decomposer.js");
const { MultiHopRouter } = await import("./reasoning/multi-hop-router.js");
const decomposer = new QueryDecomposer();
await decomposer.init();
const multiHopRouter = new MultiHopRouter(router);
console.log(`\n๐ง Routing query with multi-hop reasoning: "${query}"\n`);
const startDecomp = Date.now();
const decomposition = await decomposer.decompose(query);
const decompTime = Date.now() - startDecomp;
if (!decomposition.isComplex) {
console.log(pc.yellow("Query is not complex enough for multi-hop reasoning"));
console.log(pc.gray("Falling back to single-hop routing\n"));
// Fall through to regular routing
const response = await router.route({ query });
console.log(`๐ Classification: ${response.classification.type} (${response.classification.confidence})`);
console.log(`๐ฏ Layers: ${response.routing.layers.join(", ")}`);
console.log(`๐ก Reasoning: ${response.routing.reasoning}`);
console.log(`\nโฑ๏ธ Stats:`);
console.log(` Classification: ${response.stats.classificationTime}ms`);
console.log(` Retrieval: ${response.stats.retrievalTime}ms`);
console.log(` Fusion: ${response.stats.fusionTime}ms`);
console.log(` Total: ${response.stats.totalTime}ms`);
console.log(` Estimated tokens: ${response.stats.tokensEstimated}`);
console.log(` Context items: ${response.context.length}`);
}
else {
console.log(pc.bold(pc.cyan("๐ Decomposing query...")));
console.log(pc.gray(` Time: ${decompTime}ms`));
console.log(pc.gray(` Strategy: ${decomposition.strategy}\n`));
if (opts.verbose) {
decomposition.subQueries.forEach((sq) => {
const deps = sq.dependencies.length > 0 ? ` (depends on ${sq.dependencies.join(", ")})` : "";
console.log(pc.gray(` Sub-query ${sq.id}: "${sq.query}"${deps}`));
});
console.log("");
}
console.log(pc.bold(pc.cyan(`๐ฏ Executing ${decomposition.subQueries.length} hops (${decomposition.strategy})...\n`)));
const result = await multiHopRouter.route(decomposition);
// Display hop results
for (const hop of result.hops) {
const icon = hop.context.length > 0 ? "โ
" : "โ ๏ธ";
console.log(`${icon} Hop ${hop.subQueryId}: ${hop.subQuery}`);
console.log(pc.gray(` Classification: ${hop.classification.type}`));
console.log(pc.gray(` Results: ${hop.context.length}`));
console.log(pc.gray(` Relevance: ${(hop.relevanceScore * 100).toFixed(0)}%`));
console.log(pc.gray(` Time: ${hop.executionTime}ms\n`));
}
console.log(pc.bold(pc.green(`โ
Combined ${result.combinedContext.filter(c => c.metadata?.type !== "separator").length} results (deduplicated from ${result.hops.reduce((sum, h) => sum + h.context.length, 0)})\n`)));
console.log(pc.bold("๐ Multi-Hop Stats:"));
console.log(pc.gray(` Total hops: ${result.stats.totalHops}`));
console.log(pc.gray(` Execution strategy: ${decomposition.strategy}`));
console.log(pc.gray(` Total time: ${result.stats.totalTime}ms`));
console.log(pc.gray(` Decomposition: ${decompTime}ms`));
console.log(pc.gray(` Execution: ${result.stats.executionTime}ms`));
console.log(pc.gray(` Combination: ${result.stats.combinationTime}ms`));
console.log(pc.gray(` Results per hop: ${result.stats.resultsPerHop.toFixed(1)} avg`));
console.log(pc.gray(` Deduplication: ${result.stats.deduplicationRate.toFixed(1)}% reduction`));
console.log(pc.gray(` Estimated tokens: ${result.stats.tokensEstimated}\n`));
if (opts.verbose) {
console.log(pc.bold("\n๐ฆ Combined Context:"));
console.log(JSON.stringify(result.combinedContext, null, 2));
}
}
// Store last query in session for feedback
try {
const { SessionMemory } = await import("./memory/session.js");
const session = new SessionMemory(process.cwd());
await session.init();
await session.setContext("lastQuery", {
query,
classification: null,
routing: null,
timestamp: Date.now(),
});
}
catch (error) {
console.debug("Failed to store query in session:", error);
}
process.exit(0);
}
// Regular single-hop routing
console.log(`\n๐ง Routing query: "${query}"\n`);
const response = await router.route({ query });
console.log(`๐ Classification: ${response.classification.type} (${response.classification.confidence})`);
console.log(`๐ฏ Layers: ${response.routing.layers.join(", ")}`);
console.log(`๐ก Reasoning: ${response.routing.reasoning}`);
console.log(`\nโฑ๏ธ Stats:`);
console.log(` Classification: ${response.stats.classificationTime}ms`);
console.log(` Retrieval: ${response.stats.retrievalTime}ms`);
console.log(` Fusion: ${response.stats.fusionTime}ms`);
console.log(` Total: ${response.stats.totalTime}ms`);
console.log(` Estimated tokens: ${response.stats.tokensEstimated}`);
console.log(` Context items: ${response.context.length}`);
if (opts.verbose) {
console.log(`\n๐ฆ Context:`);
console.log(JSON.stringify(response.context, null, 2));
}
// Store last query in session for feedback
try {
const { SessionMemory } = await import("./memory/session.js");
const session = new SessionMemory(process.cwd());
await session.init();
await session.setContext("lastQuery", {
query,
classification: response.classification,
routing: response.routing,
timestamp: Date.now(),
});
}
catch (error) {
// Non-critical - just log silently
console.debug("Failed to store query in session:", error);
}
process.exit(0);
}
catch (error) {
console.error(pc.red(`\nโ Context routing failed: ${error.message}\n`));
console.error(pc.dim(error.stack));
process.exit(1);
}
});
/**
* arela update - Check for updates
*/
program
.command("update")
.description("Check for arela updates")
.action(async () => {
const { forceUpdateCheck } = await import("./utils/update-checker.js");
await forceUpdateCheck(VERSION);
});
/**
* 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 summarize - Summarize a code file
*/
program
.command("summarize <file>")
.description("Summarize a code file using AST + LLM pipeline")
.option("--no-cache", "Skip cache, force re-summarization")
.option("--output <format>", "Output format (json|markdown)", "markdown")
.option("--cwd <dir>", "Working directory", process.cwd())
.action(async (file, opts) => {
try {
const { CodeSummarizer, summaryToMarkdown } = await import("./summarization/code-summarizer.js");
console.log(pc.cyan(`\n๐ Summarizing ${file}...\n`));
const summarizer = new CodeSummarizer(opts.cwd);
const summary = await summarizer.summarize(file, { noCache: !opts.cache });
if (opts.output === "json") {
console.log(JSON.stringify(summary, null, 2));
}
else {
console.log(summaryToMarkdown(summary));
}
const stats = summarizer.getCacheStats();
console.log(pc.cyan(`\n๐ Cache Stats: ${stats.hitRate ?? 0}% hit rate, $${stats.savings.toFixed(4)} saved\n`));
}
catch (error) {
console.error(pc.red(`\nโ Summarization failed: ${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);
}
});
// install-hook command moved to line ~1230 (comprehensive version with all 3 hooks)
/**
* 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 (op