shoplazza-ai-dev-cli 0.1.1 → 0.1.4

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as __toESM, n as __exportAll } from "./_chunks/rolldown-runtime.mjs";
2
+ import { r as __toESM } from "./_chunks/rolldown-runtime.mjs";
3
3
  import { l as pD, u as require_picocolors } from "./_chunks/libs/@clack/core.mjs";
4
4
  import { a as Y, c as ve, i as Se, l as xe, n as M, o as be, r as Me, s as fe, t as Ie, u as ye } from "./_chunks/libs/@clack/prompts.mjs";
5
5
  import "./_chunks/libs/@kwsites/file-exists.mjs";
@@ -11,12 +11,13 @@ import "./_chunks/libs/core-util-is.mjs";
11
11
  import "./_chunks/libs/inherits.mjs";
12
12
  import "./_chunks/libs/immediate.mjs";
13
13
  import { exec, execSync, spawnSync } from "child_process";
14
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
14
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "fs";
15
15
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
16
- import { homedir, platform, tmpdir } from "os";
17
16
  import { URL as URL$1, fileURLToPath } from "url";
18
17
  import { stripVTControlCharacters } from "node:util";
18
+ import { homedir, hostname, platform, tmpdir } from "os";
19
19
  import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
20
+ import { createServer } from "http";
20
21
  import { gunzipSync } from "node:zlib";
21
22
  import { mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
22
23
  import { dirname as dirname$1, normalize as normalize$1, resolve as resolve$1, sep as sep$1 } from "node:path";
@@ -24,7 +25,6 @@ import { parse } from "yaml";
24
25
  import * as readline from "readline";
25
26
  import { Writable } from "stream";
26
27
  import { createHash } from "crypto";
27
- import { createServer } from "http";
28
28
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
29
29
  function getOwnerRepo(parsed) {
30
30
  if (parsed.type === "local") return null;
@@ -79,6 +79,11 @@ function decomposePortalRef(ref) {
79
79
  }
80
80
  return null;
81
81
  }
82
+ function portalRefToSkillPath(ref) {
83
+ const m = ref.match(/^(?:@public|@teams\/[^/]+)\/(skills|rules)\/([^/]+)$/);
84
+ if (!m) return null;
85
+ return `${m[1]}/${m[2]}/SKILL.md`;
86
+ }
82
87
  function sanitizeSubpath(subpath) {
83
88
  const segments = subpath.replace(/\\/g, "/").split("/");
84
89
  for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
@@ -313,12 +318,13 @@ const agents = {
313
318
  globalSkillsDir: join(codexHome, "skills"),
314
319
  detectInstalled: async () => {
315
320
  return existsSync(codexHome) || existsSync("/etc/codex");
316
- }
321
+ },
322
+ hiddenInPicker: true
317
323
  },
318
324
  cursor: {
319
325
  name: "cursor",
320
326
  displayName: "Cursor",
321
- skillsDir: ".agents/skills",
327
+ skillsDir: ".cursor/skills",
322
328
  globalSkillsDir: join(home, ".cursor/skills"),
323
329
  detectInstalled: async () => {
324
330
  return existsSync(join(home, ".cursor"));
@@ -331,6 +337,9 @@ async function detectInstalledAgents() {
331
337
  installed: await config.detectInstalled()
332
338
  })))).filter((r) => r.installed).map((r) => r.type);
333
339
  }
340
+ function getPickerVisibleAgents() {
341
+ return Object.entries(agents).filter(([_, config]) => !config.hiddenInPicker).map(([type]) => type);
342
+ }
334
343
  function getUniversalAgents() {
335
344
  return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
336
345
  }
