xaq-cli 0.0.0 → 0.1.0-nightly.72

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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/dist/admin-5gThRwgj.js +162 -0
  4. package/dist/admin-5gThRwgj.js.map +1 -0
  5. package/dist/admin-Btrm8mCd.js +218 -0
  6. package/dist/admin-Btrm8mCd.js.map +1 -0
  7. package/dist/admin-CWofnT_Q.js +2 -0
  8. package/dist/api-BFpElwqK.js +89 -0
  9. package/dist/api-BFpElwqK.js.map +1 -0
  10. package/dist/audit-BB7jK9Z5.js +2 -0
  11. package/dist/audit-BVYknW2i.js +79 -0
  12. package/dist/audit-BVYknW2i.js.map +1 -0
  13. package/dist/auth-CXmBdMqw.js +2 -0
  14. package/dist/auth-D-GmwNv-.js +550 -0
  15. package/dist/auth-D-GmwNv-.js.map +1 -0
  16. package/dist/brand--ZB8jTn1.js +32 -0
  17. package/dist/brand--ZB8jTn1.js.map +1 -0
  18. package/dist/bus-Cz0flS37.js +12 -0
  19. package/dist/bus-Cz0flS37.js.map +1 -0
  20. package/dist/bus-OKIGut72.js +2 -0
  21. package/dist/cli.js +234 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/commands-BNVLjYXA.js +167 -0
  24. package/dist/commands-BNVLjYXA.js.map +1 -0
  25. package/dist/commands-CdA6-WE7.js +112 -0
  26. package/dist/commands-CdA6-WE7.js.map +1 -0
  27. package/dist/config-C9XmYZWO.js +94 -0
  28. package/dist/config-C9XmYZWO.js.map +1 -0
  29. package/dist/config-CNVi0v5t.js +2 -0
  30. package/dist/connect-TtJ9n0Rw.js +362 -0
  31. package/dist/connect-TtJ9n0Rw.js.map +1 -0
  32. package/dist/content-C1lKMxFS.js +1170 -0
  33. package/dist/content-C1lKMxFS.js.map +1 -0
  34. package/dist/content-worker.js +28 -0
  35. package/dist/content-worker.js.map +1 -0
  36. package/dist/crypto-fv2fj3S8.js +75 -0
  37. package/dist/crypto-fv2fj3S8.js.map +1 -0
  38. package/dist/db-BEUEUPyP.js +561 -0
  39. package/dist/db-BEUEUPyP.js.map +1 -0
  40. package/dist/db-Q5BQuwga.js +2 -0
  41. package/dist/errors-FbMS3QBo.js +39 -0
  42. package/dist/errors-FbMS3QBo.js.map +1 -0
  43. package/dist/filter-Ck6OMxvu.js +158 -0
  44. package/dist/filter-Ck6OMxvu.js.map +1 -0
  45. package/dist/hermes-CKyzt1mH.js +3 -0
  46. package/dist/hermes-DHnDTYJp.js +1023 -0
  47. package/dist/hermes-DHnDTYJp.js.map +1 -0
  48. package/dist/import-DsN5NZT3.js +910 -0
  49. package/dist/import-DsN5NZT3.js.map +1 -0
  50. package/dist/instance-CwWbFbJ8.js +2 -0
  51. package/dist/instance-Dj9TpJB2.js +195 -0
  52. package/dist/instance-Dj9TpJB2.js.map +1 -0
  53. package/dist/listen-B9iO7HgQ.js +1568 -0
  54. package/dist/listen-B9iO7HgQ.js.map +1 -0
  55. package/dist/listen-CXVKG9XD.js +2 -0
  56. package/dist/manage-Bo6COB4p.js +477 -0
  57. package/dist/manage-Bo6COB4p.js.map +1 -0
  58. package/dist/manage-DoXqvjnU.js +2 -0
  59. package/dist/mcpheader-B8c5YO1B.js +2 -0
  60. package/dist/mcpheader-C9H4vudu.js +185 -0
  61. package/dist/mcpheader-C9H4vudu.js.map +1 -0
  62. package/dist/mcpsetup-BnHudwpq.js +2 -0
  63. package/dist/mcpsetup-DXXo2qjf.js +1053 -0
  64. package/dist/mcpsetup-DXXo2qjf.js.map +1 -0
  65. package/dist/node-check-CjTk6VNL.js +24 -0
  66. package/dist/node-check-CjTk6VNL.js.map +1 -0
  67. package/dist/paths-d-C2kdjo.js +2 -0
  68. package/dist/paths-yzOKD6ow.js +46 -0
  69. package/dist/paths-yzOKD6ow.js.map +1 -0
  70. package/dist/profile-CcMZ4hn7.js +2 -0
  71. package/dist/profile-NmMKAczz.js +222 -0
  72. package/dist/profile-NmMKAczz.js.map +1 -0
  73. package/dist/prompt-CKMQx_MJ.js +114 -0
  74. package/dist/prompt-CKMQx_MJ.js.map +1 -0
  75. package/dist/recipes-C7TIDlRj.js +3 -0
  76. package/dist/recipes-DG_-doGq.js +314 -0
  77. package/dist/recipes-DG_-doGq.js.map +1 -0
  78. package/dist/send-Dk-t8ia-.js +108 -0
  79. package/dist/send-Dk-t8ia-.js.map +1 -0
  80. package/dist/server-CDqDp1q3.js +20640 -0
  81. package/dist/server-CDqDp1q3.js.map +1 -0
  82. package/dist/service-BVn3soZZ.js +766 -0
  83. package/dist/service-BVn3soZZ.js.map +1 -0
  84. package/dist/service-BaWGPmSd.js +2 -0
  85. package/dist/service-C7jfo-Ex.js +457 -0
  86. package/dist/service-C7jfo-Ex.js.map +1 -0
  87. package/dist/service-DxDyKDQ-.js +2 -0
  88. package/dist/settings-B5qSHxRM.js +134 -0
  89. package/dist/settings-B5qSHxRM.js.map +1 -0
  90. package/dist/settings-DQvZaMN1.js +2 -0
  91. package/dist/setup-token-CwuSzfLK.js +48 -0
  92. package/dist/setup-token-CwuSzfLK.js.map +1 -0
  93. package/dist/skill/SKILL.md +445 -0
  94. package/dist/skill-1UQj3qgW.js +69 -0
  95. package/dist/skill-1UQj3qgW.js.map +1 -0
  96. package/dist/skill-pelq18JQ.js +2 -0
  97. package/dist/ssrf-BlVpGZ7C.js +181 -0
  98. package/dist/ssrf-BlVpGZ7C.js.map +1 -0
  99. package/dist/types-BhJDbSzE.js +135 -0
  100. package/dist/types-BhJDbSzE.js.map +1 -0
  101. package/dist/ui-B8Sd4Y7Y.js +81 -0
  102. package/dist/ui-B8Sd4Y7Y.js.map +1 -0
  103. package/dist/urls-Dp8pBGMU.js +2 -0
  104. package/dist/urls-U_LfYV61.js +27 -0
  105. package/dist/urls-U_LfYV61.js.map +1 -0
  106. package/dist/version-BxH7OuWD.js +20 -0
  107. package/dist/version-BxH7OuWD.js.map +1 -0
  108. package/dist/web/assets/_agentId-DonOCOlC.js +1 -0
  109. package/dist/web/assets/_app-B7t5lEia.js +1 -0
  110. package/dist/web/assets/_app-DMTGqYpM.js +65 -0
  111. package/dist/web/assets/_connectionId-DR5vFXOg.js +1 -0
  112. package/dist/web/assets/_webhookId-LfUlsULp.js +1 -0
  113. package/dist/web/assets/account-status-CEszs13C.js +1 -0
  114. package/dist/web/assets/accounts-5m7aiXuZ.js +1 -0
  115. package/dist/web/assets/accounts-list-YfJzThZa.js +1 -0
  116. package/dist/web/assets/activity-DOIe7SAg.js +1 -0
  117. package/dist/web/assets/add-connection-C8JR-13R.js +1 -0
  118. package/dist/web/assets/agent-create-form-CPxnPYH4.js +1 -0
  119. package/dist/web/assets/agents-B2Qj5KxT.js +1 -0
  120. package/dist/web/assets/approvals-BGaVaoFy.js +1 -0
  121. package/dist/web/assets/approve._id-BLrQnzPM.js +1 -0
  122. package/dist/web/assets/arrow-left-DZVAXoKj.js +1 -0
  123. package/dist/web/assets/arrow-right-C2zcqRr2.js +1 -0
  124. package/dist/web/assets/attention-gO9Nw5uW.js +1 -0
  125. package/dist/web/assets/audit-BPXXre8S.js +1 -0
  126. package/dist/web/assets/audit-bits-CGyIprzK.js +1 -0
  127. package/dist/web/assets/bell-ring-ig3zn1iS.js +1 -0
  128. package/dist/web/assets/bot-Zk76q_39.js +1 -0
  129. package/dist/web/assets/calendar-notice-DsCfq3VJ.js +1 -0
  130. package/dist/web/assets/calendar-plus-Cycfkk0y.js +1 -0
  131. package/dist/web/assets/calendar-setup-Pn61Wj1n.js +2 -0
  132. package/dist/web/assets/card-GSO5o-Ug.js +1 -0
  133. package/dist/web/assets/check-check-Ds1bPIGv.js +1 -0
  134. package/dist/web/assets/checkbox-B937U-Fd.js +1 -0
  135. package/dist/web/assets/chevron-right-Cqh4mDk1.js +1 -0
  136. package/dist/web/assets/chips-input-2xWDofmE.js +1 -0
  137. package/dist/web/assets/choice-cards-CgAIqBGX.js +1 -0
  138. package/dist/web/assets/circle-x-DcU-0zVh.js +1 -0
  139. package/dist/web/assets/command-khhto70y.js +1 -0
  140. package/dist/web/assets/common-BPZV8TFb.js +1 -0
  141. package/dist/web/assets/connect-agent-guide-CJdw31p2.js +1 -0
  142. package/dist/web/assets/connect-host-lr2c3xWh.js +6 -0
  143. package/dist/web/assets/connection-row-DYx_5o0c.js +1 -0
  144. package/dist/web/assets/connections-C4NOFb5R.js +1 -0
  145. package/dist/web/assets/data-table-kW1pkRB5.js +1 -0
  146. package/dist/web/assets/empty-DzTDioBP.js +1 -0
  147. package/dist/web/assets/events-C9zJnPKK.js +1 -0
  148. package/dist/web/assets/eye-B9gT1QSq.js +1 -0
  149. package/dist/web/assets/eye-off-Diktqz_g.js +1 -0
  150. package/dist/web/assets/field-C6ejurju.js +1 -0
  151. package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  152. package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  153. package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  154. package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  155. package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  156. package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  157. package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  158. package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  159. package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  160. package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  161. package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  162. package/dist/web/assets/get-started-C38bq1gZ.js +2 -0
  163. package/dist/web/assets/hidden-chars-Dr9TxfvF.js +1 -0
  164. package/dist/web/assets/index-DGzo3kGh.js +11 -0
  165. package/dist/web/assets/index-zKOCQLGX.css +2 -0
  166. package/dist/web/assets/input-CWwJgozn.js +1 -0
  167. package/dist/web/assets/input-group-HG8OXcJ4.js +1 -0
  168. package/dist/web/assets/label-multi-select-sH8mllw8.js +1 -0
  169. package/dist/web/assets/labels-B5UqVwFo.js +1 -0
  170. package/dist/web/assets/laptop-BxJ4Nbj7.js +1 -0
  171. package/dist/web/assets/last-call-D-O5O94z.js +1 -0
  172. package/dist/web/assets/listener-handled-HNCuVrQ7.js +1 -0
  173. package/dist/web/assets/login-DaeKclcf.js +1 -0
  174. package/dist/web/assets/mail-C5fm2pTC.js +1 -0
  175. package/dist/web/assets/mcp-call-UoOTPFyh.js +1 -0
  176. package/dist/web/assets/mcp-call-card-Cr-OEDX2.js +3 -0
  177. package/dist/web/assets/message-square-quote-se8Bf3RP.js +1 -0
  178. package/dist/web/assets/mutations-CLPqJ6cj.js +1 -0
  179. package/dist/web/assets/network-igNw0VVx.js +1 -0
  180. package/dist/web/assets/pairing-XjuaVr2u.js +1 -0
  181. package/dist/web/assets/pairing-notifier-KPzOEZSO.js +1 -0
  182. package/dist/web/assets/paperclip-znS4iTrg.js +1 -0
  183. package/dist/web/assets/password-input-DV3aqcNf.js +1 -0
  184. package/dist/web/assets/pause-o-dK3khK.js +1 -0
  185. package/dist/web/assets/pencil-DkPFAhEq.js +1 -0
  186. package/dist/web/assets/play-DvCHND93.js +1 -0
  187. package/dist/web/assets/plug-CXAniEiR.js +1 -0
  188. package/dist/web/assets/plug-zap-BwR-l5wb.js +1 -0
  189. package/dist/web/assets/plus-B_TUslu3.js +1 -0
  190. package/dist/web/assets/preload-helper-Cc_aKyUb.js +1 -0
  191. package/dist/web/assets/public-url-rF2Xjyel.js +1 -0
  192. package/dist/web/assets/queries-Cv3nXx0U.js +42 -0
  193. package/dist/web/assets/refresh-cw-Ch5FNrby.js +1 -0
  194. package/dist/web/assets/revealed-keys-C_9NtJPY.js +1 -0
  195. package/dist/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  196. package/dist/web/assets/rotate-ccw-B-mzPDk0.js +1 -0
  197. package/dist/web/assets/search-CTVKGomS.js +1 -0
  198. package/dist/web/assets/select-Bb806bpq.js +1 -0
  199. package/dist/web/assets/send-DuMVuYs1.js +1 -0
  200. package/dist/web/assets/settings-BWauhfFf.js +1 -0
  201. package/dist/web/assets/setup-B7bNKQOp.js +1 -0
  202. package/dist/web/assets/sheet-CYPwpUsH.js +1 -0
  203. package/dist/web/assets/shield-alert-CSYB-ThV.js +1 -0
  204. package/dist/web/assets/shield-check-BulpsYqK.js +1 -0
  205. package/dist/web/assets/sparkles-CUTiob4K.js +1 -0
  206. package/dist/web/assets/stamp-BNXUGNa3.js +1 -0
  207. package/dist/web/assets/step-url-51aMNak-.js +1 -0
  208. package/dist/web/assets/stream-Du7fY5_Y.js +1 -0
  209. package/dist/web/assets/tag-I5_-1jFL.js +1 -0
  210. package/dist/web/assets/terminal-BvxDFTJc.js +1 -0
  211. package/dist/web/assets/test-email-button-DYi7V_6X.js +1 -0
  212. package/dist/web/assets/toggle-group-Bdo1gmQ1.js +1 -0
  213. package/dist/web/assets/trash-Ci0aICEl.js +1 -0
  214. package/dist/web/assets/use-delivery-speed-BBh7W2q6.js +1 -0
  215. package/dist/web/assets/use-label-names-BXicesnP.js +1 -0
  216. package/dist/web/assets/use-sign-out-CPrY_oVO.js +1 -0
  217. package/dist/web/assets/useInfiniteQuery-DJLIM8yJ.js +1 -0
  218. package/dist/web/assets/useQuery-MrTwwNy5.js +1 -0
  219. package/dist/web/assets/useRouterState-D4OBK8l5.js +1 -0
  220. package/dist/web/assets/webhook-DjeeZiNC.js +1 -0
  221. package/dist/web/assets/webhook-bits-jw-y2D3u.js +1 -0
  222. package/dist/web/assets/webhook-sheet-B8h0skDe.js +34 -0
  223. package/dist/web/assets/webhooks-Cfsdpoyd.js +1 -0
  224. package/dist/web/assets/x-MF2TZ9aG.js +1 -0
  225. package/dist/web/favicon.svg +1 -0
  226. package/dist/web/index.html +27 -0
  227. package/package.json +98 -1
