Files
Ready2Blob/.planning/phases/02-generators/02-RESEARCH.md
T
kawaandClaude Sonnet 4.6 aff2923b14 docs(phase-02): research generators phase
Covers rclone.conf INI generation (type mapping, empty field omission),
PowerShell encoding (UTF-8 no-BOM via UTF8Encoding($false)), Intune
detection script contract (STDERR breaks detection), and RMM SYSTEM
context patterns. All key findings verified against live official sources.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 10:39:12 +01:00

32 KiB

Phase 2: Generators - Research

Researched: 2026-03-26 Domain: Pure TypeScript generator functions (rclone INI conf), PowerShell script templating (Intune install, Intune detection, RMM), PowerShell encoding (UTF-8 no-BOM in SYSTEM context) Confidence: HIGH


<phase_requirements>

Phase Requirements

ID Description Research Support
CONF-01 App generates a valid rclone.conf file (INI format, correct key/value pairs per backend type) rclone INI format verified from official docs; backend type values and field keys verified from rclone.org
DEPL-01 App generates a PowerShell install script for MS Intune (runs as SYSTEM, places config at machine-wide path, idempotent, correct exit codes) SYSTEM context encoding requirements verified from Microsoft Learn; idempotency patterns documented
DEPL-02 App generates a separate Intune detection script (checks rclone.exe presence at install path AND config file presence; exits 0 if both found) Intune detection script contract (exit 0 + STDOUT) verified from Microsoft Learn; STDERR-breaks-detection pitfall documented
DEPL-03 App generates a PowerShell script for RMM tools (generic SYSTEM-context, idempotent, works across NinjaRMM/Datto/ConnectWise) Self-contained idempotent PowerShell pattern documented; no RMM-specific SDK required
DEPL-04 User can toggle "Include rclone installation" to add rclone binary download step to generated scripts rclone official download URL pattern verified; conditional script section design documented
DEPL-05 User can choose config deployment path: machine-wide (C:\ProgramData\rclone\) vs user profile (%APPDATA%\rclone\) Both Windows path conventions documented; SYSTEM context caveat for %APPDATA% documented
</phase_requirements>

Summary

Phase 2 implements pure generator functions: given a completed WizardState, produce correct string content for rclone.conf and all PowerShell scripts. These are the most correctness-critical deliverables in the entire project — a wrong rclone config key silently breaks authentication, a wrong PowerShell encoding breaks Intune script upload, and a detection script that writes to STDERR fails to detect even when the app is installed.

All generators are pure TypeScript functions with no React dependencies. They take WizardState as input and return a string. This makes them trivially unit-testable and completely decoupled from UI. The rclone.conf generator reads remote.backendType, remote.name, and remote.params from state; the PowerShell generators additionally read deployment.includeInstall, deployment.configPath, and deployment.scriptTargets. The configPath option maps to literal Windows paths: machine-wideC:\ProgramData\rclone\, user-profile%APPDATA%\rclone\.

The single most complex research finding is PowerShell encoding: Intune recommends UTF-8 with BOM for detection scripts (so Windows PowerShell 5.1 correctly handles non-ASCII characters), but many community sources say no-BOM. For scripts that contain only ASCII characters (which all generated scripts do — all credential values are treated as opaque strings), this distinction is moot. The generator outputs plain JavaScript strings; the encoding is only relevant if Phase 4 downloads the script as a file. Phase 2 does not handle file saving — it only produces string content. The encoding concern is therefore a Phase 4 (download manager) concern, not a Phase 2 concern.

Primary recommendation: Implement all generators as pure TypeScript functions in src/generators/. Each function takes WizardState and returns string. Test with Vitest unit tests that assert specific substrings are present or absent in the output. No templating library is needed — template literals are sufficient and produce readable, auditable script content.


Standard Stack

Core

Library Version Purpose Why Standard
TypeScript template literals Built-in String generation for all outputs Pure functions, no dep, fully auditable — you can read the exact PS code that will be generated
WizardState (Phase 1) Already built Input to all generators Phase 1 types.ts already defines the full state shape; generators consume it directly
Vitest 2.x (already installed) Unit testing generator output Already configured in vitest.config.ts with node environment — ideal for pure string functions

No Additional Libraries Needed

Phase 2 adds zero new npm dependencies. All generation is string manipulation over the existing WizardState type. The BACKEND_REGISTRY from Phase 1 (src/schemas/registry.ts) provides the field-to-key mapping that the rclone.conf generator iterates over.

