1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
//! Clap derive definition for the `upskill` CLI.
//!
//! Lives in the library crate so the man-page generator
//! (`examples/mangen.rs`) and integration tests can reach the same
//! `Command` tree the binary parses against. `main.rs` matches on
//! `Commands` and dispatches.
use std::path::PathBuf;
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "upskill")]
#[command(version)]
#[command(about = "Author and distribute AI-assistance content across coding agents")]
#[command(
after_help = "DOCUMENTATION:\n https://driftsys.github.io/upskill/\n\n\
REPORT BUGS:\n https://github.com/driftsys/upskill/issues"
)]
pub struct Cli {
/// Disable colored output. Honored alongside `NO_COLOR`,
/// `UPSKILL_NO_COLOR`, `TERM=dumb`, and TTY auto-detection.
#[arg(long = "no-color", global = true)]
pub no_color: bool,
/// Suppress informational stdout. Errors on stderr and exit codes
/// are unaffected. Useful for CI use of `lint`, `update --dry-run`,
/// etc.
#[arg(short = 'q', long = "quiet", global = true)]
pub quiet: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Install rules / skills / agents from a source.
///
/// Parses each item from the source, renders per-client output, and
/// records the install in `.upskill-lock.json`. Default scope is the
/// current project; falls back to global (`$HOME`) when `cwd` is not
/// inside a git repo.
#[command(after_help = "EXAMPLES:\n \
upskill add owner/repo\n \
upskill add owner/repo@v1.2\n \
upskill add owner/repo:skills/code-review\n \
upskill add owner/repo code-review secret-scanner\n \
upskill add gitlab:team/repo\n \
upskill add ./local-source\n \
upskill add owner/repo --global")]
Add {
/// Source: `owner/repo[@ref][:subfolder]`, full https URL, or local path.
source: String,
/// Optional subset filter — only items whose name matches one of
/// these is installed. Empty means install everything in the
/// source (the default).
items: Vec<String>,
/// Install into `$HOME` instead of the current directory.
#[arg(short = 'g', long = "global", conflicts_with = "project")]
global: bool,
/// Force project scope (current directory). Overrides the auto-detect
/// fallback to global when `cwd` is not inside a git repo.
#[arg(short = 'p', long = "project")]
project: bool,
},
/// Remove installed content.
///
/// Either name one or more items, or pass `--source <label>` to
/// remove every item that came from a single source. Bare
/// `upskill remove` is rejected — be explicit. Ancillary files
/// (`CLAUDE.md`, `opencode.json`, `.vscode/settings.json`) are not
/// touched.
#[command(after_help = "EXAMPLES:\n \
upskill remove code-review\n \
upskill remove rule-a skill-b agent-c\n \
upskill remove --source github:owner/repo\n \
upskill remove --global code-review")]
Remove {
/// Item names to remove. Mutually exclusive with `--source`.
names: Vec<String>,
/// Remove every item whose lockfile `source` label matches this
/// string. Use the value reported by the install or shown in
/// `.upskill-lock.json` (e.g. `local:/path` or
/// `github:owner/repo`).
#[arg(short = 's', long = "source")]
source: Option<String>,
/// Operate on `$HOME` instead of the current directory.
#[arg(short = 'g', long = "global", conflicts_with = "project")]
global: bool,
/// Force project scope (current directory). Overrides the auto-detect
/// fallback to global when `cwd` is not inside a git repo.
#[arg(short = 'p', long = "project")]
project: bool,
/// Skip the confirmation prompt that `--source` shows on a TTY.
/// Already implicit when stdin is not a terminal (CI / pipes).
#[arg(short = 'y', long = "yes")]
yes: bool,
},
/// Pull latest sources and regenerate changed items.
///
/// Re-fetches the source for every (or just the named) lockfile
/// entries and reinstalls those sources. With `--dry-run`, hashes
/// the new SSOT and reports what would change without writing.
/// `update` always fetches.
#[command(after_help = "EXAMPLES:\n \
upskill update\n \
upskill update code-review\n \
upskill update --dry-run\n \
upskill update --global")]
Update {
/// Item names to update (omit to update everything).
names: Vec<String>,
/// Report what would change without writing.
#[arg(short = 'n', long = "dry-run")]
dry_run: bool,
/// Operate on `$HOME` instead of the current directory.
#[arg(short = 'g', long = "global", conflicts_with = "project")]
global: bool,
/// Force project scope (current directory). Overrides the auto-detect
/// fallback to global when `cwd` is not inside a git repo.
#[arg(short = 'p', long = "project")]
project: bool,
},
/// List installed content recorded in `.upskill-lock.json`.
///
/// Items are grouped by kind (rules, skills, agents). Bundles, when
/// present, are surfaced as a separate section. The command never
/// fetches and never inspects per-client output files — for that, run
/// `upskill doctor`.
#[command(after_help = "EXAMPLES:\n \
upskill list\n \
upskill list --global")]
List {
/// Read `$HOME/.upskill-lock.json` instead of the current directory.
#[arg(short = 'g', long = "global", conflicts_with = "project")]
global: bool,
/// Force project scope (current directory). Overrides the auto-detect
/// fallback to global when `cwd` is not inside a git repo.
#[arg(short = 'p', long = "project")]
project: bool,
/// Emit a stable JSON document instead of the human-readable
/// grouping. See `docs/commands.md` for the schema.
#[arg(long = "json")]
json: bool,
},
/// Verify installed-state consistency.
///
/// Three independent drift buckets:
/// - missing per-client output files (reinstall fixes)
/// - SSOT hash drift on `local:` sources (`update` fixes)
/// - lockfile entries with no recoverable source (manual `remove`)
///
/// Doctor never fetches; remote-source drift detection is
/// `update --dry-run`. Exit 0 when clean, 1 when any drift is found.
#[command(after_help = "EXAMPLES:\n \
upskill doctor\n \
upskill doctor --global")]
Doctor {
/// Operate on `$HOME` instead of the current directory.
#[arg(short = 'g', long = "global", conflicts_with = "project")]
global: bool,
/// Force project scope (current directory). Overrides the auto-detect
/// fallback to global when `cwd` is not inside a git repo.
#[arg(short = 'p', long = "project")]
project: bool,
/// Emit the three drift buckets as a stable JSON document. Exit
/// code is unchanged (0 clean, 1 drifted). See `docs/commands.md`
/// for the schema.
#[arg(long = "json")]
json: bool,
},
/// Search the public skills registry.
#[command(after_help = "EXAMPLES:\n \
upskill search code-review\n \
upskill search api --limit 5")]
Search {
/// Search query.
query: String,
/// Maximum number of results.
#[arg(short = 'l', long, default_value = "10")]
limit: usize,
},
/// Validate SSOT files against the format spec.
///
/// Author command — runs only inside a source registry. Refuses to
/// run inside a consumer project (detected by `.upskill-lock.json`).
/// Default mode emits warnings and exits 0 unless an error rule
/// fires; `--strict` promotes warnings to errors (CI mode). With no
/// paths, lints the current directory.
#[command(after_help = "EXAMPLES:\n \
upskill lint\n \
upskill lint rules/\n \
upskill lint --strict")]
Lint {
/// Files or directories to lint. Empty = current directory.
paths: Vec<PathBuf>,
/// Promote warnings to errors. Use in CI.
#[arg(short = 's', long)]
strict: bool,
},
/// Canonicalise YAML frontmatter in SSOT files.
///
/// Author command — runs only inside a source registry. Body
/// markdown is left untouched (dprint's job). Refuses to run inside
/// a consumer project (detected by `.upskill-lock.json`). With no
/// paths, formats the current directory.
#[command(after_help = "EXAMPLES:\n \
upskill fmt\n \
upskill fmt rules/")]
Fmt {
/// Files or directories to format. Empty = current directory.
paths: Vec<PathBuf>,
},
/// Scaffold a new rule, skill, or agent.
///
/// Writes the minimum frontmatter required plus kind-specific
/// defaults (e.g. `mode: subagent` / `model: sonnet` for agents)
/// into `<cwd>/<kind>s/<name>/<KIND>.md`. Author command — refuses
/// to run inside a consumer project.
#[command(after_help = "EXAMPLES:\n \
upskill new rule no-direct-database-access\n \
upskill new skill code-review\n \
upskill new agent security-reviewer")]
New {
/// One of `rule`, `skill`, `agent`.
kind: String,
/// Item name. Lowercase letters, digits, hyphens; max 64 chars.
name: String,
},
}