feat(core): Add permission-controlled Deno execution, configurable script server port, improved downloader, dependency bumps and test updates
This commit is contained in:
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
47
.serena/memories/code_style_and_conventions.md
Normal file
47
.serena/memories/code_style_and_conventions.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Code Style and Conventions
|
||||
|
||||
## Naming Conventions
|
||||
- **Interfaces**: Prefix with `I` (e.g., `IDenoRelease`, `IAsset`)
|
||||
- **Types**: Prefix with `T` (not heavily used in this codebase)
|
||||
- **Classes**: PascalCase (e.g., `SmartDeno`, `DenoDownloader`)
|
||||
- **Files**: Lowercase, hyphenated (e.g., `classes.smartdeno.ts`, `classes.denodownloader.ts`)
|
||||
- **Methods/Properties**: camelCase
|
||||
|
||||
## File Organization
|
||||
- **Source**: `ts/` directory
|
||||
- **Tests**: `test/` directory
|
||||
- **Compiled Output**: `dist_ts/` (excluded from git)
|
||||
- **Temporary Files**: `.nogit/` directory (excluded from git)
|
||||
|
||||
## File Naming Patterns
|
||||
- Classes: `classes.<name>.ts` (e.g., `classes.smartdeno.ts`)
|
||||
- Entry point: `index.ts`
|
||||
- Plugin/dependency imports: `plugins.ts`
|
||||
- Path configurations: `paths.ts`
|
||||
|
||||
## Import Patterns
|
||||
1. **All module dependencies** imported in `ts/plugins.ts`
|
||||
2. **References use full path**: `plugins.moduleName.className()`
|
||||
3. **Local imports** use `.js` extension (for ESM compatibility)
|
||||
|
||||
Example from plugins.ts:
|
||||
```typescript
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
export { smartfile };
|
||||
```
|
||||
|
||||
Usage:
|
||||
```typescript
|
||||
import * as plugins from './plugins.js';
|
||||
plugins.smartfile.fs.writeFile(...);
|
||||
```
|
||||
|
||||
## Class Patterns
|
||||
- Private properties for internal state
|
||||
- Public async methods for API
|
||||
- Dependency injection where appropriate
|
||||
- JSDoc comments for public methods
|
||||
|
||||
## Module Resolution
|
||||
- Always use `.js` extension in imports (even for `.ts` files)
|
||||
- This is required for ESM compatibility with NodeNext resolution
|
||||
63
.serena/memories/codebase_structure.md
Normal file
63
.serena/memories/codebase_structure.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Codebase Structure
|
||||
|
||||
## Directory Layout
|
||||
```
|
||||
smartdeno/
|
||||
├── ts/ # Source code
|
||||
│ ├── index.ts # Main entry point (exports SmartDeno)
|
||||
│ ├── plugins.ts # Dependency imports
|
||||
│ ├── paths.ts # Path configurations
|
||||
│ ├── classes.smartdeno.ts # Main SmartDeno class
|
||||
│ ├── classes.denodownloader.ts # Deno download logic
|
||||
│ ├── classes.scriptserver.ts # Script execution server
|
||||
│ ├── classes.denoexecution.ts # Script execution wrapper
|
||||
│ └── 00_commitinfo_data.ts # Commit metadata
|
||||
├── test/ # Test files
|
||||
│ └── test.ts # Main test suite
|
||||
├── dist_ts/ # Compiled output (gitignored)
|
||||
├── .nogit/ # Temporary/debug files (gitignored)
|
||||
├── assets/ # Static assets
|
||||
├── .gitea/ # Gitea-specific configs
|
||||
├── .vscode/ # VS Code settings
|
||||
├── .claude/ # Claude Code settings
|
||||
├── .serena/ # Serena agent settings
|
||||
├── package.json # Package manifest
|
||||
├── tsconfig.json # TypeScript config
|
||||
├── npmextra.json # Extended npm metadata
|
||||
├── readme.md # Main documentation
|
||||
└── readme.hints.md # Development hints (currently empty)
|
||||
```
|
||||
|
||||
## Core Classes
|
||||
|
||||
### SmartDeno (ts/classes.smartdeno.ts)
|
||||
Main orchestrator class with methods:
|
||||
- `start(options)` - Initialize and download Deno if needed
|
||||
- `stop()` - Cleanup resources
|
||||
- `executeScript(scriptArg)` - Execute a Deno script
|
||||
|
||||
### DenoDownloader (ts/classes.denodownloader.ts)
|
||||
Handles Deno binary download:
|
||||
- Fetches latest Deno release from GitHub
|
||||
- Platform detection (Linux, macOS, Windows)
|
||||
- Architecture detection (x64, arm64)
|
||||
- Downloads and extracts Deno binary
|
||||
|
||||
### ScriptServer (ts/classes.scriptserver.ts)
|
||||
Internal server for script execution
|
||||
|
||||
### DenoExecution (ts/classes.denoexecution.ts)
|
||||
Wraps individual script execution
|
||||
|
||||
## Entry Point Flow
|
||||
1. User imports from `@push.rocks/smartdeno`
|
||||
2. `index.ts` exports `SmartDeno` class
|
||||
3. User creates instance: `new SmartDeno()`
|
||||
4. User calls `await smartDeno.start()` to initialize
|
||||
5. User calls `await smartDeno.executeScript(code)` to run Deno code
|
||||
|
||||
## Dependencies Pattern
|
||||
All external dependencies are:
|
||||
1. Imported in `plugins.ts`
|
||||
2. Exported as namespace
|
||||
3. Used with full path (e.g., `plugins.smartfile.fs.readFile()`)
|
||||
20
.serena/memories/project_overview.md
Normal file
20
.serena/memories/project_overview.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Project Overview
|
||||
|
||||
## Purpose
|
||||
**@push.rocks/smartdeno** is a Node.js module that enables running Deno scripts from within Node.js environments. It provides functionality for:
|
||||
- Automatically downloading Deno if not available in the system
|
||||
- Executing Deno scripts from Node.js
|
||||
- Running a script server for ephemeral execution
|
||||
- Cross-platform support (Linux, macOS, Windows)
|
||||
|
||||
## Key Features
|
||||
- Seamless integration of Deno into Node.js workflows
|
||||
- Automatic Deno version management and downloading
|
||||
- Script execution via internal server
|
||||
- Support for both local and system-wide Deno installations
|
||||
|
||||
## Business Context
|
||||
- Owned and maintained by Task Venture Capital GmbH
|
||||
- MIT License (with trademark restrictions)
|
||||
- Published as `@push.rocks/smartdeno` on npm
|
||||
- Repository hosted at https://code.foss.global/push.rocks/smartdeno
|
||||
89
.serena/memories/suggested_commands.md
Normal file
89
.serena/memories/suggested_commands.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Suggested Commands
|
||||
|
||||
## Package Management
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Add runtime dependency
|
||||
pnpm install --save <package>
|
||||
|
||||
# Add dev dependency
|
||||
pnpm install --save-dev <package>
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Build the project (compiles TypeScript)
|
||||
pnpm build
|
||||
# Equivalent to: tsbuild --web --allowimplicitany
|
||||
|
||||
# Type check without building
|
||||
tsbuild check ts/**/* --skiplibcheck # For source files
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
# Equivalent to: tstest test/ --web
|
||||
|
||||
# Run specific test file
|
||||
tstest test/test.ts --verbose
|
||||
|
||||
# Run test with log file output
|
||||
tstest test/test.ts --verbose --logfile .nogit/testlogs/test.log
|
||||
```
|
||||
|
||||
### Documentation
|
||||
```bash
|
||||
# Build documentation
|
||||
pnpm buildDocs
|
||||
# Equivalent to: tsdoc
|
||||
```
|
||||
|
||||
### Running TypeScript Files
|
||||
```bash
|
||||
# tsx is available globally for direct execution
|
||||
tsx path/to/file.ts
|
||||
```
|
||||
|
||||
## Git Commands
|
||||
```bash
|
||||
# Check status
|
||||
git status
|
||||
|
||||
# Stage and commit (use small, focused commits)
|
||||
git add <files>
|
||||
git commit -m "description"
|
||||
|
||||
# Move files (preserves history)
|
||||
git mv old-path new-path
|
||||
```
|
||||
|
||||
## System Commands (Linux)
|
||||
- `ls` - List directory contents
|
||||
- `cd` - Change directory
|
||||
- `grep` - Search text
|
||||
- `find` - Find files
|
||||
- `cat` - Display file contents
|
||||
- `kill <PID>` - Kill specific process (NEVER use `killall node`)
|
||||
|
||||
## Port Management
|
||||
```bash
|
||||
# Find process on port
|
||||
lsof -i :80
|
||||
lsof -i :443
|
||||
|
||||
# Kill specific PID
|
||||
kill <PID>
|
||||
|
||||
# Wait between server restarts
|
||||
sleep 10
|
||||
```
|
||||
|
||||
## Debug Scripts
|
||||
- Store debug scripts in `.nogit/debug/`
|
||||
- Run with: `tsx .nogit/debug/script-name.ts`
|
||||
86
.serena/memories/task_completion_checklist.md
Normal file
86
.serena/memories/task_completion_checklist.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Task Completion Checklist
|
||||
|
||||
## After Code Changes
|
||||
|
||||
### 1. Type Checking
|
||||
```bash
|
||||
# Check source files
|
||||
pnpm build
|
||||
|
||||
# Check test files
|
||||
tsbuild check test/**/* --skiplibcheck
|
||||
```
|
||||
|
||||
### 2. Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Or run specific test
|
||||
tstest test/test.ts --verbose
|
||||
```
|
||||
|
||||
### 3. Linting/Formatting
|
||||
- No explicit linter/formatter configured in package.json
|
||||
- Follow existing code style patterns
|
||||
|
||||
### 4. Documentation
|
||||
```bash
|
||||
# Update documentation if public API changed
|
||||
pnpm buildDocs
|
||||
```
|
||||
|
||||
## Before Committing
|
||||
|
||||
### 1. Verify Changes
|
||||
```bash
|
||||
git status
|
||||
git diff
|
||||
```
|
||||
|
||||
### 2. Stage Files
|
||||
```bash
|
||||
git add <files>
|
||||
```
|
||||
|
||||
### 3. Commit with Clear Message
|
||||
```bash
|
||||
git commit -m "Brief description of single focused change"
|
||||
```
|
||||
|
||||
### Commit Best Practices
|
||||
- **Small, focused commits** with single clear purpose
|
||||
- **Descriptive messages** explaining "what" and "why"
|
||||
- **Avoid mixing** different change types in one commit
|
||||
- **Use git mv** for file operations to preserve history
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Test File Naming
|
||||
- `*.both.ts` - Browser and Node tests
|
||||
- `*.node.ts` - Node-only tests
|
||||
- `*.browser.ts` - Browser-only tests
|
||||
|
||||
### Test File Requirements
|
||||
- Import `expect`, `expectAsync`, `tap` from `@push.rocks/tapbundle`
|
||||
- Import TypeScript files directly (never compiled JS)
|
||||
- **MUST end with**: `export default tap.start()` or `tap.start()`
|
||||
- Place stubs ONLY in test files, never in production code
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Missing tsrun
|
||||
```bash
|
||||
# If you get "tsrun: command not found"
|
||||
pnpm install --save-dev @git.zone/tsrun
|
||||
```
|
||||
|
||||
### Server Management
|
||||
- **Before reading logs**: Wait 20 seconds for complete log writes
|
||||
- **When killing servers**: Find specific PID, never `killall node`
|
||||
- **Between restarts**: Wait 10 seconds
|
||||
|
||||
## Documentation Updates
|
||||
- Update `readme.md` if public API changes
|
||||
- Consider updating `readme.hints.md` for development findings
|
||||
- Store plans in `readme.plan.md` if needed
|
||||
35
.serena/memories/tech_stack.md
Normal file
35
.serena/memories/tech_stack.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Tech Stack
|
||||
|
||||
## Language & Runtime
|
||||
- **TypeScript** (ES2022 target)
|
||||
- **Node.js** (ES Modules, NodeNext module resolution)
|
||||
- Experimental decorators enabled
|
||||
|
||||
## Package Manager
|
||||
- **pnpm** exclusively (never npm or yarn)
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
### Runtime Dependencies
|
||||
- `@api.global/typedserver` - Typed server functionality
|
||||
- `@push.rocks/lik` - Core utilities
|
||||
- `@push.rocks/smartarchive` - Archive handling (for Deno zip extraction)
|
||||
- `@push.rocks/smartfile` - File system operations
|
||||
- `@push.rocks/smartpath` - Path utilities
|
||||
- `@push.rocks/smartshell` - Shell command execution
|
||||
- `@push.rocks/smartunique` - Unique ID generation
|
||||
|
||||
### Dev Dependencies
|
||||
- `@git.zone/tsbuild` - TypeScript building
|
||||
- `@git.zone/tsbundle` - Bundling
|
||||
- `@git.zone/tsrun` - Running TypeScript
|
||||
- `@git.zone/tstest` - Testing framework
|
||||
- `@push.rocks/tapbundle` - TAP testing utilities
|
||||
|
||||
## Build Configuration
|
||||
- **Module System**: ES Modules (type: "module")
|
||||
- **Module Resolution**: NodeNext
|
||||
- **Verbatim Module Syntax**: Enabled
|
||||
- **Experimental Decorators**: Enabled
|
||||
- **Use Define for Class Fields**: False
|
||||
- **Browser Target**: Latest Chrome version
|
||||
67
.serena/project.yml
Normal file
67
.serena/project.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "smartdeno"
|
||||
38
changelog.md
Normal file
38
changelog.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-02 - 1.1.0 - feat(core)
|
||||
Add permission-controlled Deno execution, configurable script server port, improved downloader, dependency bumps and test updates
|
||||
|
||||
- Add TDenoPermission type and support passing permissions to executeScript (translates to Deno flags)
|
||||
- Introduce IDenoExecutionOptions and make DenoExecution build and use permission flags; execution now served via ScriptServer URL and cleaned from executionMap after run
|
||||
- Make ScriptServer configurable with IScriptServerOptions (port), expose getPort(), return 404 for unknown execution ids, properly stop server and wipe execution map
|
||||
- SmartDeno: add ISmartDenoOptions (forceLocalDeno, port), guard executeScript to require start(), pass denoBinaryPath and permissions to DenoExecution
|
||||
- DenoDownloader: track denoBinaryPath, use @push.rocks/smartfs, extract archive, set executable permissions on Unix, and clean up zip file
|
||||
- Update plugins to export smartfs and smartshell (fix typos) and re-export path/typedserver namespaces
|
||||
- Export TDenoPermission from package entry (ts/index.ts)
|
||||
- Update tests to new tapbundle import, add lifecycle and error handling tests, and adapt to API changes
|
||||
- Bump various dependencies and devDependencies in package.json and add packageManager lock info
|
||||
- Add project memory/docs files (.serena) and project config updates
|
||||
|
||||
## 2024-05-29 - 1.0.3 - maintenance
|
||||
Consolidated metadata and build configuration updates for 1.0.3.
|
||||
|
||||
- Updated package description (2024-05-29).
|
||||
- Updated TypeScript configuration (tsconfig) (2024-04-14).
|
||||
- Updated npmextra.json githost entries (multiple commits on 2024-03-30 and 2024-04-01).
|
||||
- Duplicate/no-op version commits are summarized below.
|
||||
|
||||
## 2024-03-17 - 1.0.2 - fix(core)
|
||||
Core fix included in the 1.0.2 release.
|
||||
|
||||
- fix(core): update (2024-03-17).
|
||||
|
||||
## 2024-03-16 - 1.0.1 - fix(core)
|
||||
Core fix included in the 1.0.1 release.
|
||||
|
||||
- fix(core): update (2024-03-16).
|
||||
|
||||
## 2024-03-16 - 2024-03-17 - 1.0.2..1.0.3 - housekeeping
|
||||
Version-only commits (no substantive code changes) recorded for completeness.
|
||||
|
||||
- Commits with messages equal to the version number (e.g., "1.0.2", "1.0.3") were made on 2024-03-16–2024-03-17; no additional changes to summarize.
|
||||
31
package.json
31
package.json
@@ -9,26 +9,24 @@
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"test": "(tstest test/ --verbose --timeout 60)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "(tsdoc)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.25",
|
||||
"@git.zone/tsbundle": "^2.0.5",
|
||||
"@git.zone/tsrun": "^1.2.46",
|
||||
"@git.zone/tstest": "^1.0.44",
|
||||
"@push.rocks/tapbundle": "^5.0.15",
|
||||
"@types/node": "^20.8.7"
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@types/node": "^24.10.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedserver": "^3.0.27",
|
||||
"@push.rocks/lik": "^6.0.14",
|
||||
"@push.rocks/smartarchive": "^4.0.22",
|
||||
"@push.rocks/smartfile": "^11.0.4",
|
||||
"@push.rocks/smartpath": "^5.0.11",
|
||||
"@push.rocks/smartshell": "^3.0.4",
|
||||
"@push.rocks/smartunique": "^3.0.8"
|
||||
"@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",
|
||||
"@push.rocks/smartshell": "^3.3.0",
|
||||
"@push.rocks/smartunique": "^3.0.9"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -64,5 +62,6 @@
|
||||
"Scripting Server",
|
||||
"Ephemeral Execution",
|
||||
"Cross-platform"
|
||||
]
|
||||
}
|
||||
],
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
}
|
||||
|
||||
10453
pnpm-lock.yaml
generated
10453
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
251
readme.md
251
readme.md
@@ -1,114 +1,249 @@
|
||||
# @push.rocks/smartdeno
|
||||
a module to run deno from node
|
||||
|
||||
Run Deno scripts from Node.js with automatic Deno management, permission control, and ephemeral script execution. 🦕➡️📦
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Install
|
||||
|
||||
To install `@push.rocks/smartdeno`, run the following command in your project directory:
|
||||
|
||||
```bash
|
||||
npm install @push.rocks/smartdeno --save
|
||||
npm install @push.rocks/smartdeno
|
||||
# or
|
||||
pnpm add @push.rocks/smartdeno
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
This guide will provide an overview of how to use `@push.rocks/smartdeno` in your Node.js project to run Deno scripts. Given the nature of the project, which allows running Deno from Node.js environments, we'll explore various scenarios including setup, executing a simple Deno script, integrating with existing Node.js workflows, and managing Deno downloads.
|
||||
|
||||
### Setting Up
|
||||
|
||||
First, ensure you import `SmartDeno` in your TypeScript file using the ESM syntax:
|
||||
### 🚀 Basic Example
|
||||
|
||||
```typescript
|
||||
import { SmartDeno } from '@push.rocks/smartdeno';
|
||||
|
||||
const smartDeno = new SmartDeno();
|
||||
|
||||
// Start SmartDeno (downloads Deno if needed)
|
||||
await smartDeno.start();
|
||||
|
||||
// Execute a Deno script
|
||||
const result = await smartDeno.executeScript(`console.log("Hello from Deno!")`);
|
||||
console.log(result.stdout); // "Hello from Deno!"
|
||||
console.log(result.exitCode); // 0
|
||||
|
||||
// Clean up when done
|
||||
await smartDeno.stop();
|
||||
```
|
||||
|
||||
#### Initializing SmartDeno
|
||||
### ⚙️ Configuration Options
|
||||
|
||||
To start working with Deno in your Node.js project, you first need to create an instance of `SmartDeno` and start it. This process prepares the environment, including downloading Deno if necessary:
|
||||
#### Start Options
|
||||
|
||||
```typescript
|
||||
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,
|
||||
});
|
||||
```
|
||||
|
||||
### 🔐 Deno Permissions
|
||||
|
||||
Control what your Deno scripts can access using the permissions option:
|
||||
|
||||
```typescript
|
||||
// Allow network access
|
||||
const result = await smartDeno.executeScript(
|
||||
`const response = await fetch("https://api.example.com/data");
|
||||
console.log(await response.text());`,
|
||||
{ permissions: ['net'] }
|
||||
);
|
||||
|
||||
// Allow environment variable access
|
||||
const envResult = await smartDeno.executeScript(
|
||||
`console.log(Deno.env.get("HOME"))`,
|
||||
{ permissions: ['env'] }
|
||||
);
|
||||
|
||||
// Allow all permissions (use with caution! ⚠️)
|
||||
const fullResult = await smartDeno.executeScript(
|
||||
`// Script with full access`,
|
||||
{ permissions: ['all'] }
|
||||
);
|
||||
```
|
||||
|
||||
#### Available Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `all` | Grant all permissions (`-A` flag) |
|
||||
| `env` | Environment variable access |
|
||||
| `ffi` | Foreign function interface |
|
||||
| `hrtime` | High-resolution time measurement |
|
||||
| `net` | Network access |
|
||||
| `read` | File system read access |
|
||||
| `run` | Subprocess execution |
|
||||
| `sys` | System information access |
|
||||
| `write` | File system write access |
|
||||
|
||||
### 📤 Execution Results
|
||||
|
||||
The `executeScript` method returns detailed execution information:
|
||||
|
||||
```typescript
|
||||
const result = await smartDeno.executeScript(`console.log("test")`);
|
||||
|
||||
console.log(result.exitCode); // 0 for success, non-zero for errors
|
||||
console.log(result.stdout); // Standard output
|
||||
console.log(result.stderr); // Standard error output
|
||||
```
|
||||
|
||||
### 🛡️ Error Handling
|
||||
|
||||
```typescript
|
||||
// Scripts that throw errors return non-zero exit codes
|
||||
const result = await smartDeno.executeScript(`throw new Error("Something went wrong")`);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
console.error('Script failed:', result.stderr);
|
||||
}
|
||||
|
||||
// Attempting to execute before starting throws an error
|
||||
try {
|
||||
const unstarted = new SmartDeno();
|
||||
await unstarted.executeScript(`console.log("test")`);
|
||||
} catch (error) {
|
||||
console.error(error.message); // "SmartDeno is not started. Call start() first."
|
||||
}
|
||||
```
|
||||
|
||||
### 🔄 Lifecycle Management
|
||||
|
||||
```typescript
|
||||
const smartDeno = new SmartDeno();
|
||||
|
||||
async function setup() {
|
||||
await smartDeno.start({
|
||||
forceLocalDeno: true // Use this to force a local download of Deno even if it's globally available
|
||||
});
|
||||
}
|
||||
// Check if running
|
||||
console.log(smartDeno.isRunning()); // false
|
||||
|
||||
setup();
|
||||
await smartDeno.start();
|
||||
console.log(smartDeno.isRunning()); // true
|
||||
|
||||
// Execute scripts...
|
||||
|
||||
await smartDeno.stop();
|
||||
console.log(smartDeno.isRunning()); // false
|
||||
|
||||
// Safe to call stop() multiple times
|
||||
await smartDeno.stop(); // No error
|
||||
```
|
||||
|
||||
### Executing a Deno Script
|
||||
### 🌐 Integration Example
|
||||
|
||||
With `SmartDeno` set and started, you can now execute Deno scripts. Let's run a simple script that prints a message:
|
||||
|
||||
```typescript
|
||||
async function runDenoScript() {
|
||||
const script = `console.log("Hello from Deno!");`;
|
||||
await smartDeno.executeScript(script);
|
||||
}
|
||||
|
||||
runDenoScript();
|
||||
```
|
||||
|
||||
This method sends the script to Deno for execution. The flexibility of `SmartDeno` allows for more complex scripts to be executed similarly.
|
||||
|
||||
### Integrating with Node.js Workflows
|
||||
|
||||
`SmartDeno` can be seamlessly integrated into existing Node.js applications or workflows. Here's an example of integrating a Deno script execution within a Node.js Express server:
|
||||
Using SmartDeno in an Express server:
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { SmartDeno } from '@push.rocks/smartdeno';
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
const smartDeno = new SmartDeno();
|
||||
|
||||
app.get('/run-deno-script', async (req, res) => {
|
||||
const script = `console.log("Deno script executed!");`;
|
||||
await smartDeno.executeScript(script);
|
||||
res.send('Deno script executed. Check console for output.');
|
||||
app.use(express.json());
|
||||
|
||||
// Initialize on startup
|
||||
await smartDeno.start({ port: 3211 });
|
||||
|
||||
app.post('/execute', async (req, res) => {
|
||||
const { script, permissions } = req.body;
|
||||
|
||||
try {
|
||||
const result = await smartDeno.executeScript(script, { permissions });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening at http://localhost:${port}`);
|
||||
// Cleanup on shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
await smartDeno.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
### Managing Deno Version
|
||||
## 🔧 How It Works
|
||||
|
||||
To manage which Deno version `SmartDeno` uses, the setup step allows for specifying `forceLocalDeno`. If you need to use a specific version of Deno, you can customize the download process by extending or modifying the download logic in `classes.denodownloader.ts`.
|
||||
SmartDeno works by:
|
||||
|
||||
### Stopping SmartDeno
|
||||
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
|
||||
4. **🔒 Permission Control** — Translates permission options to Deno's security flags
|
||||
|
||||
When your application is done using Deno, or if you need to clean up resources, you can stop the `SmartDeno` instance:
|
||||
## 📚 API Reference
|
||||
|
||||
### `SmartDeno`
|
||||
|
||||
#### Constructor
|
||||
|
||||
```typescript
|
||||
async function teardown() {
|
||||
await smartDeno.stop();
|
||||
}
|
||||
|
||||
teardown();
|
||||
const smartDeno = new SmartDeno();
|
||||
```
|
||||
|
||||
### Conclusion
|
||||
#### Methods
|
||||
|
||||
`@push.rocks/smartdeno` provides a powerful and simple pathway for Node.js applications to run Deno scripts, combine the strengths of both environments, and leverage Deno's features within Node.js projects. Whether it's for executing isolated scripts, taking advantage of Deno's security model, or using Deno modules, `SmartDeno` offers the tools necessary for seamless integration.
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `start(options?)` | Initialize and start SmartDeno |
|
||||
| `stop()` | Stop SmartDeno and clean up resources |
|
||||
| `isRunning()` | Check if SmartDeno is currently running |
|
||||
| `executeScript(script, options?)` | Execute a Deno script |
|
||||
|
||||
For further information and advanced use cases, refer to the official GitHub repository and the typed documentation linked at the top of this guide.
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
interface ISmartDenoOptions {
|
||||
forceLocalDeno?: boolean;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
interface IExecuteScriptOptions {
|
||||
permissions?: TDenoPermission[];
|
||||
}
|
||||
|
||||
type TDenoPermission =
|
||||
| 'all'
|
||||
| 'env'
|
||||
| 'ffi'
|
||||
| 'hrtime'
|
||||
| 'net'
|
||||
| 'read'
|
||||
| 'run'
|
||||
| 'sys'
|
||||
| 'write';
|
||||
```
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
55
test/test.ts
55
test/test.ts
@@ -1,20 +1,59 @@
|
||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
||||
import * as smartdeno from '../ts/index.js'
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartdeno from '../ts/index.js';
|
||||
|
||||
let testSmartdeno: smartdeno.SmartDeno;
|
||||
|
||||
tap.test('should create a valid smartdeno instance', async () => {
|
||||
testSmartdeno = new smartdeno.SmartDeno();
|
||||
expect(testSmartdeno).toBeInstanceOf(smartdeno.SmartDeno);
|
||||
expect(testSmartdeno.isRunning()).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should download deno', async () => {
|
||||
tap.test('should throw when executing before start', async () => {
|
||||
let threw = false;
|
||||
try {
|
||||
await testSmartdeno.executeScript('console.log("test")');
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
expect(e.message).toInclude('not started');
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should download deno and start', async () => {
|
||||
await testSmartdeno.start({
|
||||
forceLocalDeno: true
|
||||
});
|
||||
})
|
||||
expect(testSmartdeno.isRunning()).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should execute a script', async () => {
|
||||
await testSmartdeno.executeScript(`console.log("A script from deno!")`);
|
||||
})
|
||||
tap.test('should execute a script and return result', async () => {
|
||||
const result = await testSmartdeno.executeScript(`console.log("Hello from Deno!")`);
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toInclude('Hello from Deno!');
|
||||
});
|
||||
|
||||
tap.start()
|
||||
tap.test('should execute a script with permissions', async () => {
|
||||
const result = await testSmartdeno.executeScript(
|
||||
`console.log(Deno.env.get("HOME") || "no-home")`,
|
||||
{ permissions: ['env'] }
|
||||
);
|
||||
expect(result.exitCode).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should handle script errors gracefully', async () => {
|
||||
const result = await testSmartdeno.executeScript(`throw new Error("test error")`);
|
||||
expect(result.exitCode).not.toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('should stop the instance', async () => {
|
||||
await testSmartdeno.stop();
|
||||
expect(testSmartdeno.isRunning()).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should be idempotent on multiple stop calls', async () => {
|
||||
await testSmartdeno.stop();
|
||||
expect(testSmartdeno.isRunning()).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartdeno',
|
||||
version: '1.0.3',
|
||||
description: 'a module to run deno from node'
|
||||
version: '1.1.0',
|
||||
description: 'A module to run Deno scripts from Node.js, including functionalities for downloading Deno and executing Deno scripts.'
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ interface IAsset {
|
||||
}
|
||||
|
||||
export class DenoDownloader {
|
||||
private denoBinaryPath: string | null = null;
|
||||
|
||||
private async getDenoDownloadUrl(): Promise<string> {
|
||||
const osPlatform = platform(); // 'darwin', 'linux', 'win32', etc.
|
||||
const arch = process.arch; // 'x64', 'arm64', etc.
|
||||
const osPlatform = platform();
|
||||
const arch = process.arch;
|
||||
let osPart: string;
|
||||
|
||||
switch (osPlatform) {
|
||||
@@ -36,9 +38,12 @@ export class DenoDownloader {
|
||||
const archPart = arch === 'x64' ? 'x86_64' : 'aarch64';
|
||||
|
||||
const releasesResponse = await fetch('https://api.github.com/repos/denoland/deno/releases/latest');
|
||||
if (!releasesResponse.ok) {
|
||||
throw new Error(`Failed to fetch Deno releases: ${releasesResponse.statusText}`);
|
||||
}
|
||||
const release: IDenoRelease = await releasesResponse.json();
|
||||
|
||||
const executableName = `deno-${archPart}-${osPart}.zip`; // Adjust if naming convention changes
|
||||
const executableName = `deno-${archPart}-${osPart}.zip`;
|
||||
const asset = release.assets.find(a => a.name === executableName);
|
||||
|
||||
if (!asset) {
|
||||
@@ -57,24 +62,60 @@ export class DenoDownloader {
|
||||
await fs.writeFile(outputPath, Buffer.from(buffer));
|
||||
}
|
||||
|
||||
public async download(outputPath: string = './deno.zip'): Promise<void> {
|
||||
try {
|
||||
const url = await this.getDenoDownloadUrl();
|
||||
await this.downloadDeno(url, outputPath);
|
||||
console.log(`Deno downloaded successfully to ${outputPath}`);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading Deno: ${error.message}`);
|
||||
}
|
||||
if (await plugins.smartfile.fs.fileExists(plugins.path.join(paths.nogitDir, 'deno'))) {
|
||||
return;
|
||||
}
|
||||
const smartarchive = await plugins.smartarchive.SmartArchive.fromArchiveFile(outputPath);
|
||||
/**
|
||||
* Get the path to the Deno binary after download
|
||||
*/
|
||||
public getDenoBinaryPath(): string | null {
|
||||
return this.denoBinaryPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and extract Deno to the specified directory
|
||||
* @param outputPath Path where the deno.zip will be downloaded
|
||||
* @returns Path to the Deno binary
|
||||
*/
|
||||
public async download(outputPath: string = './deno.zip'): Promise<string> {
|
||||
const fsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||
const directory = plugins.path.dirname(outputPath);
|
||||
const denoBinaryPath = plugins.path.join(directory, platform() === 'win32' ? 'deno.exe' : 'deno');
|
||||
|
||||
// Check if Deno is already downloaded
|
||||
if (await fsInstance.file(denoBinaryPath).exists()) {
|
||||
console.log(`Deno already exists at ${denoBinaryPath}`);
|
||||
this.denoBinaryPath = denoBinaryPath;
|
||||
return denoBinaryPath;
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
await fsInstance.directory(directory).create();
|
||||
|
||||
// Download Deno
|
||||
const url = await this.getDenoDownloadUrl();
|
||||
await this.downloadDeno(url, outputPath);
|
||||
console.log(`Deno downloaded successfully to ${outputPath}`);
|
||||
|
||||
// Extract the archive
|
||||
console.log(`Extracting deno.zip to ${directory}`);
|
||||
await smartarchive.exportToFs(directory);
|
||||
const smartshellInstance = new plugins.smarthshell.Smartshell({
|
||||
executor: 'bash'
|
||||
});
|
||||
await smartshellInstance.exec(`(cd ${paths.nogitDir} && chmod +x deno)`);
|
||||
await plugins.smartarchive.SmartArchive.create()
|
||||
.file(outputPath)
|
||||
.extract(directory);
|
||||
|
||||
// Make the binary executable (Unix-like systems)
|
||||
if (platform() !== 'win32') {
|
||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash'
|
||||
});
|
||||
await smartshellInstance.exec(`chmod +x "${denoBinaryPath}"`);
|
||||
}
|
||||
|
||||
// Clean up the zip file
|
||||
try {
|
||||
await fsInstance.file(outputPath).delete();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
this.denoBinaryPath = denoBinaryPath;
|
||||
return denoBinaryPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,69 @@
|
||||
import type { ScriptServer } from './classes.scriptserver.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
/* This file contains logic to execute deno commands in an ephermal way */
|
||||
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) {
|
||||
constructor(scriptserverRef: ScriptServer, scriptArg: string, options: IDenoExecutionOptions = {}) {
|
||||
this.scriptserverRef = scriptserverRef;
|
||||
this.script = scriptArg;
|
||||
this.options = options;
|
||||
this.id = plugins.smartunique.shortId();
|
||||
}
|
||||
|
||||
public async execute() {
|
||||
this.scriptserverRef.executionMap.add(this);
|
||||
await this.scriptserverRef.smartshellInstance.exec(`deno run http://localhost:3210/getscript/${this.id}`)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
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;
|
||||
public smartshellInstance = new plugins.smarthshell.Smartshell({
|
||||
private port: number;
|
||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash'
|
||||
});
|
||||
|
||||
public executionMap = new plugins.lik.ObjectMap<DenoExecution>();
|
||||
|
||||
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: 3210,
|
||||
port: this.port,
|
||||
cors: true,
|
||||
});
|
||||
this.server.addRoute(
|
||||
@@ -20,12 +33,24 @@ export class ScriptServer {
|
||||
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() {}
|
||||
|
||||
public async stop() {
|
||||
if (this.server) {
|
||||
await this.server.stop();
|
||||
}
|
||||
this.executionMap.wipe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +2,102 @@ 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 } from './classes.denoexecution.js';
|
||||
import { DenoExecution, type TDenoPermission } from './classes.denoexecution.js';
|
||||
|
||||
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 {
|
||||
/**
|
||||
* Deno permissions to grant to the script
|
||||
*/
|
||||
permissions?: TDenoPermission[];
|
||||
}
|
||||
|
||||
export class SmartDeno {
|
||||
private denoDownloader = new DenoDownloader();
|
||||
private scriptServer = new ScriptServer();
|
||||
private scriptServer: ScriptServer;
|
||||
private denoBinaryPath: string | null = null;
|
||||
private isStarted = false;
|
||||
|
||||
public async start(optionsArg: {
|
||||
forceLocalDeno?: boolean;
|
||||
} = {}) {
|
||||
const denoAlreadyInPath = await plugins.smarthshell.which('deno', {
|
||||
nothrow: true
|
||||
});
|
||||
if (!denoAlreadyInPath || optionsArg.forceLocalDeno) {
|
||||
await this.denoDownloader.download(plugins.path.join(paths.nogitDir, 'deno.zip'));
|
||||
}
|
||||
await this.scriptServer.start();
|
||||
constructor() {
|
||||
this.scriptServer = new ScriptServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the smartdeno instance
|
||||
* Starts the SmartDeno instance
|
||||
* @param optionsArg Configuration options
|
||||
*/
|
||||
public async stop() {
|
||||
public async start(optionsArg: ISmartDenoOptions = {}): Promise<void> {
|
||||
if (this.isStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create script server with configured port
|
||||
this.scriptServer = new ScriptServer({ port: optionsArg.port });
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
await this.scriptServer.start();
|
||||
this.isStarted = true;
|
||||
}
|
||||
|
||||
public async executeScript(scriptArg: string) {
|
||||
const denoExecution = new DenoExecution(this.scriptServer, scriptArg);
|
||||
await denoExecution.execute();
|
||||
/**
|
||||
* Stops the SmartDeno instance and cleans up resources
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (!this.isStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.scriptServer.stop();
|
||||
this.isStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SmartDeno instance is running
|
||||
*/
|
||||
public isRunning(): boolean {
|
||||
return this.isStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 denoExecution = new DenoExecution(this.scriptServer, scriptArg, {
|
||||
permissions: options.permissions,
|
||||
denoBinaryPath: this.denoBinaryPath || undefined,
|
||||
});
|
||||
|
||||
return denoExecution.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export * from './classes.smartdeno.js';
|
||||
export type { TDenoPermission } from './classes.denoexecution.js';
|
||||
|
||||
@@ -15,16 +15,16 @@ export {
|
||||
// @push.rocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartarchive from '@push.rocks/smartarchive';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartfs from '@push.rocks/smartfs';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smarthshell from '@push.rocks/smartshell';
|
||||
import * as smartshell from '@push.rocks/smartshell';
|
||||
import * as smartunique from '@push.rocks/smartunique';
|
||||
|
||||
export {
|
||||
lik,
|
||||
smartarchive,
|
||||
smartfile,
|
||||
smartfs,
|
||||
smartpath,
|
||||
smarthshell,
|
||||
smartshell,
|
||||
smartunique,
|
||||
}
|
||||
Reference in New Issue
Block a user