@danieldeusing/seedr 0.1.86 → 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/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
- cancelled,
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
- getInstalledSkills,
24
+ getEffectiveSourceRevision,
17
25
  getItem,
18
26
  getItemContent,
19
27
  getItemSourcePath,
20
- getMcpPath,
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
- parseAgentsArg,
41
+ parseAgentsArgStrict,
42
+ parseGitHubRepo,
30
43
  printLogo,
31
- removeFile,
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-77M7PGDO.js";
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
- console.error(
64
- chalk.red(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`)
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
- function trackInstalls(slug, type, results, scope) {
106
- if (process.env.SEEDR_NO_TELEMETRY) return;
107
- const version = true ? "0.1.86" : "dev";
108
- for (const result of results) {
109
- if (!result.success) continue;
110
- fetch(ANALYTICS_URL, {
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
- slug,
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", "gemini", "codex", "opencode"],
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", "copilot", "gemini", "codex", "opencode"]
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 { readdir } from "fs/promises";
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
- const destDir = getContentPath(agent, "agent", scope, cwd);
152
- if (!destDir) {
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
- if (sourcePath && method === "symlink") {
159
- const sourceFile = join(sourcePath, "AGENT.md");
160
- if (await exists(sourceFile)) {
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(destDir);
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
- const destDir = getContentPath(agent, "agent", scope, cwd);
194
- if (!destDir) return false;
195
- const destPath = join(destDir, `${slug}.md`);
196
- if (!await exists(destPath)) {
197
- return false;
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
- return removeFile(destPath);
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, copyFile, chmod, rm } from "fs/promises";
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, writeFile } from "fs/promises";
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 writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
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
- switch (scope) {
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("Hooks are only supported for Claude Code");
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
- const hooksDir = getHooksDir(scope, cwd);
310
- await mkdir(hooksDir, { recursive: true });
311
- const sourcePath = getItemSourcePath(item);
312
- const destScriptPath = join2(hooksDir, scriptFile);
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
- if (sourcePath && await exists(sourcePath)) {
315
- const sourceScriptPath = join2(sourcePath, scriptFile);
316
- await copyFile(sourceScriptPath, destScriptPath);
317
- } else {
318
- const tempDir = join2(cwd, ".claude", ".tmp", item.slug);
319
- await fetchItemToDestination(item, tempDir);
320
- await copyFile(join2(tempDir, scriptFile), destScriptPath);
321
- await rm(tempDir, { recursive: true, force: true });
322
- }
323
- await chmod(destScriptPath, 493);
324
- const settingsPath = getSettingsPath(scope, cwd);
325
- const settings = await readJson(settingsPath);
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 = false;
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
- const hooksDir = getHooksDir(scope, cwd);
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
- const slug = basename(hook.command, ".sh");
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
- return JSON.parse(content);
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 content = await getItemContent(item);
471
- const mcpDef = parseMcpDefinition(content);
472
- const configPath = getMcpPath(scope, cwd);
473
- const config = await readJson(configPath);
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
- config.mcpServers[mcpDef.name] = mcpDef.config;
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 uninstallMcp(slug, _agent, scope, cwd = process.cwd()) {
503
- const configPath = getMcpPath(scope, cwd);
504
- if (!await exists(configPath)) return false;
505
- const config = await readJson(configPath);
506
- if (!config.mcpServers || !(slug in config.mcpServers)) {
507
- return false;
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 getInstalledMcpServers(_agent, scope, cwd = process.cwd()) {
514
- const configPath = getMcpPath(scope, cwd);
515
- if (!await exists(configPath)) return [];
516
- const config = await readJson(configPath);
517
- return Object.keys(config.mcpServers || {});
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("Settings are only supported for Claude Code");
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(agent, scope, cwd = process.cwd()) {
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 homedir2 } from "os";
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 = homedir2();
651
- var PLUGINS_CACHE_DIR = join3(home, ".claude/plugins/cache");
652
- var INSTALLED_PLUGINS_PATH = join3(home, ".claude/plugins/installed_plugins.json");
653
- var KNOWN_MARKETPLACES_PATH = join3(home, ".claude/plugins/known_marketplaces.json");
654
- var MARKETPLACES_DIR = join3(home, ".claude/plugins/marketplaces");
655
- function getPluginCachePath(marketplace, name, version) {
656
- return join3(PLUGINS_CACHE_DIR, marketplace, name, version);
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 match = url.match(/github\.com\/([^/]+\/[^/]+)/);
663
- return match?.[1]?.replace(/\.git$/, "") ?? null;
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 repo = item.externalUrl ? extractGitHubRepo(item.externalUrl) : null;
1213
+ const sourceUrl = marketplaceRepoUrl(item);
1214
+ const repo = sourceUrl ? extractGitHubRepo(sourceUrl) : null;
671
1215
  if (!repo) return;
672
- const installLocation = join3(MARKETPLACES_DIR, marketplace);
673
- const execFileAsync = promisify(execFile);
1216
+ const installLocation = await resolveContained(MARKETPLACES_DIR, marketplace);
674
1217
  await mkdir2(MARKETPLACES_DIR, { recursive: true });
675
- await execFileAsync("git", [
676
- "clone",
677
- "--depth",
678
- "1",
679
- `https://github.com/${repo}.git`,
680
- installLocation
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 installPluginForAgent(item, agent, scope, method, force, cwd) {
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("Plugins are only supported for Claude Code");
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
- const pluginJson = await readJson(
705
- join3(tmpPath, ".claude-plugin", "plugin.json")
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
- } catch (error2) {
716
- await rm2(tmpPath, { recursive: true, force: true });
717
- throw error2;
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 matchingKey = Object.keys(registry.plugins).find(
770
- (key) => key.startsWith(`${slug}@`)
771
- );
772
- if (!matchingKey) return false;
773
- const entries = registry.plugins[matchingKey] || [];
774
- const filteredEntries = entries.filter(
775
- (e) => !(e.scope === scope && (scope === "user" || e.projectPath === cwd))
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[matchingKey] = filteredEntries;
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 && matchingKey in settings.enabledPlugins) {
786
- delete settings.enabledPlugins[matchingKey];
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
- for (const entry of entries) {
798
- if (entry.scope === scope && (scope === "user" || entry.projectPath === cwd)) {
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
- async function resolveItem(itemName, type) {
830
- if (itemName) {
831
- const item = await getItem(itemName, type);
832
- if (item) return item;
833
- const results = await searchItems(itemName);
834
- const filtered = type ? results.filter((r) => r.type === type) : results;
835
- if (filtered.length === 0) {
836
- if (type && results.length > 0) {
837
- const match = results.find((r) => r.slug === itemName);
838
- if (match) {
839
- error(`"${itemName}" is a ${match.type}, not a ${type}. Run: seedr add ${itemName} --type ${match.type}`);
840
- } else {
841
- error(`No ${type} matching "${itemName}". Found: ${results.map((r) => `${r.slug} (${r.type})`).join(", ")}`);
842
- }
843
- } else {
844
- error(`"${itemName}" not found`);
845
- }
846
- return null;
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
- if (filtered.length === 1) {
849
- return filtered[0];
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
- warn(`Multiple matches for "${itemName}"`);
852
- const selected2 = await selectSkill(filtered);
853
- if (p.isCancel(selected2)) {
854
- cancelled();
855
- return null;
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
- cancelled();
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 resolveAgents(agentsArg, item) {
872
- const typeCompatible = filterCompatibleAgents(item.type, item.compatibility);
873
- if (!agentsArg) return [];
874
- const agents = parseAgentsArg(agentsArg, typeCompatible);
875
- if (agentsArg === "all") return agents;
876
- const incompatible = agents.filter((a) => !typeCompatible.includes(a));
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 printDryRunSummary(item, agents, scope, method, cwd) {
884
- info("Dry run - no files will be written\n");
885
- console.log(brand(" Would install:"));
886
- console.log(` ${item.type}: ${chalk7.white(item.name)}`);
887
- console.log(` Scope: ${chalk7.white(scope)}`);
888
- console.log(` Method: ${chalk7.white(method)}`);
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
- if (method === "symlink" && item.type === "skill") {
891
- const centralPath = getAgentsPath("skill", item.slug, scope, cwd);
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.filter((r) => r.success);
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 what would be installed without making changes").action(async (name, options) => {
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 optionError = validateType(options.type) || validateScope(options.scope) || validateMethod(options.method);
936
- if (optionError) {
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
- var listCommand = new Command2("list").alias("ls").description("List available or installed skills").option("-t, --type <type>", "Filter by type (skill, hook, agent, plugin)").option("-i, --installed", "Show only installed items").option("--scope <scope>", "Scope for installed check (project, user)", "project").action(async (options) => {
1037
- try {
1038
- if (options.installed) {
1039
- await listInstalled(options.scope);
1040
- } else {
1041
- await listAvailable(options.type);
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].shortName).join(" ");
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(scope) {
1856
+ async function listInstalled(params) {
1074
1857
  console.log(brand(`
1075
- Installed skills (${scope} scope):
1858
+ Installed items (${params.scope} scope):
1076
1859
  `));
1860
+ const groups = await collectInstalledItems(params);
1077
1861
  let total = 0;
1078
- for (const agent of ALL_AGENTS) {
1079
- const installed = await getInstalledSkills(
1080
- agent,
1081
- scope
1082
- );
1083
- if (installed.length > 0) {
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 skills installed"));
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 optionError = validateScope(options.scope) || validateType(options.type);
1169
- if (optionError) {
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
- var initCommand = new Command4("init").description("Initialize coding agent configuration directories").option(
1226
- "-a, --agents <agents>",
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
- console.log("");
1277
- console.log(
1278
- brand("Done! Use 'npx @danieldeusing/seedr add <skill>' to install skills.")
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.86",
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",