@nathapp/nax 0.38.0 → 0.38.2
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/dist/nax.js +3294 -2907
- package/package.json +2 -2
- package/src/agents/claude-complete.ts +72 -0
- package/src/agents/claude-execution.ts +189 -0
- package/src/agents/claude-interactive.ts +77 -0
- package/src/agents/claude-plan.ts +23 -8
- package/src/agents/claude.ts +64 -349
- package/src/analyze/classifier.ts +2 -1
- package/src/cli/config-descriptions.ts +206 -0
- package/src/cli/config-diff.ts +103 -0
- package/src/cli/config-display.ts +285 -0
- package/src/cli/config-get.ts +55 -0
- package/src/cli/config.ts +7 -618
- package/src/cli/plugins.ts +15 -4
- package/src/cli/prompts-export.ts +58 -0
- package/src/cli/prompts-init.ts +200 -0
- package/src/cli/prompts-main.ts +237 -0
- package/src/cli/prompts-tdd.ts +78 -0
- package/src/cli/prompts.ts +10 -541
- package/src/commands/logs-formatter.ts +201 -0
- package/src/commands/logs-reader.ts +171 -0
- package/src/commands/logs.ts +11 -362
- package/src/config/loader.ts +4 -15
- package/src/config/runtime-types.ts +451 -0
- package/src/config/schema-types.ts +53 -0
- package/src/config/schemas.ts +2 -0
- package/src/config/types.ts +49 -486
- package/src/context/auto-detect.ts +2 -1
- package/src/context/builder.ts +3 -2
- package/src/execution/crash-heartbeat.ts +77 -0
- package/src/execution/crash-recovery.ts +23 -365
- package/src/execution/crash-signals.ts +149 -0
- package/src/execution/crash-writer.ts +154 -0
- package/src/execution/lifecycle/run-setup.ts +7 -1
- package/src/execution/parallel-coordinator.ts +278 -0
- package/src/execution/parallel-executor-rectification-pass.ts +117 -0
- package/src/execution/parallel-executor-rectify.ts +135 -0
- package/src/execution/parallel-executor.ts +19 -211
- package/src/execution/parallel-worker.ts +148 -0
- package/src/execution/parallel.ts +5 -404
- package/src/execution/pid-registry.ts +3 -8
- package/src/execution/runner-completion.ts +160 -0
- package/src/execution/runner-execution.ts +221 -0
- package/src/execution/runner-setup.ts +82 -0
- package/src/execution/runner.ts +53 -202
- package/src/execution/timeout-handler.ts +100 -0
- package/src/hooks/runner.ts +11 -21
- package/src/metrics/tracker.ts +7 -30
- package/src/pipeline/runner.ts +2 -1
- package/src/pipeline/stages/completion.ts +0 -1
- package/src/pipeline/stages/context.ts +2 -1
- package/src/plugins/extensions.ts +225 -0
- package/src/plugins/loader.ts +40 -4
- package/src/plugins/types.ts +18 -221
- package/src/prd/index.ts +2 -1
- package/src/prd/validate.ts +41 -0
- package/src/precheck/checks-blockers.ts +15 -419
- package/src/precheck/checks-cli.ts +68 -0
- package/src/precheck/checks-config.ts +102 -0
- package/src/precheck/checks-git.ts +87 -0
- package/src/precheck/checks-system.ts +163 -0
- package/src/review/orchestrator.ts +19 -6
- package/src/review/runner.ts +17 -5
- package/src/routing/chain.ts +2 -1
- package/src/routing/loader.ts +2 -5
- package/src/tdd/orchestrator.ts +2 -1
- package/src/tdd/verdict-reader.ts +266 -0
- package/src/tdd/verdict.ts +6 -271
- package/src/utils/errors.ts +12 -0
- package/src/utils/git.ts +12 -5
- package/src/utils/json-file.ts +72 -0
- package/src/verification/executor.ts +2 -1
- package/src/verification/smart-runner.ts +23 -3
- package/src/worktree/manager.ts +9 -3
- package/src/worktree/merge.ts +3 -2
package/src/commands/logs.ts
CHANGED
|
@@ -3,25 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Displays run logs with filtering, follow mode, and multiple output formats.
|
|
5
5
|
* Uses resolveProject() for directory resolution and formatter for output.
|
|
6
|
+
*
|
|
7
|
+
* Re-exports reader and formatter modules for backward compatibility.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import { existsSync
|
|
9
|
-
import { readdir } from "node:fs/promises";
|
|
10
|
-
import { homedir } from "node:os";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import type { MetaJson } from "../pipeline/subscribers/registry";
|
|
17
|
-
import { type ResolveProjectOptions, resolveProject } from "./common";
|
|
12
|
+
import type { LogLevel } from "../logger/types";
|
|
13
|
+
import { resolveProject } from "./common";
|
|
14
|
+
import { displayLogs, displayRunsList, followLogs } from "./logs-formatter";
|
|
15
|
+
import { resolveRunFileFromRegistry, selectRunFile } from "./logs-reader";
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
getRunsDir: () => process.env.NAX_RUNS_DIR ?? join(homedir(), ".nax", "runs"),
|
|
24
|
-
};
|
|
17
|
+
// Re-exports for backward compatibility
|
|
18
|
+
export { _deps } from "./logs-reader";
|
|
19
|
+
export { extractRunSummary, resolveRunFileFromRegistry, selectRunFile } from "./logs-reader";
|
|
20
|
+
export { displayLogs, displayRunsList, followLogs, formatDuration } from "./logs-formatter";
|
|
25
21
|
|
|
26
22
|
/**
|
|
27
23
|
* Options for logs command
|
|
@@ -43,78 +39,8 @@ export interface LogsOptions {
|
|
|
43
39
|
json?: boolean;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
/**
|
|
47
|
-
* Log level hierarchy for filtering
|
|
48
|
-
*/
|
|
49
|
-
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
50
|
-
debug: 0,
|
|
51
|
-
info: 1,
|
|
52
|
-
warn: 2,
|
|
53
|
-
error: 3,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Resolve log file path for a runId from the central registry (~/.nax/runs/).
|
|
58
|
-
*
|
|
59
|
-
* Scans all ~/.nax/runs/*\/meta.json entries for an exact or prefix match on runId.
|
|
60
|
-
* Returns the path to the matching run's JSONL file, or null if eventsDir/file is unavailable.
|
|
61
|
-
* Throws if the runId is not found in the registry at all.
|
|
62
|
-
*
|
|
63
|
-
* @param runId - Full or prefix run ID to look up
|
|
64
|
-
* @returns Absolute path to the JSONL log file, or null if unavailable
|
|
65
|
-
*/
|
|
66
|
-
async function resolveRunFileFromRegistry(runId: string): Promise<string | null> {
|
|
67
|
-
const runsDir = _deps.getRunsDir();
|
|
68
|
-
|
|
69
|
-
let entries: string[];
|
|
70
|
-
try {
|
|
71
|
-
entries = await readdir(runsDir);
|
|
72
|
-
} catch {
|
|
73
|
-
throw new Error(`Run not found in registry: ${runId}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let matched: MetaJson | null = null;
|
|
77
|
-
for (const entry of entries) {
|
|
78
|
-
const metaPath = join(runsDir, entry, "meta.json");
|
|
79
|
-
try {
|
|
80
|
-
const meta: MetaJson = await Bun.file(metaPath).json();
|
|
81
|
-
if (meta.runId === runId || meta.runId.startsWith(runId)) {
|
|
82
|
-
matched = meta;
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
// skip unreadable meta.json entries
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!matched) {
|
|
91
|
-
throw new Error(`Run not found in registry: ${runId}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!existsSync(matched.eventsDir)) {
|
|
95
|
-
console.log(`Log directory unavailable for run: ${runId}`);
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const files = readdirSync(matched.eventsDir)
|
|
100
|
-
.filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
|
|
101
|
-
.sort()
|
|
102
|
-
.reverse();
|
|
103
|
-
|
|
104
|
-
if (files.length === 0) {
|
|
105
|
-
console.log(`No log files found for run: ${runId}`);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Look for the specific run file by runId, fall back to newest
|
|
110
|
-
const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
|
|
111
|
-
return join(matched.eventsDir, specificFile ?? files[0]);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
42
|
/**
|
|
115
43
|
* Display logs with filtering and formatting
|
|
116
|
-
*
|
|
117
|
-
* @param options - Command options
|
|
118
44
|
*/
|
|
119
45
|
export async function logsCommand(options: LogsOptions): Promise<void> {
|
|
120
46
|
// When --run <runId> is provided, resolve via central registry
|
|
@@ -175,280 +101,3 @@ export async function logsCommand(options: LogsOptions): Promise<void> {
|
|
|
175
101
|
// Display static logs
|
|
176
102
|
await displayLogs(runFile, options);
|
|
177
103
|
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Select which run file to display (always returns the latest run)
|
|
181
|
-
*/
|
|
182
|
-
async function selectRunFile(runsDir: string): Promise<string | null> {
|
|
183
|
-
const files = readdirSync(runsDir)
|
|
184
|
-
.filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
|
|
185
|
-
.sort()
|
|
186
|
-
.reverse();
|
|
187
|
-
|
|
188
|
-
if (files.length === 0) {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return join(runsDir, files[0]);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Display runs table
|
|
197
|
-
*/
|
|
198
|
-
async function displayRunsList(runsDir: string): Promise<void> {
|
|
199
|
-
const files = readdirSync(runsDir)
|
|
200
|
-
.filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl")
|
|
201
|
-
.sort()
|
|
202
|
-
.reverse();
|
|
203
|
-
|
|
204
|
-
if (files.length === 0) {
|
|
205
|
-
console.log(chalk.dim("No runs found"));
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
console.log(chalk.bold("\nRuns:\n"));
|
|
210
|
-
console.log(chalk.gray(" Timestamp Stories Duration Cost Status"));
|
|
211
|
-
console.log(chalk.gray(" ─────────────────────────────────────────────────────────"));
|
|
212
|
-
|
|
213
|
-
for (const file of files) {
|
|
214
|
-
const filePath = join(runsDir, file);
|
|
215
|
-
const summary = await extractRunSummary(filePath);
|
|
216
|
-
|
|
217
|
-
const timestamp = file.replace(".jsonl", "");
|
|
218
|
-
const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
|
|
219
|
-
const duration = summary ? formatDuration(summary.durationMs) : "?";
|
|
220
|
-
const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
|
|
221
|
-
const status = summary ? (summary.failed === 0 ? chalk.green("✓") : chalk.red("✗")) : "?";
|
|
222
|
-
|
|
223
|
-
console.log(` ${timestamp} ${stories.padEnd(7)} ${duration.padEnd(8)} ${cost.padEnd(8)} ${status}`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
console.log();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Extract run summary from log file
|
|
231
|
-
*/
|
|
232
|
-
async function extractRunSummary(filePath: string): Promise<RunSummary | null> {
|
|
233
|
-
const file = Bun.file(filePath);
|
|
234
|
-
const content = await file.text();
|
|
235
|
-
const lines = content.trim().split("\n");
|
|
236
|
-
|
|
237
|
-
let total = 0;
|
|
238
|
-
let passed = 0;
|
|
239
|
-
let failed = 0;
|
|
240
|
-
let skipped = 0;
|
|
241
|
-
let totalCost = 0;
|
|
242
|
-
let startedAt = "";
|
|
243
|
-
let completedAt: string | undefined;
|
|
244
|
-
let firstTimestamp = "";
|
|
245
|
-
let lastTimestamp = "";
|
|
246
|
-
|
|
247
|
-
for (const line of lines) {
|
|
248
|
-
if (!line.trim()) continue;
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const entry: LogEntry = JSON.parse(line);
|
|
252
|
-
|
|
253
|
-
if (!firstTimestamp) {
|
|
254
|
-
firstTimestamp = entry.timestamp;
|
|
255
|
-
}
|
|
256
|
-
lastTimestamp = entry.timestamp;
|
|
257
|
-
|
|
258
|
-
if (entry.stage === "run.start") {
|
|
259
|
-
startedAt = entry.timestamp;
|
|
260
|
-
const runData = entry.data as Record<string, unknown>;
|
|
261
|
-
total = typeof runData?.totalStories === "number" ? runData.totalStories : 0;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (entry.stage === "story.complete" || entry.stage === "agent.complete") {
|
|
265
|
-
const data = entry.data as Record<string, unknown>;
|
|
266
|
-
const success = data?.success ?? true;
|
|
267
|
-
const action = data?.finalAction || data?.action;
|
|
268
|
-
|
|
269
|
-
if (success) {
|
|
270
|
-
passed++;
|
|
271
|
-
} else if (action === "skip") {
|
|
272
|
-
skipped++;
|
|
273
|
-
} else {
|
|
274
|
-
failed++;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (data?.cost && typeof data.cost === "number") {
|
|
278
|
-
totalCost += data.cost;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (entry.stage === "run.end") {
|
|
283
|
-
completedAt = entry.timestamp;
|
|
284
|
-
}
|
|
285
|
-
} catch {
|
|
286
|
-
// Skip invalid JSON lines
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (!startedAt) {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const durationMs = lastTimestamp ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime() : 0;
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
total,
|
|
298
|
-
passed,
|
|
299
|
-
failed,
|
|
300
|
-
skipped,
|
|
301
|
-
durationMs,
|
|
302
|
-
totalCost,
|
|
303
|
-
startedAt,
|
|
304
|
-
completedAt,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Display static logs
|
|
310
|
-
*/
|
|
311
|
-
async function displayLogs(filePath: string, options: LogsOptions): Promise<void> {
|
|
312
|
-
const file = Bun.file(filePath);
|
|
313
|
-
const content = await file.text();
|
|
314
|
-
const lines = content.trim().split("\n");
|
|
315
|
-
|
|
316
|
-
const mode: VerbosityMode = options.json ? "json" : "normal";
|
|
317
|
-
|
|
318
|
-
for (const line of lines) {
|
|
319
|
-
if (!line.trim()) continue;
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
const entry: LogEntry = JSON.parse(line);
|
|
323
|
-
|
|
324
|
-
// Apply filters
|
|
325
|
-
if (!shouldDisplayEntry(entry, options)) {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Format and display
|
|
330
|
-
const formatted = formatLogEntry(entry, { mode, useColor: true });
|
|
331
|
-
|
|
332
|
-
if (formatted.shouldDisplay && formatted.output) {
|
|
333
|
-
console.log(formatted.output);
|
|
334
|
-
}
|
|
335
|
-
} catch {
|
|
336
|
-
// Skip invalid JSON lines
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Display summary footer (unless in json mode)
|
|
341
|
-
if (!options.json) {
|
|
342
|
-
const summary = await extractRunSummary(filePath);
|
|
343
|
-
if (summary) {
|
|
344
|
-
console.log(formatRunSummary(summary, { mode: "normal", useColor: true }));
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Follow logs in real-time (tail -f mode)
|
|
351
|
-
*/
|
|
352
|
-
async function followLogs(filePath: string, options: LogsOptions): Promise<void> {
|
|
353
|
-
const mode: VerbosityMode = options.json ? "json" : "normal";
|
|
354
|
-
|
|
355
|
-
// Display existing logs first
|
|
356
|
-
const file = Bun.file(filePath);
|
|
357
|
-
const content = await file.text();
|
|
358
|
-
const lines = content.trim().split("\n");
|
|
359
|
-
|
|
360
|
-
for (const line of lines) {
|
|
361
|
-
if (!line.trim()) continue;
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
const entry: LogEntry = JSON.parse(line);
|
|
365
|
-
|
|
366
|
-
if (!shouldDisplayEntry(entry, options)) {
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const formatted = formatLogEntry(entry, { mode, useColor: true });
|
|
371
|
-
|
|
372
|
-
if (formatted.shouldDisplay && formatted.output) {
|
|
373
|
-
console.log(formatted.output);
|
|
374
|
-
}
|
|
375
|
-
} catch {
|
|
376
|
-
// Skip invalid JSON lines
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Now watch for new lines
|
|
381
|
-
let lastSize = (await Bun.file(filePath).stat()).size;
|
|
382
|
-
|
|
383
|
-
while (true) {
|
|
384
|
-
await Bun.sleep(500);
|
|
385
|
-
|
|
386
|
-
const currentSize = (await Bun.file(filePath).stat()).size;
|
|
387
|
-
|
|
388
|
-
if (currentSize > lastSize) {
|
|
389
|
-
// File has grown, read new content
|
|
390
|
-
const newFile = Bun.file(filePath);
|
|
391
|
-
const newContent = await newFile.text();
|
|
392
|
-
const newLines = newContent.slice(lastSize).trim().split("\n");
|
|
393
|
-
|
|
394
|
-
for (const line of newLines) {
|
|
395
|
-
if (!line.trim()) continue;
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
const entry: LogEntry = JSON.parse(line);
|
|
399
|
-
|
|
400
|
-
if (!shouldDisplayEntry(entry, options)) {
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const formatted = formatLogEntry(entry, { mode, useColor: true });
|
|
405
|
-
|
|
406
|
-
if (formatted.shouldDisplay && formatted.output) {
|
|
407
|
-
console.log(formatted.output);
|
|
408
|
-
}
|
|
409
|
-
} catch {
|
|
410
|
-
// Skip invalid JSON lines
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
lastSize = currentSize;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Check if entry should be displayed based on filters
|
|
421
|
-
*/
|
|
422
|
-
function shouldDisplayEntry(entry: LogEntry, options: LogsOptions): boolean {
|
|
423
|
-
// Story filter
|
|
424
|
-
if (options.story && entry.storyId !== options.story) {
|
|
425
|
-
return false;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Level filter
|
|
429
|
-
if (options.level) {
|
|
430
|
-
const entryPriority = LOG_LEVEL_PRIORITY[entry.level];
|
|
431
|
-
const filterPriority = LOG_LEVEL_PRIORITY[options.level];
|
|
432
|
-
|
|
433
|
-
if (entryPriority < filterPriority) {
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return true;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Format duration in milliseconds
|
|
443
|
-
*/
|
|
444
|
-
function formatDuration(ms: number): string {
|
|
445
|
-
if (ms < 1000) {
|
|
446
|
-
return `${ms}ms`;
|
|
447
|
-
}
|
|
448
|
-
if (ms < 60000) {
|
|
449
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
450
|
-
}
|
|
451
|
-
const minutes = Math.floor(ms / 60000);
|
|
452
|
-
const seconds = Math.floor((ms % 60000) / 1000);
|
|
453
|
-
return `${minutes}m${seconds}s`;
|
|
454
|
-
}
|
package/src/config/loader.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { getLogger } from "../logger";
|
|
10
|
+
import { loadJsonFile } from "../utils/json-file";
|
|
10
11
|
import { deepMergeConfig } from "./merger";
|
|
11
12
|
import { MAX_DIRECTORY_DEPTH } from "./path-security";
|
|
12
|
-
import { globalConfigDir
|
|
13
|
+
import { globalConfigDir } from "./paths";
|
|
13
14
|
import { DEFAULT_CONFIG, type NaxConfig, NaxConfigSchema } from "./schema";
|
|
14
15
|
|
|
15
16
|
/** Global config path */
|
|
@@ -36,18 +37,6 @@ export function findProjectDir(startDir: string = process.cwd()): string | null
|
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/** Load and parse a JSON config file */
|
|
40
|
-
async function loadJsonFile<T>(path: string): Promise<T | null> {
|
|
41
|
-
if (!existsSync(path)) return null;
|
|
42
|
-
try {
|
|
43
|
-
return await Bun.file(path).json();
|
|
44
|
-
} catch (err) {
|
|
45
|
-
const logger = getLogger();
|
|
46
|
-
logger.warn("config", "Failed to parse config file", { path, error: String(err) });
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
40
|
/** @internal Backward compat: map deprecated routing.llm.batchMode to routing.llm.mode.
|
|
52
41
|
* Returns a new object (immutable -- does not mutate the input). */
|
|
53
42
|
function applyBatchModeCompat(conf: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -83,7 +72,7 @@ export async function loadConfig(projectDir?: string, cliOverrides?: Record<stri
|
|
|
83
72
|
let rawConfig: Record<string, unknown> = structuredClone(DEFAULT_CONFIG as unknown as Record<string, unknown>);
|
|
84
73
|
|
|
85
74
|
// Layer 1: Global config (~/.nax/config.json)
|
|
86
|
-
const globalConfRaw = await loadJsonFile<Record<string, unknown>>(globalConfigPath());
|
|
75
|
+
const globalConfRaw = await loadJsonFile<Record<string, unknown>>(globalConfigPath(), "config");
|
|
87
76
|
if (globalConfRaw) {
|
|
88
77
|
// Backward compatibility: apply batchMode->mode shim before merge so defaults don't shadow it
|
|
89
78
|
const globalConf = applyBatchModeCompat(globalConfRaw);
|
|
@@ -93,7 +82,7 @@ export async function loadConfig(projectDir?: string, cliOverrides?: Record<stri
|
|
|
93
82
|
// Layer 2: Project config (nax/config.json)
|
|
94
83
|
const projDir = projectDir ?? findProjectDir();
|
|
95
84
|
if (projDir) {
|
|
96
|
-
const projConf = await loadJsonFile<Record<string, unknown>>(join(projDir, "config.json"));
|
|
85
|
+
const projConf = await loadJsonFile<Record<string, unknown>>(join(projDir, "config.json"), "config");
|
|
97
86
|
if (projConf) {
|
|
98
87
|
// Backward compatibility: map deprecated batchMode -> mode on raw user config
|
|
99
88
|
// MUST run before deepMergeConfig so defaults don't shadow the check.
|