shoplazza-ai-dev-cli 0.1.2 → 0.1.5

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +258 -201
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -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,7 +318,8 @@ 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",
@@ -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
  }
@@ -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 });
@@ -742,27 +910,34 @@ async function ensureForgeHooks(agent) {
742
910
  try {
743
911
  const portal = loadConfig().portal || DEFAULT_PORTAL;
744
912
  const home = getAgentHome(agent);
745
- const scriptPath = join(home, HOOK_DIRNAME, HOOK_SCRIPT_BASENAME);
746
- await writeHookScript(scriptPath, portal);
747
- if (agent === "claude-code") await upsertClaudeCodeSettings(join(home, "settings.json"), scriptPath);
748
- else if (agent === "cursor") await upsertCursorHooks(join(home, "hooks.json"), scriptPath);
749
- 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
+ }
750
926
  } catch (err) {
751
927
  warnings.push(`Failed to install forge hooks for ${agent}: ${err instanceof Error ? err.message : String(err)}`);
752
928
  }
753
929
  return { warnings };
754
930
  }
755
- async function writeHookScript(scriptPath, portalUrl) {
756
- const next = getForgeHookScript(portalUrl);
931
+ async function writeScriptIfChanged(scriptPath, content) {
757
932
  let prev = null;
758
933
  if (existsSync(scriptPath)) try {
759
934
  prev = await readFile(scriptPath, "utf-8");
760
935
  } catch {
761
936
  prev = null;
762
937
  }
763
- if (prev === next) return;
938
+ if (prev === content) return;
764
939
  await mkdir(dirname(scriptPath), { recursive: true });
765
- await writeFile(scriptPath, next, {
940
+ await writeFile(scriptPath, content, {
766
941
  encoding: "utf-8",
767
942
  mode: 493
768
943
  });
@@ -1598,7 +1773,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1598
1773
  if (!isPathSafe(join(storeRoot, "skills"), canonicalDir)) return {
1599
1774
  success: false,
1600
1775
  path: "",
1601
- mode: "symlink",
1602
1776
  error: "Invalid skill name: path traversal detected",
1603
1777
  forgeOtherReplacements: []
1604
1778
  };
@@ -1625,7 +1799,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1625
1799
  if (!proceed) return {
1626
1800
  success: false,
1627
1801
  path: entry,
1628
- mode: "symlink",
1629
1802
  error: "aborted: user-owned content at entry path",
1630
1803
  forgeOtherReplacements: []
1631
1804
  };
@@ -1659,7 +1832,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1659
1832
  return {
1660
1833
  success: true,
1661
1834
  path: entry,
1662
- mode: "symlink",
1663
1835
  symlinkFailed: true,
1664
1836
  canonicalPath: canonicalDir,
1665
1837
  forgeOtherReplacements
@@ -1668,7 +1840,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1668
1840
  return {
1669
1841
  success: true,
1670
1842
  path: entry,
1671
- mode: "symlink",
1672
1843
  canonicalPath: canonicalDir,
1673
1844
  forgeOtherReplacements
1674
1845
  };
@@ -1676,7 +1847,6 @@ async function installSkillForAgent(skill, agentType, opts) {
1676
1847
  return {
1677
1848
  success: false,
1678
1849
  path: entry,
1679
- mode: "symlink",
1680
1850
  error: error instanceof Error ? error.message : "Unknown error",
1681
1851
  forgeOtherReplacements: []
1682
1852
  };
@@ -1689,7 +1859,6 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1689
1859
  if (isGlobal && agent.globalSkillsDir === void 0) return {
1690
1860
  success: false,
1691
1861
  path: "",
1692
- mode: options.mode ?? "symlink",
1693
1862
  error: `${agent.displayName} does not support global skill installation`,
1694
1863
  forgeOtherReplacements: []
1695
1864
  };
@@ -1698,39 +1867,25 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1698
1867
  const canonicalDir = join(canonicalBase, skillName);
1699
1868
  const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
1700
1869
  const agentDir = join(agentBase, skillName);
1701
- const installMode = options.mode ?? "symlink";
1702
1870
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1703
1871
  success: false,
1704
1872
  path: agentDir,
1705
- mode: installMode,
1706
1873
  error: "Invalid skill name: potential path traversal detected",
1707
1874
  forgeOtherReplacements: []
1708
1875
  };
1709
1876
  if (!isPathSafe(agentBase, agentDir)) return {
1710
1877
  success: false,
1711
1878
  path: agentDir,
1712
- mode: installMode,
1713
1879
  error: "Invalid skill name: potential path traversal detected",
1714
1880
  forgeOtherReplacements: []
1715
1881
  };
1716
1882
  try {
1717
- if (installMode === "copy") {
1718
- await cleanAndCreateDirectory(agentDir);
1719
- await copyDirectory(skill.path, agentDir);
1720
- return {
1721
- success: true,
1722
- path: agentDir,
1723
- mode: "copy",
1724
- forgeOtherReplacements: []
1725
- };
1726
- }
1727
1883
  await cleanAndCreateDirectory(canonicalDir);
1728
1884
  await copyDirectory(skill.path, canonicalDir);
1729
1885
  if (isGlobal && isUniversalAgent(agentType)) return {
1730
1886
  success: true,
1731
1887
  path: canonicalDir,
1732
1888
  canonicalPath: canonicalDir,
1733
- mode: "symlink",
1734
1889
  forgeOtherReplacements: []
1735
1890
  };
1736
1891
  if (!await createSymlink(canonicalDir, agentDir)) {
@@ -1740,7 +1895,6 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1740
1895
  success: true,
1741
1896
  path: agentDir,
1742
1897
  canonicalPath: canonicalDir,
1743
- mode: "symlink",
1744
1898
  symlinkFailed: true,
1745
1899
  forgeOtherReplacements: []
1746
1900
  };
@@ -1749,14 +1903,12 @@ async function _installSkillForAgentLegacy(skill, agentType, options) {
1749
1903
  success: true,
1750
1904
  path: agentDir,
1751
1905
  canonicalPath: canonicalDir,
1752
- mode: "symlink",
1753
1906
  forgeOtherReplacements: []
1754
1907
  };
1755
1908
  } catch (error) {
1756
1909
  return {
1757
1910
  success: false,
1758
1911
  path: agentDir,
1759
- mode: installMode,
1760
1912
  error: error instanceof Error ? error.message : "Unknown error",
1761
1913
  forgeOtherReplacements: []
1762
1914
  };
@@ -1816,11 +1968,9 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1816
1968
  const agent = agents[agentType];
1817
1969
  const isGlobal = options.global ?? false;
1818
1970
  const cwd = options.cwd || process.cwd();
1819
- const installMode = options.mode ?? "symlink";
1820
1971
  if (isGlobal && agent.globalSkillsDir === void 0) return {
1821
1972
  success: false,
1822
1973
  path: "",
1823
- mode: installMode,
1824
1974
  error: `${agent.displayName} does not support global skill installation`
1825
1975
  };
1826
1976
  const skillName = sanitizeName(skill.installName);
@@ -1831,13 +1981,11 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1831
1981
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1832
1982
  success: false,
1833
1983
  path: agentDir,
1834
- mode: installMode,
1835
1984
  error: "Invalid skill name: potential path traversal detected"
1836
1985
  };
1837
1986
  if (!isPathSafe(agentBase, agentDir)) return {
1838
1987
  success: false,
1839
1988
  path: agentDir,
1840
- mode: installMode,
1841
1989
  error: "Invalid skill name: potential path traversal detected"
1842
1990
  };
1843
1991
  async function writeSkillFiles(targetDir) {
@@ -1850,22 +1998,12 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1850
1998
  }
1851
1999
  }
1852
2000
  try {
1853
- if (installMode === "copy") {
1854
- await cleanAndCreateDirectory(agentDir);
1855
- await writeSkillFiles(agentDir);
1856
- return {
1857
- success: true,
1858
- path: agentDir,
1859
- mode: "copy"
1860
- };
1861
- }
1862
2001
  await cleanAndCreateDirectory(canonicalDir);
1863
2002
  await writeSkillFiles(canonicalDir);
1864
2003
  if (isGlobal && isUniversalAgent(agentType)) return {
1865
2004
  success: true,
1866
2005
  path: canonicalDir,
1867
- canonicalPath: canonicalDir,
1868
- mode: "symlink"
2006
+ canonicalPath: canonicalDir
1869
2007
  };
1870
2008
  if (!await createSymlink(canonicalDir, agentDir)) {
1871
2009
  await cleanAndCreateDirectory(agentDir);
@@ -1874,21 +2012,18 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1874
2012
  success: true,
1875
2013
  path: agentDir,
1876
2014
  canonicalPath: canonicalDir,
1877
- mode: "symlink",
1878
2015
  symlinkFailed: true
1879
2016
  };
1880
2017
  }
1881
2018
  return {
1882
2019
  success: true,
1883
2020
  path: agentDir,
1884
- canonicalPath: canonicalDir,
1885
- mode: "symlink"
2021
+ canonicalPath: canonicalDir
1886
2022
  };
1887
2023
  } catch (error) {
1888
2024
  return {
1889
2025
  success: false,
1890
2026
  path: agentDir,
1891
- mode: installMode,
1892
2027
  error: error instanceof Error ? error.message : "Unknown error"
1893
2028
  };
1894
2029
  }
@@ -1897,11 +2032,9 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1897
2032
  const agent = agents[agentType];
1898
2033
  const isGlobal = options.global ?? false;
1899
2034
  const cwd = options.cwd || process.cwd();
1900
- const installMode = options.mode ?? "symlink";
1901
2035
  if (isGlobal && agent.globalSkillsDir === void 0) return {
1902
2036
  success: false,
1903
2037
  path: "",
1904
- mode: installMode,
1905
2038
  error: `${agent.displayName} does not support global skill installation`
1906
2039
  };
1907
2040
  const skillName = sanitizeName(skill.installName);
@@ -1912,13 +2045,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1912
2045
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1913
2046
  success: false,
1914
2047
  path: agentDir,
1915
- mode: installMode,
1916
2048
  error: "Invalid skill name: potential path traversal detected"
1917
2049
  };
1918
2050
  if (!isPathSafe(agentBase, agentDir)) return {
1919
2051
  success: false,
1920
2052
  path: agentDir,
1921
- mode: installMode,
1922
2053
  error: "Invalid skill name: potential path traversal detected"
1923
2054
  };
1924
2055
  async function writeSkillFiles(targetDir) {
@@ -1931,22 +2062,12 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1931
2062
  }
1932
2063
  }
1933
2064
  try {
1934
- if (installMode === "copy") {
1935
- await cleanAndCreateDirectory(agentDir);
1936
- await writeSkillFiles(agentDir);
1937
- return {
1938
- success: true,
1939
- path: agentDir,
1940
- mode: "copy"
1941
- };
1942
- }
1943
2065
  await cleanAndCreateDirectory(canonicalDir);
1944
2066
  await writeSkillFiles(canonicalDir);
1945
2067
  if (isGlobal && isUniversalAgent(agentType)) return {
1946
2068
  success: true,
1947
2069
  path: canonicalDir,
1948
- canonicalPath: canonicalDir,
1949
- mode: "symlink"
2070
+ canonicalPath: canonicalDir
1950
2071
  };
1951
2072
  if (!await createSymlink(canonicalDir, agentDir)) {
1952
2073
  await cleanAndCreateDirectory(agentDir);
@@ -1955,21 +2076,18 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1955
2076
  success: true,
1956
2077
  path: agentDir,
1957
2078
  canonicalPath: canonicalDir,
1958
- mode: "symlink",
1959
2079
  symlinkFailed: true
1960
2080
  };
1961
2081
  }
1962
2082
  return {
1963
2083
  success: true,
1964
2084
  path: agentDir,
1965
- canonicalPath: canonicalDir,
1966
- mode: "symlink"
2085
+ canonicalPath: canonicalDir
1967
2086
  };
1968
2087
  } catch (error) {
1969
2088
  return {
1970
2089
  success: false,
1971
2090
  path: agentDir,
1972
- mode: installMode,
1973
2091
  error: error instanceof Error ? error.message : "Unknown error"
1974
2092
  };
1975
2093
  }
