@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +3 -2
  6. package/src/cli/entrypoint.ts +2 -2
  7. package/src/cli/help.ts +1 -1
  8. package/src/cli/status.ts +9 -9
  9. package/src/cli/surface-command.ts +46 -11
  10. package/src/cli/tui-startup.ts +4 -4
  11. package/src/daemon/cli.ts +7 -0
  12. package/src/input/handler-interactions.ts +14 -1
  13. package/src/input/handler-onboarding.ts +161 -118
  14. package/src/input/handler.ts +1 -1
  15. package/src/input/onboarding/handler-onboarding-routes.ts +35 -15
  16. package/src/input/onboarding/onboarding-wizard-apply.ts +35 -25
  17. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -5
  18. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +93 -5
  19. package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -3
  20. package/src/input/onboarding/onboarding-wizard-rules.ts +40 -8
  21. package/src/input/onboarding/onboarding-wizard-state.ts +19 -8
  22. package/src/input/onboarding/onboarding-wizard-steps.ts +226 -93
  23. package/src/input/onboarding/onboarding-wizard-types.ts +15 -0
  24. package/src/input/onboarding/onboarding-wizard.ts +123 -6
  25. package/src/input/settings-modal-types.ts +2 -1
  26. package/src/input/settings-modal.ts +4 -0
  27. package/src/main.ts +35 -27
  28. package/src/renderer/compositor.ts +3 -3
  29. package/src/renderer/onboarding/onboarding-wizard.ts +141 -57
  30. package/src/renderer/settings-modal-helpers.ts +9 -0
  31. package/src/renderer/settings-modal.ts +3 -0
  32. package/src/runtime/bootstrap.ts +15 -0
  33. package/src/runtime/onboarding/apply.ts +45 -90
  34. package/src/runtime/onboarding/derivation.ts +7 -7
  35. package/src/runtime/onboarding/markers.ts +41 -55
  36. package/src/runtime/onboarding/snapshot.ts +1 -0
  37. package/src/runtime/onboarding/state.ts +6 -6
  38. package/src/runtime/onboarding/types.ts +24 -27
  39. package/src/runtime/onboarding/verify.ts +3 -65
  40. package/src/runtime/surface-feature-flags.ts +67 -0
  41. 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, getOnboardingCompletionMarkerPath, verifyOnboardingRequest } from '../runtime/onboarding/index.ts';
