@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
125 lines • 4.12 kB
JavaScript
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 };