@puddle-code/cli 0.2.4 → 0.2.6

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 (129) hide show
  1. package/CHANGELOG.md +2 -13
  2. package/README.md +33 -23
  3. package/dist/host-control.mjs +2 -2
  4. package/dist/index.js +501 -312
  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-CzuHShdO.js} +2 -2
  8. package/dist/public/assets/{Dashboard-DUFOyL1_.js → Dashboard-B5qWtmwb.js} +1 -1
  9. package/dist/public/assets/{EditorTabClose-C3wpMhYL.js → EditorTabClose-uxuNJdgx.js} +1 -1
  10. package/dist/public/assets/{FindWidget-Dt1gfA70.js → FindWidget-lUpiYZJG.js} +1 -1
  11. package/dist/public/assets/{LazyTerminal-B9HzmRBx.js → LazyTerminal-BYbPpGEI.js} +2 -2
  12. package/dist/public/assets/{ModelRefcount-CxzQ3w05.js → ModelRefcount-N0Xh_Y-_.js} +1 -1
  13. package/dist/public/assets/{NewProjectDialog-Cq_1po_h.js → NewProjectDialog-D3bNK3u_.js} +1 -1
  14. package/dist/public/assets/{PaneEditorBody-zS2BI5Fn.js → PaneEditorBody-DrSiMzrp.js} +4 -4
  15. package/dist/public/assets/{PdfViewer-ckjWE_TR.js → PdfViewer-BhqrWfmb.js} +2 -2
  16. package/dist/public/assets/{SettingsDialog-Cb2fdV1D.js → SettingsDialog-CamnF91I.js} +1 -1
  17. package/dist/public/assets/{Terminal-BqbjwcMi.js → Terminal-Ds17sCF_.js} +1 -1
  18. package/dist/public/assets/{Workspace-BVDgHkML.js → Workspace-BmtA0kYQ.js} +4 -4
  19. package/dist/public/assets/{abnfDiagram-VCTEODGH-JZdwYDGF.js → abnfDiagram-VCTEODGH-B4ZtDUXi.js} +1 -1
  20. package/dist/public/assets/architecture-7GRP2DOG-D0iH6Lxl.js +1 -0
  21. package/dist/public/assets/{architectureDiagram-5GKGNRK7-CYoRlLvs.js → architectureDiagram-5GKGNRK7-Cyzuyzwl.js} +1 -1
  22. package/dist/public/assets/{blockDiagram-I7D4REHJ-DjPQ4-5b.js → blockDiagram-I7D4REHJ-BN00JnNl.js} +1 -1
  23. package/dist/public/assets/{buffer-store-GUPJI8Wy.js → buffer-store--4yU4YqM.js} +2 -2
  24. package/dist/public/assets/{button-CKes3Qwq.js → button-DehrlYw7.js} +1 -1
  25. package/dist/public/assets/{c4Diagram-7LVT6UL2-BvwPGpk0.js → c4Diagram-7LVT6UL2-DpUKodBy.js} +1 -1
  26. package/dist/public/assets/channel-DT2izo70.js +1 -0
  27. package/dist/public/assets/{chunk-2E4U76K2-DcsuTdxj.js → chunk-2E4U76K2-BksaqXZU.js} +1 -1
  28. package/dist/public/assets/{chunk-4HAMMTFA-BGEaVxkg.js → chunk-4HAMMTFA-D01k6w0A.js} +1 -1
  29. package/dist/public/assets/{chunk-75Z2AOVW-BOvbyaVc.js → chunk-75Z2AOVW-DJJjslyX.js} +1 -1
  30. package/dist/public/assets/{chunk-CLGD4ZFX-8fG7spZS.js → chunk-CLGD4ZFX-CX1B9i6H.js} +1 -1
  31. package/dist/public/assets/{chunk-DU6HZSFF-C94gDWXH.js → chunk-DU6HZSFF-Bx2ELCn-.js} +1 -1
  32. package/dist/public/assets/{chunk-F27PBJKO-CZD-A2b3.js → chunk-F27PBJKO-CCDhODx8.js} +1 -1
  33. package/dist/public/assets/{chunk-GMAD6QVW-C7KK3kiY.js → chunk-GMAD6QVW-C1evXXZi.js} +1 -1
  34. package/dist/public/assets/{chunk-GVQU2GXP-CDFKc0sW.js → chunk-GVQU2GXP-C1iphgLK.js} +1 -1
  35. package/dist/public/assets/{chunk-IMKFNOWR-BWQiE1Cp.js → chunk-IMKFNOWR-5wMdkmsQ.js} +1 -1
  36. package/dist/public/assets/{chunk-L3NEJ4N5-DqP3WOXV.js → chunk-L3NEJ4N5-B-aXfMpF.js} +1 -1
  37. package/dist/public/assets/{chunk-OSK3NFVY-BlmYDJRN.js → chunk-OSK3NFVY-Bm2MaFOc.js} +1 -1
  38. package/dist/public/assets/{chunk-P2QGCYS3-v7eQ-UQT.js → chunk-P2QGCYS3-H02JaAun.js} +1 -1
  39. package/dist/public/assets/{chunk-POPQ4Y6H-D_gxItFL.js → chunk-POPQ4Y6H-Dmm46Beg.js} +1 -1
  40. package/dist/public/assets/{chunk-PWAF6VOD-Ds90M0Eb.js → chunk-PWAF6VOD-CRt8Ete5.js} +1 -1
  41. package/dist/public/assets/{chunk-SHT3W25Y-B8oepiFw.js → chunk-SHT3W25Y-w4aNK54y.js} +1 -1
  42. package/dist/public/assets/{chunk-SVP7TREG-DjFxplBp.js → chunk-SVP7TREG-Cl3QpMDs.js} +1 -1
  43. package/dist/public/assets/{chunk-TICWLB2K-GEeYOgw4.js → chunk-TICWLB2K-BpBbot9h.js} +1 -1
  44. package/dist/public/assets/{chunk-TLUHSLCS-CEnN9wOB.js → chunk-TLUHSLCS-BOnIB6P6.js} +2 -2
  45. package/dist/public/assets/classDiagram-ZZMXUADV-BSshJUxJ.js +1 -0
  46. package/dist/public/assets/classDiagram-v2-VYDZK3BY-BSshJUxJ.js +1 -0
  47. package/dist/public/assets/{context-menu-CmSSwq3B.js → context-menu-BT88Vgia.js} +1 -1
  48. package/dist/public/assets/{cssMode-BEaREsxj.js → cssMode-gXT_K4po.js} +1 -1
  49. package/dist/public/assets/{cynefin-OW5HDTMX-C_sl2wZ-.js → cynefin-OW5HDTMX-CJakxaH8.js} +1 -1
  50. package/dist/public/assets/{cynefinDiagram-5FMLGOSQ-CPpFUPl3.js → cynefinDiagram-5FMLGOSQ-BJ_fVnC9.js} +1 -1
  51. package/dist/public/assets/{dagre-GXQ25YYZ-BryapOat.js → dagre-GXQ25YYZ-CjflcbuS.js} +1 -1
  52. package/dist/public/assets/{diagram-S7CK7UJ4-dIdCar-k.js → diagram-S7CK7UJ4-g8pKIWrY.js} +1 -1
  53. package/dist/public/assets/{diagram-UQ7AKVKN-EcAWwfjf.js → diagram-UQ7AKVKN-Dm5Pg4rY.js} +1 -1
  54. package/dist/public/assets/{diagram-VSXAHHWV-Dpl4gvkd.js → diagram-VSXAHHWV-DV5CVAXg.js} +1 -1
  55. package/dist/public/assets/{diagram-VX7I27RA-CJtemv3e.js → diagram-VX7I27RA-Be9qCk-h.js} +1 -1
  56. package/dist/public/assets/{diagram-Z3DM3KII-CPSnQEgD.js → diagram-Z3DM3KII-C3-xDrxs.js} +1 -1
  57. package/dist/public/assets/{dialog-Dam8WdmS.js → dialog-CAMTxigV.js} +1 -1
  58. package/dist/public/assets/{disclosure-Ex-68BU4.js → disclosure-DvM7-z38.js} +1 -1
  59. package/dist/public/assets/{dist-CxMnSex-.js → dist-3tmIo7Ja.js} +1 -1
  60. package/dist/public/assets/{dist-ByY6lqkl.js → dist-sLs_LTiG.js} +1 -1
  61. package/dist/public/assets/{ebnfDiagram-PWID7BFC-uXsUC0ll.js → ebnfDiagram-PWID7BFC-Bq92n7k-.js} +1 -1
  62. package/dist/public/assets/{erDiagram-RLTQ6QDP-D5vJ6ZLT.js → erDiagram-RLTQ6QDP-C5I2bKnS.js} +1 -1
  63. package/dist/public/assets/eventmodeling-NTZA5JFV-CWxfOZuS.js +1 -0
  64. package/dist/public/assets/flowDiagram-HODETNUW-ddOOBXlS.js +1 -0
  65. package/dist/public/assets/{ganttDiagram-EL5Y4UJY-C6yTyvpx.js → ganttDiagram-EL5Y4UJY-qUaBgbbK.js} +1 -1
  66. package/dist/public/assets/{gitGraph-4MIJSDKK-C7nVbcZ1.js → gitGraph-4MIJSDKK-CUfoq3Sa.js} +1 -1
  67. package/dist/public/assets/{gitGraphDiagram-WWUBYQGX-DByBzF_5.js → gitGraphDiagram-WWUBYQGX-kj34Cchu.js} +1 -1
  68. package/dist/public/assets/{hotkeys-MSGaeY2m.js → hotkeys-DbIEec2f.js} +1 -1
  69. package/dist/public/assets/{htmlMode-DirKEo5j.js → htmlMode-C_uC6TDH.js} +1 -1
  70. package/dist/public/assets/{index-C1a3rCk2.js → index-BLx1AR0-.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-C938EzS-.js} +1 -1
  73. package/dist/public/assets/{infoDiagram-27XIBGKW-DDVeVGK9.js → infoDiagram-27XIBGKW-BoTxIjdn.js} +1 -1
  74. package/dist/public/assets/{input-i-kiT55N.js → input-DXYRmAFr.js} +1 -1
  75. package/dist/public/assets/{ishikawaDiagram-5VMMS53U-CTZvKcdI.js → ishikawaDiagram-5VMMS53U-j98L6THV.js} +1 -1
  76. package/dist/public/assets/{journeyDiagram-3NMN7TZE-CwFCP_8e.js → journeyDiagram-3NMN7TZE-DI-XCC2F.js} +1 -1
  77. package/dist/public/assets/{jsonMode-BLMO7T1W.js → jsonMode-BQ3590Ds.js} +1 -1
  78. package/dist/public/assets/{kanban-definition-UXKFOSKX-BCdCN6ov.js → kanban-definition-UXKFOSKX-BCTvFJTJ.js} +1 -1
  79. package/dist/public/assets/{line-e7ifLbsv.js → line-hE2Okp1W.js} +1 -1
  80. package/dist/public/assets/{media-refresh-store-d6Ihz_Yj.js → media-refresh-store-CxqhHl8x.js} +1 -1
  81. package/dist/public/assets/{mermaid-parser.core-DfmtQ5si.js → mermaid-parser.core-n0Wb6T7Q.js} +3 -3
  82. package/dist/public/assets/{mermaid.core-B0Ceo892.js → mermaid.core-Cb0bw4D8.js} +3 -3
  83. package/dist/public/assets/{mindmap-definition-YA3MSWOX-BKWZ0hGZ.js → mindmap-definition-YA3MSWOX-92LM1OuK.js} +1 -1
  84. package/dist/public/assets/{notification-permission-DThcB8rO.js → notification-permission-CG2r3Oi8.js} +1 -1
  85. package/dist/public/assets/{packet-AYTQ26CC-C_WSBR7W.js → packet-AYTQ26CC-_HBkwHYp.js} +1 -1
  86. package/dist/public/assets/{pdf-CW6jyF4i.js → pdf-DABk-fGs.js} +1 -1
  87. package/dist/public/assets/{pegDiagram-XKGWAZYB-CQw-vxyv.js → pegDiagram-XKGWAZYB-B-fG_WHS.js} +1 -1
  88. package/dist/public/assets/{pie-WAS4IAKB-CyAolSkk.js → pie-WAS4IAKB-ByxJto7v.js} +1 -1
  89. package/dist/public/assets/{pieDiagram-E7YTZNPT-fEikc7gQ.js → pieDiagram-E7YTZNPT-Ci8ZSq4L.js} +1 -1
  90. package/dist/public/assets/{project-abbrev-0K47Phcy.js → project-abbrev-BU2DBpdq.js} +1 -1
  91. package/dist/public/assets/{quadrantDiagram-AXDQQJYC-BLZF9CN6.js → quadrantDiagram-AXDQQJYC-B97Yoxpb.js} +1 -1
  92. package/dist/public/assets/{radar-RG4KPBEZ-DZj09Z3T.js → radar-RG4KPBEZ-BT2qdAqW.js} +1 -1
  93. package/dist/public/assets/{railroad-74A4TZTK-DrrM4RhH.js → railroad-74A4TZTK-Buny6l3B.js} +1 -1
  94. package/dist/public/assets/railroad-abnf-HS5TGJTU-C_aUf3vj.js +1 -0
  95. package/dist/public/assets/railroad-ebnf-LZEXJU2U-BGUVD_Qq.js +1 -0
  96. package/dist/public/assets/railroad-peg-WCYAUIDC-CGdTjBGv.js +1 -0
  97. package/dist/public/assets/{railroadDiagram-O6MQD6OU-BjnjeLPo.js → railroadDiagram-O6MQD6OU-BktpI6J7.js} +1 -1
  98. package/dist/public/assets/{requirementDiagram-BXWQKSXE-CJnHoVjp.js → requirementDiagram-BXWQKSXE-DYy_YKmd.js} +1 -1
  99. package/dist/public/assets/{sankeyDiagram-P5KCCOFB-BFBAycgU.js → sankeyDiagram-P5KCCOFB-BeKH_sFV.js} +1 -1
  100. package/dist/public/assets/{sequenceDiagram-WJ2MYXX4-IgIUmdp2.js → sequenceDiagram-WJ2MYXX4-DD8okFGM.js} +1 -1
  101. package/dist/public/assets/{stateDiagram-D77RDMKH-C8ragRoI.js → stateDiagram-D77RDMKH-Bfk3E3Oa.js} +1 -1
  102. package/dist/public/assets/stateDiagram-v2-MP3YSRHH-DCg8-XDy.js +1 -0
  103. package/dist/public/assets/{swimlanes-42K2YHIH-mm0BJE0h.js → swimlanes-42K2YHIH-OVNrzhfn.js} +1 -1
  104. package/dist/public/assets/swimlanesDiagram-VR7AAH4N-DAqvbP32.js +8 -0
  105. package/dist/public/assets/{switch-TfZl8YQu.js → switch-CN9NW-du.js} +1 -1
  106. package/dist/public/assets/{timeline-definition-24CTP7MA-CLtACXsf.js → timeline-definition-24CTP7MA-C78a8EdU.js} +1 -1
  107. package/dist/public/assets/{treeView-Q6P3EWNA-B_s8YnCD.js → treeView-Q6P3EWNA-Cl5tCoga.js} +1 -1
  108. package/dist/public/assets/{treemap-WGGIJYW6-CiyeynoG.js → treemap-WGGIJYW6-4YVcjSz9.js} +1 -1
  109. package/dist/public/assets/{tsMode-BJ9DHdAB.js → tsMode-CmGZ7MCT.js} +1 -1
  110. package/dist/public/assets/{use-ui-state-2bNBLDPU.js → use-ui-state-CzR7YpWb.js} +1 -1
  111. package/dist/public/assets/{vennDiagram-4TSXK5OY-C9rcT-1u.js → vennDiagram-4TSXK5OY-Lih9MwBs.js} +1 -1
  112. package/dist/public/assets/{wardley-WFR3VGLG-D_hqUvOM.js → wardley-WFR3VGLG-CcU7I6bt.js} +1 -1
  113. package/dist/public/assets/{wardleyDiagram-VM6X3IG4-Dfn7mGAc.js → wardleyDiagram-VM6X3IG4-DxHrszj8.js} +1 -1
  114. package/dist/public/assets/{ws-CDXqi1pN.js → ws-C-kvwpeD.js} +1 -1
  115. package/dist/public/assets/{xychartDiagram-S5SC5T6Z-vwVJPUe2.js → xychartDiagram-S5SC5T6Z-DpLvvdIG.js} +1 -1
  116. package/dist/public/index.html +2 -2
  117. package/package.json +1 -1
  118. package/dist/public/assets/architecture-7GRP2DOG-RMEcXYv9.js +0 -1
  119. package/dist/public/assets/channel-BheYsxDc.js +0 -1
  120. package/dist/public/assets/classDiagram-ZZMXUADV-CDDLBiV5.js +0 -1
  121. package/dist/public/assets/classDiagram-v2-VYDZK3BY-CDDLBiV5.js +0 -1
  122. package/dist/public/assets/eventmodeling-NTZA5JFV-WlMCgowE.js +0 -1
  123. package/dist/public/assets/flowDiagram-HODETNUW-DJ99l88N.js +0 -1
  124. package/dist/public/assets/index-Ctw2P0rY.css +0 -2
  125. package/dist/public/assets/railroad-abnf-HS5TGJTU-lXe6u3FO.js +0 -1
  126. package/dist/public/assets/railroad-ebnf-LZEXJU2U-C7R4jvUV.js +0 -1
  127. package/dist/public/assets/railroad-peg-WCYAUIDC-B24HGcGJ.js +0 -1
  128. package/dist/public/assets/stateDiagram-v2-MP3YSRHH-AU0bFC0Z.js +0 -1
  129. 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) {
@@ -20039,7 +20039,7 @@ var LocalTransport = class {
20039
20039
  kind = "local";
20040
20040
  label = "this machine";
20041
20041
  exec(command, opts = {}) {
20042
- return new Promise((resolve5) => {
20042
+ return new Promise((resolve6) => {
20043
20043
  const child = spawn("sh", ["-c", command], { stdio: ["pipe", "pipe", "pipe"] });
20044
20044
  let stdout = "";
20045
20045
  let stderr = "";
@@ -20056,11 +20056,11 @@ var LocalTransport = class {
20056
20056
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20057
20057
  child.on("error", (err) => {
20058
20058
  if (timer) clearTimeout(timer);
20059
- resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20059
+ resolve6({ code: -1, stdout, stderr: stderr + String(err) });
20060
20060
  });
20061
20061
  child.on("close", (code) => {
20062
20062
  if (timer) clearTimeout(timer);
20063
- resolve5({ code: code ?? -1, stdout, stderr });
20063
+ resolve6({ code: code ?? -1, stdout, stderr });
20064
20064
  });
20065
20065
  child.stdin.on("error", () => {
20066
20066
  });
@@ -20154,7 +20154,7 @@ var SshTransport = class {
20154
20154
  * live control socket behind.
20155
20155
  */
20156
20156
  open() {
20157
- return new Promise((resolve5, reject) => {
20157
+ return new Promise((resolve6, reject) => {
20158
20158
  const child = spawn2(this.ssh, this.args(this.host, "true"), {
20159
20159
  // A GUI has no useful inherited terminal; OpenSSH calls its askpass
20160
20160
  // helper instead. The terminal CLI keeps byte-for-byte inheritance.
@@ -20166,7 +20166,7 @@ var SshTransport = class {
20166
20166
  (err) => reject(new CliError("ssh_unreachable", `could not run ${this.ssh}: ${err.message}`))
20167
20167
  );
20168
20168
  child.on("close", (code) => {
20169
- if (code === 0) resolve5();
20169
+ if (code === 0) resolve6();
20170
20170
  else {
20171
20171
  reject(
20172
20172
  new CliError(
@@ -20182,13 +20182,13 @@ var SshTransport = class {
20182
20182
  /** Whether the master connection is still alive (-O check). */
20183
20183
  isAlive() {
20184
20184
  if (!this.hasControlMaster) return Promise.resolve(true);
20185
- return new Promise((resolve5) => {
20185
+ return new Promise((resolve6) => {
20186
20186
  const child = spawn2(this.ssh, this.args("-O", "check", this.host), {
20187
20187
  stdio: "ignore",
20188
20188
  env: this.spawnEnv()
20189
20189
  });
20190
- child.on("error", () => resolve5(false));
20191
- child.on("close", (code) => resolve5(code === 0));
20190
+ child.on("error", () => resolve6(false));
20191
+ child.on("close", (code) => resolve6(code === 0));
20192
20192
  });
20193
20193
  }
20194
20194
  /**
@@ -20201,18 +20201,18 @@ var SshTransport = class {
20201
20201
  */
20202
20202
  cancelForward(localPort, remotePort) {
20203
20203
  if (!this.hasControlMaster) return Promise.resolve();
20204
- return new Promise((resolve5) => {
20204
+ return new Promise((resolve6) => {
20205
20205
  const spec = `${localPort}:127.0.0.1:${remotePort}`;
20206
20206
  const child = spawn2(this.ssh, this.args("-O", "cancel", "-L", spec, this.host), {
20207
20207
  stdio: "ignore",
20208
20208
  env: this.spawnEnv()
20209
20209
  });
20210
- child.on("error", () => resolve5());
20211
- child.on("close", () => resolve5());
20210
+ child.on("error", () => resolve6());
20211
+ child.on("close", () => resolve6());
20212
20212
  });
20213
20213
  }
20214
20214
  exec(command, opts = {}) {
20215
- return new Promise((resolve5) => {
20215
+ return new Promise((resolve6) => {
20216
20216
  const child = spawn2(this.ssh, this.args(this.host, "--", `sh -c ${shellQuote(command)}`), {
20217
20217
  stdio: ["pipe", "pipe", "pipe"],
20218
20218
  env: this.spawnEnv()
@@ -20232,11 +20232,11 @@ var SshTransport = class {
20232
20232
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20233
20233
  child.on("error", (err) => {
20234
20234
  if (timer) clearTimeout(timer);
20235
- resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20235
+ resolve6({ code: -1, stdout, stderr: stderr + String(err) });
20236
20236
  });
20237
20237
  child.on("close", (code) => {
20238
20238
  if (timer) clearTimeout(timer);
20239
- resolve5({ code: code ?? -1, stdout, stderr });
20239
+ resolve6({ code: code ?? -1, stdout, stderr });
20240
20240
  });
20241
20241
  child.stdin.on("error", () => {
20242
20242
  });
@@ -20256,11 +20256,11 @@ var SshTransport = class {
20256
20256
  });
20257
20257
  let stdout = "";
20258
20258
  let stderr = "";
20259
- const result = new Promise((resolve5) => {
20259
+ const result = new Promise((resolve6) => {
20260
20260
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
20261
20261
  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 }));
20262
+ child.on("error", (err) => resolve6({ code: -1, stdout, stderr: stderr + String(err) }));
20263
+ child.on("close", (code) => resolve6({ code: code ?? -1, stdout, stderr }));
20264
20264
  });
20265
20265
  return {
20266
20266
  result,
@@ -20275,14 +20275,14 @@ var SshTransport = class {
20275
20275
  }
20276
20276
  async copyTo(localPath, destPath) {
20277
20277
  await this.exec(`mkdir -p $(dirname ${destPath})`, { timeoutMs: 15e3 });
20278
- await new Promise((resolve5, reject) => {
20278
+ await new Promise((resolve6, reject) => {
20279
20279
  const child = spawn2(this.scp, [...this.controlArgs, localPath, `${this.host}:${destPath}`], {
20280
20280
  stdio: ["ignore", "ignore", "inherit"],
20281
20281
  env: this.spawnEnv()
20282
20282
  });
20283
20283
  child.on("error", reject);
20284
20284
  child.on("close", (code) => {
20285
- if (code === 0) resolve5();
20285
+ if (code === 0) resolve6();
20286
20286
  else reject(new CliError("ssh_unreachable", `scp to ${this.host} failed (exit ${code})`));
20287
20287
  });
20288
20288
  });
@@ -20383,7 +20383,7 @@ async function registrationCode() {
20383
20383
  const readline = createInterface({ input: process.stdin, output: silent, terminal: true });
20384
20384
  try {
20385
20385
  return remoteSecretSchema.parse(
20386
- await new Promise((resolve5) => readline.question("", resolve5))
20386
+ await new Promise((resolve6) => readline.question("", resolve6))
20387
20387
  );
20388
20388
  } finally {
20389
20389
  readline.close();
@@ -20507,6 +20507,8 @@ running in the background once ready, so the terminal may close (--foreground
20507
20507
  to stay attached; Ctrl-C then stops the cockpit). refresh stops a target's
20508
20508
  cockpit (even a wedged one) and runs the full launch flow again \u2014 tunnel,
20509
20509
  daemon restart if needed \u2014 keeping the old UI port so open tabs survive.
20510
+ An older host protocol prompts before updating its daemon; --no-upgrade
20511
+ aborts before prompting. Without a terminal, upgrade the daemon explicitly first.
20510
20512
  list shows running cockpits; kill stops one. Sessions keep running under a
20511
20513
  persistent supervisor; an SSH-attached daemon keeps their state for the next launch.
20512
20514
 
@@ -20516,7 +20518,7 @@ AppImage placed where you say (default ~/puddle), then opened in the file
20516
20518
  manager. Already installed and no @version \u2192 nothing changes.
20517
20519
  upgrade moves components to the newest release (or the named @version),
20518
20520
  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
20521
+ installed on the target \u2014 the CLI last, using its installation channel. cli and desktop are
20520
20522
  client-machine artefacts: user@host targets the daemon only. remove
20521
20523
  uninstalls a component after confirmation; 'daemon' stops it (interrupting
20522
20524
  its sessions), unregisters the supervisor, and keeps ~/.puddle's data \u2014
@@ -20703,7 +20705,7 @@ function parseArgs(argv) {
20703
20705
  throw new CliError(
20704
20706
  "bad_arguments",
20705
20707
  "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"
20708
+ 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
20709
  );
20708
20710
  }
20709
20711
  return {
@@ -20780,6 +20782,182 @@ function argvFor(command) {
20780
20782
  return argv;
20781
20783
  }
20782
20784
 
20785
+ // src/cli/daemon-upgrade-prompt.ts
20786
+ import { randomUUID } from "node:crypto";
20787
+
20788
+ // src/lib/handshake.ts
20789
+ init_types();
20790
+ var CLI_UPGRADE_COMMAND = "puddle upgrade cli";
20791
+ function decideHandshake(cli, daemon) {
20792
+ if (daemon.major === cli.major) return { kind: "proceed" };
20793
+ if (daemon.major < cli.major) return { kind: "upgrade-daemon" };
20794
+ return { kind: "refuse-cli-outdated", command: CLI_UPGRADE_COMMAND };
20795
+ }
20796
+ function daemonUpgradeMessage(request) {
20797
+ const { host, daemon, clientProtocol, liveSessions } = request;
20798
+ 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.
20799
+
20800
+ Updating the host daemon will interrupt ${liveSessions} live session(s). Their saved state remains and they can resume after the update.`;
20801
+ }
20802
+ function verifyDaemonVersion(daemon) {
20803
+ const decision = decideHandshake(PROTOCOL_VERSION, daemon.protocol);
20804
+ if (decision.kind === "proceed") return daemon;
20805
+ if (decision.kind === "refuse-cli-outdated") {
20806
+ throw new CliError(
20807
+ "cli_outdated",
20808
+ `the daemon speaks protocol ${daemon.protocol.major}; this client speaks ${PROTOCOL_VERSION.major}`,
20809
+ `update the desktop app, or update the CLI: ${decision.command}`
20810
+ );
20811
+ }
20812
+ throw new CliError(
20813
+ "protocol_mismatch",
20814
+ `the daemon speaks protocol ${daemon.protocol.major}; this client speaks ${PROTOCOL_VERSION.major}`,
20815
+ "reconnect to update the host daemon"
20816
+ );
20817
+ }
20818
+ async function runHandshake(opts) {
20819
+ const { version: daemon, liveSessions } = opts.inspection;
20820
+ if (decideHandshake(PROTOCOL_VERSION, daemon.protocol).kind !== "upgrade-daemon") {
20821
+ return verifyDaemonVersion(daemon);
20822
+ }
20823
+ if (opts.noUpgrade) {
20824
+ throw new CliError(
20825
+ "upgrade_failed",
20826
+ `the daemon speaks protocol ${daemon.protocol.major} (client: ${PROTOCOL_VERSION.major}) and --no-upgrade is set`,
20827
+ `re-run without --no-upgrade to update it (${liveSessions} live session(s) would be interrupted and can be resumed)`
20828
+ );
20829
+ }
20830
+ const request = {
20831
+ host: opts.host,
20832
+ daemon,
20833
+ clientProtocol: PROTOCOL_VERSION,
20834
+ liveSessions
20835
+ };
20836
+ if (!await opts.confirmDaemonUpgrade?.(request)) {
20837
+ throw new CliError(
20838
+ "upgrade_failed",
20839
+ `the daemon update on ${opts.host} was not approved`,
20840
+ "launch again to approve the update, or run puddle upgrade daemon with the host as an optional argument"
20841
+ );
20842
+ }
20843
+ const latest = await opts.reinspect?.();
20844
+ if (latest && decideHandshake(PROTOCOL_VERSION, latest.version.protocol).kind !== "upgrade-daemon") {
20845
+ return verifyDaemonVersion(latest.version);
20846
+ }
20847
+ const logger = opts.logger ?? silentLogger;
20848
+ logger.info(
20849
+ `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`
20850
+ );
20851
+ try {
20852
+ return verifyDaemonVersion(await opts.upgradeDaemon());
20853
+ } catch (err) {
20854
+ if (err instanceof CliError && ["cli_outdated", "protocol_mismatch"].includes(err.code)) {
20855
+ throw new CliError(
20856
+ "upgrade_failed",
20857
+ `the daemon protocol is still incompatible after updating: ${err.message}`,
20858
+ "inspect the daemon logs: puddle logs"
20859
+ );
20860
+ }
20861
+ throw err;
20862
+ }
20863
+ }
20864
+
20865
+ // src/cli/prompt.ts
20866
+ init_types();
20867
+ import { createInterface as createInterface2 } from "node:readline";
20868
+ async function ask(question, def) {
20869
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return def;
20870
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
20871
+ try {
20872
+ const answer = await new Promise(
20873
+ (resolve6) => rl.question(`${question} [${def}] `, resolve6)
20874
+ );
20875
+ return answer.trim() === "" ? def : answer.trim();
20876
+ } finally {
20877
+ rl.close();
20878
+ }
20879
+ }
20880
+ async function confirm(question, opts = {}) {
20881
+ if (opts.skip === true) return true;
20882
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
20883
+ throw new CliError(
20884
+ "bad_arguments",
20885
+ "refusing to confirm without a terminal",
20886
+ "pass --yes to proceed non-interactively"
20887
+ );
20888
+ }
20889
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
20890
+ try {
20891
+ const answer = await new Promise(
20892
+ (resolve6) => rl.question(`${question} [y/N] `, resolve6)
20893
+ );
20894
+ return /^y(es)?$/i.test(answer.trim());
20895
+ } finally {
20896
+ rl.close();
20897
+ }
20898
+ }
20899
+
20900
+ // src/cli/daemon-upgrade-prompt.ts
20901
+ var REQUEST = "puddle:confirm-daemon-upgrade";
20902
+ var RESPONSE = "puddle:daemon-upgrade-answer";
20903
+ async function askToUpgrade(message) {
20904
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
20905
+ return confirm(`${message}
20906
+
20907
+ Update and connect?`);
20908
+ }
20909
+ var confirmDaemonUpgrade = async (request) => {
20910
+ const message = daemonUpgradeMessage(request);
20911
+ if (!process.send || !process.connected) return askToUpgrade(message);
20912
+ return requestDaemonUpgradeApproval(message, process);
20913
+ };
20914
+ function requestDaemonUpgradeApproval(message, channel) {
20915
+ if (!channel.send || !channel.connected) return Promise.resolve(false);
20916
+ const id = randomUUID();
20917
+ return new Promise((resolve6) => {
20918
+ const finish = (approved) => {
20919
+ channel.off("message", answered);
20920
+ channel.off("disconnect", disconnected);
20921
+ resolve6(approved);
20922
+ };
20923
+ const answered = (value) => {
20924
+ if (!value || typeof value !== "object") return;
20925
+ const response = value;
20926
+ if (response.t === RESPONSE && response.id === id && typeof response.approved === "boolean")
20927
+ finish(response.approved);
20928
+ };
20929
+ const disconnected = () => finish(false);
20930
+ channel.on("message", answered);
20931
+ channel.once("disconnect", disconnected);
20932
+ channel.send({ t: REQUEST, id, message }, (err) => {
20933
+ if (err) finish(false);
20934
+ });
20935
+ });
20936
+ }
20937
+ function relayDaemonUpgradePrompt(child, ask2 = askToUpgrade) {
20938
+ let active = true;
20939
+ let pending = false;
20940
+ const receive = (value) => {
20941
+ if (pending || !value || typeof value !== "object") return;
20942
+ const request = value;
20943
+ if (request.t !== REQUEST || typeof request.id !== "string" || typeof request.message !== "string")
20944
+ return;
20945
+ pending = true;
20946
+ void ask2(request.message).catch(() => false).then((approved) => {
20947
+ pending = false;
20948
+ if (active && child.connected)
20949
+ child.send({ t: RESPONSE, id: request.id, approved }, () => {
20950
+ });
20951
+ });
20952
+ };
20953
+ child.on("message", receive);
20954
+ return () => {
20955
+ active = false;
20956
+ child.off("message", receive);
20957
+ if (child.connected) child.disconnect();
20958
+ };
20959
+ }
20960
+
20783
20961
  // ../shared/src/node/security.ts
20784
20962
  import { createHash, randomBytes } from "node:crypto";
20785
20963
  import {
@@ -20867,14 +21045,14 @@ async function listenPrivate(path, onConnection) {
20867
21045
  privatePath(path);
20868
21046
  const before = lstatSync(path);
20869
21047
  if (!before.isSocket()) throw new Error("Puddle control path is not a socket");
20870
- const stale = await new Promise((resolve5, reject) => {
21048
+ const stale = await new Promise((resolve6, reject) => {
20871
21049
  const probe = connect(path);
20872
21050
  probe.once("connect", () => {
20873
21051
  probe.destroy();
20874
- resolve5(false);
21052
+ resolve6(false);
20875
21053
  });
20876
21054
  probe.once("error", (err) => {
20877
- if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve5(true);
21055
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve6(true);
20878
21056
  else reject(err);
20879
21057
  });
20880
21058
  });
@@ -20887,11 +21065,11 @@ async function listenPrivate(path, onConnection) {
20887
21065
  }
20888
21066
  }
20889
21067
  const server = createServer(onConnection);
20890
- await new Promise((resolve5, reject) => {
21068
+ await new Promise((resolve6, reject) => {
20891
21069
  server.once("error", reject);
20892
21070
  server.listen(path, () => {
20893
21071
  server.off("error", reject);
20894
- resolve5();
21072
+ resolve6();
20895
21073
  });
20896
21074
  });
20897
21075
  if (process.platform !== "win32") chmodSync(path, 384);
@@ -21005,7 +21183,7 @@ var HostControlClient = class {
21005
21183
  }
21006
21184
  this.channel = channel;
21007
21185
  this.until = performance.now() + CONNECTION_POLICY.leaseMs;
21008
- await new Promise((resolve5, reject) => {
21186
+ await new Promise((resolve6, reject) => {
21009
21187
  let pendingAt = null;
21010
21188
  let initial = true;
21011
21189
  const send = (message, sentAt = performance.now()) => {
@@ -21036,8 +21214,8 @@ var HostControlClient = class {
21036
21214
  this.status = "protocol_mismatch";
21037
21215
  reject(
21038
21216
  this.error(
21039
- "cli_outdated",
21040
- `Host protocol ${message.version.protocol.major} requires a compatible CLI`
21217
+ "protocol_mismatch",
21218
+ `Host protocol ${message.version.protocol.major} does not match client protocol ${PROTOCOL_VERSION.major}`
21041
21219
  )
21042
21220
  );
21043
21221
  channel.destroy();
@@ -21058,7 +21236,7 @@ var HostControlClient = class {
21058
21236
  if (this.channel !== channel) return;
21059
21237
  this.status = "ready";
21060
21238
  this.emit();
21061
- resolve5();
21239
+ resolve6();
21062
21240
  }).catch(() => channel.destroy());
21063
21241
  }
21064
21242
  },
@@ -21155,7 +21333,7 @@ async function inspectLegacyHost(home) {
21155
21333
  };
21156
21334
  try {
21157
21335
  const version2 = versionResponseSchema.parse(await get("/api/version"));
21158
- const sessions = sessionSchema.array().parse(await get("/api/sessions"));
21336
+ const sessions = sessionSchema.pick({ status: true }).array().parse(await get("/api/sessions"));
21159
21337
  return hostInspectionSchema.parse({
21160
21338
  t: "inspection",
21161
21339
  port,
@@ -21179,7 +21357,7 @@ async function inspectHostLocally(home) {
21179
21357
  }
21180
21358
  const socket = connect2(path);
21181
21359
  try {
21182
- const grant = await new Promise((resolve5, reject) => {
21360
+ const grant = await new Promise((resolve6, reject) => {
21183
21361
  socket.setTimeout(5e3, () => socket.destroy(new Error("Host inspection timed out")));
21184
21362
  socket.once("error", reject);
21185
21363
  socket.once("close", () => reject(new Error("Host control closed")));
@@ -21189,7 +21367,7 @@ async function inspectHostLocally(home) {
21189
21367
  const message = controlResponseSchema.parse(value);
21190
21368
  if (message.t !== "authority" || !message.token)
21191
21369
  throw new Error("Host authority unavailable");
21192
- resolve5(message);
21370
+ resolve6(message);
21193
21371
  },
21194
21372
  () => socket.destroy(new Error("Invalid host control response"))
21195
21373
  );
@@ -21200,7 +21378,7 @@ async function inspectHostLocally(home) {
21200
21378
  signal: AbortSignal.timeout(3e3)
21201
21379
  });
21202
21380
  if (!response.ok) throw new Error("Host inspection rejected");
21203
- const sessions = sessionSchema.array().parse(await response.json());
21381
+ const sessions = sessionSchema.pick({ status: true }).array().parse(await response.json());
21204
21382
  return hostInspectionSchema.parse({
21205
21383
  t: "inspection",
21206
21384
  port: grant.port,
@@ -21302,9 +21480,9 @@ async function acquireAuthority(transport) {
21302
21480
 
21303
21481
  // src/cli/run.ts
21304
21482
  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";
21483
+ import { existsSync as existsSync7 } from "node:fs";
21484
+ import { dirname as dirname12, join as join17 } from "node:path";
21485
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
21308
21486
 
21309
21487
  // src/lib/browser.ts
21310
21488
  import { spawn as spawn3 } from "node:child_process";
@@ -21347,7 +21525,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
21347
21525
  import { dirname as dirname4, join as join5 } from "node:path";
21348
21526
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21349
21527
  function cliVersion() {
21350
- if (true) return "0.2.4";
21528
+ if (true) return "0.2.6";
21351
21529
  const here = dirname4(fileURLToPath2(import.meta.url));
21352
21530
  for (const candidate of [
21353
21531
  join5(here, "..", "..", "package.json"),
@@ -21590,7 +21768,7 @@ function swapScript(staged2, opts) {
21590
21768
  }
21591
21769
  async function findInstalledDesktopApp(opts = {}) {
21592
21770
  if ((opts.platform ?? process.platform) !== "darwin") return null;
21593
- const { readFile: readFile2 } = await import("node:fs/promises");
21771
+ const { readFile: readFile3 } = await import("node:fs/promises");
21594
21772
  const { homedir: homedir3 } = await import("node:os");
21595
21773
  const systemApplications = opts.systemApplicationsDir ?? "/Applications";
21596
21774
  const home = opts.homeDir ?? homedir3();
@@ -21599,7 +21777,7 @@ async function findInstalledDesktopApp(opts = {}) {
21599
21777
  join6(home, "Applications", "Puddle.app")
21600
21778
  ]) {
21601
21779
  try {
21602
- const plist = await readFile2(join6(appPath, "Contents/Info.plist"), "utf8");
21780
+ const plist = await readFile3(join6(appPath, "Contents/Info.plist"), "utf8");
21603
21781
  const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(
21604
21782
  plist
21605
21783
  );
@@ -21627,12 +21805,12 @@ async function desktopAppInstallPath(opts = {}) {
21627
21805
  return join6(userApplications, "Puddle.app");
21628
21806
  }
21629
21807
  function isDesktopAppRunning(appPath) {
21630
- return new Promise((resolve5) => {
21808
+ return new Promise((resolve6) => {
21631
21809
  const child = spawn4("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
21632
21810
  stdio: "ignore"
21633
21811
  });
21634
- child.on("error", () => resolve5(false));
21635
- child.on("exit", (code) => resolve5(code === 0));
21812
+ child.on("error", () => resolve6(false));
21813
+ child.on("exit", (code) => resolve6(code === 0));
21636
21814
  });
21637
21815
  }
21638
21816
  function fileSha256(path) {
@@ -21640,12 +21818,12 @@ function fileSha256(path) {
21640
21818
  return pipeline(createReadStream(path), hash2).then(() => hash2.digest("hex"));
21641
21819
  }
21642
21820
  function run(command, args) {
21643
- return new Promise((resolve5, reject) => {
21821
+ return new Promise((resolve6, reject) => {
21644
21822
  const child = spawn4(command, args, { stdio: "ignore" });
21645
21823
  child.on("error", reject);
21646
21824
  child.on(
21647
21825
  "exit",
21648
- (code) => code === 0 ? resolve5() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
21826
+ (code) => code === 0 ? resolve6() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
21649
21827
  );
21650
21828
  });
21651
21829
  }
@@ -21724,7 +21902,9 @@ var RELEASE_PROTOCOLS = {
21724
21902
  "0.2.1": { major: 19, minor: 0 },
21725
21903
  "0.2.2": { major: 20, minor: 0 },
21726
21904
  "0.2.3": { major: 20, minor: 0 },
21727
- "0.2.4": { major: 21, minor: 2 }
21905
+ "0.2.4": { major: 21, minor: 2 },
21906
+ "0.2.5": { major: 21, minor: 2 },
21907
+ "0.2.6": { major: 21, minor: 2 }
21728
21908
  };
21729
21909
  function validProtocol(value) {
21730
21910
  if (typeof value !== "object" || value === null) return false;
@@ -21837,7 +22017,7 @@ function formatComponentVersions(components) {
21837
22017
  import { join as join12 } from "node:path";
21838
22018
 
21839
22019
  // src/lib/attached-daemon.ts
21840
- import { randomUUID } from "node:crypto";
22020
+ import { randomUUID as randomUUID2 } from "node:crypto";
21841
22021
 
21842
22022
  // src/lib/bootstrap.ts
21843
22023
  import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
@@ -21927,22 +22107,22 @@ function readInstallScript() {
21927
22107
  // src/lib/net.ts
21928
22108
  import { connect as connect4, createServer as createServer2 } from "node:net";
21929
22109
  function findFreePort() {
21930
- return new Promise((resolve5, reject) => {
22110
+ return new Promise((resolve6, reject) => {
21931
22111
  const srv = createServer2();
21932
22112
  srv.once("error", reject);
21933
22113
  srv.listen(0, "127.0.0.1", () => {
21934
22114
  const address = srv.address();
21935
22115
  const port = typeof address === "object" && address !== null ? address.port : 0;
21936
- srv.close(() => resolve5(port));
22116
+ srv.close(() => resolve6(port));
21937
22117
  });
21938
22118
  });
21939
22119
  }
21940
22120
  function tcpListening(port, timeoutMs = 500) {
21941
- return new Promise((resolve5) => {
22121
+ return new Promise((resolve6) => {
21942
22122
  const sock = connect4({ host: "127.0.0.1", port });
21943
22123
  const done = (ok) => {
21944
22124
  sock.destroy();
21945
- resolve5(ok);
22125
+ resolve6(ok);
21946
22126
  };
21947
22127
  sock.setTimeout(timeoutMs, () => done(false));
21948
22128
  sock.once("connect", () => done(true));
@@ -21978,35 +22158,50 @@ init_types();
21978
22158
  async function ensureDaemon(transport, opts) {
21979
22159
  const logger = opts.logger ?? silentLogger;
21980
22160
  const startTimeoutMs = opts.startTimeoutMs ?? (opts.attachedFallback === void 0 ? 2e4 : 5e3);
22161
+ let mismatch;
21981
22162
  try {
21982
22163
  const authority = await acquireAuthority(transport);
21983
22164
  return { port: authority.port, authority, bootstrapped: false, daemonLifetime: "persistent" };
21984
22165
  } catch (err) {
21985
- if (err instanceof CliError && err.code === "cli_outdated") throw err;
22166
+ if (err instanceof CliError && err.code === "protocol_mismatch") mismatch = err;
21986
22167
  }
21987
22168
  const inspection = await inspectHost(transport).catch((err) => {
21988
- if (err instanceof CliError) throw err;
22169
+ if (err instanceof CliError || mismatch) throw err;
21989
22170
  return null;
21990
22171
  });
22172
+ if (!inspection && mismatch) throw mismatch;
22173
+ const install = async () => {
22174
+ await installDaemon(transport, opts);
22175
+ return waitAfterInstall(transport, opts, logger, true, startTimeoutMs);
22176
+ };
21991
22177
  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
- );
22178
+ let replacement;
22179
+ try {
22180
+ await runHandshake({
22181
+ host: transport.label,
22182
+ inspection,
22183
+ noUpgrade: opts.noUpgrade,
22184
+ confirmDaemonUpgrade: opts.confirmDaemonUpgrade,
22185
+ reinspect: () => inspectHost(transport),
22186
+ logger,
22187
+ upgradeDaemon: async () => {
22188
+ replacement = await install();
22189
+ return replacement.authority.version;
22190
+ }
22191
+ });
22192
+ if (replacement) return replacement;
22193
+ const authority = await acquireAuthority(transport);
22194
+ return { port: authority.port, authority, bootstrapped: false, daemonLifetime: "persistent" };
22195
+ } catch (err) {
22196
+ replacement?.authority.close();
22197
+ await replacement?.lease?.stop();
22198
+ throw err;
22001
22199
  }
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) {
22200
+ }
22201
+ if (await installedVersion(transport) !== null) {
22006
22202
  logger.info(`restarting puddled on ${transport.label}`);
22007
22203
  }
22008
- await installDaemon(transport, opts);
22009
- return waitAfterInstall(transport, opts, logger, true, startTimeoutMs);
22204
+ return install();
22010
22205
  }
22011
22206
  async function waitAfterInstall(transport, opts, logger, bootstrapped, timeoutMs) {
22012
22207
  try {
@@ -22042,7 +22237,7 @@ async function waitForStartedDaemon(transport, timeoutMs, signal) {
22042
22237
  }
22043
22238
  return { port: authority.port, authority };
22044
22239
  } catch (err) {
22045
- if (err instanceof CliError && err.code === "cli_outdated") throw err;
22240
+ if (err instanceof CliError && err.code === "protocol_mismatch") throw err;
22046
22241
  }
22047
22242
  await sleep(500);
22048
22243
  }
@@ -22055,23 +22250,6 @@ function startTimeout(transport) {
22055
22250
  transport.kind === "ssh" ? `inspect it with: puddle logs ${transport.label}` : "inspect it with: puddle logs"
22056
22251
  );
22057
22252
  }
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
22253
 
22076
22254
  // src/lib/attached-daemon.ts
22077
22255
  init_types();
@@ -22134,7 +22312,7 @@ var AttachedDaemon = class {
22134
22312
  }
22135
22313
  async launchAndWait(preferredPort) {
22136
22314
  const portFlag = preferredPort === void 0 ? "" : ` --port ${preferredPort}`;
22137
- const leasePidPath = `${hostPaths.home}/attached-${randomUUID()}.pid`;
22315
+ const leasePidPath = `${hostPaths.home}/attached-${randomUUID2()}.pid`;
22138
22316
  this.leasePidPath = leasePidPath;
22139
22317
  const command = this.ssh.runAttached(
22140
22318
  `printf '%s\\n' "$$" > ${leasePidPath}; exec ${hostPaths.current}/puddled${portFlag} >> ${hostPaths.logs}/puddled.out.log 2>&1`
@@ -22230,10 +22408,6 @@ var DaemonClient = class {
22230
22408
  this.port = port;
22231
22409
  this.authority = authority;
22232
22410
  }
22233
- setAuthority(authority) {
22234
- this.authority.close();
22235
- this.authority = authority;
22236
- }
22237
22411
  setPort(port) {
22238
22412
  this.port = port;
22239
22413
  }
@@ -22275,21 +22449,6 @@ var DaemonClient = class {
22275
22449
  async liveSessions() {
22276
22450
  return (await this.sessions()).filter((s) => LIVE.has(s.status));
22277
22451
  }
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
22452
  };
22294
22453
  async function readDaemonPort(transport) {
22295
22454
  const runtime = await transport.readFile(hostPaths.runtime);
@@ -22311,51 +22470,8 @@ async function readDaemonPort(transport) {
22311
22470
  return 7434;
22312
22471
  }
22313
22472
 
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
22473
  // src/lib/serve/ui-server.ts
22358
- import { randomUUID as randomUUID2 } from "node:crypto";
22474
+ import { randomUUID as randomUUID3 } from "node:crypto";
22359
22475
  import { createServer as createServer3 } from "node:http";
22360
22476
 
22361
22477
  // src/lib/auth/launcher.ts
@@ -22391,7 +22507,7 @@ async function startLauncher(home, identity, invite) {
22391
22507
  path,
22392
22508
  close: async () => {
22393
22509
  for (const socket of sockets) socket.destroy();
22394
- await new Promise((resolve5) => server.close(() => resolve5()));
22510
+ await new Promise((resolve6) => server.close(() => resolve6()));
22395
22511
  }
22396
22512
  };
22397
22513
  }
@@ -22401,14 +22517,14 @@ async function requestInvitation(home, path) {
22401
22517
  const stored = readPrivateJson(secretFile(home, path));
22402
22518
  pipeSecret = stored.secret;
22403
22519
  } else privatePath(path);
22404
- return new Promise((resolve5, reject) => {
22520
+ return new Promise((resolve6, reject) => {
22405
22521
  const socket = connect5(path);
22406
22522
  socket.setTimeout(5e3, () => socket.destroy(new Error("Launcher did not answer")));
22407
22523
  jsonLines(
22408
22524
  socket,
22409
22525
  (value) => {
22410
22526
  const message = launcherResponseSchema.parse(value);
22411
- resolve5(message.url);
22527
+ resolve6(message.url);
22412
22528
  socket.end();
22413
22529
  },
22414
22530
  () => socket.destroy(new Error("Invalid launcher reply"))
@@ -22628,7 +22744,7 @@ function writeStore(file2, store) {
22628
22744
  renameSync3(tmp, file2);
22629
22745
  }
22630
22746
  function readBody(req) {
22631
- return new Promise((resolve5, reject) => {
22747
+ return new Promise((resolve6, reject) => {
22632
22748
  let size = 0;
22633
22749
  const chunks = [];
22634
22750
  req.on("data", (chunk) => {
@@ -22640,7 +22756,7 @@ function readBody(req) {
22640
22756
  }
22641
22757
  chunks.push(chunk);
22642
22758
  });
22643
- req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
22759
+ req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
22644
22760
  req.on("error", reject);
22645
22761
  });
22646
22762
  }
@@ -23227,7 +23343,7 @@ async function startUiServer(opts) {
23227
23343
  const tracker = new ProxySocketTracker();
23228
23344
  const serveStatic = createStaticHandler(opts.assetsDir);
23229
23345
  const handleRemoteAccess = remoteAccessHandler(opts.remoteAccess);
23230
- const nonce = randomUUID2();
23346
+ const nonce = randomUUID3();
23231
23347
  let origin = "";
23232
23348
  let proxyOrigin = "";
23233
23349
  let browsers;
@@ -23365,8 +23481,8 @@ async function startUiServer(opts) {
23365
23481
  browsers.close();
23366
23482
  bridge.close();
23367
23483
  tracker.destroyAll();
23368
- await new Promise((resolve5) => {
23369
- server.close(() => resolve5());
23484
+ await new Promise((resolve6) => {
23485
+ server.close(() => resolve6());
23370
23486
  server.closeAllConnections();
23371
23487
  });
23372
23488
  }
@@ -23376,15 +23492,15 @@ async function listen(server, startPort, strict, avoidPort) {
23376
23492
  for (let probe = 0; probe < MAX_PORT_PROBES; probe += 1) {
23377
23493
  const port = startPort + probe;
23378
23494
  if (!strict && port === avoidPort) continue;
23379
- const ok = await new Promise((resolve5, rejectListen) => {
23495
+ const ok = await new Promise((resolve6, rejectListen) => {
23380
23496
  const onError = (err) => {
23381
23497
  server.off("listening", onListening);
23382
- if (err.code === "EADDRINUSE") resolve5(false);
23498
+ if (err.code === "EADDRINUSE") resolve6(false);
23383
23499
  else rejectListen(err);
23384
23500
  };
23385
23501
  const onListening = () => {
23386
23502
  server.off("error", onError);
23387
- resolve5(true);
23503
+ resolve6(true);
23388
23504
  };
23389
23505
  server.once("error", onError);
23390
23506
  server.once("listening", onListening);
@@ -23665,7 +23781,12 @@ async function connectRemote(opts) {
23665
23781
  let uiResource;
23666
23782
  let endpointResource;
23667
23783
  try {
23668
- const bootstrap = { tarball: opts.tarball, logger, noUpgrade: opts.noUpgrade };
23784
+ const bootstrap = {
23785
+ tarball: opts.tarball,
23786
+ logger,
23787
+ noUpgrade: opts.noUpgrade,
23788
+ confirmDaemonUpgrade: opts.confirmDaemonUpgrade
23789
+ };
23669
23790
  const attachedDaemon = new AttachedDaemon(ssh, logger);
23670
23791
  const endpoint = await ensureDaemon(ssh, {
23671
23792
  ...bootstrap,
@@ -23706,12 +23827,7 @@ async function connectRemote(opts) {
23706
23827
  logger
23707
23828
  });
23708
23829
  oauthForwardResource = oauthForward;
23709
- const daemon = await runHandshake({
23710
- client,
23711
- noUpgrade: opts.noUpgrade,
23712
- upgradeDaemon: makeUpgrader(ssh, client, bootstrap, endpoint.lease),
23713
- logger
23714
- });
23830
+ const daemon = verifyDaemonVersion(await client.version());
23715
23831
  const avoidPort = await readDaemonPort(new LocalTransport());
23716
23832
  endpoint.authority.setVerifier(async () => {
23717
23833
  const generation = endpoint.authority.generation;
@@ -23734,7 +23850,7 @@ async function connectRemote(opts) {
23734
23850
  watchTunnel(replacement);
23735
23851
  await previous?.close();
23736
23852
  }
23737
- await client.version();
23853
+ verifyDaemonVersion(await client.version());
23738
23854
  });
23739
23855
  const ui = await startUiServer({
23740
23856
  authority: endpoint.authority,
@@ -23936,24 +24052,26 @@ init_types();
23936
24052
  async function startLocal(opts) {
23937
24053
  const logger = opts.logger ?? silentLogger;
23938
24054
  const transport = new LocalTransport();
23939
- const bootstrap = { tarball: opts.tarball, logger, noUpgrade: opts.noUpgrade };
23940
- const endpoint = await ensureDaemon(transport, bootstrap);
24055
+ const bootstrap = {
24056
+ tarball: opts.tarball,
24057
+ logger,
24058
+ noUpgrade: opts.noUpgrade,
24059
+ confirmDaemonUpgrade: opts.confirmDaemonUpgrade
24060
+ };
24061
+ let endpoint;
23941
24062
  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
- });
24063
+ endpoint = await ensureDaemon(transport, bootstrap);
24064
+ const authority = endpoint.authority;
24065
+ const client = new DaemonClient(endpoint.port, authority);
24066
+ const daemon = verifyDaemonVersion(await client.version());
23949
24067
  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 });
24068
+ authority.setVerifier(async () => {
24069
+ client.setPort(authority.port);
24070
+ verifyDaemonVersion(await client.version());
24071
+ currentUi?.setTarget({ host: "127.0.0.1", port: authority.port });
23954
24072
  });
23955
24073
  const ui = await startUiServer({
23956
- authority: endpoint.authority,
24074
+ authority,
23957
24075
  identity: "local",
23958
24076
  refreshId: opts.refreshId,
23959
24077
  assetsDir: opts.assetsDir,
@@ -23982,12 +24100,12 @@ async function startLocal(opts) {
23982
24100
  },
23983
24101
  async stop() {
23984
24102
  await ui.close();
23985
- endpoint.authority.close();
24103
+ authority.close();
23986
24104
  transport.dispose();
23987
24105
  }
23988
24106
  };
23989
24107
  } catch (err) {
23990
- endpoint.authority.close();
24108
+ endpoint?.authority.close();
23991
24109
  transport.dispose();
23992
24110
  throw err;
23993
24111
  }
@@ -24017,12 +24135,170 @@ async function statusReport(client) {
24017
24135
  // src/cli/run.ts
24018
24136
  init_types();
24019
24137
 
24020
- // src/cli/manage.ts
24138
+ // src/cli/manage-cli.ts
24139
+ init_types();
24021
24140
  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";
24141
+ import { dirname as dirname10 } from "node:path";
24142
+
24143
+ // src/lib/standalone-cli.ts
24144
+ init_types();
24145
+ import { spawn as spawn6 } from "node:child_process";
24146
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readFileSync as readFileSync9, realpathSync as realpathSync2 } from "node:fs";
24147
+ import { mkdir as mkdir2, readFile as readFile2, rm as rm2, unlink } from "node:fs/promises";
24148
+ import { dirname as dirname9, isAbsolute, join as join15, resolve as resolve5 } from "node:path";
24149
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
24150
+ function standaloneInstallation(entryFile = fileURLToPath4(import.meta.url), execPath = process.execPath) {
24151
+ const release = resolve5(dirname9(entryFile), "..");
24152
+ if (!existsSync5(join15(release, "STANDALONE"))) return null;
24153
+ try {
24154
+ const root = resolve5(release, "..", "..");
24155
+ const record2 = JSON.parse(readFileSync9(join15(root, "installation.json"), "utf8"));
24156
+ 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))
24157
+ throw new Error("Installation metadata does not match this CLI");
24158
+ return { root, release, launcher: record2.launcher };
24159
+ } catch {
24160
+ throw new CliError(
24161
+ "not_installed",
24162
+ "this standalone archive is not an installer-managed CLI",
24163
+ "run the public install.sh to install the CLI before using upgrade or remove"
24164
+ );
24165
+ }
24166
+ }
24167
+ function upgradeStandaloneCli(installation, version2, repo) {
24168
+ const script = readFileSync9(join15(installation.release, "cli/install-cli.sh"), "utf8");
24169
+ const args = [
24170
+ "-s",
24171
+ "--",
24172
+ "--prefix",
24173
+ installation.root,
24174
+ "--bin-dir",
24175
+ dirname9(installation.launcher)
24176
+ ];
24177
+ if (version2 !== void 0) args.push("--version", version2);
24178
+ if (repo !== void 0) args.push("--repo", repo);
24179
+ return new Promise((resolve6, reject) => {
24180
+ const child = spawn6("sh", args, { stdio: ["pipe", "inherit", "inherit"] });
24181
+ child.on("error", reject);
24182
+ child.stdin.on("error", (error51) => {
24183
+ if (error51.code !== "EPIPE") reject(error51);
24184
+ });
24185
+ child.stdin.end(script);
24186
+ child.on("exit", (code) => {
24187
+ if (code === 0) resolve6(0);
24188
+ else reject(new CliError("not_installed", `CLI installer exited with ${code ?? "a signal"}`));
24189
+ });
24190
+ });
24191
+ }
24192
+ function standaloneLauncher(root) {
24193
+ const quoted = `'${join15(root, "current/puddle").replaceAll("'", "'\\''")}'`;
24194
+ return `#!/bin/sh
24195
+ # puddle standalone launcher
24196
+ exec ${quoted} "$@"
24197
+ `;
24198
+ }
24199
+ async function removeStandaloneCli(installation) {
24200
+ const lock = join15(installation.root, ".install-lock");
24201
+ try {
24202
+ await mkdir2(lock);
24203
+ } catch {
24204
+ throw new CliError("already_running", "another CLI installation operation is running");
24205
+ }
24206
+ try {
24207
+ if (lstatSync2(installation.launcher).isSymbolicLink() || await readFile2(installation.launcher, "utf8") !== standaloneLauncher(installation.root))
24208
+ throw new CliError("not_installed", "the CLI launcher was replaced; refusing removal");
24209
+ await unlink(installation.launcher);
24210
+ await rm2(installation.root, { recursive: true, force: true });
24211
+ } finally {
24212
+ await rm2(lock, { recursive: true, force: true });
24213
+ }
24214
+ }
24215
+
24216
+ // src/cli/manage-cli.ts
24217
+ function upgradeCli(version2, logger) {
24218
+ const standalone = standaloneInstallation();
24219
+ if (standalone) {
24220
+ logger.info(`upgrading standalone CLI ${cliVersion()} to ${version2 ?? "the latest release"}`);
24221
+ return upgradeStandaloneCli(standalone, version2, repoSlug());
24222
+ }
24223
+ const spec = `@puddle-code/cli@${version2 ?? "latest"}`;
24224
+ logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for ${version2 ?? "the latest release"}`);
24225
+ return new Promise((resolve6, reject) => {
24226
+ const child = spawn7("npm", ["install", "-g", spec], { stdio: "inherit" });
24227
+ child.on(
24228
+ "error",
24229
+ () => reject(
24230
+ new CliError(
24231
+ "not_installed",
24232
+ "npm is not on PATH",
24233
+ `the CLI is installed and upgraded via npm: npm install -g ${spec}`
24234
+ )
24235
+ )
24236
+ );
24237
+ child.on("exit", (code) => {
24238
+ if (code === 0) {
24239
+ logger.info("done \u2014 `puddle --version` shows the installed version");
24240
+ resolve6(0);
24241
+ } else {
24242
+ reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
24243
+ }
24244
+ });
24245
+ });
24246
+ }
24247
+ async function removeCli(yes, logger) {
24248
+ const standalone = standaloneInstallation();
24249
+ if (standalone) {
24250
+ const proceed2 = await confirm(
24251
+ `Remove the standalone CLI at ${standalone.root} and its launcher in ${dirname10(standalone.launcher)}? Close its cockpits first; daemons and their sessions are untouched.`,
24252
+ { skip: yes }
24253
+ );
24254
+ if (!proceed2) {
24255
+ logger.info("nothing removed");
24256
+ return 0;
24257
+ }
24258
+ await removeStandaloneCli(standalone);
24259
+ logger.info("removed the standalone CLI \u2014 daemon installations and data are untouched");
24260
+ return 0;
24261
+ }
24262
+ const managed = await new Promise((resolve6) => {
24263
+ const child = spawn7("npm", ["ls", "-g", "@puddle-code/cli", "--depth=0"], { stdio: "ignore" });
24264
+ child.on("error", () => resolve6(false));
24265
+ child.on("exit", (code) => resolve6(code === 0));
24266
+ });
24267
+ if (!managed) {
24268
+ throw new CliError(
24269
+ "not_installed",
24270
+ "the puddle CLI is not an npm global install on this machine",
24271
+ "a repo checkout or linked dev build is removed the way it was installed"
24272
+ );
24273
+ }
24274
+ const proceed = await confirm(
24275
+ `Remove the puddle CLI (npm uninstall -g @puddle-code/cli)? Running cockpits keep running until killed; daemons and their sessions are untouched.`,
24276
+ { skip: yes }
24277
+ );
24278
+ if (!proceed) {
24279
+ logger.info("nothing removed");
24280
+ return 0;
24281
+ }
24282
+ return new Promise((resolve6, reject) => {
24283
+ const child = spawn7("npm", ["uninstall", "-g", "@puddle-code/cli"], { stdio: "inherit" });
24284
+ child.on("error", () => reject(new CliError("not_installed", "npm is not on PATH")));
24285
+ child.on("exit", (code) => {
24286
+ if (code === 0) {
24287
+ logger.info("removed \u2014 daemons keep running (puddle remove daemon uninstalls one)");
24288
+ resolve6(0);
24289
+ } else {
24290
+ reject(new CliError("not_installed", `npm uninstall exited with ${code ?? "a signal"}`));
24291
+ }
24292
+ });
24293
+ });
24294
+ }
24295
+
24296
+ // src/cli/manage.ts
24297
+ import { spawn as spawn9 } from "node:child_process";
24298
+ import { existsSync as existsSync6 } from "node:fs";
24299
+ import { chmod as chmod2, copyFile, mkdir as mkdir3, rename, rm as rm3 } from "node:fs/promises";
24024
24300
  import { homedir as homedir2 } from "node:os";
24025
- import { join as join15 } from "node:path";
24301
+ import { join as join16 } from "node:path";
24026
24302
 
24027
24303
  // src/lib/releases.ts
24028
24304
  init_types();
@@ -24155,9 +24431,9 @@ async function upgradeDaemon(transport, opts = {}) {
24155
24431
  }
24156
24432
 
24157
24433
  // 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";
24434
+ import { spawn as spawn8 } from "node:child_process";
24435
+ import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync10 } from "node:fs";
24436
+ import { dirname as dirname11 } from "node:path";
24161
24437
  init_types();
24162
24438
  var COCKPIT_CHILD_ENV = "PUDDLE_COCKPIT_CHILD";
24163
24439
  var READY_TIMEOUT_MS = 3e5;
@@ -24196,12 +24472,12 @@ async function launchDetached(opts) {
24196
24472
  }
24197
24473
  if (target !== "local") await new SshTransport(target).open();
24198
24474
  const logFile = cockpitLogPath(target);
24199
- mkdirSync6(dirname9(logFile), { recursive: true });
24475
+ mkdirSync6(dirname11(logFile), { recursive: true });
24200
24476
  const fd = openSync2(logFile, "w");
24201
24477
  const argv = opts.argv ?? process.argv.slice(2);
24202
- const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
24478
+ const child = spawn8(process.execPath, [process.argv[1] ?? "", ...argv], {
24203
24479
  detached: true,
24204
- stdio: ["ignore", fd, fd],
24480
+ stdio: ["ignore", fd, fd, "ipc"],
24205
24481
  env: { ...process.env, [COCKPIT_CHILD_ENV]: "1" }
24206
24482
  });
24207
24483
  closeSync2(fd);
@@ -24210,7 +24486,13 @@ async function launchDetached(opts) {
24210
24486
  throw new CliError("cockpit_launch_failed", "could not spawn the background cockpit process");
24211
24487
  }
24212
24488
  logger.info(`starting the cockpit for ${target} in the background\u2026`);
24213
- const record2 = await followStartup(target, child.pid, logFile);
24489
+ const stopPromptRelay = relayDaemonUpgradePrompt(child);
24490
+ let record2;
24491
+ try {
24492
+ record2 = await followStartup(target, child.pid, logFile);
24493
+ } finally {
24494
+ stopPromptRelay();
24495
+ }
24214
24496
  if (record2 === null || record2.status !== "ready") {
24215
24497
  if (record2?.status === "error") {
24216
24498
  logger.warn(record2.message ?? "the cockpit failed to start");
@@ -24233,7 +24515,7 @@ async function followStartup(target, childPid, logFile) {
24233
24515
  let offset = 0;
24234
24516
  const relay = () => {
24235
24517
  try {
24236
- const buf = readFileSync9(logFile);
24518
+ const buf = readFileSync10(logFile);
24237
24519
  if (buf.length > offset) {
24238
24520
  process.stderr.write(buf.subarray(offset));
24239
24521
  offset = buf.length;
@@ -24258,7 +24540,7 @@ async function followStartup(target, childPid, logFile) {
24258
24540
  }
24259
24541
  function spawnDetachedRefresh(target, argv, refreshId) {
24260
24542
  const logFile = cockpitLogPath(target);
24261
- mkdirSync6(dirname9(logFile), { recursive: true });
24543
+ mkdirSync6(dirname11(logFile), { recursive: true });
24262
24544
  const fd = openSync2(logFile, "a");
24263
24545
  const env = {
24264
24546
  ...process.env,
@@ -24266,7 +24548,7 @@ function spawnDetachedRefresh(target, argv, refreshId) {
24266
24548
  PUDDLE_INTERNAL_REFRESH: "1"
24267
24549
  };
24268
24550
  delete env[COCKPIT_CHILD_ENV];
24269
- const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
24551
+ const child = spawn8(process.execPath, [process.argv[1] ?? "", ...argv], {
24270
24552
  detached: true,
24271
24553
  stdio: ["ignore", fd, fd],
24272
24554
  env
@@ -24299,41 +24581,6 @@ async function presentInvitation(record2, noBrowser, logger) {
24299
24581
  else openBrowser(url2);
24300
24582
  }
24301
24583
 
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
24584
  // src/cli/manage.ts
24338
24585
  function assertLocal(what, host, verb) {
24339
24586
  if (host === void 0 || host === "local") return;
@@ -24442,31 +24689,6 @@ async function runUpgrade(cmd, logger) {
24442
24689
  else if (process.platform === "darwin") logger.info("no desktop app installed \u2014 skipping");
24443
24690
  return upgradeCli(cmd.version, logger);
24444
24691
  }
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
24692
  async function setDesktop(opts, logger) {
24471
24693
  if (process.platform === "linux") {
24472
24694
  if (opts.verb !== "install") {
@@ -24538,22 +24760,22 @@ async function installDesktopAppImage(version2, logger) {
24538
24760
  );
24539
24761
  }
24540
24762
  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) {
24763
+ const target = join16(dir, "Puddle.AppImage");
24764
+ if (existsSync6(target) && version2 === void 0) {
24543
24765
  logger.info(`${target} already exists \u2014 the app updates itself in-app (its update toast)`);
24544
24766
  return 0;
24545
24767
  }
24546
24768
  logger.info(`installing Puddle ${update.version} to ${target}`);
24547
24769
  const staged2 = await stageDesktopUpdate(update, { logger });
24548
- await mkdir2(dir, { recursive: true });
24549
- await rm2(target, { force: true });
24770
+ await mkdir3(dir, { recursive: true });
24771
+ await rm3(target, { force: true });
24550
24772
  try {
24551
24773
  await rename(staged2.stagedPath, target);
24552
24774
  } catch {
24553
24775
  await copyFile(staged2.stagedPath, target);
24554
24776
  await chmod2(target, 493);
24555
24777
  }
24556
- await rm2(staged2.dir, { recursive: true, force: true });
24778
+ await rm3(staged2.dir, { recursive: true, force: true });
24557
24779
  const protocol = componentProtocolForVersion(update.version);
24558
24780
  recordDesktopInstallation({
24559
24781
  path: target,
@@ -24561,14 +24783,14 @@ async function installDesktopAppImage(version2, logger) {
24561
24783
  ...protocol ? { protocol } : {}
24562
24784
  });
24563
24785
  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 });
24786
+ const opener = spawn9("xdg-open", [dir], { stdio: "ignore", detached: true });
24565
24787
  opener.on("error", () => void 0);
24566
24788
  opener.unref();
24567
24789
  return 0;
24568
24790
  }
24569
24791
  function expandHome2(path) {
24570
24792
  if (path === "~") return homedir2();
24571
- return path.startsWith("~/") ? join15(homedir2(), path.slice(2)) : path;
24793
+ return path.startsWith("~/") ? join16(homedir2(), path.slice(2)) : path;
24572
24794
  }
24573
24795
  async function runRemove(cmd, logger) {
24574
24796
  switch (cmd.what) {
@@ -24582,40 +24804,6 @@ async function runRemove(cmd, logger) {
24582
24804
  return removeDaemonCmd(cmd, logger);
24583
24805
  }
24584
24806
  }
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
24807
  async function removeDesktop(yes, logger) {
24620
24808
  if (process.platform !== "darwin") {
24621
24809
  throw new CliError(
@@ -24640,7 +24828,7 @@ async function removeDesktop(yes, logger) {
24640
24828
  logger.info("nothing removed");
24641
24829
  return 0;
24642
24830
  }
24643
- await rm2(installed.appPath, { recursive: true, force: true });
24831
+ await rm3(installed.appPath, { recursive: true, force: true });
24644
24832
  await pruneDesktopUpdateCache([]);
24645
24833
  logger.info(`deleted ${installed.appPath} (recent hosts kept in ~/.puddle/recent-hosts.json)`);
24646
24834
  return 0;
@@ -24762,9 +24950,9 @@ function timestampedLogger() {
24762
24950
 
24763
24951
  // src/cli/run.ts
24764
24952
  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;
24953
+ const here = dirname12(fileURLToPath5(import.meta.url));
24954
+ for (const candidate of [join17(here, "public"), join17(here, "..", "..", "dist", "public")]) {
24955
+ if (existsSync7(join17(candidate, "index.html"))) return candidate;
24768
24956
  }
24769
24957
  throw new CliError(
24770
24958
  "not_installed",
@@ -24798,7 +24986,7 @@ async function openTarget(host) {
24798
24986
  };
24799
24987
  }
24800
24988
  function runUntilInterrupted(stop, farewell) {
24801
- return new Promise((resolve5) => {
24989
+ return new Promise((resolve6) => {
24802
24990
  let stopping = false;
24803
24991
  process.on("SIGINT", () => {
24804
24992
  if (stopping) process.exit(130);
@@ -24806,10 +24994,10 @@ function runUntilInterrupted(stop, farewell) {
24806
24994
  process.stderr.write(`
24807
24995
  ${farewell}
24808
24996
  `);
24809
- void stop().then(resolve5);
24997
+ void stop().then(resolve6);
24810
24998
  });
24811
24999
  process.on("SIGTERM", () => {
24812
- void stop().then(resolve5);
25000
+ void stop().then(resolve6);
24813
25001
  });
24814
25002
  });
24815
25003
  }
@@ -25097,6 +25285,7 @@ async function runCockpit(command, target, terminal) {
25097
25285
  const common = {
25098
25286
  ...command,
25099
25287
  assetsDir: assetsDir(),
25288
+ confirmDaemonUpgrade,
25100
25289
  logger,
25101
25290
  onRefreshRequest,
25102
25291
  refreshId: process.env.PUDDLE_REFRESH_ID