dev-loops 0.1.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/.pi/dev-loop/defaults.yaml +477 -0
- package/AGENTS.md +25 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/dev-loop.agent.md +82 -0
- package/agents/developer.agent.md +37 -0
- package/agents/docs.agent.md +33 -0
- package/agents/fixer.agent.md +53 -0
- package/agents/quality.agent.md +28 -0
- package/agents/refiner.agent.md +87 -0
- package/agents/review.agent.md +64 -0
- package/cli/index.mjs +424 -0
- package/extension/README.md +233 -0
- package/extension/checks.ts +94 -0
- package/extension/index.ts +131 -0
- package/extension/post-merge-update.ts +512 -0
- package/extension/presentation.ts +107 -0
- package/lib/dev-loops-core.mjs +284 -0
- package/package.json +103 -0
- package/scripts/README.md +1007 -0
- package/scripts/_cli-primitives.mjs +10 -0
- package/scripts/_core-helpers.mjs +30 -0
- package/scripts/docs/validate-links.mjs +567 -0
- package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
- package/scripts/github/_review-thread-mutations.mjs +214 -0
- package/scripts/github/capture-review-threads.mjs +180 -0
- package/scripts/github/create-draft-pr.mjs +108 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
- package/scripts/github/detect-linked-issue-pr.mjs +331 -0
- package/scripts/github/manage-sub-issues.mjs +394 -0
- package/scripts/github/probe-copilot-review.mjs +323 -0
- package/scripts/github/ready-for-review.mjs +93 -0
- package/scripts/github/reconcile-draft-gate.mjs +328 -0
- package/scripts/github/reply-resolve-review-thread.mjs +42 -0
- package/scripts/github/reply-resolve-review-threads.mjs +329 -0
- package/scripts/github/request-copilot-review.mjs +551 -0
- package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
- package/scripts/github/stage-reviewer-draft.mjs +191 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
- package/scripts/github/verify-fresh-review-context.mjs +125 -0
- package/scripts/github/write-gate-findings-log.mjs +212 -0
- package/scripts/loop/_checkpoint-io.mjs +55 -0
- package/scripts/loop/_checkpoint-paths.mjs +28 -0
- package/scripts/loop/_handoff-contract.mjs +230 -0
- package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
- package/scripts/loop/_loop-evidence.mjs +32 -0
- package/scripts/loop/_pr-runner-coordination.mjs +611 -0
- package/scripts/loop/_stale-runner-detection.mjs +145 -0
- package/scripts/loop/_steering-state-file.mjs +134 -0
- package/scripts/loop/build-handoff-envelope.mjs +181 -0
- package/scripts/loop/checkpoint-contract.mjs +49 -0
- package/scripts/loop/conductor-monitor.mjs +1850 -0
- package/scripts/loop/conductor.mjs +214 -0
- package/scripts/loop/copilot-pr-handoff.mjs +493 -0
- package/scripts/loop/debt-remediate.mjs +304 -0
- package/scripts/loop/detect-change-scope.mjs +102 -0
- package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
- package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
- package/scripts/loop/detect-internal-only-pr.mjs +270 -0
- package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
- package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
- package/scripts/loop/detect-stale-runner.mjs +250 -0
- package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
- package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
- package/scripts/loop/info.mjs +267 -0
- package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
- package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
- package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
- package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
- package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
- package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
- package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
- package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
- package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
- package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
- package/scripts/loop/inspect-run-viewer.mjs +82 -0
- package/scripts/loop/inspect-run.mjs +382 -0
- package/scripts/loop/outer-loop.mjs +419 -0
- package/scripts/loop/pr-runner-coordination.mjs +143 -0
- package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
- package/scripts/loop/pre-flight-gate.mjs +236 -0
- package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
- package/scripts/loop/pre-push-main-guard.mjs +103 -0
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
- package/scripts/loop/print-gates.mjs +42 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
- package/scripts/loop/run-conductor-cycle.mjs +322 -0
- package/scripts/loop/run-queue.mjs +124 -0
- package/scripts/loop/run-refinement-audit.mjs +513 -0
- package/scripts/loop/run-watch-cycle.mjs +358 -0
- package/scripts/loop/steer-loop.mjs +841 -0
- package/scripts/loop/ui-designer-review-contract.mjs +76 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
- package/scripts/projects/add-queue-item.mjs +528 -0
- package/scripts/projects/ensure-queue-board.mjs +837 -0
- package/scripts/projects/list-queue-items.mjs +489 -0
- package/scripts/projects/move-queue-item.mjs +549 -0
- package/scripts/projects/reorder-queue-item.mjs +518 -0
- package/scripts/refine/_refine-helpers.mjs +258 -0
- package/scripts/refine/prose-linkage-detector.mjs +92 -0
- package/scripts/refine/refinement-completeness-checker.mjs +88 -0
- package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
- package/scripts/refine/tree-integrity-validator.mjs +211 -0
- package/scripts/refine/verify.mjs +178 -0
- package/scripts/repo-wiki-local.mjs +156 -0
- package/scripts/repo-wiki.mjs +119 -0
- package/skills/copilot-pr-followup/SKILL.md +380 -0
- package/skills/dev-loop/SKILL.md +141 -0
- package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
- package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
- package/skills/dev-loop/scripts/init-phase.mjs +71 -0
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
- package/skills/dev-loop/scripts/phase-files.mjs +29 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
- package/skills/dev-loop/scripts/render-template.mjs +82 -0
- package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
- package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/skills/dev-loop/templates/phase-doc.md +27 -0
- package/skills/dev-loop/templates/phase-summary.md +13 -0
- package/skills/dev-loop/templates/phase-variant.md +15 -0
- package/skills/dev-loop/templates/retrospective.md +11 -0
- package/skills/dev-loop/templates/review.md +32 -0
- package/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/skills/docs/acceptance-criteria-verification.md +21 -0
- package/skills/docs/anti-patterns.md +21 -0
- package/skills/docs/artifact-authority-contract.md +119 -0
- package/skills/docs/confirmation-rules.md +28 -0
- package/skills/docs/copilot-ci-status-contract.md +52 -0
- package/skills/docs/copilot-loop-operations.md +233 -0
- package/skills/docs/debt-remediation-contract.md +107 -0
- package/skills/docs/entrypoint-strategies.md +115 -0
- package/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/skills/docs/issue-intake-procedure.md +235 -0
- package/skills/docs/main-agent-contract.md +72 -0
- package/skills/docs/merge-preconditions.md +29 -0
- package/skills/docs/pr-lifecycle-contract.md +209 -0
- package/skills/docs/public-dev-loop-contract.md +497 -0
- package/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/skills/docs/stop-conditions.md +29 -0
- package/skills/docs/structural-quality.md +42 -0
- package/skills/docs/tracker-first-loop-state.md +281 -0
- package/skills/docs/validation-policy.md +27 -0
- package/skills/docs/workflow-handoff-contract.md +135 -0
- package/skills/final-approval/SKILL.md +19 -0
- package/skills/local-implementation/SKILL.md +640 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
+
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
|
|
5
|
+
const USAGE = `Usage: dev-loops project add --repo <owner/name> --project <number|id> --item <number>
|
|
6
|
+
|
|
7
|
+
Add an existing issue or PR to a GitHub Projects V2 board.
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--repo <owner/name> Required. Repository containing the issue/PR.
|
|
11
|
+
--project <number|id> Required. Project number (integer) or node ID.
|
|
12
|
+
--item <number> Required. Issue or PR number to add.
|
|
13
|
+
--status <name> Initial Status column (default: "Backlog").
|
|
14
|
+
--help, -h Show this help.
|
|
15
|
+
|
|
16
|
+
Output (stdout):
|
|
17
|
+
JSON: { ok: true, item: { itemId, issueNumber, prNumber, status, alreadyPresent } }
|
|
18
|
+
|
|
19
|
+
Exit codes:
|
|
20
|
+
0 — success (or no-op when already present)
|
|
21
|
+
1 — usage or argument error
|
|
22
|
+
2 — GitHub API error
|
|
23
|
+
3 — project, field, column, or issue/PR not found
|
|
24
|
+
`.trim();
|
|
25
|
+
|
|
26
|
+
const VALID_ARGS = new Set(["--repo", "--project", "--item", "--status", "--help", "-h"]);
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const args = {};
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
if (!VALID_ARGS.has(arg) && arg.startsWith("-")) {
|
|
33
|
+
throw Object.assign(
|
|
34
|
+
new Error(`Unknown flag: ${arg}`),
|
|
35
|
+
{ code: "INVALID_ARGS", usage: USAGE },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--repo") {
|
|
39
|
+
if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
|
|
40
|
+
throw Object.assign(new Error("--repo requires a value (owner/name)"), { code: "INVALID_REPO" });
|
|
41
|
+
}
|
|
42
|
+
args.repo = argv[++i];
|
|
43
|
+
} else if (arg === "--project") {
|
|
44
|
+
if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
|
|
45
|
+
throw Object.assign(new Error("--project requires a value (number or node ID)"), { code: "INVALID_PROJECT" });
|
|
46
|
+
}
|
|
47
|
+
args.project = argv[++i];
|
|
48
|
+
} else if (arg === "--item") {
|
|
49
|
+
if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
|
|
50
|
+
throw Object.assign(new Error("--item requires a value (number)"), { code: "INVALID_ITEM" });
|
|
51
|
+
}
|
|
52
|
+
const val = Number(argv[++i]);
|
|
53
|
+
if (!Number.isInteger(val) || val < 1) {
|
|
54
|
+
throw Object.assign(
|
|
55
|
+
new Error(`--item must be a positive integer, got "${argv[i]}"`),
|
|
56
|
+
{ code: "INVALID_ITEM" },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
args.item = val;
|
|
60
|
+
} else if (arg === "--status") {
|
|
61
|
+
if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
|
|
62
|
+
throw Object.assign(new Error("--status requires a value"), { code: "INVALID_STATUS" });
|
|
63
|
+
}
|
|
64
|
+
args.status = argv[++i];
|
|
65
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
66
|
+
args.help = true;
|
|
67
|
+
} else {
|
|
68
|
+
throw Object.assign(
|
|
69
|
+
new Error(`Unexpected argument: ${arg}`),
|
|
70
|
+
{ code: "INVALID_ARGS", usage: USAGE },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Validation ───────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
80
|
+
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
81
|
+
const GLOBAL_NODE_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
82
|
+
|
|
83
|
+
function validateRepo(repo) {
|
|
84
|
+
if (!repo || typeof repo !== "string") {
|
|
85
|
+
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
86
|
+
}
|
|
87
|
+
const trimmed = repo.trim();
|
|
88
|
+
if (trimmed !== repo) {
|
|
89
|
+
throw Object.assign(
|
|
90
|
+
new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`),
|
|
91
|
+
{ code: "INVALID_REPO" },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const slashIdx = repo.indexOf("/");
|
|
95
|
+
if (slashIdx === -1) {
|
|
96
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
97
|
+
}
|
|
98
|
+
const owner = repo.slice(0, slashIdx);
|
|
99
|
+
const name = repo.slice(slashIdx + 1);
|
|
100
|
+
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
101
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
102
|
+
}
|
|
103
|
+
return repo;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseProjectRef(raw) {
|
|
107
|
+
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
108
|
+
throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
|
|
109
|
+
}
|
|
110
|
+
const trimmed = raw.trim();
|
|
111
|
+
const asNum = Number(trimmed);
|
|
112
|
+
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
113
|
+
return { kind: "number", value: asNum };
|
|
114
|
+
}
|
|
115
|
+
if (trimmed === "0") {
|
|
116
|
+
throw Object.assign(
|
|
117
|
+
new Error(`--project must be a positive integer or a node ID, got "${raw}"`),
|
|
118
|
+
{ code: "INVALID_PROJECT" },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
122
|
+
return { kind: "id", value: trimmed };
|
|
123
|
+
}
|
|
124
|
+
throw Object.assign(
|
|
125
|
+
new Error(`--project must be a positive integer or a node ID, got "${raw}"`),
|
|
126
|
+
{ code: "INVALID_PROJECT" },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── API helpers ──────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
async function ghGraphql(query, vars, env, runChild = _runChild, { allowErrors = false } = {}) {
|
|
133
|
+
const fieldArgs = [];
|
|
134
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
135
|
+
fieldArgs.push("--field", `${key}=${value}`);
|
|
136
|
+
}
|
|
137
|
+
const result = await runChild(
|
|
138
|
+
"gh",
|
|
139
|
+
["api", "graphql", "--field", `query=${query}`, ...fieldArgs],
|
|
140
|
+
env,
|
|
141
|
+
);
|
|
142
|
+
if (result.code !== 0) {
|
|
143
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
144
|
+
throw Object.assign(new Error(`gh api graphql failed: ${detail}`), { code: "GH_API_ERROR" });
|
|
145
|
+
}
|
|
146
|
+
const payload = parseJsonText(result.stdout);
|
|
147
|
+
if (!allowErrors && payload.errors && payload.errors.length > 0) {
|
|
148
|
+
throw Object.assign(
|
|
149
|
+
new Error(`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`),
|
|
150
|
+
{ code: "GRAPHQL_ERROR" },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return payload;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── GraphQL fragments ────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
const GET_USER_ID = [
|
|
159
|
+
"query($login:String!) {",
|
|
160
|
+
" user(login:$login) { id }",
|
|
161
|
+
"}"
|
|
162
|
+
].join("\n");
|
|
163
|
+
|
|
164
|
+
const GET_ORG_ID = [
|
|
165
|
+
"query($login:String!) {",
|
|
166
|
+
" organization(login:$login) { id }",
|
|
167
|
+
"}"
|
|
168
|
+
].join("\n");
|
|
169
|
+
|
|
170
|
+
const LIST_USER_PROJECTS = [
|
|
171
|
+
"query($login:String!, $after:String) {",
|
|
172
|
+
" user(login:$login) {",
|
|
173
|
+
" projectsV2(first:50, after:$after) {",
|
|
174
|
+
" pageInfo { hasNextPage endCursor }",
|
|
175
|
+
" nodes { id number title url }",
|
|
176
|
+
" }",
|
|
177
|
+
" }",
|
|
178
|
+
"}"
|
|
179
|
+
].join("\n");
|
|
180
|
+
|
|
181
|
+
const LIST_ORG_PROJECTS = [
|
|
182
|
+
"query($login:String!, $after:String) {",
|
|
183
|
+
" organization(login:$login) {",
|
|
184
|
+
" projectsV2(first:50, after:$after) {",
|
|
185
|
+
" pageInfo { hasNextPage endCursor }",
|
|
186
|
+
" nodes { id number title url }",
|
|
187
|
+
" }",
|
|
188
|
+
" }",
|
|
189
|
+
"}"
|
|
190
|
+
].join("\n");
|
|
191
|
+
|
|
192
|
+
const GET_PROJECT_FIELDS = [
|
|
193
|
+
"query($projectId:ID!, $after:String) {",
|
|
194
|
+
" node(id:$projectId) {",
|
|
195
|
+
" ... on ProjectV2 {",
|
|
196
|
+
" fields(first:50, after:$after) {",
|
|
197
|
+
" pageInfo { hasNextPage endCursor }",
|
|
198
|
+
" nodes {",
|
|
199
|
+
" ... on ProjectV2SingleSelectField {",
|
|
200
|
+
" id name",
|
|
201
|
+
" options { id name }",
|
|
202
|
+
" }",
|
|
203
|
+
" }",
|
|
204
|
+
" }",
|
|
205
|
+
" }",
|
|
206
|
+
" }",
|
|
207
|
+
"}"
|
|
208
|
+
].join("\n");
|
|
209
|
+
|
|
210
|
+
// Resolve an issue or PR's GraphQL node ID by number
|
|
211
|
+
const RESOLVE_CONTENT_NODE_ID = [
|
|
212
|
+
"query($owner:String!, $repo:String!, $number:Int!) {",
|
|
213
|
+
" repository(owner:$owner, name:$repo) {",
|
|
214
|
+
" issueOrPullRequest(number:$number) {",
|
|
215
|
+
" ... on Issue { id __typename }",
|
|
216
|
+
" ... on PullRequest { id __typename }",
|
|
217
|
+
" }",
|
|
218
|
+
" }",
|
|
219
|
+
"}"
|
|
220
|
+
].join("\n");
|
|
221
|
+
|
|
222
|
+
const ADD_PROJECT_ITEM = [
|
|
223
|
+
"mutation($projectId:ID!, $contentId:ID!) {",
|
|
224
|
+
" addProjectV2ItemById(input:{projectId:$projectId, contentId:$contentId}) {",
|
|
225
|
+
" item {",
|
|
226
|
+
" id",
|
|
227
|
+
" }",
|
|
228
|
+
" }",
|
|
229
|
+
"}"
|
|
230
|
+
].join("\n");
|
|
231
|
+
|
|
232
|
+
const UPDATE_ITEM_FIELD = [
|
|
233
|
+
"mutation($projectId:ID!, $itemId:ID!, $fieldId:ID!, $optionId:String!) {",
|
|
234
|
+
" updateProjectV2ItemFieldValue(input:{projectId:$projectId, itemId:$itemId, fieldId:$fieldId, value:{singleSelectOptionId:$optionId}}) {",
|
|
235
|
+
" projectV2Item {",
|
|
236
|
+
" id",
|
|
237
|
+
" }",
|
|
238
|
+
" }",
|
|
239
|
+
"}"
|
|
240
|
+
].join("\n");
|
|
241
|
+
|
|
242
|
+
// Check if an item already exists in the project by content ID
|
|
243
|
+
const GET_PROJECT_ITEMS_BY_CONTENT = [
|
|
244
|
+
"query($projectId:ID!, $after:String) {",
|
|
245
|
+
" node(id:$projectId) {",
|
|
246
|
+
" ... on ProjectV2 {",
|
|
247
|
+
" items(first:10, after:$after, orderBy:{field:POSITION, direction:ASC}) {",
|
|
248
|
+
" pageInfo { hasNextPage endCursor }",
|
|
249
|
+
" nodes {",
|
|
250
|
+
" id",
|
|
251
|
+
" fieldValues(first:20) {",
|
|
252
|
+
" nodes {",
|
|
253
|
+
" ... on ProjectV2ItemFieldSingleSelectValue {",
|
|
254
|
+
" field { ... on ProjectV2SingleSelectField { id name } }",
|
|
255
|
+
" name",
|
|
256
|
+
" }",
|
|
257
|
+
" }",
|
|
258
|
+
" }",
|
|
259
|
+
" content {",
|
|
260
|
+
" ... on Issue { number repository { nameWithOwner } }",
|
|
261
|
+
" ... on PullRequest { number repository { nameWithOwner } }",
|
|
262
|
+
" }",
|
|
263
|
+
" }",
|
|
264
|
+
" }",
|
|
265
|
+
" }",
|
|
266
|
+
" }",
|
|
267
|
+
"}"
|
|
268
|
+
].join("\n");
|
|
269
|
+
|
|
270
|
+
// ── Owner resolution ────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
async function resolveOwner(login, env, runChild) {
|
|
273
|
+
const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
|
|
274
|
+
if (userPayload?.data?.user?.id) {
|
|
275
|
+
return { id: userPayload.data.user.id, kind: "user" };
|
|
276
|
+
}
|
|
277
|
+
const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
|
|
278
|
+
if (orgPayload?.data?.organization?.id) {
|
|
279
|
+
return { id: orgPayload.data.organization.id, kind: "org" };
|
|
280
|
+
}
|
|
281
|
+
throw Object.assign(
|
|
282
|
+
new Error(`Could not resolve owner ID for "${login}"`),
|
|
283
|
+
{ code: "NO_USER_ID" },
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Paginated project listing ────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
async function listAllProjects(login, kind, env, runChild) {
|
|
290
|
+
const query = kind === "org" ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS;
|
|
291
|
+
const projects = [];
|
|
292
|
+
let after = null;
|
|
293
|
+
while (true) {
|
|
294
|
+
const vars = { login };
|
|
295
|
+
if (after) vars.after = after;
|
|
296
|
+
const payload = await ghGraphql(query, vars, env, runChild);
|
|
297
|
+
const connection = kind === "org"
|
|
298
|
+
? payload?.data?.organization?.projectsV2
|
|
299
|
+
: payload?.data?.user?.projectsV2;
|
|
300
|
+
const nodes = connection?.nodes ?? [];
|
|
301
|
+
projects.push(...nodes);
|
|
302
|
+
const pageInfo = connection?.pageInfo ?? {};
|
|
303
|
+
if (!pageInfo.hasNextPage) break;
|
|
304
|
+
if (!pageInfo.endCursor) {
|
|
305
|
+
throw Object.assign(
|
|
306
|
+
new Error("Invalid projects list payload: hasNextPage is true but endCursor is missing"),
|
|
307
|
+
{ code: "GH_API_ERROR" },
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
after = pageInfo.endCursor;
|
|
311
|
+
}
|
|
312
|
+
return projects;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Paginated field listing ──────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
async function listAllFields(projectId, env, runChild) {
|
|
318
|
+
const fields = [];
|
|
319
|
+
let after = null;
|
|
320
|
+
while (true) {
|
|
321
|
+
const vars = { projectId };
|
|
322
|
+
if (after) vars.after = after;
|
|
323
|
+
const payload = await ghGraphql(GET_PROJECT_FIELDS, vars, env, runChild);
|
|
324
|
+
const connection = payload?.data?.node?.fields;
|
|
325
|
+
const nodes = connection?.nodes ?? [];
|
|
326
|
+
fields.push(...nodes);
|
|
327
|
+
const pageInfo = connection?.pageInfo ?? {};
|
|
328
|
+
if (!pageInfo.hasNextPage) break;
|
|
329
|
+
if (!pageInfo.endCursor) {
|
|
330
|
+
throw Object.assign(
|
|
331
|
+
new Error("Invalid fields payload: hasNextPage is true but endCursor is missing"),
|
|
332
|
+
{ code: "GH_API_ERROR" },
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
after = pageInfo.endCursor;
|
|
336
|
+
}
|
|
337
|
+
return fields;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Exit code classification ────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
function classifyExitCode(err) {
|
|
343
|
+
if (err.code === "INVALID_REPO" || err.code === "INVALID_PROJECT" || err.code === "INVALID_ITEM" ||
|
|
344
|
+
err.code === "INVALID_STATUS" || err.code === "INVALID_ARGS") return 1;
|
|
345
|
+
if (err.code === "PROJECT_NOT_FOUND" || err.code === "FIELD_NOT_FOUND" || err.code === "COLUMN_NOT_FOUND" ||
|
|
346
|
+
err.code === "ITEM_NOT_FOUND" || err.code === "CONTENT_NOT_FOUND") return 3;
|
|
347
|
+
return 2;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Main logic ──────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
async function main(args, { env = process.env, runChild } = {}) {
|
|
353
|
+
const child = runChild ?? _runChild;
|
|
354
|
+
const repo = validateRepo(args.repo);
|
|
355
|
+
const [owner, repoName] = repo.split("/");
|
|
356
|
+
const projectRef = parseProjectRef(args.project);
|
|
357
|
+
const itemNumber = args.item;
|
|
358
|
+
if (!Number.isInteger(itemNumber) || itemNumber < 1) {
|
|
359
|
+
throw Object.assign(new Error("--item is required and must be a positive integer"), { code: "INVALID_ITEM" });
|
|
360
|
+
}
|
|
361
|
+
const targetStatus = (args.status ?? "Backlog").trim();
|
|
362
|
+
if (!targetStatus) {
|
|
363
|
+
throw Object.assign(new Error("--status must not be empty"), { code: "INVALID_STATUS" });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 1. Resolve owner
|
|
367
|
+
const { id: ownerId, kind: ownerKind } = await resolveOwner(owner, env, child);
|
|
368
|
+
|
|
369
|
+
// 2. Resolve project
|
|
370
|
+
const projects = await listAllProjects(owner, ownerKind, env, child);
|
|
371
|
+
let project;
|
|
372
|
+
if (projectRef.kind === "id") {
|
|
373
|
+
project = projects.find((p) => p.id === projectRef.value);
|
|
374
|
+
} else {
|
|
375
|
+
project = projects.find((p) => p.number === projectRef.value);
|
|
376
|
+
}
|
|
377
|
+
if (!project) {
|
|
378
|
+
throw Object.assign(
|
|
379
|
+
new Error(`Project ${projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`} not found under owner "${owner}"`),
|
|
380
|
+
{ code: "PROJECT_NOT_FOUND" },
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 3. Resolve Status field and target column
|
|
385
|
+
const fieldNodes = await listAllFields(project.id, env, child);
|
|
386
|
+
const statusField = fieldNodes.find((f) => f.name === "Status" && f.options);
|
|
387
|
+
if (!statusField) {
|
|
388
|
+
throw Object.assign(
|
|
389
|
+
new Error(`Status field not found in project "${project.title}" (number ${project.number})`),
|
|
390
|
+
{ code: "FIELD_NOT_FOUND" },
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const targetOption = statusField.options.find((o) => o.name === targetStatus);
|
|
395
|
+
if (!targetOption) {
|
|
396
|
+
const available = statusField.options.map((o) => o.name).join(", ");
|
|
397
|
+
throw Object.assign(
|
|
398
|
+
new Error(`Column "${targetStatus}" not found in Status field. Available: ${available}`),
|
|
399
|
+
{ code: "COLUMN_NOT_FOUND" },
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 4. Check if item already exists in the project
|
|
404
|
+
const existingItemsPayload = await ghGraphql(GET_PROJECT_ITEMS_BY_CONTENT, {
|
|
405
|
+
projectId: project.id,
|
|
406
|
+
}, env, child);
|
|
407
|
+
const existingItems = existingItemsPayload?.data?.node?.items?.nodes ?? [];
|
|
408
|
+
|
|
409
|
+
const alreadyPresent = existingItems.filter((it) => {
|
|
410
|
+
if (!it.content) return false;
|
|
411
|
+
return it.content.repository?.nameWithOwner === repo && it.content.number === itemNumber;
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (alreadyPresent.length > 0) {
|
|
415
|
+
const existing = alreadyPresent[0];
|
|
416
|
+
let existingStatus = null;
|
|
417
|
+
const fvs = existing.fieldValues?.nodes ?? [];
|
|
418
|
+
for (const fv of fvs) {
|
|
419
|
+
if (fv && fv.field && fv.field.name === "Status") {
|
|
420
|
+
existingStatus = fv.name;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let issueNumber = null;
|
|
425
|
+
let prNumber = null;
|
|
426
|
+
if (existing.content) {
|
|
427
|
+
if (existing.content.__typename === "Issue") issueNumber = existing.content.number;
|
|
428
|
+
else prNumber = existing.content.number;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
item: {
|
|
433
|
+
itemId: existing.id,
|
|
434
|
+
issueNumber,
|
|
435
|
+
prNumber,
|
|
436
|
+
status: existingStatus,
|
|
437
|
+
alreadyPresent: true,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 5. Resolve content node ID (issue or PR)
|
|
443
|
+
const contentPayload = await ghGraphql(RESOLVE_CONTENT_NODE_ID, {
|
|
444
|
+
owner,
|
|
445
|
+
repo: repoName,
|
|
446
|
+
number: itemNumber,
|
|
447
|
+
}, env, child, { allowErrors: true });
|
|
448
|
+
const repoData = contentPayload?.data?.repository;
|
|
449
|
+
const fullResult = repoData?.issueOrPullRequest;
|
|
450
|
+
if (!fullResult) {
|
|
451
|
+
throw Object.assign(
|
|
452
|
+
new Error(`Issue or PR #${itemNumber} not found in "${repo}"`),
|
|
453
|
+
{ code: "CONTENT_NOT_FOUND" },
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let contentId = fullResult.id;
|
|
458
|
+
let issueNumber = fullResult.__typename === "Issue" ? itemNumber : null;
|
|
459
|
+
let prNumber = fullResult.__typename === "PullRequest" ? itemNumber : null;
|
|
460
|
+
|
|
461
|
+
// 6. Add item to project
|
|
462
|
+
const addPayload = await ghGraphql(ADD_PROJECT_ITEM, {
|
|
463
|
+
projectId: project.id,
|
|
464
|
+
contentId,
|
|
465
|
+
}, env, child);
|
|
466
|
+
|
|
467
|
+
const newItem = addPayload?.data?.addProjectV2ItemById?.item;
|
|
468
|
+
if (!newItem) {
|
|
469
|
+
throw Object.assign(new Error("Failed to add item to project"), { code: "MUTATION_FAILED" });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 7. Set initial Status
|
|
473
|
+
const updatePayload = await ghGraphql(UPDATE_ITEM_FIELD, {
|
|
474
|
+
projectId: project.id,
|
|
475
|
+
itemId: newItem.id,
|
|
476
|
+
fieldId: statusField.id,
|
|
477
|
+
optionId: targetOption.id,
|
|
478
|
+
}, env, child);
|
|
479
|
+
|
|
480
|
+
const updated = updatePayload?.data?.updateProjectV2ItemFieldValue?.projectV2Item;
|
|
481
|
+
if (!updated) {
|
|
482
|
+
throw Object.assign(new Error("Failed to set initial Status on new item"), { code: "MUTATION_FAILED" });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
ok: true,
|
|
487
|
+
item: {
|
|
488
|
+
itemId: newItem.id,
|
|
489
|
+
issueNumber,
|
|
490
|
+
prNumber,
|
|
491
|
+
status: targetStatus,
|
|
492
|
+
alreadyPresent: false,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
500
|
+
let args;
|
|
501
|
+
try {
|
|
502
|
+
args = parseArgs(argv);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
stderr.write(`${formatCliError(err)}\n`);
|
|
505
|
+
process.exitCode = 1;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (args.help) {
|
|
509
|
+
stdout.write(USAGE);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const result = await main(args, { env });
|
|
514
|
+
stdout.write(JSON.stringify(result) + "\n");
|
|
515
|
+
} catch (err) {
|
|
516
|
+
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
517
|
+
process.exitCode = classifyExitCode(err);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
522
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
523
|
+
process.stderr.write(JSON.stringify({ ok: false, error: error.message, code: error.code ?? "UNKNOWN" }) + "\n");
|
|
524
|
+
process.exitCode = 2;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export { main };
|