@pellux/goodvibes-tui 1.27.1 → 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 (405) hide show
  1. package/CHANGELOG.md +116 -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 +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 +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,268 +0,0 @@
1
- /**
2
- * render.ts — turning the daemon's answers into lines a person reads.
3
- *
4
- * Every function here is pure: structured data in, strings out. The verb layer
5
- * decided what is true; this decides how to say it. That split is what lets
6
- * `--json` print the same data with no second code path, and what lets the TUI
7
- * and a web UI render the same answers their own way.
8
- */
9
- import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
10
- import type {
11
- RotateKeyResult,
12
- CreateGroupResult,
13
- DiscoveredGroup,
14
- ForgetNodeResult,
15
- GroupStatusReport,
16
- JoinGroupResult,
17
- JoinKeyResult,
18
- NodesResult,
19
- } from '@pellux/goodvibes-sdk/platform/cluster';
20
-
21
- /** How long ago, in words. Never a raw timestamp — nobody reads epoch millis. */
22
- export function describeAge(at: number, now: number): string {
23
- const seconds = Math.max(0, Math.round((now - at) / 1_000));
24
- if (seconds < 60) return `${seconds}s ago`;
25
- const minutes = Math.round(seconds / 60);
26
- if (minutes < 60) return `${minutes}m ago`;
27
- const hours = Math.round(minutes / 60);
28
- if (hours < 48) return `${hours}h ago`;
29
- return `${Math.round(hours / 24)}d ago`;
30
- }
31
-
32
- function pad(value: string, width: number): string {
33
- return value.length >= width ? value : value + ' '.repeat(width - value.length);
34
- }
35
-
36
- /**
37
- * `cluster status`.
38
- *
39
- * Leads with the thing the operator came for — the role of THIS machine and
40
- * which surfaces it holds — and finishes with the one sentence of advice, if
41
- * there is anything to do.
42
- */
43
- export function renderStatus(report: GroupStatusReport, now: number): string[] {
44
- if (report.membership !== 'member') {
45
- return [
46
- report.membership === 'no-group'
47
- ? 'this machine is not in a group'
48
- : 'this machine has group key material that could not be read',
49
- `machine: ${report.nodeName} (${report.nodeId})`,
50
- `version: ${report.version}`,
51
- ...(report.advice ? ['', report.advice] : []),
52
- ];
53
- }
54
- const lines = [
55
- `group: ${report.groupName ?? '(unnamed)'} [${report.groupId ?? 'unknown'}]`,
56
- `machine: ${report.nodeName} (${report.nodeId})`,
57
- `members: ${report.memberCount}`,
58
- `version: ${report.version}`,
59
- ];
60
-
61
- if (report.surfaces === null) {
62
- lines.push('surfaces: not reported by this daemon');
63
- } else if (report.surfaces.length === 0) {
64
- lines.push('surfaces: none held by this machine right now');
65
- } else {
66
- lines.push('surfaces held by this machine:');
67
- for (const holding of report.surfaces) {
68
- lines.push(` ${holding.surfaceId} — ${holding.reason}`);
69
- }
70
- }
71
-
72
- lines.push(
73
- '',
74
- `group key: generation ${report.keyGeneration ?? 0}, rotates every ${report.rotationHours}h`,
75
- ` ${report.keyGenerationsHeld} of at most ${report.keyGenerationCap} generations kept; `
76
- + `accepting ${report.acceptedGenerations.join(' and ') || 'none'}`,
77
- );
78
- if (report.removedNodeCount > 0) {
79
- lines.push(`removed machines on record: ${report.removedNodeCount}`);
80
- }
81
- if (report.replication) {
82
- const replication = report.replication;
83
- lines.push(
84
- '',
85
- `shared settings: ${replication.entries} setting${replication.entries === 1 ? '' : 's'} and `
86
- + `${replication.secrets} credential${replication.secrets === 1 ? '' : 's'} at revision ${replication.revision}`,
87
- );
88
- if (replication.tombstones > 0) {
89
- lines.push(` ${replication.tombstones} deleted setting(s) still on record`);
90
- }
91
- if (replication.lastAppliedFrom) {
92
- lines.push(
93
- ` last change from ${replication.lastAppliedFrom}`
94
- + (replication.lastAppliedAt ? ` ${describeAge(replication.lastAppliedAt, now)}` : ''),
95
- );
96
- }
97
- if (replication.pendingProposals > 0) {
98
- lines.push(` ${replication.pendingProposals} change(s) waiting for the responsible machine`);
99
- }
100
- }
101
- if (report.wire) {
102
- lines.push(
103
- `traffic: ${report.wire.sent} sent, ${report.wire.received} received; dropped `
104
- + `${report.wire.droppedOtherGroup} from other groups, ${report.wire.droppedBadSignature} unverified, `
105
- + `${report.wire.droppedOldGeneration} on an old key, ${report.wire.droppedMalformed} malformed`
106
- + (report.wire.droppedNoGroup > 0
107
- ? `; ${report.wire.droppedNoGroup} not sent because this machine is in no group`
108
- : ''),
109
- );
110
- }
111
- if (report.advice) lines.push('', report.advice);
112
- // Deliberately absent, and it should stay absent: the join key, the group
113
- // key, any generation of it, this machine's private keys, and the VALUE of
114
- // any replicated credential. `cluster key` is the only path to the join key
115
- // and nothing shows the rest at all.
116
- return lines;
117
- }
118
-
119
- /** `cluster nodes`. */
120
- export function renderNodes(result: NodesResult, now: number): string[] {
121
- const lines = [`group: ${result.groupName} [${result.groupId}]`, ''];
122
- if (result.members.length === 0) {
123
- lines.push('no machines in this group yet');
124
- } else {
125
- const width = Math.max(...result.members.map((member) => member.displayName.length), 12);
126
- lines.push(`${pad('MACHINE', width)} ${pad('NODE ID', 38)} LAST SEEN`);
127
- for (const member of result.members) {
128
- const suffix = member.isThisMachine ? ' (this machine)' : '';
129
- lines.push(
130
- `${pad(member.displayName, width)} ${pad(member.nodeId, 38)} ${describeAge(member.lastSeenAt, now)}${suffix}`,
131
- );
132
- }
133
- }
134
- if (result.removed.length > 0) {
135
- lines.push('', 'removed from this group:');
136
- for (const entry of result.removed) {
137
- lines.push(` ${entry.nodeId} ${describeAge(entry.removedAt, now)} (${entry.reason})`);
138
- }
139
- }
140
- return lines;
141
- }
142
-
143
- /** `cluster create`. */
144
- export function renderCreated(result: CreateGroupResult): string[] {
145
- return [
146
- `created the group "${result.groupName}" [${result.groupId}]`,
147
- '',
148
- result.generatedKey
149
- ? 'a join key was generated for you. Run `cluster key` on this machine at any'
150
- : 'your join phrase is in use. Run `cluster key` on this machine at any',
151
- 'time to see it again — it is not a one-time reveal.',
152
- '',
153
- 'to add another machine: run `cluster join` on it and give it this key.',
154
- '',
155
- `the group name ("${result.groupName}") is visible to anything on this network that`,
156
- 'is listening for goodvibes groups. Change it any time with `cluster rename`.',
157
- ];
158
- }
159
-
160
- /** `cluster join`. */
161
- export function renderJoined(result: JoinGroupResult): string[] {
162
- return [
163
- `joined "${result.groupName}" [${result.groupId}]`,
164
- `the group now has ${result.memberCount} machine${result.memberCount === 1 ? '' : 's'}.`,
165
- ];
166
- }
167
-
168
- /** `cluster forget`. */
169
- export function renderForgotten(result: ForgetNodeResult): string[] {
170
- return [
171
- `removed ${result.displayName} (${result.nodeId}) from the group`,
172
- `the group key was replaced immediately, so that machine's copy no longer works.`,
173
- `${result.memberCount} machine${result.memberCount === 1 ? '' : 's'} remain${result.memberCount === 1 ? 's' : ''}.`,
174
- ];
175
- }
176
-
177
- /** The groups seen advertising themselves on this network. */
178
- export function renderDiscovered(groups: readonly DiscoveredGroup[], now: number): string[] {
179
- if (groups.length === 0) {
180
- return [
181
- 'no goodvibes groups are advertising themselves on this network',
182
- '',
183
- 'if the machine you want to join is switched on and has clustering enabled,',
184
- 'check that both machines are on the same network and that it is not blocking',
185
- 'multicast. You can also join by id directly: cluster join --group <id> --key <key>',
186
- ];
187
- }
188
- const width = Math.max(...groups.map((group) => group.displayName.length), 10);
189
- const lines = [`${pad('GROUP', width)} ${pad('ID', 18)} MACHINES VERSION SEEN`];
190
- for (const group of groups) {
191
- lines.push(
192
- `${pad(group.displayName, width)} ${pad(group.groupId, 18)} ${pad(String(group.nodeCount), 8)} `
193
- + `${pad(group.version, 7)} ${describeAge(group.lastSeenAt, now)}`,
194
- );
195
- }
196
- return lines;
197
- }
198
-
199
- /**
200
- * Ask the terminal to put the join key on the clipboard, using OSC 52.
201
- *
202
- * OSC 52 is the only clipboard mechanism that works through SSH, which is the
203
- * case that matters: the operator is on a headless box in a cupboard. It is
204
- * also not universally supported and gives no acknowledgement, so this NEVER
205
- * claims success — it says what it attempted, and the key is printed either
206
- * way. Silently doing nothing would be the worst of the three options.
207
- */
208
- export function clipboardEscapeSequence(value: string): string {
209
- return `]52;c;${Buffer.from(value, 'utf8').toString('base64')}`;
210
- }
211
-
212
- export interface JoinKeyRendering {
213
- /** Lines to print. */
214
- readonly lines: string[];
215
- /** The raw escape sequence to write, or null when the output is not a terminal. */
216
- readonly clipboardSequence: string | null;
217
- }
218
-
219
- export function renderJoinKey(result: JoinKeyResult, isTerminal: boolean): JoinKeyRendering {
220
- return {
221
- lines: [
222
- `join key for "${result.groupName}" [${result.groupId}]`,
223
- '',
224
- ` ${result.joinKey}`,
225
- '',
226
- ...(isTerminal
227
- ? ['copied to the clipboard, if this terminal accepts clipboard writes over its connection.',
228
- 'if nothing was copied, the key is printed above.']
229
- : ['(not a terminal, so no clipboard copy was attempted)']),
230
- '',
231
- `to add a machine: run cluster join --group ${result.groupId} --key <the key above>`,
232
- ],
233
- clipboardSequence: isTerminal ? clipboardEscapeSequence(result.joinKey) : null,
234
- };
235
- }
236
-
237
- /** `cluster rotate`. */
238
- export function renderRotated(result: RotateKeyResult): string[] {
239
- return [
240
- `the group key is now generation ${result.keyGeneration}`,
241
- result.immediate
242
- ? 'the previous key stopped being accepted immediately, and the key the group signs with'
243
- : 'the previous key stays accepted for a few minutes so nothing is interrupted while',
244
- result.immediate
245
- ? 'was replaced too. A machine that is asleep right now will ask to come back when it wakes.'
246
- : `the other ${Math.max(0, result.memberCount - 1)} machine(s) pick the new one up.`,
247
- '',
248
- `accepting generation${result.acceptedGenerations.length === 1 ? '' : 's'} `
249
- + `${result.acceptedGenerations.join(' and ')}`,
250
- ];
251
- }
252
-
253
- /**
254
- * The join key as a QR code, for typing it into a phone or a second terminal.
255
- *
256
- * This reuses the daemon's existing QR renderer verbatim — the same one the
257
- * pairing flow already prints to a terminal — rather than introducing any new
258
- * machinery for it. Opt-in behind `--qr`, because a QR block is tall and most
259
- * of the time the operator is copying the key with the mouse or the clipboard.
260
- */
261
- export function renderJoinKeyQr(joinKey: string): string[] {
262
- return [renderQrToString(generateQrMatrix(joinKey))];
263
- }
264
-
265
- /** A refusal, in the shape every failing subcommand prints. */
266
- export function renderFailure(error: string, fix: string): string[] {
267
- return [`cluster: ${error}`, ` ${fix}`];
268
- }
@@ -1,70 +0,0 @@
1
- import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
3
- import { dirname } from 'node:path';
4
-
5
- export interface AtomicWriteOptions {
6
- /**
7
- * File permission mode for the written file. Defaults to 0o600 (owner read/write only).
8
- */
9
- readonly mode?: number;
10
- /**
11
- * When true, creates parent directories if they do not exist. Defaults to false.
12
- */
13
- readonly mkdirp?: boolean;
14
- }
15
-
16
- /**
17
- * Writes `data` to `path` atomically: writes to a sibling temp file, fsyncs
18
- * it, then renames it into place. On POSIX, rename(2) is atomic so readers
19
- * always see either the old file or the new file — never a partial write.
20
- *
21
- * If the write or fsync fails, the temp file is cleaned up before rethrowing.
22
- *
23
- * @param path Destination file path.
24
- * @param data UTF-8 string content to write.
25
- * @param opts Optional mode and mkdirp flags.
26
- */
27
- export function atomicWriteFileSync(
28
- path: string,
29
- data: string,
30
- opts: AtomicWriteOptions = {},
31
- ): void {
32
- const mode = opts.mode ?? 0o600;
33
-
34
- if (opts.mkdirp) {
35
- mkdirSync(dirname(path), { recursive: true });
36
- }
37
-
38
- const tmp = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
39
-
40
- try {
41
- writeFileSync(tmp, data, { mode });
42
-
43
- // fsync the temp file to flush OS write buffers before rename.
44
- const fd = openSync(tmp, 'r+');
45
- try {
46
- fsyncSync(fd);
47
- } finally {
48
- closeSync(fd);
49
- }
50
-
51
- renameSync(tmp, path);
52
-
53
- // fsync the parent directory to ensure the renamed directory entry is
54
- // flushed. Without this, a hard crash immediately after rename could lose
55
- // the entry on some filesystems (ext3/ext4 with data=ordered, etc.).
56
- const dirFd = openSync(dirname(path), 'r');
57
- try {
58
- fsyncSync(dirFd);
59
- } finally {
60
- closeSync(dirFd);
61
- }
62
- } catch (err) {
63
- try {
64
- unlinkSync(tmp);
65
- } catch {
66
- // Best-effort cleanup — original error takes priority.
67
- }
68
- throw err;
69
- }
70
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * credential-availability.ts — client-side credential-status read.
3
- *
4
- * When the TUI acts as a CLIENT of an adopted external daemon (the host-service
5
- * `mode === 'external'` topology — see runtime/bootstrap.ts), provider/model/secret
6
- * STATUS is read from that daemon's `credentials.get` wire method rather than from
7
- * the TUI's own surfaceRoot ('tui') store. This module folds a `credentials.get`
8
- * outcome into an honest availability value, mirroring the goodvibes-webui v1.0.1
9
- * `deriveCredentialAvailability` contract exactly:
10
- *
11
- * - a 503 CREDENTIAL_STORE_UNAVAILABLE (matched by machine code), a METHOD_NOT_FOUND
12
- * from an older daemon, or ANY transport failure -> { available: false, reason }
13
- * — an honest, reason-carrying "unavailable" state.
14
- * - NEVER a fabricated "configured"; NEVER a secret byte. Only the boolean status
15
- * metadata surface (key / configured / usable / source / secure) is carried.
16
- *
17
- * STATUS ONLY moves to the daemon path. Secret RESOLUTION — the value-reads provider
18
- * auth needs, plus the env-only API-key posture — stays local and is untouched by
19
- * this module. The daemon's `credentials.get` never returns raw key bytes over the
20
- * wire (see the SDK decision record 2026-07-06-config-sharing-shared-tier-and-secret-read),
21
- * so no plaintext can reach a caller through this path by construction.
22
- */
23
-
24
- /** One credential's status metadata from the daemon's shared store — never bytes. */
25
- export interface CredentialStatusEntry {
26
- readonly key: string;
27
- readonly configured: boolean;
28
- readonly usable: boolean;
29
- readonly source?: string;
30
- readonly secure?: boolean;
31
- }
32
-
33
- /** Honest availability: either a status list, or a reason we could not read it. */
34
- export type CredentialAvailability =
35
- | { readonly available: true; readonly credentials: readonly CredentialStatusEntry[] }
36
- | { readonly available: false; readonly reason: string };
37
-
38
- /** A `credentials.get` invocation outcome — success value OR a thrown error. */
39
- export type CredentialStatusOutcome =
40
- | { readonly ok: true; readonly value: unknown }
41
- | { readonly ok: false; readonly error: unknown };
42
-
43
- /** Credential-read posture: read the daemon's shared store, or the local own store. */
44
- export type CredentialReadMode = 'host' | 'client';
45
-
46
- function asRecord(value: unknown): Record<string, unknown> | null {
47
- return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
48
- }
49
-
50
- function readString(record: Record<string, unknown> | null, key: string): string {
51
- if (!record) return '';
52
- const value = record[key];
53
- return typeof value === 'string' ? value : '';
54
- }
55
-
56
- /**
57
- * Map the authoritative host-service mode string to the credential-read posture.
58
- * Only 'external' (a separately-running daemon this TUI adopted) reads credential
59
- * STATUS over the wire; every other mode ('embedded'/'disabled'/'blocked'/
60
- * 'incompatible'/'unavailable') is the local host reading its own store.
61
- */
62
- export function credentialReadModeFromHostMode(hostMode: string): CredentialReadMode {
63
- return hostMode === 'external' ? 'client' : 'host';
64
- }
65
-
66
- /**
67
- * Fold a `credentials.get` outcome into an honest availability value. Mirrors
68
- * goodvibes-webui v1.0.1 `deriveCredentialAvailability`: any failure becomes a
69
- * reason-carrying `available: false`; a malformed success (no credentials array)
70
- * is treated as unavailable rather than fabricated as configured; on success only
71
- * the boolean status surface is carried, never a secret value.
72
- */
73
- export function deriveCredentialAvailability(outcome: CredentialStatusOutcome): CredentialAvailability {
74
- if (!outcome.ok) {
75
- const err = asRecord(outcome.error);
76
- const code = readString(err, 'code');
77
- if (code === 'CREDENTIAL_STORE_UNAVAILABLE') {
78
- return { available: false, reason: 'The daemon has no shared credential store wired.' };
79
- }
80
- if (code === 'METHOD_NOT_FOUND' || code === 'NOT_INVOKABLE') {
81
- return { available: false, reason: 'This daemon does not serve credential status yet.' };
82
- }
83
- return { available: false, reason: 'Credential status unavailable right now.' };
84
- }
85
- const value = asRecord(outcome.value);
86
- const raw = value?.credentials;
87
- if (!Array.isArray(raw)) return { available: false, reason: 'Credential status unavailable right now.' };
88
- const credentials: CredentialStatusEntry[] = [];
89
- for (const item of raw) {
90
- const rec = asRecord(item);
91
- const key = readString(rec, 'key');
92
- if (!rec || !key) continue;
93
- credentials.push({
94
- key,
95
- configured: rec.configured === true,
96
- usable: rec.usable === true,
97
- source: readString(rec, 'source') || undefined,
98
- secure: rec.secure === true ? true : rec.secure === false ? false : undefined,
99
- });
100
- }
101
- return { available: true, credentials };
102
- }
103
-
104
- /**
105
- * Client-side credential-status read. Invokes the adopted external daemon's
106
- * `credentials.get` and folds success OR any thrown transport/daemon error into an
107
- * honest availability. The daemon's `credentials.get` transport error carries the
108
- * machine `code` at the top level (e.g. an HttpStatusError whose body supplied
109
- * `CREDENTIAL_STORE_UNAVAILABLE` / `METHOD_NOT_FOUND`), which `deriveCredentialAvailability`
110
- * classifies. Only the boolean status surface is returned; a raw secret value can
111
- * never reach a caller through this path.
112
- */
113
- export async function readClientCredentialStatus(
114
- invokeCredentialsGet: () => Promise<unknown>,
115
- ): Promise<CredentialAvailability> {
116
- try {
117
- const value = await invokeCredentialsGet();
118
- return deriveCredentialAvailability({ ok: true, value });
119
- } catch (error) {
120
- return deriveCredentialAvailability({ ok: false, error });
121
- }
122
- }
@@ -1,149 +0,0 @@
1
- /**
2
- * goodvibes-home.ts — where this process's GoodVibes tree lives, resolved in
3
- * ONE place for every entry point.
4
- *
5
- * ## The defect this closes
6
- *
7
- * `GOODVIBES_HOME` relocates the tree root. It was read by exactly one file —
8
- * `src/daemon/cli.ts` — and the CLIENT entry point (`src/main.ts`) called
9
- * `homedir()` unconditionally, on the same object literal where it did honour
10
- * `GOODVIBES_WORKING_DIR`. So a harness that set `GOODVIBES_HOME` to a
11
- * throwaway directory and then ran a client command got a process that read and
12
- * wrote the REAL tree: settings, workspace state, and — because the daemon-tier
13
- * secret store is derived from the tree root — `~/.goodvibes/daemon/secrets.enc`.
14
- *
15
- * That is not hypothetical. An isolated round stored two throwaway credentials
16
- * and they landed in the owner's live secret store, where they had to be
17
- * removed by hand. An isolation boundary a process can walk out of is not a
18
- * boundary; the honest fix is that no entry point resolves a home of its own.
19
- *
20
- * ## What each variable means
21
- *
22
- * - `GOODVIBES_HOME` — the tree ROOT, the directory `.goodvibes/` sits under.
23
- * Setting it relocates settings, workspace, discovery roots, and every tier
24
- * of the secret store. This is what an isolated harness or a service unit
25
- * sets.
26
- * - `GOODVIBES_DAEMON_HOME` — the daemon's own IDENTITY directory (auth users,
27
- * operator tokens, daemon settings). It falls under the tree root unless set
28
- * separately, and it names only that directory: it is not a second way to
29
- * move the tree. Both entry points resolve it the same way now, so a client
30
- * given only this variable no longer computes the daemon-tier secret path
31
- * from the real home while everything else points at a sandbox.
32
- *
33
- * Both readers take `env` as an argument rather than reaching for
34
- * `process.env`, so the resolution is exercisable without a test having to
35
- * mutate the environment of the process running it.
36
- */
37
-
38
- import { homedir } from 'node:os';
39
- import { isAbsolute, join, resolve } from 'node:path';
40
-
41
- /** The two roots every surface derives its storage from. */
42
- export interface GoodVibesHomeOwnership {
43
- /** The tree root: the directory `.goodvibes/` sits under. */
44
- readonly homeDirectory: string;
45
- /** The daemon's own identity directory, under the tree root by default. */
46
- readonly daemonHomeDirectory: string;
47
- /**
48
- * True when either `GOODVIBES_HOME` or `GOODVIBES_DAEMON_HOME` named an
49
- * override (a blank/whitespace-only value does not count — see
50
- * resolveGoodVibesHome). Callers that need to know "is this process running
51
- * out of a relocated tree or identity directory" (e.g. to refuse adopting
52
- * the machine's real service unit) read this instead of re-reading the raw
53
- * environment variable themselves — this module is the one reader.
54
- */
55
- readonly isOverridden: boolean;
56
- }
57
-
58
- /**
59
- * The tree root for this process.
60
- *
61
- * A blank or whitespace-only value is treated as absent rather than as the
62
- * empty path, because `GOODVIBES_HOME=` in a unit file or a shell wrapper means
63
- * "unset", and resolving it to `''` would put the whole tree at the filesystem
64
- * root.
65
- */
66
- export function resolveGoodVibesHome(env: NodeJS.ProcessEnv = process.env): string {
67
- const override = env['GOODVIBES_HOME']?.trim();
68
- if (override) return isAbsolute(override) ? override : resolve(process.cwd(), override);
69
- // The login home comes from the injected env first so the whole resolution is
70
- // exercisable without a test mutating the environment it is running in. This
71
- // is not a behaviour change: on Linux and macOS `homedir()` already prefers
72
- // $HOME, and where it does not (Windows, where HOME is usually unset) the
73
- // fallback below is what answers.
74
- const loginHome = env['HOME']?.trim();
75
- return loginHome ? loginHome : homedir();
76
- }
77
-
78
- /**
79
- * The daemon's identity directory.
80
- *
81
- * @param homeDirectory - The already-resolved tree root, so a caller cannot
82
- * accidentally combine an overridden root with a default daemon home.
83
- */
84
- export function resolveGoodVibesDaemonHome(
85
- homeDirectory: string,
86
- env: NodeJS.ProcessEnv = process.env,
87
- ): string {
88
- const override = env['GOODVIBES_DAEMON_HOME']?.trim();
89
- if (!override) return join(homeDirectory, '.goodvibes', 'daemon');
90
- return isAbsolute(override) ? override : resolve(process.cwd(), override);
91
- }
92
-
93
- /**
94
- * The `.goodvibes` directory itself — the tree, not the root it sits under.
95
- *
96
- * This exists because the two were being derived independently and did not
97
- * agree. `GOODVIBES_HOME` meant the tree ROOT to the runtime
98
- * (`<GOODVIBES_HOME>/.goodvibes/...`) and meant the `.goodvibes` DIRECTORY to
99
- * the two reporting scripts, which read it as
100
- * `process.env.GOODVIBES_HOME ?? join(homedir(), '.goodvibes')`. Set it to
101
- * `/tmp/sandbox` to isolate a run and the runtime wrote
102
- * `/tmp/sandbox/.goodvibes/`, while a script inspected `/tmp/sandbox` as though
103
- * it were already the tree — so the two halves of one round disagreed about
104
- * which files were even in scope.
105
- *
106
- * That is the shape of an incident that has already happened here: a
107
- * home-redirection variable honoured by one entry point and ignored by another
108
- * put two throwaway credentials in the owner's live secret store. One variable
109
- * has one meaning now, and it is the runtime's, because the runtime is what
110
- * writes secrets and because an isolation harness needs a single root that
111
- * everything falls under.
112
- *
113
- * A caller that genuinely wants to name a `.goodvibes` directory directly —
114
- * auditing a tree that is not under any home — passes it explicitly rather than
115
- * through the environment. Both scripts keep a `--home` flag for exactly that.
116
- */
117
- export function resolveGoodVibesTreeDirectory(env: NodeJS.ProcessEnv = process.env): string {
118
- return join(resolveGoodVibesHome(env), '.goodvibes');
119
- }
120
-
121
- /** Both roots at once — what an entry point wants. */
122
- export function resolveGoodVibesHomeOwnership(
123
- env: NodeJS.ProcessEnv = process.env,
124
- ): GoodVibesHomeOwnership {
125
- const homeDirectory = resolveGoodVibesHome(env);
126
- return {
127
- homeDirectory,
128
- daemonHomeDirectory: resolveGoodVibesDaemonHome(homeDirectory, env),
129
- isOverridden: Boolean(env['GOODVIBES_HOME']?.trim()) || Boolean(env['GOODVIBES_DAEMON_HOME']?.trim()),
130
- };
131
- }
132
-
133
- /**
134
- * True when either root was relocated for this process.
135
- *
136
- * The daemon lifecycle asks this to decide whether it may adopt the machine's
137
- * service unit: a process told to run out of somewhere else is a throwaway, and
138
- * one that adopted the unit had systemd supervise it as the machine's daemon
139
- * for five hours while it read the real home's credentials.
140
- *
141
- * It lives here because the two variables have exactly one reader, and the
142
- * answer is a question ABOUT them. A caller checking `process.env` for itself is
143
- * how the second meaning that started all of this gets reintroduced — so the
144
- * question is answered here rather than asked again somewhere else.
145
- */
146
- export function hasOverriddenGoodVibesHome(env: NodeJS.ProcessEnv = process.env): boolean {
147
- return (env['GOODVIBES_HOME']?.trim() ?? '') !== ''
148
- || (env['GOODVIBES_DAEMON_HOME']?.trim() ?? '') !== '';
149
- }