@dunnewold-labs/mr-manager 0.4.22 → 0.4.23

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 +188 -76
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -132,7 +132,7 @@ Remote login \u2014 visit this URL in any browser:
132
132
  }
133
133
  async function loginWithLocalServer(apiUrl) {
134
134
  const port = getRandomPort();
135
- return new Promise((resolve8, reject) => {
135
+ return new Promise((resolve9, reject) => {
136
136
  const server = createServer((req, res) => {
137
137
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
138
138
  const key = url.searchParams.get("key");
@@ -144,7 +144,7 @@ async function loginWithLocalServer(apiUrl) {
144
144
  </body></html>
145
145
  `);
146
146
  server.close();
147
- if (key) resolve8(key);
147
+ if (key) resolve9(key);
148
148
  else reject(new Error("No key received from server"));
149
149
  });
150
150
  server.listen(port, () => {
@@ -185,7 +185,7 @@ import { fileURLToPath } from "url";
185
185
  // cli/package.json
186
186
  var package_default = {
187
187
  name: "@dunnewold-labs/mr-manager",
188
- version: "0.4.22",
188
+ version: "0.4.23",
189
189
  description: "Mr. Manager - Task and project management CLI",
190
190
  bin: {
191
191
  mr: "./dist/index.mjs"
@@ -395,10 +395,10 @@ function detectVcs(cwd) {
395
395
  // cli/commands/link.ts
396
396
  function prompt(question) {
397
397
  const rl = createInterface({ input: process.stdin, output: process.stdout });
398
- return new Promise((resolve8) => {
398
+ return new Promise((resolve9) => {
399
399
  rl.question(question, (answer) => {
400
400
  rl.close();
401
- resolve8(answer.trim());
401
+ resolve9(answer.trim());
402
402
  });
403
403
  });
404
404
  }
@@ -1188,7 +1188,7 @@ Run the setup script: cd browse && ./setup`
1188
1188
  async function runBrowseCommand2(browseArgs) {
1189
1189
  const runner = getBrowseRunner();
1190
1190
  const fullArgs = [...runner.args, ...browseArgs];
1191
- return new Promise((resolve8) => {
1191
+ return new Promise((resolve9) => {
1192
1192
  const proc = spawn3(runner.cmd, fullArgs, {
1193
1193
  stdio: ["pipe", "pipe", "pipe"],
1194
1194
  env: { ...process.env }
@@ -1205,10 +1205,10 @@ async function runBrowseCommand2(browseArgs) {
1205
1205
  if (stderr && code !== 0) {
1206
1206
  process.stderr.write(stderr);
1207
1207
  }
1208
- resolve8({ stdout: stdout.trim(), exitCode: code || 0 });
1208
+ resolve9({ stdout: stdout.trim(), exitCode: code || 0 });
1209
1209
  });
1210
1210
  proc.on("error", () => {
1211
- resolve8({ stdout: "", exitCode: 1 });
1211
+ resolve9({ stdout: "", exitCode: 1 });
1212
1212
  });
1213
1213
  });
1214
1214
  }
@@ -1595,13 +1595,13 @@ ${task.notes}` : "";
1595
1595
  }
1596
1596
  function findPrUrl(branchName, repoDir, vcs = "github") {
1597
1597
  const cmd = vcs === "gitlab" ? `glab mr view "${branchName}" --output json 2>/dev/null | jq -r '.web_url // empty'` : `gh pr view "${branchName}" --json url -q .url`;
1598
- return new Promise((resolve8) => {
1598
+ return new Promise((resolve9) => {
1599
1599
  exec(
1600
1600
  cmd,
1601
1601
  { cwd: repoDir },
1602
1602
  (err, stdout) => {
1603
- if (err) resolve8(null);
1604
- else resolve8(stdout.trim() || null);
1603
+ if (err) resolve9(null);
1604
+ else resolve9(stdout.trim() || null);
1605
1605
  }
1606
1606
  );
1607
1607
  });
@@ -1640,8 +1640,8 @@ function extractPrUrlFromText(value) {
1640
1640
  return match ? match[0] : null;
1641
1641
  }
1642
1642
  function commandSucceeds(command, cwd) {
1643
- return new Promise((resolve8) => {
1644
- exec(command, { cwd }, (err) => resolve8(!err));
1643
+ return new Promise((resolve9) => {
1644
+ exec(command, { cwd }, (err) => resolve9(!err));
1645
1645
  });
1646
1646
  }
1647
1647
  async function createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRefs = [], feedbackUpdates = [], existingResources = [], skillRefs = []) {
@@ -1656,13 +1656,13 @@ async function createPrInRepo(task, branchName, repoDir, vcs, subtasks, protoRef
1656
1656
  `, "utf-8");
1657
1657
  const createCommand2 = vcs === "gitlab" ? `glab mr create --source-branch ${JSON.stringify(branchName)} --title ${JSON.stringify(task.title)} --description-file ${JSON.stringify(bodyPath)} --yes` : `gh pr create --head ${JSON.stringify(branchName)} --title ${JSON.stringify(task.title)} --body-file ${JSON.stringify(bodyPath)}`;
1658
1658
  try {
1659
- const output = await new Promise((resolve8, reject) => {
1659
+ const output = await new Promise((resolve9, reject) => {
1660
1660
  exec(createCommand2, { cwd: repoDir }, (err, stdout, stderr) => {
1661
1661
  if (err) {
1662
1662
  reject(new Error(stderr.trim() || stdout.trim() || err.message));
1663
1663
  return;
1664
1664
  }
1665
- resolve8(`${stdout}
1665
+ resolve9(`${stdout}
1666
1666
  ${stderr}`.trim());
1667
1667
  });
1668
1668
  });
@@ -1744,32 +1744,43 @@ async function extractPrUrlFromUpdates(taskId) {
1744
1744
  return null;
1745
1745
  }
1746
1746
  function checkPrStatus(prUrl, repoDir, vcs = "github") {
1747
- const cmd = vcs === "gitlab" ? `glab mr view "${prUrl}" --output json 2>/dev/null` : `gh pr view "${prUrl}" --json merged,mergeable 2>/dev/null`;
1748
- return new Promise((resolve8) => {
1747
+ const cmd = vcs === "gitlab" ? `glab mr view "${prUrl}" --output json 2>/dev/null` : `gh pr view "${prUrl}" --json merged,mergeable,isDraft,statusCheckRollup,reviewDecision 2>/dev/null`;
1748
+ return new Promise((resolve9) => {
1749
1749
  exec(cmd, { cwd: repoDir }, (err, stdout) => {
1750
1750
  if (err || !stdout.trim()) {
1751
- resolve8(null);
1751
+ resolve9(null);
1752
1752
  return;
1753
1753
  }
1754
1754
  try {
1755
1755
  const data = JSON.parse(stdout.trim());
1756
1756
  if (vcs === "gitlab") {
1757
- resolve8({
1758
- merged: data.state === "merged",
1759
- hasConflicts: data.has_conflicts === true
1760
- });
1757
+ const merged = data.state === "merged";
1758
+ const hasConflicts = data.has_conflicts === true;
1759
+ const isMergeable = !merged && data.state === "opened" && !hasConflicts && !data.draft && (data.merge_status === "can_be_merged" || data.merge_status === "can_be_merged_rebase");
1760
+ resolve9({ merged, hasConflicts, isMergeable });
1761
1761
  } else {
1762
- resolve8({
1763
- merged: data.merged === true,
1764
- hasConflicts: data.mergeable === "CONFLICTING"
1765
- });
1762
+ const merged = data.merged === true;
1763
+ const hasConflicts = data.mergeable === "CONFLICTING";
1764
+ const checks = data.statusCheckRollup ?? [];
1765
+ const allChecksPassed = checks.length === 0 || checks.every((c12) => c12.state === "SUCCESS" || c12.state === "NEUTRAL" || c12.state === "SKIPPED");
1766
+ const noChangesRequested = data.reviewDecision !== "CHANGES_REQUESTED";
1767
+ const isMergeable = !merged && !hasConflicts && !data.isDraft && data.mergeable === "MERGEABLE" && allChecksPassed && noChangesRequested;
1768
+ resolve9({ merged, hasConflicts, isMergeable });
1766
1769
  }
1767
1770
  } catch {
1768
- resolve8(null);
1771
+ resolve9(null);
1769
1772
  }
1770
1773
  });
1771
1774
  });
1772
1775
  }
1776
+ function mergePrViaCli(prUrl, repoDir, vcs = "github") {
1777
+ const cmd = vcs === "gitlab" ? `glab mr merge "${prUrl}" --squash --remove-source-branch 2>&1` : `gh pr merge "${prUrl}" --squash --delete-branch 2>&1`;
1778
+ return new Promise((resolve9) => {
1779
+ exec(cmd, { cwd: repoDir }, (err) => {
1780
+ resolve9(!err);
1781
+ });
1782
+ });
1783
+ }
1773
1784
  function buildPrototypeSection(protoRefs, workingDir) {
1774
1785
  if (protoRefs.length === 0) return "";
1775
1786
  const sections = [
@@ -2464,7 +2475,7 @@ function buildIdeaPrompt(idea, repoDir) {
2464
2475
  `- Do NOT exit until both files have been written and verified`
2465
2476
  ].join("\n");
2466
2477
  }
2467
- function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt, maxTurns) {
2478
+ function buildAgentArgs(agent, prompt2, mode, sessionId, name, resumeSession = false, systemPrompt, maxTurns, claudeModel) {
2468
2479
  if (agent === "codex") {
2469
2480
  const args = [];
2470
2481
  if (mode === "execute") {
@@ -2494,19 +2505,37 @@ ${systemPrompt}` : prompt2;
2494
2505
  const nameArgs = name ? ["--name", name] : [];
2495
2506
  const systemArgs = systemPrompt ? ["--append-system-prompt", systemPrompt] : [];
2496
2507
  const turnsArgs = maxTurns ? ["--max-turns", String(maxTurns)] : [];
2508
+ const modelArgs = claudeModel ? ["--model", claudeModel] : [];
2497
2509
  if (mode === "plan") {
2498
- return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, "--permission-mode", "plan", "-p", prompt2] };
2510
+ return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, ...modelArgs, "--permission-mode", "plan", "-p", prompt2] };
2499
2511
  }
2500
2512
  const cfg = loadConfig();
2501
- const permissionMode = cfg.claudePermissionMode ?? "dangerously-skip-permissions";
2513
+ const permissionMode = cfg.claudePermissionMode ?? "auto";
2502
2514
  const permissionArgs = permissionMode === "dangerously-skip-permissions" ? ["--dangerously-skip-permissions"] : ["--permission-mode", "auto", "--enable-auto-mode"];
2503
- return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, ...permissionArgs, "-p", prompt2] };
2515
+ return { bin: "claude", args: [...sessionArgs, ...nameArgs, ...systemArgs, ...turnsArgs, ...modelArgs, ...permissionArgs, "-p", prompt2] };
2504
2516
  }
2505
2517
  function commandExists(cmd) {
2506
- return new Promise((resolve8) => {
2507
- exec(`command -v ${cmd}`, (err) => resolve8(!err));
2518
+ return new Promise((resolve9) => {
2519
+ exec(`command -v ${cmd}`, (err) => resolve9(!err));
2508
2520
  });
2509
2521
  }
2522
+ function resolveTaskAgentAndModel(delegatedModel, watchAgent) {
2523
+ if (!delegatedModel) return { agent: watchAgent };
2524
+ switch (delegatedModel) {
2525
+ case "claude-opus":
2526
+ return { agent: "claude", claudeModel: "claude-opus-4-6" };
2527
+ case "claude-sonnet":
2528
+ return { agent: "claude", claudeModel: "claude-sonnet-4-6" };
2529
+ case "claude-haiku":
2530
+ return { agent: "claude", claudeModel: "claude-haiku-4-5-20251001" };
2531
+ case "codex":
2532
+ return { agent: "codex" };
2533
+ case "gemini":
2534
+ return { agent: "gemini" };
2535
+ default:
2536
+ return { agent: watchAgent };
2537
+ }
2538
+ }
2510
2539
  function runPlanningPhase(task, repoDir, agent) {
2511
2540
  return new Promise((res, reject) => {
2512
2541
  const planPrompt = buildPlanningPrompt(task, repoDir);
@@ -2544,21 +2573,21 @@ ${output.trim()}`));
2544
2573
  });
2545
2574
  }
2546
2575
  function askYesNo(question) {
2547
- return new Promise((resolve8) => {
2576
+ return new Promise((resolve9) => {
2548
2577
  const rl = readline.createInterface({
2549
2578
  input: process.stdin,
2550
2579
  output: process.stdout
2551
2580
  });
2552
2581
  rl.question(question, (answer) => {
2553
2582
  rl.close();
2554
- resolve8(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
2583
+ resolve9(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
2555
2584
  });
2556
2585
  });
2557
2586
  }
2558
- function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name, resumeSession = false, onSpawnError, systemPrompt, maxTurns) {
2587
+ function spawnAgent(agent, repoDir, prompt2, prefix, onActivity, sessionId, name, resumeSession = false, onSpawnError, systemPrompt, maxTurns, claudeModel) {
2559
2588
  const jobLabel = name ?? "unknown";
2560
2589
  console.log(`${timestamp()} ${prefix} ${paint("dim", tokenLogLine("agent", jobLabel, prompt2, systemPrompt))}`);
2561
- const { bin, args } = buildAgentArgs(agent, prompt2, "execute", sessionId, name, resumeSession, systemPrompt, maxTurns);
2590
+ const { bin, args } = buildAgentArgs(agent, prompt2, "execute", sessionId, name, resumeSession, systemPrompt, maxTurns, claudeModel);
2562
2591
  const child = spawn4(bin, args, { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] });
2563
2592
  child.on("error", (err) => {
2564
2593
  logError(prefix, `Failed to spawn ${agent}: ${err.message}`);
@@ -2818,10 +2847,16 @@ var watchCommand = new Command8("watch").description(
2818
2847
  const touchActivity = () => {
2819
2848
  activeEntry.lastActivityAt = Date.now();
2820
2849
  };
2821
- const attemptOrder = await resolveAgentChain(agent);
2850
+ const { agent: taskAgent, claudeModel: taskClaudeModel } = resolveTaskAgentAndModel(task.delegatedModel, agent);
2851
+ if (taskClaudeModel) {
2852
+ logInfo(prefix, `Using model: ${paint("cyan", taskClaudeModel)}`);
2853
+ } else if (taskAgent !== agent) {
2854
+ logInfo(prefix, `Using agent override: ${paint("cyan", taskAgent)}`);
2855
+ }
2856
+ const attemptOrder = await resolveAgentChain(taskAgent);
2822
2857
  if (attemptOrder.length === 0) {
2823
- logError(prefix, `No available agents found for fallback chain starting at ${agent}`);
2824
- await moveTaskToError(task, prefix, `No available agent found for fallback chain starting at ${agent}`);
2858
+ logError(prefix, `No available agents found for fallback chain starting at ${taskAgent}`);
2859
+ await moveTaskToError(task, prefix, `No available agent found for fallback chain starting at ${taskAgent}`);
2825
2860
  if (activeEntry.cleanupRepoDir && activeEntry.cleanupWorktreePath) {
2826
2861
  removeWorktree(activeEntry.cleanupRepoDir, activeEntry.cleanupWorktreePath);
2827
2862
  }
@@ -2835,6 +2870,7 @@ var watchCommand = new Command8("watch").description(
2835
2870
  const pausedForNetwork = networkPaused.get(task.id);
2836
2871
  const shouldResumeClaudeSession = attemptAgent === "claude" && !!task.claudeSessionId && !resumeAlreadyRetried && (hasFeedback || pausedForNetwork?.resumeSession === true);
2837
2872
  const sessionId = attemptAgent === "claude" ? shouldResumeClaudeSession ? task.claudeSessionId : randomUUID() : void 0;
2873
+ const effectiveClaudeModel = attemptAgent === "claude" ? taskClaudeModel : void 0;
2838
2874
  const executionSystemPrompt = composeSystemPrompt(EXECUTION_SYSTEM_SECTIONS);
2839
2875
  const child = spawnAgent(
2840
2876
  attemptAgent,
@@ -2848,7 +2884,9 @@ var watchCommand = new Command8("watch").description(
2848
2884
  (err) => {
2849
2885
  spawnFailureReason = err.message;
2850
2886
  },
2851
- executionSystemPrompt
2887
+ executionSystemPrompt,
2888
+ void 0,
2889
+ effectiveClaudeModel
2852
2890
  );
2853
2891
  activeEntry.process = child;
2854
2892
  activeEntry.currentAgent = attemptAgent;
@@ -4097,8 +4135,7 @@ ${divider}`);
4097
4135
  const prefix = taskTag(sid);
4098
4136
  const prLabel = task.link.includes("gitlab") ? "MR" : "PR";
4099
4137
  const vcs = task.link.includes("gitlab") ? "gitlab" : "github";
4100
- const repoDir = findDirectoryForProject(config, task.projectId, rootDir);
4101
- if (!repoDir) continue;
4138
+ const repoDir = findDirectoryForProject(config, task.projectId, rootDir) ?? rootDir;
4102
4139
  const status = await checkPrStatus(task.link, repoDir, vcs);
4103
4140
  if (!status) continue;
4104
4141
  if (status.merged) {
@@ -4115,6 +4152,26 @@ ${divider}`);
4115
4152
  }
4116
4153
  continue;
4117
4154
  }
4155
+ if (task.autoMerge && status.isMergeable) {
4156
+ logInfo(prefix, `Auto-merging ${prLabel} for "${paint("bold", task.title)}"`);
4157
+ const mergeOk = await mergePrViaCli(task.link, repoDir, vcs);
4158
+ if (mergeOk) {
4159
+ logSuccess(prefix, `${prLabel} auto-merged \u2014 completing "${paint("bold", task.title)}"`);
4160
+ try {
4161
+ await api.patch(`/api/tasks/${task.id}`, {
4162
+ status: "completed",
4163
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4164
+ inProgressSince: null
4165
+ });
4166
+ await postTaskUpdate(task.id, `${prLabel} auto-merged and task completed`, "system");
4167
+ } catch (err) {
4168
+ logError(prefix, `Failed to complete task after auto-merge: ${err.message}`);
4169
+ }
4170
+ } else {
4171
+ logWarn(prefix, `Auto-merge of ${prLabel} failed for "${paint("bold", task.title)}" \u2014 will retry next poll`);
4172
+ }
4173
+ continue;
4174
+ }
4118
4175
  if (status.hasConflicts) {
4119
4176
  logWarn(prefix, `${prLabel} has merge conflicts \u2014 re-dispatching agent for "${paint("bold", task.title)}"`);
4120
4177
  try {
@@ -4445,14 +4502,14 @@ function paint5(color, text) {
4445
4502
  return `${c5[color]}${text}${c5.reset}`;
4446
4503
  }
4447
4504
  function commandExists2(cmd) {
4448
- return new Promise((resolve8) => {
4449
- exec2(`which ${cmd}`, (err) => resolve8(!err));
4505
+ return new Promise((resolve9) => {
4506
+ exec2(`which ${cmd}`, (err) => resolve9(!err));
4450
4507
  });
4451
4508
  }
4452
4509
  function execQuiet(cmd) {
4453
- return new Promise((resolve8) => {
4510
+ return new Promise((resolve9) => {
4454
4511
  exec2(cmd, (err, stdout, stderr) => {
4455
- resolve8({ ok: !err, stdout: stdout.trim(), stderr: stderr.trim() });
4512
+ resolve9({ ok: !err, stdout: stdout.trim(), stderr: stderr.trim() });
4456
4513
  });
4457
4514
  });
4458
4515
  }
@@ -4707,26 +4764,26 @@ async function autoFix(checks, agent) {
4707
4764
  if (claudeCheck && !claudeCheck.ok && agent === "claude") {
4708
4765
  console.log(paint5("cyan", " Installing Claude Code..."));
4709
4766
  console.log(paint5("dim", " Running: curl -fsSL https://claude.ai/install.sh | bash"));
4710
- await new Promise((resolve8) => {
4767
+ await new Promise((resolve9) => {
4711
4768
  const child = spawn8("bash", ["-c", "curl -fsSL https://claude.ai/install.sh | bash"], { stdio: "inherit" });
4712
- child.on("exit", () => resolve8());
4769
+ child.on("exit", () => resolve9());
4713
4770
  });
4714
4771
  console.log("");
4715
4772
  }
4716
4773
  if (ghInstalled && !ghAuthed) {
4717
4774
  console.log(paint5("cyan", " Running gh auth login..."));
4718
- await new Promise((resolve8) => {
4775
+ await new Promise((resolve9) => {
4719
4776
  const child = spawn8("gh", ["auth", "login"], { stdio: "inherit" });
4720
- child.on("exit", () => resolve8());
4777
+ child.on("exit", () => resolve9());
4721
4778
  });
4722
4779
  console.log("");
4723
4780
  }
4724
4781
  if (!mrAuthed) {
4725
4782
  console.log(paint5("cyan", " Running mr login..."));
4726
4783
  const entry = process.argv[1];
4727
- await new Promise((resolve8) => {
4784
+ await new Promise((resolve9) => {
4728
4785
  const child = spawn8(process.execPath, [entry, "login"], { stdio: "inherit" });
4729
- child.on("exit", () => resolve8());
4786
+ child.on("exit", () => resolve9());
4730
4787
  });
4731
4788
  console.log("");
4732
4789
  }
@@ -4975,7 +5032,8 @@ var resumeCommand = new Command17("resume").description("Resume an interactive C
4975
5032
  import { Command as Command18 } from "commander";
4976
5033
  import { execSync as execSync4, spawn as spawn6 } from "child_process";
4977
5034
  import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
4978
- import { join as join8 } from "path";
5035
+ import { createHash } from "crypto";
5036
+ import { join as join8, resolve as resolve4 } from "path";
4979
5037
  var BROWSE_DIR2 = join8(import.meta.dirname, "..", "..", "browse");
4980
5038
  function isProcessAlive(pid) {
4981
5039
  try {
@@ -5003,43 +5061,93 @@ async function findAvailablePort2(startPort) {
5003
5061
  }
5004
5062
  throw new Error(`No available port found in range ${startPort}-${startPort + 99}`);
5005
5063
  }
5006
- async function ensureDevServer() {
5007
- const devStateFile = "/tmp/mr-dev-server.json";
5064
+ function detectDevCommand2(cwd) {
5065
+ try {
5066
+ const pkg = JSON.parse(readFileSync7(join8(cwd, "package.json"), "utf-8"));
5067
+ const scripts = pkg.scripts || {};
5068
+ if (scripts.dev) return "npm run dev";
5069
+ if (scripts.start) return "npm start";
5070
+ if (scripts.serve) return "npm run serve";
5071
+ } catch {
5072
+ }
5073
+ return "npm run dev";
5074
+ }
5075
+ function parseCommand(cmd) {
5076
+ const args = [];
5077
+ let current = "";
5078
+ let inSingle = false;
5079
+ let inDouble = false;
5080
+ for (let i = 0; i < cmd.length; i++) {
5081
+ const ch = cmd[i];
5082
+ if (ch === "'" && !inDouble) {
5083
+ inSingle = !inSingle;
5084
+ } else if (ch === '"' && !inSingle) {
5085
+ inDouble = !inDouble;
5086
+ } else if (ch === " " && !inSingle && !inDouble) {
5087
+ if (current) {
5088
+ args.push(current);
5089
+ current = "";
5090
+ }
5091
+ } else {
5092
+ current += ch;
5093
+ }
5094
+ }
5095
+ if (current) args.push(current);
5096
+ return args;
5097
+ }
5098
+ async function ensureDevServer(options = {}) {
5099
+ const projectCwd = options.cwd ? resolve4(options.cwd) : join8(import.meta.dirname, "..", "..");
5100
+ const devCmd = options.cmd || detectDevCommand2(projectCwd);
5101
+ const cwdHash = createHash("md5").update(projectCwd).digest("hex").slice(0, 8);
5102
+ const devStateFile = `/tmp/mr-dev-server-${cwdHash}.json`;
5008
5103
  try {
5009
5104
  const state = JSON.parse(readFileSync7(devStateFile, "utf-8"));
5010
5105
  if (isProcessAlive(state.pid) && await isPortResponding2(state.port)) {
5106
+ console.log(`[browse] Reusing dev server on port ${state.port} (${projectCwd})`);
5011
5107
  return state.port;
5012
5108
  }
5013
5109
  } catch {
5014
5110
  }
5015
5111
  const port = await findAvailablePort2(3e3);
5016
- console.log(`[browse] Starting dev server on port ${port}...`);
5017
- const devProc = spawn6("npm", ["run", "dev", "--", "--port", String(port)], {
5112
+ const argv = parseCommand(devCmd);
5113
+ const portFlag = options.portFlag;
5114
+ let spawnArgs;
5115
+ let spawnEnv = { ...process.env };
5116
+ if (portFlag) {
5117
+ spawnArgs = [...argv.slice(1), portFlag, String(port)];
5118
+ } else if (devCmd.includes("next") || devCmd.includes("vite") || devCmd.includes("webpack")) {
5119
+ spawnArgs = [...argv.slice(1), "--", "--port", String(port)];
5120
+ } else {
5121
+ spawnEnv.PORT = String(port);
5122
+ spawnArgs = argv.slice(1);
5123
+ }
5124
+ console.log(`[browse] Starting dev server: ${devCmd} (port ${port}) in ${projectCwd}`);
5125
+ const devProc = spawn6(argv[0], spawnArgs, {
5018
5126
  stdio: ["ignore", "pipe", "pipe"],
5019
5127
  detached: true,
5020
- cwd: join8(import.meta.dirname, "..", ".."),
5021
- env: { ...process.env }
5128
+ cwd: projectCwd,
5129
+ env: spawnEnv
5022
5130
  });
5023
5131
  devProc.unref();
5024
5132
  writeFileSync4(
5025
5133
  devStateFile,
5026
- JSON.stringify({ pid: devProc.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
5134
+ JSON.stringify({ pid: devProc.pid, port, cwd: projectCwd, cmd: devCmd, startedAt: (/* @__PURE__ */ new Date()).toISOString() }),
5027
5135
  { mode: 384 }
5028
5136
  );
5029
5137
  const start = Date.now();
5030
- while (Date.now() - start < 3e4) {
5138
+ while (Date.now() - start < 6e4) {
5031
5139
  if (await isPortResponding2(port)) {
5032
5140
  console.log(`[browse] Dev server ready on port ${port}`);
5033
5141
  return port;
5034
5142
  }
5035
5143
  await new Promise((r) => setTimeout(r, 500));
5036
5144
  }
5037
- throw new Error("Dev server failed to start within 30s");
5145
+ throw new Error(`Dev server failed to start within 60s. Command: ${devCmd} in ${projectCwd}`);
5038
5146
  }
5039
5147
  var browseCommand = new Command18("browse").description("Control a headless browser for QA and testing").argument("[command]", "Browse command (goto, click, fill, screenshot, etc.)").argument("[args...]", "Command arguments").option(
5040
5148
  "--task-id <id>",
5041
5149
  "Attach output to a task update (for screenshot and recording-stop commands)"
5042
- ).option("--dev", "Auto-start local dev server before browsing").allowUnknownOption(true).action(
5150
+ ).option("--dev", "Auto-start local dev server before browsing").option("--dev-cwd <path>", "Working directory for the dev server (defaults to mr-manager root)").option("--dev-cmd <command>", "Dev server command to run (auto-detected from package.json if omitted)").option("--dev-port-flag <flag>", "CLI flag name used to set port (e.g. --port). Omit to use PORT env var.").allowUnknownOption(true).action(
5043
5151
  async (command, args, opts) => {
5044
5152
  if (!command) {
5045
5153
  const { stdout: stdout2 } = await runBrowseCommand2(["--help"]);
@@ -5058,7 +5166,11 @@ var browseCommand = new Command18("browse").description("Control a headless brow
5058
5166
  }
5059
5167
  if (opts.dev) {
5060
5168
  try {
5061
- const port = await ensureDevServer();
5169
+ const port = await ensureDevServer({
5170
+ cwd: opts.devCwd,
5171
+ cmd: opts.devCmd,
5172
+ portFlag: opts.devPortFlag
5173
+ });
5062
5174
  if (command === "goto" && args[0]) {
5063
5175
  const url = args[0];
5064
5176
  if (url.startsWith("/")) {
@@ -5176,10 +5288,10 @@ var browseCommand = new Command18("browse").description("Control a headless brow
5176
5288
 
5177
5289
  // cli/commands/set-path.ts
5178
5290
  import { Command as Command19 } from "commander";
5179
- import { resolve as resolve4 } from "path";
5291
+ import { resolve as resolve5 } from "path";
5180
5292
  import { existsSync as existsSync10 } from "fs";
5181
5293
  var setPathCommand = new Command19("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) => {
5182
- const absolutePath = resolve4(pathArg);
5294
+ const absolutePath = resolve5(pathArg);
5183
5295
  if (!existsSync10(absolutePath)) {
5184
5296
  console.error(`Error: Path does not exist: ${absolutePath}`);
5185
5297
  process.exit(1);
@@ -5362,7 +5474,7 @@ var testCommand = new Command20("test").description("Run automated browser test
5362
5474
  // cli/commands/features.ts
5363
5475
  import { Command as Command21 } from "commander";
5364
5476
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync12 } from "fs";
5365
- import { resolve as resolve5, sep as sep2 } from "path";
5477
+ import { resolve as resolve6, sep as sep2 } from "path";
5366
5478
  var FEATURES_FILE3 = ".mr-features.md";
5367
5479
  var c7 = {
5368
5480
  reset: "\x1B[0m",
@@ -5387,7 +5499,7 @@ function resolveProjectRoot2() {
5387
5499
  return cwd;
5388
5500
  }
5389
5501
  function getFeaturesPath() {
5390
- return resolve5(resolveProjectRoot2(), FEATURES_FILE3);
5502
+ return resolve6(resolveProjectRoot2(), FEATURES_FILE3);
5391
5503
  }
5392
5504
  function readFeatures2() {
5393
5505
  const path = getFeaturesPath();
@@ -5400,7 +5512,7 @@ var featuresCommand = new Command21("features").description("View or update the
5400
5512
  return;
5401
5513
  }
5402
5514
  if (opts.file) {
5403
- const content2 = readFileSync9(resolve5(opts.file), "utf-8");
5515
+ const content2 = readFileSync9(resolve6(opts.file), "utf-8");
5404
5516
  const featuresPath = getFeaturesPath();
5405
5517
  writeFileSync5(featuresPath, content2);
5406
5518
  console.log(`${paint7("green", "\u2713")} Updated ${paint7("cyan", featuresPath)} from ${paint7("cyan", opts.file)}`);
@@ -5424,10 +5536,10 @@ var featuresCommand = new Command21("features").description("View or update the
5424
5536
  // cli/commands/no-mr.ts
5425
5537
  import { Command as Command22 } from "commander";
5426
5538
  import { writeFileSync as writeFileSync6 } from "fs";
5427
- import { resolve as resolve6 } from "path";
5539
+ import { resolve as resolve7 } from "path";
5428
5540
  var NO_MR_FILE = ".mr-no-mr";
5429
5541
  var noMrCommand = new Command22("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) => {
5430
- const filePath = resolve6(process.cwd(), NO_MR_FILE);
5542
+ const filePath = resolve7(process.cwd(), NO_MR_FILE);
5431
5543
  writeFileSync6(filePath, description, "utf-8");
5432
5544
  await api.post(`/api/tasks/${taskId}/updates`, {
5433
5545
  message: `No MR/PR needed \u2014 ${description}`,
@@ -6114,7 +6226,7 @@ async function fetchScanContext(opts) {
6114
6226
  };
6115
6227
  }
6116
6228
  function runClaude(prompt2) {
6117
- return new Promise((resolve8, reject) => {
6229
+ return new Promise((resolve9, reject) => {
6118
6230
  const child = spawn7("claude", ["-p", "--dangerously-skip-permissions", prompt2], {
6119
6231
  stdio: ["ignore", "pipe", "pipe"]
6120
6232
  });
@@ -6127,7 +6239,7 @@ function runClaude(prompt2) {
6127
6239
  errOutput += d.toString();
6128
6240
  });
6129
6241
  child.on("exit", (code) => {
6130
- if (code === 0) resolve8(output.trim());
6242
+ if (code === 0) resolve9(output.trim());
6131
6243
  else reject(new Error(`claude exited with code ${code}
6132
6244
  ${errOutput.trim()}`));
6133
6245
  });
@@ -6567,7 +6679,7 @@ var doctorCommand = new Command25("doctor").description("Diagnose Mr. Manager CL
6567
6679
 
6568
6680
  // cli/commands/prompt-audit.ts
6569
6681
  import { Command as Command26 } from "commander";
6570
- import { resolve as resolve7 } from "path";
6682
+ import { resolve as resolve8 } from "path";
6571
6683
  import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
6572
6684
  function auditLine(label, tokens) {
6573
6685
  const bar = "\u2588".repeat(Math.min(60, Math.round(tokens / 200)));
@@ -6622,7 +6734,7 @@ ${task.notes}` : "";
6622
6734
  const config = loadConfig();
6623
6735
  const repoDir = Object.entries(config.directories).find(([, pid]) => pid === task.projectId)?.[0];
6624
6736
  if (repoDir) {
6625
- const featuresPath = resolve7(repoDir, ".mr-features.md");
6737
+ const featuresPath = resolve8(repoDir, ".mr-features.md");
6626
6738
  if (existsSync16(featuresPath)) {
6627
6739
  const featuresContent = readFileSync12(featuresPath, "utf-8");
6628
6740
  sections.push({ name: "features-doc", tokens: estimateTokens(featuresContent) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunnewold-labs/mr-manager",
3
- "version": "0.4.22",
3
+ "version": "0.4.23",
4
4
  "description": "Mr. Manager - Task and project management CLI",
5
5
  "bin": {
6
6
  "mr": "./dist/index.mjs"