Skip to content

Commit 4a8d961

Browse files
committed
refactor(cli): trim one-shot startup path
1 parent d9db292 commit 4a8d961

2 files changed

Lines changed: 126 additions & 35 deletions

File tree

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,9 @@ pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }
9696
napi = { version = "3.0.0", default-features = false, features = ["napi6", "compat-mode"] }
9797
napi-derive = "3.0.0"
9898
napi-build = "2"
99+
100+
[profile.release]
101+
lto = "thin"
102+
codegen-units = 1
103+
panic = "abort"
104+
strip = "symbols"

crates/bashkit-cli/src/main.rs

Lines changed: 120 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Decision: enable http, git, python by default for CLI users.
22
// Provide --no-http, --no-git, --no-python to disable individually.
3+
// Decision: keep one-shot CLI on a current-thread runtime; reserve multi-thread
4+
// runtime for MCP only so cold-start work stays off the common path.
35

46
//! Bashkit CLI - Command line interface for virtual bash execution
57
//!
@@ -14,6 +16,7 @@ mod mcp;
1416
use anyhow::{Context, Result};
1517
use clap::{Parser, Subcommand};
1618
use std::path::PathBuf;
19+
use tokio::runtime::Builder;
1720

1821
/// Bashkit - Virtual bash interpreter
1922
#[derive(Parser, Debug)]
@@ -75,6 +78,21 @@ enum SubCmd {
7578
Mcp,
7679
}
7780

