From 43d4cb67fc0b3a2ffe7bf4f28723e350f3bbc1b0 Mon Sep 17 00:00:00 2001 From: Brenno Ferrari Date: Sun, 31 May 2026 14:00:04 +0200 Subject: [PATCH 1/8] feat(adopt): add AdoptValuesGate confirm-or-override component (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt collects values before classification (the planner renders the shard's .njk against them), but its user already has a populated vault — the full step-by-step InstallWizard reads as a fresh-install flow shoehorned onto a retrofit. AdoptValuesGate opens on a single page that surfaces the exact values that will drive classification (resolved defaults, computed defaults, --values prefill) each labelled with provenance, plus the module default, and offers Use these values / Override individually / Cancel. "Override" reuses InstallWizard verbatim rather than forking its value-iteration / computed-preview / module-review logic. Required values with no default and no prefill open straight into the wizard (no confident set to confirm) — the predicate feeds missingValueKeys the *merged* map, exactly as the --yes path does, so all-defaults shards land on the confirm page. formatValue is exported from InstallWizard for reuse. The --yes/--values non-interactive paths never reach this component, so they are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- source/components/AdoptValuesGate.tsx | 170 ++++++++++++++++++ source/components/InstallWizard.tsx | 2 +- tests/component/AdoptValuesGate.test.tsx | 214 +++++++++++++++++++++++ 3 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 source/components/AdoptValuesGate.tsx create mode 100644 tests/component/AdoptValuesGate.test.tsx diff --git a/source/components/AdoptValuesGate.tsx b/source/components/AdoptValuesGate.tsx new file mode 100644 index 0000000..24528d2 --- /dev/null +++ b/source/components/AdoptValuesGate.tsx @@ -0,0 +1,170 @@ +import { useState, useMemo, useEffect } from 'react'; +import { Box, Text } from 'ink'; +import { Select } from './ui.js'; +import type { ShardManifest, ShardSchema } from '../runtime/types.js'; +import Header from './Header.js'; +import InstallWizard, { type WizardResult, formatValue } from './InstallWizard.js'; +import { + mergePrefill, + missingValueKeys, + resolveComputedDefaults, + defaultModuleSelections, +} from '../core/install-planner.js'; +import { isComputedDefault } from '../core/schema.js'; + +/** + * Adopt's value-collection gate (#104). + * + * Install interrogates a clean target step-by-step; adopt's user already + * *has* a populated vault, so opening with the full `InstallWizard` reads + * as a fresh-install flow shoehorned onto a retrofit. Instead, adopt opens + * on a single page that surfaces the exact values that will drive + * classification (`adopt-planner.ts::classifyAdoption` renders the shard's + * `.njk` against them) — resolved defaults, computed defaults, and any + * `--values` prefill, each labelled with its provenance — and lets the user + * accept them in one keystroke or drop into the wizard to edit. + * + * "Override individually" reuses `InstallWizard` verbatim rather than + * forking the value-iteration / computed-preview / module-review logic. + * + * The `--yes` / `--values` non-interactive paths never reach this component + * (the machine resolves values directly), so they are unchanged. + */ +interface AdoptValuesGateProps { + manifest: ShardManifest; + schema: ShardSchema; + /** Raw `--values` prefill (pre-merge), used for provenance + override seed. */ + prefillValues: Record; + onComplete: (result: WizardResult) => void; + onCancel: () => void; + onError: (err: Error) => void; +} + +type Provenance = 'from --values' | 'computed' | 'default'; + +export default function AdoptValuesGate({ + manifest, + schema, + prefillValues, + onComplete, + onCancel, + onError, +}: AdoptValuesGateProps) { + const merged = useMemo( + () => mergePrefill(schema, prefillValues), + [schema, prefillValues], + ); + + // Required values with no default and not supplied via `--values`: there + // is no confident "use these" set to confirm, so fall straight through to + // the per-value wizard (adopt's pre-#104 behaviour for this shard shape). + // `missingValueKeys` is fed the *merged* map (literal defaults filled), + // exactly as the `--yes` path does (`use-adopt-machine.ts::runNonInteractive`) + // — feeding it the raw prefill would flag every default-bearing key as + // "missing" and wrongly force override for the common all-defaults shard. + const hasMissingRequired = useMemo( + () => missingValueKeys(schema, merged).length > 0, + [schema, merged], + ); + + const [mode, setMode] = useState<'confirm' | 'override'>( + hasMissingRequired ? 'override' : 'confirm', + ); + const [resolved, setResolved] = useState>(merged); + + // Resolve computed defaults once, before the confirm page reads them. + // Done in an effect (not render) to keep onError off the render path and + // dodge React 18 strict-mode double-fire — same pattern as InstallWizard. + useEffect(() => { + if (hasMissingRequired) return; + try { + setResolved(resolveComputedDefaults(schema, merged)); + } catch (err) { + onError(err as Error); + } + // Mount-only — schema/prefill are stable within a gate session. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (mode === 'override') { + return ( + [id, 0]), + )} + alwaysIncludedFileCount={0} + onComplete={onComplete} + onCancel={onCancel} + onError={onError} + /> + ); + } + + const moduleCount = Object.keys(schema.modules).length; + const valueKeys = Object.keys(schema.values); + + return ( + +
+ + These values will be used to compare the shard against your vault: + + + {valueKeys.length === 0 ? ( + (this shard declares no values) + ) : ( + valueKeys.map((key) => ( + + {key}: + {formatValue(resolved[key])} + ({provenanceOf(key, schema, prefillValues)}) + + )) + )} + + + + Modules: all {moduleCount} included (override to customize) + + +