@ariacode/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +404 -0
  3. package/dist/actions.d.ts +271 -0
  4. package/dist/actions.js +1809 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/agent.d.ts +26 -0
  7. package/dist/agent.js +182 -0
  8. package/dist/agent.js.map +1 -0
  9. package/dist/app.d.ts +14 -0
  10. package/dist/app.js +83 -0
  11. package/dist/app.js.map +1 -0
  12. package/dist/cli.d.ts +12 -0
  13. package/dist/cli.js +157 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +170 -0
  16. package/dist/config.js +291 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/context.d.ts +58 -0
  19. package/dist/context.js +44 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/parser.d.ts +39 -0
  22. package/dist/parser.js +323 -0
  23. package/dist/parser.js.map +1 -0
  24. package/dist/prompts/ask.md +20 -0
  25. package/dist/prompts/explore.md +38 -0
  26. package/dist/prompts/patch.md +27 -0
  27. package/dist/prompts/plan.md +41 -0
  28. package/dist/prompts/prompts/ask.md +20 -0
  29. package/dist/prompts/prompts/explore.md +38 -0
  30. package/dist/prompts/prompts/patch.md +27 -0
  31. package/dist/prompts/prompts/plan.md +41 -0
  32. package/dist/prompts/prompts/review.md +33 -0
  33. package/dist/prompts/review.md +33 -0
  34. package/dist/provider.d.ts +148 -0
  35. package/dist/provider.js +486 -0
  36. package/dist/provider.js.map +1 -0
  37. package/dist/repo.d.ts +22 -0
  38. package/dist/repo.js +154 -0
  39. package/dist/repo.js.map +1 -0
  40. package/dist/safety.d.ts +48 -0
  41. package/dist/safety.js +140 -0
  42. package/dist/safety.js.map +1 -0
  43. package/dist/storage.d.ts +133 -0
  44. package/dist/storage.js +300 -0
  45. package/dist/storage.js.map +1 -0
  46. package/dist/tools.d.ts +70 -0
  47. package/dist/tools.js +654 -0
  48. package/dist/tools.js.map +1 -0
  49. package/dist/ui.d.ts +203 -0
  50. package/dist/ui.js +410 -0
  51. package/dist/ui.js.map +1 -0
  52. package/package.json +73 -0
