git-aiflow
Version:
🚀 An AI-powered workflow automation tool for effortless Git-based development, combining smart GitLab/GitHub merge & pull request creation with Conan package management.
120 lines • 5.18 kB
JavaScript
import { spawnSync } from "node:child_process";
import { logger } from './logger.js';
import { platform } from 'os';
import { parse } from 'shell-quote';
/**
* Cross-platform shell executor
* - Windows: PowerShell Core (pwsh) or Windows PowerShell (powershell)
* - macOS/Linux: bash or zsh
*/
export class Shell {
constructor() {
logger.debug(`Initializing Shell, platform: ${platform()} process.cwd: ${process.cwd()}`);
}
static instance() {
const pwd = process.cwd();
if (Shell.instances.get(pwd)) {
return Shell.instances.get(pwd);
}
const shell = new Shell();
Shell.instances.set(pwd, shell);
return shell;
}
/**
* Execute a shell command and return stdout as string
* @param command The command to execute
*/
run(shell_command, ...args) {
logger.debug(`Executing shell command: \n----\n${shell_command}${args.length > 0 ? ` ${args.join(" ")}` : ""}\n----`);
const startTime = Date.now();
try {
let command;
let commandArgs;
if (args.length > 0) {
// Prefer explicit (executable, ...args)
command = shell_command;
commandArgs = args;
}
else {
// Safely split the shell_command string into command and args
const parsed = parse(shell_command).filter((x) => typeof x === "string");
if (parsed.length === 0) {
throw new Error("No command to execute.");
}
command = parsed[0];
commandArgs = parsed.slice(1);
}
// Always use shell: false to avoid command injection
const result = spawnSync(command, commandArgs, {
encoding: "utf-8", shell: false, cwd: process.cwd(),
maxBuffer: 1024 * 1024 * 10,
});
const duration = Date.now() - startTime;
if (result.error) {
logger.error(`Command failed (${duration}ms): ${shell_command}${args.length > 0 ? ` ${args.join(" ")}` : ""}`, result.error);
return result.error.message;
}
const output = result.stdout.replace(/[\r\n]+/g, "\n").trimEnd() || result.stderr.replace(/[\r\n]+/g, "\n").trimEnd();
logger.debug(`Command output: \n----\n${output}\n----`);
logger.debug(`Command completed (${duration}ms) ${JSON.stringify({
command: shell_command + (args.length > 0 ? ` ${args.join(" ")}` : ""),
output,
outputLength: output.length,
exitCode: result.status
})}`);
return output;
}
catch (error) {
const duration = Date.now() - startTime;
logger.error(`Shell execution failed after ${duration}ms`, error);
return error instanceof Error ? error.message : String(error);
}
}
runProcess(process, ...args) {
return this.run(process, ...args);
}
/**
* Execute a shell command and return detailed result including exit code
* @param command The command to execute
* @param args Command arguments
* @returns Object containing success status, output, and exit code
*/
runWithExitCode(command, ...args) {
logger.debug(`Executing shell command: \n----\n${command}${args.length > 0 ? ` ${args.join(" ")}` : ""}\n----`);
const startTime = Date.now();
try {
// Always use shell: false to avoid command injection
const result = spawnSync(command, args, {
encoding: "utf-8",
shell: false,
cwd: process.cwd(),
maxBuffer: 1024 * 1024 * 10,
});
const duration = Date.now() - startTime;
const exitCode = result.status || 0;
const success = exitCode === 0 && !result.error;
if (result.error) {
logger.debug(`Command failed with error (${duration}ms): ${result.error.message}`);
return { success: false, output: result.error.message, exitCode: -1 };
}
const output = result.stdout.replace(/[\r\n]+/g, "\n").trimEnd() || result.stderr.replace(/[\r\n]+/g, "\n").trimEnd();
logger.debug(`Command output: \n----\n${output}\n----`);
logger.debug(`Command completed (${duration}ms) ${JSON.stringify({
command: command + (args.length > 0 ? ` ${args.join(" ")}` : ""),
output,
outputLength: output.length,
exitCode,
success
})}`);
return { success, output, exitCode };
}
catch (error) {
const duration = Date.now() - startTime;
logger.error(`Shell execution failed after ${duration}ms`, error);
const errorMessage = error instanceof Error ? error.message : String(error);
return { success: false, output: errorMessage, exitCode: -1 };
}
}
}
Shell.instances = new Map();
//# sourceMappingURL=shell.js.map