@puddle-code/cli 0.2.3 → 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 (138) hide show
  1. package/CHANGELOG.md +2 -13
  2. package/README.md +33 -23
  3. package/dist/host-control.mjs +40 -2
  4. package/dist/index.js +541 -313
  5. package/dist/install-cli.sh +167 -0
  6. package/dist/install.sh +7 -10
  7. package/dist/public/assets/{App-JZgRWo4t.js → App-CzuHShdO.js} +2 -2
  8. package/dist/public/assets/{Dashboard-DNWLZwxT.js → Dashboard-B5qWtmwb.js} +1 -1
  9. package/dist/public/assets/{EditorTabClose-CdpMpKYq.js → EditorTabClose-uxuNJdgx.js} +1 -1
  10. package/dist/public/assets/FindWidget-lUpiYZJG.js +1 -0
  11. package/dist/public/assets/{LazyTerminal-DVeI2Lkl.js → LazyTerminal-BYbPpGEI.js} +2 -2
  12. package/dist/public/assets/{ModelRefcount-ALfiNB9K.js → ModelRefcount-N0Xh_Y-_.js} +1 -1
  13. package/dist/public/assets/{NewProjectDialog-RHBHLIQe.js → NewProjectDialog-D3bNK3u_.js} +1 -1
  14. package/dist/public/assets/PaneEditorBody-DrSiMzrp.js +553 -0
  15. package/dist/public/assets/{PdfViewer-DZrh-S3Z.js → PdfViewer-BhqrWfmb.js} +2 -2
  16. package/dist/public/assets/{SettingsDialog-D-yMKqY9.js → SettingsDialog-CamnF91I.js} +2 -2
  17. package/dist/public/assets/Terminal-Ds17sCF_.js +60 -0
  18. package/dist/public/assets/Workspace-B6mgQyIM.css +1 -0
  19. package/dist/public/assets/{Workspace-DhFU_Ltc.js → Workspace-BmtA0kYQ.js} +6 -6
  20. package/dist/public/assets/{abnfDiagram-VCTEODGH-Bb_QGl6J.js → abnfDiagram-VCTEODGH-B4ZtDUXi.js} +1 -1
  21. package/dist/public/assets/architecture-7GRP2DOG-D0iH6Lxl.js +1 -0
  22. package/dist/public/assets/{architectureDiagram-5GKGNRK7-CF6YHpo7.js → architectureDiagram-5GKGNRK7-Cyzuyzwl.js} +1 -1
  23. package/dist/public/assets/{blockDiagram-I7D4REHJ-DgUouS8v.js → blockDiagram-I7D4REHJ-BN00JnNl.js} +1 -1
  24. package/dist/public/assets/{buffer-store-DcARHjQ8.js → buffer-store--4yU4YqM.js} +2 -2
  25. package/dist/public/assets/{button-CyO-gAMJ.js → button-DehrlYw7.js} +1 -1
  26. package/dist/public/assets/{c4Diagram-7LVT6UL2-CFhvq4-y.js → c4Diagram-7LVT6UL2-DpUKodBy.js} +1 -1
  27. package/dist/public/assets/channel-DT2izo70.js +1 -0
  28. package/dist/public/assets/{chunk-2E4U76K2-CskNiADZ.js → chunk-2E4U76K2-BksaqXZU.js} +1 -1
  29. package/dist/public/assets/{chunk-4HAMMTFA-DY74ei7y.js → chunk-4HAMMTFA-D01k6w0A.js} +1 -1
  30. package/dist/public/assets/{chunk-75Z2AOVW-sHPItT4L.js → chunk-75Z2AOVW-DJJjslyX.js} +1 -1
  31. package/dist/public/assets/{chunk-CLGD4ZFX-BbZdl00J.js → chunk-CLGD4ZFX-CX1B9i6H.js} +1 -1
  32. package/dist/public/assets/{chunk-DU6HZSFF-BZvHUJoN.js → chunk-DU6HZSFF-Bx2ELCn-.js} +1 -1
  33. package/dist/public/assets/{chunk-F27PBJKO-C-FJqBSD.js → chunk-F27PBJKO-CCDhODx8.js} +1 -1
  34. package/dist/public/assets/{chunk-GMAD6QVW-DPkzF1Ic.js → chunk-GMAD6QVW-C1evXXZi.js} +1 -1
  35. package/dist/public/assets/{chunk-GVQU2GXP-Batc2xZS.js → chunk-GVQU2GXP-C1iphgLK.js} +1 -1
  36. package/dist/public/assets/{chunk-IMKFNOWR-CAMx72vu.js → chunk-IMKFNOWR-5wMdkmsQ.js} +1 -1
  37. package/dist/public/assets/{chunk-L3NEJ4N5-CuIGdGy-.js → chunk-L3NEJ4N5-B-aXfMpF.js} +1 -1
  38. package/dist/public/assets/{chunk-OSK3NFVY-uNp8Mp7C.js → chunk-OSK3NFVY-Bm2MaFOc.js} +1 -1
  39. package/dist/public/assets/{chunk-P2QGCYS3-CoRPhFtM.js → chunk-P2QGCYS3-H02JaAun.js} +1 -1
  40. package/dist/public/assets/{chunk-POPQ4Y6H-DMOCBqAw.js → chunk-POPQ4Y6H-Dmm46Beg.js} +1 -1
  41. package/dist/public/assets/{chunk-PWAF6VOD-DOQkLdyp.js → chunk-PWAF6VOD-CRt8Ete5.js} +1 -1
  42. package/dist/public/assets/{chunk-SHT3W25Y-BJJe_uau.js → chunk-SHT3W25Y-w4aNK54y.js} +1 -1
  43. package/dist/public/assets/{chunk-SVP7TREG-D5B1jrjj.js → chunk-SVP7TREG-Cl3QpMDs.js} +1 -1
  44. package/dist/public/assets/{chunk-TICWLB2K-C9NafHGC.js → chunk-TICWLB2K-BpBbot9h.js} +1 -1
  45. package/dist/public/assets/{chunk-TLUHSLCS-Ctl_RxEM.js → chunk-TLUHSLCS-BOnIB6P6.js} +2 -2
  46. package/dist/public/assets/classDiagram-ZZMXUADV-BSshJUxJ.js +1 -0
  47. package/dist/public/assets/classDiagram-v2-VYDZK3BY-BSshJUxJ.js +1 -0
  48. package/dist/public/assets/{context-menu-BuiHQTu6.js → context-menu-BT88Vgia.js} +1 -1
  49. package/dist/public/assets/{cssMode-DozYXR6g.js → cssMode-gXT_K4po.js} +1 -1
  50. package/dist/public/assets/{cynefin-OW5HDTMX-gj0lEe4i.js → cynefin-OW5HDTMX-CJakxaH8.js} +1 -1
  51. package/dist/public/assets/{cynefinDiagram-5FMLGOSQ-lYCffbhY.js → cynefinDiagram-5FMLGOSQ-BJ_fVnC9.js} +1 -1
  52. package/dist/public/assets/{dagre-GXQ25YYZ-DyYVAAw7.js → dagre-GXQ25YYZ-CjflcbuS.js} +1 -1
  53. package/dist/public/assets/{diagram-S7CK7UJ4-Dop5OEsZ.js → diagram-S7CK7UJ4-g8pKIWrY.js} +1 -1
  54. package/dist/public/assets/{diagram-UQ7AKVKN-B5qxPK0m.js → diagram-UQ7AKVKN-Dm5Pg4rY.js} +1 -1
  55. package/dist/public/assets/{diagram-VSXAHHWV-zvsxaxBC.js → diagram-VSXAHHWV-DV5CVAXg.js} +1 -1
  56. package/dist/public/assets/{diagram-VX7I27RA-S7nRhoRV.js → diagram-VX7I27RA-Be9qCk-h.js} +1 -1
  57. package/dist/public/assets/{diagram-Z3DM3KII-C_jkP2wb.js → diagram-Z3DM3KII-C3-xDrxs.js} +1 -1
  58. package/dist/public/assets/{dialog-D6I1fiX4.js → dialog-CAMTxigV.js} +1 -1
  59. package/dist/public/assets/{disclosure-Dn2Pws0i.js → disclosure-DvM7-z38.js} +1 -1
  60. package/dist/public/assets/dist-3tmIo7Ja.js +1 -0
  61. package/dist/public/assets/{dist-Dt0LZb5E.js → dist-sLs_LTiG.js} +1 -1
  62. package/dist/public/assets/{ebnfDiagram-PWID7BFC-B1OmWQdM.js → ebnfDiagram-PWID7BFC-Bq92n7k-.js} +1 -1
  63. package/dist/public/assets/{erDiagram-RLTQ6QDP-DJKky3Wx.js → erDiagram-RLTQ6QDP-C5I2bKnS.js} +1 -1
  64. package/dist/public/assets/eventmodeling-NTZA5JFV-CWxfOZuS.js +1 -0
  65. package/dist/public/assets/flowDiagram-HODETNUW-ddOOBXlS.js +1 -0
  66. package/dist/public/assets/{ganttDiagram-EL5Y4UJY-qEYH72Jx.js → ganttDiagram-EL5Y4UJY-qUaBgbbK.js} +1 -1
  67. package/dist/public/assets/{gitGraph-4MIJSDKK-DlVqGPr_.js → gitGraph-4MIJSDKK-CUfoq3Sa.js} +1 -1
  68. package/dist/public/assets/{gitGraphDiagram-WWUBYQGX-DjfCMlWr.js → gitGraphDiagram-WWUBYQGX-kj34Cchu.js} +1 -1
  69. package/dist/public/assets/{hotkeys-Bx0QQoOi.js → hotkeys-DbIEec2f.js} +1 -1
  70. package/dist/public/assets/{htmlMode-DlKAkkNW.js → htmlMode-C_uC6TDH.js} +1 -1
  71. package/dist/public/assets/index-BLx1AR0-.js +82 -0
  72. package/dist/public/assets/index-uKreLbru.css +2 -0
  73. package/dist/public/assets/{info-A6RAGUB7-CH2cc1Sk.js → info-A6RAGUB7-C938EzS-.js} +1 -1
  74. package/dist/public/assets/{infoDiagram-27XIBGKW-QLQ5mJz6.js → infoDiagram-27XIBGKW-BoTxIjdn.js} +1 -1
  75. package/dist/public/assets/input-DXYRmAFr.js +1 -0
  76. package/dist/public/assets/{ishikawaDiagram-5VMMS53U-Dgd8STMm.js → ishikawaDiagram-5VMMS53U-j98L6THV.js} +1 -1
  77. package/dist/public/assets/{journeyDiagram-3NMN7TZE-BaIBwMwN.js → journeyDiagram-3NMN7TZE-DI-XCC2F.js} +1 -1
  78. package/dist/public/assets/{jsonMode-B-xhmxpv.js → jsonMode-BQ3590Ds.js} +1 -1
  79. package/dist/public/assets/{kanban-definition-UXKFOSKX-DUwkSFm5.js → kanban-definition-UXKFOSKX-BCTvFJTJ.js} +1 -1
  80. package/dist/public/assets/{line-DBd--km9.js → line-hE2Okp1W.js} +1 -1
  81. package/dist/public/assets/{media-refresh-store-C2PF6Nl9.js → media-refresh-store-CxqhHl8x.js} +1 -1
  82. package/dist/public/assets/{mermaid-parser.core-CH9r0Plq.js → mermaid-parser.core-n0Wb6T7Q.js} +3 -3
  83. package/dist/public/assets/{mermaid.core-Dx4uf1Og.js → mermaid.core-Cb0bw4D8.js} +3 -3
  84. package/dist/public/assets/{mindmap-definition-YA3MSWOX-DFlzr0Rk.js → mindmap-definition-YA3MSWOX-92LM1OuK.js} +1 -1
  85. package/dist/public/assets/{notification-permission-BJ5Ee1QS.js → notification-permission-CG2r3Oi8.js} +1 -1
  86. package/dist/public/assets/{packet-AYTQ26CC-BoGgl5A0.js → packet-AYTQ26CC-_HBkwHYp.js} +1 -1
  87. package/dist/public/assets/{pdf-CIIeLqAE.js → pdf-DABk-fGs.js} +1 -1
  88. package/dist/public/assets/{pegDiagram-XKGWAZYB-dBubzdqe.js → pegDiagram-XKGWAZYB-B-fG_WHS.js} +1 -1
  89. package/dist/public/assets/{pie-WAS4IAKB-C8Ujmmnq.js → pie-WAS4IAKB-ByxJto7v.js} +1 -1
  90. package/dist/public/assets/{pieDiagram-E7YTZNPT-BarO3dDM.js → pieDiagram-E7YTZNPT-Ci8ZSq4L.js} +1 -1
  91. package/dist/public/assets/{project-abbrev-BlBz9tcM.js → project-abbrev-BU2DBpdq.js} +1 -1
  92. package/dist/public/assets/{quadrantDiagram-AXDQQJYC-Bq7mziYZ.js → quadrantDiagram-AXDQQJYC-B97Yoxpb.js} +1 -1
  93. package/dist/public/assets/{radar-RG4KPBEZ-CSOiAaDJ.js → radar-RG4KPBEZ-BT2qdAqW.js} +1 -1
  94. package/dist/public/assets/{railroad-74A4TZTK-B72WpbJE.js → railroad-74A4TZTK-Buny6l3B.js} +1 -1
  95. package/dist/public/assets/railroad-abnf-HS5TGJTU-C_aUf3vj.js +1 -0
  96. package/dist/public/assets/railroad-ebnf-LZEXJU2U-BGUVD_Qq.js +1 -0
  97. package/dist/public/assets/railroad-peg-WCYAUIDC-CGdTjBGv.js +1 -0
  98. package/dist/public/assets/{railroadDiagram-O6MQD6OU-CdpFpJTq.js → railroadDiagram-O6MQD6OU-BktpI6J7.js} +1 -1
  99. package/dist/public/assets/{requirementDiagram-BXWQKSXE-D5gAf9X3.js → requirementDiagram-BXWQKSXE-DYy_YKmd.js} +1 -1
  100. package/dist/public/assets/{sankeyDiagram-P5KCCOFB-CWGxwWQO.js → sankeyDiagram-P5KCCOFB-BeKH_sFV.js} +1 -1
  101. package/dist/public/assets/{sequenceDiagram-WJ2MYXX4-D4OU6YFv.js → sequenceDiagram-WJ2MYXX4-DD8okFGM.js} +1 -1
  102. package/dist/public/assets/{stateDiagram-D77RDMKH-DL34KHS6.js → stateDiagram-D77RDMKH-Bfk3E3Oa.js} +1 -1
  103. package/dist/public/assets/stateDiagram-v2-MP3YSRHH-DCg8-XDy.js +1 -0
  104. package/dist/public/assets/{swimlanes-42K2YHIH-ClnyergN.js → swimlanes-42K2YHIH-OVNrzhfn.js} +1 -1
  105. package/dist/public/assets/swimlanesDiagram-VR7AAH4N-DAqvbP32.js +8 -0
  106. package/dist/public/assets/{switch-JLAneJz1.js → switch-CN9NW-du.js} +1 -1
  107. package/dist/public/assets/{timeline-definition-24CTP7MA-Bouu1PMd.js → timeline-definition-24CTP7MA-C78a8EdU.js} +1 -1
  108. package/dist/public/assets/{treeView-Q6P3EWNA-OLbwx3G_.js → treeView-Q6P3EWNA-Cl5tCoga.js} +1 -1
  109. package/dist/public/assets/{treemap-WGGIJYW6-BWtOLy1c.js → treemap-WGGIJYW6-4YVcjSz9.js} +1 -1
  110. package/dist/public/assets/{tsMode-BChcsbnx.js → tsMode-CmGZ7MCT.js} +1 -1
  111. package/dist/public/assets/{use-ui-state-xRIsbDTY.js → use-ui-state-CzR7YpWb.js} +1 -1
  112. package/dist/public/assets/{vennDiagram-4TSXK5OY-B7Iv-Yti.js → vennDiagram-4TSXK5OY-Lih9MwBs.js} +1 -1
  113. package/dist/public/assets/{wardley-WFR3VGLG-bREi8xGw.js → wardley-WFR3VGLG-CcU7I6bt.js} +1 -1
  114. package/dist/public/assets/{wardleyDiagram-VM6X3IG4-BCZ7iNOW.js → wardleyDiagram-VM6X3IG4-DxHrszj8.js} +1 -1
  115. package/dist/public/assets/{ws-BzWidrUz.js → ws-C-kvwpeD.js} +1 -1
  116. package/dist/public/assets/{xychartDiagram-S5SC5T6Z-eLBpE0yW.js → xychartDiagram-S5SC5T6Z-DpLvvdIG.js} +1 -1
  117. package/dist/public/index.html +2 -2
  118. package/dist/public/preview.html +18 -0
  119. package/package.json +1 -1
  120. package/dist/public/assets/PaneEditorBody-BnTDt5LG.js +0 -553
  121. package/dist/public/assets/Terminal-CoQpOpFQ.js +0 -60
  122. package/dist/public/assets/Workspace-klU-NMuZ.css +0 -1
  123. package/dist/public/assets/architecture-7GRP2DOG-DR2uP7PV.js +0 -1
  124. package/dist/public/assets/channel-CUVxklBM.js +0 -1
  125. package/dist/public/assets/classDiagram-ZZMXUADV-Cgl1n0Dz.js +0 -1
  126. package/dist/public/assets/classDiagram-v2-VYDZK3BY-Cgl1n0Dz.js +0 -1
  127. package/dist/public/assets/dist-tn5vjhJE.js +0 -1
  128. package/dist/public/assets/eventmodeling-NTZA5JFV-mkLPvEKM.js +0 -1
  129. package/dist/public/assets/flowDiagram-HODETNUW-O3VTLiVc.js +0 -1
  130. package/dist/public/assets/index-Ctw2P0rY.css +0 -2
  131. package/dist/public/assets/index-DWTNflld.js +0 -82
  132. package/dist/public/assets/input-CWrHR6Ba.js +0 -1
  133. package/dist/public/assets/railroad-abnf-HS5TGJTU-6aqbSR4_.js +0 -1
  134. package/dist/public/assets/railroad-ebnf-LZEXJU2U-DSe-edrj.js +0 -1
  135. package/dist/public/assets/railroad-peg-WCYAUIDC-CxQjyuGw.js +0 -1
  136. package/dist/public/assets/stateDiagram-v2-MP3YSRHH-C86Q0rld.js +0 -1
  137. package/dist/public/assets/swimlanesDiagram-VR7AAH4N-TVdovaZm.js +0 -8
  138. package/dist/public/assets/use-find-controls-DEA0SpBh.js +0 -1
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: 20, minor: 0 };
3928
+ var PROTOCOL_VERSION = { major: 21, minor: 2 };
3929
3929
 
