dev-loops 0.2.6 → 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 (92) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +2 -1
  3. package/.claude/agents/developer.md +1 -0
  4. package/.claude/agents/fixer.md +1 -0
  5. package/.claude/agents/review.md +30 -0
  6. package/.claude/skills/copilot-pr-followup/SKILL.md +57 -3
  7. package/.claude/skills/dev-loop/SKILL.md +5 -5
  8. package/.claude/skills/docs/anti-patterns.md +2 -0
  9. package/.claude/skills/docs/copilot-loop-operations.md +2 -2
  10. package/.claude/skills/local-implementation/SKILL.md +17 -3
  11. package/AGENTS.md +1 -1
  12. package/CHANGELOG.md +60 -0
  13. package/agents/dev-loop.agent.md +5 -1
  14. package/agents/developer.agent.md +1 -0
  15. package/agents/fixer.agent.md +1 -0
  16. package/agents/review.agent.md +30 -0
  17. package/cli/index.mjs +42 -7
  18. package/package.json +2 -2
  19. package/scripts/README.md +6 -5
  20. package/scripts/_cli-primitives.mjs +2 -0
  21. package/scripts/_core-helpers.mjs +1 -0
  22. package/scripts/claude/generate-claude-assets.mjs +12 -2
  23. package/scripts/claude/headless-dev-loop.mjs +53 -13
  24. package/scripts/claude/headless-info-smoke.mjs +45 -11
  25. package/scripts/docs/validate-links.mjs +20 -11
  26. package/scripts/github/build-adjacent-bundle.mjs +448 -0
  27. package/scripts/github/capture-review-threads.mjs +32 -14
  28. package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
  29. package/scripts/github/detect-checkpoint-evidence.mjs +123 -15
  30. package/scripts/github/detect-linked-issue-pr.mjs +22 -10
  31. package/scripts/github/manage-sub-issues.mjs +37 -16
  32. package/scripts/github/post-gate-findings.mjs +392 -0
  33. package/scripts/github/probe-copilot-review.mjs +24 -12
  34. package/scripts/github/ready-for-review.mjs +17 -8
  35. package/scripts/github/reconcile-draft-gate.mjs +24 -12
  36. package/scripts/github/reply-resolve-review-threads.mjs +34 -15
  37. package/scripts/github/request-copilot-review.mjs +97 -19
  38. package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
  39. package/scripts/github/stage-reviewer-draft.mjs +32 -14
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +646 -41
  41. package/scripts/github/verify-fresh-review-context.mjs +12 -1
  42. package/scripts/github/write-gate-context.mjs +634 -0
  43. package/scripts/github/write-gate-findings-log.mjs +42 -21
  44. package/scripts/loop/build-handoff-envelope.mjs +32 -14
  45. package/scripts/loop/conductor-monitor.mjs +25 -9
  46. package/scripts/loop/conductor.mjs +31 -12
  47. package/scripts/loop/copilot-pr-handoff.mjs +31 -14
  48. package/scripts/loop/debt-remediate.mjs +28 -11
  49. package/scripts/loop/detect-change-scope.mjs +36 -11
  50. package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
  51. package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
  52. package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
  53. package/scripts/loop/detect-internal-only-pr.mjs +31 -13
  54. package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
  55. package/scripts/loop/detect-pr-gate-coordination-state.mjs +56 -28
  56. package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
  57. package/scripts/loop/detect-stale-runner.mjs +32 -14
  58. package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
  59. package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
  60. package/scripts/loop/info.mjs +28 -10
  61. package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
  62. package/scripts/loop/inspect-run.mjs +35 -16
  63. package/scripts/loop/outer-loop.mjs +35 -16
  64. package/scripts/loop/pr-runner-coordination.mjs +31 -12
  65. package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
  66. package/scripts/loop/pre-flight-gate.mjs +25 -9
  67. package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
  68. package/scripts/loop/pre-push-main-guard.mjs +19 -5
  69. package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
  70. package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
  71. package/scripts/loop/run-conductor-cycle.mjs +23 -8
  72. package/scripts/loop/run-queue.mjs +87 -15
  73. package/scripts/loop/run-refinement-audit.mjs +44 -22
  74. package/scripts/loop/run-watch-cycle.mjs +28 -12
  75. package/scripts/loop/steer-loop.mjs +122 -62
  76. package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
  77. package/scripts/projects/add-queue-item.mjs +60 -43
  78. package/scripts/projects/archive-done-items.mjs +506 -0
  79. package/scripts/projects/ensure-queue-board.mjs +65 -64
  80. package/scripts/projects/list-queue-items.mjs +57 -56
  81. package/scripts/projects/move-queue-item.mjs +123 -124
  82. package/scripts/projects/reorder-queue-item.mjs +384 -108
  83. package/scripts/projects/sync-item-status.mjs +198 -0
  84. package/scripts/refine/_refine-helpers.mjs +21 -9
  85. package/scripts/refine/verify.mjs +31 -13
  86. package/skills/copilot-pr-followup/SKILL.md +57 -3
  87. package/skills/dev-loop/SKILL.md +9 -5
  88. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
  89. package/skills/dev-loop/scripts/phase-files.mjs +2 -2
  90. package/skills/docs/anti-patterns.md +2 -0
  91. package/skills/docs/copilot-loop-operations.md +2 -2
  92. package/skills/local-implementation/SKILL.md +17 -3
