frizz 0.12.11 → 0.12.13

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 (101) hide show
  1. package/dist/codex-app-server-daemon.js +8 -1
  2. package/dist/dev-child.js +639 -293
  3. package/dist/frizz.js +499 -82
  4. package/package.json +1 -1
  5. package/runtime/cc-worker/bin/frizz-mcp.mjs +19 -14
  6. package/runtime/cc-worker/hooks/bash-background.d.mts +1 -1
  7. package/runtime/cc-worker/hooks/bash-background.mjs +17 -6
  8. package/runtime/cc-worker/hooks/deny-ask.mjs +5 -4
  9. package/runtime/cc-worker/hooks/deny-plan.mjs +3 -3
  10. package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
  11. package/runtime/cc-worker/skills/gh/SKILL.md +3 -3
  12. package/web-dist/assets/{TerminalPane-DHvHCziB.js → TerminalPane-Cv0c_SNS.js} +1 -1
  13. package/web-dist/assets/{abnfDiagram-VRR7QNED-DIzM_IN8.js → abnfDiagram-VRR7QNED-B-wED8ht.js} +1 -1
  14. package/web-dist/assets/architecture-TIHT7OUA-nsH-QfGr.js +1 -0
  15. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-Bm_8vd5v.js → architectureDiagram-ZJ3FMSHR-kHm_O2sR.js} +1 -1
  16. package/web-dist/assets/{blockDiagram-677ZJIJ3-BXUW_N2o.js → blockDiagram-677ZJIJ3-CdazUaKU.js} +1 -1
  17. package/web-dist/assets/{c4Diagram-LMCZKHZV-C7rfqxUO.js → c4Diagram-LMCZKHZV-rauLKZbJ.js} +1 -1
  18. package/web-dist/assets/channel-fvwjeUt3.js +1 -0
  19. package/web-dist/assets/{chunk-32BRIVSS-DbfUERLa.js → chunk-32BRIVSS-DjOqyEDX.js} +1 -1
  20. package/web-dist/assets/{chunk-52WLFC77-BT_3wKSP.js → chunk-52WLFC77-f1SThegZ.js} +1 -1
  21. package/web-dist/assets/{chunk-C7G6YPKG-JqApnOVt.js → chunk-C7G6YPKG-BfQsesjz.js} +1 -1
  22. package/web-dist/assets/{chunk-EX3LRPZG-Cj6hlA4t.js → chunk-EX3LRPZG-BIzZW7OQ.js} +1 -1
  23. package/web-dist/assets/{chunk-FWX5IMBZ-BGthhuW_.js → chunk-FWX5IMBZ-CATINZFC.js} +2 -2
  24. package/web-dist/assets/{chunk-HOUHSVGY-uyLpm_YS.js → chunk-HOUHSVGY-DbMBRCCE.js} +1 -1
  25. package/web-dist/assets/{chunk-ICXQ74PX-BlwUMZAk.js → chunk-ICXQ74PX-COcoSWx4.js} +1 -1
  26. package/web-dist/assets/{chunk-MOJQB5TN-MXbNNk-P.js → chunk-MOJQB5TN-CVnI-o_4.js} +1 -1
  27. package/web-dist/assets/{chunk-OGEWGWER-Da4My1Rf.js → chunk-OGEWGWER-U0vdZqB6.js} +1 -1
  28. package/web-dist/assets/{chunk-PUDLZKDR-C-PgvO__.js → chunk-PUDLZKDR-Bukb7rem.js} +1 -1
  29. package/web-dist/assets/{chunk-Q4XR5HBZ-BxZFe7gr.js → chunk-Q4XR5HBZ-Bl019PHd.js} +1 -1
  30. package/web-dist/assets/{chunk-V7JOEXUC-DgERuXm_.js → chunk-V7JOEXUC-YJR0vzI6.js} +1 -1
  31. package/web-dist/assets/{chunk-VAUOI2AC-T5OoQE-_.js → chunk-VAUOI2AC-CK6NU9kj.js} +1 -1
  32. package/web-dist/assets/{chunk-VR4S4FIN--HFvJUG1.js → chunk-VR4S4FIN-B3LBv3C3.js} +1 -1
  33. package/web-dist/assets/{chunk-WYO6CB5R-DlAvy41i.js → chunk-WYO6CB5R-DjRPyRuS.js} +1 -1
  34. package/web-dist/assets/{chunk-ZGVPDNZ5-BdDg7X_H.js → chunk-ZGVPDNZ5-BUJCayKP.js} +1 -1
  35. package/web-dist/assets/classDiagram-OUVF2IWQ-BRZQ_xnN.js +1 -0
  36. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BRZQ_xnN.js +1 -0
  37. package/web-dist/assets/{cynefin-VYW2F7L2-BhjNO28R.js → cynefin-VYW2F7L2-CE3ce51v.js} +1 -1
  38. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-B6VA5gCd.js → cynefinDiagram-TSTJHNR4-DSXSPZQV.js} +1 -1
  39. package/web-dist/assets/{dagre-VKFMJZFB-6MXoEc6K.js → dagre-VKFMJZFB-VwBQdjum.js} +1 -1
  40. package/web-dist/assets/{diagram-FQU43EPY-D2RSPoK4.js → diagram-FQU43EPY-7Tf5_WDu.js} +1 -1
  41. package/web-dist/assets/{diagram-G47NLZAW-BOSbtzOf.js → diagram-G47NLZAW-mu4aclWA.js} +1 -1
  42. package/web-dist/assets/{diagram-NH7WQ7WH-Dt_u21nP.js → diagram-NH7WQ7WH-B3kAVNwv.js} +1 -1
  43. package/web-dist/assets/{diagram-OA4YK3LP-CjuWW-7r.js → diagram-OA4YK3LP-DAaV-2ou.js} +1 -1
  44. package/web-dist/assets/{diagram-WEI45ONY-rANIKJ8N.js → diagram-WEI45ONY-DijPfgly.js} +1 -1
  45. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-Bf3kfaKZ.js → ebnfDiagram-CCIWWBDH-B1o42Wp-.js} +1 -1
  46. package/web-dist/assets/{erDiagram-Q63AITRT-6vBp0ITn.js → erDiagram-Q63AITRT-Dqej9Wi3.js} +1 -1
  47. package/web-dist/assets/eventmodeling-45OFAUF4-B8xeGcXU.js +1 -0
  48. package/web-dist/assets/flowDiagram-23GEKE2U-Cnyfcz6Z.js +1 -0
  49. package/web-dist/assets/{ganttDiagram-NO4QXBWP-C2qN2PMS.js → ganttDiagram-NO4QXBWP-UoyrLPUT.js} +1 -1
  50. package/web-dist/assets/{gitGraph-TEB2WS4Q-CRASqc9n.js → gitGraph-TEB2WS4Q-CwjIwQ3T.js} +1 -1
  51. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-gsXSPrH2.js → gitGraphDiagram-IHSO6WYX-BxU758_g.js} +1 -1
  52. package/web-dist/assets/index-CT6k_A5y.css +1 -0
  53. package/web-dist/assets/index-k8HCkyvh.js +319 -0
  54. package/web-dist/assets/{info-DKCQHKI2-rDkFlFOd.js → info-DKCQHKI2-CWtSI6aK.js} +1 -1
  55. package/web-dist/assets/{infoDiagram-FWYZ7A6U-03ql8Tqr.js → infoDiagram-FWYZ7A6U-n5xRfy_d.js} +1 -1
  56. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-CfDSAVNp.js → ishikawaDiagram-FXEZZL3T-DU1LTQY-.js} +1 -1
  57. package/web-dist/assets/{journeyDiagram-5HDEW3XC-RWaA6AvH.js → journeyDiagram-5HDEW3XC-BzZWHoPv.js} +1 -1
  58. package/web-dist/assets/{kanban-definition-HUTT4EX6-C89qresM.js → kanban-definition-HUTT4EX6-CeneyR7c.js} +1 -1
  59. package/web-dist/assets/{line-CZtSsvHu.js → line-DTfkLWnw.js} +1 -1
  60. package/web-dist/assets/{mermaid-parser.core-DKNnvxt0.js → mermaid-parser.core-CHzoJ5LT.js} +3 -3
  61. package/web-dist/assets/{mermaid.core-DX8cFeBF.js → mermaid.core-Cl84DLN2.js} +3 -3
  62. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DW8ClyAX.js → mindmap-definition-LN4V7U3C-C39sBNqB.js} +1 -1
  63. package/web-dist/assets/{packet-7NZHBO7P-CUOv3apI.js → packet-7NZHBO7P-Bj73rlQn.js} +1 -1
  64. package/web-dist/assets/{pegDiagram-2B236MQR-CFyxKEBL.js → pegDiagram-2B236MQR-BEEM1vSv.js} +1 -1
  65. package/web-dist/assets/{pie-RZYD4A2V-DyXXRie2.js → pie-RZYD4A2V-CJ-fEfdy.js} +1 -1
  66. package/web-dist/assets/{pieDiagram-ENE6RG2P-yUw7HA8l.js → pieDiagram-ENE6RG2P-Dcqj3oyh.js} +1 -1
  67. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-Mc7aea_i.js → quadrantDiagram-ABIIQ3AL-D5cljQ3D.js} +1 -1
  68. package/web-dist/assets/{radar-I7S5WNFK-BKj0BsQy.js → radar-I7S5WNFK-G8JqqGPf.js} +1 -1
  69. package/web-dist/assets/{railroad-3IZDKUUU-DUK_kJIY.js → railroad-3IZDKUUU-CaGNL0iN.js} +1 -1
  70. package/web-dist/assets/railroad-abnf-AHOZXSZD-DWZmdy-r.js +1 -0
  71. package/web-dist/assets/railroad-ebnf-EBAXGLYW-C-XoPuo_.js +1 -0
  72. package/web-dist/assets/railroad-peg-LSFZ7HO6-BGpxqg46.js +1 -0
  73. package/web-dist/assets/{railroadDiagram-RFXS5EU6-By0Xgb_U.js → railroadDiagram-RFXS5EU6-BZGZXMY4.js} +1 -1
  74. package/web-dist/assets/{requirementDiagram-TGXJPOKE-CgTycsRY.js → requirementDiagram-TGXJPOKE-Dj5yn_9e.js} +1 -1
  75. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-mJ_-Grkn.js → sankeyDiagram-HTMAVEWB-7iJ_NDNf.js} +1 -1
  76. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-Dn6YyeNv.js → sequenceDiagram-DBY2YBRQ-Bl54CknA.js} +1 -1
  77. package/web-dist/assets/{stateDiagram-2N3HPSRC-BBws1E-e.js → stateDiagram-2N3HPSRC-DuvjeNgJ.js} +1 -1
  78. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-BR-vrMgH.js +1 -0
  79. package/web-dist/assets/{swimlanes-5IMT3BWC-Jab-07o9.js → swimlanes-5IMT3BWC-B7-Jj4Sk.js} +1 -1
  80. package/web-dist/assets/swimlanesDiagram-G3AALYLV-C1HvMf2k.js +8 -0
  81. package/web-dist/assets/{timeline-definition-FHXFAJF6-Cq6RznHz.js → timeline-definition-FHXFAJF6-jsey2pEG.js} +1 -1
  82. package/web-dist/assets/{treeView-QDETBFTQ-KSelJnyL.js → treeView-QDETBFTQ-DhPmOzHr.js} +1 -1
  83. package/web-dist/assets/{treemap-6X3UGDF4-B-iiUurZ.js → treemap-6X3UGDF4-Dw2IaXqK.js} +1 -1
  84. package/web-dist/assets/{vennDiagram-L72KCM5P-B8t4NeQy.js → vennDiagram-L72KCM5P-CXKZkBab.js} +1 -1
  85. package/web-dist/assets/{wardley-OPB4EBWU-CtkAPZLQ.js → wardley-OPB4EBWU-CTCdDwyE.js} +1 -1
  86. package/web-dist/assets/{wardleyDiagram-EHGQE667-DC-ms_sD.js → wardleyDiagram-EHGQE667-B1w0xIqB.js} +1 -1
  87. package/web-dist/assets/{xychartDiagram-FW5EYKEG-DW6fcgjF.js → xychartDiagram-FW5EYKEG-gULk-uIl.js} +1 -1
  88. package/web-dist/index.html +2 -2
  89. package/web-dist/assets/architecture-TIHT7OUA-DBvKOf0j.js +0 -1
  90. package/web-dist/assets/channel-CbdXYswc.js +0 -1
  91. package/web-dist/assets/classDiagram-OUVF2IWQ-BVXwECjS.js +0 -1
  92. package/web-dist/assets/classDiagram-v2-EOCWNBFH-BVXwECjS.js +0 -1
  93. package/web-dist/assets/eventmodeling-45OFAUF4-STTT-geQ.js +0 -1
  94. package/web-dist/assets/flowDiagram-23GEKE2U-CC7DF1Fz.js +0 -1
  95. package/web-dist/assets/index-DimQCLe8.js +0 -319
  96. package/web-dist/assets/index-XhfdAXcw.css +0 -1
  97. package/web-dist/assets/railroad-abnf-AHOZXSZD-BDImJcFw.js +0 -1
  98. package/web-dist/assets/railroad-ebnf-EBAXGLYW-BR2XIYqq.js +0 -1
  99. package/web-dist/assets/railroad-peg-LSFZ7HO6-DRCwBbJY.js +0 -1
  100. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-B2jOFVGG.js +0 -1
  101. package/web-dist/assets/swimlanesDiagram-G3AALYLV-DhYK_6fH.js +0 -8
