From 2879485a2480ba66d54b7bdbd3d4d0814b0a72d7 Mon Sep 17 00:00:00 2001 From: timzhong2000 Date: Sat, 22 Aug 2026 23:37:03 +0800 Subject: [PATCH 1/3] refactor: unify temporary project launch paths --- Cargo.lock | 61 --------------------------------------- Cargo.toml | 1 - src/runner/dotnet.rs | 6 +--- src/runner/node.rs | 69 +++++++++----------------------------------- src/runner/python.rs | 6 ++-- src/runner/rust.rs | 6 ++-- 6 files changed, 18 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec11a06..3837194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,12 +187,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - [[package]] name = "libc" version = "0.2.189" @@ -214,12 +208,6 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - [[package]] name = "once_cell" version = "1.21.4" @@ -280,7 +268,6 @@ dependencies = [ "clap", "directories", "dotenvy", - "serde_json", "tempfile", ] @@ -297,48 +284,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - [[package]] name = "strsim" version = "0.11.1" @@ -421,9 +366,3 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 62c6a54..a3749ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,4 @@ exclude = [".github", "npm", "scripts"] clap = { version = "4", features = ["derive"] } directories = "6" dotenvy = "0.15.7" -serde_json = "1" tempfile = "3" diff --git a/src/runner/dotnet.rs b/src/runner/dotnet.rs index 0287904..efb44fc 100644 --- a/src/runner/dotnet.rs +++ b/src/runner/dotnet.rs @@ -35,11 +35,7 @@ impl Backend for DotnetBackend<'_> { let toolchain = format!("dotnet@{}", self.toolchain.version); let mut args = vec!["exec".into(), toolchain]; args.extend(strings(&["--", "dotnet", "run"])); - args.push(if execution.has_custom_cwd() { - path_text(&source_path) - } else { - "snippet.cs".into() - }); + args.push(path_text(&source_path)); if !arguments.is_empty() { args.push("--".into()); args.extend(arguments.iter().cloned()); diff --git a/src/runner/node.rs b/src/runner/node.rs index 2216c9c..71de614 100644 --- a/src/runner/node.rs +++ b/src/runner/node.rs @@ -1,14 +1,19 @@ use super::Backend; use crate::cli::ToolchainSpec; use crate::execution::ExecutionContext; -use crate::process::{RunFailure, run_checked, run_checked_hidden, run_final}; +use crate::process::{RunFailure, run_checked, run_final}; use crate::util::{path_text, strings, write_source}; -use serde_json::json; use std::fs; use std::path::Path; use tempfile::Builder; const TSX_VERSION: &str = "4.23.12"; +const PACKAGE_JSON: &str = r#"{ + "name": "run-code-snippet", + "private": true, + "type": "module" +} +"#; pub struct NodeBackend<'a> { toolchain: &'a ToolchainSpec, @@ -88,34 +93,12 @@ impl Backend for NodeBackend<'_> { quiet: bool, ) -> Result { let file = self.file_name(); - let manifest = serde_json::to_string_pretty(&json!({ - "name": "run-code-snippet", - "private": true, - "type": "module", - "scripts": { "start": format!("tsx {file}") } - })) - .map_err(|e| RunFailure::message(format!("failed to build package.json: {e}")))?; - fs::write(dir.join("package.json"), format!("{manifest}\n")) + fs::write(dir.join("package.json"), PACKAGE_JSON) .map_err(|e| RunFailure::message(format!("failed to write package.json: {e}")))?; let source = dir.join(file); write_source(&source, code)?; let version = &self.toolchain.version; - run_checked_hidden( - "pin Node.js toolchain", - "vp", - &[ - "env".into(), - "pin".into(), - version.clone(), - "--target".into(), - "node-version".into(), - "--force".into(), - ], - Some(dir), - &[], - )?; - let mut install = vec![ "env".into(), "exec".into(), @@ -136,11 +119,7 @@ impl Backend for NodeBackend<'_> { quiet, )?; - let final_args = if execution.has_custom_cwd() { - final_command_from(dir, &source, version, arguments) - } else { - final_command(file, arguments, quiet) - }; + let final_args = final_command(dir, &source, version, arguments); let result = run_final( "vp", &final_args, @@ -153,7 +132,7 @@ impl Backend for NodeBackend<'_> { } } -fn final_command_from( +fn final_command( project_dir: &Path, source: &Path, version: &str, @@ -169,38 +148,15 @@ fn final_command_from( command } -fn final_command(file: &str, arguments: &[String], quiet: bool) -> Vec { - let mut command = if quiet { - strings(&["exec", "--", "tsx", file]) - } else { - strings(&["run", "start"]) - }; - command.extend(arguments.iter().cloned()); - command -} - #[cfg(test)] mod tests { use super::*; #[test] - fn snippet_arguments_do_not_include_the_cli_separator() { - let arguments = vec!["first".into(), "--flag".into()]; - assert_eq!( - final_command("snippet.ts", &arguments, false), - ["run", "start", "first", "--flag"] - ); - assert_eq!( - final_command("snippet.ts", &arguments, true), - ["exec", "--", "tsx", "snippet.ts", "first", "--flag"] - ); - } - - #[test] - fn custom_cwd_command_uses_absolute_project_tools_and_source() { + fn project_command_uses_explicit_toolchain_and_absolute_paths() { let project = Path::new("template"); let source = project.join("snippet.ts"); - let command = final_command_from(project, &source, "20", &["first".into()]); + let command = final_command(project, &source, "20", &["first".into(), "--flag".into()]); assert_eq!(&command[..5], ["env", "exec", "--node", "20", "node"]); assert_eq!( command[5], @@ -208,5 +164,6 @@ mod tests { ); assert_eq!(command[6], path_text(&source)); assert_eq!(command[7], "first"); + assert_eq!(command[8], "--flag"); } } diff --git a/src/runner/python.rs b/src/runner/python.rs index f25f03c..9a30a2f 100644 --- a/src/runner/python.rs +++ b/src/runner/python.rs @@ -90,10 +90,8 @@ impl Backend for PythonBackend<'_> { if quiet { args.push("--quiet".into()); } - if execution.has_custom_cwd() { - args.extend(strings(&["--project"])); - args.push(path_text(dir)); - } + args.extend(strings(&["--project"])); + args.push(path_text(dir)); args.extend(strings(&["--managed-python", "--python"])); args.push(version.clone()); args.push("python".into()); diff --git a/src/runner/rust.rs b/src/runner/rust.rs index 841fdbb..9009110 100644 --- a/src/runner/rust.rs +++ b/src/runner/rust.rs @@ -69,10 +69,8 @@ impl Backend for RustBackend<'_> { let mut args = strings(&["run", "--install"]); args.push(version.clone()); args.extend(strings(&["cargo", "run", "--quiet"])); - if execution.has_custom_cwd() { - args.extend(strings(&["--manifest-path"])); - args.push(path_text(&dir.join("Cargo.toml"))); - } + args.extend(strings(&["--manifest-path"])); + args.push(path_text(&dir.join("Cargo.toml"))); if !arguments.is_empty() { args.push("--".into()); args.extend(arguments.iter().cloned()); From c3ac3848f187030d2e0aef15f0323fecc39d892b Mon Sep 17 00:00:00 2001 From: timzhong2000 Date: Sat, 22 Aug 2026 23:53:17 +0800 Subject: [PATCH 2/3] refactor: collapse internal execution representations --- src/cli.rs | 78 +++++++++++++------------------------ src/execution.rs | 1 - src/main.rs | 8 +++- src/process.rs | 52 +++++++++---------------- src/runner/dotnet.rs | 20 +++------- src/runner/go.rs | 23 ++++------- src/runner/mod.rs | 91 ++++++++++++++------------------------------ src/runner/node.rs | 27 +++++-------- src/runner/python.rs | 24 ++++-------- src/runner/rust.rs | 19 +++------ 10 files changed, 114 insertions(+), 229 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 89c93d0..fd8a33d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,29 +19,12 @@ pub enum ToolchainKind { Dotnet, } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct ToolchainSpec { pub kind: ToolchainKind, pub version: String, } -#[cfg(test)] -impl ToolchainSpec { - pub fn name(&self) -> &'static str { - match self.kind { - ToolchainKind::Python => "python", - ToolchainKind::Node => "node", - ToolchainKind::Rust => "rust", - ToolchainKind::Go => "go", - ToolchainKind::Dotnet => "dotnet", - } - } - - pub fn display(&self) -> String { - format!("{}@{}", self.name(), self.version) - } -} - impl FromStr for ToolchainSpec { type Err = String; @@ -79,7 +62,7 @@ impl FromStr for ToolchainSpec { } } -#[derive(Debug, Parser)] +#[derive(Parser)] #[command( name = "run-code", version, @@ -126,7 +109,7 @@ pub struct Cli { pub quiet: bool, } -#[derive(Debug, Subcommand)] +#[derive(Subcommand)] pub enum Command { /// Print the installable Codex skill to stdout Skill, @@ -157,6 +140,8 @@ impl Cli { } } else if self.toolchain.is_none() { Some("missing required TOOLCHAIN[@VERSION] or the skill command".into()) + } else if self.commonjs && self.toolchain().kind != ToolchainKind::Node { + Some("--commonjs is only valid with the node toolchain".into()) } else { None } @@ -167,6 +152,12 @@ impl Cli { mod tests { use super::*; + fn assert_toolchain(input: &str, kind: ToolchainKind, version: &str) { + let spec = input.parse::().unwrap(); + assert_eq!(spec.kind, kind); + assert_eq!(spec.version, version); + } + #[test] fn cli_surface_stays_small() { let command = Cli::command(); @@ -180,7 +171,8 @@ mod tests { #[test] fn positional_toolchain_accepts_a_version() { let cli = Cli::try_parse_from(["run-code", "node@20", "--commonjs"]).unwrap(); - assert_eq!(cli.toolchain().display(), "node@20"); + assert_eq!(cli.toolchain().kind, ToolchainKind::Node); + assert_eq!(cli.toolchain().version, "20"); assert!(cli.commonjs); } @@ -234,45 +226,29 @@ mod tests { #[test] fn omitted_versions_use_stable_policy() { - assert_eq!( - "python".parse::().unwrap().display(), - "python@3.14" - ); - assert_eq!( - "node".parse::().unwrap().display(), - "node@latest" - ); - assert_eq!( - "rust".parse::().unwrap().display(), - "rust@stable" - ); - assert_eq!( - "go".parse::().unwrap().display(), - "go@latest" - ); - assert_eq!( - "dotnet".parse::().unwrap().display(), - "dotnet@10" - ); + assert_toolchain("python", ToolchainKind::Python, "3.14"); + assert_toolchain("node", ToolchainKind::Node, "latest"); + assert_toolchain("rust", ToolchainKind::Rust, "stable"); + assert_toolchain("go", ToolchainKind::Go, "latest"); + assert_toolchain("dotnet", ToolchainKind::Dotnet, "10"); } #[test] fn csharp_aliases_select_dotnet() { - assert_eq!( - "csharp@10".parse::().unwrap().display(), - "dotnet@10" - ); - assert_eq!( - "cs".parse::().unwrap().display(), - "dotnet@10" - ); + assert_toolchain("csharp@10", ToolchainKind::Dotnet, "10"); + assert_toolchain("cs", ToolchainKind::Dotnet, "10"); } #[test] fn language_names_alias_to_node() { for alias in ["javascript", "typescript"] { - let spec = format!("{alias}@20").parse::().unwrap(); - assert_eq!(spec.display(), "node@20"); + assert_toolchain(&format!("{alias}@20"), ToolchainKind::Node, "20"); } } + + #[test] + fn commonjs_is_node_only() { + let cli = Cli::try_parse_from(["run-code", "python", "--commonjs"]).unwrap(); + assert!(cli.validation_error().is_some()); + } } diff --git a/src/execution.rs b/src/execution.rs index 09fea41..cc363b8 100644 --- a/src/execution.rs +++ b/src/execution.rs @@ -1,7 +1,6 @@ use crate::cli::Cli; use std::path::{Path, PathBuf}; -#[derive(Debug, Default)] pub struct ExecutionContext { working_directory: Option, environment: Vec<(String, String)>, diff --git a/src/main.rs b/src/main.rs index 18804c1..0e1e84e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,13 @@ fn main() { Ok(exit_code) => exit(exit_code), Err(error) => { eprintln!("run-code: {}", error.message); - if let Some(hint) = error.hint { + let hint = error.hint.or_else(|| { + error + .missing_program + .as_deref() + .and_then(util::program_install_hint) + }); + if let Some(hint) = hint { eprintln!("hint: {hint}"); } exit(error.exit_code.unwrap_or(1)); diff --git a/src/process.rs b/src/process.rs index 8b82085..894eaab 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,16 +1,11 @@ use std::io::{self, Write}; use std::path::Path; -use std::process::{Command, Output}; - -#[derive(Debug)] -pub struct ProcessResult { - pub success: bool, - pub exit_code: Option, -} +use std::process::{Command, ExitStatus, Output}; #[derive(Debug)] pub struct RunFailure { pub message: String, + pub hint: Option, pub exit_code: Option, pub missing_program: Option, } @@ -19,6 +14,7 @@ impl RunFailure { pub fn message(message: impl Into) -> Self { Self { message: message.into(), + hint: None, exit_code: None, missing_program: None, } @@ -27,15 +23,17 @@ impl RunFailure { fn start(program: &str, error: io::Error) -> Self { Self { message: format!("failed to start {program}: {error}"), + hint: None, exit_code: None, missing_program: (error.kind() == io::ErrorKind::NotFound).then(|| program.to_string()), } } - fn process(stage: &str, process: ProcessResult) -> Self { + fn process(stage: &str, exit_code: Option) -> Self { Self { message: format!("{stage} failed"), - exit_code: process.exit_code, + hint: None, + exit_code, missing_program: None, } } @@ -47,13 +45,8 @@ pub fn run_checked_hidden( args: &[String], cwd: Option<&Path>, env: &[(String, String)], -) -> Result { - let result = run_setup(program, args, cwd, env, true)?; - if result.success { - Ok(result) - } else { - Err(RunFailure::process(stage, result)) - } +) -> Result<(), RunFailure> { + run_checked(stage, program, args, cwd, env, true) } pub fn run_checked( @@ -63,12 +56,12 @@ pub fn run_checked( cwd: Option<&Path>, env: &[(String, String)], quiet: bool, -) -> Result { +) -> Result<(), RunFailure> { let result = run_setup(program, args, cwd, env, quiet)?; - if result.success { - Ok(result) + if result.success() { + Ok(()) } else { - Err(RunFailure::process(stage, result)) + Err(RunFailure::process(stage, result.code())) } } @@ -79,17 +72,14 @@ pub fn run_final( env: &[(String, String)], unprinted_env: &[(String, String)], quiet: bool, -) -> Result { +) -> Result { if !quiet { eprintln!("+ {}", display_command(program, args, cwd, env)); } let status = command(program, args, cwd, env, unprinted_env) .status() .map_err(|error| RunFailure::start(program, error))?; - Ok(ProcessResult { - success: status.success(), - exit_code: status.code(), - }) + Ok(status.code().unwrap_or(1)) } fn run_setup( @@ -98,16 +88,13 @@ fn run_setup( cwd: Option<&Path>, env: &[(String, String)], quiet: bool, -) -> Result { +) -> Result { if !quiet { eprintln!("+ {}", display_command(program, args, cwd, env)); let status = command(program, args, cwd, env, &[]) .status() .map_err(|error| RunFailure::start(program, error))?; - return Ok(ProcessResult { - success: status.success(), - exit_code: status.code(), - }); + return Ok(status); } let output = command(program, args, cwd, env, &[]) @@ -116,10 +103,7 @@ fn run_setup( if !output.status.success() { replay_output(&output); } - Ok(ProcessResult { - success: output.status.success(), - exit_code: output.status.code(), - }) + Ok(output.status) } fn command( diff --git a/src/runner/dotnet.rs b/src/runner/dotnet.rs index efb44fc..18e2cea 100644 --- a/src/runner/dotnet.rs +++ b/src/runner/dotnet.rs @@ -5,18 +5,9 @@ use crate::process::{RunFailure, run_final}; use crate::util::{path_text, strings, write_source}; use std::path::Path; -pub struct DotnetBackend<'a> { - toolchain: &'a ToolchainSpec, - packages: &'a [String], -} - -impl<'a> DotnetBackend<'a> { - pub fn new(toolchain: &'a ToolchainSpec, packages: &'a [String]) -> Self { - Self { - toolchain, - packages, - } - } +pub(super) struct DotnetBackend<'a> { + pub(super) toolchain: &'a ToolchainSpec, + pub(super) packages: &'a [String], } impl Backend for DotnetBackend<'_> { @@ -40,15 +31,14 @@ impl Backend for DotnetBackend<'_> { args.push("--".into()); args.extend(arguments.iter().cloned()); } - let result = run_final( + run_final( "mise", &args, Some(execution.cwd_or(dir)), &[], execution.environment(), quiet, - )?; - Ok(result.exit_code.unwrap_or(1)) + ) } } diff --git a/src/runner/go.rs b/src/runner/go.rs index 7496ddc..3781e34 100644 --- a/src/runner/go.rs +++ b/src/runner/go.rs @@ -5,17 +5,9 @@ use crate::process::{RunFailure, run_checked, run_checked_hidden, run_final}; use crate::util::{path_text, strings, write_source}; use std::path::{Path, PathBuf}; -pub struct GoBackend<'a> { - toolchain: &'a ToolchainSpec, - packages: &'a [String], -} -impl<'a> GoBackend<'a> { - pub fn new(toolchain: &'a ToolchainSpec, packages: &'a [String]) -> Self { - Self { - toolchain, - packages, - } - } +pub(super) struct GoBackend<'a> { + pub(super) toolchain: &'a ToolchainSpec, + pub(super) packages: &'a [String], } impl Backend for GoBackend<'_> { fn prepare( @@ -58,7 +50,7 @@ impl Backend for GoBackend<'_> { quiet, )?; } - let result = if execution.has_custom_cwd() { + if execution.has_custom_cwd() { let binary = snippet_binary(dir); let mut build = vec!["exec".into(), toolchain.clone()]; build.extend(strings(&["--", "go", "build", "-o"])); @@ -72,7 +64,7 @@ impl Backend for GoBackend<'_> { &[], execution.environment(), quiet, - )? + ) } else { let mut args = vec!["exec".into(), toolchain.clone()]; args.extend(strings(&["--", "go", "run", "."])); @@ -84,9 +76,8 @@ impl Backend for GoBackend<'_> { &env, execution.environment(), quiet, - )? - }; - Ok(result.exit_code.unwrap_or(1)) + ) + } } } diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 8cda569..a23a106 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -35,74 +35,49 @@ trait Backend { ) -> Result; } -fn backend_for(cli: &Cli) -> Result, String> { +fn backend_for(cli: &Cli) -> Box { let toolchain = cli.toolchain(); match toolchain.kind { - ToolchainKind::Python | ToolchainKind::Rust | ToolchainKind::Go | ToolchainKind::Dotnet - if cli.commonjs => - { - Err("--commonjs is only valid with the node toolchain".into()) - } - ToolchainKind::Python => Ok(Box::new(python::PythonBackend::new( + ToolchainKind::Python => Box::new(python::PythonBackend { toolchain, - &cli.packages, - ))), - ToolchainKind::Node => Ok(Box::new(node::NodeBackend::new( + packages: &cli.packages, + }), + ToolchainKind::Node => Box::new(node::NodeBackend { toolchain, - &cli.packages, - cli.commonjs, - ))), - ToolchainKind::Rust => Ok(Box::new(rust::RustBackend::new(toolchain, &cli.packages))), - ToolchainKind::Go => Ok(Box::new(go::GoBackend::new(toolchain, &cli.packages))), - ToolchainKind::Dotnet => Ok(Box::new(dotnet::DotnetBackend::new( + packages: &cli.packages, + commonjs: cli.commonjs, + }), + ToolchainKind::Rust => Box::new(rust::RustBackend { toolchain, - &cli.packages, - ))), - } -} - -#[derive(Debug)] -pub struct RunError { - pub message: String, - pub hint: Option, - pub exit_code: Option, -} - -impl From for RunError { - fn from(error: RunFailure) -> Self { - let hint = error - .missing_program - .as_deref() - .and_then(crate::util::program_install_hint); - Self { - message: error.message, - hint, - exit_code: error.exit_code, - } + packages: &cli.packages, + }), + ToolchainKind::Go => Box::new(go::GoBackend { + toolchain, + packages: &cli.packages, + }), + ToolchainKind::Dotnet => Box::new(dotnet::DotnetBackend { + toolchain, + packages: &cli.packages, + }), } } -pub fn run_snippet(cli: &Cli, execution: &ExecutionContext, code: &str) -> Result { - let backend = backend_for(cli).map_err(|message| RunError { - message, - hint: None, - exit_code: Some(2), - })?; +pub fn run_snippet(cli: &Cli, execution: &ExecutionContext, code: &str) -> Result { + let backend = backend_for(cli); // 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, execution, cli.quiet) - .map_err(Into::into); + return backend.run_direct(code, &cli.args, execution, cli.quiet); } let temp_dir = Builder::new() .prefix("run-code-") .tempdir() - .map_err(|error| RunError { + .map_err(|error| RunFailure { message: format!("failed to allocate a system temporary directory: {error}"), hint: Some("Check that the operating system temporary directory is writable.".into()), exit_code: None, + missing_program: None, })?; let (project_dir, _guard): (PathBuf, Option) = if cli.clean { (temp_dir.path().to_path_buf(), Some(temp_dir)) @@ -110,9 +85,7 @@ pub fn run_snippet(cli: &Cli, execution: &ExecutionContext, code: &str) -> Resul (temp_dir.keep(), None) }; - backend - .prepare(&project_dir, code, &cli.args, execution, cli.quiet) - .map_err(Into::into) + backend.prepare(&project_dir, code, &cli.args, execution, cli.quiet) } fn should_run_direct(cli: &Cli, backend: &dyn Backend) -> bool { @@ -124,21 +97,15 @@ mod tests { use super::*; use clap::Parser; - #[test] - fn commonjs_is_node_only() { - let cli = Cli::try_parse_from(["run-code", "python", "--commonjs"]).unwrap(); - assert!(backend_for(&cli).is_err()); - } - #[test] fn python_and_node_only_need_projects_for_user_packages() { for toolchain in ["python", "node"] { let direct = Cli::try_parse_from(["run-code", toolchain]).unwrap(); - assert!(backend_for(&direct).unwrap().runs_without_project()); + assert!(backend_for(&direct).runs_without_project()); let with_package = Cli::try_parse_from(["run-code", toolchain, "--package", "example"]).unwrap(); - assert!(!backend_for(&with_package).unwrap().runs_without_project()); + assert!(!backend_for(&with_package).runs_without_project()); } } @@ -146,11 +113,11 @@ mod tests { 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(); + let stdin_backend = backend_for(&stdin); 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(); + let backend = backend_for(&cli); assert!(!should_run_direct(&cli, backend.as_ref())); } } diff --git a/src/runner/node.rs b/src/runner/node.rs index 71de614..14d5ad8 100644 --- a/src/runner/node.rs +++ b/src/runner/node.rs @@ -15,19 +15,12 @@ const PACKAGE_JSON: &str = r#"{ } "#; -pub struct NodeBackend<'a> { - toolchain: &'a ToolchainSpec, - packages: &'a [String], - commonjs: bool, +pub(super) struct NodeBackend<'a> { + pub(super) toolchain: &'a ToolchainSpec, + pub(super) packages: &'a [String], + pub(super) commonjs: bool, } -impl<'a> NodeBackend<'a> { - pub fn new(toolchain: &'a ToolchainSpec, packages: &'a [String], commonjs: bool) -> Self { - Self { - toolchain, - packages, - commonjs, - } - } +impl NodeBackend<'_> { fn file_name(&self) -> &'static str { if self.commonjs { "snippet.cts" @@ -73,15 +66,14 @@ impl Backend for NodeBackend<'_> { ]; args.extend(arguments.iter().cloned()); let fallback = std::env::temp_dir(); - let result = run_final( + run_final( "vp", &args, Some(execution.cwd_or(&fallback)), &[], execution.environment(), quiet, - )?; - Ok(result.exit_code.unwrap_or(1)) + ) } fn prepare( @@ -120,15 +112,14 @@ impl Backend for NodeBackend<'_> { )?; let final_args = final_command(dir, &source, version, arguments); - let result = run_final( + run_final( "vp", &final_args, Some(execution.cwd_or(dir)), &[], execution.environment(), quiet, - )?; - Ok(result.exit_code.unwrap_or(1)) + ) } } diff --git a/src/runner/python.rs b/src/runner/python.rs index 9a30a2f..7fe8341 100644 --- a/src/runner/python.rs +++ b/src/runner/python.rs @@ -5,17 +5,9 @@ use crate::process::{RunFailure, run_checked, run_checked_hidden, run_final}; use crate::util::{path_text, strings, write_source}; use std::path::Path; -pub struct PythonBackend<'a> { - toolchain: &'a ToolchainSpec, - packages: &'a [String], -} -impl<'a> PythonBackend<'a> { - pub fn new(toolchain: &'a ToolchainSpec, packages: &'a [String]) -> Self { - Self { - toolchain, - packages, - } - } +pub(super) struct PythonBackend<'a> { + pub(super) toolchain: &'a ToolchainSpec, + pub(super) packages: &'a [String], } impl Backend for PythonBackend<'_> { @@ -37,15 +29,14 @@ impl Backend for PythonBackend<'_> { args.push(code.into()); args.extend(arguments.iter().cloned()); let fallback = std::env::temp_dir(); - let result = run_final( + run_final( "uv", &args, Some(execution.cwd_or(&fallback)), &[], execution.environment(), quiet, - )?; - Ok(result.exit_code.unwrap_or(1)) + ) } fn prepare( @@ -97,15 +88,14 @@ impl Backend for PythonBackend<'_> { args.push("python".into()); args.push(path_text(&source)); args.extend(arguments.iter().cloned()); - let result = run_final( + run_final( "uv", &args, Some(execution.cwd_or(dir)), &[], execution.environment(), quiet, - )?; - Ok(result.exit_code.unwrap_or(1)) + ) } } diff --git a/src/runner/rust.rs b/src/runner/rust.rs index 9009110..83ededa 100644 --- a/src/runner/rust.rs +++ b/src/runner/rust.rs @@ -7,17 +7,9 @@ use directories::ProjectDirs; use std::fs; use std::path::{Path, PathBuf}; -pub struct RustBackend<'a> { - toolchain: &'a ToolchainSpec, - packages: &'a [String], -} -impl<'a> RustBackend<'a> { - pub fn new(toolchain: &'a ToolchainSpec, packages: &'a [String]) -> Self { - Self { - toolchain, - packages, - } - } +pub(super) struct RustBackend<'a> { + pub(super) toolchain: &'a ToolchainSpec, + pub(super) packages: &'a [String], } impl Backend for RustBackend<'_> { fn prepare( @@ -75,15 +67,14 @@ impl Backend for RustBackend<'_> { args.push("--".into()); args.extend(arguments.iter().cloned()); } - let result = run_final( + run_final( "rustup", &args, Some(execution.cwd_or(dir)), &env, execution.environment(), quiet, - )?; - Ok(result.exit_code.unwrap_or(1)) + ) } } From 3e0516effb6f38c56daf1192f0e6a8dc4ae992ae Mon Sep 17 00:00:00 2001 From: timzhong2000 Date: Sun, 23 Aug 2026 00:02:19 +0800 Subject: [PATCH 3/3] docs: make project purpose and usage easier to scan --- README.md | 200 ++++++++++++++++++++++++++------------------------- README_zh.md | 200 ++++++++++++++++++++++++++------------------------- 2 files changed, 202 insertions(+), 198 deletions(-) diff --git a/README.md b/README.md index a809b67..a15257d 100644 --- a/README.md +++ b/README.md @@ -2,68 +2,95 @@ [简体中文](README_zh.md) -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. +Run a disposable Python, TypeScript/JavaScript, Rust, Go, or C# snippet with a selected runtime version and temporary dependencies—without modifying your global environment or current project. -## Installation +```bash +echo 'print("hello")' | run-code python@3.14 +``` + +## Why run-code exists + +A small experiment should not require creating a project, choosing a package manager layout, switching the active runtime, installing dependencies, and deleting everything afterward. Installing a package globally or into the current project is faster initially, but leaves unrelated state behind. + +`run-code` turns the disposable case into one command: -Homebrew is recommended on macOS: +1. select a runtime or toolchain version; +2. prepare dependencies in an isolated temporary environment; +3. run code from stdin or a source file. + +Use it to try a package from its README, verify behavior on a specific runtime, reproduce a small example, or let an agent run a focused check. Use a normal project for multi-file programs, durable dependencies, or build configuration. `run-code` is isolation for convenience, not a security sandbox. + +## For agents + +After installing the binary, an agent can read its matching `run-code-snippet` skill and exact version-specific instructions by running: ```bash -brew install timzhong1024/tap/run-code +run-code skill ``` -You can also install the prebuilt binary through npm: +Install the skill into a project when its use should be repository-specific: ```bash -npm install --global @timzhong2000/run-code +mkdir -p .agents/skills/run-code-snippet +run-code skill > .agents/skills/run-code-snippet/SKILL.md ``` -Or install from source: +Or install it globally for reuse across projects: ```bash -cargo install --locked --git https://github.com/timzhong1024/run-code +mkdir -p ~/.agents/skills/run-code-snippet +run-code skill > ~/.agents/skills/run-code-snippet/SKILL.md ``` -GitHub Releases also provide standalone binaries for macOS, Linux, and Windows. +For a one-off check, the essential pattern is: -`run-code` delegates to external tools for each language. Install only the backends you use: +```bash +run-code TOOLCHAIN[@VERSION] [--package SPEC ...] [--clean] [--quiet] <<'LANG' +CODE +LANG +``` -| Language | Required tool | -| --- | --- | -| Python | [uv](https://docs.astral.sh/uv/getting-started/installation/) | -| TypeScript / JavaScript | [Vite+ (`vp`)](https://viteplus.dev/guide/) | -| Rust | [rustup](https://rustup.rs/) | -| Go | [mise](https://mise.jdx.dev/getting-started.html) | -| C# / .NET | [mise](https://mise.jdx.dev/getting-started.html) | +This gives the agent an explicit runtime, disposable dependencies, isolated project state, and normal stdout/stderr without asking it to scaffold a project manually. -## Examples +## Install -### Source file +On macOS: ```bash -run-code node@20 snippet.ts -- first --verbose +brew install timzhong1024/tap/run-code ``` -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. - -### Working directory and environment +Other options: ```bash -run-code node@20 --cwd ./fixtures --env-file ./snippet.env snippet.ts +npm install --global @timzhong2000/run-code +cargo install --locked --git https://github.com/timzhong1024/run-code ``` -`--cwd` changes the working directory seen by the final snippet process. Template initialization and dependency installation still happen inside the isolated temporary project. `--env-file` loads dotenv-compatible variables for the final launch and snippet process; it does not modify the current shell or earlier setup and dependency-installation steps, and loaded values are omitted from displayed commands. Both paths are resolved from the directory where `run-code` was invoked. +Standalone macOS, Linux, and Windows binaries are available from [GitHub Releases](https://github.com/timzhong1024/run-code/releases). + +`run-code` delegates runtime installation to existing tools. Install only the backends you use: -### TypeScript +| Code | Toolchain argument | Required backend | +| --- | --- | --- | +| Python | `python[@VERSION]` | [uv](https://docs.astral.sh/uv/getting-started/installation/) | +| TypeScript / JavaScript | `node[@VERSION]` | [Vite+ (`vp`)](https://viteplus.dev/guide/) | +| Rust | `rust[@VERSION]` | [rustup](https://rustup.rs/) | +| Go | `go[@VERSION]` | [mise](https://mise.jdx.dev/getting-started.html) | +| C# | `dotnet[@VERSION]` | [mise](https://mise.jdx.dev/getting-started.html) | + +## Quick examples + +### TypeScript with an npm package ```bash run-code node@20 --package zod@4 --clean <<'TS' import { z } from "zod"; -console.log(await Promise.resolve(z.string().parse("hello"))); +console.log(await Promise.resolve(z.object({ id: z.number() }).parse({ id: 1 }))); TS ``` -### Python +### Python with a PyPI package ```bash run-code python@3.14 --package requests==2.32.5 --clean <<'PY' @@ -72,7 +99,7 @@ print(requests.__version__) PY ``` -### Rust +### Rust with a crate ```bash run-code rust@stable --package serde_json@1 --clean <<'RS' @@ -82,35 +109,53 @@ fn main() { RS ``` -### C# +### Source files, arguments, working directory, and environment ```bash -run-code dotnet@10 --package Spectre.Console@0.50.0 --clean <<'CS' -using Spectre.Console; -AnsiConsole.MarkupLine("[green]Hello from C#[/]"); -CS +run-code node@20 \ + --package zod@4 \ + --cwd ./fixtures \ + --env-file ./snippet.env \ + snippet.ts -- first --verbose ``` -For asynchronous Rust, specify Cargo features in the dependency spec: +The file is copied into a fresh isolated template; its existing project, dependencies, and sibling files are not used. Arguments after `--` go to the snippet. `--cwd` affects only the final code process, while `--env-file` supplies dotenv-compatible variables without changing the current shell or exposing their values in the displayed command. -```bash -run-code rust@stable --package 'tokio@1[full]' --clean <<'RS' -#[tokio::main] -async fn main() { - println!("async"); -} -RS -``` +## Execution behavior -In Windows PowerShell 7, use a single-quoted here-string: +- Omitting a version selects the built-in stable policy: Python 3.14, Node latest, Rust stable, Go latest, and .NET 10. +- Python and Node stdin snippets without `--package` run directly; dependencies or file input use an isolated template project. +- Temporary projects are retained by default so their generated command paths remain inspectable. Add `--clean` for a one-off run. +- Package-manager download caches remain enabled, so isolation does not mean downloading every package again. +- By default, dependency installation and the final command are displayed and their output is streamed. `--quiet` leaves only the final process stdout/stderr. +- Node defaults to ESM with TypeScript and top-level `await` support. Use `--commonjs` only for CommonJS-specific code. -```powershell -@' -print("hello\nworld") -'@ | run-code python@3.14 --clean +## CLI reference + +```text +run-code [OPTIONS] TOOLCHAIN[@VERSION] +run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...] +run-code skill ``` -Fish does not support heredocs, so use `printf`: +| Argument | Meaning | +| --- | --- | +| `TOOLCHAIN[@VERSION]` | `python`, `node`, `rust`, `go`, or `dotnet`; `javascript`/`typescript` alias `node`, and `csharp`/`cs` alias `dotnet` | +| `FILE` | Copy and run one source file instead of reading stdin | +| `-- ARG ...` | Pass trailing arguments to the snippet | +| `-p, --package SPEC` | Add a dependency; repeat for multiple packages | +| `--cwd DIR` | Set the final snippet process working directory | +| `--env-file FILE` | Load dotenv variables for the final process | +| `--commonjs` | Run Node code as CommonJS instead of ESM | +| `--clean` | Remove the temporary project after execution | +| `--quiet` | Show only final-process stdout/stderr | +| `skill` | Print the bundled agent skill | + +Package specs follow their ecosystems. Python accepts `NAME==VERSION`; Node, Rust, Go, and .NET accept `NAME@VERSION`. Rust features use `NAME[@VERSION][FEATURE,...]`, for example `'tokio@1[full]'`. + +### Shell input + +Bash and Zsh use the quoted heredocs shown above. Fish can pipe `printf`: ```fish printf '%s\n' \ @@ -119,59 +164,16 @@ printf '%s\n' \ run-code node@20 ``` -## Agent Skill - -`run-code skill` prints the complete bundled `SKILL.md` from the installed binary. An agent can discover the skill by its name and description, then read the full instructions when the task matches. - -Install it in the current project: - -```bash -mkdir -p .agents/skills/run-code-snippet -run-code skill > .agents/skills/run-code-snippet/SKILL.md -``` - -Or install it in your user directory to make it available across projects: +PowerShell 7 can pipe a single-quoted here-string: -```bash -mkdir -p ~/.agents/skills/run-code-snippet -run-code skill > ~/.agents/skills/run-code-snippet/SKILL.md +```powershell +@' +print("hello\nworld") +'@ | run-code python@3.14 --clean ``` -Codex automatically discovers skills in these directories. See the [Codex Skills documentation](https://learn.chatgpt.com/docs/build-skills) for details. - -## Why this project exists - -Running a temporary snippet often means paying the setup cost of creating a project, installing dependencies, and preparing an environment. Switching to a different runtime or toolchain version for one task is also cumbersome, while installing packages globally or into an existing project creates unwanted state. - -Several related tools solve parts of this problem, but none matched the combination of temporary dependencies, version switching, and isolated execution needed here. Inspired by snippet runners, version managers, and temporary package executors, `run-code` combines those steps into one command. - ## Security -`run-code` provides environment and dependency isolation; it is not a security sandbox. Snippets and third-party dependencies run with the current user's permissions and may access local files, the network, environment variables, and credentials. Variables loaded with `--env-file` are deliberately available to the snippet, so do not pass secrets to untrusted code. - -Dependency installation may execute npm lifecycle scripts, Python build backends, Cargo `build.rs` scripts, or other ecosystem-specific build code. Run only trusted code and dependencies. Inspect unfamiliar packages before use, pin versions in sensitive environments, and avoid exposing unnecessary secrets. `--clean` removes only the temporary project; it cannot undo system or network side effects, and package-manager download caches remain in place. - -Report vulnerabilities privately through GitHub private vulnerability reporting. See [SECURITY.md](SECURITY.md) for scope and reporting instructions. - -## CLI reference - -```text -run-code [OPTIONS] TOOLCHAIN[@VERSION] -run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...] -run-code skill -``` +Snippets, dependencies, package lifecycle hooks, Python build backends, and Cargo build scripts run with the current user's permissions. They may access files, the network, environment variables, credentials, and other processes. Run only trusted code and packages; pin versions when reproducibility matters and do not pass secrets to untrusted snippets. -- `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]'`. -- `--cwd DIR`: Set the final snippet process's working directory. Template setup and dependency installation remain isolated from this directory. -- `--env-file FILE`: Load dotenv-compatible variables for the final launch and snippet process. Values override inherited variables with the same name and are not printed in displayed commands; runner-owned isolation variables take precedence. -- `--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. -- `--quiet`: Hide project setup, dependency installation, and command display; print only stdout/stderr from the final code process. -- `skill`: Print the bundled `run-code-snippet` 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. 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. +`--clean` removes the temporary project, but cannot undo filesystem or network side effects. See [SECURITY.md](SECURITY.md) for the reporting policy and complete security boundary. diff --git a/README_zh.md b/README_zh.md index feaa160..855f341 100644 --- a/README_zh.md +++ b/README_zh.md @@ -2,68 +2,95 @@ [English](README.md) -在隔离的临时环境中,用指定版本的 runtime/toolchain 和依赖快速运行来自 stdin 或源文件的一段 Python、TypeScript、JavaScript、Rust、Go 或 C# 代码。 +使用指定版本的 runtime/toolchain 和临时依赖,运行一段一次性的 Python、TypeScript/JavaScript、Rust、Go 或 C# 代码,同时不修改全局环境或当前项目。 -## 安装 +```bash +echo 'print("hello")' | run-code python@3.14 +``` + +## 为什么需要 run-code + +一个小实验不应该要求你创建项目、选择包管理布局、切换当前 runtime、安装依赖,最后再删除所有文件。把包直接安装到全局或当前项目虽然开始得快,却会留下与项目无关的状态。 -macOS 推荐使用 Homebrew: +`run-code` 把一次性运行收敛为一个命令: + +1. 选择 runtime 或 toolchain 版本; +2. 在隔离的临时环境中准备依赖; +3. 运行来自 stdin 或源文件的代码。 + +它适合试用 README 里看到的包、验证特定 runtime 的行为、复现小型示例,或者让 Agent 做一次聚焦检查。多文件程序、长期依赖和正式构建配置仍应使用普通项目。`run-code` 提供的是便利性的隔离,不是安全沙箱。 + +## 给 Agent 使用 + +安装 binary 后,Agent 可以用下面的命令读取内置的 `run-code-snippet` skill,以及与当前版本准确匹配的说明: ```bash -brew install timzhong1024/tap/run-code +run-code skill ``` -也可以通过 npm 安装预编译 binary: +如果只希望当前项目使用,将 skill 安装到项目: ```bash -npm install --global @timzhong2000/run-code +mkdir -p .agents/skills/run-code-snippet +run-code skill > .agents/skills/run-code-snippet/SKILL.md ``` -或者从源码安装: +需要跨项目复用时,安装到全局: ```bash -cargo install --locked --git https://github.com/timzhong1024/run-code +mkdir -p ~/.agents/skills/run-code-snippet +run-code skill > ~/.agents/skills/run-code-snippet/SKILL.md ``` -GitHub Release 还会提供 macOS、Linux 和 Windows 的独立 binary。 +一次性检查的核心调用方式是: -`run-code` 按语言调用外部工具;只需安装自己会使用的后端: +```bash +run-code TOOLCHAIN[@VERSION] [--package SPEC ...] [--clean] [--quiet] <<'LANG' +CODE +LANG +``` -| 语言 | 必需工具 | -| --- | --- | -| Python | [uv](https://docs.astral.sh/uv/getting-started/installation/) | -| TypeScript / JavaScript | [Vite+ (`vp`)](https://viteplus.dev/guide/) | -| Rust | [rustup](https://rustup.rs/) | -| Go | [mise](https://mise.jdx.dev/getting-started.html) | -| C# / .NET | [mise](https://mise.jdx.dev/getting-started.html) | +这样 Agent 可以明确指定 runtime、使用一次性依赖和隔离项目状态,并获得正常的 stdout/stderr,无需自行初始化项目。 -## 示例 +## 安装 -### 源文件 +macOS: ```bash -run-code node@20 snippet.ts -- first --verbose +brew install timzhong1024/tap/run-code ``` -执行前,`run-code` 会读取源文件,并将内容复制到新建的隔离模板项目中。它不会在源文件所属的已有工程里运行,不会读取该工程的依赖,也不会复制同目录的其他文件。代码片段需要的依赖应通过 `--package` 明确添加;`--` 后的参数会传给代码进程。 - -### 工作目录与环境变量 +其他安装方式: ```bash -run-code node@20 --cwd ./fixtures --env-file ./snippet.env snippet.ts +npm install --global @timzhong2000/run-code +cargo install --locked --git https://github.com/timzhong1024/run-code ``` -`--cwd` 设置最终代码进程看到的工作目录;模板初始化和依赖安装仍然在隔离的临时项目中完成。`--env-file` 按 dotenv 语法为最终启动命令和代码进程加载变量,不修改当前 shell,也不会用于之前的初始化及依赖安装步骤;加载的值不会显示在输出的命令中。两个路径都基于调用 `run-code` 时所在的目录解析。 +[GitHub Releases](https://github.com/timzhong1024/run-code/releases) 还提供 macOS、Linux 和 Windows 的独立 binary。 + +`run-code` 把 runtime 安装交给现有工具;只需安装会用到的后端: + +| 代码 | Toolchain 参数 | 必需后端 | +| --- | --- | --- | +| Python | `python[@VERSION]` | [uv](https://docs.astral.sh/uv/getting-started/installation/) | +| TypeScript / JavaScript | `node[@VERSION]` | [Vite+ (`vp`)](https://viteplus.dev/guide/) | +| Rust | `rust[@VERSION]` | [rustup](https://rustup.rs/) | +| Go | `go[@VERSION]` | [mise](https://mise.jdx.dev/getting-started.html) | +| C# | `dotnet[@VERSION]` | [mise](https://mise.jdx.dev/getting-started.html) | -### TypeScript +## 快速示例 + +### 使用 npm 包的 TypeScript ```bash run-code node@20 --package zod@4 --clean <<'TS' import { z } from "zod"; -console.log(await Promise.resolve(z.string().parse("hello"))); +console.log(await Promise.resolve(z.object({ id: z.number() }).parse({ id: 1 }))); TS ``` -### Python +### 使用 PyPI 包的 Python ```bash run-code python@3.14 --package requests==2.32.5 --clean <<'PY' @@ -72,7 +99,7 @@ print(requests.__version__) PY ``` -### Rust +### 使用 crate 的 Rust ```bash run-code rust@stable --package serde_json@1 --clean <<'RS' @@ -82,35 +109,53 @@ fn main() { RS ``` -### C# +### 源文件、参数、工作目录与环境变量 ```bash -run-code dotnet@10 --package Spectre.Console@0.50.0 --clean <<'CS' -using Spectre.Console; -AnsiConsole.MarkupLine("[green]Hello from C#[/]"); -CS +run-code node@20 \ + --package zod@4 \ + --cwd ./fixtures \ + --env-file ./snippet.env \ + snippet.ts -- first --verbose ``` -异步 Rust 可以为依赖指定 Cargo features: +源文件会被复制到全新的隔离模板中;它原有的项目、依赖和同目录文件都不会被使用。`--` 后的参数会传给代码进程。`--cwd` 只影响最终代码进程,`--env-file` 则提供 dotenv 变量,不修改当前 shell,也不会在展示的命令中暴露变量值。 -```bash -run-code rust@stable --package 'tokio@1[full]' --clean <<'RS' -#[tokio::main] -async fn main() { - println!("async"); -} -RS -``` +## 执行行为 -Windows PowerShell 7 使用单引号 here-string: +- 省略版本时使用内置的稳定策略:Python 3.14、Node latest、Rust stable、Go latest、.NET 10。 +- Python 和 Node 从 stdin 读取且没有 `--package` 时直接运行;有依赖或使用源文件时会创建隔离模板项目。 +- 临时项目默认保留,方便检查输出命令中的生成路径;只运行一次时添加 `--clean`。 +- 包管理器下载缓存保持启用,因此隔离运行不等于每次重新下载所有依赖。 +- 默认展示依赖安装和最终运行命令,并透传输出;`--quiet` 只保留最终进程的 stdout/stderr。 +- Node 默认使用支持 TypeScript 和顶层 `await` 的 ESM;只有明确依赖 CommonJS 时才使用 `--commonjs`。 -```powershell -@' -print("hello\nworld") -'@ | run-code python@3.14 --clean +## 参数 + +```text +run-code [OPTIONS] TOOLCHAIN[@VERSION] +run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...] +run-code skill ``` -Fish 不支持 heredoc,使用 `printf`: +| 参数 | 含义 | +| --- | --- | +| `TOOLCHAIN[@VERSION]` | `python`、`node`、`rust`、`go` 或 `dotnet`;`javascript`/`typescript` 是 `node` 别名,`csharp`/`cs` 是 `dotnet` 别名 | +| `FILE` | 不读取 stdin,改为复制并运行一个源文件 | +| `-- ARG ...` | 向代码进程传递参数 | +| `-p, --package SPEC` | 添加依赖;多个依赖可重复使用 | +| `--cwd DIR` | 设置最终代码进程的工作目录 | +| `--env-file FILE` | 为最终进程加载 dotenv 变量 | +| `--commonjs` | 让 Node 使用 CommonJS 而不是 ESM | +| `--clean` | 执行后删除临时项目 | +| `--quiet` | 只显示最终进程 stdout/stderr | +| `skill` | 输出内置的 Agent skill | + +依赖格式遵循各自生态。Python 使用 `NAME==VERSION`;Node、Rust、Go 和 .NET 使用 `NAME@VERSION`。Rust features 使用 `NAME[@VERSION][FEATURE,...]`,例如 `'tokio@1[full]'`。 + +### Shell 输入 + +Bash 和 Zsh 使用上面展示的 quoted heredoc。Fish 可以使用 `printf`: ```fish printf '%s\n' \ @@ -119,59 +164,16 @@ printf '%s\n' \ run-code node@20 ``` -## Agent Skill - -`run-code skill` 从已安装的二进制中输出完整 `SKILL.md`。Agent 会先读取 skill 的名称和 description,在匹配任务后再读取完整内容。 - -推荐安装到当前项目: +PowerShell 7 可以使用单引号 here-string: -```bash -mkdir -p .agents/skills/run-code-snippet -run-code skill > .agents/skills/run-code-snippet/SKILL.md -``` - -需要在所有项目中使用时,安装到用户目录: - -```bash -mkdir -p ~/.agents/skills/run-code-snippet -run-code skill > ~/.agents/skills/run-code-snippet/SKILL.md +```powershell +@' +print("hello\nworld") +'@ | run-code python@3.14 --clean ``` -Codex 会自动发现这些目录中的 skill;详细约定见 [Codex Skills 文档](https://learn.chatgpt.com/docs/build-skills)。 - -## 为什么做这个项目 - -临时运行代码时,初始化项目、安装依赖和准备运行环境的成本很高;临时切换 runtime 或 toolchain 版本也很麻烦。直接安装依赖又容易污染全局环境或当前项目环境。 - -已经有一些相似工具,但没有同时满足临时依赖、版本切换和隔离运行这些需求。`run-code` 受到这些代码片段运行器、版本管理器和临时包执行工具的启发,把这几个步骤统一成一个命令。 - ## 安全 -`run-code` 提供的是环境和依赖隔离,不是安全沙箱。输入的代码和第三方依赖都以当前用户权限运行,可以访问本机文件、网络、环境变量和凭据。`--env-file` 加载的变量会明确提供给代码片段,不要把密钥传给不可信代码。 - -依赖安装还可能执行 npm lifecycle scripts、Python build backend、Cargo `build.rs` 或其他生态的构建代码。只运行可信代码和依赖;使用陌生包前先检查官方文档与源码,敏感环境中固定版本,并避免暴露不必要的密钥。`--clean` 只删除临时项目,不会撤销代码已经产生的系统或网络副作用;各包管理器的下载缓存会继续保留。 - -漏洞请通过 GitHub private vulnerability reporting 私下提交;范围和报告方式见 [SECURITY.md](SECURITY.md)。 - -## 参数 - -```text -run-code [OPTIONS] TOOLCHAIN[@VERSION] -run-code [OPTIONS] TOOLCHAIN[@VERSION] FILE [-- ARG ...] -run-code skill -``` +代码片段、依赖、包 lifecycle hook、Python build backend 和 Cargo build script 都以当前用户权限运行,可以访问文件、网络、环境变量、凭据和其他进程。只运行可信代码和依赖;需要可复现时固定版本,不要把密钥传给不可信代码。 -- `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]'`。 -- `--cwd DIR`:设置最终代码进程的工作目录;模板初始化和依赖安装仍与该目录隔离。 -- `--env-file FILE`:按 dotenv 语法为最终启动命令和代码进程加载变量;同名变量会覆盖继承的环境变量,值不会显示在输出命令中,但 runner 自身用于隔离的变量优先。 -- `--commonjs`:让 Node 以 CommonJS 方式运行;默认使用支持顶层 `await` 的 ESM。 -- `--clean`:运行结束后删除临时项目;不指定时保留项目,默认输出的执行命令中会包含项目路径。 -- `--quiet`:隐藏项目初始化、依赖安装及命令本身,只输出最终代码进程的 stdout/stderr。 -- `skill`:输出内置的 `run-code-snippet` Skill 内容。 -- `-h, --help`:显示帮助。 -- `-V, --version`:显示版本。 - -未指定版本时使用内置默认值:Python 3.14、Node latest、Rust stable、Go latest、.NET 10。C# 通过 .NET 10+ file-based app 运行。使用 stdin 且未指定 `--package` 时,Python 和 Node 直接执行;使用文件输入时,即使没有依赖也始终创建隔离模板项目。包管理器的下载缓存保持启用。默认只显示依赖安装和最终代码运行命令,并实时透传它们的 stdout/stderr;项目初始化仅在失败时输出诊断信息。 +`--clean` 只能删除临时项目,不能撤销文件系统或网络副作用。完整安全边界和漏洞报告方式见 [SECURITY.md](SECURITY.md)。