diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index dc2a84c..20af19f 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -46,7 +46,13 @@ Plans:
3. The Intune detection script exits 0 when both rclone.exe and the config file are present, and non-zero otherwise
4. The RMM script is self-contained, idempotent, and runs correctly in a generic SYSTEM context
5. Toggling "include rclone installation" adds a download step to all generated scripts; disabling it removes it
-**Plans**: TBD
+**Plans**: 4 plans
+
+Plans:
+- [ ] 02-01-PLAN.md — Wave 0 test stubs for all four generators + index.ts barrel
+- [ ] 02-02-PLAN.md — Implement buildRcloneConf (CONF-01)
+- [ ] 02-03-PLAN.md — Implement buildIntuneInstall + buildIntuneDetection + ps-helpers (DEPL-01, DEPL-02, DEPL-04, DEPL-05)
+- [ ] 02-04-PLAN.md — Implement buildRmmScript (DEPL-03, DEPL-04, DEPL-05)
### Phase 3: Wizard UI
**Goal**: An IT admin can navigate the full wizard from backend selection through deployment options without losing data
@@ -80,6 +86,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundation | 4/4 | Complete | 2026-03-26 |
-| 2. Generators | 0/? | Not started | - |
+| 2. Generators | 0/4 | Not started | - |
| 3. Wizard UI | 0/? | Not started | - |
| 4. Review, Download & Security | 0/? | Not started | - |
diff --git a/.planning/phases/02-generators/02-01-PLAN.md b/.planning/phases/02-generators/02-01-PLAN.md
new file mode 100644
index 0000000..9ed0901
--- /dev/null
+++ b/.planning/phases/02-generators/02-01-PLAN.md
@@ -0,0 +1,236 @@
+---
+phase: 02-generators
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - src/generators/rclone-conf.test.ts
+ - src/generators/intune-install.test.ts
+ - src/generators/intune-detection.test.ts
+ - src/generators/rmm-script.test.ts
+ - src/generators/index.ts
+autonomous: true
+requirements:
+ - CONF-01
+ - DEPL-01
+ - DEPL-02
+ - DEPL-03
+ - DEPL-04
+ - DEPL-05
+
+must_haves:
+ truths:
+ - "All four generator test files exist and fail RED (imports resolve, implementations missing)"
+ - "The index.ts barrel file exports all four generator functions"
+ - "npm test passes (passWithNoTests configured; stubs may skip or fail)"
+ artifacts:
+ - path: "src/generators/rclone-conf.test.ts"
+ provides: "Failing unit tests for CONF-01"
+ contains: "buildRcloneConf"
+ - path: "src/generators/intune-install.test.ts"
+ provides: "Failing unit tests for DEPL-01, DEPL-04, DEPL-05"
+ contains: "buildIntuneInstall"
+ - path: "src/generators/intune-detection.test.ts"
+ provides: "Failing unit tests for DEPL-02, DEPL-05"
+ contains: "buildIntuneDetection"
+ - path: "src/generators/rmm-script.test.ts"
+ provides: "Failing unit tests for DEPL-03, DEPL-04, DEPL-05"
+ contains: "buildRmmScript"
+ - path: "src/generators/index.ts"
+ provides: "Re-export barrel for all generators"
+ exports:
+ - buildRcloneConf
+ - buildIntuneInstall
+ - buildIntuneDetection
+ - buildRmmScript
+ key_links:
+ - from: "src/generators/rclone-conf.test.ts"
+ to: "src/generators/rclone-conf.ts"
+ via: "import { buildRcloneConf } from './rclone-conf'"
+ pattern: "from './rclone-conf'"
+ - from: "src/generators/index.ts"
+ to: "src/generators/*.ts"
+ via: "named re-exports"
+ pattern: "export.*from"
+---
+
+
+Write all four generator test files (failing) and the index.ts barrel before any implementation exists.
+
+Purpose: Nyquist rule — tests must exist before implementations. Plans 02, 03, 04 implement against these stubs. The barrel establishes the public API that Phase 4 will import from.
+Output: Four test files (RED), one barrel file, all four generator module paths defined.
+
+
+
+@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
+
+@src/store/types.ts
+@src/schemas/registry.ts
+
+
+
+
+
+From src/store/types.ts:
+```typescript
+export interface WizardState {
+ currentStep: number;
+ remote: {
+ name: string;
+ backendType: BackendType | null;
+ params: Record;
+ };
+ deployment: {
+ includeInstall: boolean;
+ configPath: 'machine-wide' | 'user-profile';
+ scriptTargets: ('intune' | 'rmm')[];
+ };
+}
+
+export const INITIAL_STATE: WizardState = {
+ currentStep: 0,
+ remote: { name: '', backendType: null, params: {} },
+ deployment: {
+ includeInstall: false,
+ configPath: 'machine-wide',
+ scriptTargets: ['intune', 'rmm'],
+ },
+};
+```
+
+From src/schemas/registry.ts:
+```typescript
+export type BackendType = 'azureblob' | 's3' | 's3-compatible';
+```
+
+
+
+
+
+ Task 1: Write test stubs for all four generators
+
+ src/generators/rclone-conf.test.ts,
+ src/generators/intune-install.test.ts,
+ src/generators/intune-detection.test.ts,
+ src/generators/rmm-script.test.ts
+
+
+ rclone-conf.test.ts:
+ - imports buildRcloneConf from './rclone-conf' (file does not exist yet — RED)
+ - azureblob: output contains '[my-azure]', 'type = azureblob', 'account = mystorageaccount', 'key = BASE64KEY', does NOT contain 'sas_url' (empty field omitted)
+ - s3: output contains '[my-s3]', 'type = s3', 'provider = AWS', 'access_key_id = AKID', 'secret_access_key = SKEY', 'region = us-east-1'
+ - s3-compatible: output contains 'type = s3' (NOT 'type = s3-compatible'), 'provider = Other', 'endpoint = https://s3.wasabisys.com', does NOT contain 'region' (empty field omitted)
+ - throws if backendType is null
+ - throws if remote name is empty string
+
+ intune-install.test.ts:
+ - imports buildIntuneInstall from './intune-install' (file does not exist yet — RED)
+ - machine-wide path: output contains 'C:\ProgramData\rclone'
+ - user-profile path: output contains '%APPDATA%\rclone'
+ - includeInstall=true: output contains 'https://downloads.rclone.org/rclone-current-windows-amd64.zip'
+ - includeInstall=false: output does NOT contain 'downloads.rclone.org'
+ - output contains '[System.IO.File]::WriteAllText' (UTF-8 no-BOM idiom present)
+ - output contains 'exit 0'
+ - output does NOT contain credential variable interpolation inside config content (no $account, $key inside here-string block)
+
+ intune-detection.test.ts:
+ - imports buildIntuneDetection from './intune-detection' (file does not exist yet — RED)
+ - machine-wide path: output contains 'C:\ProgramData\rclone'
+ - user-profile path: output contains '%APPDATA%\rclone'
+ - output contains 'exit 0' and 'exit 1'
+ - output contains 'Write-Output' (STDOUT signal)
+ - output does NOT contain '$ErrorActionPreference' (STDERR contamination risk)
+ - output does NOT contain 'Write-Error' or 'Write-Host' (no STDERR leakage)
+
+ rmm-script.test.ts:
+ - imports buildRmmScript from './rmm-script' (file does not exist yet — RED)
+ - machine-wide path: output contains 'C:\ProgramData\rclone'
+ - user-profile path: output contains '%APPDATA%\rclone'
+ - includeInstall=true: output contains 'https://downloads.rclone.org/rclone-current-windows-amd64.zip'
+ - includeInstall=false: output does NOT contain 'downloads.rclone.org'
+ - output contains '$ErrorActionPreference = ''Stop''' (RMM scripts DO use Stop — unlike detection)
+ - output contains '[System.IO.File]::WriteAllText' (UTF-8 no-BOM)
+
+
+ Create all four test files. Use INITIAL_STATE + overrides to build typed WizardState fixtures. Import from '../store/types' for WizardState and INITIAL_STATE. Use Vitest globals (describe, it, expect — no import needed, globals: true in vitest.config.ts).
+
+ Representative fixture pattern (replicate across all four files):
+ ```typescript
+ import { buildRcloneConf } from './rclone-conf';
+ import { INITIAL_STATE } from '../store/types';
+ import type { WizardState } from '../store/types';
+
+ const azureState: WizardState = {
+ ...INITIAL_STATE,
+ remote: {
+ name: 'my-azure',
+ backendType: 'azureblob',
+ params: { account: 'mystorageaccount', key: 'BASE64KEY', sas_url: '' },
+ },
+ };
+ ```
+
+ Tests will fail at import (module not found) until Plans 02-04 create implementations. This is intentional RED state. Vitest with passWithNoTests:true will still pass overall suite; individual test files will error. That is correct behavior for Wave 0 stubs.
+
+ Keep test descriptions concise and specific to the substring being asserted. No mocking needed — these are pure string functions.
+
+
+ npm test 2>&1 | head -50
+
+
+ All four test files exist. npm test runs without crashing the runner (passWithNoTests:true). Individual generator test files show import errors or failing tests — RED state is correct. TypeScript compilation of test files is the only requirement at this stage.
+
+
+
+
+ Task 2: Create index.ts barrel
+ src/generators/index.ts
+
+ Create `src/generators/index.ts` that re-exports all four generator functions. The module files do not exist yet — use type-only re-exports or forward-compatible syntax that will resolve once Plans 02-04 create the implementations.
+
+ Since the generator files don't exist yet, write the barrel with the final export shape. Vitest will see the import error from test files; the barrel itself need not be tested.
+
+ ```typescript
+ // src/generators/index.ts
+ // Re-exports all generator functions.
+ // Phase 4 imports generators from this barrel — do not remove exports.
+ export { buildRcloneConf } from './rclone-conf';
+ export { buildIntuneInstall } from './intune-install';
+ export { buildIntuneDetection } from './intune-detection';
+ export { buildRmmScript } from './rmm-script';
+ ```
+
+ No implementation logic in this file. The barrel is purely structural.
+
+
+ test -f src/generators/index.ts && echo "exists"
+
+
+ src/generators/index.ts exists with all four named re-exports. File contains no implementation logic.
+
+
+
+
+
+
+- All four test files exist in src/generators/
+- src/generators/index.ts exists with four re-export lines
+- npm test runs without runner crash
+- Test files correctly import from sibling modules (not yet implemented — RED is expected)
+
+
+
+Wave 0 is complete: test stubs define every assertion that Plans 02, 03, 04 must satisfy. The barrel defines the public API contract. No implementation exists yet — all generator imports are RED.
+
+
+
diff --git a/.planning/phases/02-generators/02-02-PLAN.md b/.planning/phases/02-generators/02-02-PLAN.md
new file mode 100644
index 0000000..989d6c7
--- /dev/null
+++ b/.planning/phases/02-generators/02-02-PLAN.md
@@ -0,0 +1,166 @@
+---
+phase: 02-generators
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 02-01
+files_modified:
+ - src/generators/rclone-conf.ts
+autonomous: true
+requirements:
+ - CONF-01
+
+must_haves:
+ truths:
+ - "buildRcloneConf(azureState) produces a valid INI block with type = azureblob and non-empty fields"
+ - "buildRcloneConf(s3State) produces type = s3 with provider = AWS"
+ - "buildRcloneConf(s3CompatState) produces type = s3 (not s3-compatible) with provider = Other"
+ - "Empty optional fields are omitted from the output (no 'sas_url =' line when sas_url is empty)"
+ - "All rclone-conf.test.ts assertions pass GREEN"
+ artifacts:
+ - path: "src/generators/rclone-conf.ts"
+ provides: "buildRcloneConf pure function"
+ exports: ["buildRcloneConf"]
+ min_lines: 25
+ key_links:
+ - from: "src/generators/rclone-conf.ts"
+ to: "src/store/types.ts"
+ via: "import type { WizardState } from '../store/types'"
+ pattern: "WizardState"
+ - from: "RCLONE_TYPE_MAP"
+ to: "rclone backend type strings"
+ via: "Record lookup"
+ pattern: "'s3-compatible': 's3'"
+---
+
+
+Implement buildRcloneConf() — the pure TypeScript function that converts WizardState into a valid rclone.conf INI string.
+
+Purpose: CONF-01 — the foundational output artifact. Every downstream phase depends on this being correct. Wrong type values or included empty fields silently break rclone remotes.
+Output: src/generators/rclone-conf.ts with all rclone-conf.test.ts assertions green.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/02-generators/02-01-SUMMARY.md
+@src/store/types.ts
+@src/schemas/registry.ts
+@src/generators/rclone-conf.test.ts
+
+
+
+
+
+From src/store/types.ts:
+```typescript
+export interface WizardState {
+ currentStep: number;
+ remote: {
+ name: string;
+ backendType: BackendType | null; // null must throw
+ params: Record; // iterate, skip empty values
+ };
+ deployment: {
+ includeInstall: boolean;
+ configPath: 'machine-wide' | 'user-profile';
+ scriptTargets: ('intune' | 'rmm')[];
+ };
+}
+```
+
+From src/schemas/registry.ts:
+```typescript
+export type BackendType = 'azureblob' | 's3' | 's3-compatible';
+```
+
+Critical type mapping (from research — verified against rclone.org):
+```typescript
+// BackendType → rclone config 'type' value
+// 'azureblob' → 'azureblob'
+// 's3' → 's3'
+// 's3-compatible' → 's3' (NOT 's3-compatible' — rclone does not recognize that string)
+// S3-compatible backends use type = s3, differentiated by provider = Other
+```
+
+
+
+
+
+ Task 1: Implement buildRcloneConf
+ src/generators/rclone-conf.ts
+
+ - azureblob: '[my-azure]\ntype = azureblob\naccount = mystorageaccount\nkey = BASE64KEY\n' (sas_url omitted — empty)
+ - s3: contains 'type = s3', 'provider = AWS', 'access_key_id = AKID', 'region = us-east-1'
+ - s3-compatible: contains 'type = s3', NOT 'type = s3-compatible', 'provider = Other', 'endpoint = https://s3.wasabisys.com', NOT 'region' line (empty optional field)
+ - throws Error if backendType is null
+ - throws Error if name is empty string
+ - output ends with '\n' (trailing newline — rclone convention)
+ - no empty value lines: `if (value !== '') { lines.push(...) }`
+
+
+ Create src/generators/rclone-conf.ts implementing buildRcloneConf(state: WizardState): string.
+
+ Follow this exact structure:
+ ```typescript
+ import type { WizardState } from '../store/types';
+
+ // Maps our BackendType to rclone's internal type string.
+ // IMPORTANT: s3-compatible uses 'type = s3' — rclone does not recognize 's3-compatible' as a type.
+ // S3-compatible backends are differentiated by provider = Other in params.
+ const RCLONE_TYPE_MAP: Record = {
+ azureblob: 'azureblob',
+ s3: 's3',
+ 's3-compatible': 's3',
+ };
+
+ export function buildRcloneConf(state: WizardState): string {
+ const { name, backendType, params } = state.remote;
+ if (!backendType) throw new Error('buildRcloneConf: backendType is required');
+ if (!name) throw new Error('buildRcloneConf: remote name is required');
+
+ const rcloneType = RCLONE_TYPE_MAP[backendType];
+ const lines: string[] = [`[${name}]`, `type = ${rcloneType}`];
+
+ for (const [key, value] of Object.entries(params)) {
+ if (value !== '') {
+ lines.push(`${key} = ${value}`);
+ }
+ }
+
+ return lines.join('\n') + '\n';
+ }
+ ```
+
+ Do NOT import BACKEND_REGISTRY — the function iterates over state.remote.params directly (already contains the right keys from the registry-driven form). Do NOT add any console.log or Write-Host equivalent — credentials must not be logged.
+
+ Anti-pattern to avoid: Do not write `type = s3-compatible`. The RCLONE_TYPE_MAP lookup handles the translation.
+
+
+ npm test -- src/generators/rclone-conf.test.ts
+
+
+ All rclone-conf.test.ts assertions pass GREEN. buildRcloneConf produces correct INI output for all three backends. Empty fields are omitted. Type mapping is correct (s3-compatible → type = s3).
+
+
+
+
+
+
+npm test -- src/generators/rclone-conf.test.ts is fully green.
+npm test (full suite) still passes.
+src/generators/rclone-conf.ts is importable from index.ts without TypeScript errors.
+
+
+
+CONF-01 is satisfied: buildRcloneConf produces valid INI-format rclone.conf strings with correct key/value pairs for all three supported backends (azureblob, s3, s3-compatible).
+
+
+
diff --git a/.planning/phases/02-generators/02-03-PLAN.md b/.planning/phases/02-generators/02-03-PLAN.md
new file mode 100644
index 0000000..3e39c94
--- /dev/null
+++ b/.planning/phases/02-generators/02-03-PLAN.md
@@ -0,0 +1,297 @@
+---
+phase: 02-generators
+plan: 03
+type: execute
+wave: 2
+depends_on:
+ - 02-01
+files_modified:
+ - src/generators/intune-install.ts
+ - src/generators/intune-detection.ts
+ - src/generators/ps-helpers.ts
+autonomous: true
+requirements:
+ - DEPL-01
+ - DEPL-02
+ - DEPL-04
+ - DEPL-05
+
+must_haves:
+ truths:
+ - "buildIntuneInstall() produces a PowerShell script containing the UTF-8 no-BOM write idiom"
+ - "buildIntuneInstall() with includeInstall=true adds rclone download block; with false, omits it"
+ - "buildIntuneInstall() uses C:\\ProgramData\\rclone when configPath=machine-wide and %APPDATA%\\rclone when user-profile"
+ - "buildIntuneDetection() does NOT contain $ErrorActionPreference (STDERR contamination guard)"
+ - "buildIntuneDetection() exits 0 with Write-Output signal; exits 1 otherwise"
+ - "buildIntuneDetection() uses the same config path as the install script for the same configPath value"
+ - "All intune-install.test.ts and intune-detection.test.ts assertions pass GREEN"
+ artifacts:
+ - path: "src/generators/ps-helpers.ts"
+ provides: "Shared PowerShell snippet builders (path map, install block)"
+ exports:
+ - CONFIG_DIR
+ - buildRcloneInstallBlock
+ - path: "src/generators/intune-install.ts"
+ provides: "buildIntuneInstall pure function"
+ exports: ["buildIntuneInstall"]
+ min_lines: 40
+ - path: "src/generators/intune-detection.ts"
+ provides: "buildIntuneDetection pure function"
+ exports: ["buildIntuneDetection"]
+ min_lines: 20
+ key_links:
+ - from: "src/generators/intune-install.ts"
+ to: "src/generators/ps-helpers.ts"
+ via: "import { CONFIG_DIR, buildRcloneInstallBlock }"
+ pattern: "from './ps-helpers'"
+ - from: "src/generators/intune-detection.ts"
+ to: "src/generators/ps-helpers.ts"
+ via: "import { CONFIG_DIR }"
+ pattern: "from './ps-helpers'"
+ - from: "ps-helpers CONFIG_DIR"
+ to: "WizardState.deployment.configPath"
+ via: "Record<'machine-wide'|'user-profile', string> lookup"
+ pattern: "machine-wide.*ProgramData|user-profile.*APPDATA"
+---
+
+
+Implement buildIntuneInstall() and buildIntuneDetection() with a shared ps-helpers.ts for the config path map and conditional rclone install block.
+
+Purpose: DEPL-01 and DEPL-02. The shared helper ensures the install and detection scripts always reference the same paths — a mismatch would cause detection to fail even when the app is correctly installed.
+Output: Three files — ps-helpers.ts (shared), intune-install.ts, intune-detection.ts — with all Intune test assertions green.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/02-generators/02-01-SUMMARY.md
+@src/store/types.ts
+@src/generators/intune-install.test.ts
+@src/generators/intune-detection.test.ts
+
+
+
+
+
+From src/store/types.ts:
+```typescript
+export interface WizardState {
+ remote: {
+ name: string;
+ backendType: BackendType | null;
+ params: Record;
+ };
+ deployment: {
+ includeInstall: boolean; // controls rclone download block
+ configPath: 'machine-wide' | 'user-profile'; // controls Windows path used
+ scriptTargets: ('intune' | 'rmm')[];
+ };
+}
+```
+
+Path mapping (from research):
+```
+'machine-wide' → C:\ProgramData\rclone
+'user-profile' → %APPDATA%\rclone
+ (note: under SYSTEM, %APPDATA% = C:\Windows\system32\config\systemprofile\AppData\Roaming)
+```
+
+Intune detection contract (from Microsoft Learn, HIGH confidence):
+- Exit 0 + Write-Output → app detected
+- Any STDERR output → app NOT detected (even with exit 0)
+- Therefore: NO $ErrorActionPreference = 'Stop' in detection scripts
+
+
+
+
+
+ Task 1: Create ps-helpers.ts shared module
+ src/generators/ps-helpers.ts
+
+ - CONFIG_DIR['machine-wide'] === 'C:\\ProgramData\\rclone'
+ - CONFIG_DIR['user-profile'] === '%APPDATA%\\rclone'
+ - buildRcloneInstallBlock('C:\\ProgramData\\rclone') returns string containing 'downloads.rclone.org/rclone-current-windows-amd64.zip'
+ - buildRcloneInstallBlock return value contains '-UseBasicParsing' (SYSTEM context safety)
+ - buildRcloneInstallBlock return value contains 'rclone.exe'
+
+
+ Create src/generators/ps-helpers.ts with two exports:
+
+ ```typescript
+ import type { WizardState } from '../store/types';
+
+ // Canonical Windows paths for each configPath option.
+ // Both generators (install + detection) import this map to guarantee path consistency.
+ // Note: %APPDATA% under SYSTEM resolves to C:\Windows\system32\config\systemprofile\AppData\Roaming\
+ // The generated script will contain an inline comment warning about this.
+ export const CONFIG_DIR: Record = {
+ 'machine-wide': 'C:\\ProgramData\\rclone',
+ 'user-profile': '%APPDATA%\\rclone',
+ };
+
+ // Conditional rclone binary download block.
+ // Returns the PowerShell snippet if includeInstall = true, empty string if false.
+ export function buildRcloneInstallBlock(installPath: string): string {
+ return `
+ # Download rclone binary (idempotent — skips if already present)
+ $rclonePath = "${installPath}\\rclone.exe"
+ if (-not (Test-Path $rclonePath)) {
+ $zipPath = "$env:TEMP\\rclone.zip"
+ Invoke-WebRequest -Uri 'https://downloads.rclone.org/rclone-current-windows-amd64.zip' -OutFile $zipPath -UseBasicParsing
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath)
+ $entry = $zip.Entries | Where-Object { $_.Name -eq 'rclone.exe' }
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $rclonePath, $true)
+ $zip.Dispose()
+ Remove-Item $zipPath -Force
+ }`;
+ }
+ ```
+
+ Keep this module free of generator logic. It is purely a shared constants + helper module.
+
+
+ npm test -- src/generators/intune-install.test.ts 2>&1 | head -30
+
+
+ ps-helpers.ts exists with CONFIG_DIR exported and buildRcloneInstallBlock exported. TypeScript compiles without errors.
+
+
+
+
+ Task 2: Implement buildIntuneInstall and buildIntuneDetection
+ src/generators/intune-install.ts, src/generators/intune-detection.ts
+
+ buildIntuneInstall:
+ - starts with '$ErrorActionPreference = ''Stop'''
+ - uses CONFIG_DIR from ps-helpers for the config directory path
+ - creates directory idempotently: 'if (-not (Test-Path $configDir))'
+ - writes config using '[System.IO.File]::WriteAllText' with '[System.Text.UTF8Encoding]::new($false)'
+ - the config content is embedded as a PowerShell here-string (@'...'@) — NO $variable interpolation inside the here-string block
+ - includeInstall=true: script body contains 'downloads.rclone.org'
+ - includeInstall=false: script body does NOT contain 'downloads.rclone.org'
+ - ends with 'exit 0'
+
+ buildIntuneDetection:
+ - does NOT contain '$ErrorActionPreference' anywhere in output
+ - does NOT contain 'Write-Error' or 'Write-Host'
+ - uses CONFIG_DIR from ps-helpers (same path as install script for the same configPath value)
+ - checks: '(Test-Path $rclonePath) -and (Test-Path $configPath)'
+ - on success: 'Write-Output "rclone and config detected"' then 'exit 0'
+ - on failure: 'exit 1'
+
+
+ Create intune-install.ts:
+ ```typescript
+ import type { WizardState } from '../store/types';
+ import { CONFIG_DIR, buildRcloneInstallBlock } from './ps-helpers';
+
+ export function buildIntuneInstall(state: WizardState): string {
+ const configDir = CONFIG_DIR[state.deployment.configPath];
+ const configPath = `${configDir}\\rclone.conf`;
+ const confContent = buildRcloneConfContent(state);
+ const installBlock = state.deployment.includeInstall
+ ? buildRcloneInstallBlock(configDir)
+ : '';
+
+ return [
+ `$ErrorActionPreference = 'Stop'`,
+ ``,
+ `$configDir = '${configDir}'`,
+ `$configPath = "$configDir\\rclone.conf"`,
+ ``,
+ `# Idempotent: create directory if it doesn't exist`,
+ `if (-not (Test-Path $configDir)) {`,
+ ` New-Item -ItemType Directory -Path $configDir -Force | Out-Null`,
+ `}`,
+ ``,
+ `# Write config using UTF-8 no-BOM encoding`,
+ `# Note: Set-Content -Encoding UTF8 in PS 5.1 writes UTF-8 WITH BOM — do not use`,
+ `$confContent = @'`,
+ confContent.trimEnd(),
+ `'@`,
+ `[System.IO.File]::WriteAllText($configPath, $confContent, [System.Text.UTF8Encoding]::new($false))`,
+ installBlock,
+ ``,
+ `exit 0`,
+ ].join('\n') + '\n';
+ }
+
+ // Build the rclone.conf content string to embed in the here-string.
+ // Import buildRcloneConf to avoid duplicating the INI logic.
+ function buildRcloneConfContent(state: WizardState): string {
+ // Inline the INI format here rather than importing buildRcloneConf,
+ // to keep the dependency graph simple and avoid circular imports.
+ const RCLONE_TYPE_MAP: Record = {
+ azureblob: 'azureblob',
+ s3: 's3',
+ 's3-compatible': 's3',
+ };
+ const { name, backendType, params } = state.remote;
+ const rcloneType = backendType ? RCLONE_TYPE_MAP[backendType] : 'unknown';
+ const lines: string[] = [`[${name}]`, `type = ${rcloneType}`];
+ for (const [key, value] of Object.entries(params)) {
+ if (value !== '') lines.push(`${key} = ${value}`);
+ }
+ return lines.join('\n') + '\n';
+ }
+ ```
+
+ IMPORTANT: The here-string block (@'...'@) embeds literal credential values directly as a JavaScript string. There is NO PowerShell variable interpolation inside the here-string content. The single-quoted @'...'@ PS here-string syntax also prevents PS-level interpolation. This satisfies the credential non-echo requirement.
+
+ Create intune-detection.ts:
+ ```typescript
+ import type { WizardState } from '../store/types';
+ import { CONFIG_DIR } from './ps-helpers';
+
+ export function buildIntuneDetection(state: WizardState): string {
+ const configDir = CONFIG_DIR[state.deployment.configPath];
+ // CRITICAL: No $ErrorActionPreference = 'Stop' here.
+ // Unhandled exceptions write to STDERR.
+ // Intune detection contract: any STDERR output = app not detected, regardless of exit code.
+ return [
+ `$configDir = '${configDir}'`,
+ `$rclonePath = "$configDir\\rclone.exe"`,
+ `$configPath = "$configDir\\rclone.conf"`,
+ ``,
+ `if ((Test-Path $rclonePath) -and (Test-Path $configPath)) {`,
+ ` Write-Output "rclone and config detected"`,
+ ` exit 0`,
+ `} else {`,
+ ` exit 1`,
+ `}`,
+ ].join('\n') + '\n';
+ }
+ ```
+
+
+ npm test -- src/generators/intune-install.test.ts src/generators/intune-detection.test.ts
+
+
+ All intune-install.test.ts and intune-detection.test.ts assertions pass GREEN. The install script contains the UTF-8 no-BOM write idiom, correct path for both configPath values, and conditional rclone download block. The detection script has no $ErrorActionPreference and uses Write-Output + exit 0/1.
+
+
+
+
+
+
+npm test -- src/generators/intune-install.test.ts src/generators/intune-detection.test.ts — all GREEN.
+npm test (full suite) — passes.
+Verify: grep for '$ErrorActionPreference' in intune-detection.ts returns nothing.
+Verify: grep for 'downloads.rclone.org' absent from buildIntuneInstall with includeInstall=false state.
+
+
+
+DEPL-01: Intune install script writes config at correct path with UTF-8 no-BOM encoding idiom.
+DEPL-02: Detection script exits 0+STDOUT when files present, no STDERR contamination risk.
+DEPL-04: includeInstall toggle correctly adds/removes rclone download block.
+DEPL-05: Both scripts resolve to C:\ProgramData\rclone or %APPDATA%\rclone based on configPath.
+
+
+
diff --git a/.planning/phases/02-generators/02-04-PLAN.md b/.planning/phases/02-generators/02-04-PLAN.md
new file mode 100644
index 0000000..8d14c6a
--- /dev/null
+++ b/.planning/phases/02-generators/02-04-PLAN.md
@@ -0,0 +1,195 @@
+---
+phase: 02-generators
+plan: 04
+type: execute
+wave: 2
+depends_on:
+ - 02-01
+files_modified:
+ - src/generators/rmm-script.ts
+autonomous: true
+requirements:
+ - DEPL-03
+ - DEPL-04
+ - DEPL-05
+
+must_haves:
+ truths:
+ - "buildRmmScript() produces a self-contained PowerShell script with $ErrorActionPreference = 'Stop'"
+ - "buildRmmScript() creates the config directory idempotently and writes rclone.conf with UTF-8 no-BOM"
+ - "buildRmmScript() with includeInstall=true adds rclone download block; with false, omits it"
+ - "buildRmmScript() uses C:\\ProgramData\\rclone for machine-wide and %APPDATA%\\rclone for user-profile"
+ - "All rmm-script.test.ts assertions pass GREEN"
+ artifacts:
+ - path: "src/generators/rmm-script.ts"
+ provides: "buildRmmScript pure function"
+ exports: ["buildRmmScript"]
+ min_lines: 35
+ key_links:
+ - from: "src/generators/rmm-script.ts"
+ to: "src/generators/ps-helpers.ts"
+ via: "import { CONFIG_DIR, buildRcloneInstallBlock }"
+ pattern: "from './ps-helpers'"
+ - from: "CONFIG_DIR lookup"
+ to: "WizardState.deployment.configPath"
+ via: "Record<'machine-wide'|'user-profile', string>"
+ pattern: "machine-wide|user-profile"
+---
+
+
+Implement buildRmmScript() — the generic RMM PowerShell installer using shared ps-helpers.ts from Plan 03.
+
+Purpose: DEPL-03. RMM script is structurally similar to the Intune install script but has no separate detection artifact, no Intune packaging constraints, and explicitly uses $ErrorActionPreference = 'Stop' (RMM platforms surface exit codes, not STDERR).
+Output: src/generators/rmm-script.ts with all rmm-script.test.ts assertions green.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/ROADMAP.md
+@.planning/phases/02-generators/02-01-SUMMARY.md
+@.planning/phases/02-generators/02-03-SUMMARY.md
+@src/store/types.ts
+@src/generators/ps-helpers.ts
+@src/generators/rmm-script.test.ts
+
+
+
+
+
+From src/generators/ps-helpers.ts (created in Plan 03):
+```typescript
+export const CONFIG_DIR: Record<'machine-wide' | 'user-profile', string> = {
+ 'machine-wide': 'C:\\ProgramData\\rclone',
+ 'user-profile': '%APPDATA%\\rclone',
+};
+
+export function buildRcloneInstallBlock(installPath: string): string;
+// Returns PowerShell snippet with Invoke-WebRequest -UseBasicParsing and zip extraction.
+// Returns empty string if called conditionally — caller decides whether to include.
+```
+
+From src/store/types.ts:
+```typescript
+export interface WizardState {
+ remote: { name: string; backendType: BackendType | null; params: Record };
+ deployment: {
+ includeInstall: boolean;
+ configPath: 'machine-wide' | 'user-profile';
+ scriptTargets: ('intune' | 'rmm')[];
+ };
+}
+```
+
+Key difference from Intune install:
+- RMM scripts DO use $ErrorActionPreference = 'Stop' (RMM platforms capture exit codes; no STDERR-breaks-detection contract)
+- No exit 0 / exit 1 convention required (but including for clarity is fine)
+- No separate detection script — the RMM platform handles existence checks natively
+
+
+
+
+
+ Task 1: Implement buildRmmScript
+ src/generators/rmm-script.ts
+
+ - output contains '$ErrorActionPreference = ''Stop''' (RMM scripts use Stop — unlike detection)
+ - machine-wide path: output contains 'C:\ProgramData\rclone'
+ - user-profile path: output contains '%APPDATA%\rclone'
+ - includeInstall=true: output contains 'downloads.rclone.org/rclone-current-windows-amd64.zip'
+ - includeInstall=false: output does NOT contain 'downloads.rclone.org'
+ - output contains '[System.IO.File]::WriteAllText' (UTF-8 no-BOM write)
+ - output contains '@'' / ''@' or equivalent here-string (credentials not interpolated at PS runtime)
+ - config directory creation is idempotent: 'if (-not (Test-Path $configDir))'
+
+
+ Create src/generators/rmm-script.ts importing from ps-helpers.ts. Structure mirrors buildIntuneInstall but with explicit comments that this is for generic RMM tools (NinjaRMM, Datto, ConnectWise).
+
+ ```typescript
+ import type { WizardState } from '../store/types';
+ import { CONFIG_DIR, buildRcloneInstallBlock } from './ps-helpers';
+
+ export function buildRmmScript(state: WizardState): string {
+ const configDir = CONFIG_DIR[state.deployment.configPath];
+ const confContent = buildRcloneConfContent(state);
+ const installBlock = state.deployment.includeInstall
+ ? buildRcloneInstallBlock(configDir)
+ : '';
+
+ return [
+ `# RMM Deployment Script — compatible with NinjaRMM, Datto, ConnectWise`,
+ `# Runs as SYSTEM. Idempotent (safe to re-run).`,
+ `$ErrorActionPreference = 'Stop'`,
+ ``,
+ `$configDir = '${configDir}'`,
+ `$configPath = "$configDir\\rclone.conf"`,
+ ``,
+ `# Idempotent: create directory if it doesn't exist`,
+ `if (-not (Test-Path $configDir)) {`,
+ ` New-Item -ItemType Directory -Path $configDir -Force | Out-Null`,
+ `}`,
+ ``,
+ `# Write config using UTF-8 no-BOM encoding`,
+ `# Note: Set-Content -Encoding UTF8 in PS 5.1 writes UTF-8 WITH BOM — do not use`,
+ `$confContent = @'`,
+ confContent.trimEnd(),
+ `'@`,
+ `[System.IO.File]::WriteAllText($configPath, $confContent, [System.Text.UTF8Encoding]::new($false))`,
+ installBlock,
+ ``,
+ `exit 0`,
+ ].join('\n') + '\n';
+ }
+
+ function buildRcloneConfContent(state: WizardState): string {
+ const RCLONE_TYPE_MAP: Record = {
+ azureblob: 'azureblob',
+ s3: 's3',
+ 's3-compatible': 's3',
+ };
+ const { name, backendType, params } = state.remote;
+ const rcloneType = backendType ? RCLONE_TYPE_MAP[backendType] : 'unknown';
+ const lines: string[] = [`[${name}]`, `type = ${rcloneType}`];
+ for (const [key, value] of Object.entries(params)) {
+ if (value !== '') lines.push(`${key} = ${value}`);
+ }
+ return lines.join('\n') + '\n';
+ }
+ ```
+
+ Note: The buildRcloneConfContent helper is duplicated from intune-install.ts intentionally — both files are self-contained. If this feels wrong, the alternative is to import buildRcloneConf from './rclone-conf', which is also fine (no circular dependency). Use whichever approach keeps the code clear. Either satisfies the tests.
+
+ Anti-patterns to avoid:
+ - Do NOT use 'Write-Error' or 'Write-Host' for logging (leaks to STDERR / logs)
+ - Do NOT interpolate credential values in the here-string using PowerShell variables
+ - Do NOT use 'Set-Content -Encoding UTF8' (writes BOM in PS 5.1)
+
+
+ npm test -- src/generators/rmm-script.test.ts
+
+
+ All rmm-script.test.ts assertions pass GREEN. The RMM script is self-contained, idempotent, uses UTF-8 no-BOM write, and conditionally includes the rclone binary download block.
+
+
+
+
+
+
+npm test -- src/generators/rmm-script.test.ts — all GREEN.
+npm test (full suite) — all GREEN (Plans 01, 02, 03, 04 all passing).
+src/generators/index.ts re-exports buildRmmScript without TypeScript errors.
+
+
+
+DEPL-03: RMM script is self-contained, idempotent, runs correctly in a generic SYSTEM context.
+DEPL-04: includeInstall toggle adds/removes rclone download block in RMM script.
+DEPL-05: configPath correctly maps to C:\ProgramData\rclone or %APPDATA%\rclone in RMM script.
+Phase 2 is complete: all six requirements (CONF-01, DEPL-01 through DEPL-05) have green unit tests.
+
+
+