@pellux/goodvibes-tui 0.19.27 → 0.19.28

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 CHANGED
@@ -4,6 +4,11 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.28] — 2026-04-24
8
+
9
+ ### Changes
10
+ - 89221c1 fix: mark onboarding checked when opened
11
+
7
12
  ## [0.19.27] — 2026-04-24
8
13
 
9
14
  ### Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.27-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.28-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.25.1"
6
+ "version": "0.25.2"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.27",
3
+ "version": "0.19.28",
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.1",
94
+ "@pellux/goodvibes-sdk": "^0.25.2",
95
95
  "bash-language-server": "^5.6.0",
96
96
  "fuse.js": "^7.1.0",
97
97
  "graphql": "^16.13.2",
@@ -5,6 +5,7 @@ import { createShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/s
5
5
  import { listProviderRuntimeSnapshots } from '@pellux/goodvibes-sdk/platform/providers/runtime-snapshot';
6
6
  import { createRuntimeServices } from '../runtime/services.ts';
7
7
  import { createRuntimeStore } from '../runtime/store/index.ts';
8
+ import { getOnboardingCheckMarkerPath } from '../runtime/onboarding/index.ts';
8
9
  import { CONFIG_SCHEMA } from '../config/index.ts';
9
10
  import { SecretsManager } from '../config/secrets.ts';
10
11
  import type { ConfigKey } from '../config/index.ts';
@@ -178,8 +179,8 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
178
179
  },
179
180
  secrets: await secrets.inspect(),
180
181
  onboarding: {
181
- projectMarker: existsSync(shellPaths.resolveProjectPath('tui', 'onboarding.json')),
182
- userMarker: existsSync(shellPaths.resolveUserPath('tui', 'onboarding.json')),
182
+ userMarker: existsSync(getOnboardingCheckMarkerPath(shellPaths, 'user')),
183
+ projectMarker: existsSync(getOnboardingCheckMarkerPath(shellPaths, 'project')),
183
184
  },
184
185
  };
185
186
  const targetPath = shellPaths.resolveWorkspacePath(outputPath);
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ConfigManager } from '../config/index.ts';
4
- import { readOnboardingCompletionMarkers } from '../runtime/onboarding/index.ts';
4
+ import { readOnboardingCheckMarkers } from '../runtime/onboarding/index.ts';
5
5
  import { GlobalNetworkTransportInstaller } from '@pellux/goodvibes-sdk/platform/runtime/network/index';
