@puddle-code/cli 0.2.4 → 0.2.7

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 (132) hide show
  1. package/CHANGELOG.md +4 -14
  2. package/README.md +33 -23
  3. package/dist/host-control.mjs +7 -4
  4. package/dist/index.js +508 -315
  5. package/dist/install-cli.sh +167 -0
  6. package/dist/install.sh +7 -10
  7. package/dist/public/assets/{App-C2IQg_6K.js → App-BkCGcO4p.js} +2 -2
  8. package/dist/public/assets/{Dashboard-DUFOyL1_.js → Dashboard-BOK1fw_Q.js} +1 -1
  9. package/dist/public/assets/{EditorTabClose-C3wpMhYL.js → EditorTabClose-CzMQj4jF.js} +1 -1
  10. package/dist/public/assets/{FindWidget-Dt1gfA70.js → FindWidget-6LifDkeS.js} +1 -1
  11. package/dist/public/assets/{LazyTerminal-B9HzmRBx.js → LazyTerminal-Beep0VuJ.js} +2 -2
  12. package/dist/public/assets/{ModelRefcount-CxzQ3w05.js → ModelRefcount-F2Tf2V5f.js} +1 -1
  13. package/dist/public/assets/{NewProjectDialog-Cq_1po_h.js → NewProjectDialog-CoI17sqS.js} +1 -1
  14. package/dist/public/assets/{PaneEditorBody-zS2BI5Fn.js → PaneEditorBody-57BHgi2R.js} +4 -4
  15. package/dist/public/assets/{PdfViewer-ckjWE_TR.js → PdfViewer-T-uYvXEd.js} +2 -2
  16. package/dist/public/assets/SettingsDialog-CkBdXZyv.js +8 -0
  17. package/dist/public/assets/{Terminal-BqbjwcMi.js → Terminal-CJMmk0kO.js} +1 -1
  18. package/dist/public/assets/{Workspace-BVDgHkML.js → Workspace-0P5NTRzC.js} +6 -6
  19. package/dist/public/assets/{abnfDiagram-VCTEODGH-JZdwYDGF.js → abnfDiagram-VCTEODGH-Dl2GTGCZ.js} +1 -1
  20. package/dist/public/assets/architecture-7GRP2DOG-UXEF_VQt.js +1 -0
  21. package/dist/public/assets/{architectureDiagram-5GKGNRK7-CYoRlLvs.js → architectureDiagram-5GKGNRK7-TLkiib9Z.js} +1 -1
  22. package/dist/public/assets/{blockDiagram-I7D4REHJ-DjPQ4-5b.js → blockDiagram-I7D4REHJ-CFdDK-5H.js} +1 -1
  23. package/dist/public/assets/{buffer-store-GUPJI8Wy.js → buffer-store-Pex0dRS2.js} +2 -2
  24. package/dist/public/assets/{button-CKes3Qwq.js → button-IwQVXqQs.js} +1 -1
  25. package/dist/public/assets/{c4Diagram-7LVT6UL2-BvwPGpk0.js → c4Diagram-7LVT6UL2-9M5y_7Rr.js} +1 -1
  26. package/dist/public/assets/channel-D3NmDqec.js +1 -0
  27. package/dist/public/assets/{chunk-2E4U76K2-DcsuTdxj.js → chunk-2E4U76K2-BEq-rPTx.js} +1 -1
  28. package/dist/public/assets/{chunk-4HAMMTFA-BGEaVxkg.js → chunk-4HAMMTFA-DSsufXZj.js} +1 -1
  29. package/dist/public/assets/{chunk-75Z2AOVW-BOvbyaVc.js → chunk-75Z2AOVW-CUihRphW.js} +1 -1
  30. package/dist/public/assets/{chunk-CLGD4ZFX-8fG7spZS.js → chunk-CLGD4ZFX-8FntHoH6.js} +1 -1
  31. package/dist/public/assets/{chunk-DU6HZSFF-C94gDWXH.js → chunk-DU6HZSFF-BhI6mdvA.js} +1 -1
  32. package/dist/public/assets/{chunk-F27PBJKO-CZD-A2b3.js → chunk-F27PBJKO-CflkfzDR.js} +1 -1
  33. package/dist/public/assets/{chunk-GMAD6QVW-C7KK3kiY.js → chunk-GMAD6QVW-C6sQnCBM.js} +1 -1
  34. package/dist/public/assets/{chunk-GVQU2GXP-CDFKc0sW.js → chunk-GVQU2GXP-BTEA9yBL.js} +1 -1
  35. package/dist/public/assets/{chunk-IMKFNOWR-BWQiE1Cp.js → chunk-IMKFNOWR-BTN8pZaT.js} +1 -1
  36. package/dist/public/assets/{chunk-L3NEJ4N5-DqP3WOXV.js → chunk-L3NEJ4N5-CPMRHY2b.js} +1 -1
  37. package/dist/public/assets/{chunk-OSK3NFVY-BlmYDJRN.js → chunk-OSK3NFVY-D62Fb29m.js} +1 -1
  38. package/dist/public/assets/{chunk-P2QGCYS3-v7eQ-UQT.js → chunk-P2QGCYS3-B_AAuki5.js} +1 -1
  39. package/dist/public/assets/{chunk-POPQ4Y6H-D_gxItFL.js → chunk-POPQ4Y6H-C-wGGj3u.js} +1 -1
  40. package/dist/public/assets/{chunk-PWAF6VOD-Ds90M0Eb.js → chunk-PWAF6VOD-DElrnMgG.js} +1 -1
  41. package/dist/public/assets/{chunk-SHT3W25Y-B8oepiFw.js → chunk-SHT3W25Y-D4tV8JU2.js} +1 -1
  42. package/dist/public/assets/{chunk-SVP7TREG-DjFxplBp.js → chunk-SVP7TREG-CFj2bSX9.js} +1 -1
  43. package/dist/public/assets/{chunk-TICWLB2K-GEeYOgw4.js → chunk-TICWLB2K-B-5yvDQM.js} +1 -1
  44. package/dist/public/assets/{chunk-TLUHSLCS-CEnN9wOB.js → chunk-TLUHSLCS-CpBFfaGS.js} +2 -2
  45. package/dist/public/assets/classDiagram-ZZMXUADV-8X6mwi75.js +1 -0
  46. package/dist/public/assets/classDiagram-v2-VYDZK3BY-8X6mwi75.js +1 -0
  47. package/dist/public/assets/{context-menu-CmSSwq3B.js → context-menu-Bh2z-xpu.js} +1 -1
  48. package/dist/public/assets/{cssMode-BEaREsxj.js → cssMode-iZ32bnYe.js} +1 -1
  49. package/dist/public/assets/{cynefin-OW5HDTMX-C_sl2wZ-.js → cynefin-OW5HDTMX-D5BzXyX_.js} +1 -1
  50. package/dist/public/assets/{cynefinDiagram-5FMLGOSQ-CPpFUPl3.js → cynefinDiagram-5FMLGOSQ-CdbgNH0t.js} +1 -1
  51. package/dist/public/assets/{dagre-GXQ25YYZ-BryapOat.js → dagre-GXQ25YYZ-B7zDDwyp.js} +1 -1
  52. package/dist/public/assets/{diagram-S7CK7UJ4-dIdCar-k.js → diagram-S7CK7UJ4-WMtT-mtt.js} +1 -1
  53. package/dist/public/assets/{diagram-UQ7AKVKN-EcAWwfjf.js → diagram-UQ7AKVKN-DtvvWbC3.js} +1 -1
  54. package/dist/public/assets/{diagram-VSXAHHWV-Dpl4gvkd.js → diagram-VSXAHHWV-DBLLhbuk.js} +1 -1
  55. package/dist/public/assets/{diagram-VX7I27RA-CJtemv3e.js → diagram-VX7I27RA-6sDOwqqU.js} +1 -1
  56. package/dist/public/assets/{diagram-Z3DM3KII-CPSnQEgD.js → diagram-Z3DM3KII-giEr0RfN.js} +1 -1
  57. package/dist/public/assets/{dialog-Dam8WdmS.js → dialog-ClHoWaGa.js} +1 -1
  58. package/dist/public/assets/{disclosure-Ex-68BU4.js → disclosure-Ch2qCUT3.js} +1 -1
  59. package/dist/public/assets/{dist-ByY6lqkl.js → dist-C1IA4PaM.js} +1 -1
  60. package/dist/public/assets/{dist-CxMnSex-.js → dist-ujnACQRA.js} +1 -1
  61. package/dist/public/assets/{ebnfDiagram-PWID7BFC-uXsUC0ll.js → ebnfDiagram-PWID7BFC-CDfLo0uv.js} +1 -1
  62. package/dist/public/assets/{erDiagram-RLTQ6QDP-D5vJ6ZLT.js → erDiagram-RLTQ6QDP-z5xLMSlR.js} +1 -1
  63. package/dist/public/assets/eventmodeling-NTZA5JFV-CzmQ0jM3.js +1 -0
  64. package/dist/public/assets/flowDiagram-HODETNUW-_tJIQrw7.js +1 -0
  65. package/dist/public/assets/{ganttDiagram-EL5Y4UJY-C6yTyvpx.js → ganttDiagram-EL5Y4UJY-C2poetEB.js} +1 -1
  66. package/dist/public/assets/{gitGraph-4MIJSDKK-C7nVbcZ1.js → gitGraph-4MIJSDKK-D40KkO3l.js} +1 -1
  67. package/dist/public/assets/{gitGraphDiagram-WWUBYQGX-DByBzF_5.js → gitGraphDiagram-WWUBYQGX-B4839p8c.js} +1 -1
  68. package/dist/public/assets/{hotkeys-MSGaeY2m.js → hotkeys-CY98l7MF.js} +1 -1
  69. package/dist/public/assets/{htmlMode-DirKEo5j.js → htmlMode-XXBiFVzW.js} +1 -1
  70. package/dist/public/assets/{index-C1a3rCk2.js → index-D0Lm0Z_9.js} +2 -2
  71. package/dist/public/assets/index-uKreLbru.css +2 -0
  72. package/dist/public/assets/{info-A6RAGUB7-BCEGc_J2.js → info-A6RAGUB7-R6D6gcdH.js} +1 -1
  73. package/dist/public/assets/{infoDiagram-27XIBGKW-DDVeVGK9.js → infoDiagram-27XIBGKW-CQMe7eLM.js} +1 -1
  74. package/dist/public/assets/{input-i-kiT55N.js → input-BUZTRTzW.js} +1 -1
  75. package/dist/public/assets/{ishikawaDiagram-5VMMS53U-CTZvKcdI.js → ishikawaDiagram-5VMMS53U-hWJnyGf_.js} +1 -1
  76. package/dist/public/assets/{journeyDiagram-3NMN7TZE-CwFCP_8e.js → journeyDiagram-3NMN7TZE-DRy7w8VL.js} +1 -1
  77. package/dist/public/assets/{jsonMode-BLMO7T1W.js → jsonMode-B4KgbobV.js} +1 -1
  78. package/dist/public/assets/{kanban-definition-UXKFOSKX-BCdCN6ov.js → kanban-definition-UXKFOSKX-Dvv0eWiD.js} +1 -1
  79. package/dist/public/assets/{line-e7ifLbsv.js → line-K2RkIT6o.js} +1 -1
  80. package/dist/public/assets/{media-refresh-store-d6Ihz_Yj.js → media-refresh-store-OAgBTFYO.js} +1 -1
  81. package/dist/public/assets/{mermaid-parser.core-DfmtQ5si.js → mermaid-parser.core-B36v6vc8.js} +3 -3
  82. package/dist/public/assets/{mermaid.core-B0Ceo892.js → mermaid.core-kqdkh9ua.js} +3 -3
  83. package/dist/public/assets/{mindmap-definition-YA3MSWOX-BKWZ0hGZ.js → mindmap-definition-YA3MSWOX-CykDXfif.js} +1 -1
  84. package/dist/public/assets/{notification-permission-DThcB8rO.js → notification-permission-B9hBLJAo.js} +1 -1
  85. package/dist/public/assets/{packet-AYTQ26CC-C_WSBR7W.js → packet-AYTQ26CC-BDu33u4C.js} +1 -1
  86. package/dist/public/assets/{pdf-CW6jyF4i.js → pdf-BTD1hhPr.js} +1 -1
  87. package/dist/public/assets/{pegDiagram-XKGWAZYB-CQw-vxyv.js → pegDiagram-XKGWAZYB-DWdbGFng.js} +1 -1
  88. package/dist/public/assets/{pie-WAS4IAKB-CyAolSkk.js → pie-WAS4IAKB-CeKUt-Xf.js} +1 -1
  89. package/dist/public/assets/{pieDiagram-E7YTZNPT-fEikc7gQ.js → pieDiagram-E7YTZNPT-D94ZuRp2.js} +1 -1
  90. package/dist/public/assets/{project-abbrev-0K47Phcy.js → project-abbrev-E6FngLNw.js} +1 -1
  91. package/dist/public/assets/protocol-support-C3qZDXsO.js +1 -0
  92. package/dist/public/assets/{quadrantDiagram-AXDQQJYC-BLZF9CN6.js → quadrantDiagram-AXDQQJYC-DXkTGcPo.js} +1 -1
  93. package/dist/public/assets/{radar-RG4KPBEZ-DZj09Z3T.js → radar-RG4KPBEZ-C1wtnYrQ.js} +1 -1
  94. package/dist/public/assets/{railroad-74A4TZTK-DrrM4RhH.js → railroad-74A4TZTK-Dx_D-zM9.js} +1 -1
  95. package/dist/public/assets/railroad-abnf-HS5TGJTU-CBfnoAOv.js +1 -0
  96. package/dist/public/assets/railroad-ebnf-LZEXJU2U-0kuF9A11.js +1 -0
  97. package/dist/public/assets/railroad-peg-WCYAUIDC-DCe0oM6p.js +1 -0
  98. package/dist/public/assets/{railroadDiagram-O6MQD6OU-BjnjeLPo.js → railroadDiagram-O6MQD6OU-DVuwN3bC.js} +1 -1
  99. package/dist/public/assets/{requirementDiagram-BXWQKSXE-CJnHoVjp.js → requirementDiagram-BXWQKSXE-nX3JtB1N.js} +1 -1
  100. package/dist/public/assets/{sankeyDiagram-P5KCCOFB-BFBAycgU.js → sankeyDiagram-P5KCCOFB-DvErANKF.js} +1 -1
  101. package/dist/public/assets/{sequenceDiagram-WJ2MYXX4-IgIUmdp2.js → sequenceDiagram-WJ2MYXX4-BruH0Mma.js} +1 -1
  102. package/dist/public/assets/{stateDiagram-D77RDMKH-C8ragRoI.js → stateDiagram-D77RDMKH-BMiYrZ8Y.js} +1 -1
  103. package/dist/public/assets/stateDiagram-v2-MP3YSRHH-CF86bnO8.js +1 -0
  104. package/dist/public/assets/{swimlanes-42K2YHIH-mm0BJE0h.js → swimlanes-42K2YHIH-CK3jGfyc.js} +1 -1
  105. package/dist/public/assets/swimlanesDiagram-VR7AAH4N-COC62XcR.js +8 -0
  106. package/dist/public/assets/{switch-TfZl8YQu.js → switch-BrFrNOcn.js} +1 -1
  107. package/dist/public/assets/{timeline-definition-24CTP7MA-CLtACXsf.js → timeline-definition-24CTP7MA-DHf_pf2_.js} +1 -1
  108. package/dist/public/assets/{treeView-Q6P3EWNA-B_s8YnCD.js → treeView-Q6P3EWNA-C5uB7_Kg.js} +1 -1
  109. package/dist/public/assets/{treemap-WGGIJYW6-CiyeynoG.js → treemap-WGGIJYW6-BGzjaDxJ.js} +1 -1
  110. package/dist/public/assets/{tsMode-BJ9DHdAB.js → tsMode-C39Hmal5.js} +1 -1
  111. package/dist/public/assets/{use-ui-state-2bNBLDPU.js → use-ui-state-D5kpevK-.js} +1 -1
  112. package/dist/public/assets/{vennDiagram-4TSXK5OY-C9rcT-1u.js → vennDiagram-4TSXK5OY-9mb8KINT.js} +1 -1
  113. package/dist/public/assets/{wardley-WFR3VGLG-D_hqUvOM.js → wardley-WFR3VGLG-DW5VvBTA.js} +1 -1
  114. package/dist/public/assets/{wardleyDiagram-VM6X3IG4-Dfn7mGAc.js → wardleyDiagram-VM6X3IG4-DCM5g6RC.js} +1 -1
  115. package/dist/public/assets/{ws-CDXqi1pN.js → ws-BZ4-LssP.js} +1 -1
  116. package/dist/public/assets/{xychartDiagram-S5SC5T6Z-vwVJPUe2.js → xychartDiagram-S5SC5T6Z-Bks8gKZc.js} +1 -1
  117. package/dist/public/index.html +2 -2
  118. package/package.json +1 -1
  119. package/dist/public/assets/SettingsDialog-Cb2fdV1D.js +0 -8
  120. package/dist/public/assets/architecture-7GRP2DOG-RMEcXYv9.js +0 -1
  121. package/dist/public/assets/channel-BheYsxDc.js +0 -1
  122. package/dist/public/assets/classDiagram-ZZMXUADV-CDDLBiV5.js +0 -1
  123. package/dist/public/assets/classDiagram-v2-VYDZK3BY-CDDLBiV5.js +0 -1
  124. package/dist/public/assets/eventmodeling-NTZA5JFV-WlMCgowE.js +0 -1
  125. package/dist/public/assets/flowDiagram-HODETNUW-DJ99l88N.js +0 -1
  126. package/dist/public/assets/index-Ctw2P0rY.css +0 -2
  127. package/dist/public/assets/protocol-support-C1NCVjbV.js +0 -1
  128. package/dist/public/assets/railroad-abnf-HS5TGJTU-lXe6u3FO.js +0 -1
  129. package/dist/public/assets/railroad-ebnf-LZEXJU2U-C7R4jvUV.js +0 -1
  130. package/dist/public/assets/railroad-peg-WCYAUIDC-B24HGcGJ.js +0 -1
  131. package/dist/public/assets/stateDiagram-v2-MP3YSRHH-AU0bFC0Z.js +0 -1
  132. package/dist/public/assets/swimlanesDiagram-VR7AAH4N-cBVuNXB5.js +0 -8
