shoplazza-ai-dev-cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,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;
@@ -318,7 +318,7 @@ const agents = {
318
318
  cursor: {
319
319
  name: "cursor",
320
320
  displayName: "Cursor",
321
- skillsDir: ".agents/skills",
321
+ skillsDir: ".cursor/skills",
322
322
  globalSkillsDir: join(home, ".cursor/skills"),
323
323
  detectInstalled: async () => {
324
324
  return existsSync(join(home, ".cursor"));
@@ -340,8 +340,8 @@ function isUniversalAgent(type) {
340
340
  function getStoreRoot(_scope, _cwd) {
341
341
  return join(home, FORGE_STORE_DIRNAME, "store");
342
342
  }
343
- function getAgentEntryRoot(agent, scope, cwd) {
344
- if (scope === "global") return getAgentHome(agent);
343
+ function getAgentEntryRoot(_agent, scope, cwd) {
344
+ if (scope === "global") return home;
345
345
  return cwd ?? process.cwd();
346
346
  }
347
347
  function getInstallTargets(agent, component, scope, cwd) {
@@ -628,35 +628,93 @@ function getForgeHookScript(portalUrl) {
628
628
  return `#!/usr/bin/env node
629
629
  // ${FORGE_HOOK_TAG}
630
630
  // 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).
631
+ // Reports {type:'skill_invoked', asset_ref, install_mode} when Claude Code
632
+ // loads a forge-installed skill via slash command (UserPromptExpansion)
633
+ // or via the Skill tool (PreToolUse). PostToolUse(Skill) is unreliable on
634
+ // Claude Code 2.1.x; see anthropics/claude-code#43630.
633
635
  // Anonymous: /events accepts unauthenticated posts.
634
636
  import fs from 'node:fs';
637
+ import os from 'node:os';
635
638
  import path from 'node:path';
636
639
 
637
640
  const PORTAL = ${JSON.stringify(portal)};
638
641
 
639
- setTimeout(() => process.exit(0), 5000);
642
+ // Safety net: never let a stuck fetch block the IDE for more than 5 seconds.
643
+ // The fetch itself uses AbortSignal.timeout(2000); this catches edge cases
644
+ // like a stalled DNS lookup before the abort fires.
645
+ const exitTimer = setTimeout(() => process.exit(0), 5000);
646
+ exitTimer.unref?.();
647
+
648
+ function readManifest(dir) {
649
+ try {
650
+ const raw = fs.readFileSync(path.join(dir, '.forge-source.json'), 'utf8');
651
+ return JSON.parse(raw);
652
+ } catch {
653
+ return null;
654
+ }
655
+ }
656
+
657
+ function manifestFromSkillName(skillId, cwd) {
658
+ if (!skillId || typeof skillId !== 'string') return null;
659
+ // \`plugin:skill-name\` → drop the plugin prefix; forge installs flatten into
660
+ // .claude/skills/<name>/ regardless of plugin namespace (see agents.ts).
661
+ const name = skillId.includes(':') ? skillId.split(':').pop() : skillId;
662
+ if (!name) return null;
663
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || path.join(os.homedir(), '.claude');
664
+ // 1) IDE entry under cwd (project-scope install).
665
+ // 2) IDE entry under CLAUDE_CONFIG_DIR (global-scope install).
666
+ const ideCandidates = [];
667
+ if (cwd) ideCandidates.push(path.join(cwd, '.claude', 'skills', name));
668
+ ideCandidates.push(path.join(claudeHome, 'skills', name));
669
+ for (const dir of ideCandidates) {
670
+ const m = readManifest(dir);
671
+ if (m) return m;
672
+ }
673
+ // 3) Forge canonical store. Truth source — regardless of where the IDE
674
+ // entry symlink lives or what the agent's cwd is, the manifest always
675
+ // exists under ~/.ai-dev-cli/store. Skill dirs are named \`<scope>__<name>\`;
676
+ // plugin-internal skills live one level deeper.
677
+ const storeRoot = path.join(os.homedir(), '.ai-dev-cli', 'store');
678
+ for (const sub of ['skills', 'plugins']) {
679
+ try {
680
+ const root = path.join(storeRoot, sub);
681
+ for (const entry of fs.readdirSync(root)) {
682
+ if (sub === 'skills') {
683
+ if (!entry.endsWith('__' + name)) continue;
684
+ const m = readManifest(path.join(root, entry));
685
+ if (m && m.name === name) return m;
686
+ } else {
687
+ const m = readManifest(path.join(root, entry, 'skills', name));
688
+ if (m && m.name === name) return m;
689
+ }
690
+ }
691
+ } catch {
692
+ /* store dir missing — fall through */
693
+ }
694
+ }
695
+ return null;
696
+ }
640
697
 
641
698
  let buf = '';
642
699
  process.stdin.on('data', (c) => (buf += c));
643
700
  process.stdin.on('end', () => {
701
+ let skillId = null;
644
702
  try {
645
703
  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);
704
+ if (ev.hook_event_name === 'UserPromptExpansion') {
705
+ // Slash command path. \`command_name\` is the skill identifier as the
706
+ // user typed it (without the leading slash), e.g. "ai-coding-llm-wiki".
707
+ if (ev.expansion_type !== 'slash_command') process.exit(0);
708
+ skillId = ev.command_name;
709
+ } else if (ev.hook_event_name === 'PreToolUse' && ev.tool_name === 'Skill') {
710
+ skillId = ev.tool_input?.skill ?? ev.tool_input?.name;
711
+ } else {
712
+ process.exit(0);
713
+ }
714
+ const manifest = manifestFromSkillName(skillId, ev.cwd || process.cwd());
715
+ if (!manifest?.ref) {
716
+ process.exit(0);
658
717
  }
659
- if (!manifest?.ref) return;
660
718
 
661
719
  fetch(PORTAL + '/events', {
662
720
  method: 'POST',
@@ -670,9 +728,11 @@ process.stdin.on('end', () => {
670
728
  },
671
729
  }),
672
730
  signal: AbortSignal.timeout(2000),
673
- }).catch(() => {});
731
+ })
732
+ .catch(() => {})
733
+ .finally(() => process.exit(0));
674
734
  } catch {
675
- /* swallow */
735
+ process.exit(0);
676
736
  }
677
737
  });
678
738
  `;
@@ -710,6 +770,27 @@ async function writeHookScript(scriptPath, portalUrl) {
710
770
  await chmod(scriptPath, 493);
711
771
  } catch {}
712
772
  }
773
+ const FORGE_LEGACY_LOCATIONS = [
774
+ {
775
+ event: "PostToolUse",
776
+ matcher: "Read"
777
+ },
778
+ {
779
+ event: "PostToolUse",
780
+ matcher: "Read|Skill"
781
+ },
782
+ {
783
+ event: "PostToolUse",
784
+ matcher: "Skill"
785
+ }
786
+ ];
787
+ const FORGE_CANONICAL_LOCATIONS = [{
788
+ event: "UserPromptExpansion",
789
+ matcher: void 0
790
+ }, {
791
+ event: "PreToolUse",
792
+ matcher: "Skill"
793
+ }];
713
794
  async function upsertClaudeCodeSettings(settingsPath, scriptPath) {
714
795
  const command = `node ${shellEscape(scriptPath)}`;
715
796
  let raw = "";
@@ -725,26 +806,38 @@ async function upsertClaudeCodeSettings(settingsPath, scriptPath) {
725
806
  throw new Error(`Refusing to overwrite invalid JSON at ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
726
807
  }
727
808
  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);
809
+ const hooks = settings.hooks;
810
+ const isOurEntry = (h) => !!(h && typeof h.command === "string" && h.command.includes(HOOK_SCRIPT_BASENAME));
811
+ const owned = new Set([...FORGE_LEGACY_LOCATIONS, ...FORGE_CANONICAL_LOCATIONS].map((l) => `${l.event}::${l.matcher ?? ""}`));
812
+ for (const eventName of Object.keys(hooks)) {
813
+ const matcherList = hooks[eventName];
814
+ if (!Array.isArray(matcherList)) continue;
815
+ for (let i = matcherList.length - 1; i >= 0; i--) {
816
+ const m = matcherList[i];
817
+ if (!m || !Array.isArray(m.hooks)) continue;
818
+ m.hooks = m.hooks.filter((h) => !isOurEntry(h));
819
+ const key = `${eventName}::${m.matcher ?? ""}`;
820
+ if (m.hooks.length === 0 && owned.has(key)) matcherList.splice(i, 1);
821
+ }
822
+ if (matcherList.length === 0) delete hooks[eventName];
823
+ }
824
+ for (const loc of FORGE_CANONICAL_LOCATIONS) {
825
+ if (!Array.isArray(hooks[loc.event])) hooks[loc.event] = [];
826
+ const matcherList = hooks[loc.event];
827
+ let target = matcherList.find((m) => m && (m.matcher ?? void 0) === loc.matcher);
828
+ if (!target) {
829
+ target = loc.matcher === void 0 ? { hooks: [] } : {
830
+ matcher: loc.matcher,
831
+ hooks: []
832
+ };
833
+ matcherList.push(target);
834
+ }
835
+ if (!Array.isArray(target.hooks)) target.hooks = [];
836
+ target.hooks.push({
837
+ type: "command",
838
+ command
839
+ });
737
840
  }
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
- });
748
841
  await mkdir(dirname(settingsPath), { recursive: true });
749
842
  await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
750
843
  }
@@ -752,18 +845,273 @@ function shellEscape(p) {
752
845
  if (!/[\s"'$`\\]/.test(p)) return p;
753
846
  return `'${p.replace(/'/g, `'\\''`)}'`;
754
847
  }
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;
848
+ function getRandomPort() {
849
+ return new Promise((resolve, reject) => {
850
+ const server = createServer();
851
+ server.listen(0, "127.0.0.1", () => {
852
+ const address = server.address();
853
+ if (address && typeof address !== "string" && "port" in address) {
854
+ const port = address.port;
855
+ server.close(() => resolve(port));
856
+ } else server.close(() => reject(/* @__PURE__ */ new Error("Failed to get port")));
857
+ });
858
+ server.on("error", reject);
859
+ });
860
+ }
861
+ const PORTAL_THEME_CSS = `
862
+ :root {
863
+ --bg: #FDFCF8;
864
+ --ink: #201F1D;
865
+ --text: #37352F;
866
+ --muted: #827E7A;
867
+ --line: #EDECE8;
868
+ --success: #0F6E37;
869
+ --success-soft: #EDF7F1;
870
+ --danger: #E5484D;
871
+ --danger-soft: #FDF1EF;
872
+ --serif: "Newsreader", "Source Serif Pro", Georgia, serif;
873
+ --sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", sans-serif;
874
+ --shadow-sm: 0 1px 2px rgba(32, 31, 29, 0.06);
875
+ }
876
+ @media (prefers-color-scheme: dark) {
877
+ :root {
878
+ --bg: #27251F;
879
+ --ink: #EDEAE5;
880
+ --text: #D4CFC8;
881
+ --muted: #918D89;
882
+ --line: #35332F;
883
+ --success: #3FC67A;
884
+ --success-soft: #062A14;
885
+ --danger: #E5484D;
886
+ --danger-soft: #2B1212;
887
+ }
888
+ }
889
+ * { box-sizing: border-box; }
890
+ body {
891
+ margin: 0;
892
+ min-height: 100vh;
893
+ background: var(--bg);
894
+ color: var(--text);
895
+ font-family: var(--sans);
896
+ font-size: 15px;
897
+ line-height: 1.65;
898
+ display: flex;
899
+ align-items: center;
900
+ justify-content: center;
901
+ }
902
+ .card {
903
+ max-width: 480px;
904
+ margin: 0 24px;
905
+ padding: 40px 32px;
906
+ text-align: center;
907
+ }
908
+ .icon {
909
+ width: 56px;
910
+ height: 56px;
911
+ margin: 0 auto 24px;
912
+ border-radius: 50%;
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ }
917
+ .icon.success { background: var(--success-soft); }
918
+ .icon.error { background: var(--danger-soft); }
919
+ .icon svg { display: block; }
920
+ h1 {
921
+ font-family: var(--serif);
922
+ font-size: 28px;
923
+ font-weight: 500;
924
+ letter-spacing: -0.01em;
925
+ color: var(--ink);
926
+ margin: 0 0 12px;
927
+ }
928
+ p { color: var(--muted); margin: 0 0 8px; }
929
+ p.detail { color: var(--text); }
930
+ `;
931
+ function renderCallbackPage(opts) {
932
+ return `<!DOCTYPE html>
933
+ <html>
934
+ <head>
935
+ <meta charset="utf-8">
936
+ <title>${opts.title}</title>
937
+ <style>${PORTAL_THEME_CSS}</style>
938
+ </head>
939
+ <body>
940
+ <div class="card">
941
+ <div class="icon ${opts.variant}">${opts.iconSvg}</div>
942
+ <h1>${opts.heading}</h1>
943
+ ${opts.body}
944
+ </div>
945
+ </body>
946
+ </html>`;
947
+ }
948
+ function getSuccessHtml() {
949
+ return renderCallbackPage({
950
+ title: "Login successful",
951
+ heading: "Login successful",
952
+ body: "<p>You can close this window and return to the CLI.</p>",
953
+ variant: "success",
954
+ iconSvg: `
955
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor"
956
+ stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"
957
+ style="color: var(--success);">
958
+ <polyline points="4 12 10 18 20 6"></polyline>
959
+ </svg>`
960
+ });
961
+ }
962
+ function escapeHtml(input) {
963
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
964
+ }
965
+ function getErrorHtml(error) {
966
+ return renderCallbackPage({
967
+ title: "Login failed",
968
+ heading: "Login failed",
969
+ body: `<p class="detail">${escapeHtml(error)}</p><p>Please try again.</p>`,
970
+ variant: "error",
971
+ iconSvg: `
972
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor"
973
+ stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"
974
+ style="color: var(--danger);">
975
+ <line x1="6" y1="6" x2="18" y2="18"></line>
976
+ <line x1="18" y1="6" x2="6" y2="18"></line>
977
+ </svg>`
978
+ });
979
+ }
980
+ let callbackResolve = null;
981
+ async function runLogin(options = {}) {
982
+ const cfg = loadConfig();
983
+ if (options.logout) {
984
+ if (cfg.token) {
985
+ saveConfig({
986
+ token: void 0,
987
+ token_name: void 0
988
+ });
989
+ M.success("Logged out successfully.");
990
+ M.message(import_picocolors.default.dim("Your token has been removed from the config."));
991
+ } else M.message("You are not logged in.");
992
+ return;
993
+ }
994
+ if (cfg.token && !options.force) {
995
+ M.message("You are already logged in.");
996
+ M.info(import_picocolors.default.dim(`Token: ${cfg.token_name || "default"}`));
997
+ const shouldReauth = await ye({
998
+ message: "Re-authenticate?",
999
+ initialValue: false
1000
+ });
1001
+ if (isCancelled$1(shouldReauth)) {
1002
+ Se(import_picocolors.default.dim("Cancelled"));
1003
+ return;
1004
+ }
1005
+ if (!shouldReauth) return;
1006
+ }
1007
+ console.log();
1008
+ Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" login ")));
1009
+ const portalUrl = cfg.portal;
1010
+ const port = await getRandomPort();
1011
+ const redirectUrl = `http://127.0.0.1:${port}/callback`;
1012
+ const machine = hostname().slice(0, 64);
1013
+ const authUrl = `${portalUrl}/cli-auth?redirect=${encodeURIComponent(redirectUrl)}&hostname=${encodeURIComponent(machine)}`;
1014
+ M.message(`Opening browser for authentication...`);
1015
+ M.info(import_picocolors.default.dim(authUrl));
1016
+ await new Promise((resolve, reject) => {
1017
+ exec(`open "${authUrl}"`, (err) => {
1018
+ if (err) reject(err);
1019
+ else resolve();
1020
+ });
1021
+ });
1022
+ const result = await new Promise((resolve) => {
1023
+ callbackResolve = resolve;
1024
+ const server = createServer((req, res) => {
1025
+ const url = new URL$1(req.url || "", `http://127.0.0.1:${port}`);
1026
+ if (url.pathname === "/callback") {
1027
+ const token = url.searchParams.get("token");
1028
+ const tokenName = url.searchParams.get("token_name");
1029
+ const error = url.searchParams.get("error");
1030
+ if (error) {
1031
+ res.writeHead(400, { "Content-Type": "text/html" });
1032
+ res.end(getErrorHtml(error));
1033
+ resolve({
1034
+ success: false,
1035
+ error
1036
+ });
1037
+ } else if (token) {
1038
+ res.writeHead(200, { "Content-Type": "text/html" });
1039
+ res.end(getSuccessHtml());
1040
+ resolve({
1041
+ success: true,
1042
+ token,
1043
+ tokenName: tokenName || "default"
1044
+ });
1045
+ } else {
1046
+ res.writeHead(400, { "Content-Type": "text/html" });
1047
+ res.end(getErrorHtml("No token received"));
1048
+ resolve({
1049
+ success: false,
1050
+ error: "No token received"
1051
+ });
1052
+ }
1053
+ } else {
1054
+ res.writeHead(404);
1055
+ res.end("Not found");
1056
+ }
1057
+ });
1058
+ server.listen(port, "127.0.0.1", () => {});
1059
+ setTimeout(() => {
1060
+ server.close();
1061
+ if (callbackResolve) callbackResolve({
1062
+ success: false,
1063
+ error: "Login timed out"
1064
+ });
1065
+ }, 12e4);
1066
+ });
1067
+ if (!result.success) {
1068
+ Se(import_picocolors.default.red("Login failed: " + result.error));
1069
+ process.exit(1);
1070
+ }
1071
+ saveConfig({
1072
+ token: result.token,
1073
+ token_name: result.tokenName
1074
+ });
1075
+ M.success("Login successful!");
1076
+ M.message(import_picocolors.default.dim(`Token saved as: ${result.tokenName}`));
1077
+ Se(import_picocolors.default.green("You can now publish skills."));
1078
+ }
1079
+ function isCancelled$1(value) {
1080
+ return typeof value === "symbol";
1081
+ }
1082
+ function parseLoginOptions(args) {
1083
+ const options = {};
1084
+ for (let i = 0; i < args.length; i++) {
1085
+ const arg = args[i];
1086
+ if (arg === "--logout" || arg === "-l") options.logout = true;
1087
+ }
1088
+ return options;
1089
+ }
1090
+ async function rawFetch(url, headers, portalUrl) {
760
1091
  try {
761
- resp = await fetch(url, { headers });
1092
+ return await fetch(url, { headers });
762
1093
  } catch (err) {
763
1094
  const msg = err instanceof Error ? err.message : String(err);
764
1095
  throw new Error(`Failed to reach portal at ${portalUrl}: ${msg}`);
765
1096
  }
766
- if (resp.status === 401) throw new Error("Login required: run `ai-dev-cli login` first");
1097
+ }
1098
+ async function fetchWithAutoAuth(url, accept, bearerToken, portalUrl) {
1099
+ const buildHeaders = (token) => {
1100
+ const h = { Accept: accept };
1101
+ if (token) h.Authorization = `Bearer ${token}`;
1102
+ return h;
1103
+ };
1104
+ let resp = await rawFetch(url, buildHeaders(bearerToken), portalUrl);
1105
+ if (resp.status !== 401) return resp;
1106
+ await resp.arrayBuffer().catch(() => void 0);
1107
+ await runLogin({ force: true });
1108
+ const refreshed = loadConfig().token;
1109
+ if (!refreshed) return resp;
1110
+ return rawFetch(url, buildHeaders(refreshed), portalUrl);
1111
+ }
1112
+ async function fetchSkillArchive(ref, portalUrl, bearerToken) {
1113
+ const resp = await fetchWithAutoAuth(`${portalUrl.replace(/\/$/, "")}/api/cli/skills/archive?ref=${encodeURIComponent(ref)}`, "application/gzip", bearerToken, portalUrl);
1114
+ if (resp.status === 401) throw new Error("Authentication failed after login. Please try again.");
767
1115
  if (resp.status === 403) throw new Error(`Permission denied for ${ref}`);
768
1116
  if (resp.status === 404) throw new Error(`Skill not found: ${ref}`);
769
1117
  if (!resp.ok) {
@@ -773,19 +1121,8 @@ async function fetchSkillArchive(ref, portalUrl, bearerToken) {
773
1121
  return Buffer.from(await resp.arrayBuffer());
774
1122
  }
775
1123
  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");
1124
+ const resp = await fetchWithAutoAuth(`${portalUrl.replace(/\/$/, "")}/api/cli/teams/${encodeURIComponent(teamScope)}/assets`, "application/json", bearerToken, portalUrl);
1125
+ if (resp.status === 401) throw new Error("Authentication failed after login. Please try again.");
789
1126
  if (resp.status === 403) throw new Error(`Not a member of team "${teamScope}"`);
790
1127
  if (resp.status === 404) throw new Error(`Team not found: ${teamScope}`);
791
1128
  if (!resp.ok) {
@@ -795,17 +1132,8 @@ async function fetchTeamAssets(teamScope, portalUrl, bearerToken) {
795
1132
  return (await resp.json()).items ?? [];
796
1133
  }
797
1134
  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");
1135
+ const resp = await fetchWithAutoAuth(`${portalUrl.replace(/\/$/, "")}/api/cli/plugins/archive?ref=${encodeURIComponent(ref)}`, "application/gzip", bearerToken, portalUrl);
1136
+ if (resp.status === 401) throw new Error("Authentication failed after login. Please try again.");
809
1137
  if (resp.status === 403) throw new Error(`Permission denied for ${ref}`);
810
1138
  if (resp.status === 404) throw new Error(`Plugin not found: ${ref}`);
811
1139
  if (!resp.ok) {
@@ -947,7 +1275,7 @@ async function promptForeignOverwrite(conflicts, opts = {}) {
947
1275
  }
948
1276
  return choice === "yes";
949
1277
  }
950
- const AGENTS_DIR$2 = ".agents";
1278
+ const AGENTS_DIR$1 = ".agents";
951
1279
  const SKILLS_SUBDIR = "skills";
952
1280
  function parseFrontmatter(raw) {
953
1281
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
@@ -1187,17 +1515,8 @@ function isPathSafe(basePath, targetPath) {
1187
1515
  const normalizedTarget = normalize(resolve(targetPath));
1188
1516
  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
1189
1517
  }
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
1518
  function getCanonicalSkillsDir(global, cwd) {
1200
- return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$2, SKILLS_SUBDIR);
1519
+ return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
1201
1520
  }
1202
1521
  function getAgentBaseDir(agentType, global, cwd) {
1203
1522
  if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
@@ -1283,8 +1602,7 @@ async function installSkillForAgent(skill, agentType, opts) {
1283
1602
  error: "Invalid skill name: path traversal detected",
1284
1603
  forgeOtherReplacements: []
1285
1604
  };
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);
1605
+ const entry = join(getAgentBaseDir(agentType, scope === "global", cwd), skillName);
1288
1606
  try {
1289
1607
  const status = await assertEntryReplaceable(entry, {
1290
1608
  intendedRef: newOpts.ref,
@@ -1487,15 +1805,6 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
1487
1805
  return false;
1488
1806
  }
1489
1807
  }
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
1808
  function getCanonicalPath(skillName, options = {}) {
1500
1809
  const sanitized = sanitizeName(skillName);
1501
1810
  const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
@@ -1665,166 +1974,6 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
1665
1974
  };
1666
1975
  }
1667
1976
  }
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
1977
  async function installPlugin(ref, opts) {
1829
1978
  const cfg = loadConfig();
1830
1979
  const tarball = await fetchPluginArchive(ref, cfg.portal, cfg.token);
@@ -1866,7 +2015,7 @@ async function installFromExtracted(ref, extractedDir, opts) {
1866
2015
  const forgeOtherReplacements = [];
1867
2016
  const statuses = /* @__PURE__ */ new Map();
1868
2017
  for (const pe of preflight) {
1869
- const intendedRef = ref + "/" + componentSubRef(pe.component);
2018
+ const intendedRef = ref + "/" + componentSubRef$1(pe.component);
1870
2019
  const status = await assertEntryReplaceable(pe.entryPath, {
1871
2020
  intendedRef,
1872
2021
  intendedCanonical: pe.intendedCanonical,
@@ -2053,7 +2202,7 @@ function parsePluginRef$1(ref) {
2053
2202
  };
2054
2203
  throw new Error(`invalid plugin ref: ${ref}`);
2055
2204
  }
2056
- function componentSubRef(c) {
2205
+ function componentSubRef$1(c) {
2057
2206
  switch (c.kind) {
2058
2207
  case "plugin-skill": return `skills/${c.skillName}`;
2059
2208
  case "plugin-agent-manifest": return `agents/${c.filename}`;
@@ -2140,14 +2289,6 @@ async function commitPluginInstall(params) {
2140
2289
  else await copyFileShallow(pe.intendedCanonical, pe.entryPath);
2141
2290
  }
2142
2291
  }
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
2292
  if (opts.agents.includes("codex") && components.agentManifests.length > 0) await registerCodexAgents({
2152
2293
  pluginCanonical,
2153
2294
  agentFilenames: components.agentManifests,
@@ -2172,38 +2313,6 @@ async function copyFileShallow(src, dest) {
2172
2313
  await mkdir(dirname(dest), { recursive: true });
2173
2314
  await writeFile(dest, buf);
2174
2315
  }
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
2316
  async function registerCodexAgents(params) {
2208
2317
  const configToml = join(params.scope === "global" ? getAgentHome("codex") : join(params.cwd ?? process.cwd(), ".codex"), "config.toml");
2209
2318
  let raw = "";
@@ -2497,8 +2606,6 @@ function track(data) {
2497
2606
  if (!portalUrl) portalUrl = loadConfig().portal;
2498
2607
  try {
2499
2608
  const bodyPayload = {};
2500
- if ("event" in data) bodyPayload.event = data.event;
2501
- else if (data.event_type) bodyPayload.event_type = data.event_type;
2502
2609
  for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) bodyPayload[key] = value;
2503
2610
  const p = fetch(`${portalUrl}${TELEMETRY_URL_SUFFIX}`, {
2504
2611
  method: "POST",
@@ -2705,16 +2812,16 @@ var WellKnownProvider = class {
2705
2812
  }
2706
2813
  };
2707
2814
  const wellKnownProvider = new WellKnownProvider();
2708
- const AGENTS_DIR$1 = ".agents";
2709
- const LOCK_FILE$1 = ".skill-lock.json";
2815
+ const AGENTS_DIR = ".agents";
2816
+ const LOCK_FILE = ".skill-lock.json";
2710
2817
  const CURRENT_VERSION$1 = 3;
2711
- function getSkillLockPath$1() {
2818
+ function getSkillLockPath() {
2712
2819
  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);
2820
+ if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
2821
+ return join(homedir(), AGENTS_DIR, LOCK_FILE);
2715
2822
  }
2716
- async function readSkillLock$1() {
2717
- const lockPath = getSkillLockPath$1();
2823
+ async function readSkillLock() {
2824
+ const lockPath = getSkillLockPath();
2718
2825
  try {
2719
2826
  const content = await readFile(lockPath, "utf-8");
2720
2827
  const parsed = JSON.parse(content);
@@ -2726,7 +2833,7 @@ async function readSkillLock$1() {
2726
2833
  }
2727
2834
  }
2728
2835
  async function writeSkillLock(lock) {
2729
- const lockPath = getSkillLockPath$1();
2836
+ const lockPath = getSkillLockPath();
2730
2837
  await mkdir(dirname(lockPath), { recursive: true });
2731
2838
  await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2732
2839
  }
@@ -2746,14 +2853,8 @@ function getGitHubToken() {
2746
2853
  } catch {}
2747
2854
  return null;
2748
2855
  }
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
2856
  async function addSkillToLock(skillName, entry) {
2756
- const lock = await readSkillLock$1();
2857
+ const lock = await readSkillLock();
2757
2858
  const now = (/* @__PURE__ */ new Date()).toISOString();
2758
2859
  const existingEntry = lock.skills[skillName];
2759
2860
  lock.skills[skillName] = {
@@ -2764,20 +2865,14 @@ async function addSkillToLock(skillName, entry) {
2764
2865
  await writeSkillLock(lock);
2765
2866
  }
2766
2867
  async function removeSkillFromLock(skillName) {
2767
- const lock = await readSkillLock$1();
2868
+ const lock = await readSkillLock();
2768
2869
  if (!(skillName in lock.skills)) return false;
2769
2870
  delete lock.skills[skillName];
2770
2871
  await writeSkillLock(lock);
2771
2872
  return true;
2772
2873
  }
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
2874
  async function addPluginToLock(pluginName, entry) {
2780
- const lock = await readSkillLock$1();
2875
+ const lock = await readSkillLock();
2781
2876
  if (!lock.plugins) lock.plugins = {};
2782
2877
  const now = (/* @__PURE__ */ new Date()).toISOString();
2783
2878
  const existing = lock.plugins[pluginName];
@@ -2789,7 +2884,7 @@ async function addPluginToLock(pluginName, entry) {
2789
2884
  await writeSkillLock(lock);
2790
2885
  }
2791
2886
  async function removePluginFromLock(pluginName) {
2792
- const lock = await readSkillLock$1();
2887
+ const lock = await readSkillLock();
2793
2888
  if (!lock.plugins || !(pluginName in lock.plugins)) return false;
2794
2889
  delete lock.plugins[pluginName];
2795
2890
  await writeSkillLock(lock);
@@ -2804,10 +2899,10 @@ function createEmptyLockFile() {
2804
2899
  };
2805
2900
  }
2806
2901
  async function isPromptDismissed(promptKey) {
2807
- return (await readSkillLock$1()).dismissed?.[promptKey] === true;
2902
+ return (await readSkillLock()).dismissed?.[promptKey] === true;
2808
2903
  }
2809
2904
  async function dismissPrompt(promptKey) {
2810
- const lock = await readSkillLock$1();
2905
+ const lock = await readSkillLock();
2811
2906
  if (!lock.dismissed) lock.dismissed = {};
2812
2907
  lock.dismissed[promptKey] = true;
2813
2908
  await writeSkillLock(lock);
@@ -2877,6 +2972,13 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
2877
2972
  lock.skills[skillName] = entry;
2878
2973
  await writeLocalLock(lock, cwd);
2879
2974
  }
2975
+ async function removeSkillFromLocalLock(skillName, cwd) {
2976
+ const lock = await readLocalLock(cwd);
2977
+ if (!(skillName in lock.skills)) return false;
2978
+ delete lock.skills[skillName];
2979
+ await writeLocalLock(lock, cwd);
2980
+ return true;
2981
+ }
2880
2982
  async function addPluginToLocalLock(pluginName, entry, cwd) {
2881
2983
  const lock = await readLocalLock(cwd);
2882
2984
  if (!lock.plugins) lock.plugins = {};
@@ -2896,13 +2998,6 @@ function createEmptyLocalLock() {
2896
2998
  skills: {}
2897
2999
  };
2898
3000
  }
2899
- var blob_exports = /* @__PURE__ */ __exportAll({
2900
- fetchRepoTree: () => fetchRepoTree,
2901
- findSkillMdPaths: () => findSkillMdPaths,
2902
- getSkillFolderHashFromTree: () => getSkillFolderHashFromTree,
2903
- toSkillSlug: () => toSkillSlug,
2904
- tryBlobInstall: () => tryBlobInstall
2905
- });
2906
3001
  const DOWNLOAD_BASE_URL = process.env.SKILLS_DOWNLOAD_URL || "https://skills.sh";
2907
3002
  const FETCH_TIMEOUT = 1e4;
2908
3003
  function toSkillSlug(name) {
@@ -3159,7 +3254,7 @@ function shortenPath$2(fullPath, cwd) {
3159
3254
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
3160
3255
  return fullPath;
3161
3256
  }
3162
- function formatList$1(items, maxShow = 5) {
3257
+ function formatList(items, maxShow = 5) {
3163
3258
  if (items.length <= maxShow) return items.join(", ");
3164
3259
  const shown = items.slice(0, maxShow);
3165
3260
  const remaining = items.length - maxShow;
@@ -3175,15 +3270,22 @@ function splitAgentsByType(agentTypes) {
3175
3270
  symlinked
3176
3271
  };
3177
3272
  }
3178
- function buildAgentSummaryLines(targetAgents, installMode) {
3273
+ function buildAgentSummaryLines(targetAgents, installMode, entry) {
3179
3274
  const lines = [];
3180
3275
  const { universal, symlinked } = splitAgentsByType(targetAgents);
3181
3276
  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)}`);
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)}`);
3184
3286
  } else {
3185
3287
  const allNames = targetAgents.map((a) => agents[a].displayName);
3186
- lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
3288
+ lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList(allNames)}`);
3187
3289
  }
3188
3290
  return lines;
3189
3291
  }
@@ -3198,9 +3300,9 @@ function buildResultLines(results, targetAgents) {
3198
3300
  const { universal, symlinked: symlinkAgents } = splitAgentsByType(targetAgents);
3199
3301
  const successfulSymlinks = results.filter((r) => !r.symlinkFailed && !universal.includes(r.agent)).map((r) => r.agent);
3200
3302
  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)}`);
3303
+ if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList(universal)}`);
3304
+ if (successfulSymlinks.length > 0) lines.push(` ${import_picocolors.default.dim("symlinked:")} ${formatList(successfulSymlinks)}`);
3305
+ if (failedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("copied:")} ${formatList(failedSymlinks)}`);
3204
3306
  return lines;
3205
3307
  }
3206
3308
  function multiselect(opts) {
@@ -3218,15 +3320,26 @@ async function promptForAgents(message, choices) {
3218
3320
  required: true
3219
3321
  });
3220
3322
  }
3323
+ const PLUGIN_ENTRY_ROOTS = {
3324
+ "claude-code": [".claude"],
3325
+ cursor: [".cursor"],
3326
+ codex: [".codex", ".agents/skills"]
3327
+ };
3221
3328
  function buildAgentChoices(agentList, options = {}) {
3222
3329
  return agentList.map((a) => {
3223
3330
  const cfg = agents[a];
3224
- const baseHint = options.global && cfg.globalSkillsDir ? cfg.globalSkillsDir : cfg.skillsDir;
3225
- const isShared = !options.global && isUniversalAgent(a);
3331
+ let hint;
3332
+ if (options.forPlugin) {
3333
+ const roots = PLUGIN_ENTRY_ROOTS[a];
3334
+ hint = options.global ? roots.map((r) => `~/${r}`).join(", ") : roots.join(", ");
3335
+ } else {
3336
+ const baseHint = options.global && cfg.globalSkillsDir ? cfg.globalSkillsDir : cfg.skillsDir;
3337
+ hint = !options.global && isUniversalAgent(a) ? `${baseHint}, shared` : baseHint;
3338
+ }
3226
3339
  return {
3227
3340
  value: a,
3228
3341
  label: cfg.displayName,
3229
- hint: isShared ? `${baseHint}, shared` : baseHint
3342
+ hint
3230
3343
  };
3231
3344
  });
3232
3345
  }
@@ -3404,11 +3517,15 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3404
3517
  if (summaryLines.length > 0) summaryLines.push("");
3405
3518
  const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
3406
3519
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
3407
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
3520
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode, {
3521
+ skillName: skill.installName,
3522
+ scope: installGlobally ? "global" : "project",
3523
+ cwd
3524
+ }));
3408
3525
  if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
3409
3526
  const skillOverwrites = overwriteStatus.get(skill.installName);
3410
3527
  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)}`);
3528
+ if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
3412
3529
  }
3413
3530
  console.log();
3414
3531
  Me(summaryLines.join("\n"), "Installation Summary");
@@ -3501,7 +3618,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3501
3618
  const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3502
3619
  Me(resultLines.join("\n"), title);
3503
3620
  if (symlinkFailures.length > 0) {
3504
- M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
3621
+ M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
3505
3622
  M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
3506
3623
  }
3507
3624
  }
@@ -3515,14 +3632,16 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3515
3632
  await promptForFindSkills(options, targetAgents);
3516
3633
  }
3517
3634
  async function runTeamBatchAdd(teamScope, options) {
3518
- const cfg = loadConfig();
3635
+ let cfg = loadConfig();
3519
3636
  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);
3637
+ await runLogin({ force: true });
3638
+ cfg = loadConfig();
3639
+ if (!cfg.token) {
3640
+ console.log();
3641
+ console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Login required for team installs"));
3642
+ console.log();
3643
+ process.exit(1);
3644
+ }
3526
3645
  }
3527
3646
  console.log();
3528
3647
  Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
@@ -3537,10 +3656,9 @@ async function runTeamBatchAdd(teamScope, options) {
3537
3656
  Se(import_picocolors.default.red(msg));
3538
3657
  process.exit(1);
3539
3658
  }
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.`));
3659
+ spinner.stop(`Found ${items.length} asset${items.length === 1 ? "" : "s"}`);
3660
+ if (items.length === 0) {
3661
+ Se(import_picocolors.default.dim(`Team "${teamScope}" has no assets.`));
3544
3662
  return;
3545
3663
  }
3546
3664
  const validAgents = Object.keys(agents);
@@ -3616,8 +3734,8 @@ async function runTeamBatchAdd(teamScope, options) {
3616
3734
  global: installGlobally
3617
3735
  };
3618
3736
  let failures = 0;
3619
- for (const item of skills) {
3620
- M.step(`Installing ${import_picocolors.default.cyan(item.name)}`);
3737
+ for (const item of items) {
3738
+ M.step(`Installing ${import_picocolors.default.cyan(item.name)} (${item.kind})`);
3621
3739
  try {
3622
3740
  await runAdd([item.ref], childOptions);
3623
3741
  } catch (err) {
@@ -3627,9 +3745,9 @@ async function runTeamBatchAdd(teamScope, options) {
3627
3745
  }
3628
3746
  }
3629
3747
  console.log();
3630
- if (failures === 0) Se(import_picocolors.default.green(`Installed ${skills.length} skill${skills.length === 1 ? "" : "s"} from team "${teamScope}"`));
3748
+ if (failures === 0) Se(import_picocolors.default.green(`Installed ${items.length} asset${items.length === 1 ? "" : "s"} from team "${teamScope}"`));
3631
3749
  else {
3632
- Se(import_picocolors.default.yellow(`Installed ${skills.length - failures}/${skills.length} skills (${failures} failed) from team "${teamScope}"`));
3750
+ Se(import_picocolors.default.yellow(`Installed ${items.length - failures}/${items.length} assets (${failures} failed) from team "${teamScope}"`));
3633
3751
  process.exit(1);
3634
3752
  }
3635
3753
  await promptForFindSkills(options, targetAgents);
@@ -3730,7 +3848,10 @@ async function runAdd(args, options = {}) {
3730
3848
  spinner.stop(`${Object.keys(agents).length} agents`);
3731
3849
  if (options.yes) targetAgents = installedAgents.length > 0 ? ensureUniversalAgents(installedAgents) : validAgents;
3732
3850
  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 }));
3851
+ const selected = await promptForAgents("Which IDEs do you want to install this plugin to?", buildAgentChoices(Object.keys(agents), {
3852
+ global: options.global,
3853
+ forPlugin: true
3854
+ }));
3734
3855
  if (pD(selected)) {
3735
3856
  xe("Installation cancelled");
3736
3857
  await cleanup(tempDir);
@@ -3741,7 +3862,10 @@ async function runAdd(args, options = {}) {
3741
3862
  targetAgents = ensureUniversalAgents(installedAgents);
3742
3863
  M.info(`Installing to: ${import_picocolors.default.cyan(agents[installedAgents[0]].displayName)}`);
3743
3864
  } else {
3744
- const selected = await selectAgentsInteractive({ global: options.global });
3865
+ const selected = await selectAgentsInteractive({
3866
+ global: options.global,
3867
+ forPlugin: true
3868
+ });
3745
3869
  if (pD(selected)) {
3746
3870
  xe("Installation cancelled");
3747
3871
  await cleanup(tempDir);
@@ -3776,8 +3900,8 @@ async function runAdd(args, options = {}) {
3776
3900
  const summaryLines = [];
3777
3901
  summaryLines.push(import_picocolors.default.cyan(shortenPath$2(previewCanonical, process.cwd())));
3778
3902
  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)}`);
3903
+ if (agentNames.length > 0) summaryLines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList(agentNames)}`);
3904
+ if (existsSync(previewCanonical)) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList(agentNames)}`);
3781
3905
  console.log();
3782
3906
  Me(summaryLines.join("\n"), "Installation Summary");
3783
3907
  }
@@ -3848,7 +3972,7 @@ async function runAdd(args, options = {}) {
3848
3972
  pluginPath: lockPluginPath,
3849
3973
  computedHash: lockHash
3850
3974
  });
3851
- M.info(`Recorded in lock file: ${import_picocolors.default.dim(getSkillLockPath$1())}`);
3975
+ M.info(`Recorded in lock file: ${import_picocolors.default.dim(getSkillLockPath())}`);
3852
3976
  } else {
3853
3977
  await addPluginToLocalLock(lockPluginName, {
3854
3978
  source: lockSource,
@@ -4225,16 +4349,21 @@ async function runAdd(args, options = {}) {
4225
4349
  if (!groupedSummary[group]) groupedSummary[group] = [];
4226
4350
  groupedSummary[group].push(skill);
4227
4351
  } else ungroupedSummary.push(skill);
4228
- const _summaryStoreRoot = getStoreRoot(installGlobally ? "global" : "project", cwd);
4352
+ const _summaryScope = installGlobally ? "global" : "project";
4353
+ const _summaryStoreRoot = getStoreRoot(_summaryScope, cwd);
4229
4354
  const printSkillSummary = (skills) => {
4230
4355
  for (const skill of skills) {
4231
4356
  if (summaryLines.length > 0) summaryLines.push("");
4232
4357
  const shortCanonical = shortenPath$2(join(_summaryStoreRoot, "skills", `${_summaryScopeNs}__${sanitizeName(skill.name)}`), cwd);
4233
4358
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
4234
- summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
4359
+ summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode, {
4360
+ skillName: skill.name,
4361
+ scope: _summaryScope,
4362
+ cwd
4363
+ }));
4235
4364
  const skillOverwrites = overwriteStatus.get(skill.name);
4236
4365
  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)}`);
4366
+ if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
4238
4367
  }
4239
4368
  };
4240
4369
  const sortedGroups = Object.keys(groupedSummary).sort();
@@ -4493,7 +4622,7 @@ async function runAdd(args, options = {}) {
4493
4622
  const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
4494
4623
  Me(resultLines.join("\n"), title);
4495
4624
  if (symlinkFailures.length > 0) {
4496
- M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
4625
+ M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
4497
4626
  M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
4498
4627
  }
4499
4628
  }
@@ -4627,7 +4756,7 @@ function parseAddOptions(args) {
4627
4756
  const RESET$2 = "\x1B[0m";
4628
4757
  const BOLD$2 = "\x1B[1m";
4629
4758
  const DIM$2 = "\x1B[38;5;102m";
4630
- const TEXT$1 = "\x1B[38;5;145m";
4759
+ const TEXT$2 = "\x1B[38;5;145m";
4631
4760
  const CYAN$1 = "\x1B[36m";
4632
4761
  const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
4633
4762
  function formatInstalls(count) {
@@ -4672,7 +4801,7 @@ async function runSearchPrompt(initialQuery = "") {
4672
4801
  process.stdout.write(CLEAR_DOWN);
4673
4802
  const lines = [];
4674
4803
  const cursor = `${BOLD$2}_${RESET$2}`;
4675
- lines.push(`${TEXT$1}Search skills:${RESET$2} ${query}${cursor}`);
4804
+ lines.push(`${TEXT$2}Search skills:${RESET$2} ${query}${cursor}`);
4676
4805
  lines.push("");
4677
4806
  if (!query || query.length < 2) lines.push(`${DIM$2}Start typing to search (min 2 chars)${RESET$2}`);
4678
4807
  else if (results.length === 0 && loading) lines.push(`${DIM$2}Searching...${RESET$2}`);
@@ -4683,7 +4812,7 @@ async function runSearchPrompt(initialQuery = "") {
4683
4812
  const skill = visible[i];
4684
4813
  const isSelected = i === selectedIndex;
4685
4814
  const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " ";
4686
- const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`;
4815
+ const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$2}${skill.name}${RESET$2}`;
4687
4816
  const source = skill.source ? ` ${DIM$2}${skill.source}${RESET$2}` : "";
4688
4817
  const installs = formatInstalls(skill.installs);
4689
4818
  const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$2}` : "";
@@ -4793,11 +4922,6 @@ ${DIM$2} 1) npx skills find [query]${RESET$2}
4793
4922
  ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4794
4923
  if (query) {
4795
4924
  const results = await searchSkillsAPI(query);
4796
- track({
4797
- event: "find",
4798
- query,
4799
- resultCount: String(results.length)
4800
- });
4801
4925
  if (results.length === 0) {
4802
4926
  console.log(`${DIM$2}No skills found for "${query}"${RESET$2}`);
4803
4927
  return;
@@ -4807,7 +4931,7 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4807
4931
  for (const skill of results.slice(0, 6)) {
4808
4932
  const pkg = skill.source || skill.slug;
4809
4933
  const installs = formatInstalls(skill.installs);
4810
- console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
4934
+ console.log(`${TEXT$2}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
4811
4935
  console.log(`${DIM$2}└ https://skills.sh/${skill.slug}${RESET$2}`);
4812
4936
  console.log();
4813
4937
  }
@@ -4818,12 +4942,6 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4818
4942
  console.log();
4819
4943
  }
4820
4944
  const selected = await runSearchPrompt();
4821
- track({
4822
- event: "find",
4823
- query: "",
4824
- resultCount: selected ? "1" : "0",
4825
- interactive: "1"
4826
- });
4827
4945
  if (!selected) {
4828
4946
  console.log(`${DIM$2}Search cancelled${RESET$2}`);
4829
4947
  console.log();
@@ -4832,7 +4950,7 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4832
4950
  const pkg = selected.source || selected.slug;
4833
4951
  const skillName = selected.name;
4834
4952
  console.log();
4835
- console.log(`${TEXT$1}Installing ${BOLD$2}${skillName}${RESET$2} from ${DIM$2}${pkg}${RESET$2}...`);
4953
+ console.log(`${TEXT$2}Installing ${BOLD$2}${skillName}${RESET$2} from ${DIM$2}${pkg}${RESET$2}...`);
4836
4954
  console.log();
4837
4955
  const { source, options } = parseAddOptions([
4838
4956
  pkg,
@@ -4842,11 +4960,11 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4842
4960
  await runAdd(source, options);
4843
4961
  console.log();
4844
4962
  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}`);
4963
+ 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}`);
4964
+ else console.log(`${DIM$2}Discover more skills at${RESET$2} ${TEXT$2}https://skills.sh${RESET$2}`);
4847
4965
  console.log();
4848
4966
  }
4849
- const isCancelled$1 = (value) => typeof value === "symbol";
4967
+ const isCancelled = (value) => typeof value === "symbol";
4850
4968
  function shortenPath$1(fullPath, cwd) {
4851
4969
  const home = homedir();
4852
4970
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
@@ -4997,7 +5115,7 @@ async function runSync(args, options = {}) {
4997
5115
  initialSelected: [],
4998
5116
  required: true
4999
5117
  });
5000
- if (isCancelled$1(selected)) {
5118
+ if (isCancelled(selected)) {
5001
5119
  xe("Sync cancelled");
5002
5120
  process.exit(0);
5003
5121
  }
@@ -5023,7 +5141,7 @@ async function runSync(args, options = {}) {
5023
5141
  initialSelected: installedAgents.filter((a) => allAgents.includes(a)),
5024
5142
  required: true
5025
5143
  });
5026
- if (isCancelled$1(selected)) {
5144
+ if (isCancelled(selected)) {
5027
5145
  xe("Sync cancelled");
5028
5146
  process.exit(0);
5029
5147
  }
@@ -5102,12 +5220,6 @@ async function runSync(args, options = {}) {
5102
5220
  M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
5103
5221
  for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
5104
5222
  }
5105
- track({
5106
- event: "experimental_sync",
5107
- skillCount: String(toInstall.length),
5108
- successCount: String(successfulSkillNames.size),
5109
- agents: targetAgents.join(",")
5110
- });
5111
5223
  console.log();
5112
5224
  Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
5113
5225
  }
@@ -5183,26 +5295,14 @@ async function runInstallFromLock(args) {
5183
5295
  const RESET$1 = "\x1B[0m";
5184
5296
  const BOLD$1 = "\x1B[1m";
5185
5297
  const DIM$1 = "\x1B[38;5;102m";
5298
+ const TEXT$1 = "\x1B[38;5;145m";
5186
5299
  const CYAN = "\x1B[36m";
5187
5300
  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
5301
  function parseListOptions(args) {
5201
5302
  const options = {};
5202
5303
  for (let i = 0; i < args.length; i++) {
5203
5304
  const arg = args[i];
5204
- if (arg === "-g" || arg === "--global") options.global = true;
5205
- else if (arg === "--json") options.json = true;
5305
+ if (arg === "--json") options.json = true;
5206
5306
  else if (arg === "-a" || arg === "--agent") {
5207
5307
  options.agent = options.agent || [];
5208
5308
  while (i + 1 < args.length && !args[i + 1].startsWith("-")) options.agent.push(args[++i]);
@@ -5210,152 +5310,393 @@ function parseListOptions(args) {
5210
5310
  }
5211
5311
  return options;
5212
5312
  }
5313
+ function shortenPath(fullPath, cwd) {
5314
+ const home = homedir();
5315
+ if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
5316
+ if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
5317
+ return fullPath;
5318
+ }
5319
+ function ledgerToComponent$1(c, pluginCanonical) {
5320
+ switch (c.type) {
5321
+ case "skill": return {
5322
+ kind: "plugin-skill",
5323
+ pluginCanonical,
5324
+ skillName: c.name
5325
+ };
5326
+ case "agent-manifest": return {
5327
+ kind: "plugin-agent-manifest",
5328
+ pluginCanonical,
5329
+ filename: c.name
5330
+ };
5331
+ case "agent-deps-dir": return {
5332
+ kind: "plugin-agent-deps-dir",
5333
+ pluginCanonical,
5334
+ dirname: c.name
5335
+ };
5336
+ case "rule-manifest": return {
5337
+ kind: "plugin-rule-manifest",
5338
+ pluginCanonical,
5339
+ filename: c.name
5340
+ };
5341
+ case "rule-deps-dir": return {
5342
+ kind: "plugin-rule-deps-dir",
5343
+ pluginCanonical,
5344
+ dirname: c.name
5345
+ };
5346
+ default: return null;
5347
+ }
5348
+ }
5349
+ async function entryHits(entryPath, canonicalRoot) {
5350
+ if (!entryPath) return false;
5351
+ try {
5352
+ const realEntry = await realpath(entryPath);
5353
+ let realCanon = canonicalRoot;
5354
+ try {
5355
+ realCanon = await realpath(canonicalRoot);
5356
+ } catch {}
5357
+ return realEntry === realCanon || realEntry.startsWith(realCanon + sep) || realEntry === canonicalRoot || realEntry.startsWith(canonicalRoot + sep);
5358
+ } catch {
5359
+ return false;
5360
+ }
5361
+ }
5362
+ function standaloneSkillEntryPaths(skillName, agent, cwd) {
5363
+ const paths = [];
5364
+ if (agent === "claude-code") {
5365
+ paths.push(join(cwd, ".claude", "skills", skillName));
5366
+ paths.push(join(homedir(), ".claude", "skills", skillName));
5367
+ } else if (agent === "cursor") {
5368
+ paths.push(join(cwd, ".cursor", "skills", skillName));
5369
+ paths.push(join(homedir(), ".cursor", "skills", skillName));
5370
+ } else if (agent === "codex") {
5371
+ paths.push(join(cwd, ".agents", "skills", skillName));
5372
+ paths.push(join(homedir(), ".agents", "skills", skillName));
5373
+ }
5374
+ return paths;
5375
+ }
5376
+ async function collectPluginLinks(pluginCanonical, installedComponents, agentFilter, cwd) {
5377
+ const out = [];
5378
+ for (const ag of agentFilter) for (const c of installedComponents) {
5379
+ const comp = ledgerToComponent$1(c, pluginCanonical);
5380
+ if (!comp) continue;
5381
+ for (const scope of ["project", "global"]) {
5382
+ const t = getInstallTargets(ag, comp, scope, cwd);
5383
+ if (!t.entry) continue;
5384
+ if (await entryHits(t.entry, pluginCanonical)) out.push({
5385
+ agent: ag,
5386
+ entryPath: t.entry,
5387
+ componentType: c.type,
5388
+ componentName: c.name
5389
+ });
5390
+ }
5391
+ }
5392
+ return out;
5393
+ }
5394
+ async function collectSkillLinks(canonical, skillName, agentFilter, cwd) {
5395
+ const out = [];
5396
+ for (const ag of agentFilter) for (const p of standaloneSkillEntryPaths(skillName, ag, cwd)) if (await entryHits(p, canonical)) out.push({
5397
+ agent: ag,
5398
+ entryPath: p
5399
+ });
5400
+ return out;
5401
+ }
5402
+ function uniqueAgents(links) {
5403
+ const seen = /* @__PURE__ */ new Set();
5404
+ for (const l of links) seen.add(l.agent);
5405
+ return Array.from(seen);
5406
+ }
5407
+ async function scanStore(cwd, agentFilter) {
5408
+ const storeRoot = getStoreRoot();
5409
+ const pluginsDir = join(storeRoot, "plugins");
5410
+ const skillsDir = join(storeRoot, "skills");
5411
+ const agentList = agentFilter ?? Object.keys(agents);
5412
+ const plugins = [];
5413
+ const skills = [];
5414
+ let pluginDirs = [];
5415
+ try {
5416
+ pluginDirs = (await readdir(pluginsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
5417
+ } catch {}
5418
+ for (const dirName of pluginDirs) {
5419
+ const canonical = join(pluginsDir, dirName);
5420
+ const ledgerPath = join(canonical, ".forge-plugin.json");
5421
+ let ledger;
5422
+ try {
5423
+ ledger = JSON.parse(await readFile(ledgerPath, "utf-8"));
5424
+ } catch {
5425
+ continue;
5426
+ }
5427
+ if (!ledger.ref || !ledger.name) continue;
5428
+ const installedComponents = ledger.installed_components ?? [];
5429
+ const components = {
5430
+ skills: [],
5431
+ agents: [],
5432
+ rules: []
5433
+ };
5434
+ for (const c of installedComponents) if (c.type === "skill") components.skills.push(c.name);
5435
+ else if (c.type === "agent-manifest" || c.type === "agent-deps-dir") components.agents.push(c.name);
5436
+ else if (c.type === "rule-manifest" || c.type === "rule-deps-dir") components.rules.push(c.name);
5437
+ const links = await collectPluginLinks(canonical, installedComponents, agentList, cwd);
5438
+ plugins.push({
5439
+ name: ledger.name,
5440
+ ref: ledger.ref,
5441
+ canonical,
5442
+ components,
5443
+ agents: uniqueAgents(links),
5444
+ links
5445
+ });
5446
+ }
5447
+ let skillDirs = [];
5448
+ try {
5449
+ skillDirs = (await readdir(skillsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
5450
+ } catch {}
5451
+ for (const dirName of skillDirs) {
5452
+ const canonical = join(skillsDir, dirName);
5453
+ const sidecarPath = join(canonical, ".forge-source.json");
5454
+ let sidecar;
5455
+ try {
5456
+ sidecar = JSON.parse(await readFile(sidecarPath, "utf-8"));
5457
+ } catch {
5458
+ continue;
5459
+ }
5460
+ if (sidecar.type && sidecar.type !== "skill") continue;
5461
+ if (sidecar.parent_plugin_ref) continue;
5462
+ if (!sidecar.name) continue;
5463
+ const links = await collectSkillLinks(canonical, sidecar.name, agentList, cwd);
5464
+ skills.push({
5465
+ name: sidecar.name,
5466
+ ref: sidecar.ref,
5467
+ canonical,
5468
+ agents: uniqueAgents(links),
5469
+ links
5470
+ });
5471
+ }
5472
+ plugins.sort((a, b) => a.name.localeCompare(b.name));
5473
+ skills.sort((a, b) => a.name.localeCompare(b.name));
5474
+ return {
5475
+ plugins,
5476
+ skills
5477
+ };
5478
+ }
5479
+ function pluralize(count, singular, plural) {
5480
+ return count === 1 ? `${count} ${singular}` : `${count} ${plural ?? singular + "s"}`;
5481
+ }
5482
+ function pluginComponentSummary(plugin) {
5483
+ const parts = [];
5484
+ parts.push(pluralize(plugin.components.skills.length, "skill"));
5485
+ parts.push(pluralize(plugin.components.agents.length, "agent"));
5486
+ parts.push(pluralize(plugin.components.rules.length, "rule"));
5487
+ return parts.join(" · ");
5488
+ }
5213
5489
  async function runList(args) {
5214
5490
  const options = parseListOptions(args);
5215
- const scope = options.global === true ? true : false;
5491
+ const cwd = process.cwd();
5216
5492
  let agentFilter;
5217
5493
  if (options.agent && options.agent.length > 0) {
5218
5494
  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}`);
5495
+ const invalid = options.agent.filter((a) => !validAgents.includes(a));
5496
+ if (invalid.length > 0) {
5497
+ console.log(`${YELLOW}Invalid agents: ${invalid.join(", ")}${RESET$1}`);
5222
5498
  console.log(`${DIM$1}Valid agents: ${validAgents.join(", ")}${RESET$1}`);
5223
5499
  process.exit(1);
5224
5500
  }
5225
5501
  agentFilter = options.agent;
5226
5502
  }
5227
- const installedSkills = await listInstalledSkills({
5228
- global: scope,
5229
- agentFilter
5230
- });
5503
+ const inventory = await scanStore(cwd, agentFilter);
5231
5504
  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));
5505
+ const out = {
5506
+ plugins: inventory.plugins.map((p) => ({
5507
+ name: p.name,
5508
+ ref: p.ref,
5509
+ canonical: p.canonical,
5510
+ agents: p.agents,
5511
+ components: p.components,
5512
+ links: p.links
5513
+ })),
5514
+ skills: inventory.skills.map((s) => ({
5515
+ name: s.name,
5516
+ ref: s.ref ?? null,
5517
+ canonical: s.canonical,
5518
+ agents: s.agents,
5519
+ links: s.links
5520
+ }))
5521
+ };
5522
+ console.log(JSON.stringify(out, null, 2));
5239
5523
  return;
5240
5524
  }
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}`);
5525
+ if (inventory.plugins.length === 0 && inventory.skills.length === 0) {
5526
+ console.log(`${DIM$1}No forge-managed skills or plugins installed.${RESET$1}`);
5527
+ console.log(`${DIM$1}Run${RESET$1} ${TEXT$1}add <ref>${RESET$1} ${DIM$1}to install one.${RESET$1}`);
5252
5528
  return;
5253
5529
  }
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);
5530
+ if (inventory.plugins.length > 0) {
5531
+ console.log(`${BOLD$1}Plugins${RESET$1}`);
5532
+ for (const plugin of inventory.plugins) {
5533
+ const safeName = sanitizeMetadata(plugin.name);
5534
+ console.log(` ${CYAN}${safeName}${RESET$1} ${DIM$1}${plugin.ref}${RESET$1}`);
5535
+ console.log(` ${DIM$1}${shortenPath(plugin.canonical, cwd)}${RESET$1}`);
5536
+ console.log(` ${DIM$1}Components:${RESET$1} ${pluginComponentSummary(plugin)}`);
5537
+ printLinkedFrom(plugin.links, cwd);
5281
5538
  console.log();
5282
5539
  }
5283
- if (ungroupedSkills.length > 0) {
5284
- console.log(`${BOLD$1}General${RESET$1}`);
5285
- for (const skill of ungroupedSkills) printSkill(skill, true);
5540
+ }
5541
+ if (inventory.skills.length > 0) {
5542
+ console.log(`${BOLD$1}Skills${RESET$1}`);
5543
+ for (const skill of inventory.skills) {
5544
+ const safeName = sanitizeMetadata(skill.name);
5545
+ const refTail = skill.ref ? ` ${DIM$1}${skill.ref}${RESET$1}` : "";
5546
+ console.log(` ${CYAN}${safeName}${RESET$1}${refTail}`);
5547
+ console.log(` ${DIM$1}${shortenPath(skill.canonical, cwd)}${RESET$1}`);
5548
+ printLinkedFrom(skill.links, cwd);
5286
5549
  console.log();
5287
5550
  }
5288
- } else {
5289
- for (const skill of installedSkills) printSkill(skill);
5290
- console.log();
5291
5551
  }
5292
5552
  }
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)}`));
5553
+ function printLinkedFrom(links, cwd) {
5554
+ if (links.length === 0) {
5555
+ console.log(` ${DIM$1}Linked from:${RESET$1} ${YELLOW}(none)${RESET$1}`);
5556
+ return;
5557
+ }
5558
+ const grouped = /* @__PURE__ */ new Map();
5559
+ for (const l of links) {
5560
+ const list = grouped.get(l.agent) ?? [];
5561
+ list.push(shortenPath(l.entryPath, cwd));
5562
+ grouped.set(l.agent, list);
5563
+ }
5564
+ if (grouped.size === 1) {
5565
+ const [agent, paths] = grouped.entries().next().value;
5566
+ if (paths.length === 1) {
5567
+ console.log(` ${DIM$1}Linked from:${RESET$1} ${agents[agent].displayName} ${DIM$1}${paths[0]}${RESET$1}`);
5568
+ return;
5306
5569
  }
5307
- const nonPluginNames = skillNames.filter((n) => !pluginRefRe.test(n));
5308
- if (nonPluginNames.length === 0) return;
5309
- skillNames = nonPluginNames;
5310
5570
  }
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}`);
5571
+ console.log(` ${DIM$1}Linked from:${RESET$1}`);
5572
+ const labelWidth = Math.max(...Array.from(grouped.keys()).map((a) => agents[a].displayName.length));
5573
+ for (const [agent, paths] of grouped) {
5574
+ const label = agents[agent].displayName.padEnd(labelWidth);
5575
+ if (paths.length === 1) console.log(` ${label} ${DIM$1}${paths[0]}${RESET$1}`);
5576
+ else {
5577
+ console.log(` ${label}`);
5578
+ for (const p of paths) console.log(` ${DIM$1}${p}${RESET$1}`);
5320
5579
  }
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
5580
  }
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;
5581
+ }
5582
+ const PLUGIN_REF_RE = /^@(?:public|teams\/[^/]+)\/plugins\/[^/]+/;
5583
+ function isPluginRef(s) {
5584
+ return PLUGIN_REF_RE.test(s);
5585
+ }
5586
+ function parseRemoveOptions(args) {
5587
+ const options = {};
5588
+ const skills = [];
5589
+ for (let i = 0; i < args.length; i++) {
5590
+ const arg = args[i];
5591
+ if (arg === "-y" || arg === "--yes") options.yes = true;
5592
+ else if (arg === "--all") options.all = true;
5593
+ else if (arg === "-a" || arg === "--agent") {
5594
+ options.agent = options.agent || [];
5595
+ i++;
5596
+ let nextArg = args[i];
5597
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
5598
+ options.agent.push(nextArg);
5599
+ i++;
5600
+ nextArg = args[i];
5601
+ }
5602
+ i--;
5603
+ } else if (arg && !arg.startsWith("-")) skills.push(arg);
5334
5604
  }
5605
+ return {
5606
+ skills,
5607
+ options
5608
+ };
5609
+ }
5610
+ function findOwningPlugin(name, inventory) {
5611
+ const lower = name.toLowerCase();
5612
+ 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;
5613
+ return null;
5614
+ }
5615
+ async function removeCommand(skillNames, options) {
5616
+ const cwd = process.cwd();
5335
5617
  if (options.agent && options.agent.length > 0) {
5336
5618
  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(", ")}`);
5619
+ const invalid = options.agent.filter((a) => !validAgents.includes(a));
5620
+ if (invalid.length > 0) {
5621
+ M.error(`Invalid agents: ${invalid.join(", ")}`);
5340
5622
  M.info(`Valid agents: ${validAgents.join(", ")}`);
5341
5623
  process.exit(1);
5342
5624
  }
5343
5625
  }
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(", ")}`);
5626
+ const inventory = await scanStore(cwd);
5627
+ const targets = [];
5628
+ const errors = [];
5629
+ const explicitPluginRefs = skillNames.filter(isPluginRef);
5630
+ const bareNames = skillNames.filter((n) => !isPluginRef(n));
5631
+ for (const ref of explicitPluginRefs) {
5632
+ const plugin = inventory.plugins.find((pl) => pl.ref === ref);
5633
+ if (plugin) targets.push({
5634
+ kind: "plugin",
5635
+ plugin
5636
+ });
5637
+ else errors.push(`No installed plugin matching ref ${ref}`);
5638
+ }
5639
+ if (options.all) {
5640
+ if (bareNames.length > 0) errors.push("--all cannot be combined with positional names");
5641
+ for (const plugin of inventory.plugins) targets.push({
5642
+ kind: "plugin",
5643
+ plugin
5644
+ });
5645
+ for (const skill of inventory.skills) targets.push({
5646
+ kind: "skill",
5647
+ skill
5648
+ });
5649
+ } else if (bareNames.length > 0) for (const name of bareNames) {
5650
+ const lower = name.toLowerCase();
5651
+ const plugin = inventory.plugins.find((pl) => pl.name.toLowerCase() === lower);
5652
+ if (plugin) {
5653
+ targets.push({
5654
+ kind: "plugin",
5655
+ plugin
5656
+ });
5657
+ continue;
5658
+ }
5659
+ const skill = inventory.skills.find((s) => s.name.toLowerCase() === lower);
5660
+ if (skill) {
5661
+ targets.push({
5662
+ kind: "skill",
5663
+ skill
5664
+ });
5665
+ continue;
5666
+ }
5667
+ const owner = findOwningPlugin(name, inventory);
5668
+ if (owner) {
5669
+ errors.push(`${name} belongs to plugin ${owner.ref}\n Cannot remove individual plugin components.\n Run: remove ${owner.ref}`);
5670
+ continue;
5671
+ }
5672
+ errors.push(`No forge-managed skill or plugin matching '${name}'.`);
5673
+ }
5674
+ else if (explicitPluginRefs.length === 0) {
5675
+ if (inventory.plugins.length === 0 && inventory.skills.length === 0) {
5676
+ Se(import_picocolors.default.yellow("No skills or plugins installed."));
5350
5677
  return;
5351
5678
  }
5352
- } else {
5353
- const choices = installedSkills.map((s) => ({
5354
- value: s,
5355
- label: s
5356
- }));
5679
+ const choices = [];
5680
+ for (const plugin of inventory.plugins) {
5681
+ const summary = `${plugin.components.skills.length} skill(s), ${plugin.components.agents.length} agent(s), ${plugin.components.rules.length} rule(s)`;
5682
+ choices.push({
5683
+ value: {
5684
+ kind: "plugin",
5685
+ plugin
5686
+ },
5687
+ label: `[plugin] ${plugin.name}`,
5688
+ hint: summary
5689
+ });
5690
+ }
5691
+ for (const skill of inventory.skills) choices.push({
5692
+ value: {
5693
+ kind: "skill",
5694
+ skill
5695
+ },
5696
+ label: `[skill] ${skill.name}`
5697
+ });
5357
5698
  const selected = await fe({
5358
- message: `Select skills to remove ${import_picocolors.default.dim("(space to toggle)")}`,
5699
+ message: `Select what to remove ${import_picocolors.default.dim("(space to toggle)")}`,
5359
5700
  options: choices,
5360
5701
  required: true
5361
5702
  });
@@ -5363,172 +5704,157 @@ async function removeCommand(skillNames, options) {
5363
5704
  xe("Removal cancelled");
5364
5705
  process.exit(0);
5365
5706
  }
5366
- selectedSkills = selected;
5707
+ targets.push(...selected);
5367
5708
  }
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)`);
5709
+ for (const err of errors) M.error(err);
5710
+ if (targets.length === 0) {
5711
+ if (errors.length === 0) Se(import_picocolors.default.yellow("Nothing to remove."));
5712
+ return;
5373
5713
  }
5374
5714
  if (!options.yes) {
5375
5715
  console.log();
5376
- M.info("Skills to remove:");
5377
- for (const skill of selectedSkills) M.message(` ${import_picocolors.default.red("•")} ${skill}`);
5716
+ M.info("Planned removals:");
5717
+ for (const t of targets) {
5718
+ const tag = t.kind === "plugin" ? "[plugin]" : "[skill] ";
5719
+ const name = t.kind === "plugin" ? t.plugin?.name : t.skill?.name;
5720
+ const ref = t.kind === "plugin" ? t.plugin?.ref : t.skill?.ref;
5721
+ const refTail = ref ? ` ${import_picocolors.default.dim(ref)}` : "";
5722
+ M.message(` ${import_picocolors.default.red("•")} ${tag} ${name}${refTail}`);
5723
+ }
5378
5724
  console.log();
5379
- const confirmed = await ye({ message: `Are you sure you want to uninstall ${selectedSkills.length} skill(s)?` });
5725
+ const confirmed = await ye({ message: `Uninstall ${targets.length} item(s)?` });
5380
5726
  if (pD(confirmed) || !confirmed) {
5381
5727
  xe("Removal cancelled");
5382
5728
  process.exit(0);
5383
5729
  }
5384
5730
  }
5385
- spinner.start("Removing skills...");
5731
+ const spinner = Y();
5732
+ spinner.start("Removing...");
5386
5733
  const results = [];
5387
- for (const skillName of selectedSkills) try {
5388
- const canonicalPath = getCanonicalPath(skillName, {
5389
- global: isGlobal,
5390
- cwd
5734
+ const agentFilter = options.agent ?? null;
5735
+ for (const t of targets) if (t.kind === "plugin" && t.plugin) try {
5736
+ await uninstallPlugin(t.plugin, cwd, agentFilter);
5737
+ results.push({
5738
+ name: t.plugin.name,
5739
+ kind: "plugin",
5740
+ success: true
5391
5741
  });
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
5742
+ } catch (err) {
5743
+ results.push({
5744
+ name: t.plugin.name,
5745
+ kind: "plugin",
5746
+ success: false,
5747
+ error: err instanceof Error ? err.message : String(err)
5426
5748
  });
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);
5749
+ }
5750
+ else if (t.kind === "skill" && t.skill) try {
5751
+ await uninstallStandaloneSkill(t.skill, cwd, agentFilter);
5431
5752
  results.push({
5432
- skill: skillName,
5433
- success: true,
5434
- source: effectiveSource,
5435
- sourceType: effectiveSourceType
5753
+ name: t.skill.name,
5754
+ kind: "skill",
5755
+ success: true
5436
5756
  });
5437
5757
  } catch (err) {
5438
5758
  results.push({
5439
- skill: skillName,
5759
+ name: t.skill.name,
5760
+ kind: "skill",
5440
5761
  success: false,
5441
5762
  error: err instanceof Error ? err.message : String(err)
5442
5763
  });
5443
5764
  }
5444
- spinner.stop("Removal process complete");
5765
+ spinner.stop("Done");
5445
5766
  const successful = results.filter((r) => r.success);
5446
5767
  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)`));
5768
+ if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} item(s)`));
5466
5769
  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}`);
5770
+ M.error(import_picocolors.default.red(`Failed to remove ${failed.length} item(s)`));
5771
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.name}: ${r.error}`);
5469
5772
  }
5470
5773
  console.log();
5471
5774
  Se(import_picocolors.default.green("Done!"));
5472
5775
  }
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);
5492
- }
5493
- return {
5494
- skills,
5495
- options
5496
- };
5497
- }
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");
5503
- let ledger;
5504
- try {
5505
- ledger = JSON.parse(await readFile(ledgerPath, "utf-8"));
5506
- } 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,
5776
+ async function uninstallStandaloneSkill(skill, cwd, agentFilter) {
5777
+ const storeRoot = getStoreRoot();
5778
+ const intendedRef = skill.ref ?? `<store>/${skill.name}`;
5779
+ const candidates = standaloneSkillEntryCandidates(skill.name, cwd, agentFilter);
5780
+ for (const entry of candidates) try {
5781
+ const status = await assertEntryReplaceable(entry, {
5782
+ intendedRef,
5783
+ intendedCanonical: skill.canonical,
5515
5784
  storeRoot
5516
5785
  });
5517
- if (status.kind === "forge-self") try {
5518
- await rm(entryPath, {
5519
- recursive: true,
5520
- force: true
5521
- });
5522
- } catch {}
5523
- else if (status.kind !== "absent") M.warn(`skipping ${entryPath} (${status.kind}) — not deleted (preserved by forge)`);
5524
- }
5525
- if (ledger.install_modes.includes("codex")) await cleanupCodexConfig(ledger, scope, cwd, scopeNamespace, pluginName);
5526
- try {
5527
- await rm(pluginCanonical, {
5786
+ if (status.kind === "absent") continue;
5787
+ if (status.kind === "forge-self" || status.kind === "forge-other") await rm(entry, {
5528
5788
  recursive: true,
5529
5789
  force: true
5530
5790
  });
5791
+ else M.warn(`skipping ${entry} (${status.kind}) — not deleted (preserved by forge)`);
5531
5792
  } catch {}
5793
+ await rm(skill.canonical, {
5794
+ recursive: true,
5795
+ force: true
5796
+ });
5797
+ try {
5798
+ await removeSkillFromLock(skill.name);
5799
+ } catch {}
5800
+ try {
5801
+ await removeSkillFromLocalLock(skill.name, cwd);
5802
+ } catch {}
5803
+ }
5804
+ function standaloneSkillEntryCandidates(skillName, cwd, agentFilter) {
5805
+ const home = homedir();
5806
+ const list = agentFilter ?? Object.keys(agents);
5807
+ const out = [];
5808
+ for (const ag of list) if (ag === "claude-code") {
5809
+ out.push(join(cwd, ".claude", "skills", skillName));
5810
+ out.push(join(home, ".claude", "skills", skillName));
5811
+ } else if (ag === "cursor") {
5812
+ out.push(join(cwd, ".cursor", "skills", skillName));
5813
+ out.push(join(home, ".cursor", "skills", skillName));
5814
+ } else if (ag === "codex") {
5815
+ out.push(join(cwd, ".agents", "skills", skillName));
5816
+ out.push(join(home, ".agents", "skills", skillName));
5817
+ }
5818
+ return out;
5819
+ }
5820
+ async function uninstallPlugin(plugin, cwd, agentFilter) {
5821
+ const storeRoot = getStoreRoot();
5822
+ const ledgerPath = join(plugin.canonical, ".forge-plugin.json");
5823
+ let ledger;
5824
+ try {
5825
+ ledger = JSON.parse(await readFile(ledgerPath, "utf-8"));
5826
+ } catch {
5827
+ throw new Error(`plugin ledger missing or unreadable: ${ledgerPath}`);
5828
+ }
5829
+ const installModes = agentFilter ? ledger.install_modes.filter((a) => agentFilter.includes(a)) : ledger.install_modes;
5830
+ for (const agent of installModes) for (const c of ledger.installed_components) {
5831
+ const comp = ledgerToComponent(c, plugin.canonical);
5832
+ if (!comp) continue;
5833
+ for (const scope of ["project", "global"]) {
5834
+ const t = getInstallTargets(agent, comp, scope, cwd);
5835
+ if (!t.entry) continue;
5836
+ const intendedRef = `${plugin.ref}/${componentSubRef(comp)}`;
5837
+ const status = await assertEntryReplaceable(t.entry, {
5838
+ intendedRef,
5839
+ intendedCanonical: t.canonical,
5840
+ storeRoot
5841
+ });
5842
+ if (status.kind === "absent") continue;
5843
+ if (status.kind === "forge-self") try {
5844
+ await rm(t.entry, {
5845
+ recursive: true,
5846
+ force: true
5847
+ });
5848
+ } catch {}
5849
+ else M.warn(`skipping ${t.entry} (${status.kind}) — not deleted (preserved by forge)`);
5850
+ }
5851
+ }
5852
+ if (installModes.includes("codex")) for (const scope of ["project", "global"]) await cleanupCodexConfigToml(ledger, scope, cwd);
5853
+ await rm(plugin.canonical, {
5854
+ recursive: true,
5855
+ force: true
5856
+ });
5857
+ const { pluginName } = parsePluginRef(plugin.ref);
5532
5858
  try {
5533
5859
  await removePluginFromLock(pluginName);
5534
5860
  } catch {}
@@ -5536,75 +5862,63 @@ async function uninstallPlugin(ref, scope, cwd) {
5536
5862
  await removePluginFromLocalLock(pluginName, cwd);
5537
5863
  } catch {}
5538
5864
  }
5539
- function resolveEntryPath(agent, c, pluginCanonical, scope, cwd) {
5540
- let component;
5865
+ function componentSubRef(c) {
5866
+ switch (c.kind) {
5867
+ case "plugin-skill": return `skills/${c.skillName}`;
5868
+ case "plugin-agent-manifest": return `agents/${c.filename}`;
5869
+ case "plugin-agent-deps-dir": return `agents/${c.dirname}`;
5870
+ case "plugin-rule-manifest": return `rules/${c.filename}`;
5871
+ case "plugin-rule-deps-dir": return `rules/${c.dirname}`;
5872
+ }
5873
+ }
5874
+ function ledgerToComponent(c, pluginCanonical) {
5541
5875
  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 "";
5876
+ case "skill": return {
5877
+ kind: "plugin-skill",
5878
+ pluginCanonical,
5879
+ skillName: c.name
5880
+ };
5881
+ case "agent-manifest": return {
5882
+ kind: "plugin-agent-manifest",
5883
+ pluginCanonical,
5884
+ filename: c.name
5885
+ };
5886
+ case "agent-deps-dir": return {
5887
+ kind: "plugin-agent-deps-dir",
5888
+ pluginCanonical,
5889
+ dirname: c.name
5890
+ };
5891
+ case "rule-manifest": return {
5892
+ kind: "plugin-rule-manifest",
5893
+ pluginCanonical,
5894
+ filename: c.name
5895
+ };
5896
+ case "rule-deps-dir": return {
5897
+ kind: "plugin-rule-deps-dir",
5898
+ pluginCanonical,
5899
+ dirname: c.name
5900
+ };
5901
+ default: return null;
5578
5902
  }
5579
- return getInstallTargets(agent, component, scope, cwd).entry;
5580
5903
  }
5581
- async function cleanupCodexConfig(ledger, scope, cwd, scopeNs, pluginName) {
5582
- const configToml = join(scope === "global" ? getAgentHome("codex") : join(cwd ?? process.cwd(), ".codex"), "config.toml");
5904
+ async function cleanupCodexConfigToml(ledger, scope, cwd) {
5905
+ const configToml = join(scope === "global" ? getAgentHome("codex") : join(cwd, ".codex"), "config.toml");
5583
5906
  let tomlRaw = "";
5584
5907
  try {
5585
5908
  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
5909
  } catch {
5604
5910
  return;
5605
5911
  }
5606
- const updatedAgentsMd = removePluginRules(agentsRaw, `${scopeNs}:${pluginName}`);
5607
- if (updatedAgentsMd !== agentsRaw) await writeFile(canonicalAgentsMd, updatedAgentsMd, "utf-8");
5912
+ let changed = false;
5913
+ for (const c of ledger.installed_components) if (c.type === "agent-manifest") {
5914
+ const id = c.name.replace(/\.md$/, "");
5915
+ const updated = removeCodexAgent(tomlRaw, id);
5916
+ if (updated !== tomlRaw) {
5917
+ tomlRaw = updated;
5918
+ changed = true;
5919
+ }
5920
+ }
5921
+ if (changed) await writeFile(configToml, tomlRaw, "utf-8");
5608
5922
  }
5609
5923
  function parsePluginRef(ref) {
5610
5924
  let m = /^@public\/plugins\/(.+)$/.exec(ref);
@@ -5619,202 +5933,6 @@ function parsePluginRef(ref) {
5619
5933
  };
5620
5934
  throw new Error(`invalid plugin ref: ${ref}`);
5621
5935
  }
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
5936
  var import_lib = /* @__PURE__ */ __toESM(require_lib(), 1);
5819
5937
  async function getBootstrap(opts = {}) {
5820
5938
  const cfg = loadConfig();
@@ -5979,11 +6097,14 @@ function validatePath(path) {
5979
6097
  return { valid: true };
5980
6098
  }
5981
6099
  async function runPublish(paths, options = {}) {
5982
- const cfg = loadConfig();
6100
+ let cfg = loadConfig();
5983
6101
  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);
6102
+ await runLogin({ force: true });
6103
+ cfg = loadConfig();
6104
+ if (!cfg.token) {
6105
+ M.error("Login required to publish.");
6106
+ process.exit(1);
6107
+ }
5987
6108
  }
5988
6109
  console.log();
5989
6110
  Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" publish ")));
@@ -6032,14 +6153,25 @@ async function runPublish(paths, options = {}) {
6032
6153
  form.set("asset_type", assetType);
6033
6154
  form.set("file", blob, filename);
6034
6155
  try {
6035
- const resp = await fetch(contributionsUrl, {
6156
+ const sendRequest = (token) => fetch(contributionsUrl, {
6036
6157
  method: "POST",
6037
6158
  headers: {
6038
- Authorization: `Bearer ${cfg.token}`,
6159
+ Authorization: `Bearer ${token}`,
6039
6160
  "User-Agent": "shoplazza-ai-dev-cli"
6040
6161
  },
6041
6162
  body: form
6042
6163
  });
6164
+ let resp = await sendRequest(cfg.token);
6165
+ if (resp.status === 401) {
6166
+ await resp.arrayBuffer().catch(() => void 0);
6167
+ await runLogin({ force: true });
6168
+ cfg = loadConfig();
6169
+ if (!cfg.token) {
6170
+ M.error(`Failed to publish ${name}: login required`);
6171
+ continue;
6172
+ }
6173
+ resp = await sendRequest(cfg.token);
6174
+ }
6043
6175
  if (!resp.ok) {
6044
6176
  const errorText = await resp.text();
6045
6177
  M.error(`Failed to publish ${name}: ${resp.status} ${errorText}`);
@@ -6049,12 +6181,6 @@ async function runPublish(paths, options = {}) {
6049
6181
  M.success(`Published: ${import_picocolors.default.green(name)}`);
6050
6182
  if (result?.asset_ref) M.message(import_picocolors.default.dim(`Asset: ${result.asset_ref}`));
6051
6183
  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
6184
  } catch (err) {
6059
6185
  const message = err instanceof Error ? err.message : String(err);
6060
6186
  M.error(`Failed to publish ${name}: ${message}`);
@@ -6136,12 +6262,12 @@ function showBanner() {
6136
6262
  console.log();
6137
6263
  console.log(`${DIM}Shoplazza Forge skill CLI${RESET}`);
6138
6264
  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}`);
6265
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli add ${DIM}<source>${RESET} ${DIM}Add a skill or plugin${RESET}`);
6266
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli remove${RESET} ${DIM}Remove installed skills or plugins${RESET}`);
6267
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli list${RESET} ${DIM}List installed skills and plugins${RESET}`);
6142
6268
  console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
6143
6269
  console.log();
6144
- console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli update${RESET} ${DIM}Update installed skills${RESET}`);
6270
+ console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli update${RESET} ${DIM}Refresh installed skills and plugins${RESET}`);
6145
6271
  console.log();
6146
6272
  console.log(` ${DIM}$${RESET} ${TEXT}npx shoplazza-ai-dev-cli init ${DIM}[name]${RESET} ${DIM}Initialize a skill${RESET}`);
6147
6273
  console.log();
@@ -6153,29 +6279,24 @@ function showHelp() {
6153
6279
  ${BOLD}Usage:${RESET} shoplazza-ai-dev-cli <command> [options]
6154
6280
 
6155
6281
  ${BOLD}Manage Skills:${RESET}
6156
- add <source> Add a skill (alias: a)
6282
+ add <source> Add a skill or plugin (alias: a)
6157
6283
  e.g. @public/skills --skill <name>
6158
6284
  @teams/<scope>/skills --skill <name>
6159
- @public/skills/<name> # legacy, still supported
6285
+ @public/plugins/<name>
6160
6286
  https://gitlab.shoplazza.site/.../<repo>.git
6161
6287
  add --team <scope> Install every skill owned by a team
6162
- remove [skills] Remove installed skills
6163
- list, ls List installed skills
6288
+ remove [names...] Remove installed skills or plugins
6289
+ list, ls List installed skills and plugins
6164
6290
  find [query] Search for skills interactively
6165
6291
 
6166
6292
  ${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)
6293
+ update [names...] Refresh installed skills/plugins from source (alias: upgrade)
6173
6294
 
6174
6295
  ${BOLD}Project:${RESET}
6175
6296
  init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
6176
6297
 
6177
6298
  ${BOLD}Add Options:${RESET}
6178
- -g, --global Install skill globally (user-level) instead of project-level
6299
+ -g, --global Install at global scope (entry symlinks under ~/)
6179
6300
  -a, --agent <agents> Specify agents to install to (use '*' for all agents)
6180
6301
  -s, --skill <skills> Specify skill names to install (use '*' for all skills)
6181
6302
  -l, --list List available skills in the source without installing
@@ -6187,62 +6308,62 @@ ${BOLD}Add Options:${RESET}
6187
6308
  --full-depth Search all subdirectories even when a root SKILL.md exists
6188
6309
 
6189
6310
  ${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)
6311
+ -a, --agent <agents> Restrict cleanup to specific agents
6193
6312
  -y, --yes Skip confirmation prompts
6194
- --all Shorthand for --skill '*' --agent '*' -y
6313
+ --all Remove every installed skill and plugin
6195
6314
 
6196
6315
  ${BOLD}List Options:${RESET}
6197
- -g, --global List global skills (default: project)
6198
- -a, --agent <agents> Filter by specific agents
6316
+ -a, --agent <agents> Filter the "Linked from" column by agent
6199
6317
  --json Output as JSON (machine-readable, no ANSI codes)
6200
6318
 
6319
+ ${BOLD}Update Options:${RESET}
6320
+ -y, --yes Skip confirmation prompts
6321
+ --json Machine-readable summary
6322
+
6201
6323
  ${BOLD}Options:${RESET}
6202
6324
  --help, -h Show this help message
6203
6325
  --version, -v Show version number
6204
6326
 
6205
6327
  ${BOLD}Examples:${RESET}
6206
6328
  ${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
6329
+ ${DIM}$${RESET} shoplazza-ai-dev-cli add @public/plugins/forge-demo-plugin
6209
6330
  ${DIM}$${RESET} shoplazza-ai-dev-cli add @teams/infra/skills --skill <name>
6210
6331
  ${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}
6332
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove my-skill ${DIM}# bare-name resolved against store${RESET}
6333
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove @public/plugins/foo ${DIM}# explicit plugin ref${RESET}
6334
+ ${DIM}$${RESET} shoplazza-ai-dev-cli list ${DIM}# list everything in store${RESET}
6335
+ ${DIM}$${RESET} shoplazza-ai-dev-cli list --json
6216
6336
  ${DIM}$${RESET} shoplazza-ai-dev-cli update
6337
+ ${DIM}$${RESET} shoplazza-ai-dev-cli update some-skill
6217
6338
  ${DIM}$${RESET} shoplazza-ai-dev-cli init my-skill
6218
6339
  `);
6219
6340
  }
