--- phase: 06-new-backends plan: "02" type: execute wave: 1 depends_on: - "06-00" files_modified: - src/schemas/registry.ts - src/schemas/index.ts - src/generators/rclone-conf.ts - src/components/wizard/SftpAuthToggle.tsx autonomous: true requirements: - BACK-02 must_haves: truths: - "BACKEND_REGISTRY has sftp entry with host (required), user (required), pass (optional), key_pem (optional)" - "SftpAuthToggle renders a Password tab and Private Key tab; inactive tab's field is CSS-hidden (not unmounted)" - "Switching SFTP auth tabs does not clear the hidden field value (CSS-hidden, not conditional render)" - "buildRcloneConf(sftpPasswordState) outputs 'type = sftp' with pass, omits key_pem" - "buildRcloneConf(sftpKeyState) outputs 'type = sftp' with key_pem, omits pass" artifacts: - path: "src/schemas/registry.ts" provides: "sftp entry in BACKEND_REGISTRY" contains: "key_pem" - path: "src/components/wizard/SftpAuthToggle.tsx" provides: "SFTP password vs private-key toggle following AzureAuthToggle CSS-hidden pattern" exports: ["SftpAuthToggle"] - path: "src/generators/rclone-conf.ts" provides: "RCLONE_TYPE_MAP sftp entry" contains: "sftp.*sftp" key_links: - from: "src/components/wizard/SftpAuthToggle.tsx" to: "src/components/ui/PasswordField.tsx" via: "PasswordField component for both pass and key_pem fields" pattern: "PasswordField.*pass|PasswordField.*key_pem" - from: "src/schemas/registry.ts" to: "BACKEND_REGISTRY sftp entry" via: "pass and key_pem fields required: false — SftpAuthToggle registers them directly" pattern: "key_pem.*required.*false" --- Add SFTP to the data layer and create the SftpAuthToggle component. Purpose: SFTP is the only new backend requiring a custom auth-method toggle (password vs private key). Isolating SFTP work in its own plan keeps Plan 01 clean and allows parallel execution. Output: sftp entry in registry.ts/index.ts/rclone-conf.ts; SftpAuthToggle.tsx component following the AzureAuthToggle CSS-hidden pattern. @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 @.planning/phases/06-new-backends/06-RESEARCH.md @.planning/phases/06-new-backends/06-00-SUMMARY.md From src/components/wizard/AzureAuthToggle.tsx: ```typescript import { useState } from 'react'; import type { UseFormRegister, FieldError } from 'react-hook-form'; import { PasswordField } from '../ui/PasswordField'; type AuthMethod = 'sas' | 'key'; interface AzureAuthToggleProps { register: UseFormRegister; errors: { key?: FieldError; sas_url?: FieldError; }; } export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) { const [authMethod, setAuthMethod] = useState('sas'); return (
{/* Both fields always registered — CSS toggling only */}
); } ``` From src/schemas/registry.ts (current sftp fields — to add): ```typescript // sftp registry entry (add to BACKEND_REGISTRY): sftp: { displayName: 'SFTP', description: 'SSH File Transfer Protocol', fields: [ { key: 'host', label: 'Host', inputType: 'text', required: true, placeholder: 'sftp.example.com', helpText: 'The SSH server hostname or IP address' }, { key: 'user', label: 'Username', inputType: 'text', required: true, placeholder: 'admin' }, { key: 'pass', label: 'Password', inputType: 'password', required: false, helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value.' }, { key: 'key_pem', label: 'Private Key (PEM)', inputType: 'password', required: false, helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)' }, ], }, ```
Task 1: Add sftp entry to registry.ts, index.ts, and rclone-conf.ts src/schemas/registry.ts, src/schemas/index.ts, src/generators/rclone-conf.ts - BACKEND_REGISTRY gains sftp entry with host (text, required), user (text, required), pass (password, optional), key_pem (password, optional) - pass helpText mentions rclone obscure requirement (security note, does not block generation) - BACKEND_SCHEMAS gains sftp entry via buildZodSchema('sftp') - RCLONE_TYPE_MAP gains sftp → 'sftp' - `npx vitest run src/schemas/registry.test.ts` passes all sftp assertions (host, user, pass, key_pem) - `npx vitest run src/generators/rclone-conf.test.ts` passes sftp password and sftp key describe blocks - `npx tsc --noEmit` is clean (no more "sftp missing from BACKEND_REGISTRY" error) **src/schemas/registry.ts:** Add sftp entry to BACKEND_REGISTRY (after 's3-compatible', before or after the entries added by Plan 01 — order does not matter): ```typescript sftp: { displayName: 'SFTP', description: 'SSH File Transfer Protocol', fields: [ { key: 'host', label: 'Host', inputType: 'text', required: true, placeholder: 'sftp.example.com', helpText: 'The SSH server hostname or IP address', }, { key: 'user', label: 'Username', inputType: 'text', required: true, placeholder: 'admin', }, { key: 'pass', label: 'Password', inputType: 'password', required: false, helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value instead of the plain password.', }, { key: 'key_pem', label: 'Private Key (PEM)', inputType: 'password', required: false, helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)', }, ], }, ``` **src/schemas/index.ts:** Add sftp to BACKEND_SCHEMAS: ```typescript sftp: buildZodSchema('sftp'), ``` **src/generators/rclone-conf.ts:** Add sftp to RCLONE_TYPE_MAP: ```typescript sftp: 'sftp', ``` Note: BackendType union already includes 'sftp' (added by Plan 01 Task 1). This plan only adds the registry/schema/map entries. If Plan 01 and Plan 02 run in parallel on separate branches, merge Plan 01 first then add sftp entries on top. npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts 2>&1 | tail -20 sftp entry in registry with all four field keys; sftp in BACKEND_SCHEMAS and RCLONE_TYPE_MAP; registry and rclone-conf tests fully green for all 7 backends; zero TypeScript errors Task 2: Create SftpAuthToggle.tsx src/components/wizard/SftpAuthToggle.tsx - Component renders a segmented control with two buttons: "Password" (left) and "Private Key" (right) - Default auth method is 'password' — Password tab is active on first render - Password tab active: pass field wrapper has class 'block', key_pem field wrapper has class 'hidden' - Private Key tab active: pass field wrapper has class 'hidden', key_pem field wrapper has class 'block' - Both fields are ALWAYS registered with react-hook-form (CSS-hidden, not conditional render) - Pass field: id="pass", label="Password", uses PasswordField - Key field: id="key_pem", label="Private Key (PEM)", uses PasswordField with multiline hint in helpText - Active button has class 'bg-blue-600 text-white', inactive has 'bg-white text-gray-700 hover:bg-gray-50' - Component accepts register and errors props matching SftpAuthToggleProps interface Create src/components/wizard/SftpAuthToggle.tsx following AzureAuthToggle.tsx exactly, with these substitutions: - AuthMethod type: 'password' | 'key' (was 'sas' | 'key') - Initial state: 'password' (was 'sas') - Tab 1 button text: "Password" (was "SAS URL"), onClick: setAuthMethod('password') - Tab 2 button text: "Private Key" (was "Access Key"), onClick: setAuthMethod('key') - Active condition for tab 1: authMethod === 'password' - Active condition for tab 2: authMethod === 'key' - Field 1 wrapper: className={authMethod === 'password' ? 'block' : 'hidden'} PasswordField: id="pass", label="Password", error={errors.pass}, registration={register('pass')}, helpText="SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value." - Field 2 wrapper: className={authMethod === 'key' ? 'block' : 'hidden'} PasswordField: id="key_pem", label="Private Key (PEM)", error={errors.key_pem}, registration={register('key_pem')}, helpText="Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)" Props interface: ```typescript interface SftpAuthToggleProps { register: UseFormRegister; errors: { pass?: FieldError; key_pem?: FieldError; }; } ``` The SFTP toggle test in RemoteConfigStep.test.tsx asserts: - getByText(/^password$/i, { selector: 'button' }) — button text must be exactly "Password" - getByText(/private key/i, { selector: 'button' }) — button text must contain "Private Key" - getByLabelText(/^password$/i) — PasswordField label must be exactly "Password" - getByLabelText(/private key.*pem/i) — PasswordField label must match "Private Key (PEM)" Ensure button text and label text match these patterns exactly. npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | grep -E "(BACK-02|SFTP|PASS|FAIL)" | head -20 SftpAuthToggle.tsx created; component matches AzureAuthToggle pattern; SFTP toggle tests in RemoteConfigStep.test.tsx will pass after Plan 03 wires it into RemoteConfigStep (component exists and is importable) `npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts` Expected: fully green — all 7 backends pass in both files. `npx tsc --noEmit` Expected: zero errors (sftp now present in BACKEND_REGISTRY satisfying the exhaustive Record type). - sftp registry entry with host, user, pass, key_pem (correct rclone key names) - SftpAuthToggle.tsx exists, exports SftpAuthToggle, follows AzureAuthToggle CSS-hidden pattern - sftp in BACKEND_SCHEMAS and RCLONE_TYPE_MAP - registry and rclone-conf tests fully green for all 7 backends - Zero TypeScript errors After completion, create `.planning/phases/06-new-backends/06-02-SUMMARY.md`