use std::env;
use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::{Context, Result};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
fn main() -> ExitCode {
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.init();
match run() {
Ok(code) => code,
Err(e) => {
eprintln!("Error: {e:?}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<ExitCode> {
let args: Vec<String> = env::args().collect();
match args.get(1).map(|s| s.as_str()) {
None => {
kaish_repl::run()?;
Ok(ExitCode::SUCCESS)
}
Some("--help" | "-h") => {
print_help();
Ok(ExitCode::SUCCESS)
}
Some("--version" | "-V") => {
println!("kaish {}", env!("CARGO_PKG_VERSION"));
Ok(ExitCode::SUCCESS)
}
Some("-c") => {
let cmd = args.get(2)
.context("-c requires a command argument")?;
run_command(cmd)
}
Some("serve") => {
run_serve(&args[2..])
}
Some(arg) if arg.starts_with("--connect=") => {
let socket = &arg["--connect=".len()..];
run_repl_connected(socket)
}
Some("--connect") => {
let socket = args.get(2)
.context("--connect requires a socket path")?;
run_repl_connected(socket)
}
Some(path) if !path.starts_with('-') => {
run_script(path)
}
Some(unknown) => {
eprintln!("Unknown option: {unknown}");
eprintln!("Run 'kaish --help' for usage.");
Ok(ExitCode::FAILURE)
}
}
}
fn print_help() {
println!(r#"会sh — kaish v{}
Usage:
kaish Interactive REPL
kaish -c <command> Execute command and exit
kaish <script.kai> Run a script file
kaish serve [OPTIONS] Start RPC server
kaish --connect <socket> REPL connected to remote kernel
Options:
-c <command> Execute command string and exit
-h, --help Show this help
-V, --version Show version
Serve Options:
--socket=<path> Socket path (default: $XDG_RUNTIME_DIR/kaish/default.sock)
--name=<name> Kernel name (default: "default")
Examples:
kaish # Start interactive REPL
kaish -c 'echo hello' # Run a command
kaish deploy.kai # Run a deployment script
kaish serve # Start kernel server
kaish --connect /tmp/k.sock # Connect REPL to running kernel
"#, env!("CARGO_PKG_VERSION"));
}
fn run_script(path: &str) -> Result<ExitCode> {
use kaish_client::EmbeddedClient;
use kaish_kernel::{Kernel, KernelConfig};
let source = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read script: {path}"))?;
let source = if source.starts_with("#!") {
source.lines().skip(1).collect::<Vec<_>>().join("\n")
} else {
source
};
let config = KernelConfig::repl().with_interactive(true);
let kernel = Kernel::new(config)
.context("Failed to create kernel")?;
let client = EmbeddedClient::new(kernel);
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(client.execute_streaming(&source, &mut |r| {
if !r.out.is_empty() {
print!("{}", r.out);
}
if !r.err.is_empty() {
eprint!("{}", r.err);
}
}))?;
if result.ok() {
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::from(result.code as u8))
}
}
fn run_command(cmd: &str) -> Result<ExitCode> {
use kaish_client::EmbeddedClient;
use kaish_kernel::{Kernel, KernelConfig};
let config = KernelConfig::repl().with_interactive(true);
let kernel = Kernel::new(config)
.context("Failed to create kernel")?;
let client = EmbeddedClient::new(kernel);
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(client.execute_streaming(cmd, &mut |r| {
if !r.out.is_empty() {
print!("{}", r.out);
}
if !r.err.is_empty() {
eprint!("{}", r.err);
}
}))?;
if result.ok() {
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::from(result.code as u8))
}
}
fn run_serve(args: &[String]) -> Result<ExitCode> {
use kaish_kernel::rpc::KernelRpcServer;
use kaish_kernel::{Kernel, KernelConfig};
use tokio::task::LocalSet;
let mut socket_path: Option<PathBuf> = None;
let mut kernel_name = "default".to_string();
for arg in args {
if let Some(path) = arg.strip_prefix("--socket=") {
socket_path = Some(PathBuf::from(path));
} else if let Some(name) = arg.strip_prefix("--name=") {
kernel_name = name.to_string();
} else if arg == "--help" || arg == "-h" {
println!("kaish serve - Start RPC server\n");
println!("Options:");
println!(" --socket=<path> Socket path");
println!(" --name=<name> Kernel name (default: \"default\")");
return Ok(ExitCode::SUCCESS);
} else {
eprintln!("Unknown serve option: {arg}");
return Ok(ExitCode::FAILURE);
}
}
let config = KernelConfig::named(&kernel_name);
let kernel = Kernel::new(config)
.context("Failed to create kernel")?;
let server = KernelRpcServer::new(kernel);
let rt = tokio::runtime::Runtime::new()?;
let local = LocalSet::new();
local.block_on(&rt, async {
if let Some(path) = socket_path {
server.serve(&path).await
} else {
server.serve_default().await
}
})?;
Ok(ExitCode::SUCCESS)
}
fn run_repl_connected(socket_path: &str) -> Result<ExitCode> {
use kaish_client::IpcClient;
use tokio::task::LocalSet;
println!("会sh — kaish v{} (connected to {})",
env!("CARGO_PKG_VERSION"), socket_path);
println!("Type /help for commands, /quit to exit.\n");
let rt = tokio::runtime::Runtime::new()?;
let local = LocalSet::new();
let client = local.block_on(&rt, IpcClient::connect(socket_path))
.with_context(|| format!("Failed to connect to kernel at {socket_path}"))?;
kaish_repl::run_with_client(client, &rt, &local)?;
Ok(ExitCode::SUCCESS)
}