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 or a change. |
| 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 | }, |
| 120 | /// Undo the last vcs command here, or take a landed version back out. |
| 121 | Undo { version: Option<u64> }, |
| 122 | /// Bring files back from an earlier point. |
| 123 | Restore { |
| 124 | #[arg(required = true)] |
| 125 | paths: Vec<String>, |
| 126 | /// From your own earlier save: "10 min ago", "2h ago", 15:30. |
| 127 | #[arg(long)] |
| 128 | at: Option<String>, |
| 129 | /// From a version of main, a change, or change:save. |
| 130 | #[arg(long)] |
| 131 | from: Option<String>, |
| 132 | }, |
| 133 | /// See or set who can see a change or a path. |
| 134 | Access { |
| 135 | target: String, |
| 136 | who: Vec<String>, |
| 137 | /// Also let them work on the change. |
| 138 | #[arg(long)] |
| 139 | edit: bool, |
| 140 | }, |
| 141 | /// Approve someone else's change (the last approval needed lands it). |
| 142 | Approve { change: String }, |
| 143 | /// The project's host settings, like `approvals=1`. |
| 144 | Settings { pairs: Vec<String> }, |
| 145 | /// Check that nothing is damaged: this machine's objects, and the host's copy. |
| 146 | Check, |
| 147 | /// This project's workspaces on this machine (one per agent): list, add, clean up. |
| 148 | Workspaces { |
| 149 | #[command(subcommand)] |
| 150 | action: Option<WorkspacesAction>, |
| 151 | }, |
| 152 | /// Put the whole hosted project back as it was at a time (owners; the last 30 days). |
| 153 | Rollback { |
| 154 | /// When: "2h ago", "yesterday", 2026-10-27 14:30. |
| 155 | #[arg(long, conflicts_with = "undo")] |
| 156 | to: Option<String>, |
| 157 | /// Reverse a rollback, with the bookmark it printed. |
| 158 | #[arg(long)] |
| 159 | undo: Option<String>, |
| 160 | }, |
| 161 | /// The project's groups, for `vcs access <path> @group`. |
| 162 | Group { |
| 163 | name: Option<String>, |
| 164 | /// Its people (this replaces them). |
| 165 | members: Vec<String>, |
| 166 | /// Remove the group. |
| 167 | #[arg(long)] |
| 168 | remove: bool, |
| 169 | }, |
| 170 | /// Print the guide for coding agents. |
| 171 | Guide, |
| 172 | #[command(hide = true)] |
| 173 | Saver, |
| 174 | } |
| 175 |
| 176 | #[derive(Subcommand)] |
| 177 | enum WorkspacesAction { |
| 178 | /// Another workspace (for another agent): a new change on main, or on --on <change> (`.` is this one's). |
| 179 | Add { |
| 180 | folder: PathBuf, |
| 181 | #[arg(long)] |
| 182 | on: Option<String>, |
| 183 | }, |
| 184 | /// Remove workspaces whose work is done: saved, empty and unused for --idle. |
| 185 | Clean { |
| 186 | #[arg(long, default_value = "2h")] |
| 187 | idle: String, |
| 188 | }, |
| 189 | } |
| 190 |
| 191 | fn run(app: &App, command: Command) -> Out { |
| 192 | match command { |
| 193 | Command::Get { project, folder, change, only } => cmd::get(app, &project, folder, change, only, None), |
| 194 | Command::Init { folder } => cmd::init(app, folder), |
| 195 | Command::Import { url, to, folder, change } => vcs_cli::remote::import(app, &url, to, folder, change), |
| 196 | Command::Publish { project } => vcs_cli::remote::publish(app, &project), |
| 197 | Command::Git { action } => vcs_cli::remote::git_view(app, action.is_some()), |
| 198 | Command::Login { host, token } => vcs_cli::remote::login(app, host, token), |
| 199 | Command::Status { change } => cmd::status(app, change), |
| 200 | Command::Diff { paths, change, from, to, stat } => cmd::diff_cmd(app, paths, change, from, to, stat), |
| 201 | Command::Log { what, changes, saves, ops, limit } => cmd::log(app, what, changes, saves, ops, limit), |
| 202 | Command::New { title, on } => cmd::new(app, title, on), |
| 203 | Command::Describe { parts } => cmd::describe(app, parts), |
| 204 | Command::Switch { change } => cmd::switch(app, &change), |
| 205 | Command::Drop { change } => cmd::drop(app, change), |
| 206 | Command::Sync => cmd::sync(app), |
| 207 | Command::Land { change, message, allow_secrets } => cmd::land(app, change, message, allow_secrets), |
| 208 | Command::Undo { version } => cmd::undo(app, version), |
| 209 | Command::Restore { paths, at, from } => cmd::restore(app, paths, at, from), |
| 210 | Command::Access { target, who, edit } => access::run(app, &target, who, edit), |
| 211 | Command::Approve { change } => vcs_cli::remote::approve(app, &change), |
| 212 | Command::Settings { pairs } => vcs_cli::remote::settings(app, pairs), |
| 213 | Command::Check => vcs_cli::remote::check(app), |
| 214 | Command::Workspaces { action: None } => vcs_cli::workspaces::list(app), |
| 215 | Command::Workspaces { action: Some(WorkspacesAction::Add { folder, on }) } => vcs_cli::workspaces::add(app, folder, on), |
| 216 | Command::Workspaces { action: Some(WorkspacesAction::Clean { idle }) } => vcs_cli::workspaces::clean(app, &idle), |
| 217 | Command::Rollback { to, undo } => vcs_cli::remote::rollback(app, to, undo), |
| 218 | Command::Group { name, members, remove } => vcs_cli::remote::group(app, name, members, remove), |
| 219 | Command::Guide => cmd::guide(), |
| 220 | Command::Saver => vcs_cli::remote::saver(app), |
| 221 | } |
| 222 | } |
| 223 |
| 224 | fn main() { |
| 225 | let cli = match Cli::try_parse() { |
| 226 | Ok(cli) => cli, |
| 227 | Err(e) => { |
| 228 | let code = if e.use_stderr() { 2 } else { 0 }; |
| 229 | let _ = e.print(); |
| 230 | std::process::exit(code); |
| 231 | } |
| 232 | }; |
| 233 | let json = cli.json || std::env::var("VCS_JSON").is_ok_and(|v| v == "1"); |
| 234 | let cwd = match cli.dir { |
| 235 | Some(d) => std::env::current_dir().map(|c| c.join(d)).unwrap_or_default(), |
| 236 | None => std::env::current_dir().unwrap_or_default(), |
| 237 | }; |
| 238 | let app = match App::open(json, cwd) { |
| 239 | Ok(app) => app, |
| 240 | Err(e) => { |
| 241 | Fail::from(e).print(json); |
| 242 | std::process::exit(1); |
| 243 | } |
| 244 | }; |
| 245 | let starts_saver = !matches!(cli.command, Command::Saver | Command::Guide | Command::Login { .. }); |
| 246 | let result = run(&app, cli.command); |
| 247 | if starts_saver { |
| 248 | vcs_cli::saver::ensure_running(); |
| 249 | } |
| 250 | let notes = app.notes.borrow().clone(); |
| 251 | match result { |
| 252 | Ok(mut report) => { |
| 253 | if !notes.is_empty() { |
| 254 | report.set("notes", notes.clone()); |
| 255 | } |
| 256 | report.print(json); |
| 257 | if !json { |
| 258 | for n in ¬es { |
| 259 | eprintln!("note: {n}"); |
| 260 | } |
| 261 | } |
| 262 | std::process::exit(report.exit); |
| 263 | } |
| 264 | Err(mut fail) => { |
| 265 | if !notes.is_empty() { |
| 266 | fail = fail.set("notes", notes.clone()); |
| 267 | } |
| 268 | fail.print(json); |
| 269 | if !json { |
| 270 | for n in ¬es { |
| 271 | eprintln!("note: {n}"); |
| 272 | } |
| 273 | } |
| 274 | std::process::exit(fail.exit); |
| 275 | } |
| 276 | } |
| 277 | } |
| 278 |