Alternatives Considered

Instead of Could Use Tradeoff
Template literals Handlebars / Mustache Template engines add a dependency and obscure the output. With template literals, the generated script is plainly readable in source code — critical for a security-sensitive tool.
Template literals Tagged template literals Unnecessary complexity for this use case; plain template literals are cleaner
Inline type assertions Zod parse of WizardState before generation Overkill — WizardState already typed; generators can assume valid input (wizard validates before reaching review step)

Architecture Patterns

src/
├── generators/
│   ├── rclone-conf.ts        # buildRcloneConf(state) → string
│   ├── intune-install.ts     # buildIntuneInstall(state) → string
│   ├── intune-detection.ts   # buildIntuneDetection(state) → string
│   ├── rmm-script.ts         # buildRmmScript(state) → string
│   ├── index.ts              # Re-exports all four generators
│   ├── rclone-conf.test.ts   # Unit tests for CONF-01
│   ├── intune-install.test.ts
│   ├── intune-detection.test.ts
│   └── rmm-script.test.ts
├── schemas/                  # Phase 1 — unchanged
└── store/                    # Phase 1 — unchanged

Pattern 1: rclone.conf INI Generator

What: Produces a valid INI-format rclone.conf string. The INI format is: [remoteName] section header, then key = value pairs, one per line, blank line between sections (only one remote in v1). The mandatory type field maps BackendType to rclone's internal backend name.

BackendType → rclone type mapping (HIGH confidence — verified from rclone.org):

  • 'azureblob'type = azureblob
  • 's3'type = s3
  • 's3-compatible'type = s3 (NOT s3-compatible; rclone uses type = s3 with provider = Other for all S3-compatible storage)

When to use: Always first — the rclone.conf content is the foundation; scripts reference the config path.

Example:

// src/generators/rclone-conf.ts
import type { WizardState } from '../store/types';

const BACKEND_TYPE_MAP: Record<string, string> = {
  azureblob: 'azureblob',
  s3: 's3',
  's3-compatible': 's3',
};

export function buildRcloneConf(state: WizardState): string {
  const { name, backendType, params } = state.remote;
  if (!backendType || !name) {
    throw new Error('buildRcloneConf: remote name and backendType are required');
  }

  const rcloneType = BACKEND_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';
}

Correct rclone.conf output for Azure Blob (account + SAS URL):

[my-azure]
type = azureblob
account = mystorageaccount
sas_url = https://mystorageaccount.blob.core.windows.net/?sv=...

Correct rclone.conf output for S3:

[my-s3]
type = s3
provider = AWS
access_key_id = AKIAIOSFODNN7EXAMPLE
secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region = us-east-1

Correct rclone.conf output for S3-compatible (Wasabi example):

[my-wasabi]
type = s3
provider = Other
access_key_id = mykey
secret_access_key = mysecret
endpoint = https://s3.wasabisys.com

Pattern 2: Intune Install Script Generator

What: A PowerShell script that runs as SYSTEM, creates the config directory, writes the rclone.conf content, and optionally downloads rclone.exe. Must be idempotent (safe to run twice). Must exit with code 0 on success, non-zero on error.

Key requirements (HIGH confidence — verified from Microsoft Learn):

  • Runs as SYSTEM — no user context, no $env:USERPROFILE, no interactive prompts
  • Machine-wide path: C:\ProgramData\rclone\ — accessible by SYSTEM account
  • User profile path: %APPDATA%\rclone\caution: %APPDATA% resolves to C:\Windows\system32\config\systemprofile\AppData\Roaming when running as SYSTEM, NOT the logged-in user's APPDATA. This is documented behavior and should be surfaced as a UI warning in Phase 3.
  • [System.IO.File]::WriteAllText() with explicit System.Text.UTF8Encoding($false) for UTF-8 no-BOM file writing (required because PowerShell 5.1's Set-Content -Encoding UTF8 writes UTF-8 with BOM)
  • exit 0 / exit 1 at end of script — Intune reads the process exit code

rclone download URL (HIGH confidence — verified from rclone.org/install/):

https://downloads.rclone.org/rclone-current-windows-amd64.zip

This always points to the latest stable release. It is the official URL from rclone.org.

Example:

# Generated by buildIntuneInstall()
$ErrorActionPreference = 'Stop'

$configDir = 'C:\ProgramData\rclone'
$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
$confContent = @'
[my-azure]
type = azureblob
account = mystorageaccount
key = BASE64KEY
'@
[System.IO.File]::WriteAllText($configPath, $confContent, [System.Text.UTF8Encoding]::new($false))

# Optional: download rclone.exe (only if includeInstall = true)
$rclonePath = "$configDir\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
}