9
- import type { OnboardingApplyOperation, OnboardingApplyRequest, OnboardingShellPaths, OnboardingVerificationItem } from '../runtime/onboarding/index.ts';
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 splitCompletionMarkerOperations(request: OnboardingApplyRequest): {
42
- readonly settingsRequest: OnboardingApplyRequest;
43
- readonly markerRequest: OnboardingApplyRequest;
44
- } {
45
- const markerOperations = request.operations.filter((operation) => operation.kind === 'set-completion-marker');
46
- const settingsOperations = request.operations.filter((operation) => operation.kind !== 'set-completion-marker');
47
- return {
48
- settingsRequest: { ...request, operations: settingsOperations },
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 snapshotCompletionMarkers(
54
- shellPaths: OnboardingShellPaths,
55
- operations: readonly OnboardingApplyOperation[],
56
- ): readonly CompletionMarkerSnapshot[] {
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 restoreCompletionMarkers(snapshots: readonly CompletionMarkerSnapshot[]): void {
69
- for (const snapshot of snapshots) {
70
- if (snapshot.previous === null) {
71
- rmSync(snapshot.path, { force: true });
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
- mkdirSync(dirname(snapshot.path), { recursive: true });
75
- writeFileSync(snapshot.path, snapshot.previous, 'utf-8');
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 isLoopbackHostValue(value: string | null | undefined): boolean {
80
- const normalized = (value ?? '').trim().toLowerCase();
81
- if (normalized.length === 0) return false;
82
- return normalized === 'localhost'
83
- || normalized === '::1'
84
- || normalized === '[::1]'
85
- || normalized === '0:0:0:0:0:0:0:1'
86
- || /^127(?:\.\d{1,3}){3}$/.test(normalized);
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.commandContext?.print?.([
164
- 'Onboarding needs required confirmations before applying.',
165
- ...blockers.map((label) => ` ${label}`),
166
- ].join('\n'));
167
- handler.requestRender();
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
- const { settingsRequest, markerRequest } = splitCompletionMarkerOperations(request);
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 settingsApplied = await applyOnboardingRequest(deps, settingsRequest);
185
- const settingsVerification = await verifyOnboardingRequest(deps, settingsRequest);
186
- verificationItems = settingsVerification.items;
187
- appliedErrors = [
188
- ...settingsApplied.errors.map((error) => `apply ${error.kind}: ${error.message}`),
189
- ...settingsVerification.items
190
- .filter((item) => item.status !== 'pass')
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 && markerRequest.operations.length > 0) {
204
- const markerSnapshots = snapshotCompletionMarkers(deps.shellPaths, markerRequest.operations);
205
- const markerApplied = await applyOnboardingRequest(deps, markerRequest);
206
- const finalVerification = await verifyOnboardingRequest(deps, request);
207
- const runtimeVerification = handler.verifyOnboardingRuntimePosture(request);
208
- verificationItems = [...finalVerification.items, ...runtimeVerification];
209
- appliedErrors = [
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.commandContext?.print?.([
222
- 'Onboarding apply did not complete.',
223
- ` ${error instanceof Error ? error.message : String(error)}`,
224
- ].join('\n'));
225
- handler.requestRender();
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.commandContext?.print?.([
233
- 'Onboarding apply did not complete.',
234
- ...appliedErrors.map((error) => ` ${error}`),
235
- ].join('\n'));
236
- handler.requestRender();
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
- const warnings = verificationItems.filter((item) => item.status === 'warn');
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: 'The GoodVibes daemon did not start after applying onboarding settings.',
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: 'The GoodVibes daemon port is still occupied after onboarding disabled server-backed surfaces.',
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: 'The HTTP listener did not start after applying onboarding settings.',
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: 'The HTTP listener port is still occupied after onboarding disabled incoming event surfaces.',
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: 'The GoodVibes daemon port is still occupied after onboarding disabled server-backed surfaces.',
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: 'The HTTP listener port is still occupied after onboarding disabled incoming event surfaces.',
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 hasAdmin = auth.users.some((user) => user.roles.includes('admin'));
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: hasAdmin && !auth.bootstrapCredentialPresent ? 'pass' : 'fail',
642
- message: hasAdmin && !auth.bootstrapCredentialPresent
643
- ? 'Local admin auth is configured and bootstrap credentials are not present.'
644
- : 'Network-capable surfaces require local admin auth with no bootstrap credential file.',
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: hasAdmin ? 'pass' : 'fail',
651
- message: hasAdmin
652
- ? 'Remote-capable bind settings have local admin auth available.'
653
- : 'Remote-capable bind settings cannot be applied without local admin auth.',
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
- : 'The GoodVibes daemon is not running after onboarding apply.',
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: 'The GoodVibes daemon port is still occupied after onboarding disabled server-backed surfaces.',
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
- : 'The HTTP listener is not running after onboarding apply.',
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: 'The HTTP listener port is still occupied after onboarding disabled incoming event surfaces.',
733
+ message: formatRuntimeStoppedFailureMessage(handler, request, 'httpListener'),
691
734
  target: 'service',
692
735
  });
693
736
  }
@@ -31,7 +31,7 @@ import { OnboardingWizardController, type OnboardingWizardAction, type Onboardin
31
31
  import {
32
32
  applyOnboardingRequest,
33
33
  collectOnboardingSnapshot,
34
- getOnboardingCompletionMarkerPath,
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.logicalName === 'enter') {
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 if (token.logicalName === 'space') {
56
- state.onboardingWizard.editChar(' ');
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 if (token.logicalName === 'enter' || token.logicalName === 'space') {
78
- activateSelection(state);
79
- } else if (token.logicalName === 'backspace') {
80
- state.onboardingWizard.editBackspace();
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 === 'h') {
86
- state.onboardingWizard.prevStep();
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 { EXTERNAL_SURFACE_SPECS } from './onboarding-wizard-external-surfaces.ts';
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
- if (controller.requiresAuthBootstrap()) {
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: controller.getStringFieldValue('accounts.admin-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 enabled = externalIntegrations
105
- && controller.getBooleanFieldValue(surface.enabledFieldId, surface.defaultEnabled(controller.runtimeSnapshot));
106
- setConfig(surface.enabledConfigKey, enabled);
107
- if (!enabled) continue;
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',