issue-flow 0.14.0 → 0.15.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.
Files changed (142) hide show
  1. package/dist/{agent-OX3KPHE4.js → agent-AT64RXTA.js} +11 -11
  2. package/dist/{analyze-WIND2SEB.js → analyze-O2PYDIFK.js} +18 -18
  3. package/dist/{apply-JNOI2LR4.js → apply-SSU6QCD4.js} +11 -11
  4. package/dist/{bench-B4VZON7E.js → bench-ODFPAGW6.js} +16 -16
  5. package/dist/{chunk-QD4VOILZ.js → chunk-2ZAM45PZ.js} +9 -9
  6. package/dist/{chunk-PBTA3QQX.js → chunk-4RXYFWP7.js} +3 -3
  7. package/dist/{chunk-CGKLE6WR.js → chunk-5ZKBIXET.js} +12 -12
  8. package/dist/{chunk-WB2X37MO.js → chunk-6E2PEBFT.js} +7 -7
  9. package/dist/{chunk-UJ3L2MS7.js → chunk-6FXOU2KR.js} +6 -6
  10. package/dist/{chunk-RVO7L4FX.js → chunk-6SRE4KD7.js} +4 -4
  11. package/dist/{chunk-KMRFRXIT.js → chunk-A35HE4V7.js} +3 -3
  12. package/dist/{chunk-6BENHR2I.js → chunk-ARWNYXFL.js} +12 -12
  13. package/dist/{chunk-3WFZETIM.js → chunk-AYYMW33T.js} +2 -2
  14. package/dist/{chunk-LFEQIZLB.js → chunk-BMHGRYL6.js} +396 -64
  15. package/dist/chunk-BMHGRYL6.js.map +1 -0
  16. package/dist/{chunk-PZTWU277.js → chunk-F4OK7KEP.js} +7 -7
  17. package/dist/{chunk-SZMHCEYV.js → chunk-FK4IDY6P.js} +9 -9
  18. package/dist/{chunk-BEZ5BA5U.js → chunk-GSZCQUCY.js} +2 -2
  19. package/dist/{chunk-X44FKNKP.js → chunk-HGSYD57B.js} +34 -30
  20. package/dist/chunk-HGSYD57B.js.map +1 -0
  21. package/dist/{chunk-2IQ6M4ZE.js → chunk-KAHQ6F5C.js} +11 -11
  22. package/dist/{chunk-F6TOYSYY.js → chunk-MEFY4B7T.js} +9 -9
  23. package/dist/{chunk-BRCNQXYQ.js → chunk-MGCOQU6N.js} +4 -4
  24. package/dist/{chunk-KT7JKOXB.js → chunk-MROS3SKL.js} +3 -3
  25. package/dist/{chunk-TYRNTSPJ.js → chunk-NVZMBXTE.js} +3 -3
  26. package/dist/{chunk-PHW2YV6N.js → chunk-PCN7ZQND.js} +5 -5
  27. package/dist/{chunk-76MMPJ53.js → chunk-QRCGW2ZO.js} +5 -5
  28. package/dist/{chunk-OR43364P.js → chunk-RPXDBX6R.js} +3 -3
  29. package/dist/{chunk-JN6D3MY6.js → chunk-RWEXK55D.js} +2 -2
  30. package/dist/{chunk-67YIU2VB.js → chunk-TBDP6QPJ.js} +4 -4
  31. package/dist/{chunk-G5EK2XRN.js → chunk-UNQYMR3H.js} +2 -2
  32. package/dist/{chunk-QK7WUO7A.js → chunk-VR34TXHF.js} +6 -6
  33. package/dist/{chunk-QJ5UZAH5.js → chunk-WM3Y7Q4A.js} +10 -10
  34. package/dist/{chunk-LBGHI4S2.js → chunk-XDKGA6UX.js} +4 -2
  35. package/dist/{chunk-LBGHI4S2.js.map → chunk-XDKGA6UX.js.map} +1 -1
  36. package/dist/{chunk-REO3PXXO.js → chunk-XG2NYDRA.js} +3 -3
  37. package/dist/{chunk-LEW4QM74.js → chunk-XLVGPXNL.js} +2 -2
  38. package/dist/{chunk-J3SOTUVI.js → chunk-XQHFC62U.js} +2 -2
  39. package/dist/{chunk-QZVKRDJY.js → chunk-XRLRKTQA.js} +10 -10
  40. package/dist/{chunk-B72RQZBV.js → chunk-ZFIXEYUJ.js} +21 -18
  41. package/dist/chunk-ZFIXEYUJ.js.map +1 -0
  42. package/dist/{chunk-V7MO5EDS.js → chunk-ZPIVCM7Q.js} +3 -3
  43. package/dist/{chunk-OOVG2KBY.js → chunk-ZXPASFHG.js} +2 -2
  44. package/dist/cli.js +56 -65
  45. package/dist/cli.js.map +1 -1
  46. package/dist/{config-HMFMCY2M.js → config-UMP6DBJB.js} +10 -10
  47. package/dist/{conventions-NJF2OPVQ.js → conventions-DKH7AE4T.js} +12 -12
  48. package/dist/{diagnostics-THH42ZMP.js → diagnostics-GOGCLTRS.js} +6 -6
  49. package/dist/execute-E3BRMDAQ.js +35 -0
  50. package/dist/{generate-FW5C6NP7.js → generate-AGBT7EHX.js} +18 -18
  51. package/dist/{git-W72QQ6GT.js → git-AC542GU2.js} +2 -2
  52. package/dist/init-LLBVJNPE.js +24 -0
  53. package/dist/{operations-BCFOBH42.js → operations-JQYK2EMT.js} +11 -11
  54. package/dist/{permissions-4AKNQZHK.js → permissions-6HL52K54.js} +3 -3
  55. package/dist/plan-HSLWDJEZ.js +34 -0
  56. package/dist/{policy-DT6Q6ZVM.js → policy-IMRXGG7X.js} +11 -11
  57. package/dist/pr-6TCZXG6K.js +39 -0
  58. package/dist/pr-review-6MWJO2NP.js +32 -0
  59. package/dist/prd-D6YDYAFX.js +33 -0
  60. package/dist/{ps-RFORTCUD.js → ps-KUUZ6TUL.js} +9 -9
  61. package/dist/{recorder-KCCPMKQG.js → recorder-BJ2KIAWT.js} +5 -5
  62. package/dist/{registry-GLPXOMFG.js → registry-SB7XTMGE.js} +5 -5
  63. package/dist/{resume-M5RLKP4I.js → resume-QNLQSCR5.js} +34 -34
  64. package/dist/review-3KJ4BH2V.js +35 -0
  65. package/dist/{routing-NNT5WKGX.js → routing-TN7GLMGU.js} +11 -11
  66. package/dist/run-5EHIFF2V.js +54 -0
  67. package/dist/{state-manager-GQH32C44.js → state-manager-ZEPWTCA6.js} +3 -3
  68. package/dist/{usage-V2CHE5PY.js → usage-QL563I7F.js} +9 -9
  69. package/dist/{web-EA3KGNFN.js → web-PPSMNTQ6.js} +18 -57
  70. package/dist/{web-EA3KGNFN.js.map → web-PPSMNTQ6.js.map} +1 -1
  71. package/package.json +1 -1
  72. package/web/AGENTS.md +4 -0
  73. package/web/public/app.js +29 -10
  74. package/dist/chunk-B72RQZBV.js.map +0 -1
  75. package/dist/chunk-LFEQIZLB.js.map +0 -1
  76. package/dist/chunk-X44FKNKP.js.map +0 -1
  77. package/dist/execute-KFNNZZHI.js +0 -35
  78. package/dist/init-HTN4V67Q.js +0 -24
  79. package/dist/plan-SIZLGA4V.js +0 -34
  80. package/dist/pr-OMWCGUOW.js +0 -39
  81. package/dist/pr-review-N24CZPKR.js +0 -32
  82. package/dist/prd-5OFTZAHJ.js +0 -33
  83. package/dist/review-YM4JAABY.js +0 -35
  84. package/dist/run-PZUKHPQK.js +0 -54
  85. /package/dist/{agent-OX3KPHE4.js.map → agent-AT64RXTA.js.map} +0 -0
  86. /package/dist/{analyze-WIND2SEB.js.map → analyze-O2PYDIFK.js.map} +0 -0
  87. /package/dist/{apply-JNOI2LR4.js.map → apply-SSU6QCD4.js.map} +0 -0
  88. /package/dist/{bench-B4VZON7E.js.map → bench-ODFPAGW6.js.map} +0 -0
  89. /package/dist/{chunk-QD4VOILZ.js.map → chunk-2ZAM45PZ.js.map} +0 -0
  90. /package/dist/{chunk-PBTA3QQX.js.map → chunk-4RXYFWP7.js.map} +0 -0
  91. /package/dist/{chunk-CGKLE6WR.js.map → chunk-5ZKBIXET.js.map} +0 -0
  92. /package/dist/{chunk-WB2X37MO.js.map → chunk-6E2PEBFT.js.map} +0 -0
  93. /package/dist/{chunk-UJ3L2MS7.js.map → chunk-6FXOU2KR.js.map} +0 -0
  94. /package/dist/{chunk-RVO7L4FX.js.map → chunk-6SRE4KD7.js.map} +0 -0
  95. /package/dist/{chunk-KMRFRXIT.js.map → chunk-A35HE4V7.js.map} +0 -0
  96. /package/dist/{chunk-6BENHR2I.js.map → chunk-ARWNYXFL.js.map} +0 -0
  97. /package/dist/{chunk-3WFZETIM.js.map → chunk-AYYMW33T.js.map} +0 -0
  98. /package/dist/{chunk-PZTWU277.js.map → chunk-F4OK7KEP.js.map} +0 -0
  99. /package/dist/{chunk-SZMHCEYV.js.map → chunk-FK4IDY6P.js.map} +0 -0
  100. /package/dist/{chunk-BEZ5BA5U.js.map → chunk-GSZCQUCY.js.map} +0 -0
  101. /package/dist/{chunk-2IQ6M4ZE.js.map → chunk-KAHQ6F5C.js.map} +0 -0
  102. /package/dist/{chunk-F6TOYSYY.js.map → chunk-MEFY4B7T.js.map} +0 -0
  103. /package/dist/{chunk-BRCNQXYQ.js.map → chunk-MGCOQU6N.js.map} +0 -0
  104. /package/dist/{chunk-KT7JKOXB.js.map → chunk-MROS3SKL.js.map} +0 -0
  105. /package/dist/{chunk-TYRNTSPJ.js.map → chunk-NVZMBXTE.js.map} +0 -0
  106. /package/dist/{chunk-PHW2YV6N.js.map → chunk-PCN7ZQND.js.map} +0 -0
  107. /package/dist/{chunk-76MMPJ53.js.map → chunk-QRCGW2ZO.js.map} +0 -0
  108. /package/dist/{chunk-OR43364P.js.map → chunk-RPXDBX6R.js.map} +0 -0
  109. /package/dist/{chunk-JN6D3MY6.js.map → chunk-RWEXK55D.js.map} +0 -0
  110. /package/dist/{chunk-67YIU2VB.js.map → chunk-TBDP6QPJ.js.map} +0 -0
  111. /package/dist/{chunk-G5EK2XRN.js.map → chunk-UNQYMR3H.js.map} +0 -0
  112. /package/dist/{chunk-QK7WUO7A.js.map → chunk-VR34TXHF.js.map} +0 -0
  113. /package/dist/{chunk-QJ5UZAH5.js.map → chunk-WM3Y7Q4A.js.map} +0 -0
  114. /package/dist/{chunk-REO3PXXO.js.map → chunk-XG2NYDRA.js.map} +0 -0
  115. /package/dist/{chunk-LEW4QM74.js.map → chunk-XLVGPXNL.js.map} +0 -0
  116. /package/dist/{chunk-J3SOTUVI.js.map → chunk-XQHFC62U.js.map} +0 -0
  117. /package/dist/{chunk-QZVKRDJY.js.map → chunk-XRLRKTQA.js.map} +0 -0
  118. /package/dist/{chunk-V7MO5EDS.js.map → chunk-ZPIVCM7Q.js.map} +0 -0
  119. /package/dist/{chunk-OOVG2KBY.js.map → chunk-ZXPASFHG.js.map} +0 -0
  120. /package/dist/{config-HMFMCY2M.js.map → config-UMP6DBJB.js.map} +0 -0
  121. /package/dist/{conventions-NJF2OPVQ.js.map → conventions-DKH7AE4T.js.map} +0 -0
  122. /package/dist/{diagnostics-THH42ZMP.js.map → diagnostics-GOGCLTRS.js.map} +0 -0
  123. /package/dist/{execute-KFNNZZHI.js.map → execute-E3BRMDAQ.js.map} +0 -0
  124. /package/dist/{generate-FW5C6NP7.js.map → generate-AGBT7EHX.js.map} +0 -0
  125. /package/dist/{git-W72QQ6GT.js.map → git-AC542GU2.js.map} +0 -0
  126. /package/dist/{init-HTN4V67Q.js.map → init-LLBVJNPE.js.map} +0 -0
  127. /package/dist/{operations-BCFOBH42.js.map → operations-JQYK2EMT.js.map} +0 -0
  128. /package/dist/{permissions-4AKNQZHK.js.map → permissions-6HL52K54.js.map} +0 -0
  129. /package/dist/{plan-SIZLGA4V.js.map → plan-HSLWDJEZ.js.map} +0 -0
  130. /package/dist/{policy-DT6Q6ZVM.js.map → policy-IMRXGG7X.js.map} +0 -0
  131. /package/dist/{pr-OMWCGUOW.js.map → pr-6TCZXG6K.js.map} +0 -0
  132. /package/dist/{pr-review-N24CZPKR.js.map → pr-review-6MWJO2NP.js.map} +0 -0
  133. /package/dist/{prd-5OFTZAHJ.js.map → prd-D6YDYAFX.js.map} +0 -0
  134. /package/dist/{ps-RFORTCUD.js.map → ps-KUUZ6TUL.js.map} +0 -0
  135. /package/dist/{recorder-KCCPMKQG.js.map → recorder-BJ2KIAWT.js.map} +0 -0
  136. /package/dist/{registry-GLPXOMFG.js.map → registry-SB7XTMGE.js.map} +0 -0
  137. /package/dist/{resume-M5RLKP4I.js.map → resume-QNLQSCR5.js.map} +0 -0
  138. /package/dist/{review-YM4JAABY.js.map → review-3KJ4BH2V.js.map} +0 -0
  139. /package/dist/{routing-NNT5WKGX.js.map → routing-TN7GLMGU.js.map} +0 -0
  140. /package/dist/{run-PZUKHPQK.js.map → run-5EHIFF2V.js.map} +0 -0
  141. /package/dist/{state-manager-GQH32C44.js.map → state-manager-ZEPWTCA6.js.map} +0 -0
  142. /package/dist/{usage-V2CHE5PY.js.map → usage-QL563I7F.js.map} +0 -0