package/dist/frizz.js CHANGED
@@ -12841,14 +12841,16 @@ var init_src = __esm({
12841
12841
  "the write-up.",
12842
12842
  "",
12843
12843
  "**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
12844
- "right now: ask that one in a question fence. Every other open choice INSIDE the task \u2014 a name, a",
12844
+ "right now: register that one with `mcp__frizz__ask`. Every other open choice INSIDE the task \u2014 a name, a",
12845
12845
  "default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
12846
12846
  "what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
12847
12847
  "unanswered question is not permission to go build the answer.",
12848
12848
  "",
12849
- "Otherwise, add a fence at the END of your next message:",
12849
+ "Otherwise, sign off \u2014 a registration, or a fence at the END of your next message:",
12850
12850
  "",
12851
- "- `` ```question `` \u2014 you need the human. One question per fence, lettered options, one recommended.",
12851
+ "- `mcp__frizz__ask` \u2014 you need the human. NOT a fence: the ```question fence is retired, and a fence",
12852
+ " with a question in its body is plain prose. Register it (options with one-line trade-offs, the",
12853
+ " recommended one first), then rest normally \u2014 an open registered question is the sign-off.",
12852
12854
  "- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
12853
12855
  " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
12854
12856
  " **bolded verb phrase**.",
@@ -19831,7 +19833,7 @@ init_local_origin();
19831
19833
  import { readFileSync as readFileSync13 } from "node:fs";
19832
19834
  import { homedir as homedir11 } from "node:os";
19833
19835
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19834
- import { join as join15 } from "node:path";
19836
+ import { join as join16 } from "node:path";
19835
19837
 
19836
19838
  // src/browser.ts
19837
19839
  import { spawn, execFile } from "node:child_process";
@@ -22489,6 +22491,8 @@ async function readJsonBody(req, limit = 64 * 1024) {
22489
22491
  var RestartSupervisorProxy = class {
22490
22492
  server = null;
22491
22493
  restartInFlight = null;
22494
+ /** Which action `restartInFlight` is. An update's prepare phase leaves the child serving; a restart does not. */
22495
+ transition = null;
22492
22496
  state = "ready";
22493
22497
  message;
22494
22498
  options;
@@ -22615,10 +22619,11 @@ var RestartSupervisorProxy = class {
22615
22619
  status() {
22616
22620
  const delegated = this.options.status?.();
22617
22621
  const updateVersion = this.options.updateVersion?.();
22618
- const ownerTransition = this.restartInFlight !== null || this.state === "failed";
22622
+ const owner = { state: this.state, ...this.message ? { message: this.message } : {} };
22623
+ const reported = this.transition === "update" && delegated ? { ...delegated, ...delegated.state === "ready" ? { preparing: true } : {} } : this.restartInFlight !== null || this.state === "failed" ? owner : delegated ?? owner;
22619
22624
  return {
22620
22625
  ...delegated ?? {},
22621
- ...ownerTransition ? { state: this.state, ...this.message ? { message: this.message } : {} } : delegated ?? { state: this.state, ...this.message ? { message: this.message } : {} },
22626
+ ...reported,
22622
22627
  // Never infer this from the generic protocol: legacy/static supervisors can recover a child
22623
22628
  // but cannot build and promote the canonical Frizz artifact.
22624
22629
  updateRestart: typeof this.options.updateRestart === "function",
@@ -22634,8 +22639,9 @@ var RestartSupervisorProxy = class {
22634
22639
  ...this.options.dev ? { dev: true } : {}
22635
22640
  };
22636
22641
  }
22637
- async runAction(action) {
22642
+ async runAction(action, transition) {
22638
22643
  if (this.restartInFlight) return this.restartInFlight;
22644
+ this.transition = transition;
22639
22645
  this.state = "restarting";
22640
22646
  this.message = void 0;
22641
22647
  const work = (async () => {
@@ -22655,6 +22661,7 @@ var RestartSupervisorProxy = class {
22655
22661
  }
22656
22662
  ).finally(() => {
22657
22663
  this.restartInFlight = null;
22664
+ this.transition = null;
22658
22665
  });
22659
22666
  this.restartInFlight = work;
22660
22667
  return work;
@@ -22738,11 +22745,11 @@ var RestartSupervisorProxy = class {
22738
22745
  return;
22739
22746
  }
22740
22747
  if (pathname === SUPERVISOR_UPDATE_RESTART_PATH) {
22741
- void this.runAction(this.options.updateRestart);
22748
+ void this.runAction(this.options.updateRestart, "update");
22742
22749
  responseJson(res, 202, { protocol: SUPERVISOR_CONTROL_PROTOCOL, state: "restarting" });
22743
22750
  return;
22744
22751
  }
22745
- const result = await this.runAction(this.options.restart);
22752
+ const result = await this.runAction(this.options.restart, "restart");
22746
22753
  responseJson(res, result.state === "ready" ? 202 : 503, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...result });
22747
22754
  }
22748
22755
  handleLocalImage(req, res) {
@@ -22975,10 +22982,17 @@ async function signOutSession(port, target2) {
22975
22982
 
22976
22983
  // src/pane-host.ts
22977
22984
  var CTRL_C = "";
22985
+ function raiseInterrupt() {
22986
+ if (process.listenerCount("SIGINT") > 0) {
22987
+ process.emit("SIGINT", "SIGINT");
22988
+ return;
22989
+ }
22990
+ process.kill(process.pid, "SIGINT");
22991
+ }
22978
22992
  function installPaneHost(options2) {
22979
22993
  const input = options2.input ?? process.stdin;
22980
22994
  const output = options2.output ?? process.stdout;
22981
- const onInterrupt = options2.onInterrupt ?? (() => process.kill(process.pid, "SIGINT"));
22995
+ const onInterrupt = options2.onInterrupt ?? raiseInterrupt;
22982
22996
  if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return null;
22983
22997
  let active = null;
22984
22998
  let disposed = false;
@@ -23498,9 +23512,9 @@ function connectRelay(options2) {
23498
23512
  };
23499
23513
  const scheduleRetry = (reason) => {
23500
23514
  if (stopped) return;
23501
- const delay3 = backoff(attempt++);
23502
- options2.onStatus?.("retrying", `${reason}; retrying in ${Math.round(delay3 / 1e3)}s`);
23503
- retryHandle = setTimer(() => void open(), delay3);
23515
+ const delay4 = backoff(attempt++);
23516
+ options2.onStatus?.("retrying", `${reason}; retrying in ${Math.round(delay4 / 1e3)}s`);
23517
+ retryHandle = setTimer(() => void open(), delay4);
23504
23518
  };
23505
23519
  const open = async () => {
23506
23520
  if (stopped) return;
@@ -24300,7 +24314,7 @@ function formatDuration(ms) {
24300
24314
  function tildePath(path, home) {
24301
24315
  if (!home || !path.startsWith(home)) return path;
24302
24316
  const rest = path.slice(home.length);
24303
- return rest === "" ? "~" : rest.startsWith("/") ? `~${rest}` : path;
24317
+ return rest === "" ? "~" : /^[\\/]/.test(rest) ? `~${rest}` : path;
24304
24318
  }
24305
24319
  var Readout = class {
24306
24320
  out;
@@ -24952,6 +24966,54 @@ async function probeFrizz(port, expected2, fetcher = fetch, timeoutMs = HEALTH_P
24952
24966
  clearTimeout(timeout);
24953
24967
  }
24954
24968
  }
24969
+ async function readFrizzHealth(port, fetcher = fetch, timeoutMs = HEALTH_PROBE_TIMEOUT_MS) {
24970
+ const controller = new AbortController();
24971
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
24972
+ timeout.unref?.();
24973
+ try {
24974
+ const response = await fetcher(`http://127.0.0.1:${port}${FRIZZ_ROUTE_PREFIX}/health`, { signal: controller.signal });
24975
+ if (!response.ok) return null;
24976
+ const health = await response.json();
24977
+ if (health.ok !== true || typeof health.projectId !== "string" || typeof health.projectDir !== "string" || typeof health.bootId !== "string") return null;
24978
+ return health;
24979
+ } catch {
24980
+ return null;
24981
+ } finally {
24982
+ clearTimeout(timeout);
24983
+ }
24984
+ }
24985
+ async function resolveLaunchControlTarget(options2) {
24986
+ const own = { target: options2.target, stateDir: options2.stateDir, host: true };
24987
+ if (readProjectLaunchOwner(options2.stateDir)) return own;
24988
+ const fetcher = options2.fetcher ?? fetch;
24989
+ const stateDirFor = options2.stateDirFor ?? ((projectId) => projectStateDir(projectId));
24990
+ for (const port of new Set(options2.ports ?? [DEFAULT_PORT, fallbackPort(DEFAULT_PORT)])) {
24991
+ const health = await readFrizzHealth(port, fetcher);
24992
+ if (!health || health.projectId === options2.target.projectId) continue;
24993
+ const stateDir = stateDirFor(health.projectId);
24994
+ if (!readProjectLaunchOwner(stateDir)) continue;
24995
+ return { target: { projectId: health.projectId, projectDir: health.projectDir, stateDir }, stateDir, host: false };
24996
+ }
24997
+ return own;
24998
+ }
24999
+ async function requestFrizzStop(port, expected2, ownerToken, fetcher = fetch) {
25000
+ if (!await probeFrizz(port, expected2, fetcher)) return false;
25001
+ const controller = new AbortController();
25002
+ const timeout = setTimeout(() => controller.abort(), 1e3);
25003
+ timeout.unref?.();
25004
+ try {
25005
+ const response = await fetcher(`http://127.0.0.1:${port}${FRIZZ_ROUTE_PREFIX}/control/stop`, {
25006
+ method: "POST",
25007
+ headers: { "x-frizz-launch-token": ownerToken },
25008
+ signal: controller.signal
25009
+ });
25010
+ return response.status === 202;
25011
+ } catch {
25012
+ return false;
25013
+ } finally {
25014
+ clearTimeout(timeout);
25015
+ }
25016
+ }
24955
25017
  function expectedOwnerHealth(target2, owner) {
24956
25018
  return {
24957
25019
  projectId: target2.projectId,
@@ -25042,6 +25104,66 @@ async function waitForWorkspace(port, expected2, timeoutMs = LAUNCH_TIMEOUT_MS,
25042
25104
  await delay2(150);
25043
25105
  }
25044
25106
  }
25107
+ async function stopProjectLaunch(options2) {
25108
+ const { stateDir, target: target2 } = options2;
25109
+ const adapter = options2.adapter ?? defaultProcessPlatformAdapter;
25110
+ const fetcher = options2.fetcher ?? fetch;
25111
+ const sleep = options2.sleep ?? ((ms) => delay2(ms));
25112
+ const owner = readProjectLaunchOwner(stateDir);
25113
+ if (!owner) return { kind: "not-running" };
25114
+ const status = liveWorkspaceOwner(stateDir, target2, adapter);
25115
+ const controlPort = status?.port ?? (status ? void 0 : await options2.fallbackPort?.());
25116
+ const controlled = controlPort ? await requestFrizzStop(controlPort, expectedOwnerHealth(target2, owner), owner.token, fetcher) : false;
25117
+ if (!controlled && !processGenerationIsStale(owner, adapter)) {
25118
+ throw new Error(
25119
+ "Frizz refused to stop a live owner without authenticated token-bound control; the owner was left untouched"
25120
+ );
25121
+ }
25122
+ const reap = () => {
25123
+ if (!readProjectLaunchOwner(stateDir)) return true;
25124
+ const reaped = tryAcquireProjectLaunchOwner(target2, "launcher", { delegateDrainTimeoutMs: 250, adapter });
25125
+ if (reaped.kind !== "acquired") return false;
25126
+ reaped.lease.release();
25127
+ return true;
25128
+ };
25129
+ const deadline = adapter.now() + (options2.timeoutMs ?? 1e4);
25130
+ while (adapter.now() < deadline) {
25131
+ if (reap()) return { kind: "stopped", stale: !controlled };
25132
+ await sleep(100);
25133
+ }
25134
+ await sleep(100);
25135
+ if (reap()) return { kind: "stopped", stale: !controlled };
25136
+ throw new Error(`supervisor pid ${owner.pid} did not stop within ${Math.round((options2.timeoutMs ?? 1e4) / 1e3)}s`);
25137
+ }
25138
+ async function runningFrizzStatus(options2) {
25139
+ const fetcher = options2.fetcher ?? fetch;
25140
+ const owner = liveWorkspaceOwner(options2.stateDir, options2.target, options2.adapter ?? defaultProcessPlatformAdapter);
25141
+ if (!owner?.port) return null;
25142
+ const expected2 = expectedOwnerHealth(options2.target, readProjectLaunchOwner(options2.stateDir));
25143
+ if (!await probeFrizz(owner.port, expected2, fetcher)) return null;
25144
+ const controller = new AbortController();
25145
+ const timeout = setTimeout(() => controller.abort(), HEALTH_PROBE_TIMEOUT_MS);
25146
+ timeout.unref?.();
25147
+ let control = {};
25148
+ try {
25149
+ const response = await fetcher(`http://127.0.0.1:${owner.port}${FRIZZ_ROUTE_PREFIX}/control/status`, {
25150
+ signal: controller.signal,
25151
+ headers: { origin: `http://127.0.0.1:${owner.port}`, "cache-control": "no-store" }
25152
+ });
25153
+ if (response.ok) {
25154
+ const body = await response.json();
25155
+ control = {
25156
+ ...typeof body.version === "string" ? { version: body.version } : {},
25157
+ ...typeof body.state === "string" ? { state: body.state } : {},
25158
+ ...typeof body.message === "string" ? { message: body.message } : {}
25159
+ };
25160
+ }
25161
+ } catch {
25162
+ } finally {
25163
+ clearTimeout(timeout);
25164
+ }
25165
+ return { port: owner.port, pid: owner.pid, ...control };
25166
+ }
25045
25167
 
25046
25168
  // src/production.ts
25047
25169
  init_project_launch();
@@ -25061,6 +25183,7 @@ var DEV_CRASH_STABLE_MS = 5e3;
25061
25183
  var DEV_CRASH_RETRY_BASE_MS = 500;
25062
25184
  var DEV_CRASH_RETRY_MAX_MS = 1e4;
25063
25185
  var CHILD_STOP_TIMEOUT_MS = 15e3;
25186
+ var CHILD_KILL_GRACE_MS = 5e3;
25064
25187
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".json"]);
25065
25188
  var CONFIG_NAMES = /* @__PURE__ */ new Set(["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc"]);
25066
25189
  var CHILD_RUNTIME_PACKAGES = /* @__PURE__ */ new Set(["server", "shared", "rpc", "claude-agent-sdk-runtime"]);
@@ -25091,7 +25214,7 @@ function devCrashRetryDelay(attempt) {
25091
25214
  return Math.min(DEV_CRASH_RETRY_BASE_MS * 2 ** exponent, DEV_CRASH_RETRY_MAX_MS);
25092
25215
  }
25093
25216
  var SUPERVISOR_ESCALATE_GRACE_MS = 500;
25094
- var SUPERVISOR_DRAIN_DEADLINE_MS = CHILD_STOP_TIMEOUT_MS + 5e3;
25217
+ var SUPERVISOR_DRAIN_DEADLINE_MS = CHILD_STOP_TIMEOUT_MS + CHILD_KILL_GRACE_MS + 5e3;
25095
25218
  function stripPrefix(line) {
25096
25219
  return line.startsWith("[frizz] ") ? line.slice("[frizz] ".length) : line;
25097
25220
  }
@@ -25306,6 +25429,13 @@ var Supervisor = class {
25306
25429
  /** Environment of the generation that actually reached ready, never merely the latest pointer. */
25307
25430
  activeChildEnvironment = {};
25308
25431
  lastRestartFailure;
25432
+ /**
25433
+ * True from the moment an update starts draining the old child until that update has settled.
25434
+ * Before it — the prepare phase — the old child is untouched and serving, and the status delegate
25435
+ * says so; during it the child bookkeeping reads as "failed" (no child, no boot), which is not
25436
+ * what a poll during those seconds should learn.
25437
+ */
25438
+ handoffDraining = false;
25309
25439
  constructor(opts) {
25310
25440
  const launchOwner = verifyProjectLaunchDelegate(opts.launchTarget, opts.launchOwnerToken);
25311
25441
  const callerGeneration = currentProcessGeneration();
@@ -25324,7 +25454,7 @@ var Supervisor = class {
25324
25454
  this.childLaunchProvider = opts.childLaunchProvider;
25325
25455
  this.childArgs = opts.childArgs ?? [];
25326
25456
  this.watchSubscribe = opts.watchSubscribe ?? ((root, callback, options2) => watcher.subscribe(root, callback, options2));
25327
- this.reexec = opts.reexec ?? (typeof process.execve === "function" ? (request) => process.execve(request.executable, request.argv, request.env) : void 0);
25457
+ this.reexec = opts.reexec ?? (process.platform !== "win32" && typeof process.execve === "function" ? (request) => process.execve(request.executable, request.argv, request.env) : void 0);
25328
25458
  if (opts.stateDir && resolve5(opts.stateDir) !== opts.launchTarget.stateDir) {
25329
25459
  throw new Error("dev supervisor state directory does not match its owned project");
25330
25460
  }
@@ -25352,7 +25482,7 @@ var Supervisor = class {
25352
25482
  updateVersion: opts.updateVersion,
25353
25483
  dev: opts.dev,
25354
25484
  status: () => {
25355
- if (this.browserRestart || this.restartRunning) return { state: "restarting" };
25485
+ if (this.handoffDraining || this.browserRestart || this.restartRunning) return { state: "restarting" };
25356
25486
  const failed = this.child === null && this.boot === null;
25357
25487
  const artifactDigest = this.activeChildEnvironment.FRIZZ_STABLE_ARTIFACT;
25358
25488
  return failed ? { state: "failed", message: "Frizz application server is not ready", ...artifactDigest ? { artifactDigest } : {} } : { state: "ready", ...artifactDigest ? { artifactDigest } : {} };
@@ -25505,7 +25635,7 @@ var Supervisor = class {
25505
25635
  }
25506
25636
  async updateFromBrowser() {
25507
25637
  if (!this.updateRestart) return { state: "failed", message: "Update & Restart is unavailable in this launcher mode" };
25508
- this.emitActivity("updating", "preparing the new build \u2014 the running one is untouched until it is ready");
25638
+ this.emitActivity("updating", "preparing the update \u2014 the running Frizz is untouched until it is ready");
25509
25639
  const candidate = await this.updateRestart();
25510
25640
  if (candidate.state !== "ready") {
25511
25641
  this.emitActivity("failed", candidate.message ?? "the update could not be prepared");
@@ -25527,6 +25657,8 @@ var Supervisor = class {
25527
25657
  } catch (error) {
25528
25658
  const message = `durable supervisor handoff failed: ${error instanceof Error ? error.message : error}`;
25529
25659
  return this.failDurableUpdate(message, handoffPreparationStarted);
25660
+ } finally {
25661
+ this.handoffDraining = false;
25530
25662
  }
25531
25663
  }
25532
25664
  /**
@@ -25566,6 +25698,7 @@ var Supervisor = class {
25566
25698
  if (!ready) throw new Error("the recovered control plane did not become ready");
25567
25699
  }
25568
25700
  async prepareDurableReexec() {
25701
+ this.handoffDraining = true;
25569
25702
  this.closed = true;
25570
25703
  this.clearCrashStability();
25571
25704
  if (this.debounce) clearTimeout(this.debounce);
@@ -25598,7 +25731,13 @@ var Supervisor = class {
25598
25731
  // The supervisor may itself run under `node --input-type`, `--test`, an inspector, etc. Those
25599
25732
  // parent-only flags are invalid or dangerous for a file-backed control-plane child.
25600
25733
  execArgv: [],
25601
- stdio: ["inherit", "inherit", "inherit", "ipc"]
25734
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
25735
+ // A registry update leaves this supervisor with no console on Windows (its successor starts
25736
+ // detached). A console program forked from a console-less parent gets a new, VISIBLE console
25737
+ // window, and closing that window would stop the board. `windowsHide` keeps the window from
25738
+ // appearing (measured 2026-09-07: hwnd 0 for the child and its children). No effect elsewhere.
25739
+ // `fork` passes it to `spawn` at runtime; `ForkOptions` in @types/node 24 does not declare it.
25740
+ ...{ windowsHide: true }
25602
25741
  });
25603
25742
  } catch (error) {
25604
25743
  const message = `child spawn failed: ${error instanceof Error ? error.message : error}; watching for a corrective edit`;
@@ -25774,6 +25913,21 @@ var Supervisor = class {
25774
25913
  this.errorLine("[frizz] dev launcher re-exec returned unexpectedly; restart Frizz once");
25775
25914
  this.resolveStopRequested();
25776
25915
  }
25916
+ /**
25917
+ * Drain the control-plane child: ASK first, signal only a child that did not answer.
25918
+ *
25919
+ * The ask is `child.disconnect()` — closing the IPC channel — because dev-child already treats a
25920
+ * lost supervisor as an order to shut down (`process.once("disconnect")`), so it needs no second
25921
+ * protocol and it works on every platform. The ask used to be `kill("SIGTERM")`, which on POSIX
25922
+ * runs the same shutdown handler but on Windows is a TerminateProcess: Node maps EVERY signal it
25923
+ * can send to a hard kill there, so the child's `server.close()`, its shutdown fence and its
25924
+ * delegate release never ran, and every Restart and every update on Windows cut open RPCs and
25925
+ * socket writes mid-flight (audit 2026-09-11, finding 3). Only a child that has not exited within
25926
+ * CHILD_STOP_TIMEOUT_MS is signalled — SIGTERM then, CHILD_KILL_GRACE_MS later, SIGKILL on POSIX,
25927
+ * where a wedged event loop ignores the first; a single kill() on win32, where the first is final.
25928
+ * A child with no channel to ask over (already gone, or never had one) gets the signal at once,
25929
+ * which is exactly what it got before.
25930
+ */
25777
25931
  async stopChild() {
25778
25932
  this.clearCrashStability();
25779
25933
  const child = this.child;
@@ -25781,19 +25935,38 @@ var Supervisor = class {
25781
25935
  this.stopping = child;
25782
25936
  await new Promise((resolveStop) => {
25783
25937
  let done = false;
25938
+ const timers = [];
25784
25939
  const finish = () => {
25785
25940
  if (done) return;
25786
25941
  done = true;
25787
- clearTimeout(force);
25942
+ for (const timer of timers) clearTimeout(timer);
25788
25943
  resolveStop();
25789
25944
  };
25945
+ const later = (callback, delayMs) => {
25946
+ const timer = setTimeout(callback, delayMs);
25947
+ timer.unref();
25948
+ timers.push(timer);
25949
+ };
25950
+ const who = `dev child ${child.pid ?? "?"}`;
25951
+ const signal = (name, why) => {
25952
+ this.errorLine(`[frizz] ${who} ${why}; ${name === "SIGTERM" ? "sending SIGTERM to" : "killing"} the control plane only`);
25953
+ if (!child.kill(name)) finish();
25954
+ };
25955
+ const escalate = (why) => {
25956
+ if (process.platform === "win32") {
25957
+ signal(void 0, why);
25958
+ return;
25959
+ }
25960
+ signal("SIGTERM", why);
25961
+ later(() => signal("SIGKILL", `ignored SIGTERM for ${CHILD_KILL_GRACE_MS}ms`), CHILD_KILL_GRACE_MS);
25962
+ };
25790
25963
  child.once("exit", finish);
25791
- const force = setTimeout(() => {
25792
- this.errorLine(`[frizz] dev child ${child.pid ?? "?"} did not close in ${CHILD_STOP_TIMEOUT_MS}ms; killing control plane only`);
25793
- child.kill("SIGKILL");
25794
- }, CHILD_STOP_TIMEOUT_MS);
25795
- force.unref();
25796
- if (!child.kill("SIGTERM")) finish();
25964
+ if (!child.connected) {
25965
+ escalate("has no IPC channel to ask over");
25966
+ return;
25967
+ }
25968
+ later(() => escalate(`did not close in ${CHILD_STOP_TIMEOUT_MS}ms`), CHILD_STOP_TIMEOUT_MS);
25969
+ child.disconnect();
25797
25970
  });
25798
25971
  if (this.child === child) this.child = null;
25799
25972
  if (this.childPort !== void 0) this.childPort = void 0;
@@ -25839,11 +26012,32 @@ async function startDevSupervisor(opts) {
25839
26012
  }
25840
26013
 
25841
26014
  // src/production-update.ts
26015
+ init_src();
25842
26016
  import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
25843
26017
  import { existsSync as existsSync7 } from "node:fs";
25844
- import { isAbsolute as isAbsolute5 } from "node:path";
26018
+ import { dirname as dirname12, isAbsolute as isAbsolute5, join as join14 } from "node:path";
26019
+ import { setTimeout as delay3 } from "node:timers/promises";
25845
26020
  var PRODUCTION_REEXEC_FLAG = "--_frizz-production-reexec";
26021
+ function canReexecInPlace(platform = process.platform, execve = process.execve) {
26022
+ return platform !== "win32" && typeof execve === "function";
26023
+ }
25846
26024
  var PRODUCTION_PRINT_LAUNCHER_FLAG = "--_frizz-print-launcher";
26025
+ function resolveNpmInvocation(env = process.env, execPath = process.execPath, exists = existsSync7, platform = process.platform) {
26026
+ const nodeDir = dirname12(execPath);
26027
+ const candidates = [
26028
+ // pnpm, yarn and bun set `npm_execpath` too, to their own entry file. Their directories hold no
26029
+ // `npm-cli.js`, so the existence check below skips them.
26030
+ env.npm_execpath ? join14(dirname12(env.npm_execpath), "npm-cli.js") : void 0,
26031
+ join14(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"),
26032
+ join14(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
26033
+ ].filter((candidate) => candidate !== void 0);
26034
+ const script = candidates.find((candidate) => exists(candidate));
26035
+ if (script) return { command: execPath, prefixArgs: [script] };
26036
+ if (platform === "win32") {
26037
+ throw new Error(`npm-cli.js not found beside ${execPath} (searched: ${candidates.join(", ")})`);
26038
+ }
26039
+ return { command: "npm", prefixArgs: [] };
26040
+ }
25847
26041
  function compareReleaseVersions(a, b) {
25848
26042
  const parse2 = (value) => {
25849
26043
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
@@ -25860,7 +26054,23 @@ function compareReleaseVersions(a, b) {
25860
26054
  if (left.prerelease === right.prerelease) return 0;
25861
26055
  if (!left.prerelease) return 1;
25862
26056
  if (!right.prerelease) return -1;
25863
- return left.prerelease < right.prerelease ? -1 : 1;
26057
+ return comparePrerelease(left.prerelease, right.prerelease);
26058
+ }
26059
+ function comparePrerelease(a, b) {
26060
+ const left = a.split(".");
26061
+ const right = b.split(".");
26062
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
26063
+ const l = left[index];
26064
+ const r = right[index];
26065
+ if (l === r) continue;
26066
+ const lNumeric = /^\d+$/u.test(l);
26067
+ const rNumeric = /^\d+$/u.test(r);
26068
+ if (lNumeric && rNumeric) return Number(l) < Number(r) ? -1 : 1;
26069
+ if (lNumeric !== rNumeric) return lNumeric ? -1 : 1;
26070
+ return l < r ? -1 : 1;
26071
+ }
26072
+ if (left.length === right.length) return 0;
26073
+ return left.length < right.length ? -1 : 1;
25864
26074
  }
25865
26075
  async function planRegistryUpdate(packageName, currentVersion, adapter) {
25866
26076
  const latestVersion = (await adapter.latestVersion(packageName)).trim();
@@ -25873,6 +26083,20 @@ async function planRegistryUpdate(packageName, currentVersion, adapter) {
25873
26083
  function successorArgs(port) {
25874
26084
  return [PRODUCTION_REEXEC_FLAG, "--port", String(port)];
25875
26085
  }
26086
+ function reexecArgv(rawArgs2) {
26087
+ const kept = [];
26088
+ for (let index = 0; index < rawArgs2.length; index++) {
26089
+ const arg = rawArgs2[index];
26090
+ if (arg === PRODUCTION_REEXEC_FLAG) continue;
26091
+ if (arg === "--port" || arg === "--sign-out") {
26092
+ kept.push(arg);
26093
+ if (index + 1 < rawArgs2.length) kept.push(rawArgs2[++index]);
26094
+ continue;
26095
+ }
26096
+ if (arg.startsWith("-")) kept.push(arg);
26097
+ }
26098
+ return kept;
26099
+ }
25876
26100
  async function resolveRegistrySuccessor(plan, request, adapter) {
25877
26101
  const result = await adapter.npmExec({
25878
26102
  packageSpec: plan.packageSpec,
@@ -25913,52 +26137,168 @@ function handoffToRegistrySuccessor(successor, request, adapter) {
25913
26137
  });
25914
26138
  child.once("error", () => {
25915
26139
  });
26140
+ if (child.pid === void 0) throw new Error(`could not start Frizz ${successor.plan.latestVersion} from ${successor.entry}`);
25916
26141
  child.unref();
25917
- }
25918
- var npmRegistryReleaseAdapter = {
25919
- latestVersion(packageName) {
25920
- return new Promise((resolveVersion, reject) => {
25921
- execFile4("npm", ["view", `${packageName}@latest`, "version", "--json"], { encoding: "utf8" }, (error, stdout) => {
25922
- if (error) return reject(new Error(`could not check npm for ${packageName}: ${error.message}`));
26142
+ return child;
26143
+ }
26144
+ var SUCCESSOR_READY_TIMEOUT_MS = 6e4;
26145
+ async function awaitRegistrySuccessor(child, expected2, watch) {
26146
+ const deadlineMs = watch.deadlineMs ?? SUCCESSOR_READY_TIMEOUT_MS;
26147
+ const intervalMs = watch.intervalMs ?? 250;
26148
+ const now = watch.now ?? Date.now;
26149
+ const sleep = watch.sleep ?? ((ms) => delay3(ms));
26150
+ const where = expected2.logFile ? `; its log is ${expected2.logFile}` : "";
26151
+ let exited = child.exitCode !== null || child.signalCode !== null ? describeExit(child.exitCode, child.signalCode) : void 0;
26152
+ const onExit = (code, signal) => {
26153
+ exited = describeExit(code, signal);
26154
+ };
26155
+ child.once("exit", onExit);
26156
+ try {
26157
+ const started = now();
26158
+ for (; ; ) {
26159
+ if (exited) throw new Error(`Frizz ${expected2.version} exited (${exited}) before it answered on port ${expected2.port}${where}`);
26160
+ const status = await watch.status();
26161
+ if (status?.version === expected2.version) return;
26162
+ if (now() - started >= deadlineMs) {
25923
26163
  try {
25924
- const parsed = JSON.parse(stdout);
25925
- resolveVersion(typeof parsed === "string" ? parsed : String(parsed));
26164
+ child.kill();
25926
26165
  } catch {
25927
- resolveVersion(stdout.trim().replaceAll('"', ""));
25928
26166
  }
25929
- });
26167
+ throw new Error(
26168
+ `Frizz ${expected2.version} did not answer on port ${expected2.port} within ${Math.round(deadlineMs / 1e3)}s${where}`
26169
+ );
26170
+ }
26171
+ await sleep(intervalMs);
26172
+ }
26173
+ } finally {
26174
+ child.off("exit", onExit);
26175
+ }
26176
+ }
26177
+ function describeExit(code, signal) {
26178
+ return signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`;
26179
+ }
26180
+ function launcherControlHeaders(port) {
26181
+ return { origin: `http://127.0.0.1:${port}`, "cache-control": "no-store" };
26182
+ }
26183
+ async function readLauncherStatus(port, fetcher = fetch) {
26184
+ const controller = new AbortController();
26185
+ const timeout = setTimeout(() => controller.abort(), 1e3);
26186
+ timeout.unref?.();
26187
+ try {
26188
+ const response = await fetcher(`http://127.0.0.1:${port}${FRIZZ_ROUTE_PREFIX}/control/status`, {
26189
+ signal: controller.signal,
26190
+ headers: launcherControlHeaders(port)
25930
26191
  });
25931
- },
25932
- npmExec({ packageSpec, bin, args: args2, cwd, env }) {
25933
- return new Promise((settle) => {
25934
- const child = spawn3("npm", ["exec", "--yes", `--package=${packageSpec}`, "--", bin, ...args2], {
25935
- cwd,
25936
- env,
25937
- stdio: ["ignore", "pipe", "pipe"]
25938
- });
25939
- let stdout = "";
25940
- let stderr = "";
25941
- child.stdout.setEncoding("utf8").on("data", (chunk) => {
25942
- stdout += chunk;
25943
- });
25944
- child.stderr.setEncoding("utf8").on("data", (chunk) => {
25945
- stderr += chunk;
26192
+ if (!response.ok) return void 0;
26193
+ const body = await response.json();
26194
+ return {
26195
+ ...typeof body.version === "string" ? { version: body.version } : {},
26196
+ ...typeof body.state === "string" ? { state: body.state } : {}
26197
+ };
26198
+ } catch {
26199
+ return void 0;
26200
+ } finally {
26201
+ clearTimeout(timeout);
26202
+ }
26203
+ }
26204
+ function isGlobalInstall(bundleDir, env = process.env) {
26205
+ if (env.npm_execpath) return false;
26206
+ const segments = bundleDir.split(/[\\/]/u);
26207
+ return segments.includes("node_modules") && !segments.includes("_npx");
26208
+ }
26209
+ var GLOBAL_INSTALL_ENV = "FRIZZ_REGISTRY_GLOBAL_INSTALL";
26210
+ function keepUpdateHint(packageName, version) {
26211
+ return `this session only \u2014 run npm i -g ${packageName}@${version} to keep it`;
26212
+ }
26213
+ var NPM_VIEW_TIMEOUT_MS = 3e4;
26214
+ var NPM_EXEC_TIMEOUT_MS = 10 * 6e4;
26215
+ var NPM_KILL_SIGNAL = "SIGTERM";
26216
+ function formatTimeout(ms) {
26217
+ return ms >= 6e4 ? `${Math.ceil(ms / 6e4)}m` : `${Math.ceil(ms / 1e3)}s`;
26218
+ }
26219
+ function createNpmRegistryReleaseAdapter(options2 = {}) {
26220
+ const viewTimeoutMs = options2.viewTimeoutMs ?? NPM_VIEW_TIMEOUT_MS;
26221
+ const execTimeoutMs = options2.execTimeoutMs ?? NPM_EXEC_TIMEOUT_MS;
26222
+ const npmInvocation = options2.npm ?? (() => resolveNpmInvocation());
26223
+ return {
26224
+ latestVersion(packageName) {
26225
+ return new Promise((resolveVersion, reject) => {
26226
+ let npm;
26227
+ try {
26228
+ npm = npmInvocation();
26229
+ } catch (error) {
26230
+ return reject(error);
26231
+ }
26232
+ execFile4(
26233
+ npm.command,
26234
+ [...npm.prefixArgs, "view", `${packageName}@latest`, "version", "--json"],
26235
+ { encoding: "utf8", windowsHide: true, timeout: viewTimeoutMs, killSignal: NPM_KILL_SIGNAL },
26236
+ (error, stdout) => {
26237
+ if (error) {
26238
+ const why = error.killed && error.signal === NPM_KILL_SIGNAL ? `npm view did not answer within ${formatTimeout(viewTimeoutMs)}` : error.message;
26239
+ return reject(new Error(`could not check npm for ${packageName}: ${why}`));
26240
+ }
26241
+ try {
26242
+ const parsed = JSON.parse(stdout);
26243
+ resolveVersion(typeof parsed === "string" ? parsed : String(parsed));
26244
+ } catch {
26245
+ resolveVersion(stdout.trim().replaceAll('"', ""));
26246
+ }
26247
+ }
26248
+ );
25946
26249
  });
25947
- child.once("error", (error) => settle({ code: null, signal: null, stdout, stderr: `${stderr}
26250
+ },
26251
+ npmExec({ packageSpec, bin, args: args2, cwd, env }) {
26252
+ return new Promise((settle) => {
26253
+ let npm;
26254
+ try {
26255
+ npm = npmInvocation();
26256
+ } catch (error) {
26257
+ return settle({ code: null, signal: null, stdout: "", stderr: error instanceof Error ? error.message : String(error) });
26258
+ }
26259
+ const started = Date.now();
26260
+ const child = spawn3(npm.command, [...npm.prefixArgs, "exec", "--yes", `--package=${packageSpec}`, "--", bin, ...args2], {
26261
+ cwd,
26262
+ env,
26263
+ stdio: ["ignore", "pipe", "pipe"],
26264
+ windowsHide: true,
26265
+ timeout: execTimeoutMs,
26266
+ killSignal: NPM_KILL_SIGNAL
26267
+ });
26268
+ let stdout = "";
26269
+ let stderr = "";
26270
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
26271
+ stdout += chunk;
26272
+ });
26273
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
26274
+ stderr += chunk;
26275
+ });
26276
+ child.once("error", (error) => settle({ code: null, signal: null, stdout, stderr: `${stderr}
25948
26277
  ${error.message}` }));
25949
- child.once("close", (code, signal) => settle({ code, signal, stdout, stderr }));
25950
- });
25951
- },
25952
- spawnDetached({ entry, args: args2, cwd, env }) {
25953
- return spawn3(process.execPath, [entry, ...args2], { cwd, env, detached: true, stdio: "ignore" });
25954
- }
25955
- };
26278
+ child.once("close", (code, signal) => {
26279
+ const timedOut = signal === NPM_KILL_SIGNAL && Date.now() - started >= execTimeoutMs;
26280
+ settle({
26281
+ code,
26282
+ signal,
26283
+ stdout,
26284
+ stderr: timedOut ? `${stderr}
26285
+ npm exec ${packageSpec} did not finish within ${formatTimeout(execTimeoutMs)} and was stopped` : stderr
26286
+ });
26287
+ });
26288
+ });
26289
+ },
26290
+ spawnDetached({ entry, args: args2, cwd, env }) {
26291
+ return spawn3(process.execPath, [entry, ...args2], { cwd, env, detached: true, stdio: "ignore", windowsHide: true });
26292
+ }
26293
+ };
26294
+ }
26295
+ var npmRegistryReleaseAdapter = createNpmRegistryReleaseAdapter();
25956
26296
 
25957
26297
  // src/preflight.ts
25958
26298
  import { spawnSync } from "node:child_process";
25959
26299
  import { chmodSync as chmodSync2, statSync as statSync5 } from "node:fs";
25960
26300
  import { createRequire } from "node:module";
25961
- import { dirname as dirname12, join as join14 } from "node:path";
26301
+ import { dirname as dirname13, join as join15 } from "node:path";
25962
26302
  var SUPPORTED_NODE_LINES = [
25963
26303
  { major: 22, minor: 13 },
25964
26304
  { major: 23, minor: 4 }
@@ -26009,8 +26349,8 @@ function ensureNativeHelperPermissions(options2 = {}) {
26009
26349
  const chmod = options2.chmod ?? ((path, mode) => chmodSync2(path, mode));
26010
26350
  const resolvePty = options2.resolvePty ?? (() => createRequire(import.meta.url).resolve("node-pty/package.json"));
26011
26351
  try {
26012
- const helper = join14(
26013
- dirname12(resolvePty()),
26352
+ const helper = join15(
26353
+ dirname13(resolvePty()),
26014
26354
  "prebuilds",
26015
26355
  `${platform}-${options2.arch ?? process.arch}`,
26016
26356
  "spawn-helper"
@@ -26029,7 +26369,7 @@ init_project_identity();
26029
26369
  var PACKAGE_NAME = process.env.FRIZZ_REGISTRY_PACKAGE ?? "frizz";
26030
26370
  function resolvePackageVersion() {
26031
26371
  try {
26032
- const manifest = JSON.parse(readFileSync13(join15(import.meta.dirname, "..", "package.json"), "utf8"));
26372
+ const manifest = JSON.parse(readFileSync13(join16(import.meta.dirname, "..", "package.json"), "utf8"));
26033
26373
  if (typeof manifest.version === "string" && manifest.version.trim()) return manifest.version.trim();
26034
26374
  } catch {
26035
26375
  }
@@ -26042,7 +26382,7 @@ if (rawArgs.includes(PRODUCTION_PRINT_LAUNCHER_FLAG)) {
26042
26382
  console.log(fileURLToPath2(import.meta.url));
26043
26383
  process.exit(0);
26044
26384
  }
26045
- var args = rawArgs.filter((arg) => arg !== PRODUCTION_REEXEC_FLAG);
26385
+ var args = reexec ? reexecArgv(rawArgs) : rawArgs;
26046
26386
  var fail = (error) => {
26047
26387
  console.error(`frizz: ${error instanceof Error ? error.message : error}`);
26048
26388
  process.exit(1);
@@ -26074,6 +26414,8 @@ Options:
26074
26414
  --link print a fresh single-use access link for the running board
26075
26415
  --sessions list the devices signed in to the running board
26076
26416
  --sign-out <id|all> sign one device out, or every one of them
26417
+ --status report the board running for this project: address, pid, version
26418
+ --stop stop the board running for this project (running agents keep going)
26077
26419
  --debug stream the full event feed to the terminal instead of the compact readout
26078
26420
  -h, --help show this help
26079
26421
 
@@ -26091,6 +26433,8 @@ var sandbox = options.sandbox ? prepareSandbox() : null;
26091
26433
  if (sandbox) process.on("exit", () => cleanupSandbox(sandbox.home));
26092
26434
  var remote = null;
26093
26435
  var paneHost = null;
26436
+ var rearmPaneHost = null;
26437
+ var globalInstall = process.env[GLOBAL_INSTALL_ENV] === "1" || isGlobalInstall(import.meta.dirname);
26094
26438
  var accessPane = null;
26095
26439
  var activeAccessLink = null;
26096
26440
  var launchIntent;
@@ -26117,6 +26461,34 @@ var workspace = (() => {
26117
26461
  }
26118
26462
  })();
26119
26463
  process.chdir(workspace.root);
26464
+ if (options.stop) {
26465
+ try {
26466
+ const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace) });
26467
+ const where = control.host ? workspace.root : `${workspace.root} (the board was launched from ${control.target.projectDir})`;
26468
+ const result = await stopProjectLaunch({ stateDir: control.stateDir, target: control.target });
26469
+ console.log(
26470
+ result.kind !== "stopped" ? `Frizz is not running for ${where}` : result.stale ? `Frizz was not running for ${where}; cleared the record a dead board left behind` : `stopped Frizz for ${where}; running agents keep going \u2014 they are detached daemons`
26471
+ );
26472
+ process.exit(0);
26473
+ } catch (error) {
26474
+ fail(error);
26475
+ }
26476
+ }
26477
+ if (options.status) {
26478
+ const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace) });
26479
+ const running = await runningFrizzStatus({ stateDir: control.stateDir, target: control.target });
26480
+ if (!running) {
26481
+ console.log(`Frizz is not running for ${workspace.root}`);
26482
+ process.exit(1);
26483
+ }
26484
+ console.log(`running: http://127.0.0.1:${running.port}`);
26485
+ console.log(`workspace: ${workspace.root}`);
26486
+ if (!control.host) console.log(`launched from: ${control.target.projectDir} (one server serves every project)`);
26487
+ console.log(`supervisor pid: ${running.pid}`);
26488
+ console.log(`version: ${running.version ?? "unknown"}`);
26489
+ if (running.state && running.state !== "ready") console.log(`state: ${running.state}${running.message ? ` \u2014 ${running.message}` : ""}`);
26490
+ process.exit(0);
26491
+ }
26120
26492
  if (options.sessions || options.signOut !== void 0) {
26121
26493
  const running = liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace));
26122
26494
  if (!running?.port) {
@@ -26207,7 +26579,7 @@ async function openOrPrint(port, reused, path = "") {
26207
26579
  readout?.begin("browser", options.appMode ? "requesting app window" : "requesting default browser");
26208
26580
  try {
26209
26581
  if (options.appMode) {
26210
- await launchApp(url, { dataPath: join15(workspace.stateDir, "browser-profile") });
26582
+ await launchApp(url, { dataPath: join16(workspace.stateDir, "browser-profile") });
26211
26583
  browser = reused ? "focused the Frizz app window" : "opened the Frizz app window";
26212
26584
  } else {
26213
26585
  await launchBrowserTab(url);
@@ -26265,13 +26637,13 @@ async function runSupervisor(port, token) {
26265
26637
  target,
26266
26638
  owner.token
26267
26639
  );
26268
- const webDist = join15(import.meta.dirname, "..", "web-dist");
26269
- const runtimeDir = join15(import.meta.dirname, "..", "runtime");
26270
- const scriptsDir = join15(runtimeDir, "board");
26271
- const workerPluginDir = join15(runtimeDir, "cc-worker");
26640
+ const webDist = join16(import.meta.dirname, "..", "web-dist");
26641
+ const runtimeDir = join16(import.meta.dirname, "..", "runtime");
26642
+ const scriptsDir = join16(runtimeDir, "board");
26643
+ const workerPluginDir = join16(runtimeDir, "cc-worker");
26272
26644
  const childEntry = fileURLToPath2(new URL("./dev-child.js", import.meta.url));
26273
26645
  let plannedSuccessor;
26274
- let updateAvailable = true;
26646
+ let updateAvailable = false;
26275
26647
  let updateVersion;
26276
26648
  const refreshUpdateAvailable = async () => {
26277
26649
  try {
@@ -26325,22 +26697,56 @@ async function runSupervisor(port, token) {
26325
26697
  return { state: "failed", message: error instanceof Error ? error.message : String(error) };
26326
26698
  }
26327
26699
  },
26700
+ // The registry launcher promotes no pointer — the release sits in npm's own execution cache and
26701
+ // the running package is never touched — so there is nothing to roll back. Present and empty
26702
+ // rather than absent: the supervisor appends "rollback callback is unavailable" to every failed
26703
+ // handoff it cannot roll back, which for this launcher was noise on a message that already said
26704
+ // what went wrong (audit 2026-09-11, finding 4).
26705
+ rollbackUpdate: () => {
26706
+ },
26328
26707
  durableReexec: async () => {
26329
26708
  const successor = plannedSuccessor;
26330
26709
  if (!successor) throw new Error("no update was prepared");
26331
26710
  const { plan } = successor;
26332
- const successorEnv = { ...env, FRIZZ_REGISTRY_PACKAGE: plan.packageName, FRIZZ_REGISTRY_VERSION: plan.latestVersion };
26333
- if (typeof process.execve === "function") {
26711
+ const successorEnv = {
26712
+ ...env,
26713
+ FRIZZ_REGISTRY_PACKAGE: plan.packageName,
26714
+ FRIZZ_REGISTRY_VERSION: plan.latestVersion,
26715
+ ...globalInstall ? { [GLOBAL_INSTALL_ENV]: "1" } : {}
26716
+ };
26717
+ if (canReexecInPlace()) {
26334
26718
  activityReadout?.notice("progress", "Updating", `reloading this launcher in place on Frizz ${plan.latestVersion}`);
26335
26719
  paneHost?.dispose();
26336
26720
  remote?.stop();
26337
- reexecIntoRegistrySuccessor(successor, { port, env: successorEnv });
26721
+ try {
26722
+ reexecIntoRegistrySuccessor(successor, { port, env: successorEnv });
26723
+ } catch (error) {
26724
+ rearmPaneHost?.();
26725
+ try {
26726
+ await remote?.serveSaved();
26727
+ } catch (remoteError) {
26728
+ const message = remoteError instanceof Error ? remoteError.message : String(remoteError);
26729
+ logger.error("remote", `could not re-arm the saved remote setup after a failed update: ${message}`);
26730
+ }
26731
+ throw error;
26732
+ }
26338
26733
  }
26339
- handoffToRegistrySuccessor(successor, { port, cwd: workspace.root, env: successorEnv }, npmRegistryReleaseAdapter);
26340
- readout?.notice("done", "Updated", `Frizz ${plan.latestVersion} is taking over on port ${port}`);
26341
- readout?.note(`
26342
- Frizz ${plan.latestVersion} now runs in the background \u2014 ctrl-c here no longer reaches it. Stop it with ${PACKAGE_NAME} --stop.
26343
- `);
26734
+ const child = handoffToRegistrySuccessor(successor, { port, cwd: workspace.root, env: successorEnv }, npmRegistryReleaseAdapter);
26735
+ activityReadout?.notice("progress", "Updating", `starting Frizz ${plan.latestVersion} in the background and waiting for it to answer on port ${port}`);
26736
+ await awaitRegistrySuccessor(
26737
+ child,
26738
+ { version: plan.latestVersion, port, logFile: successorEnv.FRIZZ_LOG_FILE },
26739
+ { status: () => readLauncherStatus(port) }
26740
+ );
26741
+ paneHost?.dispose();
26742
+ remote?.stop();
26743
+ activityReadout?.notice("done", "Updated", `Frizz ${plan.latestVersion} is serving on port ${port}`);
26744
+ activityReadout?.note(
26745
+ `
26746
+ Frizz ${plan.latestVersion} now runs in the background \u2014 ctrl-c here no longer reaches it. Stop it with ${PACKAGE_NAME} --stop.${globalInstall ? `
26747
+ Update: ${keepUpdateHint(PACKAGE_NAME, plan.latestVersion)}.` : ""}
26748
+ `
26749
+ );
26344
26750
  process.exit(0);
26345
26751
  }
26346
26752
  });
@@ -26364,7 +26770,12 @@ async function runSupervisor(port, token) {
26364
26770
  onChanged: (config) => logger.info("remote", config ? `reached at https://${config.hostname}` : "loopback only"),
26365
26771
  sandbox: sandbox !== null
26366
26772
  });
26367
- paneHost = installPaneHost({ bindings: { l: accessPane, L: accessPane, r: remotePane, R: remotePane } });
26773
+ const bindings = { l: accessPane, L: accessPane, r: remotePane, R: remotePane };
26774
+ const armPaneHost = () => {
26775
+ paneHost = installPaneHost({ bindings });
26776
+ };
26777
+ armPaneHost();
26778
+ rearmPaneHost = armPaneHost;
26368
26779
  const stop = createSupervisorShutdownHandler({
26369
26780
  close: () => supervisor.close(),
26370
26781
  force: () => supervisor.forceStop(),
@@ -26398,7 +26809,13 @@ async function runSupervisor(port, token) {
26398
26809
  process.on("SIGHUP", stop);
26399
26810
  void supervisor.stopRequested.then(stop);
26400
26811
  await supervisor.firstBoot;
26401
- if (reexec) activityReadout?.notice("done", "Updated", `Frizz ${PACKAGE_VERSION} is serving on port ${port} \xB7 ctrl-c to stop`);
26812
+ if (reexec) {
26813
+ activityReadout?.notice(
26814
+ "done",
26815
+ "Updated",
26816
+ `Frizz ${PACKAGE_VERSION} is serving on port ${port} \xB7 ctrl-c to stop${globalInstall ? ` \xB7 ${keepUpdateHint(PACKAGE_NAME, PACKAGE_VERSION)}` : ""}`
26817
+ );
26818
+ }
26402
26819
  return await new Promise(() => {
26403
26820
  });
26404
26821
  }