@mhmdreza-rafiei/agentry 0.2.2 → 0.3.1

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,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { DEFAULT_IGNORES, createIgnoreMatcher, isExplicitSource, isOwnerRepoShape, listAll, listKind, parseSource, resolveSource, select, selectAll, sourcesEqual } from "./_chunks/discovery.mjs";
2
3
  import { animateLogo, badge, delay, printHelp, sky, theme } from "./_chunks/theme2.mjs";
3
4
  import { execFileSync } from "node:child_process";
4
5
  import https from "node:https";
5
- import { createHash } from "node:crypto";
6
- import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
7
- import { homedir, tmpdir } from "node:os";
8
- import { dirname, join, resolve, sep } from "node:path";
9
- import { xdgCache, xdgConfig } from "xdg-basedir";
6
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join, relative, resolve, sep } from "node:path";
9
+ import { xdgConfig } from "xdg-basedir";
10
10
  import { parse, stringify } from "yaml";
11
11
  import { z } from "zod";
12
12
  import * as clack from "@clack/prompts";
@@ -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.2";
19
+ var version = "0.3.1";
20
20
  const ARTIFACT_KINDS = [
21
21
  "skill",
22
22
  "rule",
@@ -24,384 +24,6 @@ const ARTIFACT_KINDS = [
24
24
  "profile",
25
25
  "script"
26
26
  ];
27
- function isLocalPath(source) {
28
- if (!source) return false;
29
- if (source === "." || source === "..") return true;
30
- if (source.startsWith("./") || source.startsWith("../") || source.startsWith("/") || source.startsWith("~")) return true;
31
- try {
32
- return existsSync(source) && statSync(source).isDirectory();
33
- } catch {
34
- return false;
35
- }
36
- }
37
- const SAFE_REF$1 = /^[a-zA-Z0-9][\w./-]*$/;
38
- function parseGithubTreeUrl(url) {
39
- const m = url.match(/^https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)\/tree\/([^/]+)\/(.+)$/);
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}"`);
43
- return {
44
- ownerRepo: m[1],
45
- ref,
46
- subpath: m[3].replace(/\/+$/, "")
47
- };
48
- }
49
- function redactUrl(url) {
50
- return url.replace(/:\/\/([^/@]+)@/g, "://***@");
51
- }
52
- function parseSource(source) {
53
- if (!source || typeof source !== "string") throw new Error("Empty source.");
54
- const raw = source;
55
- if (isLocalPath(source)) return {
56
- kind: "local",
57
- raw,
58
- url: null,
59
- subpath: "",
60
- local: resolve(source.replace(/^~/, homedir()))
61
- };
62
- const tree = parseGithubTreeUrl(source);
63
- if (tree) return {
64
- kind: "github-url",
65
- raw,
66
- url: `https://github.com/${tree.ownerRepo}.git`,
67
- subpath: tree.subpath,
68
- ref: tree.ref,
69
- local: null
70
- };
71
- let m = source.match(/^https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/);
72
- if (m) return {
73
- kind: "github-url",
74
- raw,
75
- url: `https://github.com/${m[1]}.git`,
76
- subpath: "",
77
- local: null
78
- };
79
- m = source.match(/^https?:\/\/gitlab\.com\/([\w./-]+?)(?:\.git)?\/?$/);
80
- if (m) return {
81
- kind: "gitlab-url",
82
- raw,
83
- url: `https://gitlab.com/${m[1]}.git`,
84
- subpath: "",
85
- local: null
86
- };
87
- if (source.startsWith("git@") || /^https?:\/\/.*\.git$/.test(source)) return {
88
- kind: "git-url",
89
- raw,
90
- url: source,
91
- subpath: "",
92
- local: null
93
- };
94
- if (/^[\w.-]+\/[\w.-]+$/.test(source)) return {
95
- kind: "github-shorthand",
96
- raw,
97
- url: `https://github.com/${source}.git`,
98
- subpath: "",
99
- local: null
100
- };
101
- throw new Error(`Cannot parse source "${source}". Use author/repo, a GitHub/GitLab URL, a git URL, or a local path.`);
102
- }
103
- function sourceIdentity(source) {
104
- try {
105
- const p = parseSource(source);
106
- if (p.kind === "local" && p.local) return `local:${resolve(p.local).replace(/\\/g, "/").toLowerCase()}`;
107
- return `git:${(p.url || "").replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase()}${p.subpath ? `/${p.subpath.replace(/^\/+|\/+$/g, "")}` : ""}${p.ref ? `@${p.ref}` : ""}`;
108
- } catch {
109
- return `raw:${source.trim().toLowerCase()}`;
110
- }
111
- }
112
- function sourcesEqual(a, b) {
113
- if (!a || !b) return false;
114
- if (a === b) return true;
115
- return sourceIdentity(a) === sourceIdentity(b);
116
- }
117
- function isExplicitSource(arg) {
118
- if (!arg) return false;
119
- if (isLocalPath(arg)) return true;
120
- if (/^https?:\/\//i.test(arg)) return true;
121
- if (arg.startsWith("git@")) return true;
122
- if (/^https?:\/\/.*\.git$/i.test(arg)) return true;
123
- return false;
124
- }
125
- function isOwnerRepoShape(arg) {
126
- return /^[\w.-]+\/[\w.-]+$/.test(arg);
127
- }
128
- function cacheRoot() {
129
- return join(xdgCache ?? join(homedir(), ".cache"), "agentry", "repos");
130
- }
131
- function cacheKey(parsed) {
132
- const id = sourceIdentity(parsed.raw);
133
- return createHash("sha256").update(id).digest("hex").slice(0, 20);
134
- }
135
- function cacheDirFor(parsed) {
136
- return join(cacheRoot(), cacheKey(parsed));
137
- }
138
- function saveToCache(parsed, clonedRoot) {
139
- const dest = cacheDirFor(parsed);
140
- try {
141
- rmSync(dest, {
142
- recursive: true,
143
- force: true
144
- });
145
- mkdirSync(join(dest, ".."), { recursive: true });
146
- cpSync(clonedRoot, dest, { recursive: true });
147
- } catch {}
148
- }
149
- const SAFE_REF = /^[a-zA-Z0-9][\w./-]*$/;
150
- function cloneTo(url, dest, ref) {
151
- if (ref && !SAFE_REF.test(ref)) throw new Error(`Invalid git ref: "${ref}"`);
152
- execFileSync("git", [
153
- "clone",
154
- "--quiet",
155
- ...ref ? [
156
- "--branch",
157
- ref,
158
- "--depth",
159
- "1"
160
- ] : ["--depth", "1"],
161
- url,
162
- dest
163
- ], { stdio: [
164
- "ignore",
165
- "ignore",
166
- "pipe"
167
- ] });
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
- }
179
- function withSubpath(root, subpath, cleanupRoot) {
180
- const cleanup = () => rmSync(cleanupRoot, {
181
- recursive: true,
182
- force: true
183
- });
184
- if (!subpath) return {
185
- root,
186
- cleanup
187
- };
188
- let sub;
189
- try {
190
- sub = assertInsideRoot(root, join(root, subpath));
191
- } catch (e) {
192
- cleanup();
193
- throw e;
194
- }
195
- if (!existsSync(sub)) {
196
- cleanup();
197
- throw new Error(`Subpath "${subpath}" not found.`);
198
- }
199
- return {
200
- root: sub,
201
- cleanup
202
- };
203
- }
204
- async function resolveSource(source, opts = {}) {
205
- const parsed = typeof source === "string" ? parseSource(source) : source;
206
- if (parsed.kind === "local" && parsed.local) {
207
- if (!existsSync(parsed.local)) throw new Error(`Path not found: ${parsed.raw}`);
208
- return {
209
- root: parsed.local,
210
- cleanup: () => {}
211
- };
212
- }
213
- const url = parsed.url;
214
- if (!url) throw new Error(`Cannot resolve source "${parsed.raw}".`);
215
- const safeUrl = redactUrl(url);
216
- const tmp = mkdtempSync(join(tmpdir(), "agentry-src-"));
217
- try {
218
- cloneTo(url, tmp, parsed.ref);
219
- } catch (e) {
220
- rmSync(tmp, {
221
- recursive: true,
222
- force: true
223
- });
224
- const detail = e.stderr ? e.stderr.toString().trim() : e.message;
225
- const cached = cacheDirFor(parsed);
226
- const cacheOk = existsSync(cached) && existsSync(join(cached, ".git"));
227
- if (cacheOk && opts.confirmCache) {
228
- if (await opts.confirmCache({
229
- url: safeUrl,
230
- cacheDir: cached,
231
- error: detail
232
- })) {
233
- const work = mkdtempSync(join(tmpdir(), "agentry-src-"));
234
- cpSync(cached, work, { recursive: true });
235
- return {
236
- ...withSubpath(work, parsed.subpath, work),
237
- fromCache: true
238
- };
239
- }
240
- }
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.)" : ""}`);
242
- }
243
- saveToCache(parsed, tmp);
244
- return withSubpath(tmp, parsed.subpath, tmp);
245
- }
246
- const SKILL_MARKER = "SKILL.md";
247
- const FILE_EXT = ".mdc";
248
- const PROFILE_EXTS = [".yaml", ".yml"];
249
- const KIND_DIRS = {
250
- skill: "skills",
251
- rule: "rules",
252
- agent: "agents",
253
- profile: "profiles",
254
- script: "scripts"
255
- };
256
- const ROOT_SKIP = /* @__PURE__ */ new Set([
257
- ".",
258
- "node_modules",
259
- "skills",
260
- "rules",
261
- "agents",
262
- "profiles",
263
- "scripts"
264
- ]);
265
- function entries(dir) {
266
- try {
267
- return readdirSync(dir, { withFileTypes: true });
268
- } catch {
269
- return [];
270
- }
271
- }
272
- function firstHeading(md) {
273
- const m = md.match(/^#\s+(.+)$/m);
274
- return m ? m[1].trim() : "";
275
- }
276
- function frontmatterDescription(md) {
277
- const fm = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
278
- if (!fm) return "";
279
- const lines = fm[1].split(/\r?\n/);
280
- for (let i = 0; i < lines.length; i++) {
281
- const dm = lines[i].match(/^description:\s*(.*)$/);
282
- if (!dm) continue;
283
- let val = dm[1].trim();
284
- if (val === "" || val === ">" || val === "|" || val === ">-" || val === "|-") {
285
- const collected = [];
286
- for (let j = i + 1; j < lines.length; j++) if (/^\s+\S/.test(lines[j])) collected.push(lines[j].trim());
287
- else break;
288
- val = collected.join(" ");
289
- }
290
- return val.trim();
291
- }
292
- return "";
293
- }
294
- function readDescription(filePath) {
295
- try {
296
- const md = readFileSync(filePath, "utf8");
297
- return frontmatterDescription(md) || firstHeading(md);
298
- } catch {
299
- return "";
300
- }
301
- }
302
- function folderDescription(dir) {
303
- for (const f of [SKILL_MARKER, "README.md"]) {
304
- const p = join(dir, f);
305
- if (existsSync(p)) {
306
- const d = readDescription(p);
307
- if (d) return d;
308
- }
309
- }
310
- return "";
311
- }
312
- function makeArtifact(kind, category, name, srcPath, isFile) {
313
- return {
314
- kind,
315
- category,
316
- name,
317
- id: category ? `${category}/${name}` : name,
318
- dir: srcPath,
319
- isFile,
320
- description: isFile ? readDescription(srcPath) : folderDescription(srcPath)
321
- };
322
- }
323
- function collectSkills(base, out) {
324
- for (const child of entries(base)) {
325
- if (!child.isDirectory()) continue;
326
- const childDir = join(base, child.name);
327
- if (existsSync(join(childDir, SKILL_MARKER))) out.push(makeArtifact("skill", null, child.name, childDir, false));
328
- else for (const item of entries(childDir)) {
329
- if (!item.isDirectory()) continue;
330
- const itemDir = join(childDir, item.name);
331
- if (existsSync(join(itemDir, SKILL_MARKER))) out.push(makeArtifact("skill", child.name, item.name, itemDir, false));
332
- }
333
- }
334
- }
335
- function collectMdc(kind, base, out) {
336
- 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));
337
- else if (child.isDirectory()) {
338
- const subDir = join(base, child.name);
339
- 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));
340
- }
341
- }
342
- function collectProfiles(base, out) {
343
- for (const child of entries(base)) if (child.isFile() && PROFILE_EXTS.some((e) => child.name.endsWith(e))) {
344
- const stem = child.name.replace(/\.(yaml|yml)$/, "");
345
- out.push(makeArtifact("profile", null, stem, join(base, child.name), true));
346
- } else if (child.isDirectory()) {
347
- const subDir = join(base, child.name);
348
- for (const item of entries(subDir)) if (item.isFile() && PROFILE_EXTS.some((e) => item.name.endsWith(e))) {
349
- const stem = item.name.replace(/\.(yaml|yml)$/, "");
350
- out.push(makeArtifact("profile", child.name, stem, join(subDir, item.name), true));
351
- }
352
- }
353
- }
354
- function collectScripts(base, out) {
355
- for (const child of entries(base)) {
356
- if (!child.isDirectory()) continue;
357
- const childDir = join(base, child.name);
358
- if (entries(childDir).some((e) => e.isFile())) out.push(makeArtifact("script", null, child.name, childDir, false));
359
- 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));
360
- }
361
- }
362
- function collectRootSkills(root, out) {
363
- for (const child of entries(root)) {
364
- if (!child.isDirectory() || child.name.startsWith(".") || ROOT_SKIP.has(child.name)) continue;
365
- const childDir = join(root, child.name);
366
- if (existsSync(join(childDir, SKILL_MARKER))) out.push(makeArtifact("skill", null, child.name, childDir, false));
367
- 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));
368
- }
369
- }
370
- function collectRootMdc(kind, root, out) {
371
- 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));
372
- else if (child.isDirectory() && !child.name.startsWith(".") && !ROOT_SKIP.has(child.name)) {
373
- const subDir = join(root, child.name);
374
- 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));
375
- }
376
- }
377
- function listKind(root, kind) {
378
- const out = [];
379
- const base = join(root, KIND_DIRS[kind]);
380
- if (existsSync(base)) if (kind === "skill") collectSkills(base, out);
381
- else if (kind === "profile") collectProfiles(base, out);
382
- else if (kind === "script") collectScripts(base, out);
383
- else collectMdc(kind, base, out);
384
- if (kind === "skill") collectRootSkills(root, out);
385
- else if (kind === "agent" || kind === "rule") collectRootMdc(kind, root, out);
386
- const seen = /* @__PURE__ */ new Set();
387
- return out.filter((a) => seen.has(a.id) ? false : seen.add(a.id)).sort((a, b) => a.id.localeCompare(b.id));
388
- }
389
- function listAll(root) {
390
- return [
391
- "skill",
392
- "rule",
393
- "agent",
394
- "profile"
395
- ].flatMap((k) => listKind(root, k));
396
- }
397
- function select(root, kind, selector) {
398
- const items = listKind(root, kind);
399
- if (!selector) return items;
400
- if (selector.includes("/")) return items.filter((a) => a.id === selector);
401
- const byCategory = items.filter((a) => a.category === selector);
402
- if (byCategory.length) return byCategory;
403
- return items.filter((a) => a.name === selector);
404
- }
405
27
  const home$1 = homedir();
