@pellux/goodvibes-tui 0.19.28 → 0.19.29
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 +6 -0
- package/README.md +3 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/surface-command.ts +46 -11
- package/src/daemon/cli.ts +7 -0
- package/src/input/handler-onboarding.ts +151 -44
- package/src/input/onboarding/handler-onboarding-routes.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +35 -8
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -5
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +93 -5
- package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-rules.ts +22 -3
- package/src/input/onboarding/onboarding-wizard-state.ts +12 -7
- package/src/input/onboarding/onboarding-wizard-steps.ts +133 -59
- package/src/input/onboarding/onboarding-wizard-types.ts +10 -0
- package/src/input/onboarding/onboarding-wizard.ts +56 -4
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +4 -0
- package/src/main.ts +33 -26
- package/src/renderer/compositor.ts +3 -3
- package/src/renderer/onboarding/onboarding-wizard.ts +38 -21
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +3 -0
- package/src/runtime/bootstrap.ts +15 -0
- package/src/runtime/onboarding/apply.ts +36 -8
- package/src/runtime/onboarding/derivation.ts +7 -7
- package/src/runtime/onboarding/snapshot.ts +1 -0
- package/src/runtime/onboarding/types.ts +4 -1
- package/src/runtime/onboarding/verify.ts +1 -1
- package/src/runtime/surface-feature-flags.ts +67 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.19.29] — 2026-04-24
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
- 617b8860 feat: improve onboarding and surface setup
|
|
11
|
+
- 5e39b0f8 docs: add onboarding wizard WIP notice to README top
|
|
12
|
+
|
|
7
13
|
## [0.19.28] — 2026-04-24
|
|
8
14
|
|
|
9
15
|
### Changes
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
> **ATTENTION:** Currently updating the Onboarding Wizard - Expect problems with starting the TUI until this work is complete!
|
|
2
|
+
|
|
1
3
|
# goodvibes-tui
|
|
2
4
|
|
|
3
5
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
6
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
8
|
|
|
7
9
|
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
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.29",
|
|
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",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
92
92
|
"@ast-grep/napi": "^0.42.0",
|
|
93
93
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
94
|
-
"@pellux/goodvibes-sdk": "^0.25.
|
|
94
|
+
"@pellux/goodvibes-sdk": "^0.25.4",
|
|
95
95
|
"bash-language-server": "^5.6.0",
|
|
96
96
|
"fuse.js": "^7.1.0",
|
|
97
97
|
"graphql": "^16.13.2",
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { ConfigKey } from '../config/index.ts';
|
|
2
|
+
import {
|
|
3
|
+
GOODVIBES_NTFY_AGENT_TOPIC,
|
|
4
|
+
GOODVIBES_NTFY_CHAT_TOPIC,
|
|
5
|
+
GOODVIBES_NTFY_REMOTE_TOPIC,
|
|
6
|
+
resolveGoodVibesNtfyTopics,
|
|
7
|
+
} from '@pellux/goodvibes-sdk/platform/integrations/ntfy';
|
|
8
|
+
import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags } from '../runtime/surface-feature-flags.ts';
|
|
2
9
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
3
10
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
4
11
|
import type { CliCommandRuntime } from './management.ts';
|
|
@@ -18,7 +25,7 @@ export const SURFACE_CONFIGS = [
|
|
|
18
25
|
['discord', 'Discord', ['surfaces.discord.publicKey', 'surfaces.discord.botToken', 'surfaces.discord.applicationId']],
|
|
19
26
|
['telegram', 'Telegram', ['surfaces.telegram.botToken']],
|
|
20
27
|
['webhook', 'Webhook', ['surfaces.webhook.secret']],
|
|
21
|
-
['ntfy', 'ntfy', ['surfaces.ntfy.baseUrl'
|
|
28
|
+
['ntfy', 'ntfy', ['surfaces.ntfy.baseUrl']],
|
|
22
29
|
['googleChat', 'Google Chat', ['surfaces.googleChat.webhookUrl']],
|
|
23
30
|
['signal', 'Signal', ['surfaces.signal.bridgeUrl', 'surfaces.signal.account']],
|
|
24
31
|
['whatsapp', 'WhatsApp', ['surfaces.whatsapp.accessToken', 'surfaces.whatsapp.phoneNumberId']],
|
|
@@ -39,6 +46,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
39
46
|
if (target === 'web') {
|
|
40
47
|
runtime.configManager.setDynamic('web.enabled', enabled);
|
|
41
48
|
if (enabled) {
|
|
49
|
+
enableFeatureFlags(runtime.configManager, getServerSurfaceFeatureFlags({ serverBacked: true, web: true }));
|
|
42
50
|
runtime.configManager.setDynamic('danger.daemon', true);
|
|
43
51
|
runtime.configManager.setDynamic('controlPlane.enabled', true);
|
|
44
52
|
const webError = applyTargetEndpointFlagsOrDefault(runtime, 'web');
|
|
@@ -71,6 +79,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
71
79
|
else if (SURFACE_CONFIGS.some(([id]) => id === target)) {
|
|
72
80
|
runtime.configManager.setDynamic(`surfaces.${target}.enabled` as ConfigKey, enabled);
|
|
73
81
|
if (enabled) {
|
|
82
|
+
enableFeatureFlags(runtime.configManager, getServerSurfaceFeatureFlags({ serverBacked: true, externalSurfaces: [target] }));
|
|
74
83
|
runtime.configManager.setDynamic('danger.httpListener', true);
|
|
75
84
|
enableEndpointLanDefault(runtime.configManager, 'httpListener');
|
|
76
85
|
}
|
|
@@ -88,34 +97,45 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
88
97
|
const web = resolveRuntimeEndpointBinding(config, 'web');
|
|
89
98
|
const httpListener = resolveRuntimeEndpointBinding(config, 'httpListener');
|
|
90
99
|
const includeProbe = sub === 'check';
|
|
100
|
+
const targetExternalSurface = target && SURFACE_CONFIGS.some(([id]) => id === target);
|
|
101
|
+
const shouldProbeControlPlane = includeProbe && !target;
|
|
102
|
+
const shouldProbeWeb = includeProbe && !target;
|
|
103
|
+
const shouldProbeListener = includeProbe && (!target || targetExternalSurface);
|
|
91
104
|
const [controlPlaneReachable, webReachable, listenerReachable] = includeProbe
|
|
92
105
|
? await Promise.all([
|
|
93
|
-
probeTcp(controlPlane.host, controlPlane.port),
|
|
94
|
-
probeTcp(web.host, web.port),
|
|
95
|
-
probeTcp(httpListener.host, httpListener.port),
|
|
106
|
+
shouldProbeControlPlane ? probeTcp(controlPlane.host, controlPlane.port) : Promise.resolve(undefined),
|
|
107
|
+
shouldProbeWeb ? probeTcp(web.host, web.port) : Promise.resolve(undefined),
|
|
108
|
+
shouldProbeListener ? probeTcp(httpListener.host, httpListener.port) : Promise.resolve(undefined),
|
|
96
109
|
])
|
|
97
110
|
: [undefined, undefined, undefined];
|
|
98
111
|
const externalSurfaces = SURFACE_CONFIGS.map(([id, label, requiredKeys]) => {
|
|
99
112
|
const enabled = config.get(`surfaces.${id}.enabled` as ConfigKey);
|
|
100
113
|
const missing = requiredKeys.filter((key) => !isPresentConfigValue(config.get(key as ConfigKey)));
|
|
114
|
+
const missingFeatureFlags = enabled === true ? getMissingSurfaceFeatureFlags(config, id) : [];
|
|
101
115
|
return {
|
|
102
116
|
id,
|
|
103
117
|
label,
|
|
104
118
|
enabled,
|
|
105
|
-
ready: !enabled || missing.length === 0,
|
|
119
|
+
ready: !enabled || (missing.length === 0 && missingFeatureFlags.length === 0),
|
|
106
120
|
missing,
|
|
121
|
+
missingFeatureFlags,
|
|
107
122
|
};
|
|
108
123
|
});
|
|
109
124
|
const filteredSurfaces = target ? externalSurfaces.filter((surface) => surface.id === target) : externalSurfaces;
|
|
110
125
|
if (target && filteredSurfaces.length === 0) return { output: `Unknown surface: ${target}`, exitCode: 1 };
|
|
126
|
+
const ntfyTopics = resolveGoodVibesNtfyTopics({
|
|
127
|
+
chatTopic: String(config.get('surfaces.ntfy.chatTopic' as ConfigKey) || GOODVIBES_NTFY_CHAT_TOPIC),
|
|
128
|
+
agentTopic: String(config.get('surfaces.ntfy.agentTopic' as ConfigKey) || GOODVIBES_NTFY_AGENT_TOPIC),
|
|
129
|
+
remoteTopic: String(config.get('surfaces.ntfy.remoteTopic' as ConfigKey) || GOODVIBES_NTFY_REMOTE_TOPIC),
|
|
130
|
+
});
|
|
111
131
|
const readinessIssues: string[] = [];
|
|
112
|
-
if (
|
|
132
|
+
if (shouldProbeControlPlane && config.get('controlPlane.enabled') === true && !controlPlaneReachable) {
|
|
113
133
|
readinessIssues.push(`Control plane is enabled but not reachable on ${controlPlane.host}:${controlPlane.port}.`);
|
|
114
134
|
}
|
|
115
|
-
if (
|
|
135
|
+
if (shouldProbeWeb && config.get('web.enabled') === true && !webReachable) {
|
|
116
136
|
readinessIssues.push(`Web surface is enabled but not reachable on ${web.host}:${web.port}.`);
|
|
117
137
|
}
|
|
118
|
-
if (
|
|
138
|
+
if (shouldProbeListener && config.get('danger.httpListener') === true && !listenerReachable) {
|
|
119
139
|
readinessIssues.push(`HTTP listener is enabled but not reachable on ${httpListener.host}:${httpListener.port}.`);
|
|
120
140
|
}
|
|
121
141
|
for (const surface of filteredSurfaces) {
|
|
@@ -126,6 +146,9 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
126
146
|
if (surface.missing.length > 0) {
|
|
127
147
|
readinessIssues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
|
|
128
148
|
}
|
|
149
|
+
if (surface.missingFeatureFlags.length > 0) {
|
|
150
|
+
readinessIssues.push(`${surface.label} is enabled but feature gates are disabled: ${surface.missingFeatureFlags.join(', ')}.`);
|
|
151
|
+
}
|
|
129
152
|
}
|
|
130
153
|
const value = {
|
|
131
154
|
controlPlane: {
|
|
@@ -162,7 +185,15 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
162
185
|
` http-listener: ${yesNo(value.httpListener.enabled)} (${value.httpListener.hostMode} ${value.httpListener.host}:${value.httpListener.port})${includeProbe ? ` reachable=${yesNo(value.httpListener.reachable)}` : ''}`,
|
|
163
186
|
'',
|
|
164
187
|
'External surfaces:',
|
|
165
|
-
...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}`),
|
|
188
|
+
...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? ` featureGates=${surface.missingFeatureFlags.join(',')}` : ''}`),
|
|
189
|
+
...(filteredSurfaces.some((surface) => surface.id === 'ntfy') ? [
|
|
190
|
+
'',
|
|
191
|
+
'ntfy inbound topics:',
|
|
192
|
+
` chat: ${ntfyTopics.chatTopic}`,
|
|
193
|
+
` agent: ${ntfyTopics.agentTopic}`,
|
|
194
|
+
` daemon-only remote: ${ntfyTopics.remoteTopic}`,
|
|
195
|
+
` default delivery topic: ${String(config.get('surfaces.ntfy.topic') || '(none)')}`,
|
|
196
|
+
] : []),
|
|
166
197
|
...(includeProbe ? [
|
|
167
198
|
readinessIssues.length === 0 ? 'Readiness: ready' : 'Readiness: needs attention',
|
|
168
199
|
...readinessIssues.map((issue) => ` - ${issue}`),
|
|
@@ -191,6 +222,7 @@ export interface ListenerTestResult {
|
|
|
191
222
|
readonly enabled: unknown;
|
|
192
223
|
readonly ready: boolean;
|
|
193
224
|
readonly missing: readonly string[];
|
|
225
|
+
readonly missingFeatureFlags: readonly string[];
|
|
194
226
|
}[];
|
|
195
227
|
readonly issues: readonly string[];
|
|
196
228
|
}
|
|
@@ -209,12 +241,14 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
|
|
|
209
241
|
const surfaces = SURFACE_CONFIGS.map(([id, label, requiredKeys]) => {
|
|
210
242
|
const surfaceEnabled = runtime.configManager.get(`surfaces.${id}.enabled` as ConfigKey);
|
|
211
243
|
const missing = requiredKeys.filter((key) => !isPresentConfigValue(runtime.configManager.get(key as ConfigKey)));
|
|
244
|
+
const missingFeatureFlags = surfaceEnabled === true ? getMissingSurfaceFeatureFlags(runtime.configManager, id) : [];
|
|
212
245
|
return {
|
|
213
246
|
id,
|
|
214
247
|
label,
|
|
215
248
|
enabled: surfaceEnabled,
|
|
216
|
-
ready: surfaceEnabled !== true || missing.length === 0,
|
|
249
|
+
ready: surfaceEnabled !== true || (missing.length === 0 && missingFeatureFlags.length === 0),
|
|
217
250
|
missing,
|
|
251
|
+
missingFeatureFlags,
|
|
218
252
|
};
|
|
219
253
|
}).filter((surface) => surface.enabled === true);
|
|
220
254
|
const issues: string[] = [];
|
|
@@ -226,6 +260,7 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
|
|
|
226
260
|
if (isNetworkFacing(enabled, binding) && auth.bootstrapCredentialPresent) issues.push('Network-facing listener still has a bootstrap credential file.');
|
|
227
261
|
for (const surface of surfaces) {
|
|
228
262
|
if (surface.missing.length > 0) issues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
|
|
263
|
+
if (surface.missingFeatureFlags.length > 0) issues.push(`${surface.label} is enabled but feature gates are disabled: ${surface.missingFeatureFlags.join(', ')}.`);
|
|
229
264
|
}
|
|
230
265
|
return { enabled, ...binding, posture, reachable, service, auth, surfaces, issues };
|
|
231
266
|
}
|
|
@@ -241,7 +276,7 @@ export function formatListenerTestResult(runtime: CliCommandRuntime, value: List
|
|
|
241
276
|
` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
|
|
242
277
|
` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
|
|
243
278
|
value.surfaces.length === 0 ? ' enabled webhook surfaces: none' : ' enabled webhook surfaces:',
|
|
244
|
-
...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}`),
|
|
279
|
+
...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.missingFeatureFlags.length > 0 ? ` featureGates=${surface.missingFeatureFlags.join(',')}` : ''}`),
|
|
245
280
|
value.issues.length === 0 ? ' readiness: ready' : ' readiness: needs attention',
|
|
246
281
|
...value.issues.map((issue) => ` - ${issue}`),
|
|
247
282
|
].join('\n'));
|
package/src/daemon/cli.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
|
|
5
5
|
import { RuntimeEventBus } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
|
|
6
|
+
import { createFeatureFlagManager } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/index';
|
|
7
|
+
import type { FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
|
|
6
8
|
import { createRuntimeStore } from '../runtime/store/index.ts';
|
|
7
9
|
import { createRuntimeServices } from '../runtime/services.ts';
|
|
8
10
|
import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon/server';
|
|
@@ -155,8 +157,13 @@ async function main(): Promise<void> {
|
|
|
155
157
|
}
|
|
156
158
|
const runtimeBus = new RuntimeEventBus();
|
|
157
159
|
const runtimeStore = createRuntimeStore();
|
|
160
|
+
const featureFlags = createFeatureFlagManager();
|
|
161
|
+
featureFlags.loadFromConfig({
|
|
162
|
+
flags: (config.getCategory('featureFlags') as Record<string, FlagState>) ?? {},
|
|
163
|
+
});
|
|
158
164
|
const runtimeServices = createRuntimeServices({
|
|
159
165
|
configManager: config,
|
|
166
|
+
featureFlags,
|
|
160
167
|
runtimeBus,
|
|
161
168
|
runtimeStore,
|
|
162
169
|
getConversationTitle: () => 'goodvibes daemon',
|
|
@@ -2,7 +2,7 @@ import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config/
|
|
|
2
2
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
|
|
3
3
|
import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils/open-external';
|
|
4
4
|
import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
5
|
-
import { OnboardingWizardController, type OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
|
|
5
|
+
import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
6
6
|
import { applyOnboardingRequest, collectOnboardingSnapshot, verifyOnboardingRequest } from '../runtime/onboarding/index.ts';
|
|
7
7
|
import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
|
|
8
8
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
@@ -19,6 +19,15 @@ export interface OnboardingRuntimePosture {
|
|
|
19
19
|
readonly remoteExposure: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
interface OnboardingExternalServiceState {
|
|
23
|
+
readonly daemonRunning?: boolean;
|
|
24
|
+
readonly daemonPortInUse?: boolean;
|
|
25
|
+
readonly httpListenerRunning?: boolean;
|
|
26
|
+
readonly httpListenerPortInUse?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type OnboardingRuntimeEndpoint = 'daemon' | 'httpListener';
|
|
30
|
+
|
|
22
31
|
function extractAuthorizationCode(input: string): string | null {
|
|
23
32
|
const trimmed = input.trim();
|
|
24
33
|
if (!trimmed) return null;
|
|
@@ -41,6 +50,101 @@ function isLoopbackHostValue(value: string | null | undefined): boolean {
|
|
|
41
50
|
|| /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
function onboardingVerificationStatusRank(item: OnboardingVerificationItem): number {
|
|
54
|
+
if (item.status === 'fail') return 3;
|
|
55
|
+
if (item.status === 'warn') return 2;
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dedupeOnboardingVerificationItems(
|
|
60
|
+
items: readonly OnboardingVerificationItem[],
|
|
61
|
+
): OnboardingVerificationItem[] {
|
|
62
|
+
const order: string[] = [];
|
|
63
|
+
const byId = new Map<string, OnboardingVerificationItem>();
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const existing = byId.get(item.id);
|
|
66
|
+
if (!existing) {
|
|
67
|
+
order.push(item.id);
|
|
68
|
+
byId.set(item.id, item);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (onboardingVerificationStatusRank(item) > onboardingVerificationStatusRank(existing)) {
|
|
72
|
+
byId.set(item.id, item);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return order.map((id) => byId.get(id)).filter((item): item is OnboardingVerificationItem => Boolean(item));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatOnboardingApplyCompletionMessage(items: readonly OnboardingVerificationItem[]): string {
|
|
79
|
+
const warnings = items.filter((item) => item.status === 'warn');
|
|
80
|
+
if (warnings.length === 0) return `Onboarding applied and verified ${items.length} item(s).`;
|
|
81
|
+
const passed = items.filter((item) => item.status === 'pass').length;
|
|
82
|
+
return [
|
|
83
|
+
`Onboarding settings applied. ${passed} verification item(s) passed; ${warnings.length} warning(s) need attention.`,
|
|
84
|
+
...warnings.map((warning) => ` warning ${warning.id}: ${warning.message}`),
|
|
85
|
+
].join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getRuntimeEndpointBinding(
|
|
89
|
+
handler: InputHandler,
|
|
90
|
+
request: OnboardingApplyRequest,
|
|
91
|
+
endpoint: OnboardingRuntimeEndpoint,
|
|
92
|
+
): { readonly label: string; readonly host: string; readonly port: number } {
|
|
93
|
+
const hostKey = endpoint === 'daemon' ? 'controlPlane.host' : 'httpListener.host';
|
|
94
|
+
const portKey = endpoint === 'daemon' ? 'controlPlane.port' : 'httpListener.port';
|
|
95
|
+
const fallbackHost = '127.0.0.1';
|
|
96
|
+
const fallbackPort = endpoint === 'daemon' ? 3421 : 3422;
|
|
97
|
+
const rawHost = handler.getOnboardingConfigValue(request, hostKey);
|
|
98
|
+
const rawPort = handler.getOnboardingConfigValue(request, portKey);
|
|
99
|
+
const parsedPort = typeof rawPort === 'number' ? rawPort : Number(rawPort);
|
|
100
|
+
return {
|
|
101
|
+
label: endpoint === 'daemon' ? 'GoodVibes daemon' : 'HTTP listener',
|
|
102
|
+
host: String(rawHost ?? fallbackHost),
|
|
103
|
+
port: Number.isFinite(parsedPort) ? parsedPort : fallbackPort,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function runtimePortDiagnostic(
|
|
108
|
+
binding: { readonly label: string; readonly host: string; readonly port: number },
|
|
109
|
+
portInUse: boolean | undefined,
|
|
110
|
+
): string {
|
|
111
|
+
if (portInUse) {
|
|
112
|
+
return `The configured port ${binding.host}:${binding.port} is occupied after restart; another GoodVibes process, an overlapping restart, or another service may still own it.`;
|
|
113
|
+
}
|
|
114
|
+
return `No process is listening on ${binding.host}:${binding.port} after restart.`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatRuntimeActiveFailureMessage(
|
|
118
|
+
handler: InputHandler,
|
|
119
|
+
request: OnboardingApplyRequest,
|
|
120
|
+
endpoint: OnboardingRuntimeEndpoint,
|
|
121
|
+
state: OnboardingExternalServiceState | undefined,
|
|
122
|
+
): string {
|
|
123
|
+
const binding = getRuntimeEndpointBinding(handler, request, endpoint);
|
|
124
|
+
const portInUse = endpoint === 'daemon' ? state?.daemonPortInUse : state?.httpListenerPortInUse;
|
|
125
|
+
const impact = endpoint === 'daemon'
|
|
126
|
+
? 'browser, LAN, and service-backed GoodVibes surfaces may be unavailable until the daemon is running there.'
|
|
127
|
+
: 'incoming webhooks and event surfaces will not receive traffic until the listener is running there.';
|
|
128
|
+
return `${binding.label} is enabled for ${binding.host}:${binding.port}, but onboarding could not confirm it is running in this TUI instance after restart. ${runtimePortDiagnostic(binding, portInUse)} Settings were saved; ${impact}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatRuntimeStoppedFailureMessage(
|
|
132
|
+
handler: InputHandler,
|
|
133
|
+
request: OnboardingApplyRequest,
|
|
134
|
+
endpoint: OnboardingRuntimeEndpoint,
|
|
135
|
+
): string {
|
|
136
|
+
const binding = getRuntimeEndpointBinding(handler, request, endpoint);
|
|
137
|
+
const disabledSurface = endpoint === 'daemon' ? 'server-backed surfaces' : 'incoming event surfaces';
|
|
138
|
+
return `${binding.label} was disabled for ${disabledSurface}, but ${binding.host}:${binding.port} is still occupied. Settings were saved; another GoodVibes process or external service may still be running on that port.`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function showOnboardingApplyFeedbackForHandler(handler: InputHandler, feedback: OnboardingWizardApplyFeedback): void {
|
|
142
|
+
handler.onboardingWizard.setApplyFeedback(feedback);
|
|
143
|
+
const reviewIndex = handler.onboardingWizard.steps.findIndex((step) => step.id === 'review');
|
|
144
|
+
if (reviewIndex >= 0) handler.onboardingWizard.setStep(reviewIndex);
|
|
145
|
+
handler.requestRender();
|
|
146
|
+
}
|
|
147
|
+
|
|
44
148
|
export function clearOnboardingPendingModelPickerTargetForHandler(handler: InputHandler): void {
|
|
45
149
|
handler.onboardingWizard.clearPendingModelPickerTarget();
|
|
46
150
|
}
|
|
@@ -115,15 +219,17 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
115
219
|
if (handler.onboardingApplyPending) return;
|
|
116
220
|
const blockers = handler.onboardingWizard.getBlockingFieldLabels();
|
|
117
221
|
if (blockers.length > 0) {
|
|
118
|
-
handler
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
222
|
+
showOnboardingApplyFeedbackForHandler(handler, {
|
|
223
|
+
severity: 'error',
|
|
224
|
+
title: 'Cannot apply yet',
|
|
225
|
+
summary: 'Fix these required or invalid fields, then apply again.',
|
|
226
|
+
messages: blockers,
|
|
227
|
+
});
|
|
123
228
|
return;
|
|
124
229
|
}
|
|
125
230
|
|
|
126
231
|
const request = handler.onboardingWizard.buildApplyRequest();
|
|
232
|
+
handler.onboardingWizard.clearApplyFeedback();
|
|
127
233
|
const deps = {
|
|
128
234
|
config: handler.uiServices.platform.configManager,
|
|
129
235
|
secrets: handler.uiServices.platform.secretsManager,
|
|
@@ -133,43 +239,48 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
133
239
|
};
|
|
134
240
|
let appliedErrors: string[] = [];
|
|
135
241
|
let verificationItems: readonly OnboardingVerificationItem[] = [];
|
|
242
|
+
let runtimeWarnings: readonly OnboardingVerificationItem[] = [];
|
|
136
243
|
handler.onboardingApplyPending = true;
|
|
137
244
|
try {
|
|
138
245
|
const applied = await applyOnboardingRequest(deps, request);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
|
|
246
|
+
if (applied.errors.length > 0) {
|
|
247
|
+
appliedErrors = applied.errors.map((error) => `apply ${error.kind}: ${error.message}`);
|
|
248
|
+
} else {
|
|
249
|
+
const verification = await verifyOnboardingRequest(deps, request);
|
|
250
|
+
verificationItems = verification.items;
|
|
251
|
+
appliedErrors = verification.items
|
|
252
|
+
.filter((item) => item.status === 'fail')
|
|
253
|
+
.map((item) => `verify ${item.id}: ${item.message}`);
|
|
254
|
+
}
|
|
147
255
|
|
|
148
256
|
if (appliedErrors.length === 0) {
|
|
149
257
|
const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
258
|
+
runtimeWarnings = dedupeOnboardingVerificationItems([...activationVerification, ...handler.verifyOnboardingRuntimePosture(request)]
|
|
259
|
+
.map((item): OnboardingVerificationItem => item.status === 'fail'
|
|
260
|
+
? { ...item, status: 'warn' }
|
|
261
|
+
: item));
|
|
262
|
+
verificationItems = dedupeOnboardingVerificationItems([...verificationItems, ...runtimeWarnings]);
|
|
155
263
|
}
|
|
156
264
|
} catch (error) {
|
|
157
|
-
handler
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
265
|
+
showOnboardingApplyFeedbackForHandler(handler, {
|
|
266
|
+
severity: 'error',
|
|
267
|
+
title: 'Apply failed',
|
|
268
|
+
summary: 'The wizard could not persist these settings. No service restart was attempted.',
|
|
269
|
+
messages: [error instanceof Error ? error.message : String(error)],
|
|
270
|
+
});
|
|
162
271
|
return;
|
|
163
272
|
} finally {
|
|
164
273
|
handler.onboardingApplyPending = false;
|
|
274
|
+
handler.requestRender();
|
|
165
275
|
}
|
|
166
276
|
|
|
167
277
|
if (appliedErrors.length > 0) {
|
|
168
|
-
handler
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
278
|
+
showOnboardingApplyFeedbackForHandler(handler, {
|
|
279
|
+
severity: 'error',
|
|
280
|
+
title: 'Apply did not complete',
|
|
281
|
+
summary: 'The settings were not fully applied. Review the messages below and try again.',
|
|
282
|
+
messages: appliedErrors,
|
|
283
|
+
});
|
|
173
284
|
return;
|
|
174
285
|
}
|
|
175
286
|
|
|
@@ -185,11 +296,7 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
185
296
|
handler.indicatorFocused = returnFocus === 'indicator';
|
|
186
297
|
handler.modalReturnFocus = 'prompt';
|
|
187
298
|
}
|
|
188
|
-
|
|
189
|
-
handler.commandContext?.print?.([
|
|
190
|
-
`Onboarding applied and verified ${verificationItems.length} item(s).`,
|
|
191
|
-
...warnings.map((warning) => ` warning ${warning.id}: ${warning.message}`),
|
|
192
|
-
].join('\n'));
|
|
299
|
+
handler.commandContext?.print?.(formatOnboardingApplyCompletionMessage(verificationItems));
|
|
193
300
|
handler.requestRender();
|
|
194
301
|
}
|
|
195
302
|
|
|
@@ -472,7 +579,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
472
579
|
failures.push({
|
|
473
580
|
id: 'runtime:daemon-active',
|
|
474
581
|
status: 'fail',
|
|
475
|
-
message:
|
|
582
|
+
message: formatRuntimeActiveFailureMessage(handler, request, 'daemon', state),
|
|
476
583
|
target: 'service',
|
|
477
584
|
});
|
|
478
585
|
}
|
|
@@ -480,7 +587,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
480
587
|
failures.push({
|
|
481
588
|
id: 'runtime:daemon-stopped',
|
|
482
589
|
status: 'fail',
|
|
483
|
-
message:
|
|
590
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
|
|
484
591
|
target: 'service',
|
|
485
592
|
});
|
|
486
593
|
}
|
|
@@ -488,7 +595,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
488
595
|
failures.push({
|
|
489
596
|
id: 'runtime:http-listener-active',
|
|
490
597
|
status: 'fail',
|
|
491
|
-
message:
|
|
598
|
+
message: formatRuntimeActiveFailureMessage(handler, request, 'httpListener', state),
|
|
492
599
|
target: 'service',
|
|
493
600
|
});
|
|
494
601
|
}
|
|
@@ -496,7 +603,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
496
603
|
failures.push({
|
|
497
604
|
id: 'runtime:http-listener-stopped',
|
|
498
605
|
status: 'fail',
|
|
499
|
-
message:
|
|
606
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
|
|
500
607
|
target: 'service',
|
|
501
608
|
});
|
|
502
609
|
}
|
|
@@ -537,7 +644,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
537
644
|
stoppedItems.push({
|
|
538
645
|
id: 'runtime:daemon-stopped',
|
|
539
646
|
status: 'fail',
|
|
540
|
-
message:
|
|
647
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
|
|
541
648
|
target: 'service',
|
|
542
649
|
});
|
|
543
650
|
}
|
|
@@ -545,7 +652,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
545
652
|
stoppedItems.push({
|
|
546
653
|
id: 'runtime:http-listener-stopped',
|
|
547
654
|
status: 'fail',
|
|
548
|
-
message:
|
|
655
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
|
|
549
656
|
target: 'service',
|
|
550
657
|
});
|
|
551
658
|
}
|
|
@@ -597,7 +704,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
597
704
|
status: externalState?.daemonRunning ? 'pass' : 'fail',
|
|
598
705
|
message: externalState?.daemonRunning
|
|
599
706
|
? 'The GoodVibes daemon is running with the applied onboarding settings.'
|
|
600
|
-
:
|
|
707
|
+
: formatRuntimeActiveFailureMessage(handler, request, 'daemon', externalState),
|
|
601
708
|
target: 'service',
|
|
602
709
|
});
|
|
603
710
|
}
|
|
@@ -605,7 +712,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
605
712
|
items.push({
|
|
606
713
|
id: 'runtime:daemon-stopped',
|
|
607
714
|
status: 'fail',
|
|
608
|
-
message:
|
|
715
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
|
|
609
716
|
target: 'service',
|
|
610
717
|
});
|
|
611
718
|
}
|
|
@@ -615,7 +722,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
615
722
|
status: externalState?.httpListenerRunning ? 'pass' : 'fail',
|
|
616
723
|
message: externalState?.httpListenerRunning
|
|
617
724
|
? 'The HTTP listener is running with the applied onboarding settings.'
|
|
618
|
-
:
|
|
725
|
+
: formatRuntimeActiveFailureMessage(handler, request, 'httpListener', externalState),
|
|
619
726
|
target: 'service',
|
|
620
727
|
});
|
|
621
728
|
}
|
|
@@ -623,7 +730,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
623
730
|
items.push({
|
|
624
731
|
id: 'runtime:http-listener-stopped',
|
|
625
732
|
status: 'fail',
|
|
626
|
-
message:
|
|
733
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
|
|
627
734
|
target: 'service',
|
|
628
735
|
});
|
|
629
736
|
}
|
|
@@ -64,6 +64,8 @@ export function handleOnboardingWizardToken(state: OnboardingRouteState, token:
|
|
|
64
64
|
if (editing) {
|
|
65
65
|
if (isEnterKey(token)) {
|
|
66
66
|
state.onboardingWizard.commitEdit();
|
|
67
|
+
} else if ((token.ctrl && token.logicalName === 'u') || token.logicalName === 'delete') {
|
|
68
|
+
state.onboardingWizard.clearEditingValue();
|
|
67
69
|
} else if (token.logicalName === 'backspace') {
|
|
68
70
|
state.onboardingWizard.editBackspace();
|
|
69
71
|
} else {
|
|
@@ -97,6 +99,8 @@ export function handleOnboardingWizardToken(state: OnboardingRouteState, token:
|
|
|
97
99
|
}
|
|
98
100
|
if (isEnterKey(token) || token.logicalName === 'space') {
|
|
99
101
|
activateSelection(state);
|
|
102
|
+
} else if ((token.ctrl && token.logicalName === 'u') || token.logicalName === 'delete') {
|
|
103
|
+
state.onboardingWizard.clearSelectedTextField();
|
|
100
104
|
} else if (token.logicalName === 'backspace') {
|
|
101
105
|
state.onboardingWizard.editBackspace();
|
|
102
106
|
}
|