diff --git a/changelog.md b/changelog.md index 1c86232..a2eb5f5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-12-02 - 1.2.0 - 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. + +- Execute scripts < 2MB via stdin (deno run -) to support fully in-memory execution with no disk I/O. +- Automatically write scripts >= 2MB to a temp file under .nogit and clean up after execution. +- Removed internal HTTP script server implementation and related types; start() no longer starts a script server. +- Dropped plugin dependencies and exports related to @api.global/typedserver and @push.rocks/lik; plugins.ts simplified to only include necessary push.rocks modules and node path. +- Updated package.json to remove unused dependencies and adjust keywords to reflect in-memory execution. +- Updated README to document new start() behavior, in-memory execution, temp-file fallback, and simplified API signatures (start/stop/isRunning/executeScript). +- ts index now only exports SmartDeno (removed direct type export of TDenoPermission from separate file). + ## 2025-12-02 - 1.1.0 - feat(core) Add permission-controlled Deno execution, configurable script server port, improved downloader, dependency bumps and test updates diff --git a/package.json b/package.json index 5ef18c5..17226b1 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,6 @@ "@types/node": "^24.10.1" }, "dependencies": { - "@api.global/typedserver": "^4.0.0", - "@push.rocks/lik": "^6.2.2", "@push.rocks/smartarchive": "^5.0.1", "@push.rocks/smartfs": "^1.2.0", "@push.rocks/smartpath": "^6.0.0", @@ -59,9 +57,9 @@ "Development Tools", "Deno Download", "Code Execution", - "Scripting Server", "Ephemeral Execution", - "Cross-platform" + "Cross-platform", + "In-memory" ], "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00680f4..12e2d63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,6 @@ importers: .: dependencies: - '@api.global/typedserver': - specifier: ^4.0.0 - version: 4.0.0 - '@push.rocks/lik': - specifier: ^6.2.2 - version: 6.2.2 '@push.rocks/smartarchive': specifier: ^5.0.1 version: 5.0.1 @@ -57,9 +51,6 @@ packages: '@api.global/typedserver@3.0.80': resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==} - '@api.global/typedserver@4.0.0': - resolution: {integrity: sha512-FfAeVcGnVJdjDae9ryVZZZqtegk/N4Mbq+IIGVGJ/lCfNiwtXqBHXhsiLhVvz0Buja0swLnO6pF1DOS8cxzBzw==} - '@api.global/typedsocket@3.0.1': resolution: {integrity: sha512-xojiAVNXtHoxkpBo8U2HHJG8FrVXXuLvDNndSHXwx4C9VslUwDn5zSCI+PdBl8iAg+ZuBmKjqkpZZ9sL6DC5yQ==} @@ -769,10 +760,6 @@ packages: '@push.rocks/smartversion@3.0.5': resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==} - '@push.rocks/smartwatch@5.0.0': - resolution: {integrity: sha512-uuWUlTo0l5LWOWoOuTMG7zzxpUNKBcyqoB+zyQ24NHTtSYNcaUJtaQzTO2gxMXr5sqiZDkohlThS0KvsBc3g7w==} - engines: {node: '>=20.0.0'} - '@push.rocks/smartxml@2.0.0': resolution: {integrity: sha512-1d06zYJX4Zt8s5w5qFOUg2LAEz9ykrh9d6CQPK4WAgOBIefb1xzVEWHc7yoxicc2OkzNgC3IBCEg3s6BncZKWw==} @@ -3385,54 +3372,6 @@ snapshots: - utf-8-validate - vue - '@api.global/typedserver@4.0.0': - dependencies: - '@api.global/typedrequest': 3.1.10 - '@api.global/typedrequest-interfaces': 3.0.19 - '@api.global/typedsocket': 3.0.1 - '@cloudflare/workers-types': 4.20251202.0 - '@design.estate/dees-comms': 1.0.27 - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartenv': 6.0.0 - '@push.rocks/smartfeed': 1.4.0 - '@push.rocks/smartfile': 13.1.0 - '@push.rocks/smartfs': 1.2.0 - '@push.rocks/smartjson': 5.2.0 - '@push.rocks/smartlog': 3.1.10 - '@push.rocks/smartlog-destination-devtools': 1.0.12 - '@push.rocks/smartlog-interfaces': 3.0.2 - '@push.rocks/smartmanifest': 2.0.2 - '@push.rocks/smartmatch': 2.0.0 - '@push.rocks/smartmime': 2.0.4 - '@push.rocks/smartntml': 2.0.8 - '@push.rocks/smartopen': 2.0.0 - '@push.rocks/smartpath': 6.0.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 5.0.1 - '@push.rocks/smartrx': 3.0.10 - '@push.rocks/smartsitemap': 2.0.4 - '@push.rocks/smartstream': 3.2.5 - '@push.rocks/smarttime': 4.1.1 - '@push.rocks/smartwatch': 5.0.0 - '@push.rocks/taskbuffer': 3.4.0 - '@push.rocks/webrequest': 4.0.1 - '@push.rocks/webstore': 2.0.20 - '@tsclass/tsclass': 9.3.0 - '@types/express': 5.0.6 - body-parser: 2.2.1 - cors: 2.8.5 - express: 5.2.1 - express-force-ssl: 0.3.2 - lit: 3.3.1 - transitivePeerDependencies: - - '@nuxt/kit' - - bufferutil - - react - - supports-color - - utf-8-validate - - vue - '@api.global/typedsocket@3.0.1': dependencies: '@api.global/typedrequest': 3.1.10 @@ -5067,14 +5006,6 @@ snapshots: '@types/semver': 7.7.1 semver: 7.7.3 - '@push.rocks/smartwatch@5.0.0': - dependencies: - '@push.rocks/lik': 6.2.2 - '@push.rocks/smartenv': 6.0.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrx': 3.0.10 - picomatch: 4.0.3 - '@push.rocks/smartxml@2.0.0': dependencies: fast-xml-parser: 5.3.2 diff --git a/readme.md b/readme.md index 47231f1..8665d3c 100644 --- a/readme.md +++ b/readme.md @@ -43,9 +43,6 @@ await smartDeno.stop(); await smartDeno.start({ // Force download a local copy of Deno even if it's in PATH forceLocalDeno: true, - - // Custom port for the internal script server (default: 3210) - port: 4000, }); ``` @@ -153,7 +150,7 @@ const smartDeno = new SmartDeno(); app.use(express.json()); // Initialize on startup -await smartDeno.start({ port: 3211 }); +await smartDeno.start(); app.post('/execute', async (req, res) => { const { script, permissions } = req.body; @@ -180,8 +177,8 @@ app.listen(3000); SmartDeno works by: 1. **🦕 Deno Management** — Automatically downloads the latest Deno binary for your platform if not available or if `forceLocalDeno` is set -2. **🖥️ Script Server** — Runs an internal HTTP server that serves scripts to Deno -3. **⚡ Ephemeral Execution** — Each script execution is isolated and cleaned up after completion +2. **💾 In-Memory Execution** — Scripts < 2MB are executed via stdin (`deno run -`), staying fully in-memory with no disk I/O +3. **📁 Large Script Support** — Scripts >= 2MB automatically use a temp file (cleaned up after execution) 4. **🔒 Permission Control** — Translates permission options to Deno's security flags ## 📚 API Reference @@ -198,8 +195,8 @@ const smartDeno = new SmartDeno(); | Method | Description | |--------|-------------| -| `start(options?)` | Initialize and start SmartDeno | -| `stop()` | Stop SmartDeno and clean up resources | +| `start(options?)` | Initialize SmartDeno (downloads Deno if needed) | +| `stop()` | Stop SmartDeno instance | | `isRunning()` | Check if SmartDeno is currently running | | `executeScript(script, options?)` | Execute a Deno script | @@ -208,7 +205,6 @@ const smartDeno = new SmartDeno(); ```typescript interface ISmartDenoOptions { forceLocalDeno?: boolean; - port?: number; } interface IExecuteScriptOptions { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index a4fd500..fd196cb 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartdeno', - version: '1.1.0', + version: '1.2.0', description: 'A module to run Deno scripts from Node.js, including functionalities for downloading Deno and executing Deno scripts.' } diff --git a/ts/classes.denoexecution.ts b/ts/classes.denoexecution.ts deleted file mode 100644 index 4168323..0000000 --- a/ts/classes.denoexecution.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { ScriptServer } from './classes.scriptserver.js'; -import * as plugins from './plugins.js'; - -export type TDenoPermission = - | 'all' - | 'env' - | 'ffi' - | 'hrtime' - | 'net' - | 'read' - | 'run' - | 'sys' - | 'write'; - -export interface IDenoExecutionOptions { - permissions?: TDenoPermission[]; - denoBinaryPath?: string; -} - -/** - * This class contains logic to execute deno commands in an ephemeral way - */ -export class DenoExecution { - public id: string; - public scriptserverRef: ScriptServer; - public script: string; - public options: IDenoExecutionOptions; - - constructor(scriptserverRef: ScriptServer, scriptArg: string, options: IDenoExecutionOptions = {}) { - this.scriptserverRef = scriptserverRef; - this.script = scriptArg; - this.options = options; - this.id = plugins.smartunique.shortId(); - } - - private buildPermissionFlags(): string { - const permissions = this.options.permissions || []; - if (permissions.length === 0) { - return ''; - } - if (permissions.includes('all')) { - return '-A'; - } - return permissions.map(p => `--allow-${p}`).join(' '); - } - - public async execute(): Promise<{ exitCode: number; stdout: string; stderr: string }> { - this.scriptserverRef.executionMap.add(this); - - try { - const denoBinary = this.options.denoBinaryPath || 'deno'; - const permissionFlags = this.buildPermissionFlags(); - const port = this.scriptserverRef.getPort(); - const scriptUrl = `http://localhost:${port}/getscript/${this.id}`; - - const command = `${denoBinary} run ${permissionFlags} ${scriptUrl}`.replace(/\s+/g, ' ').trim(); - const result = await this.scriptserverRef.smartshellInstance.exec(command); - - return { - exitCode: result.exitCode, - stdout: result.stdout, - stderr: result.stderr, - }; - } finally { - // Clean up: remove from execution map after execution completes - this.scriptserverRef.executionMap.remove(this); - } - } -} diff --git a/ts/classes.scriptserver.ts b/ts/classes.scriptserver.ts deleted file mode 100644 index 26506a4..0000000 --- a/ts/classes.scriptserver.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { DenoExecution } from './classes.denoexecution.js'; -import * as plugins from './plugins.js'; - -export interface IScriptServerOptions { - port?: number; -} - -export class ScriptServer { - private server: plugins.typedserver.servertools.Server; - private port: number; - public smartshellInstance = new plugins.smartshell.Smartshell({ - executor: 'bash' - }); - - public executionMap = new plugins.lik.ObjectMap(); - - constructor(options: IScriptServerOptions = {}) { - this.port = options.port ?? 3210; - } - - public getPort(): number { - return this.port; - } - - public async start() { - this.server = new plugins.typedserver.servertools.Server({ - port: this.port, - cors: true, - }); - this.server.addRoute( - '/getscript/:executionId', - new plugins.typedserver.servertools.Handler('GET', async (req, res) => { - const executionId = req.params.executionId; - const denoExecution = await this.executionMap.find(async denoExecutionArg => { - return denoExecutionArg.id === executionId; - }); - if (!denoExecution) { - res.statusCode = 404; - res.write('Execution not found'); - res.end(); - return; - } - res.write(denoExecution.script); - res.end(); - }) - ); - await this.server.start(); - } - - public async stop() { - if (this.server) { - await this.server.stop(); - } - this.executionMap.wipe(); - } -} diff --git a/ts/classes.smartdeno.ts b/ts/classes.smartdeno.ts index 7d62af5..08399d4 100644 --- a/ts/classes.smartdeno.ts +++ b/ts/classes.smartdeno.ts @@ -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 { @@ -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 { - 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 + } + } } } diff --git a/ts/index.ts b/ts/index.ts index 9aaba06..7db0de5 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,2 +1 @@ export * from './classes.smartdeno.js'; -export type { TDenoPermission } from './classes.denoexecution.js'; diff --git a/ts/plugins.ts b/ts/plugins.ts index 06c4d8e..25b192c 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -5,15 +5,7 @@ export { path, } -// @api.global scope -import * as typedserver from '@api.global/typedserver'; - -export { - typedserver, -} - // @push.rocks scope -import * as lik from '@push.rocks/lik'; import * as smartarchive from '@push.rocks/smartarchive'; import * as smartfs from '@push.rocks/smartfs'; import * as smartpath from '@push.rocks/smartpath'; @@ -21,10 +13,9 @@ import * as smartshell from '@push.rocks/smartshell'; import * as smartunique from '@push.rocks/smartunique'; export { - lik, smartarchive, smartfs, smartpath, smartshell, smartunique, -} \ No newline at end of file +}