@dunnewold-labs/mr-manager 0.4.31 → 0.4.35
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 +698 -164
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command30 } from "commander";
|
|
5
5
|
import { existsSync as existsSync17 } from "fs";
|
|
6
6
|
import { homedir as homedir3 } from "os";
|
|
7
7
|
import { join as join12 } from "path";
|
|
@@ -185,7 +185,7 @@ import { fileURLToPath } from "url";
|
|
|
185
185
|
// cli/package.json
|
|
186
186
|
var package_default = {
|
|
187
187
|
name: "@dunnewold-labs/mr-manager",
|
|
188
|
-
version: "0.4.
|
|
188
|
+
version: "0.4.35",
|
|
189
189
|
description: "Mr. Manager - Task and project management CLI",
|
|
190
190
|
bin: {
|
|
191
191
|
mr: "./dist/index.mjs"
|
|
@@ -880,7 +880,7 @@ async function runTest(options) {
|
|
|
880
880
|
recordingEnabled = true,
|
|
881
881
|
recordingContext = "test-run"
|
|
882
882
|
} = options;
|
|
883
|
-
const
|
|
883
|
+
const log3 = onProgress || (() => {
|
|
884
884
|
});
|
|
885
885
|
const result = {
|
|
886
886
|
status: "passed",
|
|
@@ -897,30 +897,30 @@ async function runTest(options) {
|
|
|
897
897
|
let wtPath = null;
|
|
898
898
|
const worktreeName = `mr-test-${taskId.slice(0, 8)}`;
|
|
899
899
|
const timeoutHandle = setTimeout(() => {
|
|
900
|
-
|
|
900
|
+
log3("Test timed out after 5 minutes");
|
|
901
901
|
if (devProc) devProc.kill("SIGTERM");
|
|
902
902
|
}, 5 * 60 * 1e3);
|
|
903
903
|
try {
|
|
904
|
-
|
|
904
|
+
log3("Extracting branch from MR/PR link...");
|
|
905
905
|
const branch = extractBranchFromLink(taskLink, localPath);
|
|
906
906
|
if (!branch) {
|
|
907
907
|
throw new Error(`Could not extract branch from link: ${taskLink}`);
|
|
908
908
|
}
|
|
909
|
-
|
|
910
|
-
|
|
909
|
+
log3(`Branch: ${branch}`);
|
|
910
|
+
log3("Creating git worktree...");
|
|
911
911
|
wtPath = createWorktree(localPath, branch, worktreeName).path;
|
|
912
|
-
|
|
913
|
-
|
|
912
|
+
log3(`Worktree created at ${wtPath}`);
|
|
913
|
+
log3("Installing dependencies...");
|
|
914
914
|
try {
|
|
915
915
|
installDependencies(wtPath);
|
|
916
916
|
} catch (err) {
|
|
917
|
-
|
|
917
|
+
log3(`Warning: dependency install failed: ${err.message}`);
|
|
918
918
|
}
|
|
919
|
-
|
|
919
|
+
log3("Starting dev server...");
|
|
920
920
|
const port = await findAvailablePort(4e3);
|
|
921
921
|
devProc = await startDevServer(wtPath, port);
|
|
922
922
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
923
|
-
|
|
923
|
+
log3(`Dev server running on ${baseUrl}`);
|
|
924
924
|
let recordingStarted = false;
|
|
925
925
|
if (recordingEnabled) {
|
|
926
926
|
try {
|
|
@@ -929,28 +929,28 @@ async function runTest(options) {
|
|
|
929
929
|
throw new Error(recordingStart.stdout || "recording-start failed");
|
|
930
930
|
}
|
|
931
931
|
recordingStarted = true;
|
|
932
|
-
|
|
932
|
+
log3("Proof recording started");
|
|
933
933
|
} catch (err) {
|
|
934
934
|
result.proof = {
|
|
935
935
|
state: "proof_missing_capture_failed",
|
|
936
936
|
details: `Proof recording could not start: ${err.message}`
|
|
937
937
|
};
|
|
938
|
-
|
|
938
|
+
log3(result.proof.details);
|
|
939
939
|
}
|
|
940
940
|
} else {
|
|
941
941
|
result.proof = {
|
|
942
942
|
state: "proof_missing_disabled",
|
|
943
943
|
details: "Proof recording disabled for this run."
|
|
944
944
|
};
|
|
945
|
-
|
|
945
|
+
log3(result.proof.details);
|
|
946
946
|
}
|
|
947
947
|
await browseRunner(["goto", baseUrl]);
|
|
948
948
|
const plan = customPlan || buildDefaultTestPlan(baseUrl);
|
|
949
|
-
|
|
949
|
+
log3(`Executing ${plan.length}-step test plan...`);
|
|
950
950
|
for (let i = 0; i < plan.length; i++) {
|
|
951
951
|
const step = plan[i];
|
|
952
952
|
const stepDesc = step.description || `${step.command} ${(step.args || []).join(" ")}`;
|
|
953
|
-
|
|
953
|
+
log3(`Step ${i + 1}/${plan.length}: ${stepDesc}`);
|
|
954
954
|
try {
|
|
955
955
|
if (step.command.startsWith("assert")) {
|
|
956
956
|
const assertResult = await evaluateAssertion(step, i, browseRunner);
|
|
@@ -1000,7 +1000,7 @@ async function runTest(options) {
|
|
|
1000
1000
|
}
|
|
1001
1001
|
} catch (err) {
|
|
1002
1002
|
result.errors.push(`Step ${i + 1} (${step.command}): ${err.message}`);
|
|
1003
|
-
|
|
1003
|
+
log3(`Step ${i + 1} error: ${err.message}`);
|
|
1004
1004
|
}
|
|
1005
1005
|
}
|
|
1006
1006
|
if (recordingStarted) {
|
|
@@ -1012,7 +1012,7 @@ async function runTest(options) {
|
|
|
1012
1012
|
throw new Error(recordingStop.stdout || "recording-stop did not return a file path");
|
|
1013
1013
|
}
|
|
1014
1014
|
result.proof.localPath = savedPath;
|
|
1015
|
-
|
|
1015
|
+
log3(`Proof recording finalized at ${savedPath}`);
|
|
1016
1016
|
if (uploadVideo) {
|
|
1017
1017
|
const videoUrl = await uploadVideo(savedPath);
|
|
1018
1018
|
if (videoUrl) {
|
|
@@ -1034,14 +1034,14 @@ async function runTest(options) {
|
|
|
1034
1034
|
}
|
|
1035
1035
|
]
|
|
1036
1036
|
};
|
|
1037
|
-
|
|
1037
|
+
log3(`Proof recording uploaded to ${videoUrl}`);
|
|
1038
1038
|
} else {
|
|
1039
1039
|
result.proof = {
|
|
1040
1040
|
state: "proof_missing_upload_failed",
|
|
1041
1041
|
details: "Proof recording captured, but upload failed.",
|
|
1042
1042
|
localPath: savedPath
|
|
1043
1043
|
};
|
|
1044
|
-
|
|
1044
|
+
log3(result.proof.details);
|
|
1045
1045
|
}
|
|
1046
1046
|
} else {
|
|
1047
1047
|
result.proof = {
|
|
@@ -1049,14 +1049,14 @@ async function runTest(options) {
|
|
|
1049
1049
|
details: "Proof recording captured, but no upload handler was configured.",
|
|
1050
1050
|
localPath: savedPath
|
|
1051
1051
|
};
|
|
1052
|
-
|
|
1052
|
+
log3(result.proof.details);
|
|
1053
1053
|
}
|
|
1054
1054
|
} catch (err) {
|
|
1055
1055
|
result.proof = {
|
|
1056
1056
|
state: "proof_missing_capture_failed",
|
|
1057
1057
|
details: `Proof recording could not be finalized: ${err.message}`
|
|
1058
1058
|
};
|
|
1059
|
-
|
|
1059
|
+
log3(result.proof.details);
|
|
1060
1060
|
}
|
|
1061
1061
|
}
|
|
1062
1062
|
const totalAssertions = result.assertions.length;
|
|
@@ -1162,6 +1162,19 @@ function taskLikelyDoesNotNeedCodeChanges(task) {
|
|
|
1162
1162
|
return NON_CODE_TASK_KEYWORDS.some((keyword) => haystack.includes(keyword)) || NON_CODE_TASK_PATTERNS.some((pattern) => pattern.test(haystack));
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
|
+
// lib/task-branch.ts
|
|
1166
|
+
function isPrOrMrUrl(input) {
|
|
1167
|
+
try {
|
|
1168
|
+
const url = new URL(input.trim());
|
|
1169
|
+
const path = url.pathname;
|
|
1170
|
+
if (url.hostname.includes("github") && /\/pull\/\d+/.test(path)) return true;
|
|
1171
|
+
if (url.hostname.includes("gitlab") && /\/merge_requests\/\d+/.test(path)) return true;
|
|
1172
|
+
} catch {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1165
1178
|
// cli/browse-runner.ts
|
|
1166
1179
|
import { execSync as execSync3, spawn as spawn3 } from "child_process";
|
|
1167
1180
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -1477,11 +1490,21 @@ function ownerPrefix(task) {
|
|
|
1477
1490
|
return first || "mr";
|
|
1478
1491
|
}
|
|
1479
1492
|
function taskBranchName(task) {
|
|
1480
|
-
|
|
1481
|
-
|
|
1493
|
+
const branch = task.attachedBranch?.trim();
|
|
1494
|
+
if (branch && !isPrOrMrUrl(branch)) {
|
|
1495
|
+
return branch;
|
|
1482
1496
|
}
|
|
1483
1497
|
return `${ownerPrefix(task)}/${slugify(task.title)}`;
|
|
1484
1498
|
}
|
|
1499
|
+
function resolveBranchFromPrUrl(prUrl, repoDir, vcs) {
|
|
1500
|
+
const cmd = vcs === "gitlab" ? `glab mr view "${prUrl}" --output json 2>/dev/null | jq -r '.source_branch // empty'` : `gh pr view "${prUrl}" --json headRefName -q .headRefName 2>/dev/null`;
|
|
1501
|
+
return new Promise((resolve9) => {
|
|
1502
|
+
exec(cmd, { cwd: repoDir }, (err, stdout) => {
|
|
1503
|
+
const branch = stdout?.trim();
|
|
1504
|
+
resolve9(branch || null);
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1485
1508
|
function formatElapsed(ms) {
|
|
1486
1509
|
const totalMinutes = Math.max(1, Math.floor(ms / 6e4));
|
|
1487
1510
|
const hours = Math.floor(totalMinutes / 60);
|
|
@@ -1992,7 +2015,7 @@ ${task.notes}` : "";
|
|
|
1992
2015
|
],
|
|
1993
2016
|
``,
|
|
1994
2017
|
`3. Implement the task. You may read, write, and run code across any relevant repos under ${repoDir}.`,
|
|
1995
|
-
` - If you do significant research or investigation,
|
|
2018
|
+
` - If you do significant research or investigation, save your findings as a resource using: \`mr update ${task.id} --resource research "Research \u2014 <short title>" '<markdown content>'\``,
|
|
1996
2019
|
...pendingSubtasks.length > 0 ? [
|
|
1997
2020
|
` - Work through each subtask in order. After completing each subtask, immediately run \`mr subtask-complete ${task.id} <subtask-id>\` to mark it done before moving on.`
|
|
1998
2021
|
] : [],
|
|
@@ -2000,7 +2023,7 @@ ${task.notes}` : "";
|
|
|
2000
2023
|
`4. Once implementation is complete, for each repo that has changes:`,
|
|
2001
2024
|
` a. Commit all changes with a clear, descriptive message.`,
|
|
2002
2025
|
` b. Push the branch: \`git push -u origin HEAD\``,
|
|
2003
|
-
...hasFeedback ? [
|
|
2026
|
+
...hasFeedback || hasAttachedBranch && task.attachedBranchLink && isPrOrMrUrl(task.attachedBranchLink) ? [
|
|
2004
2027
|
` c. The existing ${vcs === "gitlab" ? "merge request" : "pull request"} will be updated automatically when you push to the branch. No need to create a new one.`
|
|
2005
2028
|
] : [
|
|
2006
2029
|
` c. Write a structured ${vcs === "gitlab" ? "merge request" : "pull request"} description to \`${prBodyPath}\` using the template below.`,
|
|
@@ -2019,7 +2042,7 @@ ${task.notes}` : "";
|
|
|
2019
2042
|
``,
|
|
2020
2043
|
`This tells the watch system to skip looking for a ${vcs === "gitlab" ? "MR" : "PR"} and records what action was taken. You should still clean up any worktrees and exit normally.`,
|
|
2021
2044
|
``,
|
|
2022
|
-
...hasFeedback ? [] : [
|
|
2045
|
+
...hasFeedback || hasAttachedBranch && task.attachedBranchLink && isPrOrMrUrl(task.attachedBranchLink) ? [] : [
|
|
2023
2046
|
`## PR Description Template`,
|
|
2024
2047
|
``,
|
|
2025
2048
|
`Write this template to \`${prBodyPath}\`, then replace the placeholders with the actual details from your implementation.`,
|
|
@@ -2269,7 +2292,7 @@ function buildPrototypePrompt(proto, repoDir) {
|
|
|
2269
2292
|
}
|
|
2270
2293
|
};
|
|
2271
2294
|
const config = typeConfig[prototypeType] ?? typeConfig.web_app;
|
|
2272
|
-
const
|
|
2295
|
+
const typeLabel2 = {
|
|
2273
2296
|
web_app: "Web App",
|
|
2274
2297
|
mobile_app: "Mobile App",
|
|
2275
2298
|
desktop_app: "Desktop App",
|
|
@@ -2283,7 +2306,7 @@ function buildPrototypePrompt(proto, repoDir) {
|
|
|
2283
2306
|
`## Prototype Request`,
|
|
2284
2307
|
`Title: ${proto.title}`,
|
|
2285
2308
|
`ID: ${proto.id}`,
|
|
2286
|
-
`Type: ${
|
|
2309
|
+
`Type: ${typeLabel2[prototypeType] ?? prototypeType}`,
|
|
2287
2310
|
``,
|
|
2288
2311
|
`## Design Prompt`,
|
|
2289
2312
|
`${proto.prompt}`,
|
|
@@ -2351,7 +2374,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
2351
2374
|
desktop_app: "You are a desktop UI designer and frontend engineer. Your job is to REFINE an existing desktop app prototype based on user feedback.",
|
|
2352
2375
|
logo: "You are a graphic designer specializing in logo and brand identity. Your job is to REFINE an existing logo prototype based on user feedback."
|
|
2353
2376
|
};
|
|
2354
|
-
const
|
|
2377
|
+
const typeLabel2 = {
|
|
2355
2378
|
web_app: "Web App",
|
|
2356
2379
|
mobile_app: "Mobile App",
|
|
2357
2380
|
desktop_app: "Desktop App",
|
|
@@ -2365,7 +2388,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
2365
2388
|
`## Prototype Request`,
|
|
2366
2389
|
`Title: ${proto.title}`,
|
|
2367
2390
|
`ID: ${proto.id}`,
|
|
2368
|
-
`Type: ${
|
|
2391
|
+
`Type: ${typeLabel2[prototypeType] ?? prototypeType}`,
|
|
2369
2392
|
``,
|
|
2370
2393
|
`## Original Design Prompt`,
|
|
2371
2394
|
`${proto.prompt}`,
|
|
@@ -2639,7 +2662,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2639
2662
|
logWarn(watchTag(), `Network unavailable \u2014 pausing active tasks until connectivity returns (${reason})`);
|
|
2640
2663
|
}
|
|
2641
2664
|
for (const taskId of active.keys()) {
|
|
2642
|
-
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "test-"];
|
|
2665
|
+
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "review-", "test-"];
|
|
2643
2666
|
if (nonTaskPrefixes.some((prefix) => taskId.startsWith(prefix))) continue;
|
|
2644
2667
|
pauseTaskForNetwork(taskId, reason);
|
|
2645
2668
|
}
|
|
@@ -2686,10 +2709,17 @@ var watchCommand = new Command8("watch").description(
|
|
|
2686
2709
|
const sid = shortId(task.id);
|
|
2687
2710
|
const slug = slugify(task.title);
|
|
2688
2711
|
const owner = ownerPrefix(task);
|
|
2689
|
-
|
|
2712
|
+
let branchName = taskBranchName(task);
|
|
2690
2713
|
const legacyBranchName = `mr/${sid}/${slug}`;
|
|
2691
2714
|
const prefix = taskTag(sid);
|
|
2692
2715
|
const vcs = detectVcs(repoDir)?.provider ?? "github";
|
|
2716
|
+
if (!task.attachedBranch?.trim() && task.attachedBranchLink && isPrOrMrUrl(task.attachedBranchLink)) {
|
|
2717
|
+
const resolved = await resolveBranchFromPrUrl(task.attachedBranchLink, repoDir, vcs);
|
|
2718
|
+
if (resolved) {
|
|
2719
|
+
branchName = resolved;
|
|
2720
|
+
logInfo(prefix, `Resolved branch ${paint("cyan", resolved)} from attached ${vcs === "gitlab" ? "MR" : "PR"}`);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2693
2723
|
logDispatch(prefix, `"${paint("bold", task.title)}" ${paint("gray", repoDir)} ${paint("dim", `[${vcs}]`)}`);
|
|
2694
2724
|
await postTaskUpdate(task.id, `Agent dispatched \u2014 starting work on "${task.title}"`, "system");
|
|
2695
2725
|
let subtasks = [];
|
|
@@ -2888,29 +2918,6 @@ var watchCommand = new Command8("watch").description(
|
|
|
2888
2918
|
try {
|
|
2889
2919
|
if (code === 0) {
|
|
2890
2920
|
try {
|
|
2891
|
-
const researchPath = resolve2(executionDir, "research.md");
|
|
2892
|
-
if (existsSync7(researchPath)) {
|
|
2893
|
-
try {
|
|
2894
|
-
const researchContent = readFileSync5(researchPath, "utf-8");
|
|
2895
|
-
const existingResearch = existingResources.find((r) => r.type === "research");
|
|
2896
|
-
if (existingResearch) {
|
|
2897
|
-
await api.patch(`/api/tasks/${task.id}/resources/${existingResearch.id}`, {
|
|
2898
|
-
content: researchContent
|
|
2899
|
-
});
|
|
2900
|
-
logSuccess(prefix, `Updated existing research resource`);
|
|
2901
|
-
} else {
|
|
2902
|
-
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2903
|
-
type: "research",
|
|
2904
|
-
title: `Research \u2014 ${task.title}`,
|
|
2905
|
-
content: researchContent
|
|
2906
|
-
});
|
|
2907
|
-
logSuccess(prefix, `Uploaded research.md as task resource`);
|
|
2908
|
-
}
|
|
2909
|
-
unlinkSync(researchPath);
|
|
2910
|
-
} catch (err) {
|
|
2911
|
-
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
2921
|
const noMrPath = resolve2(executionDir, ".mr-no-mr");
|
|
2915
2922
|
const noMrRequested = existsSync7(noMrPath);
|
|
2916
2923
|
let noMrDescription;
|
|
@@ -2921,7 +2928,11 @@ var watchCommand = new Command8("watch").description(
|
|
|
2921
2928
|
}
|
|
2922
2929
|
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2923
2930
|
let prUrl = null;
|
|
2924
|
-
if (!noMrRequested) {
|
|
2931
|
+
if (!noMrRequested && task.attachedBranchLink && isPrOrMrUrl(task.attachedBranchLink)) {
|
|
2932
|
+
prUrl = task.attachedBranchLink;
|
|
2933
|
+
logInfo(prefix, `Using attached ${prLabel} link: ${paint("cyan", prUrl)}`);
|
|
2934
|
+
}
|
|
2935
|
+
if (!noMrRequested && !prUrl) {
|
|
2925
2936
|
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2926
2937
|
if (!prUrl && !task.attachedBranch?.trim()) {
|
|
2927
2938
|
prUrl = await findPrUrlAcrossRepos(legacyBranchName, repoDir, vcs);
|
|
@@ -2968,6 +2979,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2968
2979
|
logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
|
|
2969
2980
|
await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
|
|
2970
2981
|
}
|
|
2982
|
+
} else if (prUrl) {
|
|
2983
|
+
logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
|
|
2971
2984
|
}
|
|
2972
2985
|
const currentTask = await api.get(`/api/tasks/${task.id}`);
|
|
2973
2986
|
if (currentTask.status === "completed" || currentTask.status === "review") {
|
|
@@ -3412,6 +3425,49 @@ var watchCommand = new Command8("watch").description(
|
|
|
3412
3425
|
}
|
|
3413
3426
|
});
|
|
3414
3427
|
}
|
|
3428
|
+
function dispatchCodeReview(review, prefix, key) {
|
|
3429
|
+
logDispatch(prefix, `Running code review on branch ${paint("cyan", review.branch)}`);
|
|
3430
|
+
const reviewProc = spawn4(process.execPath, [process.argv[1], "review", "--project", review.projectId, "--report", review.id, "--branch", review.branch, "--base", review.baseBranch], {
|
|
3431
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3432
|
+
cwd: rootDir
|
|
3433
|
+
});
|
|
3434
|
+
reviewProc.on("error", (err) => {
|
|
3435
|
+
logError(prefix, `Failed to spawn review: ${err.message}`);
|
|
3436
|
+
active.delete(key);
|
|
3437
|
+
queued.delete(key);
|
|
3438
|
+
failed.set(key, err.message);
|
|
3439
|
+
});
|
|
3440
|
+
active.set(key, {
|
|
3441
|
+
process: reviewProc,
|
|
3442
|
+
title: `review-${review.id.slice(0, 8)}`,
|
|
3443
|
+
repoDir: rootDir,
|
|
3444
|
+
startedAt: Date.now(),
|
|
3445
|
+
lastActivityAt: Date.now(),
|
|
3446
|
+
outputBytes: 0
|
|
3447
|
+
});
|
|
3448
|
+
reviewProc.stdout?.on("data", (d) => {
|
|
3449
|
+
const lines = d.toString().trim().split("\n");
|
|
3450
|
+
for (const line of lines) {
|
|
3451
|
+
console.log(`${timestamp()} ${watchTag()} ${prefix} ${line}`);
|
|
3452
|
+
}
|
|
3453
|
+
});
|
|
3454
|
+
reviewProc.stderr?.on("data", (d) => {
|
|
3455
|
+
const lines = d.toString().trim().split("\n");
|
|
3456
|
+
for (const line of lines) {
|
|
3457
|
+
console.error(`${timestamp()} ${watchTag()} ${prefix} ${line}`);
|
|
3458
|
+
}
|
|
3459
|
+
});
|
|
3460
|
+
reviewProc.on("exit", (code) => {
|
|
3461
|
+
active.delete(key);
|
|
3462
|
+
queued.delete(key);
|
|
3463
|
+
if (code === 0) {
|
|
3464
|
+
logSuccess(prefix, `Code review completed`);
|
|
3465
|
+
} else {
|
|
3466
|
+
logError(prefix, `Code review exited with code ${code}`);
|
|
3467
|
+
failed.set(key, `exit code ${code}`);
|
|
3468
|
+
}
|
|
3469
|
+
});
|
|
3470
|
+
}
|
|
3415
3471
|
async function processApprovalQueue() {
|
|
3416
3472
|
if (approvalRunning || approvalQueue.length === 0) return;
|
|
3417
3473
|
approvalRunning = true;
|
|
@@ -3554,7 +3610,7 @@ ${divider}`);
|
|
|
3554
3610
|
}
|
|
3555
3611
|
}
|
|
3556
3612
|
for (const [taskId, entry] of active) {
|
|
3557
|
-
if (taskId.startsWith("proto-") || taskId.startsWith("repo-") || taskId.startsWith("scan-")) continue;
|
|
3613
|
+
if (taskId.startsWith("proto-") || taskId.startsWith("repo-") || taskId.startsWith("scan-") || taskId.startsWith("review-")) continue;
|
|
3558
3614
|
if (!activeTaskIds.has(taskId)) {
|
|
3559
3615
|
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer active, terminating\u2026`);
|
|
3560
3616
|
entry.terminatedForError = true;
|
|
@@ -3563,7 +3619,7 @@ ${divider}`);
|
|
|
3563
3619
|
queued.delete(taskId);
|
|
3564
3620
|
}
|
|
3565
3621
|
}
|
|
3566
|
-
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "test-"];
|
|
3622
|
+
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "review-", "test-"];
|
|
3567
3623
|
for (const taskId of failed.keys()) {
|
|
3568
3624
|
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
3569
3625
|
if (!activeTaskIds.has(taskId)) failed.delete(taskId);
|
|
@@ -3869,6 +3925,29 @@ ${divider}`);
|
|
|
3869
3925
|
}
|
|
3870
3926
|
dispatchScan(scan, prefix, key);
|
|
3871
3927
|
}
|
|
3928
|
+
let pendingReviews = [];
|
|
3929
|
+
try {
|
|
3930
|
+
pendingReviews = await api.get("/api/reviews?status=pending&limit=5");
|
|
3931
|
+
} catch (err) {
|
|
3932
|
+
logError(watchTag(), `Failed to fetch pending reviews: ${err.message}`);
|
|
3933
|
+
}
|
|
3934
|
+
for (const review of pendingReviews) {
|
|
3935
|
+
const key = `review-${review.id}`;
|
|
3936
|
+
if (queued.has(key)) continue;
|
|
3937
|
+
if (finishing.has(key)) continue;
|
|
3938
|
+
if (failed.has(key)) continue;
|
|
3939
|
+
const sid = shortId(review.id);
|
|
3940
|
+
const prefix = `${paint("blue", `[review:${sid}]`)}`;
|
|
3941
|
+
queued.add(key);
|
|
3942
|
+
if (dryRun) {
|
|
3943
|
+
logInfo(
|
|
3944
|
+
watchTag(),
|
|
3945
|
+
`${paint("yellow", "[dry-run]")} would run code review ${paint("yellow", sid)} on branch ${paint("cyan", review.branch)}`
|
|
3946
|
+
);
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
dispatchCodeReview(review, prefix, key);
|
|
3950
|
+
}
|
|
3872
3951
|
let reviewTasks = [];
|
|
3873
3952
|
try {
|
|
3874
3953
|
reviewTasks = await api.get("/api/tasks?status=review");
|
|
@@ -4152,9 +4231,9 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
4152
4231
|
};
|
|
4153
4232
|
for (const p of prototypes) {
|
|
4154
4233
|
const date = new Date(p.createdAt).toLocaleDateString();
|
|
4155
|
-
const
|
|
4234
|
+
const typeLabel2 = typeLabels[p.prototypeType] ?? p.prototypeType ?? "web";
|
|
4156
4235
|
console.log(
|
|
4157
|
-
` ${paint4("bold", p.title)} ${statusBadge(p.status)} ${paint4("blue", `[${
|
|
4236
|
+
` ${paint4("bold", p.title)} ${statusBadge(p.status)} ${paint4("blue", `[${typeLabel2}]`)} ${paint4("gray", p.id.slice(0, 8))} ${paint4("dim", date)}`
|
|
4158
4237
|
);
|
|
4159
4238
|
console.log(` ${paint4("dim", p.prompt.slice(0, 80) + (p.prompt.length > 80 ? "\u2026" : ""))}`);
|
|
4160
4239
|
console.log();
|
|
@@ -4470,7 +4549,7 @@ async function checkApiConnectivity() {
|
|
|
4470
4549
|
}
|
|
4471
4550
|
}
|
|
4472
4551
|
function printResults(checks) {
|
|
4473
|
-
const maxNameLen = Math.max(...checks.map((
|
|
4552
|
+
const maxNameLen = Math.max(...checks.map((c13) => c13.name.length));
|
|
4474
4553
|
let allOk = true;
|
|
4475
4554
|
for (const check of checks) {
|
|
4476
4555
|
const isOptional = check.optional ?? false;
|
|
@@ -4483,16 +4562,16 @@ function printResults(checks) {
|
|
|
4483
4562
|
return allOk;
|
|
4484
4563
|
}
|
|
4485
4564
|
async function autoFix(checks, agent) {
|
|
4486
|
-
const { spawn:
|
|
4487
|
-
const ghInstalled = checks.find((
|
|
4488
|
-
const ghAuthed = checks.find((
|
|
4489
|
-
const mrAuthed = checks.find((
|
|
4490
|
-
const claudeCheck = checks.find((
|
|
4565
|
+
const { spawn: spawn9 } = await import("child_process");
|
|
4566
|
+
const ghInstalled = checks.find((c13) => c13.name === "GitHub CLI (gh)").ok;
|
|
4567
|
+
const ghAuthed = checks.find((c13) => c13.name === "GitHub CLI auth").ok;
|
|
4568
|
+
const mrAuthed = checks.find((c13) => c13.name === "Mr. Manager CLI auth").ok;
|
|
4569
|
+
const claudeCheck = checks.find((c13) => c13.name === "Claude Code (claude)");
|
|
4491
4570
|
if (claudeCheck && !claudeCheck.ok && agent === "claude") {
|
|
4492
4571
|
console.log(paint5("cyan", " Installing Claude Code..."));
|
|
4493
4572
|
console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
4494
4573
|
await new Promise((resolve9) => {
|
|
4495
|
-
const child =
|
|
4574
|
+
const child = spawn9("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
4496
4575
|
child.on("exit", () => resolve9());
|
|
4497
4576
|
});
|
|
4498
4577
|
console.log("");
|
|
@@ -4500,7 +4579,7 @@ async function autoFix(checks, agent) {
|
|
|
4500
4579
|
if (ghInstalled && !ghAuthed) {
|
|
4501
4580
|
console.log(paint5("cyan", " Running gh auth login..."));
|
|
4502
4581
|
await new Promise((resolve9) => {
|
|
4503
|
-
const child =
|
|
4582
|
+
const child = spawn9("gh", ["auth", "login"], { stdio: "inherit" });
|
|
4504
4583
|
child.on("exit", () => resolve9());
|
|
4505
4584
|
});
|
|
4506
4585
|
console.log("");
|
|
@@ -4509,7 +4588,7 @@ async function autoFix(checks, agent) {
|
|
|
4509
4588
|
console.log(paint5("cyan", " Running mr login..."));
|
|
4510
4589
|
const entry = process.argv[1];
|
|
4511
4590
|
await new Promise((resolve9) => {
|
|
4512
|
-
const child =
|
|
4591
|
+
const child = spawn9(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
4513
4592
|
child.on("exit", () => resolve9());
|
|
4514
4593
|
});
|
|
4515
4594
|
console.log("");
|
|
@@ -4550,7 +4629,7 @@ var setupCommand = new Command14("setup").description("Check that all dependenci
|
|
|
4550
4629
|
console.log("");
|
|
4551
4630
|
return;
|
|
4552
4631
|
}
|
|
4553
|
-
const fixes = checks.filter((
|
|
4632
|
+
const fixes = checks.filter((c13) => !c13.ok && c13.fix && !c13.optional);
|
|
4554
4633
|
if (fixes.length > 0) {
|
|
4555
4634
|
console.log(paint5("yellow", " To fix:"));
|
|
4556
4635
|
for (const fix of fixes) {
|
|
@@ -4566,9 +4645,26 @@ var setupCommand = new Command14("setup").description("Check that all dependenci
|
|
|
4566
4645
|
|
|
4567
4646
|
// cli/commands/update.ts
|
|
4568
4647
|
import { Command as Command15 } from "commander";
|
|
4569
|
-
var updateCommand = new Command15("update").description("Post a status update to a task
|
|
4648
|
+
var updateCommand = new Command15("update").description("Post a status update to a task, or attach a resource").argument("<task-id>", "Task ID").argument("[message-or-title]", "Status update message, or resource title when using --resource").argument("[content]", "Resource content (only used with --resource)").option("--source <source>", "Update source: agent, system, or user", "agent").option("--resource <type>", "Create a task resource (e.g. test-plan, note, plan, research)").action(async (taskId, messageOrTitle, content, opts) => {
|
|
4649
|
+
if (opts.resource) {
|
|
4650
|
+
if (!messageOrTitle || !content) {
|
|
4651
|
+
console.error(`Usage: mr update <task-id> --resource <type> "<title>" '<content>'`);
|
|
4652
|
+
process.exit(1);
|
|
4653
|
+
}
|
|
4654
|
+
await api.post(`/api/tasks/${taskId}/resources`, {
|
|
4655
|
+
type: opts.resource,
|
|
4656
|
+
title: messageOrTitle,
|
|
4657
|
+
content
|
|
4658
|
+
});
|
|
4659
|
+
console.log(`\u2713 Resource created (${opts.resource}): ${messageOrTitle}`);
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
if (!messageOrTitle) {
|
|
4663
|
+
console.error("Message is required for status updates.");
|
|
4664
|
+
process.exit(1);
|
|
4665
|
+
}
|
|
4570
4666
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4571
|
-
message,
|
|
4667
|
+
message: messageOrTitle,
|
|
4572
4668
|
source: opts.source
|
|
4573
4669
|
});
|
|
4574
4670
|
console.log(`\u2713 Status update posted`);
|
|
@@ -5276,11 +5372,329 @@ var noMrCommand = new Command22("no-mr").description("Signal that a task does no
|
|
|
5276
5372
|
console.log(` Reason: ${description}`);
|
|
5277
5373
|
});
|
|
5278
5374
|
|
|
5279
|
-
// cli/commands/
|
|
5375
|
+
// cli/commands/review.ts
|
|
5280
5376
|
import { Command as Command23 } from "commander";
|
|
5377
|
+
import { spawn as spawn7, execSync as execSync5 } from "child_process";
|
|
5378
|
+
var c8 = {
|
|
5379
|
+
reset: "\x1B[0m",
|
|
5380
|
+
bold: "\x1B[1m",
|
|
5381
|
+
dim: "\x1B[2m",
|
|
5382
|
+
cyan: "\x1B[36m",
|
|
5383
|
+
green: "\x1B[32m",
|
|
5384
|
+
yellow: "\x1B[33m",
|
|
5385
|
+
red: "\x1B[31m",
|
|
5386
|
+
magenta: "\x1B[35m",
|
|
5387
|
+
gray: "\x1B[90m",
|
|
5388
|
+
blue: "\x1B[34m"
|
|
5389
|
+
};
|
|
5390
|
+
function paint8(color, text) {
|
|
5391
|
+
return `${c8[color]}${text}${c8.reset}`;
|
|
5392
|
+
}
|
|
5393
|
+
function timestamp2() {
|
|
5394
|
+
return paint8("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
5395
|
+
}
|
|
5396
|
+
function tag() {
|
|
5397
|
+
return paint8("blue", "[review]");
|
|
5398
|
+
}
|
|
5399
|
+
function log(msg) {
|
|
5400
|
+
console.log(`${timestamp2()} ${tag()} ${msg}`);
|
|
5401
|
+
}
|
|
5402
|
+
function logOk(msg) {
|
|
5403
|
+
console.log(`${timestamp2()} ${tag()} ${paint8("green", "\u2713")} ${msg}`);
|
|
5404
|
+
}
|
|
5405
|
+
function logErr(msg) {
|
|
5406
|
+
console.error(`${timestamp2()} ${tag()} ${paint8("red", "\u2717")} ${msg}`);
|
|
5407
|
+
}
|
|
5408
|
+
var reviewCommand = new Command23("review").description("Run an automated code review on a branch").option("--project <id>", "Project ID (defaults to linked project)").option("--report <id>", "Use an existing review report ID (created by UI trigger)").option("--branch <name>", "Branch to review (defaults to current branch)").option("--base <name>", "Base branch to diff against (defaults to main)").action(async (opts) => {
|
|
5409
|
+
const config = loadConfig();
|
|
5410
|
+
if (!config.apiKey) {
|
|
5411
|
+
logErr('Not authenticated. Run "mr login" first.');
|
|
5412
|
+
process.exit(1);
|
|
5413
|
+
}
|
|
5414
|
+
const banner = [
|
|
5415
|
+
``,
|
|
5416
|
+
paint8("blue", ` \u2566\u2550\u2557\u2554\u2550\u2557\u2566 \u2566\u2566\u2554\u2550\u2557\u2566 \u2566`),
|
|
5417
|
+
paint8("blue", ` \u2560\u2566\u255D\u2551\u2563 \u255A\u2557\u2554\u255D\u2551\u2551\u2563 \u2551\u2551\u2551`),
|
|
5418
|
+
paint8("blue", ` \u2569\u255A\u2550\u255A\u2550\u255D \u255A\u255D \u2569\u255A\u2550\u255D\u255A\u2569\u255D`),
|
|
5419
|
+
paint8("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
5420
|
+
paint8("dim", ` automated code review`),
|
|
5421
|
+
``
|
|
5422
|
+
].join("\n");
|
|
5423
|
+
console.log(banner);
|
|
5424
|
+
const projectId = opts.project || getLinkedProjectId();
|
|
5425
|
+
if (!projectId) {
|
|
5426
|
+
logErr('No project linked. Run "mr link" or pass --project <id>.');
|
|
5427
|
+
process.exit(1);
|
|
5428
|
+
}
|
|
5429
|
+
let project;
|
|
5430
|
+
try {
|
|
5431
|
+
project = await api.get(`/api/projects/${projectId}`);
|
|
5432
|
+
} catch {
|
|
5433
|
+
logErr(`Failed to fetch project ${projectId}`);
|
|
5434
|
+
process.exit(1);
|
|
5435
|
+
}
|
|
5436
|
+
let projectPath = project.localPath;
|
|
5437
|
+
if (!projectPath) {
|
|
5438
|
+
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
5439
|
+
if (pid === projectId) {
|
|
5440
|
+
projectPath = dir;
|
|
5441
|
+
break;
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
if (!projectPath) {
|
|
5446
|
+
projectPath = process.cwd();
|
|
5447
|
+
}
|
|
5448
|
+
let branch = opts.branch;
|
|
5449
|
+
if (!branch) {
|
|
5450
|
+
try {
|
|
5451
|
+
branch = execSync5("git rev-parse --abbrev-ref HEAD", {
|
|
5452
|
+
cwd: projectPath,
|
|
5453
|
+
encoding: "utf-8"
|
|
5454
|
+
}).trim();
|
|
5455
|
+
} catch {
|
|
5456
|
+
logErr("Could not determine current branch. Pass --branch <name>.");
|
|
5457
|
+
process.exit(1);
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
const baseBranch = opts.base || "main";
|
|
5461
|
+
log(`Reviewing branch: ${paint8("cyan", branch)} against ${paint8("dim", baseBranch)}`);
|
|
5462
|
+
log(`Project: ${paint8("cyan", project.name)}`);
|
|
5463
|
+
let diff;
|
|
5464
|
+
try {
|
|
5465
|
+
diff = execSync5(`git diff ${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
|
|
5466
|
+
cwd: projectPath,
|
|
5467
|
+
encoding: "utf-8",
|
|
5468
|
+
maxBuffer: 10 * 1024 * 1024
|
|
5469
|
+
}).trim();
|
|
5470
|
+
} catch (err) {
|
|
5471
|
+
try {
|
|
5472
|
+
execSync5(`git fetch origin ${baseBranch}`, { cwd: projectPath, encoding: "utf-8", stdio: "pipe" });
|
|
5473
|
+
diff = execSync5(`git diff origin/${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
|
|
5474
|
+
cwd: projectPath,
|
|
5475
|
+
encoding: "utf-8",
|
|
5476
|
+
maxBuffer: 10 * 1024 * 1024
|
|
5477
|
+
}).trim();
|
|
5478
|
+
} catch {
|
|
5479
|
+
logErr(`Failed to get diff between ${baseBranch} and ${branch}: ${err.message}`);
|
|
5480
|
+
process.exit(1);
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
if (!diff) {
|
|
5484
|
+
logOk("No changes found between branches. Nothing to review.");
|
|
5485
|
+
process.exit(0);
|
|
5486
|
+
}
|
|
5487
|
+
const filesChanged = (() => {
|
|
5488
|
+
try {
|
|
5489
|
+
const stat = execSync5(`git diff --stat ${baseBranch}...${branch} -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml'`, {
|
|
5490
|
+
cwd: projectPath,
|
|
5491
|
+
encoding: "utf-8"
|
|
5492
|
+
}).trim();
|
|
5493
|
+
const lines = stat.split("\n");
|
|
5494
|
+
return Math.max(0, lines.length - 1);
|
|
5495
|
+
} catch {
|
|
5496
|
+
const fileHeaders = diff.match(/^diff --git/gm);
|
|
5497
|
+
return fileHeaders?.length ?? 0;
|
|
5498
|
+
}
|
|
5499
|
+
})();
|
|
5500
|
+
log(`Diff size: ${paint8("yellow", `${diff.length.toLocaleString()} chars`)}, ${paint8("yellow", `${filesChanged} files`)}`);
|
|
5501
|
+
let reportId;
|
|
5502
|
+
if (opts.report) {
|
|
5503
|
+
reportId = opts.report;
|
|
5504
|
+
log(`Using existing review report ${paint8("yellow", reportId.slice(0, 8))}`);
|
|
5505
|
+
} else {
|
|
5506
|
+
try {
|
|
5507
|
+
const report = await api.post("/api/reviews", {
|
|
5508
|
+
projectId,
|
|
5509
|
+
branch,
|
|
5510
|
+
baseBranch
|
|
5511
|
+
});
|
|
5512
|
+
reportId = report.id;
|
|
5513
|
+
log(`Created review report ${paint8("yellow", reportId.slice(0, 8))}`);
|
|
5514
|
+
} catch (err) {
|
|
5515
|
+
logErr(`Failed to create review report: ${err.message}`);
|
|
5516
|
+
process.exit(1);
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
try {
|
|
5520
|
+
await api.patch(`/api/reviews/${reportId}`, { status: "processing" });
|
|
5521
|
+
} catch {
|
|
5522
|
+
}
|
|
5523
|
+
const startTime = Date.now();
|
|
5524
|
+
const MAX_DIFF_CHARS = 8e4;
|
|
5525
|
+
let truncatedDiff = diff;
|
|
5526
|
+
if (diff.length > MAX_DIFF_CHARS) {
|
|
5527
|
+
truncatedDiff = diff.slice(0, MAX_DIFF_CHARS) + "\n\n... (diff truncated, review covers first " + MAX_DIFF_CHARS.toLocaleString() + " characters)";
|
|
5528
|
+
log(paint8("yellow", `Diff truncated to ${MAX_DIFF_CHARS.toLocaleString()} chars for review`));
|
|
5529
|
+
}
|
|
5530
|
+
try {
|
|
5531
|
+
log("Running code review with Claude...");
|
|
5532
|
+
const prompt2 = buildReviewPrompt(branch, baseBranch, truncatedDiff);
|
|
5533
|
+
const output = await runClaude(prompt2);
|
|
5534
|
+
const result = parseReviewOutput(output);
|
|
5535
|
+
const duration = Date.now() - startTime;
|
|
5536
|
+
let wasCancelled = false;
|
|
5537
|
+
try {
|
|
5538
|
+
const current = await api.get(`/api/reviews/${reportId}`);
|
|
5539
|
+
wasCancelled = current.status === "cancelled";
|
|
5540
|
+
} catch {
|
|
5541
|
+
}
|
|
5542
|
+
if (wasCancelled) {
|
|
5543
|
+
log(paint8("yellow", "Review was cancelled \u2014 discarding results."));
|
|
5544
|
+
process.exit(0);
|
|
5545
|
+
}
|
|
5546
|
+
await api.patch(`/api/reviews/${reportId}`, {
|
|
5547
|
+
status: "completed",
|
|
5548
|
+
summary: result.summary,
|
|
5549
|
+
findings: result.findings,
|
|
5550
|
+
filesReviewed: filesChanged,
|
|
5551
|
+
reviewDurationMs: duration
|
|
5552
|
+
});
|
|
5553
|
+
logOk(`Review completed in ${paint8("cyan", formatDuration(duration))}`);
|
|
5554
|
+
logOk(`Found ${paint8("yellow", String(result.findings.length))} findings`);
|
|
5555
|
+
if (result.findings.length > 0) {
|
|
5556
|
+
console.log("");
|
|
5557
|
+
const critical = result.findings.filter((f) => f.severity === "critical").length;
|
|
5558
|
+
const high = result.findings.filter((f) => f.severity === "high").length;
|
|
5559
|
+
const medium = result.findings.filter((f) => f.severity === "medium").length;
|
|
5560
|
+
const low = result.findings.filter((f) => f.severity === "low").length;
|
|
5561
|
+
if (critical > 0) console.log(` ${paint8("red", "\u25CF")} ${critical} critical`);
|
|
5562
|
+
if (high > 0) console.log(` ${paint8("red", "\u25CF")} ${high} high`);
|
|
5563
|
+
if (medium > 0) console.log(` ${paint8("yellow", "\u25CF")} ${medium} medium`);
|
|
5564
|
+
if (low > 0) console.log(` ${paint8("dim", "\u25CF")} ${low} low`);
|
|
5565
|
+
console.log("");
|
|
5566
|
+
}
|
|
5567
|
+
if (result.summary) {
|
|
5568
|
+
console.log(paint8("dim", " " + result.summary));
|
|
5569
|
+
console.log("");
|
|
5570
|
+
}
|
|
5571
|
+
} catch (err) {
|
|
5572
|
+
const duration = Date.now() - startTime;
|
|
5573
|
+
const errorMessage = err.message || "Unknown error";
|
|
5574
|
+
logErr(`Review failed: ${errorMessage}`);
|
|
5575
|
+
try {
|
|
5576
|
+
await api.patch(`/api/reviews/${reportId}`, {
|
|
5577
|
+
status: "failed",
|
|
5578
|
+
errorMessage,
|
|
5579
|
+
reviewDurationMs: duration
|
|
5580
|
+
});
|
|
5581
|
+
} catch {
|
|
5582
|
+
}
|
|
5583
|
+
process.exit(1);
|
|
5584
|
+
}
|
|
5585
|
+
});
|
|
5586
|
+
function formatDuration(ms) {
|
|
5587
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
5588
|
+
const s = Math.round(ms / 1e3);
|
|
5589
|
+
if (s < 60) return `${s}s`;
|
|
5590
|
+
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
5591
|
+
}
|
|
5592
|
+
function buildReviewPrompt(branch, baseBranch, diff) {
|
|
5593
|
+
return `You are a senior code reviewer. Review the following git diff for branch "${branch}" compared to "${baseBranch}".
|
|
5594
|
+
|
|
5595
|
+
Analyze the code changes and produce a JSON response with your review findings.
|
|
5596
|
+
|
|
5597
|
+
Focus on:
|
|
5598
|
+
- Bugs and logical errors
|
|
5599
|
+
- Security vulnerabilities (XSS, injection, auth issues, secrets exposure)
|
|
5600
|
+
- Performance issues (N+1 queries, missing indexes, unnecessary re-renders)
|
|
5601
|
+
- Code style and best practices violations
|
|
5602
|
+
- Suggestions for improvement
|
|
5603
|
+
- Nitpicks (minor style/naming issues)
|
|
5604
|
+
|
|
5605
|
+
For each finding, provide:
|
|
5606
|
+
- A unique ID (e.g. "f1", "f2", etc.)
|
|
5607
|
+
- Type: "bug", "security", "performance", "style", "suggestion", or "nitpick"
|
|
5608
|
+
- Severity: "critical", "high", "medium", or "low"
|
|
5609
|
+
- Title: a brief one-line summary
|
|
5610
|
+
- Description: detailed explanation of the issue
|
|
5611
|
+
- File: the file path where the issue was found
|
|
5612
|
+
- Line: the approximate line number in the new code (optional)
|
|
5613
|
+
- Suggestion: suggested fix or improvement (optional, include actual code when possible)
|
|
5614
|
+
|
|
5615
|
+
Return ONLY a JSON object with this structure (no markdown, no explanation before/after):
|
|
5616
|
+
{
|
|
5617
|
+
"summary": "Brief overall assessment of the code changes (2-3 sentences)",
|
|
5618
|
+
"findings": [
|
|
5619
|
+
{
|
|
5620
|
+
"id": "f1",
|
|
5621
|
+
"type": "bug",
|
|
5622
|
+
"severity": "high",
|
|
5623
|
+
"title": "Brief title",
|
|
5624
|
+
"description": "Detailed description",
|
|
5625
|
+
"file": "path/to/file.ts",
|
|
5626
|
+
"line": 42,
|
|
5627
|
+
"suggestion": "Suggested fix code"
|
|
5628
|
+
}
|
|
5629
|
+
]
|
|
5630
|
+
}
|
|
5631
|
+
|
|
5632
|
+
If the code looks good with no issues, return an empty findings array with a positive summary.
|
|
5633
|
+
|
|
5634
|
+
Here is the diff to review:
|
|
5635
|
+
|
|
5636
|
+
\`\`\`diff
|
|
5637
|
+
${diff}
|
|
5638
|
+
\`\`\``;
|
|
5639
|
+
}
|
|
5640
|
+
function runClaude(prompt2) {
|
|
5641
|
+
return new Promise((resolve9, reject) => {
|
|
5642
|
+
const child = spawn7("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
5643
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5644
|
+
});
|
|
5645
|
+
let output = "";
|
|
5646
|
+
let errOutput = "";
|
|
5647
|
+
child.stdout?.on("data", (d) => {
|
|
5648
|
+
output += d.toString();
|
|
5649
|
+
});
|
|
5650
|
+
child.stderr?.on("data", (d) => {
|
|
5651
|
+
errOutput += d.toString();
|
|
5652
|
+
});
|
|
5653
|
+
child.on("exit", (code) => {
|
|
5654
|
+
if (code === 0) resolve9(output.trim());
|
|
5655
|
+
else reject(new Error(`claude exited with code ${code}
|
|
5656
|
+
${errOutput.trim()}`));
|
|
5657
|
+
});
|
|
5658
|
+
});
|
|
5659
|
+
}
|
|
5660
|
+
function parseReviewOutput(output) {
|
|
5661
|
+
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
5662
|
+
if (!jsonMatch) {
|
|
5663
|
+
return {
|
|
5664
|
+
summary: "Failed to parse review output",
|
|
5665
|
+
findings: []
|
|
5666
|
+
};
|
|
5667
|
+
}
|
|
5668
|
+
try {
|
|
5669
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
5670
|
+
return {
|
|
5671
|
+
summary: parsed.summary || "",
|
|
5672
|
+
findings: (parsed.findings || []).map((f) => ({
|
|
5673
|
+
id: f.id || `f${Math.random().toString(36).slice(2, 8)}`,
|
|
5674
|
+
type: f.type || "suggestion",
|
|
5675
|
+
severity: f.severity || "medium",
|
|
5676
|
+
title: f.title || "Untitled finding",
|
|
5677
|
+
description: f.description || "",
|
|
5678
|
+
file: f.file || "unknown",
|
|
5679
|
+
line: f.line,
|
|
5680
|
+
endLine: f.endLine,
|
|
5681
|
+
suggestion: f.suggestion,
|
|
5682
|
+
status: "new"
|
|
5683
|
+
}))
|
|
5684
|
+
};
|
|
5685
|
+
} catch {
|
|
5686
|
+
return {
|
|
5687
|
+
summary: "Failed to parse review JSON",
|
|
5688
|
+
findings: []
|
|
5689
|
+
};
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
// cli/commands/scan.ts
|
|
5694
|
+
import { Command as Command24 } from "commander";
|
|
5281
5695
|
|
|
5282
5696
|
// lib/scanner/index.ts
|
|
5283
|
-
import { spawn as
|
|
5697
|
+
import { spawn as spawn8 } from "child_process";
|
|
5284
5698
|
|
|
5285
5699
|
// lib/scanner/config.ts
|
|
5286
5700
|
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
@@ -5349,7 +5763,7 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
|
|
|
5349
5763
|
// lib/scanner/codebase-analysis.ts
|
|
5350
5764
|
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
|
|
5351
5765
|
import { join as join10, relative } from "path";
|
|
5352
|
-
import { execSync as
|
|
5766
|
+
import { execSync as execSync6 } from "child_process";
|
|
5353
5767
|
function resolveDir(projectPath, candidates) {
|
|
5354
5768
|
for (const candidate of candidates) {
|
|
5355
5769
|
const dir = join10(projectPath, candidate);
|
|
@@ -5475,7 +5889,7 @@ function extractInternalLinks(projectPath) {
|
|
|
5475
5889
|
}
|
|
5476
5890
|
function getRecentCommits(projectPath, count = 20) {
|
|
5477
5891
|
try {
|
|
5478
|
-
const output =
|
|
5892
|
+
const output = execSync6(
|
|
5479
5893
|
`git log --oneline -${count} --no-decorate`,
|
|
5480
5894
|
{ cwd: projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
5481
5895
|
);
|
|
@@ -5743,10 +6157,10 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
|
|
|
5743
6157
|
${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
|
|
5744
6158
|
|
|
5745
6159
|
**Components:**
|
|
5746
|
-
${codebaseAnalysis.components.slice(0, 15).map((
|
|
6160
|
+
${codebaseAnalysis.components.slice(0, 15).map((c13) => `- ${c13}`).join("\n")}
|
|
5747
6161
|
|
|
5748
6162
|
**Recent Git Commits:**
|
|
5749
|
-
${codebaseAnalysis.recentCommits.slice(0, 8).map((
|
|
6163
|
+
${codebaseAnalysis.recentCommits.slice(0, 8).map((c13) => `- ${c13}`).join("\n")}
|
|
5750
6164
|
|
|
5751
6165
|
**Completed Tasks:**
|
|
5752
6166
|
${context.completedTasks.slice(0, 10).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
@@ -5891,7 +6305,7 @@ async function runScanPipeline(opts) {
|
|
|
5891
6305
|
crawlResults,
|
|
5892
6306
|
context.priorFindings
|
|
5893
6307
|
);
|
|
5894
|
-
const synthesisResult = await
|
|
6308
|
+
const synthesisResult = await runClaude2(prompt2);
|
|
5895
6309
|
const parsed = parseSynthesisOutput(synthesisResult);
|
|
5896
6310
|
const scanDurationMs = Date.now() - startTime;
|
|
5897
6311
|
opts.onLog(`Scan complete in ${Math.round(scanDurationMs / 1e3)}s \u2014 ${parsed.findings.length} findings`);
|
|
@@ -5952,9 +6366,9 @@ async function fetchScanContext(opts) {
|
|
|
5952
6366
|
priorFindings
|
|
5953
6367
|
};
|
|
5954
6368
|
}
|
|
5955
|
-
function
|
|
6369
|
+
function runClaude2(prompt2) {
|
|
5956
6370
|
return new Promise((resolve9, reject) => {
|
|
5957
|
-
const child =
|
|
6371
|
+
const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
5958
6372
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5959
6373
|
});
|
|
5960
6374
|
let output = "";
|
|
@@ -6010,7 +6424,7 @@ function parseSynthesisOutput(output) {
|
|
|
6010
6424
|
}
|
|
6011
6425
|
|
|
6012
6426
|
// cli/commands/scan.ts
|
|
6013
|
-
var
|
|
6427
|
+
var c9 = {
|
|
6014
6428
|
reset: "\x1B[0m",
|
|
6015
6429
|
bold: "\x1B[1m",
|
|
6016
6430
|
dim: "\x1B[2m",
|
|
@@ -6021,53 +6435,53 @@ var c8 = {
|
|
|
6021
6435
|
magenta: "\x1B[35m",
|
|
6022
6436
|
gray: "\x1B[90m"
|
|
6023
6437
|
};
|
|
6024
|
-
function
|
|
6025
|
-
return `${
|
|
6438
|
+
function paint9(color, text) {
|
|
6439
|
+
return `${c9[color]}${text}${c9.reset}`;
|
|
6026
6440
|
}
|
|
6027
|
-
function
|
|
6028
|
-
return
|
|
6441
|
+
function timestamp3() {
|
|
6442
|
+
return paint9("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
6029
6443
|
}
|
|
6030
6444
|
function scanTag() {
|
|
6031
|
-
return
|
|
6445
|
+
return paint9("magenta", "[scan]");
|
|
6032
6446
|
}
|
|
6033
|
-
function
|
|
6034
|
-
console.log(`${
|
|
6447
|
+
function log2(msg) {
|
|
6448
|
+
console.log(`${timestamp3()} ${scanTag()} ${msg}`);
|
|
6035
6449
|
}
|
|
6036
|
-
function
|
|
6037
|
-
console.log(`${
|
|
6450
|
+
function logOk2(msg) {
|
|
6451
|
+
console.log(`${timestamp3()} ${scanTag()} ${paint9("green", "\u2713")} ${msg}`);
|
|
6038
6452
|
}
|
|
6039
|
-
function
|
|
6040
|
-
console.error(`${
|
|
6453
|
+
function logErr2(msg) {
|
|
6454
|
+
console.error(`${timestamp3()} ${scanTag()} ${paint9("red", "\u2717")} ${msg}`);
|
|
6041
6455
|
}
|
|
6042
|
-
var scanCommand = new
|
|
6456
|
+
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) => {
|
|
6043
6457
|
const config = loadConfig();
|
|
6044
6458
|
if (!config.apiKey) {
|
|
6045
|
-
|
|
6459
|
+
logErr2('Not authenticated. Run "mr login" first.');
|
|
6046
6460
|
process.exit(1);
|
|
6047
6461
|
}
|
|
6048
6462
|
const banner = [
|
|
6049
6463
|
``,
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6464
|
+
paint9("magenta", ` \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554`),
|
|
6465
|
+
paint9("magenta", ` \u255A\u2550\u2557\u2551 \u2560\u2550\u2563\u2551\u2551\u2551`),
|
|
6466
|
+
paint9("magenta", ` \u255A\u2550\u255D\u255A\u2550\u255D\u2569 \u2569\u255D\u255A\u255D`),
|
|
6467
|
+
paint9("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
6468
|
+
paint9("dim", ` autonomous product scanner`),
|
|
6055
6469
|
``
|
|
6056
6470
|
].join("\n");
|
|
6057
6471
|
console.log(banner);
|
|
6058
6472
|
const projectId = opts.project || getLinkedProjectId();
|
|
6059
6473
|
if (!projectId) {
|
|
6060
|
-
|
|
6474
|
+
logErr2('No project linked. Run "mr link" or pass --project <id>.');
|
|
6061
6475
|
process.exit(1);
|
|
6062
6476
|
}
|
|
6063
6477
|
let project;
|
|
6064
6478
|
try {
|
|
6065
6479
|
project = await api.get(`/api/projects/${projectId}`);
|
|
6066
6480
|
} catch {
|
|
6067
|
-
|
|
6481
|
+
logErr2(`Failed to fetch project ${projectId}`);
|
|
6068
6482
|
process.exit(1);
|
|
6069
6483
|
}
|
|
6070
|
-
|
|
6484
|
+
log2(`Scanning project: ${paint9("cyan", project.name)}`);
|
|
6071
6485
|
let projectPath = project.localPath;
|
|
6072
6486
|
if (!projectPath) {
|
|
6073
6487
|
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
@@ -6083,12 +6497,12 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6083
6497
|
let reportId;
|
|
6084
6498
|
if (opts.report) {
|
|
6085
6499
|
reportId = opts.report;
|
|
6086
|
-
|
|
6500
|
+
log2(`Using existing scan report ${paint9("yellow", reportId.slice(0, 8))}`);
|
|
6087
6501
|
} else {
|
|
6088
6502
|
try {
|
|
6089
6503
|
const scans = await api.get(`/api/scans?projectId=${projectId}&status=processing`);
|
|
6090
6504
|
if (scans.length > 0) {
|
|
6091
|
-
|
|
6505
|
+
logErr2("A scan is already in progress for this project. Wait for it to complete.");
|
|
6092
6506
|
process.exit(1);
|
|
6093
6507
|
}
|
|
6094
6508
|
} catch {
|
|
@@ -6099,9 +6513,9 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6099
6513
|
status: "pending"
|
|
6100
6514
|
});
|
|
6101
6515
|
reportId = report.id;
|
|
6102
|
-
|
|
6516
|
+
log2(`Created scan report ${paint9("yellow", reportId.slice(0, 8))}`);
|
|
6103
6517
|
} catch (err) {
|
|
6104
|
-
|
|
6518
|
+
logErr2(`Failed to create scan report: ${err.message}`);
|
|
6105
6519
|
process.exit(1);
|
|
6106
6520
|
}
|
|
6107
6521
|
}
|
|
@@ -6112,7 +6526,7 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6112
6526
|
try {
|
|
6113
6527
|
const current = await api.get(`/api/scans/${reportId}`);
|
|
6114
6528
|
if (current.status === "cancelled") {
|
|
6115
|
-
|
|
6529
|
+
log2(paint9("yellow", "Scan was cancelled \u2014 aborting."));
|
|
6116
6530
|
process.exit(0);
|
|
6117
6531
|
}
|
|
6118
6532
|
} catch {
|
|
@@ -6126,9 +6540,9 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6126
6540
|
apiUrl: config.apiUrl,
|
|
6127
6541
|
apiKey: config.apiKey,
|
|
6128
6542
|
runBrowse: runBrowseCommand2,
|
|
6129
|
-
onLog:
|
|
6543
|
+
onLog: log2,
|
|
6130
6544
|
onProgress: (phase, detail) => {
|
|
6131
|
-
|
|
6545
|
+
log2(`${paint9("dim", `[${phase}]`)} ${detail}`);
|
|
6132
6546
|
}
|
|
6133
6547
|
});
|
|
6134
6548
|
let wasCancelled = false;
|
|
@@ -6140,7 +6554,7 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6140
6554
|
} catch {
|
|
6141
6555
|
}
|
|
6142
6556
|
if (wasCancelled) {
|
|
6143
|
-
|
|
6557
|
+
log2(paint9("yellow", "Scan was cancelled by user \u2014 discarding results."));
|
|
6144
6558
|
process.exit(0);
|
|
6145
6559
|
}
|
|
6146
6560
|
await api.patch(`/api/scans/${reportId}`, {
|
|
@@ -6151,37 +6565,37 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6151
6565
|
scanDurationMs: result.scanDurationMs,
|
|
6152
6566
|
routesCrawled: result.routesCrawled
|
|
6153
6567
|
});
|
|
6154
|
-
|
|
6568
|
+
logOk2(`Scan complete \u2014 ${paint9("cyan", String(result.findings.length))} findings`);
|
|
6155
6569
|
console.log("");
|
|
6156
|
-
console.log(` ${
|
|
6570
|
+
console.log(` ${paint9("bold", "Summary:")} ${result.summary}`);
|
|
6157
6571
|
console.log("");
|
|
6158
6572
|
const high = result.findings.filter((f) => f.priority === "high");
|
|
6159
6573
|
const medium = result.findings.filter((f) => f.priority === "medium");
|
|
6160
6574
|
const low = result.findings.filter((f) => f.priority === "low");
|
|
6161
6575
|
if (high.length > 0) {
|
|
6162
|
-
console.log(` ${
|
|
6576
|
+
console.log(` ${paint9("bold", paint9("red", `High Priority (${high.length})`))}`);
|
|
6163
6577
|
for (const f of high) {
|
|
6164
|
-
console.log(` ${
|
|
6165
|
-
console.log(` ${
|
|
6578
|
+
console.log(` ${paint9("red", "\u25CF")} [${f.type}] ${f.title}`);
|
|
6579
|
+
console.log(` ${paint9("dim", f.description.slice(0, 120))}`);
|
|
6166
6580
|
}
|
|
6167
6581
|
console.log("");
|
|
6168
6582
|
}
|
|
6169
6583
|
if (medium.length > 0) {
|
|
6170
|
-
console.log(` ${
|
|
6584
|
+
console.log(` ${paint9("bold", paint9("yellow", `Medium Priority (${medium.length})`))}`);
|
|
6171
6585
|
for (const f of medium) {
|
|
6172
|
-
console.log(` ${
|
|
6586
|
+
console.log(` ${paint9("yellow", "\u25CF")} [${f.type}] ${f.title}`);
|
|
6173
6587
|
}
|
|
6174
6588
|
console.log("");
|
|
6175
6589
|
}
|
|
6176
6590
|
if (low.length > 0) {
|
|
6177
|
-
console.log(` ${
|
|
6591
|
+
console.log(` ${paint9("dim", `Low Priority (${low.length})`)} `);
|
|
6178
6592
|
for (const f of low) {
|
|
6179
|
-
console.log(` ${
|
|
6593
|
+
console.log(` ${paint9("dim", `\u25CB [${f.type}] ${f.title}`)}`);
|
|
6180
6594
|
}
|
|
6181
6595
|
console.log("");
|
|
6182
6596
|
}
|
|
6183
6597
|
} catch (err) {
|
|
6184
|
-
|
|
6598
|
+
logErr2(`Scan failed: ${err.message}`);
|
|
6185
6599
|
try {
|
|
6186
6600
|
await api.patch(`/api/scans/${reportId}`, {
|
|
6187
6601
|
status: "failed",
|
|
@@ -6194,7 +6608,7 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6194
6608
|
});
|
|
6195
6609
|
|
|
6196
6610
|
// cli/commands/doctor.ts
|
|
6197
|
-
import { Command as
|
|
6611
|
+
import { Command as Command25 } from "commander";
|
|
6198
6612
|
import { existsSync as existsSync15 } from "fs";
|
|
6199
6613
|
import { homedir as homedir2 } from "os";
|
|
6200
6614
|
import { join as join11 } from "path";
|
|
@@ -6242,7 +6656,7 @@ async function checkProjectLink() {
|
|
|
6242
6656
|
optional: true
|
|
6243
6657
|
};
|
|
6244
6658
|
}
|
|
6245
|
-
var doctorCommand = new
|
|
6659
|
+
var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
|
|
6246
6660
|
const banner = [
|
|
6247
6661
|
``,
|
|
6248
6662
|
paint5("cyan", ` MR DOCTOR`),
|
|
@@ -6273,7 +6687,7 @@ var doctorCommand = new Command24("doctor").description("Diagnose Mr. Manager CL
|
|
|
6273
6687
|
console.log("");
|
|
6274
6688
|
return;
|
|
6275
6689
|
}
|
|
6276
|
-
const fixes = checks.filter((
|
|
6690
|
+
const fixes = checks.filter((c13) => !c13.ok && c13.fix && !c13.optional);
|
|
6277
6691
|
if (fixes.length > 0) {
|
|
6278
6692
|
console.log(paint5("yellow", " To fix:"));
|
|
6279
6693
|
for (const fix of fixes) {
|
|
@@ -6285,14 +6699,14 @@ var doctorCommand = new Command24("doctor").description("Diagnose Mr. Manager CL
|
|
|
6285
6699
|
});
|
|
6286
6700
|
|
|
6287
6701
|
// cli/commands/prompt-audit.ts
|
|
6288
|
-
import { Command as
|
|
6702
|
+
import { Command as Command26 } from "commander";
|
|
6289
6703
|
import { resolve as resolve8 } from "path";
|
|
6290
6704
|
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
|
|
6291
6705
|
function auditLine(label, tokens) {
|
|
6292
6706
|
const bar = "\u2588".repeat(Math.min(60, Math.round(tokens / 200)));
|
|
6293
6707
|
return ` ${label.padEnd(30)} ${formatTokenCount(tokens).padStart(8)} ${bar}`;
|
|
6294
6708
|
}
|
|
6295
|
-
var promptAuditCommand = new
|
|
6709
|
+
var promptAuditCommand = new Command26("prompt-audit").description("Dry-run prompt construction and report estimated token counts by job type").option("--task <id>", "Audit prompts for a specific task ID").option("--all", "Audit all supported job types with representative data", false).option("--json", "Output as JSON instead of plain text", false).action(async (opts) => {
|
|
6296
6710
|
const results = [];
|
|
6297
6711
|
if (opts.task) {
|
|
6298
6712
|
try {
|
|
@@ -6497,8 +6911,8 @@ ${r.jobType} [${r.identifier}]`);
|
|
|
6497
6911
|
});
|
|
6498
6912
|
|
|
6499
6913
|
// cli/commands/skill.ts
|
|
6500
|
-
import { Command as
|
|
6501
|
-
var
|
|
6914
|
+
import { Command as Command27 } from "commander";
|
|
6915
|
+
var c10 = {
|
|
6502
6916
|
reset: "\x1B[0m",
|
|
6503
6917
|
bold: "\x1B[1m",
|
|
6504
6918
|
dim: "\x1B[2m",
|
|
@@ -6506,7 +6920,7 @@ var c9 = {
|
|
|
6506
6920
|
green: "\x1B[32m",
|
|
6507
6921
|
yellow: "\x1B[33m"
|
|
6508
6922
|
};
|
|
6509
|
-
var skillCommand = new
|
|
6923
|
+
var skillCommand = new Command27("skill").description("Manage skills \u2014 reusable playbooks for AI agents");
|
|
6510
6924
|
skillCommand.command("list").alias("ls").description("List all skills").option("--category <category>", "Filter by category").action(async (opts) => {
|
|
6511
6925
|
const params = new URLSearchParams();
|
|
6512
6926
|
if (opts.category) params.set("category", opts.category);
|
|
@@ -6514,17 +6928,17 @@ skillCommand.command("list").alias("ls").description("List all skills").option("
|
|
|
6514
6928
|
`/api/skills${params.toString() ? `?${params}` : ""}`
|
|
6515
6929
|
);
|
|
6516
6930
|
if (skills.length === 0) {
|
|
6517
|
-
console.log(`${
|
|
6931
|
+
console.log(`${c10.dim}No skills found.${c10.reset}`);
|
|
6518
6932
|
return;
|
|
6519
6933
|
}
|
|
6520
6934
|
for (const skill of skills) {
|
|
6521
|
-
const cat = skill.category ? ` ${
|
|
6522
|
-
const scope = skill.projectId ? ` ${
|
|
6523
|
-
console.log(` ${
|
|
6935
|
+
const cat = skill.category ? ` ${c10.dim}[${skill.category}]${c10.reset}` : "";
|
|
6936
|
+
const scope = skill.projectId ? ` ${c10.dim}(project)${c10.reset}` : ` ${c10.dim}(global)${c10.reset}`;
|
|
6937
|
+
console.log(` ${c10.cyan}${skill.name}${c10.reset}${cat}${scope}`);
|
|
6524
6938
|
if (skill.description) {
|
|
6525
|
-
console.log(` ${
|
|
6939
|
+
console.log(` ${c10.dim}${skill.description}${c10.reset}`);
|
|
6526
6940
|
}
|
|
6527
|
-
console.log(` ${
|
|
6941
|
+
console.log(` ${c10.dim}id: ${skill.id}${c10.reset}`);
|
|
6528
6942
|
}
|
|
6529
6943
|
});
|
|
6530
6944
|
skillCommand.command("create").description("Create a new skill from a markdown file or inline content").argument("<name>", "Skill name").option("-d, --description <desc>", "Short description").option("-c, --category <cat>", "Category (e.g. Deployment, Testing)").option("-f, --file <path>", "Read content from a markdown file").option("--content <text>", "Inline markdown content").option("-p, --project", "Scope to the linked project").action(async (name, opts) => {
|
|
@@ -6560,7 +6974,7 @@ skillCommand.command("create").description("Create a new skill from a markdown f
|
|
|
6560
6974
|
projectId
|
|
6561
6975
|
});
|
|
6562
6976
|
console.log(
|
|
6563
|
-
`${
|
|
6977
|
+
`${c10.green}Created skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset} ${c10.dim}(${skill.id})${c10.reset}`
|
|
6564
6978
|
);
|
|
6565
6979
|
});
|
|
6566
6980
|
skillCommand.command("generate").alias("gen").description("Generate a new skill using AI from a text prompt").argument("<prompt>", "Describe the skill to generate").option("-p, --project", "Scope to the linked project").action(async (prompt2, opts) => {
|
|
@@ -6574,40 +6988,158 @@ skillCommand.command("generate").alias("gen").description("Generate a new skill
|
|
|
6574
6988
|
process.exit(1);
|
|
6575
6989
|
}
|
|
6576
6990
|
}
|
|
6577
|
-
console.log(`${
|
|
6991
|
+
console.log(`${c10.dim}Generating skill...${c10.reset}`);
|
|
6578
6992
|
try {
|
|
6579
6993
|
const skill = await api.post("/api/skills/generate", {
|
|
6580
6994
|
prompt: prompt2,
|
|
6581
6995
|
projectId
|
|
6582
6996
|
});
|
|
6583
6997
|
console.log(
|
|
6584
|
-
`${
|
|
6998
|
+
`${c10.green}Generated skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset}`
|
|
6585
6999
|
);
|
|
6586
7000
|
if (skill.description) {
|
|
6587
|
-
console.log(` ${
|
|
7001
|
+
console.log(` ${c10.dim}${skill.description}${c10.reset}`);
|
|
6588
7002
|
}
|
|
6589
7003
|
if (skill.category) {
|
|
6590
|
-
console.log(` ${
|
|
7004
|
+
console.log(` ${c10.dim}Category: ${skill.category}${c10.reset}`);
|
|
6591
7005
|
}
|
|
6592
|
-
console.log(` ${
|
|
7006
|
+
console.log(` ${c10.dim}id: ${skill.id}${c10.reset}`);
|
|
6593
7007
|
} catch (err) {
|
|
6594
7008
|
console.error(`Failed to generate skill: ${err.message}`);
|
|
6595
7009
|
process.exit(1);
|
|
6596
7010
|
}
|
|
6597
7011
|
});
|
|
6598
7012
|
|
|
7013
|
+
// cli/commands/resource.ts
|
|
7014
|
+
import { Command as Command28 } from "commander";
|
|
7015
|
+
var c11 = {
|
|
7016
|
+
reset: "\x1B[0m",
|
|
7017
|
+
bold: "\x1B[1m",
|
|
7018
|
+
dim: "\x1B[2m",
|
|
7019
|
+
cyan: "\x1B[36m",
|
|
7020
|
+
green: "\x1B[32m",
|
|
7021
|
+
yellow: "\x1B[33m",
|
|
7022
|
+
magenta: "\x1B[35m"
|
|
7023
|
+
};
|
|
7024
|
+
var TYPE_COLORS = {
|
|
7025
|
+
plan: c11.cyan,
|
|
7026
|
+
research: c11.magenta,
|
|
7027
|
+
"test-plan": c11.yellow,
|
|
7028
|
+
note: c11.green
|
|
7029
|
+
};
|
|
7030
|
+
function typeLabel(type) {
|
|
7031
|
+
const color = TYPE_COLORS[type] ?? c11.dim;
|
|
7032
|
+
return `${color}${type}${c11.reset}`;
|
|
7033
|
+
}
|
|
7034
|
+
var resourceCommand = new Command28("resource").description("Manage resources \u2014 documents, plans, research, and notes");
|
|
7035
|
+
resourceCommand.command("list").alias("ls").description("List resources for the linked project (or all)").option("--all", "List all resources across projects").action(async (opts) => {
|
|
7036
|
+
const params = new URLSearchParams();
|
|
7037
|
+
if (opts.all) {
|
|
7038
|
+
params.set("all", "true");
|
|
7039
|
+
} else {
|
|
7040
|
+
const projectId = getLinkedProjectId();
|
|
7041
|
+
if (projectId) params.set("projectId", projectId);
|
|
7042
|
+
}
|
|
7043
|
+
const resources = await api.get(
|
|
7044
|
+
`/api/resources${params.toString() ? `?${params}` : ""}`
|
|
7045
|
+
);
|
|
7046
|
+
if (resources.length === 0) {
|
|
7047
|
+
console.log(`${c11.dim}No resources found.${c11.reset}`);
|
|
7048
|
+
return;
|
|
7049
|
+
}
|
|
7050
|
+
for (const r of resources) {
|
|
7051
|
+
const project = r.projectName ? ` ${c11.dim}[${r.projectName}]${c11.reset}` : "";
|
|
7052
|
+
console.log(` ${typeLabel(r.type)} ${c11.bold}${r.title}${c11.reset}${project}`);
|
|
7053
|
+
console.log(` ${c11.dim}id: ${r.id}${c11.reset}`);
|
|
7054
|
+
}
|
|
7055
|
+
});
|
|
7056
|
+
resourceCommand.command("create").description("Create a new resource from a file or inline content").argument("<title>", "Resource title").option("-t, --type <type>", "Resource type (note, plan, research, test-plan)", "note").option("-f, --file <path>", "Read content from a file").option("--content <text>", "Inline content").option("--task <taskId>", "Attach to an existing task instead of creating a standalone resource").option("-p, --project", "Scope to the linked project").action(async (title, opts) => {
|
|
7057
|
+
let content = opts.content ?? "";
|
|
7058
|
+
if (opts.file) {
|
|
7059
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
7060
|
+
try {
|
|
7061
|
+
content = readFileSync13(opts.file, "utf-8");
|
|
7062
|
+
} catch (err) {
|
|
7063
|
+
console.error(`Failed to read file: ${err.message}`);
|
|
7064
|
+
process.exit(1);
|
|
7065
|
+
}
|
|
7066
|
+
}
|
|
7067
|
+
if (opts.task) {
|
|
7068
|
+
if (!content.trim()) {
|
|
7069
|
+
console.error("Content is required. Use --file <path> or --content <text>.");
|
|
7070
|
+
process.exit(1);
|
|
7071
|
+
}
|
|
7072
|
+
const resource = await api.post(`/api/tasks/${opts.task}/resources`, {
|
|
7073
|
+
type: opts.type,
|
|
7074
|
+
title,
|
|
7075
|
+
content: content.trim()
|
|
7076
|
+
});
|
|
7077
|
+
console.log(
|
|
7078
|
+
`${c11.green}Created resource:${c11.reset} ${c11.bold}${title}${c11.reset} ${c11.dim}(${resource.id})${c11.reset}`
|
|
7079
|
+
);
|
|
7080
|
+
console.log(` ${c11.dim}Attached to task ${opts.task}${c11.reset}`);
|
|
7081
|
+
return;
|
|
7082
|
+
}
|
|
7083
|
+
let projectId;
|
|
7084
|
+
if (opts.project) {
|
|
7085
|
+
projectId = getLinkedProjectId() ?? void 0;
|
|
7086
|
+
if (!projectId) {
|
|
7087
|
+
console.error(
|
|
7088
|
+
'No project linked to this directory. Run "mr link <project-id>" first.'
|
|
7089
|
+
);
|
|
7090
|
+
process.exit(1);
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
const result = await api.post("/api/resources", {
|
|
7094
|
+
type: opts.type,
|
|
7095
|
+
title,
|
|
7096
|
+
content: content.trim() || " ",
|
|
7097
|
+
projectId
|
|
7098
|
+
});
|
|
7099
|
+
console.log(
|
|
7100
|
+
`${c11.green}Created resource:${c11.reset} ${c11.bold}${title}${c11.reset} ${c11.dim}(${result.resource.id})${c11.reset}`
|
|
7101
|
+
);
|
|
7102
|
+
});
|
|
7103
|
+
resourceCommand.command("generate").alias("gen").description("Generate a resource using AI from a text prompt").argument("<prompt>", "Describe the resource to generate").option("-t, --type <type>", "Resource type (plan or research)", "research").option("-p, --project", "Scope to the linked project").action(async (prompt2, opts) => {
|
|
7104
|
+
const type = opts.type;
|
|
7105
|
+
if (type !== "plan" && type !== "research") {
|
|
7106
|
+
console.error('Type must be "plan" or "research" for AI generation.');
|
|
7107
|
+
process.exit(1);
|
|
7108
|
+
}
|
|
7109
|
+
let projectId;
|
|
7110
|
+
if (opts.project) {
|
|
7111
|
+
projectId = getLinkedProjectId() ?? void 0;
|
|
7112
|
+
if (!projectId) {
|
|
7113
|
+
console.error(
|
|
7114
|
+
'No project linked to this directory. Run "mr link <project-id>" first.'
|
|
7115
|
+
);
|
|
7116
|
+
process.exit(1);
|
|
7117
|
+
}
|
|
7118
|
+
}
|
|
7119
|
+
console.log(`${c11.dim}Generating ${type}...${c11.reset}`);
|
|
7120
|
+
const result = await api.post("/api/resources", {
|
|
7121
|
+
type,
|
|
7122
|
+
prompt: prompt2,
|
|
7123
|
+
projectId
|
|
7124
|
+
});
|
|
7125
|
+
console.log(
|
|
7126
|
+
`${c11.green}Queued:${c11.reset} ${c11.bold}${result.task.title}${c11.reset}`
|
|
7127
|
+
);
|
|
7128
|
+
console.log(` ${c11.dim}task: ${result.task.id}${c11.reset}`);
|
|
7129
|
+
});
|
|
7130
|
+
|
|
6599
7131
|
// cli/commands/tests.ts
|
|
6600
|
-
import { Command as
|
|
6601
|
-
var
|
|
7132
|
+
import { Command as Command29 } from "commander";
|
|
7133
|
+
var c12 = {
|
|
6602
7134
|
reset: "\x1B[0m",
|
|
6603
7135
|
dim: "\x1B[2m",
|
|
6604
7136
|
yellow: "\x1B[33m"
|
|
6605
7137
|
};
|
|
6606
|
-
var testsCommand = new
|
|
7138
|
+
var testsCommand = new Command29("tests").description("List MR Test scenarios for the linked project").action(async () => {
|
|
6607
7139
|
const projectId = getLinkedProjectId();
|
|
6608
7140
|
if (!projectId) {
|
|
6609
7141
|
console.error(
|
|
6610
|
-
`${
|
|
7142
|
+
`${c12.yellow}No project linked to this directory.${c12.reset} Run "mr link <project-id>" first.`
|
|
6611
7143
|
);
|
|
6612
7144
|
process.exit(1);
|
|
6613
7145
|
}
|
|
@@ -6620,13 +7152,13 @@ var testsCommand = new Command27("tests").description("List MR Test scenarios fo
|
|
|
6620
7152
|
process.exit(1);
|
|
6621
7153
|
}
|
|
6622
7154
|
if (scenarios.length === 0) {
|
|
6623
|
-
console.log(`${
|
|
7155
|
+
console.log(`${c12.dim}No test scenarios found for this project.${c12.reset}`);
|
|
6624
7156
|
return;
|
|
6625
7157
|
}
|
|
6626
7158
|
for (const scenario of scenarios) {
|
|
6627
7159
|
console.log(`### ${scenario.name}`);
|
|
6628
7160
|
if (scenario.description) {
|
|
6629
|
-
console.log(`${
|
|
7161
|
+
console.log(`${c12.dim}${scenario.description}${c12.reset}`);
|
|
6630
7162
|
console.log();
|
|
6631
7163
|
}
|
|
6632
7164
|
console.log(scenario.content);
|
|
@@ -6641,7 +7173,7 @@ var userArgs = process.argv.slice(2);
|
|
|
6641
7173
|
var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
|
|
6642
7174
|
var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
|
|
6643
7175
|
if (isFirstRun && !shouldBypass) {
|
|
6644
|
-
const
|
|
7176
|
+
const c13 = {
|
|
6645
7177
|
reset: "\x1B[0m",
|
|
6646
7178
|
bold: "\x1B[1m",
|
|
6647
7179
|
dim: "\x1B[2m",
|
|
@@ -6651,28 +7183,28 @@ if (isFirstRun && !shouldBypass) {
|
|
|
6651
7183
|
magenta: "\x1B[35m"
|
|
6652
7184
|
};
|
|
6653
7185
|
console.log("");
|
|
6654
|
-
console.log(`${
|
|
6655
|
-
console.log(`${
|
|
6656
|
-
console.log(`${
|
|
6657
|
-
console.log(`${
|
|
7186
|
+
console.log(`${c13.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${c13.reset}`);
|
|
7187
|
+
console.log(`${c13.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${c13.reset}`);
|
|
7188
|
+
console.log(`${c13.cyan} \u2569 \u2569\u2569\u255A\u2550 \u2569 \u2569\u2569 \u2569\u255D\u255A\u255D\u2569 \u2569\u255A\u2550\u255D\u255A\u2550\u255D\u2569\u255A\u2550${c13.reset}`);
|
|
7189
|
+
console.log(`${c13.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${c13.reset}`);
|
|
6658
7190
|
console.log("");
|
|
6659
|
-
console.log(`${
|
|
6660
|
-
console.log(`${
|
|
7191
|
+
console.log(`${c13.bold} Welcome to Mr. Manager!${c13.reset}`);
|
|
7192
|
+
console.log(`${c13.dim} Let's get you set up in a few quick steps.${c13.reset}`);
|
|
6661
7193
|
console.log("");
|
|
6662
|
-
console.log(` ${
|
|
6663
|
-
console.log(` ${
|
|
7194
|
+
console.log(` ${c13.yellow}Step 1:${c13.reset} Authenticate via Google OAuth`);
|
|
7195
|
+
console.log(` ${c13.dim}Run:${c13.reset} ${c13.cyan}mr login${c13.reset}`);
|
|
6664
7196
|
console.log("");
|
|
6665
|
-
console.log(` ${
|
|
6666
|
-
console.log(` ${
|
|
7197
|
+
console.log(` ${c13.yellow}Step 2:${c13.reset} Verify your environment`);
|
|
7198
|
+
console.log(` ${c13.dim}Run:${c13.reset} ${c13.cyan}mr setup${c13.reset}`);
|
|
6667
7199
|
console.log("");
|
|
6668
|
-
console.log(` ${
|
|
6669
|
-
console.log(` ${
|
|
7200
|
+
console.log(` ${c13.yellow}Step 3:${c13.reset} Link a repo and start watching`);
|
|
7201
|
+
console.log(` ${c13.dim}Run:${c13.reset} ${c13.cyan}mr link${c13.reset} ${c13.dim}&&${c13.reset} ${c13.cyan}mr watch${c13.reset}`);
|
|
6670
7202
|
console.log("");
|
|
6671
|
-
console.log(`${
|
|
7203
|
+
console.log(`${c13.dim} Or run ${c13.reset}${c13.cyan}mr login${c13.reset}${c13.dim} to get started now.${c13.reset}`);
|
|
6672
7204
|
console.log("");
|
|
6673
7205
|
process.exit(0);
|
|
6674
7206
|
}
|
|
6675
|
-
var program = new
|
|
7207
|
+
var program = new Command30();
|
|
6676
7208
|
program.name("mr").description("Mr. Manager - Task and project management CLI").version(CLI_VERSION);
|
|
6677
7209
|
program.addCommand(initCommand);
|
|
6678
7210
|
program.addCommand(authCommand);
|
|
@@ -6699,9 +7231,11 @@ program.addCommand(setPathCommand);
|
|
|
6699
7231
|
program.addCommand(testCommand);
|
|
6700
7232
|
program.addCommand(featuresCommand);
|
|
6701
7233
|
program.addCommand(noMrCommand);
|
|
7234
|
+
program.addCommand(reviewCommand);
|
|
6702
7235
|
program.addCommand(scanCommand);
|
|
6703
7236
|
program.addCommand(doctorCommand);
|
|
6704
7237
|
program.addCommand(promptAuditCommand);
|
|
6705
7238
|
program.addCommand(skillCommand);
|
|
7239
|
+
program.addCommand(resourceCommand);
|
|
6706
7240
|
program.addCommand(testsCommand);
|
|
6707
7241
|
program.parse();
|