@ariacode/cli 0.2.1 → 0.2.3
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.
- package/README.md +78 -11
- package/dist/actions/db-ask.d.ts +7 -1
- package/dist/actions/db-ask.js +27 -10
- package/dist/actions/db-ask.js.map +1 -1
- package/dist/actions/db-explain.d.ts +6 -0
- package/dist/actions/db-explain.js +25 -7
- package/dist/actions/db-explain.js.map +1 -1
- package/dist/actions/db-migrate.d.ts +4 -0
- package/dist/actions/db-migrate.js +2 -2
- package/dist/actions/db-migrate.js.map +1 -1
- package/dist/actions/db-schema.d.ts +3 -1
- package/dist/actions/db-schema.js +18 -5
- package/dist/actions/db-schema.js.map +1 -1
- package/dist/actions/upgrade-deps.d.ts +6 -0
- package/dist/actions/upgrade-deps.js +20 -2
- package/dist/actions/upgrade-deps.js.map +1 -1
- package/dist/actions/upgrade-prisma.d.ts +4 -0
- package/dist/actions/upgrade-prisma.js +2 -2
- package/dist/actions/upgrade-prisma.js.map +1 -1
- package/dist/actions.d.ts +106 -71
- package/dist/actions.js +438 -204
- package/dist/actions.js.map +1 -1
- package/dist/agent.d.ts +0 -1
- package/dist/agent.js +1 -2
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts +3 -3
- package/dist/app.js +3 -3
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +37 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +17 -1
- package/dist/config.js +40 -1
- package/dist/config.js.map +1 -1
- package/dist/output/schemas.d.ts +92 -0
- package/dist/output/schemas.js +142 -0
- package/dist/output/schemas.js.map +1 -0
- package/dist/parser.d.ts +10 -3
- package/dist/parser.js +70 -8
- package/dist/parser.js.map +1 -1
- package/dist/provider.d.ts +7 -2
- package/dist/provider.js +6 -4
- package/dist/provider.js.map +1 -1
- package/dist/repo.d.ts +0 -1
- package/dist/repo.js +6 -9
- package/dist/repo.js.map +1 -1
- package/dist/safety.d.ts +0 -4
- package/dist/safety.js +0 -4
- package/dist/safety.js.map +1 -1
- package/dist/storage/queries.d.ts +39 -0
- package/dist/storage/queries.js +211 -0
- package/dist/storage/queries.js.map +1 -0
- package/dist/tools.d.ts +6 -7
- package/dist/tools.js +20 -7
- package/dist/tools.js.map +1 -1
- package/dist/ui/diff-renderer.d.ts +28 -0
- package/dist/ui/diff-renderer.js +388 -0
- package/dist/ui/diff-renderer.js.map +1 -0
- package/dist/ui/highlight.d.ts +25 -0
- package/dist/ui/highlight.js +239 -0
- package/dist/ui/highlight.js.map +1 -0
- package/dist/ui.d.ts +3 -16
- package/dist/ui.js +7 -28
- package/dist/ui.js.map +1 -1
- package/dist/upgrade/prisma-upgrade.js +5 -2
- package/dist/upgrade/prisma-upgrade.js.map +1 -1
- package/package.json +1 -1
package/dist/actions.js
CHANGED
|
@@ -19,12 +19,15 @@ import { getConfig } from "./config.js";
|
|
|
19
19
|
import { detectProjectType } from "./repo.js";
|
|
20
20
|
import { createProvider, ProviderError } from "./provider.js";
|
|
21
21
|
import { initializeDatabase, createSession, updateSessionStatus, getSession, logMessage, listSessions, } from "./storage.js";
|
|
22
|
-
import {
|
|
22
|
+
import { searchSessions, filterSessions, exportSessionMarkdown, } from "./storage/queries.js";
|
|
23
|
+
import { readFileTool, listDirectoryTool, searchCodeTool, readPackageJsonTool, readPrismaSchemaTool, proposeDiffTool, applyDiffTool, setDiffRenderOptions, } from "./tools.js";
|
|
23
24
|
import { agentLoop, UserCancelledError } from "./agent.js";
|
|
24
25
|
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 { initUI, info, print, error as uiError, bold, yellow, green, dim, cyan, red, stripAnsi, renderTable, generateAndRenderDiff, confirm, ConfirmCancelledError, } from "./ui.js";
|
|
27
|
+
import { renderDiff as renderDiffEnhanced } from "./ui/diff-renderer.js";
|
|
28
|
+
import { formatOutput, AskOutputSchema, PlanOutputSchema, ReviewOutputSchema, ExploreOutputSchema, HistoryOutputSchema, DoctorOutputSchema, } from "./output/schemas.js";
|
|
26
29
|
import { loadConfig, validateConfig } from "./config.js";
|
|
27
|
-
import { getShellRcPath } from "./fs-helpers.js";
|
|
30
|
+
import { getShellRcPath, writeFileAtomic } from "./fs-helpers.js";
|
|
28
31
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
32
|
// ---------------------------------------------------------------------------
|
|
30
33
|
// Provider resolution with interactive setup
|
|
@@ -48,6 +51,20 @@ const DEFAULT_MODELS = {
|
|
|
48
51
|
ollama: "llama3",
|
|
49
52
|
openrouter: "anthropic/claude-sonnet-4-6",
|
|
50
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the effective model for the current provider config.
|
|
56
|
+
* Per-provider model overrides take precedence over the global model setting.
|
|
57
|
+
*/
|
|
58
|
+
function resolveModel(config) {
|
|
59
|
+
const provider = config.provider.default;
|
|
60
|
+
if (provider === "anthropic" && config.provider.anthropic?.model) {
|
|
61
|
+
return config.provider.anthropic.model;
|
|
62
|
+
}
|
|
63
|
+
if (provider === "openrouter" && config.provider.openrouter?.model) {
|
|
64
|
+
return config.provider.openrouter.model;
|
|
65
|
+
}
|
|
66
|
+
return config.provider.model;
|
|
67
|
+
}
|
|
51
68
|
/**
|
|
52
69
|
* Check if the given provider has its API key available.
|
|
53
70
|
*/
|
|
@@ -69,7 +86,7 @@ function isProviderReady(providerName) {
|
|
|
69
86
|
async function resolveProvider(config) {
|
|
70
87
|
// 1. Try the configured provider first
|
|
71
88
|
if (isProviderReady(config.provider.default)) {
|
|
72
|
-
return createProvider(config.provider.default);
|
|
89
|
+
return createProvider(config.provider.default, config.provider);
|
|
73
90
|
}
|
|
74
91
|
// 2. Check if any other provider is already configured via env
|
|
75
92
|
for (const [name, envKey] of Object.entries(PROVIDER_ENV_KEYS)) {
|
|
@@ -79,7 +96,7 @@ async function resolveProvider(config) {
|
|
|
79
96
|
info(dim(`${config.provider.default} not configured, falling back to ${name}`));
|
|
80
97
|
config.provider.default = name;
|
|
81
98
|
config.provider.model = DEFAULT_MODELS[name] ?? config.provider.model;
|
|
82
|
-
return createProvider(name);
|
|
99
|
+
return createProvider(name, config.provider);
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
// 3. No provider ready — interactive setup
|
|
@@ -172,7 +189,7 @@ async function resolveProvider(config) {
|
|
|
172
189
|
info(yellow(`Could not write to ~/${shellRcName}. Set ${envKey} manually.`));
|
|
173
190
|
}
|
|
174
191
|
}
|
|
175
|
-
return createProvider(selectedProvider);
|
|
192
|
+
return createProvider(selectedProvider, config.provider);
|
|
176
193
|
}
|
|
177
194
|
/**
|
|
178
195
|
* Save the provider/model choice to ~/.aria/config.toml
|
|
@@ -198,7 +215,6 @@ function saveProviderChoice(config) {
|
|
|
198
215
|
// ---------------------------------------------------------------------------
|
|
199
216
|
/**
|
|
200
217
|
* The five read-only tools exposed to the ask command.
|
|
201
|
-
* Requirements: 9.4
|
|
202
218
|
*/
|
|
203
219
|
const READ_ONLY_TOOLS = [
|
|
204
220
|
readFileTool,
|
|
@@ -326,33 +342,34 @@ function buildSystemPrompt(templateName, ctx, extraVars = {}) {
|
|
|
326
342
|
* Execute the ask command.
|
|
327
343
|
*
|
|
328
344
|
* Flow:
|
|
329
|
-
* 1. Load configuration and detect project type
|
|
330
|
-
* 2. Create or resume session with mode: "plan"
|
|
331
|
-
* 3. Build system prompt from ask.md template
|
|
332
|
-
* 4. Expose only read-only tools
|
|
333
|
-
* 5. Execute agent loop
|
|
334
|
-
* 6. Render response to terminal
|
|
335
|
-
* 7. Persist session to database
|
|
345
|
+
* 1. Load configuration and detect project type
|
|
346
|
+
* 2. Create or resume session with mode: "plan"
|
|
347
|
+
* 3. Build system prompt from ask.md template
|
|
348
|
+
* 4. Expose only read-only tools
|
|
349
|
+
* 5. Execute agent loop
|
|
350
|
+
* 6. Render response to terminal
|
|
351
|
+
* 7. Persist session to database
|
|
336
352
|
*
|
|
337
|
-
* Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9
|
|
338
353
|
*/
|
|
339
354
|
export async function runAsk(options) {
|
|
340
355
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
341
|
-
// 1. Load configuration
|
|
356
|
+
// 1. Load configuration
|
|
342
357
|
const config = getConfig(projectRoot, {
|
|
343
358
|
quiet: options.quiet,
|
|
344
359
|
maxTokens: options.maxTokens,
|
|
360
|
+
provider: options.provider,
|
|
361
|
+
model: options.model,
|
|
345
362
|
});
|
|
346
363
|
// Initialize UI with config settings
|
|
347
|
-
initUI(config.ui.color, config.ui.quiet);
|
|
348
|
-
// Detect project type early to fail fast if package.json is missing
|
|
364
|
+
initUI(config.ui.color, config.ui.quiet || (options.format === 'json' || options.format === 'ndjson'));
|
|
365
|
+
// Detect project type early to fail fast if package.json is missing
|
|
349
366
|
detectProjectType(projectRoot);
|
|
350
|
-
// 2. Initialize database and create/resume session
|
|
367
|
+
// 2. Initialize database and create/resume session
|
|
351
368
|
const db = initializeDatabase();
|
|
352
369
|
let sessionId;
|
|
353
370
|
let resumedMessages = [];
|
|
354
371
|
if (options.session) {
|
|
355
|
-
// Resume existing session
|
|
372
|
+
// Resume existing session
|
|
356
373
|
const existing = getSession(db, options.session);
|
|
357
374
|
if (!existing) {
|
|
358
375
|
uiError(`Session not found: ${options.session}`);
|
|
@@ -367,14 +384,14 @@ export async function runAsk(options) {
|
|
|
367
384
|
resumedMessages = rows;
|
|
368
385
|
}
|
|
369
386
|
else {
|
|
370
|
-
// Create new session
|
|
387
|
+
// Create new session
|
|
371
388
|
sessionId = randomUUID();
|
|
372
389
|
createSession(db, {
|
|
373
390
|
id: sessionId,
|
|
374
391
|
command: "ask",
|
|
375
392
|
projectRoot,
|
|
376
393
|
provider: config.provider.default,
|
|
377
|
-
model: config
|
|
394
|
+
model: resolveModel(config),
|
|
378
395
|
});
|
|
379
396
|
}
|
|
380
397
|
// 3. Resolve provider (interactive setup if needed)
|
|
@@ -392,12 +409,12 @@ export async function runAsk(options) {
|
|
|
392
409
|
updateSessionStatus(db, sessionId, "failed", String(err));
|
|
393
410
|
process.exit(4);
|
|
394
411
|
}
|
|
395
|
-
// 4. Build execution context with mode: "plan"
|
|
412
|
+
// 4. Build execution context with mode: "plan"
|
|
396
413
|
const ctx = {
|
|
397
414
|
projectRoot,
|
|
398
415
|
sessionId,
|
|
399
416
|
provider: config.provider.default,
|
|
400
|
-
model: config
|
|
417
|
+
model: resolveModel(config),
|
|
401
418
|
mode: "plan", // ask is always read-only
|
|
402
419
|
dryRun: false,
|
|
403
420
|
assumeYes: false,
|
|
@@ -408,7 +425,7 @@ export async function runAsk(options) {
|
|
|
408
425
|
if (options.maxTokens !== undefined) {
|
|
409
426
|
config.provider.maxTokens = options.maxTokens;
|
|
410
427
|
}
|
|
411
|
-
// 5. Build system prompt from ask.md template
|
|
428
|
+
// 5. Build system prompt from ask.md template
|
|
412
429
|
const systemPrompt = buildSystemPrompt("ask", ctx);
|
|
413
430
|
// Log system prompt as a system message
|
|
414
431
|
logMessage(db, sessionId, "system", systemPrompt);
|
|
@@ -425,15 +442,34 @@ export async function runAsk(options) {
|
|
|
425
442
|
.join("\n\n");
|
|
426
443
|
userRequest = `[Resumed session context]\n${priorContext}\n\n[New question]: ${options.question}`;
|
|
427
444
|
}
|
|
428
|
-
// 6. Execute agent loop
|
|
429
|
-
// agentLoop streams the response to stdout as it arrives
|
|
445
|
+
// 6. Execute agent loop
|
|
446
|
+
// agentLoop streams the response to stdout as it arrives
|
|
430
447
|
try {
|
|
431
|
-
await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "ask", db, systemPrompt);
|
|
432
|
-
//
|
|
448
|
+
const answer = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "ask", db, systemPrompt);
|
|
449
|
+
// v0.2.3: structured output
|
|
450
|
+
if (options.format === 'json' || options.format === 'ndjson') {
|
|
451
|
+
process.stdout.write(formatOutput({ version: '1', answer, sessionId }, options.format, AskOutputSchema));
|
|
452
|
+
}
|
|
453
|
+
else if (options.format === 'plain') {
|
|
454
|
+
// Strip ANSI codes and write plain text
|
|
455
|
+
process.stdout.write(stripAnsi(answer) + '\n');
|
|
456
|
+
}
|
|
457
|
+
// 7. Mark session as completed
|
|
433
458
|
updateSessionStatus(db, sessionId, "completed");
|
|
434
459
|
}
|
|
435
460
|
catch (err) {
|
|
436
461
|
const message = err instanceof Error ? err.message : String(err);
|
|
462
|
+
// v0.2.3: structured error output
|
|
463
|
+
if (options.format === 'json') {
|
|
464
|
+
process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
|
|
465
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
else if (options.format === 'ndjson') {
|
|
469
|
+
process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
|
|
470
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
437
473
|
uiError(message);
|
|
438
474
|
updateSessionStatus(db, sessionId, "failed", message);
|
|
439
475
|
process.exit(1);
|
|
@@ -443,33 +479,34 @@ export async function runAsk(options) {
|
|
|
443
479
|
* Execute the plan command.
|
|
444
480
|
*
|
|
445
481
|
* Flow:
|
|
446
|
-
* 1. Load configuration and detect project type
|
|
447
|
-
* 2. Create or resume session with mode: "plan"
|
|
448
|
-
* 3. Build system prompt from plan.md template
|
|
449
|
-
* 4. Expose only read-only tools
|
|
450
|
-
* 5. Execute agent loop
|
|
451
|
-
* 6. Render structured plan to terminal
|
|
452
|
-
* 7. Save to file if --output flag provided
|
|
453
|
-
* 8. Persist session to database
|
|
482
|
+
* 1. Load configuration and detect project type
|
|
483
|
+
* 2. Create or resume session with mode: "plan"
|
|
484
|
+
* 3. Build system prompt from plan.md template
|
|
485
|
+
* 4. Expose only read-only tools
|
|
486
|
+
* 5. Execute agent loop
|
|
487
|
+
* 6. Render structured plan to terminal
|
|
488
|
+
* 7. Save to file if --output flag provided
|
|
489
|
+
* 8. Persist session to database
|
|
454
490
|
*
|
|
455
|
-
* Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8
|
|
456
491
|
*/
|
|
457
492
|
export async function runPlan(options) {
|
|
458
493
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
459
|
-
// 1. Load configuration
|
|
494
|
+
// 1. Load configuration
|
|
460
495
|
const config = getConfig(projectRoot, {
|
|
461
496
|
quiet: options.quiet,
|
|
497
|
+
provider: options.provider,
|
|
498
|
+
model: options.model,
|
|
462
499
|
});
|
|
463
500
|
// Initialize UI with config settings
|
|
464
|
-
initUI(config.ui.color, config.ui.quiet);
|
|
465
|
-
// Detect project type early to fail fast if package.json is missing
|
|
501
|
+
initUI(config.ui.color, config.ui.quiet || (options.format === 'json' || options.format === 'ndjson'));
|
|
502
|
+
// Detect project type early to fail fast if package.json is missing
|
|
466
503
|
detectProjectType(projectRoot);
|
|
467
|
-
// 2. Initialize database and create/resume session
|
|
504
|
+
// 2. Initialize database and create/resume session
|
|
468
505
|
const db = initializeDatabase();
|
|
469
506
|
let sessionId;
|
|
470
507
|
let resumedMessages = [];
|
|
471
508
|
if (options.session) {
|
|
472
|
-
// Resume existing session
|
|
509
|
+
// Resume existing session
|
|
473
510
|
const existing = getSession(db, options.session);
|
|
474
511
|
if (!existing) {
|
|
475
512
|
uiError(`Session not found: ${options.session}`);
|
|
@@ -484,14 +521,14 @@ export async function runPlan(options) {
|
|
|
484
521
|
resumedMessages = rows;
|
|
485
522
|
}
|
|
486
523
|
else {
|
|
487
|
-
// Create new session
|
|
524
|
+
// Create new session
|
|
488
525
|
sessionId = randomUUID();
|
|
489
526
|
createSession(db, {
|
|
490
527
|
id: sessionId,
|
|
491
528
|
command: "plan",
|
|
492
529
|
projectRoot,
|
|
493
530
|
provider: config.provider.default,
|
|
494
|
-
model: config
|
|
531
|
+
model: resolveModel(config),
|
|
495
532
|
});
|
|
496
533
|
}
|
|
497
534
|
// 3. Resolve provider (interactive setup if needed)
|
|
@@ -509,19 +546,19 @@ export async function runPlan(options) {
|
|
|
509
546
|
updateSessionStatus(db, sessionId, "failed", String(err));
|
|
510
547
|
process.exit(4);
|
|
511
548
|
}
|
|
512
|
-
// 4. Build execution context with mode: "plan"
|
|
549
|
+
// 4. Build execution context with mode: "plan"
|
|
513
550
|
const ctx = {
|
|
514
551
|
projectRoot,
|
|
515
552
|
sessionId,
|
|
516
553
|
provider: config.provider.default,
|
|
517
|
-
model: config
|
|
554
|
+
model: resolveModel(config),
|
|
518
555
|
mode: "plan", // plan is always read-only
|
|
519
556
|
dryRun: false,
|
|
520
557
|
assumeYes: false,
|
|
521
558
|
maxIterations: config.agent.maxIterations,
|
|
522
559
|
timeoutSeconds: config.agent.timeoutSeconds,
|
|
523
560
|
};
|
|
524
|
-
// 5. Build system prompt from plan.md template
|
|
561
|
+
// 5. Build system prompt from plan.md template
|
|
525
562
|
const systemPrompt = buildSystemPrompt("plan", ctx, { userGoal: options.goal });
|
|
526
563
|
// Log system prompt as a system message
|
|
527
564
|
logMessage(db, sessionId, "system", systemPrompt);
|
|
@@ -535,17 +572,21 @@ export async function runPlan(options) {
|
|
|
535
572
|
.join("\n\n");
|
|
536
573
|
userRequest = `[Resumed session context]\n${priorContext}\n\n[New goal]: ${options.goal}`;
|
|
537
574
|
}
|
|
538
|
-
// 6. Execute agent loop — streams response to stdout
|
|
575
|
+
// 6. Execute agent loop — streams response to stdout
|
|
539
576
|
try {
|
|
540
577
|
const planContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "plan", db, systemPrompt);
|
|
541
|
-
//
|
|
578
|
+
// v0.2.3: structured output
|
|
579
|
+
if (options.format === 'json' || options.format === 'ndjson') {
|
|
580
|
+
process.stdout.write(formatOutput({ version: '1', plan: planContent, sessionId, outputPath: options.output }, options.format, PlanOutputSchema));
|
|
581
|
+
}
|
|
582
|
+
// 7. Save to file if --output flag provided
|
|
542
583
|
if (options.output) {
|
|
543
584
|
const outputPath = path.resolve(options.output);
|
|
544
585
|
const outputDir = path.dirname(outputPath);
|
|
545
586
|
if (!existsSync(outputDir)) {
|
|
546
587
|
mkdirSync(outputDir, { recursive: true });
|
|
547
588
|
}
|
|
548
|
-
|
|
589
|
+
writeFileAtomic(outputPath, planContent);
|
|
549
590
|
info(`Plan saved to ${options.output}`);
|
|
550
591
|
}
|
|
551
592
|
// 8. Mark session as completed (Req 10.8)
|
|
@@ -553,6 +594,17 @@ export async function runPlan(options) {
|
|
|
553
594
|
}
|
|
554
595
|
catch (err) {
|
|
555
596
|
const message = err instanceof Error ? err.message : String(err);
|
|
597
|
+
// v0.2.3: structured error output
|
|
598
|
+
if (options.format === 'json') {
|
|
599
|
+
process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
|
|
600
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
else if (options.format === 'ndjson') {
|
|
604
|
+
process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
|
|
605
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
556
608
|
uiError(message);
|
|
557
609
|
updateSessionStatus(db, sessionId, "failed", message);
|
|
558
610
|
process.exit(1);
|
|
@@ -562,36 +614,42 @@ export async function runPlan(options) {
|
|
|
562
614
|
* Execute the patch command.
|
|
563
615
|
*
|
|
564
616
|
* Flow:
|
|
565
|
-
* 1. Load configuration and detect project type
|
|
566
|
-
* 2. Create session with mode: "build"
|
|
567
|
-
* 3. Build system prompt from patch.md template
|
|
568
|
-
* 4. Expose read-only + mutation tools
|
|
569
|
-
* 5. Execute agent loop — agent calls propose_diff
|
|
570
|
-
* 6. Render diff preview with syntax highlighting
|
|
571
|
-
* 7. Render mutation summary
|
|
572
|
-
* 8. If --dry-run, exit with code 0
|
|
573
|
-
* 9. If not --yes, prompt for confirmation
|
|
574
|
-
* 10. Agent calls apply_diff atomically
|
|
575
|
-
* 11. Log mutation to database
|
|
576
|
-
* 12. Display rollback hints
|
|
577
|
-
* 13. Persist session to database
|
|
617
|
+
* 1. Load configuration and detect project type
|
|
618
|
+
* 2. Create session with mode: "build"
|
|
619
|
+
* 3. Build system prompt from patch.md template
|
|
620
|
+
* 4. Expose read-only + mutation tools
|
|
621
|
+
* 5. Execute agent loop — agent calls propose_diff
|
|
622
|
+
* 6. Render diff preview with syntax highlighting
|
|
623
|
+
* 7. Render mutation summary
|
|
624
|
+
* 8. If --dry-run, exit with code 0
|
|
625
|
+
* 9. If not --yes, prompt for confirmation
|
|
626
|
+
* 10. Agent calls apply_diff atomically
|
|
627
|
+
* 11. Log mutation to database
|
|
628
|
+
* 12. Display rollback hints
|
|
629
|
+
* 13. Persist session to database
|
|
578
630
|
*
|
|
579
|
-
* Requirements: 11.1–11.13, 17.1–17.9
|
|
580
631
|
*/
|
|
581
632
|
export async function runPatch(options) {
|
|
582
633
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
583
|
-
// 1. Load configuration
|
|
634
|
+
// 1. Load configuration
|
|
584
635
|
const config = getConfig(projectRoot, {
|
|
585
636
|
quiet: options.quiet,
|
|
637
|
+
provider: options.provider,
|
|
638
|
+
model: options.model,
|
|
586
639
|
});
|
|
587
640
|
// Apply flag overrides to config
|
|
588
641
|
if (options.dryRun)
|
|
589
642
|
config.agent.mode = "build"; // keep build mode, dryRun handled via ctx
|
|
590
643
|
// Initialize UI with config settings
|
|
591
644
|
initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
|
|
592
|
-
//
|
|
645
|
+
// Plain format guard: require --yes for mutation commands
|
|
646
|
+
if (options.format === 'plain' && !options.yes) {
|
|
647
|
+
process.stdout.write('plain format requires --yes for mutation commands\n');
|
|
648
|
+
process.exit(2);
|
|
649
|
+
}
|
|
650
|
+
// Detect project type early to fail fast
|
|
593
651
|
detectProjectType(projectRoot);
|
|
594
|
-
// 2. Initialize database and create session
|
|
652
|
+
// 2. Initialize database and create session
|
|
595
653
|
const db = initializeDatabase();
|
|
596
654
|
let sessionId;
|
|
597
655
|
if (options.session) {
|
|
@@ -610,7 +668,7 @@ export async function runPatch(options) {
|
|
|
610
668
|
command: "patch",
|
|
611
669
|
projectRoot,
|
|
612
670
|
provider: config.provider.default,
|
|
613
|
-
model: config
|
|
671
|
+
model: resolveModel(config),
|
|
614
672
|
});
|
|
615
673
|
}
|
|
616
674
|
// 3. Resolve provider (interactive setup if needed)
|
|
@@ -628,22 +686,22 @@ export async function runPatch(options) {
|
|
|
628
686
|
updateSessionStatus(db, sessionId, "failed", String(err));
|
|
629
687
|
process.exit(4);
|
|
630
688
|
}
|
|
631
|
-
// 4. Build execution context with mode: "build"
|
|
689
|
+
// 4. Build execution context with mode: "build"
|
|
632
690
|
const ctx = {
|
|
633
691
|
projectRoot,
|
|
634
692
|
sessionId,
|
|
635
693
|
provider: config.provider.default,
|
|
636
|
-
model: config
|
|
694
|
+
model: resolveModel(config),
|
|
637
695
|
mode: "build",
|
|
638
696
|
dryRun: Boolean(options.dryRun),
|
|
639
697
|
assumeYes: Boolean(options.yes),
|
|
640
698
|
maxIterations: config.agent.maxIterations,
|
|
641
699
|
timeoutSeconds: config.agent.timeoutSeconds,
|
|
642
700
|
};
|
|
643
|
-
// 5. Build system prompt from patch.md template
|
|
701
|
+
// 5. Build system prompt from patch.md template
|
|
644
702
|
const systemPrompt = buildSystemPrompt("patch", ctx);
|
|
645
703
|
logMessage(db, sessionId, "system", systemPrompt);
|
|
646
|
-
// Expose read-only tools + mutation tools
|
|
704
|
+
// Expose read-only tools + mutation tools
|
|
647
705
|
const patchTools = [
|
|
648
706
|
readFileTool,
|
|
649
707
|
listDirectoryTool,
|
|
@@ -656,15 +714,25 @@ export async function runPatch(options) {
|
|
|
656
714
|
if (options.dryRun) {
|
|
657
715
|
info(bold("Dry-run mode — changes will be previewed but not applied."));
|
|
658
716
|
}
|
|
659
|
-
//
|
|
717
|
+
// Configure enhanced diff renderer
|
|
718
|
+
const terminalWidth = process.stdout.columns ?? 80;
|
|
719
|
+
const diffRenderOptions = {
|
|
720
|
+
split: options.split ?? false,
|
|
721
|
+
lineNumbers: options.format !== 'plain',
|
|
722
|
+
collapseThreshold: 5,
|
|
723
|
+
terminalWidth,
|
|
724
|
+
...(options.format === 'plain' ? { language: undefined } : {}),
|
|
725
|
+
};
|
|
726
|
+
setDiffRenderOptions((diffText) => renderDiffEnhanced(diffText, diffRenderOptions));
|
|
727
|
+
// 6. Execute agent loop
|
|
660
728
|
// The agent loop handles:
|
|
661
|
-
// - propose_diff: generates diff + MutationSummary
|
|
662
|
-
// - dry-run enforcement: skips apply_diff
|
|
663
|
-
// - confirmation prompt before apply_diff
|
|
664
|
-
// - atomic application via apply_diff
|
|
729
|
+
// - propose_diff: generates diff + MutationSummary
|
|
730
|
+
// - dry-run enforcement: skips apply_diff
|
|
731
|
+
// - confirmation prompt before apply_diff
|
|
732
|
+
// - atomic application via apply_diff
|
|
665
733
|
try {
|
|
666
734
|
await agentLoop(ctx, options.description, patchTools, provider, config, "patch", db, systemPrompt);
|
|
667
|
-
// 12. Display rollback hints after successful application
|
|
735
|
+
// 12. Display rollback hints after successful application
|
|
668
736
|
// The agent loop streams the response which includes rollback hints from
|
|
669
737
|
// the apply_diff result. We add a final summary line here.
|
|
670
738
|
if (!options.dryRun) {
|
|
@@ -676,22 +744,37 @@ export async function runPatch(options) {
|
|
|
676
744
|
info("");
|
|
677
745
|
info(yellow("Dry-run complete — no files were modified."));
|
|
678
746
|
}
|
|
679
|
-
// 13. Mark session as completed
|
|
747
|
+
// 13. Mark session as completed
|
|
680
748
|
updateSessionStatus(db, sessionId, "completed");
|
|
681
749
|
}
|
|
682
750
|
catch (err) {
|
|
683
751
|
const message = err instanceof Error ? err.message : String(err);
|
|
684
|
-
// Handle user cancellation
|
|
752
|
+
// Handle user cancellation
|
|
685
753
|
if (err instanceof UserCancelledError || err instanceof ConfirmCancelledError) {
|
|
686
754
|
info("");
|
|
687
755
|
info(yellow("Operation cancelled."));
|
|
688
756
|
updateSessionStatus(db, sessionId, "cancelled");
|
|
689
757
|
process.exit(130);
|
|
690
758
|
}
|
|
759
|
+
// v0.2.3: structured error output
|
|
760
|
+
if (options.format === 'json') {
|
|
761
|
+
process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
|
|
762
|
+
updateSessionStatus(db, sessionId, "failed", message);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
}
|
|
765
|
+
else if (options.format === 'ndjson') {
|
|
766
|
+
process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
|
|
767
|
+
updateSessionStatus(db, sessionId, "failed", message);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
691
770
|
uiError(message);
|
|
692
771
|
updateSessionStatus(db, sessionId, "failed", message);
|
|
693
772
|
process.exit(1);
|
|
694
773
|
}
|
|
774
|
+
finally {
|
|
775
|
+
// Clear diff render options after patch completes
|
|
776
|
+
setDiffRenderOptions(null);
|
|
777
|
+
}
|
|
695
778
|
}
|
|
696
779
|
/**
|
|
697
780
|
* Read git diff based on the provided options.
|
|
@@ -700,21 +783,20 @@ export async function runPatch(options) {
|
|
|
700
783
|
* - --unstaged: unstaged changes (`git diff`)
|
|
701
784
|
* - --branch <base>: compare to base branch (`git diff <base>...HEAD`)
|
|
702
785
|
*
|
|
703
|
-
* Requirements: 12.3, 12.4, 12.5
|
|
704
786
|
*/
|
|
705
787
|
function readGitDiff(options, projectRoot) {
|
|
706
788
|
try {
|
|
707
789
|
let args;
|
|
708
790
|
if (options.branch) {
|
|
709
|
-
// Compare current branch to specified base
|
|
791
|
+
// Compare current branch to specified base
|
|
710
792
|
args = ["diff", `${options.branch}...HEAD`];
|
|
711
793
|
}
|
|
712
794
|
else if (options.unstaged) {
|
|
713
|
-
// Unstaged changes
|
|
795
|
+
// Unstaged changes
|
|
714
796
|
args = ["diff"];
|
|
715
797
|
}
|
|
716
798
|
else {
|
|
717
|
-
// Staged changes — default
|
|
799
|
+
// Staged changes — default
|
|
718
800
|
args = ["diff", "--cached"];
|
|
719
801
|
}
|
|
720
802
|
const output = execFileSync("git", args, {
|
|
@@ -734,7 +816,6 @@ function readGitDiff(options, projectRoot) {
|
|
|
734
816
|
* Extracts summary, issues (with severity), and suggestions from the
|
|
735
817
|
* "# Code Review" markdown format defined in review.md.
|
|
736
818
|
*
|
|
737
|
-
* Requirements: 12.7
|
|
738
819
|
*/
|
|
739
820
|
function parseReviewResponse(content) {
|
|
740
821
|
const result = {
|
|
@@ -785,7 +866,6 @@ function parseReviewResponse(content) {
|
|
|
785
866
|
/**
|
|
786
867
|
* Render the structured review to the terminal in readable format.
|
|
787
868
|
*
|
|
788
|
-
* Requirements: 12.8
|
|
789
869
|
*/
|
|
790
870
|
function renderReview(review) {
|
|
791
871
|
info("");
|
|
@@ -824,30 +904,31 @@ function renderReview(review) {
|
|
|
824
904
|
* Execute the review command.
|
|
825
905
|
*
|
|
826
906
|
* Flow:
|
|
827
|
-
* 1. Parse flags and load configuration
|
|
828
|
-
* 2. Detect project type
|
|
829
|
-
* 3. Create session with mode: "plan"
|
|
830
|
-
* 4. Read git diff (staged / unstaged / branch)
|
|
831
|
-
* 5. Build system prompt from review.md template
|
|
832
|
-
* 6. Send diff + project context to provider
|
|
833
|
-
* 7. Parse structured review (summary, issues, suggestions)
|
|
834
|
-
* 8. Render review to terminal
|
|
835
|
-
* 9. Output JSON if --format json
|
|
836
|
-
* 10. Persist session to database
|
|
907
|
+
* 1. Parse flags and load configuration
|
|
908
|
+
* 2. Detect project type
|
|
909
|
+
* 3. Create session with mode: "plan"
|
|
910
|
+
* 4. Read git diff (staged / unstaged / branch)
|
|
911
|
+
* 5. Build system prompt from review.md template
|
|
912
|
+
* 6. Send diff + project context to provider
|
|
913
|
+
* 7. Parse structured review (summary, issues, suggestions)
|
|
914
|
+
* 8. Render review to terminal
|
|
915
|
+
* 9. Output JSON if --format json
|
|
916
|
+
* 10. Persist session to database
|
|
837
917
|
*
|
|
838
|
-
* Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9, 12.10
|
|
839
918
|
*/
|
|
840
919
|
export async function runReview(options) {
|
|
841
920
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
842
|
-
// 1. Load configuration
|
|
921
|
+
// 1. Load configuration
|
|
843
922
|
const config = getConfig(projectRoot, {
|
|
844
923
|
quiet: options.quiet,
|
|
924
|
+
provider: options.provider,
|
|
925
|
+
model: options.model,
|
|
845
926
|
});
|
|
846
927
|
// Initialize UI with config settings
|
|
847
|
-
initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
|
|
848
|
-
// 2. Detect project type early to fail fast
|
|
928
|
+
initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet) || (options.format === 'json' || options.format === 'ndjson'));
|
|
929
|
+
// 2. Detect project type early to fail fast
|
|
849
930
|
detectProjectType(projectRoot);
|
|
850
|
-
// 3. Initialize database and create session
|
|
931
|
+
// 3. Initialize database and create session
|
|
851
932
|
const db = initializeDatabase();
|
|
852
933
|
const sessionId = randomUUID();
|
|
853
934
|
createSession(db, {
|
|
@@ -855,7 +936,7 @@ export async function runReview(options) {
|
|
|
855
936
|
command: "review",
|
|
856
937
|
projectRoot,
|
|
857
938
|
provider: config.provider.default,
|
|
858
|
-
model: config
|
|
939
|
+
model: resolveModel(config),
|
|
859
940
|
});
|
|
860
941
|
// Resolve provider (interactive setup if needed)
|
|
861
942
|
let provider;
|
|
@@ -872,19 +953,19 @@ export async function runReview(options) {
|
|
|
872
953
|
updateSessionStatus(db, sessionId, "failed", String(err));
|
|
873
954
|
process.exit(4);
|
|
874
955
|
}
|
|
875
|
-
// Build execution context with mode: "plan"
|
|
956
|
+
// Build execution context with mode: "plan"
|
|
876
957
|
const ctx = {
|
|
877
958
|
projectRoot,
|
|
878
959
|
sessionId,
|
|
879
960
|
provider: config.provider.default,
|
|
880
|
-
model: config
|
|
961
|
+
model: resolveModel(config),
|
|
881
962
|
mode: "plan", // review is always read-only
|
|
882
963
|
dryRun: false,
|
|
883
964
|
assumeYes: false,
|
|
884
965
|
maxIterations: config.agent.maxIterations,
|
|
885
966
|
timeoutSeconds: config.agent.timeoutSeconds,
|
|
886
967
|
};
|
|
887
|
-
// 4. Read git diff
|
|
968
|
+
// 4. Read git diff
|
|
888
969
|
let diff;
|
|
889
970
|
try {
|
|
890
971
|
diff = readGitDiff(options, projectRoot);
|
|
@@ -905,10 +986,10 @@ export async function runReview(options) {
|
|
|
905
986
|
updateSessionStatus(db, sessionId, "completed");
|
|
906
987
|
return;
|
|
907
988
|
}
|
|
908
|
-
// 5. Build system prompt from review.md template
|
|
989
|
+
// 5. Build system prompt from review.md template
|
|
909
990
|
const systemPrompt = buildSystemPrompt("review", ctx);
|
|
910
991
|
logMessage(db, sessionId, "system", systemPrompt);
|
|
911
|
-
// 6. Build user request: diff + project context
|
|
992
|
+
// 6. Build user request: diff + project context
|
|
912
993
|
const project = detectProjectType(projectRoot);
|
|
913
994
|
const diffSource = options.branch
|
|
914
995
|
? `branch diff (current vs ${options.branch})`
|
|
@@ -928,23 +1009,28 @@ export async function runReview(options) {
|
|
|
928
1009
|
]
|
|
929
1010
|
.filter((l) => l !== null)
|
|
930
1011
|
.join("\n");
|
|
931
|
-
// Execute agent loop — streams response to stdout
|
|
1012
|
+
// Execute agent loop — streams response to stdout
|
|
932
1013
|
try {
|
|
933
1014
|
const reviewContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "review", db, systemPrompt);
|
|
934
|
-
// 7. Parse structured review
|
|
1015
|
+
// 7. Parse structured review
|
|
935
1016
|
const review = parseReviewResponse(reviewContent);
|
|
936
|
-
// 8 & 9. Render or output
|
|
937
|
-
if (options.format ===
|
|
938
|
-
//
|
|
939
|
-
process.stdout.write(
|
|
1017
|
+
// 8 & 9. Render or output
|
|
1018
|
+
if (options.format === 'json' || options.format === 'ndjson') {
|
|
1019
|
+
// v0.2.3: structured output via formatOutput
|
|
1020
|
+
process.stdout.write(formatOutput({ version: '1', review: reviewContent, sessionId, branch: options.branch }, options.format, ReviewOutputSchema));
|
|
1021
|
+
}
|
|
1022
|
+
else if (options.format === 'plain') {
|
|
1023
|
+
// Strip ANSI codes for plain output
|
|
1024
|
+
const plainReview = stripAnsi(reviewContent);
|
|
1025
|
+
process.stdout.write(plainReview + '\n');
|
|
940
1026
|
}
|
|
941
1027
|
else {
|
|
942
|
-
// Render to terminal in readable format
|
|
1028
|
+
// Render to terminal in readable format
|
|
943
1029
|
// Note: agentLoop already streamed the raw response; renderReview
|
|
944
1030
|
// provides a structured re-render for clarity.
|
|
945
1031
|
renderReview(review);
|
|
946
1032
|
}
|
|
947
|
-
// 10. Mark session as completed
|
|
1033
|
+
// 10. Mark session as completed
|
|
948
1034
|
updateSessionStatus(db, sessionId, "completed");
|
|
949
1035
|
}
|
|
950
1036
|
catch (err) {
|
|
@@ -956,6 +1042,17 @@ export async function runReview(options) {
|
|
|
956
1042
|
updateSessionStatus(db, sessionId, "cancelled");
|
|
957
1043
|
process.exit(130);
|
|
958
1044
|
}
|
|
1045
|
+
// v0.2.3: structured error output
|
|
1046
|
+
if (options.format === 'json') {
|
|
1047
|
+
process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
|
|
1048
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
1049
|
+
process.exit(1);
|
|
1050
|
+
}
|
|
1051
|
+
else if (options.format === 'ndjson') {
|
|
1052
|
+
process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
|
|
1053
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
959
1056
|
uiError(message);
|
|
960
1057
|
updateSessionStatus(db, sessionId, "failed", message);
|
|
961
1058
|
process.exit(1);
|
|
@@ -965,31 +1062,32 @@ export async function runReview(options) {
|
|
|
965
1062
|
* Execute the explore command.
|
|
966
1063
|
*
|
|
967
1064
|
* Flow:
|
|
968
|
-
* 1. Parse flags and load configuration
|
|
969
|
-
* 2. Detect project type
|
|
970
|
-
* 3. Create session with mode: "plan"
|
|
971
|
-
* 4. Scan repository structure respecting .gitignore
|
|
972
|
-
* 5. Detect frameworks and key configuration files
|
|
973
|
-
* 6. Identify entry points based on project type
|
|
974
|
-
* 7. Build system prompt from explore.md template
|
|
975
|
-
* 8. Execute agent loop to summarize structure/patterns
|
|
976
|
-
* 9. Render exploration summary to terminal
|
|
977
|
-
* 10. Save to ./.aria/explore.md if --save flag
|
|
978
|
-
* 11. Persist session to database
|
|
1065
|
+
* 1. Parse flags and load configuration
|
|
1066
|
+
* 2. Detect project type
|
|
1067
|
+
* 3. Create session with mode: "plan"
|
|
1068
|
+
* 4. Scan repository structure respecting .gitignore
|
|
1069
|
+
* 5. Detect frameworks and key configuration files
|
|
1070
|
+
* 6. Identify entry points based on project type
|
|
1071
|
+
* 7. Build system prompt from explore.md template
|
|
1072
|
+
* 8. Execute agent loop to summarize structure/patterns
|
|
1073
|
+
* 9. Render exploration summary to terminal
|
|
1074
|
+
* 10. Save to ./.aria/explore.md if --save flag
|
|
1075
|
+
* 11. Persist session to database
|
|
979
1076
|
*
|
|
980
|
-
* Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9, 13.10
|
|
981
1077
|
*/
|
|
982
1078
|
export async function runExplore(options) {
|
|
983
1079
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
984
|
-
// 1. Load configuration
|
|
1080
|
+
// 1. Load configuration
|
|
985
1081
|
const config = getConfig(projectRoot, {
|
|
986
1082
|
quiet: options.quiet,
|
|
1083
|
+
provider: options.provider,
|
|
1084
|
+
model: options.model,
|
|
987
1085
|
});
|
|
988
1086
|
// Initialize UI with config settings
|
|
989
|
-
initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
|
|
990
|
-
// Detect project type early to fail fast
|
|
1087
|
+
initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet) || (options.format === 'json' || options.format === 'ndjson'));
|
|
1088
|
+
// Detect project type early to fail fast
|
|
991
1089
|
const project = detectProjectType(projectRoot);
|
|
992
|
-
// 2. Initialize database and create session
|
|
1090
|
+
// 2. Initialize database and create session
|
|
993
1091
|
const db = initializeDatabase();
|
|
994
1092
|
const sessionId = randomUUID();
|
|
995
1093
|
createSession(db, {
|
|
@@ -997,7 +1095,7 @@ export async function runExplore(options) {
|
|
|
997
1095
|
command: "explore",
|
|
998
1096
|
projectRoot,
|
|
999
1097
|
provider: config.provider.default,
|
|
1000
|
-
model: config
|
|
1098
|
+
model: resolveModel(config),
|
|
1001
1099
|
});
|
|
1002
1100
|
// 3. Resolve provider (interactive setup if needed)
|
|
1003
1101
|
let provider;
|
|
@@ -1014,22 +1112,22 @@ export async function runExplore(options) {
|
|
|
1014
1112
|
updateSessionStatus(db, sessionId, "failed", String(err));
|
|
1015
1113
|
process.exit(4);
|
|
1016
1114
|
}
|
|
1017
|
-
// 4. Build execution context with mode: "plan"
|
|
1115
|
+
// 4. Build execution context with mode: "plan"
|
|
1018
1116
|
const ctx = {
|
|
1019
1117
|
projectRoot,
|
|
1020
1118
|
sessionId,
|
|
1021
1119
|
provider: config.provider.default,
|
|
1022
|
-
model: config
|
|
1120
|
+
model: resolveModel(config),
|
|
1023
1121
|
mode: "plan", // explore is always read-only
|
|
1024
1122
|
dryRun: false,
|
|
1025
1123
|
assumeYes: false,
|
|
1026
1124
|
maxIterations: config.agent.maxIterations,
|
|
1027
1125
|
timeoutSeconds: config.agent.timeoutSeconds,
|
|
1028
1126
|
};
|
|
1029
|
-
// 5. Build system prompt from explore.md template
|
|
1127
|
+
// 5. Build system prompt from explore.md template
|
|
1030
1128
|
const systemPrompt = buildSystemPrompt("explore", ctx);
|
|
1031
1129
|
logMessage(db, sessionId, "system", systemPrompt);
|
|
1032
|
-
// 6. Build user request with project context and depth hint
|
|
1130
|
+
// 6. Build user request with project context and depth hint
|
|
1033
1131
|
const frameworkInfo = project.framework
|
|
1034
1132
|
? `${project.framework.name}${project.framework.version ? ` ${project.framework.version}` : ""}${project.framework.router ? ` (${project.framework.router} router)` : ""}`
|
|
1035
1133
|
: "none";
|
|
@@ -1053,20 +1151,24 @@ export async function runExplore(options) {
|
|
|
1053
1151
|
]
|
|
1054
1152
|
.filter((l) => l !== null)
|
|
1055
1153
|
.join("\n");
|
|
1056
|
-
// 7. Execute agent loop — streams response to stdout
|
|
1154
|
+
// 7. Execute agent loop — streams response to stdout
|
|
1057
1155
|
try {
|
|
1058
1156
|
const exploreContent = await agentLoop(ctx, userRequest, READ_ONLY_TOOLS, provider, config, "explore", db, systemPrompt);
|
|
1059
|
-
//
|
|
1157
|
+
// v0.2.3: structured output
|
|
1158
|
+
if (options.format === 'json' || options.format === 'ndjson') {
|
|
1159
|
+
process.stdout.write(formatOutput({ version: '1', summary: exploreContent, sessionId }, options.format, ExploreOutputSchema));
|
|
1160
|
+
}
|
|
1161
|
+
// 8. Save to ./.aria/explore.md if --save flag provided
|
|
1060
1162
|
if (options.save) {
|
|
1061
1163
|
const ariaDir = path.join(projectRoot, ".aria");
|
|
1062
1164
|
const savePath = path.join(ariaDir, "explore.md");
|
|
1063
1165
|
if (!existsSync(ariaDir)) {
|
|
1064
1166
|
mkdirSync(ariaDir, { recursive: true });
|
|
1065
1167
|
}
|
|
1066
|
-
|
|
1168
|
+
writeFileAtomic(savePath, exploreContent);
|
|
1067
1169
|
info(`Exploration summary saved to .aria/explore.md`);
|
|
1068
1170
|
}
|
|
1069
|
-
// 9. Mark session as completed
|
|
1171
|
+
// 9. Mark session as completed
|
|
1070
1172
|
updateSessionStatus(db, sessionId, "completed");
|
|
1071
1173
|
}
|
|
1072
1174
|
catch (err) {
|
|
@@ -1077,6 +1179,17 @@ export async function runExplore(options) {
|
|
|
1077
1179
|
updateSessionStatus(db, sessionId, "cancelled");
|
|
1078
1180
|
process.exit(130);
|
|
1079
1181
|
}
|
|
1182
|
+
// v0.2.3: structured error output
|
|
1183
|
+
if (options.format === 'json') {
|
|
1184
|
+
process.stderr.write(JSON.stringify({ version: '1', error: message, exitCode: 1 }) + '\n');
|
|
1185
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
else if (options.format === 'ndjson') {
|
|
1189
|
+
process.stderr.write(JSON.stringify({ version: '1', event: 'error', error: message }) + '\n');
|
|
1190
|
+
updateSessionStatus(db, sessionId, 'failed', message);
|
|
1191
|
+
process.exit(1);
|
|
1192
|
+
}
|
|
1080
1193
|
uiError(message);
|
|
1081
1194
|
updateSessionStatus(db, sessionId, "failed", message);
|
|
1082
1195
|
process.exit(1);
|
|
@@ -1086,7 +1199,6 @@ export async function runExplore(options) {
|
|
|
1086
1199
|
* Format a SQLite timestamp string into a human-readable relative time.
|
|
1087
1200
|
* e.g. "2 hours ago", "3 days ago", "just now"
|
|
1088
1201
|
*
|
|
1089
|
-
* Requirements: 14.7
|
|
1090
1202
|
*/
|
|
1091
1203
|
function formatTimestamp(timestamp) {
|
|
1092
1204
|
// SQLite CURRENT_TIMESTAMP returns "YYYY-MM-DD HH:MM:SS" (space, no T, no Z).
|
|
@@ -1138,8 +1250,20 @@ function colorizeStatus(status) {
|
|
|
1138
1250
|
* Render a tool execution tree for a session.
|
|
1139
1251
|
* Shows tool calls in chronological order with input/output summaries.
|
|
1140
1252
|
*
|
|
1141
|
-
* Requirements: 14.6
|
|
1142
1253
|
*/
|
|
1254
|
+
// Tool type icon sets
|
|
1255
|
+
const READ_TOOLS = new Set(['read_file', 'read_package_json', 'read_prisma_schema']);
|
|
1256
|
+
const SEARCH_TOOLS = new Set(['search_code', 'list_directory']);
|
|
1257
|
+
const MUTATION_TOOLS = new Set(['propose_diff', 'apply_diff', 'apply_schema_change']);
|
|
1258
|
+
function toolIcon(toolName) {
|
|
1259
|
+
if (READ_TOOLS.has(toolName))
|
|
1260
|
+
return '[R]';
|
|
1261
|
+
if (SEARCH_TOOLS.has(toolName))
|
|
1262
|
+
return '[S]';
|
|
1263
|
+
if (MUTATION_TOOLS.has(toolName))
|
|
1264
|
+
return '[W]';
|
|
1265
|
+
return '[.]';
|
|
1266
|
+
}
|
|
1143
1267
|
function renderToolTree(db, sessionId) {
|
|
1144
1268
|
const executions = db
|
|
1145
1269
|
.prepare(`SELECT tool_name, input, output, error, created_at
|
|
@@ -1157,7 +1281,8 @@ function renderToolTree(db, sessionId) {
|
|
|
1157
1281
|
const prefix = isLast ? "└─" : "├─";
|
|
1158
1282
|
const childPrefix = isLast ? " " : "│ ";
|
|
1159
1283
|
const statusIcon = exec.error ? red("✗") : green("✓");
|
|
1160
|
-
|
|
1284
|
+
const typeIcon = toolIcon(exec.tool_name);
|
|
1285
|
+
info(` ${prefix} ${typeIcon} ${statusIcon} ${bold(exec.tool_name)} ${dim(formatTimestamp(exec.created_at))}`);
|
|
1161
1286
|
// Show a brief summary of the input
|
|
1162
1287
|
try {
|
|
1163
1288
|
const inputObj = JSON.parse(exec.input);
|
|
@@ -1190,23 +1315,88 @@ function renderToolTree(db, sessionId) {
|
|
|
1190
1315
|
* Execute the history command.
|
|
1191
1316
|
*
|
|
1192
1317
|
* Flow:
|
|
1193
|
-
* 1. If no --session flag: list recent sessions in a table
|
|
1194
|
-
* 2. If --session flag: display full session log
|
|
1195
|
-
* 3. If --tree flag: render tool execution tree
|
|
1196
|
-
* 4. Format timestamps in human-readable format
|
|
1197
|
-
* 5. Support pagination for large result sets
|
|
1318
|
+
* 1. If no --session flag: list recent sessions in a table
|
|
1319
|
+
* 2. If --session flag: display full session log
|
|
1320
|
+
* 3. If --tree flag: render tool execution tree
|
|
1321
|
+
* 4. Format timestamps in human-readable format
|
|
1322
|
+
* 5. Support pagination for large result sets
|
|
1198
1323
|
*
|
|
1199
|
-
* Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.8
|
|
1200
1324
|
*/
|
|
1201
1325
|
export async function runHistory(options) {
|
|
1202
1326
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
1203
1327
|
// Load configuration and initialize UI
|
|
1204
1328
|
const config = getConfig(projectRoot, { quiet: options.quiet });
|
|
1205
|
-
initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet));
|
|
1329
|
+
initUI(config.ui.color, config.ui.quiet || Boolean(options.quiet) || (options.format === 'json' || options.format === 'ndjson'));
|
|
1206
1330
|
// Initialize database
|
|
1207
1331
|
const db = initializeDatabase();
|
|
1208
1332
|
// ---------------------------------------------------------------------------
|
|
1209
|
-
//
|
|
1333
|
+
// v0.2.3: Structured output for json/ndjson/plain — applies to session listing
|
|
1334
|
+
// ---------------------------------------------------------------------------
|
|
1335
|
+
const isStructuredFormat = options.format === 'json' || options.format === 'ndjson';
|
|
1336
|
+
const isPlainFormat = options.format === 'plain';
|
|
1337
|
+
// ---------------------------------------------------------------------------
|
|
1338
|
+
// v0.2.3: Search path — full-text search across session content
|
|
1339
|
+
// ---------------------------------------------------------------------------
|
|
1340
|
+
if (options.search) {
|
|
1341
|
+
const results = searchSessions(db, options.search, { limit: options.limit });
|
|
1342
|
+
if (results.length === 0) {
|
|
1343
|
+
info("No sessions found matching query.");
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const rows = results.map(({ session, matchedInMessages }) => [
|
|
1347
|
+
dim(session.id.slice(0, 8)),
|
|
1348
|
+
cyan(session.command),
|
|
1349
|
+
colorizeStatus(session.status),
|
|
1350
|
+
formatTimestamp(session.createdAt),
|
|
1351
|
+
matchedInMessages ? dim("message") : dim("metadata"),
|
|
1352
|
+
]);
|
|
1353
|
+
const table = renderTable({ head: ["ID", "Command", "Status", "Started", "Match"], colWidths: [12, 12, 12, 20, 12] }, rows);
|
|
1354
|
+
info(table);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
// ---------------------------------------------------------------------------
|
|
1358
|
+
// v0.2.3: Filter path — filter by command, since, status
|
|
1359
|
+
// ---------------------------------------------------------------------------
|
|
1360
|
+
if (options.command || options.since || options.status) {
|
|
1361
|
+
const sessions = filterSessions(db, {
|
|
1362
|
+
command: options.command,
|
|
1363
|
+
since: options.since,
|
|
1364
|
+
status: options.status,
|
|
1365
|
+
limit: options.limit,
|
|
1366
|
+
});
|
|
1367
|
+
if (sessions.length === 0) {
|
|
1368
|
+
info("No sessions found.");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const rows = sessions.map((s) => [
|
|
1372
|
+
dim(s.id.slice(0, 8)),
|
|
1373
|
+
cyan(s.command),
|
|
1374
|
+
formatTimestamp(s.createdAt),
|
|
1375
|
+
colorizeStatus(s.status),
|
|
1376
|
+
]);
|
|
1377
|
+
const table = renderTable({ head: ["ID", "Command", "When", "Status"], colWidths: [12, 12, 20, 12] }, rows);
|
|
1378
|
+
info(table);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
// ---------------------------------------------------------------------------
|
|
1382
|
+
// v0.2.3: Export path — export session transcript to markdown
|
|
1383
|
+
// ---------------------------------------------------------------------------
|
|
1384
|
+
if (options.export && options.session) {
|
|
1385
|
+
// Validate export path is within project root to prevent path traversal
|
|
1386
|
+
try {
|
|
1387
|
+
const { validatePath } = await import('./safety.js');
|
|
1388
|
+
validatePath(path.resolve(options.export), projectRoot);
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
uiError(`Export path must be within the project root: ${options.export}`);
|
|
1392
|
+
process.exit(2);
|
|
1393
|
+
}
|
|
1394
|
+
exportSessionMarkdown(db, options.session, options.export);
|
|
1395
|
+
info(`Session exported to ${path.resolve(options.export)}`);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
// ---------------------------------------------------------------------------
|
|
1399
|
+
// Case 1: --session flag — show full session log
|
|
1210
1400
|
// ---------------------------------------------------------------------------
|
|
1211
1401
|
if (options.session) {
|
|
1212
1402
|
const session = getSession(db, options.session);
|
|
@@ -1264,7 +1454,7 @@ export async function runHistory(options) {
|
|
|
1264
1454
|
if (toolCount > 0) {
|
|
1265
1455
|
info(bold(`Tool Executions (${toolCount}):`));
|
|
1266
1456
|
if (options.tree) {
|
|
1267
|
-
// Render as tree
|
|
1457
|
+
// Render as tree
|
|
1268
1458
|
renderToolTree(db, options.session);
|
|
1269
1459
|
}
|
|
1270
1460
|
else {
|
|
@@ -1285,30 +1475,55 @@ export async function runHistory(options) {
|
|
|
1285
1475
|
return;
|
|
1286
1476
|
}
|
|
1287
1477
|
// ---------------------------------------------------------------------------
|
|
1288
|
-
// Case 2: No --session flag — list recent sessions
|
|
1478
|
+
// Case 2: No --session flag — list recent sessions
|
|
1289
1479
|
// ---------------------------------------------------------------------------
|
|
1290
1480
|
const PAGE_SIZE = 20;
|
|
1291
1481
|
const limit = options.limit ?? PAGE_SIZE;
|
|
1292
|
-
// Fetch sessions with pagination support
|
|
1482
|
+
// Fetch sessions with pagination support
|
|
1293
1483
|
const sessions = listSessions(db, { limit });
|
|
1294
1484
|
if (sessions.length === 0) {
|
|
1295
|
-
|
|
1485
|
+
if (!isStructuredFormat) {
|
|
1486
|
+
info("No sessions found. Run a command to create your first session.");
|
|
1487
|
+
}
|
|
1488
|
+
if (isStructuredFormat) {
|
|
1489
|
+
process.stdout.write(formatOutput({ version: '1', sessions: [], total: 0 }, options.format, HistoryOutputSchema));
|
|
1490
|
+
}
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
// v0.2.3: structured output
|
|
1494
|
+
if (isStructuredFormat) {
|
|
1495
|
+
const outputSessions = sessions.map((s) => ({
|
|
1496
|
+
id: s.id,
|
|
1497
|
+
command: s.command,
|
|
1498
|
+
status: s.status,
|
|
1499
|
+
createdAt: s.createdAt,
|
|
1500
|
+
completedAt: s.completedAt ?? null,
|
|
1501
|
+
}));
|
|
1502
|
+
process.stdout.write(formatOutput({ version: '1', sessions: outputSessions, total: sessions.length }, options.format, HistoryOutputSchema));
|
|
1296
1503
|
return;
|
|
1297
1504
|
}
|
|
1298
|
-
//
|
|
1505
|
+
// v0.2.3: plain format — tab-separated, no cli-table3 borders
|
|
1506
|
+
if (isPlainFormat) {
|
|
1507
|
+
process.stdout.write('ID\tCommand\tWhen\tStatus\n');
|
|
1508
|
+
for (const s of sessions) {
|
|
1509
|
+
process.stdout.write(`${s.id.slice(0, 8)}\t${s.command}\t${s.createdAt}\t${s.status}\n`);
|
|
1510
|
+
}
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
// Build table rows
|
|
1299
1514
|
const rows = sessions.map((s) => [
|
|
1300
1515
|
dim(s.id.slice(0, 8)), // abbreviated ID
|
|
1301
1516
|
cyan(s.command),
|
|
1302
|
-
formatTimestamp(s.createdAt), // human-readable timestamp
|
|
1517
|
+
formatTimestamp(s.createdAt), // human-readable timestamp
|
|
1303
1518
|
colorizeStatus(s.status),
|
|
1304
1519
|
]);
|
|
1305
|
-
// Render table with cli-table3
|
|
1520
|
+
// Render table with cli-table3
|
|
1306
1521
|
const table = renderTable({
|
|
1307
1522
|
head: ["ID", "Command", "When", "Status"],
|
|
1308
1523
|
colWidths: [12, 12, 20, 12],
|
|
1309
1524
|
}, rows);
|
|
1310
1525
|
info(table);
|
|
1311
|
-
// Show pagination hint if there may be more results
|
|
1526
|
+
// Show pagination hint if there may be more results
|
|
1312
1527
|
if (sessions.length === limit && !options.limit) {
|
|
1313
1528
|
info(dim(`\nShowing ${limit} most recent sessions. Use --limit <n> to see more.`));
|
|
1314
1529
|
}
|
|
@@ -1399,7 +1614,6 @@ function parseConfigValue(value) {
|
|
|
1399
1614
|
}
|
|
1400
1615
|
/**
|
|
1401
1616
|
* Display the effective configuration with precedence sources.
|
|
1402
|
-
* Requirements: 15.2
|
|
1403
1617
|
*/
|
|
1404
1618
|
function displayEffectiveConfig(projectRoot, config) {
|
|
1405
1619
|
const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
|
|
@@ -1440,13 +1654,12 @@ function displayEffectiveConfig(projectRoot, config) {
|
|
|
1440
1654
|
* Execute the config command.
|
|
1441
1655
|
*
|
|
1442
1656
|
* Subcommands:
|
|
1443
|
-
* - (none): Display effective configuration with precedence sources
|
|
1444
|
-
* - get <key>: Display value for specified key
|
|
1445
|
-
* - set <key> <value>: Write key-value to ~/.aria/config.toml
|
|
1446
|
-
* - path: Display configuration file resolution paths
|
|
1447
|
-
* - init: Create ./.aria.toml with default values
|
|
1657
|
+
* - (none): Display effective configuration with precedence sources
|
|
1658
|
+
* - get <key>: Display value for specified key
|
|
1659
|
+
* - set <key> <value>: Write key-value to ~/.aria/config.toml
|
|
1660
|
+
* - path: Display configuration file resolution paths
|
|
1661
|
+
* - init: Create ./.aria.toml with default values
|
|
1448
1662
|
*
|
|
1449
|
-
* Requirements: 15.1–15.10
|
|
1450
1663
|
*/
|
|
1451
1664
|
export async function runConfig(options) {
|
|
1452
1665
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
@@ -1456,14 +1669,14 @@ export async function runConfig(options) {
|
|
|
1456
1669
|
const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
|
|
1457
1670
|
const projectConfigPath = path.join(projectRoot, ".aria.toml");
|
|
1458
1671
|
// ---------------------------------------------------------------------------
|
|
1459
|
-
// No subcommand: display effective configuration
|
|
1672
|
+
// No subcommand: display effective configuration
|
|
1460
1673
|
// ---------------------------------------------------------------------------
|
|
1461
1674
|
if (!options.subcommand) {
|
|
1462
1675
|
displayEffectiveConfig(projectRoot, config);
|
|
1463
1676
|
return;
|
|
1464
1677
|
}
|
|
1465
1678
|
// ---------------------------------------------------------------------------
|
|
1466
|
-
// config path: display config file resolution paths
|
|
1679
|
+
// config path: display config file resolution paths
|
|
1467
1680
|
// ---------------------------------------------------------------------------
|
|
1468
1681
|
if (options.subcommand === "path") {
|
|
1469
1682
|
info(bold("Configuration file paths:"));
|
|
@@ -1476,7 +1689,7 @@ export async function runConfig(options) {
|
|
|
1476
1689
|
return;
|
|
1477
1690
|
}
|
|
1478
1691
|
// ---------------------------------------------------------------------------
|
|
1479
|
-
// config get <key>: display value for key
|
|
1692
|
+
// config get <key>: display value for key
|
|
1480
1693
|
// ---------------------------------------------------------------------------
|
|
1481
1694
|
if (options.subcommand === "get") {
|
|
1482
1695
|
const key = options.key;
|
|
@@ -1489,14 +1702,14 @@ export async function runConfig(options) {
|
|
|
1489
1702
|
return;
|
|
1490
1703
|
}
|
|
1491
1704
|
// ---------------------------------------------------------------------------
|
|
1492
|
-
// config set <key> <value>: write to user config
|
|
1705
|
+
// config set <key> <value>: write to user config
|
|
1493
1706
|
// ---------------------------------------------------------------------------
|
|
1494
1707
|
if (options.subcommand === "set") {
|
|
1495
1708
|
const key = options.key;
|
|
1496
1709
|
const rawValue = options.value;
|
|
1497
1710
|
// Parse the value to the appropriate type
|
|
1498
1711
|
const parsedValue = parseConfigValue(rawValue);
|
|
1499
|
-
// Validate by applying to current config and re-validating
|
|
1712
|
+
// Validate by applying to current config and re-validating
|
|
1500
1713
|
const currentMerged = loadConfig(projectRoot);
|
|
1501
1714
|
const updatedMerged = setNestedValue(currentMerged, key, parsedValue);
|
|
1502
1715
|
let validatedConfig;
|
|
@@ -1512,16 +1725,16 @@ export async function runConfig(options) {
|
|
|
1512
1725
|
? readFileSync(userConfigPath, "utf-8")
|
|
1513
1726
|
: "";
|
|
1514
1727
|
const newContent = serializeConfigToToml(validatedConfig);
|
|
1515
|
-
// Preview the diff
|
|
1728
|
+
// Preview the diff
|
|
1516
1729
|
const diffOutput = generateAndRenderDiff(userConfigPath, oldContent, newContent);
|
|
1517
1730
|
info(bold("Preview:"));
|
|
1518
1731
|
info(diffOutput);
|
|
1519
|
-
// If --dry-run, exit without writing
|
|
1732
|
+
// If --dry-run, exit without writing
|
|
1520
1733
|
if (options.dryRun) {
|
|
1521
1734
|
info(yellow("Dry-run mode — no changes written."));
|
|
1522
1735
|
return;
|
|
1523
1736
|
}
|
|
1524
|
-
// If not --yes, prompt for confirmation
|
|
1737
|
+
// If not --yes, prompt for confirmation
|
|
1525
1738
|
if (!options.yes) {
|
|
1526
1739
|
let confirmed;
|
|
1527
1740
|
try {
|
|
@@ -1539,7 +1752,7 @@ export async function runConfig(options) {
|
|
|
1539
1752
|
process.exit(130);
|
|
1540
1753
|
}
|
|
1541
1754
|
}
|
|
1542
|
-
// Write to ~/.aria/config.toml
|
|
1755
|
+
// Write to ~/.aria/config.toml
|
|
1543
1756
|
const ariaDir = path.join(os.homedir(), ".aria");
|
|
1544
1757
|
if (!existsSync(ariaDir)) {
|
|
1545
1758
|
mkdirSync(ariaDir, { recursive: true });
|
|
@@ -1549,7 +1762,7 @@ export async function runConfig(options) {
|
|
|
1549
1762
|
return;
|
|
1550
1763
|
}
|
|
1551
1764
|
// ---------------------------------------------------------------------------
|
|
1552
|
-
// config init: create ./.aria.toml with defaults
|
|
1765
|
+
// config init: create ./.aria.toml with defaults
|
|
1553
1766
|
// ---------------------------------------------------------------------------
|
|
1554
1767
|
if (options.subcommand === "init") {
|
|
1555
1768
|
// Generate default config content
|
|
@@ -1562,12 +1775,12 @@ export async function runConfig(options) {
|
|
|
1562
1775
|
const diffOutput = generateAndRenderDiff(projectConfigPath, oldContent, defaultContent);
|
|
1563
1776
|
info(bold("Preview (.aria.toml):"));
|
|
1564
1777
|
info(diffOutput);
|
|
1565
|
-
// If --dry-run, exit without writing
|
|
1778
|
+
// If --dry-run, exit without writing
|
|
1566
1779
|
if (options.dryRun) {
|
|
1567
1780
|
info(yellow("Dry-run mode — no file created."));
|
|
1568
1781
|
return;
|
|
1569
1782
|
}
|
|
1570
|
-
// If not --yes, prompt for confirmation
|
|
1783
|
+
// If not --yes, prompt for confirmation
|
|
1571
1784
|
if (!options.yes) {
|
|
1572
1785
|
let confirmed;
|
|
1573
1786
|
try {
|
|
@@ -1585,7 +1798,7 @@ export async function runConfig(options) {
|
|
|
1585
1798
|
process.exit(130);
|
|
1586
1799
|
}
|
|
1587
1800
|
}
|
|
1588
|
-
// Write ./.aria.toml
|
|
1801
|
+
// Write ./.aria.toml
|
|
1589
1802
|
writeFileSync(projectConfigPath, defaultContent, "utf-8");
|
|
1590
1803
|
info(green(`✓ Created ${projectConfigPath}`));
|
|
1591
1804
|
return;
|
|
@@ -1597,7 +1810,6 @@ export async function runConfig(options) {
|
|
|
1597
1810
|
* Runs a series of environment diagnostic checks and reports results.
|
|
1598
1811
|
* Exits with code 1 if any critical check fails.
|
|
1599
1812
|
*
|
|
1600
|
-
* Requirements: 16.1–16.13
|
|
1601
1813
|
*/
|
|
1602
1814
|
export async function runDoctor(options = {}) {
|
|
1603
1815
|
const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
|
|
@@ -1612,7 +1824,7 @@ export async function runDoctor(options = {}) {
|
|
|
1612
1824
|
}
|
|
1613
1825
|
const checks = [];
|
|
1614
1826
|
// -------------------------------------------------------------------------
|
|
1615
|
-
// 1. Node.js version >= 20
|
|
1827
|
+
// 1. Node.js version >= 20
|
|
1616
1828
|
// -------------------------------------------------------------------------
|
|
1617
1829
|
{
|
|
1618
1830
|
const nodeVersion = process.version; // e.g. "v20.11.0"
|
|
@@ -1629,7 +1841,7 @@ export async function runDoctor(options = {}) {
|
|
|
1629
1841
|
}
|
|
1630
1842
|
}
|
|
1631
1843
|
// -------------------------------------------------------------------------
|
|
1632
|
-
// 2. git availability
|
|
1844
|
+
// 2. git availability — WARN only
|
|
1633
1845
|
// -------------------------------------------------------------------------
|
|
1634
1846
|
{
|
|
1635
1847
|
try {
|
|
@@ -1641,7 +1853,7 @@ export async function runDoctor(options = {}) {
|
|
|
1641
1853
|
}
|
|
1642
1854
|
}
|
|
1643
1855
|
// -------------------------------------------------------------------------
|
|
1644
|
-
// 3. ripgrep (rg) availability
|
|
1856
|
+
// 3. ripgrep (rg) availability — WARN only
|
|
1645
1857
|
// -------------------------------------------------------------------------
|
|
1646
1858
|
{
|
|
1647
1859
|
try {
|
|
@@ -1653,7 +1865,7 @@ export async function runDoctor(options = {}) {
|
|
|
1653
1865
|
}
|
|
1654
1866
|
}
|
|
1655
1867
|
// -------------------------------------------------------------------------
|
|
1656
|
-
// 4. Config file syntax and schema validation
|
|
1868
|
+
// 4. Config file syntax and schema validation — CRITICAL
|
|
1657
1869
|
// -------------------------------------------------------------------------
|
|
1658
1870
|
{
|
|
1659
1871
|
const userConfigPath = path.join(os.homedir(), ".aria", "config.toml");
|
|
@@ -1680,7 +1892,7 @@ export async function runDoctor(options = {}) {
|
|
|
1680
1892
|
}
|
|
1681
1893
|
}
|
|
1682
1894
|
// -------------------------------------------------------------------------
|
|
1683
|
-
// 5. History DB accessibility and schema version
|
|
1895
|
+
// 5. History DB accessibility and schema version — CRITICAL
|
|
1684
1896
|
// -------------------------------------------------------------------------
|
|
1685
1897
|
{
|
|
1686
1898
|
try {
|
|
@@ -1698,34 +1910,44 @@ export async function runDoctor(options = {}) {
|
|
|
1698
1910
|
}
|
|
1699
1911
|
}
|
|
1700
1912
|
// -------------------------------------------------------------------------
|
|
1701
|
-
// 6. Provider readiness — API key presence
|
|
1913
|
+
// 6. Provider readiness — API key presence — CRITICAL for default
|
|
1914
|
+
// v0.2.2: report all configured providers, fail only if default key is missing
|
|
1702
1915
|
// -------------------------------------------------------------------------
|
|
1703
1916
|
{
|
|
1704
|
-
const
|
|
1917
|
+
const defaultProvider = config?.provider.default ?? "anthropic";
|
|
1705
1918
|
const keyMap = {
|
|
1706
1919
|
anthropic: "ANTHROPIC_API_KEY",
|
|
1707
1920
|
openai: "OPENAI_API_KEY",
|
|
1708
1921
|
openrouter: "OPENROUTER_API_KEY",
|
|
1709
|
-
ollama:
|
|
1922
|
+
ollama: null, // no key needed
|
|
1710
1923
|
};
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
checks.push({ name: "provider", status: "pass", message: `${
|
|
1924
|
+
// Critical check: default provider must be ready
|
|
1925
|
+
const defaultEnvKey = keyMap[defaultProvider];
|
|
1926
|
+
if (defaultEnvKey === null) {
|
|
1927
|
+
checks.push({ name: "provider", status: "pass", message: `${defaultProvider} (no API key required)` });
|
|
1715
1928
|
}
|
|
1716
|
-
else if (process.env[
|
|
1717
|
-
|
|
1929
|
+
else if (process.env[defaultEnvKey]) {
|
|
1930
|
+
const model = config?.provider.model ?? "default";
|
|
1931
|
+
checks.push({ name: "provider", status: "pass", message: `${defaultProvider} (${defaultEnvKey} present, model: ${model})` });
|
|
1718
1932
|
}
|
|
1719
1933
|
else {
|
|
1720
1934
|
checks.push({
|
|
1721
1935
|
name: "provider",
|
|
1722
1936
|
status: "fail",
|
|
1723
|
-
message: `${
|
|
1937
|
+
message: `${defaultProvider} (${defaultEnvKey} not set)`,
|
|
1724
1938
|
});
|
|
1725
1939
|
}
|
|
1940
|
+
// Non-critical: only report secondary providers that are actually configured (key present)
|
|
1941
|
+
const secondaryProviders = Object.entries(keyMap).filter(([name]) => name !== defaultProvider && name !== "ollama");
|
|
1942
|
+
for (const [name, envKey] of secondaryProviders) {
|
|
1943
|
+
if (envKey && process.env[envKey]) {
|
|
1944
|
+
checks.push({ name: `provider:${name}`, status: "pass", message: `${name} (${envKey} present)` });
|
|
1945
|
+
}
|
|
1946
|
+
// Don't warn about unconfigured secondary providers — too noisy on vanilla installs
|
|
1947
|
+
}
|
|
1726
1948
|
}
|
|
1727
1949
|
// -------------------------------------------------------------------------
|
|
1728
|
-
// 7. Project type detection
|
|
1950
|
+
// 7. Project type detection
|
|
1729
1951
|
// -------------------------------------------------------------------------
|
|
1730
1952
|
{
|
|
1731
1953
|
try {
|
|
@@ -1744,7 +1966,7 @@ export async function runDoctor(options = {}) {
|
|
|
1744
1966
|
}
|
|
1745
1967
|
}
|
|
1746
1968
|
// -------------------------------------------------------------------------
|
|
1747
|
-
// 8. Prisma schema existence and model count
|
|
1969
|
+
// 8. Prisma schema existence and model count
|
|
1748
1970
|
// -------------------------------------------------------------------------
|
|
1749
1971
|
{
|
|
1750
1972
|
try {
|
|
@@ -1784,7 +2006,7 @@ export async function runDoctor(options = {}) {
|
|
|
1784
2006
|
}
|
|
1785
2007
|
}
|
|
1786
2008
|
// -------------------------------------------------------------------------
|
|
1787
|
-
// 9. Ollama reachability if Ollama provider selected
|
|
2009
|
+
// 9. Ollama reachability if Ollama provider selected — WARN
|
|
1788
2010
|
// -------------------------------------------------------------------------
|
|
1789
2011
|
{
|
|
1790
2012
|
const provider = config?.provider.default ?? "anthropic";
|
|
@@ -1807,10 +2029,22 @@ export async function runDoctor(options = {}) {
|
|
|
1807
2029
|
// -------------------------------------------------------------------------
|
|
1808
2030
|
const criticalNames = new Set(["nodejs", "config", "history_db", "provider"]);
|
|
1809
2031
|
const hasCriticalFailure = checks.some((c) => c.status === "fail" && criticalNames.has(c.name));
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2032
|
+
const allPassed = !checks.some((c) => c.status === "fail");
|
|
2033
|
+
if (options.format === 'json' || options.format === 'ndjson') {
|
|
2034
|
+
// v0.2.3: structured output via formatOutput
|
|
2035
|
+
const doctorChecks = checks.map((c) => ({
|
|
2036
|
+
name: c.name,
|
|
2037
|
+
passed: c.status !== 'fail',
|
|
2038
|
+
message: c.message,
|
|
2039
|
+
}));
|
|
2040
|
+
process.stdout.write(formatOutput({ version: '1', checks: doctorChecks, allPassed }, options.format, DoctorOutputSchema));
|
|
2041
|
+
}
|
|
2042
|
+
else if (options.format === 'plain') {
|
|
2043
|
+
// Plain text — no ANSI codes, tab-separated
|
|
2044
|
+
for (const check of checks) {
|
|
2045
|
+
const icon = check.status === 'pass' ? 'OK' : check.status === 'warn' ? 'WARN' : 'FAIL';
|
|
2046
|
+
process.stdout.write(`${icon}\t${check.name}\t${check.message}\n`);
|
|
2047
|
+
}
|
|
1814
2048
|
}
|
|
1815
2049
|
else {
|
|
1816
2050
|
// Text output (Req 16.11)
|
|
@@ -1836,7 +2070,7 @@ export async function runDoctor(options = {}) {
|
|
|
1836
2070
|
info(green("All critical checks passed."));
|
|
1837
2071
|
}
|
|
1838
2072
|
}
|
|
1839
|
-
// Exit with code 1 if any critical check fails
|
|
2073
|
+
// Exit with code 1 if any critical check fails
|
|
1840
2074
|
if (hasCriticalFailure) {
|
|
1841
2075
|
process.exit(1);
|
|
1842
2076
|
}
|