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