@rubytech/create-maxy 1.0.647 → 1.0.649

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 (150) hide show
  1. package/dist/index.js +76 -3
  2. package/package.json +1 -1
  3. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +1 -11
  4. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -2
  5. package/payload/platform/plugins/cloudflare/scripts/_cdp-authorize.mjs +274 -0
  6. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +69 -0
  7. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +401 -0
  8. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +73 -3
  9. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +4 -2
  10. package/payload/platform/plugins/docs/references/cloudflare.md +1 -0
  11. package/payload/platform/plugins/docs/references/deployment.md +11 -0
  12. package/payload/platform/plugins/docs/references/getting-started.md +2 -0
  13. package/payload/platform/plugins/docs/references/memory-guide.md +2 -2
  14. package/payload/platform/plugins/docs/references/platform.md +6 -0
  15. package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
  16. package/payload/platform/plugins/memory/references/graph-primitives.md +5 -5
  17. package/payload/platform/templates/dotfiles/.tmux.conf +1 -0
  18. package/payload/platform/templates/systemd/maxy-ttyd.service +20 -0
  19. package/payload/server/public/assets/admin-BtjYSo0M.js +362 -0
  20. package/payload/server/public/assets/admin-kHJ-D0s7.css +1 -0
  21. package/payload/server/public/assets/{arc-DcrodP5U.js → arc-BMhgytDB.js} +1 -1
  22. package/payload/server/public/assets/architecture-YZFGNWBL-S9-oeq_x.js +1 -0
  23. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Cvo_8X6C.js → architectureDiagram-Q4EWVU46-BePoi8XC.js} +1 -1
  24. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CEK06TEn.js → blockDiagram-DXYQGD6D-BkiwLTtq.js} +1 -1
  25. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-QjYUSwTU.js → c4Diagram-AHTNJAMY-bpjPj2Ln.js} +1 -1
  26. package/payload/server/public/assets/channel-D3U0_a1j.js +1 -0
  27. package/payload/server/public/assets/{chunk-2KRD3SAO-BBifNfFc.js → chunk-2KRD3SAO-ZcHg_orY.js} +1 -1
  28. package/payload/server/public/assets/{chunk-336JU56O-B-N_zWuf.js → chunk-336JU56O-BpATJiGl.js} +2 -2
  29. package/payload/server/public/assets/chunk-426QAEUC-Wz6Bpsil.js +1 -0
  30. package/payload/server/public/assets/{chunk-4BX2VUAB-CtDQKj9B.js → chunk-4BX2VUAB-zJekz2NU.js} +1 -1
  31. package/payload/server/public/assets/{chunk-4TB4RGXK-Dnu9n3p1.js → chunk-4TB4RGXK-CLXL19Wd.js} +1 -1
  32. package/payload/server/public/assets/{chunk-55IACEB6-DSLoJJSj.js → chunk-55IACEB6-CzqB8aoU.js} +1 -1
  33. package/payload/server/public/assets/{chunk-5FUZZQ4R-rx-IvMNE.js → chunk-5FUZZQ4R-BoTfWHuW.js} +1 -1
  34. package/payload/server/public/assets/{chunk-5PVQY5BW-B9w9AKCS.js → chunk-5PVQY5BW-RhIfPCRB.js} +1 -1
  35. package/payload/server/public/assets/{chunk-67CJDMHE-C1yEjtiu.js → chunk-67CJDMHE-mM1sFmlz.js} +1 -1
  36. package/payload/server/public/assets/{chunk-7N4EOEYR-C2f3zeVH.js → chunk-7N4EOEYR-GUck0jv1.js} +1 -1
  37. package/payload/server/public/assets/{chunk-AA7GKIK3-B_U-NsDK.js → chunk-AA7GKIK3-BYhfUc1V.js} +1 -1
  38. package/payload/server/public/assets/{chunk-BSJP7CBP-DM6_wafW.js → chunk-BSJP7CBP-CTsYuARh.js} +1 -1
  39. package/payload/server/public/assets/{chunk-CIAEETIT-DG7WkfNj.js → chunk-CIAEETIT-CGsGmUze.js} +1 -1
  40. package/payload/server/public/assets/{chunk-EDXVE4YY-aS3_rdwQ.js → chunk-EDXVE4YY-utELKGQK.js} +1 -1
  41. package/payload/server/public/assets/{chunk-ENJZ2VHE-r1I0uoCf.js → chunk-ENJZ2VHE-CNHjq5xK.js} +1 -1
  42. package/payload/server/public/assets/{chunk-FMBD7UC4-CTm3YRE5.js → chunk-FMBD7UC4-DaRrfk3s.js} +1 -1
  43. package/payload/server/public/assets/{chunk-FOC6F5B3-CAIttx3K.js → chunk-FOC6F5B3-BaeLcJVt.js} +1 -1
  44. package/payload/server/public/assets/{chunk-ICPOFSXX-DQFV4c1l.js → chunk-ICPOFSXX-Di63NBur.js} +2 -2
  45. package/payload/server/public/assets/{chunk-K5T4RW27-B2WCPQBa.js → chunk-K5T4RW27-CTTOezMH.js} +1 -1
  46. package/payload/server/public/assets/{chunk-KGLVRYIC-C6w2sUOF.js → chunk-KGLVRYIC-DCkohKP2.js} +1 -1
  47. package/payload/server/public/assets/{chunk-LIHQZDEY-BT1hcDTK.js → chunk-LIHQZDEY-osQO30uB.js} +1 -1
  48. package/payload/server/public/assets/{chunk-ORNJ4GCN-Brl32BSe.js → chunk-ORNJ4GCN-DoLajOOe.js} +1 -1
  49. package/payload/server/public/assets/{chunk-OYMX7WX6-BCO6n1CX.js → chunk-OYMX7WX6-BSPzqyxs.js} +1 -1
  50. package/payload/server/public/assets/chunk-QZHKN3VN-BAQp1OEl.js +1 -0
  51. package/payload/server/public/assets/{chunk-U2HBQHQK-BvgC0fpb.js → chunk-U2HBQHQK-BZnA7c4T.js} +1 -1
  52. package/payload/server/public/assets/{chunk-X2U36JSP-3yLdcqYf.js → chunk-X2U36JSP-DpQ2OA_c.js} +1 -1
  53. package/payload/server/public/assets/{chunk-XPW4576I-D876RWxK.js → chunk-XPW4576I-BccP1mlQ.js} +1 -1
  54. package/payload/server/public/assets/{chunk-YZCP3GAM-CiuA4hOC.js → chunk-YZCP3GAM-BAkNXu0G.js} +1 -1
  55. package/payload/server/public/assets/{chunk-ZZ45TVLE-COjEBPzv.js → chunk-ZZ45TVLE-DBSm41oP.js} +1 -1
  56. package/payload/server/public/assets/classDiagram-6PBFFD2Q-6EGGLDD_.js +1 -0
  57. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfAV4tgE.js +1 -0
  58. package/payload/server/public/assets/clone-BoV8noAi.js +1 -0
  59. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-Dap0yL0o.js → cose-bilkent-S5V4N54A-Boeb8aWs.js} +1 -1
  60. package/payload/server/public/assets/{dagre-KV5264BT-DEia9UJj.js → dagre-KV5264BT-BkvWofSp.js} +1 -1
  61. package/payload/server/public/assets/{dagre-DBbjK-Cf.js → dagre-nvPNAunb.js} +1 -1
  62. package/payload/server/public/assets/data-gtcFdXVv.js +1 -0
  63. package/payload/server/public/assets/{diagram-5BDNPKRD-CWSP9MzJ.js → diagram-5BDNPKRD-CMEgyt4E.js} +1 -1
  64. package/payload/server/public/assets/{diagram-G4DWMVQ6-DWRsfitL.js → diagram-G4DWMVQ6-ChorrAF0.js} +1 -1
  65. package/payload/server/public/assets/{diagram-MMDJMWI5-n-jyzS4D.js → diagram-MMDJMWI5-D_iD27po.js} +1 -1
  66. package/payload/server/public/assets/{diagram-TYMM5635-CLPTbfLq.js → diagram-TYMM5635-8qXI1ioG.js} +1 -1
  67. package/payload/server/public/assets/{dist-Tkw8EOuG.js → dist-CrzV1W3-.js} +1 -1
  68. package/payload/server/public/assets/{erDiagram-SMLLAGMA-CmPC9Cnc.js → erDiagram-SMLLAGMA-BFjtKDSB.js} +1 -1
  69. package/payload/server/public/assets/file-DRa7iPfT.js +1 -0
  70. package/payload/server/public/assets/{flatten-Db2kUB5j.js → flatten-ya0TqRLc.js} +1 -1
  71. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DKMNmUbX.js → flowDiagram-DWJPFMVM-Bpd7IL9l.js} +1 -1
  72. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-C5-y3w-l.js → ganttDiagram-T4ZO3ILL-CwOozU85.js} +1 -1
  73. package/payload/server/public/assets/gitGraph-7Q5UKJZL-BOC4CldZ.js +1 -0
  74. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-D9hG5kTg.js → gitGraphDiagram-UUTBAWPF-CcPILiC9.js} +1 -1
  75. package/payload/server/public/assets/graph-ydtwufZX.js +49 -0
  76. package/payload/server/public/assets/{graphlib-StP6GUhM.js → graphlib-B_mcXEVr.js} +1 -1
  77. package/payload/server/public/assets/house-DEu5yOIQ.js +1 -0
  78. package/payload/server/public/assets/info-OMHHGYJF-BSCPTUIx.js +1 -0
  79. package/payload/server/public/assets/infoDiagram-42DDH7IO-T2sn--WJ.js +2 -0
  80. package/payload/server/public/assets/{isEmpty-CXH_nKTs.js → isEmpty-h-wRi_o9.js} +1 -1
  81. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A--K4KOS61.js → ishikawaDiagram-UXIWVN3A-DOP9-Q8H.js} +1 -1
  82. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DE-28YrW.js → journeyDiagram-VCZTEJTY-DGATg0WC.js} +1 -1
  83. package/payload/server/public/assets/{jsx-runtime-DwoXvzmf.js → jsx-runtime-BrB6Lw5Y.js} +1 -1
  84. package/payload/server/public/assets/{jsx-runtime-BQmd8XDE.css → jsx-runtime-C8kAju5f.css} +1 -1
  85. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CxSHjau2.js → kanban-definition-6JOO6SKY-C5PigmKg.js} +1 -1
  86. package/payload/server/public/assets/{line-DbcqYIG0.js → line-DlKKhwkO.js} +1 -1
  87. package/payload/server/public/assets/{linear-DXHoZSN3.js → linear-DD4JiB1l.js} +1 -1
  88. package/payload/server/public/assets/{mermaid-parser.core-CNGUA13J.js → mermaid-parser.core-C8xGCa9p.js} +2 -2
  89. package/payload/server/public/assets/{mermaid.core-IQgx_upQ.js → mermaid.core-CCUSwZB_.js} +3 -3
  90. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-BT9Up6-C.js → mindmap-definition-QFDTVHPH-75k-IVhC.js} +1 -1
  91. package/payload/server/public/assets/{ordinal-CN3oz6oW.js → ordinal-Dwxksj1B.js} +1 -1
  92. package/payload/server/public/assets/packet-4T2RLAQJ-pBa_ZhNI.js +1 -0
  93. package/payload/server/public/assets/pie-ZZUOXDRM-BzYOyiMb.js +1 -0
  94. package/payload/server/public/assets/{pieDiagram-DEJITSTG-BuewQTi6.js → pieDiagram-DEJITSTG-DN5RsDwZ.js} +1 -1
  95. package/payload/server/public/assets/public-D0ymcICW.js +5 -0
  96. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-Cu8y2zQL.js → quadrantDiagram-34T5L4WZ-Sd9x6pNe.js} +1 -1
  97. package/payload/server/public/assets/radar-PYXPWWZC-CTVOaAq6.js +1 -0
  98. package/payload/server/public/assets/{reduce-SDh8_UdG.js → reduce-BUuWaDl2.js} +1 -1
  99. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DiT9bo27.js → requirementDiagram-MS252O5E-BDgifYzj.js} +1 -1
  100. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-EkRnGTxM.js → sankeyDiagram-XADWPNL6-BX9VULNJ.js} +1 -1
  101. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BUiQA3SI.js → sequenceDiagram-FGHM5R23-z3vMxhgE.js} +1 -1
  102. package/payload/server/public/assets/share-2-Cnd_9DYU.js +1 -0
  103. package/payload/server/public/assets/{src-DQQCRlaQ.js → src-Bo15iQ7w.js} +1 -1
  104. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-Fij35Tic.js → stateDiagram-FHFEXIEX-DlP0hBxF.js} +1 -1
  105. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DSddQStC.js +1 -0
  106. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BMsyr7wU.js → timeline-definition-GMOUNBTQ-DwQbhKCo.js} +1 -1
  107. package/payload/server/public/assets/treeView-SZITEDCU-OTnF4Qzw.js +1 -0
  108. package/payload/server/public/assets/treemap-W4RFUUIX-DlIRmHFb.js +1 -0
  109. package/payload/server/public/assets/{useVoiceRecorder-C0Fvv_Bt.js → useVoiceRecorder-CFIpzGaB.js} +3 -3
  110. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-6fegYFB3.js → vennDiagram-DHZGUBPP-WTqmZWWa.js} +1 -1
  111. package/payload/server/public/assets/wardley-RL74JXVD-DwMXAC4U.js +1 -0
  112. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BtJ_B35h.js → wardleyDiagram-NUSXRM2D-BUY50x5T.js} +1 -1
  113. package/payload/server/public/assets/x-DitohdYO.js +1 -0
  114. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DJ20B4NY.js → xychartDiagram-5P7HB3ND-Btdq-fDj.js} +1 -1
  115. package/payload/server/public/data.html +7 -5
  116. package/payload/server/public/graph.html +19 -0
  117. package/payload/server/public/index.html +10 -7
  118. package/payload/server/public/public.html +6 -6
  119. package/payload/server/server.js +965 -664
  120. package/payload/server/public/assets/admin-ITLuG4BN.js +0 -352
  121. package/payload/server/public/assets/architecture-YZFGNWBL-C38eyeNF.js +0 -1
  122. package/payload/server/public/assets/channel-D0dIwjlN.js +0 -1
  123. package/payload/server/public/assets/chunk-426QAEUC-C8oXXITm.js +0 -1
  124. package/payload/server/public/assets/chunk-QZHKN3VN-BE_lylks.js +0 -1
  125. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DH37CWIF.js +0 -1
  126. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DNJ7bv8r.js +0 -1
  127. package/payload/server/public/assets/clone-rrGuX3ZR.js +0 -1
  128. package/payload/server/public/assets/data-D8kol1ed.js +0 -1
  129. package/payload/server/public/assets/gitGraph-7Q5UKJZL-CCjgA3FG.js +0 -1
  130. package/payload/server/public/assets/info-OMHHGYJF-B65K6dQJ.js +0 -1
  131. package/payload/server/public/assets/infoDiagram-42DDH7IO-DUJfTICr.js +0 -2
  132. package/payload/server/public/assets/packet-4T2RLAQJ-fp5ishAK.js +0 -1
  133. package/payload/server/public/assets/pie-ZZUOXDRM-Bc3VMuuU.js +0 -1
  134. package/payload/server/public/assets/public-CWuf8cLU.js +0 -5
  135. package/payload/server/public/assets/radar-PYXPWWZC-D9jy5QAa.js +0 -1
  136. package/payload/server/public/assets/share-2-wGga_ldi.js +0 -1
  137. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DQzhSd8K.js +0 -1
  138. package/payload/server/public/assets/treeView-SZITEDCU-CHyRL9e4.js +0 -1
  139. package/payload/server/public/assets/treemap-W4RFUUIX-DpQ_FOO6.js +0 -1
  140. package/payload/server/public/assets/wardley-RL74JXVD-CWBIAatW.js +0 -1
  141. /package/payload/server/public/assets/{_baseFor-D71p92tl.js → _baseFor-Dn4GSmI6.js} +0 -0
  142. /package/payload/server/public/assets/{array-Bs_owIvv.js → array-DJN9YAVf.js} +0 -0
  143. /package/payload/server/public/assets/{chunk-lgnzUk6H.js → chunk-DD-I1_y5.js} +0 -0
  144. /package/payload/server/public/assets/{cytoscape.esm-DLG5qhup.js → cytoscape.esm-BcJTl1re.js} +0 -0
  145. /package/payload/server/public/assets/{defaultLocale-Du_2bjyv.js → defaultLocale-B4F_XsBB.js} +0 -0
  146. /package/payload/server/public/assets/{init-BYLBkHX_.js → init-DX0Y1qU4.js} +0 -0
  147. /package/payload/server/public/assets/{katex-lkho_UhZ.js → katex-CjHJ1D7d.js} +0 -0
  148. /package/payload/server/public/assets/{path-BO54iFkf.js → path-7vUsG-o2.js} +0 -0
  149. /package/payload/server/public/assets/{preload-helper-DWTEM3RW.js → preload-helper-qlgyTAkD.js} +0 -0
  150. /package/payload/server/public/assets/{rough.esm-BCiZEpQC.js → rough.esm-NLRoWnq-.js} +0 -0
