--- 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. After completion, create `.planning/phases/02-generators/02-04-SUMMARY.md`