issue-flow 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +14 -8
  2. package/dist/{analyze-WRFFG2R5.js → analyze-XMEULTT2.js} +11 -11
  3. package/dist/analyze-XMEULTT2.js.map +1 -0
  4. package/dist/{chunk-EWMSQQL2.js → chunk-6UBA7CSJ.js} +22 -19
  5. package/dist/chunk-6UBA7CSJ.js.map +1 -0
  6. package/dist/{chunk-BTBSVBE3.js → chunk-CYU6RSDJ.js} +15 -12
  7. package/dist/chunk-CYU6RSDJ.js.map +1 -0
  8. package/dist/{chunk-HS4QWZAO.js → chunk-DKMBLNGO.js} +71 -22
  9. package/dist/chunk-DKMBLNGO.js.map +1 -0
  10. package/dist/{chunk-JV7P6UGG.js → chunk-EHVKXFFN.js} +2 -2
  11. package/dist/chunk-G3TJHMAS.js +759 -0
  12. package/dist/chunk-G3TJHMAS.js.map +1 -0
  13. package/dist/{chunk-KUDAY57D.js → chunk-GOZT2YP5.js} +19 -16
  14. package/dist/chunk-GOZT2YP5.js.map +1 -0
  15. package/dist/{chunk-2Y3UO7H2.js → chunk-JNEHVQH5.js} +13 -1
  16. package/dist/chunk-JNEHVQH5.js.map +1 -0
  17. package/dist/{chunk-Z3UIGLZG.js → chunk-MWUF4LMF.js} +17 -9
  18. package/dist/chunk-MWUF4LMF.js.map +1 -0
  19. package/dist/{chunk-CW6UE3NW.js → chunk-PRVWJZV4.js} +3 -3
  20. package/dist/{chunk-UX6YAVLT.js → chunk-RGCU2KE6.js} +10 -10
  21. package/dist/chunk-RGCU2KE6.js.map +1 -0
  22. package/dist/{chunk-YXJPKBPV.js → chunk-WQ4SUZ4P.js} +10 -9
  23. package/dist/chunk-WQ4SUZ4P.js.map +1 -0
  24. package/dist/{chunk-75K5IKAC.js → chunk-WUITOU5B.js} +12 -12
  25. package/dist/chunk-WUITOU5B.js.map +1 -0
  26. package/dist/{chunk-VLSZFMLZ.js → chunk-YLXPLHEL.js} +2 -2
  27. package/dist/cli.js +13 -13
  28. package/dist/{config-UHQ6XXX2.js → config-2NDV7Y72.js} +9 -3
  29. package/dist/execute-BOQPTRH4.js +13 -0
  30. package/dist/{generate-DUKQCADD.js → generate-YEQRH36H.js} +11 -7
  31. package/dist/generate-YEQRH36H.js.map +1 -0
  32. package/dist/init-5KXCG36P.js +9 -0
  33. package/dist/plan-4I47CQM5.js +15 -0
  34. package/dist/pr-F672VPMM.js +15 -0
  35. package/dist/pr-review-GKP4TJRJ.js +15 -0
  36. package/dist/prd-3QME4DYX.js +15 -0
  37. package/dist/review-RG2UIPOB.js +13 -0
  38. package/dist/{run-VC647DQI.js → run-EFXDXSB4.js} +29 -24
  39. package/dist/run-EFXDXSB4.js.map +1 -0
  40. package/package.json +1 -1
  41. package/prompts/execute.md +17 -5
  42. package/prompts/generate.md +1 -1
  43. package/prompts/plan.md +1 -0
  44. package/prompts/prd.md +1 -1
  45. package/prompts/review.md +2 -0
  46. package/dist/analyze-WRFFG2R5.js.map +0 -1
  47. package/dist/chunk-2Y3UO7H2.js.map +0 -1
  48. package/dist/chunk-75K5IKAC.js.map +0 -1
  49. package/dist/chunk-BTBSVBE3.js.map +0 -1
  50. package/dist/chunk-EWMSQQL2.js.map +0 -1
  51. package/dist/chunk-HS4QWZAO.js.map +0 -1
  52. package/dist/chunk-KUDAY57D.js.map +0 -1
  53. package/dist/chunk-OVFYJLOH.js +0 -329
  54. package/dist/chunk-OVFYJLOH.js.map +0 -1
  55. package/dist/chunk-UX6YAVLT.js.map +0 -1
  56. package/dist/chunk-YXJPKBPV.js.map +0 -1
  57. package/dist/chunk-Z3UIGLZG.js.map +0 -1
  58. package/dist/execute-K5B4UMUT.js +0 -13
  59. package/dist/generate-DUKQCADD.js.map +0 -1
  60. package/dist/init-W4HBLW7R.js +0 -9
  61. package/dist/plan-NKPU45NP.js +0 -15
  62. package/dist/pr-JYGLLIEF.js +0 -15
  63. package/dist/pr-review-6H6F4XUY.js +0 -15
  64. package/dist/prd-FC2ZCEOX.js +0 -15
  65. package/dist/review-X4NGDZZI.js +0 -13
  66. package/dist/run-VC647DQI.js.map +0 -1
  67. /package/dist/{chunk-JV7P6UGG.js.map → chunk-EHVKXFFN.js.map} +0 -0
  68. /package/dist/{chunk-CW6UE3NW.js.map → chunk-PRVWJZV4.js.map} +0 -0
  69. /package/dist/{chunk-VLSZFMLZ.js.map → chunk-YLXPLHEL.js.map} +0 -0
  70. /package/dist/{config-UHQ6XXX2.js.map → config-2NDV7Y72.js.map} +0 -0
  71. /package/dist/{execute-K5B4UMUT.js.map → execute-BOQPTRH4.js.map} +0 -0
  72. /package/dist/{init-W4HBLW7R.js.map → init-5KXCG36P.js.map} +0 -0
  73. /package/dist/{plan-NKPU45NP.js.map → plan-4I47CQM5.js.map} +0 -0
  74. /package/dist/{pr-JYGLLIEF.js.map → pr-F672VPMM.js.map} +0 -0
  75. /package/dist/{pr-review-6H6F4XUY.js.map → pr-review-GKP4TJRJ.js.map} +0 -0
  76. /package/dist/{prd-FC2ZCEOX.js.map → prd-3QME4DYX.js.map} +0 -0
  77. /package/dist/{review-X4NGDZZI.js.map → review-RG2UIPOB.js.map} +0 -0
