@pellux/goodvibes-tui 0.19.24 → 0.19.26

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 (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/bin/goodvibes +10 -0
  4. package/bin/goodvibes-daemon +10 -0
  5. package/docs/foundation-artifacts/operator-contract.json +1 -1
  6. package/package.json +3 -2
  7. package/src/cli/bundle-command.ts +225 -0
  8. package/src/cli/completion.ts +90 -0
  9. package/src/cli/config-overrides.ts +159 -0
  10. package/src/cli/endpoints.ts +63 -0
  11. package/src/cli/entrypoint.ts +169 -0
  12. package/src/cli/help.ts +301 -0
  13. package/src/cli/index.ts +11 -0
  14. package/src/cli/management-commands.ts +426 -0
  15. package/src/cli/management.ts +719 -0
  16. package/src/cli/network-posture.ts +46 -0
  17. package/src/cli/package-verification.ts +119 -0
  18. package/src/cli/parser.ts +369 -0
  19. package/src/cli/provider-classification.ts +107 -0
  20. package/src/cli/redaction.ts +105 -0
  21. package/src/cli/service-command.ts +45 -0
  22. package/src/cli/service-posture.ts +247 -0
  23. package/src/cli/status.ts +382 -0
  24. package/src/cli/surface-command.ts +248 -0
  25. package/src/cli/tui-startup.ts +32 -0
  26. package/src/cli/types.ts +69 -0
  27. package/src/cli-flags.ts +18 -55
  28. package/src/config/index.ts +1 -1
  29. package/src/config/secrets.ts +44 -0
  30. package/src/daemon/cli.ts +62 -11
  31. package/src/input/command-registry.ts +3 -0
  32. package/src/input/commands/guidance-runtime.ts +9 -4
  33. package/src/input/commands/local-runtime.ts +21 -7
  34. package/src/input/commands/local-setup.ts +31 -38
  35. package/src/input/commands/onboarding-runtime.ts +14 -0
  36. package/src/input/commands/runtime-services.ts +9 -0
  37. package/src/input/commands.ts +2 -0
  38. package/src/input/feed-context-factory.ts +8 -1
  39. package/src/input/handler-feed.ts +13 -8
  40. package/src/input/handler-interactions.ts +266 -0
  41. package/src/input/handler-modal-stack.ts +23 -3
  42. package/src/input/handler-modal-token-routes.ts +23 -1
  43. package/src/input/handler-onboarding.ts +696 -0
  44. package/src/input/handler-picker-routes.ts +15 -7
  45. package/src/input/handler-ui-state.ts +58 -0
  46. package/src/input/handler.ts +120 -246
  47. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  48. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  49. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  50. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  51. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  52. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  53. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  54. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  55. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  56. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  57. package/src/main.ts +32 -39
  58. package/src/panels/builtin/operations.ts +0 -10
  59. package/src/panels/index.ts +0 -1
  60. package/src/renderer/conversation-overlays.ts +6 -0
  61. package/src/renderer/help-overlay.ts +1 -1
  62. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  63. package/src/runtime/bootstrap-core.ts +1 -0
  64. package/src/runtime/bootstrap.ts +123 -0
  65. package/src/runtime/onboarding/apply.ts +685 -0
  66. package/src/runtime/onboarding/derivation.ts +495 -0
  67. package/src/runtime/onboarding/index.ts +7 -0
  68. package/src/runtime/onboarding/markers.ts +161 -0
  69. package/src/runtime/onboarding/snapshot.ts +400 -0
  70. package/src/runtime/onboarding/state.ts +140 -0
  71. package/src/runtime/onboarding/types.ts +402 -0
  72. package/src/runtime/onboarding/verify.ts +233 -0
  73. package/src/runtime/ui-services.ts +16 -0
  74. package/src/shell/ui-openers.ts +12 -2
  75. package/src/version.ts +1 -1
  76. package/src/panels/welcome-panel.ts +0 -64
@@ -0,0 +1,402 @@
1
+ import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../../config/index.ts';
2
+ import type { SecretsManager, SecretRecord, SecretStorageReview } from '../../config/secrets.ts';
3
+ import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security/user-auth';
4
+ import type { ShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
5
+ import type {
6
+ LocalAuthInspectionQuery,
7
+ ServiceInspectionQuery,
8
+ SubscriptionAccessQuery,
9
+ } from '../ui-service-queries.ts';
10
+
11
+ export type OnboardingMode = 'new' | 'edit' | 'reopen';
12
+
13
+ export type OnboardingNetworkMode = 'local-network-default' | 'custom';
14
+
15
+ export type OnboardingAcknowledgementTarget = 'providers' | 'subscriptions' | 'auth';
16
+
17
+ export type OnboardingAcknowledgementReason =
18
+ | 'not-needed'
19
+ | 'configured-routing'
20
+ | 'customized-config'
21
+ | 'subscription-state'
22
+ | 'pending-login'
23
+ | 'auth-state'
24
+ | 'bootstrap-credential'
25
+ | 'active-sessions';
26
+
27
+ export type OnboardingVerificationStatus = 'pass' | 'warn' | 'fail';
28
+
29
+ export type OnboardingCompletionMarkerScope = 'user' | 'project';
30
+
31
+ export type OnboardingCompletionMarkerSource = 'wizard' | 'command' | 'import' | 'unknown';
32
+
33
+ export interface OnboardingConfigSnapshot {
34
+ readonly display: GoodVibesConfig['display'];
35
+ readonly provider: GoodVibesConfig['provider'];
36
+ readonly behavior: GoodVibesConfig['behavior'];
37
+ readonly storage: GoodVibesConfig['storage'];
38
+ readonly permissions: GoodVibesConfig['permissions'];
39
+ readonly helper: GoodVibesConfig['helper'];
40
+ readonly tools: Pick<GoodVibesConfig['tools'], 'llmEnabled' | 'llmProvider' | 'llmModel'>;
41
+ readonly danger: GoodVibesConfig['danger'];
42
+ readonly controlPlane: GoodVibesConfig['controlPlane'];
43
+ readonly httpListener: GoodVibesConfig['httpListener'];
44
+ readonly web: GoodVibesConfig['web'];
45
+ readonly network: GoodVibesConfig['network'];
46
+ readonly surfaces: GoodVibesConfig['surfaces'];
47
+ readonly service: GoodVibesConfig['service'];
48
+ }
49
+
50
+ export interface OnboardingProviderRoutingSnapshot {
51
+ readonly primaryProviderId: string;
52
+ readonly primaryModelId: string;
53
+ readonly primaryReasoningEffort: GoodVibesConfig['provider']['reasoningEffort'];
54
+ readonly embeddingProviderId: string;
55
+ readonly systemPromptFile: string;
56
+ readonly helperEnabled: boolean;
57
+ readonly helperProviderId: string;
58
+ readonly helperModelId: string;
59
+ readonly toolLlmEnabled: boolean;
60
+ readonly toolProviderId: string;
61
+ readonly toolModelId: string;
62
+ }
63
+
64
+ export interface OnboardingServiceState {
65
+ readonly name: string;
66
+ readonly providerId: string;
67
+ readonly baseUrl: string | null;
68
+ readonly authType: 'bearer' | 'basic' | 'api-key' | 'oauth';
69
+ readonly tokenKey: string;
70
+ readonly oauthConfigured: boolean;
71
+ readonly hasPrimaryCredential: boolean;
72
+ readonly hasPasswordCredential: boolean;
73
+ readonly hasWebhookUrl: boolean;
74
+ readonly hasSigningSecret: boolean;
75
+ readonly hasPublicKey: boolean;
76
+ }
77
+
78
+ export interface OnboardingServicesSnapshot {
79
+ readonly total: number;
80
+ readonly oauthProviderIds: readonly string[];
81
+ readonly services: readonly OnboardingServiceState[];
82
+ }
83
+
84
+ export interface OnboardingSubscriptionsSnapshot {
85
+ readonly active: ReturnType<SubscriptionAccessQuery['list']>;
86
+ readonly pending: ReturnType<SubscriptionAccessQuery['listPending']>;
87
+ readonly activeProviderIds: readonly string[];
88
+ readonly pendingProviderIds: readonly string[];
89
+ }
90
+
91
+ export interface OnboardingSecretsSnapshot {
92
+ readonly review: SecretStorageReview;
93
+ readonly records: readonly SecretRecord[];
94
+ }
95
+
96
+ export interface OnboardingAuthSnapshot {
97
+ readonly snapshot: LocalAuthSnapshot;
98
+ }
99
+
100
+ export interface OnboardingBindSettingsSnapshot {
101
+ readonly daemonEnabled: boolean;
102
+ readonly httpListenerEnabled: boolean;
103
+ readonly controlPlane: GoodVibesConfig['controlPlane'];
104
+ readonly httpListener: GoodVibesConfig['httpListener'];
105
+ readonly web: GoodVibesConfig['web'];
106
+ }
107
+
108
+ export interface OnboardingSurfaceRecord {
109
+ readonly id: string;
110
+ readonly kind: string;
111
+ readonly label: string;
112
+ readonly enabled: boolean;
113
+ readonly state: string;
114
+ readonly capabilities: readonly string[];
115
+ readonly metadata: Readonly<Record<string, unknown>>;
116
+ }
117
+
118
+ export interface OnboardingSurfacesSnapshot {
119
+ readonly configuredEnabledKinds: readonly string[];
120
+ readonly records: readonly OnboardingSurfaceRecord[];
121
+ }
122
+
123
+ export interface OnboardingProviderAccountRecord {
124
+ readonly providerId: string;
125
+ readonly configured: boolean;
126
+ readonly active: boolean;
127
+ readonly oauthReady: boolean;
128
+ readonly pendingLogin: boolean;
129
+ readonly availableRoutes: readonly string[];
130
+ readonly activeRoute: string;
131
+ readonly authFreshness: string;
132
+ }
133
+
134
+ export interface OnboardingProviderAccountsSnapshot {
135
+ readonly capturedAt: number;
136
+ readonly configuredCount: number;
137
+ readonly issueCount: number;
138
+ readonly providers: readonly OnboardingProviderAccountRecord[];
139
+ }
140
+
141
+ export interface OnboardingRuntimeDefaultsSnapshot {
142
+ readonly providerReasoningEffort: GoodVibesConfig['provider']['reasoningEffort'];
143
+ readonly permissionsMode: GoodVibesConfig['permissions']['mode'];
144
+ readonly behavior: GoodVibesConfig['behavior'];
145
+ readonly display: GoodVibesConfig['display'];
146
+ readonly secretStoragePolicy: GoodVibesConfig['storage']['secretPolicy'];
147
+ }
148
+
149
+ export interface OnboardingAcknowledgementSnapshot {
150
+ readonly scope: OnboardingCompletionMarkerScope;
151
+ readonly exists: boolean;
152
+ readonly updatedAt: number | null;
153
+ readonly source: string | null;
154
+ readonly mode?: OnboardingMode;
155
+ readonly workspaceRoot?: string;
156
+ readonly accepted: Partial<Record<OnboardingAcknowledgementTarget, boolean>>;
157
+ }
158
+
159
+ export type OnboardingSnapshotCollectionIssueArea =
160
+ | 'services'
161
+ | 'subscriptions-active'
162
+ | 'subscriptions-pending'
163
+ | 'auth'
164
+ | 'secrets-review'
165
+ | 'secrets-records'
166
+ | 'surfaces'
167
+ | 'provider-accounts'
168
+ | 'acknowledgements';
169
+
170
+ export interface OnboardingSnapshotCollectionIssue {
171
+ readonly area: OnboardingSnapshotCollectionIssueArea;
172
+ readonly message: string;
173
+ }
174
+
175
+ export interface OnboardingSnapshotState {
176
+ readonly capturedAt: number;
177
+ readonly config: OnboardingConfigSnapshot;
178
+ readonly providerRouting: OnboardingProviderRoutingSnapshot;
179
+ readonly runtimeDefaults: OnboardingRuntimeDefaultsSnapshot;
180
+ readonly acknowledgements: OnboardingAcknowledgementSnapshot;
181
+ readonly services: OnboardingServicesSnapshot;
182
+ readonly subscriptions: OnboardingSubscriptionsSnapshot;
183
+ readonly secrets: OnboardingSecretsSnapshot;
184
+ readonly auth: OnboardingAuthSnapshot;
185
+ readonly bindSettings: OnboardingBindSettingsSnapshot;
186
+ readonly surfaces: OnboardingSurfacesSnapshot;
187
+ readonly providerAccounts: OnboardingProviderAccountsSnapshot | null;
188
+ readonly collectionIssues: readonly OnboardingSnapshotCollectionIssue[];
189
+ }
190
+
191
+ export type OnboardingStep1CapabilityId =
192
+ | 'local-tui-only'
193
+ | 'browser-access'
194
+ | 'network-access'
195
+ | 'webhook-events'
196
+ | 'external-integrations';
197
+
198
+ export interface OnboardingStep1CapabilityItem {
199
+ readonly id: OnboardingStep1CapabilityId;
200
+ readonly label: string;
201
+ readonly selected: boolean;
202
+ readonly detail: string;
203
+ }
204
+
205
+ export interface OnboardingStep1CapabilityFlags {
206
+ readonly providers: boolean;
207
+ readonly services: boolean;
208
+ readonly subscriptions: boolean;
209
+ readonly auth: boolean;
210
+ readonly controlPlane: boolean;
211
+ readonly httpListener: boolean;
212
+ readonly web: boolean;
213
+ readonly surfaces: boolean;
214
+ }
215
+
216
+ export interface OnboardingAcknowledgementState {
217
+ readonly required: boolean;
218
+ readonly accepted: boolean;
219
+ readonly reason: OnboardingAcknowledgementReason;
220
+ readonly detail: string;
221
+ }
222
+
223
+ export interface OnboardingReopenEditAcknowledgementState {
224
+ readonly providers: OnboardingAcknowledgementState;
225
+ readonly subscriptions: OnboardingAcknowledgementState;
226
+ readonly auth: OnboardingAcknowledgementState;
227
+ }
228
+
229
+ export interface OnboardingStepDerivationState {
230
+ readonly step1Capabilities: readonly OnboardingStep1CapabilityItem[];
231
+ readonly step1_5NetworkMode: OnboardingNetworkMode;
232
+ readonly reopenEditAcknowledgements: OnboardingReopenEditAcknowledgementState;
233
+ }
234
+
235
+ export type OnboardingApplyOperation =
236
+ | {
237
+ readonly kind: 'set-config';
238
+ readonly key: ConfigKey;
239
+ readonly value: unknown;
240
+ readonly scope?: 'global' | 'project';
241
+ }
242
+ | {
243
+ readonly kind: 'set-secret';
244
+ readonly key: string;
245
+ readonly value: string;
246
+ readonly scope?: 'project' | 'user';
247
+ readonly medium: 'secure' | 'plaintext';
248
+ }
249
+ | {
250
+ readonly kind: 'ensure-auth-user';
251
+ readonly username: string;
252
+ readonly password: string;
253
+ readonly roles?: readonly string[];
254
+ readonly createSession?: boolean;
255
+ readonly retireBootstrapCredential?: boolean;
256
+ }
257
+ | {
258
+ readonly kind: 'acknowledge';
259
+ readonly target: OnboardingAcknowledgementTarget;
260
+ readonly acknowledged: boolean;
261
+ }
262
+ | {
263
+ readonly kind: 'set-completion-marker';
264
+ readonly scope: OnboardingCompletionMarkerScope;
265
+ readonly completed: boolean;
266
+ readonly payload?: Partial<OnboardingCompletionMarkerPayload>;
267
+ };
268
+
269
+ export interface OnboardingApplyRequest {
270
+ readonly mode: OnboardingMode;
271
+ readonly source: string;
272
+ readonly operations: readonly OnboardingApplyOperation[];
273
+ }
274
+
275
+ export interface OnboardingAppliedOperation {
276
+ readonly kind: OnboardingApplyOperation['kind'];
277
+ readonly summary: string;
278
+ }
279
+
280
+ export interface OnboardingSkippedOperation {
281
+ readonly kind: OnboardingApplyOperation['kind'];
282
+ readonly reason: string;
283
+ }
284
+
285
+ export interface OnboardingApplyError {
286
+ readonly kind: OnboardingApplyOperation['kind'];
287
+ readonly message: string;
288
+ }
289
+
290
+ export interface OnboardingApplyResult {
291
+ readonly ok: boolean;
292
+ readonly applied: readonly OnboardingAppliedOperation[];
293
+ readonly skipped: readonly OnboardingSkippedOperation[];
294
+ readonly errors: readonly OnboardingApplyError[];
295
+ }
296
+
297
+ export interface OnboardingVerificationItem {
298
+ readonly id: string;
299
+ readonly status: OnboardingVerificationStatus;
300
+ readonly message: string;
301
+ readonly target?: string;
302
+ }
303
+
304
+ export interface OnboardingVerificationResult {
305
+ readonly verifiedAt: number;
306
+ readonly ok: boolean;
307
+ readonly items: readonly OnboardingVerificationItem[];
308
+ }
309
+
310
+ export interface OnboardingAcknowledgementRuntimeState {
311
+ readonly version: 1;
312
+ readonly updatedAt: number;
313
+ readonly source: string;
314
+ readonly mode?: OnboardingMode;
315
+ readonly workspaceRoot?: string;
316
+ readonly acknowledgements: Partial<Record<OnboardingAcknowledgementTarget, boolean>>;
317
+ }
318
+
319
+ export interface OnboardingCompletionMarkerPayload {
320
+ readonly version: 1;
321
+ readonly completedAt: number;
322
+ readonly updatedAt: number;
323
+ readonly source: OnboardingCompletionMarkerSource;
324
+ readonly mode?: OnboardingMode;
325
+ readonly workspaceRoot?: string;
326
+ }
327
+
328
+ export interface OnboardingCompletionMarkerState {
329
+ readonly scope: OnboardingCompletionMarkerScope;
330
+ readonly path: string;
331
+ readonly exists: boolean;
332
+ readonly payload: OnboardingCompletionMarkerPayload | null;
333
+ readonly parseError?: string;
334
+ }
335
+
336
+ export interface OnboardingCompletionMarkersState {
337
+ readonly user: OnboardingCompletionMarkerState;
338
+ readonly project: OnboardingCompletionMarkerState;
339
+ readonly effective: OnboardingCompletionMarkerState | null;
340
+ }
341
+
342
+ export interface WriteOnboardingCompletionMarkerOptions {
343
+ readonly scope?: OnboardingCompletionMarkerScope;
344
+ readonly completedAt?: number;
345
+ readonly updatedAt?: number;
346
+ readonly source?: OnboardingCompletionMarkerSource;
347
+ readonly mode?: OnboardingMode;
348
+ readonly workspaceRoot?: string;
349
+ }
350
+
351
+ export interface OnboardingSurfaceReadHelper {
352
+ list(): readonly OnboardingSurfaceRecord[] | Promise<readonly OnboardingSurfaceRecord[]>;
353
+ }
354
+
355
+ export interface OnboardingProviderAccountReadHelper {
356
+ loadSnapshot(): Promise<OnboardingProviderAccountsSnapshot>;
357
+ }
358
+
359
+ export type OnboardingShellPaths = Pick<
360
+ ShellPathService,
361
+ 'workingDirectory' | 'resolveProjectPath' | 'resolveUserPath'
362
+ >;
363
+
364
+ export interface OnboardingSnapshotDependencies {
365
+ readonly clock?: () => number;
366
+ readonly config: Pick<ConfigManager, 'get' | 'getCategory'>;
367
+ readonly subscriptions: Pick<SubscriptionAccessQuery, 'list' | 'listPending' | 'get' | 'getPending'>;
368
+ readonly secrets: Pick<SecretsManager, 'inspect' | 'listDetailed'>;
369
+ readonly auth: Pick<LocalAuthInspectionQuery, 'inspect'>;
370
+ readonly services: Pick<ServiceInspectionQuery, 'getAll' | 'inspect'>;
371
+ readonly surfaces?: OnboardingSurfaceReadHelper;
372
+ readonly providerAccounts?: OnboardingProviderAccountReadHelper;
373
+ readonly shellPaths?: OnboardingShellPaths;
374
+ readonly acknowledgementScope?: OnboardingCompletionMarkerScope;
375
+ }
376
+
377
+ export interface OnboardingApplyDependencies {
378
+ readonly clock?: () => number;
379
+ readonly config: Pick<ConfigManager, 'get' | 'getRaw' | 'load' | 'setDynamic'>;
380
+ readonly secrets?: Pick<SecretsManager, 'delete' | 'get' | 'inspect' | 'set'>;
381
+ readonly auth?: Pick<
382
+ UserAuthManager,
383
+ 'addUser'
384
+ | 'clearBootstrapCredentialFile'
385
+ | 'createSession'
386
+ | 'deleteUser'
387
+ | 'getUser'
388
+ | 'inspect'
389
+ | 'revokeSession'
390
+ >;
391
+ readonly shellPaths: OnboardingShellPaths;
392
+ readonly acknowledgementScope?: OnboardingCompletionMarkerScope;
393
+ }
394
+
395
+ export interface OnboardingVerificationDependencies {
396
+ readonly clock?: () => number;
397
+ readonly config: Pick<ConfigManager, 'get'>;
398
+ readonly secrets?: Pick<SecretsManager, 'get'>;
399
+ readonly auth?: Pick<UserAuthManager, 'inspect'>;
400
+ readonly shellPaths: OnboardingShellPaths;
401
+ readonly acknowledgementScope?: OnboardingCompletionMarkerScope;
402
+ }
@@ -0,0 +1,233 @@
1
+ import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
2
+ import { readOnboardingCompletionMarker } from './markers.ts';
3
+ import { readOnboardingRuntimeState } from './state.ts';
4
+ import type {
5
+ OnboardingApplyOperation,
6
+ OnboardingApplyRequest,
7
+ OnboardingVerificationDependencies,
8
+ OnboardingVerificationItem,
9
+ OnboardingVerificationResult,
10
+ } from './types.ts';
11
+
12
+ function getNow(deps: Pick<OnboardingVerificationDependencies, 'clock'>): number {
13
+ return deps.clock?.() ?? Date.now();
14
+ }
15
+
16
+ function normalizeCompletionSource(source: string): 'wizard' | 'command' | 'import' | 'unknown' {
17
+ if (source === 'wizard' || source === 'command' || source === 'import') return source;
18
+ return 'unknown';
19
+ }
20
+
21
+ function isDeepEqual(left: unknown, right: unknown): boolean {
22
+ if (Object.is(left, right)) return true;
23
+ if (Array.isArray(left) && Array.isArray(right)) {
24
+ return left.length === right.length && left.every((value, index) => isDeepEqual(value, right[index]));
25
+ }
26
+
27
+ if (
28
+ typeof left === 'object' && left !== null
29
+ && typeof right === 'object' && right !== null
30
+ && !Array.isArray(left)
31
+ && !Array.isArray(right)
32
+ ) {
33
+ const leftEntries = Object.entries(left);
34
+ const rightEntries = Object.entries(right);
35
+ if (leftEntries.length !== rightEntries.length) return false;
36
+
37
+ return leftEntries.every(([key, value]) => isDeepEqual(value, (right as Record<string, unknown>)[key]));
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ function isGoodVibesSecretRefInput(value: string): boolean {
44
+ const normalized = value.trim();
45
+ return normalized.startsWith('goodvibes://secrets/') && isSecretRefInput(normalized);
46
+ }
47
+
48
+ function verifyConfigOperation(
49
+ deps: OnboardingVerificationDependencies,
50
+ operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>,
51
+ ): OnboardingVerificationItem {
52
+ const actual = deps.config.get(operation.key);
53
+ const ok = isDeepEqual(actual, operation.value);
54
+
55
+ return {
56
+ id: `config:${operation.key}`,
57
+ status: ok ? 'pass' : 'fail',
58
+ message: ok
59
+ ? `${operation.key} matches the requested onboarding value.`
60
+ : `${operation.key} does not match the requested onboarding value.`,
61
+ target: operation.key,
62
+ };
63
+ }
64
+
65
+ function verifyAcknowledgementOperation(
66
+ deps: OnboardingVerificationDependencies,
67
+ operation: Extract<OnboardingApplyOperation, { kind: 'acknowledge' }>,
68
+ ): OnboardingVerificationItem {
69
+ const state = readOnboardingRuntimeState(deps.shellPaths, deps.acknowledgementScope ?? 'project');
70
+ if (state.parseError) {
71
+ return {
72
+ id: `acknowledge:${operation.target}`,
73
+ status: 'fail',
74
+ message: `Onboarding acknowledgement state could not be parsed: ${state.parseError}`,
75
+ target: operation.target,
76
+ };
77
+ }
78
+
79
+ const actual = state.payload?.acknowledgements[operation.target] ?? false;
80
+ const ok = actual === operation.acknowledged;
81
+
82
+ return {
83
+ id: `acknowledge:${operation.target}`,
84
+ status: ok ? 'pass' : 'fail',
85
+ message: ok
86
+ ? `${operation.target} acknowledgement matches the requested onboarding state.`
87
+ : `${operation.target} acknowledgement does not match the requested onboarding state.`,
88
+ target: operation.target,
89
+ };
90
+ }
91
+
92
+ function verifyCompletionMarkerOperation(
93
+ deps: OnboardingVerificationDependencies,
94
+ request: OnboardingApplyRequest,
95
+ operation: Extract<OnboardingApplyOperation, { kind: 'set-completion-marker' }>,
96
+ ): OnboardingVerificationItem {
97
+ const marker = readOnboardingCompletionMarker(deps.shellPaths, operation.scope);
98
+ if (marker.parseError) {
99
+ return {
100
+ id: `marker:${operation.scope}`,
101
+ status: 'fail',
102
+ message: `Onboarding completion marker could not be parsed: ${marker.parseError}`,
103
+ target: operation.scope,
104
+ };
105
+ }
106
+
107
+ if (!operation.completed) {
108
+ return {
109
+ id: `marker:${operation.scope}`,
110
+ status: marker.exists ? 'fail' : 'pass',
111
+ message: marker.exists
112
+ ? `${operation.scope} onboarding completion marker still exists.`
113
+ : `${operation.scope} onboarding completion marker is cleared.`,
114
+ target: operation.scope,
115
+ };
116
+ }
117
+
118
+ if (!marker.payload) {
119
+ return {
120
+ id: `marker:${operation.scope}`,
121
+ status: 'fail',
122
+ message: `${operation.scope} onboarding completion marker is missing.`,
123
+ target: operation.scope,
124
+ };
125
+ }
126
+
127
+ const expectedSource = operation.payload?.source ?? normalizeCompletionSource(request.source);
128
+ const expectedMode = operation.payload?.mode ?? request.mode;
129
+ const expectedWorkspaceRoot = operation.payload?.workspaceRoot ?? deps.shellPaths.workingDirectory;
130
+ const payloadMatches = marker.payload.source === expectedSource
131
+ && (expectedMode === undefined || marker.payload.mode === expectedMode)
132
+ && (expectedWorkspaceRoot === undefined || marker.payload.workspaceRoot === expectedWorkspaceRoot)
133
+ && (operation.payload?.completedAt === undefined || marker.payload.completedAt === operation.payload.completedAt)
134
+ && (operation.payload?.updatedAt === undefined || marker.payload.updatedAt === operation.payload.updatedAt);
135
+
136
+ return {
137
+ id: `marker:${operation.scope}`,
138
+ status: payloadMatches ? 'pass' : 'fail',
139
+ message: payloadMatches
140
+ ? `${operation.scope} onboarding completion marker matches the requested state.`
141
+ : `${operation.scope} onboarding completion marker does not match the requested state.`,
142
+ target: operation.scope,
143
+ };
144
+ }
145
+
146
+ async function verifySecretOperation(
147
+ deps: OnboardingVerificationDependencies,
148
+ operation: Extract<OnboardingApplyOperation, { kind: 'set-secret' }>,
149
+ ): Promise<OnboardingVerificationItem> {
150
+ if (!deps.secrets) {
151
+ return {
152
+ id: `secret:${operation.key}`,
153
+ status: 'fail',
154
+ message: 'Secret manager is unavailable.',
155
+ target: operation.key,
156
+ };
157
+ }
158
+
159
+ const actual = await deps.secrets.get(operation.key);
160
+ const ok = isGoodVibesSecretRefInput(operation.value)
161
+ ? actual !== null
162
+ : actual === operation.value;
163
+ return {
164
+ id: `secret:${operation.key}`,
165
+ status: ok ? 'pass' : 'fail',
166
+ message: ok
167
+ ? isGoodVibesSecretRefInput(operation.value)
168
+ ? `${operation.key} resolves through the stored GoodVibes secret reference.`
169
+ : `${operation.key} was stored through the secret manager.`
170
+ : `${operation.key} did not match the requested secret value.`,
171
+ target: operation.key,
172
+ };
173
+ }
174
+
175
+ function verifyAuthOperation(
176
+ deps: OnboardingVerificationDependencies,
177
+ operation: Extract<OnboardingApplyOperation, { kind: 'ensure-auth-user' }>,
178
+ ): OnboardingVerificationItem {
179
+ if (!deps.auth) {
180
+ return {
181
+ id: `auth:${operation.username}`,
182
+ status: 'fail',
183
+ message: 'Local auth manager is unavailable.',
184
+ target: operation.username,
185
+ };
186
+ }
187
+
188
+ const snapshot = deps.auth.inspect();
189
+ const username = operation.username.trim();
190
+ const user = snapshot.users.find((entry) => entry.username === username);
191
+ const requiredRoles = operation.roles ?? ['admin'];
192
+ const userExists = Boolean(user) && requiredRoles.every((role) => user!.roles.includes(role));
193
+ const sessionExists = operation.createSession === false
194
+ ? true
195
+ : snapshot.sessions.some((session) => session.username === username);
196
+ const bootstrapRetired = operation.retireBootstrapCredential
197
+ ? snapshot.bootstrapCredentialPresent === false
198
+ : true;
199
+ const ok = userExists && sessionExists && bootstrapRetired;
200
+ return {
201
+ id: `auth:${username}`,
202
+ status: ok ? 'pass' : 'fail',
203
+ message: ok
204
+ ? `${username} local auth user has required role(s) and session state.`
205
+ : `${username} local auth user/session/role/bootstrap state was not created.`,
206
+ target: username,
207
+ };
208
+ }
209
+
210
+ async function verifyOperation(
211
+ deps: OnboardingVerificationDependencies,
212
+ request: OnboardingApplyRequest,
213
+ operation: OnboardingApplyOperation,
214
+ ): Promise<OnboardingVerificationItem> {
215
+ if (operation.kind === 'set-config') return verifyConfigOperation(deps, operation);
216
+ if (operation.kind === 'set-secret') return verifySecretOperation(deps, operation);
217
+ if (operation.kind === 'ensure-auth-user') return verifyAuthOperation(deps, operation);
218
+ if (operation.kind === 'acknowledge') return verifyAcknowledgementOperation(deps, operation);
219
+ return verifyCompletionMarkerOperation(deps, request, operation);
220
+ }
221
+
222
+ export async function verifyOnboardingRequest(
223
+ deps: OnboardingVerificationDependencies,
224
+ request: OnboardingApplyRequest,
225
+ ): Promise<OnboardingVerificationResult> {
226
+ const items = await Promise.all(request.operations.map((operation) => verifyOperation(deps, request, operation)));
227
+
228
+ return {
229
+ verifiedAt: getNow(deps),
230
+ ok: items.every((item) => item.status === 'pass'),
231
+ items,
232
+ };
233
+ }
@@ -48,12 +48,27 @@ export interface UiPlatformServices {
48
48
  readonly localUserAuthManager: RuntimeServices['localUserAuthManager'];
49
49
  readonly mcpRegistry: RuntimeServices['mcpRegistry'];
50
50
  readonly serviceRegistry: RuntimeServices['serviceRegistry'];
51
+ readonly surfaceRegistry: RuntimeServices['surfaceRegistry'];
51
52
  readonly subscriptionManager: RuntimeServices['subscriptionManager'];
52
53
  readonly secretsManager: SecretsManager;
53
54
  readonly tokenAuditor: RuntimeServices['tokenAuditor'];
54
55
  readonly replayEngine: RuntimeServices['replayEngine'];
55
56
  readonly webhookNotifier: RuntimeServices['webhookNotifier'];
56
57
  readonly policyRuntimeState: RuntimeServices['policyRuntimeState'];
58
+ readonly externalServices?: {
59
+ inspect(): {
60
+ readonly daemonRunning: boolean;
61
+ readonly daemonPortInUse?: boolean;
62
+ readonly httpListenerRunning: boolean;
63
+ readonly httpListenerPortInUse?: boolean;
64
+ };
65
+ restart(): Promise<{
66
+ readonly daemonRunning: boolean;
67
+ readonly daemonPortInUse?: boolean;
68
+ readonly httpListenerRunning: boolean;
69
+ readonly httpListenerPortInUse?: boolean;
70
+ }>;
71
+ };
57
72
  }
58
73
 
59
74
  export interface UiPlanningServices {
@@ -138,6 +153,7 @@ export function createUiRuntimeServices(
138
153
  localUserAuthManager: runtimeServices.localUserAuthManager,
139
154
  mcpRegistry: runtimeServices.mcpRegistry,
140
155
  serviceRegistry: runtimeServices.serviceRegistry,
156
+ surfaceRegistry: runtimeServices.surfaceRegistry,
141
157
  subscriptionManager: runtimeServices.subscriptionManager,
142
158
  secretsManager: runtimeServices.secretsManager,
143
159
  tokenAuditor: runtimeServices.tokenAuditor,
@@ -126,7 +126,10 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
126
126
  input.modalOpened('modelPicker');
127
127
  input.modelPicker.openAllModels(models, runtime.model);
128
128
  render();
129
- })();
129
+ })().catch((error: unknown) => {
130
+ commandContext.print?.(`Model picker failed to open: ${error instanceof Error ? error.message : String(error)}`);
131
+ render();
132
+ });
130
133
  };
131
134
 
132
135
  commandContext.openProviderPicker = () => {
@@ -139,13 +142,20 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
139
142
  input.modalOpened('modelPicker');
140
143
  input.modelPicker.openProviders(providers, runtime.provider);
141
144
  render();
142
- })();
145
+ })().catch((error: unknown) => {
146
+ commandContext.print?.(`Provider picker failed to open: ${error instanceof Error ? error.message : String(error)}`);
147
+ render();
148
+ });
143
149
  };
144
150
 
145
151
  commandContext.openSelection = (title, items, opts, callback) => {
146
152
  input.openSelection(title, items, opts, callback);
147
153
  };
148
154
 
155
+ commandContext.openOnboardingWizard = (modeOrOptions) => {
156
+ input.openOnboardingWizard(modeOrOptions);
157
+ };
158
+
149
159
  commandContext.openContextInspector = () => {
150
160
  input.modalOpened('contextInspector');
151
161
  input.contextInspectorModal.open();