import { spawn } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { config } from '../config.js'; // Wraps a PSADT/package source folder into a `.intunewin` using the Microsoft // Win32 Content Prep Tool (IntuneWinAppUtil.exe) on Windows, or an optional // cross-platform packager via INTUNEWIN_BUILD_COMMAND. The command construction // is pure and unit-tested; the spawn wrapper is thin. export function resolveToolPath() { return config.intunewinUtilPath || path.join(config.toolsDir, 'IntuneWinAppUtil.exe'); } // Where to put the tool and whether we can build here. export function builderAvailability() { if (config.intunewinBuildCommand) { return { available: true, mode: 'external-command', toolPath: null }; } const toolPath = resolveToolPath(); if (!fs.existsSync(toolPath)) { return { available: false, mode: 'bundled', toolPath, reason: `IntuneWinAppUtil.exe not found at ${toolPath}. Place it in the tools directory or set INTUNEWIN_UTIL_PATH / INTUNEWIN_BUILD_COMMAND.` }; } if (process.platform !== 'win32') { return { available: false, mode: 'bundled', toolPath, reason: 'IntuneWinAppUtil.exe requires Windows. Set INTUNEWIN_BUILD_COMMAND to use a cross-platform packager.' }; } return { available: true, mode: 'bundled', toolPath }; } // Pure: produce the spawn descriptor for the configured packager. export function buildPackagerInvocation({ toolPath, sourceFolder, setupFile, outputDir, commandTemplate }) { if (commandTemplate) { const command = commandTemplate .split('{source}').join(sourceFolder) .split('{setup}').join(setupFile) .split('{output}').join(outputDir); return { shell: true, command }; } return { shell: false, command: toolPath, args: ['-c', sourceFolder, '-s', setupFile, '-o', outputDir, '-q'] }; } // IntuneWinAppUtil names the output after the setup file's base name. export function expectedOutputFile(outputDir, setupFile) { return path.join(outputDir, `${path.parse(setupFile).name}.intunewin`); } export function buildIntuneWin({ sourceFolder, setupFile, outputDir }) { return new Promise((resolve, reject) => { const availability = builderAvailability(); if (!availability.available) return reject(new Error(availability.reason || 'Packager not available')); if (!sourceFolder || !fs.existsSync(sourceFolder)) return reject(new Error(`Source folder not found: ${sourceFolder}`)); if (!setupFile) return reject(new Error('A setup file (entry installer) is required')); fs.mkdirSync(outputDir, { recursive: true }); const invocation = buildPackagerInvocation({ toolPath: availability.toolPath, sourceFolder, setupFile, outputDir, commandTemplate: config.intunewinBuildCommand }); const child = invocation.shell ? spawn(invocation.command, { shell: true }) : spawn(invocation.command, invocation.args, { shell: false }); let stderr = ''; child.stderr.on('data', (chunk) => { stderr += String(chunk); }); child.on('error', (error) => reject(new Error(`Packager failed to start: ${error.message}`))); child.on('close', (code) => { const outputFile = expectedOutputFile(outputDir, setupFile); if (code === 0 && fs.existsSync(outputFile)) return resolve({ outputFile }); reject(new Error(`Packager exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`)); }); }); }