UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

125 lines 4.12 kB
import { execFileSync as nodeExecFileSync, spawn as nodeSpawn } from 'child_process'; import { isString } from 'es-toolkit/compat'; import { logger, runWithSuspendedSpinner, shouldSuspendSpinnerForStdio } from './logger.js'; import { serializeCliOptionsToArgs } from './cli.js'; const parseCommandParts = command => { if (!isString(command) || command.trim().length === 0) { throw new Error('Command execution requires a non-empty command string.'); } const parts = []; let currentPart = ''; let quote = null; for (const character of command.trim()) { if (quote) { if (character === quote) { quote = null; continue; } currentPart += character; continue; } if (character === '"' || character === "'") { quote = character; continue; } if (/\s/u.test(character)) { if (currentPart.length > 0) { parts.push(currentPart); currentPart = ''; } continue; } currentPart += character; } if (quote) { throw new Error(`Command execution could not parse ${command}. Unterminated quote.`); } if (currentPart.length > 0) { parts.push(currentPart); } if (parts.length === 0) { throw new Error('Command execution requires a non-empty command string.'); } return parts; }; const quotePowerShellArgument = value => { if (!isString(value)) { return String(value); } return `'${value.replace(/'/g, "''")}'`; }; const createPowerShellInvocationScript = (executable, args) => `& ${quotePowerShellArgument(executable)}${args.length > 0 ? ` ${args.map(quotePowerShellArgument).join(' ')}` : ''}`; const createCommandInvocation = (cmd, options, serializeCliOptionsImpl) => { const [executable, ...commandArgs] = parseCommandParts(cmd); return { args: [...commandArgs, ...serializeCliOptionsImpl(options)], executable }; }; const createProcessInvocation = (executable, args, runner, nativeRunner) => { if (process.platform !== 'win32' || runner !== nativeRunner) { return { args, executable }; } return { args: ['-NoProfile', '-Command', createPowerShellInvocationScript(executable, args)], executable: 'powershell.exe' }; }; export const quoteShellArgument = value => { if (!isString(value)) { return String(value); } if (value.length === 0) { return '""'; } if (!/[\s",]/.test(value)) { return value; } return `"${value.replace(/"/g, '\\"')}"`; }; export const formatShellCommand = parts => parts.map(quoteShellArgument).join(' '); export const execCommand = (cmd, options = {}, dependencies = {}) => { const { stdio = 'inherit', cwd, ...rest } = options; const { execImpl = nodeSpawn, serializeCliOptionsImpl = serializeCliOptionsToArgs } = dependencies; const invocation = createCommandInvocation(cmd, rest, serializeCliOptionsImpl); const processInvocation = createProcessInvocation(invocation.executable, invocation.args, execImpl, nodeSpawn); return execImpl(processInvocation.executable, processInvocation.args, { stdio, cwd }); }; export const execSyncCommand = (cmd, options = {}, dependencies = {}) => { const { stdio = 'inherit', cwd, log = false, ...rest } = options; const { execSyncImpl = nodeExecFileSync, loggerImpl = logger, serializeCliOptionsImpl = serializeCliOptionsToArgs } = dependencies; const invocation = createCommandInvocation(cmd, rest, serializeCliOptionsImpl); const processInvocation = createProcessInvocation(invocation.executable, invocation.args, execSyncImpl, nodeExecFileSync); const renderedCommand = formatShellCommand([invocation.executable, ...invocation.args]); if (log) { loggerImpl.debug(`Executing: ${renderedCommand}`); } return runWithSuspendedSpinner(() => execSyncImpl(processInvocation.executable, processInvocation.args, { stdio, cwd }), shouldSuspendSpinnerForStdio(stdio)); }; export const isInteractiveTerminal = () => process.stdin?.isTTY === true && process.stdout?.isTTY === true; export { execCommand as exec, execSyncCommand as execSync };