@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.
- package/CHANGELOG.md +11 -0
- package/README.md +3 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +3 -2
- package/src/cli/entrypoint.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/status.ts +9 -9
- package/src/cli/surface-command.ts +46 -11
- package/src/cli/tui-startup.ts +4 -4
- package/src/daemon/cli.ts +7 -0
- package/src/input/handler-interactions.ts +14 -1
- package/src/input/handler-onboarding.ts +161 -118
- package/src/input/handler.ts +1 -1
- package/src/input/onboarding/handler-onboarding-routes.ts +35 -15
- package/src/input/onboarding/onboarding-wizard-apply.ts +35 -25
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -5
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +93 -5
- package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -3
- package/src/input/onboarding/onboarding-wizard-rules.ts +40 -8
- package/src/input/onboarding/onboarding-wizard-state.ts +19 -8
- package/src/input/onboarding/onboarding-wizard-steps.ts +226 -93
- package/src/input/onboarding/onboarding-wizard-types.ts +15 -0
- package/src/input/onboarding/onboarding-wizard.ts +123 -6
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +4 -0
- package/src/main.ts +35 -27
- package/src/renderer/compositor.ts +3 -3
- package/src/renderer/onboarding/onboarding-wizard.ts +141 -57
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +3 -0
- package/src/runtime/bootstrap.ts +15 -0
- package/src/runtime/onboarding/apply.ts +45 -90
- package/src/runtime/onboarding/derivation.ts +7 -7
- package/src/runtime/onboarding/markers.ts +41 -55
- package/src/runtime/onboarding/snapshot.ts +1 -0
- package/src/runtime/onboarding/state.ts +6 -6
- package/src/runtime/onboarding/types.ts +24 -27
- package/src/runtime/onboarding/verify.ts +3 -65
- package/src/runtime/surface-feature-flags.ts +67 -0
- 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 '
|
|
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 '
|
|
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
|
|
252
|
-
: '
|
|
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
|
|
258
|
-
: '
|
|
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 '
|
|
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,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
OnboardingCheckMarkerPayload,
|
|
6
|
+
OnboardingCheckMarkerState,
|
|
7
|
+
OnboardingCheckMarkersState,
|
|
8
|
+
OnboardingStateScope,
|
|
9
|
+
WriteOnboardingCheckMarkerOptions,
|
|
10
10
|
} from './types.ts';
|
|
11
11
|
|
|
12
|
-
const
|
|
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:
|
|
21
|
+
scope: OnboardingStateScope,
|
|
22
22
|
): string {
|
|
23
23
|
return scope === 'project'
|
|
24
|
-
? shellPaths.resolveProjectPath('tui',
|
|
25
|
-
: shellPaths.resolveUserPath('tui',
|
|
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
|
|
32
|
+
function isOnboardingMode(value: unknown): value is OnboardingCheckMarkerPayload['mode'] {
|
|
33
33
|
return value === 'new' || value === 'edit' || value === 'reopen';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function
|
|
36
|
+
function isCheckMarkerPayload(value: unknown): value is OnboardingCheckMarkerPayload {
|
|
37
37
|
return isObject(value)
|
|
38
38
|
&& value.version === 1
|
|
39
|
-
&& typeof value.
|
|
40
|
-
&& Number.isFinite(value.
|
|
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:
|
|
49
|
+
scope: OnboardingStateScope,
|
|
50
50
|
path: string,
|
|
51
|
-
):
|
|
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:
|
|
61
|
+
scope: OnboardingStateScope,
|
|
62
62
|
path: string,
|
|
63
63
|
parseError: string,
|
|
64
|
-
):
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
82
|
+
export function getOnboardingCheckMarkerPath(
|
|
86
83
|
shellPaths: OnboardingShellPaths,
|
|
87
|
-
scope:
|
|
84
|
+
scope: OnboardingStateScope = 'user',
|
|
88
85
|
): string {
|
|
89
86
|
return resolveMarkerPath(shellPaths, scope);
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
export function
|
|
89
|
+
export function readOnboardingCheckMarker(
|
|
93
90
|
shellPaths: OnboardingShellPaths,
|
|
94
|
-
scope:
|
|
95
|
-
):
|
|
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 (!
|
|
102
|
-
return buildParseErrorState(scope, path, 'Invalid onboarding
|
|
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
|
|
114
|
+
export function readOnboardingCheckMarkers(
|
|
118
115
|
shellPaths: OnboardingShellPaths,
|
|
119
|
-
):
|
|
120
|
-
const user =
|
|
121
|
-
const 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(
|
|
123
|
+
effective: pickEffectiveMarker(user),
|
|
127
124
|
};
|
|
128
125
|
}
|
|
129
126
|
|
|
130
|
-
export function
|
|
127
|
+
export function writeOnboardingCheckMarker(
|
|
131
128
|
shellPaths: OnboardingShellPaths,
|
|
132
|
-
options:
|
|
133
|
-
):
|
|
129
|
+
options: WriteOnboardingCheckMarkerOptions = {},
|
|
130
|
+
): OnboardingCheckMarkerState {
|
|
134
131
|
const scope = options.scope ?? 'user';
|
|
135
132
|
const path = resolveMarkerPath(shellPaths, scope);
|
|
136
|
-
const
|
|
137
|
-
const payload:
|
|
133
|
+
const checkedAt = options.checkedAt ?? Date.now();
|
|
134
|
+
const payload: OnboardingCheckMarkerPayload = {
|
|
138
135
|
version: 1,
|
|
139
|
-
|
|
140
|
-
updatedAt: options.updatedAt ??
|
|
136
|
+
checkedAt,
|
|
137
|
+
updatedAt: options.updatedAt ?? checkedAt,
|
|
141
138
|
source: options.source ?? 'wizard',
|
|
142
139
|
...(options.mode ? { mode: options.mode } : {}),
|
|
143
|
-
...(options.workspaceRoot
|
|
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
|
|
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
|
}
|
|
@@ -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:
|
|
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?:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
30
|
+
export type OnboardingStateScope = 'user' | 'project';
|
|
30
31
|
|
|
31
|
-
export type
|
|
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:
|
|
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
|
|
315
|
+
export interface OnboardingCheckMarkerPayload {
|
|
320
316
|
readonly version: 1;
|
|
321
|
-
readonly
|
|
317
|
+
readonly checkedAt: number;
|
|
322
318
|
readonly updatedAt: number;
|
|
323
|
-
readonly source:
|
|
319
|
+
readonly source: OnboardingCheckMarkerSource;
|
|
324
320
|
readonly mode?: OnboardingMode;
|
|
325
321
|
readonly workspaceRoot?: string;
|
|
326
322
|
}
|
|
327
323
|
|
|
328
|
-
export interface
|
|
329
|
-
readonly scope:
|
|
324
|
+
export interface OnboardingCheckMarkerState {
|
|
325
|
+
readonly scope: OnboardingStateScope;
|
|
330
326
|
readonly path: string;
|
|
331
327
|
readonly exists: boolean;
|
|
332
|
-
readonly payload:
|
|
328
|
+
readonly payload: OnboardingCheckMarkerPayload | null;
|
|
333
329
|
readonly parseError?: string;
|
|
334
330
|
}
|
|
335
331
|
|
|
336
|
-
export interface
|
|
337
|
-
readonly user:
|
|
338
|
-
readonly project:
|
|
339
|
-
readonly effective:
|
|
332
|
+
export interface OnboardingCheckMarkersState {
|
|
333
|
+
readonly user: OnboardingCheckMarkerState;
|
|
334
|
+
readonly project: OnboardingCheckMarkerState;
|
|
335
|
+
readonly effective: OnboardingCheckMarkerState | null;
|
|
340
336
|
}
|
|
341
337
|
|
|
342
|
-
export interface
|
|
343
|
-
readonly scope?:
|
|
344
|
-
readonly
|
|
338
|
+
export interface WriteOnboardingCheckMarkerOptions {
|
|
339
|
+
readonly scope?: OnboardingStateScope;
|
|
340
|
+
readonly checkedAt?: number;
|
|
345
341
|
readonly updatedAt?: number;
|
|
346
|
-
readonly source?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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;
|