@pellux/goodvibes-tui 0.19.26 → 0.19.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -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/tui-startup.ts +4 -4
- package/src/input/handler-interactions.ts +14 -1
- package/src/input/handler-onboarding.ts +18 -82
- package/src/input/handler.ts +1 -1
- package/src/input/onboarding/handler-onboarding-routes.ts +31 -15
- package/src/input/onboarding/onboarding-wizard-apply.ts +0 -17
- package/src/input/onboarding/onboarding-wizard-helpers.ts +1 -2
- package/src/input/onboarding/onboarding-wizard-rules.ts +18 -5
- package/src/input/onboarding/onboarding-wizard-state.ts +8 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +118 -59
- package/src/input/onboarding/onboarding-wizard-types.ts +5 -0
- package/src/input/onboarding/onboarding-wizard.ts +70 -5
- package/src/main.ts +2 -1
- package/src/renderer/onboarding/onboarding-wizard.ts +115 -48
- package/src/runtime/onboarding/apply.ts +9 -82
- package/src/runtime/onboarding/markers.ts +41 -55
- package/src/runtime/onboarding/state.ts +6 -6
- package/src/runtime/onboarding/types.ts +20 -26
- package/src/runtime/onboarding/verify.ts +2 -64
- package/src/version.ts +1 -1
|
@@ -26,7 +26,6 @@ type RenderedFieldRow =
|
|
|
26
26
|
readonly kind: 'field';
|
|
27
27
|
readonly field: OnboardingWizardFieldDefinition;
|
|
28
28
|
readonly absoluteIndex: number;
|
|
29
|
-
readonly line: 0 | 1;
|
|
30
29
|
};
|
|
31
30
|
|
|
32
31
|
function clamp(value: number, min: number, max: number): number {
|
|
@@ -95,7 +94,11 @@ function fieldBadgeTone(
|
|
|
95
94
|
return wizard.getFieldValue(field) ? UI_TONES.state.good : UI_TONES.fg.muted;
|
|
96
95
|
}
|
|
97
96
|
if (field.kind === 'radio') return UI_TONES.state.active;
|
|
98
|
-
if (field.kind === 'masked')
|
|
97
|
+
if (field.kind === 'text' || field.kind === 'masked') {
|
|
98
|
+
const missingRequired = wizard.getFieldValueLabel(field) === 'Missing';
|
|
99
|
+
if (missingRequired) return UI_TONES.state.warn;
|
|
100
|
+
if (field.kind === 'masked') return UI_TONES.state.warn;
|
|
101
|
+
}
|
|
99
102
|
return UI_TONES.fg.secondary;
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -116,8 +119,7 @@ function buildFieldRows(
|
|
|
116
119
|
|
|
117
120
|
fieldWindow.fields.forEach((field, index) => {
|
|
118
121
|
const absoluteIndex = fieldWindow.start + index;
|
|
119
|
-
rows.push({ kind: 'field', field, absoluteIndex
|
|
120
|
-
rows.push({ kind: 'field', field, absoluteIndex, line: 1 });
|
|
122
|
+
rows.push({ kind: 'field', field, absoluteIndex });
|
|
121
123
|
});
|
|
122
124
|
|
|
123
125
|
if (fieldWindow.end < fieldWindow.total) {
|
|
@@ -155,6 +157,48 @@ function fieldHint(
|
|
|
155
157
|
return field.hint;
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
function fieldRowPrefix(
|
|
161
|
+
wizard: OnboardingWizardController,
|
|
162
|
+
field: OnboardingWizardFieldDefinition,
|
|
163
|
+
selected: boolean,
|
|
164
|
+
): string {
|
|
165
|
+
if (selected) return `${OVERLAY_GLYPHS.selected} `;
|
|
166
|
+
if (wizard.isFieldDirty(field.id)) return '◇ ';
|
|
167
|
+
if (field.kind === 'checklist') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
|
|
168
|
+
if (field.kind === 'acknowledgement') return (wizard.getFieldValue(field) as boolean) ? '✓ ' : '□ ';
|
|
169
|
+
if (field.kind === 'action') return '▶ ';
|
|
170
|
+
if (field.kind === 'radio') return '◉ ';
|
|
171
|
+
return ' ';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function selectedFieldText(wizard: OnboardingWizardController): {
|
|
175
|
+
readonly title: string;
|
|
176
|
+
readonly hint: string;
|
|
177
|
+
} {
|
|
178
|
+
if (wizard.isEditingTextField() && wizard.editingFieldId !== null) {
|
|
179
|
+
const editingField = wizard.getFieldById(wizard.editingFieldId);
|
|
180
|
+
if (editingField) {
|
|
181
|
+
return {
|
|
182
|
+
title: `Editing: ${editingField.label}`,
|
|
183
|
+
hint: fieldHint(wizard, editingField, true),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const field = wizard.getSelectedField();
|
|
189
|
+
if (!field) {
|
|
190
|
+
return {
|
|
191
|
+
title: 'Selected: none',
|
|
192
|
+
hint: 'No selectable row is active on this screen.',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
title: `Selected: ${field.label} [${wizard.getFieldValueLabel(field)}]`,
|
|
198
|
+
hint: fieldHint(wizard, field, true),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
158
202
|
function renderFieldRow(
|
|
159
203
|
line: Line,
|
|
160
204
|
wizard: OnboardingWizardController,
|
|
@@ -178,38 +222,36 @@ function renderFieldRow(
|
|
|
178
222
|
const fieldBg = selected ? DEFAULT_OVERLAY_PALETTE.selectedBg : UI_TONES.bg.base;
|
|
179
223
|
fillRange(line, startX, width, fieldBg);
|
|
180
224
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const labelWidth = Math.max(0, width - badgeWidth - 4);
|
|
186
|
-
|
|
187
|
-
putOverlayText(line, startX + 1, labelWidth, truncateDisplay(`${prefix}${field.label}`, labelWidth), {
|
|
188
|
-
fg: UI_TONES.fg.primary,
|
|
189
|
-
bg: fieldBg,
|
|
190
|
-
bold: selected,
|
|
191
|
-
});
|
|
192
|
-
putOverlayText(line, startX + width - badgeWidth - 1, badgeWidth, badge, {
|
|
193
|
-
fg: fieldBadgeTone(wizard, field),
|
|
194
|
-
bg: fieldBg,
|
|
195
|
-
bold: selected,
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
225
|
+
const badge = truncateDisplay(`[${wizard.getFieldValueLabel(field)}]`, Math.max(8, Math.floor(width * 0.34)));
|
|
226
|
+
const badgeWidth = getDisplayWidth(badge);
|
|
227
|
+
const labelWidth = Math.max(0, width - badgeWidth - 4);
|
|
228
|
+
const prefix = fieldRowPrefix(wizard, field, selected);
|
|
199
229
|
|
|
200
|
-
putOverlayText(line, startX +
|
|
201
|
-
fg:
|
|
230
|
+
putOverlayText(line, startX + 1, labelWidth, truncateDisplay(`${prefix}${field.label}`, labelWidth), {
|
|
231
|
+
fg: UI_TONES.fg.primary,
|
|
202
232
|
bg: fieldBg,
|
|
203
|
-
|
|
233
|
+
bold: selected,
|
|
234
|
+
});
|
|
235
|
+
putOverlayText(line, startX + width - badgeWidth - 1, badgeWidth, badge, {
|
|
236
|
+
fg: fieldBadgeTone(wizard, field),
|
|
237
|
+
bg: fieldBg,
|
|
238
|
+
bold: selected,
|
|
204
239
|
});
|
|
205
240
|
}
|
|
206
241
|
|
|
207
242
|
function footerText(wizard: OnboardingWizardController): string {
|
|
208
243
|
if (wizard.isEditingTextField()) {
|
|
209
|
-
return '[Enter] Save [Esc] Cancel [Backspace] Delete [Type] Edit value';
|
|
244
|
+
return '[Enter] Save value [Esc] Cancel edit [Backspace] Delete [Type] Edit value';
|
|
210
245
|
}
|
|
211
246
|
|
|
212
|
-
return '[
|
|
247
|
+
return '[Enter] Toggle/open selected [Tab] Next screen [Shift+Tab] Previous [↑↓] Move [Esc] Close';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function controlsText(wizard: OnboardingWizardController): string {
|
|
251
|
+
if (wizard.isEditingTextField()) {
|
|
252
|
+
return 'Controls: Enter saves this value, Esc cancels editing, Backspace deletes, typing edits the value.';
|
|
253
|
+
}
|
|
254
|
+
return 'Controls: Enter or Space changes the selected row; Tab/Shift+Tab changes screens; arrows move; typing edits selected inputs.';
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
function renderWideLayout(
|
|
@@ -252,12 +294,13 @@ function renderWideLayout(
|
|
|
252
294
|
const descriptionLines = wrapText(currentStep.description, Math.max(18, centerWidth - 2)).slice(0, 2);
|
|
253
295
|
const summaryLines = [
|
|
254
296
|
currentStep.summaryTitle,
|
|
255
|
-
...currentStep.summaryLines,
|
|
297
|
+
...currentStep.summaryLines.slice(0, 2),
|
|
256
298
|
`Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
|
|
257
299
|
`Dirty steps ${wizard.dirtyStepCount}`,
|
|
258
|
-
`Pending picker ${wizard.pendingModelPickerTarget ?? 'none'}`,
|
|
259
300
|
];
|
|
260
|
-
const
|
|
301
|
+
const fieldStartRow = 5;
|
|
302
|
+
const selectedText = selectedFieldText(wizard);
|
|
303
|
+
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
|
|
261
304
|
|
|
262
305
|
const topLine = createOverlayFilledBorderLine(
|
|
263
306
|
width,
|
|
@@ -296,7 +339,7 @@ function renderWideLayout(
|
|
|
296
339
|
bg: headerBg,
|
|
297
340
|
bold: true,
|
|
298
341
|
});
|
|
299
|
-
putOverlayText(headerLine, rightStart + 1, rightWidth - 2, 'Summary
|
|
342
|
+
putOverlayText(headerLine, rightStart + 1, rightWidth - 2, 'Summary', {
|
|
300
343
|
fg: UI_TONES.fg.secondary,
|
|
301
344
|
bg: summaryBg,
|
|
302
345
|
bold: true,
|
|
@@ -333,28 +376,40 @@ function renderWideLayout(
|
|
|
333
376
|
bg: bodyBg,
|
|
334
377
|
});
|
|
335
378
|
} else if (row === 2) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
379
|
+
fillRange(line, centerStart, centerWidth, railBg);
|
|
380
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(controlsText(wizard), centerWidth - 2), {
|
|
381
|
+
fg: UI_TONES.state.info,
|
|
382
|
+
bg: railBg,
|
|
383
|
+
});
|
|
384
|
+
} else if (row === 3) {
|
|
385
|
+
fillRange(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
386
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, centerWidth - 2), {
|
|
387
|
+
fg: UI_TONES.fg.primary,
|
|
388
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
389
|
+
bold: true,
|
|
390
|
+
});
|
|
391
|
+
} else if (row === 4) {
|
|
392
|
+
fillRange(line, centerStart, centerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
393
|
+
putOverlayText(line, centerStart + 1, centerWidth - 2, truncateDisplay(selectedText.hint, centerWidth - 2), {
|
|
341
394
|
fg: UI_TONES.fg.secondary,
|
|
342
|
-
bg:
|
|
395
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
343
396
|
});
|
|
344
397
|
} else {
|
|
345
|
-
renderFieldRow(line, wizard, fieldRows[row -
|
|
398
|
+
renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, centerStart, centerWidth);
|
|
346
399
|
}
|
|
347
400
|
|
|
348
401
|
const step = wizard.steps[row] ?? null;
|
|
349
402
|
if (step) {
|
|
350
403
|
const stepState = stepGlyph(wizard, step, row);
|
|
351
404
|
const completion = `${wizard.getCompletedFieldCount(row)}/${wizard.getStepFieldCount(row)}`;
|
|
352
|
-
|
|
405
|
+
const completionWidth = getDisplayWidth(completion);
|
|
406
|
+
const stepLabelWidth = Math.max(0, leftWidth - completionWidth - 4);
|
|
407
|
+
putOverlayText(line, leftStart + 1, stepLabelWidth, truncateDisplay(`${stepState.glyph} ${row + 1}. ${step.shortLabel}`, stepLabelWidth), {
|
|
353
408
|
fg: stepState.fg,
|
|
354
409
|
bg: railBg,
|
|
355
410
|
bold: row === wizard.stepIndex,
|
|
356
411
|
});
|
|
357
|
-
putOverlayText(line, Math.max(leftStart + 1, leftStart + leftWidth -
|
|
412
|
+
putOverlayText(line, Math.max(leftStart + 1, leftStart + leftWidth - completionWidth - 2), completionWidth, completion, {
|
|
358
413
|
fg: wizard.isStepDirty(row) ? UI_TONES.state.warn : UI_TONES.fg.muted,
|
|
359
414
|
bg: railBg,
|
|
360
415
|
});
|
|
@@ -418,7 +473,9 @@ function renderCollapsedLayout(
|
|
|
418
473
|
const innerStart = layout.margin + 1;
|
|
419
474
|
const innerWidth = layout.innerWidth;
|
|
420
475
|
const descriptionLines = wrapText(currentStep.description, Math.max(14, innerWidth - 2)).slice(0, 2);
|
|
421
|
-
const
|
|
476
|
+
const fieldStartRow = 5;
|
|
477
|
+
const selectedText = selectedFieldText(wizard);
|
|
478
|
+
const fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - fieldStartRow));
|
|
422
479
|
|
|
423
480
|
const topLine = createOverlayFilledBorderLine(
|
|
424
481
|
width,
|
|
@@ -476,16 +533,26 @@ function renderCollapsedLayout(
|
|
|
476
533
|
bg: bodyBg,
|
|
477
534
|
});
|
|
478
535
|
} else if (row === 2) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
536
|
+
fillRange(line, innerStart, innerWidth, UI_TONES.bg.section);
|
|
537
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(controlsText(wizard), innerWidth - 2), {
|
|
538
|
+
fg: UI_TONES.state.info,
|
|
539
|
+
bg: UI_TONES.bg.section,
|
|
540
|
+
});
|
|
541
|
+
} else if (row === 3) {
|
|
542
|
+
fillRange(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
543
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(`Focus: ${selectedText.title.replace(/^Selected: /, '')}`, innerWidth - 2), {
|
|
544
|
+
fg: UI_TONES.fg.primary,
|
|
545
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
546
|
+
bold: true,
|
|
547
|
+
});
|
|
548
|
+
} else if (row === 4) {
|
|
549
|
+
fillRange(line, innerStart, innerWidth, DEFAULT_OVERLAY_PALETTE.selectedBg);
|
|
550
|
+
putOverlayText(line, innerStart + 1, innerWidth - 2, truncateDisplay(selectedText.hint, innerWidth - 2), {
|
|
484
551
|
fg: UI_TONES.fg.secondary,
|
|
485
|
-
bg:
|
|
552
|
+
bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
|
|
486
553
|
});
|
|
487
554
|
} else {
|
|
488
|
-
renderFieldRow(line, wizard, fieldRows[row -
|
|
555
|
+
renderFieldRow(line, wizard, fieldRows[row - fieldStartRow] ?? { kind: 'empty' }, innerStart, innerWidth);
|
|
489
556
|
}
|
|
490
557
|
|
|
491
558
|
lines.push(line);
|
|
@@ -2,12 +2,6 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config/secret-refs';
|
|
4
4
|
import { CONFIG_SCHEMA, DEFAULT_CONFIG } from '../../config/index.ts';
|
|
5
|
-
import {
|
|
6
|
-
clearOnboardingCompletionMarker,
|
|
7
|
-
getOnboardingCompletionMarkerPath,
|
|
8
|
-
readOnboardingCompletionMarker,
|
|
9
|
-
writeOnboardingCompletionMarker,
|
|
10
|
-
} from './markers.ts';
|
|
11
5
|
import {
|
|
12
6
|
getOnboardingRuntimeStatePath,
|
|
13
7
|
readOnboardingRuntimeState,
|
|
@@ -27,11 +21,6 @@ function getNow(deps: Pick<OnboardingApplyDependencies, 'clock'>): number {
|
|
|
27
21
|
return deps.clock?.() ?? Date.now();
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
function normalizeCompletionSource(source: string): 'wizard' | 'command' | 'import' | 'unknown' {
|
|
31
|
-
if (source === 'wizard' || source === 'command' || source === 'import') return source;
|
|
32
|
-
return 'unknown';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
24
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
25
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
26
|
}
|
|
@@ -234,17 +223,6 @@ function validateAcknowledgementOperation(
|
|
|
234
223
|
}
|
|
235
224
|
}
|
|
236
225
|
|
|
237
|
-
function validateCompletionMarkerOperation(
|
|
238
|
-
deps: OnboardingApplyDependencies,
|
|
239
|
-
operation: Extract<OnboardingApplyOperation, { kind: 'set-completion-marker' }>,
|
|
240
|
-
): void {
|
|
241
|
-
const marker = readOnboardingCompletionMarker(deps.shellPaths, operation.scope);
|
|
242
|
-
if (marker.parseError && !operation.completed) return;
|
|
243
|
-
if (marker.parseError) {
|
|
244
|
-
throw new Error(`Existing ${operation.scope} onboarding marker could not be parsed: ${marker.parseError}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
226
|
function applyConfigOperation(
|
|
249
227
|
deps: OnboardingApplyDependencies,
|
|
250
228
|
operation: Extract<OnboardingApplyOperation, { kind: 'set-config' }>,
|
|
@@ -433,9 +411,8 @@ async function buildRollbackAction(
|
|
|
433
411
|
);
|
|
434
412
|
}
|
|
435
413
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
);
|
|
414
|
+
const neverOperation: never = operation;
|
|
415
|
+
throw new Error(`Unsupported onboarding operation: ${JSON.stringify(neverOperation)}`);
|
|
439
416
|
}
|
|
440
417
|
|
|
441
418
|
function applyAcknowledgementOperation(
|
|
@@ -458,35 +435,6 @@ function applyAcknowledgementOperation(
|
|
|
458
435
|
};
|
|
459
436
|
}
|
|
460
437
|
|
|
461
|
-
function applyCompletionMarkerOperation(
|
|
462
|
-
deps: OnboardingApplyDependencies,
|
|
463
|
-
request: OnboardingApplyRequest,
|
|
464
|
-
operation: Extract<OnboardingApplyOperation, { kind: 'set-completion-marker' }>,
|
|
465
|
-
): OnboardingAppliedOperation {
|
|
466
|
-
if (!operation.completed) {
|
|
467
|
-
clearOnboardingCompletionMarker(deps.shellPaths, operation.scope);
|
|
468
|
-
return {
|
|
469
|
-
kind: operation.kind,
|
|
470
|
-
summary: `Cleared ${operation.scope} onboarding completion marker.`,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const updatedAt = getNow(deps);
|
|
475
|
-
writeOnboardingCompletionMarker(deps.shellPaths, {
|
|
476
|
-
scope: operation.scope,
|
|
477
|
-
completedAt: operation.payload?.completedAt,
|
|
478
|
-
updatedAt: operation.payload?.updatedAt ?? updatedAt,
|
|
479
|
-
source: operation.payload?.source ?? normalizeCompletionSource(request.source),
|
|
480
|
-
mode: operation.payload?.mode ?? request.mode,
|
|
481
|
-
workspaceRoot: operation.payload?.workspaceRoot,
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
kind: operation.kind,
|
|
486
|
-
summary: `Wrote ${operation.scope} onboarding completion marker.`,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
438
|
function orderApplyOperations(
|
|
491
439
|
operations: readonly OnboardingApplyOperation[],
|
|
492
440
|
): readonly OnboardingApplyOperation[] {
|
|
@@ -499,7 +447,7 @@ function orderApplyOperations(
|
|
|
499
447
|
operation.kind === 'set-config' && operation.key !== 'storage.secretPolicy'
|
|
500
448
|
));
|
|
501
449
|
const finalOperations = operations.filter((operation) => (
|
|
502
|
-
operation.kind === 'acknowledge'
|
|
450
|
+
operation.kind === 'acknowledge'
|
|
503
451
|
));
|
|
504
452
|
|
|
505
453
|
return [
|
|
@@ -543,7 +491,8 @@ function prevalidateApplyRequest(
|
|
|
543
491
|
continue;
|
|
544
492
|
}
|
|
545
493
|
|
|
546
|
-
|
|
494
|
+
const neverOperation: never = operation;
|
|
495
|
+
throw new Error(`Unsupported onboarding operation: ${JSON.stringify(neverOperation)}`);
|
|
547
496
|
} catch (error) {
|
|
548
497
|
errors.push({
|
|
549
498
|
kind: operation.kind,
|
|
@@ -560,22 +509,9 @@ function getVerificationFailureKind(itemId: string): OnboardingApplyOperation['k
|
|
|
560
509
|
if (itemId.startsWith('secret:')) return 'set-secret';
|
|
561
510
|
if (itemId.startsWith('auth:')) return 'ensure-auth-user';
|
|
562
511
|
if (itemId.startsWith('acknowledge:')) return 'acknowledge';
|
|
563
|
-
if (itemId.startsWith('marker:')) return 'set-completion-marker';
|
|
564
512
|
return 'set-config';
|
|
565
513
|
}
|
|
566
514
|
|
|
567
|
-
function splitCompletionMarkerOperations(
|
|
568
|
-
operations: readonly OnboardingApplyOperation[],
|
|
569
|
-
): {
|
|
570
|
-
readonly preMarkerOperations: readonly OnboardingApplyOperation[];
|
|
571
|
-
readonly markerOperations: readonly OnboardingApplyOperation[];
|
|
572
|
-
} {
|
|
573
|
-
return {
|
|
574
|
-
preMarkerOperations: operations.filter((operation) => operation.kind !== 'set-completion-marker'),
|
|
575
|
-
markerOperations: operations.filter((operation) => operation.kind === 'set-completion-marker'),
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
515
|
export async function applyOnboardingRequest(
|
|
580
516
|
deps: OnboardingApplyDependencies,
|
|
581
517
|
request: OnboardingApplyRequest,
|
|
@@ -593,7 +529,6 @@ export async function applyOnboardingRequest(
|
|
|
593
529
|
}
|
|
594
530
|
|
|
595
531
|
const orderedOperations = orderApplyOperations(request.operations);
|
|
596
|
-
const { preMarkerOperations, markerOperations } = splitCompletionMarkerOperations(orderedOperations);
|
|
597
532
|
const rollbacks: RollbackAction[] = [];
|
|
598
533
|
|
|
599
534
|
const applyOperations = async (operations: readonly OnboardingApplyOperation[]): Promise<boolean> => {
|
|
@@ -625,8 +560,8 @@ export async function applyOnboardingRequest(
|
|
|
625
560
|
continue;
|
|
626
561
|
}
|
|
627
562
|
|
|
628
|
-
|
|
629
|
-
|
|
563
|
+
const neverOperation: never = operation;
|
|
564
|
+
throw new Error(`Unsupported onboarding operation: ${JSON.stringify(neverOperation)}`);
|
|
630
565
|
} catch (error) {
|
|
631
566
|
const rollbackErrors = await runRollbacks([...rollbacks, rollback]);
|
|
632
567
|
applied.length = 0;
|
|
@@ -660,19 +595,11 @@ export async function applyOnboardingRequest(
|
|
|
660
595
|
return false;
|
|
661
596
|
};
|
|
662
597
|
|
|
663
|
-
if (!await applyOperations(
|
|
664
|
-
return { ok: false, applied, skipped, errors };
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (!await verifyOrRollback(preMarkerOperations)) {
|
|
668
|
-
return { ok: false, applied, skipped, errors };
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (!await applyOperations(markerOperations)) {
|
|
598
|
+
if (!await applyOperations(orderedOperations)) {
|
|
672
599
|
return { ok: false, applied, skipped, errors };
|
|
673
600
|
}
|
|
674
601
|
|
|
675
|
-
if (!await verifyOrRollback(
|
|
602
|
+
if (!await verifyOrRollback(orderedOperations)) {
|
|
676
603
|
return { ok: false, applied, skipped, errors };
|
|
677
604
|
}
|
|
678
605
|
|
|
@@ -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)) {
|