shoplazza-ai-dev-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +1920 -353
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -16,13 +16,13 @@ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep
16
16
  import { homedir, platform, tmpdir } from "os";
17
17
  import { URL as URL$1, fileURLToPath } from "url";
18
18
  import { stripVTControlCharacters } from "node:util";
19
+ import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
19
20
  import { gunzipSync } from "node:zlib";
20
21
  import { mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
21
22
  import { dirname as dirname$1, normalize as normalize$1, resolve as resolve$1, sep as sep$1 } from "node:path";
23
+ import { parse } from "yaml";
22
24
  import * as readline from "readline";
23
25
  import { Writable } from "stream";
24
- import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
25
- import { parse } from "yaml";
26
26
  import { createHash } from "crypto";
27
27
  import { createServer } from "http";
28
28
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
@@ -284,6 +284,95 @@ function isWellKnownUrl(input) {
284
284
  return false;
285
285
  }
286
286
  }
287
+ const FORGE_STORE_DIRNAME = ".ai-dev-cli";
288
+ const home = homedir();
289
+ const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
290
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
291
+ const cursorHome = join(home, ".cursor");
292
+ function getAgentHome(type) {
293
+ switch (type) {
294
+ case "claude-code": return claudeHome;
295
+ case "codex": return codexHome;
296
+ case "cursor": return cursorHome;
297
+ }
298
+ }
299
+ const agents = {
300
+ "claude-code": {
301
+ name: "claude-code",
302
+ displayName: "Claude Code",
303
+ skillsDir: ".claude/skills",
304
+ globalSkillsDir: join(claudeHome, "skills"),
305
+ detectInstalled: async () => {
306
+ return existsSync(claudeHome);
307
+ }
308
+ },
309
+ codex: {
310
+ name: "codex",
311
+ displayName: "Codex",
312
+ skillsDir: ".agents/skills",
313
+ globalSkillsDir: join(codexHome, "skills"),
314
+ detectInstalled: async () => {
315
+ return existsSync(codexHome) || existsSync("/etc/codex");
316
+ }
317
+ },
318
+ cursor: {
319
+ name: "cursor",
320
+ displayName: "Cursor",
321
+ skillsDir: ".agents/skills",
322
+ globalSkillsDir: join(home, ".cursor/skills"),
323
+ detectInstalled: async () => {
324
+ return existsSync(join(home, ".cursor"));
325
+ }
326
+ }
327
+ };
328
+ async function detectInstalledAgents() {
329
+ return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
330
+ type,
331
+ installed: await config.detectInstalled()
332
+ })))).filter((r) => r.installed).map((r) => r.type);
333
+ }
334
+ function getUniversalAgents() {
335
+ return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
336
+ }
337
+ function isUniversalAgent(type) {
338
+ return agents[type].skillsDir === ".agents/skills";
339
+ }
340
+ function getStoreRoot(_scope, _cwd) {
341
+ return join(home, FORGE_STORE_DIRNAME, "store");
342
+ }
343
+ function getAgentEntryRoot(agent, scope, cwd) {
344
+ if (scope === "global") return getAgentHome(agent);
345
+ return cwd ?? process.cwd();
346
+ }
347
+ function getInstallTargets(agent, component, scope, cwd) {
348
+ const entryRoot = getAgentEntryRoot(agent, scope, cwd);
349
+ switch (component.kind) {
350
+ case "plugin-skill": return {
351
+ canonical: join(component.pluginCanonical, "skills", component.skillName),
352
+ entry: agent === "claude-code" ? join(entryRoot, ".claude", "skills", component.skillName) : agent === "cursor" ? join(entryRoot, ".cursor", "skills", component.skillName) : join(entryRoot, ".agents", "skills", component.skillName)
353
+ };
354
+ case "plugin-agent-manifest": return {
355
+ canonical: join(component.pluginCanonical, "agents", component.filename),
356
+ entry: agent === "claude-code" ? join(entryRoot, ".claude", "agents", component.filename) : agent === "cursor" ? join(entryRoot, ".cursor", "agents", component.filename) : join(entryRoot, ".codex", "agents", component.filename)
357
+ };
358
+ case "plugin-agent-deps-dir": return {
359
+ canonical: join(component.pluginCanonical, "agents", component.dirname),
360
+ entry: agent === "claude-code" ? join(entryRoot, ".claude", "agents", component.dirname) : agent === "cursor" ? join(entryRoot, ".cursor", "agents", component.dirname) : join(entryRoot, ".codex", "agents", component.dirname)
361
+ };
362
+ case "plugin-rule-manifest": {
363
+ const canonical = join(component.pluginCanonical, "rules", component.filename);
364
+ const cursorName = component.filename.endsWith(".md") ? component.filename.slice(0, -3) + ".mdc" : component.filename;
365
+ return {
366
+ canonical,
367
+ entry: agent === "claude-code" ? join(entryRoot, ".claude", "rules", component.filename) : agent === "cursor" ? join(entryRoot, ".cursor", "rules", cursorName) : ""
368
+ };
369
+ }
370
+ case "plugin-rule-deps-dir": return {
371
+ canonical: join(component.pluginCanonical, "rules", component.dirname),
372
+ entry: agent === "claude-code" ? join(entryRoot, ".claude", "rules", component.dirname) : agent === "cursor" ? join(entryRoot, ".cursor", "rules", component.dirname) : ""
373
+ };
374
+ }
375
+ }
287
376
  const CONFIG_DIR = join(homedir(), ".ai-dev-cli");
288
377
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
289
378
  const DEFAULT_PORTAL = "https://forge.shoplazza.site";
@@ -401,6 +490,268 @@ function configCommand(args) {
401
490
  console.error("Available: get | set | unset | list | path");
402
491
  process.exit(2);
403
492
  }