@@ -0,0 +1,759 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isoNow,
4
+ issuesConfigSchema,
5
+ prReviewConfigSchema,
6
+ printInfo,
7
+ printWarning,
8
+ webConfigSchema,
9
+ writeFileAtomic
10
+ } from "./chunk-JNEHVQH5.js";
11
+
12
+ // src/config.ts
13
+ import { readFile as readFile2 } from "fs/promises";
14
+ import { platform } from "os";
15
+ import { join as join4 } from "path";
16
+
17
+ // src/storage/paths.ts
18
+ import { createHash } from "crypto";
19
+ import { homedir } from "os";
20
+ import { basename, join, resolve } from "path";
21
+
22
+ // src/utils/shell.ts
23
+ import { execa } from "execa";
24
+ async function run(command, args = [], options) {
25
+ const result = await execa(command, args, {
26
+ reject: false,
27
+ ...options
28
+ });
29
+ return {
30
+ stdout: result.stdout?.toString() ?? "",
31
+ stderr: result.stderr?.toString() ?? "",
32
+ exitCode: result.exitCode ?? 1
33
+ };
34
+ }
35
+
36
+ // src/utils/git.ts
37
+ async function getProjectRoot() {
38
+ const result = await run("git", ["rev-parse", "--show-toplevel"]);
39
+ if (result.exitCode !== 0) {
40
+ throw new Error(
41
+ "Not inside a git repository. Please run issue-flow from within a git project."
42
+ );
43
+ }
44
+ return result.stdout.trim();
45
+ }
46
+ async function getCurrentBranch() {
47
+ const result = await run("git", ["branch", "--show-current"]);
48
+ if (result.exitCode !== 0) {
49
+ throw new Error(
50
+ "Failed to detect git branch. Ensure git is installed and you are inside a repository."
51
+ );
52
+ }
53
+ return result.stdout.trim();
54
+ }
55
+ async function getBaseBranch() {
56
+ const remoteHead = await run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
57
+ if (remoteHead.exitCode === 0) {
58
+ const name = remoteHead.stdout.trim().replace(/^origin\//, "");
59
+ if (name) return name;
60
+ }
61
+ for (const candidate of ["main", "master"]) {
62
+ const check = await run("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`]);
63
+ if (check.exitCode === 0) return candidate;
64
+ }
65
+ return "main";
66
+ }
67
+ async function getRemoteUrl(cwd) {
68
+ let result;
69
+ try {
70
+ result = await run("git", ["remote", "get-url", "origin"], { cwd });
71
+ } catch {
72
+ return null;
73
+ }
74
+ if (result.exitCode !== 0) return null;
75
+ const url = result.stdout.trim();
76
+ return url === "" ? null : url;
77
+ }
78
+ function normalizeRemoteUrl(url) {
79
+ if (typeof url !== "string") return null;
80
+ const trimmed = url.trim();
81
+ if (trimmed === "") return null;
82
+ let authority;
83
+ let path;
84
+ const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(trimmed);
85
+ if (scheme) {
86
+ const rest = trimmed.slice(scheme[0].length);
87
+ const slash = rest.indexOf("/");
88
+ if (slash === -1) return null;
89
+ authority = rest.slice(0, slash);
90
+ path = rest.slice(slash + 1);
91
+ } else {
92
+ const colon = trimmed.indexOf(":");
93
+ if (colon === -1) return null;
94
+ authority = trimmed.slice(0, colon);
95
+ path = trimmed.slice(colon + 1);
96
+ }
97
+ const at = authority.lastIndexOf("@");
98
+ if (at !== -1) authority = authority.slice(at + 1);
99
+ const host = authority.replace(/:\d+$/, "").toLowerCase();
100
+ if (host === "") return null;
101
+ const normalizedPath = path.replace(/[?#].*$/, "").replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
102
+ if (normalizedPath === "") return null;
103
+ return `${host}/${normalizedPath}`;
104
+ }
105
+ async function getCommitsSince(base) {
106
+ const result = await run("git", ["log", "--pretty=format:%h%x09%s", `${base}..HEAD`]);
107
+ if (result.exitCode !== 0) return [];
108
+ return result.stdout.split("\n").filter((line) => line.trim() !== "").map((line) => {
109
+ const tab = line.indexOf(" ");
110
+ return tab === -1 ? { hash: line.trim(), subject: "" } : { hash: line.slice(0, tab), subject: line.slice(tab + 1) };
111
+ });
112
+ }
113
+
114
+ // src/storage/paths.ts
115
+ var GLOBAL_DIR_NAME = ".issue-flow";
116
+ var GLOBAL_ROOT_ENV = "ISSUE_FLOW_HOME";
117
+ var PROJECTS_DIR_NAME = "projects";
118
+ var ISSUES_DIR_NAME = "issues";
119
+ function getGlobalRoot(options = {}) {
120
+ const env = options.env ?? process.env;
121
+ const override = env[GLOBAL_ROOT_ENV]?.trim();
122
+ if (override) {
123
+ return resolve(override);
124
+ }
125
+ let home;
126
+ try {
127
+ home = homedir();
128
+ } catch {
129
+ home = "";
130
+ }
131
+ if (home.trim() === "") {
132
+ throw new Error(
133
+ `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`
134
+ );
135
+ }
136
+ return join(home, GLOBAL_DIR_NAME);
137
+ }
138
+ var SLUG_MAX_LENGTH = 32;
139
+ var HASH_LENGTH = 12;
140
+ var FALLBACK_SLUG = "project";
141
+ function slugify(name) {
142
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
143
+ return slug === "" ? FALLBACK_SLUG : slug;
144
+ }
145
+ function projectIdFromRemote(remote, projectRoot) {
146
+ let seed;
147
+ let name;
148
+ if (remote) {
149
+ seed = `remote:${remote}`;
150
+ name = remote.split("/").pop() ?? "";
151
+ } else {
152
+ const absolute = resolve(projectRoot);
153
+ seed = `path:${absolute}`;
154
+ name = basename(absolute);
155
+ }
156
+ const hash = createHash("sha256").update(seed).digest("hex").slice(0, HASH_LENGTH);
157
+ return `${slugify(name)}-${hash}`;
158
+ }
159
+ function getProjectDir(projectId, options = {}) {
160
+ return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);
161
+ }
162
+ function normalizeIssueNumber(issueNumber) {
163
+ const normalized = String(issueNumber).trim().replace(/^#/, "");
164
+ if (normalized.length === 0) {
165
+ throw new Error("Issue identifier cannot be empty");
166
+ }
167
+ if (/[/\\]/.test(normalized) || normalized === "." || normalized === "..") {
168
+ throw new Error(`Invalid issue identifier: '${issueNumber}'`);
169
+ }
170
+ return normalized;
171
+ }
172
+ function getIssuePaths(projectId, issueNumber, options = {}) {
173
+ const issueDir = join(
174
+ getProjectDir(projectId, options),
175
+ ISSUES_DIR_NAME,
176
+ normalizeIssueNumber(issueNumber)
177
+ );
178
+ return {
179
+ issueDir,
180
+ issueFile: join(issueDir, "issue.md"),
181
+ metadataFile: join(issueDir, "metadata.json"),
182
+ prdFile: join(issueDir, "prd.md"),
183
+ tasksFile: join(issueDir, "tasks.json"),
184
+ progressFile: join(issueDir, "progress.txt"),
185
+ analysisFile: join(issueDir, "analysis.md"),
186
+ sessionFile: join(issueDir, "session.json"),
187
+ lastBranchFile: join(issueDir, ".last-branch"),
188
+ archiveDir: join(issueDir, "archive"),
189
+ prReviewDir: join(issueDir, "pr-review")
190
+ };
191
+ }
192
+
193
+ // src/storage/resolve.ts
194
+ import { stat as stat2 } from "fs/promises";
195
+ import { basename as basename2, join as join3, resolve as resolve3 } from "path";
196
+
197
+ // src/storage/compat.ts
198
+ import { copyFile, mkdir, readdir, readFile, stat } from "fs/promises";
199
+ import { join as join2, resolve as resolve2, sep } from "path";
200
+
201
+ // src/storage/schemas.ts
202
+ import { z } from "zod";
203
+ var STORAGE_SCHEMA_VERSION = 1;
204
+ var projectMetadataSchema = z.object({
205
+ schemaVersion: z.number().int().positive(),
206
+ projectId: z.string().min(1),
207
+ root: z.string().min(1),
208
+ /** Normalized remote (`host/org/repo`), or null when the project has none. */
209
+ remoteUrl: z.string().nullable(),
210
+ createdAt: z.string().min(1),
211
+ updatedAt: z.string().min(1),
212
+ /** Null until the project is used by a pipeline run. */
213
+ lastAttemptAt: z.string().nullable()
214
+ });
215
+ var webShape = webConfigSchema.shape;
216
+ var globalWebConfigSchema = z.object({
217
+ port: webShape.port.unwrap(),
218
+ host: webShape.host.unwrap(),
219
+ refreshSeconds: webShape.refreshSeconds.unwrap(),
220
+ logLimit: webShape.logLimit.unwrap()
221
+ }).partial();
222
+ var globalRetryConfigSchema = z.object({
223
+ retryLimit: z.number().int().nonnegative(),
224
+ retryForever: z.boolean(),
225
+ backoffBaseSeconds: z.number().nonnegative(),
226
+ backoffMaxSeconds: z.number().nonnegative()
227
+ }).partial();
228
+ var globalCommitConfigSchema = z.object({
229
+ signoff: z.boolean(),
230
+ conventional: z.boolean()
231
+ }).partial();
232
+ var globalConfigSchema = z.object({
233
+ schemaVersion: z.number().int().positive(),
234
+ /** Override for the directory holding `projects/`. */
235
+ storageDir: z.string().min(1),
236
+ web: globalWebConfigSchema,
237
+ retry: globalRetryConfigSchema,
238
+ commit: globalCommitConfigSchema
239
+ }).partial();
240
+
241
+ // src/storage/compat.ts
242
+ var LEGACY_ISSUES_DIR_NAME = "issues";
243
+ var METADATA_FILENAME = "metadata.json";
244
+ async function directoryExists(path) {
245
+ try {
246
+ return (await stat(path)).isDirectory();
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+ async function pathExists(path) {
252
+ try {
253
+ await stat(path);
254
+ return true;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ async function resolveStorageMode(projectRoot, options = {}) {
260
+ const remoteUrl = normalizeRemoteUrl(await getRemoteUrl(projectRoot));
261
+ const projectId = projectIdFromRemote(remoteUrl, projectRoot);
262
+ const globalDir = getProjectDir(projectId, options);
263
+ const legacyDir = join2(resolve2(projectRoot), LEGACY_ISSUES_DIR_NAME);
264
+ const [globalExists, legacyExists] = await Promise.all([
265
+ directoryExists(globalDir),
266
+ directoryExists(legacyDir)
267
+ ]);
268
+ return {
269
+ // Only one state calls for work: legacy content that has nowhere to be read
270
+ // from yet. Everything else — including "neither exists" — is already the
271
+ // global layout.
272
+ mode: legacyExists && !globalExists ? "needs-migration" : "global",
273
+ projectId,
274
+ globalDir,
275
+ legacyDir,
276
+ globalExists,
277
+ legacyExists,
278
+ remoteUrl
279
+ };
280
+ }
281
+ function migrationFailure(issue, relativePath, err) {
282
+ const reason = err instanceof Error ? err.message : String(err);
283
+ return new Error(
284
+ `Failed to migrate '${relativePath}' (issue '${issue}'): ${reason}. The legacy directory was left untouched \u2014 fix the cause and run the migration again.`
285
+ );
286
+ }
287
+ function issueOf(relativePath) {
288
+ return relativePath.split(sep)[0] || ".";
289
+ }
290
+ async function copyTree(sourceRoot, targetRoot, relativeDir, result) {
291
+ const source = join2(sourceRoot, relativeDir);
292
+ const target = join2(targetRoot, relativeDir);
293
+ let entries;
294
+ try {
295
+ entries = await readdir(source, { withFileTypes: true });
296
+ } catch (err) {
297
+ throw migrationFailure(issueOf(relativeDir), relativeDir || ".", err);
298
+ }
299
+ try {
300
+ await mkdir(target, { recursive: true });
301
+ } catch (err) {
302
+ throw migrationFailure(issueOf(relativeDir), relativeDir || ".", err);
303
+ }
304
+ for (const entry of entries) {
305
+ const relativePath = join2(relativeDir, entry.name);
306
+ if (entry.isDirectory()) {
307
+ await copyTree(sourceRoot, targetRoot, relativePath, result);
308
+ continue;
309
+ }
310
+ if (!entry.isFile()) {
311
+ continue;
312
+ }
313
+ const to = join2(targetRoot, relativePath);
314
+ try {
315
+ if (await pathExists(to)) {
316
+ result.skipped.push(relativePath);
317
+ continue;
318
+ }
319
+ await copyFile(join2(sourceRoot, relativePath), to);
320
+ result.copied.push(relativePath);
321
+ } catch (err) {
322
+ throw migrationFailure(issueOf(relativePath), relativePath, err);
323
+ }
324
+ }
325
+ }
326
+ async function readExistingMetadata(file) {
327
+ let raw;
328
+ try {
329
+ raw = await readFile(file, "utf-8");
330
+ } catch {
331
+ return null;
332
+ }
333
+ try {
334
+ const parsed = projectMetadataSchema.safeParse(JSON.parse(raw));
335
+ return parsed.success ? parsed.data : null;
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+ async function migrateLegacyStorage(projectRoot, options = {}) {
341
+ const status = await resolveStorageMode(projectRoot, { env: options.env });
342
+ const result = { copied: [], skipped: [] };
343
+ try {
344
+ await mkdir(status.globalDir, { recursive: true });
345
+ } catch (err) {
346
+ throw migrationFailure(".", status.globalDir, err);
347
+ }
348
+ if (status.legacyExists) {
349
+ await copyTree(status.legacyDir, join2(status.globalDir, ISSUES_DIR_NAME), "", result);
350
+ }
351
+ const now = (options.now ?? isoNow)();
352
+ const metadataFile = join2(status.globalDir, METADATA_FILENAME);
353
+ const existing = await readExistingMetadata(metadataFile);
354
+ const metadata = {
355
+ schemaVersion: STORAGE_SCHEMA_VERSION,
356
+ projectId: status.projectId,
357
+ root: resolve2(projectRoot),
358
+ remoteUrl: status.remoteUrl,
359
+ // The project was first seen whenever it was first written, not now.
360
+ createdAt: existing?.createdAt ?? now,
361
+ updatedAt: now,
362
+ lastAttemptAt: existing?.lastAttemptAt ?? null
363
+ };
364
+ await writeFileAtomic(metadataFile, `${JSON.stringify(metadata, null, 2)}
365
+ `);
366
+ return {
367
+ projectId: status.projectId,
368
+ globalDir: status.globalDir,
369
+ legacyDir: status.legacyDir,
370
+ previousMode: status.mode,
371
+ ...result,
372
+ metadataFile,
373
+ metadata
374
+ };
375
+ }
376
+
377
+ // src/storage/resolve.ts
378
+ var projectCache = /* @__PURE__ */ new Map();
379
+ var checkedIssues = /* @__PURE__ */ new Set();
380
+ async function directoryExists2(path) {
381
+ try {
382
+ return (await stat2(path)).isDirectory();
383
+ } catch {
384
+ return false;
385
+ }
386
+ }
387
+ function announceMigration(result) {
388
+ if (result.copied.length === 0) return;
389
+ const count = result.copied.length;
390
+ const target = join3(result.globalDir, ISSUES_DIR_NAME);
391
+ printInfo(
392
+ `Migrated ${count} file${count === 1 ? "" : "s"} from ${result.legacyDir} to ${target}`
393
+ );
394
+ printInfo(
395
+ `The legacy directory was not modified or removed \u2014 ${result.legacyDir} is kept as-is, read-only, for compatibility.`
396
+ );
397
+ }
398
+ async function resolveProject(projectRoot, options) {
399
+ const status = await resolveStorageMode(projectRoot, options);
400
+ if (status.mode === "needs-migration") {
401
+ announceMigration(await migrateLegacyStorage(projectRoot, options));
402
+ }
403
+ return {
404
+ projectId: status.projectId,
405
+ legacyDir: status.legacyDir
406
+ };
407
+ }
408
+ function getProjectResolution(projectRoot, options) {
409
+ const key = `${getGlobalRoot(options)}::${projectRoot}`;
410
+ const cached = projectCache.get(key);
411
+ if (cached) return cached;
412
+ const pending = resolveProject(projectRoot, options).catch((err) => {
413
+ projectCache.delete(key);
414
+ throw err;
415
+ });
416
+ projectCache.set(key, pending);
417
+ return pending;
418
+ }
419
+ async function resolveProjectPaths(options = {}) {
420
+ const { projectRoot: projectRootOption, ...rootOptions } = options;
421
+ const projectRoot = resolve3(projectRootOption ?? await getProjectRoot());
422
+ const project = await getProjectResolution(projectRoot, rootOptions);
423
+ const projectDir = getProjectDir(project.projectId, rootOptions);
424
+ return {
425
+ projectId: project.projectId,
426
+ projectDir,
427
+ issuesDir: join3(projectDir, ISSUES_DIR_NAME)
428
+ };
429
+ }
430
+ async function resolveIssuePaths(issueNumber, options = {}) {
431
+ const { projectRoot: projectRootOption, ...rootOptions } = options;
432
+ const projectRoot = resolve3(projectRootOption ?? await getProjectRoot());
433
+ const project = await getProjectResolution(projectRoot, rootOptions);
434
+ const paths = getIssuePaths(project.projectId, issueNumber, rootOptions);
435
+ if (!checkedIssues.has(paths.issueDir)) {
436
+ const legacyIssueDir = join3(project.legacyDir, basename2(paths.issueDir));
437
+ if (!await directoryExists2(paths.issueDir) && await directoryExists2(legacyIssueDir)) {
438
+ announceMigration(await migrateLegacyStorage(projectRoot, rootOptions));
439
+ }
440
+ checkedIssues.add(paths.issueDir);
441
+ }
442
+ return paths;
443
+ }
444
+
445
+ // src/config.ts
446
+ var DEFAULTS = {
447
+ retryLimit: 10,
448
+ retryForever: false,
449
+ backoffBaseSeconds: 30,
450
+ backoffMaxSeconds: 900
451
+ };
452
+ function createConfig(options) {
453
+ return {
454
+ issueNumber: options.issueNumber,
455
+ maxIterations: options.maxIterations,
456
+ retryLimit: options.retryLimit ?? DEFAULTS.retryLimit,
457
+ retryForever: options.retryForever ?? DEFAULTS.retryForever,
458
+ backoffBaseSeconds: options.backoffBaseSeconds ?? DEFAULTS.backoffBaseSeconds,
459
+ backoffMaxSeconds: options.backoffMaxSeconds ?? DEFAULTS.backoffMaxSeconds
460
+ };
461
+ }
462
+ async function resolvePaths(config, scriptDir) {
463
+ const projectRoot = await getProjectRoot();
464
+ if (config.issueNumber) {
465
+ const issuePaths = await resolveIssuePaths(config.issueNumber, { projectRoot });
466
+ return {
467
+ prdFile: issuePaths.tasksFile,
468
+ progressFile: issuePaths.progressFile,
469
+ archiveDir: issuePaths.archiveDir,
470
+ lastBranchFile: issuePaths.lastBranchFile,
471
+ projectRoot
472
+ };
473
+ }
474
+ const base = scriptDir ?? projectRoot;
475
+ return {
476
+ prdFile: join4(base, "prd.json"),
477
+ progressFile: join4(base, "progress.txt"),
478
+ archiveDir: join4(base, "archive"),
479
+ lastBranchFile: join4(base, ".last-branch"),
480
+ projectRoot
481
+ };
482
+ }
483
+ function getInstallHint(pkg) {
484
+ const os = platform();
485
+ if (os === "darwin") {
486
+ return `brew install ${pkg}`;
487
+ }
488
+ if (os === "linux") {
489
+ return `apt install ${pkg} (or your distro's package manager)`;
490
+ }
491
+ if (os === "win32") {
492
+ return `winget install ${pkg} (or choco install ${pkg})`;
493
+ }
494
+ return `install ${pkg} using your system package manager`;
495
+ }
496
+ async function validateDependencies() {
497
+ const errors = [];
498
+ const gitResult = await run("git", ["--version"]);
499
+ if (gitResult.exitCode !== 0) {
500
+ errors.push(` - git (install with: ${getInstallHint("git")})`);
501
+ }
502
+ const claudeResult = await run("claude", ["--version"]);
503
+ if (claudeResult.exitCode !== 0) {
504
+ errors.push(" - claude (install with: npm install -g @anthropic-ai/claude-code)");
505
+ }
506
+ return errors;
507
+ }
508
+ var PROJECT_CONFIG_FILENAME = ".issue-flow.json";
509
+ var WEB_CONFIG_FILENAME = PROJECT_CONFIG_FILENAME;
510
+ async function readProjectConfigFile(projectRoot, warn) {
511
+ let root = projectRoot;
512
+ if (root === void 0) {
513
+ try {
514
+ root = await getProjectRoot();
515
+ } catch {
516
+ return null;
517
+ }
518
+ }
519
+ const filePath = join4(root, PROJECT_CONFIG_FILENAME);
520
+ let raw;
521
+ try {
522
+ raw = await readFile2(filePath, "utf-8");
523
+ } catch {
524
+ return null;
525
+ }
526
+ let parsed;
527
+ try {
528
+ parsed = JSON.parse(raw);
529
+ } catch {
530
+ warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: invalid JSON.`);
531
+ return null;
532
+ }
533
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
534
+ warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: expected a JSON object.`);
535
+ return null;
536
+ }
537
+ return parsed;
538
+ }
539
+ var GLOBAL_CONFIG_FILENAME = "config.json";
540
+ async function loadGlobalConfig(options = {}) {
541
+ const warn = options.warn ?? printWarning;
542
+ let root;
543
+ try {
544
+ root = options.globalRoot ?? getGlobalRoot({ env: options.env ?? process.env });
545
+ } catch (err) {
546
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${err.message}`);
547
+ return {};
548
+ }
549
+ const filePath = join4(root, GLOBAL_CONFIG_FILENAME);
550
+ let raw;
551
+ try {
552
+ raw = await readFile2(filePath, "utf-8");
553
+ } catch (err) {
554
+ if (err.code !== "ENOENT") {
555
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${err.message}`);
556
+ }
557
+ return {};
558
+ }
559
+ let parsed;
560
+ try {
561
+ parsed = JSON.parse(raw);
562
+ } catch {
563
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: invalid JSON.`);
564
+ return {};
565
+ }
566
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
567
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: expected a JSON object.`);
568
+ return {};
569
+ }
570
+ const config = {};
571
+ for (const [key, value] of Object.entries(parsed)) {
572
+ const result = globalConfigSchema.safeParse({ [key]: value });
573
+ if (!result.success) {
574
+ warn(
575
+ `Ignoring "${key}" key of ${GLOBAL_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? "invalid value"}.`
576
+ );
577
+ continue;
578
+ }
579
+ Object.assign(config, result.data);
580
+ }
581
+ return config;
582
+ }
583
+ function mergeConfigLayers(layers) {
584
+ const merged = {};
585
+ for (const layer of [layers.defaults, layers.global, layers.project, layers.env, layers.cli]) {
586
+ for (const [key, value] of Object.entries(layer ?? {})) {
587
+ if (value !== void 0) {
588
+ merged[key] = value;
589
+ }
590
+ }
591
+ }
592
+ return merged;
593
+ }
594
+ var webCliOverrides = {};
595
+ function setWebCliOverrides(overrides) {
596
+ webCliOverrides = overrides;
597
+ }
598
+ var FALSY_ENV_VALUES = /* @__PURE__ */ new Set(["", "0", "false", "no", "off"]);
599
+ function parseBooleanEnv(value) {
600
+ return !FALSY_ENV_VALUES.has(value.trim().toLowerCase());
601
+ }
602
+ function readNumberEnv(env, name, warn) {
603
+ const raw = env[name];
604
+ if (raw === void 0) {
605
+ return void 0;
606
+ }
607
+ const parsed = Number(raw);
608
+ if (!Number.isFinite(parsed)) {
609
+ warn(`Ignoring ${name}="${raw}": not a number.`);
610
+ return void 0;
611
+ }
612
+ return parsed;
613
+ }
614
+ function readWebConfigEnv(env, warn) {
615
+ const layer = {};
616
+ if (env.ISSUE_FLOW_WEB !== void 0) {
617
+ layer.enabled = parseBooleanEnv(env.ISSUE_FLOW_WEB);
618
+ }
619
+ const port = readNumberEnv(env, "ISSUE_FLOW_WEB_PORT", warn);
620
+ if (port !== void 0) {
621
+ layer.port = port;
622
+ }
623
+ if (env.ISSUE_FLOW_WEB_HOST !== void 0) {
624
+ layer.host = env.ISSUE_FLOW_WEB_HOST;
625
+ }
626
+ const refresh = readNumberEnv(env, "ISSUE_FLOW_WEB_REFRESH", warn);
627
+ if (refresh !== void 0) {
628
+ layer.refreshSeconds = refresh;
629
+ }
630
+ const logLimit = readNumberEnv(env, "ISSUE_FLOW_WEB_LOG_LIMIT", warn);
631
+ if (logLimit !== void 0) {
632
+ layer.logLimit = logLimit;
633
+ }
634
+ return layer;
635
+ }
636
+ async function readWebConfigFile(projectRoot, warn) {
637
+ const file = await readProjectConfigFile(projectRoot, warn);
638
+ const web = file?.web;
639
+ if (web === void 0) {
640
+ return {};
641
+ }
642
+ const result = webConfigSchema.partial().safeParse(web);
643
+ if (!result.success) {
644
+ warn(
645
+ `Ignoring "web" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? "invalid value"}.`
646
+ );
647
+ return {};
648
+ }
649
+ return result.data;
650
+ }
651
+ async function loadWebConfig(options = {}) {
652
+ const warn = options.warn ?? printWarning;
653
+ const cli = options.cli ?? webCliOverrides;
654
+ const env = options.env ?? process.env;
655
+ const fileLayer = await readWebConfigFile(options.projectRoot, warn);
656
+ const envLayer = readWebConfigEnv(env, warn);
657
+ const merged = { ...fileLayer, ...envLayer, ...cli };
658
+ const result = webConfigSchema.safeParse(merged);
659
+ if (result.success) {
660
+ return result.data;
661
+ }
662
+ warn(
663
+ `Invalid web monitoring configuration (${result.error.issues[0]?.message ?? "invalid value"}); using defaults.`
664
+ );
665
+ return webConfigSchema.parse({});
666
+ }
667
+ var issuesCliOverrides = {};
668
+ function setIssuesCliOverrides(overrides) {
669
+ issuesCliOverrides = overrides;
670
+ }
671
+ async function readIssuesConfigFile(projectRoot, warn) {
672
+ const file = await readProjectConfigFile(projectRoot, warn);
673
+ const issues = file?.issues;
674
+ if (issues === void 0) {
675
+ return {};
676
+ }
677
+ const result = issuesConfigSchema.partial().safeParse(issues);
678
+ if (!result.success) {
679
+ warn(
680
+ `Ignoring "issues" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? "invalid value"}.`
681
+ );
682
+ return {};
683
+ }
684
+ return result.data;
685
+ }
686
+ async function loadIssuesConfig(options = {}) {
687
+ const warn = options.warn ?? printWarning;
688
+ const cli = options.cli ?? issuesCliOverrides;
689
+ const fileLayer = await readIssuesConfigFile(options.projectRoot, warn);
690
+ const merged = { ...fileLayer, ...cli };
691
+ const result = issuesConfigSchema.safeParse(merged);
692
+ if (result.success) {
693
+ return result.data;
694
+ }
695
+ warn(
696
+ `Invalid Issue provider configuration (${result.error.issues[0]?.message ?? "invalid value"}); using defaults.`
697
+ );
698
+ return issuesConfigSchema.parse({});
699
+ }
700
+ function readPrReviewConfigEnv(env) {
701
+ const publisher = env.ISSUE_FLOW_PR_REVIEW_PUBLISHER;
702
+ return publisher === void 0 ? {} : { publisher };
703
+ }
704
+ async function readPrReviewConfigFile(projectRoot, warn) {
705
+ const file = await readProjectConfigFile(projectRoot, warn);
706
+ const prReview = file?.prReview;
707
+ if (prReview === void 0) {
708
+ return {};
709
+ }
710
+ const result = prReviewConfigSchema.partial().safeParse(prReview);
711
+ if (!result.success) {
712
+ warn(
713
+ `Ignoring "prReview" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? "invalid value"}.`
714
+ );
715
+ return {};
716
+ }
717
+ return result.data;
718
+ }
719
+ async function loadPrReviewConfig(options = {}) {
720
+ const warn = options.warn ?? printWarning;
721
+ const env = options.env ?? process.env;
722
+ const fileLayer = await readPrReviewConfigFile(options.projectRoot, warn);
723
+ const envLayer = readPrReviewConfigEnv(env);
724
+ const merged = { ...fileLayer, ...envLayer, ...options.cli ?? {} };
725
+ const result = prReviewConfigSchema.safeParse(merged);
726
+ if (result.success) {
727
+ return result.data;
728
+ }
729
+ warn(
730
+ `Invalid PR review configuration (${result.error.issues[0]?.message ?? "invalid value"}); using defaults.`
731
+ );
732
+ return prReviewConfigSchema.parse({});
733
+ }
734
+
735
+ export {
736
+ run,
737
+ getProjectRoot,
738
+ getCurrentBranch,
739
+ getBaseBranch,
740
+ getCommitsSince,
741
+ resolveProjectPaths,
742
+ resolveIssuePaths,
743
+ DEFAULTS,
744
+ createConfig,
745
+ resolvePaths,
746
+ getInstallHint,
747
+ validateDependencies,
748
+ PROJECT_CONFIG_FILENAME,
749
+ WEB_CONFIG_FILENAME,
750
+ GLOBAL_CONFIG_FILENAME,
751
+ loadGlobalConfig,
752
+ mergeConfigLayers,
753
+ setWebCliOverrides,
754
+ loadWebConfig,
755
+ setIssuesCliOverrides,
756
+ loadIssuesConfig,
757
+ loadPrReviewConfig
758
+ };
759
+ //# sourceMappingURL=chunk-G3TJHMAS.js.map