@@ -3270,23 +3388,18 @@ function splitAgentsByType(agentTypes) {
3270
3388
  symlinked
3271
3389
  };
3272
3390
  }
3273
- function buildAgentSummaryLines(targetAgents, installMode, entry) {
3391
+ function buildAgentSummaryLines(targetAgents, entry) {
3274
3392
  const lines = [];
3275
3393
  const { universal, symlinked } = splitAgentsByType(targetAgents);
3276
- if (installMode === "symlink") {
3277
- if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList(universal)}`);
3278
- if (symlinked.length > 0) if (entry) {
3279
- const sanitized = sanitizeName(entry.skillName);
3280
- for (const a of targetAgents) {
3281
- if (isUniversalAgent(a)) continue;
3282
- const entryPath = join(getAgentBaseDir(a, entry.scope === "global", entry.cwd), sanitized);
3283
- lines.push(` ${import_picocolors.default.dim("symlink →")} ${import_picocolors.default.cyan(shortenPath$2(entryPath, entry.cwd))} ${import_picocolors.default.dim(`(${agents[a].displayName})`)}`);
3284
- }
3285
- } else lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList(symlinked)}`);
3286
- } else {
3287
- const allNames = targetAgents.map((a) => agents[a].displayName);
3288
- lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList(allNames)}`);
3289
- }
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)}`);
3290
3403
  return lines;