exit 0

Pattern 3: Intune Detection Script Generator

What: A PowerShell script that checks whether rclone.exe AND rclone.conf both exist at the expected paths. Exits 0 and writes to STDOUT when both are found. Exits non-zero when either is missing.

Intune detection contract (HIGH confidence — verified from Microsoft Learn docs updated 2026-02-06):

  • Exit code 0 + STDOUT output → app is detected (installed)
  • Exit code non-zero → app is not detected
  • Any output to STDERR → detection result is "not installed", even if exit code is 0 and STDOUT has data

Critical: The detection script must NEVER write to STDERR. Use try/catch and handle errors silently or exit non-zero rather than letting PowerShell write an exception to STDERR.

Example:

# Generated by buildIntuneDetection()
$configDir = 'C:\ProgramData\rclone'
$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
}

Note: No $ErrorActionPreference = 'Stop' here — exceptions would go to STDERR and break detection.

Pattern 4: RMM Script Generator

What: A self-contained PowerShell script for generic RMM tools (NinjaRMM, Datto, ConnectWise). Same logic as the Intune install script but without Intune-specific packaging concerns. Must be idempotent.

RMM context considerations (MEDIUM confidence — community knowledge, no single authoritative source):

  • All major RMM platforms run scripts as SYSTEM
  • Scripts must be self-contained — no external file dependencies, no IntuneWinAppUtil packaging
  • Path behavior under SYSTEM is the same as Intune (same %APPDATA% caveat applies)
  • No specific exit code contract (unlike Intune) — use exit 0 / exit 1 as standard practice
  • Include $ErrorActionPreference = 'Stop' so failures are surfaced to the RMM platform

Structural difference from Intune install: The RMM script has no separate "detection" artifact — detection is handled by the RMM platform's own file/registry check capability. The generated script only installs.

Pattern 5: Conditional rclone Install Block

What: When deployment.includeInstall = true, all three script generators (Intune install, RMM) include a download + extract section. When false, that section is omitted.

Implementation: Each generator function receives the full WizardState. A helper function buildRcloneInstallBlock(installPath: string): string returns the download/extract PowerShell block or empty string, and is called conditionally.

function buildRcloneInstallBlock(installPath: string): string {
  return `
# Download rclone binary
$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
}`;
}

Pattern 6: Config Path Resolution

What: deployment.configPath maps to absolute Windows paths in all generated scripts.

const CONFIG_PATHS: Record<WizardState['deployment']['configPath'], string> = {
  'machine-wide': 'C:\\ProgramData\\rclone',
  'user-profile': '%APPDATA%\\rclone',
};

The Intune detection script must use the same path the install script wrote to. Both generators must share this mapping (import from a shared constants file or directly from the config path mapping).

Anti-Patterns to Avoid

  • Echoing credential values in scripts: The install script must write the config file content using a PowerShell here-string (@'...'@), not by echoing individual field values with Write-Host or echo. A Write-Host "account = $account" would leak credentials to the Intune management extension log.
  • Using Set-Content -Encoding UTF8 in PowerShell 5.1: This writes UTF-8 with BOM. Use [System.IO.File]::WriteAllText() with an explicit UTF8Encoding($false) instance instead.
  • Using $ErrorActionPreference = 'Stop' in detection scripts: Unhandled exceptions write to STDERR, which causes Intune to report the app as "not installed" even when it is.
  • Hardcoding C:\ProgramData\rclone in the detection script independently from the install script: If the install path is user-profile, the detection script must check %APPDATA%\rclone, not the machine-wide path. Both scripts must derive their path from the same deployment.configPath value.
  • Using Invoke-WebRequest without -UseBasicParsing in SYSTEM context: IE's first-run wizard may not be completed for the SYSTEM account, causing Invoke-WebRequest to hang without this flag on Windows Server and some Windows 10 builds.
  • s3-compatible using type = s3-compatible in rclone.conf: The correct rclone type value is s3 with provider = Other. Using s3-compatible as the type string will cause rclone to fail to find the backend.

Don't Hand-Roll

