issue-flow 0.5.2 → 0.7.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/dist/{analyze-QVQ4CTCU.js → analyze-WRFFG2R5.js} +20 -6
- package/dist/analyze-WRFFG2R5.js.map +1 -0
- package/dist/{chunk-O5XI2CFF.js → chunk-2Y3UO7H2.js} +81 -21
- package/dist/chunk-2Y3UO7H2.js.map +1 -0
- package/dist/{chunk-KMX3DUJN.js → chunk-75K5IKAC.js} +19 -7
- package/dist/chunk-75K5IKAC.js.map +1 -0
- package/dist/{chunk-JMSRJ43G.js → chunk-BTBSVBE3.js} +19 -7
- package/dist/chunk-BTBSVBE3.js.map +1 -0
- package/dist/chunk-CW6UE3NW.js +72 -0
- package/dist/chunk-CW6UE3NW.js.map +1 -0
- package/dist/chunk-EWMSQQL2.js +725 -0
- package/dist/chunk-EWMSQQL2.js.map +1 -0
- package/dist/chunk-HS4QWZAO.js +707 -0
- package/dist/chunk-HS4QWZAO.js.map +1 -0
- package/dist/{chunk-3ULIAC2J.js → chunk-JV7P6UGG.js} +2 -2
- package/dist/{chunk-KLBAHTXX.js → chunk-KUDAY57D.js} +2 -2
- package/dist/{chunk-PA3GI7LR.js → chunk-OVFYJLOH.js} +130 -41
- package/dist/chunk-OVFYJLOH.js.map +1 -0
- package/dist/{chunk-LJI45O5B.js → chunk-UX6YAVLT.js} +40 -7
- package/dist/chunk-UX6YAVLT.js.map +1 -0
- package/dist/{chunk-VM7C7L3A.js → chunk-VLSZFMLZ.js} +21 -9
- package/dist/chunk-VLSZFMLZ.js.map +1 -0
- package/dist/{chunk-DLWRUTRL.js → chunk-YXJPKBPV.js} +43 -68
- package/dist/chunk-YXJPKBPV.js.map +1 -0
- package/dist/{chunk-OEJSMZMO.js → chunk-Z3UIGLZG.js} +18 -6
- package/dist/chunk-Z3UIGLZG.js.map +1 -0
- package/dist/cli.js +177 -31
- package/dist/cli.js.map +1 -1
- package/dist/config-UHQ6XXX2.js +31 -0
- package/dist/execute-K5B4UMUT.js +13 -0
- package/dist/generate-DUKQCADD.js +145 -0
- package/dist/generate-DUKQCADD.js.map +1 -0
- package/dist/init-W4HBLW7R.js +9 -0
- package/dist/plan-NKPU45NP.js +15 -0
- package/dist/pr-JYGLLIEF.js +15 -0
- package/dist/pr-review-6H6F4XUY.js +15 -0
- package/dist/prd-FC2ZCEOX.js +15 -0
- package/dist/prd-FC2ZCEOX.js.map +1 -0
- package/dist/review-X4NGDZZI.js +13 -0
- package/dist/review-X4NGDZZI.js.map +1 -0
- package/dist/{run-3GJC54DB.js → run-VC647DQI.js} +177 -63
- package/dist/run-VC647DQI.js.map +1 -0
- package/package.json +2 -1
- package/prompts/analyze.md +18 -5
- package/prompts/generate.md +27 -6
- package/prompts/plan.md +19 -2
- package/prompts/pr-review.md +144 -0
- package/prompts/pr.md +23 -6
- package/prompts/prd.md +18 -5
- package/prompts/review.md +18 -6
- package/dist/analyze-QVQ4CTCU.js.map +0 -1
- package/dist/chunk-DLWRUTRL.js.map +0 -1
- package/dist/chunk-JMSRJ43G.js.map +0 -1
- package/dist/chunk-KMX3DUJN.js.map +0 -1
- package/dist/chunk-LJI45O5B.js.map +0 -1
- package/dist/chunk-O5XI2CFF.js.map +0 -1
- package/dist/chunk-OEJSMZMO.js.map +0 -1
- package/dist/chunk-PA3GI7LR.js.map +0 -1
- package/dist/chunk-VM7C7L3A.js.map +0 -1
- package/dist/execute-TEJ26L4O.js +0 -12
- package/dist/generate-KBQ4KW4T.js +0 -48
- package/dist/generate-KBQ4KW4T.js.map +0 -1
- package/dist/init-4VMPBRMA.js +0 -9
- package/dist/plan-PWYZP5AN.js +0 -13
- package/dist/pr-CJQWZUDR.js +0 -13
- package/dist/prd-4KLQY7BC.js +0 -13
- package/dist/review-YFCWBBKY.js +0 -11
- package/dist/run-3GJC54DB.js.map +0 -1
- /package/dist/{chunk-3ULIAC2J.js.map → chunk-JV7P6UGG.js.map} +0 -0
- /package/dist/{chunk-KLBAHTXX.js.map → chunk-KUDAY57D.js.map} +0 -0
- /package/dist/{execute-TEJ26L4O.js.map → config-UHQ6XXX2.js.map} +0 -0
- /package/dist/{init-4VMPBRMA.js.map → execute-K5B4UMUT.js.map} +0 -0
- /package/dist/{plan-PWYZP5AN.js.map → init-W4HBLW7R.js.map} +0 -0
- /package/dist/{pr-CJQWZUDR.js.map → plan-NKPU45NP.js.map} +0 -0
- /package/dist/{prd-4KLQY7BC.js.map → pr-JYGLLIEF.js.map} +0 -0
- /package/dist/{review-YFCWBBKY.js.map → pr-review-6H6F4XUY.js.map} +0 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getProjectRoot,
|
|
4
|
+
loadIssuesConfig,
|
|
5
|
+
run
|
|
6
|
+
} from "./chunk-OVFYJLOH.js";
|
|
7
|
+
import {
|
|
8
|
+
isoNow,
|
|
9
|
+
issueMetadataSchema,
|
|
10
|
+
printError,
|
|
11
|
+
printInfo,
|
|
12
|
+
printWarning,
|
|
13
|
+
writeFileAtomic
|
|
14
|
+
} from "./chunk-2Y3UO7H2.js";
|
|
15
|
+
|
|
16
|
+
// src/issues/registry.ts
|
|
17
|
+
var providers = /* @__PURE__ */ new Map();
|
|
18
|
+
function registerProvider(provider) {
|
|
19
|
+
providers.set(provider.name, provider);
|
|
20
|
+
}
|
|
21
|
+
function getRegisteredSources() {
|
|
22
|
+
return [...providers.keys()];
|
|
23
|
+
}
|
|
24
|
+
function getProvider(source) {
|
|
25
|
+
const provider = providers.get(source);
|
|
26
|
+
if (provider === void 0) {
|
|
27
|
+
const available = getRegisteredSources();
|
|
28
|
+
const hint = available.length > 0 ? `Available sources: ${available.join(", ")}.` : "No Issue providers are registered.";
|
|
29
|
+
throw new Error(`Unknown Issue source: '${source}'. ${hint}`);
|
|
30
|
+
}
|
|
31
|
+
return provider;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/issues/hash.ts
|
|
35
|
+
import { createHash } from "crypto";
|
|
36
|
+
function normalizeText(value) {
|
|
37
|
+
return value.replace(/\r\n?/g, "\n").trim();
|
|
38
|
+
}
|
|
39
|
+
function hashIssueContent(title, body) {
|
|
40
|
+
const payload = JSON.stringify({
|
|
41
|
+
title: normalizeText(title),
|
|
42
|
+
body: normalizeText(body)
|
|
43
|
+
});
|
|
44
|
+
const digest = createHash("sha256").update(payload, "utf8").digest("hex");
|
|
45
|
+
return `sha256:${digest}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/issues/providers/github.ts
|
|
49
|
+
var VIEW_FIELDS = "number,title,body,labels,state,url,createdAt,updatedAt";
|
|
50
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
51
|
+
var NOT_FOUND_PATTERN = /could not resolve to an issue|no issues? found|issue not found|could not find any issue/i;
|
|
52
|
+
function normalizeState(state) {
|
|
53
|
+
return String(state ?? "").toUpperCase() === "CLOSED" ? "closed" : "open";
|
|
54
|
+
}
|
|
55
|
+
function normalizeLabels(labels) {
|
|
56
|
+
if (!Array.isArray(labels)) return [];
|
|
57
|
+
return labels.map((label) => typeof label === "string" ? label : label?.name).filter((name) => typeof name === "string" && name.length > 0);
|
|
58
|
+
}
|
|
59
|
+
function toIssue(payload) {
|
|
60
|
+
const title = typeof payload.title === "string" ? payload.title : "";
|
|
61
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
62
|
+
const number = typeof payload.number === "number" ? payload.number : null;
|
|
63
|
+
const url = typeof payload.url === "string" && payload.url.length > 0 ? payload.url : null;
|
|
64
|
+
return {
|
|
65
|
+
id: number === null ? "" : String(number),
|
|
66
|
+
number,
|
|
67
|
+
title,
|
|
68
|
+
body,
|
|
69
|
+
labels: normalizeLabels(payload.labels),
|
|
70
|
+
state: normalizeState(payload.state),
|
|
71
|
+
source: "github",
|
|
72
|
+
remoteRef: url,
|
|
73
|
+
createdAt: typeof payload.createdAt === "string" ? payload.createdAt : "",
|
|
74
|
+
updatedAt: typeof payload.updatedAt === "string" ? payload.updatedAt : "",
|
|
75
|
+
contentHash: hashIssueContent(title, body),
|
|
76
|
+
raw: payload
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function normalizeId(id) {
|
|
80
|
+
return id.trim().replace(/^#/, "");
|
|
81
|
+
}
|
|
82
|
+
function extractIssueNumber(output) {
|
|
83
|
+
return output.match(/\/issues\/(\d+)/)?.[1] ?? null;
|
|
84
|
+
}
|
|
85
|
+
var GitHubIssueProvider = class {
|
|
86
|
+
name = "github";
|
|
87
|
+
/** True when gh is installed and authenticated. Never throws. */
|
|
88
|
+
async isAvailable() {
|
|
89
|
+
try {
|
|
90
|
+
const version = await run("gh", ["--version"], { timeout: PROBE_TIMEOUT_MS });
|
|
91
|
+
if (version.exitCode !== 0) return false;
|
|
92
|
+
const auth = await run("gh", ["auth", "status"], { timeout: PROBE_TIMEOUT_MS });
|
|
93
|
+
return auth.exitCode === 0;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async get(id) {
|
|
99
|
+
const issueId = normalizeId(id);
|
|
100
|
+
const result = await run("gh", ["issue", "view", issueId, "--json", VIEW_FIELDS]);
|
|
101
|
+
if (result.exitCode !== 0) {
|
|
102
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
103
|
+
if (NOT_FOUND_PATTERN.test(detail)) return null;
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Failed to fetch GitHub issue #${issueId}: ${detail || "gh issue view failed"}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
let payload;
|
|
109
|
+
try {
|
|
110
|
+
payload = JSON.parse(result.stdout);
|
|
111
|
+
} catch {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Failed to parse gh output for issue #${issueId}: ${result.stdout.slice(0, 200)}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return toIssue(payload);
|
|
117
|
+
}
|
|
118
|
+
async create(draft) {
|
|
119
|
+
const args = ["issue", "create", "--title", draft.title, "--body", draft.body];
|
|
120
|
+
for (const label of draft.labels) {
|
|
121
|
+
args.push("--label", label);
|
|
122
|
+
}
|
|
123
|
+
const result = await run("gh", args);
|
|
124
|
+
if (result.exitCode !== 0) {
|
|
125
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
126
|
+
throw new Error(`Failed to create GitHub issue: ${detail || "gh issue create failed"}`);
|
|
127
|
+
}
|
|
128
|
+
const number = extractIssueNumber(result.stdout);
|
|
129
|
+
if (!number) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`GitHub issue creation did not report an issue URL: ${result.stdout.trim() || "(no output)"}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const created = await this.get(number);
|
|
135
|
+
if (!created) {
|
|
136
|
+
throw new Error(`GitHub issue #${number} was created but could not be read back`);
|
|
137
|
+
}
|
|
138
|
+
return created;
|
|
139
|
+
}
|
|
140
|
+
async close(id) {
|
|
141
|
+
const issueId = normalizeId(id);
|
|
142
|
+
const result = await run("gh", ["issue", "close", issueId]);
|
|
143
|
+
if (result.exitCode !== 0) {
|
|
144
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Failed to close GitHub issue #${issueId}: ${detail || "gh issue close failed"}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var githubIssueProvider = new GitHubIssueProvider();
|
|
152
|
+
|
|
153
|
+
// src/issues/providers/local.ts
|
|
154
|
+
import { constants } from "fs";
|
|
155
|
+
import { access, mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
|
|
156
|
+
import { join } from "path";
|
|
157
|
+
import { ZodError } from "zod";
|
|
158
|
+
var ISSUES_DIR = "issues";
|
|
159
|
+
var REMOTE_PROBE_TIMEOUT_MS = 1e4;
|
|
160
|
+
function isNotFound(err) {
|
|
161
|
+
const code = err.code;
|
|
162
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
163
|
+
}
|
|
164
|
+
function normalizeId2(id) {
|
|
165
|
+
const normalized = id.trim().replace(/^#/, "");
|
|
166
|
+
if (normalized.length === 0) {
|
|
167
|
+
throw new Error("Local issue identifier cannot be empty");
|
|
168
|
+
}
|
|
169
|
+
if (/[/\\]/.test(normalized) || normalized === "." || normalized === "..") {
|
|
170
|
+
throw new Error(`Invalid local issue identifier: '${id}'`);
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
function toNumber(id) {
|
|
175
|
+
return /^\d+$/.test(id) ? Number.parseInt(id, 10) : null;
|
|
176
|
+
}
|
|
177
|
+
function parseIssueMarkdown(content) {
|
|
178
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
179
|
+
const headingIndex = lines.findIndex((line) => line.trim().length > 0);
|
|
180
|
+
const heading = headingIndex === -1 ? void 0 : lines[headingIndex].match(/^#[ \t]+(.*)$/);
|
|
181
|
+
if (!heading) {
|
|
182
|
+
return { title: "", body: lines.join("\n").trim() };
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
title: heading[1].trim(),
|
|
186
|
+
body: lines.slice(headingIndex + 1).join("\n").trim()
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function renderIssueMarkdown(title, body) {
|
|
190
|
+
return `# ${title}
|
|
191
|
+
|
|
192
|
+
${body.trim()}
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
var LocalFileIssueProvider = class {
|
|
196
|
+
name = "local";
|
|
197
|
+
configuredRoot;
|
|
198
|
+
cachedRoot;
|
|
199
|
+
constructor(projectRoot) {
|
|
200
|
+
this.configuredRoot = projectRoot;
|
|
201
|
+
}
|
|
202
|
+
/** True whenever the project directory exists and is writable. Never throws. */
|
|
203
|
+
async isAvailable() {
|
|
204
|
+
try {
|
|
205
|
+
await access(await this.root(), constants.W_OK);
|
|
206
|
+
return true;
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async get(id) {
|
|
212
|
+
const issueId = normalizeId2(id);
|
|
213
|
+
const markdownPath = await this.issueFile(issueId);
|
|
214
|
+
let raw;
|
|
215
|
+
try {
|
|
216
|
+
raw = await readFile(markdownPath, "utf-8");
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (isNotFound(err)) return null;
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
const { title: headingTitle, body } = parseIssueMarkdown(raw);
|
|
222
|
+
const metadata = await this.readMetadata(issueId);
|
|
223
|
+
const title = headingTitle || metadata?.title || "";
|
|
224
|
+
if (metadata) {
|
|
225
|
+
return {
|
|
226
|
+
id: metadata.id,
|
|
227
|
+
number: metadata.number,
|
|
228
|
+
title,
|
|
229
|
+
body,
|
|
230
|
+
labels: metadata.labels,
|
|
231
|
+
state: metadata.state,
|
|
232
|
+
source: "local",
|
|
233
|
+
remoteRef: metadata.remote?.ref ?? null,
|
|
234
|
+
createdAt: metadata.createdAt,
|
|
235
|
+
updatedAt: metadata.updatedAt,
|
|
236
|
+
contentHash: hashIssueContent(title, body),
|
|
237
|
+
raw: metadata
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const stats = await stat(markdownPath);
|
|
241
|
+
return {
|
|
242
|
+
id: issueId,
|
|
243
|
+
number: toNumber(issueId),
|
|
244
|
+
title,
|
|
245
|
+
body,
|
|
246
|
+
labels: [],
|
|
247
|
+
state: "open",
|
|
248
|
+
source: "local",
|
|
249
|
+
remoteRef: null,
|
|
250
|
+
createdAt: stats.birthtime.toISOString(),
|
|
251
|
+
updatedAt: stats.mtime.toISOString(),
|
|
252
|
+
contentHash: hashIssueContent(title, body)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async create(draft) {
|
|
256
|
+
const id = draft.id === void 0 ? String(await this.allocateNumber()) : normalizeId2(draft.id);
|
|
257
|
+
const dir = await this.issueDir(id);
|
|
258
|
+
await mkdir(dir, { recursive: true });
|
|
259
|
+
try {
|
|
260
|
+
await writeFile(join(dir, "issue.md"), renderIssueMarkdown(draft.title, draft.body), {
|
|
261
|
+
encoding: "utf-8",
|
|
262
|
+
flag: "wx"
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err.code === "EEXIST") {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Local issue '${id}' already exists at ${join(ISSUES_DIR, id, "issue.md")}. Remove it or pick another identifier before creating a new Issue.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
const timestamp = isoNow();
|
|
273
|
+
const metadata = {
|
|
274
|
+
schemaVersion: 1,
|
|
275
|
+
id,
|
|
276
|
+
number: toNumber(id),
|
|
277
|
+
source: "local",
|
|
278
|
+
title: draft.title,
|
|
279
|
+
labels: draft.labels,
|
|
280
|
+
state: "open",
|
|
281
|
+
createdAt: timestamp,
|
|
282
|
+
updatedAt: timestamp,
|
|
283
|
+
contentHash: hashIssueContent(draft.title, draft.body),
|
|
284
|
+
...draft.remote ? { remote: draft.remote } : {}
|
|
285
|
+
};
|
|
286
|
+
await this.writeMetadata(id, metadata);
|
|
287
|
+
return {
|
|
288
|
+
id,
|
|
289
|
+
number: metadata.number,
|
|
290
|
+
title: draft.title,
|
|
291
|
+
body: draft.body.trim(),
|
|
292
|
+
labels: draft.labels,
|
|
293
|
+
state: "open",
|
|
294
|
+
source: "local",
|
|
295
|
+
remoteRef: draft.remote?.ref ?? null,
|
|
296
|
+
createdAt: timestamp,
|
|
297
|
+
updatedAt: timestamp,
|
|
298
|
+
contentHash: metadata.contentHash,
|
|
299
|
+
raw: metadata
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async close(id) {
|
|
303
|
+
const issue = await this.get(id);
|
|
304
|
+
if (!issue) {
|
|
305
|
+
throw new Error(`Local issue '${normalizeId2(id)}' not found under ${ISSUES_DIR}/`);
|
|
306
|
+
}
|
|
307
|
+
const issueId = normalizeId2(id);
|
|
308
|
+
const existing = await this.readMetadata(issueId);
|
|
309
|
+
await this.writeMetadata(issueId, {
|
|
310
|
+
schemaVersion: 1,
|
|
311
|
+
id: issue.id,
|
|
312
|
+
number: issue.number,
|
|
313
|
+
source: existing?.source ?? "local",
|
|
314
|
+
title: issue.title,
|
|
315
|
+
labels: issue.labels,
|
|
316
|
+
state: "closed",
|
|
317
|
+
createdAt: issue.createdAt,
|
|
318
|
+
updatedAt: isoNow(),
|
|
319
|
+
contentHash: issue.contentHash,
|
|
320
|
+
...existing?.remote ? { remote: existing.remote } : {}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Next free identifier: above every local number and, when GitHub answers,
|
|
325
|
+
* above every remote one too. Sharing a numbering space with the remote is
|
|
326
|
+
* what makes a naive `localMax + 1` collide the moment someone opens an Issue
|
|
327
|
+
* on GitHub.
|
|
328
|
+
*/
|
|
329
|
+
async allocateNumber() {
|
|
330
|
+
const [local, remote] = await Promise.all([this.highestLocalNumber(), highestRemoteNumber()]);
|
|
331
|
+
return Math.max(local, remote) + 1;
|
|
332
|
+
}
|
|
333
|
+
async root() {
|
|
334
|
+
if (this.configuredRoot !== void 0) return this.configuredRoot;
|
|
335
|
+
this.cachedRoot ??= await getProjectRoot();
|
|
336
|
+
return this.cachedRoot;
|
|
337
|
+
}
|
|
338
|
+
async issueDir(id) {
|
|
339
|
+
return join(await this.root(), ISSUES_DIR, id);
|
|
340
|
+
}
|
|
341
|
+
async issueFile(id) {
|
|
342
|
+
return join(await this.issueDir(id), "issue.md");
|
|
343
|
+
}
|
|
344
|
+
async metadataFile(id) {
|
|
345
|
+
return join(await this.issueDir(id), "metadata.json");
|
|
346
|
+
}
|
|
347
|
+
/** Parsed metadata.json, or `null` when the file is absent. */
|
|
348
|
+
async readMetadata(id) {
|
|
349
|
+
const path = await this.metadataFile(id);
|
|
350
|
+
const relative = join(ISSUES_DIR, id, "metadata.json");
|
|
351
|
+
let raw;
|
|
352
|
+
try {
|
|
353
|
+
raw = await readFile(path, "utf-8");
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (isNotFound(err)) return null;
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
let parsed;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(raw);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
throw new Error(`Invalid JSON in ${relative}: ${err.message}`);
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
return issueMetadataSchema.parse(parsed);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
if (err instanceof ZodError) {
|
|
368
|
+
const details = err.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
369
|
+
throw new Error(`Invalid issue metadata at ${relative}:
|
|
370
|
+
${details}`);
|
|
371
|
+
}
|
|
372
|
+
throw err;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async writeMetadata(id, metadata) {
|
|
376
|
+
await writeFileAtomic(await this.metadataFile(id), `${JSON.stringify(metadata, null, 2)}
|
|
377
|
+
`);
|
|
378
|
+
}
|
|
379
|
+
/** Highest number across every issues/<id>/metadata.json and numeric directory name. */
|
|
380
|
+
async highestLocalNumber() {
|
|
381
|
+
const dir = join(await this.root(), ISSUES_DIR);
|
|
382
|
+
let entries;
|
|
383
|
+
try {
|
|
384
|
+
entries = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (isNotFound(err)) return 0;
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
const numbers = await Promise.all(
|
|
390
|
+
entries.map(async (name) => {
|
|
391
|
+
const fromName = toNumber(name) ?? 0;
|
|
392
|
+
const fromMetadata = await this.readMetadata(name).then((metadata) => metadata?.number ?? 0).catch(() => 0);
|
|
393
|
+
return Math.max(fromName, fromMetadata);
|
|
394
|
+
})
|
|
395
|
+
);
|
|
396
|
+
return numbers.reduce((max, value) => Math.max(max, value), 0);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
var REMOTE_PROBE_SAMPLE_SIZE = 20;
|
|
400
|
+
async function highestRemoteNumber() {
|
|
401
|
+
const probes = [
|
|
402
|
+
["issue", "list"],
|
|
403
|
+
["pr", "list"]
|
|
404
|
+
];
|
|
405
|
+
const results = await Promise.all(
|
|
406
|
+
probes.map(async (args) => {
|
|
407
|
+
try {
|
|
408
|
+
const result = await run(
|
|
409
|
+
"gh",
|
|
410
|
+
[
|
|
411
|
+
...args,
|
|
412
|
+
"--state",
|
|
413
|
+
"all",
|
|
414
|
+
"--limit",
|
|
415
|
+
String(REMOTE_PROBE_SAMPLE_SIZE),
|
|
416
|
+
"--json",
|
|
417
|
+
"number"
|
|
418
|
+
],
|
|
419
|
+
{ timeout: REMOTE_PROBE_TIMEOUT_MS }
|
|
420
|
+
);
|
|
421
|
+
if (result.exitCode !== 0) return 0;
|
|
422
|
+
const parsed = JSON.parse(result.stdout);
|
|
423
|
+
if (!Array.isArray(parsed)) return 0;
|
|
424
|
+
return parsed.reduce((max, entry) => {
|
|
425
|
+
const n = entry?.number;
|
|
426
|
+
return typeof n === "number" ? Math.max(max, n) : max;
|
|
427
|
+
}, 0);
|
|
428
|
+
} catch {
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
return results.reduce((max, value) => Math.max(max, value), 0);
|
|
434
|
+
}
|
|
435
|
+
var localFileIssueProvider = new LocalFileIssueProvider();
|
|
436
|
+
|
|
437
|
+
// src/issues/bootstrap.ts
|
|
438
|
+
function ensureProvidersRegistered() {
|
|
439
|
+
const registered = new Set(getRegisteredSources());
|
|
440
|
+
if (!registered.has("github")) {
|
|
441
|
+
registerProvider(githubIssueProvider);
|
|
442
|
+
}
|
|
443
|
+
if (!registered.has("local")) {
|
|
444
|
+
registerProvider(localFileIssueProvider);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/issues/resolver.ts
|
|
449
|
+
import { createInterface } from "readline";
|
|
450
|
+
var IssueResolutionError = class extends Error {
|
|
451
|
+
exitCode;
|
|
452
|
+
constructor(message, exitCode = 1) {
|
|
453
|
+
super(message);
|
|
454
|
+
this.name = "IssueResolutionError";
|
|
455
|
+
this.exitCode = exitCode;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
459
|
+
var SOURCE_LABELS = {
|
|
460
|
+
github: "GitHub",
|
|
461
|
+
local: "local"
|
|
462
|
+
};
|
|
463
|
+
function sourceLabel(source) {
|
|
464
|
+
return SOURCE_LABELS[source] ?? source;
|
|
465
|
+
}
|
|
466
|
+
function promptLabel(source) {
|
|
467
|
+
const label = sourceLabel(source);
|
|
468
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
469
|
+
}
|
|
470
|
+
function errorMessage(err) {
|
|
471
|
+
return err instanceof Error ? err.message : String(err);
|
|
472
|
+
}
|
|
473
|
+
async function fetchCandidate(source, id, warn) {
|
|
474
|
+
let provider;
|
|
475
|
+
try {
|
|
476
|
+
provider = getProvider(source);
|
|
477
|
+
} catch (err) {
|
|
478
|
+
return { source, issue: null, reason: errorMessage(err) };
|
|
479
|
+
}
|
|
480
|
+
let available;
|
|
481
|
+
try {
|
|
482
|
+
available = await provider.isAvailable();
|
|
483
|
+
} catch (err) {
|
|
484
|
+
return { source, issue: null, reason: `provider unavailable (${errorMessage(err)})` };
|
|
485
|
+
}
|
|
486
|
+
if (!available) {
|
|
487
|
+
return { source, issue: null, reason: "provider unavailable" };
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const issue = await provider.get(id);
|
|
491
|
+
return { source, issue, reason: issue === null ? "not found" : null };
|
|
492
|
+
} catch (err) {
|
|
493
|
+
const reason = errorMessage(err);
|
|
494
|
+
warn(`Could not read issue '${id}' from ${sourceLabel(source)}: ${reason}`);
|
|
495
|
+
return { source, issue: null, reason };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function shortHash(hash) {
|
|
499
|
+
const hex = hash.startsWith("sha256:") ? hash.slice("sha256:".length) : hash;
|
|
500
|
+
return hex.slice(0, 12);
|
|
501
|
+
}
|
|
502
|
+
function describeCandidate(source, issue) {
|
|
503
|
+
const lines = issue.body.length === 0 ? 0 : issue.body.split("\n").length;
|
|
504
|
+
return ` ${sourceLabel(source).padEnd(6)} title: "${issue.title}" | body: ${lines} line(s), ${issue.body.length} char(s) | updated: ${issue.updatedAt} | hash: ${shortHash(issue.contentHash)}`;
|
|
505
|
+
}
|
|
506
|
+
function listSources(candidates) {
|
|
507
|
+
const labels = candidates.map((candidate) => sourceLabel(candidate.source));
|
|
508
|
+
if (labels.length <= 1) {
|
|
509
|
+
return labels.join("");
|
|
510
|
+
}
|
|
511
|
+
return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
|
|
512
|
+
}
|
|
513
|
+
function createLineReader(rl, output) {
|
|
514
|
+
const queued = [];
|
|
515
|
+
let pending = null;
|
|
516
|
+
let closed = false;
|
|
517
|
+
rl.on("line", (line) => {
|
|
518
|
+
if (pending !== null) {
|
|
519
|
+
const resolve = pending;
|
|
520
|
+
pending = null;
|
|
521
|
+
resolve(line);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
queued.push(line);
|
|
525
|
+
});
|
|
526
|
+
rl.on("close", () => {
|
|
527
|
+
closed = true;
|
|
528
|
+
if (pending !== null) {
|
|
529
|
+
const resolve = pending;
|
|
530
|
+
pending = null;
|
|
531
|
+
resolve(null);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
return (query) => new Promise((resolve) => {
|
|
535
|
+
const buffered = queued.shift();
|
|
536
|
+
if (buffered !== void 0) {
|
|
537
|
+
resolve(buffered);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (closed) {
|
|
541
|
+
resolve(null);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
output.write(query);
|
|
545
|
+
pending = resolve;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
async function promptChoice(sources, stdin, stdout, warn) {
|
|
549
|
+
const options = sources.map((source, index) => `[${index + 1}] ${promptLabel(source)}`);
|
|
550
|
+
const cancelOption = sources.length + 1;
|
|
551
|
+
const query = `Which version should be used? ${options.join(" ")} [${cancelOption}] Cancel: `;
|
|
552
|
+
const numbers = Array.from({ length: cancelOption }, (_, index) => String(index + 1));
|
|
553
|
+
const accepted = `${numbers.slice(0, -1).join(", ")} or ${numbers[numbers.length - 1]}`;
|
|
554
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
555
|
+
const ask = createLineReader(rl, stdout);
|
|
556
|
+
try {
|
|
557
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {
|
|
558
|
+
const answer = await ask(query);
|
|
559
|
+
if (answer === null) {
|
|
560
|
+
return "cancel";
|
|
561
|
+
}
|
|
562
|
+
const choice = answer.trim();
|
|
563
|
+
const picked = sources[Number(choice) - 1];
|
|
564
|
+
if (choice.length > 0 && picked !== void 0) {
|
|
565
|
+
return picked;
|
|
566
|
+
}
|
|
567
|
+
if (choice === String(cancelOption)) {
|
|
568
|
+
return "cancel";
|
|
569
|
+
}
|
|
570
|
+
warn(`Invalid choice: '${choice}'. Enter ${accepted}.`);
|
|
571
|
+
}
|
|
572
|
+
return "cancel";
|
|
573
|
+
} finally {
|
|
574
|
+
rl.close();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function isInteractiveByDefault() {
|
|
578
|
+
const ci = process.env.CI;
|
|
579
|
+
const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
|
|
580
|
+
return Boolean(process.stdin.isTTY) && !inCi;
|
|
581
|
+
}
|
|
582
|
+
function buildResolved(issue, source, local, github, divergent) {
|
|
583
|
+
return { issue, source, local, github, divergent };
|
|
584
|
+
}
|
|
585
|
+
function policyTarget(policy) {
|
|
586
|
+
if (policy === "prefer-local") {
|
|
587
|
+
return "local";
|
|
588
|
+
}
|
|
589
|
+
if (policy === "prefer-github") {
|
|
590
|
+
return "github";
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
async function resolveIssue(id, opts = {}) {
|
|
595
|
+
const info = opts.info ?? printInfo;
|
|
596
|
+
const warn = opts.warn ?? printWarning;
|
|
597
|
+
const config = opts.config ?? await loadIssuesConfig({ warn });
|
|
598
|
+
ensureProvidersRegistered();
|
|
599
|
+
const sources = opts.sources ?? getRegisteredSources();
|
|
600
|
+
const candidates = await Promise.all(
|
|
601
|
+
sources.map((source) => fetchCandidate(source, id, warn))
|
|
602
|
+
);
|
|
603
|
+
const issueOf = (source) => candidates.find((candidate) => candidate.source === source)?.issue ?? null;
|
|
604
|
+
const local = issueOf("local");
|
|
605
|
+
const github = issueOf("github");
|
|
606
|
+
const found = candidates.filter(
|
|
607
|
+
(candidate) => candidate.issue !== null
|
|
608
|
+
);
|
|
609
|
+
if (found.length === 0) {
|
|
610
|
+
const details = candidates.map((candidate) => `${sourceLabel(candidate.source)}: ${candidate.reason ?? "not found"}`).join("; ");
|
|
611
|
+
const where = details.length > 0 ? ` (${details})` : "";
|
|
612
|
+
throw new IssueResolutionError(`Issue '${id}' not found in any registered origin${where}`);
|
|
613
|
+
}
|
|
614
|
+
const build = (candidate, divergent) => buildResolved(candidate.issue, candidate.source, local, github, divergent);
|
|
615
|
+
const firstFound = found[0];
|
|
616
|
+
if (found.length === 1) {
|
|
617
|
+
return build(firstFound, false);
|
|
618
|
+
}
|
|
619
|
+
const preferred = found.find((candidate) => candidate.source === config.preferredProvider) ?? firstFound;
|
|
620
|
+
const distinct = new Set(found.map((candidate) => candidate.issue.contentHash));
|
|
621
|
+
if (distinct.size === 1) {
|
|
622
|
+
info(
|
|
623
|
+
`Issue '${id}' has identical content in ${listSources(found)}; using ${sourceLabel(preferred.source)}.`
|
|
624
|
+
);
|
|
625
|
+
return build(preferred, false);
|
|
626
|
+
}
|
|
627
|
+
info(`Issue '${id}' differs between origins:`);
|
|
628
|
+
for (const candidate of found) {
|
|
629
|
+
info(describeCandidate(candidate.source, candidate.issue));
|
|
630
|
+
}
|
|
631
|
+
const target = policyTarget(config.conflictPolicy);
|
|
632
|
+
if (target !== null) {
|
|
633
|
+
const chosen = found.find((candidate) => candidate.source === target);
|
|
634
|
+
if (chosen !== void 0) {
|
|
635
|
+
info(`Conflict policy '${config.conflictPolicy}': using the ${sourceLabel(target)} version.`);
|
|
636
|
+
return build(chosen, true);
|
|
637
|
+
}
|
|
638
|
+
warn(
|
|
639
|
+
`Conflict policy '${config.conflictPolicy}' does not apply to Issue '${id}' (${sourceLabel(target)} has no version of it); using ${sourceLabel(preferred.source)}.`
|
|
640
|
+
);
|
|
641
|
+
return build(preferred, true);
|
|
642
|
+
}
|
|
643
|
+
const interactive = opts.interactive ?? isInteractiveByDefault();
|
|
644
|
+
if (!interactive) {
|
|
645
|
+
warn(
|
|
646
|
+
`Divergent Issue '${id}' and conflict policy 'ask' in a non-interactive environment; using the preferred provider (${sourceLabel(preferred.source)}).`
|
|
647
|
+
);
|
|
648
|
+
return build(preferred, true);
|
|
649
|
+
}
|
|
650
|
+
const choice = await promptChoice(
|
|
651
|
+
found.map((candidate) => candidate.source),
|
|
652
|
+
opts.stdin ?? process.stdin,
|
|
653
|
+
opts.stdout ?? process.stdout,
|
|
654
|
+
warn
|
|
655
|
+
);
|
|
656
|
+
if (choice === "cancel") {
|
|
657
|
+
throw new IssueResolutionError(
|
|
658
|
+
`Cancelled: Issue '${id}' diverges between ${listSources(found)}.`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const picked = found.find((candidate) => candidate.source === choice);
|
|
662
|
+
return build(picked, true);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/issues/context.ts
|
|
666
|
+
function localIssueRef(id) {
|
|
667
|
+
return `issues/${id}/issue.md`;
|
|
668
|
+
}
|
|
669
|
+
function issueReference(issue) {
|
|
670
|
+
return issue.remoteRef ?? localIssueRef(issue.id);
|
|
671
|
+
}
|
|
672
|
+
var NO_LABELS = "(none)";
|
|
673
|
+
function issuePlaceholders(resolved) {
|
|
674
|
+
const { issue } = resolved;
|
|
675
|
+
return {
|
|
676
|
+
__ISSUE_TITLE__: issue.title,
|
|
677
|
+
__ISSUE_BODY__: issue.body,
|
|
678
|
+
__ISSUE_LABELS__: issue.labels.length > 0 ? issue.labels.join(", ") : NO_LABELS,
|
|
679
|
+
__ISSUE_SOURCE__: resolved.source,
|
|
680
|
+
__ISSUE_URL__: issueReference(issue)
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async function resolveCommandIssue(id, preResolved, options) {
|
|
684
|
+
if (preResolved !== void 0) {
|
|
685
|
+
return { ok: true, resolved: preResolved };
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
return { ok: true, resolved: await resolveIssue(id, options) };
|
|
689
|
+
} catch (err) {
|
|
690
|
+
if (err instanceof IssueResolutionError) {
|
|
691
|
+
printError(err.message);
|
|
692
|
+
return { ok: false, code: err.exitCode };
|
|
693
|
+
}
|
|
694
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
695
|
+
printError(`Could not resolve issue '${id}': ${message}`);
|
|
696
|
+
return { ok: false, code: 1 };
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export {
|
|
701
|
+
getProvider,
|
|
702
|
+
ensureProvidersRegistered,
|
|
703
|
+
localIssueRef,
|
|
704
|
+
issuePlaceholders,
|
|
705
|
+
resolveCommandIssue
|
|
706
|
+
};
|
|
707
|
+
//# sourceMappingURL=chunk-HS4QWZAO.js.map
|