@pellux/goodvibes-tui 0.19.24 → 0.19.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +5 -5
  3. package/bin/goodvibes +5 -0
  4. package/bin/goodvibes-daemon +5 -0
  5. package/docs/foundation-artifacts/operator-contract.json +1 -1
  6. package/package.json +2 -2
  7. package/src/cli/completion.ts +89 -0
  8. package/src/cli/config-overrides.ts +159 -0
  9. package/src/cli/endpoints.ts +63 -0
  10. package/src/cli/entrypoint.ts +155 -0
  11. package/src/cli/help.ts +122 -0
  12. package/src/cli/index.ts +8 -0
  13. package/src/cli/management-commands.ts +576 -0
  14. package/src/cli/management.ts +693 -0
  15. package/src/cli/parser.ts +367 -0
  16. package/src/cli/status.ts +112 -0
  17. package/src/cli/tui-startup.ts +32 -0
  18. package/src/cli/types.ts +63 -0
  19. package/src/cli-flags.ts +17 -55
  20. package/src/config/index.ts +1 -1
  21. package/src/config/secrets.ts +44 -0
  22. package/src/daemon/cli.ts +62 -11
  23. package/src/input/command-registry.ts +3 -0
  24. package/src/input/commands/guidance-runtime.ts +9 -4
  25. package/src/input/commands/local-runtime.ts +21 -7
  26. package/src/input/commands/local-setup.ts +31 -38
  27. package/src/input/commands/onboarding-runtime.ts +14 -0
  28. package/src/input/commands/runtime-services.ts +9 -0
  29. package/src/input/commands.ts +2 -0
  30. package/src/input/feed-context-factory.ts +8 -1
  31. package/src/input/handler-feed.ts +13 -8
  32. package/src/input/handler-interactions.ts +266 -0
  33. package/src/input/handler-modal-stack.ts +23 -3
  34. package/src/input/handler-modal-token-routes.ts +23 -1
  35. package/src/input/handler-onboarding.ts +696 -0
  36. package/src/input/handler-picker-routes.ts +15 -7
  37. package/src/input/handler-ui-state.ts +58 -0
  38. package/src/input/handler.ts +120 -246
  39. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  40. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  41. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  42. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  43. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  44. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  45. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  46. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  47. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  48. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  49. package/src/main.ts +32 -39
  50. package/src/panels/builtin/operations.ts +0 -10
  51. package/src/panels/index.ts +0 -1
  52. package/src/renderer/conversation-overlays.ts +6 -0
  53. package/src/renderer/help-overlay.ts +1 -1
  54. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  55. package/src/runtime/bootstrap-core.ts +1 -0
  56. package/src/runtime/bootstrap.ts +123 -0
  57. package/src/runtime/onboarding/apply.ts +685 -0
  58. package/src/runtime/onboarding/derivation.ts +495 -0
  59. package/src/runtime/onboarding/index.ts +7 -0
  60. package/src/runtime/onboarding/markers.ts +161 -0
  61. package/src/runtime/onboarding/snapshot.ts +400 -0
  62. package/src/runtime/onboarding/state.ts +140 -0
  63. package/src/runtime/onboarding/types.ts +402 -0
  64. package/src/runtime/onboarding/verify.ts +233 -0
  65. package/src/runtime/ui-services.ts +16 -0
  66. package/src/shell/ui-openers.ts +12 -2
  67. package/src/version.ts +1 -1
  68. package/src/panels/welcome-panel.ts +0 -64
