@eugene218/noxdev 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,2380 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { createRequire } from "module";
5
+ import { Command } from "commander";
6
+ import chalk12 from "chalk";
7
+
8
+ // src/brand.ts
9
+ var BANNER = (version2) => ` ,___,
10
+ [O.O] noxdev v${version2}
11
+ /) )\\ ship code while you sleep
12
+ " \\|/ "
13
+ ---m-m---`;
14
+
15
+ // src/commands/init.ts
16
+ import { execSync } from "child_process";
17
+ import {
18
+ existsSync,
19
+ readFileSync,
20
+ mkdirSync as mkdirSync2,
21
+ writeFileSync
22
+ } from "fs";
23
+ import { join as join2, resolve } from "path";
24
+ import { homedir as homedir2 } from "os";
25
+ import chalk from "chalk";
26
+ import ora from "ora";
27
+
28
+ // src/db/index.ts
29
+ import Database from "better-sqlite3";
30
+ import { mkdirSync } from "fs";
31
+ import { join } from "path";
32
+ import { homedir } from "os";
33
+
34
+ // src/db/migrate.ts
35
+ var SCHEMA = `
36
+ CREATE TABLE IF NOT EXISTS projects (
37
+ id TEXT PRIMARY KEY,
38
+ display_name TEXT NOT NULL,
39
+ repo_path TEXT NOT NULL,
40
+ worktree_path TEXT NOT NULL,
41
+ branch TEXT NOT NULL,
42
+ test_command TEXT,
43
+ build_command TEXT,
44
+ lint_command TEXT,
45
+ docker_memory TEXT DEFAULT '4g',
46
+ docker_cpus INTEGER DEFAULT 2,
47
+ docker_timeout_seconds INTEGER DEFAULT 1800,
48
+ secrets_file TEXT,
49
+ created_at TEXT DEFAULT (datetime('now')),
50
+ updated_at TEXT DEFAULT (datetime('now'))
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS runs (
54
+ id TEXT PRIMARY KEY,
55
+ project_id TEXT NOT NULL REFERENCES projects(id),
56
+ started_at TEXT NOT NULL,
57
+ finished_at TEXT,
58
+ auth_mode TEXT NOT NULL DEFAULT 'max',
59
+ total_tasks INTEGER DEFAULT 0,
60
+ completed INTEGER DEFAULT 0,
61
+ failed INTEGER DEFAULT 0,
62
+ skipped INTEGER DEFAULT 0,
63
+ status TEXT DEFAULT 'running',
64
+ log_file TEXT,
65
+ commit_before TEXT,
66
+ commit_after TEXT
67
+ );
68
+
69
+ CREATE TABLE IF NOT EXISTS task_results (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ run_id TEXT NOT NULL REFERENCES runs(id),
72
+ task_id TEXT NOT NULL,
73
+ title TEXT NOT NULL,
74
+ status TEXT NOT NULL,
75
+ exit_code INTEGER,
76
+ auth_mode TEXT,
77
+ critic_mode TEXT DEFAULT 'review',
78
+ push_mode TEXT DEFAULT 'auto',
79
+ attempt INTEGER DEFAULT 1,
80
+ commit_sha TEXT,
81
+ started_at TEXT,
82
+ finished_at TEXT,
83
+ duration_seconds INTEGER,
84
+ dev_log_file TEXT,
85
+ critic_log_file TEXT,
86
+ diff_file TEXT,
87
+ merge_decision TEXT DEFAULT 'pending',
88
+ merged_at TEXT
89
+ );
90
+
91
+ CREATE INDEX IF NOT EXISTS idx_task_results_run ON task_results(run_id);
92
+ CREATE INDEX IF NOT EXISTS idx_task_results_status ON task_results(status);
93
+
94
+ CREATE TABLE IF NOT EXISTS tasks (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ run_id TEXT NOT NULL REFERENCES runs(id),
97
+ task_id TEXT NOT NULL,
98
+ title TEXT NOT NULL,
99
+ files TEXT,
100
+ verify TEXT,
101
+ critic TEXT DEFAULT 'review',
102
+ push TEXT DEFAULT 'auto',
103
+ spec TEXT,
104
+ status_before TEXT DEFAULT 'pending',
105
+ UNIQUE(run_id, task_id)
106
+ );
107
+ `;
108
+ function migrate(db) {
109
+ db.exec(SCHEMA);
110
+ }
111
+
112
+ // src/db/index.ts
113
+ var DB_DIR = join(homedir(), ".noxdev");
114
+ var DB_PATH = join(DB_DIR, "ledger.db");
115
+ var _db;
116
+ function getDb() {
117
+ if (_db) return _db;
118
+ mkdirSync(DB_DIR, { recursive: true });
119
+ _db = new Database(DB_PATH);
120
+ _db.pragma("journal_mode = WAL");
121
+ _db.pragma("foreign_keys = ON");
122
+ migrate(_db);
123
+ return _db;
124
+ }
125
+
126
+ // src/commands/init.ts
127
+ function detectCommands(repoPath) {
128
+ const defaults = {
129
+ test_command: "pnpm test",
130
+ build_command: "pnpm build",
131
+ lint_command: "pnpm lint"
132
+ };
133
+ const pkgPath = join2(repoPath, "package.json");
134
+ if (!existsSync(pkgPath)) return defaults;
135
+ try {
136
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
137
+ const scripts = pkg.scripts ?? {};
138
+ return {
139
+ test_command: scripts.test ? `pnpm test` : defaults.test_command,
140
+ build_command: scripts.build ? `pnpm build` : defaults.build_command,
141
+ lint_command: scripts.lint ? `pnpm lint` : defaults.lint_command
142
+ };
143
+ } catch {
144
+ return defaults;
145
+ }
146
+ }
147
+ function registerInit(program2) {
148
+ program2.command("init").description("Initialize a new project").argument("<project>", "project name").requiredOption("--repo <path>", "path to git repository").action(async (project, opts) => {
149
+ try {
150
+ await runInit(project, opts.repo);
151
+ } catch (err) {
152
+ console.error(
153
+ chalk.red(
154
+ `Error: ${err instanceof Error ? err.message : String(err)}`
155
+ )
156
+ );
157
+ process.exitCode = 1;
158
+ }
159
+ });
160
+ }
161
+ async function runInit(project, repoPath) {
162
+ const resolvedRepo = resolve(repoPath);
163
+ const branch = `noxdev/${project}`;
164
+ const worktreePath = join2(homedir2(), "worktrees", project);
165
+ if (!existsSync(resolvedRepo)) {
166
+ throw new Error(`Repository path does not exist: ${resolvedRepo}`);
167
+ }
168
+ const gitDir = join2(resolvedRepo, ".git");
169
+ if (!existsSync(gitDir)) {
170
+ throw new Error(
171
+ `Not a git repository (missing .git): ${resolvedRepo}`
172
+ );
173
+ }
174
+ console.log(chalk.green("\u2713") + " Repository validated: " + resolvedRepo);
175
+ try {
176
+ execSync("git rev-parse HEAD", { cwd: resolvedRepo, stdio: "pipe" });
177
+ } catch {
178
+ console.log(chalk.yellow(" \u26A0 Empty repository detected. Creating initial commit..."));
179
+ const readmePath = join2(resolvedRepo, "README.md");
180
+ if (!existsSync(readmePath)) {
181
+ writeFileSync(readmePath, `# ${project}
182
+ `);
183
+ }
184
+ execSync("git add .", { cwd: resolvedRepo, stdio: "pipe" });
185
+ try {
186
+ execSync("git config user.name", { cwd: resolvedRepo, stdio: "pipe" });
187
+ } catch {
188
+ execSync('git config user.name "noxdev"', { cwd: resolvedRepo, stdio: "pipe" });
189
+ }
190
+ try {
191
+ execSync("git config user.email", { cwd: resolvedRepo, stdio: "pipe" });
192
+ } catch {
193
+ execSync('git config user.email "noxdev@local"', { cwd: resolvedRepo, stdio: "pipe" });
194
+ }
195
+ execSync('git commit -m "init"', { cwd: resolvedRepo, stdio: "pipe" });
196
+ console.log(chalk.green(" \u2713 Initial commit created"));
197
+ }
198
+ const spinnerWt = ora("Creating git worktree\u2026").start();
199
+ try {
200
+ const defaultBranch = execSync("git symbolic-ref --short HEAD", {
201
+ cwd: resolvedRepo,
202
+ encoding: "utf-8"
203
+ }).trim();
204
+ let branchExists = false;
205
+ try {
206
+ const result = execSync(`git branch --list ${branch}`, {
207
+ cwd: resolvedRepo,
208
+ encoding: "utf-8"
209
+ }).trim();
210
+ branchExists = result.length > 0;
211
+ } catch {
212
+ }
213
+ let worktreeExisted = false;
214
+ if (branchExists) {
215
+ try {
216
+ execSync(`git worktree add ${worktreePath} ${branch}`, {
217
+ cwd: resolvedRepo,
218
+ stdio: "pipe"
219
+ });
220
+ } catch (err) {
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ if (msg.includes("already exists")) {
223
+ worktreeExisted = true;
224
+ } else {
225
+ throw err;
226
+ }
227
+ }
228
+ } else {
229
+ try {
230
+ execSync(
231
+ `git worktree add -b ${branch} ${worktreePath} ${defaultBranch}`,
232
+ { cwd: resolvedRepo, stdio: "pipe" }
233
+ );
234
+ } catch (err) {
235
+ const msg = err instanceof Error ? err.message : String(err);
236
+ if (msg.includes("already exists")) {
237
+ worktreeExisted = true;
238
+ } else {
239
+ throw err;
240
+ }
241
+ }
242
+ }
243
+ if (worktreeExisted) {
244
+ spinnerWt.warn(`Worktree already exists at ${worktreePath}`);
245
+ } else {
246
+ spinnerWt.succeed(
247
+ `Worktree created at ${chalk.cyan(worktreePath)}`
248
+ );
249
+ }
250
+ } catch (err) {
251
+ spinnerWt.fail("Failed to create worktree");
252
+ throw err;
253
+ }
254
+ const detected = detectCommands(resolvedRepo);
255
+ const configDir = join2(resolvedRepo, ".noxdev");
256
+ const configPath = join2(configDir, "config.json");
257
+ const projectConfig = {
258
+ project,
259
+ display_name: project,
260
+ test_command: detected.test_command,
261
+ build_command: detected.build_command,
262
+ lint_command: detected.lint_command,
263
+ docker: {
264
+ memory: "4g",
265
+ cpus: 2,
266
+ timeout_minutes: 30
267
+ },
268
+ secrets: "",
269
+ tasks_file: "TASKS.md",
270
+ critic_default: "strict",
271
+ push_default: "never"
272
+ };
273
+ mkdirSync2(configDir, { recursive: true });
274
+ writeFileSync(configPath, JSON.stringify(projectConfig, null, 2) + "\n");
275
+ console.log(chalk.green("\u2713") + " Config written: " + configPath);
276
+ const db = getDb();
277
+ const existing = db.prepare("SELECT id FROM projects WHERE id = ?").get(project);
278
+ if (existing) {
279
+ console.log(
280
+ chalk.yellow("\u26A0") + ` Project "${project}" already registered \u2014 updating`
281
+ );
282
+ db.prepare(
283
+ `UPDATE projects
284
+ SET repo_path = ?, worktree_path = ?, branch = ?,
285
+ test_command = ?, build_command = ?, lint_command = ?,
286
+ updated_at = datetime('now')
287
+ WHERE id = ?`
288
+ ).run(
289
+ resolvedRepo,
290
+ worktreePath,
291
+ branch,
292
+ detected.test_command,
293
+ detected.build_command,
294
+ detected.lint_command,
295
+ project
296
+ );
297
+ } else {
298
+ db.prepare(
299
+ `INSERT INTO projects (id, display_name, repo_path, worktree_path, branch,
300
+ test_command, build_command, lint_command)
301
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
302
+ ).run(
303
+ project,
304
+ project,
305
+ resolvedRepo,
306
+ worktreePath,
307
+ branch,
308
+ detected.test_command,
309
+ detected.build_command,
310
+ detected.lint_command
311
+ );
312
+ }
313
+ console.log(chalk.green("\u2713") + ` Project "${project}" registered in database`);
314
+ let dockerOk = false;
315
+ try {
316
+ const result = execSync("docker images -q noxdev-runner:latest", {
317
+ encoding: "utf-8",
318
+ stdio: ["pipe", "pipe", "pipe"]
319
+ }).trim();
320
+ dockerOk = result.length > 0;
321
+ } catch {
322
+ }
323
+ if (!dockerOk) {
324
+ console.log(
325
+ chalk.yellow("\u26A0") + " Docker image noxdev-runner:latest not found. Build it before running tasks."
326
+ );
327
+ } else {
328
+ console.log(chalk.green("\u2713") + " Docker image noxdev-runner:latest found");
329
+ }
330
+ console.log("");
331
+ console.log(chalk.bold("Project initialized:"));
332
+ console.log(` Worktree: ${chalk.cyan(worktreePath)}`);
333
+ console.log(` Branch: ${chalk.cyan(branch)}`);
334
+ console.log(` Test: ${detected.test_command}`);
335
+ console.log(` Build: ${detected.build_command}`);
336
+ console.log(` Lint: ${detected.lint_command}`);
337
+ console.log(` Config: ${configPath}`);
338
+ console.log("");
339
+ console.log(
340
+ chalk.blue("\u2192") + ` Write tasks in ${worktreePath}/TASKS.md then run: ${chalk.bold(`noxdev run ${project}`)}`
341
+ );
342
+ }
343
+
344
+ // src/commands/run.ts
345
+ import { join as join7 } from "path";
346
+ import { homedir as homedir6 } from "os";
347
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
348
+ import { spawn, execSync as execSync4 } from "child_process";
349
+ import chalk3 from "chalk";
350
+
351
+ // src/db/queries.ts
352
+ function insertRun(db, run) {
353
+ db.prepare(
354
+ `INSERT INTO runs (id, project_id, started_at, auth_mode, total_tasks, commit_before, log_file)
355
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
356
+ ).run(
357
+ run.id,
358
+ run.projectId,
359
+ run.startedAt,
360
+ run.authMode,
361
+ run.totalTasks,
362
+ run.commitBefore,
363
+ run.logFile
364
+ );
365
+ }
366
+ function updateRunFinished(db, runId, updates) {
367
+ db.prepare(
368
+ `UPDATE runs
369
+ SET finished_at = ?, completed = ?, failed = ?, skipped = ?, status = ?, commit_after = ?
370
+ WHERE id = ?`
371
+ ).run(
372
+ updates.finishedAt,
373
+ updates.completed,
374
+ updates.failed,
375
+ updates.skipped,
376
+ updates.status,
377
+ updates.commitAfter,
378
+ runId
379
+ );
380
+ }
381
+ function insertTaskCache(db, runId, tasks) {
382
+ const stmt = db.prepare(
383
+ `INSERT INTO tasks (run_id, task_id, title, files, verify, critic, push, spec, status_before)
384
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
385
+ );
386
+ const insertMany = db.transaction((rows) => {
387
+ for (const t of rows) {
388
+ stmt.run(runId, t.taskId, t.title, t.files, t.verify, t.critic, t.push, t.spec, t.statusBefore);
389
+ }
390
+ });
391
+ insertMany(tasks);
392
+ }
393
+ function insertTaskResult(db, result) {
394
+ db.prepare(
395
+ `INSERT INTO task_results
396
+ (run_id, task_id, title, status, exit_code, auth_mode, critic_mode, push_mode,
397
+ attempt, commit_sha, started_at, finished_at, duration_seconds,
398
+ dev_log_file, critic_log_file, diff_file)
399
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
400
+ ).run(
401
+ result.runId,
402
+ result.taskId,
403
+ result.title,
404
+ result.status,
405
+ result.exitCode,
406
+ result.authMode,
407
+ result.criticMode,
408
+ result.pushMode,
409
+ result.attempt,
410
+ result.commitSha,
411
+ result.startedAt,
412
+ result.finishedAt,
413
+ result.durationSeconds,
414
+ result.devLogFile,
415
+ result.criticLogFile,
416
+ result.diffFile
417
+ );
418
+ }
419
+ function updateMergeDecision(db, taskResultId, decision, mergedAt) {
420
+ db.prepare(
421
+ `UPDATE task_results SET merge_decision = ?, merged_at = ? WHERE id = ?`
422
+ ).run(decision, mergedAt ?? null, taskResultId);
423
+ }
424
+ function getLatestRun(db, projectId) {
425
+ return db.prepare(`SELECT * FROM runs WHERE project_id = ? ORDER BY started_at DESC LIMIT 1`).get(projectId) ?? null;
426
+ }
427
+ function getTaskResults(db, runId) {
428
+ return db.prepare(`SELECT * FROM task_results WHERE run_id = ?`).all(runId);
429
+ }
430
+ function getPendingMerge(db, runId) {
431
+ return db.prepare(`SELECT * FROM task_results WHERE run_id = ? AND merge_decision = 'pending'`).all(runId);
432
+ }
433
+ function getProject(db, projectId) {
434
+ return db.prepare(`SELECT * FROM projects WHERE id = ?`).get(projectId) ?? null;
435
+ }
436
+ function getAllProjects(db) {
437
+ return db.prepare(
438
+ `SELECT p.*,
439
+ r.id AS latest_run_id,
440
+ r.started_at AS latest_run_started_at,
441
+ r.status AS latest_run_status,
442
+ r.total_tasks AS latest_run_total_tasks,
443
+ r.completed AS latest_run_completed,
444
+ r.failed AS latest_run_failed
445
+ FROM projects p
446
+ LEFT JOIN runs r ON r.id = (
447
+ SELECT r2.id FROM runs r2
448
+ WHERE r2.project_id = p.id
449
+ ORDER BY r2.started_at DESC
450
+ LIMIT 1
451
+ )`
452
+ ).all();
453
+ }
454
+
455
+ // src/config/index.ts
456
+ import { readFileSync as readFileSync2 } from "fs";
457
+ import { join as join3 } from "path";
458
+ import { homedir as homedir3 } from "os";
459
+ var DEFAULT_GLOBAL_CONFIG = {
460
+ accounts: {
461
+ max: {
462
+ preferred: true,
463
+ rate_limit_ceiling: 80
464
+ },
465
+ api: {
466
+ fallback: true,
467
+ daily_cap_usd: 5,
468
+ model: "claude-sonnet-4-6"
469
+ }
470
+ },
471
+ safety: {
472
+ auto_push: false,
473
+ max_retries_per_task: 3,
474
+ circuit_breaker_threshold: 5
475
+ },
476
+ secrets: {
477
+ provider: "age",
478
+ global: "",
479
+ age_key: ""
480
+ }
481
+ };
482
+ var DEFAULT_PROJECT_CONFIG = {
483
+ project: "",
484
+ display_name: "",
485
+ test_command: "pnpm test",
486
+ build_command: "pnpm build",
487
+ lint_command: "pnpm lint",
488
+ docker: {
489
+ memory: "4g",
490
+ cpus: 2,
491
+ timeout_minutes: 30
492
+ },
493
+ secrets: "",
494
+ tasks_file: "TASKS.md",
495
+ critic_default: "strict",
496
+ push_default: "never"
497
+ };
498
+ function deepMerge(defaults, overrides) {
499
+ const result = { ...defaults };
500
+ for (const key of Object.keys(overrides)) {
501
+ const val = overrides[key];
502
+ const def = defaults[key];
503
+ if (val !== null && typeof val === "object" && !Array.isArray(val) && def !== null && typeof def === "object" && !Array.isArray(def)) {
504
+ result[key] = deepMerge(
505
+ def,
506
+ val
507
+ );
508
+ } else {
509
+ result[key] = val;
510
+ }
511
+ }
512
+ return result;
513
+ }
514
+ function readJsonFile(filePath) {
515
+ try {
516
+ const content = readFileSync2(filePath, "utf-8");
517
+ return JSON.parse(content);
518
+ } catch (err) {
519
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
520
+ return null;
521
+ }
522
+ throw new Error(
523
+ `Failed to parse config at ${filePath}: ${err instanceof Error ? err.message : String(err)}`
524
+ );
525
+ }
526
+ }
527
+ function loadGlobalConfig() {
528
+ const configPath = join3(homedir3(), ".noxdev", "config.json");
529
+ const overrides = readJsonFile(configPath);
530
+ if (!overrides) {
531
+ return { ...DEFAULT_GLOBAL_CONFIG };
532
+ }
533
+ return deepMerge(DEFAULT_GLOBAL_CONFIG, overrides);
534
+ }
535
+ function loadProjectConfig(projectPath) {
536
+ const configPath = join3(projectPath, ".noxdev", "config.json");
537
+ const overrides = readJsonFile(configPath);
538
+ if (!overrides) {
539
+ return { ...DEFAULT_PROJECT_CONFIG };
540
+ }
541
+ return deepMerge(DEFAULT_PROJECT_CONFIG, overrides);
542
+ }
543
+
544
+ // src/auth/index.ts
545
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
546
+ import { execSync as execSync2 } from "child_process";
547
+ import { homedir as homedir4 } from "os";
548
+ import { join as join4 } from "path";
549
+ function getMaxCredentialPath() {
550
+ return join4(homedir4(), ".claude.json");
551
+ }
552
+ function isMaxAvailable() {
553
+ const credPath = getMaxCredentialPath();
554
+ if (!existsSync2(credPath)) return false;
555
+ const content = readFileSync3(credPath, "utf-8");
556
+ return content.trim().length > 0;
557
+ }
558
+ function resolveAuth(config) {
559
+ if (config.max.preferred && isMaxAvailable()) {
560
+ return { mode: "max", model: "claude-sonnet-4-20250514" };
561
+ }
562
+ if (config.api.fallback) {
563
+ try {
564
+ const decrypted = execSync2(
565
+ `sops -d --extract '["ANTHROPIC_API_KEY"]' ${config.secrets.globalSecretsFile}`,
566
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
567
+ );
568
+ return { mode: "api", apiKey: decrypted.trim(), model: config.api.model };
569
+ } catch {
570
+ }
571
+ }
572
+ throw new Error(
573
+ "No auth available. Max credentials not found at ~/.claude.json and API fallback is disabled or decryption failed."
574
+ );
575
+ }
576
+
577
+ // src/engine/orchestrator.ts
578
+ import { execSync as execSync3 } from "child_process";
579
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync, readFileSync as readFileSync6, copyFileSync, existsSync as existsSync3 } from "fs";
580
+ import { join as join6 } from "path";
581
+ import { tmpdir, homedir as homedir5 } from "os";
582
+ import chalk2 from "chalk";
583
+
584
+ // src/parser/tasks.ts
585
+ import { readFileSync as readFileSync4 } from "fs";
586
+ var TASK_HEADER_RE = /^## (T\d+): (.+)$/;
587
+ var FIELD_RE = /^- (STATUS|FILES|VERIFY|CRITIC|PUSH|SPEC): (.+)$/i;
588
+ function parseTasks(content, includeDone = false) {
589
+ const lines = content.split("\n");
590
+ const tasks = [];
591
+ let current = null;
592
+ let inSpec = false;
593
+ for (let i = 0; i < lines.length; i++) {
594
+ const line = lines[i];
595
+ const headerMatch = line.match(TASK_HEADER_RE);
596
+ if (headerMatch) {
597
+ if (current) tasks.push(current);
598
+ current = {
599
+ taskId: headerMatch[1],
600
+ title: headerMatch[2],
601
+ status: "pending",
602
+ files: [],
603
+ verify: "",
604
+ critic: "review",
605
+ push: "auto",
606
+ spec: ""
607
+ };
608
+ inSpec = false;
609
+ continue;
610
+ }
611
+ if (!current) continue;
612
+ if (inSpec) {
613
+ if (/^ {2,}/.test(line)) {
614
+ current.spec += "\n" + line.trim();
615
+ continue;
616
+ }
617
+ inSpec = false;
618
+ }
619
+ const fieldMatch = line.match(FIELD_RE);
620
+ if (fieldMatch) {
621
+ const name = fieldMatch[1].toUpperCase();
622
+ const value = fieldMatch[2];
623
+ switch (name) {
624
+ case "STATUS":
625
+ current.status = value.trim().toLowerCase();
626
+ break;
627
+ case "FILES":
628
+ current.files = value.split(",").map((f) => f.trim()).filter((f) => f.length > 0);
629
+ break;
630
+ case "VERIFY":
631
+ current.verify = value.trim();
632
+ break;
633
+ case "CRITIC":
634
+ current.critic = value.trim();
635
+ break;
636
+ case "PUSH":
637
+ current.push = value.trim();
638
+ break;
639
+ case "SPEC":
640
+ current.spec = value.trimEnd();
641
+ inSpec = true;
642
+ break;
643
+ }
644
+ }
645
+ }
646
+ if (current) tasks.push(current);
647
+ if (includeDone) return tasks;
648
+ return tasks.filter((t) => t.status === "pending");
649
+ }
650
+ function parseTasksFromFile(filePath, includeDone = false) {
651
+ const content = readFileSync4(filePath, "utf-8");
652
+ return parseTasks(content, includeDone);
653
+ }
654
+
655
+ // src/parser/status-update.ts
656
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
657
+ var TASK_HEADER_RE2 = /^## (T\d+): /;
658
+ var STATUS_LINE_RE = /^(- STATUS: )\w+/;
659
+ function replaceTaskStatus(content, taskId, newStatus) {
660
+ const lines = content.split("\n");
661
+ let inTargetTask = false;
662
+ let found = false;
663
+ for (let i = 0; i < lines.length; i++) {
664
+ const headerMatch = lines[i].match(TASK_HEADER_RE2);
665
+ if (headerMatch) {
666
+ inTargetTask = headerMatch[1] === taskId;
667
+ continue;
668
+ }
669
+ if (inTargetTask && STATUS_LINE_RE.test(lines[i])) {
670
+ lines[i] = lines[i].replace(STATUS_LINE_RE, `$1${newStatus}`);
671
+ found = true;
672
+ break;
673
+ }
674
+ }
675
+ return { content: lines.join("\n"), found };
676
+ }
677
+ function updateTaskStatus(filePath, taskId, newStatus) {
678
+ const content = readFileSync5(filePath, "utf-8");
679
+ const result = replaceTaskStatus(content, taskId, newStatus);
680
+ if (!result.found) {
681
+ throw new Error(`Task ${taskId} not found in ${filePath}`);
682
+ }
683
+ writeFileSync2(filePath, result.content, "utf-8");
684
+ }
685
+
686
+ // src/prompts/builder.ts
687
+ function buildTaskPrompt(ctx) {
688
+ const { task, projectConfig, worktreePath, attempt, previousError } = ctx;
689
+ const filesSection = task.files.length > 0 ? task.files.join("\n") : "No specific files listed.";
690
+ let prompt = `You are an autonomous coding agent working on project "${projectConfig.display_name}".
691
+ Working directory: ${worktreePath}
692
+
693
+ ## Task: ${task.taskId} \u2014 ${task.title}
694
+
695
+ ${task.spec}
696
+
697
+ ## Files to focus on (hints, not constraints):
698
+ ${filesSection}
699
+
700
+ ## Verification:
701
+ After completing the task, run: ${task.verify}
702
+ If the verification command fails, fix the issue and re-run until it passes.
703
+ `;
704
+ if (attempt > 1 && previousError) {
705
+ prompt += `
706
+ ## Previous attempt failed:
707
+ ${previousError}
708
+ Analyze what went wrong and try a different approach.
709
+ `;
710
+ }
711
+ prompt += `
712
+ ## Rules:
713
+ - Make only the changes needed for this task. Do not refactor unrelated code.
714
+ - Commit your changes with message: "noxdev(${task.taskId}): ${task.title}"
715
+ - Do not push to any remote.
716
+ - If you cannot complete the task, create a file FAILED.md explaining what went wrong.
717
+ `;
718
+ return prompt;
719
+ }
720
+ function buildCriticPrompt(task, diffContent) {
721
+ return `You are a code review critic. Review this diff for task "${task.taskId}: ${task.title}".
722
+
723
+ ## Task specification:
724
+ ${task.spec}
725
+
726
+ ## Diff to review:
727
+ \`\`\`
728
+ ${diffContent}
729
+ \`\`\`
730
+
731
+ ## Review checklist:
732
+ 1. Does the diff implement what the spec asks for? (correctness)
733
+ 2. Are changes scoped to the task? No unrelated modifications? (scope)
734
+ 3. Are there security issues? (credential exposure, injection, missing validation)
735
+ 4. Does the code follow existing patterns in the project?
736
+
737
+ Respond with APPROVED or REJECTED followed by a brief explanation.
738
+ If REJECTED, explain what needs to change.
739
+ `;
740
+ }
741
+
742
+ // src/docker/runner.ts
743
+ import { execFileSync } from "child_process";
744
+ import { statSync } from "fs";
745
+ import { dirname, join as join5 } from "path";
746
+ import { fileURLToPath } from "url";
747
+ var __filename = fileURLToPath(import.meta.url);
748
+ var __dirname = dirname(__filename);
749
+ function resolveScriptsDir() {
750
+ return join5(__dirname, "scripts");
751
+ }
752
+ function runTaskInDocker(options, auth) {
753
+ const scriptName = auth.mode === "max" ? "docker-run-max.sh" : "docker-run-api.sh";
754
+ const scriptPath = join5(resolveScriptsDir(), scriptName);
755
+ const args2 = [
756
+ options.promptFile,
757
+ options.taskLog,
758
+ String(options.timeoutSeconds),
759
+ options.worktreeDir,
760
+ options.projectGitDir,
761
+ options.gitTargetPath,
762
+ options.memoryLimit,
763
+ String(options.cpuLimit),
764
+ options.dockerImage
765
+ ];
766
+ if (auth.mode === "api") {
767
+ args2.push(auth.apiKey);
768
+ }
769
+ const startTime = Date.now();
770
+ try {
771
+ execFileSync(scriptPath, args2, {
772
+ stdio: "inherit",
773
+ timeout: (options.timeoutSeconds + 60) * 1e3
774
+ });
775
+ const durationSeconds = Math.round((Date.now() - startTime) / 1e3);
776
+ return { exitCode: 0, logFile: options.taskLog, durationSeconds };
777
+ } catch (err) {
778
+ const durationSeconds = Math.round((Date.now() - startTime) / 1e3);
779
+ const exitCode = err.status ?? 1;
780
+ return { exitCode, logFile: options.taskLog, durationSeconds };
781
+ }
782
+ }
783
+ function captureDiff(worktreeDir, outputFile) {
784
+ const scriptPath = join5(resolveScriptsDir(), "docker-capture-diff.sh");
785
+ execFileSync(scriptPath, [worktreeDir, outputFile], {
786
+ stdio: "inherit"
787
+ });
788
+ try {
789
+ const stat = statSync(outputFile);
790
+ return stat.size > 0;
791
+ } catch {
792
+ return false;
793
+ }
794
+ }
795
+
796
+ // src/engine/orchestrator.ts
797
+ function getCurrentSha(cwd) {
798
+ return execSync3("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
799
+ }
800
+ function isoNow() {
801
+ return (/* @__PURE__ */ new Date()).toISOString();
802
+ }
803
+ async function executeRun(ctx) {
804
+ const claudeJsonSrc = join6(homedir5(), ".claude.json");
805
+ const claudeSnapshot = join6(homedir5(), ".noxdev", ".claude-snapshot.json");
806
+ if (existsSync3(claudeJsonSrc)) {
807
+ mkdirSync3(join6(homedir5(), ".noxdev"), { recursive: true });
808
+ copyFileSync(claudeJsonSrc, claudeSnapshot);
809
+ console.log(chalk2.dim(" Credential snapshot saved"));
810
+ }
811
+ const tasksFile = join6(ctx.worktreeDir, ctx.projectConfig.tasks_file);
812
+ const pendingTasks = parseTasksFromFile(tasksFile);
813
+ if (pendingTasks.length === 0) {
814
+ console.log("No pending tasks");
815
+ return;
816
+ }
817
+ const commitBefore = getCurrentSha(ctx.worktreeDir);
818
+ const logDir = join6(homedir5(), ".noxdev", "logs", ctx.runId);
819
+ insertRun(ctx.db, {
820
+ id: ctx.runId,
821
+ projectId: ctx.projectId,
822
+ startedAt: isoNow(),
823
+ authMode: ctx.auth.mode,
824
+ totalTasks: pendingTasks.length,
825
+ commitBefore,
826
+ logFile: logDir
827
+ });
828
+ insertTaskCache(
829
+ ctx.db,
830
+ ctx.runId,
831
+ pendingTasks.map((t) => ({
832
+ taskId: t.taskId,
833
+ title: t.title,
834
+ files: t.files.join(","),
835
+ verify: t.verify,
836
+ critic: t.critic,
837
+ push: t.push,
838
+ spec: t.spec,
839
+ statusBefore: t.status
840
+ }))
841
+ );
842
+ console.log(
843
+ chalk2.bold(
844
+ `noxdev run ${ctx.projectId} \u2014 ${pendingTasks.length} pending tasks`
845
+ )
846
+ );
847
+ let completed = 0;
848
+ let failed = 0;
849
+ let skipped = 0;
850
+ let consecutiveFailures = 0;
851
+ const maxRetries = ctx.projectConfig.docker?.timeout_minutes ? 1 : 1;
852
+ const circuitBreakerThreshold = 3;
853
+ let lastSha = commitBefore;
854
+ for (const task of pendingTasks) {
855
+ if (consecutiveFailures >= circuitBreakerThreshold) {
856
+ console.log(
857
+ chalk2.yellow(
858
+ `\u26A0 Circuit breaker: ${consecutiveFailures} consecutive failures, stopping run`
859
+ )
860
+ );
861
+ skipped += pendingTasks.length - completed - failed;
862
+ break;
863
+ }
864
+ const result = await executeTask(ctx, task, lastSha, logDir, 1, maxRetries, claudeJsonSrc, claudeSnapshot);
865
+ if (result.commitSha) {
866
+ lastSha = result.commitSha;
867
+ }
868
+ const newStatus = result.status === "COMPLETED" ? "done" : "failed";
869
+ try {
870
+ updateTaskStatus(tasksFile, task.taskId, newStatus);
871
+ } catch {
872
+ console.log(
873
+ chalk2.yellow(`\u26A0 Could not update TASKS.md status for ${task.taskId}`)
874
+ );
875
+ }
876
+ if (result.status === "COMPLETED") {
877
+ completed++;
878
+ consecutiveFailures = 0;
879
+ } else {
880
+ failed++;
881
+ consecutiveFailures++;
882
+ }
883
+ }
884
+ const commitAfter = getCurrentSha(ctx.worktreeDir);
885
+ const runStatus = failed === 0 ? "completed" : "partial";
886
+ updateRunFinished(ctx.db, ctx.runId, {
887
+ finishedAt: isoNow(),
888
+ completed,
889
+ failed,
890
+ skipped,
891
+ status: runStatus,
892
+ commitAfter
893
+ });
894
+ const parts = [];
895
+ if (completed > 0) parts.push(chalk2.green(`${completed} completed`));
896
+ if (failed > 0) parts.push(chalk2.red(`${failed} failed`));
897
+ if (skipped > 0) parts.push(chalk2.yellow(`${skipped} skipped`));
898
+ console.log(`
899
+ Run ${ctx.runId} complete: ${parts.join(", ")}`);
900
+ }
901
+ async function executeTask(ctx, task, lastSha, logDir, attempt, maxRetries, claudeJsonSrc, claudeSnapshot, previousError) {
902
+ console.log(chalk2.bold(`
903
+ \u2501\u2501\u2501 ${task.taskId}: ${task.title} \u2501\u2501\u2501`));
904
+ if (attempt > 1) {
905
+ console.log(chalk2.yellow(` Retry attempt ${attempt}`));
906
+ }
907
+ const startedAt = isoNow();
908
+ const startTime = Date.now();
909
+ const prompt = buildTaskPrompt({
910
+ task,
911
+ projectConfig: ctx.projectConfig,
912
+ worktreePath: ctx.worktreeDir,
913
+ runId: ctx.runId,
914
+ attempt,
915
+ previousError
916
+ });
917
+ const promptFile = join6(
918
+ tmpdir(),
919
+ `noxdev-prompt-${ctx.runId}-${task.taskId}.md`
920
+ );
921
+ writeFileSync3(promptFile, prompt, "utf-8");
922
+ const taskLogDir = join6(logDir, task.taskId);
923
+ mkdirSync3(taskLogDir, { recursive: true });
924
+ const taskLog = join6(taskLogDir, `attempt-${attempt}.log`);
925
+ if (existsSync3(claudeSnapshot)) {
926
+ copyFileSync(claudeSnapshot, claudeJsonSrc);
927
+ }
928
+ const timeoutSeconds = ctx.projectConfig.docker.timeout_minutes * 60;
929
+ const dockerResult = runTaskInDocker(
930
+ {
931
+ promptFile,
932
+ taskLog,
933
+ timeoutSeconds,
934
+ worktreeDir: ctx.worktreeDir,
935
+ projectGitDir: ctx.projectGitDir,
936
+ gitTargetPath: ctx.gitTargetPath,
937
+ memoryLimit: ctx.projectConfig.docker.memory,
938
+ cpuLimit: ctx.projectConfig.docker.cpus,
939
+ dockerImage: "noxdev-runner:latest"
940
+ },
941
+ ctx.auth
942
+ );
943
+ const endTime = Date.now();
944
+ const durationSeconds = Math.round((endTime - startTime) / 1e3);
945
+ let commitSha = null;
946
+ try {
947
+ const currentSha = getCurrentSha(ctx.worktreeDir);
948
+ if (currentSha !== lastSha) {
949
+ commitSha = currentSha;
950
+ }
951
+ } catch {
952
+ }
953
+ let status = dockerResult.exitCode === 0 ? "COMPLETED" : "FAILED";
954
+ if (status === "FAILED" && attempt < maxRetries) {
955
+ let errorContext = `Exit code: ${dockerResult.exitCode}`;
956
+ try {
957
+ const logContent = readFileSync6(taskLog, "utf-8");
958
+ const lines = logContent.split("\n");
959
+ errorContext = lines.slice(-50).join("\n");
960
+ } catch {
961
+ }
962
+ try {
963
+ unlinkSync(promptFile);
964
+ } catch {
965
+ }
966
+ return executeTask(
967
+ ctx,
968
+ task,
969
+ commitSha ?? lastSha,
970
+ logDir,
971
+ attempt + 1,
972
+ maxRetries,
973
+ claudeJsonSrc,
974
+ claudeSnapshot,
975
+ errorContext
976
+ );
977
+ }
978
+ let criticLogFile = null;
979
+ let diffFile = null;
980
+ if (task.critic === "review" && status === "COMPLETED") {
981
+ const criticResult = await runCritic(ctx, task, logDir, attempt, claudeJsonSrc, claudeSnapshot);
982
+ criticLogFile = criticResult.criticLogFile;
983
+ diffFile = criticResult.diffFile;
984
+ if (criticResult.rejected) {
985
+ status = "FAILED";
986
+ if (attempt < maxRetries) {
987
+ try {
988
+ unlinkSync(promptFile);
989
+ } catch {
990
+ }
991
+ return executeTask(
992
+ ctx,
993
+ task,
994
+ commitSha ?? lastSha,
995
+ logDir,
996
+ attempt + 1,
997
+ maxRetries,
998
+ claudeJsonSrc,
999
+ claudeSnapshot,
1000
+ `Critic rejected the changes: ${criticResult.reason}`
1001
+ );
1002
+ }
1003
+ }
1004
+ }
1005
+ const finishedAt = isoNow();
1006
+ insertTaskResult(ctx.db, {
1007
+ runId: ctx.runId,
1008
+ taskId: task.taskId,
1009
+ title: task.title,
1010
+ status,
1011
+ exitCode: dockerResult.exitCode,
1012
+ authMode: ctx.auth.mode,
1013
+ criticMode: task.critic,
1014
+ pushMode: task.push,
1015
+ attempt,
1016
+ commitSha,
1017
+ startedAt,
1018
+ finishedAt,
1019
+ durationSeconds,
1020
+ devLogFile: taskLog,
1021
+ criticLogFile,
1022
+ diffFile
1023
+ });
1024
+ if (status === "COMPLETED") {
1025
+ console.log(
1026
+ chalk2.green(` \u2713 ${task.taskId} completed in ${durationSeconds}s`)
1027
+ );
1028
+ } else {
1029
+ console.log(
1030
+ chalk2.red(
1031
+ ` \u2717 ${task.taskId} failed (exit ${dockerResult.exitCode}) in ${durationSeconds}s`
1032
+ )
1033
+ );
1034
+ }
1035
+ try {
1036
+ unlinkSync(promptFile);
1037
+ } catch {
1038
+ }
1039
+ return {
1040
+ taskId: task.taskId,
1041
+ title: task.title,
1042
+ status,
1043
+ exitCode: dockerResult.exitCode,
1044
+ commitSha,
1045
+ durationSeconds,
1046
+ attempt
1047
+ };
1048
+ }
1049
+ async function runCritic(ctx, task, logDir, attempt, claudeJsonSrc, claudeSnapshot) {
1050
+ console.log(chalk2.dim(` Running critic review for ${task.taskId}\u2026`));
1051
+ const taskLogDir = join6(logDir, task.taskId);
1052
+ const diffOutputFile = join6(taskLogDir, `diff-attempt-${attempt}.patch`);
1053
+ const hasDiff = captureDiff(ctx.worktreeDir, diffOutputFile);
1054
+ if (!hasDiff) {
1055
+ console.log(chalk2.yellow(" \u26A0 No diff to review, skipping critic"));
1056
+ return { rejected: false, reason: "", criticLogFile: null, diffFile: null };
1057
+ }
1058
+ const diffContent = readFileSync6(diffOutputFile, "utf-8");
1059
+ const criticPromptContent = buildCriticPrompt(task, diffContent);
1060
+ const criticPromptFile = join6(
1061
+ tmpdir(),
1062
+ `noxdev-critic-${ctx.runId}-${task.taskId}.md`
1063
+ );
1064
+ writeFileSync3(criticPromptFile, criticPromptContent, "utf-8");
1065
+ if (existsSync3(claudeSnapshot)) {
1066
+ copyFileSync(claudeSnapshot, claudeJsonSrc);
1067
+ }
1068
+ const criticLog = join6(taskLogDir, `critic-attempt-${attempt}.log`);
1069
+ const criticResult = runTaskInDocker(
1070
+ {
1071
+ promptFile: criticPromptFile,
1072
+ taskLog: criticLog,
1073
+ timeoutSeconds: 120,
1074
+ worktreeDir: ctx.worktreeDir,
1075
+ projectGitDir: ctx.projectGitDir,
1076
+ gitTargetPath: ctx.gitTargetPath,
1077
+ memoryLimit: ctx.projectConfig.docker.memory,
1078
+ cpuLimit: ctx.projectConfig.docker.cpus,
1079
+ dockerImage: "noxdev-runner:latest"
1080
+ },
1081
+ ctx.auth
1082
+ );
1083
+ try {
1084
+ unlinkSync(criticPromptFile);
1085
+ } catch {
1086
+ }
1087
+ let rejected = false;
1088
+ let reason = "";
1089
+ try {
1090
+ const criticOutput = readFileSync6(criticLog, "utf-8");
1091
+ if (/\bREJECTED\b/i.test(criticOutput)) {
1092
+ rejected = true;
1093
+ const match = criticOutput.match(/REJECTED[:\s]*(.*)/i);
1094
+ reason = match?.[1]?.trim() ?? "Critic rejected without explanation";
1095
+ console.log(chalk2.red(` \u2717 Critic REJECTED: ${reason}`));
1096
+ } else {
1097
+ console.log(chalk2.green(" \u2713 Critic APPROVED"));
1098
+ }
1099
+ } catch {
1100
+ console.log(chalk2.yellow(" \u26A0 Could not read critic output, assuming approved"));
1101
+ }
1102
+ return {
1103
+ rejected,
1104
+ reason,
1105
+ criticLogFile: criticLog,
1106
+ diffFile: diffOutputFile
1107
+ };
1108
+ }
1109
+
1110
+ // src/commands/run.ts
1111
+ function generateRunId() {
1112
+ const now = /* @__PURE__ */ new Date();
1113
+ const pad3 = (n) => String(n).padStart(2, "0");
1114
+ return [
1115
+ now.getFullYear(),
1116
+ pad3(now.getMonth() + 1),
1117
+ pad3(now.getDate()),
1118
+ "_",
1119
+ pad3(now.getHours()),
1120
+ pad3(now.getMinutes()),
1121
+ pad3(now.getSeconds())
1122
+ ].join("");
1123
+ }
1124
+ function generateRunIdForProject(projectId) {
1125
+ const now = /* @__PURE__ */ new Date();
1126
+ const pad3 = (n) => String(n).padStart(2, "0");
1127
+ return [
1128
+ now.getFullYear(),
1129
+ pad3(now.getMonth() + 1),
1130
+ pad3(now.getDate()),
1131
+ "_",
1132
+ pad3(now.getHours()),
1133
+ pad3(now.getMinutes()),
1134
+ pad3(now.getSeconds()),
1135
+ "_",
1136
+ projectId
1137
+ ].join("");
1138
+ }
1139
+ async function runProject(project) {
1140
+ const db = getDb();
1141
+ const globalConfig = loadGlobalConfig();
1142
+ const projectConfig = loadProjectConfig(project.repo_path);
1143
+ const auth = resolveAuth({
1144
+ max: { preferred: globalConfig.accounts.max.preferred },
1145
+ api: {
1146
+ fallback: globalConfig.accounts.api.fallback,
1147
+ dailyCapUsd: globalConfig.accounts.api.daily_cap_usd,
1148
+ model: globalConfig.accounts.api.model
1149
+ },
1150
+ secrets: {
1151
+ provider: globalConfig.secrets.provider,
1152
+ globalSecretsFile: globalConfig.secrets.global,
1153
+ ageKeyFile: globalConfig.secrets.age_key
1154
+ }
1155
+ });
1156
+ const runId = generateRunId();
1157
+ const gitDir = join7(project.repo_path, ".git");
1158
+ try {
1159
+ const baseBranch = execSync4("git symbolic-ref --short HEAD", {
1160
+ cwd: project.repo_path,
1161
+ encoding: "utf-8"
1162
+ }).trim();
1163
+ execSync4(`git merge ${baseBranch} --no-edit`, {
1164
+ cwd: project.worktree_path,
1165
+ stdio: "pipe"
1166
+ });
1167
+ console.log(chalk3.gray(` \u2713 Worktree synced with ${baseBranch}`));
1168
+ } catch (err) {
1169
+ const msg = err instanceof Error ? err.message : String(err);
1170
+ if (msg.includes("CONFLICT")) {
1171
+ console.error(chalk3.red("\u2716 Merge conflict syncing worktree with base branch."));
1172
+ console.error(chalk3.gray(" Resolve manually: cd " + project.worktree_path));
1173
+ console.error(chalk3.gray(" Then re-run: noxdev run " + project.id));
1174
+ process.exit(1);
1175
+ }
1176
+ console.log(chalk3.gray(" \u2713 Worktree up to date"));
1177
+ }
1178
+ const ctx = {
1179
+ projectId: project.id,
1180
+ projectConfig,
1181
+ worktreeDir: project.worktree_path,
1182
+ projectGitDir: gitDir,
1183
+ gitTargetPath: gitDir,
1184
+ runId,
1185
+ db,
1186
+ auth
1187
+ };
1188
+ await executeRun(ctx);
1189
+ try {
1190
+ const worktreePath = project.worktree_path;
1191
+ execSync4("git add TASKS.md", { cwd: worktreePath, stdio: "pipe" });
1192
+ execSync4('git commit -m "noxdev: update task statuses"', { cwd: worktreePath, stdio: "pipe" });
1193
+ console.log(chalk3.gray(" \u2713 TASKS.md status updates committed"));
1194
+ } catch {
1195
+ }
1196
+ }
1197
+ async function runAllProjects(db) {
1198
+ const projects = getAllProjects(db);
1199
+ if (projects.length === 0) {
1200
+ console.log("No registered projects. Run `noxdev init` first.");
1201
+ return;
1202
+ }
1203
+ console.log(
1204
+ chalk3.bold(
1205
+ `noxdev run --all: ${projects.length} registered projects`
1206
+ )
1207
+ );
1208
+ const globalConfig = loadGlobalConfig();
1209
+ const auth = resolveAuth({
1210
+ max: { preferred: globalConfig.accounts.max.preferred },
1211
+ api: {
1212
+ fallback: globalConfig.accounts.api.fallback,
1213
+ dailyCapUsd: globalConfig.accounts.api.daily_cap_usd,
1214
+ model: globalConfig.accounts.api.model
1215
+ },
1216
+ secrets: {
1217
+ provider: globalConfig.secrets.provider,
1218
+ globalSecretsFile: globalConfig.secrets.global,
1219
+ ageKeyFile: globalConfig.secrets.age_key
1220
+ }
1221
+ });
1222
+ const results = [];
1223
+ for (let i = 0; i < projects.length; i++) {
1224
+ const proj = projects[i];
1225
+ const projectConfig = loadProjectConfig(proj.repo_path);
1226
+ const tasksFile = join7(proj.worktree_path, projectConfig.tasks_file);
1227
+ let pendingCount = 0;
1228
+ try {
1229
+ const pending = parseTasksFromFile(tasksFile);
1230
+ pendingCount = pending.length;
1231
+ } catch {
1232
+ pendingCount = 0;
1233
+ }
1234
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1235
+ console.log(
1236
+ `[${time}] Project ${i + 1}/${projects.length}: ${proj.display_name} (${pendingCount} pending tasks)`
1237
+ );
1238
+ const runId = generateRunIdForProject(proj.id);
1239
+ const gitDir = join7(proj.repo_path, ".git");
1240
+ const ctx = {
1241
+ projectId: proj.id,
1242
+ projectConfig,
1243
+ worktreeDir: proj.worktree_path,
1244
+ projectGitDir: gitDir,
1245
+ gitTargetPath: gitDir,
1246
+ runId,
1247
+ db,
1248
+ auth
1249
+ };
1250
+ let completed = 0;
1251
+ let total = pendingCount;
1252
+ try {
1253
+ await executeRun(ctx);
1254
+ const run = db.prepare("SELECT * FROM runs WHERE id = ?").get(runId);
1255
+ if (run) {
1256
+ completed = run.completed ?? 0;
1257
+ total = run.total_tasks;
1258
+ if (run.failed === run.total_tasks && run.total_tasks > 0) {
1259
+ console.log(
1260
+ chalk3.yellow(
1261
+ `\u26A0 Project ${proj.display_name}: all tasks failed (circuit-break), continuing to next project`
1262
+ )
1263
+ );
1264
+ }
1265
+ }
1266
+ } catch (err) {
1267
+ console.log(
1268
+ chalk3.yellow(
1269
+ `\u26A0 Project ${proj.display_name} failed: ${err instanceof Error ? err.message : String(err)}`
1270
+ )
1271
+ );
1272
+ }
1273
+ try {
1274
+ const worktreePath = proj.worktree_path;
1275
+ execSync4("git add TASKS.md", { cwd: worktreePath, stdio: "pipe" });
1276
+ execSync4('git commit -m "noxdev: update task statuses"', { cwd: worktreePath, stdio: "pipe" });
1277
+ console.log(chalk3.gray(" \u2713 TASKS.md status updates committed"));
1278
+ } catch {
1279
+ }
1280
+ results.push({
1281
+ displayName: proj.display_name,
1282
+ completed,
1283
+ total
1284
+ });
1285
+ }
1286
+ console.log("\n---");
1287
+ console.log("MULTI-PROJECT RUN COMPLETE");
1288
+ for (const r of results) {
1289
+ console.log(` ${r.displayName}: ${r.completed}/${r.total} completed`);
1290
+ }
1291
+ console.log("---");
1292
+ }
1293
+ function whichCommand(cmd) {
1294
+ try {
1295
+ execSync4(`which ${cmd}`, { stdio: "ignore" });
1296
+ return true;
1297
+ } catch {
1298
+ return false;
1299
+ }
1300
+ }
1301
+ async function startOvernightRun(project, all) {
1302
+ const childArgs = ["run"];
1303
+ if (project) childArgs.push(project);
1304
+ if (all) childArgs.push("--all");
1305
+ const entryPoint = join7(import.meta.dirname, "..", "index.js");
1306
+ let spawnCmd;
1307
+ let spawnArgs;
1308
+ if (whichCommand("systemd-inhibit")) {
1309
+ spawnCmd = "systemd-inhibit";
1310
+ spawnArgs = [
1311
+ "--what=sleep",
1312
+ "--who=noxdev",
1313
+ "--why=Overnight coding run",
1314
+ "node",
1315
+ entryPoint,
1316
+ ...childArgs
1317
+ ];
1318
+ } else if (whichCommand("caffeinate")) {
1319
+ spawnCmd = "caffeinate";
1320
+ spawnArgs = ["-s", "node", entryPoint, ...childArgs];
1321
+ } else {
1322
+ console.log(
1323
+ chalk3.yellow(
1324
+ "Could not inhibit sleep. Machine may sleep during overnight run."
1325
+ )
1326
+ );
1327
+ spawnCmd = "node";
1328
+ spawnArgs = [entryPoint, ...childArgs];
1329
+ }
1330
+ const child = spawn(spawnCmd, spawnArgs, {
1331
+ detached: true,
1332
+ stdio: "ignore"
1333
+ });
1334
+ const pid = child.pid;
1335
+ if (!pid) {
1336
+ console.error(chalk3.red("Failed to start overnight process."));
1337
+ process.exitCode = 1;
1338
+ return;
1339
+ }
1340
+ child.unref();
1341
+ const noxdevDir = join7(homedir6(), ".noxdev");
1342
+ mkdirSync4(noxdevDir, { recursive: true });
1343
+ writeFileSync4(join7(noxdevDir, "noxdev.pid"), String(pid), "utf-8");
1344
+ console.log(
1345
+ `noxdev overnight run started (PID: ${pid}). Check status with: noxdev status`
1346
+ );
1347
+ }
1348
+ function registerRun(program2) {
1349
+ program2.command("run").description("Run coding tasks").argument("[project]", "project name").option("--overnight", "run in overnight mode").option("--all", "run for all projects").action(
1350
+ async (project, opts) => {
1351
+ try {
1352
+ if (opts.overnight) {
1353
+ await startOvernightRun(project, opts.all);
1354
+ return;
1355
+ }
1356
+ const db = getDb();
1357
+ if (opts.all) {
1358
+ await runAllProjects(db);
1359
+ return;
1360
+ }
1361
+ let projectRow;
1362
+ if (project) {
1363
+ projectRow = getProject(db, project);
1364
+ if (!projectRow) {
1365
+ console.error(
1366
+ chalk3.red(
1367
+ `Project "${project}" not found. Run \`noxdev init ${project}\` first.`
1368
+ )
1369
+ );
1370
+ process.exitCode = 1;
1371
+ return;
1372
+ }
1373
+ } else {
1374
+ const projects = getAllProjects(db);
1375
+ if (projects.length === 0) {
1376
+ console.error(
1377
+ chalk3.red(
1378
+ "No projects registered. Run `noxdev init` first."
1379
+ )
1380
+ );
1381
+ process.exitCode = 1;
1382
+ return;
1383
+ }
1384
+ if (projects.length > 1) {
1385
+ console.error(
1386
+ chalk3.red(
1387
+ `Multiple projects registered. Specify one: ${projects.map((p) => p.id).join(", ")}`
1388
+ )
1389
+ );
1390
+ process.exitCode = 1;
1391
+ return;
1392
+ }
1393
+ projectRow = projects[0];
1394
+ }
1395
+ await runProject(projectRow);
1396
+ } catch (err) {
1397
+ console.error(
1398
+ chalk3.red(
1399
+ `Error: ${err instanceof Error ? err.message : String(err)}`
1400
+ )
1401
+ );
1402
+ process.exitCode = 1;
1403
+ }
1404
+ }
1405
+ );
1406
+ }
1407
+
1408
+ // src/commands/status.ts
1409
+ import chalk5 from "chalk";
1410
+
1411
+ // src/engine/summary.ts
1412
+ import chalk4 from "chalk";
1413
+ function getAllProjectSummaries(db) {
1414
+ const projects = getAllProjects(db);
1415
+ const summaries = [];
1416
+ for (const p of projects) {
1417
+ const run = getLatestRun(db, p.id);
1418
+ if (!run) {
1419
+ summaries.push({
1420
+ projectId: p.id,
1421
+ displayName: p.display_name,
1422
+ runId: null,
1423
+ status: null,
1424
+ total: 0,
1425
+ completed: 0,
1426
+ failed: 0,
1427
+ skipped: 0,
1428
+ pendingMerge: 0,
1429
+ startedAt: null,
1430
+ finishedAt: null
1431
+ });
1432
+ continue;
1433
+ }
1434
+ const pending = getPendingMerge(db, run.id);
1435
+ summaries.push({
1436
+ projectId: p.id,
1437
+ displayName: p.display_name,
1438
+ runId: run.id,
1439
+ status: run.status,
1440
+ total: run.total_tasks ?? 0,
1441
+ completed: run.completed ?? 0,
1442
+ failed: run.failed ?? 0,
1443
+ skipped: run.skipped ?? 0,
1444
+ pendingMerge: pending.length,
1445
+ startedAt: run.started_at,
1446
+ finishedAt: run.finished_at
1447
+ });
1448
+ }
1449
+ return summaries;
1450
+ }
1451
+ function relativeTime(isoDate) {
1452
+ const now = Date.now();
1453
+ const dateStr = isoDate.endsWith("Z") ? isoDate : isoDate + "Z";
1454
+ const then = new Date(dateStr).getTime();
1455
+ const diffMs = now - then;
1456
+ const diffMin = Math.floor(diffMs / 6e4);
1457
+ const diffHr = Math.floor(diffMs / 36e5);
1458
+ const diffDay = Math.floor(diffMs / 864e5);
1459
+ if (diffMin < 60) return `${Math.max(diffMin, 1)}m ago`;
1460
+ if (diffHr < 24) return `${diffHr}h ago`;
1461
+ if (diffDay < 7) return `${diffDay}d ago`;
1462
+ return isoDate.slice(0, 10);
1463
+ }
1464
+ function pad(str, len) {
1465
+ return str.length >= len ? str : str + " ".repeat(len - str.length);
1466
+ }
1467
+ function formatSummaryTable(summaries) {
1468
+ const header = `${pad("PROJECT", 21)}${pad("LAST RUN", 13)}${pad("STATUS", 14)}${pad("TASKS", 14)}MERGE`;
1469
+ const lines = [chalk4.bold(header)];
1470
+ for (const s of summaries) {
1471
+ if (!s.runId || !s.startedAt) {
1472
+ const line = `${pad(s.projectId, 21)}${pad("never", 13)}${pad("\u2014", 14)}${pad("\u2014", 14)}\u2014`;
1473
+ lines.push(chalk4.dim(line));
1474
+ continue;
1475
+ }
1476
+ const timeStr = relativeTime(s.startedAt);
1477
+ const statusStr = s.status ?? "\u2014";
1478
+ let tasksStr;
1479
+ if (s.failed > 0) {
1480
+ tasksStr = `${s.completed}/${s.total} (${s.failed} fail)`;
1481
+ } else if (s.completed === s.total && s.total > 0) {
1482
+ tasksStr = `${s.completed}/${s.total} \u2713`;
1483
+ } else {
1484
+ tasksStr = `${s.completed}/${s.total}`;
1485
+ }
1486
+ const mergeStr = s.pendingMerge > 0 ? `${s.pendingMerge} pending` : "\u2014";
1487
+ const raw = `${pad(s.projectId, 21)}${pad(timeStr, 13)}${pad(statusStr, 14)}${pad(tasksStr, 14)}${mergeStr}`;
1488
+ if (s.failed > 0) {
1489
+ lines.push(chalk4.yellow(raw));
1490
+ } else if (s.completed === s.total && s.total > 0) {
1491
+ lines.push(chalk4.green(raw));
1492
+ } else {
1493
+ lines.push(raw);
1494
+ }
1495
+ }
1496
+ return lines.join("\n");
1497
+ }
1498
+
1499
+ // src/commands/status.ts
1500
+ function relativeTime2(isoDate) {
1501
+ const now = Date.now();
1502
+ const dateStr = isoDate.endsWith("Z") ? isoDate : isoDate + "Z";
1503
+ const then = new Date(dateStr).getTime();
1504
+ const diffMs = now - then;
1505
+ const diffMin = Math.floor(diffMs / 6e4);
1506
+ const diffHr = Math.floor(diffMs / 36e5);
1507
+ const diffDay = Math.floor(diffMs / 864e5);
1508
+ if (diffMin < 60) return `${Math.max(diffMin, 1)}m ago`;
1509
+ if (diffHr < 24) return `${diffHr}h ago`;
1510
+ if (diffDay < 7) return `${diffDay}d ago`;
1511
+ return isoDate.slice(0, 10);
1512
+ }
1513
+ function statusBadge(status) {
1514
+ switch (status) {
1515
+ case "completed":
1516
+ return chalk5.green("COMPLETED");
1517
+ case "failed":
1518
+ return chalk5.red("FAILED");
1519
+ case "skipped":
1520
+ return chalk5.yellow("SKIPPED");
1521
+ case "completed_retry":
1522
+ return chalk5.green("COMPLETED") + chalk5.green(" (retry)");
1523
+ default:
1524
+ return status.toUpperCase();
1525
+ }
1526
+ }
1527
+ function showProjectStatus(db, projectId) {
1528
+ const project = getProject(db, projectId);
1529
+ if (!project) {
1530
+ console.log(chalk5.red(`Project not found: ${projectId}`));
1531
+ return;
1532
+ }
1533
+ const run = getLatestRun(db, projectId);
1534
+ if (!run) {
1535
+ console.log(`${projectId}: No runs yet. Run: noxdev run ${projectId}`);
1536
+ return;
1537
+ }
1538
+ const tasks = getTaskResults(db, run.id);
1539
+ const timeStr = relativeTime2(run.started_at);
1540
+ console.log(`noxdev status: ${chalk5.bold(project.display_name)}`);
1541
+ if (run.status === "running") {
1542
+ console.log(`Run ${run.id} \xB7 ${timeStr} \xB7 ${chalk5.cyan("running")}`);
1543
+ console.log(chalk5.cyan(`Run in progress since ${timeStr}...`));
1544
+ } else {
1545
+ console.log(`Run ${run.id} \xB7 ${timeStr} \xB7 ${run.status}`);
1546
+ }
1547
+ const completed = run.completed ?? 0;
1548
+ const failed = run.failed ?? 0;
1549
+ const skipped = run.skipped ?? 0;
1550
+ const total = run.total_tasks ?? 0;
1551
+ console.log("");
1552
+ console.log(
1553
+ `Tasks: ${chalk5.green(String(completed))} completed, ${chalk5.red(String(failed))} failed, ${chalk5.yellow(String(skipped))} skipped (of ${total})`
1554
+ );
1555
+ if (tasks.length > 0) {
1556
+ console.log("");
1557
+ console.log("Commits:");
1558
+ for (const t of tasks) {
1559
+ const badge = statusBadge(t.status);
1560
+ const sha = t.commit_sha || "no commit";
1561
+ const dur = t.duration_seconds != null ? `${t.duration_seconds}s` : "";
1562
+ console.log(` ${t.task_id}: ${t.title} ${badge} ${sha} ${dur}`);
1563
+ }
1564
+ }
1565
+ const pending = getPendingMerge(db, run.id);
1566
+ if (pending.length > 0) {
1567
+ console.log("");
1568
+ console.log(`Pending merge: ${pending.length} tasks awaiting review`);
1569
+ console.log(`Next step: noxdev merge ${projectId}`);
1570
+ }
1571
+ }
1572
+ function isRecentRun(startedAt) {
1573
+ const dateStr = startedAt.endsWith("Z") ? startedAt : startedAt + "Z";
1574
+ const then = new Date(dateStr).getTime();
1575
+ const diffMs = Date.now() - then;
1576
+ return diffMs < 24 * 36e5;
1577
+ }
1578
+ function registerStatus(program2) {
1579
+ program2.command("status").description("Show project status").argument("[project]", "project name").option("--summary", "Show only the summary table, no per-project detail").action((project, opts) => {
1580
+ try {
1581
+ const db = getDb();
1582
+ if (project) {
1583
+ showProjectStatus(db, project);
1584
+ return;
1585
+ }
1586
+ const projects = getAllProjects(db);
1587
+ if (projects.length === 0) {
1588
+ console.log("No projects registered. Run: noxdev init <project> --repo <path>");
1589
+ return;
1590
+ }
1591
+ if (projects.length === 1 && !opts.summary) {
1592
+ showProjectStatus(db, projects[0].id);
1593
+ return;
1594
+ }
1595
+ const summaries = getAllProjectSummaries(db);
1596
+ console.log(formatSummaryTable(summaries));
1597
+ if (opts.summary) {
1598
+ return;
1599
+ }
1600
+ const recentProjects = summaries.filter(
1601
+ (s) => s.startedAt && isRecentRun(s.startedAt)
1602
+ );
1603
+ if (recentProjects.length > 0) {
1604
+ console.log("");
1605
+ for (const s of recentProjects) {
1606
+ console.log("---");
1607
+ showProjectStatus(db, s.projectId);
1608
+ }
1609
+ }
1610
+ } catch (err) {
1611
+ console.error(
1612
+ chalk5.red(`Error: ${err instanceof Error ? err.message : String(err)}`)
1613
+ );
1614
+ process.exitCode = 1;
1615
+ }
1616
+ });
1617
+ }
1618
+
1619
+ // src/commands/log.ts
1620
+ import chalk6 from "chalk";
1621
+ function statusBadge2(status) {
1622
+ switch (status) {
1623
+ case "completed":
1624
+ return chalk6.green("COMPLETED");
1625
+ case "failed":
1626
+ return chalk6.red("FAILED");
1627
+ case "skipped":
1628
+ return chalk6.yellow("SKIPPED");
1629
+ case "completed_retry":
1630
+ return chalk6.green("COMPLETED") + chalk6.green(" (retry)");
1631
+ default:
1632
+ return status.toUpperCase();
1633
+ }
1634
+ }
1635
+ function showTaskLog(db, taskId) {
1636
+ const results = db.prepare(`SELECT * FROM task_results WHERE task_id = ? ORDER BY id DESC`).all(taskId);
1637
+ if (results.length === 0) {
1638
+ console.log(chalk6.red(`No results found for task: ${taskId}`));
1639
+ return;
1640
+ }
1641
+ const latest = results[0];
1642
+ const taskCache = db.prepare(
1643
+ `SELECT spec, files, verify, critic, push FROM tasks WHERE task_id = ? ORDER BY id DESC LIMIT 1`
1644
+ ).get(taskId);
1645
+ console.log(`noxdev log: ${chalk6.bold(taskId)} \u2014 ${latest.title}`);
1646
+ console.log("");
1647
+ console.log(
1648
+ `Latest run: ${latest.run_id} \xB7 ${statusBadge2(latest.status)} \xB7 attempt ${latest.attempt}`
1649
+ );
1650
+ console.log("");
1651
+ if (taskCache?.spec) {
1652
+ console.log("Spec:");
1653
+ for (const line of taskCache.spec.split("\n")) {
1654
+ console.log(` ${line}`);
1655
+ }
1656
+ console.log("");
1657
+ }
1658
+ console.log(`Files: ${taskCache?.files || "none specified"}`);
1659
+ console.log(`Verify: ${taskCache?.verify || "none"}`);
1660
+ console.log(
1661
+ `Critic: ${taskCache?.critic || latest.critic_mode || "review"} Push: ${taskCache?.push || latest.push_mode || "auto"}`
1662
+ );
1663
+ console.log("");
1664
+ console.log("Execution:");
1665
+ console.log(` Started: ${latest.started_at || "unknown"}`);
1666
+ console.log(` Finished: ${latest.finished_at || "unknown"}`);
1667
+ console.log(
1668
+ ` Duration: ${latest.duration_seconds != null ? `${latest.duration_seconds}s` : "unknown"}`
1669
+ );
1670
+ console.log(` Exit code: ${latest.exit_code ?? "none"}`);
1671
+ console.log(` Auth mode: ${latest.auth_mode || "unknown"}`);
1672
+ console.log(` Commit: ${latest.commit_sha || "none"}`);
1673
+ console.log("");
1674
+ console.log(`Merge: ${latest.merge_decision}`);
1675
+ console.log("");
1676
+ console.log("Logs:");
1677
+ console.log(` Dev agent: ${latest.dev_log_file || "not available"}`);
1678
+ console.log(` Critic: ${latest.critic_log_file || "not available"}`);
1679
+ console.log(` Diff: ${latest.diff_file || "not available"}`);
1680
+ if (latest.dev_log_file) {
1681
+ console.log("");
1682
+ console.log(`View dev agent log? Run: cat ${latest.dev_log_file}`);
1683
+ }
1684
+ if (results.length > 1) {
1685
+ console.log("");
1686
+ console.log("History:");
1687
+ for (const r of results) {
1688
+ const dur = r.duration_seconds != null ? `${r.duration_seconds}s` : "unknown";
1689
+ console.log(
1690
+ ` Run ${r.run_id}: ${statusBadge2(r.status)} \xB7 ${dur} \xB7 attempt ${r.attempt}`
1691
+ );
1692
+ }
1693
+ }
1694
+ }
1695
+ function registerLog(program2) {
1696
+ program2.command("log").description("Show task log").argument("<task-id>", "task identifier").action((taskId) => {
1697
+ try {
1698
+ const db = getDb();
1699
+ showTaskLog(db, taskId);
1700
+ } catch (err) {
1701
+ console.error(
1702
+ chalk6.red(
1703
+ `Error: ${err instanceof Error ? err.message : String(err)}`
1704
+ )
1705
+ );
1706
+ process.exitCode = 1;
1707
+ }
1708
+ });
1709
+ }
1710
+
1711
+ // src/commands/merge.ts
1712
+ import chalk7 from "chalk";
1713
+ import { createInterface } from "readline";
1714
+
1715
+ // src/merge/interactive.ts
1716
+ import { execSync as execSync5 } from "child_process";
1717
+ function getMergeCandidates(db, projectId) {
1718
+ const run = getLatestRun(db, projectId);
1719
+ if (!run) return [];
1720
+ const rows = db.prepare(
1721
+ `SELECT id, task_id, title, status, commit_sha, diff_file
1722
+ FROM task_results
1723
+ WHERE run_id = ?
1724
+ AND status IN ('completed', 'completed_retry')
1725
+ AND merge_decision = 'pending'
1726
+ AND commit_sha IS NOT NULL`
1727
+ ).all(run.id);
1728
+ return rows.map((r) => ({
1729
+ taskResultId: r.id,
1730
+ taskId: r.task_id,
1731
+ title: r.title,
1732
+ status: r.status,
1733
+ commitSha: r.commit_sha,
1734
+ diffFile: r.diff_file
1735
+ }));
1736
+ }
1737
+ function getDiffStats(worktreeDir, commitSha) {
1738
+ return execSync5(`git show --stat --format="" ${commitSha}`, {
1739
+ cwd: worktreeDir,
1740
+ encoding: "utf-8"
1741
+ }).trim();
1742
+ }
1743
+ function getFullDiff(worktreeDir, commitSha) {
1744
+ return execSync5(`git show ${commitSha}`, {
1745
+ cwd: worktreeDir,
1746
+ encoding: "utf-8"
1747
+ });
1748
+ }
1749
+ function applyMergeDecisions(db, worktreeDir, projectGitDir, decisions) {
1750
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1751
+ let merged = 0;
1752
+ let rejected = 0;
1753
+ let skipped = 0;
1754
+ const candidates = getMergeCandidatesForDecisions(db, decisions);
1755
+ for (const d of decisions) {
1756
+ if (d.decision === "rejected") {
1757
+ const candidate = candidates.get(d.taskResultId);
1758
+ if (candidate) {
1759
+ execSync5(`git revert --no-commit ${candidate.commitSha}`, {
1760
+ cwd: worktreeDir
1761
+ });
1762
+ execSync5(
1763
+ `git commit -m "noxdev: revert ${d.taskId} (rejected in merge review)"`,
1764
+ { cwd: worktreeDir }
1765
+ );
1766
+ }
1767
+ updateMergeDecision(db, d.taskResultId, "rejected", now);
1768
+ rejected++;
1769
+ } else if (d.decision === "approved") {
1770
+ updateMergeDecision(db, d.taskResultId, "approved", now);
1771
+ merged++;
1772
+ } else {
1773
+ skipped++;
1774
+ }
1775
+ }
1776
+ if (merged > 0) {
1777
+ const run = getRunForDecisions(db, decisions, candidates);
1778
+ const branch = getBranchFromWorktree(worktreeDir);
1779
+ const runId = run?.id ?? "unknown";
1780
+ execSync5(
1781
+ `git merge ${branch} -m "noxdev: merge ${merged} approved tasks from run ${runId}"`,
1782
+ { cwd: projectGitDir }
1783
+ );
1784
+ }
1785
+ return { merged, rejected, skipped };
1786
+ }
1787
+ function getMergeCandidatesForDecisions(db, decisions) {
1788
+ const result = /* @__PURE__ */ new Map();
1789
+ for (const d of decisions) {
1790
+ const row = db.prepare(`SELECT commit_sha, run_id FROM task_results WHERE id = ?`).get(d.taskResultId);
1791
+ if (row) {
1792
+ result.set(d.taskResultId, {
1793
+ commitSha: row.commit_sha,
1794
+ runId: row.run_id
1795
+ });
1796
+ }
1797
+ }
1798
+ return result;
1799
+ }
1800
+ function getRunForDecisions(db, decisions, candidates) {
1801
+ for (const d of decisions) {
1802
+ const c = candidates.get(d.taskResultId);
1803
+ if (c) {
1804
+ return db.prepare(`SELECT id FROM runs WHERE id = ?`).get(c.runId);
1805
+ }
1806
+ }
1807
+ return null;
1808
+ }
1809
+ function getBranchFromWorktree(worktreeDir) {
1810
+ return execSync5("git rev-parse --abbrev-ref HEAD", {
1811
+ cwd: worktreeDir,
1812
+ encoding: "utf-8"
1813
+ }).trim();
1814
+ }
1815
+
1816
+ // src/commands/merge.ts
1817
+ function askQuestion(rl, prompt) {
1818
+ return new Promise((resolve2) => {
1819
+ rl.question(prompt, (answer) => resolve2(answer.trim().toLowerCase()));
1820
+ });
1821
+ }
1822
+ async function reviewCandidate(rl, candidate, worktreeDir) {
1823
+ const shortSha = candidate.commitSha.slice(0, 7);
1824
+ let diffStats = "";
1825
+ try {
1826
+ diffStats = getDiffStats(worktreeDir, candidate.commitSha);
1827
+ } catch {
1828
+ diffStats = "(diff stats unavailable)";
1829
+ }
1830
+ console.log("");
1831
+ console.log(
1832
+ `${chalk7.bold(candidate.taskId)}: ${candidate.title} [${candidate.status}]`
1833
+ );
1834
+ console.log(` commit: ${shortSha} ${diffStats}`);
1835
+ while (true) {
1836
+ const answer = await askQuestion(
1837
+ rl,
1838
+ ` ${chalk7.cyan("[a]pprove")} ${chalk7.red("[r]eject")} ${chalk7.yellow("[d]iff")} ${chalk7.gray("[s]kip")} > `
1839
+ );
1840
+ if (answer === "a") return "approved";
1841
+ if (answer === "r") return "rejected";
1842
+ if (answer === "s") return "skipped";
1843
+ if (answer === "d") {
1844
+ try {
1845
+ const diff = getFullDiff(worktreeDir, candidate.commitSha);
1846
+ console.log("");
1847
+ console.log(diff);
1848
+ } catch {
1849
+ console.log(" (could not load diff)");
1850
+ }
1851
+ while (true) {
1852
+ const answer2 = await askQuestion(
1853
+ rl,
1854
+ ` ${chalk7.cyan("[a]pprove")} ${chalk7.red("[r]eject")} > `
1855
+ );
1856
+ if (answer2 === "a") return "approved";
1857
+ if (answer2 === "r") return "rejected";
1858
+ }
1859
+ }
1860
+ }
1861
+ }
1862
+ function registerMerge(program2) {
1863
+ program2.command("merge").description("Merge completed branches").argument("[project]", "project name").action(async (project) => {
1864
+ const rl = createInterface({
1865
+ input: process.stdin,
1866
+ output: process.stdout
1867
+ });
1868
+ try {
1869
+ const db = getDb();
1870
+ let projectId;
1871
+ if (project) {
1872
+ projectId = project;
1873
+ } else {
1874
+ const projects = getAllProjects(db);
1875
+ if (projects.length === 0) {
1876
+ console.log(
1877
+ "No projects registered. Run: noxdev init <project> --repo <path>"
1878
+ );
1879
+ return;
1880
+ }
1881
+ if (projects.length === 1) {
1882
+ projectId = projects[0].id;
1883
+ } else {
1884
+ console.log(
1885
+ chalk7.red(
1886
+ "Multiple projects found. Specify one: noxdev merge <project>"
1887
+ )
1888
+ );
1889
+ return;
1890
+ }
1891
+ }
1892
+ const proj = getProject(db, projectId);
1893
+ if (!proj) {
1894
+ console.log(chalk7.red(`Project not found: ${projectId}`));
1895
+ return;
1896
+ }
1897
+ const candidates = getMergeCandidates(db, projectId);
1898
+ if (candidates.length === 0) {
1899
+ console.log("No pending merge tasks.");
1900
+ return;
1901
+ }
1902
+ const run = getLatestRun(db, projectId);
1903
+ const runId = run?.id ?? "unknown";
1904
+ console.log(
1905
+ `Run ${runId}: ${chalk7.bold(String(candidates.length))} tasks pending review`
1906
+ );
1907
+ const decisions = [];
1908
+ for (const candidate of candidates) {
1909
+ const decision = await reviewCandidate(rl, candidate, proj.worktree_path);
1910
+ decisions.push({
1911
+ taskResultId: candidate.taskResultId,
1912
+ taskId: candidate.taskId,
1913
+ decision
1914
+ });
1915
+ }
1916
+ const approved = decisions.filter((d) => d.decision === "approved").length;
1917
+ const rejected = decisions.filter((d) => d.decision === "rejected").length;
1918
+ const skipped = decisions.filter((d) => d.decision === "skipped").length;
1919
+ console.log("");
1920
+ console.log(
1921
+ `Summary: ${chalk7.green(String(approved))} approved, ${chalk7.red(String(rejected))} rejected, ${chalk7.gray(String(skipped))} skipped`
1922
+ );
1923
+ if (approved > 0) {
1924
+ const confirm = await askQuestion(
1925
+ rl,
1926
+ `Merge ${approved} approved commits to main? [y/n] `
1927
+ );
1928
+ if (confirm === "y") {
1929
+ const result = applyMergeDecisions(
1930
+ db,
1931
+ proj.worktree_path,
1932
+ proj.repo_path,
1933
+ decisions
1934
+ );
1935
+ console.log(
1936
+ `Merged: ${result.merged}, Rejected: ${result.rejected}, Skipped: ${result.skipped}`
1937
+ );
1938
+ } else {
1939
+ console.log("Merge cancelled.");
1940
+ }
1941
+ }
1942
+ console.log("Run 'git push origin main' when ready.");
1943
+ } catch (err) {
1944
+ console.error(
1945
+ chalk7.red(
1946
+ `Error: ${err instanceof Error ? err.message : String(err)}`
1947
+ )
1948
+ );
1949
+ process.exitCode = 1;
1950
+ } finally {
1951
+ rl.close();
1952
+ }
1953
+ });
1954
+ }
1955
+
1956
+ // src/commands/projects.ts
1957
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
1958
+ import { join as join8 } from "path";
1959
+ import chalk8 from "chalk";
1960
+ function relativeTime3(isoDate) {
1961
+ const now = Date.now();
1962
+ const then = (/* @__PURE__ */ new Date(isoDate + "Z")).getTime();
1963
+ const diffMs = now - then;
1964
+ const diffMin = Math.floor(diffMs / 6e4);
1965
+ const diffHr = Math.floor(diffMs / 36e5);
1966
+ const diffDay = Math.floor(diffMs / 864e5);
1967
+ if (diffMin < 60) return `${Math.max(diffMin, 1)}m ago`;
1968
+ if (diffHr < 24) return `${diffHr}h ago`;
1969
+ if (diffDay < 7) return `${diffDay}d ago`;
1970
+ return isoDate.slice(0, 10);
1971
+ }
1972
+ function countPendingTasks(repoPath) {
1973
+ const tasksPath = join8(repoPath, "TASKS.md");
1974
+ if (!existsSync4(tasksPath)) return -1;
1975
+ try {
1976
+ const content = readFileSync7(tasksPath, "utf-8");
1977
+ const matches = content.match(/^- STATUS:\s*pending/gim);
1978
+ return matches ? matches.length : 0;
1979
+ } catch {
1980
+ return -1;
1981
+ }
1982
+ }
1983
+ function pad2(str, len) {
1984
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
1985
+ }
1986
+ function registerProjects(program2) {
1987
+ program2.command("projects").description("List all projects").action(() => {
1988
+ try {
1989
+ const db = getDb();
1990
+ const rows = db.prepare(
1991
+ `SELECT p.id, p.display_name, p.repo_path, p.worktree_path,
1992
+ r.started_at, r.completed, r.failed, r.status AS run_status
1993
+ FROM projects p
1994
+ LEFT JOIN runs r ON r.id = (
1995
+ SELECT r2.id FROM runs r2
1996
+ WHERE r2.project_id = p.id
1997
+ ORDER BY r2.started_at DESC LIMIT 1
1998
+ )
1999
+ ORDER BY p.display_name`
2000
+ ).all();
2001
+ if (rows.length === 0) {
2002
+ console.log(
2003
+ "No projects registered. Run: noxdev init <project> --repo <path>"
2004
+ );
2005
+ return;
2006
+ }
2007
+ const header = chalk8.bold(pad2("PROJECT", 20)) + chalk8.bold(pad2("LAST RUN", 14)) + chalk8.bold(pad2("STATUS", 20)) + chalk8.bold("TASKS");
2008
+ console.log(header);
2009
+ console.log("-".repeat(60));
2010
+ for (const row of rows) {
2011
+ const project = pad2(row.display_name, 20);
2012
+ const lastRun = row.started_at ? relativeTime3(row.started_at) : "never";
2013
+ const lastRunCol = pad2(lastRun, 14);
2014
+ let statusCol;
2015
+ if (row.started_at) {
2016
+ const c = row.completed ?? 0;
2017
+ const f = row.failed ?? 0;
2018
+ const plain = `${c} done / ${f} fail`;
2019
+ const trailing = " ".repeat(Math.max(0, 20 - plain.length));
2020
+ statusCol = `${chalk8.green(String(c))} done / ${chalk8.red(String(f))} fail${trailing}`;
2021
+ } else {
2022
+ statusCol = pad2("-", 20);
2023
+ }
2024
+ const pending = countPendingTasks(row.worktree_path);
2025
+ const tasksCol = pending >= 0 ? String(pending) : "-";
2026
+ console.log(`${project}${lastRunCol}${statusCol}${tasksCol}`);
2027
+ }
2028
+ } catch (err) {
2029
+ console.error(
2030
+ chalk8.red(
2031
+ `Error: ${err instanceof Error ? err.message : String(err)}`
2032
+ )
2033
+ );
2034
+ process.exitCode = 1;
2035
+ }
2036
+ });
2037
+ }
2038
+
2039
+ // src/commands/dashboard.ts
2040
+ import { spawn as spawn2 } from "child_process";
2041
+ import { existsSync as existsSync5 } from "fs";
2042
+ import path from "path";
2043
+ import chalk9 from "chalk";
2044
+ import express from "express";
2045
+ function registerDashboard(program2) {
2046
+ program2.command("dashboard").description("Launch the noxdev dashboard (API + UI)").option("--port <port>", "frontend port", "4401").option("--api-port <port>", "API port", "4400").action(async (opts) => {
2047
+ const bundledDashboard = path.resolve(import.meta.dirname, "dashboard");
2048
+ const monorepoDevDashboard = path.resolve(import.meta.dirname, "..", "..", "..", "packages", "dashboard");
2049
+ const dashboardDir = existsSync5(path.join(bundledDashboard, "index.html")) ? bundledDashboard : monorepoDevDashboard;
2050
+ if (!existsSync5(dashboardDir)) {
2051
+ console.error(chalk9.red("Dashboard not found. Run 'pnpm build' in the noxdev monorepo first."));
2052
+ process.exitCode = 1;
2053
+ return;
2054
+ }
2055
+ const isBundled = existsSync5(path.join(dashboardDir, "index.html"));
2056
+ if (isBundled) {
2057
+ console.log(chalk9.bold("noxdev dashboard"));
2058
+ console.log(` Dashboard: http://localhost:${opts.apiPort}`);
2059
+ console.log(" Press Ctrl+C to stop");
2060
+ const serverUrl = new URL(path.join(dashboardDir, "api", "server.js"), "file://").href;
2061
+ const { app } = await import(serverUrl);
2062
+ app.use(express.static(dashboardDir));
2063
+ app.get("*", (req, res) => {
2064
+ res.sendFile(path.join(dashboardDir, "index.html"));
2065
+ });
2066
+ const server = app.listen(parseInt(opts.apiPort), () => {
2067
+ console.log(chalk9.green(`Dashboard running on http://localhost:${opts.apiPort}`));
2068
+ });
2069
+ const cleanup = () => {
2070
+ console.log(chalk9.yellow("\nShutting down dashboard..."));
2071
+ server.close();
2072
+ process.exit(0);
2073
+ };
2074
+ process.on("SIGINT", cleanup);
2075
+ process.on("SIGTERM", cleanup);
2076
+ await new Promise(() => {
2077
+ });
2078
+ } else {
2079
+ const apiServerPath = path.join(dashboardDir, "dist", "api", "server.js");
2080
+ if (!existsSync5(apiServerPath)) {
2081
+ console.error(chalk9.red("Dashboard API not built. Run 'pnpm build' in the noxdev monorepo first."));
2082
+ process.exitCode = 1;
2083
+ return;
2084
+ }
2085
+ console.log(chalk9.bold("noxdev dashboard"));
2086
+ console.log(` API: http://localhost:${opts.apiPort}`);
2087
+ console.log(` UI: http://localhost:${opts.port}`);
2088
+ console.log(" Press Ctrl+C to stop");
2089
+ const processes = [];
2090
+ const apiProcess = spawn2("node", [apiServerPath], {
2091
+ stdio: "pipe",
2092
+ env: { ...process.env, PORT: opts.apiPort }
2093
+ });
2094
+ processes.push(apiProcess);
2095
+ const uiProcess = spawn2("npx", ["vite", "--port", opts.port, "--open"], {
2096
+ cwd: dashboardDir,
2097
+ stdio: "pipe"
2098
+ });
2099
+ processes.push(uiProcess);
2100
+ apiProcess.stdout?.on("data", (data) => {
2101
+ const lines = data.toString().split("\n").filter((line) => line.trim());
2102
+ lines.forEach((line) => {
2103
+ console.log(chalk9.blue("[API]"), line);
2104
+ });
2105
+ });
2106
+ apiProcess.stderr?.on("data", (data) => {
2107
+ const lines = data.toString().split("\n").filter((line) => line.trim());
2108
+ lines.forEach((line) => {
2109
+ console.log(chalk9.red("[API ERROR]"), line);
2110
+ });
2111
+ });
2112
+ uiProcess.stdout?.on("data", (data) => {
2113
+ const lines = data.toString().split("\n").filter((line) => line.trim());
2114
+ lines.forEach((line) => {
2115
+ console.log(chalk9.green("[UI]"), line);
2116
+ });
2117
+ });
2118
+ uiProcess.stderr?.on("data", (data) => {
2119
+ const lines = data.toString().split("\n").filter((line) => line.trim());
2120
+ lines.forEach((line) => {
2121
+ console.log(chalk9.yellow("[UI WARN]"), line);
2122
+ });
2123
+ });
2124
+ apiProcess.on("exit", (code) => {
2125
+ if (code !== 0) {
2126
+ console.log(chalk9.red(`API server exited with code ${code}`));
2127
+ }
2128
+ });
2129
+ uiProcess.on("exit", (code) => {
2130
+ if (code !== 0) {
2131
+ console.log(chalk9.red(`UI server exited with code ${code}`));
2132
+ }
2133
+ });
2134
+ const cleanup = () => {
2135
+ console.log(chalk9.yellow("\nShutting down dashboard..."));
2136
+ processes.forEach((proc) => {
2137
+ if (!proc.killed) {
2138
+ proc.kill("SIGTERM");
2139
+ }
2140
+ });
2141
+ process.exit(0);
2142
+ };
2143
+ process.on("SIGINT", cleanup);
2144
+ process.on("SIGTERM", cleanup);
2145
+ await Promise.race([
2146
+ new Promise((resolve2) => apiProcess.on("exit", () => resolve2())),
2147
+ new Promise((resolve2) => uiProcess.on("exit", () => resolve2()))
2148
+ ]);
2149
+ cleanup();
2150
+ }
2151
+ });
2152
+ }
2153
+
2154
+ // src/commands/doctor.ts
2155
+ import { execSync as execSync6 } from "child_process";
2156
+ import { existsSync as existsSync6 } from "fs";
2157
+ import { homedir as homedir7 } from "os";
2158
+ import { join as join9 } from "path";
2159
+ import chalk10 from "chalk";
2160
+ import Database2 from "better-sqlite3";
2161
+ function runCheck(name, critical, checkFn) {
2162
+ try {
2163
+ const result = checkFn();
2164
+ return { name, passed: result.passed, critical, message: result.message };
2165
+ } catch (error) {
2166
+ return {
2167
+ name,
2168
+ passed: false,
2169
+ critical,
2170
+ message: error instanceof Error ? error.message : String(error)
2171
+ };
2172
+ }
2173
+ }
2174
+ function formatCheckResult(result) {
2175
+ const symbol = result.passed ? chalk10.green("\u2713") : result.critical ? chalk10.red("\u2717") : chalk10.yellow("!");
2176
+ const message = result.message ? ` ${result.message}` : "";
2177
+ return `[${symbol}] ${result.name}${message}`;
2178
+ }
2179
+ function registerDoctor(program2) {
2180
+ program2.command("doctor").description("Check prerequisites for running noxdev").action(() => {
2181
+ console.log("noxdev doctor - checking prerequisites...\n");
2182
+ const checks = [];
2183
+ checks.push(runCheck("Node.js version >= 18", true, () => {
2184
+ const version2 = process.version;
2185
+ const major = parseInt(version2.slice(1).split(".")[0]);
2186
+ if (major >= 18) {
2187
+ return { passed: true };
2188
+ } else {
2189
+ return { passed: false, message: `Node.js 18+ required, found ${version2}` };
2190
+ }
2191
+ }));
2192
+ checks.push(runCheck("Docker installed", true, () => {
2193
+ try {
2194
+ const output = execSync6("docker --version", { encoding: "utf8" }).trim();
2195
+ return { passed: true, message: output };
2196
+ } catch {
2197
+ return { passed: false, message: "Docker not found. Install: https://docs.docker.com/get-docker/" };
2198
+ }
2199
+ }));
2200
+ checks.push(runCheck("Docker daemon running", true, () => {
2201
+ try {
2202
+ execSync6("docker info", { stdio: "pipe" });
2203
+ return { passed: true };
2204
+ } catch {
2205
+ return { passed: false, message: "Docker daemon not running. Start Docker Desktop or run: sudo systemctl start docker" };
2206
+ }
2207
+ }));
2208
+ checks.push(runCheck("Docker image exists", false, () => {
2209
+ try {
2210
+ const output = execSync6("docker images -q noxdev-runner:latest", { encoding: "utf8" }).trim();
2211
+ if (output) {
2212
+ return { passed: true };
2213
+ } else {
2214
+ return { passed: false, message: "noxdev-runner image not found. Build it with: docker build -t noxdev-runner ." };
2215
+ }
2216
+ } catch {
2217
+ return { passed: false, message: "noxdev-runner image not found. Build it with: docker build -t noxdev-runner ." };
2218
+ }
2219
+ }));
2220
+ checks.push(runCheck("noxdev config directory", false, () => {
2221
+ const configDir = join9(homedir7(), ".noxdev");
2222
+ if (existsSync6(configDir)) {
2223
+ return { passed: true };
2224
+ } else {
2225
+ return { passed: false, message: "No config directory. Run: noxdev init <project>" };
2226
+ }
2227
+ }));
2228
+ checks.push(runCheck("SQLite database", false, () => {
2229
+ const dbPath = join9(homedir7(), ".noxdev", "ledger.db");
2230
+ if (!existsSync6(dbPath)) {
2231
+ return { passed: false, message: "No database. Run: noxdev init <project>" };
2232
+ }
2233
+ try {
2234
+ const db = new Database2(dbPath, { readonly: true });
2235
+ const result = db.prepare("SELECT count(*) as count FROM projects").get();
2236
+ db.close();
2237
+ return { passed: true, message: `${result.count} projects registered` };
2238
+ } catch {
2239
+ return { passed: false, message: "No database. Run: noxdev init <project>" };
2240
+ }
2241
+ }));
2242
+ checks.push(runCheck("Git installed", true, () => {
2243
+ try {
2244
+ execSync6("git --version", { stdio: "pipe" });
2245
+ return { passed: true };
2246
+ } catch {
2247
+ return { passed: false };
2248
+ }
2249
+ }));
2250
+ checks.push(runCheck("SOPS installed", false, () => {
2251
+ try {
2252
+ execSync6("sops --version", { stdio: "pipe" });
2253
+ return { passed: true };
2254
+ } catch {
2255
+ return { passed: false, message: "SOPS not found. Secrets encryption unavailable." };
2256
+ }
2257
+ }));
2258
+ checks.push(runCheck("Claude credentials", true, () => {
2259
+ const claudePath = join9(homedir7(), ".claude.json");
2260
+ if (existsSync6(claudePath)) {
2261
+ return { passed: true };
2262
+ } else {
2263
+ return { passed: false, message: "Claude credentials not found. Run: claude login" };
2264
+ }
2265
+ }));
2266
+ for (const check of checks) {
2267
+ console.log(formatCheckResult(check));
2268
+ }
2269
+ const passed = checks.filter((c) => c.passed).length;
2270
+ const total = checks.length;
2271
+ const criticalFailed = checks.some((c) => c.critical && !c.passed);
2272
+ console.log(`
2273
+ ${passed}/${total} checks passed. ${criticalFailed ? "Issues found" : "Ready"}`);
2274
+ if (criticalFailed) {
2275
+ process.exitCode = 1;
2276
+ }
2277
+ });
2278
+ }
2279
+
2280
+ // src/commands/remove.ts
2281
+ import { execSync as execSync7 } from "child_process";
2282
+ import { rmSync, existsSync as existsSync7 } from "fs";
2283
+ import readline from "readline";
2284
+ import chalk11 from "chalk";
2285
+ function askConfirmation(message) {
2286
+ const rl = readline.createInterface({
2287
+ input: process.stdin,
2288
+ output: process.stdout
2289
+ });
2290
+ return new Promise((resolve2) => {
2291
+ rl.question(message, (answer) => {
2292
+ rl.close();
2293
+ resolve2(answer.trim().toLowerCase() === "y");
2294
+ });
2295
+ });
2296
+ }
2297
+ function registerRemove(program2) {
2298
+ program2.command("remove").description("Remove a project from noxdev").argument("<project>", "project ID to remove").option("-f, --force", "Skip confirmation prompt").action(async (projectId, opts) => {
2299
+ try {
2300
+ await runRemove(projectId, opts.force ?? false);
2301
+ } catch (err) {
2302
+ console.error(
2303
+ chalk11.red(
2304
+ `Error: ${err instanceof Error ? err.message : String(err)}`
2305
+ )
2306
+ );
2307
+ process.exitCode = 1;
2308
+ }
2309
+ });
2310
+ }
2311
+ async function runRemove(projectId, force) {
2312
+ const db = getDb();
2313
+ const project = db.prepare("SELECT id, worktree_path, repo_path FROM projects WHERE id = ?").get(projectId);
2314
+ if (!project) {
2315
+ console.error(chalk11.red(`\u2716 Project not found: ${projectId}`));
2316
+ process.exit(1);
2317
+ }
2318
+ if (!force) {
2319
+ console.log(chalk11.yellow(`\u26A0 This will remove project "${projectId}" from noxdev.`));
2320
+ console.log(chalk11.gray(" SQLite records (runs, tasks, results) will be deleted."));
2321
+ console.log(chalk11.gray(` Worktree at ${project.worktree_path} will be removed.`));
2322
+ console.log(chalk11.gray(" Your repo and main branch are NOT affected."));
2323
+ console.log();
2324
+ const confirmed = await askConfirmation("Confirm? [y/N] ");
2325
+ if (!confirmed) {
2326
+ console.log("Operation cancelled.");
2327
+ return;
2328
+ }
2329
+ }
2330
+ const worktreePath = project.worktree_path;
2331
+ const repoPath = project.repo_path;
2332
+ try {
2333
+ execSync7(`git worktree remove "${worktreePath}" --force`, {
2334
+ cwd: repoPath,
2335
+ stdio: "pipe"
2336
+ });
2337
+ } catch {
2338
+ try {
2339
+ rmSync(worktreePath, { recursive: true, force: true });
2340
+ } catch {
2341
+ }
2342
+ }
2343
+ if (!existsSync7(worktreePath)) {
2344
+ console.log(chalk11.green(` \u2713 Worktree removed: ${worktreePath}`));
2345
+ }
2346
+ db.exec("BEGIN");
2347
+ try {
2348
+ db.prepare("DELETE FROM task_results WHERE run_id IN (SELECT id FROM runs WHERE project_id = ?)").run(projectId);
2349
+ db.prepare("DELETE FROM tasks WHERE run_id IN (SELECT id FROM runs WHERE project_id = ?)").run(projectId);
2350
+ db.prepare("DELETE FROM runs WHERE project_id = ?").run(projectId);
2351
+ db.prepare("DELETE FROM projects WHERE id = ?").run(projectId);
2352
+ db.exec("COMMIT");
2353
+ } catch (err) {
2354
+ db.exec("ROLLBACK");
2355
+ throw err;
2356
+ }
2357
+ console.log(chalk11.green(`\u2713 Project "${projectId}" removed from noxdev.`));
2358
+ }
2359
+
2360
+ // src/index.ts
2361
+ var require2 = createRequire(import.meta.url);
2362
+ var { version } = require2("../package.json");
2363
+ var program = new Command();
2364
+ program.name("noxdev").description("Autonomous overnight coding agent orchestrator").version(version);
2365
+ registerInit(program);
2366
+ registerRun(program);
2367
+ registerStatus(program);
2368
+ registerLog(program);
2369
+ registerMerge(program);
2370
+ registerProjects(program);
2371
+ registerDashboard(program);
2372
+ registerDoctor(program);
2373
+ registerRemove(program);
2374
+ var args = process.argv.slice(2);
2375
+ var hasNoSubcommand = args.length === 0 || args.length === 1 && args[0].startsWith("-");
2376
+ if (hasNoSubcommand && !args.includes("--version") && !args.includes("-V")) {
2377
+ console.log(chalk12.hex("#C9A84C")(BANNER(version)));
2378
+ console.log();
2379
+ }
2380
+ program.parse();