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