493
+ const FORGE_HOOK_BASENAME$1 = "track-skill-read.mjs";
494
+ async function upsertCursorHooks(hooksJsonPath, scriptPath) {
495
+ let raw = "";
496
+ try {
497
+ raw = await readFile(hooksJsonPath, "utf-8");
498
+ } catch {}
499
+ const cfg = raw.trim() ? JSON.parse(raw) : { version: 1 };
500
+ if (!cfg.hooks) cfg.hooks = {};
501
+ if (!Array.isArray(cfg.hooks.postToolUse)) cfg.hooks.postToolUse = [];
502
+ let readMatcher = cfg.hooks.postToolUse.find((m) => m.matcher === "Read");
503
+ if (!readMatcher) {
504
+ readMatcher = {
505
+ matcher: "Read",
506
+ hooks: []
507
+ };
508
+ cfg.hooks.postToolUse.push(readMatcher);
509
+ }
510
+ if (!Array.isArray(readMatcher.hooks)) readMatcher.hooks = [];
511
+ const command = `node ${shellEscape$2(scriptPath)}`;
512
+ const ourEntry = readMatcher.hooks.find((h) => h.command.includes(FORGE_HOOK_BASENAME$1));
513
+ if (ourEntry) {
514
+ if (ourEntry.command !== command) ourEntry.command = command;
515
+ } else readMatcher.hooks.push({
516
+ type: "command",
517
+ command
518
+ });
519
+ await mkdir(dirname(hooksJsonPath), { recursive: true });
520
+ await writeFile(hooksJsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
521
+ }
522
+ function shellEscape$2(p) {
523
+ if (!/[\s"'$`\\]/.test(p)) return p;
524
+ return `'${p.replace(/'/g, `'\\''`)}'`;
525
+ }
526
+ function upsertCodexAgent(toml, entry) {
527
+ const header = `[agents.${entry.id}]`;
528
+ return upsertSection(toml, header, `${header}\nconfig_file = ${jsonString(entry.configFile)}\n`);
529
+ }
530
+ function removeCodexAgent(toml, id) {
531
+ return removeSection(toml, `[agents.${id}]`);
532
+ }
533
+ function ensureCodexHooksFeature(toml) {
534
+ const lines = toml.split("\n");
535
+ let inFeatures = false;
536
+ let hasFlag = false;
537
+ let featuresLineIdx = -1;
538
+ for (const [i, line] of lines.entries()) {
539
+ const trimmed = line.trim();
540
+ if (trimmed === "[features]") {
541
+ inFeatures = true;
542
+ featuresLineIdx = i;
543
+ continue;
544
+ }
545
+ if (trimmed.startsWith("[") && trimmed.endsWith("]") && trimmed !== "[features]") {
546
+ inFeatures = false;
547
+ continue;
548
+ }
549
+ if (inFeatures && /^codex_hooks\s*=/.test(trimmed)) {
550
+ hasFlag = true;
551
+ lines[i] = "codex_hooks = true";
552
+ }
553
+ }
554
+ if (featuresLineIdx === -1) return toml + (toml.length > 0 && !toml.endsWith("\n") ? "\n" : "") + "\n[features]\ncodex_hooks = true\n";
555
+ if (!hasFlag) lines.splice(featuresLineIdx + 1, 0, "codex_hooks = true");
556
+ return lines.join("\n");
557
+ }
558
+ function upsertSection(toml, header, newBlock) {
559
+ const idx = toml.indexOf(header);
560
+ if (idx < 0) return toml + (toml.length > 0 && !toml.endsWith("\n") ? "\n" : "") + "\n" + newBlock;
561
+ const after = toml.indexOf("\n[", idx + header.length);
562
+ const blockEnd = after < 0 ? toml.length : after + 1;
563
+ return toml.slice(0, idx) + newBlock + toml.slice(blockEnd);
564
+ }
565
+ function removeSection(toml, header) {
566
+ const idx = toml.indexOf(header);
567
+ if (idx < 0) return toml;
568
+ const after = toml.indexOf("\n[", idx + header.length);
569
+ const blockEnd = after < 0 ? toml.length : after + 1;
570
+ let start = idx;
571
+ while (start > 0 && toml[start - 1] === "\n" && (start - 2 < 0 || toml[start - 2] === "\n")) start--;
572
+ return toml.slice(0, start) + toml.slice(blockEnd);
573
+ }
574
+ function jsonString(s) {
575
+ return JSON.stringify(s);
576
+ }
577
+ const FORGE_HOOK_BASENAME = "track-skill-read.mjs";
578
+ async function upsertCodexHooks(hooksJsonPath, configTomlPath, scriptPath) {
579
+ let raw = "";
580
+ try {
581
+ raw = await readFile(hooksJsonPath, "utf-8");
582
+ } catch {}
583
+ const cfg = raw.trim() ? JSON.parse(raw) : {};
584
+ if (!Array.isArray(cfg.UserPromptSubmit)) cfg.UserPromptSubmit = [];
585
+ let m = cfg.UserPromptSubmit.find((x) => x.matcher === ".*");
586
+ if (!m) {
587
+ m = {
588
+ matcher: ".*",
589
+ hooks: []
590
+ };
591
+ cfg.UserPromptSubmit.push(m);
592
+ }
593
+ if (!Array.isArray(m.hooks)) m.hooks = [];
594
+ const command = `node ${shellEscape$1(scriptPath)} --codex-mode`;
595
+ const our = m.hooks.find((h) => h.command.includes(FORGE_HOOK_BASENAME));
596
+ if (our) {
597
+ if (our.command !== command) our.command = command;
598
+ } else m.hooks.push({
599
+ type: "command",
600
+ command
601
+ });
602
+ await mkdir(dirname(hooksJsonPath), { recursive: true });
603
+ await writeFile(hooksJsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
604
+ let configRaw = "";
605
+ try {
606
+ configRaw = await readFile(configTomlPath, "utf-8");
607
+ } catch {}
608
+ const updated = ensureCodexHooksFeature(configRaw);
609
+ if (updated !== configRaw) {
610
+ await mkdir(dirname(configTomlPath), { recursive: true });
611
+ await writeFile(configTomlPath, updated, "utf-8");
612
+ }
613
+ }
614
+ function shellEscape$1(p) {
615
+ if (!/[\s"'$`\\]/.test(p)) return p;
616
+ return `'${p.replace(/'/g, `'\\''`)}'`;
617
+ }
618
+ const HOOK_SCRIPT_BASENAME = "track-skill-read.mjs";
619
+ const HOOK_DIRNAME = "forge-hooks";
620
+ const FORGE_HOOK_TAG = "forge:track-skill-read";
621
+ async function writeForgeSourceManifest(skillDir, manifest) {
622
+ const target = join(skillDir, ".forge-source.json");
623
+ await mkdir(skillDir, { recursive: true });
624
+ await writeFile(target, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
625
+ }
626
+ function getForgeHookScript(portalUrl) {
627
+ const portal = portalUrl.replace(/\/$/, "");
628
+ return `#!/usr/bin/env node
629
+ // ${FORGE_HOOK_TAG}
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).
633
+ // Anonymous: /events accepts unauthenticated posts.
634
+ import fs from 'node:fs';
635
+ import path from 'node:path';
636
+
637
+ const PORTAL = ${JSON.stringify(portal)};
638
+
639
+ setTimeout(() => process.exit(0), 5000);
640
+
641
+ let buf = '';
642
+ process.stdin.on('data', (c) => (buf += c));
643
+ process.stdin.on('end', () => {
644
+ try {
645
+ 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);
658
+ }
659
+ if (!manifest?.ref) return;
660
+
661
+ fetch(PORTAL + '/events', {
662
+ method: 'POST',
663
+ headers: { 'Content-Type': 'application/json' },
664
+ body: JSON.stringify({
665
+ event_type: 'skill_invoked',
666
+ asset_ref: manifest.ref,
667
+ install_mode: manifest.install_mode || 'claude-code',
668
+ metadata: {
669
+ parent_plugin_ref: manifest.parent_plugin_ref || null,
670
+ },
671
+ }),
672
+ signal: AbortSignal.timeout(2000),
673
+ }).catch(() => {});
674
+ } catch {
675
+ /* swallow */
676
+ }
677
+ });
678
+ `;
679
+ }
680
+ async function ensureForgeHooks(agent) {
681
+ const warnings = [];
682
+ try {
683
+ const portal = loadConfig().portal || DEFAULT_PORTAL;
684
+ const home = getAgentHome(agent);
685
+ const scriptPath = join(home, HOOK_DIRNAME, HOOK_SCRIPT_BASENAME);
686
+ await writeHookScript(scriptPath, portal);
687
+ if (agent === "claude-code") await upsertClaudeCodeSettings(join(home, "settings.json"), scriptPath);
688
+ else if (agent === "cursor") await upsertCursorHooks(join(home, "hooks.json"), scriptPath);
689
+ else if (agent === "codex") await upsertCodexHooks(join(home, "hooks.json"), join(home, "config.toml"), scriptPath);
690
+ } catch (err) {
691
+ warnings.push(`Failed to install forge hooks for ${agent}: ${err instanceof Error ? err.message : String(err)}`);
692
+ }
693
+ return { warnings };
694
+ }
695
+ async function writeHookScript(scriptPath, portalUrl) {
696
+ const next = getForgeHookScript(portalUrl);
697
+ let prev = null;
698
+ if (existsSync(scriptPath)) try {
699
+ prev = await readFile(scriptPath, "utf-8");
700
+ } catch {
701
+ prev = null;
702
+ }
703
+ if (prev === next) return;
704
+ await mkdir(dirname(scriptPath), { recursive: true });
705
+ await writeFile(scriptPath, next, {
706
+ encoding: "utf-8",
707
+ mode: 493
708
+ });
709
+ try {
710
+ await chmod(scriptPath, 493);
711
+ } catch {}
712
+ }
713
+ async function upsertClaudeCodeSettings(settingsPath, scriptPath) {
714
+ const command = `node ${shellEscape(scriptPath)}`;
715
+ let raw = "";
716
+ try {
717
+ raw = await readFile(settingsPath, "utf-8");
718
+ } catch {
719
+ raw = "";
720
+ }
721
+ let settings = {};
722
+ if (raw.trim().length > 0) try {
723
+ settings = JSON.parse(raw);
724
+ } catch (err) {
725
+ throw new Error(`Refusing to overwrite invalid JSON at ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
726
+ }
727
+ if (!settings.hooks || typeof settings.hooks !== "object") settings.hooks = {};
728
+ if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
729
+ const matcherList = settings.hooks.PostToolUse;
730
+ let readMatcher = matcherList.find((m) => m && m.matcher === "Read");
731
+ if (!readMatcher) {
732
+ readMatcher = {
733
+ matcher: "Read",
734
+ hooks: []
735
+ };
736
+ matcherList.push(readMatcher);
737
+ }
738
+ if (!Array.isArray(readMatcher.hooks)) readMatcher.hooks = [];
739
+ const ourEntry = readMatcher.hooks.find((h) => typeof h?.command === "string" && h.command.includes(HOOK_SCRIPT_BASENAME));
740
+ if (ourEntry) {
741
+ if (ourEntry.command === command && ourEntry.type === "command") return;
742
+ ourEntry.type = "command";
743
+ ourEntry.command = command;
744
+ } else readMatcher.hooks.push({
745
+ type: "command",
746
+ command
747
+ });
748
+ await mkdir(dirname(settingsPath), { recursive: true });
749
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
750
+ }
751
+ function shellEscape(p) {
752
+ if (!/[\s"'$`\\]/.test(p)) return p;
753
+ return `'${p.replace(/'/g, `'\\''`)}'`;
754
+ }
404
755
  async function fetchSkillArchive(ref, portalUrl, bearerToken) {
405
756
  const url = `${portalUrl.replace(/\/$/, "")}/api/cli/skills/archive?ref=${encodeURIComponent(ref)}`;
406
757
  const headers = { Accept: "application/gzip" };
@@ -421,6 +772,48 @@ async function fetchSkillArchive(ref, portalUrl, bearerToken) {
421
772
  }
422
773
  return Buffer.from(await resp.arrayBuffer());
423
774
  }
775
+ 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");
789
+ if (resp.status === 403) throw new Error(`Not a member of team "${teamScope}"`);
790
+ if (resp.status === 404) throw new Error(`Team not found: ${teamScope}`);
791
+ if (!resp.ok) {
792
+ const body = await resp.text().catch(() => "");
793
+ throw new Error(`Portal returned ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`);
794
+ }
795
+ return (await resp.json()).items ?? [];
796
+ }
797
+ 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");
809
+ if (resp.status === 403) throw new Error(`Permission denied for ${ref}`);
810
+ if (resp.status === 404) throw new Error(`Plugin not found: ${ref}`);
811
+ if (!resp.ok) {
812
+ const body = await resp.text().catch(() => "");
813
+ throw new Error(`Portal returned ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`);
814
+ }
815
+ return Buffer.from(await resp.arrayBuffer());
816
+ }
424
817
  function extractTarGz(gz, dest) {
425
818
  const tar = gunzipSync(gz);
426
819
  const destAbs = resolve$1(dest);
@@ -456,6 +849,117 @@ function readCStr(buf, start, len) {
456
849
  while (end < slice.length && slice[end] !== 0) end++;
457
850
  return slice.subarray(0, end).toString("utf-8");
458
851
  }
852
+ const FORGE_SIDECARS = [".forge-source.json", ".forge-plugin.json"];
853
+ async function assertEntryReplaceable(entryPath, ctx) {
854
+ let st;
855
+ try {
856
+ st = await lstat(entryPath);
857
+ } catch (e) {
858
+ if (e.code === "ENOENT") return { kind: "absent" };
859
+ throw e;
860
+ }
861
+ if (st.isSymbolicLink()) {
862
+ const target = await readlink(entryPath);
863
+ const absTarget = isAbsolute(target) ? target : resolve(dirname(entryPath), target);
864
+ if (!pathInside(ctx.storeRoot, absTarget)) return {
865
+ kind: "foreign-symlink",
866
+ realPath: absTarget
867
+ };
868
+ const ref = await readForgeRef(absTarget);
869
+ if (ref === ctx.intendedRef || pathsEqual(absTarget, ctx.intendedCanonical)) return { kind: "forge-self" };
870
+ return {
871
+ kind: "forge-other",
872
+ otherRef: ref ?? "<unknown>"
873
+ };
874
+ }
875
+ if (st.isDirectory()) {
876
+ const ref = await readForgeRef(entryPath);
877
+ if (ref === ctx.intendedRef) return { kind: "forge-self" };
878
+ if (ref) return {
879
+ kind: "forge-other",
880
+ otherRef: ref
881
+ };
882
+ return {
883
+ kind: "foreign-dir",
884
+ realPath: entryPath
885
+ };
886
+ }
887
+ return {
888
+ kind: "foreign-file",
889
+ realPath: entryPath
890
+ };
891
+ }
892
+ function pathInside(root, p) {
893
+ const r = relative(resolve(root), resolve(p));
894
+ return r === "" || !r.startsWith("..") && !isAbsolute(r);
895
+ }
896
+ function pathsEqual(a, b) {
897
+ return resolve(a) === resolve(b);
898
+ }
899
+ async function readForgeRef(p) {
900
+ for (const name of FORGE_SIDECARS) try {
901
+ const buf = await readFile(join(p, name), "utf-8");
902
+ const json = JSON.parse(buf);
903
+ if (typeof json.ref === "string") return json.ref;
904
+ } catch {}
905
+ const parent = dirname(p);
906
+ if (parent === p) return null;
907
+ for (const name of FORGE_SIDECARS) try {
908
+ const buf = await readFile(join(parent, name), "utf-8");
909
+ const json = JSON.parse(buf);
910
+ if (typeof json.ref === "string") return json.ref;
911
+ } catch {}
912
+ return null;
913
+ }
914
+ async function promptForeignOverwrite(conflicts, opts = {}) {
915
+ if (conflicts.length === 0) return true;
916
+ if (opts.yes || opts.force) return true;
917
+ if (!process.stdin.isTTY) return false;
918
+ console.log();
919
+ console.log(import_picocolors.default.yellow(`⚠ Found ${conflicts.length} conflicts with user-owned content:`));
920
+ console.log();
921
+ for (const c of conflicts) console.log(` ${c.entryPath} (${c.kind})`);
922
+ console.log();
923
+ console.log("These are not managed by forge. Overwriting will replace your content.");
924
+ const choice = await ve({
925
+ message: "Force overwrite all?",
926
+ options: [
927
+ {
928
+ value: "no",
929
+ label: "no — abort install (default)"
930
+ },
931
+ {
932
+ value: "yes",
933
+ label: "yes — replace everything (existing content will be lost)"
934
+ },
935
+ {
936
+ value: "detail",
937
+ label: "show details (file sizes / dir trees)"
938
+ }
939
+ ],
940
+ initialValue: "no"
941
+ });
942
+ if (pD(choice)) return false;
943
+ if (choice === "detail") {
944
+ console.log();
945
+ for (const c of conflicts) console.log(` ${c.entryPath} → ${c.kind}`);
946
+ return promptForeignOverwrite(conflicts, opts);
947
+ }
948
+ return choice === "yes";
949
+ }
950
+ const AGENTS_DIR$2 = ".agents";
951
+ const SKILLS_SUBDIR = "skills";
952
+ function parseFrontmatter(raw) {
953
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
954
+ if (!match) return {
955
+ data: {},
956
+ content: raw
957
+ };
958
+ return {
959
+ data: parse(match[1]) ?? {},
960
+ content: match[2] ?? ""
961
+ };
962
+ }
459
963
  const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
460
964
  const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
461
965
  const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
@@ -468,298 +972,40 @@ function stripTerminalEscapes(str) {
468
972
  function sanitizeMetadata(str) {
469
973
  return stripTerminalEscapes(str).replace(/[\r\n]+/g, " ").trim();
470
974
  }
471
- const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
472
- callback();
473
- } });
474
- const S_STEP_ACTIVE = import_picocolors.default.green("◆");
475
- const S_STEP_CANCEL = import_picocolors.default.red("■");
476
- const S_STEP_SUBMIT = import_picocolors.default.green("◇");
477
- const S_RADIO_ACTIVE = import_picocolors.default.green("●");
478
- const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
479
- import_picocolors.default.green("✓");
480
- const S_BULLET = import_picocolors.default.green("•");
481
- const S_BAR = import_picocolors.default.dim("│");
482
- const S_BAR_H = import_picocolors.default.dim("─");
483
- const cancelSymbol = Symbol("cancel");
484
- function approxStringWidth(plain) {
485
- let width = 0;
486
- for (const ch of plain) {
487
- const code = ch.codePointAt(0);
488
- if (code === 0) continue;
489
- width += code >= 4352 && code <= 4447 || code >= 8986 && code <= 8987 || code >= 9001 && code <= 9002 || code >= 9193 && code <= 9196 || code === 9200 || code === 9203 || code >= 9725 && code <= 9726 || code >= 9748 && code <= 9749 || code >= 9800 && code <= 9811 || code >= 9855 && code <= 9855 || code >= 9875 && code <= 9875 || code >= 9889 && code <= 9889 || code >= 9898 && code <= 9899 || code >= 9917 && code <= 9918 || code >= 9924 && code <= 9925 || code >= 9934 && code <= 9934 || code >= 9940 && code <= 9940 || code >= 9962 && code <= 9962 || code >= 9970 && code <= 9971 || code >= 9973 && code <= 9973 || code >= 9978 && code <= 9978 || code >= 9981 && code <= 9981 || code >= 9989 && code <= 9989 || code >= 9994 && code <= 9995 || code >= 10024 && code <= 10024 || code >= 10060 && code <= 10060 || code >= 10062 && code <= 10062 || code >= 10067 && code <= 10069 || code >= 10071 && code <= 10071 || code >= 10133 && code <= 10135 || code >= 10160 && code <= 10160 || code >= 10175 && code <= 10175 || code >= 11035 && code <= 11036 || code >= 11088 && code <= 11088 || code >= 11093 && code <= 11093 || code >= 11904 && code <= 42191 && code !== 12351 || code >= 43360 && code <= 43388 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 126976 && code <= 129535 ? 2 : 1;
490
- }
491
- return width;
975
+ function isContainedIn(targetPath, basePath) {
976
+ const normalizedBase = normalize(resolve(basePath));
977
+ const normalizedTarget = normalize(resolve(targetPath));
978
+ return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
492
979
  }
493
- function visualRowsForLine(line, columns) {
494
- const plain = stripVTControlCharacters(line);
495
- const cols = Math.max(1, columns);
496
- const w = approxStringWidth(plain);
497
- return Math.max(1, Math.ceil(w / cols));
980
+ function isValidRelativePath(path) {
981
+ return path.startsWith("./");
498
982
  }
499
- function countVisualRowsForLines(lines, columns) {
500
- const cols = columns !== void 0 && columns > 0 ? columns : process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
501
- return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0);
502
- }
503
- async function searchMultiselect(options) {
504
- const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection } = options;
505
- return new Promise((resolve) => {
506
- const rl = readline.createInterface({
507
- input: process.stdin,
508
- output: silentOutput,
509
- terminal: false
510
- });
511
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
512
- readline.emitKeypressEvents(process.stdin, rl);
513
- let query = "";
514
- let cursor = 0;
515
- const selected = new Set(initialSelected);
516
- let lastRenderHeight = 0;
517
- const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
518
- const filter = (item, q) => {
519
- if (!q) return true;
520
- const lowerQ = q.toLowerCase();
521
- return item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ);
522
- };
523
- const getFiltered = () => {
524
- return items.filter((item) => filter(item, query));
525
- };
526
- const clearRender = () => {
527
- if (lastRenderHeight > 0) {
528
- process.stdout.write(`\x1b[${lastRenderHeight}A`);
529
- for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B");
530
- process.stdout.write(`\x1b[${lastRenderHeight}A`);
531
- }
532
- };
533
- const render = (state = "active") => {
534
- clearRender();
535
- const lines = [];
536
- const filtered = getFiltered();
537
- const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
538
- lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
539
- if (state === "active") {
540
- if (lockedSection && lockedSection.items.length > 0) {
541
- lines.push(`${S_BAR}`);
542
- const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
543
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
544
- for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
545
- lines.push(`${S_BAR}`);
546
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
547
- }
548
- const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
549
- lines.push(searchLine);
550
- lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
551
- lines.push(`${S_BAR}`);
552
- const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
553
- const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
554
- const visibleItems = filtered.slice(visibleStart, visibleEnd);
555
- if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
556
- else {
557
- for (let i = 0; i < visibleItems.length; i++) {
558
- const item = visibleItems[i];
559
- const actualIndex = visibleStart + i;
560
- const isSelected = selected.has(item.value);
561
- const isCursor = actualIndex === cursor;
562
- const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
563
- const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
564
- const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
565
- const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
566
- lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
567
- }
568
- const hiddenBefore = visibleStart;
569
- const hiddenAfter = filtered.length - visibleEnd;
570
- if (hiddenBefore > 0 || hiddenAfter > 0) {
571
- const parts = [];
572
- if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
573
- if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
574
- lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
575
- }
576
- }
577
- lines.push(`${S_BAR}`);
578
- const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
579
- if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
580
- else {
581
- const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
582
- lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
583
- }
584
- lines.push(`${import_picocolors.default.dim("└")}`);
585
- } else if (state === "submit") {
586
- const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
587
- lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
588
- } else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
589
- process.stdout.write(lines.join("\n") + "\n");
590
- lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns);
591
- };
592
- const cleanup = () => {
593
- process.stdin.removeListener("keypress", keypressHandler);
594
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
595
- rl.close();
596
- };
597
- const submit = () => {
598
- if (required && selected.size === 0 && lockedValues.length === 0) return;
599
- render("submit");
600
- cleanup();
601
- resolve([...lockedValues, ...Array.from(selected)]);
602
- };
603
- const cancel = () => {
604
- render("cancel");
605
- cleanup();
606
- resolve(cancelSymbol);
607
- };
608
- const keypressHandler = (_str, key) => {
609
- if (!key) return;
610
- const filtered = getFiltered();
611
- if (key.name === "return") {
612
- submit();
613
- return;
614
- }
615
- if (key.name === "escape" || key.ctrl && key.name === "c") {
616
- cancel();
617
- return;
618
- }
619
- if (key.name === "up") {
620
- cursor = Math.max(0, cursor - 1);
621
- render();
622
- return;
623
- }
624
- if (key.name === "down") {
625
- cursor = Math.min(filtered.length - 1, cursor + 1);
626
- render();
627
- return;
628
- }
629
- if (key.name === "space") {
630
- const item = filtered[cursor];
631
- if (item) if (selected.has(item.value)) selected.delete(item.value);
632
- else selected.add(item.value);
633
- render();
634
- return;
635
- }
636
- if (key.name === "backspace") {
637
- query = query.slice(0, -1);
638
- cursor = 0;
639
- render();
640
- return;
641
- }
642
- if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
643
- query += key.sequence;
644
- cursor = 0;
645
- render();
646
- return;
647
- }
648
- };
649
- process.stdin.on("keypress", keypressHandler);
650
- render();
651
- });
652
- }
653
- const DEFAULT_CLONE_TIMEOUT_MS = 3e5;
654
- const CLONE_TIMEOUT_MS = (() => {
655
- const raw = process.env.SKILLS_CLONE_TIMEOUT_MS;
656
- if (!raw) return DEFAULT_CLONE_TIMEOUT_MS;
657
- const parsed = Number.parseInt(raw, 10);
658
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
659
- })();
660
- var GitCloneError = class extends Error {
661
- url;
662
- isTimeout;
663
- isAuthError;
664
- constructor(message, url, isTimeout = false, isAuthError = false) {
665
- super(message);
666
- this.name = "GitCloneError";
667
- this.url = url;
668
- this.isTimeout = isTimeout;
669
- this.isAuthError = isAuthError;
670
- }
671
- };
672
- async function cloneRepo(url, ref) {
673
- const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
674
- const git = esm_default({
675
- timeout: { block: CLONE_TIMEOUT_MS },
676
- config: [
677
- "filter.lfs.required=false",
678
- "filter.lfs.smudge=",
679
- "filter.lfs.clean=",
680
- "filter.lfs.process="
681
- ]
682
- }).env({
683
- ...process.env,
684
- GIT_TERMINAL_PROMPT: "0",
685
- GIT_LFS_SKIP_SMUDGE: "1"
686
- });
687
- const cloneOptions = ref ? [
688
- "--depth",
689
- "1",
690
- "--branch",
691
- ref
692
- ] : ["--depth", "1"];
693
- try {
694
- await git.clone(url, tempDir, cloneOptions);
695
- return tempDir;
696
- } catch (error) {
697
- await rm(tempDir, {
698
- recursive: true,
699
- force: true
700
- }).catch(() => {});
701
- const errorMessage = error instanceof Error ? error.message : String(error);
702
- const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
703
- const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
704
- if (isTimeout) throw new GitCloneError(`Clone timed out after ${Math.round(CLONE_TIMEOUT_MS / 1e3)}s. Common causes:\n - Large repository: raise the timeout with SKILLS_CLONE_TIMEOUT_MS=600000 (10m)\n - Slow network: retry, or clone manually and pass the local path to 'skills add'\n - Private repo without credentials: ensure auth is configured\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)`, url, true, false);
705
- if (isAuthError) throw new GitCloneError(`Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`, url, false, true);
706
- throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
707
- }
708
- }
709
- async function cleanupTempDir(dir) {
710
- const normalizedDir = normalize(resolve(dir));
711
- const normalizedTmpDir = normalize(resolve(tmpdir()));
712
- if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
713
- await rm(dir, {
714
- recursive: true,
715
- force: true
716
- });
717
- }
718
- function parseFrontmatter(raw) {
719
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
720
- if (!match) return {
721
- data: {},
722
- content: raw
723
- };
724
- return {
725
- data: parse(match[1]) ?? {},
726
- content: match[2] ?? ""
727
- };
728
- }
729
- function isContainedIn(targetPath, basePath) {
730
- const normalizedBase = normalize(resolve(basePath));
731
- const normalizedTarget = normalize(resolve(targetPath));
732
- return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
733
- }
734
- function isValidRelativePath(path) {
735
- return path.startsWith("./");
736
- }
737
- async function getPluginSkillPaths(basePath) {
738
- const searchDirs = [];
739
- const addPluginSkillPaths = (pluginBase, skills) => {
740
- if (!isContainedIn(pluginBase, basePath)) return;
741
- if (skills && skills.length > 0) for (const skillPath of skills) {
742
- if (!isValidRelativePath(skillPath)) continue;
743
- const skillDir = dirname(join(pluginBase, skillPath));
744
- if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir);
745
- }
746
- searchDirs.push(join(pluginBase, "skills"));
747
- };
748
- try {
749
- const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
750
- const manifest = JSON.parse(content);
751
- const pluginRoot = manifest.metadata?.pluginRoot;
752
- if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
753
- if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
754
- if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
755
- addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills);
756
- }
757
- } catch {}
758
- try {
759
- const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
760
- addPluginSkillPaths(basePath, JSON.parse(content).skills);
761
- } catch {}
762
- return searchDirs;
983
+ async function getPluginSkillPaths(basePath) {
984
+ const searchDirs = [];
985
+ const addPluginSkillPaths = (pluginBase, skills) => {
986
+ if (!isContainedIn(pluginBase, basePath)) return;
987
+ if (skills && skills.length > 0) for (const skillPath of skills) {
988
+ if (!isValidRelativePath(skillPath)) continue;
989
+ const skillDir = dirname(join(pluginBase, skillPath));
990
+ if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir);
991
+ }
992
+ searchDirs.push(join(pluginBase, "skills"));
993
+ };
994
+ try {
995
+ const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
996
+ const manifest = JSON.parse(content);
997
+ const pluginRoot = manifest.metadata?.pluginRoot;
998
+ if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
999
+ if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
1000
+ if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
1001
+ addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills);
1002
+ }
1003
+ } catch {}
1004
+ try {
1005
+ const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
1006
+ addPluginSkillPaths(basePath, JSON.parse(content).skills);
1007
+ } catch {}
1008
+ return searchDirs;
763
1009
  }
764
1010
  async function getPluginGroupings(basePath) {
765
1011
  const groupings = /* @__PURE__ */ new Map();
@@ -933,52 +1179,6 @@ function filterSkills(skills, inputNames) {
933
1179
  return normalizedInputs.some((input) => input === name || input === displayName);
934
1180
  });
935
1181
  }
936
- const home = homedir();
937
- const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
938
- const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
939
- const agents = {
940
- "claude-code": {
941
- name: "claude-code",
942
- displayName: "Claude Code",
943
- skillsDir: ".claude/skills",
944
- globalSkillsDir: join(claudeHome, "skills"),
945
- detectInstalled: async () => {
946
- return existsSync(claudeHome);
947
- }
948
- },
949
- codex: {
950
- name: "codex",
951
- displayName: "Codex",
952
- skillsDir: ".agents/skills",
953
- globalSkillsDir: join(codexHome, "skills"),
954
- detectInstalled: async () => {
955
- return existsSync(codexHome) || existsSync("/etc/codex");
956
- }
957
- },
958
- cursor: {
959
- name: "cursor",
960
- displayName: "Cursor",
961
- skillsDir: ".agents/skills",
962
- globalSkillsDir: join(home, ".cursor/skills"),
963
- detectInstalled: async () => {
964
- return existsSync(join(home, ".cursor"));
965
- }
966
- }
967
- };
968
- async function detectInstalledAgents() {
969
- return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
970
- type,
971
- installed: await config.detectInstalled()
972
- })))).filter((r) => r.installed).map((r) => r.type);
973
- }
974
- function getUniversalAgents() {
975
- return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
976
- }
977
- function isUniversalAgent(type) {
978
- return agents[type].skillsDir === ".agents/skills";
979
- }
980
- const AGENTS_DIR$2 = ".agents";
981
- const SKILLS_SUBDIR = "skills";
982
1182
  function sanitizeName(name) {
983
1183
  return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
984
1184
  }
@@ -1021,6 +1221,17 @@ async function cleanAndCreateDirectory(path) {
1021
1221
  } catch {}
1022
1222
  await mkdir(path, { recursive: true });
1023
1223
  }
1224
+ async function cleanCanonicalDir(canonicalPath, storeRoot) {
1225
+ const rel = relative(resolve(storeRoot), resolve(canonicalPath));
1226
+ if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`refusing to clean path outside forge store: ${canonicalPath}`);
1227
+ try {
1228
+ await rm(canonicalPath, {
1229
+ recursive: true,
1230
+ force: true
1231
+ });
1232
+ } catch {}
1233
+ await mkdir(canonicalPath, { recursive: true });
1234
+ }
1024
1235
  async function resolveParentSymlinks(path) {
1025
1236
  const resolved = resolve(path);
1026
1237
  const dir = dirname(resolved);
@@ -1056,7 +1267,104 @@ async function createSymlink(target, linkPath) {
1056
1267
  return false;
1057
1268
  }
1058
1269
  }
1059
- async function installSkillForAgent(skill, agentType, options = {}) {
1270
+ async function installSkillForAgent(skill, agentType, opts) {
1271
+ if (!("ref" in opts && "scopeNamespace" in opts)) return _installSkillForAgentLegacy(skill, agentType, opts);
1272
+ const newOpts = opts;
1273
+ const scope = newOpts.scope ?? (newOpts.global ? "global" : "project");
1274
+ const cwd = newOpts.cwd ?? process.cwd();
1275
+ const skillName = sanitizeName(skill.name || basename(skill.path));
1276
+ const canonicalKey = `${newOpts.scopeNamespace}__${skillName}`;
1277
+ const storeRoot = getStoreRoot(scope, cwd);
1278
+ const canonicalDir = join(storeRoot, "skills", canonicalKey);
1279
+ if (!isPathSafe(join(storeRoot, "skills"), canonicalDir)) return {
1280
+ success: false,
1281
+ path: "",
1282
+ mode: "symlink",
1283
+ error: "Invalid skill name: path traversal detected",
1284
+ forgeOtherReplacements: []
1285
+ };
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);
1288
+ try {
1289
+ const status = await assertEntryReplaceable(entry, {
1290
+ intendedRef: newOpts.ref,
1291
+ intendedCanonical: canonicalDir,
1292
+ storeRoot
1293
+ });
1294
+ const forgeOtherReplacements = [];
1295
+ const foreignConflicts = [];
1296
+ if (status.kind === "forge-other") forgeOtherReplacements.push({
1297
+ entryPath: entry,
1298
+ otherRef: status.otherRef
1299
+ });
1300
+ else if (status.kind === "foreign-file" || status.kind === "foreign-dir" || status.kind === "foreign-symlink") foreignConflicts.push({
1301
+ entryPath: entry,
1302
+ kind: status.kind
1303
+ });
1304
+ if (foreignConflicts.length > 0) {
1305
+ let proceed = newOpts.forceOverwrite ?? false;
1306
+ if (!proceed && newOpts.onConflictPrompt) proceed = await newOpts.onConflictPrompt(foreignConflicts);
1307
+ if (!proceed) return {
1308
+ success: false,
1309
+ path: entry,
1310
+ mode: "symlink",
1311
+ error: "aborted: user-owned content at entry path",
1312
+ forgeOtherReplacements: []
1313
+ };
1314
+ }
1315
+ await cleanCanonicalDir(canonicalDir, storeRoot);
1316
+ await copyDirectory(skill.path, canonicalDir);
1317
+ await writeFile(join(canonicalDir, ".forge-source.json"), JSON.stringify({
1318
+ ref: newOpts.ref,
1319
+ scope: newOpts.scopeNamespace,
1320
+ type: "skill",
1321
+ name: skillName,
1322
+ install_mode: agentType,
1323
+ parent_plugin_ref: null,
1324
+ installed_at: (/* @__PURE__ */ new Date()).toISOString()
1325
+ }, null, 2) + "\n");
1326
+ if (status.kind !== "absent" && status.kind !== "forge-self") try {
1327
+ await rm(entry, {
1328
+ recursive: true,
1329
+ force: true
1330
+ });
1331
+ } catch {}
1332
+ if (!await createSymlink(canonicalDir, entry)) {
1333
+ await mkdir(dirname(entry), { recursive: true });
1334
+ try {
1335
+ await rm(entry, {
1336
+ recursive: true,
1337
+ force: true
1338
+ });
1339
+ } catch {}
1340
+ await copyDirectory(canonicalDir, entry);
1341
+ return {
1342
+ success: true,
1343
+ path: entry,
1344
+ mode: "symlink",
1345
+ symlinkFailed: true,
1346
+ canonicalPath: canonicalDir,
1347
+ forgeOtherReplacements
1348
+ };
1349
+ }
1350
+ return {
1351
+ success: true,
1352
+ path: entry,
1353
+ mode: "symlink",
1354
+ canonicalPath: canonicalDir,
1355
+ forgeOtherReplacements
1356
+ };
1357
+ } catch (error) {
1358
+ return {
1359
+ success: false,
1360
+ path: entry,
1361
+ mode: "symlink",
1362
+ error: error instanceof Error ? error.message : "Unknown error",
1363
+ forgeOtherReplacements: []
1364
+ };
1365
+ }
1366
+ }
1367
+ async function _installSkillForAgentLegacy(skill, agentType, options) {
1060
1368
  const agent = agents[agentType];
1061
1369
  const isGlobal = options.global ?? false;
1062
1370
  const cwd = options.cwd || process.cwd();
@@ -1064,7 +1372,8 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1064
1372
  success: false,
1065
1373
  path: "",
1066
1374
  mode: options.mode ?? "symlink",
1067
- error: `${agent.displayName} does not support global skill installation`
1375
+ error: `${agent.displayName} does not support global skill installation`,
1376
+ forgeOtherReplacements: []
1068
1377
  };
1069
1378
  const skillName = sanitizeName(skill.name || basename(skill.path));
1070
1379
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
@@ -1076,13 +1385,15 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1076
1385
  success: false,
1077
1386
  path: agentDir,
1078
1387
  mode: installMode,
1079
- error: "Invalid skill name: potential path traversal detected"
1388
+ error: "Invalid skill name: potential path traversal detected",
1389
+ forgeOtherReplacements: []
1080
1390
  };
1081
1391
  if (!isPathSafe(agentBase, agentDir)) return {
1082
1392
  success: false,
1083
1393
  path: agentDir,
1084
1394
  mode: installMode,
1085
- error: "Invalid skill name: potential path traversal detected"
1395
+ error: "Invalid skill name: potential path traversal detected",
1396
+ forgeOtherReplacements: []
1086
1397
  };
1087
1398
  try {
1088
1399
  if (installMode === "copy") {
@@ -1091,7 +1402,8 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1091
1402
  return {
1092
1403
  success: true,
1093
1404
  path: agentDir,
1094
- mode: "copy"
1405
+ mode: "copy",
1406
+ forgeOtherReplacements: []
1095
1407
  };
1096
1408
  }
1097
1409
  await cleanAndCreateDirectory(canonicalDir);
@@ -1100,7 +1412,8 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1100
1412
  success: true,
1101
1413
  path: canonicalDir,
1102
1414
  canonicalPath: canonicalDir,
1103
- mode: "symlink"
1415
+ mode: "symlink",
1416
+ forgeOtherReplacements: []
1104
1417
  };
1105
1418
  if (!await createSymlink(canonicalDir, agentDir)) {
1106
1419
  await cleanAndCreateDirectory(agentDir);
@@ -1110,21 +1423,24 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1110
1423
  path: agentDir,
1111
1424
  canonicalPath: canonicalDir,
1112
1425
  mode: "symlink",
1113
- symlinkFailed: true
1426
+ symlinkFailed: true,
1427
+ forgeOtherReplacements: []
1114
1428
  };
1115
1429
  }
1116
1430
  return {
1117
1431
  success: true,
1118
1432
  path: agentDir,
1119
1433
  canonicalPath: canonicalDir,
1120
- mode: "symlink"
1434
+ mode: "symlink",
1435
+ forgeOtherReplacements: []
1121
1436
  };
1122
1437
  } catch (error) {
1123
1438
  return {
1124
1439
  success: false,
1125
1440
  path: agentDir,
1126
1441
  mode: installMode,
1127
- error: error instanceof Error ? error.message : "Unknown error"
1442
+ error: error instanceof Error ? error.message : "Unknown error",
1443
+ forgeOtherReplacements: []
1128
1444
  };
1129
1445
  }
1130
1446
  }
@@ -1472,6 +1788,686 @@ async function listInstalledSkills(options = {}) {
1472
1788
  } catch {}
1473
1789
  return Array.from(skillsMap.values());
1474
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
+ async function installPlugin(ref, opts) {
1829
+ const cfg = loadConfig();
1830
+ const tarball = await fetchPluginArchive(ref, cfg.portal, cfg.token);
1831
+ const tmp = await mkdtemp(join(tmpdir(), "plugin-"));
1832
+ try {
1833
+ extractTarGz(tarball, tmp);
1834
+ return await installFromExtracted(ref, await findPluginRoot(tmp), opts);
1835
+ } finally {
1836
+ await rm(tmp, {
1837
+ recursive: true,
1838
+ force: true
1839
+ });
1840
+ }
1841
+ }
1842
+ async function findPluginRoot(start) {
1843
+ const queue = [start];
1844
+ while (queue.length > 0) {
1845
+ const dir = queue.shift();
1846
+ if (existsSync(join(dir, "plugin.json"))) return dir;
1847
+ if (existsSync(join(dir, ".claude-plugin", "plugin.json"))) return dir;
1848
+ let entries;
1849
+ try {
1850
+ entries = await readdir(dir, { withFileTypes: true });
1851
+ } catch {
1852
+ continue;
1853
+ }
1854
+ for (const e of entries) if (e.isDirectory()) queue.push(join(dir, e.name));
1855
+ }
1856
+ throw new Error(`plugin.json not found under ${start} (corrupt archive?)`);
1857
+ }
1858
+ async function installFromExtracted(ref, extractedDir, opts) {
1859
+ const { scopeNamespace, pluginName } = parsePluginRef$1(ref);
1860
+ const storeRoot = getStoreRoot(opts.scope, opts.cwd);
1861
+ const pluginCanonical = join(storeRoot, "plugins", `${scopeNamespace}__${pluginName}`);
1862
+ const components = await classifyExtracted(extractedDir);
1863
+ const ignoredComponents = await collectIgnoredTopLevel(extractedDir);
1864
+ const preflight = buildPreflightList(pluginCanonical, components, opts);
1865
+ const foreignConflicts = [];
1866
+ const forgeOtherReplacements = [];
1867
+ const statuses = /* @__PURE__ */ new Map();
1868
+ for (const pe of preflight) {
1869
+ const intendedRef = ref + "/" + componentSubRef(pe.component);
1870
+ const status = await assertEntryReplaceable(pe.entryPath, {
1871
+ intendedRef,
1872
+ intendedCanonical: pe.intendedCanonical,
1873
+ storeRoot
1874
+ });
1875
+ statuses.set(pe.entryPath, status);
1876
+ if (status.kind === "forge-other") forgeOtherReplacements.push({
1877
+ entryPath: pe.entryPath,
1878
+ otherRef: status.otherRef
1879
+ });
1880
+ else if (status.kind === "foreign-file" || status.kind === "foreign-dir" || status.kind === "foreign-symlink") foreignConflicts.push({
1881
+ entryPath: pe.entryPath,
1882
+ kind: status.kind
1883
+ });
1884
+ }
1885
+ if (foreignConflicts.length > 0) {
1886
+ if (!await promptForeignOverwrite(foreignConflicts, {
1887
+ yes: opts.yes,
1888
+ force: opts.force
1889
+ })) throw new Error(`aborted: ${foreignConflicts.length} foreign conflicts at plugin entry paths`);
1890
+ }
1891
+ return {
1892
+ pluginRef: ref,
1893
+ pluginCanonical,
1894
+ installedComponents: await commitPluginInstall({
1895
+ ref,
1896
+ scopeNamespace,
1897
+ pluginName,
1898
+ pluginCanonical,
1899
+ extractedDir,
1900
+ components,
1901
+ preflight,
1902
+ statuses,
1903
+ opts
1904
+ }),
1905
+ installedEntries: preflight.map((pe) => ({
1906
+ agent: pe.agent,
1907
+ kind: componentLedgerKind(pe.component.kind),
1908
+ name: componentDisplayName(pe.component),
1909
+ entryPath: pe.entryPath
1910
+ })),
1911
+ forgeOtherReplacements,
1912
+ ignoredComponents
1913
+ };
1914
+ }
1915
+ function componentLedgerKind(kind) {
1916
+ switch (kind) {
1917
+ case "plugin-skill": return "skill";
1918
+ case "plugin-agent-manifest": return "agent-manifest";
1919
+ case "plugin-agent-deps-dir": return "agent-deps-dir";
1920
+ case "plugin-rule-manifest": return "rule-manifest";
1921
+ case "plugin-rule-deps-dir": return "rule-deps-dir";
1922
+ }
1923
+ }
1924
+ function componentDisplayName(c) {
1925
+ switch (c.kind) {
1926
+ case "plugin-skill": return c.skillName;
1927
+ case "plugin-agent-manifest":
1928
+ case "plugin-rule-manifest": return c.filename;
1929
+ case "plugin-agent-deps-dir":
1930
+ case "plugin-rule-deps-dir": return c.dirname;
1931
+ }
1932
+ }
1933
+ function buildPreflightList(pluginCanonical, components, opts) {
1934
+ const preflight = [];
1935
+ for (const agent of opts.agents) {
1936
+ for (const skillName of components.skills) {
1937
+ const c = {
1938
+ kind: "plugin-skill",
1939
+ pluginCanonical,
1940
+ skillName
1941
+ };
1942
+ const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
1943
+ preflight.push({
1944
+ agent,
1945
+ component: c,
1946
+ entryPath: t.entry,
1947
+ intendedCanonical: t.canonical
1948
+ });
1949
+ }
1950
+ for (const filename of components.agentManifests) {
1951
+ const c = {
1952
+ kind: "plugin-agent-manifest",
1953
+ pluginCanonical,
1954
+ filename
1955
+ };
1956
+ const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
1957
+ preflight.push({
1958
+ agent,
1959
+ component: c,
1960
+ entryPath: t.entry,
1961
+ intendedCanonical: t.canonical
1962
+ });
1963
+ }
1964
+ for (const dirname of components.agentDepsDirs) {
1965
+ const c = {
1966
+ kind: "plugin-agent-deps-dir",
1967
+ pluginCanonical,
1968
+ dirname
1969
+ };
1970
+ const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
1971
+ preflight.push({
1972
+ agent,
1973
+ component: c,
1974
+ entryPath: t.entry,
1975
+ intendedCanonical: t.canonical
1976
+ });
1977
+ }
1978
+ for (const filename of components.ruleManifests) {
1979
+ const c = {
1980
+ kind: "plugin-rule-manifest",
1981
+ pluginCanonical,
1982
+ filename
1983
+ };
1984
+ const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
1985
+ if (t.entry) preflight.push({
1986
+ agent,
1987
+ component: c,
1988
+ entryPath: t.entry,
1989
+ intendedCanonical: t.canonical
1990
+ });
1991
+ }
1992
+ for (const dirname of components.ruleDepsDirs) {
1993
+ const c = {
1994
+ kind: "plugin-rule-deps-dir",
1995
+ pluginCanonical,
1996
+ dirname
1997
+ };
1998
+ const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
1999
+ if (t.entry) preflight.push({
2000
+ agent,
2001
+ component: c,
2002
+ entryPath: t.entry,
2003
+ intendedCanonical: t.canonical
2004
+ });
2005
+ }
2006
+ }
2007
+ return preflight;
2008
+ }
2009
+ async function classifyExtracted(dir) {
2010
+ const out = {
2011
+ skills: [],
2012
+ agentManifests: [],
2013
+ agentDepsDirs: [],
2014
+ ruleManifests: [],
2015
+ ruleDepsDirs: []
2016
+ };
2017
+ if (existsSync(join(dir, "skills"))) {
2018
+ for (const e of await readdir(join(dir, "skills"), { withFileTypes: true })) if (e.isDirectory()) out.skills.push(e.name);
2019
+ }
2020
+ if (existsSync(join(dir, "agents"))) {
2021
+ for (const e of await readdir(join(dir, "agents"), { withFileTypes: true })) if (e.isFile() && e.name.endsWith(".md")) out.agentManifests.push(e.name);
2022
+ else if (e.isDirectory()) out.agentDepsDirs.push(e.name);
2023
+ }
2024
+ if (existsSync(join(dir, "rules"))) {
2025
+ for (const e of await readdir(join(dir, "rules"), { withFileTypes: true })) if (e.isFile() && (e.name.endsWith(".md") || e.name.endsWith(".mdc"))) out.ruleManifests.push(e.name);
2026
+ else if (e.isDirectory()) out.ruleDepsDirs.push(e.name);
2027
+ }
2028
+ return out;
2029
+ }
2030
+ async function collectIgnoredTopLevel(dir) {
2031
+ const accepted = new Set([
2032
+ "skills",
2033
+ "agents",
2034
+ "rules",
2035
+ "plugin.json",
2036
+ ".claude-plugin"
2037
+ ]);
2038
+ const out = [];
2039
+ for (const e of await readdir(dir, { withFileTypes: true })) if (!accepted.has(e.name)) out.push(e.isDirectory() ? `${e.name}/` : e.name);
2040
+ out.sort();
2041
+ return out;
2042
+ }
2043
+ function parsePluginRef$1(ref) {
2044
+ let m = /^@public\/plugins\/(.+)$/.exec(ref);
2045
+ if (m) return {
2046
+ scopeNamespace: "public",
2047
+ pluginName: m[1]
2048
+ };
2049
+ m = /^@teams\/([^/]+)\/plugins\/(.+)$/.exec(ref);
2050
+ if (m) return {
2051
+ scopeNamespace: m[1],
2052
+ pluginName: m[2]
2053
+ };
2054
+ throw new Error(`invalid plugin ref: ${ref}`);
2055
+ }
2056
+ function componentSubRef(c) {
2057
+ switch (c.kind) {
2058
+ case "plugin-skill": return `skills/${c.skillName}`;
2059
+ case "plugin-agent-manifest": return `agents/${c.filename}`;
2060
+ case "plugin-agent-deps-dir": return `agents/${c.dirname}`;
2061
+ case "plugin-rule-manifest": return `rules/${c.filename}`;
2062
+ case "plugin-rule-deps-dir": return `rules/${c.dirname}`;
2063
+ }
2064
+ }
2065
+ async function commitPluginInstall(params) {
2066
+ const { ref, scopeNamespace, pluginName, pluginCanonical, extractedDir, components, preflight, statuses, opts } = params;
2067
+ await rm(pluginCanonical, {
2068
+ recursive: true,
2069
+ force: true
2070
+ });
2071
+ await mkdir(pluginCanonical, { recursive: true });
2072
+ await copyDirRecursive(extractedDir, pluginCanonical, true);
2073
+ for (const skillName of components.skills) {
2074
+ const sidecarPath = join(pluginCanonical, "skills", skillName, ".forge-source.json");
2075
+ await mkdir(dirname(sidecarPath), { recursive: true });
2076
+ await writeFile(sidecarPath, JSON.stringify({
2077
+ ref: `${ref}/skills/${skillName}`,
2078
+ scope: scopeNamespace,
2079
+ type: "skill",
2080
+ name: skillName,
2081
+ install_mode: opts.agents[0],
2082
+ parent_plugin_ref: ref,
2083
+ installed_at: (/* @__PURE__ */ new Date()).toISOString()
2084
+ }, null, 2) + "\n");
2085
+ }
2086
+ const ledger = [
2087
+ ...components.skills.map((n) => ({
2088
+ type: "skill",
2089
+ name: n
2090
+ })),
2091
+ ...components.agentManifests.map((n) => ({
2092
+ type: "agent-manifest",
2093
+ name: n
2094
+ })),
2095
+ ...components.agentDepsDirs.map((n) => ({
2096
+ type: "agent-deps-dir",
2097
+ name: n
2098
+ })),
2099
+ ...components.ruleManifests.map((n) => ({
2100
+ type: "rule-manifest",
2101
+ name: n
2102
+ })),
2103
+ ...components.ruleDepsDirs.map((n) => ({
2104
+ type: "rule-deps-dir",
2105
+ name: n
2106
+ }))
2107
+ ];
2108
+ await writeFile(join(pluginCanonical, ".forge-plugin.json"), JSON.stringify({
2109
+ ref,
2110
+ scope: scopeNamespace,
2111
+ name: pluginName,
2112
+ installed_at: (/* @__PURE__ */ new Date()).toISOString(),
2113
+ install_modes: opts.agents,
2114
+ installed_components: ledger
2115
+ }, null, 2) + "\n");
2116
+ for (const pe of preflight) {
2117
+ const status = statuses.get(pe.entryPath);
2118
+ if (!status) continue;
2119
+ if (status.kind !== "absent" && status.kind !== "forge-self") try {
2120
+ await rm(pe.entryPath, {
2121
+ recursive: true,
2122
+ force: true
2123
+ });
2124
+ } catch {}
2125
+ await mkdir(dirname(pe.entryPath), { recursive: true });
2126
+ if (!await createSymlink(pe.intendedCanonical, pe.entryPath)) {
2127
+ let stEntry;
2128
+ try {
2129
+ stEntry = await stat(pe.intendedCanonical);
2130
+ } catch {
2131
+ continue;
2132
+ }
2133
+ try {
2134
+ await rm(pe.entryPath, {
2135
+ recursive: true,
2136
+ force: true
2137
+ });
2138
+ } catch {}
2139
+ if (stEntry.isDirectory()) await copyDirRecursive(pe.intendedCanonical, pe.entryPath, false);
2140
+ else await copyFileShallow(pe.intendedCanonical, pe.entryPath);
2141
+ }
2142
+ }
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
+ if (opts.agents.includes("codex") && components.agentManifests.length > 0) await registerCodexAgents({
2152
+ pluginCanonical,
2153
+ agentFilenames: components.agentManifests,
2154
+ scope: opts.scope,
2155
+ cwd: opts.cwd
2156
+ });
2157
+ if (components.skills.length > 0) for (const agent of opts.agents) await ensureForgeHooks(agent);
2158
+ return ledger;
2159
+ }
2160
+ async function copyDirRecursive(src, dest, skipDotClaudePlugin) {
2161
+ await mkdir(dest, { recursive: true });
2162
+ for (const e of await readdir(src, { withFileTypes: true })) {
2163
+ if (skipDotClaudePlugin && e.name === ".claude-plugin") continue;
2164
+ const s = join(src, e.name);
2165
+ const d = join(dest, e.name);
2166
+ if (e.isDirectory()) await copyDirRecursive(s, d, false);
2167
+ else if (e.isFile()) await writeFile(d, await readFile(s));
2168
+ }
2169
+ }
2170
+ async function copyFileShallow(src, dest) {
2171
+ const buf = await readFile(src);
2172
+ await mkdir(dirname(dest), { recursive: true });
2173
+ await writeFile(dest, buf);
2174
+ }
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
+ async function registerCodexAgents(params) {
2208
+ const configToml = join(params.scope === "global" ? getAgentHome("codex") : join(params.cwd ?? process.cwd(), ".codex"), "config.toml");
2209
+ let raw = "";
2210
+ try {
2211
+ raw = await readFile(configToml, "utf-8");
2212
+ } catch {}
2213
+ for (const fname of params.agentFilenames) {
2214
+ const id = fname.replace(/\.md$/, "");
2215
+ const configFile = join(params.pluginCanonical, "agents", fname);
2216
+ raw = upsertCodexAgent(raw, {
2217
+ id,
2218
+ configFile
2219
+ });
2220
+ }
2221
+ await mkdir(dirname(configToml), { recursive: true });
2222
+ await writeFile(configToml, raw, "utf-8");
2223
+ }
2224
+ const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
2225
+ callback();
2226
+ } });
2227
+ const S_STEP_ACTIVE = import_picocolors.default.green("◆");
2228
+ const S_STEP_CANCEL = import_picocolors.default.red("■");
2229
+ const S_STEP_SUBMIT = import_picocolors.default.green("◇");
2230
+ const S_RADIO_ACTIVE = import_picocolors.default.green("●");
2231
+ const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
2232
+ import_picocolors.default.green("✓");
2233
+ const S_BULLET = import_picocolors.default.green("•");
2234
+ const S_BAR = import_picocolors.default.dim("│");
2235
+ const S_BAR_H = import_picocolors.default.dim("─");
2236
+ const cancelSymbol = Symbol("cancel");
2237
+ function approxStringWidth(plain) {
2238
+ let width = 0;
2239
+ for (const ch of plain) {
2240
+ const code = ch.codePointAt(0);
2241
+ if (code === 0) continue;
2242
+ width += code >= 4352 && code <= 4447 || code >= 8986 && code <= 8987 || code >= 9001 && code <= 9002 || code >= 9193 && code <= 9196 || code === 9200 || code === 9203 || code >= 9725 && code <= 9726 || code >= 9748 && code <= 9749 || code >= 9800 && code <= 9811 || code >= 9855 && code <= 9855 || code >= 9875 && code <= 9875 || code >= 9889 && code <= 9889 || code >= 9898 && code <= 9899 || code >= 9917 && code <= 9918 || code >= 9924 && code <= 9925 || code >= 9934 && code <= 9934 || code >= 9940 && code <= 9940 || code >= 9962 && code <= 9962 || code >= 9970 && code <= 9971 || code >= 9973 && code <= 9973 || code >= 9978 && code <= 9978 || code >= 9981 && code <= 9981 || code >= 9989 && code <= 9989 || code >= 9994 && code <= 9995 || code >= 10024 && code <= 10024 || code >= 10060 && code <= 10060 || code >= 10062 && code <= 10062 || code >= 10067 && code <= 10069 || code >= 10071 && code <= 10071 || code >= 10133 && code <= 10135 || code >= 10160 && code <= 10160 || code >= 10175 && code <= 10175 || code >= 11035 && code <= 11036 || code >= 11088 && code <= 11088 || code >= 11093 && code <= 11093 || code >= 11904 && code <= 42191 && code !== 12351 || code >= 43360 && code <= 43388 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 126976 && code <= 129535 ? 2 : 1;
2243
+ }
2244
+ return width;
2245
+ }
2246
+ function visualRowsForLine(line, columns) {
2247
+ const plain = stripVTControlCharacters(line);
2248
+ const cols = Math.max(1, columns);
2249
+ const w = approxStringWidth(plain);
2250
+ return Math.max(1, Math.ceil(w / cols));
2251
+ }
2252
+ function countVisualRowsForLines(lines, columns) {
2253
+ const cols = columns !== void 0 && columns > 0 ? columns : process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
2254
+ return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0);
2255
+ }
2256
+ async function searchMultiselect(options) {
2257
+ const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection } = options;
2258
+ return new Promise((resolve) => {
2259
+ const rl = readline.createInterface({
2260
+ input: process.stdin,
2261
+ output: silentOutput,
2262
+ terminal: false
2263
+ });
2264
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
2265
+ readline.emitKeypressEvents(process.stdin, rl);
2266
+ let query = "";
2267
+ let cursor = 0;
2268
+ const selected = new Set(initialSelected);
2269
+ let lastRenderHeight = 0;
2270
+ const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
2271
+ const filter = (item, q) => {
2272
+ if (!q) return true;
2273
+ const lowerQ = q.toLowerCase();
2274
+ return item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ);
2275
+ };
2276
+ const getFiltered = () => {
2277
+ return items.filter((item) => filter(item, query));
2278
+ };
2279
+ const clearRender = () => {
2280
+ if (lastRenderHeight > 0) {
2281
+ process.stdout.write(`\x1b[${lastRenderHeight}A`);
2282
+ for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B");
2283
+ process.stdout.write(`\x1b[${lastRenderHeight}A`);
2284
+ }
2285
+ };
2286
+ const render = (state = "active") => {
2287
+ clearRender();
2288
+ const lines = [];
2289
+ const filtered = getFiltered();
2290
+ const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
2291
+ lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
2292
+ if (state === "active") {
2293
+ if (lockedSection && lockedSection.items.length > 0) {
2294
+ lines.push(`${S_BAR}`);
2295
+ const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
2296
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
2297
+ for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
2298
+ lines.push(`${S_BAR}`);
2299
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
2300
+ }
2301
+ const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
2302
+ lines.push(searchLine);
2303
+ lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
2304
+ lines.push(`${S_BAR}`);
2305
+ const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
2306
+ const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
2307
+ const visibleItems = filtered.slice(visibleStart, visibleEnd);
2308
+ if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
2309
+ else {
2310
+ for (let i = 0; i < visibleItems.length; i++) {
2311
+ const item = visibleItems[i];
2312
+ const actualIndex = visibleStart + i;
2313
+ const isSelected = selected.has(item.value);
2314
+ const isCursor = actualIndex === cursor;
2315
+ const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
2316
+ const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
2317
+ const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
2318
+ const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
2319
+ lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
2320
+ }
2321
+ const hiddenBefore = visibleStart;
2322
+ const hiddenAfter = filtered.length - visibleEnd;
2323
+ if (hiddenBefore > 0 || hiddenAfter > 0) {
2324
+ const parts = [];
2325
+ if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
2326
+ if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
2327
+ lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
2328
+ }
2329
+ }
2330
+ lines.push(`${S_BAR}`);
2331
+ const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
2332
+ if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
2333
+ else {
2334
+ const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
2335
+ lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
2336
+ }
2337
+ lines.push(`${import_picocolors.default.dim("└")}`);
2338
+ } else if (state === "submit") {
2339
+ const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
2340
+ lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
2341
+ } else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
2342
+ process.stdout.write(lines.join("\n") + "\n");
2343
+ lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns);
2344
+ };
2345
+ const cleanup = () => {
2346
+ process.stdin.removeListener("keypress", keypressHandler);
2347
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
2348
+ rl.close();
2349
+ };
2350
+ const submit = () => {
2351
+ if (required && selected.size === 0 && lockedValues.length === 0) return;
2352
+ render("submit");
2353
+ cleanup();
2354
+ resolve([...lockedValues, ...Array.from(selected)]);
2355
+ };
2356
+ const cancel = () => {
2357
+ render("cancel");
2358
+ cleanup();
2359
+ resolve(cancelSymbol);
2360
+ };
2361
+ const keypressHandler = (_str, key) => {
2362
+ if (!key) return;
2363
+ const filtered = getFiltered();
2364
+ if (key.name === "return") {
2365
+ submit();
2366
+ return;
2367
+ }
2368
+ if (key.name === "escape" || key.ctrl && key.name === "c") {
2369
+ cancel();
2370
+ return;
2371
+ }
2372
+ if (key.name === "up") {
2373
+ cursor = Math.max(0, cursor - 1);
2374
+ render();
2375
+ return;
2376
+ }
2377
+ if (key.name === "down") {
2378
+ cursor = Math.min(filtered.length - 1, cursor + 1);
2379
+ render();
2380
+ return;
2381
+ }
2382
+ if (key.name === "space") {
2383
+ const item = filtered[cursor];
2384
+ if (item) if (selected.has(item.value)) selected.delete(item.value);
2385
+ else selected.add(item.value);
2386
+ render();
2387
+ return;
2388
+ }
2389
+ if (key.name === "backspace") {
2390
+ query = query.slice(0, -1);
2391
+ cursor = 0;
2392
+ render();
2393
+ return;
2394
+ }
2395
+ if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
2396
+ query += key.sequence;
2397
+ cursor = 0;
2398
+ render();
2399
+ return;
2400
+ }
2401
+ };
2402
+ process.stdin.on("keypress", keypressHandler);
2403
+ render();
2404
+ });
2405
+ }
2406
+ const DEFAULT_CLONE_TIMEOUT_MS = 3e5;
2407
+ const CLONE_TIMEOUT_MS = (() => {
2408
+ const raw = process.env.SKILLS_CLONE_TIMEOUT_MS;
2409
+ if (!raw) return DEFAULT_CLONE_TIMEOUT_MS;
2410
+ const parsed = Number.parseInt(raw, 10);
2411
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
2412
+ })();
2413
+ var GitCloneError = class extends Error {
2414
+ url;
2415
+ isTimeout;
2416
+ isAuthError;
2417
+ constructor(message, url, isTimeout = false, isAuthError = false) {
2418
+ super(message);
2419
+ this.name = "GitCloneError";
2420
+ this.url = url;
2421
+ this.isTimeout = isTimeout;
2422
+ this.isAuthError = isAuthError;
2423
+ }
2424
+ };
2425
+ async function cloneRepo(url, ref) {
2426
+ const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
2427
+ const git = esm_default({
2428
+ timeout: { block: CLONE_TIMEOUT_MS },
2429
+ config: [
2430
+ "filter.lfs.required=false",
2431
+ "filter.lfs.smudge=",
2432
+ "filter.lfs.clean=",
2433
+ "filter.lfs.process="
2434
+ ]
2435
+ }).env({
2436
+ ...process.env,
2437
+ GIT_TERMINAL_PROMPT: "0",
2438
+ GIT_LFS_SKIP_SMUDGE: "1"
2439
+ });
2440
+ const cloneOptions = ref ? [
2441
+ "--depth",
2442
+ "1",
2443
+ "--branch",
2444
+ ref
2445
+ ] : ["--depth", "1"];
2446
+ try {
2447
+ await git.clone(url, tempDir, cloneOptions);
2448
+ return tempDir;
2449
+ } catch (error) {
2450
+ await rm(tempDir, {
2451
+ recursive: true,
2452
+ force: true
2453
+ }).catch(() => {});
2454
+ const errorMessage = error instanceof Error ? error.message : String(error);
2455
+ const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
2456
+ const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
2457
+ if (isTimeout) throw new GitCloneError(`Clone timed out after ${Math.round(CLONE_TIMEOUT_MS / 1e3)}s. Common causes:\n - Large repository: raise the timeout with SKILLS_CLONE_TIMEOUT_MS=600000 (10m)\n - Slow network: retry, or clone manually and pass the local path to 'skills add'\n - Private repo without credentials: ensure auth is configured\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)`, url, true, false);
2458
+ if (isAuthError) throw new GitCloneError(`Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`, url, false, true);
2459
+ throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
2460
+ }
2461
+ }
2462
+ async function cleanupTempDir(dir) {
2463
+ const normalizedDir = normalize(resolve(dir));
2464
+ const normalizedTmpDir = normalize(resolve(tmpdir()));
2465
+ if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
2466
+ await rm(dir, {
2467
+ recursive: true,
2468
+ force: true
2469
+ });
2470
+ }
1475
2471
  const TELEMETRY_URL_SUFFIX = "/events";
1476
2472
  const AUDIT_URL = "https://add-skill.vercel.sh/audit";
1477
2473
  let portalUrl = null;
@@ -1780,10 +2776,30 @@ async function getSkillFromLock(skillName) {
1780
2776
  async function getAllLockedSkills() {
1781
2777
  return (await readSkillLock$1()).skills;
1782
2778
  }
2779
+ async function addPluginToLock(pluginName, entry) {
2780
+ const lock = await readSkillLock$1();
2781
+ if (!lock.plugins) lock.plugins = {};
2782
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2783
+ const existing = lock.plugins[pluginName];
2784
+ lock.plugins[pluginName] = {
2785
+ ...entry,
2786
+ installedAt: existing?.installedAt ?? now,
2787
+ updatedAt: now
2788
+ };
2789
+ await writeSkillLock(lock);
2790
+ }
2791
+ async function removePluginFromLock(pluginName) {
2792
+ const lock = await readSkillLock$1();
2793
+ if (!lock.plugins || !(pluginName in lock.plugins)) return false;
2794
+ delete lock.plugins[pluginName];
2795
+ await writeSkillLock(lock);
2796
+ return true;
2797
+ }
1783
2798
  function createEmptyLockFile() {
1784
2799
  return {
1785
2800
  version: CURRENT_VERSION$1,
1786
2801
  skills: {},
2802
+ plugins: {},
1787
2803
  dismissed: {}
1788
2804
  };
1789
2805
  }
@@ -1821,6 +2837,11 @@ async function writeLocalLock(lock, cwd) {
1821
2837
  version: lock.version,
1822
2838
  skills: sortedSkills
1823
2839
  };
2840
+ if (lock.plugins && Object.keys(lock.plugins).length > 0) {
2841
+ const sortedPlugins = {};
2842
+ for (const key of Object.keys(lock.plugins).sort()) sortedPlugins[key] = lock.plugins[key];
2843
+ sorted.plugins = sortedPlugins;
2844
+ }
1824
2845
  await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
1825
2846
  }
1826
2847
  async function computeSkillFolderHash(skillDir) {
@@ -1856,6 +2877,19 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
1856
2877
  lock.skills[skillName] = entry;
1857
2878
  await writeLocalLock(lock, cwd);
1858
2879
  }
2880
+ async function addPluginToLocalLock(pluginName, entry, cwd) {
2881
+ const lock = await readLocalLock(cwd);
2882
+ if (!lock.plugins) lock.plugins = {};
2883
+ lock.plugins[pluginName] = entry;
2884
+ await writeLocalLock(lock, cwd);
2885
+ }
2886
+ async function removePluginFromLocalLock(pluginName, cwd) {
2887
+ const lock = await readLocalLock(cwd);
2888
+ if (!lock.plugins || !(pluginName in lock.plugins)) return false;
2889
+ delete lock.plugins[pluginName];
2890
+ await writeLocalLock(lock, cwd);
2891
+ return true;
2892
+ }
1859
2893
  function createEmptyLocalLock() {
1860
2894
  return {
1861
2895
  version: CURRENT_VERSION,
@@ -2115,6 +3149,10 @@ function buildSecurityLines(auditData, skills, source) {
2115
3149
  lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
2116
3150
  return lines;
2117
3151
  }
3152
+ function agentTypeFromDisplay(display) {
3153
+ for (const [type, cfg] of Object.entries(agents)) if (cfg.displayName === display) return type;
3154
+ return null;
3155
+ }
2118
3156
  function shortenPath$2(fullPath, cwd) {
2119
3157
  const home = homedir();
2120
3158
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
@@ -2476,6 +3514,126 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2476
3514
  Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
2477
3515
  await promptForFindSkills(options, targetAgents);
2478
3516
  }
3517
+ async function runTeamBatchAdd(teamScope, options) {
3518
+ const cfg = loadConfig();
3519
+ 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);
3526
+ }
3527
+ console.log();
3528
+ Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
3529
+ const spinner = Y();
3530
+ spinner.start(`Fetching team "${teamScope}" assets...`);
3531
+ let items;
3532
+ try {
3533
+ items = await fetchTeamAssets(teamScope, cfg.portal, cfg.token);
3534
+ } catch (err) {
3535
+ spinner.stop(import_picocolors.default.red("Failed to list team assets"));
3536
+ const msg = err instanceof Error ? err.message : String(err);
3537
+ Se(import_picocolors.default.red(msg));
3538
+ process.exit(1);
3539
+ }
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.`));
3544
+ return;
3545
+ }
3546
+ const validAgents = Object.keys(agents);
3547
+ let targetAgents;
3548
+ if (options.agent?.includes("*")) {
3549
+ targetAgents = validAgents;
3550
+ M.info(`Installing to all ${targetAgents.length} agents`);
3551
+ } else if (options.agent && options.agent.length > 0) {
3552
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3553
+ if (invalidAgents.length > 0) {
3554
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
3555
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
3556
+ process.exit(1);
3557
+ }
3558
+ targetAgents = options.agent;
3559
+ } else {
3560
+ spinner.start("Loading agents...");
3561
+ const installedAgents = await detectInstalledAgents();
3562
+ spinner.stop(`${validAgents.length} agents`);
3563
+ if (installedAgents.length === 0) if (options.yes) {
3564
+ targetAgents = validAgents;
3565
+ M.info("Installing to all agents");
3566
+ } else {
3567
+ M.info("Select agents to install skills to");
3568
+ const selected = await promptForAgents("Which agents do you want to install to?", buildAgentChoices(validAgents, { global: options.global }));
3569
+ if (pD(selected)) {
3570
+ xe("Installation cancelled");
3571
+ process.exit(0);
3572
+ }
3573
+ targetAgents = selected;
3574
+ }
3575
+ else if (installedAgents.length === 1 || options.yes) {
3576
+ targetAgents = ensureUniversalAgents(installedAgents);
3577
+ if (installedAgents.length === 1) {
3578
+ const firstAgent = installedAgents[0];
3579
+ M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
3580
+ } else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
3581
+ } else {
3582
+ const selected = await selectAgentsInteractive({ global: options.global });
3583
+ if (pD(selected)) {
3584
+ xe("Installation cancelled");
3585
+ process.exit(0);
3586
+ }
3587
+ targetAgents = selected;
3588
+ }
3589
+ }
3590
+ let installGlobally = options.global ?? false;
3591
+ const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
3592
+ if (options.global === void 0 && !options.yes && supportsGlobal) {
3593
+ const scope = await ve({
3594
+ message: "Installation scope",
3595
+ options: [{
3596
+ value: false,
3597
+ label: "Project",
3598
+ hint: "Install in current directory (committed with your project)"
3599
+ }, {
3600
+ value: true,
3601
+ label: "Global",
3602
+ hint: "Install in home directory (available across all projects)"
3603
+ }]
3604
+ });
3605
+ if (pD(scope)) {
3606
+ xe("Installation cancelled");
3607
+ process.exit(0);
3608
+ }
3609
+ installGlobally = scope;
3610
+ }
3611
+ const childOptions = {
3612
+ ...options,
3613
+ team: void 0,
3614
+ yes: true,
3615
+ agent: targetAgents,
3616
+ global: installGlobally
3617
+ };
3618
+ let failures = 0;
3619
+ for (const item of skills) {
3620
+ M.step(`Installing ${import_picocolors.default.cyan(item.name)}`);
3621
+ try {
3622
+ await runAdd([item.ref], childOptions);
3623
+ } catch (err) {
3624
+ failures++;
3625
+ const msg = err instanceof Error ? err.message : String(err);
3626
+ M.warn(`Failed to install ${item.name}: ${msg}`);
3627
+ }
3628
+ }
3629
+ console.log();
3630
+ if (failures === 0) Se(import_picocolors.default.green(`Installed ${skills.length} skill${skills.length === 1 ? "" : "s"} from team "${teamScope}"`));
3631
+ else {
3632
+ Se(import_picocolors.default.yellow(`Installed ${skills.length - failures}/${skills.length} skills (${failures} failed) from team "${teamScope}"`));
3633
+ process.exit(1);
3634
+ }
3635
+ await promptForFindSkills(options, targetAgents);
3636
+ }
2479
3637
  async function runAdd(args, options = {}) {
2480
3638
  const source = args[0];
2481
3639
  let installTipShown = false;
@@ -2484,6 +3642,20 @@ async function runAdd(args, options = {}) {
2484
3642
  M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
2485
3643
  installTipShown = true;
2486
3644
  };
3645
+ if (options.team && source) {
3646
+ console.log();
3647
+ console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("--team cannot be combined with a positional source"));
3648
+ console.log();
3649
+ console.log(import_picocolors.default.dim(" Use one of:"));
3650
+ console.log(` ${import_picocolors.default.cyan("npx shoplazza-ai-dev-cli add")} ${import_picocolors.default.yellow("<source>")}`);
3651
+ console.log(` ${import_picocolors.default.cyan("npx shoplazza-ai-dev-cli add")} ${import_picocolors.default.yellow("--team <scope>")}`);
3652
+ console.log();
3653
+ process.exit(1);
3654
+ }
3655
+ if (!source && options.team) {
3656
+ await runTeamBatchAdd(options.team, options);
3657
+ return;
3658
+ }
2487
3659
  if (!source) {
2488
3660
  console.log();
2489
3661
  console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
@@ -2537,6 +3709,176 @@ async function runAdd(args, options = {}) {
2537
3709
  options.skill = options.skill || [];
2538
3710
  if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
2539
3711
  }
3712
+ if (parsed.type === "portal" && parsed.portalRef) {
3713
+ const portalRef = parsed.portalRef;
3714
+ const pluginsDirRe = /^(?:@public|@teams\/[^/]+)\/plugins$/;
3715
+ const pluginsItemRe = /^(?:@public|@teams\/[^/]+)\/plugins\/[^/]+/;
3716
+ let resolvedPluginRef = null;
3717
+ if (pluginsDirRe.test(portalRef) && options.plugin && options.plugin.length === 1) resolvedPluginRef = `${portalRef}/${options.plugin[0]}`;
3718
+ else if (pluginsItemRe.test(portalRef)) resolvedPluginRef = portalRef;
3719
+ if (resolvedPluginRef !== null) {
3720
+ const isTeam = resolvedPluginRef.startsWith("@teams/");
3721
+ const teamMatch = isTeam ? resolvedPluginRef.match(/^@teams\/([^/]+)/) : null;
3722
+ const scopeNs = isTeam && teamMatch ? teamMatch[1] : "public";
3723
+ let targetAgents;
3724
+ const validAgents = Object.keys(agents);
3725
+ if (options.agent?.includes("*")) targetAgents = validAgents;
3726
+ else if (options.agent && options.agent.length > 0) targetAgents = options.agent;
3727
+ else {
3728
+ spinner.start("Loading agents...");
3729
+ const installedAgents = await detectInstalledAgents();
3730
+ spinner.stop(`${Object.keys(agents).length} agents`);
3731
+ if (options.yes) targetAgents = installedAgents.length > 0 ? ensureUniversalAgents(installedAgents) : validAgents;
3732
+ 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 }));
3734
+ if (pD(selected)) {
3735
+ xe("Installation cancelled");
3736
+ await cleanup(tempDir);
3737
+ process.exit(0);
3738
+ }
3739
+ targetAgents = selected;
3740
+ } else if (installedAgents.length === 1) {
3741
+ targetAgents = ensureUniversalAgents(installedAgents);
3742
+ M.info(`Installing to: ${import_picocolors.default.cyan(agents[installedAgents[0]].displayName)}`);
3743
+ } else {
3744
+ const selected = await selectAgentsInteractive({ global: options.global });
3745
+ if (pD(selected)) {
3746
+ xe("Installation cancelled");
3747
+ await cleanup(tempDir);
3748
+ process.exit(0);
3749
+ }
3750
+ targetAgents = selected;
3751
+ }
3752
+ }
3753
+ let installGloballyForPlugin = options.global ?? false;
3754
+ if (options.global === void 0 && !options.yes) {
3755
+ const scopeChoice = await ve({
3756
+ message: "Installation scope",
3757
+ options: [{
3758
+ value: false,
3759
+ label: "Project",
3760
+ hint: "Install in current directory"
3761
+ }, {
3762
+ value: true,
3763
+ label: "Global",
3764
+ hint: "Install in home directory"
3765
+ }]
3766
+ });
3767
+ if (pD(scopeChoice)) {
3768
+ xe("Installation cancelled");
3769
+ await cleanup(tempDir);
3770
+ process.exit(0);
3771
+ }
3772
+ installGloballyForPlugin = scopeChoice;
3773
+ }
3774
+ {
3775
+ const previewCanonical = join(getStoreRoot(), "plugins", `${scopeNs}__${options.plugin && options.plugin[0] || resolvedPluginRef.split("/").pop()}`);
3776
+ const summaryLines = [];
3777
+ summaryLines.push(import_picocolors.default.cyan(shortenPath$2(previewCanonical, process.cwd())));
3778
+ 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)}`);
3781
+ console.log();
3782
+ Me(summaryLines.join("\n"), "Installation Summary");
3783
+ }
3784
+ if (!options.yes) {
3785
+ const confirmed = await ye({ message: `Install plugin ${import_picocolors.default.cyan(resolvedPluginRef)}?` });
3786
+ if (pD(confirmed) || !confirmed) {
3787
+ xe("Installation cancelled");
3788
+ await cleanup(tempDir);
3789
+ process.exit(0);
3790
+ }
3791
+ }
3792
+ spinner.start(`Installing plugin ${import_picocolors.default.cyan(resolvedPluginRef)}...`);
3793
+ let pluginResult;
3794
+ try {
3795
+ pluginResult = await installPlugin(resolvedPluginRef, {
3796
+ scope: installGloballyForPlugin ? "global" : "project",
3797
+ agents: targetAgents,
3798
+ cwd: process.cwd(),
3799
+ yes: options.yes,
3800
+ force: options.force
3801
+ });
3802
+ } catch (err) {
3803
+ spinner.stop(import_picocolors.default.red("Plugin installation failed"));
3804
+ throw err;
3805
+ }
3806
+ spinner.stop(import_picocolors.default.green("Plugin installed"));
3807
+ if (pluginResult.installedEntries.length > 0) {
3808
+ const grouped = /* @__PURE__ */ new Map();
3809
+ for (const e of pluginResult.installedEntries) {
3810
+ const list = grouped.get(e.agent) ?? [];
3811
+ list.push(e);
3812
+ grouped.set(e.agent, list);
3813
+ }
3814
+ console.log();
3815
+ for (const [agent, list] of grouped) {
3816
+ console.log(import_picocolors.default.bold(agents[agent].displayName));
3817
+ for (const e of list) console.log(` ${import_picocolors.default.dim(e.kind.padEnd(16))}${e.entryPath}`);
3818
+ }
3819
+ }
3820
+ if (pluginResult.forgeOtherReplacements.length > 0) {
3821
+ console.log();
3822
+ console.log(import_picocolors.default.yellow(`⚠ Replaced ${pluginResult.forgeOtherReplacements.length} forge-other entries:`));
3823
+ for (const r of pluginResult.forgeOtherReplacements) {
3824
+ console.log(` ${r.entryPath}`);
3825
+ console.log(` was ${r.otherRef} (now broken — uninstall recommended)`);
3826
+ }
3827
+ }
3828
+ if (pluginResult.ignoredComponents.length > 0) {
3829
+ console.log();
3830
+ console.log(import_picocolors.default.dim(`ℹ Ignored non-MVP components: ${pluginResult.ignoredComponents.join(", ")}`));
3831
+ }
3832
+ for (const agentType of targetAgents) {
3833
+ const { warnings } = await ensureForgeHooks(agentType);
3834
+ for (const w of warnings) M.warn(import_picocolors.default.yellow(w));
3835
+ }
3836
+ const { pluginName: lockPluginName } = parsePluginRef$1(resolvedPluginRef);
3837
+ const lockSource = resolvedPluginRef.startsWith("@teams/") ? `@teams/${resolvedPluginRef.split("/")[1]}` : "@public";
3838
+ const lockPluginPath = `plugins/${lockPluginName}/plugin.json`;
3839
+ let lockHash = "";
3840
+ try {
3841
+ lockHash = await computeSkillFolderHash(pluginResult.pluginCanonical);
3842
+ } catch {}
3843
+ try {
3844
+ if (installGloballyForPlugin) {
3845
+ await addPluginToLock(lockPluginName, {
3846
+ source: lockSource,
3847
+ sourceType: "portal",
3848
+ pluginPath: lockPluginPath,
3849
+ computedHash: lockHash
3850
+ });
3851
+ M.info(`Recorded in lock file: ${import_picocolors.default.dim(getSkillLockPath$1())}`);
3852
+ } else {
3853
+ await addPluginToLocalLock(lockPluginName, {
3854
+ source: lockSource,
3855
+ sourceType: "portal",
3856
+ pluginPath: lockPluginPath,
3857
+ computedHash: lockHash
3858
+ }, process.cwd());
3859
+ M.info(`Recorded in lock file: ${import_picocolors.default.dim(getLocalLockPath(process.cwd()))}`);
3860
+ }
3861
+ } catch (err) {
3862
+ M.warn(import_picocolors.default.yellow(`failed to record plugin in lock file (install still succeeded): ${err instanceof Error ? err.message : String(err)}`));
3863
+ }
3864
+ track({
3865
+ event_type: "install",
3866
+ asset_ref: resolvedPluginRef,
3867
+ agent: "cli",
3868
+ install_mode: "cli"
3869
+ });
3870
+ Se(import_picocolors.default.green(`Plugin ${import_picocolors.default.bold(resolvedPluginRef)} installed successfully`));
3871
+ return;
3872
+ }
3873
+ if (/^(?:@public|@teams\/[^/]+)\/plugins/.test(portalRef) && !resolvedPluginRef) {
3874
+ Se(import_picocolors.default.red(`Cannot install plugin directory without a name. Use --plugin <name> or specify a full ref: ${portalRef}/<name>`));
3875
+ process.exit(1);
3876
+ }
3877
+ }
3878
+ if (parsed.type === "portal" && parsed.portalRef && /^(?:@public|@teams\/[^/]+)\/rules/.test(parsed.portalRef)) {
3879
+ Se(import_picocolors.default.red("Rules are not installable as standalone — install the plugin that contains the rule."));
3880
+ process.exit(1);
3881
+ }
2540
3882
  if (parsed.type === "portal" && parsed.portalRef && isDirectoryPortalRef(parsed.portalRef) && options.skill && options.skill.length === 1 && options.skill[0] !== "*") parsed.portalRef = `${parsed.portalRef}/${options.skill[0]}`;
2541
3883
  const includeInternal = !!(options.skill && options.skill.length > 0);
2542
3884
  let skills;
@@ -2858,8 +4200,12 @@ async function runAdd(args, options = {}) {
2858
4200
  process.exit(0);
2859
4201
  }
2860
4202
  installMode = modeChoice;
2861
- } else if (uniqueDirs.size <= 1) installMode = "copy";
4203
+ }
2862
4204
  const cwd = process.cwd();
4205
+ const _summaryPortalRef = parsed.type === "portal" || parsed.type === "public" || parsed.type === "team" ? parsed.portalRef ?? null : null;
4206
+ const _summaryIsTeam = _summaryPortalRef?.startsWith("@teams/") ?? false;
4207
+ const _summaryTeamMatch = _summaryIsTeam ? _summaryPortalRef?.match(/^@teams\/([^/]+)/) : null;
4208
+ const _summaryScopeNs = _summaryIsTeam && _summaryTeamMatch ? _summaryTeamMatch[1] : "public";
2863
4209
  const summaryLines = [];
2864
4210
  targetAgents.map((a) => agents[a].displayName);
2865
4211
  const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
@@ -2879,10 +4225,11 @@ async function runAdd(args, options = {}) {
2879
4225
  if (!groupedSummary[group]) groupedSummary[group] = [];
2880
4226
  groupedSummary[group].push(skill);
2881
4227
  } else ungroupedSummary.push(skill);
4228
+ const _summaryStoreRoot = getStoreRoot(installGlobally ? "global" : "project", cwd);
2882
4229
  const printSkillSummary = (skills) => {
2883
4230
  for (const skill of skills) {
2884
4231
  if (summaryLines.length > 0) summaryLines.push("");
2885
- const shortCanonical = shortenPath$2(getCanonicalPath(skill.name, { global: installGlobally }), cwd);
4232
+ const shortCanonical = shortenPath$2(join(_summaryStoreRoot, "skills", `${_summaryScopeNs}__${sanitizeName(skill.name)}`), cwd);
2886
4233
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2887
4234
  summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2888
4235
  const skillOverwrites = overwriteStatus.get(skill.name);
@@ -2926,6 +4273,11 @@ async function runAdd(args, options = {}) {
2926
4273
  }
2927
4274
  spinner.start("Installing skills...");
2928
4275
  const results = [];
4276
+ const allForgeOtherReplacements = [];
4277
+ const _installPortalRef = parsed.type === "portal" || parsed.type === "public" || parsed.type === "team" ? parsed.portalRef ?? null : null;
4278
+ const _installIsTeam = _installPortalRef?.startsWith("@teams/") ?? false;
4279
+ const _installTeamMatch = _installIsTeam ? _installPortalRef?.match(/^@teams\/([^/]+)/) : null;
4280
+ const _installScopeNs = _installIsTeam && _installTeamMatch ? _installTeamMatch[1] : "public";
2929
4281
  for (const skill of selectedSkills) for (const agent of targetAgents) {
2930
4282
  let result;
2931
4283
  if (blobResult && "files" in skill) {
@@ -2937,10 +4289,21 @@ async function runAdd(args, options = {}) {
2937
4289
  global: installGlobally,
2938
4290
  mode: installMode
2939
4291
  });
2940
- } else result = await installSkillForAgent(skill, agent, {
2941
- global: installGlobally,
2942
- mode: installMode
2943
- });
4292
+ } else {
4293
+ const skillRef = _installPortalRef ? `${_installPortalRef}/${skill.name ?? ""}` : skill.name ?? "unknown";
4294
+ result = await installSkillForAgent(skill, agent, {
4295
+ scope: installGlobally ? "global" : "project",
4296
+ ref: skillRef,
4297
+ scopeNamespace: _installScopeNs,
4298
+ mode: installMode,
4299
+ forceOverwrite: options.force || options.yes,
4300
+ onConflictPrompt: (conflicts) => promptForeignOverwrite(conflicts, {
4301
+ yes: options.yes,
4302
+ force: options.force
4303
+ })
4304
+ });
4305
+ if (result.forgeOtherReplacements.length > 0) allForgeOtherReplacements.push(...result.forgeOtherReplacements);
4306
+ }
2944
4307
  results.push({
2945
4308
  skill: getSkillDisplayName(skill),
2946
4309
  agent: agents[agent].displayName,
@@ -2952,6 +4315,57 @@ async function runAdd(args, options = {}) {
2952
4315
  console.log();
2953
4316
  const successful = results.filter((r) => r.success);
2954
4317
  const failed = results.filter((r) => !r.success);
4318
+ if (allForgeOtherReplacements.length > 0) {
4319
+ console.log();
4320
+ console.log(import_picocolors.default.yellow(`⚠ Replaced ${allForgeOtherReplacements.length} forge-other entries:`));
4321
+ for (const r of allForgeOtherReplacements) console.log(` ${r.entryPath}\n was ${r.otherRef} (now broken — uninstall recommended)`);
4322
+ }
4323
+ if (parsed.type === "portal" && parsed.portalRef && successful.length > 0) {
4324
+ const portalRef = parsed.portalRef;
4325
+ const isTeam = portalRef.startsWith("@teams/");
4326
+ const teamMatch = isTeam ? portalRef.match(/^@teams\/([^/]+)/) : null;
4327
+ const scope = isTeam && teamMatch ? `team:${teamMatch[1]}` : "public";
4328
+ const directoryRef = isDirectoryPortalRef(portalRef);
4329
+ const assetType = (portalRef.match(/\/(skills|rules)(?:\/|$)/)?.[1] || "skills") === "rules" ? "rule" : "skill";
4330
+ const installedAt = (/* @__PURE__ */ new Date()).toISOString();
4331
+ const manifestWarnings = [];
4332
+ for (const r of successful) {
4333
+ const targetDir = r.canonicalPath || r.path;
4334
+ if (!targetDir) continue;
4335
+ const skillName = r.skill;
4336
+ const manifest = {
4337
+ ref: directoryRef ? `${portalRef}/${skillName}` : portalRef,
4338
+ scope,
4339
+ type: assetType,
4340
+ name: skillName,
4341
+ install_mode: agentTypeFromDisplay(r.agent) ?? "claude-code",
4342
+ installed_at: installedAt
4343
+ };
4344
+ try {
4345
+ await writeForgeSourceManifest(targetDir, manifest);
4346
+ } catch (err) {
4347
+ manifestWarnings.push(`Failed to write .forge-source.json for ${skillName}: ${err instanceof Error ? err.message : String(err)}`);
4348
+ }
4349
+ }
4350
+ if (manifestWarnings.length > 0) for (const w of manifestWarnings) M.warn(import_picocolors.default.yellow(w));
4351
+ const successfulAgentTypes = /* @__PURE__ */ new Set();
4352
+ for (const r of successful) {
4353
+ const t = agentTypeFromDisplay(r.agent);
4354
+ if (t) successfulAgentTypes.add(t);
4355
+ }
4356
+ for (const agentType of successfulAgentTypes) {
4357
+ const { warnings } = await ensureForgeHooks(agentType);
4358
+ for (const w of warnings) M.warn(import_picocolors.default.yellow(w));
4359
+ }
4360
+ const installedSkillNames = /* @__PURE__ */ new Set();
4361
+ for (const r of successful) installedSkillNames.add(r.skill);
4362
+ for (const skillName of installedSkillNames) track({
4363
+ event_type: "install",
4364
+ asset_ref: directoryRef ? `${portalRef}/${skillName}` : portalRef,
4365
+ agent: "cli",
4366
+ install_mode: "cli"
4367
+ });
4368
+ }
2955
4369
  const skillFiles = {};
2956
4370
  for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
2957
4371
  else if (repoRoot && skill.path === repoRoot) skillFiles[skill.name] = "SKILL.md";
@@ -3154,6 +4568,7 @@ function parseAddOptions(args) {
3154
4568
  const arg = args[i];
3155
4569
  if (arg === "-g" || arg === "--global") options.global = true;
3156
4570
  else if (arg === "-y" || arg === "--yes") options.yes = true;
4571
+ else if (arg === "-f" || arg === "--force") options.force = true;
3157
4572
  else if (arg === "-l" || arg === "--list") options.list = true;
3158
4573
  else if (arg === "--all") options.all = true;
3159
4574
  else if (arg === "-a" || arg === "--agent") {
@@ -3176,6 +4591,16 @@ function parseAddOptions(args) {
3176
4591
  nextArg = args[i];
3177
4592
  }
3178
4593
  i--;
4594
+ } else if (arg === "--plugin") {
4595
+ options.plugin = options.plugin || [];
4596
+ i++;
4597
+ let nextArg = args[i];
4598
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
4599
+ options.plugin.push(nextArg);
4600
+ i++;
4601
+ nextArg = args[i];
4602
+ }
4603
+ i--;
3179
4604
  } else if (arg === "--rule") {
3180
4605
  options.rule = options.rule || [];
3181
4606
  i++;
@@ -3868,6 +5293,21 @@ async function runList(args) {
3868
5293
  async function removeCommand(skillNames, options) {
3869
5294
  const isGlobal = options.global ?? false;
3870
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)}`));
5306
+ }
5307
+ const nonPluginNames = skillNames.filter((n) => !pluginRefRe.test(n));
5308
+ if (nonPluginNames.length === 0) return;
5309
+ skillNames = nonPluginNames;
5310
+ }
3871
5311
  const spinner = Y();
