use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Parser)]
pub struct Cli {
#[arg(
long,
global = true,
env = "KNOT_STATIC_CONFIG",
default_value = "knot-static.toml"
)]
pub config: PathBuf,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// create the knot's identity, meta repo and signing key
Init,
Repo {
#[command(subcommand)]
command: RepoCommand,
},
Plc {
#[command(subcommand)]
command: PlcCommand,
},
/// answer one write request under nginx and fcgiwrap
Cgi,
/// manage issues as the repo's owner
Issue {
#[command(subcommand)]
command: IssueCommand,
},
/// manage label definitions as the repo's owner
Label {
#[command(subcommand)]
command: LabelCommand,
},
/// manage pull requests as the repo's owner
Pull {
#[command(subcommand)]
command: PullCommand,
},
/// build a new generation without switching traffic to it
Generate {
/// only rebuild this repo did and carry the rest over
#[arg(long)]
repo: Option<String>,
},
/// validate a finished generation and switch traffic to it
Activate {
generation: String,
/// roll back to an older generation and skip the stale check
#[arg(long)]
rollback: bool,
},
/// remove unreferenced generations, releases and cache entries
Gc {
/// how many rollback generations to keep besides the active one
#[arg(long)]
keep: Option<usize>,
/// also remove generations retired workers may still be draining
#[arg(long)]
force: bool,
},
#[command(name = "oauth")]
OAuth {
#[command(subcommand)]
command: OAuthCommand,
},
/// ssh ForceCommand entry point for fetches and pushes
GitSsh {
/// accounts the pushing key belongs to, from `keys list`
#[arg(long = "actor", conflicts_with = "operator")]
actors: Vec<String>,
/// operator key allowed to push to any repo
#[arg(long)]
operator: bool,
},
/// manage ssh keys of accounts allowed to push
Keys {
#[command(subcommand)]
command: KeysCommand,
},
/// write one record from stdin as the repo's owner and publish a new generation
Write {
/// createRecord | putRecord | deleteRecord | submitPatch
method: String,
#[arg(long)]
repo: String,
/// write as this account instead of the repo's owner
#[arg(long = "as")]
actor: Option<String>,
},
/// run post-activation crawl, pds declaration, spindle and ssh key tasks
PostActivate {
/// repo dids that changed in this activation
#[arg(long = "repo")]
repos: Vec<String>,
/// skip updating owner declarations on their pds
#[arg(long)]
no_declarations: bool,
},
/// start the hooks and collect garbage after a write's activation
#[command(hide = true)]
AfterActivation {
generation: String,
#[arg(long)]
previous: Option<String>,
/// when the activation happened, in unix seconds
#[arg(long)]
at: u64,
},
/// after a deploy, lay out what this binary serves from and publish a generation it built
#[command(hide = true)]
Prepare,
/// ask configured crawlers to crawl this knot now
Crawl,
/// run repack, gc or prune on one or all hosted repos under the build lock
Maintenance {
/// repo did to maintain, from `repo list`
#[arg(long, required_unless_present = "all", conflicts_with = "all")]
repo: Option<String>,
/// maintain every hosted repo one after another
#[arg(long)]
all: bool,
/// repack | gc | prune
#[arg(long, default_value = "gc")]
command: String,
},
}
#[derive(Subcommand)]
pub enum KeysCommand {
/// fetch ssh keys of accounts allowed to push
Sync {
/// only fetch accounts without stored keys and drop removed accounts
#[arg(long)]
new: bool,
},
/// print authorized_keys lines for sshd's `AuthorizedKeysCommand`
List {
/// wrapper command each key is forced to run (`--actor DID` is appended)
#[arg(long)]
command: String,
},
}
#[derive(Subcommand)]
pub enum RepoCommand {
Import(ImportArgs),
Create(CreateArgs),
/// print `repoDid owner rkey name head source-path` for each hosted repo
List,
/// print or publish the owner's `org.tangled.repo.declaration` record
Record(RecordArgs),
/// update a repo's description, website, topics, policy or spindle
Settings(SettingsArgs),
/// rename a repo and move its declaration on the owner's pds
Rename(RenameArgs),
/// manage who else can push to a repo
Collaborator {
#[command(subcommand)]
command: CollaboratorCommand,
},
}
#[derive(Subcommand)]
pub enum CollaboratorCommand {
Add {
/// repo did or rkey
repo: String,
did: String,
},
Rm {
repo: String,
did: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum ImportFrom {
Knot2,
}
#[derive(Args)]
pub struct ImportArgs {
/// owner did (defaults to the source registry/manifest cob or `[knot].owner`)
#[arg(long)]
pub owner: Option<String>,
/// repo record key (defaults to the source registry/manifest cob or `--name`)
#[arg(long)]
pub rkey: Option<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub default_branch: Option<String>,
/// optional `at://` source uri stored in the registry
#[arg(long)]
pub source: Option<String>,
/// adopt an existing `did:plc` instead of minting a new one
#[arg(long)]
pub did: Option<String>,
/// migrate from another knot (`knot2`), reading its state/config and deregistering the repo there
#[arg(long, value_enum, requires = "did")]
pub from: Option<ImportFrom>,
/// env var holding the current 32-byte hex/base64 plc rotation key for `--did`
#[arg(long, requires = "did")]
pub rotation_key_env: Option<String>,
/// sealed key store holding the current plc rotation key for `--did`
#[arg(long, requires = "did")]
pub from_sealed_store: Option<PathBuf>,
/// master key env var for `--from-sealed-store` or `--from knot2`
#[arg(long, requires = "did")]
pub from_master_key_env: Option<String>,
/// source knot did (`did:web:...`) for `--from-sealed-store` or `--from knot2`
#[arg(long, requires = "did")]
pub from_knot_did: Option<String>,
/// import even when git lfs pointer files are present
#[arg(long)]
pub allow_lfs_pointers: bool,
/// worktree, bare repo, or source knot config/state path to import
#[arg(
long = "path",
value_name = "PATH",
required_unless_present = "positional_path",
conflicts_with = "positional_path"
)]
pub flag_path: Option<PathBuf>,
/// worktree, bare repo, or source knot config/state path to import
#[arg(value_name = "PATH", required_unless_present = "flag_path")]
pub positional_path: Option<PathBuf>,
}
impl ImportArgs {
pub fn path(&self) -> &std::path::Path {
self.flag_path
.as_deref()
.or(self.positional_path.as_deref())
.expect("clap requires --path or positional PATH")
}
}
#[derive(Args)]
pub(crate) struct RecordArgs {
pub repo_did: String,
/// also write the record to the owner's pds over oauth
#[arg(long)]
pub publish: bool,
}
#[derive(Args)]
pub struct CreateArgs {
#[arg(long)]
pub owner: String,
#[arg(long)]
pub rkey: String,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub default_branch: Option<String>,
}
#[derive(Subcommand)]
pub enum PlcCommand {
/// retry pending plc submissions
Sync,
/// hand a hosted repo's did:plc over to another knot
Handover(HandoverArgs),
}
#[derive(Args)]
pub struct HandoverArgs {
/// repo did or rkey
pub repo: String,
/// target knot base url (e.g. `https://knot.example.com`)
#[arg(long)]
pub to_url: String,
/// target knot `#atproto` key (`zQ3sh...` or `did:key:zQ3sh...`), or omit to fetch `<to-url>/.well-known/did.json`
#[arg(long)]
pub to_key: Option<String>,
}
#[derive(Subcommand)]
pub enum OAuthCommand {
/// sign in with the owner's pds in a browser
Login {
#[arg(long)]
owner: Option<String>,
},
/// check stored oauth session status
Status,
}
#[derive(Subcommand)]
pub enum IssueCommand {
New {
repo: String,
#[arg(long)]
title: String,
#[arg(long, conflicts_with = "body_file")]
body: Option<String>,
#[arg(long)]
body_file: Option<PathBuf>,
},
Comment {
repo: String,
number: String,
#[arg(long, conflicts_with = "body_file")]
body: Option<String>,
#[arg(long)]
body_file: Option<PathBuf>,
/// rkey of the comment being replied to
#[arg(long)]
reply_to: Option<String>,
},
Close {
repo: String,
number: String,
/// mark as completed instead of closed
#[arg(long)]
completed: bool,
},
Reopen {
repo: String,
number: String,
},
Edit {
repo: String,
number: String,
#[arg(long)]
title: Option<String>,
#[arg(long, conflicts_with = "body_file")]
body: Option<String>,
#[arg(long)]
body_file: Option<PathBuf>,
},
/// add or remove a label by its slug
Label {
repo: String,
number: String,
action: LabelAction,
label: String,
},
}
#[derive(Clone, Copy, ValueEnum)]
pub enum LabelAction {
Add,
Rm,
}
#[derive(Subcommand)]
pub enum LabelCommand {
Add {
repo: String,
name: String,
#[arg(long)]
color: Option<String>,
/// record key for the label (defaults to its name)
#[arg(long)]
slug: Option<String>,
},
Rm {
repo: String,
slug: String,
},
}
#[derive(Subcommand)]
pub enum PullCommand {
New {
repo: String,
#[arg(long)]
title: String,
#[arg(long, conflicts_with = "body_file")]
body: Option<String>,
#[arg(long)]
body_file: Option<PathBuf>,
/// branch the pull request merges into
#[arg(long)]
target: String,
/// branch the changes come from
#[arg(long)]
branch: String,
/// fork repo did when the branch comes from a fork
#[arg(long)]
from: Option<String>,
},
/// add or remove a label by its slug
Label {
repo: String,
number: String,
action: LabelAction,
label: String,
},
}
#[derive(Args)]
pub struct SettingsArgs {
pub repo: String,
#[arg(long)]
pub description: Option<String>,
#[arg(long)]
pub website: Option<String>,
#[arg(long = "topic")]
pub topics: Vec<String>,
/// anyone | members | collaborators
#[arg(long)]
pub policy: Option<String>,
/// spindle (`did:web`) that runs ci for this repo
#[arg(long)]
pub spindle: Option<String>,
/// stop sending push events to a spindle
#[arg(long, conflicts_with = "spindle")]
pub no_spindle: bool,
}
#[derive(Args)]
pub struct RenameArgs {
/// repo did or current name/rkey
pub repo: String,
/// new repository name
pub new_name: String,
}