@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.
Files changed (2) hide show
  1. package/dist/index.mjs +100 -54
  2. 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
- var loginCommand = new Command3("login").description("Authenticate the CLI via browser (Google OAuth)").option("--url <url>", "API URL", "https://mr-manager-gold.vercel.app").action(async (opts) => {
93
- const config = loadConfig();
94
- const apiUrl = opts.url ?? config.apiUrl ?? "https://mr-manager-gold.vercel.app";
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
- const key = await new Promise((resolve7, reject) => {
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 key2 = url.searchParams.get("key");
138
+ const key = url.searchParams.get("key");
100
139
  res.writeHead(200, { "Content-Type": "text/html" });
101
140
  res.end(`
102
- <html><body style="font-family:sans-serif;text-align:center;padding:60px">
103
- <h2>${key2 ? "\u2713 Authenticated!" : "Something went wrong."}</h2>
104
- <p>${key2 ? "You can close this tab and return to the terminal." : "No key received. Please try again."}</p>
105
- </body></html>
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 (key2) resolve7(key2);
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.1.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("inProgress", "true");
258
+ params.set("status", "in_progress");
214
259
  } else if (opts.completed) {
215
- params.set("completed", "true");
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.inProgress && !t.completed);
368
- const todo = allTasks.filter((t) => !t.completed && !t.inProgress);
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
- inProgress: false,
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
- inProgress: false,
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 tasks;
2446
+ let queuedTasks;
2447
+ let delegatedTasks;
2406
2448
  try {
2407
- const allInProgress = await api.get("/api/tasks?inProgress=true");
2408
- tasks = allInProgress.filter((t) => t.delegated || t.planMode);
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 inProgressIds = new Set(tasks.map((t) => t.id));
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 (!inProgressIds.has(taskId)) {
2418
- logWarn(watchTag(), `Task ${paint("yellow", taskId.slice(0, 8))} no longer in-progress, terminating\u2026`);
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 (!inProgressIds.has(taskId)) failed.delete(taskId);
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 (!inProgressIds.has(taskId)) queued.delete(taskId);
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.planMode) {
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
- let testTasks = [];
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}`, { testStatus: "running" });
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}`, { testStatus: result.status });
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}`, { testStatus: "failed" });
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
- inProgress: true,
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("Mark a task as in-progress and delegate it to the watch agent").argument("<task-id>", "Task ID to delegate").action(async (taskId) => {
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
- inProgress: true,
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 (keep it in-progress)").argument("<task-id>", "Task ID to undelegate").action(async (taskId) => {
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
- delegated: false
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
- inProgress: opts.inProgress ?? false
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
- completed: true,
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 || !task.inProgress && !task.delegated) {
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
- inProgress: true
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.1.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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunnewold-labs/mr-manager",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Mr. Manager - Task and project management CLI",
5
5
  "bin": {
6
6
  "mr": "./dist/index.mjs"