@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
@@ -240,22 +240,22 @@ function hasExternalIntegrations(snapshot: OnboardingSnapshotState): boolean {
240
240
 
241
241
  function describeLocalTuiOnly(snapshot: OnboardingSnapshotState): string {
242
242
  if (!hasAnyServerEnabled(snapshot)) {
243
- return 'Keep GoodVibes in this terminal and disable browser access, background services, network listeners, and external surfaces.';
243
+ return 'Use GoodVibes only in this terminal. No browser access, background service, HTTP listener, external app surface, or network setup.';
244
244
  }
245
245
 
246
- return 'Switching to this disables browser access, background services, network listeners, and external surfaces.';
246
+ return 'Turn off browser access, background services, HTTP listeners, external app surfaces, and network setup.';
247
247
  }
248
248
 
249
249
  function describeBrowserAccess(snapshot: OnboardingSnapshotState): string {
250
250
  return snapshot.bindSettings.web.enabled
251
- ? 'Keep the background service and web UI enabled, reachable according to the network step.'
252
- : 'Enable the background service and web UI, reachable on the local network by default unless customized.';
251
+ ? 'Keep the background service and web UI enabled. Network reachability is controlled on the next screen.'
252
+ : 'Run the background service and web UI. GoodVibes will use the local network by default; you can restrict or customize it next.';
253
253
  }
254
254
 
255
255
  function describeRemoteDeviceAccess(snapshot: OnboardingSnapshotState): string {
256
256
  return hasRemoteDeviceAccess(snapshot)
257
- ? 'Keep enabled GoodVibes services reachable from other devices on your LAN. Local auth is required.'
258
- : 'Expose enabled GoodVibes services on your LAN so other devices can reach them. Local auth is required.';
257
+ ? 'Keep enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.'
258
+ : 'Make enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.';
259
259
  }
260
260
 
261
261
  function describeWebhookIngress(snapshot: OnboardingSnapshotState): string {
@@ -272,7 +272,7 @@ function describeExternalIntegrations(snapshot: OnboardingSnapshotState): string
272
272
  ]).size;
273
273
 
274
274
  if (integrationCount === 0) {
275
- return 'Show Slack, Discord, Telegram, Teams, Matrix, and other app surfaces so they can be enabled and configured here.';
275
+ return 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.';
276
276
  }
277
277
 
278
278
  return `Review and configure ${integrationCount} detected external app, service, or surface integration signal(s).`;