3291
3404
  }
3292
3405
  function ensureUniversalAgents(targetAgents) {
@@ -3347,7 +3460,7 @@ async function selectAgentsInteractive(options) {
3347
3460
  const supportsGlobalFilter = (a) => !options.global || agents[a].globalSkillsDir;
3348
3461
  return await searchMultiselect({
3349
3462
  message: "Which agents do you want to install to?",
3350
- items: buildAgentChoices(Object.keys(agents).filter(supportsGlobalFilter), options),
3463
+ items: buildAgentChoices(getPickerVisibleAgents().filter(supportsGlobalFilter), options),
3351
3464
  initialSelected: [],
3352
3465
  required: true
3353
3466
  });
@@ -3436,7 +3549,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3436
3549
  M.info("Installing to all agents");
3437
3550
  } else {
3438
3551
  M.info("Select agents to install skills to");
3439
- 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 }));
3440
3553
  if (pD(selected)) {
3441
3554
  xe("Installation cancelled");
3442
3555
  process.exit(0);
@@ -3479,27 +3592,6 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3479
3592
  }
3480
3593
  installGlobally = scope;
3481
3594
  }
3482
- let installMode = options.copy ? "copy" : "symlink";
3483
- const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
3484
- if (!options.copy && !options.yes && uniqueDirs.size > 1) {
3485
- const modeChoice = await ve({
3486
- message: "Installation method",
3487
- options: [{
3488
- value: "symlink",
3489
- label: "Symlink (Recommended)",
3490
- hint: "Single source of truth, easy updates"
3491
- }, {
3492
- value: "copy",
3493
- label: "Copy to all agents",
3494
- hint: "Independent copies for each agent"
3495
- }]
3496
- });
3497
- if (pD(modeChoice)) {
3498
- xe("Installation cancelled");
3499
- process.exit(0);
3500
- }
3501
- installMode = modeChoice;
3502
- } else if (uniqueDirs.size <= 1) installMode = "copy";
3503
3595
  const cwd = process.cwd();