81+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82+
enum CliMode {
83+
Mcp,
84+
Command,
85+
Script,
86+
Interactive,
87+
}
88+
89+
#[derive(Debug)]
90+
struct RunOutput {
91+
stdout: String,
92+
stderr: String,
93+
exit_code: i32,
94+
}
95+
7896
fn build_bash(args: &Args) -> bashkit::Bash {
7997
let mut builder = bashkit::Bash::builder();
8098

@@ -99,6 +117,18 @@ fn build_bash(args: &Args) -> bashkit::Bash {
99117
builder.build()
100118
}
101119

120+
fn cli_mode(args: &Args) -> CliMode {
121+
if matches!(args.subcommand, Some(SubCmd::Mcp)) {
122+
CliMode::Mcp
123+
} else if args.command.is_some() {
124+
CliMode::Command
125+
} else if args.script.is_some() {
126+
CliMode::Script
127+
} else {
128+
CliMode::Interactive
129+
}
130+
}
131+
102132
/// Parse mount specs (HOST_PATH or HOST_PATH:VFS_PATH) and apply to builder.
103133
#[cfg(feature = "realfs")]
104134
fn apply_real_mounts(
@@ -123,47 +153,69 @@ fn apply_real_mounts(
123153
builder
124154
}
125155

126-
#[tokio::main]
127-
async fn main() -> Result<()> {
156+
fn main() -> Result<()> {
128157
let args = Args::parse();
129158

130-
// Handle subcommands first
131-
if let Some(SubCmd::Mcp) = args.subcommand {
132-
return mcp::run().await;
133-
}
134-
135-
let mut bash = build_bash(&args);
136-
137-
// Execute command string if provided
138-
if let Some(cmd) = args.command {
139-
let result = bash.exec(&cmd).await.context("Failed to execute command")?;
140-
print!("{}", result.stdout);
141-
if !result.stderr.is_empty() {
142-
eprint!("{}", result.stderr);
159+
match cli_mode(&args) {
160+
CliMode::Mcp => run_mcp(),
161+
CliMode::Command | CliMode::Script => {
162+
let output = run_oneshot(args)?;
163+
print!("{}", output.stdout);
164+
if !output.stderr.is_empty() {
165+
eprint!("{}", output.stderr);
166+
}
167+
std::process::exit(output.exit_code);
143168
}
144-
std::process::exit(result.exit_code);
145-
}
146-
147-
// Execute script file if provided
148-
if let Some(script_path) = args.script {
149-
let script = std::fs::read_to_string(&script_path)
150-
.with_context(|| format!("Failed to read script: {}", script_path.display()))?;
151-
152-
let result = bash
153-
.exec(&script)
154-
.await
155-
.context("Failed to execute script")?;
156-
print!("{}", result.stdout);
157-
if !result.stderr.is_empty() {
158-
eprint!("{}", result.stderr);
169+
CliMode::Interactive => {
170+
eprintln!("bashkit: interactive mode not yet implemented");
171+
eprintln!("Usage: bashkit -c 'command' or bashkit script.sh or bashkit mcp");
172+
std::process::exit(1);
159173
}
160-
std::process::exit(result.exit_code);
161174
}
175+
}
176+
177+
fn run_mcp() -> Result<()> {
178+
Builder::new_multi_thread()
179+
.enable_all()
180+
.build()
181+
.context("Failed to build MCP runtime")?
182+
.block_on(mcp::run())
183+
}
162184

163-
// Interactive REPL (not yet implemented)
164-
eprintln!("bashkit: interactive mode not yet implemented");
165-
eprintln!("Usage: bashkit -c 'command' or bashkit script.sh or bashkit mcp");
166-
std::process::exit(1);
185+
fn run_oneshot(args: Args) -> Result<RunOutput> {
186+
Builder::new_current_thread()
187+
.enable_all()
188+
.build()
189+
.context("Failed to build CLI runtime")?
190+
.block_on(async move {
191+
let mut bash = build_bash(&args);
192+
193+
if let Some(cmd) = args.command {
194+
let result = bash.exec(&cmd).await.context("Failed to execute command")?;
195+
return Ok(RunOutput {
196+
stdout: result.stdout,
197+
stderr: result.stderr,
198+
exit_code: result.exit_code,
199+
});
200+
}
201+
202+
if let Some(script_path) = args.script {
203+
let script = std::fs::read_to_string(&script_path)
204+
.with_context(|| format!("Failed to read script: {}", script_path.display()))?;
205+
206+
let result = bash
207+
.exec(&script)
208+
.await
209+
.context("Failed to execute script")?;
210+
return Ok(RunOutput {
211+
stdout: result.stdout,
212+
stderr: result.stderr,
213+
exit_code: result.exit_code,
214+
});
215+
}
216+
217+
unreachable!("run_oneshot called for non-executable mode");
218+
})
167219
}
168220

169221
#[cfg(test)]
@@ -194,6 +246,30 @@ mod tests {
194246
assert!(!args.no_python);
195247
}
196248

249+
#[test]
250+
fn cli_mode_prefers_mcp() {
251+
let args = Args::parse_from(["bashkit", "mcp"]);
252+
assert_eq!(cli_mode(&args), CliMode::Mcp);
253+
}
254+
255+
#[test]
256+
fn cli_mode_detects_command() {
257+
let args = Args::parse_from(["bashkit", "-c", "echo hi"]);
258+
assert_eq!(cli_mode(&args), CliMode::Command);
259+
}
260+
261+
#[test]
262+
fn cli_mode_detects_script() {
263+
let args = Args::parse_from(["bashkit", "script.sh"]);
264+
assert_eq!(cli_mode(&args), CliMode::Script);
265+
}
266+
267+
#[test]
268+
fn cli_mode_falls_back_to_interactive() {
269+
let args = Args::parse_from(["bashkit"]);
270+
assert_eq!(cli_mode(&args), CliMode::Interactive);
271+
}
272+
197273
#[cfg(feature = "python")]
198274
#[tokio::test]
199275
async fn python_enabled_by_default() {
@@ -261,6 +337,15 @@ mod tests {
261337
assert_eq!(result.exit_code, 0);
262338
}
263339

340+
#[test]
341+
fn run_oneshot_executes_command_on_current_thread_runtime() {
342+
let args = Args::parse_from(["bashkit", "--no-http", "--no-git", "-c", "echo works"]);
343+
let output = run_oneshot(args).expect("run");
344+
assert_eq!(output.stdout, "works\n");
345+
assert_eq!(output.stderr, "");
346+
assert_eq!(output.exit_code, 0);
347+
}
348+
264349
#[cfg(feature = "realfs")]
265350
#[test]
266351
fn parse_mount_flags() {

0 commit comments

Comments
 (0)