@pellux/goodvibes-tui 1.27.0 → 2.0.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 (404) hide show
  1. package/CHANGELOG.md +125 -2
  2. package/README.md +7 -9
  3. package/bin/goodvibes +2 -2
  4. package/bin/launcher-support.js +8 -4
  5. package/docs/README.md +11 -2
  6. package/docs/foundation-artifacts/operator-contract.json +6403 -2347
  7. package/package.json +14 -25
  8. package/scripts/postinstall.js +35 -35
  9. package/src/audio/spoken-turn-model-routing.ts +1 -1
  10. package/src/cli/bundle-command.ts +5 -5
  11. package/src/cli/completions/generate.ts +1 -1
  12. package/src/cli/doctor.ts +3 -3
  13. package/src/cli/entrypoint.ts +12 -7
  14. package/src/cli/help.ts +4 -5
  15. package/src/cli/hooks-command.ts +1 -1
  16. package/src/cli/index.ts +1 -4
  17. package/src/cli/launch-auto-update.ts +59 -7
  18. package/src/cli/management-commands.ts +6 -6
  19. package/src/cli/management-utils.ts +5 -5
  20. package/src/cli/management.ts +7 -7
  21. package/src/cli/network-posture.ts +1 -1
  22. package/src/cli/package-verification.ts +7 -6
  23. package/src/cli/plugin-bundles-command.ts +1 -1
  24. package/src/cli/plugin-command.ts +2 -2
  25. package/src/cli/service-command.ts +2 -2
  26. package/src/cli/service-posture.ts +4 -4
  27. package/src/cli/status.ts +9 -8
  28. package/src/cli/surface-command.ts +4 -4
  29. package/src/cli/tui-startup.ts +12 -3
  30. package/src/cli-flags.ts +3 -3
  31. package/src/config/goodvibes-home-audit.ts +0 -2
  32. package/src/config/secret-config.ts +37 -5
  33. package/src/config/tui-extension-settings.ts +1 -1
  34. package/src/core/approval-alert.ts +2 -2
  35. package/src/core/budget-breach-notifier.ts +3 -3
  36. package/src/core/conversation-compaction-render.ts +1 -1
  37. package/src/core/conversation-line-cache.ts +1 -1
  38. package/src/core/conversation-render-context.ts +1 -1
  39. package/src/core/conversation-rendering.ts +2 -2
  40. package/src/core/conversation.ts +10 -10
  41. package/src/core/long-task-notifier.ts +2 -2
  42. package/src/core/memory-diagnostics-gateway.ts +1 -1
  43. package/src/core/scriptable-statusline.ts +1 -1
  44. package/src/core/session-auto-titler.ts +1 -1
  45. package/src/core/session-resume-core.ts +2 -2
  46. package/src/core/stream-event-wiring.ts +24 -44
  47. package/src/core/terminal-notifier.ts +3 -3
  48. package/src/core/turn-cancellation.ts +1 -1
  49. package/src/core/turn-event-wiring.ts +5 -5
  50. package/src/input/command-registry.ts +6 -6
  51. package/src/input/commands/channel-pairing.ts +1 -1
  52. package/src/input/commands/cluster-runtime.ts +1 -1
  53. package/src/input/commands/codebase-runtime.ts +1 -1
  54. package/src/input/commands/devices-runtime.ts +1 -1
  55. package/src/input/commands/hosted-runtime.ts +330 -0
  56. package/src/input/commands/local-setup-review.ts +1 -1
  57. package/src/input/commands/local-setup-transfer.ts +2 -2
  58. package/src/input/commands/local-setup.ts +3 -3
  59. package/src/input/commands/managed-runtime.ts +2 -2
  60. package/src/input/commands/operator-rpc.ts +24 -110
  61. package/src/input/commands/payment-card-intake.ts +1 -1
  62. package/src/input/commands/permissions-provenance.ts +2 -2
  63. package/src/input/commands/plugin-runtime.ts +1 -1
  64. package/src/input/commands/provider.ts +1 -1
  65. package/src/input/commands/relay-runtime.ts +18 -9
  66. package/src/input/commands/rewind-runtime.ts +1 -1
  67. package/src/input/commands/runtime-services.ts +1 -1
  68. package/src/input/commands/services-runtime.ts +1 -1
  69. package/src/input/commands/session-workflow.ts +1 -1
  70. package/src/input/commands/settings-sync-runtime.ts +2 -2
  71. package/src/input/commands/share-runtime.ts +1 -1
  72. package/src/input/commands/tasks-runtime.ts +145 -55
  73. package/src/input/commands/update-runtime.ts +31 -28
  74. package/src/input/commands/work-plan-runtime.ts +2 -2
  75. package/src/input/commands/workstream-attempts.ts +1 -1
  76. package/src/input/commands/workstream-runtime.ts +2 -2
  77. package/src/input/commands.ts +2 -0
  78. package/src/input/feature-unit-layout.ts +1 -1
  79. package/src/input/feed-context-factory.ts +3 -3
  80. package/src/input/handler-content-actions.ts +2 -2
  81. package/src/input/handler-feed-routes.ts +1 -1
  82. package/src/input/handler-feed.ts +3 -3
  83. package/src/input/handler-modal-token-routes.ts +1 -1
  84. package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
  85. package/src/input/handler-onboarding.ts +1 -1
  86. package/src/input/handler-types.ts +2 -2
  87. package/src/input/handler-ui-state.ts +1 -1
  88. package/src/input/handler.ts +4 -4
  89. package/src/input/input-history.ts +1 -1
  90. package/src/input/onboarding/onboarding-feature-units.ts +2 -2
  91. package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
  92. package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
  93. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
  94. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
  95. package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
  96. package/src/input/payments-config.ts +6 -6
  97. package/src/input/search.ts +1 -1
  98. package/src/input/session-picker-modal.ts +46 -0
  99. package/src/input/settings-modal-activation.ts +1 -1
  100. package/src/input/settings-modal-behavior.ts +4 -4
  101. package/src/input/settings-modal-commit.ts +11 -0
  102. package/src/input/settings-modal-data.ts +13 -80
  103. package/src/input/settings-modal-mutations.ts +33 -0
  104. package/src/input/settings-modal-secrets.ts +34 -0
  105. package/src/input/settings-modal-subscriptions.ts +1 -1
  106. package/src/input/settings-modal-types.ts +15 -1
  107. package/src/input/settings-modal.ts +28 -3
  108. package/src/input/worktree-setup-config.ts +1 -1
  109. package/src/main.ts +11 -9
  110. package/src/panels/agent-inspector-shared.ts +2 -2
  111. package/src/panels/base-panel.ts +2 -2
  112. package/src/panels/builtin/operations.ts +6 -8
  113. package/src/panels/builtin/session.ts +17 -0
  114. package/src/panels/builtin/shared.ts +5 -5
  115. package/src/panels/builtin-modals.ts +3 -3
  116. package/src/panels/confirm-state.ts +1 -1
  117. package/src/panels/cost-tracker-panel.ts +4 -4
  118. package/src/panels/diff-panel.ts +2 -2
  119. package/src/panels/diff-review-panel.ts +2 -2
  120. package/src/panels/expandable-list-panel.ts +2 -2
  121. package/src/panels/fleet-acts.ts +1 -1
  122. package/src/panels/fleet-observed-render.ts +1 -1
  123. package/src/panels/fleet-panel-format.ts +1 -1
  124. package/src/panels/fleet-panel.ts +1 -1
  125. package/src/panels/fleet-spawn.ts +1 -1
  126. package/src/panels/fleet-steer.ts +1 -1
  127. package/src/panels/fleet-transcript.ts +1 -1
  128. package/src/panels/git-panel.ts +1 -1
  129. package/src/panels/hosted-session-feed.ts +352 -0
  130. package/src/panels/hosted-session-panel.ts +221 -0
  131. package/src/panels/local-auth-panel.ts +3 -3
  132. package/src/panels/modals/devices-modal.ts +1 -1
  133. package/src/panels/modals/local-auth-modal.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +1 -1
  135. package/src/panels/modals/services-modal.ts +1 -1
  136. package/src/panels/modals/settings-sync-modal.ts +1 -1
  137. package/src/panels/modals/subscription-modal.ts +1 -1
  138. package/src/panels/modals/work-plan-modal.ts +1 -1
  139. package/src/panels/notifications-panel.ts +1 -1
  140. package/src/panels/panel-confirm-overlay.ts +1 -1
  141. package/src/panels/plugins-panel.ts +2 -2
  142. package/src/panels/polish-core.ts +2 -2
  143. package/src/panels/polish-tables.ts +1 -1
  144. package/src/panels/polish.ts +2 -2
  145. package/src/panels/scrollable-list-panel.ts +3 -3
  146. package/src/panels/skills-panel.ts +3 -3
  147. package/src/panels/token-budget-panel.ts +5 -5
  148. package/src/panels/types.ts +1 -2
  149. package/src/permissions/prompt.ts +1 -1
  150. package/src/renderer/autocomplete-overlay.ts +1 -1
  151. package/src/renderer/block-actions-overlay.ts +1 -1
  152. package/src/renderer/bookmark-modal.ts +1 -1
  153. package/src/renderer/buffer.ts +1 -1
  154. package/src/renderer/code-block.ts +1 -1
  155. package/src/renderer/composer-fragments.ts +1 -1
  156. package/src/renderer/compositor.ts +2 -2
  157. package/src/renderer/config-modal.ts +1 -1
  158. package/src/renderer/context-inspector.ts +1 -1
  159. package/src/renderer/conversation-layout.ts +2 -2
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/conversation-surface.ts +2 -2
  162. package/src/renderer/diff-view.ts +1 -1
  163. package/src/renderer/diff.ts +1 -1
  164. package/src/renderer/file-picker-overlay.ts +1 -1
  165. package/src/renderer/fleet-tab-strip.ts +1 -1
  166. package/src/renderer/fullscreen-primitives.ts +2 -2
  167. package/src/renderer/fullscreen-workspace.ts +1 -1
  168. package/src/renderer/header-line.ts +1 -1
  169. package/src/renderer/help-overlay.ts +1 -1
  170. package/src/renderer/history-search-overlay.ts +2 -2
  171. package/src/renderer/layout.ts +5 -8
  172. package/src/renderer/markdown-inline.ts +1 -1
  173. package/src/renderer/markdown-table.ts +1 -1
  174. package/src/renderer/markdown.ts +1 -1
  175. package/src/renderer/mcp-workspace.ts +1 -1
  176. package/src/renderer/modal-factory.ts +2 -2
  177. package/src/renderer/model-workspace.ts +2 -2
  178. package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
  179. package/src/renderer/overlay-box.ts +1 -1
  180. package/src/renderer/panel-composite.ts +1 -1
  181. package/src/renderer/panel-workspace-bar.ts +1 -1
  182. package/src/renderer/process-indicator.ts +1 -1
  183. package/src/renderer/profile-picker-modal.ts +1 -1
  184. package/src/renderer/search-overlay.ts +2 -2
  185. package/src/renderer/selection-modal-overlay.ts +1 -1
  186. package/src/renderer/session-picker-modal.ts +124 -3
  187. package/src/renderer/settings-modal-helpers.ts +2 -0
  188. package/src/renderer/settings-modal.ts +2 -2
  189. package/src/renderer/shell-surface.ts +2 -2
  190. package/src/renderer/system-message.ts +1 -1
  191. package/src/renderer/tab-strip.ts +1 -1
  192. package/src/renderer/theme-mode-config.ts +1 -1
  193. package/src/renderer/theme.ts +11 -12
  194. package/src/renderer/thinking.ts +1 -1
  195. package/src/renderer/tool-call.ts +2 -2
  196. package/src/renderer/tool-result-expanded-lines.ts +1 -1
  197. package/src/renderer/turn-injection.ts +7 -8
  198. package/src/renderer/ui-factory.ts +3 -3
  199. package/src/runtime/bootstrap-command-context.ts +4 -4
  200. package/src/runtime/bootstrap-command-parts.ts +5 -5
  201. package/src/runtime/bootstrap-core.ts +30 -30
  202. package/src/runtime/bootstrap-shell.ts +4 -4
  203. package/src/runtime/bootstrap.ts +128 -157
  204. package/src/runtime/client/approval-updates.ts +82 -0
  205. package/src/runtime/client/build-floors.ts +167 -0
  206. package/src/runtime/client/daemon-attach-handshake.ts +115 -0
  207. package/src/runtime/client/fleet-union.ts +147 -0
  208. package/src/runtime/client/host-service-status.ts +71 -0
  209. package/src/runtime/client/hosted-exit.ts +86 -0
  210. package/src/runtime/client/hosted-roster.ts +110 -0
  211. package/src/runtime/client/hosted-session-stream.ts +178 -0
  212. package/src/runtime/client/hosted-sessions.ts +187 -0
  213. package/src/runtime/client/operator-endpoint.ts +213 -0
  214. package/src/runtime/client/spine-adoption.ts +99 -0
  215. package/src/runtime/cloudflare-control-plane.ts +1 -1
  216. package/src/runtime/context-accounting-source.ts +1 -1
  217. package/src/runtime/context.ts +2 -2
  218. package/src/runtime/conversation-rewind-port.ts +42 -14
  219. package/src/runtime/daemon-attach-notices.ts +55 -19
  220. package/src/runtime/diagnostics/panels/ops.ts +1 -1
  221. package/src/runtime/disposal-wiring.ts +4 -24
  222. package/src/runtime/index.ts +119 -105
  223. package/src/runtime/install-self-check-startup.ts +2 -2
  224. package/src/runtime/interaction-seams.ts +1 -1
  225. package/src/runtime/knowledge-services.ts +8 -2
  226. package/src/runtime/legacy-daemon-migration.ts +15 -3
  227. package/src/runtime/legacy-daemon-reconcile.ts +3 -2
  228. package/src/runtime/memory-spine-transport.ts +37 -267
  229. package/src/runtime/notification-dispatch.ts +2 -2
  230. package/src/runtime/onboarding/apply.ts +2 -2
  231. package/src/runtime/onboarding/derivation.ts +2 -2
  232. package/src/runtime/onboarding/markers.ts +2 -2
  233. package/src/runtime/onboarding/progress.ts +2 -2
  234. package/src/runtime/onboarding/snapshot.ts +1 -1
  235. package/src/runtime/onboarding/state.ts +2 -2
  236. package/src/runtime/onboarding/types.ts +2 -2
  237. package/src/runtime/orchestrator-core-services.ts +1 -1
  238. package/src/runtime/path-shadow-startup.ts +28 -221
  239. package/src/runtime/power-keepawake-remote.ts +3 -2
  240. package/src/runtime/process-lifecycle.ts +2 -2
  241. package/src/runtime/provider-fallback.ts +10 -66
  242. package/src/runtime/recovery-autosave.ts +1 -1
  243. package/src/runtime/recovery-offer-wiring.ts +39 -23
  244. package/src/runtime/recovery-prompt.ts +31 -17
  245. package/src/runtime/relay-reachability-bridge.ts +53 -42
  246. package/src/runtime/release-artifacts.ts +2 -1
  247. package/src/runtime/runtime-services-types.ts +82 -35
  248. package/src/runtime/services.ts +305 -300
  249. package/src/runtime/session-resume-liveness-confirm.ts +1 -1
  250. package/src/runtime/ui-services.ts +7 -6
  251. package/src/runtime/update-check.ts +23 -43
  252. package/src/runtime/workspace-checkpointing.ts +1 -1
  253. package/src/runtime/wrfc-persistence.ts +2 -2
  254. package/src/shell/ui-openers.ts +30 -7
  255. package/src/utils/clipboard.ts +1 -1
  256. package/src/verification/verification-ledger.ts +10 -8
  257. package/src/version.ts +1 -1
  258. package/bin/goodvibes-daemon +0 -90
  259. package/src/cli/config-overrides.ts +0 -267
  260. package/src/cli/endpoints.ts +0 -109
  261. package/src/cli/parser.ts +0 -395
  262. package/src/cli/redaction.ts +0 -170
  263. package/src/cli/types.ts +0 -84
  264. package/src/cluster/commands.ts +0 -382
  265. package/src/cluster/remote-daemon-target.ts +0 -225
  266. package/src/cluster/render.ts +0 -268
  267. package/src/config/atomic-write.ts +0 -70
  268. package/src/config/credential-availability.ts +0 -122
  269. package/src/config/goodvibes-home.ts +0 -149
  270. package/src/config/index.ts +0 -88
  271. package/src/config/provider-model.ts +0 -23
  272. package/src/config/read-versioned.ts +0 -228
  273. package/src/core/alert-gating.ts +0 -67
  274. package/src/core/consolidation-receipt.ts +0 -41
  275. package/src/core/focus-tracker.ts +0 -41
  276. package/src/core/history.ts +0 -45
  277. package/src/core/memory-status.ts +0 -118
  278. package/src/core/orchestrator.ts +0 -7
  279. package/src/core/pairing-devices.ts +0 -49
  280. package/src/core/pairing-handoff.ts +0 -101
  281. package/src/core/pairing-offers.ts +0 -83
  282. package/src/core/pairing-origin.ts +0 -78
  283. package/src/core/rewind-turn-anchors.ts +0 -311
  284. package/src/core/session-recovery.ts +0 -210
  285. package/src/core/stable-host.ts +0 -171
  286. package/src/core/transcript-journal.ts +0 -503
  287. package/src/daemon/cli.ts +0 -641
  288. package/src/daemon/handlers/context.ts +0 -29
  289. package/src/daemon/handlers/contracts.ts +0 -77
  290. package/src/daemon/handlers/credentials.ts +0 -139
  291. package/src/daemon/handlers/drafts/draft-store.ts +0 -427
  292. package/src/daemon/handlers/drafts/index.ts +0 -17
  293. package/src/daemon/handlers/drafts/register.ts +0 -331
  294. package/src/daemon/handlers/errors.ts +0 -18
  295. package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
  296. package/src/daemon/handlers/inbox/index.ts +0 -284
  297. package/src/daemon/handlers/inbox/mapping.ts +0 -192
  298. package/src/daemon/handlers/inbox/poller.ts +0 -212
  299. package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
  300. package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
  301. package/src/daemon/handlers/inbox/providers/email.ts +0 -152
  302. package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
  303. package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
  304. package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
  305. package/src/daemon/handlers/index.ts +0 -117
  306. package/src/daemon/handlers/register.ts +0 -168
  307. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
  308. package/src/daemon/handlers/remote/backends/docker.ts +0 -79
  309. package/src/daemon/handlers/remote/backends/index.ts +0 -40
  310. package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
  311. package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
  312. package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
  313. package/src/daemon/handlers/remote/backends/types.ts +0 -97
  314. package/src/daemon/handlers/remote/dispatcher.ts +0 -181
  315. package/src/daemon/handlers/remote/index.ts +0 -119
  316. package/src/daemon/handlers/remote/peer-registry.ts +0 -357
  317. package/src/daemon/handlers/remote/service.ts +0 -191
  318. package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
  319. package/src/daemon/handlers/routing/index.ts +0 -261
  320. package/src/daemon/handlers/routing/route-store.ts +0 -319
  321. package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
  322. package/src/daemon/handlers/sqlite-store.ts +0 -278
  323. package/src/daemon/handlers/triage/index.ts +0 -57
  324. package/src/daemon/handlers/triage/integration.ts +0 -213
  325. package/src/daemon/handlers/triage/pipeline.ts +0 -274
  326. package/src/daemon/handlers/triage/scorer.ts +0 -287
  327. package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
  328. package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
  329. package/src/daemon/handlers/triage/tagger/index.ts +0 -184
  330. package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
  331. package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
  332. package/src/daemon/handlers/triage/types.ts +0 -50
  333. package/src/daemon/lifecycle.ts +0 -41
  334. package/src/daemon/provision-wake-model.ts +0 -81
  335. package/src/daemon/safe-serve.ts +0 -61
  336. package/src/daemon/send/channels.ts +0 -200
  337. package/src/daemon/send/command.ts +0 -332
  338. package/src/daemon/send/composition.ts +0 -99
  339. package/src/daemon/send/failure-text.ts +0 -93
  340. package/src/daemon/send/inert-text.ts +0 -225
  341. package/src/daemon/send/stdin.ts +0 -24
  342. package/src/daemon/service-commands.ts +0 -329
  343. package/src/export/cost-utils.ts +0 -319
  344. package/src/input/selection.ts +0 -135
  345. package/src/plugins/loader.ts +0 -15
  346. package/src/renderer/bottom-bar.ts +0 -58
  347. package/src/renderer/conversation-tree.ts +0 -228
  348. package/src/runtime/agent-graph-composition.ts +0 -78
  349. package/src/runtime/channel-composition.ts +0 -46
  350. package/src/runtime/cluster-composition.ts +0 -124
  351. package/src/runtime/cluster-group-composition.ts +0 -284
  352. package/src/runtime/code-index-services.ts +0 -167
  353. package/src/runtime/daemon-handler-composition.ts +0 -76
  354. package/src/runtime/device-posture-composition.ts +0 -110
  355. package/src/runtime/durability-housekeeping.ts +0 -183
  356. package/src/runtime/durability-services.ts +0 -155
  357. package/src/runtime/embedded-service-factories.ts +0 -58
  358. package/src/runtime/feature-settings.ts +0 -154
  359. package/src/runtime/fleet-needs-input-push.ts +0 -70
  360. package/src/runtime/fleet-services.ts +0 -41
  361. package/src/runtime/idle-power-services.ts +0 -85
  362. package/src/runtime/mail-composition.ts +0 -65
  363. package/src/runtime/memory-fold.ts +0 -55
  364. package/src/runtime/operator-token-cleanup.ts +0 -109
  365. package/src/runtime/path-shadow.ts +0 -418
  366. package/src/runtime/perf/panel-health-monitor.ts +0 -18
  367. package/src/runtime/provider-optimizer-wiring.ts +0 -55
  368. package/src/runtime/reachability-notice.ts +0 -100
  369. package/src/runtime/remote-execution-composition.ts +0 -62
  370. package/src/runtime/session-liveness-marker.ts +0 -257
  371. package/src/runtime/session-pointer-surface.ts +0 -22
  372. package/src/runtime/session-storage-services.ts +0 -41
  373. package/src/runtime/surface-feature-flags.ts +0 -96
  374. package/src/runtime/terminal-output-guard.ts +0 -235
  375. package/src/runtime/trust/workspace-registration.ts +0 -192
  376. package/src/runtime/trust/workspace-trust.ts +0 -229
  377. package/src/runtime/ui/index.ts +0 -39
  378. package/src/runtime/ui/model-picker/data-provider.ts +0 -182
  379. package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
  380. package/src/runtime/ui/model-picker/index.ts +0 -59
  381. package/src/runtime/ui/model-picker/types.ts +0 -153
  382. package/src/runtime/ui/provider-health/data-provider.ts +0 -244
  383. package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
  384. package/src/runtime/ui/provider-health/index.ts +0 -46
  385. package/src/runtime/ui/provider-health/types.ts +0 -146
  386. package/src/runtime/ui-events.ts +0 -1
  387. package/src/runtime/ui-read-model-helpers.ts +0 -1
  388. package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
  389. package/src/runtime/ui-read-models-observability-options.ts +0 -1
  390. package/src/runtime/ui-read-models-observability-remote.ts +0 -1
  391. package/src/runtime/ui-read-models-observability-security.ts +0 -1
  392. package/src/runtime/ui-read-models-observability-system.ts +0 -1
  393. package/src/runtime/ui-read-models-observability.ts +0 -1
  394. package/src/runtime/ui-service-queries.ts +0 -1
  395. package/src/runtime/voice-setup-services.ts +0 -98
  396. package/src/runtime/workstream-attempts-validation.ts +0 -77
  397. package/src/runtime/workstream-draft-edits.ts +0 -95
  398. package/src/runtime/workstream-draft-store.ts +0 -274
  399. package/src/runtime/workstream-draft-types.ts +0 -42
  400. package/src/runtime/workstream-services.ts +0 -373
  401. package/src/tools/index.ts +0 -1
  402. package/src/types/grid.ts +0 -48
  403. package/src/types/sql-js.d.ts +0 -15
  404. package/src/work-plans/work-plan-store.ts +0 -702