3930
3930
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
3931
3931
  var external_exports = {};
@@ -18459,6 +18459,8 @@ var profileId = external_exports.string().regex(/^[0-9a-f]{10}$/);
18459
18459
  // ../shared/src/api/version.ts
18460
18460
  var versionResponseSchema = external_exports.object({
18461
18461
  version: external_exports.string(),
18462
+ /** Added by the host connector; absent on direct daemon responses and older connectors. */
18463
+ remote_image_uploads: external_exports.boolean().optional(),
18462
18464
  protocol: external_exports.object({
18463
18465
  major: external_exports.number().int().nonnegative(),
18464
18466
  minor: external_exports.number().int().nonnegative()
@@ -19386,6 +19388,11 @@ var fileResponseSchema = external_exports.object({
19386
19388
  size: external_exports.number().int().nonnegative(),
19387
19389
  mtime_ms: external_exports.number()
19388
19390
  });
19391
+ var MAX_PREVIEW_ASSET_BYTES = 8 * 1024 * 1024;
19392
+ var previewAssetResponseSchema = external_exports.object({
19393
+ mime: external_exports.string().min(1).max(128),
19394
+ data: external_exports.string().max(4 * Math.ceil(MAX_PREVIEW_ASSET_BYTES / 3))
19395
+ });
19389
19396
  var putFileRequestSchema = external_exports.object({
19390
19397
  content: external_exports.string(),
19391
19398
  /** Optimistic-concurrency guard: reject the write if the file changed since this mtime. */
@@ -19914,6 +19921,36 @@ var browserAuthorityStateSchema = external_exports.object({
19914
19921
  proxies: external_exports.array(proxyRecordSchema)
19915
19922
  });
19916
19923
 
19924
+ // ../shared/src/remote/image-upload.ts
19925
+ var IMAGE_UPLOAD_POLICY = {
19926
+ imageBytes: 4 * 1024 * 1024,
19927
+ chunkBytes: 64 * 1024,
19928
+ idleMs: 6e4,
19929
+ lifetimeMs: 5 * 6e4
19930
+ };
19931
+ var imageUploadRequestSchema = external_exports.discriminatedUnion("action", [
19932
+ external_exports.object({
19933
+ action: external_exports.literal("start"),
19934
+ upload: external_exports.string().uuid(),
19935
+ mime: pasteImageMimeSchema,
19936
+ size: external_exports.number().int().positive().max(IMAGE_UPLOAD_POLICY.imageBytes)
19937
+ }).strict(),
19938
+ external_exports.object({
19939
+ action: external_exports.literal("chunk"),
19940
+ upload: external_exports.string().uuid(),
19941
+ offset: external_exports.number().int().nonnegative().max(IMAGE_UPLOAD_POLICY.imageBytes),
19942
+ data: external_exports.string().min(4).max(4 * Math.ceil(IMAGE_UPLOAD_POLICY.chunkBytes / 3)).regex(/^[A-Za-z0-9+/]+={0,2}$/).refine((data) => {
19943
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
19944
+ return data.length % 4 === 0 && data.length / 4 * 3 - padding <= IMAGE_UPLOAD_POLICY.chunkBytes;
19945
+ }, "Image chunk is too large or malformed")
19946
+ }).strict(),
19947
+ external_exports.object({ action: external_exports.literal("finish"), upload: external_exports.string().uuid() }).strict(),
19948
+ external_exports.object({ action: external_exports.literal("cancel"), upload: external_exports.string().uuid() }).strict()
19949
+ ]);
19950
+ var imageUploadProgressSchema = external_exports.object({
19951
+ received: external_exports.number().int().nonnegative().max(IMAGE_UPLOAD_POLICY.imageBytes)
19952
+ });
19953
+
19917
19954
  // ../shared/src/remote/service.ts
19918
19955
  var remoteHostSchema = external_exports.object({
19919
19956
  id: remoteIdSchema,
@@ -19938,6 +19975,7 @@ var remoteLoginStateSchema = external_exports.object({
19938
19975
  var registerHostRequestSchema = external_exports.object({ label: remoteLabelSchema }).strict();
19939
19976
  var registrationResponseSchema = external_exports.object({ host: remoteIdSchema, code: remoteSecretSchema, expires: external_exports.number() }).strict();
19940
19977
  var redeemRegistrationSchema = external_exports.object({ code: remoteSecretSchema }).strict();
19978
+ var unregisterHostRequestSchema = external_exports.object({ credential: remoteSecretSchema }).strict();
19941
19979
  var desktopRegistrationSchema = external_exports.object({
19942
19980
  challenge: remoteSecretSchema,
19943
19981
  label: remoteLabelSchema,
@@ -20001,7 +20039,7 @@ var LocalTransport = class {
20001
20039
  kind = "local";
20002
20040
  label = "this machine";
20003
20041
  exec(command, opts = {}) {
20004
- return new Promise((resolve5) => {
20042
+ return new Promise((resolve6) => {
20005
20043
  const child = spawn("sh", ["-c", command], { stdio: ["pipe", "pipe", "pipe"] });
20006
20044
  let stdout = "";
20007
20045
  let stderr = "";
@@ -20018,11 +20056,11 @@ var LocalTransport = class {
20018
20056
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20019
20057
  child.on("error", (err) => {
20020
20058
  if (timer) clearTimeout(timer);
20021
- resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20059
+ resolve6({ code: -1, stdout, stderr: stderr + String(err) });
20022
20060
  });
20023
20061
  child.on("close", (code) => {
20024
20062
  if (timer) clearTimeout(timer);
20025
- resolve5({ code: code ?? -1, stdout, stderr });
20063
+ resolve6({ code: code ?? -1, stdout, stderr });
20026
20064
  });
20027
20065
  child.stdin.on("error", () => {
20028
20066
  });
@@ -20116,7 +20154,7 @@ var SshTransport = class {
20116
20154
  * live control socket behind.
20117
20155
  */
20118
20156
  open() {
20119
- return new Promise((resolve5, reject) => {
20157
+ return new Promise((resolve6, reject) => {
20120
20158
  const child = spawn2(this.ssh, this.args(this.host, "true"), {
20121
20159
  // A GUI has no useful inherited terminal; OpenSSH calls its askpass
20122
20160
  // helper instead. The terminal CLI keeps byte-for-byte inheritance.
@@ -20128,7 +20166,7 @@ var SshTransport = class {
20128
20166
  (err) => reject(new CliError("ssh_unreachable", `could not run ${this.ssh}: ${err.message}`))
20129
20167
  );
20130
20168
  child.on("close", (code) => {
20131
- if (code === 0) resolve5();
20169
+ if (code === 0) resolve6();
20132
20170
  else {
20133
20171
  reject(
20134
20172
  new CliError(
@@ -20144,13 +20182,13 @@ var SshTransport = class {
20144
20182
  /** Whether the master connection is still alive (-O check). */
20145
20183
  isAlive() {
20146
20184
  if (!this.hasControlMaster) return Promise.resolve(true);
20147
- return new Promise((resolve5) => {
20185
+ return new Promise((resolve6) => {
20148
20186
  const child = spawn2(this.ssh, this.args("-O", "check", this.host), {
20149
20187
  stdio: "ignore",
20150
20188
  env: this.spawnEnv()
20151
20189
  });
20152
- child.on("error", () => resolve5(false));
20153
- child.on("close", (code) => resolve5(code === 0));
20190
+ child.on("error", () => resolve6(false));
20191
+ child.on("close", (code) => resolve6(code === 0));
20154
20192
  });
20155
20193
  }
20156
20194
  /**
@@ -20163,18 +20201,18 @@ var SshTransport = class {
20163
20201
  */
20164
20202
  cancelForward(localPort, remotePort) {
20165
20203
  if (!this.hasControlMaster) return Promise.resolve();
20166
- return new Promise((resolve5) => {
20204
+ return new Promise((resolve6) => {
20167
20205
  const spec = `${localPort}:127.0.0.1:${remotePort}`;
20168
20206
  const child = spawn2(this.ssh, this.args("-O", "cancel", "-L", spec, this.host), {
20169
20207
  stdio: "ignore",
20170
20208
  env: this.spawnEnv()
20171
20209
  });
20172
- child.on("error", () => resolve5());
20173
- child.on("close", () => resolve5());
20210
+ child.on("error", () => resolve6());
20211
+ child.on("close", () => resolve6());
20174
20212
  });
20175
20213
  }
20176
20214
  exec(command, opts = {}) {
20177
- return new Promise((resolve5) => {
20215
+ return new Promise((resolve6) => {
20178
20216
  const child = spawn2(this.ssh, this.args(this.host, "--", `sh -c ${shellQuote(command)}`), {
20179
20217
  stdio: ["pipe", "pipe", "pipe"],
20180
20218
  env: this.spawnEnv()
@@ -20194,11 +20232,11 @@ var SshTransport = class {
20194
20232
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20195
20233
  child.on("error", (err) => {
20196
20234
  if (timer) clearTimeout(timer);
20197
- resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20235
+ resolve6({ code: -1, stdout, stderr: stderr + String(err) });
20198
20236
  });
20199
20237
  child.on("close", (code) => {
20200
20238
  if (timer) clearTimeout(timer);
20201
- resolve5({ code: code ?? -1, stdout, stderr });
20239
+ resolve6({ code: code ?? -1, stdout, stderr });
20202
20240
  });
20203
20241
  child.stdin.on("error", () => {
20204
20242
  });
@@ -20218,11 +20256,11 @@ var SshTransport = class {
20218
20256
  });
20219
20257
  let stdout = "";
20220
20258
  let stderr = "";
20221
- const result = new Promise((resolve5) => {
20259
+ const result = new Promise((resolve6) => {
20222
20260
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
20223
20261
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20224
- child.on("error", (err) => resolve5({ code: -1, stdout, stderr: stderr + String(err) }));
20225
- 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 }));
20226
20264
  });
20227
20265
  return {
20228
20266
  result,
@@ -20237,14 +20275,14 @@ var SshTransport = class {
20237
20275
  }
20238
20276
  async copyTo(localPath, destPath) {
20239
20277
  await this.exec(`mkdir -p $(dirname ${destPath})`, { timeoutMs: 15e3 });
20240
- await new Promise((resolve5, reject) => {
20278
+ await new Promise((resolve6, reject) => {
20241
20279
  const child = spawn2(this.scp, [...this.controlArgs, localPath, `${this.host}:${destPath}`], {
20242
20280
  stdio: ["ignore", "ignore", "inherit"],
20243
20281
  env: this.spawnEnv()
20244
20282
  });
20245
20283
  child.on("error", reject);
20246
20284
  child.on("close", (code) => {
20247
- if (code === 0) resolve5();
20285
+ if (code === 0) resolve6();
20248
20286
  else reject(new CliError("ssh_unreachable", `scp to ${this.host} failed (exit ${code})`));
20249
20287
  });
20250
20288
  });
@@ -20345,7 +20383,7 @@ async function registrationCode() {
20345
20383
  const readline = createInterface({ input: process.stdin, output: silent, terminal: true });
20346
20384
  try {
20347
20385
  return remoteSecretSchema.parse(
20348
- await new Promise((resolve5) => readline.question("", resolve5))
20386
+ await new Promise((resolve6) => readline.question("", resolve6))
20349
20387
  );
20350
20388
  } finally {
20351
20389
  readline.close();
@@ -20469,6 +20507,8 @@ running in the background once ready, so the terminal may close (--foreground
20469
20507
  to stay attached; Ctrl-C then stops the cockpit). refresh stops a target's
20470
20508
  cockpit (even a wedged one) and runs the full launch flow again \u2014 tunnel,
20471
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.
20472
20512
  list shows running cockpits; kill stops one. Sessions keep running under a
20473
20513
  persistent supervisor; an SSH-attached daemon keeps their state for the next launch.
20474
20514
 
@@ -20478,7 +20518,7 @@ AppImage placed where you say (default ~/puddle), then opened in the file
20478
20518
  manager. Already installed and no @version \u2192 nothing changes.
20479
20519
  upgrade moves components to the newest release (or the named @version),
20480
20520
  installing any that are missing; with no component it covers everything
20481
- 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
20482
20522
  client-machine artefacts: user@host targets the daemon only. remove
20483
20523
  uninstalls a component after confirmation; 'daemon' stops it (interrupting
20484
20524
  its sessions), unregisters the supervisor, and keeps ~/.puddle's data \u2014
@@ -20665,7 +20705,7 @@ function parseArgs(argv) {
20665
20705
  throw new CliError(
20666
20706
  "bad_arguments",
20667
20707
  "install takes one of: daemon | desktop (with an optional @version)",
20668
- 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"
20669
20709
  );
20670
20710
  }
20671
20711
  return {
@@ -20742,6 +20782,182 @@ function argvFor(command) {
20742
20782
  return argv;
20743
20783
  }
20744
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
+
20745
20961
  // ../shared/src/node/security.ts
20746
20962
  import { createHash, randomBytes } from "node:crypto";
20747
20963
  import {
@@ -20829,14 +21045,14 @@ async function listenPrivate(path, onConnection) {
20829
21045
  privatePath(path);
20830
21046
  const before = lstatSync(path);
20831
21047
  if (!before.isSocket()) throw new Error("Puddle control path is not a socket");
20832
- const stale = await new Promise((resolve5, reject) => {
21048
+ const stale = await new Promise((resolve6, reject) => {
20833
21049
  const probe = connect(path);
20834
21050
  probe.once("connect", () => {
20835
21051
  probe.destroy();
20836
- resolve5(false);
21052
+ resolve6(false);
20837
21053
  });
20838
21054
  probe.once("error", (err) => {
20839
- if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve5(true);
21055
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve6(true);
20840
21056
  else reject(err);
20841
21057
  });
20842
21058
  });
@@ -20849,11 +21065,11 @@ async function listenPrivate(path, onConnection) {
20849
21065
  }
20850
21066
  }
20851
21067
  const server = createServer(onConnection);
20852
- await new Promise((resolve5, reject) => {
21068
+ await new Promise((resolve6, reject) => {
20853
21069
  server.once("error", reject);
20854
21070
  server.listen(path, () => {
20855
21071
  server.off("error", reject);
20856
- resolve5();
21072
+ resolve6();
20857
21073
  });
20858
21074
  });
20859
21075
  if (process.platform !== "win32") chmodSync(path, 384);
@@ -20967,7 +21183,7 @@ var HostControlClient = class {
20967
21183
  }
20968
21184
  this.channel = channel;
20969
21185
  this.until = performance.now() + CONNECTION_POLICY.leaseMs;
20970
- await new Promise((resolve5, reject) => {
21186
+ await new Promise((resolve6, reject) => {
20971
21187
  let pendingAt = null;
20972
21188
  let initial = true;
20973
21189
  const send = (message, sentAt = performance.now()) => {
@@ -20998,8 +21214,8 @@ var HostControlClient = class {
20998
21214
  this.status = "protocol_mismatch";
20999
21215
  reject(
21000
21216
  this.error(
21001
- "cli_outdated",
21002
- `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}`
21003
21219
  )
21004
21220
  );
21005
21221
  channel.destroy();
@@ -21020,7 +21236,7 @@ var HostControlClient = class {
21020
21236
  if (this.channel !== channel) return;
21021
21237
  this.status = "ready";
21022
21238
  this.emit();
21023
- resolve5();
21239
+ resolve6();
21024
21240
  }).catch(() => channel.destroy());
21025
21241
  }
21026
21242
  },
@@ -21117,7 +21333,7 @@ async function inspectLegacyHost(home) {
21117
21333
  };
21118
21334
  try {
21119
21335
  const version2 = versionResponseSchema.parse(await get("/api/version"));
21120
- const sessions = sessionSchema.array().parse(await get("/api/sessions"));
21336
+ const sessions = sessionSchema.pick({ status: true }).array().parse(await get("/api/sessions"));
21121
21337
  return hostInspectionSchema.parse({
21122
21338
  t: "inspection",
21123
21339
  port,
@@ -21141,7 +21357,7 @@ async function inspectHostLocally(home) {
21141
21357
  }
21142
21358
  const socket = connect2(path);
21143
21359
  try {
21144
- const grant = await new Promise((resolve5, reject) => {
21360
+ const grant = await new Promise((resolve6, reject) => {
21145
21361
  socket.setTimeout(5e3, () => socket.destroy(new Error("Host inspection timed out")));
21146
21362
  socket.once("error", reject);
21147
21363
  socket.once("close", () => reject(new Error("Host control closed")));
@@ -21151,7 +21367,7 @@ async function inspectHostLocally(home) {
21151
21367
  const message = controlResponseSchema.parse(value);
21152
21368
  if (message.t !== "authority" || !message.token)
21153
21369
  throw new Error("Host authority unavailable");
21154
- resolve5(message);
21370
+ resolve6(message);
21155
21371
  },
21156
21372
  () => socket.destroy(new Error("Invalid host control response"))
21157
21373
  );
@@ -21162,7 +21378,7 @@ async function inspectHostLocally(home) {
21162
21378
  signal: AbortSignal.timeout(3e3)
21163
21379
  });
21164
21380
  if (!response.ok) throw new Error("Host inspection rejected");
21165
- const sessions = sessionSchema.array().parse(await response.json());
21381
+ const sessions = sessionSchema.pick({ status: true }).array().parse(await response.json());
21166
21382
  return hostInspectionSchema.parse({
21167
21383
  t: "inspection",
21168
21384
  port: grant.port,
@@ -21264,9 +21480,9 @@ async function acquireAuthority(transport) {
21264
21480
 
21265
21481
  // src/cli/run.ts
21266
21482
  init_attach();
21267
- import { existsSync as existsSync6 } from "node:fs";
21268
- import { dirname as dirname10, join as join16 } from "node:path";
21269
- 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";
21270
21486
 
21271
21487
  // src/lib/browser.ts
21272
21488
  import { spawn as spawn3 } from "node:child_process";
@@ -21309,7 +21525,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
21309
21525
  import { dirname as dirname4, join as join5 } from "node:path";
21310
21526
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21311
21527
  function cliVersion() {
21312
- if (true) return "0.2.3";
21528
+ if (true) return "0.2.6";
21313
21529
  const here = dirname4(fileURLToPath2(import.meta.url));
21314
21530
  for (const candidate of [
21315
21531
  join5(here, "..", "..", "package.json"),
@@ -21552,7 +21768,7 @@ function swapScript(staged2, opts) {
21552
21768
  }
21553
21769
  async function findInstalledDesktopApp(opts = {}) {
21554
21770
  if ((opts.platform ?? process.platform) !== "darwin") return null;
21555
- const { readFile: readFile2 } = await import("node:fs/promises");
21771
+ const { readFile: readFile3 } = await import("node:fs/promises");
21556
21772
  const { homedir: homedir3 } = await import("node:os");
21557
21773
  const systemApplications = opts.systemApplicationsDir ?? "/Applications";
21558
21774
  const home = opts.homeDir ?? homedir3();
@@ -21561,7 +21777,7 @@ async function findInstalledDesktopApp(opts = {}) {
21561
21777
  join6(home, "Applications", "Puddle.app")
21562
21778
  ]) {
21563
21779
  try {
21564
- const plist = await readFile2(join6(appPath, "Contents/Info.plist"), "utf8");
21780
+ const plist = await readFile3(join6(appPath, "Contents/Info.plist"), "utf8");
21565
21781
  const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(
21566
21782
  plist
21567
21783
  );
@@ -21589,12 +21805,12 @@ async function desktopAppInstallPath(opts = {}) {
21589
21805
  return join6(userApplications, "Puddle.app");
21590
21806
  }
21591
21807
  function isDesktopAppRunning(appPath) {
21592
- return new Promise((resolve5) => {
21808
+ return new Promise((resolve6) => {
21593
21809
  const child = spawn4("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
21594
21810
  stdio: "ignore"
21595
21811
  });
21596
- child.on("error", () => resolve5(false));
21597
- child.on("exit", (code) => resolve5(code === 0));
21812
+ child.on("error", () => resolve6(false));
21813
+ child.on("exit", (code) => resolve6(code === 0));
21598
21814
  });
21599
21815
  }
21600
21816
  function fileSha256(path) {
@@ -21602,12 +21818,12 @@ function fileSha256(path) {
21602
21818
  return pipeline(createReadStream(path), hash2).then(() => hash2.digest("hex"));
21603
21819
  }
21604
21820
  function run(command, args) {
21605
- return new Promise((resolve5, reject) => {
21821
+ return new Promise((resolve6, reject) => {
21606
21822
  const child = spawn4(command, args, { stdio: "ignore" });
21607
21823
  child.on("error", reject);
21608
21824
  child.on(
21609
21825
  "exit",
21610
- (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"}`))
21611
21827
  );
21612
21828
  });
21613
21829
  }
@@ -21685,7 +21901,10 @@ var RELEASE_PROTOCOLS = {
21685
21901
  "0.2.0": { major: 19, minor: 0 },
21686
21902
  "0.2.1": { major: 19, minor: 0 },
21687
21903
  "0.2.2": { major: 20, minor: 0 },
21688
- "0.2.3": { major: 20, minor: 0 }
21904
+ "0.2.3": { major: 20, minor: 0 },
21905
+ "0.2.4": { major: 21, minor: 2 },
21906
+ "0.2.5": { major: 21, minor: 2 },
21907
+ "0.2.6": { major: 21, minor: 2 }
21689
21908
  };
21690
21909
  function validProtocol(value) {
21691
21910
  if (typeof value !== "object" || value === null) return false;
@@ -21798,7 +22017,7 @@ function formatComponentVersions(components) {
21798
22017
  import { join as join12 } from "node:path";
21799
22018
 
21800
22019
  // src/lib/attached-daemon.ts
21801
- import { randomUUID } from "node:crypto";
22020
+ import { randomUUID as randomUUID2 } from "node:crypto";
21802
22021
 
21803
22022
  // src/lib/bootstrap.ts
21804
22023
  import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
@@ -21888,22 +22107,22 @@ function readInstallScript() {
21888
22107
  // src/lib/net.ts
21889
22108
  import { connect as connect4, createServer as createServer2 } from "node:net";
21890
22109
  function findFreePort() {
21891
- return new Promise((resolve5, reject) => {
22110
+ return new Promise((resolve6, reject) => {
21892
22111
  const srv = createServer2();
21893
22112
  srv.once("error", reject);
21894
22113
  srv.listen(0, "127.0.0.1", () => {
21895
22114
  const address = srv.address();
21896
22115
  const port = typeof address === "object" && address !== null ? address.port : 0;
21897
- srv.close(() => resolve5(port));
22116
+ srv.close(() => resolve6(port));
21898
22117
  });
21899
22118
  });
21900
22119
  }
21901
22120
  function tcpListening(port, timeoutMs = 500) {
21902
- return new Promise((resolve5) => {
22121
+ return new Promise((resolve6) => {
21903
22122
  const sock = connect4({ host: "127.0.0.1", port });
21904
22123
  const done = (ok) => {
21905
22124
  sock.destroy();
21906
- resolve5(ok);
22125
+ resolve6(ok);
21907
22126
  };
21908
22127
  sock.setTimeout(timeoutMs, () => done(false));
21909
22128
  sock.once("connect", () => done(true));
@@ -21939,35 +22158,50 @@ init_types();
21939
22158
  async function ensureDaemon(transport, opts) {
21940
22159
  const logger = opts.logger ?? silentLogger;
21941
22160
  const startTimeoutMs = opts.startTimeoutMs ?? (opts.attachedFallback === void 0 ? 2e4 : 5e3);
22161
+ let mismatch;
21942
22162
  try {
21943
22163
  const authority = await acquireAuthority(transport);
21944
22164
  return { port: authority.port, authority, bootstrapped: false, daemonLifetime: "persistent" };
21945
22165
  } catch (err) {
21946
- if (err instanceof CliError && err.code === "cli_outdated") throw err;
22166
+ if (err instanceof CliError && err.code === "protocol_mismatch") mismatch = err;
21947
22167
  }
21948
22168
  const inspection = await inspectHost(transport).catch((err) => {
21949
- if (err instanceof CliError) throw err;
22169
+ if (err instanceof CliError || mismatch) throw err;
21950
22170
  return null;
21951
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
+ };
21952
22177
  if (inspection) {
21953
- if (inspection.version.protocol.major > PROTOCOL_VERSION.major) {
21954
- throw new CliError("cli_outdated", "the host uses a newer protocol; update the CLI");
21955
- }
21956
- if (opts.noUpgrade) {
21957
- throw new CliError(
21958
- "upgrade_failed",
21959
- `the daemon speaks protocol ${inspection.version.protocol.major} and --no-upgrade is set`,
21960
- "run puddle launch without --no-upgrade"
21961
- );
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;
21962
22199
  }
21963
- logger.info(
21964
- `updating puddled \u2014 ${inspection.liveSessions} live session(s) will be interrupted and can be resumed`
21965
- );
21966
- } else if (await installedVersion(transport) !== null) {
22200
+ }
22201
+ if (await installedVersion(transport) !== null) {
21967
22202
  logger.info(`restarting puddled on ${transport.label}`);
21968
22203
  }
21969
- await installDaemon(transport, opts);
21970
- return waitAfterInstall(transport, opts, logger, true, startTimeoutMs);
22204
+ return install();
21971
22205
  }
21972
22206
  async function waitAfterInstall(transport, opts, logger, bootstrapped, timeoutMs) {
21973
22207
  try {
@@ -22003,7 +22237,7 @@ async function waitForStartedDaemon(transport, timeoutMs, signal) {
22003
22237
  }
22004
22238
  return { port: authority.port, authority };
22005
22239
  } catch (err) {
22006
- if (err instanceof CliError && err.code === "cli_outdated") throw err;
22240
+ if (err instanceof CliError && err.code === "protocol_mismatch") throw err;
22007
22241
  }
22008
22242
  await sleep(500);
22009
22243
  }
@@ -22016,23 +22250,6 @@ function startTimeout(transport) {
22016
22250
  transport.kind === "ssh" ? `inspect it with: puddle logs ${transport.label}` : "inspect it with: puddle logs"
22017
22251
  );
22018
22252
  }
22019
- function makeUpgrader(transport, client, opts, lease) {
22020
- return async () => {
22021
- await installDaemon(transport, opts);
22022
- await lease?.ensureRunning();
22023
- const fresh = await waitForStartedDaemon(transport, 3e4);
22024
- client.setAuthority(fresh.authority);
22025
- const deadline = Date.now() + 3e4;
22026
- while (Date.now() < deadline) {
22027
- if (await client.responds()) return;
22028
- await sleep(500);
22029
- }
22030
- throw new CliError(
22031
- "daemon_start_timeout",
22032
- `puddled did not come back after updating on ${transport.label}`
22033
- );
22034
- };
22035
- }
22036
22253
 
22037
22254
  // src/lib/attached-daemon.ts
22038
22255
  init_types();
@@ -22095,7 +22312,7 @@ var AttachedDaemon = class {
22095
22312
  }
22096
22313
  async launchAndWait(preferredPort) {
22097
22314
  const portFlag = preferredPort === void 0 ? "" : ` --port ${preferredPort}`;
22098
- const leasePidPath = `${hostPaths.home}/attached-${randomUUID()}.pid`;
22315
+ const leasePidPath = `${hostPaths.home}/attached-${randomUUID2()}.pid`;
22099
22316
  this.leasePidPath = leasePidPath;
22100
22317
  const command = this.ssh.runAttached(
22101
22318
  `printf '%s\\n' "$$" > ${leasePidPath}; exec ${hostPaths.current}/puddled${portFlag} >> ${hostPaths.logs}/puddled.out.log 2>&1`
@@ -22191,10 +22408,6 @@ var DaemonClient = class {
22191
22408
  this.port = port;
22192
22409
  this.authority = authority;
22193
22410
  }
22194
- setAuthority(authority) {
22195
- this.authority.close();
22196
- this.authority = authority;
22197
- }
22198
22411
  setPort(port) {
22199
22412
  this.port = port;
22200
22413
  }
@@ -22236,21 +22449,6 @@ var DaemonClient = class {
22236
22449
  async liveSessions() {
22237
22450
  return (await this.sessions()).filter((s) => LIVE.has(s.status));
22238
22451
  }
22239
- async liveSessionCount() {
22240
- return (await this.sessions()).filter((s) => LIVE.has(s.status)).length;
22241
- }
22242
- /** Readiness requires an authenticated response; rejection never counts as ready. */
22243
- async responds() {
22244
- try {
22245
- const res = await fetch(`http://127.0.0.1:${this.port}/api/version`, {
22246
- headers: { authorization: `Bearer ${this.authority.credential()}` },
22247
- signal: AbortSignal.timeout(2e3)
22248
- });
22249
- return res.ok;
22250
- } catch {
22251
- return false;
22252
- }
22253
- }
22254
22452
  };
22255
22453
  async function readDaemonPort(transport) {
22256
22454
  const runtime = await transport.readFile(hostPaths.runtime);
@@ -22272,51 +22470,8 @@ async function readDaemonPort(transport) {
22272
22470
  return 7434;
22273
22471
  }
22274
22472
 
22275
- // src/lib/handshake.ts
22276
- init_types();
22277
- var CLI_UPGRADE_COMMAND = "npm install -g @puddle-code/cli";
22278
- function decideHandshake(cli, daemon) {
22279
- if (daemon.major === cli.major) return { kind: "proceed" };
22280
- if (daemon.major < cli.major) return { kind: "upgrade-daemon" };
22281
- return { kind: "refuse-cli-outdated", command: CLI_UPGRADE_COMMAND };
22282
- }
22283
- async function runHandshake(opts) {
22284
- const logger = opts.logger ?? silentLogger;
22285
- const daemon = await opts.client.version();
22286
- const decision = decideHandshake(PROTOCOL_VERSION, daemon.protocol);
22287
- if (decision.kind === "proceed") return daemon;
22288
- if (decision.kind === "refuse-cli-outdated") {
22289
- throw new CliError(
22290
- "cli_outdated",
22291
- `the daemon speaks protocol ${daemon.protocol.major}; this CLI speaks ${PROTOCOL_VERSION.major}`,
22292
- `update the CLI: ${decision.command}`
22293
- );
22294
- }
22295
- const liveSessions = await opts.client.liveSessionCount().catch(() => 0);
22296
- if (opts.noUpgrade) {
22297
- throw new CliError(
22298
- "upgrade_failed",
22299
- `the daemon speaks protocol ${daemon.protocol.major} (CLI: ${PROTOCOL_VERSION.major}) and --no-upgrade is set`,
22300
- `re-run without --no-upgrade to update it (${liveSessions} live session(s) would be interrupted and can be resumed)`
22301
- );
22302
- }
22303
- logger.info(
22304
- `updating puddled (protocol ${daemon.protocol.major} \u2192 ${PROTOCOL_VERSION.major}) \u2014 ${liveSessions} live session(s) will be interrupted and can be resumed`
22305
- );
22306
- await opts.upgradeDaemon({ from: daemon.version, liveSessions });
22307
- const after = await opts.client.version();
22308
- if (after.protocol.major !== PROTOCOL_VERSION.major) {
22309
- throw new CliError(
22310
- "upgrade_failed",
22311
- `the daemon still speaks protocol ${after.protocol.major} after updating`,
22312
- "inspect the daemon logs: puddle logs"
22313
- );
22314
- }
22315
- return after;
22316
- }
22317
-
22318
22473
  // src/lib/serve/ui-server.ts
22319
- import { randomUUID as randomUUID2 } from "node:crypto";
22474
+ import { randomUUID as randomUUID3 } from "node:crypto";
22320
22475
  import { createServer as createServer3 } from "node:http";
22321
22476
 
22322
22477
  // src/lib/auth/launcher.ts
@@ -22352,7 +22507,7 @@ async function startLauncher(home, identity, invite) {
22352
22507
  path,
22353
22508
  close: async () => {
22354
22509
  for (const socket of sockets) socket.destroy();
22355
- await new Promise((resolve5) => server.close(() => resolve5()));
22510
+ await new Promise((resolve6) => server.close(() => resolve6()));
22356
22511
  }
22357
22512
  };
22358
22513
  }
@@ -22362,14 +22517,14 @@ async function requestInvitation(home, path) {
22362
22517
  const stored = readPrivateJson(secretFile(home, path));
22363
22518
  pipeSecret = stored.secret;
22364
22519
  } else privatePath(path);
22365
- return new Promise((resolve5, reject) => {
22520
+ return new Promise((resolve6, reject) => {
22366
22521
  const socket = connect5(path);
22367
22522
  socket.setTimeout(5e3, () => socket.destroy(new Error("Launcher did not answer")));
22368
22523
  jsonLines(
22369
22524
  socket,
22370
22525
  (value) => {
22371
22526
  const message = launcherResponseSchema.parse(value);
22372
- resolve5(message.url);
22527
+ resolve6(message.url);
22373
22528
  socket.end();
22374
22529
  },
22375
22530
  () => socket.destroy(new Error("Invalid launcher reply"))
@@ -22589,7 +22744,7 @@ function writeStore(file2, store) {
22589
22744
  renameSync3(tmp, file2);
22590
22745
  }
22591
22746
  function readBody(req) {
22592
- return new Promise((resolve5, reject) => {
22747
+ return new Promise((resolve6, reject) => {
22593
22748
  let size = 0;
22594
22749
  const chunks = [];
22595
22750
  req.on("data", (chunk) => {
@@ -22601,7 +22756,7 @@ function readBody(req) {
22601
22756
  }
22602
22757
  chunks.push(chunk);
22603
22758
  });
22604
- req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
22759
+ req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
22605
22760
  req.on("error", reject);
22606
22761
  });
22607
22762
  }
@@ -23188,7 +23343,7 @@ async function startUiServer(opts) {
23188
23343
  const tracker = new ProxySocketTracker();
23189
23344
  const serveStatic = createStaticHandler(opts.assetsDir);
23190
23345
  const handleRemoteAccess = remoteAccessHandler(opts.remoteAccess);
23191
- const nonce = randomUUID2();
23346
+ const nonce = randomUUID3();
23192
23347
  let origin = "";
23193
23348
  let proxyOrigin = "";
23194
23349
  let browsers;
@@ -23326,8 +23481,8 @@ async function startUiServer(opts) {
23326
23481
  browsers.close();
23327
23482
  bridge.close();
23328
23483
  tracker.destroyAll();
23329
- await new Promise((resolve5) => {
23330
- server.close(() => resolve5());
23484
+ await new Promise((resolve6) => {
23485
+ server.close(() => resolve6());
23331
23486
  server.closeAllConnections();
23332
23487
  });
23333
23488
  }
@@ -23337,15 +23492,15 @@ async function listen(server, startPort, strict, avoidPort) {
23337
23492
  for (let probe = 0; probe < MAX_PORT_PROBES; probe += 1) {
23338
23493
  const port = startPort + probe;
23339
23494
  if (!strict && port === avoidPort) continue;
23340
- const ok = await new Promise((resolve5, rejectListen) => {
23495
+ const ok = await new Promise((resolve6, rejectListen) => {
23341
23496
  const onError = (err) => {
23342
23497
  server.off("listening", onListening);
23343
- if (err.code === "EADDRINUSE") resolve5(false);
23498
+ if (err.code === "EADDRINUSE") resolve6(false);
23344
23499
  else rejectListen(err);
23345
23500
  };
23346
23501
  const onListening = () => {
23347
23502
  server.off("error", onError);
23348
- resolve5(true);
23503
+ resolve6(true);
23349
23504
  };
23350
23505
  server.once("error", onError);
23351
23506
  server.once("listening", onListening);
@@ -23626,7 +23781,12 @@ async function connectRemote(opts) {
23626
23781
  let uiResource;
23627
23782
  let endpointResource;
23628
23783
  try {
23629
- 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
+ };
23630
23790
  const attachedDaemon = new AttachedDaemon(ssh, logger);
23631
23791
  const endpoint = await ensureDaemon(ssh, {
23632
23792
  ...bootstrap,
@@ -23667,12 +23827,7 @@ async function connectRemote(opts) {
23667
23827
  logger
23668
23828
  });
23669
23829
  oauthForwardResource = oauthForward;
23670
- const daemon = await runHandshake({
23671
- client,
23672
- noUpgrade: opts.noUpgrade,
23673
- upgradeDaemon: makeUpgrader(ssh, client, bootstrap, endpoint.lease),
23674
- logger
23675
- });
23830
+ const daemon = verifyDaemonVersion(await client.version());
23676
23831
  const avoidPort = await readDaemonPort(new LocalTransport());
23677
23832
  endpoint.authority.setVerifier(async () => {
23678
23833
  const generation = endpoint.authority.generation;
@@ -23695,7 +23850,7 @@ async function connectRemote(opts) {
23695
23850
  watchTunnel(replacement);
23696
23851
  await previous?.close();
23697
23852
  }
23698
- await client.version();
23853
+ verifyDaemonVersion(await client.version());
23699
23854
  });
23700
23855
  const ui = await startUiServer({
23701
23856
  authority: endpoint.authority,
@@ -23897,24 +24052,26 @@ init_types();
23897
24052
  async function startLocal(opts) {
23898
24053
  const logger = opts.logger ?? silentLogger;
23899
24054
  const transport = new LocalTransport();
23900
- const bootstrap = { tarball: opts.tarball, logger, noUpgrade: opts.noUpgrade };
23901
- 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;
23902
24062
  try {
23903
- const client = new DaemonClient(endpoint.port, endpoint.authority);
23904
- const daemon = await runHandshake({
23905
- client,
23906
- noUpgrade: opts.noUpgrade,
23907
- upgradeDaemon: makeUpgrader(transport, client, bootstrap),
23908
- logger
23909
- });
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());
23910
24067
  let currentUi = void 0;
23911
- endpoint.authority.setVerifier(async () => {
23912
- client.setPort(endpoint.authority.port);
23913
- await client.version();
23914
- 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 });
23915
24072
  });
23916
24073
  const ui = await startUiServer({
23917
- authority: endpoint.authority,
24074
+ authority,
23918
24075
  identity: "local",
23919
24076
  refreshId: opts.refreshId,
23920
24077
  assetsDir: opts.assetsDir,
@@ -23943,12 +24100,12 @@ async function startLocal(opts) {
23943
24100
  },
23944
24101
  async stop() {
23945
24102
  await ui.close();
23946
- endpoint.authority.close();
24103
+ authority.close();
23947
24104
  transport.dispose();
23948
24105
  }
23949
24106
  };
23950
24107
  } catch (err) {
23951
- endpoint.authority.close();
24108
+ endpoint?.authority.close();
23952
24109
  transport.dispose();
23953
24110
  throw err;
23954
24111
  }
@@ -23978,12 +24135,170 @@ async function statusReport(client) {
23978
24135
  // src/cli/run.ts
23979
24136
  init_types();
23980
24137
 
23981
- // src/cli/manage.ts
24138
+ // src/cli/manage-cli.ts
24139
+ init_types();
23982
24140
  import { spawn as spawn7 } from "node:child_process";
23983
- import { existsSync as existsSync5 } from "node:fs";
23984
- 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";
23985
24300
  import { homedir as homedir2 } from "node:os";
23986
- import { join as join15 } from "node:path";
24301
+ import { join as join16 } from "node:path";
23987
24302
 
23988
24303
  // src/lib/releases.ts
23989
24304
  init_types();
@@ -24116,9 +24431,9 @@ async function upgradeDaemon(transport, opts = {}) {
24116
24431
  }
24117
24432
 
24118
24433
  // src/cli/detach.ts
24119
- import { spawn as spawn6 } from "node:child_process";
24120
- import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync9 } from "node:fs";
24121
- 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";
24122
24437
  init_types();
24123
24438
  var COCKPIT_CHILD_ENV = "PUDDLE_COCKPIT_CHILD";
24124
24439
  var READY_TIMEOUT_MS = 3e5;
@@ -24157,12 +24472,12 @@ async function launchDetached(opts) {
24157
24472
  }
24158
24473
  if (target !== "local") await new SshTransport(target).open();
24159
24474
  const logFile = cockpitLogPath(target);
24160
- mkdirSync6(dirname9(logFile), { recursive: true });
24475
+ mkdirSync6(dirname11(logFile), { recursive: true });
24161
24476
  const fd = openSync2(logFile, "w");
24162
24477
  const argv = opts.argv ?? process.argv.slice(2);
24163
- const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
24478
+ const child = spawn8(process.execPath, [process.argv[1] ?? "", ...argv], {
24164
24479
  detached: true,
24165
- stdio: ["ignore", fd, fd],
24480
+ stdio: ["ignore", fd, fd, "ipc"],
24166
24481
  env: { ...process.env, [COCKPIT_CHILD_ENV]: "1" }
24167
24482
  });
24168
24483
  closeSync2(fd);
@@ -24171,7 +24486,13 @@ async function launchDetached(opts) {
24171
24486
  throw new CliError("cockpit_launch_failed", "could not spawn the background cockpit process");
24172
24487
  }
24173
24488
  logger.info(`starting the cockpit for ${target} in the background\u2026`);
24174
- 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
+ }
24175
24496
  if (record2 === null || record2.status !== "ready") {
24176
24497
  if (record2?.status === "error") {
24177
24498
  logger.warn(record2.message ?? "the cockpit failed to start");
@@ -24194,7 +24515,7 @@ async function followStartup(target, childPid, logFile) {
24194
24515
  let offset = 0;
24195
24516
  const relay = () => {
24196
24517
  try {
24197
- const buf = readFileSync9(logFile);
24518
+ const buf = readFileSync10(logFile);
24198
24519
  if (buf.length > offset) {
24199
24520
  process.stderr.write(buf.subarray(offset));
24200
24521
  offset = buf.length;
@@ -24219,7 +24540,7 @@ async function followStartup(target, childPid, logFile) {
24219
24540
  }
24220
24541
  function spawnDetachedRefresh(target, argv, refreshId) {
24221
24542
  const logFile = cockpitLogPath(target);
24222
- mkdirSync6(dirname9(logFile), { recursive: true });
24543
+ mkdirSync6(dirname11(logFile), { recursive: true });
24223
24544
  const fd = openSync2(logFile, "a");
24224
24545
  const env = {
24225
24546
  ...process.env,
@@ -24227,7 +24548,7 @@ function spawnDetachedRefresh(target, argv, refreshId) {
24227
24548
  PUDDLE_INTERNAL_REFRESH: "1"
24228
24549
  };
24229
24550
  delete env[COCKPIT_CHILD_ENV];
24230
- const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
24551
+ const child = spawn8(process.execPath, [process.argv[1] ?? "", ...argv], {
24231
24552
  detached: true,
24232
24553
  stdio: ["ignore", fd, fd],
24233
24554
  env
@@ -24260,41 +24581,6 @@ async function presentInvitation(record2, noBrowser, logger) {
24260
24581
  else openBrowser(url2);
24261
24582
  }
24262
24583
 
24263
- // src/cli/prompt.ts
24264
- init_types();
24265
- import { createInterface as createInterface2 } from "node:readline";
24266
- async function ask(question, def) {
24267
- if (!process.stdin.isTTY || !process.stdout.isTTY) return def;
24268
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
24269
- try {
24270
- const answer = await new Promise(
24271
- (resolve5) => rl.question(`${question} [${def}] `, resolve5)
24272
- );
24273
- return answer.trim() === "" ? def : answer.trim();
24274
- } finally {
24275
- rl.close();
24276
- }
24277
- }
24278
- async function confirm(question, opts = {}) {
24279
- if (opts.skip === true) return true;
24280
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
24281
- throw new CliError(
24282
- "bad_arguments",
24283
- "refusing to confirm without a terminal",
24284
- "pass --yes to proceed non-interactively"
24285
- );
24286
- }
24287
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
24288
- try {
24289
- const answer = await new Promise(
24290
- (resolve5) => rl.question(`${question} [y/N] `, resolve5)
24291
- );
24292
- return /^y(es)?$/i.test(answer.trim());
24293
- } finally {
24294
- rl.close();
24295
- }
24296
- }
24297
-
24298
24584
  // src/cli/manage.ts
24299
24585
  function assertLocal(what, host, verb) {
24300
24586
  if (host === void 0 || host === "local") return;
@@ -24403,31 +24689,6 @@ async function runUpgrade(cmd, logger) {
24403
24689
  else if (process.platform === "darwin") logger.info("no desktop app installed \u2014 skipping");
24404
24690
  return upgradeCli(cmd.version, logger);
24405
24691
  }
24406
- function upgradeCli(version2, logger) {
24407
- const spec = `@puddle-code/cli@${version2 ?? "latest"}`;
24408
- logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for ${version2 ?? "the latest release"}`);
24409
- return new Promise((resolve5, reject) => {
24410
- const child = spawn7("npm", ["install", "-g", spec], { stdio: "inherit" });
24411
- child.on(
24412
- "error",
24413
- () => reject(
24414
- new CliError(
24415
- "not_installed",
24416
- "npm is not on PATH",
24417
- `the CLI is installed and upgraded via npm: npm install -g ${spec}`
24418
- )
24419
- )
24420
- );
24421
- child.on("exit", (code) => {
24422
- if (code === 0) {
24423
- logger.info("done \u2014 `puddle --version` shows the installed version");
24424
- resolve5(0);
24425
- } else {
24426
- reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
24427
- }
24428
- });
24429
- });
24430
- }
24431
24692
  async function setDesktop(opts, logger) {
24432
24693
  if (process.platform === "linux") {
24433
24694
  if (opts.verb !== "install") {
@@ -24499,22 +24760,22 @@ async function installDesktopAppImage(version2, logger) {
24499
24760
  );
24500
24761
  }
24501
24762
  const dir = expandHome2(await ask("Where should the AppImage be stored?", "~/puddle"));
24502
- const target = join15(dir, "Puddle.AppImage");
24503
- if (existsSync5(target) && version2 === void 0) {
24763
+ const target = join16(dir, "Puddle.AppImage");
24764
+ if (existsSync6(target) && version2 === void 0) {
24504
24765
  logger.info(`${target} already exists \u2014 the app updates itself in-app (its update toast)`);
24505
24766
  return 0;
24506
24767
  }
24507
24768
  logger.info(`installing Puddle ${update.version} to ${target}`);
24508
24769
  const staged2 = await stageDesktopUpdate(update, { logger });
24509
- await mkdir2(dir, { recursive: true });
24510
- await rm2(target, { force: true });
24770
+ await mkdir3(dir, { recursive: true });
24771
+ await rm3(target, { force: true });
24511
24772
  try {
24512
24773
  await rename(staged2.stagedPath, target);
24513
24774
  } catch {
24514
24775
  await copyFile(staged2.stagedPath, target);
24515
24776
  await chmod2(target, 493);
24516
24777
  }
24517
- await rm2(staged2.dir, { recursive: true, force: true });
24778
+ await rm3(staged2.dir, { recursive: true, force: true });
24518
24779
  const protocol = componentProtocolForVersion(update.version);
24519
24780
  recordDesktopInstallation({
24520
24781
  path: target,
@@ -24522,14 +24783,14 @@ async function installDesktopAppImage(version2, logger) {
24522
24783
  ...protocol ? { protocol } : {}
24523
24784
  });
24524
24785
  logger.info(`installed \u2014 run it directly (${target}) or add it to your launcher`);
24525
- const opener = spawn7("xdg-open", [dir], { stdio: "ignore", detached: true });
24786
+ const opener = spawn9("xdg-open", [dir], { stdio: "ignore", detached: true });
24526
24787
  opener.on("error", () => void 0);
24527
24788
  opener.unref();
24528
24789
  return 0;
24529
24790
  }
24530
24791
  function expandHome2(path) {
24531
24792
  if (path === "~") return homedir2();
24532
- return path.startsWith("~/") ? join15(homedir2(), path.slice(2)) : path;
24793
+ return path.startsWith("~/") ? join16(homedir2(), path.slice(2)) : path;
24533
24794
  }
24534
24795
  async function runRemove(cmd, logger) {
24535
24796
  switch (cmd.what) {
@@ -24543,40 +24804,6 @@ async function runRemove(cmd, logger) {
24543
24804
  return removeDaemonCmd(cmd, logger);
24544
24805
  }
24545
24806
  }
24546
- async function removeCli(yes, logger) {
24547
- const managed = await new Promise((resolve5) => {
24548
- const child = spawn7("npm", ["ls", "-g", "@puddle-code/cli", "--depth=0"], { stdio: "ignore" });
24549
- child.on("error", () => resolve5(false));
24550
- child.on("exit", (code) => resolve5(code === 0));
24551
- });
24552
- if (!managed) {
24553
- throw new CliError(
24554
- "not_installed",
24555
- "the puddle CLI is not an npm global install on this machine",
24556
- "a repo checkout or linked dev build is removed the way it was installed"
24557
- );
24558
- }
24559
- const proceed = await confirm(
24560
- `Remove the puddle CLI (npm uninstall -g @puddle-code/cli)? Running cockpits keep running until killed; daemons and their sessions are untouched.`,
24561
- { skip: yes }
24562
- );
24563
- if (!proceed) {
24564
- logger.info("nothing removed");
24565
- return 0;
24566
- }
24567
- return new Promise((resolve5, reject) => {
24568
- const child = spawn7("npm", ["uninstall", "-g", "@puddle-code/cli"], { stdio: "inherit" });
24569
- child.on("error", () => reject(new CliError("not_installed", "npm is not on PATH")));
24570
- child.on("exit", (code) => {
24571
- if (code === 0) {
24572
- logger.info("removed \u2014 daemons keep running (puddle remove daemon uninstalls one)");
24573
- resolve5(0);
24574
- } else {
24575
- reject(new CliError("not_installed", `npm uninstall exited with ${code ?? "a signal"}`));
24576
- }
24577
- });
24578
- });
24579
- }
24580
24807
  async function removeDesktop(yes, logger) {
24581
24808
  if (process.platform !== "darwin") {
24582
24809
  throw new CliError(
@@ -24601,7 +24828,7 @@ async function removeDesktop(yes, logger) {
24601
24828
  logger.info("nothing removed");
24602
24829
  return 0;
24603
24830
  }
24604
- await rm2(installed.appPath, { recursive: true, force: true });
24831
+ await rm3(installed.appPath, { recursive: true, force: true });
24605
24832
  await pruneDesktopUpdateCache([]);
24606
24833
  logger.info(`deleted ${installed.appPath} (recent hosts kept in ~/.puddle/recent-hosts.json)`);
24607
24834
  return 0;
@@ -24723,9 +24950,9 @@ function timestampedLogger() {
24723
24950
 
24724
24951
  // src/cli/run.ts
24725
24952
  function assetsDir() {
24726
- const here = dirname10(fileURLToPath4(import.meta.url));
24727
- for (const candidate of [join16(here, "public"), join16(here, "..", "..", "dist", "public")]) {
24728
- 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;
24729
24956
  }
24730
24957
  throw new CliError(
24731
24958
  "not_installed",
@@ -24759,7 +24986,7 @@ async function openTarget(host) {
24759
24986
  };
24760
24987
  }
24761
24988
  function runUntilInterrupted(stop, farewell) {
24762
- return new Promise((resolve5) => {
24989
+ return new Promise((resolve6) => {
24763
24990
  let stopping = false;
24764
24991
  process.on("SIGINT", () => {
24765
24992
  if (stopping) process.exit(130);
@@ -24767,10 +24994,10 @@ function runUntilInterrupted(stop, farewell) {
24767
24994
  process.stderr.write(`
24768
24995
  ${farewell}
24769
24996
  `);
24770
- void stop().then(resolve5);
24997
+ void stop().then(resolve6);
24771
24998
  });
24772
24999
  process.on("SIGTERM", () => {
24773
- void stop().then(resolve5);
25000
+ void stop().then(resolve6);
24774
25001
  });
24775
25002
  });
24776
25003
  }
@@ -25058,6 +25285,7 @@ async function runCockpit(command, target, terminal) {
25058
25285
  const common = {
25059
25286
  ...command,
25060
25287
  assetsDir: assetsDir(),
25288
+ confirmDaemonUpgrade,
25061
25289
  logger,
25062
25290
  onRefreshRequest,
25063
25291
  refreshId: process.env.PUDDLE_REFRESH_ID