3872
5312
  spinner.start("Scanning for installed skills...");
3873
5313
  const skillNamesSet = /* @__PURE__ */ new Set();
@@ -4055,6 +5495,130 @@ function parseRemoveOptions(args) {
4055
5495
  options
4056
5496
  };
4057
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,
5515
+ storeRoot
5516
+ });
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, {
5528
+ recursive: true,
5529
+ force: true
5530
+ });
5531
+ } catch {}
5532
+ try {
5533
+ await removePluginFromLock(pluginName);
5534
+ } catch {}
5535
+ try {
5536
+ await removePluginFromLocalLock(pluginName, cwd);
5537
+ } catch {}
5538
+ }
5539
+ function resolveEntryPath(agent, c, pluginCanonical, scope, cwd) {
5540
+ let component;
5541
+ 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 "";
5578
+ }
5579
+ return getInstallTargets(agent, component, scope, cwd).entry;
5580
+ }
5581
+ async function cleanupCodexConfig(ledger, scope, cwd, scopeNs, pluginName) {
5582
+ const configToml = join(scope === "global" ? getAgentHome("codex") : join(cwd ?? process.cwd(), ".codex"), "config.toml");
5583
+ let tomlRaw = "";
5584
+ try {
5585
+ 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
+ } catch {
5604
+ return;
5605
+ }
5606
+ const updatedAgentsMd = removePluginRules(agentsRaw, `${scopeNs}:${pluginName}`);
5607
+ if (updatedAgentsMd !== agentsRaw) await writeFile(canonicalAgentsMd, updatedAgentsMd, "utf-8");
5608
+ }
5609
+ function parsePluginRef(ref) {
5610
+ let m = /^@public\/plugins\/(.+)$/.exec(ref);
5611
+ if (m) return {
5612
+ scopeNamespace: "public",
5613
+ pluginName: m[1]
5614
+ };
5615
+ m = /^@teams\/([^/]+)\/plugins\/(.+)$/.exec(ref);
5616
+ if (m) return {
5617
+ scopeNamespace: m[1],
5618
+ pluginName: m[2]
5619
+ };
5620
+ throw new Error(`invalid plugin ref: ${ref}`);
5621
+ }
4058
5622
  function formatSourceInput(sourceUrl, ref) {
4059
5623
  if (!ref) return sourceUrl;
4060
5624
  return `${sourceUrl}#${ref}`;
@@ -4594,6 +6158,7 @@ ${BOLD}Manage Skills:${RESET}
4594
6158
  @teams/<scope>/skills --skill <name>
4595
6159
  @public/skills/<name> # legacy, still supported
4596
6160
  https://gitlab.shoplazza.site/.../<repo>.git
6161
+ add --team <scope> Install every skill owned by a team
4597
6162
  remove [skills] Remove installed skills
4598
6163
  list, ls List installed skills
4599
6164
  find [query] Search for skills interactively
@@ -4615,6 +6180,8 @@ ${BOLD}Add Options:${RESET}
4615
6180
  -s, --skill <skills> Specify skill names to install (use '*' for all skills)
4616
6181
  -l, --list List available skills in the source without installing
4617
6182
  -y, --yes Skip confirmation prompts
6183
+ --team <scope> Install every skill owned by the team
6184
+ (no positional source; mutually exclusive with <source>)
4618
6185
  --copy Copy files instead of symlinking to agent directories
4619
6186
  --all Shorthand for --skill '*' --agent '*' -y
4620
6187
  --full-depth Search all subdirectories even when a root SKILL.md exists