Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "run-code"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "Run code from stdin with selected toolchains and temporary dependencies"
description = "Run code snippets with selected toolchains and temporary dependencies"
license = "MIT"
repository = "https://github.com/timzhong1024/run-code"
homepage = "https://github.com/timzhong1024/run-code"
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[简体中文](README_zh.md)

Run Python, TypeScript, JavaScript, Rust, Go, or C# snippets with a selected runtime/toolchain version and temporary dependencies in an isolated environment.
Run Python, TypeScript, JavaScript, Rust, Go, or C# snippets from stdin or a source file with a selected runtime/toolchain version and temporary dependencies in an isolated environment.

## Installation

Expand Down Expand Up @@ -38,6 +38,14 @@ GitHub Releases also provide standalone binaries for macOS, Linux, and Windows.

## Examples

### Source file

```bash
run-code node@20 snippet.ts -- first --verbose
```

The source file is read and its contents are copied into a newly created isolated template project before execution. `run-code` does not execute inside the source file's existing project, discover that project's dependencies, or copy sibling files. Add everything the snippet needs with `--package`; arguments after `--` are passed to the snippet.

### TypeScript

```bash
Expand Down Expand Up @@ -141,10 +149,13 @@ Report vulnerabilities privately through GitHub private vulnerability reporting.

```text
run-code [OPTIONS] TOOLCHAIN[@VERSION]
run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...]
run-code skill
```

- `TOOLCHAIN[@VERSION]`: Select a language and optional version. Supported toolchains are `python`, `node`, `rust`, `go`, and `dotnet`; `javascript` and `typescript` are aliases for `node`, while `csharp` and `cs` are aliases for `dotnet`.
- `FILE`: Read a source file and copy its contents into a new isolated template project. The file's existing project and sibling files are not used. When omitted, source code is read from stdin.
- `ARG`: Pass arguments after `--` to the snippet process. This also works with stdin input.
- `-p, --package SPEC`: Add a temporary dependency. Repeat the option to install multiple packages. Specs follow each ecosystem: Python uses `NAME==VERSION`; Node, Rust, Go, and .NET use `NAME@VERSION`. Rust also supports `NAME[@VERSION][FEATURE,...]`, such as `'tokio@1[full]'`.
- `--commonjs`: Run Node code as CommonJS. The default is ESM with top-level `await` support.
- `--clean`: Delete the generated project after execution. Without this option, the project is retained and its path appears in the displayed command.
Expand All @@ -153,4 +164,4 @@ run-code skill
- `-h, --help`: Show help.
- `-V, --version`: Show the version.

When the version is omitted, built-in defaults are used: Python 3.14, Node latest, Rust stable, Go latest, and .NET 10. C# runs as a .NET 10+ file-based app. Python and Node execute directly without creating a project when no `--package` option is provided; an isolated project is created only when dependencies are needed. Source code is read from stdin, and package-manager download caches remain enabled. By default, `run-code` displays only dependency installation and final execution commands while streaming their stdout/stderr; project initialization output is shown only when initialization fails.
When the version is omitted, built-in defaults are used: Python 3.14, Node latest, Rust stable, Go latest, and .NET 10. C# runs as a .NET 10+ file-based app. For stdin input, Python and Node execute directly when no `--package` option is provided. File input always creates an isolated template project, even without dependencies. Package-manager download caches remain enabled. By default, `run-code` displays only dependency installation and final execution commands while streaming their stdout/stderr; project initialization output is shown only when initialization fails.
15 changes: 13 additions & 2 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[English](README.md)

在隔离的临时环境中,用指定版本的 runtime/toolchain 和依赖快速运行一段 Python、TypeScript、JavaScript、Rust、Go 或 C# 代码。
在隔离的临时环境中,用指定版本的 runtime/toolchain 和依赖快速运行来自 stdin 或源文件的一段 Python、TypeScript、JavaScript、Rust、Go 或 C# 代码。

## 安装

Expand Down Expand Up @@ -38,6 +38,14 @@ GitHub Release 还会提供 macOS、Linux 和 Windows 的独立 binary。

## 示例

### 源文件

```bash
run-code node@20 snippet.ts -- first --verbose
```

执行前,`run-code` 会读取源文件,并将内容复制到新建的隔离模板项目中。它不会在源文件所属的已有工程里运行,不会读取该工程的依赖,也不会复制同目录的其他文件。代码片段需要的依赖应通过 `--package` 明确添加;`--` 后的参数会传给代码进程。

### TypeScript

```bash
Expand Down Expand Up @@ -141,10 +149,13 @@ Codex 会自动发现这些目录中的 skill;详细约定见 [Codex Skills

```text
run-code [OPTIONS] TOOLCHAIN[@VERSION]
run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...]
run-code skill
```

- `TOOLCHAIN[@VERSION]`:选择语言及版本。支持 `python`、`node`、`rust`、`go` 和 `dotnet`;`javascript`、`typescript` 是 `node` 的别名,`csharp`、`cs` 是 `dotnet` 的别名,版本可以省略。
- `FILE`:读取源文件,并将内容复制进新的隔离模板项目;不会使用文件所在的已有工程或同目录文件。省略时从 stdin 读取代码。
- `ARG`:在 `--` 后提供并传给代码进程;stdin 输入同样可以传参。
- `-p, --package SPEC`:添加临时依赖,可重复使用以安装多个包。依赖格式遵循对应生态;Python 版本使用 `NAME==VERSION`,Node、Rust、Go 和 .NET 使用 `NAME@VERSION`。Rust 还支持 `NAME[@VERSION][FEATURE,...]`,例如 `'tokio@1[full]'`。
- `--commonjs`:让 Node 以 CommonJS 方式运行;默认使用支持顶层 `await` 的 ESM。
- `--clean`:运行结束后删除临时项目;不指定时保留项目,默认输出的执行命令中会包含项目路径。
Expand All @@ -153,4 +164,4 @@ run-code skill
- `-h, --help`:显示帮助。
- `-V, --version`:显示版本。

未指定版本时使用内置默认值:Python 3.14、Node latest、Rust stable、Go latest、.NET 10。C# 通过 .NET 10+ file-based app 运行。Python 和 Node 未指定 `--package` 时直接执行,不创建临时项目;需要依赖时才创建隔离项目。代码通过 stdin 输入,包管理器的下载缓存保持启用。默认只显示依赖安装和最终代码运行命令,并实时透传它们的 stdout/stderr;项目初始化仅在失败时输出诊断信息。
未指定版本时使用内置默认值:Python 3.14、Node latest、Rust stable、Go latest、.NET 10。C# 通过 .NET 10+ file-based app 运行。使用 stdin 且未指定 `--package` 时,Python 和 Node 直接执行;使用文件输入时,即使没有依赖也始终创建隔离模板项目。包管理器的下载缓存保持启用。默认只显示依赖安装和最终代码运行命令,并实时透传它们的 stdout/stderr;项目初始化仅在失败时输出诊断信息。
6 changes: 6 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ Prebuilt npm distribution of [`run-code`](https://github.com/timzhong1024/run-co
echo 'print("hello")' | npx @timzhong2000/run-code python
```

Or copy a source snippet into an isolated template project and pass arguments:

```bash
npx @timzhong2000/run-code node@20 snippet.ts -- first --verbose
```

The package selects the bundled binary for the current operating system and CPU architecture.
12 changes: 11 additions & 1 deletion skills/run-code-snippet/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ CODE
LANG
```

也可以读取已有代码片段文件,并在 `--` 后传入参数:

```bash
run-code TOOLCHAIN[@VERSION] [--package SPEC ...] [--commonjs] [--clean] [--quiet] FILE [-- ARG ...]
```

文件模式只读取 `FILE` 的内容,再将其复制进新建的隔离模板项目运行;不会在 `FILE` 所属的已有工程中执行,不会读取该工程的依赖,也不会复制相邻文件。缺少的依赖仍用 `--package` 明确添加。

Fish 不支持 heredoc;用 `printf` 将每行代码送入 stdin:

```fish
Expand All @@ -30,11 +38,13 @@ CODE
'@ | run-code TOOLCHAIN[@VERSION] [--package SPEC ...] [--commonjs] [--clean] [--quiet]
```

使用 `python`、`node`、`rust`、`go` 或 `dotnet`;`javascript` 和 `typescript` 是 `node` 的别名,`csharp` 和 `cs` 是 `dotnet` 的别名。省略版本时使用内置的最新稳定版本。Python 依赖版本使用 `NAME==VERSION`;Rust 依赖可用 `NAME[@VERSION][FEATURE,...]` 指定 Cargo features;.NET 依赖使用 NuGet 的 `NAME[@VERSION]`。Node 默认以 ESM 统一运行 JavaScript/TypeScript,仅在代码明确依赖 CommonJS 时添加 `--commonjs`。C# 使用 .NET 10+ file-based app。Python 和 Node 未指定 `--package` 时直接执行且不创建项目;指定依赖时才创建隔离项目。需要项目时默认保留;仅在明确只需一次结果时添加 `--clean`,仅需代码本身的输出时添加 `--quiet`。直接调用工具,不自行创建项目或检查运行环境。
使用 `python`、`node`、`rust`、`go` 或 `dotnet`;`javascript` 和 `typescript` 是 `node` 的别名,`csharp` 和 `cs` 是 `dotnet` 的别名。省略版本时使用内置的最新稳定版本。Python 依赖版本使用 `NAME==VERSION`;Rust 依赖可用 `NAME[@VERSION][FEATURE,...]` 指定 Cargo features;.NET 依赖使用 NuGet 的 `NAME[@VERSION]`。Node 默认以 ESM 统一运行 JavaScript/TypeScript,仅在代码明确依赖 CommonJS 时添加 `--commonjs`。C# 使用 .NET 10+ file-based app。stdin 模式下,Python 和 Node 未指定 `--package` 时直接执行;文件模式始终创建隔离模板项目。需要项目时默认保留;仅在明确只需一次结果时添加 `--clean`,仅需代码本身的输出时添加 `--quiet`。直接调用工具,不自行创建项目或检查运行环境。

## Examples

```bash
run-code node@20 snippet.ts -- first --verbose

run-code node@20 --package zod@4 --clean <<'TS'
import { z } from "zod";
console.log(z.object({ id: z.number() }).parse({ id: 1 }));
Expand Down
31 changes: 29 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[cfg(test)]
use clap::CommandFactory;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::str::FromStr;

pub const DEFAULT_PYTHON_TOOLCHAIN: &str = "3.14";
Expand Down Expand Up @@ -82,7 +83,7 @@ impl FromStr for ToolchainSpec {
#[command(
name = "run-code",
version,
about = "Run stdin with a selected toolchain and temporary dependencies"
about = "Run a code snippet with a selected toolchain and temporary dependencies"
)]
pub struct Cli {
#[command(subcommand)]
Expand All @@ -92,6 +93,14 @@ pub struct Cli {
#[arg(value_name = "TOOLCHAIN[@VERSION]")]
pub toolchain: Option<ToolchainSpec>,

/// Source file to copy into an isolated template project; reads stdin when omitted
#[arg(value_name = "FILE")]
pub source: Option<PathBuf>,

/// Arguments passed to the code snippet
#[arg(last = true, value_name = "ARG")]
pub args: Vec<String>,

/// Dependency specification; Python supports NAME==VERSION
#[arg(short = 'p', long = "package", value_name = "SPEC")]
pub packages: Vec<String>,
Expand Down Expand Up @@ -129,6 +138,8 @@ impl Cli {
|| self.commonjs
|| self.clean
|| self.quiet
|| self.source.is_some()
|| !self.args.is_empty()
{
Some("the skill command does not accept execution arguments".into())
} else {
Expand All @@ -153,7 +164,7 @@ mod tests {
.get_arguments()
.filter(|arg| arg.get_id() != "help" && arg.get_id() != "version")
.count();
assert_eq!(visible, 5);
assert_eq!(visible, 7);
}

#[test]
Expand All @@ -163,6 +174,22 @@ mod tests {
assert!(cli.commonjs);
}

#[test]
fn source_file_and_trailing_arguments_are_distinct() {
let cli =
Cli::try_parse_from(["run-code", "node@20", "snippet.ts", "--", "first", "--flag"])
.unwrap();
assert_eq!(cli.source, Some(PathBuf::from("snippet.ts")));
assert_eq!(cli.args, ["first", "--flag"]);
}

#[test]
fn stdin_can_also_receive_trailing_arguments() {
let cli = Cli::try_parse_from(["run-code", "python", "--", "first"]).unwrap();
assert!(cli.source.is_none());
assert_eq!(cli.args, ["first"]);
}

#[test]
fn skill_is_a_command_without_a_toolchain() {
let cli = Cli::try_parse_from(["run-code", "skill"]).unwrap();
Expand Down
17 changes: 11 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ mod cli;
mod process;
mod runner;
mod skill;
mod source;
mod util;

use clap::{Parser, error::ErrorKind};
use cli::{Cli, Command};
use std::io::{self, Read};

fn main() {
let cli = match Cli::try_parse() {
Expand All @@ -28,12 +28,17 @@ fn main() {
return;
}

let mut code = String::new();
if let Err(error) = io::stdin().read_to_string(&mut code) {
return exit_with_error(&format!("failed to read stdin: {error}"), 1);
}
let code = match source::read(cli.source.as_deref()) {
Ok(code) => code,
Err(error) => return exit_with_error(&error, 1),
};
if code.is_empty() {
return exit_with_error("stdin did not contain source code", 2);
let input = if cli.source.is_some() {
"source file"
} else {
"stdin"
};
return exit_with_error(&format!("{input} did not contain source code"), 2);
}

match runner::run_snippet(&cli, &code) {
Expand Down
12 changes: 11 additions & 1 deletion src/runner/dotnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,23 @@ impl<'a> DotnetBackend<'a> {
}

impl Backend for DotnetBackend<'_> {
fn prepare(&self, dir: &Path, code: &str, quiet: bool) -> Result<i32, RunFailure> {
fn prepare(
&self,
dir: &Path,
code: &str,
arguments: &[String],
quiet: bool,
) -> Result<i32, RunFailure> {
let source = source_with_packages(code, self.packages)?;
write_source(&dir.join("snippet.cs"), &source)?;

let toolchain = format!("dotnet@{}", self.toolchain.version);
let mut args = vec!["exec".into(), toolchain];
args.extend(strings(&["--", "dotnet", "run", "snippet.cs"]));
if !arguments.is_empty() {
args.push("--".into());
args.extend(arguments.iter().cloned());
}
let result = run_final("mise", &args, Some(dir), &[], quiet)?;
Ok(result.exit_code.unwrap_or(1))
}
Expand Down
9 changes: 8 additions & 1 deletion src/runner/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ impl<'a> GoBackend<'a> {
}
}
impl Backend for GoBackend<'_> {
fn prepare(&self, dir: &Path, code: &str, quiet: bool) -> Result<i32, RunFailure> {
fn prepare(
&self,
dir: &Path,
code: &str,
arguments: &[String],
quiet: bool,
) -> Result<i32, RunFailure> {
let toolchain = format!("go@{}", self.toolchain.version);
let env = [("GOWORK".into(), "off".into())];
run_checked_hidden(
Expand Down Expand Up @@ -52,6 +58,7 @@ impl Backend for GoBackend<'_> {
}
let mut args = vec!["exec".into(), toolchain.clone()];
args.extend(strings(&["--", "go", "run", "."]));
args.extend(arguments.iter().cloned());
let result = run_final("mise", &args, Some(dir), &env, quiet)?;
Ok(result.exit_code.unwrap_or(1))
}
Expand Down
42 changes: 37 additions & 5 deletions src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,23 @@ trait Backend {
fn runs_without_project(&self) -> bool {
false
}
fn run_direct(&self, _code: &str, _quiet: bool) -> Result<i32, RunFailure> {
fn run_direct(
&self,
_code: &str,
_arguments: &[String],
_quiet: bool,
) -> Result<i32, RunFailure> {
Err(RunFailure::message(
"this backend does not support direct execution",
))
}
fn prepare(&self, project_dir: &Path, code: &str, quiet: bool) -> Result<i32, RunFailure>;
fn prepare(
&self,
project_dir: &Path,
code: &str,
arguments: &[String],
quiet: bool,
) -> Result<i32, RunFailure>;
}

fn backend_for(cli: &Cli) -> Result<Box<dyn Backend + '_>, String> {
Expand Down Expand Up @@ -74,8 +85,12 @@ pub fn run_snippet(cli: &Cli, code: &str) -> Result<i32, RunError> {
hint: None,
exit_code: Some(2),
})?;
if backend.runs_without_project() {
return backend.run_direct(code, cli.quiet).map_err(Into::into);
// File input is always copied into an isolated template project. It never
// runs in, or discovers dependencies from, the source file's project.
if should_run_direct(cli, backend.as_ref()) {
return backend
.run_direct(code, &cli.args, cli.quiet)
.map_err(Into::into);
}

let temp_dir = Builder::new()
Expand All @@ -93,10 +108,14 @@ pub fn run_snippet(cli: &Cli, code: &str) -> Result<i32, RunError> {
};

backend
.prepare(&project_dir, code, cli.quiet)
.prepare(&project_dir, code, &cli.args, cli.quiet)
.map_err(Into::into)
}

fn should_run_direct(cli: &Cli, backend: &dyn Backend) -> bool {
cli.source.is_none() && backend.runs_without_project()
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -119,4 +138,17 @@ mod tests {
assert!(!backend_for(&with_package).unwrap().runs_without_project());
}
}

#[test]
fn source_files_force_an_isolated_template_project() {
for toolchain in ["python", "node"] {
let stdin = Cli::try_parse_from(["run-code", toolchain]).unwrap();
let stdin_backend = backend_for(&stdin).unwrap();
assert!(should_run_direct(&stdin, stdin_backend.as_ref()));

let cli = Cli::try_parse_from(["run-code", toolchain, "snippet.txt"]).unwrap();
let backend = backend_for(&cli).unwrap();
assert!(!should_run_direct(&cli, backend.as_ref()));
}
}
}
Loading