@serfab/cadre-host 0.7.1

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 (300) hide show
  1. package/README.md +101 -0
  2. package/dist/auth/duration.d.ts +1 -0
  3. package/dist/auth/duration.js +32 -0
  4. package/dist/auth/duration.js.map +1 -0
  5. package/dist/auth/index.d.ts +9 -0
  6. package/dist/auth/index.js +8 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/auth/trust-circle-store.d.ts +26 -0
  9. package/dist/auth/trust-circle-store.js +129 -0
  10. package/dist/auth/trust-circle-store.js.map +1 -0
  11. package/dist/auth/trust-circle.d.ts +91 -0
  12. package/dist/auth/trust-circle.js +230 -0
  13. package/dist/auth/trust-circle.js.map +1 -0
  14. package/dist/auth/types.d.ts +81 -0
  15. package/dist/auth/types.js +24 -0
  16. package/dist/auth/types.js.map +1 -0
  17. package/dist/bin/host.d.ts +13 -0
  18. package/dist/bin/host.js +826 -0
  19. package/dist/bin/host.js.map +1 -0
  20. package/dist/index.d.ts +32 -0
  21. package/dist/index.js +27 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/installer/browser.d.ts +14 -0
  24. package/dist/installer/browser.js +35 -0
  25. package/dist/installer/browser.js.map +1 -0
  26. package/dist/installer/config.d.ts +43 -0
  27. package/dist/installer/config.js +113 -0
  28. package/dist/installer/config.js.map +1 -0
  29. package/dist/installer/identity.d.ts +23 -0
  30. package/dist/installer/identity.js +36 -0
  31. package/dist/installer/identity.js.map +1 -0
  32. package/dist/installer/index.d.ts +83 -0
  33. package/dist/installer/index.js +204 -0
  34. package/dist/installer/index.js.map +1 -0
  35. package/dist/installer/paths.d.ts +33 -0
  36. package/dist/installer/paths.js +84 -0
  37. package/dist/installer/paths.js.map +1 -0
  38. package/dist/installer/platform.d.ts +9 -0
  39. package/dist/installer/platform.js +21 -0
  40. package/dist/installer/platform.js.map +1 -0
  41. package/dist/installer/service-host/index.d.ts +11 -0
  42. package/dist/installer/service-host/index.js +16 -0
  43. package/dist/installer/service-host/index.js.map +1 -0
  44. package/dist/installer/service-host/launchd.d.ts +16 -0
  45. package/dist/installer/service-host/launchd.js +79 -0
  46. package/dist/installer/service-host/launchd.js.map +1 -0
  47. package/dist/installer/service-host/nssm.d.ts +20 -0
  48. package/dist/installer/service-host/nssm.js +88 -0
  49. package/dist/installer/service-host/nssm.js.map +1 -0
  50. package/dist/installer/service-host/systemd.d.ts +16 -0
  51. package/dist/installer/service-host/systemd.js +78 -0
  52. package/dist/installer/service-host/systemd.js.map +1 -0
  53. package/dist/installer/service-host/template.d.ts +6 -0
  54. package/dist/installer/service-host/template.js +20 -0
  55. package/dist/installer/service-host/template.js.map +1 -0
  56. package/dist/installer/service-host/types.d.ts +40 -0
  57. package/dist/installer/service-host/types.js +6 -0
  58. package/dist/installer/service-host/types.js.map +1 -0
  59. package/dist/installer/wizard.d.ts +31 -0
  60. package/dist/installer/wizard.js +67 -0
  61. package/dist/installer/wizard.js.map +1 -0
  62. package/dist/nat/address-resolver.d.ts +22 -0
  63. package/dist/nat/address-resolver.js +31 -0
  64. package/dist/nat/address-resolver.js.map +1 -0
  65. package/dist/nat/ddns/duckdns.d.ts +12 -0
  66. package/dist/nat/ddns/duckdns.js +57 -0
  67. package/dist/nat/ddns/duckdns.js.map +1 -0
  68. package/dist/nat/ddns/index.d.ts +30 -0
  69. package/dist/nat/ddns/index.js +21 -0
  70. package/dist/nat/ddns/index.js.map +1 -0
  71. package/dist/nat/ddns/updater.d.ts +68 -0
  72. package/dist/nat/ddns/updater.js +194 -0
  73. package/dist/nat/ddns/updater.js.map +1 -0
  74. package/dist/nat/external-ip.d.ts +62 -0
  75. package/dist/nat/external-ip.js +114 -0
  76. package/dist/nat/external-ip.js.map +1 -0
  77. package/dist/nat/index.d.ts +22 -0
  78. package/dist/nat/index.js +16 -0
  79. package/dist/nat/index.js.map +1 -0
  80. package/dist/nat/nat-service.d.ts +101 -0
  81. package/dist/nat/nat-service.js +284 -0
  82. package/dist/nat/nat-service.js.map +1 -0
  83. package/dist/nat/nat-store.d.ts +19 -0
  84. package/dist/nat/nat-store.js +136 -0
  85. package/dist/nat/nat-store.js.map +1 -0
  86. package/dist/nat/port-mapper.d.ts +98 -0
  87. package/dist/nat/port-mapper.js +226 -0
  88. package/dist/nat/port-mapper.js.map +1 -0
  89. package/dist/nat/reachability.d.ts +19 -0
  90. package/dist/nat/reachability.js +29 -0
  91. package/dist/nat/reachability.js.map +1 -0
  92. package/dist/nat/secrets/file-store.d.ts +23 -0
  93. package/dist/nat/secrets/file-store.js +102 -0
  94. package/dist/nat/secrets/file-store.js.map +1 -0
  95. package/dist/nat/secrets/index.d.ts +36 -0
  96. package/dist/nat/secrets/index.js +56 -0
  97. package/dist/nat/secrets/index.js.map +1 -0
  98. package/dist/nat/secrets/keytar-store.d.ts +18 -0
  99. package/dist/nat/secrets/keytar-store.js +52 -0
  100. package/dist/nat/secrets/keytar-store.js.map +1 -0
  101. package/dist/nat/types.d.ts +120 -0
  102. package/dist/nat/types.js +24 -0
  103. package/dist/nat/types.js.map +1 -0
  104. package/dist/orchestrator/host-process-orchestrator.d.ts +87 -0
  105. package/dist/orchestrator/host-process-orchestrator.js +557 -0
  106. package/dist/orchestrator/host-process-orchestrator.js.map +1 -0
  107. package/dist/orchestrator/index.d.ts +3 -0
  108. package/dist/orchestrator/index.js +2 -0
  109. package/dist/orchestrator/index.js.map +1 -0
  110. package/dist/orchestrator/log-rotator.d.ts +25 -0
  111. package/dist/orchestrator/log-rotator.js +56 -0
  112. package/dist/orchestrator/log-rotator.js.map +1 -0
  113. package/dist/orchestrator/pid-liveness.d.ts +11 -0
  114. package/dist/orchestrator/pid-liveness.js +26 -0
  115. package/dist/orchestrator/pid-liveness.js.map +1 -0
  116. package/dist/orchestrator/port-allocator.d.ts +16 -0
  117. package/dist/orchestrator/port-allocator.js +38 -0
  118. package/dist/orchestrator/port-allocator.js.map +1 -0
  119. package/dist/orchestrator/state-store.d.ts +32 -0
  120. package/dist/orchestrator/state-store.js +44 -0
  121. package/dist/orchestrator/state-store.js.map +1 -0
  122. package/dist/orchestrator/types.d.ts +85 -0
  123. package/dist/orchestrator/types.js +31 -0
  124. package/dist/orchestrator/types.js.map +1 -0
  125. package/dist/server/error-handler.d.ts +26 -0
  126. package/dist/server/error-handler.js +95 -0
  127. package/dist/server/error-handler.js.map +1 -0
  128. package/dist/server/events/bus.d.ts +17 -0
  129. package/dist/server/events/bus.js +36 -0
  130. package/dist/server/events/bus.js.map +1 -0
  131. package/dist/server/events/sse-route.d.ts +22 -0
  132. package/dist/server/events/sse-route.js +65 -0
  133. package/dist/server/events/sse-route.js.map +1 -0
  134. package/dist/server/events/types.d.ts +25 -0
  135. package/dist/server/events/types.js +8 -0
  136. package/dist/server/events/types.js.map +1 -0
  137. package/dist/server/index.d.ts +67 -0
  138. package/dist/server/index.js +149 -0
  139. package/dist/server/index.js.map +1 -0
  140. package/dist/server/origin-guard.d.ts +24 -0
  141. package/dist/server/origin-guard.js +72 -0
  142. package/dist/server/origin-guard.js.map +1 -0
  143. package/dist/server/routes/nat.d.ts +16 -0
  144. package/dist/server/routes/nat.js +42 -0
  145. package/dist/server/routes/nat.js.map +1 -0
  146. package/dist/server/routes/nodes.d.ts +23 -0
  147. package/dist/server/routes/nodes.js +128 -0
  148. package/dist/server/routes/nodes.js.map +1 -0
  149. package/dist/server/routes/settings.d.ts +26 -0
  150. package/dist/server/routes/settings.js +97 -0
  151. package/dist/server/routes/settings.js.map +1 -0
  152. package/dist/server/routes/status.d.ts +42 -0
  153. package/dist/server/routes/status.js +76 -0
  154. package/dist/server/routes/status.js.map +1 -0
  155. package/dist/server/routes/trust-circle.d.ts +17 -0
  156. package/dist/server/routes/trust-circle.js +35 -0
  157. package/dist/server/routes/trust-circle.js.map +1 -0
  158. package/dist/server/routes/update.d.ts +16 -0
  159. package/dist/server/routes/update.js +29 -0
  160. package/dist/server/routes/update.js.map +1 -0
  161. package/dist/server/server.d.ts +40 -0
  162. package/dist/server/server.js +90 -0
  163. package/dist/server/server.js.map +1 -0
  164. package/dist/server/settings-store.d.ts +24 -0
  165. package/dist/server/settings-store.js +27 -0
  166. package/dist/server/settings-store.js.map +1 -0
  167. package/dist/server/static.d.ts +25 -0
  168. package/dist/server/static.js +177 -0
  169. package/dist/server/static.js.map +1 -0
  170. package/dist/ui/assets/index-BwU5mJGF.css +1 -0
  171. package/dist/ui/assets/index-C9uChk2P.js +14 -0
  172. package/dist/ui/assets/index-C9uChk2P.js.map +1 -0
  173. package/dist/ui/index.html +14 -0
  174. package/dist/update/apply.d.ts +37 -0
  175. package/dist/update/apply.js +102 -0
  176. package/dist/update/apply.js.map +1 -0
  177. package/dist/update/index.d.ts +101 -0
  178. package/dist/update/index.js +228 -0
  179. package/dist/update/index.js.map +1 -0
  180. package/dist/update/manifest.d.ts +48 -0
  181. package/dist/update/manifest.js +152 -0
  182. package/dist/update/manifest.js.map +1 -0
  183. package/dist/update/release-key.d.ts +24 -0
  184. package/dist/update/release-key.js +52 -0
  185. package/dist/update/release-key.js.map +1 -0
  186. package/dist/update/store.d.ts +22 -0
  187. package/dist/update/store.js +81 -0
  188. package/dist/update/store.js.map +1 -0
  189. package/dist/update/types.d.ts +90 -0
  190. package/dist/update/types.js +22 -0
  191. package/dist/update/types.js.map +1 -0
  192. package/dist/update/version.d.ts +18 -0
  193. package/dist/update/version.js +76 -0
  194. package/dist/update/version.js.map +1 -0
  195. package/package.json +80 -0
  196. package/service/README.md +86 -0
  197. package/service/cadre-host.service.tmpl +16 -0
  198. package/service/com.serfab.cadre-host.plist.tmpl +35 -0
  199. package/service/install-service.ps1 +98 -0
  200. package/service/uninstall-service.ps1 +19 -0
  201. package/src/__tests__/cli-invite.smoke.test.ts +187 -0
  202. package/src/__tests__/cli-nat.smoke.test.ts +207 -0
  203. package/src/__tests__/cli.smoke.test.ts +41 -0
  204. package/src/__tests__/orchestrator.test.ts +570 -0
  205. package/src/auth/__tests__/duration.test.ts +28 -0
  206. package/src/auth/__tests__/trust-circle-integration.test.ts +128 -0
  207. package/src/auth/__tests__/trust-circle-store.test.ts +115 -0
  208. package/src/auth/__tests__/trust-circle.test.ts +334 -0
  209. package/src/auth/duration.ts +33 -0
  210. package/src/auth/index.ts +19 -0
  211. package/src/auth/trust-circle-store.ts +155 -0
  212. package/src/auth/trust-circle.ts +304 -0
  213. package/src/auth/types.ts +96 -0
  214. package/src/bin/host.ts +905 -0
  215. package/src/index.ts +167 -0
  216. package/src/installer/__tests__/config.test.ts +86 -0
  217. package/src/installer/__tests__/identity.test.ts +34 -0
  218. package/src/installer/__tests__/installer.smoke.test.ts +124 -0
  219. package/src/installer/__tests__/paths.test.ts +47 -0
  220. package/src/installer/__tests__/service-host.test.ts +62 -0
  221. package/src/installer/__tests__/wizard.test.ts +60 -0
  222. package/src/installer/browser.ts +42 -0
  223. package/src/installer/config.ts +153 -0
  224. package/src/installer/identity.ts +49 -0
  225. package/src/installer/index.ts +303 -0
  226. package/src/installer/paths.ts +109 -0
  227. package/src/installer/platform.ts +27 -0
  228. package/src/installer/service-host/index.ts +21 -0
  229. package/src/installer/service-host/launchd.ts +90 -0
  230. package/src/installer/service-host/nssm.ts +100 -0
  231. package/src/installer/service-host/systemd.ts +88 -0
  232. package/src/installer/service-host/template.ts +21 -0
  233. package/src/installer/service-host/types.ts +48 -0
  234. package/src/installer/wizard.ts +92 -0
  235. package/src/nat/__tests__/address-resolver.test.ts +82 -0
  236. package/src/nat/__tests__/ddns-duckdns.test.ts +83 -0
  237. package/src/nat/__tests__/ddns-updater.test.ts +253 -0
  238. package/src/nat/__tests__/external-ip.test.ts +124 -0
  239. package/src/nat/__tests__/nat-service.test.ts +305 -0
  240. package/src/nat/__tests__/nat-store.test.ts +93 -0
  241. package/src/nat/__tests__/port-mapper.test.ts +191 -0
  242. package/src/nat/__tests__/reachability.test.ts +24 -0
  243. package/src/nat/__tests__/secrets.test.ts +156 -0
  244. package/src/nat/address-resolver.ts +51 -0
  245. package/src/nat/ddns/duckdns.ts +76 -0
  246. package/src/nat/ddns/index.ts +55 -0
  247. package/src/nat/ddns/updater.ts +243 -0
  248. package/src/nat/external-ip.ts +154 -0
  249. package/src/nat/index.ts +71 -0
  250. package/src/nat/nat-service.ts +375 -0
  251. package/src/nat/nat-store.ts +154 -0
  252. package/src/nat/port-mapper.ts +329 -0
  253. package/src/nat/reachability.ts +34 -0
  254. package/src/nat/secrets/file-store.ts +130 -0
  255. package/src/nat/secrets/index.ts +82 -0
  256. package/src/nat/secrets/keytar-store.ts +67 -0
  257. package/src/nat/types.ts +157 -0
  258. package/src/orchestrator/host-process-orchestrator.ts +626 -0
  259. package/src/orchestrator/index.ts +3 -0
  260. package/src/orchestrator/log-rotator.ts +53 -0
  261. package/src/orchestrator/pid-liveness.ts +24 -0
  262. package/src/orchestrator/port-allocator.ts +43 -0
  263. package/src/orchestrator/state-store.ts +72 -0
  264. package/src/orchestrator/types.ts +95 -0
  265. package/src/server/__tests__/bus.test.ts +38 -0
  266. package/src/server/__tests__/error-handler.test.ts +96 -0
  267. package/src/server/__tests__/nodes-route.test.ts +179 -0
  268. package/src/server/__tests__/origin-guard.test.ts +72 -0
  269. package/src/server/__tests__/publishers.test.ts +216 -0
  270. package/src/server/__tests__/server.smoke.test.ts +178 -0
  271. package/src/server/__tests__/settings-route.test.ts +177 -0
  272. package/src/server/__tests__/sse-route.test.ts +119 -0
  273. package/src/server/__tests__/status-route.test.ts +113 -0
  274. package/src/server/error-handler.ts +108 -0
  275. package/src/server/events/bus.ts +42 -0
  276. package/src/server/events/sse-route.ts +81 -0
  277. package/src/server/events/types.ts +22 -0
  278. package/src/server/index.ts +209 -0
  279. package/src/server/origin-guard.ts +84 -0
  280. package/src/server/routes/nat.ts +63 -0
  281. package/src/server/routes/nodes.ts +147 -0
  282. package/src/server/routes/settings.ts +115 -0
  283. package/src/server/routes/status.ts +108 -0
  284. package/src/server/routes/trust-circle.ts +49 -0
  285. package/src/server/routes/update.ts +43 -0
  286. package/src/server/server.ts +122 -0
  287. package/src/server/settings-store.ts +37 -0
  288. package/src/server/static.ts +191 -0
  289. package/src/update/__tests__/apply.test.ts +101 -0
  290. package/src/update/__tests__/manifest.test.ts +93 -0
  291. package/src/update/__tests__/store.test.ts +81 -0
  292. package/src/update/__tests__/update-service.integration.test.ts +221 -0
  293. package/src/update/__tests__/version.test.ts +31 -0
  294. package/src/update/apply.ts +148 -0
  295. package/src/update/index.ts +303 -0
  296. package/src/update/manifest.ts +172 -0
  297. package/src/update/release-key.ts +55 -0
  298. package/src/update/store.ts +101 -0
  299. package/src/update/types.ts +109 -0
  300. package/src/update/version.ts +80 -0