@@ -340,8 +349,8 @@ function isUniversalAgent(type) {
340
349
  function getStoreRoot(_scope, _cwd) {
341
350
  return join(home, FORGE_STORE_DIRNAME, "store");
342
351
  }
343
- function getAgentEntryRoot(agent, scope, cwd) {
344
- if (scope === "global") return getAgentHome(agent);
352
+ function getAgentEntryRoot(_agent, scope, cwd) {
353
+ if (scope === "global") return home;
345
354
  return cwd ?? process.cwd();
346
355
  }
347
356
  function getInstallTargets(agent, component, scope, cwd) {
@@ -490,7 +499,153 @@ function configCommand(args) {
490
499
  console.error("Available: get | set | unset | list | path");
491
500
  process.exit(2);
492
501
  }
493
- const FORGE_HOOK_BASENAME$1 = "track-skill-read.mjs";
502
+ const FORGE_CURSOR_EVENTS = ["beforeReadFile", "beforeSubmitPrompt"];
503
+ const CURSOR_HOOK_SCRIPT_BASENAME = "track-skill-cursor.mjs";
504
+ const LEGACY_CURSOR_HOOK_BASENAME = "track-skill-read.mjs";
505
+ function getCursorHookScript(portalUrl) {
506
+ const portal = portalUrl.replace(/\/$/, "");
507
+ return `#!/usr/bin/env node
508
+ // forge:track-skill-cursor
509
+ // Auto-generated by ai-dev-cli. Do not edit; run \`ai-dev-cli add\` to refresh.
510
+ // Cursor (2.4+) has no dedicated skill_invoked event. We listen on:
511
+ // - beforeReadFile → agent reads SKILL.md when natural-language-loading
512
+ // a skill into context.
513
+ // - beforeSubmitPrompt → user types \`/<skill-name>\` (slash command);
514
+ // Cursor loads the skill internally without reading
515
+ // SKILL.md, so we parse the prompt instead.
516
+ // IMPORTANT: both are permission-gating hooks. We MUST output \`{}\` and
517
+ // exit 0 on every path so Cursor never blocks user input or file reads.
518
+ import fs from 'node:fs';
519
+ import os from 'node:os';
520
+ import path from 'node:path';
521
+
522
+ const PORTAL = ${JSON.stringify(portal)};
523
+
524
+ // Always allow; never block on any code path.
525
+ function allowAndExit() {
526
+ try { process.stdout.write('{}'); } catch {}
527
+ process.exit(0);
528
+ }
529
+
530
+ // Hard ceiling: even if fetch hangs past its own 2s timeout, free Cursor
531
+ // after 5s total.
532
+ const exitTimer = setTimeout(allowAndExit, 5000);
533
+ exitTimer.unref?.();
534
+
535
+ function readManifest(dir) {
536
+ try {
537
+ const raw = fs.readFileSync(path.join(dir, '.forge-source.json'), 'utf8');
538
+ return JSON.parse(raw);
539
+ } catch {
540
+ return null;
541
+ }
542
+ }
543
+
544
+ // Walk up parent directories from a SKILL.md path looking for the
545
+ // .forge-source.json manifest forge writes at install time. Bounded depth
546
+ // so a stray read on an unrelated file can't walk to filesystem root.
547
+ function manifestFromFilePath(filePath) {
548
+ if (!filePath || typeof filePath !== 'string') return null;
549
+ let dir = path.dirname(filePath);
550
+ for (let i = 0; i < 8; i++) {
551
+ const m = readManifest(dir);
552
+ if (m) return m;
553
+ const parent = path.dirname(dir);
554
+ if (parent === dir) break;
555
+ dir = parent;
556
+ }
557
+ return null;
558
+ }
559
+
560
+ // Resolve a skill name (as typed in a slash command) to its forge manifest.
561
+ // Checks the IDE entry first (~/.cursor/skills/<name>/), then falls back
562
+ // to the canonical store (~/.ai-dev-cli/store/skills|plugins) which is
563
+ // authoritative regardless of where the IDE entry symlink lives.
564
+ function manifestFromSkillName(skillId) {
565
+ if (!skillId || typeof skillId !== 'string') return null;
566
+ const name = skillId.includes(':') ? skillId.split(':').pop() : skillId;
567
+ if (!name) return null;
568
+ const cursorHome = path.join(os.homedir(), '.cursor');
569
+ const m = readManifest(path.join(cursorHome, 'skills', name));
570
+ if (m) return m;
571
+ const storeRoot = path.join(os.homedir(), '.ai-dev-cli', 'store');
572
+ for (const sub of ['skills', 'plugins']) {
573
+ try {
574
+ const root = path.join(storeRoot, sub);
575
+ for (const entry of fs.readdirSync(root)) {
576
+ if (sub === 'skills') {
577
+ if (!entry.endsWith('__' + name)) continue;
578
+ const m2 = readManifest(path.join(root, entry));
579
+ if (m2 && m2.name === name) return m2;
580
+ } else {
581
+ const m2 = readManifest(path.join(root, entry, 'skills', name));
582
+ if (m2 && m2.name === name) return m2;
583
+ }
584
+ }
585
+ } catch {
586
+ /* store dir missing — fall through */
587
+ }
588
+ }
589
+ return null;
590
+ }
591
+
592
+ let buf = '';
593
+ process.stdin.on('data', (c) => (buf += c));
594
+ process.stdin.on('end', () => {
595
+ try {
596
+ const ev = JSON.parse(buf);
597
+ let manifest = null;
598
+
599
+ if (ev.hook_event_name === 'beforeReadFile') {
600
+ const filePath = ev.file_path;
601
+ if (
602
+ typeof filePath !== 'string' ||
603
+ (!filePath.endsWith(path.sep + 'SKILL.md') && !filePath.endsWith('/SKILL.md'))
604
+ ) {
605
+ return allowAndExit();
606
+ }
607
+ manifest = manifestFromFilePath(filePath);
608
+ } else if (ev.hook_event_name === 'beforeSubmitPrompt') {
609
+ const prompt = typeof ev.prompt === 'string' ? ev.prompt.trimStart() : '';
610
+ // Slash commands are at the start of the prompt; the token after \`/\`
611
+ // up to the first whitespace is the skill identifier the user typed.
612
+ const m = prompt.match(/^\\/([^\\s]+)/);
613
+ if (!m) return allowAndExit();
614
+ manifest = manifestFromSkillName(m[1]);
615
+ } else {
616
+ return allowAndExit();
617
+ }
618
+
619
+ if (!manifest?.ref) return allowAndExit();
620
+
621
+ fetch(PORTAL + '/events', {
622
+ method: 'POST',
623
+ headers: { 'Content-Type': 'application/json' },
624
+ body: JSON.stringify({
625
+ event_type: 'skill_invoked',
626
+ asset_ref: manifest.ref,
627
+ install_mode: manifest.install_mode || 'cursor',
628
+ metadata: {
629
+ parent_plugin_ref: manifest.parent_plugin_ref || null,
630
+ },
631
+ }),
632
+ signal: AbortSignal.timeout(2000),
633
+ })
634
+ .catch(() => {})
635
+ .finally(allowAndExit);
636
+ } catch {
637
+ allowAndExit();
638
+ }
639
+ });
640
+ `;
641
+ }
642
+ function isForgeOwnedCommand(command) {
643
+ if (typeof command !== "string") return false;
644
+ return command.includes(CURSOR_HOOK_SCRIPT_BASENAME) || command.includes(LEGACY_CURSOR_HOOK_BASENAME);
645
+ }
646
+ function isFlatEntry(e) {
647
+ return !!e && typeof e === "object" && typeof e.command === "string";
648
+ }
494
649
  async function upsertCursorHooks(hooksJsonPath, scriptPath) {
495
650
  let raw = "";
496
651
  try {
@@ -498,24 +653,37 @@ async function upsertCursorHooks(hooksJsonPath, scriptPath) {
498
653
  } catch {}
499
654
  const cfg = raw.trim() ? JSON.parse(raw) : { version: 1 };
500
655
  if (!cfg.hooks) cfg.hooks = {};
501
- if (!Array.isArray(cfg.hooks.postToolUse)) cfg.hooks.postToolUse = [];
502
- let readMatcher = cfg.hooks.postToolUse.find((m) => m.matcher === "Read");
503
- if (!readMatcher) {
504
- readMatcher = {
505
- matcher: "Read",
506
- hooks: []
507
- };
508
- cfg.hooks.postToolUse.push(readMatcher);
656
+ if (Array.isArray(cfg.hooks.postToolUse)) {
657
+ for (let i = cfg.hooks.postToolUse.length - 1; i >= 0; i--) {
658
+ const m = cfg.hooks.postToolUse[i];
659
+ if (!m || !Array.isArray(m.hooks)) continue;
660
+ m.hooks = m.hooks.filter((h) => !isForgeOwnedCommand(h.command));
661
+ if (m.hooks.length === 0 && m.matcher === "Read") cfg.hooks.postToolUse.splice(i, 1);
662
+ }
663
+ if (cfg.hooks.postToolUse.length === 0) delete cfg.hooks.postToolUse;
509
664
  }
510
- if (!Array.isArray(readMatcher.hooks)) readMatcher.hooks = [];
511
665
  const command = `node ${shellEscape$2(scriptPath)}`;
512
- const ourEntry = readMatcher.hooks.find((h) => h.command.includes(FORGE_HOOK_BASENAME$1));
513
- if (ourEntry) {
514
- if (ourEntry.command !== command) ourEntry.command = command;
515
- } else readMatcher.hooks.push({
516
- type: "command",
517
- command
518
- });
666
+ for (const eventName of FORGE_CURSOR_EVENTS) {
667
+ if (!Array.isArray(cfg.hooks[eventName])) cfg.hooks[eventName] = [];
668
+ const arr = cfg.hooks[eventName];
669
+ for (let i = arr.length - 1; i >= 0; i--) {
670
+ const item = arr[i];
671
+ if (!item || typeof item !== "object") continue;
672
+ if (isFlatEntry(item)) {
673
+ if (isForgeOwnedCommand(item.command)) arr.splice(i, 1);
674
+ continue;
675
+ }
676
+ const wrapper = item;
677
+ if (Array.isArray(wrapper.hooks)) {
678
+ wrapper.hooks = wrapper.hooks.filter((h) => !isForgeOwnedCommand(h?.command));
679
+ if (wrapper.hooks.length === 0 && wrapper.matcher === void 0) arr.splice(i, 1);
680
+ }
681
+ }
682
+ arr.push({
683
+ type: "command",
684
+ command
685
+ });
686
+ }
519
687
  await mkdir(dirname(hooksJsonPath), { recursive: true });
520
688
  await writeFile(hooksJsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
521
689
  }
@@ -574,7 +742,7 @@ function removeSection(toml, header) {
574
742
  function jsonString(s) {
575
743
  return JSON.stringify(s);
576
744
  }
577
- const FORGE_HOOK_BASENAME = "track-skill-read.mjs";
745
+ const FORGE_HOOK_BASENAME = "track-skill-claude-code.mjs";
578
746
  async function upsertCodexHooks(hooksJsonPath, configTomlPath, scriptPath) {
579
747
  let raw = "";
580
748
  try {
@@ -615,9 +783,9 @@ function shellEscape$1(p) {
615
783
  if (!/[\s"'$`\\]/.test(p)) return p;
616
784
  return `'${p.replace(/'/g, `'\\''`)}'`;
617
785
  }
618
- const HOOK_SCRIPT_BASENAME = "track-skill-read.mjs";
786
+ const HOOK_SCRIPT_BASENAME = "track-skill-claude-code.mjs";
619
787
  const HOOK_DIRNAME = "forge-hooks";
620
- const FORGE_HOOK_TAG = "forge:track-skill-read";
788
+ const FORGE_HOOK_TAG = "forge:track-skill-claude-code";
621
789
  async function writeForgeSourceManifest(skillDir, manifest) {
622
790
  const target = join(skillDir, ".forge-source.json");
623
791
  await mkdir(skillDir, { recursive: true });
@@ -628,35 +796,93 @@ function getForgeHookScript(portalUrl) {
628
796
  return `#!/usr/bin/env node
629
797
  // ${FORGE_HOOK_TAG}
630
798
  // Auto-generated by ai-dev-cli. Do not edit; run \`ai-dev-cli add\` to refresh.
631
- // Reports {type:'skill_invoked', asset_ref, install_mode} when the agent reads
632
- // SKILL.md from a forge-installed skill (identified by .forge-source.json).
799
+ // Reports {type:'skill_invoked', asset_ref, install_mode} when Claude Code
800
+ // loads a forge-installed skill via slash command (UserPromptExpansion)
801
+ // or via the Skill tool (PreToolUse). PostToolUse(Skill) is unreliable on
802
+ // Claude Code 2.1.x; see anthropics/claude-code#43630.
633
803
  // Anonymous: /events accepts unauthenticated posts.
634
804
  import fs from 'node:fs';
805
+ import os from 'node:os';
635
806
  import path from 'node:path';
636
807
 
637
808
  const PORTAL = ${JSON.stringify(portal)};
638
809
 
639
- setTimeout(() => process.exit(0), 5000);
810
+ // Safety net: never let a stuck fetch block the IDE for more than 5 seconds.
811
+ // The fetch itself uses AbortSignal.timeout(2000); this catches edge cases
812
+ // like a stalled DNS lookup before the abort fires.
813
+ const exitTimer = setTimeout(() => process.exit(0), 5000);
814
+ exitTimer.unref?.();
815
+
816
+ function readManifest(dir) {
817
+ try {
818
+ const raw = fs.readFileSync(path.join(dir, '.forge-source.json'), 'utf8');
819
+ return JSON.parse(raw);
820
+ } catch {
821
+ return null;
822
+ }
823
+ }
824
+
825
+ function manifestFromSkillName(skillId, cwd) {
826
+ if (!skillId || typeof skillId !== 'string') return null;
827
+ // \`plugin:skill-name\` → drop the plugin prefix; forge installs flatten into
828
+ // .claude/skills/<name>/ regardless of plugin namespace (see agents.ts).
829
+ const name = skillId.includes(':') ? skillId.split(':').pop() : skillId;
830
+ if (!name) return null;
831
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || path.join(os.homedir(), '.claude');
832
+ // 1) IDE entry under cwd (project-scope install).
833
+ // 2) IDE entry under CLAUDE_CONFIG_DIR (global-scope install).
834
+ const ideCandidates = [];
835
+ if (cwd) ideCandidates.push(path.join(cwd, '.claude', 'skills', name));
836
+ ideCandidates.push(path.join(claudeHome, 'skills', name));
837
+ for (const dir of ideCandidates) {
838
+ const m = readManifest(dir);
839
+ if (m) return m;
840
+ }
841
+ // 3) Forge canonical store. Truth source — regardless of where the IDE
842
+ // entry symlink lives or what the agent's cwd is, the manifest always
843
+ // exists under ~/.ai-dev-cli/store. Skill dirs are named \`<scope>__<name>\`;
844
+ // plugin-internal skills live one level deeper.
845
+ const storeRoot = path.join(os.homedir(), '.ai-dev-cli', 'store');
846
+ for (const sub of ['skills', 'plugins']) {
847
+ try {
848
+ const root = path.join(storeRoot, sub);
849
+ for (const entry of fs.readdirSync(root)) {
850
+ if (sub === 'skills') {
851
+ if (!entry.endsWith('__' + name)) continue;
852
+ const m = readManifest(path.join(root, entry));
853
+ if (m && m.name === name) return m;
854
+ } else {
855
+ const m = readManifest(path.join(root, entry, 'skills', name));
856
+ if (m && m.name === name) return m;
857
+ }
858
+ }
859
+ } catch {
860
+ /* store dir missing — fall through */
861
+ }
862
+ }
863
+ return null;
864
+ }
640
865
 
641
866
  let buf = '';
642
867
  process.stdin.on('data', (c) => (buf += c));
643
868
  process.stdin.on('end', () => {
869
+ let skillId = null;
644
870
  try {
645
871
  const ev = JSON.parse(buf);
646
- const filePath = ev.tool_input?.file_path || '';
647
- if (!filePath.endsWith(path.sep + 'SKILL.md') && !filePath.endsWith('/SKILL.md')) return;
648
-
649
- let dir = path.dirname(filePath);
650
- let manifest = null;
651
- for (let i = 0; i < 8 && dir && dir !== path.dirname(dir); i++) {
652
- const p = path.join(dir, '.forge-source.json');
653
- if (fs.existsSync(p)) {
654
- manifest = JSON.parse(fs.readFileSync(p, 'utf8'));
655
- break;
656
- }
657
- dir = path.dirname(dir);
872
+ if (ev.hook_event_name === 'UserPromptExpansion') {
873
+ // Slash command path. \`command_name\` is the skill identifier as the
874
+ // user typed it (without the leading slash), e.g. "ai-coding-llm-wiki".
875
+ if (ev.expansion_type !== 'slash_command') process.exit(0);
876
+ skillId = ev.command_name;
877
+ } else if (ev.hook_event_name === 'PreToolUse' && ev.tool_name === 'Skill') {
878
+ skillId = ev.tool_input?.skill ?? ev.tool_input?.name;
879
+ } else {
880
+ process.exit(0);
881
+ }
882
+ const manifest = manifestFromSkillName(skillId, ev.cwd || process.cwd());
883
+ if (!manifest?.ref) {
884
+ process.exit(0);
658
885
  }
659
- if (!manifest?.ref) return;
660
886
 
661
887
  fetch(PORTAL + '/events', {
662
888
  method: 'POST',
@@ -670,9 +896,11 @@ process.stdin.on('end', () => {
670
896
  },
671
897
  }),
672
898
  signal: AbortSignal.timeout(2000),
673
- }).catch(() => {});
899
+ })
900
+ .catch(() => {})
901
+ .finally(() => process.exit(0));
674
902
  } catch {
675
- /* swallow */
903
+ process.exit(0);
676
904
  }
677
905
  });
678
906
  `;
@@ -682,27 +910,34 @@ async function ensureForgeHooks(agent) {
682
910
  try {
683
911
  const portal = loadConfig().portal || DEFAULT_PORTAL;
684
912
  const home = getAgentHome(agent);
685
- const scriptPath = join(home, HOOK_DIRNAME, HOOK_SCRIPT_BASENAME);
686
- await writeHookScript(scriptPath, portal);
687
- if (agent === "claude-code") await upsertClaudeCodeSettings(join(home, "settings.json"), scriptPath);
688
- else if (agent === "cursor") await upsertCursorHooks(join(home, "hooks.json"), scriptPath);
689
- else if (agent === "codex") await upsertCodexHooks(join(home, "hooks.json"), join(home, "config.toml"), scriptPath);
913
+ if (agent === "claude-code") {
914
+ const scriptPath = join(home, HOOK_DIRNAME, HOOK_SCRIPT_BASENAME);
915
+ await writeScriptIfChanged(scriptPath, getForgeHookScript(portal));
916
+ await upsertClaudeCodeSettings(join(home, "settings.json"), scriptPath);
917
+ } else if (agent === "cursor") {
918
+ const cursorScriptPath = join(home, HOOK_DIRNAME, CURSOR_HOOK_SCRIPT_BASENAME);
919
+ await writeScriptIfChanged(cursorScriptPath, getCursorHookScript(portal));
920
+ await upsertCursorHooks(join(home, "hooks.json"), cursorScriptPath);
921
+ } else if (agent === "codex") {
922
+ const scriptPath = join(home, HOOK_DIRNAME, HOOK_SCRIPT_BASENAME);
923
+ await writeScriptIfChanged(scriptPath, getForgeHookScript(portal));
924
+ await upsertCodexHooks(join(home, "hooks.json"), join(home, "config.toml"), scriptPath);
925
+ }
690
926
  } catch (err) {
691
927
  warnings.push(`Failed to install forge hooks for ${agent}: ${err instanceof Error ? err.message : String(err)}`);
692
928
  }
693
929
  return { warnings };
694
930
  }
695
- async function writeHookScript(scriptPath, portalUrl) {
696
- const next = getForgeHookScript(portalUrl);
931
+ async function writeScriptIfChanged(scriptPath, content) {
697
932
  let prev = null;
698
933
  if (existsSync(scriptPath)) try {
699
934
  prev = await readFile(scriptPath, "utf-8");
700
935
  } catch {
701
936
  prev = null;
702
937
  }
703
- if (prev === next) return;
938
+ if (prev === content) return;
704
939
  await mkdir(dirname(scriptPath), { recursive: true });
705
- await writeFile(scriptPath, next, {
940
+ await writeFile(scriptPath, content, {
706
941
  encoding: "utf-8",
707
942
  mode: 493
708
943
  });
@@ -710,6 +945,27 @@ async function writeHookScript(scriptPath, portalUrl) {
710
945
  await chmod(scriptPath, 493);
711
946
  } catch {}
712
947
  }
948
+ const FORGE_LEGACY_LOCATIONS = [
949
+ {
950
+ event: "PostToolUse",
951
+ matcher: "Read"
952
+ },
953
+ {
954
+ event: "PostToolUse",
955
+ matcher: "Read|Skill"
956
+ },
957
+ {
958
+ event: "PostToolUse",
959
+ matcher: "Skill"
960
+ }
961
+ ];
962
+ const FORGE_CANONICAL_LOCATIONS = [{
963
+ event: "UserPromptExpansion",
964
+ matcher: void 0
965
+ }, {
966
+ event: "PreToolUse",
967
+ matcher: "Skill"
968
+ }];
713
969
  async function upsertClaudeCodeSettings(settingsPath, scriptPath) {
714
970
  const command = `node ${shellEscape(scriptPath)}`;
715
971
  let raw = "";
@@ -725,26 +981,38 @@ async function upsertClaudeCodeSettings(settingsPath, scriptPath) {
725
981
  throw new Error(`Refusing to overwrite invalid JSON at ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
726
982
  }
727
983
  if (!settings.hooks || typeof settings.hooks !== "object") settings.hooks = {};
728
- if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
729
- const matcherList = settings.hooks.PostToolUse;
730
- let readMatcher = matcherList.find((m) => m && m.matcher === "Read");
731
- if (!readMatcher) {
732
- readMatcher = {
733
- matcher: "Read",
734
- hooks: []
735
- };
736
- matcherList.push(readMatcher);
737
- }
738
- if (!Array.isArray(readMatcher.hooks)) readMatcher.hooks = [];
739
- const ourEntry = readMatcher.hooks.find((h) => typeof h?.command === "string" && h.command.includes(HOOK_SCRIPT_BASENAME));
740
- if (ourEntry) {
741
- if (ourEntry.command === command && ourEntry.type === "command") return;
742
- ourEntry.type = "command";
743
- ourEntry.command = command;
744
- } else readMatcher.hooks.push({
745
- type: "command",
746
- command
747
- });
984
+ const hooks = settings.hooks;
985
+ const isOurEntry = (h) => !!(h && typeof h.command === "string" && h.command.includes(HOOK_SCRIPT_BASENAME));
986
+ const owned = new Set([...FORGE_LEGACY_LOCATIONS, ...FORGE_CANONICAL_LOCATIONS].map((l) => `${l.event}::${l.matcher ?? ""}`));
987
+ for (const eventName of Object.keys(hooks)) {
988
+ const matcherList = hooks[eventName];
989
+ if (!Array.isArray(matcherList)) continue;
990
+ for (let i = matcherList.length - 1; i >= 0; i--) {
991
+ const m = matcherList[i];
992
+ if (!m || !Array.isArray(m.hooks)) continue;
993
+ m.hooks = m.hooks.filter((h) => !isOurEntry(h));
994
+ const key = `${eventName}::${m.matcher ?? ""}`;
995
+ if (m.hooks.length === 0 && owned.has(key)) matcherList.splice(i, 1);
996
+ }
997
+ if (matcherList.length === 0) delete hooks[eventName];
998
+ }
999
+ for (const loc of FORGE_CANONICAL_LOCATIONS) {
1000
+ if (!Array.isArray(hooks[loc.event])) hooks[loc.event] = [];
1001
+ const matcherList = hooks[loc.event];
1002
+ let target = matcherList.find((m) => m && (m.matcher ?? void 0) === loc.matcher);
1003
+ if (!target) {
1004
+ target = loc.matcher === void 0 ? { hooks: [] } : {
1005
+ matcher: loc.matcher,
1006
+ hooks: []
1007
+ };
1008
+ matcherList.push(target);
1009
+ }
1010
+ if (!Array.isArray(target.hooks)) target.hooks = [];
1011
+ target.hooks.push({
1012
+ type: "command",
1013
+ command
1014
+ });
1015
+ }
748
1016
  await mkdir(dirname(settingsPath), { recursive: true });
749
1017
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
750
1018
  }
@@ -752,18 +1020,273 @@ function shellEscape(p) {
752
1020
  if (!/[\s"'$`\\]/.test(p)) return p;
753
1021
  return `'${p.replace(/'/g, `'\\''`)}'`;
754
1022
  }
755
- async function fetchSkillArchive(ref, portalUrl, bearerToken) {
756
- const url = `${portalUrl.replace(/\/$/, "")}/api/cli/skills/archive?ref=${encodeURIComponent(ref)}`;
757
- const headers = { Accept: "application/gzip" };
758
- if (bearerToken) headers.Authorization = `Bearer ${bearerToken}`;
759
- let resp;
1023
+ function getRandomPort() {
1024
+ return new Promise((resolve, reject) => {
1025
+ const server = createServer();
1026
+ server.listen(0, "127.0.0.1", () => {
1027
+ const address = server.address();
1028
+ if (address && typeof address !== "string" && "port" in address) {
1029
+ const port = address.port;
1030
+ server.close(() => resolve(port));
1031
+ } else server.close(() => reject(/* @__PURE__ */ new Error("Failed to get port")));
1032
+ });
1033
+ server.on("error", reject);
1034
+ });
1035
+ }
1036
+ const PORTAL_THEME_CSS = `
1037
+ :root {
1038
+ --bg: #FDFCF8;
1039
+ --ink: #201F1D;
1040
+ --text: #37352F;
1041
+ --muted: #827E7A;
1042
+ --line: #EDECE8;
1043
+ --success: #0F6E37;
1044
+ --success-soft: #EDF7F1;
1045
+ --danger: #E5484D;
1046
+ --danger-soft: #FDF1EF;
1047
+ --serif: "Newsreader", "Source Serif Pro", Georgia, serif;
1048
+ --sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", sans-serif;
1049
+ --shadow-sm: 0 1px 2px rgba(32, 31, 29, 0.06);
1050
+ }
1051
+ @media (prefers-color-scheme: dark) {
1052
+ :root {
1053
+ --bg: #27251F;
1054
+ --ink: #EDEAE5;
1055
+ --text: #D4CFC8;
1056
+ --muted: #918D89;
1057
+ --line: #35332F;
1058
+ --success: #3FC67A;
1059
+ --success-soft: #062A14;
1060
+ --danger: #E5484D;
1061
+ --danger-soft: #2B1212;
1062
+ }
1063
+ }
1064
+ * { box-sizing: border-box; }
1065
+ body {
1066
+ margin: 0;
1067
+ min-height: 100vh;
1068
+ background: var(--bg);
1069
+ color: var(--text);
1070
+ font-family: var(--sans);
1071
+ font-size: 15px;
1072
+ line-height: 1.65;
1073
+ display: flex;
1074
+ align-items: center;
1075
+ justify-content: center;
1076
+ }
1077
+ .card {
1078
+ max-width: 480px;
1079
+ margin: 0 24px;
1080
+ padding: 40px 32px;
1081
+ text-align: center;
1082
+ }
1083
+ .icon {
1084
+ width: 56px;
1085
+ height: 56px;
1086
+ margin: 0 auto 24px;
1087
+ border-radius: 50%;
1088
+ display: flex;
1089
+ align-items: center;
1090
+ justify-content: center;
1091
+ }
1092
+ .icon.success { background: var(--success-soft); }
1093
+ .icon.error { background: var(--danger-soft); }
1094
+ .icon svg { display: block; }
1095
+ h1 {
1096
+ font-family: var(--serif);
1097
+ font-size: 28px;
1098
+ font-weight: 500;
1099
+ letter-spacing: -0.01em;
1100
+ color: var(--ink);
1101
+ margin: 0 0 12px;
1102
+ }
1103
+ p { color: var(--muted); margin: 0 0 8px; }
1104
+ p.detail { color: var(--text); }
1105
+ `;
1106
+ function renderCallbackPage(opts) {
1107
+ return `<!DOCTYPE html>
1108
+ <html>
1109
+ <head>
1110
+ <meta charset="utf-8">
1111
+ <title>${opts.title}</title>
1112
+ <style>${PORTAL_THEME_CSS}</style>
1113
+ </head>
1114
+ <body>
1115
+ <div class="card">
1116
+ <div class="icon ${opts.variant}">${opts.iconSvg}</div>
1117
+ <h1>${opts.heading}</h1>
1118
+ ${opts.body}
1119
+ </div>
1120
+ </body>
1121
+ </html>`;
1122
+ }
1123
+ function getSuccessHtml() {
1124
+ return renderCallbackPage({
1125
+ title: "Login successful",
1126
+ heading: "Login successful",
1127
+ body: "<p>You can close this window and return to the CLI.</p>",
1128
+ variant: "success",
1129
+ iconSvg: `
1130
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor"
1131
+ stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"
1132
+ style="color: var(--success);">
1133
+ <polyline points="4 12 10 18 20 6"></polyline>
1134
+ </svg>`
1135
+ });
1136
+ }
1137
+ function escapeHtml(input) {
1138
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1139
+ }
1140
+ function getErrorHtml(error) {
1141
+ return renderCallbackPage({
1142
+ title: "Login failed",
1143
+ heading: "Login failed",
1144
+ body: `<p class="detail">${escapeHtml(error)}</p><p>Please try again.</p>`,
1145
+ variant: "error",
1146
+ iconSvg: `
1147
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor"
1148
+ stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"
1149
+ style="color: var(--danger);">
1150
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1151
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1152
+ </svg>`
1153
+ });
1154
+ }
1155
+ let callbackResolve = null;
1156
+ async function runLogin(options = {}) {
1157
+ const cfg = loadConfig();
1158
+ if (options.logout) {
1159
+ if (cfg.token) {
1160
+ saveConfig({
1161
+ token: void 0,
1162
+ token_name: void 0
1163
+ });
1164
+ M.success("Logged out successfully.");
1165
+ M.message(import_picocolors.default.dim("Your token has been removed from the config."));
1166
+ } else M.message("You are not logged in.");
1167
+ return;
1168
+ }
1169
+ if (cfg.token && !options.force) {
1170
+ M.message("You are already logged in.");
1171
+ M.info(import_picocolors.default.dim(`Token: ${cfg.token_name || "default"}`));
1172
+ const shouldReauth = await ye({
1173
+ message: "Re-authenticate?",
1174
+ initialValue: false
1175
+ });
1176
+ if (isCancelled$1(shouldReauth)) {
1177
+ Se(import_picocolors.default.dim("Cancelled"));
1178
+ return;
1179
+ }
1180
+ if (!shouldReauth) return;
1181
+ }
1182
+ console.log();
1183
+ Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" login ")));
1184
+ const portalUrl = cfg.portal;
1185
+ const port = await getRandomPort();
1186
+ const redirectUrl = `http://127.0.0.1:${port}/callback`;
1187
+ const machine = hostname().slice(0, 64);
1188
+ const authUrl = `${portalUrl}/cli-auth?redirect=${encodeURIComponent(redirectUrl)}&hostname=${encodeURIComponent(machine)}`;
1189
+ M.message(`Opening browser for authentication...`);
1190
+ M.info(import_picocolors.default.dim(authUrl));
1191
+ await new Promise((resolve, reject) => {
1192
+ exec(`open "${authUrl}"`, (err) => {
1193
+ if (err) reject(err);
1194
+ else resolve();
1195
+ });
1196
+ });
1197
+ const result = await new Promise((resolve) => {
1198
+ callbackResolve = resolve;
1199
+ const server = createServer((req, res) => {
1200
+ const url = new URL$1(req.url || "", `http://127.0.0.1:${port}`);
1201
+ if (url.pathname === "/callback") {
1202
+ const token = url.searchParams.get("token");
1203
+ const tokenName = url.searchParams.get("token_name");
1204
+ const error = url.searchParams.get("error");
1205
+ if (error) {
1206
+ res.writeHead(400, { "Content-Type": "text/html" });
1207
+ res.end(getErrorHtml(error));
1208
+ resolve({
1209
+ success: false,
1210
+ error
1211
+ });
1212
+ } else if (token) {
1213
+ res.writeHead(200, { "Content-Type": "text/html" });
1214
+ res.end(getSuccessHtml());
1215
+ resolve({
1216
+ success: true,
1217
+ token,
1218
+ tokenName: tokenName || "default"
1219
+ });
1220
+ } else {
1221
+ res.writeHead(400, { "Content-Type": "text/html" });
1222
+ res.end(getErrorHtml("No token received"));
1223
+ resolve({
1224
+ success: false,
1225
+ error: "No token received"
1226
+ });
1227
+ }
1228
+ } else {
1229
+ res.writeHead(404);
1230
+ res.end("Not found");
1231
+ }
1232
+ });
1233
+ server.listen(port, "127.0.0.1", () => {});
1234
+ setTimeout(() => {
1235
+ server.close();
1236
+ if (callbackResolve) callbackResolve({
1237
+ success: false,
1238
+ error: "Login timed out"
1239
+ });
1240
+ }, 12e4);
1241
+ });
1242
+ if (!result.success) {
1243
+ Se(import_picocolors.default.red("Login failed: " + result.error));
1244
+ process.exit(1);
1245
+ }
1246
+ saveConfig({
1247
+ token: result.token,
1248
+ token_name: result.tokenName
1249
+ });
1250
+ M.success("Login successful!");
1251
+ M.message(import_picocolors.default.dim(`Token saved as: ${result.tokenName}`));
1252
+ Se(import_picocolors.default.green("You can now publish skills."));
1253
+ }
1254
+ function isCancelled$1(value) {
1255
+ return typeof value === "symbol";
1256
+ }
1257
+ function parseLoginOptions(args) {
1258
+ const options = {};
1259
+ for (let i = 0; i < args.length; i++) {
1260
+ const arg = args[i];
1261
+ if (arg === "--logout" || arg === "-l") options.logout = true;
1262
+ }
1263
+ return options;
1264
+ }
1265
+ async function rawFetch(url, headers, portalUrl) {
760
1266
  try {
761
- resp = await fetch(url, { headers });
1267
+ return await fetch(url, { headers });
762
1268
  } catch (err) {
763
1269
  const msg = err instanceof Error ? err.message : String(err);
764
1270
  throw new Error(`Failed to reach portal at ${portalUrl}: ${msg}`);
765
1271
  }
766
- if (resp.status === 401) throw new Error("Login required: run `ai-dev-cli login` first");
1272
+ }
1273
+ async function fetchWithAutoAuth(url, accept, bearerToken, portalUrl) {
1274
+ const buildHeaders = (token) => {
1275
+ const h = { Accept: accept };
1276
+ if (token) h.Authorization = `Bearer ${token}`;
1277
+ return h;
1278
+ };
1279
+ let resp = await rawFetch(url, buildHeaders(bearerToken), portalUrl);
1280
+ if (resp.status !== 401) return resp;
1281
+ await resp.arrayBuffer().catch(() => void 0);
1282
+ await runLogin({ force: true });
1283
+ const refreshed = loadConfig().token;
1284
+ if (!refreshed) return resp;
1285
+ return rawFetch(url, buildHeaders(refreshed), portalUrl);
1286
+ }
1287
+ async function fetchSkillArchive(ref, portalUrl, bearerToken) {
1288
+ const resp = await fetchWithAutoAuth(`${portalUrl.replace(/\/$/, "")}/api/cli/skills/archive?ref=${encodeURIComponent(ref)}`, "application/gzip", bearerToken, portalUrl);
1289
+ if (resp.status === 401) throw new Error("Authentication failed after login. Please try again.");
767
1290
  if (resp.status === 403) throw new Error(`Permission denied for ${ref}`);
768
1291
  if (resp.status === 404) throw new Error(`Skill not found: ${ref}`);
769
1292
  if (!resp.ok) {
@@ -773,19 +1296,8 @@ async function fetchSkillArchive(ref, portalUrl, bearerToken) {
773
1296
  return Buffer.from(await resp.arrayBuffer());
774
1297
  }
775
1298
  async function fetchTeamAssets(teamScope, portalUrl, bearerToken) {
776
- const url = `${portalUrl.replace(/\/$/, "")}/api/cli/teams/${encodeURIComponent(teamScope)}/assets`;
777
- const headers = {
778
- Accept: "application/json",
779
- Authorization: `Bearer ${bearerToken}`
780
- };
781
- let resp;
782
- try {
783
- resp = await fetch(url, { headers });
784
- } catch (err) {
785
- const msg = err instanceof Error ? err.message : String(err);
786
- throw new Error(`Failed to reach portal at ${portalUrl}: ${msg}`);
787
- }
788
- if (resp.status === 401) throw new Error("Login required: run `shoplazza-ai-dev-cli login` first");
1299
+ const resp = await fetchWithAutoAuth(`${portalUrl.replace(/\/$/, "")}/api/cli/teams/${encodeURIComponent(teamScope)}/assets`, "application/json", bearerToken, portalUrl);
1300
+ if (resp.status === 401) throw new Error("Authentication failed after login. Please try again.");
789
1301
  if (resp.status === 403) throw new Error(`Not a member of team "${teamScope}"`);
790
1302
  if (resp.status === 404) throw new Error(`Team not found: ${teamScope}`);
791
1303
  if (!resp.ok) {
@@ -795,17 +1307,8 @@ async function fetchTeamAssets(teamScope, portalUrl, bearerToken) {
795
1307
  return (await resp.json()).items ?? [];
796
1308
  }
797
1309
  async function fetchPluginArchive(ref, portalUrl, bearerToken) {
798
- const url = `${portalUrl.replace(/\/$/, "")}/api/cli/plugins/archive?ref=${encodeURIComponent(ref)}`;
799
- const headers = { Accept: "application/gzip" };
800
- if (bearerToken) headers.Authorization = `Bearer ${bearerToken}`;
801
- let resp;
802
- try {
803
- resp = await fetch(url, { headers });
804
- } catch (err) {
805
- const msg = err instanceof Error ? err.message : String(err);
806
- throw new Error(`Failed to reach portal at ${portalUrl}: ${msg}`);
807
- }
808
- if (resp.status === 401) throw new Error("Login required: run `ai-dev-cli login` first");
1310
+ const resp = await fetchWithAutoAuth(`${portalUrl.replace(/\/$/, "")}/api/cli/plugins/archive?ref=${encodeURIComponent(ref)}`, "application/gzip", bearerToken, portalUrl);
1311
+ if (resp.status === 401) throw new Error("Authentication failed after login. Please try again.");
809
1312
  if (resp.status === 403) throw new Error(`Permission denied for ${ref}`);
810
1313
  if (resp.status === 404) throw new Error(`Plugin not found: ${ref}`);
811
1314
  if (!resp.ok) {
@@ -947,7 +1450,7 @@ async function promptForeignOverwrite(conflicts, opts = {}) {
947
1450
  }
948
1451
  return choice === "yes";
949
1452
  }
950
- const AGENTS_DIR$2 = ".agents";
1453
+ const AGENTS_DIR$1 = ".agents";
951
1454
  const SKILLS_SUBDIR = "skills";
952
1455
  function parseFrontmatter(raw) {
953
1456
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
@@ -1187,17 +1690,8 @@ function isPathSafe(basePath, targetPath) {
1187
1690
  const normalizedTarget = normalize(resolve(targetPath));
1188
1691
  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
1189
1692
  }
1190
- async function isDirEntryOrSymlinkToDir(entry, entryPath) {
1191
- if (entry.isDirectory()) return true;
1192
- if (!entry.isSymbolicLink()) return false;
1193
- try {
1194
- return (await stat(entryPath)).isDirectory();
1195
- } catch {
1196
- return false;
1197
- }
1198
- }
1199
1693
  function getCanonicalSkillsDir(global, cwd) {
1200
- return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$2, SKILLS_SUBDIR);
1694
+ return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
1201
1695
  }
1202
1696
  function getAgentBaseDir(agentType, global, cwd) {
1203
1697
  if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
@@ -1279,12 +1773,10 @@ async function installSkillForAgent(skill, agentType, opts) {
1279
1773
  if (!isPathSafe(join(storeRoot, "skills"), canonicalDir)) return {
1280
1774
  success: false,
1281
1775
  path: "",
1282
- mode: "symlink",
1283
1776
  error: "Invalid skill name: path traversal detected",
1284
1777
  forgeOtherReplacements: []
1285
1778
  };
1286
- const entryAgentRoot = scope === "global" ? getAgentHome(agentType) : cwd;
1287
- const entry = agentType === "claude-code" ? join(entryAgentRoot, ".claude", "skills", skillName) : agentType === "cursor" ? join(entryAgentRoot, ".cursor", "skills", skillName) : join(entryAgentRoot, ".agents", "skills", skillName);
1779
+ const entry = join(getAgentBaseDir(agentType, scope === "global", cwd), skillName);
1288
1780
  try {
1289
1781
  const status = await assertEntryReplaceable(entry, {
1290
1782
  intendedRef: newOpts.ref,
@@ -1307,7 +1799,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1307
1799
  if (!proceed) return {
1308
1800
  success: false,
1309
1801
  path: entry,
1310
- mode: "symlink",
1311
1802
  error: "aborted: user-owned content at entry path",
1312
1803
  forgeOtherReplacements: []
1313
1804
  };
@@ -1341,7 +1832,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1341
1832
  return {
1342
1833
  success: true,
1343
1834
  path: entry,
1344
- mode: "symlink",
1345
1835
  symlinkFailed: true,
1346
1836
  canonicalPath: canonicalDir,
1347
1837
  forgeOtherReplacements
@@ -1350,7 +1840,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1350
1840
  return {
1351
1841
  success: true,
1352
1842
  path: entry,
1353
- mode: "symlink",
1354
1843
  canonicalPath: canonicalDir,
1355
1844
  forgeOtherReplacements
1356
1845
  };
@@ -1358,7 +1847,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1358
1847
  return {
1359
1848
  success: false,
1360
1849
  path: entry,
1361
- mode: "symlink",
1362
1850
  error: error instanceof Error ? error.message : "Unknown error",
1363
1851
  forgeOtherReplacements: []
1364
1852
  };
@@ -1371,7 +1859,6 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1371
1859
  if (isGlobal && agent.globalSkillsDir === void 0) return {
1372
1860
  success: false,
1373
1861
  path: "",
1374
- mode: options.mode ?? "symlink",
1375
1862
  error: `${agent.displayName} does not support global skill installation`,
1376
1863
  forgeOtherReplacements: []
1377
1864
  };
@@ -1380,39 +1867,25 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1380
1867
  const canonicalDir = join(canonicalBase, skillName);
1381
1868
  const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
1382
1869
  const agentDir = join(agentBase, skillName);
1383
- const installMode = options.mode ?? "symlink";
1384
1870
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1385
1871
  success: false,
1386
1872
  path: agentDir,
1387
- mode: installMode,
1388
1873
  error: "Invalid skill name: potential path traversal detected",
1389
1874
  forgeOtherReplacements: []
1390
1875
  };
1391
1876
  if (!isPathSafe(agentBase, agentDir)) return {
1392
1877
  success: false,
1393
1878
  path: agentDir,
1394
- mode: installMode,
1395
1879
  error: "Invalid skill name: potential path traversal detected",
1396
1880
  forgeOtherReplacements: []
1397
1881
  };
1398
1882
  try {
1399
- if (installMode === "copy") {
1400
- await cleanAndCreateDirectory(agentDir);
1401
- await copyDirectory(skill.path, agentDir);
1402
- return {
1403
- success: true,
1404
- path: agentDir,
1405
- mode: "copy",
1406
- forgeOtherReplacements: []
1407
- };
1408
- }
1409
1883
  await cleanAndCreateDirectory(canonicalDir);
1410
1884
  await copyDirectory(skill.path, canonicalDir);
1411
1885
  if (isGlobal && isUniversalAgent(agentType)) return {
1412
1886
  success: true,
1413
1887
  path: canonicalDir,
1414
1888
  canonicalPath: canonicalDir,
1415
- mode: "symlink",
1416
1889
  forgeOtherReplacements: []
1417
1890
  };
1418
1891
  if (!await createSymlink(canonicalDir, agentDir)) {
@@ -1422,7 +1895,6 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1422
1895
  success: true,
1423
1896
  path: agentDir,
1424
1897
  canonicalPath: canonicalDir,
1425
- mode: "symlink",
1426
1898
  symlinkFailed: true,
1427
1899
  forgeOtherReplacements: []
1428
1900
  };
@@ -1431,14 +1903,12 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1431
1903
  success: true,
1432
1904
  path: agentDir,
1433
1905
  canonicalPath: canonicalDir,
1434
- mode: "symlink",
1435
1906
  forgeOtherReplacements: []
1436
1907
  };
1437
1908
  } catch (error) {
1438
1909
  return {
1439
1910
  success: false,
1440
1911
  path: agentDir,
1441
- mode: installMode,
1442
1912
  error: error instanceof Error ? error.message : "Unknown error",
1443
1913
  forgeOtherReplacements: []
1444
1914
  };
@@ -1487,15 +1957,6 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
1487
1957
  return false;
1488
1958
  }
1489
1959
  }
1490
- function getInstallPath(skillName, agentType, options = {}) {
1491
- agents[agentType];
1492
- options.cwd || process.cwd();
1493
- const sanitized = sanitizeName(skillName);
1494
- const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
1495
- const installPath = join(targetBase, sanitized);
1496
- if (!isPathSafe(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
1497
- return installPath;
1498
- }
1499
1960
  function getCanonicalPath(skillName, options = {}) {
1500
1961
  const sanitized = sanitizeName(skillName);
1501
1962
  const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
@@ -1507,11 +1968,9 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1507
1968
  const agent = agents[agentType];
1508
1969
  const isGlobal = options.global ?? false;
1509
1970
  const cwd = options.cwd || process.cwd();
1510
- const installMode = options.mode ?? "symlink";
1511
1971
  if (isGlobal && agent.globalSkillsDir === void 0) return {
1512
1972
  success: false,
1513
1973
  path: "",
1514
- mode: installMode,
1515
1974
  error: `${agent.displayName} does not support global skill installation`
1516
1975
  };
1517
1976
  const skillName = sanitizeName(skill.installName);
@@ -1522,13 +1981,11 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1522
1981
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1523
1982
  success: false,
1524
1983
  path: agentDir,
1525
- mode: installMode,
1526
1984
  error: "Invalid skill name: potential path traversal detected"
1527
1985
  };
1528
1986
  if (!isPathSafe(agentBase, agentDir)) return {
1529
1987
  success: false,
1530
1988
  path: agentDir,
1531
- mode: installMode,
1532
1989
  error: "Invalid skill name: potential path traversal detected"
1533
1990
  };
1534
1991
  async function writeSkillFiles(targetDir) {
@@ -1541,22 +1998,12 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1541
1998
  }
1542
1999
  }
1543
2000
  try {
1544
- if (installMode === "copy") {
1545
- await cleanAndCreateDirectory(agentDir);
1546
- await writeSkillFiles(agentDir);
1547
- return {
1548
- success: true,
1549
- path: agentDir,
1550
- mode: "copy"
1551
- };
1552
- }
1553
2001
  await cleanAndCreateDirectory(canonicalDir);
1554
2002
  await writeSkillFiles(canonicalDir);
1555
2003
  if (isGlobal && isUniversalAgent(agentType)) return {
1556
2004
  success: true,
1557
2005
  path: canonicalDir,
1558
- canonicalPath: canonicalDir,
1559
- mode: "symlink"
2006
+ canonicalPath: canonicalDir
1560
2007
  };
1561
2008
  if (!await createSymlink(canonicalDir, agentDir)) {
1562
2009
  await cleanAndCreateDirectory(agentDir);
@@ -1565,21 +2012,18 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1565
2012
  success: true,
1566
2013
  path: agentDir,
1567
2014
  canonicalPath: canonicalDir,
1568
- mode: "symlink",
1569
2015
  symlinkFailed: true
1570
2016
  };
1571
2017
  }
1572
2018
  return {
1573
2019
  success: true,
1574
2020
  path: agentDir,
1575
- canonicalPath: canonicalDir,
1576
- mode: "symlink"
2021
+ canonicalPath: canonicalDir
1577
2022
  };
1578
2023
  } catch (error) {
1579
2024
  return {
1580
2025
  success: false,
1581
2026
  path: agentDir,
1582
- mode: installMode,
1583
2027
  error: error instanceof Error ? error.message : "Unknown error"
1584
2028
  };
1585
2029
  }
@@ -1588,11 +2032,9 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1588
2032
  const agent = agents[agentType];
1589
2033
  const isGlobal = options.global ?? false;
1590
2034
  const cwd = options.cwd || process.cwd();
1591
- const installMode = options.mode ?? "symlink";
1592
2035
  if (isGlobal && agent.globalSkillsDir === void 0) return {
1593
2036
  success: false,
1594
2037
  path: "",
1595
- mode: installMode,
1596
2038
  error: `${agent.displayName} does not support global skill installation`
1597
2039
  };
1598
2040
  const skillName = sanitizeName(skill.installName);
@@ -1603,13 +2045,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1603
2045
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1604
2046
  success: false,
1605
2047
  path: agentDir,
1606
- mode: installMode,
1607
2048
  error: "Invalid skill name: potential path traversal detected"
1608
2049
  };
1609
2050
  if (!isPathSafe(agentBase, agentDir)) return {
1610
2051
  success: false,
1611
2052
  path: agentDir,
1612
- mode: installMode,
1613
2053
  error: "Invalid skill name: potential path traversal detected"
1614
2054
  };
1615
2055
  async function writeSkillFiles(targetDir) {
@@ -1622,22 +2062,12 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1622
2062
  }
1623
2063
  }
1624
2064
  try {
1625
- if (installMode === "copy") {
1626
- await cleanAndCreateDirectory(agentDir);
1627
- await writeSkillFiles(agentDir);
1628
- return {
1629
- success: true,
1630
- path: agentDir,
1631
- mode: "copy"
1632
- };
1633
- }
1634
2065
  await cleanAndCreateDirectory(canonicalDir);
1635
2066
  await writeSkillFiles(canonicalDir);
1636
2067
  if (isGlobal && isUniversalAgent(agentType)) return {
1637
2068
  success: true,
1638
2069
  path: canonicalDir,
1639
- canonicalPath: canonicalDir,
1640
- mode: "symlink"
2070
+ canonicalPath: canonicalDir
1641
2071
  };
1642
2072
  if (!await createSymlink(canonicalDir, agentDir)) {
1643
2073
  await cleanAndCreateDirectory(agentDir);
@@ -1646,185 +2076,22 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1646
2076
  success: true,
1647
2077
  path: agentDir,
1648
2078
  canonicalPath: canonicalDir,
1649
- mode: "symlink",
1650
2079
  symlinkFailed: true
1651
2080
  };
1652
2081
  }
1653
2082
  return {
1654
2083
  success: true,
1655
2084
  path: agentDir,
1656
- canonicalPath: canonicalDir,
1657
- mode: "symlink"
2085
+ canonicalPath: canonicalDir
1658
2086
  };
1659
2087
  } catch (error) {
1660
2088
  return {
1661
2089
  success: false,
1662
2090
  path: agentDir,
1663
- mode: installMode,
1664
2091
  error: error instanceof Error ? error.message : "Unknown error"
1665
2092
  };
1666
2093
  }
1667
2094
  }
1668
- async function listInstalledSkills(options = {}) {
1669
- const cwd = options.cwd || process.cwd();
1670
- const skillsMap = /* @__PURE__ */ new Map();
1671
- const scopes = [];
1672
- const detectedAgents = await detectInstalledAgents();
1673
- const agentFilter = options.agentFilter;
1674
- const agentsToCheck = agentFilter ? detectedAgents.filter((a) => agentFilter.includes(a)) : detectedAgents;
1675
- const scopeTypes = [];
1676
- if (options.global === void 0) scopeTypes.push({ global: false }, { global: true });
1677
- else scopeTypes.push({ global: options.global });
1678
- for (const { global: isGlobal } of scopeTypes) {
1679
- scopes.push({
1680
- global: isGlobal,
1681
- path: getCanonicalSkillsDir(isGlobal, cwd)
1682
- });
1683
- for (const agentType of agentsToCheck) {
1684
- const agent = agents[agentType];
1685
- if (isGlobal && agent.globalSkillsDir === void 0) continue;
1686
- const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1687
- if (!scopes.some((s) => s.path === agentDir && s.global === isGlobal)) scopes.push({
1688
- global: isGlobal,
1689
- path: agentDir,
1690
- agentType
1691
- });
1692
- }
1693
- const allAgentTypes = Object.keys(agents);
1694
- for (const agentType of allAgentTypes) {
1695
- if (agentsToCheck.includes(agentType)) continue;
1696
- const agent = agents[agentType];
1697
- if (isGlobal && agent.globalSkillsDir === void 0) continue;
1698
- const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1699
- if (scopes.some((s) => s.path === agentDir && s.global === isGlobal)) continue;
1700
- if (existsSync(agentDir)) scopes.push({
1701
- global: isGlobal,
1702
- path: agentDir,
1703
- agentType
1704
- });
1705
- }
1706
- }
1707
- for (const scope of scopes) try {
1708
- const entries = await readdir(scope.path, { withFileTypes: true });
1709
- for (const entry of entries) {
1710
- const skillDir = join(scope.path, entry.name);
1711
- if (!await isDirEntryOrSymlinkToDir(entry, skillDir)) continue;
1712
- const skillMdPath = join(skillDir, "SKILL.md");
1713
- try {
1714
- await stat(skillMdPath);
1715
- } catch {
1716
- continue;
1717
- }
1718
- const skill = await parseSkillMd(skillMdPath);
1719
- if (!skill) continue;
1720
- const scopeKey = scope.global ? "global" : "project";
1721
- const skillKey = `${scopeKey}:${skill.name}`;
1722
- if (scope.agentType) {
1723
- if (skillsMap.has(skillKey)) {
1724
- const existing = skillsMap.get(skillKey);
1725
- if (!existing.agents.includes(scope.agentType)) existing.agents.push(scope.agentType);
1726
- } else skillsMap.set(skillKey, {
1727
- name: skill.name,
1728
- description: skill.description,
1729
- path: skillDir,
1730
- canonicalPath: skillDir,
1731
- scope: scopeKey,
1732
- agents: [scope.agentType]
1733
- });
1734
- continue;
1735
- }
1736
- const sanitizedSkillName = sanitizeName(skill.name);
1737
- const installedAgents = [];
1738
- for (const agentType of agentsToCheck) {
1739
- const agent = agents[agentType];
1740
- if (scope.global && agent.globalSkillsDir === void 0) continue;
1741
- const agentBase = scope.global ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1742
- let found = false;
1743
- const possibleNames = Array.from(new Set([
1744
- entry.name,
1745
- sanitizedSkillName,
1746
- skill.name.toLowerCase().replace(/\s+/g, "-").replace(/[\/\\:\0]/g, "")
1747
- ]));
1748
- for (const possibleName of possibleNames) {
1749
- const agentSkillDir = join(agentBase, possibleName);
1750
- if (!isPathSafe(agentBase, agentSkillDir)) continue;
1751
- try {
1752
- await access(agentSkillDir);
1753
- found = true;
1754
- break;
1755
- } catch {}
1756
- }
1757
- if (!found) try {
1758
- const agentEntries = await readdir(agentBase, { withFileTypes: true });
1759
- for (const agentEntry of agentEntries) {
1760
- const candidateDir = join(agentBase, agentEntry.name);
1761
- if (!await isDirEntryOrSymlinkToDir(agentEntry, candidateDir)) continue;
1762
- if (!isPathSafe(agentBase, candidateDir)) continue;
1763
- try {
1764
- const candidateSkillMd = join(candidateDir, "SKILL.md");
1765
- await stat(candidateSkillMd);
1766
- const candidateSkill = await parseSkillMd(candidateSkillMd);
1767
- if (candidateSkill && candidateSkill.name === skill.name) {
1768
- found = true;
1769
- break;
1770
- }
1771
- } catch {}
1772
- }
1773
- } catch {}
1774
- if (found) installedAgents.push(agentType);
1775
- }
1776
- if (skillsMap.has(skillKey)) {
1777
- const existing = skillsMap.get(skillKey);
1778
- for (const agent of installedAgents) if (!existing.agents.includes(agent)) existing.agents.push(agent);
1779
- } else skillsMap.set(skillKey, {
1780
- name: skill.name,
1781
- description: skill.description,
1782
- path: skillDir,
1783
- canonicalPath: skillDir,
1784
- scope: scopeKey,
1785
- agents: installedAgents
1786
- });
1787
- }
1788
- } catch {}
1789
- return Array.from(skillsMap.values());
1790
- }
1791
- const USER_START = "<!-- forge:user-content:start -->";
1792
- const USER_END = "<!-- forge:user-content:end -->";
1793
- function takeoverWithUserContent(existing) {
1794
- if (existing.includes(USER_START)) return existing;
1795
- return `${USER_START}\n${existing.trim()}\n${USER_END}\n`;
1796
- }
1797
- function injectPluginRules(agentsMd, plugin, rules) {
1798
- const start = `<!-- forge-plugin:${plugin.key}:start ref=${plugin.ref} install_mode=codex -->`;
1799
- const end = `<!-- forge-plugin:${plugin.key}:end -->`;
1800
- const block = `${start}\n${rules.map((r) => {
1801
- const rs = `<!-- forge-rule:${r.relativePath}:start -->`;
1802
- const re = `<!-- forge-rule:${r.relativePath}:end -->`;
1803
- return ` ${rs}\n${r.content.trimEnd()}\n ${re}`;
1804
- }).join("\n")}\n${end}\n`;
1805
- const existing = findPluginBlock(agentsMd, plugin.key);
1806
- if (existing) return agentsMd.slice(0, existing.start) + block + agentsMd.slice(existing.end);
1807
- return agentsMd + (agentsMd.endsWith("\n") ? "" : "\n") + "\n" + block;
1808
- }
1809
- function removePluginRules(agentsMd, pluginKey) {
1810
- const existing = findPluginBlock(agentsMd, pluginKey);
1811
- if (!existing) return agentsMd;
1812
- return agentsMd.slice(0, existing.start) + agentsMd.slice(existing.end);
1813
- }
1814
- function findPluginBlock(agentsMd, pluginKey) {
1815
- const startMarker = `<!-- forge-plugin:${pluginKey}:start`;
1816
- const endMarker = `<!-- forge-plugin:${pluginKey}:end -->`;
1817
- const startIdx = agentsMd.indexOf(startMarker);
1818
- if (startIdx < 0) return null;
1819
- const endIdx = agentsMd.indexOf(endMarker, startIdx);
1820
- if (endIdx < 0) return null;
1821
- let blockEnd = endIdx + endMarker.length;
1822
- if (agentsMd[blockEnd] === "\n") blockEnd++;
1823
- return {
1824
- start: startIdx,
1825
- end: blockEnd
1826
- };
1827
- }
1828
2095
  async function installPlugin(ref, opts) {
1829
2096
  const cfg = loadConfig();
1830
2097
  const tarball = await fetchPluginArchive(ref, cfg.portal, cfg.token);
@@ -1866,7 +2133,7 @@ async function installFromExtracted(ref, extractedDir, opts) {
1866
2133
  const forgeOtherReplacements = [];
1867
2134
  const statuses = /* @__PURE__ */ new Map();
1868
2135
  for (const pe of preflight) {
1869
- const intendedRef = ref + "/" + componentSubRef(pe.component);
2136
+ const intendedRef = ref + "/" + componentSubRef$1(pe.component);
1870
2137
  const status = await assertEntryReplaceable(pe.entryPath, {
1871
2138
  intendedRef,
1872
2139
  intendedCanonical: pe.intendedCanonical,
@@ -2053,7 +2320,7 @@ function parsePluginRef$1(ref) {
2053
2320
  };
2054
2321
  throw new Error(`invalid plugin ref: ${ref}`);
2055
2322
  }
2056
- function componentSubRef(c) {
2323
+ function componentSubRef$1(c) {
2057
2324
  switch (c.kind) {
2058
2325
  case "plugin-skill": return `skills/${c.skillName}`;
2059
2326
  case "plugin-agent-manifest": return `agents/${c.filename}`;
@@ -2140,14 +2407,6 @@ async function commitPluginInstall(params) {
2140
2407
  else await copyFileShallow(pe.intendedCanonical, pe.entryPath);
2141
2408
  }
2142
2409
  }
2143
- if (opts.agents.includes("codex") && components.ruleManifests.length > 0) await injectCodexAgentsMdForPlugin({
2144
- pluginCanonical,
2145
- pluginRef: ref,
2146
- pluginKey: `${scopeNamespace}:${pluginName}`,
2147
- ruleManifests: components.ruleManifests,
2148
- scope: opts.scope,
2149
- cwd: opts.cwd
2150
- });
2151
2410
  if (opts.agents.includes("codex") && components.agentManifests.length > 0) await registerCodexAgents({
2152
2411
  pluginCanonical,
2153
2412
  agentFilenames: components.agentManifests,
@@ -2172,38 +2431,6 @@ async function copyFileShallow(src, dest) {
2172
2431
  await mkdir(dirname(dest), { recursive: true });
2173
2432
  await writeFile(dest, buf);
2174
2433
  }
2175
- async function injectCodexAgentsMdForPlugin(params) {
2176
- const codexAgentsMd = join(params.scope === "global" ? getAgentHome("codex") : join(params.cwd ?? process.cwd(), ".codex"), "AGENTS.md");
2177
- const canonicalAgentsMd = join(getStoreRoot(params.scope, params.cwd), "codex-agents.md");
2178
- let userContent = "";
2179
- try {
2180
- if ((await stat(codexAgentsMd)).isFile()) userContent = await readFile(codexAgentsMd, "utf-8");
2181
- } catch {}
2182
- let canonicalContent = "";
2183
- try {
2184
- canonicalContent = await readFile(canonicalAgentsMd, "utf-8");
2185
- } catch {}
2186
- if (!canonicalContent) canonicalContent = takeoverWithUserContent(userContent);
2187
- const rules = [];
2188
- for (const fname of params.ruleManifests) {
2189
- const content = await readFile(join(params.pluginCanonical, "rules", fname), "utf-8");
2190
- rules.push({
2191
- relativePath: `rules/${fname}`,
2192
- content
2193
- });
2194
- }
2195
- const updated = injectPluginRules(canonicalContent, {
2196
- ref: params.pluginRef,
2197
- key: params.pluginKey
2198
- }, rules);
2199
- await mkdir(dirname(canonicalAgentsMd), { recursive: true });
2200
- await writeFile(canonicalAgentsMd, updated, "utf-8");
2201
- try {
2202
- await rm(codexAgentsMd, { force: true });
2203
- } catch {}
2204
- await mkdir(dirname(codexAgentsMd), { recursive: true });
2205
- await symlink(canonicalAgentsMd, codexAgentsMd);
2206
- }
2207
2434
  async function registerCodexAgents(params) {
2208
2435
  const configToml = join(params.scope === "global" ? getAgentHome("codex") : join(params.cwd ?? process.cwd(), ".codex"), "config.toml");
2209
2436
  let raw = "";
@@ -2497,8 +2724,6 @@ function track(data) {
2497
2724
  if (!portalUrl) portalUrl = loadConfig().portal;
2498
2725
  try {
2499
2726
  const bodyPayload = {};
2500
- if ("event" in data) bodyPayload.event = data.event;
2501
- else if (data.event_type) bodyPayload.event_type = data.event_type;
2502
2727
  for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) bodyPayload[key] = value;
2503
2728
  const p = fetch(`${portalUrl}${TELEMETRY_URL_SUFFIX}`, {
2504
2729
  method: "POST",
@@ -2705,16 +2930,16 @@ var WellKnownProvider = class {
2705
2930
  }
2706
2931
  };
2707
2932
  const wellKnownProvider = new WellKnownProvider();
2708
- const AGENTS_DIR$1 = ".agents";
2709
- const LOCK_FILE$1 = ".skill-lock.json";
2933
+ const AGENTS_DIR = ".agents";
2934
+ const LOCK_FILE = ".skill-lock.json";
2710
2935
  const CURRENT_VERSION$1 = 3;
2711
- function getSkillLockPath$1() {
2936
+ function getSkillLockPath() {
2712
2937
  const xdgStateHome = process.env.XDG_STATE_HOME;
2713
- if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE$1);
2714
- return join(homedir(), AGENTS_DIR$1, LOCK_FILE$1);
2938
+ if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
2939
+ return join(homedir(), AGENTS_DIR, LOCK_FILE);
2715
2940
  }
2716
- async function readSkillLock$1() {
2717
- const lockPath = getSkillLockPath$1();
2941
+ async function readSkillLock() {
2942
+ const lockPath = getSkillLockPath();
2718
2943
  try {
2719
2944
  const content = await readFile(lockPath, "utf-8");
2720
2945
  const parsed = JSON.parse(content);
@@ -2726,7 +2951,7 @@ async function readSkillLock$1() {
2726
2951
  }
2727
2952
  }
2728
2953
  async function writeSkillLock(lock) {
2729
- const lockPath = getSkillLockPath$1();
2954
+ const lockPath = getSkillLockPath();
2730
2955
  await mkdir(dirname(lockPath), { recursive: true });
2731
2956
  await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2732
2957
  }
@@ -2746,14 +2971,8 @@ function getGitHubToken() {
2746
2971
  } catch {}
2747
2972
  return null;
2748
2973
  }
2749
- async function fetchSkillFolderHash(ownerRepo, skillPath, token, ref) {
2750
- const { fetchRepoTree, getSkillFolderHashFromTree } = await Promise.resolve().then(() => blob_exports);
2751
- const tree = await fetchRepoTree(ownerRepo, ref, token);
2752
- if (!tree) return null;
2753
- return getSkillFolderHashFromTree(tree, skillPath);
2754
- }
2755
2974
  async function addSkillToLock(skillName, entry) {
2756
- const lock = await readSkillLock$1();
2975
+ const lock = await readSkillLock();
2757
2976
  const now = (/* @__PURE__ */ new Date()).toISOString();
2758
2977
  const existingEntry = lock.skills[skillName];
2759
2978
  lock.skills[skillName] = {
@@ -2764,20 +2983,14 @@ async function addSkillToLock(skillName, entry) {
2764
2983
  await writeSkillLock(lock);
2765
2984
  }
2766
2985
  async function removeSkillFromLock(skillName) {
2767
- const lock = await readSkillLock$1();
2986
+ const lock = await readSkillLock();
2768
2987
  if (!(skillName in lock.skills)) return false;
2769
2988
  delete lock.skills[skillName];
2770
2989
  await writeSkillLock(lock);
2771
2990
  return true;
2772
2991
  }
2773
- async function getSkillFromLock(skillName) {
2774
- return (await readSkillLock$1()).skills[skillName] ?? null;
2775
- }
2776
- async function getAllLockedSkills() {
2777
- return (await readSkillLock$1()).skills;
2778
- }
2779
2992
  async function addPluginToLock(pluginName, entry) {
2780
- const lock = await readSkillLock$1();
2993
+ const lock = await readSkillLock();
2781
2994
  if (!lock.plugins) lock.plugins = {};
2782
2995
  const now = (/* @__PURE__ */ new Date()).toISOString();
2783
2996
  const existing = lock.plugins[pluginName];
@@ -2789,7 +3002,7 @@ async function addPluginToLock(pluginName, entry) {
2789
3002
  await writeSkillLock(lock);
2790
3003
  }
2791
3004
  async function removePluginFromLock(pluginName) {
2792
- const lock = await readSkillLock$1();
3005
+ const lock = await readSkillLock();
2793
3006
  if (!lock.plugins || !(pluginName in lock.plugins)) return false;
2794
3007
  delete lock.plugins[pluginName];
2795
3008
  await writeSkillLock(lock);
@@ -2804,10 +3017,10 @@ function createEmptyLockFile() {
2804
3017
  };
2805
3018
  }
2806
3019
  async function isPromptDismissed(promptKey) {
2807
- return (await readSkillLock$1()).dismissed?.[promptKey] === true;
3020
+ return (await readSkillLock()).dismissed?.[promptKey] === true;
2808
3021
  }
2809
3022
  async function dismissPrompt(promptKey) {
2810
- const lock = await readSkillLock$1();
3023
+ const lock = await readSkillLock();
2811
3024
  if (!lock.dismissed) lock.dismissed = {};
2812
3025
  lock.dismissed[promptKey] = true;
2813
3026
  await writeSkillLock(lock);
@@ -2877,6 +3090,13 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
2877
3090
  lock.skills[skillName] = entry;
2878
3091
  await writeLocalLock(lock, cwd);
2879
3092
  }
3093
+ async function removeSkillFromLocalLock(skillName, cwd) {
3094
+ const lock = await readLocalLock(cwd);
3095
+ if (!(skillName in lock.skills)) return false;
3096
+ delete lock.skills[skillName];
3097
+ await writeLocalLock(lock, cwd);
3098
+ return true;
3099
+ }
2880
3100
  async function addPluginToLocalLock(pluginName, entry, cwd) {
2881
3101
  const lock = await readLocalLock(cwd);
2882
3102
  if (!lock.plugins) lock.plugins = {};
@@ -2896,13 +3116,6 @@ function createEmptyLocalLock() {
2896
3116
  skills: {}
2897
3117
  };
2898
3118
  }
2899
- var blob_exports = /* @__PURE__ */ __exportAll({
2900
- fetchRepoTree: () => fetchRepoTree,
2901
- findSkillMdPaths: () => findSkillMdPaths,
2902
- getSkillFolderHashFromTree: () => getSkillFolderHashFromTree,
2903
- toSkillSlug: () => toSkillSlug,
2904
- tryBlobInstall: () => tryBlobInstall
2905
- });
2906
3119
  const DOWNLOAD_BASE_URL = process.env.SKILLS_DOWNLOAD_URL || "https://skills.sh";
2907
3120
  const FETCH_TIMEOUT = 1e4;
2908
3121
  function toSkillSlug(name) {
@@ -3159,7 +3372,7 @@ function shortenPath$2(fullPath, cwd) {
3159
3372
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
3160
3373
  return fullPath;
3161
3374
  }
3162
- function formatList$1(items, maxShow = 5) {
3375
+ function formatList(items, maxShow = 5) {
3163
3376
  if (items.length <= maxShow) return items.join(", ");
3164
3377
  const shown = items.slice(0, maxShow);
3165
3378
  const remaining = items.length - maxShow;
@@ -3175,16 +3388,18 @@ function splitAgentsByType(agentTypes) {
3175
3388
  symlinked
3176
3389
  };
3177
3390
  }
3178
- function buildAgentSummaryLines(targetAgents, installMode) {
3391
+ function buildAgentSummaryLines(targetAgents, entry) {
3179
3392
  const lines = [];
3180
3393
  const { universal, symlinked } = splitAgentsByType(targetAgents);
3181
- if (installMode === "symlink") {
3182
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
3183
- if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
3184
- } else {
3185
- const allNames = targetAgents.map((a) => agents[a].displayName);
3186
- lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
3187
- }
3394
+ if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList(universal)}`);
3395
+ if (symlinked.length > 0) if (entry) {
3396
+ const sanitized = sanitizeName(entry.skillName);
3397
+ for (const a of targetAgents) {
3398
+ if (isUniversalAgent(a)) continue;
3399
+ const entryPath = join(getAgentBaseDir(a, entry.scope === "global", entry.cwd), sanitized);
3400
+ lines.push(` ${import_picocolors.default.dim("symlink →")} ${import_picocolors.default.cyan(shortenPath$2(entryPath, entry.cwd))} ${import_picocolors.default.dim(`(${agents[a].displayName})`)}`);
3401
+ }
3402
+ } else lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList(symlinked)}`);
3188
3403
  return lines;
3189
3404
  }
3190
3405
  function ensureUniversalAgents(targetAgents) {
@@ -3198,9 +3413,9 @@ function buildResultLines(results, targetAgents) {
3198
3413
  const { universal, symlinked: symlinkAgents } = splitAgentsByType(targetAgents);
3199
3414
  const successfulSymlinks = results.filter((r) => !r.symlinkFailed && !universal.includes(r.agent)).map((r) => r.agent);
3200
3415
  const failedSymlinks = results.filter((r) => r.symlinkFailed).map((r) => r.agent);
3201
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
3202
- if (successfulSymlinks.length > 0) lines.push(` ${import_picocolors.default.dim("symlinked:")} ${formatList$1(successfulSymlinks)}`);
3203
- if (failedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("copied:")} ${formatList$1(failedSymlinks)}`);
3416
+ if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList(universal)}`);
3417
+ if (successfulSymlinks.length > 0) lines.push(` ${import_picocolors.default.dim("symlinked:")} ${formatList(successfulSymlinks)}`);
3418
+ if (failedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("copied:")} ${formatList(failedSymlinks)}`);
3204
3419
  return lines;
3205
3420
  }
3206
3421
  function multiselect(opts) {
@@ -3218,15 +3433,26 @@ async function promptForAgents(message, choices) {
3218
3433
  required: true
3219
3434
  });
3220
3435
  }
3436
+ const PLUGIN_ENTRY_ROOTS = {
3437
+ "claude-code": [".claude"],
3438
+ cursor: [".cursor"],
3439
+ codex: [".codex", ".agents/skills"]
3440
+ };
3221
3441
  function buildAgentChoices(agentList, options = {}) {
3222
3442
  return agentList.map((a) => {
3223
3443
  const cfg = agents[a];
3224
- const baseHint = options.global && cfg.globalSkillsDir ? cfg.globalSkillsDir : cfg.skillsDir;
3225
- const isShared = !options.global && isUniversalAgent(a);
3444
+ let hint;
3445
+ if (options.forPlugin) {
3446
+ const roots = PLUGIN_ENTRY_ROOTS[a];
3447
+ hint = options.global ? roots.map((r) => `~/${r}`).join(", ") : roots.join(", ");
3448
+ } else {
3449
+ const baseHint = options.global && cfg.globalSkillsDir ? cfg.globalSkillsDir : cfg.skillsDir;
3450
+ hint = !options.global && isUniversalAgent(a) ? `${baseHint}, shared` : baseHint;
3451
+ }
3226
3452
  return {
3227
3453
  value: a,
3228
3454
  label: cfg.displayName,
3229
- hint: isShared ? `${baseHint}, shared` : baseHint
3455
+ hint
3230
3456
  };
3231
3457
  });
3232
3458
  }
@@ -3234,7 +3460,7 @@ async function selectAgentsInteractive(options) {
3234
3460
  const supportsGlobalFilter = (a) => !options.global || agents[a].globalSkillsDir;
3235
3461
  return await searchMultiselect({
3236
3462
  message: "Which agents do you want to install to?",
3237
- items: buildAgentChoices(Object.keys(agents).filter(supportsGlobalFilter), options),
3463
+ items: buildAgentChoices(getPickerVisibleAgents().filter(supportsGlobalFilter), options),
3238
3464
  initialSelected: [],
3239
3465
  required: true
3240
3466
  });
@@ -3323,7 +3549,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3323
3549
  M.info("Installing to all agents");
3324
3550
  } else {
3325
3551
  M.info("Select agents to install skills to");
3326
- const selected = await promptForAgents("Which agents do you want to install to?", buildAgentChoices(Object.keys(agents), { global: options.global }));
3552
+ const selected = await promptForAgents("Which agents do you want to install to?", buildAgentChoices(getPickerVisibleAgents(), { global: options.global }));
3327
3553
  if (pD(selected)) {
3328
3554
  xe("Installation cancelled");
3329
3555
  process.exit(0);
@@ -3366,27 +3592,6 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3366
3592
  }
3367
3593
  installGlobally = scope;
3368
3594
  }
3369
- let installMode = options.copy ? "copy" : "symlink";
3370
- const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
3371
- if (!options.copy && !options.yes && uniqueDirs.size > 1) {
3372
- const modeChoice = await ve({
3373
- message: "Installation method",
3374
- options: [{
3375
- value: "symlink",
3376
- label: "Symlink (Recommended)",
3377
- hint: "Single source of truth, easy updates"
3378
- }, {
3379
- value: "copy",
3380
- label: "Copy to all agents",
3381
- hint: "Independent copies for each agent"
3382
- }]
3383
- });
3384
- if (pD(modeChoice)) {
3385
- xe("Installation cancelled");
3386
- process.exit(0);
3387
- }
3388
- installMode = modeChoice;
3389
- } else if (uniqueDirs.size <= 1) installMode = "copy";
3390
3595
  const cwd = process.cwd();
3391
3596
  const summaryLines = [];
3392
3597
  targetAgents.map((a) => agents[a].displayName);
@@ -3404,11 +3609,15 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3404
3609
  if (summaryLines.length > 0) summaryLines.push("");
3405
3610
  const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
3406
3611
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
3407
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
3612
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, {
3613
+ skillName: skill.installName,
3614
+ scope: installGlobally ? "global" : "project",
3615
+ cwd
3616
+ }));
3408
3617
  if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
3409
3618
  const skillOverwrites = overwriteStatus.get(skill.installName);
3410
3619
  const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
3411
- if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
3620
+ if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
3412
3621
  }
3413
3622
  console.log();
3414
3623
  Me(summaryLines.join("\n"), "Installation Summary");
@@ -3424,10 +3633,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3424
3633
  spinner.start("Installing skills...");
3425
3634
  const results = [];
3426
3635
  for (const skill of selectedSkills) for (const agent of targetAgents) {
3427
- const result = await installWellKnownSkillForAgent(skill, agent, {
3428
- global: installGlobally,
3429
- mode: installMode
3430
- });
3636
+ const result = await installWellKnownSkillForAgent(skill, agent, { global: installGlobally });
3431
3637
  results.push({
3432
3638
  skill: skill.installName,
3433
3639
  agent: agents[agent].displayName,
@@ -3479,29 +3685,21 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3479
3685
  bySkill.set(r.skill, skillResults);
3480
3686
  }
3481
3687
  const skillCount = bySkill.size;
3482
- const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
3688
+ const symlinkFailures = successful.filter((r) => r.symlinkFailed);
3483
3689
  const copiedAgents = symlinkFailures.map((r) => r.agent);
3484
3690
  const resultLines = [];
3485
3691
  for (const [skillName, skillResults] of bySkill) {
3486
3692
  const firstResult = skillResults[0];
3487
- if (firstResult.mode === "copy") {
3488
- resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3489
- for (const r of skillResults) {
3490
- const shortPath = shortenPath$2(r.path, cwd);
3491
- resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
3492
- }
3493
- } else {
3494
- if (firstResult.canonicalPath) {
3495
- const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3496
- resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3497
- } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
3498
- resultLines.push(...buildResultLines(skillResults, targetAgents));
3499
- }
3693
+ if (firstResult.canonicalPath) {
3694
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3695
+ resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3696
+ } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
3697
+ resultLines.push(...buildResultLines(skillResults, targetAgents));
3500
3698
  }
3501
3699
  const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3502
3700
  Me(resultLines.join("\n"), title);
3503
3701
  if (symlinkFailures.length > 0) {
3504
- M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
3702
+ M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
3505
3703
  M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
3506
3704
  }
3507
3705
  }
@@ -3515,14 +3713,16 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3515
3713
  await promptForFindSkills(options, targetAgents);
3516
3714
  }
3517
3715
  async function runTeamBatchAdd(teamScope, options) {
3518
- const cfg = loadConfig();
3716
+ let cfg = loadConfig();
3519
3717
  if (!cfg.token) {
3520
- console.log();
3521
- console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Login required for team installs"));
3522
- console.log();
3523
- console.log(` Run ${import_picocolors.default.cyan("shoplazza-ai-dev-cli login")} first.`);
3524
- console.log();
3525
- process.exit(1);
3718
+ await runLogin({ force: true });
3719
+ cfg = loadConfig();
3720
+ if (!cfg.token) {
3721
+ console.log();
3722
+ console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Login required for team installs"));
3723
+ console.log();
3724
+ process.exit(1);
3725
+ }
3526
3726
  }
3527
3727
  console.log();
3528
3728
  Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
@@ -3537,10 +3737,9 @@ async function runTeamBatchAdd(teamScope, options) {
3537
3737
  Se(import_picocolors.default.red(msg));
3538
3738
  process.exit(1);
3539
3739
  }
3540
- const skills = items.filter((i) => i.kind === "skill");
3541
- spinner.stop(`Found ${skills.length} skill${skills.length === 1 ? "" : "s"}`);
3542
- if (skills.length === 0) {
3543
- Se(import_picocolors.default.dim(`Team "${teamScope}" has no skills.`));
3740
+ spinner.stop(`Found ${items.length} asset${items.length === 1 ? "" : "s"}`);
3741
+ if (items.length === 0) {
3742
+ Se(import_picocolors.default.dim(`Team "${teamScope}" has no assets.`));
3544
3743
  return;
3545
3744
  }
3546
3745
  const validAgents = Object.keys(agents);
@@ -3616,8 +3815,8 @@ async function runTeamBatchAdd(teamScope, options) {
3616
3815
  global: installGlobally
3617
3816
  };
3618
3817
  let failures = 0;
3619
- for (const item of skills) {
3620
- M.step(`Installing ${import_picocolors.default.cyan(item.name)}`);
3818
+ for (const item of items) {
3819
+ M.step(`Installing ${import_picocolors.default.cyan(item.name)} (${item.kind})`);
3621
3820
  try {
3622
3821
  await runAdd([item.ref], childOptions);
3623
3822
  } catch (err) {
@@ -3627,9 +3826,9 @@ async function runTeamBatchAdd(teamScope, options) {
3627
3826
  }
3628
3827
  }
3629
3828
  console.log();
3630
- if (failures === 0) Se(import_picocolors.default.green(`Installed ${skills.length} skill${skills.length === 1 ? "" : "s"} from team "${teamScope}"`));
3829
+ if (failures === 0) Se(import_picocolors.default.green(`Installed ${items.length} asset${items.length === 1 ? "" : "s"} from team "${teamScope}"`));
3631
3830
  else {
3632
- Se(import_picocolors.default.yellow(`Installed ${skills.length - failures}/${skills.length} skills (${failures} failed) from team "${teamScope}"`));
3831
+ Se(import_picocolors.default.yellow(`Installed ${items.length - failures}/${items.length} assets (${failures} failed) from team "${teamScope}"`));
3633
3832
  process.exit(1);
3634
3833
  }
3635
3834
  await promptForFindSkills(options, targetAgents);
@@ -3730,7 +3929,10 @@ async function runAdd(args, options = {}) {
3730
3929
  spinner.stop(`${Object.keys(agents).length} agents`);
3731
3930
  if (options.yes) targetAgents = installedAgents.length > 0 ? ensureUniversalAgents(installedAgents) : validAgents;
3732
3931
  else if (installedAgents.length === 0) {
3733
- const selected = await promptForAgents("Which IDEs do you want to install this plugin to?", buildAgentChoices(Object.keys(agents), { global: options.global }));
3932
+ const selected = await promptForAgents("Which IDEs do you want to install this plugin to?", buildAgentChoices(getPickerVisibleAgents(), {
3933
+ global: options.global,
3934
+ forPlugin: true
3935
+ }));
3734
3936
  if (pD(selected)) {
3735
3937
  xe("Installation cancelled");
3736
3938
  await cleanup(tempDir);
@@ -3741,7 +3943,10 @@ async function runAdd(args, options = {}) {
3741
3943
  targetAgents = ensureUniversalAgents(installedAgents);
3742
3944
  M.info(`Installing to: ${import_picocolors.default.cyan(agents[installedAgents[0]].displayName)}`);
3743
3945
  } else {
3744
- const selected = await selectAgentsInteractive({ global: options.global });
3946
+ const selected = await selectAgentsInteractive({
3947
+ global: options.global,
3948
+ forPlugin: true
3949
+ });
3745
3950
  if (pD(selected)) {
3746
3951
  xe("Installation cancelled");
3747
3952
  await cleanup(tempDir);
@@ -3776,8 +3981,8 @@ async function runAdd(args, options = {}) {
3776
3981
  const summaryLines = [];
3777
3982
  summaryLines.push(import_picocolors.default.cyan(shortenPath$2(previewCanonical, process.cwd())));
3778
3983
  const agentNames = targetAgents.map((a) => agents[a].displayName);
3779
- if (agentNames.length > 0) summaryLines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(agentNames)}`);
3780
- if (existsSync(previewCanonical)) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(agentNames)}`);
3984
+ if (agentNames.length > 0) summaryLines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList(agentNames)}`);
3985
+ if (existsSync(previewCanonical)) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList(agentNames)}`);
3781
3986
  console.log();
3782
3987
  Me(summaryLines.join("\n"), "Installation Summary");
3783
3988
  }
@@ -3848,7 +4053,7 @@ async function runAdd(args, options = {}) {
3848
4053
  pluginPath: lockPluginPath,
3849
4054
  computedHash: lockHash
3850
4055
  });
3851
- M.info(`Recorded in lock file: ${import_picocolors.default.dim(getSkillLockPath$1())}`);
4056
+ M.info(`Recorded in lock file: ${import_picocolors.default.dim(getSkillLockPath())}`);
3852
4057
  } else {
3853
4058
  await addPluginToLocalLock(lockPluginName, {
3854
4059
  source: lockSource,
@@ -4133,7 +4338,7 @@ async function runAdd(args, options = {}) {
4133
4338
  M.info("Installing to all agents");
4134
4339
  } else {
4135
4340
  M.info("Select agents to install skills to");
4136
- const selected = await promptForAgents("Which agents do you want to install to?", buildAgentChoices(Object.keys(agents), { global: options.global }));
4341
+ const selected = await promptForAgents("Which agents do you want to install to?", buildAgentChoices(getPickerVisibleAgents(), { global: options.global }));
4137
4342
  if (pD(selected)) {
4138
4343
  xe("Installation cancelled");
4139
4344
  await cleanup(tempDir);
@@ -4179,28 +4384,6 @@ async function runAdd(args, options = {}) {
4179
4384
  }
4180
4385
  installGlobally = scope;
4181
4386
  }
4182
- let installMode = options.copy ? "copy" : "symlink";
4183
- const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
4184
- if (!options.copy && !options.yes && uniqueDirs.size > 1) {
4185
- const modeChoice = await ve({
4186
- message: "Installation method",
4187
- options: [{
4188
- value: "symlink",
4189
- label: "Symlink (Recommended)",
4190
- hint: "Single source of truth, easy updates"
4191
- }, {
4192
- value: "copy",
4193
- label: "Copy to all agents",
4194
- hint: "Independent copies for each agent"
4195
- }]
4196
- });
4197
- if (pD(modeChoice)) {
4198
- xe("Installation cancelled");
4199
- await cleanup(tempDir);
4200
- process.exit(0);
4201
- }
4202
- installMode = modeChoice;
4203
- }
4204
4387
  const cwd = process.cwd();
4205
4388
  const _summaryPortalRef = parsed.type === "portal" || parsed.type === "public" || parsed.type === "team" ? parsed.portalRef ?? null : null;
4206
4389
  const _summaryIsTeam = _summaryPortalRef?.startsWith("@teams/") ?? false;
@@ -4225,16 +4408,21 @@ async function runAdd(args, options = {}) {
4225
4408
  if (!groupedSummary[group]) groupedSummary[group] = [];
4226
4409
  groupedSummary[group].push(skill);
4227
4410
  } else ungroupedSummary.push(skill);
4228
- const _summaryStoreRoot = getStoreRoot(installGlobally ? "global" : "project", cwd);
4411
+ const _summaryScope = installGlobally ? "global" : "project";
4412
+ const _summaryStoreRoot = getStoreRoot(_summaryScope, cwd);
4229
4413
  const printSkillSummary = (skills) => {
4230
4414
  for (const skill of skills) {
4231
4415
  if (summaryLines.length > 0) summaryLines.push("");
4232
4416
  const shortCanonical = shortenPath$2(join(_summaryStoreRoot, "skills", `${_summaryScopeNs}__${sanitizeName(skill.name)}`), cwd);
4233
4417
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
4234
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
4418
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, {
4419
+ skillName: skill.name,
4420
+ scope: _summaryScope,
4421
+ cwd
4422
+ }));
4235
4423
  const skillOverwrites = overwriteStatus.get(skill.name);
4236
4424
  const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
4237
- if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
4425
+ if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
4238
4426
  }
4239
4427
  };
4240
4428
  const sortedGroups = Object.keys(groupedSummary).sort();
@@ -4285,17 +4473,13 @@ async function runAdd(args, options = {}) {
4285
4473
  result = await installBlobSkillForAgent({
4286
4474
  installName: blobSkill.name,
4287
4475
  files: blobSkill.files
4288
- }, agent, {
4289
- global: installGlobally,
4290
- mode: installMode
4291
- });
4476
+ }, agent, { global: installGlobally });
4292
4477
  } else {
4293
4478
  const skillRef = _installPortalRef ? `${_installPortalRef}/${skill.name ?? ""}` : skill.name ?? "unknown";
4294
4479
  result = await installSkillForAgent(skill, agent, {
4295
4480
  scope: installGlobally ? "global" : "project",
4296
4481
  ref: skillRef,
4297
4482
  scopeNamespace: _installScopeNs,
4298
- mode: installMode,
4299
4483
  forceOverwrite: options.force || options.yes,
4300
4484
  onConflictPrompt: (conflicts) => promptForeignOverwrite(conflicts, {
4301
4485
  yes: options.yes,
@@ -4408,7 +4592,7 @@ async function runAdd(args, options = {}) {
4408
4592
  const hash = await computeSkillFolderHash(join(repoRoot, dirname(rawSkillPath)));
4409
4593
  if (hash) skillFolderHash = hash;
4410
4594
  }
4411
- const lockSkillPath = portalDecomp && rawSkillPath && rawSkillPath.startsWith(portalDecomp.pathPrefix) ? rawSkillPath.slice(portalDecomp.pathPrefix.length) : rawSkillPath;
4595
+ const lockSkillPath = parsed.portalRef ? portalRefToSkillPath(parsed.portalRef) ?? rawSkillPath : portalDecomp && rawSkillPath && rawSkillPath.startsWith(portalDecomp.pathPrefix) ? rawSkillPath.slice(portalDecomp.pathPrefix.length) : rawSkillPath;
4412
4596
  await addSkillToLock(skill.name, {
4413
4597
  source: lockSource ?? normalizedSource ?? "",
4414
4598
  sourceType: parsed.type,
@@ -4428,7 +4612,7 @@ async function runAdd(args, options = {}) {
4428
4612
  if (successfulSkillNames.has(skillDisplayName)) try {
4429
4613
  const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
4430
4614
  const rawSkillPath = skillFiles[skill.name];
4431
- const lockSkillPath = portalDecomp && rawSkillPath && rawSkillPath.startsWith(portalDecomp.pathPrefix) ? rawSkillPath.slice(portalDecomp.pathPrefix.length) : rawSkillPath;
4615
+ const lockSkillPath = parsed.portalRef ? portalRefToSkillPath(parsed.portalRef) ?? rawSkillPath : portalDecomp && rawSkillPath && rawSkillPath.startsWith(portalDecomp.pathPrefix) ? rawSkillPath.slice(portalDecomp.pathPrefix.length) : rawSkillPath;
4432
4616
  await addSkillToLocalLock(skill.name, {
4433
4617
  source: lockSource || parsed.url,
4434
4618
  ref: parsed.ref,
@@ -4454,26 +4638,18 @@ async function runAdd(args, options = {}) {
4454
4638
  } else ungroupedResults.push(r);
4455
4639
  }
4456
4640
  const skillCount = bySkill.size;
4457
- const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
4641
+ const symlinkFailures = successful.filter((r) => r.symlinkFailed);
4458
4642
  const copiedAgents = symlinkFailures.map((r) => r.agent);
4459
4643
  const resultLines = [];
4460
4644
  const printSkillResults = (entries) => {
4461
4645
  for (const entry of entries) {
4462
4646
  const skillResults = bySkill.get(entry.skill) || [];
4463
4647
  const firstResult = skillResults[0];
4464
- if (firstResult.mode === "copy") {
4465
- resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill} ${import_picocolors.default.dim("(copied)")}`);
4466
- for (const r of skillResults) {
4467
- const shortPath = shortenPath$2(r.path, cwd);
4468
- resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
4469
- }
4470
- } else {
4471
- if (firstResult.canonicalPath) {
4472
- const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
4473
- resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
4474
- } else resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill}`);
4475
- resultLines.push(...buildResultLines(skillResults, targetAgents));
4476
- }
4648
+ if (firstResult.canonicalPath) {
4649
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
4650
+ resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
4651
+ } else resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill}`);
4652
+ resultLines.push(...buildResultLines(skillResults, targetAgents));
4477
4653
  }
4478
4654
  };
4479
4655
  const sortedResultGroups = Object.keys(groupedResults).sort();
@@ -4493,7 +4669,7 @@ async function runAdd(args, options = {}) {
4493
4669
  const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
4494
4670
  Me(resultLines.join("\n"), title);
4495
4671
  if (symlinkFailures.length > 0) {
4496
- M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
4672
+ M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
4497
4673
  M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
4498
4674
  }
4499
4675
  }
@@ -4615,8 +4791,7 @@ function parseAddOptions(args) {
4615
4791
  else if (arg === "--team") {
4616
4792
  i++;
4617
4793
  options.team = args[i];
4618
- } else if (arg === "--copy") options.copy = true;
4619
- else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
4794
+ } else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
4620
4795
  else if (arg && !arg.startsWith("-")) source.push(arg);
4621
4796
  }
4622
4797
  return {
@@ -4627,7 +4802,7 @@ function parseAddOptions(args) {
4627
4802
  const RESET$2 = "\x1B[0m";
4628
4803
  const BOLD$2 = "\x1B[1m";
4629
4804
  const DIM$2 = "\x1B[38;5;102m";
4630
- const TEXT$1 = "\x1B[38;5;145m";
4805
+ const TEXT$2 = "\x1B[38;5;145m";
4631
4806
  const CYAN$1 = "\x1B[36m";
4632
4807
  const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
4633
4808
  function formatInstalls(count) {
@@ -4672,7 +4847,7 @@ async function runSearchPrompt(initialQuery = "") {
4672
4847
  process.stdout.write(CLEAR_DOWN);
4673
4848
  const lines = [];
4674
4849
  const cursor = `${BOLD$2}_${RESET$2}`;
4675
- lines.push(`${TEXT$1}Search skills:${RESET$2} ${query}${cursor}`);
4850
+ lines.push(`${TEXT$2}Search skills:${RESET$2} ${query}${cursor}`);
4676
4851
  lines.push("");
4677
4852
  if (!query || query.length < 2) lines.push(`${DIM$2}Start typing to search (min 2 chars)${RESET$2}`);
4678
4853
  else if (results.length === 0 && loading) lines.push(`${DIM$2}Searching...${RESET$2}`);
@@ -4683,7 +4858,7 @@ async function runSearchPrompt(initialQuery = "") {
4683
4858
  const skill = visible[i];
4684
4859
  const isSelected = i === selectedIndex;
4685
4860
  const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " ";
4686
- const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`;
4861
+ const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$2}${skill.name}${RESET$2}`;
4687
4862
  const source = skill.source ? ` ${DIM$2}${skill.source}${RESET$2}` : "";
4688
4863
  const installs = formatInstalls(skill.installs);
4689
4864
  const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$2}` : "";
@@ -4793,11 +4968,6 @@ ${DIM$2} 1) npx skills find [query]${RESET$2}
4793
4968
  ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4794
4969
  if (query) {
4795
4970
  const results = await searchSkillsAPI(query);
4796
- track({
4797
- event: "find",
4798
- query,
4799
- resultCount: String(results.length)
4800
- });
4801
4971
  if (results.length === 0) {
4802
4972
  console.log(`${DIM$2}No skills found for "${query}"${RESET$2}`);
4803
4973
  return;
@@ -4807,7 +4977,7 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4807
4977
  for (const skill of results.slice(0, 6)) {
4808
4978
  const pkg = skill.source || skill.slug;
4809
4979
  const installs = formatInstalls(skill.installs);
4810
- console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
4980
+ console.log(`${TEXT$2}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
4811
4981
  console.log(`${DIM$2}└ https://skills.sh/${skill.slug}${RESET$2}`);
4812
4982
  console.log();
4813
4983
  }
@@ -4818,12 +4988,6 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4818
4988
  console.log();
4819
4989
  }
4820
4990
  const selected = await runSearchPrompt();
4821
- track({
4822
- event: "find",
4823
- query: "",
4824
- resultCount: selected ? "1" : "0",
4825
- interactive: "1"
4826
- });
4827
4991
  if (!selected) {
4828
4992
  console.log(`${DIM$2}Search cancelled${RESET$2}`);
4829
4993
  console.log();
@@ -4832,7 +4996,7 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4832
4996
  const pkg = selected.source || selected.slug;
4833
4997
  const skillName = selected.name;
4834
4998
  console.log();
4835
- console.log(`${TEXT$1}Installing ${BOLD$2}${skillName}${RESET$2} from ${DIM$2}${pkg}${RESET$2}...`);
4999
+ console.log(`${TEXT$2}Installing ${BOLD$2}${skillName}${RESET$2} from ${DIM$2}${pkg}${RESET$2}...`);
4836
5000
  console.log();
4837
5001
  const { source, options } = parseAddOptions([
4838
5002
  pkg,
@@ -4842,11 +5006,11 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4842
5006
  await runAdd(source, options);
4843
5007
  console.log();
4844
5008
  const info = getOwnerRepoFromString(pkg);
4845
- if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$2}View the skill at${RESET$2} ${TEXT$1}https://skills.sh/${selected.slug}${RESET$2}`);
4846
- else console.log(`${DIM$2}Discover more skills at${RESET$2} ${TEXT$1}https://skills.sh${RESET$2}`);
5009
+ if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$2}View the skill at${RESET$2} ${TEXT$2}https://skills.sh/${selected.slug}${RESET$2}`);
5010
+ else console.log(`${DIM$2}Discover more skills at${RESET$2} ${TEXT$2}https://skills.sh${RESET$2}`);
4847
5011
  console.log();
4848
5012
  }
4849
- const isCancelled$1 = (value) => typeof value === "symbol";
5013
+ const isCancelled = (value) => typeof value === "symbol";
4850
5014
  function shortenPath$1(fullPath, cwd) {
4851
5015
  const home = homedir();
4852
5016
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
@@ -4984,7 +5148,7 @@ async function runSync(args, options = {}) {
4984
5148
  } else {
4985
5149
  const selected = await searchMultiselect({
4986
5150
  message: "Which agents do you want to install to?",
4987
- items: Object.keys(agents).map((a) => {
5151
+ items: getPickerVisibleAgents().map((a) => {
4988
5152
  const cfg = agents[a];
4989
5153
  const baseHint = cfg.skillsDir;
4990
5154
  const hint = isUniversalAgent(a) ? `${baseHint}, shared` : baseHint;
@@ -4997,7 +5161,7 @@ async function runSync(args, options = {}) {
4997
5161
  initialSelected: [],
4998
5162
  required: true
4999
5163
  });
5000
- if (isCancelled$1(selected)) {
5164
+ if (isCancelled(selected)) {
5001
5165
  xe("Sync cancelled");
5002
5166
  process.exit(0);
5003
5167
  }
@@ -5007,7 +5171,7 @@ async function runSync(args, options = {}) {
5007
5171
  targetAgents = [...installedAgents];
5008
5172
  for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
5009
5173
  } else {
5010
- const allAgents = Object.keys(agents).filter((a) => installedAgents.includes(a));
5174
+ const allAgents = getPickerVisibleAgents().filter((a) => installedAgents.includes(a));
5011
5175
  const selected = await searchMultiselect({
5012
5176
  message: "Which agents do you want to install to?",
5013
5177
  items: allAgents.map((a) => {
@@ -5023,7 +5187,7 @@ async function runSync(args, options = {}) {
5023
5187
  initialSelected: installedAgents.filter((a) => allAgents.includes(a)),
5024
5188
  required: true
5025
5189
  });
5026
- if (isCancelled$1(selected)) {
5190
+ if (isCancelled(selected)) {
5027
5191
  xe("Sync cancelled");
5028
5192
  process.exit(0);
5029
5193
  }
@@ -5050,8 +5214,7 @@ async function runSync(args, options = {}) {
5050
5214
  for (const skill of toInstall) for (const agent of targetAgents) {
5051
5215
  const result = await installSkillForAgent(skill, agent, {
5052
5216
  global: false,
5053
- cwd,
5054
- mode: "symlink"
5217
+ cwd
5055
5218
  });
5056
5219
  results.push({
5057
5220
  skill: skill.name,
@@ -5102,12 +5265,6 @@ async function runSync(args, options = {}) {
5102
5265
  M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
5103
5266
  for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
5104
5267
  }
5105
- track({
5106
- event: "experimental_sync",
5107
- skillCount: String(toInstall.length),
5108
- successCount: String(successfulSkillNames.size),
5109
- agents: targetAgents.join(",")
5110
- });
5111
5268
  console.log();
5112
5269
  Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
5113
5270
  }
@@ -5183,26 +5340,14 @@ async function runInstallFromLock(args) {
5183
5340
  const RESET$1 = "\x1B[0m";
5184
5341
  const BOLD$1 = "\x1B[1m";
5185
5342
  const DIM$1 = "\x1B[38;5;102m";
5343
+ const TEXT$1 = "\x1B[38;5;145m";
5186
5344
  const CYAN = "\x1B[36m";
5187
5345
  const YELLOW = "\x1B[33m";
5188
- function shortenPath(fullPath, cwd) {
5189
- const home = homedir();
5190
- if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
5191
- if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
5192
- return fullPath;
5193
- }
5194
- function formatList(items, maxShow = 5) {
5195
- if (items.length <= maxShow) return items.join(", ");
5196
- const shown = items.slice(0, maxShow);
5197
- const remaining = items.length - maxShow;
5198
- return `${shown.join(", ")} +${remaining} more`;
5199
- }
5200
5346
  function parseListOptions(args) {
5201
5347
  const options = {};
5202
5348
  for (let i = 0; i < args.length; i++) {
5203
5349
  const arg = args[i];
5204
- if (arg === "-g" || arg === "--global") options.global = true;
5205
- else if (arg === "--json") options.json = true;
5350
+ if (arg === "--json") options.json = true;
5206
5351
  else if (arg === "-a" || arg === "--agent") {
5207
5352
  options.agent = options.agent || [];
5208
5353
  while (i + 1 < args.length && !args[i + 1].startsWith("-")) options.agent.push(args[++i]);
@@ -5210,152 +5355,393 @@ function parseListOptions(args) {
5210
5355
  }
5211
5356
  return options;
5212
5357
  }
5358
+ function shortenPath(fullPath, cwd) {
5359
+ const home = homedir();
5360
+ if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
5361
+ if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
5362
+ return fullPath;
5363
+ }
5364
+ function ledgerToComponent$1(c, pluginCanonical) {
5365
+ switch (c.type) {
5366
+ case "skill": return {
5367
+ kind: "plugin-skill",
5368
+ pluginCanonical,
5369
+ skillName: c.name
5370
+ };
5371
+ case "agent-manifest": return {
5372
+ kind: "plugin-agent-manifest",
5373
+ pluginCanonical,
5374
+ filename: c.name
5375
+ };
5376
+ case "agent-deps-dir": return {
5377
+ kind: "plugin-agent-deps-dir",
5378
+ pluginCanonical,
5379
+ dirname: c.name
5380
+ };
5381
+ case "rule-manifest": return {
5382
+ kind: "plugin-rule-manifest",
5383
+ pluginCanonical,
5384
+ filename: c.name
5385
+ };
5386
+ case "rule-deps-dir": return {
5387
+ kind: "plugin-rule-deps-dir",
5388
+ pluginCanonical,
5389
+ dirname: c.name
5390
+ };
5391
+ default: return null;
5392
+ }
5393
+ }
5394
+ async function entryHits(entryPath, canonicalRoot) {
5395
+ if (!entryPath) return false;
5396
+ try {
5397
+ const realEntry = await realpath(entryPath);
5398
+ let realCanon = canonicalRoot;
5399
+ try {
5400
+ realCanon = await realpath(canonicalRoot);
5401
+ } catch {}
5402
+ return realEntry === realCanon || realEntry.startsWith(realCanon + sep) || realEntry === canonicalRoot || realEntry.startsWith(canonicalRoot + sep);
5403
+ } catch {
5404
+ return false;
5405
+ }
5406
+ }
5407
+ function standaloneSkillEntryPaths(skillName, agent, cwd) {
5408
+ const paths = [];
5409
+ if (agent === "claude-code") {
5410
+ paths.push(join(cwd, ".claude", "skills", skillName));
5411
+ paths.push(join(homedir(), ".claude", "skills", skillName));
5412
+ } else if (agent === "cursor") {
5413
+ paths.push(join(cwd, ".cursor", "skills", skillName));
5414
+ paths.push(join(homedir(), ".cursor", "skills", skillName));
5415
+ } else if (agent === "codex") {
5416
+ paths.push(join(cwd, ".agents", "skills", skillName));
5417
+ paths.push(join(homedir(), ".agents", "skills", skillName));
5418
+ }
5419
+ return paths;
5420
+ }
5421
+ async function collectPluginLinks(pluginCanonical, installedComponents, agentFilter, cwd) {
5422
+ const out = [];
5423
+ for (const ag of agentFilter) for (const c of installedComponents) {
5424
+ const comp = ledgerToComponent$1(c, pluginCanonical);
5425
+ if (!comp) continue;
5426
+ for (const scope of ["project", "global"]) {
5427
+ const t = getInstallTargets(ag, comp, scope, cwd);
5428
+ if (!t.entry) continue;
5429
+ if (await entryHits(t.entry, pluginCanonical)) out.push({
5430
+ agent: ag,
5431
+ entryPath: t.entry,
5432
+ componentType: c.type,
5433
+ componentName: c.name
5434
+ });
5435
+ }
5436
+ }
5437
+ return out;
5438
+ }
5439
+ async function collectSkillLinks(canonical, skillName, agentFilter, cwd) {
5440
+ const out = [];
5441
+ for (const ag of agentFilter) for (const p of standaloneSkillEntryPaths(skillName, ag, cwd)) if (await entryHits(p, canonical)) out.push({
5442
+ agent: ag,
5443
+ entryPath: p
5444
+ });
5445
+ return out;
5446
+ }
5447
+ function uniqueAgents(links) {
5448
+ const seen = /* @__PURE__ */ new Set();
5449
+ for (const l of links) seen.add(l.agent);
5450
+ return Array.from(seen);
5451
+ }
5452
+ async function scanStore(cwd, agentFilter) {
5453
+ const storeRoot = getStoreRoot();
5454
+ const pluginsDir = join(storeRoot, "plugins");
5455
+ const skillsDir = join(storeRoot, "skills");
5456
+ const agentList = agentFilter ?? Object.keys(agents);
5457
+ const plugins = [];
5458
+ const skills = [];
5459
+ let pluginDirs = [];
5460
+ try {
5461
+ pluginDirs = (await readdir(pluginsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
5462
+ } catch {}
5463
+ for (const dirName of pluginDirs) {
5464
+ const canonical = join(pluginsDir, dirName);
5465
+ const ledgerPath = join(canonical, ".forge-plugin.json");
5466
+ let ledger;
5467
+ try {
5468
+ ledger = JSON.parse(await readFile(ledgerPath, "utf-8"));
5469
+ } catch {
5470
+ continue;
5471
+ }
5472
+ if (!ledger.ref || !ledger.name) continue;
5473
+ const installedComponents = ledger.installed_components ?? [];
5474
+ const components = {
5475
+ skills: [],
5476
+ agents: [],
5477
+ rules: []
5478
+ };
5479
+ for (const c of installedComponents) if (c.type === "skill") components.skills.push(c.name);
5480
+ else if (c.type === "agent-manifest" || c.type === "agent-deps-dir") components.agents.push(c.name);
5481
+ else if (c.type === "rule-manifest" || c.type === "rule-deps-dir") components.rules.push(c.name);
5482
+ const links = await collectPluginLinks(canonical, installedComponents, agentList, cwd);
5483
+ plugins.push({
5484
+ name: ledger.name,
5485
+ ref: ledger.ref,
5486
+ canonical,
5487
+ components,
5488
+ agents: uniqueAgents(links),
5489
+ links
5490
+ });
5491
+ }
5492
+ let skillDirs = [];
5493
+ try {
5494
+ skillDirs = (await readdir(skillsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
5495
+ } catch {}
5496
+ for (const dirName of skillDirs) {
5497
+ const canonical = join(skillsDir, dirName);
5498
+ const sidecarPath = join(canonical, ".forge-source.json");
5499
+ let sidecar;
5500
+ try {
5501
+ sidecar = JSON.parse(await readFile(sidecarPath, "utf-8"));
5502
+ } catch {
5503
+ continue;
5504
+ }
5505
+ if (sidecar.type && sidecar.type !== "skill") continue;
5506
+ if (sidecar.parent_plugin_ref) continue;
5507
+ if (!sidecar.name) continue;
5508
+ const links = await collectSkillLinks(canonical, sidecar.name, agentList, cwd);
5509
+ skills.push({
5510
+ name: sidecar.name,
5511
+ ref: sidecar.ref,
5512
+ canonical,
5513
+ agents: uniqueAgents(links),
5514
+ links
5515
+ });
5516
+ }
5517
+ plugins.sort((a, b) => a.name.localeCompare(b.name));
5518
+ skills.sort((a, b) => a.name.localeCompare(b.name));
5519
+ return {
5520
+ plugins,
5521
+ skills
5522
+ };
5523
+ }
5524
+ function pluralize(count, singular, plural) {
5525
+ return count === 1 ? `${count} ${singular}` : `${count} ${plural ?? singular + "s"}`;
5526
+ }
5527
+ function pluginComponentSummary(plugin) {
5528
+ const parts = [];
5529
+ parts.push(pluralize(plugin.components.skills.length, "skill"));
5530
+ parts.push(pluralize(plugin.components.agents.length, "agent"));
5531
+ parts.push(pluralize(plugin.components.rules.length, "rule"));
5532
+ return parts.join(" · ");
5533
+ }
5213
5534
  async function runList(args) {
5214
5535
  const options = parseListOptions(args);
5215
- const scope = options.global === true ? true : false;
5536
+ const cwd = process.cwd();
5216
5537
  let agentFilter;
5217
5538
  if (options.agent && options.agent.length > 0) {
5218
5539
  const validAgents = Object.keys(agents);
5219
- const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
5220
- if (invalidAgents.length > 0) {
5221
- console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$1}`);
5540
+ const invalid = options.agent.filter((a) => !validAgents.includes(a));
5541
+ if (invalid.length > 0) {
5542
+ console.log(`${YELLOW}Invalid agents: ${invalid.join(", ")}${RESET$1}`);
5222
5543
  console.log(`${DIM$1}Valid agents: ${validAgents.join(", ")}${RESET$1}`);
5223
5544
  process.exit(1);
5224
5545
  }
5225
5546
  agentFilter = options.agent;
5226
5547
  }
5227
- const installedSkills = await listInstalledSkills({
5228
- global: scope,
5229
- agentFilter
5230
- });
5548
+ const inventory = await scanStore(cwd, agentFilter);
5231
5549
  if (options.json) {
5232
- const jsonOutput = installedSkills.map((skill) => ({
5233
- name: skill.name,
5234
- path: skill.canonicalPath,
5235
- scope: skill.scope,
5236
- agents: skill.agents.map((a) => agents[a].displayName)
5237
- }));
5238
- console.log(JSON.stringify(jsonOutput, null, 2));
5550
+ const out = {
5551
+ plugins: inventory.plugins.map((p) => ({
5552
+ name: p.name,
5553
+ ref: p.ref,
5554
+ canonical: p.canonical,
5555
+ agents: p.agents,
5556
+ components: p.components,
5557
+ links: p.links
5558
+ })),
5559
+ skills: inventory.skills.map((s) => ({
5560
+ name: s.name,
5561
+ ref: s.ref ?? null,
5562
+ canonical: s.canonical,
5563
+ agents: s.agents,
5564
+ links: s.links
5565
+ }))
5566
+ };
5567
+ console.log(JSON.stringify(out, null, 2));
5239
5568
  return;
5240
5569
  }
5241
- const lockedSkills = await getAllLockedSkills();
5242
- const cwd = process.cwd();
5243
- const scopeLabel = scope ? "Global" : "Project";
5244
- if (installedSkills.length === 0) {
5245
- if (options.json) {
5246
- console.log("[]");
5247
- return;
5248
- }
5249
- console.log(`${DIM$1}No ${scopeLabel.toLowerCase()} skills found.${RESET$1}`);
5250
- if (scope) console.log(`${DIM$1}Try listing project skills without -g${RESET$1}`);
5251
- else console.log(`${DIM$1}Try listing global skills with -g${RESET$1}`);
5570
+ if (inventory.plugins.length === 0 && inventory.skills.length === 0) {
5571
+ console.log(`${DIM$1}No forge-managed skills or plugins installed.${RESET$1}`);
5572
+ console.log(`${DIM$1}Run${RESET$1} ${TEXT$1}add <ref>${RESET$1} ${DIM$1}to install one.${RESET$1}`);
5252
5573
  return;
5253
5574
  }
5254
- function printSkill(skill, indent = false) {
5255
- const prefix = indent ? " " : "";
5256
- const shortPath = shortenPath(skill.canonicalPath, cwd);
5257
- const agentNames = skill.agents.map((a) => agents[a].displayName);
5258
- const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$1}`;
5259
- console.log(`${prefix}${CYAN}${sanitizeMetadata(skill.name)}${RESET$1} ${DIM$1}${shortPath}${RESET$1}`);
5260
- console.log(`${prefix} ${DIM$1}Agents:${RESET$1} ${agentInfo}`);
5261
- }
5262
- console.log(`${BOLD$1}${scopeLabel} Skills${RESET$1}`);
5263
- console.log();
5264
- const groupedSkills = {};
5265
- const ungroupedSkills = [];
5266
- for (const skill of installedSkills) {
5267
- const lockEntry = lockedSkills[skill.name];
5268
- if (lockEntry?.pluginName) {
5269
- const group = lockEntry.pluginName;
5270
- if (!groupedSkills[group]) groupedSkills[group] = [];
5271
- groupedSkills[group].push(skill);
5272
- } else ungroupedSkills.push(skill);
5273
- }
5274
- if (Object.keys(groupedSkills).length > 0) {
5275
- const sortedGroups = Object.keys(groupedSkills).sort();
5276
- for (const group of sortedGroups) {
5277
- const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5278
- console.log(`${BOLD$1}${title}${RESET$1}`);
5279
- const skills = groupedSkills[group];
5280
- if (skills) for (const skill of skills) printSkill(skill, true);
5575
+ if (inventory.plugins.length > 0) {
5576
+ console.log(`${BOLD$1}Plugins${RESET$1}`);
5577
+ for (const plugin of inventory.plugins) {
5578
+ const safeName = sanitizeMetadata(plugin.name);
5579
+ console.log(` ${CYAN}${safeName}${RESET$1} ${DIM$1}${plugin.ref}${RESET$1}`);
5580
+ console.log(` ${DIM$1}${shortenPath(plugin.canonical, cwd)}${RESET$1}`);
5581
+ console.log(` ${DIM$1}Components:${RESET$1} ${pluginComponentSummary(plugin)}`);
5582
+ printLinkedFrom(plugin.links, cwd);
5281
5583
  console.log();
5282
5584
  }
5283
- if (ungroupedSkills.length > 0) {
5284
- console.log(`${BOLD$1}General${RESET$1}`);
5285
- for (const skill of ungroupedSkills) printSkill(skill, true);
5585
+ }
5586
+ if (inventory.skills.length > 0) {
5587
+ console.log(`${BOLD$1}Skills${RESET$1}`);
5588
+ for (const skill of inventory.skills) {
5589
+ const safeName = sanitizeMetadata(skill.name);
5590
+ const refTail = skill.ref ? ` ${DIM$1}${skill.ref}${RESET$1}` : "";
5591
+ console.log(` ${CYAN}${safeName}${RESET$1}${refTail}`);
5592
+ console.log(` ${DIM$1}${shortenPath(skill.canonical, cwd)}${RESET$1}`);
5593
+ printLinkedFrom(skill.links, cwd);
5286
5594
  console.log();
5287
5595
  }
5288
- } else {
5289
- for (const skill of installedSkills) printSkill(skill);
5290
- console.log();
5291
5596
  }
5292
5597
  }
5293
- async function removeCommand(skillNames, options) {
5294
- const isGlobal = options.global ?? false;
5295
- const cwd = process.cwd();
5296
- const pluginRefRe = /^@[^/]+\/plugins\/[^/]+/;
5297
- const pluginRefs = skillNames.filter((n) => pluginRefRe.test(n));
5298
- if (pluginRefs.length > 0) {
5299
- const scope = isGlobal ? "global" : "project";
5300
- for (const ref of pluginRefs) try {
5301
- M.info(`Uninstalling plugin ${import_picocolors.default.cyan(ref)}...`);
5302
- await uninstallPlugin(ref, scope, cwd);
5303
- M.success(import_picocolors.default.green(`Plugin ${ref} uninstalled`));
5304
- } catch (err) {
5305
- M.error(import_picocolors.default.red(`Failed to uninstall plugin ${ref}: ${err instanceof Error ? err.message : String(err)}`));
5598
+ function printLinkedFrom(links, cwd) {
5599
+ if (links.length === 0) {
5600
+ console.log(` ${DIM$1}Linked from:${RESET$1} ${YELLOW}(none)${RESET$1}`);
5601
+ return;
5602
+ }
5603
+ const grouped = /* @__PURE__ */ new Map();
5604
+ for (const l of links) {
5605
+ const list = grouped.get(l.agent) ?? [];
5606
+ list.push(shortenPath(l.entryPath, cwd));
5607
+ grouped.set(l.agent, list);
5608
+ }
5609
+ if (grouped.size === 1) {
5610
+ const [agent, paths] = grouped.entries().next().value;
5611
+ if (paths.length === 1) {
5612
+ console.log(` ${DIM$1}Linked from:${RESET$1} ${agents[agent].displayName} ${DIM$1}${paths[0]}${RESET$1}`);
5613
+ return;
5306
5614
  }
5307
- const nonPluginNames = skillNames.filter((n) => !pluginRefRe.test(n));
5308
- if (nonPluginNames.length === 0) return;
5309
- skillNames = nonPluginNames;
5310
5615
  }
5311
- const spinner = Y();
5312
- spinner.start("Scanning for installed skills...");
5313
- const skillNamesSet = /* @__PURE__ */ new Set();
5314
- const scanDir = async (dir) => {
5315
- try {
5316
- const entries = await readdir(dir, { withFileTypes: true });
5317
- for (const entry of entries) if (entry.isDirectory()) skillNamesSet.add(entry.name);
5318
- } catch (err) {
5319
- if (err instanceof Error && err.code !== "ENOENT") M.warn(`Could not scan directory ${dir}: ${err.message}`);
5616
+ console.log(` ${DIM$1}Linked from:${RESET$1}`);
5617
+ const labelWidth = Math.max(...Array.from(grouped.keys()).map((a) => agents[a].displayName.length));
5618
+ for (const [agent, paths] of grouped) {
5619
+ const label = agents[agent].displayName.padEnd(labelWidth);
5620
+ if (paths.length === 1) console.log(` ${label} ${DIM$1}${paths[0]}${RESET$1}`);
5621
+ else {
5622
+ console.log(` ${label}`);
5623
+ for (const p of paths) console.log(` ${DIM$1}${p}${RESET$1}`);
5320
5624
  }
5321
- };
5322
- if (isGlobal) {
5323
- await scanDir(getCanonicalSkillsDir(true, cwd));
5324
- for (const agent of Object.values(agents)) if (agent.globalSkillsDir !== void 0) await scanDir(agent.globalSkillsDir);
5325
- } else {
5326
- await scanDir(getCanonicalSkillsDir(false, cwd));
5327
- for (const agent of Object.values(agents)) await scanDir(join(cwd, agent.skillsDir));
5328
5625
  }
5329
- const installedSkills = Array.from(skillNamesSet).sort();
5330
- spinner.stop(`Found ${installedSkills.length} unique installed skill(s)`);
5331
- if (installedSkills.length === 0) {
5332
- Se(import_picocolors.default.yellow("No skills found to remove."));
5333
- return;
5626
+ }
5627
+ const PLUGIN_REF_RE = /^@(?:public|teams\/[^/]+)\/plugins\/[^/]+/;
5628
+ function isPluginRef(s) {
5629
+ return PLUGIN_REF_RE.test(s);
5630
+ }
5631
+ function parseRemoveOptions(args) {
5632
+ const options = {};
5633
+ const skills = [];
5634
+ for (let i = 0; i < args.length; i++) {
5635
+ const arg = args[i];
5636
+ if (arg === "-y" || arg === "--yes") options.yes = true;
5637
+ else if (arg === "--all") options.all = true;
5638
+ else if (arg === "-a" || arg === "--agent") {
5639
+ options.agent = options.agent || [];
5640
+ i++;
5641
+ let nextArg = args[i];
5642
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
5643
+ options.agent.push(nextArg);
5644
+ i++;
5645
+ nextArg = args[i];
5646
+ }
5647
+ i--;
5648
+ } else if (arg && !arg.startsWith("-")) skills.push(arg);
5334
5649
  }
5650
+ return {
5651
+ skills,
5652
+ options
5653
+ };
5654
+ }
5655
+ function findOwningPlugin(name, inventory) {
5656
+ const lower = name.toLowerCase();
5657
+ for (const plugin of inventory.plugins) if (plugin.components.skills.some((s) => s.toLowerCase() === lower) || plugin.components.agents.some((a) => a.toLowerCase() === lower) || plugin.components.rules.some((r) => r.toLowerCase() === lower)) return plugin;
5658
+ return null;
5659
+ }
5660
+ async function removeCommand(skillNames, options) {
5661
+ const cwd = process.cwd();
5335
5662
  if (options.agent && options.agent.length > 0) {
5336
5663
  const validAgents = Object.keys(agents);
5337
- const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
5338
- if (invalidAgents.length > 0) {
5339
- M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
5664
+ const invalid = options.agent.filter((a) => !validAgents.includes(a));
5665
+ if (invalid.length > 0) {
5666
+ M.error(`Invalid agents: ${invalid.join(", ")}`);
5340
5667
  M.info(`Valid agents: ${validAgents.join(", ")}`);
5341
5668
  process.exit(1);
5342
5669
  }
5343
5670
  }
5344
- let selectedSkills = [];
5345
- if (options.all) selectedSkills = installedSkills;
5346
- else if (skillNames.length > 0) {
5347
- selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
5348
- if (selectedSkills.length === 0) {
5349
- M.error(`No matching skills found for: ${skillNames.join(", ")}`);
5671
+ const inventory = await scanStore(cwd);
5672
+ const targets = [];
5673
+ const errors = [];
5674
+ const explicitPluginRefs = skillNames.filter(isPluginRef);
5675
+ const bareNames = skillNames.filter((n) => !isPluginRef(n));
5676
+ for (const ref of explicitPluginRefs) {
5677
+ const plugin = inventory.plugins.find((pl) => pl.ref === ref);
5678
+ if (plugin) targets.push({
5679
+ kind: "plugin",
5680
+ plugin
5681
+ });
5682
+ else errors.push(`No installed plugin matching ref ${ref}`);
5683
+ }
5684
+ if (options.all) {
5685
+ if (bareNames.length > 0) errors.push("--all cannot be combined with positional names");
5686
+ for (const plugin of inventory.plugins) targets.push({
5687
+ kind: "plugin",
5688
+ plugin
5689
+ });
5690
+ for (const skill of inventory.skills) targets.push({
5691
+ kind: "skill",
5692
+ skill
5693
+ });
5694
+ } else if (bareNames.length > 0) for (const name of bareNames) {
5695
+ const lower = name.toLowerCase();
5696
+ const plugin = inventory.plugins.find((pl) => pl.name.toLowerCase() === lower);
5697
+ if (plugin) {
5698
+ targets.push({
5699
+ kind: "plugin",
5700
+ plugin
5701
+ });
5702
+ continue;
5703
+ }
5704
+ const skill = inventory.skills.find((s) => s.name.toLowerCase() === lower);
5705
+ if (skill) {
5706
+ targets.push({
5707
+ kind: "skill",
5708
+ skill
5709
+ });
5710
+ continue;
5711
+ }
5712
+ const owner = findOwningPlugin(name, inventory);
5713
+ if (owner) {
5714
+ errors.push(`${name} belongs to plugin ${owner.ref}\n Cannot remove individual plugin components.\n Run: remove ${owner.ref}`);
5715
+ continue;
5716
+ }
5717
+ errors.push(`No forge-managed skill or plugin matching '${name}'.`);
5718
+ }
5719
+ else if (explicitPluginRefs.length === 0) {
5720
+ if (inventory.plugins.length === 0 && inventory.skills.length === 0) {
5721
+ Se(import_picocolors.default.yellow("No skills or plugins installed."));
5350
5722
  return;
5351
5723
  }
5352
- } else {
5353
- const choices = installedSkills.map((s) => ({
5354
- value: s,
5355
- label: s
5356
- }));
5724
+ const choices = [];
5725
+ for (const plugin of inventory.plugins) {
5726
+ const summary = `${plugin.components.skills.length} skill(s), ${plugin.components.agents.length} agent(s), ${plugin.components.rules.length} rule(s)`;
5727
+ choices.push({
5728
+ value: {
5729
+ kind: "plugin",
5730
+ plugin
5731
+ },
5732
+ label: `[plugin] ${plugin.name}`,
5733
+ hint: summary
5734
+ });
5735
+ }
5736
+ for (const skill of inventory.skills) choices.push({
5737
+ value: {
5738
+ kind: "skill",
5739
+ skill
5740
+ },
5741
+ label: `[skill] ${skill.name}`
5742
+ });
5357
5743
  const selected = await fe({
5358
- message: `Select skills to remove ${import_picocolors.default.dim("(space to toggle)")}`,
5744
+ message: `Select what to remove ${import_picocolors.default.dim("(space to toggle)")}`,
5359
5745
  options: choices,
5360
5746
  required: true
5361
5747
  });
@@ -5363,172 +5749,157 @@ async function removeCommand(skillNames, options) {
5363
5749
  xe("Removal cancelled");
5364
5750
  process.exit(0);
5365
5751
  }
5366
- selectedSkills = selected;
5752
+ targets.push(...selected);
5367
5753
  }
5368
- let targetAgents;
5369
- if (options.agent && options.agent.length > 0) targetAgents = options.agent;
5370
- else {
5371
- targetAgents = Object.keys(agents);
5372
- spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`);
5754
+ for (const err of errors) M.error(err);
5755
+ if (targets.length === 0) {
5756
+ if (errors.length === 0) Se(import_picocolors.default.yellow("Nothing to remove."));
5757
+ return;
5373
5758
  }
5374
5759
  if (!options.yes) {
5375
5760
  console.log();
5376
- M.info("Skills to remove:");
5377
- for (const skill of selectedSkills) M.message(` ${import_picocolors.default.red("•")} ${skill}`);
5761
+ M.info("Planned removals:");
5762
+ for (const t of targets) {
5763
+ const tag = t.kind === "plugin" ? "[plugin]" : "[skill] ";
5764
+ const name = t.kind === "plugin" ? t.plugin?.name : t.skill?.name;
5765
+ const ref = t.kind === "plugin" ? t.plugin?.ref : t.skill?.ref;
5766
+ const refTail = ref ? ` ${import_picocolors.default.dim(ref)}` : "";
5767
+ M.message(` ${import_picocolors.default.red("•")} ${tag} ${name}${refTail}`);
5768
+ }
5378
5769
  console.log();
5379
- const confirmed = await ye({ message: `Are you sure you want to uninstall ${selectedSkills.length} skill(s)?` });
5770
+ const confirmed = await ye({ message: `Uninstall ${targets.length} item(s)?` });
5380
5771
  if (pD(confirmed) || !confirmed) {
5381
5772
  xe("Removal cancelled");
5382
5773
  process.exit(0);
5383
5774
  }
5384
5775
  }
5385
- spinner.start("Removing skills...");
5776
+ const spinner = Y();
5777
+ spinner.start("Removing...");
5386
5778
  const results = [];
5387
- for (const skillName of selectedSkills) try {
5388
- const canonicalPath = getCanonicalPath(skillName, {
5389
- global: isGlobal,
5390
- cwd
5779
+ const agentFilter = options.agent ?? null;
5780
+ for (const t of targets) if (t.kind === "plugin" && t.plugin) try {
5781
+ await uninstallPlugin(t.plugin, cwd, agentFilter);
5782
+ results.push({
5783
+ name: t.plugin.name,
5784
+ kind: "plugin",
5785
+ success: true
5391
5786
  });
5392
- for (const agentKey of targetAgents) {
5393
- const agent = agents[agentKey];
5394
- const skillPath = getInstallPath(skillName, agentKey, {
5395
- global: isGlobal,
5396
- cwd
5397
- });
5398
- const pathsToCleanup = new Set([skillPath]);
5399
- const sanitizedName = sanitizeName(skillName);
5400
- if (isGlobal && agent.globalSkillsDir) pathsToCleanup.add(join(agent.globalSkillsDir, sanitizedName));
5401
- else pathsToCleanup.add(join(cwd, agent.skillsDir, sanitizedName));
5402
- for (const pathToCleanup of pathsToCleanup) {
5403
- if (pathToCleanup === canonicalPath) continue;
5404
- try {
5405
- if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
5406
- recursive: true,
5407
- force: true
5408
- });
5409
- } catch (err) {
5410
- M.warn(`Could not remove skill from ${agent.displayName}: ${err instanceof Error ? err.message : String(err)}`);
5411
- }
5412
- }
5413
- }
5414
- const remainingAgents = (await detectInstalledAgents()).filter((a) => !targetAgents.includes(a));
5415
- let isStillUsed = false;
5416
- for (const agentKey of remainingAgents) if (await lstat(getInstallPath(skillName, agentKey, {
5417
- global: isGlobal,
5418
- cwd
5419
- })).catch(() => null)) {
5420
- isStillUsed = true;
5421
- break;
5422
- }
5423
- if (!isStillUsed) await rm(canonicalPath, {
5424
- recursive: true,
5425
- force: true
5787
+ } catch (err) {
5788
+ results.push({
5789
+ name: t.plugin.name,
5790
+ kind: "plugin",
5791
+ success: false,
5792
+ error: err instanceof Error ? err.message : String(err)
5426
5793
  });
5427
- const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null;
5428
- const effectiveSource = lockEntry?.source || "local";
5429
- const effectiveSourceType = lockEntry?.sourceType || "local";
5430
- if (isGlobal) await removeSkillFromLock(skillName);
5794
+ }
5795
+ else if (t.kind === "skill" && t.skill) try {
5796
+ await uninstallStandaloneSkill(t.skill, cwd, agentFilter);
5431
5797
  results.push({
5432
- skill: skillName,
5433
- success: true,
5434
- source: effectiveSource,
5435
- sourceType: effectiveSourceType
5798
+ name: t.skill.name,
5799
+ kind: "skill",
5800
+ success: true
5436
5801
  });
5437
5802
  } catch (err) {
5438
5803
  results.push({
5439
- skill: skillName,
5804
+ name: t.skill.name,
5805
+ kind: "skill",
5440
5806
  success: false,
5441
5807
  error: err instanceof Error ? err.message : String(err)
5442
5808
  });
5443
5809
  }
5444
- spinner.stop("Removal process complete");
5810
+ spinner.stop("Done");
5445
5811
  const successful = results.filter((r) => r.success);
5446
5812
  const failed = results.filter((r) => !r.success);
5447
- if (successful.length > 0) {
5448
- const bySource = /* @__PURE__ */ new Map();
5449
- for (const r of successful) {
5450
- const source = r.source || "local";
5451
- const existing = bySource.get(source) || { skills: [] };
5452
- existing.skills.push(r.skill);
5453
- existing.sourceType = r.sourceType;
5454
- bySource.set(source, existing);
5455
- }
5456
- for (const [source, data] of bySource) track({
5457
- event: "remove",
5458
- source,
5459
- skills: data.skills.join(","),
5460
- agents: targetAgents.join(","),
5461
- ...isGlobal && { global: "1" },
5462
- sourceType: data.sourceType
5463
- });
5464
- }
5465
- if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} skill(s)`));
5813
+ if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} item(s)`));
5466
5814
  if (failed.length > 0) {
5467
- M.error(import_picocolors.default.red(`Failed to remove ${failed.length} skill(s)`));
5468
- for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill}: ${r.error}`);
5815
+ M.error(import_picocolors.default.red(`Failed to remove ${failed.length} item(s)`));
5816
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.name}: ${r.error}`);
5469
5817
  }
5470
5818
  console.log();
5471
5819
  Se(import_picocolors.default.green("Done!"));
5472
5820
  }
5473
- function parseRemoveOptions(args) {
5474
- const options = {};
5475
- const skills = [];
5476
- for (let i = 0; i < args.length; i++) {
5477
- const arg = args[i];
5478
- if (arg === "-g" || arg === "--global") options.global = true;
5479
- else if (arg === "-y" || arg === "--yes") options.yes = true;
5480
- else if (arg === "--all") options.all = true;
5481
- else if (arg === "-a" || arg === "--agent") {
5482
- options.agent = options.agent || [];
5483
- i++;
5484
- let nextArg = args[i];
5485
- while (i < args.length && nextArg && !nextArg.startsWith("-")) {
5486
- options.agent.push(nextArg);
5487
- i++;
5488
- nextArg = args[i];
5489
- }
5490
- i--;
5491
- } else if (arg && !arg.startsWith("-")) skills.push(arg);
5821
+ async function uninstallStandaloneSkill(skill, cwd, agentFilter) {
5822
+ const storeRoot = getStoreRoot();
5823
+ const intendedRef = skill.ref ?? `<store>/${skill.name}`;
5824
+ const candidates = standaloneSkillEntryCandidates(skill.name, cwd, agentFilter);
5825
+ for (const entry of candidates) try {
5826
+ const status = await assertEntryReplaceable(entry, {
5827
+ intendedRef,
5828
+ intendedCanonical: skill.canonical,
5829
+ storeRoot
5830
+ });
5831
+ if (status.kind === "absent") continue;
5832
+ if (status.kind === "forge-self" || status.kind === "forge-other") await rm(entry, {
5833
+ recursive: true,
5834
+ force: true
5835
+ });
5836
+ else M.warn(`skipping ${entry} (${status.kind}) — not deleted (preserved by forge)`);
5837
+ } catch {}
5838
+ await rm(skill.canonical, {
5839
+ recursive: true,
5840
+ force: true
5841
+ });
5842
+ try {
5843
+ await removeSkillFromLock(skill.name);
5844
+ } catch {}
5845
+ try {
5846
+ await removeSkillFromLocalLock(skill.name, cwd);
5847
+ } catch {}
5848
+ }
5849
+ function standaloneSkillEntryCandidates(skillName, cwd, agentFilter) {
5850
+ const home = homedir();
5851
+ const list = agentFilter ?? Object.keys(agents);
5852
+ const out = [];
5853
+ for (const ag of list) if (ag === "claude-code") {
5854
+ out.push(join(cwd, ".claude", "skills", skillName));
5855
+ out.push(join(home, ".claude", "skills", skillName));
5856
+ } else if (ag === "cursor") {
5857
+ out.push(join(cwd, ".cursor", "skills", skillName));
5858
+ out.push(join(home, ".cursor", "skills", skillName));
5859
+ } else if (ag === "codex") {
5860
+ out.push(join(cwd, ".agents", "skills", skillName));
5861
+ out.push(join(home, ".agents", "skills", skillName));
5492
5862
  }
5493
- return {
5494
- skills,
5495
- options
5496
- };
5863
+ return out;
5497
5864
  }
5498
- async function uninstallPlugin(ref, scope, cwd) {
5499
- const { scopeNamespace, pluginName } = parsePluginRef(ref);
5500
- const storeRoot = getStoreRoot(scope, cwd);
5501
- const pluginCanonical = join(storeRoot, "plugins", `${scopeNamespace}__${pluginName}`);
5502
- const ledgerPath = join(pluginCanonical, ".forge-plugin.json");
5865
+ async function uninstallPlugin(plugin, cwd, agentFilter) {
5866
+ const storeRoot = getStoreRoot();
5867
+ const ledgerPath = join(plugin.canonical, ".forge-plugin.json");
5503
5868
  let ledger;
5504
5869
  try {
5505
5870
  ledger = JSON.parse(await readFile(ledgerPath, "utf-8"));
5506
5871
  } catch {
5507
- throw new Error(`plugin not installed (no ledger at ${ledgerPath})`);
5508
- }
5509
- for (const agent of ledger.install_modes) for (const c of ledger.installed_components) {
5510
- const entryPath = resolveEntryPath(agent, c, pluginCanonical, scope, cwd);
5511
- if (!entryPath) continue;
5512
- const status = await assertEntryReplaceable(entryPath, {
5513
- intendedRef: ref,
5514
- intendedCanonical: pluginCanonical,
5515
- storeRoot
5516
- });
5517
- if (status.kind === "forge-self") try {
5518
- await rm(entryPath, {
5519
- recursive: true,
5520
- force: true
5872
+ throw new Error(`plugin ledger missing or unreadable: ${ledgerPath}`);
5873
+ }
5874
+ const installModes = agentFilter ? ledger.install_modes.filter((a) => agentFilter.includes(a)) : ledger.install_modes;
5875
+ for (const agent of installModes) for (const c of ledger.installed_components) {
5876
+ const comp = ledgerToComponent(c, plugin.canonical);
5877
+ if (!comp) continue;
5878
+ for (const scope of ["project", "global"]) {
5879
+ const t = getInstallTargets(agent, comp, scope, cwd);
5880
+ if (!t.entry) continue;
5881
+ const intendedRef = `${plugin.ref}/${componentSubRef(comp)}`;
5882
+ const status = await assertEntryReplaceable(t.entry, {
5883
+ intendedRef,
5884
+ intendedCanonical: t.canonical,
5885
+ storeRoot
5521
5886
  });
5522
- } catch {}
5523
- else if (status.kind !== "absent") M.warn(`skipping ${entryPath} (${status.kind}) — not deleted (preserved by forge)`);
5887
+ if (status.kind === "absent") continue;
5888
+ if (status.kind === "forge-self") try {
5889
+ await rm(t.entry, {
5890
+ recursive: true,
5891
+ force: true
5892
+ });
5893
+ } catch {}
5894
+ else M.warn(`skipping ${t.entry} (${status.kind}) — not deleted (preserved by forge)`);
5895
+ }
5524
5896
  }
5525
- if (ledger.install_modes.includes("codex")) await cleanupCodexConfig(ledger, scope, cwd, scopeNamespace, pluginName);
5526
- try {
5527
- await rm(pluginCanonical, {
5528
- recursive: true,
5529
- force: true
5530
- });
5531
- } catch {}
5897
+ if (installModes.includes("codex")) for (const scope of ["project", "global"]) await cleanupCodexConfigToml(ledger, scope, cwd);
5898
+ await rm(plugin.canonical, {
5899
+ recursive: true,
5900
+ force: true
5901
+ });
5902
+ const { pluginName } = parsePluginRef(plugin.ref);
5532
5903
  try {
5533
5904
  await removePluginFromLock(pluginName);
5534
5905
  } catch {}
@@ -5536,75 +5907,63 @@ async function uninstallPlugin(ref, scope, cwd) {
5536
5907
  await removePluginFromLocalLock(pluginName, cwd);
5537
5908
  } catch {}
5538
5909
  }
5539
- function resolveEntryPath(agent, c, pluginCanonical, scope, cwd) {
5540
- let component;
5910
+ function componentSubRef(c) {
5911
+ switch (c.kind) {
5912
+ case "plugin-skill": return `skills/${c.skillName}`;
5913
+ case "plugin-agent-manifest": return `agents/${c.filename}`;
5914
+ case "plugin-agent-deps-dir": return `agents/${c.dirname}`;
5915
+ case "plugin-rule-manifest": return `rules/${c.filename}`;
5916
+ case "plugin-rule-deps-dir": return `rules/${c.dirname}`;
5917
+ }
5918
+ }
5919
+ function ledgerToComponent(c, pluginCanonical) {
5541
5920
  switch (c.type) {
5542
- case "skill":
5543
- component = {
5544
- kind: "plugin-skill",
5545
- pluginCanonical,
5546
- skillName: c.name
5547
- };
5548
- break;
5549
- case "agent-manifest":
5550
- component = {
5551
- kind: "plugin-agent-manifest",
5552
- pluginCanonical,
5553
- filename: c.name
5554
- };
5555
- break;
5556
- case "agent-deps-dir":
5557
- component = {
5558
- kind: "plugin-agent-deps-dir",
5559
- pluginCanonical,
5560
- dirname: c.name
5561
- };
5562
- break;
5563
- case "rule-manifest":
5564
- component = {
5565
- kind: "plugin-rule-manifest",
5566
- pluginCanonical,
5567
- filename: c.name
5568
- };
5569
- break;
5570
- case "rule-deps-dir":
5571
- component = {
5572
- kind: "plugin-rule-deps-dir",
5573
- pluginCanonical,
5574
- dirname: c.name
5575
- };
5576
- break;
5577
- default: return "";
5921
+ case "skill": return {
5922
+ kind: "plugin-skill",
5923
+ pluginCanonical,
5924
+ skillName: c.name
5925
+ };
5926
+ case "agent-manifest": return {
5927
+ kind: "plugin-agent-manifest",
5928
+ pluginCanonical,
5929
+ filename: c.name
5930
+ };
5931
+ case "agent-deps-dir": return {
5932
+ kind: "plugin-agent-deps-dir",
5933
+ pluginCanonical,
5934
+ dirname: c.name
5935
+ };
5936
+ case "rule-manifest": return {
5937
+ kind: "plugin-rule-manifest",
5938
+ pluginCanonical,
5939
+ filename: c.name
5940
+ };
5941
+ case "rule-deps-dir": return {
5942
+ kind: "plugin-rule-deps-dir",
5943
+ pluginCanonical,
5944
+ dirname: c.name
5945
+ };
5946
+ default: return null;
5578
5947
  }
5579
- return getInstallTargets(agent, component, scope, cwd).entry;
5580
5948
  }
5581
- async function cleanupCodexConfig(ledger, scope, cwd, scopeNs, pluginName) {
5582
- const configToml = join(scope === "global" ? getAgentHome("codex") : join(cwd ?? process.cwd(), ".codex"), "config.toml");
5949
+ async function cleanupCodexConfigToml(ledger, scope, cwd) {
5950
+ const configToml = join(scope === "global" ? getAgentHome("codex") : join(cwd, ".codex"), "config.toml");
5583
5951
  let tomlRaw = "";
5584
5952
  try {
5585
5953
  tomlRaw = await readFile(configToml, "utf-8");
5586
- } catch {}
5587
- if (tomlRaw) {
5588
- let changed = false;
5589
- for (const c of ledger.installed_components) if (c.type === "agent-manifest") {
5590
- const id = c.name.replace(/\.md$/, "");
5591
- const updated = removeCodexAgent(tomlRaw, id);
5592
- if (updated !== tomlRaw) {
5593
- tomlRaw = updated;
5594
- changed = true;
5595
- }
5596
- }
5597
- if (changed) await writeFile(configToml, tomlRaw, "utf-8");
5598
- }
5599
- const canonicalAgentsMd = join(getStoreRoot(scope, cwd), "codex-agents.md");
5600
- let agentsRaw = "";
5601
- try {
5602
- agentsRaw = await readFile(canonicalAgentsMd, "utf-8");
5603
5954
  } catch {
5604
5955
  return;
5605
5956
  }
5606
- const updatedAgentsMd = removePluginRules(agentsRaw, `${scopeNs}:${pluginName}`);
5607
- if (updatedAgentsMd !== agentsRaw) await writeFile(canonicalAgentsMd, updatedAgentsMd, "utf-8");
5957
+ let changed = false;
5958
+ for (const c of ledger.installed_components) if (c.type === "agent-manifest") {
5959
+ const id = c.name.replace(/\.md$/, "");
5960
+ const updated = removeCodexAgent(tomlRaw, id);
5961
+ if (updated !== tomlRaw) {
5962
+ tomlRaw = updated;
5963
+ changed = true;
5964
+ }
5965
+ }
5966
+ if (changed) await writeFile(configToml, tomlRaw, "utf-8");
5608
5967
  }
5609
5968
  function parsePluginRef(ref) {
5610
5969
  let m = /^@public\/plugins\/(.+)$/.exec(ref);
@@ -5619,202 +5978,6 @@ function parsePluginRef(ref) {
5619
5978
  };
5620
5979
  throw new Error(`invalid plugin ref: ${ref}`);
5621
5980
  }
5622
- function formatSourceInput(sourceUrl, ref) {
5623
- if (!ref) return sourceUrl;
5624
- return `${sourceUrl}#${ref}`;
5625
- }
5626
- function deriveSkillFolder(skillPath) {
5627
- let folder = skillPath;
5628
- if (folder.endsWith("/SKILL.md")) folder = folder.slice(0, -9);
5629
- else if (folder.endsWith("SKILL.md")) folder = folder.slice(0, -8);
5630
- if (folder.endsWith("/")) folder = folder.slice(0, -1);
5631
- return folder;
5632
- }
5633
- function appendFolderAndRef(source, skillPath, ref) {
5634
- const folder = deriveSkillFolder(skillPath);
5635
- const withFolder = folder ? `${source}/${folder}` : source;
5636
- return ref ? `${withFolder}#${ref}` : withFolder;
5637
- }
5638
- function buildUpdateInstallSource(entry) {
5639
- if (!entry.skillPath) return formatSourceInput(entry.sourceUrl, entry.ref);
5640
- return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
5641
- }
5642
- function buildLocalUpdateSource(entry) {
5643
- if (!entry.skillPath) return formatSourceInput(entry.source, entry.ref);
5644
- return appendFolderAndRef(entry.source, entry.skillPath, entry.ref);
5645
- }
5646
- function getRandomPort() {
5647
- return new Promise((resolve, reject) => {
5648
- const server = createServer();
5649
- server.listen(0, "127.0.0.1", () => {
5650
- const address = server.address();
5651
- if (address && typeof address !== "string" && "port" in address) {
5652
- const port = address.port;
5653
- server.close(() => resolve(port));
5654
- } else server.close(() => reject(/* @__PURE__ */ new Error("Failed to get port")));
5655
- });
5656
- server.on("error", reject);
5657
- });
5658
- }
5659
- function getSuccessHtml() {
5660
- return `
5661
- <!DOCTYPE html>
5662
- <html>
5663
- <head>
5664
- <meta charset="utf-8">
5665
- <title>Login Successful</title>
5666
- <style>
5667
- body { font-family: system-ui, -apple-system, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #10b981; color: white; }
5668
- .container { text-align: center; padding: 2rem; }
5669
- h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
5670
- p { opacity: 0.9; }
5671
- .spinner { width: 24px; height: 24px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 1s linear infinite; margin: 1rem auto; }
5672
- @keyframes spin { to { transform: rotate(360deg); } }
5673
- </style>
5674
- </head>
5675
- <body>
5676
- <div class="container">
5677
- <h1>Login Successful!</h1>
5678
- <p>You can close this window and return to the CLI.</p>
5679
- <div class="spinner"></div>
5680
- </div>
5681
- </body>
5682
- </html>
5683
- `.trim();
5684
- }
5685
- function getErrorHtml(error) {
5686
- return `
5687
- <!DOCTYPE html>
5688
- <html>
5689
- <head>
5690
- <meta charset="utf-8">
5691
- <title>Login Failed</title>
5692
- <style>
5693
- body { font-family: system-ui, -apple-system, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #ef4444; color: white; }
5694
- .container { text-align: center; padding: 2rem; }
5695
- h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
5696
- p { opacity: 0.9; }
5697
- </style>
5698
- </head>
5699
- <body>
5700
- <div class="container">
5701
- <h1>Login Failed</h1>
5702
- <p>${import_picocolors.default.red(error)}</p>
5703
- <p>Please try again.</p>
5704
- </div>
5705
- </body>
5706
- </html>
5707
- `.trim();
5708
- }
5709
- let callbackResolve = null;
5710
- async function runLogin(options = {}) {
5711
- const cfg = loadConfig();
5712
- if (options.logout) {
5713
- if (cfg.token) {
5714
- saveConfig({
5715
- token: void 0,
5716
- token_name: void 0
5717
- });
5718
- M.success("Logged out successfully.");
5719
- M.message(import_picocolors.default.dim("Your token has been removed from the config."));
5720
- } else M.message("You are not logged in.");
5721
- return;
5722
- }
5723
- if (cfg.token) {
5724
- M.message("You are already logged in.");
5725
- M.info(import_picocolors.default.dim(`Token: ${cfg.token_name || "default"}`));
5726
- const shouldReauth = await ye({
5727
- message: "Re-authenticate?",
5728
- initialValue: false
5729
- });
5730
- if (isCancelled(shouldReauth)) {
5731
- Se(import_picocolors.default.dim("Cancelled"));
5732
- return;
5733
- }
5734
- if (!shouldReauth) return;
5735
- }
5736
- console.log();
5737
- Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" login ")));
5738
- const portalUrl = cfg.portal;
5739
- const port = await getRandomPort();
5740
- const redirectUrl = `http://127.0.0.1:${port}/callback`;
5741
- const authUrl = `${portalUrl}/cli-auth?redirect=${encodeURIComponent(redirectUrl)}`;
5742
- M.message(`Opening browser for authentication...`);
5743
- M.info(import_picocolors.default.dim(authUrl));
5744
- await new Promise((resolve, reject) => {
5745
- exec(`open "${authUrl}"`, (err) => {
5746
- if (err) reject(err);
5747
- else resolve();
5748
- });
5749
- });
5750
- const result = await new Promise((resolve) => {
5751
- callbackResolve = resolve;
5752
- const server = createServer((req, res) => {
5753
- const url = new URL$1(req.url || "", `http://127.0.0.1:${port}`);
5754
- if (url.pathname === "/callback") {
5755
- const token = url.searchParams.get("token");
5756
- const tokenName = url.searchParams.get("token_name");
5757
- const error = url.searchParams.get("error");
5758
- if (error) {
5759
- res.writeHead(400, { "Content-Type": "text/html" });
5760
- res.end(getErrorHtml(error));
5761
- resolve({
5762
- success: false,
5763
- error
5764
- });
5765
- } else if (token) {
5766
- res.writeHead(200, { "Content-Type": "text/html" });
5767
- res.end(getSuccessHtml());
5768
- resolve({
5769
- success: true,
5770
- token,
5771
- tokenName: tokenName || "default"
5772
- });
5773
- } else {
5774
- res.writeHead(400, { "Content-Type": "text/html" });
5775
- res.end(getErrorHtml("No token received"));
5776
- resolve({
5777
- success: false,
5778
- error: "No token received"
5779
- });
5780
- }
5781
- } else {
5782
- res.writeHead(404);
5783
- res.end("Not found");
5784
- }
5785
- });
5786
- server.listen(port, "127.0.0.1", () => {});
5787
- setTimeout(() => {
5788
- server.close();
5789
- if (callbackResolve) callbackResolve({
5790
- success: false,
5791
- error: "Login timed out"
5792
- });
5793
- }, 12e4);
5794
- });
5795
- if (!result.success) {
5796
- Se(import_picocolors.default.red("Login failed: " + result.error));
5797
- process.exit(1);
5798
- }
5799
- saveConfig({
5800
- token: result.token,
5801
- token_name: result.tokenName
5802
- });
5803
- M.success("Login successful!");
5804
- M.message(import_picocolors.default.dim(`Token saved as: ${result.tokenName}`));
5805
- Se(import_picocolors.default.green("You can now publish skills."));
5806
- }
5807
- function isCancelled(value) {
5808
- return typeof value === "symbol";
5809
- }
5810
- function parseLoginOptions(args) {
5811
- const options = {};
5812
- for (let i = 0; i < args.length; i++) {
5813
- const arg = args[i];
5814
- if (arg === "--logout" || arg === "-l") options.logout = true;
5815
- }
5816
- return options;
5817
- }
5818
5981
  var import_lib = /* @__PURE__ */ __toESM(require_lib(), 1);
5819
5982
  async function getBootstrap(opts = {}) {
5820
5983
  const cfg = loadConfig();
@@ -5979,11 +6142,14 @@ function validatePath(path) {
5979
6142
  return { valid: true };
5980
6143
  }
5981
6144
  async function runPublish(paths, options = {}) {
5982
- const cfg = loadConfig();
6145
+ let cfg = loadConfig();
5983
6146
  if (!cfg.token) {
5984
- M.error("You must be logged in to publish.");
5985
- M.message(import_picocolors.default.dim("Run: npx shoplazza-ai-dev-cli login"));
5986
- process.exit(1);
6147
+ await runLogin({ force: true });
6148
+ cfg = loadConfig();
6149
+ if (!cfg.token) {
6150
+ M.error("Login required to publish.");
6151
+ process.exit(1);
6152
+ }
5987
6153
  }
5988
6154
  console.log();
5989
6155
  Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" publish ")));
@@ -6032,14 +6198,25 @@ async function runPublish(paths, options = {}) {
6032
6198
  form.set("asset_type", assetType);
6033
6199
  form.set("file", blob, filename);
6034
6200
  try {
6035
- const resp = await fetch(contributionsUrl, {
6201
+ const sendRequest = (token) => fetch(contributionsUrl, {
6036
6202
  method: "POST",
6037
6203
  headers: {
6038
- Authorization: `Bearer ${cfg.token}`,
6204
+ Authorization: `Bearer ${token}`,
6039
6205
  "User-Agent": "shoplazza-ai-dev-cli"
6040
6206
  },
6041
6207
  body: form
6042
6208
  });
6209
+ let resp = await sendRequest(cfg.token);
6210
+ if (resp.status === 401) {
6211
+ await resp.arrayBuffer().catch(() => void 0);
6212
+ await runLogin({ force: true });
6213
+ cfg = loadConfig();
6214
+ if (!cfg.token) {
6215
+ M.error(`Failed to publish ${name}: login required`);
6216
+ continue;
6217
+ }
6218
+ resp = await sendRequest(cfg.token);
6219
+ }
6043
6220
  if (!resp.ok) {
6044
6221
  const errorText = await resp.text();
6045
6222
  M.error(`Failed to publish ${name}: ${resp.status} ${errorText}`);
@@ -6049,12 +6226,6 @@ async function runPublish(paths, options = {}) {
6049
6226
  M.success(`Published: ${import_picocolors.default.green(name)}`);
6050
6227
  if (result?.asset_ref) M.message(import_picocolors.default.dim(`Asset: ${result.asset_ref}`));
6051
6228
  if (result?.mr_iid) M.message(import_picocolors.default.dim(`MR !${result.mr_iid}`));
6052
- track({
6053
- event_type: "publish",
6054
- asset_ref: result?.asset_ref ?? name,
6055
- visibility,
6056
- team_id: scope.startsWith("team:") ? scope.slice(5) : void 0
6057
- });
6058
6229
  } catch (err) {
6059
6230
  const message = err instanceof Error ? err.message : String(err);
6060
6231
  M.error(`Failed to publish ${name}: ${message}`);
@@ -6136,12 +6307,12 @@ function showBanner() {
6136
6307
  console.log();
6137
6308
  console.log(`${DIM}Shoplazza Forge skill CLI${RESET}`);
6138
6309
  console.log();
6139
- console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli add ${DIM}<source>${RESET} ${DIM}Add a skill${RESET}`);
6140
- console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli remove${RESET} ${DIM}Remove installed skills${RESET}`);
6141
- console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli list${RESET} ${DIM}List installed skills${RESET}`);
6310
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli add ${DIM}<source>${RESET} ${DIM}Add a skill or plugin${RESET}`);
6311
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli remove${RESET} ${DIM}Remove installed skills or plugins${RESET}`);
6312
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli list${RESET} ${DIM}List installed skills and plugins${RESET}`);
6142
6313
  console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
6143
6314
  console.log();
6144
- console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli update${RESET} ${DIM}Update installed skills${RESET}`);
6315
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli update${RESET} ${DIM}Refresh installed skills and plugins${RESET}`);
6145
6316
  console.log();
6146
6317
  console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli init ${DIM}[name]${RESET} ${DIM}Initialize a skill${RESET}`);
6147
6318
  console.log();
@@ -6153,96 +6324,90 @@ function showHelp() {
6153
6324
  ${BOLD}Usage:${RESET} shoplazza-ai-dev-cli <command> [options]
6154
6325
 
6155
6326
  ${BOLD}Manage Skills:${RESET}
6156
- add <source> Add a skill (alias: a)
6327
+ add <source> Add a skill or plugin (alias: a)
6157
6328
  e.g. @public/skills --skill <name>
6158
6329
  @teams/<scope>/skills --skill <name>
6159
- @public/skills/<name> # legacy, still supported
6330
+ @public/plugins/<name>
6160
6331
  https://gitlab.shoplazza.site/.../<repo>.git
6161
6332
  add --team <scope> Install every skill owned by a team
6162
- remove [skills] Remove installed skills
6163
- list, ls List installed skills
6333
+ remove [names...] Remove installed skills or plugins
6334
+ list, ls List installed skills and plugins
6164
6335
  find [query] Search for skills interactively
6165
6336
 
6166
6337
  ${BOLD}Updates:${RESET}
6167
- update [skills...] Update skills to latest versions (alias: upgrade)
6168
-
6169
- ${BOLD}Update Options:${RESET}
6170
- -g, --global Update global skills only
6171
- -p, --project Update project skills only
6172
- -y, --yes Skip scope prompt (auto-detect: project if in a project, else global)
6338
+ update [names...] Refresh installed skills/plugins from source (alias: upgrade)
6173
6339
 
6174
6340
  ${BOLD}Project:${RESET}
6175
6341
  init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
6176
6342
 
6177
6343
  ${BOLD}Add Options:${RESET}
6178
- -g, --global Install skill globally (user-level) instead of project-level
6344
+ -g, --global Install at global scope (entry symlinks under ~/)
6179
6345
  -a, --agent <agents> Specify agents to install to (use '*' for all agents)
6180
6346
  -s, --skill <skills> Specify skill names to install (use '*' for all skills)
6181
6347
  -l, --list List available skills in the source without installing
6182
6348
  -y, --yes Skip confirmation prompts
6183
6349
  --team <scope> Install every skill owned by the team
6184
6350
  (no positional source; mutually exclusive with <source>)
6185
- --copy Copy files instead of symlinking to agent directories
6186
6351
  --all Shorthand for --skill '*' --agent '*' -y
6187
6352
  --full-depth Search all subdirectories even when a root SKILL.md exists
6188
6353
 
6189
6354
  ${BOLD}Remove Options:${RESET}
6190
- -g, --global Remove from global scope
6191
- -a, --agent <agents> Remove from specific agents (use '*' for all agents)
6192
- -s, --skill <skills> Specify skills to remove (use '*' for all skills)
6355
+ -a, --agent <agents> Restrict cleanup to specific agents
6193
6356
  -y, --yes Skip confirmation prompts
6194
- --all Shorthand for --skill '*' --agent '*' -y
6357
+ --all Remove every installed skill and plugin
6195
6358
 
6196
6359
  ${BOLD}List Options:${RESET}
6197
- -g, --global List global skills (default: project)
6198
- -a, --agent <agents> Filter by specific agents
6360
+ -a, --agent <agents> Filter the "Linked from" column by agent
6199
6361
  --json Output as JSON (machine-readable, no ANSI codes)
6200
6362
 
6363
+ ${BOLD}Update Options:${RESET}
6364
+ -y, --yes Skip confirmation prompts
6365
+ --json Machine-readable summary
6366
+
6201
6367
  ${BOLD}Options:${RESET}
6202
6368
  --help, -h Show this help message
6203
6369
  --version, -v Show version number
6204
6370
 
6205
6371
  ${BOLD}Examples:${RESET}
6206
6372
  ${DIM}$${RESET} shoplazza-ai-dev-cli add @public/skills --skill systematic-debugging
6207
- ${DIM}$${RESET} shoplazza-ai-dev-cli add @public/skills --skill systematic-debugging -g
6208
- ${DIM}$${RESET} shoplazza-ai-dev-cli add @public/skills --skill <name> --agent claude-code cursor
6373
+ ${DIM}$${RESET} shoplazza-ai-dev-cli add @public/plugins/forge-demo-plugin
6209
6374
  ${DIM}$${RESET} shoplazza-ai-dev-cli add @teams/infra/skills --skill <name>
6210
6375
  ${DIM}$${RESET} shoplazza-ai-dev-cli remove ${DIM}# interactive${RESET}
6211
- ${DIM}$${RESET} shoplazza-ai-dev-cli remove my-skill ${DIM}# remove by name${RESET}
6212
- ${DIM}$${RESET} shoplazza-ai-dev-cli list ${DIM}# list project skills${RESET}
6213
- ${DIM}$${RESET} shoplazza-ai-dev-cli ls -g ${DIM}# list global skills${RESET}
6214
- ${DIM}$${RESET} shoplazza-ai-dev-cli find ${DIM}# interactive search${RESET}
6215
- ${DIM}$${RESET} shoplazza-ai-dev-cli find typescript ${DIM}# search by keyword${RESET}
6376
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove my-skill ${DIM}# bare-name resolved against store${RESET}
6377
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove @public/plugins/foo ${DIM}# explicit plugin ref${RESET}
6378
+ ${DIM}$${RESET} shoplazza-ai-dev-cli list ${DIM}# list everything in store${RESET}
6379
+ ${DIM}$${RESET} shoplazza-ai-dev-cli list --json
6216
6380
  ${DIM}$${RESET} shoplazza-ai-dev-cli update
6381
+ ${DIM}$${RESET} shoplazza-ai-dev-cli update some-skill
6217
6382
  ${DIM}$${RESET} shoplazza-ai-dev-cli init my-skill
6218
6383
  `);
6219
6384
  }
6220
6385
  function showRemoveHelp() {
6221
6386
  console.log(`
6222
- ${BOLD}Usage:${RESET} shoplazza-ai-dev-cli remove [skills...] [options]
6387
+ ${BOLD}Usage:${RESET} shoplazza-ai-dev-cli remove [names...] [options]
6223
6388
 
6224
6389
  ${BOLD}Description:${RESET}
6225
- Remove installed skills from agents. If no skill names are provided,
6226
- an interactive selection menu will be shown.
6390
+ Resolves each name against the forge store (~/.ai-dev-cli/store):
6391
+ - explicit plugin ref or bare plugin name → cascades full plugin uninstall
6392
+ - bare standalone-skill name → uninstalls just that skill
6393
+ - bare component name owned by a plugin → refused (uninstall the plugin instead)
6227
6394
 
6228
6395
  ${BOLD}Arguments:${RESET}
6229
- skills Optional skill names to remove (space-separated)
6396
+ names Optional skill / plugin names (space-separated). Empty triggers an
6397
+ interactive multiselect over everything currently in the store.
6230
6398
 
6231
6399
  ${BOLD}Options:${RESET}
6232
- -g, --global Remove from global scope (~/) instead of project scope
6233
- -a, --agent Remove from specific agents (use '*' for all agents)
6234
- -s, --skill Specify skills to remove (use '*' for all skills)
6235
- -y, --yes Skip confirmation prompts
6236
- --all Shorthand for --skill '*' --agent '*' -y
6400
+ -a, --agent Restrict entry cleanup to specific IDE agents
6401
+ -y, --yes Skip confirmation prompts
6402
+ --all Remove every installed skill and plugin
6237
6403
 
6238
6404
  ${BOLD}Examples:${RESET}
6239
- ${DIM}$${RESET} shoplazza-ai-dev-cli remove ${DIM}# interactive selection${RESET}
6240
- ${DIM}$${RESET} shoplazza-ai-dev-cli remove my-skill ${DIM}# remove specific skill${RESET}
6241
- ${DIM}$${RESET} shoplazza-ai-dev-cli remove skill1 skill2 -y ${DIM}# remove multiple skills${RESET}
6242
- ${DIM}$${RESET} shoplazza-ai-dev-cli remove --global my-skill ${DIM}# remove from global scope${RESET}
6243
- ${DIM}$${RESET} shoplazza-ai-dev-cli rm --agent claude-code my-skill ${DIM}# remove from specific agent${RESET}
6244
- ${DIM}$${RESET} shoplazza-ai-dev-cli remove --all ${DIM}# remove all skills${RESET}
6245
- ${DIM}$${RESET} shoplazza-ai-dev-cli remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
6405
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove ${DIM}# interactive${RESET}
6406
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove my-skill ${DIM}# resolve and remove${RESET}
6407
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove skill1 skill2 -y ${DIM}# remove several${RESET}
6408
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove @public/plugins/foo ${DIM}# explicit plugin ref${RESET}
6409
+ ${DIM}$${RESET} shoplazza-ai-dev-cli rm --agent claude-code my-skill ${DIM}# clean only Claude Code entry${RESET}
6410
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove --all ${DIM}# remove everything${RESET}
6246
6411
  `);
6247
6412
  }
6248
6413
  function runInit(args) {
@@ -6289,383 +6454,155 @@ Describe when this skill should be used.
6289
6454
  console.log(` ${DIM}Forge:${RESET} ${TEXT}shoplazza-ai-dev-cli publish ${displayPath.replace("/SKILL.md", "")}${RESET}`);
6290
6455
  console.log();
6291
6456
  }
6292
- const AGENTS_DIR = ".agents";
6293
- const LOCK_FILE = ".skill-lock.json";
6294
- const CURRENT_LOCK_VERSION = 3;
6295
- function getSkillLockPath() {
6296
- const xdgStateHome = process.env.XDG_STATE_HOME;
6297
- if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
6298
- return join(homedir(), AGENTS_DIR, LOCK_FILE);
6299
- }
6300
- function readSkillLock() {
6301
- const lockPath = getSkillLockPath();
6302
- try {
6303
- const content = readFileSync(lockPath, "utf-8");
6304
- const parsed = JSON.parse(content);
6305
- if (typeof parsed.version !== "number" || !parsed.skills) return {
6306
- version: CURRENT_LOCK_VERSION,
6307
- skills: {}
6308
- };
6309
- if (parsed.version < CURRENT_LOCK_VERSION) return {
6310
- version: CURRENT_LOCK_VERSION,
6311
- skills: {}
6312
- };
6313
- return parsed;
6314
- } catch {
6315
- return {
6316
- version: CURRENT_LOCK_VERSION,
6317
- skills: {}
6318
- };
6319
- }
6320
- }
6321
6457
  function parseUpdateOptions(args) {
6322
6458
  const options = {};
6323
- const positional = [];
6324
- for (const arg of args) if (arg === "-g" || arg === "--global") options.global = true;
6325
- else if (arg === "-p" || arg === "--project") options.project = true;
6326
- else if (arg === "-y" || arg === "--yes") options.yes = true;
6327
- else if (!arg.startsWith("-")) positional.push(arg);
6328
- if (positional.length > 0) options.skills = positional;
6459
+ const names = [];
6460
+ for (const arg of args) if (arg === "-y" || arg === "--yes") options.yes = true;
6461
+ else if (arg === "--json") options.json = true;
6462
+ else if (!arg.startsWith("-")) names.push(arg);
6463
+ if (names.length > 0) options.names = names;
6329
6464
  return options;
6330
6465
  }
6331
- function hasProjectSkills(cwd) {
6332
- const dir = cwd || process.cwd();
6333
- if (existsSync(join(dir, "skills-lock.json"))) return true;
6334
- const skillsDir = join(dir, ".agents", "skills");
6335
- try {
6336
- const entries = readdirSync(skillsDir, { withFileTypes: true });
6337
- for (const entry of entries) if (entry.isDirectory()) {
6338
- if (existsSync(join(skillsDir, entry.name, "SKILL.md"))) return true;
6466
+ function hasProjectEntryFor(canonical, cwd) {
6467
+ const candidates = [
6468
+ join(cwd, ".claude", "skills"),
6469
+ join(cwd, ".cursor", "skills"),
6470
+ join(cwd, ".agents", "skills"),
6471
+ join(cwd, ".claude", "agents"),
6472
+ join(cwd, ".cursor", "agents"),
6473
+ join(cwd, ".codex", "agents"),
6474
+ join(cwd, ".claude", "rules"),
6475
+ join(cwd, ".cursor", "rules")
6476
+ ];
6477
+ for (const dir of candidates) {
6478
+ if (!existsSync(dir)) continue;
6479
+ let entries;
6480
+ try {
6481
+ entries = readdirSyncSafe(dir);
6482
+ } catch {
6483
+ continue;
6339
6484
  }
6340
- } catch {}
6341
- return false;
6342
- }
6343
- async function resolveUpdateScope(options) {
6344
- if (options.skills && options.skills.length > 0) {
6345
- if (options.global) return "global";
6346
- if (options.project) return "project";
6347
- return "both";
6348
- }
6349
- if (options.global && options.project) return "both";
6350
- if (options.global) return "global";
6351
- if (options.project) return "project";
6352
- if (options.yes || !process.stdin.isTTY) return hasProjectSkills() ? "project" : "global";
6353
- const scope = await ve({
6354
- message: "Update scope",
6355
- options: [
6356
- {
6357
- value: "project",
6358
- label: "Project",
6359
- hint: "Update skills in current directory"
6360
- },
6361
- {
6362
- value: "global",
6363
- label: "Global",
6364
- hint: "Update skills in home directory"
6365
- },
6366
- {
6367
- value: "both",
6368
- label: "Both",
6369
- hint: "Update all skills"
6370
- }
6371
- ]
6372
- });
6373
- if (pD(scope)) {
6374
- xe("Cancelled");
6375
- process.exit(0);
6376
- }
6377
- return scope;
6378
- }
6379
- function matchesSkillFilter(name, filter) {
6380
- if (!filter || filter.length === 0) return true;
6381
- const lower = name.toLowerCase();
6382
- return filter.some((f) => f.toLowerCase() === lower);
6383
- }
6384
- function getSkipReason(entry) {
6385
- if (entry.sourceType === "local") return "Local path";
6386
- if (entry.sourceType === "git") return "Git URL";
6387
- if (entry.sourceType === "well-known") return "Well-known skill";
6388
- if (!entry.skillFolderHash) return "Private or deleted repo";
6389
- if (!entry.skillPath) return "No skill path recorded";
6390
- return "No version tracking";
6391
- }
6392
- function getInstallSource(skill) {
6393
- let url = skill.sourceUrl;
6394
- if (skill.sourceType === "well-known") {
6395
- const idx = url.indexOf("/.well-known/");
6396
- if (idx !== -1) url = url.slice(0, idx);
6397
- }
6398
- return formatSourceInput(url, skill.ref);
6399
- }
6400
- function printSkippedSkills(skipped) {
6401
- if (skipped.length === 0) return;
6402
- console.log();
6403
- console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
6404
- const grouped = /* @__PURE__ */ new Map();
6405
- for (const skill of skipped) {
6406
- const source = getInstallSource(skill);
6407
- const existing = grouped.get(source) || [];
6408
- existing.push(skill);
6409
- grouped.set(source, existing);
6410
- }
6411
- for (const [source, skills] of grouped) {
6412
- if (skills.length === 1) {
6413
- const skill = skills[0];
6414
- console.log(` ${TEXT}•${RESET} ${sanitizeMetadata(skill.name)} ${DIM}(${skill.reason})${RESET}`);
6415
- } else {
6416
- const reason = skills[0].reason;
6417
- const names = skills.map((s) => sanitizeMetadata(s.name)).join(", ");
6418
- console.log(` ${TEXT}•${RESET} ${names} ${DIM}(${reason})${RESET}`);
6485
+ for (const name of entries) {
6486
+ const entryPath = join(dir, name);
6487
+ try {
6488
+ const real = realpathSync(entryPath);
6489
+ if (real === canonical || real.startsWith(canonical + sep)) return true;
6490
+ } catch {}
6419
6491
  }
6420
- console.log(` ${DIM}To update: ${TEXT}npx skills add ${source} -g -y${RESET}`);
6421
6492
  }
6493
+ return false;
6422
6494
  }
6423
- async function getProjectSkillsForUpdate(skillFilter) {
6424
- const localLock = await readLocalLock();
6425
- const skills = [];
6426
- for (const [name, entry] of Object.entries(localLock.skills)) {
6427
- if (!matchesSkillFilter(name, skillFilter)) continue;
6428
- if (entry.sourceType === "node_modules" || entry.sourceType === "local") continue;
6429
- skills.push({
6430
- name,
6431
- source: entry.source,
6432
- entry
6433
- });
6495
+ function readdirSyncSafe(dir) {
6496
+ try {
6497
+ return readdirSync(dir);
6498
+ } catch {
6499
+ return [];
6434
6500
  }
6435
- return skills;
6436
6501
  }
6437
- async function updateGlobalSkills(skillFilter) {
6438
- const lock = readSkillLock();
6439
- const skillNames = Object.keys(lock.skills);
6440
- let successCount = 0;
6441
- let failCount = 0;
6442
- if (skillNames.length === 0) {
6443
- if (!skillFilter) {
6444
- console.log(`${DIM}No global skills tracked in lock file.${RESET}`);
6445
- console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package> -g${RESET}`);
6446
- }
6447
- return {
6448
- successCount,
6449
- failCount,
6450
- checkedCount: 0
6451
- };
6502
+ async function runUpdate(args = []) {
6503
+ const options = parseUpdateOptions(args);
6504
+ const cwd = process.cwd();
6505
+ const inventory = await scanStore(cwd);
6506
+ let targets = [...inventory.plugins.map((pl) => ({
6507
+ kind: "plugin",
6508
+ name: pl.name,
6509
+ ref: pl.ref,
6510
+ canonical: pl.canonical,
6511
+ hasProjectEntry: hasProjectEntryFor(pl.canonical, cwd)
6512
+ })), ...inventory.skills.filter((s) => Boolean(s.ref)).map((s) => ({
6513
+ kind: "skill",
6514
+ name: s.name,
6515
+ ref: s.ref,
6516
+ canonical: s.canonical,
6517
+ hasProjectEntry: hasProjectEntryFor(s.canonical, cwd)
6518
+ }))];
6519
+ if (options.names && options.names.length > 0) {
6520
+ const wanted = new Set(options.names.map((n) => n.toLowerCase()));
6521
+ targets = targets.filter((t) => wanted.has(t.name.toLowerCase()));
6452
6522
  }
6453
- const token = getGitHubToken();
6454
- const updates = [];
6455
- const skipped = [];
6456
- const checkable = [];
6457
- for (const skillName of skillNames) {
6458
- if (!matchesSkillFilter(skillName, skillFilter)) continue;
6459
- const entry = lock.skills[skillName];
6460
- if (!entry) continue;
6461
- if (!entry.skillFolderHash || !entry.skillPath) {
6462
- skipped.push({
6463
- name: skillName,
6464
- reason: getSkipReason(entry),
6465
- sourceUrl: entry.sourceUrl,
6466
- sourceType: entry.sourceType,
6467
- ref: entry.ref
6468
- });
6469
- continue;
6523
+ if (options.json) {
6524
+ const out = { targets: targets.map((t) => ({
6525
+ kind: t.kind,
6526
+ name: t.name,
6527
+ ref: t.ref
6528
+ })) };
6529
+ if (targets.length === 0) {
6530
+ console.log(JSON.stringify(out, null, 2));
6531
+ return;
6470
6532
  }
6471
- checkable.push({
6472
- name: skillName,
6473
- entry
6474
- });
6475
- }
6476
- for (let i = 0; i < checkable.length; i++) {
6477
- const { name: skillName, entry } = checkable[i];
6478
- process.stdout.write(`\r${DIM}Checking global skill ${i + 1}/${checkable.length}: ${sanitizeMetadata(skillName)}${RESET}\x1b[K`);
6479
- try {
6480
- const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token, entry.ref);
6481
- if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
6482
- name: skillName,
6483
- source: entry.source,
6484
- entry
6485
- });
6486
- } catch {}
6487
6533
  }
6488
- if (checkable.length > 0) process.stdout.write("\r\x1B[K");
6489
- const checkedCount = checkable.length + skipped.length;
6490
- if (checkable.length === 0 && skipped.length === 0) {
6491
- if (!skillFilter) console.log(`${DIM}No global skills to check.${RESET}`);
6492
- return {
6493
- successCount,
6494
- failCount,
6495
- checkedCount: 0
6496
- };
6534
+ if (targets.length === 0) {
6535
+ if (options.names && options.names.length > 0) console.log(`${DIM}No installed skills/plugins matching: ${options.names.join(", ")}${RESET}`);
6536
+ else {
6537
+ console.log(`${DIM}Nothing to update store is empty.${RESET}`);
6538
+ console.log(`${DIM}Install something with${RESET} ${TEXT}npx skills add <ref>${RESET}`);
6539
+ }
6540
+ return;
6497
6541
  }
6498
- if (checkable.length === 0 && skipped.length > 0) {
6499
- printSkippedSkills(skipped);
6500
- return {
6501
- successCount,
6502
- failCount,
6503
- checkedCount
6504
- };
6542
+ if (!options.yes && !options.json) {
6543
+ console.log(`${TEXT}Updating ${targets.length} entr${targets.length === 1 ? "y" : "ies"}...${RESET}`);
6544
+ console.log();
6505
6545
  }
6506
- if (updates.length === 0) {
6507
- console.log(`${TEXT}✓ All global skills are up to date${RESET}`);
6508
- return {
6509
- successCount,
6510
- failCount,
6511
- checkedCount
6512
- };
6546
+ const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
6547
+ if (!existsSync(cliEntry)) {
6548
+ console.error(`${BOLD}✗${RESET} CLI entrypoint not found at ${cliEntry}`);
6549
+ process.exitCode = 1;
6550
+ return;
6513
6551
  }
6514
- console.log(`${TEXT}Found ${updates.length} global update(s)${RESET}`);
6515
- console.log();
6516
- for (const update of updates) {
6517
- const safeName = sanitizeMetadata(update.name);
6518
- console.log(`${TEXT}Updating ${safeName}...${RESET}`);
6519
- const installUrl = buildUpdateInstallSource(update.entry);
6520
- const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
6521
- if (!existsSync(cliEntry)) {
6522
- failCount++;
6523
- console.log(` ${DIM}✗ Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET}`);
6524
- continue;
6525
- }
6526
- if (spawnSync(process.execPath, [
6527
- cliEntry,
6552
+ let success = 0;
6553
+ let fail = 0;
6554
+ const results = [];
6555
+ for (const t of targets) {
6556
+ const safeName = sanitizeMetadata(t.name);
6557
+ if (!options.json) console.log(`${TEXT}Updating ${safeName}...${RESET} ${DIM}(${t.ref})${RESET}`);
6558
+ const passthrough = [
6528
6559
  "add",
6529
- installUrl,
6530
- "-g",
6560
+ t.ref,
6531
6561
  "-y"
6532
- ], {
6533
- stdio: [
6534
- "inherit",
6562
+ ];
6563
+ if (!t.hasProjectEntry) passthrough.push("-g");
6564
+ const ok = spawnSync(process.execPath, [cliEntry, ...passthrough], {
6565
+ stdio: options.json ? [
6566
+ "ignore",
6535
6567
  "pipe",
6536
6568
  "pipe"
6537
- ],
6538
- encoding: "utf-8",
6539
- shell: process.platform === "win32"
6540
- }).status === 0) {
6541
- successCount++;
6542
- console.log(` ${TEXT}✓${RESET} Updated ${safeName}`);
6543
- } else {
6544
- failCount++;
6545
- console.log(` ${DIM}✗ Failed to update ${safeName}${RESET}`);
6546
- }
6547
- }
6548
- printSkippedSkills(skipped);
6549
- return {
6550
- successCount,
6551
- failCount,
6552
- checkedCount
6553
- };
6554
- }
6555
- async function updateProjectSkills(skillFilter) {
6556
- const projectSkills = await getProjectSkillsForUpdate(skillFilter);
6557
- let successCount = 0;
6558
- let failCount = 0;
6559
- if (projectSkills.length === 0) {
6560
- if (!skillFilter) {
6561
- console.log(`${DIM}No project skills to update.${RESET}`);
6562
- console.log(`${DIM}Install project skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
6563
- }
6564
- return {
6565
- successCount,
6566
- failCount,
6567
- foundCount: 0
6568
- };
6569
- }
6570
- const updatable = projectSkills.filter((s) => s.entry.skillPath);
6571
- const legacy = projectSkills.filter((s) => !s.entry.skillPath);
6572
- if (updatable.length === 0) {
6573
- console.log(`${DIM}No project skills can be updated in place.${RESET}`);
6574
- printLegacyProjectSkills(legacy);
6575
- return {
6576
- successCount,
6577
- failCount,
6578
- foundCount: projectSkills.length
6579
- };
6580
- }
6581
- console.log(`${TEXT}Refreshing ${updatable.length} project skill(s)...${RESET}`);
6582
- console.log();
6583
- for (const skill of updatable) {
6584
- const safeName = sanitizeMetadata(skill.name);
6585
- console.log(`${TEXT}Updating ${safeName}...${RESET}`);
6586
- const installUrl = buildLocalUpdateSource(skill.entry);
6587
- const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
6588
- if (!existsSync(cliEntry)) {
6589
- failCount++;
6590
- console.log(` ${DIM}✗ Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET}`);
6591
- continue;
6592
- }
6593
- if (spawnSync(process.execPath, [
6594
- cliEntry,
6595
- "add",
6596
- installUrl,
6597
- "--skill",
6598
- skill.name,
6599
- "-y"
6600
- ], {
6601
- stdio: [
6569
+ ] : [
6602
6570
  "inherit",
6603
6571
  "pipe",
6604
6572
  "pipe"
6605
6573
  ],
6606
6574
  encoding: "utf-8",
6607
6575
  shell: process.platform === "win32"
6608
- }).status === 0) {
6609
- successCount++;
6610
- console.log(` ${TEXT}✓${RESET} Updated ${safeName}`);
6576
+ }).status === 0;
6577
+ if (ok) {
6578
+ success++;
6579
+ if (!options.json) console.log(` ${TEXT}✓${RESET} updated ${safeName}`);
6611
6580
  } else {
6612
- failCount++;
6613
- console.log(` ${DIM}✗ Failed to update ${safeName}${RESET}`);
6581
+ fail++;
6582
+ if (!options.json) console.log(` ${DIM}✗ failed ${safeName}${RESET}`);
6614
6583
  }
6584
+ results.push({
6585
+ name: t.name,
6586
+ kind: t.kind,
6587
+ ok
6588
+ });
6615
6589
  }
6616
- printLegacyProjectSkills(legacy);
6617
- return {
6618
- successCount,
6619
- failCount,
6620
- foundCount: projectSkills.length
6621
- };
6622
- }
6623
- function printLegacyProjectSkills(legacy) {
6624
- if (legacy.length === 0) return;
6625
- console.log();
6626
- console.log(`${DIM}${legacy.length} project skill(s) cannot be updated automatically (installed before skillPath tracking):${RESET}`);
6627
- for (const skill of legacy) {
6628
- const reinstall = formatSourceInput(skill.entry.source, skill.entry.ref);
6629
- console.log(` ${TEXT}•${RESET} ${sanitizeMetadata(skill.name)}`);
6630
- console.log(` ${DIM}To refresh: ${TEXT}npx skills add ${reinstall} -y${RESET}`);
6590
+ if (options.json) {
6591
+ console.log(JSON.stringify({
6592
+ targets: targets.map((t) => ({
6593
+ kind: t.kind,
6594
+ name: t.name,
6595
+ ref: t.ref
6596
+ })),
6597
+ results,
6598
+ success,
6599
+ fail
6600
+ }, null, 2));
6601
+ return;
6631
6602
  }
6632
- }
6633
- async function runUpdate(args = []) {
6634
- const options = parseUpdateOptions(args);
6635
- const scope = await resolveUpdateScope(options);
6636
- if (options.skills) console.log(`${TEXT}Updating ${options.skills.join(", ")}...${RESET}`);
6637
- else console.log(`${TEXT}Checking for skill updates...${RESET}`);
6638
- console.log();
6639
- let totalSuccess = 0;
6640
- let totalFail = 0;
6641
- let totalFound = 0;
6642
- if (scope === "global" || scope === "both") {
6643
- if (scope === "both" && !options.skills) console.log(`${BOLD}Global Skills${RESET}`);
6644
- const { successCount, failCount, checkedCount } = await updateGlobalSkills(options.skills);
6645
- totalSuccess += successCount;
6646
- totalFail += failCount;
6647
- totalFound += checkedCount;
6648
- if (scope === "both" && !options.skills) console.log();
6649
- }
6650
- if (scope === "project" || scope === "both") {
6651
- if (scope === "both" && !options.skills) console.log(`${BOLD}Project Skills${RESET}`);
6652
- const { successCount, failCount, foundCount } = await updateProjectSkills(options.skills);
6653
- totalSuccess += successCount;
6654
- totalFail += failCount;
6655
- totalFound += foundCount;
6656
- }
6657
- if (options.skills && totalFound === 0) console.log(`${DIM}No installed skills found matching: ${options.skills.join(", ")}${RESET}`);
6658
6603
  console.log();
6659
- if (totalSuccess > 0) console.log(`${TEXT}✓ Updated ${totalSuccess} skill(s)${RESET}`);
6660
- if (totalFail > 0) console.log(`${DIM}Failed to update ${totalFail} skill(s)${RESET}`);
6661
- if (totalSuccess === 0 && totalFail === 0) {}
6662
- track({
6663
- event: "update",
6664
- scope,
6665
- skillCount: String(totalSuccess + totalFail),
6666
- successCount: String(totalSuccess),
6667
- failCount: String(totalFail)
6668
- });
6604
+ if (success > 0) console.log(`${TEXT}✓ Updated ${success} entr${success === 1 ? "y" : "ies"}${RESET}`);
6605
+ if (fail > 0) console.log(`${DIM}Failed to update ${fail} entr${fail === 1 ? "y" : "ies"}${RESET}`);
6669
6606
  console.log();
6670
6607
  }
6671
6608
  async function main() {