Problem Don't Build Use Instead Why
INI file serialization Custom INI writer Template literal line-by-line rclone.conf INI is trivially simple — one section, flat key=value. No library needed and any library adds unnecessary complexity.
PowerShell templating Handlebars / Mustache Template literals Scripts are short and fully auditable. Template engines obscure the generated output.
UTF-8 no-BOM file writing Custom byte manipulation [System.IO.File]::WriteAllText(path, content, [System.Text.UTF8Encoding]::new($false)) This is the canonical PowerShell 5.1 idiom for no-BOM UTF-8 writing.
rclone binary distribution Bundle rclone.exe Download from downloads.rclone.org at deploy time Bundling adds binary to the web app; download URL is official and always points to latest stable.
Script zip/archive JSZip at generation time Return plain strings; Phase 4 handles zipping Separating string generation from file operations keeps generators pure and easily testable.

Key insight: All generation complexity in this domain comes from correctness constraints (right keys, right encoding, right exit codes), not from algorithmic complexity. Keep generators as simple readable template-literal functions. Every character of the generated PowerShell should be visible in the TypeScript source.


Common Pitfalls

Pitfall 1: s3-compatible type value in rclone.conf

What goes wrong: Generator writes type = s3-compatible for S3-compatible backends. rclone does not recognize this type string; the remote fails silently or throws "didn't find section in config file".

Why it happens: The BackendType in our TypeScript is 's3-compatible', which is the UI identifier. The rclone config type value is different: all S3 and S3-compatible remotes use type = s3, differentiated by the provider field (AWS for AWS S3, Other for S3-compatible).

How to avoid: The generator must have an explicit mapping: { 's3-compatible': 's3' }. The registry already sets provider = Other for S3-compatible backends — the generator just needs to map the type value correctly.

Warning signs: Test that asserts output contains type = s3 for an s3-compatible state, and provider = Other.

Pitfall 2: Detection script STDERR contamination

What goes wrong: The detection script includes $ErrorActionPreference = 'Stop'. An exception (e.g., access denied, path not found on an alternate drive) writes to STDERR. Intune sees STDERR output and reports the app as "not installed" even though it checked correctly and returned exit 0.

Why it happens: The Intune detection contract is: exit 0 + STDOUT → installed. Any STDERR → not installed, regardless of exit code.

How to avoid: Detection scripts must not set $ErrorActionPreference = 'Stop'. Use Test-Path which never throws — it returns $false if a path is inaccessible or doesn't exist.

Warning signs: Detection script passes local tests but Intune always shows "not installed" in the portal.

Pitfall 3: %APPDATA% resolves differently under SYSTEM

What goes wrong: User selects "user profile" config path. The install script uses %APPDATA%\rclone\ and the path resolves to C:\Windows\system32\config\systemprofile\AppData\Roaming\rclone\ under the SYSTEM account. The user expects the config to appear in their own C:\Users\<username>\AppData\Roaming\rclone\.

Why it happens: %APPDATA% is a per-user environment variable. Under SYSTEM, it resolves to the SYSTEM account's roaming profile, not the logged-in user's profile.

How to avoid: This is a documentation/UX concern more than a generator concern. The generator correctly uses %APPDATA% when configPath = 'user-profile'. Phase 3 should surface a warning when the user selects user-profile path with Intune as a target. For Phase 2, document this in the test for the user-profile path.

Warning signs: Config file not found at expected user path after Intune deployment.

Pitfall 4: Empty optional fields included in rclone.conf

What goes wrong: For Azure Blob, user fills in account and key but leaves sas_url empty. The generator includes sas_url = (empty value) in the output. rclone may interpret an empty sas_url key differently from an absent one, potentially causing auth failures.

Why it happens: Generator iterates over all params entries without filtering empty strings.

How to avoid: The generator must skip key/value pairs where value is empty string: if (value !== '') { lines.push(...) }. This is included in the Pattern 1 code example above.

Warning signs: Generated config contains lines like sas_url = or region = .

Pitfall 5: Credential values visible in PowerShell transcript logs

What goes wrong: The install script uses Write-Host "Writing config..." and then interpolates the credential value into a string that gets logged. Intune Management Extension logs can be read by any local admin.

Why it happens: Developer writes $confContent = "account = $account" and the variable contains the actual access key.

How to avoid: Write all credential content inside a PowerShell here-string literal (@'...'@ — single-quoted, no interpolation). Better yet: the generator embeds the literal credential values directly into the here-string, which is a string in JavaScript. The PowerShell script receives the conf content as a verbatim string with no variable interpolation happening at run time.

Warning signs: Any $ variable interpolation inside the config content block of the generated script.


Code Examples