@@ -1,41 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  writeAgentPreference
4
- } from "./chunk-UJ3L2MS7.js";
4
+ } from "./chunk-6FXOU2KR.js";
5
5
  import {
6
6
  resolvePackageDir
7
- } from "./chunk-KMRFRXIT.js";
7
+ } from "./chunk-A35HE4V7.js";
8
8
  import {
9
9
  printInfo,
10
10
  printWarning
11
- } from "./chunk-TYRNTSPJ.js";
11
+ } from "./chunk-NVZMBXTE.js";
12
12
  import {
13
13
  NullPublisher
14
14
  } from "./chunk-6QYMSNVA.js";
15
15
  import {
16
16
  readDiagnostics
17
- } from "./chunk-3WFZETIM.js";
17
+ } from "./chunk-AYYMW33T.js";
18
18
  import {
19
19
  getGlobalRoot
20
20
  } from "./chunk-HAKDLR6N.js";
21
21
  import {
22
22
  isoNow
23
- } from "./chunk-J3SOTUVI.js";
23
+ } from "./chunk-XQHFC62U.js";
24
24
  import {
25
25
  isAgentPhase,
26
26
  isAgentProviderId,
27
27
  isProcessAlive,
28
28
  webLockSchema
29
- } from "./chunk-LBGHI4S2.js";
29
+ } from "./chunk-XDKGA6UX.js";
30
30
 
