@pellux/goodvibes-tui 1.16.1 → 1.18.0
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 +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
105
105
|
"@ast-grep/napi": "^0.42.0",
|
|
106
106
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
107
|
-
"@pellux/goodvibes-sdk": "1.
|
|
108
|
-
"@pellux/goodvibes-terminal-shell": "1.
|
|
107
|
+
"@pellux/goodvibes-sdk": "1.9.0",
|
|
108
|
+
"@pellux/goodvibes-terminal-shell": "1.9.0",
|
|
109
109
|
"bash-language-server": "^5.6.0",
|
|
110
110
|
"fuse.js": "^7.1.0",
|
|
111
111
|
"graphql": "^16.13.2",
|
|
@@ -131,10 +131,10 @@
|
|
|
131
131
|
"typescript": "^5.9.3"
|
|
132
132
|
},
|
|
133
133
|
"optionalDependencies": {
|
|
134
|
-
"@pellux/goodvibes-tui-linux-x64": "1.
|
|
135
|
-
"@pellux/goodvibes-tui-linux-arm64": "1.
|
|
136
|
-
"@pellux/goodvibes-tui-darwin-x64": "1.
|
|
137
|
-
"@pellux/goodvibes-tui-darwin-arm64": "1.
|
|
134
|
+
"@pellux/goodvibes-tui-linux-x64": "1.18.0",
|
|
135
|
+
"@pellux/goodvibes-tui-linux-arm64": "1.18.0",
|
|
136
|
+
"@pellux/goodvibes-tui-darwin-x64": "1.18.0",
|
|
137
|
+
"@pellux/goodvibes-tui-darwin-arm64": "1.18.0"
|
|
138
138
|
},
|
|
139
139
|
"trustedDependencies": [],
|
|
140
140
|
"overrides": {
|
|
@@ -93,8 +93,8 @@ export const GLOBAL_FLAGS: readonly CompletionFlag[] = [
|
|
|
93
93
|
takesValue: true,
|
|
94
94
|
description: 'Override a config value (key=value)',
|
|
95
95
|
},
|
|
96
|
-
{ name: '--enable', takesValue: true, description: 'Enable a feature
|
|
97
|
-
{ name: '--disable', takesValue: true, description: 'Disable a feature
|
|
96
|
+
{ name: '--enable', takesValue: true, description: 'Enable a platform feature for this launch (writes its domain setting)' },
|
|
97
|
+
{ name: '--disable', takesValue: true, description: 'Disable a platform feature for this launch (writes its domain setting)' },
|
|
98
98
|
{
|
|
99
99
|
name: '--prompt',
|
|
100
100
|
short: '-p',
|
|
@@ -139,6 +139,7 @@ export const GLOBAL_FLAGS: readonly CompletionFlag[] = [
|
|
|
139
139
|
{ name: '--fork', takesValue: false, description: 'Fork session when supported' },
|
|
140
140
|
{ name: '--yes', short: '-y', takesValue: false, description: 'Auto-confirm prompts (non-interactive)' },
|
|
141
141
|
{ name: '--non-interactive', takesValue: false, description: 'Disable all interactive prompts (implies --yes)' },
|
|
142
|
+
{ name: '--strict', takesValue: false, description: 'doctor: also fail on advisory findings, for CI' },
|
|
142
143
|
] as const;
|
|
143
144
|
|
|
144
145
|
// ---------------------------------------------------------------------------
|
|
@@ -504,8 +505,8 @@ export function generateZsh(surface: CompletionSurface): string {
|
|
|
504
505
|
lines.push(` '--working-dir[Alias for --cd]:dir:_files -/' \\`);
|
|
505
506
|
lines.push(` '--daemon-home[Override daemon home]:dir:_files -/' \\`);
|
|
506
507
|
lines.push(` '(-c --config)'{-c,--config}'[Override config value]:key=value:' \\`);
|
|
507
|
-
lines.push(` '--enable[Enable feature
|
|
508
|
-
lines.push(` '--disable[Disable feature
|
|
508
|
+
lines.push(` '--enable[Enable platform feature]:feature:' \\`);
|
|
509
|
+
lines.push(` '--disable[Disable platform feature]:feature:' \\`);
|
|
509
510
|
lines.push(` '(-p --prompt)'{-p,--prompt}'[Run non-interactive prompt]:text:' \\`);
|
|
510
511
|
lines.push(` '--print[Non-interactive run mode]' \\`);
|
|
511
512
|
lines.push(` '(-o --output --output-format)'{-o,--output}'[Output format]:format:(text json stream-json)' \\`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import type { ConfigKey, ConfigManager, ConfigSetting, GoodVibesConfig
|
|
2
|
+
import type { ConfigKey, ConfigManager, ConfigSetting, GoodVibesConfig } from '../config/index.ts';
|
|
3
3
|
import { CONFIG_SCHEMA, ConfigError } from '../config/index.ts';
|
|
4
|
+
import { featureEnablementWrite, getFeatureSetting } from '../runtime/feature-settings.ts';
|
|
4
5
|
import type { GoodVibesCliCommand, GoodVibesCliFlags } from './types.ts';
|
|
5
6
|
import { RUNTIME_ENDPOINT_CONFIG_KEYS, hostModeForHostname } from './endpoints.ts';
|
|
6
7
|
import type { RuntimeEndpointId } from './endpoints.ts';
|
|
@@ -130,6 +131,32 @@ export function applyRuntimeConfigDefault(configManager: ConfigManager, key: Con
|
|
|
130
131
|
applyRuntimeConfigValue(configManager, key, defaultValue);
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Every TUI-side config default applied at startup, in one place. Currently:
|
|
136
|
+
* show token speed ON (the SDK schema default is false). applyRuntimeConfigDefault
|
|
137
|
+
* reads the global + project settings files and only applies the default
|
|
138
|
+
* in-memory when the key is absent from both (e.g. a new install); an explicit
|
|
139
|
+
* user value is respected. No disk write.
|
|
140
|
+
*/
|
|
141
|
+
export function applyTuiRuntimeConfigDefaults(configManager: ConfigManager): void {
|
|
142
|
+
applyRuntimeConfigDefault(configManager, 'display.showTokenSpeed', true);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Applies the persisted `behavior.hitlMode` config value to the live mode
|
|
147
|
+
* manager at startup, ignoring unset or unrecognized values. Extracted from
|
|
148
|
+
* main() verbatim (a plain move, no behavior change).
|
|
149
|
+
*/
|
|
150
|
+
export function applyConfiguredHitlMode(
|
|
151
|
+
configManager: ConfigManager,
|
|
152
|
+
modeManager: { setHITLMode(mode: 'quiet' | 'balanced' | 'operator'): void },
|
|
153
|
+
): void {
|
|
154
|
+
const hitlMode = configManager.get('behavior.hitlMode');
|
|
155
|
+
if (hitlMode === 'quiet' || hitlMode === 'balanced' || hitlMode === 'operator') {
|
|
156
|
+
modeManager.setHITLMode(hitlMode);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
133
160
|
export function applyRuntimeConfigValue(configManager: ConfigManager, key: ConfigKey, value: unknown): void {
|
|
134
161
|
const setting = CONFIG_SCHEMA_BY_KEY.get(key);
|
|
135
162
|
if (!setting) {
|
|
@@ -161,23 +188,36 @@ export function applyRuntimeConfigOverrides(
|
|
|
161
188
|
return errors;
|
|
162
189
|
}
|
|
163
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Session-only feature overrides (--enable-feature / --disable-feature).
|
|
193
|
+
* Each feature is switched through its real domain settings key (e.g.
|
|
194
|
+
* sandbox.enabled, behavior.compactionStrategy) in the runtime config layer;
|
|
195
|
+
* features without an off position (constant capabilities on non-boolean
|
|
196
|
+
* keys) and unknown ids are reported as errors rather than silently ignored.
|
|
197
|
+
*/
|
|
164
198
|
export function applyRuntimeFeatureFlagOverrides(
|
|
165
199
|
configManager: ConfigManager,
|
|
166
200
|
options: {
|
|
167
201
|
readonly enableFeatures: readonly string[];
|
|
168
202
|
readonly disableFeatures: readonly string[];
|
|
169
203
|
},
|
|
170
|
-
):
|
|
171
|
-
if (options.enableFeatures.length === 0 && options.disableFeatures.length === 0) return;
|
|
204
|
+
): readonly string[] {
|
|
205
|
+
if (options.enableFeatures.length === 0 && options.disableFeatures.length === 0) return [];
|
|
172
206
|
const config = getRuntimeConfig(configManager);
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
207
|
+
const errors: string[] = [];
|
|
208
|
+
const apply = (feature: string, enabled: boolean, flagName: string): void => {
|
|
209
|
+
const write = featureEnablementWrite(feature, enabled);
|
|
210
|
+
if (!write) {
|
|
211
|
+
errors.push(getFeatureSetting(feature)
|
|
212
|
+
? `${flagName} ${feature}: this capability has no ${enabled ? 'on' : 'off'} switch (its domain settings govern it directly).`
|
|
213
|
+
: `${flagName} ${feature}: unknown feature id.`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
setNestedConfigValue(config, write.key, write.value);
|
|
217
|
+
};
|
|
218
|
+
for (const feature of options.enableFeatures) apply(feature, true, '--enable-feature');
|
|
219
|
+
for (const feature of options.disableFeatures) apply(feature, false, '--disable-feature');
|
|
220
|
+
return errors;
|
|
181
221
|
}
|
|
182
222
|
|
|
183
223
|
export function applyRuntimeEndpointFlagOverrides(
|
package/src/cli/doctor.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
2
|
import { PermissionManager, createPermissionConfigReader } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
3
3
|
import type { PermissionCategory, PermissionCheckResult } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
4
|
-
import { PolicyRuntimeState, createFeatureFlagManager } from '@/runtime/index.ts';
|
|
5
|
-
import type { FlagState } from '@/runtime/index.ts';
|
|
4
|
+
import { PolicyRuntimeState, createFeatureFlagManager, deriveFeatureStates } from '@/runtime/index.ts';
|
|
6
5
|
import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
7
6
|
import type { CliCommandOutput } from './types.ts';
|
|
8
7
|
import type { GoodVibesCliOutputFormat } from './types.ts';
|
|
@@ -76,7 +75,7 @@ function resolveExplainTarget(args: readonly string[]): { tool: string; args: Re
|
|
|
76
75
|
const LAYER_NAMES: readonly string[] = [
|
|
77
76
|
'Auto-approve override (behavior.autoApprove / --no-worries-just-vibes)',
|
|
78
77
|
'Session permission mode (permissions.mode)',
|
|
79
|
-
'Runtime policy engine (
|
|
78
|
+
'Runtime policy engine (permissions.engine = policy-engine)',
|
|
80
79
|
'Per-tool config rule (permissions.tools.<tool>)',
|
|
81
80
|
'Default read auto-approve (prompt mode)',
|
|
82
81
|
'Session approval cache (remembered [A] decisions)',
|
|
@@ -164,7 +163,7 @@ async function explain(options: DoctorSubcommandOptions): Promise<CliCommandOutp
|
|
|
164
163
|
// Feature flags come from live config so the policy engine layer is walked
|
|
165
164
|
// exactly when the running app would walk it.
|
|
166
165
|
const featureFlags = createFeatureFlagManager();
|
|
167
|
-
featureFlags.loadFromConfig({ flags: (config
|
|
166
|
+
featureFlags.loadFromConfig({ flags: deriveFeatureStates(config) });
|
|
168
167
|
const policyEngineOn = featureFlags.isEnabled('permissions-policy-engine');
|
|
169
168
|
const policyRuntimeState = new PolicyRuntimeState();
|
|
170
169
|
const manager = new PermissionManager(
|
|
@@ -214,7 +213,7 @@ async function explain(options: DoctorSubcommandOptions): Promise<CliCommandOutp
|
|
|
214
213
|
'Current settings:',
|
|
215
214
|
` mode : ${mode} (${permissionModeLabel(mode)})`,
|
|
216
215
|
` autoApprove : ${autoApprove ? 'yes (behavior.autoApprove)' : 'no'}`,
|
|
217
|
-
` policyEngine: ${policyEngineOn ? 'enabled' : 'disabled'} (
|
|
216
|
+
` policyEngine: ${policyEngineOn ? 'enabled' : 'disabled'} (permissions.engine)`,
|
|
218
217
|
'',
|
|
219
218
|
'Analysis (from the platform analyzer):',
|
|
220
219
|
` risk : ${a.riskLevel.toUpperCase()} (${a.classification})`,
|
package/src/cli/entrypoint.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
renderGoodVibesHelp,
|
|
22
22
|
renderGoodVibesVersion,
|
|
23
23
|
renderOnboardingCliStatus,
|
|
24
|
+
resolveDoctorExitCode,
|
|
24
25
|
} from './index.ts';
|
|
25
26
|
import { buildCliServicePosture, getGoodVibesPackageRoot, resolveGoodVibesDaemonExecutable } from './service-posture.ts';
|
|
26
27
|
import { runInstallSelfCheck } from '../runtime/install-self-check.ts';
|
|
@@ -28,8 +29,7 @@ import { readPersistedWorkspaceTrust } from '../runtime/trust/workspace-trust.ts
|
|
|
28
29
|
import { WorkspaceRegistrationManager } from '../runtime/trust/workspace-registration.ts';
|
|
29
30
|
import type { CliWorkspaceStatus, CliSandboxStatus, CliRelayStatus } from './status.ts';
|
|
30
31
|
import { detectSandboxAvailability, probeSandboxHost } from '@pellux/goodvibes-sdk/platform/tools/exec/sandbox';
|
|
31
|
-
import { createFeatureFlagManager } from '@/runtime/index.ts';
|
|
32
|
-
import type { FlagState } from '@/runtime/index.ts';
|
|
32
|
+
import { createFeatureFlagManager, deriveFeatureStates } from '@/runtime/index.ts';
|
|
33
33
|
import { ensureGoodvibesGitignore } from './ensure-goodvibes-gitignore.ts';
|
|
34
34
|
|
|
35
35
|
type ShellEntrypointOwnership = {
|
|
@@ -117,10 +117,14 @@ export async function prepareShellCliRuntime(
|
|
|
117
117
|
console.error(overrideErrors.join('\n'));
|
|
118
118
|
process.exit(2);
|
|
119
119
|
}
|
|
120
|
-
applyRuntimeFeatureFlagOverrides(configManager, {
|
|
120
|
+
const featureOverrideErrors = applyRuntimeFeatureFlagOverrides(configManager, {
|
|
121
121
|
enableFeatures: cli.flags.enableFeatures,
|
|
122
122
|
disableFeatures: cli.flags.disableFeatures,
|
|
123
123
|
});
|
|
124
|
+
if (featureOverrideErrors.length > 0) {
|
|
125
|
+
console.error(featureOverrideErrors.join('\n'));
|
|
126
|
+
process.exit(2);
|
|
127
|
+
}
|
|
124
128
|
|
|
125
129
|
if (cli.flags.provider !== undefined || cli.flags.model !== undefined) {
|
|
126
130
|
const currentModel = configManager.get('provider.model');
|
|
@@ -181,9 +185,7 @@ export async function prepareShellCliRuntime(
|
|
|
181
185
|
// Linux, a no-op elsewhere) is what makes "available" trustworthy.
|
|
182
186
|
const sandboxAvailability = detectSandboxAvailability(probeSandboxHost());
|
|
183
187
|
const sandboxFeatureFlags = createFeatureFlagManager();
|
|
184
|
-
sandboxFeatureFlags.loadFromConfig({
|
|
185
|
-
flags: (configManager.getCategory('featureFlags') as Record<string, FlagState>) ?? {},
|
|
186
|
-
});
|
|
188
|
+
sandboxFeatureFlags.loadFromConfig({ flags: deriveFeatureStates(configManager) });
|
|
187
189
|
const sandboxStatus: CliSandboxStatus = {
|
|
188
190
|
configEnabled: Boolean(configManager.getCategory('sandbox').enabled),
|
|
189
191
|
featureEnabled: sandboxFeatureFlags.isEnabled('exec-sandbox'),
|
|
@@ -192,7 +194,7 @@ export async function prepareShellCliRuntime(
|
|
|
192
194
|
reason: sandboxAvailability.reason,
|
|
193
195
|
networkIsolationGuaranteed: sandboxAvailability.networkIsolationGuaranteed,
|
|
194
196
|
};
|
|
195
|
-
// Reuses sandboxFeatureFlags (already
|
|
197
|
+
// Reuses sandboxFeatureFlags (already derived from the same domain settings keys)
|
|
196
198
|
// for the relay-connect gate rather than constructing a second FeatureFlagManager.
|
|
197
199
|
const relayCategory = configManager.getCategory('relay');
|
|
198
200
|
const relayStatus: CliRelayStatus = {
|
|
@@ -232,7 +234,10 @@ export async function prepareShellCliRuntime(
|
|
|
232
234
|
console.log(cli.command === 'onboarding'
|
|
233
235
|
? renderOnboardingCliStatus(statusOptions)
|
|
234
236
|
: renderCliStatus(statusOptions));
|
|
235
|
-
|
|
237
|
+
// Advisory findings are notes on an otherwise-usable install and must
|
|
238
|
+
// never make a healthy install report failure — only a must-fix finding
|
|
239
|
+
// exits non-zero; --strict (for CI) flips advisories to failures too.
|
|
240
|
+
process.exit(cli.command === 'doctor' ? resolveDoctorExitCode(snapshot.findings, cli.flags.strict) : 0);
|
|
236
241
|
}
|
|
237
242
|
|
|
238
243
|
const cliCommandResult = await handleGoodVibesCliCommand({
|
package/src/cli/help.ts
CHANGED
|
@@ -68,8 +68,8 @@ export function renderGoodVibesHelp(binary = 'goodvibes'): string {
|
|
|
68
68
|
' --working-dir <dir> Alias for --cd',
|
|
69
69
|
' --daemon-home <dir> Override daemon home for daemon-backed commands',
|
|
70
70
|
' -c, --config <key=value> Override a config value for this launch',
|
|
71
|
-
' --enable <feature> Enable a feature
|
|
72
|
-
' --disable <feature> Disable a feature
|
|
71
|
+
' --enable <feature> Enable a platform feature for this launch',
|
|
72
|
+
' --disable <feature> Disable a platform feature for this launch',
|
|
73
73
|
' -p, --prompt <text> Run a non-interactive prompt',
|
|
74
74
|
' --print Alias for non-interactive run mode',
|
|
75
75
|
' -o, --output <format> text, json, or stream-json',
|
|
@@ -85,6 +85,7 @@ export function renderGoodVibesHelp(binary = 'goodvibes'): string {
|
|
|
85
85
|
' --fork [id] Fork session (current or specific id) when supported',
|
|
86
86
|
' -y, --yes Auto-confirm prompts (non-interactive)',
|
|
87
87
|
' --non-interactive Disable all interactive prompts (implies --yes)',
|
|
88
|
+
' --strict doctor: also fail on advisory findings, for CI',
|
|
88
89
|
' -h, --help Print help',
|
|
89
90
|
' -v, --version Print version',
|
|
90
91
|
'',
|
|
@@ -139,9 +140,9 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
139
140
|
examples: ['status', 'status --json'],
|
|
140
141
|
},
|
|
141
142
|
doctor: {
|
|
142
|
-
usage: ['doctor', 'doctor --json', 'doctor explain <tool|command>', 'doctor routing', 'doctor hooks'],
|
|
143
|
-
summary: 'Print status plus setup warnings, or explain a platform decision: explain (why a tool/command would be allowed/asked/denied under the current mode), routing (which model/provider serves which role), hooks (registered hooks, their source, and validation status).',
|
|
144
|
-
examples: ['doctor', 'doctor explain "rm -rf build"', 'doctor explain write ./src/app.ts', 'doctor routing', 'doctor hooks --json'],
|
|
143
|
+
usage: ['doctor', 'doctor --json', 'doctor --strict', 'doctor explain <tool|command>', 'doctor routing', 'doctor hooks'],
|
|
144
|
+
summary: 'Print status plus setup warnings, or explain a platform decision: explain (why a tool/command would be allowed/asked/denied under the current mode), routing (which model/provider serves which role), hooks (registered hooks, their source, and validation status). Exits 0 when the install is usable (advisory findings render as notes); non-zero only for a must-fix finding. --strict also fails on advisory findings, for CI.',
|
|
145
|
+
examples: ['doctor', 'doctor --strict', 'doctor explain "rm -rf build"', 'doctor explain write ./src/app.ts', 'doctor routing', 'doctor hooks --json'],
|
|
145
146
|
},
|
|
146
147
|
providers: {
|
|
147
148
|
usage: ['providers [list]', 'providers current', 'providers inspect <provider>', 'providers use <provider> [modelRegistryKey]'],
|
|
@@ -311,7 +312,7 @@ export function renderGoodVibesDaemonHelp(binary = 'goodvibes-daemon'): string {
|
|
|
311
312
|
' install-service Install + enable the daemon as a systemd user service (survives reboots)',
|
|
312
313
|
' uninstall-service Disable + remove the daemon systemd user service',
|
|
313
314
|
' service-status Show whether the daemon service is installed / enabled / active',
|
|
314
|
-
' migrate-service Guided
|
|
315
|
+
' migrate-service Guided migration from an install-script goodvibes-daemon.service unit; prints a plan',
|
|
315
316
|
' unless run with -y/--yes (never auto-migrates)',
|
|
316
317
|
'',
|
|
317
318
|
'Options:',
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch-time self-update — clients land on the newest release at startup so
|
|
3
|
+
* an installed binary never drifts behind. At TUI launch (before any runtime
|
|
4
|
+
* bootstrap or terminal mode change) this runs a quick version check and, when
|
|
5
|
+
* a newer release exists, installs it through the SAME checksum-verified
|
|
6
|
+
* download/verify/swap path `/update apply` and scripts/install.sh use
|
|
7
|
+
* (src/input/commands/update-runtime.ts — there is deliberately no second
|
|
8
|
+
* updater), then asks the caller to restart onto the new binary.
|
|
9
|
+
*
|
|
10
|
+
* Honesty rules, in both directions:
|
|
11
|
+
* - the check gets a short timeout; when it cannot complete (offline, slow
|
|
12
|
+
* network) the CURRENT version starts with exactly one line saying the
|
|
13
|
+
* check was skipped — launch is never held hostage by the network.
|
|
14
|
+
* - a successful update restarts into the new binary and the restarted
|
|
15
|
+
* process prints a receipt naming both versions, so the swap is never
|
|
16
|
+
* silent.
|
|
17
|
+
* - every swap keeps the outgoing file at `<path>.previous`; rollback is
|
|
18
|
+
* one command (`/update rollback`).
|
|
19
|
+
*
|
|
20
|
+
* The feature is a real configurable setting (`update.autoUpdateAtLaunch` in
|
|
21
|
+
* settings.json, read via readUpdateSettings): default ON, off with an
|
|
22
|
+
* explicit false. Only binary installs self-update — package-manager and
|
|
23
|
+
* from-source runs are skipped silently because a swap there would fight the
|
|
24
|
+
* package manager (see detectInstallKind).
|
|
25
|
+
*
|
|
26
|
+
* Everything effectful is injectable; tests drive the decision logic with a
|
|
27
|
+
* stubbed fetch, a stubbed apply, and pinned fixture versions — never the
|
|
28
|
+
* live build VERSION and never the real network.
|
|
29
|
+
*/
|
|
30
|
+
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import { detectInstallKind, normalizeVersion, type UpdateFetchLike } from '../runtime/update-check.ts';
|
|
32
|
+
import { applyUpdate, checkForUpdate, type ApplyUpdateOptions } from '../input/commands/update-runtime.ts';
|
|
33
|
+
import { readUpdateSettings, type UpdateSettings } from '../config/tui-extension-settings.ts';
|
|
34
|
+
import type { ConfigManager } from '../config/index.ts';
|
|
35
|
+
import { VERSION } from '../version.ts';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set on the restarted process by restartOntoUpdatedBinary, carrying the
|
|
39
|
+
* version that performed the update. Its presence means "an update already
|
|
40
|
+
* happened this launch": the restarted process prints the receipt and skips
|
|
41
|
+
* its own check, which also makes an update-that-didn't-change-the-version
|
|
42
|
+
* structurally unable to restart-loop.
|
|
43
|
+
*/
|
|
44
|
+
export const LAUNCH_UPDATED_FROM_ENV = 'GOODVIBES_LAUNCH_UPDATED_FROM';
|
|
45
|
+
|
|
46
|
+
/** Default budget for the launch-time version check; user-tunable via update.launchCheckTimeoutMs. */
|
|
47
|
+
export const LAUNCH_UPDATE_CHECK_TIMEOUT_MS = 2500;
|
|
48
|
+
|
|
49
|
+
export type LaunchAutoUpdateOutcome =
|
|
50
|
+
| {
|
|
51
|
+
readonly action: 'continue';
|
|
52
|
+
readonly reason: 'just-updated' | 'disabled' | 'not-swappable-install' | 'already-current' | 'check-skipped' | 'update-failed';
|
|
53
|
+
}
|
|
54
|
+
| { readonly action: 'restart'; readonly latestTag: string };
|
|
55
|
+
|
|
56
|
+
export interface RunLaunchAutoUpdateOptions {
|
|
57
|
+
readonly fetchImpl: UpdateFetchLike;
|
|
58
|
+
readonly execPath: string;
|
|
59
|
+
readonly platform: NodeJS.Platform;
|
|
60
|
+
readonly arch: string;
|
|
61
|
+
readonly currentVersion: string;
|
|
62
|
+
readonly settings: UpdateSettings;
|
|
63
|
+
readonly env: NodeJS.ProcessEnv;
|
|
64
|
+
readonly print: (line: string) => void;
|
|
65
|
+
readonly configManager: { get(key: string): unknown };
|
|
66
|
+
/** Injectable so tests observe the install step instead of swapping real files. */
|
|
67
|
+
readonly apply?: (options: ApplyUpdateOptions) => Promise<void>;
|
|
68
|
+
readonly timeoutMs?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Resolves to 'timeout' when `promise` does not settle within `ms`; the timer never keeps the process alive. */
|
|
72
|
+
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | 'timeout'> {
|
|
73
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
74
|
+
try {
|
|
75
|
+
return await Promise.race([
|
|
76
|
+
promise,
|
|
77
|
+
new Promise<'timeout'>((resolve) => {
|
|
78
|
+
timer = setTimeout(() => resolve('timeout'), ms);
|
|
79
|
+
timer.unref?.();
|
|
80
|
+
}),
|
|
81
|
+
]);
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The launch-time decision: receipt-and-continue (restarted process), skip
|
|
89
|
+
* (disabled / unswappable / current / unreachable), install-and-restart, or
|
|
90
|
+
* fail-and-continue on the current version. Never throws.
|
|
91
|
+
*/
|
|
92
|
+
export async function runLaunchAutoUpdate(options: RunLaunchAutoUpdateOptions): Promise<LaunchAutoUpdateOutcome> {
|
|
93
|
+
const updatedFrom = options.env[LAUNCH_UPDATED_FROM_ENV];
|
|
94
|
+
if (typeof updatedFrom === 'string' && updatedFrom.length > 0) {
|
|
95
|
+
// Consumed here so sessions spawned from inside this one never inherit it.
|
|
96
|
+
delete options.env[LAUNCH_UPDATED_FROM_ENV];
|
|
97
|
+
options.print(`auto-update: updated from v${normalizeVersion(updatedFrom)} to v${normalizeVersion(options.currentVersion)} at launch`);
|
|
98
|
+
return { action: 'continue', reason: 'just-updated' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!(options.settings.autoUpdateAtLaunch ?? true)) {
|
|
102
|
+
return { action: 'continue', reason: 'disabled' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (detectInstallKind(options.execPath) !== 'binary') {
|
|
106
|
+
return { action: 'continue', reason: 'not-swappable-install' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const timeoutMs = options.timeoutMs ?? options.settings.launchCheckTimeoutMs ?? LAUNCH_UPDATE_CHECK_TIMEOUT_MS;
|
|
110
|
+
let check: Awaited<ReturnType<typeof checkForUpdate>> | 'timeout';
|
|
111
|
+
try {
|
|
112
|
+
check = await withTimeout(checkForUpdate(options.fetchImpl, options.currentVersion), timeoutMs);
|
|
113
|
+
} catch {
|
|
114
|
+
check = 'timeout';
|
|
115
|
+
}
|
|
116
|
+
if (check === 'timeout') {
|
|
117
|
+
options.print('update check skipped: offline');
|
|
118
|
+
return { action: 'continue', reason: 'check-skipped' };
|
|
119
|
+
}
|
|
120
|
+
if (check.isCurrent) {
|
|
121
|
+
return { action: 'continue', reason: 'already-current' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const apply = options.apply ?? applyUpdate;
|
|
126
|
+
await apply({
|
|
127
|
+
fetchImpl: options.fetchImpl,
|
|
128
|
+
execPath: options.execPath,
|
|
129
|
+
platform: options.platform,
|
|
130
|
+
arch: options.arch,
|
|
131
|
+
currentVersion: options.currentVersion,
|
|
132
|
+
print: options.print,
|
|
133
|
+
configManager: options.configManager,
|
|
134
|
+
});
|
|
135
|
+
options.print(`auto-update: ${check.latestTag} installed — restarting onto the new version`);
|
|
136
|
+
return { action: 'restart', latestTag: check.latestTag };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
options.print(
|
|
139
|
+
`auto-update failed: ${error instanceof Error ? error.message : String(error)} — starting the current version v${normalizeVersion(options.currentVersion)}`,
|
|
140
|
+
);
|
|
141
|
+
return { action: 'continue', reason: 'update-failed' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface SelfUpdateAtLaunchParams {
|
|
146
|
+
readonly configManager: Pick<ConfigManager, 'get' | 'getRaw'>;
|
|
147
|
+
readonly stdout: { write(chunk: string): unknown };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The complete launch wiring for main(), with real host inputs: run the
|
|
152
|
+
* decision above; when an update was installed, restart onto the swapped
|
|
153
|
+
* binary and EXIT THIS PROCESS with the new instance's exit code (this call
|
|
154
|
+
* never returns in that case); otherwise return the honest lines that were
|
|
155
|
+
* printed (receipt / skipped check / failed update) so the caller can
|
|
156
|
+
* re-surface them through the system message router once it exists — the
|
|
157
|
+
* stdout copies written here are wiped by the TUI's alternate screen.
|
|
158
|
+
*/
|
|
159
|
+
export async function selfUpdateAtLaunch(params: SelfUpdateAtLaunchParams): Promise<readonly string[]> {
|
|
160
|
+
const lines: string[] = [];
|
|
161
|
+
const outcome = await runLaunchAutoUpdate({
|
|
162
|
+
fetchImpl: fetch as UpdateFetchLike,
|
|
163
|
+
execPath: process.execPath,
|
|
164
|
+
platform: process.platform,
|
|
165
|
+
arch: process.arch,
|
|
166
|
+
currentVersion: VERSION,
|
|
167
|
+
settings: readUpdateSettings(params.configManager),
|
|
168
|
+
env: process.env,
|
|
169
|
+
configManager: params.configManager,
|
|
170
|
+
print: (line) => {
|
|
171
|
+
lines.push(line);
|
|
172
|
+
params.stdout.write(`${line}\n`);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
if (outcome.action === 'restart') {
|
|
176
|
+
process.exit(
|
|
177
|
+
restartOntoUpdatedBinary({
|
|
178
|
+
execPath: process.execPath,
|
|
179
|
+
argv: process.argv.slice(2),
|
|
180
|
+
env: process.env,
|
|
181
|
+
fromVersion: VERSION,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface RestartOntoUpdatedBinaryOptions {
|
|
189
|
+
readonly execPath: string;
|
|
190
|
+
readonly argv: readonly string[];
|
|
191
|
+
readonly env: NodeJS.ProcessEnv;
|
|
192
|
+
/** The version that performed the update — the restarted process prints it in its receipt line. */
|
|
193
|
+
readonly fromVersion: string;
|
|
194
|
+
/** Injectable so tests observe the restart instead of spawning a process. */
|
|
195
|
+
readonly spawn?: (execPath: string, argv: readonly string[], env: NodeJS.ProcessEnv) => number | null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Runs the (just-swapped) binary at execPath with the original CLI arguments
|
|
200
|
+
* and an inherited terminal, blocking until it exits, and returns its exit
|
|
201
|
+
* code for the caller to process.exit with. The receipt marker travels in the
|
|
202
|
+
* child's environment, never on the command line.
|
|
203
|
+
*/
|
|
204
|
+
export function restartOntoUpdatedBinary(options: RestartOntoUpdatedBinaryOptions): number {
|
|
205
|
+
const spawn =
|
|
206
|
+
options.spawn ??
|
|
207
|
+
((execPath: string, argv: readonly string[], env: NodeJS.ProcessEnv) =>
|
|
208
|
+
spawnSync(execPath, [...argv], { stdio: 'inherit', env }).status);
|
|
209
|
+
const status = spawn(options.execPath, options.argv, {
|
|
210
|
+
...options.env,
|
|
211
|
+
[LAUNCH_UPDATED_FROM_ENV]: options.fromVersion,
|
|
212
|
+
});
|
|
213
|
+
return status ?? 0;
|
|
214
|
+
}
|
|
@@ -5,12 +5,16 @@ import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, r
|
|
|
5
5
|
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
6
6
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
7
7
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { generateQrMatrix, renderQrToString, PAIRING_HANDOFF_OFFER_KINDS, type PairingHandoffOfferKind } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
9
|
+
import { ensurePublicBaseUrl } from '../core/pairing-origin.ts';
|
|
10
|
+
import { formatPairingOffers, formatPostureCapabilities, pairingPostureNotice } from '../core/pairing-offers.ts';
|
|
11
|
+
import { defaultPairingTokenName } from '../core/pairing-handoff.ts';
|
|
12
|
+
import { stableUrlHostForBindHost } from '../core/stable-host.ts';
|
|
10
13
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
11
14
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
15
|
import type { CliCommandRuntime } from './types.ts';
|
|
13
|
-
import {
|
|
16
|
+
import type { RuntimeServices } from '../runtime/services.ts';
|
|
17
|
+
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, readOptionValue, runNonInteractiveAgent, withRuntimeServices, yesNo } from './management-utils.ts';
|
|
14
18
|
|
|
15
19
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
16
20
|
return await withRuntimeServices(runtime, async (services) => {
|
|
@@ -368,19 +372,80 @@ export async function renderControlPlaneStatus(runtime: CliCommandRuntime): Prom
|
|
|
368
372
|
return formatControlPlaneStatus(runtime, await buildControlPlaneStatusResult(runtime));
|
|
369
373
|
}
|
|
370
374
|
|
|
375
|
+
interface PairingHandoffCreateResult {
|
|
376
|
+
readonly token: { readonly id: string; readonly name: string; readonly token: string };
|
|
377
|
+
readonly offers: readonly { readonly kind: PairingHandoffOfferKind }[];
|
|
378
|
+
readonly fragment: string;
|
|
379
|
+
readonly deepLink?: string | undefined;
|
|
380
|
+
/** The honest TLS/capability posture of the origin the QR points at (SDK-computed). */
|
|
381
|
+
readonly posture?: import('@pellux/goodvibes-sdk/platform/pairing').OriginPosture | undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
371
384
|
export async function renderPairing(runtime: CliCommandRuntime): Promise<string> {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
return await withRuntimeServices(runtime, async (services) => {
|
|
386
|
+
// Freeze a stable web origin once (never clobbering a user-set value) so the
|
|
387
|
+
// printed link and the daemon's own handoff origin agree and survive a DHCP
|
|
388
|
+
// lease change where a stable name exists. The verb re-derives the same origin
|
|
389
|
+
// and returns its honest posture, so this call is a persistence side effect.
|
|
390
|
+
ensurePublicBaseUrl(runtime.configManager);
|
|
391
|
+
const name = readOptionValue(runtime.cli.commandArgs, 'name') ?? defaultPairingTokenName();
|
|
392
|
+
// Mint through the canonical hand-off verb: it mints a fresh per-device token
|
|
393
|
+
// and returns the exact `#pair=<token>` deep link the web app consumes. The
|
|
394
|
+
// daemon filters the requested offer set down to what it can actually satisfy.
|
|
395
|
+
const result = (await services.gatewayMethods.invoke('pairing.handoff.create', {
|
|
396
|
+
body: { name, offers: [...PAIRING_HANDOFF_OFFER_KINDS] },
|
|
397
|
+
context: { principalId: 'admin', principalKind: 'user', admin: true },
|
|
398
|
+
})) as PairingHandoffCreateResult;
|
|
399
|
+
const link = result.deepLink ?? result.fragment;
|
|
400
|
+
const qr = renderQrToString(generateQrMatrix(link));
|
|
401
|
+
const lines: string[] = [
|
|
402
|
+
'Scan to pair a device — the QR opens the web app already signed in:',
|
|
403
|
+
'',
|
|
404
|
+
` ${link}`,
|
|
405
|
+
` Token name: ${result.token.name} (rename or revoke later in /settings → security → devices)`,
|
|
406
|
+
];
|
|
407
|
+
if (result.offers.length > 0) {
|
|
408
|
+
lines.push('', 'Offers carried in this pairing (each declinable in the web app):', ...formatPairingOffers(result.offers.map((o) => o.kind)));
|
|
409
|
+
}
|
|
410
|
+
// The labeled browser-capability list and the one honest LAN line both render
|
|
411
|
+
// from the SDK posture the verb returned — never a locally-authored string.
|
|
412
|
+
const capabilities = formatPostureCapabilities(result.posture);
|
|
413
|
+
if (capabilities.length > 0) lines.push('', 'This device will get:', ...capabilities);
|
|
414
|
+
const notice = pairingPostureNotice(result.posture);
|
|
415
|
+
if (notice) lines.push('', notice);
|
|
416
|
+
// Tailscale detection is read-only here (the interactive serve lives in the
|
|
417
|
+
// pairing modal); when detected with an https MagicDNS URL, name the encrypted
|
|
418
|
+
// path. Absence stays quiet.
|
|
419
|
+
lines.push(...(await renderPairingTailscaleLines(services)));
|
|
420
|
+
lines.push('', qr);
|
|
421
|
+
return lines.join('\n');
|
|
380
422
|
});
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface TailscaleGetResult {
|
|
426
|
+
readonly available: boolean;
|
|
427
|
+
readonly httpsUrl?: string | undefined;
|
|
428
|
+
readonly detail: string;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Read-only tailscale detection for the `gv pair` block: when tailscale is
|
|
433
|
+
* available with a served https MagicDNS URL, name the encrypted path (the
|
|
434
|
+
* interactive one-action serve lives in the pairing modal). Quiet — an empty
|
|
435
|
+
* array — when tailscale is absent or the probe fails.
|
|
436
|
+
*/
|
|
437
|
+
async function renderPairingTailscaleLines(services: RuntimeServices): Promise<string[]> {
|
|
438
|
+
try {
|
|
439
|
+
const ts = (await services.gatewayMethods.invoke('tailscale.get', {
|
|
440
|
+
body: {},
|
|
441
|
+
context: { principalId: 'admin', principalKind: 'user', admin: true },
|
|
442
|
+
})) as TailscaleGetResult;
|
|
443
|
+
if (!ts.available) return [];
|
|
444
|
+
if (ts.httpsUrl) return ['', `Encrypted access (Tailscale): ${ts.httpsUrl}`];
|
|
445
|
+
return ['', 'Tailscale detected — run it as a serve target for encrypted https access (see the pairing modal).'];
|
|
446
|
+
} catch {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
384
449
|
}
|
|
385
450
|
|
|
386
451
|
export async function renderRemote(runtime: CliCommandRuntime, label: 'remote' | 'bridge'): Promise<string> {
|
|
@@ -408,9 +473,11 @@ export function renderWeb(runtime: CliCommandRuntime): string {
|
|
|
408
473
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'web');
|
|
409
474
|
const publicBaseUrl = String(runtime.configManager.get('web.publicBaseUrl') ?? '');
|
|
410
475
|
const hasEndpointOverride = runtime.cli.flags.hostname !== undefined || runtime.cli.flags.port !== undefined;
|
|
476
|
+
// Prefer the stable-name resolution over the raw first-non-internal IPv4 so the
|
|
477
|
+
// printed URL survives a DHCP lease change where a stable name exists.
|
|
411
478
|
const url = !hasEndpointOverride && publicBaseUrl
|
|
412
479
|
? publicBaseUrl
|
|
413
|
-
: `http://${
|
|
480
|
+
: `http://${stableUrlHostForBindHost(binding.host).host}:${binding.port}`;
|
|
414
481
|
const value = {
|
|
415
482
|
enabled: runtime.configManager.get('web.enabled'),
|
|
416
483
|
...binding,
|
package/src/cli/management.ts
CHANGED
|
@@ -11,8 +11,6 @@ import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, r
|
|
|
11
11
|
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
12
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
13
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
14
|
-
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
15
|
-
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
16
14
|
import type { CliCommandRuntime, GoodVibesCliParseResult } from './types.ts';
|
|
17
15
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
18
16
|
import { classifyProviderSetup } from './provider-classification.ts';
|