buildRcloneConf — complete implementation pattern

// src/generators/rclone-conf.ts
import type { WizardState } from '../store/types';

// Maps our BackendType to rclone's internal type string
// IMPORTANT: s3-compatible uses 'type = s3' — not 's3-compatible'
const RCLONE_TYPE_MAP: Record<string, string> = {
  azureblob: 'azureblob',
  s3: 's3',
  's3-compatible': 's3',
};

export function buildRcloneConf(state: WizardState): string {
  const { name, backendType, params } = state.remote;
  if (!backendType) throw new Error('backendType is required');
  if (!name) throw new Error('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';
}

buildIntuneDetection — complete implementation pattern

// src/generators/intune-detection.ts
import type { WizardState } from '../store/types';

const CONFIG_DIR: Record<WizardState['deployment']['configPath'], string> = {
  'machine-wide': 'C:\\ProgramData\\rclone',
  'user-profile': '%APPDATA%\\rclone',
};

export function buildIntuneDetection(state: WizardState): string {
  const configDir = CONFIG_DIR[state.deployment.configPath];
  // Note: No $ErrorActionPreference = 'Stop' — exceptions go to STDERR and break detection
  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';
}

Vitest test pattern for generator output

// src/generators/rclone-conf.test.ts
import { describe, it, expect } from 'vitest';
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: '' },
  },
};

describe('buildRcloneConf', () => {
  it('produces [remoteName] section header', () => {
    expect(buildRcloneConf(azureState)).toContain('[my-azure]');
  });

  it('produces type = azureblob for azureblob backend', () => {
    expect(buildRcloneConf(azureState)).toContain('type = azureblob');
  });

  it('produces type = s3 for s3-compatible backend (not s3-compatible)', () => {
    const s3CompatState: WizardState = {
      ...INITIAL_STATE,
      remote: {
        name: 'my-wasabi',
        backendType: 's3-compatible',
        params: { provider: 'Other', access_key_id: 'k', secret_access_key: 's', endpoint: 'https://s3.wasabisys.com', region: '' },
      },
    };
    const conf = buildRcloneConf(s3CompatState);
    expect(conf).toContain('type = s3');
    expect(conf).not.toContain('type = s3-compatible');
  });

  it('omits empty optional fields', () => {
    const conf = buildRcloneConf(azureState); // sas_url = ''
    expect(conf).not.toContain('sas_url');
  });
});

State of the Art

Old Approach Current Approach When Changed Impact
Set-Content -Encoding UTF8 for UTF-8 file writing [System.IO.File]::WriteAllText(path, content, [System.Text.UTF8Encoding]::new($false)) PS 5.1 limitation (unchanged) Set-Content -Encoding UTF8 always adds BOM in PS 5.1; the .NET method is the reliable no-BOM path
Invoke-WebRequest without flags Invoke-WebRequest -UseBasicParsing Consistent guidance for years Without -UseBasicParsing, the IE engine is invoked; on SYSTEM accounts this can hang
Intune PowerShell detection: any output format Exit 0 + Write-Output + NO Write-Error / no STDERR Consistent Intune behavior STDERR output (even with exit 0) marks app as not detected

Deprecated/outdated:

  • Add-Content for file writing in PS 5.1: auto-detects encoding from existing file; unreliable for new files, defaults to ANSI. Do not use.
  • Out-File -Encoding UTF8 in PS 5.1: writes UTF-8 with BOM. Avoid.

Open Questions

  1. %APPDATA% user-profile path in SYSTEM context

    • What we know: Under SYSTEM, %APPDATA% resolves to C:\Windows\system32\config\systemprofile\AppData\Roaming\ — not the logged-in user's profile. This is confirmed Windows behavior.
    • What's unclear: Whether the user-profile option is intended for manual deployment (user runs the script themselves) vs SYSTEM deployment. The REQUIREMENTS.md does not specify.
    • Recommendation: Generate the script with %APPDATA%\rclone as the path when configPath = 'user-profile', and add an inline comment in the generated script warning that under SYSTEM context this path resolves to the SYSTEM profile, not the current user. Leave the UX warning for Phase 3.
  2. Intune: UTF-8 BOM vs no-BOM recommendation contradiction

    • What we know: Microsoft Learn (2026-02-06) says "We recommend encoding your script as UTF-8 BOM" for detection scripts. Other Intune documentation says no-BOM. For Phase 2, generators produce plain JavaScript strings — encoding is irrelevant at this layer.
    • What's unclear: Whether BOM matters for scripts containing only ASCII characters. (It does not — BOM only matters when non-ASCII characters are present.)
    • Recommendation: All generated scripts use only ASCII characters (credential values are opaque strings, path names are ASCII). The BOM question is a Phase 4 (file download) concern. Mark for Phase 4 research.
  3. rclone.exe download: 32-bit vs 64-bit architecture

    • What we know: The URL https://downloads.rclone.org/rclone-current-windows-amd64.zip targets x64. Most Windows endpoints managed by Intune/RMM are x64.
    • What's unclear: Whether to auto-detect architecture or hardcode x64.
    • Recommendation: Hardcode amd64 in v1. The vast majority of managed Windows endpoints are x64. Add architecture selection as a v2 enhancement if needed.

