skillrepo 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -27
- package/bin/skillrepo.mjs +5 -5
- package/package.json +1 -1
- package/src/commands/add.mjs +21 -6
- package/src/commands/get.mjs +20 -4
- package/src/commands/init-cohort-hooks.mjs +127 -0
- package/src/commands/init-session-sync.mjs +1 -1
- package/src/commands/init.mjs +480 -117
- package/src/commands/list.mjs +1 -1
- package/src/commands/remove.mjs +10 -2
- package/src/commands/uninstall.mjs +13 -2
- package/src/commands/update.mjs +112 -19
- package/src/lib/agent-hook-merge.mjs +203 -0
- package/src/lib/agent-registry.mjs +399 -0
- package/src/lib/artifact-registry.mjs +111 -2
- package/src/lib/cli-config.mjs +146 -44
- package/src/lib/detect-agents.mjs +112 -0
- package/src/lib/file-write.mjs +162 -77
- package/src/lib/fs-utils.mjs +16 -1
- package/src/lib/mcp-merge.mjs +17 -36
- package/src/lib/mergers/agent-hook-claude-shape.mjs +519 -0
- package/src/lib/mergers/agent-hook-cursor-shape.mjs +318 -0
- package/src/lib/mergers/gitignore.mjs +55 -28
- package/src/lib/paths.mjs +27 -25
- package/src/lib/prompt-multiselect.mjs +324 -0
- package/src/lib/removers/agent-hooks.mjs +83 -0
- package/src/lib/sync.mjs +18 -19
- package/src/test/commands/add.test.mjs +18 -3
- package/src/test/commands/init-picker.test.mjs +144 -0
- package/src/test/commands/init.test.mjs +508 -41
- package/src/test/commands/remove.test.mjs +4 -1
- package/src/test/commands/update.test.mjs +148 -3
- package/src/test/e2e/cli-agent-permutations.test.mjs +631 -0
- package/src/test/e2e/cli-cohort-hooks.test.mjs +393 -0
- package/src/test/e2e/cli-commands.test.mjs +39 -13
- package/src/test/integration/agent-hooks.integration.test.mjs +340 -0
- package/src/test/integration/file-write.integration.test.mjs +31 -10
- package/src/test/lib/agent-hook-merge.test.mjs +172 -0
- package/src/test/lib/agent-registry.test.mjs +215 -0
- package/src/test/lib/artifact-registry.test.mjs +39 -0
- package/src/test/lib/cli-config.test.mjs +222 -38
- package/src/test/lib/detect-agents.test.mjs +336 -0
- package/src/test/lib/file-write-placement.test.mjs +264 -0
- package/src/test/lib/file-write.test.mjs +231 -30
- package/src/test/lib/mcp-merge.test.mjs +23 -15
- package/src/test/lib/paths.test.mjs +53 -17
- package/src/test/lib/prompt-multiselect.test.mjs +448 -0
- package/src/test/lib/sync.test.mjs +157 -0
- package/src/test/mergers/agent-hook-claude-shape.test.mjs +518 -0
- package/src/test/mergers/agent-hook-cursor-shape.test.mjs +306 -0
- package/src/test/removers/agent-hooks.test.mjs +206 -0
- package/src/lib/detect-ides.mjs +0 -44
- package/src/test/detect-ides.test.mjs +0 -65
package/src/lib/cli-config.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared credential + flag resolution for command modules.
|
|
3
3
|
*
|
|
4
4
|
* Every command needs to:
|
|
5
|
-
* 1. Resolve `--key`/`--url`/`--
|
|
5
|
+
* 1. Resolve `--key`/`--url`/`--agent`/`--global`/`--json` flags
|
|
6
6
|
* 2. Fall back to ~/.claude/skillrepo/config.json
|
|
7
7
|
* 3. Fall back to SKILLREPO_ACCESS_KEY / SKILLREPO_URL env vars
|
|
8
8
|
* 4. Hard-error with an actionable hint pointing at `init` if no
|
|
@@ -20,17 +20,28 @@
|
|
|
20
20
|
import { existsSync, readFileSync } from "node:fs";
|
|
21
21
|
|
|
22
22
|
import { globalConfigPath } from "./paths.mjs";
|
|
23
|
+
import {
|
|
24
|
+
AGENT_REGISTRY,
|
|
25
|
+
AGENTS_COHORT_KEYS,
|
|
26
|
+
getAgentByAlias,
|
|
27
|
+
} from "./agent-registry.mjs";
|
|
23
28
|
import { authError, validationError } from "./errors.mjs";
|
|
24
29
|
|
|
25
|
-
const
|
|
26
|
-
|
|
30
|
+
const ALL_VENDOR_KEYS = AGENT_REGISTRY.map((entry) => entry.key);
|
|
31
|
+
|
|
32
|
+
// Sentinel returned by parseAgentList for `--agent none`. It is the
|
|
33
|
+
// empty array literally — every consumer reads `flags.vendors.length`
|
|
34
|
+
// to detect "no placement writes." Frozen so a downstream mutation
|
|
35
|
+
// (e.g. `.push`) cannot turn one user's `none` into another's surprise.
|
|
36
|
+
const NO_VENDORS = Object.freeze([]);
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
39
|
* @typedef {Object} ResolvedFlags
|
|
30
40
|
* @property {string} serverUrl
|
|
31
41
|
* @property {string} apiKey
|
|
32
42
|
* @property {boolean} global
|
|
33
|
-
* @property {string[]|null} vendors - null = use the default
|
|
43
|
+
* @property {string[]|null} vendors - null = use the default;
|
|
44
|
+
* empty array = `--agent none` (caller skips placement)
|
|
34
45
|
* @property {boolean} json
|
|
35
46
|
*/
|
|
36
47
|
|
|
@@ -78,8 +89,22 @@ export function resolveFlags(argv, opts = {}) {
|
|
|
78
89
|
|
|
79
90
|
if (arg === "--global") {
|
|
80
91
|
flagState.global = true;
|
|
81
|
-
} else if (arg === "--
|
|
82
|
-
|
|
92
|
+
} else if (arg === "--agent") {
|
|
93
|
+
if (argv[i + 1] === undefined) {
|
|
94
|
+
throw validationError(
|
|
95
|
+
"Missing value for --agent. Pass a comma-separated list (e.g., --agent claude,agents).",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
flagState.vendors = parseAgentList(argv[++i]);
|
|
99
|
+
} else if (arg === "--ide") {
|
|
100
|
+
throw validationError(
|
|
101
|
+
"--ide was renamed to --agent in v3.0.0.",
|
|
102
|
+
{
|
|
103
|
+
hint:
|
|
104
|
+
"Replace --ide with --agent (e.g., --agent claude). " +
|
|
105
|
+
"Run the command with --help for valid options.",
|
|
106
|
+
},
|
|
107
|
+
);
|
|
83
108
|
} else if (arg === "--json") {
|
|
84
109
|
flagState.json = true;
|
|
85
110
|
} else if ((arg === "--key" || arg === "-k") && argv[i + 1] !== undefined) {
|
|
@@ -172,62 +197,139 @@ export function resolveFlags(argv, opts = {}) {
|
|
|
172
197
|
}
|
|
173
198
|
|
|
174
199
|
/**
|
|
175
|
-
* Compute the vendor list to pass to file-write / sync.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
200
|
+
* Compute the vendor list to pass to file-write / sync.
|
|
201
|
+
*
|
|
202
|
+
* Resolution order:
|
|
203
|
+
* 1. Explicit `--agent` (including the `--agent none` sentinel `[]`)
|
|
204
|
+
* always wins. Both flags propagate together, so `--global
|
|
205
|
+
* --agent windsurf` correctly resolves to `["windsurf"]` and
|
|
206
|
+
* placementTargetsFor maps that to `windsurfGlobal`.
|
|
207
|
+
* 2. No `--agent` defaults to `["claudeCode"]`, regardless of
|
|
208
|
+
* `--global`. The v3.0.0 CLI deliberately does NOT silently
|
|
209
|
+
* fall back to `[claudeCode, cursor]` like v2.0.0 did.
|
|
210
|
+
*
|
|
211
|
+
* The empty-array `--agent none` sentinel is returned verbatim —
|
|
212
|
+
* callers detect "no placement" via `vendors.length === 0`. The
|
|
213
|
+
* default-fallback branch uses an explicit null/undefined check so
|
|
214
|
+
* the empty array survives.
|
|
179
215
|
*/
|
|
180
216
|
export function effectiveVendors(flags) {
|
|
181
|
-
if (flags.
|
|
182
|
-
|
|
183
|
-
|
|
217
|
+
if (flags.vendors === null || flags.vendors === undefined) {
|
|
218
|
+
return ["claudeCode"];
|
|
219
|
+
}
|
|
220
|
+
return flags.vendors;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Reject `--agent none` for commands that have no meaningful behavior
|
|
225
|
+
* without a placement target. `init` and `update` accept `--agent none`
|
|
226
|
+
* (init still writes config + gitignore; update is a no-op). Every
|
|
227
|
+
* other write/read-write command (`add`, `get`, `remove`, `uninstall`)
|
|
228
|
+
* exists to put or remove specific skill files on disk — calling them
|
|
229
|
+
* with no targets is a user error, not a degenerate-but-valid case.
|
|
230
|
+
*
|
|
231
|
+
* Pass the resolved `vendors` array (the output of `effectiveVendors`).
|
|
232
|
+
* `undefined` (the `--global` case) is allowed because `--global`
|
|
233
|
+
* implies the Claude Code personal directory; only an empty array is
|
|
234
|
+
* rejected.
|
|
235
|
+
*
|
|
236
|
+
* @param {string[] | undefined} vendors
|
|
237
|
+
* @param {string} commandName - For the error message.
|
|
238
|
+
*/
|
|
239
|
+
export function requireVendorTargets(vendors, commandName) {
|
|
240
|
+
if (Array.isArray(vendors) && vendors.length === 0) {
|
|
241
|
+
throw validationError(
|
|
242
|
+
`--agent none has no effect on \`skillrepo ${commandName}\` ` +
|
|
243
|
+
`because the command writes or deletes specific skill files.`,
|
|
244
|
+
{
|
|
245
|
+
hint:
|
|
246
|
+
"Use --agent claude or --agent agents (or a specific vendor) to " +
|
|
247
|
+
"target a placement, or drop --agent to use the default.",
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
}
|
|
184
251
|
}
|
|
185
252
|
|
|
186
253
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
187
254
|
|
|
188
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Parse the raw `--agent <list>` argument into a deduplicated array
|
|
257
|
+
* of canonical vendor keys, OR the empty array sentinel for `none`.
|
|
258
|
+
*
|
|
259
|
+
* Token semantics (each comma-separated token):
|
|
260
|
+
*
|
|
261
|
+
* - `none` → empty array (no placement writes). Must be the only
|
|
262
|
+
* token — mixing with any other token is rejected.
|
|
263
|
+
* - `claude` → `["claudeCode"]` (the Claude Code target shortcut).
|
|
264
|
+
* - `agents` → every cohort vendor whose project writes land in
|
|
265
|
+
* `.agents/skills/`, derived from the registry.
|
|
266
|
+
* - canonical key (e.g., `claudeCode`, `cursor`) → that key.
|
|
267
|
+
* - alias declared on a registry entry (e.g., `claude-code`,
|
|
268
|
+
* `vscode`) → its canonical key.
|
|
269
|
+
* - anything else → validation error.
|
|
270
|
+
*
|
|
271
|
+
* `all` is NOT a valid token — use `claude,agents` for full coverage.
|
|
272
|
+
*/
|
|
273
|
+
function parseAgentList(raw) {
|
|
189
274
|
if (typeof raw !== "string") {
|
|
190
|
-
throw validationError(`--
|
|
275
|
+
throw validationError(`--agent expects a comma-separated list, got: ${raw}`);
|
|
191
276
|
}
|
|
192
277
|
|
|
193
|
-
// First pass: collect tokens, normalize aliases, detect `all`.
|
|
194
278
|
const pieces = raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// Defense against confusing input: `--ide cursor,all` is ambiguous
|
|
200
|
-
// — the user might think it means "cursor first, then everything"
|
|
201
|
-
// or "all minus duplicates". Both interpretations are wrong because
|
|
202
|
-
// `all` is the full set. Reject the combination so the user is
|
|
203
|
-
// forced to pick one.
|
|
204
|
-
if (hasAll && otherVendors.length > 0) {
|
|
205
|
-
throw validationError(
|
|
206
|
-
`--ide cannot mix "all" with other vendors: "${raw}"`,
|
|
207
|
-
{ hint: "Use --ide all OR --ide claude,cursor,... — not both." },
|
|
208
|
-
);
|
|
279
|
+
if (pieces.length === 0) {
|
|
280
|
+
throw validationError("--agent list is empty.", {
|
|
281
|
+
hint: "Pass one or more of: claude, agents, none, or a vendor name.",
|
|
282
|
+
});
|
|
209
283
|
}
|
|
210
284
|
|
|
211
|
-
|
|
212
|
-
|
|
285
|
+
// `none` is mutually exclusive with every other token. Mixing
|
|
286
|
+
// `--agent claude,none` is ambiguous — does the user want Claude
|
|
287
|
+
// or no placement? Reject so they pick one.
|
|
288
|
+
if (pieces.includes("none")) {
|
|
289
|
+
if (pieces.length > 1) {
|
|
290
|
+
throw validationError(
|
|
291
|
+
`--agent cannot mix "none" with other tokens: "${raw}"`,
|
|
292
|
+
{ hint: "Pass --agent none alone, or list specific targets without none." },
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return NO_VENDORS;
|
|
213
296
|
}
|
|
214
297
|
|
|
215
|
-
|
|
216
|
-
|
|
298
|
+
const expanded = [];
|
|
299
|
+
for (const token of pieces) {
|
|
300
|
+
if (token === "claude") {
|
|
301
|
+
expanded.push("claudeCode");
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (token === "agents") {
|
|
305
|
+
for (const key of AGENTS_COHORT_KEYS) {
|
|
306
|
+
expanded.push(key);
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const canonical = getAgentByAlias(token);
|
|
311
|
+
if (canonical !== null) {
|
|
312
|
+
expanded.push(canonical);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
throw validationError(`Unknown --agent target: "${token}"`, {
|
|
316
|
+
hint:
|
|
317
|
+
"Use one of: claude, agents, none, or a canonical vendor key " +
|
|
318
|
+
`(${ALL_VENDOR_KEYS.join(", ")}).`,
|
|
319
|
+
});
|
|
217
320
|
}
|
|
218
321
|
|
|
219
|
-
//
|
|
220
|
-
for
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
322
|
+
// Dedupe while preserving first-seen order — matters for tests and
|
|
323
|
+
// for downstream consumers that iterate the array deterministically.
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
const result = [];
|
|
326
|
+
for (const key of expanded) {
|
|
327
|
+
if (!seen.has(key)) {
|
|
328
|
+
seen.add(key);
|
|
329
|
+
result.push(key);
|
|
227
330
|
}
|
|
228
331
|
}
|
|
229
|
-
|
|
230
|
-
return normalized;
|
|
332
|
+
return result;
|
|
231
333
|
}
|
|
232
334
|
|
|
233
335
|
function readGlobalConfig() {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-signal agent detection (#1236, Phase 3 of #876).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the single-signal `detect-ides.mjs` module. Detection is
|
|
5
|
+
* registry-driven: every probe is described as a `DetectionSignal` on
|
|
6
|
+
* the corresponding `AGENT_REGISTRY` entry, and this module just
|
|
7
|
+
* iterates the registry and runs the platform-correct probe for each
|
|
8
|
+
* signal type. Adding a new signal is a registry edit, no change here.
|
|
9
|
+
*
|
|
10
|
+
* Three signal types:
|
|
11
|
+
*
|
|
12
|
+
* - `env` — `process.env[value]` is truthy. Indicates an active
|
|
13
|
+
* agent session (the agent itself sets the var in the
|
|
14
|
+
* shells it spawns).
|
|
15
|
+
* - `home` — `~/<value>` exists on disk. Indicates the agent has
|
|
16
|
+
* been installed for this user (most agents create the
|
|
17
|
+
* directory on first run).
|
|
18
|
+
* - `project` — `<cwd>/<value>` exists on disk. Indicates the agent
|
|
19
|
+
* is configured for this repo (dotdir / dotfile).
|
|
20
|
+
*
|
|
21
|
+
* For each agent, `detected` is the OR of all signal probes. The first
|
|
22
|
+
* signal that fires (priority order: env > home > project) becomes the
|
|
23
|
+
* human-readable `reason`. Reasons are formatted to match the picker UX:
|
|
24
|
+
*
|
|
25
|
+
* - env signal: `"CLAUDECODE=1"`
|
|
26
|
+
* - home signal: `"~/.claude/"` or `"~/.codeium/windsurf/"`
|
|
27
|
+
* - project signal: `".claude/"` or `".github/skills/"`
|
|
28
|
+
* - none: `null` (the picker translates this to
|
|
29
|
+
* `"(no signal — opt in if you use one)"`)
|
|
30
|
+
*
|
|
31
|
+
* Cross-platform: paths use `node:path.join(homedir(), …)` and
|
|
32
|
+
* `join(process.cwd(), …)`. On Windows, `homedir()` reads
|
|
33
|
+
* `USERPROFILE`; the test sandbox helper sets both HOME and USERPROFILE
|
|
34
|
+
* so probes are sandbox-isolated on every platform.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { existsSync } from "node:fs";
|
|
38
|
+
import { homedir } from "node:os";
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
|
|
41
|
+
import { AGENT_REGISTRY } from "./agent-registry.mjs";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} AgentDetection
|
|
45
|
+
* @property {string} key - Canonical agent key.
|
|
46
|
+
* @property {string} displayName - Brand name (used in picker hints).
|
|
47
|
+
* @property {boolean} detected - True if any signal fired.
|
|
48
|
+
* @property {string|null} reason - Short human-readable label of the
|
|
49
|
+
* first signal that fired, or `null` when nothing fired.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Probe one detection signal against the live process state and disk.
|
|
54
|
+
*
|
|
55
|
+
* Pure helper. Returns the formatted reason string when the signal
|
|
56
|
+
* fires, or `null` when it does not.
|
|
57
|
+
*
|
|
58
|
+
* @param {import("./agent-registry.mjs").DetectionSignal} signal
|
|
59
|
+
* @returns {string | null}
|
|
60
|
+
*/
|
|
61
|
+
function probeSignal(signal) {
|
|
62
|
+
if (signal.type === "env") {
|
|
63
|
+
const raw = process.env[signal.value];
|
|
64
|
+
if (raw === undefined || raw === "") return null;
|
|
65
|
+
// Env vars in shell environments are always strings. Truthy
|
|
66
|
+
// values include "1", "true", "yes", etc — Cline's documented
|
|
67
|
+
// value is the literal string "true". We accept any non-empty
|
|
68
|
+
// value rather than a stricter truthy-string match because the
|
|
69
|
+
// signal's purpose is "the agent set this var on me" and any
|
|
70
|
+
// value the agent chose to set qualifies.
|
|
71
|
+
return `${signal.value}=${raw}`;
|
|
72
|
+
}
|
|
73
|
+
if (signal.type === "home") {
|
|
74
|
+
const path = join(homedir(), signal.value);
|
|
75
|
+
if (!existsSync(path)) return null;
|
|
76
|
+
return `~/${signal.value}/`;
|
|
77
|
+
}
|
|
78
|
+
if (signal.type === "project") {
|
|
79
|
+
const path = join(process.cwd(), signal.value);
|
|
80
|
+
if (!existsSync(path)) return null;
|
|
81
|
+
return `${signal.value}/`;
|
|
82
|
+
}
|
|
83
|
+
// Unknown type — defensive guard. Callers should never see this
|
|
84
|
+
// because the registry's frozen entries are typed.
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Detect every agent in the registry against the current process and
|
|
90
|
+
* working directory. Returns one `AgentDetection` per registry entry,
|
|
91
|
+
* in registry order (Claude Code first, then the cohort).
|
|
92
|
+
*
|
|
93
|
+
* @returns {AgentDetection[]}
|
|
94
|
+
*/
|
|
95
|
+
export function detectAgents() {
|
|
96
|
+
return AGENT_REGISTRY.map((entry) => {
|
|
97
|
+
let reason = null;
|
|
98
|
+
for (const signal of entry.detectionSignals) {
|
|
99
|
+
const fired = probeSignal(signal);
|
|
100
|
+
if (fired !== null) {
|
|
101
|
+
reason = fired;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
key: entry.key,
|
|
107
|
+
displayName: entry.displayName,
|
|
108
|
+
detected: reason !== null,
|
|
109
|
+
reason,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|