@pellux/goodvibes-tui 0.19.27 → 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 +11 -0
- package/README.md +3 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +3 -2
- package/src/cli/entrypoint.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/status.ts +9 -9
- package/src/cli/surface-command.ts +46 -11
- package/src/cli/tui-startup.ts +4 -4
- package/src/daemon/cli.ts +7 -0
- package/src/input/handler-interactions.ts +14 -1
- package/src/input/handler-onboarding.ts +161 -118
- package/src/input/handler.ts +1 -1
- package/src/input/onboarding/handler-onboarding-routes.ts +35 -15
- package/src/input/onboarding/onboarding-wizard-apply.ts +35 -25
- 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 -3
- package/src/input/onboarding/onboarding-wizard-rules.ts +40 -8
- package/src/input/onboarding/onboarding-wizard-state.ts +19 -8
- package/src/input/onboarding/onboarding-wizard-steps.ts +226 -93
- package/src/input/onboarding/onboarding-wizard-types.ts +15 -0
- package/src/input/onboarding/onboarding-wizard.ts +123 -6
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +4 -0
- package/src/main.ts +35 -27
- package/src/renderer/compositor.ts +3 -3
- package/src/renderer/onboarding/onboarding-wizard.ts +141 -57
- 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 +45 -90
- package/src/runtime/onboarding/derivation.ts +7 -7
- package/src/runtime/onboarding/markers.ts +41 -55
- package/src/runtime/onboarding/snapshot.ts +1 -0
- package/src/runtime/onboarding/state.ts +6 -6
- package/src/runtime/onboarding/types.ts +24 -27
- package/src/runtime/onboarding/verify.ts +3 -65
- package/src/runtime/surface-feature-flags.ts +67 -0
- package/src/version.ts +1 -1
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
1
|
import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config/oauth-local-listener';
|
|
4
2
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
|
|
5
3
|
import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils/open-external';
|
|
6
4
|
import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
7
|
-
import { OnboardingWizardController, type OnboardingWizardAction } from './onboarding/onboarding-wizard.ts';
|
|
8
|
-
import { applyOnboardingRequest, collectOnboardingSnapshot,
|
|
9
|
-
import type {
|
|
5
|
+
import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardApplyFeedback } from './onboarding/onboarding-wizard.ts';
|
|
6
|
+
import { applyOnboardingRequest, collectOnboardingSnapshot, verifyOnboardingRequest } from '../runtime/onboarding/index.ts';
|
|
7
|
+
import type { OnboardingApplyRequest, OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
|
|
10
8
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
11
9
|
import { captureOnboardingWizardSnapshot, restoreOnboardingWizardSnapshot } from './handler-ui-state.ts';
|
|
12
10
|
import type { InputHandler } from './handler.ts';
|
|
13
11
|
|
|
14
|
-
interface CompletionMarkerSnapshot {
|
|
15
|
-
readonly path: string;
|
|
16
|
-
readonly previous: string | null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
export interface OnboardingRuntimePosture {
|
|
20
13
|
readonly serviceEnabled: boolean;
|
|
21
14
|
readonly serviceAutostart: boolean;
|
|
@@ -26,6 +19,15 @@ export interface OnboardingRuntimePosture {
|
|
|
26
19
|
readonly remoteExposure: boolean;
|
|
27
20
|
}
|
|
28
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
|
+
|
|
29
31
|
function extractAuthorizationCode(input: string): string | null {
|
|
30
32
|
const trimmed = input.trim();
|
|
31
33
|
if (!trimmed) return null;
|
|
@@ -38,54 +40,111 @@ function extractAuthorizationCode(input: string): string | null {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
markerRequest: { ...request, operations: markerOperations },
|
|
50
|
-
};
|
|
43
|
+
function isLoopbackHostValue(value: string | null | undefined): boolean {
|
|
44
|
+
const normalized = (value ?? '').trim().toLowerCase();
|
|
45
|
+
if (normalized.length === 0) return false;
|
|
46
|
+
return normalized === 'localhost'
|
|
47
|
+
|| normalized === '::1'
|
|
48
|
+
|| normalized === '[::1]'
|
|
49
|
+
|| normalized === '0:0:0:0:0:0:0:1'
|
|
50
|
+
|| /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return operations
|
|
58
|
-
.filter((operation) => operation.kind === 'set-completion-marker')
|
|
59
|
-
.map((operation) => {
|
|
60
|
-
const path = getOnboardingCompletionMarkerPath(shellPaths, operation.scope);
|
|
61
|
-
return {
|
|
62
|
-
path,
|
|
63
|
-
previous: existsSync(path) ? readFileSync(path, 'utf-8') : null,
|
|
64
|
-
};
|
|
65
|
-
});
|
|
53
|
+
function onboardingVerificationStatusRank(item: OnboardingVerificationItem): number {
|
|
54
|
+
if (item.status === 'fail') return 3;
|
|
55
|
+
if (item.status === 'warn') return 2;
|
|
56
|
+
return 1;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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);
|
|
72
69
|
continue;
|
|
73
70
|
}
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
if (onboardingVerificationStatusRank(item) > onboardingVerificationStatusRank(existing)) {
|
|
72
|
+
byId.set(item.id, item);
|
|
73
|
+
}
|
|
76
74
|
}
|
|
75
|
+
return order.map((id) => byId.get(id)).filter((item): item is OnboardingVerificationItem => Boolean(item));
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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');
|
|
87
86
|
}
|
|
88
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
|
+
|
|
89
148
|
export function clearOnboardingPendingModelPickerTargetForHandler(handler: InputHandler): void {
|
|
90
149
|
handler.onboardingWizard.clearPendingModelPickerTarget();
|
|
91
150
|
}
|
|
@@ -160,16 +219,17 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
160
219
|
if (handler.onboardingApplyPending) return;
|
|
161
220
|
const blockers = handler.onboardingWizard.getBlockingFieldLabels();
|
|
162
221
|
if (blockers.length > 0) {
|
|
163
|
-
handler
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
});
|
|
168
228
|
return;
|
|
169
229
|
}
|
|
170
230
|
|
|
171
231
|
const request = handler.onboardingWizard.buildApplyRequest();
|
|
172
|
-
|
|
232
|
+
handler.onboardingWizard.clearApplyFeedback();
|
|
173
233
|
const deps = {
|
|
174
234
|
config: handler.uiServices.platform.configManager,
|
|
175
235
|
secrets: handler.uiServices.platform.secretsManager,
|
|
@@ -179,61 +239,48 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
179
239
|
};
|
|
180
240
|
let appliedErrors: string[] = [];
|
|
181
241
|
let verificationItems: readonly OnboardingVerificationItem[] = [];
|
|
242
|
+
let runtimeWarnings: readonly OnboardingVerificationItem[] = [];
|
|
182
243
|
handler.onboardingApplyPending = true;
|
|
183
244
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
.map((item) => `verify ${item.id}: ${item.message}`),
|
|
192
|
-
];
|
|
193
|
-
|
|
194
|
-
if (appliedErrors.length === 0) {
|
|
195
|
-
const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
|
|
196
|
-
const runtimeVerification = [...activationVerification, ...handler.verifyOnboardingRuntimePosture(request)];
|
|
197
|
-
verificationItems = [...settingsVerification.items, ...runtimeVerification];
|
|
198
|
-
appliedErrors = runtimeVerification
|
|
245
|
+
const applied = await applyOnboardingRequest(deps, request);
|
|
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
|
|
199
252
|
.filter((item) => item.status === 'fail')
|
|
200
253
|
.map((item) => `verify ${item.id}: ${item.message}`);
|
|
201
254
|
}
|
|
202
255
|
|
|
203
|
-
if (appliedErrors.length === 0
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
...markerApplied.errors.map((error) => `apply ${error.kind}: ${error.message}`),
|
|
211
|
-
...finalVerification.items
|
|
212
|
-
.filter((item) => item.status !== 'pass')
|
|
213
|
-
.map((item) => `verify ${item.id}: ${item.message}`),
|
|
214
|
-
...runtimeVerification
|
|
215
|
-
.filter((item) => item.status === 'fail')
|
|
216
|
-
.map((item) => `verify ${item.id}: ${item.message}`),
|
|
217
|
-
];
|
|
218
|
-
if (appliedErrors.length > 0) restoreCompletionMarkers(markerSnapshots);
|
|
256
|
+
if (appliedErrors.length === 0) {
|
|
257
|
+
const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
|
|
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]);
|
|
219
263
|
}
|
|
220
264
|
} catch (error) {
|
|
221
|
-
handler
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
});
|
|
226
271
|
return;
|
|
227
272
|
} finally {
|
|
228
273
|
handler.onboardingApplyPending = false;
|
|
274
|
+
handler.requestRender();
|
|
229
275
|
}
|
|
230
276
|
|
|
231
277
|
if (appliedErrors.length > 0) {
|
|
232
|
-
handler
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
});
|
|
237
284
|
return;
|
|
238
285
|
}
|
|
239
286
|
|
|
@@ -249,11 +296,7 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
|
|
|
249
296
|
handler.indicatorFocused = returnFocus === 'indicator';
|
|
250
297
|
handler.modalReturnFocus = 'prompt';
|
|
251
298
|
}
|
|
252
|
-
|
|
253
|
-
handler.commandContext?.print?.([
|
|
254
|
-
`Onboarding applied and verified ${verificationItems.length} item(s).`,
|
|
255
|
-
...warnings.map((warning) => ` warning ${warning.id}: ${warning.message}`),
|
|
256
|
-
].join('\n'));
|
|
299
|
+
handler.commandContext?.print?.(formatOnboardingApplyCompletionMessage(verificationItems));
|
|
257
300
|
handler.requestRender();
|
|
258
301
|
}
|
|
259
302
|
|
|
@@ -536,7 +579,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
536
579
|
failures.push({
|
|
537
580
|
id: 'runtime:daemon-active',
|
|
538
581
|
status: 'fail',
|
|
539
|
-
message:
|
|
582
|
+
message: formatRuntimeActiveFailureMessage(handler, request, 'daemon', state),
|
|
540
583
|
target: 'service',
|
|
541
584
|
});
|
|
542
585
|
}
|
|
@@ -544,7 +587,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
544
587
|
failures.push({
|
|
545
588
|
id: 'runtime:daemon-stopped',
|
|
546
589
|
status: 'fail',
|
|
547
|
-
message:
|
|
590
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
|
|
548
591
|
target: 'service',
|
|
549
592
|
});
|
|
550
593
|
}
|
|
@@ -552,7 +595,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
552
595
|
failures.push({
|
|
553
596
|
id: 'runtime:http-listener-active',
|
|
554
597
|
status: 'fail',
|
|
555
|
-
message:
|
|
598
|
+
message: formatRuntimeActiveFailureMessage(handler, request, 'httpListener', state),
|
|
556
599
|
target: 'service',
|
|
557
600
|
});
|
|
558
601
|
}
|
|
@@ -560,7 +603,7 @@ export async function restartOnboardingExternalServicesIfNeededForHandler(handle
|
|
|
560
603
|
failures.push({
|
|
561
604
|
id: 'runtime:http-listener-stopped',
|
|
562
605
|
status: 'fail',
|
|
563
|
-
message:
|
|
606
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
|
|
564
607
|
target: 'service',
|
|
565
608
|
});
|
|
566
609
|
}
|
|
@@ -601,7 +644,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
601
644
|
stoppedItems.push({
|
|
602
645
|
id: 'runtime:daemon-stopped',
|
|
603
646
|
status: 'fail',
|
|
604
|
-
message:
|
|
647
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
|
|
605
648
|
target: 'service',
|
|
606
649
|
});
|
|
607
650
|
}
|
|
@@ -609,7 +652,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
609
652
|
stoppedItems.push({
|
|
610
653
|
id: 'runtime:http-listener-stopped',
|
|
611
654
|
status: 'fail',
|
|
612
|
-
message:
|
|
655
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
|
|
613
656
|
target: 'service',
|
|
614
657
|
});
|
|
615
658
|
}
|
|
@@ -625,7 +668,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
625
668
|
}
|
|
626
669
|
|
|
627
670
|
const auth = handler.uiServices.platform.localUserAuthManager.inspect();
|
|
628
|
-
const
|
|
671
|
+
const hasLocalAuth = auth.users.length > 0;
|
|
629
672
|
const items: OnboardingVerificationItem[] = [];
|
|
630
673
|
|
|
631
674
|
items.push({
|
|
@@ -638,19 +681,19 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
638
681
|
});
|
|
639
682
|
items.push({
|
|
640
683
|
id: 'runtime:auth-posture',
|
|
641
|
-
status:
|
|
642
|
-
message:
|
|
643
|
-
? 'Local
|
|
644
|
-
: 'Network-capable surfaces require local
|
|
684
|
+
status: hasLocalAuth && !auth.bootstrapCredentialPresent ? 'pass' : 'fail',
|
|
685
|
+
message: hasLocalAuth && !auth.bootstrapCredentialPresent
|
|
686
|
+
? 'Local auth is configured and bootstrap credentials are not present.'
|
|
687
|
+
: 'Network-capable surfaces require local auth with no bootstrap credential file.',
|
|
645
688
|
target: 'auth',
|
|
646
689
|
});
|
|
647
690
|
if (posture.remoteExposure) {
|
|
648
691
|
items.push({
|
|
649
692
|
id: 'runtime:remote-auth-gate',
|
|
650
|
-
status:
|
|
651
|
-
message:
|
|
652
|
-
? 'Remote-capable bind settings have local
|
|
653
|
-
: 'Remote-capable bind settings cannot be applied without local
|
|
693
|
+
status: hasLocalAuth ? 'pass' : 'fail',
|
|
694
|
+
message: hasLocalAuth
|
|
695
|
+
? 'Remote-capable bind settings have local auth available.'
|
|
696
|
+
: 'Remote-capable bind settings cannot be applied without local auth.',
|
|
654
697
|
target: 'auth',
|
|
655
698
|
});
|
|
656
699
|
}
|
|
@@ -661,7 +704,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
661
704
|
status: externalState?.daemonRunning ? 'pass' : 'fail',
|
|
662
705
|
message: externalState?.daemonRunning
|
|
663
706
|
? 'The GoodVibes daemon is running with the applied onboarding settings.'
|
|
664
|
-
:
|
|
707
|
+
: formatRuntimeActiveFailureMessage(handler, request, 'daemon', externalState),
|
|
665
708
|
target: 'service',
|
|
666
709
|
});
|
|
667
710
|
}
|
|
@@ -669,7 +712,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
669
712
|
items.push({
|
|
670
713
|
id: 'runtime:daemon-stopped',
|
|
671
714
|
status: 'fail',
|
|
672
|
-
message:
|
|
715
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'daemon'),
|
|
673
716
|
target: 'service',
|
|
674
717
|
});
|
|
675
718
|
}
|
|
@@ -679,7 +722,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
679
722
|
status: externalState?.httpListenerRunning ? 'pass' : 'fail',
|
|
680
723
|
message: externalState?.httpListenerRunning
|
|
681
724
|
? 'The HTTP listener is running with the applied onboarding settings.'
|
|
682
|
-
:
|
|
725
|
+
: formatRuntimeActiveFailureMessage(handler, request, 'httpListener', externalState),
|
|
683
726
|
target: 'service',
|
|
684
727
|
});
|
|
685
728
|
}
|
|
@@ -687,7 +730,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
|
|
|
687
730
|
items.push({
|
|
688
731
|
id: 'runtime:http-listener-stopped',
|
|
689
732
|
status: 'fail',
|
|
690
|
-
message:
|
|
733
|
+
message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
|
|
691
734
|
target: 'service',
|
|
692
735
|
});
|
|
693
736
|
}
|
package/src/input/handler.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { OnboardingWizardController, type OnboardingWizardAction, type Onboardin
|
|
|
31
31
|
import {
|
|
32
32
|
applyOnboardingRequest,
|
|
33
33
|
collectOnboardingSnapshot,
|
|
34
|
-
|
|
34
|
+
getOnboardingCheckMarkerPath,
|
|
35
35
|
verifyOnboardingRequest,
|
|
36
36
|
} from '../runtime/onboarding/index.ts';
|
|
37
37
|
import type {
|
|
@@ -28,6 +28,20 @@ function activateSelection(state: OnboardingRouteState): void {
|
|
|
28
28
|
if (action !== null) state.onAction?.(action);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function isEnterKey(token: InputToken): boolean {
|
|
32
|
+
return token.type === 'key' && (token.logicalName === 'enter' || token.logicalName === 'return');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getKeyTextInput(token: Extract<InputToken, { type: 'key' }>): string | null {
|
|
36
|
+
if (token.ctrl || token.meta) return null;
|
|
37
|
+
if (token.logicalName === 'space') return ' ';
|
|
38
|
+
if (token.logicalName.length !== 1) return null;
|
|
39
|
+
if (token.shift && token.logicalName >= 'a' && token.logicalName <= 'z') {
|
|
40
|
+
return token.logicalName.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
return token.logicalName;
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
export function handleOnboardingWizardToken(state: OnboardingRouteState, token: InputToken): boolean {
|
|
32
46
|
if (!state.onboardingWizard.active) return false;
|
|
33
47
|
|
|
@@ -48,12 +62,15 @@ export function handleOnboardingWizardToken(state: OnboardingRouteState, token:
|
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
if (editing) {
|
|
51
|
-
if (token
|
|
65
|
+
if (isEnterKey(token)) {
|
|
52
66
|
state.onboardingWizard.commitEdit();
|
|
67
|
+
} else if ((token.ctrl && token.logicalName === 'u') || token.logicalName === 'delete') {
|
|
68
|
+
state.onboardingWizard.clearEditingValue();
|
|
53
69
|
} else if (token.logicalName === 'backspace') {
|
|
54
70
|
state.onboardingWizard.editBackspace();
|
|
55
|
-
} else
|
|
56
|
-
|
|
71
|
+
} else {
|
|
72
|
+
const textInput = getKeyTextInput(token);
|
|
73
|
+
if (textInput !== null) state.onboardingWizard.editChar(textInput);
|
|
57
74
|
}
|
|
58
75
|
} else if (token.logicalName === 'left') {
|
|
59
76
|
state.onboardingWizard.prevStep();
|
|
@@ -74,22 +91,25 @@ export function handleOnboardingWizardToken(state: OnboardingRouteState, token:
|
|
|
74
91
|
state.onboardingWizard.selectFirst(visibleFields);
|
|
75
92
|
} else if (token.logicalName === 'end') {
|
|
76
93
|
state.onboardingWizard.selectLast(visibleFields);
|
|
77
|
-
} else
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
} else {
|
|
95
|
+
const textInput = getKeyTextInput(token);
|
|
96
|
+
if (textInput !== null && state.onboardingWizard.beginSelectedTextInput(textInput)) {
|
|
97
|
+
state.requestRender();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (isEnterKey(token) || token.logicalName === 'space') {
|
|
101
|
+
activateSelection(state);
|
|
102
|
+
} else if ((token.ctrl && token.logicalName === 'u') || token.logicalName === 'delete') {
|
|
103
|
+
state.onboardingWizard.clearSelectedTextField();
|
|
104
|
+
} else if (token.logicalName === 'backspace') {
|
|
105
|
+
state.onboardingWizard.editBackspace();
|
|
106
|
+
}
|
|
81
107
|
}
|
|
82
108
|
} else if (token.type === 'text') {
|
|
83
109
|
if (editing) {
|
|
84
110
|
state.onboardingWizard.editChar(token.value);
|
|
85
|
-
} else if (token.value
|
|
86
|
-
|
|
87
|
-
} else if (token.value === 'l') {
|
|
88
|
-
state.onboardingWizard.nextStep();
|
|
89
|
-
} else if (token.value === 'k') {
|
|
90
|
-
state.onboardingWizard.moveSelection(-1, visibleFields);
|
|
91
|
-
} else if (token.value === 'j') {
|
|
92
|
-
state.onboardingWizard.moveSelection(1, visibleFields);
|
|
111
|
+
} else if (state.onboardingWizard.beginSelectedTextInput(token.value)) {
|
|
112
|
+
// Direct typing into selected inputs behaves like a real form field.
|
|
93
113
|
} else if (token.value === ' ') {
|
|
94
114
|
activateSelection(state);
|
|
95
115
|
} else if (/^[1-9]$/.test(token.value)) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { OnboardingAcknowledgementTarget, OnboardingApplyOperation, OnboardingApplyRequest } from '../../runtime/onboarding/index.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { getServerSurfaceFeatureFlags } from '../../runtime/surface-feature-flags.ts';
|
|
3
|
+
import {
|
|
4
|
+
EXTERNAL_SURFACE_SPECS,
|
|
5
|
+
getExternalSurfaceAutoStartDefaultValue,
|
|
6
|
+
getExternalSurfaceAutoStartFieldId,
|
|
7
|
+
isExternalSurfaceSelectedByDefault,
|
|
8
|
+
} from './onboarding-wizard-external-surfaces.ts';
|
|
3
9
|
import { buildGoodVibesSecretKey, buildGoodVibesSecretRef, isLoopbackAddress, isSecretReferenceValue } from './onboarding-wizard-helpers.ts';
|
|
4
10
|
import type { OnboardingWizardController } from './onboarding-wizard.ts';
|
|
5
11
|
|
|
@@ -8,6 +14,7 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
8
14
|
const hasServers = controller.hasServerCapabilitiesSelected();
|
|
9
15
|
const browserAccess = controller.shouldEnableBrowserSurface();
|
|
10
16
|
const httpListener = controller.shouldEnableHttpListener();
|
|
17
|
+
const httpListenerNetworkFields = controller.shouldExposeHttpListenerNetworkFields();
|
|
11
18
|
const controlPlaneRemote = controller.shouldExposeControlPlaneNetwork();
|
|
12
19
|
const networkMode = controller.getStringFieldValue('network.mode', controller.runtimeDerived.step1_5NetworkMode);
|
|
13
20
|
const customNetwork = hasServers && networkMode === 'custom';
|
|
@@ -18,6 +25,9 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
18
25
|
): void => {
|
|
19
26
|
operations.push({ kind: 'set-config', key, value });
|
|
20
27
|
};
|
|
28
|
+
const enableFeatureFlags = (flagIds: readonly string[]): void => {
|
|
29
|
+
for (const flagId of flagIds) setConfig(`featureFlags.${flagId}`, 'enabled');
|
|
30
|
+
};
|
|
21
31
|
const acknowledge = (target: OnboardingAcknowledgementTarget, fieldId: string): void => {
|
|
22
32
|
operations.push({
|
|
23
33
|
kind: 'acknowledge',
|
|
@@ -50,11 +60,13 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
50
60
|
setConfig(key, buildGoodVibesSecretRef(secretKey));
|
|
51
61
|
};
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
const requestedAdminPassword = controller.getStringFieldValue('accounts.admin-password', '');
|
|
64
|
+
const shouldEnsureAuthUser = controller.requiresAuthBootstrap() || requestedAdminPassword.length > 0;
|
|
65
|
+
if (shouldEnsureAuthUser) {
|
|
54
66
|
operations.push({
|
|
55
67
|
kind: 'ensure-auth-user',
|
|
56
68
|
username: controller.getStringFieldValue('accounts.admin-username', controller.getDefaultAdminUsername()),
|
|
57
|
-
password:
|
|
69
|
+
password: requestedAdminPassword,
|
|
58
70
|
roles: ['admin'],
|
|
59
71
|
createSession: true,
|
|
60
72
|
retireBootstrapCredential: controller.hasBootstrapCredentialPresent(),
|
|
@@ -73,7 +85,7 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
73
85
|
addNetworkOperations(controller, operations, customNetwork, {
|
|
74
86
|
controlPlane: hasServers,
|
|
75
87
|
controlPlaneRemote,
|
|
76
|
-
httpListener,
|
|
88
|
+
httpListener: httpListenerNetworkFields,
|
|
77
89
|
web: browserAccess,
|
|
78
90
|
});
|
|
79
91
|
} else {
|
|
@@ -100,11 +112,21 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
100
112
|
setSecret('OPENAI_API_KEY', controller.getStringFieldValue('providers.openai-api-key', ''));
|
|
101
113
|
|
|
102
114
|
const externalIntegrations = controller.isCapabilitySelected('external-integrations');
|
|
115
|
+
const enabledExternalSurfaceIds: string[] = [];
|
|
103
116
|
for (const surface of EXTERNAL_SURFACE_SPECS) {
|
|
104
|
-
const
|
|
105
|
-
&& controller.getBooleanFieldValue(
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
const selected = externalIntegrations
|
|
118
|
+
&& controller.getBooleanFieldValue(
|
|
119
|
+
surface.enabledFieldId,
|
|
120
|
+
isExternalSurfaceSelectedByDefault(surface, controller.runtimeSnapshot),
|
|
121
|
+
);
|
|
122
|
+
const autoStart = selected
|
|
123
|
+
&& controller.getStringFieldValue(
|
|
124
|
+
getExternalSurfaceAutoStartFieldId(surface),
|
|
125
|
+
getExternalSurfaceAutoStartDefaultValue(surface, controller.runtimeSnapshot),
|
|
126
|
+
) === 'yes';
|
|
127
|
+
setConfig(surface.enabledConfigKey, autoStart);
|
|
128
|
+
if (!selected) continue;
|
|
129
|
+
enabledExternalSurfaceIds.push(surface.id);
|
|
108
130
|
|
|
109
131
|
for (const setupField of surface.fields) {
|
|
110
132
|
const fallback = setupField.defaultValue(controller.runtimeSnapshot);
|
|
@@ -126,28 +148,16 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
126
148
|
else setConfig(setupField.configKey, value);
|
|
127
149
|
}
|
|
128
150
|
}
|
|
151
|
+
enableFeatureFlags(getServerSurfaceFeatureFlags({
|
|
152
|
+
serverBacked: hasServers,
|
|
153
|
+
web: browserAccess,
|
|
154
|
+
externalSurfaces: enabledExternalSurfaceIds,
|
|
155
|
+
}));
|
|
129
156
|
|
|
130
157
|
acknowledge('providers', 'providers.reviewed');
|
|
131
158
|
acknowledge('subscriptions', 'accounts.subscriptions');
|
|
132
159
|
acknowledge('auth', 'accounts.auth');
|
|
133
160
|
|
|
134
|
-
if (controller.getBooleanFieldValue('review.project-marker', true)) {
|
|
135
|
-
operations.push({
|
|
136
|
-
kind: 'set-completion-marker',
|
|
137
|
-
scope: 'project',
|
|
138
|
-
completed: true,
|
|
139
|
-
payload: { source: 'wizard', mode: controller.mode },
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
if (controller.getBooleanFieldValue('review.user-marker', controller.defaultReviewUserMarker())) {
|
|
143
|
-
operations.push({
|
|
144
|
-
kind: 'set-completion-marker',
|
|
145
|
-
scope: 'user',
|
|
146
|
-
completed: true,
|
|
147
|
-
payload: { source: 'wizard', mode: controller.mode },
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
161
|
return {
|
|
152
162
|
mode: controller.mode,
|
|
153
163
|
source: 'wizard',
|