@mhmdreza-rafiei/agentry 0.2.0 → 0.2.2

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,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { parseSource } from "./_chunks/source_parser2.mjs";
3
- import { animateLogo, badge, delay, printHelp, theme } from "./_chunks/theme2.mjs";
4
- import { execFileSync, execSync } from "node:child_process";
2
+ import { animateLogo, badge, delay, printHelp, sky, theme } from "./_chunks/theme2.mjs";
3
+ import { execFileSync } from "node:child_process";
5
4
  import https from "node:https";
6
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
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
7
  import { homedir, tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { xdgConfig } from "xdg-basedir";
10
- import { parse } from "yaml";
8
+ import { dirname, join, resolve, sep } from "node:path";
9
+ import { xdgCache, xdgConfig } from "xdg-basedir";
10
+ import { parse, stringify } from "yaml";
11
11
  import { z } from "zod";
12
12
  import * as clack from "@clack/prompts";
13
+ import * as readline from "node:readline";
13
14
  import c from "picocolors";
14
15
  import { determineAgent } from "@vercel/detect-agent";
15
- import * as readline from "node:readline";
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.2.2";
20
20
  const ARTIFACT_KINDS = [
21
21
  "skill",
22
22
  "rule",
@@ -24,7 +24,184 @@ const ARTIFACT_KINDS = [
24
24
  "profile",
25
25
  "script"
26
26
  ];
27
- function resolveSource(source) {
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 = {}) {
28
205
  const parsed = typeof source === "string" ? parseSource(source) : source;
29
206
  if (parsed.kind === "local" && parsed.local) {
30
207
  if (!existsSync(parsed.local)) throw new Error(`Path not found: ${parsed.raw}`);
@@ -35,56 +212,36 @@ function resolveSource(source) {
35
212
  }
36
213
  const url = parsed.url;
37
214
  if (!url) throw new Error(`Cannot resolve source "${parsed.raw}".`);
215
+ const safeUrl = redactUrl(url);
38
216
  const tmp = mkdtempSync(join(tmpdir(), "agentry-src-"));
39
217
  try {
40
- execFileSync("git", [
41
- "clone",
42
- "--quiet",
43
- ...parsed.ref ? [
44
- "--branch",
45
- parsed.ref,
46
- "--depth",
47
- "1"
48
- ] : ["--depth", "1"],
49
- url,
50
- tmp
51
- ], { stdio: [
52
- "ignore",
53
- "ignore",
54
- "pipe"
55
- ] });
218
+ cloneTo(url, tmp, parsed.ref);
56
219
  } catch (e) {
57
220
  rmSync(tmp, {
58
221
  recursive: true,
59
222
  force: true
60
223
  });
61
224
  const detail = e.stderr ? e.stderr.toString().trim() : e.message;
62
- throw new Error(`git clone failed for ${url}\n${detail}`);
63
- }
64
- if (parsed.subpath) {
65
- const sub = join(tmp, parsed.subpath);
66
- if (!existsSync(sub)) {
67
- rmSync(tmp, {
68
- recursive: true,
69
- force: true
70
- });
71
- throw new Error(`Subpath "${parsed.subpath}" not found in ${url}.`);
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
+ }
72
240
  }
73
- return {
74
- root: sub,
75
- cleanup: () => rmSync(tmp, {
76
- recursive: true,
77
- force: true
78
- })
79
- };
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.)" : ""}`);
80
242
  }
81
- return {
82
- root: tmp,
83
- cleanup: () => rmSync(tmp, {
84
- recursive: true,
85
- force: true
86
- })
87
- };
243
+ saveToCache(parsed, tmp);
244
+ return withSubpath(tmp, parsed.subpath, tmp);
88
245
  }
89
246
  const SKILL_MARKER = "SKILL.md";
90
247
  const FILE_EXT = ".mdc";
@@ -426,6 +583,38 @@ function getNonUniversalAgents() {
426
583
  return listAgents().filter((a) => a.skillsDir !== ".agents/skills");
427
584
  }
428
585
  const home = homedir();
586
+ function copyTreeDereferenced(src, dest, seen = /* @__PURE__ */ new Set()) {
587
+ const st = lstatSync(src);
588
+ if (st.isSymbolicLink()) {
589
+ let real;
590
+ try {
591
+ real = realpathSync(src);
592
+ } catch {
593
+ return;
594
+ }
595
+ if (seen.has(real)) return;
596
+ seen.add(real);
597
+ copyTreeDereferenced(real, dest, seen);
598
+ return;
599
+ }
600
+ if (st.isDirectory()) {
601
+ let real;
602
+ try {
603
+ real = realpathSync(src);
604
+ } catch {
605
+ real = resolve(src);
606
+ }
607
+ if (seen.has(real)) return;
608
+ seen.add(real);
609
+ mkdirSync(dest, { recursive: true });
610
+ for (const name of readdirSync(src)) copyTreeDereferenced(join(src, name), join(dest, name), seen);
611
+ return;
612
+ }
613
+ if (st.isFile()) {
614
+ mkdirSync(dirname(dest), { recursive: true });
615
+ copyFileSync(src, dest);
616
+ }
617
+ }
429
618
  function lockBase(opts) {
430
619
  return opts.scope === "global" ? home : opts.dir || process.cwd();
431
620
  }
@@ -458,10 +647,74 @@ function removeLegacyLockIfPresent(opts) {
458
647
  }
459
648
  function writeLock(opts, lock) {
460
649
  const file = lockPath(opts);
650
+ if (!Object.keys(lock.items).length) {
651
+ const dir = join(lockBase(opts), ".agentry");
652
+ if (existsSync(dir)) rmSync(dir, {
653
+ recursive: true,
654
+ force: true
655
+ });
656
+ removeLegacyLockIfPresent(opts);
657
+ return;
658
+ }
461
659
  mkdirSync(join(file, ".."), { recursive: true });
462
660
  writeFileSync(file, JSON.stringify(lock, null, 2) + "\n");
463
661
  removeLegacyLockIfPresent(opts);
464
662
  }
663
+ function isDirTreeEmpty(dir) {
664
+ if (!existsSync(dir)) return true;
665
+ let entries;
666
+ try {
667
+ entries = readdirSync(dir);
668
+ } catch {
669
+ return true;
670
+ }
671
+ for (const name of entries) {
672
+ const p = join(dir, name);
673
+ let st;
674
+ try {
675
+ st = statSync(p);
676
+ } catch {
677
+ continue;
678
+ }
679
+ if (st.isDirectory()) {
680
+ if (!isDirTreeEmpty(p)) return false;
681
+ continue;
682
+ }
683
+ if (st.isFile()) {
684
+ if (name === "lock.json") {
685
+ try {
686
+ const data = JSON.parse(readFileSync(p, "utf8"));
687
+ if (data && typeof data === "object" && Object.keys(data.items || {}).length === 0) continue;
688
+ } catch {
689
+ return false;
690
+ }
691
+ return false;
692
+ }
693
+ return false;
694
+ }
695
+ }
696
+ return true;
697
+ }
698
+ function pruneEmptyAncestors(dir, stopAt, removedPaths = []) {
699
+ const stop = resolve(stopAt);
700
+ let cur = resolve(dir);
701
+ while (cur.length > stop.length) {
702
+ const prefix = stop.endsWith(sep) ? stop : stop + sep;
703
+ if (!cur.toLowerCase().startsWith(prefix.toLowerCase()) && cur.toLowerCase() !== stop.toLowerCase()) break;
704
+ if (!existsSync(cur)) {
705
+ cur = dirname(cur);
706
+ continue;
707
+ }
708
+ if (!isDirTreeEmpty(cur)) break;
709
+ rmSync(cur, {
710
+ recursive: true,
711
+ force: true
712
+ });
713
+ removedPaths.push(cur);
714
+ cur = dirname(cur);
715
+ }
716
+ return removedPaths;
717
+ }
465
718
  function listInstalled(opts) {
466
719
  return Object.entries(readLock(opts).items).map(([id, v]) => ({
467
720
  id,
@@ -489,12 +742,7 @@ function installOne(artifact, opts, agentList, source) {
489
742
  force: true
490
743
  });
491
744
  mkdirSync(join(canonical, ".."), { recursive: true });
492
- if (opts.copy) cpSync(artifact.dir, canonical, { recursive: true });
493
- else try {
494
- symlinkSync(artifact.dir, canonical, "junction");
495
- } catch {
496
- cpSync(artifact.dir, canonical, { recursive: true });
497
- }
745
+ copyTreeDereferenced(artifact.dir, canonical);
498
746
  for (const a of list) if (a.skillsDir === ".agents/skills") dests.push({
499
747
  agent: a.name,
500
748
  dir: canonical
@@ -507,7 +755,7 @@ function installOne(artifact, opts, agentList, source) {
507
755
  });
508
756
  mkdirSync(join(dir, ".."), { recursive: true });
509
757
  if (opts.copy) {
510
- cpSync(canonical, dir, { recursive: true });
758
+ copyTreeDereferenced(canonical, dir);
511
759
  dests.push({
512
760
  agent: a.name,
513
761
  dir
@@ -520,7 +768,7 @@ function installOne(artifact, opts, agentList, source) {
520
768
  symlinked: true
521
769
  });
522
770
  } catch {
523
- cpSync(canonical, dir, { recursive: true });
771
+ copyTreeDereferenced(canonical, dir);
524
772
  dests.push({
525
773
  agent: a.name,
526
774
  dir
@@ -534,12 +782,7 @@ function installOne(artifact, opts, agentList, source) {
534
782
  force: true
535
783
  });
536
784
  mkdirSync(join(dir, ".."), { recursive: true });
537
- if (opts.copy) cpSync(artifact.dir, dir, { recursive: true });
538
- else try {
539
- symlinkSync(artifact.dir, dir, "junction");
540
- } catch {
541
- cpSync(artifact.dir, dir, { recursive: true });
542
- }
785
+ copyTreeDereferenced(artifact.dir, dir);
543
786
  dests.push({
544
787
  agent: a.name,
545
788
  dir
@@ -563,37 +806,115 @@ function agentDir(artifact, a, base) {
563
806
  }
564
807
  return join(base, a.configDir || a.skillsDir, pluralKind(artifact.kind), artifact.id);
565
808
  }
566
- function removeSelection(kind, selector, opts, agentList) {
809
+ function removeSelection(kind, selector, opts, agentList, sourceFilter) {
567
810
  const base = lockBase(opts);
568
811
  const list = agentList.length ? agentList : [getAgent("cursor")];
569
812
  const rel = [kind, selector].filter(Boolean).join("/");
570
813
  const removedKeys = [];
814
+ const removedPaths = [];
571
815
  const lock = readLock(opts);
572
- const pkind = pluralKind(kind);
573
- for (const a of list) {
574
- let dir;
575
- if (kind === "skill") dir = join(base, a.skillsDir, selector || "");
576
- else if (kind === "agent" || kind === "rule" || kind === "profile") {
577
- const ext = kind === "profile" ? ".yaml" : ".mdc";
578
- if (!selector) dir = join(base, a.configDir || a.skillsDir, pkind);
579
- else if (selector.includes("/")) {
580
- const [cat, name] = selector.split("/");
581
- dir = join(base, a.configDir || a.skillsDir, pkind, cat, name + ext);
582
- } else dir = join(base, a.configDir || a.skillsDir, pkind, selector + ext);
583
- } else dir = join(base, a.configDir || a.skillsDir, pkind, selector || "");
584
- if (existsSync(dir)) rmSync(dir, {
816
+ const matchingKeys = Object.keys(lock.items).filter((key) => {
817
+ const entry = lock.items[key];
818
+ if (sourceFilter && !sourcesEqual(entry.source, sourceFilter)) return false;
819
+ if (kind === "all") return true;
820
+ if (key === rel || key.startsWith(rel + "/")) return true;
821
+ if (!selector && key.startsWith(kind + "/")) return true;
822
+ return false;
823
+ });
824
+ const rmPath = (p) => {
825
+ if (!p || !existsSync(p)) return;
826
+ const parent = dirname(p);
827
+ rmSync(p, {
585
828
  recursive: true,
586
829
  force: true
587
830
  });
588
- }
589
- for (const key of Object.keys(lock.items)) if (key === rel || key.startsWith(rel + "/")) {
831
+ removedPaths.push(p);
832
+ pruneEmptyAncestors(parent, base, removedPaths);
833
+ };
834
+ const agentsFor = (entry) => {
835
+ const fromEntry = entry.agents.map((n) => getAgent(n)).filter(Boolean);
836
+ const byName = /* @__PURE__ */ new Map();
837
+ for (const a of [...fromEntry, ...list]) byName.set(a.name, a);
838
+ return [...byName.values()];
839
+ };
840
+ const removeFilesForKey = (key, entry) => {
841
+ const parts = key.split("/");
842
+ const k = parts[0];
843
+ const sel = parts.slice(1).join("/") || void 0;
844
+ if (!sel && k === "skill") return;
845
+ const agents = agentsFor(entry);
846
+ if (k === "skill" && sel) {
847
+ rmPath(join(base, ".agents", "skills", sel));
848
+ for (const a of agents) {
849
+ if (a.skillsDir === ".agents/skills") continue;
850
+ rmPath(join(base, a.skillsDir, sel));
851
+ }
852
+ return;
853
+ }
854
+ for (const a of agents) if (k === "agent" || k === "rule" || k === "profile") {
855
+ const ext = k === "profile" ? ".yaml" : ".mdc";
856
+ if (!sel) rmPath(join(base, a.configDir || a.skillsDir, pluralKind(k)));
857
+ else if (sel.includes("/")) {
858
+ const [cat, name] = sel.split("/");
859
+ rmPath(join(base, a.configDir || a.skillsDir, pluralKind(k), cat, name + ext));
860
+ } else rmPath(join(base, a.configDir || a.skillsDir, pluralKind(k), sel + ext));
861
+ } else if (sel) rmPath(join(base, a.configDir || a.skillsDir, pluralKind(k), sel));
862
+ };
863
+ for (const key of matchingKeys) {
864
+ const entry = lock.items[key];
865
+ removeFilesForKey(key, entry);
590
866
  delete lock.items[key];
591
867
  removedKeys.push(key);
592
868
  }
869
+ if (!removedKeys.length && !sourceFilter) {
870
+ if (kind === "skill") rmPath(join(base, ".agents", "skills", selector || ""));
871
+ for (const a of list) {
872
+ let dir;
873
+ if (kind === "skill") {
874
+ if (a.skillsDir === ".agents/skills") continue;
875
+ dir = join(base, a.skillsDir, selector || "");
876
+ } else if (kind === "agent" || kind === "rule" || kind === "profile") {
877
+ const ext = kind === "profile" ? ".yaml" : ".mdc";
878
+ if (!selector) dir = join(base, a.configDir || a.skillsDir, pluralKind(kind));
879
+ else if (selector.includes("/")) {
880
+ const [cat, name] = selector.split("/");
881
+ dir = join(base, a.configDir || a.skillsDir, pluralKind(kind), cat, name + ext);
882
+ } else dir = join(base, a.configDir || a.skillsDir, pluralKind(kind), selector + ext);
883
+ } else dir = join(base, a.configDir || a.skillsDir, pluralKind(kind), selector || "");
884
+ if (existsSync(dir)) {
885
+ rmPath(dir);
886
+ if (!removedKeys.includes(rel)) removedKeys.push(rel);
887
+ }
888
+ }
889
+ for (const key of Object.keys(lock.items)) if (key === rel || key.startsWith(rel + "/")) {
890
+ delete lock.items[key];
891
+ if (!removedKeys.includes(key)) removedKeys.push(key);
892
+ }
893
+ }
593
894
  writeLock(opts, lock);
895
+ const sweepRoots = /* @__PURE__ */ new Set([
896
+ join(base, ".agents", "skills"),
897
+ join(base, ".agents"),
898
+ join(base, ".agentry")
899
+ ]);
900
+ for (const a of list) {
901
+ sweepRoots.add(join(base, a.skillsDir));
902
+ if (a.configDir) {
903
+ sweepRoots.add(join(base, a.configDir));
904
+ for (const pk of [
905
+ "skills",
906
+ "agents",
907
+ "rules",
908
+ "profiles",
909
+ "scripts"
910
+ ]) sweepRoots.add(join(base, a.configDir, pk));
911
+ }
912
+ }
913
+ for (const root of sweepRoots) if (existsSync(root) && isDirTreeEmpty(root)) pruneEmptyAncestors(root, base, removedPaths);
594
914
  return {
595
- existed: removedKeys.length > 0,
596
- removedKeys
915
+ existed: removedKeys.length > 0 || removedPaths.length > 0,
916
+ removedKeys,
917
+ removedPaths
597
918
  };
598
919
  }
599
920
  const ProfileArtifactRefSchema = z.object({
@@ -636,6 +957,98 @@ function loadProfile(name, baseDir) {
636
957
  profile: loadProfileFile(file)
637
958
  };
638
959
  }
960
+ function ensureDir(file) {
961
+ mkdirSync(join(file, ".."), { recursive: true });
962
+ }
963
+ function writeNew(file, content, force = false) {
964
+ if (existsSync(file) && !force) throw new Error(`Already exists: ${file}`);
965
+ ensureDir(file);
966
+ writeFileSync(file, content);
967
+ return file;
968
+ }
969
+ function scaffoldSkill(opts) {
970
+ const dir = join(opts.baseDir || process.cwd(), opts.category ? join("skills", opts.category, opts.name) : join("skills", opts.name));
971
+ const id = opts.category ? `${opts.category}/${opts.name}` : opts.name;
972
+ const desc = opts.description || `${opts.name} skill`;
973
+ const skillMd = `---
974
+ name: ${opts.name}
975
+ description: ${desc}
976
+ ---
977
+
978
+ # ${opts.name}
979
+
980
+ ${desc}
981
+
982
+ ## When to use
983
+
984
+ Describe when this skill should run.
985
+
986
+ ## Instructions
987
+
988
+ 1. …
989
+ `;
990
+ const paths = [writeNew(join(dir, "SKILL.md"), skillMd)];
991
+ if (opts.reference !== false) paths.push(writeNew(join(dir, "references", "TEMPLATE.md"), `# ${opts.name} — reference
992
+
993
+ Fill in details here. Keep SKILL.md lean; put depth in references/.
994
+ `));
995
+ return {
996
+ paths,
997
+ id
998
+ };
999
+ }
1000
+ function scaffoldMdc(opts) {
1001
+ const base = opts.baseDir || process.cwd();
1002
+ const folder = opts.kind === "agent" ? "agents" : "rules";
1003
+ const rel = opts.category ? join(folder, opts.category, `${opts.name}.mdc`) : join(folder, `${opts.name}.mdc`);
1004
+ const id = opts.category ? `${opts.category}/${opts.name}` : opts.name;
1005
+ const desc = opts.description || `${opts.name} ${opts.kind}`;
1006
+ const always = opts.kind === "rule" && opts.alwaysApply ? "alwaysApply: true\n" : opts.kind === "rule" ? "alwaysApply: false\n" : "";
1007
+ const body = `---
1008
+ name: ${opts.name}
1009
+ description: ${desc}
1010
+ ${always}---
1011
+
1012
+ # ${opts.name}
1013
+
1014
+ ${desc}
1015
+ `;
1016
+ return {
1017
+ paths: [writeNew(join(base, rel), body)],
1018
+ id
1019
+ };
1020
+ }
1021
+ function scaffoldScript(opts) {
1022
+ const dir = join(opts.baseDir || process.cwd(), opts.category ? join("scripts", opts.category, opts.name) : join("scripts", opts.name));
1023
+ const id = opts.category ? `${opts.category}/${opts.name}` : opts.name;
1024
+ const desc = opts.description || `${opts.name} script`;
1025
+ return {
1026
+ paths: [writeNew(join(dir, "README.md"), `# ${opts.name}
1027
+
1028
+ ${desc}
1029
+
1030
+ Place runnable scripts for this use case in this folder.
1031
+ `)],
1032
+ id
1033
+ };
1034
+ }
1035
+ function scaffoldProfile(opts) {
1036
+ return {
1037
+ paths: [writeNew(join(opts.baseDir || process.cwd(), opts.localConfig ? "profile" : "profiles", `${opts.name}.yaml`), stringify({
1038
+ name: opts.name,
1039
+ description: opts.description || `${opts.name} profile`,
1040
+ scope: "project",
1041
+ targets: { agents: opts.agents?.length ? opts.agents : ["cursor"] },
1042
+ artifacts: {
1043
+ skills: opts.skills ?? [],
1044
+ rules: opts.rules ?? [],
1045
+ agents: opts.agentsArtifacts ?? [],
1046
+ scripts: opts.scripts ?? []
1047
+ }
1048
+ }))],
1049
+ id: opts.name
1050
+ };
1051
+ }
639
1052
  let runningInsideAgent = false;
640
1053
  try {
641
1054
  const result = await determineAgent();
@@ -651,17 +1064,17 @@ const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
651
1064
  } });
652
1065
  const cancelSymbol = Symbol("cancel");
653
1066
  const ALL_VALUE = "__all__";
654
- const BLUE = c.blue;
655
- const BLUE_BOLD = (s) => c.bold(c.blue(s));
656
- const S_STEP_ACTIVE = BLUE("◆");
1067
+ const BLUE = sky;
1068
+ const BLUE_BOLD = (s) => c.bold(sky(s));
1069
+ const S_STEP_ACTIVE = sky("◆");
657
1070
  const S_STEP_CANCEL = c.red("■");
658
- const S_STEP_SUBMIT = BLUE("◇");
659
- const S_RADIO_ACTIVE = BLUE("●");
1071
+ const S_STEP_SUBMIT = sky("◇");
1072
+ const S_RADIO_ACTIVE = sky("●");
660
1073
  const S_RADIO_INACTIVE = c.dim("○");
661
1074
  const S_RADIO_DISABLED = c.dim("◌");
662
- const S_BULLET = BLUE("•");
1075
+ const S_BULLET = sky("•");
663
1076
  const S_BAR = c.dim("│");
664
- const S_BAR_H = c.dim("─");
1077
+ c.dim("─");
665
1078
  const S_CORNER = c.dim("└");
666
1079
  const S_BOX_TL = c.dim("┌");
667
1080
  const S_BOX_TR = c.dim("┐");
@@ -764,7 +1177,7 @@ function renderInfoBox(fields, innerWidth, rows) {
764
1177
  });
765
1178
  const body = expanded.map((r) => {
766
1179
  if (!r.label && !r.value) return `${S_BOX_V} ${" ".repeat(contentWidth)} ${S_BOX_V}`;
767
- const content = `${r.label ? `${c.blue(padPlain(r.label, labelW))} ` : `${" ".repeat(labelW)} `}${r.cont ? c.dim(r.value) : c.bold(r.value)}`;
1180
+ const content = `${r.label ? `${sky(padPlain(r.label, labelW))} ` : `${" ".repeat(labelW)} `}${r.cont ? c.dim(r.value) : c.bold(r.value)}`;
768
1181
  const plainW = approxStringWidth(stripVTControlCharacters(content));
769
1182
  const pad = Math.max(0, contentWidth - plainW);
770
1183
  return `${S_BOX_V} ${content}${" ".repeat(pad)} ${S_BOX_V}`;
@@ -859,6 +1272,7 @@ async function searchMultiselect(options) {
859
1272
  });
860
1273
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
861
1274
  readline.emitKeypressEvents(process.stdin, rl);
1275
+ if (process.stdout.isTTY) process.stdout.write("\x1B[?25l");
862
1276
  let query = "";
863
1277
  let cursor = 0;
864
1278
  const selected = new Set(initialSelected.filter((v) => String(v) !== ALL_VALUE));
@@ -873,6 +1287,32 @@ async function searchMultiselect(options) {
873
1287
  const matched = rest.filter((item) => item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ) || item.hint && item.hint.toLowerCase().includes(lowerQ));
874
1288
  return allItem && (allItem.label.toLowerCase().includes(lowerQ) || "all".includes(lowerQ)) && allItem ? [allItem, ...matched] : matched;
875
1289
  };
1290
+ const lockedTotal = lockedSection ? lockedSection.items.length + (lockedSection.hiddenCount ?? 0) : 0;
1291
+ const selectedSummaryLine = (allOn) => {
1292
+ if (allOn) {
1293
+ const extra = lockedTotal ? `Universal (${lockedTotal}) + All additional (${uniqueItems.length})` : `All ${uniqueItems.length}`;
1294
+ return `${S_BAR} ${BLUE("Selected:")} ${c.bold(extra)}`;
1295
+ }
1296
+ const labels = uniqueItems.filter((item) => selected.has(item.value)).map((item) => item.label);
1297
+ if (labels.length === 0) {
1298
+ if (requiredFlash) return `${S_BAR} ${c.red("Selected: pick at least one")}`;
1299
+ if (lockedTotal > 0) return `${S_BAR} ${BLUE("Selected:")} ${c.bold(`Universal (${lockedTotal})`)}`;
1300
+ return `${S_BAR} ${c.dim("Selected: (none)")}`;
1301
+ }
1302
+ const prefix = lockedTotal > 0 ? `Universal (${lockedTotal}) + ` : "";
1303
+ const summary = labels.length <= 3 ? labels.join(", ") : `${labels.slice(0, 3).join(", ")} +${labels.length - 3} more`;
1304
+ return `${S_BAR} ${BLUE("Selected:")} ${prefix}${summary}`;
1305
+ };
1306
+ const submitSummary = (allOn) => {
1307
+ if (allOn) return lockedTotal ? `Universal (${lockedTotal}) + All additional (${uniqueItems.length})` : `All (${uniqueItems.length})`;
1308
+ const labels = uniqueItems.filter((item) => selected.has(item.value)).map((item) => item.label);
1309
+ if (labels.length === 0) return lockedTotal > 0 ? `Universal (${lockedTotal})` : "none";
1310
+ return (lockedTotal > 0 ? `Universal (${lockedTotal}) + ` : "") + (labels.length <= 5 ? labels.join(", ") : `${labels.slice(0, 5).join(", ")} +${labels.length - 5}`);
1311
+ };
1312
+ const clearPrevious = (rows) => {
1313
+ if (rows <= 0) return;
1314
+ for (let i = 0; i < rows; i++) process.stdout.write("\x1B[1A\x1B[2K");
1315
+ };
876
1316
  const render = (state = "active") => {
877
1317
  const lines = [];
878
1318
  const filtered = getFiltered();
@@ -881,14 +1321,9 @@ async function searchMultiselect(options) {
881
1321
  lines.push(`${icon} ${c.bold(message)}`);
882
1322
  lines.push(`${S_BAR}`);
883
1323
  if (state === "active") {
884
- if (lockedSection && lockedSection.items.length > 0) {
885
- const lockedTitle = `${c.bold(lockedSection.title)} ${c.dim("── always included")}`;
886
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(8)}`);
887
- for (const item of lockedSection.items.slice(0, 3)) lines.push(`${S_BAR} ${S_BULLET} ${c.bold(item.label)}`);
888
- const more = (lockedSection.hiddenCount ?? 0) + Math.max(0, lockedSection.items.length - 3);
889
- if (more > 0) lines.push(`${S_BAR} ${c.dim(`…and ${more} more`)}`);
890
- lines.push(`${S_BAR}`);
891
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${c.bold("Additional")} ${S_BAR_H.repeat(26)}`);
1324
+ if (lockedSection && lockedTotal > 0) {
1325
+ lines.push(`${S_BAR} ${S_BULLET} ${c.bold(lockedSection.title)} ${c.dim(`· ${lockedTotal} always included`)}`);
1326
+ lines.push(`${S_BAR} ${c.bold("Additional")}`);
892
1327
  lines.push(`${S_BAR}`);
893
1328
  }
894
1329
  if (searchable) {
@@ -938,28 +1373,20 @@ async function searchMultiselect(options) {
938
1373
  }
939
1374
  if (showSelectedSummary) {
940
1375
  lines.push(`${S_BAR}`);
941
- if (allOn) lines.push(`${S_BAR} ${BLUE("Selected:")} ${c.bold(`All ${uniqueItems.length}`)}`);
942
- else {
943
- const labels = uniqueItems.filter((item) => selected.has(item.value)).map((item) => item.label);
944
- if (labels.length === 0) lines.push(`${S_BAR} ${requiredFlash ? c.red("Selected: pick at least one") : c.dim("Selected: (none)")}`);
945
- else {
946
- const summary = labels.length <= 3 ? labels.join(", ") : `${labels.slice(0, 3).join(", ")} +${labels.length - 3} more`;
947
- lines.push(`${S_BAR} ${BLUE("Selected:")} ${summary}`);
948
- }
949
- }
1376
+ lines.push(selectedSummaryLine(allOn));
950
1377
  }
951
1378
  lines.push(`${S_CORNER}`);
952
- } else if (state === "submit") {
953
- const labels = allOn ? [`All (${uniqueItems.length})`] : uniqueItems.filter((item) => selected.has(item.value)).map((item) => item.label);
954
- lines.push(`${S_BAR} ${c.dim(labels.join(", ") || "none")}`);
955
- } else lines.push(`${S_BAR} ${c.strikethrough(c.dim("Cancelled"))}`);
956
- const clearPreviousFrame = lastRenderHeight > 0 ? `\x1b[${lastRenderHeight}A\x1b[J` : "";
957
- process.stdout.write(clearPreviousFrame + lines.join("\n") + "\n");
1379
+ } else if (state === "submit") lines.push(`${S_BAR} ${c.dim(submitSummary(allOn))}`);
1380
+ else lines.push(`${S_BAR} ${c.strikethrough(c.dim("Cancelled"))}`);
1381
+ clearPrevious(lastRenderHeight);
1382
+ const out = lines.join("\n") + "\n";
1383
+ process.stdout.write(out);
958
1384
  lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns);
959
1385
  };
960
1386
  const cleanup = () => {
961
1387
  process.stdin.removeListener("keypress", keypressHandler);
962
1388
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1389
+ if (process.stdout.isTTY) process.stdout.write("\x1B[?25h");
963
1390
  rl.close();
964
1391
  };
965
1392
  const submit = () => {
@@ -1037,6 +1464,103 @@ async function searchMultiselect(options) {
1037
1464
  render();
1038
1465
  });
1039
1466
  }
1467
+ function agentsPrompt(action) {
1468
+ if (action === "remove") return "Which agents do you want to remove from?";
1469
+ if (action === "update") return "Which agents do you want to update for?";
1470
+ return "Which agents do you want to install to?";
1471
+ }
1472
+ function scopePrompt(action) {
1473
+ if (action === "remove") return "Removal scope";
1474
+ if (action === "update") return "Update scope";
1475
+ return "Installation scope";
1476
+ }
1477
+ async function selectInstallScope(message, paths) {
1478
+ if (isQuiet()) return "project";
1479
+ return new Promise((resolve) => {
1480
+ if (!process.stdin.isTTY) {
1481
+ resolve("project");
1482
+ return;
1483
+ }
1484
+ const rl = readline.createInterface({
1485
+ input: process.stdin,
1486
+ output: process.stdout
1487
+ });
1488
+ process.stdin.setRawMode(true);
1489
+ readline.emitKeypressEvents(process.stdin, rl);
1490
+ let value = "project";
1491
+ let lastH = 0;
1492
+ const joinRel = (base, rel) => {
1493
+ const sep = base.includes("\\") ? "\\" : "/";
1494
+ return `${base.replace(/[\\/]+$/, "")}${sep}${rel.replace(/\//g, sep)}`;
1495
+ };
1496
+ const optionBlock = (key, selected, done) => {
1497
+ const active = done ? done === key : selected;
1498
+ const bullet = active ? sky(c.bold("●")) : c.dim("○");
1499
+ const title = key === "project" ? "Project" : "Global";
1500
+ const hint = key === "project" ? "Install in the current directory (committed with your project)" : "Install in your home directory (available across all projects)";
1501
+ const path = key === "project" ? paths.projectDir : paths.homeDir;
1502
+ const lock = key === "project" ? joinRel(paths.projectDir, ".agentry/lock.json") : joinRel(paths.homeDir, ".agentry/lock.json");
1503
+ const lines = [];
1504
+ if (done === "cancel") lines.push(`${c.dim("│")} ${c.strikethrough(c.dim(title))}`);
1505
+ else {
1506
+ lines.push(`${c.dim("│")} ${bullet} ${active ? c.bold(title) : title}`);
1507
+ lines.push(`${c.dim("│")} ${c.dim(hint)}`);
1508
+ lines.push(`${c.dim("│")} ${sky("→")} ${c.dim(path)}`);
1509
+ lines.push(`${c.dim("│")} ${c.dim(`lock ${lock}`)}`);
1510
+ }
1511
+ return lines;
1512
+ };
1513
+ const paint = (done) => {
1514
+ const lines = [];
1515
+ if (done === "cancel") {
1516
+ lines.push(`${sky("■")} ${c.bold(message)}`);
1517
+ lines.push(`${c.dim("│")} ${c.strikethrough(c.dim("Cancelled"))}`);
1518
+ } else if (done === "project" || done === "global") {
1519
+ lines.push(`${sky("◇")} ${c.bold(message)}`);
1520
+ lines.push(...optionBlock(done, true, done).slice(0, 2));
1521
+ lines.push(`${c.dim("│")} ${sky("→")} ${c.dim(done === "project" ? paths.projectDir : paths.homeDir)}`);
1522
+ } else {
1523
+ lines.push(`${sky("◆")} ${c.bold(message)}`);
1524
+ lines.push(`${c.dim("│")}`);
1525
+ lines.push(...optionBlock("project", value === "project"));
1526
+ lines.push(`${c.dim("│")}`);
1527
+ lines.push(...optionBlock("global", value === "global"));
1528
+ lines.push(`${c.dim("└")} ${c.dim("↑/↓ select · enter confirm")}`);
1529
+ }
1530
+ if (lastH > 0) for (let i = 0; i < lastH; i++) process.stdout.write("\x1B[1A\x1B[2K");
1531
+ process.stdout.write(lines.join("\n") + "\n");
1532
+ lastH = lines.length;
1533
+ };
1534
+ const cleanup = () => {
1535
+ process.stdin.removeListener("keypress", onKey);
1536
+ process.stdin.setRawMode(false);
1537
+ rl.close();
1538
+ };
1539
+ const onKey = (_s, key) => {
1540
+ if (!key) return;
1541
+ if (key.name === "up" || key.name === "down" || key.name === "tab") {
1542
+ value = value === "project" ? "global" : "project";
1543
+ paint();
1544
+ return;
1545
+ }
1546
+ if (key.name === "return") {
1547
+ paint(value);
1548
+ cleanup();
1549
+ blank();
1550
+ resolve(value);
1551
+ return;
1552
+ }
1553
+ if (key.name === "escape" || key.ctrl && key.name === "c") {
1554
+ paint("cancel");
1555
+ cleanup();
1556
+ resolve(null);
1557
+ }
1558
+ };
1559
+ process.stdin.on("keypress", onKey);
1560
+ blank();
1561
+ paint();
1562
+ });
1563
+ }
1040
1564
  function isQuiet() {
1041
1565
  return isCI() || !process.stdout.isTTY;
1042
1566
  }
@@ -1054,6 +1578,79 @@ function outro(message) {
1054
1578
  }
1055
1579
  clack.outro(message);
1056
1580
  }
1581
+ async function confirm(message) {
1582
+ if (isQuiet()) return true;
1583
+ return new Promise((resolve) => {
1584
+ if (!process.stdin.isTTY) {
1585
+ resolve(true);
1586
+ return;
1587
+ }
1588
+ const rl = readline.createInterface({
1589
+ input: process.stdin,
1590
+ output: process.stdout
1591
+ });
1592
+ process.stdin.setRawMode(true);
1593
+ readline.emitKeypressEvents(process.stdin, rl);
1594
+ let value = true;
1595
+ let lastH = 0;
1596
+ const paint = (done) => {
1597
+ const lines = [];
1598
+ if (done === "cancel") {
1599
+ lines.push(`${sky("■")} ${c.bold(message)}`);
1600
+ lines.push(`${c.dim("│")} ${c.strikethrough(c.dim("Cancelled"))}`);
1601
+ } else if (done === "yes" || done === "no") {
1602
+ lines.push(`${sky("◇")} ${c.bold(message)}`);
1603
+ lines.push(`${c.dim("│")} ${c.dim(done === "yes" ? "Yes" : "No")}`);
1604
+ } else {
1605
+ lines.push(`${sky("◆")} ${c.bold(message)}`);
1606
+ lines.push(`${c.dim("│")}`);
1607
+ const yes = value ? sky(c.bold("● Yes")) : c.dim("○ Yes");
1608
+ const no = !value ? sky(c.bold("● No")) : c.dim("○ No");
1609
+ lines.push(`${c.dim("│")} ${yes} / ${no}`);
1610
+ lines.push(`${c.dim("└")} ${c.dim("←/→ toggle · enter confirm")}`);
1611
+ }
1612
+ if (lastH > 0) for (let i = 0; i < lastH; i++) process.stdout.write("\x1B[1A\x1B[2K");
1613
+ process.stdout.write(lines.join("\n") + "\n");
1614
+ lastH = lines.length;
1615
+ };
1616
+ const cleanup = () => {
1617
+ process.stdin.removeListener("keypress", onKey);
1618
+ process.stdin.setRawMode(false);
1619
+ rl.close();
1620
+ };
1621
+ const onKey = (_s, key) => {
1622
+ if (!key) return;
1623
+ if (key.name === "left" || key.name === "right" || key.name === "tab") {
1624
+ value = !value;
1625
+ paint();
1626
+ return;
1627
+ }
1628
+ if (key.name === "y") {
1629
+ value = true;
1630
+ paint();
1631
+ return;
1632
+ }
1633
+ if (key.name === "n") {
1634
+ value = false;
1635
+ paint();
1636
+ return;
1637
+ }
1638
+ if (key.name === "return") {
1639
+ paint(value ? "yes" : "no");
1640
+ cleanup();
1641
+ resolve(value);
1642
+ return;
1643
+ }
1644
+ if (key.name === "escape" || key.ctrl && key.name === "c") {
1645
+ paint("cancel");
1646
+ cleanup();
1647
+ resolve(false);
1648
+ }
1649
+ };
1650
+ process.stdin.on("keypress", onKey);
1651
+ paint();
1652
+ });
1653
+ }
1057
1654
  function log(message) {
1058
1655
  process.stdout.write(message + "\n");
1059
1656
  }
@@ -1063,7 +1660,7 @@ function blank() {
1063
1660
  async function sectionTransition(doneLabel, nextLabel) {
1064
1661
  if (isQuiet()) return;
1065
1662
  blank();
1066
- log(`${c.blue("◇")} ${c.dim("Done:")} ${c.bold(doneLabel)}`);
1663
+ log(`${sky("◇")} ${c.dim("Done:")} ${c.bold(doneLabel)}`);
1067
1664
  await delay(320);
1068
1665
  for (const ch of [
1069
1666
  "│",
@@ -1074,15 +1671,15 @@ async function sectionTransition(doneLabel, nextLabel) {
1074
1671
  await delay(140);
1075
1672
  }
1076
1673
  await delay(200);
1077
- log(`${c.blue("◆")} ${c.bold("Next:")} ${nextLabel}`);
1674
+ log(`${sky("◆")} ${c.bold("Next:")} ${nextLabel}`);
1078
1675
  await delay(420);
1079
1676
  blank();
1080
1677
  }
1081
1678
  function step(message) {
1082
- process.stdout.write(` ${c.blue("→")} ${message}\n`);
1679
+ process.stdout.write(` ${sky("→")} ${message}\n`);
1083
1680
  }
1084
1681
  function ok(message) {
1085
- process.stdout.write(`${c.blue("✓")} ${message}\n`);
1682
+ process.stdout.write(`${sky("✓")} ${message}\n`);
1086
1683
  }
1087
1684
  function err(message) {
1088
1685
  process.stdout.write(`${c.red("✗")} ${message}\n`);
@@ -1091,13 +1688,13 @@ function warn(message) {
1091
1688
  process.stdout.write(`${c.yellow("⚠")} ${message}\n`);
1092
1689
  }
1093
1690
  function info(message) {
1094
- process.stdout.write(`${c.blue("ℹ")} ${message}\n`);
1691
+ process.stdout.write(`${sky("ℹ")} ${message}\n`);
1095
1692
  }
1096
1693
  async function spinner(startMsg, fn, stopWith, minMs = 480) {
1097
1694
  if (isQuiet()) {
1098
1695
  process.stdout.write(c.dim(`• ${startMsg}...`) + "\n");
1099
1696
  const r = await fn();
1100
- if (stopWith) process.stdout.write(`${c.blue("◇")} ${stopWith(r)}\n`);
1697
+ if (stopWith) process.stdout.write(`${sky("◇")} ${stopWith(r)}\n`);
1101
1698
  return r;
1102
1699
  }
1103
1700
  const frames = [
@@ -1114,11 +1711,11 @@ async function spinner(startMsg, fn, stopWith, minMs = 480) {
1114
1711
  ];
1115
1712
  let i = 0;
1116
1713
  let alive = true;
1117
- process.stdout.write(`${c.blue(frames[0])} ${startMsg}`);
1714
+ process.stdout.write(`${sky(frames[0])} ${startMsg}`);
1118
1715
  const tick = setInterval(() => {
1119
1716
  if (!alive) return;
1120
1717
  i = (i + 1) % frames.length;
1121
- process.stdout.write(`\r${c.blue(frames[i])} ${startMsg} `);
1718
+ process.stdout.write(`\r${sky(frames[i])} ${startMsg} `);
1122
1719
  }, 80);
1123
1720
  const started = Date.now();
1124
1721
  try {
@@ -1128,7 +1725,7 @@ async function spinner(startMsg, fn, stopWith, minMs = 480) {
1128
1725
  alive = false;
1129
1726
  clearInterval(tick);
1130
1727
  const msg = stopWith ? stopWith(result) : startMsg;
1131
- process.stdout.write(`\r\x1b[K${c.blue("◇")} ${msg}\n`);
1728
+ process.stdout.write(`\r\x1b[K${sky("◇")} ${msg}\n`);
1132
1729
  return result;
1133
1730
  } catch (e) {
1134
1731
  alive = false;
@@ -1235,23 +1832,23 @@ async function selectAgents(message, universal, others, detectedNames = []) {
1235
1832
  }));
1236
1833
  const locked = universal.length > 0 ? {
1237
1834
  title: "Universal",
1238
- items: universal.slice(0, 6).map((a) => ({
1835
+ items: universal.map((a) => ({
1239
1836
  value: a.name,
1240
1837
  label: a.displayName,
1241
1838
  fields: agentFields(a, true)
1242
1839
  })),
1243
- hiddenCount: Math.max(0, universal.length - 6)
1840
+ compact: true
1244
1841
  } : void 0;
1245
1842
  const res = await searchMultiselect({
1246
1843
  message,
1247
1844
  items,
1248
- maxVisible: 8,
1845
+ maxVisible: 5,
1249
1846
  required: false,
1250
1847
  initialSelected: detected.map((d) => d.name),
1251
1848
  lockedSection: locked,
1252
1849
  searchable: true,
1253
1850
  showDetail: true,
1254
- detailLines: 8,
1851
+ detailLines: 4,
1255
1852
  showSelectedSummary: true,
1256
1853
  includeAllOption: true,
1257
1854
  allLabel: `All additional (${uniqueOthers.length})`,
@@ -1273,12 +1870,12 @@ function installSummary(artifacts, agents, scopeLabel) {
1273
1870
  blank();
1274
1871
  const bar = c.dim("│");
1275
1872
  const corner = c.dim("└");
1276
- log(`${c.blue("◆")} ${c.bold("Install summary")}`);
1873
+ log(`${sky("◆")} ${c.bold("Install summary")}`);
1277
1874
  log(bar);
1278
- log(`${bar} ${c.blue("Scope")} ${scopeLabel}`);
1875
+ log(`${bar} ${sky("Scope")} ${scopeLabel}`);
1279
1876
  const universalCount = agents.filter((a) => a.skillsDir === ".agents/skills").length;
1280
1877
  const otherCount = agents.length - universalCount;
1281
- log(`${bar} ${c.blue("Targets")} ${agents.length} agent${agents.length === 1 ? "" : "s"}` + c.dim(` · ${universalCount} universal` + (otherCount ? ` · ${otherCount} additional` : "")));
1878
+ log(`${bar} ${sky("Targets")} ${agents.length} agent${agents.length === 1 ? "" : "s"}` + c.dim(` · ${universalCount} universal` + (otherCount ? ` · ${otherCount} additional` : "")));
1282
1879
  const names = agents.map((a) => a.displayName);
1283
1880
  const chunks = [];
1284
1881
  let line = "";
@@ -1297,7 +1894,7 @@ function installSummary(artifacts, agents, scopeLabel) {
1297
1894
  log(bar);
1298
1895
  for (const a of artifacts) {
1299
1896
  const desc = (a.description || "").replace(/\s+/g, " ").trim().slice(0, 52);
1300
- log(`${bar} ${c.blue("•")} ${c.bold(`${a.kind}/${a.id}`)}`);
1897
+ log(`${bar} ${sky("•")} ${c.bold(`${a.kind}/${a.id}`)}`);
1301
1898
  if (desc) log(`${bar} ${c.dim(desc)}`);
1302
1899
  }
1303
1900
  log(bar);
@@ -1307,16 +1904,29 @@ function installSummary(artifacts, agents, scopeLabel) {
1307
1904
  function preview(artifacts, agents) {
1308
1905
  installSummary(artifacts, agents, "preview (dry-run)");
1309
1906
  }
1907
+ async function resolveSourceWithUi(source, opts) {
1908
+ const resolved = await resolveSource(source, { confirmCache: async ({ url, error }) => {
1909
+ warn(`git clone failed for ${url}`);
1910
+ if (error) step(c.dim(error.split("\n")[0] || error));
1911
+ if (opts.yes || isQuiet()) {
1912
+ info("Using cached copy.");
1913
+ return true;
1914
+ }
1915
+ return confirm("Load from cache instead?");
1916
+ } });
1917
+ if (resolved.fromCache) info("Loaded source from cache.");
1918
+ return resolved;
1919
+ }
1310
1920
  async function cmdAdd(kind, source, selector, opts) {
1311
1921
  if (!source) throw new Error(`Missing source. e.g. agentry add ${kind} author/repo`);
1312
1922
  intro();
1313
1923
  await delayQuiet(180);
1314
1924
  await spinner("Parsing source…", () => parseSourceForUi(source), (p) => {
1315
1925
  const where = p.url || p.local || source;
1316
- return `${c.blue("Source:")} ${where}${p.ref ? c.dim(` @ ${p.ref}`) : ""}${p.subpath ? c.dim(` (${p.subpath})`) : ""}`;
1926
+ return `${theme.primary("Source:")} ${where}${p.ref ? c.dim(` @ ${p.ref}`) : ""}${p.subpath ? c.dim(` (${p.subpath})`) : ""}`;
1317
1927
  }, 520);
1318
1928
  await delayQuiet(280);
1319
- const { root, cleanup } = resolveSource(source);
1929
+ const { root, cleanup } = await resolveSourceWithUi(source, opts);
1320
1930
  try {
1321
1931
  const artifacts = await spinner("Discovering…", () => kind === "all" ? listAll(root) : select(root, kind, selector), (a) => {
1322
1932
  const noun = kind === "all" ? "artifact" : kind;
@@ -1329,7 +1939,7 @@ async function cmdAdd(kind, source, selector, opts) {
1329
1939
  await delayQuiet(320);
1330
1940
  blank();
1331
1941
  if (opts.dryRun) {
1332
- preview(artifacts, await resolveAgentList(opts));
1942
+ preview(artifacts, await resolveAgentList(opts, "install"));
1333
1943
  return;
1334
1944
  }
1335
1945
  let chosen = artifacts;
@@ -1347,8 +1957,11 @@ async function cmdAdd(kind, source, selector, opts) {
1347
1957
  await delayQuiet(200);
1348
1958
  blank();
1349
1959
  }
1350
- const agentList = await resolveAgentList(opts);
1351
- if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${agentList.length} agent${agentList.length === 1 ? "" : "s"} selected`, "install summary");
1960
+ const agentList = await resolveAgentList(opts, "install");
1961
+ const askScope = !opts.scopeExplicit && !opts.yes && !isQuiet();
1962
+ if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${agentList.length} agent${agentList.length === 1 ? "" : "s"} selected`, askScope ? "installation scope" : "install summary");
1963
+ if (!await ensureInstallScope(opts, "install")) return;
1964
+ if (askScope) await sectionTransition("scope set", "install summary");
1352
1965
  installAll(chosen, opts, agentList, source);
1353
1966
  } finally {
1354
1967
  cleanup();
@@ -1360,14 +1973,15 @@ async function delayQuiet(ms) {
1360
1973
  await delay(ms);
1361
1974
  }
1362
1975
  function parseSourceForUi(source) {
1363
- return import("./_chunks/source_parser.mjs").then((m) => m.parseSource(source));
1976
+ return parseSource(source);
1364
1977
  }
1365
1978
  async function cmdAddProfile(name, source, opts) {
1366
1979
  const { file, profile } = loadProfile(name);
1367
1980
  const repo = source || (profile.artifacts.skills[0]?.source ?? profile.artifacts.rules[0]?.source);
1368
1981
  if (!repo) throw new Error(`No source given and ${file} lists none. Try: agentry add profile ${name} author/repo`);
1369
- const agentList = await resolveAgentList(opts);
1370
- const { root, cleanup } = resolveSource(repo);
1982
+ const agentList = await resolveAgentList(opts, "install");
1983
+ if (!await ensureInstallScope(opts, "install")) return;
1984
+ const { root, cleanup } = await resolveSourceWithUi(repo, opts);
1371
1985
  try {
1372
1986
  const artifacts = [];
1373
1987
  const plural = (k) => k === "skill" ? "skills" : k === "rule" ? "rules" : k === "script" ? "scripts" : "agents";
@@ -1375,7 +1989,7 @@ async function cmdAddProfile(name, source, opts) {
1375
1989
  if (k === "profile") continue;
1376
1990
  const refs = profile.artifacts[plural(k)] ?? [];
1377
1991
  for (const ref of refs) {
1378
- const src = ref.source ? resolveSource(ref.source) : {
1992
+ const src = ref.source ? await resolveSourceWithUi(ref.source, opts) : {
1379
1993
  root,
1380
1994
  cleanup: () => {}
1381
1995
  };
@@ -1392,7 +2006,7 @@ async function cmdAddProfile(name, source, opts) {
1392
2006
  cleanup();
1393
2007
  }
1394
2008
  }
1395
- async function resolveAgentList(opts) {
2009
+ async function resolveAgentList(opts, action = "install") {
1396
2010
  if (opts.all) return listAgents();
1397
2011
  if (opts.agents.length) {
1398
2012
  const { agents, unknown } = resolveAgents(opts.agents);
@@ -1408,10 +2022,27 @@ async function resolveAgentList(opts) {
1408
2022
  const picked = detectedOthers.length ? detectedOthers : ["cursor"];
1409
2023
  return [...universal, ...others.filter((o) => picked.includes(o.name))];
1410
2024
  }
1411
- const selected = await selectAgents("Which agents do you want to install to?", visibleUniversal, others, detectedOthers);
2025
+ const selected = await selectAgents(agentsPrompt(action), visibleUniversal, others, detectedOthers);
1412
2026
  if (isCancelLike(selected)) throw new Error("Cancelled.");
1413
2027
  return selected;
1414
2028
  }
2029
+ async function ensureInstallScope(opts, action) {
2030
+ if (opts.scopeExplicit) return true;
2031
+ if (opts.yes || isQuiet()) return true;
2032
+ const { homedir } = await import("node:os");
2033
+ if (!isQuiet()) await sectionTransition(`${action === "install" ? "agents" : action} ready`, scopePrompt(action).toLowerCase());
2034
+ const picked = await selectInstallScope(scopePrompt(action), {
2035
+ projectDir: opts.dir || process.cwd(),
2036
+ homeDir: homedir()
2037
+ });
2038
+ if (picked == null || isCancelLike(picked)) {
2039
+ outro("Cancelled.");
2040
+ return false;
2041
+ }
2042
+ opts.scope = picked;
2043
+ opts.scopeExplicit = true;
2044
+ return true;
2045
+ }
1415
2046
  function installAll(artifacts, opts, agentList, source) {
1416
2047
  if (!artifacts.length) {
1417
2048
  warn("Nothing matched - nothing installed.");
@@ -1430,74 +2061,220 @@ function toInstallOpts(opts) {
1430
2061
  dryRun: opts.dryRun
1431
2062
  };
1432
2063
  }
1433
- async function cmdRemove(kind, selector, opts) {
1434
- const agentList = await resolveAgentList(opts);
2064
+ function resolveSourceOrSelector(arg1, arg2, opts) {
2065
+ if (arg1 && arg2) return {
2066
+ source: arg1,
2067
+ selector: arg2
2068
+ };
2069
+ if (!arg1) return {};
2070
+ if (isExplicitSource(arg1)) return { source: arg1 };
2071
+ if (isOwnerRepoShape(arg1)) {
2072
+ const bases = [{
2073
+ scope: "project",
2074
+ dir: opts.dir,
2075
+ agents: opts.agents,
2076
+ copy: opts.copy,
2077
+ dryRun: opts.dryRun
2078
+ }, {
2079
+ scope: "global",
2080
+ agents: opts.agents,
2081
+ copy: opts.copy,
2082
+ dryRun: opts.dryRun
2083
+ }];
2084
+ for (const base of bases) {
2085
+ const lock = readLock(base);
2086
+ if (Object.values(lock.items).some((e) => e.source && sourcesEqual(e.source, arg1))) return { source: arg1 };
2087
+ }
2088
+ }
2089
+ return { selector: arg1 };
2090
+ }
2091
+ async function cmdRemove(kind, arg1, arg2, opts) {
2092
+ intro();
2093
+ const { source, selector } = resolveSourceOrSelector(arg1, arg2, opts);
2094
+ if (source) await spinner("Resolving source…", () => parseSource(source), (p) => {
2095
+ const where = p.url || p.local || source;
2096
+ return `${theme.primary("Source:")} ${where}${p.ref ? c.dim(` @ ${p.ref}`) : ""}`;
2097
+ }, 420);
2098
+ if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition("prepare remove", "choose agents to remove from");
2099
+ const agentList = await resolveAgentList(opts, "remove");
2100
+ if (!await ensureInstallScope(opts, "remove")) return;
1435
2101
  const installOpts = toInstallOpts(opts);
1436
- const scope = [kind, selector].filter(Boolean).join("/");
2102
+ const scope = [
2103
+ kind,
2104
+ source,
2105
+ selector
2106
+ ].filter(Boolean).join(" / ");
1437
2107
  if (kind === "all") {
1438
2108
  let count = 0;
1439
- for (const t of ARTIFACT_KINDS) count += removeSelection(t, void 0, installOpts, agentList).removedKeys.length;
1440
- ok(`Removed ${count} item(s).`);
2109
+ let paths = 0;
2110
+ for (const t of ARTIFACT_KINDS) {
2111
+ const r = removeSelection(t, void 0, installOpts, agentList, source);
2112
+ count += r.removedKeys.length;
2113
+ paths += r.removedPaths.length;
2114
+ }
2115
+ ok(`Removed ${count} item(s)${paths ? c.dim(` · ${paths} path(s)`) : ""}${source ? c.dim(` from ${source}`) : ""}.`);
1441
2116
  return;
1442
2117
  }
1443
- const r = removeSelection(kind, selector, installOpts, agentList);
2118
+ const r = removeSelection(kind, selector, installOpts, agentList, source);
1444
2119
  if (!r.existed) {
1445
- warn(`Nothing installed at "${scope}".`);
2120
+ warn(`Nothing installed matching "${scope}".`);
1446
2121
  return;
1447
2122
  }
1448
2123
  for (const k of r.removedKeys) step(`removed ${k}`);
1449
- ok(`Removed ${r.removedKeys.length} item(s).`);
2124
+ for (const p of r.removedPaths.slice(0, 12)) step(c.dim(`deleted ${p}`));
2125
+ if (r.removedPaths.length > 12) step(c.dim(`…and ${r.removedPaths.length - 12} more paths`));
2126
+ ok(`Removed ${r.removedKeys.length} item(s)${r.removedPaths.length ? c.dim(` · ${r.removedPaths.length} path(s)`) : ""}${source ? c.dim(` from ${source}`) : ""}.`);
1450
2127
  }
1451
- function cmdList(source, kind) {
1452
- if (!source) throw new Error("Missing source. e.g. agentry list author/repo");
1453
- const { root, cleanup } = resolveSource(source);
2128
+ async function cmdList(source, kind, selector, opts) {
2129
+ if (!source) {
2130
+ cmdListInstalled(opts, kind, selector);
2131
+ return;
2132
+ }
2133
+ intro();
2134
+ await spinner("Parsing source…", () => parseSource(source), (p) => {
2135
+ const where = p.url || p.local || source;
2136
+ return `${theme.primary("Source:")} ${where}${p.ref ? c.dim(` @ ${p.ref}`) : ""}${p.subpath ? c.dim(` (${p.subpath})`) : ""}`;
2137
+ }, 420);
2138
+ const { root, cleanup } = await resolveSourceWithUi(source, opts);
1454
2139
  try {
1455
- for (const k of kind ? [kind] : ARTIFACT_KINDS) {
1456
- log(theme.info(`${k}s:`));
1457
- const items = listKind(root, k);
1458
- if (!items.length) {
1459
- step("(none)");
1460
- continue;
1461
- }
1462
- for (const it of items) {
1463
- const desc = (it.description || "").replace(/\s+/g, " ").slice(0, 80);
1464
- step(`${it.id}${desc ? " - " + desc : ""}`);
1465
- }
1466
- }
2140
+ printArtifactList(await spinner("Discovering…", () => {
2141
+ if (kind) return select(root, kind, selector);
2142
+ return listAll(root);
2143
+ }, (a) => `Found ${a.length} artifact${a.length === 1 ? "" : "s"}`, 520), {
2144
+ title: "Available in source",
2145
+ subtitle: source
2146
+ });
1467
2147
  } finally {
1468
2148
  cleanup();
1469
2149
  }
1470
2150
  }
1471
- function cmdListInstalled(opts) {
1472
- const installed = listInstalled(toInstallOpts(opts));
2151
+ function cmdListInstalled(opts, kind, selector) {
2152
+ intro();
2153
+ let installed = listInstalled(toInstallOpts(opts));
2154
+ if (kind) installed = installed.filter((it) => it.kind === kind || it.id.startsWith(kind + "/"));
2155
+ if (selector && kind) {
2156
+ const rel = `${kind}/${selector}`;
2157
+ installed = installed.filter((it) => it.id === rel || it.id.startsWith(rel + "/") || it.id.endsWith("/" + selector) || it.id === `${kind}/${selector}`);
2158
+ } else if (selector) installed = installed.filter((it) => it.id.includes(selector));
1473
2159
  if (!installed.length) {
1474
- info("Nothing installed.");
2160
+ info(kind || selector ? `Nothing installed matching that filter.` : "Nothing installed.");
2161
+ step(c.dim("Try: agentry add skills mhmdreza-rafiei/agent-tools"));
2162
+ return;
2163
+ }
2164
+ const bar = c.dim("│");
2165
+ const corner = c.dim("└");
2166
+ blank();
2167
+ log(`${sky("◆")} ${c.bold("Installed")}`);
2168
+ log(bar);
2169
+ log(`${bar} ${sky("Scope")} ${opts.scope === "global" ? "global (~)" : `project (${opts.dir || process.cwd()})`}`);
2170
+ log(`${bar} ${sky("Total")} ${installed.length} artifact${installed.length === 1 ? "" : "s"}`);
2171
+ log(bar);
2172
+ const byKind = /* @__PURE__ */ new Map();
2173
+ for (const it of installed) {
2174
+ const k = it.kind || it.id.split("/")[0];
2175
+ const arr = byKind.get(k) || [];
2176
+ arr.push(it);
2177
+ byKind.set(k, arr);
2178
+ }
2179
+ for (const k of ARTIFACT_KINDS) {
2180
+ const items = byKind.get(k);
2181
+ if (!items?.length) continue;
2182
+ log(`${bar} ${c.bold(k + "s")} ${c.dim(`(${items.length})`)}`);
2183
+ for (const it of items) {
2184
+ const id = it.id.includes("/") ? it.id.slice(it.id.indexOf("/") + 1) : it.id;
2185
+ log(`${bar} ${sky("•")} ${c.bold(id)}`);
2186
+ const agents = it.agents?.length ? it.agents.join(", ") : "—";
2187
+ log(`${bar} ${c.dim(`agents: ${agents}`)}`);
2188
+ if (it.source) log(`${bar} ${c.dim(`source: ${it.source}`)}`);
2189
+ }
2190
+ log(bar);
2191
+ }
2192
+ log(`${corner} ${c.dim("agentry remove <kind> [source] [selector] · agentry update <kind> [source]")}`);
2193
+ blank();
2194
+ }
2195
+ function printArtifactList(artifacts, meta) {
2196
+ const bar = c.dim("│");
2197
+ const corner = c.dim("└");
2198
+ blank();
2199
+ log(`${sky("◆")} ${c.bold(meta.title)}`);
2200
+ log(bar);
2201
+ if (meta.subtitle) log(`${bar} ${sky("From")} ${meta.subtitle}`);
2202
+ log(`${bar} ${sky("Total")} ${artifacts.length}`);
2203
+ log(bar);
2204
+ if (!artifacts.length) {
2205
+ log(`${bar} ${c.dim("(none)")}`);
2206
+ log(bar);
2207
+ log(`${corner}`);
2208
+ blank();
1475
2209
  return;
1476
2210
  }
1477
- for (const it of installed) step(`${it.id} -> ${it.agents.join(", ")}`);
2211
+ const byKind = /* @__PURE__ */ new Map();
2212
+ for (const a of artifacts) {
2213
+ const arr = byKind.get(a.kind) || [];
2214
+ arr.push(a);
2215
+ byKind.set(a.kind, arr);
2216
+ }
2217
+ for (const k of ARTIFACT_KINDS) {
2218
+ const items = byKind.get(k);
2219
+ if (!items?.length) continue;
2220
+ log(`${bar} ${c.bold(k + "s")} ${c.dim(`(${items.length})`)}`);
2221
+ for (const it of items) {
2222
+ const desc = (it.description || "").replace(/\s+/g, " ").trim().slice(0, 56);
2223
+ log(`${bar} ${sky("•")} ${c.bold(it.id)}`);
2224
+ if (desc) log(`${bar} ${c.dim(desc)}`);
2225
+ }
2226
+ log(bar);
2227
+ }
2228
+ log(`${corner} ${c.dim("agentry add <kind> <source> [selector]")}`);
2229
+ blank();
1478
2230
  }
1479
- async function cmdUpdateAssets(kind, source, selector, opts) {
1480
- const installOpts = toInstallOpts(opts);
1481
- const agentList = await resolveAgentList(opts);
2231
+ async function cmdUpdateAssets(kind, arg1, arg2, opts) {
2232
+ intro();
2233
+ const { source, selector } = resolveSourceOrSelector(arg1, arg2, opts);
1482
2234
  if (source) {
1483
- const { root, cleanup } = resolveSource(source);
2235
+ await spinner("Parsing source…", () => parseSource(source), (p) => {
2236
+ const where = p.url || p.local || source;
2237
+ return `${theme.primary("Source:")} ${where}${p.ref ? c.dim(` @ ${p.ref}`) : ""}`;
2238
+ }, 520);
2239
+ const { root, cleanup } = await resolveSourceWithUi(source, opts);
1484
2240
  try {
1485
- const artifacts = kind === "all" ? listAll(root) : select(root, kind, selector);
2241
+ const artifacts = await spinner("Discovering…", () => kind === "all" ? listAll(root) : select(root, kind, selector), (a) => `Found ${a.length} to update`, 620);
1486
2242
  if (!artifacts.length) {
1487
2243
  warn(`Nothing found in ${source}.`);
1488
2244
  return;
1489
2245
  }
1490
- installAll(artifacts, opts, agentList, source);
2246
+ 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);
2250
+ if (isCancelLike(picked)) {
2251
+ outro("Cancelled.");
2252
+ return;
2253
+ }
2254
+ 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");
2257
+ const agentList = await resolveAgentList(opts, "update");
2258
+ if (!await ensureInstallScope(opts, "update")) return;
2259
+ if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition(`${agentList.length} agent${agentList.length === 1 ? "" : "s"} selected`, "update summary");
2260
+ installAll(chosen, opts, agentList, source);
1491
2261
  } finally {
1492
2262
  cleanup();
1493
2263
  }
1494
2264
  return;
1495
2265
  }
1496
- const installed = listInstalled(installOpts).filter((it) => kind === "all" || it.id.startsWith(kind + "/"));
2266
+ if (!await ensureInstallScope(opts, "update")) return;
2267
+ let installed = listInstalled(toInstallOpts(opts)).filter((it) => kind === "all" || it.id.startsWith(kind + "/"));
2268
+ if (selector) {
2269
+ const rel = `${kind}/${selector}`;
2270
+ installed = installed.filter((it) => it.id === rel || it.id.startsWith(rel + "/") || it.id.endsWith("/" + selector));
2271
+ }
1497
2272
  if (!installed.length) {
1498
2273
  warn(`No installed ${kind} recorded in lockfile. Provide a source.`);
1499
2274
  return;
1500
2275
  }
2276
+ if (!isQuiet() && !opts.agents.length && !opts.all && !opts.yes) await sectionTransition("lockfile sources", "choose agents to update for");
2277
+ const agentList = await resolveAgentList(opts, "update");
1501
2278
  const bySource = /* @__PURE__ */ new Map();
1502
2279
  for (const it of installed) if (it.source) {
1503
2280
  const arr = bySource.get(it.source) || [];
@@ -1509,22 +2286,216 @@ async function cmdUpdateAssets(kind, source, selector, opts) {
1509
2286
  return;
1510
2287
  }
1511
2288
  for (const [s, ids] of bySource) {
1512
- const { root, cleanup } = resolveSource(s);
2289
+ info(`Updating from ${s}…`);
2290
+ const { root, cleanup } = await resolveSourceWithUi(s, opts);
1513
2291
  try {
1514
- installAll(ids.map((id) => select(root, id.split("/")[0], id.split("/").slice(1).join("/") || void 0)).flat(), opts, agentList, s);
2292
+ installAll(ids.map((id) => {
2293
+ const [k, ...rest] = id.split("/");
2294
+ return select(root, k, rest.join("/") || void 0);
2295
+ }).flat(), opts, agentList, s);
1515
2296
  } finally {
1516
2297
  cleanup();
1517
2298
  }
1518
2299
  }
1519
2300
  }
2301
+ async function promptText(message, initial) {
2302
+ if (isQuiet()) return initial || null;
2303
+ const res = await clack.text({
2304
+ message,
2305
+ initialValue: initial || "",
2306
+ placeholder: initial
2307
+ });
2308
+ if (clack.isCancel(res)) return null;
2309
+ return String(res).trim() || null;
2310
+ }
2311
+ async function pickFromSource(kind, source, message, opts) {
2312
+ const { root, cleanup } = await resolveSourceWithUi(source, opts);
2313
+ try {
2314
+ const artifacts = listKind(root, kind);
2315
+ if (!artifacts.length) {
2316
+ warn(`No ${kind}s in ${source}.`);
2317
+ return [];
2318
+ }
2319
+ if (isQuiet() || artifacts.length === 1) return artifacts.map((a) => ({
2320
+ id: a.id,
2321
+ source
2322
+ }));
2323
+ const picked = await selectArtifacts(message, artifacts);
2324
+ if (isCancelLike(picked) || !picked) return [];
2325
+ return picked.map((a) => ({
2326
+ id: a.id,
2327
+ source
2328
+ }));
2329
+ } finally {
2330
+ cleanup();
2331
+ }
2332
+ }
2333
+ async function cmdInit(kind, nameArg, categoryArg, opts) {
2334
+ intro();
2335
+ const baseDir = opts.dir || process.cwd();
2336
+ let name = nameArg;
2337
+ let category = categoryArg || null;
2338
+ if (!name && !isQuiet()) name = await promptText(`Name for the new ${kind}`) || void 0;
2339
+ if (!name) throw new Error(`Missing name. e.g. agentry init ${kind} my-${kind}`);
2340
+ let description = opts.description;
2341
+ if (!description && !opts.yes && !isQuiet()) description = await promptText("Description (optional)", `${name} ${kind}`) || `${name} ${kind}`;
2342
+ description = description || `${name} ${kind}`;
2343
+ if (kind === "skill") {
2344
+ const reference = opts.reference !== false;
2345
+ if (opts.dryRun) {
2346
+ info(`Would create skills/${category ? category + "/" : ""}${name}/SKILL.md${reference ? " + references/TEMPLATE.md" : ""}`);
2347
+ return;
2348
+ }
2349
+ const r = scaffoldSkill({
2350
+ name,
2351
+ category,
2352
+ description,
2353
+ reference,
2354
+ baseDir
2355
+ });
2356
+ for (const p of r.paths) step(p);
2357
+ ok(`Initialized skill ${r.id}`);
2358
+ return;
2359
+ }
2360
+ if (kind === "agent" || kind === "rule") {
2361
+ if (opts.dryRun) {
2362
+ info(`Would create ${kind}s/${category ? category + "/" : ""}${name}.mdc`);
2363
+ return;
2364
+ }
2365
+ const r = scaffoldMdc({
2366
+ kind,
2367
+ name,
2368
+ category,
2369
+ description,
2370
+ alwaysApply: opts.alwaysApply,
2371
+ baseDir
2372
+ });
2373
+ for (const p of r.paths) step(p);
2374
+ ok(`Initialized ${kind} ${r.id}`);
2375
+ return;
2376
+ }
2377
+ if (kind === "script") {
2378
+ if (opts.dryRun) {
2379
+ info(`Would create scripts/${category ? category + "/" : ""}${name}/`);
2380
+ return;
2381
+ }
2382
+ const r = scaffoldScript({
2383
+ name,
2384
+ category,
2385
+ description,
2386
+ baseDir
2387
+ });
2388
+ for (const p of r.paths) step(p);
2389
+ ok(`Initialized script ${r.id}`);
2390
+ return;
2391
+ }
2392
+ if (kind === "profile") {
2393
+ await initProfile(name, description, opts, baseDir);
2394
+ return;
2395
+ }
2396
+ throw new Error(`Cannot init kind: ${kind}`);
2397
+ }
2398
+ async function initProfile(name, description, opts, baseDir) {
2399
+ const initKinds = opts.initKinds || {};
2400
+ const wantAll = opts.all;
2401
+ let wantSkills = wantAll || initKinds.skills !== void 0;
2402
+ let wantAgents = wantAll || initKinds.agents !== void 0;
2403
+ let wantRules = wantAll || initKinds.rules !== void 0;
2404
+ let wantScripts = wantAll || initKinds.scripts !== void 0;
2405
+ if (!wantSkills && !wantAgents && !wantRules && !wantScripts && !opts.yes && !isQuiet()) {
2406
+ const res = await clack.multiselect({
2407
+ message: "What should this profile include?",
2408
+ options: [
2409
+ {
2410
+ value: "skills",
2411
+ label: "Skills"
2412
+ },
2413
+ {
2414
+ value: "agents",
2415
+ label: "Agents"
2416
+ },
2417
+ {
2418
+ value: "rules",
2419
+ label: "Rules"
2420
+ },
2421
+ {
2422
+ value: "scripts",
2423
+ label: "Scripts"
2424
+ }
2425
+ ],
2426
+ required: false
2427
+ });
2428
+ if (clack.isCancel(res)) {
2429
+ outro("Cancelled.");
2430
+ return;
2431
+ }
2432
+ const set = new Set(res);
2433
+ wantSkills = set.has("skills");
2434
+ wantAgents = set.has("agents");
2435
+ wantRules = set.has("rules");
2436
+ wantScripts = set.has("scripts");
2437
+ }
2438
+ const agentList = opts.agents.length ? (await resolveAgentList(opts, "install")).map((a) => a.name) : ["cursor"];
2439
+ const skills = [];
2440
+ const rules = [];
2441
+ const agentsArtifacts = [];
2442
+ const scripts = [];
2443
+ async function fill(flag, enabled, k, into) {
2444
+ if (!enabled) return;
2445
+ const src = typeof flag === "string" ? flag : void 0;
2446
+ if (src) {
2447
+ const picked = await pickFromSource(k, src, `Select ${k}s for profile`, opts);
2448
+ into.push(...picked);
2449
+ return;
2450
+ }
2451
+ if (isQuiet() || opts.yes) return;
2452
+ const ask = await promptText(`Source for ${k}s (author/repo, URL, or local path — empty to skip)`);
2453
+ if (!ask) return;
2454
+ const picked = await pickFromSource(k, ask, `Select ${k}s for profile`, opts);
2455
+ into.push(...picked);
2456
+ }
2457
+ await fill(initKinds.skills, wantSkills, "skill", skills);
2458
+ await fill(initKinds.agents, wantAgents, "agent", agentsArtifacts);
2459
+ await fill(initKinds.rules, wantRules, "rule", rules);
2460
+ await fill(initKinds.scripts, wantScripts, "script", scripts);
2461
+ if (opts.dryRun) {
2462
+ info(`Would write profiles/${name}.yaml with ${skills.length} skills, ${agentsArtifacts.length} agents, ${rules.length} rules, ${scripts.length} scripts`);
2463
+ return;
2464
+ }
2465
+ const r = scaffoldProfile({
2466
+ name,
2467
+ description,
2468
+ agents: agentList,
2469
+ skills,
2470
+ rules,
2471
+ agentsArtifacts,
2472
+ scripts,
2473
+ baseDir
2474
+ });
2475
+ for (const p of r.paths) step(p);
2476
+ ok(`Initialized profile ${r.id}`);
2477
+ }
2478
+ function takeOptValue(argv, i) {
2479
+ const next = argv[i + 1];
2480
+ if (next && !next.startsWith("-")) return {
2481
+ value: next,
2482
+ next: i + 1
2483
+ };
2484
+ return {
2485
+ value: true,
2486
+ next: i
2487
+ };
2488
+ }
1520
2489
  function parseArgs(argv) {
1521
2490
  const opts = {
1522
2491
  scope: "project",
2492
+ scopeExplicit: false,
1523
2493
  agents: [],
1524
2494
  copy: false,
1525
2495
  dryRun: false,
1526
2496
  yes: false,
1527
- all: false
2497
+ all: false,
2498
+ initKinds: {}
1528
2499
  };
1529
2500
  const positional = [];
1530
2501
  const flags = {};
@@ -1532,15 +2503,19 @@ function parseArgs(argv) {
1532
2503
  const a = argv[i];
1533
2504
  if (a === "-g" || a === "--global") {
1534
2505
  opts.scope = "global";
2506
+ opts.scopeExplicit = true;
1535
2507
  flags.global = true;
1536
- } else if (a === "--project") {
2508
+ } else if (a === "-p" || a === "--project") {
1537
2509
  opts.scope = "project";
2510
+ opts.scopeExplicit = true;
1538
2511
  flags.project = true;
1539
2512
  } else if (a === "-y" || a === "--yes") {
1540
2513
  opts.yes = true;
1541
2514
  flags.yes = true;
1542
- } else if (a === "--dir") {
2515
+ } else if (a === "-d" || a === "--dir") {
1543
2516
  opts.dir = argv[++i];
2517
+ opts.scope = "project";
2518
+ opts.scopeExplicit = true;
1544
2519
  flags.dir = opts.dir;
1545
2520
  } else if (a === "-a" || a === "--agent") {
1546
2521
  opts.agents.push(argv[++i]);
@@ -1554,6 +2529,38 @@ function parseArgs(argv) {
1554
2529
  } else if (a === "--all") {
1555
2530
  opts.all = true;
1556
2531
  flags.all = true;
2532
+ } else if (a === "--description") {
2533
+ opts.description = argv[++i];
2534
+ flags.description = opts.description;
2535
+ } else if (a === "--alwaysApply" || a === "--always-apply") {
2536
+ opts.alwaysApply = true;
2537
+ flags.alwaysApply = true;
2538
+ } else if (a === "--reference") {
2539
+ opts.reference = true;
2540
+ flags.reference = true;
2541
+ } else if (a === "--no-reference") {
2542
+ opts.reference = false;
2543
+ flags.reference = false;
2544
+ } else if (a === "--skills") {
2545
+ const { value, next } = takeOptValue(argv, i);
2546
+ opts.initKinds.skills = value;
2547
+ i = next;
2548
+ flags.skills = true;
2549
+ } else if (a === "--agents") {
2550
+ const { value, next } = takeOptValue(argv, i);
2551
+ opts.initKinds.agents = value;
2552
+ i = next;
2553
+ flags.agents = true;
2554
+ } else if (a === "--rules") {
2555
+ const { value, next } = takeOptValue(argv, i);
2556
+ opts.initKinds.rules = value;
2557
+ i = next;
2558
+ flags.rules = true;
2559
+ } else if (a === "--scripts") {
2560
+ const { value, next } = takeOptValue(argv, i);
2561
+ opts.initKinds.scripts = value;
2562
+ i = next;
2563
+ flags.scripts = true;
1557
2564
  } else if (a === "-v" || a === "--version") flags.version = true;
1558
2565
  else if (a === "-h" || a === "--help") flags.help = true;
1559
2566
  else positional.push(a);
@@ -1599,7 +2606,11 @@ async function runUpdateSelf() {
1599
2606
  function runNpmInstall(spec) {
1600
2607
  spinner(`Updating agentry CLI`, () => {
1601
2608
  try {
1602
- execSync(`npm install -g ${spec}`, { stdio: "inherit" });
2609
+ execFileSync("npm", [
2610
+ "install",
2611
+ "-g",
2612
+ spec
2613
+ ], { stdio: "inherit" });
1603
2614
  } catch {
1604
2615
  err("Update failed. Once published, run: npm install -g " + spec);
1605
2616
  process.exit(1);
@@ -1609,7 +2620,11 @@ function runNpmInstall(spec) {
1609
2620
  function runUninstall() {
1610
2621
  spinner("Uninstalling agentry CLI", () => {
1611
2622
  try {
1612
- execSync(`npm uninstall -g ${name}`, { stdio: "inherit" });
2623
+ execFileSync("npm", [
2624
+ "uninstall",
2625
+ "-g",
2626
+ name
2627
+ ], { stdio: "inherit" });
1613
2628
  } catch {
1614
2629
  err("Uninstall failed. Try: npm uninstall -g " + name);
1615
2630
  process.exit(1);
@@ -1650,12 +2665,21 @@ async function main() {
1650
2665
  return;
1651
2666
  }
1652
2667
  if (action === "list") {
1653
- if (positional.length === 1) {
1654
- cmdListInstalled(opts);
2668
+ const a1 = positional[1];
2669
+ const a2 = positional[2];
2670
+ const a3 = positional[3];
2671
+ if (!a1) {
2672
+ await cmdList(void 0, void 0, void 0, opts);
1655
2673
  return;
1656
2674
  }
1657
- const k = positional[2] ? normalizeKind(positional[2]) : void 0;
1658
- cmdList(positional[1], k && isKind(k) ? k : void 0);
2675
+ const k1 = normalizeKind(a1);
2676
+ if (isKind(k1) && !isExplicitSource(a1) && !/^[\w.-]+\/[\w.-]+$/.test(a1)) {
2677
+ await cmdList(void 0, k1, a2, opts);
2678
+ return;
2679
+ }
2680
+ const kind = a2 ? normalizeKind(a2) : void 0;
2681
+ if (kind && kind !== "all" && !isKind(kind)) throw new Error(`Unknown kind: ${kind}`);
2682
+ await cmdList(a1, kind && isKind(kind) ? kind : void 0, a3, opts);
1659
2683
  return;
1660
2684
  }
1661
2685
  if (action === "add") {
@@ -1673,7 +2697,13 @@ async function main() {
1673
2697
  const kind = positional[1] ? normalizeKind(positional[1]) : void 0;
1674
2698
  if (!kind) throw new Error("Missing kind. e.g. agentry remove skills enhance-prompt");
1675
2699
  if (kind !== "all" && !isKind(kind)) throw new Error(`Unknown kind: ${kind}`);
1676
- await cmdRemove(kind, positional[2], opts);
2700
+ await cmdRemove(kind, positional[2], positional[3], opts);
2701
+ return;
2702
+ }
2703
+ if (action === "init") {
2704
+ const kind = positional[1] ? normalizeKind(positional[1]) : void 0;
2705
+ if (!kind || !isKind(kind)) throw new Error("Missing kind. e.g. agentry init skill my-skill [category]");
2706
+ await cmdInit(kind, positional[2], positional[3], opts);
1677
2707
  return;
1678
2708
  }
1679
2709
  err(`Unknown action: ${action}`);