@@ -0,0 +1,1809 @@
1
+ /**
2
+ * Command implementations for Aria Code CLI
3
+ *
4
+ * Each exported function corresponds to a CLI command and orchestrates:
5
+ * - Configuration loading
6
+ * - Project detection
7
+ * - Session management
8
+ * - Agent loop execution
9
+ * - Terminal output
10
+ */
11
+ import { randomUUID } from "node:crypto";
12
+ import * as path from "node:path";
13
+ import * as os from "node:os";
14
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
+ import { dirname } from "node:path";
17
+ import { execFileSync } from "node:child_process";
18
+ import { getConfig } from "./config.js";
19
+ import { detectProjectType } from "./repo.js";
20
+ import { createProvider, ProviderError } from "./provider.js";
21
+ import { initializeDatabase, createSession, updateSessionStatus, getSession, logMessage, listSessions, } from "./storage.js";
22
+ import { readFileTool, listDirectoryTool, searchCodeTool, readPackageJsonTool, readPrismaSchemaTool, proposeDiffTool, applyDiffTool, } from "./tools.js";
23
+ import { agentLoop, UserCancelledError } from "./agent.js";
24
+ import prompts from "prompts";
25
+ import { initUI, info, print, error as uiError, bold, yellow, green, dim, cyan, red, renderTable, generateAndRenderDiff, confirm, ConfirmCancelledError, } from "./ui.js";
26
+ import { loadConfig, validateConfig } from "./config.js";
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ // ---------------------------------------------------------------------------
29
+ // Provider resolution with interactive setup
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Map of provider names to their required environment variable.
33
+ * Ollama doesn't need an API key.
34
+ */
35
+ const PROVIDER_ENV_KEYS = {
36
+ anthropic: "ANTHROPIC_API_KEY",
37
+ openai: "OPENAI_API_KEY",
38
+ openrouter: "OPENROUTER_API_KEY",
39
+ ollama: null,
40
+ };
41
+ /**
42
+ * Default models per provider.
43
+ */
44
+ const DEFAULT_MODELS = {
45
+ anthropic: "claude-sonnet-4-6",
46
+ openai: "gpt-4o",
47
+ ollama: "llama3",
48
+ openrouter: "anthropic/claude-sonnet-4-6",
49
+ };
50
+ /**
51
+ * Check if the given provider has its API key available.
52
+ */
53
+ function isProviderReady(providerName) {
54
+ const envKey = PROVIDER_ENV_KEYS[providerName];
55
+ if (envKey === null)
56
+ return true; // Ollama — no key needed
57
+ return Boolean(envKey && process.env[envKey]);
58
+ }
59
+ /**
60
+ * Interactively select a provider and configure its API key.
61
+ *
62
+ * Called when the configured provider's API key is missing.
63
+ * Saves the key to the shell environment for the current process
64
+ * and updates ~/.aria/config.toml with the chosen provider/model.
65
+ *
66
+ * Returns the created Provider instance and updates config in-place.
67
+ */
68
+ async function resolveProvider(config) {
69
+ // 1. Try the configured provider first
70
+ if (isProviderReady(config.provider.default)) {
71
+ return createProvider(config.provider.default);
72
+ }
73
+ // 2. Check if any other provider is already configured via env
74
+ for (const [name, envKey] of Object.entries(PROVIDER_ENV_KEYS)) {
75
+ if (name === config.provider.default)
76
+ continue;
77
+ if (envKey === null || process.env[envKey]) {
78
+ info(dim(`${config.provider.default} not configured, falling back to ${name}`));
79
+ config.provider.default = name;
80
+ config.provider.model = DEFAULT_MODELS[name] ?? config.provider.model;
81
+ return createProvider(name);
82
+ }
83
+ }
84
+ // 3. No provider ready — interactive setup
85
+ info("");
86
+ info(bold("No API key found. Let's set up a provider."));
87
+ info("");
88
+ const providerChoices = [
89
+ { title: "Anthropic (Claude)", value: "anthropic", description: "Requires ANTHROPIC_API_KEY" },
90
+ { title: "OpenAI (GPT)", value: "openai", description: "Requires OPENAI_API_KEY" },
91
+ { title: "OpenRouter", value: "openrouter", description: "Requires OPENROUTER_API_KEY" },
92
+ { title: "Ollama (local)", value: "ollama", description: "No API key needed, runs locally" },
93
+ ];
94
+ const { provider: selectedProvider } = await prompts({
95
+ type: "select",
96
+ name: "provider",
97
+ message: "Choose a provider",
98
+ choices: providerChoices,
99
+ }, {
100
+ onCancel: () => {
101
+ throw new ConfirmCancelledError();
102
+ },
103
+ });
104
+ if (!selectedProvider) {
105
+ throw new ConfirmCancelledError();
106
+ }
107
+ const envKey = PROVIDER_ENV_KEYS[selectedProvider];
108
+ // Ollama — no key needed, just check URL
109
+ if (envKey === null) {
110
+ const { baseUrl } = await prompts({
111
+ type: "text",
112
+ name: "baseUrl",
113
+ message: "Ollama base URL",
114
+ initial: process.env.OLLAMA_BASE_URL || "http://localhost:11434",
115
+ }, {
116
+ onCancel: () => { throw new ConfirmCancelledError(); },
117
+ });
118
+ if (baseUrl && baseUrl !== "http://localhost:11434") {
119
+ process.env.OLLAMA_BASE_URL = baseUrl;
120
+ }
121
+ config.provider.default = "ollama";
122
+ config.provider.model = DEFAULT_MODELS.ollama;
123
+ saveProviderChoice(config);
124
+ return createProvider("ollama");
125
+ }
126
+ // API key providers — prompt for key
127
+ const { apiKey } = await prompts({
128
+ type: "password",
129
+ name: "apiKey",
130
+ message: `Enter your ${envKey}`,
131
+ }, {
132
+ onCancel: () => { throw new ConfirmCancelledError(); },
133
+ });
134
+ if (!apiKey) {
135
+ throw new ProviderError(`${envKey} is required for ${selectedProvider}`, selectedProvider);
136
+ }
137
+ // Set in current process environment
138
+ process.env[envKey] = apiKey;
139
+ // Update config
140
+ config.provider.default = selectedProvider;
141
+ config.provider.model = DEFAULT_MODELS[selectedProvider] ?? config.provider.model;
142
+ // Save choice to ~/.aria/config.toml
143
+ saveProviderChoice(config);
144
+ // Offer to save the key to shell profile
145
+ const { saveKey } = await prompts({
146
+ type: "confirm",
147
+ name: "saveKey",
148
+ message: `Save ${envKey} to ~/.zshrc for future sessions?`,
149
+ initial: true,
150
+ }, {
151
+ onCancel: () => { },
152
+ });
153
+ if (saveKey) {
154
+ const shellRc = path.join(os.homedir(), ".zshrc");
155
+ const exportLine = `\nexport ${envKey}="${apiKey}"\n`;
156
+ try {
157
+ const existing = existsSync(shellRc) ? readFileSync(shellRc, "utf-8") : "";
158
+ if (!existing.includes(envKey)) {
159
+ writeFileSync(shellRc, existing + exportLine, "utf-8");
160
+ info(green(`✓ Added ${envKey} to ~/.zshrc`));
161
+ info(dim(" Run `source ~/.zshrc` or open a new terminal to apply."));
162
+ }
163
+ else {
164
+ info(dim(`${envKey} already exists in ~/.zshrc, skipping.`));
165
+ }
166
+ }
167
+ catch {
168
+ info(yellow(`Could not write to ~/.zshrc. Set ${envKey} manually.`));
169
+ }
170
+ }
171
+ return createProvider(selectedProvider);
172
+ }
173
+ /**
174
+ * Save the provider/model choice to ~/.aria/config.toml
175
+ */
176
+ function saveProviderChoice(config) {
177
+ try {
178
+ const configPath = path.join(os.homedir(), ".aria", "config.toml");
179
+ if (existsSync(configPath)) {
180
+ let content = readFileSync(configPath, "utf-8");
181
+ // Update provider default and model lines
182
+ content = content.replace(/^default\s*=\s*".*"/m, `default = "${config.provider.default}"`);
183
+ content = content.replace(/^model\s*=\s*".*"/m, `model = "${config.provider.model}"`);
184
+ writeFileSync(configPath, content, { encoding: "utf-8", mode: 0o600 });
185
+ info(dim(`Updated ~/.aria/config.toml with provider: ${config.provider.default}`));
186
+ }
187
+ }
188
+ catch {
189
+ // Non-fatal
190
+ }
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Read-only tool set for ask / plan commands
194
+ // ---------------------------------------------------------------------------
195
+ /**
196
+ * The five read-only tools exposed to the ask command.
197
+ * Requirements: 9.4
198
+ */
199
+ const READ_ONLY_TOOLS = [
200
+ readFileTool,
201
+ listDirectoryTool,
202
+ searchCodeTool,
203
+ readPackageJsonTool,
204
+ readPrismaSchemaTool,
205
+ ];
206
+ // ---------------------------------------------------------------------------
207
+ // System prompt builder (shared across all commands)
208
+ // ---------------------------------------------------------------------------
209
+ /**
210
+ * Default fallback templates when prompt files are missing.
211
+ */
212
+ const FALLBACK_TEMPLATES = {
213
+ ask: [
214
+ "You are Aria Code, a coding assistant for {{projectType}} projects.",
215
+ "",
216
+ "Project: {{projectRoot}}",
217
+ "Framework: {{frameworkInfo}}",
218
+ "Has Prisma: {{hasPrisma}}",
219
+ "",
220
+ "You are in read-only mode. Do NOT propose or apply any file changes.",
221
+ "Answer the user's question using the available read-only tools.",
222
+ ].join("\n"),
223
+ plan: [
224
+ "You are Aria Code, a planning assistant for {{projectType}} projects.",
225
+ "",
226
+ "Project: {{projectRoot}}",
227
+ "Framework: {{frameworkInfo}}",
228
+ "Has Prisma: {{hasPrisma}}",
229
+ "",
230
+ "You are in read-only mode. Do NOT propose or apply any file changes.",
231
+ "Generate a structured implementation plan for the user's goal.",
232
+ "Include: ordered steps, affected files, risks, and implementation notes.",
233
+ ].join("\n"),
234
+ patch: [
235
+ "You are Aria Code, a coding agent for {{projectType}} projects.",
236
+ "",
237
+ "Project: {{projectRoot}}",
238
+ "Framework: {{frameworkInfo}}",
239
+ "Has Prisma: {{hasPrisma}}",
240
+ "",
241
+ "Analyze the repository, then use propose_diff to generate changes.",
242
+ "After proposing, use apply_diff to apply the changes if confirmed.",
243
+ "Be precise and minimal — only change what is necessary.",
244
+ ].join("\n"),
245
+ review: [
246
+ "You are Aria Code, a code review assistant for {{projectType}} projects.",
247
+ "",
248
+ "Project: {{projectRoot}}",
249
+ "Framework: {{frameworkInfo}}",
250
+ "Has Prisma: {{hasPrisma}}",
251
+ "",
252
+ "You are in read-only mode. Analyze the provided diff and return a structured review.",
253
+ "",
254
+ "Return your review using this format:",
255
+ "",
256
+ "# Code Review",
257
+ "",
258
+ "## Summary",
259
+ "(brief overview of what the diff does)",
260
+ "",
261
+ "## Issues",
262
+ "- [HIGH] (critical bugs, security vulnerabilities, data loss risks)",
263
+ "- [MEDIUM] (logic errors, missing error handling, performance concerns)",
264
+ "- [LOW] (style inconsistencies, minor improvements)",
265
+ "",
266
+ "## Suggestions",
267
+ "- (non-blocking improvements or alternatives to consider)",
268
+ ].join("\n"),
269
+ explore: [
270
+ "You are Aria Code, a repository exploration assistant.",
271
+ "",
272
+ "Project: {{projectRoot}}",
273
+ "",
274
+ "Scan the repository structure, detect frameworks, identify entry points,",
275
+ "and summarize the architecture.",
276
+ "",
277
+ "Use the available read-only tools:",
278
+ "- list_directory: Scan directory structure (respect .gitignore)",
279
+ "- read_file: Read key configuration and source files",
280
+ "- search_code: Search for patterns, exports, and entry points",
281
+ "- read_package_json: Detect dependencies and scripts",
282
+ "- read_prisma_schema: Read Prisma schema (when available)",
283
+ "",
284
+ "Return your findings in a structured markdown format covering:",
285
+ "Project Type, Key Files, Entry Points, Structure, and Notable Patterns.",
286
+ ].join("\n"),
287
+ };
288
+ /**
289
+ * Build a system prompt from a template file with project context interpolation.
290
+ *
291
+ * Loads the template from src/prompts/{templateName}.md, falls back to a
292
+ * built-in default if the file is missing, then replaces standard variables.
293
+ *
294
+ * @param templateName - Name of the template (ask, plan, patch, review, explore)
295
+ * @param ctx - Execution context
296
+ * @param extraVars - Additional template variables to replace (e.g. {{userGoal}})
297
+ */
298
+ function buildSystemPrompt(templateName, ctx, extraVars = {}) {
299
+ const templatePath = path.join(__dirname, "prompts", `${templateName}.md`);
300
+ let template;
301
+ try {
302
+ template = readFileSync(templatePath, "utf-8");
303
+ }
304
+ catch {
305
+ template = FALLBACK_TEMPLATES[templateName] ?? "";
306
+ }
307
+ const project = detectProjectType(ctx.projectRoot);
308
+ const frameworkInfo = project.framework
309
+ ? `${project.framework.name}${project.framework.version ? ` ${project.framework.version}` : ""}${project.framework.router ? ` (${project.framework.router} router)` : ""}`
310
+ : "none";
311
+ let result = template
312
+ .replace(/\{\{projectType\}\}/g, project.type)
313
+ .replace(/\{\{projectRoot\}\}/g, ctx.projectRoot)
314
+ .replace(/\{\{frameworkInfo\}\}/g, frameworkInfo)
315
+ .replace(/\{\{hasPrisma\}\}/g, project.hasPrisma ? "yes" : "no");
316
+ for (const [key, value] of Object.entries(extraVars)) {
317
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
318
+ }
319
+ return result;
320
+ }
321
+ /**
322
+ * Execute the ask command.
323
+ *
324
+ * Flow:
325
+ * 1. Load configuration and detect project type (Req 9.1)
326
+ * 2. Create or resume session with mode: "plan" (Req 9.2, 9.9)
327
+ * 3. Build system prompt from ask.md template (Req 9.3)
328
+ * 4. Expose only read-only tools (Req 9.4)
329
+ * 5. Execute agent loop (Req 9.5, 9.6)
330
+ * 6. Render response to terminal (Req 9.7)
331
+ * 7. Persist session to database (Req 9.8)
332
+ *
333
+ * Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9
334
+ */
335
+ export async function runAsk(options) {
336
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
337
+ // 1. Load configuration (Req 9.1)
338
+ const config = getConfig(projectRoot, {
339
+ quiet: options.quiet,
340
+ maxTokens: options.maxTokens,
341
+ });
342
+ // Initialize UI with config settings
343
+ initUI(config.ui.color, config.ui.quiet);
344
+ // Detect project type early to fail fast if package.json is missing (Req 9.1)
345
+ detectProjectType(projectRoot);
346
+ // 2. Initialize database and create/resume session (Req 9.2, 9.8, 9.9)
347
+ const db = initializeDatabase();
348
+ let sessionId;
349
+ let resumedMessages = [];
350
+ if (options.session) {
351
+ // Resume existing session (Req 9.9)
352
+ const existing = getSession(db, options.session);
353
+ if (!existing) {
354
+ uiError(`Session not found: ${options.session}`);
355
+ process.exit(1);
356
+ }
357
+ sessionId = existing.id;
358
+ info(`Resuming session ${sessionId}`);
359
+ // Load previous messages for context
360
+ const rows = db
361
+ .prepare(`SELECT role, content FROM messages WHERE session_id = ? ORDER BY created_at ASC`)
362
+ .all(sessionId);
363
+ resumedMessages = rows;
364
+ }
365
+ else {
366
+ // Create new session (Req 9.2)
367
+ sessionId = randomUUID();
368
+ createSession(db, {
369
+ id: sessionId,
370
+ command: "ask",
371
+ projectRoot,
372
+ provider: config.provider.default,
373
+ model: config.provider.model,
374
+ });
375
+ }
376
+ // 3. Resolve provider (interactive setup if needed)
377
+ let provider;
378
+ try {
379
+ provider = await resolveProvider(config);
380
+ }
381
+ catch (err) {
382
+ if (err instanceof ConfirmCancelledError) {
383
+ info(yellow("Setup cancelled."));
384
+ updateSessionStatus(db, sessionId, "cancelled");
385
+ process.exit(130);
386
+ }
387
+ uiError(err instanceof Error ? err.message : String(err));
388
+ updateSessionStatus(db, sessionId, "failed", String(err));
389
+ process.exit(4);
390
+ }
391
+ // 4. Build execution context with mode: "plan" (Req 9.2)
392
+ const ctx = {
393
+ projectRoot,
394
+ sessionId,
395
+ provider: config.provider.default,
396
+ model: config.provider.model,
397
+ mode: "plan", // ask is always read-only
398
+ dryRun: false,
399
+ assumeYes: false,
400
+ maxIterations: config.agent.maxIterations,
401
+ timeoutSeconds: config.agent.timeoutSeconds,
402
+ };
403
+ // Override maxTokens if provided via flag
404
+ if (options.maxTokens !== undefined) {
405
+ config.provider.maxTokens = options.maxTokens;
406
+ }
407
+ // 5. Build system prompt from ask.md template (Req 9.3)
408
+ const systemPrompt = buildSystemPrompt("ask", ctx);
409
+ // Log system prompt as a system message
410
+ logMessage(db, sessionId, "system", systemPrompt);
411
+ // Prepend system prompt to the message array that agentLoop will use.
412
+ // agentLoop builds its own messages array starting with system + user,
413
+ // so we pass the question as the userRequest and let it handle the rest.
414
+ // For session resumption, we inject prior messages via the question context.
415
+ let userRequest = options.question;
416
+ if (resumedMessages.length > 0) {
417
+ // Summarise prior context so the model has continuity
418
+ const priorContext = resumedMessages
419
+ .filter((m) => m.role !== "system")
420
+ .map((m) => `[${m.role}]: ${m.content}`)
421
+ .join("\n\n");
422
+ userRequest = `[Resumed session context]\n${priorContext}\n\n[New question]: ${options.question}`;
423
+ }
424
+ // 6. Execute agent loop (Req 9.5, 9.6)
425
+ // agentLoop streams the response to stdout as it arrives (Req 9.7)
426
+ try {
427
+ await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "ask", db, systemPrompt);
428
+ // 7. Mark session as completed (Req 9.8)
429
+ updateSessionStatus(db, sessionId, "completed");
430
+ }
431
+ catch (err) {
432
+ const message = err instanceof Error ? err.message : String(err);
433
+ uiError(message);
434
+ updateSessionStatus(db, sessionId, "failed", message);
435
+ process.exit(1);
436
+ }
437
+ }
438
+ /**
439
+ * Execute the plan command.
440
+ *
441
+ * Flow:
442
+ * 1. Load configuration and detect project type (Req 10.1)
443
+ * 2. Create or resume session with mode: "plan" (Req 10.2, 10.8)
444
+ * 3. Build system prompt from plan.md template (Req 10.3)
445
+ * 4. Expose only read-only tools (Req 10.4)
446
+ * 5. Execute agent loop (Req 10.5)
447
+ * 6. Render structured plan to terminal (Req 10.6)
448
+ * 7. Save to file if --output flag provided (Req 10.7)
449
+ * 8. Persist session to database (Req 10.8)
450
+ *
451
+ * Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8
452
+ */
453
+ export async function runPlan(options) {
454
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
455
+ // 1. Load configuration (Req 10.1)
456
+ const config = getConfig(projectRoot, {
457
+ quiet: options.quiet,
458
+ });
459
+ // Initialize UI with config settings
460
+ initUI(config.ui.color, config.ui.quiet);
461
+ // Detect project type early to fail fast if package.json is missing (Req 10.1)
462
+ detectProjectType(projectRoot);
463
+ // 2. Initialize database and create/resume session (Req 10.2, 10.8)
464
+ const db = initializeDatabase();
465
+ let sessionId;
466
+ let resumedMessages = [];
467
+ if (options.session) {
468
+ // Resume existing session (Req 10.8)
469
+ const existing = getSession(db, options.session);
470
+ if (!existing) {
471
+ uiError(`Session not found: ${options.session}`);
472
+ process.exit(1);
473
+ }
474
+ sessionId = existing.id;
475
+ info(`Resuming session ${sessionId}`);
476
+ // Load previous messages for context
477
+ const rows = db
478
+ .prepare(`SELECT role, content FROM messages WHERE session_id = ? ORDER BY created_at ASC`)
479
+ .all(sessionId);
480
+ resumedMessages = rows;
481
+ }
482
+ else {
483
+ // Create new session (Req 10.2)
484
+ sessionId = randomUUID();
485
+ createSession(db, {
486
+ id: sessionId,
487
+ command: "plan",
488
+ projectRoot,
489
+ provider: config.provider.default,
490
+ model: config.provider.model,
491
+ });
492
+ }
493
+ // 3. Resolve provider (interactive setup if needed)
494
+ let provider;
495
+ try {
496
+ provider = await resolveProvider(config);
497
+ }
498
+ catch (err) {
499
+ if (err instanceof ConfirmCancelledError) {
500
+ info(yellow("Setup cancelled."));
501
+ updateSessionStatus(db, sessionId, "cancelled");
502
+ process.exit(130);
503
+ }
504
+ uiError(err instanceof Error ? err.message : String(err));
505
+ updateSessionStatus(db, sessionId, "failed", String(err));
506
+ process.exit(4);
507
+ }
508
+ // 4. Build execution context with mode: "plan" (Req 10.2)
509
+ const ctx = {
510
+ projectRoot,
511
+ sessionId,
512
+ provider: config.provider.default,
513
+ model: config.provider.model,
514
+ mode: "plan", // plan is always read-only
515
+ dryRun: false,
516
+ assumeYes: false,
517
+ maxIterations: config.agent.maxIterations,
518
+ timeoutSeconds: config.agent.timeoutSeconds,
519
+ };
520
+ // 5. Build system prompt from plan.md template (Req 10.3)
521
+ const systemPrompt = buildSystemPrompt("plan", ctx, { userGoal: options.goal });
522
+ // Log system prompt as a system message
523
+ logMessage(db, sessionId, "system", systemPrompt);
524
+ // Build user request, incorporating prior session context if resuming
525
+ let userRequest = options.goal;
526
+ if (resumedMessages.length > 0) {
527
+ // Summarise prior context so the model has continuity
528
+ const priorContext = resumedMessages
529
+ .filter((m) => m.role !== "system")
530
+ .map((m) => `[${m.role}]: ${m.content}`)
531
+ .join("\n\n");
532
+ userRequest = `[Resumed session context]\n${priorContext}\n\n[New goal]: ${options.goal}`;
533
+ }
534
+ // 6. Execute agent loop — streams response to stdout (Req 10.5, 10.6)
535
+ try {
536
+ const planContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "plan", db, systemPrompt);
537
+ // 7. Save to file if --output flag provided (Req 10.7)
538
+ if (options.output) {
539
+ const outputPath = path.resolve(options.output);
540
+ const outputDir = path.dirname(outputPath);
541
+ if (!existsSync(outputDir)) {
542
+ mkdirSync(outputDir, { recursive: true });
543
+ }
544
+ writeFileSync(outputPath, planContent, "utf-8");
545
+ info(`Plan saved to ${options.output}`);
546
+ }
547
+ // 8. Mark session as completed (Req 10.8)
548
+ updateSessionStatus(db, sessionId, "completed");
549
+ }
550
+ catch (err) {
551
+ const message = err instanceof Error ? err.message : String(err);
552
+ uiError(message);
553
+ updateSessionStatus(db, sessionId, "failed", message);
554
+ process.exit(1);
555
+ }
556
+ }
557
+ /**
558
+ * Execute the patch command.
559
+ *
560
+ * Flow:
561
+ * 1. Load configuration and detect project type (Req 11.1)
562
+ * 2. Create session with mode: "build" (Req 11.2)
563
+ * 3. Build system prompt from patch.md template (Req 11.3)
564
+ * 4. Expose read-only + mutation tools (Req 11.4)
565
+ * 5. Execute agent loop — agent calls propose_diff (Req 11.4, 11.5)
566
+ * 6. Render diff preview with syntax highlighting (Req 11.6)
567
+ * 7. Render mutation summary (Req 11.7)
568
+ * 8. If --dry-run, exit with code 0 (Req 11.8, 17.3, 17.4)
569
+ * 9. If not --yes, prompt for confirmation (Req 11.9, 17.5)
570
+ * 10. Agent calls apply_diff atomically (Req 11.10, 11.11, 11.12)
571
+ * 11. Log mutation to database (Req 11.11)
572
+ * 12. Display rollback hints (Req 11.12, 17.9)
573
+ * 13. Persist session to database (Req 11.13)
574
+ *
575
+ * Requirements: 11.1–11.13, 17.1–17.9
576
+ */
577
+ export async function runPatch(options) {
578
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
579
+ // 1. Load configuration (Req 11.1)
580
+ const config = getConfig(projectRoot, {
581
+ quiet: options.quiet,
582
+ });
583
+ // Apply flag overrides to config
584
+ if (options.dryRun)
585
+ config.agent.mode = "build"; // keep build mode, dryRun handled via ctx
586
+ // Initialize UI with config settings
587
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
588
+ // Detect project type early to fail fast (Req 11.1)
589
+ detectProjectType(projectRoot);
590
+ // 2. Initialize database and create session (Req 11.13)
591
+ const db = initializeDatabase();
592
+ let sessionId;
593
+ if (options.session) {
594
+ const existing = getSession(db, options.session);
595
+ if (!existing) {
596
+ uiError(`Session not found: ${options.session}`);
597
+ process.exit(1);
598
+ }
599
+ sessionId = existing.id;
600
+ info(`Resuming session ${sessionId}`);
601
+ }
602
+ else {
603
+ sessionId = randomUUID();
604
+ createSession(db, {
605
+ id: sessionId,
606
+ command: "patch",
607
+ projectRoot,
608
+ provider: config.provider.default,
609
+ model: config.provider.model,
610
+ });
611
+ }
612
+ // 3. Resolve provider (interactive setup if needed)
613
+ let provider;
614
+ try {
615
+ provider = await resolveProvider(config);
616
+ }
617
+ catch (err) {
618
+ if (err instanceof ConfirmCancelledError) {
619
+ info(yellow("Setup cancelled."));
620
+ updateSessionStatus(db, sessionId, "cancelled");
621
+ process.exit(130);
622
+ }
623
+ uiError(err instanceof Error ? err.message : String(err));
624
+ updateSessionStatus(db, sessionId, "failed", String(err));
625
+ process.exit(4);
626
+ }
627
+ // 4. Build execution context with mode: "build" (Req 11.2, 17.1, 17.2)
628
+ const ctx = {
629
+ projectRoot,
630
+ sessionId,
631
+ provider: config.provider.default,
632
+ model: config.provider.model,
633
+ mode: "build",
634
+ dryRun: Boolean(options.dryRun),
635
+ assumeYes: Boolean(options.yes),
636
+ maxIterations: config.agent.maxIterations,
637
+ timeoutSeconds: config.agent.timeoutSeconds,
638
+ };
639
+ // 5. Build system prompt from patch.md template (Req 11.3)
640
+ const systemPrompt = buildSystemPrompt("patch", ctx);
641
+ logMessage(db, sessionId, "system", systemPrompt);
642
+ // Expose read-only tools + mutation tools (Req 11.4)
643
+ const patchTools = [
644
+ readFileTool,
645
+ listDirectoryTool,
646
+ searchCodeTool,
647
+ readPackageJsonTool,
648
+ readPrismaSchemaTool,
649
+ proposeDiffTool,
650
+ applyDiffTool,
651
+ ];
652
+ if (options.dryRun) {
653
+ info(bold("Dry-run mode — changes will be previewed but not applied."));
654
+ }
655
+ // 6. Execute agent loop (Req 11.4, 11.5, 11.6, 11.7, 11.8, 11.9, 11.10)
656
+ // The agent loop handles:
657
+ // - propose_diff: generates diff + MutationSummary (Req 11.4, 11.5)
658
+ // - dry-run enforcement: skips apply_diff (Req 11.8, 17.3, 17.4)
659
+ // - confirmation prompt before apply_diff (Req 11.9, 17.5, 17.6)
660
+ // - atomic application via apply_diff (Req 11.10, 11.11, 11.12)
661
+ try {
662
+ await agentLoop(ctx, options.description, patchTools, provider, config, "patch", db, systemPrompt);
663
+ // 12. Display rollback hints after successful application (Req 11.12, 17.9)
664
+ // The agent loop streams the response which includes rollback hints from
665
+ // the apply_diff result. We add a final summary line here.
666
+ if (!options.dryRun) {
667
+ info("");
668
+ info(green("✓ Patch applied successfully."));
669
+ info(dim("Tip: use `git diff HEAD` to review changes, or `git checkout -- .` to revert."));
670
+ }
671
+ else {
672
+ info("");
673
+ info(yellow("Dry-run complete — no files were modified."));
674
+ }
675
+ // 13. Mark session as completed (Req 11.13)
676
+ updateSessionStatus(db, sessionId, "completed");
677
+ }
678
+ catch (err) {
679
+ const message = err instanceof Error ? err.message : String(err);
680
+ // Handle user cancellation (Req 17.6, exit code 130)
681
+ if (err instanceof UserCancelledError || err instanceof ConfirmCancelledError) {
682
+ info("");
683
+ info(yellow("Operation cancelled."));
684
+ updateSessionStatus(db, sessionId, "cancelled");
685
+ process.exit(130);
686
+ }
687
+ uiError(message);
688
+ updateSessionStatus(db, sessionId, "failed", message);
689
+ process.exit(1);
690
+ }
691
+ }
692
+ /**
693
+ * Read git diff based on the provided options.
694
+ *
695
+ * - Default: staged changes (`git diff --cached`)
696
+ * - --unstaged: unstaged changes (`git diff`)
697
+ * - --branch <base>: compare to base branch (`git diff <base>...HEAD`)
698
+ *
699
+ * Requirements: 12.3, 12.4, 12.5
700
+ */
701
+ function readGitDiff(options, projectRoot) {
702
+ try {
703
+ let args;
704
+ if (options.branch) {
705
+ // Compare current branch to specified base (Req 12.5)
706
+ args = ["diff", `${options.branch}...HEAD`];
707
+ }
708
+ else if (options.unstaged) {
709
+ // Unstaged changes (Req 12.4)
710
+ args = ["diff"];
711
+ }
712
+ else {
713
+ // Staged changes — default (Req 12.3)
714
+ args = ["diff", "--cached"];
715
+ }
716
+ const output = execFileSync("git", args, {
717
+ encoding: "utf-8",
718
+ cwd: projectRoot,
719
+ maxBuffer: 10 * 1024 * 1024, // 10 MB
720
+ });
721
+ return output;
722
+ }
723
+ catch (err) {
724
+ throw new Error(`Failed to read git diff: ${err instanceof Error ? err.message : String(err)}`);
725
+ }
726
+ }
727
+ /**
728
+ * Parse the structured review from the provider's markdown response.
729
+ *
730
+ * Extracts summary, issues (with severity), and suggestions from the
731
+ * "# Code Review" markdown format defined in review.md.
732
+ *
733
+ * Requirements: 12.7
734
+ */
735
+ function parseReviewResponse(content) {
736
+ const result = {
737
+ summary: "",
738
+ issues: [],
739
+ suggestions: [],
740
+ };
741
+ // Extract Summary section
742
+ const summaryMatch = content.match(/##\s+Summary\s*\n([\s\S]*?)(?=\n##|\s*$)/i);
743
+ if (summaryMatch) {
744
+ result.summary = summaryMatch[1].trim();
745
+ }
746
+ // Extract Issues section
747
+ const issuesMatch = content.match(/##\s+Issues\s*\n([\s\S]*?)(?=\n##|\s*$)/i);
748
+ if (issuesMatch) {
749
+ const issuesText = issuesMatch[1];
750
+ const issueLines = issuesText.split("\n").filter((l) => l.trim().startsWith("-"));
751
+ for (const line of issueLines) {
752
+ const severityMatch = line.match(/\[(HIGH|MEDIUM|LOW)\]\s*(.*)/i);
753
+ if (severityMatch) {
754
+ result.issues.push({
755
+ severity: severityMatch[1].toUpperCase(),
756
+ description: severityMatch[2].trim(),
757
+ });
758
+ }
759
+ else {
760
+ // Issue without explicit severity tag — treat as LOW
761
+ const text = line.replace(/^-\s*/, "").trim();
762
+ if (text) {
763
+ result.issues.push({ severity: "LOW", description: text });
764
+ }
765
+ }
766
+ }
767
+ }
768
+ // Extract Suggestions section
769
+ const suggestionsMatch = content.match(/##\s+Suggestions\s*\n([\s\S]*?)(?=\n##|\s*$)/i);
770
+ if (suggestionsMatch) {
771
+ const suggestionsText = suggestionsMatch[1];
772
+ const suggestionLines = suggestionsText
773
+ .split("\n")
774
+ .filter((l) => l.trim().startsWith("-"))
775
+ .map((l) => l.replace(/^-\s*/, "").trim())
776
+ .filter(Boolean);
777
+ result.suggestions = suggestionLines;
778
+ }
779
+ return result;
780
+ }
781
+ /**
782
+ * Render the structured review to the terminal in readable format.
783
+ *
784
+ * Requirements: 12.8
785
+ */
786
+ function renderReview(review) {
787
+ info("");
788
+ info(bold("# Code Review"));
789
+ info("");
790
+ info(bold("## Summary"));
791
+ info(review.summary || "(no summary provided)");
792
+ info("");
793
+ info(bold("## Issues"));
794
+ if (review.issues.length === 0) {
795
+ info(green(" No issues found."));
796
+ }
797
+ else {
798
+ for (const issue of review.issues) {
799
+ const severityLabel = issue.severity === "HIGH"
800
+ ? red(`[HIGH]`)
801
+ : issue.severity === "MEDIUM"
802
+ ? yellow(`[MEDIUM]`)
803
+ : cyan(`[LOW]`);
804
+ info(` - ${severityLabel} ${issue.description}`);
805
+ }
806
+ }
807
+ info("");
808
+ info(bold("## Suggestions"));
809
+ if (review.suggestions.length === 0) {
810
+ info(dim(" No suggestions."));
811
+ }
812
+ else {
813
+ for (const suggestion of review.suggestions) {
814
+ info(` - ${suggestion}`);
815
+ }
816
+ }
817
+ info("");
818
+ }
819
+ /**
820
+ * Execute the review command.
821
+ *
822
+ * Flow:
823
+ * 1. Parse flags and load configuration (Req 12.1)
824
+ * 2. Detect project type (Req 12.1)
825
+ * 3. Create session with mode: "plan" (Req 12.2)
826
+ * 4. Read git diff (staged / unstaged / branch) (Req 12.3, 12.4, 12.5)
827
+ * 5. Build system prompt from review.md template (Req 12.6)
828
+ * 6. Send diff + project context to provider (Req 12.6)
829
+ * 7. Parse structured review (summary, issues, suggestions) (Req 12.7)
830
+ * 8. Render review to terminal (Req 12.8)
831
+ * 9. Output JSON if --format json (Req 12.9)
832
+ * 10. Persist session to database (Req 12.10)
833
+ *
834
+ * Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9, 12.10
835
+ */
836
+ export async function runReview(options) {
837
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
838
+ // 1. Load configuration (Req 12.1)
839
+ const config = getConfig(projectRoot, {
840
+ quiet: options.quiet,
841
+ });
842
+ // Initialize UI with config settings
843
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
844
+ // 2. Detect project type early to fail fast (Req 12.1)
845
+ detectProjectType(projectRoot);
846
+ // 3. Initialize database and create session (Req 12.10)
847
+ const db = initializeDatabase();
848
+ const sessionId = randomUUID();
849
+ createSession(db, {
850
+ id: sessionId,
851
+ command: "review",
852
+ projectRoot,
853
+ provider: config.provider.default,
854
+ model: config.provider.model,
855
+ });
856
+ // Resolve provider (interactive setup if needed)
857
+ let provider;
858
+ try {
859
+ provider = await resolveProvider(config);
860
+ }
861
+ catch (err) {
862
+ if (err instanceof ConfirmCancelledError) {
863
+ info(yellow("Setup cancelled."));
864
+ updateSessionStatus(db, sessionId, "cancelled");
865
+ process.exit(130);
866
+ }
867
+ uiError(err instanceof Error ? err.message : String(err));
868
+ updateSessionStatus(db, sessionId, "failed", String(err));
869
+ process.exit(4);
870
+ }
871
+ // Build execution context with mode: "plan" (Req 12.2)
872
+ const ctx = {
873
+ projectRoot,
874
+ sessionId,
875
+ provider: config.provider.default,
876
+ model: config.provider.model,
877
+ mode: "plan", // review is always read-only
878
+ dryRun: false,
879
+ assumeYes: false,
880
+ maxIterations: config.agent.maxIterations,
881
+ timeoutSeconds: config.agent.timeoutSeconds,
882
+ };
883
+ // 4. Read git diff (Req 12.3, 12.4, 12.5)
884
+ let diff;
885
+ try {
886
+ diff = readGitDiff(options, projectRoot);
887
+ }
888
+ catch (err) {
889
+ const message = err instanceof Error ? err.message : String(err);
890
+ uiError(message);
891
+ updateSessionStatus(db, sessionId, "failed", message);
892
+ process.exit(1);
893
+ }
894
+ if (!diff.trim()) {
895
+ const diffSource = options.branch
896
+ ? `branch diff against ${options.branch}`
897
+ : options.unstaged
898
+ ? "unstaged changes"
899
+ : "staged changes";
900
+ info(`No ${diffSource} found. Nothing to review.`);
901
+ updateSessionStatus(db, sessionId, "completed");
902
+ return;
903
+ }
904
+ // 5. Build system prompt from review.md template (Req 12.6)
905
+ const systemPrompt = buildSystemPrompt("review", ctx);
906
+ logMessage(db, sessionId, "system", systemPrompt);
907
+ // 6. Build user request: diff + project context (Req 12.6)
908
+ const project = detectProjectType(projectRoot);
909
+ const diffSource = options.branch
910
+ ? `branch diff (current vs ${options.branch})`
911
+ : options.unstaged
912
+ ? "unstaged changes"
913
+ : "staged changes";
914
+ const userRequest = [
915
+ `Please review the following git diff (${diffSource}).`,
916
+ "",
917
+ `Project type: ${project.type}`,
918
+ project.framework ? `Framework: ${project.framework.name}` : null,
919
+ `Has Prisma: ${project.hasPrisma ? "yes" : "no"}`,
920
+ "",
921
+ "```diff",
922
+ diff,
923
+ "```",
924
+ ]
925
+ .filter((l) => l !== null)
926
+ .join("\n");
927
+ // Execute agent loop — streams response to stdout (Req 12.6, 12.7, 12.8)
928
+ try {
929
+ const reviewContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "review", db, systemPrompt);
930
+ // 7. Parse structured review (Req 12.7)
931
+ const review = parseReviewResponse(reviewContent);
932
+ // 8 & 9. Render or output JSON (Req 12.8, 12.9)
933
+ if (options.format === "json") {
934
+ // JSON output to stdout (Req 12.9)
935
+ process.stdout.write(JSON.stringify(review, null, 2) + "\n");
936
+ }
937
+ else {
938
+ // Render to terminal in readable format (Req 12.8)
939
+ // Note: agentLoop already streamed the raw response; renderReview
940
+ // provides a structured re-render for clarity.
941
+ renderReview(review);
942
+ }
943
+ // 10. Mark session as completed (Req 12.10)
944
+ updateSessionStatus(db, sessionId, "completed");
945
+ }
946
+ catch (err) {
947
+ const message = err instanceof Error ? err.message : String(err);
948
+ // Handle user cancellation (exit code 130)
949
+ if (err instanceof UserCancelledError || err instanceof ConfirmCancelledError) {
950
+ info("");
951
+ info(yellow("Operation cancelled."));
952
+ updateSessionStatus(db, sessionId, "cancelled");
953
+ process.exit(130);
954
+ }
955
+ uiError(message);
956
+ updateSessionStatus(db, sessionId, "failed", message);
957
+ process.exit(1);
958
+ }
959
+ }
960
+ /**
961
+ * Execute the explore command.
962
+ *
963
+ * Flow:
964
+ * 1. Parse flags and load configuration (Req 13.1)
965
+ * 2. Detect project type (Req 13.1)
966
+ * 3. Create session with mode: "plan" (Req 13.2)
967
+ * 4. Scan repository structure respecting .gitignore (Req 13.3)
968
+ * 5. Detect frameworks and key configuration files (Req 13.4)
969
+ * 6. Identify entry points based on project type (Req 13.5)
970
+ * 7. Build system prompt from explore.md template (Req 13.3–13.6)
971
+ * 8. Execute agent loop to summarize structure/patterns (Req 13.6)
972
+ * 9. Render exploration summary to terminal (Req 13.7)
973
+ * 10. Save to ./.aria/explore.md if --save flag (Req 13.8)
974
+ * 11. Persist session to database (Req 13.10)
975
+ *
976
+ * Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9, 13.10
977
+ */
978
+ export async function runExplore(options) {
979
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
980
+ // 1. Load configuration (Req 13.1)
981
+ const config = getConfig(projectRoot, {
982
+ quiet: options.quiet,
983
+ });
984
+ // Initialize UI with config settings
985
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
986
+ // Detect project type early to fail fast (Req 13.1)
987
+ const project = detectProjectType(projectRoot);
988
+ // 2. Initialize database and create session (Req 13.10)
989
+ const db = initializeDatabase();
990
+ const sessionId = randomUUID();
991
+ createSession(db, {
992
+ id: sessionId,
993
+ command: "explore",
994
+ projectRoot,
995
+ provider: config.provider.default,
996
+ model: config.provider.model,
997
+ });
998
+ // 3. Resolve provider (interactive setup if needed)
999
+ let provider;
1000
+ try {
1001
+ provider = await resolveProvider(config);
1002
+ }
1003
+ catch (err) {
1004
+ if (err instanceof ConfirmCancelledError) {
1005
+ info(yellow("Setup cancelled."));
1006
+ updateSessionStatus(db, sessionId, "cancelled");
1007
+ process.exit(130);
1008
+ }
1009
+ uiError(err instanceof Error ? err.message : String(err));
1010
+ updateSessionStatus(db, sessionId, "failed", String(err));
1011
+ process.exit(4);
1012
+ }
1013
+ // 4. Build execution context with mode: "plan" (Req 13.2)
1014
+ const ctx = {
1015
+ projectRoot,
1016
+ sessionId,
1017
+ provider: config.provider.default,
1018
+ model: config.provider.model,
1019
+ mode: "plan", // explore is always read-only
1020
+ dryRun: false,
1021
+ assumeYes: false,
1022
+ maxIterations: config.agent.maxIterations,
1023
+ timeoutSeconds: config.agent.timeoutSeconds,
1024
+ };
1025
+ // 5. Build system prompt from explore.md template (Req 13.3–13.6)
1026
+ const systemPrompt = buildSystemPrompt("explore", ctx);
1027
+ logMessage(db, sessionId, "system", systemPrompt);
1028
+ // 6. Build user request with project context and depth hint (Req 13.9)
1029
+ const frameworkInfo = project.framework
1030
+ ? `${project.framework.name}${project.framework.version ? ` ${project.framework.version}` : ""}${project.framework.router ? ` (${project.framework.router} router)` : ""}`
1031
+ : "none";
1032
+ const depthInstruction = options.depth !== undefined
1033
+ ? `Limit directory traversal to a maximum depth of ${options.depth}.`
1034
+ : "Use a reasonable depth to cover the top-level structure.";
1035
+ const userRequest = [
1036
+ `Explore this ${project.type} repository and produce a structured summary.`,
1037
+ "",
1038
+ `Project root: ${projectRoot}`,
1039
+ `Project type: ${project.type}`,
1040
+ `Framework: ${frameworkInfo}`,
1041
+ `Has Prisma: ${project.hasPrisma ? "yes" : "no"}`,
1042
+ project.packageManager ? `Package manager: ${project.packageManager}` : null,
1043
+ "",
1044
+ depthInstruction,
1045
+ "",
1046
+ "Use list_directory, read_file, search_code, and read_package_json to scan the",
1047
+ "repository. Identify entry points, key configuration files, and notable patterns.",
1048
+ "Return your findings in the structured markdown format defined in your instructions.",
1049
+ ]
1050
+ .filter((l) => l !== null)
1051
+ .join("\n");
1052
+ // 7. Execute agent loop — streams response to stdout (Req 13.6, 13.7)
1053
+ try {
1054
+ const exploreContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "explore", db, systemPrompt);
1055
+ // 8. Save to ./.aria/explore.md if --save flag provided (Req 13.8)
1056
+ if (options.save) {
1057
+ const ariaDir = path.join(projectRoot, ".aria");
1058
+ const savePath = path.join(ariaDir, "explore.md");
1059
+ if (!existsSync(ariaDir)) {
1060
+ mkdirSync(ariaDir, { recursive: true });
1061
+ }
1062
+ writeFileSync(savePath, exploreContent, "utf-8");
1063
+ info(`Exploration summary saved to .aria/explore.md`);
1064
+ }
1065
+ // 9. Mark session as completed (Req 13.10)
1066
+ updateSessionStatus(db, sessionId, "completed");
1067
+ }
1068
+ catch (err) {
1069
+ const message = err instanceof Error ? err.message : String(err);
1070
+ if (err instanceof UserCancelledError || err instanceof ConfirmCancelledError) {
1071
+ info("");
1072
+ info(yellow("Operation cancelled."));
1073
+ updateSessionStatus(db, sessionId, "cancelled");
1074
+ process.exit(130);
1075
+ }
1076
+ uiError(message);
1077
+ updateSessionStatus(db, sessionId, "failed", message);
1078
+ process.exit(1);
1079
+ }
1080
+ }
1081
+ /**
1082
+ * Format a SQLite timestamp string into a human-readable relative time.
1083
+ * e.g. "2 hours ago", "3 days ago", "just now"
1084
+ *
1085
+ * Requirements: 14.7
1086
+ */
1087
+ function formatTimestamp(timestamp) {
1088
+ const date = new Date(timestamp.endsWith("Z") ? timestamp : timestamp + "Z");
1089
+ const now = Date.now();
1090
+ const diffMs = now - date.getTime();
1091
+ const diffSec = Math.floor(diffMs / 1000);
1092
+ if (diffSec < 60)
1093
+ return "just now";
1094
+ if (diffSec < 3600) {
1095
+ const m = Math.floor(diffSec / 60);
1096
+ return `${m} minute${m !== 1 ? "s" : ""} ago`;
1097
+ }
1098
+ if (diffSec < 86400) {
1099
+ const h = Math.floor(diffSec / 3600);
1100
+ return `${h} hour${h !== 1 ? "s" : ""} ago`;
1101
+ }
1102
+ if (diffSec < 86400 * 30) {
1103
+ const d = Math.floor(diffSec / 86400);
1104
+ return `${d} day${d !== 1 ? "s" : ""} ago`;
1105
+ }
1106
+ // Fall back to locale date string for older entries
1107
+ return date.toLocaleDateString(undefined, {
1108
+ year: "numeric",
1109
+ month: "short",
1110
+ day: "numeric",
1111
+ });
1112
+ }
1113
+ /**
1114
+ * Colorize a session status string.
1115
+ */
1116
+ function colorizeStatus(status) {
1117
+ switch (status) {
1118
+ case "completed":
1119
+ return green(status);
1120
+ case "failed":
1121
+ return red(status);
1122
+ case "cancelled":
1123
+ return yellow(status);
1124
+ case "running":
1125
+ return cyan(status);
1126
+ default:
1127
+ return status;
1128
+ }
1129
+ }
1130
+ /**
1131
+ * Render a tool execution tree for a session.
1132
+ * Shows tool calls in chronological order with input/output summaries.
1133
+ *
1134
+ * Requirements: 14.6
1135
+ */
1136
+ function renderToolTree(db, sessionId) {
1137
+ const executions = db
1138
+ .prepare(`SELECT tool_name, input, output, error, created_at
1139
+ FROM tool_executions
1140
+ WHERE session_id = ?
1141
+ ORDER BY created_at ASC`)
1142
+ .all(sessionId);
1143
+ if (executions.length === 0) {
1144
+ info(dim(" (no tool executions recorded)"));
1145
+ return;
1146
+ }
1147
+ for (let i = 0; i < executions.length; i++) {
1148
+ const exec = executions[i];
1149
+ const isLast = i === executions.length - 1;
1150
+ const prefix = isLast ? "└─" : "├─";
1151
+ const childPrefix = isLast ? " " : "│ ";
1152
+ const statusIcon = exec.error ? red("✗") : green("✓");
1153
+ info(` ${prefix} ${statusIcon} ${bold(exec.tool_name)} ${dim(formatTimestamp(exec.created_at))}`);
1154
+ // Show a brief summary of the input
1155
+ try {
1156
+ const inputObj = JSON.parse(exec.input);
1157
+ const inputSummary = Object.entries(inputObj)
1158
+ .slice(0, 2)
1159
+ .map(([k, v]) => `${k}=${JSON.stringify(v).slice(0, 40)}`)
1160
+ .join(", ");
1161
+ if (inputSummary) {
1162
+ info(` ${childPrefix} ${dim("in:")} ${dim(inputSummary)}`);
1163
+ }
1164
+ }
1165
+ catch {
1166
+ // ignore parse errors
1167
+ }
1168
+ if (exec.error) {
1169
+ info(` ${childPrefix} ${red("err:")} ${exec.error.slice(0, 80)}`);
1170
+ }
1171
+ else if (exec.output) {
1172
+ try {
1173
+ const outputStr = JSON.stringify(JSON.parse(exec.output)).slice(0, 80);
1174
+ info(` ${childPrefix} ${dim("out:")} ${dim(outputStr)}`);
1175
+ }
1176
+ catch {
1177
+ info(` ${childPrefix} ${dim("out:")} ${dim(exec.output.slice(0, 80))}`);
1178
+ }
1179
+ }
1180
+ }
1181
+ }
1182
+ /**
1183
+ * Execute the history command.
1184
+ *
1185
+ * Flow:
1186
+ * 1. If no --session flag: list recent sessions in a table (Req 14.2, 14.3, 14.4)
1187
+ * 2. If --session flag: display full session log (Req 14.5)
1188
+ * 3. If --tree flag: render tool execution tree (Req 14.6)
1189
+ * 4. Format timestamps in human-readable format (Req 14.7)
1190
+ * 5. Support pagination for large result sets (Req 14.8)
1191
+ *
1192
+ * Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.8
1193
+ */
1194
+ export async function runHistory(options) {
1195
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
1196
+ // Load configuration and initialize UI
1197
+ const config = getConfig(projectRoot, { quiet: options.quiet });
1198
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
1199
+ // Initialize database
1200
+ const db = initializeDatabase();
1201
+ // ---------------------------------------------------------------------------
1202
+ // Case 1: --session flag — show full session log (Req 14.5)
1203
+ // ---------------------------------------------------------------------------
1204
+ if (options.session) {
1205
+ const session = getSession(db, options.session);
1206
+ if (!session) {
1207
+ uiError(`Session not found: ${options.session}`);
1208
+ process.exit(1);
1209
+ }
1210
+ // Session header
1211
+ info("");
1212
+ info(bold(`Session: ${session.id}`));
1213
+ info(` Command: ${cyan(session.command)}`);
1214
+ info(` Status: ${colorizeStatus(session.status)}`);
1215
+ info(` Started: ${formatTimestamp(session.createdAt)}`);
1216
+ if (session.completedAt) {
1217
+ info(` Completed: ${formatTimestamp(session.completedAt)}`);
1218
+ }
1219
+ if (session.error) {
1220
+ info(` Error: ${red(session.error)}`);
1221
+ }
1222
+ info(` Project: ${dim(session.projectRoot)}`);
1223
+ info(` Provider: ${session.provider} / ${session.model}`);
1224
+ info("");
1225
+ // Messages log
1226
+ const messages = db
1227
+ .prepare(`SELECT role, content, created_at
1228
+ FROM messages
1229
+ WHERE session_id = ?
1230
+ ORDER BY created_at ASC`)
1231
+ .all(options.session);
1232
+ if (messages.length > 0) {
1233
+ info(bold("Messages:"));
1234
+ for (const msg of messages) {
1235
+ const roleLabel = msg.role === "user"
1236
+ ? cyan("[user]")
1237
+ : msg.role === "assistant"
1238
+ ? green("[assistant]")
1239
+ : dim("[system]");
1240
+ const timestamp = dim(formatTimestamp(msg.created_at));
1241
+ info(` ${roleLabel} ${timestamp}`);
1242
+ // Truncate very long messages for readability
1243
+ const preview = msg.content.length > 300
1244
+ ? msg.content.slice(0, 300) + dim("…")
1245
+ : msg.content;
1246
+ // Indent content lines
1247
+ for (const line of preview.split("\n").slice(0, 10)) {
1248
+ info(` ${line}`);
1249
+ }
1250
+ info("");
1251
+ }
1252
+ }
1253
+ // Tool executions
1254
+ const toolCount = db
1255
+ .prepare(`SELECT COUNT(*) as count FROM tool_executions WHERE session_id = ?`)
1256
+ .get(options.session).count;
1257
+ if (toolCount > 0) {
1258
+ info(bold(`Tool Executions (${toolCount}):`));
1259
+ if (options.tree) {
1260
+ // Render as tree (Req 14.6)
1261
+ renderToolTree(db, options.session);
1262
+ }
1263
+ else {
1264
+ // Render as flat list
1265
+ const executions = db
1266
+ .prepare(`SELECT tool_name, input, output, error, created_at
1267
+ FROM tool_executions
1268
+ WHERE session_id = ?
1269
+ ORDER BY created_at ASC`)
1270
+ .all(options.session);
1271
+ for (const exec of executions) {
1272
+ const statusIcon = exec.error ? red("✗") : green("✓");
1273
+ info(` ${statusIcon} ${bold(exec.tool_name)} ${dim(formatTimestamp(exec.created_at))}`);
1274
+ }
1275
+ }
1276
+ info("");
1277
+ }
1278
+ return;
1279
+ }
1280
+ // ---------------------------------------------------------------------------
1281
+ // Case 2: No --session flag — list recent sessions (Req 14.2, 14.3, 14.4)
1282
+ // ---------------------------------------------------------------------------
1283
+ const PAGE_SIZE = 20;
1284
+ const limit = options.limit ?? PAGE_SIZE;
1285
+ // Fetch sessions with pagination support (Req 14.8)
1286
+ const sessions = listSessions(db, { limit });
1287
+ if (sessions.length === 0) {
1288
+ info("No sessions found. Run a command to create your first session.");
1289
+ return;
1290
+ }
1291
+ // Build table rows (Req 14.3)
1292
+ const rows = sessions.map((s) => [
1293
+ dim(s.id.slice(0, 8)), // abbreviated ID
1294
+ cyan(s.command),
1295
+ formatTimestamp(s.createdAt), // human-readable timestamp (Req 14.7)
1296
+ colorizeStatus(s.status),
1297
+ ]);
1298
+ // Render table with cli-table3 (Req 14.3, 20.4)
1299
+ const table = renderTable({
1300
+ head: ["ID", "Command", "When", "Status"],
1301
+ colWidths: [12, 12, 20, 12],
1302
+ }, rows);
1303
+ info(table);
1304
+ // Show pagination hint if there may be more results (Req 14.8)
1305
+ if (sessions.length === limit && !options.limit) {
1306
+ info(dim(`\nShowing ${limit} most recent sessions. Use --limit <n> to see more.`));
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Serialize a Config object to TOML format.
1311
+ * Produces a minimal TOML representation suitable for ~/.aria/config.toml.
1312
+ */
1313
+ function serializeConfigToToml(config) {
1314
+ const lines = [];
1315
+ lines.push("[provider]");
1316
+ lines.push(`default = "${config.provider.default}"`);
1317
+ lines.push(`model = "${config.provider.model}"`);
1318
+ lines.push(`max_tokens = ${config.provider.maxTokens}`);
1319
+ lines.push("");
1320
+ lines.push("[agent]");
1321
+ lines.push(`max_iterations = ${config.agent.maxIterations}`);
1322
+ lines.push(`mode = "${config.agent.mode}"`);
1323
+ lines.push(`timeout_seconds = ${config.agent.timeoutSeconds}`);
1324
+ lines.push("");
1325
+ lines.push("[safety]");
1326
+ lines.push(`require_confirm_for_shell = ${config.safety.requireConfirmForShell}`);
1327
+ const cmds = config.safety.allowedShellCommands.map((c) => `"${c}"`).join(", ");
1328
+ lines.push(`allowed_shell_commands = [${cmds}]`);
1329
+ lines.push(`max_file_size_kb = ${config.safety.maxFileSizeKb}`);
1330
+ lines.push(`max_files_per_patch = ${config.safety.maxFilesPerPatch}`);
1331
+ lines.push("");
1332
+ lines.push("[ui]");
1333
+ lines.push(`color = "${config.ui.color}"`);
1334
+ lines.push(`quiet = ${config.ui.quiet}`);
1335
+ lines.push("");
1336
+ lines.push("[history]");
1337
+ lines.push(`retain_days = ${config.history.retainDays}`);
1338
+ lines.push("");
1339
+ return lines.join("\n");
1340
+ }
1341
+ /**
1342
+ * Get a nested value from a Config object using dot-notation key.
1343
+ * e.g. "provider.model" → config.provider.model
1344
+ */
1345
+ function getConfigValue(config, key) {
1346
+ const parts = key.split(".");
1347
+ let current = config;
1348
+ for (const part of parts) {
1349
+ if (current === null || typeof current !== "object")
1350
+ return undefined;
1351
+ current = current[part];
1352
+ }
1353
+ return current;
1354
+ }
1355
+ /**
1356
+ * Set a nested value in a plain object using dot-notation key.
1357
+ * Returns a new object with the value set.
1358
+ */
1359
+ function setNestedValue(obj, key, value) {
1360
+ const parts = key.split(".");
1361
+ const result = { ...obj };
1362
+ let current = result;
1363
+ for (let i = 0; i < parts.length - 1; i++) {
1364
+ const part = parts[i];
1365
+ current[part] = { ...(current[part] ?? {}) };
1366
+ current = current[part];
1367
+ }
1368
+ current[parts[parts.length - 1]] = value;
1369
+ return result;
1370
+ }
1371
+ /**
1372
+ * Parse a string value into the appropriate type for a config key.
1373
+ * Handles booleans, numbers, and strings.
1374
+ */
1375
+ function parseConfigValue(value) {
1376
+ if (value === "true")
1377
+ return true;
1378
+ if (value === "false")
1379
+ return false;
1380
+ const num = Number(value);
1381
+ if (!isNaN(num) && value.trim() !== "")
1382
+ return num;
1383
+ return value;
1384
+ }
1385
+ /**
1386
+ * Display the effective configuration with precedence sources.
1387
+ * Requirements: 15.2
1388
+ */
1389
+ function displayEffectiveConfig(projectRoot, config) {
1390
+ const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
1391
+ const projectConfigPath = path.join(projectRoot, ".aria.toml");
1392
+ info(bold("Effective Configuration"));
1393
+ info("");
1394
+ info(dim(`Sources (highest to lowest precedence):`));
1395
+ info(dim(` 1. CLI flags`));
1396
+ info(dim(` 2. Environment variables`));
1397
+ info(dim(` 3. Project config: ${projectConfigPath}${existsSync(projectConfigPath) ? green(" (found)") : yellow(" (not found)")}`));
1398
+ info(dim(` 4. User config: ${userConfigPath}${existsSync(userConfigPath) ? green(" (found)") : yellow(" (not found)")}`));
1399
+ info(dim(` 5. Defaults`));
1400
+ info("");
1401
+ info(bold("[provider]"));
1402
+ info(` default = ${cyan(config.provider.default)}`);
1403
+ info(` model = ${cyan(config.provider.model)}`);
1404
+ info(` max_tokens = ${cyan(String(config.provider.maxTokens))}`);
1405
+ info("");
1406
+ info(bold("[agent]"));
1407
+ info(` max_iterations = ${cyan(String(config.agent.maxIterations))}`);
1408
+ info(` mode = ${cyan(config.agent.mode)}`);
1409
+ info(` timeout_seconds = ${cyan(String(config.agent.timeoutSeconds))}`);
1410
+ info("");
1411
+ info(bold("[safety]"));
1412
+ info(` require_confirm_for_shell = ${cyan(String(config.safety.requireConfirmForShell))}`);
1413
+ info(` allowed_shell_commands = ${cyan(JSON.stringify(config.safety.allowedShellCommands))}`);
1414
+ info(` max_file_size_kb = ${cyan(String(config.safety.maxFileSizeKb))}`);
1415
+ info(` max_files_per_patch = ${cyan(String(config.safety.maxFilesPerPatch))}`);
1416
+ info("");
1417
+ info(bold("[ui]"));
1418
+ info(` color = ${cyan(config.ui.color)}`);
1419
+ info(` quiet = ${cyan(String(config.ui.quiet))}`);
1420
+ info("");
1421
+ info(bold("[history]"));
1422
+ info(` retain_days = ${cyan(String(config.history.retainDays))}`);
1423
+ }
1424
+ /**
1425
+ * Execute the config command.
1426
+ *
1427
+ * Subcommands:
1428
+ * - (none): Display effective configuration with precedence sources (Req 15.2)
1429
+ * - get <key>: Display value for specified key (Req 15.3)
1430
+ * - set <key> <value>: Write key-value to ~/.aria/config.toml (Req 15.4–15.6, 15.10)
1431
+ * - path: Display configuration file resolution paths (Req 15.7)
1432
+ * - init: Create ./.aria.toml with default values (Req 15.8, 15.9)
1433
+ *
1434
+ * Requirements: 15.1–15.10
1435
+ */
1436
+ export async function runConfig(options) {
1437
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
1438
+ // Load configuration and initialize UI
1439
+ const config = getConfig(projectRoot, { quiet: options.quiet });
1440
+ initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
1441
+ const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
1442
+ const projectConfigPath = path.join(projectRoot, ".aria.toml");
1443
+ // ---------------------------------------------------------------------------
1444
+ // No subcommand: display effective configuration (Req 15.2)
1445
+ // ---------------------------------------------------------------------------
1446
+ if (!options.subcommand) {
1447
+ displayEffectiveConfig(projectRoot, config);
1448
+ return;
1449
+ }
1450
+ // ---------------------------------------------------------------------------
1451
+ // config path: display config file resolution paths (Req 15.7)
1452
+ // ---------------------------------------------------------------------------
1453
+ if (options.subcommand === "path") {
1454
+ info(bold("Configuration file paths:"));
1455
+ info("");
1456
+ info(` User config: ${userConfigPath}`);
1457
+ info(` ${existsSync(userConfigPath) ? green("✓ exists") : yellow("✗ not found")}`);
1458
+ info("");
1459
+ info(` Project config: ${projectConfigPath}`);
1460
+ info(` ${existsSync(projectConfigPath) ? green("✓ exists") : yellow("✗ not found")}`);
1461
+ return;
1462
+ }
1463
+ // ---------------------------------------------------------------------------
1464
+ // config get <key>: display value for key (Req 15.3)
1465
+ // ---------------------------------------------------------------------------
1466
+ if (options.subcommand === "get") {
1467
+ const key = options.key;
1468
+ const value = getConfigValue(config, key);
1469
+ if (value === undefined) {
1470
+ uiError(`Unknown configuration key: ${key}`);
1471
+ process.exit(1);
1472
+ }
1473
+ print(JSON.stringify(value));
1474
+ return;
1475
+ }
1476
+ // ---------------------------------------------------------------------------
1477
+ // config set <key> <value>: write to user config (Req 15.4–15.6, 15.10)
1478
+ // ---------------------------------------------------------------------------
1479
+ if (options.subcommand === "set") {
1480
+ const key = options.key;
1481
+ const rawValue = options.value;
1482
+ // Parse the value to the appropriate type
1483
+ const parsedValue = parseConfigValue(rawValue);
1484
+ // Validate by applying to current config and re-validating (Req 15.10)
1485
+ const currentMerged = loadConfig(projectRoot);
1486
+ const updatedMerged = setNestedValue(currentMerged, key, parsedValue);
1487
+ let validatedConfig;
1488
+ try {
1489
+ validatedConfig = validateConfig(updatedMerged);
1490
+ }
1491
+ catch (err) {
1492
+ uiError(`Invalid value for ${key}: ${err instanceof Error ? err.message : String(err)}`);
1493
+ process.exit(3);
1494
+ }
1495
+ // Serialize the full validated config for the diff preview
1496
+ const oldContent = existsSync(userConfigPath)
1497
+ ? readFileSync(userConfigPath, "utf-8")
1498
+ : "";
1499
+ const newContent = serializeConfigToToml(validatedConfig);
1500
+ // Preview the diff (Req 15.5, 15.6)
1501
+ const diffOutput = generateAndRenderDiff(userConfigPath, oldContent, newContent);
1502
+ info(bold("Preview:"));
1503
+ info(diffOutput);
1504
+ // If --dry-run, exit without writing (Req 15.6)
1505
+ if (options.dryRun) {
1506
+ info(yellow("Dry-run mode — no changes written."));
1507
+ return;
1508
+ }
1509
+ // If not --yes, prompt for confirmation (Req 15.5)
1510
+ if (!options.yes) {
1511
+ let confirmed;
1512
+ try {
1513
+ confirmed = await confirm(`Write to ${userConfigPath}?`);
1514
+ }
1515
+ catch (err) {
1516
+ if (err instanceof ConfirmCancelledError) {
1517
+ info(yellow("Operation cancelled."));
1518
+ process.exit(130);
1519
+ }
1520
+ throw err;
1521
+ }
1522
+ if (!confirmed) {
1523
+ info(yellow("Operation cancelled."));
1524
+ process.exit(130);
1525
+ }
1526
+ }
1527
+ // Write to ~/.aria/config.toml (Req 15.4)
1528
+ const ariaDir = path.join(os.homedir(), ".aria");
1529
+ if (!existsSync(ariaDir)) {
1530
+ mkdirSync(ariaDir, { recursive: true });
1531
+ }
1532
+ writeFileSync(userConfigPath, newContent, { encoding: "utf-8", mode: 0o600 });
1533
+ info(green(`✓ Written to ${userConfigPath}`));
1534
+ return;
1535
+ }
1536
+ // ---------------------------------------------------------------------------
1537
+ // config init: create ./.aria.toml with defaults (Req 15.8, 15.9)
1538
+ // ---------------------------------------------------------------------------
1539
+ if (options.subcommand === "init") {
1540
+ // Generate default config content
1541
+ const defaultConfig = validateConfig({});
1542
+ const defaultContent = serializeConfigToToml(defaultConfig);
1543
+ // Preview content
1544
+ const oldContent = existsSync(projectConfigPath)
1545
+ ? readFileSync(projectConfigPath, "utf-8")
1546
+ : "";
1547
+ const diffOutput = generateAndRenderDiff(projectConfigPath, oldContent, defaultContent);
1548
+ info(bold("Preview (.aria.toml):"));
1549
+ info(diffOutput);
1550
+ // If --dry-run, exit without writing (Req 17.4)
1551
+ if (options.dryRun) {
1552
+ info(yellow("Dry-run mode — no file created."));
1553
+ return;
1554
+ }
1555
+ // If not --yes, prompt for confirmation (Req 15.9)
1556
+ if (!options.yes) {
1557
+ let confirmed;
1558
+ try {
1559
+ confirmed = await confirm(`Create ${projectConfigPath}?`);
1560
+ }
1561
+ catch (err) {
1562
+ if (err instanceof ConfirmCancelledError) {
1563
+ info(yellow("Operation cancelled."));
1564
+ process.exit(130);
1565
+ }
1566
+ throw err;
1567
+ }
1568
+ if (!confirmed) {
1569
+ info(yellow("Operation cancelled."));
1570
+ process.exit(130);
1571
+ }
1572
+ }
1573
+ // Write ./.aria.toml (Req 15.8)
1574
+ writeFileSync(projectConfigPath, defaultContent, "utf-8");
1575
+ info(green(`✓ Created ${projectConfigPath}`));
1576
+ return;
1577
+ }
1578
+ }
1579
+ /**
1580
+ * Execute the doctor command.
1581
+ *
1582
+ * Runs a series of environment diagnostic checks and reports results.
1583
+ * Exits with code 1 if any critical check fails.
1584
+ *
1585
+ * Requirements: 16.1–16.13
1586
+ */
1587
+ export async function runDoctor(options = {}) {
1588
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
1589
+ // Initialize UI (best-effort — config may be broken, that's what we're diagnosing)
1590
+ let config = null;
1591
+ try {
1592
+ config = getConfig(projectRoot, {});
1593
+ initUI(config.ui.color, false);
1594
+ }
1595
+ catch {
1596
+ initUI("auto", false);
1597
+ }
1598
+ const checks = [];
1599
+ // -------------------------------------------------------------------------
1600
+ // 1. Node.js version >= 20 (Req 16.2) — CRITICAL
1601
+ // -------------------------------------------------------------------------
1602
+ {
1603
+ const nodeVersion = process.version; // e.g. "v20.11.0"
1604
+ const major = parseInt(nodeVersion.slice(1).split(".")[0], 10);
1605
+ if (major >= 20) {
1606
+ checks.push({ name: "nodejs", status: "pass", message: nodeVersion });
1607
+ }
1608
+ else {
1609
+ checks.push({
1610
+ name: "nodejs",
1611
+ status: "fail",
1612
+ message: `${nodeVersion} (requires >= v20)`,
1613
+ });
1614
+ }
1615
+ }
1616
+ // -------------------------------------------------------------------------
1617
+ // 2. git availability (Req 16.3) — WARN only
1618
+ // -------------------------------------------------------------------------
1619
+ {
1620
+ try {
1621
+ const out = execFileSync("git", ["--version"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1622
+ checks.push({ name: "git", status: "pass", message: out });
1623
+ }
1624
+ catch {
1625
+ checks.push({ name: "git", status: "warn", message: "not found in PATH" });
1626
+ }
1627
+ }
1628
+ // -------------------------------------------------------------------------
1629
+ // 3. ripgrep (rg) availability (Req 16.4) — WARN only
1630
+ // -------------------------------------------------------------------------
1631
+ {
1632
+ try {
1633
+ const out = execFileSync("rg", ["--version"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).split("\n")[0].trim();
1634
+ checks.push({ name: "ripgrep", status: "pass", message: out });
1635
+ }
1636
+ catch {
1637
+ checks.push({ name: "ripgrep", status: "warn", message: "not found in PATH (search_code tool will be unavailable)" });
1638
+ }
1639
+ }
1640
+ // -------------------------------------------------------------------------
1641
+ // 4. Config file syntax and schema validation (Req 16.5) — CRITICAL
1642
+ // -------------------------------------------------------------------------
1643
+ {
1644
+ const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
1645
+ const projectConfigPath = path.join(projectRoot, ".aria.toml");
1646
+ if (!existsSync(userConfigPath) && !existsSync(projectConfigPath)) {
1647
+ checks.push({ name: "config", status: "pass", message: "no config files found (using defaults)" });
1648
+ }
1649
+ else {
1650
+ try {
1651
+ getConfig(projectRoot, {});
1652
+ const found = [
1653
+ existsSync(userConfigPath) ? "~/.aria/config.toml" : null,
1654
+ existsSync(projectConfigPath) ? ".aria.toml" : null,
1655
+ ].filter(Boolean).join(", ");
1656
+ checks.push({ name: "config", status: "pass", message: `valid (${found})` });
1657
+ }
1658
+ catch (err) {
1659
+ checks.push({
1660
+ name: "config",
1661
+ status: "fail",
1662
+ message: err instanceof Error ? err.message : String(err),
1663
+ });
1664
+ }
1665
+ }
1666
+ }
1667
+ // -------------------------------------------------------------------------
1668
+ // 5. History DB accessibility and schema version (Req 16.6) — CRITICAL
1669
+ // -------------------------------------------------------------------------
1670
+ {
1671
+ try {
1672
+ const db = initializeDatabase();
1673
+ const { getCurrentSchemaVersion } = await import("./storage.js"); // lazy: only needed here
1674
+ const version = getCurrentSchemaVersion(db);
1675
+ checks.push({ name: "history_db", status: "pass", message: `accessible (schema v${version})` });
1676
+ }
1677
+ catch (err) {
1678
+ checks.push({
1679
+ name: "history_db",
1680
+ status: "fail",
1681
+ message: err instanceof Error ? err.message : String(err),
1682
+ });
1683
+ }
1684
+ }
1685
+ // -------------------------------------------------------------------------
1686
+ // 6. Provider readiness — API key presence (Req 16.7) — CRITICAL
1687
+ // -------------------------------------------------------------------------
1688
+ {
1689
+ const provider = config?.provider.default ?? "anthropic";
1690
+ const keyMap = {
1691
+ anthropic: "ANTHROPIC_API_KEY",
1692
+ openai: "OPENAI_API_KEY",
1693
+ openrouter: "OPENROUTER_API_KEY",
1694
+ ollama: "", // no key needed
1695
+ };
1696
+ const envKey = keyMap[provider];
1697
+ if (!envKey) {
1698
+ // Ollama — no API key required
1699
+ checks.push({ name: "provider", status: "pass", message: `${provider} (no API key required)` });
1700
+ }
1701
+ else if (process.env[envKey]) {
1702
+ checks.push({ name: "provider", status: "pass", message: `${provider} (${envKey} present)` });
1703
+ }
1704
+ else {
1705
+ checks.push({
1706
+ name: "provider",
1707
+ status: "fail",
1708
+ message: `${provider} (${envKey} not set)`,
1709
+ });
1710
+ }
1711
+ }
1712
+ // -------------------------------------------------------------------------
1713
+ // 7. Project type detection (Req 16.8)
1714
+ // -------------------------------------------------------------------------
1715
+ {
1716
+ try {
1717
+ const project = detectProjectType(projectRoot);
1718
+ const frameworkLabel = project.framework
1719
+ ? ` (${project.framework.name}${project.framework.router ? ` ${project.framework.router} router` : ""})`
1720
+ : "";
1721
+ checks.push({ name: "project", status: "pass", message: `${project.type}${frameworkLabel}` });
1722
+ }
1723
+ catch (err) {
1724
+ checks.push({
1725
+ name: "project",
1726
+ status: "fail",
1727
+ message: err instanceof Error ? err.message : String(err),
1728
+ });
1729
+ }
1730
+ }
1731
+ // -------------------------------------------------------------------------
1732
+ // 8. Prisma schema existence if Prisma detected (Req 16.9)
1733
+ // -------------------------------------------------------------------------
1734
+ {
1735
+ try {
1736
+ const project = detectProjectType(projectRoot);
1737
+ if (project.hasPrisma) {
1738
+ if (project.prismaSchemaPath && existsSync(project.prismaSchemaPath)) {
1739
+ checks.push({ name: "prisma", status: "pass", message: `detected at ${project.prismaSchemaPath}` });
1740
+ }
1741
+ else {
1742
+ checks.push({ name: "prisma", status: "warn", message: "dependency detected but prisma/schema.prisma not found" });
1743
+ }
1744
+ }
1745
+ // If no Prisma, skip this check entirely
1746
+ }
1747
+ catch {
1748
+ // project detection already reported above
1749
+ }
1750
+ }
1751
+ // -------------------------------------------------------------------------
1752
+ // 9. Ollama reachability if Ollama provider selected (Req 16.10) — WARN
1753
+ // -------------------------------------------------------------------------
1754
+ {
1755
+ const provider = config?.provider.default ?? "anthropic";
1756
+ if (provider === "ollama") {
1757
+ const ollamaHost = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
1758
+ try {
1759
+ const controller = new AbortController();
1760
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
1761
+ const res = await fetch(ollamaHost, { signal: controller.signal });
1762
+ clearTimeout(timeoutId);
1763
+ checks.push({ name: "ollama", status: "pass", message: `reachable at ${ollamaHost} (HTTP ${res.status})` });
1764
+ }
1765
+ catch {
1766
+ checks.push({ name: "ollama", status: "warn", message: `not reachable at ${ollamaHost}` });
1767
+ }
1768
+ }
1769
+ }
1770
+ // -------------------------------------------------------------------------
1771
+ // Output results
1772
+ // -------------------------------------------------------------------------
1773
+ const criticalNames = new Set(["nodejs", "config", "history_db", "provider"]);
1774
+ const hasCriticalFailure = checks.some((c) => c.status === "fail" && criticalNames.has(c.name));
1775
+ if (options.format === "json") {
1776
+ // JSON output (Req 16.12)
1777
+ const allPassed = !checks.some((c) => c.status === "fail");
1778
+ process.stdout.write(JSON.stringify({ checks, allPassed }, null, 2) + "\n");
1779
+ }
1780
+ else {
1781
+ // Text output (Req 16.11)
1782
+ info("");
1783
+ info(bold("Aria environment diagnostics"));
1784
+ info("");
1785
+ for (const check of checks) {
1786
+ if (check.status === "pass") {
1787
+ info(`${green("✓")} ${bold(check.name)}: ${check.message}`);
1788
+ }
1789
+ else if (check.status === "warn") {
1790
+ info(`${yellow("!")} ${bold(check.name)}: ${check.message}`);
1791
+ }
1792
+ else {
1793
+ info(`${red("✗")} ${bold(check.name)}: ${check.message}`);
1794
+ }
1795
+ }
1796
+ info("");
1797
+ if (hasCriticalFailure) {
1798
+ info(red("One or more critical checks failed. Please fix the issues above."));
1799
+ }
1800
+ else {
1801
+ info(green("All critical checks passed."));
1802
+ }
1803
+ }
1804
+ // Exit with code 1 if any critical check fails (Req 16.13)
1805
+ if (hasCriticalFailure) {
1806
+ process.exit(1);
1807
+ }
1808
+ }
1809
+ //# sourceMappingURL=actions.js.map