skillrepo 3.2.0 → 4.0.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 +90 -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-session-sync.mjs +1 -1
- package/src/commands/init.mjs +435 -111
- package/src/commands/list.mjs +1 -1
- package/src/commands/remove.mjs +10 -2
- package/src/commands/uninstall.mjs +1 -1
- package/src/commands/update.mjs +15 -3
- package/src/lib/agent-registry.mjs +215 -0
- 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/mcp-merge.mjs +17 -36
- 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/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 +228 -42
- package/src/test/commands/remove.test.mjs +4 -1
- package/src/test/commands/update.test.mjs +13 -3
- package/src/test/e2e/cli-agent-permutations.test.mjs +631 -0
- package/src/test/e2e/cli-commands.test.mjs +39 -13
- package/src/test/integration/file-write.integration.test.mjs +31 -10
- package/src/test/lib/agent-registry.test.mjs +215 -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/lib/detect-ides.mjs +0 -44
- package/src/test/detect-ides.test.mjs +0 -65
package/src/commands/remove.mjs
CHANGED
|
@@ -32,13 +32,17 @@
|
|
|
32
32
|
* - 403 scope → scopeError (via http.mjs) with write-key hint
|
|
33
33
|
* - 401 → authError
|
|
34
34
|
*
|
|
35
|
-
* Flags: --global / --
|
|
35
|
+
* Flags: --global / --agent / --json / --key / --url
|
|
36
36
|
* Positional: <@owner/name>
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { removeSkillFromLibrary } from "../lib/http.mjs";
|
|
40
40
|
import { removeSkillDir, cleanupOrphans } from "../lib/file-write.mjs";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
resolveFlags,
|
|
43
|
+
effectiveVendors,
|
|
44
|
+
requireVendorTargets,
|
|
45
|
+
} from "../lib/cli-config.mjs";
|
|
42
46
|
import { parseIdentifier, formatIdentifier } from "../lib/identifier.mjs";
|
|
43
47
|
import { validationError } from "../lib/errors.mjs";
|
|
44
48
|
|
|
@@ -75,6 +79,10 @@ export async function runRemove(argv, io = {}) {
|
|
|
75
79
|
|
|
76
80
|
const { owner, name } = parseIdentifier(identifier);
|
|
77
81
|
const vendors = effectiveVendors(flags);
|
|
82
|
+
// `remove` deletes specific files from disk — `--agent none` means
|
|
83
|
+
// "no targets to delete from", which is a no-op the user almost
|
|
84
|
+
// certainly didn't intend. Reject early.
|
|
85
|
+
requireVendorTargets(vendors, "remove");
|
|
78
86
|
|
|
79
87
|
// Pre-flight: clean any .old/ orphans from a prior crashed write.
|
|
80
88
|
// `remove` isn't writing new files, but it IS deleting, and the
|
|
@@ -430,7 +430,7 @@ function renderPreviewLine(descriptor, result) {
|
|
|
430
430
|
function parseUninstallFlags(argv) {
|
|
431
431
|
let dryRun = false;
|
|
432
432
|
let yes = false;
|
|
433
|
-
// Reuse resolveFlags for the standard --global / --json / --
|
|
433
|
+
// Reuse resolveFlags for the standard --global / --json / --agent /
|
|
434
434
|
// --key / --url shape. resolveFlags ignores unknown flags when an
|
|
435
435
|
// acceptPositional callback is provided that can consume them —
|
|
436
436
|
// the callback pattern matches init's own parsing.
|
package/src/commands/update.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Flags (parsed by the shared `resolveFlags` helper):
|
|
9
9
|
* --global Write to ~/.claude/skills/ instead of project-local
|
|
10
|
-
* --
|
|
10
|
+
* --agent <list> Comma-separated agent target list
|
|
11
11
|
* --json Print summary as JSON
|
|
12
12
|
* --key <key> Override config-file access key
|
|
13
13
|
* --url <url> Override config-file server URL
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { runSync } from "../lib/sync.mjs";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
resolveFlags,
|
|
31
|
+
effectiveVendors,
|
|
32
|
+
requireVendorTargets,
|
|
33
|
+
} from "../lib/cli-config.mjs";
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Run `update`.
|
|
@@ -70,7 +74,7 @@ export async function runUpdate(argv, io = {}) {
|
|
|
70
74
|
// fires before `skillrepo init` has run — a real first-run
|
|
71
75
|
// scenario, not synthetic)
|
|
72
76
|
// - `validationError` on unknown flags
|
|
73
|
-
// - `validationError` from
|
|
77
|
+
// - `validationError` from parseAgentList on a malformed --agent
|
|
74
78
|
//
|
|
75
79
|
// All three happen INSIDE resolveFlags, before our try/catch block
|
|
76
80
|
// could see them if we called it after. The only robust answer is
|
|
@@ -98,6 +102,13 @@ export async function runUpdate(argv, io = {}) {
|
|
|
98
102
|
},
|
|
99
103
|
});
|
|
100
104
|
const vendors = effectiveVendors(flags);
|
|
105
|
+
// `--agent none` makes `update` a no-op — the user opted out of
|
|
106
|
+
// placement, so there's nowhere to write the synced library.
|
|
107
|
+
// Reject in session-hook mode too: the contract is "exit 0 on
|
|
108
|
+
// any error", but the requireVendorTargets throw is a typed
|
|
109
|
+
// validation error and the outer try/catch maps it to the
|
|
110
|
+
// documented one-line failure message.
|
|
111
|
+
requireVendorTargets(vendors, "update");
|
|
101
112
|
|
|
102
113
|
const summary = await runSync({
|
|
103
114
|
serverUrl: flags.serverUrl,
|
|
@@ -143,6 +154,7 @@ export async function runUpdate(argv, io = {}) {
|
|
|
143
154
|
// when tests inject one.
|
|
144
155
|
const flags = resolveFlags(argv);
|
|
145
156
|
const vendors = effectiveVendors(flags);
|
|
157
|
+
requireVendorTargets(vendors, "update");
|
|
146
158
|
|
|
147
159
|
const summary = await runSync({
|
|
148
160
|
serverUrl: flags.serverUrl,
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the agent vendors the CLI supports (#1234).
|
|
3
|
+
*
|
|
4
|
+
* Every agent the CLI knows about is declared here exactly once. Path
|
|
5
|
+
* resolvers (file-write.mjs), flag parsers (cli-config.mjs), MCP merge
|
|
6
|
+
* coordinators (mcp-merge.mjs), gitignore management (mergers/gitignore.mjs),
|
|
7
|
+
* detection probes (detect-agents.mjs), and orphan sweeps all derive their
|
|
8
|
+
* vendor lists from this registry.
|
|
9
|
+
*
|
|
10
|
+
* Adding a vendor: append a frozen entry. Renaming a vendor: change `key`
|
|
11
|
+
* and put the old key in `aliases` so prior CLI invocations keep working.
|
|
12
|
+
*
|
|
13
|
+
* The two-target placement model (per the verified vendor matrix):
|
|
14
|
+
* - Claude Code uses Anthropic's documented `.claude/skills/` path.
|
|
15
|
+
* - Every other supported agent shares `.agents/skills/` for project
|
|
16
|
+
* scope and (with the exception of Windsurf and Copilot) for personal
|
|
17
|
+
* scope. Windsurf has a vendor-specific personal path under
|
|
18
|
+
* `~/.codeium/windsurf/skills/`. Copilot has no personal scope.
|
|
19
|
+
*
|
|
20
|
+
* Detection signals (`detectionSignals`) are the primary-source-verified
|
|
21
|
+
* fingerprints `detect-agents.mjs` probes when picking which agents have
|
|
22
|
+
* footprint on this machine / in this project. Three signal types:
|
|
23
|
+
*
|
|
24
|
+
* - `env` — an environment variable the agent sets in shells/sub-processes
|
|
25
|
+
* it spawns. Truthy presence in `process.env` indicates an active session.
|
|
26
|
+
* NOTE: only assert env vars the vendor's own docs document as set BY the
|
|
27
|
+
* agent in spawned shells. Variables the agent merely *reads* (e.g.
|
|
28
|
+
* `CODEX_HOME` for Codex's config dir) are config inputs, not detection
|
|
29
|
+
* signals — listing them here would produce false positives.
|
|
30
|
+
* - `home` — a HOME-relative path the agent creates on first run. Used as
|
|
31
|
+
* a "this user has installed the agent" proxy when no env var is
|
|
32
|
+
* documented.
|
|
33
|
+
* - `project` — a CWD-relative dotfile / dotdir the user keeps in the
|
|
34
|
+
* repo to drive agent behavior. Used as the "configured in this repo"
|
|
35
|
+
* signal.
|
|
36
|
+
*
|
|
37
|
+
* Detection is registry-driven: adding a new signal is a registry edit,
|
|
38
|
+
* no detect-agents.mjs change required.
|
|
39
|
+
*
|
|
40
|
+
* @typedef {"claudeProject" | "claudeGlobal" | "agentsProject" | "agentsGlobal" | "windsurfGlobal"} PlacementTarget
|
|
41
|
+
*
|
|
42
|
+
* @typedef {Object} DetectionSignal
|
|
43
|
+
* @property {"env"|"home"|"project"} type
|
|
44
|
+
* @property {string} value - Env var name, HOME-relative path, or
|
|
45
|
+
* project (CWD)-relative path. Use forward slashes; resolution is
|
|
46
|
+
* via `node:path.join` so Windows separators are produced
|
|
47
|
+
* automatically at probe time.
|
|
48
|
+
*
|
|
49
|
+
* @typedef {Object} AgentEntry
|
|
50
|
+
* @property {string} key - Canonical key. Stored in config files.
|
|
51
|
+
* @property {string} displayName - User-facing name (used in prompts and summaries).
|
|
52
|
+
* @property {string[]} aliases - Alternate names accepted on the command line.
|
|
53
|
+
* @property {PlacementTarget} projectTarget - Where project-scope writes land.
|
|
54
|
+
* @property {PlacementTarget|null} globalTarget - Where personal-scope writes land, or null if the vendor has no personal scope.
|
|
55
|
+
* @property {boolean} hasMcp - True if the CLI has a per-vendor MCP merger; false for file-only vendors (cohort vendors without a documented MCP config path).
|
|
56
|
+
* @property {readonly DetectionSignal[]} detectionSignals - Fingerprints that mark this agent as present.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/** @type {readonly AgentEntry[]} */
|
|
60
|
+
export const AGENT_REGISTRY = Object.freeze([
|
|
61
|
+
Object.freeze({
|
|
62
|
+
key: "claudeCode",
|
|
63
|
+
displayName: "Claude Code",
|
|
64
|
+
// `claude` is the canonical short token (also recognized
|
|
65
|
+
// explicitly by parseAgentList for the target-shortcut role).
|
|
66
|
+
// `claude-code` is the kebab-case form some users reach for by
|
|
67
|
+
// habit; accepting it as a silent alias matches the spec's
|
|
68
|
+
// "vendor names accepted silently as courtesy aliases" rule.
|
|
69
|
+
aliases: Object.freeze(["claude", "claude-code"]),
|
|
70
|
+
projectTarget: "claudeProject",
|
|
71
|
+
globalTarget: "claudeGlobal",
|
|
72
|
+
hasMcp: true,
|
|
73
|
+
detectionSignals: Object.freeze([
|
|
74
|
+
Object.freeze({ type: "env", value: "CLAUDECODE" }),
|
|
75
|
+
Object.freeze({ type: "home", value: ".claude" }),
|
|
76
|
+
Object.freeze({ type: "project", value: ".claude" }),
|
|
77
|
+
]),
|
|
78
|
+
}),
|
|
79
|
+
Object.freeze({
|
|
80
|
+
key: "cursor",
|
|
81
|
+
displayName: "Cursor",
|
|
82
|
+
aliases: Object.freeze([]),
|
|
83
|
+
projectTarget: "agentsProject",
|
|
84
|
+
globalTarget: "agentsGlobal",
|
|
85
|
+
hasMcp: true,
|
|
86
|
+
detectionSignals: Object.freeze([
|
|
87
|
+
Object.freeze({ type: "env", value: "CURSOR_AGENT" }),
|
|
88
|
+
// CURSOR_CLI is the documented fallback when CURSOR_AGENT is
|
|
89
|
+
// not set in older Cursor builds; both fire under the same
|
|
90
|
+
// OR semantics so listing both costs nothing.
|
|
91
|
+
Object.freeze({ type: "env", value: "CURSOR_CLI" }),
|
|
92
|
+
Object.freeze({ type: "home", value: ".cursor" }),
|
|
93
|
+
Object.freeze({ type: "project", value: ".cursor" }),
|
|
94
|
+
]),
|
|
95
|
+
}),
|
|
96
|
+
Object.freeze({
|
|
97
|
+
key: "windsurf",
|
|
98
|
+
displayName: "Windsurf",
|
|
99
|
+
aliases: Object.freeze([]),
|
|
100
|
+
projectTarget: "agentsProject",
|
|
101
|
+
globalTarget: "windsurfGlobal",
|
|
102
|
+
hasMcp: true,
|
|
103
|
+
detectionSignals: Object.freeze([
|
|
104
|
+
// No documented active-session env var (verified absent in
|
|
105
|
+
// Windsurf docs 2026-05). HOME trace under the Codeium prefix
|
|
106
|
+
// is the strongest available signal.
|
|
107
|
+
Object.freeze({ type: "home", value: ".codeium/windsurf" }),
|
|
108
|
+
Object.freeze({ type: "project", value: ".windsurf" }),
|
|
109
|
+
]),
|
|
110
|
+
}),
|
|
111
|
+
Object.freeze({
|
|
112
|
+
key: "gemini",
|
|
113
|
+
displayName: "Gemini CLI",
|
|
114
|
+
aliases: Object.freeze([]),
|
|
115
|
+
projectTarget: "agentsProject",
|
|
116
|
+
globalTarget: "agentsGlobal",
|
|
117
|
+
hasMcp: false,
|
|
118
|
+
detectionSignals: Object.freeze([
|
|
119
|
+
Object.freeze({ type: "env", value: "GEMINI_CLI" }),
|
|
120
|
+
Object.freeze({ type: "home", value: ".gemini" }),
|
|
121
|
+
Object.freeze({ type: "project", value: ".gemini" }),
|
|
122
|
+
]),
|
|
123
|
+
}),
|
|
124
|
+
Object.freeze({
|
|
125
|
+
key: "codex",
|
|
126
|
+
displayName: "Codex CLI",
|
|
127
|
+
aliases: Object.freeze([]),
|
|
128
|
+
projectTarget: "agentsProject",
|
|
129
|
+
globalTarget: "agentsGlobal",
|
|
130
|
+
hasMcp: false,
|
|
131
|
+
detectionSignals: Object.freeze([
|
|
132
|
+
// Codex has no documented active-session env var. CODEX_HOME
|
|
133
|
+
// is a config var Codex *reads*, not one it sets — listing it
|
|
134
|
+
// would produce false positives for users who simply have the
|
|
135
|
+
// var pointed elsewhere. Detection is HOME + project only.
|
|
136
|
+
Object.freeze({ type: "home", value: ".codex" }),
|
|
137
|
+
Object.freeze({ type: "project", value: ".codex" }),
|
|
138
|
+
]),
|
|
139
|
+
}),
|
|
140
|
+
Object.freeze({
|
|
141
|
+
key: "cline",
|
|
142
|
+
displayName: "Cline",
|
|
143
|
+
aliases: Object.freeze([]),
|
|
144
|
+
projectTarget: "agentsProject",
|
|
145
|
+
globalTarget: "agentsGlobal",
|
|
146
|
+
hasMcp: false,
|
|
147
|
+
detectionSignals: Object.freeze([
|
|
148
|
+
// Cline v3.24+ sets CLINE_ACTIVE="true" in spawned shells.
|
|
149
|
+
// Detection treats any truthy value as the signal.
|
|
150
|
+
Object.freeze({ type: "env", value: "CLINE_ACTIVE" }),
|
|
151
|
+
Object.freeze({ type: "home", value: ".cline" }),
|
|
152
|
+
Object.freeze({ type: "project", value: ".cline" }),
|
|
153
|
+
]),
|
|
154
|
+
}),
|
|
155
|
+
Object.freeze({
|
|
156
|
+
key: "copilot",
|
|
157
|
+
displayName: "VS Code + Copilot",
|
|
158
|
+
aliases: Object.freeze(["vscode"]),
|
|
159
|
+
projectTarget: "agentsProject",
|
|
160
|
+
globalTarget: null,
|
|
161
|
+
hasMcp: true,
|
|
162
|
+
detectionSignals: Object.freeze([
|
|
163
|
+
// Copilot has no documented active-session env var. The HOME
|
|
164
|
+
// path is the directory the Copilot CLI / VS Code Copilot
|
|
165
|
+
// extension creates on first run; the project path is the
|
|
166
|
+
// documented Agent Skills location for VS Code + Copilot.
|
|
167
|
+
Object.freeze({ type: "home", value: ".copilot" }),
|
|
168
|
+
Object.freeze({ type: "project", value: ".github/skills" }),
|
|
169
|
+
]),
|
|
170
|
+
}),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Canonical keys of every cohort vendor — every registry entry whose
|
|
175
|
+
* project-scope writes land in `.agents/skills/`. Derived from the
|
|
176
|
+
* registry so a future cohort addition flows through every consumer
|
|
177
|
+
* without touching their code. Used by `cli-config.mjs` to expand the
|
|
178
|
+
* `--agent agents` token, and by `init.mjs` to translate the
|
|
179
|
+
* "Other agents" picker row to a vendor list.
|
|
180
|
+
*
|
|
181
|
+
* @type {readonly string[]}
|
|
182
|
+
*/
|
|
183
|
+
export const AGENTS_COHORT_KEYS = Object.freeze(
|
|
184
|
+
AGENT_REGISTRY.filter((entry) => entry.projectTarget === "agentsProject").map(
|
|
185
|
+
(entry) => entry.key,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Look up a registry entry by its canonical key. Returns `undefined`
|
|
191
|
+
* for unknown keys — callers should treat that as a programming error.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} key
|
|
194
|
+
* @returns {AgentEntry | undefined}
|
|
195
|
+
*/
|
|
196
|
+
export function getAgentByKey(key) {
|
|
197
|
+
return AGENT_REGISTRY.find((entry) => entry.key === key);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve an alias (or canonical key) to its canonical key. Returns
|
|
202
|
+
* `null` for inputs that match neither a canonical key nor any
|
|
203
|
+
* declared alias. The function is case-sensitive — alias normalization
|
|
204
|
+
* is the caller's responsibility.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} alias
|
|
207
|
+
* @returns {string | null}
|
|
208
|
+
*/
|
|
209
|
+
export function getAgentByAlias(alias) {
|
|
210
|
+
for (const entry of AGENT_REGISTRY) {
|
|
211
|
+
if (entry.key === alias) return entry.key;
|
|
212
|
+
if (entry.aliases.includes(alias)) return entry.key;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
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
|
+
}
|