@mhmdreza-rafiei/agentry 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { animateLogo, badge, delay, printHelp, sky, theme } from "./_chunks/theme2.mjs";
3
- import { execFileSync, execSync } from "node:child_process";
3
+ import { execFileSync } from "node:child_process";
4
4
  import https from "node:https";
5
5
  import { createHash } from "node:crypto";
6
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
6
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
7
7
  import { homedir, tmpdir } from "node:os";
8
- import { dirname, join, resolve, sep } from "node:path";
8
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
9
9
  import { xdgCache, xdgConfig } from "xdg-basedir";
10
10
  import { parse, stringify } from "yaml";
11
11
  import { z } from "zod";
@@ -16,7 +16,7 @@ import { determineAgent } from "@vercel/detect-agent";
16
16
  import { stripVTControlCharacters } from "node:util";
17
17
  import { Writable } from "node:stream";
18
18
  var name = "@mhmdreza-rafiei/agentry";
19
- var version = "0.2.0";
19
+ var version = "0.3.0";
20
20
  const ARTIFACT_KINDS = [
21
21
  "skill",
22
22
  "rule",
@@ -34,15 +34,21 @@ function isLocalPath(source) {
34
34
  return false;
35
35
  }
36
36
  }
37
+ const SAFE_REF$1 = /^[a-zA-Z0-9][\w./-]*$/;
37
38
  function parseGithubTreeUrl(url) {
38
39
  const m = url.match(/^https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)\/tree\/([^/]+)\/(.+)$/);
39
40
  if (!m) return null;
41
+ const ref = m[2];
42
+ if (!SAFE_REF$1.test(ref)) throw new Error(`Invalid git ref in source URL: "${ref}"`);
40
43
  return {
41
44
  ownerRepo: m[1],
42
- ref: m[2],
45
+ ref,
43
46
  subpath: m[3].replace(/\/+$/, "")
44
47
  };
45
48
  }
49
+ function redactUrl(url) {
50
+ return url.replace(/:\/\/([^/@]+)@/g, "://***@");
51
+ }
46
52
  function parseSource(source) {
47
53
  if (!source || typeof source !== "string") throw new Error("Empty source.");
48
54
  const raw = source;
@@ -140,7 +146,9 @@ function saveToCache(parsed, clonedRoot) {
140
146
  cpSync(clonedRoot, dest, { recursive: true });
141
147
  } catch {}
142
148
  }
149
+ const SAFE_REF = /^[a-zA-Z0-9][\w./-]*$/;
143
150
  function cloneTo(url, dest, ref) {
151
+ if (ref && !SAFE_REF.test(ref)) throw new Error(`Invalid git ref: "${ref}"`);
144
152
  execFileSync("git", [
145
153
  "clone",
146
154
  "--quiet",
@@ -158,28 +166,39 @@ function cloneTo(url, dest, ref) {
158
166
  "pipe"
159
167
  ] });
160
168
  }
169
+ function assertInsideRoot(root, candidate) {
170
+ const base = resolve(root);
171
+ const resolved = resolve(candidate);
172
+ const prefix = base.endsWith(sep) ? base : base + sep;
173
+ const a = resolved.toLowerCase();
174
+ const b = base.toLowerCase();
175
+ const p = prefix.toLowerCase();
176
+ if (a !== b && !a.startsWith(p)) throw new Error(`Path escapes source root: "${candidate}"`);
177
+ return resolved;
178
+ }
161
179
  function withSubpath(root, subpath, cleanupRoot) {
180
+ const cleanup = () => rmSync(cleanupRoot, {
181
+ recursive: true,
182
+ force: true
183
+ });
162
184
  if (!subpath) return {
163
185
  root,
164
- cleanup: () => rmSync(cleanupRoot, {
165
- recursive: true,
166
- force: true
167
- })
186
+ cleanup
168
187
  };
169
- const sub = join(root, subpath);
188
+ let sub;
189
+ try {
190
+ sub = assertInsideRoot(root, join(root, subpath));
191
+ } catch (e) {
192
+ cleanup();
193
+ throw e;
194
+ }
170
195
  if (!existsSync(sub)) {
171
- rmSync(cleanupRoot, {
172
- recursive: true,
173
- force: true
174
- });
196
+ cleanup();
175
197
  throw new Error(`Subpath "${subpath}" not found.`);
176
198
  }
177
199
  return {
178
200
  root: sub,
179
- cleanup: () => rmSync(cleanupRoot, {
180
- recursive: true,
181
- force: true
182
- })
201
+ cleanup
183
202
  };
184
203
  }
