@pellux/goodvibes-tui 1.27.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -2
- package/README.md +7 -9
- package/bin/goodvibes +2 -2
- package/bin/launcher-support.js +8 -4
- package/docs/README.md +11 -2
- package/docs/foundation-artifacts/operator-contract.json +6403 -2347
- package/package.json +14 -25
- package/scripts/postinstall.js +35 -35
- package/src/audio/spoken-turn-model-routing.ts +1 -1
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completions/generate.ts +1 -1
- package/src/cli/doctor.ts +3 -3
- package/src/cli/entrypoint.ts +12 -7
- package/src/cli/help.ts +4 -5
- package/src/cli/hooks-command.ts +1 -1
- package/src/cli/index.ts +1 -4
- package/src/cli/launch-auto-update.ts +59 -7
- package/src/cli/management-commands.ts +6 -6
- package/src/cli/management-utils.ts +5 -5
- package/src/cli/management.ts +7 -7
- package/src/cli/network-posture.ts +1 -1
- package/src/cli/package-verification.ts +7 -6
- package/src/cli/plugin-bundles-command.ts +1 -1
- package/src/cli/plugin-command.ts +2 -2
- package/src/cli/service-command.ts +2 -2
- package/src/cli/service-posture.ts +4 -4
- package/src/cli/status.ts +9 -8
- package/src/cli/surface-command.ts +4 -4
- package/src/cli/tui-startup.ts +12 -3
- package/src/cli-flags.ts +3 -3
- package/src/config/goodvibes-home-audit.ts +0 -2
- package/src/config/secret-config.ts +37 -5
- package/src/config/tui-extension-settings.ts +1 -1
- package/src/core/approval-alert.ts +2 -2
- package/src/core/budget-breach-notifier.ts +3 -3
- package/src/core/conversation-compaction-render.ts +1 -1
- package/src/core/conversation-line-cache.ts +1 -1
- package/src/core/conversation-render-context.ts +1 -1
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +10 -10
- package/src/core/long-task-notifier.ts +2 -2
- package/src/core/memory-diagnostics-gateway.ts +1 -1
- package/src/core/scriptable-statusline.ts +1 -1
- package/src/core/session-auto-titler.ts +1 -1
- package/src/core/session-resume-core.ts +2 -2
- package/src/core/stream-event-wiring.ts +24 -44
- package/src/core/terminal-notifier.ts +3 -3
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +5 -5
- package/src/input/command-registry.ts +6 -6
- package/src/input/commands/channel-pairing.ts +1 -1
- package/src/input/commands/cluster-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +1 -1
- package/src/input/commands/hosted-runtime.ts +330 -0
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup-transfer.ts +2 -2
- package/src/input/commands/local-setup.ts +3 -3
- package/src/input/commands/managed-runtime.ts +2 -2
- package/src/input/commands/operator-rpc.ts +24 -110
- package/src/input/commands/payment-card-intake.ts +1 -1
- package/src/input/commands/permissions-provenance.ts +2 -2
- package/src/input/commands/plugin-runtime.ts +1 -1
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/relay-runtime.ts +18 -9
- package/src/input/commands/rewind-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +1 -1
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +145 -55
- package/src/input/commands/update-runtime.ts +31 -28
- package/src/input/commands/work-plan-runtime.ts +2 -2
- package/src/input/commands/workstream-attempts.ts +1 -1
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands.ts +2 -0
- package/src/input/feature-unit-layout.ts +1 -1
- package/src/input/feed-context-factory.ts +3 -3
- package/src/input/handler-content-actions.ts +2 -2
- package/src/input/handler-feed-routes.ts +1 -1
- package/src/input/handler-feed.ts +3 -3
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
- package/src/input/handler-onboarding.ts +1 -1
- package/src/input/handler-types.ts +2 -2
- package/src/input/handler-ui-state.ts +1 -1
- package/src/input/handler.ts +4 -4
- package/src/input/input-history.ts +1 -1
- package/src/input/onboarding/onboarding-feature-units.ts +2 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
- package/src/input/payments-config.ts +6 -6
- package/src/input/search.ts +1 -1
- package/src/input/session-picker-modal.ts +46 -0
- package/src/input/settings-modal-activation.ts +1 -1
- package/src/input/settings-modal-behavior.ts +4 -4
- package/src/input/settings-modal-commit.ts +11 -0
- package/src/input/settings-modal-data.ts +13 -80
- package/src/input/settings-modal-mutations.ts +33 -0
- package/src/input/settings-modal-secrets.ts +34 -0
- package/src/input/settings-modal-subscriptions.ts +1 -1
- package/src/input/settings-modal-types.ts +15 -1
- package/src/input/settings-modal.ts +28 -3
- package/src/input/worktree-setup-config.ts +1 -1
- package/src/main.ts +11 -9
- package/src/panels/agent-inspector-shared.ts +2 -2
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/operations.ts +6 -8
- package/src/panels/builtin/session.ts +17 -0
- package/src/panels/builtin/shared.ts +5 -5
- package/src/panels/builtin-modals.ts +3 -3
- package/src/panels/confirm-state.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +4 -4
- package/src/panels/diff-panel.ts +2 -2
- package/src/panels/diff-review-panel.ts +2 -2
- package/src/panels/expandable-list-panel.ts +2 -2
- package/src/panels/fleet-acts.ts +1 -1
- package/src/panels/fleet-observed-render.ts +1 -1
- package/src/panels/fleet-panel-format.ts +1 -1
- package/src/panels/fleet-panel.ts +1 -1
- package/src/panels/fleet-spawn.ts +1 -1
- package/src/panels/fleet-steer.ts +1 -1
- package/src/panels/fleet-transcript.ts +1 -1
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/hosted-session-feed.ts +352 -0
- package/src/panels/hosted-session-panel.ts +221 -0
- package/src/panels/local-auth-panel.ts +3 -3
- package/src/panels/modals/devices-modal.ts +1 -1
- package/src/panels/modals/local-auth-modal.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +1 -1
- package/src/panels/modals/services-modal.ts +1 -1
- package/src/panels/modals/settings-sync-modal.ts +1 -1
- package/src/panels/modals/subscription-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +1 -1
- package/src/panels/notifications-panel.ts +1 -1
- package/src/panels/panel-confirm-overlay.ts +1 -1
- package/src/panels/plugins-panel.ts +2 -2
- package/src/panels/polish-core.ts +2 -2
- package/src/panels/polish-tables.ts +1 -1
- package/src/panels/polish.ts +2 -2
- package/src/panels/scrollable-list-panel.ts +3 -3
- package/src/panels/skills-panel.ts +3 -3
- package/src/panels/token-budget-panel.ts +5 -5
- package/src/panels/types.ts +1 -2
- package/src/permissions/prompt.ts +1 -1
- package/src/renderer/autocomplete-overlay.ts +1 -1
- package/src/renderer/block-actions-overlay.ts +1 -1
- package/src/renderer/bookmark-modal.ts +1 -1
- package/src/renderer/buffer.ts +1 -1
- package/src/renderer/code-block.ts +1 -1
- package/src/renderer/composer-fragments.ts +1 -1
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/context-inspector.ts +1 -1
- package/src/renderer/conversation-layout.ts +2 -2
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/conversation-surface.ts +2 -2
- package/src/renderer/diff-view.ts +1 -1
- package/src/renderer/diff.ts +1 -1
- package/src/renderer/file-picker-overlay.ts +1 -1
- package/src/renderer/fleet-tab-strip.ts +1 -1
- package/src/renderer/fullscreen-primitives.ts +2 -2
- package/src/renderer/fullscreen-workspace.ts +1 -1
- package/src/renderer/header-line.ts +1 -1
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/history-search-overlay.ts +2 -2
- package/src/renderer/layout.ts +5 -8
- package/src/renderer/markdown-inline.ts +1 -1
- package/src/renderer/markdown-table.ts +1 -1
- package/src/renderer/markdown.ts +1 -1
- package/src/renderer/mcp-workspace.ts +1 -1
- package/src/renderer/modal-factory.ts +2 -2
- package/src/renderer/model-workspace.ts +2 -2
- package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
- package/src/renderer/overlay-box.ts +1 -1
- package/src/renderer/panel-composite.ts +1 -1
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/profile-picker-modal.ts +1 -1
- package/src/renderer/search-overlay.ts +2 -2
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +124 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -2
- package/src/renderer/shell-surface.ts +2 -2
- package/src/renderer/system-message.ts +1 -1
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/theme-mode-config.ts +1 -1
- package/src/renderer/theme.ts +11 -12
- package/src/renderer/thinking.ts +1 -1
- package/src/renderer/tool-call.ts +2 -2
- package/src/renderer/tool-result-expanded-lines.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -8
- package/src/renderer/ui-factory.ts +3 -3
- package/src/runtime/bootstrap-command-context.ts +4 -4
- package/src/runtime/bootstrap-command-parts.ts +5 -5
- package/src/runtime/bootstrap-core.ts +30 -30
- package/src/runtime/bootstrap-shell.ts +4 -4
- package/src/runtime/bootstrap.ts +128 -157
- package/src/runtime/client/approval-updates.ts +82 -0
- package/src/runtime/client/build-floors.ts +167 -0
- package/src/runtime/client/daemon-attach-handshake.ts +115 -0
- package/src/runtime/client/fleet-union.ts +147 -0
- package/src/runtime/client/host-service-status.ts +71 -0
- package/src/runtime/client/hosted-exit.ts +86 -0
- package/src/runtime/client/hosted-roster.ts +110 -0
- package/src/runtime/client/hosted-session-stream.ts +178 -0
- package/src/runtime/client/hosted-sessions.ts +187 -0
- package/src/runtime/client/operator-endpoint.ts +213 -0
- package/src/runtime/client/spine-adoption.ts +99 -0
- package/src/runtime/cloudflare-control-plane.ts +1 -1
- package/src/runtime/context-accounting-source.ts +1 -1
- package/src/runtime/context.ts +2 -2
- package/src/runtime/conversation-rewind-port.ts +42 -14
- package/src/runtime/daemon-attach-notices.ts +55 -19
- package/src/runtime/diagnostics/panels/ops.ts +1 -1
- package/src/runtime/disposal-wiring.ts +4 -24
- package/src/runtime/index.ts +119 -105
- package/src/runtime/install-self-check-startup.ts +2 -2
- package/src/runtime/interaction-seams.ts +1 -1
- package/src/runtime/knowledge-services.ts +8 -2
- package/src/runtime/legacy-daemon-migration.ts +15 -3
- package/src/runtime/legacy-daemon-reconcile.ts +3 -2
- package/src/runtime/memory-spine-transport.ts +37 -267
- package/src/runtime/notification-dispatch.ts +2 -2
- package/src/runtime/onboarding/apply.ts +2 -2
- package/src/runtime/onboarding/derivation.ts +2 -2
- package/src/runtime/onboarding/markers.ts +2 -2
- package/src/runtime/onboarding/progress.ts +2 -2
- package/src/runtime/onboarding/snapshot.ts +1 -1
- package/src/runtime/onboarding/state.ts +2 -2
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/orchestrator-core-services.ts +1 -1
- package/src/runtime/path-shadow-startup.ts +28 -221
- package/src/runtime/power-keepawake-remote.ts +3 -2
- package/src/runtime/process-lifecycle.ts +2 -2
- package/src/runtime/provider-fallback.ts +10 -66
- package/src/runtime/recovery-autosave.ts +1 -1
- package/src/runtime/recovery-offer-wiring.ts +39 -23
- package/src/runtime/recovery-prompt.ts +31 -17
- package/src/runtime/relay-reachability-bridge.ts +53 -42
- package/src/runtime/release-artifacts.ts +2 -1
- package/src/runtime/runtime-services-types.ts +82 -35
- package/src/runtime/services.ts +305 -300
- package/src/runtime/session-resume-liveness-confirm.ts +1 -1
- package/src/runtime/ui-services.ts +7 -6
- package/src/runtime/update-check.ts +23 -43
- package/src/runtime/workspace-checkpointing.ts +1 -1
- package/src/runtime/wrfc-persistence.ts +2 -2
- package/src/shell/ui-openers.ts +30 -7
- package/src/utils/clipboard.ts +1 -1
- package/src/verification/verification-ledger.ts +10 -8
- package/src/version.ts +1 -1
- package/bin/goodvibes-daemon +0 -90
- package/src/cli/config-overrides.ts +0 -267
- package/src/cli/endpoints.ts +0 -109
- package/src/cli/parser.ts +0 -395
- package/src/cli/redaction.ts +0 -170
- package/src/cli/types.ts +0 -84
- package/src/cluster/commands.ts +0 -382
- package/src/cluster/remote-daemon-target.ts +0 -225
- package/src/cluster/render.ts +0 -268
- package/src/config/atomic-write.ts +0 -70
- package/src/config/credential-availability.ts +0 -122
- package/src/config/goodvibes-home.ts +0 -149
- package/src/config/index.ts +0 -88
- package/src/config/provider-model.ts +0 -23
- package/src/config/read-versioned.ts +0 -228
- package/src/core/alert-gating.ts +0 -67
- package/src/core/consolidation-receipt.ts +0 -41
- package/src/core/focus-tracker.ts +0 -41
- package/src/core/history.ts +0 -45
- package/src/core/memory-status.ts +0 -118
- package/src/core/orchestrator.ts +0 -7
- package/src/core/pairing-devices.ts +0 -49
- package/src/core/pairing-handoff.ts +0 -101
- package/src/core/pairing-offers.ts +0 -83
- package/src/core/pairing-origin.ts +0 -78
- package/src/core/rewind-turn-anchors.ts +0 -311
- package/src/core/session-recovery.ts +0 -210
- package/src/core/stable-host.ts +0 -171
- package/src/core/transcript-journal.ts +0 -503
- package/src/daemon/cli.ts +0 -641
- package/src/daemon/handlers/context.ts +0 -29
- package/src/daemon/handlers/contracts.ts +0 -77
- package/src/daemon/handlers/credentials.ts +0 -139
- package/src/daemon/handlers/drafts/draft-store.ts +0 -427
- package/src/daemon/handlers/drafts/index.ts +0 -17
- package/src/daemon/handlers/drafts/register.ts +0 -331
- package/src/daemon/handlers/errors.ts +0 -18
- package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
- package/src/daemon/handlers/inbox/index.ts +0 -284
- package/src/daemon/handlers/inbox/mapping.ts +0 -192
- package/src/daemon/handlers/inbox/poller.ts +0 -212
- package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
- package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
- package/src/daemon/handlers/inbox/providers/email.ts +0 -152
- package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
- package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
- package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
- package/src/daemon/handlers/index.ts +0 -117
- package/src/daemon/handlers/register.ts +0 -168
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
- package/src/daemon/handlers/remote/backends/docker.ts +0 -79
- package/src/daemon/handlers/remote/backends/index.ts +0 -40
- package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
- package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
- package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
- package/src/daemon/handlers/remote/backends/types.ts +0 -97
- package/src/daemon/handlers/remote/dispatcher.ts +0 -181
- package/src/daemon/handlers/remote/index.ts +0 -119
- package/src/daemon/handlers/remote/peer-registry.ts +0 -357
- package/src/daemon/handlers/remote/service.ts +0 -191
- package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
- package/src/daemon/handlers/routing/index.ts +0 -261
- package/src/daemon/handlers/routing/route-store.ts +0 -319
- package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
- package/src/daemon/handlers/sqlite-store.ts +0 -278
- package/src/daemon/handlers/triage/index.ts +0 -57
- package/src/daemon/handlers/triage/integration.ts +0 -213
- package/src/daemon/handlers/triage/pipeline.ts +0 -274
- package/src/daemon/handlers/triage/scorer.ts +0 -287
- package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
- package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
- package/src/daemon/handlers/triage/tagger/index.ts +0 -184
- package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
- package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
- package/src/daemon/handlers/triage/types.ts +0 -50
- package/src/daemon/lifecycle.ts +0 -41
- package/src/daemon/provision-wake-model.ts +0 -81
- package/src/daemon/safe-serve.ts +0 -61
- package/src/daemon/send/channels.ts +0 -200
- package/src/daemon/send/command.ts +0 -332
- package/src/daemon/send/composition.ts +0 -99
- package/src/daemon/send/failure-text.ts +0 -93
- package/src/daemon/send/inert-text.ts +0 -225
- package/src/daemon/send/stdin.ts +0 -24
- package/src/daemon/service-commands.ts +0 -329
- package/src/export/cost-utils.ts +0 -319
- package/src/input/selection.ts +0 -135
- package/src/plugins/loader.ts +0 -15
- package/src/renderer/bottom-bar.ts +0 -58
- package/src/renderer/conversation-tree.ts +0 -228
- package/src/runtime/agent-graph-composition.ts +0 -78
- package/src/runtime/channel-composition.ts +0 -46
- package/src/runtime/cluster-composition.ts +0 -124
- package/src/runtime/cluster-group-composition.ts +0 -284
- package/src/runtime/code-index-services.ts +0 -167
- package/src/runtime/daemon-handler-composition.ts +0 -76
- package/src/runtime/device-posture-composition.ts +0 -110
- package/src/runtime/durability-housekeeping.ts +0 -183
- package/src/runtime/durability-services.ts +0 -155
- package/src/runtime/embedded-service-factories.ts +0 -58
- package/src/runtime/feature-settings.ts +0 -154
- package/src/runtime/fleet-needs-input-push.ts +0 -70
- package/src/runtime/fleet-services.ts +0 -41
- package/src/runtime/idle-power-services.ts +0 -85
- package/src/runtime/mail-composition.ts +0 -65
- package/src/runtime/memory-fold.ts +0 -55
- package/src/runtime/operator-token-cleanup.ts +0 -109
- package/src/runtime/path-shadow.ts +0 -418
- package/src/runtime/perf/panel-health-monitor.ts +0 -18
- package/src/runtime/provider-optimizer-wiring.ts +0 -55
- package/src/runtime/reachability-notice.ts +0 -100
- package/src/runtime/remote-execution-composition.ts +0 -62
- package/src/runtime/session-liveness-marker.ts +0 -257
- package/src/runtime/session-pointer-surface.ts +0 -22
- package/src/runtime/session-storage-services.ts +0 -41
- package/src/runtime/surface-feature-flags.ts +0 -96
- package/src/runtime/terminal-output-guard.ts +0 -235
- package/src/runtime/trust/workspace-registration.ts +0 -192
- package/src/runtime/trust/workspace-trust.ts +0 -229
- package/src/runtime/ui/index.ts +0 -39
- package/src/runtime/ui/model-picker/data-provider.ts +0 -182
- package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
- package/src/runtime/ui/model-picker/index.ts +0 -59
- package/src/runtime/ui/model-picker/types.ts +0 -153
- package/src/runtime/ui/provider-health/data-provider.ts +0 -244
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
- package/src/runtime/ui/provider-health/index.ts +0 -46
- package/src/runtime/ui/provider-health/types.ts +0 -146
- package/src/runtime/ui-events.ts +0 -1
- package/src/runtime/ui-read-model-helpers.ts +0 -1
- package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
- package/src/runtime/ui-read-models-observability-options.ts +0 -1
- package/src/runtime/ui-read-models-observability-remote.ts +0 -1
- package/src/runtime/ui-read-models-observability-security.ts +0 -1
- package/src/runtime/ui-read-models-observability-system.ts +0 -1
- package/src/runtime/ui-read-models-observability.ts +0 -1
- package/src/runtime/ui-service-queries.ts +0 -1
- package/src/runtime/voice-setup-services.ts +0 -98
- package/src/runtime/workstream-attempts-validation.ts +0 -77
- package/src/runtime/workstream-draft-edits.ts +0 -95
- package/src/runtime/workstream-draft-store.ts +0 -274
- package/src/runtime/workstream-draft-types.ts +0 -42
- package/src/runtime/workstream-services.ts +0 -373
- package/src/tools/index.ts +0 -1
- package/src/types/grid.ts +0 -48
- package/src/types/sql-js.d.ts +0 -15
- package/src/work-plans/work-plan-store.ts +0 -702
package/src/cluster/render.ts
DELETED
|
@@ -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
|
-
}
|