@danieldeusing/seedr 0.1.87 → 0.1.88
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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/chunk-WJEBBHTL.js +1169 -0
- package/dist/cli.js +1363 -554
- package/dist/index.d.ts +18 -56
- package/dist/index.js +1 -106
- package/package.json +3 -5
- package/dist/chunk-77M7PGDO.js +0 -762
package/dist/cli.js
CHANGED
|
@@ -2,44 +2,61 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ALL_AGENTS,
|
|
4
4
|
CODING_AGENTS,
|
|
5
|
+
MAX_SLUG_LENGTH,
|
|
6
|
+
SLUG_PATTERN,
|
|
7
|
+
assertDirectoryWithin,
|
|
5
8
|
assertOverwritable,
|
|
9
|
+
assertSafePathSegment,
|
|
10
|
+
assertValidSlug,
|
|
6
11
|
brand,
|
|
7
|
-
|
|
12
|
+
canonicalAgent,
|
|
13
|
+
canonicalAgents,
|
|
8
14
|
confirm,
|
|
15
|
+
copyDirectory,
|
|
9
16
|
ensureDir,
|
|
10
17
|
error,
|
|
11
18
|
exists,
|
|
19
|
+
fetchItemFile,
|
|
12
20
|
fetchItemToDestination,
|
|
13
21
|
getAgentPath,
|
|
14
22
|
getAgentsPath,
|
|
15
23
|
getContentPath,
|
|
16
|
-
|
|
24
|
+
getEffectiveSourceRevision,
|
|
17
25
|
getItem,
|
|
18
26
|
getItemContent,
|
|
19
27
|
getItemSourcePath,
|
|
20
|
-
|
|
28
|
+
getItemsBySlug,
|
|
29
|
+
getMcpConfigPath,
|
|
21
30
|
getSettingsPath,
|
|
22
31
|
info,
|
|
23
|
-
installDirectory,
|
|
24
32
|
installFile,
|
|
25
33
|
intro,
|
|
34
|
+
isInteractive,
|
|
35
|
+
isLegacyAgent,
|
|
36
|
+
isValidSlug,
|
|
26
37
|
listItems,
|
|
38
|
+
moveDirectory,
|
|
27
39
|
outro,
|
|
28
40
|
p,
|
|
29
|
-
|
|
41
|
+
parseAgentsArgStrict,
|
|
42
|
+
parseGitHubRepo,
|
|
30
43
|
printLogo,
|
|
31
|
-
|
|
44
|
+
removePathEntry,
|
|
45
|
+
resolveContained,
|
|
46
|
+
restoreFile,
|
|
32
47
|
searchItems,
|
|
33
48
|
selectAgents,
|
|
34
49
|
selectMethod,
|
|
35
50
|
selectScope,
|
|
36
51
|
selectSkill,
|
|
37
52
|
skillHandler,
|
|
53
|
+
snapshotFile,
|
|
38
54
|
step,
|
|
39
55
|
success,
|
|
40
56
|
warn,
|
|
57
|
+
writeFileAtomic,
|
|
41
58
|
writeTextFile
|
|
42
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-WJEBBHTL.js";
|
|
43
60
|
|
|
44
61
|
// src/cli.ts
|
|
45
62
|
import { Command as Command5 } from "commander";
|
|
@@ -56,13 +73,30 @@ function registerHandler(handler) {
|
|
|
56
73
|
function getHandler(type) {
|
|
57
74
|
return handlers.get(type);
|
|
58
75
|
}
|
|
76
|
+
function getRegisteredTypes() {
|
|
77
|
+
return Array.from(handlers.keys());
|
|
78
|
+
}
|
|
59
79
|
|
|
60
80
|
// src/utils/errors.ts
|
|
61
81
|
import chalk from "chalk";
|
|
82
|
+
function describe(error2) {
|
|
83
|
+
if (!(error2 instanceof Error)) return [typeof error2 === "string" ? error2 : "Unknown error"];
|
|
84
|
+
const lines = [error2.message];
|
|
85
|
+
let cause = error2.cause;
|
|
86
|
+
while (cause instanceof Error) {
|
|
87
|
+
lines.push(cause.message);
|
|
88
|
+
cause = cause.cause;
|
|
89
|
+
}
|
|
90
|
+
if (cause !== void 0 && typeof cause === "string") lines.push(cause);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
62
93
|
function handleCommandError(error2) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
94
|
+
const [message, ...causes] = describe(error2);
|
|
95
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
96
|
+
for (const cause of causes) console.error(chalk.gray(` caused by: ${cause}`));
|
|
97
|
+
if (process.env.SEEDR_DEBUG && error2 instanceof Error && error2.stack) {
|
|
98
|
+
console.error(chalk.gray(error2.stack));
|
|
99
|
+
}
|
|
66
100
|
process.exit(1);
|
|
67
101
|
}
|
|
68
102
|
|
|
@@ -102,71 +136,94 @@ function validateType(value) {
|
|
|
102
136
|
|
|
103
137
|
// src/utils/analytics.ts
|
|
104
138
|
var ANALYTICS_URL = "https://seedr.danieldeusing.de/api/installs";
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
var TELEMETRY_OPT_OUT_VARIABLE = "SEEDR_NO_TELEMETRY";
|
|
140
|
+
var REQUEST_TIMEOUT_MS = 4e3;
|
|
141
|
+
var TELEMETRY_HELP_TEXT = `Sends one anonymous install event per successful agent target to ${ANALYTICS_URL}; set ${TELEMETRY_OPT_OUT_VARIABLE}=1 to disable`;
|
|
142
|
+
function isTelemetryDisabled(env = process.env) {
|
|
143
|
+
return env[TELEMETRY_OPT_OUT_VARIABLE] !== void 0;
|
|
144
|
+
}
|
|
145
|
+
function cliVersion() {
|
|
146
|
+
return true ? "0.1.88" : "dev";
|
|
147
|
+
}
|
|
148
|
+
async function sendEvent(event) {
|
|
149
|
+
try {
|
|
150
|
+
await fetch(ANALYTICS_URL, {
|
|
111
151
|
method: "POST",
|
|
112
152
|
headers: { "Content-Type": "application/json" },
|
|
113
|
-
body: JSON.stringify(
|
|
114
|
-
|
|
115
|
-
type,
|
|
116
|
-
tool: result.agent,
|
|
117
|
-
scope,
|
|
118
|
-
version
|
|
119
|
-
}),
|
|
120
|
-
signal: AbortSignal.timeout(4e3)
|
|
121
|
-
}).catch(() => {
|
|
153
|
+
body: JSON.stringify(event),
|
|
154
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
122
155
|
});
|
|
156
|
+
} catch {
|
|
123
157
|
}
|
|
124
158
|
}
|
|
159
|
+
function trackInstalls(slug, type, results, scope) {
|
|
160
|
+
if (isTelemetryDisabled()) return Promise.resolve();
|
|
161
|
+
const version = cliVersion();
|
|
162
|
+
const sends = results.filter((result) => result.success).map((result) => sendEvent({ slug, type, tool: result.agent, scope, version }));
|
|
163
|
+
return Promise.all(sends).then(() => void 0);
|
|
164
|
+
}
|
|
125
165
|
|
|
126
166
|
// src/config/compatibility.ts
|
|
127
167
|
var AGENT_COMPATIBILITY = {
|
|
128
|
-
skill: ["claude", "copilot", "
|
|
168
|
+
skill: ["claude", "copilot", "antigravity", "codex", "opencode"],
|
|
129
169
|
command: ["claude"],
|
|
130
170
|
agent: ["claude"],
|
|
131
171
|
hook: ["claude"],
|
|
132
172
|
plugin: ["claude"],
|
|
133
173
|
settings: ["claude"],
|
|
134
|
-
mcp: ["claude", "
|
|
174
|
+
mcp: ["claude", "codex", "opencode"]
|
|
135
175
|
};
|
|
176
|
+
function isTypeSupported(type, agent) {
|
|
177
|
+
const canonical = canonicalAgent(agent);
|
|
178
|
+
return canonical !== null && AGENT_COMPATIBILITY[type].includes(canonical);
|
|
179
|
+
}
|
|
136
180
|
function filterCompatibleAgents(type, agents) {
|
|
137
181
|
const compatible = AGENT_COMPATIBILITY[type];
|
|
138
|
-
return agents.filter((a) => compatible.includes(a));
|
|
182
|
+
return canonicalAgents(agents).filter((a) => compatible.includes(a));
|
|
183
|
+
}
|
|
184
|
+
var MCP_UNSUPPORTED_REASONS = {
|
|
185
|
+
copilot: "GitHub Copilot's MCP configuration format could not be verified against primary documentation, so seedr does not write it",
|
|
186
|
+
antigravity: "Google Antigravity's MCP configuration format could not be verified against primary documentation, so seedr does not write it"
|
|
187
|
+
};
|
|
188
|
+
function describeIncompatibility(type, agent) {
|
|
189
|
+
const canonical = canonicalAgent(agent);
|
|
190
|
+
if (type === "mcp" && canonical !== null && MCP_UNSUPPORTED_REASONS[canonical]) {
|
|
191
|
+
return MCP_UNSUPPORTED_REASONS[canonical];
|
|
192
|
+
}
|
|
193
|
+
return `${canonical ?? agent} does not support ${type} content`;
|
|
139
194
|
}
|
|
140
195
|
|
|
141
196
|
// src/handlers/agent.ts
|
|
142
|
-
import { join } from "path";
|
|
143
|
-
import {
|
|
197
|
+
import { join, relative } from "path";
|
|
198
|
+
import { homedir } from "os";
|
|
199
|
+
import { lstat, readdir, unlink } from "fs/promises";
|
|
144
200
|
import chalk2 from "chalk";
|
|
145
201
|
import ora from "ora";
|
|
202
|
+
var SLUG_LABEL = "agent slug";
|
|
203
|
+
async function resolveAgentFilePath(agent, slug, scope, cwd) {
|
|
204
|
+
const destDir = getContentPath(agent, "agent", scope, cwd);
|
|
205
|
+
if (!destDir) return null;
|
|
206
|
+
const scopeRoot = scope === "user" ? homedir() : cwd;
|
|
207
|
+
return resolveContained(scopeRoot, relative(scopeRoot, destDir), `${slug}.md`);
|
|
208
|
+
}
|
|
146
209
|
async function installAgentForCodingAgent(item, agent, scope, method, force, cwd) {
|
|
147
210
|
const spinner = ora(
|
|
148
211
|
`Installing ${item.name} for ${CODING_AGENTS[agent].name}...`
|
|
149
212
|
).start();
|
|
150
213
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
214
|
+
assertValidSlug(item.slug, SLUG_LABEL);
|
|
215
|
+
const destPath = await resolveAgentFilePath(agent, item.slug, scope, cwd);
|
|
216
|
+
if (!destPath) {
|
|
153
217
|
throw new Error(`${CODING_AGENTS[agent].name} does not support agents`);
|
|
154
218
|
}
|
|
155
|
-
const destPath = join(destDir, `${item.slug}.md`);
|
|
156
219
|
await assertOverwritable(destPath, force);
|
|
157
220
|
const sourcePath = getItemSourcePath(item);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await installFile(sourceFile, destPath, "symlink");
|
|
162
|
-
} else {
|
|
163
|
-
const content = await getItemContent(item);
|
|
164
|
-
await ensureDir(destDir);
|
|
165
|
-
await writeTextFile(destPath, content);
|
|
166
|
-
}
|
|
221
|
+
const sourceFile = sourcePath ? join(sourcePath, "AGENT.md") : null;
|
|
222
|
+
if (method === "symlink" && sourceFile && await exists(sourceFile)) {
|
|
223
|
+
await installFile(sourceFile, destPath, "symlink");
|
|
167
224
|
} else {
|
|
168
225
|
const content = await getItemContent(item);
|
|
169
|
-
await ensureDir(
|
|
226
|
+
await ensureDir(getContentPath(agent, "agent", scope, cwd));
|
|
170
227
|
await writeTextFile(destPath, content);
|
|
171
228
|
}
|
|
172
229
|
spinner.succeed(
|
|
@@ -190,13 +247,21 @@ async function installAgent(item, agents, scope, method, force, cwd = process.cw
|
|
|
190
247
|
return results;
|
|
191
248
|
}
|
|
192
249
|
async function uninstallAgent(slug, agent, scope, cwd = process.cwd()) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
250
|
+
assertValidSlug(slug, SLUG_LABEL);
|
|
251
|
+
const destPath = await resolveAgentFilePath(agent, slug, scope, cwd);
|
|
252
|
+
if (!destPath) return false;
|
|
253
|
+
let stats;
|
|
254
|
+
try {
|
|
255
|
+
stats = await lstat(destPath);
|
|
256
|
+
} catch (error2) {
|
|
257
|
+
if (error2.code === "ENOENT") return false;
|
|
258
|
+
throw error2;
|
|
198
259
|
}
|
|
199
|
-
|
|
260
|
+
if (stats.isDirectory()) {
|
|
261
|
+
throw new Error(`${destPath} is a directory; refusing to remove it`);
|
|
262
|
+
}
|
|
263
|
+
await unlink(destPath);
|
|
264
|
+
return true;
|
|
200
265
|
}
|
|
201
266
|
async function getInstalledAgents(agent, scope, cwd = process.cwd()) {
|
|
202
267
|
const destDir = getContentPath(agent, "agent", scope, cwd);
|
|
@@ -206,6 +271,23 @@ async function getInstalledAgents(agent, scope, cwd = process.cwd()) {
|
|
|
206
271
|
const files = await readdir(destDir);
|
|
207
272
|
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
|
|
208
273
|
}
|
|
274
|
+
async function planAgent(item, agents, scope, method, cwd) {
|
|
275
|
+
assertValidSlug(item.slug, SLUG_LABEL);
|
|
276
|
+
const changes = [];
|
|
277
|
+
for (const agent of agents) {
|
|
278
|
+
const destPath = await resolveAgentFilePath(agent, item.slug, scope, cwd);
|
|
279
|
+
if (!destPath) throw new Error(`${CODING_AGENTS[agent].name} does not support agents`);
|
|
280
|
+
const sourcePath = getItemSourcePath(item);
|
|
281
|
+
const linked = method === "symlink" && sourcePath !== null && await exists(join(sourcePath, "AGENT.md"));
|
|
282
|
+
changes.push({
|
|
283
|
+
agent,
|
|
284
|
+
kind: await exists(destPath) ? "modify" : "create",
|
|
285
|
+
path: destPath,
|
|
286
|
+
detail: linked ? `symlink \u2192 ${join(sourcePath, "AGENT.md")}` : "agent definition file"
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return changes;
|
|
290
|
+
}
|
|
209
291
|
var agentHandler = {
|
|
210
292
|
type: "agent",
|
|
211
293
|
async install(item, agents, scope, method, force, cwd) {
|
|
@@ -216,18 +298,19 @@ var agentHandler = {
|
|
|
216
298
|
},
|
|
217
299
|
async listInstalled(agent, scope, cwd) {
|
|
218
300
|
return getInstalledAgents(agent, scope, cwd);
|
|
219
|
-
}
|
|
301
|
+
},
|
|
302
|
+
plan: planAgent
|
|
220
303
|
};
|
|
221
304
|
|
|
222
305
|
// src/handlers/hook.ts
|
|
223
|
-
import { join as join2, basename } from "path";
|
|
224
|
-
import { homedir } from "os";
|
|
225
|
-
import { mkdir,
|
|
306
|
+
import { join as join2, basename, dirname as dirname2 } from "path";
|
|
307
|
+
import { homedir as homedir2 } from "os";
|
|
308
|
+
import { lstat as lstat2, mkdir, mkdtemp, readFile as readFile2, realpath, rm, unlink as unlink2 } from "fs/promises";
|
|
226
309
|
import chalk3 from "chalk";
|
|
227
310
|
import ora2 from "ora";
|
|
228
311
|
|
|
229
312
|
// src/utils/json.ts
|
|
230
|
-
import { readFile,
|
|
313
|
+
import { readFile, stat } from "fs/promises";
|
|
231
314
|
import { dirname } from "path";
|
|
232
315
|
async function readJson(path) {
|
|
233
316
|
try {
|
|
@@ -237,12 +320,24 @@ async function readJson(path) {
|
|
|
237
320
|
if (error2.code === "ENOENT") {
|
|
238
321
|
return {};
|
|
239
322
|
}
|
|
323
|
+
if (error2 instanceof SyntaxError) {
|
|
324
|
+
throw new Error(`${path} is not valid JSON: ${error2.message}`, { cause: error2 });
|
|
325
|
+
}
|
|
326
|
+
throw error2;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function existingFileMode(path) {
|
|
330
|
+
try {
|
|
331
|
+
return (await stat(path)).mode & 511;
|
|
332
|
+
} catch (error2) {
|
|
333
|
+
if (error2.code === "ENOENT") return void 0;
|
|
240
334
|
throw error2;
|
|
241
335
|
}
|
|
242
336
|
}
|
|
243
337
|
async function writeJson(path, data) {
|
|
244
338
|
await ensureDir(dirname(path));
|
|
245
|
-
await
|
|
339
|
+
const mode = await existingFileMode(path);
|
|
340
|
+
await writeFileAtomic(path, JSON.stringify(data, null, 2) + "\n", { mode });
|
|
246
341
|
}
|
|
247
342
|
function deepMerge(target, source) {
|
|
248
343
|
const result = { ...target };
|
|
@@ -262,14 +357,14 @@ function deepMerge(target, source) {
|
|
|
262
357
|
}
|
|
263
358
|
|
|
264
359
|
// src/handlers/hook.ts
|
|
360
|
+
var CLAUDE_ONLY_ERROR = "Hooks are only supported for Claude Code";
|
|
361
|
+
var SCRIPT_NAME_LABEL = "hook script name";
|
|
362
|
+
var SCRIPT_MODE = 493;
|
|
363
|
+
function getScopeRoot(scope, cwd) {
|
|
364
|
+
return scope === "user" ? homedir2() : cwd;
|
|
365
|
+
}
|
|
265
366
|
function getHooksDir(scope, cwd) {
|
|
266
|
-
|
|
267
|
-
case "user":
|
|
268
|
-
return join2(homedir(), ".claude", "hooks");
|
|
269
|
-
case "project":
|
|
270
|
-
case "local":
|
|
271
|
-
return join2(cwd, ".claude", "hooks");
|
|
272
|
-
}
|
|
367
|
+
return join2(getScopeRoot(scope, cwd), ".claude", "hooks");
|
|
273
368
|
}
|
|
274
369
|
function getScriptPath(scope, scriptName) {
|
|
275
370
|
switch (scope) {
|
|
@@ -290,13 +385,65 @@ function findScriptFile(item) {
|
|
|
290
385
|
}
|
|
291
386
|
return null;
|
|
292
387
|
}
|
|
388
|
+
async function prepareHooksDir(scope, cwd) {
|
|
389
|
+
const scopeRoot = getScopeRoot(scope, cwd);
|
|
390
|
+
const hooksDir = await resolveContained(scopeRoot, ".claude", "hooks");
|
|
391
|
+
await mkdir(hooksDir, { recursive: true });
|
|
392
|
+
await assertDirectoryWithin(hooksDir, scopeRoot, "hooks directory");
|
|
393
|
+
return realpath(hooksDir);
|
|
394
|
+
}
|
|
395
|
+
async function assertRegularFileOrMissing(path) {
|
|
396
|
+
let stats;
|
|
397
|
+
try {
|
|
398
|
+
stats = await lstat2(path);
|
|
399
|
+
} catch (error2) {
|
|
400
|
+
if (error2.code === "ENOENT") return;
|
|
401
|
+
throw error2;
|
|
402
|
+
}
|
|
403
|
+
if (stats.isSymbolicLink()) {
|
|
404
|
+
throw new Error(`${path} is a symbolic link; refusing to write through it \u2014 remove it first`);
|
|
405
|
+
}
|
|
406
|
+
if (!stats.isFile()) {
|
|
407
|
+
throw new Error(`${path} exists and is not a regular file; refusing to replace it`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function loadScriptBytes(item, scriptFile, hooksDir) {
|
|
411
|
+
const sourcePath = getItemSourcePath(item);
|
|
412
|
+
if (sourcePath && await exists(sourcePath)) {
|
|
413
|
+
return readFile2(join2(sourcePath, scriptFile));
|
|
414
|
+
}
|
|
415
|
+
const fetchDir = await mkdtemp(join2(dirname2(hooksDir), ".seedr-hook-"));
|
|
416
|
+
try {
|
|
417
|
+
const contentDir = join2(fetchDir, "content");
|
|
418
|
+
await fetchItemToDestination(item, contentDir);
|
|
419
|
+
return await readFile2(join2(contentDir, scriptFile));
|
|
420
|
+
} finally {
|
|
421
|
+
await rm(fetchDir, { recursive: true, force: true });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function mergeTriggers(hooks, triggers, command) {
|
|
425
|
+
for (const trigger of triggers) {
|
|
426
|
+
const entries = hooks[trigger.event] ??= [];
|
|
427
|
+
const existingEntry = entries.find((entry) => entry.matcher === trigger.matcher);
|
|
428
|
+
const hookCommand = { type: "command", command };
|
|
429
|
+
if (existingEntry) {
|
|
430
|
+
if (!existingEntry.hooks.some((hook) => hook.command === command)) {
|
|
431
|
+
existingEntry.hooks.push(hookCommand);
|
|
432
|
+
}
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const newEntry = { hooks: [hookCommand] };
|
|
436
|
+
if (trigger.matcher) newEntry.matcher = trigger.matcher;
|
|
437
|
+
entries.push(newEntry);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
293
440
|
async function installHookForAgent(item, agent, scope, _method, force, cwd) {
|
|
294
441
|
const spinner = ora2(
|
|
295
442
|
`Installing ${item.name} for ${CODING_AGENTS[agent].name}...`
|
|
296
443
|
).start();
|
|
297
444
|
try {
|
|
298
445
|
if (agent !== "claude") {
|
|
299
|
-
throw new Error(
|
|
446
|
+
throw new Error(CLAUDE_ONLY_ERROR);
|
|
300
447
|
}
|
|
301
448
|
const triggers = item.contents?.triggers;
|
|
302
449
|
if (!triggers || triggers.length === 0) {
|
|
@@ -306,53 +453,24 @@ async function installHookForAgent(item, agent, scope, _method, force, cwd) {
|
|
|
306
453
|
if (!scriptFile) {
|
|
307
454
|
throw new Error("No script file found in hook");
|
|
308
455
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
456
|
+
assertSafePathSegment(scriptFile, SCRIPT_NAME_LABEL);
|
|
457
|
+
const hooksDir = await prepareHooksDir(scope, cwd);
|
|
458
|
+
const destScriptPath = await resolveContained(hooksDir, scriptFile);
|
|
459
|
+
await assertRegularFileOrMissing(destScriptPath);
|
|
313
460
|
await assertOverwritable(destScriptPath, force);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
await
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
settings.hooks = settings.hooks || {};
|
|
327
|
-
const scriptPath = getScriptPath(scope, scriptFile);
|
|
328
|
-
for (const trigger of triggers) {
|
|
329
|
-
const event = trigger.event;
|
|
330
|
-
const matcher = trigger.matcher;
|
|
331
|
-
if (!settings.hooks[event]) {
|
|
332
|
-
settings.hooks[event] = [];
|
|
333
|
-
}
|
|
334
|
-
const existingEntry = settings.hooks[event].find(
|
|
335
|
-
(e) => e.matcher === matcher
|
|
336
|
-
);
|
|
337
|
-
const hookCommand = { type: "command", command: scriptPath };
|
|
338
|
-
if (existingEntry) {
|
|
339
|
-
const alreadyExists = existingEntry.hooks.some(
|
|
340
|
-
(h) => h.command === scriptPath
|
|
341
|
-
);
|
|
342
|
-
if (!alreadyExists) {
|
|
343
|
-
existingEntry.hooks.push(hookCommand);
|
|
344
|
-
}
|
|
345
|
-
} else {
|
|
346
|
-
const newEntry = {
|
|
347
|
-
hooks: [hookCommand]
|
|
348
|
-
};
|
|
349
|
-
if (matcher) {
|
|
350
|
-
newEntry.matcher = matcher;
|
|
351
|
-
}
|
|
352
|
-
settings.hooks[event].push(newEntry);
|
|
353
|
-
}
|
|
461
|
+
const scriptBytes = await loadScriptBytes(item, scriptFile, hooksDir);
|
|
462
|
+
const previousScript = await snapshotFile(destScriptPath);
|
|
463
|
+
await writeFileAtomic(destScriptPath, scriptBytes, { mode: SCRIPT_MODE });
|
|
464
|
+
try {
|
|
465
|
+
const settingsPath = getSettingsPath(scope, cwd);
|
|
466
|
+
const settings = await readJson(settingsPath);
|
|
467
|
+
settings.hooks = settings.hooks || {};
|
|
468
|
+
mergeTriggers(settings.hooks, triggers, getScriptPath(scope, scriptFile));
|
|
469
|
+
await writeJson(settingsPath, settings);
|
|
470
|
+
} catch (error2) {
|
|
471
|
+
await restoreFile(destScriptPath, previousScript);
|
|
472
|
+
throw error2;
|
|
354
473
|
}
|
|
355
|
-
await writeJson(settingsPath, settings);
|
|
356
474
|
spinner.succeed(
|
|
357
475
|
brand(`Installed ${item.name} for ${CODING_AGENTS[agent].name}`)
|
|
358
476
|
);
|
|
@@ -373,7 +491,47 @@ async function installHook(item, agents, scope, method, force, cwd = process.cwd
|
|
|
373
491
|
}
|
|
374
492
|
return results;
|
|
375
493
|
}
|
|
494
|
+
function removeHookCommands(hooks, slug, expectedPath) {
|
|
495
|
+
let removed = false;
|
|
496
|
+
const belongsToHook = (hook) => expectedPath !== null && hook.command === expectedPath || basename(hook.command, ".sh") === slug;
|
|
497
|
+
for (const event of Object.keys(hooks)) {
|
|
498
|
+
const entries = hooks[event] ?? [];
|
|
499
|
+
const keptEntries = [];
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
const keptHooks = entry.hooks.filter((hook) => !belongsToHook(hook));
|
|
502
|
+
if (keptHooks.length < entry.hooks.length) removed = true;
|
|
503
|
+
if (keptHooks.length > 0) keptEntries.push({ ...entry, hooks: keptHooks });
|
|
504
|
+
}
|
|
505
|
+
if (keptEntries.length === 0) {
|
|
506
|
+
delete hooks[event];
|
|
507
|
+
} else {
|
|
508
|
+
hooks[event] = keptEntries;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return removed;
|
|
512
|
+
}
|
|
513
|
+
async function removeScriptFile(scope, cwd, scriptFileName) {
|
|
514
|
+
const scopeRoot = getScopeRoot(scope, cwd);
|
|
515
|
+
const hooksDir = getHooksDir(scope, cwd);
|
|
516
|
+
if (!await exists(hooksDir)) return false;
|
|
517
|
+
await assertDirectoryWithin(hooksDir, scopeRoot, "hooks directory");
|
|
518
|
+
assertSafePathSegment(scriptFileName, SCRIPT_NAME_LABEL);
|
|
519
|
+
const scriptFilePath = await resolveContained(hooksDir, scriptFileName);
|
|
520
|
+
let stats;
|
|
521
|
+
try {
|
|
522
|
+
stats = await lstat2(scriptFilePath);
|
|
523
|
+
} catch (error2) {
|
|
524
|
+
if (error2.code === "ENOENT") return false;
|
|
525
|
+
throw error2;
|
|
526
|
+
}
|
|
527
|
+
if (stats.isDirectory()) {
|
|
528
|
+
throw new Error(`${scriptFilePath} is a directory; refusing to remove it`);
|
|
529
|
+
}
|
|
530
|
+
await unlink2(scriptFilePath);
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
376
533
|
async function uninstallHook(slug, agent, scope, cwd = process.cwd()) {
|
|
534
|
+
assertValidSlug(slug, "hook slug");
|
|
377
535
|
if (agent !== "claude") return false;
|
|
378
536
|
const settingsPath = getSettingsPath(scope, cwd);
|
|
379
537
|
if (!await exists(settingsPath)) return false;
|
|
@@ -382,43 +540,15 @@ async function uninstallHook(slug, agent, scope, cwd = process.cwd()) {
|
|
|
382
540
|
const expectedPath = scriptFile ? getScriptPath(scope, scriptFile) : null;
|
|
383
541
|
const settings = await readJson(settingsPath);
|
|
384
542
|
if (!settings.hooks) return false;
|
|
385
|
-
let removed =
|
|
386
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
387
|
-
const entries = settings.hooks[event];
|
|
388
|
-
if (!entries) continue;
|
|
389
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
390
|
-
const entry = entries[i];
|
|
391
|
-
const originalLength = entry.hooks.length;
|
|
392
|
-
entry.hooks = entry.hooks.filter((h) => {
|
|
393
|
-
if (expectedPath && h.command === expectedPath) return false;
|
|
394
|
-
const cmdBasename = basename(h.command, ".sh");
|
|
395
|
-
return cmdBasename !== slug;
|
|
396
|
-
});
|
|
397
|
-
if (entry.hooks.length < originalLength) {
|
|
398
|
-
removed = true;
|
|
399
|
-
}
|
|
400
|
-
if (entry.hooks.length === 0) {
|
|
401
|
-
entries.splice(i, 1);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (entries.length === 0) {
|
|
405
|
-
delete settings.hooks[event];
|
|
406
|
-
}
|
|
407
|
-
}
|
|
543
|
+
let removed = removeHookCommands(settings.hooks, slug, expectedPath);
|
|
408
544
|
if (Object.keys(settings.hooks).length === 0) {
|
|
409
545
|
delete settings.hooks;
|
|
410
546
|
}
|
|
411
547
|
if (removed) {
|
|
412
548
|
await writeJson(settingsPath, settings);
|
|
413
549
|
}
|
|
414
|
-
|
|
415
|
-
const scriptFileName = scriptFile || `${slug}.sh`;
|
|
416
|
-
const scriptFilePath = join2(hooksDir, scriptFileName);
|
|
417
|
-
try {
|
|
418
|
-
await rm(scriptFilePath);
|
|
550
|
+
if (await removeScriptFile(scope, cwd, scriptFile || `${slug}.sh`)) {
|
|
419
551
|
removed = true;
|
|
420
|
-
} catch (e) {
|
|
421
|
-
if (e.code !== "ENOENT") throw e;
|
|
422
552
|
}
|
|
423
553
|
return removed;
|
|
424
554
|
}
|
|
@@ -432,13 +562,35 @@ async function getInstalledHooks(agent, scope, cwd = process.cwd()) {
|
|
|
432
562
|
for (const entries of Object.values(settings.hooks)) {
|
|
433
563
|
for (const entry of entries) {
|
|
434
564
|
for (const hook of entry.hooks) {
|
|
435
|
-
|
|
436
|
-
slugs.add(slug);
|
|
565
|
+
slugs.add(basename(hook.command, ".sh"));
|
|
437
566
|
}
|
|
438
567
|
}
|
|
439
568
|
}
|
|
440
569
|
return Array.from(slugs);
|
|
441
570
|
}
|
|
571
|
+
async function planHook(item, agents, scope, _method, cwd) {
|
|
572
|
+
const changes = [];
|
|
573
|
+
for (const agent of agents) {
|
|
574
|
+
if (agent !== "claude") throw new Error(CLAUDE_ONLY_ERROR);
|
|
575
|
+
const scriptFile = findScriptFile(item);
|
|
576
|
+
if (!scriptFile) throw new Error("No script file found in hook");
|
|
577
|
+
assertSafePathSegment(scriptFile, SCRIPT_NAME_LABEL);
|
|
578
|
+
const scopeRoot = getScopeRoot(scope, cwd);
|
|
579
|
+
const scriptPath = await resolveContained(scopeRoot, ".claude", "hooks", scriptFile);
|
|
580
|
+
const settingsPath = getSettingsPath(scope, cwd);
|
|
581
|
+
const events = (item.contents?.triggers ?? []).map((trigger) => trigger.matcher ? `${trigger.event}[${trigger.matcher}]` : trigger.event).join(", ");
|
|
582
|
+
changes.push(
|
|
583
|
+
{ agent, kind: await exists(scriptPath) ? "modify" : "create", path: scriptPath, detail: "executable hook script" },
|
|
584
|
+
{
|
|
585
|
+
agent,
|
|
586
|
+
kind: await exists(settingsPath) ? "modify" : "create",
|
|
587
|
+
path: settingsPath,
|
|
588
|
+
detail: `hooks: ${events || "(no triggers)"} \u2192 ${getScriptPath(scope, scriptFile)}`
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return changes;
|
|
593
|
+
}
|
|
442
594
|
var hookHandler = {
|
|
443
595
|
type: "hook",
|
|
444
596
|
async install(item, agents, scope, method, force, cwd) {
|
|
@@ -449,36 +601,362 @@ var hookHandler = {
|
|
|
449
601
|
},
|
|
450
602
|
async listInstalled(agent, scope, cwd) {
|
|
451
603
|
return getInstalledHooks(agent, scope, cwd);
|
|
452
|
-
}
|
|
604
|
+
},
|
|
605
|
+
plan: planHook
|
|
453
606
|
};
|
|
454
607
|
|
|
455
608
|
// src/handlers/mcp.ts
|
|
609
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
610
|
+
import { dirname as dirname3 } from "path";
|
|
456
611
|
import chalk4 from "chalk";
|
|
457
612
|
import ora3 from "ora";
|
|
613
|
+
|
|
614
|
+
// src/utils/toml.ts
|
|
615
|
+
var BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
616
|
+
function parseTomlHeader(line) {
|
|
617
|
+
const trimmed = line.trim();
|
|
618
|
+
if (!trimmed.startsWith("[")) return null;
|
|
619
|
+
const isArrayTable = trimmed.startsWith("[[");
|
|
620
|
+
const open = isArrayTable ? "[[" : "[";
|
|
621
|
+
const close = isArrayTable ? "]]" : "]";
|
|
622
|
+
const closeIndex = findHeaderClose(trimmed, open.length, close);
|
|
623
|
+
if (closeIndex === -1) return null;
|
|
624
|
+
const inner = trimmed.slice(open.length, closeIndex);
|
|
625
|
+
return splitKeyPath(inner);
|
|
626
|
+
}
|
|
627
|
+
function findHeaderClose(line, start, close) {
|
|
628
|
+
let quote = null;
|
|
629
|
+
for (let i = start; i < line.length; i++) {
|
|
630
|
+
const ch = line[i];
|
|
631
|
+
if (quote) {
|
|
632
|
+
if (ch === "\\" && quote === '"') {
|
|
633
|
+
i++;
|
|
634
|
+
} else if (ch === quote) {
|
|
635
|
+
quote = null;
|
|
636
|
+
}
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (ch === '"' || ch === "'") {
|
|
640
|
+
quote = ch;
|
|
641
|
+
} else if (line.startsWith(close, i)) {
|
|
642
|
+
return i;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return -1;
|
|
646
|
+
}
|
|
647
|
+
function readQuotedKey(inner, start) {
|
|
648
|
+
const quote = inner[start];
|
|
649
|
+
let text = "";
|
|
650
|
+
for (let i = start + 1; i < inner.length; i++) {
|
|
651
|
+
const ch = inner[i];
|
|
652
|
+
if (ch === quote) return { text, end: i + 1 };
|
|
653
|
+
if (ch === "\\" && quote === '"') {
|
|
654
|
+
const next = inner[i + 1];
|
|
655
|
+
if (next === void 0) return null;
|
|
656
|
+
text += unescapeBasic(next);
|
|
657
|
+
i++;
|
|
658
|
+
} else {
|
|
659
|
+
text += ch;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
function readKeyPart(inner, start) {
|
|
665
|
+
let text = "";
|
|
666
|
+
let sawQuoted = false;
|
|
667
|
+
let i = start;
|
|
668
|
+
while (i < inner.length && inner[i] !== ".") {
|
|
669
|
+
const ch = inner[i];
|
|
670
|
+
if (ch === '"' || ch === "'") {
|
|
671
|
+
const quoted = readQuotedKey(inner, i);
|
|
672
|
+
if (!quoted) return null;
|
|
673
|
+
text += quoted.text;
|
|
674
|
+
sawQuoted = true;
|
|
675
|
+
i = quoted.end;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (ch !== " " && ch !== " ") text += ch;
|
|
679
|
+
i++;
|
|
680
|
+
}
|
|
681
|
+
if (text === "" && !sawQuoted) return null;
|
|
682
|
+
return { text, end: i };
|
|
683
|
+
}
|
|
684
|
+
function splitKeyPath(inner) {
|
|
685
|
+
const parts = [];
|
|
686
|
+
let i = 0;
|
|
687
|
+
for (; ; ) {
|
|
688
|
+
const part = readKeyPart(inner, i);
|
|
689
|
+
if (!part) return null;
|
|
690
|
+
parts.push(part.text);
|
|
691
|
+
if (part.end >= inner.length) return parts;
|
|
692
|
+
i = part.end + 1;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function unescapeBasic(ch) {
|
|
696
|
+
switch (ch) {
|
|
697
|
+
case "n":
|
|
698
|
+
return "\n";
|
|
699
|
+
case "t":
|
|
700
|
+
return " ";
|
|
701
|
+
case "r":
|
|
702
|
+
return "\r";
|
|
703
|
+
case "b":
|
|
704
|
+
return "\b";
|
|
705
|
+
case "f":
|
|
706
|
+
return "\f";
|
|
707
|
+
default:
|
|
708
|
+
return ch;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function parseTomlSections(text) {
|
|
712
|
+
const lines = text.split(/\r?\n/);
|
|
713
|
+
const sections = [{ keyPath: [], lines: [] }];
|
|
714
|
+
for (const line of lines) {
|
|
715
|
+
const keyPath = parseTomlHeader(line);
|
|
716
|
+
if (keyPath) {
|
|
717
|
+
sections.push({ keyPath, lines: [line] });
|
|
718
|
+
} else {
|
|
719
|
+
sections[sections.length - 1].lines.push(line);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return sections;
|
|
723
|
+
}
|
|
724
|
+
function keyPathStartsWith(keyPath, prefix) {
|
|
725
|
+
return prefix.length <= keyPath.length && prefix.every((part, i) => keyPath[i] === part);
|
|
726
|
+
}
|
|
727
|
+
function trimBlankEdges(lines) {
|
|
728
|
+
let start = 0;
|
|
729
|
+
let end = lines.length;
|
|
730
|
+
while (start < end && lines[start].trim() === "") start++;
|
|
731
|
+
while (end > start && lines[end - 1].trim() === "") end--;
|
|
732
|
+
return lines.slice(start, end);
|
|
733
|
+
}
|
|
734
|
+
function joinSections(sections) {
|
|
735
|
+
const blocks = sections.map((section) => trimBlankEdges(section.lines)).filter((lines) => lines.length > 0).map((lines) => lines.join("\n"));
|
|
736
|
+
return blocks.length === 0 ? "" : blocks.join("\n\n") + "\n";
|
|
737
|
+
}
|
|
738
|
+
function formatTomlKey(key) {
|
|
739
|
+
return BARE_KEY_PATTERN.test(key) ? key : formatTomlString(key);
|
|
740
|
+
}
|
|
741
|
+
function formatTomlString(value) {
|
|
742
|
+
let out = '"';
|
|
743
|
+
for (const ch of value) {
|
|
744
|
+
const code = ch.codePointAt(0);
|
|
745
|
+
if (ch === '"') out += '\\"';
|
|
746
|
+
else if (ch === "\\") out += "\\\\";
|
|
747
|
+
else if (ch === "\n") out += "\\n";
|
|
748
|
+
else if (ch === " ") out += "\\t";
|
|
749
|
+
else if (ch === "\r") out += "\\r";
|
|
750
|
+
else if (code < 32 || code === 127) out += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
751
|
+
else out += ch;
|
|
752
|
+
}
|
|
753
|
+
return out + '"';
|
|
754
|
+
}
|
|
755
|
+
function formatTomlValue(value) {
|
|
756
|
+
if (Array.isArray(value)) {
|
|
757
|
+
return `[${value.map((v) => formatTomlValue(v)).join(", ")}]`;
|
|
758
|
+
}
|
|
759
|
+
if (typeof value === "string") return formatTomlString(value);
|
|
760
|
+
if (typeof value === "number") {
|
|
761
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot encode ${value} as TOML`);
|
|
762
|
+
return String(value);
|
|
763
|
+
}
|
|
764
|
+
return value ? "true" : "false";
|
|
765
|
+
}
|
|
766
|
+
function renderTable(table) {
|
|
767
|
+
const header = `[${table.keyPath.map(formatTomlKey).join(".")}]`;
|
|
768
|
+
const lines = [header];
|
|
769
|
+
for (const [key, value] of Object.entries(table.entries)) {
|
|
770
|
+
lines.push(`${formatTomlKey(key)} = ${formatTomlValue(value)}`);
|
|
771
|
+
}
|
|
772
|
+
return { keyPath: table.keyPath, lines };
|
|
773
|
+
}
|
|
774
|
+
function upsertTomlTables(text, prefix, tables) {
|
|
775
|
+
const kept = parseTomlSections(text).filter((section) => !keyPathStartsWith(section.keyPath, prefix));
|
|
776
|
+
return joinSections([...kept, ...tables.map(renderTable)]);
|
|
777
|
+
}
|
|
778
|
+
function removeTomlTables(text, prefix) {
|
|
779
|
+
const sections = parseTomlSections(text);
|
|
780
|
+
const kept = sections.filter((section) => !keyPathStartsWith(section.keyPath, prefix));
|
|
781
|
+
if (kept.length === sections.length) return { text, removed: false };
|
|
782
|
+
return { text: joinSections(kept), removed: true };
|
|
783
|
+
}
|
|
784
|
+
function hasTomlTable(text, keyPath) {
|
|
785
|
+
return parseTomlSections(text).some(
|
|
786
|
+
(section) => section.keyPath.length === keyPath.length && keyPathStartsWith(section.keyPath, keyPath)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
function listTomlChildTables(text, parent) {
|
|
790
|
+
const names = /* @__PURE__ */ new Set();
|
|
791
|
+
for (const section of parseTomlSections(text)) {
|
|
792
|
+
if (section.keyPath.length === parent.length + 1 && keyPathStartsWith(section.keyPath, parent)) {
|
|
793
|
+
names.add(section.keyPath[parent.length]);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return Array.from(names);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/handlers/mcp.ts
|
|
800
|
+
var MCP_SERVERS_KEY = "mcpServers";
|
|
801
|
+
var CODEX_TABLE = "mcp_servers";
|
|
802
|
+
var NO_COMMAND_ERROR = "MCP definition has no command for a stdio server";
|
|
803
|
+
var NO_URL_ERROR = "MCP definition has no url for a remote server";
|
|
458
804
|
function parseMcpDefinition(content) {
|
|
805
|
+
let parsed;
|
|
459
806
|
try {
|
|
460
|
-
|
|
807
|
+
parsed = JSON.parse(content);
|
|
461
808
|
} catch {
|
|
462
809
|
throw new Error("Invalid MCP definition: must be valid JSON");
|
|
463
810
|
}
|
|
811
|
+
const definition = parsed;
|
|
812
|
+
if (!definition || typeof definition.name !== "string" || definition.name === "" || typeof definition.config !== "object" || definition.config === null) {
|
|
813
|
+
throw new Error('Invalid MCP definition: expected { "name": string, "config": object }');
|
|
814
|
+
}
|
|
815
|
+
return definition;
|
|
816
|
+
}
|
|
817
|
+
function transportOf(config) {
|
|
818
|
+
if (config.type === "http" || config.type === "sse") return config.type;
|
|
819
|
+
if (config.type === void 0 && config.url && !config.command) return "http";
|
|
820
|
+
return "stdio";
|
|
821
|
+
}
|
|
822
|
+
function definedEntries(object) {
|
|
823
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
|
|
824
|
+
}
|
|
825
|
+
function toOpenCodeServer(config) {
|
|
826
|
+
if (transportOf(config) === "stdio") {
|
|
827
|
+
if (!config.command) throw new Error(NO_COMMAND_ERROR);
|
|
828
|
+
return definedEntries({
|
|
829
|
+
type: "local",
|
|
830
|
+
command: [config.command, ...config.args ?? []],
|
|
831
|
+
environment: config.env,
|
|
832
|
+
enabled: true
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
if (!config.url) throw new Error(NO_URL_ERROR);
|
|
836
|
+
return definedEntries({ type: "remote", url: config.url, headers: config.headers, enabled: true });
|
|
837
|
+
}
|
|
838
|
+
function codexMainEntries(config) {
|
|
839
|
+
if (transportOf(config) === "stdio") {
|
|
840
|
+
if (!config.command) throw new Error(NO_COMMAND_ERROR);
|
|
841
|
+
return definedEntries({
|
|
842
|
+
command: config.command,
|
|
843
|
+
args: config.args && config.args.length > 0 ? config.args : void 0,
|
|
844
|
+
cwd: config.cwd || void 0
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (!config.url) throw new Error(NO_URL_ERROR);
|
|
848
|
+
return definedEntries({
|
|
849
|
+
url: config.url,
|
|
850
|
+
bearer_token_env_var: config.bearer_token_env_var || void 0
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
function hasEntries(record) {
|
|
854
|
+
return record !== void 0 && Object.keys(record).length > 0;
|
|
855
|
+
}
|
|
856
|
+
function toCodexTables(name, config) {
|
|
857
|
+
const base = [CODEX_TABLE, name];
|
|
858
|
+
const tables = [{ keyPath: base, entries: codexMainEntries(config) }];
|
|
859
|
+
if (hasEntries(config.env)) {
|
|
860
|
+
tables.push({ keyPath: [...base, "env"], entries: { ...config.env } });
|
|
861
|
+
}
|
|
862
|
+
if (hasEntries(config.headers)) {
|
|
863
|
+
tables.push({ keyPath: [...base, "http_headers"], entries: { ...config.headers } });
|
|
864
|
+
}
|
|
865
|
+
return tables;
|
|
866
|
+
}
|
|
867
|
+
function serverMapOf(doc, key) {
|
|
868
|
+
const servers = doc[key];
|
|
869
|
+
return servers !== null && typeof servers === "object" && !Array.isArray(servers) ? servers : {};
|
|
870
|
+
}
|
|
871
|
+
async function readDocument(path) {
|
|
872
|
+
return await exists(path) ? readJson(path) : null;
|
|
873
|
+
}
|
|
874
|
+
function jsonAdapter(agent, key, translate, defaults = {}) {
|
|
875
|
+
return {
|
|
876
|
+
configPath: (scope, cwd) => getMcpConfigPath(agent, scope, cwd),
|
|
877
|
+
describeEntry: (name) => `${key}.${name}`,
|
|
878
|
+
async has(path, name) {
|
|
879
|
+
const doc = await readDocument(path);
|
|
880
|
+
return doc !== null && name in serverMapOf(doc, key);
|
|
881
|
+
},
|
|
882
|
+
async write(path, name, config) {
|
|
883
|
+
const doc = await readDocument(path) ?? { ...defaults };
|
|
884
|
+
const servers = serverMapOf(doc, key);
|
|
885
|
+
servers[name] = translate(config);
|
|
886
|
+
doc[key] = servers;
|
|
887
|
+
await writeJson(path, doc);
|
|
888
|
+
},
|
|
889
|
+
async remove(path, name) {
|
|
890
|
+
const doc = await readDocument(path);
|
|
891
|
+
if (doc === null) return false;
|
|
892
|
+
const servers = serverMapOf(doc, key);
|
|
893
|
+
if (!(name in servers)) return false;
|
|
894
|
+
delete servers[name];
|
|
895
|
+
doc[key] = servers;
|
|
896
|
+
await writeJson(path, doc);
|
|
897
|
+
return true;
|
|
898
|
+
},
|
|
899
|
+
async list(path) {
|
|
900
|
+
const doc = await readDocument(path);
|
|
901
|
+
return doc === null ? [] : Object.keys(serverMapOf(doc, key));
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
async function readTextOrEmpty(path) {
|
|
906
|
+
try {
|
|
907
|
+
return await readFile3(path, "utf-8");
|
|
908
|
+
} catch (error2) {
|
|
909
|
+
if (error2.code === "ENOENT") return "";
|
|
910
|
+
throw error2;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
var codexAdapter = {
|
|
914
|
+
configPath: (scope, cwd) => getMcpConfigPath("codex", scope, cwd),
|
|
915
|
+
describeEntry: (name) => `[${CODEX_TABLE}.${name}]`,
|
|
916
|
+
async has(path, name) {
|
|
917
|
+
return hasTomlTable(await readTextOrEmpty(path), [CODEX_TABLE, name]);
|
|
918
|
+
},
|
|
919
|
+
async write(path, name, config) {
|
|
920
|
+
const text = await readTextOrEmpty(path);
|
|
921
|
+
await ensureDir(dirname3(path));
|
|
922
|
+
await writeFileAtomic(path, upsertTomlTables(text, [CODEX_TABLE, name], toCodexTables(name, config)));
|
|
923
|
+
},
|
|
924
|
+
async remove(path, name) {
|
|
925
|
+
if (!await exists(path)) return false;
|
|
926
|
+
const { text, removed } = removeTomlTables(await readTextOrEmpty(path), [CODEX_TABLE, name]);
|
|
927
|
+
if (removed) await writeFileAtomic(path, text);
|
|
928
|
+
return removed;
|
|
929
|
+
},
|
|
930
|
+
async list(path) {
|
|
931
|
+
return listTomlChildTables(await readTextOrEmpty(path), [CODEX_TABLE]);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
var ADAPTERS = {
|
|
935
|
+
claude: jsonAdapter("claude", MCP_SERVERS_KEY, (config) => ({ ...config })),
|
|
936
|
+
opencode: jsonAdapter("opencode", "mcp", toOpenCodeServer, { $schema: "https://opencode.ai/config.json" }),
|
|
937
|
+
codex: codexAdapter
|
|
938
|
+
};
|
|
939
|
+
function adapterFor(agent) {
|
|
940
|
+
const adapter = ADAPTERS[agent];
|
|
941
|
+
if (!adapter || !isTypeSupported("mcp", agent)) {
|
|
942
|
+
throw new Error(`MCP servers are not supported for ${CODING_AGENTS[agent].name}`);
|
|
943
|
+
}
|
|
944
|
+
return adapter;
|
|
464
945
|
}
|
|
465
946
|
async function installMcpForAgent(item, agent, scope, _method, force, cwd) {
|
|
466
947
|
const spinner = ora3(
|
|
467
948
|
`Installing ${item.name} for ${CODING_AGENTS[agent].name}...`
|
|
468
949
|
).start();
|
|
469
950
|
try {
|
|
470
|
-
const
|
|
471
|
-
const mcpDef = parseMcpDefinition(
|
|
472
|
-
const configPath =
|
|
473
|
-
|
|
474
|
-
config.mcpServers = config.mcpServers || {};
|
|
475
|
-
if (!force && mcpDef.name in config.mcpServers) {
|
|
951
|
+
const adapter = adapterFor(agent);
|
|
952
|
+
const mcpDef = parseMcpDefinition(await getItemContent(item));
|
|
953
|
+
const configPath = adapter.configPath(scope, cwd);
|
|
954
|
+
if (!force && await adapter.has(configPath, mcpDef.name)) {
|
|
476
955
|
throw new Error(
|
|
477
956
|
`MCP server "${mcpDef.name}" already exists in ${configPath}; pass --force to overwrite`
|
|
478
957
|
);
|
|
479
958
|
}
|
|
480
|
-
|
|
481
|
-
await writeJson(configPath, config);
|
|
959
|
+
await adapter.write(configPath, mcpDef.name, mcpDef.config);
|
|
482
960
|
spinner.succeed(
|
|
483
961
|
brand(`Installed ${item.name} for ${CODING_AGENTS[agent].name}`)
|
|
484
962
|
);
|
|
@@ -499,22 +977,45 @@ async function installMcp(item, agents, scope, method, force, cwd = process.cwd(
|
|
|
499
977
|
}
|
|
500
978
|
return results;
|
|
501
979
|
}
|
|
502
|
-
async function
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
980
|
+
async function resolveServerName(slug) {
|
|
981
|
+
try {
|
|
982
|
+
const item = await getItem(slug, "mcp");
|
|
983
|
+
if (!item) return slug;
|
|
984
|
+
return parseMcpDefinition(await getItemContent(item)).name;
|
|
985
|
+
} catch {
|
|
986
|
+
return slug;
|
|
508
987
|
}
|
|
509
|
-
delete config.mcpServers[slug];
|
|
510
|
-
await writeJson(configPath, config);
|
|
511
|
-
return true;
|
|
512
988
|
}
|
|
513
|
-
async function
|
|
514
|
-
|
|
515
|
-
if (!
|
|
516
|
-
const
|
|
517
|
-
|
|
989
|
+
async function uninstallMcp(slug, agent, scope, cwd = process.cwd()) {
|
|
990
|
+
assertValidSlug(slug);
|
|
991
|
+
if (!isTypeSupported("mcp", agent)) return false;
|
|
992
|
+
const adapter = adapterFor(agent);
|
|
993
|
+
const configPath = adapter.configPath(scope, cwd);
|
|
994
|
+
if (!await exists(configPath)) return false;
|
|
995
|
+
const name = await resolveServerName(slug);
|
|
996
|
+
return adapter.remove(configPath, name);
|
|
997
|
+
}
|
|
998
|
+
async function getInstalledMcpServers(agent, scope, cwd = process.cwd()) {
|
|
999
|
+
if (!isTypeSupported("mcp", agent)) return [];
|
|
1000
|
+
const adapter = adapterFor(agent);
|
|
1001
|
+
return adapter.list(adapter.configPath(scope, cwd));
|
|
1002
|
+
}
|
|
1003
|
+
async function planMcp(item, agents, scope, _method, cwd) {
|
|
1004
|
+
const mcpDef = parseMcpDefinition(await getItemContent(item));
|
|
1005
|
+
const changes = [];
|
|
1006
|
+
for (const agent of agents) {
|
|
1007
|
+
const adapter = adapterFor(agent);
|
|
1008
|
+
const path = adapter.configPath(scope, cwd);
|
|
1009
|
+
const fileExists = await exists(path);
|
|
1010
|
+
const replaces = fileExists && await adapter.has(path, mcpDef.name);
|
|
1011
|
+
changes.push({
|
|
1012
|
+
agent,
|
|
1013
|
+
kind: fileExists ? "modify" : "create",
|
|
1014
|
+
path,
|
|
1015
|
+
detail: `${adapter.describeEntry(mcpDef.name)}${replaces ? " (replaces existing entry)" : ""}`
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
return changes;
|
|
518
1019
|
}
|
|
519
1020
|
var mcpHandler = {
|
|
520
1021
|
type: "mcp",
|
|
@@ -526,12 +1027,15 @@ var mcpHandler = {
|
|
|
526
1027
|
},
|
|
527
1028
|
async listInstalled(agent, scope, cwd) {
|
|
528
1029
|
return getInstalledMcpServers(agent, scope, cwd);
|
|
529
|
-
}
|
|
1030
|
+
},
|
|
1031
|
+
plan: planMcp
|
|
530
1032
|
};
|
|
531
1033
|
|
|
532
1034
|
// src/handlers/settings.ts
|
|
533
1035
|
import chalk5 from "chalk";
|
|
534
1036
|
import ora4 from "ora";
|
|
1037
|
+
var SETTINGS_NOT_DISCOVERABLE = "settings items cannot be discovered (they are merged into settings.json)";
|
|
1038
|
+
var CLAUDE_ONLY_ERROR2 = "Settings are only supported for Claude Code";
|
|
535
1039
|
function parseSettings(content) {
|
|
536
1040
|
try {
|
|
537
1041
|
return JSON.parse(content);
|
|
@@ -545,7 +1049,7 @@ async function installSettingsForAgent(item, agent, scope, _method, _force, cwd)
|
|
|
545
1049
|
).start();
|
|
546
1050
|
try {
|
|
547
1051
|
if (agent !== "claude") {
|
|
548
|
-
throw new Error(
|
|
1052
|
+
throw new Error(CLAUDE_ONLY_ERROR2);
|
|
549
1053
|
}
|
|
550
1054
|
const content = await getItemContent(item);
|
|
551
1055
|
const newSettings = parseSettings(content);
|
|
@@ -601,6 +1105,7 @@ function deepUnmerge(target, source) {
|
|
|
601
1105
|
return { result, changed };
|
|
602
1106
|
}
|
|
603
1107
|
async function uninstallSettings(slug, agent, scope, cwd = process.cwd()) {
|
|
1108
|
+
assertValidSlug(slug, "settings slug");
|
|
604
1109
|
if (agent !== "claude") return false;
|
|
605
1110
|
const item = await getItem(slug, "settings");
|
|
606
1111
|
if (!item) return false;
|
|
@@ -620,12 +1125,24 @@ async function uninstallSettings(slug, agent, scope, cwd = process.cwd()) {
|
|
|
620
1125
|
}
|
|
621
1126
|
return changed;
|
|
622
1127
|
}
|
|
623
|
-
async function getInstalledSettings(
|
|
624
|
-
if (agent !== "claude") return [];
|
|
625
|
-
const settingsPath = getSettingsPath(scope, cwd);
|
|
626
|
-
if (!await exists(settingsPath)) return [];
|
|
1128
|
+
async function getInstalledSettings(_agent, _scope, _cwd = process.cwd()) {
|
|
627
1129
|
return [];
|
|
628
1130
|
}
|
|
1131
|
+
async function planSettings(item, agents, scope, _method, cwd) {
|
|
1132
|
+
const changes = [];
|
|
1133
|
+
for (const agent of agents) {
|
|
1134
|
+
if (agent !== "claude") throw new Error(CLAUDE_ONLY_ERROR2);
|
|
1135
|
+
const keys = Object.keys(parseSettings(await getItemContent(item)));
|
|
1136
|
+
const settingsPath = getSettingsPath(scope, cwd);
|
|
1137
|
+
changes.push({
|
|
1138
|
+
agent,
|
|
1139
|
+
kind: await exists(settingsPath) ? "modify" : "create",
|
|
1140
|
+
path: settingsPath,
|
|
1141
|
+
detail: `deep-merge keys: ${keys.join(", ") || "(none)"}`
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
return changes;
|
|
1145
|
+
}
|
|
629
1146
|
var settingsHandler = {
|
|
630
1147
|
type: "settings",
|
|
631
1148
|
async install(item, agents, scope, method, force, cwd) {
|
|
@@ -636,49 +1153,78 @@ var settingsHandler = {
|
|
|
636
1153
|
},
|
|
637
1154
|
async listInstalled(agent, scope, cwd) {
|
|
638
1155
|
return getInstalledSettings(agent, scope, cwd);
|
|
639
|
-
}
|
|
1156
|
+
},
|
|
1157
|
+
plan: planSettings
|
|
640
1158
|
};
|
|
641
1159
|
|
|
642
1160
|
// src/handlers/plugin.ts
|
|
643
|
-
import { homedir as
|
|
644
|
-
import { join as join3 } from "path";
|
|
645
|
-
import { mkdir as mkdir2, rm as rm2 } from "fs/promises";
|
|
1161
|
+
import { homedir as homedir3 } from "os";
|
|
1162
|
+
import { join as join3, relative as relative2 } from "path";
|
|
1163
|
+
import { mkdir as mkdir2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
|
|
646
1164
|
import { execFile } from "child_process";
|
|
647
1165
|
import { promisify } from "util";
|
|
648
1166
|
import chalk6 from "chalk";
|
|
649
1167
|
import ora5 from "ora";
|
|
650
|
-
var home =
|
|
651
|
-
var
|
|
652
|
-
var
|
|
653
|
-
var
|
|
654
|
-
var
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1168
|
+
var home = homedir3();
|
|
1169
|
+
var PLUGINS_DIR = join3(home, ".claude", "plugins");
|
|
1170
|
+
var PLUGINS_CACHE_DIR = join3(PLUGINS_DIR, "cache");
|
|
1171
|
+
var INSTALLED_PLUGINS_PATH = join3(PLUGINS_DIR, "installed_plugins.json");
|
|
1172
|
+
var KNOWN_MARKETPLACES_PATH = join3(PLUGINS_DIR, "known_marketplaces.json");
|
|
1173
|
+
var MARKETPLACES_DIR = join3(PLUGINS_DIR, "marketplaces");
|
|
1174
|
+
var PLUGIN_JSON_PATH = ".claude-plugin/plugin.json";
|
|
1175
|
+
var CLAUDE_ONLY_ERROR3 = "Plugins are only supported for Claude Code";
|
|
1176
|
+
var SLUG_LABEL2 = "plugin slug";
|
|
1177
|
+
var GITHUB_REPO_SLUG_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
658
1178
|
function getPluginId(name, marketplace) {
|
|
659
1179
|
return `${name}@${marketplace}`;
|
|
660
1180
|
}
|
|
1181
|
+
function splitPluginId(pluginId) {
|
|
1182
|
+
const at = pluginId.indexOf("@");
|
|
1183
|
+
if (at <= 0 || at === pluginId.length - 1) return null;
|
|
1184
|
+
return { name: pluginId.slice(0, at), marketplace: pluginId.slice(at + 1) };
|
|
1185
|
+
}
|
|
1186
|
+
function resolveMarketplaceName(item) {
|
|
1187
|
+
const marketplace = item.marketplace || item.author?.name || "seedr";
|
|
1188
|
+
assertSafePathSegment(marketplace, "plugin marketplace");
|
|
1189
|
+
return marketplace;
|
|
1190
|
+
}
|
|
1191
|
+
function resolvePluginIdentity(item, pluginJson) {
|
|
1192
|
+
const name = pluginJson.name || item.slug;
|
|
1193
|
+
const version = pluginJson.version || "1.0.0";
|
|
1194
|
+
assertSafePathSegment(name, "plugin name");
|
|
1195
|
+
assertSafePathSegment(version, "plugin version");
|
|
1196
|
+
return { name, version };
|
|
1197
|
+
}
|
|
661
1198
|
function extractGitHubRepo(url) {
|
|
662
|
-
const
|
|
663
|
-
|
|
1199
|
+
const parsed = parseGitHubRepo(url);
|
|
1200
|
+
if (!parsed) return null;
|
|
1201
|
+
const slug = `${parsed.owner}/${parsed.repo}`;
|
|
1202
|
+
if (!GITHUB_REPO_SLUG_PATTERN.test(slug) || slug.includes("..")) return null;
|
|
1203
|
+
return slug;
|
|
1204
|
+
}
|
|
1205
|
+
function marketplaceRepoUrl(item) {
|
|
1206
|
+
return item.marketplaceRef?.url ?? item.externalUrl;
|
|
664
1207
|
}
|
|
665
1208
|
async function ensureMarketplaceRegistered(marketplace, item) {
|
|
666
1209
|
const known = await readJson(
|
|
667
1210
|
KNOWN_MARKETPLACES_PATH
|
|
668
1211
|
);
|
|
669
1212
|
if (known[marketplace]) return;
|
|
670
|
-
const
|
|
1213
|
+
const sourceUrl = marketplaceRepoUrl(item);
|
|
1214
|
+
const repo = sourceUrl ? extractGitHubRepo(sourceUrl) : null;
|
|
671
1215
|
if (!repo) return;
|
|
672
|
-
const installLocation =
|
|
673
|
-
const execFileAsync = promisify(execFile);
|
|
1216
|
+
const installLocation = await resolveContained(MARKETPLACES_DIR, marketplace);
|
|
674
1217
|
await mkdir2(MARKETPLACES_DIR, { recursive: true });
|
|
675
|
-
await
|
|
676
|
-
|
|
677
|
-
"
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1218
|
+
if (!await exists(installLocation)) {
|
|
1219
|
+
const execFileAsync = promisify(execFile);
|
|
1220
|
+
await execFileAsync("git", [
|
|
1221
|
+
"clone",
|
|
1222
|
+
"--depth",
|
|
1223
|
+
"1",
|
|
1224
|
+
`https://github.com/${repo}.git`,
|
|
1225
|
+
installLocation
|
|
1226
|
+
]);
|
|
1227
|
+
}
|
|
682
1228
|
known[marketplace] = {
|
|
683
1229
|
source: { source: "github", repo },
|
|
684
1230
|
installLocation,
|
|
@@ -686,66 +1232,122 @@ async function ensureMarketplaceRegistered(marketplace, item) {
|
|
|
686
1232
|
};
|
|
687
1233
|
await writeJson(KNOWN_MARKETPLACES_PATH, known);
|
|
688
1234
|
}
|
|
689
|
-
async function
|
|
1235
|
+
async function createStagingDir(slug) {
|
|
1236
|
+
const tmpRoot = await resolveContained(PLUGINS_CACHE_DIR, ".tmp");
|
|
1237
|
+
await mkdir2(tmpRoot, { recursive: true });
|
|
1238
|
+
const staging = await mkdtemp2(join3(tmpRoot, `${slug}-`));
|
|
1239
|
+
return resolveContained(PLUGINS_CACHE_DIR, relative2(PLUGINS_CACHE_DIR, staging));
|
|
1240
|
+
}
|
|
1241
|
+
async function stageContent(item, contentPath) {
|
|
1242
|
+
const sourcePath = getItemSourcePath(item);
|
|
1243
|
+
if (sourcePath) {
|
|
1244
|
+
await copyDirectory(sourcePath, contentPath);
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
return fetchItemToDestination(item, contentPath);
|
|
1248
|
+
}
|
|
1249
|
+
function resolveGitCommitSha(item, fetched) {
|
|
1250
|
+
const sha = fetched?.sourceRevision ?? getEffectiveSourceRevision(item);
|
|
1251
|
+
if (!sha) {
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
`Cannot record a source revision for "${item.slug}": the registry entry has neither sourceRevision nor pluginSource.sha`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
return sha;
|
|
1257
|
+
}
|
|
1258
|
+
function isScopeEntry(entry, scope, cwd) {
|
|
1259
|
+
return entry.scope === scope && (scope === "user" || entry.projectPath === cwd);
|
|
1260
|
+
}
|
|
1261
|
+
async function writeInstalledEntry(params) {
|
|
1262
|
+
const { cachePath, pluginId, version, gitCommitSha, scope, cwd } = params;
|
|
1263
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1264
|
+
const registry = await readJson(INSTALLED_PLUGINS_PATH);
|
|
1265
|
+
registry.version = registry.version || 2;
|
|
1266
|
+
registry.plugins = registry.plugins || {};
|
|
1267
|
+
const installInfo = {
|
|
1268
|
+
scope,
|
|
1269
|
+
...scope !== "user" ? { projectPath: cwd } : {},
|
|
1270
|
+
installPath: cachePath,
|
|
1271
|
+
version,
|
|
1272
|
+
installedAt: now,
|
|
1273
|
+
lastUpdated: now,
|
|
1274
|
+
gitCommitSha
|
|
1275
|
+
};
|
|
1276
|
+
const existingEntries = registry.plugins[pluginId] || [];
|
|
1277
|
+
registry.plugins[pluginId] = [
|
|
1278
|
+
...existingEntries.filter((entry) => !isScopeEntry(entry, scope, cwd)),
|
|
1279
|
+
installInfo
|
|
1280
|
+
];
|
|
1281
|
+
await writeJson(INSTALLED_PLUGINS_PATH, registry);
|
|
1282
|
+
}
|
|
1283
|
+
async function enableInSettings(settingsPath, pluginId) {
|
|
1284
|
+
const settings = await readJson(settingsPath);
|
|
1285
|
+
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
1286
|
+
settings.enabledPlugins[pluginId] = true;
|
|
1287
|
+
await writeJson(settingsPath, settings);
|
|
1288
|
+
}
|
|
1289
|
+
async function rollback(undo, error2) {
|
|
1290
|
+
const failures = [];
|
|
1291
|
+
for (const step2 of undo.reverse()) {
|
|
1292
|
+
try {
|
|
1293
|
+
await step2();
|
|
1294
|
+
} catch (undoError) {
|
|
1295
|
+
failures.push(undoError instanceof Error ? undoError.message : String(undoError));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (failures.length > 0) {
|
|
1299
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1300
|
+
throw new Error(`${message} (rollback incomplete: ${failures.join("; ")})`, { cause: error2 });
|
|
1301
|
+
}
|
|
1302
|
+
throw error2;
|
|
1303
|
+
}
|
|
1304
|
+
async function commitInstall(params) {
|
|
1305
|
+
const { contentPath, cachePath, pluginId, scope, cwd } = params;
|
|
1306
|
+
const undo = [];
|
|
1307
|
+
try {
|
|
1308
|
+
await removePathEntry(cachePath);
|
|
1309
|
+
await moveDirectory(contentPath, cachePath);
|
|
1310
|
+
undo.push(() => removePathEntry(cachePath).then(() => void 0));
|
|
1311
|
+
const registrySnapshot = await snapshotFile(INSTALLED_PLUGINS_PATH);
|
|
1312
|
+
await writeInstalledEntry(params);
|
|
1313
|
+
undo.push(() => restoreFile(INSTALLED_PLUGINS_PATH, registrySnapshot));
|
|
1314
|
+
const settingsPath = getSettingsPath(scope, cwd);
|
|
1315
|
+
const settingsSnapshot = await snapshotFile(settingsPath);
|
|
1316
|
+
await enableInSettings(settingsPath, pluginId);
|
|
1317
|
+
undo.push(() => restoreFile(settingsPath, settingsSnapshot));
|
|
1318
|
+
} catch (error2) {
|
|
1319
|
+
await rollback(undo, error2);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async function installPluginForAgent(item, agent, scope, _method, force, cwd) {
|
|
690
1323
|
const spinner = ora5(
|
|
691
1324
|
`Installing ${item.name} for ${CODING_AGENTS[agent].name}...`
|
|
692
1325
|
).start();
|
|
693
1326
|
try {
|
|
694
1327
|
if (agent !== "claude") {
|
|
695
|
-
throw new Error(
|
|
696
|
-
}
|
|
697
|
-
const tmpPath = join3(PLUGINS_CACHE_DIR, ".tmp", item.slug);
|
|
698
|
-
const sourcePath = getItemSourcePath(item);
|
|
699
|
-
if (sourcePath) {
|
|
700
|
-
await installDirectory(sourcePath, tmpPath, method);
|
|
701
|
-
} else {
|
|
702
|
-
await fetchItemToDestination(item, tmpPath);
|
|
1328
|
+
throw new Error(CLAUDE_ONLY_ERROR3);
|
|
703
1329
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
);
|
|
707
|
-
const marketplace = item.marketplace || item.author?.name || "seedr";
|
|
708
|
-
const pluginName = pluginJson.name || item.slug;
|
|
709
|
-
const version = pluginJson.version || "1.0.0";
|
|
710
|
-
const pluginId = getPluginId(pluginName, marketplace);
|
|
711
|
-
await ensureMarketplaceRegistered(marketplace, item);
|
|
712
|
-
const cachePath = getPluginCachePath(marketplace, pluginName, version);
|
|
1330
|
+
assertValidSlug(item.slug, SLUG_LABEL2);
|
|
1331
|
+
const marketplace = resolveMarketplaceName(item);
|
|
1332
|
+
const staging = await createStagingDir(item.slug);
|
|
713
1333
|
try {
|
|
1334
|
+
const contentPath = join3(staging, "content");
|
|
1335
|
+
const fetched = await stageContent(item, contentPath);
|
|
1336
|
+
const pluginJson = await readJson(join3(contentPath, ...PLUGIN_JSON_PATH.split("/")));
|
|
1337
|
+
const { name, version } = resolvePluginIdentity(item, pluginJson);
|
|
1338
|
+
const gitCommitSha = resolveGitCommitSha(item, fetched);
|
|
1339
|
+
const pluginId = getPluginId(name, marketplace);
|
|
1340
|
+
await ensureMarketplaceRegistered(marketplace, item);
|
|
1341
|
+
const cachePath = await resolveContained(PLUGINS_CACHE_DIR, marketplace, name, version);
|
|
714
1342
|
await assertOverwritable(cachePath, force);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1343
|
+
await commitInstall({ contentPath, cachePath, pluginId, version, gitCommitSha, scope, cwd });
|
|
1344
|
+
spinner.succeed(
|
|
1345
|
+
brand(`Installed ${item.name} for ${CODING_AGENTS[agent].name}`)
|
|
1346
|
+
);
|
|
1347
|
+
return { agent, success: true, path: cachePath };
|
|
1348
|
+
} finally {
|
|
1349
|
+
await rm2(staging, { recursive: true, force: true });
|
|
718
1350
|
}
|
|
719
|
-
await installDirectory(tmpPath, cachePath, "copy");
|
|
720
|
-
await rm2(tmpPath, { recursive: true, force: true });
|
|
721
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
722
|
-
const registry = await readJson(INSTALLED_PLUGINS_PATH);
|
|
723
|
-
registry.version = registry.version || 2;
|
|
724
|
-
registry.plugins = registry.plugins || {};
|
|
725
|
-
const installInfo = {
|
|
726
|
-
scope,
|
|
727
|
-
...scope !== "user" ? { projectPath: cwd } : {},
|
|
728
|
-
installPath: cachePath,
|
|
729
|
-
version,
|
|
730
|
-
installedAt: now,
|
|
731
|
-
lastUpdated: now,
|
|
732
|
-
gitCommitSha: ""
|
|
733
|
-
};
|
|
734
|
-
const existingEntries = registry.plugins[pluginId] || [];
|
|
735
|
-
const filteredEntries = existingEntries.filter(
|
|
736
|
-
(e) => !(e.scope === scope && (scope === "user" || e.projectPath === cwd))
|
|
737
|
-
);
|
|
738
|
-
registry.plugins[pluginId] = [...filteredEntries, installInfo];
|
|
739
|
-
await writeJson(INSTALLED_PLUGINS_PATH, registry);
|
|
740
|
-
const settingsPath = getSettingsPath(scope, cwd);
|
|
741
|
-
const settings = await readJson(settingsPath);
|
|
742
|
-
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
743
|
-
settings.enabledPlugins[pluginId] = true;
|
|
744
|
-
await writeJson(settingsPath, settings);
|
|
745
|
-
spinner.succeed(
|
|
746
|
-
brand(`Installed ${item.name} for ${CODING_AGENTS[agent].name}`)
|
|
747
|
-
);
|
|
748
|
-
return { agent, success: true, path: cachePath };
|
|
749
1351
|
} catch (error2) {
|
|
750
1352
|
const errorMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
751
1353
|
spinner.fail(
|
|
@@ -762,30 +1364,60 @@ async function installPlugin(item, agents, scope, method, force, cwd = process.c
|
|
|
762
1364
|
}
|
|
763
1365
|
return results;
|
|
764
1366
|
}
|
|
1367
|
+
async function findInstalledPluginId(registry, slug) {
|
|
1368
|
+
let registryMarketplace = null;
|
|
1369
|
+
try {
|
|
1370
|
+
const item = await getItem(slug, "plugin");
|
|
1371
|
+
if (item) registryMarketplace = resolveMarketplaceName(item);
|
|
1372
|
+
} catch {
|
|
1373
|
+
registryMarketplace = null;
|
|
1374
|
+
}
|
|
1375
|
+
if (registryMarketplace !== null) {
|
|
1376
|
+
const exact = getPluginId(slug, registryMarketplace);
|
|
1377
|
+
return exact in registry.plugins ? exact : null;
|
|
1378
|
+
}
|
|
1379
|
+
const candidates = Object.keys(registry.plugins).filter((id) => splitPluginId(id)?.name === slug);
|
|
1380
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
1381
|
+
}
|
|
1382
|
+
async function removeOrphanedCache(installPath, registry) {
|
|
1383
|
+
const stillReferenced = Object.values(registry.plugins).some(
|
|
1384
|
+
(entries) => entries.some((entry) => entry.installPath === installPath)
|
|
1385
|
+
);
|
|
1386
|
+
if (stillReferenced) return;
|
|
1387
|
+
let contained;
|
|
1388
|
+
try {
|
|
1389
|
+
contained = await resolveContained(PLUGINS_CACHE_DIR, installPath);
|
|
1390
|
+
} catch {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
await removePathEntry(contained);
|
|
1394
|
+
}
|
|
765
1395
|
async function uninstallPlugin(slug, agent, scope, cwd = process.cwd()) {
|
|
1396
|
+
assertValidSlug(slug, SLUG_LABEL2);
|
|
766
1397
|
if (agent !== "claude") return false;
|
|
767
1398
|
const registry = await readJson(INSTALLED_PLUGINS_PATH);
|
|
768
1399
|
if (!registry.plugins) return false;
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (filteredEntries.length === 0) {
|
|
778
|
-
delete registry.plugins[matchingKey];
|
|
1400
|
+
const pluginId = await findInstalledPluginId(registry, slug);
|
|
1401
|
+
if (!pluginId) return false;
|
|
1402
|
+
const entries = registry.plugins[pluginId] || [];
|
|
1403
|
+
const removedEntries = entries.filter((entry) => isScopeEntry(entry, scope, cwd));
|
|
1404
|
+
if (removedEntries.length === 0) return false;
|
|
1405
|
+
const remaining = entries.filter((entry) => !isScopeEntry(entry, scope, cwd));
|
|
1406
|
+
if (remaining.length === 0) {
|
|
1407
|
+
delete registry.plugins[pluginId];
|
|
779
1408
|
} else {
|
|
780
|
-
registry.plugins[
|
|
1409
|
+
registry.plugins[pluginId] = remaining;
|
|
781
1410
|
}
|
|
782
1411
|
await writeJson(INSTALLED_PLUGINS_PATH, registry);
|
|
783
1412
|
const settingsPath = getSettingsPath(scope, cwd);
|
|
784
1413
|
const settings = await readJson(settingsPath);
|
|
785
|
-
if (settings.enabledPlugins &&
|
|
786
|
-
delete settings.enabledPlugins[
|
|
1414
|
+
if (settings.enabledPlugins && pluginId in settings.enabledPlugins) {
|
|
1415
|
+
delete settings.enabledPlugins[pluginId];
|
|
787
1416
|
await writeJson(settingsPath, settings);
|
|
788
1417
|
}
|
|
1418
|
+
for (const installPath of new Set(removedEntries.map((entry) => entry.installPath))) {
|
|
1419
|
+
await removeOrphanedCache(installPath, registry);
|
|
1420
|
+
}
|
|
789
1421
|
return true;
|
|
790
1422
|
}
|
|
791
1423
|
async function getInstalledPlugins(agent, scope, cwd = process.cwd()) {
|
|
@@ -794,16 +1426,57 @@ async function getInstalledPlugins(agent, scope, cwd = process.cwd()) {
|
|
|
794
1426
|
if (!registry.plugins) return [];
|
|
795
1427
|
const installed = [];
|
|
796
1428
|
for (const [pluginId, entries] of Object.entries(registry.plugins)) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const slug = pluginId.split("@")[0] || pluginId;
|
|
800
|
-
installed.push(slug);
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
1429
|
+
if (entries.some((entry) => isScopeEntry(entry, scope, cwd))) {
|
|
1430
|
+
installed.push(splitPluginId(pluginId)?.name ?? pluginId);
|
|
803
1431
|
}
|
|
804
1432
|
}
|
|
805
1433
|
return installed;
|
|
806
1434
|
}
|
|
1435
|
+
async function readPluginJsonForPlan(item) {
|
|
1436
|
+
const sourcePath = getItemSourcePath(item);
|
|
1437
|
+
if (sourcePath) {
|
|
1438
|
+
return readJson(join3(sourcePath, ...PLUGIN_JSON_PATH.split("/")));
|
|
1439
|
+
}
|
|
1440
|
+
return JSON.parse(await fetchItemFile(item, PLUGIN_JSON_PATH));
|
|
1441
|
+
}
|
|
1442
|
+
async function kindFor(path) {
|
|
1443
|
+
return await exists(path) ? "modify" : "create";
|
|
1444
|
+
}
|
|
1445
|
+
async function planPlugin(item, agents, scope, _method, cwd) {
|
|
1446
|
+
const changes = [];
|
|
1447
|
+
for (const agent of agents) {
|
|
1448
|
+
if (agent !== "claude") throw new Error(CLAUDE_ONLY_ERROR3);
|
|
1449
|
+
assertValidSlug(item.slug, SLUG_LABEL2);
|
|
1450
|
+
const marketplace = resolveMarketplaceName(item);
|
|
1451
|
+
const { name, version } = resolvePluginIdentity(item, await readPluginJsonForPlan(item));
|
|
1452
|
+
const pluginId = getPluginId(name, marketplace);
|
|
1453
|
+
const cachePath = await resolveContained(PLUGINS_CACHE_DIR, marketplace, name, version);
|
|
1454
|
+
const settingsPath = getSettingsPath(scope, cwd);
|
|
1455
|
+
changes.push(
|
|
1456
|
+
{ agent, kind: await kindFor(cachePath), path: cachePath, detail: "plugin files (digest-verified download)" },
|
|
1457
|
+
{
|
|
1458
|
+
agent,
|
|
1459
|
+
kind: await kindFor(INSTALLED_PLUGINS_PATH),
|
|
1460
|
+
path: INSTALLED_PLUGINS_PATH,
|
|
1461
|
+
detail: `plugins["${pluginId}"] entry for ${scope} scope`
|
|
1462
|
+
}
|
|
1463
|
+
);
|
|
1464
|
+
const known = await readJson(KNOWN_MARKETPLACES_PATH);
|
|
1465
|
+
if (!known[marketplace]) {
|
|
1466
|
+
const sourceUrl = marketplaceRepoUrl(item);
|
|
1467
|
+
const repo = sourceUrl ? extractGitHubRepo(sourceUrl) : null;
|
|
1468
|
+
if (repo) {
|
|
1469
|
+
const cloneDir = await resolveContained(MARKETPLACES_DIR, marketplace);
|
|
1470
|
+
changes.push({ agent, kind: await kindFor(KNOWN_MARKETPLACES_PATH), path: KNOWN_MARKETPLACES_PATH, detail: `marketplace "${marketplace}"` });
|
|
1471
|
+
if (!await exists(cloneDir)) {
|
|
1472
|
+
changes.push({ agent, kind: "create", path: cloneDir, detail: `git clone https://github.com/${repo}.git` });
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
changes.push({ agent, kind: await kindFor(settingsPath), path: settingsPath, detail: `enabledPlugins["${pluginId}"] = true` });
|
|
1477
|
+
}
|
|
1478
|
+
return changes;
|
|
1479
|
+
}
|
|
807
1480
|
var pluginHandler = {
|
|
808
1481
|
type: "plugin",
|
|
809
1482
|
async install(item, agents, scope, method, force, cwd) {
|
|
@@ -814,7 +1487,8 @@ var pluginHandler = {
|
|
|
814
1487
|
},
|
|
815
1488
|
async listInstalled(agent, scope, cwd) {
|
|
816
1489
|
return getInstalledPlugins(agent, scope, cwd);
|
|
817
|
-
}
|
|
1490
|
+
},
|
|
1491
|
+
plan: planPlugin
|
|
818
1492
|
};
|
|
819
1493
|
|
|
820
1494
|
// src/handlers/index.ts
|
|
@@ -826,91 +1500,192 @@ registerHandler(settingsHandler);
|
|
|
826
1500
|
registerHandler(pluginHandler);
|
|
827
1501
|
|
|
828
1502
|
// src/commands/add.ts
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1503
|
+
var CANCELLED_MESSAGE = "Operation cancelled";
|
|
1504
|
+
var CANCELLED = /* @__PURE__ */ Symbol("cancelled");
|
|
1505
|
+
function cancelPrompt() {
|
|
1506
|
+
p.cancel(CANCELLED_MESSAGE);
|
|
1507
|
+
return CANCELLED;
|
|
1508
|
+
}
|
|
1509
|
+
function compatibleAgentsFor(item) {
|
|
1510
|
+
return filterCompatibleAgents(item.type, item.compatibility);
|
|
1511
|
+
}
|
|
1512
|
+
function explainIncompatibility(item, agent) {
|
|
1513
|
+
if (!isTypeSupported(item.type, agent)) {
|
|
1514
|
+
return describeIncompatibility(item.type, agent);
|
|
1515
|
+
}
|
|
1516
|
+
return `the registry lists "${item.slug}" for ${item.compatibility.join(", ")} only`;
|
|
1517
|
+
}
|
|
1518
|
+
function resolveRequestedAgents(agentsArg, item) {
|
|
1519
|
+
const compatible = compatibleAgentsFor(item);
|
|
1520
|
+
const compatibleList = compatible.length > 0 ? compatible.join(", ") : "(none)";
|
|
1521
|
+
if (agentsArg === void 0 || agentsArg.trim() === "") {
|
|
1522
|
+
return { ok: true, agents: [], explicit: false };
|
|
1523
|
+
}
|
|
1524
|
+
const deprecationWarning = agentsArg.split(",").map((raw) => raw.trim().toLowerCase()).some((id) => isLegacyAgent(id)) ? "'gemini' is now 'antigravity' (Google Antigravity, installs to .agents/)" : void 0;
|
|
1525
|
+
if (agentsArg.trim() === "all") {
|
|
1526
|
+
if (compatible.length === 0) {
|
|
1527
|
+
return { ok: false, error: `No agent supports ${item.type} "${item.slug}"` };
|
|
847
1528
|
}
|
|
848
|
-
|
|
849
|
-
|
|
1529
|
+
return { ok: true, agents: compatible, explicit: true, deprecationWarning };
|
|
1530
|
+
}
|
|
1531
|
+
const { agents, unknown } = parseAgentsArgStrict(agentsArg);
|
|
1532
|
+
if (unknown.length > 0) {
|
|
1533
|
+
return {
|
|
1534
|
+
ok: false,
|
|
1535
|
+
error: `Unknown agent(s): ${unknown.join(", ")}. Valid agents: ${ALL_AGENTS.join(", ")} or "all"`
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
if (agents.length === 0) {
|
|
1539
|
+
return { ok: false, error: "No agents given" };
|
|
1540
|
+
}
|
|
1541
|
+
const incompatible = agents.filter((agent) => !compatible.includes(agent));
|
|
1542
|
+
if (incompatible.length > 0) {
|
|
1543
|
+
const reasons = incompatible.map((agent) => `${agent}: ${explainIncompatibility(item, agent)}`);
|
|
1544
|
+
return {
|
|
1545
|
+
ok: false,
|
|
1546
|
+
error: `Cannot install ${item.type} "${item.slug}" for ${incompatible.join(", ")}. Compatible agents: ${compatibleList}. ${reasons.join("; ")}`
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
return { ok: true, agents, explicit: true, deprecationWarning };
|
|
1550
|
+
}
|
|
1551
|
+
function decideForce(options) {
|
|
1552
|
+
return Boolean(options.force) || !options.yes;
|
|
1553
|
+
}
|
|
1554
|
+
async function planInstall(handler, item, agents, scope, method, cwd) {
|
|
1555
|
+
if (handler.plan) {
|
|
1556
|
+
return handler.plan(item, agents, scope, method, cwd);
|
|
1557
|
+
}
|
|
1558
|
+
return agents.map((agent) => ({
|
|
1559
|
+
agent,
|
|
1560
|
+
kind: "create",
|
|
1561
|
+
path: getAgentsPath(item.type, item.slug, scope, cwd),
|
|
1562
|
+
detail: "(handler provides no detailed plan)"
|
|
1563
|
+
}));
|
|
1564
|
+
}
|
|
1565
|
+
var KIND_LABEL = {
|
|
1566
|
+
create: "create",
|
|
1567
|
+
modify: "modify",
|
|
1568
|
+
delete: "delete"
|
|
1569
|
+
};
|
|
1570
|
+
function formatPlan(changes) {
|
|
1571
|
+
if (changes.length === 0) return [" (no filesystem changes)"];
|
|
1572
|
+
const lines = [];
|
|
1573
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1574
|
+
for (const change of changes) {
|
|
1575
|
+
const label = change.agent === "shared" ? "shared" : CODING_AGENTS[change.agent].name;
|
|
1576
|
+
groups.set(label, [...groups.get(label) ?? [], change]);
|
|
1577
|
+
}
|
|
1578
|
+
for (const [label, group] of groups) {
|
|
1579
|
+
lines.push(` ${label}:`);
|
|
1580
|
+
for (const change of group) {
|
|
1581
|
+
const detail = change.detail ? ` ${chalk7.gray(`\u2014 ${change.detail}`)}` : "";
|
|
1582
|
+
lines.push(` ${chalk7.gray(`[${KIND_LABEL[change.kind]}]`)} ${change.path}${detail}`);
|
|
850
1583
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1584
|
+
}
|
|
1585
|
+
return lines;
|
|
1586
|
+
}
|
|
1587
|
+
function summarizeResults(results) {
|
|
1588
|
+
const successful = results.filter((r) => r.success);
|
|
1589
|
+
const failed = results.filter((r) => !r.success);
|
|
1590
|
+
return { successful, failed, exitCode: failed.length > 0 ? 1 : 0 };
|
|
1591
|
+
}
|
|
1592
|
+
async function resolveItemByName(itemName, type) {
|
|
1593
|
+
if (type) {
|
|
1594
|
+
const typed = await getItem(itemName, type);
|
|
1595
|
+
if (typed) return typed;
|
|
1596
|
+
} else {
|
|
1597
|
+
const matches = await getItemsBySlug(itemName);
|
|
1598
|
+
if (matches.length === 1) return matches[0];
|
|
1599
|
+
if (matches.length > 1) {
|
|
1600
|
+
const types = matches.map((match) => match.type);
|
|
1601
|
+
if (!isInteractive()) {
|
|
1602
|
+
error(`"${itemName}" exists as ${types.join(" and ")}. Pass --type <${types.join("|")}> to choose.`);
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
warn(`"${itemName}" exists as ${types.join(" and ")} \u2014 pass --type to skip this question.`);
|
|
1606
|
+
const chosen = await selectSkill(matches);
|
|
1607
|
+
if (p.isCancel(chosen)) return cancelPrompt();
|
|
1608
|
+
return chosen;
|
|
856
1609
|
}
|
|
857
|
-
return selected2;
|
|
858
1610
|
}
|
|
1611
|
+
const results = await searchItems(itemName);
|
|
1612
|
+
const filtered = type ? results.filter((r) => r.type === type) : results;
|
|
1613
|
+
if (filtered.length === 0) {
|
|
1614
|
+
reportNotFound(itemName, type, results);
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
if (filtered.length === 1) {
|
|
1618
|
+
return filtered[0];
|
|
1619
|
+
}
|
|
1620
|
+
warn(`Multiple matches for "${itemName}"`);
|
|
1621
|
+
const selected = await selectSkill(filtered);
|
|
1622
|
+
if (p.isCancel(selected)) return cancelPrompt();
|
|
1623
|
+
return selected;
|
|
1624
|
+
}
|
|
1625
|
+
function reportNotFound(itemName, type, results) {
|
|
1626
|
+
if (!type || results.length === 0) {
|
|
1627
|
+
error(`"${itemName}" not found`);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const match = results.find((r) => r.slug === itemName);
|
|
1631
|
+
if (match) {
|
|
1632
|
+
error(`"${itemName}" is a ${match.type}, not a ${type}. Run: seedr add ${itemName} --type ${match.type}`);
|
|
1633
|
+
} else {
|
|
1634
|
+
error(`No ${type} matching "${itemName}". Found: ${results.map((r) => `${r.slug} (${r.type})`).join(", ")}`);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async function resolveItem(itemName, type) {
|
|
1638
|
+
if (itemName) return resolveItemByName(itemName, type);
|
|
859
1639
|
const items = await listItems(type || "skill");
|
|
860
1640
|
if (items.length === 0) {
|
|
861
1641
|
error(`No ${type || "skill"}s available in registry`);
|
|
862
1642
|
return null;
|
|
863
1643
|
}
|
|
864
1644
|
const selected = await selectSkill(items);
|
|
865
|
-
if (p.isCancel(selected))
|
|
866
|
-
|
|
1645
|
+
if (p.isCancel(selected)) return cancelPrompt();
|
|
1646
|
+
return selected;
|
|
1647
|
+
}
|
|
1648
|
+
async function chooseAgents(options, item) {
|
|
1649
|
+
const resolution = resolveRequestedAgents(options.agents, item);
|
|
1650
|
+
if (!resolution.ok) {
|
|
1651
|
+
error(resolution.error);
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
if (resolution.deprecationWarning) warn(resolution.deprecationWarning);
|
|
1655
|
+
if (resolution.agents.length > 0) return resolution.agents;
|
|
1656
|
+
const compatible = compatibleAgentsFor(item);
|
|
1657
|
+
if (compatible.length === 0) {
|
|
1658
|
+
error(`No agent supports ${item.type} "${item.slug}"`);
|
|
867
1659
|
return null;
|
|
868
1660
|
}
|
|
1661
|
+
if (compatible.length === 1) return compatible;
|
|
1662
|
+
const selected = await selectAgents(compatible);
|
|
1663
|
+
if (p.isCancel(selected)) return cancelPrompt();
|
|
869
1664
|
return selected;
|
|
870
1665
|
}
|
|
871
|
-
function
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
if (incompatible.length > 0) {
|
|
878
|
-
warn(`${incompatible.join(", ")} not compatible with this ${item.type}`);
|
|
879
|
-
return agents.filter((a) => typeCompatible.includes(a));
|
|
880
|
-
}
|
|
881
|
-
return agents;
|
|
1666
|
+
async function chooseScope(options, item) {
|
|
1667
|
+
if (options.scope) return options.scope;
|
|
1668
|
+
const supportsLocal = ["plugin", "settings", "hook"].includes(item.type);
|
|
1669
|
+
const selected = await selectScope(supportsLocal);
|
|
1670
|
+
if (p.isCancel(selected)) return cancelPrompt();
|
|
1671
|
+
return selected;
|
|
882
1672
|
}
|
|
883
|
-
function
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1673
|
+
async function chooseMethod(options, item, agents, scope, cwd) {
|
|
1674
|
+
if (options.method) return options.method;
|
|
1675
|
+
if (agents.length === 1) return "copy";
|
|
1676
|
+
const symlinkPath = getAgentsPath(item.type, item.slug, scope, cwd);
|
|
1677
|
+
const selected = await selectMethod(symlinkPath);
|
|
1678
|
+
if (p.isCancel(selected)) return cancelPrompt();
|
|
1679
|
+
return selected;
|
|
1680
|
+
}
|
|
1681
|
+
function printPlan(title, changes) {
|
|
889
1682
|
console.log();
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
console.log(brand(" Central storage:"));
|
|
893
|
-
console.log(` ${chalk7.gray("\u2192")} ${chalk7.gray(centralPath)}`);
|
|
894
|
-
console.log();
|
|
895
|
-
console.log(brand(" Symlinks from agent folders:"));
|
|
896
|
-
} else {
|
|
897
|
-
console.log(brand(" Target locations:"));
|
|
898
|
-
}
|
|
899
|
-
for (const agent of agents) {
|
|
900
|
-
const config = CODING_AGENTS[agent];
|
|
901
|
-
const contentPath = getContentPath(agent, item.type, scope, cwd);
|
|
902
|
-
if (!contentPath) {
|
|
903
|
-
console.log(` ${chalk7.gray("\u2192")} ${chalk7.white(config.name)}: ${chalk7.red("not supported")}`);
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
const targetPath = `${contentPath}/${item.slug}`;
|
|
907
|
-
console.log(` ${chalk7.gray("\u2192")} ${chalk7.white(config.name)}: ${chalk7.gray(targetPath)}`);
|
|
908
|
-
}
|
|
1683
|
+
console.log(brand(` ${title}`));
|
|
1684
|
+
for (const line of formatPlan(changes)) console.log(line);
|
|
909
1685
|
console.log();
|
|
910
1686
|
}
|
|
911
1687
|
function printInstallSummary(results) {
|
|
912
|
-
const successful = results
|
|
913
|
-
const failed = results.filter((r) => !r.success);
|
|
1688
|
+
const { successful, failed, exitCode } = summarizeResults(results);
|
|
914
1689
|
if (successful.length > 0) {
|
|
915
1690
|
success(`Installed for ${successful.length} agent(s)`);
|
|
916
1691
|
for (const r of successful) {
|
|
@@ -922,98 +1697,65 @@ function printInstallSummary(results) {
|
|
|
922
1697
|
for (const r of failed) {
|
|
923
1698
|
console.log(chalk7.red(` \xD7 ${r.agent}: ${r.error}`));
|
|
924
1699
|
}
|
|
925
|
-
process.exit(1);
|
|
926
1700
|
}
|
|
1701
|
+
return exitCode;
|
|
1702
|
+
}
|
|
1703
|
+
async function runAdd(name, options, cwd = process.cwd()) {
|
|
1704
|
+
const optionError = validateType(options.type) || validateScope(options.scope) || validateMethod(options.method);
|
|
1705
|
+
if (optionError) {
|
|
1706
|
+
error(optionError);
|
|
1707
|
+
return 1;
|
|
1708
|
+
}
|
|
1709
|
+
const item = await resolveItem(name, options.type);
|
|
1710
|
+
if (item === CANCELLED) return 0;
|
|
1711
|
+
if (!item) return 1;
|
|
1712
|
+
step(`Selected: ${brand(item.name)} ${chalk7.gray(`(${item.type})`)} ${chalk7.gray(`- ${item.description}`)}`);
|
|
1713
|
+
const handler = getHandler(item.type);
|
|
1714
|
+
if (!handler) {
|
|
1715
|
+
error(`No handler found for type "${item.type}"`);
|
|
1716
|
+
return 1;
|
|
1717
|
+
}
|
|
1718
|
+
const agents = await chooseAgents(options, item);
|
|
1719
|
+
if (agents === CANCELLED) return 0;
|
|
1720
|
+
if (!agents) return 1;
|
|
1721
|
+
step(`Agents: ${brand(agents.join(", "))}`);
|
|
1722
|
+
const scope = await chooseScope(options, item);
|
|
1723
|
+
if (scope === CANCELLED) return 0;
|
|
1724
|
+
step(`Scope: ${brand(scope)}`);
|
|
1725
|
+
const method = await chooseMethod(options, item, agents, scope, cwd);
|
|
1726
|
+
if (method === CANCELLED) return 0;
|
|
1727
|
+
step(`Method: ${brand(method)}`);
|
|
1728
|
+
if (options.dryRun) {
|
|
1729
|
+
info("Dry run - no files will be written");
|
|
1730
|
+
printPlan("Would write:", await planInstall(handler, item, agents, scope, method, cwd));
|
|
1731
|
+
outro("Dry run complete");
|
|
1732
|
+
return 0;
|
|
1733
|
+
}
|
|
1734
|
+
if (!options.yes) {
|
|
1735
|
+
printPlan("Will write:", await planInstall(handler, item, agents, scope, method, cwd));
|
|
1736
|
+
const confirmed = await confirm("Proceed with installation?");
|
|
1737
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
1738
|
+
cancelPrompt();
|
|
1739
|
+
return 0;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
console.log();
|
|
1743
|
+
const results = await handler.install(item, agents, scope, method, decideForce(options), cwd);
|
|
1744
|
+
void trackInstalls(item.slug, item.type, results, scope);
|
|
1745
|
+
const exitCode = printInstallSummary(results);
|
|
1746
|
+
if (exitCode === 0) outro("Installation complete");
|
|
1747
|
+
return exitCode;
|
|
927
1748
|
}
|
|
928
1749
|
var addCommand = new Command("add").description("Install a skill, agent, hook, or other configuration").argument("[name]", "Name of the item to install").option("-t, --type <type>", "Content type: skill, agent, hook, mcp, plugin, settings").option(
|
|
929
1750
|
"-a, --agents <agents>",
|
|
930
|
-
"Comma-separated coding agents or 'all' (claude,copilot,gemini,codex,opencode)"
|
|
931
|
-
).option("-s, --scope <scope>", "Installation scope: project, user, or local").option("-m, --method <method>", "Installation method: symlink or copy").option("-y, --yes", "Skip confirmation prompts").option("-f, --force", "Overwrite existing files").option("-n, --dry-run", "Show
|
|
1751
|
+
"Comma-separated coding agents or 'all' (claude,copilot,antigravity,codex,opencode; 'gemini' is a deprecated alias of antigravity). Every named agent must support the item; 'all' means all compatible agents. MCP servers: claude, codex, opencode (copilot's and antigravity's formats are unverified)"
|
|
1752
|
+
).option("-s, --scope <scope>", "Installation scope: project, user, or local").option("-m, --method <method>", "Installation method: symlink or copy").option("-y, --yes", "Skip confirmation prompts").option("-f, --force", "Overwrite existing files").option("-n, --dry-run", "Show exactly which files would be written, without writing or reporting anything").addHelpText("after", `
|
|
1753
|
+
Telemetry: ${TELEMETRY_HELP_TEXT}.`).action(async (name, options) => {
|
|
932
1754
|
try {
|
|
933
1755
|
printLogo();
|
|
934
1756
|
intro("Seedr");
|
|
935
|
-
const
|
|
936
|
-
if (
|
|
937
|
-
error(optionError);
|
|
938
|
-
process.exit(1);
|
|
939
|
-
}
|
|
940
|
-
const itemName = name;
|
|
941
|
-
const contentType = options.type;
|
|
942
|
-
const item = await resolveItem(itemName, contentType);
|
|
943
|
-
if (!item) process.exit(1);
|
|
944
|
-
step(`Selected: ${brand(item.name)} ${chalk7.gray(`(${item.type})`)} ${chalk7.gray(`- ${item.description}`)}`);
|
|
945
|
-
const handler = getHandler(item.type);
|
|
946
|
-
if (!handler) {
|
|
947
|
-
error(`No handler found for type "${item.type}"`);
|
|
948
|
-
process.exit(1);
|
|
949
|
-
}
|
|
950
|
-
let agents = resolveAgents(options.agents, item);
|
|
951
|
-
const typeCompatible = filterCompatibleAgents(item.type, item.compatibility);
|
|
952
|
-
if (agents.length === 0) {
|
|
953
|
-
if (typeCompatible.length === 1) {
|
|
954
|
-
agents = typeCompatible;
|
|
955
|
-
} else {
|
|
956
|
-
const selected = await selectAgents(typeCompatible);
|
|
957
|
-
if (p.isCancel(selected)) {
|
|
958
|
-
cancelled();
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
agents = selected;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
if (agents.length === 0) {
|
|
965
|
-
error("No valid agents selected");
|
|
966
|
-
process.exit(1);
|
|
967
|
-
}
|
|
968
|
-
step(`Agents: ${brand(agents.join(", "))}`);
|
|
969
|
-
let scope;
|
|
970
|
-
if (options.scope) {
|
|
971
|
-
scope = options.scope;
|
|
972
|
-
} else {
|
|
973
|
-
const supportsLocal = ["plugin", "settings", "hook"].includes(item.type);
|
|
974
|
-
const selected = await selectScope(supportsLocal);
|
|
975
|
-
if (p.isCancel(selected)) {
|
|
976
|
-
cancelled();
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
scope = selected;
|
|
980
|
-
}
|
|
981
|
-
step(`Scope: ${brand(scope)}`);
|
|
982
|
-
let method;
|
|
983
|
-
if (options.method) {
|
|
984
|
-
method = options.method;
|
|
985
|
-
} else if (agents.length === 1) {
|
|
986
|
-
method = "copy";
|
|
987
|
-
} else {
|
|
988
|
-
const symlinkPath = getAgentsPath(item.type, item.slug, scope, process.cwd());
|
|
989
|
-
const selected = await selectMethod(symlinkPath);
|
|
990
|
-
if (p.isCancel(selected)) {
|
|
991
|
-
cancelled();
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
method = selected;
|
|
995
|
-
}
|
|
996
|
-
step(`Method: ${brand(method)}`);
|
|
997
|
-
if (options.dryRun) {
|
|
998
|
-
console.log();
|
|
999
|
-
printDryRunSummary(item, agents, scope, method, process.cwd());
|
|
1000
|
-
outro("Dry run complete");
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
if (!options.yes) {
|
|
1004
|
-
console.log();
|
|
1005
|
-
const confirmed = await confirm("Proceed with installation?");
|
|
1006
|
-
if (p.isCancel(confirmed) || !confirmed) {
|
|
1007
|
-
cancelled();
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
const force = Boolean(options.force) || !options.yes;
|
|
1012
|
-
console.log();
|
|
1013
|
-
const results = await handler.install(item, agents, scope, method, force, process.cwd());
|
|
1014
|
-
trackInstalls(item.slug, item.type, results, scope);
|
|
1015
|
-
printInstallSummary(results);
|
|
1016
|
-
outro("Installation complete");
|
|
1757
|
+
const exitCode = await runAdd(name, options);
|
|
1758
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
1017
1759
|
} catch (error2) {
|
|
1018
1760
|
handleCommandError(error2);
|
|
1019
1761
|
}
|
|
@@ -1033,13 +1775,54 @@ var TYPE_COLORS = {
|
|
|
1033
1775
|
settings: chalk8.hex("#f97316"),
|
|
1034
1776
|
mcp: chalk8.hex("#2dd4bf")
|
|
1035
1777
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1778
|
+
async function collectInstalledItems(params) {
|
|
1779
|
+
const types = params.types ?? getRegisteredTypes();
|
|
1780
|
+
const agents = params.agents ?? ALL_AGENTS;
|
|
1781
|
+
const groups = [];
|
|
1782
|
+
for (const type of types) {
|
|
1783
|
+
const handler = getHandler(type);
|
|
1784
|
+
if (!handler) continue;
|
|
1785
|
+
for (const agent of agents) {
|
|
1786
|
+
const slugs = await handler.listInstalled(agent, params.scope, params.cwd);
|
|
1787
|
+
if (slugs.length > 0) {
|
|
1788
|
+
groups.push({ type, agent, slugs: [...slugs].sort() });
|
|
1789
|
+
}
|
|
1042
1790
|
}
|
|
1791
|
+
}
|
|
1792
|
+
return groups;
|
|
1793
|
+
}
|
|
1794
|
+
function resolveListAgents(agentsArg) {
|
|
1795
|
+
if (!agentsArg || agentsArg === "all") return [...ALL_AGENTS];
|
|
1796
|
+
const { agents, unknown } = parseAgentsArgStrict(agentsArg);
|
|
1797
|
+
if (unknown.length > 0) {
|
|
1798
|
+
return `Unknown agent(s): ${unknown.join(", ")}. Valid agents: ${ALL_AGENTS.join(", ")} or "all"`;
|
|
1799
|
+
}
|
|
1800
|
+
return agents;
|
|
1801
|
+
}
|
|
1802
|
+
async function runList(options, cwd = process.cwd()) {
|
|
1803
|
+
const optionError = validateType(options.type) || validateScope(options.scope);
|
|
1804
|
+
if (optionError) {
|
|
1805
|
+
console.log(chalk8.red(optionError));
|
|
1806
|
+
return 1;
|
|
1807
|
+
}
|
|
1808
|
+
const type = options.type;
|
|
1809
|
+
if (!options.installed) {
|
|
1810
|
+
await listAvailable(type);
|
|
1811
|
+
return 0;
|
|
1812
|
+
}
|
|
1813
|
+
const agents = resolveListAgents(options.agents);
|
|
1814
|
+
if (typeof agents === "string") {
|
|
1815
|
+
console.log(chalk8.red(agents));
|
|
1816
|
+
return 1;
|
|
1817
|
+
}
|
|
1818
|
+
const scope = options.scope ?? "project";
|
|
1819
|
+
await listInstalled({ types: type ? [type] : void 0, agents, scope, cwd });
|
|
1820
|
+
return 0;
|
|
1821
|
+
}
|
|
1822
|
+
var listCommand = new Command2("list").alias("ls").description("List available or installed items").option("-t, --type <type>", "Filter by type (skill, hook, agent, plugin, mcp, settings)").option("-i, --installed", "Show only installed items").option("-a, --agents <agents>", "Comma-separated coding agents or 'all' (installed check)", "all").option("--scope <scope>", "Scope for installed check (project, user, local)", "project").action(async (options) => {
|
|
1823
|
+
try {
|
|
1824
|
+
const exitCode = await runList(options);
|
|
1825
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
1043
1826
|
} catch (error2) {
|
|
1044
1827
|
handleCommandError(error2);
|
|
1045
1828
|
}
|
|
@@ -1057,7 +1840,7 @@ async function listAvailable(type) {
|
|
|
1057
1840
|
${itemType.toUpperCase()}S`));
|
|
1058
1841
|
console.log(chalk8.gray("\u2500".repeat(SEPARATOR_WIDTH)));
|
|
1059
1842
|
for (const item of typeItems) {
|
|
1060
|
-
const compatIcons = item.compatibility.map((a) => CODING_AGENTS[a]
|
|
1843
|
+
const compatIcons = [...new Set(item.compatibility.map((a) => CODING_AGENTS[a]?.shortName ?? a))].join(" ");
|
|
1061
1844
|
const featured = item.featured ? chalk8.yellow("\u2605 ") : " ";
|
|
1062
1845
|
console.log(
|
|
1063
1846
|
`${featured}${chalk8.white(item.slug.padEnd(SLUG_COLUMN_WIDTH))} ${chalk8.gray(compatIcons)}`
|
|
@@ -1070,27 +1853,31 @@ ${itemType.toUpperCase()}S`));
|
|
|
1070
1853
|
chalk8.gray(`Total: ${items.length} items. Use 'npx @danieldeusing/seedr add <name>' to install.`)
|
|
1071
1854
|
);
|
|
1072
1855
|
}
|
|
1073
|
-
async function listInstalled(
|
|
1856
|
+
async function listInstalled(params) {
|
|
1074
1857
|
console.log(brand(`
|
|
1075
|
-
Installed
|
|
1858
|
+
Installed items (${params.scope} scope):
|
|
1076
1859
|
`));
|
|
1860
|
+
const groups = await collectInstalledItems(params);
|
|
1077
1861
|
let total = 0;
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
console.log(chalk8.blue(CODING_AGENTS[agent].name));
|
|
1085
|
-
for (const skill of installed) {
|
|
1086
|
-
console.log(` ${chalk8.white(skill)}`);
|
|
1087
|
-
total++;
|
|
1088
|
-
}
|
|
1089
|
-
console.log("");
|
|
1862
|
+
let currentType = null;
|
|
1863
|
+
for (const group of groups) {
|
|
1864
|
+
if (group.type !== currentType) {
|
|
1865
|
+
currentType = group.type;
|
|
1866
|
+
const colorFn = TYPE_COLORS[group.type] ?? chalk8.white;
|
|
1867
|
+
console.log(colorFn(`${group.type.toUpperCase()}S`));
|
|
1090
1868
|
}
|
|
1869
|
+
console.log(chalk8.blue(` ${CODING_AGENTS[group.agent].name}`));
|
|
1870
|
+
for (const slug of group.slugs) {
|
|
1871
|
+
console.log(` ${chalk8.white(slug)}`);
|
|
1872
|
+
total++;
|
|
1873
|
+
}
|
|
1874
|
+
console.log("");
|
|
1875
|
+
}
|
|
1876
|
+
if (!params.types || params.types.includes("settings")) {
|
|
1877
|
+
console.log(chalk8.gray(`Note: ${SETTINGS_NOT_DISCOVERABLE}`));
|
|
1091
1878
|
}
|
|
1092
1879
|
if (total === 0) {
|
|
1093
|
-
console.log(chalk8.yellow("No
|
|
1880
|
+
console.log(chalk8.yellow("No items installed"));
|
|
1094
1881
|
} else {
|
|
1095
1882
|
console.log(chalk8.gray(`Total: ${total} installed`));
|
|
1096
1883
|
}
|
|
@@ -1112,41 +1899,25 @@ function groupByType(items) {
|
|
|
1112
1899
|
import { Command as Command3 } from "commander";
|
|
1113
1900
|
import chalk9 from "chalk";
|
|
1114
1901
|
import ora6 from "ora";
|
|
1115
|
-
|
|
1116
|
-
// src/utils/prompts.ts
|
|
1117
|
-
import inquirer from "inquirer";
|
|
1118
|
-
async function promptConfirm(message, defaultValue = true) {
|
|
1119
|
-
const { confirmed } = await inquirer.prompt([
|
|
1120
|
-
{
|
|
1121
|
-
type: "confirm",
|
|
1122
|
-
name: "confirmed",
|
|
1123
|
-
message,
|
|
1124
|
-
default: defaultValue
|
|
1125
|
-
}
|
|
1126
|
-
]);
|
|
1127
|
-
return confirmed;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// src/commands/remove.ts
|
|
1131
|
-
async function findInstalledAgents(slug, type, scope) {
|
|
1902
|
+
async function findInstalledAgents(slug, type, scope, cwd) {
|
|
1132
1903
|
const handler = getHandler(type);
|
|
1133
1904
|
if (!handler) return [];
|
|
1134
1905
|
const agents = [];
|
|
1135
1906
|
for (const agent of ALL_AGENTS) {
|
|
1136
|
-
const installed = await handler.listInstalled(agent, scope);
|
|
1907
|
+
const installed = await handler.listInstalled(agent, scope, cwd);
|
|
1137
1908
|
if (installed.includes(slug)) {
|
|
1138
1909
|
agents.push(agent);
|
|
1139
1910
|
}
|
|
1140
1911
|
}
|
|
1141
1912
|
return agents;
|
|
1142
1913
|
}
|
|
1143
|
-
async function removeFromAgents(slug, type, agents, scope) {
|
|
1914
|
+
async function removeFromAgents(slug, type, agents, scope, cwd) {
|
|
1144
1915
|
const handler = getHandler(type);
|
|
1145
1916
|
if (!handler) return 0;
|
|
1146
1917
|
let successCount = 0;
|
|
1147
1918
|
for (const agent of agents) {
|
|
1148
1919
|
const spinner = ora6(`Removing from ${CODING_AGENTS[agent].name}...`).start();
|
|
1149
|
-
const removed = await handler.uninstall(slug, agent, scope);
|
|
1920
|
+
const removed = await handler.uninstall(slug, agent, scope, cwd);
|
|
1150
1921
|
if (removed) {
|
|
1151
1922
|
spinner.succeed(brand(`Removed from ${CODING_AGENTS[agent].name}`));
|
|
1152
1923
|
successCount++;
|
|
@@ -1156,6 +1927,76 @@ async function removeFromAgents(slug, type, agents, scope) {
|
|
|
1156
1927
|
}
|
|
1157
1928
|
return successCount;
|
|
1158
1929
|
}
|
|
1930
|
+
function validateRemoveRequest(name, options) {
|
|
1931
|
+
if (!isValidSlug(name)) {
|
|
1932
|
+
return `Invalid item name ${JSON.stringify(name)}: expected a slug matching ${SLUG_PATTERN.source} (at most ${MAX_SLUG_LENGTH} characters)`;
|
|
1933
|
+
}
|
|
1934
|
+
const optionError = validateScope(options.scope) || validateType(options.type);
|
|
1935
|
+
if (optionError) return optionError;
|
|
1936
|
+
if (!options.type) {
|
|
1937
|
+
return "Please specify the content type with --type (skill, plugin, agent, hook, mcp, settings)";
|
|
1938
|
+
}
|
|
1939
|
+
if (options.agents && options.agents !== "all") {
|
|
1940
|
+
const { unknown } = parseAgentsArgStrict(options.agents);
|
|
1941
|
+
if (unknown.length > 0) {
|
|
1942
|
+
return `Unknown agent(s): ${unknown.join(", ")}. Valid agents: ${ALL_AGENTS.join(", ")} or "all"`;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return null;
|
|
1946
|
+
}
|
|
1947
|
+
async function runRemove(name, options, cwd = process.cwd()) {
|
|
1948
|
+
const requestError = validateRemoveRequest(name, options);
|
|
1949
|
+
if (requestError) {
|
|
1950
|
+
console.log(chalk9.red(requestError));
|
|
1951
|
+
return 1;
|
|
1952
|
+
}
|
|
1953
|
+
const scope = options.scope ?? "project";
|
|
1954
|
+
const type = options.type;
|
|
1955
|
+
const handler = getHandler(type);
|
|
1956
|
+
if (!handler) {
|
|
1957
|
+
console.log(chalk9.red(`No handler found for type "${type}"`));
|
|
1958
|
+
return 1;
|
|
1959
|
+
}
|
|
1960
|
+
let agents;
|
|
1961
|
+
if (!options.agents) {
|
|
1962
|
+
agents = await findInstalledAgents(name, type, scope, cwd);
|
|
1963
|
+
} else if (options.agents === "all") {
|
|
1964
|
+
agents = [...ALL_AGENTS];
|
|
1965
|
+
} else {
|
|
1966
|
+
agents = parseAgentsArgStrict(options.agents).agents;
|
|
1967
|
+
}
|
|
1968
|
+
if (agents.length === 0) {
|
|
1969
|
+
if (type === "settings") {
|
|
1970
|
+
console.log(chalk9.yellow(`Cannot detect where "${name}" is installed: ${SETTINGS_NOT_DISCOVERABLE}`));
|
|
1971
|
+
console.log(chalk9.gray("Name the agents explicitly, e.g. --agents claude"));
|
|
1972
|
+
return 1;
|
|
1973
|
+
}
|
|
1974
|
+
console.log(chalk9.yellow(`${type} "${name}" is not installed in ${scope} scope`));
|
|
1975
|
+
return 0;
|
|
1976
|
+
}
|
|
1977
|
+
if (!options.yes) {
|
|
1978
|
+
console.log(brand(`
|
|
1979
|
+
Will remove ${type} "${name}" from:`));
|
|
1980
|
+
for (const agent of agents) {
|
|
1981
|
+
console.log(` - ${CODING_AGENTS[agent].name}`);
|
|
1982
|
+
}
|
|
1983
|
+
console.log("");
|
|
1984
|
+
const answer = await confirm("Proceed with removal?");
|
|
1985
|
+
const confirmed = !p.isCancel(answer) && answer;
|
|
1986
|
+
if (!confirmed) {
|
|
1987
|
+
console.log(chalk9.yellow("Removal cancelled"));
|
|
1988
|
+
return 0;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
const successCount = await removeFromAgents(name, type, agents, scope, cwd);
|
|
1992
|
+
console.log("");
|
|
1993
|
+
if (successCount > 0) {
|
|
1994
|
+
console.log(brand(`Successfully removed from ${successCount} agent(s)`));
|
|
1995
|
+
return 0;
|
|
1996
|
+
}
|
|
1997
|
+
console.log(chalk9.yellow("Nothing to remove"));
|
|
1998
|
+
return 0;
|
|
1999
|
+
}
|
|
1159
2000
|
var removeCommand = new Command3("remove").alias("rm").description("Remove an installed item (skill, plugin, agent, hook, mcp)").argument("<name>", "Name/slug of the item to remove").option("-t, --type <type>", "Content type: skill, agent, hook, mcp, plugin, settings").option(
|
|
1160
2001
|
"-a, --agents <agents>",
|
|
1161
2002
|
"Comma-separated coding agents or 'all'"
|
|
@@ -1165,53 +2006,8 @@ var removeCommand = new Command3("remove").alias("rm").description("Remove an in
|
|
|
1165
2006
|
"project"
|
|
1166
2007
|
).option("-y, --yes", "Skip confirmation prompts").action(async (name, options) => {
|
|
1167
2008
|
try {
|
|
1168
|
-
const
|
|
1169
|
-
if (
|
|
1170
|
-
console.log(chalk9.red(optionError));
|
|
1171
|
-
process.exit(1);
|
|
1172
|
-
}
|
|
1173
|
-
const scope = options.scope;
|
|
1174
|
-
const type = options.type;
|
|
1175
|
-
if (!type) {
|
|
1176
|
-
console.log(
|
|
1177
|
-
chalk9.yellow(`Please specify the content type with --type (skill, plugin, agent, hook, mcp, settings)`)
|
|
1178
|
-
);
|
|
1179
|
-
process.exit(1);
|
|
1180
|
-
}
|
|
1181
|
-
const handler = getHandler(type);
|
|
1182
|
-
if (!handler) {
|
|
1183
|
-
console.log(chalk9.red(`No handler found for type "${type}"`));
|
|
1184
|
-
process.exit(1);
|
|
1185
|
-
}
|
|
1186
|
-
const agents = options.agents ? parseAgentsArg(options.agents, ALL_AGENTS) : await findInstalledAgents(name, type, scope);
|
|
1187
|
-
if (agents.length === 0) {
|
|
1188
|
-
console.log(
|
|
1189
|
-
chalk9.yellow(`${type} "${name}" is not installed in ${scope} scope`)
|
|
1190
|
-
);
|
|
1191
|
-
process.exit(0);
|
|
1192
|
-
}
|
|
1193
|
-
if (!options.yes) {
|
|
1194
|
-
console.log(brand(`
|
|
1195
|
-
Will remove ${type} "${name}" from:`));
|
|
1196
|
-
for (const agent of agents) {
|
|
1197
|
-
console.log(` - ${CODING_AGENTS[agent].name}`);
|
|
1198
|
-
}
|
|
1199
|
-
console.log("");
|
|
1200
|
-
const confirmed = await promptConfirm("Proceed with removal?");
|
|
1201
|
-
if (!confirmed) {
|
|
1202
|
-
console.log(chalk9.yellow("Removal cancelled"));
|
|
1203
|
-
process.exit(0);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
const successCount = await removeFromAgents(name, type, agents, scope);
|
|
1207
|
-
console.log("");
|
|
1208
|
-
if (successCount > 0) {
|
|
1209
|
-
console.log(
|
|
1210
|
-
brand(`Successfully removed from ${successCount} agent(s)`)
|
|
1211
|
-
);
|
|
1212
|
-
} else {
|
|
1213
|
-
console.log(chalk9.yellow("Nothing to remove"));
|
|
1214
|
-
}
|
|
2009
|
+
const exitCode = await runRemove(name, options);
|
|
2010
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
1215
2011
|
} catch (error2) {
|
|
1216
2012
|
handleCommandError(error2);
|
|
1217
2013
|
}
|
|
@@ -1222,44 +2018,8 @@ import { Command as Command4 } from "commander";
|
|
|
1222
2018
|
import chalk10 from "chalk";
|
|
1223
2019
|
import ora7 from "ora";
|
|
1224
2020
|
import { join as join4 } from "path";
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
"Comma-separated coding agents or 'all'",
|
|
1228
|
-
"claude"
|
|
1229
|
-
).option("-y, --yes", "Skip confirmation prompts").action(async (options) => {
|
|
1230
|
-
try {
|
|
1231
|
-
const agents = parseAgentsArg(options.agents, ALL_AGENTS);
|
|
1232
|
-
if (agents.length === 0) {
|
|
1233
|
-
console.error(chalk10.red("No valid agents specified"));
|
|
1234
|
-
process.exit(1);
|
|
1235
|
-
}
|
|
1236
|
-
console.log(brand("\nWill initialize configuration for:"));
|
|
1237
|
-
for (const agent of agents) {
|
|
1238
|
-
const path = getAgentPath(agent, "project");
|
|
1239
|
-
console.log(` - ${CODING_AGENTS[agent].name} \u2192 ${path}`);
|
|
1240
|
-
}
|
|
1241
|
-
console.log("");
|
|
1242
|
-
if (!options.yes) {
|
|
1243
|
-
const confirmed = await promptConfirm("Proceed?");
|
|
1244
|
-
if (!confirmed) {
|
|
1245
|
-
console.log(chalk10.yellow("Cancelled"));
|
|
1246
|
-
process.exit(0);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
for (const agent of agents) {
|
|
1250
|
-
const spinner = ora7(`Initializing ${CODING_AGENTS[agent].name}...`).start();
|
|
1251
|
-
const path = getAgentPath(agent, "project");
|
|
1252
|
-
if (await exists(path)) {
|
|
1253
|
-
spinner.info(
|
|
1254
|
-
chalk10.gray(`${CODING_AGENTS[agent].name} already initialized`)
|
|
1255
|
-
);
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1258
|
-
await ensureDir(path);
|
|
1259
|
-
const readmePath = join4(path, "README.md");
|
|
1260
|
-
await writeTextFile(
|
|
1261
|
-
readmePath,
|
|
1262
|
-
`# ${CODING_AGENTS[agent].name} Configuration
|
|
2021
|
+
function readmeFor(agent) {
|
|
2022
|
+
return `# ${CODING_AGENTS[agent].name} Configuration
|
|
1263
2023
|
|
|
1264
2024
|
This directory contains AI configuration files for ${CODING_AGENTS[agent].name}.
|
|
1265
2025
|
|
|
@@ -1269,14 +2029,65 @@ npx @danieldeusing/seedr add <skill-name> --agents ${agent}
|
|
|
1269
2029
|
\`\`\`
|
|
1270
2030
|
|
|
1271
2031
|
Browse available skills at https://seedr.danieldeusing.de
|
|
1272
|
-
|
|
1273
|
-
|
|
2032
|
+
`;
|
|
2033
|
+
}
|
|
2034
|
+
async function initializeAgent(agent, cwd) {
|
|
2035
|
+
const path = getAgentPath(agent, "project", cwd);
|
|
2036
|
+
if (await exists(path)) return false;
|
|
2037
|
+
await ensureDir(path);
|
|
2038
|
+
await writeTextFile(join4(path, "README.md"), readmeFor(agent));
|
|
2039
|
+
return true;
|
|
2040
|
+
}
|
|
2041
|
+
async function runInit(options, cwd = process.cwd()) {
|
|
2042
|
+
const agentsArg = options.agents ?? "claude";
|
|
2043
|
+
let agents;
|
|
2044
|
+
if (agentsArg === "all") {
|
|
2045
|
+
agents = [...ALL_AGENTS];
|
|
2046
|
+
} else {
|
|
2047
|
+
const parsed = parseAgentsArgStrict(agentsArg);
|
|
2048
|
+
if (parsed.unknown.length > 0) {
|
|
2049
|
+
console.error(chalk10.red(`Unknown agent(s): ${parsed.unknown.join(", ")}. Valid agents: ${ALL_AGENTS.join(", ")} or "all"`));
|
|
2050
|
+
return 1;
|
|
2051
|
+
}
|
|
2052
|
+
agents = parsed.agents;
|
|
2053
|
+
}
|
|
2054
|
+
if (agents.length === 0) {
|
|
2055
|
+
console.error(chalk10.red("No valid agents specified"));
|
|
2056
|
+
return 1;
|
|
2057
|
+
}
|
|
2058
|
+
console.log(brand("\nWill initialize configuration for:"));
|
|
2059
|
+
for (const agent of agents) {
|
|
2060
|
+
console.log(` - ${CODING_AGENTS[agent].name} \u2192 ${getAgentPath(agent, "project", cwd)}`);
|
|
2061
|
+
}
|
|
2062
|
+
console.log("");
|
|
2063
|
+
if (!options.yes) {
|
|
2064
|
+
const answer = await confirm("Proceed?");
|
|
2065
|
+
const confirmed = !p.isCancel(answer) && answer;
|
|
2066
|
+
if (!confirmed) {
|
|
2067
|
+
console.log(chalk10.yellow("Cancelled"));
|
|
2068
|
+
return 0;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
for (const agent of agents) {
|
|
2072
|
+
const spinner = ora7(`Initializing ${CODING_AGENTS[agent].name}...`).start();
|
|
2073
|
+
if (await initializeAgent(agent, cwd)) {
|
|
1274
2074
|
spinner.succeed(brand(`Initialized ${CODING_AGENTS[agent].name}`));
|
|
2075
|
+
} else {
|
|
2076
|
+
spinner.info(chalk10.gray(`${CODING_AGENTS[agent].name} already initialized`));
|
|
1275
2077
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
2078
|
+
}
|
|
2079
|
+
console.log("");
|
|
2080
|
+
console.log(brand("Done! Use 'npx @danieldeusing/seedr add <skill>' to install skills."));
|
|
2081
|
+
return 0;
|
|
2082
|
+
}
|
|
2083
|
+
var initCommand = new Command4("init").description("Initialize coding agent configuration directories").option(
|
|
2084
|
+
"-a, --agents <agents>",
|
|
2085
|
+
"Comma-separated coding agents or 'all'",
|
|
2086
|
+
"claude"
|
|
2087
|
+
).option("-y, --yes", "Skip confirmation prompts").action(async (options) => {
|
|
2088
|
+
try {
|
|
2089
|
+
const exitCode = await runInit(options);
|
|
2090
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
1280
2091
|
} catch (error2) {
|
|
1281
2092
|
handleCommandError(error2);
|
|
1282
2093
|
}
|
|
@@ -1285,7 +2096,7 @@ Browse available skills at https://seedr.danieldeusing.de
|
|
|
1285
2096
|
// package.json
|
|
1286
2097
|
var package_default = {
|
|
1287
2098
|
name: "@danieldeusing/seedr",
|
|
1288
|
-
version: "0.1.
|
|
2099
|
+
version: "0.1.88",
|
|
1289
2100
|
description: "Seed your projects with AI configurations",
|
|
1290
2101
|
type: "module",
|
|
1291
2102
|
bin: {
|
|
@@ -1315,20 +2126,18 @@ var package_default = {
|
|
|
1315
2126
|
"@clack/prompts": "^1.0.1",
|
|
1316
2127
|
chalk: "^5.3.0",
|
|
1317
2128
|
commander: "^14.0.0",
|
|
1318
|
-
"gray-matter": "^4.0.3",
|
|
1319
|
-
inquirer: "^13.0.0",
|
|
1320
2129
|
ora: "^9.0.0"
|
|
1321
2130
|
},
|
|
1322
2131
|
devDependencies: {
|
|
1323
2132
|
"@seedr/shared": "workspace:*",
|
|
1324
|
-
"@types/inquirer": "^9.0.7",
|
|
1325
2133
|
"@types/node": "^25.0.0",
|
|
1326
2134
|
"@vitest/coverage-v8": "^4.1.0",
|
|
1327
2135
|
memfs: "^4.14.0",
|
|
1328
2136
|
tsup: "^8.3.0",
|
|
1329
2137
|
tsx: "^4.19.0",
|
|
1330
2138
|
typescript: "^5.7.0",
|
|
1331
|
-
vitest: "^4.1.0"
|
|
2139
|
+
vitest: "^4.1.0",
|
|
2140
|
+
"@seedr/registry-ops": "workspace:*"
|
|
1332
2141
|
},
|
|
1333
2142
|
keywords: [
|
|
1334
2143
|
"ai",
|