Validation Architecture

Test Framework

Property Value
Framework Vitest 4.1.1 (already installed)
Config file vitest.config.ts — exists, environment: 'node', globals: true
Quick run command npm test (runs vitest run)
Full suite command npm test

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
CONF-01 buildRcloneConf() produces valid INI with correct type and key/value pairs for all three backends unit npm test -- src/generators/rclone-conf.test.ts Wave 0
DEPL-01 Intune install script contains config directory creation, UTF-8 no-BOM write idiom, correct path, exit 0 unit (substring assertions) npm test -- src/generators/intune-install.test.ts Wave 0
DEPL-02 Detection script exits 0 with Write-Output when both files present; exits 1 otherwise; no $ErrorActionPreference Stop unit (substring assertions) npm test -- src/generators/intune-detection.test.ts Wave 0
DEPL-03 RMM script is self-contained, contains directory creation and config write, no Intune-specific constructs unit (substring assertions) npm test -- src/generators/rmm-script.test.ts Wave 0
DEPL-04 With includeInstall=true: all three scripts contain rclone download URL; with false: URL is absent unit (presence/absence assertions) npm test -- src/generators/ Wave 0
DEPL-05 With configPath='machine-wide': scripts contain C:\ProgramData\rclone; with user-profile: %APPDATA%\rclone unit npm test -- src/generators/ Wave 0

Sampling Rate

  • Per task commit: npm test
  • Per wave merge: npm test
  • Phase gate: All generator unit tests green before marking Phase 2 complete

Wave 0 Gaps

  • src/generators/rclone-conf.test.ts — covers CONF-01 (all three backends, type mapping, empty field omission)
  • src/generators/intune-install.test.ts — covers DEPL-01, DEPL-04, DEPL-05
  • src/generators/intune-detection.test.ts — covers DEPL-02, DEPL-05
  • src/generators/rmm-script.test.ts — covers DEPL-03, DEPL-04, DEPL-05
  • src/generators/index.ts — re-exports (no tests needed; but file must exist for Phase 4 imports)

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

  • Phase 1 RESEARCH.md — rclone.conf INI format, field key verification status (MEDIUM from Phase 1, now upgraded to HIGH after live rclone.org verification in this research)
  • Phase 1 src/schemas/registry.ts — confirmed field keys: account, key, sas_url (azureblob); provider, access_key_id, secret_access_key, region (s3); adds endpoint (s3-compatible)
  • https://debay.blog/2019/10/03/powershell-utf8-and-bom/ — PowerShell UTF-8 BOM behavior in 5.1
  • https://www.spguides.com/powershell-write-to-file-utf8/[System.IO.File]::WriteAllText with UTF8Encoding($false) as canonical PS 5.1 no-BOM pattern

Tertiary (LOW confidence)

  • Community knowledge on RMM platform SYSTEM context behavior — multiple sources confirm all major RMM tools run scripts as SYSTEM; no single authoritative Microsoft/NinjaRMM/Datto source verified

Metadata

Confidence breakdown:

  • rclone.conf INI format and field keys: HIGH — verified from live rclone.org docs (azureblob, s3 pages)
  • PowerShell encoding (UTF-8 no-BOM): HIGH — verified from Microsoft Learn official PS encoding docs
  • Intune detection script contract: HIGH — verified from Microsoft Learn Win32 app docs (2026-02-06)
  • RMM SYSTEM context behavior: MEDIUM — community consensus, no single authoritative source
  • rclone download URL: HIGH — verified from rclone.org/install/ official docs

Research date: 2026-03-26 Valid until: 2026-06-26 (rclone backend field names are stable; Intune detection contract is stable; PowerShell 5.1 encoding behavior is stable)