@@ -0,0 +1,218 @@
1
+ import { isIP } from 'node:net';
2
+ import { deriveOnboardingStepState, type OnboardingStep1CapabilityItem, type OnboardingStepDerivationState } from '../../runtime/onboarding/index.ts';
3
+ import { DEFAULT_CAPABILITIES } from './onboarding-wizard-constants.ts';
4
+ import { EXTERNAL_SURFACE_SPECS, type ExternalSurfaceSetupFieldSpec, type ExternalSurfaceSpec } from './onboarding-wizard-external-surfaces.ts';
5
+ import type { OnboardingWizardAcknowledgementFieldDefinition, OnboardingWizardModelSelection, OnboardingWizardRuntimeHydration } from './onboarding-wizard-types.ts';
6
+
7
+ export function clamp(value: number, min: number, max: number): number {
8
+ return Math.max(min, Math.min(max, value));
9
+ }
10
+
11
+ export function countSelected(items: readonly OnboardingStep1CapabilityItem[]): number {
12
+ return items.filter((item) => item.selected).length;
13
+ }
14
+
15
+ export function normalizeText(value: string | null | undefined): string {
16
+ return (value ?? '').trim();
17
+ }
18
+
19
+ export function normalizeSecretKeyPart(value: string): string {
20
+ return value
21
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
22
+ .replace(/[^a-zA-Z0-9]+/g, '_')
23
+ .replace(/^_+|_+$/g, '')
24
+ .toUpperCase();
25
+ }
26
+
27
+ export function buildGoodVibesSecretKey(configKey: string): string {
28
+ return `GOODVIBES_${configKey.split('.').map(normalizeSecretKeyPart).filter(Boolean).join('_')}`;
29
+ }
30
+
31
+ export function buildGoodVibesSecretRef(secretKey: string): string {
32
+ return `goodvibes://secrets/goodvibes/${encodeURIComponent(secretKey)}`;
33
+ }
34
+
35
+ export function isSecretReferenceValue(value: string): boolean {
36
+ const normalized = value.trim();
37
+ if (normalized.length === 0) return false;
38
+ let url: URL;
39
+ try {
40
+ url = new URL(normalized);
41
+ } catch {
42
+ return false;
43
+ }
44
+ if (url.protocol !== 'goodvibes:' || url.hostname !== 'secrets') return false;
45
+ const segments = url.pathname.split('/').filter(Boolean).map((segment) => decodeURIComponent(segment));
46
+ const source = (segments[0] ?? '').toLowerCase();
47
+ const rest = segments.slice(1);
48
+ const params = url.searchParams;
49
+ if (source === 'env' || source === 'goodvibes') {
50
+ return Boolean(rest[0] ?? params.get('id') ?? params.get('key') ?? params.get('name'));
51
+ }
52
+ if (source === 'file') {
53
+ return Boolean(rest.length > 0 || params.get('path'));
54
+ }
55
+ if (source === 'exec') {
56
+ return Boolean(rest[0] ?? params.get('command') ?? params.get('cmd'));
57
+ }
58
+ if (source === '1password' || source === 'onepassword' || source === 'op') {
59
+ return Boolean(params.get('ref') ?? params.get('uri'))
60
+ || Boolean((params.get('vault') ?? rest[0]) && (params.get('item') ?? rest[1]) && (params.get('field') ?? rest[2]));
61
+ }
62
+ if (source === 'bitwarden' || source === 'vaultwarden') {
63
+ return Boolean(rest[0] ?? params.get('item') ?? params.get('id') ?? params.get('name'));
64
+ }
65
+ if (source === 'bitwarden-secrets-manager' || source === 'bws') {
66
+ return Boolean(rest[0] ?? params.get('id') ?? params.get('secretId') ?? params.get('secret'));
67
+ }
68
+ return false;
69
+ }
70
+
71
+ export function isMalformedGoodVibesSecretReferenceValue(value: string): boolean {
72
+ const normalized = value.trim();
73
+ return normalized.startsWith('goodvibes://') && !isSecretReferenceValue(normalized);
74
+ }
75
+
76
+ export function isValidHostValue(value: string): boolean {
77
+ const normalized = value.trim();
78
+ if (normalized.length === 0) return false;
79
+ if (/\s/.test(normalized)) return false;
80
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized)) return false;
81
+ if (normalized.includes('/')) return false;
82
+ if (normalized.includes(':')) {
83
+ const unwrapped = normalized.startsWith('[') && normalized.endsWith(']')
84
+ ? normalized.slice(1, -1)
85
+ : normalized;
86
+ if (isIP(unwrapped) === 0) return false;
87
+ }
88
+ return true;
89
+ }
90
+
91
+ export function isLoopbackAddress(value: string): boolean {
92
+ const normalized = value.trim().toLowerCase();
93
+ return normalized === 'localhost'
94
+ || normalized === '::1'
95
+ || normalized === '[::1]'
96
+ || normalized === '0:0:0:0:0:0:0:1'
97
+ || /^127(?:\.\d{1,3}){3}$/.test(normalized);
98
+ }
99
+
100
+ export function uniqueNonEmpty(values: readonly string[]): readonly string[] {
101
+ return [...new Set(values.map((value) => normalizeText(value)).filter((value) => value.length > 0))];
102
+ }
103
+
104
+ export function makeNotNeededAcknowledgement(detail: string): OnboardingWizardAcknowledgementFieldDefinition {
105
+ return {
106
+ kind: 'acknowledgement',
107
+ id: 'ack.placeholder',
108
+ label: 'Acknowledgement not required',
109
+ hint: detail,
110
+ defaultValue: false,
111
+ required: false,
112
+ reason: 'not-needed',
113
+ };
114
+ }
115
+
116
+ export function buildDefaultDerivedState(): OnboardingStepDerivationState {
117
+ return {
118
+ step1Capabilities: DEFAULT_CAPABILITIES,
119
+ step1_5NetworkMode: 'local-network-default',
120
+ reopenEditAcknowledgements: {
121
+ providers: {
122
+ required: false,
123
+ accepted: false,
124
+ reason: 'not-needed',
125
+ detail: 'No existing provider routing needs confirmation.',
126
+ },
127
+ subscriptions: {
128
+ required: false,
129
+ accepted: false,
130
+ reason: 'not-needed',
131
+ detail: 'No stored subscription sessions need confirmation.',
132
+ },
133
+ auth: {
134
+ required: false,
135
+ accepted: false,
136
+ reason: 'not-needed',
137
+ detail: 'No local auth state needs confirmation.',
138
+ },
139
+ },
140
+ };
141
+ }
142
+
143
+ export function maskValue(value: string): string {
144
+ if (value.length === 0) return 'unset';
145
+ if (value.length <= 3) return '•'.repeat(value.length);
146
+ return `${'•'.repeat(Math.max(0, value.length - 2))}${value.slice(-2)}`;
147
+ }
148
+
149
+ export function areSelectionsEqual(
150
+ left: OnboardingWizardModelSelection | undefined,
151
+ right: OnboardingWizardModelSelection | undefined,
152
+ ): boolean {
153
+ return (left?.providerId ?? '') === (right?.providerId ?? '')
154
+ && (left?.modelId ?? '') === (right?.modelId ?? '')
155
+ && (left?.enabled ?? true) === (right?.enabled ?? true);
156
+ }
157
+
158
+ export function cloneSelection(selection: OnboardingWizardModelSelection): OnboardingWizardModelSelection {
159
+ return {
160
+ providerId: selection.providerId,
161
+ modelId: selection.modelId,
162
+ enabled: selection.enabled,
163
+ };
164
+ }
165
+
166
+ export function modelSelectionLabel(selection: OnboardingWizardModelSelection | undefined): string {
167
+ if (!selection) return 'Choose model';
168
+ if (selection.enabled === false && selection.providerId.length === 0 && selection.modelId.length === 0) {
169
+ return 'Disabled';
170
+ }
171
+
172
+ const provider = selection.providerId.length > 0 ? selection.providerId : 'provider';
173
+ const model = selection.modelId.length > 0 ? selection.modelId : 'model';
174
+ if (selection.enabled === false) return `Off (${provider}/${model})`;
175
+ return `${provider}/${model}`;
176
+ }
177
+
178
+ export function getExternalSurfaceSetupFieldSpec(fieldId: string): ExternalSurfaceSetupFieldSpec | null {
179
+ for (const surface of EXTERNAL_SURFACE_SPECS) {
180
+ const field = surface.fields.find((entry) => entry.id === fieldId);
181
+ if (field) return field;
182
+ }
183
+ return null;
184
+ }
185
+
186
+ export function getExternalSurfaceSpecByFieldId(fieldId: string): ExternalSurfaceSpec | null {
187
+ for (const surface of EXTERNAL_SURFACE_SPECS) {
188
+ if (surface.fields.some((entry) => entry.id === fieldId)) return surface;
189
+ }
190
+ return null;
191
+ }
192
+
193
+ export function getRuntimeDerivedState(hydration: OnboardingWizardRuntimeHydration): OnboardingStepDerivationState {
194
+ if (hydration.derived) {
195
+ const fallback = buildDefaultDerivedState();
196
+ return {
197
+ step1Capabilities: hydration.derived.step1Capabilities ?? fallback.step1Capabilities,
198
+ step1_5NetworkMode: hydration.derived.step1_5NetworkMode ?? fallback.step1_5NetworkMode,
199
+ reopenEditAcknowledgements: {
200
+ providers: hydration.derived.reopenEditAcknowledgements?.providers ?? fallback.reopenEditAcknowledgements.providers,
201
+ subscriptions: hydration.derived.reopenEditAcknowledgements?.subscriptions ?? fallback.reopenEditAcknowledgements.subscriptions,
202
+ auth: hydration.derived.reopenEditAcknowledgements?.auth ?? fallback.reopenEditAcknowledgements.auth,
203
+ },
204
+ };
205
+ }
206
+
207
+ if (hydration.snapshot) return deriveOnboardingStepState(hydration.snapshot);
208
+ return buildDefaultDerivedState();
209
+ }
210
+
211
+ export function getOnboardingWizardBodyRows(viewportHeight: number): number {
212
+ return Math.max(5, viewportHeight - 5);
213
+ }
214
+
215
+ export function getOnboardingWizardVisibleFieldCount(viewportHeight: number): number {
216
+ return Math.max(1, Math.floor((getOnboardingWizardBodyRows(viewportHeight) - 5) / 2));
217
+ }
218
+
@@ -0,0 +1,224 @@
1
+ import type { OnboardingStep1CapabilityId } from '../../runtime/onboarding/index.ts';
2
+ import { INBOUND_EXTERNAL_SURFACE_IDS, REQUIRED_EXTERNAL_SETUP_FIELD_IDS } from './onboarding-wizard-constants.ts';
3
+ import { EXTERNAL_SURFACE_SPECS } from './onboarding-wizard-external-surfaces.ts';
4
+ import { getExternalSurfaceSpecByFieldId, normalizeText, uniqueNonEmpty } from './onboarding-wizard-helpers.ts';
5
+ import type { OnboardingWizardController } from './onboarding-wizard.ts';
6
+
7
+ export function getSharedIpDefault(
8
+ controller: OnboardingWizardController,
9
+ enabled: { readonly controlPlane: boolean; readonly httpListener: boolean; readonly web: boolean },
10
+ ): boolean {
11
+ if (!controller.runtimeSnapshot) return true;
12
+ const hosts = uniqueNonEmpty([
13
+ enabled.controlPlane ? controller.runtimeSnapshot.bindSettings.controlPlane.host : '',
14
+ enabled.httpListener ? controller.runtimeSnapshot.bindSettings.httpListener.host : '',
15
+ enabled.web ? controller.runtimeSnapshot.bindSettings.web.host : '',
16
+ ]);
17
+ return hosts.length <= 1;
18
+ }
19
+
20
+ export function getSharedIpHostDefault(
21
+ controller: OnboardingWizardController,
22
+ enabled: { readonly controlPlane: boolean; readonly httpListener: boolean; readonly web: boolean },
23
+ ): string {
24
+ if (!controller.runtimeSnapshot) return '0.0.0.0';
25
+ const hosts = uniqueNonEmpty([
26
+ enabled.controlPlane ? controller.runtimeSnapshot.bindSettings.controlPlane.host : '',
27
+ enabled.httpListener ? controller.runtimeSnapshot.bindSettings.httpListener.host : '',
28
+ enabled.web ? controller.runtimeSnapshot.bindSettings.web.host : '',
29
+ ]);
30
+ return hosts[0] ?? '0.0.0.0';
31
+ }
32
+
33
+ export function defaultReviewUserMarker(controller: OnboardingWizardController): boolean {
34
+ return controller.mode === 'new';
35
+ }
36
+
37
+ export function toggleCapability(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId): void {
38
+ if (capabilityId === 'local-tui-only') {
39
+ for (const capability of controller.getCurrentCapabilities()) {
40
+ controller.toggleState.set(`capabilities.${capability.id}`, capability.id === 'local-tui-only');
41
+ }
42
+ return;
43
+ }
44
+
45
+ const fieldId = `capabilities.${capabilityId}`;
46
+ const nextValue = !(controller.toggleState.get(fieldId) ?? false);
47
+ controller.toggleState.set(fieldId, nextValue);
48
+ if (nextValue) {
49
+ controller.toggleState.set('capabilities.local-tui-only', false);
50
+ return;
51
+ }
52
+
53
+ const anyServerCapability = controller.getCurrentCapabilities().some((capability) => (
54
+ capability.id !== 'local-tui-only'
55
+ && (controller.toggleState.get(`capabilities.${capability.id}`) ?? false)
56
+ ));
57
+ if (!anyServerCapability) controller.toggleState.set('capabilities.local-tui-only', true);
58
+ }
59
+
60
+ export function selectAllServerCapabilities(controller: OnboardingWizardController): void {
61
+ for (const capability of controller.getCurrentCapabilities()) {
62
+ controller.toggleState.set(`capabilities.${capability.id}`, capability.id !== 'local-tui-only');
63
+ }
64
+ }
65
+
66
+ export function selectLocalTuiOnly(controller: OnboardingWizardController): void {
67
+ for (const capability of controller.getCurrentCapabilities()) {
68
+ controller.toggleState.set(`capabilities.${capability.id}`, capability.id === 'local-tui-only');
69
+ }
70
+ }
71
+
72
+ export function setCapabilityValue(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId, selected: boolean): void {
73
+ if (capabilityId === 'local-tui-only') {
74
+ if (selected) {
75
+ for (const capability of controller.getCurrentCapabilities()) {
76
+ controller.toggleState.set(`capabilities.${capability.id}`, capability.id === 'local-tui-only');
77
+ }
78
+ return;
79
+ }
80
+
81
+ const anyServerCapability = controller.getCurrentCapabilities().some((capability) => (
82
+ capability.id !== 'local-tui-only'
83
+ && (controller.toggleState.get(`capabilities.${capability.id}`) ?? false)
84
+ ));
85
+ controller.toggleState.set('capabilities.local-tui-only', !anyServerCapability);
86
+ return;
87
+ }
88
+
89
+ controller.toggleState.set(`capabilities.${capabilityId}`, selected);
90
+ if (selected) {
91
+ controller.toggleState.set('capabilities.local-tui-only', false);
92
+ return;
93
+ }
94
+
95
+ const anyServerCapability = controller.getCurrentCapabilities().some((capability) => (
96
+ capability.id !== 'local-tui-only'
97
+ && (controller.toggleState.get(`capabilities.${capability.id}`) ?? false)
98
+ ));
99
+ if (!anyServerCapability) controller.toggleState.set('capabilities.local-tui-only', true);
100
+ }
101
+
102
+ export function isCapabilitySelected(controller: OnboardingWizardController, capabilityId: OnboardingStep1CapabilityId): boolean {
103
+ return controller.getCapabilitySelectionState().some((capability) => capability.id === capabilityId && capability.selected);
104
+ }
105
+
106
+ export function hasServerCapabilitiesSelected(controller: OnboardingWizardController): boolean {
107
+ return controller.getCapabilitySelectionState().some((capability) => capability.id !== 'local-tui-only' && capability.selected);
108
+ }
109
+
110
+ export function shouldEnableBrowserSurface(controller: OnboardingWizardController): boolean {
111
+ return controller.hasServerCapabilitiesSelected()
112
+ && (controller.isCapabilitySelected('browser-access') || controller.isCapabilitySelected('network-access'));
113
+ }
114
+
115
+ export function hasSelectedInboundExternalSurface(controller: OnboardingWizardController): boolean {
116
+ if (!controller.isCapabilitySelected('external-integrations')) return false;
117
+ return EXTERNAL_SURFACE_SPECS.some((surface) => (
118
+ INBOUND_EXTERNAL_SURFACE_IDS.has(surface.id)
119
+ && controller.getBooleanFieldValue(surface.enabledFieldId, surface.defaultEnabled(controller.runtimeSnapshot))
120
+ ));
121
+ }
122
+
123
+ export function isRequiredExternalSetupField(controller: OnboardingWizardController, fieldId: string): boolean {
124
+ if (!REQUIRED_EXTERNAL_SETUP_FIELD_IDS.has(fieldId)) return false;
125
+ const surface = getExternalSurfaceSpecByFieldId(fieldId);
126
+ if (!surface) return false;
127
+ if (!controller.isCapabilitySelected('external-integrations')
128
+ || !controller.getBooleanFieldValue(surface.enabledFieldId, surface.defaultEnabled(controller.runtimeSnapshot))) {
129
+ return false;
130
+ }
131
+ if (fieldId === 'external-services.telegram.webhook-secret') {
132
+ return controller.getStringFieldValue('external-services.telegram.mode', 'webhook') === 'webhook';
133
+ }
134
+ if (
135
+ fieldId === 'external-services.whatsapp.verify-token'
136
+ || fieldId === 'external-services.whatsapp.phone-number-id'
137
+ ) {
138
+ return controller.getStringFieldValue('external-services.whatsapp.provider', 'meta-cloud') === 'meta-cloud';
139
+ }
140
+ return true;
141
+ }
142
+
143
+ export function getSelectedSecretMedium(controller: OnboardingWizardController): 'secure' | 'plaintext' {
144
+ const policy = controller.getStringFieldValue(
145
+ 'external-services.secret-policy',
146
+ controller.runtimeSnapshot?.runtimeDefaults.secretStoragePolicy ?? 'preferred_secure',
147
+ );
148
+ if (policy === 'require_secure') return 'secure';
149
+ if (policy === 'plaintext_allowed') return 'plaintext';
150
+ if (controller.runtimeSnapshot?.secrets.review.secureAvailable) return 'secure';
151
+ return 'plaintext';
152
+ }
153
+
154
+ export function shouldEnableHttpListener(controller: OnboardingWizardController): boolean {
155
+ return controller.hasServerCapabilitiesSelected()
156
+ && (controller.isCapabilitySelected('webhook-events') || controller.hasSelectedInboundExternalSurface());
157
+ }
158
+
159
+ export function shouldExposeHttpListenerNetworkFields(controller: OnboardingWizardController): boolean {
160
+ return controller.hasServerCapabilitiesSelected()
161
+ && (
162
+ controller.isCapabilitySelected('webhook-events')
163
+ || controller.isCapabilitySelected('external-integrations')
164
+ || controller.hasSelectedInboundExternalSurface()
165
+ );
166
+ }
167
+
168
+ export function shouldExposeControlPlaneNetwork(controller: OnboardingWizardController): boolean {
169
+ return controller.hasServerCapabilitiesSelected()
170
+ && (controller.isCapabilitySelected('browser-access') || controller.isCapabilitySelected('network-access'));
171
+ }
172
+
173
+ export function requiresAuthBootstrap(controller: OnboardingWizardController): boolean {
174
+ return controller.hasServerCapabilitiesSelected()
175
+ && (!controller.hasAdminAuthUser() || controller.hasBootstrapCredentialPresent());
176
+ }
177
+
178
+ export function hasAdminAuthUser(controller: OnboardingWizardController): boolean {
179
+ return (controller.runtimeSnapshot?.auth.snapshot.users ?? [])
180
+ .some((user) => user.roles.includes('admin'));
181
+ }
182
+
183
+ export function hasBootstrapCredentialPresent(controller: OnboardingWizardController): boolean {
184
+ return controller.runtimeSnapshot?.auth.snapshot.bootstrapCredentialPresent === true;
185
+ }
186
+
187
+ export function getDefaultAdminUsername(controller: OnboardingWizardController): string {
188
+ const users = controller.runtimeSnapshot?.auth.snapshot.users ?? [];
189
+ const candidates = controller.hasBootstrapCredentialPresent()
190
+ ? ['goodvibes-admin', 'admin']
191
+ : ['admin', 'goodvibes-admin'];
192
+ const candidate = candidates.find((username) => !users.some((user) => user.username === username));
193
+ return candidate ?? `goodvibes-admin-${users.length + 1}`;
194
+ }
195
+
196
+ export function getBooleanFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: boolean): boolean {
197
+ return controller.toggleState.get(fieldId) ?? fallback;
198
+ }
199
+
200
+ export function getStringFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: string): string {
201
+ const value = controller.textState.get(fieldId) ?? controller.radioState.get(fieldId);
202
+ return normalizeText(value ?? fallback);
203
+ }
204
+
205
+ export function parseIntegerFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: number): number | null {
206
+ const raw = controller.getStringFieldValue(fieldId, String(fallback));
207
+ if (!/^-?\d+$/.test(raw)) return null;
208
+ const parsed = Number.parseInt(raw, 10);
209
+ return Number.isInteger(parsed) ? parsed : null;
210
+ }
211
+
212
+ export function getPortFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: number): number {
213
+ const parsed = controller.parseIntegerFieldValue(fieldId, fallback);
214
+ if (parsed === null || parsed < 1 || parsed > 65535) return fallback;
215
+ return parsed;
216
+ }
217
+
218
+ export function getNumberFieldValue(controller: OnboardingWizardController, fieldId: string, fallback: number, min?: number, max?: number): number {
219
+ const parsed = controller.parseIntegerFieldValue(fieldId, fallback);
220
+ if (parsed === null) return fallback;
221
+ if (min !== undefined && parsed < min) return fallback;
222
+ if (max !== undefined && parsed > max) return fallback;
223
+ return parsed;
224
+ }