6220
6341
  function showRemoveHelp() {
6221
6342
  console.log(`
6222
- ${BOLD}Usage:${RESET} shoplazza-ai-dev-cli remove [skills...] [options]
6343
+ ${BOLD}Usage:${RESET} shoplazza-ai-dev-cli remove [names...] [options]
6223
6344
 
6224
6345
  ${BOLD}Description:${RESET}
6225
- Remove installed skills from agents. If no skill names are provided,
6226
- an interactive selection menu will be shown.
6346
+ Resolves each name against the forge store (~/.ai-dev-cli/store):
6347
+ - explicit plugin ref or bare plugin name → cascades full plugin uninstall
6348
+ - bare standalone-skill name → uninstalls just that skill
6349
+ - bare component name owned by a plugin → refused (uninstall the plugin instead)
6227
6350
 
6228
6351
  ${BOLD}Arguments:${RESET}
6229
- skills Optional skill names to remove (space-separated)
6352
+ names Optional skill / plugin names (space-separated). Empty triggers an
6353
+ interactive multiselect over everything currently in the store.
6230
6354
 
6231
6355
  ${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
6356
+ -a, --agent Restrict entry cleanup to specific IDE agents
6357
+ -y, --yes Skip confirmation prompts
6358
+ --all Remove every installed skill and plugin
6237
6359
 
6238
6360
  ${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}
6361
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove ${DIM}# interactive${RESET}
6362
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove my-skill ${DIM}# resolve and remove${RESET}
6363
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove skill1 skill2 -y ${DIM}# remove several${RESET}
6364
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove @public/plugins/foo ${DIM}# explicit plugin ref${RESET}
6365
+ ${DIM}$${RESET} shoplazza-ai-dev-cli rm --agent claude-code my-skill ${DIM}# clean only Claude Code entry${RESET}
6366
+ ${DIM}$${RESET} shoplazza-ai-dev-cli remove --all ${DIM}# remove everything${RESET}
6246
6367
  `);
6247
6368
  }
