@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.
@@ -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') return UI_TONES.state.warn;
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, line: 0 });
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
- if (fieldRow.line === 0) {
182
- const badge = truncateDisplay(`[${wizard.getFieldValueLabel(field)}]`, Math.max(8, Math.floor(width * 0.38)));
183
- const badgeWidth = getDisplayWidth(badge);
184
- const prefix = selected ? `${OVERLAY_GLYPHS.selected} ` : wizard.isFieldDirty(field.id) ? '◇ ' : ' ';
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 + 3, Math.max(0, width - 4), truncateDisplay(fieldHint(wizard, field, selected), Math.max(0, width - 4)), {
201
- fg: selected ? UI_TONES.fg.secondary : UI_TONES.fg.muted,
230
+ putOverlayText(line, startX + 1, labelWidth, truncateDisplay(`${prefix}${field.label}`, labelWidth), {
231
+ fg: UI_TONES.fg.primary,
202
232
  bg: fieldBg,
203
- dim: !selected,
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 '[Tab/Shift+Tab] Step [↑↓/j/k] Move [Enter/Space] Toggle/Open [1-9] Jump [Esc] Close';
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 fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - 3));
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 Rail', {
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
- const status = fitDisplay(
337
- `Fields ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete ${modeLabel(wizard.mode)}`,
338
- centerWidth - 2,
339
- );
340
- putOverlayText(line, centerStart + 1, centerWidth - 2, descriptionLines[1] ?? status, {
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: bodyBg,
395
+ bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
343
396
  });
344
397
  } else {
345
- renderFieldRow(line, wizard, fieldRows[row - 3] ?? { kind: 'empty' }, centerStart, centerWidth);
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
- putOverlayText(line, leftStart + 1, leftWidth - 2, truncateDisplay(`${stepState.glyph} ${row + 1}. ${step.shortLabel}`, leftWidth - 2), {
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 - getDisplayWidth(completion) - 2), leftWidth - 2, completion, {
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 fieldRows = buildFieldRows(wizard, visibleFields, Math.max(0, bodyRows - 3));
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
- const compactMeta = fitDisplay(
480
- `${currentStep.summaryTitle} ${wizard.getCompletedFieldCount(wizard.stepIndex)}/${wizard.getStepFieldCount(wizard.stepIndex)} complete`,
481
- innerWidth - 2,
482
- );
483
- putOverlayText(line, innerStart + 1, innerWidth - 2, descriptionLines[1] ?? compactMeta, {
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: bodyBg,
552
+ bg: DEFAULT_OVERLAY_PALETTE.selectedBg,
486
553
  });
487
554
  } else {
488
- renderFieldRow(line, wizard, fieldRows[row - 3] ?? { kind: 'empty' }, innerStart, innerWidth);
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
- return snapshotFileRollback(
437
- getOnboardingCompletionMarkerPath(deps.shellPaths, operation.scope),
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' || operation.kind === 'set-completion-marker'
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
- validateCompletionMarkerOperation(deps, operation);
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
- applied.push(applyCompletionMarkerOperation(deps, request, operation));
629
- rollbacks.push(rollback);
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(preMarkerOperations)) {
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(request.operations)) {
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, 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
  }
@@ -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)) {