@dunnewold-labs/mr-manager 0.4.32 → 0.4.36
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 +665 -161
- 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.36",
|
|
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;
|
|
@@ -2015,7 +2015,7 @@ ${task.notes}` : "";
|
|
|
2015
2015
|
],
|
|
2016
2016
|
``,
|
|
2017
2017
|
`3. Implement the task. You may read, write, and run code across any relevant repos under ${repoDir}.`,
|
|
2018
|
-
` - 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>'\``,
|
|
2019
2019
|
...pendingSubtasks.length > 0 ? [
|
|
2020
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.`
|
|
2021
2021
|
] : [],
|
|
@@ -2023,7 +2023,7 @@ ${task.notes}` : "";
|
|
|
2023
2023
|
`4. Once implementation is complete, for each repo that has changes:`,
|
|
2024
2024
|
` a. Commit all changes with a clear, descriptive message.`,
|
|
2025
2025
|
` b. Push the branch: \`git push -u origin HEAD\``,
|
|
2026
|
-
...hasFeedback ? [
|
|
2026
|
+
...hasFeedback || hasAttachedBranch && task.attachedBranchLink && isPrOrMrUrl(task.attachedBranchLink) ? [
|
|
2027
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.`
|
|
2028
2028
|
] : [
|
|
2029
2029
|
` c. Write a structured ${vcs === "gitlab" ? "merge request" : "pull request"} description to \`${prBodyPath}\` using the template below.`,
|
|
@@ -2042,7 +2042,7 @@ ${task.notes}` : "";
|
|
|
2042
2042
|
``,
|
|
2043
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.`,
|
|
2044
2044
|
``,
|
|
2045
|
-
...hasFeedback ? [] : [
|
|
2045
|
+
...hasFeedback || hasAttachedBranch && task.attachedBranchLink && isPrOrMrUrl(task.attachedBranchLink) ? [] : [
|
|
2046
2046
|
`## PR Description Template`,
|
|
2047
2047
|
``,
|
|
2048
2048
|
`Write this template to \`${prBodyPath}\`, then replace the placeholders with the actual details from your implementation.`,
|
|
@@ -2292,7 +2292,7 @@ function buildPrototypePrompt(proto, repoDir) {
|
|
|
2292
2292
|
}
|
|
2293
2293
|
};
|
|
2294
2294
|
const config = typeConfig[prototypeType] ?? typeConfig.web_app;
|
|
2295
|
-
const
|
|
2295
|
+
const typeLabel2 = {
|
|
2296
2296
|
web_app: "Web App",
|
|
2297
2297
|
mobile_app: "Mobile App",
|
|
2298
2298
|
desktop_app: "Desktop App",
|
|
@@ -2306,7 +2306,7 @@ function buildPrototypePrompt(proto, repoDir) {
|
|
|
2306
2306
|
`## Prototype Request`,
|
|
2307
2307
|
`Title: ${proto.title}`,
|
|
2308
2308
|
`ID: ${proto.id}`,
|
|
2309
|
-
`Type: ${
|
|
2309
|
+
`Type: ${typeLabel2[prototypeType] ?? prototypeType}`,
|
|
2310
2310
|
``,
|
|
2311
2311
|
`## Design Prompt`,
|
|
2312
2312
|
`${proto.prompt}`,
|
|
@@ -2374,7 +2374,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
2374
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.",
|
|
2375
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."
|
|
2376
2376
|
};
|
|
2377
|
-
const
|
|
2377
|
+
const typeLabel2 = {
|
|
2378
2378
|
web_app: "Web App",
|
|
2379
2379
|
mobile_app: "Mobile App",
|
|
2380
2380
|
desktop_app: "Desktop App",
|
|
@@ -2388,7 +2388,7 @@ function buildRefinementPrompt(proto, parentFiles, repoDir) {
|
|
|
2388
2388
|
`## Prototype Request`,
|
|
2389
2389
|
`Title: ${proto.title}`,
|
|
2390
2390
|
`ID: ${proto.id}`,
|
|
2391
|
-
`Type: ${
|
|
2391
|
+
`Type: ${typeLabel2[prototypeType] ?? prototypeType}`,
|
|
2392
2392
|
``,
|
|
2393
2393
|
`## Original Design Prompt`,
|
|
2394
2394
|
`${proto.prompt}`,
|
|
@@ -2662,7 +2662,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2662
2662
|
logWarn(watchTag(), `Network unavailable \u2014 pausing active tasks until connectivity returns (${reason})`);
|
|
2663
2663
|
}
|
|
2664
2664
|
for (const taskId of active.keys()) {
|
|
2665
|
-
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "test-"];
|
|
2665
|
+
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "review-", "test-"];
|
|
2666
2666
|
if (nonTaskPrefixes.some((prefix) => taskId.startsWith(prefix))) continue;
|
|
2667
2667
|
pauseTaskForNetwork(taskId, reason);
|
|
2668
2668
|
}
|
|
@@ -2918,29 +2918,6 @@ var watchCommand = new Command8("watch").description(
|
|
|
2918
2918
|
try {
|
|
2919
2919
|
if (code === 0) {
|
|
2920
2920
|
try {
|
|
2921
|
-
const researchPath = resolve2(executionDir, "research.md");
|
|
2922
|
-
if (existsSync7(researchPath)) {
|
|
2923
|
-
try {
|
|
2924
|
-
const researchContent = readFileSync5(researchPath, "utf-8");
|
|
2925
|
-
const existingResearch = existingResources.find((r) => r.type === "research");
|
|
2926
|
-
if (existingResearch) {
|
|
2927
|
-
await api.patch(`/api/tasks/${task.id}/resources/${existingResearch.id}`, {
|
|
2928
|
-
content: researchContent
|
|
2929
|
-
});
|
|
2930
|
-
logSuccess(prefix, `Updated existing research resource`);
|
|
2931
|
-
} else {
|
|
2932
|
-
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2933
|
-
type: "research",
|
|
2934
|
-
title: `Research \u2014 ${task.title}`,
|
|
2935
|
-
content: researchContent
|
|
2936
|
-
});
|
|
2937
|
-
logSuccess(prefix, `Uploaded research.md as task resource`);
|
|
2938
|
-
}
|
|
2939
|
-
unlinkSync(researchPath);
|
|
2940
|
-
} catch (err) {
|
|
2941
|
-
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2942
|
-
}
|
|
2943
|
-
}
|
|
2944
2921
|
const noMrPath = resolve2(executionDir, ".mr-no-mr");
|
|
2945
2922
|
const noMrRequested = existsSync7(noMrPath);
|
|
2946
2923
|
let noMrDescription;
|
|
@@ -2951,7 +2928,11 @@ var watchCommand = new Command8("watch").description(
|
|
|
2951
2928
|
}
|
|
2952
2929
|
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2953
2930
|
let prUrl = null;
|
|
2954
|
-
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) {
|
|
2955
2936
|
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2956
2937
|
if (!prUrl && !task.attachedBranch?.trim()) {
|
|
2957
2938
|
prUrl = await findPrUrlAcrossRepos(legacyBranchName, repoDir, vcs);
|
|
@@ -2998,6 +2979,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
2998
2979
|
logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
|
|
2999
2980
|
await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
|
|
3000
2981
|
}
|
|
2982
|
+
} else if (prUrl) {
|
|
2983
|
+
logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
|
|
3001
2984
|
}
|
|
3002
2985
|
const currentTask = await api.get(`/api/tasks/${task.id}`);
|
|
3003
2986
|
if (currentTask.status === "completed" || currentTask.status === "review") {
|
|
@@ -3442,6 +3425,49 @@ var watchCommand = new Command8("watch").description(
|
|
|
3442
3425
|
}
|
|
3443
3426
|
});
|
|
3444
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
|
+
}
|
|
3445
3471
|
async function processApprovalQueue() {
|
|
3446
3472
|
if (approvalRunning || approvalQueue.length === 0) return;
|
|
3447
3473
|
approvalRunning = true;
|
|
@@ -3584,7 +3610,7 @@ ${divider}`);
|
|
|
3584
3610
|
}
|
|
3585
3611
|
}
|
|
3586
3612
|
for (const [taskId, entry] of active) {
|
|
3587
|
-
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;
|
|
3588
3614
|
if (!activeTaskIds.has(taskId)) {
|
|
3589
3615
|
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer active, terminating\u2026`);
|
|
3590
3616
|
entry.terminatedForError = true;
|
|
@@ -3593,7 +3619,7 @@ ${divider}`);
|
|
|
3593
3619
|
queued.delete(taskId);
|
|
3594
3620
|
}
|
|
3595
3621
|
}
|
|
3596
|
-
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "test-"];
|
|
3622
|
+
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "review-", "test-"];
|
|
3597
3623
|
for (const taskId of failed.keys()) {
|
|
3598
3624
|
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
3599
3625
|
if (!activeTaskIds.has(taskId)) failed.delete(taskId);
|
|
@@ -3899,6 +3925,29 @@ ${divider}`);
|
|
|
3899
3925
|
}
|
|
3900
3926
|
dispatchScan(scan, prefix, key);
|
|
3901
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
|
+
}
|
|
3902
3951
|
let reviewTasks = [];
|
|
3903
3952
|
try {
|
|
3904
3953
|
reviewTasks = await api.get("/api/tasks?status=review");
|
|
@@ -4182,9 +4231,9 @@ var prototypeCommand = new Command13("prototype").description("Manage prototypes
|
|
|
4182
4231
|
};
|
|
4183
4232
|
for (const p of prototypes) {
|
|
4184
4233
|
const date = new Date(p.createdAt).toLocaleDateString();
|
|
4185
|
-
const
|
|
4234
|
+
const typeLabel2 = typeLabels[p.prototypeType] ?? p.prototypeType ?? "web";
|
|
4186
4235
|
console.log(
|
|
4187
|
-
` ${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)}`
|
|
4188
4237
|
);
|
|
4189
4238
|
console.log(` ${paint4("dim", p.prompt.slice(0, 80) + (p.prompt.length > 80 ? "\u2026" : ""))}`);
|
|
4190
4239
|
console.log();
|
|
@@ -4500,7 +4549,7 @@ async function checkApiConnectivity() {
|
|
|
4500
4549
|
}
|
|
4501
4550
|
}
|
|
4502
4551
|
function printResults(checks) {
|
|
4503
|
-
const maxNameLen = Math.max(...checks.map((
|
|
4552
|
+
const maxNameLen = Math.max(...checks.map((c13) => c13.name.length));
|
|
4504
4553
|
let allOk = true;
|
|
4505
4554
|
for (const check of checks) {
|
|
4506
4555
|
const isOptional = check.optional ?? false;
|
|
@@ -4513,16 +4562,16 @@ function printResults(checks) {
|
|
|
4513
4562
|
return allOk;
|
|
4514
4563
|
}
|
|
4515
4564
|
async function autoFix(checks, agent) {
|
|
4516
|
-
const { spawn:
|
|
4517
|
-
const ghInstalled = checks.find((
|
|
4518
|
-
const ghAuthed = checks.find((
|
|
4519
|
-
const mrAuthed = checks.find((
|
|
4520
|
-
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)");
|
|
4521
4570
|
if (claudeCheck && !claudeCheck.ok && agent === "claude") {
|
|
4522
4571
|
console.log(paint5("cyan", " Installing Claude Code..."));
|
|
4523
4572
|
console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
4524
4573
|
await new Promise((resolve9) => {
|
|
4525
|
-
const child =
|
|
4574
|
+
const child = spawn9("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
4526
4575
|
child.on("exit", () => resolve9());
|
|
4527
4576
|
});
|
|
4528
4577
|
console.log("");
|
|
@@ -4530,7 +4579,7 @@ async function autoFix(checks, agent) {
|
|
|
4530
4579
|
if (ghInstalled && !ghAuthed) {
|
|
4531
4580
|
console.log(paint5("cyan", " Running gh auth login..."));
|
|
4532
4581
|
await new Promise((resolve9) => {
|
|
4533
|
-
const child =
|
|
4582
|
+
const child = spawn9("gh", ["auth", "login"], { stdio: "inherit" });
|
|
4534
4583
|
child.on("exit", () => resolve9());
|
|
4535
4584
|
});
|
|
4536
4585
|
console.log("");
|
|
@@ -4539,7 +4588,7 @@ async function autoFix(checks, agent) {
|
|
|
4539
4588
|
console.log(paint5("cyan", " Running mr login..."));
|
|
4540
4589
|
const entry = process.argv[1];
|
|
4541
4590
|
await new Promise((resolve9) => {
|
|
4542
|
-
const child =
|
|
4591
|
+
const child = spawn9(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
4543
4592
|
child.on("exit", () => resolve9());
|
|
4544
4593
|
});
|
|
4545
4594
|
console.log("");
|
|
@@ -4580,7 +4629,7 @@ var setupCommand = new Command14("setup").description("Check that all dependenci
|
|
|
4580
4629
|
console.log("");
|
|
4581
4630
|
return;
|
|
4582
4631
|
}
|
|
4583
|
-
const fixes = checks.filter((
|
|
4632
|
+
const fixes = checks.filter((c13) => !c13.ok && c13.fix && !c13.optional);
|
|
4584
4633
|
if (fixes.length > 0) {
|
|
4585
4634
|
console.log(paint5("yellow", " To fix:"));
|
|
4586
4635
|
for (const fix of fixes) {
|
|
@@ -4596,9 +4645,26 @@ var setupCommand = new Command14("setup").description("Check that all dependenci
|
|
|
4596
4645
|
|
|
4597
4646
|
// cli/commands/update.ts
|
|
4598
4647
|
import { Command as Command15 } from "commander";
|
|
4599
|
-
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
|
+
}
|
|
4600
4666
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
4601
|
-
message,
|
|
4667
|
+
message: messageOrTitle,
|
|
4602
4668
|
source: opts.source
|
|
4603
4669
|
});
|
|
4604
4670
|
console.log(`\u2713 Status update posted`);
|
|
@@ -5306,11 +5372,329 @@ var noMrCommand = new Command22("no-mr").description("Signal that a task does no
|
|
|
5306
5372
|
console.log(` Reason: ${description}`);
|
|
5307
5373
|
});
|
|
5308
5374
|
|
|
5309
|
-
// cli/commands/
|
|
5375
|
+
// cli/commands/review.ts
|
|
5310
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";
|
|
5311
5695
|
|
|
5312
5696
|
// lib/scanner/index.ts
|
|
5313
|
-
import { spawn as
|
|
5697
|
+
import { spawn as spawn8 } from "child_process";
|
|
5314
5698
|
|
|
5315
5699
|
// lib/scanner/config.ts
|
|
5316
5700
|
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
@@ -5379,7 +5763,7 @@ async function authenticateBrowseSession(magicUrl, runBrowse) {
|
|
|
5379
5763
|
// lib/scanner/codebase-analysis.ts
|
|
5380
5764
|
import { readdirSync as readdirSync2, readFileSync as readFileSync11, existsSync as existsSync14 } from "fs";
|
|
5381
5765
|
import { join as join10, relative } from "path";
|
|
5382
|
-
import { execSync as
|
|
5766
|
+
import { execSync as execSync6 } from "child_process";
|
|
5383
5767
|
function resolveDir(projectPath, candidates) {
|
|
5384
5768
|
for (const candidate of candidates) {
|
|
5385
5769
|
const dir = join10(projectPath, candidate);
|
|
@@ -5505,7 +5889,7 @@ function extractInternalLinks(projectPath) {
|
|
|
5505
5889
|
}
|
|
5506
5890
|
function getRecentCommits(projectPath, count = 20) {
|
|
5507
5891
|
try {
|
|
5508
|
-
const output =
|
|
5892
|
+
const output = execSync6(
|
|
5509
5893
|
`git log --oneline -${count} --no-decorate`,
|
|
5510
5894
|
{ cwd: projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
5511
5895
|
);
|
|
@@ -5773,10 +6157,10 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
|
|
|
5773
6157
|
${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
|
|
5774
6158
|
|
|
5775
6159
|
**Components:**
|
|
5776
|
-
${codebaseAnalysis.components.slice(0, 15).map((
|
|
6160
|
+
${codebaseAnalysis.components.slice(0, 15).map((c13) => `- ${c13}`).join("\n")}
|
|
5777
6161
|
|
|
5778
6162
|
**Recent Git Commits:**
|
|
5779
|
-
${codebaseAnalysis.recentCommits.slice(0, 8).map((
|
|
6163
|
+
${codebaseAnalysis.recentCommits.slice(0, 8).map((c13) => `- ${c13}`).join("\n")}
|
|
5780
6164
|
|
|
5781
6165
|
**Completed Tasks:**
|
|
5782
6166
|
${context.completedTasks.slice(0, 10).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
@@ -5921,7 +6305,7 @@ async function runScanPipeline(opts) {
|
|
|
5921
6305
|
crawlResults,
|
|
5922
6306
|
context.priorFindings
|
|
5923
6307
|
);
|
|
5924
|
-
const synthesisResult = await
|
|
6308
|
+
const synthesisResult = await runClaude2(prompt2);
|
|
5925
6309
|
const parsed = parseSynthesisOutput(synthesisResult);
|
|
5926
6310
|
const scanDurationMs = Date.now() - startTime;
|
|
5927
6311
|
opts.onLog(`Scan complete in ${Math.round(scanDurationMs / 1e3)}s \u2014 ${parsed.findings.length} findings`);
|
|
@@ -5982,9 +6366,9 @@ async function fetchScanContext(opts) {
|
|
|
5982
6366
|
priorFindings
|
|
5983
6367
|
};
|
|
5984
6368
|
}
|
|
5985
|
-
function
|
|
6369
|
+
function runClaude2(prompt2) {
|
|
5986
6370
|
return new Promise((resolve9, reject) => {
|
|
5987
|
-
const child =
|
|
6371
|
+
const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
5988
6372
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5989
6373
|
});
|
|
5990
6374
|
let output = "";
|
|
@@ -6040,7 +6424,7 @@ function parseSynthesisOutput(output) {
|
|
|
6040
6424
|
}
|
|
6041
6425
|
|
|
6042
6426
|
// cli/commands/scan.ts
|
|
6043
|
-
var
|
|
6427
|
+
var c9 = {
|
|
6044
6428
|
reset: "\x1B[0m",
|
|
6045
6429
|
bold: "\x1B[1m",
|
|
6046
6430
|
dim: "\x1B[2m",
|
|
@@ -6051,53 +6435,53 @@ var c8 = {
|
|
|
6051
6435
|
magenta: "\x1B[35m",
|
|
6052
6436
|
gray: "\x1B[90m"
|
|
6053
6437
|
};
|
|
6054
|
-
function
|
|
6055
|
-
return `${
|
|
6438
|
+
function paint9(color, text) {
|
|
6439
|
+
return `${c9[color]}${text}${c9.reset}`;
|
|
6056
6440
|
}
|
|
6057
|
-
function
|
|
6058
|
-
return
|
|
6441
|
+
function timestamp3() {
|
|
6442
|
+
return paint9("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
6059
6443
|
}
|
|
6060
6444
|
function scanTag() {
|
|
6061
|
-
return
|
|
6445
|
+
return paint9("magenta", "[scan]");
|
|
6062
6446
|
}
|
|
6063
|
-
function
|
|
6064
|
-
console.log(`${
|
|
6447
|
+
function log2(msg) {
|
|
6448
|
+
console.log(`${timestamp3()} ${scanTag()} ${msg}`);
|
|
6065
6449
|
}
|
|
6066
|
-
function
|
|
6067
|
-
console.log(`${
|
|
6450
|
+
function logOk2(msg) {
|
|
6451
|
+
console.log(`${timestamp3()} ${scanTag()} ${paint9("green", "\u2713")} ${msg}`);
|
|
6068
6452
|
}
|
|
6069
|
-
function
|
|
6070
|
-
console.error(`${
|
|
6453
|
+
function logErr2(msg) {
|
|
6454
|
+
console.error(`${timestamp3()} ${scanTag()} ${paint9("red", "\u2717")} ${msg}`);
|
|
6071
6455
|
}
|
|
6072
|
-
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) => {
|
|
6073
6457
|
const config = loadConfig();
|
|
6074
6458
|
if (!config.apiKey) {
|
|
6075
|
-
|
|
6459
|
+
logErr2('Not authenticated. Run "mr login" first.');
|
|
6076
6460
|
process.exit(1);
|
|
6077
6461
|
}
|
|
6078
6462
|
const banner = [
|
|
6079
6463
|
``,
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
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`),
|
|
6085
6469
|
``
|
|
6086
6470
|
].join("\n");
|
|
6087
6471
|
console.log(banner);
|
|
6088
6472
|
const projectId = opts.project || getLinkedProjectId();
|
|
6089
6473
|
if (!projectId) {
|
|
6090
|
-
|
|
6474
|
+
logErr2('No project linked. Run "mr link" or pass --project <id>.');
|
|
6091
6475
|
process.exit(1);
|
|
6092
6476
|
}
|
|
6093
6477
|
let project;
|
|
6094
6478
|
try {
|
|
6095
6479
|
project = await api.get(`/api/projects/${projectId}`);
|
|
6096
6480
|
} catch {
|
|
6097
|
-
|
|
6481
|
+
logErr2(`Failed to fetch project ${projectId}`);
|
|
6098
6482
|
process.exit(1);
|
|
6099
6483
|
}
|
|
6100
|
-
|
|
6484
|
+
log2(`Scanning project: ${paint9("cyan", project.name)}`);
|
|
6101
6485
|
let projectPath = project.localPath;
|
|
6102
6486
|
if (!projectPath) {
|
|
6103
6487
|
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
@@ -6113,12 +6497,12 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6113
6497
|
let reportId;
|
|
6114
6498
|
if (opts.report) {
|
|
6115
6499
|
reportId = opts.report;
|
|
6116
|
-
|
|
6500
|
+
log2(`Using existing scan report ${paint9("yellow", reportId.slice(0, 8))}`);
|
|
6117
6501
|
} else {
|
|
6118
6502
|
try {
|
|
6119
6503
|
const scans = await api.get(`/api/scans?projectId=${projectId}&status=processing`);
|
|
6120
6504
|
if (scans.length > 0) {
|
|
6121
|
-
|
|
6505
|
+
logErr2("A scan is already in progress for this project. Wait for it to complete.");
|
|
6122
6506
|
process.exit(1);
|
|
6123
6507
|
}
|
|
6124
6508
|
} catch {
|
|
@@ -6129,9 +6513,9 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6129
6513
|
status: "pending"
|
|
6130
6514
|
});
|
|
6131
6515
|
reportId = report.id;
|
|
6132
|
-
|
|
6516
|
+
log2(`Created scan report ${paint9("yellow", reportId.slice(0, 8))}`);
|
|
6133
6517
|
} catch (err) {
|
|
6134
|
-
|
|
6518
|
+
logErr2(`Failed to create scan report: ${err.message}`);
|
|
6135
6519
|
process.exit(1);
|
|
6136
6520
|
}
|
|
6137
6521
|
}
|
|
@@ -6142,7 +6526,7 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6142
6526
|
try {
|
|
6143
6527
|
const current = await api.get(`/api/scans/${reportId}`);
|
|
6144
6528
|
if (current.status === "cancelled") {
|
|
6145
|
-
|
|
6529
|
+
log2(paint9("yellow", "Scan was cancelled \u2014 aborting."));
|
|
6146
6530
|
process.exit(0);
|
|
6147
6531
|
}
|
|
6148
6532
|
} catch {
|
|
@@ -6156,9 +6540,9 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6156
6540
|
apiUrl: config.apiUrl,
|
|
6157
6541
|
apiKey: config.apiKey,
|
|
6158
6542
|
runBrowse: runBrowseCommand2,
|
|
6159
|
-
onLog:
|
|
6543
|
+
onLog: log2,
|
|
6160
6544
|
onProgress: (phase, detail) => {
|
|
6161
|
-
|
|
6545
|
+
log2(`${paint9("dim", `[${phase}]`)} ${detail}`);
|
|
6162
6546
|
}
|
|
6163
6547
|
});
|
|
6164
6548
|
let wasCancelled = false;
|
|
@@ -6170,7 +6554,7 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6170
6554
|
} catch {
|
|
6171
6555
|
}
|
|
6172
6556
|
if (wasCancelled) {
|
|
6173
|
-
|
|
6557
|
+
log2(paint9("yellow", "Scan was cancelled by user \u2014 discarding results."));
|
|
6174
6558
|
process.exit(0);
|
|
6175
6559
|
}
|
|
6176
6560
|
await api.patch(`/api/scans/${reportId}`, {
|
|
@@ -6181,37 +6565,37 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6181
6565
|
scanDurationMs: result.scanDurationMs,
|
|
6182
6566
|
routesCrawled: result.routesCrawled
|
|
6183
6567
|
});
|
|
6184
|
-
|
|
6568
|
+
logOk2(`Scan complete \u2014 ${paint9("cyan", String(result.findings.length))} findings`);
|
|
6185
6569
|
console.log("");
|
|
6186
|
-
console.log(` ${
|
|
6570
|
+
console.log(` ${paint9("bold", "Summary:")} ${result.summary}`);
|
|
6187
6571
|
console.log("");
|
|
6188
6572
|
const high = result.findings.filter((f) => f.priority === "high");
|
|
6189
6573
|
const medium = result.findings.filter((f) => f.priority === "medium");
|
|
6190
6574
|
const low = result.findings.filter((f) => f.priority === "low");
|
|
6191
6575
|
if (high.length > 0) {
|
|
6192
|
-
console.log(` ${
|
|
6576
|
+
console.log(` ${paint9("bold", paint9("red", `High Priority (${high.length})`))}`);
|
|
6193
6577
|
for (const f of high) {
|
|
6194
|
-
console.log(` ${
|
|
6195
|
-
console.log(` ${
|
|
6578
|
+
console.log(` ${paint9("red", "\u25CF")} [${f.type}] ${f.title}`);
|
|
6579
|
+
console.log(` ${paint9("dim", f.description.slice(0, 120))}`);
|
|
6196
6580
|
}
|
|
6197
6581
|
console.log("");
|
|
6198
6582
|
}
|
|
6199
6583
|
if (medium.length > 0) {
|
|
6200
|
-
console.log(` ${
|
|
6584
|
+
console.log(` ${paint9("bold", paint9("yellow", `Medium Priority (${medium.length})`))}`);
|
|
6201
6585
|
for (const f of medium) {
|
|
6202
|
-
console.log(` ${
|
|
6586
|
+
console.log(` ${paint9("yellow", "\u25CF")} [${f.type}] ${f.title}`);
|
|
6203
6587
|
}
|
|
6204
6588
|
console.log("");
|
|
6205
6589
|
}
|
|
6206
6590
|
if (low.length > 0) {
|
|
6207
|
-
console.log(` ${
|
|
6591
|
+
console.log(` ${paint9("dim", `Low Priority (${low.length})`)} `);
|
|
6208
6592
|
for (const f of low) {
|
|
6209
|
-
console.log(` ${
|
|
6593
|
+
console.log(` ${paint9("dim", `\u25CB [${f.type}] ${f.title}`)}`);
|
|
6210
6594
|
}
|
|
6211
6595
|
console.log("");
|
|
6212
6596
|
}
|
|
6213
6597
|
} catch (err) {
|
|
6214
|
-
|
|
6598
|
+
logErr2(`Scan failed: ${err.message}`);
|
|
6215
6599
|
try {
|
|
6216
6600
|
await api.patch(`/api/scans/${reportId}`, {
|
|
6217
6601
|
status: "failed",
|
|
@@ -6224,7 +6608,7 @@ var scanCommand = new Command23("scan").description("Run a product scan on the c
|
|
|
6224
6608
|
});
|
|
6225
6609
|
|
|
6226
6610
|
// cli/commands/doctor.ts
|
|
6227
|
-
import { Command as
|
|
6611
|
+
import { Command as Command25 } from "commander";
|
|
6228
6612
|
import { existsSync as existsSync15 } from "fs";
|
|
6229
6613
|
import { homedir as homedir2 } from "os";
|
|
6230
6614
|
import { join as join11 } from "path";
|
|
@@ -6272,7 +6656,7 @@ async function checkProjectLink() {
|
|
|
6272
6656
|
optional: true
|
|
6273
6657
|
};
|
|
6274
6658
|
}
|
|
6275
|
-
var doctorCommand = new
|
|
6659
|
+
var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
|
|
6276
6660
|
const banner = [
|
|
6277
6661
|
``,
|
|
6278
6662
|
paint5("cyan", ` MR DOCTOR`),
|
|
@@ -6303,7 +6687,7 @@ var doctorCommand = new Command24("doctor").description("Diagnose Mr. Manager CL
|
|
|
6303
6687
|
console.log("");
|
|
6304
6688
|
return;
|
|
6305
6689
|
}
|
|
6306
|
-
const fixes = checks.filter((
|
|
6690
|
+
const fixes = checks.filter((c13) => !c13.ok && c13.fix && !c13.optional);
|
|
6307
6691
|
if (fixes.length > 0) {
|
|
6308
6692
|
console.log(paint5("yellow", " To fix:"));
|
|
6309
6693
|
for (const fix of fixes) {
|
|
@@ -6315,14 +6699,14 @@ var doctorCommand = new Command24("doctor").description("Diagnose Mr. Manager CL
|
|
|
6315
6699
|
});
|
|
6316
6700
|
|
|
6317
6701
|
// cli/commands/prompt-audit.ts
|
|
6318
|
-
import { Command as
|
|
6702
|
+
import { Command as Command26 } from "commander";
|
|
6319
6703
|
import { resolve as resolve8 } from "path";
|
|
6320
6704
|
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
|
|
6321
6705
|
function auditLine(label, tokens) {
|
|
6322
6706
|
const bar = "\u2588".repeat(Math.min(60, Math.round(tokens / 200)));
|
|
6323
6707
|
return ` ${label.padEnd(30)} ${formatTokenCount(tokens).padStart(8)} ${bar}`;
|
|
6324
6708
|
}
|
|
6325
|
-
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) => {
|
|
6326
6710
|
const results = [];
|
|
6327
6711
|
if (opts.task) {
|
|
6328
6712
|
try {
|
|
@@ -6527,8 +6911,8 @@ ${r.jobType} [${r.identifier}]`);
|
|
|
6527
6911
|
});
|
|
6528
6912
|
|
|
6529
6913
|
// cli/commands/skill.ts
|
|
6530
|
-
import { Command as
|
|
6531
|
-
var
|
|
6914
|
+
import { Command as Command27 } from "commander";
|
|
6915
|
+
var c10 = {
|
|
6532
6916
|
reset: "\x1B[0m",
|
|
6533
6917
|
bold: "\x1B[1m",
|
|
6534
6918
|
dim: "\x1B[2m",
|
|
@@ -6536,7 +6920,7 @@ var c9 = {
|
|
|
6536
6920
|
green: "\x1B[32m",
|
|
6537
6921
|
yellow: "\x1B[33m"
|
|
6538
6922
|
};
|
|
6539
|
-
var skillCommand = new
|
|
6923
|
+
var skillCommand = new Command27("skill").description("Manage skills \u2014 reusable playbooks for AI agents");
|
|
6540
6924
|
skillCommand.command("list").alias("ls").description("List all skills").option("--category <category>", "Filter by category").action(async (opts) => {
|
|
6541
6925
|
const params = new URLSearchParams();
|
|
6542
6926
|
if (opts.category) params.set("category", opts.category);
|
|
@@ -6544,17 +6928,17 @@ skillCommand.command("list").alias("ls").description("List all skills").option("
|
|
|
6544
6928
|
`/api/skills${params.toString() ? `?${params}` : ""}`
|
|
6545
6929
|
);
|
|
6546
6930
|
if (skills.length === 0) {
|
|
6547
|
-
console.log(`${
|
|
6931
|
+
console.log(`${c10.dim}No skills found.${c10.reset}`);
|
|
6548
6932
|
return;
|
|
6549
6933
|
}
|
|
6550
6934
|
for (const skill of skills) {
|
|
6551
|
-
const cat = skill.category ? ` ${
|
|
6552
|
-
const scope = skill.projectId ? ` ${
|
|
6553
|
-
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}`);
|
|
6554
6938
|
if (skill.description) {
|
|
6555
|
-
console.log(` ${
|
|
6939
|
+
console.log(` ${c10.dim}${skill.description}${c10.reset}`);
|
|
6556
6940
|
}
|
|
6557
|
-
console.log(` ${
|
|
6941
|
+
console.log(` ${c10.dim}id: ${skill.id}${c10.reset}`);
|
|
6558
6942
|
}
|
|
6559
6943
|
});
|
|
6560
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) => {
|
|
@@ -6590,7 +6974,7 @@ skillCommand.command("create").description("Create a new skill from a markdown f
|
|
|
6590
6974
|
projectId
|
|
6591
6975
|
});
|
|
6592
6976
|
console.log(
|
|
6593
|
-
`${
|
|
6977
|
+
`${c10.green}Created skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset} ${c10.dim}(${skill.id})${c10.reset}`
|
|
6594
6978
|
);
|
|
6595
6979
|
});
|
|
6596
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) => {
|
|
@@ -6604,40 +6988,158 @@ skillCommand.command("generate").alias("gen").description("Generate a new skill
|
|
|
6604
6988
|
process.exit(1);
|
|
6605
6989
|
}
|
|
6606
6990
|
}
|
|
6607
|
-
console.log(`${
|
|
6991
|
+
console.log(`${c10.dim}Generating skill...${c10.reset}`);
|
|
6608
6992
|
try {
|
|
6609
6993
|
const skill = await api.post("/api/skills/generate", {
|
|
6610
6994
|
prompt: prompt2,
|
|
6611
6995
|
projectId
|
|
6612
6996
|
});
|
|
6613
6997
|
console.log(
|
|
6614
|
-
`${
|
|
6998
|
+
`${c10.green}Generated skill:${c10.reset} ${c10.bold}${skill.name}${c10.reset}`
|
|
6615
6999
|
);
|
|
6616
7000
|
if (skill.description) {
|
|
6617
|
-
console.log(` ${
|
|
7001
|
+
console.log(` ${c10.dim}${skill.description}${c10.reset}`);
|
|
6618
7002
|
}
|
|
6619
7003
|
if (skill.category) {
|
|
6620
|
-
console.log(` ${
|
|
7004
|
+
console.log(` ${c10.dim}Category: ${skill.category}${c10.reset}`);
|
|
6621
7005
|
}
|
|
6622
|
-
console.log(` ${
|
|
7006
|
+
console.log(` ${c10.dim}id: ${skill.id}${c10.reset}`);
|
|
6623
7007
|
} catch (err) {
|
|
6624
7008
|
console.error(`Failed to generate skill: ${err.message}`);
|
|
6625
7009
|
process.exit(1);
|
|
6626
7010
|
}
|
|
6627
7011
|
});
|
|
6628
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
|
+
|
|
6629
7131
|
// cli/commands/tests.ts
|
|
6630
|
-
import { Command as
|
|
6631
|
-
var
|
|
7132
|
+
import { Command as Command29 } from "commander";
|
|
7133
|
+
var c12 = {
|
|
6632
7134
|
reset: "\x1B[0m",
|
|
6633
7135
|
dim: "\x1B[2m",
|
|
6634
7136
|
yellow: "\x1B[33m"
|
|
6635
7137
|
};
|
|
6636
|
-
var testsCommand = new
|
|
7138
|
+
var testsCommand = new Command29("tests").description("List MR Test scenarios for the linked project").action(async () => {
|
|
6637
7139
|
const projectId = getLinkedProjectId();
|
|
6638
7140
|
if (!projectId) {
|
|
6639
7141
|
console.error(
|
|
6640
|
-
`${
|
|
7142
|
+
`${c12.yellow}No project linked to this directory.${c12.reset} Run "mr link <project-id>" first.`
|
|
6641
7143
|
);
|
|
6642
7144
|
process.exit(1);
|
|
6643
7145
|
}
|
|
@@ -6650,13 +7152,13 @@ var testsCommand = new Command27("tests").description("List MR Test scenarios fo
|
|
|
6650
7152
|
process.exit(1);
|
|
6651
7153
|
}
|
|
6652
7154
|
if (scenarios.length === 0) {
|
|
6653
|
-
console.log(`${
|
|
7155
|
+
console.log(`${c12.dim}No test scenarios found for this project.${c12.reset}`);
|
|
6654
7156
|
return;
|
|
6655
7157
|
}
|
|
6656
7158
|
for (const scenario of scenarios) {
|
|
6657
7159
|
console.log(`### ${scenario.name}`);
|
|
6658
7160
|
if (scenario.description) {
|
|
6659
|
-
console.log(`${
|
|
7161
|
+
console.log(`${c12.dim}${scenario.description}${c12.reset}`);
|
|
6660
7162
|
console.log();
|
|
6661
7163
|
}
|
|
6662
7164
|
console.log(scenario.content);
|
|
@@ -6671,7 +7173,7 @@ var userArgs = process.argv.slice(2);
|
|
|
6671
7173
|
var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
|
|
6672
7174
|
var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
|
|
6673
7175
|
if (isFirstRun && !shouldBypass) {
|
|
6674
|
-
const
|
|
7176
|
+
const c13 = {
|
|
6675
7177
|
reset: "\x1B[0m",
|
|
6676
7178
|
bold: "\x1B[1m",
|
|
6677
7179
|
dim: "\x1B[2m",
|
|
@@ -6681,28 +7183,28 @@ if (isFirstRun && !shouldBypass) {
|
|
|
6681
7183
|
magenta: "\x1B[35m"
|
|
6682
7184
|
};
|
|
6683
7185
|
console.log("");
|
|
6684
|
-
console.log(`${
|
|
6685
|
-
console.log(`${
|
|
6686
|
-
console.log(`${
|
|
6687
|
-
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}`);
|
|
6688
7190
|
console.log("");
|
|
6689
|
-
console.log(`${
|
|
6690
|
-
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}`);
|
|
6691
7193
|
console.log("");
|
|
6692
|
-
console.log(` ${
|
|
6693
|
-
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}`);
|
|
6694
7196
|
console.log("");
|
|
6695
|
-
console.log(` ${
|
|
6696
|
-
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}`);
|
|
6697
7199
|
console.log("");
|
|
6698
|
-
console.log(` ${
|
|
6699
|
-
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}`);
|
|
6700
7202
|
console.log("");
|
|
6701
|
-
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}`);
|
|
6702
7204
|
console.log("");
|
|
6703
7205
|
process.exit(0);
|
|
6704
7206
|
}
|
|
6705
|
-
var program = new
|
|
7207
|
+
var program = new Command30();
|
|
6706
7208
|
program.name("mr").description("Mr. Manager - Task and project management CLI").version(CLI_VERSION);
|
|
6707
7209
|
program.addCommand(initCommand);
|
|
6708
7210
|
program.addCommand(authCommand);
|
|
@@ -6729,9 +7231,11 @@ program.addCommand(setPathCommand);
|
|
|
6729
7231
|
program.addCommand(testCommand);
|
|
6730
7232
|
program.addCommand(featuresCommand);
|
|
6731
7233
|
program.addCommand(noMrCommand);
|
|
7234
|
+
program.addCommand(reviewCommand);
|
|
6732
7235
|
program.addCommand(scanCommand);
|
|
6733
7236
|
program.addCommand(doctorCommand);
|
|
6734
7237
|
program.addCommand(promptAuditCommand);
|
|
6735
7238
|
program.addCommand(skillCommand);
|
|
7239
|
+
program.addCommand(resourceCommand);
|
|
6736
7240
|
program.addCommand(testsCommand);
|
|
6737
7241
|
program.parse();
|