31
31
  // src/web/lock.ts
32
32
  import { spawn as nodeSpawn } from "child_process";
33
- import { readFile as readFile2, rm, writeFile } from "fs/promises";
33
+ import { randomUUID as randomUUID2 } from "crypto";
34
+ import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
34
35
  import { join as join2 } from "path";
35
36
  import chalk2 from "chalk";
37
+ import { execa } from "execa";
36
38
 
37
39
  // src/web/server.ts
38
- import { createHash } from "crypto";
40
+ import { createHash, randomUUID } from "crypto";
39
41
  import { readFile } from "fs/promises";
40
42
  import { createServer } from "http";
41
43
  import { createRequire } from "module";
@@ -44,11 +46,13 @@ import chalk from "chalk";
44
46
  var require2 = createRequire(import.meta.url);
45
47
  var SESSION_LIST_DESCRIPTION_MAX = 280;
46
48
  function readPackageVersion() {
47
- try {
48
- return require2("../../package.json").version;
49
- } catch {
50
- return "0.0.0";
49
+ for (const candidate of ["../package.json", "../../package.json"]) {
50
+ try {
51
+ return require2(candidate).version;
52
+ } catch {
53
+ }
51
54
  }
55
+ return "0.0.0";
52
56
  }
53
57
  function truncateSessionDescription(text, max = SESSION_LIST_DESCRIPTION_MAX) {
54
58
  if (text === null || text === void 0) return null;
@@ -99,8 +103,9 @@ async function loadStaticAssets(publicDir) {
99
103
  }
100
104
  return assets;
101
105
  }
102
- function baseHeaders(res) {
106
+ function baseHeaders(res, instanceId) {
103
107
  res.setHeader("Cache-Control", "no-store");
108
+ res.setHeader("X-Issue-Flow-Instance", instanceId);
104
109
  res.setHeader("X-Content-Type-Options", "nosniff");
105
110
  res.setHeader("Referrer-Policy", "no-referrer");
106
111
  res.setHeader("X-Robots-Tag", "noindex, nofollow");
@@ -131,6 +136,7 @@ async function startWebServer(options) {
131
136
  const info = options.info ?? printInfo;
132
137
  const warn = options.warn ?? printWarning;
133
138
  const version = options.version ?? readPackageVersion();
139
+ const instanceId = options.instanceId ?? randomUUID();
134
140
  const startedAtMs = Date.now();
135
141
  const assets = await loadStaticAssets(
136
142
  options.publicDir ?? resolvePackageDir(join("web", "public"))
@@ -148,7 +154,7 @@ async function startWebServer(options) {
148
154
  return entry;
149
155
  };
150
156
  const handleRequest = async (req, res) => {
151
- baseHeaders(res);
157
+ baseHeaders(res, instanceId);
152
158
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
153
159
  const path = requestUrl.pathname;
154
160
  if (req.method === "POST" && path === "/api/config/agent") {
@@ -290,6 +296,9 @@ async function startWebServer(options) {
290
296
  if (path === "/api/health") {
291
297
  respondJson(res, 200, {
292
298
  ok: true,
299
+ pid: process.pid,
300
+ instanceId,
301
+ startedAt: new Date(startedAtMs).toISOString(),
293
302
  uptime: Math.round((Date.now() - startedAtMs) / 1e3),
294
303
  version,
295
304
  refreshSeconds: options.refreshSeconds ?? 5,
@@ -358,7 +367,7 @@ async function startWebServer(options) {
358
367
  closePromise = doClose();
359
368
  return closePromise;
360
369
  };
361
- const handle = { server, host: options.host, port, url, close };
370
+ const handle = { server, host: options.host, port, url, instanceId, close };
362
371
  const onSignal = (signal) => {
363
372
  void handle.close().finally(() => {
364
373
  process.kill(process.pid, signal);
@@ -371,12 +380,16 @@ async function startWebServer(options) {
371
380
 
372
381
  // src/web/lock.ts
373
382
  var WEB_LOCK_FILENAME = "web.lock";
383
+ var WEB_RESTART_LOCK_FILENAME = "web.restart.lock";
374
384
  var HEALTH_PROBE_TIMEOUT_MS = 1e3;
375
385
  var LOCK_CLAIM_RETRIES = 5;
376
386
  var LOCK_CLAIM_RETRY_DELAY_MS = 100;
377
387
  function getWebLockFile(options = {}) {
378
388
  return join2(getGlobalRoot(options), WEB_LOCK_FILENAME);
379
389
  }
390
+ function getWebRestartLockFile(options = {}) {
391
+ return join2(getGlobalRoot(options), WEB_RESTART_LOCK_FILENAME);
392
+ }
380
393
  function sleep(ms) {
381
394
  return new Promise((resolve) => setTimeout(resolve, ms));
382
395
  }
@@ -389,11 +402,81 @@ async function probeHealth(host, port) {
389
402
  const res = await fetch(probeUrl(host, port), {
390
403
  signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS)
391
404
  });
392
- return res.ok;
405
+ if (!res.ok) return null;
406
+ const body = await res.json();
407
+ if (body.ok !== true) return null;
408
+ return {
409
+ ok: true,
410
+ ...typeof body.pid === "number" && Number.isInteger(body.pid) && body.pid > 0 ? { pid: body.pid } : {},
411
+ ...typeof body.instanceId === "string" && body.instanceId !== "" ? { instanceId: body.instanceId } : {},
412
+ ...typeof body.startedAt === "string" && body.startedAt !== "" ? { startedAt: body.startedAt } : {},
413
+ ...typeof body.uptime === "number" && Number.isFinite(body.uptime) ? { uptime: body.uptime } : {},
414
+ ...typeof body.version === "string" && body.version !== "" ? { version: body.version } : {}
415
+ };
393
416
  } catch {
394
- return false;
417
+ return null;
418
+ }
419
+ }
420
+ function sameLock(a, b) {
421
+ return a !== null && a.pid === b.pid && a.port === b.port && a.host === b.host && a.startedAt === b.startedAt && a.instanceId === b.instanceId;
422
+ }
423
+ async function removeWebLockIfOwned(lockFile, expected) {
424
+ if (sameLock(await readWebLock(lockFile), expected)) {
425
+ await removeWebLock(lockFile);
395
426
  }
396
427
  }
428
+ async function inspectProcessCommand(pid) {
429
+ const invocation = process.platform === "win32" ? {
430
+ command: "powershell.exe",
431
+ args: [
432
+ "-NoProfile",
433
+ "-NonInteractive",
434
+ "-Command",
435
+ `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`
436
+ ]
437
+ } : { command: "ps", args: ["-p", String(pid), "-o", "command="] };
438
+ try {
439
+ const result = await execa(invocation.command, invocation.args, { reject: false });
440
+ const command = result.stdout.trim();
441
+ return result.exitCode === 0 && command !== "" ? command : null;
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ async function findListeningProcess(port) {
447
+ try {
448
+ let pids;
449
+ if (process.platform === "win32") {
450
+ const result = await execa(
451
+ "powershell.exe",
452
+ [
453
+ "-NoProfile",
454
+ "-NonInteractive",
455
+ "-Command",
456
+ `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique) -join [Environment]::NewLine`
457
+ ],
458
+ { reject: false }
459
+ );
460
+ pids = result.stdout.split(/\s+/).map(Number).filter((pid2) => Number.isInteger(pid2) && pid2 > 0);
461
+ } else {
462
+ const result = await execa("lsof", ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"], {
463
+ reject: false
464
+ });
465
+ pids = result.stdout.split(/\s+/).map(Number).filter((pid2) => Number.isInteger(pid2) && pid2 > 0);
466
+ }
467
+ const unique = [...new Set(pids)];
468
+ if (unique.length !== 1) return null;
469
+ const pid = unique[0];
470
+ const command = await inspectProcessCommand(pid);
471
+ return command === null ? null : { pid, command };
472
+ } catch {
473
+ return null;
474
+ }
475
+ }
476
+ function isIssueFlowWebServe(owner, port) {
477
+ const escapedPort = String(port).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
478
+ return /(?:^|\s)web\s+serve(?:\s|$)/.test(owner.command) && new RegExp(`(?:^|\\s)--port(?:=|\\s+)${escapedPort}(?:\\s|$)`).test(owner.command) && /issue-flow|(?:^|[/\\])dist[/\\]cli\.js/.test(owner.command);
479
+ }
397
480
  async function readWebLock(lockFile) {
398
481
  let raw;
399
482
  try {
@@ -417,10 +500,11 @@ async function removeWebLock(lockFile) {
417
500
  async function detectActiveInstance(lockFile) {
418
501
  const lock = await readWebLock(lockFile);
419
502
  if (lock === null) return null;
420
- if (isProcessAlive(lock.pid) && await probeHealth(lock.host, lock.port)) {
503
+ const health = isProcessAlive(lock.pid) ? await probeHealth(lock.host, lock.port) : null;
504
+ if (health !== null && (lock.instanceId === void 0 || health.instanceId === lock.instanceId)) {
421
505
  return lock;
422
506
  }
423
- await removeWebLock(lockFile);
507
+ await removeWebLockIfOwned(lockFile, lock);
424
508
  return null;
425
509
  }
426
510
  async function claimWebLock(lockFile, lock) {
@@ -432,12 +516,74 @@ async function claimWebLock(lockFile, lock) {
432
516
  throw err;
433
517
  }
434
518
  }
519
+ async function readRestartLock(file) {
520
+ try {
521
+ const value = JSON.parse(await readFile2(file, "utf-8"));
522
+ return typeof value.pid === "number" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.operationId === "string" && value.operationId !== "" && typeof value.startedAt === "string" && value.startedAt !== "" ? { pid: value.pid, operationId: value.operationId, startedAt: value.startedAt } : null;
523
+ } catch {
524
+ return null;
525
+ }
526
+ }
527
+ async function removeRestartLockIfOwned(file, expected) {
528
+ const current = await readRestartLock(file);
529
+ if (current?.operationId === expected.operationId) {
530
+ await rm(file, { force: true }).catch(() => {
531
+ });
532
+ }
533
+ }
534
+ async function waitForRestart(file, timeoutMs) {
535
+ const deadline = Date.now() + timeoutMs;
536
+ while (Date.now() < deadline) {
537
+ const lock = await readRestartLock(file);
538
+ if (lock === null) {
539
+ return true;
540
+ }
541
+ if (!isProcessAlive(lock.pid)) {
542
+ await removeRestartLockIfOwned(file, lock);
543
+ return true;
544
+ }
545
+ await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
546
+ }
547
+ return false;
548
+ }
549
+ async function acquireRestartLock(file, timeoutMs) {
550
+ const deadline = Date.now() + timeoutMs;
551
+ let waited = false;
552
+ while (Date.now() < deadline) {
553
+ const lock = {
554
+ pid: process.pid,
555
+ operationId: randomUUID2(),
556
+ startedAt: isoNow()
557
+ };
558
+ try {
559
+ await writeFile(file, JSON.stringify(lock, null, 2), { encoding: "utf-8", flag: "wx" });
560
+ return {
561
+ lock,
562
+ waited,
563
+ release: () => removeRestartLockIfOwned(file, lock)
564
+ };
565
+ } catch (err) {
566
+ if (err.code !== "EEXIST") return null;
567
+ }
568
+ waited = true;
569
+ const existing = await readRestartLock(file);
570
+ if (existing === null || !isProcessAlive(existing.pid)) {
571
+ if (existing === null) await rm(file, { force: true }).catch(() => {
572
+ });
573
+ else await removeRestartLockIfOwned(file, existing);
574
+ continue;
575
+ }
576
+ await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
577
+ }
578
+ return null;
579
+ }
435
580
  function reusedHandle(lock) {
436
581
  const displayHost = lock.host === "0.0.0.0" ? "localhost" : lock.host;
437
582
  return {
438
583
  host: lock.host,
439
584
  port: lock.port,
440
585
  url: `http://${displayHost}:${lock.port}`,
586
+ instanceId: lock.instanceId ?? `legacy-${lock.pid}-${lock.startedAt}`,
441
587
  close: async () => {
442
588
  }
443
589
  };
@@ -463,7 +609,8 @@ async function ensureSingleWebServer(options, lockOptions = {}) {
463
609
  pid: process.pid,
464
610
  port: handle.port,
465
611
  host: handle.host,
466
- startedAt: isoNow()
612
+ startedAt: isoNow(),
613
+ instanceId: handle.instanceId
467
614
  });
468
615
  } catch (err) {
469
616
  await handle.close();
@@ -486,73 +633,258 @@ async function ensureSingleWebServer(options, lockOptions = {}) {
486
633
  }
487
634
  var DETACHED_CLAIM_TIMEOUT_MS = 5e3;
488
635
  var DETACHED_CLAIM_POLL_MS = 150;
636
+ function inferredStartedAt(health) {
637
+ if (health.startedAt !== void 0) return health.startedAt;
638
+ if (health.uptime !== void 0) {
639
+ return new Date(Date.now() - Math.max(0, health.uptime) * 1e3).toISOString();
640
+ }
641
+ return isoNow();
642
+ }
643
+ async function discoverManagedInstance(lockFile, options, monitorOptions) {
644
+ const existingLock = await readWebLock(lockFile);
645
+ if (existingLock !== null) {
646
+ const health2 = isProcessAlive(existingLock.pid) ? await probeHealth(existingLock.host, existingLock.port) : null;
647
+ if (health2 !== null && (existingLock.instanceId === void 0 || health2.instanceId === existingLock.instanceId)) {
648
+ return { instance: { lock: existingLock, health: health2, source: "lock" }, unownedMonitor: false };
649
+ }
650
+ await removeWebLockIfOwned(lockFile, existingLock);
651
+ }
652
+ const health = await probeHealth(options.host, options.port);
653
+ if (health === null) return { instance: null, unownedMonitor: false };
654
+ const inspect = monitorOptions.inspectProcess ?? inspectProcessCommand;
655
+ let owner = null;
656
+ if (health.pid !== void 0) {
657
+ const command = await inspect(health.pid);
658
+ if (command !== null) owner = { pid: health.pid, command };
659
+ }
660
+ owner ??= await (monitorOptions.findPortOwner ?? findListeningProcess)(options.port);
661
+ if (owner === null || !isIssueFlowWebServe(owner, options.port)) {
662
+ return { instance: null, unownedMonitor: true };
663
+ }
664
+ const recovered = {
665
+ pid: owner.pid,
666
+ port: options.port,
667
+ host: options.host,
668
+ startedAt: inferredStartedAt(health),
669
+ ...health.instanceId === void 0 ? {} : { instanceId: health.instanceId }
670
+ };
671
+ const claimed = await claimWebLock(lockFile, recovered).catch(() => false);
672
+ if (!claimed) {
673
+ const winner = await readWebLock(lockFile);
674
+ if (winner !== null) {
675
+ const winnerHealth = await probeHealth(winner.host, winner.port);
676
+ if (winnerHealth !== null) {
677
+ return {
678
+ instance: { lock: winner, health: winnerHealth, source: "lock" },
679
+ unownedMonitor: false
680
+ };
681
+ }
682
+ }
683
+ }
684
+ return { instance: { lock: recovered, health, source: "port" }, unownedMonitor: false };
685
+ }
686
+ async function stopManagedInstance(instance, lockFile, options, monitorOptions) {
687
+ const info = options.info ?? printInfo;
688
+ const warn = options.warn ?? printWarning;
689
+ const version = instance.health.version === void 0 ? "" : `, version ${instance.health.version}`;
690
+ info(`Web monitor: stopping previous instance (pid ${instance.lock.pid}${version}).`);
691
+ try {
692
+ (monitorOptions.kill ?? process.kill)(instance.lock.pid, "SIGTERM");
693
+ } catch (err) {
694
+ if (err.code === "ESRCH") {
695
+ await removeWebLockIfOwned(lockFile, instance.lock);
696
+ info(`Web monitor: previous pid ${instance.lock.pid} was already gone; stale lock removed.`);
697
+ return true;
698
+ }
699
+ warn(
700
+ `Web monitor: failed to stop pid ${instance.lock.pid} (${err instanceof Error ? err.message : String(err)}). Continuing the pipeline without restarting it.`
701
+ );
702
+ return false;
703
+ }
704
+ const isAlive = monitorOptions.isAlive ?? isProcessAlive;
705
+ const deadline = Date.now() + (monitorOptions.shutdownTimeoutMs ?? 5e3);
706
+ const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;
707
+ while (Date.now() < deadline) {
708
+ const alive = isAlive(instance.lock.pid);
709
+ const health = alive ? await probeHealth(instance.lock.host, instance.lock.port) : null;
710
+ if (!alive || health === null) {
711
+ await removeWebLockIfOwned(lockFile, instance.lock);
712
+ info(`Web monitor: previous instance stopped (pid ${instance.lock.pid}).`);
713
+ info(
714
+ "Web monitor: process-local static asset and ETag caches invalidated; no on-disk web build cache exists."
715
+ );
716
+ return true;
717
+ }
718
+ await sleep(pollMs);
719
+ }
720
+ warn(
721
+ `Web monitor: pid ${instance.lock.pid} did not confirm shutdown in time. Continuing the pipeline without starting a competing instance.`
722
+ );
723
+ return false;
724
+ }
725
+ async function stopWebMonitor(options, monitorOptions = {}) {
726
+ const info = options.info ?? printInfo;
727
+ const warn = options.warn ?? printWarning;
728
+ let lockFile;
729
+ let restartLockFile;
730
+ try {
731
+ await mkdir(getGlobalRoot(monitorOptions), { recursive: true });
732
+ lockFile = getWebLockFile(monitorOptions);
733
+ restartLockFile = getWebRestartLockFile(monitorOptions);
734
+ } catch (err) {
735
+ warn(
736
+ `Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}).`
737
+ );
738
+ return "failed";
739
+ }
740
+ const lease = await acquireRestartLock(
741
+ restartLockFile,
742
+ monitorOptions.restartLockTimeoutMs ?? 5e3
743
+ );
744
+ if (lease === null) {
745
+ warn("Web monitor: could not acquire the maintenance lock; no process was stopped.");
746
+ return "failed";
747
+ }
748
+ try {
749
+ const stale = await readWebLock(lockFile);
750
+ const discovered = await discoverManagedInstance(lockFile, options, monitorOptions);
751
+ if (discovered.instance === null) {
752
+ if (stale !== null && !isProcessAlive(stale.pid)) {
753
+ info(`Web monitor: stale lock for dead pid ${stale.pid} removed.`);
754
+ }
755
+ if (discovered.unownedMonitor) {
756
+ warn(
757
+ `Web monitor: port ${options.port} looks like an Issue Flow monitor, but its process ownership could not be verified; it was left untouched.`
758
+ );
759
+ return "unowned";
760
+ }
761
+ info("No web monitor is currently running.");
762
+ return "not-running";
763
+ }
764
+ return await stopManagedInstance(discovered.instance, lockFile, options, monitorOptions) ? "stopped" : "failed";
765
+ } finally {
766
+ await lease.release();
767
+ }
768
+ }
489
769
  async function ensureWebMonitor(options, monitorOptions = {}) {
490
770
  const warn = options.warn ?? printWarning;
491
771
  const info = options.info ?? printInfo;
492
772
  let lockFile;
773
+ let restartLockFile;
493
774
  try {
775
+ await mkdir(getGlobalRoot(monitorOptions), { recursive: true });
494
776
  lockFile = getWebLockFile(monitorOptions);
777
+ restartLockFile = getWebRestartLockFile(monitorOptions);
495
778
  } catch (err) {
496
779
  warn(
497
780
  `Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to legacy in-process mode.`
498
781
  );
499
782
  return startWebServer(options);
500
783
  }
501
- const active = await detectActiveInstance(lockFile);
502
- if (active !== null) {
503
- info(`Reusing existing web monitor at ${reusedHandle(active).url}`);
504
- return reusedHandle(active);
505
- }
506
- const execPath = monitorOptions.execPath ?? process.execPath;
507
- const entryScript = monitorOptions.entryScript ?? process.argv[1];
508
- if (entryScript === void 0) {
509
- warn("Web monitor: could not determine the CLI entry point to spawn a detached server.");
784
+ const coordinationTimeout = monitorOptions.restartLockTimeoutMs ?? 5e3;
785
+ const restartRequestedAt = Date.now();
786
+ const restartLease = monitorOptions.restart ? await acquireRestartLock(restartLockFile, coordinationTimeout) : null;
787
+ if (monitorOptions.restart && restartLease === null) {
788
+ warn(
789
+ "Web monitor: could not acquire the restart lock. Continuing the pipeline without changing the server."
790
+ );
510
791
  return null;
511
792
  }
512
- const args = [
513
- entryScript,
514
- "web",
515
- "serve",
516
- "--port",
517
- String(options.port),
518
- "--host",
519
- options.host
520
- ];
521
- if (options.refreshSeconds !== void 0) {
522
- args.push("--refresh", String(options.refreshSeconds));
523
- }
524
- const spawnFn = monitorOptions.spawn ?? nodeSpawn;
525
- try {
526
- const child = spawnFn(execPath, args, { detached: true, stdio: "ignore" });
527
- child.unref();
528
- } catch (err) {
793
+ if (!monitorOptions.restart && !await waitForRestart(restartLockFile, coordinationTimeout)) {
529
794
  warn(
530
- `Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`
795
+ "Web monitor: another restart is still in progress. Continuing the pipeline without starting a competing instance."
531
796
  );
532
797
  return null;
533
798
  }
534
- const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;
535
- const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;
536
- const deadline = Date.now() + timeoutMs;
537
- while (Date.now() < deadline) {
538
- const claimed = await detectActiveInstance(lockFile);
539
- if (claimed !== null) {
540
- info(`Web monitor running at ${chalk2.bold.cyan(reusedHandle(claimed).url)}`);
541
- return reusedHandle(claimed);
799
+ try {
800
+ const discovered = await discoverManagedInstance(lockFile, options, monitorOptions);
801
+ const active = discovered.instance;
802
+ if (monitorOptions.restart && restartLease?.waited === true && active !== null && Date.parse(active.lock.startedAt) >= restartRequestedAt) {
803
+ info(
804
+ `Web monitor: a concurrent restart already started pid ${active.lock.pid}; reusing ${reusedHandle(active.lock).url}`
805
+ );
806
+ return reusedHandle(active.lock);
542
807
  }
543
- await sleep(pollMs);
808
+ if (active !== null && !monitorOptions.restart) {
809
+ if (active.source === "port") {
810
+ info(
811
+ `Recovered orphaned web monitor pid ${active.lock.pid} on port ${active.lock.port} and restored web.lock.`
812
+ );
813
+ }
814
+ info(`Reusing existing web monitor at ${reusedHandle(active.lock).url}`);
815
+ return reusedHandle(active.lock);
816
+ }
817
+ if (active !== null && monitorOptions.restart) {
818
+ if (!await stopManagedInstance(active, lockFile, options, monitorOptions)) return null;
819
+ } else if (discovered.unownedMonitor) {
820
+ warn(
821
+ `Web monitor: port ${options.port} answers an Issue Flow health check, but its owning process could not be verified. It was left untouched and no competing server was started.`
822
+ );
823
+ return null;
824
+ } else if (monitorOptions.restart) {
825
+ info("Web monitor: no previous managed instance was found; starting a fresh one.");
826
+ info("Web monitor: no on-disk web build cache exists; no files were removed.");
827
+ }
828
+ const execPath = monitorOptions.execPath ?? process.execPath;
829
+ const entryScript = monitorOptions.entryScript ?? process.argv[1];
830
+ if (entryScript === void 0) {
831
+ warn("Web monitor: could not determine the CLI entry point to spawn a detached server.");
832
+ return null;
833
+ }
834
+ const args = [
835
+ entryScript,
836
+ "web",
837
+ "serve",
838
+ "--port",
839
+ String(options.port),
840
+ "--host",
841
+ options.host
842
+ ];
843
+ if (options.refreshSeconds !== void 0) {
844
+ args.push("--refresh", String(options.refreshSeconds));
845
+ }
846
+ const spawnFn = monitorOptions.spawn ?? nodeSpawn;
847
+ try {
848
+ const child = spawnFn(execPath, args, { detached: true, stdio: "ignore" });
849
+ child.unref();
850
+ } catch (err) {
851
+ warn(
852
+ `Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`
853
+ );
854
+ return null;
855
+ }
856
+ const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;
857
+ const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;
858
+ const deadline = Date.now() + timeoutMs;
859
+ while (Date.now() < deadline) {
860
+ const claimed = await detectActiveInstance(lockFile);
861
+ if (claimed !== null) {
862
+ const handle = reusedHandle(claimed);
863
+ const health = await probeHealth(claimed.host, claimed.port);
864
+ if (monitorOptions.restart) {
865
+ const version = health?.version === void 0 ? "" : `, version ${health.version}`;
866
+ info(
867
+ `Web monitor: new instance started (pid ${claimed.pid}${version}) at ${chalk2.bold.cyan(handle.url)}`
868
+ );
869
+ } else {
870
+ info(`Web monitor running at ${chalk2.bold.cyan(handle.url)}`);
871
+ }
872
+ return handle;
873
+ }
874
+ await sleep(pollMs);
875
+ }
876
+ warn(
877
+ "Web monitor: the detached server did not start in time. Continuing without the web server."
878
+ );
879
+ return null;
880
+ } finally {
881
+ await restartLease?.release();
544
882
  }
545
- warn(
546
- "Web monitor: the detached server did not start in time. Continuing without the web server."
547
- );
548
- return null;
549
883
  }
550
884
 
551
885
  export {
552
- getWebLockFile,
553
- readWebLock,
554
- removeWebLock,
555
886
  ensureSingleWebServer,
887
+ stopWebMonitor,
556
888
  ensureWebMonitor
557
889
  };
558
- //# sourceMappingURL=chunk-LFEQIZLB.js.map
890
+ //# sourceMappingURL=chunk-BMHGRYL6.js.map