diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 393bbf5..550876b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -28,7 +28,13 @@ Decimal phases appear between their surrounding integers in numeric order. 2. The Backend Schema Registry defines at least Azure Blob, S3, and S3-compatible backends with typed field definitions 3. Zod validation schemas can be derived from the registry and validate correct/incorrect input correctly 4. The WizardState useReducer store initializes, accepts dispatch actions, and state is accessible via Context -**Plans**: TBD +**Plans**: 4 plans + +Plans: +- [ ] 01-01-PLAN.md — Scaffold Vite 6 + React 18 + TypeScript 5, install all Phase 1 deps, configure Tailwind v4, set up Vitest +- [ ] 01-02-PLAN.md — Create Backend Schema Registry (registry.ts) and Wave 0 test stubs for SC-2, SC-3, SC-4 +- [ ] 01-03-PLAN.md — Implement Zod schemas derived from registry (index.ts), all schema tests green +- [ ] 01-04-PLAN.md — Implement WizardState types, pure reducer, Context provider, and wire into App ### Phase 2: Generators **Goal**: Given a completed wizard state, the app can produce correct, deployment-ready file content for all output types @@ -73,7 +79,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. Foundation | 0/? | Not started | - | +| 1. Foundation | 0/4 | Not started | - | | 2. Generators | 0/? | Not started | - | | 3. Wizard UI | 0/? | Not started | - | | 4. Review, Download & Security | 0/? | Not started | - | diff --git a/.planning/phases/01-foundation/01-01-PLAN.md b/.planning/phases/01-foundation/01-01-PLAN.md new file mode 100644 index 0000000..ca5bb4f --- /dev/null +++ b/.planning/phases/01-foundation/01-01-PLAN.md @@ -0,0 +1,196 @@ +--- +phase: 01-foundation +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - vite.config.ts + - tsconfig.json + - tsconfig.app.json + - tsconfig.node.json + - src/index.css + - src/main.tsx + - src/App.tsx + - vitest.config.ts + - index.html +autonomous: true +requirements: [] + +must_haves: + truths: + - "npm run dev starts the Vite dev server with no console errors" + - "npm run build succeeds with no TypeScript errors" + - "npx vitest run exits 0 (even with zero test files)" + - "Tailwind utility classes applied in JSX produce visible styles" + artifacts: + - path: "vite.config.ts" + provides: "Vite build config with React plugin and Tailwind v4 plugin" + contains: "@tailwindcss/vite" + - path: "src/index.css" + provides: "Tailwind v4 CSS entry point" + contains: "@import \"tailwindcss\"" + - path: "vitest.config.ts" + provides: "Vitest configuration for pure-function unit tests" + contains: "environment: 'node'" + - path: "package.json" + provides: "All Phase 1 dependencies declared" + contains: "zod" + key_links: + - from: "vite.config.ts" + to: "src/index.css" + via: "tailwindcss() plugin processes @import directive" + pattern: "tailwindcss\\(\\)" + - from: "src/main.tsx" + to: "src/index.css" + via: "import './index.css'" + pattern: "import.*index\\.css" +--- + + +Bootstrap the project: scaffold a Vite 6 + React 18 + TypeScript 5 SPA, install all Phase 1 dependencies, configure Tailwind v4 via the Vite plugin, and install Vitest so automated tests can run. + +Purpose: Every downstream plan needs a working project with correct tooling. Getting the scaffold wrong (e.g., Tailwind v3 path, old shadcn CLI) causes painful migration later. +Output: A runnable dev server, a passing build, and Vitest ready to execute unit tests. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md + + + + + + + + + + + + Task 1: Scaffold and install all Phase 1 dependencies + package.json, vite.config.ts, src/index.css, src/main.tsx, src/App.tsx, index.html, tsconfig.json, tsconfig.app.json, tsconfig.node.json + + First verify Node.js version meets Vite 6 requirements: + ``` + node --version + ``` + Must be 20.19+ or 22.12+. If below, stop and report — do not proceed. + + Scaffold the project in the current directory (not a subdirectory — the repo root IS the project): + ``` + npm create vite@latest . -- --template react-ts + ``` + Accept overwrite prompts if any files exist. + + Install all Phase 1 production and dev dependencies in one command: + ``` + npm install + npm install zod react-hook-form @hookform/resolvers + npm install -D tailwindcss @tailwindcss/vite + npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom + ``` + + Update vite.config.ts to add the Tailwind v4 plugin (use @tailwindcss/vite, NOT postcss): + ```typescript + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import tailwindcss from '@tailwindcss/vite'; + + export default defineConfig({ + plugins: [react(), tailwindcss()], + }); + ``` + + Replace the contents of src/index.css with the single Tailwind v4 import directive: + ```css + @import "tailwindcss"; + ``` + Do NOT add tailwind.config.js, do NOT add postcss.config.js, do NOT add content globs. + + Clean up the Vite default boilerplate in src/App.tsx — replace with a minimal placeholder that uses one Tailwind class to confirm styles work: + ```tsx + export default function App() { + return ( +
+

