dev-loops 0.2.7 → 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/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +1 -1
- package/.claude/agents/developer.md +1 -0
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/review.md +30 -0
- package/.claude/hooks/_run-context.mjs +9 -16
- package/.claude/skills/copilot-pr-followup/SKILL.md +62 -6
- package/.claude/skills/dev-loop/SKILL.md +6 -6
- package/.claude/skills/docs/anti-patterns.md +2 -0
- package/.claude/skills/docs/copilot-loop-operations.md +3 -3
- package/.claude/skills/docs/issue-intake-procedure.md +4 -0
- package/.claude/skills/docs/merge-preconditions.md +65 -1
- package/.claude/skills/docs/stop-conditions.md +1 -0
- package/.claude/skills/docs/tracker-first-loop-state.md +6 -6
- package/.claude/skills/local-implementation/SKILL.md +25 -5
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +69 -0
- package/README.md +8 -2
- package/agents/developer.agent.md +1 -0
- package/agents/fixer.agent.md +1 -0
- package/agents/review.agent.md +30 -0
- package/cli/index.mjs +60 -8
- package/extension/README.md +1 -1
- package/package.json +3 -3
- package/scripts/README.md +8 -7
- package/scripts/_core-helpers.mjs +1 -0
- package/scripts/claude/headless-dev-loop.mjs +53 -13
- package/scripts/claude/headless-info-smoke.mjs +45 -11
- package/scripts/github/build-adjacent-bundle.mjs +448 -0
- package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
- package/scripts/github/detect-checkpoint-evidence.mjs +95 -4
- package/scripts/github/offer-human-handoff.mjs +147 -0
- package/scripts/github/post-gate-findings.mjs +392 -0
- package/scripts/github/probe-ci-status.mjs +468 -0
- package/scripts/github/reconcile-draft-gate.mjs +2 -2
- package/scripts/github/request-copilot-review.mjs +72 -11
- package/scripts/github/resolve-handoff-candidates.mjs +412 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +599 -17
- package/scripts/github/verify-fresh-review-context.mjs +12 -1
- package/scripts/github/write-gate-context.mjs +634 -0
- package/scripts/github/write-gate-findings-log.mjs +1 -1
- package/scripts/loop/_stale-runner-detection.mjs +1 -1
- package/scripts/loop/_worktree-path.mjs +27 -0
- package/scripts/loop/cleanup-worktree.mjs +175 -0
- package/scripts/loop/copilot-pr-handoff.mjs +1 -1
- package/scripts/loop/detect-change-scope.mjs +36 -11
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +30 -18
- package/scripts/loop/detect-stale-runner.mjs +3 -4
- package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
- package/scripts/loop/ensure-worktree.mjs +219 -0
- package/scripts/loop/outer-loop.mjs +1 -1
- package/scripts/loop/pr-runner-coordination.mjs +1 -1
- package/scripts/loop/pre-flight-gate.mjs +10 -7
- package/scripts/loop/pre-push-main-guard.mjs +4 -4
- package/scripts/loop/provision-worktree.mjs +243 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +5 -5
- package/scripts/loop/run-queue.mjs +112 -16
- package/scripts/loop/run-watch-cycle.mjs +75 -22
- package/scripts/projects/add-queue-item.mjs +80 -48
- package/scripts/projects/archive-done-items.mjs +136 -39
- package/scripts/projects/ensure-queue-board.mjs +67 -65
- package/scripts/projects/list-queue-items.mjs +59 -57
- package/scripts/projects/move-queue-item.mjs +125 -125
- package/scripts/projects/reorder-queue-item.mjs +67 -48
- package/scripts/projects/sync-item-status.mjs +199 -0
- package/skills/copilot-pr-followup/SKILL.md +62 -6
- package/skills/dev-loop/SKILL.md +2 -2
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
- package/skills/dev-loop/scripts/phase-files.mjs +2 -2
- package/skills/docs/anti-patterns.md +2 -0
- package/skills/docs/copilot-loop-operations.md +3 -3
- package/skills/docs/issue-intake-procedure.md +4 -0
- package/skills/docs/merge-preconditions.md +65 -1
- package/skills/docs/stop-conditions.md +1 -0
- package/skills/docs/tracker-first-loop-state.md +6 -6
- package/skills/local-implementation/SKILL.md +25 -5
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
2
5
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
6
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
4
8
|
|
|
5
|
-
const USAGE = `Usage: dev-loops
|
|
9
|
+
const USAGE = `Usage: dev-loops queue archive-done --repo <owner/name> [--project <number|id>] [--older-than <duration>] [--dry-run]
|
|
10
|
+
(dev-loops project archive-done … is a back-compat alias)
|
|
6
11
|
|
|
7
12
|
Archive GitHub Projects V2 items whose issue/PR has been closed for at least the
|
|
8
13
|
given duration. Operator-triggered (no webhooks). Uses archiveProjectV2Item.
|
|
9
14
|
|
|
10
15
|
Options:
|
|
11
16
|
--repo <owner/name> Required. Repository to scope the project search.
|
|
12
|
-
--project <number|id>
|
|
17
|
+
--project <number|id> Project number (integer) or node ID. When omitted,
|
|
18
|
+
resolved from .devloops queue.projectNumber /
|
|
19
|
+
queue.boardTitle.
|
|
13
20
|
--older-than <duration> Closed-for threshold. Format: <n><unit> where unit is
|
|
14
|
-
h (hours), d (days), or w (weeks). Default
|
|
21
|
+
h (hours), d (days), or w (weeks). Default resolves
|
|
22
|
+
from .devloops queue.archiveOlderThanDays, else 7d.
|
|
15
23
|
--dry-run Print the intended archive mutation(s) without executing.
|
|
16
24
|
--help, -h Show this help.
|
|
17
25
|
|
|
@@ -27,36 +35,61 @@ Exit codes:
|
|
|
27
35
|
3 — project not found
|
|
28
36
|
`.trim();
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
function parseCliArgs(argv) {
|
|
39
|
+
const requireValue = (token, message, code) => {
|
|
40
|
+
const v = token.value;
|
|
41
|
+
if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
|
|
42
|
+
throw Object.assign(new Error(message), { code });
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
};
|
|
31
46
|
|
|
32
|
-
function parseArgs(argv) {
|
|
33
47
|
const args = {};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
const { tokens } = parseArgs({
|
|
49
|
+
args: [...argv],
|
|
50
|
+
options: {
|
|
51
|
+
repo: { type: "string" },
|
|
52
|
+
project: { type: "string" },
|
|
53
|
+
"older-than": { type: "string" },
|
|
54
|
+
"dry-run": { type: "boolean" },
|
|
55
|
+
help: { type: "boolean", short: "h" },
|
|
56
|
+
},
|
|
57
|
+
allowPositionals: true,
|
|
58
|
+
strict: false,
|
|
59
|
+
tokens: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
if (token.kind === "positional") {
|
|
64
|
+
throw Object.assign(new Error(`Unexpected argument: ${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
38
65
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
if (token.kind !== "option") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
switch (token.name) {
|
|
70
|
+
case "help":
|
|
71
|
+
if (token.value !== undefined) {
|
|
72
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
73
|
+
}
|
|
74
|
+
args.help = true;
|
|
75
|
+
break;
|
|
76
|
+
case "repo":
|
|
77
|
+
args.repo = requireValue(token, "--repo requires a value (owner/name)", "INVALID_REPO");
|
|
78
|
+
break;
|
|
79
|
+
case "project":
|
|
80
|
+
args.project = requireValue(token, "--project requires a value (number or node ID)", "INVALID_PROJECT");
|
|
81
|
+
break;
|
|
82
|
+
case "older-than":
|
|
83
|
+
args.olderThan = requireValue(token, "--older-than requires a value (e.g. 30d)", "INVALID_DURATION");
|
|
84
|
+
break;
|
|
85
|
+
case "dry-run":
|
|
86
|
+
if (token.value !== undefined) {
|
|
87
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
88
|
+
}
|
|
89
|
+
args.dryRun = true;
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
60
93
|
}
|
|
61
94
|
}
|
|
62
95
|
return args;
|
|
@@ -121,6 +154,37 @@ function parseDuration(raw) {
|
|
|
121
154
|
return n * UNIT_MS[m[2]];
|
|
122
155
|
}
|
|
123
156
|
|
|
157
|
+
// ── Settings fallback ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
// Read .devloops (and extension variants) queue settings, mirroring the
|
|
160
|
+
// resolution used by ensure-queue-board.mjs. Returns { project }, { title },
|
|
161
|
+
// and/or { olderThanDays } when configured; never throws on a missing/bad file.
|
|
162
|
+
function resolveSettings(cwd) {
|
|
163
|
+
const basePath = path.join(cwd, ".devloops");
|
|
164
|
+
const extensions = ["", ".yaml", ".yml", ".json"];
|
|
165
|
+
for (const ext of extensions) {
|
|
166
|
+
try {
|
|
167
|
+
const raw = readFileSync(basePath + ext, "utf-8");
|
|
168
|
+
const settings = ext === ".json" ? JSON.parse(raw) : parseYaml(raw);
|
|
169
|
+
const queue = settings?.queue;
|
|
170
|
+
if (!queue) return null;
|
|
171
|
+
const out = {};
|
|
172
|
+
if (typeof queue.projectNumber === "number" && Number.isInteger(queue.projectNumber) && queue.projectNumber > 0) {
|
|
173
|
+
out.project = queue.projectNumber;
|
|
174
|
+
} else if (typeof queue.boardTitle === "string" && queue.boardTitle.trim().length > 0) {
|
|
175
|
+
out.title = queue.boardTitle.trim();
|
|
176
|
+
}
|
|
177
|
+
if (typeof queue.archiveOlderThanDays === "number" && Number.isInteger(queue.archiveOlderThanDays) && queue.archiveOlderThanDays > 0) {
|
|
178
|
+
out.olderThanDays = queue.archiveOlderThanDays;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
} catch {
|
|
182
|
+
// extension not present or unparseable — try next
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
124
188
|
// ── API helpers ──────────────────────────────────────────────────────────
|
|
125
189
|
|
|
126
190
|
async function ghGraphql(query, vars, env, runChild = _runChild) {
|
|
@@ -289,6 +353,9 @@ function normalizeItem(node) {
|
|
|
289
353
|
function selectArchivable(items, { now, olderThanMs }) {
|
|
290
354
|
return items.filter((it) => {
|
|
291
355
|
if (it.isArchived) return false;
|
|
356
|
+
// Only archive items in the Done column — a closed issue/PR parked in
|
|
357
|
+
// another column (Backlog/Next Up/In Progress) must be left untouched.
|
|
358
|
+
if (it.status !== "Done") return false;
|
|
292
359
|
const c = it.content;
|
|
293
360
|
if (!c || !c.closed || !c.closedAt) return false;
|
|
294
361
|
const closedAtMs = Date.parse(c.closedAt);
|
|
@@ -312,19 +379,36 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
312
379
|
const child = runChild ?? _runChild;
|
|
313
380
|
const repo = validateRepo(args.repo);
|
|
314
381
|
const [owner] = repo.split("/");
|
|
315
|
-
|
|
316
|
-
|
|
382
|
+
// Board: explicit --project ref wins; otherwise resolve by board title from
|
|
383
|
+
// .devloops (passed in as args.projectTitle by runCli). Fail closed if neither.
|
|
384
|
+
const hasProjectRef = typeof args.project === "string" && args.project.trim().length > 0;
|
|
385
|
+
const projectRef = hasProjectRef ? parseProjectRef(args.project) : null;
|
|
386
|
+
const projectTitle = !hasProjectRef && typeof args.projectTitle === "string" && args.projectTitle.trim().length > 0
|
|
387
|
+
? args.projectTitle.trim()
|
|
388
|
+
: null;
|
|
389
|
+
if (!projectRef && !projectTitle) {
|
|
390
|
+
throw Object.assign(
|
|
391
|
+
new Error("--project is required (or configure queue.projectNumber / queue.boardTitle in .devloops)"),
|
|
392
|
+
{ code: "INVALID_PROJECT" },
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const olderThanRaw = args.olderThan ?? args.olderThanDefault ?? "7d";
|
|
317
396
|
const olderThanMs = parseDuration(olderThanRaw);
|
|
318
397
|
const now = args.now ?? Date.now();
|
|
319
398
|
|
|
320
399
|
const { kind: ownerKind } = await resolveOwner(owner, env, child);
|
|
321
400
|
const projects = await listAllProjects(owner, ownerKind, env, child);
|
|
322
|
-
const project = projectRef
|
|
323
|
-
?
|
|
324
|
-
|
|
401
|
+
const project = projectRef
|
|
402
|
+
? (projectRef.kind === "id"
|
|
403
|
+
? projects.find((p) => p.id === projectRef.value)
|
|
404
|
+
: projects.find((p) => p.number === projectRef.value))
|
|
405
|
+
: projects.find((p) => p.title === projectTitle);
|
|
325
406
|
if (!project) {
|
|
407
|
+
const desc = projectRef
|
|
408
|
+
? (projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`)
|
|
409
|
+
: `title "${projectTitle}"`;
|
|
326
410
|
throw Object.assign(
|
|
327
|
-
new Error(`Project ${
|
|
411
|
+
new Error(`Project ${desc} not found under owner "${owner}"`),
|
|
328
412
|
{ code: "PROJECT_NOT_FOUND" },
|
|
329
413
|
);
|
|
330
414
|
}
|
|
@@ -378,10 +462,10 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
378
462
|
|
|
379
463
|
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
380
464
|
|
|
381
|
-
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
465
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
|
|
382
466
|
let args;
|
|
383
467
|
try {
|
|
384
|
-
args =
|
|
468
|
+
args = parseCliArgs(argv);
|
|
385
469
|
} catch (err) {
|
|
386
470
|
stderr.write(`${formatCliError(err)}\n`);
|
|
387
471
|
process.exitCode = 1;
|
|
@@ -391,6 +475,19 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
391
475
|
stdout.write(USAGE);
|
|
392
476
|
return;
|
|
393
477
|
}
|
|
478
|
+
|
|
479
|
+
// Resolve board + threshold defaults from .devloops when the flags are absent.
|
|
480
|
+
// Precedence: explicit --project flag > queue.projectNumber/boardTitle.
|
|
481
|
+
// explicit --older-than flag > queue.archiveOlderThanDays > 7d.
|
|
482
|
+
const settings = resolveSettings(cwd);
|
|
483
|
+
if (args.project === undefined && settings) {
|
|
484
|
+
if (settings.project) args.project = String(settings.project);
|
|
485
|
+
else if (settings.title) args.projectTitle = settings.title;
|
|
486
|
+
}
|
|
487
|
+
if (args.olderThan === undefined && settings?.olderThanDays) {
|
|
488
|
+
args.olderThanDefault = `${settings.olderThanDays}d`;
|
|
489
|
+
}
|
|
490
|
+
|
|
394
491
|
try {
|
|
395
492
|
const result = await main(args, { env });
|
|
396
493
|
stdout.write(JSON.stringify(result) + "\n");
|
|
@@ -407,4 +504,4 @@ if (isDirectCliRun(import.meta.url)) {
|
|
|
407
504
|
});
|
|
408
505
|
}
|
|
409
506
|
|
|
410
|
-
export { main, parseDuration, selectArchivable };
|
|
507
|
+
export { main, parseCliArgs, parseDuration, selectArchivable, resolveSettings, runCli };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
4
5
|
import { parse as parseYaml } from "yaml";
|
|
5
6
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
6
7
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
7
8
|
|
|
8
|
-
const USAGE = `Usage: dev-loops
|
|
9
|
+
const USAGE = `Usage: dev-loops queue ensure --repo <owner/name> [--project <number>] [--title <title>] [--link-repo <owner/name>] [--repair-rename]
|
|
10
|
+
(dev-loops project ensure … is a back-compat alias)
|
|
9
11
|
|
|
10
12
|
--repair-rename Rename semantically equivalent Status columns to the standard names
|
|
11
13
|
(e.g. "Ready" -> "Next Up"). Without this flag the helper only
|
|
@@ -32,76 +34,76 @@ Exit codes:
|
|
|
32
34
|
3 — board schema/config mismatch (manual reconciliation needed)
|
|
33
35
|
`;
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
function parseCliArgs(argv) {
|
|
38
|
+
const parseError = (message) => Object.assign(new Error(message), { usage: USAGE });
|
|
39
|
+
const requireValue = (token, message) => {
|
|
40
|
+
const v = token.value;
|
|
41
|
+
if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
|
|
42
|
+
throw parseError(message);
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
};
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const args = {};
|
|
48
|
+
const { tokens } = parseArgs({
|
|
49
|
+
args: [...argv],
|
|
50
|
+
options: {
|
|
51
|
+
repo: { type: "string" },
|
|
52
|
+
project: { type: "string" },
|
|
53
|
+
title: { type: "string" },
|
|
54
|
+
"link-repo": { type: "string" },
|
|
55
|
+
"repair-rename": { type: "boolean" },
|
|
56
|
+
help: { type: "boolean", short: "h" },
|
|
57
|
+
},
|
|
58
|
+
allowPositionals: true,
|
|
59
|
+
strict: false,
|
|
60
|
+
tokens: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
for (const token of tokens) {
|
|
64
|
+
if (token.kind === "positional") {
|
|
65
|
+
throw parseError(`Unexpected argument: ${token.value}`);
|
|
48
66
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
args.project = num;
|
|
73
|
-
} else if (arg === "--title") {
|
|
74
|
-
if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
|
|
75
|
-
throw Object.assign(
|
|
76
|
-
new Error("--title requires a value"),
|
|
77
|
-
{ code: "INVALID_REPO", usage: USAGE },
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
args.title = argv[++i];
|
|
81
|
-
consumed.add(i);
|
|
82
|
-
} else if (arg === "--link-repo") {
|
|
83
|
-
if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
|
|
84
|
-
throw Object.assign(
|
|
85
|
-
new Error("--link-repo requires a value (owner/name)"),
|
|
86
|
-
{ code: "INVALID_REPO", usage: USAGE },
|
|
87
|
-
);
|
|
67
|
+
if (token.kind !== "option") {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
switch (token.name) {
|
|
71
|
+
case "help":
|
|
72
|
+
if (token.value !== undefined) {
|
|
73
|
+
throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
|
|
74
|
+
}
|
|
75
|
+
args.help = true;
|
|
76
|
+
break;
|
|
77
|
+
case "repo":
|
|
78
|
+
args.repo = requireValue(token, "--repo requires a value (owner/name)");
|
|
79
|
+
break;
|
|
80
|
+
case "project": {
|
|
81
|
+
const raw = requireValue(token, "--project requires a numeric value");
|
|
82
|
+
const num = Number(raw);
|
|
83
|
+
if (!Number.isInteger(num) || num <= 0) {
|
|
84
|
+
throw parseError(`--project must be a positive integer, got "${raw}"`);
|
|
85
|
+
}
|
|
86
|
+
args.project = num;
|
|
87
|
+
break;
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
case "title":
|
|
90
|
+
args.title = requireValue(token, "--title requires a value");
|
|
91
|
+
break;
|
|
92
|
+
case "link-repo":
|
|
93
|
+
args.linkRepo = requireValue(token, "--link-repo requires a value (owner/name)");
|
|
94
|
+
break;
|
|
95
|
+
case "repair-rename":
|
|
96
|
+
if (token.value !== undefined) {
|
|
97
|
+
throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
|
|
98
|
+
}
|
|
99
|
+
args.repairRename = true;
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
return args;
|
|
103
106
|
}
|
|
104
|
-
|
|
105
107
|
// ── Validation ───────────────────────────────────────────────────────────
|
|
106
108
|
|
|
107
109
|
// GitHub slug rules: owner 1-39 chars (alnum/dash, no leading/trailing dash,
|
|
@@ -798,7 +800,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
798
800
|
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
|
|
799
801
|
let args;
|
|
800
802
|
try {
|
|
801
|
-
args =
|
|
803
|
+
args = parseCliArgs(argv);
|
|
802
804
|
} catch (err) {
|
|
803
805
|
stderr.write(`${formatCliError(err)}\n`);
|
|
804
806
|
process.exitCode = 1;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
4
5
|
|
|
5
|
-
const USAGE = `Usage: dev-loops
|
|
6
|
+
const USAGE = `Usage: dev-loops queue list --repo <owner/name> --project <number|id> [--column <name>] [--limit <n>]
|
|
7
|
+
(dev-loops project list … is a back-compat alias)
|
|
6
8
|
|
|
7
9
|
List GitHub Projects V2 items filtered by Status column, ordered by position
|
|
8
10
|
ascending. Returns machine-readable JSON.
|
|
@@ -24,69 +26,69 @@ Exit codes:
|
|
|
24
26
|
3 — project, field, or column not found
|
|
25
27
|
`.trim();
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
function parseCliArgs(argv) {
|
|
30
|
+
const parseError = (message) => Object.assign(new Error(message), { usage: USAGE });
|
|
31
|
+
const requireValue = (token, message) => {
|
|
32
|
+
const v = token.value;
|
|
33
|
+
if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
|
|
34
|
+
throw parseError(message);
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
37
|
+
};
|
|
28
38
|
|
|
29
|
-
function parseArgs(argv) {
|
|
30
39
|
const args = {};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const { tokens } = parseArgs({
|
|
41
|
+
args: [...argv],
|
|
42
|
+
options: {
|
|
43
|
+
repo: { type: "string" },
|
|
44
|
+
project: { type: "string" },
|
|
45
|
+
column: { type: "string" },
|
|
46
|
+
limit: { type: "string" },
|
|
47
|
+
help: { type: "boolean", short: "h" },
|
|
48
|
+
},
|
|
49
|
+
allowPositionals: true,
|
|
50
|
+
strict: false,
|
|
51
|
+
tokens: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
for (const token of tokens) {
|
|
55
|
+
if (token.kind === "positional") {
|
|
56
|
+
throw parseError(`Unexpected argument: ${token.value}`);
|
|
38
57
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
new Error("--limit requires a positive integer"),
|
|
67
|
-
{ code: "INVALID_ARGS", usage: USAGE },
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
const val = Number(argv[++i]);
|
|
71
|
-
if (!Number.isInteger(val) || val < 1) {
|
|
72
|
-
throw Object.assign(
|
|
73
|
-
new Error(`--limit must be a positive integer, got "${argv[i]}"`),
|
|
74
|
-
{ code: "INVALID_ARGS", usage: USAGE },
|
|
75
|
-
);
|
|
58
|
+
if (token.kind !== "option") {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
switch (token.name) {
|
|
62
|
+
case "help":
|
|
63
|
+
if (token.value !== undefined) {
|
|
64
|
+
throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
|
|
65
|
+
}
|
|
66
|
+
args.help = true;
|
|
67
|
+
break;
|
|
68
|
+
case "repo":
|
|
69
|
+
args.repo = requireValue(token, "--repo requires a value (owner/name)");
|
|
70
|
+
break;
|
|
71
|
+
case "project":
|
|
72
|
+
args.project = requireValue(token, "--project requires a value (number or node ID)");
|
|
73
|
+
break;
|
|
74
|
+
case "column":
|
|
75
|
+
args.column = requireValue(token, "--column requires a value");
|
|
76
|
+
break;
|
|
77
|
+
case "limit": {
|
|
78
|
+
const raw = requireValue(token, "--limit requires a positive integer");
|
|
79
|
+
const val = Number(raw);
|
|
80
|
+
if (!Number.isInteger(val) || val < 1) {
|
|
81
|
+
throw parseError(`--limit must be a positive integer, got "${raw}"`);
|
|
82
|
+
}
|
|
83
|
+
args.limit = val;
|
|
84
|
+
break;
|
|
76
85
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
args.help = true;
|
|
80
|
-
} else {
|
|
81
|
-
throw Object.assign(
|
|
82
|
-
new Error(`Unexpected argument: ${arg}`),
|
|
83
|
-
{ code: "INVALID_ARGS", usage: USAGE },
|
|
84
|
-
);
|
|
86
|
+
default:
|
|
87
|
+
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
return args;
|
|
88
91
|
}
|
|
89
|
-
|
|
90
92
|
// ── Validation ───────────────────────────────────────────────────────────
|
|
91
93
|
|
|
92
94
|
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
@@ -460,7 +462,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
460
462
|
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
461
463
|
let args;
|
|
462
464
|
try {
|
|
463
|
-
args =
|
|
465
|
+
args = parseCliArgs(argv);
|
|
464
466
|
} catch (err) {
|
|
465
467
|
stderr.write(`${formatCliError(err)}\n`);
|
|
466
468
|
process.exitCode = 1;
|