@@ -0,0 +1,826 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entrypoint for cadre-host — the self-hosted cadre node manager.
4
+ *
5
+ * Most subcommands are still stubs at this stage (real implementations land
6
+ * in the sibling tickets cadre-host-nat, cadre-host-installer,
7
+ * cadre-host-local-ui).
8
+ *
9
+ * The `invite` and `trust` commands talk to the running cadre-host
10
+ * management API over loopback. They are thin HTTP clients — they don't
11
+ * spin up an inline service, so cadre-host must be running.
12
+ */
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { createRequire } from 'node:module';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, join, resolve } from 'node:path';
17
+ import { Command } from 'commander';
18
+ import { parseDuration } from '../auth/duration.js';
19
+ import { Installer } from '../installer/index.js';
20
+ import { readHostConfig, updateHostConfig } from '../installer/config.js';
21
+ import { configPath as resolveConfigPath, defaultDataDir, defaultHostJs, defaultServiceDir, identityPath as resolveIdentityPath, } from '../installer/paths.js';
22
+ import { detectPlatform } from '../installer/platform.js';
23
+ import { createServiceHost } from '../installer/service-host/index.js';
24
+ import { UpdateService } from '../update/index.js';
25
+ import { HostProcessOrchestrator } from '../orchestrator/index.js';
26
+ import { TrustCircleService, TrustCircleStore } from '../auth/index.js';
27
+ import { NatService } from '../nat/index.js';
28
+ import { createLocalUiServer, HostSettingsStore } from '../server/index.js';
29
+ import { openBrowser } from '../installer/browser.js';
30
+ const DEFAULT_PORT = Number(process.env.CADRE_HOST_PORT ?? '8765');
31
+ /** libp2p peer-ID prefixes by key type: Ed25519, secp256k1, legacy RSA. */
32
+ const PEER_ID_PREFIXES = ['12D3Koo', '16Uiu2HAm', 'Qm'];
33
+ function looksLikePeerId(id) {
34
+ return PEER_ID_PREFIXES.some((p) => id.startsWith(p));
35
+ }
36
+ function resolvePort(raw) {
37
+ const n = Number(raw);
38
+ if (!Number.isFinite(n) || n <= 0 || n > 65535) {
39
+ // eslint-disable-next-line no-console
40
+ console.error(`Invalid --port: ${raw}`);
41
+ process.exit(1);
42
+ }
43
+ return n;
44
+ }
45
+ const program = new Command();
46
+ program
47
+ .name('cadre-host')
48
+ .description('Sereus cadre node manager for self-hosted basement-PC deployments')
49
+ .version('0.6.0');
50
+ // ============================================================================
51
+ // install — run the first-run wizard + register the system service
52
+ // ============================================================================
53
+ program
54
+ .command('install')
55
+ .description('Install cadre-host as a system service and run first-run setup')
56
+ .option('--non-interactive', 'Use defaults / CLI flags only; never prompt')
57
+ .option('--data-dir <path>', 'Override the data directory')
58
+ .option('--ui-port <port>', 'Override the management UI port', parseIntArg)
59
+ .option('--libp2p-port <port>', 'Override the cadre libp2p port', parseIntArg)
60
+ .option('--no-upnp', 'Disable UPnP/NAT-PMP probing on first run')
61
+ .option('--no-browser', 'Do not open a browser after install')
62
+ .option('--no-invite', 'Skip generating the first enrollment invite')
63
+ .option('--system', 'System-wide install (not yet supported in v1)')
64
+ .option('--node-path <path>', 'Override the node binary embedded in the service unit')
65
+ .action(async (opts) => {
66
+ const installer = new Installer();
67
+ try {
68
+ const result = await installer.install({
69
+ nonInteractive: Boolean(opts.nonInteractive),
70
+ ...(opts.dataDir ? { dataDir: opts.dataDir } : {}),
71
+ ...(typeof opts.uiPort === 'number' ? { uiPort: opts.uiPort } : {}),
72
+ ...(typeof opts.libp2pPort === 'number' ? { libp2pPort: opts.libp2pPort } : {}),
73
+ noUpnp: opts.upnp === false,
74
+ openBrowser: opts.browser !== false,
75
+ noInvite: opts.invite === false,
76
+ system: Boolean(opts.system),
77
+ ...(opts.nodePath ? { nodePath: opts.nodePath } : {}),
78
+ });
79
+ // eslint-disable-next-line no-console
80
+ console.log(`cadre-host installed.`);
81
+ // eslint-disable-next-line no-console
82
+ console.log(` Data dir: ${result.dataDir}`);
83
+ // eslint-disable-next-line no-console
84
+ console.log(` UI: ${result.uiUrl}`);
85
+ // eslint-disable-next-line no-console
86
+ console.log(` Service: ${result.serviceName}`);
87
+ if (result.enrollmentInvite) {
88
+ printEnrollmentInvite(result.enrollmentInvite);
89
+ }
90
+ process.exit(0);
91
+ }
92
+ catch (err) {
93
+ // eslint-disable-next-line no-console
94
+ console.error(`install failed: ${err.message}`);
95
+ process.exit(1);
96
+ }
97
+ });
98
+ // ============================================================================
99
+ // uninstall — stop + deregister the system service
100
+ // ============================================================================
101
+ program
102
+ .command('uninstall')
103
+ .description('Stop and uninstall the cadre-host service')
104
+ .option('--yes', 'Skip the confirmation prompt (for scripts)')
105
+ .option('--remove-data', 'Also remove the data directory (default: preserve)')
106
+ .option('--data-dir <path>', 'Override the data directory to clean up')
107
+ .action(async (opts) => {
108
+ const installer = new Installer();
109
+ try {
110
+ // --remove-data is destructive and irreversible (identity + trust circle
111
+ // are wiped). Require explicit --yes when stdin isn't a TTY, and prompt
112
+ // confirmation when it is.
113
+ if (opts.removeData && !opts.yes) {
114
+ if (!process.stdin.isTTY) {
115
+ console.error('uninstall: --remove-data requires --yes when stdin is not a TTY.');
116
+ process.exit(1);
117
+ return;
118
+ }
119
+ const dataDir = opts.dataDir ?? '(default data directory)';
120
+ const confirmed = await confirmDestructive(`This will permanently delete ${dataDir} (identity, trust circle, NAT state). Continue? [y/N] `);
121
+ if (!confirmed) {
122
+ console.error('uninstall aborted.');
123
+ process.exit(1);
124
+ return;
125
+ }
126
+ }
127
+ await installer.uninstall({
128
+ yes: Boolean(opts.yes),
129
+ removeData: Boolean(opts.removeData),
130
+ ...(opts.dataDir ? { dataDir: opts.dataDir } : {}),
131
+ });
132
+ // eslint-disable-next-line no-console
133
+ console.log('cadre-host uninstalled.');
134
+ process.exit(0);
135
+ }
136
+ catch (err) {
137
+ // eslint-disable-next-line no-console
138
+ console.error(`uninstall failed: ${err.message}`);
139
+ process.exit(1);
140
+ }
141
+ });
142
+ // ============================================================================
143
+ // status — show service-host registration / running state
144
+ // ============================================================================
145
+ program
146
+ .command('status')
147
+ .description('Show running status of cadre-host and the cadre nodes it manages')
148
+ .action(async () => {
149
+ const installer = new Installer();
150
+ try {
151
+ const status = await installer.status();
152
+ // eslint-disable-next-line no-console
153
+ console.log(`Service installed: ${status.installed ? 'yes' : 'no'}`);
154
+ // eslint-disable-next-line no-console
155
+ console.log(`Service running: ${status.running ? 'yes' : 'no'}`);
156
+ process.exit(0);
157
+ }
158
+ catch (err) {
159
+ // eslint-disable-next-line no-console
160
+ console.error(`status failed: ${err.message}`);
161
+ process.exit(1);
162
+ }
163
+ });
164
+ // ============================================================================
165
+ // start — load config + identity, bind management API listener, wait
166
+ // ============================================================================
167
+ program
168
+ .command('start')
169
+ .description('Start cadre-host in the foreground')
170
+ .option('--data-dir <path>', 'Override the data directory (env: CADRE_HOST_DATA_DIR)')
171
+ .option('--no-tui', 'Reserved — currently a no-op (no TUI implemented yet)')
172
+ .action(async (opts) => {
173
+ try {
174
+ void opts.tui;
175
+ const platform = detectPlatform();
176
+ const dataDir = opts.dataDir ?? process.env.CADRE_HOST_DATA_DIR ?? defaultDataDir(platform);
177
+ const cfgPath = resolveConfigPath(dataDir);
178
+ if (!existsSync(cfgPath)) {
179
+ // eslint-disable-next-line no-console
180
+ console.error(`cadre-host start: ${cfgPath} not found. ` +
181
+ `Run \`cadre-host install\` first (or pass --data-dir to an existing install).`);
182
+ process.exit(1);
183
+ return;
184
+ }
185
+ const cfg = readHostConfig(cfgPath);
186
+ const idPath = resolveIdentityPath(cfg.dataDir);
187
+ if (!existsSync(idPath)) {
188
+ // eslint-disable-next-line no-console
189
+ console.error(`cadre-host start: identity file not found at ${idPath}. Re-run \`cadre-host install\`.`);
190
+ process.exit(1);
191
+ return;
192
+ }
193
+ // eslint-disable-next-line no-console
194
+ console.log(`cadre-host starting (dataDir=${cfg.dataDir}, uiPort=${cfg.uiPort})`);
195
+ // Update flow: notify-by-default; auto-apply opt-in via host.config.json.
196
+ // The local-UI ticket (6.5) wires the handlers into HTTP routes; here we
197
+ // just construct the service so the in-process timer + state file are
198
+ // populated.
199
+ const serviceHost = createServiceHost(platform);
200
+ const updateService = new UpdateService({
201
+ dataDir: cfg.dataDir,
202
+ currentVersion: readPackageVersionForStart(),
203
+ settings: cfg.updates,
204
+ ...(cfg.updates.manifestUrl ? { manifestUrl: cfg.updates.manifestUrl } : {}),
205
+ restart: async () => {
206
+ try {
207
+ await serviceHost.restart({
208
+ nodePath: process.execPath,
209
+ hostJs: defaultHostJs(),
210
+ dataDir: cfg.dataDir,
211
+ serviceDir: defaultServiceDir(),
212
+ });
213
+ return undefined;
214
+ }
215
+ catch (err) {
216
+ return err.message;
217
+ }
218
+ },
219
+ });
220
+ // Persist settings changes back to host.config.json on PUT /update/settings.
221
+ updateService.onSettingsChanged((next) => {
222
+ try {
223
+ updateHostConfig(cfgPath, { updates: next });
224
+ }
225
+ catch (err) {
226
+ // eslint-disable-next-line no-console
227
+ console.error(`failed to persist updated settings to ${cfgPath}: ${err.message}`);
228
+ }
229
+ });
230
+ // Don't block startup on the network round-trip — call and forget.
231
+ void updateService.check();
232
+ updateService.start();
233
+ // Wire the long-lived HTTP management server. The trust-circle and
234
+ // NAT services depend on a libp2p `CadreNodeLike`; v1 has no node-
235
+ // spawning path yet (that's a follow-up ticket), so we inject minimal
236
+ // stubs that fail loudly if a real call is attempted. The /auth/* and
237
+ // /nat/* routes are still mounted so the CLI's existing HTTP contract
238
+ // is intact — operations that genuinely need the libp2p node will
239
+ // surface a NatError / TrustCircleError, mapped to 500 by the server.
240
+ const orchestrator = new HostProcessOrchestrator({ rootDir: join(cfg.dataDir, 'orchestrator') });
241
+ await orchestrator.init();
242
+ const trustCircle = new TrustCircleService({
243
+ cadreNode: missingCadreNodeStub('trust-circle'),
244
+ store: new TrustCircleStore(cfg.dataDir),
245
+ });
246
+ const natService = new NatService({
247
+ rootDir: cfg.dataDir,
248
+ cadreNode: missingNatNodeStub(),
249
+ });
250
+ // Best-effort NAT start. Failures here aren't fatal — the local UI
251
+ // can still serve the trust circle, settings, etc.
252
+ try {
253
+ await natService.start();
254
+ }
255
+ catch (err) {
256
+ // eslint-disable-next-line no-console
257
+ console.error(`NAT start failed: ${err.message}`);
258
+ }
259
+ const settingsStore = new HostSettingsStore({ dataDir: cfg.dataDir });
260
+ const server = createLocalUiServer({
261
+ uiPort: cfg.uiPort,
262
+ dataDir: cfg.dataDir,
263
+ orchestrator,
264
+ trustCircle,
265
+ nat: natService,
266
+ update: updateService,
267
+ settingsStore,
268
+ });
269
+ const { url, port } = await server.start();
270
+ if (port !== cfg.uiPort) {
271
+ // eslint-disable-next-line no-console
272
+ console.log(`(configured port ${cfg.uiPort} was in use — bound on ${port} instead)`);
273
+ }
274
+ // eslint-disable-next-line no-console
275
+ console.log(`cadre-host local UI: ${url}`);
276
+ await waitForTermination();
277
+ try {
278
+ await server.stop();
279
+ }
280
+ catch { /* ignore */ }
281
+ try {
282
+ await natService.stop();
283
+ }
284
+ catch { /* ignore */ }
285
+ updateService.stop();
286
+ // eslint-disable-next-line no-console
287
+ console.log('cadre-host stopped.');
288
+ process.exit(0);
289
+ }
290
+ catch (err) {
291
+ // eslint-disable-next-line no-console
292
+ console.error(`start failed: ${err.message}`);
293
+ process.exit(1);
294
+ }
295
+ });
296
+ // ============================================================================
297
+ // ui — print the local-UI URL and open it in a browser
298
+ // ============================================================================
299
+ program
300
+ .command('ui')
301
+ .description('Print the local-UI URL and open it in the default browser')
302
+ .option('--data-dir <path>', 'Override the data directory (env: CADRE_HOST_DATA_DIR)')
303
+ .option('--no-browser', 'Print the URL but do not open a browser')
304
+ .action((opts) => {
305
+ try {
306
+ const platform = detectPlatform();
307
+ const dataDir = opts.dataDir ?? process.env.CADRE_HOST_DATA_DIR ?? defaultDataDir(platform);
308
+ const cfgPath = resolveConfigPath(dataDir);
309
+ if (!existsSync(cfgPath)) {
310
+ // eslint-disable-next-line no-console
311
+ console.error(`cadre-host ui: ${cfgPath} not found. Run \`cadre-host install\` first.`);
312
+ process.exit(1);
313
+ return;
314
+ }
315
+ const cfg = readHostConfig(cfgPath);
316
+ const url = `http://127.0.0.1:${cfg.uiPort}`;
317
+ // eslint-disable-next-line no-console
318
+ console.log(url);
319
+ if (opts.browser !== false) {
320
+ const res = openBrowser(url);
321
+ if (res.spawned) {
322
+ // eslint-disable-next-line no-console
323
+ console.log('(Opening in your default browser...)');
324
+ }
325
+ else if (res.error) {
326
+ // eslint-disable-next-line no-console
327
+ console.error(`(unable to launch browser: ${res.error})`);
328
+ }
329
+ }
330
+ process.exit(0);
331
+ }
332
+ catch (err) {
333
+ // eslint-disable-next-line no-console
334
+ console.error(`ui failed: ${err.message}`);
335
+ process.exit(1);
336
+ }
337
+ });
338
+ function parseIntArg(raw) {
339
+ const n = Number(raw);
340
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
341
+ throw new Error(`Invalid integer: ${raw}`);
342
+ }
343
+ return n;
344
+ }
345
+ function readPackageVersionForStart() {
346
+ try {
347
+ // dist/bin/host.js -> ../../package.json
348
+ const here = dirname(fileURLToPath(import.meta.url));
349
+ const pkgPath = resolve(here, '..', '..', 'package.json');
350
+ const raw = readFileSync(pkgPath, 'utf8');
351
+ const parsed = JSON.parse(raw);
352
+ return parsed.version ?? '0.0.0-unknown';
353
+ }
354
+ catch {
355
+ return '0.0.0-unknown';
356
+ }
357
+ }
358
+ /**
359
+ * Returns a `CadreNodeLike` for TrustCircleService that fails loudly on any
360
+ * libp2p call. cadre-host v1 doesn't yet spawn the host's own cadre node —
361
+ * that ticket is separate. The trust-circle HTTP endpoints still load the
362
+ * local labels/pending file (listing works), but issuing/redeeming an invite
363
+ * requires a real node and surfaces a 500.
364
+ */
365
+ function missingCadreNodeStub(name) {
366
+ return {
367
+ async createInvite() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot issue invite`); },
368
+ async acceptPhone() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot redeem`); },
369
+ async removePeer() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot remove peer`); },
370
+ encodeInvite() { throw new Error(`${name}: cadre-host v1 has no inline cadre node — cannot encode invite`); },
371
+ getControlDatabase() { return null; },
372
+ };
373
+ }
374
+ function missingNatNodeStub() {
375
+ return {
376
+ getPeerId() { return ''; },
377
+ getMultiaddrs() { return []; },
378
+ };
379
+ }
380
+ function waitForTermination() {
381
+ return new Promise((resolveTerm) => {
382
+ const done = () => resolveTerm();
383
+ process.once('SIGINT', done);
384
+ process.once('SIGTERM', done);
385
+ });
386
+ }
387
+ async function confirmDestructive(message) {
388
+ const { createInterface } = await import('node:readline');
389
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
390
+ try {
391
+ const answer = await new Promise((res) => rl.question(message, res));
392
+ return /^(y|yes)$/i.test(answer.trim());
393
+ }
394
+ finally {
395
+ rl.close();
396
+ }
397
+ }
398
+ const requireForQr = createRequire(import.meta.url);
399
+ function printEnrollmentInvite(invite) {
400
+ // Best-effort QR render — fall back to the bare token if the lib chokes.
401
+ // eslint-disable-next-line no-console
402
+ console.log('\nEnroll your first device with this invite:');
403
+ try {
404
+ const qr = requireForQr('qrcode-terminal');
405
+ qr.generate(invite.encodedInvite, { small: true }, (rendered) => {
406
+ // eslint-disable-next-line no-console
407
+ console.log(rendered);
408
+ });
409
+ }
410
+ catch (err) {
411
+ // eslint-disable-next-line no-console
412
+ console.error(`(qrcode-terminal unavailable: ${err.message})`);
413
+ }
414
+ // eslint-disable-next-line no-console
415
+ console.log(` ${invite.encodedInvite}`);
416
+ if (invite.expiresAt) {
417
+ // eslint-disable-next-line no-console
418
+ console.log(` (expires ${invite.expiresAt})`);
419
+ }
420
+ }
421
+ program
422
+ .command('invite')
423
+ .description('Generate an invite to add a member to the trust circle')
424
+ .argument('<label>', 'Display label for the new member (e.g. "Mom\'s phone")')
425
+ .option('--ttl <duration>', 'Invite lifetime (e.g. 24h, 7d, 30m)', '24h')
426
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
427
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
428
+ .action(async (label, opts) => {
429
+ let ttlMs;
430
+ try {
431
+ ttlMs = parseDuration(opts.ttl);
432
+ }
433
+ catch (err) {
434
+ // eslint-disable-next-line no-console
435
+ console.error(`Invalid --ttl: ${err.message}`);
436
+ process.exit(1);
437
+ return;
438
+ }
439
+ const port = resolvePort(opts.port);
440
+ const url = `http://${opts.host}:${port}/auth/invites`;
441
+ let response;
442
+ try {
443
+ response = await fetch(url, {
444
+ method: 'POST',
445
+ headers: { 'content-type': 'application/json' },
446
+ body: JSON.stringify({ label, ttlMs }),
447
+ });
448
+ }
449
+ catch (err) {
450
+ // eslint-disable-next-line no-console
451
+ console.error(`Failed to reach cadre-host at ${url}: ${err.message}\n` +
452
+ `Hint: is cadre-host running? Try \`cadre-host start\`.`);
453
+ process.exit(2);
454
+ return;
455
+ }
456
+ if (!response.ok) {
457
+ const text = await response.text().catch(() => '');
458
+ // eslint-disable-next-line no-console
459
+ console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
460
+ process.exit(1);
461
+ return;
462
+ }
463
+ const body = await response.json();
464
+ if (!body.encodedInvite) {
465
+ // eslint-disable-next-line no-console
466
+ console.error('cadre-host returned malformed response (missing encodedInvite)');
467
+ process.exit(1);
468
+ return;
469
+ }
470
+ // eslint-disable-next-line no-console
471
+ console.log(body.encodedInvite);
472
+ if (body.expiresAt) {
473
+ // eslint-disable-next-line no-console
474
+ console.error(`(expires at ${body.expiresAt})`);
475
+ }
476
+ process.exit(0);
477
+ });
478
+ const trust = program
479
+ .command('trust')
480
+ .description('Manage the trust circle (list members, revoke invites/members)');
481
+ trust
482
+ .command('list')
483
+ .description('List trust-circle members and pending invites')
484
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
485
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
486
+ .action(async (opts) => {
487
+ const url = `http://${opts.host}:${resolvePort(opts.port)}/auth/trust-circle`;
488
+ let response;
489
+ try {
490
+ response = await fetch(url);
491
+ }
492
+ catch (err) {
493
+ // eslint-disable-next-line no-console
494
+ console.error(`Failed to reach cadre-host at ${url}: ${err.message}`);
495
+ process.exit(2);
496
+ return;
497
+ }
498
+ if (!response.ok) {
499
+ // eslint-disable-next-line no-console
500
+ console.error(`cadre-host returned ${response.status}: ${response.statusText}`);
501
+ process.exit(1);
502
+ return;
503
+ }
504
+ const body = await response.json();
505
+ // eslint-disable-next-line no-console
506
+ console.log('Members:');
507
+ if (body.members.length === 0) {
508
+ // eslint-disable-next-line no-console
509
+ console.log(' (none)');
510
+ }
511
+ else {
512
+ for (const m of body.members) {
513
+ const self = m.self ? ' [self]' : '';
514
+ // eslint-disable-next-line no-console
515
+ console.log(` ${m.peerId} ${m.label}${self}`);
516
+ }
517
+ }
518
+ // eslint-disable-next-line no-console
519
+ console.log('\nPending invites:');
520
+ if (body.pending.length === 0) {
521
+ // eslint-disable-next-line no-console
522
+ console.log(' (none)');
523
+ }
524
+ else {
525
+ for (const p of body.pending) {
526
+ const expires = p.expiresAt ? ` (expires ${p.expiresAt})` : '';
527
+ // eslint-disable-next-line no-console
528
+ console.log(` ${p.token} ${p.label}${expires}`);
529
+ }
530
+ }
531
+ process.exit(0);
532
+ });
533
+ trust
534
+ .command('revoke')
535
+ .description('Revoke a pending invite (by token) or remove a member (by peerId)')
536
+ .argument('<id>', 'Token (pending invite) or peerId (existing member) to remove')
537
+ .option('--kind <kind>', 'Force interpretation: "invite" | "member" (default: auto)', 'auto')
538
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
539
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
540
+ .action(async (id, opts) => {
541
+ const base = `http://${opts.host}:${resolvePort(opts.port)}`;
542
+ const kind = opts.kind === 'auto'
543
+ ? (looksLikePeerId(id) ? 'member' : 'invite')
544
+ : opts.kind;
545
+ const path = kind === 'member'
546
+ ? `/auth/members/${encodeURIComponent(id)}`
547
+ : `/auth/invites/${encodeURIComponent(id)}`;
548
+ let response;
549
+ try {
550
+ response = await fetch(`${base}${path}`, { method: 'DELETE' });
551
+ }
552
+ catch (err) {
553
+ // eslint-disable-next-line no-console
554
+ console.error(`Failed to reach cadre-host at ${base}: ${err.message}`);
555
+ process.exit(2);
556
+ return;
557
+ }
558
+ if (!response.ok) {
559
+ const text = await response.text().catch(() => '');
560
+ // eslint-disable-next-line no-console
561
+ console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
562
+ process.exit(1);
563
+ return;
564
+ }
565
+ // eslint-disable-next-line no-console
566
+ console.log(`revoked ${kind}: ${id}`);
567
+ process.exit(0);
568
+ });
569
+ // ============================================================================
570
+ // nat subcommands
571
+ // ============================================================================
572
+ const nat = program
573
+ .command('nat')
574
+ .description('Manage NAT traversal, port forwarding, and dynamic DNS');
575
+ nat
576
+ .command('status')
577
+ .description('Show NAT, external-IP, and DDNS status')
578
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
579
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
580
+ .option('--json', 'Output raw JSON instead of a human-readable summary')
581
+ .action(async (opts) => {
582
+ const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/status`;
583
+ const body = await getJson(url);
584
+ if (opts.json) {
585
+ // eslint-disable-next-line no-console
586
+ console.log(JSON.stringify(body, null, 2));
587
+ }
588
+ else {
589
+ printNatStatus(body);
590
+ }
591
+ process.exit(0);
592
+ });
593
+ nat
594
+ .command('test')
595
+ .description('Re-run reachability probes and print the result')
596
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
597
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
598
+ .option('--json', 'Output raw JSON instead of a human-readable summary')
599
+ .action(async (opts) => {
600
+ const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/test`;
601
+ const body = await postJson(url, {});
602
+ if (opts.json) {
603
+ // eslint-disable-next-line no-console
604
+ console.log(JSON.stringify(body, null, 2));
605
+ }
606
+ else {
607
+ printNatStatus(body);
608
+ }
609
+ process.exit(0);
610
+ });
611
+ nat
612
+ .command('settings')
613
+ .description('Update NAT settings (external port, UPnP toggle)')
614
+ .option('--external-port <port>', 'External port to map')
615
+ .option('--internal-port <port>', 'Internal libp2p port (defaults to external)')
616
+ .option('--no-upnp', 'Disable UPnP/NAT-PMP port mapping')
617
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
618
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
619
+ .action(async (opts) => {
620
+ const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/settings`;
621
+ const patch = {};
622
+ if (opts.externalPort)
623
+ patch.externalPort = resolvePort(opts.externalPort);
624
+ if (opts.internalPort)
625
+ patch.internalPort = resolvePort(opts.internalPort);
626
+ // Commander assigns `upnp: false` when `--no-upnp` is passed.
627
+ if (opts.upnp === false)
628
+ patch.upnpEnabled = false;
629
+ if (opts.upnp === true)
630
+ patch.upnpEnabled = true;
631
+ const body = await putJson(url, patch);
632
+ printNatStatus(body);
633
+ process.exit(0);
634
+ });
635
+ const ddns = nat
636
+ .command('ddns')
637
+ .description('Configure dynamic DNS');
638
+ ddns
639
+ .command('set')
640
+ .description('Configure a DDNS provider (and store credentials in the keychain)')
641
+ .argument('<provider>', 'Provider ID (e.g. "duckdns")')
642
+ .option('--hostname <hostname>', 'Hostname to publish (e.g. foo.duckdns.org)')
643
+ .option('--token <token>', 'Provider token (a secret value; prompts if omitted)')
644
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
645
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
646
+ .action(async (provider, opts) => {
647
+ if (!opts.hostname) {
648
+ // eslint-disable-next-line no-console
649
+ console.error('--hostname is required');
650
+ process.exit(1);
651
+ return;
652
+ }
653
+ const config = {};
654
+ if (opts.token)
655
+ config.token = opts.token;
656
+ if (!config.token) {
657
+ if (!process.stdin.isTTY) {
658
+ // eslint-disable-next-line no-console
659
+ console.error('Missing --token for provider "' + provider + '" and stdin is not a TTY. ' +
660
+ 'Re-run with --token <secret>.');
661
+ process.exit(1);
662
+ return;
663
+ }
664
+ try {
665
+ config.token = await readSecretLine('DDNS token: ');
666
+ }
667
+ catch (err) {
668
+ // eslint-disable-next-line no-console
669
+ console.error('Failed to read token: ' + err.message);
670
+ process.exit(1);
671
+ return;
672
+ }
673
+ }
674
+ const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/ddns`;
675
+ const body = await putJson(url, {
676
+ providerId: provider,
677
+ hostname: opts.hostname,
678
+ config,
679
+ externallyManaged: false,
680
+ });
681
+ printNatStatus(body);
682
+ process.exit(0);
683
+ });
684
+ ddns
685
+ .command('external')
686
+ .description('Declare an externally-managed DDNS hostname (cadre-host does not update it)')
687
+ .requiredOption('--hostname <hostname>', 'Hostname (no provider; cadre-host will not push updates)')
688
+ .option('--port <port>', 'cadre-host management API port', String(DEFAULT_PORT))
689
+ .option('--host <host>', 'cadre-host management API host', '127.0.0.1')
690
+ .action(async (opts) => {
691
+ const url = `http://${opts.host}:${resolvePort(opts.port)}/nat/settings`;
692
+ const body = await putJson(url, {
693
+ ddns: {
694
+ providerId: 'external',
695
+ hostname: opts.hostname,
696
+ externallyManaged: true,
697
+ },
698
+ });
699
+ printNatStatus(body);
700
+ process.exit(0);
701
+ });
702
+ async function getJson(url) {
703
+ return await callJson(url, 'GET');
704
+ }
705
+ async function postJson(url, body) {
706
+ return await callJson(url, 'POST', body);
707
+ }
708
+ async function putJson(url, body) {
709
+ return await callJson(url, 'PUT', body);
710
+ }
711
+ async function callJson(url, method, body) {
712
+ let response;
713
+ try {
714
+ const init = { method };
715
+ if (body !== undefined) {
716
+ init.headers = { 'content-type': 'application/json' };
717
+ init.body = JSON.stringify(body);
718
+ }
719
+ response = await fetch(url, init);
720
+ }
721
+ catch (err) {
722
+ // eslint-disable-next-line no-console
723
+ console.error(`Failed to reach cadre-host at ${url}: ${err.message}\n` +
724
+ `Hint: is cadre-host running? Try \`cadre-host start\`.`);
725
+ process.exit(2);
726
+ throw err;
727
+ }
728
+ if (!response.ok) {
729
+ const text = await response.text().catch(() => '');
730
+ // eslint-disable-next-line no-console
731
+ console.error(`cadre-host returned ${response.status}: ${text || response.statusText}`);
732
+ process.exit(1);
733
+ throw new Error('non-ok');
734
+ }
735
+ if (response.status === 204)
736
+ return {};
737
+ return await response.json();
738
+ }
739
+ function printNatStatus(s) {
740
+ // eslint-disable-next-line no-console
741
+ console.log(`Port mapping: ${s.portMode ?? '?'} (external ${s.externalPort ?? '?'} → internal ${s.internalPort ?? '?'})`);
742
+ if (s.routerExternalIp) {
743
+ // eslint-disable-next-line no-console
744
+ console.log(`Router IP: ${s.routerExternalIp}`);
745
+ }
746
+ // eslint-disable-next-line no-console
747
+ console.log(`External IP: ${s.externalIp ?? '(unknown)'}${s.cgnatDetected ? ' [CGNAT detected]' : ''}`);
748
+ // eslint-disable-next-line no-console
749
+ console.log(`Reachability: ${s.directReachability ?? 'unknown'}${s.lastTestedAt ? ` (tested ${s.lastTestedAt})` : ''}`);
750
+ if (s.mappingLeaseExpiresAt) {
751
+ // eslint-disable-next-line no-console
752
+ console.log(`Lease expires: ${s.mappingLeaseExpiresAt}`);
753
+ }
754
+ const ddns = s.ddns ?? {};
755
+ if (ddns.providerId || ddns.hostname) {
756
+ // eslint-disable-next-line no-console
757
+ console.log(`\nDDNS:`);
758
+ // eslint-disable-next-line no-console
759
+ console.log(` Provider: ${ddns.providerId ?? '(none)'}${ddns.externallyManaged ? ' [externally managed]' : ''}`);
760
+ // eslint-disable-next-line no-console
761
+ console.log(` Hostname: ${ddns.hostname ?? '(unset)'}`);
762
+ if (ddns.lastUpdateAt) {
763
+ const ok = ddns.lastUpdateOk ? 'OK' : 'FAIL';
764
+ // eslint-disable-next-line no-console
765
+ console.log(` Last update: ${ok} at ${ddns.lastUpdateAt}${ddns.lastError ? ` — ${ddns.lastError}` : ''}`);
766
+ }
767
+ }
768
+ else {
769
+ // eslint-disable-next-line no-console
770
+ console.log('\nDDNS: not configured');
771
+ }
772
+ }
773
+ /** Read a single line from stdin with echo suppressed (TTY only). */
774
+ function readSecretLine(prompt) {
775
+ return new Promise((resolveLine, rejectLine) => {
776
+ const stdin = process.stdin;
777
+ const stdout = process.stdout;
778
+ const wasRaw = stdin.isRaw;
779
+ try {
780
+ stdout.write(prompt);
781
+ stdin.setRawMode(true);
782
+ }
783
+ catch (err) {
784
+ rejectLine(err);
785
+ return;
786
+ }
787
+ stdin.resume();
788
+ stdin.setEncoding('utf8');
789
+ let buf = '';
790
+ const onData = (chunk) => {
791
+ for (const ch of chunk) {
792
+ if (ch === '\n' || ch === '\r' || ch === '') {
793
+ stdin.removeListener('data', onData);
794
+ try {
795
+ stdin.setRawMode(wasRaw);
796
+ }
797
+ catch { /* ignore */ }
798
+ stdin.pause();
799
+ stdout.write('\n');
800
+ resolveLine(buf);
801
+ return;
802
+ }
803
+ if (ch === '') {
804
+ // Ctrl-C
805
+ stdin.removeListener('data', onData);
806
+ try {
807
+ stdin.setRawMode(wasRaw);
808
+ }
809
+ catch { /* ignore */ }
810
+ stdin.pause();
811
+ stdout.write('\n');
812
+ rejectLine(new Error('cancelled'));
813
+ return;
814
+ }
815
+ if (ch === '' || ch === '\b') {
816
+ buf = buf.slice(0, -1);
817
+ continue;
818
+ }
819
+ buf += ch;
820
+ }
821
+ };
822
+ stdin.on('data', onData);
823
+ });
824
+ }
825
+ program.parseAsync();
826
+ //# sourceMappingURL=host.js.map