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