@pellux/goodvibes-tui 1.27.1 → 2.0.2

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 (405) hide show
  1. package/CHANGELOG.md +127 -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 +3 -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 +9 -122
  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 +21 -33
  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 +4 -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 -693
  288. package/src/daemon/fatal-boot-report.ts +0 -111
  289. package/src/daemon/handlers/context.ts +0 -29
  290. package/src/daemon/handlers/contracts.ts +0 -77
  291. package/src/daemon/handlers/credentials.ts +0 -139
  292. package/src/daemon/handlers/drafts/draft-store.ts +0 -427
  293. package/src/daemon/handlers/drafts/index.ts +0 -17
  294. package/src/daemon/handlers/drafts/register.ts +0 -331
  295. package/src/daemon/handlers/errors.ts +0 -18
  296. package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
  297. package/src/daemon/handlers/inbox/index.ts +0 -284
  298. package/src/daemon/handlers/inbox/mapping.ts +0 -192
  299. package/src/daemon/handlers/inbox/poller.ts +0 -212
  300. package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
  301. package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
  302. package/src/daemon/handlers/inbox/providers/email.ts +0 -152
  303. package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
  304. package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
  305. package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
  306. package/src/daemon/handlers/index.ts +0 -117
  307. package/src/daemon/handlers/register.ts +0 -168
  308. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
  309. package/src/daemon/handlers/remote/backends/docker.ts +0 -79
  310. package/src/daemon/handlers/remote/backends/index.ts +0 -40
  311. package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
  312. package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
  313. package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
  314. package/src/daemon/handlers/remote/backends/types.ts +0 -97
  315. package/src/daemon/handlers/remote/dispatcher.ts +0 -181
  316. package/src/daemon/handlers/remote/index.ts +0 -119
  317. package/src/daemon/handlers/remote/peer-registry.ts +0 -357
  318. package/src/daemon/handlers/remote/service.ts +0 -191
  319. package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
  320. package/src/daemon/handlers/routing/index.ts +0 -261
  321. package/src/daemon/handlers/routing/route-store.ts +0 -319
  322. package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
  323. package/src/daemon/handlers/sqlite-store.ts +0 -278
  324. package/src/daemon/handlers/triage/index.ts +0 -57
  325. package/src/daemon/handlers/triage/integration.ts +0 -213
  326. package/src/daemon/handlers/triage/pipeline.ts +0 -274
  327. package/src/daemon/handlers/triage/scorer.ts +0 -287
  328. package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
  329. package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
  330. package/src/daemon/handlers/triage/tagger/index.ts +0 -184
  331. package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
  332. package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
  333. package/src/daemon/handlers/triage/types.ts +0 -50
  334. package/src/daemon/lifecycle.ts +0 -41
  335. package/src/daemon/provision-wake-model.ts +0 -81
  336. package/src/daemon/safe-serve.ts +0 -61
  337. package/src/daemon/send/channels.ts +0 -200
  338. package/src/daemon/send/command.ts +0 -332
  339. package/src/daemon/send/composition.ts +0 -99
  340. package/src/daemon/send/failure-text.ts +0 -93
  341. package/src/daemon/send/inert-text.ts +0 -225
  342. package/src/daemon/send/stdin.ts +0 -24
  343. package/src/daemon/service-commands.ts +0 -329
  344. package/src/export/cost-utils.ts +0 -319
  345. package/src/input/selection.ts +0 -135
  346. package/src/plugins/loader.ts +0 -15
  347. package/src/renderer/bottom-bar.ts +0 -58
  348. package/src/renderer/conversation-tree.ts +0 -228
  349. package/src/runtime/agent-graph-composition.ts +0 -78
  350. package/src/runtime/channel-composition.ts +0 -46
  351. package/src/runtime/cluster-composition.ts +0 -124
  352. package/src/runtime/cluster-group-composition.ts +0 -284
  353. package/src/runtime/code-index-services.ts +0 -167
  354. package/src/runtime/daemon-handler-composition.ts +0 -76
  355. package/src/runtime/device-posture-composition.ts +0 -110
  356. package/src/runtime/durability-housekeeping.ts +0 -183
  357. package/src/runtime/durability-services.ts +0 -155
  358. package/src/runtime/embedded-service-factories.ts +0 -74
  359. package/src/runtime/feature-settings.ts +0 -154
  360. package/src/runtime/fleet-needs-input-push.ts +0 -70
  361. package/src/runtime/fleet-services.ts +0 -41
  362. package/src/runtime/idle-power-services.ts +0 -85
  363. package/src/runtime/mail-composition.ts +0 -65
  364. package/src/runtime/memory-fold.ts +0 -55
  365. package/src/runtime/operator-token-cleanup.ts +0 -109
  366. package/src/runtime/path-shadow.ts +0 -418
  367. package/src/runtime/perf/panel-health-monitor.ts +0 -18
  368. package/src/runtime/provider-optimizer-wiring.ts +0 -55
  369. package/src/runtime/reachability-notice.ts +0 -100
  370. package/src/runtime/remote-execution-composition.ts +0 -62
  371. package/src/runtime/session-liveness-marker.ts +0 -257
  372. package/src/runtime/session-pointer-surface.ts +0 -22
  373. package/src/runtime/session-storage-services.ts +0 -41
  374. package/src/runtime/surface-feature-flags.ts +0 -96
  375. package/src/runtime/terminal-output-guard.ts +0 -235
  376. package/src/runtime/trust/workspace-registration.ts +0 -192
  377. package/src/runtime/trust/workspace-trust.ts +0 -229
  378. package/src/runtime/ui/index.ts +0 -39
  379. package/src/runtime/ui/model-picker/data-provider.ts +0 -182
  380. package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
  381. package/src/runtime/ui/model-picker/index.ts +0 -59
  382. package/src/runtime/ui/model-picker/types.ts +0 -153
  383. package/src/runtime/ui/provider-health/data-provider.ts +0 -244
  384. package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
  385. package/src/runtime/ui/provider-health/index.ts +0 -46
  386. package/src/runtime/ui/provider-health/types.ts +0 -146
  387. package/src/runtime/ui-events.ts +0 -1
  388. package/src/runtime/ui-read-model-helpers.ts +0 -1
  389. package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
  390. package/src/runtime/ui-read-models-observability-options.ts +0 -1
  391. package/src/runtime/ui-read-models-observability-remote.ts +0 -1
  392. package/src/runtime/ui-read-models-observability-security.ts +0 -1
  393. package/src/runtime/ui-read-models-observability-system.ts +0 -1
  394. package/src/runtime/ui-read-models-observability.ts +0 -1
  395. package/src/runtime/ui-service-queries.ts +0 -1
  396. package/src/runtime/voice-setup-services.ts +0 -98
  397. package/src/runtime/workstream-attempts-validation.ts +0 -77
  398. package/src/runtime/workstream-draft-edits.ts +0 -95
  399. package/src/runtime/workstream-draft-store.ts +0 -274
  400. package/src/runtime/workstream-draft-types.ts +0 -42
  401. package/src/runtime/workstream-services.ts +0 -373
  402. package/src/tools/index.ts +0 -1
  403. package/src/types/grid.ts +0 -48
  404. package/src/types/sql-js.d.ts +0 -15
  405. 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
- }