accs-cli
Version:
ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects
233 lines (197 loc) • 6.54 kB
JavaScript
/**
* Run files command - Execute different file types
*/
import { execa } from 'execa';
import path from 'path';
import chalk from 'chalk';
import { logger } from '../utils/logger.js';
import { FileUtils } from '../utils/file-utils.js';
import { SystemCheck } from '../utils/system-check.js';
const RUNNERS = {
'.js': { command: 'node', args: [] },
'.mjs': { command: 'node', args: [] },
'.ts': { command: 'tsx', args: [] },
'.py': { command: 'python3', args: [], fallback: 'python' },
'.php': { command: 'php', args: [] },
'.html': { command: 'open', args: [], description: 'Open in browser' },
'.sh': { command: 'bash', args: [] },
'.ps1': { command: 'powershell', args: ['-ExecutionPolicy', 'Bypass', '-File'] }
};
export function runCommand(program) {
program
.command('run')
.argument('<file>', 'File to run')
.option('-a, --args <args...>', 'Arguments to pass to the script')
.option('-e, --env <env>', 'Environment variables (JSON string)')
.option('-w, --watch', 'Watch file for changes and re-run')
.option('-v, --verbose', 'Verbose output')
.description('Run JavaScript, Python, PHP, HTML, or shell scripts')
.action(async (file, options) => {
try {
await runFile(file, options);
} catch (error) {
logger.error('Failed to run file:', error.message);
process.exit(1);
}
});
}
async function runFile(file, options) {
// Resolve file path
const filePath = path.resolve(file);
if (!FileUtils.exists(filePath)) {
throw new Error(`File not found: ${file}`);
}
if (!FileUtils.isFile(filePath)) {
throw new Error(`Path is not a file: ${file}`);
}
const extension = FileUtils.getExtension(filePath);
const runner = RUNNERS[extension];
if (!runner) {
throw new Error(`Unsupported file type: ${extension}. Supported types: ${Object.keys(RUNNERS).join(', ')}`);
}
// Check if runner command exists
let command = runner.command;
const commandExists = await SystemCheck.commandExists(command);
if (!commandExists && runner.fallback) {
const fallbackExists = await SystemCheck.commandExists(runner.fallback);
if (fallbackExists) {
command = runner.fallback;
} else {
throw new Error(`Neither ${runner.command} nor ${runner.fallback} is installed`);
}
} else if (!commandExists) {
throw new Error(`${runner.command} is not installed`);
}
// Special handling for HTML files
if (extension === '.html') {
await openHtmlFile(filePath, options);
return;
}
// Prepare execution
const args = [...runner.args, filePath];
if (options.args) {
args.push(...options.args);
}
// Parse environment variables
let env = process.env;
if (options.env) {
try {
const extraEnv = JSON.parse(options.env);
env = { ...process.env, ...extraEnv };
} catch (error) {
logger.warn('Invalid environment variables JSON, ignoring');
}
}
if (options.watch) {
await runWithWatch(command, args, filePath, env, options);
} else {
await executeSingleRun(command, args, env, options);
}
}
async function executeSingleRun(command, args, env) {
logger.info(`Running: ${chalk.cyan(command)} ${args.join(' ')}`);
logger.separator();
const startTime = Date.now();
try {
const subprocess = execa(command, args, {
env,
stdio: 'inherit',
shell: true
});
await subprocess;
const duration = Date.now() - startTime;
logger.separator();
logger.success(`Completed in ${duration}ms`);
} catch (error) {
const duration = Date.now() - startTime;
logger.separator();
logger.error(`Process failed after ${duration}ms`);
if (error.exitCode !== undefined) {
logger.error(`Exit code: ${error.exitCode}`);
}
throw error;
}
}
async function runWithWatch(command, args, filePath, env, options) {
logger.info(`Watching file: ${chalk.cyan(filePath)}`);
logger.info(`Command: ${chalk.gray(command)} ${args.join(' ')}`);
logger.info('Press Ctrl+C to stop watching');
logger.separator();
const chokidar = await import('chokidar');
const watcher = chokidar.watch(filePath, {
persistent: true,
ignoreInitial: false
});
let currentProcess = null;
const runCommand = async () => {
// Kill existing process if running
if (currentProcess && !currentProcess.killed) {
currentProcess.kill('SIGTERM');
await new Promise(resolve => {
currentProcess.on('exit', resolve);
setTimeout(resolve, 1000); // Force kill after 1 second
});
}
const timestamp = new Date().toLocaleTimeString();
logger.info(`[${timestamp}] Running: ${chalk.cyan(command)} ${args.join(' ')}`);
try {
currentProcess = execa(command, args, {
env,
stdio: 'inherit',
shell: true
});
await currentProcess;
logger.success(`[${timestamp}] Completed successfully`);
} catch (error) {
if (!error.killed) {
logger.error(`[${timestamp}] Process failed`);
if (options.verbose) {
logger.error(error.message);
}
}
}
};
watcher.on('change', () => {
logger.info(`File changed: ${chalk.yellow(path.basename(filePath))}`);
runCommand();
});
watcher.on('ready', () => {
runCommand();
});
// Handle process exit
process.on('SIGINT', () => {
logger.info('\nStopping file watcher...');
if (currentProcess && !currentProcess.killed) {
currentProcess.kill('SIGTERM');
}
watcher.close();
process.exit(0);
});
}
async function openHtmlFile(filePath) {
const absolutePath = path.resolve(filePath);
const fileUrl = `file://${absolutePath}`;
logger.info(`Opening HTML file in browser: ${chalk.cyan(fileUrl)}`);
try {
const { default: open } = await import('open');
await open(fileUrl);
logger.success('File opened in default browser');
} catch (error) {
// Fallback to system commands
try {
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
if (isWindows) {
await execa('start', [fileUrl], { shell: true });
} else if (isMac) {
await execa('open', [fileUrl]);
} else {
await execa('xdg-open', [fileUrl]);
}
logger.success('File opened in default browser');
} catch (fallbackError) {
logger.error('Failed to open HTML file in browser');
logger.info(`Manual: ${fileUrl}`);
}
}
}