@pellux/goodvibes-tui 0.20.3 → 0.22.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 +50 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +4 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +658 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +31 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +14 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/context-auto-compact.ts +77 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +32 -1
- package/src/input/commands/control-room-runtime.ts +10 -10
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -4
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +128 -12
- package/src/input/handler-modal-token-routes.ts +22 -5
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +73 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +6 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +378 -0
- package/src/input/settings-modal-mutations.ts +157 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +236 -232
- package/src/main.ts +93 -85
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +5 -1
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/session-maintenance.ts +66 -15
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +118 -13
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +90 -10
- package/src/renderer/shell-surface.ts +10 -0
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +18 -0
- package/src/runtime/bootstrap-core.ts +145 -13
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +9 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +27 -1
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/panels/knowledge-panel.ts +0 -345
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1
2
|
import type { ConfigKey, ConfigManager, ConfigSetting, GoodVibesConfig, PersistedFlagState } from '../config/index.ts';
|
|
2
3
|
import { CONFIG_SCHEMA, ConfigError } from '../config/index.ts';
|
|
3
4
|
import type { GoodVibesCliCommand, GoodVibesCliFlags } from './types.ts';
|
|
@@ -62,6 +63,73 @@ function setNestedConfigValue(config: GoodVibesConfig, key: ConfigKey, value: un
|
|
|
62
63
|
(cursor as Record<string, unknown>)[parts[parts.length - 1]!] = value;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Typed accessor for the SDK's private file paths on ConfigManager.
|
|
68
|
+
*
|
|
69
|
+
* SDK coupling: configPath and projectConfigPath are declared `private readonly`
|
|
70
|
+
* in the SDK's ConfigManager (see manager.d.ts — configPath, projectConfigPath). No public accessors exist
|
|
71
|
+
* as of the SDK version pinned in this project. This cast is the narrowest
|
|
72
|
+
* possible workaround. SDK public-accessor request: see HANDOFF-FROM-TUI-SESSION-20260611.md,
|
|
73
|
+
* Item 6 area. If the SDK ever exposes getConfigPath()/getProjectConfigPath(), replace
|
|
74
|
+
* this cast with those calls and remove this comment.
|
|
75
|
+
*
|
|
76
|
+
* Fail-open: if the cast produces undefined (SDK internal rename), the paths are
|
|
77
|
+
* treated as absent and the default is applied safely.
|
|
78
|
+
*/
|
|
79
|
+
function getPersistedPaths(configManager: ConfigManager): { configPath: string | undefined; projectConfigPath: string | undefined } {
|
|
80
|
+
const manager = configManager as unknown as { configPath?: string; projectConfigPath?: string };
|
|
81
|
+
return {
|
|
82
|
+
configPath: typeof manager.configPath === 'string' ? manager.configPath : undefined,
|
|
83
|
+
projectConfigPath: typeof manager.projectConfigPath === 'string' ? manager.projectConfigPath : undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns true if the given dot-path key is explicitly present anywhere
|
|
89
|
+
* in the provided raw JSON object.
|
|
90
|
+
*/
|
|
91
|
+
function isKeyPresentInRaw(raw: Record<string, unknown>, key: ConfigKey): boolean {
|
|
92
|
+
const parts = key.split('.');
|
|
93
|
+
let cursor: unknown = raw;
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (cursor == null || typeof cursor !== 'object' || !(part in (cursor as object))) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
cursor = (cursor as Record<string, unknown>)[part];
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Apply a TUI default to a config key, but ONLY if the user has not explicitly
|
|
105
|
+
* set the key in EITHER their global OR project persisted settings files.
|
|
106
|
+
* Reads the raw settings JSON from disk (bypasses the in-memory merged config)
|
|
107
|
+
* so a user's explicit `false` in either file is never silently overridden at startup.
|
|
108
|
+
*
|
|
109
|
+
* Each settings file is read independently: a parse failure on one file
|
|
110
|
+
* contributes nothing but does NOT prevent the other file from being checked.
|
|
111
|
+
* The default is applied only when the key is absent from every readable file
|
|
112
|
+
* (e.g. new install, both files missing, or both unparseable).
|
|
113
|
+
*/
|
|
114
|
+
export function applyRuntimeConfigDefault(configManager: ConfigManager, key: ConfigKey, defaultValue: unknown): void {
|
|
115
|
+
const { configPath, projectConfigPath } = getPersistedPaths(configManager);
|
|
116
|
+
// Check each settings file independently. A read/parse failure on one path
|
|
117
|
+
// contributes nothing but must not prevent the other path from being checked.
|
|
118
|
+
for (const filePath of [configPath, projectConfigPath]) {
|
|
119
|
+
if (typeof filePath !== 'string' || !existsSync(filePath)) continue;
|
|
120
|
+
try {
|
|
121
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8')) as Record<string, unknown>;
|
|
122
|
+
if (isKeyPresentInRaw(raw, key)) {
|
|
123
|
+
// Key is explicitly set in this persisted config — respect the user's value.
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Unreadable or malformed JSON — this file contributes nothing; continue to next.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
applyRuntimeConfigValue(configManager, key, defaultValue);
|
|
131
|
+
}
|
|
132
|
+
|
|
65
133
|
export function applyRuntimeConfigValue(configManager: ConfigManager, key: ConfigKey, value: unknown): void {
|
|
66
134
|
const setting = CONFIG_SCHEMA_BY_KEY.get(key);
|
|
67
135
|
if (!setting) {
|
package/src/cli/entrypoint.ts
CHANGED
|
@@ -61,6 +61,12 @@ export async function prepareShellCliRuntime(
|
|
|
61
61
|
process.exit(2);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
if (cli.warnings.length > 0) {
|
|
65
|
+
for (const warning of cli.warnings) {
|
|
66
|
+
console.warn(`[goodvibes] warning: ${warning}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
if (cli.flags.help || cli.command === 'help') {
|
|
65
71
|
const helpTopic = cli.command === 'help'
|
|
66
72
|
? cli.commandArgs[0]
|
package/src/cli/help.ts
CHANGED
|
@@ -71,12 +71,14 @@ export function renderGoodVibesHelp(binary = 'goodvibes'): string {
|
|
|
71
71
|
' --json Alias for --output-format json',
|
|
72
72
|
' --no-alt-screen Keep output in normal terminal scrollback',
|
|
73
73
|
' --port <port> Port for server/web commands',
|
|
74
|
-
' --hostname <host> Hostname for server/web commands',
|
|
74
|
+
' --hostname <host> Hostname for server/web commands (--host is an alias)',
|
|
75
75
|
' --open Open browser when supported',
|
|
76
76
|
' -r, --resume [id|latest] Resume saved session when supported',
|
|
77
77
|
' -s, --session <id> Use a specific session when supported',
|
|
78
78
|
' --continue Continue the latest session when supported',
|
|
79
|
-
' --fork
|
|
79
|
+
' --fork [id] Fork session (current or specific id) when supported',
|
|
80
|
+
' -y, --yes Auto-confirm prompts (non-interactive)',
|
|
81
|
+
' --non-interactive Disable all interactive prompts (implies --yes)',
|
|
80
82
|
' -h, --help Print help',
|
|
81
83
|
' -v, --version Print version',
|
|
82
84
|
'',
|
|
@@ -9,7 +9,7 @@ import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnecti
|
|
|
9
9
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
10
10
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
11
11
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
|
-
import type { CliCommandRuntime } from './
|
|
12
|
+
import type { CliCommandRuntime } from './types.ts';
|
|
13
13
|
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
|
|
14
14
|
|
|
15
15
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
package/src/cli/management.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
|
|
|
21
21
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
22
22
|
import { getOrCreateCompanionToken, buildCompanionConnectionInfo, encodeConnectionPayload, formatConnectionBlock } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
23
23
|
import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
24
|
-
import type { GoodVibesCliParseResult } from './types.ts';
|
|
24
|
+
import type { CliCommandRuntime, GoodVibesCliParseResult } from './types.ts';
|
|
25
25
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
26
26
|
import { classifyProviderSetup } from './provider-classification.ts';
|
|
27
27
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
@@ -32,13 +32,6 @@ import { handleBundleCommand } from './bundle-command.ts';
|
|
|
32
32
|
import { buildListenerTestResult, formatListenerTestResult, handleSurfacesCommand } from './surface-command.ts';
|
|
33
33
|
import { buildControlPlaneStatusResult, formatControlPlaneStatus, handleSecrets, handleSessions, handleTasks, renderPairing, renderRemote, renderSubscriptions, renderWeb } from './management-commands.ts';
|
|
34
34
|
|
|
35
|
-
export interface CliCommandRuntime {
|
|
36
|
-
readonly cli: GoodVibesCliParseResult;
|
|
37
|
-
readonly configManager: ConfigManager;
|
|
38
|
-
readonly workingDirectory: string;
|
|
39
|
-
readonly homeDirectory: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
35
|
interface CliCommandResult {
|
|
43
36
|
readonly handled: boolean;
|
|
44
37
|
readonly exitCode: number;
|
package/src/cli/parser.ts
CHANGED
|
@@ -76,9 +76,9 @@ function createDefaultFlags(): GoodVibesCliFlags {
|
|
|
76
76
|
continueLast: false,
|
|
77
77
|
resume: undefined,
|
|
78
78
|
session: undefined,
|
|
79
|
-
fork:
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
fork: undefined,
|
|
80
|
+
yes: false,
|
|
81
|
+
nonInteractive: false,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -132,9 +132,9 @@ function parsePort(value: string | undefined, optionName: string, errors: string
|
|
|
132
132
|
return port;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function normalizeOutputFormat(value: string | undefined, errors: string[]): GoodVibesCliOutputFormat {
|
|
135
|
+
function normalizeOutputFormat(value: string | undefined, flagName: string, errors: string[]): GoodVibesCliOutputFormat {
|
|
136
136
|
if (value === 'text' || value === 'json' || value === 'stream-json') return value;
|
|
137
|
-
errors.push(
|
|
137
|
+
errors.push(`${flagName} must be one of: text, json, stream-json.`);
|
|
138
138
|
return 'text';
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -174,6 +174,7 @@ export function parseGoodVibesCli(
|
|
|
174
174
|
const commandArgs: string[] = [];
|
|
175
175
|
const positionals: string[] = [];
|
|
176
176
|
const errors: string[] = [];
|
|
177
|
+
const warnings: string[] = [];
|
|
177
178
|
let sawCommand = false;
|
|
178
179
|
let passthrough = false;
|
|
179
180
|
|
|
@@ -238,15 +239,17 @@ export function parseGoodVibesCli(
|
|
|
238
239
|
continue;
|
|
239
240
|
}
|
|
240
241
|
if (name === '--fork') {
|
|
241
|
-
|
|
242
|
+
const consumed = getOptionalValue(argv, index, inlineValue);
|
|
243
|
+
index = consumed.nextIndex;
|
|
244
|
+
flags = withFlag(flags, 'fork', consumed.value ?? true);
|
|
242
245
|
continue;
|
|
243
246
|
}
|
|
244
|
-
if (name === '--
|
|
245
|
-
flags = withFlag(flags, '
|
|
247
|
+
if (name === '--yes' || name === '-y') {
|
|
248
|
+
flags = withFlag(flags, 'yes', true);
|
|
246
249
|
continue;
|
|
247
250
|
}
|
|
248
|
-
if (name === '--
|
|
249
|
-
flags = withFlag(flags, '
|
|
251
|
+
if (name === '--non-interactive') {
|
|
252
|
+
flags = withFlag(withFlag(flags, 'nonInteractive', true), 'yes', true);
|
|
250
253
|
continue;
|
|
251
254
|
}
|
|
252
255
|
|
|
@@ -289,16 +292,13 @@ export function parseGoodVibesCli(
|
|
|
289
292
|
if (name === '--output-format' || name === '--output' || name === '-o') {
|
|
290
293
|
const consumed = getValue(argv, index, inlineValue, name, errors);
|
|
291
294
|
index = consumed.nextIndex;
|
|
292
|
-
flags = withFlag(flags, 'outputFormat', normalizeOutputFormat(consumed.value, errors));
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const consumed = getValue(argv, index, inlineValue, name, errors);
|
|
297
|
-
index = consumed.nextIndex;
|
|
298
|
-
if (consumed.value !== undefined) flags = appendFlagArray(flags, 'configOverrides', consumed.value);
|
|
295
|
+
flags = withFlag(flags, 'outputFormat', normalizeOutputFormat(consumed.value, name, errors));
|
|
296
|
+
if (name === '--output-format') {
|
|
297
|
+
warnings.push('--output-format is deprecated; use --output (or -o) instead.');
|
|
298
|
+
}
|
|
299
299
|
continue;
|
|
300
300
|
}
|
|
301
|
-
if (name === '-c') {
|
|
301
|
+
if (name === '--config' || name === '-c') {
|
|
302
302
|
const consumed = getValue(argv, index, inlineValue, name, errors);
|
|
303
303
|
index = consumed.nextIndex;
|
|
304
304
|
if (consumed.value !== undefined) flags = appendFlagArray(flags, 'configOverrides', consumed.value);
|
|
@@ -357,6 +357,18 @@ export function parseGoodVibesCli(
|
|
|
357
357
|
errors.push(`Unknown command: ${rawCommand}`);
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
// Session lifecycle conflict detection — only one of --continue / --resume / --fork may be used.
|
|
361
|
+
const sessionLifecycleFlags = [
|
|
362
|
+
flags.continueLast ? '--continue' : undefined,
|
|
363
|
+
flags.resume !== undefined ? '--resume' : undefined,
|
|
364
|
+
flags.fork !== undefined ? '--fork' : undefined,
|
|
365
|
+
].filter((f): f is string => f !== undefined);
|
|
366
|
+
if (sessionLifecycleFlags.length > 1) {
|
|
367
|
+
errors.push(
|
|
368
|
+
`Conflicting session lifecycle flags: ${sessionLifecycleFlags.join(' and ')}. Use only one of --continue, --resume, or --fork.`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
360
372
|
return {
|
|
361
373
|
binary,
|
|
362
374
|
command,
|
|
@@ -365,5 +377,6 @@ export function parseGoodVibesCli(
|
|
|
365
377
|
positionals,
|
|
366
378
|
flags,
|
|
367
379
|
errors,
|
|
380
|
+
warnings,
|
|
368
381
|
};
|
|
369
382
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import type { CliCommandRuntime } from './
|
|
2
|
+
import type { CliCommandRuntime } from './types.ts';
|
|
3
3
|
import { buildCliServicePosture, createPlatformServiceManager, formatCliServicePosture, getServiceStateRoot } from './service-posture.ts';
|
|
4
4
|
import type { CliCommandOutput } from './types.ts';
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags } from '../runtime/surface-feature-flags.ts';
|
|
9
9
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
10
10
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
11
|
-
import type { CliCommandRuntime } from './
|
|
11
|
+
import type { CliCommandRuntime } from './types.ts';
|
|
12
12
|
import {
|
|
13
13
|
applyTargetEndpointFlagsOrDefault,
|
|
14
14
|
enableEndpointLanDefault,
|
package/src/cli/tui-startup.ts
CHANGED
|
@@ -1,32 +1,94 @@
|
|
|
1
1
|
import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
|
|
2
2
|
import type { InputHandler } from '../input/handler.ts';
|
|
3
|
-
import { readOnboardingCheckMarker } from '../runtime/onboarding/index.ts';
|
|
3
|
+
import { hasResumableWizardProgress, readOnboardingCheckMarker, readWizardProgress } from '../runtime/onboarding/index.ts';
|
|
4
|
+
import { readLastSessionPointer } from '@/runtime/index.ts';
|
|
4
5
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
5
6
|
|
|
7
|
+
export type TuiStartupShellPaths = Parameters<typeof readOnboardingCheckMarker>[0] & {
|
|
8
|
+
readonly workingDirectory: string;
|
|
9
|
+
readonly homeDirectory: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
6
12
|
export function applyInitialTuiCliState(options: {
|
|
7
13
|
readonly cli: GoodVibesCliParseResult;
|
|
8
14
|
readonly input: InputHandler;
|
|
9
15
|
readonly commandRegistry: CommandRegistry;
|
|
10
16
|
readonly commandContext: CommandContext;
|
|
11
|
-
readonly shellPaths:
|
|
17
|
+
readonly shellPaths: TuiStartupShellPaths;
|
|
12
18
|
readonly render: () => void;
|
|
13
|
-
}): void {
|
|
19
|
+
}): Promise<void> | undefined {
|
|
14
20
|
const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
|
|
15
21
|
const globalOnboardingMarker = readOnboardingCheckMarker(shellPaths, 'user');
|
|
22
|
+
|
|
23
|
+
// Seeded prompt is always applied synchronously, regardless of session branch.
|
|
24
|
+
const seededPrompt = cli.flags.prompt ?? (cli.rawCommand === undefined && cli.positionals.length > 0 ? cli.positionals.join(' ') : undefined);
|
|
25
|
+
if (seededPrompt) {
|
|
26
|
+
input.prompt = seededPrompt;
|
|
27
|
+
input.cursorPos = seededPrompt.length;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
if (cli.command === 'onboarding') {
|
|
17
31
|
input.openOnboardingWizard({ mode: 'edit', reset: true });
|
|
18
32
|
} else if (cli.command === 'sessions' && cli.commandArgs[0] === 'resume') {
|
|
19
33
|
const target = cli.commandArgs.slice(1).join(' ').trim();
|
|
20
34
|
if (target) {
|
|
21
|
-
|
|
35
|
+
return commandRegistry.execute('session', ['resume', target], commandContext).then(() => render());
|
|
36
|
+
}
|
|
37
|
+
} else if (cli.flags.continueLast) {
|
|
38
|
+
// --continue: resume the last session tracked by the pointer file
|
|
39
|
+
const lastId = readLastSessionPointer({
|
|
40
|
+
workingDirectory: shellPaths.workingDirectory,
|
|
41
|
+
homeDirectory: shellPaths.homeDirectory,
|
|
42
|
+
surfaceRoot: 'tui',
|
|
43
|
+
});
|
|
44
|
+
if (lastId) {
|
|
45
|
+
return commandRegistry.execute('session', ['resume', lastId], commandContext).then(() => render());
|
|
46
|
+
}
|
|
47
|
+
} else if (cli.flags.resume !== undefined) {
|
|
48
|
+
// --resume [id]: explicit id dispatches directly; bare form (sentinel 'latest') resolves via pointer
|
|
49
|
+
if (cli.flags.resume !== 'latest') {
|
|
50
|
+
return commandRegistry.execute('session', ['resume', cli.flags.resume], commandContext).then(() => render());
|
|
51
|
+
} else {
|
|
52
|
+
const lastId = readLastSessionPointer({
|
|
53
|
+
workingDirectory: shellPaths.workingDirectory,
|
|
54
|
+
homeDirectory: shellPaths.homeDirectory,
|
|
55
|
+
surfaceRoot: 'tui',
|
|
56
|
+
});
|
|
57
|
+
if (lastId) {
|
|
58
|
+
return commandRegistry.execute('session', ['resume', lastId], commandContext).then(() => render());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (cli.flags.fork !== undefined) {
|
|
62
|
+
// --fork [id]: fork specific session (true = bare fork-current; string = explicit id to resume then fork)
|
|
63
|
+
if (cli.flags.fork === true) {
|
|
64
|
+
// Bare --fork: fork the current session without a prior resume
|
|
65
|
+
return commandRegistry.execute('session', ['fork'], commandContext).then(() => render());
|
|
66
|
+
} else {
|
|
67
|
+
// Explicit id: resume the named session first, then fork
|
|
68
|
+
return commandRegistry.execute('session', ['resume', cli.flags.fork], commandContext)
|
|
69
|
+
.then(() => commandRegistry.execute('session', ['fork'], commandContext))
|
|
70
|
+
.then(() => render());
|
|
22
71
|
}
|
|
23
72
|
} else if (!globalOnboardingMarker.exists) {
|
|
24
73
|
input.openOnboardingWizard({ mode: 'new', reset: true });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
74
|
+
} else {
|
|
75
|
+
// User has completed onboarding before but left a wizard session in progress.
|
|
76
|
+
// Reopen the wizard at the last saved step so they can continue or dismiss.
|
|
77
|
+
const progressState = readWizardProgress(shellPaths);
|
|
78
|
+
if (hasResumableWizardProgress(shellPaths, { state: progressState })) {
|
|
79
|
+
const { payload } = progressState;
|
|
80
|
+
if (payload !== null) {
|
|
81
|
+
input.openOnboardingWizard({
|
|
82
|
+
mode: payload.mode,
|
|
83
|
+
reset: true,
|
|
84
|
+
preload: (wizard) => {
|
|
85
|
+
wizard.setStep(payload.stepIndex);
|
|
86
|
+
for (const [fieldId, value] of payload.toggleState) wizard.toggleState.set(fieldId, value);
|
|
87
|
+
for (const [fieldId, value] of payload.radioState) wizard.radioState.set(fieldId, value);
|
|
88
|
+
for (const [fieldId, value] of payload.textState) wizard.textState.set(fieldId, value);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
31
93
|
}
|
|
32
94
|
}
|
package/src/cli/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ConfigManager } from '../config/index.ts';
|
|
2
|
+
|
|
1
3
|
export type GoodVibesCliCommand =
|
|
2
4
|
| 'tui'
|
|
3
5
|
| 'run'
|
|
@@ -53,9 +55,9 @@ export interface GoodVibesCliFlags {
|
|
|
53
55
|
readonly continueLast: boolean;
|
|
54
56
|
readonly resume: string | undefined;
|
|
55
57
|
readonly session: string | undefined;
|
|
56
|
-
readonly fork:
|
|
57
|
-
readonly
|
|
58
|
-
readonly
|
|
58
|
+
readonly fork: string | true | undefined;
|
|
59
|
+
readonly yes: boolean;
|
|
60
|
+
readonly nonInteractive: boolean;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
export interface GoodVibesCliParseResult {
|
|
@@ -66,4 +68,13 @@ export interface GoodVibesCliParseResult {
|
|
|
66
68
|
readonly positionals: readonly string[];
|
|
67
69
|
readonly flags: GoodVibesCliFlags;
|
|
68
70
|
readonly errors: readonly string[];
|
|
71
|
+
/** Deprecation and soft-warning messages (non-fatal). Callers should surface these to users. */
|
|
72
|
+
readonly warnings: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CliCommandRuntime {
|
|
76
|
+
readonly cli: GoodVibesCliParseResult;
|
|
77
|
+
readonly configManager: ConfigManager;
|
|
78
|
+
readonly workingDirectory: string;
|
|
79
|
+
readonly homeDirectory: string;
|
|
69
80
|
}
|
package/src/cli-flags.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Compatibility wrapper for older imports. New CLI code lives in src/cli.
|
|
2
2
|
export type { GoodVibesCliFlags as CliFlags } from './cli/types.ts';
|
|
3
3
|
export {
|
|
4
|
+
applyRuntimeConfigDefault,
|
|
4
5
|
applyRuntimeConfigOverrides,
|
|
5
6
|
applyRuntimeConfigValue,
|
|
6
7
|
applyRuntimeCommandEndpointFlagOverrides,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export interface AtomicWriteOptions {
|
|
6
|
+
/**
|
|
7
|
+
* File permission mode for the written file. Defaults to 0o600 (owner read/write only).
|
|
8
|
+
*/
|
|
9
|
+
readonly mode?: number;
|
|
10
|
+
/**
|
|
11
|
+
* When true, creates parent directories if they do not exist. Defaults to false.
|
|
12
|
+
*/
|
|
13
|
+
readonly mkdirp?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Writes `data` to `path` atomically: writes to a sibling temp file, fsyncs
|
|
18
|
+
* it, then renames it into place. On POSIX, rename(2) is atomic so readers
|
|
19
|
+
* always see either the old file or the new file — never a partial write.
|
|
20
|
+
*
|
|
21
|
+
* If the write or fsync fails, the temp file is cleaned up before rethrowing.
|
|
22
|
+
*
|
|
23
|
+
* @param path Destination file path.
|
|
24
|
+
* @param data UTF-8 string content to write.
|
|
25
|
+
* @param opts Optional mode and mkdirp flags.
|
|
26
|
+
*/
|
|
27
|
+
export function atomicWriteFileSync(
|
|
28
|
+
path: string,
|
|
29
|
+
data: string,
|
|
30
|
+
opts: AtomicWriteOptions = {},
|
|
31
|
+
): void {
|
|
32
|
+
const mode = opts.mode ?? 0o600;
|
|
33
|
+
|
|
34
|
+
if (opts.mkdirp) {
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(tmp, data, { mode });
|
|
42
|
+
|
|
43
|
+
// fsync the temp file to flush OS write buffers before rename.
|
|
44
|
+
const fd = openSync(tmp, 'r+');
|
|
45
|
+
try {
|
|
46
|
+
fsyncSync(fd);
|
|
47
|
+
} finally {
|
|
48
|
+
closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
renameSync(tmp, path);
|
|
52
|
+
|
|
53
|
+
// fsync the parent directory to ensure the renamed directory entry is
|
|
54
|
+
// flushed. Without this, a hard crash immediately after rename could lose
|
|
55
|
+
// the entry on some filesystems (ext3/ext4 with data=ordered, etc.).
|
|
56
|
+
const dirFd = openSync(dirname(path), 'r');
|
|
57
|
+
try {
|
|
58
|
+
fsyncSync(dirFd);
|
|
59
|
+
} finally {
|
|
60
|
+
closeSync(dirFd);
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(tmp);
|
|
65
|
+
} catch {
|
|
66
|
+
// Best-effort cleanup — original error takes priority.
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -117,6 +117,8 @@ const KNOWN_DYNAMIC_KEYS = [
|
|
|
117
117
|
/^featureFlags(?:\.|$)/,
|
|
118
118
|
/^notifications\.webhookUrls$/,
|
|
119
119
|
/^wrfc\.gates$/,
|
|
120
|
+
// TUI-bridged setting awaiting SDK schema registration (handoff Item 5b)
|
|
121
|
+
/^tts\.speed$/,
|
|
120
122
|
];
|
|
121
123
|
|
|
122
124
|
export const GOODVIBES_ALLOWED_WRITE_ROOTS = ['tui/', 'daemon/'] as const;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A migration function that transforms data from version N to N+1.
|
|
5
|
+
* Receives the raw parsed object and must return the upgraded object.
|
|
6
|
+
*/
|
|
7
|
+
export type VersionMigration = (data: Record<string, unknown>) => Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
export interface ReadVersionedOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The version number this reader expects. When the file version equals
|
|
12
|
+
* `currentVersion`, no migrations are run. When it is lower, migrations
|
|
13
|
+
* are applied stepwise. When it is higher or unrecognised, `onUnknown`
|
|
14
|
+
* behaviour fires.
|
|
15
|
+
*/
|
|
16
|
+
readonly currentVersion: number;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Optional stepwise migrations indexed by the FROM version.
|
|
20
|
+
* `migrations[1]` upgrades version-1 data to version-2 data.
|
|
21
|
+
* Applied in ascending order until `currentVersion` is reached.
|
|
22
|
+
*/
|
|
23
|
+
readonly migrations?: Readonly<Record<number, VersionMigration>>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What to do when the file version is unrecognised (higher than
|
|
27
|
+
* `currentVersion` or missing/non-numeric).
|
|
28
|
+
*
|
|
29
|
+
* `'quarantine'` — rename the file to `<path>.unrecognized` and return null.
|
|
30
|
+
*/
|
|
31
|
+
readonly onUnknown: 'quarantine';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Migration-aware, quarantine-on-failure versioned file reader.
|
|
36
|
+
*
|
|
37
|
+
* Parse flow:
|
|
38
|
+
* 1. If the file does not exist → return null.
|
|
39
|
+
* 2. If JSON is corrupt → quarantine to `<path>.unrecognized`, return null.
|
|
40
|
+
* 3. If the version field is missing or higher than currentVersion →
|
|
41
|
+
* quarantine, return null.
|
|
42
|
+
* 4. If the version is lower than currentVersion → apply stepwise migrations.
|
|
43
|
+
* If no migration exists for a version gap, or a migration throws,
|
|
44
|
+
* quarantine and return null.
|
|
45
|
+
* 5. Return the (possibly migrated) object. Callers are responsible for
|
|
46
|
+
* narrowing the returned value — this helper handles versioning and
|
|
47
|
+
* corruption only, not schema validation.
|
|
48
|
+
*/
|
|
49
|
+
export function readVersioned<T extends { version: number }>(
|
|
50
|
+
path: string,
|
|
51
|
+
options: ReadVersionedOptions,
|
|
52
|
+
): T | null {
|
|
53
|
+
if (!existsSync(path)) return null;
|
|
54
|
+
|
|
55
|
+
let raw: unknown;
|
|
56
|
+
try {
|
|
57
|
+
raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
58
|
+
} catch {
|
|
59
|
+
quarantine(path);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!isPlainObject(raw)) {
|
|
64
|
+
quarantine(path);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fileVersion = raw['version'];
|
|
69
|
+
if (typeof fileVersion !== 'number' || !Number.isFinite(fileVersion)) {
|
|
70
|
+
quarantine(path);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (fileVersion > options.currentVersion) {
|
|
75
|
+
// Produced by a newer process — quarantine rather than corrupt.
|
|
76
|
+
quarantine(path);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let data: Record<string, unknown> = raw;
|
|
81
|
+
|
|
82
|
+
if (fileVersion < options.currentVersion) {
|
|
83
|
+
const migrations = options.migrations ?? {};
|
|
84
|
+
for (let v = fileVersion; v < options.currentVersion; v++) {
|
|
85
|
+
const migrate = migrations[v];
|
|
86
|
+
if (!migrate) {
|
|
87
|
+
// No migration path for this version gap — quarantine.
|
|
88
|
+
quarantine(path);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
data = migrate(data);
|
|
93
|
+
} catch {
|
|
94
|
+
quarantine(path);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return data as T;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
106
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function quarantine(path: string): void {
|
|
110
|
+
try {
|
|
111
|
+
renameSync(path, `${path}.unrecognized`);
|
|
112
|
+
} catch {
|
|
113
|
+
// Best-effort — if rename fails (e.g. race), proceed silently.
|
|
114
|
+
}
|
|
115
|
+
}
|