@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
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-liveness-marker.ts — best-effort "this session is open in another
|
|
3
|
-
* terminal" signal for multi-instance safety.
|
|
4
|
-
*
|
|
5
|
-
* Two independent TUI instances resuming or writing recovery snapshots for
|
|
6
|
-
* the SAME session can silently step on each other: one instance's periodic
|
|
7
|
-
* recovery write looks, to a second instance, indistinguishable from a
|
|
8
|
-
* genuine crash snapshot, and resuming a session another instance still has
|
|
9
|
-
* open forks its live state without warning.
|
|
10
|
-
*
|
|
11
|
-
* This module writes a small marker file (session id + pid), refreshed on
|
|
12
|
-
* the SAME 60s cadence as the existing recovery-file write (main.ts), so a
|
|
13
|
-
* second instance can do a best-effort liveness check before either warning
|
|
14
|
-
* about a stale-looking recovery snapshot or resuming a live session out
|
|
15
|
-
* from under its owner.
|
|
16
|
-
*
|
|
17
|
-
* Every check here is best-effort by design: a missing or stale marker never
|
|
18
|
-
* blocks anything — it just means "we can't tell, proceed as before". This is
|
|
19
|
-
* a convenience signal, not a lock.
|
|
20
|
-
*
|
|
21
|
-
* Location: <homeDirectory>/.goodvibes/<surfaceRoot>/liveness/<sessionId>.json.
|
|
22
|
-
* Both halves come off the caller's SessionSurface rather than being spelled
|
|
23
|
-
* out here, so the markers land under the same scope as the sessions and
|
|
24
|
-
* recovery snapshots they describe — mirrors the transcript-journal.ts
|
|
25
|
-
* convention (homeDirectory-scoped, under the surface's own directory).
|
|
26
|
-
*/
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
28
|
-
import { dirname, join } from 'node:path';
|
|
29
|
-
import type { SessionSurface } from '@/runtime/index.ts';
|
|
30
|
-
|
|
31
|
-
export interface LivenessMarker {
|
|
32
|
-
readonly sessionId: string;
|
|
33
|
-
readonly pid: number;
|
|
34
|
-
readonly updatedAt: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A marker older than this is treated as stale even if its pid happens to
|
|
39
|
-
* still resolve to a running process (pid reuse after a crash) — the marker
|
|
40
|
-
* is refreshed every 60s (main.ts's recovery cadence), so anything past
|
|
41
|
-
* ~2.5x that cadence means the writer stopped refreshing it.
|
|
42
|
-
*/
|
|
43
|
-
export const LIVENESS_STALE_AFTER_MS = 150_000;
|
|
44
|
-
|
|
45
|
-
/** Absolute path to the directory holding every session's liveness marker. */
|
|
46
|
-
export function livenessMarkerDirFor(surface: SessionSurface): string {
|
|
47
|
-
return join(surface.homeDirectory, '.goodvibes', surface.surfaceRoot, 'liveness');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Absolute path to a session's liveness marker file. */
|
|
51
|
-
export function livenessMarkerPathFor(surface: SessionSurface, sessionId: string): string {
|
|
52
|
-
return join(livenessMarkerDirFor(surface), `${sessionId}.json`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Refresh (creating if needed) the liveness marker for this session/pid. Best-effort — never throws. */
|
|
56
|
-
export function writeLivenessMarker(surface: SessionSurface, sessionId: string, pid: number = process.pid): void {
|
|
57
|
-
try {
|
|
58
|
-
const path = livenessMarkerPathFor(surface, sessionId);
|
|
59
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
60
|
-
const marker: LivenessMarker = { sessionId, pid, updatedAt: Date.now() };
|
|
61
|
-
writeFileSync(path, JSON.stringify(marker), { mode: 0o600 });
|
|
62
|
-
} catch {
|
|
63
|
-
// Best-effort — a missed liveness refresh never blocks anything.
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Best-effort removal on exit. Never throws. */
|
|
68
|
-
export function removeLivenessMarker(surface: SessionSurface, sessionId: string): void {
|
|
69
|
-
try {
|
|
70
|
-
const path = livenessMarkerPathFor(surface, sessionId);
|
|
71
|
-
if (existsSync(path)) unlinkSync(path);
|
|
72
|
-
} catch {
|
|
73
|
-
// Best-effort.
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Parse one marker file by absolute path. Returns null for missing, empty, torn, or wrong-shaped content. */
|
|
78
|
-
function parseMarkerFile(path: string): LivenessMarker | null {
|
|
79
|
-
try {
|
|
80
|
-
if (!existsSync(path)) return null;
|
|
81
|
-
const text = readFileSync(path, 'utf-8');
|
|
82
|
-
// A crash between create and write leaves a zero-byte file. Content, not
|
|
83
|
-
// existence, decides whether this marker says anything at all.
|
|
84
|
-
if (text.trim().length === 0) return null;
|
|
85
|
-
const parsed = JSON.parse(text) as Partial<LivenessMarker>;
|
|
86
|
-
if (typeof parsed.sessionId !== 'string' || typeof parsed.pid !== 'number' || typeof parsed.updatedAt !== 'number') return null;
|
|
87
|
-
if (!Number.isFinite(parsed.pid) || !Number.isFinite(parsed.updatedAt)) return null;
|
|
88
|
-
return { sessionId: parsed.sessionId, pid: parsed.pid, updatedAt: parsed.updatedAt };
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function readLivenessMarker(surface: SessionSurface, sessionId: string): LivenessMarker | null {
|
|
95
|
-
return parseMarkerFile(livenessMarkerPathFor(surface, sessionId));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Best-effort check that a pid still refers to a running process. Injectable for tests. */
|
|
99
|
-
export function isPidAlive(pid: number, kill: (pid: number, signal: 0) => void = process.kill.bind(process)): boolean {
|
|
100
|
-
try {
|
|
101
|
-
kill(pid, 0);
|
|
102
|
-
return true;
|
|
103
|
-
} catch (err) {
|
|
104
|
-
// ESRCH = no such process. Any other error (e.g. EPERM — exists but this
|
|
105
|
-
// process lacks permission to signal it) is treated as "still alive" —
|
|
106
|
-
// best-effort here means erring toward not silently dropping a real warning.
|
|
107
|
-
return (err as NodeJS.ErrnoException | undefined)?.code === 'EPERM';
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface SessionLivenessCheck {
|
|
112
|
-
readonly live: boolean;
|
|
113
|
-
readonly pid: number | null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Best-effort: is `sessionId` apparently open in another still-running
|
|
118
|
-
* process right now? A missing, stale (older than LIVENESS_STALE_AFTER_MS),
|
|
119
|
-
* or unreadable marker all resolve to `{ live: false, pid: null }` — never a
|
|
120
|
-
* throw, never a block on the caller.
|
|
121
|
-
*/
|
|
122
|
-
export function checkSessionLiveness(
|
|
123
|
-
surface: SessionSurface,
|
|
124
|
-
sessionId: string,
|
|
125
|
-
opts: { readonly now?: () => number; readonly isPidAliveFn?: typeof isPidAlive } = {},
|
|
126
|
-
): SessionLivenessCheck {
|
|
127
|
-
const marker = readLivenessMarker(surface, sessionId);
|
|
128
|
-
if (!marker) return { live: false, pid: null };
|
|
129
|
-
const now = opts.now?.() ?? Date.now();
|
|
130
|
-
if (now - marker.updatedAt > LIVENESS_STALE_AFTER_MS) return { live: false, pid: null };
|
|
131
|
-
const aliveCheck = opts.isPidAliveFn ?? isPidAlive;
|
|
132
|
-
if (!aliveCheck(marker.pid)) return { live: false, pid: null };
|
|
133
|
-
return { live: true, pid: marker.pid };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ─── Reaping ────────────────────────────────────────────────────────────────
|
|
137
|
-
//
|
|
138
|
-
// `removeLivenessMarker` only runs on a CLEAN exit (process-lifecycle.ts). A
|
|
139
|
-
// SIGKILL, a panic, or a pulled power cord leaves the marker behind forever,
|
|
140
|
-
// and `checkSessionLiveness` treating it as not-live does not remove the file
|
|
141
|
-
// — so the liveness directory grows one file per session id ever opened. The
|
|
142
|
-
// sweep below is the reclaim half of that lifecycle.
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Hard ceiling on marker files kept after a sweep, oldest dropped first.
|
|
146
|
-
*
|
|
147
|
-
* The liveness rule below already reclaims ordinary crash residue, but it can
|
|
148
|
-
* be defeated: `isPidAlive` deliberately reports EPERM (a pid this user cannot
|
|
149
|
-
* signal) as alive, so on a busy host a reused pid can keep a dead session's
|
|
150
|
-
* marker "live" indefinitely. This cap is the second, unconditional bound so
|
|
151
|
-
* the directory can never grow without limit whatever the pid probe says.
|
|
152
|
-
*/
|
|
153
|
-
export const LIVENESS_MARKER_MAX_FILES = 200;
|
|
154
|
-
|
|
155
|
-
export interface LivenessReapResult {
|
|
156
|
-
/** Marker files examined this sweep. */
|
|
157
|
-
readonly scanned: number;
|
|
158
|
-
/** Marker files deleted this sweep. */
|
|
159
|
-
readonly reaped: number;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface LivenessReapOptions {
|
|
163
|
-
readonly now?: () => number;
|
|
164
|
-
readonly isPidAliveFn?: typeof isPidAlive;
|
|
165
|
-
/**
|
|
166
|
-
* Session ids that must never be reaped regardless of what the marker says
|
|
167
|
-
* — normally just the current session's id. Belt-and-braces: the current
|
|
168
|
-
* session refreshes its own marker every 60s, so the liveness rule already
|
|
169
|
-
* keeps it.
|
|
170
|
-
*/
|
|
171
|
-
readonly keepSessionIds?: readonly string[];
|
|
172
|
-
/** Override the count cap (tests). */
|
|
173
|
-
readonly maxFiles?: number;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Delete liveness markers that no longer describe a running session.
|
|
178
|
-
*
|
|
179
|
-
* A marker is reaped when it is either:
|
|
180
|
-
* - unreadable, empty, or not shaped like a marker (crash residue that can
|
|
181
|
-
* never resolve to a live session again — validated by parsing the
|
|
182
|
-
* content, not by the file merely existing); or
|
|
183
|
-
* - definitively not live: older than `LIVENESS_STALE_AFTER_MS` AND its pid
|
|
184
|
-
* no longer resolves to a running process.
|
|
185
|
-
*
|
|
186
|
-
* Both halves of that second rule are required on purpose. Deleting is
|
|
187
|
-
* destructive while a liveness answer is only advisory, so a marker that is
|
|
188
|
-
* stale-but-pid-alive (a wedged owner still holding the session) or
|
|
189
|
-
* fresh-but-pid-dead (a process that died seconds ago, or a pid probe that
|
|
190
|
-
* failed transiently) is left for a later sweep. A genuinely crashed session
|
|
191
|
-
* satisfies both conditions within `LIVENESS_STALE_AFTER_MS` and is reclaimed
|
|
192
|
-
* on the next sweep after that.
|
|
193
|
-
*
|
|
194
|
-
* Idempotent and safe to run concurrently from several processes: a marker
|
|
195
|
-
* another sweeper unlinked between the directory listing and this unlink
|
|
196
|
-
* (ENOENT) counts as reaped, never as an error.
|
|
197
|
-
*/
|
|
198
|
-
export function reapStaleLivenessMarkers(surface: SessionSurface, opts: LivenessReapOptions = {}): LivenessReapResult {
|
|
199
|
-
const dir = livenessMarkerDirFor(surface);
|
|
200
|
-
let names: string[];
|
|
201
|
-
try {
|
|
202
|
-
names = readdirSync(dir).filter((name) => name.endsWith('.json'));
|
|
203
|
-
} catch {
|
|
204
|
-
// No liveness directory yet, or it is unreadable — nothing to reclaim.
|
|
205
|
-
return { scanned: 0, reaped: 0 };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const now = opts.now?.() ?? Date.now();
|
|
209
|
-
const aliveCheck = opts.isPidAliveFn ?? isPidAlive;
|
|
210
|
-
const keep = new Set(opts.keepSessionIds ?? []);
|
|
211
|
-
const maxFiles = opts.maxFiles ?? LIVENESS_MARKER_MAX_FILES;
|
|
212
|
-
|
|
213
|
-
let reaped = 0;
|
|
214
|
-
/** Survivors, so the count cap below can drop the oldest of them. */
|
|
215
|
-
const survivors: { readonly path: string; readonly updatedAt: number }[] = [];
|
|
216
|
-
|
|
217
|
-
for (const name of names) {
|
|
218
|
-
const sessionId = name.slice(0, -'.json'.length);
|
|
219
|
-
if (keep.has(sessionId)) continue;
|
|
220
|
-
const path = join(dir, name);
|
|
221
|
-
const marker = parseMarkerFile(path);
|
|
222
|
-
if (marker === null) {
|
|
223
|
-
if (unlinkIfPresent(path)) reaped++;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
const stale = now - marker.updatedAt > LIVENESS_STALE_AFTER_MS;
|
|
227
|
-
if (stale && !aliveCheck(marker.pid)) {
|
|
228
|
-
if (unlinkIfPresent(path)) reaped++;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
survivors.push({ path, updatedAt: marker.updatedAt });
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (survivors.length > maxFiles) {
|
|
235
|
-
survivors.sort((a, b) => a.updatedAt - b.updatedAt); // oldest first
|
|
236
|
-
for (const victim of survivors.slice(0, survivors.length - maxFiles)) {
|
|
237
|
-
if (unlinkIfPresent(victim.path)) reaped++;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return { scanned: names.length, reaped };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Best-effort unlink shared by the sweep. Returns true when the file is gone
|
|
246
|
-
* because of (or by the time of) this call. A concurrent sweeper winning the
|
|
247
|
-
* race (ENOENT) is success, not failure; any other error leaves the file for
|
|
248
|
-
* the next sweep.
|
|
249
|
-
*/
|
|
250
|
-
function unlinkIfPresent(path: string): boolean {
|
|
251
|
-
try {
|
|
252
|
-
unlinkSync(path);
|
|
253
|
-
return true;
|
|
254
|
-
} catch (err) {
|
|
255
|
-
return (err as NodeJS.ErrnoException | undefined)?.code === 'ENOENT';
|
|
256
|
-
}
|
|
257
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { writeLastSessionPointer } from '@/runtime/index.ts';
|
|
2
|
-
import type { SessionSurface } from '@/runtime/index.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Binds the SDK's two-argument `writeLastSessionPointer(sessionId, options?)`
|
|
6
|
-
* to one `SessionSurface`, returning a plain `(sessionId: string) => void`.
|
|
7
|
-
*
|
|
8
|
-
* Never pass the raw SDK function reference into a `(sessionId: string) =>
|
|
9
|
-
* void` slot. A function with an extra optional parameter is structurally
|
|
10
|
-
* assignable there, so it type-checks, but every downstream caller
|
|
11
|
-
* (bootstrap-shell.ts -> bootstrap-hook-bridge.ts's resume handler) invokes
|
|
12
|
-
* it with exactly one argument: `options` arrives as `undefined`, the legacy
|
|
13
|
-
* compat path's `requireWorkingDirectory(undefined)` throws, and
|
|
14
|
-
* `writeLastSessionPointer`'s own try/catch swallows that into a logged
|
|
15
|
-
* warning. The pointer file then silently never gets written after a resume,
|
|
16
|
-
* and the next launch's `--continue` / boot notice sees nothing. That was the
|
|
17
|
-
* live defect at bootstrap.ts's `createBootstrapShell({ writeLastSessionPointer })`
|
|
18
|
-
* call. See session-surface-pointer-journey.test.ts for the on-disk proof.
|
|
19
|
-
*/
|
|
20
|
-
export function bindWriteLastSessionPointerToSurface(surface: SessionSurface): (sessionId: string) => void {
|
|
21
|
-
return (sessionId: string): void => writeLastSessionPointer(sessionId, { surface });
|
|
22
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session-storage-services.ts — the declare-once storage handle and the
|
|
3
|
-
* session store built on it.
|
|
4
|
-
*
|
|
5
|
-
* Every path the app uses for session state (the sessions directory, the
|
|
6
|
-
* last-session pointer, crash-recovery snapshots, the checkpoint store, the
|
|
7
|
-
* transcript journal, the multi-instance liveness markers) derives from ONE
|
|
8
|
-
* `SessionSurface`, built here, once, at composition time.
|
|
9
|
-
*
|
|
10
|
-
* The reason it is one object rather than a scope argument repeated at each
|
|
11
|
-
* call site: when each site derived its own scope, they drifted. A resume
|
|
12
|
-
* wrote the last-session pointer with no scope at all — into the shared,
|
|
13
|
-
* unscoped `.goodvibes/` directory — while `--continue` and the boot notice
|
|
14
|
-
* read a scoped path under `.goodvibes/tui/`. Nothing errored; the pointer
|
|
15
|
-
* simply never existed where anyone looked. `createSessionSurface` also
|
|
16
|
-
* throws synchronously on a bad scope instead of silently resolving to the
|
|
17
|
-
* unscoped fallback on first use, so a mistake here fails at boot rather than
|
|
18
|
-
* six months later as missing state.
|
|
19
|
-
*/
|
|
20
|
-
import { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
21
|
-
import { createSessionSurface, type SessionSurface } from '@/runtime/index.ts';
|
|
22
|
-
import { GOODVIBES_TUI_SURFACE_ROOT } from '../config/surface.ts';
|
|
23
|
-
|
|
24
|
-
export interface SessionStorageServices {
|
|
25
|
-
readonly surface: SessionSurface;
|
|
26
|
-
readonly sessionManager: SessionManager;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function createSessionStorageServices(input: {
|
|
30
|
-
readonly workingDirectory: string;
|
|
31
|
-
readonly homeDirectory: string;
|
|
32
|
-
}): SessionStorageServices {
|
|
33
|
-
// Also performs the SDK's one-time migration of pre-surface state (pointer
|
|
34
|
-
// copy-forward, journal moves) into the scoped location.
|
|
35
|
-
const surface = createSessionSurface({
|
|
36
|
-
surfaceRoot: GOODVIBES_TUI_SURFACE_ROOT,
|
|
37
|
-
workingDirectory: input.workingDirectory,
|
|
38
|
-
homeDirectory: input.homeDirectory,
|
|
39
|
-
});
|
|
40
|
-
return { surface, sessionManager: new SessionManager(input.workingDirectory, { surface }) };
|
|
41
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* surface-feature-flags — which platform capabilities a surface/server setup
|
|
3
|
-
* needs, and how to switch them on through their real domain settings keys.
|
|
4
|
-
*
|
|
5
|
-
* Capabilities are configured through first-class domain settings (see
|
|
6
|
-
* feature-settings.ts); enabling a surface writes keys like
|
|
7
|
-
* `controlPlane.gateway` or `surfaces.slack.enabled`, never a separate
|
|
8
|
-
* enablement namespace.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { ConfigManager } from '../config/index.ts';
|
|
12
|
-
import { surfaceFeatureGateId } from '@/runtime/index.ts';
|
|
13
|
-
import { featureEnablementWrite, getFeatureSetting, isFeatureConfigEnabled } from './feature-settings.ts';
|
|
14
|
-
|
|
15
|
-
export const CONTROL_PLANE_FEATURE_FLAG = 'control-plane-gateway';
|
|
16
|
-
export const ROUTE_BINDING_FEATURE_FLAG = 'route-binding';
|
|
17
|
-
export const DELIVERY_ENGINE_FEATURE_FLAG = 'delivery-engine';
|
|
18
|
-
export const SERVICE_MANAGEMENT_FEATURE_FLAG = 'service-management';
|
|
19
|
-
|
|
20
|
-
const CORE_CHANNEL_FEATURE_FLAGS = [
|
|
21
|
-
CONTROL_PLANE_FEATURE_FLAG,
|
|
22
|
-
ROUTE_BINDING_FEATURE_FLAG,
|
|
23
|
-
DELIVERY_ENGINE_FEATURE_FLAG,
|
|
24
|
-
] as const;
|
|
25
|
-
|
|
26
|
-
export function getSurfaceFeatureFlag(surfaceId: string): string | null {
|
|
27
|
-
// Onboarding surface ids may be camelCase ('googleChat'); the SDK's gate
|
|
28
|
-
// map is keyed by the kebab-case ChannelSurface id ('google-chat'). Try the
|
|
29
|
-
// literal id first, then the kebab-case form, so choosing Google Chat in
|
|
30
|
-
// onboarding really flips google-chat-surface.
|
|
31
|
-
const literal = surfaceFeatureGateId(surfaceId);
|
|
32
|
-
if (literal) return literal;
|
|
33
|
-
const kebab = surfaceId.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
|
|
34
|
-
return kebab === surfaceId ? null : surfaceFeatureGateId(kebab);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getServerSurfaceFeatureFlags(options: {
|
|
38
|
-
readonly serverBacked?: boolean;
|
|
39
|
-
readonly web?: boolean;
|
|
40
|
-
readonly externalSurfaces?: readonly string[];
|
|
41
|
-
}): readonly string[] {
|
|
42
|
-
const flags = new Set<string>();
|
|
43
|
-
const hasExternalSurfaces = (options.externalSurfaces?.length ?? 0) > 0;
|
|
44
|
-
|
|
45
|
-
if (options.serverBacked || options.web || hasExternalSurfaces) {
|
|
46
|
-
flags.add(CONTROL_PLANE_FEATURE_FLAG);
|
|
47
|
-
flags.add(SERVICE_MANAGEMENT_FEATURE_FLAG);
|
|
48
|
-
}
|
|
49
|
-
if (options.web) {
|
|
50
|
-
const webFlag = getSurfaceFeatureFlag('web');
|
|
51
|
-
if (webFlag) flags.add(webFlag);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (hasExternalSurfaces) {
|
|
55
|
-
for (const flag of CORE_CHANNEL_FEATURE_FLAGS) flags.add(flag);
|
|
56
|
-
for (const surfaceId of options.externalSurfaces ?? []) {
|
|
57
|
-
const surfaceFlag = getSurfaceFeatureFlag(surfaceId);
|
|
58
|
-
if (surfaceFlag) flags.add(surfaceFlag);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return [...flags].sort((left, right) => left.localeCompare(right));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Whether the live config has the capability on, derived from its domain settings key. */
|
|
66
|
-
export function isFeatureFlagEnabled(config: Pick<ConfigManager, 'get'>, flagId: string): boolean {
|
|
67
|
-
return isFeatureConfigEnabled(config, flagId);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Translate internal feature-gate ids (e.g. 'control-plane-gateway') to the
|
|
72
|
-
* canonical domain settings keys the user actually sets (e.g.
|
|
73
|
-
* 'controlPlane.gateway') — matching the SDK's own gate errors, which name the
|
|
74
|
-
* settings key. A gate with no mapped setting falls back to its id. Deduped and
|
|
75
|
-
* sorted for stable output.
|
|
76
|
-
*/
|
|
77
|
-
export function surfaceFeatureGateSettingsKeys(flagIds: readonly string[]): string[] {
|
|
78
|
-
const keys = new Set<string>();
|
|
79
|
-
for (const flagId of flagIds) keys.add(getFeatureSetting(flagId)?.enablement.key ?? flagId);
|
|
80
|
-
return [...keys].sort((left, right) => left.localeCompare(right));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function getMissingSurfaceFeatureFlags(config: Pick<ConfigManager, 'get'>, surfaceId: string): readonly string[] {
|
|
84
|
-
const required = surfaceId === 'web'
|
|
85
|
-
? getServerSurfaceFeatureFlags({ web: true })
|
|
86
|
-
: getServerSurfaceFeatureFlags({ externalSurfaces: [surfaceId] });
|
|
87
|
-
return required.filter((flagId) => !isFeatureFlagEnabled(config, flagId));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Switch each capability on by writing its real enablement settings key. */
|
|
91
|
-
export function enableFeatureFlags(config: Pick<ConfigManager, 'setDynamic'>, flagIds: readonly string[]): void {
|
|
92
|
-
for (const flagId of flagIds) {
|
|
93
|
-
const write = featureEnablementWrite(flagId, true);
|
|
94
|
-
if (write) config.setDynamic(write.key, write.value);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import { format } from 'node:util';
|
|
2
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
3
|
-
|
|
4
|
-
type WritableStreamLike = {
|
|
5
|
-
write: {
|
|
6
|
-
(buffer: string | Uint8Array, cb?: (error?: Error | null) => void): boolean;
|
|
7
|
-
(buffer: string | Uint8Array, encoding?: BufferEncoding, cb?: (error?: Error | null) => void): boolean;
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type TerminalOutputInterceptSource =
|
|
12
|
-
| 'stdout'
|
|
13
|
-
| 'stderr'
|
|
14
|
-
| 'console.debug'
|
|
15
|
-
| 'console.error'
|
|
16
|
-
| 'console.info'
|
|
17
|
-
| 'console.log'
|
|
18
|
-
| 'console.warn';
|
|
19
|
-
|
|
20
|
-
export type TerminalOutputIntercept = {
|
|
21
|
-
readonly source: TerminalOutputInterceptSource;
|
|
22
|
-
readonly text: string;
|
|
23
|
-
readonly preview: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type TerminalOutputGuard = {
|
|
27
|
-
setActive(active: boolean): void;
|
|
28
|
-
allowTerminalWrite<T>(fn: () => T): T;
|
|
29
|
-
dispose(): void;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type TerminalOutputGuardOptions = {
|
|
33
|
-
readonly stdout: WritableStreamLike;
|
|
34
|
-
readonly stderr?: WritableStreamLike;
|
|
35
|
-
readonly active?: boolean;
|
|
36
|
-
readonly onIntercept?: (event: TerminalOutputIntercept) => void;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type TuiTerminalOutputGuardOptions = {
|
|
40
|
-
readonly stdout: WritableStreamLike;
|
|
41
|
-
readonly stderr?: WritableStreamLike;
|
|
42
|
-
readonly active?: boolean;
|
|
43
|
-
/**
|
|
44
|
-
* Called (rate-limited) after intercepts with the cumulative count of direct
|
|
45
|
-
* writes captured this session, so an honest quiet counter (e.g. /debug) can
|
|
46
|
-
* refresh. The per-write detail is already recorded to the activity log via
|
|
47
|
-
* logger.warn — this callback intentionally does NOT push transcript lines.
|
|
48
|
-
* (item 1a.)
|
|
49
|
-
*/
|
|
50
|
-
readonly onCapture?: (total: number) => void;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const MAX_LOG_TEXT = 4_000;
|
|
54
|
-
const MAX_PREVIEW_TEXT = 180;
|
|
55
|
-
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
56
|
-
|
|
57
|
-
let currentGuard: TerminalOutputGuard | null = null;
|
|
58
|
-
|
|
59
|
-
function writeCallback(args: unknown[]): ((error?: Error | null) => void) | undefined {
|
|
60
|
-
const maybeCallback = args[args.length - 1];
|
|
61
|
-
return typeof maybeCallback === 'function'
|
|
62
|
-
? maybeCallback as (error?: Error | null) => void
|
|
63
|
-
: undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function chunkToText(chunk: unknown): string {
|
|
67
|
-
if (typeof chunk === 'string') return chunk;
|
|
68
|
-
if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString('utf8');
|
|
69
|
-
return String(chunk);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function normalizeText(text: string): string {
|
|
73
|
-
return text.replace(ANSI_RE, '').replace(/\r/g, '').trim();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function previewText(text: string): string {
|
|
77
|
-
const singleLine = normalizeText(text).replace(/\s+/g, ' ');
|
|
78
|
-
if (singleLine.length <= MAX_PREVIEW_TEXT) return singleLine;
|
|
79
|
-
return `${singleLine.slice(0, MAX_PREVIEW_TEXT - 1)}...`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function truncateForLog(text: string): string {
|
|
83
|
-
if (text.length <= MAX_LOG_TEXT) return text;
|
|
84
|
-
return `${text.slice(0, MAX_LOG_TEXT)}\n[truncated ${text.length - MAX_LOG_TEXT} byte(s)]`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function invokeSuppressedCallback(args: unknown[]): void {
|
|
88
|
-
const callback = writeCallback(args);
|
|
89
|
-
if (callback) {
|
|
90
|
-
queueMicrotask(() => callback(null));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function allowTerminalWrite<T>(fn: () => T): T {
|
|
95
|
-
return currentGuard ? currentGuard.allowTerminalWrite(fn) : fn();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function installTerminalOutputGuard(options: TerminalOutputGuardOptions): TerminalOutputGuard {
|
|
99
|
-
const stdout = options.stdout;
|
|
100
|
-
const stderr = options.stderr ?? process.stderr;
|
|
101
|
-
const originalStdoutWriteMethod = stdout.write;
|
|
102
|
-
const originalStderrWriteMethod = stderr.write;
|
|
103
|
-
const originalStdoutWrite = (...args: unknown[]): boolean =>
|
|
104
|
-
Reflect.apply(originalStdoutWriteMethod, stdout, args) as boolean;
|
|
105
|
-
const originalStderrWrite = (...args: unknown[]): boolean =>
|
|
106
|
-
Reflect.apply(originalStderrWriteMethod, stderr, args) as boolean;
|
|
107
|
-
const originalConsole = {
|
|
108
|
-
debug: console.debug.bind(console),
|
|
109
|
-
error: console.error.bind(console),
|
|
110
|
-
info: console.info.bind(console),
|
|
111
|
-
log: console.log.bind(console),
|
|
112
|
-
warn: console.warn.bind(console),
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
let active = options.active ?? true;
|
|
116
|
-
let disposed = false;
|
|
117
|
-
let allowDepth = 0;
|
|
118
|
-
let captureDepth = 0;
|
|
119
|
-
|
|
120
|
-
const record = (source: TerminalOutputInterceptSource, text: string): void => {
|
|
121
|
-
if (disposed || !active) return;
|
|
122
|
-
const normalized = normalizeText(text);
|
|
123
|
-
if (!normalized) return;
|
|
124
|
-
if (normalized.startsWith('[ActivityLogger]')) return;
|
|
125
|
-
if (captureDepth > 0) return;
|
|
126
|
-
|
|
127
|
-
captureDepth++;
|
|
128
|
-
try {
|
|
129
|
-
const event: TerminalOutputIntercept = {
|
|
130
|
-
source,
|
|
131
|
-
text: truncateForLog(normalized),
|
|
132
|
-
preview: previewText(normalized),
|
|
133
|
-
};
|
|
134
|
-
logger.warn('Intercepted terminal output while TUI renderer was active', {
|
|
135
|
-
source: event.source,
|
|
136
|
-
text: event.text,
|
|
137
|
-
});
|
|
138
|
-
options.onIntercept?.(event);
|
|
139
|
-
} finally {
|
|
140
|
-
captureDepth--;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const shouldPassThrough = (): boolean => !active || allowDepth > 0 || disposed;
|
|
145
|
-
|
|
146
|
-
stdout.write = ((...args: unknown[]) => {
|
|
147
|
-
if (shouldPassThrough()) {
|
|
148
|
-
return originalStdoutWrite(...args);
|
|
149
|
-
}
|
|
150
|
-
record('stdout', chunkToText(args[0]));
|
|
151
|
-
invokeSuppressedCallback(args);
|
|
152
|
-
return true;
|
|
153
|
-
}) as WritableStreamLike['write'];
|
|
154
|
-
|
|
155
|
-
stderr.write = ((...args: unknown[]) => {
|
|
156
|
-
if (shouldPassThrough()) {
|
|
157
|
-
return originalStderrWrite(...args);
|
|
158
|
-
}
|
|
159
|
-
record('stderr', chunkToText(args[0]));
|
|
160
|
-
invokeSuppressedCallback(args);
|
|
161
|
-
return true;
|
|
162
|
-
}) as WritableStreamLike['write'];
|
|
163
|
-
|
|
164
|
-
console.debug = (...args: unknown[]) => {
|
|
165
|
-
if (!active || disposed) return originalConsole.debug(...args);
|
|
166
|
-
record('console.debug', format(...args));
|
|
167
|
-
};
|
|
168
|
-
console.error = (...args: unknown[]) => {
|
|
169
|
-
if (!active || disposed) return originalConsole.error(...args);
|
|
170
|
-
record('console.error', format(...args));
|
|
171
|
-
};
|
|
172
|
-
console.info = (...args: unknown[]) => {
|
|
173
|
-
if (!active || disposed) return originalConsole.info(...args);
|
|
174
|
-
record('console.info', format(...args));
|
|
175
|
-
};
|
|
176
|
-
console.log = (...args: unknown[]) => {
|
|
177
|
-
if (!active || disposed) return originalConsole.log(...args);
|
|
178
|
-
record('console.log', format(...args));
|
|
179
|
-
};
|
|
180
|
-
console.warn = (...args: unknown[]) => {
|
|
181
|
-
if (!active || disposed) return originalConsole.warn(...args);
|
|
182
|
-
record('console.warn', format(...args));
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const guard: TerminalOutputGuard = {
|
|
186
|
-
setActive(nextActive) {
|
|
187
|
-
active = nextActive;
|
|
188
|
-
},
|
|
189
|
-
allowTerminalWrite<T>(fn: () => T): T {
|
|
190
|
-
allowDepth++;
|
|
191
|
-
try {
|
|
192
|
-
return fn();
|
|
193
|
-
} finally {
|
|
194
|
-
allowDepth--;
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
dispose() {
|
|
198
|
-
if (disposed) return;
|
|
199
|
-
disposed = true;
|
|
200
|
-
stdout.write = originalStdoutWriteMethod;
|
|
201
|
-
stderr.write = originalStderrWriteMethod;
|
|
202
|
-
console.debug = originalConsole.debug;
|
|
203
|
-
console.error = originalConsole.error;
|
|
204
|
-
console.info = originalConsole.info;
|
|
205
|
-
console.log = originalConsole.log;
|
|
206
|
-
console.warn = originalConsole.warn;
|
|
207
|
-
if (currentGuard === guard) {
|
|
208
|
-
currentGuard = null;
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
currentGuard = guard;
|
|
214
|
-
return guard;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function installTuiTerminalOutputGuard(options: TuiTerminalOutputGuardOptions): TerminalOutputGuard {
|
|
218
|
-
let totalInterceptedWrites = 0;
|
|
219
|
-
let lastNoticeAt = 0;
|
|
220
|
-
return installTerminalOutputGuard({
|
|
221
|
-
stdout: options.stdout,
|
|
222
|
-
stderr: options.stderr,
|
|
223
|
-
active: options.active,
|
|
224
|
-
onIntercept: () => {
|
|
225
|
-
// Each intercept is already logged (logger.warn in record()). Here we only
|
|
226
|
-
// maintain a cumulative counter and, rate-limited, refresh a quiet counter
|
|
227
|
-
// surface — no repeated transcript lines. (item 1a.)
|
|
228
|
-
totalInterceptedWrites++;
|
|
229
|
-
const now = Date.now();
|
|
230
|
-
if (now - lastNoticeAt < 5_000) return;
|
|
231
|
-
lastNoticeAt = now;
|
|
232
|
-
options.onCapture?.(totalInterceptedWrites);
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
}
|