6
6
  import { createShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
7
7
  import { configureActivityLogger } from '@pellux/goodvibes-sdk/platform/utils/logger';
@@ -125,7 +125,7 @@ export async function prepareShellCliRuntime(
125
125
  const userStorePath = shellPaths.resolveUserPath('tui', 'auth-users.json');
126
126
  const bootstrapCredentialPath = shellPaths.resolveUserPath('tui', 'auth-bootstrap.txt');
127
127
  const operatorTokenPath = join(bootstrapHomeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
128
- const onboardingMarkers = readOnboardingCompletionMarkers(shellPaths);
128
+ const onboardingMarkers = readOnboardingCheckMarkers(shellPaths);
129
129
  const service = await buildCliServicePosture({
130
130
  configManager,
131
131
  workingDirectory: bootstrapWorkingDir,
package/src/cli/help.ts CHANGED
@@ -124,7 +124,7 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
124
124
  },
125
125
  onboarding: {
126
126
  usage: ['onboarding', 'setup', 'onboarding status'],
127
- summary: 'Open the setup wizard on the next TUI startup, or inspect onboarding marker status from the CLI.',
127
+ summary: 'Open the setup wizard, or inspect whether onboarding has already been shown for this user.',
128
128
  examples: ['onboarding', 'onboarding status'],
129
129
  },
130
130
  status: {
package/src/cli/status.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
2
- import type { OnboardingCompletionMarkersState } from '../runtime/onboarding/index.ts';
2
+ import type { OnboardingCheckMarkersState } from '../runtime/onboarding/index.ts';
3
3
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
4
4
  import { isNetworkFacing } from './network-posture.ts';
5
5
  import type { GoodVibesCliOutputFormat } from './types.ts';
@@ -9,7 +9,7 @@ export interface CliStatusOptions {
9
9
  readonly configManager: Pick<ConfigManager, 'get'>;
10
10
  readonly workingDirectory: string;
11
11
  readonly homeDirectory: string;
12
- readonly onboardingMarkers?: OnboardingCompletionMarkersState;
12
+ readonly onboardingMarkers?: OnboardingCheckMarkersState;
13
13
  readonly auth?: CliAuthStatus;
14
14
  readonly service?: CliServicePosture;
15
15
  readonly doctor?: boolean;
@@ -63,7 +63,7 @@ export interface CliStatusSnapshot {
63
63
  readonly web: ReturnType<typeof resolveRuntimeEndpointBinding> & { readonly enabled: unknown };
64
64
  };
65
65
  readonly onboarding: {
66
- readonly completed: boolean;
66
+ readonly checked: boolean;
67
67
  readonly scope: string;
68
68
  readonly updatedAt: number | null;
69
69
  };
@@ -167,13 +167,13 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
167
167
  }
168
168
  }
169
169
 
170
- if (!marker?.payload) {
170
+ if (!marker?.exists) {
171
171
  findings.push({
172
172
  id: 'onboarding-incomplete',
173
173
  area: 'onboarding',
174
174
  severity: 'warning',
175
- summary: 'Onboarding has not been completed for this user/project.',
176
- cause: 'No effective onboarding completion marker was found.',
175
+ summary: 'Onboarding has not been shown for this user.',
176
+ cause: 'No global user onboarding check marker was found.',
177
177
  impact: 'Important service, network, provider, auth, or permission choices may still be implicit defaults.',
178
178
  action: 'Run /onboarding in the TUI or goodvibes onboarding status to review setup state.',
179
179
  });
@@ -277,7 +277,7 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
277
277
  web: { enabled: config.get('web.enabled'), ...webBinding },
278
278
  },
279
279
  onboarding: {
280
- completed: Boolean(marker?.payload),
280
+ checked: Boolean(marker?.exists),
281
281
  scope: marker?.scope ?? 'none',
282
282
  updatedAt: marker?.payload?.updatedAt ?? null,
283
283
  },
@@ -344,7 +344,7 @@ export function renderCliStatus(options: CliStatusOptions): string {
344
344
  bindLine('web', webEnabled, webBinding),
345
345
  '',
346
346
  'Onboarding:',
347
- ` completed: ${marker?.payload ? 'yes' : 'no'}`,
347
+ ` checked: ${marker?.exists ? 'yes' : 'no'}`,
348
348
  ` scope: ${marker?.scope ?? 'none'}`,
349
349
  ` updatedAt: ${marker?.payload ? new Date(marker.payload.updatedAt).toISOString() : 'n/a'}`,
350
350
  ];
@@ -372,7 +372,7 @@ export function renderOnboardingCliStatus(options: CliStatusOptions): string {
372
372
  const marker = options.onboardingMarkers?.effective;
373
373
  return [
374
374
  'GoodVibes onboarding status',
375
- ` completed: ${marker?.payload ? 'yes' : 'no'}`,
375
+ ` checked: ${marker?.exists ? 'yes' : 'no'}`,
376
376
  ` scope: ${marker?.scope ?? 'none'}`,
377
377
  ` source: ${marker?.payload?.source ?? 'n/a'}`,
378
378
  ` mode: ${marker?.payload?.mode ?? 'n/a'}`,
@@ -1,6 +1,6 @@
1
1
  import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
2
2
  import type { InputHandler } from '../input/handler.ts';
3
- import { readOnboardingCompletionMarkers } from '../runtime/onboarding/index.ts';
3
+ import { readOnboardingCheckMarker } from '../runtime/onboarding/index.ts';
4
4
  import type { GoodVibesCliParseResult } from './types.ts';
5
5
 
6
6
  export function applyInitialTuiCliState(options: {
@@ -8,11 +8,11 @@ export function applyInitialTuiCliState(options: {
8
8
  readonly input: InputHandler;
9
9
  readonly commandRegistry: CommandRegistry;
10
10
  readonly commandContext: CommandContext;
11
- readonly shellPaths: Parameters<typeof readOnboardingCompletionMarkers>[0];
11
+ readonly shellPaths: Parameters<typeof readOnboardingCheckMarker>[0];
12
12
  readonly render: () => void;
13
13
  }): void {
14
14
  const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
15
- const onboardingMarkers = readOnboardingCompletionMarkers(shellPaths);
15
+ const globalOnboardingMarker = readOnboardingCheckMarker(shellPaths, 'user');
16
16
  if (cli.command === 'onboarding') {
17
17
  input.openOnboardingWizard({ mode: 'edit', reset: true });
18
18
  } else if (cli.command === 'sessions' && cli.commandArgs[0] === 'resume') {
@@ -20,7 +20,7 @@ export function applyInitialTuiCliState(options: {
20
20
  if (target) {
21
21
  void commandRegistry.execute('session', ['resume', target], commandContext).then(() => render());
22
22
  }
23
- } else if (!onboardingMarkers.effective?.payload) {
23
+ } else if (!globalOnboardingMarker.exists) {
24
24
  input.openOnboardingWizard({ mode: 'new', reset: true });
25
25
  }
26
26
 
@@ -1,6 +1,6 @@
1
1
  import { buildProviderAccountSnapshot } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
2
2
  import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
3
- import { collectOnboardingSnapshot } from '../runtime/onboarding/index.ts';
3
+ import { collectOnboardingSnapshot, readOnboardingCheckMarker, writeOnboardingCheckMarker } from '../runtime/onboarding/index.ts';
4
4
  import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy, handleBlockRerun, handleBlockSave, handleBlockToggle, handleBookmark, handleClipboardPaste, handleCopy, handleCtrlC, handleDiffApply, registerPaste } from './handler-content-actions.ts';
5
5
  import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
6
6
  import { openOnboardingWizardState, type OpenOnboardingWizardOptions } from './handler-ui-state.ts';
@@ -11,6 +11,19 @@ export function openOnboardingWizardForHandler(
11
11
  modeOrOptions: OnboardingWizardMode | OpenOnboardingWizardOptions = 'new',
12
12
  ): void {
13
13
  const options = typeof modeOrOptions === 'string' ? { mode: modeOrOptions } : modeOrOptions;
14
+ const userMarker = readOnboardingCheckMarker(handler.uiServices.environment.shellPaths, 'user');
15
+ if (!userMarker.payload) {
16
+ try {
17
+ writeOnboardingCheckMarker(handler.uiServices.environment.shellPaths, {
18
+ scope: 'user',
19
+ source: 'wizard',
20
+ mode: options.mode ?? 'new',
21
+ });
22
+ } catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ handler.commandContext?.print?.(`Onboarding check marker could not be written: ${message}`);
25
+ }
26
+ }
14
27
  if (!handler.modalStack.includes('onboarding')) handler.modalOpened('onboarding');
15
28
  handler.clearOnboardingModelPickerCancelState();
16
29
  openOnboardingWizardState(handler.onboardingWizard, options);
@@ -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
5
  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';
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;
@@ -38,44 +31,6 @@ function extractAuthorizationCode(input: string): string | null {
38
31
  }
39
32
  }
40
33
 
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
- };
51
- }
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
- });
66
- }
67
-
68
- function restoreCompletionMarkers(snapshots: readonly CompletionMarkerSnapshot[]): void {
69
- for (const snapshot of snapshots) {
70
- if (snapshot.previous === null) {
71
- rmSync(snapshot.path, { force: true });
72
- continue;
73
- }
74
- mkdirSync(dirname(snapshot.path), { recursive: true });
75
- writeFileSync(snapshot.path, snapshot.previous, 'utf-8');
76
- }
77
- }
78
-
79
34
  function isLoopbackHostValue(value: string | null | undefined): boolean {
80
35
  const normalized = (value ?? '').trim().toLowerCase();
81
36
  if (normalized.length === 0) return false;
@@ -161,7 +116,7 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
161
116
  const blockers = handler.onboardingWizard.getBlockingFieldLabels();
162
117
  if (blockers.length > 0) {
163
118
  handler.commandContext?.print?.([
164
- 'Onboarding needs required confirmations before applying.',
119
+ 'Onboarding needs these fields before applying.',
165
120
  ...blockers.map((label) => ` ${label}`),
166
121
  ].join('\n'));
167
122
  handler.requestRender();
@@ -169,7 +124,6 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
169
124
  }
170
125
 
171
126
  const request = handler.onboardingWizard.buildApplyRequest();
172
- const { settingsRequest, markerRequest } = splitCompletionMarkerOperations(request);
173
127
  const deps = {
174
128
  config: handler.uiServices.platform.configManager,
175
129
  secrets: handler.uiServices.platform.secretsManager,
@@ -181,12 +135,12 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
181
135
  let verificationItems: readonly OnboardingVerificationItem[] = [];
182
136
  handler.onboardingApplyPending = true;
183
137
  try {
184
- const settingsApplied = await applyOnboardingRequest(deps, settingsRequest);
185
- const settingsVerification = await verifyOnboardingRequest(deps, settingsRequest);
186
- verificationItems = settingsVerification.items;
138
+ const applied = await applyOnboardingRequest(deps, request);
139
+ const verification = await verifyOnboardingRequest(deps, request);
140
+ verificationItems = verification.items;
187
141
  appliedErrors = [
188
- ...settingsApplied.errors.map((error) => `apply ${error.kind}: ${error.message}`),
189
- ...settingsVerification.items
142
+ ...applied.errors.map((error) => `apply ${error.kind}: ${error.message}`),
143
+ ...verification.items
190
144
  .filter((item) => item.status !== 'pass')
191
145
  .map((item) => `verify ${item.id}: ${item.message}`),
192
146
  ];
@@ -194,29 +148,11 @@ export async function handleOnboardingActionForHandler(handler: InputHandler, ac
194
148
  if (appliedErrors.length === 0) {
195
149
  const activationVerification = await handler.restartOnboardingExternalServicesIfNeeded(request);
196
150
  const runtimeVerification = [...activationVerification, ...handler.verifyOnboardingRuntimePosture(request)];
197
- verificationItems = [...settingsVerification.items, ...runtimeVerification];
151
+ verificationItems = [...verification.items, ...runtimeVerification];
198
152
  appliedErrors = runtimeVerification
199
153
  .filter((item) => item.status === 'fail')
200
154
  .map((item) => `verify ${item.id}: ${item.message}`);
201
155
  }
202
-
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);
219
- }
220
156
  } catch (error) {
221
157
  handler.commandContext?.print?.([
222
158
  'Onboarding apply did not complete.',
@@ -625,7 +561,7 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
625
561
  }
626
562
 
627
563
  const auth = handler.uiServices.platform.localUserAuthManager.inspect();
628
- const hasAdmin = auth.users.some((user) => user.roles.includes('admin'));
564
+ const hasLocalAuth = auth.users.length > 0;
629
565
  const items: OnboardingVerificationItem[] = [];
630
566
 
631
567
  items.push({
@@ -638,19 +574,19 @@ export function verifyOnboardingRuntimePostureForHandler(handler: InputHandler,
638
574
  });
639
575
  items.push({
640
576
  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.',
577
+ status: hasLocalAuth && !auth.bootstrapCredentialPresent ? 'pass' : 'fail',
578
+ message: hasLocalAuth && !auth.bootstrapCredentialPresent
579
+ ? 'Local auth is configured and bootstrap credentials are not present.'
580
+ : 'Network-capable surfaces require local auth with no bootstrap credential file.',
645
581
  target: 'auth',
646
582
  });
647
583
  if (posture.remoteExposure) {
648
584
  items.push({
649
585
  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.',
586
+ status: hasLocalAuth ? 'pass' : 'fail',
587
+ message: hasLocalAuth
588
+ ? 'Remote-capable bind settings have local auth available.'
589
+ : 'Remote-capable bind settings cannot be applied without local auth.',
654
590
  target: 'auth',
655
591
  });
656
592
  }
@@ -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,13 @@ 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();
53
67
  } else if (token.logicalName === 'backspace') {
54
68
  state.onboardingWizard.editBackspace();
55
- } else if (token.logicalName === 'space') {
56
- state.onboardingWizard.editChar(' ');
69
+ } else {
70
+ const textInput = getKeyTextInput(token);
71
+ if (textInput !== null) state.onboardingWizard.editChar(textInput);
57
72
  }
58
73
  } else if (token.logicalName === 'left') {
59
74
  state.onboardingWizard.prevStep();
@@ -74,22 +89,23 @@ export function handleOnboardingWizardToken(state: OnboardingRouteState, token:
74
89
  state.onboardingWizard.selectFirst(visibleFields);
75
90
  } else if (token.logicalName === 'end') {
76
91
  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();
92
+ } else {
93
+ const textInput = getKeyTextInput(token);
94
+ if (textInput !== null && state.onboardingWizard.beginSelectedTextInput(textInput)) {
95
+ state.requestRender();
96
+ return true;
97
+ }
98
+ if (isEnterKey(token) || token.logicalName === 'space') {
99
+ activateSelection(state);
100
+ } else if (token.logicalName === 'backspace') {
101
+ state.onboardingWizard.editBackspace();
102
+ }
81
103
  }
82
104
  } else if (token.type === 'text') {
83
105
  if (editing) {
84
106
  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);
107
+ } else if (state.onboardingWizard.beginSelectedTextInput(token.value)) {
108
+ // Direct typing into selected inputs behaves like a real form field.
93
109
  } else if (token.value === ' ') {
94
110
  activateSelection(state);
95
111
  } else if (/^[1-9]$/.test(token.value)) {
@@ -131,23 +131,6 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
131
131
  acknowledge('subscriptions', 'accounts.subscriptions');
132
132
  acknowledge('auth', 'accounts.auth');
133
133
 
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
134
  return {
152
135
  mode: controller.mode,
153
136
  source: 'wizard',
@@ -213,6 +213,5 @@ export function getOnboardingWizardBodyRows(viewportHeight: number): number {
213
213
  }
214
214
 
215
215
  export function getOnboardingWizardVisibleFieldCount(viewportHeight: number): number {
216
- return Math.max(1, Math.floor((getOnboardingWizardBodyRows(viewportHeight) - 5) / 2));
216
+ return Math.max(1, getOnboardingWizardBodyRows(viewportHeight) - 5);
217
217
  }
218
-
@@ -30,10 +30,6 @@ export function getSharedIpHostDefault(
30
30
  return hosts[0] ?? '0.0.0.0';
31
31
  }
32
32
 
33
- export function defaultReviewUserMarker(controller: OnboardingWizardController): boolean {
34
- return controller.mode === 'new';
35
- }
36
-
37
33
  export function toggleCapability(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId): void {
38
34
  if (capabilityId === 'local-tui-only') {
39
35
  for (const capability of controller.getCurrentCapabilities()) {
@@ -69,6 +65,18 @@ export function selectLocalTuiOnly(controller: OnboardingWizardController): void
69
65
  }
70
66
  }
71
67
 
68
+ export function selectAllExternalSurfaces(controller: OnboardingWizardController): void {
69
+ for (const surface of EXTERNAL_SURFACE_SPECS) {
70
+ controller.toggleState.set(surface.enabledFieldId, true);
71
+ }
72
+ }
73
+
74
+ export function clearExternalSurfaces(controller: OnboardingWizardController): void {
75
+ for (const surface of EXTERNAL_SURFACE_SPECS) {
76
+ controller.toggleState.set(surface.enabledFieldId, false);
77
+ }
78
+ }
79
+
72
80
  export function setCapabilityValue(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId, selected: boolean): void {
73
81
  if (capabilityId === 'local-tui-only') {
74
82
  if (selected) {
@@ -172,7 +180,7 @@ export function shouldExposeControlPlaneNetwork(controller: OnboardingWizardCont
172
180
 
173
181
  export function requiresAuthBootstrap(controller: OnboardingWizardController): boolean {
174
182
  return controller.hasServerCapabilitiesSelected()
175
- && (!controller.hasAdminAuthUser() || controller.hasBootstrapCredentialPresent());
183
+ && (!controller.hasLocalAuthUser() || controller.hasBootstrapCredentialPresent());
176
184
  }
177
185
 
178
186
  export function hasAdminAuthUser(controller: OnboardingWizardController): boolean {
@@ -180,6 +188,11 @@ export function hasAdminAuthUser(controller: OnboardingWizardController): boolea
180
188
  .some((user) => user.roles.includes('admin'));
181
189
  }
182
190
 
191
+ export function hasLocalAuthUser(controller: OnboardingWizardController): boolean {
192
+ return (controller.runtimeSnapshot?.auth.snapshot.userCount ?? 0) > 0
193
+ || (controller.runtimeSnapshot?.auth.snapshot.users ?? []).length > 0;
194
+ }
195
+
183
196
  export function hasBootstrapCredentialPresent(controller: OnboardingWizardController): boolean {
184
197
  return controller.runtimeSnapshot?.auth.snapshot.bootstrapCredentialPresent === true;
185
198
  }
@@ -313,14 +313,20 @@ export function isFieldDirtyByDefinition(controller: OnboardingWizardController,
313
313
  }
314
314
 
315
315
  export function isFieldSatisfied(controller: OnboardingWizardController, field: OnboardingWizardFieldDefinition): boolean {
316
- if (field.kind === 'checklist' || field.kind === 'acknowledgement') {
317
- if (field.kind === 'acknowledgement' && !field.required) return true;
316
+ if (field.kind === 'checklist') {
317
+ return true;
318
+ }
319
+
320
+ if (field.kind === 'acknowledgement') {
321
+ if (!field.required) return true;
318
322
  return Boolean(controller.getFieldValue(field));
319
323
  }
320
324
 
321
325
  if (field.kind === 'radio') return true;
322
326
 
323
327
  if (field.kind === 'text' || field.kind === 'masked') {
328
+ const required = field.required === true || controller.isRequiredExternalSetupField(field.id);
329
+ if (!required) return true;
324
330
  return normalizeText(controller.getFieldValue(field) as string).length > 0;
325
331
  }
326
332