Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 4 |
| use vcs_cli::app::App; |
| 5 | use vcs_cli::out::{Fail, Out}; |
| 6 | use vcs_cli::{access, cmd}; |
| 7 |
| 8 | /// Version control where nobody waits and nothing gets lost. |
| 9 | #[derive(Parser)] |
| 10 | #[command(name = "vcs", version = vcs_cli::VERSION, about, disable_help_subcommand = true)] |
| 11 | struct Cli { |
| 12 | /// Print one JSON object instead of text. |
| 13 | #[arg(long, global = true)] |
| 14 | json: bool, |
| 15 | /// Run as if started in this folder. |
| 16 | #[arg(short = 'C', global = true, value_name = "FOLDER")] |
| 17 | dir: Option<PathBuf>, |
| 18 | #[command(subcommand)] |
| 19 | command: Command, |
| 20 | } |
| 21 |
| 22 | #[derive(Subcommand)] |
| 23 | enum Command { |
| 24 | /// Make a workspace for a project. |
| 25 | Get { |
| 26 | /// host/owner/name, or the folder of a local project. |
| 27 | project: String, |
| 28 | folder: Option<PathBuf>, |
| 29 | /// Start on an existing change instead of a new one. |
| 30 | #[arg(long)] |
| 31 | change: Option<String>, |
| 32 | /// Get only these folders (repeatable). |
| 33 | #[arg(long)] |
| 34 | only: Vec<String>, |
| 35 | }, |
| 36 | /// Start a new project that lives only on this machine. |
| 37 | Init { folder: Option<PathBuf> }, |
| 38 | /// Make a project from a git repository, with its history. |
| 39 | Import { |
| 40 | /// A git repository: a folder, a URL, or upstream.codes/owner/name. |
| 41 | url: String, |
| 42 | folder: Option<PathBuf>, |
| 43 | /// Then put it on a host as host/owner/name. |
| 44 | #[arg(long)] |
| 45 | to: Option<String>, |
| 46 | /// Bring this git branch (or any commit) into this workspace's project as a change instead. |
| 47 | #[arg(long, value_name = "REF", conflicts_with_all = ["to", "folder"])] |
| 48 | change: Option<String>, |
| 49 | }, |
| 50 | /// Put a local project on a host. |
| 51 | Publish { project: String }, |
| 52 | /// The project's git view: where git clones it and whether it's current. |
| 53 | Git { |
| 54 | /// `resume` starts a paused git view again. |
| 55 | #[arg(value_parser = ["resume"])] |
| 56 | action: Option<String>, |
| 57 | }, |
| 58 | /// Sign in to a host. |
| 59 | Login { |
| 60 | host: Option<String>, |
| 61 | /// Use this token instead of signing in through the browser. |
| 62 | #[arg(long)] |
| 63 | token: Option<String>, |
| 64 | }, |
| 65 | /// Where you are and what to do next. |
| 66 | Status { change: Option<String> }, |
| 67 | /// What a change changes. |
| 68 | Diff { |
| 69 | paths: Vec<String>, |
| 70 | #[arg(long)] |
| 71 | change: Option<String>, |
| 72 | #[arg(long)] |
| 73 | from: Option<String>, |
| 74 | #[arg(long)] |
| 75 | to: Option<String>, |
| 76 | #[arg(long)] |
| 77 | stat: bool, |
| 78 | }, |
| 79 | /// History of main, open changes, saves or operations. |
| 80 | Log { |
| 81 | /// A version number, a change, or a path (the versions that changed it). |
| 82 | what: Option<String>, |
| 83 | #[arg(long)] |
| 84 | changes: bool, |
| 85 | #[arg(long)] |
| 86 | saves: bool, |
| 87 | #[arg(long)] |
| 88 | ops: bool, |
| 89 | #[arg(short = 'n', default_value_t = 20)] |
| 90 | limit: i64, |
| 91 | }, |
| 92 | /// Start another change. The current one is kept. |
| 93 | New { |
| 94 | title: Option<String>, |
| 95 | /// Build on a change that hasn't landed yet. |
| 96 | #[arg(long)] |
| 97 | on: Option<String>, |
| 98 | }, |
| 99 | /// Set the current change's description. |
| 100 | Describe { |
| 101 | #[arg(required = true)] |
| 102 | parts: Vec<String>, |
| 103 | }, |
| 104 | /// Put this workspace on another change. |
| 105 | Switch { change: String }, |
| 106 | /// Abandon a change. |
| 107 | Drop { change: Option<String> }, |
| 108 | /// Bring the change up to date with main. |
| 109 | Sync, |
| 110 | /// Put a change on main. |
| 111 | Land { |
| 112 | change: Option<String>, |
| 113 | /// Description: the first is the title, the rest the body. |
| 114 | #[arg(short = 'm')] |
| 115 | message: Vec<String>, |
| 116 | /// Land even though the secret check flagged something (recorded). |
| 117 | #[arg(long)] |
| 118 | allow_secrets: bool, |
| 119 | /// Skip vcs.toml's [land] run commands this time. |
| 120 | #[arg(long)] |
| 121 | no_run: bool, |
| 122 | /// Land at a set time ("fri 9:00", "in 2h", "2026-11-02 09:00"); the host keeps it until then. |
| 123 | #[arg(long, value_name = "WHEN")] |
| 124 | at: Option<String>, |
| 125 | /// Other workspaces whose changes land with this one: all of them, or none. |
| 126 | #[arg(long = "with", value_name = "WORKSPACE")] |
| 127 | with: Vec<PathBuf>, |
| 128 | }, |
| 129 | /// Undo the last vcs command here, or take a landed version back out. |
| 130 | Undo { version: Option<u64> }, |
| 131 | /// Bring files back from an earlier point. |
| 132 | Restore { |
| 133 | #[arg(required = true)] |
| 134 | paths: Vec<String>, |
| 135 | /// From your own earlier save: "10 min ago", "2h ago", 15:30. |
| 136 | #[arg(long)] |
| 137 | at: Option<String>, |
| 138 | /// From a version of main, a change, or change:save. |
| 139 | #[arg(long)] |
| 140 | from: Option<String>, |
| 141 | }, |
| 142 | /// See or set who can see a change or a path. |
| 143 | Access { |
| 144 | target: String, |
| 145 | who: Vec<String>, |
| 146 | /// Also let them work on the change. |
| 147 | #[arg(long)] |
| 148 | edit: bool, |
| 149 | }, |
| 150 | /// Approve someone else's change (the last approval needed lands it). |
| 151 | Approve { change: String }, |
| 152 | /// The project's host settings, like `approvals=1`. |
| 153 | Settings { pairs: Vec<String> }, |
| 154 | /// Each line of a file in main, with the version that last changed it. |
| 155 | Blame { path: String }, |
| 156 | /// Print a file as it is in main, or in --from (a version, a change or change:save). |
| 157 | Show { |
| 158 | path: String, |
| 159 | #[arg(long)] |
| 160 | from: Option<String>, |
| 161 | }, |
| 162 | /// Print shell completions: bash, zsh, fish, powershell or elvish. |
| 163 | Completions { shell: clap_complete::Shell }, |
| 164 | /// Check that nothing is damaged: this machine's objects, and the host's copy. |
| 165 | Check, |
| 166 | /// This project's workspaces on this machine (one per agent): list, add, clean up. |
| 167 | Workspaces { |
| 168 | #[command(subcommand)] |
| 169 | action: Option<WorkspacesAction>, |
| 170 | }, |
| 171 | /// Put the whole hosted project back as it was at a time (owners; the last 30 days). |
| 172 | Rollback { |
| 173 | /// When: "2h ago", "yesterday", 2026-10-27 14:30. |
| 174 | #[arg(long, conflicts_with = "undo")] |
| 175 | to: Option<String>, |
| 176 | /// Reverse a rollback, with the bookmark it printed. |
| 177 | #[arg(long)] |
| 178 | undo: Option<String>, |
| 179 | }, |
| 180 | /// Name a version of main (main's latest when left out), list the names, or --remove one. |
| 181 | Tag { |
| 182 | name: Option<String>, |
| 183 | version: Option<String>, |
| 184 | #[arg(long)] |
| 185 | remove: bool, |
| 186 | }, |
| 187 | /// The project's groups, for `vcs access <path> @group`. |
| 188 | Group { |
| 189 | name: Option<String>, |
| 190 | /// Its people (this replaces them). |
| 191 | members: Vec<String>, |
| 192 | /// Remove the group. |
| 193 | #[arg(long)] |
| 194 | remove: bool, |
| 195 | }, |
| 196 | /// Print the guide for coding agents. |
| 197 | Guide, |
| 198 | #[command(hide = true)] |
| 199 | Saver, |
| 200 | } |
| 201 |
| 202 | #[derive(Subcommand)] |
| 203 | enum WorkspacesAction { |
| 204 | /// Another workspace (for another agent): a new change on main, or on --on <change> (`.` is this one's). |
| 205 | Add { |
| 206 | folder: PathBuf, |
| 207 | #[arg(long)] |
| 208 | on: Option<String>, |
| 209 | }, |
| 210 | /// Remove workspaces whose work is done: saved, empty and unused for --idle. |
| 211 | Clean { |
| 212 | #[arg(long, default_value = "2h")] |
| 213 | idle: String, |
| 214 | }, |
| 215 | } |
| 216 |
| 217 | fn run(app: &App, command: Command) -> Out { |
| 218 | match command { |
| 219 | Command::Get { project, folder, change, only } => cmd::get(app, &project, folder, change, only, None), |
| 220 | Command::Init { folder } => cmd::init(app, folder), |
| 221 | Command::Import { url, to, folder, change } => vcs_cli::remote::import(app, &url, to, folder, change), |
| 222 | Command::Publish { project } => vcs_cli::remote::publish(app, &project), |
| 223 | Command::Git { action } => vcs_cli::remote::git_view(app, action.is_some()), |
| 224 | Command::Login { host, token } => vcs_cli::remote::login(app, host, token), |
| 225 | Command::Status { change } => cmd::status(app, change), |
| 226 | Command::Diff { paths, change, from, to, stat } => cmd::diff_cmd(app, paths, change, from, to, stat), |
| 227 | Command::Log { what, changes, saves, ops, limit } => cmd::log(app, what, changes, saves, ops, limit), |
| 228 | Command::New { title, on } => cmd::new(app, title, on), |
| 229 | Command::Describe { parts } => cmd::describe(app, parts), |
| 230 | Command::Switch { change } => cmd::switch(app, &change), |
| 231 | Command::Drop { change } => cmd::drop(app, change), |
| 232 | Command::Sync => cmd::sync(app), |
| 233 | Command::Land { change, message, allow_secrets, no_run, at, with } => cmd::land(app, change, message, allow_secrets, !no_run, at, with), |
| 234 | Command::Undo { version } => cmd::undo(app, version), |
| 235 | Command::Restore { paths, at, from } => cmd::restore(app, paths, at, from), |
| 236 | Command::Access { target, who, edit } => access::run(app, &target, who, edit), |
| 237 | Command::Approve { change } => vcs_cli::remote::approve(app, &change), |
| 238 | Command::Settings { pairs } => vcs_cli::remote::settings(app, pairs), |
| 239 | Command::Blame { path } => vcs_cli::history::blame_cmd(app, path), |
| 240 | Command::Show { path, from } => cmd::show(app, path, from), |
| 241 | Command::Completions { shell } => { |
| 242 | clap_complete::generate(shell, &mut <Cli as clap::CommandFactory>::command(), "vcs", &mut std::io::stdout()); |
| 243 | Ok(vcs_cli::out::Report::new()) |
| 244 | } |
| 245 | Command::Check => vcs_cli::remote::check(app), |
| 246 | Command::Workspaces { action: None } => vcs_cli::workspaces::list(app), |
| 247 | Command::Workspaces { action: Some(WorkspacesAction::Add { folder, on }) } => vcs_cli::workspaces::add(app, folder, on), |
| 248 | Command::Workspaces { action: Some(WorkspacesAction::Clean { idle }) } => vcs_cli::workspaces::clean(app, &idle), |
| 249 | Command::Rollback { to, undo } => vcs_cli::remote::rollback(app, to, undo), |
| 250 | Command::Tag { name, version, remove } => vcs_cli::tags::run(app, name, version, remove), |
| 251 | Command::Group { name, members, remove } => vcs_cli::remote::group(app, name, members, remove), |
| 252 | Command::Guide => cmd::guide(), |
| 253 | Command::Saver => vcs_cli::remote::saver(app), |
| 254 | } |
| 255 | } |
| 256 |
| 257 | fn main() { |
| 258 | let cli = match Cli::try_parse() { |
| 259 | Ok(cli) => cli, |
| 260 | Err(e) => { |
| 261 | let code = if e.use_stderr() { 2 } else { 0 }; |
| 262 | let _ = e.print(); |
| 263 | std::process::exit(code); |
| 264 | } |
| 265 | }; |
| 266 | let json = cli.json || std::env::var("VCS_JSON").is_ok_and(|v| v == "1"); |
| 267 | let cwd = match cli.dir { |
| 268 | Some(d) => std::env::current_dir().map(|c| c.join(d)).unwrap_or_default(), |
| 269 | None => std::env::current_dir().unwrap_or_default(), |
| 270 | }; |
| 271 | let app = match App::open(json, cwd) { |
| 272 | Ok(app) => app, |
| 273 | Err(e) => { |
| 274 | Fail::from(e).print(json); |
| 275 | std::process::exit(1); |
| 276 | } |
| 277 | }; |
| 278 | let starts_saver = !matches!(cli.command, Command::Saver | Command::Guide | Command::Login { .. } | Command::Completions { .. }); |
| 279 | let result = run(&app, cli.command); |
| 280 | if starts_saver { |
| 281 | vcs_cli::saver::ensure_running(); |
| 282 | } |
| 283 | let notes = app.notes.borrow().clone(); |
| 284 | match result { |
| 285 | Ok(mut report) => { |
| 286 | if !notes.is_empty() { |
| 287 | report.set("notes", notes.clone()); |
| 288 | } |
| 289 | report.print(json); |
| 290 | if !json { |
| 291 | for n in ¬es { |
| 292 | eprintln!("note: {n}"); |
| 293 | } |
| 294 | } |
| 295 | std::process::exit(report.exit); |
| 296 | } |
| 297 | Err(mut fail) => { |
| 298 | if !notes.is_empty() { |
| 299 | fail = fail.set("notes", notes.clone()); |
| 300 | } |
| 301 | fail.print(json); |
| 302 | if !json { |
| 303 | for n in ¬es { |
| 304 | eprintln!("note: {n}"); |
| 305 | } |
| 306 | } |
| 307 | std::process::exit(fail.exit); |
| 308 | } |
| 309 | } |
| 310 | } |
| 311 |