406
28
  const configHome = xdgConfig ?? join(home$1, ".config");
407
29
  const codexHome = process.env.CODEX_HOME?.trim() || join(home$1, ".codex");
@@ -583,7 +205,7 @@ function getNonUniversalAgents() {
583
205
  return listAgents().filter((a) => a.skillsDir !== ".agents/skills");
584
206
  }
585
207
  const home = homedir();
586
- function copyTreeDereferenced(src, dest, seen = /* @__PURE__ */ new Set()) {
208
+ function copyTreeDereferenced(src, dest, seen = /* @__PURE__ */ new Set(), filter, baseSrc = src) {
587
209
  const st = lstatSync(src);
588
210
  if (st.isSymbolicLink()) {
589
211
  let real;
@@ -594,7 +216,7 @@ function copyTreeDereferenced(src, dest, seen = /* @__PURE__ */ new Set()) {
594
216
  }
595
217
  if (seen.has(real)) return;
596
218
  seen.add(real);
597
- copyTreeDereferenced(real, dest, seen);
219
+ copyTreeDereferenced(real, dest, seen, filter, baseSrc);
598
220
  return;
599
221
  }
600
222
  if (st.isDirectory()) {
@@ -607,7 +229,17 @@ function copyTreeDereferenced(src, dest, seen = /* @__PURE__ */ new Set()) {
607
229
  if (seen.has(real)) return;
608
230
  seen.add(real);
609
231
  mkdirSync(dest, { recursive: true });
610
- for (const name of readdirSync(src)) copyTreeDereferenced(join(src, name), join(dest, name), seen);
232
+ for (const name of readdirSync(src)) {
233
+ const childSrc = join(src, name);
234
+ const relPath = relative(baseSrc, childSrc).replace(/\\/g, "/");
235
+ let isDir = false;
236
+ try {
237
+ isDir = statSync(childSrc).isDirectory();
238
+ } catch {}
239
+ if (DEFAULT_IGNORES.includes(name) || name === ".agentryignore" || name === ".agentry.ignore") continue;
240
+ if (filter && filter(relPath, isDir)) continue;
241
+ copyTreeDereferenced(childSrc, join(dest, name), seen, filter, baseSrc);
242
+ }
611
243
  return;
612
244
  }
613
245
  if (st.isFile()) {
@@ -735,6 +367,7 @@ function installOne(artifact, opts, agentList, source) {
735
367
  });
736
368
  return dests;
737
369
  }
370
+ const ignoreFilter = artifact.isFile ? void 0 : createIgnoreMatcher(artifact.dir);
738
371
  if (artifact.kind === "skill") {
739
372
  const canonical = join(base, ".agents", "skills", artifact.id);
740
373
  rmSync(canonical, {
@@ -742,7 +375,7 @@ function installOne(artifact, opts, agentList, source) {
742
375
  force: true
743
376
  });
744
377
  mkdirSync(join(canonical, ".."), { recursive: true });
745
- copyTreeDereferenced(artifact.dir, canonical);
378
+ copyTreeDereferenced(artifact.dir, canonical, /* @__PURE__ */ new Set(), ignoreFilter);
746
379
  for (const a of list) if (a.skillsDir === ".agents/skills") dests.push({
747
380
  agent: a.name,
748
381
  dir: canonical
@@ -782,7 +415,7 @@ function installOne(artifact, opts, agentList, source) {
782
415
  force: true
783
416
  });
784
417
  mkdirSync(join(dir, ".."), { recursive: true });
785
- copyTreeDereferenced(artifact.dir, dir);
418
+ copyTreeDereferenced(artifact.dir, dir, /* @__PURE__ */ new Set(), ignoreFilter);
786
419
  dests.push({
787
420
  agent: a.name,
788
421
  dir
@@ -1928,7 +1561,7 @@ async function cmdAdd(kind, source, selector, opts) {
1928
1561
  await delayQuiet(280);
1929
1562
  const { root, cleanup } = await resolveSourceWithUi(source, opts);
1930
1563
  try {
1931
- const artifacts = await spinner("Discovering…", () => kind === "all" ? listAll(root) : select(root, kind, selector), (a) => {
1564
+ const artifacts = await spinner("Discovering…", () => kind === "all" ? selector ? selectAll(root, selector) : listAll(root) : select(root, kind, selector), (a) => {
1932
1565
  const noun = kind === "all" ? "artifact" : kind;
1933
1566
  return `Found ${a.length} ${noun}${a.length === 1 ? "" : "s"}`;
1934
1567
  }, 620);
@@ -1943,17 +1576,20 @@ async function cmdAdd(kind, source, selector, opts) {
1943
1576
  return;
1944
1577
  }
1945
1578
  let chosen = artifacts;
1946
- if (!selector && kind !== "all" && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
1947
- await sectionTransition("source discovery", `select ${kind}s to install`);
1948
- const picked = await selectArtifacts(`Select ${kind}s to install`, artifacts);
1579
+ if (!selector && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
1580
+ const noun = kind === "all" ? "artifacts" : `${kind}s`;
1581
+ await sectionTransition("source discovery", `select ${noun} to install`);
1582
+ const picked = await selectArtifacts(`Select ${noun} to install`, artifacts);
1949
1583
  if (isCancelLike(picked)) {
1950
1584
  outro("Cancelled.");
1951
1585
  return;
1952
1586
  }
1953
1587
  chosen = picked;
1954
- await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} selected`, "choose target agents");
1955
- } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} ready`, "choose target agents");
1956
- else {
1588
+ await sectionTransition(`${chosen.length} ${chosen.length === 1 ? "artifact" : "artifacts"} selected`, "choose target agents");
1589
+ } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) {
1590
+ const noun = kind === "all" ? chosen.length === 1 ? "artifact" : "artifacts" : `${kind}${chosen.length === 1 ? "" : "s"}`;
1591
+ await sectionTransition(`${chosen.length} ${noun} ready`, "choose target agents");
1592
+ } else {
1957
1593
  await delayQuiet(200);
1958
1594
  blank();
1959
1595
  }
@@ -2108,7 +1744,7 @@ async function cmdRemove(kind, arg1, arg2, opts) {
2108
1744
  let count = 0;
2109
1745
  let paths = 0;
2110
1746
  for (const t of ARTIFACT_KINDS) {
2111
- const r = removeSelection(t, void 0, installOpts, agentList, source);
1747
+ const r = removeSelection(t, selector, installOpts, agentList, source);
2112
1748
  count += r.removedKeys.length;
2113
1749
  paths += r.removedPaths.length;
2114
1750
  }
@@ -2139,7 +1775,7 @@ async function cmdList(source, kind, selector, opts) {
2139
1775
  try {
2140
1776
  printArtifactList(await spinner("Discovering…", () => {
2141
1777
  if (kind) return select(root, kind, selector);
2142
- return listAll(root);
1778
+ return selector ? selectAll(root, selector) : listAll(root);
2143
1779
  }, (a) => `Found ${a.length} artifact${a.length === 1 ? "" : "s"}`, 520), {
2144
1780
  title: "Available in source",
2145
1781
  subtitle: source
@@ -2238,22 +1874,26 @@ async function cmdUpdateAssets(kind, arg1, arg2, opts) {
2238
1874
  }, 520);
2239
1875
  const { root, cleanup } = await resolveSourceWithUi(source, opts);
2240
1876
  try {
2241
- const artifacts = await spinner("Discovering…", () => kind === "all" ? listAll(root) : select(root, kind, selector), (a) => `Found ${a.length} to update`, 620);
1877
+ const artifacts = await spinner("Discovering…", () => kind === "all" ? selector ? selectAll(root, selector) : listAll(root) : select(root, kind, selector), (a) => `Found ${a.length} to update`, 620);
2242
1878
  if (!artifacts.length) {
2243
1879
  warn(`Nothing found in ${source}.`);
2244
1880
  return;
2245
1881
  }
2246
1882
  let chosen = artifacts;
2247
- if (!selector && kind !== "all" && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
2248
- await sectionTransition("source discovery", `select ${kind}s to update`);
2249
- const picked = await selectArtifacts(`Select ${kind}s to update`, artifacts);
1883
+ if (!selector && artifacts.length > 1 && !opts.all && !opts.yes && !isQuiet()) {
1884
+ const noun = kind === "all" ? "artifacts" : `${kind}s`;
1885
+ await sectionTransition("source discovery", `select ${noun} to update`);
1886
+ const picked = await selectArtifacts(`Select ${noun} to update`, artifacts);
2250
1887
  if (isCancelLike(picked)) {
2251
1888
  outro("Cancelled.");
2252
1889
  return;
2253
1890
  }
2254
1891
  chosen = picked;
2255
- await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} selected`, "choose target agents");
2256
- } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${chosen.length} ${kind}${chosen.length === 1 ? "" : "s"} ready`, "choose target agents");
1892
+ await sectionTransition(`${chosen.length} ${chosen.length === 1 ? "artifact" : "artifacts"} selected`, "choose target agents");
1893
+ } else if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) {
1894
+ const noun = kind === "all" ? chosen.length === 1 ? "artifact" : "artifacts" : `${kind}${chosen.length === 1 ? "" : "s"}`;
1895
+ await sectionTransition(`${chosen.length} ${noun} ready`, "choose target agents");
1896
+ }
2257
1897
  const agentList = await resolveAgentList(opts, "update");
2258
1898
  if (!await ensureInstallScope(opts, "update")) return;
2259
1899
  if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${agentList.length} agent${agentList.length === 1 ? "" : "s"} selected`, "update summary");
@@ -2664,6 +2304,13 @@ async function main() {
2664
2304
  await cmdUpdateAssets(kind, positional[2], positional[3], opts);
2665
2305
  return;
2666
2306
  }
2307
+ if (action === "from") {
2308
+ const source = positional[1];
2309
+ if (!source) throw new Error("Missing URL. e.g. agentry from https://awesomeskill.ai/skill/...");
2310
+ const selector = positional[2];
2311
+ await cmdAdd("all", source, selector, opts);
2312
+ return;
2313
+ }
2667
2314
  if (action === "list") {
2668
2315
  const a1 = positional[1];
2669
2316
  const a2 = positional[2];
@@ -2683,21 +2330,45 @@ async function main() {
2683
2330
  return;
2684
2331
  }
2685
2332
  if (action === "add") {
2686
- const kind = positional[1] ? normalizeKind(positional[1]) : void 0;
2687
- if (!kind) throw new Error("Missing kind. e.g. agentry add skills author/repo");
2688
- if (kind === "profile") {
2333
+ const a1 = positional[1];
2334
+ if (!a1) throw new Error("Missing source or kind. e.g. agentry add author/repo, agentry add from <url>, or agentry add skills author/repo");
2335
+ if (a1 === "from") {
2336
+ const source = positional[2];
2337
+ if (!source) throw new Error("Missing URL after \"from\". e.g. agentry add from https://awesomeskill.ai/skill/...");
2338
+ const selector = positional[3];
2339
+ await cmdAdd("all", source, selector, opts);
2340
+ return;
2341
+ }
2342
+ const k1 = normalizeKind(a1);
2343
+ if (k1 === "profile") {
2689
2344
  await cmdAddProfile(positional[2], positional[3], opts);
2690
2345
  return;
2691
2346
  }
2692
- if (kind !== "all" && !isKind(kind)) throw new Error(`Unknown kind: ${kind}`);
2693
- await cmdAdd(kind, positional[2], positional[3], opts);
2347
+ if (k1 === "all" || isKind(k1)) {
2348
+ let source = positional[2];
2349
+ let selector = positional[3];
2350
+ if (source === "from") {
2351
+ source = positional[3];
2352
+ selector = positional[4];
2353
+ }
2354
+ if (!source) throw new Error(`Missing source. e.g. agentry add ${k1} author/repo or agentry add ${k1} from <url>`);
2355
+ await cmdAdd(k1, source, selector, opts);
2356
+ return;
2357
+ }
2358
+ const source = a1;
2359
+ const selector = positional[2];
2360
+ await cmdAdd("all", source, selector, opts);
2694
2361
  return;
2695
2362
  }
2696
2363
  if (action === "remove" || action === "rm") {
2697
- const kind = positional[1] ? normalizeKind(positional[1]) : void 0;
2698
- if (!kind) throw new Error("Missing kind. e.g. agentry remove skills enhance-prompt");
2699
- if (kind !== "all" && !isKind(kind)) throw new Error(`Unknown kind: ${kind}`);
2700
- await cmdRemove(kind, positional[2], positional[3], opts);
2364
+ const a1 = positional[1];
2365
+ if (!a1) throw new Error("Missing kind or selector. e.g. agentry remove skills enhance-prompt");
2366
+ const k1 = normalizeKind(a1);
2367
+ if (k1 === "all" || isKind(k1)) {
2368
+ await cmdRemove(k1, positional[2], positional[3], opts);
2369
+ return;
2370
+ }
2371
+ await cmdRemove("all", positional[1], positional[2], opts);
2701
2372
  return;
2702
2373
  }
2703
2374
  if (action === "init") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmdreza-rafiei/agentry",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "One CLI to install agents, skills, rules, and profiles onto AI coding agents — portable AI/agent tooling.",
5
5
  "type": "module",
6
6
  "bin": {