177 lines
4.9 KiB
TypeScript
177 lines
4.9 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|