Ready2Blob

+
+ ); + } + ``` + + Verify the dev server starts without console errors: + ``` + npm run dev + ``` + Then Ctrl+C. Verify the build succeeds: + ``` + npm run build + ``` +
+ + npm run build 2>&1 | tail -5 + + npm run build exits 0 with no TypeScript errors. npm run dev starts without console errors (manual check: open browser, check devtools console). +
+ + + Task 2: Configure Vitest for pure-function unit tests + vitest.config.ts + + Create vitest.config.ts at the project root with the node environment (pure functions do not need a DOM): + ```typescript + import { defineConfig } from 'vitest/config'; + + export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, + }); + ``` + + Add a test script to package.json if not already present: + ```json + "test": "vitest run" + ``` + + Run the test suite to confirm Vitest executes (zero tests is acceptable at this stage): + ``` + npx vitest run + ``` + Expected output: "No test files found" or similar — this is correct. Exit code must be 0. + + + npx vitest run 2>&1; echo "exit: $?" + + npx vitest run exits 0. vitest.config.ts exists with environment: 'node' and globals: true. + + +
+ + +Run in sequence after both tasks complete: +1. `npm run build` — exits 0, no TypeScript errors +2. `npx vitest run` — exits 0 +3. Manual: `npm run dev`, open browser, confirm no console errors and "Ready2Blob" heading renders with bold styling + + + +- Vite 6 + React 18 + TypeScript 5 project builds cleanly +- Tailwind v4 configured via @tailwindcss/vite plugin (no postcss.config.js, no tailwind.config.js) +- All Phase 1 dependencies installed: zod, react-hook-form, @hookform/resolvers, vitest, @testing-library/react +- Vitest configured and executable (exits 0 with no test files) +- src/index.css contains only `@import "tailwindcss"` for Tailwind v4 + + + +After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` using the summary template. + diff --git a/.planning/phases/01-foundation/01-02-PLAN.md b/.planning/phases/01-foundation/01-02-PLAN.md new file mode 100644 index 0000000..8f89d90 --- /dev/null +++ b/.planning/phases/01-foundation/01-02-PLAN.md @@ -0,0 +1,439 @@ +--- +phase: 01-foundation +plan: 02 +type: execute +wave: 2 +depends_on: + - 01-01 +files_modified: + - src/schemas/registry.ts + - src/schemas/registry.test.ts + - src/schemas/index.test.ts + - src/store/reducer.test.ts +autonomous: true +requirements: [] + +must_haves: + truths: + - "BACKEND_REGISTRY exports entries for 'azureblob', 's3', and 's3-compatible'" + - "Each registry entry is an array of FieldDef objects with the correct rclone config keys" + - "Test stubs exist for all three success criteria (SC-2, SC-3, SC-4) and fail with informative messages" + - "npx vitest run fails on registry.test.ts (RED — implementation not yet written for index.ts and reducer)" + artifacts: + - path: "src/schemas/registry.ts" + provides: "BackendType union, FieldDef interface, BACKEND_REGISTRY constant" + exports: ["BackendType", "FieldDef", "BACKEND_REGISTRY"] + - path: "src/schemas/registry.test.ts" + provides: "Test stubs for SC-2 (registry structure verification)" + contains: "BACKEND_REGISTRY" + - path: "src/schemas/index.test.ts" + provides: "Test stubs for SC-3 (Zod schema safeParse verification)" + contains: "BACKEND_SCHEMAS" + - path: "src/store/reducer.test.ts" + provides: "Test stubs for SC-4 (WizardState reducer action verification)" + contains: "wizardReducer" + key_links: + - from: "src/schemas/registry.ts" + to: "rclone config keys" + via: "FieldDef.key values must match rclone's INI key names exactly" + pattern: "key:.*'account'|key:.*'access_key_id'|key:.*'secret_access_key'" +--- + + +Create the Backend Schema Registry — the single source of truth for all backend field definitions — and write the Wave 0 test stubs that will drive implementation in Plans 03 and 04. + +Purpose: The registry is architecturally critical: Phase 2 generators use its keys to build rclone.conf, Phase 3 uses it to render dynamic forms. Wrong keys here cause silently broken configs. Writing test stubs first establishes the acceptance criteria before any implementation. +Output: src/schemas/registry.ts with typed field definitions for three backends, plus three test stub files (failing, per TDD RED phase). + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/phases/01-foundation/01-01-SUMMARY.md + + + + + + + + + + + + Task 1: Create the Backend Schema Registry + src/schemas/registry.ts + + - BackendType is a union: 'azureblob' | 's3' | 's3-compatible' + - FieldDef has: key (string), label (string), inputType ('text'|'password'|'select'|'toggle'), required (boolean), and optional placeholder, helpText, options + - BACKEND_REGISTRY['azureblob'] has at least 3 fields: account (required), key (optional), sas_url (optional) + - BACKEND_REGISTRY['s3'] has at least 4 fields: provider (required, select, value='AWS'), access_key_id (required), secret_access_key (required, password), region (required) + - BACKEND_REGISTRY['s3-compatible'] has at least 5 fields: provider (required, select, value='Other'), access_key_id (required), secret_access_key (required, password), endpoint (required), region (optional) + - All FieldDef.key values use snake_case matching rclone's actual config key names (not camelCase) + + + Create src/schemas/registry.ts: + + ```typescript + // src/schemas/registry.ts + // Backend Schema Registry — single source of truth for all rclone backend field definitions. + // IMPORTANT: FieldDef.key values MUST match rclone config key names exactly. + // These keys are used by Phase 2 generators to build rclone.conf INI content. + // Verify against https://rclone.org/azureblob/ and https://rclone.org/s3/ before Phase 2. + + export type BackendType = 'azureblob' | 's3' | 's3-compatible'; + + export interface FieldDef { + key: string; // MUST match rclone config key exactly (snake_case) + label: string; + inputType: 'text' | 'password' | 'select' | 'toggle'; + required: boolean; + placeholder?: string; + helpText?: string; + options?: { value: string; label: string }[]; // for inputType: 'select' + } + + export const BACKEND_REGISTRY: Record = { + azureblob: [ + { + key: 'account', + label: 'Storage Account Name', + inputType: 'text', + required: true, + placeholder: 'mystorageaccount', + helpText: 'The storage account name (not the full URL)', + }, + { + key: 'key', + label: 'Access Key', + inputType: 'password', + required: false, + helpText: 'Base64-encoded storage account key. Provide either this or a SAS URL, not both.', + }, + { + key: 'sas_url', + label: 'SAS URL', + inputType: 'password', + required: false, + placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...', + helpText: 'Full SAS URL including account and container. Provide either this or an access key, not both.', + }, + ], + s3: [ + { + key: 'provider', + label: 'Provider', + inputType: 'select', + required: true, + options: [{ value: 'AWS', label: 'Amazon S3' }], + }, + { + key: 'access_key_id', + label: 'Access Key ID', + inputType: 'text', + required: true, + placeholder: 'AKIAIOSFODNN7EXAMPLE', + }, + { + key: 'secret_access_key', + label: 'Secret Access Key', + inputType: 'password', + required: true, + }, + { + key: 'region', + label: 'Region', + inputType: 'text', + required: true, + placeholder: 'us-east-1', + }, + ], + 's3-compatible': [ + { + key: 'provider', + label: 'Provider', + inputType: 'select', + required: true, + options: [{ value: 'Other', label: 'S3-Compatible' }], + helpText: 'Covers Wasabi, MinIO, Cloudflare R2, and any S3-compatible storage', + }, + { + key: 'access_key_id', + label: 'Access Key ID', + inputType: 'text', + required: true, + }, + { + key: 'secret_access_key', + label: 'Secret Access Key', + inputType: 'password', + required: true, + }, + { + key: 'endpoint', + label: 'Endpoint URL', + inputType: 'text', + required: true, + placeholder: 'https://s3.wasabisys.com', + helpText: 'The S3-compatible endpoint URL for your storage provider', + }, + { + key: 'region', + label: 'Region', + inputType: 'text', + required: false, + placeholder: 'us-east-1', + helpText: 'Optional for most S3-compatible providers', + }, + ], + }; + ``` + + + npx vitest run src/schemas/registry.test.ts 2>&1; echo "exit: $?" + + registry.ts exports BackendType, FieldDef, and BACKEND_REGISTRY. All three backend entries have the correct rclone-compatible key names. registry.test.ts passes. + + + + Task 2: Write Wave 0 test stubs for SC-2, SC-3, SC-4 + src/schemas/registry.test.ts, src/schemas/index.test.ts, src/store/reducer.test.ts + + Create three test stub files. These are the Wave 0 test scaffolds — they define acceptance criteria now so Plans 03 and 04 implement against them (TDD RED phase). Tests for registry.test.ts should PASS; tests for index.test.ts and reducer.test.ts will FAIL (the implementation files don't exist yet). + + **src/schemas/registry.test.ts** (SC-2 — verifies registry structure; should PASS after Task 1): + ```typescript + import { describe, it, expect } from 'vitest'; + import { BACKEND_REGISTRY, BackendType } from './registry'; + + const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible']; + + describe('Backend Schema Registry', () => { + it('exports all three required backend types', () => { + for (const backend of EXPECTED_BACKENDS) { + expect(BACKEND_REGISTRY[backend]).toBeDefined(); + } + }); + + it('each backend has at least one field definition', () => { + for (const backend of EXPECTED_BACKENDS) { + expect(BACKEND_REGISTRY[backend].length).toBeGreaterThan(0); + } + }); + + it('each FieldDef has a non-empty key (snake_case, no camelCase)', () => { + for (const backend of EXPECTED_BACKENDS) { + for (const field of BACKEND_REGISTRY[backend]) { + expect(field.key).toBeTruthy(); + // Reject camelCase — rclone keys are snake_case or lowercase + expect(field.key).not.toMatch(/[A-Z]/); + } + } + }); + + it('each FieldDef has a non-empty label', () => { + for (const backend of EXPECTED_BACKENDS) { + for (const field of BACKEND_REGISTRY[backend]) { + expect(field.label).toBeTruthy(); + } + } + }); + + it('Azure Blob has account field (required)', () => { + const accountField = BACKEND_REGISTRY.azureblob.find(f => f.key === 'account'); + expect(accountField).toBeDefined(); + expect(accountField!.required).toBe(true); + }); + + it('S3 has access_key_id, secret_access_key, and region fields', () => { + const keys = BACKEND_REGISTRY.s3.map(f => f.key); + expect(keys).toContain('access_key_id'); + expect(keys).toContain('secret_access_key'); + expect(keys).toContain('region'); + }); + + it('S3-compatible has endpoint field (required)', () => { + const endpointField = BACKEND_REGISTRY['s3-compatible'].find(f => f.key === 'endpoint'); + expect(endpointField).toBeDefined(); + expect(endpointField!.required).toBe(true); + }); + }); + ``` + + **src/schemas/index.test.ts** (SC-3 — will FAIL until Plan 03 creates src/schemas/index.ts): + ```typescript + import { describe, it, expect } from 'vitest'; + import { BACKEND_SCHEMAS } from './index'; + + describe('Azure Blob Zod schema', () => { + it('accepts valid account + access key', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ + account: 'mystorageaccount', + key: 'dGVzdGtleQ==', + }); + expect(result.success).toBe(true); + }); + + it('accepts valid account + sas_url', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ + account: 'mystorageaccount', + sas_url: 'https://mystorageaccount.blob.core.windows.net/?sv=2021-01-01', + }); + expect(result.success).toBe(true); + }); + + it('rejects empty account (required field)', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: '' }); + expect(result.success).toBe(false); + }); + + it('rejects missing account (required field)', () => { + const result = BACKEND_SCHEMAS.azureblob.safeParse({ key: 'dGVzdGtleQ==' }); + expect(result.success).toBe(false); + }); + }); + + describe('S3 Zod schema', () => { + it('accepts valid S3 credentials', () => { + const result = BACKEND_SCHEMAS.s3.safeParse({ + provider: 'AWS', + access_key_id: 'AKIAIOSFODNN7EXAMPLE', + secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', + region: 'us-east-1', + }); + expect(result.success).toBe(true); + }); + + it('rejects missing access_key_id (required field)', () => { + const result = BACKEND_SCHEMAS.s3.safeParse({ + provider: 'AWS', + secret_access_key: 'secret', + region: 'us-east-1', + }); + expect(result.success).toBe(false); + }); + }); + + describe('S3-compatible Zod schema', () => { + it('accepts valid S3-compatible credentials with endpoint', () => { + const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({ + provider: 'Other', + access_key_id: 'mykey', + secret_access_key: 'mysecret', + endpoint: 'https://s3.wasabisys.com', + }); + expect(result.success).toBe(true); + }); + + it('rejects missing endpoint (required for s3-compatible)', () => { + const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({ + provider: 'Other', + access_key_id: 'mykey', + secret_access_key: 'mysecret', + }); + expect(result.success).toBe(false); + }); + }); + ``` + + **src/store/reducer.test.ts** (SC-4 — will FAIL until Plan 04 creates reducer.ts and types.ts): + ```typescript + import { describe, it, expect } from 'vitest'; + import { wizardReducer } from './reducer'; + import { INITIAL_STATE, WizardState } from './types'; + + describe('wizardReducer', () => { + it('returns INITIAL_STATE on first call', () => { + // @ts-expect-error intentional undefined action for initialization test + const state = wizardReducer(undefined, { type: '@@INIT' }); + expect(state.currentStep).toBe(0); + expect(state.remote.backendType).toBeNull(); + expect(state.remote.name).toBe(''); + expect(state.remote.params).toEqual({}); + expect(state.deployment.includeInstall).toBe(false); + expect(state.deployment.configPath).toBe('machine-wide'); + }); + + it('SET_STEP updates currentStep', () => { + const state = wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 2 }); + expect(state.currentStep).toBe(2); + }); + + it('SET_BACKEND_TYPE updates remote.backendType', () => { + const state = wizardReducer(INITIAL_STATE, { type: 'SET_BACKEND_TYPE', payload: 'azureblob' }); + expect(state.remote.backendType).toBe('azureblob'); + }); + + it('SET_REMOTE_NAME updates remote.name', () => { + const state = wizardReducer(INITIAL_STATE, { type: 'SET_REMOTE_NAME', payload: 'my-blob' }); + expect(state.remote.name).toBe('my-blob'); + }); + + it('SET_REMOTE_PARAMS updates remote.params', () => { + const state = wizardReducer(INITIAL_STATE, { + type: 'SET_REMOTE_PARAMS', + payload: { account: 'myaccount', key: 'mykey' }, + }); + expect(state.remote.params).toEqual({ account: 'myaccount', key: 'mykey' }); + }); + + it('SET_DEPLOYMENT partially updates deployment', () => { + const state = wizardReducer(INITIAL_STATE, { + type: 'SET_DEPLOYMENT', + payload: { includeInstall: true }, + }); + expect(state.deployment.includeInstall).toBe(true); + expect(state.deployment.configPath).toBe('machine-wide'); // unchanged + }); + + it('RESET returns to INITIAL_STATE', () => { + const modified: WizardState = { + ...INITIAL_STATE, + currentStep: 3, + remote: { name: 'test', backendType: 's3', params: { region: 'us-east-1' } }, + }; + const state = wizardReducer(modified, { type: 'RESET' }); + expect(state).toEqual(INITIAL_STATE); + }); + + it('is a pure function — does not mutate input state', () => { + const before = { ...INITIAL_STATE }; + wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 5 }); + expect(INITIAL_STATE.currentStep).toBe(before.currentStep); + }); + }); + ``` + + After creating all three files, run the test suite. registry.test.ts should pass; index.test.ts and reducer.test.ts will fail with "Cannot find module" errors — this is correct (RED phase). + + + npx vitest run src/schemas/registry.test.ts 2>&1; echo "registry exit: $?" + + All three test stub files exist. registry.test.ts passes (GREEN). index.test.ts and reducer.test.ts fail with module-not-found errors (RED — expected). Test stubs provide complete acceptance criteria for Plans 03 and 04. + + + + + +1. `npx vitest run src/schemas/registry.test.ts` — all tests pass +2. `npx vitest run src/schemas/index.test.ts` — fails with "Cannot find module './index'" (expected at this stage) +3. `npx vitest run src/store/reducer.test.ts` — fails with "Cannot find module './reducer'" (expected at this stage) +4. `cat src/schemas/registry.ts` — confirm no camelCase keys (grep for uppercase in key values) + + + +- src/schemas/registry.ts exports BackendType, FieldDef, BACKEND_REGISTRY +- All FieldDef.key values are snake_case matching rclone config key names +- BACKEND_REGISTRY contains entries for all three backends with correct field definitions +- registry.test.ts passes (7 tests green) +- index.test.ts and reducer.test.ts exist and fail with module-not-found (RED phase — correct) + + + +After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` using the summary template. + diff --git a/.planning/phases/01-foundation/01-03-PLAN.md b/.planning/phases/01-foundation/01-03-PLAN.md new file mode 100644 index 0000000..3a0b082 --- /dev/null +++ b/.planning/phases/01-foundation/01-03-PLAN.md @@ -0,0 +1,167 @@ +--- +phase: 01-foundation +plan: 03 +type: execute +wave: 3 +depends_on: + - 01-02 +files_modified: + - src/schemas/index.ts +autonomous: true +requirements: [] + +must_haves: + truths: + - "BACKEND_SCHEMAS['azureblob'].safeParse({account: 'x'}) returns success: true" + - "BACKEND_SCHEMAS['azureblob'].safeParse({account: ''}) returns success: false" + - "BACKEND_SCHEMAS['s3-compatible'].safeParse({...without endpoint}) returns success: false" + - "Zod schemas are derived programmatically from BACKEND_REGISTRY — no hand-written z.object() calls" + - "All 12 tests in src/schemas/index.test.ts pass" + artifacts: + - path: "src/schemas/index.ts" + provides: "BACKEND_SCHEMAS constant and BackendFormValues utility type" + exports: ["BACKEND_SCHEMAS", "BackendFormValues"] + key_links: + - from: "src/schemas/index.ts" + to: "src/schemas/registry.ts" + via: "imports BACKEND_REGISTRY and BackendType to build schemas programmatically" + pattern: "import.*BACKEND_REGISTRY.*registry" +--- + + +Implement the Zod schema builder that derives runtime validation schemas programmatically from the Backend Schema Registry. + +Purpose: Schema and registry must never drift — if someone adds a field to the registry, validation automatically covers it. Writing Zod schemas by hand separate from the registry breaks this invariant. +Output: src/schemas/index.ts with BACKEND_SCHEMAS and BackendFormValues type — all 12 tests in src/schemas/index.test.ts pass. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/01-foundation/01-02-SUMMARY.md + + + +```typescript +export type BackendType = 'azureblob' | 's3' | 's3-compatible'; + +export interface FieldDef { + key: string; + label: string; + inputType: 'text' | 'password' | 'select' | 'toggle'; + required: boolean; + placeholder?: string; + helpText?: string; + options?: { value: string; label: string }[]; +} + +export const BACKEND_REGISTRY: Record; +// azureblob fields: account (required), key (optional), sas_url (optional) +// s3 fields: provider (required), access_key_id (required), secret_access_key (required), region (required) +// s3-compatible fields: provider (required), access_key_id (required), secret_access_key (required), endpoint (required), region (optional) +``` + + + + + + + + + + + + + + + + Task 1: Implement schema builder and BACKEND_SCHEMAS + src/schemas/index.ts + + - buildZodSchema(backendType) constructs a z.object() from BACKEND_REGISTRY[backendType] + - required fields → z.string().min(1, '{label} is required') + - optional fields → z.string().optional() + - BACKEND_SCHEMAS is a const object keyed by BackendType + - BackendFormValues infers the TypeScript type from the schema using z.infer + - No z.object() calls hardcoded with field names — all field shapes derived from the registry loop + + + Run the failing test first to confirm RED state: + ``` + npx vitest run src/schemas/index.test.ts + ``` + Expected: "Cannot find module './index'" error. + + Create src/schemas/index.ts: + + ```typescript + // src/schemas/index.ts + // Zod schemas derived programmatically from the Backend Schema Registry. + // DO NOT hand-write z.object() calls with hardcoded field names — all shapes come from the registry. + // Adding a field to BACKEND_REGISTRY automatically adds it to validation. + + import { z } from 'zod'; + import { BACKEND_REGISTRY, BackendType } from './registry'; + + function buildZodSchema(backendType: BackendType): z.ZodObject> { + const fields = BACKEND_REGISTRY[backendType]; + const shape: Record = {}; + for (const field of fields) { + shape[field.key] = field.required + ? z.string().min(1, `${field.label} is required`) + : z.string().optional(); + } + return z.object(shape); + } + + export const BACKEND_SCHEMAS = { + azureblob: buildZodSchema('azureblob'), + s3: buildZodSchema('s3'), + 's3-compatible': buildZodSchema('s3-compatible'), + } as const; + + // Utility type: infer TypeScript type from a backend's Zod schema + export type BackendFormValues = + z.infer; + ``` + + Run the tests: + ``` + npx vitest run src/schemas/index.test.ts + ``` + All tests must pass (GREEN). If any fail, diagnose and fix — do not move on with failing tests. + + Run the full test suite to confirm no regressions: + ``` + npx vitest run + ``` + + + npx vitest run src/schemas/index.test.ts 2>&1 + + All tests in src/schemas/index.test.ts pass. BACKEND_SCHEMAS exported with entries for all three backends. BackendFormValues type exported. npx vitest run (full suite) exits 0 with registry and schema tests green. + + + + + +1. `npx vitest run src/schemas/index.test.ts` — all tests pass (12 tests green) +2. `npx vitest run src/schemas/registry.test.ts` — still passing (no regression) +3. `npx vitest run` — full suite green for all schema tests +4. Confirm src/schemas/index.ts has no hardcoded field names in z.object() — only the loop over BACKEND_REGISTRY + + + +- src/schemas/index.ts creates Zod schemas by looping over BACKEND_REGISTRY field definitions +- All 12 tests in src/schemas/index.test.ts pass +- BACKEND_SCHEMAS and BackendFormValues are exported +- Full test suite (registry + index) exits 0 + + + +After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md` using the summary template. + diff --git a/.planning/phases/01-foundation/01-04-PLAN.md b/.planning/phases/01-foundation/01-04-PLAN.md new file mode 100644 index 0000000..664034d --- /dev/null +++ b/.planning/phases/01-foundation/01-04-PLAN.md @@ -0,0 +1,304 @@ +--- +phase: 01-foundation +plan: 04 +type: execute +wave: 3 +depends_on: + - 01-02 +files_modified: + - src/store/types.ts + - src/store/reducer.ts + - src/store/context.tsx + - src/App.tsx +autonomous: true +requirements: [] + +must_haves: + truths: + - "wizardReducer(INITIAL_STATE, {type:'SET_STEP', payload:2}) returns state with currentStep: 2" + - "wizardReducer(INITIAL_STATE, {type:'RESET'}) returns INITIAL_STATE" + - "wizardReducer is a pure function — does not mutate its input" + - "WizardProvider wraps App.tsx and exposes state + dispatch via useWizard hook" + - "All 8 tests in src/store/reducer.test.ts pass" + - "WizardState is never written to localStorage or sessionStorage" + artifacts: + - path: "src/store/types.ts" + provides: "WizardState interface, WizardAction union type, INITIAL_STATE constant" + exports: ["WizardState", "WizardAction", "INITIAL_STATE"] + - path: "src/store/reducer.ts" + provides: "Pure wizardReducer function" + exports: ["wizardReducer"] + - path: "src/store/context.tsx" + provides: "WizardContext, WizardProvider component, useWizard hook" + exports: ["WizardProvider", "useWizard"] + key_links: + - from: "src/store/context.tsx" + to: "src/store/reducer.ts" + via: "useReducer(wizardReducer, INITIAL_STATE)" + pattern: "useReducer.*wizardReducer" + - from: "src/App.tsx" + to: "src/store/context.tsx" + via: "WizardProvider wraps entire app" + pattern: "WizardProvider" +--- + + +Implement the WizardState store: types, a pure reducer function handling all action types, a Context provider, and the useWizard hook. Wire WizardProvider into App.tsx so all future components can access state without prop drilling. + +Purpose: Per-step local state causes data loss on back-navigation (a known anti-pattern). All form data must live in this centralized store. SECU-03 requires that state is never persisted to browser storage. +Output: Complete wizard state infrastructure with all 8 reducer tests passing and WizardProvider wired in App. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/01-foundation/01-02-SUMMARY.md + + + + + + + + + + + + + + + + + + + + + + + + Task 1: Implement WizardState types and pure reducer + src/store/types.ts, src/store/reducer.ts + + - INITIAL_STATE has currentStep:0, remote.name:'', remote.backendType:null, remote.params:{}, deployment.includeInstall:false, deployment.configPath:'machine-wide', deployment.scriptTargets:['intune','rmm'] + - SET_STEP replaces currentStep with payload + - SET_BACKEND_TYPE replaces remote.backendType with payload + - SET_REMOTE_NAME replaces remote.name with payload + - SET_REMOTE_PARAMS replaces remote.params with payload (full replacement, not merge) + - SET_DEPLOYMENT merges payload into deployment (partial update via spread) + - RESET returns INITIAL_STATE + - Default case returns state unchanged (required for React strict mode double-invocation) + - Reducer is pure: returns new object, never mutates input + + + Run the failing test to confirm RED state: + ``` + npx vitest run src/store/reducer.test.ts + ``` + Expected: "Cannot find module './reducer'" error. + + Create src/store/types.ts: + ```typescript + // src/store/types.ts + // WizardState shape, action union type, and initial state. + // SECURITY: This state is exclusively in-memory. + // NEVER add localStorage.setItem, sessionStorage.setItem, or IndexedDB here. + // Closing the browser tab is the intended "clear credentials" operation (SECU-03). + + import type { BackendType } from '../schemas/registry'; + + // Re-export BackendType so store consumers import from one place + export type { BackendType }; + + export interface WizardState { + currentStep: number; + remote: { + name: string; + backendType: BackendType | null; + params: Record; // backend-specific key/value pairs (rclone config keys) + }; + deployment: { + includeInstall: boolean; + configPath: 'machine-wide' | 'user-profile'; // machine-wide = C:\ProgramData\rclone\ + scriptTargets: ('intune' | 'rmm')[]; + }; + } + + export type WizardAction = + | { type: 'SET_STEP'; payload: number } + | { type: 'SET_BACKEND_TYPE'; payload: BackendType } + | { type: 'SET_REMOTE_NAME'; payload: string } + | { type: 'SET_REMOTE_PARAMS'; payload: Record } + | { type: 'SET_DEPLOYMENT'; payload: Partial } + | { type: 'RESET' }; + + export const INITIAL_STATE: WizardState = { + currentStep: 0, + remote: { + name: '', + backendType: null, + params: {}, + }, + deployment: { + includeInstall: false, + configPath: 'machine-wide', + scriptTargets: ['intune', 'rmm'], + }, + }; + ``` + + Create src/store/reducer.ts: + ```typescript + // src/store/reducer.ts + // Pure reducer — no side effects, no localStorage, no async operations. + + import { WizardState, WizardAction, INITIAL_STATE } from './types'; + + export function wizardReducer( + state: WizardState = INITIAL_STATE, + action: WizardAction + ): WizardState { + switch (action.type) { + case 'SET_STEP': + return { ...state, currentStep: action.payload }; + + case 'SET_BACKEND_TYPE': + return { + ...state, + remote: { ...state.remote, backendType: action.payload }, + }; + + case 'SET_REMOTE_NAME': + return { + ...state, + remote: { ...state.remote, name: action.payload }, + }; + + case 'SET_REMOTE_PARAMS': + return { + ...state, + remote: { ...state.remote, params: action.payload }, + }; + + case 'SET_DEPLOYMENT': + return { + ...state, + deployment: { ...state.deployment, ...action.payload }, + }; + + case 'RESET': + return INITIAL_STATE; + + default: + return state; + } + } + ``` + + Run tests: + ``` + npx vitest run src/store/reducer.test.ts + ``` + All 8 tests must pass (GREEN). Fix any failures before proceeding. + + + npx vitest run src/store/reducer.test.ts 2>&1 + + All 8 tests in src/store/reducer.test.ts pass. wizardReducer is pure (immutable state transitions). INITIAL_STATE matches expected shape. + + + + Task 2: Create WizardContext provider and wire into App + src/store/context.tsx, src/App.tsx + + Create src/store/context.tsx: + ```typescript + // src/store/context.tsx + // WizardContext, WizardProvider, and useWizard hook. + // useWizard throws if used outside WizardProvider — prevents silent "undefined state" bugs. + + import React, { createContext, useContext, useReducer } from 'react'; + import { WizardState, WizardAction, INITIAL_STATE } from './types'; + import { wizardReducer } from './reducer'; + + interface WizardContextValue { + state: WizardState; + dispatch: React.Dispatch; + } + + const WizardContext = createContext(null); + + export function WizardProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(wizardReducer, INITIAL_STATE); + return ( + + {children} + + ); + } + + export function useWizard(): WizardContextValue { + const ctx = useContext(WizardContext); + if (!ctx) throw new Error('useWizard must be used inside '); + return ctx; + } + ``` + + Update src/App.tsx to wrap with WizardProvider: + ```tsx + // src/App.tsx + import { WizardProvider } from './store/context'; + + export default function App() { + return ( + +
+

