use clap::{Parser, Subcommand}; mod commands; mod config; mod guess; mod guide; mod resume; use std::path::PathBuf; #[derive(Parser)] #[command( name = "nd", version, about = "push/pull vibecoded projects with rich, searchable metadata to a notdoublehub server", long_about = guide::LONG_ABOUT )] struct Cli { /// hub url; overrides ND_SERVER/config. default: the public hub #[arg(long, global = true)] server: Option, /// bearer token; overrides ND_TOKEN/config #[arg(long, global = true)] token: Option, #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { /// create a notdouble.toml manifest (and .ndignore) in a project Init { /// project dir (default: current dir) path: Option, /// overwrite an existing manifest #[arg(long)] force: bool, /// guess manifest fields by inspecting the repo (Cargo.toml, /// package.json, pyproject, go.mod, README, git config) #[arg(long)] guess: bool, }, /// validate a manifest and print searchability tips Validate { path: Option, #[arg(long)] json: bool, }, /// preview what would be packed (default), or write the bundle with -o Pack { path: Option, /// write the tar.gz bundle here instead of just listing #[arg(long)] out: Option, /// also show which ignore rule excluded each file #[arg(long)] explain: bool, /// compact rollup: per top-level dir + 10 largest files #[arg(long)] summary: bool, }, /// show resolved config: which hub, identity, token, reachability Status, /// compare a local project dir with the published bundle Diff { slug: String, /// hash-level three-way preview (exactly what nd pull would do) #[arg(long)] content: bool, /// line-level unified diff (local dir vs the hub copy) #[arg(long)] patch: bool, /// project dir (default: current dir) path: Option, }, /// pack + push a project (auto-creates or updates by slug) Upload { path: Option, /// force through the big-upload guard #[arg(long)] yes: bool, /// include machine specs (os, cpu, ram, gpu) without asking #[arg(long)] specs: bool, /// never include machine specs (skip the prompt) #[arg(long)] no_specs: bool, /// show what would be packed and where it would go, then stop #[arg(long)] dry_run: bool, #[arg(long)] json: bool, }, /// download a project's full source Download { slug: String, /// destination dir (default: ./) dest: Option, #[arg(long)] overwrite: bool, }, /// fork a project (from a hub ref, or the dir you're standing in) Fork { /// hub ref (owner/slug) — or a project dir to fork in place source: Option, /// name for the fork (default: -fork) #[arg(long)] name: Option, /// publish the fork immediately (you become its maintainer) #[arg(long)] upload: bool, }, /// run a project: inside its dir (`nd run`), or fetch+cache a ref /// (`nd run owner/slug`) Run { /// project ref (omit to run the project in the current dir) r#ref: Option, /// re-download the bundle even if cached #[arg(long)] refresh: bool, /// require a bundled bin/-- binary (error if absent) #[arg(long)] bin: bool, /// force the manifest's `run` command (ignore any bundled binary) #[arg(long)] shell: bool, /// extra args passed to the binary/run command (no `--` needed) #[arg(trailing_var_arg = true)] args: Vec, }, /// pull the hub copy into a project dir (three-way merge; never /// silently overwrites local edits) Pull { /// ref to pull (default: the dir's tracked nd_ref, else its name) #[arg(long, name = "ref")] ref_: Option, /// project dir (default: current dir) path: Option, /// on conflicts, take the hub copy #[arg(long)] force: bool, }, /// search projects (names, descriptions, keywords, code keywords, ...) Search { query: String, #[arg(long, default_value_t = 20)] limit: usize, /// include forked projects in the results #[arg(long)] with_forks: bool, #[arg(long)] json: bool, }, /// show full metadata for one project (use --json for machines) Info { slug: String, #[arg(long)] json: bool, }, /// post a review of a project you downloaded and tried Review { slug: String, /// works | issues | broken | notes (free text allowed) #[arg(long, default_value = "notes")] verdict: String, /// review text; reads stdin if omitted and stdin is piped #[arg(long, short = 'm')] message: Option, /// name shown on the review (your name or your model's) #[arg(long, default_value = "anonymous")] author: String, }, /// read the reviews on a project Reviews { slug: String, #[arg(long)] json: bool, }, /// post a maintainer update on a project page (signed) Update { slug: String, /// headline (e.g. "v1.1.0 — faster parser") #[arg(long)] title: String, /// body text; reads stdin if omitted and stdin is piped #[arg(long, short = 'm')] message: Option, }, /// read a project's updates Updates { slug: String, #[arg(long)] json: bool, }, /// projects that look like this one ("doubles") Doubles { slug: String, #[arg(long)] json: bool, }, /// list the forks of a project (with each fork's maintainer) Forks { slug: String, #[arg(long)] json: bool, }, /// (maintainers) pull a fork's changes into your project dir, /// three-way merged — resolve, review, then nd upload Merge { /// the fork to merge (owner/slug) fork_ref: String, /// project dir (default: current dir) path: Option, /// resolve conflicts toward the fork #[arg(long)] force: bool, }, /// delete a project from the hub (needs --yes when non-interactive) Delete { slug: String, /// skip the confirmation prompt #[arg(long)] yes: bool, }, /// list projects you own on the hub (or all with --all) Ls { /// show every project, not just yours #[arg(long)] all: bool, #[arg(long)] json: bool, }, /// build a maintainership resume from all of an owner's projects Resume { /// owner id (8-char key; default: you) — works for any maintainer owner: Option, /// markdown, ready to paste into a resume #[arg(long)] markdown: bool, #[arg(long)] json: bool, }, /// bump the manifest's version for a release (patch by default) Bump { path: Option, #[arg(long)] patch: bool, #[arg(long)] minor: bool, #[arg(long)] major: bool, }, /// print your nd identity (public key used for ownership) Whoami { /// also print the SECRET seed so you can back it up somewhere safe #[arg(long)] export: bool, }, /// hand a project to another maintainer's public key (signed) Transfer { slug: String, /// base64 ed25519 public key (`nd whoami` prints yours) new_owner: String, }, /// replace this nd binary with the latest one published on the hub UpdateNd { /// replace even if versions match #[arg(long)] force: bool, }, /// print a compact usage guide written for llm agents Llmhelp, } fn main() { let cli = Cli::parse(); if let Err(e) = dispatch(cli.cmd, cli.server, cli.token) { eprintln!("error: {e}"); for cause in e.chain().skip(1) { eprintln!(" caused by: {cause}"); } std::process::exit(1); } } fn dispatch(cmd: Cmd, server: Option, token: Option) -> anyhow::Result<()> { match cmd { Cmd::Init { path, force, guess } => commands::init(path, force, guess), Cmd::Validate { path, json } => commands::validate(path, json), Cmd::Pack { path, out, explain, summary } => commands::pack(path, out, explain, summary), Cmd::Status => commands::status(server), Cmd::Diff { slug, path, content, patch } => { commands::diff(&slug, path, content, patch, server, token) } Cmd::Upload { path, yes, specs, no_specs, dry_run, json } => { commands::upload(path, yes, server, token, specs, no_specs, dry_run, json) } Cmd::Download { slug, dest, overwrite } => { commands::download(&slug, dest, server, token, overwrite) } Cmd::Fork { source, name, upload } => commands::fork(source, name, upload, server, token), Cmd::Run { r#ref, refresh, bin, shell, args } => { commands::run(r#ref, refresh, &args, bin, shell, server, token) } Cmd::Pull { ref_, path, force } => { commands::pull(ref_, path, force, false, true, server, token) } Cmd::Search { query, limit, with_forks, json } => { commands::search(&query, limit, with_forks, server, json) } Cmd::Info { slug, json } => commands::info(&slug, server, token, json), Cmd::Review { slug, verdict, message, author } => { commands::review(&slug, &verdict, message, &author, server, token) } Cmd::Reviews { slug, json } => commands::reviews(&slug, server, json), Cmd::Update { slug, title, message } => { commands::post_update(&slug, &title, message, server, token) } Cmd::Updates { slug, json } => commands::updates_list(&slug, server, json), Cmd::Doubles { slug, json } => commands::doubles(&slug, server, json), Cmd::Forks { slug, json } => commands::forks(&slug, server, json), Cmd::Merge { fork_ref, path, force } => { commands::merge(&fork_ref, path, force, server, token) } Cmd::Delete { slug, yes } => commands::delete(&slug, yes, server, token), Cmd::Ls { all, json } => commands::ls(all, server, json), Cmd::Resume { owner, markdown, json } => commands::resume(owner, markdown, json, server), Cmd::Bump { path, minor, major, .. } => { let kind = if major { "major" } else if minor { "minor" } else { "patch" }; commands::bump(path, kind) } Cmd::Whoami { export } => commands::whoami(export), Cmd::Transfer { slug, new_owner } => commands::transfer(&slug, &new_owner, server, token), Cmd::UpdateNd { force } => commands::update_nd(force, server, token), Cmd::Llmhelp => { print!("{}", guide::LLM_GUIDE); Ok(()) } } }