1use std::ffi::OsString;
2use std::path::PathBuf;
3
4use anyhow::{anyhow, Context, Result};
5use clap::{ArgAction, Parser};
6
7use crate::{
8 builtins, default_db_path, format_help, load_spec, match_commands, resolve_git_branch,
9 resolve_preferred_spec, run_matched, RootSpec, RunContext,
10};
11
12#[derive(Parser, Debug)]
13#[command(name = "jan")]
14#[command(
15 about = "YAML-driven command tree loaded from the preferred directory (`jan use`)",
16 version,
17 disable_help_flag = true
18)]
19pub struct JanCli {
20 #[arg(long, short = 'h', action = ArgAction::SetTrue, global = true)]
22 pub help: bool,
23
24 #[arg(long, value_name = "FILE", env = "JAN_DB", global = true)]
26 pub db: Option<PathBuf>,
27
28 #[arg(long, global = true)]
30 pub branch: Option<String>,
31
32 #[arg(long, global = true)]
34 pub no_log: bool,
35
36 #[arg(long, global = true, default_value = ".")]
38 pub cwd: PathBuf,
39
40 #[arg(short, long, global = true)]
42 pub verbose: bool,
43
44 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
46 pub command: Vec<OsString>,
47}
48
49fn framework_builtins_help() -> String {
50 "\
51Built-in commands:
52 use — set, show, or clear the preferred jan directory
53 bundle — pack the preferred YAML tree into a ZIP
54 alias — emit shell aliases for executable leaves
55
56Global options:
57 -h, --help Show this help (lists live subcommands when a preferred dir is set)
58 -V, --version Print version
59 -v, --verbose Explain how the preferred directory was resolved
60 --cwd <DIR> Working directory for subprocesses (default: .)
61 --db <FILE> SQLite audit log path
62 --branch <B> Override git branch recorded in the audit log
63 --no-log Do not write to the audit log
64
65Configure a command tree with `jan use <DIR>`, then re-run `jan --help` to list live subcommands.
66"
67 .to_string()
68}
69
70fn print_root_help(spec: Option<&RootSpec>) {
71 match spec {
72 Some(spec) => {
73 print!("{}", format_help(spec, &[], None));
74 }
75 None => {
76 println!("jan — YAML-driven command tree\n");
77 println!("No preferred jan directory is configured yet.\n");
78 println!(" jan use <DIR> Save a directory containing scripts.spec.yaml");
79 println!(" jan use --show Show the saved preference\n");
80 }
81 }
82 print!("{}", framework_builtins_help());
83}
84
85pub fn run_jan() -> Result<i32> {
86 let cli = JanCli::parse();
87 let cwd = cli.cwd.canonicalize().unwrap_or_else(|_| cli.cwd.clone());
88
89 if let Some(first) = cli.command.first() {
91 let key = first.to_string_lossy();
92 if builtins::is_pre_spec_builtin(key.as_ref()) {
93 let tail: Vec<_> = cli.command[1..].to_vec();
94 return match key.as_ref() {
95 "use" => builtins::run_use(&tail),
96 _ => Ok(0),
97 };
98 }
99 }
100
101 let loaded = match resolve_preferred_spec() {
102 Ok(pair) => Some(pair),
103 Err(e) => {
104 if cli.help || cli.command.is_empty() {
105 print_root_help(None);
106 if cli.verbose {
107 eprintln!("jan: no preferred spec ({e:#})");
108 }
109 return Ok(0);
110 }
111 return Err(e);
112 }
113 };
114
115 let (spec_path, spec_identity) = loaded.expect("Ok branch always sets Some");
116 let spec = load_spec(&spec_path).with_context(|| format!("load {}", spec_path.display()))?;
117
118 if cli.verbose {
119 eprintln!("jan: cwd={}", cwd.display());
120 eprintln!(
121 "jan: preferred directory: {} / {}",
122 spec_identity.spec_dir, spec_identity.root_yaml
123 );
124 }
125
126 let branch = resolve_git_branch(&cwd, cli.branch.as_deref());
127 let db_path = if cli.no_log {
128 None
129 } else {
130 Some(cli.db.as_ref().cloned().unwrap_or_else(default_db_path))
131 };
132
133 let ctx = RunContext {
134 cwd: &cwd,
135 db_path: db_path.as_deref(),
136 branch,
137 no_log: cli.no_log,
138 spec_root: &spec_identity,
139 };
140
141 if cli.command.is_empty() {
142 print_root_help(Some(&spec));
143 return Ok(0);
144 }
145
146 if let Some(first) = cli.command.first() {
147 let key = first.to_string_lossy();
148 if builtins::is_builtin_reserved(key.as_ref()) {
149 let tail: Vec<_> = cli.command[1..].to_vec();
150 return match key.as_ref() {
151 "bundle" => builtins::bundle_spec_zip(&spec_identity, &tail, cli.verbose),
152 "alias" => builtins::emit_shell_aliases(&spec, &tail),
153 "use" => builtins::run_use(&tail),
154 _ => Ok(0),
155 };
156 }
157 }
158
159 let m = match_commands(&spec, &cli.command);
160
161 if cli.help || m.wants_help {
162 let help = format_help(&spec, &m.chain, m.node);
163 print!("{help}");
164 return Ok(0);
165 }
166
167 if m.trailing.iter().any(|a| a == "--help" || a == "-h") {
168 return Err(anyhow!(
169 "place --help immediately after the subcommand prefix you want help for"
170 ));
171 }
172
173 let node = match m.node {
174 Some(n) => n,
175 None => {
176 let key = cli.command[0].to_string_lossy();
177 return Err(anyhow!("unknown top-level command `{key}`"));
178 }
179 };
180
181 if !m.trailing.is_empty() && !node.is_leaf_exec() {
182 let t = m.trailing[0].to_string_lossy();
183 return Err(anyhow!("unknown subcommand `{t}`"));
184 }
185
186 if node.is_leaf_exec() {
187 return run_matched(&spec, &m.chain, node, &m.trailing, &ctx);
188 }
189
190 let help = format_help(&spec, &m.chain, Some(node));
191 print!("{help}");
192 Ok(0)
193}