@dunnewold-labs/mr-manager 0.1.0 → 0.3.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 +100 -54
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -89,23 +89,62 @@ function openBrowser(url) {
|
|
|
89
89
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
90
90
|
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
function isHeadless() {
|
|
93
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
async function loginWithDeviceCode(apiUrl) {
|
|
102
|
+
const res = await fetch(`${apiUrl}/api/auth/device-code`, { method: "POST" });
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
throw new Error(`Failed to create device code: ${res.status} ${res.statusText}`);
|
|
105
|
+
}
|
|
106
|
+
const { code } = await res.json();
|
|
107
|
+
const loginUrl = `${apiUrl}/login?deviceCode=${code}`;
|
|
108
|
+
console.log(`
|
|
109
|
+
Remote login \u2014 visit this URL in any browser:
|
|
110
|
+
`);
|
|
111
|
+
console.log(` ${loginUrl}
|
|
112
|
+
`);
|
|
113
|
+
console.log(`Waiting for authentication\u2026`);
|
|
114
|
+
const timeout = 10 * 60 * 1e3;
|
|
115
|
+
const interval = 3e3;
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
while (Date.now() - start < timeout) {
|
|
118
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
119
|
+
const pollRes = await fetch(`${apiUrl}/api/auth/device-code/poll?code=${code}`);
|
|
120
|
+
if (pollRes.status === 410) {
|
|
121
|
+
throw new Error("Device code expired. Please try again.");
|
|
122
|
+
}
|
|
123
|
+
if (!pollRes.ok) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const data = await pollRes.json();
|
|
127
|
+
if (data.status === "complete" && data.key) {
|
|
128
|
+
return data.key;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error("Login timed out after 10 minutes");
|
|
132
|
+
}
|
|
133
|
+
async function loginWithLocalServer(apiUrl) {
|
|
95
134
|
const port = getRandomPort();
|
|
96
|
-
|
|
135
|
+
return new Promise((resolve7, reject) => {
|
|
97
136
|
const server = createServer((req, res) => {
|
|
98
137
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
99
|
-
const
|
|
138
|
+
const key = url.searchParams.get("key");
|
|
100
139
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
101
140
|
res.end(`
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
141
|
+
<html><body style="font-family:sans-serif;text-align:center;padding:60px">
|
|
142
|
+
<h2>${key ? "\u2713 Authenticated!" : "Something went wrong."}</h2>
|
|
143
|
+
<p>${key ? "You can close this tab and return to the terminal." : "No key received. Please try again."}</p>
|
|
144
|
+
</body></html>
|
|
145
|
+
`);
|
|
107
146
|
server.close();
|
|
108
|
-
if (
|
|
147
|
+
if (key) resolve7(key);
|
|
109
148
|
else reject(new Error("No key received from server"));
|
|
110
149
|
});
|
|
111
150
|
server.listen(port, () => {
|
|
@@ -123,6 +162,12 @@ Opening browser for authentication\u2026`);
|
|
|
123
162
|
reject(new Error("Login timed out after 5 minutes"));
|
|
124
163
|
}, 5 * 60 * 1e3);
|
|
125
164
|
});
|
|
165
|
+
}
|
|
166
|
+
var loginCommand = new Command3("login").description("Authenticate the CLI via browser (Google OAuth)").option("--url <url>", "API URL", "https://mr-manager-gold.vercel.app").option("--headless", "Use device-code flow for remote/headless environments").action(async (opts) => {
|
|
167
|
+
const config = loadConfig();
|
|
168
|
+
const apiUrl = opts.url ?? config.apiUrl ?? "https://mr-manager-gold.vercel.app";
|
|
169
|
+
const useDeviceCode = opts.headless || isHeadless();
|
|
170
|
+
const key = useDeviceCode ? await loginWithDeviceCode(apiUrl) : await loginWithLocalServer(apiUrl);
|
|
126
171
|
config.apiKey = key;
|
|
127
172
|
config.apiUrl = apiUrl;
|
|
128
173
|
saveConfig(config);
|
|
@@ -143,7 +188,7 @@ try {
|
|
|
143
188
|
const pkg = JSON.parse(readFileSync2(join2(__dirname, "..", "package.json"), "utf-8"));
|
|
144
189
|
cliVersion = pkg.version;
|
|
145
190
|
} catch {
|
|
146
|
-
cliVersion = "0.
|
|
191
|
+
cliVersion = "0.2.0";
|
|
147
192
|
}
|
|
148
193
|
var ApiError = class extends Error {
|
|
149
194
|
constructor(status, statusText, body) {
|
|
@@ -210,11 +255,9 @@ var tasksCommand = new Command5("tasks").description("List tasks for the linked
|
|
|
210
255
|
const params = new URLSearchParams();
|
|
211
256
|
params.set("projectId", projectId);
|
|
212
257
|
if (opts.inProgress) {
|
|
213
|
-
params.set("
|
|
258
|
+
params.set("status", "in_progress");
|
|
214
259
|
} else if (opts.completed) {
|
|
215
|
-
params.set("
|
|
216
|
-
} else if (!opts.all) {
|
|
217
|
-
params.set("completed", "false");
|
|
260
|
+
params.set("status", "completed");
|
|
218
261
|
}
|
|
219
262
|
path += `?${params.toString()}`;
|
|
220
263
|
const tasks = await api.get(path);
|
|
@@ -364,9 +407,9 @@ var contextCommand = new Command7("context").description("Output project context
|
|
|
364
407
|
const allTasks = await api.get(
|
|
365
408
|
`/api/projects/${projectId}/tasks`
|
|
366
409
|
);
|
|
367
|
-
const inProgress = allTasks.filter((t) => t.
|
|
368
|
-
const todo = allTasks.filter((t) =>
|
|
369
|
-
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);
|
|
370
413
|
const summaryLines = [
|
|
371
414
|
`Project: ${project.name} (${project.status})`,
|
|
372
415
|
...inProgress.map((t) => `In Progress: ${t.title}`),
|
|
@@ -2023,8 +2066,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2023
2066
|
}
|
|
2024
2067
|
}
|
|
2025
2068
|
await api.patch(`/api/tasks/${task.id}`, {
|
|
2026
|
-
|
|
2027
|
-
readyForReview: true,
|
|
2069
|
+
status: "review",
|
|
2028
2070
|
...prUrl ? { link: prUrl } : {}
|
|
2029
2071
|
});
|
|
2030
2072
|
logSuccess(prefix, `"${paint("bold", task.title)}" marked ready for review`);
|
|
@@ -2125,8 +2167,7 @@ var watchCommand = new Command8("watch").description(
|
|
|
2125
2167
|
}
|
|
2126
2168
|
}
|
|
2127
2169
|
await api.patch(`/api/tasks/${task.id}`, {
|
|
2128
|
-
|
|
2129
|
-
readyForReview: true,
|
|
2170
|
+
status: "review",
|
|
2130
2171
|
...prdContent ? { prdContent } : {}
|
|
2131
2172
|
});
|
|
2132
2173
|
logSuccess(prefix, `"${paint("bold", task.title)}" PRD generated and marked ready for review`);
|
|
@@ -2402,20 +2443,24 @@ ${divider}`);
|
|
|
2402
2443
|
}
|
|
2403
2444
|
}
|
|
2404
2445
|
async function poll() {
|
|
2405
|
-
let
|
|
2446
|
+
let queuedTasks;
|
|
2447
|
+
let delegatedTasks;
|
|
2406
2448
|
try {
|
|
2407
|
-
|
|
2408
|
-
|
|
2449
|
+
queuedTasks = await api.get("/api/tasks?status=queued");
|
|
2450
|
+
delegatedTasks = await api.get("/api/tasks?status=delegated");
|
|
2409
2451
|
} catch (err) {
|
|
2410
2452
|
logError(watchTag(), `Failed to fetch tasks: ${err.message}`);
|
|
2411
2453
|
return;
|
|
2412
2454
|
}
|
|
2455
|
+
const nonTestQueued = queuedTasks.filter((t) => t.mode !== "testing");
|
|
2456
|
+
const nonTestDelegated = delegatedTasks.filter((t) => t.mode !== "testing");
|
|
2457
|
+
const tasks = [...nonTestQueued, ...nonTestDelegated];
|
|
2413
2458
|
const config = loadConfig();
|
|
2414
|
-
const
|
|
2459
|
+
const activeTaskIds = new Set(tasks.map((t) => t.id));
|
|
2415
2460
|
for (const [taskId, entry] of active) {
|
|
2416
2461
|
if (taskId.startsWith("proto-") || taskId.startsWith("repo-") || taskId.startsWith("scan-")) continue;
|
|
2417
|
-
if (!
|
|
2418
|
-
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer
|
|
2462
|
+
if (!activeTaskIds.has(taskId)) {
|
|
2463
|
+
logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer active, terminating\u2026`);
|
|
2419
2464
|
entry.process.kill("SIGTERM");
|
|
2420
2465
|
active.delete(taskId);
|
|
2421
2466
|
queued.delete(taskId);
|
|
@@ -2424,11 +2469,11 @@ ${divider}`);
|
|
|
2424
2469
|
const nonTaskPrefixes = ["proto-", "repo-", "scan-", "idea-", "test-"];
|
|
2425
2470
|
for (const taskId of failed.keys()) {
|
|
2426
2471
|
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2427
|
-
if (!
|
|
2472
|
+
if (!activeTaskIds.has(taskId)) failed.delete(taskId);
|
|
2428
2473
|
}
|
|
2429
2474
|
for (const taskId of queued) {
|
|
2430
2475
|
if (nonTaskPrefixes.some((p) => taskId.startsWith(p))) continue;
|
|
2431
|
-
if (!
|
|
2476
|
+
if (!activeTaskIds.has(taskId)) queued.delete(taskId);
|
|
2432
2477
|
}
|
|
2433
2478
|
for (const task of tasks) {
|
|
2434
2479
|
if (queued.has(task.id)) continue;
|
|
@@ -2448,6 +2493,14 @@ ${divider}`);
|
|
|
2448
2493
|
failed.set(task.id, reason);
|
|
2449
2494
|
continue;
|
|
2450
2495
|
}
|
|
2496
|
+
if (task.status === "queued") {
|
|
2497
|
+
try {
|
|
2498
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "delegated" });
|
|
2499
|
+
} catch (err) {
|
|
2500
|
+
logError(prefix, `Failed to mark task as delegated: ${err.message}`);
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2451
2504
|
queued.add(task.id);
|
|
2452
2505
|
if (dryRun) {
|
|
2453
2506
|
logInfo(
|
|
@@ -2456,7 +2509,7 @@ ${divider}`);
|
|
|
2456
2509
|
);
|
|
2457
2510
|
continue;
|
|
2458
2511
|
}
|
|
2459
|
-
if (task.
|
|
2512
|
+
if (task.mode === "planning") {
|
|
2460
2513
|
dispatchPlanModeTask(task, repoDir);
|
|
2461
2514
|
} else if (planApproval) {
|
|
2462
2515
|
approvalQueue.push({ task, repoDir });
|
|
@@ -2520,12 +2573,7 @@ ${divider}`);
|
|
|
2520
2573
|
}
|
|
2521
2574
|
dispatchPrototypeJob(proto, repoDir);
|
|
2522
2575
|
}
|
|
2523
|
-
|
|
2524
|
-
try {
|
|
2525
|
-
testTasks = await api.get("/api/tasks?testStatus=pending");
|
|
2526
|
-
} catch (err) {
|
|
2527
|
-
logError(watchTag(), `Failed to fetch test tasks: ${err.message}`);
|
|
2528
|
-
}
|
|
2576
|
+
const testTasks = queuedTasks.filter((t) => t.mode === "testing");
|
|
2529
2577
|
for (const task of testTasks) {
|
|
2530
2578
|
const key = `test-${task.id}`;
|
|
2531
2579
|
if (queued.has(key)) continue;
|
|
@@ -2547,7 +2595,7 @@ ${divider}`);
|
|
|
2547
2595
|
(async () => {
|
|
2548
2596
|
logDispatch(prefix, `Running test for "${paint("bold", task.title)}"\u2026`);
|
|
2549
2597
|
try {
|
|
2550
|
-
await api.patch(`/api/tasks/${task.id}`, {
|
|
2598
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "delegated" });
|
|
2551
2599
|
await postTaskUpdate(task.id, "Test started \u2014 setting up environment\u2026");
|
|
2552
2600
|
let localPath;
|
|
2553
2601
|
try {
|
|
@@ -2610,12 +2658,12 @@ ${divider}`);
|
|
|
2610
2658
|
},
|
|
2611
2659
|
onProgress: (msg) => logInfo(prefix, msg)
|
|
2612
2660
|
});
|
|
2613
|
-
await api.patch(`/api/tasks/${task.id}`, {
|
|
2661
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: result.status });
|
|
2614
2662
|
logSuccess(prefix, result.summary);
|
|
2615
2663
|
} catch (err) {
|
|
2616
2664
|
logError(prefix, `Test failed: ${err.message}`);
|
|
2617
2665
|
try {
|
|
2618
|
-
await api.patch(`/api/tasks/${task.id}`, {
|
|
2666
|
+
await api.patch(`/api/tasks/${task.id}`, { status: "completed", testResult: "failed" });
|
|
2619
2667
|
await postTaskUpdate(task.id, `Test failed: ${err.message}`);
|
|
2620
2668
|
} catch {
|
|
2621
2669
|
}
|
|
@@ -2814,21 +2862,20 @@ function printTaskBanner(action, title, id) {
|
|
|
2814
2862
|
}
|
|
2815
2863
|
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) => {
|
|
2816
2864
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2817
|
-
|
|
2818
|
-
delegated: false
|
|
2865
|
+
status: "in_progress"
|
|
2819
2866
|
});
|
|
2820
2867
|
printTaskBanner(paint2("green", "start"), task.title, task.id);
|
|
2821
2868
|
});
|
|
2822
|
-
var delegateCommand = new Command9("delegate").description("
|
|
2869
|
+
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) => {
|
|
2823
2870
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2824
|
-
|
|
2825
|
-
delegated: true
|
|
2871
|
+
status: "queued"
|
|
2826
2872
|
});
|
|
2827
2873
|
printTaskBanner(paint2("cyan", "delegate"), task.title, task.id);
|
|
2828
2874
|
});
|
|
2829
|
-
var undelegateCommand = new Command9("undelegate").description("Remove delegation from a task
|
|
2875
|
+
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) => {
|
|
2830
2876
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2831
|
-
|
|
2877
|
+
status: "todo",
|
|
2878
|
+
mode: "development"
|
|
2832
2879
|
});
|
|
2833
2880
|
printTaskBanner(paint2("yellow", "undelegate"), task.title, task.id);
|
|
2834
2881
|
});
|
|
@@ -2847,7 +2894,7 @@ var createCommand = new Command10("create").description("Create a new task in th
|
|
|
2847
2894
|
title,
|
|
2848
2895
|
projectId,
|
|
2849
2896
|
notes: opts.notes,
|
|
2850
|
-
|
|
2897
|
+
status: opts.inProgress ? "in_progress" : "todo"
|
|
2851
2898
|
});
|
|
2852
2899
|
const t = task;
|
|
2853
2900
|
console.log(`Created: ${t.title} (${t.id})`);
|
|
@@ -2882,8 +2929,7 @@ function printTaskBanner2(action, title, id) {
|
|
|
2882
2929
|
}
|
|
2883
2930
|
var completeCommand = new Command11("complete").description("Mark a task as completed").argument("<task-id>", "Task ID to complete").action(async (taskId) => {
|
|
2884
2931
|
const task = await api.patch(`/api/tasks/${taskId}`, {
|
|
2885
|
-
|
|
2886
|
-
inProgress: false
|
|
2932
|
+
status: "completed"
|
|
2887
2933
|
});
|
|
2888
2934
|
printTaskBanner2(paint3("green", "complete \u2713"), task.title, task.id);
|
|
2889
2935
|
});
|
|
@@ -3818,7 +3864,7 @@ var resumeCommand = new Command19("resume").description("Resume an interactive C
|
|
|
3818
3864
|
);
|
|
3819
3865
|
process.exit(1);
|
|
3820
3866
|
}
|
|
3821
|
-
if (task.completed ||
|
|
3867
|
+
if (task.status === "completed" || task.status === "todo") {
|
|
3822
3868
|
console.error(
|
|
3823
3869
|
`
|
|
3824
3870
|
${paint8("yellow", "\u26A0")} Task ${paint8("dim", taskId.slice(0, 8))} has already completed.`
|
|
@@ -4271,7 +4317,7 @@ var mobileCommand = new Command25("mobile").description(
|
|
|
4271
4317
|
const task = await api.post("/api/tasks", {
|
|
4272
4318
|
title: "Convert to Mobile App",
|
|
4273
4319
|
projectId,
|
|
4274
|
-
|
|
4320
|
+
status: "in_progress"
|
|
4275
4321
|
});
|
|
4276
4322
|
console.log(` Created task: ${task.title} (${task.id})`);
|
|
4277
4323
|
console.log(" Analyzing web app...");
|
|
@@ -5510,7 +5556,7 @@ if (isFirstRun && !shouldBypass) {
|
|
|
5510
5556
|
process.exit(0);
|
|
5511
5557
|
}
|
|
5512
5558
|
var program = new Command29();
|
|
5513
|
-
program.name("mr").description("Mr. Manager - Task and project management CLI").version("0.
|
|
5559
|
+
program.name("mr").description("Mr. Manager - Task and project management CLI").version("0.2.0");
|
|
5514
5560
|
program.addCommand(initCommand);
|
|
5515
5561
|
program.addCommand(authCommand);
|
|
5516
5562
|
program.addCommand(loginCommand);
|