import { DenoDownloader } from './classes.denodownloader.js'; import * as plugins from './plugins.js'; import * as paths from './paths.js'; const MAX_STDIN_SIZE = 2 * 1024 * 1024; // 2MB threshold for stdin execution export type TDenoPermission = | 'all' | 'env' | 'ffi' | 'hrtime' | 'net' | 'read' | 'run' | 'sys' | 'write'; export interface ISmartDenoOptions { /** * Force downloading a local copy of Deno even if it's available in PATH */ forceLocalDeno?: boolean; } export interface IExecuteScriptOptions { /** * Deno permissions to grant to the script */ permissions?: TDenoPermission[]; } export class SmartDeno { private denoDownloader = new DenoDownloader(); private denoBinaryPath: string | null = null; private isStarted = false; private smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', }); /** * Starts the SmartDeno instance (downloads Deno if needed) * @param optionsArg Configuration options */ public async start(optionsArg: ISmartDenoOptions = {}): Promise { if (this.isStarted) { return; } const denoAlreadyInPath = await plugins.smartshell.which('deno', { nothrow: true, }); if (!denoAlreadyInPath || optionsArg.forceLocalDeno) { this.denoBinaryPath = await this.denoDownloader.download( plugins.path.join(paths.nogitDir, 'deno.zip') ); } else { this.denoBinaryPath = 'deno'; } this.isStarted = true; } /** * Stops the SmartDeno instance */ public async stop(): Promise { this.isStarted = false; } /** * Check if the SmartDeno instance is running */ public isRunning(): boolean { return this.isStarted; } /** * Build permission flags for Deno */ private buildPermissionFlags(permissions?: TDenoPermission[]): string { if (!permissions || permissions.length === 0) { return ''; } if (permissions.includes('all')) { return '-A'; } return permissions.map((p) => `--allow-${p}`).join(' '); } /** * Execute a Deno script * @param scriptArg The script content to execute * @param options Execution options including permissions * @returns Execution result with exitCode, stdout, and stderr */ public async executeScript( scriptArg: string, options: IExecuteScriptOptions = {} ): Promise<{ exitCode: number; stdout: string; stderr: string }> { if (!this.isStarted) { throw new Error('SmartDeno is not started. Call start() first.'); } const denoBinary = this.denoBinaryPath || 'deno'; const permissionFlags = this.buildPermissionFlags(options.permissions); const scriptSize = Buffer.byteLength(scriptArg, 'utf8'); if (scriptSize < MAX_STDIN_SIZE) { // Use stdin for small scripts (in-memory) return this.executeViaStdin(denoBinary, permissionFlags, scriptArg); } else { // Use temp file for large scripts return this.executeViaTempFile(denoBinary, permissionFlags, scriptArg); } } /** * Execute script via stdin (in-memory, for scripts < 2MB) * Uses `deno run -` which reads from stdin and supports all permission flags */ private async executeViaStdin( denoBinary: string, permissionFlags: string, script: string ): Promise<{ exitCode: number; stdout: string; stderr: string }> { // Use base64 encoding to safely pass script through shell const base64Script = Buffer.from(script).toString('base64'); const command = `echo '${base64Script}' | base64 -d | ${denoBinary} run ${permissionFlags} -`.trim().replace(/\s+/g, ' '); const result = await this.smartshellInstance.exec(command); return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, }; } /** * Execute script via temp file (for scripts >= 2MB) */ private async executeViaTempFile( denoBinary: string, permissionFlags: string, script: string ): Promise<{ exitCode: number; stdout: string; stderr: string }> { const tempFileName = `deno_script_${plugins.smartunique.shortId()}.ts`; const tempFilePath = plugins.path.join(paths.nogitDir, tempFileName); const fsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode()); try { // Ensure .nogit directory exists await fsInstance.directory(paths.nogitDir).create(); // Write script to temp file await fsInstance.file(tempFilePath).write(script); // Execute the script const command = `${denoBinary} run ${permissionFlags} "${tempFilePath}"`.trim().replace(/\s+/g, ' '); const result = await this.smartshellInstance.exec(command); return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, }; } finally { // Clean up temp file try { await fsInstance.file(tempFilePath).delete(); } catch { // Ignore cleanup errors } } } }