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 |
|
|
true |
|
|
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.tsFrom 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
```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.
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'
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';
}
```
<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`