package/cli/index.mjs CHANGED
@@ -22,6 +22,7 @@ const SUBCOMMAND_ROUTES = {
22
22
  "upsert-verdict": "scripts/github/upsert-checkpoint-verdict.mjs",
23
23
  "detect-evidence": "scripts/github/detect-checkpoint-evidence.mjs",
24
24
  "write-findings-log": "scripts/github/write-gate-findings-log.mjs",
25
+ "post-findings": "scripts/github/post-gate-findings.mjs",
25
26
  "request-copilot": "scripts/github/request-copilot-review.mjs",
26
27
  "probe-copilot": "scripts/github/probe-copilot-review.mjs",
27
28
  "capture-threads": "scripts/github/capture-review-threads.mjs",
@@ -43,7 +44,7 @@ const SUBCOMMAND_ROUTES = {
43
44
  "debt-remediate": "scripts/loop/debt-remediate.mjs",
44
45
  },
45
46
  pr: {
46
- "create-draft": "scripts/github/create-draft-pr.mjs",
47
+ create: "scripts/github/create-pr.mjs",
47
48
  "ready-for-review": "scripts/github/ready-for-review.mjs",
48
49
  "reconcile-draft": "scripts/github/reconcile-draft-gate.mjs",
49
50
  },
@@ -52,6 +53,8 @@ const SUBCOMMAND_ROUTES = {
52
53
  add: "scripts/projects/add-queue-item.mjs",
53
54
  move: "scripts/projects/move-queue-item.mjs",
54
55
  reorder: "scripts/projects/reorder-queue-item.mjs",
56
+ "archive-done": "scripts/projects/archive-done-items.mjs",
57
+ "sync-status": "scripts/projects/sync-item-status.mjs",
55
58
  ensure: "scripts/projects/ensure-queue-board.mjs",
56
59
  },
57
60
  queue: {
@@ -66,6 +69,22 @@ const SUBCOMMAND_ROUTES = {
66
69
  },
67
70
  };
68
71
 
72
+ // Back-compat subcommand aliases: { category: { oldName: { canonical, notice } } }.
73
+ // Aliases keep existing callers working while emitting a one-line deprecation
74
+ // notice to stderr so they migrate to the canonical subcommand.
75
+ const SUBCOMMAND_ALIASES = {
76
+ pr: {
77
+ "create-draft": {
78
+ canonical: "create",
79
+ notice: "[dev-loops] `pr create-draft` is deprecated; use `pr create` (always draft, self-assigned by default).",
80
+ },
81
+ },
82
+ };
83
+
84
+ function resolveSubcommandAlias(category, subcommand) {
85
+ return SUBCOMMAND_ALIASES[category]?.[subcommand] ?? null;
86
+ }
87
+
69
88
  const TOP_LEVEL_COMMANDS = new Set(["help", "status", "doctor", "gates", "hide"]);
70
89
 
71
90
  const HELP_CATEGORY_LABELS = {
@@ -84,6 +103,7 @@ const SUBCOMMAND_DESCRIPTIONS = {
84
103
  "upsert-verdict": "Post/update gate review comment",
85
104
  "detect-evidence": "Check merge preconditions",
86
105
  "write-findings-log": "Write disposition ledger",
106
+ "post-findings": "Post gate fan-out findings comment",
87
107
  "request-copilot": "Request Copilot review",
88
108
  "probe-copilot": "Poll for Copilot review activity",
89
109
  "capture-threads": "Capture review threads",
@@ -105,7 +125,7 @@ const SUBCOMMAND_DESCRIPTIONS = {
105
125
  "debt-remediate": "File debt remediation issues",
106
126
  },
107
127
  pr: {
108
- "create-draft": "Create draft PR",
128
+ create: "Create PR (always draft, self-assigned by default)",
109
129
  "ready-for-review": "Mark PR ready for review",
110
130
  "reconcile-draft": "Reconcile non-draft PR",
111
131
  },
@@ -113,7 +133,9 @@ const SUBCOMMAND_DESCRIPTIONS = {
113
133
  list: "List queue board items",
114
134
  add: "Add issue/PR to queue board",
115
135
  move: "Move queue item between Status columns",
116
- reorder: "Reorder queue board items",
136
+ reorder: "Reorder items (move-to-top/move-after/order, --dry-run)",
137
+ "archive-done": "Archive closed Done items older than a duration",
138
+ "sync-status": "Sync a queued issue/PR's board Status column (best-effort)",
117
139
  ensure: "Create/repair queue board bootstrap surface",
118
140
  },
119
141
  queue: {
@@ -273,16 +295,19 @@ function resolveSubcommandRoute(args) {
273
295
  return { error: `Missing subcommand for '${category}'. Available: ${subs}` };
274
296
  }
275
297
 
276
- const subcommand = args[1];
298
+ const requestedSubcommand = args[1];
299
+ const alias = resolveSubcommandAlias(category, requestedSubcommand);
300
+ const subcommand = alias ? alias.canonical : requestedSubcommand;
277
301
  const scriptPath = routes[subcommand];
278
302
  if (!scriptPath) {
279
303
  const subs = Object.keys(routes).join(", ");
280
- return { error: `Unknown subcommand '${subcommand}' for '${category}'. Available: ${subs}` };
304
+ return { error: `Unknown subcommand '${requestedSubcommand}' for '${category}'. Available: ${subs}` };
281
305
  }
282
306
 
283
307
  return {
284
308
  scriptPath: path.resolve(REPO_ROOT, scriptPath),
285
309
  forwardedArgs: args.slice(2),
310
+ ...(alias ? { deprecationNotice: alias.notice } : {}),
286
311
  };
287
312
  }
288
313
 
@@ -311,9 +336,17 @@ function parseTopLevelCommand(argv) {
311
336
  }
312
337
  // Check if any remaining arg is --help — delegate to script
313
338
  if (args.slice(1).some((a) => a === "--help" || a === "-h")) {
314
- const scriptPath = routes[sub];
339
+ const alias = resolveSubcommandAlias(cmd, sub);
340
+ const resolvedSub = alias ? alias.canonical : sub;
341
+ const scriptPath = routes[resolvedSub];
315
342
  if (!scriptPath) return { kind: "category_help", category: cmd };
316
- return { kind: "subcommand_help", scriptPath: path.resolve(REPO_ROOT, scriptPath) };
343
+ return {
344
+ kind: "subcommand_help",
345
+ scriptPath: path.resolve(REPO_ROOT, scriptPath),
346
+ // Surface the deprecation notice on the --help fast-path too, so a
347
+ // deprecated alias signals migration in help mode (not only on dispatch).
348
+ ...(alias ? { deprecationNotice: alias.notice } : {}),
349
+ };
317
350
  }
318
351
  const route = resolveSubcommandRoute(args);
319
352
  if (route) return { kind: "subcommand", ...route };
@@ -343,6 +376,7 @@ export async function runCli({
343
376
  return 0;
344
377
  }
345
378
  case "subcommand_help": {
379
+ if (fromTop.deprecationNotice) { writeLines(stderr, [fromTop.deprecationNotice]); }
346
380
  const result = spawnSync("node", [fromTop.scriptPath, "--help"], {
347
381
  cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
348
382
  });
@@ -381,6 +415,7 @@ export async function runCli({
381
415
  }
382
416
  case "subcommand": {
383
417
  if (fromTop.error) { writeLines(stderr, [fromTop.error]); return 1; }
418
+ if (fromTop.deprecationNotice) { writeLines(stderr, [fromTop.deprecationNotice]); }
384
419
  const scriptArgs = fromTop.forwardedArgs || [];
385
420
  const result = spawnSync("node", [fromTop.scriptPath, ...scriptArgs], {
386
421
  cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
package/package.json CHANGED
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "mermaid": "11.15.0",
71
- "@dev-loops/core": "^0.2.6"
71
+ "@dev-loops/core": "^0.3.0"
72
72
  },
73
73
  "repository": {
74
74
  "type": "git",
@@ -78,7 +78,7 @@
78
78
  "url": "https://github.com/mfittko/dev-loops/issues"
79
79
  },
80
80
  "homepage": "https://github.com/mfittko/dev-loops#readme",
81
- "version": "0.2.6",
81
+ "version": "0.3.0",
82
82
  "files": [
83
83
  "cli/",
84
84
  "lib/",
package/scripts/README.md CHANGED
@@ -68,16 +68,17 @@ Failure behavior:
68
68
  - malformed arguments, invalid JSON, and `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
69
69
  - live capture is only allowed when both `--repo` and `--pr` are present
70
70
 
71
- ### `scripts/github/create-draft-pr.mjs`
71
+ ### `scripts/github/create-pr.mjs`
72
72
 
73
- Thin wrapper around `gh pr create` for draft-first PR creation.
73
+ Canonical wrapper around `gh pr create`. Every PR opened through this tool is ALWAYS a draft and is always assigned — self-assigned by default (`--assignee @me` when none is given), while still honoring an explicit `--assignee <login>`. Never call raw `gh pr create` to open a PR. (`dev-loops pr create` dispatches here; the legacy `dev-loops pr create-draft` subcommand still works as a deprecated alias.)
74
74
 
75
75
  Usage:
76
- - `node scripts/github/create-draft-pr.mjs [gh pr create args...]`
77
- - `node <resolved-skill-scripts>/github/create-draft-pr.mjs [gh pr create args...]`
76
+ - `node scripts/github/create-pr.mjs [gh pr create args...]`
77
+ - `node <resolved-skill-scripts>/github/create-pr.mjs [gh pr create args...]`
78
78
 
79
79
  Contract:
80
- - injects exactly one `--draft` when the caller did not already supply it
80
+ - injects exactly one `--draft` when the caller did not already supply it (draft is the only mode)
81
+ - self-assigns by default (defaults `--assignee @me` when no `--assignee` is provided); honors an explicit `--assignee <login>` when one is given
81
82
  - rejects `--ready` before invoking `gh`; use `gh pr ready` later after draft-gate approval
82
83
  - forwards every other argument to `gh pr create` unchanged and in order
83
84
  - preserves the underlying `gh pr create` stdout, stderr, and exit code without wrapping success output
@@ -1,6 +1,8 @@
1
1
  // Re-export from shared library (Phase 2, issue #548)
2
2
  export {
3
+ parseCliTokens,
3
4
  requireOptionValue,
5
+ requireTokenValue,
4
6
  parsePositiveInteger,
5
7
  parseNonNegativeInteger,
6
8
  parsePrNumber,
@@ -19,6 +19,7 @@ export {
19
19
  normalizeTimestamp,
20
20
  parseGateReviewCommentBody,
21
21
  parseGateReviewCommentMarkerBody,
22
+ resolveDraftGateRoundResetMs,
22
23
  summarizeCopilotReviews,
23
24
  summarizeGateReviewCommentMarkers,
24
25
  summarizeGateReviewComments,
@@ -24,6 +24,16 @@ import { transformAgent, transformSkill, stripPiOnlyBlocks } from "@dev-loops/co
24
24
  export function collectGeneratedAssets({ repoRoot = process.cwd() } = {}) {
25
25
  const assets = [];
26
26
 
27
+ // The dev-loops package version pins the Claude `npx dev-loops@<version>` CLI invocation so the
28
+ // generated plugin tree cannot drift against the published version (#833). Read it once here.
29
+ // Falls back to `latest` when no repo-root package.json is present (e.g. a consumer/fixture tree
30
+ // that mirrors only agents/skills).
31
+ let version = "latest";
32
+ const pkgPath = path.join(repoRoot, "package.json");
33
+ if (fs.existsSync(pkgPath)) {
34
+ version = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version ?? "latest";
35
+ }
36
+
27
37
  const agentsDir = path.join(repoRoot, "agents");
28
38
  if (fs.existsSync(agentsDir)) {
29
39
  for (const entry of fs.readdirSync(agentsDir).sort()) {
@@ -31,7 +41,7 @@ export function collectGeneratedAssets({ repoRoot = process.cwd() } = {}) {
31
41
  const source = `agents/${entry}`;
32
42
  const raw = fs.readFileSync(path.join(repoRoot, source), "utf8");
33
43
  const base = entry.slice(0, -".agent.md".length);
34
- assets.push({ target: `.claude/agents/${base}.md`, content: transformAgent({ source, raw }) });
44
+ assets.push({ target: `.claude/agents/${base}.md`, content: transformAgent({ source, raw, version }) });
35
45
  }
36
46
  }
37
47
 
@@ -43,7 +53,7 @@ export function collectGeneratedAssets({ repoRoot = process.cwd() } = {}) {
43
53
  const abs = path.join(repoRoot, source);
44
54
  if (!fs.existsSync(abs)) continue; // e.g. skills/docs/ holds shared docs, not a SKILL.md
45
55
  const raw = fs.readFileSync(abs, "utf8");
46
- assets.push({ target: `.claude/skills/${entry.name}/SKILL.md`, content: transformSkill({ source, raw }) });
56
+ assets.push({ target: `.claude/skills/${entry.name}/SKILL.md`, content: transformSkill({ source, raw, version }) });
47
57
  }
48
58
  }
49
59
 
@@ -20,6 +20,7 @@
20
20
  import { spawnSync } from "node:child_process";
21
21
  import path from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
+ import { parseArgs } from "node:util";
23
24
 
24
25
  import { ensureRunId } from "@dev-loops/core/loop/run-context";
25
26
  import {
@@ -28,22 +29,61 @@ import {
28
29
  DEFAULT_CLAUDE_BIN,
29
30
  } from "@dev-loops/core/claude/headless-entry";
30
31
 
31
- function parseArgs(argv) {
32
+ function parseCliArgs(argv) {
32
33
  const opts = { dryRun: false, claudeBin: DEFAULT_CLAUDE_BIN };
33
- const requireValue = (name, i) => {
34
- const v = argv[i + 1];
35
- if (v === undefined || v.startsWith("--")) throw new Error(`${name} requires a value`);
34
+ const requireValue = (name, token) => {
35
+ const v = token.value;
36
+ if (typeof v !== "string" || v.length === 0 || v.startsWith("--")) {
37
+ throw new Error(`${name} requires a value`);
38
+ }
36
39
  return v;
37
40
  };
38
- for (let i = 0; i < argv.length; i++) {
39
- const a = argv[i];
40
- if (a === "--dry-run") opts.dryRun = true;
41
- else if (a === "--issue") { opts.issue = requireValue(a, i); i++; }
42
- else if (a === "--pr") { opts.pr = requireValue(a, i); i++; }
43
- else if (a === "--claude-bin") { opts.claudeBin = requireValue(a, i); i++; }
44
- else if (a === "--prompt") { opts.prompt = requireValue(a, i); i++; }
45
- else throw new Error(`unknown argument: ${a}`);
41
+
42
+ const { tokens } = parseArgs({
43
+ args: [...argv],
44
+ options: {
45
+ "dry-run": { type: "boolean" },
46
+ issue: { type: "string" },
47
+ pr: { type: "string" },
48
+ "claude-bin": { type: "string" },
49
+ prompt: { type: "string" },
50
+ },
51
+ allowPositionals: true,
52
+ strict: false,
53
+ tokens: true,
54
+ });
55
+
56
+ for (const token of tokens) {
57
+ if (token.kind === "positional") {
58
+ throw new Error(`unknown argument: ${token.value}`);
59
+ }
60
+ if (token.kind !== "option") {
61
+ continue;
62
+ }
63
+ switch (token.name) {
64
+ case "dry-run":
65
+ if (token.value !== undefined) {
66
+ throw new Error(`unknown argument: ${token.rawName}=${token.value}`);
67
+ }
68
+ opts.dryRun = true;
69
+ break;
70
+ case "issue":
71
+ opts.issue = requireValue("--issue", token);
72
+ break;
73
+ case "pr":
74
+ opts.pr = requireValue("--pr", token);
75
+ break;
76
+ case "claude-bin":
77
+ opts.claudeBin = requireValue("--claude-bin", token);
78
+ break;
79
+ case "prompt":
80
+ opts.prompt = requireValue("--prompt", token);
81
+ break;
82
+ default:
83
+ throw new Error(`unknown argument: ${token.rawName}`);
84
+ }
46
85
  }
86
+
47
87
  if (opts.issue != null && opts.pr != null) {
48
88
  throw new Error("--issue and --pr are mutually exclusive");
49
89
  }
@@ -53,7 +93,7 @@ function parseArgs(argv) {
53
93
  function main(argv) {
54
94
  let opts;
55
95
  try {
56
- opts = parseArgs(argv);
96
+ opts = parseCliArgs(argv);
57
97
  } catch (error) {
58
98
  process.stderr.write(JSON.stringify({ ok: false, error: error.message }) + "\n");
59
99
  return 1;
@@ -18,21 +18,55 @@
18
18
  import { spawnSync } from "node:child_process";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
+ import { parseArgs } from "node:util";
21
22
 
22
- function parseArgs(argv) {
23
+ function parseCliArgs(argv) {
23
24
  const opts = { loopInfo: false };
24
- const requireValue = (name, i) => {
25
- const v = argv[i + 1];
26
- if (v === undefined || v.startsWith("--")) throw new Error(`${name} requires a value`);
25
+ const requireValue = (name, token) => {
26
+ const v = token.value;
27
+ if (typeof v !== "string" || v.length === 0 || v.startsWith("--")) {
28
+ throw new Error(`${name} requires a value`);
29
+ }
27
30
  return v;
28
31
  };
29
- for (let i = 0; i < argv.length; i++) {
30
- const a = argv[i];
31
- if (a === "--loop-info") opts.loopInfo = true;
32
- else if (a === "--issue") { opts.issue = requireValue(a, i); i++; }
33
- else if (a === "--pr") { opts.pr = requireValue(a, i); i++; }
34
- else throw new Error(`unknown argument: ${a}`);
32
+
33
+ const { tokens } = parseArgs({
34
+ args: [...argv],
35
+ options: {
36
+ "loop-info": { type: "boolean" },
37
+ issue: { type: "string" },
38
+ pr: { type: "string" },
39
+ },
40
+ allowPositionals: true,
41
+ strict: false,
42
+ tokens: true,
43
+ });
44
+
45
+ for (const token of tokens) {
46
+ if (token.kind === "positional") {
47
+ throw new Error(`unknown argument: ${token.value}`);
48
+ }
49
+ if (token.kind !== "option") {
50
+ continue;
51
+ }
52
+ switch (token.name) {
53
+ case "loop-info":
54
+ if (token.value !== undefined) {
55
+ throw new Error(`unknown argument: ${token.rawName}=${token.value}`);
56
+ }
57
+ opts.loopInfo = true;
58
+ break;
59
+ case "issue":
60
+ opts.issue = requireValue("--issue", token);
61
+ break;
62
+ case "pr":
63
+ opts.pr = requireValue("--pr", token);
64
+ break;
65
+ default:
66
+ throw new Error(`unknown argument: ${token.rawName}`);
67
+ }
35
68
  }
69
+
36
70
  if (opts.issue != null && opts.pr != null) {
37
71
  throw new Error("--issue and --pr are mutually exclusive");
38
72
  }
@@ -46,7 +80,7 @@ function runCli(cliEntry, repoRoot, args) {
46
80
  function main(argv) {
47
81
  let opts;
48
82
  try {
49
- opts = parseArgs(argv);
83
+ opts = parseCliArgs(argv);
50
84
  } catch (error) {
51
85
  process.stderr.write(JSON.stringify({ ok: false, error: error.message }) + "\n");
52
86
  return 1;
@@ -2,9 +2,10 @@
2
2
  import { lstat, readdir, readFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { parseArgs } from "node:util";
5
6
 
6
7
  import { isDirectCliRun } from "../_core-helpers.mjs";
7
- import { requireOptionValue } from "../_cli-primitives.mjs";
8
+ import { requireTokenValue } from "../_cli-primitives.mjs";
8
9
 
9
10
  const DEFAULT_SCAN_PATHS = Object.freeze([
10
11
  "README.md",
@@ -52,26 +53,34 @@ function parseError(message) {
52
53
  }
53
54
 
54
55
  export function parseValidateLinksCliArgs(argv) {
55
- const args = [...argv];
56
+ const { tokens } = parseArgs({
57
+ args: [...argv],
58
+ options: { help: { type: "boolean", short: "h" }, root: { type: "string" } },
59
+ allowPositionals: true,
60
+ strict: false,
61
+ tokens: true,
62
+ });
56
63
  const options = {
57
64
  help: false,
58
65
  repoRoot: resolveDefaultRepoRoot(),
59
66
  };
60
67
 
61
- while (args.length > 0) {
62
- const token = args.shift();
63
-
64
- if (token === "--help" || token === "-h") {
68
+ for (const token of tokens) {
69
+ if (token.kind === "positional") {
70
+ throw parseError(`Unknown argument: ${token.value}`);
71
+ }
72
+ if (token.kind !== "option") {
73
+ continue;
74
+ }
75
+ if (token.name === "help") {
65
76
  options.help = true;
66
77
  return options;
67
78
  }
68
-
69
- if (token === "--root") {
70
- options.repoRoot = path.resolve(requireOptionValue(args, "--root"));
79
+ if (token.name === "root") {
80
+ options.repoRoot = path.resolve(requireTokenValue(token));
71
81
  continue;
72
82
  }
73
-
74
- throw parseError(`Unknown argument: ${token}`);
83
+ throw parseError(`Unknown argument: ${token.rawName}`);
75
84
  }
76
85
 
77
86
  return options;