package/dist/index.js CHANGED
@@ -3756,7 +3756,7 @@ var init_wrapper = __esm({
3756
3756
 
3757
3757
  // src/lib/ws-client.ts
3758
3758
  function connectGateway(port, authority) {
3759
- return new Promise((resolve5, reject) => {
3759
+ return new Promise((resolve6, reject) => {
3760
3760
  const ws = new wrapper_default(`ws://127.0.0.1:${port}/ws`, {
3761
3761
  headers: { host: `localhost:${port}` }
3762
3762
  });
@@ -3781,7 +3781,7 @@ function connectGateway(port, authority) {
3781
3781
  }
3782
3782
  if (parsed.t === "authenticated") {
3783
3783
  clearTimeout(timer);
3784
- resolve5({
3784
+ resolve6({
3785
3785
  send(message) {
3786
3786
  if (resource.valid() && ws.readyState === wrapper_default.OPEN)
3787
3787
  ws.send(JSON.stringify(message));
@@ -3855,7 +3855,7 @@ async function attachSession(opts) {
3855
3855
  rows: streams.stdout.rows ?? 24
3856
3856
  });
3857
3857
  gateway.send({ t: "attach", session: session.id, term, ...dims2() });
3858
- return new Promise((resolve5) => {
3858
+ return new Promise((resolve6) => {
3859
3859
  let settled = false;
3860
3860
  const finish = (outcome) => {
3861
3861
  if (settled) return;
@@ -3864,7 +3864,7 @@ async function attachSession(opts) {
3864
3864
  stdin.removeListener("data", onStdin);
3865
3865
  unResize?.();
3866
3866
  gateway.close();
3867
- resolve5(outcome);
3867
+ resolve6(outcome);
3868
3868
  };
3869
3869
  gateway.onMessage((message) => {
3870
3870
  switch (message.t) {
@@ -3925,7 +3925,7 @@ import { createInterface } from "node:readline";
3925
3925
  import { Writable } from "node:stream";
3926
3926
 
3927
3927
  // ../shared/src/protocol.ts
3928
- var PROTOCOL_VERSION = { major: 21, minor: 2 };
3928
+ var PROTOCOL_VERSION = { major: 21, minor: 3 };
3929
3929
 
3930
3930
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
3931
3931
  var external_exports = {};
@@ -18754,6 +18754,7 @@ var agentTypeSchema = external_exports.object({
18754
18754
  var repoSchema = external_exports.object({
18755
18755
  id: rowId,
18756
18756
  path: external_exports.string(),
18757
+ /** Empty follows the branch currently checked out at path, resolved for each new session. */
18757
18758
  default_base_branch: external_exports.string(),
18758
18759
  onboarding_notes: external_exports.string().nullable(),
18759
18760
  fetch_enabled: external_exports.boolean(),
@@ -18764,7 +18765,8 @@ var repoWithOrphansSchema = repoSchema.extend({
18764
18765
  });
18765
18766
  var createRepoRequestSchema = external_exports.object({
18766
18767
  path: external_exports.string().min(1),
18767
- default_base_branch: external_exports.string().min(1).optional(),
18768
+ /** Empty follows the clone's current branch; omitted snapshots it at registration. */
18769
+ default_base_branch: external_exports.string().optional(),
18768
18770
  onboarding_notes: external_exports.string().nullable().optional(),
18769
18771
  fetch_enabled: external_exports.boolean().optional()
18770
18772
  });
@@ -18805,7 +18807,8 @@ var repoWorktreesResponseSchema = external_exports.object({
18805
18807
  orphan_branches: external_exports.array(orphanBranchSchema)
18806
18808
  });
18807
18809
  var patchRepoRequestSchema = external_exports.object({
18808
- default_base_branch: external_exports.string().min(1).optional(),
18810
+ /** Empty follows the clone's current branch; omitted leaves the setting unchanged. */
18811
+ default_base_branch: external_exports.string().optional(),
18809
18812
  onboarding_notes: external_exports.string().nullable().optional(),
18810
18813
  fetch_enabled: external_exports.boolean().optional()
18811
18814
  });
@@ -20039,7 +20042,7 @@ var LocalTransport = class {
20039
20042
  kind = "local";
20040
20043
  label = "this machine";
20041
20044
  exec(command, opts = {}) {
20042
- return new Promise((resolve5) => {
20045
+ return new Promise((resolve6) => {
20043
20046
  const child = spawn("sh", ["-c", command], { stdio: ["pipe", "pipe", "pipe"] });
20044
20047
  let stdout = "";
20045
20048
  let stderr = "";
@@ -20056,11 +20059,11 @@ var LocalTransport = class {
20056
20059
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20057
20060
  child.on("error", (err) => {
20058
20061
  if (timer) clearTimeout(timer);
20059
- resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20062
+ resolve6({ code: -1, stdout, stderr: stderr + String(err) });
20060
20063
  });
20061
20064
  child.on("close", (code) => {
20062
20065
  if (timer) clearTimeout(timer);
20063
- resolve5({ code: code ?? -1, stdout, stderr });
20066
+ resolve6({ code: code ?? -1, stdout, stderr });
20064
20067
  });
20065
20068
  child.stdin.on("error", () => {
20066
20069
  });
@@ -20154,7 +20157,7 @@ var SshTransport = class {
20154
20157
  * live control socket behind.
20155
20158
  */
20156
20159
  open() {
20157
- return new Promise((resolve5, reject) => {
20160
+ return new Promise((resolve6, reject) => {
20158
20161
  const child = spawn2(this.ssh, this.args(this.host, "true"), {
20159
20162
  // A GUI has no useful inherited terminal; OpenSSH calls its askpass
20160
20163
  // helper instead. The terminal CLI keeps byte-for-byte inheritance.
@@ -20166,7 +20169,7 @@ var SshTransport = class {
20166
20169
  (err) => reject(new CliError("ssh_unreachable", `could not run ${this.ssh}: ${err.message}`))
20167
20170
  );
20168
20171
  child.on("close", (code) => {
20169
- if (code === 0) resolve5();
20172
+ if (code === 0) resolve6();
20170
20173
  else {
20171
20174
  reject(
20172
20175
  new CliError(
@@ -20182,13 +20185,13 @@ var SshTransport = class {
20182
20185
  /** Whether the master connection is still alive (-O check). */
20183
20186
  isAlive() {
20184
20187
  if (!this.hasControlMaster) return Promise.resolve(true);
20185
- return new Promise((resolve5) => {
20188
+ return new Promise((resolve6) => {
20186
20189
  const child = spawn2(this.ssh, this.args("-O", "check", this.host), {
20187
20190
  stdio: "ignore",
20188
20191
  env: this.spawnEnv()
20189
20192
  });
20190
- child.on("error", () => resolve5(false));
20191
- child.on("close", (code) => resolve5(code === 0));
20193
+ child.on("error", () => resolve6(false));
20194
+ child.on("close", (code) => resolve6(code === 0));
20192
20195
  });
20193
20196
  }
20194
20197
  /**
@@ -20201,18 +20204,18 @@ var SshTransport = class {
20201
20204
  */
20202
20205
  cancelForward(localPort, remotePort) {
20203
20206
  if (!this.hasControlMaster) return Promise.resolve();
20204
- return new Promise((resolve5) => {
20207
+ return new Promise((resolve6) => {
20205
20208
  const spec = `${localPort}:127.0.0.1:${remotePort}`;
20206
20209
  const child = spawn2(this.ssh, this.args("-O", "cancel", "-L", spec, this.host), {
20207
20210
  stdio: "ignore",
20208
20211
  env: this.spawnEnv()
20209
20212
  });
20210
- child.on("error", () => resolve5());
20211
- child.on("close", () => resolve5());
20213
+ child.on("error", () => resolve6());
20214
+ child.on("close", () => resolve6());
20212
20215
  });
20213
20216
  }
20214
20217
  exec(command, opts = {}) {
20215
- return new Promise((resolve5) => {
20218
+ return new Promise((resolve6) => {
20216
20219
  const child = spawn2(this.ssh, this.args(this.host, "--", `sh -c ${shellQuote(command)}`), {
20217
20220
  stdio: ["pipe", "pipe", "pipe"],
20218
20221
  env: this.spawnEnv()
@@ -20232,11 +20235,11 @@ var SshTransport = class {
20232
20235
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20233
20236
  child.on("error", (err) => {
20234
20237
  if (timer) clearTimeout(timer);
20235
- resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20238
+ resolve6({ code: -1, stdout, stderr: stderr + String(err) });
20236
20239
  });
20237
20240
  child.on("close", (code) => {
20238
20241
  if (timer) clearTimeout(timer);
20239
- resolve5({ code: code ?? -1, stdout, stderr });
20242
+ resolve6({ code: code ?? -1, stdout, stderr });
20240
20243
  });
20241
20244
  child.stdin.on("error", () => {
20242
20245
  });
@@ -20256,11 +20259,11 @@ var SshTransport = class {
20256
20259
  });
20257
20260
  let stdout = "";
20258
20261
  let stderr = "";
20259
- const result = new Promise((resolve5) => {
20262
+ const result = new Promise((resolve6) => {
20260
20263
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
20261
20264
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20262
- child.on("error", (err) => resolve5({ code: -1, stdout, stderr: stderr + String(err) }));
20263
- child.on("close", (code) => resolve5({ code: code ?? -1, stdout, stderr }));
20265
+ child.on("error", (err) => resolve6({ code: -1, stdout, stderr: stderr + String(err) }));
20266
+ child.on("close", (code) => resolve6({ code: code ?? -1, stdout, stderr }));
20264
20267
  });
20265
20268
  return {
20266
20269
  result,
@@ -20275,14 +20278,14 @@ var SshTransport = class {
20275
20278
  }
20276
20279
  async copyTo(localPath, destPath) {
20277
20280
  await this.exec(`mkdir -p $(dirname ${destPath})`, { timeoutMs: 15e3 });
20278
- await new Promise((resolve5, reject) => {
20281
+ await new Promise((resolve6, reject) => {
20279
20282
  const child = spawn2(this.scp, [...this.controlArgs, localPath, `${this.host}:${destPath}`], {
20280
20283
  stdio: ["ignore", "ignore", "inherit"],
20281
20284
  env: this.spawnEnv()
20282
20285
  });
20283
20286
  child.on("error", reject);
20284
20287
  child.on("close", (code) => {
20285
- if (code === 0) resolve5();
20288
+ if (code === 0) resolve6();
20286
20289
  else reject(new CliError("ssh_unreachable", `scp to ${this.host} failed (exit ${code})`));
20287
20290
  });
20288
20291
  });
@@ -20383,7 +20386,7 @@ async function registrationCode() {
20383
20386
  const readline = createInterface({ input: process.stdin, output: silent, terminal: true });
20384
20387
  try {
20385
20388
  return remoteSecretSchema.parse(
20386
- await new Promise((resolve5) => readline.question("", resolve5))
20389
+ await new Promise((resolve6) => readline.question("", resolve6))
20387
20390
  );
20388
20391
  } finally {
20389
20392
  readline.close();
@@ -20507,6 +20510,8 @@ running in the background once ready, so the terminal may close (--foreground
20507
20510
  to stay attached; Ctrl-C then stops the cockpit). refresh stops a target's
20508
20511
  cockpit (even a wedged one) and runs the full launch flow again \u2014 tunnel,
20509
20512
  daemon restart if needed \u2014 keeping the old UI port so open tabs survive.
20513
+ An older host protocol prompts before updating its daemon; --no-upgrade
20514
+ aborts before prompting. Without a terminal, upgrade the daemon explicitly first.
20510
20515
  list shows running cockpits; kill stops one. Sessions keep running under a
20511
20516
  persistent supervisor; an SSH-attached daemon keeps their state for the next launch.
20512
20517
 
@@ -20516,7 +20521,7 @@ AppImage placed where you say (default ~/puddle), then opened in the file
20516
20521
  manager. Already installed and no @version \u2192 nothing changes.
20517
20522
  upgrade moves components to the newest release (or the named @version),
20518
20523
  installing any that are missing; with no component it covers everything
20519
- installed on the target \u2014 the CLI last, via npm. cli and desktop are
20524
+ installed on the target \u2014 the CLI last, using its installation channel. cli and desktop are
20520
20525
  client-machine artefacts: user@host targets the daemon only. remove
20521
20526
  uninstalls a component after confirmation; 'daemon' stops it (interrupting
20522
20527
  its sessions), unregisters the supervisor, and keeps ~/.puddle's data \u2014
@@ -20703,7 +20708,7 @@ function parseArgs(argv) {
20703
20708
  throw new CliError(
20704
20709
  "bad_arguments",
20705
20710
  "install takes one of: daemon | desktop (with an optional @version)",
20706
- spec?.what === "cli" ? "the CLI installs itself via npm: npm install -g @puddle-code/cli" : "e.g. puddle install daemon@v0.0.32 user@host"
20711
+ spec?.what === "cli" ? "install the CLI with the public install.sh or npm; update it with puddle upgrade cli" : "e.g. puddle install daemon@v0.0.32 user@host"
20707
20712
  );
20708
20713
  }
20709
20714
  return {
@@ -20780,6 +20785,182 @@ function argvFor(command) {
20780
20785
  return argv;
20781
20786
  }
20782
20787
 
20788
+ // src/cli/daemon-upgrade-prompt.ts
20789
+ import { randomUUID } from "node:crypto";
20790
+
20791
+ // src/lib/handshake.ts
20792
+ init_types();
20793
+ var CLI_UPGRADE_COMMAND = "puddle upgrade cli";
20794
+ function decideHandshake(cli, daemon) {
20795
+ if (daemon.major === cli.major) return { kind: "proceed" };
20796
+ if (daemon.major < cli.major) return { kind: "upgrade-daemon" };
20797
+ return { kind: "refuse-cli-outdated", command: CLI_UPGRADE_COMMAND };
20798
+ }
20799
+ function daemonUpgradeMessage(request) {
20800
+ const { host, daemon, clientProtocol, liveSessions } = request;
20801
+ return `${host} runs puddled ${daemon.version} (protocol ${daemon.protocol.major}.${daemon.protocol.minor}). This client uses protocol ${clientProtocol.major}.${clientProtocol.minor} and needs the same protocol major.
20802
+
20803
+ Updating the host daemon will interrupt ${liveSessions} live session(s). Their saved state remains and they can resume after the update.`;
20804
+ }
20805
+ function verifyDaemonVersion(daemon) {
20806
+ const decision = decideHandshake(PROTOCOL_VERSION, daemon.protocol);
20807
+ if (decision.kind === "proceed") return daemon;
20808
+ if (decision.kind === "refuse-cli-outdated") {
20809
+ throw new CliError(
20810
+ "cli_outdated",
20811
+ `the daemon speaks protocol ${daemon.protocol.major}; this client speaks ${PROTOCOL_VERSION.major}`,
20812
+ `update the desktop app, or update the CLI: ${decision.command}`
20813
+ );
20814
+ }
20815
+ throw new CliError(
20816
+ "protocol_mismatch",
20817
+ `the daemon speaks protocol ${daemon.protocol.major}; this client speaks ${PROTOCOL_VERSION.major}`,
20818
+ "reconnect to update the host daemon"
20819
+ );
20820
+ }
20821
+ async function runHandshake(opts) {
20822
+ const { version: daemon, liveSessions } = opts.inspection;
20823
+ if (decideHandshake(PROTOCOL_VERSION, daemon.protocol).kind !== "upgrade-daemon") {
20824
+ return verifyDaemonVersion(daemon);
20825
+ }
20826
+ if (opts.noUpgrade) {
20827
+ throw new CliError(
20828
+ "upgrade_failed",
20829
+ `the daemon speaks protocol ${daemon.protocol.major} (client: ${PROTOCOL_VERSION.major}) and --no-upgrade is set`,
20830
+ `re-run without --no-upgrade to update it (${liveSessions} live session(s) would be interrupted and can be resumed)`
20831
+ );
20832
+ }
20833
+ const request = {
20834
+ host: opts.host,
20835
+ daemon,
20836
+ clientProtocol: PROTOCOL_VERSION,
20837
+ liveSessions
20838
+ };
20839
+ if (!await opts.confirmDaemonUpgrade?.(request)) {
20840
+ throw new CliError(
20841
+ "upgrade_failed",
20842
+ `the daemon update on ${opts.host} was not approved`,
20843
+ "launch again to approve the update, or run puddle upgrade daemon with the host as an optional argument"
20844
+ );
20845
+ }
20846
+ const latest = await opts.reinspect?.();
20847
+ if (latest && decideHandshake(PROTOCOL_VERSION, latest.version.protocol).kind !== "upgrade-daemon") {
20848
+ return verifyDaemonVersion(latest.version);
20849
+ }
20850
+ const logger = opts.logger ?? silentLogger;
20851
+ logger.info(
20852
+ `updating puddled on ${opts.host} (protocol ${daemon.protocol.major} \u2192 ${PROTOCOL_VERSION.major}) \u2014 ${liveSessions} live session(s) will be interrupted and can be resumed`
20853
+ );
20854
+ try {
20855
+ return verifyDaemonVersion(await opts.upgradeDaemon());
20856
+ } catch (err) {
20857
+ if (err instanceof CliError && ["cli_outdated", "protocol_mismatch"].includes(err.code)) {
20858
+ throw new CliError(
20859
+ "upgrade_failed",
20860
+ `the daemon protocol is still incompatible after updating: ${err.message}`,
20861
+ "inspect the daemon logs: puddle logs"
20862
+ );
20863
+ }
20864
+ throw err;
20865
+ }
20866
+ }
20867
+
20868
+ // src/cli/prompt.ts
20869
+ init_types();
20870
+ import { createInterface as createInterface2 } from "node:readline";
20871
+ async function ask(question, def) {
20872
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return def;
20873
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
20874
+ try {
20875
+ const answer = await new Promise(
20876
+ (resolve6) => rl.question(`${question} [${def}] `, resolve6)
20877
+ );
20878
+ return answer.trim() === "" ? def : answer.trim();
20879
+ } finally {
20880
+ rl.close();
20881
+ }
20882
+ }
20883
+ async function confirm(question, opts = {}) {
20884
+ if (opts.skip === true) return true;
20885
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
20886
+ throw new CliError(
20887
+ "bad_arguments",
20888
+ "refusing to confirm without a terminal",
20889
+ "pass --yes to proceed non-interactively"
20890
+ );
20891
+ }
20892
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
20893
+ try {
20894
+ const answer = await new Promise(
20895
+ (resolve6) => rl.question(`${question} [y/N] `, resolve6)
20896
+ );
20897
+ return /^y(es)?$/i.test(answer.trim());
20898
+ } finally {
20899
+ rl.close();
20900
+ }
20901
+ }
20902
+
20903
+ // src/cli/daemon-upgrade-prompt.ts
20904
+ var REQUEST = "puddle:confirm-daemon-upgrade";
20905
+ var RESPONSE = "puddle:daemon-upgrade-answer";
20906
+ async function askToUpgrade(message) {
20907
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
20908
+ return confirm(`${message}
20909
+
20910
+ Update and connect?`);
20911
+ }
20912
+ var confirmDaemonUpgrade = async (request) => {
20913
+ const message = daemonUpgradeMessage(request);
20914
+ if (!process.send || !process.connected) return askToUpgrade(message);
20915
+ return requestDaemonUpgradeApproval(message, process);
20916
+ };
20917
+ function requestDaemonUpgradeApproval(message, channel) {
20918
+ if (!channel.send || !channel.connected) return Promise.resolve(false);
20919
+ const id = randomUUID();
20920
+ return new Promise((resolve6) => {
20921
+ const finish = (approved) => {
20922
+ channel.off("message", answered);
20923
+ channel.off("disconnect", disconnected);
20924
+ resolve6(approved);
20925
+ };
20926
+ const answered = (value) => {
20927
+ if (!value || typeof value !== "object") return;
20928
+ const response = value;
20929
+ if (response.t === RESPONSE && response.id === id && typeof response.approved === "boolean")
20930
+ finish(response.approved);
20931
+ };
20932
+ const disconnected = () => finish(false);
20933
+ channel.on("message", answered);
20934
+ channel.once("disconnect", disconnected);
20935
+ channel.send({ t: REQUEST, id, message }, (err) => {
20936
+ if (err) finish(false);
20937
+ });
20938
+ });
20939
+ }
20940
+ function relayDaemonUpgradePrompt(child, ask2 = askToUpgrade) {
20941
+ let active = true;
20942
+ let pending = false;
20943
+ const receive = (value) => {
20944
+ if (pending || !value || typeof value !== "object") return;
20945
+ const request = value;
20946
+ if (request.t !== REQUEST || typeof request.id !== "string" || typeof request.message !== "string")
20947
+ return;
20948
+ pending = true;
20949
+ void ask2(request.message).catch(() => false).then((approved) => {
20950
+ pending = false;
20951
+ if (active && child.connected)
20952
+ child.send({ t: RESPONSE, id: request.id, approved }, () => {
20953
+ });
20954
+ });
20955
+ };
20956
+ child.on("message", receive);
20957
+ return () => {
20958
+ active = false;
20959
+ child.off("message", receive);
20960
+ if (child.connected) child.disconnect();
20961
+ };
20962
+ }
20963
+
20783
20964
  // ../shared/src/node/security.ts
20784
20965
  import { createHash, randomBytes } from "node:crypto";
20785
20966
  import {
@@ -20867,14 +21048,14 @@ async function listenPrivate(path, onConnection) {
20867
21048
  privatePath(path);
20868
21049
  const before = lstatSync(path);
20869
21050
  if (!before.isSocket()) throw new Error("Puddle control path is not a socket");
20870
- const stale = await new Promise((resolve5, reject) => {
21051
+ const stale = await new Promise((resolve6, reject) => {
20871
21052
  const probe = connect(path);
20872
21053
  probe.once("connect", () => {
20873
21054
  probe.destroy();
20874
- resolve5(false);
21055
+ resolve6(false);
20875
21056
  });
20876
21057
  probe.once("error", (err) => {
20877
- if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve5(true);
21058
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve6(true);
20878
21059
  else reject(err);
20879
21060
  });
20880
21061
  });
@@ -20887,11 +21068,11 @@ async function listenPrivate(path, onConnection) {
20887
21068
  }
20888
21069
  }
20889
21070
  const server = createServer(onConnection);
20890
- await new Promise((resolve5, reject) => {
21071
+ await new Promise((resolve6, reject) => {
20891
21072
  server.once("error", reject);
20892
21073
  server.listen(path, () => {
20893
21074
  server.off("error", reject);
20894
- resolve5();
21075
+ resolve6();
20895
21076
  });
20896
21077
  });
20897
21078
  if (process.platform !== "win32") chmodSync(path, 384);
@@ -21005,7 +21186,7 @@ var HostControlClient = class {
21005
21186
  }
21006
21187
  this.channel = channel;
21007
21188
  this.until = performance.now() + CONNECTION_POLICY.leaseMs;
21008
- await new Promise((resolve5, reject) => {
21189
+ await new Promise((resolve6, reject) => {
21009
21190
  let pendingAt = null;
21010
21191
  let initial = true;
21011
21192
  const send = (message, sentAt = performance.now()) => {
@@ -21036,8 +21217,8 @@ var HostControlClient = class {
21036
21217
  this.status = "protocol_mismatch";
21037
21218
  reject(
21038
21219
  this.error(
21039
- "cli_outdated",
21040
- `Host protocol ${message.version.protocol.major} requires a compatible CLI`
21220
+ "protocol_mismatch",
21221
+ `Host protocol ${message.version.protocol.major} does not match client protocol ${PROTOCOL_VERSION.major}`
21041
21222
  )
21042
21223
  );
21043
21224
  channel.destroy();
@@ -21058,7 +21239,7 @@ var HostControlClient = class {
21058
21239
  if (this.channel !== channel) return;
21059
21240
  this.status = "ready";
21060
21241
  this.emit();
21061
- resolve5();
21242
+ resolve6();
21062
21243
  }).catch(() => channel.destroy());
21063
21244
  }
21064
21245
  },
@@ -21155,7 +21336,7 @@ async function inspectLegacyHost(home) {
21155
21336
  };
21156
21337
  try {
21157
21338
  const version2 = versionResponseSchema.parse(await get("/api/version"));
21158
- const sessions = sessionSchema.array().parse(await get("/api/sessions"));
21339
+ const sessions = sessionSchema.pick({ status: true }).array().parse(await get("/api/sessions"));
21159
21340
  return hostInspectionSchema.parse({
21160
21341
  t: "inspection",
21161
21342
  port,
@@ -21179,7 +21360,7 @@ async function inspectHostLocally(home) {
21179
21360
  }
21180
21361
  const socket = connect2(path);
21181
21362
  try {
21182
- const grant = await new Promise((resolve5, reject) => {
21363
+ const grant = await new Promise((resolve6, reject) => {
21183
21364
  socket.setTimeout(5e3, () => socket.destroy(new Error("Host inspection timed out")));
21184
21365
  socket.once("error", reject);
21185
21366
  socket.once("close", () => reject(new Error("Host control closed")));
@@ -21189,7 +21370,7 @@ async function inspectHostLocally(home) {
21189
21370
  const message = controlResponseSchema.parse(value);
21190
21371
  if (message.t !== "authority" || !message.token)
21191
21372
  throw new Error("Host authority unavailable");
21192
- resolve5(message);
21373
+ resolve6(message);
21193
21374
  },
21194
21375
  () => socket.destroy(new Error("Invalid host control response"))
21195
21376
  );
@@ -21200,7 +21381,7 @@ async function inspectHostLocally(home) {
21200
21381
  signal: AbortSignal.timeout(3e3)
21201
21382
  });
21202
21383
  if (!response.ok) throw new Error("Host inspection rejected");
21203
- const sessions = sessionSchema.array().parse(await response.json());
21384
+ const sessions = sessionSchema.pick({ status: true }).array().parse(await response.json());
21204
21385
  return hostInspectionSchema.parse({
21205
21386
  t: "inspection",
21206
21387
  port: grant.port,
@@ -21302,9 +21483,9 @@ async function acquireAuthority(transport) {
21302
21483
 
21303
21484
  // src/cli/run.ts
21304
21485
  init_attach();
21305
- import { existsSync as existsSync6 } from "node:fs";
21306
- import { dirname as dirname10, join as join16 } from "node:path";
21307
- import { fileURLToPath as fileURLToPath4 } from "node:url";
21486
+ import { existsSync as existsSync7 } from "node:fs";
21487
+ import { dirname as dirname12, join as join17 } from "node:path";
21488
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
21308
21489
 
21309
21490
  // src/lib/browser.ts
21310
21491
  import { spawn as spawn3 } from "node:child_process";
@@ -21347,7 +21528,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
21347
21528
  import { dirname as dirname4, join as join5 } from "node:path";
21348
21529
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21349
21530
  function cliVersion() {
21350
- if (true) return "0.2.4";
21531
+ if (true) return "0.2.7";
21351
21532
  const here = dirname4(fileURLToPath2(import.meta.url));
21352
21533
  for (const candidate of [
21353
21534
  join5(here, "..", "..", "package.json"),
@@ -21590,7 +21771,7 @@ function swapScript(staged2, opts) {
21590
21771
  }
21591
21772
  async function findInstalledDesktopApp(opts = {}) {
21592
21773
  if ((opts.platform ?? process.platform) !== "darwin") return null;
21593
- const { readFile: readFile2 } = await import("node:fs/promises");
21774
+ const { readFile: readFile3 } = await import("node:fs/promises");
21594
21775
  const { homedir: homedir3 } = await import("node:os");
21595
21776
  const systemApplications = opts.systemApplicationsDir ?? "/Applications";
21596
21777
  const home = opts.homeDir ?? homedir3();
@@ -21599,7 +21780,7 @@ async function findInstalledDesktopApp(opts = {}) {
21599
21780
  join6(home, "Applications", "Puddle.app")
21600
21781
  ]) {
21601
21782
  try {
21602
- const plist = await readFile2(join6(appPath, "Contents/Info.plist"), "utf8");
21783
+ const plist = await readFile3(join6(appPath, "Contents/Info.plist"), "utf8");
21603
21784
  const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(
21604
21785
  plist
21605
21786
  );
@@ -21627,12 +21808,12 @@ async function desktopAppInstallPath(opts = {}) {
21627
21808
  return join6(userApplications, "Puddle.app");
21628
21809
  }
21629
21810
  function isDesktopAppRunning(appPath) {
21630
- return new Promise((resolve5) => {
21811
+ return new Promise((resolve6) => {
21631
21812
  const child = spawn4("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
21632
21813
  stdio: "ignore"
21633
21814
  });
21634
- child.on("error", () => resolve5(false));
21635
- child.on("exit", (code) => resolve5(code === 0));
21815
+ child.on("error", () => resolve6(false));
21816
+ child.on("exit", (code) => resolve6(code === 0));
21636
21817
  });
21637
21818
  }
21638
21819
  function fileSha256(path) {
@@ -21640,12 +21821,12 @@ function fileSha256(path) {
21640
21821
  return pipeline(createReadStream(path), hash2).then(() => hash2.digest("hex"));
21641
21822
  }
21642
21823
  function run(command, args) {
21643
- return new Promise((resolve5, reject) => {
21824
+ return new Promise((resolve6, reject) => {
21644
21825
  const child = spawn4(command, args, { stdio: "ignore" });
21645
21826
  child.on("error", reject);
21646
21827
  child.on(
21647
21828
  "exit",
21648
- (code) => code === 0 ? resolve5() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
21829
+ (code) => code === 0 ? resolve6() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
21649
21830
  );
21650
21831
  });
21651
21832
  }
@@ -21724,7 +21905,10 @@ var RELEASE_PROTOCOLS = {
21724
21905
  "0.2.1": { major: 19, minor: 0 },
21725
21906
  "0.2.2": { major: 20, minor: 0 },
21726
21907
  "0.2.3": { major: 20, minor: 0 },
21727
- "0.2.4": { major: 21, minor: 2 }
21908
+ "0.2.4": { major: 21, minor: 2 },
21909
+ "0.2.5": { major: 21, minor: 2 },
21910
+ "0.2.6": { major: 21, minor: 2 },
21911
+ "0.2.7": { major: 21, minor: 3 }
21728
21912
  };
21729
21913
  function validProtocol(value) {
21730
21914
  if (typeof value !== "object" || value === null) return false;
@@ -21837,7 +22021,7 @@ function formatComponentVersions(components) {
21837
22021
  import { join as join12 } from "node:path";
21838
22022
 
21839
22023
  // src/lib/attached-daemon.ts
21840
- import { randomUUID } from "node:crypto";
22024
+ import { randomUUID as randomUUID2 } from "node:crypto";
21841
22025
 
21842
22026
  // src/lib/bootstrap.ts
21843
22027
  import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
@@ -21927,22 +22111,22 @@ function readInstallScript() {
21927
22111
  // src/lib/net.ts
21928
22112
  import { connect as connect4, createServer as createServer2 } from "node:net";
21929
22113
  function findFreePort() {
21930
- return new Promise((resolve5, reject) => {
22114
+ return new Promise((resolve6, reject) => {
21931
22115
  const srv = createServer2();
21932
22116
  srv.once("error", reject);
21933
22117
  srv.listen(0, "127.0.0.1", () => {
21934
22118
  const address = srv.address();
21935
22119
  const port = typeof address === "object" && address !== null ? address.port : 0;
21936
- srv.close(() => resolve5(port));
22120
+ srv.close(() => resolve6(port));
21937
22121
  });
21938
22122
  });
21939
22123
  }
21940
22124
  function tcpListening(port, timeoutMs = 500) {
21941
- return new Promise((resolve5) => {
22125
+ return new Promise((resolve6) => {
21942
22126
  const sock = connect4({ host: "127.0.0.1", port });
21943
22127
  const done = (ok) => {
21944
22128
  sock.destroy();
21945
- resolve5(ok);
22129
+ resolve6(ok);
21946
22130
  };
21947
22131
  sock.setTimeout(timeoutMs, () => done(false));
21948
22132
  sock.once("connect", () => done(true));
@@ -21978,35 +22162,50 @@ init_types();
21978
22162
  async function ensureDaemon(transport, opts) {
21979
22163
  const logger = opts.logger ?? silentLogger;
21980
22164
  const startTimeoutMs = opts.startTimeoutMs ?? (opts.attachedFallback === void 0 ? 2e4 : 5e3);
22165
+ let mismatch;
21981
22166
  try {
21982
22167
  const authority = await acquireAuthority(transport);
21983
22168
  return { port: authority.port, authority, bootstrapped: false, daemonLifetime: "persistent" };
21984
22169
  } catch (err) {
21985
- if (err instanceof CliError && err.code === "cli_outdated") throw err;
22170
+ if (err instanceof CliError && err.code === "protocol_mismatch") mismatch = err;
21986
22171
  }
21987
22172
  const inspection = await inspectHost(transport).catch((err) => {
21988
- if (err instanceof CliError) throw err;
22173
+ if (err instanceof CliError || mismatch) throw err;
21989
22174
  return null;
21990
22175
  });
22176
+ if (!inspection && mismatch) throw mismatch;
22177
+ const install = async () => {
22178
+ await installDaemon(transport, opts);
22179
+ return waitAfterInstall(transport, opts, logger, true, startTimeoutMs);
22180
+ };
21991
22181
  if (inspection) {
21992
- if (inspection.version.protocol.major > PROTOCOL_VERSION.major) {
21993
- throw new CliError("cli_outdated", "the host uses a newer protocol; update the CLI");
21994
- }
21995
- if (opts.noUpgrade) {
21996
- throw new CliError(
21997
- "upgrade_failed",
21998
- `the daemon speaks protocol ${inspection.version.protocol.major} and --no-upgrade is set`,
21999
- "run puddle launch without --no-upgrade"
22000
- );
22182
+ let replacement;
22183
+ try {
22184
+ await runHandshake({
22185
+ host: transport.label,
22186
+ inspection,
22187
+ noUpgrade: opts.noUpgrade,
22188
+ confirmDaemonUpgrade: opts.confirmDaemonUpgrade,
22189
+ reinspect: () => inspectHost(transport),
22190
+ logger,
22191
+ upgradeDaemon: async () => {
22192
+ replacement = await install();
22193
+ return replacement.authority.version;
22194
+ }
22195
+ });
22196
+ if (replacement) return replacement;
22197
+ const authority = await acquireAuthority(transport);
22198
+ return { port: authority.port, authority, bootstrapped: false, daemonLifetime: "persistent" };
22199
+ } catch (err) {
22200
+ replacement?.authority.close();
22201
+ await replacement?.lease?.stop();
22202
+ throw err;
22001
22203
  }
22002
- logger.info(
22003
- `updating puddled \u2014 ${inspection.liveSessions} live session(s) will be interrupted and can be resumed`
22004
- );
22005
- } else if (await installedVersion(transport) !== null) {
22204
+ }
22205
+ if (await installedVersion(transport) !== null) {
22006
22206
  logger.info(`restarting puddled on ${transport.label}`);
22007
22207
  }
22008
- await installDaemon(transport, opts);
22009
- return waitAfterInstall(transport, opts, logger, true, startTimeoutMs);
22208
+ return install();
22010
22209
  }
22011
22210
  async function waitAfterInstall(transport, opts, logger, bootstrapped, timeoutMs) {
22012
22211
  try {
@@ -22042,7 +22241,7 @@ async function waitForStartedDaemon(transport, timeoutMs, signal) {
22042
22241
  }
22043
22242
  return { port: authority.port, authority };
22044
22243
  } catch (err) {
22045
- if (err instanceof CliError && err.code === "cli_outdated") throw err;
22244
+ if (err instanceof CliError && err.code === "protocol_mismatch") throw err;
22046
22245
  }
22047
22246
  await sleep(500);
22048
22247
  }
@@ -22055,23 +22254,6 @@ function startTimeout(transport) {
22055
22254
  transport.kind === "ssh" ? `inspect it with: puddle logs ${transport.label}` : "inspect it with: puddle logs"
22056
22255
  );
22057
22256
  }
22058
- function makeUpgrader(transport, client, opts, lease) {
22059
- return async () => {
22060
- await installDaemon(transport, opts);
22061
- await lease?.ensureRunning();
22062
- const fresh = await waitForStartedDaemon(transport, 3e4);
22063
- client.setAuthority(fresh.authority);
22064
- const deadline = Date.now() + 3e4;
22065
- while (Date.now() < deadline) {
22066
- if (await client.responds()) return;
22067
- await sleep(500);
22068
- }
22069
- throw new CliError(
22070
- "daemon_start_timeout",
22071
- `puddled did not come back after updating on ${transport.label}`
22072
- );
22073
- };
22074
- }
22075
22257
 
22076
22258
  // src/lib/attached-daemon.ts
22077
22259
  init_types();
@@ -22134,7 +22316,7 @@ var AttachedDaemon = class {
22134
22316
  }
22135
22317
  async launchAndWait(preferredPort) {
22136
22318
  const portFlag = preferredPort === void 0 ? "" : ` --port ${preferredPort}`;
22137
- const leasePidPath = `${hostPaths.home}/attached-${randomUUID()}.pid`;
22319
+ const leasePidPath = `${hostPaths.home}/attached-${randomUUID2()}.pid`;
22138
22320
  this.leasePidPath = leasePidPath;
22139
22321
  const command = this.ssh.runAttached(
22140
22322
  `printf '%s\\n' "$$" > ${leasePidPath}; exec ${hostPaths.current}/puddled${portFlag} >> ${hostPaths.logs}/puddled.out.log 2>&1`
@@ -22230,10 +22412,6 @@ var DaemonClient = class {
22230
22412
  this.port = port;
22231
22413
  this.authority = authority;
22232
22414
  }
22233
- setAuthority(authority) {
22234
- this.authority.close();
22235
- this.authority = authority;
22236
- }
22237
22415
  setPort(port) {
22238
22416
  this.port = port;
22239
22417
  }
@@ -22275,21 +22453,6 @@ var DaemonClient = class {
22275
22453
  async liveSessions() {
22276
22454
  return (await this.sessions()).filter((s) => LIVE.has(s.status));
22277
22455
  }
22278
- async liveSessionCount() {
22279
- return (await this.sessions()).filter((s) => LIVE.has(s.status)).length;
22280
- }
22281
- /** Readiness requires an authenticated response; rejection never counts as ready. */
22282
- async responds() {
22283
- try {
22284
- const res = await fetch(`http://127.0.0.1:${this.port}/api/version`, {
22285
- headers: { authorization: `Bearer ${this.authority.credential()}` },
22286
- signal: AbortSignal.timeout(2e3)
22287
- });
22288
- return res.ok;
22289
- } catch {
22290
- return false;
22291
- }
22292
- }
22293
22456
  };
22294
22457
  async function readDaemonPort(transport) {
22295
22458
  const runtime = await transport.readFile(hostPaths.runtime);
@@ -22311,51 +22474,8 @@ async function readDaemonPort(transport) {
22311
22474
  return 7434;
22312
22475
  }
22313
22476
 
22314
- // src/lib/handshake.ts
22315
- init_types();
22316
- var CLI_UPGRADE_COMMAND = "npm install -g @puddle-code/cli";
22317
- function decideHandshake(cli, daemon) {
22318
- if (daemon.major === cli.major) return { kind: "proceed" };
22319
- if (daemon.major < cli.major) return { kind: "upgrade-daemon" };
22320
- return { kind: "refuse-cli-outdated", command: CLI_UPGRADE_COMMAND };
22321
- }
22322
- async function runHandshake(opts) {
22323
- const logger = opts.logger ?? silentLogger;
22324
- const daemon = await opts.client.version();
22325
- const decision = decideHandshake(PROTOCOL_VERSION, daemon.protocol);
22326
- if (decision.kind === "proceed") return daemon;
22327
- if (decision.kind === "refuse-cli-outdated") {
22328
- throw new CliError(
22329
- "cli_outdated",
22330
- `the daemon speaks protocol ${daemon.protocol.major}; this CLI speaks ${PROTOCOL_VERSION.major}`,
22331
- `update the CLI: ${decision.command}`
22332
- );
22333
- }
22334
- const liveSessions = await opts.client.liveSessionCount().catch(() => 0);
22335
- if (opts.noUpgrade) {
22336
- throw new CliError(
22337
- "upgrade_failed",
22338
- `the daemon speaks protocol ${daemon.protocol.major} (CLI: ${PROTOCOL_VERSION.major}) and --no-upgrade is set`,
22339
- `re-run without --no-upgrade to update it (${liveSessions} live session(s) would be interrupted and can be resumed)`
22340
- );
22341
- }
22342
- logger.info(
22343
- `updating puddled (protocol ${daemon.protocol.major} \u2192 ${PROTOCOL_VERSION.major}) \u2014 ${liveSessions} live session(s) will be interrupted and can be resumed`
22344
- );
22345
- await opts.upgradeDaemon({ from: daemon.version, liveSessions });
22346
- const after = await opts.client.version();
22347
- if (after.protocol.major !== PROTOCOL_VERSION.major) {
22348
- throw new CliError(
22349
- "upgrade_failed",
22350
- `the daemon still speaks protocol ${after.protocol.major} after updating`,
22351
- "inspect the daemon logs: puddle logs"
22352
- );
22353
- }
22354
- return after;
22355
- }
22356
-
22357
22477
  // src/lib/serve/ui-server.ts
22358
- import { randomUUID as randomUUID2 } from "node:crypto";
22478
+ import { randomUUID as randomUUID3 } from "node:crypto";
22359
22479
  import { createServer as createServer3 } from "node:http";
22360
22480
 
22361
22481
  // src/lib/auth/launcher.ts
@@ -22391,7 +22511,7 @@ async function startLauncher(home, identity, invite) {
22391
22511
  path,
22392
22512
  close: async () => {
22393
22513
  for (const socket of sockets) socket.destroy();
22394
- await new Promise((resolve5) => server.close(() => resolve5()));
22514
+ await new Promise((resolve6) => server.close(() => resolve6()));
22395
22515
  }
22396
22516
  };
22397
22517
  }
@@ -22401,14 +22521,14 @@ async function requestInvitation(home, path) {
22401
22521
  const stored = readPrivateJson(secretFile(home, path));
22402
22522
  pipeSecret = stored.secret;
22403
22523
  } else privatePath(path);
22404
- return new Promise((resolve5, reject) => {
22524
+ return new Promise((resolve6, reject) => {
22405
22525
  const socket = connect5(path);
22406
22526
  socket.setTimeout(5e3, () => socket.destroy(new Error("Launcher did not answer")));
22407
22527
  jsonLines(
22408
22528
  socket,
22409
22529
  (value) => {
22410
22530
  const message = launcherResponseSchema.parse(value);
22411
- resolve5(message.url);
22531
+ resolve6(message.url);
22412
22532
  socket.end();
22413
22533
  },
22414
22534
  () => socket.destroy(new Error("Invalid launcher reply"))
@@ -22628,7 +22748,7 @@ function writeStore(file2, store) {
22628
22748
  renameSync3(tmp, file2);
22629
22749
  }
22630
22750
  function readBody(req) {
22631
- return new Promise((resolve5, reject) => {
22751
+ return new Promise((resolve6, reject) => {
22632
22752
  let size = 0;
22633
22753
  const chunks = [];
22634
22754
  req.on("data", (chunk) => {
@@ -22640,7 +22760,7 @@ function readBody(req) {
22640
22760
  }
22641
22761
  chunks.push(chunk);
22642
22762
  });
22643
- req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
22763
+ req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
22644
22764
  req.on("error", reject);
22645
22765
  });
22646
22766
  }
@@ -23227,7 +23347,7 @@ async function startUiServer(opts) {
23227
23347
  const tracker = new ProxySocketTracker();
23228
23348
  const serveStatic = createStaticHandler(opts.assetsDir);
23229
23349
  const handleRemoteAccess = remoteAccessHandler(opts.remoteAccess);
23230
- const nonce = randomUUID2();
23350
+ const nonce = randomUUID3();
23231
23351
  let origin = "";
23232
23352
  let proxyOrigin = "";
23233
23353
  let browsers;
@@ -23365,8 +23485,8 @@ async function startUiServer(opts) {
23365
23485
  browsers.close();
23366
23486
  bridge.close();
23367
23487
  tracker.destroyAll();
23368
- await new Promise((resolve5) => {
23369
- server.close(() => resolve5());
23488
+ await new Promise((resolve6) => {
23489
+ server.close(() => resolve6());
23370
23490
  server.closeAllConnections();
23371
23491
  });
23372
23492
  }
@@ -23376,15 +23496,15 @@ async function listen(server, startPort, strict, avoidPort) {
23376
23496
  for (let probe = 0; probe < MAX_PORT_PROBES; probe += 1) {
23377
23497
  const port = startPort + probe;
23378
23498
  if (!strict && port === avoidPort) continue;
23379
- const ok = await new Promise((resolve5, rejectListen) => {
23499
+ const ok = await new Promise((resolve6, rejectListen) => {
23380
23500
  const onError = (err) => {
23381
23501
  server.off("listening", onListening);
23382
- if (err.code === "EADDRINUSE") resolve5(false);
23502
+ if (err.code === "EADDRINUSE") resolve6(false);
23383
23503
  else rejectListen(err);
23384
23504
  };
23385
23505
  const onListening = () => {
23386
23506
  server.off("error", onError);
23387
- resolve5(true);
23507
+ resolve6(true);
23388
23508
  };
23389
23509
  server.once("error", onError);
23390
23510
  server.once("listening", onListening);
@@ -23665,7 +23785,12 @@ async function connectRemote(opts) {
23665
23785
  let uiResource;
23666
23786
  let endpointResource;
23667
23787
  try {
23668
- const bootstrap = { tarball: opts.tarball, logger, noUpgrade: opts.noUpgrade };
23788
+ const bootstrap = {
23789
+ tarball: opts.tarball,
23790
+ logger,
23791
+ noUpgrade: opts.noUpgrade,
23792
+ confirmDaemonUpgrade: opts.confirmDaemonUpgrade
23793
+ };
23669
23794
  const attachedDaemon = new AttachedDaemon(ssh, logger);
23670
23795
  const endpoint = await ensureDaemon(ssh, {
23671
23796
  ...bootstrap,
@@ -23706,12 +23831,7 @@ async function connectRemote(opts) {
23706
23831
  logger
23707
23832
  });
23708
23833
  oauthForwardResource = oauthForward;
23709
- const daemon = await runHandshake({
23710
- client,
23711
- noUpgrade: opts.noUpgrade,
23712
- upgradeDaemon: makeUpgrader(ssh, client, bootstrap, endpoint.lease),
23713
- logger
23714
- });
23834
+ const daemon = verifyDaemonVersion(await client.version());
23715
23835
  const avoidPort = await readDaemonPort(new LocalTransport());
23716
23836
  endpoint.authority.setVerifier(async () => {
23717
23837
  const generation = endpoint.authority.generation;
@@ -23734,7 +23854,7 @@ async function connectRemote(opts) {
23734
23854
  watchTunnel(replacement);
23735
23855
  await previous?.close();
23736
23856
  }
23737
- await client.version();
23857
+ verifyDaemonVersion(await client.version());
23738
23858
  });
23739
23859
  const ui = await startUiServer({
23740
23860
  authority: endpoint.authority,
@@ -23936,24 +24056,26 @@ init_types();
23936
24056
  async function startLocal(opts) {
23937
24057
  const logger = opts.logger ?? silentLogger;
23938
24058
  const transport = new LocalTransport();
23939
- const bootstrap = { tarball: opts.tarball, logger, noUpgrade: opts.noUpgrade };
23940
- const endpoint = await ensureDaemon(transport, bootstrap);
24059
+ const bootstrap = {
24060
+ tarball: opts.tarball,
24061
+ logger,
24062
+ noUpgrade: opts.noUpgrade,
24063
+ confirmDaemonUpgrade: opts.confirmDaemonUpgrade
24064
+ };
24065
+ let endpoint;
23941
24066
  try {
23942
- const client = new DaemonClient(endpoint.port, endpoint.authority);
23943
- const daemon = await runHandshake({
23944
- client,
23945
- noUpgrade: opts.noUpgrade,
23946
- upgradeDaemon: makeUpgrader(transport, client, bootstrap),
23947
- logger
23948
- });
24067
+ endpoint = await ensureDaemon(transport, bootstrap);
24068
+ const authority = endpoint.authority;
24069
+ const client = new DaemonClient(endpoint.port, authority);
24070
+ const daemon = verifyDaemonVersion(await client.version());
23949
24071
  let currentUi = void 0;
23950
- endpoint.authority.setVerifier(async () => {
23951
- client.setPort(endpoint.authority.port);
23952
- await client.version();
23953
- currentUi?.setTarget({ host: "127.0.0.1", port: endpoint.authority.port });
24072
+ authority.setVerifier(async () => {
24073
+ client.setPort(authority.port);
24074
+ verifyDaemonVersion(await client.version());
24075
+ currentUi?.setTarget({ host: "127.0.0.1", port: authority.port });
23954
24076
  });
23955
24077
  const ui = await startUiServer({
23956
- authority: endpoint.authority,
24078
+ authority,
23957
24079
  identity: "local",
23958
24080
  refreshId: opts.refreshId,
23959
24081
  assetsDir: opts.assetsDir,
@@ -23982,12 +24104,12 @@ async function startLocal(opts) {
23982
24104
  },
23983
24105
  async stop() {
23984
24106
  await ui.close();
23985
- endpoint.authority.close();
24107
+ authority.close();
23986
24108
  transport.dispose();
23987
24109
  }
23988
24110
  };
23989
24111
  } catch (err) {
23990
- endpoint.authority.close();
24112
+ endpoint?.authority.close();
23991
24113
  transport.dispose();
23992
24114
  throw err;
23993
24115
  }
@@ -24017,12 +24139,170 @@ async function statusReport(client) {
24017
24139
  // src/cli/run.ts
24018
24140
  init_types();
24019
24141
 
24020
- // src/cli/manage.ts
24142
+ // src/cli/manage-cli.ts
24143
+ init_types();
24021
24144
  import { spawn as spawn7 } from "node:child_process";
24022
- import { existsSync as existsSync5 } from "node:fs";
24023
- import { chmod as chmod2, copyFile, mkdir as mkdir2, rename, rm as rm2 } from "node:fs/promises";
24145
+ import { dirname as dirname10 } from "node:path";
24146
+
24147
+ // src/lib/standalone-cli.ts
24148
+ init_types();
24149
+ import { spawn as spawn6 } from "node:child_process";
24150
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readFileSync as readFileSync9, realpathSync as realpathSync2 } from "node:fs";
24151
+ import { mkdir as mkdir2, readFile as readFile2, rm as rm2, unlink } from "node:fs/promises";
24152
+ import { dirname as dirname9, isAbsolute, join as join15, resolve as resolve5 } from "node:path";
24153
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
24154
+ function standaloneInstallation(entryFile = fileURLToPath4(import.meta.url), execPath = process.execPath) {
24155
+ const release = resolve5(dirname9(entryFile), "..");
24156
+ if (!existsSync5(join15(release, "STANDALONE"))) return null;
24157
+ try {
24158
+ const root = resolve5(release, "..", "..");
24159
+ const record2 = JSON.parse(readFileSync9(join15(root, "installation.json"), "utf8"));
24160
+ if (readFileSync9(join15(release, "STANDALONE"), "utf8").trim() !== "puddle-cli" || readFileSync9(join15(root, "INSTALLATION"), "utf8").trim() !== "puddle-cli" || dirname9(release) !== join15(root, "versions") || realpathSync2(release) !== release || realpathSync2(execPath) !== join15(release, "bin/node") || record2.kind !== "puddle-cli" || typeof record2.launcher !== "string" || !isAbsolute(record2.launcher))
24161
+ throw new Error("Installation metadata does not match this CLI");
24162
+ return { root, release, launcher: record2.launcher };
24163
+ } catch {
24164
+ throw new CliError(
24165
+ "not_installed",
24166
+ "this standalone archive is not an installer-managed CLI",
24167
+ "run the public install.sh to install the CLI before using upgrade or remove"
24168
+ );
24169
+ }
24170
+ }
24171
+ function upgradeStandaloneCli(installation, version2, repo) {
24172
+ const script = readFileSync9(join15(installation.release, "cli/install-cli.sh"), "utf8");
24173
+ const args = [
24174
+ "-s",
24175
+ "--",
24176
+ "--prefix",
24177
+ installation.root,
24178
+ "--bin-dir",
24179
+ dirname9(installation.launcher)
24180
+ ];
24181
+ if (version2 !== void 0) args.push("--version", version2);
24182
+ if (repo !== void 0) args.push("--repo", repo);
24183
+ return new Promise((resolve6, reject) => {
24184
+ const child = spawn6("sh", args, { stdio: ["pipe", "inherit", "inherit"] });
24185
+ child.on("error", reject);
24186
+ child.stdin.on("error", (error51) => {
24187
+ if (error51.code !== "EPIPE") reject(error51);
24188
+ });
24189
+ child.stdin.end(script);
24190
+ child.on("exit", (code) => {
24191
+ if (code === 0) resolve6(0);
24192
+ else reject(new CliError("not_installed", `CLI installer exited with ${code ?? "a signal"}`));
24193
+ });
24194
+ });
24195
+ }
24196
+ function standaloneLauncher(root) {
24197
+ const quoted = `'${join15(root, "current/puddle").replaceAll("'", "'\\''")}'`;
24198
+ return `#!/bin/sh
24199
+ # puddle standalone launcher
24200
+ exec ${quoted} "$@"
24201
+ `;
24202
+ }
24203
+ async function removeStandaloneCli(installation) {
24204
+ const lock = join15(installation.root, ".install-lock");
24205
+ try {
24206
+ await mkdir2(lock);
24207
+ } catch {
24208
+ throw new CliError("already_running", "another CLI installation operation is running");
24209
+ }
24210
+ try {
24211
+ if (lstatSync2(installation.launcher).isSymbolicLink() || await readFile2(installation.launcher, "utf8") !== standaloneLauncher(installation.root))
24212
+ throw new CliError("not_installed", "the CLI launcher was replaced; refusing removal");
24213
+ await unlink(installation.launcher);
24214
+ await rm2(installation.root, { recursive: true, force: true });
24215
+ } finally {
24216
+ await rm2(lock, { recursive: true, force: true });
24217
+ }
24218
+ }
24219
+
24220
+ // src/cli/manage-cli.ts
24221
+ function upgradeCli(version2, logger) {
24222
+ const standalone = standaloneInstallation();
24223
+ if (standalone) {
24224
+ logger.info(`upgrading standalone CLI ${cliVersion()} to ${version2 ?? "the latest release"}`);
24225
+ return upgradeStandaloneCli(standalone, version2, repoSlug());
24226
+ }
24227
+ const spec = `@puddle-code/cli@${version2 ?? "latest"}`;
24228
+ logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for ${version2 ?? "the latest release"}`);
24229
+ return new Promise((resolve6, reject) => {
24230
+ const child = spawn7("npm", ["install", "-g", spec], { stdio: "inherit" });
24231
+ child.on(
24232
+ "error",
24233
+ () => reject(
24234
+ new CliError(
24235
+ "not_installed",
24236
+ "npm is not on PATH",
24237
+ `the CLI is installed and upgraded via npm: npm install -g ${spec}`
24238
+ )
24239
+ )
24240
+ );
24241
+ child.on("exit", (code) => {
24242
+ if (code === 0) {
24243
+ logger.info("done \u2014 `puddle --version` shows the installed version");
24244
+ resolve6(0);
24245
+ } else {
24246
+ reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
24247
+ }
24248
+ });
24249
+ });
24250
+ }
24251
+ async function removeCli(yes, logger) {
24252
+ const standalone = standaloneInstallation();
24253
+ if (standalone) {
24254
+ const proceed2 = await confirm(
24255
+ `Remove the standalone CLI at ${standalone.root} and its launcher in ${dirname10(standalone.launcher)}? Close its cockpits first; daemons and their sessions are untouched.`,
24256
+ { skip: yes }
24257
+ );
24258
+ if (!proceed2) {
24259
+ logger.info("nothing removed");
24260
+ return 0;
24261
+ }
24262
+ await removeStandaloneCli(standalone);
24263
+ logger.info("removed the standalone CLI \u2014 daemon installations and data are untouched");
24264
+ return 0;
24265
+ }
24266
+ const managed = await new Promise((resolve6) => {
24267
+ const child = spawn7("npm", ["ls", "-g", "@puddle-code/cli", "--depth=0"], { stdio: "ignore" });
24268
+ child.on("error", () => resolve6(false));
24269
+ child.on("exit", (code) => resolve6(code === 0));
24270
+ });
24271
+ if (!managed) {
24272
+ throw new CliError(
24273
+ "not_installed",
24274
+ "the puddle CLI is not an npm global install on this machine",
24275
+ "a repo checkout or linked dev build is removed the way it was installed"
24276
+ );
24277
+ }
24278
+ const proceed = await confirm(
24279
+ `Remove the puddle CLI (npm uninstall -g @puddle-code/cli)? Running cockpits keep running until killed; daemons and their sessions are untouched.`,
24280
+ { skip: yes }
24281
+ );
24282
+ if (!proceed) {
24283
+ logger.info("nothing removed");
24284
+ return 0;
24285
+ }
24286
+ return new Promise((resolve6, reject) => {
24287
+ const child = spawn7("npm", ["uninstall", "-g", "@puddle-code/cli"], { stdio: "inherit" });
24288
+ child.on("error", () => reject(new CliError("not_installed", "npm is not on PATH")));
24289
+ child.on("exit", (code) => {
24290
+ if (code === 0) {
24291
+ logger.info("removed \u2014 daemons keep running (puddle remove daemon uninstalls one)");
24292
+ resolve6(0);
24293
+ } else {
24294
+ reject(new CliError("not_installed", `npm uninstall exited with ${code ?? "a signal"}`));
24295
+ }
24296
+ });
24297
+ });
24298
+ }
24299
+
24300
+ // src/cli/manage.ts
24301
+ import { spawn as spawn9 } from "node:child_process";
24302
+ import { existsSync as existsSync6 } from "node:fs";
24303
+ import { chmod as chmod2, copyFile, mkdir as mkdir3, rename, rm as rm3 } from "node:fs/promises";
24024
24304
  import { homedir as homedir2 } from "node:os";
24025
- import { join as join15 } from "node:path";
24305
+ import { join as join16 } from "node:path";
24026
24306
 
24027
24307
  // src/lib/releases.ts
24028
24308
  init_types();
@@ -24155,9 +24435,9 @@ async function upgradeDaemon(transport, opts = {}) {
24155
24435
  }
24156
24436
 
24157
24437
  // src/cli/detach.ts
24158
- import { spawn as spawn6 } from "node:child_process";
24159
- import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync9 } from "node:fs";
24160
- import { dirname as dirname9 } from "node:path";
24438
+ import { spawn as spawn8 } from "node:child_process";
24439
+ import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync10 } from "node:fs";
24440
+ import { dirname as dirname11 } from "node:path";
24161
24441
  init_types();
24162
24442
  var COCKPIT_CHILD_ENV = "PUDDLE_COCKPIT_CHILD";
24163
24443
  var READY_TIMEOUT_MS = 3e5;
@@ -24196,12 +24476,12 @@ async function launchDetached(opts) {
24196
24476
  }
24197
24477
  if (target !== "local") await new SshTransport(target).open();
24198
24478
  const logFile = cockpitLogPath(target);
24199
- mkdirSync6(dirname9(logFile), { recursive: true });
24479
+ mkdirSync6(dirname11(logFile), { recursive: true });
24200
24480
  const fd = openSync2(logFile, "w");
24201
24481
  const argv = opts.argv ?? process.argv.slice(2);
24202
- const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
24482
+ const child = spawn8(process.execPath, [process.argv[1] ?? "", ...argv], {
24203
24483
  detached: true,
24204
- stdio: ["ignore", fd, fd],
24484
+ stdio: ["ignore", fd, fd, "ipc"],
24205
24485
  env: { ...process.env, [COCKPIT_CHILD_ENV]: "1" }
24206
24486
  });
24207
24487
  closeSync2(fd);
@@ -24210,7 +24490,13 @@ async function launchDetached(opts) {
24210
24490
  throw new CliError("cockpit_launch_failed", "could not spawn the background cockpit process");
24211
24491
  }
24212
24492
  logger.info(`starting the cockpit for ${target} in the background\u2026`);
24213
- const record2 = await followStartup(target, child.pid, logFile);
24493
+ const stopPromptRelay = relayDaemonUpgradePrompt(child);
24494
+ let record2;
24495
+ try {
24496
+ record2 = await followStartup(target, child.pid, logFile);
24497
+ } finally {
24498
+ stopPromptRelay();
24499
+ }
24214
24500
  if (record2 === null || record2.status !== "ready") {
24215
24501
  if (record2?.status === "error") {
24216
24502
  logger.warn(record2.message ?? "the cockpit failed to start");
@@ -24233,7 +24519,7 @@ async function followStartup(target, childPid, logFile) {
24233
24519
  let offset = 0;
24234
24520
  const relay = () => {
24235
24521
  try {
24236
- const buf = readFileSync9(logFile);
24522
+ const buf = readFileSync10(logFile);
24237
24523
  if (buf.length > offset) {
24238
24524
  process.stderr.write(buf.subarray(offset));
24239
24525
  offset = buf.length;
@@ -24258,7 +24544,7 @@ async function followStartup(target, childPid, logFile) {
24258
24544
  }
24259
24545
  function spawnDetachedRefresh(target, argv, refreshId) {
24260
24546
  const logFile = cockpitLogPath(target);
24261
- mkdirSync6(dirname9(logFile), { recursive: true });
24547
+ mkdirSync6(dirname11(logFile), { recursive: true });
24262
24548
  const fd = openSync2(logFile, "a");
24263
24549
  const env = {
24264
24550
  ...process.env,
@@ -24266,7 +24552,7 @@ function spawnDetachedRefresh(target, argv, refreshId) {
24266
24552
  PUDDLE_INTERNAL_REFRESH: "1"
24267
24553
  };
24268
24554
  delete env[COCKPIT_CHILD_ENV];
24269
- const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
24555
+ const child = spawn8(process.execPath, [process.argv[1] ?? "", ...argv], {
24270
24556
  detached: true,
24271
24557
  stdio: ["ignore", fd, fd],
24272
24558
  env
@@ -24299,41 +24585,6 @@ async function presentInvitation(record2, noBrowser, logger) {
24299
24585
  else openBrowser(url2);
24300
24586
  }
24301
24587
 
24302
- // src/cli/prompt.ts
24303
- init_types();
24304
- import { createInterface as createInterface2 } from "node:readline";
24305
- async function ask(question, def) {
24306
- if (!process.stdin.isTTY || !process.stdout.isTTY) return def;
24307
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
24308
- try {
24309
- const answer = await new Promise(
24310
- (resolve5) => rl.question(`${question} [${def}] `, resolve5)
24311
- );
24312
- return answer.trim() === "" ? def : answer.trim();
24313
- } finally {
24314
- rl.close();
24315
- }
24316
- }
24317
- async function confirm(question, opts = {}) {
24318
- if (opts.skip === true) return true;
24319
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
24320
- throw new CliError(
24321
- "bad_arguments",
24322
- "refusing to confirm without a terminal",
24323
- "pass --yes to proceed non-interactively"
24324
- );
24325
- }
24326
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
24327
- try {
24328
- const answer = await new Promise(
24329
- (resolve5) => rl.question(`${question} [y/N] `, resolve5)
24330
- );
24331
- return /^y(es)?$/i.test(answer.trim());
24332
- } finally {
24333
- rl.close();
24334
- }
24335
- }
24336
-
24337
24588
  // src/cli/manage.ts
24338
24589
  function assertLocal(what, host, verb) {
24339
24590
  if (host === void 0 || host === "local") return;
@@ -24442,31 +24693,6 @@ async function runUpgrade(cmd, logger) {
24442
24693
  else if (process.platform === "darwin") logger.info("no desktop app installed \u2014 skipping");
24443
24694
  return upgradeCli(cmd.version, logger);
24444
24695
  }
24445
- function upgradeCli(version2, logger) {
24446
- const spec = `@puddle-code/cli@${version2 ?? "latest"}`;
24447
- logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for ${version2 ?? "the latest release"}`);
24448
- return new Promise((resolve5, reject) => {
24449
- const child = spawn7("npm", ["install", "-g", spec], { stdio: "inherit" });
24450
- child.on(
24451
- "error",
24452
- () => reject(
24453
- new CliError(
24454
- "not_installed",
24455
- "npm is not on PATH",
24456
- `the CLI is installed and upgraded via npm: npm install -g ${spec}`
24457
- )
24458
- )
24459
- );
24460
- child.on("exit", (code) => {
24461
- if (code === 0) {
24462
- logger.info("done \u2014 `puddle --version` shows the installed version");
24463
- resolve5(0);
24464
- } else {
24465
- reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
24466
- }
24467
- });
24468
- });
24469
- }
24470
24696
  async function setDesktop(opts, logger) {
24471
24697
  if (process.platform === "linux") {
24472
24698
  if (opts.verb !== "install") {
@@ -24538,22 +24764,22 @@ async function installDesktopAppImage(version2, logger) {
24538
24764
  );
24539
24765
  }
24540
24766
  const dir = expandHome2(await ask("Where should the AppImage be stored?", "~/puddle"));
24541
- const target = join15(dir, "Puddle.AppImage");
24542
- if (existsSync5(target) && version2 === void 0) {
24767
+ const target = join16(dir, "Puddle.AppImage");
24768
+ if (existsSync6(target) && version2 === void 0) {
24543
24769
  logger.info(`${target} already exists \u2014 the app updates itself in-app (its update toast)`);
24544
24770
  return 0;
24545
24771
  }
24546
24772
  logger.info(`installing Puddle ${update.version} to ${target}`);
24547
24773
  const staged2 = await stageDesktopUpdate(update, { logger });
24548
- await mkdir2(dir, { recursive: true });
24549
- await rm2(target, { force: true });
24774
+ await mkdir3(dir, { recursive: true });
24775
+ await rm3(target, { force: true });
24550
24776
  try {
24551
24777
  await rename(staged2.stagedPath, target);
24552
24778
  } catch {
24553
24779
  await copyFile(staged2.stagedPath, target);
24554
24780
  await chmod2(target, 493);
24555
24781
  }
24556
- await rm2(staged2.dir, { recursive: true, force: true });
24782
+ await rm3(staged2.dir, { recursive: true, force: true });
24557
24783
  const protocol = componentProtocolForVersion(update.version);
24558
24784
  recordDesktopInstallation({
24559
24785
  path: target,
@@ -24561,14 +24787,14 @@ async function installDesktopAppImage(version2, logger) {
24561
24787
  ...protocol ? { protocol } : {}
24562
24788
  });
24563
24789
  logger.info(`installed \u2014 run it directly (${target}) or add it to your launcher`);
24564
- const opener = spawn7("xdg-open", [dir], { stdio: "ignore", detached: true });
24790
+ const opener = spawn9("xdg-open", [dir], { stdio: "ignore", detached: true });
24565
24791
  opener.on("error", () => void 0);
24566
24792
  opener.unref();
24567
24793
  return 0;
24568
24794
  }
24569
24795
  function expandHome2(path) {
24570
24796
  if (path === "~") return homedir2();
24571
- return path.startsWith("~/") ? join15(homedir2(), path.slice(2)) : path;
24797
+ return path.startsWith("~/") ? join16(homedir2(), path.slice(2)) : path;
24572
24798
  }
24573
24799
  async function runRemove(cmd, logger) {
24574
24800
  switch (cmd.what) {
@@ -24582,40 +24808,6 @@ async function runRemove(cmd, logger) {
24582
24808
  return removeDaemonCmd(cmd, logger);
24583
24809
  }
24584
24810
  }
24585
- async function removeCli(yes, logger) {
24586
- const managed = await new Promise((resolve5) => {
24587
- const child = spawn7("npm", ["ls", "-g", "@puddle-code/cli", "--depth=0"], { stdio: "ignore" });
24588
- child.on("error", () => resolve5(false));
24589
- child.on("exit", (code) => resolve5(code === 0));
24590
- });
24591
- if (!managed) {
24592
- throw new CliError(
24593
- "not_installed",
24594
- "the puddle CLI is not an npm global install on this machine",
24595
- "a repo checkout or linked dev build is removed the way it was installed"
24596
- );
24597
- }
24598
- const proceed = await confirm(
24599
- `Remove the puddle CLI (npm uninstall -g @puddle-code/cli)? Running cockpits keep running until killed; daemons and their sessions are untouched.`,
24600
- { skip: yes }
24601
- );
24602
- if (!proceed) {
24603
- logger.info("nothing removed");
24604
- return 0;
24605
- }
24606
- return new Promise((resolve5, reject) => {
24607
- const child = spawn7("npm", ["uninstall", "-g", "@puddle-code/cli"], { stdio: "inherit" });
24608
- child.on("error", () => reject(new CliError("not_installed", "npm is not on PATH")));
24609
- child.on("exit", (code) => {
24610
- if (code === 0) {
24611
- logger.info("removed \u2014 daemons keep running (puddle remove daemon uninstalls one)");
24612
- resolve5(0);
24613
- } else {
24614
- reject(new CliError("not_installed", `npm uninstall exited with ${code ?? "a signal"}`));
24615
- }
24616
- });
24617
- });
24618
- }
24619
24811
  async function removeDesktop(yes, logger) {
24620
24812
  if (process.platform !== "darwin") {
24621
24813
  throw new CliError(
@@ -24640,7 +24832,7 @@ async function removeDesktop(yes, logger) {
24640
24832
  logger.info("nothing removed");
24641
24833
  return 0;
24642
24834
  }
24643
- await rm2(installed.appPath, { recursive: true, force: true });
24835
+ await rm3(installed.appPath, { recursive: true, force: true });
24644
24836
  await pruneDesktopUpdateCache([]);
24645
24837
  logger.info(`deleted ${installed.appPath} (recent hosts kept in ~/.puddle/recent-hosts.json)`);
24646
24838
  return 0;
@@ -24762,9 +24954,9 @@ function timestampedLogger() {
24762
24954
 
24763
24955
  // src/cli/run.ts
24764
24956
  function assetsDir() {
24765
- const here = dirname10(fileURLToPath4(import.meta.url));
24766
- for (const candidate of [join16(here, "public"), join16(here, "..", "..", "dist", "public")]) {
24767
- if (existsSync6(join16(candidate, "index.html"))) return candidate;
24957
+ const here = dirname12(fileURLToPath5(import.meta.url));
24958
+ for (const candidate of [join17(here, "public"), join17(here, "..", "..", "dist", "public")]) {
24959
+ if (existsSync7(join17(candidate, "index.html"))) return candidate;
24768
24960
  }
24769
24961
  throw new CliError(
24770
24962
  "not_installed",
@@ -24798,7 +24990,7 @@ async function openTarget(host) {
24798
24990
  };
24799
24991
  }
24800
24992
  function runUntilInterrupted(stop, farewell) {
24801
- return new Promise((resolve5) => {
24993
+ return new Promise((resolve6) => {
24802
24994
  let stopping = false;
24803
24995
  process.on("SIGINT", () => {
24804
24996
  if (stopping) process.exit(130);
@@ -24806,10 +24998,10 @@ function runUntilInterrupted(stop, farewell) {
24806
24998
  process.stderr.write(`
24807
24999
  ${farewell}
24808
25000
  `);
24809
- void stop().then(resolve5);
25001
+ void stop().then(resolve6);
24810
25002
  });
24811
25003
  process.on("SIGTERM", () => {
24812
- void stop().then(resolve5);
25004
+ void stop().then(resolve6);
24813
25005
  });
24814
25006
  });
24815
25007
  }
@@ -25097,6 +25289,7 @@ async function runCockpit(command, target, terminal) {
25097
25289
  const common = {
25098
25290
  ...command,
25099
25291
  assetsDir: assetsDir(),
25292
+ confirmDaemonUpgrade,
25100
25293
  logger,
25101
25294
  onRefreshRequest,
25102
25295
  refreshId: process.env.PUDDLE_REFRESH_ID