6248
6369
  function runInit(args) {
@@ -6289,383 +6410,155 @@ Describe when this skill should be used.
6289
6410
  console.log(` ${DIM}Forge:${RESET} ${TEXT}shoplazza-ai-dev-cli publish ${displayPath.replace("/SKILL.md", "")}${RESET}`);
6290
6411
  console.log();
6291
6412
  }
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
6413
  function parseUpdateOptions(args) {
6322
6414
  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;
6415
+ const names = [];
6416
+ for (const arg of args) if (arg === "-y" || arg === "--yes") options.yes = true;
6417
+ else if (arg === "--json") options.json = true;
6418
+ else if (!arg.startsWith("-")) names.push(arg);
6419
+ if (names.length > 0) options.names = names;
6329
6420
  return options;
6330
6421
  }
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;
6422
+ function hasProjectEntryFor(canonical, cwd) {
6423
+ const candidates = [
6424
+ join(cwd, ".claude", "skills"),
6425
+ join(cwd, ".cursor", "skills"),
6426
+ join(cwd, ".agents", "skills"),
6427
+ join(cwd, ".claude", "agents"),
6428
+ join(cwd, ".cursor", "agents"),
6429
+ join(cwd, ".codex", "agents"),
6430
+ join(cwd, ".claude", "rules"),
6431
+ join(cwd, ".cursor", "rules")
6432
+ ];
6433
+ for (const dir of candidates) {
6434
+ if (!existsSync(dir)) continue;
6435
+ let entries;
6436
+ try {
6437
+ entries = readdirSyncSafe(dir);
6438
+ } catch {
6439
+ continue;
6339
6440
  }
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}`);
6441
+ for (const name of entries) {
6442
+ const entryPath = join(dir, name);
6443
+ try {
6444
+ const real = realpathSync(entryPath);
6445
+ if (real === canonical || real.startsWith(canonical + sep)) return true;
6446
+ } catch {}
6419
6447
  }
6420
- console.log(` ${DIM}To update: ${TEXT}npx skills add ${source} -g -y${RESET}`);
6421
6448
  }
6449
+ return false;
6422
6450
  }
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
- });
6451
+ function readdirSyncSafe(dir) {
6452
+ try {
6453
+ return readdirSync(dir);
6454
+ } catch {
6455
+ return [];
6434
6456
  }
6435
- return skills;
6436
6457
  }
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
- };
6458
+ async function runUpdate(args = []) {
6459
+ const options = parseUpdateOptions(args);
6460
+ const cwd = process.cwd();
6461
+ const inventory = await scanStore(cwd);
6462
+ let targets = [...inventory.plugins.map((pl) => ({
6463
+ kind: "plugin",
6464
+ name: pl.name,
6465
+ ref: pl.ref,
6466
+ canonical: pl.canonical,
6467
+ hasProjectEntry: hasProjectEntryFor(pl.canonical, cwd)
6468
+ })), ...inventory.skills.filter((s) => Boolean(s.ref)).map((s) => ({
6469
+ kind: "skill",
6470
+ name: s.name,
6471
+ ref: s.ref,
6472
+ canonical: s.canonical,
6473
+ hasProjectEntry: hasProjectEntryFor(s.canonical, cwd)
6474
+ }))];
6475
+ if (options.names && options.names.length > 0) {
6476
+ const wanted = new Set(options.names.map((n) => n.toLowerCase()));
6477
+ targets = targets.filter((t) => wanted.has(t.name.toLowerCase()));
6452
6478
  }
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;
6479
+ if (options.json) {
6480
+ const out = { targets: targets.map((t) => ({
6481
+ kind: t.kind,
6482
+ name: t.name,
6483
+ ref: t.ref
6484
+ })) };
6485
+ if (targets.length === 0) {
6486
+ console.log(JSON.stringify(out, null, 2));
6487
+ return;
6470
6488
  }
6471
- checkable.push({
6472
- name: skillName,
6473
- entry
6474
- });
6475
6489
  }
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
- }
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
- };
6490
+ if (targets.length === 0) {
6491
+ if (options.names && options.names.length > 0) console.log(`${DIM}No installed skills/plugins matching: ${options.names.join(", ")}${RESET}`);
6492
+ else {
6493
+ console.log(`${DIM}Nothing to update — store is empty.${RESET}`);
6494
+ console.log(`${DIM}Install something with${RESET} ${TEXT}npx skills add <ref>${RESET}`);
6495
+ }
6496
+ return;
6497
6497
  }
6498
- if (checkable.length === 0 && skipped.length > 0) {
6499
- printSkippedSkills(skipped);
6500
- return {
6501
- successCount,
6502
- failCount,
6503
- checkedCount
6504
- };
6498
+ if (!options.yes && !options.json) {
6499
+ console.log(`${TEXT}Updating ${targets.length} entr${targets.length === 1 ? "y" : "ies"}...${RESET}`);
6500
+ console.log();
6505
6501
  }
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
- };
6502
+ const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
6503
+ if (!existsSync(cliEntry)) {
6504
+ console.error(`${BOLD}✗${RESET} CLI entrypoint not found at ${cliEntry}`);
6505
+ process.exitCode = 1;
6506
+ return;
6513
6507
  }
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,
6508
+ let success = 0;
6509
+ let fail = 0;
6510
+ const results = [];
6511
+ for (const t of targets) {
6512
+ const safeName = sanitizeMetadata(t.name);
6513
+ if (!options.json) console.log(`${TEXT}Updating ${safeName}...${RESET} ${DIM}(${t.ref})${RESET}`);
6514
+ const passthrough = [
6528
6515
  "add",
6529
- installUrl,
6530
- "-g",
6516
+ t.ref,
6531
6517
  "-y"
6532
- ], {
6533
- stdio: [
6534
- "inherit",
6518
+ ];
6519
+ if (!t.hasProjectEntry) passthrough.push("-g");
6520
+ const ok = spawnSync(process.execPath, [cliEntry, ...passthrough], {
6521
+ stdio: options.json ? [
6522
+ "ignore",
6535
6523
  "pipe",
6536
6524
  "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: [
6525
+ ] : [
6602
6526
  "inherit",
6603
6527
  "pipe",
6604
6528
  "pipe"
6605
6529
  ],
6606
6530
  encoding: "utf-8",
6607
6531
  shell: process.platform === "win32"
6608
- }).status === 0) {
6609
- successCount++;
6610
- console.log(` ${TEXT}✓${RESET} Updated ${safeName}`);
6532
+ }).status === 0;
6533
+ if (ok) {
6534
+ success++;
6535
+ if (!options.json) console.log(` ${TEXT}✓${RESET} updated ${safeName}`);
6611
6536
  } else {
6612
- failCount++;
6613
- console.log(` ${DIM}✗ Failed to update ${safeName}${RESET}`);
6537
+ fail++;
6538
+ if (!options.json) console.log(` ${DIM}✗ failed ${safeName}${RESET}`);
6614
6539
  }
6540
+ results.push({
6541
+ name: t.name,
6542
+ kind: t.kind,
6543
+ ok
6544
+ });
6615
6545
  }
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}`);
6546
+ if (options.json) {
6547
+ console.log(JSON.stringify({
6548
+ targets: targets.map((t) => ({
6549
+ kind: t.kind,
6550
+ name: t.name,
6551
+ ref: t.ref
6552
+ })),
6553
+ results,
6554
+ success,
6555
+ fail
6556
+ }, null, 2));
6557
+ return;
6631
6558
  }
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
6559
  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
- 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
- });
6560
+ if (success > 0) console.log(`${TEXT}✓ Updated ${success} entr${success === 1 ? "y" : "ies"}${RESET}`);
6561
+ if (fail > 0) console.log(`${DIM}Failed to update ${fail} entr${fail === 1 ? "y" : "ies"}${RESET}`);
6669
6562
  console.log();
6670
6563
  }
6671
6564
  async function main() {