3504
3596
  const summaryLines = [];
3505
3597
  targetAgents.map((a) => agents[a].displayName);
@@ -3517,7 +3609,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3517
3609
  if (summaryLines.length > 0) summaryLines.push("");
3518
3610
  const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
3519
3611
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
3520
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode, {
3612
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, {
3521
3613
  skillName: skill.installName,
3522
3614
  scope: installGlobally ? "global" : "project",
3523
3615
  cwd
@@ -3541,10 +3633,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3541
3633
  spinner.start("Installing skills...");
3542
3634
  const results = [];
3543
3635
  for (const skill of selectedSkills) for (const agent of targetAgents) {
3544
- const result = await installWellKnownSkillForAgent(skill, agent, {
3545
- global: installGlobally,
3546
- mode: installMode
3547
- });
3636
+ const result = await installWellKnownSkillForAgent(skill, agent, { global: installGlobally });
3548
3637
  results.push({
3549
3638
  skill: skill.installName,
3550
3639
  agent: agents[agent].displayName,
@@ -3596,24 +3685,16 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3596
3685
  bySkill.set(r.skill, skillResults);
3597
3686
  }
3598
3687
  const skillCount = bySkill.size;
3599
- const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
3688
+ const symlinkFailures = successful.filter((r) => r.symlinkFailed);
3600
3689
  const copiedAgents = symlinkFailures.map((r) => r.agent);
3601
3690
  const resultLines = [];
3602
3691
  for (const [skillName, skillResults] of bySkill) {
3603
3692
  const firstResult = skillResults[0];
3604
- if (firstResult.mode === "copy") {
3605
- resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3606
- for (const r of skillResults) {
3607
- const shortPath = shortenPath$2(r.path, cwd);
3608
- resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
3609
- }
3610
- } else {
3611
- if (firstResult.canonicalPath) {
3612
- const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3613
- resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3614
- } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
3615
- resultLines.push(...buildResultLines(skillResults, targetAgents));
3616
- }
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));
3617
3698
  }
3618
3699
  const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3619
3700
  Me(resultLines.join("\n"), title);
@@ -3848,7 +3929,7 @@ async function runAdd(args, options = {}) {
3848
3929
  spinner.stop(`${Object.keys(agents).length} agents`);
3849
3930
  if (options.yes) targetAgents = installedAgents.length > 0 ? ensureUniversalAgents(installedAgents) : validAgents;
3850
3931
  else if (installedAgents.length === 0) {
3851
- const selected = await promptForAgents("Which IDEs do you want to install this plugin to?", buildAgentChoices(Object.keys(agents), {
3932
+ const selected = await promptForAgents("Which IDEs do you want to install this plugin to?", buildAgentChoices(getPickerVisibleAgents(), {
3852
3933
  global: options.global,
3853
3934
  forPlugin: true
3854
3935
  }));
@@ -4257,7 +4338,7 @@ async function runAdd(args, options = {}) {
4257
4338
  M.info("Installing to all agents");
4258
4339
  } else {
4259
4340
  M.info("Select agents to install skills to");
4260
- 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 }));
4261
4342
  if (pD(selected)) {
4262
4343
  xe("Installation cancelled");
4263
4344
  await cleanup(tempDir);
@@ -4303,28 +4384,6 @@ async function runAdd(args, options = {}) {
4303
4384
  }
4304
4385
  installGlobally = scope;
4305
4386
  }
4306
- let installMode = options.copy ? "copy" : "symlink";
4307
- const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
4308
- if (!options.copy && !options.yes && uniqueDirs.size > 1) {
4309
- const modeChoice = await ve({
4310
- message: "Installation method",
4311
- options: [{
4312
- value: "symlink",
4313
- label: "Symlink (Recommended)",
4314
- hint: "Single source of truth, easy updates"
4315
- }, {
4316
- value: "copy",
4317
- label: "Copy to all agents",
4318
- hint: "Independent copies for each agent"
4319
- }]
4320
- });
4321
- if (pD(modeChoice)) {
4322
- xe("Installation cancelled");
4323
- await cleanup(tempDir);
4324
- process.exit(0);
4325
- }
4326
- installMode = modeChoice;
4327
- }
4328
4387
  const cwd = process.cwd();
4329
4388
  const _summaryPortalRef = parsed.type === "portal" || parsed.type === "public" || parsed.type === "team" ? parsed.portalRef ?? null : null;
4330
4389
  const _summaryIsTeam = _summaryPortalRef?.startsWith("@teams/") ?? false;
@@ -4356,7 +4415,7 @@ async function runAdd(args, options = {}) {
4356
4415
  if (summaryLines.length > 0) summaryLines.push("");
4357
4416
  const shortCanonical = shortenPath$2(join(_summaryStoreRoot, "skills", `${_summaryScopeNs}__${sanitizeName(skill.name)}`), cwd);
4358
4417
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
4359
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode, {
4418
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, {
4360
4419
  skillName: skill.name,
4361
4420
  scope: _summaryScope,
4362
4421
  cwd
@@ -4414,17 +4473,13 @@ async function runAdd(args, options = {}) {
4414
4473
  result = await installBlobSkillForAgent({
4415
4474
  installName: blobSkill.name,
4416
4475
  files: blobSkill.files
4417
- }, agent, {
4418
- global: installGlobally,
4419
- mode: installMode
4420
- });
4476
+ }, agent, { global: installGlobally });
4421
4477
  } else {
4422
4478
  const skillRef = _installPortalRef ? `${_installPortalRef}/${skill.name ?? ""}` : skill.name ?? "unknown";
4423
4479
  result = await installSkillForAgent(skill, agent, {
4424
4480
  scope: installGlobally ? "global" : "project",
4425
4481
  ref: skillRef,
4426
4482
  scopeNamespace: _installScopeNs,
4427
- mode: installMode,
4428
4483
  forceOverwrite: options.force || options.yes,
4429
4484
  onConflictPrompt: (conflicts) => promptForeignOverwrite(conflicts, {
4430
4485
  yes: options.yes,
@@ -4537,7 +4592,7 @@ async function runAdd(args, options = {}) {
4537
4592
  const hash = await computeSkillFolderHash(join(repoRoot, dirname(rawSkillPath)));
4538
4593
  if (hash) skillFolderHash = hash;
4539
4594
  }
4540
- 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;
4541
4596
  await addSkillToLock(skill.name, {
4542
4597
  source: lockSource ?? normalizedSource ?? "",
4543
4598
  sourceType: parsed.type,
@@ -4557,7 +4612,7 @@ async function runAdd(args, options = {}) {
4557
4612
  if (successfulSkillNames.has(skillDisplayName)) try {
4558
4613
  const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
4559
4614
  const rawSkillPath = skillFiles[skill.name];
4560
- 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;
4561
4616
  await addSkillToLocalLock(skill.name, {
4562
4617
  source: lockSource || parsed.url,
4563
4618
  ref: parsed.ref,
@@ -4583,26 +4638,18 @@ async function runAdd(args, options = {}) {
4583
4638
  } else ungroupedResults.push(r);
4584
4639
  }
4585
4640
  const skillCount = bySkill.size;
4586
- const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
4641
+ const symlinkFailures = successful.filter((r) => r.symlinkFailed);
4587
4642
  const copiedAgents = symlinkFailures.map((r) => r.agent);
4588
4643
  const resultLines = [];
4589
4644
  const printSkillResults = (entries) => {
4590
4645
  for (const entry of entries) {
4591
4646
  const skillResults = bySkill.get(entry.skill) || [];
4592
4647
  const firstResult = skillResults[0];
4593
- if (firstResult.mode === "copy") {
4594
- resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill} ${import_picocolors.default.dim("(copied)")}`);
4595
- for (const r of skillResults) {
4596
- const shortPath = shortenPath$2(r.path, cwd);
4597
- resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
4598
- }
4599
- } else {
4600
- if (firstResult.canonicalPath) {
4601
- const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
4602
- resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
4603
- } else resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill}`);
4604
- resultLines.push(...buildResultLines(skillResults, targetAgents));
4605
- }
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));
4606
4653
  }
4607
4654
  };
4608
4655
  const sortedResultGroups = Object.keys(groupedResults).sort();
@@ -4744,8 +4791,7 @@ function parseAddOptions(args) {
4744
4791
  else if (arg === "--team") {
4745
4792
  i++;
4746
4793
  options.team = args[i];
4747
- } else if (arg === "--copy") options.copy = true;
4748
- else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
4794
+ } else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
4749
4795
  else if (arg && !arg.startsWith("-")) source.push(arg);
4750
4796
  }
4751
4797
  return {
@@ -5102,7 +5148,7 @@ async function runSync(args, options = {}) {
5102
5148
  } else {
5103
5149
  const selected = await searchMultiselect({
5104
5150
  message: "Which agents do you want to install to?",
5105
- items: Object.keys(agents).map((a) => {
5151
+ items: getPickerVisibleAgents().map((a) => {
5106
5152
  const cfg = agents[a];
5107
5153
  const baseHint = cfg.skillsDir;
5108
5154
  const hint = isUniversalAgent(a) ? `${baseHint}, shared` : baseHint;
@@ -5125,7 +5171,7 @@ async function runSync(args, options = {}) {
5125
5171
  targetAgents = [...installedAgents];
5126
5172
  for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
5127
5173
  } else {
5128
- const allAgents = Object.keys(agents).filter((a) => installedAgents.includes(a));
5174
+ const allAgents = getPickerVisibleAgents().filter((a) => installedAgents.includes(a));
5129
5175
  const selected = await searchMultiselect({
5130
5176
  message: "Which agents do you want to install to?",
5131
5177
  items: allAgents.map((a) => {
@@ -5168,8 +5214,7 @@ async function runSync(args, options = {}) {
5168
5214
  for (const skill of toInstall) for (const agent of targetAgents) {
5169
5215
  const result = await installSkillForAgent(skill, agent, {
5170
5216
  global: false,
5171
- cwd,
5172
- mode: "symlink"
5217
+ cwd
5173
5218
  });
5174
5219
  results.push({
5175
5220
  skill: skill.name,
@@ -6303,7 +6348,6 @@ ${BOLD}Add Options:${RESET}
6303
6348
  -y, --yes Skip confirmation prompts
6304
6349
  --team <scope> Install every skill owned by the team
6305
6350
  (no positional source; mutually exclusive with <source>)
6306
- --copy Copy files instead of symlinking to agent directories
6307
6351
  --all Shorthand for --skill '*' --agent '*' -y
6308
6352
  --full-depth Search all subdirectories even when a root SKILL.md exists
6309
6353
 
@@ -6464,12 +6508,14 @@ async function runUpdate(args = []) {
6464
6508
  name: pl.name,
6465
6509
  ref: pl.ref,
6466
6510
  canonical: pl.canonical,
6511
+ agents: pl.agents,
6467
6512
  hasProjectEntry: hasProjectEntryFor(pl.canonical, cwd)
6468
6513
  })), ...inventory.skills.filter((s) => Boolean(s.ref)).map((s) => ({
6469
6514
  kind: "skill",
6470
6515
  name: s.name,
6471
6516
  ref: s.ref,
6472
6517
  canonical: s.canonical,
6518
+ agents: s.agents,
6473
6519
  hasProjectEntry: hasProjectEntryFor(s.canonical, cwd)
6474
6520
  }))];
6475
6521
  if (options.names && options.names.length > 0) {
@@ -6511,12 +6557,23 @@ async function runUpdate(args = []) {
6511
6557
  for (const t of targets) {
6512
6558
  const safeName = sanitizeMetadata(t.name);
6513
6559
  if (!options.json) console.log(`${TEXT}Updating ${safeName}...${RESET} ${DIM}(${t.ref})${RESET}`);
6560
+ if (t.agents.length === 0) {
6561
+ fail++;
6562
+ if (!options.json) console.log(` ${DIM}✗ skipped ${safeName} — no IDE entries point at this canonical${RESET}`);
6563
+ results.push({
6564
+ name: t.name,
6565
+ kind: t.kind,
6566
+ ok: false
6567
+ });
6568
+ continue;
6569
+ }
6514
6570
  const passthrough = [
6515
6571
  "add",
6516
6572
  t.ref,
6517
6573
  "-y"
6518
6574
  ];
6519
6575
  if (!t.hasProjectEntry) passthrough.push("-g");
6576
+ passthrough.push("-a", ...t.agents);
6520
6577
  const ok = spawnSync(process.execPath, [cliEntry, ...passthrough], {
6521
6578
  stdio: options.json ? [
6522
6579
  "ignore",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shoplazza-ai-dev-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Shoplazza AI Dev CLI: install, publish and manage AI agent skills/rules",
5
5
  "type": "module",
6
6
  "bin": {