feat(smartdeno): Run small scripts in-memory via stdin, fall back to temp files for large scripts; remove internal HTTP script server and simplify plugin dependencies; update API and docs.

This commit is contained in:
2025-12-02 11:39:46 +00:00
parent 51b0af3d2e
commit d4a100ff32
10 changed files with 121 additions and 247 deletions

View File

@@ -1,18 +1,25 @@
import { DenoDownloader } from './classes.denodownloader.js';
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { ScriptServer } from './classes.scriptserver.js';
import { DenoExecution, type TDenoPermission } from './classes.denoexecution.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;
/**
* Port for the internal script server (default: 3210)
*/
port?: number;
}
export interface IExecuteScriptOptions {
@@ -24,16 +31,14 @@ export interface IExecuteScriptOptions {
export class SmartDeno {
private denoDownloader = new DenoDownloader();
private scriptServer: ScriptServer;
private denoBinaryPath: string | null = null;
private isStarted = false;
constructor() {
this.scriptServer = new ScriptServer();
}
private smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
/**
* Starts the SmartDeno instance
* Starts the SmartDeno instance (downloads Deno if needed)
* @param optionsArg Configuration options
*/
public async start(optionsArg: ISmartDenoOptions = {}): Promise<void> {
@@ -41,11 +46,8 @@ export class SmartDeno {
return;
}
// Create script server with configured port
this.scriptServer = new ScriptServer({ port: optionsArg.port });
const denoAlreadyInPath = await plugins.smartshell.which('deno', {
nothrow: true
nothrow: true,
});
if (!denoAlreadyInPath || optionsArg.forceLocalDeno) {
@@ -56,19 +58,13 @@ export class SmartDeno {
this.denoBinaryPath = 'deno';
}
await this.scriptServer.start();
this.isStarted = true;
}
/**
* Stops the SmartDeno instance and cleans up resources
* Stops the SmartDeno instance
*/
public async stop(): Promise<void> {
if (!this.isStarted) {
return;
}
await this.scriptServer.stop();
this.isStarted = false;
}
@@ -79,6 +75,19 @@ export class SmartDeno {
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
@@ -93,11 +102,75 @@ export class SmartDeno {
throw new Error('SmartDeno is not started. Call start() first.');
}
const denoExecution = new DenoExecution(this.scriptServer, scriptArg, {
permissions: options.permissions,
denoBinaryPath: this.denoBinaryPath || undefined,
});
const denoBinary = this.denoBinaryPath || 'deno';
const permissionFlags = this.buildPermissionFlags(options.permissions);
const scriptSize = Buffer.byteLength(scriptArg, 'utf8');
return denoExecution.execute();
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
}
}
}
}