@@ -1,382 +0,0 @@
1
- /**
2
- * commands.ts — `goodvibes-daemon cluster …`, and `/cluster` in the TUI.
3
- *
4
- * The authoritative surface for LAN groups. A homelab node is headless: the
5
- * operator SSHes in, and everything they can do to a group they must be able to
6
- * do here.
7
- *
8
- * This module contains NO logic of its own. It parses arguments, calls a daemon
9
- * verb over the convention in remote-daemon-target.ts, and renders the answer.
10
- * The daemon decides everything else, which is what makes this command, the
11
- * TUI's `/cluster` and any web UI three renderings of one implementation.
12
- *
13
- * Every failure exits non-zero with a plain-language message naming the fix.
14
- */
15
- import { createInterface } from 'node:readline/promises';
16
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
17
- import type {
18
- RotateKeyResult,
19
- CreateGroupResult,
20
- DiscoveredGroup,
21
- ForgetNodeResult,
22
- GroupStatusReport,
23
- JoinGroupResult,
24
- JoinKeyResult,
25
- NodesResult,
26
- } from '@pellux/goodvibes-sdk/platform/cluster';
27
- import {
28
- callDaemonVerb,
29
- resolveRemoteDaemonTarget,
30
- type DaemonFetch,
31
- type DaemonVerbOutcome,
32
- } from './remote-daemon-target.ts';
33
- import {
34
- renderCreated,
35
- renderDiscovered,
36
- renderFailure,
37
- renderForgotten,
38
- renderJoinKey,
39
- renderJoinKeyQr,
40
- renderJoined,
41
- renderNodes,
42
- renderRotated,
43
- renderStatus,
44
- } from './render.ts';
45
-
46
- export const CLUSTER_SUBCOMMANDS = [
47
- 'status', 'create', 'join', 'key', 'nodes', 'forget', 'rotate', 'leave', 'rename', 'groups',
48
- ] as const;
49
- export type ClusterSubcommand = (typeof CLUSTER_SUBCOMMANDS)[number];
50
-
51
- export function isClusterSubcommand(value: string | undefined): value is ClusterSubcommand {
52
- return typeof value === 'string' && (CLUSTER_SUBCOMMANDS as readonly string[]).includes(value);
53
- }
54
-
55
- /** What the parser produced, or why it could not. */
56
- export interface ParsedClusterCommand {
57
- readonly subcommand: ClusterSubcommand | null;
58
- readonly positional: string | null;
59
- readonly name: string | null;
60
- readonly passphrase: string | null;
61
- readonly group: string | null;
62
- readonly key: string | null;
63
- readonly host: string | null;
64
- readonly port: number | null;
65
- readonly token: string | null;
66
- readonly json: boolean;
67
- readonly yes: boolean;
68
- /** `key` only: also render the join key as a QR code. */
69
- readonly qr: boolean;
70
- /** `rotate` only: stop accepting the outgoing key at once. */
71
- readonly now: boolean;
72
- readonly errors: readonly string[];
73
- }
74
-
75
- const VALUE_FLAGS = new Set(['--name', '--passphrase', '--group', '--key', '--host', '--port', '--token']);
76
-
77
- /**
78
- * Parse `cluster <subcommand> [flags]`.
79
- *
80
- * Unknown flags are an ERROR rather than being ignored. A typo in `--group`
81
- * that silently became "no group given" would send the operator to the
82
- * interactive picker with no explanation of why their arguments vanished.
83
- */
84
- export function parseClusterCommand(argv: readonly string[]): ParsedClusterCommand {
85
- const errors: string[] = [];
86
- let subcommand: ClusterSubcommand | null = null;
87
- let positional: string | null = null;
88
- const values = new Map<string, string>();
89
- let json = false;
90
- let yes = false;
91
- let qr = false;
92
- let now = false;
93
-
94
- for (let index = 0; index < argv.length; index += 1) {
95
- const token = argv[index] ?? '';
96
- if (token === '--json') { json = true; continue; }
97
- if (token === '-y' || token === '--yes') { yes = true; continue; }
98
- if (token === '--qr') { qr = true; continue; }
99
- if (token === '--now') { now = true; continue; }
100
- if (VALUE_FLAGS.has(token)) {
101
- const value = argv[index + 1];
102
- if (value === undefined || value.startsWith('--')) {
103
- errors.push(`${token} needs a value`);
104
- continue;
105
- }
106
- values.set(token, value);
107
- index += 1;
108
- continue;
109
- }
110
- const inline = /^(--[a-z]+)=(.*)$/.exec(token);
111
- if (inline && VALUE_FLAGS.has(inline[1] ?? '')) {
112
- values.set(inline[1] as string, inline[2] ?? '');
113
- continue;
114
- }
115
- if (token.startsWith('-')) {
116
- errors.push(`'${token}' is not a flag this command understands`);
117
- continue;
118
- }
119
- if (subcommand === null) {
120
- if (!isClusterSubcommand(token)) {
121
- errors.push(`'${token}' is not a cluster command — try one of: ${CLUSTER_SUBCOMMANDS.join(', ')}`);
122
- continue;
123
- }
124
- subcommand = token;
125
- continue;
126
- }
127
- if (positional === null) {
128
- positional = token;
129
- continue;
130
- }
131
- errors.push(`'${token}' is one argument too many`);
132
- }
133
-
134
- const portText = values.get('--port');
135
- let port: number | null = null;
136
- if (portText !== undefined) {
137
- const parsed = Number(portText);
138
- if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65_535) {
139
- errors.push(`--port must be a number between 1 and 65535, not '${portText}'`);
140
- } else {
141
- port = parsed;
142
- }
143
- }
144
- if (subcommand === 'forget' && positional === null) {
145
- errors.push('forget needs the machine to remove — run `cluster nodes` to see them');
146
- }
147
- if (subcommand === 'rename' && positional === null && values.get('--name') === undefined) {
148
- errors.push('rename needs the new name — `cluster rename "the workshop"`');
149
- }
150
-
151
- return {
152
- subcommand,
153
- positional,
154
- name: values.get('--name') ?? null,
155
- passphrase: values.get('--passphrase') ?? null,
156
- group: values.get('--group') ?? null,
157
- key: values.get('--key') ?? null,
158
- host: values.get('--host') ?? null,
159
- port,
160
- token: values.get('--token') ?? null,
161
- json,
162
- yes,
163
- qr,
164
- now,
165
- errors,
166
- };
167
- }
168
-
169
- /** What the caller prints and exits with. */
170
- export interface ClusterCommandResult {
171
- readonly lines: readonly string[];
172
- readonly exitCode: number;
173
- /** Written raw to stdout — the OSC 52 clipboard attempt, when there is one. */
174
- readonly rawOutput?: string | undefined;
175
- }
176
-
177
- export interface RunClusterCommandInput {
178
- readonly argv: readonly string[];
179
- readonly configManager: Pick<ConfigManager, 'get'>;
180
- readonly daemonHomeDir: string;
181
- readonly now?: (() => number) | undefined;
182
- readonly isTerminal?: boolean | undefined;
183
- /** Injected in tests so nothing opens a socket or reads a real token. */
184
- readonly fetchImpl?: DaemonFetch | undefined;
185
- readonly readToken?: ((daemonHomeDir: string) => string | undefined) | undefined;
186
- /** Injected in tests; the real one reads stdin. */
187
- readonly prompt?: ((question: string) => Promise<string>) | undefined;
188
- }
189
-
190
- function failure(error: string, fix: string, json: boolean): ClusterCommandResult {
191
- return {
192
- lines: json
193
- ? [JSON.stringify({ ok: false, error, fix }, null, 2)]
194
- : renderFailure(error, fix),
195
- exitCode: 1,
196
- };
197
- }
198
-
199
- function success(data: unknown, lines: readonly string[], json: boolean, rawOutput?: string): ClusterCommandResult {
200
- return {
201
- lines: json ? [JSON.stringify({ ok: true, data }, null, 2)] : lines,
202
- exitCode: 0,
203
- ...(rawOutput ? { rawOutput } : {}),
204
- };
205
- }
206
-
207
- /** Ask a question on stdin. Only ever called when stdin is a terminal. */
208
- async function askOnStdin(question: string): Promise<string> {
209
- const rl = createInterface({ input: process.stdin, output: process.stdout });
210
- try {
211
- return (await rl.question(question)).trim();
212
- } finally {
213
- rl.close();
214
- }
215
- }
216
-
217
- export async function runClusterCommand(input: RunClusterCommandInput): Promise<ClusterCommandResult> {
218
- const parsed = parseClusterCommand(input.argv);
219
- if (parsed.errors.length > 0) {
220
- return failure(parsed.errors[0] as string, `usage: goodvibes-daemon cluster <${CLUSTER_SUBCOMMANDS.join('|')}>`, parsed.json);
221
- }
222
- if (!parsed.subcommand) {
223
- return failure(
224
- 'no cluster command was given',
225
- `usage: goodvibes-daemon cluster <${CLUSTER_SUBCOMMANDS.join('|')}>`,
226
- parsed.json,
227
- );
228
- }
229
-
230
- const resolved = resolveRemoteDaemonTarget({
231
- flags: {
232
- ...(parsed.host !== null ? { host: parsed.host } : {}),
233
- ...(parsed.port !== null ? { port: parsed.port } : {}),
234
- ...(parsed.token !== null ? { token: parsed.token } : {}),
235
- },
236
- configManager: input.configManager,
237
- daemonHomeDir: input.daemonHomeDir,
238
- ...(input.readToken ? { readToken: input.readToken } : {}),
239
- });
240
- if (!resolved.ok) return failure(resolved.error, resolved.fix, parsed.json);
241
-
242
- const now = input.now?.() ?? Date.now();
243
- const isTerminal = input.isTerminal ?? Boolean(process.stdout.isTTY);
244
- const call = <T>(path: string, method: 'GET' | 'POST' = 'GET', body?: unknown): Promise<DaemonVerbOutcome<T>> =>
245
- callDaemonVerb<T>(resolved.target, path, { method, ...(body === undefined ? {} : { body }) }, input.fetchImpl ?? fetch);
246
-
247
- switch (parsed.subcommand) {
248
- case 'status': {
249
- const result = await call<GroupStatusReport>('/api/cluster/status');
250
- return result.ok
251
- ? success(result.data, renderStatus(result.data, now), parsed.json)
252
- : failure(result.error, result.fix, parsed.json);
253
- }
254
- case 'nodes': {
255
- const result = await call<NodesResult>('/api/cluster/nodes');
256
- return result.ok
257
- ? success(result.data, renderNodes(result.data, now), parsed.json)
258
- : failure(result.error, result.fix, parsed.json);
259
- }
260
- case 'groups': {
261
- const result = await call<readonly DiscoveredGroup[]>('/api/cluster/groups');
262
- return result.ok
263
- ? success(result.data, renderDiscovered(result.data, now), parsed.json)
264
- : failure(result.error, result.fix, parsed.json);
265
- }
266
- case 'key': {
267
- const result = await call<JoinKeyResult>('/api/cluster/key');
268
- if (!result.ok) return failure(result.error, result.fix, parsed.json);
269
- const rendered = renderJoinKey(result.data, isTerminal && !parsed.json);
270
- const lines = parsed.qr
271
- ? [...rendered.lines, '', ...renderJoinKeyQr(result.data.joinKey)]
272
- : rendered.lines;
273
- return success(result.data, lines, parsed.json, rendered.clipboardSequence ?? undefined);
274
- }
275
- case 'create': {
276
- const result = await call<CreateGroupResult>('/api/cluster/create', 'POST', {
277
- ...(parsed.name !== null ? { name: parsed.name } : {}),
278
- ...(parsed.passphrase !== null ? { passphrase: parsed.passphrase } : {}),
279
- });
280
- return result.ok
281
- ? success(result.data, renderCreated(result.data), parsed.json)
282
- : failure(result.error, result.fix, parsed.json);
283
- }
284
- case 'rotate': {
285
- const result = await call<RotateKeyResult>('/api/cluster/rotate', 'POST', { immediate: parsed.now });
286
- return result.ok
287
- ? success(result.data, renderRotated(result.data), parsed.json)
288
- : failure(result.error, result.fix, parsed.json);
289
- }
290
- case 'leave': {
291
- const result = await call<{ groupId: string; groupName: string }>('/api/cluster/leave', 'POST');
292
- return result.ok
293
- ? success(result.data, [`left "${result.data.groupName}" [${result.data.groupId}]`,
294
- 'this machine no longer coordinates with it. The other machines still list it —',
295
- 'run `cluster forget` on one of them to tidy up.'], parsed.json)
296
- : failure(result.error, result.fix, parsed.json);
297
- }
298
- case 'rename': {
299
- const name = parsed.name ?? parsed.positional ?? '';
300
- const result = await call<{ groupId: string; groupName: string }>('/api/cluster/rename', 'POST', { name });
301
- return result.ok
302
- ? success(result.data, [`the group is now called "${result.data.groupName}"`,
303
- 'this name is visible to anything on this network, and every machine in the',
304
- 'group will pick it up shortly.'], parsed.json)
305
- : failure(result.error, result.fix, parsed.json);
306
- }
307
- case 'forget': {
308
- const result = await call<ForgetNodeResult>('/api/cluster/forget', 'POST', { nodeId: parsed.positional });
309
- return result.ok
310
- ? success(result.data, renderForgotten(result.data), parsed.json)
311
- : failure(result.error, result.fix, parsed.json);
312
- }
313
- case 'join':
314
- return runJoin(parsed, call, now, input, isTerminal);
315
- }
316
- }
317
-
318
- /**
319
- * `cluster join`.
320
- *
321
- * Two shapes, and both matter. With `--group` and `--key` it is a scriptable
322
- * one-liner, which is what provisioning a machine from a config-management tool
323
- * needs. Without them it lists the groups advertising themselves on this
324
- * network and asks — which is what a person adding their second machine needs,
325
- * because they do not know the group id and should not have to.
326
- */
327
- async function runJoin(
328
- parsed: ParsedClusterCommand,
329
- call: <T>(path: string, method?: 'GET' | 'POST', body?: unknown) => Promise<DaemonVerbOutcome<T>>,
330
- now: number,
331
- input: RunClusterCommandInput,
332
- isTerminal: boolean,
333
- ): Promise<ClusterCommandResult> {
334
- let groupId = parsed.group;
335
- let joinKey = parsed.key;
336
-
337
- if (groupId === null || joinKey === null) {
338
- const interactive = input.prompt !== undefined || (isTerminal && Boolean(process.stdin.isTTY));
339
- if (!interactive) {
340
- return failure(
341
- 'a group and a join key are both needed, and this is not an interactive terminal',
342
- 'pass them directly: cluster join --group <id> --key <join key>',
343
- parsed.json,
344
- );
345
- }
346
- const ask = input.prompt ?? askOnStdin;
347
-
348
- if (groupId === null) {
349
- const found = await call<readonly DiscoveredGroup[]>('/api/cluster/groups');
350
- if (!found.ok) return failure(found.error, found.fix, parsed.json);
351
- if (found.data.length === 0) {
352
- return failure(
353
- 'no goodvibes groups are advertising themselves on this network',
354
- 'check the other machine is switched on with clustering enabled, or join by id: '
355
- + 'cluster join --group <id> --key <join key>',
356
- parsed.json,
357
- );
358
- }
359
- const listing = renderDiscovered(found.data, now);
360
- for (const line of listing) process.stdout.write(`${line}\n`);
361
- const answer = await ask('\nwhich group? (name or id, blank to cancel): ');
362
- if (answer.length === 0) return { lines: ['cancelled'], exitCode: 130 };
363
- const chosen = found.data.find((group) => group.groupId === answer)
364
- ?? found.data.find((group) => group.displayName === answer)
365
- ?? found.data.find((group) => group.groupId.startsWith(answer));
366
- if (!chosen) {
367
- return failure(`no group on this network matches '${answer}'`, 'run `cluster groups` to see them again', parsed.json);
368
- }
369
- groupId = chosen.groupId;
370
- }
371
-
372
- if (joinKey === null) {
373
- joinKey = await ask('join key (run `cluster key` on a machine already in the group): ');
374
- if (joinKey.length === 0) return { lines: ['cancelled'], exitCode: 130 };
375
- }
376
- }
377
-
378
- const result = await call<JoinGroupResult>('/api/cluster/join', 'POST', { groupId, joinKey });
379
- return result.ok
380
- ? success(result.data, renderJoined(result.data), parsed.json)
381
- : failure(result.error, result.fix, parsed.json);
382
- }
@@ -1,225 +0,0 @@
1
- /**
2
- * remote-daemon-target.ts — how a subcommand reaches a daemon.
3
- *
4
- * Lives under src/cluster/ rather than src/daemon/ so the TUI can use it too:
5
- * `/cluster` and the `cluster` subcommands must reach the same daemon the same
6
- * way, and the architecture rules (rightly) forbid the input layer from
7
- * importing a CLI entrypoint.
8
- *
9
- * ── THE CONVENTION ────────────────────────────────────────────────────────
10
- *
11
- * This is the first daemon subcommand that talks to a RUNNING daemon rather
12
- * than to systemd, so it establishes the pattern every later one should follow.
13
- * If you are adding `goodvibes-daemon <something>` that needs the daemon
14
- * itself, use this module rather than inventing a second way.
15
- *
16
- * --host <name> default: the control plane's configured host, which for a
17
- * default install is 127.0.0.1
18
- * --port <n> default: the control plane's configured port
19
- * --token <t> default: this machine's operator token, read from
20
- * <daemon home>/operator-tokens.json
21
- *
22
- * Authentication is `Authorization: Bearer <operator token>` against the
23
- * control plane — the same credential the TUI and the web UI already use.
24
- *
25
- * The defaults are the whole point. A homelab box is headless: the operator has
26
- * SSHed into it and wants `cluster status` to work with no flags at all. Flags
27
- * exist for the other case — driving a machine in the next room from a laptop —
28
- * and for scripting.
29
- *
30
- * What this module deliberately does NOT do: hold any knowledge of what the
31
- * verbs mean. It resolves a target, makes a request, and returns the parsed
32
- * body. Every decision about what the answer MEANS belongs to the daemon.
33
- */
34
- import { readControlPlaneBinding } from '@pellux/goodvibes-sdk/platform/config';
35
- import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
36
- import { readOperatorTokenFile } from '@pellux/goodvibes-sdk/platform/workspace';
37
-
38
- /** Where to send a verb, and what to authenticate with. */
39
- export interface RemoteDaemonTarget {
40
- readonly baseUrl: string;
41
- readonly token: string;
42
- /** True when the target is this machine's own daemon, for message wording. */
43
- readonly isLocal: boolean;
44
- }
45
-
46
- /** The flags every remote-capable daemon subcommand accepts. */
47
- export interface RemoteTargetFlags {
48
- readonly host?: string | undefined;
49
- readonly port?: number | undefined;
50
- readonly token?: string | undefined;
51
- }
52
-
53
- export type RemoteTargetResolution =
54
- | { readonly ok: true; readonly target: RemoteDaemonTarget }
55
- | { readonly ok: false; readonly error: string; readonly fix: string };
56
-
57
- export interface ResolveRemoteTargetInput {
58
- readonly flags: RemoteTargetFlags;
59
- readonly configManager: Pick<ConfigManager, 'get'>;
60
- readonly daemonHomeDir: string;
61
- /** Injectable so tests never read a real token file. */
62
- readonly readToken?: ((daemonHomeDir: string) => string | undefined) | undefined;
63
- }
64
-
65
- /**
66
- * Pull the token out of the operator-token file's contents.
67
- *
68
- * `readOperatorTokenFile` hands back the FILE, and the file is a JSON record —
69
- * `{ token, peerId, createdAt }` — not a bare token. Sending the whole document
70
- * as a bearer credential produces a 401 that looks exactly like a stale token,
71
- * which is a genuinely confusing way to fail. A file that is not JSON is
72
- * treated as a bare token, which is what a hand-written one would be.
73
- */
74
- export function extractOperatorToken(contents: string | undefined): string | undefined {
75
- const trimmed = contents?.trim();
76
- if (!trimmed) return undefined;
77
- if (!trimmed.startsWith('{')) return trimmed;
78
- try {
79
- const parsed = JSON.parse(trimmed) as { token?: unknown };
80
- return typeof parsed.token === 'string' && parsed.token.length > 0 ? parsed.token : undefined;
81
- } catch {
82
- return undefined;
83
- }
84
- }
85
-
86
- /**
87
- * Work out which daemon to talk to.
88
- *
89
- * A missing token is a refusal rather than an anonymous attempt: sending an
90
- * unauthenticated request would come back 401 and the operator would be left
91
- * guessing whether the daemon was down or the credential was missing.
92
- */
93
- export function resolveRemoteDaemonTarget(input: ResolveRemoteTargetInput): RemoteTargetResolution {
94
- const binding = readControlPlaneBinding((key) => input.configManager.get(key as never));
95
- const host = input.flags.host?.trim() || binding.host;
96
- const port = input.flags.port ?? binding.port;
97
- if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
98
- return {
99
- ok: false,
100
- error: `'${String(port)}' is not a usable port`,
101
- fix: 'pass --port with a number between 1 and 65535',
102
- };
103
- }
104
-
105
- const isLocal = host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '0.0.0.0';
106
- const explicitToken = input.flags.token?.trim();
107
- const token = explicitToken
108
- || extractOperatorToken((input.readToken ?? readOperatorTokenFile)(input.daemonHomeDir));
109
- if (!token) {
110
- return {
111
- ok: false,
112
- error: 'no operator token was found for this machine',
113
- fix: isLocal
114
- ? 'start the daemon once so it creates its operator token, or pass --token'
115
- : 'pass --token with the operator token from the machine you are trying to reach',
116
- };
117
- }
118
-
119
- return { ok: true, target: { baseUrl: buildBaseUrl(host, port, binding.tlsMode), token, isLocal } };
120
- }
121
-
122
- /**
123
- * Build the URL to CALL.
124
- *
125
- * Deliberately not `deriveControlPlaneBaseUrl`. That function answers a
126
- * different question — where a daemon should BIND — and it applies the
127
- * `hostMode` policy while doing so: with the default `local` mode it forces
128
- * 127.0.0.1 and discards the host it was given. For a client that is precisely
129
- * wrong, and it silently turned `--host 10.0.0.7` into a call to this machine.
130
- *
131
- * Here the host the operator typed is the host, full stop. Only the scheme
132
- * comes from configuration.
133
- */
134
- function buildBaseUrl(host: string, port: number, tlsMode: string | undefined): string {
135
- const scheme = tlsMode && tlsMode !== 'off' && tlsMode !== 'none' ? 'https' : 'http';
136
- // An IPv6 literal has to be bracketed or the port reads as part of the address.
137
- const authority = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
138
- return `${scheme}://${authority}:${port}`;
139
- }
140
-
141
- /**
142
- * The one thing this module needs from `fetch`.
143
- *
144
- * Narrower than `typeof fetch` on purpose: the global carries runtime-specific
145
- * extras (Bun adds `preconnect`) that a test double has no business
146
- * implementing, and requiring them would push every test into an `any`.
147
- */
148
- export type DaemonFetch = (input: string, init?: RequestInit) => Promise<Response>;
149
-
150
- /** What a verb call came back with. */
151
- export type DaemonVerbOutcome<T> =
152
- | { readonly ok: true; readonly data: T }
153
- | { readonly ok: false; readonly error: string; readonly fix: string };
154
-
155
- /**
156
- * Call a daemon verb.
157
- *
158
- * Every failure shape becomes an `error`/`fix` pair in plain language, because
159
- * this is the layer where "ECONNREFUSED" has to turn into something an operator
160
- * can act on. The daemon's OWN refusals already carry a fix and are passed
161
- * through unchanged rather than being reworded here.
162
- */
163
- export async function callDaemonVerb<T>(
164
- target: RemoteDaemonTarget,
165
- path: string,
166
- init: { method: 'GET' | 'POST'; body?: unknown } = { method: 'GET' },
167
- fetchImpl: DaemonFetch = fetch,
168
- ): Promise<DaemonVerbOutcome<T>> {
169
- const where = target.isLocal ? 'the daemon on this machine' : `the daemon at ${target.baseUrl}`;
170
- let response: Response;
171
- try {
172
- response = await fetchImpl(`${target.baseUrl}${path}`, {
173
- method: init.method,
174
- headers: {
175
- Authorization: `Bearer ${target.token}`,
176
- ...(init.body === undefined ? {} : { 'Content-Type': 'application/json' }),
177
- },
178
- ...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
179
- });
180
- } catch (error) {
181
- return {
182
- ok: false,
183
- error: `could not reach ${where}`,
184
- fix: target.isLocal
185
- ? 'check the daemon is running: goodvibes-daemon service-status'
186
- : `check that machine is switched on and reachable, and that its daemon is listening on ${target.baseUrl} `
187
- + `(${error instanceof Error ? error.message : 'no further detail'})`,
188
- };
189
- }
190
-
191
- if (response.status === 401 || response.status === 403) {
192
- return {
193
- ok: false,
194
- error: `${where} refused the operator token`,
195
- fix: target.isLocal
196
- ? 'the token may be stale — restart the daemon, or pass --token'
197
- : 'pass --token with the operator token from that machine (its <daemon home>/operator-tokens.json)',
198
- };
199
- }
200
- if (response.status === 404) {
201
- return {
202
- ok: false,
203
- error: `${where} does not know this command`,
204
- fix: 'that daemon is running an older build — update it, then try again',
205
- };
206
- }
207
-
208
- let payload: unknown;
209
- try {
210
- payload = await response.json();
211
- } catch {
212
- return {
213
- ok: false,
214
- error: `${where} sent a reply this build could not read`,
215
- fix: 'check that both machines are running the same version',
216
- };
217
- }
218
- const body = payload as { ok?: unknown; data?: unknown; error?: unknown; fix?: unknown };
219
- if (body.ok === true) return { ok: true, data: body.data as T };
220
- return {
221
- ok: false,
222
- error: typeof body.error === 'string' ? body.error : `${where} refused the request`,
223
- fix: typeof body.fix === 'string' ? body.fix : 'run `cluster status` to see the current state',
224
- };
225
- }