package/dist/index.js CHANGED
@@ -208,7 +208,7 @@ function canSudo() {
208
208
  // ---------------------------------------------------------------------------
209
209
  // Installation steps
210
210
  // ---------------------------------------------------------------------------
211
- const TOTAL = "11";
211
+ const TOTAL = "12";
212
212
  function installSystemDeps() {
213
213
  log("1", TOTAL, "System dependencies and network...");
214
214
  if (!isLinux()) {
@@ -220,6 +220,11 @@ function installSystemDeps() {
220
220
  shell("apt-get", ["install", "-y", "curl", "git", "unzip", "jq", "avahi-daemon", "avahi-utils", "poppler-utils", "ffmpeg"], { sudo: true });
221
221
  shell("apt-get", ["install", "-y", "tigervnc-standalone-server", "python3-websockify", "novnc", "xdg-utils", "chromium"], { sudo: true });
222
222
  shell("apt-get", ["install", "-y", "hostapd", "dnsmasq"], { sudo: true });
223
+ // ttyd + tmux power the admin terminal surface (Task 591) — ttyd serves a
224
+ // WebSocket PTY on 127.0.0.1:7681, tmux provides the persistent named
225
+ // session that outlives admin-server restarts. Both are in Debian/Raspbian
226
+ // repos; no third-party source needed.
227
+ shell("apt-get", ["install", "-y", "ttyd", "tmux"], { sudo: true });
223
228
  }
224
229
  else {
225
230
  console.log(" Skipping apt-get (sudo unavailable non-interactively — deps assumed present from prior install)");
@@ -1400,9 +1405,17 @@ function createTunnelSymlink(linkPath, target) {
1400
1405
  function installTunnelScripts() {
1401
1406
  const setupSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/setup-tunnel.sh");
1402
1407
  const resetSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/reset-tunnel.sh");
1408
+ const listSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/list-cf-domains.sh");
1409
+ // _cdp-authorize.mjs is invoked by setup-tunnel.sh via `node <path>` and
1410
+ // resolved via readlink -f → same dir as the script. It does NOT get a
1411
+ // $HOME symlink — it's a helper, not a top-level operator command. We do
1412
+ // chmod +x defensively so `ls -l` and any ad-hoc `~/setup-tunnel.sh` copy
1413
+ // flow sees it as executable (Task 588).
1414
+ const cdpAuthorizeSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize.mjs");
1403
1415
  const setupLink = resolve(process.env.HOME ?? "/root", "setup-tunnel.sh");
1404
1416
  const resetLink = resolve(process.env.HOME ?? "/root", "reset-tunnel.sh");
1405
- for (const src of [setupSrc, resetSrc]) {
1417
+ const listLink = resolve(process.env.HOME ?? "/root", "list-cf-domains.sh");
1418
+ for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc]) {
1406
1419
  try {
1407
1420
  chmodSync(src, 0o755);
1408
1421
  }
@@ -1415,6 +1428,7 @@ function installTunnelScripts() {
1415
1428
  }
1416
1429
  createTunnelSymlink(setupLink, setupSrc);
1417
1430
  createTunnelSymlink(resetLink, resetSrc);
1431
+ createTunnelSymlink(listLink, listSrc);
1418
1432
  }
1419
1433
  // ---------------------------------------------------------------------------
1420
1434
  // Cron Registration
@@ -1490,8 +1504,65 @@ function installCrons() {
1490
1504
  logFile(` crontab write failed: ${write.stderr}`);
1491
1505
  }
1492
1506
  }
1507
+ function installTerminalService() {
1508
+ log("11", TOTAL, "Installing admin terminal service (ttyd + tmux)...");
1509
+ if (!isLinux()) {
1510
+ console.log(" Skipping admin terminal service (not Linux). On macOS start manually:");
1511
+ console.log(" brew install ttyd tmux && ttyd -p 7681 -i 127.0.0.1 -W tmux new-session -A -s maxy-pty");
1512
+ return;
1513
+ }
1514
+ // Default ~/.tmux.conf — only written if the operator doesn't already have
1515
+ // one. `history-limit 50000` is load-bearing: a closed-tab + reopen during
1516
+ // an upgrade must show every line the operator missed in scrollback.
1517
+ const homeDir = process.env.HOME ?? "/root";
1518
+ const tmuxConfDest = resolve(homeDir, ".tmux.conf");
1519
+ if (!existsSync(tmuxConfDest)) {
1520
+ const tmuxConfTemplate = resolve(INSTALL_DIR, "platform/templates/dotfiles/.tmux.conf");
1521
+ try {
1522
+ if (existsSync(tmuxConfTemplate)) {
1523
+ writeFileSync(tmuxConfDest, readFileSync(tmuxConfTemplate, "utf-8"));
1524
+ console.log(` Wrote default ~/.tmux.conf (history-limit 50000)`);
1525
+ }
1526
+ else {
1527
+ // Fallback if the template was not in the payload for any reason —
1528
+ // preserves the load-bearing scrollback-size guarantee.
1529
+ writeFileSync(tmuxConfDest, "set -g history-limit 50000\n");
1530
+ console.log(` Wrote default ~/.tmux.conf (fallback — template missing)`);
1531
+ }
1532
+ }
1533
+ catch (err) {
1534
+ console.error(` WARNING: failed to write ~/.tmux.conf: ${err instanceof Error ? err.message : String(err)}`);
1535
+ }
1536
+ }
1537
+ // Install and enable the maxy-ttyd.service --user unit. Independent of
1538
+ // BRAND.serviceName — a single device runs one admin terminal regardless of
1539
+ // brand, because the unit binds to 127.0.0.1:7681 which only one process can
1540
+ // hold anyway. On a multi-brand device, the first brand's install writes the
1541
+ // unit and every subsequent install is a no-op (idempotent overwrite).
1542
+ const systemdUserDir = resolve(homeDir, ".config/systemd/user");
1543
+ mkdirSync(systemdUserDir, { recursive: true });
1544
+ const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-ttyd.service");
1545
+ const ttydUnitDest = join(systemdUserDir, "maxy-ttyd.service");
1546
+ try {
1547
+ if (existsSync(ttydUnitTemplate)) {
1548
+ writeFileSync(ttydUnitDest, readFileSync(ttydUnitTemplate, "utf-8"));
1549
+ }
1550
+ else {
1551
+ console.error(` WARNING: maxy-ttyd.service template missing at ${ttydUnitTemplate} — admin terminal will not work`);
1552
+ return;
1553
+ }
1554
+ }
1555
+ catch (err) {
1556
+ console.error(` WARNING: failed to write ${ttydUnitDest}: ${err instanceof Error ? err.message : String(err)}`);
1557
+ return;
1558
+ }
1559
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
1560
+ spawnSync("systemctl", ["--user", "enable", "maxy-ttyd"], { stdio: "inherit" });
1561
+ spawnSync("systemctl", ["--user", "restart", "maxy-ttyd"], { stdio: "inherit" });
1562
+ console.log(" maxy-ttyd.service enabled — admin terminal available on 127.0.0.1:7681");
1563
+ }
1493
1564
  function installService() {
1494
- log("11", TOTAL, `Starting ${BRAND.productName}...`);
1565
+ log("12", TOTAL, `Starting ${BRAND.productName}...`);
1495
1566
  if (!isLinux()) {
1496
1567
  console.log(" Skipping systemd service (not Linux). Start manually with:");
1497
1568
  console.log(` cd ${INSTALL_DIR}/server && MAXY_PLATFORM_ROOT=${INSTALL_DIR}/platform PORT=${PORT} HOSTNAME=0.0.0.0 KEEP_ALIVE_TIMEOUT=61000 node --require ./server-init.cjs server.js`);
@@ -1716,6 +1787,7 @@ WantedBy=multi-user.target
1716
1787
  for (const linkPath of [
1717
1788
  resolve(process.env.HOME ?? "/root", "setup-tunnel.sh"),
1718
1789
  resolve(process.env.HOME ?? "/root", "reset-tunnel.sh"),
1790
+ resolve(process.env.HOME ?? "/root", "list-cf-domains.sh"),
1719
1791
  ]) {
1720
1792
  try {
1721
1793
  accessSync(linkPath, fsConstants.X_OK);
@@ -2032,6 +2104,7 @@ try {
2032
2104
  setupVncViewer();
2033
2105
  setupAccount();
2034
2106
  installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
2107
+ installTerminalService(); // Task 591: ttyd + tmux systemd --user unit (sibling of maxy-ui)
2035
2108
  installService();
2036
2109
  console.log("");
2037
2110
  console.log("================================================================");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.647",
3
+ "version": "1.0.649",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -129,22 +129,12 @@ If the user skips, let them know they can set up Cloudflare at any time by askin
129
129
 
130
130
  If the user wants to proceed, first confirm the domain state in one sentence: "Is your domain already on a Cloudflare account?" If not, quote the click-path from `plugins/cloudflare/references/dashboard-guide.md` § "Add a domain to a Cloudflare account" and wait for the user to confirm the zone is **Active**.
131
131
 
132
- Then call `render-component` with `name: "cloudflare-setup-form"` and data containing the domain option lists for this brand. The form collects admin label + admin domain + optional public label + public domain + optional apex + admin password in one submission, then the submit handler runs `setRemotePassword`, `~/setup-tunnel.sh`, and alias-domain writes server-side and relays the script's output — including any `ACTION REQUIRED` block — back as the component's submitted payload.
133
-
134
- Data shape (select the option lists by brand — read `brand.json` first to confirm which brand is installed):
135
-
136
- - **Maxy brand**: `adminDomainOptions: ["maxy.bot"]`, `publicDomainOptions: ["maxy.bot", "maxy.chat"]`, `apexDomainOptions: ["maxy.chat"]`.
137
- - **Real Agent brand**: `adminDomainOptions: ["realagent.network"]`, `publicDomainOptions: ["realagent.network"]`, `apexDomainOptions: []`.
132
+ Then call `render-component` with `name: "cloudflare-setup-form"` and data containing only the form copy — the form self-discovers the admin/public/apex domain options from the operator's logged-in Cloudflare dashboard (Task 589) via `/api/admin/cloudflare/domains`. Do not read `brand.json` for domain options; brand identity has no authority over what zones the operator's Cloudflare account holds. The form collects admin label + admin domain + optional public label + public domain + optional apex + admin password in one submission, then the submit handler runs `setRemotePassword`, `~/setup-tunnel.sh`, and alias-domain writes server-side and relays the script's output — including any `ACTION REQUIRED` block — back as the component's submitted payload.
138
133
 
139
134
  ```
140
135
  {
141
136
  title: "Set up remote access",
142
137
  description: "Connect your device to a custom domain.",
143
- adminDomainOptions: [...],
144
- publicDomainOptions: [...],
145
- apexDomainOptions: [...],
146
- defaultAdminDomain: "<first admin domain>",
147
- defaultPublicDomain: "<first public domain>",
148
138
  submitLabel: "Set up Cloudflare"
149
139
  }
150
140
  ```
@@ -30,8 +30,9 @@ The plugin registers no agent-facing MCP tools (Task 554). Every Cloudflare oper
30
30
 
31
31
  | Script | Purpose |
32
32
  |---|---|
33
- | [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel create, DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. |
34
- | [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. |
33
+ | [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel create, DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Task 588 drives the Cloudflare Authorize click via CDP WebSocket (`_cdp-authorize.mjs`, below) — no human click required when the VNC browser is already signed into Cloudflare. The OAuth cert-wait after the click is bounded at 20 s with a 2-second `step=oauth-login result=awaiting-cert elapsed=<N>s` heartbeat so no poll exceeds ~2 s silent. |
34
+ | [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. No polling blocks — every long-wait is bounded by `cloudflared`'s network round-trip, so no heartbeat contract applies. |
35
+ | [`scripts/_cdp-authorize.mjs`](scripts/_cdp-authorize.mjs) | Node 22 ESM helper driving the Cloudflare argotunnel Authorize click via CDP WebSocket. Self-contained (native `fetch` + `WebSocket`), invoked by `setup-tunnel.sh` with the target id returned from the `PUT /json/new?<url>` step. Exits 0 on successful click; 1 on `authorize-button-not-found` (the happy-path loud failure when the VNC browser isn't signed into Cloudflare); 2/3/4/5 on CDP/protocol errors. Emits one `cdp-authorize result=<…> reason=<…>` line to stdout per invocation, teed into the stream log under the `[setup-tunnel:cdp-click]` tag. |
35
36
 
36
37
  ### Skills
37
38
 
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ // Drive the Cloudflare argotunnel Authorize click via CDP WebSocket (Task 588).
3
+ //
4
+ // Why this exists: setup-tunnel.sh navigates the VNC browser to the argotunnel
5
+ // consent URL via `PUT /json/new?<url>`. That opens the page but does not
6
+ // press the Authorize button. Pre-Task 588 the script then polled for
7
+ // cert.pem for up to 180 seconds, waiting for a human click in VNC. This
8
+ // helper does the click from code, collapsing the wait to ~1-3 seconds.
9
+ //
10
+ // Protocol: Chrome DevTools Protocol over WebSocket (https://chromedevtools.github.io/devtools-protocol/).
11
+ // 1. GET http://127.0.0.1:9222/json/list → find the target with matching id;
12
+ // extract webSocketDebuggerUrl.
13
+ // 2. Open WS, enable Page + Runtime domains.
14
+ // 3. Wait for Page.loadEventFired (or observe document.readyState==='complete').
15
+ // 4. Poll Runtime.evaluate every 200 ms for up to ~3 s, looking for a
16
+ // button or input[type=submit] whose trimmed text/value matches
17
+ // /^(authorize|connect)$/i. When found, click via JS (click() on the
18
+ // matched node). Exit 0 on success, 1 with structured stderr otherwise.
19
+ //
20
+ // Exit codes:
21
+ // 0 - clicked successfully
22
+ // 1 - authorize-button-not-found (the happy-path loud failure)
23
+ // 2 - cdp-ws-unreachable or node-websocket-unavailable
24
+ // 3 - target-not-found (the target_id isn't in /json/list)
25
+ // 4 - click-evaluate-threw (Runtime.evaluate returned wasThrown=true)
26
+ // 5 - protocol-error (malformed CDP response)
27
+ //
28
+ // Each structured failure prints ONE line to stdout in phase_line-compatible
29
+ // shape so setup-tunnel.sh's tee_subprocess picks it up into the stream log:
30
+ // cdp-authorize result=<ok|error> reason=<…> elapsed_ms=<…> [detail=<…>]
31
+
32
+ const CDP_HOST = '127.0.0.1';
33
+ const CDP_PORT = 9222;
34
+ const HTTP_TIMEOUT_MS = 3000;
35
+ const BUTTON_POLL_TIMEOUT_MS = 3000;
36
+ const BUTTON_POLL_INTERVAL_MS = 200;
37
+ const LOAD_WAIT_TIMEOUT_MS = 5000;
38
+ const WS_CONNECT_TIMEOUT_MS = 3000;
39
+ const RESULT_WAIT_TIMEOUT_MS = 3000;
40
+
41
+ function emit(result, reason, extra = {}) {
42
+ const parts = [`cdp-authorize result=${result} reason=${reason}`];
43
+ for (const [k, v] of Object.entries(extra)) {
44
+ if (v === undefined || v === null) continue;
45
+ const s = String(v).replace(/\s+/g, ' ');
46
+ parts.push(`${k}="${s.slice(0, 200)}"`);
47
+ }
48
+ process.stdout.write(parts.join(' ') + '\n');
49
+ }
50
+
51
+ function die(code, reason, extra) {
52
+ emit('error', reason, extra);
53
+ process.exit(code);
54
+ }
55
+
56
+ if (typeof WebSocket === 'undefined') {
57
+ die(2, 'node-websocket-unavailable', {
58
+ detail: `Node ${process.version} has no global WebSocket — upgrade to Node 22+`,
59
+ });
60
+ }
61
+
62
+ const targetId = process.argv[2];
63
+ if (!targetId) {
64
+ die(2, 'missing-target-id', { detail: 'usage: _cdp-authorize.mjs <target-id>' });
65
+ }
66
+
67
+ const started = Date.now();
68
+
69
+ async function fetchTargets() {
70
+ const ac = new AbortController();
71
+ const timer = setTimeout(() => ac.abort(), HTTP_TIMEOUT_MS);
72
+ try {
73
+ const res = await fetch(`http://${CDP_HOST}:${CDP_PORT}/json/list`, { signal: ac.signal });
74
+ if (!res.ok) throw new Error(`/json/list returned ${res.status}`);
75
+ return await res.json();
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ }
80
+
81
+ let targets;
82
+ try {
83
+ targets = await fetchTargets();
84
+ } catch (err) {
85
+ die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
86
+ }
87
+
88
+ const target = Array.isArray(targets) ? targets.find((t) => t && t.id === targetId) : undefined;
89
+ if (!target || typeof target.webSocketDebuggerUrl !== 'string') {
90
+ die(3, 'target-not-found', { target_id: targetId, count: Array.isArray(targets) ? targets.length : -1 });
91
+ }
92
+
93
+ // Open WebSocket. Native Node 22 global.
94
+ let ws;
95
+ try {
96
+ ws = new WebSocket(target.webSocketDebuggerUrl);
97
+ } catch (err) {
98
+ die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
99
+ }
100
+
101
+ // WS open with race guards. Three outcomes are possible:
102
+ // 1. `open` fires before `error` / timeout → resolve and continue.
103
+ // 2. `error` fires before `open` → reject; caller emits die() and exits.
104
+ // 3. Timeout fires before either → reject; caller emits die() and exits.
105
+ // The `.once` on `open`/`error` listeners handles the race where both fire
106
+ // in quick succession (some proxy-protocol mismatches can emit `error`
107
+ // before the open event is dispatched). The catch below awaits the die()
108
+ // call and then `throw`s to guarantee execution does not fall through to
109
+ // the Page.enable call with a dead socket — even if a future refactor
110
+ // removes process.exit() from die().
111
+ try {
112
+ await new Promise((resolveOpen, rejectOpen) => {
113
+ const timer = setTimeout(() => rejectOpen(new Error('ws open timeout')), WS_CONNECT_TIMEOUT_MS);
114
+ ws.addEventListener('open', () => { clearTimeout(timer); resolveOpen(); }, { once: true });
115
+ ws.addEventListener('error', (e) => { clearTimeout(timer); rejectOpen(new Error(`ws error: ${e.message ?? 'unknown'}`)); }, { once: true });
116
+ });
117
+ } catch (err) {
118
+ die(2, 'cdp-ws-unreachable', { detail: err instanceof Error ? err.message : String(err) });
119
+ throw err; // unreachable — die() exits, but guards the type system and future refactors.
120
+ }
121
+
122
+ // Message router: CDP replies come back keyed by the `id` we sent. Events
123
+ // arrive with no `id` but a `method` (e.g. "Page.loadEventFired").
124
+ let nextId = 1;
125
+ const pending = new Map();
126
+ const eventListeners = new Map();
127
+
128
+ ws.addEventListener('message', (evt) => {
129
+ let msg;
130
+ try {
131
+ msg = JSON.parse(typeof evt.data === 'string' ? evt.data : evt.data.toString());
132
+ } catch {
133
+ return;
134
+ }
135
+ if (typeof msg.id === 'number' && pending.has(msg.id)) {
136
+ const { resolveIt, rejectIt } = pending.get(msg.id);
137
+ pending.delete(msg.id);
138
+ if (msg.error) rejectIt(new Error(`CDP error: ${msg.error.message ?? JSON.stringify(msg.error)}`));
139
+ else resolveIt(msg.result);
140
+ } else if (typeof msg.method === 'string') {
141
+ const listeners = eventListeners.get(msg.method);
142
+ if (listeners) for (const l of listeners) l(msg.params);
143
+ }
144
+ });
145
+
146
+ ws.addEventListener('close', () => {
147
+ for (const { rejectIt } of pending.values()) {
148
+ rejectIt(new Error('ws closed while awaiting response'));
149
+ }
150
+ pending.clear();
151
+ });
152
+
153
+ function send(method, params = {}) {
154
+ const id = nextId++;
155
+ return new Promise((resolveIt, rejectIt) => {
156
+ const timer = setTimeout(() => {
157
+ pending.delete(id);
158
+ rejectIt(new Error(`CDP ${method} timed out after ${RESULT_WAIT_TIMEOUT_MS}ms`));
159
+ }, RESULT_WAIT_TIMEOUT_MS);
160
+ pending.set(id, {
161
+ resolveIt: (r) => { clearTimeout(timer); resolveIt(r); },
162
+ rejectIt: (e) => { clearTimeout(timer); rejectIt(e); },
163
+ });
164
+ ws.send(JSON.stringify({ id, method, params }));
165
+ });
166
+ }
167
+
168
+ function onEvent(method, listener) {
169
+ let set = eventListeners.get(method);
170
+ if (!set) { set = new Set(); eventListeners.set(method, set); }
171
+ set.add(listener);
172
+ return () => set.delete(listener);
173
+ }
174
+
175
+ // Enable the two domains we need. Page.enable fires events; Runtime.enable
176
+ // lets us run expressions in the page context.
177
+ try {
178
+ await send('Page.enable');
179
+ await send('Runtime.enable');
180
+ } catch (err) {
181
+ die(5, 'protocol-error', { detail: err.message });
182
+ }
183
+
184
+ // Wait for load — if the page is already loaded (navigated before this helper
185
+ // ran), Page.loadEventFired may never fire again; poll document.readyState
186
+ // as a fallback. The first of the two to resolve wins.
187
+ async function waitForLoad() {
188
+ const loadFired = new Promise((resolveIt) => {
189
+ const off = onEvent('Page.loadEventFired', () => { off(); resolveIt('loadEvent'); });
190
+ });
191
+ const readyStatePoll = (async () => {
192
+ const deadline = Date.now() + LOAD_WAIT_TIMEOUT_MS;
193
+ while (Date.now() < deadline) {
194
+ try {
195
+ const r = await send('Runtime.evaluate', {
196
+ expression: 'document.readyState',
197
+ returnByValue: true,
198
+ });
199
+ if (r?.result?.value === 'complete' || r?.result?.value === 'interactive') return 'readyState';
200
+ } catch { /* keep polling */ }
201
+ await new Promise((res) => setTimeout(res, 200));
202
+ }
203
+ throw new Error('load-wait-timeout');
204
+ })();
205
+ return Promise.race([loadFired, readyStatePoll]);
206
+ }
207
+
208
+ try {
209
+ await waitForLoad();
210
+ } catch (err) {
211
+ die(1, 'page-load-timeout', { detail: err.message, elapsed_ms: Date.now() - started });
212
+ }
213
+
214
+ // Poll for the Authorize button. The expression finds the first
215
+ // button/input[type=submit] whose trimmed textContent/value matches
216
+ // /^(authorize|connect)$/i, clicks it, and returns a descriptor. Returns
217
+ // null when no match yet.
218
+ const CLICK_EXPR = `
219
+ (() => {
220
+ const candidates = Array.from(document.querySelectorAll('button, input[type="submit"]'));
221
+ const match = candidates.find((el) => {
222
+ const text = (el.textContent ?? el.value ?? '').trim();
223
+ return /^(authorize|connect)$/i.test(text) && !el.disabled;
224
+ });
225
+ if (!match) return null;
226
+ const descriptor = {
227
+ tag: match.tagName.toLowerCase(),
228
+ text: (match.textContent ?? match.value ?? '').trim().slice(0, 40),
229
+ disabled: Boolean(match.disabled),
230
+ };
231
+ match.click();
232
+ return descriptor;
233
+ })()
234
+ `;
235
+
236
+ const clickDeadline = Date.now() + BUTTON_POLL_TIMEOUT_MS;
237
+ let clicked = null;
238
+ while (Date.now() < clickDeadline) {
239
+ let r;
240
+ try {
241
+ r = await send('Runtime.evaluate', {
242
+ expression: CLICK_EXPR,
243
+ returnByValue: true,
244
+ awaitPromise: false,
245
+ });
246
+ } catch (err) {
247
+ die(5, 'protocol-error', { detail: err.message });
248
+ }
249
+ if (r?.exceptionDetails) {
250
+ die(4, 'click-evaluate-threw', {
251
+ detail: r.exceptionDetails.text ?? 'unknown exception',
252
+ elapsed_ms: Date.now() - started,
253
+ });
254
+ }
255
+ const val = r?.result?.value;
256
+ if (val && typeof val === 'object') {
257
+ clicked = val;
258
+ break;
259
+ }
260
+ await new Promise((res) => setTimeout(res, BUTTON_POLL_INTERVAL_MS));
261
+ }
262
+
263
+ if (!clicked) {
264
+ die(1, 'authorize-button-not-found', { elapsed_ms: Date.now() - started });
265
+ }
266
+
267
+ emit('ok', 'clicked', {
268
+ tag: clicked.tag,
269
+ text: clicked.text,
270
+ elapsed_ms: Date.now() - started,
271
+ });
272
+
273
+ try { ws.close(); } catch { /* best-effort */ }
274
+ process.exit(0);
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Deterministic scrape of the operator's Cloudflare dashboard to discover
3
+ # the domains on the logged-in account. Output on stdout is a JSON `string[]`,
4
+ # sorted and deduped. Every failure exits non-zero with a `reason=<enum>`
5
+ # token on stderr tagged `[list-cf-domains]`.
6
+ #
7
+ # Usage:
8
+ # list-cf-domains.sh [<brand>]
9
+ #
10
+ # The brand arg (default: maxy) names the `${HOME}/.${BRAND}/logs/` directory
11
+ # where selector-drift body dumps land. The script does not read brand.json —
12
+ # the directory is the only brand-derived path it needs.
13
+ #
14
+ # Runtime: Node 22's `--experimental-strip-types` runs the .ts helper
15
+ # directly, so no tsx / playwright / ws dependency exists.
16
+
17
+ set -euo pipefail
18
+
19
+ # --------------------------------------------------------------------------
20
+ # Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, …).
21
+ # --------------------------------------------------------------------------
22
+
23
+ # shellcheck source=_stream-log.sh
24
+ # Resolve symlinks before dirname — ~/list-cf-domains.sh is installed as a
25
+ # symlink into $HOME, so the raw BASH_SOURCE[0] points at $HOME, not the
26
+ # scripts directory where _stream-log.sh lives.
27
+ source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
28
+ require_stream_log_path list-cf-domains
29
+
30
+ BRAND="${1:-maxy}"
31
+ CONFIG_DIR=".${BRAND}"
32
+ mkdir -p "${HOME}/${CONFIG_DIR}/logs"
33
+
34
+ phase_line list-cf-domains phase=script-start brand="${BRAND}" config_dir="${CONFIG_DIR}"
35
+
36
+ SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
37
+ TS_ENTRY="${SCRIPT_DIR}/list-cf-domains.ts"
38
+
39
+ if [ ! -f "${TS_ENTRY}" ]; then
40
+ phase_line list-cf-domains phase=error reason=entry-missing path="${TS_ENTRY}"
41
+ exit 1
42
+ fi
43
+
44
+ # 30s hard ceiling. The node helper has its own per-phase budgets totalling
45
+ # ~27s; the wrapper's extra 3s absorbs process startup and CF network jitter.
46
+ # Use `timeout --preserve-status` so the exit code of a timeout is distinct
47
+ # (124) from a helper-reported failure (1).
48
+ HARD_TIMEOUT_SECS=30
49
+
50
+ # CONFIG_DIR is consumed by the node helper when writing selector-drift dumps
51
+ # to ~/.${BRAND}/logs/list-cf-domains-<ts>.html.
52
+ #
53
+ # `node --experimental-strip-types` (stable in Node 22.22) runs the .ts file
54
+ # natively: type annotations are stripped, no enums / decorators are used, so
55
+ # no TS type-transform is needed. `--no-warnings` suppresses the experimental
56
+ # banner which would otherwise leak into stderr and confuse the route parser.
57
+ set +e
58
+ CONFIG_DIR="${CONFIG_DIR}" timeout --preserve-status --signal=TERM "${HARD_TIMEOUT_SECS}" \
59
+ node --experimental-strip-types --no-warnings "${TS_ENTRY}"
60
+ EXIT_CODE=$?
61
+ set -e
62
+
63
+ if [ "${EXIT_CODE}" -eq 124 ]; then
64
+ phase_line list-cf-domains phase=script-exit code=124 reason=wrapper-timeout budget_secs="${HARD_TIMEOUT_SECS}"
65
+ exit 1
66
+ fi
67
+
68
+ phase_line list-cf-domains phase=script-exit code="${EXIT_CODE}"
69
+ exit "${EXIT_CODE}"