@@ -1,15 +1,15 @@
1
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import type { ShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
4
4
  import type {
5
- OnboardingCompletionMarkerPayload,
6
- OnboardingCompletionMarkerScope,
7
- OnboardingCompletionMarkerState,
8
- OnboardingCompletionMarkersState,
9
- WriteOnboardingCompletionMarkerOptions,
5
+ OnboardingCheckMarkerPayload,
6
+ OnboardingCheckMarkerState,
7
+ OnboardingCheckMarkersState,
8
+ OnboardingStateScope,
9
+ WriteOnboardingCheckMarkerOptions,
10
10
  } from './types.ts';
11
11
 
12
- const ONBOARDING_COMPLETION_MARKER_FILE = 'onboarding-complete.json';
12
+ const ONBOARDING_CHECK_MARKER_FILE = 'onboarding-checked.json';
13
13
 
14
14
  type OnboardingShellPaths = Pick<
15
15
  ShellPathService,
@@ -18,26 +18,26 @@ type OnboardingShellPaths = Pick<
18
18
 
19
19
  function resolveMarkerPath(
20
20
  shellPaths: OnboardingShellPaths,
21
- scope: OnboardingCompletionMarkerScope,
21
+ scope: OnboardingStateScope,
22
22
  ): string {
23
23
  return scope === 'project'
24
- ? shellPaths.resolveProjectPath('tui', ONBOARDING_COMPLETION_MARKER_FILE)
25
- : shellPaths.resolveUserPath('tui', ONBOARDING_COMPLETION_MARKER_FILE);
24
+ ? shellPaths.resolveProjectPath('tui', ONBOARDING_CHECK_MARKER_FILE)
25
+ : shellPaths.resolveUserPath('tui', ONBOARDING_CHECK_MARKER_FILE);
26
26
  }
27
27
 
28
28
  function isObject(value: unknown): value is Record<string, unknown> {
29
29
  return typeof value === 'object' && value !== null;
30
30
  }
31
31
 
32
- function isOnboardingMode(value: unknown): value is OnboardingCompletionMarkerPayload['mode'] {
32
+ function isOnboardingMode(value: unknown): value is OnboardingCheckMarkerPayload['mode'] {
33
33
  return value === 'new' || value === 'edit' || value === 'reopen';
34
34
  }
35
35
 
36
- function isCompletionMarkerPayload(value: unknown): value is OnboardingCompletionMarkerPayload {
36
+ function isCheckMarkerPayload(value: unknown): value is OnboardingCheckMarkerPayload {
37
37
  return isObject(value)
38
38
  && value.version === 1
39
- && typeof value.completedAt === 'number'
40
- && Number.isFinite(value.completedAt)
39
+ && typeof value.checkedAt === 'number'
40
+ && Number.isFinite(value.checkedAt)
41
41
  && typeof value.updatedAt === 'number'
42
42
  && Number.isFinite(value.updatedAt)
43
43
  && typeof value.source === 'string'
@@ -46,9 +46,9 @@ function isCompletionMarkerPayload(value: unknown): value is OnboardingCompletio
46
46
  }
47
47
 
48
48
  function buildMissingMarkerState(
49
- scope: OnboardingCompletionMarkerScope,
49
+ scope: OnboardingStateScope,
50
50
  path: string,
51
- ): OnboardingCompletionMarkerState {
51
+ ): OnboardingCheckMarkerState {
52
52
  return {
53
53
  scope,
54
54
  path,
@@ -58,10 +58,10 @@ function buildMissingMarkerState(
58
58
  }
59
59
 
60
60
  function buildParseErrorState(
61
- scope: OnboardingCompletionMarkerScope,
61
+ scope: OnboardingStateScope,
62
62
  path: string,
63
63
  parseError: string,
64
- ): OnboardingCompletionMarkerState {
64
+ ): OnboardingCheckMarkerState {
65
65
  return {
66
66
  scope,
67
67
  path,
@@ -72,34 +72,31 @@ function buildParseErrorState(
72
72
  }
73
73
 
74
74
  function pickEffectiveMarker(
75
- project: OnboardingCompletionMarkerState,
76
- user: OnboardingCompletionMarkerState,
77
- ): OnboardingCompletionMarkerState | null {
78
- if (project.payload) return project;
75
+ user: OnboardingCheckMarkerState,
76
+ ): OnboardingCheckMarkerState | null {
79
77
  if (user.payload) return user;
80
- if (project.exists) return project;
81
78
  if (user.exists) return user;
82
79
  return null;
83
80
  }
84
81
 
85
- export function getOnboardingCompletionMarkerPath(
82
+ export function getOnboardingCheckMarkerPath(
86
83
  shellPaths: OnboardingShellPaths,
87
- scope: OnboardingCompletionMarkerScope = 'user',
84
+ scope: OnboardingStateScope = 'user',
88
85
  ): string {
89
86
  return resolveMarkerPath(shellPaths, scope);
90
87
  }
91
88
 
92
- export function readOnboardingCompletionMarker(
89
+ export function readOnboardingCheckMarker(
93
90
  shellPaths: OnboardingShellPaths,
94
- scope: OnboardingCompletionMarkerScope = 'user',
95
- ): OnboardingCompletionMarkerState {
91
+ scope: OnboardingStateScope = 'user',
92
+ ): OnboardingCheckMarkerState {
96
93
  const path = resolveMarkerPath(shellPaths, scope);
97
94
  if (!existsSync(path)) return buildMissingMarkerState(scope, path);
98
95
 
99
96
  try {
100
97
  const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
101
- if (!isCompletionMarkerPayload(parsed)) {
102
- return buildParseErrorState(scope, path, 'Invalid onboarding completion marker payload.');
98
+ if (!isCheckMarkerPayload(parsed)) {
99
+ return buildParseErrorState(scope, path, 'Invalid onboarding check marker payload.');
103
100
  }
104
101
 
105
102
  return {
@@ -114,48 +111,37 @@ export function readOnboardingCompletionMarker(
114
111
  }
115
112
  }
116
113
 
117
- export function readOnboardingCompletionMarkers(
114
+ export function readOnboardingCheckMarkers(
118
115
  shellPaths: OnboardingShellPaths,
119
- ): OnboardingCompletionMarkersState {
120
- const user = readOnboardingCompletionMarker(shellPaths, 'user');
121
- const project = readOnboardingCompletionMarker(shellPaths, 'project');
116
+ ): OnboardingCheckMarkersState {
117
+ const user = readOnboardingCheckMarker(shellPaths, 'user');
118
+ const project = readOnboardingCheckMarker(shellPaths, 'project');
122
119
 
123
120
  return {
124
121
  user,
125
122
  project,
126
- effective: pickEffectiveMarker(project, user),
123
+ effective: pickEffectiveMarker(user),
127
124
  };
128
125
  }
129
126
 
130
- export function writeOnboardingCompletionMarker(
127
+ export function writeOnboardingCheckMarker(
131
128
  shellPaths: OnboardingShellPaths,
132
- options: WriteOnboardingCompletionMarkerOptions = {},
133
- ): OnboardingCompletionMarkerState {
129
+ options: WriteOnboardingCheckMarkerOptions = {},
130
+ ): OnboardingCheckMarkerState {
134
131
  const scope = options.scope ?? 'user';
135
132
  const path = resolveMarkerPath(shellPaths, scope);
136
- const completedAt = options.completedAt ?? Date.now();
137
- const payload: OnboardingCompletionMarkerPayload = {
133
+ const checkedAt = options.checkedAt ?? Date.now();
134
+ const payload: OnboardingCheckMarkerPayload = {
138
135
  version: 1,
139
- completedAt,
140
- updatedAt: options.updatedAt ?? completedAt,
136
+ checkedAt,
137
+ updatedAt: options.updatedAt ?? checkedAt,
141
138
  source: options.source ?? 'wizard',
142
139
  ...(options.mode ? { mode: options.mode } : {}),
143
- ...(options.workspaceRoot ?? shellPaths.workingDirectory
144
- ? { workspaceRoot: options.workspaceRoot ?? shellPaths.workingDirectory }
145
- : {}),
140
+ ...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}),
146
141
  };
147
142
 
148
143
  mkdirSync(dirname(path), { recursive: true });
149
144
  writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
150
145
 
151
- return readOnboardingCompletionMarker(shellPaths, scope);
152
- }
153
-
154
- export function clearOnboardingCompletionMarker(
155
- shellPaths: OnboardingShellPaths,
156
- scope: OnboardingCompletionMarkerScope = 'user',
157
- ): OnboardingCompletionMarkerState {
158
- const path = resolveMarkerPath(shellPaths, scope);
159
- if (existsSync(path)) unlinkSync(path);
160
- return buildMissingMarkerState(scope, path);
146
+ return readOnboardingCheckMarker(shellPaths, scope);
161
147
  }
@@ -40,6 +40,7 @@ function buildConfigSnapshot(
40
40
  network: config.getCategory('network'),
41
41
  surfaces: config.getCategory('surfaces'),
42
42
  service: config.getCategory('service'),
43
+ featureFlags: config.getCategory('featureFlags'),
43
44
  };
44
45
  }
45
46
 
@@ -3,15 +3,15 @@ import { dirname } from 'node:path';
3
3
  import type {
4
4
  OnboardingAcknowledgementRuntimeState,
5
5
  OnboardingAcknowledgementTarget,
6
- OnboardingCompletionMarkerScope,
7
6
  OnboardingMode,
8
7
  OnboardingShellPaths,
8
+ OnboardingStateScope,
9
9
  } from './types.ts';
10
10
 
11
11
  const ONBOARDING_RUNTIME_STATE_FILE = 'onboarding-state.json';
12
12
 
13
13
  export interface OnboardingRuntimeStateRecord {
14
- readonly scope: OnboardingCompletionMarkerScope;
14
+ readonly scope: OnboardingStateScope;
15
15
  readonly path: string;
16
16
  readonly exists: boolean;
17
17
  readonly payload: OnboardingAcknowledgementRuntimeState | null;
@@ -19,7 +19,7 @@ export interface OnboardingRuntimeStateRecord {
19
19
  }
20
20
 
21
21
  interface WriteOnboardingAcknowledgementStateOptions {
22
- readonly scope?: OnboardingCompletionMarkerScope;
22
+ readonly scope?: OnboardingStateScope;
23
23
  readonly target: OnboardingAcknowledgementTarget;
24
24
  readonly acknowledged: boolean;
25
25
  readonly updatedAt?: number;
@@ -30,7 +30,7 @@ interface WriteOnboardingAcknowledgementStateOptions {
30
30
 
31
31
  function resolveStatePath(
32
32
  shellPaths: OnboardingShellPaths,
33
- scope: OnboardingCompletionMarkerScope,
33
+ scope: OnboardingStateScope,
34
34
  ): string {
35
35
  return scope === 'project'
36
36
  ? shellPaths.resolveProjectPath('tui', ONBOARDING_RUNTIME_STATE_FILE)
@@ -63,14 +63,14 @@ function isRuntimeStatePayload(value: unknown): value is OnboardingAcknowledgeme
63
63
 
64
64
  export function getOnboardingRuntimeStatePath(
65
65
  shellPaths: OnboardingShellPaths,
66
- scope: OnboardingCompletionMarkerScope = 'project',
66
+ scope: OnboardingStateScope = 'project',
67
67
  ): string {
68
68
  return resolveStatePath(shellPaths, scope);
69
69
  }
70
70
 
71
71
  export function readOnboardingRuntimeState(
72
72
  shellPaths: OnboardingShellPaths,
73
- scope: OnboardingCompletionMarkerScope = 'project',
73
+ scope: OnboardingStateScope = 'project',
74
74
  ): OnboardingRuntimeStateRecord {
75
75
  const path = resolveStatePath(shellPaths, scope);
76
76
  if (!existsSync(path)) {
@@ -1,5 +1,6 @@
1
1
  import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../../config/index.ts';
2
2
  import type { SecretsManager, SecretRecord, SecretStorageReview } from '../../config/secrets.ts';
3
+ import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
3
4
  import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security/user-auth';
4
5
  import type { ShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
5
6
  import type {
@@ -26,9 +27,9 @@ export type OnboardingAcknowledgementReason =
26
27
 
27
28
  export type OnboardingVerificationStatus = 'pass' | 'warn' | 'fail';
28
29
 
29
- export type OnboardingCompletionMarkerScope = 'user' | 'project';
30
+ export type OnboardingStateScope = 'user' | 'project';
30
31
 
31
- export type OnboardingCompletionMarkerSource = 'wizard' | 'command' | 'import' | 'unknown';
32
+ export type OnboardingCheckMarkerSource = 'wizard' | 'command' | 'import' | 'unknown';
32
33
 
33
34
  export interface OnboardingConfigSnapshot {
34
35
  readonly display: GoodVibesConfig['display'];
@@ -45,6 +46,7 @@ export interface OnboardingConfigSnapshot {
45
46
  readonly network: GoodVibesConfig['network'];
46
47
  readonly surfaces: GoodVibesConfig['surfaces'];
47
48
  readonly service: GoodVibesConfig['service'];
49
+ readonly featureFlags: GoodVibesConfig['featureFlags'];
48
50
  }
49
51
 
50
52
  export interface OnboardingProviderRoutingSnapshot {
@@ -147,7 +149,7 @@ export interface OnboardingRuntimeDefaultsSnapshot {
147
149
  }
148
150
 
149
151
  export interface OnboardingAcknowledgementSnapshot {
150
- readonly scope: OnboardingCompletionMarkerScope;
152
+ readonly scope: OnboardingStateScope;
151
153
  readonly exists: boolean;
152
154
  readonly updatedAt: number | null;
153
155
  readonly source: string | null;
@@ -235,7 +237,7 @@ export interface OnboardingStepDerivationState {
235
237
  export type OnboardingApplyOperation =
236
238
  | {
237
239
  readonly kind: 'set-config';
238
- readonly key: ConfigKey;
240
+ readonly key: ConfigKey | FeatureFlagConfigKey;
239
241
  readonly value: unknown;
240
242
  readonly scope?: 'global' | 'project';
241
243
  }
@@ -258,12 +260,6 @@ export type OnboardingApplyOperation =
258
260
  readonly kind: 'acknowledge';
259
261
  readonly target: OnboardingAcknowledgementTarget;
260
262
  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
263
  };
268
264
 
269
265
  export interface OnboardingApplyRequest {
@@ -316,34 +312,34 @@ export interface OnboardingAcknowledgementRuntimeState {
316
312
  readonly acknowledgements: Partial<Record<OnboardingAcknowledgementTarget, boolean>>;
317
313
  }
318
314
 
319
- export interface OnboardingCompletionMarkerPayload {
315
+ export interface OnboardingCheckMarkerPayload {
320
316
  readonly version: 1;
321
- readonly completedAt: number;
317
+ readonly checkedAt: number;
322
318
  readonly updatedAt: number;
323
- readonly source: OnboardingCompletionMarkerSource;
319
+ readonly source: OnboardingCheckMarkerSource;
324
320
  readonly mode?: OnboardingMode;
325
321
  readonly workspaceRoot?: string;
326
322
  }
327
323
 
328
- export interface OnboardingCompletionMarkerState {
329
- readonly scope: OnboardingCompletionMarkerScope;
324
+ export interface OnboardingCheckMarkerState {
325
+ readonly scope: OnboardingStateScope;
330
326
  readonly path: string;
331
327
  readonly exists: boolean;
332
- readonly payload: OnboardingCompletionMarkerPayload | null;
328
+ readonly payload: OnboardingCheckMarkerPayload | null;
333
329
  readonly parseError?: string;
334
330
  }
335
331
 
336
- export interface OnboardingCompletionMarkersState {
337
- readonly user: OnboardingCompletionMarkerState;
338
- readonly project: OnboardingCompletionMarkerState;
339
- readonly effective: OnboardingCompletionMarkerState | null;
332
+ export interface OnboardingCheckMarkersState {
333
+ readonly user: OnboardingCheckMarkerState;
334
+ readonly project: OnboardingCheckMarkerState;
335
+ readonly effective: OnboardingCheckMarkerState | null;
340
336
  }
341
337
 
342
- export interface WriteOnboardingCompletionMarkerOptions {
343
- readonly scope?: OnboardingCompletionMarkerScope;
344
- readonly completedAt?: number;
338
+ export interface WriteOnboardingCheckMarkerOptions {
339
+ readonly scope?: OnboardingStateScope;
340
+ readonly checkedAt?: number;
345
341
  readonly updatedAt?: number;
346
- readonly source?: OnboardingCompletionMarkerSource;
342
+ readonly source?: OnboardingCheckMarkerSource;
347
343
  readonly mode?: OnboardingMode;
348
344
  readonly workspaceRoot?: string;
349
345
  }
@@ -371,7 +367,7 @@ export interface OnboardingSnapshotDependencies {
371
367
  readonly surfaces?: OnboardingSurfaceReadHelper;
372
368
  readonly providerAccounts?: OnboardingProviderAccountReadHelper;
373
369
  readonly shellPaths?: OnboardingShellPaths;
374
- readonly acknowledgementScope?: OnboardingCompletionMarkerScope;
370
+ readonly acknowledgementScope?: OnboardingStateScope;
375
371
  }
376
372
 
377
373
  export interface OnboardingApplyDependencies {
@@ -387,9 +383,10 @@ export interface OnboardingApplyDependencies {
387
383
  | 'getUser'
388
384
  | 'inspect'
389
385
  | 'revokeSession'
386
+ | 'rotatePassword'
390
387
  >;
391
388
  readonly shellPaths: OnboardingShellPaths;
392
- readonly acknowledgementScope?: OnboardingCompletionMarkerScope;
389
+ readonly acknowledgementScope?: OnboardingStateScope;
393
390
  }
394
391
 
395
392
  export interface OnboardingVerificationDependencies {
@@ -398,5 +395,5 @@ export interface OnboardingVerificationDependencies {
398
395
  readonly secrets?: Pick<SecretsManager, 'get'>;
399
396
  readonly auth?: Pick<UserAuthManager, 'inspect'>;
400
397
  readonly shellPaths: OnboardingShellPaths;
401
- readonly acknowledgementScope?: OnboardingCompletionMarkerScope;
398
+ readonly acknowledgementScope?: OnboardingStateScope;
402
399
  }
@@ -1,5 +1,4 @@
1
1
  import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
2
- import { readOnboardingCompletionMarker } from './markers.ts';
3
2
  import { readOnboardingRuntimeState } from './state.ts';
4
3
  import type {
5
4
  OnboardingApplyOperation,
@@ -13,11 +12,6 @@ function getNow(deps: Pick<OnboardingVerificationDependencies, 'clock'>): number
13
12
  return deps.clock?.() ?? Date.now();
14
13
  }
15
14
 
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
15
  function isDeepEqual(left: unknown, right: unknown): boolean {
22
16
  if (Object.is(left, right)) return true;
23
17
  if (Array.isArray(left) && Array.isArray(right)) {
@@ -49,7 +43,7 @@ function verifyConfigOperation(
49
43
  deps: OnboardingVerificationDependencies,
50
44
  operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>,
51
45
  ): OnboardingVerificationItem {
52
- const actual = deps.config.get(operation.key);
46
+ const actual = deps.config.get(operation.key as never);
53
47
  const ok = isDeepEqual(actual, operation.value);
54
48
 
55
49
  return {
@@ -89,60 +83,6 @@ function verifyAcknowledgementOperation(
89
83
  };
90
84
  }
91
85
 
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
86
  async function verifySecretOperation(
147
87
  deps: OnboardingVerificationDependencies,
148
88
  operation: Extract<OnboardingApplyOperation, { kind: 'set-secret' }>,
@@ -209,21 +149,19 @@ function verifyAuthOperation(
209
149
 
210
150
  async function verifyOperation(
211
151
  deps: OnboardingVerificationDependencies,
212
- request: OnboardingApplyRequest,
213
152
  operation: OnboardingApplyOperation,
214
153
  ): Promise<OnboardingVerificationItem> {
215
154
  if (operation.kind === 'set-config') return verifyConfigOperation(deps, operation);
216
155
  if (operation.kind === 'set-secret') return verifySecretOperation(deps, operation);
217
156
  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);
157
+ return verifyAcknowledgementOperation(deps, operation);
220
158
  }
221
159
 
222
160
  export async function verifyOnboardingRequest(
223
161
  deps: OnboardingVerificationDependencies,
224
162
  request: OnboardingApplyRequest,
225
163
  ): Promise<OnboardingVerificationResult> {
226
- const items = await Promise.all(request.operations.map((operation) => verifyOperation(deps, request, operation)));
164
+ const items = await Promise.all(request.operations.map((operation) => verifyOperation(deps, operation)));
227
165
 
228
166
  return {
229
167
  verifiedAt: getNow(deps),
@@ -0,0 +1,67 @@
1
+ import type { ConfigKey, ConfigManager, PersistedFlagState } from '../config/index.ts';
2
+ import { surfaceFeatureGateId } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/index';
3
+
4
+ export const CONTROL_PLANE_FEATURE_FLAG = 'control-plane-gateway';
5
+ export const ROUTE_BINDING_FEATURE_FLAG = 'route-binding';
6
+ export const DELIVERY_ENGINE_FEATURE_FLAG = 'delivery-engine';
7
+ export const OMNICHANNEL_SURFACE_FEATURE_FLAG = 'omnichannel-surface-adapters';
8
+ export const SERVICE_MANAGEMENT_FEATURE_FLAG = 'service-management';
9
+
10
+ const CORE_CHANNEL_FEATURE_FLAGS = [
11
+ CONTROL_PLANE_FEATURE_FLAG,
12
+ ROUTE_BINDING_FEATURE_FLAG,
13
+ DELIVERY_ENGINE_FEATURE_FLAG,
14
+ OMNICHANNEL_SURFACE_FEATURE_FLAG,
15
+ ] as const;
16
+
17
+ export type FeatureFlagConfigKey = 'featureFlags' | `featureFlags.${string}`;
18
+
19
+ export function getSurfaceFeatureFlag(surfaceId: string): string | null {
20
+ return surfaceFeatureGateId(surfaceId);
21
+ }
22
+
23
+ export function getServerSurfaceFeatureFlags(options: {
24
+ readonly serverBacked?: boolean;
25
+ readonly web?: boolean;
26
+ readonly externalSurfaces?: readonly string[];
27
+ }): readonly string[] {
28
+ const flags = new Set<string>();
29
+ const hasExternalSurfaces = (options.externalSurfaces?.length ?? 0) > 0;
30
+
31
+ if (options.serverBacked || options.web || hasExternalSurfaces) {
32
+ flags.add(CONTROL_PLANE_FEATURE_FLAG);
33
+ flags.add(SERVICE_MANAGEMENT_FEATURE_FLAG);
34
+ }
35
+ if (options.web) {
36
+ const webFlag = getSurfaceFeatureFlag('web');
37
+ if (webFlag) flags.add(webFlag);
38
+ }
39
+
40
+ if (hasExternalSurfaces) {
41
+ for (const flag of CORE_CHANNEL_FEATURE_FLAGS) flags.add(flag);
42
+ for (const surfaceId of options.externalSurfaces ?? []) {
43
+ const surfaceFlag = getSurfaceFeatureFlag(surfaceId);
44
+ if (surfaceFlag) flags.add(surfaceFlag);
45
+ }
46
+ }
47
+
48
+ return [...flags].sort((left, right) => left.localeCompare(right));
49
+ }
50
+
51
+ export function isFeatureFlagEnabled(config: Pick<ConfigManager, 'getCategory'>, flagId: string): boolean {
52
+ const flags = (config.getCategory('featureFlags') as Record<string, PersistedFlagState | undefined>) ?? {};
53
+ return flags[flagId] === 'enabled';
54
+ }
55
+
56
+ export function getMissingSurfaceFeatureFlags(config: Pick<ConfigManager, 'getCategory'>, surfaceId: string): readonly string[] {
57
+ const required = surfaceId === 'web'
58
+ ? getServerSurfaceFeatureFlags({ web: true })
59
+ : getServerSurfaceFeatureFlags({ externalSurfaces: [surfaceId] });
60
+ return required.filter((flagId) => !isFeatureFlagEnabled(config, flagId));
61
+ }
62
+
63
+ export function enableFeatureFlags(config: Pick<ConfigManager, 'setDynamic'>, flagIds: readonly string[]): void {
64
+ for (const flagId of flagIds) {
65
+ config.setDynamic(`featureFlags.${flagId}` as ConfigKey, 'enabled');
66
+ }
67
+ }
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.27';
9
+ let _version = '0.19.29';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;