@@ -0,0 +1,766 @@
1
+ #!/usr/bin/env node
2
+ import { n as envVar, t as LEGACY_PRODUCT } from "./brand--ZB8jTn1.js";
3
+ import { t as Crypto } from "./crypto-fv2fj3S8.js";
4
+ import { r as readSetupToken } from "./setup-token-CwuSzfLK.js";
5
+ import { i as legacyDataDir, n as dataFiles } from "./config-C9XmYZWO.js";
6
+ import { f as NPM_PACKAGE } from "./types-BhJDbSzE.js";
7
+ import { c as runningInstance, i as cliCommand, l as serviceInstallCommand, n as assertMasterKeyUsable } from "./instance-Dj9TpJB2.js";
8
+ import { i as loopbackUrl, t as consoleUrls } from "./urls-U_LfYV61.js";
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
+ import { basename, dirname, join, resolve } from "node:path";
11
+ import { createServer } from "node:net";
12
+ import { homedir, tmpdir, userInfo } from "node:os";
13
+ import { execFileSync } from "node:child_process";
14
+ const LAUNCHD_LABEL = "sh.xaq.daemon";
15
+ /**
16
+ * The service's names before the rename (agentinbox). `service install` stops and replaces a service
17
+ * installed under them (rolling back to it on failure); `status` and `uninstall` find it too.
18
+ */
19
+ const LEGACY_SERVICE_NAME = "agentinbox";
20
+ const LEGACY_LAUNCHD_LABEL = "com.unlaboredlabs.agentinbox";
21
+ /** The package a pre-rename service install put into `<data>/app`. */
22
+ const LEGACY_PACKAGE = "agentinbox-cli";
23
+ const DAEMON_UNIT = {
24
+ launchd: LAUNCHD_LABEL,
25
+ systemd: "xaq"
26
+ };
27
+ const LEGACY_DAEMON_UNIT = {
28
+ launchd: LEGACY_LAUNCHD_LABEL,
29
+ systemd: LEGACY_SERVICE_NAME
30
+ };
31
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
32
+ /** Package root: one level above the running module (dist/cli.js or server/service.ts). */
33
+ const ROOT = resolve(import.meta.dirname, "..");
34
+ /**
35
+ * Settings that come from the environment rather than flags. They are kept in `<data>/service.env`
36
+ * (mode 0600) so re-running `service install` from a shell without them (the documented update
37
+ * path) keeps them. Set one to an empty value to drop it.
38
+ */
39
+ const SERVICE_ENV_KEYS = [
40
+ "XAQ_MASTER_KEY",
41
+ "TRUST_PROXY",
42
+ "TRUST_PROXY_HOPS",
43
+ "TRUST_PROXY_FROM",
44
+ "XAQ_LOG_LEVEL"
45
+ ];
46
+ /** Their names before the rename: still read from the shell and from a service.env an older install wrote. */
47
+ const LEGACY_ENV_KEYS = {
48
+ XAQ_MASTER_KEY: "AGENTINBOX_MASTER_KEY",
49
+ XAQ_LOG_LEVEL: "AGENTINBOX_LOG_LEVEL"
50
+ };
51
+ const serviceEnvPath = (dataDir) => join(dataDir, "service.env");
52
+ /** Plain variables written into the unit/plist. Secrets never go into the systemd unit. */
53
+ function plainEnv(spec) {
54
+ return {
55
+ XAQ_DATA: spec.dataDir,
56
+ PORT: String(spec.port),
57
+ HOST: spec.host,
58
+ XAQ_SERVICE: "1"
59
+ };
60
+ }
61
+ /** systemd word quoting for ExecStart/Environment: double quotes, escaped backslashes, quotes and specifiers. */
62
+ const sdQuote = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%")}"`;
63
+ /**
64
+ * The systemd unit. Everything from service.env (including XAQ_MASTER_KEY) is loaded with
65
+ * EnvironmentFile=, so the world-readable unit holds no secrets. EnvironmentFile takes an
66
+ * unquoted absolute path (systemd ignores quoted ones), which is why data dirs with whitespace are refused.
67
+ */
68
+ function renderSystemdUnit(spec) {
69
+ const vars = Object.entries(plainEnv(spec)).map(([k, v]) => `Environment=${sdQuote(`${k}=${v}`)}`).join("\n");
70
+ const envFile = Object.keys(spec.extraEnv).length ? `\nEnvironmentFile=${serviceEnvPath(spec.dataDir).replace(/%/g, "%%")}` : "";
71
+ return `[Unit]
72
+ Description=xaq (MCP broker for AI agents)${spec.system ? "\nAfter=network-online.target\nWants=network-online.target" : ""}
73
+
74
+ [Service]
75
+ ExecStart=${sdQuote(spec.execPath)} ${sdQuote(spec.entry)} start --no-open
76
+ ${vars}${envFile}
77
+ Restart=on-failure
78
+ RestartSec=3
79
+ TimeoutStopSec=20
80
+
81
+ [Install]
82
+ WantedBy=${spec.system ? "multi-user.target" : "default.target"}
83
+ `;
84
+ }
85
+ /** The launchd plist. Written with mode 0600, since it may carry XAQ_MASTER_KEY. */
86
+ /** Escapes text for a plist <string>. */
87
+ const xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
88
+ function renderLaunchdPlist(spec) {
89
+ const esc = xmlEscape;
90
+ const vars = Object.entries({
91
+ ...plainEnv(spec),
92
+ ...spec.extraEnv
93
+ }).map(([k, v]) => ` <key>${esc(k)}</key><string>${esc(v)}</string>`).join("\n");
94
+ const log = esc(dataFiles(spec.dataDir).log);
95
+ return `<?xml version="1.0" encoding="UTF-8"?>
96
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
97
+ <plist version="1.0">
98
+ <dict>
99
+ <key>Label</key><string>${LAUNCHD_LABEL}</string>
100
+ <key>ProgramArguments</key>
101
+ <array>
102
+ <string>${esc(spec.execPath)}</string>
103
+ <string>${esc(spec.entry)}</string>
104
+ <string>start</string>
105
+ <string>--no-open</string>
106
+ </array>
107
+ <key>EnvironmentVariables</key>
108
+ <dict>
109
+ ${vars}
110
+ </dict>
111
+ <key>RunAtLoad</key><true/>
112
+ <key>KeepAlive</key><true/>
113
+ <key>StandardOutPath</key><string>${log}</string>
114
+ <key>StandardErrorPath</key><string>${log}</string>
115
+ </dict>
116
+ </plist>
117
+ `;
118
+ }
119
+ /** Parses service.env (KEY=value lines). */
120
+ function parseEnvFile(text) {
121
+ const out = {};
122
+ for (const line of text.split("\n")) {
123
+ const m = /^\s*([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
124
+ if (m) out[m[1]] = m[2].trim();
125
+ }
126
+ return out;
127
+ }
128
+ /**
129
+ * The service's env-derived settings: the current shell wins, previous install fills the gaps,
130
+ * an explicitly empty value removes the setting. The pre-rename names (AGENTINBOX_MASTER_KEY…) count
131
+ * where the new one isn't given, and are written back under the new name.
132
+ */
133
+ function mergeServiceEnv(previous, current) {
134
+ const out = {};
135
+ for (const key of SERVICE_ENV_KEYS) {
136
+ const old = LEGACY_ENV_KEYS[key];
137
+ const value = key in current ? current[key] : old && old in current ? current[old] : previous[key] ?? (old ? previous[old] : void 0);
138
+ if (value) out[key] = key === "TRUST_PROXY_FROM" ? value.split(/[\s,]+/).filter(Boolean).join(",") : value;
139
+ }
140
+ return out;
141
+ }
142
+ /** Fails early (before anything is touched) when the master key can't work. */
143
+ function validateMasterKey(value) {
144
+ if (!value) return;
145
+ try {
146
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value.trim())) throw new Error("not base64");
147
+ new Crypto(Buffer.from(value, "base64"));
148
+ } catch {
149
+ throw new Error("XAQ_MASTER_KEY must be 32 random bytes in base64 (e.g. `openssl rand -base64 32`).");
150
+ }
151
+ }
152
+ function readPreviousEnv(dataDir) {
153
+ try {
154
+ return parseEnvFile(readFileSync(serviceEnvPath(dataDir), "utf8"));
155
+ } catch {
156
+ return {};
157
+ }
158
+ }
159
+ function writeServiceEnv(dataDir, env) {
160
+ const file = serviceEnvPath(dataDir);
161
+ if (!Object.keys(env).length) {
162
+ rmSync(file, { force: true });
163
+ return;
164
+ }
165
+ for (const [k, v] of Object.entries(env)) if (/[\r\n]/.test(v)) throw new Error(`${k} must be a single line.`);
166
+ writeFileSync(file, serviceEnvText(env), { mode: 384 });
167
+ }
168
+ /**
169
+ * service.env's content. For one release, XAQ_MASTER_KEY and XAQ_LOG_LEVEL are also written under their
170
+ * pre-rename names, so going back to agentinbox-cli by hand (its `service install` reads only those)
171
+ * still finds the key and starts.
172
+ */
173
+ function serviceEnvText(env) {
174
+ const lines = Object.entries(env).map(([k, v]) => `${k}=${v}\n`);
175
+ for (const [key, old] of Object.entries(LEGACY_ENV_KEYS)) {
176
+ const value = env[key];
177
+ if (value && old) lines.push(`${old}=${value}\n`);
178
+ }
179
+ return lines.join("");
180
+ }
181
+ /**
182
+ * Installs the package into `staging` (a fresh private npm prefix), so a failed npm install leaves the
183
+ * current install untouched. From an installed package that is the latest release; from a source
184
+ * checkout, the local build. Returns `staging`. The one install path for the daemon and the listeners.
185
+ */
186
+ function stagePackage(staging, opts) {
187
+ const fromSource = basename(dirname(ROOT)) !== "node_modules";
188
+ let spec = `${NPM_PACKAGE}@latest`;
189
+ let tmp;
190
+ if (fromSource) {
191
+ if (!existsSync(join(ROOT, "dist/cli.js")) || opts.requireWeb && !existsSync(join(ROOT, "dist/web/index.html"))) throw new Error("Run `pnpm build` before installing the service from a source checkout.");
192
+ tmp = mkdtempSync(join(tmpdir(), "xaq-pack-"));
193
+ const out = execFileSync(npm, [
194
+ "pack",
195
+ "--ignore-scripts",
196
+ "--pack-destination",
197
+ tmp,
198
+ "--loglevel=error"
199
+ ], {
200
+ cwd: ROOT,
201
+ encoding: "utf8"
202
+ });
203
+ spec = join(tmp, out.trim().split("\n").pop());
204
+ }
205
+ console.log(fromSource ? ` Installing this build into ${opts.where}…` : ` Installing the latest release into ${opts.where}…`);
206
+ rmSync(staging, {
207
+ recursive: true,
208
+ force: true
209
+ });
210
+ mkdirSync(staging, { recursive: true });
211
+ writeFileSync(join(staging, "package.json"), "{ \"private\": true }\n");
212
+ try {
213
+ execFileSync(npm, [
214
+ "install",
215
+ spec,
216
+ "--omit=dev",
217
+ "--prefer-online",
218
+ "--no-audit",
219
+ "--no-fund",
220
+ "--loglevel=error"
221
+ ], {
222
+ cwd: staging,
223
+ stdio: "inherit"
224
+ });
225
+ if (!existsSync(installedEntry(staging))) throw new Error("the installed package has no dist/cli.js");
226
+ } catch (err) {
227
+ rmSync(staging, {
228
+ recursive: true,
229
+ force: true
230
+ });
231
+ throw new Error(`Installing xaq failed, so the existing service was left as it was. ${err instanceof Error ? err.message : ""}`.trim());
232
+ } finally {
233
+ if (tmp) rmSync(tmp, {
234
+ recursive: true,
235
+ force: true
236
+ });
237
+ }
238
+ return staging;
239
+ }
240
+ /** The CLI entry inside an install made by stagePackage. */
241
+ const installedEntry = (app) => join(app, "node_modules", NPM_PACKAGE, "dist/cli.js");
242
+ /** Installs into `<dir>/app.new` (see stagePackage). Returns the staging directory. */
243
+ const stageApp = (dir) => stagePackage(join(dir, "app.new"), {
244
+ where: "the data directory",
245
+ requireWeb: true
246
+ });
247
+ /**
248
+ * Moves a staged install into place as `app` (the current one becomes `app.old`). `restore()` puts
249
+ * the previous install back; `commit()` deletes it. If the move itself fails, nothing changes.
250
+ */
251
+ function swapIn(staging, app) {
252
+ const old = `${app}.old`;
253
+ rmSync(old, {
254
+ recursive: true,
255
+ force: true
256
+ });
257
+ if (existsSync(app)) renameSync(app, old);
258
+ try {
259
+ renameSync(staging, app);
260
+ } catch (err) {
261
+ if (existsSync(old)) renameSync(old, app);
262
+ throw err;
263
+ }
264
+ return {
265
+ restore() {
266
+ rmSync(app, {
267
+ recursive: true,
268
+ force: true
269
+ });
270
+ if (existsSync(old)) renameSync(old, app);
271
+ },
272
+ commit() {
273
+ rmSync(old, {
274
+ recursive: true,
275
+ force: true
276
+ });
277
+ }
278
+ };
279
+ }
280
+ /** Deletes `<dir>/app`, `app.new` and `app.old`, but only where they really are our own installs (xaq-cli, or agentinbox-cli from before the rename). */
281
+ function removeOwnInstall(dir) {
282
+ for (const name of [
283
+ "app",
284
+ "app.new",
285
+ "app.old"
286
+ ]) {
287
+ const path = join(dir, name);
288
+ if (["xaq-cli", "agentinbox-cli"].some((p) => existsSync(join(path, "node_modules", p)))) rmSync(path, {
289
+ recursive: true,
290
+ force: true
291
+ });
292
+ }
293
+ }
294
+ const systemOps = () => {
295
+ const root = process.getuid?.() === 0;
296
+ const ops = {
297
+ platform: process.platform,
298
+ root,
299
+ exec: (cmd, args, opts) => void execFileSync(cmd, args, { stdio: opts?.quiet ? "ignore" : "inherit" }),
300
+ capture: (cmd, args) => execFileSync(cmd, args, {
301
+ encoding: "utf8",
302
+ stdio: [
303
+ "ignore",
304
+ "pipe",
305
+ "pipe"
306
+ ]
307
+ }),
308
+ stage: stageApp,
309
+ unitDir: () => {
310
+ if (process.platform === "darwin") return join(homedir(), "Library/LaunchAgents");
311
+ if (root) return "/etc/systemd/system";
312
+ unitDirCache ??= join(userManagerConfigHome(ops) ?? join(homedir(), ".config"), "systemd/user");
313
+ return unitDirCache;
314
+ },
315
+ waitReady: waitForHealth,
316
+ instance: runningInstance,
317
+ portInUse,
318
+ uid: process.getuid?.() ?? 0,
319
+ user: userInfo().username
320
+ };
321
+ let unitDirCache;
322
+ return ops;
323
+ };
324
+ /**
325
+ * XDG_CONFIG_HOME as the systemd user manager sees it (where `systemctl --user` looks for units),
326
+ * which can differ from this shell's. Null when unset there or the manager can't be asked.
327
+ */
328
+ function userManagerConfigHome(ops) {
329
+ try {
330
+ const env = ops.capture("systemctl", ["--user", "show-environment"]);
331
+ return /^XDG_CONFIG_HOME=(.+)$/m.exec(env)?.[1]?.trim() || null;
332
+ } catch {
333
+ return null;
334
+ }
335
+ }
336
+ /** The unit file (systemd) or plist (launchd) for a service's names. */
337
+ const unitPath = (ops, id) => join(ops.unitDir(), ops.platform === "darwin" ? `${id.launchd}.plist` : `${id.systemd}.service`);
338
+ const unitFile = (ops) => unitPath(ops, DAEMON_UNIT);
339
+ const unitLabel = (ops, id) => ops.platform === "darwin" ? id.launchd : id.systemd;
340
+ const systemctl = (ops, ...args) => ops.exec("systemctl", ops.root ? args : ["--user", ...args]);
341
+ const launchdTarget = (ops) => `gui/${ops.uid}`;
342
+ function assertSupported(ops) {
343
+ if (ops.platform !== "linux" && ops.platform !== "darwin") throw new Error(`Service install supports Linux (systemd) and macOS (launchd). Elsewhere, install the command (\`npm i -g ${NPM_PACKAGE}\`) and run \`xaq start\` under your own supervisor.`);
344
+ }
345
+ const readText = (file) => {
346
+ try {
347
+ return readFileSync(file, "utf8");
348
+ } catch {
349
+ return null;
350
+ }
351
+ };
352
+ /** Where to look when the service doesn't start. */
353
+ function serviceHints(ops, env = process.env, id = DAEMON_UNIT) {
354
+ if (ops.platform === "darwin") return [`launchctl print ${launchdTarget(ops)}/${id.launchd}`, "and the log file xaq.log (agentinbox.log from before the rename) in the data directory"];
355
+ const scope = ops.root ? "" : " --user";
356
+ const hints = [`systemctl${scope} status ${id.systemd}`, `journalctl${scope} -u ${id.systemd} -e`];
357
+ if (!ops.root && !env.XDG_RUNTIME_DIR) hints.push(`XDG_RUNTIME_DIR is not set, so there may be no systemd user session (common over \`su\` or \`sudo -u\`). Log in as ${ops.user} directly (e.g. over ssh), or run \`sudo loginctl enable-linger ${ops.user}\` and then \`export XDG_RUNTIME_DIR=/run/user/${ops.uid}\` before installing.`);
358
+ return hints;
359
+ }
360
+ const firstLine = (err) => (err instanceof Error ? err.message : String(err)).split("\n")[0];
361
+ /** First line of a failed command's stderr (what launchctl / systemctl said), else of the error. */
362
+ const commandFailure = (err) => {
363
+ return (String(err?.stderr ?? "").trim() || (err instanceof Error ? err.message : String(err))).split("\n")[0];
364
+ };
365
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
366
+ /** Whether launchd has a job with this label loaded (any `launchctl print` failure counts as not loaded). */
367
+ function launchdLoaded(ops, label) {
368
+ try {
369
+ ops.capture("launchctl", ["print", `${launchdTarget(ops)}/${label}`]);
370
+ return true;
371
+ } catch {
372
+ return false;
373
+ }
374
+ }
375
+ /**
376
+ * Loads the launchd job in `file`, replacing a loaded job with the same label. `launchctl bootout` returns
377
+ * before the old job is torn down (launchd gives it up to 20 s to exit), and `bootstrap` of that label fails
378
+ * with "5: Input/output error" until it is. So: boot it out, wait until `launchctl print` no longer finds it,
379
+ * then bootstrap; on failure, retry with growing delays. Throws launchd's reason when every attempt failed.
380
+ */
381
+ async function bootstrapLaunchd(ops, label, file, opts = {}) {
382
+ const { attempts = 5, pollMs = 250, unloadTimeoutMs = 3e4 } = opts;
383
+ let last = null;
384
+ const deadline = Date.now() + unloadTimeoutMs;
385
+ for (let attempt = 1; attempt <= attempts; attempt++) {
386
+ try {
387
+ ops.exec("launchctl", ["bootout", `${launchdTarget(ops)}/${label}`], { quiet: true });
388
+ } catch {}
389
+ await waitUnloaded(ops, label, deadline, pollMs);
390
+ try {
391
+ ops.capture("launchctl", [
392
+ "bootstrap",
393
+ launchdTarget(ops),
394
+ file
395
+ ]);
396
+ return;
397
+ } catch (err) {
398
+ last = err;
399
+ if (attempt < attempts) await sleep(pollMs * 4 * Math.min(2 ** (attempt - 1), 4));
400
+ }
401
+ }
402
+ const stillLoaded = launchdLoaded(ops, label);
403
+ throw new Error(`launchctl bootstrap failed: ${commandFailure(last)}${stillLoaded ? ` (a job labelled ${label} is still loaded and wouldn't unload)` : ""}`);
404
+ }
405
+ /** Polls `launchctl print` until launchd no longer lists the label, or the deadline passes. */
406
+ async function waitUnloaded(ops, label, deadline, pollMs) {
407
+ while (launchdLoaded(ops, label) && Date.now() < deadline) await sleep(pollMs);
408
+ }
409
+ function unitState(ops, id) {
410
+ if (ops.platform === "darwin") return {
411
+ running: launchdLoaded(ops, id.launchd),
412
+ enabled: false
413
+ };
414
+ const ask = (verb) => {
415
+ try {
416
+ return ops.capture("systemctl", [
417
+ ...ops.root ? [] : ["--user"],
418
+ verb,
419
+ id.systemd
420
+ ]).trim();
421
+ } catch (err) {
422
+ return String(err?.stdout ?? "").trim();
423
+ }
424
+ };
425
+ const active = ask("is-active");
426
+ return {
427
+ running: active === "active" || active === "activating" || active === "reloading",
428
+ enabled: ask("is-enabled") === "enabled"
429
+ };
430
+ }
431
+ /**
432
+ * Stops a service and waits until it is really gone, so a replacement never runs next to it (they would
433
+ * fight over the port and the data directory). systemd's stop waits by itself; launchd's bootout returns
434
+ * at once and tears the job down asynchronously. Throws when it is still running afterwards.
435
+ */
436
+ async function stopAndWait(ops, id, opts = {}) {
437
+ const { pollMs = 250, unloadTimeoutMs = 3e4 } = opts;
438
+ if (ops.platform === "darwin") {
439
+ try {
440
+ ops.exec("launchctl", ["bootout", `${launchdTarget(ops)}/${id.launchd}`], { quiet: true });
441
+ } catch {}
442
+ await waitUnloaded(ops, id.launchd, Date.now() + unloadTimeoutMs, pollMs);
443
+ if (launchdLoaded(ops, id.launchd)) throw new Error(`launchd still has ${id.launchd} loaded after booting it out`);
444
+ return;
445
+ }
446
+ try {
447
+ systemctl(ops, "disable", "--now", id.systemd);
448
+ } catch {}
449
+ if (unitState(ops, id).running) throw new Error(`systemd still reports ${id.systemd} running after stopping it`);
450
+ }
451
+ /** Puts a stopped service's unit file back and returns it to the state it was in (see unitState). */
452
+ async function restoreUnit(ops, id, file, text, state, pollMs) {
453
+ writeFileSync(file, text, { mode: ops.platform === "darwin" ? 384 : 420 });
454
+ if (ops.platform === "linux") {
455
+ systemctl(ops, "daemon-reload");
456
+ if (state.enabled) systemctl(ops, "enable", id.systemd);
457
+ if (state.running) systemctl(ops, "start", id.systemd);
458
+ } else if (state.running) await bootstrapLaunchd(ops, id.launchd, file, { pollMs });
459
+ }
460
+ /** Fails before anything is touched when systemd can't be reached (e.g. no user bus over su). */
461
+ function preflight(ops) {
462
+ if (ops.platform !== "linux") return;
463
+ try {
464
+ systemctl(ops, "daemon-reload");
465
+ } catch (err) {
466
+ const hints = serviceHints(ops).map((h) => ` ${h}`).join("\n");
467
+ throw new Error(`Can't talk to systemd${ops.root ? "" : " (user session)"}: ${firstLine(err)}. Nothing was changed.\n${hints}`);
468
+ }
469
+ }
470
+ /** Whether host:port can't be bound because something already listens there. */
471
+ function portInUse(host, port) {
472
+ if (!port) return Promise.resolve(false);
473
+ return new Promise((done) => {
474
+ const probe = createServer();
475
+ probe.once("error", (err) => done(err.code === "EADDRINUSE"));
476
+ probe.listen({
477
+ host,
478
+ port,
479
+ exclusive: true
480
+ }, () => probe.close(() => done(false)));
481
+ });
482
+ }
483
+ /**
484
+ * The service manager's main pid for a service (default: the xaq daemon; the listener passes its
485
+ * own label and unit); null when it isn't running or can't be asked.
486
+ */
487
+ function serviceMainPid(ops, id = DAEMON_UNIT) {
488
+ try {
489
+ const out = ops.platform === "darwin" ? /^\s*pid = (\d+)/m.exec(ops.capture("launchctl", ["print", `${launchdTarget(ops)}/${id.launchd}`]))?.[1] ?? "" : ops.capture("systemctl", [
490
+ ...ops.root ? [] : ["--user"],
491
+ "show",
492
+ "-p",
493
+ "MainPID",
494
+ "--value",
495
+ id.systemd
496
+ ]).trim();
497
+ const pid = Number(out);
498
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
499
+ } catch {
500
+ return null;
501
+ }
502
+ }
503
+ /**
504
+ * Refuses (before anything is touched) while xaq already runs outside the service: in a
505
+ * terminal on this data directory (its instance lock), or anything on the port while the service
506
+ * isn't running. The service could only fail to start, and the health check would hear the other one.
507
+ * A lock holder is the service when its lock says so, or (early nightlies didn't write that flag)
508
+ * when, with a unit installed, it is the service manager's main process. `units` are the installed
509
+ * services (the new one and/or one from before the rename).
510
+ */
511
+ async function assertNothingElseRunning(config, ops, unitInstalled, units = [DAEMON_UNIT]) {
512
+ const other = ops.instance(config.dataDir);
513
+ const pids = unitInstalled ? units.map((id) => serviceMainPid(ops, id)).filter((p) => p !== null) : [];
514
+ if (other && !other.service && !pids.includes(other.pid)) throw new Error(`xaq is already running in a terminal (pid ${other.pid}${other.port ? `, port ${other.port}` : ""}): stop it with Ctrl-C, then run this again. The service uses the same data directory (${config.dataDir}), and only one xaq can use it at a time. Nothing was changed.`);
515
+ if (!other && !pids.length && await ops.portInUse(config.host, config.port)) throw new Error(`Port ${config.port} is already in use: by xaq (or agentinbox) running in a terminal with another --data directory, or by another program. Stop it (Ctrl-C in its terminal), or choose another port with --port. Nothing was changed.`);
516
+ }
517
+ /**
518
+ * After the health check answered: whether the data directory's lock holder is the new service's own
519
+ * process. It must be the service manager's main process (or, when that pid is unknown, say in its lock
520
+ * that it is the service), and not the process that held the lock before the install. This checks the
521
+ * lock holder, not who answered: xaq binds its port before taking the lock and exits when the
522
+ * port is taken, so a service that holds the lock is the one listening. Null when it is; else why not.
523
+ */
524
+ function notTheService(owner, before, mainPid) {
525
+ if (!owner) return "no xaq holds the data directory";
526
+ const same = before && owner.pid === before.pid && owner.startedAt === before.startedAt;
527
+ const isService = mainPid !== null ? owner.pid === mainPid : !!owner.service;
528
+ if (same || !isService) return `the xaq holding the data directory (pid ${owner.pid}) is not the service${mainPid ? ` (pid ${mainPid})` : ""}`;
529
+ return null;
530
+ }
531
+ /**
532
+ * A daemon service installed under the names from before the rename (agentinbox), if any: its unit file,
533
+ * the file's content, and the data directory it runs on.
534
+ */
535
+ function legacyDaemonUnit(ops) {
536
+ if (ops.platform !== "linux" && ops.platform !== "darwin") return null;
537
+ const file = unitPath(ops, LEGACY_DAEMON_UNIT);
538
+ const text = readText(file);
539
+ if (text === null) return null;
540
+ const rawPort = unitEnv(text, "PORT");
541
+ const port = rawPort && /^\d+$/.test(rawPort) && Number(rawPort) <= 65535 ? Number(rawPort) : null;
542
+ return {
543
+ file,
544
+ text,
545
+ dataDir: unitDataDir(text),
546
+ port,
547
+ host: unitEnv(text, "HOST") || null
548
+ };
549
+ }
550
+ /**
551
+ * Where `service install` puts the service when the command line and environment don't say: replacing a
552
+ * service from before the rename keeps its data directory, port and host (a bind to 127.0.0.1 must not
553
+ * widen to 0.0.0.0 on upgrade). A service already moved keeps ~/.agentinbox too, even if a ~/.xaq has
554
+ * appeared since. `given` is what the flags (and PORT / HOST / XAQ_DATA) said.
555
+ */
556
+ function serviceInstallInput(given, ops, env = process.env) {
557
+ const legacy = legacyDaemonUnit(ops);
558
+ const installed = installedDaemonDataDir(ops);
559
+ return {
560
+ dataDir: given.dataDir ?? (envVar("DATA", env) || void 0) ?? legacy?.dataDir ?? (installed && installed === legacyDataDir() ? installed : void 0) ?? void 0,
561
+ port: given.port ?? (env.PORT?.trim() ? void 0 : legacy?.port ?? void 0),
562
+ host: given.host ?? (env.HOST ? void 0 : legacy?.host ?? void 0)
563
+ };
564
+ }
565
+ /** The data directory the installed (xaq) daemon service runs on, if one is installed and says. */
566
+ function installedDaemonDataDir(ops) {
567
+ if (ops.platform !== "linux" && ops.platform !== "darwin") return null;
568
+ const text = readText(unitPath(ops, DAEMON_UNIT));
569
+ return text === null ? null : unitDataDir(text);
570
+ }
571
+ async function installService(config, ops = systemOps(), opts = {}) {
572
+ assertSupported(ops);
573
+ if (ops.platform === "linux" && /\s/.test(config.dataDir)) throw new Error(`The data directory "${config.dataDir}" contains whitespace, which systemd can't load settings from. Choose another one with --data.`);
574
+ const extraEnv = mergeServiceEnv(readPreviousEnv(config.dataDir), process.env);
575
+ validateMasterKey(extraEnv.XAQ_MASTER_KEY);
576
+ assertMasterKeyUsable(config, extraEnv.XAQ_MASTER_KEY ?? "");
577
+ preflight(ops);
578
+ const file = unitFile(ops);
579
+ const previousUnit = readText(file);
580
+ const legacy = legacyDaemonUnit(ops);
581
+ const legacyState = legacy ? unitState(ops, LEGACY_DAEMON_UNIT) : null;
582
+ await assertNothingElseRunning(config, ops, previousUnit !== null || legacy !== null, [DAEMON_UNIT, ...legacy ? [LEGACY_DAEMON_UNIT] : []]);
583
+ const before = ops.instance(config.dataDir);
584
+ mkdirSync(config.dataDir, {
585
+ recursive: true,
586
+ mode: 448
587
+ });
588
+ const staging = ops.stage(config.dataDir);
589
+ const previousEnv = readText(serviceEnvPath(config.dataDir));
590
+ const app = join(config.dataDir, "app");
591
+ const entry = installedEntry(app);
592
+ const spec = {
593
+ execPath: process.execPath,
594
+ entry,
595
+ dataDir: config.dataDir,
596
+ port: config.port,
597
+ host: config.host,
598
+ extraEnv,
599
+ system: ops.root
600
+ };
601
+ let swap = null;
602
+ try {
603
+ if (legacy) {
604
+ console.log(` Replacing the ${LEGACY_PRODUCT} service (${unitLabel(ops, LEGACY_DAEMON_UNIT)}) with ${unitLabel(ops, DAEMON_UNIT)}…`);
605
+ await stopAndWait(ops, LEGACY_DAEMON_UNIT, { pollMs: opts.launchdPollMs });
606
+ rmSync(legacy.file, { force: true });
607
+ if (ops.platform === "linux") systemctl(ops, "daemon-reload");
608
+ }
609
+ uninstallService(config, ops, {
610
+ quiet: true,
611
+ keepApp: true
612
+ });
613
+ swap = swapIn(staging, app);
614
+ writeServiceEnv(config.dataDir, extraEnv);
615
+ mkdirSync(ops.unitDir(), { recursive: true });
616
+ if (ops.platform === "linux") {
617
+ writeFileSync(file, renderSystemdUnit(spec));
618
+ systemctl(ops, "daemon-reload");
619
+ systemctl(ops, "enable", "--now", "xaq");
620
+ } else {
621
+ writeFileSync(file, renderLaunchdPlist(spec), { mode: 384 });
622
+ await bootstrapLaunchd(ops, LAUNCHD_LABEL, file, { pollMs: opts.launchdPollMs });
623
+ }
624
+ if (!await ops.waitReady(config)) throw new Error(`it did not answer on port ${config.port}`);
625
+ const wrong = notTheService(ops.instance(config.dataDir), before, serviceMainPid(ops));
626
+ if (wrong) throw new Error(wrong);
627
+ } catch (err) {
628
+ await rollback();
629
+ const hints = serviceHints(ops, process.env, legacy && previousUnit === null ? LEGACY_DAEMON_UNIT : DAEMON_UNIT).map((h) => ` ${h}`).join("\n");
630
+ const restored = legacy ? `the ${LEGACY_PRODUCT} service as it was` : previousUnit ? "the previous install" : "no service";
631
+ throw new Error(`Starting the service failed (${firstLine(err)}). Rolled back to ${restored}.\n To find out why:\n${hints}`);
632
+ }
633
+ swap.commit();
634
+ async function rollback() {
635
+ const quietly = async (fn) => {
636
+ try {
637
+ await fn();
638
+ } catch {}
639
+ };
640
+ if (legacy) await quietly(() => stopAndWait(ops, DAEMON_UNIT, { pollMs: opts.launchdPollMs }));
641
+ await quietly(() => uninstallService(config, ops, {
642
+ quiet: true,
643
+ keepApp: true
644
+ }));
645
+ await quietly(() => rmSync(file, { force: true }));
646
+ if (ops.platform === "linux") await quietly(() => systemctl(ops, "daemon-reload"));
647
+ await quietly(() => rmSync(staging, {
648
+ recursive: true,
649
+ force: true
650
+ }));
651
+ if (swap) await quietly(() => swap.restore());
652
+ await quietly(() => previousEnv === null ? rmSync(serviceEnvPath(config.dataDir), { force: true }) : writeFileSync(serviceEnvPath(config.dataDir), previousEnv, { mode: 384 }));
653
+ if (previousUnit !== null) await quietly(async () => {
654
+ writeFileSync(file, previousUnit, { mode: ops.platform === "darwin" ? 384 : 420 });
655
+ if (ops.platform === "linux") {
656
+ systemctl(ops, "daemon-reload");
657
+ systemctl(ops, "enable", "--now", "xaq");
658
+ } else await bootstrapLaunchd(ops, LAUNCHD_LABEL, file, { pollMs: opts.launchdPollMs });
659
+ });
660
+ if (legacy) await quietly(() => restoreUnit(ops, LEGACY_DAEMON_UNIT, legacy.file, legacy.text, legacyState, opts.launchdPollMs));
661
+ }
662
+ enableLinger(ops);
663
+ const fromShell = (k) => k in process.env || (LEGACY_ENV_KEYS[k] ?? k) in process.env;
664
+ const kept = Object.keys(extraEnv).filter((k) => !fromShell(k));
665
+ console.log(`\n Installed xaq as a ${ops.platform === "linux" ? "systemd" : "launchd"} service (${unitLabel(ops, DAEMON_UNIT)}).`);
666
+ if (legacy) console.log(` It replaces the ${LEGACY_PRODUCT} service (${unitLabel(ops, LEGACY_DAEMON_UNIT)}), which was removed. Your data stays in ${config.dataDir}.`);
667
+ console.log(` It runs the Node.js at ${process.execPath} (v${process.versions.node}). If you remove or replace that Node, run \`${serviceInstallCommand(config)}\` again.`);
668
+ if (kept.length) console.log(` Kept ${kept.join(", ")} from the previous install.`);
669
+ printLinks(config);
670
+ }
671
+ /** Keeps user services running after logout and starts them at boot (systemd user units only). */
672
+ function enableLinger(ops) {
673
+ if (ops.platform !== "linux" || ops.root) return;
674
+ try {
675
+ ops.exec("loginctl", ["enable-linger", ops.user], { quiet: true });
676
+ } catch {
677
+ console.warn(` Could not enable lingering. Run \`sudo loginctl enable-linger ${ops.user}\` so the service starts at boot.`);
678
+ }
679
+ }
680
+ /** XAQ_DATA (AGENTINBOX_DATA in a unit from before the rename) from an installed unit or plist. */
681
+ function unitDataDir(text) {
682
+ return unitEnv(text, "XAQ_DATA") ?? unitEnv(text, "AGENTINBOX_DATA");
683
+ }
684
+ /** An environment variable a unit (`Environment="K=v"`) or plist (`<key>K</key><string>v</string>`) sets. */
685
+ function unitEnv(text, name) {
686
+ const systemd = new RegExp(`^Environment="${name}=((?:[^"\\\\]|\\\\.)*)"`, "m").exec(text);
687
+ if (systemd) return systemd[1].replace(/\\(.)/g, "$1").replace(/%%/g, "%");
688
+ const plist = new RegExp(`<key>${name}</key><string>([^<]*)</string>`).exec(text);
689
+ if (plist) return plist[1].replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
690
+ return null;
691
+ }
692
+ /**
693
+ * Stops and removes the service, under its own name or the one from before the rename. The data
694
+ * directory is the one the installed unit uses (falling back to `config.dataDir`), and only directories
695
+ * that really are the service's own install are deleted; nothing is created.
696
+ */
697
+ function uninstallService(config, ops = systemOps(), opts = {}) {
698
+ assertSupported(ops);
699
+ const units = [DAEMON_UNIT, LEGACY_DAEMON_UNIT].map((id) => ({
700
+ id,
701
+ file: unitPath(ops, id),
702
+ text: readText(unitPath(ops, id))
703
+ })).filter((u) => u.text !== null);
704
+ const dataDir = units.map((u) => unitDataDir(u.text)).find(Boolean) || config.dataDir;
705
+ if (!units.length && !opts.quiet) console.log("The xaq service is not installed.");
706
+ for (const u of units) {
707
+ if (ops.platform === "linux") {
708
+ try {
709
+ systemctl(ops, "disable", "--now", u.id.systemd);
710
+ } catch {}
711
+ rmSync(u.file);
712
+ systemctl(ops, "daemon-reload");
713
+ } else {
714
+ try {
715
+ ops.exec("launchctl", ["bootout", `${launchdTarget(ops)}/${u.id.launchd}`], { quiet: true });
716
+ } catch {}
717
+ rmSync(u.file);
718
+ }
719
+ if (!opts.quiet) console.log(`Removed the ${unitLabel(ops, u.id)} service. Your data in ${dataDir} was kept.`);
720
+ }
721
+ if (!opts.keepApp) removeOwnInstall(dataDir);
722
+ }
723
+ function serviceStatus(ops = systemOps()) {
724
+ assertSupported(ops);
725
+ const id = existsSync(unitFile(ops)) ? DAEMON_UNIT : existsSync(unitPath(ops, LEGACY_DAEMON_UNIT)) ? LEGACY_DAEMON_UNIT : null;
726
+ if (!id) {
727
+ console.log(`The xaq service is not installed. Install it with \`${cliCommand("service install")}\`.`);
728
+ process.exitCode = 1;
729
+ return;
730
+ }
731
+ if (id === LEGACY_DAEMON_UNIT) console.log(`This is the service from before the rename to xaq (${unitLabel(ops, id)}). \`${cliCommand("service install")}\` moves it to ${unitLabel(ops, DAEMON_UNIT)}, keeping your data.\n`);
732
+ try {
733
+ if (ops.platform === "linux") systemctl(ops, "status", id.systemd, "--no-pager");
734
+ else ops.exec("launchctl", ["print", `${launchdTarget(ops)}/${id.launchd}`]);
735
+ } catch {
736
+ process.exitCode = 1;
737
+ }
738
+ }
739
+ /** A real HealthResponse (not the "starting" placeholder or something else on the port). */
740
+ function isHealthResponse(body) {
741
+ const b = body;
742
+ return !!b && (b.status === "ok" || b.status === "degraded") && typeof b.version === "string";
743
+ }
744
+ /** Polls /api/health for up to ~45 s. 503 with a HealthResponse means "up but degraded", which is up. */
745
+ async function waitForHealth(config) {
746
+ const base = loopbackUrl(config.host, config.port);
747
+ for (let i = 0; i < 90; i++) {
748
+ try {
749
+ const res = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(1e3) });
750
+ if ((res.status === 200 || res.status === 503) && isHealthResponse(await res.json().catch(() => null))) return true;
751
+ } catch {}
752
+ await new Promise((r) => setTimeout(r, 500));
753
+ }
754
+ return false;
755
+ }
756
+ function printLinks(config) {
757
+ const token = readSetupToken(config.dataDir);
758
+ console.log(` Running at:\n`);
759
+ for (const url of consoleUrls(config.host, config.port)) console.log(` ${url}${token ? `/setup?token=${token}` : ""}`);
760
+ if (token) console.log(`\n Open the link above to claim the instance and finish setup.`);
761
+ console.log();
762
+ }
763
+ //#endregion
764
+ export { serviceEnvPath as A, systemOps as B, preflight as C, renderSystemdUnit as D, renderLaunchdPlist as E, serviceStatus as F, unitPath as G, uninstallService as H, stageApp as I, validateMasterKey as J, unitState as K, stagePackage as L, serviceHints as M, serviceInstallInput as N, restoreUnit as O, serviceMainPid as P, stopAndWait as R, parseEnvFile as S, removeOwnInstall as T, unitDataDir as U, systemctl as V, unitEnv as W, xmlEscape as Y, launchdLoaded as _, LEGACY_PACKAGE as a, mergeServiceEnv as b, assertNothingElseRunning as c, commandFailure as d, enableLinger as f, isHealthResponse as g, installedEntry as h, LEGACY_LAUNCHD_LABEL as i, serviceEnvText as j, sdQuote as k, assertSupported as l, installedDaemonDataDir as m, LAUNCHD_LABEL as n, LEGACY_SERVICE_NAME as o, installService as p, userManagerConfigHome as q, LEGACY_DAEMON_UNIT as r, SERVICE_ENV_KEYS as s, DAEMON_UNIT as t, bootstrapLaunchd as u, launchdTarget as v, readText as w, notTheService as x, legacyDaemonUnit as y, swapIn as z };
765
+
766
+ //# sourceMappingURL=service-BVn3soZZ.js.map