UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

403 lines 15.8 kB
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