@dunnewold-labs/mr-manager 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +823 -1025
- 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 Command28 } from "commander";
|
|
5
5
|
import { existsSync as existsSync15 } from "fs";
|
|
6
6
|
import { homedir as homedir4 } from "os";
|
|
7
7
|
import { join as join13 } from "path";
|
|
@@ -255,11 +255,9 @@ var tasksCommand = new Command5("tasks").description("List tasks for the linked
|
|
|
255
255
|
const params = new URLSearchParams();
|
|
256
256
|
params.set("projectId", projectId);
|
|
257
257
|
if (opts.inProgress) {
|
|
258
|
-
params.set("
|
|
258
|
+
params.set("status", "in_progress");
|
|
259
259
|
} else if (opts.completed) {
|
|
260
|
-
params.set("
|
|
261
|
-
} else if (!opts.all) {
|
|
262
|
-
params.set("completed", "false");
|
|
260
|
+
params.set("status", "completed");
|
|
263
261
|
}
|
|
264
262
|
path += `?${params.toString()}`;
|
|
265
263
|
const tasks = await api.get(path);
|
|
@@ -409,9 +407,9 @@ var contextCommand = new Command7("context").description("Output project context
|
|
|
409
407
|
const allTasks = await api.get(
|
|
410
408
|
`/api/projects/${projectId}/tasks`
|
|
411
409
|
);
|
|
412
|
-
const inProgress = allTasks.filter((t) => t.
|
|
413
|
-
const todo = allTasks.filter((t) =>
|
|
414
|
-
const recentlyCompleted = allTasks.filter((t) => t.completed).slice(0, 10);
|
|
410
|
+
const inProgress = allTasks.filter((t) => t.status === "in_progress" || t.status === "queued" || t.status === "delegated" || t.status === "review");
|
|
411
|
+
const todo = allTasks.filter((t) => t.status === "todo");
|
|
412
|
+
const recentlyCompleted = allTasks.filter((t) => t.status === "completed").slice(0, 10);
|
|
415
413
|
const summaryLines = [
|
|
416
414
|
`Project: ${project.name} (${project.status})`,
|
|
417
415
|
...inProgress.map((t) => `In Progress: ${t.title}`),
|
|
@@ -698,7 +696,7 @@ async function runTest(options) {
|
|
|
698
696
|
postUpdate,
|
|
699
697
|
onProgress
|
|
700
698
|
} = options;
|
|
701
|
-
const
|
|
699
|
+
const log3 = onProgress || (() => {
|
|
702
700
|
});
|
|
703
701
|
const result = {
|
|
704
702
|
status: "passed",
|
|
@@ -711,37 +709,37 @@ async function runTest(options) {
|
|
|
711
709
|
let wtPath = null;
|
|
712
710
|
const worktreeName = `mr-test-${taskId.slice(0, 8)}`;
|
|
713
711
|
const timeoutHandle = setTimeout(() => {
|
|
714
|
-
|
|
712
|
+
log3("Test timed out after 5 minutes");
|
|
715
713
|
if (devProc) devProc.kill("SIGTERM");
|
|
716
714
|
}, 5 * 60 * 1e3);
|
|
717
715
|
try {
|
|
718
|
-
|
|
716
|
+
log3("Extracting branch from MR/PR link...");
|
|
719
717
|
const branch = extractBranchFromLink(taskLink, localPath);
|
|
720
718
|
if (!branch) {
|
|
721
719
|
throw new Error(`Could not extract branch from link: ${taskLink}`);
|
|
722
720
|
}
|
|
723
|
-
|
|
724
|
-
|
|
721
|
+
log3(`Branch: ${branch}`);
|
|
722
|
+
log3("Creating git worktree...");
|
|
725
723
|
wtPath = createWorktree(localPath, branch, worktreeName);
|
|
726
|
-
|
|
727
|
-
|
|
724
|
+
log3(`Worktree created at ${wtPath}`);
|
|
725
|
+
log3("Installing dependencies...");
|
|
728
726
|
try {
|
|
729
727
|
installDependencies(wtPath);
|
|
730
728
|
} catch (err) {
|
|
731
|
-
|
|
729
|
+
log3(`Warning: dependency install failed: ${err.message}`);
|
|
732
730
|
}
|
|
733
|
-
|
|
731
|
+
log3("Starting dev server...");
|
|
734
732
|
const port = await findAvailablePort(4e3);
|
|
735
733
|
devProc = await startDevServer(wtPath, port);
|
|
736
734
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
737
|
-
|
|
735
|
+
log3(`Dev server running on ${baseUrl}`);
|
|
738
736
|
await browseRunner(["goto", baseUrl]);
|
|
739
737
|
const plan = customPlan || buildDefaultTestPlan(baseUrl);
|
|
740
|
-
|
|
738
|
+
log3(`Executing ${plan.length}-step test plan...`);
|
|
741
739
|
for (let i = 0; i < plan.length; i++) {
|
|
742
740
|
const step = plan[i];
|
|
743
741
|
const stepDesc = step.description || `${step.command} ${(step.args || []).join(" ")}`;
|
|
744
|
-
|
|
742
|
+
log3(`Step ${i + 1}/${plan.length}: ${stepDesc}`);
|
|
745
743
|
try {
|
|
746
744
|
if (step.command.startsWith("assert")) {
|
|
747
745
|
const assertResult = await evaluateAssertion(step, i, browseRunner);
|
|
@@ -791,7 +789,7 @@ async function runTest(options) {
|
|
|
791
789
|
}
|
|
792
790
|
} catch (err) {
|
|
793
791
|
result.errors.push(`Step ${i + 1} (${step.command}): ${err.message}`);
|
|
794
|
-
|
|
792
|
+
log3(`Step ${i + 1} error: ${err.message}`);
|
|
795
793
|
}
|
|
796
794
|
}
|
|
797
795
|
const totalAssertions = result.assertions.length;
|
|
@@ -1908,6 +1906,8 @@ var watchCommand = new Command8("watch").description(
|
|
|
1908
1906
|
const active = /* @__PURE__ */ new Map();
|
|
1909
1907
|
const failed = /* @__PURE__ */ new Map();
|
|
1910
1908
|
const queued = /* @__PURE__ */ new Set();
|
|
1909
|
+
const finishing = /* @__PURE__ */ new Set();
|
|
1910
|
+
let pollRunning = false;
|
|
1911
1911
|
const approvalQueue = [];
|
|
1912
1912
|
let approvalRunning = false;
|
|
1913
1913
|
const flags = [
|
|
@@ -1992,32 +1992,94 @@ var watchCommand = new Command8("watch").description(
|
|
|
1992
1992
|
active.set(task.id, { process: child, title: task.title, repoDir });
|
|
1993
1993
|
child.on("exit", async (code) => {
|
|
1994
1994
|
active.delete(task.id);
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
1995
|
+
finishing.add(task.id);
|
|
1996
|
+
try {
|
|
1997
|
+
if (code === 0) {
|
|
1998
|
+
try {
|
|
1999
|
+
const researchPath = resolve(repoDir, "research.md");
|
|
2000
|
+
if (existsSync6(researchPath)) {
|
|
2001
|
+
try {
|
|
2002
|
+
const researchContent = readFileSync5(researchPath, "utf-8");
|
|
2003
|
+
const existingResearch = existingResources.find((r) => r.type === "research");
|
|
2004
|
+
if (existingResearch) {
|
|
2005
|
+
await api.patch(`/api/tasks/${task.id}/resources/${existingResearch.id}`, {
|
|
2006
|
+
content: researchContent
|
|
2007
|
+
});
|
|
2008
|
+
logSuccess(prefix, `Updated existing research resource`);
|
|
2009
|
+
} else {
|
|
2010
|
+
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2011
|
+
type: "research",
|
|
2012
|
+
title: `Research \u2014 ${task.title}`,
|
|
2013
|
+
content: researchContent
|
|
2014
|
+
});
|
|
2015
|
+
logSuccess(prefix, `Uploaded research.md as task resource`);
|
|
2016
|
+
}
|
|
2017
|
+
unlinkSync(researchPath);
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (sessionId) {
|
|
2023
|
+
try {
|
|
2024
|
+
const sessionEntries = readAndParseSessionLog(sessionId, repoDir);
|
|
2025
|
+
if (sessionEntries && sessionEntries.length > 0) {
|
|
2026
|
+
const sessionContent = JSON.stringify(sessionEntries);
|
|
2027
|
+
const existingSessionLog = existingResources.find((r) => r.type === "session-log");
|
|
2028
|
+
if (existingSessionLog) {
|
|
2029
|
+
await api.patch(`/api/tasks/${task.id}/resources/${existingSessionLog.id}`, {
|
|
2030
|
+
content: sessionContent
|
|
2031
|
+
});
|
|
2032
|
+
logSuccess(prefix, `Updated session log resource (${sessionEntries.length} messages)`);
|
|
2033
|
+
} else {
|
|
2034
|
+
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2035
|
+
type: "session-log",
|
|
2036
|
+
title: `Session Log \u2014 ${task.title}`,
|
|
2037
|
+
content: sessionContent
|
|
2038
|
+
});
|
|
2039
|
+
logSuccess(prefix, `Uploaded session log (${sessionEntries.length} messages)`);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
logWarn(prefix, `Failed to upload session log: ${err.message}`);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const noMrPath = resolve(repoDir, ".mr-no-mr");
|
|
2047
|
+
const noMrRequested = existsSync6(noMrPath);
|
|
2048
|
+
let noMrDescription;
|
|
2049
|
+
if (noMrRequested) {
|
|
2050
|
+
noMrDescription = readFileSync5(noMrPath, "utf-8").trim();
|
|
2051
|
+
unlinkSync(noMrPath);
|
|
2052
|
+
logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
|
|
2053
|
+
}
|
|
2054
|
+
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2055
|
+
let prUrl = null;
|
|
2056
|
+
if (!noMrRequested) {
|
|
2057
|
+
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2058
|
+
if (!prUrl) {
|
|
2059
|
+
prUrl = await extractPrUrlFromUpdates(task.id);
|
|
2060
|
+
if (prUrl) {
|
|
2061
|
+
logInfo(prefix, `Found ${prLabel} URL from agent updates: ${paint("cyan", prUrl)}`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
if (prUrl) {
|
|
2065
|
+
logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
|
|
2008
2066
|
} else {
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
title: `Research \u2014 ${task.title}`,
|
|
2012
|
-
content: researchContent
|
|
2013
|
-
});
|
|
2014
|
-
logSuccess(prefix, `Uploaded research.md as task resource`);
|
|
2067
|
+
logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
|
|
2068
|
+
await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
|
|
2015
2069
|
}
|
|
2016
|
-
unlinkSync(researchPath);
|
|
2017
|
-
} catch (err) {
|
|
2018
|
-
logWarn(prefix, `Failed to upload research resource: ${err.message}`);
|
|
2019
2070
|
}
|
|
2071
|
+
await api.patch(`/api/tasks/${task.id}`, {
|
|
2072
|
+
status: "review",
|
|
2073
|
+
...prUrl ? { link: prUrl } : {}
|
|
2074
|
+
});
|
|
2075
|
+
logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
|
|
2076
|
+
await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2020
2079
|
}
|
|
2080
|
+
} else {
|
|
2081
|
+
logError(prefix, `"${paint("bold", task.title)}" failed (exit ${code}), leaving status unchanged`);
|
|
2082
|
+
await postTaskUpdate(task.id, `Agent failed with exit code ${code}`, "system");
|
|
2021
2083
|
if (sessionId) {
|
|
2022
2084
|
try {
|
|
2023
2085
|
const sessionEntries = readAndParseSessionLog(sessionId, repoDir);
|
|
@@ -2028,80 +2090,22 @@ var watchCommand = new Command8("watch").description(
|
|
|
2028
2090
|
await api.patch(`/api/tasks/${task.id}/resources/${existingSessionLog.id}`, {
|
|
2029
2091
|
content: sessionContent
|
|
2030
2092
|
});
|
|
2031
|
-
logSuccess(prefix, `Updated session log resource (${sessionEntries.length} messages)`);
|
|
2032
2093
|
} else {
|
|
2033
2094
|
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2034
2095
|
type: "session-log",
|
|
2035
2096
|
title: `Session Log \u2014 ${task.title}`,
|
|
2036
2097
|
content: sessionContent
|
|
2037
2098
|
});
|
|
2038
|
-
logSuccess(prefix, `Uploaded session log (${sessionEntries.length} messages)`);
|
|
2039
2099
|
}
|
|
2100
|
+
logInfo(prefix, `Uploaded session log for failed task (${sessionEntries.length} messages)`);
|
|
2040
2101
|
}
|
|
2041
|
-
} catch
|
|
2042
|
-
logWarn(prefix, `Failed to upload session log: ${err.message}`);
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
const noMrPath = resolve(repoDir, ".mr-no-mr");
|
|
2046
|
-
const noMrRequested = existsSync6(noMrPath);
|
|
2047
|
-
let noMrDescription;
|
|
2048
|
-
if (noMrRequested) {
|
|
2049
|
-
noMrDescription = readFileSync5(noMrPath, "utf-8").trim();
|
|
2050
|
-
unlinkSync(noMrPath);
|
|
2051
|
-
logSuccess(prefix, `No ${vcs === "gitlab" ? "MR" : "PR"} needed \u2014 ${noMrDescription}`);
|
|
2052
|
-
}
|
|
2053
|
-
const prLabel = vcs === "gitlab" ? "MR" : "PR";
|
|
2054
|
-
let prUrl = null;
|
|
2055
|
-
if (!noMrRequested) {
|
|
2056
|
-
prUrl = await findPrUrlAcrossRepos(branchName, repoDir, vcs);
|
|
2057
|
-
if (!prUrl) {
|
|
2058
|
-
prUrl = await extractPrUrlFromUpdates(task.id);
|
|
2059
|
-
if (prUrl) {
|
|
2060
|
-
logInfo(prefix, `Found ${prLabel} URL from agent updates: ${paint("cyan", prUrl)}`);
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
if (prUrl) {
|
|
2064
|
-
logSuccess(prefix, `${prLabel} ready: ${paint("cyan", prUrl)}`);
|
|
2065
|
-
} else {
|
|
2066
|
-
logWarn(prefix, `No ${prLabel} found for branch ${paint("cyan", branchName)}`);
|
|
2067
|
-
await postTaskUpdate(task.id, `Agent finished \u2014 no ${prLabel} found for branch ${branchName}`, "system");
|
|
2102
|
+
} catch {
|
|
2068
2103
|
}
|
|
2069
2104
|
}
|
|
2070
|
-
await api.patch(`/api/tasks/${task.id}`, {
|
|
2071
|
-
inProgress: false,
|
|
2072
|
-
readyForReview: true,
|
|
2073
|
-
...prUrl ? { link: prUrl } : {}
|
|
2074
|
-
});
|
|
2075
|
-
logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
|
|
2076
|
-
await postTaskUpdate(task.id, noMrRequested ? `Task marked ready for review \u2014 no ${prLabel} needed: ${noMrDescription}` : "Task marked ready for review", "system");
|
|
2077
|
-
} catch (err) {
|
|
2078
|
-
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2079
|
-
}
|
|
2080
|
-
} else {
|
|
2081
|
-
logError(prefix, `"${paint("bold", task.title)}" failed (exit ${code}), leaving status unchanged`);
|
|
2082
|
-
await postTaskUpdate(task.id, `Agent failed with exit code ${code}`, "system");
|
|
2083
|
-
if (sessionId) {
|
|
2084
|
-
try {
|
|
2085
|
-
const sessionEntries = readAndParseSessionLog(sessionId, repoDir);
|
|
2086
|
-
if (sessionEntries && sessionEntries.length > 0) {
|
|
2087
|
-
const sessionContent = JSON.stringify(sessionEntries);
|
|
2088
|
-
const existingSessionLog = existingResources.find((r) => r.type === "session-log");
|
|
2089
|
-
if (existingSessionLog) {
|
|
2090
|
-
await api.patch(`/api/tasks/${task.id}/resources/${existingSessionLog.id}`, {
|
|
2091
|
-
content: sessionContent
|
|
2092
|
-
});
|
|
2093
|
-
} else {
|
|
2094
|
-
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2095
|
-
type: "session-log",
|
|
2096
|
-
title: `Session Log \u2014 ${task.title}`,
|
|
2097
|
-
content: sessionContent
|
|
2098
|
-
});
|
|
2099
|
-
}
|
|
2100
|
-
logInfo(prefix, `Uploaded session log for failed task (${sessionEntries.length} messages)`);
|
|
2101
|
-
}
|
|
2102
|
-
} catch {
|
|
2103
|
-
}
|
|
2104
2105
|
}
|
|
2106
|
+
} finally {
|
|
2107
|
+
queued.delete(task.id);
|
|
2108
|
+
finishing.delete(task.id);
|
|
2105
2109
|
}
|
|
2106
2110
|
});
|
|
2107
2111
|
}
|
|
@@ -2138,50 +2142,54 @@ var watchCommand = new Command8("watch").description(
|
|
|
2138
2142
|
active.set(task.id, { process: child, title: task.title, repoDir });
|
|
2139
2143
|
child.on("exit", async (code) => {
|
|
2140
2144
|
active.delete(task.id);
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
const prdPath = resolve(repoDir, "prd.md");
|
|
2145
|
-
let prdContent;
|
|
2145
|
+
finishing.add(task.id);
|
|
2146
|
+
try {
|
|
2147
|
+
if (code === 0) {
|
|
2146
2148
|
try {
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
unlinkSync(prdPath);
|
|
2150
|
-
} catch {
|
|
2151
|
-
logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
|
|
2152
|
-
}
|
|
2153
|
-
if (prdContent) {
|
|
2149
|
+
const prdPath = resolve(repoDir, "prd.md");
|
|
2150
|
+
let prdContent;
|
|
2154
2151
|
try {
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2152
|
+
prdContent = readFileSync5(prdPath, "utf-8");
|
|
2153
|
+
logInfo(prefix, `Read PRD from ${paint("cyan", "prd.md")} (${prdContent.length} chars)`);
|
|
2154
|
+
unlinkSync(prdPath);
|
|
2155
|
+
} catch {
|
|
2156
|
+
logWarn(prefix, `No prd.md file found in ${repoDir} \u2014 PRD may have been posted inline`);
|
|
2157
|
+
}
|
|
2158
|
+
if (prdContent) {
|
|
2159
|
+
try {
|
|
2160
|
+
if (existingPlanResource) {
|
|
2161
|
+
await api.patch(`/api/tasks/${task.id}/resources/${existingPlanResource.id}`, {
|
|
2162
|
+
content: prdContent
|
|
2163
|
+
});
|
|
2164
|
+
logSuccess(prefix, `Updated existing PRD resource`);
|
|
2165
|
+
} else {
|
|
2166
|
+
await api.post(`/api/tasks/${task.id}/resources`, {
|
|
2167
|
+
type: "plan",
|
|
2168
|
+
title: `PRD \u2014 ${task.title}`,
|
|
2169
|
+
content: prdContent
|
|
2170
|
+
});
|
|
2171
|
+
logSuccess(prefix, `Uploaded PRD as task resource`);
|
|
2172
|
+
}
|
|
2173
|
+
} catch (err) {
|
|
2174
|
+
logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
|
|
2167
2175
|
}
|
|
2168
|
-
} catch (err) {
|
|
2169
|
-
logWarn(prefix, `Failed to upload PRD resource: ${err.message}`);
|
|
2170
2176
|
}
|
|
2177
|
+
await api.patch(`/api/tasks/${task.id}`, {
|
|
2178
|
+
status: "review",
|
|
2179
|
+
...prdContent ? { prdContent } : {}
|
|
2180
|
+
});
|
|
2181
|
+
logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
|
|
2182
|
+
await postTaskUpdate(task.id, "PRD generated \u2014 ready for review", "system");
|
|
2183
|
+
} catch (err) {
|
|
2184
|
+
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2171
2185
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
...prdContent ? { prdContent } : {}
|
|
2176
|
-
});
|
|
2177
|
-
logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
|
|
2178
|
-
await postTaskUpdate(task.id, "PRD generated \u2014 ready for review", "system");
|
|
2179
|
-
} catch (err) {
|
|
2180
|
-
logError(prefix, `Failed to update task: ${err.message}`);
|
|
2186
|
+
} else {
|
|
2187
|
+
logError(prefix, `"${paint("bold", task.title)}" PRD generation failed (exit ${code}), leaving status unchanged`);
|
|
2188
|
+
await postTaskUpdate(task.id, `PRD generation failed with exit code ${code}`, "system");
|
|
2181
2189
|
}
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2190
|
+
} finally {
|
|
2191
|
+
queued.delete(task.id);
|
|
2192
|
+
finishing.delete(task.id);
|
|
2185
2193
|
}
|
|
2186
2194
|
});
|
|
2187
2195
|
}
|
|
@@ -2212,42 +2220,48 @@ var watchCommand = new Command8("watch").description(
|
|
|
2212
2220
|
const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, proto.title);
|
|
2213
2221
|
active.set(`proto-${proto.id}`, { process: child, title: proto.title, repoDir });
|
|
2214
2222
|
child.on("exit", async (code) => {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2223
|
+
const key = `proto-${proto.id}`;
|
|
2224
|
+
active.delete(key);
|
|
2225
|
+
finishing.add(key);
|
|
2226
|
+
try {
|
|
2227
|
+
if (code === 0) {
|
|
2228
|
+
try {
|
|
2229
|
+
const protoPattern = /^prototype-\d+\.html$/;
|
|
2230
|
+
const found = readdirSync(repoDir).filter((f) => protoPattern.test(f)).sort();
|
|
2231
|
+
const files = found.map((f) => ({
|
|
2232
|
+
name: f,
|
|
2233
|
+
content: readFileSync5(resolve(repoDir, f), "utf-8")
|
|
2234
|
+
}));
|
|
2235
|
+
if (files.length === 0) {
|
|
2236
|
+
logError(prefix, `No prototype HTML files found in ${repoDir}`);
|
|
2237
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "completed", files });
|
|
2241
|
+
logSuccess(prefix, `"${paint("bold", proto.title)}" uploaded ${files.length} file(s)`);
|
|
2242
|
+
for (const file of files) {
|
|
2243
|
+
try {
|
|
2244
|
+
unlinkSync(resolve(repoDir, file.name));
|
|
2245
|
+
} catch {
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
logError(prefix, `Failed to upload prototype: ${err.message}`);
|
|
2233
2250
|
try {
|
|
2234
|
-
|
|
2251
|
+
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2235
2252
|
} catch {
|
|
2236
2253
|
}
|
|
2237
2254
|
}
|
|
2238
|
-
}
|
|
2239
|
-
logError(prefix, `
|
|
2255
|
+
} else {
|
|
2256
|
+
logError(prefix, `"${paint("bold", proto.title)}" prototype failed (exit ${code})`);
|
|
2240
2257
|
try {
|
|
2241
2258
|
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2242
2259
|
} catch {
|
|
2243
2260
|
}
|
|
2244
2261
|
}
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
await api.patch(`/api/prototypes/${proto.id}`, { status: "failed" });
|
|
2249
|
-
} catch {
|
|
2250
|
-
}
|
|
2262
|
+
} finally {
|
|
2263
|
+
queued.delete(key);
|
|
2264
|
+
finishing.delete(key);
|
|
2251
2265
|
}
|
|
2252
2266
|
});
|
|
2253
2267
|
}
|
|
@@ -2263,16 +2277,22 @@ var watchCommand = new Command8("watch").description(
|
|
|
2263
2277
|
const child = spawnAgent(agent, workDir, prompt2, prefix, void 0, project.name);
|
|
2264
2278
|
active.set(`repo-${project.id}`, { process: child, title: project.name, repoDir: workDir });
|
|
2265
2279
|
child.on("exit", async (code) => {
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2280
|
+
const key = `repo-${project.id}`;
|
|
2281
|
+
active.delete(key);
|
|
2282
|
+
finishing.add(key);
|
|
2283
|
+
try {
|
|
2284
|
+
if (code === 0) {
|
|
2285
|
+
logSuccess(prefix, `"${paint("bold", project.name)}" repo creation complete`);
|
|
2286
|
+
} else {
|
|
2287
|
+
logError(prefix, `"${paint("bold", project.name)}" repo creation failed (exit ${code})`);
|
|
2288
|
+
try {
|
|
2289
|
+
await api.patch(`/api/projects/${project.id}`, { repoCreationStatus: "failed" });
|
|
2290
|
+
} catch {
|
|
2291
|
+
}
|
|
2275
2292
|
}
|
|
2293
|
+
} finally {
|
|
2294
|
+
queued.delete(key);
|
|
2295
|
+
finishing.delete(key);
|
|
2276
2296
|
}
|
|
2277
2297
|
});
|
|
2278
2298
|
}
|
|
@@ -2290,85 +2310,91 @@ var watchCommand = new Command8("watch").description(
|
|
|
2290
2310
|
const child = spawnAgent(agent, repoDir, prompt2, prefix, void 0, idea.title);
|
|
2291
2311
|
active.set(`idea-${idea.id}`, { process: child, title: idea.title, repoDir });
|
|
2292
2312
|
child.on("exit", async (code) => {
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
let protoHtml;
|
|
2299
|
-
let followUpTasks;
|
|
2300
|
-
const planPath = resolve(repoDir, "idea-plan.md");
|
|
2301
|
-
const tasksPath = resolve(repoDir, "idea-tasks.json");
|
|
2302
|
-
const protoPath = resolve(repoDir, "idea-prototype.html");
|
|
2303
|
-
try {
|
|
2304
|
-
plan = readFileSync5(planPath, "utf-8");
|
|
2305
|
-
} catch {
|
|
2306
|
-
}
|
|
2307
|
-
try {
|
|
2308
|
-
protoHtml = readFileSync5(protoPath, "utf-8");
|
|
2309
|
-
} catch {
|
|
2310
|
-
}
|
|
2313
|
+
const key = `idea-${idea.id}`;
|
|
2314
|
+
active.delete(key);
|
|
2315
|
+
finishing.add(key);
|
|
2316
|
+
try {
|
|
2317
|
+
if (code === 0) {
|
|
2311
2318
|
try {
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2319
|
+
let plan;
|
|
2320
|
+
let protoHtml;
|
|
2321
|
+
let followUpTasks;
|
|
2322
|
+
const planPath = resolve(repoDir, "idea-plan.md");
|
|
2323
|
+
const tasksPath = resolve(repoDir, "idea-tasks.json");
|
|
2324
|
+
const protoPath = resolve(repoDir, "idea-prototype.html");
|
|
2325
|
+
try {
|
|
2326
|
+
plan = readFileSync5(planPath, "utf-8");
|
|
2327
|
+
} catch {
|
|
2316
2328
|
}
|
|
2317
|
-
} catch {
|
|
2318
|
-
}
|
|
2319
|
-
if (!plan && !protoHtml) {
|
|
2320
|
-
logError(prefix, `No output files found in ${repoDir}`);
|
|
2321
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2322
|
-
return;
|
|
2323
|
-
}
|
|
2324
|
-
const updateData = { status: "generated" };
|
|
2325
|
-
if (plan) updateData.plan = plan;
|
|
2326
|
-
if (followUpTasks) updateData.followUpTasks = followUpTasks;
|
|
2327
|
-
if (protoHtml) {
|
|
2328
2329
|
try {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2330
|
+
protoHtml = readFileSync5(protoPath, "utf-8");
|
|
2331
|
+
} catch {
|
|
2332
|
+
}
|
|
2333
|
+
try {
|
|
2334
|
+
const tasksRaw = readFileSync5(tasksPath, "utf-8");
|
|
2335
|
+
const parsed = JSON.parse(tasksRaw);
|
|
2336
|
+
if (Array.isArray(parsed)) {
|
|
2337
|
+
followUpTasks = parsed.filter((t) => t && typeof t === "object" && "title" in t);
|
|
2338
|
+
}
|
|
2339
|
+
} catch {
|
|
2340
|
+
}
|
|
2341
|
+
if (!plan && !protoHtml) {
|
|
2342
|
+
logError(prefix, `No output files found in ${repoDir}`);
|
|
2343
|
+
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const updateData = { status: "generated" };
|
|
2347
|
+
if (plan) updateData.plan = plan;
|
|
2348
|
+
if (followUpTasks) updateData.followUpTasks = followUpTasks;
|
|
2349
|
+
if (protoHtml) {
|
|
2350
|
+
try {
|
|
2351
|
+
const proto = await api.post("/api/prototypes", {
|
|
2352
|
+
title: `${idea.title} \u2014 Idea Prototype`,
|
|
2353
|
+
prompt: idea.description || idea.title,
|
|
2354
|
+
variantCount: 1,
|
|
2355
|
+
projectId: idea.projectId ?? null
|
|
2356
|
+
});
|
|
2357
|
+
await api.patch(`/api/prototypes/${proto.id}`, {
|
|
2358
|
+
status: "completed",
|
|
2359
|
+
files: [{ name: "idea-prototype.html", content: protoHtml }]
|
|
2360
|
+
});
|
|
2361
|
+
updateData.generatedPrototypeId = proto.id;
|
|
2362
|
+
logSuccess(prefix, `Prototype created: ${paint("gray", proto.id.slice(0, 8))}`);
|
|
2363
|
+
} catch (err) {
|
|
2364
|
+
logError(prefix, `Failed to create prototype: ${err.message}`);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
await api.patch(`/api/ideas/${idea.id}`, updateData);
|
|
2368
|
+
logSuccess(prefix, `"${paint("bold", idea.title)}" generation complete`);
|
|
2369
|
+
try {
|
|
2370
|
+
unlinkSync(planPath);
|
|
2371
|
+
} catch {
|
|
2372
|
+
}
|
|
2373
|
+
try {
|
|
2374
|
+
unlinkSync(tasksPath);
|
|
2375
|
+
} catch {
|
|
2376
|
+
}
|
|
2377
|
+
try {
|
|
2378
|
+
unlinkSync(protoPath);
|
|
2379
|
+
} catch {
|
|
2380
|
+
}
|
|
2381
|
+
} catch (err) {
|
|
2382
|
+
logError(prefix, `Failed to upload idea output: ${err.message}`);
|
|
2383
|
+
try {
|
|
2384
|
+
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2385
|
+
} catch {
|
|
2343
2386
|
}
|
|
2344
2387
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
try {
|
|
2348
|
-
unlinkSync(planPath);
|
|
2349
|
-
} catch {
|
|
2350
|
-
}
|
|
2351
|
-
try {
|
|
2352
|
-
unlinkSync(tasksPath);
|
|
2353
|
-
} catch {
|
|
2354
|
-
}
|
|
2355
|
-
try {
|
|
2356
|
-
unlinkSync(protoPath);
|
|
2357
|
-
} catch {
|
|
2358
|
-
}
|
|
2359
|
-
} catch (err) {
|
|
2360
|
-
logError(prefix, `Failed to upload idea output: ${err.message}`);
|
|
2388
|
+
} else {
|
|
2389
|
+
logError(prefix, `"${paint("bold", idea.title)}" generation failed (exit ${code})`);
|
|
2361
2390
|
try {
|
|
2362
2391
|
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2363
2392
|
} catch {
|
|
2364
2393
|
}
|
|
2365
2394
|
}
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
await api.patch(`/api/ideas/${idea.id}`, { status: "draft" });
|
|
2370
|
-
} catch {
|
|
2371
|
-
}
|
|
2395
|
+
} finally {
|
|
2396
|
+
queued.delete(key);
|
|
2397
|
+
finishing.delete(key);
|
|
2372
2398
|
}
|
|
2373
2399
|
});
|
|
2374
2400
|
}
|
|
@@ -2447,322 +2473,342 @@ ${divider}`);
|
|
|
2447
2473
|
}
|
|
2448
2474
|
}
|
|
2449
2475
|
async function poll() {
|
|
2450
|
-
|
|
2476
|
+
if (pollRunning) return;
|
|
2477
|
+
pollRunning = true;
|
|
2451
2478
|
try {
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
for (const [taskId, entry] of active) {
|
|
2461
|
-
if (taskId.startsWith("proto-") || taskId.startsWith("repo-") || taskId.startsWith("scan-")) continue;
|
|
2462
|
-
if (!inProgressIds.has(taskId)) {
|
|
2463
|
-
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer in-progress, terminating\u2026`);
|
|
2464
|
-
entry.process.kill("SIGTERM");
|
|
2465
|
-
active.delete(taskId);
|
|
2466
|
-
queued.delete(taskId);
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "idea-", "test-"];
|
|
2470
|
-
for (const taskId of failed.keys()) {
|
|
2471
|
-
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2472
|
-
if (!inProgressIds.has(taskId)) failed.delete(taskId);
|
|
2473
|
-
}
|
|
2474
|
-
for (const taskId of queued) {
|
|
2475
|
-
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2476
|
-
if (!inProgressIds.has(taskId)) queued.delete(taskId);
|
|
2477
|
-
}
|
|
2478
|
-
for (const task of tasks) {
|
|
2479
|
-
if (queued.has(task.id)) continue;
|
|
2480
|
-
if (failed.has(task.id)) continue;
|
|
2481
|
-
const sid = shortId(task.id);
|
|
2482
|
-
const prefix = taskTag(sid);
|
|
2483
|
-
const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
|
|
2484
|
-
if (!repoDir) {
|
|
2485
|
-
const reason = `no linked directory found under ${rootDir}`;
|
|
2486
|
-
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
2487
|
-
failed.set(task.id, reason);
|
|
2488
|
-
continue;
|
|
2479
|
+
let queuedTasks;
|
|
2480
|
+
let delegatedTasks;
|
|
2481
|
+
try {
|
|
2482
|
+
queuedTasks = await api.get("/api/tasks?status=queued");
|
|
2483
|
+
delegatedTasks = await api.get("/api/tasks?status=delegated");
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
logError(watchTag(), `Failed to fetch tasks: ${err.message}`);
|
|
2486
|
+
return;
|
|
2489
2487
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2488
|
+
const nonTestQueued = queuedTasks.filter((t) => t.mode !== "testing");
|
|
2489
|
+
const nonTestDelegated = delegatedTasks.filter((t) => t.mode !== "testing");
|
|
2490
|
+
const tasks = [...nonTestQueued, ...nonTestDelegated];
|
|
2491
|
+
const config = loadConfig();
|
|
2492
|
+
const activeTaskIds = new Set(tasks.map((t) => t.id));
|
|
2493
|
+
for (const [taskId, entry] of active) {
|
|
2494
|
+
if (taskId.startsWith("proto-") || taskId.startsWith("repo-") || taskId.startsWith("scan-")) continue;
|
|
2495
|
+
if (!activeTaskIds.has(taskId)) {
|
|
2496
|
+
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer active, terminating\u2026`);
|
|
2497
|
+
entry.process.kill("SIGTERM");
|
|
2498
|
+
active.delete(taskId);
|
|
2499
|
+
queued.delete(taskId);
|
|
2500
|
+
}
|
|
2495
2501
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
`${paint("yellow", "[dry-run]")} would dispatch "${paint("bold", task.title)}" (${paint("gray", task.id)}) in ${paint("cyan", repoDir)}`
|
|
2501
|
-
);
|
|
2502
|
-
continue;
|
|
2502
|
+
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "idea-", "test-"];
|
|
2503
|
+
for (const taskId of failed.keys()) {
|
|
2504
|
+
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2505
|
+
if (!activeTaskIds.has(taskId)) failed.delete(taskId);
|
|
2503
2506
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
processApprovalQueue();
|
|
2509
|
-
} else {
|
|
2510
|
-
dispatchTask(task, repoDir);
|
|
2507
|
+
for (const taskId of queued) {
|
|
2508
|
+
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2509
|
+
if (finishing.has(taskId)) continue;
|
|
2510
|
+
if (!activeTaskIds.has(taskId)) queued.delete(taskId);
|
|
2511
2511
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2512
|
+
for (const task of tasks) {
|
|
2513
|
+
if (queued.has(task.id)) continue;
|
|
2514
|
+
if (finishing.has(task.id)) continue;
|
|
2515
|
+
if (failed.has(task.id)) continue;
|
|
2516
|
+
const sid = shortId(task.id);
|
|
2517
|
+
const prefix = taskTag(sid);
|
|
2518
|
+
const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
|
|
2519
|
+
if (!repoDir) {
|
|
2520
|
+
const reason = `no linked directory found under ${rootDir}`;
|
|
2521
|
+
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
2522
|
+
failed.set(task.id, reason);
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
if (!existsSync6(repoDir)) {
|
|
2526
|
+
const reason = `linked directory "${repoDir}" does not exist`;
|
|
2527
|
+
logError(prefix, `"${task.title}": ${reason} \u2014 will not retry`);
|
|
2528
|
+
failed.set(task.id, reason);
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
if (task.status === "queued") {
|
|
2532
|
+
try {
|
|
2533
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "delegated" });
|
|
2534
|
+
} catch (err) {
|
|
2535
|
+
logError(prefix, `Failed to mark task as delegated: ${err.message}`);
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
queued.add(task.id);
|
|
2540
|
+
if (dryRun) {
|
|
2541
|
+
logInfo(
|
|
2542
|
+
watchTag(),
|
|
2543
|
+
`${paint("yellow", "[dry-run]")} would dispatch "${paint("bold", task.title)}" (${paint("gray", task.id)}) in ${paint("cyan", repoDir)}`
|
|
2544
|
+
);
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
if (task.mode === "planning") {
|
|
2548
|
+
dispatchPlanModeTask(task, repoDir);
|
|
2549
|
+
} else if (planApproval) {
|
|
2550
|
+
approvalQueue.push({ task, repoDir });
|
|
2551
|
+
processApprovalQueue();
|
|
2552
|
+
} else {
|
|
2553
|
+
dispatchTask(task, repoDir);
|
|
2554
|
+
}
|
|
2546
2555
|
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2556
|
+
let prototypes = [];
|
|
2557
|
+
try {
|
|
2558
|
+
prototypes = await api.get("/api/prototypes?status=in_progress");
|
|
2559
|
+
} catch (err) {
|
|
2560
|
+
logError(watchTag(), `Failed to fetch prototypes: ${err.message}`);
|
|
2551
2561
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2562
|
+
const inProgressProtoKeys = new Set(prototypes.map((p) => `proto-${p.id}`));
|
|
2563
|
+
for (const [key, entry] of active) {
|
|
2564
|
+
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key)) {
|
|
2565
|
+
logWarn(watchTag(), `Prototype ${paint("yellow", key)} no longer in_progress, terminating\u2026`);
|
|
2566
|
+
entry.process.kill("SIGTERM");
|
|
2567
|
+
active.delete(key);
|
|
2568
|
+
queued.delete(key);
|
|
2569
|
+
}
|
|
2556
2570
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
if (dryRun) {
|
|
2560
|
-
logInfo(
|
|
2561
|
-
watchTag(),
|
|
2562
|
-
`${paint("yellow", "[dry-run]")} would dispatch prototype "${paint("bold", proto.title)}" in ${paint("cyan", repoDir)}`
|
|
2563
|
-
);
|
|
2564
|
-
continue;
|
|
2571
|
+
for (const key of failed.keys()) {
|
|
2572
|
+
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key)) failed.delete(key);
|
|
2565
2573
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
let testTasks = [];
|
|
2569
|
-
try {
|
|
2570
|
-
testTasks = await api.get("/api/tasks?testStatus=pending");
|
|
2571
|
-
} catch (err) {
|
|
2572
|
-
logError(watchTag(), `Failed to fetch test tasks: ${err.message}`);
|
|
2573
|
-
}
|
|
2574
|
-
for (const task of testTasks) {
|
|
2575
|
-
const key = `test-${task.id}`;
|
|
2576
|
-
if (queued.has(key)) continue;
|
|
2577
|
-
if (failed.has(key)) continue;
|
|
2578
|
-
const sid = shortId(task.id);
|
|
2579
|
-
const prefix = testTag(sid);
|
|
2580
|
-
if (!task.link) {
|
|
2581
|
-
logWarn(prefix, `"${task.title}": no MR/PR link \u2014 skipping`);
|
|
2582
|
-
continue;
|
|
2574
|
+
for (const key of queued) {
|
|
2575
|
+
if (key.startsWith("proto-") && !inProgressProtoKeys.has(key) && !finishing.has(key)) queued.delete(key);
|
|
2583
2576
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
);
|
|
2590
|
-
continue;
|
|
2577
|
+
const activeProtoCount = [...active.keys()].filter((k) => k.startsWith("proto-")).length;
|
|
2578
|
+
let protoSlots = Math.max(0, 2 - activeProtoCount);
|
|
2579
|
+
for (const proto of prototypes) {
|
|
2580
|
+
if (protoSlots <= 0) break;
|
|
2581
|
+
const key = `proto-${proto.id}`;
|
|
2582
|
+
if (queued.has(key)) continue;
|
|
2583
|
+
if (finishing.has(key)) continue;
|
|
2584
|
+
if (failed.has(key)) continue;
|
|
2585
|
+
const sid = shortId(proto.id);
|
|
2586
|
+
const prefix = protoTag(sid);
|
|
2587
|
+
if (!proto.projectId) {
|
|
2588
|
+
logWarn(prefix, `"${proto.title}": no projectId \u2014 skipping`);
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
const repoDir = findDirectoryForProject(config, proto.projectId, rootDir);
|
|
2592
|
+
if (!repoDir) {
|
|
2593
|
+
logWarn(prefix, `"${proto.title}": no linked directory found \u2014 skipping`);
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
if (!existsSync6(repoDir)) {
|
|
2597
|
+
logError(prefix, `"${proto.title}": linked directory "${repoDir}" does not exist \u2014 skipping`);
|
|
2598
|
+
failed.set(key, `directory does not exist: ${repoDir}`);
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
queued.add(key);
|
|
2602
|
+
protoSlots--;
|
|
2603
|
+
if (dryRun) {
|
|
2604
|
+
logInfo(
|
|
2605
|
+
watchTag(),
|
|
2606
|
+
`${paint("yellow", "[dry-run]")} would dispatch prototype "${paint("bold", proto.title)}" in ${paint("cyan", repoDir)}`
|
|
2607
|
+
);
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
dispatchPrototypeJob(proto, repoDir);
|
|
2591
2611
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2612
|
+
const testTasks = queuedTasks.filter((t) => t.mode === "testing");
|
|
2613
|
+
for (const task of testTasks) {
|
|
2614
|
+
const key = `test-${task.id}`;
|
|
2615
|
+
if (queued.has(key)) continue;
|
|
2616
|
+
if (finishing.has(key)) continue;
|
|
2617
|
+
if (failed.has(key)) continue;
|
|
2618
|
+
const sid = shortId(task.id);
|
|
2619
|
+
const prefix = testTag(sid);
|
|
2620
|
+
if (!task.link) {
|
|
2621
|
+
logWarn(prefix, `"${task.title}": no MR/PR link \u2014 skipping`);
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
queued.add(key);
|
|
2625
|
+
if (dryRun) {
|
|
2626
|
+
logInfo(
|
|
2627
|
+
watchTag(),
|
|
2628
|
+
`${paint("yellow", "[dry-run]")} would run test for "${paint("bold", task.title)}"`
|
|
2629
|
+
);
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
(async () => {
|
|
2633
|
+
logDispatch(prefix, `Running test for "${paint("bold", task.title)}"\u2026`);
|
|
2611
2634
|
try {
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2635
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "delegated" });
|
|
2636
|
+
await postTaskUpdate(task.id, "Test started \u2014 setting up environment\u2026");
|
|
2637
|
+
let localPath;
|
|
2638
|
+
try {
|
|
2639
|
+
const project = await api.get(`/api/projects/${task.projectId}`);
|
|
2640
|
+
localPath = project.localPath ?? void 0;
|
|
2641
|
+
} catch {
|
|
2617
2642
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
const formData = new FormData();
|
|
2633
|
-
const blob = new Blob([imageBuffer], { type: "image/png" });
|
|
2634
|
-
formData.append("file", blob, fileName);
|
|
2635
|
-
formData.append("prefix", "test-screenshots");
|
|
2636
|
-
const uploadRes = await fetch(`${cfg.apiUrl}/api/upload`, {
|
|
2637
|
-
method: "POST",
|
|
2638
|
-
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
2639
|
-
body: formData
|
|
2640
|
-
});
|
|
2641
|
-
if (!uploadRes.ok) return null;
|
|
2642
|
-
const uploadData = await uploadRes.json();
|
|
2643
|
-
await api.post(`/api/tasks/${task.id}/updates`, {
|
|
2644
|
-
message,
|
|
2645
|
-
imageUrl: uploadData.url,
|
|
2646
|
-
source: "system"
|
|
2647
|
-
});
|
|
2648
|
-
return uploadData.url;
|
|
2649
|
-
} catch {
|
|
2650
|
-
return null;
|
|
2643
|
+
if (!localPath) {
|
|
2644
|
+
const cfg = loadConfig();
|
|
2645
|
+
localPath = findDirectoryForProject(cfg, task.projectId, rootDir) ?? void 0;
|
|
2646
|
+
}
|
|
2647
|
+
if (!localPath) {
|
|
2648
|
+
throw new Error("Local repo path not configured. Run `mr link` in the project directory.");
|
|
2649
|
+
}
|
|
2650
|
+
let testPlan;
|
|
2651
|
+
try {
|
|
2652
|
+
const resources = await api.get(`/api/tasks/${task.id}/resources`);
|
|
2653
|
+
const planResource = resources.find((r) => r.type === "test-plan");
|
|
2654
|
+
if (planResource) {
|
|
2655
|
+
testPlan = JSON.parse(planResource.content);
|
|
2656
|
+
logInfo(prefix, `Using agent-authored test plan (${testPlan.length} steps)`);
|
|
2651
2657
|
}
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2658
|
+
} catch {
|
|
2659
|
+
}
|
|
2660
|
+
const result = await runTest({
|
|
2661
|
+
taskId: task.id,
|
|
2662
|
+
taskLink: task.link,
|
|
2663
|
+
localPath,
|
|
2664
|
+
testPlan,
|
|
2665
|
+
browseRunner: runBrowseCommand2,
|
|
2666
|
+
uploadScreenshot: async (screenshotPath, message) => {
|
|
2667
|
+
try {
|
|
2668
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
2669
|
+
const cfg = loadConfig();
|
|
2670
|
+
const imageBuffer = readFileSync12(screenshotPath);
|
|
2671
|
+
const fileName = screenshotPath.split("/").pop() || "test-screenshot.png";
|
|
2672
|
+
const formData = new FormData();
|
|
2673
|
+
const blob = new Blob([imageBuffer], { type: "image/png" });
|
|
2674
|
+
formData.append("file", blob, fileName);
|
|
2675
|
+
formData.append("prefix", "test-screenshots");
|
|
2676
|
+
const uploadRes = await fetch(`${cfg.apiUrl}/api/upload`, {
|
|
2677
|
+
method: "POST",
|
|
2678
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
2679
|
+
body: formData
|
|
2680
|
+
});
|
|
2681
|
+
if (!uploadRes.ok) return null;
|
|
2682
|
+
const uploadData = await uploadRes.json();
|
|
2683
|
+
await api.post(`/api/tasks/${task.id}/updates`, {
|
|
2684
|
+
message,
|
|
2685
|
+
imageUrl: uploadData.url,
|
|
2686
|
+
source: "system"
|
|
2687
|
+
});
|
|
2688
|
+
return uploadData.url;
|
|
2689
|
+
} catch {
|
|
2690
|
+
return null;
|
|
2691
|
+
}
|
|
2692
|
+
},
|
|
2693
|
+
postUpdate: async (message, imageUrl) => {
|
|
2694
|
+
await postTaskUpdate(task.id, message);
|
|
2695
|
+
},
|
|
2696
|
+
onProgress: (msg) => logInfo(prefix, msg)
|
|
2697
|
+
});
|
|
2698
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: result.status });
|
|
2699
|
+
logSuccess(prefix, result.summary);
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
logError(prefix, `Test failed: ${err.message}`);
|
|
2702
|
+
try {
|
|
2703
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: "failed" });
|
|
2704
|
+
await postTaskUpdate(task.id, `Test failed: ${err.message}`);
|
|
2705
|
+
} catch {
|
|
2706
|
+
}
|
|
2707
|
+
failed.set(key, err.message);
|
|
2708
|
+
} finally {
|
|
2709
|
+
queued.delete(key);
|
|
2666
2710
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2711
|
+
})();
|
|
2712
|
+
}
|
|
2713
|
+
let pendingProjects = [];
|
|
2714
|
+
try {
|
|
2715
|
+
pendingProjects = await api.get("/api/projects?repoCreationStatus=pending");
|
|
2716
|
+
} catch (err) {
|
|
2717
|
+
logError(watchTag(), `Failed to fetch pending projects: ${err.message}`);
|
|
2718
|
+
}
|
|
2719
|
+
for (const project of pendingProjects) {
|
|
2720
|
+
const key = `repo-${project.id}`;
|
|
2721
|
+
if (queued.has(key)) continue;
|
|
2722
|
+
if (finishing.has(key)) continue;
|
|
2723
|
+
if (failed.has(key)) continue;
|
|
2724
|
+
queued.add(key);
|
|
2725
|
+
if (dryRun) {
|
|
2726
|
+
logInfo(
|
|
2727
|
+
watchTag(),
|
|
2728
|
+
`${paint("yellow", "[dry-run]")} would create repo for "${paint("bold", project.name)}" in ${paint("cyan", rootDir)}`
|
|
2729
|
+
);
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
dispatchRepoCreation(project, rootDir);
|
|
2733
|
+
}
|
|
2734
|
+
let generatingIdeas = [];
|
|
2735
|
+
try {
|
|
2736
|
+
generatingIdeas = await api.get("/api/ideas?status=generating");
|
|
2737
|
+
} catch (err) {
|
|
2738
|
+
logError(watchTag(), `Failed to fetch generating ideas: ${err.message}`);
|
|
2739
|
+
}
|
|
2740
|
+
const inProgressIdeaKeys = new Set(generatingIdeas.map((i) => `idea-${i.id}`));
|
|
2741
|
+
for (const [key, entry] of active) {
|
|
2742
|
+
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key)) {
|
|
2743
|
+
logWarn(watchTag(), `Idea ${paint("yellow", key)} no longer generating, terminating\u2026`);
|
|
2744
|
+
entry.process.kill("SIGTERM");
|
|
2745
|
+
active.delete(key);
|
|
2669
2746
|
queued.delete(key);
|
|
2670
2747
|
}
|
|
2671
|
-
})();
|
|
2672
|
-
}
|
|
2673
|
-
let pendingProjects = [];
|
|
2674
|
-
try {
|
|
2675
|
-
pendingProjects = await api.get("/api/projects?repoCreationStatus=pending");
|
|
2676
|
-
} catch (err) {
|
|
2677
|
-
logError(watchTag(), `Failed to fetch pending projects: ${err.message}`);
|
|
2678
|
-
}
|
|
2679
|
-
for (const project of pendingProjects) {
|
|
2680
|
-
const key = `repo-${project.id}`;
|
|
2681
|
-
if (queued.has(key)) continue;
|
|
2682
|
-
if (failed.has(key)) continue;
|
|
2683
|
-
queued.add(key);
|
|
2684
|
-
if (dryRun) {
|
|
2685
|
-
logInfo(
|
|
2686
|
-
watchTag(),
|
|
2687
|
-
`${paint("yellow", "[dry-run]")} would create repo for "${paint("bold", project.name)}" in ${paint("cyan", rootDir)}`
|
|
2688
|
-
);
|
|
2689
|
-
continue;
|
|
2690
2748
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
let generatingIdeas = [];
|
|
2694
|
-
try {
|
|
2695
|
-
generatingIdeas = await api.get("/api/ideas?status=generating");
|
|
2696
|
-
} catch (err) {
|
|
2697
|
-
logError(watchTag(), `Failed to fetch generating ideas: ${err.message}`);
|
|
2698
|
-
}
|
|
2699
|
-
const inProgressIdeaKeys = new Set(generatingIdeas.map((i) => `idea-${i.id}`));
|
|
2700
|
-
for (const [key, entry] of active) {
|
|
2701
|
-
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key)) {
|
|
2702
|
-
logWarn(watchTag(), `Idea ${paint("yellow", key)} no longer generating, terminating\u2026`);
|
|
2703
|
-
entry.process.kill("SIGTERM");
|
|
2704
|
-
active.delete(key);
|
|
2705
|
-
queued.delete(key);
|
|
2749
|
+
for (const key of failed.keys()) {
|
|
2750
|
+
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key)) failed.delete(key);
|
|
2706
2751
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2752
|
+
for (const key of queued) {
|
|
2753
|
+
if (key.startsWith("idea-") && !inProgressIdeaKeys.has(key) && !finishing.has(key)) queued.delete(key);
|
|
2754
|
+
}
|
|
2755
|
+
const activeIdeaCount = [...active.keys()].filter((k) => k.startsWith("idea-")).length;
|
|
2756
|
+
let ideaSlots = Math.max(0, 1 - activeIdeaCount);
|
|
2757
|
+
for (const idea of generatingIdeas) {
|
|
2758
|
+
if (ideaSlots <= 0) break;
|
|
2759
|
+
const key = `idea-${idea.id}`;
|
|
2760
|
+
if (queued.has(key)) continue;
|
|
2761
|
+
if (finishing.has(key)) continue;
|
|
2762
|
+
if (failed.has(key)) continue;
|
|
2763
|
+
const sid = shortId(idea.id);
|
|
2764
|
+
const prefix = ideaTag(sid);
|
|
2765
|
+
let repoDir;
|
|
2766
|
+
if (idea.projectId) {
|
|
2767
|
+
const dir = findDirectoryForProject(config, idea.projectId, rootDir);
|
|
2768
|
+
if (!dir) {
|
|
2769
|
+
logWarn(prefix, `"${idea.title}": no linked directory found \u2014 skipping`);
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
repoDir = dir;
|
|
2773
|
+
} else {
|
|
2774
|
+
repoDir = rootDir;
|
|
2775
|
+
}
|
|
2776
|
+
queued.add(key);
|
|
2777
|
+
ideaSlots--;
|
|
2778
|
+
if (dryRun) {
|
|
2779
|
+
logInfo(
|
|
2780
|
+
watchTag(),
|
|
2781
|
+
`${paint("yellow", "[dry-run]")} would dispatch idea "${paint("bold", idea.title)}" in ${paint("cyan", repoDir)}`
|
|
2782
|
+
);
|
|
2728
2783
|
continue;
|
|
2729
2784
|
}
|
|
2730
|
-
repoDir
|
|
2731
|
-
} else {
|
|
2732
|
-
repoDir = rootDir;
|
|
2785
|
+
dispatchIdeaJob(idea, repoDir);
|
|
2733
2786
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
`${paint("yellow", "[dry-run]")} would dispatch idea "${paint("bold", idea.title)}" in ${paint("cyan", repoDir)}`
|
|
2740
|
-
);
|
|
2741
|
-
continue;
|
|
2787
|
+
let pendingScans = [];
|
|
2788
|
+
try {
|
|
2789
|
+
pendingScans = await api.get("/api/scans?status=pending&limit=5");
|
|
2790
|
+
} catch (err) {
|
|
2791
|
+
logError(watchTag(), `Failed to fetch pending scans: ${err.message}`);
|
|
2742
2792
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
logInfo(
|
|
2760
|
-
watchTag(),
|
|
2761
|
-
`${paint("yellow", "[dry-run]")} would run scan ${paint("yellow", sid)} for project ${paint("cyan", scan.projectId)}`
|
|
2762
|
-
);
|
|
2763
|
-
continue;
|
|
2793
|
+
for (const scan of pendingScans) {
|
|
2794
|
+
const key = `scan-${scan.id}`;
|
|
2795
|
+
if (queued.has(key)) continue;
|
|
2796
|
+
if (finishing.has(key)) continue;
|
|
2797
|
+
if (failed.has(key)) continue;
|
|
2798
|
+
const sid = shortId(scan.id);
|
|
2799
|
+
const prefix = `${paint("magenta", `[scan:${sid}]`)}`;
|
|
2800
|
+
queued.add(key);
|
|
2801
|
+
if (dryRun) {
|
|
2802
|
+
logInfo(
|
|
2803
|
+
watchTag(),
|
|
2804
|
+
`${paint("yellow", "[dry-run]")} would run scan ${paint("yellow", sid)} for project ${paint("cyan", scan.projectId)}`
|
|
2805
|
+
);
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
dispatchScan(scan, prefix, key);
|
|
2764
2809
|
}
|
|
2765
|
-
|
|
2810
|
+
} finally {
|
|
2811
|
+
pollRunning = false;
|
|
2766
2812
|
}
|
|
2767
2813
|
}
|
|
2768
2814
|
async function shutdown() {
|
|
@@ -2859,21 +2905,20 @@ function printTaskBanner(action, title, id) {
|
|
|
2859
2905
|
}
|
|
2860
2906
|
var startCommand = new Command9("start").description("Mark a task as in-progress (you are working on it)").argument("<task-id>", "Task ID to start").action(async (taskId) => {
|
|
2861
2907
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2862
|
-
|
|
2863
|
-
delegated: false
|
|
2908
|
+
status: "in_progress"
|
|
2864
2909
|
});
|
|
2865
2910
|
printTaskBanner(paint2("green", "start"), task.title, task.id);
|
|
2866
2911
|
});
|
|
2867
|
-
var delegateCommand = new Command9("delegate").description("
|
|
2912
|
+
var delegateCommand = new Command9("delegate").description("Queue a task for the watch agent to pick up").argument("<task-id>", "Task ID to delegate").action(async (taskId) => {
|
|
2868
2913
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2869
|
-
|
|
2870
|
-
delegated: true
|
|
2914
|
+
status: "queued"
|
|
2871
2915
|
});
|
|
2872
2916
|
printTaskBanner(paint2("cyan", "delegate"), task.title, task.id);
|
|
2873
2917
|
});
|
|
2874
|
-
var undelegateCommand = new Command9("undelegate").description("Remove delegation from a task
|
|
2918
|
+
var undelegateCommand = new Command9("undelegate").description("Remove delegation from a task and return to todo").argument("<task-id>", "Task ID to undelegate").action(async (taskId) => {
|
|
2875
2919
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2876
|
-
|
|
2920
|
+
status: "todo",
|
|
2921
|
+
mode: "development"
|
|
2877
2922
|
});
|
|
2878
2923
|
printTaskBanner(paint2("yellow", "undelegate"), task.title, task.id);
|
|
2879
2924
|
});
|
|
@@ -2892,7 +2937,7 @@ var createCommand = new Command10("create").description("Create a new task in th
|
|
|
2892
2937
|
title,
|
|
2893
2938
|
projectId,
|
|
2894
2939
|
notes: opts.notes,
|
|
2895
|
-
|
|
2940
|
+
status: opts.inProgress ? "in_progress" : "todo"
|
|
2896
2941
|
});
|
|
2897
2942
|
const t = task;
|
|
2898
2943
|
console.log(`Created: ${t.title} (${t.id})`);
|
|
@@ -2927,8 +2972,7 @@ function printTaskBanner2(action, title, id) {
|
|
|
2927
2972
|
}
|
|
2928
2973
|
var completeCommand = new Command11("complete").description("Mark a task as completed").argument("<task-id>", "Task ID to complete").action(async (taskId) => {
|
|
2929
2974
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2930
|
-
|
|
2931
|
-
inProgress: false
|
|
2975
|
+
status: "completed"
|
|
2932
2976
|
});
|
|
2933
2977
|
printTaskBanner2(paint3("green", "complete \u2713"), task.title, task.id);
|
|
2934
2978
|
});
|
|
@@ -2943,9 +2987,11 @@ var subtaskCompleteCommand = new Command12("subtask-complete").description("Mark
|
|
|
2943
2987
|
console.log(`\u2713 Subtask completed: ${subtask.title}`);
|
|
2944
2988
|
});
|
|
2945
2989
|
|
|
2946
|
-
// cli/commands/
|
|
2990
|
+
// cli/commands/up.ts
|
|
2947
2991
|
import { Command as Command13 } from "commander";
|
|
2948
2992
|
import { spawn as spawn5 } from "child_process";
|
|
2993
|
+
import { resolve as resolve2 } from "path";
|
|
2994
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2949
2995
|
var c4 = {
|
|
2950
2996
|
reset: "\x1B[0m",
|
|
2951
2997
|
bold: "\x1B[1m",
|
|
@@ -2963,270 +3009,23 @@ function paint4(color, text) {
|
|
|
2963
3009
|
function timestamp2() {
|
|
2964
3010
|
return paint4("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
2965
3011
|
}
|
|
2966
|
-
function
|
|
2967
|
-
return paint4("
|
|
2968
|
-
}
|
|
2969
|
-
function shortId2(id) {
|
|
2970
|
-
return id.slice(0, 8);
|
|
3012
|
+
function upTag() {
|
|
3013
|
+
return paint4("cyan", "[up]");
|
|
2971
3014
|
}
|
|
2972
3015
|
function log(msg) {
|
|
2973
|
-
console.log(`${timestamp2()} ${
|
|
2974
|
-
}
|
|
2975
|
-
function logOk(msg) {
|
|
2976
|
-
console.log(`${timestamp2()} ${digestTag()} ${paint4("green", "\u2713")} ${msg}`);
|
|
3016
|
+
console.log(`${timestamp2()} ${upTag()} ${msg}`);
|
|
2977
3017
|
}
|
|
2978
3018
|
function logErr(msg) {
|
|
2979
|
-
console.error(`${timestamp2()} ${
|
|
2980
|
-
}
|
|
2981
|
-
function buildPrompt(messages) {
|
|
2982
|
-
const emailsText = messages.map(
|
|
2983
|
-
(msg, i) => `Email ${i + 1}:
|
|
2984
|
-
ID: ${msg.id}
|
|
2985
|
-
Subject: ${msg.subject}
|
|
2986
|
-
From: ${msg.sender} <${msg.senderEmail}>
|
|
2987
|
-
Date: ${msg.date}
|
|
2988
|
-
Snippet: ${msg.snippet}
|
|
2989
|
-
Body preview: ${msg.body.slice(0, 500)}`
|
|
2990
|
-
).join("\n\n---\n\n");
|
|
2991
|
-
return `You are an email triage assistant. Analyze the following unread emails and produce a structured digest.
|
|
2992
|
-
|
|
2993
|
-
For each email, determine:
|
|
2994
|
-
1. Is it relevant/actionable (requires attention, is personal, work-related, or important)?
|
|
2995
|
-
2. Or is it not relevant (newsletters, marketing, automated notifications, promotions)?
|
|
2996
|
-
3. Assign a category that groups it with similar emails (e.g. "Finance & Trading", "Real Estate", "Work", "Personal", "Promotions & Newsletters", "Travel", "Health", etc.).
|
|
2997
|
-
4. Write a 2-3 sentence summary of the email's content and what action (if any) is needed.
|
|
2998
|
-
|
|
2999
|
-
Also write a 2-3 sentence executive summary of the overall inbox state.
|
|
3000
|
-
|
|
3001
|
-
Here are the unread emails:
|
|
3002
|
-
|
|
3003
|
-
${emailsText}
|
|
3004
|
-
|
|
3005
|
-
Respond with a JSON object in this exact format:
|
|
3006
|
-
{
|
|
3007
|
-
"summary": "2-3 sentence overview of the inbox",
|
|
3008
|
-
"items": [
|
|
3009
|
-
{
|
|
3010
|
-
"emailId": "the email ID",
|
|
3011
|
-
"summary": "2-3 sentence summary of this email's content and any action needed",
|
|
3012
|
-
"isRelevant": true or false,
|
|
3013
|
-
"relevanceReason": "brief explanation of why relevant or not",
|
|
3014
|
-
"category": "short category label (2-4 words)"
|
|
3015
|
-
}
|
|
3016
|
-
]
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
Be conservative with "isRelevant: false" \u2014 only mark as not relevant if clearly promotional, newsletter, or automated. Personal emails, work emails, and anything requiring a response should be relevant. Group similar emails under the same category label.`;
|
|
3020
|
-
}
|
|
3021
|
-
function runAgent(prompt2, agent) {
|
|
3022
|
-
return new Promise((resolve7, reject) => {
|
|
3023
|
-
let bin;
|
|
3024
|
-
let args;
|
|
3025
|
-
if (agent === "codex") {
|
|
3026
|
-
bin = "codex";
|
|
3027
|
-
args = ["exec", "--full-auto", prompt2];
|
|
3028
|
-
} else {
|
|
3029
|
-
bin = "claude";
|
|
3030
|
-
args = ["-p", "--dangerously-skip-permissions", prompt2];
|
|
3031
|
-
}
|
|
3032
|
-
const child = spawn5(bin, args, {
|
|
3033
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3034
|
-
});
|
|
3035
|
-
let output = "";
|
|
3036
|
-
let errOutput = "";
|
|
3037
|
-
child.stdout?.on("data", (d) => {
|
|
3038
|
-
output += d.toString();
|
|
3039
|
-
});
|
|
3040
|
-
child.stderr?.on("data", (d) => {
|
|
3041
|
-
errOutput += d.toString();
|
|
3042
|
-
});
|
|
3043
|
-
child.on("exit", (code) => {
|
|
3044
|
-
if (code === 0) resolve7(output.trim());
|
|
3045
|
-
else reject(new Error(`${bin} exited with code ${code}
|
|
3046
|
-
${errOutput.trim()}`));
|
|
3047
|
-
});
|
|
3048
|
-
});
|
|
3049
|
-
}
|
|
3050
|
-
async function processDigest(digest, agent) {
|
|
3051
|
-
const sid = shortId2(digest.id);
|
|
3052
|
-
if (digest.rawMessages.length === 0) {
|
|
3053
|
-
await api.post(`/api/email-digest/${digest.id}/complete`, {
|
|
3054
|
-
summary: "No unread emails found in your inbox.",
|
|
3055
|
-
items: []
|
|
3056
|
-
});
|
|
3057
|
-
logOk(`${paint4("yellow", sid)} completed (no emails)`);
|
|
3058
|
-
return;
|
|
3059
|
-
}
|
|
3060
|
-
log(`${paint4("yellow", sid)} analyzing ${paint4("cyan", String(digest.rawMessages.length))} emails\u2026`);
|
|
3061
|
-
const prompt2 = buildPrompt(digest.rawMessages);
|
|
3062
|
-
const output = await runAgent(prompt2, agent);
|
|
3063
|
-
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
3064
|
-
if (!jsonMatch) {
|
|
3065
|
-
throw new Error("Could not extract JSON from Claude output");
|
|
3066
|
-
}
|
|
3067
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
3068
|
-
const msgMap = new Map(digest.rawMessages.map((m) => [m.id, m]));
|
|
3069
|
-
const items = parsed.items.map((item) => {
|
|
3070
|
-
const msg = msgMap.get(item.emailId);
|
|
3071
|
-
if (!msg) return null;
|
|
3072
|
-
return {
|
|
3073
|
-
emailId: msg.id,
|
|
3074
|
-
threadId: msg.threadId,
|
|
3075
|
-
subject: msg.subject,
|
|
3076
|
-
sender: msg.sender,
|
|
3077
|
-
senderEmail: msg.senderEmail,
|
|
3078
|
-
date: msg.date,
|
|
3079
|
-
snippet: msg.snippet,
|
|
3080
|
-
summary: item.summary || msg.snippet,
|
|
3081
|
-
isRelevant: item.isRelevant,
|
|
3082
|
-
relevanceReason: item.relevanceReason,
|
|
3083
|
-
category: item.category || (item.isRelevant ? "General" : "Promotions & Newsletters"),
|
|
3084
|
-
archived: false
|
|
3085
|
-
};
|
|
3086
|
-
}).filter((item) => item !== null);
|
|
3087
|
-
await api.post(`/api/email-digest/${digest.id}/complete`, {
|
|
3088
|
-
summary: parsed.summary,
|
|
3089
|
-
items
|
|
3090
|
-
});
|
|
3091
|
-
const relevantItems = items.filter((i) => i.isRelevant);
|
|
3092
|
-
const noiseItems = items.filter((i) => !i.isRelevant);
|
|
3093
|
-
logOk(
|
|
3094
|
-
`${paint4("yellow", sid)} done \u2014 ${paint4("green", String(relevantItems.length))} relevant, ${paint4("gray", String(noiseItems.length))} not relevant`
|
|
3095
|
-
);
|
|
3096
|
-
console.log("");
|
|
3097
|
-
console.log(` ${paint4("bold", "Summary:")} ${parsed.summary}`);
|
|
3098
|
-
console.log("");
|
|
3099
|
-
if (relevantItems.length > 0) {
|
|
3100
|
-
console.log(` ${paint4("bold", paint4("green", "Important"))}`);
|
|
3101
|
-
for (const item of relevantItems) {
|
|
3102
|
-
console.log(` ${paint4("cyan", "\u2022")} ${item.subject} ${paint4("dim", `\u2014 ${item.sender}`)}`);
|
|
3103
|
-
}
|
|
3104
|
-
console.log("");
|
|
3105
|
-
}
|
|
3106
|
-
if (noiseItems.length > 0) {
|
|
3107
|
-
console.log(` ${paint4("dim", "Not important")}`);
|
|
3108
|
-
for (const item of noiseItems) {
|
|
3109
|
-
console.log(` ${paint4("dim", `\u2022 ${item.subject} \u2014 ${item.sender}`)}`);
|
|
3110
|
-
}
|
|
3111
|
-
console.log("");
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
async function generateDigest() {
|
|
3115
|
-
let result;
|
|
3116
|
-
try {
|
|
3117
|
-
result = await api.post("/api/email-digest/generate");
|
|
3118
|
-
} catch (err) {
|
|
3119
|
-
logErr(`Auto-generate failed: ${err.message}`);
|
|
3120
|
-
return;
|
|
3121
|
-
}
|
|
3122
|
-
if (result.needsReauth) {
|
|
3123
|
-
logErr("Auto-generate skipped: Gmail re-authentication required");
|
|
3124
|
-
return;
|
|
3125
|
-
}
|
|
3126
|
-
if (result.status === "completed") {
|
|
3127
|
-
log(`Auto-generated digest ${paint4("yellow", shortId2(result.id))} \u2014 no new emails`);
|
|
3128
|
-
} else {
|
|
3129
|
-
log(`Auto-generated digest ${paint4("yellow", shortId2(result.id))} \u2014 queued for processing`);
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
var digestCommand = new Command13("digest").description("Process pending email digests using an AI coding agent (run alongside mr watch)").option("--interval <seconds>", "Polling interval in seconds", "15").option("--generate-interval <seconds>", "How often to auto-generate new digests", "3600").option("--once", "Process pending digests once and exit", false).option("--agent <agent>", "AI agent to use: claude or codex", "claude").action(async (opts) => {
|
|
3133
|
-
const intervalMs = parseInt(opts.interval, 10) * 1e3;
|
|
3134
|
-
const generateIntervalMs = parseInt(opts.generateInterval, 10) * 1e3;
|
|
3135
|
-
const once = opts.once;
|
|
3136
|
-
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
3137
|
-
const processing = /* @__PURE__ */ new Set();
|
|
3138
|
-
const banner = [
|
|
3139
|
-
``,
|
|
3140
|
-
paint4("magenta", ` \u2554\u2566\u2557\u2566\u2550\u2557 \u2554\u2566\u2557\u2566\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557`),
|
|
3141
|
-
paint4("magenta", ` \u2551\u2551\u2551\u2560\u2566\u255D \u2551\u2551\u2551\u2551 \u2566\u2551\u2563 \u255A\u2550\u2557 \u2551`),
|
|
3142
|
-
paint4("magenta", ` \u2569 \u2569\u2569\u255A\u2550 \u2550\u2569\u255D\u2569\u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u2569`),
|
|
3143
|
-
paint4("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
3144
|
-
paint4("dim", ` email digest processor \xB7 powered by ${agent}`),
|
|
3145
|
-
``
|
|
3146
|
-
].join("\n");
|
|
3147
|
-
console.log(banner);
|
|
3148
|
-
console.log(
|
|
3149
|
-
` interval=${paint4("cyan", opts.interval + "s")} generate-interval=${paint4("cyan", opts.generateInterval + "s")}${once ? ` ${paint4("yellow", "once")}` : ""}
|
|
3150
|
-
`
|
|
3151
|
-
);
|
|
3152
|
-
async function poll() {
|
|
3153
|
-
let pending;
|
|
3154
|
-
try {
|
|
3155
|
-
pending = await api.get("/api/email-digest/pending");
|
|
3156
|
-
} catch (err) {
|
|
3157
|
-
logErr(`Failed to fetch pending digests: ${err.message}`);
|
|
3158
|
-
return;
|
|
3159
|
-
}
|
|
3160
|
-
if (pending.length === 0) return;
|
|
3161
|
-
const digest = pending[pending.length - 1];
|
|
3162
|
-
if (processing.has(digest.id)) return;
|
|
3163
|
-
log(`Processing digest ${paint4("yellow", shortId2(digest.id))} (${paint4("cyan", String(digest.rawMessages.length))} emails)`);
|
|
3164
|
-
processing.add(digest.id);
|
|
3165
|
-
processDigest(digest, agent).catch((err) => logErr(`${shortId2(digest.id)} failed: ${err.message}`)).finally(() => processing.delete(digest.id));
|
|
3166
|
-
}
|
|
3167
|
-
process.on("SIGINT", () => {
|
|
3168
|
-
console.log(`
|
|
3169
|
-
${timestamp2()} ${digestTag()} Shutting down\u2026`);
|
|
3170
|
-
process.exit(0);
|
|
3171
|
-
});
|
|
3172
|
-
await generateDigest();
|
|
3173
|
-
await poll();
|
|
3174
|
-
if (once) {
|
|
3175
|
-
const wait = () => new Promise((resolve7) => {
|
|
3176
|
-
if (processing.size === 0) return resolve7();
|
|
3177
|
-
const check = setInterval(() => {
|
|
3178
|
-
if (processing.size === 0) {
|
|
3179
|
-
clearInterval(check);
|
|
3180
|
-
resolve7();
|
|
3181
|
-
}
|
|
3182
|
-
}, 500);
|
|
3183
|
-
});
|
|
3184
|
-
await wait();
|
|
3185
|
-
} else {
|
|
3186
|
-
setInterval(poll, intervalMs);
|
|
3187
|
-
setInterval(generateDigest, generateIntervalMs);
|
|
3188
|
-
}
|
|
3189
|
-
});
|
|
3190
|
-
|
|
3191
|
-
// cli/commands/up.ts
|
|
3192
|
-
import { Command as Command14 } from "commander";
|
|
3193
|
-
import { spawn as spawn6 } from "child_process";
|
|
3194
|
-
import { resolve as resolve2 } from "path";
|
|
3195
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3196
|
-
var c5 = {
|
|
3197
|
-
reset: "\x1B[0m",
|
|
3198
|
-
bold: "\x1B[1m",
|
|
3199
|
-
dim: "\x1B[2m",
|
|
3200
|
-
cyan: "\x1B[36m",
|
|
3201
|
-
green: "\x1B[32m",
|
|
3202
|
-
yellow: "\x1B[33m",
|
|
3203
|
-
red: "\x1B[31m",
|
|
3204
|
-
magenta: "\x1B[35m",
|
|
3205
|
-
gray: "\x1B[90m"
|
|
3206
|
-
};
|
|
3207
|
-
function paint5(color, text) {
|
|
3208
|
-
return `${c5[color]}${text}${c5.reset}`;
|
|
3209
|
-
}
|
|
3210
|
-
function timestamp3() {
|
|
3211
|
-
return paint5("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
3212
|
-
}
|
|
3213
|
-
function upTag() {
|
|
3214
|
-
return paint5("cyan", "[up]");
|
|
3215
|
-
}
|
|
3216
|
-
function log2(msg) {
|
|
3217
|
-
console.log(`${timestamp3()} ${upTag()} ${msg}`);
|
|
3218
|
-
}
|
|
3219
|
-
function logErr2(msg) {
|
|
3220
|
-
console.error(`${timestamp3()} ${upTag()} ${paint5("red", "\u2717")} ${msg}`);
|
|
3019
|
+
console.error(`${timestamp2()} ${upTag()} ${paint4("red", "\u2717")} ${msg}`);
|
|
3221
3020
|
}
|
|
3222
3021
|
function spawnMr(args) {
|
|
3223
3022
|
const entry = process.argv[1];
|
|
3224
|
-
const child =
|
|
3023
|
+
const child = spawn5(process.execPath, [entry, ...args], {
|
|
3225
3024
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3226
3025
|
});
|
|
3227
3026
|
return child;
|
|
3228
3027
|
}
|
|
3229
|
-
var upCommand = new
|
|
3028
|
+
var upCommand = new Command13("up").description("Run mr watch and mr digest together with a single command").option("--interval <seconds>", "Polling interval for both watch and digest", "15").option("--watch-interval <seconds>", "Override polling interval for mr watch").option("--digest-interval <seconds>", "Override polling interval for mr digest").option("--dry-run", "Pass --dry-run to mr watch", false).option("--plan-approval", "Pass --plan-approval to mr watch", false).option("--root <dir>", "Pass --root to mr watch (default: cwd)").option("--agent <agent>", "AI agent to use: claude or codex", "claude").action((opts) => {
|
|
3230
3029
|
const watchInterval = opts.watchInterval ?? opts.interval;
|
|
3231
3030
|
const digestInterval = opts.digestInterval ?? opts.interval;
|
|
3232
3031
|
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
@@ -3237,21 +3036,21 @@ var upCommand = new Command14("up").description("Run mr watch and mr digest toge
|
|
|
3237
3036
|
const digestArgs = ["digest", "--interval", digestInterval, "--agent", agent];
|
|
3238
3037
|
const banner = [
|
|
3239
3038
|
``,
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3039
|
+
paint4("cyan", ` \u2554\u2566\u2557\u2566\u2550\u2557 \u2566 \u2566\u2554\u2550\u2557`),
|
|
3040
|
+
paint4("magenta", ` \u2551\u2551\u2551\u2560\u2566\u255D \u2551 \u2551\u2560\u2550\u255D`),
|
|
3041
|
+
paint4("cyan", ` \u2569 \u2569\u2569\u255A\u2550 \u255A\u2550\u255D\u2569 `),
|
|
3042
|
+
paint4("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
3043
|
+
paint4("dim", ` mr watch + mr digest \xB7 powered by ${agent}`),
|
|
3245
3044
|
``
|
|
3246
3045
|
].join("\n");
|
|
3247
3046
|
console.log(banner);
|
|
3248
3047
|
const flags = [
|
|
3249
|
-
`watch-interval=${
|
|
3250
|
-
`digest-interval=${
|
|
3251
|
-
`agent=${
|
|
3252
|
-
...opts.dryRun ? [
|
|
3253
|
-
...opts.planApproval ? [
|
|
3254
|
-
...opts.root ? [`root=${
|
|
3048
|
+
`watch-interval=${paint4("cyan", watchInterval + "s")}`,
|
|
3049
|
+
`digest-interval=${paint4("cyan", digestInterval + "s")}`,
|
|
3050
|
+
`agent=${paint4("cyan", agent)}`,
|
|
3051
|
+
...opts.dryRun ? [paint4("yellow", "dry-run")] : [],
|
|
3052
|
+
...opts.planApproval ? [paint4("yellow", "plan-approval")] : [],
|
|
3053
|
+
...opts.root ? [`root=${paint4("cyan", resolve2(opts.root))}`] : []
|
|
3255
3054
|
].join(" ");
|
|
3256
3055
|
console.log(` ${flags}
|
|
3257
3056
|
`);
|
|
@@ -3262,12 +3061,12 @@ var upCommand = new Command14("up").description("Run mr watch and mr digest toge
|
|
|
3262
3061
|
proc.stderr?.on("data", (d) => process.stderr.write(d));
|
|
3263
3062
|
proc.on("exit", (code, signal) => {
|
|
3264
3063
|
if (signal !== "SIGTERM" && signal !== "SIGINT") {
|
|
3265
|
-
|
|
3064
|
+
logErr(`mr ${label} exited unexpectedly (code=${code ?? "?"}, signal=${signal ?? "none"})`);
|
|
3266
3065
|
}
|
|
3267
3066
|
});
|
|
3268
3067
|
}
|
|
3269
3068
|
function shutdown() {
|
|
3270
|
-
|
|
3069
|
+
log("Shutting down\u2026");
|
|
3271
3070
|
watchProc.kill("SIGTERM");
|
|
3272
3071
|
digestProc.kill("SIGTERM");
|
|
3273
3072
|
setTimeout(() => process.exit(0), 500);
|
|
@@ -3277,8 +3076,8 @@ var upCommand = new Command14("up").description("Run mr watch and mr digest toge
|
|
|
3277
3076
|
});
|
|
3278
3077
|
|
|
3279
3078
|
// cli/commands/prototype.ts
|
|
3280
|
-
import { Command as
|
|
3281
|
-
var
|
|
3079
|
+
import { Command as Command14 } from "commander";
|
|
3080
|
+
var c5 = {
|
|
3282
3081
|
reset: "\x1B[0m",
|
|
3283
3082
|
bold: "\x1B[1m",
|
|
3284
3083
|
dim: "\x1B[2m",
|
|
@@ -3289,25 +3088,25 @@ var c6 = {
|
|
|
3289
3088
|
blue: "\x1B[34m",
|
|
3290
3089
|
gray: "\x1B[90m"
|
|
3291
3090
|
};
|
|
3292
|
-
function
|
|
3293
|
-
return `${
|
|
3091
|
+
function paint5(color, text) {
|
|
3092
|
+
return `${c5[color]}${text}${c5.reset}`;
|
|
3294
3093
|
}
|
|
3295
3094
|
function statusBadge(status) {
|
|
3296
3095
|
switch (status) {
|
|
3297
3096
|
case "pending":
|
|
3298
|
-
return
|
|
3097
|
+
return paint5("gray", "\u25CB pending");
|
|
3299
3098
|
case "in_progress":
|
|
3300
|
-
return
|
|
3099
|
+
return paint5("cyan", "\u27F3 in_progress");
|
|
3301
3100
|
case "completed":
|
|
3302
|
-
return
|
|
3101
|
+
return paint5("green", "\u2713 completed");
|
|
3303
3102
|
case "failed":
|
|
3304
|
-
return
|
|
3103
|
+
return paint5("red", "\u2717 failed");
|
|
3305
3104
|
default:
|
|
3306
|
-
return
|
|
3105
|
+
return paint5("gray", status);
|
|
3307
3106
|
}
|
|
3308
3107
|
}
|
|
3309
|
-
var prototypeCommand = new
|
|
3310
|
-
new
|
|
3108
|
+
var prototypeCommand = new Command14("prototype").description("Manage prototypes").addCommand(
|
|
3109
|
+
new Command14("list").description("List prototypes for the linked project").option("--all", "Show prototypes for all projects").action(async (opts) => {
|
|
3311
3110
|
const params = new URLSearchParams();
|
|
3312
3111
|
if (!opts.all) {
|
|
3313
3112
|
const projectId = getLinkedProjectId();
|
|
@@ -3319,21 +3118,21 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
3319
3118
|
}
|
|
3320
3119
|
const prototypes = await api.get(`/api/prototypes?${params.toString()}`);
|
|
3321
3120
|
if (prototypes.length === 0) {
|
|
3322
|
-
console.log(
|
|
3121
|
+
console.log(paint5("gray", "No prototypes found."));
|
|
3323
3122
|
return;
|
|
3324
3123
|
}
|
|
3325
3124
|
console.log();
|
|
3326
3125
|
for (const p of prototypes) {
|
|
3327
3126
|
const date = new Date(p.createdAt).toLocaleDateString();
|
|
3328
3127
|
console.log(
|
|
3329
|
-
` ${
|
|
3128
|
+
` ${paint5("bold", p.title)} ${statusBadge(p.status)} ${paint5("gray", p.id.slice(0, 8))} ${paint5("dim", date)}`
|
|
3330
3129
|
);
|
|
3331
|
-
console.log(` ${
|
|
3130
|
+
console.log(` ${paint5("dim", p.prompt.slice(0, 80) + (p.prompt.length > 80 ? "\u2026" : ""))}`);
|
|
3332
3131
|
console.log();
|
|
3333
3132
|
}
|
|
3334
3133
|
})
|
|
3335
3134
|
).addCommand(
|
|
3336
|
-
new
|
|
3135
|
+
new Command14("create").description("Create a new prototype").argument("<title>", "Title of the prototype").requiredOption("--prompt <prompt>", "Design description / prompt").option("--project <projectId>", "Project ID (defaults to linked project)").option("--variants <count>", "Number of variants to generate (1-50)", "5").action(async (title, opts) => {
|
|
3337
3136
|
const projectId = opts.project ?? getLinkedProjectId();
|
|
3338
3137
|
if (!projectId) {
|
|
3339
3138
|
console.error('No project linked. Run "mr link <project-id>" or use --project.');
|
|
@@ -3347,37 +3146,37 @@ var prototypeCommand = new Command15("prototype").description("Manage prototypes
|
|
|
3347
3146
|
projectId
|
|
3348
3147
|
});
|
|
3349
3148
|
console.log();
|
|
3350
|
-
console.log(` ${
|
|
3351
|
-
console.log(` ${
|
|
3352
|
-
console.log(` ${
|
|
3149
|
+
console.log(` ${paint5("green", "\u2713")} Created prototype: ${paint5("bold", prototype.title)}`);
|
|
3150
|
+
console.log(` ${paint5("gray", "ID:")} ${prototype.id}`);
|
|
3151
|
+
console.log(` ${paint5("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
|
|
3353
3152
|
console.log();
|
|
3354
3153
|
})
|
|
3355
3154
|
).addCommand(
|
|
3356
|
-
new
|
|
3155
|
+
new Command14("start").description("Start prototype generation (sets status to in_progress)").argument("<id>", "Prototype ID").action(async (id) => {
|
|
3357
3156
|
const prototype = await api.patch(`/api/prototypes/${id}`, {
|
|
3358
3157
|
status: "in_progress"
|
|
3359
3158
|
});
|
|
3360
3159
|
console.log();
|
|
3361
|
-
console.log(` ${
|
|
3362
|
-
console.log(` ${
|
|
3160
|
+
console.log(` ${paint5("cyan", "\u27F3")} Started: ${paint5("bold", prototype.title)}`);
|
|
3161
|
+
console.log(` ${paint5("gray", "The watch agent will pick this up shortly.")}`);
|
|
3363
3162
|
console.log();
|
|
3364
3163
|
})
|
|
3365
3164
|
).addCommand(
|
|
3366
|
-
new
|
|
3165
|
+
new Command14("retry").description("Retry a failed prototype").argument("<id>", "Prototype ID").action(async (id) => {
|
|
3367
3166
|
const prototype = await api.patch(`/api/prototypes/${id}`, {
|
|
3368
3167
|
status: "in_progress",
|
|
3369
3168
|
files: null
|
|
3370
3169
|
});
|
|
3371
3170
|
console.log();
|
|
3372
|
-
console.log(` ${
|
|
3171
|
+
console.log(` ${paint5("cyan", "\u27F3")} Retrying: ${paint5("bold", prototype.title)}`);
|
|
3373
3172
|
console.log();
|
|
3374
3173
|
})
|
|
3375
3174
|
);
|
|
3376
3175
|
|
|
3377
3176
|
// cli/commands/setup.ts
|
|
3378
|
-
import { Command as
|
|
3177
|
+
import { Command as Command15 } from "commander";
|
|
3379
3178
|
import { exec as exec2 } from "child_process";
|
|
3380
|
-
var
|
|
3179
|
+
var c6 = {
|
|
3381
3180
|
reset: "\x1B[0m",
|
|
3382
3181
|
bold: "\x1B[1m",
|
|
3383
3182
|
dim: "\x1B[2m",
|
|
@@ -3388,8 +3187,8 @@ var c7 = {
|
|
|
3388
3187
|
magenta: "\x1B[35m",
|
|
3389
3188
|
gray: "\x1B[90m"
|
|
3390
3189
|
};
|
|
3391
|
-
function
|
|
3392
|
-
return `${
|
|
3190
|
+
function paint6(color, text) {
|
|
3191
|
+
return `${c6[color]}${text}${c6.reset}`;
|
|
3393
3192
|
}
|
|
3394
3193
|
function commandExists(cmd) {
|
|
3395
3194
|
return new Promise((resolve7) => {
|
|
@@ -3633,60 +3432,60 @@ async function checkApiConnectivity() {
|
|
|
3633
3432
|
}
|
|
3634
3433
|
}
|
|
3635
3434
|
function printResults(checks) {
|
|
3636
|
-
const maxNameLen = Math.max(...checks.map((
|
|
3435
|
+
const maxNameLen = Math.max(...checks.map((c11) => c11.name.length));
|
|
3637
3436
|
let allOk = true;
|
|
3638
3437
|
for (const check of checks) {
|
|
3639
3438
|
const isOptional = check.optional ?? false;
|
|
3640
|
-
const icon = check.ok ?
|
|
3439
|
+
const icon = check.ok ? paint6("green", "\u2713") : isOptional ? paint6("yellow", "\u25CB") : paint6("red", "\u2717");
|
|
3641
3440
|
const name = check.name.padEnd(maxNameLen);
|
|
3642
|
-
const msg = check.ok ?
|
|
3441
|
+
const msg = check.ok ? paint6("green", check.message) : isOptional ? paint6("yellow", check.message) : paint6("red", check.message);
|
|
3643
3442
|
console.log(` ${icon} ${name} ${msg}`);
|
|
3644
3443
|
if (!check.ok && !isOptional) allOk = false;
|
|
3645
3444
|
}
|
|
3646
3445
|
return allOk;
|
|
3647
3446
|
}
|
|
3648
3447
|
async function autoFix(checks, agent) {
|
|
3649
|
-
const { spawn:
|
|
3650
|
-
const ghInstalled = checks.find((
|
|
3651
|
-
const ghAuthed = checks.find((
|
|
3652
|
-
const mrAuthed = checks.find((
|
|
3653
|
-
const claudeCheck = checks.find((
|
|
3448
|
+
const { spawn: spawn9 } = await import("child_process");
|
|
3449
|
+
const ghInstalled = checks.find((c11) => c11.name === "GitHub CLI (gh)").ok;
|
|
3450
|
+
const ghAuthed = checks.find((c11) => c11.name === "GitHub CLI auth").ok;
|
|
3451
|
+
const mrAuthed = checks.find((c11) => c11.name === "Mr. Manager CLI auth").ok;
|
|
3452
|
+
const claudeCheck = checks.find((c11) => c11.name === "Claude Code (claude)");
|
|
3654
3453
|
if (claudeCheck && !claudeCheck.ok && agent === "claude") {
|
|
3655
|
-
console.log(
|
|
3656
|
-
console.log(
|
|
3454
|
+
console.log(paint6("cyan", " Installing Claude Code..."));
|
|
3455
|
+
console.log(paint6("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
|
|
3657
3456
|
await new Promise((resolve7) => {
|
|
3658
|
-
const child =
|
|
3457
|
+
const child = spawn9("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
|
|
3659
3458
|
child.on("exit", () => resolve7());
|
|
3660
3459
|
});
|
|
3661
3460
|
console.log("");
|
|
3662
3461
|
}
|
|
3663
3462
|
if (ghInstalled && !ghAuthed) {
|
|
3664
|
-
console.log(
|
|
3463
|
+
console.log(paint6("cyan", " Running gh auth login..."));
|
|
3665
3464
|
await new Promise((resolve7) => {
|
|
3666
|
-
const child =
|
|
3465
|
+
const child = spawn9("gh", ["auth", "login"], { stdio: "inherit" });
|
|
3667
3466
|
child.on("exit", () => resolve7());
|
|
3668
3467
|
});
|
|
3669
3468
|
console.log("");
|
|
3670
3469
|
}
|
|
3671
3470
|
if (!mrAuthed) {
|
|
3672
|
-
console.log(
|
|
3471
|
+
console.log(paint6("cyan", " Running mr login..."));
|
|
3673
3472
|
const entry = process.argv[1];
|
|
3674
3473
|
await new Promise((resolve7) => {
|
|
3675
|
-
const child =
|
|
3474
|
+
const child = spawn9(process.execPath, [entry, "login"], { stdio: "inherit" });
|
|
3676
3475
|
child.on("exit", () => resolve7());
|
|
3677
3476
|
});
|
|
3678
3477
|
console.log("");
|
|
3679
3478
|
}
|
|
3680
3479
|
}
|
|
3681
|
-
var setupCommand = new
|
|
3480
|
+
var setupCommand = new Command15("setup").description("Check that all dependencies for mr watch are installed and configured").option("--fix", "Attempt to auto-fix issues where possible", false).option("--agent <agent>", "AI agent to check: claude, codex, or gemini (default: claude)", "claude").action(async (opts) => {
|
|
3682
3481
|
const agent = opts.agent === "codex" ? "codex" : opts.agent === "gemini" ? "gemini" : "claude";
|
|
3683
3482
|
const banner = [
|
|
3684
3483
|
``,
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3484
|
+
paint6("cyan", ` \u2554\u2566\u2557\u2551\u2550\u2557 \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557\u2551 \u2551\u2554\u2550\u2557`),
|
|
3485
|
+
paint6("magenta", ` \u2551\u2551\u2551\u2560\u2566\u2563 \u255A\u2550\u2557\u2551\u2563 \u2551 \u2551 \u2551\u2560\u2550\u2563`),
|
|
3486
|
+
paint6("cyan", ` \u2569 \u2569\u2569\u255A\u2550 \u255A\u2550\u255D\u255A\u2550\u255D \u2569 \u255A\u2550\u255D\u2569 `),
|
|
3487
|
+
paint6("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`),
|
|
3488
|
+
paint6("dim", ` environment check for mr watch (agent: ${agent})`),
|
|
3690
3489
|
``
|
|
3691
3490
|
].join("\n");
|
|
3692
3491
|
console.log(banner);
|
|
@@ -3706,18 +3505,18 @@ var setupCommand = new Command16("setup").description("Check that all dependenci
|
|
|
3706
3505
|
const allOk = printResults(checks);
|
|
3707
3506
|
console.log("");
|
|
3708
3507
|
if (allOk) {
|
|
3709
|
-
console.log(
|
|
3508
|
+
console.log(paint6("green", " All checks passed! You're ready to run mr watch."));
|
|
3710
3509
|
if (agent === "claude") {
|
|
3711
|
-
console.log(
|
|
3510
|
+
console.log(paint6("dim", " Tip: check other agents with --agent codex or --agent gemini"));
|
|
3712
3511
|
}
|
|
3713
3512
|
console.log("");
|
|
3714
3513
|
return;
|
|
3715
3514
|
}
|
|
3716
|
-
const fixes = checks.filter((
|
|
3515
|
+
const fixes = checks.filter((c11) => !c11.ok && c11.fix && !c11.optional);
|
|
3717
3516
|
if (fixes.length > 0) {
|
|
3718
|
-
console.log(
|
|
3517
|
+
console.log(paint6("yellow", " To fix:"));
|
|
3719
3518
|
for (const fix of fixes) {
|
|
3720
|
-
console.log(` ${
|
|
3519
|
+
console.log(` ${paint6("dim", "\u2192")} ${paint6("bold", fix.name)}: ${fix.fix}`);
|
|
3721
3520
|
}
|
|
3722
3521
|
console.log("");
|
|
3723
3522
|
}
|
|
@@ -3728,8 +3527,8 @@ var setupCommand = new Command16("setup").description("Check that all dependenci
|
|
|
3728
3527
|
});
|
|
3729
3528
|
|
|
3730
3529
|
// cli/commands/update.ts
|
|
3731
|
-
import { Command as
|
|
3732
|
-
var updateCommand = new
|
|
3530
|
+
import { Command as Command16 } from "commander";
|
|
3531
|
+
var updateCommand = new Command16("update").description("Post a status update to a task (used by agents to report progress)").argument("<task-id>", "Task ID").argument("<message>", "Status update message").option("--source <source>", "Update source: agent, system, or user", "agent").action(async (taskId, message, opts) => {
|
|
3733
3532
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
3734
3533
|
message,
|
|
3735
3534
|
source: opts.source
|
|
@@ -3738,11 +3537,11 @@ var updateCommand = new Command17("update").description("Post a status update to
|
|
|
3738
3537
|
});
|
|
3739
3538
|
|
|
3740
3539
|
// cli/commands/screenshot.ts
|
|
3741
|
-
import { Command as
|
|
3540
|
+
import { Command as Command17 } from "commander";
|
|
3742
3541
|
import { readFileSync as readFileSync6, existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
3743
3542
|
import { join as join8 } from "path";
|
|
3744
3543
|
import { tmpdir } from "os";
|
|
3745
|
-
var screenshotCommand = new
|
|
3544
|
+
var screenshotCommand = new Command17("screenshot").description(
|
|
3746
3545
|
"Take or attach a screenshot to a task update (agents use this to show their work)"
|
|
3747
3546
|
).argument("<task-id>", "Task ID").argument("[file]", "Path to an image file (if omitted, uses headless browser to screenshot the app)").option("-m, --message <message>", "Optional message to include with the screenshot").option("-u, --url <url>", "Custom URL to screenshot (defaults to the task's project page)").action(async (taskId, file, opts) => {
|
|
3748
3547
|
let filePath = file;
|
|
@@ -3833,10 +3632,10 @@ var screenshotCommand = new Command18("screenshot").description(
|
|
|
3833
3632
|
});
|
|
3834
3633
|
|
|
3835
3634
|
// cli/commands/resume.ts
|
|
3836
|
-
import { Command as
|
|
3837
|
-
import { spawn as
|
|
3635
|
+
import { Command as Command18 } from "commander";
|
|
3636
|
+
import { spawn as spawn6 } from "child_process";
|
|
3838
3637
|
import { resolve as resolve3 } from "path";
|
|
3839
|
-
var
|
|
3638
|
+
var c7 = {
|
|
3840
3639
|
reset: "\x1B[0m",
|
|
3841
3640
|
bold: "\x1B[1m",
|
|
3842
3641
|
dim: "\x1B[2m",
|
|
@@ -3847,15 +3646,15 @@ var c8 = {
|
|
|
3847
3646
|
magenta: "\x1B[35m",
|
|
3848
3647
|
gray: "\x1B[90m"
|
|
3849
3648
|
};
|
|
3850
|
-
function
|
|
3851
|
-
return `${
|
|
3649
|
+
function paint7(color, text) {
|
|
3650
|
+
return `${c7[color]}${text}${c7.reset}`;
|
|
3852
3651
|
}
|
|
3853
|
-
var resumeCommand = new
|
|
3652
|
+
var resumeCommand = new Command18("resume").description("Resume an interactive Claude session for a task (non-headless)").argument("<task-id>", "Task ID whose Claude session to resume").option("--dir <directory>", "Override the working directory for the session").action(async (taskId, opts) => {
|
|
3854
3653
|
const task = await api.get(`/api/tasks/${taskId}`);
|
|
3855
3654
|
if (!task.claudeSessionId) {
|
|
3856
3655
|
console.error(
|
|
3857
3656
|
`
|
|
3858
|
-
${
|
|
3657
|
+
${paint7("red", "\u2717")} No Claude session found for task ${paint7("dim", taskId.slice(0, 8))}`
|
|
3859
3658
|
);
|
|
3860
3659
|
console.error(
|
|
3861
3660
|
` The task may not have been dispatched yet.
|
|
@@ -3863,19 +3662,19 @@ var resumeCommand = new Command19("resume").description("Resume an interactive C
|
|
|
3863
3662
|
);
|
|
3864
3663
|
process.exit(1);
|
|
3865
3664
|
}
|
|
3866
|
-
if (task.completed ||
|
|
3665
|
+
if (task.status === "completed" || task.status === "todo") {
|
|
3867
3666
|
console.error(
|
|
3868
3667
|
`
|
|
3869
|
-
${
|
|
3668
|
+
${paint7("yellow", "\u26A0")} Task ${paint7("dim", taskId.slice(0, 8))} has already completed.`
|
|
3870
3669
|
);
|
|
3871
3670
|
console.error(
|
|
3872
|
-
` Session ID: ${
|
|
3671
|
+
` Session ID: ${paint7("cyan", task.claudeSessionId)}`
|
|
3873
3672
|
);
|
|
3874
3673
|
console.error(
|
|
3875
3674
|
` View the session log in the Mr. Manager web UI, or run:`
|
|
3876
3675
|
);
|
|
3877
3676
|
console.error(
|
|
3878
|
-
` ${
|
|
3677
|
+
` ${paint7("dim", `claude --resume ${task.claudeSessionId}`)}
|
|
3879
3678
|
`
|
|
3880
3679
|
);
|
|
3881
3680
|
process.exit(0);
|
|
@@ -3894,16 +3693,16 @@ var resumeCommand = new Command19("resume").description("Resume an interactive C
|
|
|
3894
3693
|
const sid = taskId.slice(0, 8);
|
|
3895
3694
|
console.log([
|
|
3896
3695
|
``,
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
` ${
|
|
3696
|
+
paint7("magenta", ` \u2554\u2566\u2557\u2566\u2550\u2557 `) + paint7("bold", "resume"),
|
|
3697
|
+
paint7("magenta", ` \u2551\u2551\u2551\u2560\u2566\u255D `) + paint7("dim", "\u2500".repeat(44)),
|
|
3698
|
+
paint7("magenta", ` \u2569 \u2569\u2569\u255A\u2550 `) + task.title,
|
|
3699
|
+
` ${paint7("gray", sid)}`,
|
|
3901
3700
|
``,
|
|
3902
|
-
` ${
|
|
3903
|
-
` ${
|
|
3701
|
+
` ${paint7("dim", "session")} ${paint7("cyan", task.claudeSessionId)}`,
|
|
3702
|
+
` ${paint7("dim", "cwd")} ${paint7("cyan", cwd)}`,
|
|
3904
3703
|
``
|
|
3905
3704
|
].join("\n"));
|
|
3906
|
-
const child =
|
|
3705
|
+
const child = spawn6("claude", ["--resume", task.claudeSessionId], {
|
|
3907
3706
|
cwd,
|
|
3908
3707
|
stdio: "inherit"
|
|
3909
3708
|
});
|
|
@@ -3912,15 +3711,15 @@ var resumeCommand = new Command19("resume").description("Resume an interactive C
|
|
|
3912
3711
|
});
|
|
3913
3712
|
child.on("error", (err) => {
|
|
3914
3713
|
console.error(`
|
|
3915
|
-
${
|
|
3714
|
+
${paint7("red", "\u2717")} Failed to launch Claude: ${err.message}
|
|
3916
3715
|
`);
|
|
3917
3716
|
process.exit(1);
|
|
3918
3717
|
});
|
|
3919
3718
|
});
|
|
3920
3719
|
|
|
3921
3720
|
// cli/commands/browse.ts
|
|
3922
|
-
import { Command as
|
|
3923
|
-
import { execSync as execSync5, spawn as
|
|
3721
|
+
import { Command as Command19 } from "commander";
|
|
3722
|
+
import { execSync as execSync5, spawn as spawn7 } from "child_process";
|
|
3924
3723
|
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
3925
3724
|
import { join as join9 } from "path";
|
|
3926
3725
|
var BROWSE_DIR2 = join9(import.meta.dirname, "..", "..", "browse");
|
|
@@ -3961,7 +3760,7 @@ async function ensureDevServer() {
|
|
|
3961
3760
|
}
|
|
3962
3761
|
const port = await findAvailablePort2(3e3);
|
|
3963
3762
|
console.log(`[browse] Starting dev server on port ${port}...`);
|
|
3964
|
-
const devProc =
|
|
3763
|
+
const devProc = spawn7("npm", ["run", "dev", "--", "--port", String(port)], {
|
|
3965
3764
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3966
3765
|
detached: true,
|
|
3967
3766
|
cwd: join9(import.meta.dirname, "..", ".."),
|
|
@@ -3983,7 +3782,7 @@ async function ensureDevServer() {
|
|
|
3983
3782
|
}
|
|
3984
3783
|
throw new Error("Dev server failed to start within 30s");
|
|
3985
3784
|
}
|
|
3986
|
-
var browseCommand = new
|
|
3785
|
+
var browseCommand = new Command19("browse").description("Control a headless browser for QA and testing").argument("[command]", "Browse command (goto, click, fill, screenshot, etc.)").argument("[args...]", "Command arguments").option(
|
|
3987
3786
|
"--task-id <id>",
|
|
3988
3787
|
"Attach screenshot to a task update (only for screenshot command)"
|
|
3989
3788
|
).option("--dev", "Auto-start local dev server before browsing").allowUnknownOption(true).action(
|
|
@@ -4066,10 +3865,10 @@ var browseCommand = new Command20("browse").description("Control a headless brow
|
|
|
4066
3865
|
);
|
|
4067
3866
|
|
|
4068
3867
|
// cli/commands/set-path.ts
|
|
4069
|
-
import { Command as
|
|
3868
|
+
import { Command as Command20 } from "commander";
|
|
4070
3869
|
import { resolve as resolve4 } from "path";
|
|
4071
3870
|
import { existsSync as existsSync9 } from "fs";
|
|
4072
|
-
var setPathCommand = new
|
|
3871
|
+
var setPathCommand = new Command20("set-path").description("Set or update the local repo path for a project").argument("<project-id>", "Project ID").argument("<path>", "Absolute or relative path to the local repo").action(async (projectId, pathArg) => {
|
|
4073
3872
|
const absolutePath = resolve4(pathArg);
|
|
4074
3873
|
if (!existsSync9(absolutePath)) {
|
|
4075
3874
|
console.error(`Error: Path does not exist: ${absolutePath}`);
|
|
@@ -4087,9 +3886,9 @@ var setPathCommand = new Command21("set-path").description("Set or update the lo
|
|
|
4087
3886
|
});
|
|
4088
3887
|
|
|
4089
3888
|
// cli/commands/test.ts
|
|
4090
|
-
import { Command as
|
|
3889
|
+
import { Command as Command21 } from "commander";
|
|
4091
3890
|
import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
|
|
4092
|
-
var testCommand = new
|
|
3891
|
+
var testCommand = new Command21("test").description("Run automated browser test for a task's MR/PR").argument("<task-id>", "Task ID to test").option("--plan <file>", "Path to a custom test plan JSON file").action(async (taskId, opts) => {
|
|
4093
3892
|
const config = loadConfig();
|
|
4094
3893
|
console.log("[test] Fetching task...");
|
|
4095
3894
|
let task;
|
|
@@ -4209,11 +4008,11 @@ var testCommand = new Command22("test").description("Run automated browser test
|
|
|
4209
4008
|
});
|
|
4210
4009
|
|
|
4211
4010
|
// cli/commands/features.ts
|
|
4212
|
-
import { Command as
|
|
4011
|
+
import { Command as Command22 } from "commander";
|
|
4213
4012
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "fs";
|
|
4214
4013
|
import { resolve as resolve5, sep as sep2 } from "path";
|
|
4215
4014
|
var FEATURES_FILE3 = ".mr-features.md";
|
|
4216
|
-
var
|
|
4015
|
+
var c8 = {
|
|
4217
4016
|
reset: "\x1B[0m",
|
|
4218
4017
|
bold: "\x1B[1m",
|
|
4219
4018
|
dim: "\x1B[2m",
|
|
@@ -4223,8 +4022,8 @@ var c9 = {
|
|
|
4223
4022
|
magenta: "\x1B[35m",
|
|
4224
4023
|
gray: "\x1B[90m"
|
|
4225
4024
|
};
|
|
4226
|
-
function
|
|
4227
|
-
return `${
|
|
4025
|
+
function paint8(color, text) {
|
|
4026
|
+
return `${c8[color]}${text}${c8.reset}`;
|
|
4228
4027
|
}
|
|
4229
4028
|
function resolveProjectRoot2() {
|
|
4230
4029
|
const cwd = process.cwd();
|
|
@@ -4243,7 +4042,7 @@ function readFeatures2() {
|
|
|
4243
4042
|
if (!existsSync11(path)) return null;
|
|
4244
4043
|
return readFileSync9(path, "utf-8");
|
|
4245
4044
|
}
|
|
4246
|
-
var featuresCommand = new
|
|
4045
|
+
var featuresCommand = new Command22("features").description("View or update the project features & goals document (.mr-features.md)").option("--update <content>", "Replace the features document with the given content").option("--file <path>", "Read content from a file and use it to update the features document").option("--path", "Print the path to the features file").action(async (opts) => {
|
|
4247
4046
|
if (opts.path) {
|
|
4248
4047
|
console.log(getFeaturesPath());
|
|
4249
4048
|
return;
|
|
@@ -4252,30 +4051,30 @@ var featuresCommand = new Command23("features").description("View or update the
|
|
|
4252
4051
|
const content2 = readFileSync9(resolve5(opts.file), "utf-8");
|
|
4253
4052
|
const featuresPath = getFeaturesPath();
|
|
4254
4053
|
writeFileSync5(featuresPath, content2);
|
|
4255
|
-
console.log(`${
|
|
4054
|
+
console.log(`${paint8("green", "\u2713")} Updated ${paint8("cyan", featuresPath)} from ${paint8("cyan", opts.file)}`);
|
|
4256
4055
|
return;
|
|
4257
4056
|
}
|
|
4258
4057
|
if (opts.update) {
|
|
4259
4058
|
const featuresPath = getFeaturesPath();
|
|
4260
4059
|
writeFileSync5(featuresPath, opts.update);
|
|
4261
|
-
console.log(`${
|
|
4060
|
+
console.log(`${paint8("green", "\u2713")} Updated ${paint8("cyan", featuresPath)}`);
|
|
4262
4061
|
return;
|
|
4263
4062
|
}
|
|
4264
4063
|
const content = readFeatures2();
|
|
4265
4064
|
if (!content) {
|
|
4266
|
-
console.log(
|
|
4267
|
-
console.log(
|
|
4065
|
+
console.log(paint8("dim", `No features document found. One will be created when an agent completes a task.`));
|
|
4066
|
+
console.log(paint8("dim", `Path: ${getFeaturesPath()}`));
|
|
4268
4067
|
return;
|
|
4269
4068
|
}
|
|
4270
4069
|
console.log(content);
|
|
4271
4070
|
});
|
|
4272
4071
|
|
|
4273
4072
|
// cli/commands/no-mr.ts
|
|
4274
|
-
import { Command as
|
|
4073
|
+
import { Command as Command23 } from "commander";
|
|
4275
4074
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
4276
4075
|
import { resolve as resolve6 } from "path";
|
|
4277
4076
|
var NO_MR_FILE = ".mr-no-mr";
|
|
4278
|
-
var noMrCommand = new
|
|
4077
|
+
var noMrCommand = new Command23("no-mr").description("Signal that a task does not require a merge/pull request and describe what was done instead").argument("<task-id>", "Task ID").argument("<description>", "Description of what was done instead of creating an MR/PR").action(async (taskId, description) => {
|
|
4279
4078
|
const filePath = resolve6(process.cwd(), NO_MR_FILE);
|
|
4280
4079
|
writeFileSync6(filePath, description, "utf-8");
|
|
4281
4080
|
await api.post(`/api/tasks/${taskId}/updates`, {
|
|
@@ -4287,8 +4086,8 @@ var noMrCommand = new Command24("no-mr").description("Signal that a task does no
|
|
|
4287
4086
|
});
|
|
4288
4087
|
|
|
4289
4088
|
// cli/commands/mobile.ts
|
|
4290
|
-
import { Command as
|
|
4291
|
-
function
|
|
4089
|
+
import { Command as Command24 } from "commander";
|
|
4090
|
+
function paint9(color, text) {
|
|
4292
4091
|
const colors = {
|
|
4293
4092
|
cyan: "\x1B[36m",
|
|
4294
4093
|
green: "\x1B[32m",
|
|
@@ -4299,7 +4098,7 @@ function paint10(color, text) {
|
|
|
4299
4098
|
};
|
|
4300
4099
|
return `${colors[color] ?? ""}${text}${colors.reset}`;
|
|
4301
4100
|
}
|
|
4302
|
-
var mobileCommand = new
|
|
4101
|
+
var mobileCommand = new Command24("mobile").description(
|
|
4303
4102
|
"Start the Web-to-Mobile Conversion Wizard for the linked project"
|
|
4304
4103
|
).argument("[project-id]", "Project ID (defaults to linked project)").option("--framework <framework>", "Framework: react-native or native").option("--url <url>", "Web app URL for analysis").action(
|
|
4305
4104
|
async (projectIdArg, opts) => {
|
|
@@ -4311,12 +4110,12 @@ var mobileCommand = new Command25("mobile").description(
|
|
|
4311
4110
|
process.exit(1);
|
|
4312
4111
|
}
|
|
4313
4112
|
console.log(
|
|
4314
|
-
|
|
4113
|
+
paint9("cyan", "mobile") + paint9("dim", " \u2014 starting conversion wizard")
|
|
4315
4114
|
);
|
|
4316
4115
|
const task = await api.post("/api/tasks", {
|
|
4317
4116
|
title: "Convert to Mobile App",
|
|
4318
4117
|
projectId,
|
|
4319
|
-
|
|
4118
|
+
status: "in_progress"
|
|
4320
4119
|
});
|
|
4321
4120
|
console.log(` Created task: ${task.title} (${task.id})`);
|
|
4322
4121
|
console.log(" Analyzing web app...");
|
|
@@ -4342,13 +4141,13 @@ var mobileCommand = new Command25("mobile").description(
|
|
|
4342
4141
|
}
|
|
4343
4142
|
console.log(
|
|
4344
4143
|
`
|
|
4345
|
-
${
|
|
4144
|
+
${paint9("green", "\u2713")} Wizard initialized. Open the web UI to continue:
|
|
4346
4145
|
\u2192 Task ID: ${task.id}
|
|
4347
4146
|
\u2192 Use the "Convert to Mobile" button on the project page`
|
|
4348
4147
|
);
|
|
4349
4148
|
}
|
|
4350
4149
|
);
|
|
4351
|
-
var statusSubcommand = new
|
|
4150
|
+
var statusSubcommand = new Command24("status").description("Show mobile conversion status for a task").argument("<task-id>", "Parent conversion task ID").action(async (taskId) => {
|
|
4352
4151
|
const resources = await api.get(
|
|
4353
4152
|
`/api/tasks/${taskId}/resources`
|
|
4354
4153
|
);
|
|
@@ -4361,7 +4160,7 @@ var statusSubcommand = new Command25("status").description("Show mobile conversi
|
|
|
4361
4160
|
}
|
|
4362
4161
|
try {
|
|
4363
4162
|
const state = JSON.parse(wizardState.content);
|
|
4364
|
-
console.log(
|
|
4163
|
+
console.log(paint9("cyan", "Mobile Conversion Status"));
|
|
4365
4164
|
console.log(` Phase: ${state.phase}`);
|
|
4366
4165
|
if (state.framework) {
|
|
4367
4166
|
console.log(` Framework: ${state.framework}`);
|
|
@@ -4384,10 +4183,10 @@ var statusSubcommand = new Command25("status").description("Show mobile conversi
|
|
|
4384
4183
|
mobileCommand.addCommand(statusSubcommand);
|
|
4385
4184
|
|
|
4386
4185
|
// cli/commands/scan.ts
|
|
4387
|
-
import { Command as
|
|
4186
|
+
import { Command as Command25 } from "commander";
|
|
4388
4187
|
|
|
4389
4188
|
// lib/scanner/index.ts
|
|
4390
|
-
import { spawn as
|
|
4189
|
+
import { spawn as spawn8 } from "child_process";
|
|
4391
4190
|
|
|
4392
4191
|
// lib/scanner/config.ts
|
|
4393
4192
|
import { readFileSync as readFileSync10, existsSync as existsSync12 } from "fs";
|
|
@@ -4853,10 +4652,10 @@ ${codebaseAnalysis.routes.map((r) => `- ${r}`).join("\n")}
|
|
|
4853
4652
|
${codebaseAnalysis.prismaModels.map((m) => `- ${m}`).join("\n")}
|
|
4854
4653
|
|
|
4855
4654
|
**Components:**
|
|
4856
|
-
${codebaseAnalysis.components.slice(0, 30).map((
|
|
4655
|
+
${codebaseAnalysis.components.slice(0, 30).map((c11) => `- ${c11}`).join("\n")}
|
|
4857
4656
|
|
|
4858
4657
|
**Recent Git Commits:**
|
|
4859
|
-
${codebaseAnalysis.recentCommits.slice(0, 15).map((
|
|
4658
|
+
${codebaseAnalysis.recentCommits.slice(0, 15).map((c11) => `- ${c11}`).join("\n")}
|
|
4860
4659
|
|
|
4861
4660
|
**Completed Tasks:**
|
|
4862
4661
|
${context.completedTasks.slice(0, 20).map((t) => `- ${t.title}`).join("\n") || "None"}
|
|
@@ -5066,7 +4865,7 @@ async function fetchScanContext(opts) {
|
|
|
5066
4865
|
}
|
|
5067
4866
|
function runClaude(prompt2) {
|
|
5068
4867
|
return new Promise((resolve7, reject) => {
|
|
5069
|
-
const child =
|
|
4868
|
+
const child = spawn8("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
|
|
5070
4869
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5071
4870
|
});
|
|
5072
4871
|
let output = "";
|
|
@@ -5122,7 +4921,7 @@ function parseSynthesisOutput(output) {
|
|
|
5122
4921
|
}
|
|
5123
4922
|
|
|
5124
4923
|
// cli/commands/scan.ts
|
|
5125
|
-
var
|
|
4924
|
+
var c9 = {
|
|
5126
4925
|
reset: "\x1B[0m",
|
|
5127
4926
|
bold: "\x1B[1m",
|
|
5128
4927
|
dim: "\x1B[2m",
|
|
@@ -5133,53 +4932,53 @@ var c10 = {
|
|
|
5133
4932
|
magenta: "\x1B[35m",
|
|
5134
4933
|
gray: "\x1B[90m"
|
|
5135
4934
|
};
|
|
5136
|
-
function
|
|
5137
|
-
return `${
|
|
4935
|
+
function paint10(color, text) {
|
|
4936
|
+
return `${c9[color]}${text}${c9.reset}`;
|
|
5138
4937
|
}
|
|
5139
|
-
function
|
|
5140
|
-
return
|
|
4938
|
+
function timestamp3() {
|
|
4939
|
+
return paint10("gray", (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false }));
|
|
5141
4940
|
}
|
|
5142
4941
|
function scanTag() {
|
|
5143
|
-
return
|
|
4942
|
+
return paint10("magenta", "[scan]");
|
|
5144
4943
|
}
|
|
5145
|
-
function
|
|
5146
|
-
console.log(`${
|
|
4944
|
+
function log2(msg) {
|
|
4945
|
+
console.log(`${timestamp3()} ${scanTag()} ${msg}`);
|
|
5147
4946
|
}
|
|
5148
|
-
function
|
|
5149
|
-
console.log(`${
|
|
4947
|
+
function logOk(msg) {
|
|
4948
|
+
console.log(`${timestamp3()} ${scanTag()} ${paint10("green", "\u2713")} ${msg}`);
|
|
5150
4949
|
}
|
|
5151
|
-
function
|
|
5152
|
-
console.error(`${
|
|
4950
|
+
function logErr2(msg) {
|
|
4951
|
+
console.error(`${timestamp3()} ${scanTag()} ${paint10("red", "\u2717")} ${msg}`);
|
|
5153
4952
|
}
|
|
5154
|
-
var scanCommand = new
|
|
4953
|
+
var scanCommand = new Command25("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) => {
|
|
5155
4954
|
const config = loadConfig();
|
|
5156
4955
|
if (!config.apiKey) {
|
|
5157
|
-
|
|
4956
|
+
logErr2('Not authenticated. Run "mr login" first.');
|
|
5158
4957
|
process.exit(1);
|
|
5159
4958
|
}
|
|
5160
4959
|
const banner = [
|
|
5161
4960
|
``,
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
4961
|
+
paint10("magenta", ` \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554`),
|
|
4962
|
+
paint10("magenta", ` \u255A\u2550\u2557\u2551 \u2560\u2550\u2563\u2551\u2551\u2551`),
|
|
4963
|
+
paint10("magenta", ` \u255A\u2550\u255D\u255A\u2550\u255D\u2569 \u2569\u255D\u255A\u255D`),
|
|
4964
|
+
paint10("dim", ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`),
|
|
4965
|
+
paint10("dim", ` autonomous product scanner`),
|
|
5167
4966
|
``
|
|
5168
4967
|
].join("\n");
|
|
5169
4968
|
console.log(banner);
|
|
5170
4969
|
const projectId = opts.project || getLinkedProjectId();
|
|
5171
4970
|
if (!projectId) {
|
|
5172
|
-
|
|
4971
|
+
logErr2('No project linked. Run "mr link" or pass --project <id>.');
|
|
5173
4972
|
process.exit(1);
|
|
5174
4973
|
}
|
|
5175
4974
|
let project;
|
|
5176
4975
|
try {
|
|
5177
4976
|
project = await api.get(`/api/projects/${projectId}`);
|
|
5178
4977
|
} catch {
|
|
5179
|
-
|
|
4978
|
+
logErr2(`Failed to fetch project ${projectId}`);
|
|
5180
4979
|
process.exit(1);
|
|
5181
4980
|
}
|
|
5182
|
-
|
|
4981
|
+
log2(`Scanning project: ${paint10("cyan", project.name)}`);
|
|
5183
4982
|
let projectPath = project.localPath;
|
|
5184
4983
|
if (!projectPath) {
|
|
5185
4984
|
for (const [dir, pid] of Object.entries(config.directories)) {
|
|
@@ -5195,12 +4994,12 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5195
4994
|
let reportId;
|
|
5196
4995
|
if (opts.report) {
|
|
5197
4996
|
reportId = opts.report;
|
|
5198
|
-
|
|
4997
|
+
log2(`Using existing scan report ${paint10("yellow", reportId.slice(0, 8))}`);
|
|
5199
4998
|
} else {
|
|
5200
4999
|
try {
|
|
5201
5000
|
const scans = await api.get(`/api/scans?projectId=${projectId}&status=processing`);
|
|
5202
5001
|
if (scans.length > 0) {
|
|
5203
|
-
|
|
5002
|
+
logErr2("A scan is already in progress for this project. Wait for it to complete.");
|
|
5204
5003
|
process.exit(1);
|
|
5205
5004
|
}
|
|
5206
5005
|
} catch {
|
|
@@ -5211,9 +5010,9 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5211
5010
|
status: "pending"
|
|
5212
5011
|
});
|
|
5213
5012
|
reportId = report.id;
|
|
5214
|
-
|
|
5013
|
+
log2(`Created scan report ${paint10("yellow", reportId.slice(0, 8))}`);
|
|
5215
5014
|
} catch (err) {
|
|
5216
|
-
|
|
5015
|
+
logErr2(`Failed to create scan report: ${err.message}`);
|
|
5217
5016
|
process.exit(1);
|
|
5218
5017
|
}
|
|
5219
5018
|
}
|
|
@@ -5224,7 +5023,7 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5224
5023
|
try {
|
|
5225
5024
|
const current = await api.get(`/api/scans/${reportId}`);
|
|
5226
5025
|
if (current.status === "cancelled") {
|
|
5227
|
-
|
|
5026
|
+
log2(paint10("yellow", "Scan was cancelled \u2014 aborting."));
|
|
5228
5027
|
process.exit(0);
|
|
5229
5028
|
}
|
|
5230
5029
|
} catch {
|
|
@@ -5238,9 +5037,9 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5238
5037
|
apiUrl: config.apiUrl,
|
|
5239
5038
|
apiKey: config.apiKey,
|
|
5240
5039
|
runBrowse: runBrowseCommand2,
|
|
5241
|
-
onLog:
|
|
5040
|
+
onLog: log2,
|
|
5242
5041
|
onProgress: (phase, detail) => {
|
|
5243
|
-
|
|
5042
|
+
log2(`${paint10("dim", `[${phase}]`)} ${detail}`);
|
|
5244
5043
|
}
|
|
5245
5044
|
});
|
|
5246
5045
|
let wasCancelled = false;
|
|
@@ -5252,7 +5051,7 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5252
5051
|
} catch {
|
|
5253
5052
|
}
|
|
5254
5053
|
if (wasCancelled) {
|
|
5255
|
-
|
|
5054
|
+
log2(paint10("yellow", "Scan was cancelled by user \u2014 discarding results."));
|
|
5256
5055
|
process.exit(0);
|
|
5257
5056
|
}
|
|
5258
5057
|
await api.patch(`/api/scans/${reportId}`, {
|
|
@@ -5263,37 +5062,37 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5263
5062
|
scanDurationMs: result.scanDurationMs,
|
|
5264
5063
|
routesCrawled: result.routesCrawled
|
|
5265
5064
|
});
|
|
5266
|
-
|
|
5065
|
+
logOk(`Scan complete \u2014 ${paint10("cyan", String(result.findings.length))} findings`);
|
|
5267
5066
|
console.log("");
|
|
5268
|
-
console.log(` ${
|
|
5067
|
+
console.log(` ${paint10("bold", "Summary:")} ${result.summary}`);
|
|
5269
5068
|
console.log("");
|
|
5270
5069
|
const high = result.findings.filter((f) => f.priority === "high");
|
|
5271
5070
|
const medium = result.findings.filter((f) => f.priority === "medium");
|
|
5272
5071
|
const low = result.findings.filter((f) => f.priority === "low");
|
|
5273
5072
|
if (high.length > 0) {
|
|
5274
|
-
console.log(` ${
|
|
5073
|
+
console.log(` ${paint10("bold", paint10("red", `High Priority (${high.length})`))}`);
|
|
5275
5074
|
for (const f of high) {
|
|
5276
|
-
console.log(` ${
|
|
5277
|
-
console.log(` ${
|
|
5075
|
+
console.log(` ${paint10("red", "\u25CF")} [${f.type}] ${f.title}`);
|
|
5076
|
+
console.log(` ${paint10("dim", f.description.slice(0, 120))}`);
|
|
5278
5077
|
}
|
|
5279
5078
|
console.log("");
|
|
5280
5079
|
}
|
|
5281
5080
|
if (medium.length > 0) {
|
|
5282
|
-
console.log(` ${
|
|
5081
|
+
console.log(` ${paint10("bold", paint10("yellow", `Medium Priority (${medium.length})`))}`);
|
|
5283
5082
|
for (const f of medium) {
|
|
5284
|
-
console.log(` ${
|
|
5083
|
+
console.log(` ${paint10("yellow", "\u25CF")} [${f.type}] ${f.title}`);
|
|
5285
5084
|
}
|
|
5286
5085
|
console.log("");
|
|
5287
5086
|
}
|
|
5288
5087
|
if (low.length > 0) {
|
|
5289
|
-
console.log(` ${
|
|
5088
|
+
console.log(` ${paint10("dim", `Low Priority (${low.length})`)} `);
|
|
5290
5089
|
for (const f of low) {
|
|
5291
|
-
console.log(` ${
|
|
5090
|
+
console.log(` ${paint10("dim", `\u25CB [${f.type}] ${f.title}`)}`);
|
|
5292
5091
|
}
|
|
5293
5092
|
console.log("");
|
|
5294
5093
|
}
|
|
5295
5094
|
} catch (err) {
|
|
5296
|
-
|
|
5095
|
+
logErr2(`Scan failed: ${err.message}`);
|
|
5297
5096
|
try {
|
|
5298
5097
|
await api.patch(`/api/scans/${reportId}`, {
|
|
5299
5098
|
status: "failed",
|
|
@@ -5306,8 +5105,8 @@ var scanCommand = new Command26("scan").description("Run a product scan on the c
|
|
|
5306
5105
|
});
|
|
5307
5106
|
|
|
5308
5107
|
// cli/commands/idea.ts
|
|
5309
|
-
import { Command as
|
|
5310
|
-
var
|
|
5108
|
+
import { Command as Command26 } from "commander";
|
|
5109
|
+
var c10 = {
|
|
5311
5110
|
reset: "\x1B[0m",
|
|
5312
5111
|
bold: "\x1B[1m",
|
|
5313
5112
|
dim: "\x1B[2m",
|
|
@@ -5319,27 +5118,27 @@ var c11 = {
|
|
|
5319
5118
|
gray: "\x1B[90m",
|
|
5320
5119
|
magenta: "\x1B[35m"
|
|
5321
5120
|
};
|
|
5322
|
-
function
|
|
5323
|
-
return `${
|
|
5121
|
+
function paint11(color, text) {
|
|
5122
|
+
return `${c10[color]}${text}${c10.reset}`;
|
|
5324
5123
|
}
|
|
5325
5124
|
function statusBadge2(status) {
|
|
5326
5125
|
switch (status) {
|
|
5327
5126
|
case "draft":
|
|
5328
|
-
return
|
|
5127
|
+
return paint11("gray", "\u25CB draft");
|
|
5329
5128
|
case "generating":
|
|
5330
|
-
return
|
|
5129
|
+
return paint11("cyan", "\u27F3 generating");
|
|
5331
5130
|
case "generated":
|
|
5332
|
-
return
|
|
5131
|
+
return paint11("green", "\u2713 generated");
|
|
5333
5132
|
case "promoted":
|
|
5334
|
-
return
|
|
5133
|
+
return paint11("magenta", "\u2191 promoted");
|
|
5335
5134
|
case "archived":
|
|
5336
|
-
return
|
|
5135
|
+
return paint11("dim", "\u2298 archived");
|
|
5337
5136
|
default:
|
|
5338
|
-
return
|
|
5137
|
+
return paint11("gray", status);
|
|
5339
5138
|
}
|
|
5340
5139
|
}
|
|
5341
|
-
var ideaCommand = new
|
|
5342
|
-
new
|
|
5140
|
+
var ideaCommand = new Command26("idea").description("Manage ideas \u2014 brainstorm, generate prototypes & plans").addCommand(
|
|
5141
|
+
new Command26("list").description("List ideas for the linked project").option("--all", "Show ideas for all projects").option("--status <status>", "Filter by status").action(async (opts) => {
|
|
5343
5142
|
const params = new URLSearchParams();
|
|
5344
5143
|
if (!opts.all) {
|
|
5345
5144
|
const projectId = getLinkedProjectId();
|
|
@@ -5350,23 +5149,23 @@ var ideaCommand = new Command27("idea").description("Manage ideas \u2014 brainst
|
|
|
5350
5149
|
if (opts.status) params.set("status", opts.status);
|
|
5351
5150
|
const ideas = await api.get(`/api/ideas?${params.toString()}`);
|
|
5352
5151
|
if (ideas.length === 0) {
|
|
5353
|
-
console.log(
|
|
5152
|
+
console.log(paint11("gray", "No ideas found."));
|
|
5354
5153
|
return;
|
|
5355
5154
|
}
|
|
5356
5155
|
console.log();
|
|
5357
5156
|
for (const idea of ideas) {
|
|
5358
5157
|
const date = new Date(idea.createdAt).toLocaleDateString();
|
|
5359
5158
|
console.log(
|
|
5360
|
-
` ${
|
|
5159
|
+
` ${paint11("bold", idea.title)} ${statusBadge2(idea.status)} ${paint11("gray", idea.id.slice(0, 8))} ${paint11("dim", date)}`
|
|
5361
5160
|
);
|
|
5362
5161
|
if (idea.description) {
|
|
5363
|
-
console.log(` ${
|
|
5162
|
+
console.log(` ${paint11("dim", idea.description.slice(0, 80) + (idea.description.length > 80 ? "\u2026" : ""))}`);
|
|
5364
5163
|
}
|
|
5365
5164
|
console.log();
|
|
5366
5165
|
}
|
|
5367
5166
|
})
|
|
5368
5167
|
).addCommand(
|
|
5369
|
-
new
|
|
5168
|
+
new Command26("create").description("Create a new idea").argument("<title>", "Title of the idea").option("--description <desc>", "Description of the idea").option("--project <projectId>", "Project ID (defaults to linked project)").option("--generate", "Immediately start generating plan & prototype").action(async (title, opts) => {
|
|
5370
5169
|
const projectId = opts.project ?? getLinkedProjectId() ?? null;
|
|
5371
5170
|
const idea = await api.post("/api/ideas", {
|
|
5372
5171
|
title,
|
|
@@ -5374,59 +5173,59 @@ var ideaCommand = new Command27("idea").description("Manage ideas \u2014 brainst
|
|
|
5374
5173
|
projectId
|
|
5375
5174
|
});
|
|
5376
5175
|
console.log();
|
|
5377
|
-
console.log(` ${
|
|
5378
|
-
console.log(` ${
|
|
5176
|
+
console.log(` ${paint11("green", "\u2713")} Created idea: ${paint11("bold", idea.title)}`);
|
|
5177
|
+
console.log(` ${paint11("gray", "ID:")} ${idea.id}`);
|
|
5379
5178
|
if (opts.generate) {
|
|
5380
5179
|
await api.post(`/api/ideas/${idea.id}/generate`);
|
|
5381
|
-
console.log(` ${
|
|
5180
|
+
console.log(` ${paint11("cyan", "\u27F3")} Generation will begin automatically via the watch agent.`);
|
|
5382
5181
|
}
|
|
5383
5182
|
console.log();
|
|
5384
5183
|
})
|
|
5385
5184
|
).addCommand(
|
|
5386
|
-
new
|
|
5185
|
+
new Command26("generate").description("Start generating plan & prototype for an idea").argument("<id>", "Idea ID").action(async (id) => {
|
|
5387
5186
|
const idea = await api.post(`/api/ideas/${id}/generate`);
|
|
5388
5187
|
console.log();
|
|
5389
|
-
console.log(` ${
|
|
5390
|
-
console.log(` ${
|
|
5188
|
+
console.log(` ${paint11("cyan", "\u27F3")} Generating: ${paint11("bold", idea.title)}`);
|
|
5189
|
+
console.log(` ${paint11("gray", "The watch agent will pick this up shortly.")}`);
|
|
5391
5190
|
console.log();
|
|
5392
5191
|
})
|
|
5393
5192
|
).addCommand(
|
|
5394
|
-
new
|
|
5193
|
+
new Command26("feedback").description("Send feedback to iterate on an idea's generated content").argument("<id>", "Idea ID").argument("<feedback>", "Feedback text").action(async (id, feedback) => {
|
|
5395
5194
|
const idea = await api.post(`/api/ideas/${id}/feedback`, { feedback });
|
|
5396
5195
|
console.log();
|
|
5397
|
-
console.log(` ${
|
|
5398
|
-
console.log(` ${
|
|
5196
|
+
console.log(` ${paint11("cyan", "\u27F3")} Feedback sent for: ${paint11("bold", idea.title)}`);
|
|
5197
|
+
console.log(` ${paint11("gray", "The watch agent will re-generate with your feedback.")}`);
|
|
5399
5198
|
console.log();
|
|
5400
5199
|
})
|
|
5401
5200
|
).addCommand(
|
|
5402
|
-
new
|
|
5201
|
+
new Command26("promote").description("Promote an idea to a task").argument("<id>", "Idea ID").action(async (id) => {
|
|
5403
5202
|
const result = await api.post(`/api/ideas/${id}/promote`);
|
|
5404
5203
|
console.log();
|
|
5405
|
-
console.log(` ${
|
|
5406
|
-
console.log(` ${
|
|
5204
|
+
console.log(` ${paint11("green", "\u2713")} Promoted idea to task: ${paint11("bold", result.task.title)}`);
|
|
5205
|
+
console.log(` ${paint11("gray", "Task ID:")} ${result.task.id}`);
|
|
5407
5206
|
console.log();
|
|
5408
5207
|
})
|
|
5409
5208
|
).addCommand(
|
|
5410
|
-
new
|
|
5209
|
+
new Command26("spin-up").description("Spin up a new project with a GitHub repo from an idea").argument("<id>", "Idea ID").option("--name <name>", "Custom project name (defaults to idea title)").action(async (id, opts) => {
|
|
5411
5210
|
const body = {};
|
|
5412
5211
|
if (opts.name) body.name = opts.name;
|
|
5413
5212
|
const result = await api.post(`/api/ideas/${id}/spin-up`, body);
|
|
5414
5213
|
console.log();
|
|
5415
|
-
console.log(` ${
|
|
5416
|
-
console.log(` ${
|
|
5214
|
+
console.log(` ${paint11("green", "\u2713")} Spinning up project: ${paint11("bold", result.project.name)}`);
|
|
5215
|
+
console.log(` ${paint11("gray", "Project ID:")} ${result.project.id}`);
|
|
5417
5216
|
if (result.tasks && result.tasks.length > 0) {
|
|
5418
|
-
console.log(` ${
|
|
5217
|
+
console.log(` ${paint11("green", "\u2713")} Created ${result.tasks.length} follow-up task(s):`);
|
|
5419
5218
|
for (const task of result.tasks) {
|
|
5420
|
-
console.log(` ${
|
|
5219
|
+
console.log(` ${paint11("gray", "\u2022")} ${task.title}`);
|
|
5421
5220
|
}
|
|
5422
5221
|
}
|
|
5423
|
-
console.log(` ${
|
|
5222
|
+
console.log(` ${paint11("cyan", "\u27F3")} Repo creation is queued \u2014 the watch daemon will pick it up.`);
|
|
5424
5223
|
console.log();
|
|
5425
5224
|
})
|
|
5426
5225
|
);
|
|
5427
5226
|
|
|
5428
5227
|
// cli/commands/doctor.ts
|
|
5429
|
-
import { Command as
|
|
5228
|
+
import { Command as Command27 } from "commander";
|
|
5430
5229
|
import { existsSync as existsSync14 } from "fs";
|
|
5431
5230
|
import { homedir as homedir3 } from "os";
|
|
5432
5231
|
import { join as join12 } from "path";
|
|
@@ -5474,12 +5273,12 @@ async function checkProjectLink() {
|
|
|
5474
5273
|
optional: true
|
|
5475
5274
|
};
|
|
5476
5275
|
}
|
|
5477
|
-
var doctorCommand = new
|
|
5276
|
+
var doctorCommand = new Command27("doctor").description("Diagnose Mr. Manager CLI installation and environment").action(async () => {
|
|
5478
5277
|
const banner = [
|
|
5479
5278
|
``,
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5279
|
+
paint6("cyan", ` MR DOCTOR`),
|
|
5280
|
+
paint6("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`),
|
|
5281
|
+
paint6("dim", ` diagnosing your mr environment`),
|
|
5483
5282
|
``
|
|
5484
5283
|
].join("\n");
|
|
5485
5284
|
console.log(banner);
|
|
@@ -5501,15 +5300,15 @@ var doctorCommand = new Command28("doctor").description("Diagnose Mr. Manager CL
|
|
|
5501
5300
|
const allOk = printResults(checks);
|
|
5502
5301
|
console.log("");
|
|
5503
5302
|
if (allOk) {
|
|
5504
|
-
console.log(
|
|
5303
|
+
console.log(paint6("green", " Everything looks good!"));
|
|
5505
5304
|
console.log("");
|
|
5506
5305
|
return;
|
|
5507
5306
|
}
|
|
5508
|
-
const fixes = checks.filter((
|
|
5307
|
+
const fixes = checks.filter((c11) => !c11.ok && c11.fix && !c11.optional);
|
|
5509
5308
|
if (fixes.length > 0) {
|
|
5510
|
-
console.log(
|
|
5309
|
+
console.log(paint6("yellow", " To fix:"));
|
|
5511
5310
|
for (const fix of fixes) {
|
|
5512
|
-
console.log(` ${
|
|
5311
|
+
console.log(` ${paint6("dim", "\u2192")} ${paint6("bold", fix.name)}: ${fix.fix}`);
|
|
5513
5312
|
}
|
|
5514
5313
|
console.log("");
|
|
5515
5314
|
}
|
|
@@ -5523,7 +5322,7 @@ var userArgs = process.argv.slice(2);
|
|
|
5523
5322
|
var bypassCommands = /* @__PURE__ */ new Set(["login", "init", "auth", "help", "--help", "-h", "--version", "-V", "doctor", "setup"]);
|
|
5524
5323
|
var shouldBypass = userArgs.length > 0 && bypassCommands.has(userArgs[0]);
|
|
5525
5324
|
if (isFirstRun && !shouldBypass) {
|
|
5526
|
-
const
|
|
5325
|
+
const c11 = {
|
|
5527
5326
|
reset: "\x1B[0m",
|
|
5528
5327
|
bold: "\x1B[1m",
|
|
5529
5328
|
dim: "\x1B[2m",
|
|
@@ -5533,28 +5332,28 @@ if (isFirstRun && !shouldBypass) {
|
|
|
5533
5332
|
magenta: "\x1B[35m"
|
|
5534
5333
|
};
|
|
5535
5334
|
console.log("");
|
|
5536
|
-
console.log(`${
|
|
5537
|
-
console.log(`${
|
|
5538
|
-
console.log(`${
|
|
5539
|
-
console.log(`${
|
|
5335
|
+
console.log(`${c11.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${c11.reset}`);
|
|
5336
|
+
console.log(`${c11.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${c11.reset}`);
|
|
5337
|
+
console.log(`${c11.cyan} \u2569 \u2569\u2569\u255A\u2550 \u2569 \u2569\u2569 \u2569\u255D\u255A\u255D\u2569 \u2569\u255A\u2550\u255D\u255A\u2550\u255D\u2569\u255A\u2550${c11.reset}`);
|
|
5338
|
+
console.log(`${c11.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${c11.reset}`);
|
|
5540
5339
|
console.log("");
|
|
5541
|
-
console.log(`${
|
|
5542
|
-
console.log(`${
|
|
5340
|
+
console.log(`${c11.bold} Welcome to Mr. Manager!${c11.reset}`);
|
|
5341
|
+
console.log(`${c11.dim} Let's get you set up in a few quick steps.${c11.reset}`);
|
|
5543
5342
|
console.log("");
|
|
5544
|
-
console.log(` ${
|
|
5545
|
-
console.log(` ${
|
|
5343
|
+
console.log(` ${c11.yellow}Step 1:${c11.reset} Authenticate via Google OAuth`);
|
|
5344
|
+
console.log(` ${c11.dim}Run:${c11.reset} ${c11.cyan}mr login${c11.reset}`);
|
|
5546
5345
|
console.log("");
|
|
5547
|
-
console.log(` ${
|
|
5548
|
-
console.log(` ${
|
|
5346
|
+
console.log(` ${c11.yellow}Step 2:${c11.reset} Verify your environment`);
|
|
5347
|
+
console.log(` ${c11.dim}Run:${c11.reset} ${c11.cyan}mr setup${c11.reset}`);
|
|
5549
5348
|
console.log("");
|
|
5550
|
-
console.log(` ${
|
|
5551
|
-
console.log(` ${
|
|
5349
|
+
console.log(` ${c11.yellow}Step 3:${c11.reset} Link a repo and start watching`);
|
|
5350
|
+
console.log(` ${c11.dim}Run:${c11.reset} ${c11.cyan}mr link${c11.reset} ${c11.dim}&&${c11.reset} ${c11.cyan}mr watch${c11.reset}`);
|
|
5552
5351
|
console.log("");
|
|
5553
|
-
console.log(`${
|
|
5352
|
+
console.log(`${c11.dim} Or run ${c11.reset}${c11.cyan}mr login${c11.reset}${c11.dim} to get started now.${c11.reset}`);
|
|
5554
5353
|
console.log("");
|
|
5555
5354
|
process.exit(0);
|
|
5556
5355
|
}
|
|
5557
|
-
var program = new
|
|
5356
|
+
var program = new Command28();
|
|
5558
5357
|
program.name("mr").description("Mr. Manager - Task and project management CLI").version("0.2.0");
|
|
5559
5358
|
program.addCommand(initCommand);
|
|
5560
5359
|
program.addCommand(authCommand);
|
|
@@ -5571,7 +5370,6 @@ program.addCommand(undelegateCommand);
|
|
|
5571
5370
|
program.addCommand(createCommand);
|
|
5572
5371
|
program.addCommand(completeCommand);
|
|
5573
5372
|
program.addCommand(subtaskCompleteCommand);
|
|
5574
|
-
program.addCommand(digestCommand);
|
|
5575
5373
|
program.addCommand(upCommand);
|
|
5576
5374
|
program.addCommand(prototypeCommand);
|
|
5577
5375
|
program.addCommand(setupCommand);
|