@pellux/goodvibes-tui 0.19.23 → 0.19.25
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 +21 -0
- package/README.md +5 -5
- package/bin/goodvibes +5 -0
- package/bin/goodvibes-daemon +5 -0
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/completion.ts +89 -0
- package/src/cli/config-overrides.ts +159 -0
- package/src/cli/endpoints.ts +63 -0
- package/src/cli/entrypoint.ts +155 -0
- package/src/cli/help.ts +122 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/management-commands.ts +576 -0
- package/src/cli/management.ts +693 -0
- package/src/cli/parser.ts +367 -0
- package/src/cli/status.ts +112 -0
- package/src/cli/tui-startup.ts +32 -0
- package/src/cli/types.ts +63 -0
- package/src/cli-flags.ts +17 -55
- package/src/config/index.ts +1 -1
- package/src/config/secrets.ts +44 -0
- package/src/core/conversation.ts +36 -13
- package/src/daemon/cli.ts +62 -11
- package/src/input/command-registry.ts +3 -0
- package/src/input/commands/guidance-runtime.ts +9 -4
- package/src/input/commands/local-runtime.ts +21 -7
- package/src/input/commands/local-setup.ts +31 -38
- package/src/input/commands/onboarding-runtime.ts +14 -0
- package/src/input/commands/runtime-services.ts +9 -0
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +8 -1
- package/src/input/handler-feed.ts +13 -8
- package/src/input/handler-interactions.ts +266 -0
- package/src/input/handler-modal-stack.ts +23 -3
- package/src/input/handler-modal-token-routes.ts +23 -1
- package/src/input/handler-onboarding.ts +696 -0
- package/src/input/handler-picker-routes.ts +15 -7
- package/src/input/handler-ui-state.ts +58 -0
- package/src/input/handler.ts +120 -246
- package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
- package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
- package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
- package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
- package/src/input/onboarding/onboarding-wizard.ts +594 -0
- package/src/main.ts +32 -39
- package/src/panels/builtin/operations.ts +0 -10
- package/src/panels/index.ts +0 -1
- package/src/panels/panel-manager.ts +6 -2
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
- package/src/renderer/panel-composite.ts +42 -5
- package/src/renderer/panel-workspace-bar.ts +5 -1
- package/src/runtime/bootstrap-core.ts +1 -0
- package/src/runtime/bootstrap.ts +123 -0
- package/src/runtime/onboarding/apply.ts +685 -0
- package/src/runtime/onboarding/derivation.ts +495 -0
- package/src/runtime/onboarding/index.ts +7 -0
- package/src/runtime/onboarding/markers.ts +161 -0
- package/src/runtime/onboarding/snapshot.ts +400 -0
- package/src/runtime/onboarding/state.ts +140 -0
- package/src/runtime/onboarding/types.ts +402 -0
- package/src/runtime/onboarding/verify.ts +233 -0
- package/src/runtime/ui-services.ts +16 -0
- package/src/shell/ui-openers.ts +12 -2
- package/src/version.ts +1 -1
- package/src/panels/welcome-panel.ts +0 -64
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.19.25] — 2026-04-24
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
- 705a19b feat: add onboarding wizard and CLI management
|
|
11
|
+
|
|
12
|
+
## [0.19.24] — 2026-04-22
|
|
13
|
+
|
|
14
|
+
Four correctness and honesty fixes from an external architecture review. No SDK change (pinned at 0.23.2).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **`/clear` now actually clears the display** (`src/core/conversation.ts`). The original `clearDisplay()` immediately rebuilt the display buffer from `this.messages`, so `getDisplayBlocks().length` was unchanged before and after the call — `/clear` was a no-op. The fix introduces a `_displayFromMessageIndex` watermark: `clearDisplay()` records the current message count and clears the buffer without re-rendering; `rebuildHistory()` now only renders messages added *after* the watermark, so the screen is blank until the next message arrives. The full LLM context (`getMessagesForLLM()`, `getMessageSnapshot()`) is untouched. `resetAll()` zeroes the watermark so a full reset continues to work. Regression tests added to `src/test/core/conversation.test.ts` (4 new tests).
|
|
19
|
+
|
|
20
|
+
- **Workspace tab bar shows active state on the unfocused pane** (`src/panels/panel-manager.ts`, `src/renderer/panel-workspace-bar.ts`). `getWorkspaceTabs()` previously set both `active` and `focused` from the single globally-focused panel, so the unfocused pane's selected tab lost its active marker entirely when focus moved to the other pane. The fix splits the two semantics: `active` is now true for the currently-selected tab in *its own pane* (derived from `pane.activeIndex`, independent of keyboard focus); `focused` is true only for the one tab in the globally-focused pane. The workspace bar renderer uses the `focused` flag to append a `▸` glyph to the focused tab's label, making keyboard-focus distinct from pane-level selection. Both panes now show their selected tab highlighted regardless of which has focus. Regression tests added to `src/test/panels/panel-manager.test.ts` (2 new tests).
|
|
21
|
+
|
|
22
|
+
- **Panel render cache race guard** (`src/renderer/panel-composite.ts`). The documented hazard in the R2 cache — where a mid-render `invalidate()` call would be silently clobbered by the trailing `markRendered()` — has been resolved. The fix wraps each panel's `invalidate()` the first time it enters `renderPanel()` to maintain a per-panel generation counter (stored in a module-level `WeakMap`). The generation is snapshotted before `render()` and compared after: `markRendered()` is only called when the generation is unchanged, meaning no concurrent invalidation fired. If the generation changed, `needsRender` stays `true` and the next frame re-renders with the fresh state. Backward-compatible: existing panels that don't invalidate during render are unaffected. New test file `src/test/renderer/panel-composite.test.ts` (4 tests).
|
|
23
|
+
|
|
24
|
+
- **Release gate filenames renamed to match what the tests actually verify** (`src/test/release-gates/`). `runtime-certification-gate.test.ts` was renamed to `runtime-contract-shape-gate.test.ts` and its `describe()` block updated to `'runtime contract shape gate'`. `foundation-surfaces-gate.test.ts` was renamed to `foundation-surface-stability-gate.test.ts` and its `describe()` block updated to `'foundation surface stability gate'`. The test content (assertions, helpers) is unchanged — this is an honesty rename only, removing the implication that the tests verify runtime behavior certification when they actually verify structural shape preservation.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
7
28
|
## [0.19.23] — 2026-04-22
|
|
8
29
|
|
|
9
30
|
SDK 0.23.x constraint-propagation reconciliation. Surfaces the new per-chain constraint data across the WRFC panel, process modal, agent inspector, and agent detail modal; adds system-message notifications for constraint enumeration and violations; exposes the WRFC-injected `systemPromptAddendum` so operators can see the engineer addendum was applied.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
|
@@ -380,10 +380,10 @@ Alternatively, store keys encrypted using the `/secrets` command. Environment va
|
|
|
380
380
|
For self-hosted or external secret managers, link a GoodVibes key to a provider-backed SecretRef:
|
|
381
381
|
|
|
382
382
|
```sh
|
|
383
|
-
/secrets link OPENAI_API_KEY
|
|
384
|
-
/secrets link SLACK_BOT_TOKEN vaultwarden
|
|
385
|
-
/secrets link STRIPE_TOKEN bws
|
|
386
|
-
/secrets link OPENAI_API_KEY
|
|
383
|
+
/secrets link OPENAI_API_KEY goodvibes://secrets/bitwarden?item=GoodVibes%20OpenAI&field=password&sessionEnv=BW_SESSION
|
|
384
|
+
/secrets link SLACK_BOT_TOKEN goodvibes://secrets/vaultwarden?item=GoodVibes%20Slack&field=password&server=https%3A%2F%2Fvault.example.test
|
|
385
|
+
/secrets link STRIPE_TOKEN goodvibes://secrets/bws/00000000-0000-0000-0000-000000000000?field=value&accessTokenEnv=BWS_ACCESS_TOKEN
|
|
386
|
+
/secrets link OPENAI_API_KEY goodvibes://secrets/1password?vault=Private&item=GoodVibes%20OpenAI&field=API%20Key
|
|
387
387
|
```
|
|
388
388
|
|
|
389
389
|
Use `/secrets providers` for supported provider shapes and `/secrets test <secret-ref>` to validate a ref without printing its value.
|
package/bin/goodvibes
CHANGED
|
@@ -39,8 +39,13 @@ function run(command, args) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const artifactName = resolveArtifactName(process.platform, process.arch);
|
|
42
|
+
const localBuild = join(packageRoot, 'dist', 'goodvibes');
|
|
42
43
|
const vendoredBinary = artifactName ? join(packageRoot, 'vendor', artifactName) : null;
|
|
43
44
|
|
|
45
|
+
if (isExecutable(localBuild)) {
|
|
46
|
+
run(localBuild, process.argv.slice(2));
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
if (vendoredBinary && isExecutable(vendoredBinary)) {
|
|
45
50
|
run(vendoredBinary, process.argv.slice(2));
|
|
46
51
|
}
|
package/bin/goodvibes-daemon
CHANGED
|
@@ -39,8 +39,13 @@ function run(command, args) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const artifactName = resolveArtifactName(process.platform, process.arch);
|
|
42
|
+
const localBuild = join(packageRoot, 'dist', 'goodvibes-daemon');
|
|
42
43
|
const vendoredBinary = artifactName ? join(packageRoot, 'vendor', artifactName) : null;
|
|
43
44
|
|
|
45
|
+
if (isExecutable(localBuild)) {
|
|
46
|
+
run(localBuild, process.argv.slice(2));
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
if (vendoredBinary && isExecutable(vendoredBinary)) {
|
|
45
50
|
run(vendoredBinary, process.argv.slice(2));
|
|
46
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.25",
|
|
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",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
91
91
|
"@ast-grep/napi": "^0.42.0",
|
|
92
92
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
93
|
-
"@pellux/goodvibes-sdk": "0.
|
|
93
|
+
"@pellux/goodvibes-sdk": "^0.25.0",
|
|
94
94
|
"bash-language-server": "^5.6.0",
|
|
95
95
|
"fuse.js": "^7.1.0",
|
|
96
96
|
"graphql": "^16.13.2",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const COMMANDS = [
|
|
2
|
+
'tui',
|
|
3
|
+
'run',
|
|
4
|
+
'exec',
|
|
5
|
+
'serve',
|
|
6
|
+
'web',
|
|
7
|
+
'onboarding',
|
|
8
|
+
'setup',
|
|
9
|
+
'doctor',
|
|
10
|
+
'status',
|
|
11
|
+
'models',
|
|
12
|
+
'providers',
|
|
13
|
+
'auth',
|
|
14
|
+
'subscription',
|
|
15
|
+
'secrets',
|
|
16
|
+
'sessions',
|
|
17
|
+
'tasks',
|
|
18
|
+
'pair',
|
|
19
|
+
'qrcode',
|
|
20
|
+
'surfaces',
|
|
21
|
+
'listener',
|
|
22
|
+
'control-plane',
|
|
23
|
+
'bundle',
|
|
24
|
+
'remote',
|
|
25
|
+
'bridge',
|
|
26
|
+
'completion',
|
|
27
|
+
'version',
|
|
28
|
+
'help',
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
const OPTIONS = [
|
|
32
|
+
'--help',
|
|
33
|
+
'--version',
|
|
34
|
+
'--model',
|
|
35
|
+
'--provider',
|
|
36
|
+
'--cd',
|
|
37
|
+
'--working-dir',
|
|
38
|
+
'--daemon-home',
|
|
39
|
+
'--config',
|
|
40
|
+
'--enable',
|
|
41
|
+
'--disable',
|
|
42
|
+
'--prompt',
|
|
43
|
+
'--print',
|
|
44
|
+
'--output',
|
|
45
|
+
'--output-format',
|
|
46
|
+
'--json',
|
|
47
|
+
'--no-alt-screen',
|
|
48
|
+
'--port',
|
|
49
|
+
'--hostname',
|
|
50
|
+
'--open',
|
|
51
|
+
'--resume',
|
|
52
|
+
'--session',
|
|
53
|
+
'--continue',
|
|
54
|
+
'--fork',
|
|
55
|
+
'--password',
|
|
56
|
+
'--password-stdin',
|
|
57
|
+
'--role',
|
|
58
|
+
'--manual',
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export function renderCompletion(shell: string | undefined, binary = 'goodvibes'): string {
|
|
62
|
+
const normalized = (shell ?? 'bash').toLowerCase();
|
|
63
|
+
const words = [...COMMANDS, ...OPTIONS].join(' ');
|
|
64
|
+
|
|
65
|
+
if (normalized === 'zsh') {
|
|
66
|
+
return [
|
|
67
|
+
`#compdef ${binary}`,
|
|
68
|
+
`_${binary}() {`,
|
|
69
|
+
` compadd -- ${words}`,
|
|
70
|
+
'}',
|
|
71
|
+
`_${binary} "$@"`,
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (normalized === 'fish') {
|
|
76
|
+
return [...COMMANDS, ...OPTIONS]
|
|
77
|
+
.map((word) => `complete -c ${binary} -a ${JSON.stringify(word)}`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
`_${binary}() {`,
|
|
83
|
+
' local cur',
|
|
84
|
+
' cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
85
|
+
` COMPREPLY=( $(compgen -W "${words}" -- "$cur") )`,
|
|
86
|
+
'}',
|
|
87
|
+
`complete -F _${binary} ${binary}`,
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ConfigKey, ConfigManager, ConfigSetting, GoodVibesConfig, PersistedFlagState } from '../config/index.ts';
|
|
2
|
+
import { CONFIG_SCHEMA, ConfigError } from '../config/index.ts';
|
|
3
|
+
import type { GoodVibesCliCommand, GoodVibesCliFlags } from './types.ts';
|
|
4
|
+
import { RUNTIME_ENDPOINT_CONFIG_KEYS, hostModeForHostname } from './endpoints.ts';
|
|
5
|
+
import type { RuntimeEndpointId } from './endpoints.ts';
|
|
6
|
+
|
|
7
|
+
const CONFIG_SCHEMA_BY_KEY = new Map<string, ConfigSetting>(
|
|
8
|
+
CONFIG_SCHEMA.map((setting) => [setting.key, setting]),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
function parseConfigOverrideValue(value: string): unknown {
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (trimmed.length === 0) return '';
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(trimmed) as unknown;
|
|
16
|
+
} catch {
|
|
17
|
+
if (trimmed === 'true') return true;
|
|
18
|
+
if (trimmed === 'false') return false;
|
|
19
|
+
if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getRuntimeConfig(configManager: ConfigManager): GoodVibesConfig {
|
|
25
|
+
const mutable = configManager as unknown as { config?: GoodVibesConfig };
|
|
26
|
+
if (!mutable.config || typeof mutable.config !== 'object') {
|
|
27
|
+
throw new ConfigError('ConfigManager runtime config is not available for CLI overrides.');
|
|
28
|
+
}
|
|
29
|
+
return mutable.config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateConfigValue(setting: ConfigSetting, value: unknown): void {
|
|
33
|
+
if (setting.type === 'boolean' && typeof value !== 'boolean') {
|
|
34
|
+
throw new ConfigError(`Invalid value for ${setting.key}: expected boolean.`);
|
|
35
|
+
}
|
|
36
|
+
if (setting.type === 'number' && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
37
|
+
throw new ConfigError(`Invalid value for ${setting.key}: expected number.`);
|
|
38
|
+
}
|
|
39
|
+
if (setting.type === 'string' && typeof value !== 'string') {
|
|
40
|
+
throw new ConfigError(`Invalid value for ${setting.key}: expected string.`);
|
|
41
|
+
}
|
|
42
|
+
if (setting.type === 'enum' && setting.enumValues && !setting.enumValues.includes(String(value))) {
|
|
43
|
+
throw new ConfigError(`Invalid value for ${setting.key}: "${String(value)}". Allowed: ${setting.enumValues.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
if (setting.validate && !setting.validate(value)) {
|
|
46
|
+
throw new ConfigError(`Invalid value for ${setting.key}: ${String(value)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setNestedConfigValue(config: GoodVibesConfig, key: ConfigKey, value: unknown): void {
|
|
51
|
+
const parts = key.split('.');
|
|
52
|
+
let cursor: unknown = config;
|
|
53
|
+
for (const part of parts.slice(0, -1)) {
|
|
54
|
+
if (cursor == null || typeof cursor !== 'object' || !(part in cursor)) {
|
|
55
|
+
throw new ConfigError(`Invalid config path: section '${part}' does not exist`);
|
|
56
|
+
}
|
|
57
|
+
cursor = (cursor as Record<string, unknown>)[part];
|
|
58
|
+
}
|
|
59
|
+
if (cursor == null || typeof cursor !== 'object') {
|
|
60
|
+
throw new ConfigError(`Invalid config path: section '${parts.slice(0, -1).join('.')}' does not exist`);
|
|
61
|
+
}
|
|
62
|
+
(cursor as Record<string, unknown>)[parts[parts.length - 1]!] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function applyRuntimeConfigValue(configManager: ConfigManager, key: ConfigKey, value: unknown): void {
|
|
66
|
+
const setting = CONFIG_SCHEMA_BY_KEY.get(key);
|
|
67
|
+
if (!setting) {
|
|
68
|
+
throw new ConfigError(`Unknown config key: ${key}`);
|
|
69
|
+
}
|
|
70
|
+
validateConfigValue(setting, value);
|
|
71
|
+
setNestedConfigValue(getRuntimeConfig(configManager), key, value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function applyRuntimeConfigOverrides(
|
|
75
|
+
configManager: ConfigManager,
|
|
76
|
+
overrides: readonly string[],
|
|
77
|
+
): readonly string[] {
|
|
78
|
+
const errors: string[] = [];
|
|
79
|
+
for (const override of overrides) {
|
|
80
|
+
const index = override.indexOf('=');
|
|
81
|
+
if (index <= 0) {
|
|
82
|
+
errors.push(`Invalid --config override "${override}". Expected key=value.`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const key = override.slice(0, index) as ConfigKey;
|
|
86
|
+
const rawValue = override.slice(index + 1);
|
|
87
|
+
try {
|
|
88
|
+
applyRuntimeConfigValue(configManager, key, parseConfigOverrideValue(rawValue));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
errors.push(error instanceof Error ? `Invalid --config ${override}: ${error.message}` : `Invalid --config ${override}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function applyRuntimeFeatureFlagOverrides(
|
|
97
|
+
configManager: ConfigManager,
|
|
98
|
+
options: {
|
|
99
|
+
readonly enableFeatures: readonly string[];
|
|
100
|
+
readonly disableFeatures: readonly string[];
|
|
101
|
+
},
|
|
102
|
+
): void {
|
|
103
|
+
if (options.enableFeatures.length === 0 && options.disableFeatures.length === 0) return;
|
|
104
|
+
const config = getRuntimeConfig(configManager);
|
|
105
|
+
const flags = { ...config.featureFlags };
|
|
106
|
+
for (const feature of options.enableFeatures) {
|
|
107
|
+
flags[feature] = 'enabled' satisfies PersistedFlagState;
|
|
108
|
+
}
|
|
109
|
+
for (const feature of options.disableFeatures) {
|
|
110
|
+
flags[feature] = 'disabled' satisfies PersistedFlagState;
|
|
111
|
+
}
|
|
112
|
+
config.featureFlags = flags;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function applyRuntimeEndpointFlagOverrides(
|
|
116
|
+
configManager: ConfigManager,
|
|
117
|
+
endpoint: RuntimeEndpointId,
|
|
118
|
+
flags: Pick<GoodVibesCliFlags, 'hostname' | 'port'>,
|
|
119
|
+
): readonly string[] {
|
|
120
|
+
const keys = RUNTIME_ENDPOINT_CONFIG_KEYS[endpoint];
|
|
121
|
+
const errors: string[] = [];
|
|
122
|
+
|
|
123
|
+
if (flags.hostname !== undefined) {
|
|
124
|
+
try {
|
|
125
|
+
applyRuntimeConfigValue(configManager, keys.hostMode, hostModeForHostname(flags.hostname));
|
|
126
|
+
applyRuntimeConfigValue(configManager, keys.host, flags.hostname);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
errors.push(error instanceof Error
|
|
129
|
+
? `Invalid --hostname ${flags.hostname}: ${error.message}`
|
|
130
|
+
: `Invalid --hostname ${flags.hostname}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (flags.port !== undefined) {
|
|
135
|
+
try {
|
|
136
|
+
applyRuntimeConfigValue(configManager, keys.port, flags.port);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
errors.push(error instanceof Error
|
|
139
|
+
? `Invalid --port ${flags.port}: ${error.message}`
|
|
140
|
+
: `Invalid --port ${flags.port}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return errors;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function applyRuntimeCommandEndpointFlagOverrides(
|
|
148
|
+
configManager: ConfigManager,
|
|
149
|
+
command: GoodVibesCliCommand,
|
|
150
|
+
flags: Pick<GoodVibesCliFlags, 'hostname' | 'port'>,
|
|
151
|
+
): readonly string[] {
|
|
152
|
+
if (flags.hostname === undefined && flags.port === undefined) return [];
|
|
153
|
+
if (command === 'web') return applyRuntimeEndpointFlagOverrides(configManager, 'web', flags);
|
|
154
|
+
if (command === 'listener') return applyRuntimeEndpointFlagOverrides(configManager, 'httpListener', flags);
|
|
155
|
+
if (command === 'control-plane' || command === 'pair' || command === 'serve') {
|
|
156
|
+
return applyRuntimeEndpointFlagOverrides(configManager, 'controlPlane', flags);
|
|
157
|
+
}
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ConfigKey, ConfigManager } from '../config/index.ts';
|
|
2
|
+
|
|
3
|
+
export type RuntimeEndpointId = 'controlPlane' | 'httpListener' | 'web';
|
|
4
|
+
export type RuntimeHostMode = 'local' | 'network' | 'custom';
|
|
5
|
+
|
|
6
|
+
export const RUNTIME_ENDPOINT_DEFAULT_PORTS: Record<RuntimeEndpointId, number> = {
|
|
7
|
+
controlPlane: 3421,
|
|
8
|
+
httpListener: 3422,
|
|
9
|
+
web: 3423,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const RUNTIME_ENDPOINT_CONFIG_KEYS: Record<RuntimeEndpointId, {
|
|
13
|
+
readonly hostMode: ConfigKey;
|
|
14
|
+
readonly host: ConfigKey;
|
|
15
|
+
readonly port: ConfigKey;
|
|
16
|
+
}> = {
|
|
17
|
+
controlPlane: {
|
|
18
|
+
hostMode: 'controlPlane.hostMode',
|
|
19
|
+
host: 'controlPlane.host',
|
|
20
|
+
port: 'controlPlane.port',
|
|
21
|
+
},
|
|
22
|
+
httpListener: {
|
|
23
|
+
hostMode: 'httpListener.hostMode',
|
|
24
|
+
host: 'httpListener.host',
|
|
25
|
+
port: 'httpListener.port',
|
|
26
|
+
},
|
|
27
|
+
web: {
|
|
28
|
+
hostMode: 'web.hostMode',
|
|
29
|
+
host: 'web.host',
|
|
30
|
+
port: 'web.port',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface RuntimeEndpointBinding {
|
|
35
|
+
readonly hostMode: string;
|
|
36
|
+
readonly configuredHost: string;
|
|
37
|
+
readonly host: string;
|
|
38
|
+
readonly port: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hostModeForHostname(hostname: string): RuntimeHostMode {
|
|
42
|
+
const normalized = hostname.toLowerCase();
|
|
43
|
+
if (normalized === '0.0.0.0' || normalized === '::') return 'network';
|
|
44
|
+
if (normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1') return 'local';
|
|
45
|
+
return 'custom';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveRuntimeEndpointBinding(
|
|
49
|
+
config: Pick<ConfigManager, 'get'>,
|
|
50
|
+
endpoint: RuntimeEndpointId,
|
|
51
|
+
): RuntimeEndpointBinding {
|
|
52
|
+
const keys = RUNTIME_ENDPOINT_CONFIG_KEYS[endpoint];
|
|
53
|
+
const hostMode = String(config.get(keys.hostMode) ?? 'local');
|
|
54
|
+
const configuredHost = String(config.get(keys.host) ?? '127.0.0.1');
|
|
55
|
+
const port = Number(config.get(keys.port) || RUNTIME_ENDPOINT_DEFAULT_PORTS[endpoint]);
|
|
56
|
+
if (hostMode === 'network') {
|
|
57
|
+
return { hostMode, configuredHost, host: '0.0.0.0', port };
|
|
58
|
+
}
|
|
59
|
+
if (hostMode === 'custom') {
|
|
60
|
+
return { hostMode, configuredHost, host: configuredHost || '127.0.0.1', port };
|
|
61
|
+
}
|
|
62
|
+
return { hostMode, configuredHost, host: '127.0.0.1', port };
|
|
63
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ConfigManager } from '../config/index.ts';
|
|
4
|
+
import { readOnboardingCompletionMarkers } from '../runtime/onboarding/index.ts';
|
|
5
|
+
import { GlobalNetworkTransportInstaller } from '@pellux/goodvibes-sdk/platform/runtime/network/index';
|
|
6
|
+
import { createShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
|
|
7
|
+
import { configureActivityLogger } from '@pellux/goodvibes-sdk/platform/utils/logger';
|
|
8
|
+
import {
|
|
9
|
+
applyRuntimeCommandEndpointFlagOverrides,
|
|
10
|
+
applyRuntimeConfigOverrides,
|
|
11
|
+
applyRuntimeConfigValue,
|
|
12
|
+
applyRuntimeFeatureFlagOverrides,
|
|
13
|
+
handleGoodVibesCliCommand,
|
|
14
|
+
parseGoodVibesCli,
|
|
15
|
+
renderCliStatus,
|
|
16
|
+
renderCompletion,
|
|
17
|
+
renderGoodVibesHelp,
|
|
18
|
+
renderGoodVibesVersion,
|
|
19
|
+
renderOnboardingCliStatus,
|
|
20
|
+
} from './index.ts';
|
|
21
|
+
|
|
22
|
+
type ShellEntrypointOwnership = {
|
|
23
|
+
readonly workingDirectory: string;
|
|
24
|
+
readonly homeDirectory: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ShellEntrypointRoots = {
|
|
28
|
+
readonly defaultWorkingDirectory: string;
|
|
29
|
+
readonly homeDirectory: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type PreparedShellCliRuntime = {
|
|
33
|
+
readonly cli: ReturnType<typeof parseGoodVibesCli>;
|
|
34
|
+
readonly configManager: ConfigManager;
|
|
35
|
+
readonly bootstrapWorkingDir: string;
|
|
36
|
+
readonly bootstrapHomeDirectory: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function resolveShellEntrypointOwnership(roots: ShellEntrypointRoots, workingDirOverride?: string): ShellEntrypointOwnership {
|
|
40
|
+
return {
|
|
41
|
+
workingDirectory: workingDirOverride ?? roots.defaultWorkingDirectory,
|
|
42
|
+
homeDirectory: roots.homeDirectory,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function prepareShellCliRuntime(
|
|
47
|
+
argv: readonly string[],
|
|
48
|
+
roots: ShellEntrypointRoots,
|
|
49
|
+
binary = 'goodvibes',
|
|
50
|
+
): Promise<PreparedShellCliRuntime> {
|
|
51
|
+
const cli = parseGoodVibesCli(argv, binary);
|
|
52
|
+
|
|
53
|
+
if (cli.errors.length > 0) {
|
|
54
|
+
console.error(cli.errors.join('\n'));
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error(renderGoodVibesHelp(binary));
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (cli.flags.help || cli.command === 'help') {
|
|
61
|
+
console.log(renderGoodVibesHelp(binary));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (cli.flags.version || cli.command === 'version') {
|
|
66
|
+
console.log(renderGoodVibesVersion(binary));
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cli.command === 'completion') {
|
|
71
|
+
console.log(renderCompletion(cli.commandArgs[0], binary));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (cli.command === 'serve') {
|
|
76
|
+
await import('../daemon/cli.ts');
|
|
77
|
+
return new Promise<PreparedShellCliRuntime>(() => {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const {
|
|
81
|
+
workingDirectory: bootstrapWorkingDir,
|
|
82
|
+
homeDirectory: bootstrapHomeDirectory,
|
|
83
|
+
} = resolveShellEntrypointOwnership(roots, cli.flags.workingDir ?? (cli.command === 'tui' ? cli.commandArgs[0] : undefined));
|
|
84
|
+
configureActivityLogger(join(bootstrapWorkingDir, '.goodvibes', 'logs'));
|
|
85
|
+
const configManager = new ConfigManager({
|
|
86
|
+
workingDir: bootstrapWorkingDir,
|
|
87
|
+
homeDir: bootstrapHomeDirectory,
|
|
88
|
+
surfaceRoot: 'tui',
|
|
89
|
+
});
|
|
90
|
+
new GlobalNetworkTransportInstaller().install(configManager);
|
|
91
|
+
|
|
92
|
+
const overrideErrors = applyRuntimeConfigOverrides(configManager, cli.flags.configOverrides);
|
|
93
|
+
if (overrideErrors.length > 0) {
|
|
94
|
+
console.error(overrideErrors.join('\n'));
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
97
|
+
applyRuntimeFeatureFlagOverrides(configManager, {
|
|
98
|
+
enableFeatures: cli.flags.enableFeatures,
|
|
99
|
+
disableFeatures: cli.flags.disableFeatures,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (cli.flags.provider !== undefined) {
|
|
103
|
+
applyRuntimeConfigValue(configManager, 'provider.provider', cli.flags.provider);
|
|
104
|
+
}
|
|
105
|
+
if (cli.flags.model !== undefined) {
|
|
106
|
+
applyRuntimeConfigValue(configManager, 'provider.model', cli.flags.model);
|
|
107
|
+
}
|
|
108
|
+
const endpointOverrideErrors = applyRuntimeCommandEndpointFlagOverrides(configManager, cli.command, cli.flags);
|
|
109
|
+
if (endpointOverrideErrors.length > 0) {
|
|
110
|
+
console.error(endpointOverrideErrors.join('\n'));
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (cli.command === 'status' || cli.command === 'doctor' || (cli.command === 'onboarding' && cli.commandArgs[0] === 'status')) {
|
|
115
|
+
const shellPaths = createShellPathService({
|
|
116
|
+
workingDirectory: bootstrapWorkingDir,
|
|
117
|
+
homeDirectory: bootstrapHomeDirectory,
|
|
118
|
+
});
|
|
119
|
+
const userStorePath = shellPaths.resolveUserPath('tui', 'auth-users.json');
|
|
120
|
+
const bootstrapCredentialPath = shellPaths.resolveUserPath('tui', 'auth-bootstrap.txt');
|
|
121
|
+
const operatorTokenPath = join(bootstrapHomeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
|
|
122
|
+
const onboardingMarkers = readOnboardingCompletionMarkers(shellPaths);
|
|
123
|
+
const statusOptions = {
|
|
124
|
+
configManager,
|
|
125
|
+
workingDirectory: bootstrapWorkingDir,
|
|
126
|
+
homeDirectory: bootstrapHomeDirectory,
|
|
127
|
+
onboardingMarkers,
|
|
128
|
+
auth: {
|
|
129
|
+
userStorePath,
|
|
130
|
+
userStorePresent: existsSync(userStorePath),
|
|
131
|
+
bootstrapCredentialPath,
|
|
132
|
+
bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
|
|
133
|
+
operatorTokenPath,
|
|
134
|
+
operatorTokenPresent: existsSync(operatorTokenPath),
|
|
135
|
+
},
|
|
136
|
+
doctor: cli.command === 'doctor',
|
|
137
|
+
};
|
|
138
|
+
console.log(cli.command === 'onboarding'
|
|
139
|
+
? renderOnboardingCliStatus(statusOptions)
|
|
140
|
+
: renderCliStatus(statusOptions));
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const cliCommandResult = await handleGoodVibesCliCommand({
|
|
145
|
+
cli,
|
|
146
|
+
configManager,
|
|
147
|
+
workingDirectory: bootstrapWorkingDir,
|
|
148
|
+
homeDirectory: bootstrapHomeDirectory,
|
|
149
|
+
});
|
|
150
|
+
if (cliCommandResult.handled) {
|
|
151
|
+
process.exit(cliCommandResult.exitCode);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { cli, configManager, bootstrapWorkingDir, bootstrapHomeDirectory };
|
|
155
|
+
}
|