use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use clap::{ArgAction, Parser};
use crate::{
builtins, default_db_path, format_help, load_spec, match_commands, resolve_git_branch,
resolve_preferred_spec, run_matched, RootSpec, RunContext,
};
#[derive(Parser, Debug)]
#[command(name = "jan")]
#[command(
about = "YAML-driven command tree loaded from the preferred directory (`jan use`)",
version,
disable_help_flag = true
)]
pub struct JanCli {
#[arg(long, short = 'h', action = ArgAction::SetTrue, global = true)]
pub help: bool,
#[arg(long, value_name = "FILE", env = "JAN_DB", global = true)]
pub db: Option<PathBuf>,
#[arg(long, global = true)]
pub branch: Option<String>,
#[arg(long, global = true)]
pub no_log: bool,
#[arg(long, global = true, default_value = ".")]
pub cwd: PathBuf,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub command: Vec<OsString>,
}
fn framework_builtins_help() -> String {
"\
Built-in commands (part of the jan binary):
use — set, show, or clear the preferred jan directory
lab — isolated jan + cron environment for trying a command tree
computer — register, show, or clear the host computer id for `computer:` filtering
bundle — pack the preferred YAML tree into a ZIP
alias — emit shell aliases for executable leaves
config — emit / link / unlink / apply / deps host configuration from the preferred tree
list — list script leaves in the preferred tree
search — find scripts by name/about/category
show — print details for a script leaf
validate — structural checks for the preferred tree
audit — query the SQLite invocation log
cron — start/stop daemon; --list reads the schedule cache (100ms ticks)
systems — list/show/status for named agent systems (`system:` YAML)
runtime — warm language interpreter pool (python/node/shell/kotlin)
packages — inspect / prune cached package environments (uv, pnpm, gradle)
test — run Given/When/Then shell tests for a command path (and nested)
ps — show jan processes and collective CPU usage
Global options:
-h, --help Show this help (lists preferred-tree commands when a dir is set)
-V, --version Print version
-v, --verbose Explain how the preferred directory was resolved
--cwd <DIR> Working directory for subprocesses (default: .)
--db <FILE> SQLite audit log path
--branch <B> Override git branch recorded in the audit log
--no-log Do not write to the audit log
Configure a command tree with `jan use <DIR>`, then re-run `jan --help`.
To try agents without touching your daily setup: `jan lab <DIR> --help`.
"
.to_string()
}
fn print_root_help(spec: Option<&RootSpec>) {
match spec {
Some(spec) => {
print!("{}", format_help(spec, &[], None));
}
None => {
println!("jan — YAML-driven command tree\n");
println!("No preferred jan directory is configured yet.\n");
println!(" jan use <DIR> Save a directory containing scripts.spec.yaml");
println!(" jan use --show Show the saved preference\n");
}
}
print!("{}", framework_builtins_help());
}
pub fn run_jan() -> Result<i32> {
let cli = JanCli::parse();
let cwd = cli.cwd.canonicalize().unwrap_or_else(|_| cli.cwd.clone());
if let Some(first) = cli.command.first() {
let key = first.to_string_lossy();
if builtins::is_pre_spec_builtin(key.as_ref()) {
let tail: Vec<_> = cli.command[1..].to_vec();
return match key.as_ref() {
"use" => builtins::run_use(&tail),
"lab" => crate::lab::run_lab(&tail),
"computer" => builtins::run_computer(&tail),
"ps" => crate::ps::run_ps(&tail),
"runtime" => crate::runtime_daemon::dispatch_runtime(&tail),
_ => Ok(0),
};
}
}
let loaded = match resolve_preferred_spec() {
Ok(pair) => Some(pair),
Err(e) => {
if cli.help || cli.command.is_empty() {
print_root_help(None);
if cli.verbose {
eprintln!("jan: no preferred spec ({e:#})");
}
return Ok(0);
}
return Err(e);
}
};
let (spec_path, spec_identity) = loaded.expect("Ok branch always sets Some");
let spec = load_spec(&spec_path).with_context(|| format!("load {}", spec_path.display()))?;
if cli.verbose {
eprintln!("jan: cwd={}", cwd.display());
eprintln!(
"jan: preferred directory: {} / {}",
spec_identity.spec_dir, spec_identity.root_yaml
);
}
let branch = resolve_git_branch(&cwd, cli.branch.as_deref());
let is_test = cli
.command
.first()
.is_some_and(|s| s.to_string_lossy() == "test");
let no_log = cli.no_log || is_test || std::env::var_os("JAN_NO_LOG").is_some();
let audit_db = cli.db.as_ref().cloned().unwrap_or_else(default_db_path);
let db_path = if no_log { None } else { Some(audit_db.clone()) };
let ctx = RunContext {
cwd: &cwd,
db_path: db_path.as_deref(),
branch,
no_log,
spec_root: &spec_identity,
};
if cli.command.is_empty() {
print_root_help(Some(&spec));
return Ok(0);
}
if let Some(first) = cli.command.first() {
let key = first.to_string_lossy();
if builtins::is_builtin_reserved(key.as_ref()) {
let tail: Vec<_> = cli.command[1..].to_vec();
return match key.as_ref() {
"bundle" => builtins::bundle_spec_zip(&spec_identity, &tail, cli.verbose),
"alias" => builtins::emit_shell_aliases(&spec, &tail),
"config" => {
let root = PathBuf::from(&spec_identity.spec_dir);
crate::hostconfig::dispatch_config(&spec, &root, &tail)
}
"use" => builtins::run_use(&tail),
"computer" => builtins::run_computer(&tail),
"list" | "search" | "show" | "validate" => {
let root = PathBuf::from(&spec_identity.spec_dir);
crate::inspect::dispatch_inspect(key.as_ref(), &tail, &spec, None, Some(&root))
}
"audit" => crate::inspect::dispatch_inspect(
key.as_ref(),
&tail,
&spec,
Some(&audit_db),
None,
),
"cron" => crate::inspect::run_cron(&spec, &tail, &ctx),
"systems" => crate::systems::dispatch_systems(&tail, &spec),
"runtime" => crate::runtime_daemon::dispatch_runtime(&tail),
"packages" => {
let root = PathBuf::from(&spec_identity.spec_dir);
crate::packages::dispatch_packages(&tail, &spec, &root)
}
"test" => crate::cmdtest::dispatch_test(&tail, &spec, &ctx),
"ps" => crate::ps::run_ps(&tail),
_ => Ok(0),
};
}
}
let m = match_commands(&spec, &cli.command);
if cli.help || m.wants_help {
let help = format_help(&spec, &m.chain, m.node);
print!("{help}");
return Ok(0);
}
let passthrough_leaf = m
.node
.and_then(|n| n.exec.as_ref())
.is_some_and(|e| e.passthrough);
if !passthrough_leaf && m.trailing.iter().any(|a| a == "--help" || a == "-h") {
return Err(anyhow!(
"place --help immediately after the subcommand prefix you want help for"
));
}
let node = match m.node {
Some(n) => n,
None => {
let key = cli.command[0].to_string_lossy();
return Err(anyhow!("unknown top-level command `{key}`"));
}
};
if !m.trailing.is_empty() && !node.is_leaf_exec() {
let t = m.trailing[0].to_string_lossy();
return Err(anyhow!("unknown subcommand `{t}`"));
}
if node.is_leaf_exec() {
return run_matched(&spec, &m.chain, node, &m.trailing, &ctx);
}
let help = format_help(&spec, &m.chain, Some(node));
print!("{help}");
Ok(0)
}