Ready2Blob

+
+
+ ); + } + ``` + + Verify the build still passes (context.tsx is a React file — TypeScript must accept it): + ``` + npm run build + ``` + + Run the full test suite: + ``` + npx vitest run + ``` + Expected: registry.test.ts (7), schemas/index.test.ts (wait — Plan 03 must complete first), reducer.test.ts (8) pass. If running before Plan 03, index.test.ts may still fail — that is acceptable; this plan's scope is the store only. +
+ + npm run build 2>&1 | tail -5 && npx vitest run src/store/reducer.test.ts 2>&1 | tail -10 + + src/store/context.tsx exports WizardProvider and useWizard. App.tsx wraps content with WizardProvider. npm run build exits 0. All 8 reducer tests pass. +
+ +
+ + +1. `npx vitest run src/store/reducer.test.ts` — 8 tests pass +2. `npm run build` — exits 0, no TypeScript errors +3. Confirm no localStorage/sessionStorage references in any store file: + `grep -r "localStorage\|sessionStorage\|IndexedDB" src/store/` — must return empty +4. Manual: `npm run dev` → browser opens → no console errors + + + +- src/store/types.ts exports WizardState, WizardAction, INITIAL_STATE, BackendType +- src/store/reducer.ts exports wizardReducer as a pure function (8 tests green) +- src/store/context.tsx exports WizardProvider and useWizard hook +- App.tsx wraps content in WizardProvider +- No localStorage, sessionStorage, or IndexedDB access in any store file +- npm run build exits 0 + + + +After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md` using the summary template. +