Files
Ready2Blob/.planning/phases/02-generators/02-03-PLAN.md
T

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-generators 03 execute 2
02-01
src/generators/intune-install.ts
src/generators/intune-detection.ts
src/generators/ps-helpers.ts
true
DEPL-01
DEPL-02
DEPL-04
DEPL-05
truths artifacts key_links
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
path provides exports
src/generators/ps-helpers.ts Shared PowerShell snippet builders (path map, install block)
CONFIG_DIR
buildRcloneInstallBlock
path provides exports min_lines
src/generators/intune-install.ts buildIntuneInstall pure function
buildIntuneInstall
40
path provides exports min_lines
src/generators/intune-detection.ts buildIntuneDetection pure function
buildIntuneDetection
20
from to via pattern
src/generators/intune-install.ts src/generators/ps-helpers.ts import { CONFIG_DIR, buildRcloneInstallBlock } from './ps-helpers'
from to via pattern
src/generators/intune-detection.ts src/generators/ps-helpers.ts import { CONFIG_DIR } from './ps-helpers'
from to via pattern
ps-helpers CONFIG_DIR WizardState.deployment.configPath Record<'machine-wide'|'user-profile', string> lookup 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.

<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

export interface WizardState {
  remote: {
    name: string;
    backendType: BackendType | null;
    params: Record<string, string>;
  };
  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<WizardState['deployment']['configPath'], string> = {
  '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<string, string> = {
    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.

<success_criteria> 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. </success_criteria>

After completion, create `.planning/phases/02-generators/02-03-SUMMARY.md`