use std::path::PathBuf; use clap::{Parser, Subcommand}; /// musl's allocator is slow under the many threads a save uses (see Cargo.toml). #[cfg(target_env = "musl")] #[global_allocator] static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; use onset_cli::app::App; use onset_cli::out::{Fail, Out}; use onset_cli::{access, cmd}; /// Onset: version control where nobody waits and nothing gets lost. #[derive(Parser)] #[command(name = "on", version = onset_cli::VERSION, about, disable_help_subcommand = true)] struct Cli { /// Print one JSON object instead of text. #[arg(long, global = true)] json: bool, /// Run as if started in this folder. #[arg(short = 'C', global = true, value_name = "FOLDER")] dir: Option, #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { /// Make a workspace for a project. Get { /// host/owner/name, or the folder of a local project. project: String, folder: Option, /// Start on an existing change instead of a new one. #[arg(long)] change: Option, /// Get only these folders (repeatable). #[arg(long)] only: Vec, /// Start on a new change on this release line, not main. #[arg(long)] line: Option, /// Fetch each file the first time it's opened, not all at once (Linux). #[arg(long)] lazy: bool, }, /// Start a lazy workspace's mount again (after a restart, say), or --stop it. Mount { folder: Option, #[arg(long)] stop: bool, #[arg(long, hide = true)] serve: bool, }, /// Start a new project that lives only on this machine. Init { folder: Option }, /// Make a project from a git repository, with its history. Import { /// A git repository: a folder, a URL, or upstream.codes/owner/name. url: String, folder: Option, /// Then put it on a host as host/owner/name. #[arg(long)] to: Option, /// Bring this git branch (or any commit) into this workspace's project as a change instead. #[arg(long, value_name = "REF", conflicts_with_all = ["to", "folder"])] change: Option, }, /// Put a local project on a host. Publish { project: String }, /// The project's git view: where git clones it and whether it's current. Git { #[command(subcommand)] action: Option, }, /// Sign in to a host. Login { host: Option, /// Use this token instead of signing in through the browser. #[arg(long)] token: Option, }, /// Where you are and what to do next. Status { change: Option }, /// What a change changes. Diff { paths: Vec, #[arg(long)] change: Option, #[arg(long)] from: Option, #[arg(long)] to: Option, #[arg(long)] stat: bool, }, /// History of main, open changes, saves or operations. Log { /// A version number, a change, or a path (the versions that changed it). what: Option, #[arg(long)] changes: bool, #[arg(long)] saves: bool, #[arg(long)] ops: bool, #[arg(short = 'n', default_value_t = 20)] limit: i64, /// A release line's versions. #[arg(long)] line: Option, }, /// Start another change. The current one is kept. New { title: Option, /// Build on a change that hasn't landed yet. #[arg(long)] on: Option, /// Start on a release line, not main: on land lands there. #[arg(long)] line: Option, }, /// The project's release lines, or a new one from a version of main. Line { name: Option, /// The version of main (or tag) it starts from; main's latest by default. #[arg(long)] from: Option, }, /// Set the current change's description. Describe { #[arg(required = true)] parts: Vec, }, /// Put this workspace on another change. Switch { change: String }, /// Abandon a change. Drop { change: Option }, /// Bring the change up to date with main. Sync, /// Put a change on main. Land { change: Option, /// Description: the first is the title, the rest the body. #[arg(short = 'm')] message: Vec, /// Land even though the secret check flagged something (recorded). #[arg(long)] allow_secrets: bool, /// Skip onset.toml's [land] run commands this time. #[arg(long)] no_run: bool, /// Land at a set time ("fri 9:00", "in 2h", "2026-11-02 09:00"); the host keeps it until then. #[arg(long, value_name = "WHEN")] at: Option, /// Other workspaces whose changes land with this one: all of them, or none. #[arg(long = "with", value_name = "WORKSPACE")] with: Vec, /// Land on this release line instead (a change that landed on main is brought over). #[arg(long, value_name = "LINE")] to: Option, }, /// Undo the last Onset command here, or take a landed version back out. Undo { /// A version of main (48) or of a release line (1.x/3) to take back out. version: Option, }, /// Bring files back from an earlier point. Restore { #[arg(required = true)] paths: Vec, /// From your own earlier save: "10 min ago", "2h ago", 15:30. #[arg(long)] at: Option, /// From a version of main, a change, or change:save. #[arg(long)] from: Option, }, /// See or set who can see a change or a path. Access { target: String, who: Vec, /// Also let them work on the change. #[arg(long)] edit: bool, /// Seal the folder: encrypted on the machines of the people named, so the host can't read it. #[arg(long)] sealed: bool, }, /// Approve someone else's change (the last approval needed lands it). Approve { change: String }, /// The project's host settings, like `approvals=1`. Settings { pairs: Vec }, /// Each line of a file in main, with the version that last changed it. Blame { path: String }, /// Print a file as it is in main, or in --from (a version, a change or change:save). Show { path: String, #[arg(long)] from: Option, }, /// Print shell completions: bash, zsh, fish, powershell or elvish. Completions { shell: clap_complete::Shell }, /// Check that nothing is damaged: this machine's objects, and the host's copy. Check, /// Make this machine's signing key and add it to your account on the host: your lands are signed from then on. Key, /// Check who signed the land that made a version of main (the latest by default). Verify { version: Option }, /// Find the version of main that broke something, by trying versions between a good one and a bad one. Bisect { /// A version (or tag) where it works. good: String, /// A version (or tag, or main) where it's broken. bad: String, /// The command that tells: exit 0 means good, 125 can't tell, anything else bad. #[arg(long)] run: String, /// Write the versions it tries to this folder and keep it (a scratch folder otherwise). #[arg(long = "in", value_name = "FOLDER")] folder: Option, }, /// Keep main working: run a check on each new version of main, and take back the version that breaks it. Watch { /// The check: exit 0 passes, 125 can't tell, anything else fails. #[arg(long)] run: String, /// A version known to pass (the last one that passed here, by default). #[arg(long)] from: Option, /// Check what's new once, then stop (for a scheduled job). #[arg(long)] once: bool, /// Seconds between looks at main. #[arg(long, default_value_t = 10)] every: u64, /// Times to run a failing check before blaming a version (for flaky checks). #[arg(long, default_value_t = 2)] tries: u32, }, /// Clean up what this machine keeps: saves of changes that landed or were dropped, and what nothing needs. Gc { /// How old a landed or dropped change's saves must be to go ("30d", "12h", "2w"). #[arg(long, value_name = "AGE")] older: Option, /// Say what would go, without removing anything. #[arg(long)] dry_run: bool, }, /// Agent sessions kept with the changes they land (only you see them): /// list them, show one, turn it off or on, delete them. /// Git submodules: list them, point one at a commit (or the one checked /// out in its folder), add one (--url), remove one, or check them out. Submodule { path: Option, commit: Option, /// Its git URL (adds the submodule, or changes where it's from). #[arg(long)] url: Option, #[arg(long)] remove: bool, /// Clone each submodule (or the one named) into its folder at its commit. #[arg(long)] checkout: bool, }, Sessions { #[command(subcommand)] action: Option, }, /// Take files or folders out of the project's whole history (a leaked /// secret, a big file), deleting their content. Says what it would do /// unless --yes; it can't be undone. Purge { #[arg(required = true)] paths: Vec, /// Do it. #[arg(long)] yes: bool, }, /// This project's workspaces on this machine (one per agent): list, add, clean up. Workspaces { #[command(subcommand)] action: Option, }, /// Put the whole hosted project back as it was at a time (owners; the last 30 days). Rollback { /// When: "2h ago", "yesterday", 2026-10-27 14:30. #[arg(long, conflicts_with = "undo")] to: Option, /// Reverse a rollback, with the bookmark it printed. #[arg(long)] undo: Option, }, /// Name a version of main (main's latest when left out), list the names, or --remove one. Tag { name: Option, version: Option, #[arg(long)] remove: bool, }, /// The project's groups, for `on access @group`. Group { name: Option, /// Its people (this replaces them). members: Vec, /// Remove the group. #[arg(long)] remove: bool, }, /// Print the guide for coding agents. Guide, #[command(hide = true)] Saver, /// Brings a workspace's .git view up to date (started in the background). #[command(hide = true, name = "git-view")] GitView { folder: PathBuf }, } #[derive(Subcommand)] enum GitAction { /// Start a paused git view again. Resume, /// Keep a branch of any git repository (GitHub, GitLab, your own) as main, both ways. Mirror { /// The repository: an https or ssh URL, or a folder. git's own credentials are used. url: String, /// The branch main is mirrored to. #[arg(long, default_value = "main")] branch: String, /// Mirror what's new once, then stop (for a scheduled job). #[arg(long)] once: bool, /// Seconds between rounds. #[arg(long, default_value_t = 10)] every: u64, }, } #[derive(Debug, Clone, clap::Subcommand)] enum WorkspacesAction { /// Another workspace (for another agent): a new change on main, or on --on (`.` is this one's). Add { folder: PathBuf, #[arg(long)] on: Option, }, /// Remove workspaces whose work is done: saved, empty and unused for --idle. Clean { #[arg(long, default_value = "2h")] idle: String, }, } fn run(app: &App, command: Command) -> Out { match command { Command::Get { project, folder, change, only, line, lazy } => cmd::get(app, &project, folder, change, only, None, line, lazy), Command::Mount { folder, stop, serve } => onset_cli::lazy::run(app, folder, stop, serve), Command::Init { folder } => cmd::init(app, folder), Command::Import { url, to, folder, change } => onset_cli::remote::import(app, &url, to, folder, change), Command::Publish { project } => onset_cli::remote::publish(app, &project), Command::Git { action: None } => onset_cli::remote::git_view(app, false), Command::Git { action: Some(GitAction::Resume) } => onset_cli::remote::git_view(app, true), Command::Git { action: Some(GitAction::Mirror { url, branch, once, every }) } => onset_cli::bridge::mirror(app, &url, &branch, once, every), Command::Login { host, token } => onset_cli::remote::login(app, host, token), Command::Status { change } => cmd::status(app, change), Command::Diff { paths, change, from, to, stat } => cmd::diff_cmd(app, paths, change, from, to, stat), Command::Log { what, changes, saves, ops, limit, line } => match line { Some(l) => app.session().and_then(|s| onset_cli::lines::log(app, &s.project, &l, limit.max(1) as usize)), None => cmd::log(app, what, changes, saves, ops, limit), }, Command::New { title, on, line } => cmd::new(app, title, on, line), Command::Line { name, from } => onset_cli::lines::run(app, name, from), Command::Describe { parts } => cmd::describe(app, parts), Command::Switch { change } => cmd::switch(app, &change), Command::Drop { change } => cmd::drop(app, change), Command::Sync => cmd::sync(app), Command::Land { change, message, allow_secrets, no_run, at, with, to } => cmd::land(app, change, message, allow_secrets, !no_run, at, with, to), Command::Undo { version } => cmd::undo(app, version), Command::Restore { paths, at, from } => cmd::restore(app, paths, at, from), Command::Access { target, who, edit, sealed } => access::run(app, &target, who, edit, sealed), Command::Approve { change } => onset_cli::remote::approve(app, &change), Command::Settings { pairs } => onset_cli::remote::settings(app, pairs), Command::Blame { path } => onset_cli::history::blame_cmd(app, path), Command::Show { path, from } => cmd::show(app, path, from), Command::Completions { shell } => { clap_complete::generate(shell, &mut ::command(), "on", &mut std::io::stdout()); Ok(onset_cli::out::Report::new()) } Command::Check => onset_cli::remote::check(app), Command::Gc { older, dry_run } => onset_cli::gc::run(app, older, dry_run), Command::Purge { paths, yes } => onset_cli::purge::run(app, paths, yes), Command::Sessions { action } => onset_cli::sessions::run(app, action), Command::Submodule { path, commit, url, remove, checkout } => onset_cli::submodules::run(app, path, commit, url, remove, checkout), Command::Key => onset_cli::keys::run(app), Command::Verify { version } => onset_cli::keys::verify(app, version), Command::Bisect { good, bad, run, folder } => cmd::bisect(app, good, bad, run, folder), Command::Watch { run, from, once, every, tries } => onset_cli::watch::run(app, run, from, once, every, tries), Command::Workspaces { action: None } => onset_cli::workspaces::list(app), Command::Workspaces { action: Some(WorkspacesAction::Add { folder, on }) } => onset_cli::workspaces::add(app, folder, on), Command::Workspaces { action: Some(WorkspacesAction::Clean { idle }) } => onset_cli::workspaces::clean(app, &idle), Command::Rollback { to, undo } => onset_cli::remote::rollback(app, to, undo), Command::Tag { name, version, remove } => onset_cli::tags::run(app, name, version, remove), Command::Group { name, members, remove } => onset_cli::remote::group(app, name, members, remove), Command::Guide => cmd::guide(), Command::Saver => onset_cli::remote::saver(app), Command::GitView { folder } => onset_cli::gitdir::update(app, &folder).map(|_| onset_cli::out::Report::new()).map_err(Fail::from), } } fn main() { let cli = match Cli::try_parse() { Ok(cli) => cli, Err(e) => { let code = if e.use_stderr() { 2 } else { 0 }; let _ = e.print(); std::process::exit(code); } }; let json = cli.json || onset_cli::paths::var("JSON").is_some_and(|v| v == "1"); let cwd = match cli.dir { Some(d) => std::env::current_dir().map(|c| c.join(d)).unwrap_or_default(), None => std::env::current_dir().unwrap_or_default(), }; let app = match App::open(json, cwd) { Ok(app) => app, Err(e) => { Fail::from(e).print(json); std::process::exit(1); } }; let starts_saver = !matches!(cli.command, Command::Saver | Command::Guide | Command::Login { .. } | Command::Completions { .. } | Command::GitView { .. }); let follows = !matches!(cli.command, Command::Saver | Command::GitView { .. } | Command::Mount { .. }); let result = run(&app, cli.command); if starts_saver { onset_cli::saver::ensure_running(); } // Workspaces whose change's base moved: their .git views follow. if follows { let touched = app.touched.borrow().clone(); for root in touched { onset_cli::gitdir::follow(&app, &root); } } let notes = app.notes.borrow().clone(); match result { Ok(mut report) => { if !notes.is_empty() { report.set("notes", notes.clone()); } report.print(json); if !json { for n in ¬es { eprintln!("note: {n}"); } } std::process::exit(report.exit); } Err(mut fail) => { if !notes.is_empty() { fail = fail.set("notes", notes.clone()); } fail.print(json); if !json { for n in ¬es { eprintln!("note: {n}"); } } std::process::exit(fail.exit); } } }