@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
403 lines • 15.8 kB
JavaScript
import chalk from 'chalk';
import * as path from 'path';
import * as repl from 'repl';
import { $ } from '@xec-sh/core';
import * as fs from 'fs/promises';
import { pathToFileURL } from 'url';
import * as clack from '@clack/prompts';
import { getModuleLoader, initializeGlobalModuleContext } from './module-loader.js';
export class ScriptLoader {
constructor(options = {}) {
this.options = {
verbose: options.verbose || process.env['XEC_DEBUG'] === 'true',
cache: options.cache !== false,
preferredCDN: (options.preferredCDN || 'esm.sh'),
quiet: options.quiet || false,
typescript: options.typescript || false,
};
this.moduleLoader = getModuleLoader({
verbose: this.options.verbose,
cache: this.options.cache,
preferredCDN: this.options.preferredCDN,
});
}
async executeScript(scriptPath, options = {}) {
try {
await initializeGlobalModuleContext({
verbose: this.options.verbose,
preferredCDN: this.options.preferredCDN || 'esm.sh',
});
const absolutePath = path.resolve(scriptPath);
try {
await fs.access(absolutePath);
}
catch {
throw new Error(`Script file not found: ${scriptPath}`);
}
if (options.watch) {
return await this.executeWithWatch(absolutePath, options);
}
return await this.executeScriptInternal(absolutePath, options);
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
async executeScriptInternal(scriptPath, options) {
const context = options.context || {
args: [],
argv: [process.argv[0], scriptPath],
__filename: scriptPath,
__dirname: path.dirname(scriptPath),
};
const originalValues = new Map();
const globalsToInject = new Map();
globalsToInject.set('__xecScriptContext', context);
if (options.target && options.targetEngine) {
const targetInfo = this.createTargetInfo(options.target);
globalsToInject.set('$target', options.targetEngine);
globalsToInject.set('$targetInfo', targetInfo);
}
else if (options.target || options.targetEngine) {
const localTarget = $;
const localTargetInfo = {
type: 'local',
name: 'local',
config: {},
};
globalsToInject.set('$target', localTarget);
globalsToInject.set('$targetInfo', localTargetInfo);
}
for (const [key, value] of globalsToInject) {
if (key in globalThis) {
originalValues.set(key, globalThis[key]);
}
globalThis[key] = value;
}
try {
await this.moduleLoader.loadScript(scriptPath, context.args || []);
return {
success: true,
};
}
finally {
for (const [key] of globalsToInject) {
if (originalValues.has(key)) {
globalThis[key] = originalValues.get(key);
}
else {
delete globalThis[key];
}
}
}
}
async executeWithWatch(scriptPath, options) {
const { watch } = await import('chokidar');
const runAndLog = async () => {
try {
if (!this.options.quiet) {
clack.log.info(chalk.dim(`Running ${scriptPath}...`));
}
const result = await this.executeScriptInternal(scriptPath, options);
if (!result.success && result.error) {
console.error(result.error);
}
}
catch (error) {
console.error(error);
}
};
await runAndLog();
const watcher = watch(scriptPath, { ignoreInitial: true });
watcher.on('change', async () => {
console.clear();
clack.log.info(chalk.dim('File changed, rerunning...'));
await runAndLog();
});
process.stdin.resume();
return {
success: true,
};
}
async evaluateCode(code, options = {}) {
try {
await initializeGlobalModuleContext({
verbose: this.options.verbose,
preferredCDN: this.options.preferredCDN || 'esm.sh',
});
if (!this.options.quiet && !options.quiet) {
clack.log.info(`Evaluating code...`);
}
const needsTransform = code.includes('interface') ||
code.includes('type ') ||
options.typescript ||
this.options.typescript;
const transformedCode = needsTransform
? await this.moduleLoader.transformTypeScript(code, '<eval>')
: code;
const context = options.context || {
args: [],
argv: ['xec', '<eval>'],
__filename: '<eval>',
__dirname: process.cwd(),
};
const originalValues = new Map();
const globalsToInject = new Map();
globalsToInject.set('__xecScriptContext', context);
if (options.target && options.targetEngine) {
const targetInfo = this.createTargetInfo(options.target);
globalsToInject.set('$target', options.targetEngine);
globalsToInject.set('$targetInfo', targetInfo);
}
for (const [key, value] of globalsToInject) {
if (key in globalThis) {
originalValues.set(key, globalThis[key]);
}
globalThis[key] = value;
}
try {
const dataUrl = `data:text/javascript;base64,${Buffer.from(transformedCode).toString('base64')}`;
await import(dataUrl);
return {
success: true,
};
}
finally {
for (const [key] of globalsToInject) {
if (originalValues.has(key)) {
globalThis[key] = originalValues.get(key);
}
else {
delete globalThis[key];
}
}
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(String(error)),
};
}
}
async startRepl(options = {}) {
await initializeGlobalModuleContext({
verbose: this.options.verbose,
preferredCDN: this.options.preferredCDN || 'esm.sh',
});
const title = options.target
? `Xec Interactive Shell (${options.target.name})`
: 'Xec Interactive Shell';
clack.log.info(chalk.bold(title));
clack.log.info(chalk.dim('Type .help for commands'));
const prompt = options.target
? chalk.cyan(`xec:${options.target.name}> `)
: chalk.cyan('xec> ');
const replServer = repl.start({
prompt,
useGlobal: false,
breakEvalOnSigint: true,
useColors: true,
});
const scriptUtils = await import('./script-utils.js');
const replContext = {
$,
...scriptUtils.default,
chalk,
console,
process,
use: (spec) => globalThis.use?.(spec),
x: (spec) => globalThis.x?.(spec),
};
if (options.target && options.targetEngine) {
const targetInfo = this.createTargetInfo(options.target);
replContext.$target = options.targetEngine;
replContext.$targetInfo = targetInfo;
}
Object.assign(replServer.context, replContext);
this.addReplCommands(replServer, options);
if (options.target && options.targetEngine) {
console.log(chalk.gray('Available globals:'));
console.log(chalk.gray(' $target - Execute commands on the target'));
console.log(chalk.gray(' $targetInfo - Information about the current target'));
console.log(chalk.gray(' $ - Execute commands locally'));
console.log(chalk.gray(' chalk - Terminal colors'));
console.log(chalk.gray(' use() - Import NPM packages or CDN modules'));
console.log(chalk.gray(' import() - Import modules'));
console.log(chalk.gray(''));
console.log(chalk.gray('Example: await $target`ls -la`'));
console.log(chalk.gray('Example: const lodash = await use("lodash")'));
}
else {
console.log(chalk.gray('Type .runtime to see runtime information'));
console.log(chalk.gray('Type .load <file> to load and run a script'));
}
console.log(chalk.gray(''));
}
async loadDynamicCommand(filePath, program, commandName) {
try {
await initializeGlobalModuleContext({
verbose: this.options.verbose,
preferredCDN: this.options.preferredCDN || 'esm.sh',
});
const ext = path.extname(filePath);
let moduleExports;
const content = await fs.readFile(filePath, 'utf-8');
const processedContent = content.replace(/import\s*\(\s*['"`]((npm|jsr|esm|unpkg|skypack|jsdelivr):[^'"`]*?)['"`]\s*\)/g, "globalThis.use('$1')");
const transformedCode = (ext === '.ts' || ext === '.tsx')
? await this.moduleLoader.transformTypeScript(processedContent, filePath)
: processedContent;
const tempDir = await this.findTempDirectory(filePath);
const tempDirPath = path.join(tempDir, '.xec-temp');
await fs.mkdir(tempDirPath, { recursive: true });
const tempFileName = `${commandName.replace(/[^a-zA-Z0-9]/g, '-')}-${Date.now()}.mjs`;
const tempPath = path.join(tempDirPath, tempFileName);
await fs.writeFile(tempPath, transformedCode);
try {
moduleExports = await import(pathToFileURL(tempPath).href);
}
finally {
await fs.unlink(tempPath).catch(() => { });
}
if (!moduleExports) {
throw new Error('Module did not export anything');
}
if (moduleExports.default && typeof moduleExports.default === 'function') {
moduleExports.default(program);
}
else if (moduleExports.command && typeof moduleExports.command === 'function') {
moduleExports.command(program);
}
else if (typeof moduleExports === 'function') {
moduleExports(program);
}
else {
throw new Error('Command file must export a default function or "command" function');
}
if (this.options.verbose) {
clack.log.info(`Loaded dynamic command: ${commandName}`);
}
return { success: true };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (this.options.verbose) {
console.error(`Failed to load command ${commandName}:`, error);
if (error instanceof Error && error.stack) {
console.error('Stack trace:', error.stack);
}
}
else {
clack.log.error(`Failed to load command '${commandName}': ${errorMessage}`);
}
return {
success: false,
error: errorMessage
};
}
}
createTargetInfo(target) {
const targetInfo = {
type: target.type,
name: target.name,
config: target.config,
};
switch (target.type) {
case 'ssh':
targetInfo.host = target.config.host;
break;
case 'docker':
targetInfo.container = target.config.container || target.name;
break;
case 'k8s':
targetInfo.pod = target.config.pod || target.name;
targetInfo.namespace = target.config.namespace;
break;
}
return targetInfo;
}
async findTempDirectory(scriptPath) {
let searchDir = path.dirname(scriptPath);
for (let i = 0; i < 10; i++) {
try {
await fs.access(path.join(searchDir, 'node_modules'));
return searchDir;
}
catch {
}
const parentDir = path.dirname(searchDir);
if (parentDir === searchDir)
break;
searchDir = parentDir;
}
return process.cwd();
}
addReplCommands(replServer, options) {
const self = this;
replServer.defineCommand('load', {
help: 'Load and run a script file',
action(filename) {
const trimmed = filename.trim();
self.executeScript(trimmed, options)
.then((result) => {
if (!result.success && result.error) {
console.error(result.error);
}
this.displayPrompt();
})
.catch((error) => {
console.error(error);
this.displayPrompt();
});
}
});
replServer.defineCommand('clear', {
help: 'Clear the console',
action() {
console.clear();
this.displayPrompt();
}
});
replServer.defineCommand('runtime', {
help: 'Show current runtime information',
action() {
console.log(`Runtime: ${chalk.cyan('Node.js')} ${chalk.dim(process.version)}`);
console.log(`Features:`);
console.log(` TypeScript: ${chalk.green('✓')}`);
console.log(` ESM: ${chalk.green('✓')}`);
console.log(` Workers: ${chalk.green('✓')}`);
if (options.target) {
console.log(`Target: ${chalk.cyan(options.target.type)} (${options.target.name})`);
}
this.displayPrompt();
}
});
}
static isScript(filepath) {
const ext = path.extname(filepath);
return ['.js', '.mjs', '.ts', '.tsx'].includes(ext);
}
}
let defaultLoader;
export function getUnifiedScriptLoader(options) {
if (!defaultLoader) {
defaultLoader = new ScriptLoader(options);
}
return defaultLoader;
}
export async function executeScript(scriptPath, options) {
const loader = getUnifiedScriptLoader(options);
return loader.executeScript(scriptPath, options);
}
export async function evaluateCode(code, options) {
const loader = getUnifiedScriptLoader(options);
return loader.evaluateCode(code, options);
}
export async function startRepl(options) {
const loader = getUnifiedScriptLoader(options);
return loader.startRepl(options);
}
//# sourceMappingURL=script-loader.js.map