@dunnewold-labs/mr-manager 0.3.0 → 0.4.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/index.mjs +932 -1242
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { homedir as
|
|
4
|
+
import { Command as Command27 } from "commander";
|
|
5
|
+
import { existsSync as existsSync16 } from "fs";
|
|
6
|
+
import { homedir as homedir3 } from "os";
|
|
7
7
|
import { join as join13 } from "path";
|
|
8
8
|
|
|
9
9
|
// cli/commands/init.ts
|
|
@@ -177,19 +177,70 @@ var loginCommand = new Command3("login").description("Authenticate the CLI via b
|
|
|
177
177
|
// cli/commands/projects.ts
|
|
178
178
|
import { Command as Command4 } from "commander";
|
|
179
179
|
|
|
180
|
-
// cli/
|
|
181
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
180
|
+
// cli/package.ts
|
|
181
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
182
|
+
import { dirname, join as join2, resolve } from "path";
|
|
182
183
|
import { fileURLToPath } from "url";
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
184
|
+
|
|
185
|
+
// cli/package.json
|
|
186
|
+
var package_default = {
|
|
187
|
+
name: "@dunnewold-labs/mr-manager",
|
|
188
|
+
version: "0.4.2",
|
|
189
|
+
description: "Mr. Manager - Task and project management CLI",
|
|
190
|
+
bin: {
|
|
191
|
+
mr: "./dist/index.mjs"
|
|
192
|
+
},
|
|
193
|
+
files: [
|
|
194
|
+
"dist"
|
|
195
|
+
],
|
|
196
|
+
type: "module",
|
|
197
|
+
engines: {
|
|
198
|
+
node: ">=20"
|
|
199
|
+
},
|
|
200
|
+
publishConfig: {
|
|
201
|
+
access: "public"
|
|
202
|
+
},
|
|
203
|
+
scripts: {
|
|
204
|
+
postinstall: `echo '\\n Run "mr login" to authenticate with Mr. Manager.\\n'`
|
|
205
|
+
},
|
|
206
|
+
dependencies: {
|
|
207
|
+
commander: "^13.1.0"
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// cli/package.ts
|
|
212
|
+
var FALLBACK_CLI_VERSION = "0.2.0";
|
|
213
|
+
var CLI_PACKAGE_NAME = "@dunnewold-labs/mr-manager";
|
|
214
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
215
|
+
var __dirname = dirname(__filename);
|
|
216
|
+
function isCliPackageDir(dir) {
|
|
217
|
+
const pkgPath = join2(dir, "package.json");
|
|
218
|
+
if (!existsSync2(pkgPath)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
223
|
+
return pkg.name === CLI_PACKAGE_NAME;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function resolveCliDir() {
|
|
229
|
+
const candidates = [resolve(__dirname), resolve(__dirname, "..")];
|
|
230
|
+
for (const candidate of candidates) {
|
|
231
|
+
if (isCliPackageDir(candidate)) {
|
|
232
|
+
return candidate;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return resolve(__dirname, "..");
|
|
192
236
|
}
|
|
237
|
+
var CLI_DIR = resolveCliDir();
|
|
238
|
+
var REPO_ROOT = resolve(CLI_DIR, "..");
|
|
239
|
+
var CLI_PACKAGE_PATH = join2(CLI_DIR, "package.json");
|
|
240
|
+
var CLI_VERSION = package_default.version ?? FALLBACK_CLI_VERSION;
|
|
241
|
+
|
|
242
|
+
// cli/api.ts
|
|
243
|
+
var cliVersion = CLI_VERSION;
|
|
193
244
|
var ApiError = class extends Error {
|
|
194
245
|
constructor(status, statusText, body) {
|
|
195
246
|
super(`API error ${status} ${statusText}: ${body}`);
|
|
@@ -365,7 +416,7 @@ var unlinkCommand = new Command6("unlink").description("Remove current directory
|
|
|
365
416
|
|
|
366
417
|
// cli/commands/context.ts
|
|
367
418
|
import { Command as Command7 } from "commander";
|
|
368
|
-
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, existsSync as
|
|
419
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
369
420
|
import { join as join3, sep } from "path";
|
|
370
421
|
var FEATURES_FILE = ".mr-features.md";
|
|
371
422
|
function resolveProjectRoot() {
|
|
@@ -379,13 +430,13 @@ function resolveProjectRoot() {
|
|
|
379
430
|
}
|
|
380
431
|
function readFeatures() {
|
|
381
432
|
const path = join3(resolveProjectRoot(), FEATURES_FILE);
|
|
382
|
-
if (!
|
|
433
|
+
if (!existsSync3(path)) return null;
|
|
383
434
|
return readFileSync3(path, "utf-8");
|
|
384
435
|
}
|
|
385
436
|
var contextCommand = new Command7("context").description("Output project context JSON for Claude Code").option("--install", "Install Claude Code command to .claude/commands/").action(async (opts) => {
|
|
386
437
|
if (opts.install) {
|
|
387
438
|
const dir = join3(process.cwd(), ".claude", "commands");
|
|
388
|
-
if (!
|
|
439
|
+
if (!existsSync3(dir)) {
|
|
389
440
|
mkdirSync2(dir, { recursive: true });
|
|
390
441
|
}
|
|
391
442
|
const filePath = join3(dir, "mr-tasks.md");
|
|
@@ -408,11 +459,13 @@ var contextCommand = new Command7("context").description("Output project context
|
|
|
408
459
|
`/api/projects/${projectId}/tasks`
|
|
409
460
|
);
|
|
410
461
|
const inProgress = allTasks.filter((t) => t.status === "in_progress" || t.status === "queued" || t.status === "delegated" || t.status === "review");
|
|
462
|
+
const needsAttention = allTasks.filter((t) => t.status === "error");
|
|
411
463
|
const todo = allTasks.filter((t) => t.status === "todo");
|
|
412
464
|
const recentlyCompleted = allTasks.filter((t) => t.status === "completed").slice(0, 10);
|
|
413
465
|
const summaryLines = [
|
|
414
466
|
`Project: ${project.name} (${project.status})`,
|
|
415
467
|
...inProgress.map((t) => `In Progress: ${t.title}`),
|
|
468
|
+
...needsAttention.map((t) => `Needs Attention: ${t.title}`),
|
|
416
469
|
`Todo: ${todo.length} tasks`,
|
|
417
470
|
`Recently Completed: ${recentlyCompleted.length} tasks`
|
|
418
471
|
];
|
|
@@ -438,23 +491,22 @@ var contextCommand = new Command7("context").description("Output project context
|
|
|
438
491
|
import { Command as Command8 } from "commander";
|
|
439
492
|
import { spawn as spawn4, exec } from "child_process";
|
|
440
493
|
import { randomUUID } from "crypto";
|
|
441
|
-
import { resolve
|
|
442
|
-
import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as
|
|
443
|
-
import { homedir as homedir2 } from "os";
|
|
494
|
+
import { resolve as resolve2 } from "path";
|
|
495
|
+
import { readFileSync as readFileSync5, readdirSync, unlinkSync, existsSync as existsSync7, statSync } from "fs";
|
|
444
496
|
import * as readline from "readline";
|
|
445
497
|
|
|
446
498
|
// lib/test-runner.ts
|
|
447
499
|
import { spawn as spawn2 } from "child_process";
|
|
448
|
-
import { existsSync as
|
|
500
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
449
501
|
import { join as join5 } from "path";
|
|
450
502
|
|
|
451
503
|
// lib/git-worktree.ts
|
|
452
504
|
import { execSync as execSync2 } from "child_process";
|
|
453
|
-
import { copyFileSync, existsSync as
|
|
505
|
+
import { copyFileSync, existsSync as existsSync4 } from "fs";
|
|
454
506
|
import { join as join4 } from "path";
|
|
455
507
|
function createWorktree(repoDir, branch, worktreeName) {
|
|
456
508
|
const wtPath = join4(repoDir, ".mr-worktrees", worktreeName);
|
|
457
|
-
if (
|
|
509
|
+
if (existsSync4(wtPath)) {
|
|
458
510
|
execSync2(`git checkout ${branch}`, { cwd: wtPath, stdio: "pipe" });
|
|
459
511
|
return wtPath;
|
|
460
512
|
}
|
|
@@ -482,7 +534,7 @@ function createWorktree(repoDir, branch, worktreeName) {
|
|
|
482
534
|
}
|
|
483
535
|
for (const envFile of [".env", ".env.local"]) {
|
|
484
536
|
const src = join4(repoDir, envFile);
|
|
485
|
-
if (
|
|
537
|
+
if (existsSync4(src)) {
|
|
486
538
|
copyFileSync(src, join4(wtPath, envFile));
|
|
487
539
|
}
|
|
488
540
|
}
|
|
@@ -525,13 +577,13 @@ function extractBranchFromLink(link, cwd) {
|
|
|
525
577
|
return null;
|
|
526
578
|
}
|
|
527
579
|
function installDependencies(wtPath) {
|
|
528
|
-
if (
|
|
580
|
+
if (existsSync4(join4(wtPath, "bun.lock")) || existsSync4(join4(wtPath, "bun.lockb"))) {
|
|
529
581
|
execSync2("bun install", { cwd: wtPath, stdio: "pipe" });
|
|
530
|
-
} else if (
|
|
582
|
+
} else if (existsSync4(join4(wtPath, "pnpm-lock.yaml"))) {
|
|
531
583
|
execSync2("pnpm install --frozen-lockfile", { cwd: wtPath, stdio: "pipe" });
|
|
532
|
-
} else if (
|
|
584
|
+
} else if (existsSync4(join4(wtPath, "package-lock.json"))) {
|
|
533
585
|
execSync2("npm ci", { cwd: wtPath, stdio: "pipe" });
|
|
534
|
-
} else if (
|
|
586
|
+
} else if (existsSync4(join4(wtPath, "yarn.lock"))) {
|
|
535
587
|
execSync2("yarn install --frozen-lockfile", { cwd: wtPath, stdio: "pipe" });
|
|
536
588
|
}
|
|
537
589
|
}
|
|
@@ -539,7 +591,7 @@ function installDependencies(wtPath) {
|
|
|
539
591
|
// lib/test-runner.ts
|
|
540
592
|
function detectDevCommand(wtPath) {
|
|
541
593
|
const configPath2 = join5(wtPath, ".mr-test.json");
|
|
542
|
-
if (
|
|
594
|
+
if (existsSync5(configPath2)) {
|
|
543
595
|
try {
|
|
544
596
|
const config = JSON.parse(readFileSync4(configPath2, "utf-8"));
|
|
545
597
|
if (config.devCommand) {
|
|
@@ -549,16 +601,16 @@ function detectDevCommand(wtPath) {
|
|
|
549
601
|
} catch {
|
|
550
602
|
}
|
|
551
603
|
}
|
|
552
|
-
if (
|
|
604
|
+
if (existsSync5(join5(wtPath, "bun.lock")) || existsSync5(join5(wtPath, "bun.lockb"))) {
|
|
553
605
|
return { cmd: "bun", args: ["dev"] };
|
|
554
606
|
}
|
|
555
|
-
if (
|
|
607
|
+
if (existsSync5(join5(wtPath, "pnpm-lock.yaml"))) {
|
|
556
608
|
return { cmd: "pnpm", args: ["dev"] };
|
|
557
609
|
}
|
|
558
|
-
if (
|
|
610
|
+
if (existsSync5(join5(wtPath, "package-lock.json"))) {
|
|
559
611
|
return { cmd: "npm", args: ["run", "dev"] };
|
|
560
612
|
}
|
|
561
|
-
if (
|
|
613
|
+
if (existsSync5(join5(wtPath, "yarn.lock"))) {
|
|
562
614
|
return { cmd: "yarn", args: ["dev"] };
|
|
563
615
|
}
|
|
564
616
|
return { cmd: "npm", args: ["run", "dev"] };
|
|
@@ -696,7 +748,7 @@ async function runTest(options) {
|
|
|
696
748
|
postUpdate,
|
|
697
749
|
onProgress
|
|
698
750
|
} = options;
|
|
699
|
-
const
|
|
751
|
+
const log2 = onProgress || (() => {
|
|
700
752
|
});
|
|
701
753
|
const result = {
|
|
702
754
|
status: "passed",
|
|
@@ -709,37 +761,37 @@ async function runTest(options) {
|
|
|
709
761
|
let wtPath = null;
|
|
710
762
|
const worktreeName = `mr-test-${taskId.slice(0, 8)}`;
|
|
711
763
|
const timeoutHandle = setTimeout(() => {
|
|
712
|
-
|
|
764
|
+
log2("Test timed out after 5 minutes");
|
|
713
765
|
if (devProc) devProc.kill("SIGTERM");
|
|
714
766
|
}, 5 * 60 * 1e3);
|
|
715
767
|
try {
|
|
716
|
-
|
|
768
|
+
log2("Extracting branch from MR/PR link...");
|
|
717
769
|
const branch = extractBranchFromLink(taskLink, localPath);
|
|
718
770
|
if (!branch) {
|
|
719
771
|
throw new Error(`Could not extract branch from link: ${taskLink}`);
|
|
720
772
|
}
|
|
721
|
-
|
|
722
|
-
|
|
773
|
+
log2(`Branch: ${branch}`);
|
|
774
|
+
log2("Creating git worktree...");
|
|
723
775
|
wtPath = createWorktree(localPath, branch, worktreeName);
|
|
724
|
-
|
|
725
|
-
|
|
776
|
+
log2(`Worktree created at ${wtPath}`);
|
|
777
|
+
log2("Installing dependencies...");
|
|
726
778
|
try {
|
|
727
779
|
installDependencies(wtPath);
|
|
728
780
|
} catch (err) {
|
|
729
|
-
|
|
781
|
+
log2(`Warning: dependency install failed: ${err.message}`);
|
|
730
782
|
}
|
|
731
|
-
|
|
783
|
+
log2("Starting dev server...");
|
|
732
784
|
const port = await findAvailablePort(4e3);
|
|
733
785
|
devProc = await startDevServer(wtPath, port);
|
|
734
786
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
735
|
-
|
|
787
|
+
log2(`Dev server running on ${baseUrl}`);
|
|
736
788
|
await browseRunner(["goto", baseUrl]);
|
|
737
789
|
const plan = customPlan || buildDefaultTestPlan(baseUrl);
|
|
738
|
-
|
|
790
|
+
log2(`Executing ${plan.length}-step test plan...`);
|
|
739
791
|
for (let i = 0; i < plan.length; i++) {
|
|
740
792
|
const step = plan[i];
|
|
741
793
|
const stepDesc = step.description || `${step.command} ${(step.args || []).join(" ")}`;
|
|
742
|
-
|
|
794
|
+
log2(`Step ${i + 1}/${plan.length}: ${stepDesc}`);
|
|
743
795
|
try {
|
|
744
796
|
if (step.command.startsWith("assert")) {
|
|
745
797
|
const assertResult = await evaluateAssertion(step, i, browseRunner);
|
|
@@ -789,7 +841,7 @@ async function runTest(options) {
|
|
|
789
841
|
}
|
|
790
842
|
} catch (err) {
|
|
791
843
|
result.errors.push(`Step ${i + 1} (${step.command}): ${err.message}`);
|
|
792
|
-
|
|
844
|
+
log2(`Step ${i + 1} error: ${err.message}`);
|
|
793
845
|
}
|
|
794
846
|
}
|
|
795
847
|
const totalAssertions = result.assertions.length;
|
|
@@ -835,18 +887,18 @@ async function runTest(options) {
|
|
|
835
887
|
|
|
836
888
|
// cli/browse-runner.ts
|
|
837
889
|
import { execSync as execSync4, spawn as spawn3 } from "child_process";
|
|
838
|
-
import { existsSync as
|
|
890
|
+
import { existsSync as existsSync6 } from "fs";
|
|
839
891
|
import { join as join6 } from "path";
|
|
840
892
|
var BROWSE_DIR = join6(import.meta.dirname, "..", "..", "browse");
|
|
841
893
|
var BROWSE_BINARY = join6(BROWSE_DIR, "dist", "browse");
|
|
842
894
|
var BROWSE_DEV_CMD = join6(BROWSE_DIR, "src", "cli.ts");
|
|
843
895
|
function getBrowseRunner() {
|
|
844
|
-
if (
|
|
896
|
+
if (existsSync6(BROWSE_BINARY)) {
|
|
845
897
|
return { cmd: BROWSE_BINARY, args: [] };
|
|
846
898
|
}
|
|
847
899
|
try {
|
|
848
900
|
execSync4("which bun", { stdio: "pipe" });
|
|
849
|
-
if (
|
|
901
|
+
if (existsSync6(BROWSE_DEV_CMD)) {
|
|
850
902
|
return { cmd: "bun", args: ["run", BROWSE_DEV_CMD] };
|
|
851
903
|
}
|
|
852
904
|
} catch {
|
|
@@ -886,95 +938,11 @@ async function runBrowseCommand2(browseArgs) {
|
|
|
886
938
|
|
|
887
939
|
// cli/commands/watch.ts
|
|
888
940
|
var FEATURES_FILE2 = ".mr-features.md";
|
|
889
|
-
var CLAUDE_PROJECTS_DIR = join7(homedir2(), ".claude", "projects");
|
|
890
941
|
function readFeaturesDoc(repoDir) {
|
|
891
|
-
const path =
|
|
892
|
-
if (!
|
|
942
|
+
const path = resolve2(repoDir, FEATURES_FILE2);
|
|
943
|
+
if (!existsSync7(path)) return null;
|
|
893
944
|
return readFileSync5(path, "utf-8");
|
|
894
945
|
}
|
|
895
|
-
function toClaudePath(dir) {
|
|
896
|
-
return dir.replace(/\//g, "-");
|
|
897
|
-
}
|
|
898
|
-
function findSessionFile(sessionId, repoDir) {
|
|
899
|
-
const mainPath = join7(CLAUDE_PROJECTS_DIR, toClaudePath(repoDir));
|
|
900
|
-
const mainFile = join7(mainPath, `${sessionId}.jsonl`);
|
|
901
|
-
if (existsSync6(mainFile)) return mainFile;
|
|
902
|
-
const worktreePrefix = toClaudePath(repoDir) + "-.mr-worktrees-";
|
|
903
|
-
try {
|
|
904
|
-
const entries = readdirSync(CLAUDE_PROJECTS_DIR);
|
|
905
|
-
for (const entry of entries) {
|
|
906
|
-
if (entry.startsWith(worktreePrefix)) {
|
|
907
|
-
const worktreeFile = join7(CLAUDE_PROJECTS_DIR, entry, `${sessionId}.jsonl`);
|
|
908
|
-
if (existsSync6(worktreeFile)) return worktreeFile;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
} catch {
|
|
912
|
-
}
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
function parseSessionJsonl(raw) {
|
|
916
|
-
const lines = raw.split("\n").filter((l) => l.trim());
|
|
917
|
-
const entries = [];
|
|
918
|
-
for (const line of lines) {
|
|
919
|
-
try {
|
|
920
|
-
const parsed = JSON.parse(line);
|
|
921
|
-
if (parsed.type !== "user" && parsed.type !== "assistant") continue;
|
|
922
|
-
if (!parsed.message) continue;
|
|
923
|
-
const content = [];
|
|
924
|
-
const msgContent = parsed.message.content;
|
|
925
|
-
if (typeof msgContent === "string") {
|
|
926
|
-
content.push({ type: "text", text: msgContent });
|
|
927
|
-
} else if (Array.isArray(msgContent)) {
|
|
928
|
-
for (const block of msgContent) {
|
|
929
|
-
if (block.type === "thinking" && block.thinking) {
|
|
930
|
-
content.push({ type: "thinking", thinking: block.thinking });
|
|
931
|
-
} else if (block.type === "text" && block.text) {
|
|
932
|
-
content.push({ type: "text", text: block.text });
|
|
933
|
-
} else if (block.type === "tool_use") {
|
|
934
|
-
content.push({
|
|
935
|
-
type: "tool_use",
|
|
936
|
-
id: block.id,
|
|
937
|
-
name: block.name,
|
|
938
|
-
input: block.input ?? {}
|
|
939
|
-
});
|
|
940
|
-
} else if (block.type === "tool_result") {
|
|
941
|
-
let resultContent = block.content;
|
|
942
|
-
if (typeof resultContent === "string") {
|
|
943
|
-
} else if (Array.isArray(resultContent)) {
|
|
944
|
-
resultContent = resultContent.filter((r) => r.type === "text").map((r) => r.text).join("\n");
|
|
945
|
-
} else {
|
|
946
|
-
resultContent = "";
|
|
947
|
-
}
|
|
948
|
-
content.push({
|
|
949
|
-
type: "tool_result",
|
|
950
|
-
tool_use_id: block.tool_use_id,
|
|
951
|
-
content: resultContent
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
if (content.length === 0) continue;
|
|
957
|
-
entries.push({
|
|
958
|
-
type: parsed.type,
|
|
959
|
-
timestamp: parsed.timestamp,
|
|
960
|
-
uuid: parsed.uuid,
|
|
961
|
-
content
|
|
962
|
-
});
|
|
963
|
-
} catch {
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
return entries;
|
|
967
|
-
}
|
|
968
|
-
function readAndParseSessionLog(sessionId, repoDir) {
|
|
969
|
-
const filePath = findSessionFile(sessionId, repoDir);
|
|
970
|
-
if (!filePath) return null;
|
|
971
|
-
try {
|
|
972
|
-
const raw = readFileSync5(filePath, "utf-8");
|
|
973
|
-
return parseSessionJsonl(raw);
|
|
974
|
-
} catch {
|
|
975
|
-
return null;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
946
|
var c = {
|
|
979
947
|
reset: "\x1B[0m",
|
|
980
948
|
bold: "\x1B[1m",
|
|
@@ -1028,9 +996,20 @@ async function postTaskUpdate(taskId, message, source = "system") {
|
|
|
1028
996
|
logError(taskTag(shortId(taskId)), `Failed to post update: ${err.message}`);
|
|
1029
997
|
}
|
|
1030
998
|
}
|
|
999
|
+
var DEFAULT_TASK_STALL_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
1000
|
+
function getTaskStallTimeoutMs() {
|
|
1001
|
+
const raw = process.env.MR_TASK_STALL_TIMEOUT_MS;
|
|
1002
|
+
if (!raw) return DEFAULT_TASK_STALL_TIMEOUT_MS;
|
|
1003
|
+
const parsed = Number(raw);
|
|
1004
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TASK_STALL_TIMEOUT_MS;
|
|
1005
|
+
}
|
|
1006
|
+
function formatTimeoutMinutes(ms) {
|
|
1007
|
+
const minutes = Math.round(ms / 6e4);
|
|
1008
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
1009
|
+
}
|
|
1031
1010
|
function findDirectoryForProject(config, projectId, rootDir) {
|
|
1032
1011
|
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
1033
|
-
if (pid === projectId &&
|
|
1012
|
+
if (pid === projectId && resolve2(dir).startsWith(resolve2(rootDir))) {
|
|
1034
1013
|
return dir;
|
|
1035
1014
|
}
|
|
1036
1015
|
}
|
|
@@ -1102,14 +1081,14 @@ function findPrUrl(branchName, repoDir, vcs = "github") {
|
|
|
1102
1081
|
}
|
|
1103
1082
|
function isGitRepo(dir) {
|
|
1104
1083
|
try {
|
|
1105
|
-
return
|
|
1084
|
+
return existsSync7(resolve2(dir, ".git"));
|
|
1106
1085
|
} catch {
|
|
1107
1086
|
return false;
|
|
1108
1087
|
}
|
|
1109
1088
|
}
|
|
1110
1089
|
function findChildGitRepos(parentDir) {
|
|
1111
1090
|
try {
|
|
1112
|
-
return readdirSync(parentDir).filter((name) => !name.startsWith(".")).map((name) =>
|
|
1091
|
+
return readdirSync(parentDir).filter((name) => !name.startsWith(".")).map((name) => resolve2(parentDir, name)).filter((path) => {
|
|
1113
1092
|
try {
|
|
1114
1093
|
return statSync(path).isDirectory() && isGitRepo(path);
|
|
1115
1094
|
} catch {
|
|
@@ -1808,7 +1787,7 @@ function buildAgentArgs(agent, prompt2, mode, sessionId, name) {
|
|
|
1808
1787
|
if (agent === "codex") {
|
|
1809
1788
|
const args = ["exec"];
|
|
1810
1789
|
if (mode === "execute") {
|
|
1811
|
-
args.push("
|
|
1790
|
+
args.push("-a", "never", "-s", "danger-full-access");
|
|
1812
1791
|
}
|
|
1813
1792
|
args.push(prompt2);
|
|
1814
1793
|
return { bin: "codex", args };
|
|
@@ -1847,7 +1826,7 @@ function runPlanningPhase(task, repoDir, agent) {
|
|
|
1847
1826
|
${output.trim()}`));
|
|
1848
1827
|
return;
|
|
1849
1828
|
}
|
|
1850
|
-
const planPath =
|
|
1829
|
+
const planPath = resolve2(repoDir, "plan.md");
|
|
1851
1830
|
try {
|
|
1852
1831
|
const planContent = readFileSync5(planPath, "utf-8");
|
|
1853
1832
|
try {
|
|
@@ -1873,7 +1852,7 @@ function askYesNo(question) {
|
|
|
1873
1852
|
});
|
|
1874
1853
|
});
|
|
1875
1854
|
}
|
|
1876
|
-
function spawnAgent(agent, repoDir, prompt2, prefix, sessionId, name) {
|
|
1855
|
+
function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name) {
|
|
1877
1856
|
const { bin, args } = buildAgentArgs(agent, prompt2, "execute", sessionId, name);
|
|
1878
1857
|
const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
|
|
1879
1858
|
child.on("error", (err) => {
|
|
@@ -1882,12 +1861,19 @@ function spawnAgent(agent, repoDir, prompt2, prefix, sessionId, name) {
|
|
|
1882
1861
|
logError(prefix, `Check that "${bin}" is on PATH and "${repoDir}" exists`);
|
|
1883
1862
|
}
|
|
1884
1863
|
});
|
|
1864
|
+
if (agent === "codex") {
|
|
1865
|
+
child.stdout?.on("data", () => onActivity?.());
|
|
1866
|
+
child.stderr?.on("data", () => onActivity?.());
|
|
1867
|
+
return child;
|
|
1868
|
+
}
|
|
1885
1869
|
child.stdout?.on("data", (data) => {
|
|
1870
|
+
onActivity?.();
|
|
1886
1871
|
for (const line of data.toString().split("\n")) {
|
|
1887
1872
|
if (line) console.log(`${timestamp()} ${prefix} ${paint("dim", line)}`);
|
|
1888
1873
|
}
|
|
1889
1874
|
});
|
|
1890
1875
|
child.stderr?.on("data", (data) => {
|
|
1876
|
+
onActivity?.();
|
|
1891
1877
|
for (const line of data.toString().split("\n")) {
|
|
1892
1878
|
if (line) logError(prefix, paint("dim", line));
|
|
1893
1879
|
}
|
|
@@ -1900,18 +1886,22 @@ var watchCommand = new Command8("watch").description(
|
|
|
1900
1886
|
const intervalMs = parseInt(opts.interval, 10) * 1e3;
|
|
1901
1887
|
const dryRun = opts.dryRun;
|
|
1902
1888
|
const planApproval = opts.planApproval;
|
|
1903
|
-
const rootDir = opts.root ?
|
|
1889
|
+
const rootDir = opts.root ? resolve2(opts.root) : process.cwd();
|
|
1904
1890
|
const agent = opts.agent === "codex" ? "codex" : opts.agent === "gemini" ? "gemini" : "claude";
|
|
1905
1891
|
const scanAt = opts.scanAt;
|
|
1892
|
+
const taskStallTimeoutMs = getTaskStallTimeoutMs();
|
|
1906
1893
|
const active = /* @__PURE__ */ new Map();
|
|
1907
1894
|
const failed = /* @__PURE__ */ new Map();
|
|
1908
1895
|
const queued = /* @__PURE__ */ new Set();
|
|
1896
|
+
const finishing = /* @__PURE__ */ new Set();
|
|
1897
|
+
let pollRunning = false;
|
|
1909
1898
|
const approvalQueue = [];
|
|
1910
1899
|
let approvalRunning = false;
|
|
1911
1900
|
const flags = [
|
|
1912
1901
|
`interval=${paint("cyan", opts.interval + "s")}`,
|
|
1913
1902
|
`root=${paint("cyan", rootDir)}`,
|
|
1914
1903
|
`agent=${paint("cyan", agent)}`,
|
|
1904
|
+
`stall-timeout=${paint("cyan", formatTimeoutMinutes(taskStallTimeoutMs))}`,
|
|
1915
1905
|
...planApproval ? [paint("yellow", "plan-approval")] : [],
|
|
1916
1906
|
...dryRun ? [paint("yellow", "dry-run")] : [],
|
|
1917
1907
|
...scanAt ? [`scan-at=${paint("cyan", scanAt)}`] : []
|
|
@@ -1981,124 +1971,94 @@ var watchCommand = new Command8("watch").description(
|
|
|
1981
1971
|
}
|
|
1982
1972
|
const prompt2 = buildExecutionPrompt(task, repoDir, subtasks, vcs, protoRefs, feedbackUpdates, existingResources, skillRefs);
|
|
1983
1973
|
const sessionId = agent === "claude" ? randomUUID() : void 0;
|
|
1984
|
-
const
|
|
1974
|
+
const activeEntry = {
|
|
1975
|
+
process: void 0,
|
|
1976
|
+
title: task.title,
|
|
1977
|
+
repoDir,
|
|
1978
|
+
startedAt: Date.now(),
|
|
1979
|
+
lastActivityAt: Date.now()
|
|
1980
|
+
};
|
|
1981
|
+
const touchActivity = () => {
|
|
1982
|
+
activeEntry.lastActivityAt = Date.now();
|
|
1983
|
+
};
|
|
1984
|
+
const child = spawnAgent(agent, repoDir, prompt2, prefix, touchActivity, sessionId, task.title);
|
|
1985
|
+
activeEntry.process = child;
|
|
1985
1986
|
if (sessionId) {
|
|
1986
1987
|
api.patch(`/api/tasks/${task.id}`, { claudeSessionId: sessionId }).catch(() => {
|
|
1987
1988
|
});
|
|
1988
1989
|
logInfo(prefix, `Claude session: ${paint("dim", sessionId)}`);
|
|
1989
1990
|
}
|
|
1990
|
-
active.set(task.id,
|
|
1991
|
+
active.set(task.id, activeEntry);
|
|
1991
1992
|
child.on("exit", async (code) => {
|
|
1992
1993
|
active.delete(task.id);
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
logSuccess(prefix, `Updated existing research resource`);
|
|
2006
|
-
} else {
|
|
2007
|
-
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2008
|
-
type: "research",
|
|
2009
|
-
title: `Research \u2014 ${task.title}`,
|
|
2010
|
-
content: researchContent
|
|
2011
|
-
});
|
|
2012
|
-
logSuccess(prefix, `Uploaded research.md as task resource`);
|
|
2013
|
-
}
|
|
2014
|
-
unlinkSync(researchPath);
|
|
2015
|
-
} catch (err) {
|
|
2016
|
-
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
if (sessionId) {
|
|
2020
|
-
try {
|
|
2021
|
-
const sessionEntries = readAndParseSessionLog(sessionId, repoDir);
|
|
2022
|
-
if (sessionEntries && sessionEntries.length > 0) {
|
|
2023
|
-
const sessionContent = JSON.stringify(sessionEntries);
|
|
2024
|
-
const existingSessionLog = existingResources.find((r) => r.type === "session-log");
|
|
2025
|
-
if (existingSessionLog) {
|
|
2026
|
-
await api.patch(`/api/tasks/${task.id}/resources/${existingSessionLog.id}`, {
|
|
2027
|
-
content: sessionContent
|
|
1994
|
+
finishing.add(task.id);
|
|
1995
|
+
try {
|
|
1996
|
+
if (code === 0) {
|
|
1997
|
+
try {
|
|
1998
|
+
const researchPath = resolve2(repoDir, "research.md");
|
|
1999
|
+
if (existsSync7(researchPath)) {
|
|
2000
|
+
try {
|
|
2001
|
+
const researchContent = readFileSync5(researchPath, "utf-8");
|
|
2002
|
+
const existingResearch = existingResources.find((r) => r.type === "research");
|
|
2003
|
+
if (existingResearch) {
|
|
2004
|
+
await api.patch(`/api/tasks/${task.id}/resources/${existingResearch.id}`, {
|
|
2005
|
+
content: researchContent
|
|
2028
2006
|
});
|
|
2029
|
-
logSuccess(prefix, `Updated
|
|
2007
|
+
logSuccess(prefix, `Updated existing research resource`);
|
|
2030
2008
|
} else {
|
|
2031
2009
|
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2032
|
-
type: "
|
|
2033
|
-
title: `
|
|
2034
|
-
content:
|
|
2010
|
+
type: "research",
|
|
2011
|
+
title: `Research \u2014 ${task.title}`,
|
|
2012
|
+
content: researchContent
|
|
2035
2013
|
});
|
|
2036
|
-
logSuccess(prefix, `Uploaded
|
|
2014
|
+
logSuccess(prefix, `Uploaded research.md as task resource`);
|
|
2037
2015
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
const noMrPath = resolve(repoDir, ".mr-no-mr");
|
|
2044
|
-
const noMrRequested = existsSync6(noMrPath);
|
|
2045
|
-
let noMrDescription;
|
|
2046
|
-
if (noMrRequested) {
|
|
2047
|
-
noMrDescription = readFileSync5(noMrPath, "utf-8").trim();
|
|
2048
|
-
unlinkSync(noMrPath);
|
|
2049
|
-
logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
|
|
2050
|
-
}
|
|
2051
|
-
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2052
|
-
let prUrl = null;
|
|
2053
|
-
if (!noMrRequested) {
|
|
2054
|
-
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2055
|
-
if (!prUrl) {
|
|
2056
|
-
prUrl = await extractPrUrlFromUpdates(task.id);
|
|
2057
|
-
if (prUrl) {
|
|
2058
|
-
logInfo(prefix, `Found ${prLabel} URL from agent updates: ${paint("cyan", prUrl)}`);
|
|
2016
|
+
unlinkSync(researchPath);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2059
2019
|
}
|
|
2060
2020
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2021
|
+
const noMrPath = resolve2(repoDir, ".mr-no-mr");
|
|
2022
|
+
const noMrRequested = existsSync7(noMrPath);
|
|
2023
|
+
let noMrDescription;
|
|
2024
|
+
if (noMrRequested) {
|
|
2025
|
+
noMrDescription = readFileSync5(noMrPath, "utf-8").trim();
|
|
2026
|
+
unlinkSync(noMrPath);
|
|
2027
|
+
logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
|
|
2066
2028
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
await postTaskUpdate(task.id, `Agent failed with exit code ${code}`, "system");
|
|
2080
|
-
if (sessionId) {
|
|
2081
|
-
try {
|
|
2082
|
-
const sessionEntries = readAndParseSessionLog(sessionId, repoDir);
|
|
2083
|
-
if (sessionEntries && sessionEntries.length > 0) {
|
|
2084
|
-
const sessionContent = JSON.stringify(sessionEntries);
|
|
2085
|
-
const existingSessionLog = existingResources.find((r) => r.type === "session-log");
|
|
2086
|
-
if (existingSessionLog) {
|
|
2087
|
-
await api.patch(`/api/tasks/${task.id}/resources/${existingSessionLog.id}`, {
|
|
2088
|
-
content: sessionContent
|
|
2089
|
-
});
|
|
2029
|
+
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2030
|
+
let prUrl = null;
|
|
2031
|
+
if (!noMrRequested) {
|
|
2032
|
+
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2033
|
+
if (!prUrl) {
|
|
2034
|
+
prUrl = await extractPrUrlFromUpdates(task.id);
|
|
2035
|
+
if (prUrl) {
|
|
2036
|
+
logInfo(prefix, `Found ${prLabel} URL from agent updates: ${paint("cyan", prUrl)}`);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
if (prUrl) {
|
|
2040
|
+
logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
|
|
2090
2041
|
} else {
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
title: `Session Log \u2014 ${task.title}`,
|
|
2094
|
-
content: sessionContent
|
|
2095
|
-
});
|
|
2042
|
+
logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
|
|
2043
|
+
await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
|
|
2096
2044
|
}
|
|
2097
|
-
logInfo(prefix, `Uploaded session log for failed task (${sessionEntries.length} messages)`);
|
|
2098
2045
|
}
|
|
2099
|
-
|
|
2046
|
+
await api.patch(`/api/tasks/${task.id}`, {
|
|
2047
|
+
status: "review",
|
|
2048
|
+
...prUrl ? { link: prUrl } : {}
|
|
2049
|
+
});
|
|
2050
|
+
logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
|
|
2051
|
+
await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
|
|
2052
|
+
} catch (err) {
|
|
2053
|
+
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2100
2054
|
}
|
|
2055
|
+
} else if (!activeEntry.terminatedForError) {
|
|
2056
|
+
logError(prefix, `"${paint("bold", task.title)}" failed (exit ${code}), leaving status unchanged`);
|
|
2057
|
+
await postTaskUpdate(task.id, `Agent failed with exit code ${code}`, "system");
|
|
2101
2058
|
}
|
|
2059
|
+
} finally {
|
|
2060
|
+
queued.delete(task.id);
|
|
2061
|
+
finishing.delete(task.id);
|
|
2102
2062
|
}
|
|
2103
2063
|
});
|
|
2104
2064
|
}
|
|
@@ -2135,49 +2095,54 @@ var watchCommand = new Command8("watch").description(
|
|
|
2135
2095
|
active.set(task.id, { process: child, title: task.title, repoDir });
|
|
2136
2096
|
child.on("exit", async (code) => {
|
|
2137
2097
|
active.delete(task.id);
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
const prdPath = resolve(repoDir, "prd.md");
|
|
2142
|
-
let prdContent;
|
|
2098
|
+
finishing.add(task.id);
|
|
2099
|
+
try {
|
|
2100
|
+
if (code === 0) {
|
|
2143
2101
|
try {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
unlinkSync(prdPath);
|
|
2147
|
-
} catch {
|
|
2148
|
-
logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
|
|
2149
|
-
}
|
|
2150
|
-
if (prdContent) {
|
|
2102
|
+
const prdPath = resolve2(repoDir, "prd.md");
|
|
2103
|
+
let prdContent;
|
|
2151
2104
|
try {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2105
|
+
prdContent = readFileSync5(prdPath, "utf-8");
|
|
2106
|
+
logInfo(prefix, `Read PRD from ${paint("cyan", "prd.md")} (${prdContent.length} chars)`);
|
|
2107
|
+
unlinkSync(prdPath);
|
|
2108
|
+
} catch {
|
|
2109
|
+
logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
|
|
2110
|
+
}
|
|
2111
|
+
if (prdContent) {
|
|
2112
|
+
try {
|
|
2113
|
+
if (existingPlanResource) {
|
|
2114
|
+
await api.patch(`/api/tasks/${task.id}/resources/${existingPlanResource.id}`, {
|
|
2115
|
+
content: prdContent
|
|
2116
|
+
});
|
|
2117
|
+
logSuccess(prefix, `Updated existing PRD resource`);
|
|
2118
|
+
} else {
|
|
2119
|
+
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2120
|
+
type: "plan",
|
|
2121
|
+
title: `PRD \u2014 ${task.title}`,
|
|
2122
|
+
content: prdContent
|
|
2123
|
+
});
|
|
2124
|
+
logSuccess(prefix, `Uploaded PRD as task resource`);
|
|
2125
|
+
}
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
|
|
2164
2128
|
}
|
|
2165
|
-
} catch (err) {
|
|
2166
|
-
logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
|
|
2167
2129
|
}
|
|
2130
|
+
await api.patch(`/api/tasks/${task.id}`, {
|
|
2131
|
+
status: "review",
|
|
2132
|
+
...prdContent ? { prdContent } : {}
|
|
2133
|
+
});
|
|
2134
|
+
logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
|
|
2135
|
+
await postTaskUpdate(task.id, "PRD generated \u2014 ready for review", "system");
|
|
2136
|
+
} catch (err) {
|
|
2137
|
+
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2168
2138
|
}
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
});
|
|
2173
|
-
logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
|
|
2174
|
-
await postTaskUpdate(task.id, "PRD generated \u2014 ready for review", "system");
|
|
2175
|
-
} catch (err) {
|
|
2176
|
-
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2139
|
+
} else {
|
|
2140
|
+
logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (exit ${code}), leaving status unchanged`);
|
|
2141
|
+
await postTaskUpdate(task.id, `PRD generation failed with exit code ${code}`, "system");
|
|
2177
2142
|
}
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2143
|
+
} finally {
|
|
2144
|
+
queued.delete(task.id);
|
|
2145
|
+
finishing.delete(task.id);
|
|
2181
2146
|
}
|
|
2182
2147
|
});
|
|
2183
2148
|
}
|
|
@@ -2188,7 +2153,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2188
2153
|
const stalePattern = /^prototype-\d+\.html$/;
|
|
2189
2154
|
for (const f of readdirSync(repoDir).filter((f2) => stalePattern.test(f2))) {
|
|
2190
2155
|
try {
|
|
2191
|
-
unlinkSync(
|
|
2156
|
+
unlinkSync(resolve2(repoDir, f));
|
|
2192
2157
|
} catch {
|
|
2193
2158
|
}
|
|
2194
2159
|
}
|
|
@@ -2208,42 +2173,48 @@ var watchCommand = new Command8("watch").description(
|
|
|
2208
2173
|
const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, proto.title);
|
|
2209
2174
|
active.set(`proto-${proto.id}`, { process: child, title: proto.title, repoDir });
|
|
2210
2175
|
child.on("exit", async (code) => {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2176
|
+
const key = `proto-${proto.id}`;
|
|
2177
|
+
active.delete(key);
|
|
2178
|
+
finishing.add(key);
|
|
2179
|
+
try {
|
|
2180
|
+
if (code === 0) {
|
|
2181
|
+
try {
|
|
2182
|
+
const protoPattern = /^prototype-\d+\.html$/;
|
|
2183
|
+
const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
|
|
2184
|
+
const files = found.map((f) => ({
|
|
2185
|
+
name: f,
|
|
2186
|
+
content: readFileSync5(resolve2(repoDir, f), "utf-8")
|
|
2187
|
+
}));
|
|
2188
|
+
if (files.length === 0) {
|
|
2189
|
+
logError(prefix, `No prototype HTML files found in ${repoDir}`);
|
|
2190
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
|
|
2194
|
+
logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
|
|
2195
|
+
for (const file of files) {
|
|
2196
|
+
try {
|
|
2197
|
+
unlinkSync(resolve2(repoDir, file.name));
|
|
2198
|
+
} catch {
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
logError(prefix, `Failed to upload prototype: ${err.message}`);
|
|
2229
2203
|
try {
|
|
2230
|
-
|
|
2204
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2231
2205
|
} catch {
|
|
2232
2206
|
}
|
|
2233
2207
|
}
|
|
2234
|
-
}
|
|
2235
|
-
logError(prefix, `
|
|
2208
|
+
} else {
|
|
2209
|
+
logError(prefix, `"${paint("bold", proto.title)}" prototype failed (exit ${code})`);
|
|
2236
2210
|
try {
|
|
2237
2211
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2238
2212
|
} catch {
|
|
2239
2213
|
}
|
|
2240
2214
|
}
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2245
|
-
} catch {
|
|
2246
|
-
}
|
|
2215
|
+
} finally {
|
|
2216
|
+
queued.delete(key);
|
|
2217
|
+
finishing.delete(key);
|
|
2247
2218
|
}
|
|
2248
2219
|
});
|
|
2249
2220
|
}
|
|
@@ -2259,16 +2230,22 @@ var watchCommand = new Command8("watch").description(
|
|
|
2259
2230
|
const child = spawnAgent(agent, workDir, prompt2, prefix, void 0, project.name);
|
|
2260
2231
|
active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir });
|
|
2261
2232
|
child.on("exit", async (code) => {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2233
|
+
const key = `repo-${project.id}`;
|
|
2234
|
+
active.delete(key);
|
|
2235
|
+
finishing.add(key);
|
|
2236
|
+
try {
|
|
2237
|
+
if (code === 0) {
|
|
2238
|
+
logSuccess(prefix, `"${paint("bold", project.name)}" repo creation complete`);
|
|
2239
|
+
} else {
|
|
2240
|
+
logError(prefix, `"${paint("bold", project.name)}" repo creation failed (exit ${code})`);
|
|
2241
|
+
try {
|
|
2242
|
+
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
|
|
2243
|
+
} catch {
|
|
2244
|
+
}
|
|
2271
2245
|
}
|
|
2246
|
+
} finally {
|
|
2247
|
+
queued.delete(key);
|
|
2248
|
+
finishing.delete(key);
|
|
2272
2249
|
}
|
|
2273
2250
|
});
|
|
2274
2251
|
}
|
|
@@ -2278,7 +2255,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2278
2255
|
logDispatch(prefix, `"${paint("bold", idea.title)}"${idea.feedback ? paint("cyan", " (iteration)") : ""} ${paint("gray", repoDir)}`);
|
|
2279
2256
|
for (const f of ["idea-plan.md", "idea-tasks.json", "idea-prototype.html"]) {
|
|
2280
2257
|
try {
|
|
2281
|
-
unlinkSync(
|
|
2258
|
+
unlinkSync(resolve2(repoDir, f));
|
|
2282
2259
|
} catch {
|
|
2283
2260
|
}
|
|
2284
2261
|
}
|
|
@@ -2286,85 +2263,91 @@ var watchCommand = new Command8("watch").description(
|
|
|
2286
2263
|
const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, idea.title);
|
|
2287
2264
|
active.set(`idea-${idea.id}`, { process: child, title: idea.title, repoDir });
|
|
2288
2265
|
child.on("exit", async (code) => {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
let protoHtml;
|
|
2295
|
-
let followUpTasks;
|
|
2296
|
-
const planPath = resolve(repoDir, "idea-plan.md");
|
|
2297
|
-
const tasksPath = resolve(repoDir, "idea-tasks.json");
|
|
2298
|
-
const protoPath = resolve(repoDir, "idea-prototype.html");
|
|
2299
|
-
try {
|
|
2300
|
-
plan = readFileSync5(planPath, "utf-8");
|
|
2301
|
-
} catch {
|
|
2302
|
-
}
|
|
2303
|
-
try {
|
|
2304
|
-
protoHtml = readFileSync5(protoPath, "utf-8");
|
|
2305
|
-
} catch {
|
|
2306
|
-
}
|
|
2266
|
+
const key = `idea-${idea.id}`;
|
|
2267
|
+
active.delete(key);
|
|
2268
|
+
finishing.add(key);
|
|
2269
|
+
try {
|
|
2270
|
+
if (code === 0) {
|
|
2307
2271
|
try {
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2272
|
+
let plan;
|
|
2273
|
+
let protoHtml;
|
|
2274
|
+
let followUpTasks;
|
|
2275
|
+
const planPath = resolve2(repoDir, "idea-plan.md");
|
|
2276
|
+
const tasksPath = resolve2(repoDir, "idea-tasks.json");
|
|
2277
|
+
const protoPath = resolve2(repoDir, "idea-prototype.html");
|
|
2278
|
+
try {
|
|
2279
|
+
plan = readFileSync5(planPath, "utf-8");
|
|
2280
|
+
} catch {
|
|
2312
2281
|
}
|
|
2313
|
-
} catch {
|
|
2314
|
-
}
|
|
2315
|
-
if (!plan && !protoHtml) {
|
|
2316
|
-
logError(prefix, `No output files found in ${repoDir}`);
|
|
2317
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2318
|
-
return;
|
|
2319
|
-
}
|
|
2320
|
-
const updateData = { status: "generated" };
|
|
2321
|
-
if (plan) updateData.plan = plan;
|
|
2322
|
-
if (followUpTasks) updateData.followUpTasks = followUpTasks;
|
|
2323
|
-
if (protoHtml) {
|
|
2324
2282
|
try {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2283
|
+
protoHtml = readFileSync5(protoPath, "utf-8");
|
|
2284
|
+
} catch {
|
|
2285
|
+
}
|
|
2286
|
+
try {
|
|
2287
|
+
const tasksRaw = readFileSync5(tasksPath, "utf-8");
|
|
2288
|
+
const parsed = JSON.parse(tasksRaw);
|
|
2289
|
+
if (Array.isArray(parsed)) {
|
|
2290
|
+
followUpTasks = parsed.filter((t) => t && typeof t === "object" && "title" in t);
|
|
2291
|
+
}
|
|
2292
|
+
} catch {
|
|
2293
|
+
}
|
|
2294
|
+
if (!plan && !protoHtml) {
|
|
2295
|
+
logError(prefix, `No output files found in ${repoDir}`);
|
|
2296
|
+
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
const updateData = { status: "generated" };
|
|
2300
|
+
if (plan) updateData.plan = plan;
|
|
2301
|
+
if (followUpTasks) updateData.followUpTasks = followUpTasks;
|
|
2302
|
+
if (protoHtml) {
|
|
2303
|
+
try {
|
|
2304
|
+
const proto = await api.post("/api/prototypes", {
|
|
2305
|
+
title: `${idea.title} \u2014 Idea Prototype`,
|
|
2306
|
+
prompt: idea.description || idea.title,
|
|
2307
|
+
variantCount: 1,
|
|
2308
|
+
projectId: idea.projectId ?? null
|
|
2309
|
+
});
|
|
2310
|
+
await api.patch(`/api/prototypes/${proto.id}`, {
|
|
2311
|
+
status: "completed",
|
|
2312
|
+
files: [{ name: "idea-prototype.html", content: protoHtml }]
|
|
2313
|
+
});
|
|
2314
|
+
updateData.generatedPrototypeId = proto.id;
|
|
2315
|
+
logSuccess(prefix, `Prototype created: ${paint("gray", proto.id.slice(0, 8))}`);
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
logError(prefix, `Failed to create prototype: ${err.message}`);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
await api.patch(`/api/ideas/${idea.id}`, updateData);
|
|
2321
|
+
logSuccess(prefix, `"${paint("bold", idea.title)}" generation complete`);
|
|
2322
|
+
try {
|
|
2323
|
+
unlinkSync(planPath);
|
|
2324
|
+
} catch {
|
|
2325
|
+
}
|
|
2326
|
+
try {
|
|
2327
|
+
unlinkSync(tasksPath);
|
|
2328
|
+
} catch {
|
|
2329
|
+
}
|
|
2330
|
+
try {
|
|
2331
|
+
unlinkSync(protoPath);
|
|
2332
|
+
} catch {
|
|
2333
|
+
}
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
logError(prefix, `Failed to upload idea output: ${err.message}`);
|
|
2336
|
+
try {
|
|
2337
|
+
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2338
|
+
} catch {
|
|
2339
2339
|
}
|
|
2340
2340
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
try {
|
|
2344
|
-
unlinkSync(planPath);
|
|
2345
|
-
} catch {
|
|
2346
|
-
}
|
|
2347
|
-
try {
|
|
2348
|
-
unlinkSync(tasksPath);
|
|
2349
|
-
} catch {
|
|
2350
|
-
}
|
|
2351
|
-
try {
|
|
2352
|
-
unlinkSync(protoPath);
|
|
2353
|
-
} catch {
|
|
2354
|
-
}
|
|
2355
|
-
} catch (err) {
|
|
2356
|
-
logError(prefix, `Failed to upload idea output: ${err.message}`);
|
|
2341
|
+
} else {
|
|
2342
|
+
logError(prefix, `"${paint("bold", idea.title)}" generation failed (exit ${code})`);
|
|
2357
2343
|
try {
|
|
2358
2344
|
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2359
2345
|
} catch {
|
|
2360
2346
|
}
|
|
2361
2347
|
}
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2366
|
-
} catch {
|
|
2367
|
-
}
|
|
2348
|
+
} finally {
|
|
2349
|
+
queued.delete(key);
|
|
2350
|
+
finishing.delete(key);
|
|
2368
2351
|
}
|
|
2369
2352
|
});
|
|
2370
2353
|
}
|
|
@@ -2443,329 +2426,371 @@ ${divider}`);
|
|
|
2443
2426
|
}
|
|
2444
2427
|
}
|
|
2445
2428
|
async function poll() {
|
|
2446
|
-
|
|
2447
|
-
|
|
2429
|
+
if (pollRunning) return;
|
|
2430
|
+
pollRunning = true;
|
|
2448
2431
|
try {
|
|
2449
|
-
queuedTasks
|
|
2450
|
-
delegatedTasks
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
const tasks = [...nonTestQueued, ...nonTestDelegated];
|
|
2458
|
-
const config = loadConfig();
|
|
2459
|
-
const activeTaskIds = new Set(tasks.map((t) => t.id));
|
|
2460
|
-
for (const [taskId, entry] of active) {
|
|
2461
|
-
if (taskId.startsWith("proto-") || taskId.startsWith("repo-") || taskId.startsWith("scan-")) continue;
|
|
2462
|
-
if (!activeTaskIds.has(taskId)) {
|
|
2463
|
-
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer active, terminating\u2026`);
|
|
2464
|
-
entry.process.kill("SIGTERM");
|
|
2465
|
-
active.delete(taskId);
|
|
2466
|
-
queued.delete(taskId);
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "idea-", "test-"];
|
|
2470
|
-
for (const taskId of failed.keys()) {
|
|
2471
|
-
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2472
|
-
if (!activeTaskIds.has(taskId)) failed.delete(taskId);
|
|
2473
|
-
}
|
|
2474
|
-
for (const taskId of queued) {
|
|
2475
|
-
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2476
|
-
if (!activeTaskIds.has(taskId)) queued.delete(taskId);
|
|
2477
|
-
}
|
|
2478
|
-
for (const task of tasks) {
|
|
2479
|
-
if (queued.has(task.id)) continue;
|
|
2480
|
-
if (failed.has(task.id)) continue;
|
|
2481
|
-
const sid = shortId(task.id);
|
|
2482
|
-
const prefix = taskTag(sid);
|
|
2483
|
-
const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
|
|
2484
|
-
if (!repoDir) {
|
|
2485
|
-
const reason = `no linked directory found under ${rootDir}`;
|
|
2486
|
-
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
2487
|
-
failed.set(task.id, reason);
|
|
2488
|
-
continue;
|
|
2489
|
-
}
|
|
2490
|
-
if (!existsSync6(repoDir)) {
|
|
2491
|
-
const reason = `linked directory "${repoDir}" does not exist`;
|
|
2492
|
-
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
2493
|
-
failed.set(task.id, reason);
|
|
2494
|
-
continue;
|
|
2432
|
+
let queuedTasks;
|
|
2433
|
+
let delegatedTasks;
|
|
2434
|
+
try {
|
|
2435
|
+
queuedTasks = await api.get("/api/tasks?status=queued");
|
|
2436
|
+
delegatedTasks = await api.get("/api/tasks?status=delegated");
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
logError(watchTag(), `Failed to fetch tasks: ${err.message}`);
|
|
2439
|
+
return;
|
|
2495
2440
|
}
|
|
2496
|
-
|
|
2441
|
+
const nonTestQueued = queuedTasks.filter((t) => t.mode !== "testing");
|
|
2442
|
+
const nonTestDelegated = delegatedTasks.filter((t) => t.mode !== "testing");
|
|
2443
|
+
const tasks = [...nonTestQueued, ...nonTestDelegated];
|
|
2444
|
+
const config = loadConfig();
|
|
2445
|
+
const activeTaskIds = new Set(tasks.map((t) => t.id));
|
|
2446
|
+
for (const task of tasks) {
|
|
2447
|
+
if (task.status !== "delegated") continue;
|
|
2448
|
+
const sid = shortId(task.id);
|
|
2449
|
+
const prefix = taskTag(sid);
|
|
2450
|
+
const activeEntry = active.get(task.id);
|
|
2451
|
+
const idleMs = activeEntry ? Date.now() - activeEntry.lastActivityAt : null;
|
|
2452
|
+
const delegatedAtMs = task.inProgressSince ? Date.now() - new Date(task.inProgressSince).getTime() : null;
|
|
2453
|
+
const exceededIdleTimeout = idleMs !== null && idleMs >= taskStallTimeoutMs;
|
|
2454
|
+
const exceededOrphanTimeout = !activeEntry && delegatedAtMs !== null && delegatedAtMs >= taskStallTimeoutMs;
|
|
2455
|
+
if (!exceededIdleTimeout && !exceededOrphanTimeout) continue;
|
|
2456
|
+
const timeoutLabel = formatTimeoutMinutes(taskStallTimeoutMs);
|
|
2457
|
+
const reason = activeEntry ? `Agent process became idle for more than ${timeoutLabel}` : `Task remained delegated without an active watch process for more than ${timeoutLabel}`;
|
|
2458
|
+
logError(prefix, `"${task.title}" marked as error: ${reason}`);
|
|
2459
|
+
if (activeEntry) {
|
|
2460
|
+
activeEntry.terminatedForError = true;
|
|
2461
|
+
}
|
|
2497
2462
|
try {
|
|
2498
|
-
await api.patch(`/api/tasks/${task.id}`, { status: "
|
|
2463
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "error" });
|
|
2464
|
+
await postTaskUpdate(task.id, `Task moved to error \u2014 ${reason}`, "system");
|
|
2499
2465
|
} catch (err) {
|
|
2500
|
-
logError(prefix, `Failed to mark task as
|
|
2466
|
+
logError(prefix, `Failed to mark task as error: ${err.message}`);
|
|
2501
2467
|
continue;
|
|
2502
2468
|
}
|
|
2469
|
+
failed.set(task.id, reason);
|
|
2470
|
+
queued.delete(task.id);
|
|
2471
|
+
if (activeEntry) {
|
|
2472
|
+
activeEntry.process.kill("SIGTERM");
|
|
2473
|
+
}
|
|
2503
2474
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
watchTag(),
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2475
|
+
for (const [taskId, entry] of active) {
|
|
2476
|
+
if (taskId.startsWith("proto-") || taskId.startsWith("repo-") || taskId.startsWith("scan-")) continue;
|
|
2477
|
+
if (!activeTaskIds.has(taskId)) {
|
|
2478
|
+
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer active, terminating\u2026`);
|
|
2479
|
+
entry.process.kill("SIGTERM");
|
|
2480
|
+
active.delete(taskId);
|
|
2481
|
+
queued.delete(taskId);
|
|
2482
|
+
}
|
|
2511
2483
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
processApprovalQueue();
|
|
2517
|
-
} else {
|
|
2518
|
-
dispatchTask(task, repoDir);
|
|
2484
|
+
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "idea-", "test-"];
|
|
2485
|
+
for (const taskId of failed.keys()) {
|
|
2486
|
+
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2487
|
+
if (!activeTaskIds.has(taskId)) failed.delete(taskId);
|
|
2519
2488
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
} catch (err) {
|
|
2525
|
-
logError(watchTag(), `Failed to fetch prototypes: ${err.message}`);
|
|
2526
|
-
}
|
|
2527
|
-
const inProgressProtoKeys = new Set(prototypes.map((p) => `proto-${p.id}`));
|
|
2528
|
-
for (const [key, entry] of active) {
|
|
2529
|
-
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key)) {
|
|
2530
|
-
logWarn(watchTag(), `Prototype ${paint("yellow", key)} no longer in_progress, terminating\u2026`);
|
|
2531
|
-
entry.process.kill("SIGTERM");
|
|
2532
|
-
active.delete(key);
|
|
2533
|
-
queued.delete(key);
|
|
2489
|
+
for (const taskId of queued) {
|
|
2490
|
+
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2491
|
+
if (finishing.has(taskId)) continue;
|
|
2492
|
+
if (!activeTaskIds.has(taskId)) queued.delete(taskId);
|
|
2534
2493
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2494
|
+
for (const task of tasks) {
|
|
2495
|
+
if (queued.has(task.id)) continue;
|
|
2496
|
+
if (finishing.has(task.id)) continue;
|
|
2497
|
+
if (failed.has(task.id)) continue;
|
|
2498
|
+
const sid = shortId(task.id);
|
|
2499
|
+
const prefix = taskTag(sid);
|
|
2500
|
+
const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
|
|
2501
|
+
if (!repoDir) {
|
|
2502
|
+
const reason = `no linked directory found under ${rootDir}`;
|
|
2503
|
+
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
2504
|
+
failed.set(task.id, reason);
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
if (!existsSync7(repoDir)) {
|
|
2508
|
+
const reason = `linked directory "${repoDir}" does not exist`;
|
|
2509
|
+
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
2510
|
+
failed.set(task.id, reason);
|
|
2511
|
+
continue;
|
|
2512
|
+
}
|
|
2513
|
+
if (task.status === "queued") {
|
|
2514
|
+
try {
|
|
2515
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "delegated" });
|
|
2516
|
+
} catch (err) {
|
|
2517
|
+
logError(prefix, `Failed to mark task as delegated: ${err.message}`);
|
|
2518
|
+
continue;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
queued.add(task.id);
|
|
2522
|
+
if (dryRun) {
|
|
2523
|
+
logInfo(
|
|
2524
|
+
watchTag(),
|
|
2525
|
+
`${paint("yellow", "[dry-run]")} would dispatch "${paint("bold", task.title)}" (${paint("gray", task.id)}) in ${paint("cyan", repoDir)}`
|
|
2526
|
+
);
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
if (task.mode === "planning") {
|
|
2530
|
+
dispatchPlanModeTask(task, repoDir);
|
|
2531
|
+
} else if (planApproval) {
|
|
2532
|
+
approvalQueue.push({ task, repoDir });
|
|
2533
|
+
processApprovalQueue();
|
|
2534
|
+
} else {
|
|
2535
|
+
dispatchTask(task, repoDir);
|
|
2536
|
+
}
|
|
2554
2537
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2538
|
+
let prototypes = [];
|
|
2539
|
+
try {
|
|
2540
|
+
prototypes = await api.get("/api/prototypes?status=in_progress");
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
logError(watchTag(), `Failed to fetch prototypes: ${err.message}`);
|
|
2559
2543
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2544
|
+
const inProgressProtoKeys = new Set(prototypes.map((p) => `proto-${p.id}`));
|
|
2545
|
+
for (const [key, entry] of active) {
|
|
2546
|
+
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key)) {
|
|
2547
|
+
logWarn(watchTag(), `Prototype ${paint("yellow", key)} no longer in_progress, terminating\u2026`);
|
|
2548
|
+
entry.process.kill("SIGTERM");
|
|
2549
|
+
active.delete(key);
|
|
2550
|
+
queued.delete(key);
|
|
2551
|
+
}
|
|
2564
2552
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
if (dryRun) {
|
|
2568
|
-
logInfo(
|
|
2569
|
-
watchTag(),
|
|
2570
|
-
`${paint("yellow", "[dry-run]")} would dispatch prototype "${paint("bold", proto.title)}" in ${paint("cyan", repoDir)}`
|
|
2571
|
-
);
|
|
2572
|
-
continue;
|
|
2553
|
+
for (const key of failed.keys()) {
|
|
2554
|
+
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key)) failed.delete(key);
|
|
2573
2555
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
const testTasks = queuedTasks.filter((t) => t.mode === "testing");
|
|
2577
|
-
for (const task of testTasks) {
|
|
2578
|
-
const key = `test-${task.id}`;
|
|
2579
|
-
if (queued.has(key)) continue;
|
|
2580
|
-
if (failed.has(key)) continue;
|
|
2581
|
-
const sid = shortId(task.id);
|
|
2582
|
-
const prefix = testTag(sid);
|
|
2583
|
-
if (!task.link) {
|
|
2584
|
-
logWarn(prefix, `"${task.title}": no MR/PR link \u2014 skipping`);
|
|
2585
|
-
continue;
|
|
2556
|
+
for (const key of queued) {
|
|
2557
|
+
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key) && !finishing.has(key)) queued.delete(key);
|
|
2586
2558
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
);
|
|
2593
|
-
continue;
|
|
2559
|
+
const activeProtoCount = [...active.keys()].filter((k) => k.startsWith("proto-")).length;
|
|
2560
|
+
let protoSlots = Math.max(0, 2 - activeProtoCount);
|
|
2561
|
+
for (const proto of prototypes) {
|
|
2562
|
+
if (protoSlots <= 0) break;
|
|
2563
|
+
const key = `proto-${proto.id}`;
|
|
2564
|
+
if (queued.has(key)) continue;
|
|
2565
|
+
if (finishing.has(key)) continue;
|
|
2566
|
+
if (failed.has(key)) continue;
|
|
2567
|
+
const sid = shortId(proto.id);
|
|
2568
|
+
const prefix = protoTag(sid);
|
|
2569
|
+
if (!proto.projectId) {
|
|
2570
|
+
logWarn(prefix, `"${proto.title}": no projectId \u2014 skipping`);
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
const repoDir = findDirectoryForProject(config, proto.projectId, rootDir);
|
|
2574
|
+
if (!repoDir) {
|
|
2575
|
+
logWarn(prefix, `"${proto.title}": no linked directory found \u2014 skipping`);
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
if (!existsSync7(repoDir)) {
|
|
2579
|
+
logError(prefix, `"${proto.title}": linked directory "${repoDir}" does not exist \u2014 skipping`);
|
|
2580
|
+
failed.set(key, `directory does not exist: ${repoDir}`);
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
queued.add(key);
|
|
2584
|
+
protoSlots--;
|
|
2585
|
+
if (dryRun) {
|
|
2586
|
+
logInfo(
|
|
2587
|
+
watchTag(),
|
|
2588
|
+
`${paint("yellow", "[dry-run]")} would dispatch prototype "${paint("bold", proto.title)}" in ${paint("cyan", repoDir)}`
|
|
2589
|
+
);
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
dispatchPrototypeJob(proto, repoDir);
|
|
2594
2593
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2594
|
+
const testTasks = queuedTasks.filter((t) => t.mode === "testing");
|
|
2595
|
+
for (const task of testTasks) {
|
|
2596
|
+
const key = `test-${task.id}`;
|
|
2597
|
+
if (queued.has(key)) continue;
|
|
2598
|
+
if (finishing.has(key)) continue;
|
|
2599
|
+
if (failed.has(key)) continue;
|
|
2600
|
+
const sid = shortId(task.id);
|
|
2601
|
+
const prefix = testTag(sid);
|
|
2602
|
+
if (!task.link) {
|
|
2603
|
+
logWarn(prefix, `"${task.title}": no MR/PR link \u2014 skipping`);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
queued.add(key);
|
|
2607
|
+
if (dryRun) {
|
|
2608
|
+
logInfo(
|
|
2609
|
+
watchTag(),
|
|
2610
|
+
`${paint("yellow", "[dry-run]")} would run test for "${paint("bold", task.title)}"`
|
|
2611
|
+
);
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
(async () => {
|
|
2615
|
+
logDispatch(prefix, `Running test for "${paint("bold", task.title)}"\u2026`);
|
|
2614
2616
|
try {
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2617
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "delegated" });
|
|
2618
|
+
await postTaskUpdate(task.id, "Test started \u2014 setting up environment\u2026");
|
|
2619
|
+
let localPath;
|
|
2620
|
+
try {
|
|
2621
|
+
const project = await api.get(`/api/projects/${task.projectId}`);
|
|
2622
|
+
localPath = project.localPath ?? void 0;
|
|
2623
|
+
} catch {
|
|
2620
2624
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
const formData = new FormData();
|
|
2636
|
-
const blob = new Blob([imageBuffer], { type: "image/png" });
|
|
2637
|
-
formData.append("file", blob, fileName);
|
|
2638
|
-
formData.append("prefix", "test-screenshots");
|
|
2639
|
-
const uploadRes = await fetch(`${cfg.apiUrl}/api/upload`, {
|
|
2640
|
-
method: "POST",
|
|
2641
|
-
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
2642
|
-
body: formData
|
|
2643
|
-
});
|
|
2644
|
-
if (!uploadRes.ok) return null;
|
|
2645
|
-
const uploadData = await uploadRes.json();
|
|
2646
|
-
await api.post(`/api/tasks/${task.id}/updates`, {
|
|
2647
|
-
message,
|
|
2648
|
-
imageUrl: uploadData.url,
|
|
2649
|
-
source: "system"
|
|
2650
|
-
});
|
|
2651
|
-
return uploadData.url;
|
|
2652
|
-
} catch {
|
|
2653
|
-
return null;
|
|
2625
|
+
if (!localPath) {
|
|
2626
|
+
const cfg = loadConfig();
|
|
2627
|
+
localPath = findDirectoryForProject(cfg, task.projectId, rootDir) ?? void 0;
|
|
2628
|
+
}
|
|
2629
|
+
if (!localPath) {
|
|
2630
|
+
throw new Error("Local repo path not configured. Run `mr link` in the project directory.");
|
|
2631
|
+
}
|
|
2632
|
+
let testPlan;
|
|
2633
|
+
try {
|
|
2634
|
+
const resources = await api.get(`/api/tasks/${task.id}/resources`);
|
|
2635
|
+
const planResource = resources.find((r) => r.type === "test-plan");
|
|
2636
|
+
if (planResource) {
|
|
2637
|
+
testPlan = JSON.parse(planResource.content);
|
|
2638
|
+
logInfo(prefix, `Using agent-authored test plan (${testPlan.length} steps)`);
|
|
2654
2639
|
}
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2640
|
+
} catch {
|
|
2641
|
+
}
|
|
2642
|
+
const result = await runTest({
|
|
2643
|
+
taskId: task.id,
|
|
2644
|
+
taskLink: task.link,
|
|
2645
|
+
localPath,
|
|
2646
|
+
testPlan,
|
|
2647
|
+
browseRunner: runBrowseCommand2,
|
|
2648
|
+
uploadScreenshot: async (screenshotPath, message) => {
|
|
2649
|
+
try {
|
|
2650
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
2651
|
+
const cfg = loadConfig();
|
|
2652
|
+
const imageBuffer = readFileSync12(screenshotPath);
|
|
2653
|
+
const fileName = screenshotPath.split("/").pop() || "test-screenshot.png";
|
|
2654
|
+
const formData = new FormData();
|
|
2655
|
+
const blob = new Blob([imageBuffer], { type: "image/png" });
|
|
2656
|
+
formData.append("file", blob, fileName);
|
|
2657
|
+
formData.append("prefix", "test-screenshots");
|
|
2658
|
+
const uploadRes = await fetch(`${cfg.apiUrl}/api/upload`, {
|
|
2659
|
+
method: "POST",
|
|
2660
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
2661
|
+
body: formData
|
|
2662
|
+
});
|
|
2663
|
+
if (!uploadRes.ok) return null;
|
|
2664
|
+
const uploadData = await uploadRes.json();
|
|
2665
|
+
await api.post(`/api/tasks/${task.id}/updates`, {
|
|
2666
|
+
message,
|
|
2667
|
+
imageUrl: uploadData.url,
|
|
2668
|
+
source: "system"
|
|
2669
|
+
});
|
|
2670
|
+
return uploadData.url;
|
|
2671
|
+
} catch {
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
},
|
|
2675
|
+
postUpdate: async (message, imageUrl) => {
|
|
2676
|
+
await postTaskUpdate(task.id, message);
|
|
2677
|
+
},
|
|
2678
|
+
onProgress: (msg) => logInfo(prefix, msg)
|
|
2679
|
+
});
|
|
2680
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: result.status });
|
|
2681
|
+
logSuccess(prefix, result.summary);
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
logError(prefix, `Test failed: ${err.message}`);
|
|
2684
|
+
try {
|
|
2685
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: "failed" });
|
|
2686
|
+
await postTaskUpdate(task.id, `Test failed: ${err.message}`);
|
|
2687
|
+
} catch {
|
|
2688
|
+
}
|
|
2689
|
+
failed.set(key, err.message);
|
|
2690
|
+
} finally {
|
|
2691
|
+
queued.delete(key);
|
|
2669
2692
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2693
|
+
})();
|
|
2694
|
+
}
|
|
2695
|
+
let pendingProjects = [];
|
|
2696
|
+
try {
|
|
2697
|
+
pendingProjects = await api.get("/api/projects?repoCreationStatus=pending");
|
|
2698
|
+
} catch (err) {
|
|
2699
|
+
logError(watchTag(), `Failed to fetch pending projects: ${err.message}`);
|
|
2700
|
+
}
|
|
2701
|
+
for (const project of pendingProjects) {
|
|
2702
|
+
const key = `repo-${project.id}`;
|
|
2703
|
+
if (queued.has(key)) continue;
|
|
2704
|
+
if (finishing.has(key)) continue;
|
|
2705
|
+
if (failed.has(key)) continue;
|
|
2706
|
+
queued.add(key);
|
|
2707
|
+
if (dryRun) {
|
|
2708
|
+
logInfo(
|
|
2709
|
+
watchTag(),
|
|
2710
|
+
`${paint("yellow", "[dry-run]")} would create repo for "${paint("bold", project.name)}" in ${paint("cyan", rootDir)}`
|
|
2711
|
+
);
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
dispatchRepoCreation(project, rootDir);
|
|
2715
|
+
}
|
|
2716
|
+
let generatingIdeas = [];
|
|
2717
|
+
try {
|
|
2718
|
+
generatingIdeas = await api.get("/api/ideas?status=generating");
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
logError(watchTag(), `Failed to fetch generating ideas: ${err.message}`);
|
|
2721
|
+
}
|
|
2722
|
+
const inProgressIdeaKeys = new Set(generatingIdeas.map((i) => `idea-${i.id}`));
|
|
2723
|
+
for (const [key, entry] of active) {
|
|
2724
|
+
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key)) {
|
|
2725
|
+
logWarn(watchTag(), `Idea ${paint("yellow", key)} no longer generating, terminating\u2026`);
|
|
2726
|
+
entry.process.kill("SIGTERM");
|
|
2727
|
+
active.delete(key);
|
|
2672
2728
|
queued.delete(key);
|
|
2673
2729
|
}
|
|
2674
|
-
})();
|
|
2675
|
-
}
|
|
2676
|
-
let pendingProjects = [];
|
|
2677
|
-
try {
|
|
2678
|
-
pendingProjects = await api.get("/api/projects?repoCreationStatus=pending");
|
|
2679
|
-
} catch (err) {
|
|
2680
|
-
logError(watchTag(), `Failed to fetch pending projects: ${err.message}`);
|
|
2681
|
-
}
|
|
2682
|
-
for (const project of pendingProjects) {
|
|
2683
|
-
const key = `repo-${project.id}`;
|
|
2684
|
-
if (queued.has(key)) continue;
|
|
2685
|
-
if (failed.has(key)) continue;
|
|
2686
|
-
queued.add(key);
|
|
2687
|
-
if (dryRun) {
|
|
2688
|
-
logInfo(
|
|
2689
|
-
watchTag(),
|
|
2690
|
-
`${paint("yellow", "[dry-run]")} would create repo for "${paint("bold", project.name)}" in ${paint("cyan", rootDir)}`
|
|
2691
|
-
);
|
|
2692
|
-
continue;
|
|
2693
2730
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
let generatingIdeas = [];
|
|
2697
|
-
try {
|
|
2698
|
-
generatingIdeas = await api.get("/api/ideas?status=generating");
|
|
2699
|
-
} catch (err) {
|
|
2700
|
-
logError(watchTag(), `Failed to fetch generating ideas: ${err.message}`);
|
|
2701
|
-
}
|
|
2702
|
-
const inProgressIdeaKeys = new Set(generatingIdeas.map((i) => `idea-${i.id}`));
|
|
2703
|
-
for (const [key, entry] of active) {
|
|
2704
|
-
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key)) {
|
|
2705
|
-
logWarn(watchTag(), `Idea ${paint("yellow", key)} no longer generating, terminating\u2026`);
|
|
2706
|
-
entry.process.kill("SIGTERM");
|
|
2707
|
-
active.delete(key);
|
|
2708
|
-
queued.delete(key);
|
|
2731
|
+
for (const key of failed.keys()) {
|
|
2732
|
+
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key)) failed.delete(key);
|
|
2709
2733
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2734
|
+
for (const key of queued) {
|
|
2735
|
+
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key) && !finishing.has(key)) queued.delete(key);
|
|
2736
|
+
}
|
|
2737
|
+
const activeIdeaCount = [...active.keys()].filter((k) => k.startsWith("idea-")).length;
|
|
2738
|
+
let ideaSlots = Math.max(0, 1 - activeIdeaCount);
|
|
2739
|
+
for (const idea of generatingIdeas) {
|
|
2740
|
+
if (ideaSlots <= 0) break;
|
|
2741
|
+
const key = `idea-${idea.id}`;
|
|
2742
|
+
if (queued.has(key)) continue;
|
|
2743
|
+
if (finishing.has(key)) continue;
|
|
2744
|
+
if (failed.has(key)) continue;
|
|
2745
|
+
const sid = shortId(idea.id);
|
|
2746
|
+
const prefix = ideaTag(sid);
|
|
2747
|
+
let repoDir;
|
|
2748
|
+
if (idea.projectId) {
|
|
2749
|
+
const dir = findDirectoryForProject(config, idea.projectId, rootDir);
|
|
2750
|
+
if (!dir) {
|
|
2751
|
+
logWarn(prefix, `"${idea.title}": no linked directory found \u2014 skipping`);
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
repoDir = dir;
|
|
2755
|
+
} else {
|
|
2756
|
+
repoDir = rootDir;
|
|
2757
|
+
}
|
|
2758
|
+
queued.add(key);
|
|
2759
|
+
ideaSlots--;
|
|
2760
|
+
if (dryRun) {
|
|
2761
|
+
logInfo(
|
|
2762
|
+
watchTag(),
|
|
2763
|
+
`${paint("yellow", "[dry-run]")} would dispatch idea "${paint("bold", idea.title)}" in ${paint("cyan", repoDir)}`
|
|
2764
|
+
);
|
|
2731
2765
|
continue;
|
|
2732
2766
|
}
|
|
2733
|
-
repoDir
|
|
2734
|
-
} else {
|
|
2735
|
-
repoDir = rootDir;
|
|
2767
|
+
dispatchIdeaJob(idea, repoDir);
|
|
2736
2768
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
`${paint("yellow", "[dry-run]")} would dispatch idea "${paint("bold", idea.title)}" in ${paint("cyan", repoDir)}`
|
|
2743
|
-
);
|
|
2744
|
-
continue;
|
|
2769
|
+
let pendingScans = [];
|
|
2770
|
+
try {
|
|
2771
|
+
pendingScans = await api.get("/api/scans?status=pending&limit=5");
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
logError(watchTag(), `Failed to fetch pending scans: ${err.message}`);
|
|
2745
2774
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
logInfo(
|
|
2763
|
-
watchTag(),
|
|
2764
|
-
`${paint("yellow", "[dry-run]")} would run scan ${paint("yellow", sid)} for project ${paint("cyan", scan.projectId)}`
|
|
2765
|
-
);
|
|
2766
|
-
continue;
|
|
2775
|
+
for (const scan of pendingScans) {
|
|
2776
|
+
const key = `scan-${scan.id}`;
|
|
2777
|
+
if (queued.has(key)) continue;
|
|
2778
|
+
if (finishing.has(key)) continue;
|
|
2779
|
+
if (failed.has(key)) continue;
|
|
2780
|
+
const sid = shortId(scan.id);
|
|
2781
|
+
const prefix = `${paint("magenta", `[scan:${sid}]`)}`;
|
|
2782
|
+
queued.add(key);
|
|
2783
|
+
if (dryRun) {
|
|
2784
|
+
logInfo(
|
|
2785
|
+
watchTag(),
|
|
2786
|
+
`${paint("yellow", "[dry-run]")} would run scan ${paint("yellow", sid)} for project ${paint("cyan", scan.projectId)}`
|
|
2787
|
+
);
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
dispatchScan(scan, prefix, key);
|
|
2767
2791
|
}
|
|
2768
|
-
|
|
2792
|
+
} finally {
|
|
2793
|
+
pollRunning = false;
|
|
2769
2794
|
}
|
|
2770
2795
|
}
|
|
2771
2796
|
async function shutdown() {
|
|
@@ -2944,9 +2969,8 @@ var subtaskCompleteCommand = new Command12("subtask-complete").description("Mark
|
|
|
2944
2969
|
console.log(`\u2713 Subtask completed: ${subtask.title}`);
|
|
2945
2970
|
});
|
|
2946
2971
|
|
|
2947
|
-
// cli/commands/
|
|
2972
|
+
// cli/commands/prototype.ts
|
|
2948
2973
|
import { Command as Command13 } from "commander";
|
|
2949
|
-
import { spawn as spawn5 } from "child_process";
|
|
2950
2974
|
var c4 = {
|
|
2951
2975
|
reset: "\x1B[0m",
|
|
2952
2976
|
bold: "\x1B[1m",
|
|
@@ -2955,360 +2979,28 @@ var c4 = {
|
|
|
2955
2979
|
green: "\x1B[32m",
|
|
2956
2980
|
yellow: "\x1B[33m",
|
|
2957
2981
|
red: "\x1B[31m",
|
|
2958
|
-
|
|
2982
|
+
blue: "\x1B[34m",
|
|
2959
2983
|
gray: "\x1B[90m"
|
|
2960
2984
|
};
|
|
2961
2985
|
function paint4(color, text) {
|
|
2962
2986
|
return `${c4[color]}${text}${c4.reset}`;
|
|
2963
2987
|
}
|
|
2964
|
-
function timestamp2() {
|
|
2965
|
-
return paint4("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
2966
|
-
}
|
|
2967
|
-
function digestTag() {
|
|
2968
|
-
return paint4("magenta", "[digest]");
|
|
2969
|
-
}
|
|
2970
|
-
function shortId2(id) {
|
|
2971
|
-
return id.slice(0, 8);
|
|
2972
|
-
}
|
|
2973
|
-
function log(msg) {
|
|
2974
|
-
console.log(`${timestamp2()} ${digestTag()} ${msg}`);
|
|
2975
|
-
}
|
|
2976
|
-
function logOk(msg) {
|
|
2977
|
-
console.log(`${timestamp2()} ${digestTag()} ${paint4("green", "\u2713")} ${msg}`);
|
|
2978
|
-
}
|
|
2979
|
-
function logErr(msg) {
|
|
2980
|
-
console.error(`${timestamp2()} ${digestTag()} ${paint4("red", "\u2717")} ${msg}`);
|
|
2981
|
-
}
|
|
2982
|
-
function buildPrompt(messages) {
|
|
2983
|
-
const emailsText = messages.map(
|
|
2984
|
-
(msg, i) => `Email ${i + 1}:
|
|
2985
|
-
ID: ${msg.id}
|
|
2986
|
-
Subject: ${msg.subject}
|
|
2987
|
-
From: ${msg.sender} <${msg.senderEmail}>
|
|
2988
|
-
Date: ${msg.date}
|
|
2989
|
-
Snippet: ${msg.snippet}
|
|
2990
|
-
Body preview: ${msg.body.slice(0, 500)}`
|
|
2991
|
-
).join("\n\n---\n\n");
|
|
2992
|
-
return `You are an email triage assistant. Analyze the following unread emails and produce a structured digest.
|
|
2993
|
-
|
|
2994
|
-
For each email, determine:
|
|
2995
|
-
1. Is it relevant/actionable (requires attention, is personal, work-related, or important)?
|
|
2996
|
-
2. Or is it not relevant (newsletters, marketing, automated notifications, promotions)?
|
|
2997
|
-
3. Assign a category that groups it with similar emails (e.g. "Finance & Trading", "Real Estate", "Work", "Personal", "Promotions & Newsletters", "Travel", "Health", etc.).
|
|
2998
|
-
4. Write a 2-3 sentence summary of the email's content and what action (if any) is needed.
|
|
2999
|
-
|
|
3000
|
-
Also write a 2-3 sentence executive summary of the overall inbox state.
|
|
3001
|
-
|
|
3002
|
-
Here are the unread emails:
|
|
3003
|
-
|
|
3004
|
-
${emailsText}
|
|
3005
|
-
|
|
3006
|
-
Respond with a JSON object in this exact format:
|
|
3007
|
-
{
|
|
3008
|
-
"summary": "2-3 sentence overview of the inbox",
|
|
3009
|
-
"items": [
|
|
3010
|
-
{
|
|
3011
|
-
"emailId": "the email ID",
|
|
3012
|
-
"summary": "2-3 sentence summary of this email's content and any action needed",
|
|
3013
|
-
"isRelevant": true or false,
|
|
3014
|
-
"relevanceReason": "brief explanation of why relevant or not",
|
|
3015
|
-
"category": "short category label (2-4 words)"
|
|
3016
|
-
}
|
|
3017
|
-
]
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
Be conservative with "isRelevant: false" \u2014 only mark as not relevant if clearly promotional, newsletter, or automated. Personal emails, work emails, and anything requiring a response should be relevant. Group similar emails under the same category label.`;
|
|
3021
|
-
}
|
|
3022
|
-
function runAgent(prompt2, agent) {
|
|
3023
|
-
return new Promise((resolve7, reject) => {
|
|
3024
|
-
let bin;
|
|
3025
|
-
let args;
|
|
3026
|
-
if (agent === "codex") {
|
|
3027
|
-
bin = "codex";
|
|
3028
|
-
args = ["exec", "--full-auto", prompt2];
|
|
3029
|
-
} else {
|
|
3030
|
-
bin = "claude";
|
|
3031
|
-
args = ["-p", "--dangerously-skip-permissions", prompt2];
|
|
3032
|
-
}
|
|
3033
|
-
const child = spawn5(bin, args, {
|
|
3034
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3035
|
-
});
|
|
3036
|
-
let output = "";
|
|
3037
|
-
let errOutput = "";
|
|
3038
|
-
child.stdout?.on("data", (d) => {
|
|
3039
|
-
output += d.toString();
|
|
3040
|
-
});
|
|
3041
|
-
child.stderr?.on("data", (d) => {
|
|
3042
|
-
errOutput += d.toString();
|
|
3043
|
-
});
|
|
3044
|
-
child.on("exit", (code) => {
|
|
3045
|
-
if (code === 0) resolve7(output.trim());
|
|
3046
|
-
else reject(new Error(`${bin} exited with code ${code}
|
|
3047
|
-
${errOutput.trim()}`));
|
|
3048
|
-
});
|
|
3049
|
-
});
|
|
3050
|
-
}
|
|
3051
|
-
async function processDigest(digest, agent) {
|
|
3052
|
-
const sid = shortId2(digest.id);
|
|
3053
|
-
if (digest.rawMessages.length === 0) {
|
|
3054
|
-
await api.post(`/api/email-digest/${digest.id}/complete`, {
|
|
3055
|
-
summary: "No unread emails found in your inbox.",
|
|
3056
|
-
items: []
|
|
3057
|
-
});
|
|
3058
|
-
logOk(`${paint4("yellow", sid)} completed (no emails)`);
|
|
3059
|
-
return;
|
|
3060
|
-
}
|
|
3061
|
-
log(`${paint4("yellow", sid)} analyzing ${paint4("cyan", String(digest.rawMessages.length))} emails\u2026`);
|
|
3062
|
-
const prompt2 = buildPrompt(digest.rawMessages);
|
|
3063
|
-
const output = await runAgent(prompt2, agent);
|
|
3064
|
-
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
3065
|
-
if (!jsonMatch) {
|
|
3066
|
-
throw new Error("Could not extract JSON from Claude output");
|
|
3067
|
-
}
|
|
3068
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
3069
|
-
const msgMap = new Map(digest.rawMessages.map((m) => [m.id, m]));
|
|
3070
|
-
const items = parsed.items.map((item) => {
|
|
3071
|
-
const msg = msgMap.get(item.emailId);
|
|
3072
|
-
if (!msg) return null;
|
|
3073
|
-
return {
|
|
3074
|
-
emailId: msg.id,
|
|
3075
|
-
threadId: msg.threadId,
|
|
3076
|
-
subject: msg.subject,
|
|
3077
|
-
sender: msg.sender,
|
|
3078
|
-
senderEmail: msg.senderEmail,
|
|
3079
|
-
date: msg.date,
|
|
3080
|
-
snippet: msg.snippet,
|
|
3081
|
-
summary: item.summary || msg.snippet,
|
|
3082
|
-
isRelevant: item.isRelevant,
|
|
3083
|
-
relevanceReason: item.relevanceReason,
|
|
3084
|
-
category: item.category || (item.isRelevant ? "General" : "Promotions & Newsletters"),
|
|
3085
|
-
archived: false
|
|
3086
|
-
};
|
|
3087
|
-
}).filter((item) => item !== null);
|
|
3088
|
-
await api.post(`/api/email-digest/${digest.id}/complete`, {
|
|
3089
|
-
summary: parsed.summary,
|
|
3090
|
-
items
|
|
3091
|
-
});
|
|
3092
|
-
const relevantItems = items.filter((i) => i.isRelevant);
|
|
3093
|
-
const noiseItems = items.filter((i) => !i.isRelevant);
|
|
3094
|
-
logOk(
|
|
3095
|
-
`${paint4("yellow", sid)} done \u2014 ${paint4("green", String(relevantItems.length))} relevant, ${paint4("gray", String(noiseItems.length))} not relevant`
|
|
3096
|
-
);
|
|
3097
|
-
console.log("");
|
|
3098
|
-
console.log(` ${paint4("bold", "Summary:")} ${parsed.summary}`);
|
|
3099
|
-
console.log("");
|
|
3100
|
-
if (relevantItems.length > 0) {
|
|
3101
|
-
console.log(` ${paint4("bold", paint4("green", "Important"))}`);
|
|
3102
|
-
for (const item of relevantItems) {
|
|
3103
|
-
console.log(` ${paint4("cyan", "\u2022")} ${item.subject} ${paint4("dim", `\u2014 ${item.sender}`)}`);
|
|
3104
|
-
}
|
|
3105
|
-
console.log("");
|
|
3106
|
-
}
|
|
3107
|
-
if (noiseItems.length > 0) {
|
|
3108
|
-
console.log(` ${paint4("dim", "Not important")}`);
|
|
3109
|
-
for (const item of noiseItems) {
|
|
3110
|
-
console.log(` ${paint4("dim", `\u2022 ${item.subject} \u2014 ${item.sender}`)}`);
|
|
3111
|
-
}
|
|
3112
|
-
console.log("");
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
async function generateDigest() {
|
|
3116
|
-
let result;
|
|
3117
|
-
try {
|
|
3118
|
-
result = await api.post("/api/email-digest/generate");
|
|
3119
|
-
} catch (err) {
|
|
3120
|
-
logErr(`Auto-generate failed: ${err.message}`);
|
|
3121
|
-
return;
|
|
3122
|
-
}
|
|
3123
|
-
if (result.needsReauth) {
|
|
3124
|
-
logErr("Auto-generate skipped: Gmail re-authentication required");
|
|
3125
|
-
return;
|
|
3126
|
-
}
|
|
3127
|
-
if (result.status === "completed") {
|
|
3128
|
-
log(`Auto-generated digest ${paint4("yellow", shortId2(result.id))} \u2014 no new emails`);
|
|
3129
|
-
} else {
|
|
3130
|
-
log(`Auto-generated digest ${paint4("yellow", shortId2(result.id))} \u2014 queued for processing`);
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
var digestCommand = new Command13("digest").description("Process pending email digests using an AI coding agent (run alongside mr watch)").option("--interval <seconds>", "Polling interval in seconds", "15").option("--generate-interval <seconds>", "How often to auto-generate new digests", "3600").option("--once", "Process pending digests once and exit", false).option("--agent <agent>", "AI agent to use: claude or codex", "claude").action(async (opts) => {
|
|
3134
|
-
const intervalMs = parseInt(opts.interval, 10) * 1e3;
|
|
3135
|
-
const generateIntervalMs = parseInt(opts.generateInterval, 10) * 1e3;
|
|
3136
|
-
const once = opts.once;
|
|
3137
|
-
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
3138
|
-
const processing = /* @__PURE__ */ new Set();
|
|
3139
|
-
const banner = [
|
|
3140
|
-
``,
|
|
3141
|
-
paint4("magenta", ` \u2554\u2566\u2557\u2566\u2550\u2557 \u2554\u2566\u2557\u2566\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557`),
|
|
3142
|
-
paint4("magenta", ` \u2551\u2551\u2551\u2560\u2566\u255D \u2551\u2551\u2551\u2551 \u2566\u2551\u2563 \u255A\u2550\u2557 \u2551`),
|
|
3143
|
-
paint4("magenta", ` \u2569 \u2569\u2569\u255A\u2550 \u2550\u2569\u255D\u2569\u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u2569`),
|
|
3144
|
-
paint4("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
3145
|
-
paint4("dim", ` email digest processor \xB7 powered by ${agent}`),
|
|
3146
|
-
``
|
|
3147
|
-
].join("\n");
|
|
3148
|
-
console.log(banner);
|
|
3149
|
-
console.log(
|
|
3150
|
-
` interval=${paint4("cyan", opts.interval + "s")} generate-interval=${paint4("cyan", opts.generateInterval + "s")}${once ? ` ${paint4("yellow", "once")}` : ""}
|
|
3151
|
-
`
|
|
3152
|
-
);
|
|
3153
|
-
async function poll() {
|
|
3154
|
-
let pending;
|
|
3155
|
-
try {
|
|
3156
|
-
pending = await api.get("/api/email-digest/pending");
|
|
3157
|
-
} catch (err) {
|
|
3158
|
-
logErr(`Failed to fetch pending digests: ${err.message}`);
|
|
3159
|
-
return;
|
|
3160
|
-
}
|
|
3161
|
-
if (pending.length === 0) return;
|
|
3162
|
-
const digest = pending[pending.length - 1];
|
|
3163
|
-
if (processing.has(digest.id)) return;
|
|
3164
|
-
log(`Processing digest ${paint4("yellow", shortId2(digest.id))} (${paint4("cyan", String(digest.rawMessages.length))} emails)`);
|
|
3165
|
-
processing.add(digest.id);
|
|
3166
|
-
processDigest(digest, agent).catch((err) => logErr(`${shortId2(digest.id)} failed: ${err.message}`)).finally(() => processing.delete(digest.id));
|
|
3167
|
-
}
|
|
3168
|
-
process.on("SIGINT", () => {
|
|
3169
|
-
console.log(`
|
|
3170
|
-
${timestamp2()} ${digestTag()} Shutting down\u2026`);
|
|
3171
|
-
process.exit(0);
|
|
3172
|
-
});
|
|
3173
|
-
await generateDigest();
|
|
3174
|
-
await poll();
|
|
3175
|
-
if (once) {
|
|
3176
|
-
const wait = () => new Promise((resolve7) => {
|
|
3177
|
-
if (processing.size === 0) return resolve7();
|
|
3178
|
-
const check = setInterval(() => {
|
|
3179
|
-
if (processing.size === 0) {
|
|
3180
|
-
clearInterval(check);
|
|
3181
|
-
resolve7();
|
|
3182
|
-
}
|
|
3183
|
-
}, 500);
|
|
3184
|
-
});
|
|
3185
|
-
await wait();
|
|
3186
|
-
} else {
|
|
3187
|
-
setInterval(poll, intervalMs);
|
|
3188
|
-
setInterval(generateDigest, generateIntervalMs);
|
|
3189
|
-
}
|
|
3190
|
-
});
|
|
3191
|
-
|
|
3192
|
-
// cli/commands/up.ts
|
|
3193
|
-
import { Command as Command14 } from "commander";
|
|
3194
|
-
import { spawn as spawn6 } from "child_process";
|
|
3195
|
-
import { resolve as resolve2 } from "path";
|
|
3196
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3197
|
-
var c5 = {
|
|
3198
|
-
reset: "\x1B[0m",
|
|
3199
|
-
bold: "\x1B[1m",
|
|
3200
|
-
dim: "\x1B[2m",
|
|
3201
|
-
cyan: "\x1B[36m",
|
|
3202
|
-
green: "\x1B[32m",
|
|
3203
|
-
yellow: "\x1B[33m",
|
|
3204
|
-
red: "\x1B[31m",
|
|
3205
|
-
magenta: "\x1B[35m",
|
|
3206
|
-
gray: "\x1B[90m"
|
|
3207
|
-
};
|
|
3208
|
-
function paint5(color, text) {
|
|
3209
|
-
return `${c5[color]}${text}${c5.reset}`;
|
|
3210
|
-
}
|
|
3211
|
-
function timestamp3() {
|
|
3212
|
-
return paint5("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
3213
|
-
}
|
|
3214
|
-
function upTag() {
|
|
3215
|
-
return paint5("cyan", "[up]");
|
|
3216
|
-
}
|
|
3217
|
-
function log2(msg) {
|
|
3218
|
-
console.log(`${timestamp3()} ${upTag()} ${msg}`);
|
|
3219
|
-
}
|
|
3220
|
-
function logErr2(msg) {
|
|
3221
|
-
console.error(`${timestamp3()} ${upTag()} ${paint5("red", "\u2717")} ${msg}`);
|
|
3222
|
-
}
|
|
3223
|
-
function spawnMr(args) {
|
|
3224
|
-
const entry = process.argv[1];
|
|
3225
|
-
const child = spawn6(process.execPath, [entry, ...args], {
|
|
3226
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3227
|
-
});
|
|
3228
|
-
return child;
|
|
3229
|
-
}
|
|
3230
|
-
var upCommand = new Command14("up").description("Run mr watch and mr digest together with a single command").option("--interval <seconds>", "Polling interval for both watch and digest", "15").option("--watch-interval <seconds>", "Override polling interval for mr watch").option("--digest-interval <seconds>", "Override polling interval for mr digest").option("--dry-run", "Pass --dry-run to mr watch", false).option("--plan-approval", "Pass --plan-approval to mr watch", false).option("--root <dir>", "Pass --root to mr watch (default: cwd)").option("--agent <agent>", "AI agent to use: claude or codex", "claude").action((opts) => {
|
|
3231
|
-
const watchInterval = opts.watchInterval ?? opts.interval;
|
|
3232
|
-
const digestInterval = opts.digestInterval ?? opts.interval;
|
|
3233
|
-
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
3234
|
-
const watchArgs = ["watch", "--interval", watchInterval, "--agent", agent];
|
|
3235
|
-
if (opts.dryRun) watchArgs.push("--dry-run");
|
|
3236
|
-
if (opts.planApproval) watchArgs.push("--plan-approval");
|
|
3237
|
-
if (opts.root) watchArgs.push("--root", opts.root);
|
|
3238
|
-
const digestArgs = ["digest", "--interval", digestInterval, "--agent", agent];
|
|
3239
|
-
const banner = [
|
|
3240
|
-
``,
|
|
3241
|
-
paint5("cyan", ` \u2554\u2566\u2557\u2566\u2550\u2557 \u2566 \u2566\u2554\u2550\u2557`),
|
|
3242
|
-
paint5("magenta", ` \u2551\u2551\u2551\u2560\u2566\u255D \u2551 \u2551\u2560\u2550\u255D`),
|
|
3243
|
-
paint5("cyan", ` \u2569 \u2569\u2569\u255A\u2550 \u255A\u2550\u255D\u2569 `),
|
|
3244
|
-
paint5("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
3245
|
-
paint5("dim", ` mr watch + mr digest \xB7 powered by ${agent}`),
|
|
3246
|
-
``
|
|
3247
|
-
].join("\n");
|
|
3248
|
-
console.log(banner);
|
|
3249
|
-
const flags = [
|
|
3250
|
-
`watch-interval=${paint5("cyan", watchInterval + "s")}`,
|
|
3251
|
-
`digest-interval=${paint5("cyan", digestInterval + "s")}`,
|
|
3252
|
-
`agent=${paint5("cyan", agent)}`,
|
|
3253
|
-
...opts.dryRun ? [paint5("yellow", "dry-run")] : [],
|
|
3254
|
-
...opts.planApproval ? [paint5("yellow", "plan-approval")] : [],
|
|
3255
|
-
...opts.root ? [`root=${paint5("cyan", resolve2(opts.root))}`] : []
|
|
3256
|
-
].join(" ");
|
|
3257
|
-
console.log(` ${flags}
|
|
3258
|
-
`);
|
|
3259
|
-
const watchProc = spawnMr(watchArgs);
|
|
3260
|
-
const digestProc = spawnMr(digestArgs);
|
|
3261
|
-
for (const [proc, label] of [[watchProc, "watch"], [digestProc, "digest"]]) {
|
|
3262
|
-
proc.stdout?.on("data", (d) => process.stdout.write(d));
|
|
3263
|
-
proc.stderr?.on("data", (d) => process.stderr.write(d));
|
|
3264
|
-
proc.on("exit", (code, signal) => {
|
|
3265
|
-
if (signal !== "SIGTERM" && signal !== "SIGINT") {
|
|
3266
|
-
logErr2(`mr ${label} exited unexpectedly (code=${code ?? "?"}, signal=${signal ?? "none"})`);
|
|
3267
|
-
}
|
|
3268
|
-
});
|
|
3269
|
-
}
|
|
3270
|
-
function shutdown() {
|
|
3271
|
-
log2("Shutting down\u2026");
|
|
3272
|
-
watchProc.kill("SIGTERM");
|
|
3273
|
-
digestProc.kill("SIGTERM");
|
|
3274
|
-
setTimeout(() => process.exit(0), 500);
|
|
3275
|
-
}
|
|
3276
|
-
process.on("SIGINT", shutdown);
|
|
3277
|
-
process.on("SIGTERM", shutdown);
|
|
3278
|
-
});
|
|
3279
|
-
|
|
3280
|
-
// cli/commands/prototype.ts
|
|
3281
|
-
import { Command as Command15 } from "commander";
|
|
3282
|
-
var c6 = {
|
|
3283
|
-
reset: "\x1B[0m",
|
|
3284
|
-
bold: "\x1B[1m",
|
|
3285
|
-
dim: "\x1B[2m",
|
|
3286
|
-
cyan: "\x1B[36m",
|
|
3287
|
-
green: "\x1B[32m",
|
|
3288
|
-
yellow: "\x1B[33m",
|
|
3289
|
-
red: "\x1B[31m",
|
|
3290
|
-
blue: "\x1B[34m",
|
|
3291
|
-
gray: "\x1B[90m"
|
|
3292
|
-
};
|
|
3293
|
-
function paint6(color, text) {
|
|
3294
|
-
return `${c6[color]}${text}${c6.reset}`;
|
|
3295
|
-
}
|
|
3296
2988
|
function statusBadge(status) {
|
|
3297
2989
|
switch (status) {
|
|
3298
2990
|
case "pending":
|
|
3299
|
-
return
|
|
2991
|
+
return paint4("gray", "\u25CB pending");
|
|
3300
2992
|
case "in_progress":
|
|
3301
|
-
return
|
|
2993
|
+
return paint4("cyan", "\u27F3 in_progress");
|
|
3302
2994
|
case "completed":
|
|
3303
|
-
return
|
|
2995
|
+
return paint4("green", "\u2713 completed");
|
|
3304
2996
|
case "failed":
|
|
3305
|
-
return
|
|
2997
|
+
return paint4("red", "\u2717 failed");
|
|
3306
2998
|
default:
|
|
3307
|
-
return
|
|
2999
|
+
return paint4("gray", status);
|
|
3308
3000
|
}
|
|
3309
3001
|
}
|
|
3310
|
-
var prototypeCommand = new
|
|
3311
|
-
new
|
|
3002
|
+
var prototypeCommand = new Command13("prototype").description("Manage prototypes").addCommand(
|
|
3003
|
+
new Command13("list").description("List prototypes for the linked project").option("--all", "Show prototypes for all projects").action(async (opts) => {
|
|
3312
3004
|
const params = new URLSearchParams();
|
|
3313
3005
|
if (!opts.all) {
|
|
3314
3006
|
const projectId = getLinkedProjectId();
|
|
@@ -3320,21 +3012,21 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
3320
3012
|
}
|
|
3321
3013
|
const prototypes = await api.get(`/api/prototypes?${params.toString()}`);
|
|
3322
3014
|
if (prototypes.length === 0) {
|
|
3323
|
-
console.log(
|
|
3015
|
+
console.log(paint4("gray", "No prototypes found."));
|
|
3324
3016
|
return;
|
|
3325
3017
|
}
|
|
3326
3018
|
console.log();
|
|
3327
3019
|
for (const p of prototypes) {
|
|
3328
3020
|
const date = new Date(p.createdAt).toLocaleDateString();
|
|
3329
3021
|
console.log(
|
|
3330
|
-
` ${
|
|
3022
|
+
` ${paint4("bold", p.title)} ${statusBadge(p.status)} ${paint4("gray", p.id.slice(0, 8))} ${paint4("dim", date)}`
|
|
3331
3023
|
);
|
|
3332
|
-
console.log(` ${
|
|
3024
|
+
console.log(` ${paint4("dim", p.prompt.slice(0, 80) + (p.prompt.length > 80 ? "\u2026" : ""))}`);
|
|
3333
3025
|
console.log();
|
|
3334
3026
|
}
|
|
3335
3027
|
})
|
|
3336
3028
|
).addCommand(
|
|
3337
|
-
new
|
|
3029
|
+
new Command13("create").description("Create a new prototype").argument("<title>", "Title of the prototype").requiredOption("--prompt <prompt>", "Design description / prompt").option("--project <projectId>", "Project ID (defaults to linked project)").option("--variants <count>", "Number of variants to generate (1-50)", "5").action(async (title, opts) => {
|
|
3338
3030
|
const projectId = opts.project ?? getLinkedProjectId();
|
|
3339
3031
|
if (!projectId) {
|
|
3340
3032
|
console.error('No project linked. Run "mr link <project-id>" or use --project.');
|
|
@@ -3348,37 +3040,37 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
3348
3040
|
projectId
|
|
3349
3041
|
});
|
|
3350
3042
|
console.log();
|
|
3351
|
-
console.log(` ${
|
|
3352
|
-
console.log(` ${
|
|
3353
|
-
console.log(` ${
|
|
3043
|
+
console.log(` ${paint4("green", "\u2713")} Created prototype: ${paint4("bold", prototype.title)}`);
|
|
3044
|
+
console.log(` ${paint4("gray", "ID:")} ${prototype.id}`);
|
|
3045
|
+
console.log(` ${paint4("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
|
|
3354
3046
|
console.log();
|
|
3355
3047
|
})
|
|
3356
3048
|
).addCommand(
|
|
3357
|
-
new
|
|
3049
|
+
new Command13("start").description("Start prototype generation (sets status to in_progress)").argument("<id>", "Prototype ID").action(async (id) => {
|
|
3358
3050
|
const prototype = await api.patch(`/api/prototypes/${id}`, {
|
|
3359
3051
|
status: "in_progress"
|
|
3360
3052
|
});
|
|
3361
3053
|
console.log();
|
|
3362
|
-
console.log(` ${
|
|
3363
|
-
console.log(` ${
|
|
3054
|
+
console.log(` ${paint4("cyan", "\u27F3")} Started: ${paint4("bold", prototype.title)}`);
|
|
3055
|
+
console.log(` ${paint4("gray", "The watch agent will pick this up shortly.")}`);
|
|
3364
3056
|
console.log();
|
|
3365
3057
|
})
|
|
3366
3058
|
).addCommand(
|
|
3367
|
-
new
|
|
3059
|
+
new Command13("retry").description("Retry a failed prototype").argument("<id>", "Prototype ID").action(async (id) => {
|
|
3368
3060
|
const prototype = await api.patch(`/api/prototypes/${id}`, {
|
|
3369
3061
|
status: "in_progress",
|
|
3370
3062
|
files: null
|
|
3371
3063
|
});
|
|
3372
3064
|
console.log();
|
|
3373
|
-
console.log(` ${
|
|
3065
|
+
console.log(` ${paint4("cyan", "\u27F3")} Retrying: ${paint4("bold", prototype.title)}`);
|
|
3374
3066
|
console.log();
|
|
3375
3067
|
})
|
|
3376
3068
|
);
|
|
3377
3069
|
|
|
3378
3070
|
// cli/commands/setup.ts
|
|
3379
|
-
import { Command as
|
|
3071
|
+
import { Command as Command14 } from "commander";
|
|
3380
3072
|
import { exec as exec2 } from "child_process";
|
|
3381
|
-
var
|
|
3073
|
+
var c5 = {
|
|
3382
3074
|
reset: "\x1B[0m",
|
|
3383
3075
|
bold: "\x1B[1m",
|
|
3384
3076
|
dim: "\x1B[2m",
|
|
@@ -3389,8 +3081,8 @@ var c7 = {
|
|
|
3389
3081
|
magenta: "\x1B[35m",
|
|
3390
3082
|
gray: "\x1B[90m"
|
|
3391
3083
|
};
|
|
3392
|
-
function
|
|
3393
|
-
return `${
|
|
3084
|
+
function paint5(color, text) {
|
|
3085
|
+
return `${c5[color]}${text}${c5.reset}`;
|
|
3394
3086
|
}
|
|
3395
3087
|
function commandExists(cmd) {
|
|
3396
3088
|
return new Promise((resolve7) => {
|
|
@@ -3634,60 +3326,60 @@ async function checkApiConnectivity() {
|
|
|
3634
3326
|
}
|
|
3635
3327
|
}
|
|
3636
3328
|
function printResults(checks) {
|
|
3637
|
-
const maxNameLen = Math.max(...checks.map((
|
|
3329
|
+
const maxNameLen = Math.max(...checks.map((c10) => c10.name.length));
|
|
3638
3330
|
let allOk = true;
|
|
3639
3331
|
for (const check of checks) {
|
|
3640
3332
|
const isOptional = check.optional ?? false;
|
|
3641
|
-
const icon = check.ok ?
|
|
3333
|
+
const icon = check.ok ? paint5("green", "\u2713") : isOptional ? paint5("yellow", "\u25CB") : paint5("red", "\u2717");
|
|
3642
3334
|
const name = check.name.padEnd(maxNameLen);
|
|
3643
|
-
const msg = check.ok ?
|
|
3335
|
+
const msg = check.ok ? paint5("green", check.message) : isOptional ? paint5("yellow", check.message) : paint5("red", check.message);
|
|
3644
3336
|
console.log(` ${icon} ${name} ${msg}`);
|
|
3645
3337
|
if (!check.ok && !isOptional) allOk = false;
|
|
3646
3338
|
}
|
|
3647
3339
|
return allOk;
|
|
3648
3340
|
}
|
|
3649
3341
|
async function autoFix(checks, agent) {
|
|
3650
|
-
const { spawn:
|
|
3651
|
-
const ghInstalled = checks.find((
|
|
3652
|
-
const ghAuthed = checks.find((
|
|
3653
|
-
const mrAuthed = checks.find((
|
|
3654
|
-
const claudeCheck = checks.find((
|
|
3342
|
+
const { spawn: spawn8 } = await import("child_process");
|
|
3343
|
+
const ghInstalled = checks.find((c10) => c10.name === "GitHub CLI (gh)").ok;
|
|
3344
|
+
const ghAuthed = checks.find((c10) => c10.name === "GitHub CLI auth").ok;
|
|
3345
|
+
const mrAuthed = checks.find((c10) => c10.name === "Mr. Manager CLI auth").ok;
|
|
3346
|
+
const claudeCheck = checks.find((c10) => c10.name === "Claude Code (claude)");
|
|
3655
3347
|
if (claudeCheck && !claudeCheck.ok && agent === "claude") {
|
|
3656
|
-
console.log(
|
|
3657
|
-
console.log(
|
|
3348
|
+
console.log(paint5("cyan", " Installing Claude Code..."));
|
|
3349
|
+
console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
3658
3350
|
await new Promise((resolve7) => {
|
|
3659
|
-
const child =
|
|
3351
|
+
const child = spawn8("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
3660
3352
|
child.on("exit", () => resolve7());
|
|
3661
3353
|
});
|
|
3662
3354
|
console.log("");
|
|
3663
3355
|
}
|
|
3664
3356
|
if (ghInstalled && !ghAuthed) {
|
|
3665
|
-
console.log(
|
|
3357
|
+
console.log(paint5("cyan", " Running gh auth login..."));
|
|
3666
3358
|
await new Promise((resolve7) => {
|
|
3667
|
-
const child =
|
|
3359
|
+
const child = spawn8("gh", ["auth", "login"], { stdio: "inherit" });
|
|
3668
3360
|
child.on("exit", () => resolve7());
|
|
3669
3361
|
});
|
|
3670
3362
|
console.log("");
|
|
3671
3363
|
}
|
|
3672
3364
|
if (!mrAuthed) {
|
|
3673
|
-
console.log(
|
|
3365
|
+
console.log(paint5("cyan", " Running mr login..."));
|
|
3674
3366
|
const entry = process.argv[1];
|
|
3675
3367
|
await new Promise((resolve7) => {
|
|
3676
|
-
const child =
|
|
3368
|
+
const child = spawn8(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
3677
3369
|
child.on("exit", () => resolve7());
|
|
3678
3370
|
});
|
|
3679
3371
|
console.log("");
|
|
3680
3372
|
}
|
|
3681
3373
|
}
|
|
3682
|
-
var setupCommand = new
|
|
3374
|
+
var setupCommand = new Command14("setup").description("Check that all dependencies for mr watch are installed and configured").option("--fix", "Attempt to auto-fix issues where possible", false).option("--agent <agent>", "AI agent to check: claude, codex, or gemini (default: claude)", "claude").action(async (opts) => {
|
|
3683
3375
|
const agent = opts.agent === "codex" ? "codex" : opts.agent === "gemini" ? "gemini" : "claude";
|
|
3684
3376
|
const banner = [
|
|
3685
3377
|
``,
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3378
|
+
paint5("cyan", ` \u2554\u2566\u2557\u2551\u2550\u2557 \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557\u2551 \u2551\u2554\u2550\u2557`),
|
|
3379
|
+
paint5("magenta", ` \u2551\u2551\u2551\u2560\u2566\u2563 \u255A\u2550\u2557\u2551\u2563 \u2551 \u2551 \u2551\u2560\u2550\u2563`),
|
|
3380
|
+
paint5("cyan", ` \u2569 \u2569\u2569\u255A\u2550 \u255A\u2550\u255D\u255A\u2550\u255D \u2569 \u255A\u2550\u255D\u2569 `),
|
|
3381
|
+
paint5("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
3382
|
+
paint5("dim", ` environment check for mr watch (agent: ${agent})`),
|
|
3691
3383
|
``
|
|
3692
3384
|
].join("\n");
|
|
3693
3385
|
console.log(banner);
|
|
@@ -3707,18 +3399,18 @@ var setupCommand = new Command16("setup").description("Check that all dependenci
|
|
|
3707
3399
|
const allOk = printResults(checks);
|
|
3708
3400
|
console.log("");
|
|
3709
3401
|
if (allOk) {
|
|
3710
|
-
console.log(
|
|
3402
|
+
console.log(paint5("green", " All checks passed! You're ready to run mr watch."));
|
|
3711
3403
|
if (agent === "claude") {
|
|
3712
|
-
console.log(
|
|
3404
|
+
console.log(paint5("dim", " Tip: check other agents with --agent codex or --agent gemini"));
|
|
3713
3405
|
}
|
|
3714
3406
|
console.log("");
|
|
3715
3407
|
return;
|
|
3716
3408
|
}
|
|
3717
|
-
const fixes = checks.filter((
|
|
3409
|
+
const fixes = checks.filter((c10) => !c10.ok && c10.fix && !c10.optional);
|
|
3718
3410
|
if (fixes.length > 0) {
|
|
3719
|
-
console.log(
|
|
3411
|
+
console.log(paint5("yellow", " To fix:"));
|
|
3720
3412
|
for (const fix of fixes) {
|
|
3721
|
-
console.log(` ${
|
|
3413
|
+
console.log(` ${paint5("dim", "\u2192")} ${paint5("bold", fix.name)}: ${fix.fix}`);
|
|
3722
3414
|
}
|
|
3723
3415
|
console.log("");
|
|
3724
3416
|
}
|
|
@@ -3729,8 +3421,8 @@ var setupCommand = new Command16("setup").description("Check that all dependenci
|
|
|
3729
3421
|
});
|
|
3730
3422
|
|
|
3731
3423
|
// cli/commands/update.ts
|
|
3732
|
-
import { Command as
|
|
3733
|
-
var updateCommand = new
|
|
3424
|
+
import { Command as Command15 } from "commander";
|
|
3425
|
+
var updateCommand = new Command15("update").description("Post a status update to a task (used by agents to report progress)").argument("<task-id>", "Task ID").argument("<message>", "Status update message").option("--source <source>", "Update source: agent, system, or user", "agent").action(async (taskId, message, opts) => {
|
|
3734
3426
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
3735
3427
|
message,
|
|
3736
3428
|
source: opts.source
|
|
@@ -3739,11 +3431,11 @@ var updateCommand = new Command17("update").description("Post a status update to
|
|
|
3739
3431
|
});
|
|
3740
3432
|
|
|
3741
3433
|
// cli/commands/screenshot.ts
|
|
3742
|
-
import { Command as
|
|
3743
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
3434
|
+
import { Command as Command16 } from "commander";
|
|
3435
|
+
import { readFileSync as readFileSync6, existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
3744
3436
|
import { join as join8 } from "path";
|
|
3745
3437
|
import { tmpdir } from "os";
|
|
3746
|
-
var screenshotCommand = new
|
|
3438
|
+
var screenshotCommand = new Command16("screenshot").description(
|
|
3747
3439
|
"Take or attach a screenshot to a task update (agents use this to show their work)"
|
|
3748
3440
|
).argument("<task-id>", "Task ID").argument("[file]", "Path to an image file (if omitted, uses headless browser to screenshot the app)").option("-m, --message <message>", "Optional message to include with the screenshot").option("-u, --url <url>", "Custom URL to screenshot (defaults to the task's project page)").action(async (taskId, file, opts) => {
|
|
3749
3441
|
let filePath = file;
|
|
@@ -3780,13 +3472,13 @@ var screenshotCommand = new Command18("screenshot").description(
|
|
|
3780
3472
|
console.error("[screenshot] Make sure the browse daemon is set up: mr browse setup");
|
|
3781
3473
|
process.exit(1);
|
|
3782
3474
|
}
|
|
3783
|
-
if (!
|
|
3475
|
+
if (!existsSync8(tempFile)) {
|
|
3784
3476
|
console.error("[screenshot] Screenshot file was not created");
|
|
3785
3477
|
process.exit(1);
|
|
3786
3478
|
}
|
|
3787
3479
|
filePath = tempFile;
|
|
3788
3480
|
}
|
|
3789
|
-
if (!
|
|
3481
|
+
if (!existsSync8(filePath)) {
|
|
3790
3482
|
console.error(`File not found: ${filePath}`);
|
|
3791
3483
|
process.exit(1);
|
|
3792
3484
|
}
|
|
@@ -3828,16 +3520,16 @@ var screenshotCommand = new Command18("screenshot").description(
|
|
|
3828
3520
|
source: "agent"
|
|
3829
3521
|
});
|
|
3830
3522
|
console.log(`\u2713 Screenshot uploaded and attached to task`);
|
|
3831
|
-
if (tempFile &&
|
|
3523
|
+
if (tempFile && existsSync8(tempFile)) {
|
|
3832
3524
|
unlinkSync2(tempFile);
|
|
3833
3525
|
}
|
|
3834
3526
|
});
|
|
3835
3527
|
|
|
3836
3528
|
// cli/commands/resume.ts
|
|
3837
|
-
import { Command as
|
|
3838
|
-
import { spawn as
|
|
3529
|
+
import { Command as Command17 } from "commander";
|
|
3530
|
+
import { spawn as spawn5 } from "child_process";
|
|
3839
3531
|
import { resolve as resolve3 } from "path";
|
|
3840
|
-
var
|
|
3532
|
+
var c6 = {
|
|
3841
3533
|
reset: "\x1B[0m",
|
|
3842
3534
|
bold: "\x1B[1m",
|
|
3843
3535
|
dim: "\x1B[2m",
|
|
@@ -3848,15 +3540,15 @@ var c8 = {
|
|
|
3848
3540
|
magenta: "\x1B[35m",
|
|
3849
3541
|
gray: "\x1B[90m"
|
|
3850
3542
|
};
|
|
3851
|
-
function
|
|
3852
|
-
return `${
|
|
3543
|
+
function paint6(color, text) {
|
|
3544
|
+
return `${c6[color]}${text}${c6.reset}`;
|
|
3853
3545
|
}
|
|
3854
|
-
var resumeCommand = new
|
|
3546
|
+
var resumeCommand = new Command17("resume").description("Resume an interactive Claude session for a task (non-headless)").argument("<task-id>", "Task ID whose Claude session to resume").option("--dir <directory>", "Override the working directory for the session").action(async (taskId, opts) => {
|
|
3855
3547
|
const task = await api.get(`/api/tasks/${taskId}`);
|
|
3856
3548
|
if (!task.claudeSessionId) {
|
|
3857
3549
|
console.error(
|
|
3858
3550
|
`
|
|
3859
|
-
${
|
|
3551
|
+
${paint6("red", "\u2717")} No Claude session found for task ${paint6("dim", taskId.slice(0, 8))}`
|
|
3860
3552
|
);
|
|
3861
3553
|
console.error(
|
|
3862
3554
|
` The task may not have been dispatched yet.
|
|
@@ -3867,16 +3559,16 @@ var resumeCommand = new Command19("resume").description("Resume an interactive C
|
|
|
3867
3559
|
if (task.status === "completed" || task.status === "todo") {
|
|
3868
3560
|
console.error(
|
|
3869
3561
|
`
|
|
3870
|
-
${
|
|
3562
|
+
${paint6("yellow", "\u26A0")} Task ${paint6("dim", taskId.slice(0, 8))} has already completed.`
|
|
3871
3563
|
);
|
|
3872
3564
|
console.error(
|
|
3873
|
-
` Session ID: ${
|
|
3565
|
+
` Session ID: ${paint6("cyan", task.claudeSessionId)}`
|
|
3874
3566
|
);
|
|
3875
3567
|
console.error(
|
|
3876
|
-
`
|
|
3568
|
+
` To resume the session, run:`
|
|
3877
3569
|
);
|
|
3878
3570
|
console.error(
|
|
3879
|
-
` ${
|
|
3571
|
+
` ${paint6("dim", `claude --resume ${task.claudeSessionId}`)}
|
|
3880
3572
|
`
|
|
3881
3573
|
);
|
|
3882
3574
|
process.exit(0);
|
|
@@ -3895,16 +3587,16 @@ var resumeCommand = new Command19("resume").description("Resume an interactive C
|
|
|
3895
3587
|
const sid = taskId.slice(0, 8);
|
|
3896
3588
|
console.log([
|
|
3897
3589
|
``,
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
` ${
|
|
3590
|
+
paint6("magenta", ` \u2554\u2566\u2557\u2566\u2550\u2557 `) + paint6("bold", "resume"),
|
|
3591
|
+
paint6("magenta", ` \u2551\u2551\u2551\u2560\u2566\u255D `) + paint6("dim", "\u2500".repeat(44)),
|
|
3592
|
+
paint6("magenta", ` \u2569 \u2569\u2569\u255A\u2550 `) + task.title,
|
|
3593
|
+
` ${paint6("gray", sid)}`,
|
|
3902
3594
|
``,
|
|
3903
|
-
` ${
|
|
3904
|
-
` ${
|
|
3595
|
+
` ${paint6("dim", "session")} ${paint6("cyan", task.claudeSessionId)}`,
|
|
3596
|
+
` ${paint6("dim", "cwd")} ${paint6("cyan", cwd)}`,
|
|
3905
3597
|
``
|
|
3906
3598
|
].join("\n"));
|
|
3907
|
-
const child =
|
|
3599
|
+
const child = spawn5("claude", ["--resume", task.claudeSessionId], {
|
|
3908
3600
|
cwd,
|
|
3909
3601
|
stdio: "inherit"
|
|
3910
3602
|
});
|
|
@@ -3913,16 +3605,16 @@ var resumeCommand = new Command19("resume").description("Resume an interactive C
|
|
|
3913
3605
|
});
|
|
3914
3606
|
child.on("error", (err) => {
|
|
3915
3607
|
console.error(`
|
|
3916
|
-
${
|
|
3608
|
+
${paint6("red", "\u2717")} Failed to launch Claude: ${err.message}
|
|
3917
3609
|
`);
|
|
3918
3610
|
process.exit(1);
|
|
3919
3611
|
});
|
|
3920
3612
|
});
|
|
3921
3613
|
|
|
3922
3614
|
// cli/commands/browse.ts
|
|
3923
|
-
import { Command as
|
|
3924
|
-
import { execSync as execSync5, spawn as
|
|
3925
|
-
import { existsSync as
|
|
3615
|
+
import { Command as Command18 } from "commander";
|
|
3616
|
+
import { execSync as execSync5, spawn as spawn6 } from "child_process";
|
|
3617
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
3926
3618
|
import { join as join9 } from "path";
|
|
3927
3619
|
var BROWSE_DIR2 = join9(import.meta.dirname, "..", "..", "browse");
|
|
3928
3620
|
function isProcessAlive(pid) {
|
|
@@ -3962,7 +3654,7 @@ async function ensureDevServer() {
|
|
|
3962
3654
|
}
|
|
3963
3655
|
const port = await findAvailablePort2(3e3);
|
|
3964
3656
|
console.log(`[browse] Starting dev server on port ${port}...`);
|
|
3965
|
-
const devProc =
|
|
3657
|
+
const devProc = spawn6("npm", ["run", "dev", "--", "--port", String(port)], {
|
|
3966
3658
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3967
3659
|
detached: true,
|
|
3968
3660
|
cwd: join9(import.meta.dirname, "..", ".."),
|
|
@@ -3984,7 +3676,7 @@ async function ensureDevServer() {
|
|
|
3984
3676
|
}
|
|
3985
3677
|
throw new Error("Dev server failed to start within 30s");
|
|
3986
3678
|
}
|
|
3987
|
-
var browseCommand = new
|
|
3679
|
+
var browseCommand = new Command18("browse").description("Control a headless browser for QA and testing").argument("[command]", "Browse command (goto, click, fill, screenshot, etc.)").argument("[args...]", "Command arguments").option(
|
|
3988
3680
|
"--task-id <id>",
|
|
3989
3681
|
"Attach screenshot to a task update (only for screenshot command)"
|
|
3990
3682
|
).option("--dev", "Auto-start local dev server before browsing").allowUnknownOption(true).action(
|
|
@@ -4027,7 +3719,7 @@ var browseCommand = new Command20("browse").description("Control a headless brow
|
|
|
4027
3719
|
}
|
|
4028
3720
|
if (command === "screenshot" && opts.taskId) {
|
|
4029
3721
|
const screenshotPath = stdout.match(/Screenshot saved: (.+)/)?.[1];
|
|
4030
|
-
if (!screenshotPath || !
|
|
3722
|
+
if (!screenshotPath || !existsSync9(screenshotPath)) {
|
|
4031
3723
|
console.error("[browse] Could not find screenshot file");
|
|
4032
3724
|
process.exit(1);
|
|
4033
3725
|
}
|
|
@@ -4067,12 +3759,12 @@ var browseCommand = new Command20("browse").description("Control a headless brow
|
|
|
4067
3759
|
);
|
|
4068
3760
|
|
|
4069
3761
|
// cli/commands/set-path.ts
|
|
4070
|
-
import { Command as
|
|
3762
|
+
import { Command as Command19 } from "commander";
|
|
4071
3763
|
import { resolve as resolve4 } from "path";
|
|
4072
|
-
import { existsSync as
|
|
4073
|
-
var setPathCommand = new
|
|
3764
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3765
|
+
var setPathCommand = new Command19("set-path").description("Set or update the local repo path for a project").argument("<project-id>", "Project ID").argument("<path>", "Absolute or relative path to the local repo").action(async (projectId, pathArg) => {
|
|
4074
3766
|
const absolutePath = resolve4(pathArg);
|
|
4075
|
-
if (!
|
|
3767
|
+
if (!existsSync10(absolutePath)) {
|
|
4076
3768
|
console.error(`Error: Path does not exist: ${absolutePath}`);
|
|
4077
3769
|
process.exit(1);
|
|
4078
3770
|
}
|
|
@@ -4088,9 +3780,9 @@ var setPathCommand = new Command21("set-path").description("Set or update the lo
|
|
|
4088
3780
|
});
|
|
4089
3781
|
|
|
4090
3782
|
// cli/commands/test.ts
|
|
4091
|
-
import { Command as
|
|
4092
|
-
import { readFileSync as readFileSync8, existsSync as
|
|
4093
|
-
var testCommand = new
|
|
3783
|
+
import { Command as Command20 } from "commander";
|
|
3784
|
+
import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
|
|
3785
|
+
var testCommand = new Command20("test").description("Run automated browser test for a task's MR/PR").argument("<task-id>", "Task ID to test").option("--plan <file>", "Path to a custom test plan JSON file").action(async (taskId, opts) => {
|
|
4094
3786
|
const config = loadConfig();
|
|
4095
3787
|
console.log("[test] Fetching task...");
|
|
4096
3788
|
let task;
|
|
@@ -4111,7 +3803,7 @@ var testCommand = new Command22("test").description("Run automated browser test
|
|
|
4111
3803
|
);
|
|
4112
3804
|
process.exit(1);
|
|
4113
3805
|
}
|
|
4114
|
-
if (!
|
|
3806
|
+
if (!existsSync11(localPath)) {
|
|
4115
3807
|
console.error(
|
|
4116
3808
|
`[test] Local repo path not found at ${localPath}. Run \`mr set-path\` to update.`
|
|
4117
3809
|
);
|
|
@@ -4210,11 +3902,11 @@ var testCommand = new Command22("test").description("Run automated browser test
|
|
|
4210
3902
|
});
|
|
4211
3903
|
|
|
4212
3904
|
// cli/commands/features.ts
|
|
4213
|
-
import { Command as
|
|
4214
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as
|
|
3905
|
+
import { Command as Command21 } from "commander";
|
|
3906
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
|
|
4215
3907
|
import { resolve as resolve5, sep as sep2 } from "path";
|
|
4216
3908
|
var FEATURES_FILE3 = ".mr-features.md";
|
|
4217
|
-
var
|
|
3909
|
+
var c7 = {
|
|
4218
3910
|
reset: "\x1B[0m",
|
|
4219
3911
|
bold: "\x1B[1m",
|
|
4220
3912
|
dim: "\x1B[2m",
|
|
@@ -4224,8 +3916,8 @@ var c9 = {
|
|
|
4224
3916
|
magenta: "\x1B[35m",
|
|
4225
3917
|
gray: "\x1B[90m"
|
|
4226
3918
|
};
|
|
4227
|
-
function
|
|
4228
|
-
return `${
|
|
3919
|
+
function paint7(color, text) {
|
|
3920
|
+
return `${c7[color]}${text}${c7.reset}`;
|
|
4229
3921
|
}
|
|
4230
3922
|
function resolveProjectRoot2() {
|
|
4231
3923
|
const cwd = process.cwd();
|
|
@@ -4241,10 +3933,10 @@ function getFeaturesPath() {
|
|
|
4241
3933
|
}
|
|
4242
3934
|
function readFeatures2() {
|
|
4243
3935
|
const path = getFeaturesPath();
|
|
4244
|
-
if (!
|
|
3936
|
+
if (!existsSync12(path)) return null;
|
|
4245
3937
|
return readFileSync9(path, "utf-8");
|
|
4246
3938
|
}
|
|
4247
|
-
var featuresCommand = new
|
|
3939
|
+
var featuresCommand = new Command21("features").description("View or update the project features & goals document (.mr-features.md)").option("--update <content>", "Replace the features document with the given content").option("--file <path>", "Read content from a file and use it to update the features document").option("--path", "Print the path to the features file").action(async (opts) => {
|
|
4248
3940
|
if (opts.path) {
|
|
4249
3941
|
console.log(getFeaturesPath());
|
|
4250
3942
|
return;
|
|
@@ -4253,30 +3945,30 @@ var featuresCommand = new Command23("features").description("View or update the
|
|
|
4253
3945
|
const content2 = readFileSync9(resolve5(opts.file), "utf-8");
|
|
4254
3946
|
const featuresPath = getFeaturesPath();
|
|
4255
3947
|
writeFileSync5(featuresPath, content2);
|
|
4256
|
-
console.log(`${
|
|
3948
|
+
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)} from ${paint7("cyan", opts.file)}`);
|
|
4257
3949
|
return;
|
|
4258
3950
|
}
|
|
4259
3951
|
if (opts.update) {
|
|
4260
3952
|
const featuresPath = getFeaturesPath();
|
|
4261
3953
|
writeFileSync5(featuresPath, opts.update);
|
|
4262
|
-
console.log(`${
|
|
3954
|
+
console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)}`);
|
|
4263
3955
|
return;
|
|
4264
3956
|
}
|
|
4265
3957
|
const content = readFeatures2();
|
|
4266
3958
|
if (!content) {
|
|
4267
|
-
console.log(
|
|
4268
|
-
console.log(
|
|
3959
|
+
console.log(paint7("dim", `No features document found. One will be created when an agent completes a task.`));
|
|
3960
|
+
console.log(paint7("dim", `Path: ${getFeaturesPath()}`));
|
|
4269
3961
|
return;
|
|
4270
3962
|
}
|
|
4271
3963
|
console.log(content);
|
|
4272
3964
|
});
|
|
4273
3965
|
|
|
4274
3966
|
// cli/commands/no-mr.ts
|
|
4275
|
-
import { Command as
|
|
3967
|
+
import { Command as Command22 } from "commander";
|
|
4276
3968
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
4277
3969
|
import { resolve as resolve6 } from "path";
|
|
4278
3970
|
var NO_MR_FILE = ".mr-no-mr";
|
|
4279
|
-
var noMrCommand = new
|
|
3971
|
+
var noMrCommand = new Command22("no-mr").description("Signal that a task does not require a merge/pull request and describe what was done instead").argument("<task-id>", "Task ID").argument("<description>", "Description of what was done instead of creating an MR/PR").action(async (taskId, description) => {
|
|
4280
3972
|
const filePath = resolve6(process.cwd(), NO_MR_FILE);
|
|
4281
3973
|
writeFileSync6(filePath, description, "utf-8");
|
|
4282
3974
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
@@ -4288,8 +3980,8 @@ var noMrCommand = new Command24("no-mr").description("Signal that a task does no
|
|
|
4288
3980
|
});
|
|
4289
3981
|
|
|
4290
3982
|
// cli/commands/mobile.ts
|
|
4291
|
-
import { Command as
|
|
4292
|
-
function
|
|
3983
|
+
import { Command as Command23 } from "commander";
|
|
3984
|
+
function paint8(color, text) {
|
|
4293
3985
|
const colors = {
|
|
4294
3986
|
cyan: "\x1B[36m",
|
|
4295
3987
|
green: "\x1B[32m",
|
|
@@ -4300,7 +3992,7 @@ function paint10(color, text) {
|
|
|
4300
3992
|
};
|
|
4301
3993
|
return `${colors[color] ?? ""}${text}${colors.reset}`;
|
|
4302
3994
|
}
|
|
4303
|
-
var mobileCommand = new
|
|
3995
|
+
var mobileCommand = new Command23("mobile").description(
|
|
4304
3996
|
"Start the Web-to-Mobile Conversion Wizard for the linked project"
|
|
4305
3997
|
).argument("[project-id]", "Project ID (defaults to linked project)").option("--framework <framework>", "Framework: react-native or native").option("--url <url>", "Web app URL for analysis").action(
|
|
4306
3998
|
async (projectIdArg, opts) => {
|
|
@@ -4312,7 +4004,7 @@ var mobileCommand = new Command25("mobile").description(
|
|
|
4312
4004
|
process.exit(1);
|
|
4313
4005
|
}
|
|
4314
4006
|
console.log(
|
|
4315
|
-
|
|
4007
|
+
paint8("cyan", "mobile") + paint8("dim", " \u2014 starting conversion wizard")
|
|
4316
4008
|
);
|
|
4317
4009
|
const task = await api.post("/api/tasks", {
|
|
4318
4010
|
title: "Convert to Mobile App",
|
|
@@ -4343,13 +4035,13 @@ var mobileCommand = new Command25("mobile").description(
|
|
|
4343
4035
|
}
|
|
4344
4036
|
console.log(
|
|
4345
4037
|
`
|
|
4346
|
-
${
|
|
4038
|
+
${paint8("green", "\u2713")} Wizard initialized. Open the web UI to continue:
|
|
4347
4039
|
\u2192 Task ID: ${task.id}
|
|
4348
4040
|
\u2192 Use the "Convert to Mobile" button on the project page`
|
|
4349
4041
|
);
|
|
4350
4042
|
}
|
|
4351
4043
|
);
|
|
4352
|
-
var statusSubcommand = new
|
|
4044
|
+
var statusSubcommand = new Command23("status").description("Show mobile conversion status for a task").argument("<task-id>", "Parent conversion task ID").action(async (taskId) => {
|
|
4353
4045
|
const resources = await api.get(
|
|
4354
4046
|
`/api/tasks/${taskId}/resources`
|
|
4355
4047
|
);
|
|
@@ -4362,7 +4054,7 @@ var statusSubcommand = new Command25("status").description("Show mobile conversi
|
|
|
4362
4054
|
}
|
|
4363
4055
|
try {
|
|
4364
4056
|
const state = JSON.parse(wizardState.content);
|
|
4365
|
-
console.log(
|
|
4057
|
+
console.log(paint8("cyan", "Mobile Conversion Status"));
|
|
4366
4058
|
console.log(` Phase: ${state.phase}`);
|
|
4367
4059
|
if (state.framework) {
|
|
4368
4060
|
console.log(` Framework: ${state.framework}`);
|
|
@@ -4385,13 +4077,13 @@ var statusSubcommand = new Command25("status").description("Show mobile conversi
|
|
|
4385
4077
|
mobileCommand.addCommand(statusSubcommand);
|
|
4386
4078
|
|
|
4387
4079
|
// cli/commands/scan.ts
|
|
4388
|
-
import { Command as
|
|
4080
|
+
import { Command as Command24 } from "commander";
|
|
4389
4081
|
|
|
4390
4082
|
// lib/scanner/index.ts
|
|
4391
|
-
import { spawn as
|
|
4083
|
+
import { spawn as spawn7 } from "child_process";
|
|
4392
4084
|
|
|
4393
4085
|
// lib/scanner/config.ts
|
|
4394
|
-
import { readFileSync as readFileSync10, existsSync as
|
|
4086
|
+
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
4395
4087
|
import { join as join10 } from "path";
|
|
4396
4088
|
var ALL_FINDING_TYPES = [
|
|
4397
4089
|
"idea",
|
|
@@ -4409,7 +4101,7 @@ var DEFAULTS = {
|
|
|
4409
4101
|
};
|
|
4410
4102
|
function loadScanConfig(projectPath) {
|
|
4411
4103
|
const configPath2 = join10(projectPath, ".mr-scan.json");
|
|
4412
|
-
if (!
|
|
4104
|
+
if (!existsSync13(configPath2)) {
|
|
4413
4105
|
return { ...DEFAULTS };
|
|
4414
4106
|
}
|
|
4415
4107
|
try {
|
|
@@ -4455,13 +4147,13 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
|
|
|
4455
4147
|
}
|
|
4456
4148
|
|
|
4457
4149
|
// lib/scanner/codebase-analysis.ts
|
|
4458
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as
|
|
4150
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
|
|
4459
4151
|
import { join as join11, relative } from "path";
|
|
4460
4152
|
import { execSync as execSync6 } from "child_process";
|
|
4461
4153
|
function resolveDir(projectPath, candidates) {
|
|
4462
4154
|
for (const candidate of candidates) {
|
|
4463
4155
|
const dir = join11(projectPath, candidate);
|
|
4464
|
-
if (
|
|
4156
|
+
if (existsSync14(dir)) return dir;
|
|
4465
4157
|
}
|
|
4466
4158
|
return null;
|
|
4467
4159
|
}
|
|
@@ -4495,7 +4187,7 @@ function discoverRoutes(projectPath) {
|
|
|
4495
4187
|
}
|
|
4496
4188
|
function extractModels(projectPath) {
|
|
4497
4189
|
const schemaPath = join11(projectPath, "prisma", "schema.prisma");
|
|
4498
|
-
if (
|
|
4190
|
+
if (existsSync14(schemaPath)) {
|
|
4499
4191
|
const content = readFileSync11(schemaPath, "utf-8");
|
|
4500
4192
|
const models2 = [];
|
|
4501
4193
|
const modelRegex = /^model\s+(\w+)\s*\{/gm;
|
|
@@ -4509,7 +4201,7 @@ function extractModels(projectPath) {
|
|
|
4509
4201
|
const drizzleDirs = ["src/db", "src/schema", "db", "drizzle"];
|
|
4510
4202
|
for (const dir of drizzleDirs) {
|
|
4511
4203
|
const fullDir = join11(projectPath, dir);
|
|
4512
|
-
if (!
|
|
4204
|
+
if (!existsSync14(fullDir)) continue;
|
|
4513
4205
|
try {
|
|
4514
4206
|
const entries = readdirSync2(fullDir, { withFileTypes: true });
|
|
4515
4207
|
for (const entry of entries) {
|
|
@@ -4550,7 +4242,7 @@ function discoverComponents(projectPath) {
|
|
|
4550
4242
|
function extractInternalLinks(projectPath) {
|
|
4551
4243
|
const links = /* @__PURE__ */ new Set();
|
|
4552
4244
|
function searchDir(dir) {
|
|
4553
|
-
if (!
|
|
4245
|
+
if (!existsSync14(dir)) return;
|
|
4554
4246
|
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
4555
4247
|
for (const entry of entries) {
|
|
4556
4248
|
if (entry.isDirectory()) {
|
|
@@ -4854,10 +4546,10 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
|
|
|
4854
4546
|
${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
|
|
4855
4547
|
|
|
4856
4548
|
**Components:**
|
|
4857
|
-
${codebaseAnalysis.components.slice(0, 30).map((
|
|
4549
|
+
${codebaseAnalysis.components.slice(0, 30).map((c10) => `- ${c10}`).join("\n")}
|
|
4858
4550
|
|
|
4859
4551
|
**Recent Git Commits:**
|
|
4860
|
-
${codebaseAnalysis.recentCommits.slice(0, 15).map((
|
|
4552
|
+
${codebaseAnalysis.recentCommits.slice(0, 15).map((c10) => `- ${c10}`).join("\n")}
|
|
4861
4553
|
|
|
4862
4554
|
**Completed Tasks:**
|
|
4863
4555
|
${context.completedTasks.slice(0, 20).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
@@ -5067,7 +4759,7 @@ async function fetchScanContext(opts) {
|
|
|
5067
4759
|
}
|
|
5068
4760
|
function runClaude(prompt2) {
|
|
5069
4761
|
return new Promise((resolve7, reject) => {
|
|
5070
|
-
const child =
|
|
4762
|
+
const child = spawn7("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
5071
4763
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5072
4764
|
});
|
|
5073
4765
|
let output = "";
|
|
@@ -5123,7 +4815,7 @@ function parseSynthesisOutput(output) {
|
|
|
5123
4815
|
}
|
|
5124
4816
|
|
|
5125
4817
|
// cli/commands/scan.ts
|
|
5126
|
-
var
|
|
4818
|
+
var c8 = {
|
|
5127
4819
|
reset: "\x1B[0m",
|
|
5128
4820
|
bold: "\x1B[1m",
|
|
5129
4821
|
dim: "\x1B[2m",
|
|
@@ -5134,53 +4826,53 @@ var c10 = {
|
|
|
5134
4826
|
magenta: "\x1B[35m",
|
|
5135
4827
|
gray: "\x1B[90m"
|
|
5136
4828
|
};
|
|
5137
|
-
function
|
|
5138
|
-
return `${
|
|
4829
|
+
function paint9(color, text) {
|
|
4830
|
+
return `${c8[color]}${text}${c8.reset}`;
|
|
5139
4831
|
}
|
|
5140
|
-
function
|
|
5141
|
-
return
|
|
4832
|
+
function timestamp2() {
|
|
4833
|
+
return paint9("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
5142
4834
|
}
|
|
5143
4835
|
function scanTag() {
|
|
5144
|
-
return
|
|
4836
|
+
return paint9("magenta", "[scan]");
|
|
5145
4837
|
}
|
|
5146
|
-
function
|
|
5147
|
-
console.log(`${
|
|
4838
|
+
function log(msg) {
|
|
4839
|
+
console.log(`${timestamp2()} ${scanTag()} ${msg}`);
|
|
5148
4840
|
}
|
|
5149
|
-
function
|
|
5150
|
-
console.log(`${
|
|
4841
|
+
function logOk(msg) {
|
|
4842
|
+
console.log(`${timestamp2()} ${scanTag()} ${paint9("green", "\u2713")} ${msg}`);
|
|
5151
4843
|
}
|
|
5152
|
-
function
|
|
5153
|
-
console.error(`${
|
|
4844
|
+
function logErr(msg) {
|
|
4845
|
+
console.error(`${timestamp2()} ${scanTag()} ${paint9("red", "\u2717")} ${msg}`);
|
|
5154
4846
|
}
|
|
5155
|
-
var scanCommand = new
|
|
4847
|
+
var scanCommand = new Command24("scan").description("Run a product scan on the current project \u2014 analyzes codebase, crawls the app, and surfaces findings").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing scan report ID (created by UI trigger)").option("--no-crawl", "Skip live crawl (codebase analysis only)").action(async (opts) => {
|
|
5156
4848
|
const config = loadConfig();
|
|
5157
4849
|
if (!config.apiKey) {
|
|
5158
|
-
|
|
4850
|
+
logErr('Not authenticated. Run "mr login" first.');
|
|
5159
4851
|
process.exit(1);
|
|
5160
4852
|
}
|
|
5161
4853
|
const banner = [
|
|
5162
4854
|
``,
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
4855
|
+
paint9("magenta", ` \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554`),
|
|
4856
|
+
paint9("magenta", ` \u255A\u2550\u2557\u2551 \u2560\u2550\u2563\u2551\u2551\u2551`),
|
|
4857
|
+
paint9("magenta", ` \u255A\u2550\u255D\u255A\u2550\u255D\u2569 \u2569\u255D\u255A\u255D`),
|
|
4858
|
+
paint9("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
4859
|
+
paint9("dim", ` autonomous product scanner`),
|
|
5168
4860
|
``
|
|
5169
4861
|
].join("\n");
|
|
5170
4862
|
console.log(banner);
|
|
5171
4863
|
const projectId = opts.project || getLinkedProjectId();
|
|
5172
4864
|
if (!projectId) {
|
|
5173
|
-
|
|
4865
|
+
logErr('No project linked. Run "mr link" or pass --project <id>.');
|
|
5174
4866
|
process.exit(1);
|
|
5175
4867
|
}
|
|
5176
4868
|
let project;
|
|
5177
4869
|
try {
|
|
5178
4870
|
project = await api.get(`/api/projects/${projectId}`);
|
|
5179
4871
|
} catch {
|
|
5180
|
-
|
|
4872
|
+
logErr(`Failed to fetch project ${projectId}`);
|
|
5181
4873
|
process.exit(1);
|
|
5182
4874
|
}
|
|
5183
|
-
|
|
4875
|
+
log(`Scanning project: ${paint9("cyan", project.name)}`);
|
|
5184
4876
|
let projectPath = project.localPath;
|
|
5185
4877
|
if (!projectPath) {
|
|
5186
4878
|
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
@@ -5196,12 +4888,12 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5196
4888
|
let reportId;
|
|
5197
4889
|
if (opts.report) {
|
|
5198
4890
|
reportId = opts.report;
|
|
5199
|
-
|
|
4891
|
+
log(`Using existing scan report ${paint9("yellow", reportId.slice(0, 8))}`);
|
|
5200
4892
|
} else {
|
|
5201
4893
|
try {
|
|
5202
4894
|
const scans = await api.get(`/api/scans?projectId=${projectId}&status=processing`);
|
|
5203
4895
|
if (scans.length > 0) {
|
|
5204
|
-
|
|
4896
|
+
logErr("A scan is already in progress for this project. Wait for it to complete.");
|
|
5205
4897
|
process.exit(1);
|
|
5206
4898
|
}
|
|
5207
4899
|
} catch {
|
|
@@ -5212,9 +4904,9 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5212
4904
|
status: "pending"
|
|
5213
4905
|
});
|
|
5214
4906
|
reportId = report.id;
|
|
5215
|
-
|
|
4907
|
+
log(`Created scan report ${paint9("yellow", reportId.slice(0, 8))}`);
|
|
5216
4908
|
} catch (err) {
|
|
5217
|
-
|
|
4909
|
+
logErr(`Failed to create scan report: ${err.message}`);
|
|
5218
4910
|
process.exit(1);
|
|
5219
4911
|
}
|
|
5220
4912
|
}
|
|
@@ -5225,7 +4917,7 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5225
4917
|
try {
|
|
5226
4918
|
const current = await api.get(`/api/scans/${reportId}`);
|
|
5227
4919
|
if (current.status === "cancelled") {
|
|
5228
|
-
|
|
4920
|
+
log(paint9("yellow", "Scan was cancelled \u2014 aborting."));
|
|
5229
4921
|
process.exit(0);
|
|
5230
4922
|
}
|
|
5231
4923
|
} catch {
|
|
@@ -5239,9 +4931,9 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5239
4931
|
apiUrl: config.apiUrl,
|
|
5240
4932
|
apiKey: config.apiKey,
|
|
5241
4933
|
runBrowse: runBrowseCommand2,
|
|
5242
|
-
onLog:
|
|
4934
|
+
onLog: log,
|
|
5243
4935
|
onProgress: (phase, detail) => {
|
|
5244
|
-
|
|
4936
|
+
log(`${paint9("dim", `[${phase}]`)} ${detail}`);
|
|
5245
4937
|
}
|
|
5246
4938
|
});
|
|
5247
4939
|
let wasCancelled = false;
|
|
@@ -5253,7 +4945,7 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5253
4945
|
} catch {
|
|
5254
4946
|
}
|
|
5255
4947
|
if (wasCancelled) {
|
|
5256
|
-
|
|
4948
|
+
log(paint9("yellow", "Scan was cancelled by user \u2014 discarding results."));
|
|
5257
4949
|
process.exit(0);
|
|
5258
4950
|
}
|
|
5259
4951
|
await api.patch(`/api/scans/${reportId}`, {
|
|
@@ -5264,37 +4956,37 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5264
4956
|
scanDurationMs: result.scanDurationMs,
|
|
5265
4957
|
routesCrawled: result.routesCrawled
|
|
5266
4958
|
});
|
|
5267
|
-
|
|
4959
|
+
logOk(`Scan complete \u2014 ${paint9("cyan", String(result.findings.length))} findings`);
|
|
5268
4960
|
console.log("");
|
|
5269
|
-
console.log(` ${
|
|
4961
|
+
console.log(` ${paint9("bold", "Summary:")} ${result.summary}`);
|
|
5270
4962
|
console.log("");
|
|
5271
4963
|
const high = result.findings.filter((f) => f.priority === "high");
|
|
5272
4964
|
const medium = result.findings.filter((f) => f.priority === "medium");
|
|
5273
4965
|
const low = result.findings.filter((f) => f.priority === "low");
|
|
5274
4966
|
if (high.length > 0) {
|
|
5275
|
-
console.log(` ${
|
|
4967
|
+
console.log(` ${paint9("bold", paint9("red", `High Priority (${high.length})`))}`);
|
|
5276
4968
|
for (const f of high) {
|
|
5277
|
-
console.log(` ${
|
|
5278
|
-
console.log(` ${
|
|
4969
|
+
console.log(` ${paint9("red", "\u25CF")} [${f.type}] ${f.title}`);
|
|
4970
|
+
console.log(` ${paint9("dim", f.description.slice(0, 120))}`);
|
|
5279
4971
|
}
|
|
5280
4972
|
console.log("");
|
|
5281
4973
|
}
|
|
5282
4974
|
if (medium.length > 0) {
|
|
5283
|
-
console.log(` ${
|
|
4975
|
+
console.log(` ${paint9("bold", paint9("yellow", `Medium Priority (${medium.length})`))}`);
|
|
5284
4976
|
for (const f of medium) {
|
|
5285
|
-
console.log(` ${
|
|
4977
|
+
console.log(` ${paint9("yellow", "\u25CF")} [${f.type}] ${f.title}`);
|
|
5286
4978
|
}
|
|
5287
4979
|
console.log("");
|
|
5288
4980
|
}
|
|
5289
4981
|
if (low.length > 0) {
|
|
5290
|
-
console.log(` ${
|
|
4982
|
+
console.log(` ${paint9("dim", `Low Priority (${low.length})`)} `);
|
|
5291
4983
|
for (const f of low) {
|
|
5292
|
-
console.log(` ${
|
|
4984
|
+
console.log(` ${paint9("dim", `\u25CB [${f.type}] ${f.title}`)}`);
|
|
5293
4985
|
}
|
|
5294
4986
|
console.log("");
|
|
5295
4987
|
}
|
|
5296
4988
|
} catch (err) {
|
|
5297
|
-
|
|
4989
|
+
logErr(`Scan failed: ${err.message}`);
|
|
5298
4990
|
try {
|
|
5299
4991
|
await api.patch(`/api/scans/${reportId}`, {
|
|
5300
4992
|
status: "failed",
|
|
@@ -5307,8 +4999,8 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5307
4999
|
});
|
|
5308
5000
|
|
|
5309
5001
|
// cli/commands/idea.ts
|
|
5310
|
-
import { Command as
|
|
5311
|
-
var
|
|
5002
|
+
import { Command as Command25 } from "commander";
|
|
5003
|
+
var c9 = {
|
|
5312
5004
|
reset: "\x1B[0m",
|
|
5313
5005
|
bold: "\x1B[1m",
|
|
5314
5006
|
dim: "\x1B[2m",
|
|
@@ -5320,27 +5012,27 @@ var c11 = {
|
|
|
5320
5012
|
gray: "\x1B[90m",
|
|
5321
5013
|
magenta: "\x1B[35m"
|
|
5322
5014
|
};
|
|
5323
|
-
function
|
|
5324
|
-
return `${
|
|
5015
|
+
function paint10(color, text) {
|
|
5016
|
+
return `${c9[color]}${text}${c9.reset}`;
|
|
5325
5017
|
}
|
|
5326
5018
|
function statusBadge2(status) {
|
|
5327
5019
|
switch (status) {
|
|
5328
5020
|
case "draft":
|
|
5329
|
-
return
|
|
5021
|
+
return paint10("gray", "\u25CB draft");
|
|
5330
5022
|
case "generating":
|
|
5331
|
-
return
|
|
5023
|
+
return paint10("cyan", "\u27F3 generating");
|
|
5332
5024
|
case "generated":
|
|
5333
|
-
return
|
|
5025
|
+
return paint10("green", "\u2713 generated");
|
|
5334
5026
|
case "promoted":
|
|
5335
|
-
return
|
|
5027
|
+
return paint10("magenta", "\u2191 promoted");
|
|
5336
5028
|
case "archived":
|
|
5337
|
-
return
|
|
5029
|
+
return paint10("dim", "\u2298 archived");
|
|
5338
5030
|
default:
|
|
5339
|
-
return
|
|
5031
|
+
return paint10("gray", status);
|
|
5340
5032
|
}
|
|
5341
5033
|
}
|
|
5342
|
-
var ideaCommand = new
|
|
5343
|
-
new
|
|
5034
|
+
var ideaCommand = new Command25("idea").description("Manage ideas \u2014 brainstorm, generate prototypes & plans").addCommand(
|
|
5035
|
+
new Command25("list").description("List ideas for the linked project").option("--all", "Show ideas for all projects").option("--status <status>", "Filter by status").action(async (opts) => {
|
|
5344
5036
|
const params = new URLSearchParams();
|
|
5345
5037
|
if (!opts.all) {
|
|
5346
5038
|
const projectId = getLinkedProjectId();
|
|
@@ -5351,23 +5043,23 @@ var ideaCommand = new Command27("idea").description("Manage ideas \u2014 brainst
|
|
|
5351
5043
|
if (opts.status) params.set("status", opts.status);
|
|
5352
5044
|
const ideas = await api.get(`/api/ideas?${params.toString()}`);
|
|
5353
5045
|
if (ideas.length === 0) {
|
|
5354
|
-
console.log(
|
|
5046
|
+
console.log(paint10("gray", "No ideas found."));
|
|
5355
5047
|
return;
|
|
5356
5048
|
}
|
|
5357
5049
|
console.log();
|
|
5358
5050
|
for (const idea of ideas) {
|
|
5359
5051
|
const date = new Date(idea.createdAt).toLocaleDateString();
|
|
5360
5052
|
console.log(
|
|
5361
|
-
` ${
|
|
5053
|
+
` ${paint10("bold", idea.title)} ${statusBadge2(idea.status)} ${paint10("gray", idea.id.slice(0, 8))} ${paint10("dim", date)}`
|
|
5362
5054
|
);
|
|
5363
5055
|
if (idea.description) {
|
|
5364
|
-
console.log(` ${
|
|
5056
|
+
console.log(` ${paint10("dim", idea.description.slice(0, 80) + (idea.description.length > 80 ? "\u2026" : ""))}`);
|
|
5365
5057
|
}
|
|
5366
5058
|
console.log();
|
|
5367
5059
|
}
|
|
5368
5060
|
})
|
|
5369
5061
|
).addCommand(
|
|
5370
|
-
new
|
|
5062
|
+
new Command25("create").description("Create a new idea").argument("<title>", "Title of the idea").option("--description <desc>", "Description of the idea").option("--project <projectId>", "Project ID (defaults to linked project)").option("--generate", "Immediately start generating plan & prototype").action(async (title, opts) => {
|
|
5371
5063
|
const projectId = opts.project ?? getLinkedProjectId() ?? null;
|
|
5372
5064
|
const idea = await api.post("/api/ideas", {
|
|
5373
5065
|
title,
|
|
@@ -5375,65 +5067,65 @@ var ideaCommand = new Command27("idea").description("Manage ideas \u2014 brainst
|
|
|
5375
5067
|
projectId
|
|
5376
5068
|
});
|
|
5377
5069
|
console.log();
|
|
5378
|
-
console.log(` ${
|
|
5379
|
-
console.log(` ${
|
|
5070
|
+
console.log(` ${paint10("green", "\u2713")} Created idea: ${paint10("bold", idea.title)}`);
|
|
5071
|
+
console.log(` ${paint10("gray", "ID:")} ${idea.id}`);
|
|
5380
5072
|
if (opts.generate) {
|
|
5381
5073
|
await api.post(`/api/ideas/${idea.id}/generate`);
|
|
5382
|
-
console.log(` ${
|
|
5074
|
+
console.log(` ${paint10("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
|
|
5383
5075
|
}
|
|
5384
5076
|
console.log();
|
|
5385
5077
|
})
|
|
5386
5078
|
).addCommand(
|
|
5387
|
-
new
|
|
5079
|
+
new Command25("generate").description("Start generating plan & prototype for an idea").argument("<id>", "Idea ID").action(async (id) => {
|
|
5388
5080
|
const idea = await api.post(`/api/ideas/${id}/generate`);
|
|
5389
5081
|
console.log();
|
|
5390
|
-
console.log(` ${
|
|
5391
|
-
console.log(` ${
|
|
5082
|
+
console.log(` ${paint10("cyan", "\u27F3")} Generating: ${paint10("bold", idea.title)}`);
|
|
5083
|
+
console.log(` ${paint10("gray", "The watch agent will pick this up shortly.")}`);
|
|
5392
5084
|
console.log();
|
|
5393
5085
|
})
|
|
5394
5086
|
).addCommand(
|
|
5395
|
-
new
|
|
5087
|
+
new Command25("feedback").description("Send feedback to iterate on an idea's generated content").argument("<id>", "Idea ID").argument("<feedback>", "Feedback text").action(async (id, feedback) => {
|
|
5396
5088
|
const idea = await api.post(`/api/ideas/${id}/feedback`, { feedback });
|
|
5397
5089
|
console.log();
|
|
5398
|
-
console.log(` ${
|
|
5399
|
-
console.log(` ${
|
|
5090
|
+
console.log(` ${paint10("cyan", "\u27F3")} Feedback sent for: ${paint10("bold", idea.title)}`);
|
|
5091
|
+
console.log(` ${paint10("gray", "The watch agent will re-generate with your feedback.")}`);
|
|
5400
5092
|
console.log();
|
|
5401
5093
|
})
|
|
5402
5094
|
).addCommand(
|
|
5403
|
-
new
|
|
5095
|
+
new Command25("promote").description("Promote an idea to a task").argument("<id>", "Idea ID").action(async (id) => {
|
|
5404
5096
|
const result = await api.post(`/api/ideas/${id}/promote`);
|
|
5405
5097
|
console.log();
|
|
5406
|
-
console.log(` ${
|
|
5407
|
-
console.log(` ${
|
|
5098
|
+
console.log(` ${paint10("green", "\u2713")} Promoted idea to task: ${paint10("bold", result.task.title)}`);
|
|
5099
|
+
console.log(` ${paint10("gray", "Task ID:")} ${result.task.id}`);
|
|
5408
5100
|
console.log();
|
|
5409
5101
|
})
|
|
5410
5102
|
).addCommand(
|
|
5411
|
-
new
|
|
5103
|
+
new Command25("spin-up").description("Spin up a new project with a GitHub repo from an idea").argument("<id>", "Idea ID").option("--name <name>", "Custom project name (defaults to idea title)").action(async (id, opts) => {
|
|
5412
5104
|
const body = {};
|
|
5413
5105
|
if (opts.name) body.name = opts.name;
|
|
5414
5106
|
const result = await api.post(`/api/ideas/${id}/spin-up`, body);
|
|
5415
5107
|
console.log();
|
|
5416
|
-
console.log(` ${
|
|
5417
|
-
console.log(` ${
|
|
5108
|
+
console.log(` ${paint10("green", "\u2713")} Spinning up project: ${paint10("bold", result.project.name)}`);
|
|
5109
|
+
console.log(` ${paint10("gray", "Project ID:")} ${result.project.id}`);
|
|
5418
5110
|
if (result.tasks && result.tasks.length > 0) {
|
|
5419
|
-
console.log(` ${
|
|
5111
|
+
console.log(` ${paint10("green", "\u2713")} Created ${result.tasks.length} follow-up task(s):`);
|
|
5420
5112
|
for (const task of result.tasks) {
|
|
5421
|
-
console.log(` ${
|
|
5113
|
+
console.log(` ${paint10("gray", "\u2022")} ${task.title}`);
|
|
5422
5114
|
}
|
|
5423
5115
|
}
|
|
5424
|
-
console.log(` ${
|
|
5116
|
+
console.log(` ${paint10("cyan", "\u27F3")} Repo creation is queued \u2014 the watch daemon will pick it up.`);
|
|
5425
5117
|
console.log();
|
|
5426
5118
|
})
|
|
5427
5119
|
);
|
|
5428
5120
|
|
|
5429
5121
|
// cli/commands/doctor.ts
|
|
5430
|
-
import { Command as
|
|
5431
|
-
import { existsSync as
|
|
5432
|
-
import { homedir as
|
|
5122
|
+
import { Command as Command26 } from "commander";
|
|
5123
|
+
import { existsSync as existsSync15 } from "fs";
|
|
5124
|
+
import { homedir as homedir2 } from "os";
|
|
5433
5125
|
import { join as join12 } from "path";
|
|
5434
5126
|
async function checkConfigExists() {
|
|
5435
|
-
const configPath2 = join12(
|
|
5436
|
-
const exists =
|
|
5127
|
+
const configPath2 = join12(homedir2(), ".mr-manager", "config.json");
|
|
5128
|
+
const exists = existsSync15(configPath2);
|
|
5437
5129
|
if (!exists) {
|
|
5438
5130
|
return {
|
|
5439
5131
|
name: "Config file",
|
|
@@ -5475,12 +5167,12 @@ async function checkProjectLink() {
|
|
|
5475
5167
|
optional: true
|
|
5476
5168
|
};
|
|
5477
5169
|
}
|
|
5478
|
-
var doctorCommand = new
|
|
5170
|
+
var doctorCommand = new Command26("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
|
|
5479
5171
|
const banner = [
|
|
5480
5172
|
``,
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5173
|
+
paint5("cyan", ` MR DOCTOR`),
|
|
5174
|
+
paint5("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
5175
|
+
paint5("dim", ` diagnosing your mr environment`),
|
|
5484
5176
|
``
|
|
5485
5177
|
].join("\n");
|
|
5486
5178
|
console.log(banner);
|
|
@@ -5502,15 +5194,15 @@ var doctorCommand = new Command28("doctor").description("Diagnose Mr. Manager CL
|
|
|
5502
5194
|
const allOk = printResults(checks);
|
|
5503
5195
|
console.log("");
|
|
5504
5196
|
if (allOk) {
|
|
5505
|
-
console.log(
|
|
5197
|
+
console.log(paint5("green", " Everything looks good!"));
|
|
5506
5198
|
console.log("");
|
|
5507
5199
|
return;
|
|
5508
5200
|
}
|
|
5509
|
-
const fixes = checks.filter((
|
|
5201
|
+
const fixes = checks.filter((c10) => !c10.ok && c10.fix && !c10.optional);
|
|
5510
5202
|
if (fixes.length > 0) {
|
|
5511
|
-
console.log(
|
|
5203
|
+
console.log(paint5("yellow", " To fix:"));
|
|
5512
5204
|
for (const fix of fixes) {
|
|
5513
|
-
console.log(` ${
|
|
5205
|
+
console.log(` ${paint5("dim", "\u2192")} ${paint5("bold", fix.name)}: ${fix.fix}`);
|
|
5514
5206
|
}
|
|
5515
5207
|
console.log("");
|
|
5516
5208
|
}
|
|
@@ -5518,13 +5210,13 @@ var doctorCommand = new Command28("doctor").description("Diagnose Mr. Manager CL
|
|
|
5518
5210
|
});
|
|
5519
5211
|
|
|
5520
5212
|
// cli/index.ts
|
|
5521
|
-
var configPath = join13(
|
|
5522
|
-
var isFirstRun = !
|
|
5213
|
+
var configPath = join13(homedir3(), ".mr-manager", "config.json");
|
|
5214
|
+
var isFirstRun = !existsSync16(configPath);
|
|
5523
5215
|
var userArgs = process.argv.slice(2);
|
|
5524
5216
|
var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
|
|
5525
5217
|
var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
|
|
5526
5218
|
if (isFirstRun && !shouldBypass) {
|
|
5527
|
-
const
|
|
5219
|
+
const c10 = {
|
|
5528
5220
|
reset: "\x1B[0m",
|
|
5529
5221
|
bold: "\x1B[1m",
|
|
5530
5222
|
dim: "\x1B[2m",
|
|
@@ -5534,29 +5226,29 @@ if (isFirstRun && !shouldBypass) {
|
|
|
5534
5226
|
magenta: "\x1B[35m"
|
|
5535
5227
|
};
|
|
5536
5228
|
console.log("");
|
|
5537
|
-
console.log(`${
|
|
5538
|
-
console.log(`${
|
|
5539
|
-
console.log(`${
|
|
5540
|
-
console.log(`${
|
|
5229
|
+
console.log(`${c10.cyan} \u2554\u2566\u2557\u2566\u2550\u2557 \u2554\u2566\u2557\u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2566\u2550\u2557${c10.reset}`);
|
|
5230
|
+
console.log(`${c10.magenta} \u2551\u2551\u2551\u2560\u2566\u255D \u2551\u2551\u2551\u2560\u2550\u2563\u2551\u2551\u2551\u2560\u2550\u2563\u2551 \u2566\u2551\u2563 \u2560\u2566\u255D${c10.reset}`);
|
|
5231
|
+
console.log(`${c10.cyan} \u2569 \u2569\u2569\u255A\u2550 \u2569 \u2569\u2569 \u2569\u255D\u255A\u255D\u2569 \u2569\u255A\u2550\u255D\u255A\u2550\u255D\u2569\u255A\u2550${c10.reset}`);
|
|
5232
|
+
console.log(`${c10.dim} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c10.reset}`);
|
|
5541
5233
|
console.log("");
|
|
5542
|
-
console.log(`${
|
|
5543
|
-
console.log(`${
|
|
5234
|
+
console.log(`${c10.bold} Welcome to Mr. Manager!${c10.reset}`);
|
|
5235
|
+
console.log(`${c10.dim} Let's get you set up in a few quick steps.${c10.reset}`);
|
|
5544
5236
|
console.log("");
|
|
5545
|
-
console.log(` ${
|
|
5546
|
-
console.log(` ${
|
|
5237
|
+
console.log(` ${c10.yellow}Step 1:${c10.reset} Authenticate via Google OAuth`);
|
|
5238
|
+
console.log(` ${c10.dim}Run:${c10.reset} ${c10.cyan}mr login${c10.reset}`);
|
|
5547
5239
|
console.log("");
|
|
5548
|
-
console.log(` ${
|
|
5549
|
-
console.log(` ${
|
|
5240
|
+
console.log(` ${c10.yellow}Step 2:${c10.reset} Verify your environment`);
|
|
5241
|
+
console.log(` ${c10.dim}Run:${c10.reset} ${c10.cyan}mr setup${c10.reset}`);
|
|
5550
5242
|
console.log("");
|
|
5551
|
-
console.log(` ${
|
|
5552
|
-
console.log(` ${
|
|
5243
|
+
console.log(` ${c10.yellow}Step 3:${c10.reset} Link a repo and start watching`);
|
|
5244
|
+
console.log(` ${c10.dim}Run:${c10.reset} ${c10.cyan}mr link${c10.reset} ${c10.dim}&&${c10.reset} ${c10.cyan}mr watch${c10.reset}`);
|
|
5553
5245
|
console.log("");
|
|
5554
|
-
console.log(`${
|
|
5246
|
+
console.log(`${c10.dim} Or run ${c10.reset}${c10.cyan}mr login${c10.reset}${c10.dim} to get started now.${c10.reset}`);
|
|
5555
5247
|
console.log("");
|
|
5556
5248
|
process.exit(0);
|
|
5557
5249
|
}
|
|
5558
|
-
var program = new
|
|
5559
|
-
program.name("mr").description("Mr. Manager - Task and project management CLI").version(
|
|
5250
|
+
var program = new Command27();
|
|
5251
|
+
program.name("mr").description("Mr. Manager - Task and project management CLI").version(CLI_VERSION);
|
|
5560
5252
|
program.addCommand(initCommand);
|
|
5561
5253
|
program.addCommand(authCommand);
|
|
5562
5254
|
program.addCommand(loginCommand);
|
|
@@ -5572,8 +5264,6 @@ program.addCommand(undelegateCommand);
|
|
|
5572
5264
|
program.addCommand(createCommand);
|
|
5573
5265
|
program.addCommand(completeCommand);
|
|
5574
5266
|
program.addCommand(subtaskCompleteCommand);
|
|
5575
|
-
program.addCommand(digestCommand);
|
|
5576
|
-
program.addCommand(upCommand);
|
|
5577
5267
|
program.addCommand(prototypeCommand);
|
|
5578
5268
|
program.addCommand(setupCommand);
|
|
5579
5269
|
program.addCommand(updateCommand);
|