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
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug)]
#[command(
name = "skillctl",
version,
about = "Manage your personal Claude skills library across projects."
)]
pub struct Cli {
/// Force non-interactive mode. Required decisions must come from flags;
/// the CLI will not fall back to a prompt. Auto-enabled when stdin or
/// stdout isn't a TTY, and implied by --json.
#[arg(long, global = true)]
pub no_interaction: bool,
/// Emit a structured JSON object to stdout (per-command schema documented
/// in skillctl-usage). Implies --no-interaction; suppresses the
/// human-readable cliclack output.
#[arg(long, global = true)]
pub json: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Configure the skills library repo to pull from.
Init(InitArgs),
/// List all skills available in the configured library.
List(ListArgs),
/// Select skills to install in the current project.
Add(AddArgs),
/// Push local edits to installed skills back to the library.
Push(PushArgs),
/// Pull library updates into installed skills.
Pull(PullArgs),
/// Find skills created locally and offer to add them to the library.
Detect(DetectArgs),
}
#[derive(Args, Debug)]
pub struct InitArgs {
/// GitHub URL of the skills library (e.g. https://github.com/owner/repo).
pub url: String,
}
#[derive(Args, Debug)]
pub struct ListArgs {
/// Show only skills carrying this tag. Repeatable; default semantics is
/// union (any of the given tags).
#[arg(long = "tag", value_name = "TAG")]
pub tags: Vec<String>,
/// Switch tag matching from union (any) to intersection (all) when
/// multiple `--tag` flags are passed. Has no effect without `--tag`.
#[arg(long, requires = "tags")]
pub all_tags: bool,
}
#[derive(Args, Debug)]
pub struct AddArgs {
/// Skill name to install. Repeatable. Mutually exclusive with --all and --tag.
#[arg(long = "skill", value_name = "NAME", conflicts_with_all = ["all", "tags"])]
pub skills: Vec<String>,
/// Install every skill from the library.
#[arg(long, conflicts_with_all = ["skills", "tags"])]
pub all: bool,
/// Install every skill carrying this tag. Repeatable; default semantics is
/// union (any of the given tags). Mutually exclusive with --skill and --all.
#[arg(long = "tag", value_name = "TAG", conflicts_with_all = ["skills", "all"])]
pub tags: Vec<String>,
/// Switch tag matching from union (any) to intersection (all) when
/// multiple `--tag` flags are passed. Has no effect without `--tag`.
#[arg(long, requires = "tags")]
pub all_tags: bool,
/// Install destination relative to the project root (e.g. `.claude/skills`).
/// Required in non-interactive mode unless an existing destination is
/// implicitly chosen by the auto-detection (currently never).
#[arg(long, value_name = "PATH")]
pub dest: Option<PathBuf>,
/// Resolution strategy when an install destination already exists.
/// Required in non-interactive mode if any conflict is encountered.
#[arg(long, value_enum, value_name = "POLICY")]
pub on_conflict: Option<OnConflict>,
}
#[derive(Args, Debug)]
pub struct PushArgs {
/// Skill name to push. Repeatable. Mutually exclusive with --all and --tag.
#[arg(long = "skill", value_name = "NAME", conflicts_with_all = ["all", "tags"])]
pub skills: Vec<String>,
/// Push every skill that has pushable changes (LocalChangesOnly + diverged + library-missing).
#[arg(long, conflicts_with_all = ["skills", "tags"])]
pub all: bool,
/// Push every pushable skill carrying this tag. Repeatable; default
/// semantics is union (any of the given tags). Mutually exclusive with
/// --skill and --all. Tags are read from each skill's local SKILL.md.
#[arg(long = "tag", value_name = "TAG", conflicts_with_all = ["skills", "all"])]
pub tags: Vec<String>,
/// Switch tag matching from union (any) to intersection (all) when
/// multiple `--tag` flags are passed. Has no effect without `--tag`.
#[arg(long, requires = "tags")]
pub all_tags: bool,
/// Resolution strategy for divergent (and library-missing) skills:
/// `skip` / `overwrite` / `fork`. `fork` requires `--fork-suffix` in
/// non-interactive mode.
#[arg(long, value_enum, value_name = "POLICY")]
pub on_divergence: Option<OnDivergence>,
/// Suffix appended to the original skill name when forking
/// non-interactively (e.g. `--fork-suffix custom` → `<name>-custom`).
/// Required when `--on-divergence fork` is used without a TTY.
#[arg(long, value_name = "SUFFIX")]
pub fork_suffix: Option<String>,
/// Override the auto-generated commit message.
#[arg(long, value_name = "MESSAGE")]
pub message: Option<String>,
}
#[derive(Args, Debug)]
pub struct PullArgs {
/// Skill name to pull. Repeatable. Mutually exclusive with --all and --tag.
#[arg(long = "skill", value_name = "NAME", conflicts_with_all = ["all", "tags"])]
pub skills: Vec<String>,
/// Pull every skill that has library updates available (LibraryAhead + diverged).
#[arg(long, conflicts_with_all = ["skills", "tags"])]
pub all: bool,
/// Pull every pullable skill carrying this tag. Repeatable; default
/// semantics is union (any of the given tags). Mutually exclusive with
/// --skill and --all. Tags are read from each skill's local SKILL.md.
#[arg(long = "tag", value_name = "TAG", conflicts_with_all = ["skills", "all"])]
pub tags: Vec<String>,
/// Switch tag matching from union (any) to intersection (all) when
/// multiple `--tag` flags are passed. Has no effect without `--tag`.
#[arg(long, requires = "tags")]
pub all_tags: bool,
/// Resolution strategy for divergent skills: `skip` / `overwrite` /
/// `fork`. `fork` here means **fork-locally**: rename the existing local
/// folder under a new name, then pull the library version into the
/// original destination. Requires `--fork-suffix` in non-interactive mode.
#[arg(long, value_enum, value_name = "POLICY")]
pub on_divergence: Option<OnDivergence>,
/// Suffix appended to the original skill name when fork-locally is used
/// non-interactively (e.g. `--fork-suffix local` → `<name>-local`).
/// Required when `--on-divergence fork` is used without a TTY.
#[arg(long, value_name = "SUFFIX")]
pub fork_suffix: Option<String>,
}
#[derive(Args, Debug)]
pub struct DetectArgs {
/// Name of a new local skill to add. Repeatable. Mutually exclusive with --all and --tag.
#[arg(long = "skill", value_name = "NAME", conflicts_with_all = ["all", "tags"])]
pub skills: Vec<String>,
/// Add every detected new skill.
#[arg(long, conflicts_with_all = ["skills", "tags"])]
pub all: bool,
/// Add every newly detected skill carrying this tag. Repeatable; default
/// semantics is union (any of the given tags). Mutually exclusive with
/// --skill and --all.
#[arg(long = "tag", value_name = "TAG", conflicts_with_all = ["skills", "all"])]
pub tags: Vec<String>,
/// Switch tag matching from union (any) to intersection (all) when
/// multiple `--tag` flags are passed. Has no effect without `--tag`.
#[arg(long, requires = "tags")]
pub all_tags: bool,
/// Target path inside the library (e.g. `.` for the library root,
/// `skills`, or `.claude/skills`). Required in non-interactive mode.
#[arg(long, value_name = "PATH")]
pub target: Option<PathBuf>,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum OnConflict {
/// Replace the existing destination folder with the library version.
Overwrite,
/// Leave the existing folder untouched and skip recording the install.
Skip,
/// Stop on the first conflict, persisting whatever was already installed.
Abort,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum OnDivergence {
/// Force the local version onto the library (push), or pull the library
/// version into the local destination (pull) — discarding the other side.
Overwrite,
/// Leave the divergent skill untouched on both sides.
Skip,
/// Fork the divergent skill. On `push`, create a new library skill from
/// the local content. On `pull`, rename the local copy under a new name
/// and pull the library version into the original destination. Requires
/// `--fork-suffix` in non-interactive mode.
Fork,
}