185
204
  async function resolveSource(source, opts = {}) {
@@ -193,6 +212,7 @@ async function resolveSource(source, opts = {}) {
193
212
  }
194
213
  const url = parsed.url;
195
214
  if (!url) throw new Error(`Cannot resolve source "${parsed.raw}".`);
215
+ const safeUrl = redactUrl(url);
196
216
  const tmp = mkdtempSync(join(tmpdir(), "agentry-src-"));
197
217
  try {
198
218
  cloneTo(url, tmp, parsed.ref);
@@ -206,7 +226,7 @@ async function resolveSource(source, opts = {}) {
206
226
  const cacheOk = existsSync(cached) && existsSync(join(cached, ".git"));
207
227
  if (cacheOk && opts.confirmCache) {
208
228
  if (await opts.confirmCache({
209
- url,
229
+ url: safeUrl,
210
230
  cacheDir: cached,
211
231
  error: detail
212
232
  })) {
@@ -218,12 +238,105 @@ async function resolveSource(source, opts = {}) {
218
238
  };
219
239
  }
220
240
  }
221
- throw new Error(`git clone failed for ${url}\n${detail}${cacheOk ? "\n(A local cache exists — re-run interactively to load it, or pass -y to auto-use cache.)" : ""}`);
241
+ throw new Error(`git clone failed for ${safeUrl}\n${detail}${cacheOk ? "\n(A local cache exists — re-run interactively to load it, or pass -y to auto-use cache.)" : ""}`);
222
242
  }
223
243
  saveToCache(parsed, tmp);
224
244
  return withSubpath(tmp, parsed.subpath, tmp);
225
245
  }
246
+ const DEFAULT_IGNORES = [
247
+ ".git",
248
+ ".github",
249
+ "node_modules",
250
+ ".agents",
251
+ ".agentry",
252
+ ".cursor",
253
+ ".claude",
254
+ ".vscode",
255
+ "dist",
256
+ "build",
257
+ ".DS_Store"
258
+ ];
259
+ function parseIgnorePattern(pattern) {
260
+ const trimmed = pattern.trim();
261
+ if (!trimmed || trimmed.startsWith("#")) return null;
262
+ const isDirOnly = trimmed.endsWith("/");
263
+ let p = isDirOnly ? trimmed.slice(0, -1) : trimmed;
264
+ const isRootAnchored = p.startsWith("/");
265
+ if (isRootAnchored) p = p.slice(1);
266
+ let regexStr = "";
267
+ for (let i = 0; i < p.length; i++) {
268
+ const char = p[i];
269
+ if (char === "*") if (p[i + 1] === "*") {
270
+ regexStr += ".*";
271
+ i++;
272
+ if (p[i + 1] === "/") i++;
273
+ } else regexStr += "[^/]*";
274
+ else if (char === "?") regexStr += "[^/]";
275
+ else if ([
276
+ ".",
277
+ "+",
278
+ "^",
279
+ "$",
280
+ "{",
281
+ "}",
282
+ "(",
283
+ ")",
284
+ "|",
285
+ "[",
286
+ "]",
287
+ "\\"
288
+ ].includes(char)) regexStr += "\\" + char;
289
+ else regexStr += char;
290
+ }
291
+ let regex;
292
+ if (isRootAnchored) regex = new RegExp(`^${regexStr}(/.*)?$`, "i");
293
+ else if (p.includes("/")) regex = new RegExp(`(^|/)${regexStr}(/.*)?$`, "i");
294
+ else regex = new RegExp(`(^|/)${regexStr}($|/)`, "i");
295
+ return {
296
+ raw: trimmed,
297
+ isDirOnly,
298
+ regex
299
+ };
300
+ }
301
+ function parseIgnoreFile(content) {
302
+ return content.split(/\r?\n/).map((line) => parseIgnorePattern(line)).filter((r) => r !== null);
303
+ }
304
+ function loadIgnoreRules(root) {
305
+ const candidates = [".agentryignore", ".agentry.ignore"];
306
+ const rules = [];
307
+ for (const name of candidates) {
308
+ const file = join(root, name);
309
+ if (existsSync(file)) try {
310
+ const text = readFileSync(file, "utf8");
311
+ rules.push(...parseIgnoreFile(text));
312
+ } catch {}
313
+ }
314
+ return rules;
315
+ }
316
+ function isDefaultIgnored(relPath) {
317
+ return relPath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").some((s) => DEFAULT_IGNORES.includes(s));
318
+ }
319
+ function createIgnoreMatcher(root) {
320
+ const fileRules = loadIgnoreRules(root);
321
+ return (relPath, isDirectory = false) => {
322
+ if (!relPath || relPath === "." || relPath === "./") return false;
323
+ const normalized = relPath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
324
+ if (!normalized) return false;
325
+ if (isDefaultIgnored(normalized)) return true;
326
+ for (const rule of fileRules) {
327
+ if (rule.isDirOnly && !isDirectory) {
328
+ if (rule.regex.test(normalized + "/")) return true;
329
+ continue;
330
+ }
331
+ if (rule.regex.test(normalized)) return true;
332
+ }
333
+ return false;
334
+ };
335
+ }
226
336
  const SKILL_MARKER = "SKILL.md";
337
+ const AGENT_MARKER = "AGENT.md";
338
+ const RULE_MARKER = "RULE.md";
339
+ const CURSORRULES_MARKER = ".cursorrules";
227
340
  const FILE_EXT = ".mdc";
228
341
  const PROFILE_EXTS = [".yaml", ".yml"];
229
342
  const KIND_DIRS = {
@@ -253,6 +366,12 @@ function firstHeading(md) {
253
366
  const m = md.match(/^#\s+(.+)$/m);
254
367
  return m ? m[1].trim() : "";
255
368
  }
369
+ function frontmatterName(md) {
370
+ const fm = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
371
+ if (!fm) return "";
372
+ const m = fm[1].match(/^name:\s*(.+)$/m);
373
+ return m ? m[1].trim().replace(/^['"]|['"]$/g, "") : "";
374
+ }
256
375
  function frontmatterDescription(md) {
257
376
  const fm = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
258
377
  if (!fm) return "";
@@ -300,69 +419,178 @@ function makeArtifact(kind, category, name, srcPath, isFile) {
300
419
  description: isFile ? readDescription(srcPath) : folderDescription(srcPath)
301
420
  };
302
421
  }
303
- function collectSkills(base, out) {
422
+ function rel(root, fullPath) {
423
+ return relative(root, fullPath).replace(/\\/g, "/");
424
+ }
425
+ function collectSkills(root, base, out, matcher) {
304
426
  for (const child of entries(base)) {
305
427
  if (!child.isDirectory()) continue;
306
428
  const childDir = join(base, child.name);
429
+ if (matcher(rel(root, childDir), true)) continue;
307
430
  if (existsSync(join(childDir, SKILL_MARKER))) out.push(makeArtifact("skill", null, child.name, childDir, false));
308
431
  else for (const item of entries(childDir)) {
309
432
  if (!item.isDirectory()) continue;
310
433
  const itemDir = join(childDir, item.name);
434
+ if (matcher(rel(root, itemDir), true)) continue;
311
435
  if (existsSync(join(itemDir, SKILL_MARKER))) out.push(makeArtifact("skill", child.name, item.name, itemDir, false));
312
436
  }
313
437
  }
314
438
  }
315
- function collectMdc(kind, base, out) {
316
- for (const child of entries(base)) if (child.isFile() && child.name.endsWith(FILE_EXT)) out.push(makeArtifact(kind, null, child.name.slice(0, -4), join(base, child.name), true));
317
- else if (child.isDirectory()) {
318
- const subDir = join(base, child.name);
319
- for (const item of entries(subDir)) if (item.isFile() && item.name.endsWith(FILE_EXT)) out.push(makeArtifact(kind, child.name, item.name.slice(0, -4), join(subDir, item.name), true));
439
+ function collectMdc(root, kind, base, out, matcher) {
440
+ for (const child of entries(base)) {
441
+ const childPath = join(base, child.name);
442
+ if (child.isFile() && child.name.endsWith(FILE_EXT)) {
443
+ if (matcher(rel(root, childPath), false)) continue;
444
+ out.push(makeArtifact(kind, null, child.name.slice(0, -4), childPath, true));
445
+ } else if (child.isDirectory()) {
446
+ if (matcher(rel(root, childPath), true)) continue;
447
+ for (const item of entries(childPath)) {
448
+ const itemPath = join(childPath, item.name);
449
+ if (item.isFile() && item.name.endsWith(FILE_EXT)) {
450
+ if (matcher(rel(root, itemPath), false)) continue;
451
+ out.push(makeArtifact(kind, child.name, item.name.slice(0, -4), itemPath, true));
452
+ }
453
+ }
454
+ }
320
455
  }
321
456
  }
322
- function collectProfiles(base, out) {
323
- for (const child of entries(base)) if (child.isFile() && PROFILE_EXTS.some((e) => child.name.endsWith(e))) {
324
- const stem = child.name.replace(/\.(yaml|yml)$/, "");
325
- out.push(makeArtifact("profile", null, stem, join(base, child.name), true));
326
- } else if (child.isDirectory()) {
327
- const subDir = join(base, child.name);
328
- for (const item of entries(subDir)) if (item.isFile() && PROFILE_EXTS.some((e) => item.name.endsWith(e))) {
329
- const stem = item.name.replace(/\.(yaml|yml)$/, "");
330
- out.push(makeArtifact("profile", child.name, stem, join(subDir, item.name), true));
457
+ function collectProfiles(root, base, out, matcher) {
458
+ for (const child of entries(base)) {
459
+ const childPath = join(base, child.name);
460
+ if (child.isFile() && PROFILE_EXTS.some((e) => child.name.endsWith(e))) {
461
+ if (matcher(rel(root, childPath), false)) continue;
462
+ const stem = child.name.replace(/\.(yaml|yml)$/, "");
463
+ out.push(makeArtifact("profile", null, stem, childPath, true));
464
+ } else if (child.isDirectory()) {
465
+ if (matcher(rel(root, childPath), true)) continue;
466
+ for (const item of entries(childPath)) {
467
+ const itemPath = join(childPath, item.name);
468
+ if (item.isFile() && PROFILE_EXTS.some((e) => item.name.endsWith(e))) {
469
+ if (matcher(rel(root, itemPath), false)) continue;
470
+ const stem = item.name.replace(/\.(yaml|yml)$/, "");
471
+ out.push(makeArtifact("profile", child.name, stem, itemPath, true));
472
+ }
473
+ }
331
474
  }
332
475
  }
333
476
  }
334
- function collectScripts(base, out) {
477
+ function collectScripts(root, base, out, matcher) {
335
478
  for (const child of entries(base)) {
336
479
  if (!child.isDirectory()) continue;
337
480
  const childDir = join(base, child.name);
481
+ if (matcher(rel(root, childDir), true)) continue;
338
482
  if (entries(childDir).some((e) => e.isFile())) out.push(makeArtifact("script", null, child.name, childDir, false));
339
- else for (const item of entries(childDir)) if (item.isDirectory() && entries(join(childDir, item.name)).some((e) => e.isFile())) out.push(makeArtifact("script", child.name, item.name, join(childDir, item.name), false));
483
+ else for (const item of entries(childDir)) {
484
+ const itemDir = join(childDir, item.name);
485
+ if (!item.isDirectory() || matcher(rel(root, itemDir), true)) continue;
486
+ if (entries(itemDir).some((e) => e.isFile())) out.push(makeArtifact("script", child.name, item.name, join(childDir, item.name), false));
487
+ }
340
488
  }
341
489
  }
342
- function collectRootSkills(root, out) {
490
+ function collectRootSkills(root, out, matcher) {
491
+ const rootSkillFile = join(root, SKILL_MARKER);
492
+ if (existsSync(rootSkillFile) && !matcher(SKILL_MARKER, false)) {
493
+ let name = "";
494
+ try {
495
+ name = frontmatterName(readFileSync(rootSkillFile, "utf8"));
496
+ } catch {}
497
+ if (!name) name = basename(root) || "skill";
498
+ out.push(makeArtifact("skill", null, name, root, false));
499
+ }
343
500
  for (const child of entries(root)) {
344
501
  if (!child.isDirectory() || child.name.startsWith(".") || ROOT_SKIP.has(child.name)) continue;
345
502
  const childDir = join(root, child.name);
503
+ if (matcher(child.name, true)) continue;
346
504
  if (existsSync(join(childDir, SKILL_MARKER))) out.push(makeArtifact("skill", null, child.name, childDir, false));
347
- else for (const item of entries(childDir)) if (item.isDirectory() && existsSync(join(childDir, item.name, SKILL_MARKER))) out.push(makeArtifact("skill", child.name, item.name, join(childDir, item.name), false));
505
+ else for (const item of entries(childDir)) {
506
+ if (!item.isDirectory()) continue;
507
+ const itemDir = join(childDir, item.name);
508
+ if (matcher(rel(root, itemDir), true)) continue;
509
+ if (existsSync(join(itemDir, SKILL_MARKER))) out.push(makeArtifact("skill", child.name, item.name, itemDir, false));
510
+ }
348
511
  }
349
512
  }
350
- function collectRootMdc(kind, root, out) {
351
- for (const child of entries(root)) if (child.isFile() && child.name.endsWith(FILE_EXT) && !child.name.startsWith(".")) out.push(makeArtifact(kind, null, child.name.slice(0, -4), join(root, child.name), true));
352
- else if (child.isDirectory() && !child.name.startsWith(".") && !ROOT_SKIP.has(child.name)) {
353
- const subDir = join(root, child.name);
354
- for (const item of entries(subDir)) if (item.isFile() && item.name.endsWith(FILE_EXT)) out.push(makeArtifact(kind, child.name, item.name.slice(0, -4), join(subDir, item.name), true));
513
+ function collectRootMdc(kind, root, out, matcher) {
514
+ if (kind === "agent") {
515
+ const rootAgent = join(root, AGENT_MARKER);
516
+ if (existsSync(rootAgent) && !matcher(AGENT_MARKER, false)) {
517
+ let name = "";
518
+ try {
519
+ name = frontmatterName(readFileSync(rootAgent, "utf8"));
520
+ } catch {}
521
+ out.push(makeArtifact("agent", null, name || "agent", rootAgent, true));
522
+ }
523
+ }
524
+ if (kind === "rule") {
525
+ const rootRule = join(root, RULE_MARKER);
526
+ if (existsSync(rootRule) && !matcher(RULE_MARKER, false)) {
527
+ let name = "";
528
+ try {
529
+ name = frontmatterName(readFileSync(rootRule, "utf8"));
530
+ } catch {}
531
+ out.push(makeArtifact("rule", null, name || "rule", rootRule, true));
532
+ }
533
+ const cursorRules = join(root, CURSORRULES_MARKER);
534
+ if (existsSync(cursorRules) && !matcher(CURSORRULES_MARKER, false)) out.push(makeArtifact("rule", null, "cursorrules", cursorRules, true));
535
+ }
536
+ for (const child of entries(root)) {
537
+ const childPath = join(root, child.name);
538
+ if (child.isFile() && child.name.endsWith(FILE_EXT) && !child.name.startsWith(".")) {
539
+ if (matcher(child.name, false)) continue;
540
+ out.push(makeArtifact(kind, null, child.name.slice(0, -4), childPath, true));
541
+ } else if (child.isDirectory() && !child.name.startsWith(".") && !ROOT_SKIP.has(child.name)) {
542
+ if (matcher(child.name, true)) continue;
543
+ for (const item of entries(childPath)) {
544
+ const itemPath = join(childPath, item.name);
545
+ if (item.isFile() && item.name.endsWith(FILE_EXT)) {
546
+ if (matcher(rel(root, itemPath), false)) continue;
547
+ out.push(makeArtifact(kind, child.name, item.name.slice(0, -4), itemPath, true));
548
+ }
549
+ }
550
+ }
551
+ }
552
+ }
553
+ function collectRootProfiles(root, out, matcher) {
554
+ const localProfileDir = join(root, "profile");
555
+ if (existsSync(localProfileDir) && !matcher("profile", true)) collectProfiles(root, localProfileDir, out, matcher);
556
+ for (const child of entries(root)) {
557
+ if (!child.isFile()) continue;
558
+ if (matcher(child.name, false)) continue;
559
+ const lower = child.name.toLowerCase();
560
+ if (lower === "profile.yaml" || lower === "profile.yml" || lower.endsWith(".profile.yaml") || lower.endsWith(".profile.yml")) {
561
+ const stem = child.name.replace(/\.(profile\.)?(yaml|yml)$/i, "");
562
+ out.push(makeArtifact("profile", null, stem, join(root, child.name), true));
563
+ }
564
+ }
565
+ }
566
+ function collectRootScripts(root, out, matcher) {
567
+ const localScriptDir = join(root, "script");
568
+ if (existsSync(localScriptDir) && !matcher("script", true)) collectScripts(root, localScriptDir, out, matcher);
569
+ if (!existsSync(join(root, SKILL_MARKER))) {
570
+ for (const sf of [
571
+ "run.sh",
572
+ "script.sh",
573
+ "run.js",
574
+ "main.py"
575
+ ]) if (existsSync(join(root, sf)) && !matcher(sf, false)) {
576
+ const name = basename(root) || "script";
577
+ out.push(makeArtifact("script", null, name, root, false));
578
+ break;
579
+ }
355
580
  }
356
581
  }
357
582
  function listKind(root, kind) {
358
583
  const out = [];
584
+ const matcher = createIgnoreMatcher(root);
359
585
  const base = join(root, KIND_DIRS[kind]);
360
- if (existsSync(base)) if (kind === "skill") collectSkills(base, out);
361
- else if (kind === "profile") collectProfiles(base, out);
362
- else if (kind === "script") collectScripts(base, out);
363
- else collectMdc(kind, base, out);
364
- if (kind === "skill") collectRootSkills(root, out);
365
- else if (kind === "agent" || kind === "rule") collectRootMdc(kind, root, out);
586
+ if (existsSync(base) && !matcher(KIND_DIRS[kind], true)) if (kind === "skill") collectSkills(root, base, out, matcher);
587
+ else if (kind === "profile") collectProfiles(root, base, out, matcher);
588
+ else if (kind === "script") collectScripts(root, base, out, matcher);
589
+ else collectMdc(root, kind, base, out, matcher);
590
+ if (kind === "skill") collectRootSkills(root, out, matcher);
591
+ else if (kind === "agent" || kind === "rule") collectRootMdc(kind, root, out, matcher);
592
+ else if (kind === "profile") collectRootProfiles(root, out, matcher);
593
+ else if (kind === "script") collectRootScripts(root, out, matcher);
366
594
  const seen = /* @__PURE__ */ new Set();
367
595
  return out.filter((a) => seen.has(a.id) ? false : seen.add(a.id)).sort((a, b) => a.id.localeCompare(b.id));
368
596
  }
@@ -371,7 +599,8 @@ function listAll(root) {
371
599
  "skill",
372
600
  "rule",
373
601
  "agent",
374
- "profile"
602
+ "profile",
603
+ "script"
375
604
  ].flatMap((k) => listKind(root, k));
376
605
  }
377
606
  function select(root, kind, selector) {
@@ -382,6 +611,14 @@ function select(root, kind, selector) {
382
611
  if (byCategory.length) return byCategory;
383
612
  return items.filter((a) => a.name === selector);
384
613
  }
614
+ function selectAll(root, selector) {
615
+ const items = listAll(root);
616
+ if (!selector) return items;
617
+ if (selector.includes("/")) return items.filter((a) => a.id === selector);
618
+ const byCategory = items.filter((a) => a.category === selector);
619
+ if (byCategory.length) return byCategory;
620
+ return items.filter((a) => a.name === selector);
621
+ }
385
622
  const home$1 = homedir();
386
623
  const configHome = xdgConfig ?? join(home$1, ".config");
387
624
  const codexHome = process.env.CODEX_HOME?.trim() || join(home$1, ".codex");
@@ -563,6 +800,48 @@ function getNonUniversalAgents() {
563
800
  return listAgents().filter((a) => a.skillsDir !== ".agents/skills");
564
801
  }
565
802
  const home = homedir();
803
+ function copyTreeDereferenced(src, dest, seen = /* @__PURE__ */ new Set(), filter, baseSrc = src) {
804
+ const st = lstatSync(src);
805
+ if (st.isSymbolicLink()) {
806
+ let real;
807
+ try {
808
+ real = realpathSync(src);
809
+ } catch {
810
+ return;
811
+ }
812
+ if (seen.has(real)) return;
813
+ seen.add(real);
814
+ copyTreeDereferenced(real, dest, seen, filter, baseSrc);
815
+ return;
816
+ }
817
+ if (st.isDirectory()) {
818
+ let real;
819
+ try {
820
+ real = realpathSync(src);
821
+ } catch {
822
+ real = resolve(src);
823
+ }
824
+ if (seen.has(real)) return;
825
+ seen.add(real);
826
+ mkdirSync(dest, { recursive: true });
827
+ for (const name of readdirSync(src)) {
828
+ const childSrc = join(src, name);
829
+ const relPath = relative(baseSrc, childSrc).replace(/\\/g, "/");
830
+ let isDir = false;
831
+ try {
832
+ isDir = statSync(childSrc).isDirectory();
833
+ } catch {}
834
+ if (DEFAULT_IGNORES.includes(name) || name === ".agentryignore" || name === ".agentry.ignore") continue;
835
+ if (filter && filter(relPath, isDir)) continue;
836
+ copyTreeDereferenced(childSrc, join(dest, name), seen, filter, baseSrc);
837
+ }
838
+ return;
839
+ }
840
+ if (st.isFile()) {
841
+ mkdirSync(dirname(dest), { recursive: true });
842
+ copyFileSync(src, dest);
843
+ }
844
+ }
566
845
  function lockBase(opts) {
567
846
  return opts.scope === "global" ? home : opts.dir || process.cwd();
568
847
  }
@@ -683,6 +962,7 @@ function installOne(artifact, opts, agentList, source) {
683
962
  });
684
963
  return dests;
685
964
  }
965
+ const ignoreFilter = artifact.isFile ? void 0 : createIgnoreMatcher(artifact.dir);
686
966
  if (artifact.kind === "skill") {
687
967
  const canonical = join(base, ".agents", "skills", artifact.id);
688
968
  rmSync(canonical, {
@@ -690,7 +970,7 @@ function installOne(artifact, opts, agentList, source) {
690
970
  force: true
691
971
  });
692
972
  mkdirSync(join(canonical, ".."), { recursive: true });
693
- cpSync(artifact.dir, canonical, { recursive: true });
973
+ copyTreeDereferenced(artifact.dir, canonical, /* @__PURE__ */ new Set(), ignoreFilter);
694
974
  for (const a of list) if (a.skillsDir === ".agents/skills") dests.push({
695
975
  agent: a.name,
696
976
  dir: canonical
@@ -703,7 +983,7 @@ function installOne(artifact, opts, agentList, source) {
703
983
  });
704
984
  mkdirSync(join(dir, ".."), { recursive: true });
705
985
  if (opts.copy) {
706
- cpSync(canonical, dir, { recursive: true });
986
+ copyTreeDereferenced(canonical, dir);
707
987
  dests.push({
708
988
  agent: a.name,
709
989
  dir
@@ -716,7 +996,7 @@ function installOne(artifact, opts, agentList, source) {
716
996
  symlinked: true
717
997
  });
718
998
  } catch {
719
- cpSync(canonical, dir, { recursive: true });
999
+ copyTreeDereferenced(canonical, dir);
720
1000
  dests.push({
721
1001
  agent: a.name,
722
1002
  dir
@@ -730,7 +1010,7 @@ function installOne(artifact, opts, agentList, source) {
730
1010
  force: true
731
1011
  });
732
1012
  mkdirSync(join(dir, ".."), { recursive: true });
733
- cpSync(artifact.dir, dir, { recursive: true });
1013
+ copyTreeDereferenced(artifact.dir, dir, /* @__PURE__ */ new Set(), ignoreFilter);
734
1014
  dests.push({
735
1015
  agent: a.name,
736
1016
  dir
@@ -1417,6 +1697,98 @@ function agentsPrompt(action) {
1417
1697
  if (action === "update") return "Which agents do you want to update for?";
1418
1698
  return "Which agents do you want to install to?";
1419
1699
  }
1700
+ function scopePrompt(action) {
1701
+ if (action === "remove") return "Removal scope";
1702
+ if (action === "update") return "Update scope";
1703
+ return "Installation scope";
1704
+ }
1705
+ async function selectInstallScope(message, paths) {
1706
+ if (isQuiet()) return "project";
1707
+ return new Promise((resolve) => {
1708
+ if (!process.stdin.isTTY) {
1709
+ resolve("project");
1710
+ return;
1711
+ }
1712
+ const rl = readline.createInterface({
1713
+ input: process.stdin,
1714
+ output: process.stdout
1715
+ });
1716
+ process.stdin.setRawMode(true);
1717
+ readline.emitKeypressEvents(process.stdin, rl);
1718
+ let value = "project";
1719
+ let lastH = 0;
1720
+ const joinRel = (base, rel) => {
1721
+ const sep = base.includes("\\") ? "\\" : "/";
1722
+ return `${base.replace(/[\\/]+$/, "")}${sep}${rel.replace(/\//g, sep)}`;
1723
+ };
1724
+ const optionBlock = (key, selected, done) => {
1725
+ const active = done ? done === key : selected;
1726
+ const bullet = active ? sky(c.bold("●")) : c.dim("○");
1727
+ const title = key === "project" ? "Project" : "Global";
1728
+ const hint = key === "project" ? "Install in the current directory (committed with your project)" : "Install in your home directory (available across all projects)";
1729
+ const path = key === "project" ? paths.projectDir : paths.homeDir;
1730
+ const lock = key === "project" ? joinRel(paths.projectDir, ".agentry/lock.json") : joinRel(paths.homeDir, ".agentry/lock.json");
1731
+ const lines = [];
1732
+ if (done === "cancel") lines.push(`${c.dim("│")} ${c.strikethrough(c.dim(title))}`);
1733
+ else {
1734
+ lines.push(`${c.dim("│")} ${bullet} ${active ? c.bold(title) : title}`);
1735
+ lines.push(`${c.dim("│")} ${c.dim(hint)}`);
1736
+ lines.push(`${c.dim("│")} ${sky("→")} ${c.dim(path)}`);
1737
+ lines.push(`${c.dim("│")} ${c.dim(`lock ${lock}`)}`);
1738
+ }
1739
+ return lines;
1740
+ };
1741
+ const paint = (done) => {
1742
+ const lines = [];
1743
+ if (done === "cancel") {
1744
+ lines.push(`${sky("■")} ${c.bold(message)}`);
1745
+ lines.push(`${c.dim("│")} ${c.strikethrough(c.dim("Cancelled"))}`);
1746
+ } else if (done === "project" || done === "global") {
1747
+ lines.push(`${sky("◇")} ${c.bold(message)}`);
1748
+ lines.push(...optionBlock(done, true, done).slice(0, 2));
1749
+ lines.push(`${c.dim("│")} ${sky("→")} ${c.dim(done === "project" ? paths.projectDir : paths.homeDir)}`);
1750
+ } else {
1751
+ lines.push(`${sky("◆")} ${c.bold(message)}`);
1752
+ lines.push(`${c.dim("│")}`);
1753
+ lines.push(...optionBlock("project", value === "project"));
1754
+ lines.push(`${c.dim("│")}`);
1755
+ lines.push(...optionBlock("global", value === "global"));
1756
+ lines.push(`${c.dim("└")} ${c.dim("↑/↓ select · enter confirm")}`);
1757
+ }
1758
+ if (lastH > 0) for (let i = 0; i < lastH; i++) process.stdout.write("\x1B[1A\x1B[2K");
1759
+ process.stdout.write(lines.join("\n") + "\n");
1760
+ lastH = lines.length;
1761
+ };
1762
+ const cleanup = () => {
1763
+ process.stdin.removeListener("keypress", onKey);
1764
+ process.stdin.setRawMode(false);
1765
+ rl.close();
1766
+ };
1767
+ const onKey = (_s, key) => {
1768
+ if (!key) return;
1769
+ if (key.name === "up" || key.name === "down" || key.name === "tab") {
1770
+ value = value === "project" ? "global" : "project";
1771
+ paint();
1772
+ return;
1773
+ }
1774
+ if (key.name === "return") {
1775
+ paint(value);
1776
+ cleanup();
1777
+ blank();
1778
+ resolve(value);
1779
+ return;
1780
+ }
1781
+ if (key.name === "escape" || key.ctrl && key.name === "c") {
1782
+ paint("cancel");
1783
+ cleanup();
1784
+ resolve(null);
1785
+ }
1786
+ };
1787
+ process.stdin.on("keypress", onKey);
1788
+ blank();
1789
+ paint();
1790
+ });
1791
+ }
1420
1792
  function isQuiet() {
1421
1793
  return isCI() || !process.stdout.isTTY;
1422
1794
  }
@@ -1784,7 +2156,7 @@ async function cmdAdd(kind, source, selector, opts) {
1784
2156
  await delayQuiet(280);
1785
2157
  const { root, cleanup } = await resolveSourceWithUi(source, opts);
1786
2158
  try {
1787
- const artifacts = await spinner("Discovering…", () => kind === "all" ? listAll(root) : select(root, kind, selector), (a) => {
2159
+ const artifacts = await spinner("Discovering…", () => kind === "all" ? selector ? selectAll(root, selector) : listAll(root) : select(root, kind, selector), (a) => {
1788
2160
  const noun = kind === "all" ? "artifact" : kind;
1789
2161
  return `Found ${a.length} ${noun}${a.length === 1 ? "" : "s"}`;
1790
2162
  }, 620);
@@ -1799,22 +2171,28 @@ async function cmdAdd(kind, source, selector, opts) {
1799
2171
  return;
1800
2172
  }
1801
2173
  let chosen = artifacts;
1802
- if (!selector && kind !== "all" && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
1803
- await sectionTransition("source discovery", `select ${kind}s to install`);
1804
- const picked = await selectArtifacts(`Select ${kind}s to install`, artifacts);
2174
+ if (!selector && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
2175
+ const noun = kind === "all" ? "artifacts" : `${kind}s`;
2176
+ await sectionTransition("source discovery", `select ${noun} to install`);
2177
+ const picked = await selectArtifacts(`Select ${noun} to install`, artifacts);
1805
2178
  if (isCancelLike(picked)) {
1806
2179
  outro("Cancelled.");
1807
2180
  return;
1808
2181
  }
1809
2182
  chosen = picked;
1810
- await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} selected`, "choose target agents");
1811
- } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} ready`, "choose target agents");
1812
- else {
2183
+ await sectionTransition(`${chosen.length} ${chosen.length === 1 ? "artifact" : "artifacts"} selected`, "choose target agents");
2184
+ } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) {
2185
+ const noun = kind === "all" ? chosen.length === 1 ? "artifact" : "artifacts" : `${kind}${chosen.length === 1 ? "" : "s"}`;
2186
+ await sectionTransition(`${chosen.length} ${noun} ready`, "choose target agents");
2187
+ } else {
1813
2188
  await delayQuiet(200);
1814
2189
  blank();
1815
2190
  }
1816
2191
  const agentList = await resolveAgentList(opts, "install");
1817
- if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${agentList.length} agent${agentList.length === 1 ? "" : "s"} selected`, "install summary");
2192
+ const askScope = !opts.scopeExplicit && !opts.yes && !isQuiet();
2193
+ if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${agentList.length} agent${agentList.length === 1 ? "" : "s"} selected`, askScope ? "installation scope" : "install summary");
2194
+ if (!await ensureInstallScope(opts, "install")) return;
2195
+ if (askScope) await sectionTransition("scope set", "install summary");
1818
2196
  installAll(chosen, opts, agentList, source);
1819
2197
  } finally {
1820
2198
  cleanup();
@@ -1833,6 +2211,7 @@ async function cmdAddProfile(name, source, opts) {
1833
2211
  const repo = source || (profile.artifacts.skills[0]?.source ?? profile.artifacts.rules[0]?.source);
1834
2212
  if (!repo) throw new Error(`No source given and ${file} lists none. Try: agentry add profile ${name} author/repo`);
1835
2213
  const agentList = await resolveAgentList(opts, "install");
2214
+ if (!await ensureInstallScope(opts, "install")) return;
1836
2215
  const { root, cleanup } = await resolveSourceWithUi(repo, opts);
1837
2216
  try {
1838
2217
  const artifacts = [];
@@ -1878,6 +2257,23 @@ async function resolveAgentList(opts, action = "install") {
1878
2257
  if (isCancelLike(selected)) throw new Error("Cancelled.");
1879
2258
  return selected;
1880
2259
  }
2260
+ async function ensureInstallScope(opts, action) {
2261
+ if (opts.scopeExplicit) return true;
2262
+ if (opts.yes || isQuiet()) return true;
2263
+ const { homedir } = await import("node:os");
2264
+ if (!isQuiet()) await sectionTransition(`${action === "install" ? "agents" : action} ready`, scopePrompt(action).toLowerCase());
2265
+ const picked = await selectInstallScope(scopePrompt(action), {
2266
+ projectDir: opts.dir || process.cwd(),
2267
+ homeDir: homedir()
2268
+ });
2269
+ if (picked == null || isCancelLike(picked)) {
2270
+ outro("Cancelled.");
2271
+ return false;
2272
+ }
2273
+ opts.scope = picked;
2274
+ opts.scopeExplicit = true;
2275
+ return true;
2276
+ }
1881
2277
  function installAll(artifacts, opts, agentList, source) {
1882
2278
  if (!artifacts.length) {
1883
2279
  warn("Nothing matched - nothing installed.");
@@ -1904,8 +2300,22 @@ function resolveSourceOrSelector(arg1, arg2, opts) {
1904
2300
  if (!arg1) return {};
1905
2301
  if (isExplicitSource(arg1)) return { source: arg1 };
1906
2302
  if (isOwnerRepoShape(arg1)) {
1907
- const lock = readLock(toInstallOpts(opts));
1908
- if (Object.values(lock.items).some((e) => e.source && sourcesEqual(e.source, arg1))) return { source: arg1 };
2303
+ const bases = [{
2304
+ scope: "project",
2305
+ dir: opts.dir,
2306
+ agents: opts.agents,
2307
+ copy: opts.copy,
2308
+ dryRun: opts.dryRun
2309
+ }, {
2310
+ scope: "global",
2311
+ agents: opts.agents,
2312
+ copy: opts.copy,
2313
+ dryRun: opts.dryRun
2314
+ }];
2315
+ for (const base of bases) {
2316
+ const lock = readLock(base);
2317
+ if (Object.values(lock.items).some((e) => e.source && sourcesEqual(e.source, arg1))) return { source: arg1 };
2318
+ }
1909
2319
  }
1910
2320
  return { selector: arg1 };
1911
2321
  }
@@ -1918,6 +2328,7 @@ async function cmdRemove(kind, arg1, arg2, opts) {
1918
2328
  }, 420);
1919
2329
  if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition("prepare remove", "choose agents to remove from");
1920
2330
  const agentList = await resolveAgentList(opts, "remove");
2331
+ if (!await ensureInstallScope(opts, "remove")) return;
1921
2332
  const installOpts = toInstallOpts(opts);
1922
2333
  const scope = [
1923
2334
  kind,
@@ -1928,7 +2339,7 @@ async function cmdRemove(kind, arg1, arg2, opts) {
1928
2339
  let count = 0;
1929
2340
  let paths = 0;
1930
2341
  for (const t of ARTIFACT_KINDS) {
1931
- const r = removeSelection(t, void 0, installOpts, agentList, source);
2342
+ const r = removeSelection(t, selector, installOpts, agentList, source);
1932
2343
  count += r.removedKeys.length;
1933
2344
  paths += r.removedPaths.length;
1934
2345
  }
@@ -1959,7 +2370,7 @@ async function cmdList(source, kind, selector, opts) {
1959
2370
  try {
1960
2371
  printArtifactList(await spinner("Discovering…", () => {
1961
2372
  if (kind) return select(root, kind, selector);
1962
- return listAll(root);
2373
+ return selector ? selectAll(root, selector) : listAll(root);
1963
2374
  }, (a) => `Found ${a.length} artifact${a.length === 1 ? "" : "s"}`, 520), {
1964
2375
  title: "Available in source",
1965
2376
  subtitle: source
@@ -2051,7 +2462,6 @@ function printArtifactList(artifacts, meta) {
2051
2462
  async function cmdUpdateAssets(kind, arg1, arg2, opts) {
2052
2463
  intro();
2053
2464
  const { source, selector } = resolveSourceOrSelector(arg1, arg2, opts);
2054
- const installOpts = toInstallOpts(opts);
2055
2465
  if (source) {
2056
2466
  await spinner("Parsing source…", () => parseSource(source), (p) => {
2057
2467
  const where = p.url || p.local || source;
@@ -2059,23 +2469,28 @@ async function cmdUpdateAssets(kind, arg1, arg2, opts) {
2059
2469
  }, 520);
2060
2470
  const { root, cleanup } = await resolveSourceWithUi(source, opts);
2061
2471
  try {
2062
- const artifacts = await spinner("Discovering…", () => kind === "all" ? listAll(root) : select(root, kind, selector), (a) => `Found ${a.length} to update`, 620);
2472
+ const artifacts = await spinner("Discovering…", () => kind === "all" ? selector ? selectAll(root, selector) : listAll(root) : select(root, kind, selector), (a) => `Found ${a.length} to update`, 620);
2063
2473
  if (!artifacts.length) {
2064
2474
  warn(`Nothing found in ${source}.`);
2065
2475
  return;
2066
2476
  }
2067
2477
  let chosen = artifacts;
2068
- if (!selector && kind !== "all" && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
2069
- await sectionTransition("source discovery", `select ${kind}s to update`);
2070
- const picked = await selectArtifacts(`Select ${kind}s to update`, artifacts);
2478
+ if (!selector && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
2479
+ const noun = kind === "all" ? "artifacts" : `${kind}s`;
2480
+ await sectionTransition("source discovery", `select ${noun} to update`);
2481
+ const picked = await selectArtifacts(`Select ${noun} to update`, artifacts);
2071
2482
  if (isCancelLike(picked)) {
2072
2483
  outro("Cancelled.");
2073
2484
  return;
2074
2485
  }
2075
2486
  chosen = picked;
2076
- await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} selected`, "choose target agents");
2077
- } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} ready`, "choose target agents");
2487
+ await sectionTransition(`${chosen.length} ${chosen.length === 1 ? "artifact" : "artifacts"} selected`, "choose target agents");
2488
+ } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) {
2489
+ const noun = kind === "all" ? chosen.length === 1 ? "artifact" : "artifacts" : `${kind}${chosen.length === 1 ? "" : "s"}`;
2490
+ await sectionTransition(`${chosen.length} ${noun} ready`, "choose target agents");
2491
+ }
2078
2492
  const agentList = await resolveAgentList(opts, "update");
2493
+ if (!await ensureInstallScope(opts, "update")) return;
2079
2494
  if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${agentList.length} agent${agentList.length === 1 ? "" : "s"} selected`, "update summary");
2080
2495
  installAll(chosen, opts, agentList, source);
2081
2496
  } finally {
@@ -2083,7 +2498,8 @@ async function cmdUpdateAssets(kind, arg1, arg2, opts) {
2083
2498
  }
2084
2499
  return;
2085
2500
  }
2086
- let installed = listInstalled(installOpts).filter((it) => kind === "all" || it.id.startsWith(kind + "/"));
2501
+ if (!await ensureInstallScope(opts, "update")) return;
2502
+ let installed = listInstalled(toInstallOpts(opts)).filter((it) => kind === "all" || it.id.startsWith(kind + "/"));
2087
2503
  if (selector) {
2088
2504
  const rel = `${kind}/${selector}`;
2089
2505
  installed = installed.filter((it) => it.id === rel || it.id.startsWith(rel + "/") || it.id.endsWith("/" + selector));
@@ -2308,6 +2724,7 @@ function takeOptValue(argv, i) {
2308
2724
  function parseArgs(argv) {
2309
2725
  const opts = {
2310
2726
  scope: "project",
2727
+ scopeExplicit: false,
2311
2728
  agents: [],
2312
2729
  copy: false,
2313
2730
  dryRun: false,
@@ -2321,15 +2738,19 @@ function parseArgs(argv) {
2321
2738
  const a = argv[i];
2322
2739
  if (a === "-g" || a === "--global") {
2323
2740
  opts.scope = "global";
2741
+ opts.scopeExplicit = true;
2324
2742
  flags.global = true;
2325
- } else if (a === "--project") {
2743
+ } else if (a === "-p" || a === "--project") {
2326
2744
  opts.scope = "project";
2745
+ opts.scopeExplicit = true;
2327
2746
  flags.project = true;
2328
2747
  } else if (a === "-y" || a === "--yes") {
2329
2748
  opts.yes = true;
2330
2749
  flags.yes = true;
2331
- } else if (a === "--dir") {
2750
+ } else if (a === "-d" || a === "--dir") {
2332
2751
  opts.dir = argv[++i];
2752
+ opts.scope = "project";
2753
+ opts.scopeExplicit = true;
2333
2754
  flags.dir = opts.dir;
2334
2755
  } else if (a === "-a" || a === "--agent") {
2335
2756
  opts.agents.push(argv[++i]);
@@ -2420,7 +2841,11 @@ async function runUpdateSelf() {
2420
2841
  function runNpmInstall(spec) {
2421
2842
  spinner(`Updating agentry CLI`, () => {
2422
2843
  try {
2423
- execSync(`npm install -g ${spec}`, { stdio: "inherit" });
2844
+ execFileSync("npm", [
2845
+ "install",
2846
+ "-g",
2847
+ spec
2848
+ ], { stdio: "inherit" });
2424
2849
  } catch {
2425
2850
  err("Update failed. Once published, run: npm install -g " + spec);
2426
2851
  process.exit(1);
@@ -2430,7 +2855,11 @@ function runNpmInstall(spec) {
2430
2855
  function runUninstall() {
2431
2856
  spinner("Uninstalling agentry CLI", () => {
2432
2857
  try {
2433
- execSync(`npm uninstall -g ${name}`, { stdio: "inherit" });
2858
+ execFileSync("npm", [
2859
+ "uninstall",
2860
+ "-g",
2861
+ name
2862
+ ], { stdio: "inherit" });
2434
2863
  } catch {
2435
2864
  err("Uninstall failed. Try: npm uninstall -g " + name);
2436
2865
  process.exit(1);
@@ -2489,21 +2918,33 @@ async function main() {
2489
2918
  return;
2490
2919
  }
2491
2920
  if (action === "add") {
2492
- const kind = positional[1] ? normalizeKind(positional[1]) : void 0;
2493
- if (!kind) throw new Error("Missing kind. e.g. agentry add skills author/repo");
2494
- if (kind === "profile") {
2921
+ const a1 = positional[1];
2922
+ if (!a1) throw new Error("Missing source or kind. e.g. agentry add author/repo or agentry add skills author/repo");
2923
+ const k1 = normalizeKind(a1);
2924
+ if (k1 === "profile") {
2495
2925
  await cmdAddProfile(positional[2], positional[3], opts);
2496
2926
  return;
2497
2927
  }
2498
- if (kind !== "all" && !isKind(kind)) throw new Error(`Unknown kind: ${kind}`);
2499
- await cmdAdd(kind, positional[2], positional[3], opts);
2928
+ if (k1 === "all" || isKind(k1)) {
2929
+ const source = positional[2];
2930
+ if (!source) throw new Error(`Missing source. e.g. agentry add ${k1} author/repo`);
2931
+ await cmdAdd(k1, source, positional[3], opts);
2932
+ return;
2933
+ }
2934
+ const source = a1;
2935
+ const selector = positional[2];
2936
+ await cmdAdd("all", source, selector, opts);
2500
2937
  return;
2501
2938
  }
2502
2939
  if (action === "remove" || action === "rm") {
2503
- const kind = positional[1] ? normalizeKind(positional[1]) : void 0;
2504
- if (!kind) throw new Error("Missing kind. e.g. agentry remove skills enhance-prompt");
2505
- if (kind !== "all" && !isKind(kind)) throw new Error(`Unknown kind: ${kind}`);
2506
- await cmdRemove(kind, positional[2], positional[3], opts);
2940
+ const a1 = positional[1];
2941
+ if (!a1) throw new Error("Missing kind or selector. e.g. agentry remove skills enhance-prompt");
2942
+ const k1 = normalizeKind(a1);
2943
+ if (k1 === "all" || isKind(k1)) {
2944
+ await cmdRemove(k1, positional[2], positional[3], opts);
2945
+ return;
2946
+ }
2947
+ await cmdRemove("all", positional[1], positional[2], opts);
2507
2948
  return;
2508
2949
  }
2509
2950
  if (action === "init") {