skills 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli.mjs +745 -307
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { esm_default } from "./_chunks/libs/simple-git.mjs";
|
|
|
7
7
|
import { xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
|
|
8
8
|
import { require_dist } from "./_chunks/libs/@vercel/detect-agent.mjs";
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
10
|
-
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
10
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep } from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
import { stripVTControlCharacters } from "node:util";
|
|
13
13
|
import { createWriteStream } from "node:fs";
|
|
@@ -17,7 +17,7 @@ import * as readline from "readline";
|
|
|
17
17
|
import { Writable } from "stream";
|
|
18
18
|
import { promisify } from "util";
|
|
19
19
|
import { execFile, spawn, spawnSync } from "child_process";
|
|
20
|
-
import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
20
|
+
import { access, chmod, cp, glob, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
21
21
|
import { parse, stringify } from "yaml";
|
|
22
22
|
import { createHash } from "crypto";
|
|
23
23
|
import { cp as cp$1, mkdir as mkdir$1, mkdtemp as mkdtemp$1, readFile as readFile$1, rm as rm$1, stat as stat$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
@@ -1015,7 +1015,7 @@ async function cleanupTempDir(dir) {
|
|
|
1015
1015
|
});
|
|
1016
1016
|
}
|
|
1017
1017
|
function parseFrontmatter(raw) {
|
|
1018
|
-
const match = raw.match(
|
|
1018
|
+
const match = raw.match(/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
1019
1019
|
if (!match) return {
|
|
1020
1020
|
data: {},
|
|
1021
1021
|
content: raw
|
|
@@ -1112,14 +1112,13 @@ async function readLocalLock(cwd) {
|
|
|
1112
1112
|
async function writeLocalLock(lock, cwd) {
|
|
1113
1113
|
const lockDir = cwd || process.cwd();
|
|
1114
1114
|
const lockPath = getLocalLockPath(lockDir);
|
|
1115
|
-
const sortedSkills = {
|
|
1116
|
-
for (const key of Object.keys(lock.skills).sort()) {
|
|
1115
|
+
const sortedSkills = Object.fromEntries(Object.keys(lock.skills).sort().map((key) => {
|
|
1117
1116
|
const entry = lock.skills[key];
|
|
1118
|
-
|
|
1117
|
+
return [key, entry.sourceType === "local" ? {
|
|
1119
1118
|
...entry,
|
|
1120
1119
|
source: getPortableLocalSource(entry.source, lockDir)
|
|
1121
|
-
} : entry;
|
|
1122
|
-
}
|
|
1120
|
+
} : entry];
|
|
1121
|
+
}));
|
|
1123
1122
|
const sorted = {
|
|
1124
1123
|
version: lock.version,
|
|
1125
1124
|
skills: sortedSkills
|
|
@@ -1165,12 +1164,15 @@ async function collectFiles(baseDir, currentDir, results) {
|
|
|
1165
1164
|
}
|
|
1166
1165
|
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
1167
1166
|
const lock = await readLocalLock(cwd);
|
|
1168
|
-
lock.skills
|
|
1167
|
+
lock.skills = {
|
|
1168
|
+
...lock.skills,
|
|
1169
|
+
[skillName]: entry
|
|
1170
|
+
};
|
|
1169
1171
|
await writeLocalLock(lock, cwd);
|
|
1170
1172
|
}
|
|
1171
1173
|
async function removeSkillFromLocalLock(skillName, cwd) {
|
|
1172
1174
|
const lock = await readLocalLock(cwd);
|
|
1173
|
-
if (!(
|
|
1175
|
+
if (!Object.hasOwn(lock.skills, skillName)) return false;
|
|
1174
1176
|
delete lock.skills[skillName];
|
|
1175
1177
|
await writeLocalLock(lock, cwd);
|
|
1176
1178
|
return true;
|
|
@@ -1572,7 +1574,7 @@ const agents = {
|
|
|
1572
1574
|
name: "codex",
|
|
1573
1575
|
displayName: "Codex",
|
|
1574
1576
|
skillsDir: ".agents/skills",
|
|
1575
|
-
globalSkillsDir: join(
|
|
1577
|
+
globalSkillsDir: join(home, ".agents", "skills"),
|
|
1576
1578
|
detectInstalled: async () => {
|
|
1577
1579
|
return existsSync(codexHome) || existsSync("/etc/codex");
|
|
1578
1580
|
}
|
|
@@ -1918,8 +1920,8 @@ const agents = {
|
|
|
1918
1920
|
pi: {
|
|
1919
1921
|
name: "pi",
|
|
1920
1922
|
displayName: "Pi",
|
|
1921
|
-
skillsDir: ".
|
|
1922
|
-
globalSkillsDir: join(home, ".
|
|
1923
|
+
skillsDir: ".agents/skills",
|
|
1924
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
1923
1925
|
detectInstalled: async () => {
|
|
1924
1926
|
return existsSync(join(home, ".pi/agent"));
|
|
1925
1927
|
}
|
|
@@ -2320,22 +2322,29 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
2320
2322
|
success: true,
|
|
2321
2323
|
path: canonicalDir,
|
|
2322
2324
|
canonicalPath: canonicalDir,
|
|
2323
|
-
mode:
|
|
2325
|
+
mode: installMode,
|
|
2324
2326
|
skipped: true
|
|
2325
2327
|
};
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
+
if (installMode === "link") {
|
|
2329
|
+
if (!await createSymlink(skill.path, canonicalDir)) {
|
|
2330
|
+
await cleanAndCreateDirectory(canonicalDir);
|
|
2331
|
+
await copyDirectory(skill.path, canonicalDir, agentType);
|
|
2332
|
+
}
|
|
2333
|
+
} else {
|
|
2334
|
+
await cleanAndCreateDirectory(canonicalDir);
|
|
2335
|
+
await copyDirectory(skill.path, canonicalDir, agentType);
|
|
2336
|
+
}
|
|
2328
2337
|
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
2329
2338
|
success: true,
|
|
2330
2339
|
path: canonicalDir,
|
|
2331
2340
|
canonicalPath: canonicalDir,
|
|
2332
|
-
mode:
|
|
2341
|
+
mode: installMode
|
|
2333
2342
|
};
|
|
2334
2343
|
if (shouldSkipProjectAgentSymlink(agentType, isGlobal, cwd, options.createMissingAgentRoot ?? false)) return {
|
|
2335
2344
|
success: true,
|
|
2336
2345
|
path: canonicalDir,
|
|
2337
2346
|
canonicalPath: canonicalDir,
|
|
2338
|
-
mode:
|
|
2347
|
+
mode: installMode,
|
|
2339
2348
|
skipped: true,
|
|
2340
2349
|
skipReason: "missing-agent-project-directory"
|
|
2341
2350
|
};
|
|
@@ -2346,7 +2355,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
2346
2355
|
success: true,
|
|
2347
2356
|
path: agentDir,
|
|
2348
2357
|
canonicalPath: canonicalDir,
|
|
2349
|
-
mode:
|
|
2358
|
+
mode: installMode,
|
|
2350
2359
|
symlinkFailed: true
|
|
2351
2360
|
};
|
|
2352
2361
|
}
|
|
@@ -2354,7 +2363,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
2354
2363
|
success: true,
|
|
2355
2364
|
path: agentDir,
|
|
2356
2365
|
canonicalPath: canonicalDir,
|
|
2357
|
-
mode:
|
|
2366
|
+
mode: installMode
|
|
2358
2367
|
};
|
|
2359
2368
|
} catch (error) {
|
|
2360
2369
|
return {
|
|
@@ -2785,8 +2794,8 @@ async function listInstalledSkills(options = {}) {
|
|
|
2785
2794
|
} catch {}
|
|
2786
2795
|
return Array.from(skillsMap.values());
|
|
2787
2796
|
}
|
|
2788
|
-
const TELEMETRY_URL = "https://
|
|
2789
|
-
const AUDIT_URL = "https://
|
|
2797
|
+
const TELEMETRY_URL = "https://www.skills.sh/tele/t";
|
|
2798
|
+
const AUDIT_URL = "https://www.skills.sh/tele/audit";
|
|
2790
2799
|
let cliVersion = null;
|
|
2791
2800
|
let detectedAgentName = null;
|
|
2792
2801
|
function setDetectedAgent(agentName) {
|
|
@@ -4172,7 +4181,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
|
|
|
4172
4181
|
tree
|
|
4173
4182
|
};
|
|
4174
4183
|
}
|
|
4175
|
-
var version$1 = "1.7.
|
|
4184
|
+
var version$1 = "1.7.2";
|
|
4176
4185
|
const NOTION_API_VERSION = "2026-03-11";
|
|
4177
4186
|
const NTN_TIMEOUT_MS = 3e4;
|
|
4178
4187
|
const NTN_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
@@ -4376,18 +4385,18 @@ async function fetchNotionSkillDirectory(pageId, options = {}) {
|
|
|
4376
4385
|
}
|
|
4377
4386
|
async function prepareNotionSkillSource(pageId, options = {}) {
|
|
4378
4387
|
const download = options.download ?? downloadNotionDirectory;
|
|
4379
|
-
const spinner$
|
|
4380
|
-
spinner$
|
|
4388
|
+
const spinner$5 = spinner();
|
|
4389
|
+
spinner$5.start("Fetching Notion skill with ntn…");
|
|
4381
4390
|
try {
|
|
4382
4391
|
const [directory, workspace] = await Promise.all([fetchNotionSkillDirectory(pageId, { runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
|
|
4383
4392
|
const downloaded = await download(directory.url);
|
|
4384
|
-
spinner$
|
|
4393
|
+
spinner$5.stop(`Downloaded skill from Notion${inWorkspace(workspace)}`);
|
|
4385
4394
|
return {
|
|
4386
4395
|
rootDir: downloaded.rootDir,
|
|
4387
4396
|
tempDir: downloaded.tempDir
|
|
4388
4397
|
};
|
|
4389
4398
|
} catch (error) {
|
|
4390
|
-
spinner$
|
|
4399
|
+
spinner$5.stop(import_picocolors.default.red("Failed to prepare Notion skill"));
|
|
4391
4400
|
throw error;
|
|
4392
4401
|
}
|
|
4393
4402
|
}
|
|
@@ -4411,17 +4420,17 @@ function printAvailablePacks(packs) {
|
|
|
4411
4420
|
}
|
|
4412
4421
|
}
|
|
4413
4422
|
async function prepareNotionPackSource(options = {}) {
|
|
4414
|
-
const spinner$
|
|
4415
|
-
spinner$
|
|
4423
|
+
const spinner$6 = spinner();
|
|
4424
|
+
spinner$6.start("Fetching Notion packs with ntn…");
|
|
4416
4425
|
let packs;
|
|
4417
4426
|
let workspace = null;
|
|
4418
4427
|
try {
|
|
4419
4428
|
[packs, workspace] = await Promise.all([fetchNotionPacks({ runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
|
|
4420
4429
|
} catch (error) {
|
|
4421
|
-
spinner$
|
|
4430
|
+
spinner$6.stop(import_picocolors.default.red("Failed to load Notion packs"));
|
|
4422
4431
|
throw error;
|
|
4423
4432
|
}
|
|
4424
|
-
spinner$
|
|
4433
|
+
spinner$6.stop(`Found ${import_picocolors.default.green(packs.length)} Notion pack${packs.length === 1 ? "" : "s"}${inWorkspace(workspace)}`);
|
|
4425
4434
|
if (packs.length === 0) throw new Error("Notion returned no packs for the authenticated workspace");
|
|
4426
4435
|
if (options.list) {
|
|
4427
4436
|
printAvailablePacks(packs);
|
|
@@ -4460,7 +4469,7 @@ async function prepareNotionPackSource(options = {}) {
|
|
|
4460
4469
|
const download = options.download ?? downloadNotionDirectory;
|
|
4461
4470
|
const discover = options.discover ?? discoverSkills;
|
|
4462
4471
|
let skillCount = 0;
|
|
4463
|
-
spinner$
|
|
4472
|
+
spinner$6.start("Preparing selected Notion packs…");
|
|
4464
4473
|
try {
|
|
4465
4474
|
await mkdir$1(packsDir, { recursive: true });
|
|
4466
4475
|
for (const [index, pack] of selectedPacks.entries()) {
|
|
@@ -4501,7 +4510,7 @@ async function prepareNotionPackSource(options = {}) {
|
|
|
4501
4510
|
metadata: { pluginRoot: "./packs" },
|
|
4502
4511
|
plugins: manifestPlugins
|
|
4503
4512
|
}, null, 2)}\n`, "utf8");
|
|
4504
|
-
spinner$
|
|
4513
|
+
spinner$6.stop(`Prepared ${import_picocolors.default.green(selectedPacks.length)} Notion pack${selectedPacks.length === 1 ? "" : "s"} with ${import_picocolors.default.green(skillCount)} skill${skillCount === 1 ? "" : "s"}`);
|
|
4505
4514
|
return {
|
|
4506
4515
|
rootDir: stagingDir,
|
|
4507
4516
|
tempDir: stagingDir,
|
|
@@ -4509,7 +4518,7 @@ async function prepareNotionPackSource(options = {}) {
|
|
|
4509
4518
|
skillCount
|
|
4510
4519
|
};
|
|
4511
4520
|
} catch (error) {
|
|
4512
|
-
spinner$
|
|
4521
|
+
spinner$6.stop(import_picocolors.default.red("Failed to prepare Notion packs"));
|
|
4513
4522
|
await cleanupTempDir(stagingDir).catch(() => {});
|
|
4514
4523
|
throw error;
|
|
4515
4524
|
}
|
|
@@ -4685,7 +4694,8 @@ async function promptForAgents(message, choices) {
|
|
|
4685
4694
|
const defaultValues = [
|
|
4686
4695
|
"claude-code",
|
|
4687
4696
|
"opencode",
|
|
4688
|
-
"codex"
|
|
4697
|
+
"codex",
|
|
4698
|
+
"grok"
|
|
4689
4699
|
].filter((a) => validAgents.includes(a));
|
|
4690
4700
|
let initialValues = [];
|
|
4691
4701
|
if (lastSelected && lastSelected.length > 0) initialValues = lastSelected.filter((a) => validAgents.includes(a));
|
|
@@ -5048,6 +5058,182 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
5048
5058
|
await promptForFindSkills(options, targetAgents);
|
|
5049
5059
|
return true;
|
|
5050
5060
|
}
|
|
5061
|
+
async function resolveSkills(parsed, options, spinner) {
|
|
5062
|
+
const { includeInternal } = options;
|
|
5063
|
+
let skills;
|
|
5064
|
+
let blobResult = null;
|
|
5065
|
+
let tempDir = null;
|
|
5066
|
+
try {
|
|
5067
|
+
if (parsed.type === "local") {
|
|
5068
|
+
spinner.start("Discovering skills…");
|
|
5069
|
+
skills = await discoverSkills(parsed.localPath, parsed.subpath, {
|
|
5070
|
+
includeInternal,
|
|
5071
|
+
fullDepth: options.fullDepth
|
|
5072
|
+
});
|
|
5073
|
+
} else if (parsed.type === "well-known" || parsed.type === "download") {
|
|
5074
|
+
spinner.start("Downloading source...");
|
|
5075
|
+
const downloaded = await downloadSource(parsed.url);
|
|
5076
|
+
tempDir = downloaded.tempDir;
|
|
5077
|
+
spinner.stop(`Downloaded ${downloaded.kind === "skill-md" ? "SKILL.md file" : "archive"}`);
|
|
5078
|
+
spinner.start("Discovering skills...");
|
|
5079
|
+
skills = await discoverSkills(downloaded.rootDir, parsed.subpath, {
|
|
5080
|
+
includeInternal,
|
|
5081
|
+
fullDepth: options.fullDepth
|
|
5082
|
+
});
|
|
5083
|
+
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
5084
|
+
let attemptedBlobInstall = false;
|
|
5085
|
+
const BLOB_ALLOWED_OWNERS = [
|
|
5086
|
+
"vercel",
|
|
5087
|
+
"vercel-labs",
|
|
5088
|
+
"heygen-com",
|
|
5089
|
+
"remotion-dev"
|
|
5090
|
+
];
|
|
5091
|
+
const ownerRepo = getOwnerRepo(parsed);
|
|
5092
|
+
const owner = ownerRepo?.split("/")[0]?.toLowerCase();
|
|
5093
|
+
const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
|
|
5094
|
+
if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
|
|
5095
|
+
attemptedBlobInstall = true;
|
|
5096
|
+
spinner.start("Fetching skills…");
|
|
5097
|
+
blobResult = await tryBlobInstall(ownerRepo, {
|
|
5098
|
+
subpath: parsed.subpath,
|
|
5099
|
+
skillFilter: parsed.skillFilter,
|
|
5100
|
+
ref: parsed.ref,
|
|
5101
|
+
getToken: getGitHubToken,
|
|
5102
|
+
includeInternal
|
|
5103
|
+
});
|
|
5104
|
+
}
|
|
5105
|
+
if (blobResult) {
|
|
5106
|
+
skills = blobResult.skills;
|
|
5107
|
+
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
5108
|
+
} else {
|
|
5109
|
+
if (attemptedBlobInstall) spinner.message("Cloning repository…");
|
|
5110
|
+
else spinner.start("Cloning repository…");
|
|
5111
|
+
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
5112
|
+
spinner.stop("Repository cloned");
|
|
5113
|
+
spinner.start("Discovering skills…");
|
|
5114
|
+
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
5115
|
+
includeInternal,
|
|
5116
|
+
fullDepth: options.fullDepth
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
} else {
|
|
5120
|
+
spinner.start("Cloning repository…");
|
|
5121
|
+
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
5122
|
+
spinner.stop("Repository cloned");
|
|
5123
|
+
spinner.start("Discovering skills…");
|
|
5124
|
+
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
5125
|
+
includeInternal,
|
|
5126
|
+
fullDepth: options.fullDepth
|
|
5127
|
+
});
|
|
5128
|
+
}
|
|
5129
|
+
} catch (error) {
|
|
5130
|
+
await cleanup(tempDir);
|
|
5131
|
+
throw error;
|
|
5132
|
+
}
|
|
5133
|
+
return {
|
|
5134
|
+
skills,
|
|
5135
|
+
tempDir,
|
|
5136
|
+
blobResult
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
5139
|
+
async function installToTargets(resolved, skills, targets, options) {
|
|
5140
|
+
const results = [];
|
|
5141
|
+
for (const skill of skills) for (const target of targets) {
|
|
5142
|
+
const { agent, subagent } = target;
|
|
5143
|
+
const installOptions = {
|
|
5144
|
+
global: options.global,
|
|
5145
|
+
mode: options.mode,
|
|
5146
|
+
eveSubagent: subagent,
|
|
5147
|
+
createMissingAgentRoot: options.createMissingAgentRoot(agent)
|
|
5148
|
+
};
|
|
5149
|
+
let result;
|
|
5150
|
+
if (resolved.blobResult && "files" in skill) {
|
|
5151
|
+
const blobSkill = skill;
|
|
5152
|
+
result = await installBlobSkillForAgent({
|
|
5153
|
+
installName: blobSkill.name,
|
|
5154
|
+
files: blobSkill.files
|
|
5155
|
+
}, agent, installOptions);
|
|
5156
|
+
} else result = await installSkillForAgent(skill, agent, installOptions);
|
|
5157
|
+
results.push({
|
|
5158
|
+
skill: getSkillDisplayName(skill),
|
|
5159
|
+
agent: targetDisplayName(target),
|
|
5160
|
+
pluginName: skill.pluginName,
|
|
5161
|
+
...result
|
|
5162
|
+
});
|
|
5163
|
+
}
|
|
5164
|
+
return results;
|
|
5165
|
+
}
|
|
5166
|
+
function getSkillRepoPaths(resolved, skills) {
|
|
5167
|
+
const { tempDir, blobResult } = resolved;
|
|
5168
|
+
const skillFiles = {};
|
|
5169
|
+
for (const skill of skills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
5170
|
+
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
5171
|
+
else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
|
|
5172
|
+
return skillFiles;
|
|
5173
|
+
}
|
|
5174
|
+
function getProjectLockSource(parsed) {
|
|
5175
|
+
return getLockSource(parsed.url, getOwnerRepo(parsed)) || parsed.url;
|
|
5176
|
+
}
|
|
5177
|
+
function projectLockEntry(parsed, skillPath, computedHash) {
|
|
5178
|
+
const sourceUrl = getProjectLockSourceUrl(parsed.type, parsed.url);
|
|
5179
|
+
return {
|
|
5180
|
+
source: getProjectLockSource(parsed),
|
|
5181
|
+
...sourceUrl && { sourceUrl },
|
|
5182
|
+
ref: parsed.ref,
|
|
5183
|
+
sourceType: parsed.type,
|
|
5184
|
+
...skillPath && { skillPath },
|
|
5185
|
+
computedHash
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
async function installFromSource(parsed, options) {
|
|
5189
|
+
const spinner$4 = spinner();
|
|
5190
|
+
let resolved = null;
|
|
5191
|
+
try {
|
|
5192
|
+
resolved = await resolveSkills(parsed, { includeInternal: options.skills.length > 0 }, spinner$4);
|
|
5193
|
+
let selected = options.skills.length > 0 ? filterSkills(resolved.skills, options.skills) : resolved.skills;
|
|
5194
|
+
if (selected.length === 0) {
|
|
5195
|
+
spinner$4.stop(import_picocolors.default.red("No matching skills found"));
|
|
5196
|
+
return {
|
|
5197
|
+
installed: [],
|
|
5198
|
+
failed: [],
|
|
5199
|
+
error: "No matching skills found"
|
|
5200
|
+
};
|
|
5201
|
+
}
|
|
5202
|
+
spinner$4.stop(`Found ${import_picocolors.default.green(selected.length)} skill${selected.length > 1 ? "s" : ""}`);
|
|
5203
|
+
if (options.select) selected = await options.select(selected);
|
|
5204
|
+
const results = await installToTargets(resolved, selected, options.agents.map((agent) => ({ agent })), {
|
|
5205
|
+
global: false,
|
|
5206
|
+
mode: "symlink",
|
|
5207
|
+
createMissingAgentRoot: () => true
|
|
5208
|
+
});
|
|
5209
|
+
const installed = new Set(results.filter((r) => r.success).map((r) => r.skill));
|
|
5210
|
+
const repoPaths = getSkillRepoPaths(resolved, selected);
|
|
5211
|
+
for (const skill of selected) {
|
|
5212
|
+
if (!installed.has(getSkillDisplayName(skill))) continue;
|
|
5213
|
+
const entry = projectLockEntry(parsed, repoPaths[skill.name], await sourceSkillHash(skill));
|
|
5214
|
+
await addSkillToLocalLock(skill.name, {
|
|
5215
|
+
...entry,
|
|
5216
|
+
...options.via && { via: options.via }
|
|
5217
|
+
});
|
|
5218
|
+
}
|
|
5219
|
+
return {
|
|
5220
|
+
installed: [...installed],
|
|
5221
|
+
failed: results.filter((r) => !r.success).map((r) => `${r.skill} → ${r.agent}: ${r.error}`)
|
|
5222
|
+
};
|
|
5223
|
+
} catch (error) {
|
|
5224
|
+
spinner$4.stop(import_picocolors.default.red("Installation failed"));
|
|
5225
|
+
return {
|
|
5226
|
+
installed: [],
|
|
5227
|
+
failed: [],
|
|
5228
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5229
|
+
};
|
|
5230
|
+
} finally {
|
|
5231
|
+
await cleanup(resolved?.tempDir ?? null);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
function sourceSkillHash(skill) {
|
|
5235
|
+
return "snapshotHash" in skill ? Promise.resolve(skill.snapshotHash) : computeSkillFolderHash(skill.path);
|
|
5236
|
+
}
|
|
5051
5237
|
async function runAdd(args, options = {}) {
|
|
5052
5238
|
const source = args[0];
|
|
5053
5239
|
let installTipShown = false;
|
|
@@ -5162,8 +5348,6 @@ async function runAdd(args, options = {}) {
|
|
|
5162
5348
|
if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
|
|
5163
5349
|
}
|
|
5164
5350
|
const includeInternal = !!(options.skill && options.skill.length > 0 && !options.skill.includes("*"));
|
|
5165
|
-
let skills;
|
|
5166
|
-
let blobResult = null;
|
|
5167
5351
|
if (parsed.type === "local") {
|
|
5168
5352
|
spinner$3.start("Validating local path…");
|
|
5169
5353
|
if (!existsSync(parsed.localPath)) {
|
|
@@ -5172,67 +5356,13 @@ async function runAdd(args, options = {}) {
|
|
|
5172
5356
|
emitJsonAndExit(1, `Local path does not exist: ${parsed.localPath}`);
|
|
5173
5357
|
}
|
|
5174
5358
|
spinner$3.stop("Local path validated");
|
|
5175
|
-
spinner$3.start("Discovering skills…");
|
|
5176
|
-
skills = await discoverSkills(parsed.localPath, parsed.subpath, {
|
|
5177
|
-
includeInternal,
|
|
5178
|
-
fullDepth: options.fullDepth
|
|
5179
|
-
});
|
|
5180
|
-
} else if (parsed.type === "well-known" || parsed.type === "download") {
|
|
5181
|
-
spinner$3.start("Downloading source...");
|
|
5182
|
-
const downloaded = await downloadSource(parsed.url);
|
|
5183
|
-
tempDir = downloaded.tempDir;
|
|
5184
|
-
spinner$3.stop(`Downloaded ${downloaded.kind === "skill-md" ? "SKILL.md file" : "archive"}`);
|
|
5185
|
-
spinner$3.start("Discovering skills...");
|
|
5186
|
-
skills = await discoverSkills(downloaded.rootDir, parsed.subpath, {
|
|
5187
|
-
includeInternal,
|
|
5188
|
-
fullDepth: options.fullDepth
|
|
5189
|
-
});
|
|
5190
|
-
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
5191
|
-
let attemptedBlobInstall = false;
|
|
5192
|
-
const BLOB_ALLOWED_OWNERS = [
|
|
5193
|
-
"vercel",
|
|
5194
|
-
"vercel-labs",
|
|
5195
|
-
"heygen-com",
|
|
5196
|
-
"remotion-dev"
|
|
5197
|
-
];
|
|
5198
|
-
const ownerRepo = getOwnerRepo(parsed);
|
|
5199
|
-
const owner = ownerRepo?.split("/")[0]?.toLowerCase();
|
|
5200
|
-
const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
|
|
5201
|
-
if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
|
|
5202
|
-
attemptedBlobInstall = true;
|
|
5203
|
-
spinner$3.start("Fetching skills…");
|
|
5204
|
-
blobResult = await tryBlobInstall(ownerRepo, {
|
|
5205
|
-
subpath: parsed.subpath,
|
|
5206
|
-
skillFilter: parsed.skillFilter,
|
|
5207
|
-
ref: parsed.ref,
|
|
5208
|
-
getToken: getGitHubToken,
|
|
5209
|
-
includeInternal
|
|
5210
|
-
});
|
|
5211
|
-
}
|
|
5212
|
-
if (blobResult) {
|
|
5213
|
-
skills = blobResult.skills;
|
|
5214
|
-
spinner$3.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
5215
|
-
} else {
|
|
5216
|
-
if (attemptedBlobInstall) spinner$3.message("Cloning repository…");
|
|
5217
|
-
else spinner$3.start("Cloning repository…");
|
|
5218
|
-
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
5219
|
-
spinner$3.stop("Repository cloned");
|
|
5220
|
-
spinner$3.start("Discovering skills…");
|
|
5221
|
-
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
5222
|
-
includeInternal,
|
|
5223
|
-
fullDepth: options.fullDepth
|
|
5224
|
-
});
|
|
5225
|
-
}
|
|
5226
|
-
} else {
|
|
5227
|
-
spinner$3.start("Cloning repository…");
|
|
5228
|
-
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
5229
|
-
spinner$3.stop("Repository cloned");
|
|
5230
|
-
spinner$3.start("Discovering skills…");
|
|
5231
|
-
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
5232
|
-
includeInternal,
|
|
5233
|
-
fullDepth: options.fullDepth
|
|
5234
|
-
});
|
|
5235
5359
|
}
|
|
5360
|
+
const resolved = await resolveSkills(parsed, {
|
|
5361
|
+
includeInternal,
|
|
5362
|
+
fullDepth: options.fullDepth
|
|
5363
|
+
}, spinner$3);
|
|
5364
|
+
const { skills, blobResult } = resolved;
|
|
5365
|
+
tempDir = resolved.tempDir ?? tempDir;
|
|
5236
5366
|
if (skills.length === 0) {
|
|
5237
5367
|
spinner$3.stop(import_picocolors.default.red("No skills found"));
|
|
5238
5368
|
outro(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
@@ -5554,47 +5684,19 @@ async function runAdd(args, options = {}) {
|
|
|
5554
5684
|
}
|
|
5555
5685
|
}
|
|
5556
5686
|
spinner$3.start("Installing skills…");
|
|
5557
|
-
const results =
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
const blobSkill = skill;
|
|
5563
|
-
result = await installBlobSkillForAgent({
|
|
5564
|
-
installName: blobSkill.name,
|
|
5565
|
-
files: blobSkill.files
|
|
5566
|
-
}, agent, {
|
|
5567
|
-
global: installGlobally,
|
|
5568
|
-
mode: installMode,
|
|
5569
|
-
eveSubagent: subagent,
|
|
5570
|
-
createMissingAgentRoot: explicitlySelectedAgents.has(agent)
|
|
5571
|
-
});
|
|
5572
|
-
} else result = await installSkillForAgent(skill, agent, {
|
|
5573
|
-
global: installGlobally,
|
|
5574
|
-
mode: installMode,
|
|
5575
|
-
eveSubagent: subagent,
|
|
5576
|
-
createMissingAgentRoot: explicitlySelectedAgents.has(agent)
|
|
5577
|
-
});
|
|
5578
|
-
results.push({
|
|
5579
|
-
skill: getSkillDisplayName(skill),
|
|
5580
|
-
agent: targetDisplayName(target),
|
|
5581
|
-
pluginName: skill.pluginName,
|
|
5582
|
-
...result
|
|
5583
|
-
});
|
|
5584
|
-
}
|
|
5687
|
+
const results = await installToTargets(resolved, selectedSkills, installTargets, {
|
|
5688
|
+
global: installGlobally,
|
|
5689
|
+
mode: installMode,
|
|
5690
|
+
createMissingAgentRoot: (agent) => explicitlySelectedAgents.has(agent)
|
|
5691
|
+
});
|
|
5585
5692
|
spinner$3.stop("Installation complete");
|
|
5586
5693
|
console.log();
|
|
5587
5694
|
const successful = results.filter((r) => r.success);
|
|
5588
5695
|
const failed = results.filter((r) => !r.success);
|
|
5589
5696
|
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
5590
|
-
const skillFiles =
|
|
5591
|
-
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
5592
|
-
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
5593
|
-
else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
|
|
5594
|
-
else continue;
|
|
5697
|
+
const skillFiles = getSkillRepoPaths(resolved, selectedSkills);
|
|
5595
5698
|
const normalizedSource = directDownload ? null : getOwnerRepo(parsed);
|
|
5596
5699
|
const lockSource = directDownload ? null : getLockSource(parsed.url, normalizedSource);
|
|
5597
|
-
const projectLockSourceUrl = directDownload ? void 0 : getProjectLockSourceUrl(parsed.type, parsed.url);
|
|
5598
5700
|
if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
|
|
5599
5701
|
if (await repoPrivacyPromise === false) track({
|
|
5600
5702
|
event: "install",
|
|
@@ -5619,13 +5721,12 @@ async function runAdd(args, options = {}) {
|
|
|
5619
5721
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
5620
5722
|
if (!successfulSkillNames.has(skillDisplayName)) continue;
|
|
5621
5723
|
try {
|
|
5622
|
-
|
|
5623
|
-
installedSkillHashes.set(skillDisplayName, computedHash);
|
|
5724
|
+
installedSkillHashes.set(skillDisplayName, await sourceSkillHash(skill));
|
|
5624
5725
|
} catch {}
|
|
5625
5726
|
}
|
|
5626
|
-
if (successful.length > 0 && installGlobally && normalizedSource) {
|
|
5727
|
+
if (successful.length > 0 && installGlobally && (normalizedSource || parsed.type === "local" && !directDownload)) {
|
|
5627
5728
|
let cachedTree;
|
|
5628
|
-
if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
|
|
5729
|
+
if (parsed.type === "github" && !blobResult && normalizedSource) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
|
|
5629
5730
|
for (const skill of selectedSkills) {
|
|
5630
5731
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
5631
5732
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
@@ -5640,9 +5741,12 @@ async function runAdd(args, options = {}) {
|
|
|
5640
5741
|
} else if (skillPathValue && tempDir) {
|
|
5641
5742
|
const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
|
|
5642
5743
|
if (hash) skillFolderHash = hash;
|
|
5744
|
+
} else if (parsed.type === "local") {
|
|
5745
|
+
const hash = await computeSkillFolderHash(skill.path);
|
|
5746
|
+
if (hash) skillFolderHash = hash;
|
|
5643
5747
|
}
|
|
5644
5748
|
await addSkillToLock(skill.name, {
|
|
5645
|
-
source: lockSource || normalizedSource,
|
|
5749
|
+
source: lockSource || normalizedSource || parsed.url,
|
|
5646
5750
|
sourceType: parsed.type,
|
|
5647
5751
|
sourceUrl: parsed.url,
|
|
5648
5752
|
ref: parsed.ref,
|
|
@@ -5663,12 +5767,7 @@ async function runAdd(args, options = {}) {
|
|
|
5663
5767
|
if (computedHash === void 0) continue;
|
|
5664
5768
|
const skillPathValue = skillFiles[skill.name];
|
|
5665
5769
|
await addSkillToLocalLock(skill.name, {
|
|
5666
|
-
|
|
5667
|
-
...projectLockSourceUrl && { sourceUrl: projectLockSourceUrl },
|
|
5668
|
-
ref: parsed.ref,
|
|
5669
|
-
sourceType: parsed.type,
|
|
5670
|
-
...skillPathValue && { skillPath: skillPathValue },
|
|
5671
|
-
computedHash,
|
|
5770
|
+
...projectLockEntry(parsed, skillPathValue, computedHash),
|
|
5672
5771
|
...recordSubagents && { subagents: eveSubagents }
|
|
5673
5772
|
}, cwd);
|
|
5674
5773
|
} catch {}
|
|
@@ -6170,76 +6269,369 @@ ${DIM$3} 2) npx skills add <owner/repo@skill>${RESET$3}`;
|
|
|
6170
6269
|
else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
|
|
6171
6270
|
console.log();
|
|
6172
6271
|
}
|
|
6272
|
+
const NPM_PREFIX = "npm:";
|
|
6273
|
+
const REMOTE_SOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
6274
|
+
"github",
|
|
6275
|
+
"gitlab",
|
|
6276
|
+
"git"
|
|
6277
|
+
]);
|
|
6278
|
+
function isSkillsFieldEntry(value) {
|
|
6279
|
+
if (typeof value === "string") return true;
|
|
6280
|
+
if (!value || typeof value !== "object" || !("source" in value)) return false;
|
|
6281
|
+
if (typeof value.source !== "string") return false;
|
|
6282
|
+
const skills = "skills" in value ? value.skills : void 0;
|
|
6283
|
+
const ref = "ref" in value ? value.ref : void 0;
|
|
6284
|
+
return (skills === void 0 || Array.isArray(skills) && skills.every((s) => typeof s === "string")) && (ref === void 0 || typeof ref === "string");
|
|
6285
|
+
}
|
|
6286
|
+
function parseSkillsField(entries, declarer) {
|
|
6287
|
+
const parsed = {
|
|
6288
|
+
npm: [],
|
|
6289
|
+
remote: [],
|
|
6290
|
+
errors: []
|
|
6291
|
+
};
|
|
6292
|
+
for (const raw of entries) {
|
|
6293
|
+
if (!isSkillsFieldEntry(raw)) {
|
|
6294
|
+
parsed.errors.push(`${declarer}: invalid "skills" entry ${JSON.stringify(raw)}`);
|
|
6295
|
+
continue;
|
|
6296
|
+
}
|
|
6297
|
+
const { source, skills = [], ref } = typeof raw === "string" ? { source: raw } : raw;
|
|
6298
|
+
if (!source.startsWith(NPM_PREFIX)) {
|
|
6299
|
+
const remote = parseSource(source);
|
|
6300
|
+
if (!REMOTE_SOURCE_TYPES.has(remote.type)) parsed.errors.push(`${declarer}: "${source}" is not a git source`);
|
|
6301
|
+
else if (ref !== void 0 && remote.ref !== void 0) parsed.errors.push(`${declarer}: "${source}" already has a ref; remove "ref"`);
|
|
6302
|
+
else parsed.remote.push({
|
|
6303
|
+
parsed: {
|
|
6304
|
+
...remote,
|
|
6305
|
+
ref: ref ?? remote.ref
|
|
6306
|
+
},
|
|
6307
|
+
skills: remote.skillFilter ? [...skills, remote.skillFilter] : skills
|
|
6308
|
+
});
|
|
6309
|
+
} else if (ref !== void 0) parsed.errors.push(`${declarer}: "ref" cannot be used with "${source}"`);
|
|
6310
|
+
else parsed.npm.push({
|
|
6311
|
+
package: source.slice(4),
|
|
6312
|
+
skills
|
|
6313
|
+
});
|
|
6314
|
+
}
|
|
6315
|
+
return parsed;
|
|
6316
|
+
}
|
|
6173
6317
|
const isCancelled = (value) => typeof value === "symbol";
|
|
6318
|
+
function matchesSkill(skill, patterns) {
|
|
6319
|
+
return patterns.some((pattern) => {
|
|
6320
|
+
const hash = pattern.indexOf("#");
|
|
6321
|
+
const packagePattern = hash === -1 ? pattern : pattern.slice(0, hash);
|
|
6322
|
+
const skillPattern = hash === -1 ? "**" : pattern.slice(hash + 1);
|
|
6323
|
+
return posix.matchesGlob(skill.packageName, packagePattern) && posix.matchesGlob(sanitizeName(skill.name), skillPattern);
|
|
6324
|
+
});
|
|
6325
|
+
}
|
|
6174
6326
|
function shortenPath$1(fullPath, cwd) {
|
|
6175
6327
|
const home = homedir();
|
|
6176
6328
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
6177
6329
|
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
6178
6330
|
return fullPath;
|
|
6179
6331
|
}
|
|
6180
|
-
async function
|
|
6181
|
-
const nodeModulesDir = join(cwd, "node_modules");
|
|
6182
|
-
const skills = [];
|
|
6183
|
-
let topNames;
|
|
6332
|
+
async function readPackageJson(dir) {
|
|
6184
6333
|
try {
|
|
6185
|
-
|
|
6334
|
+
return JSON.parse(await readFile(join(dir, "package.json"), "utf-8"));
|
|
6186
6335
|
} catch {
|
|
6187
|
-
return
|
|
6188
|
-
}
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6336
|
+
return null;
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
async function discoverPackageSkills(pkgDir, packageName, depth) {
|
|
6340
|
+
const pkg = await readPackageJson(pkgDir);
|
|
6341
|
+
if (!pkg) return [];
|
|
6342
|
+
const rootSkill = await hasSkillMd(pkgDir) ? await parseSkillMd(join(pkgDir, "SKILL.md")) : null;
|
|
6343
|
+
if (rootSkill) return [{
|
|
6344
|
+
...rootSkill,
|
|
6345
|
+
packageName,
|
|
6346
|
+
packageVersion: pkg.version,
|
|
6347
|
+
skillPath: "SKILL.md",
|
|
6348
|
+
depth
|
|
6349
|
+
}];
|
|
6350
|
+
const skills = [];
|
|
6351
|
+
for (const dir of ["skills", "dist/skills"]) for (const name of await readdir(join(pkgDir, dir)).catch(() => [])) {
|
|
6352
|
+
const skillDir = join(pkgDir, dir, name);
|
|
6353
|
+
if (!await hasSkillMd(skillDir)) continue;
|
|
6354
|
+
const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
|
|
6355
|
+
if (skill) skills.push({
|
|
6356
|
+
...skill,
|
|
6357
|
+
packageName,
|
|
6358
|
+
packageVersion: pkg.version,
|
|
6359
|
+
skillPath: `${dir}/${name}/SKILL.md`,
|
|
6360
|
+
depth
|
|
6361
|
+
});
|
|
6362
|
+
}
|
|
6363
|
+
return skills;
|
|
6364
|
+
}
|
|
6365
|
+
function findInstalledPackage(from, name) {
|
|
6366
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
6367
|
+
const candidate = join(dir, "node_modules", name);
|
|
6368
|
+
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
6369
|
+
if (dirname(dir) === dir) return void 0;
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
const DEPENDENCY_FIELDS = [
|
|
6373
|
+
"dependencies",
|
|
6374
|
+
"devDependencies",
|
|
6375
|
+
"optionalDependencies",
|
|
6376
|
+
"peerDependencies"
|
|
6377
|
+
];
|
|
6378
|
+
async function findWorkspacePackages(cwd, pkg) {
|
|
6379
|
+
const patterns = [...Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages ?? []];
|
|
6380
|
+
try {
|
|
6381
|
+
const pnpm = parse(await readFile(join(cwd, "pnpm-workspace.yaml"), "utf-8"));
|
|
6382
|
+
if (pnpm && typeof pnpm === "object" && "packages" in pnpm && Array.isArray(pnpm.packages)) patterns.push(...pnpm.packages.filter((p) => typeof p === "string"));
|
|
6383
|
+
} catch {}
|
|
6384
|
+
const negated = patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
|
|
6385
|
+
const included = patterns.filter((p) => !p.startsWith("!"));
|
|
6386
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
6387
|
+
for await (const file of glob(included.map((p) => posix.join(p, "package.json")), {
|
|
6388
|
+
cwd,
|
|
6389
|
+
exclude: (path) => basename(path) === "node_modules"
|
|
6390
|
+
})) {
|
|
6391
|
+
const dir = dirname(file).split(sep).join("/");
|
|
6392
|
+
if (dir !== "." && !negated.some((n) => posix.matchesGlob(dir, n))) dirs.add(join(cwd, dir));
|
|
6393
|
+
}
|
|
6394
|
+
return [...dirs].sort();
|
|
6395
|
+
}
|
|
6396
|
+
async function discoverNodeModuleSkills(cwd, recursive) {
|
|
6397
|
+
const warnings = [];
|
|
6398
|
+
const errors = [];
|
|
6399
|
+
const remote = /* @__PURE__ */ new Map();
|
|
6400
|
+
const pkg = await readPackageJson(cwd);
|
|
6401
|
+
if (!pkg) return {
|
|
6402
|
+
skills: [],
|
|
6403
|
+
remote: [],
|
|
6404
|
+
warnings,
|
|
6405
|
+
errors
|
|
6406
|
+
};
|
|
6407
|
+
const skills = [];
|
|
6408
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6409
|
+
const add = async (found) => {
|
|
6410
|
+
for (const skill of found) {
|
|
6411
|
+
const key = await realpath(skill.path).catch(() => skill.path);
|
|
6412
|
+
if (seen.has(key)) continue;
|
|
6413
|
+
seen.add(key);
|
|
6414
|
+
skills.push(skill);
|
|
6415
|
+
}
|
|
6416
|
+
};
|
|
6417
|
+
const projects = [{
|
|
6418
|
+
name: ".",
|
|
6419
|
+
dir: cwd,
|
|
6420
|
+
pkg,
|
|
6421
|
+
depth: 0
|
|
6422
|
+
}];
|
|
6423
|
+
for (const dir of recursive ? await findWorkspacePackages(cwd, pkg) : []) {
|
|
6424
|
+
const workspacePkg = await readPackageJson(dir);
|
|
6425
|
+
const name = workspacePkg?.name ?? relative(cwd, dir).split(sep).join("/");
|
|
6426
|
+
if (workspacePkg) projects.push({
|
|
6427
|
+
name,
|
|
6428
|
+
dir,
|
|
6429
|
+
pkg: workspacePkg,
|
|
6430
|
+
depth: 1
|
|
6431
|
+
});
|
|
6432
|
+
}
|
|
6433
|
+
const declarers = [];
|
|
6434
|
+
for (const project of projects) {
|
|
6435
|
+
const installed = [...new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(project.pkg[field] ?? {})))].flatMap((name) => {
|
|
6436
|
+
const dir = findInstalledPackage(project.dir, name);
|
|
6437
|
+
return dir ? [{
|
|
6438
|
+
name,
|
|
6439
|
+
dir
|
|
6440
|
+
}] : [];
|
|
6441
|
+
});
|
|
6442
|
+
await add((await Promise.all(installed.map(({ name, dir }) => discoverPackageSkills(dir, name, project.depth)))).flat());
|
|
6443
|
+
declarers.push({
|
|
6444
|
+
name: project.name,
|
|
6445
|
+
dir: project.dir,
|
|
6446
|
+
depth: project.depth
|
|
6447
|
+
}, ...installed.map(({ name, dir }) => ({
|
|
6448
|
+
name,
|
|
6449
|
+
dir,
|
|
6450
|
+
depth: project.depth + 1
|
|
6451
|
+
})));
|
|
6452
|
+
}
|
|
6453
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6454
|
+
for (const declarer of declarers) {
|
|
6455
|
+
const dir = await realpath(declarer.dir).catch(() => null);
|
|
6456
|
+
if (!dir || visited.has(dir)) continue;
|
|
6457
|
+
visited.add(dir);
|
|
6458
|
+
const problems = declarer.name === "." ? errors : warnings;
|
|
6459
|
+
const field = (await readPackageJson(dir))?.skills;
|
|
6460
|
+
if (field === void 0) continue;
|
|
6461
|
+
if (!Array.isArray(field)) {
|
|
6462
|
+
if (problems === errors) errors.push("package.json: \"skills\" must be an array");
|
|
6463
|
+
continue;
|
|
6464
|
+
}
|
|
6465
|
+
const parsed = parseSkillsField(field, declarer.name);
|
|
6466
|
+
problems.push(...parsed.errors);
|
|
6467
|
+
for (const request of parsed.remote) {
|
|
6468
|
+
const { url, ref, subpath } = request.parsed;
|
|
6469
|
+
const key = JSON.stringify([
|
|
6470
|
+
url,
|
|
6471
|
+
ref,
|
|
6472
|
+
subpath,
|
|
6473
|
+
request.skills
|
|
6474
|
+
]);
|
|
6475
|
+
if (!remote.has(key)) remote.set(key, {
|
|
6476
|
+
...request,
|
|
6477
|
+
via: declarer.name
|
|
6195
6478
|
});
|
|
6196
|
-
return;
|
|
6197
6479
|
}
|
|
6198
|
-
const
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
for (const searchDir of searchDirs) try {
|
|
6204
|
-
const entries = await readdir(searchDir);
|
|
6205
|
-
for (const name of entries) {
|
|
6206
|
-
const skillDir = join(searchDir, name);
|
|
6207
|
-
try {
|
|
6208
|
-
if (!(await stat(skillDir)).isDirectory()) continue;
|
|
6209
|
-
} catch {
|
|
6210
|
-
continue;
|
|
6211
|
-
}
|
|
6212
|
-
const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
|
|
6213
|
-
if (skill) skills.push({
|
|
6214
|
-
...skill,
|
|
6215
|
-
packageName
|
|
6216
|
-
});
|
|
6480
|
+
for (const request of parsed.npm) {
|
|
6481
|
+
const target = findInstalledPackage(dir, request.package);
|
|
6482
|
+
if (!target) {
|
|
6483
|
+
problems.push(`${declarer.name}: cannot resolve "npm:${request.package}"; is it a dependency?`);
|
|
6484
|
+
continue;
|
|
6217
6485
|
}
|
|
6218
|
-
|
|
6486
|
+
await add((await discoverPackageSkills(target, request.package, declarer.depth)).filter((skill) => request.skills.length === 0 || request.skills.includes(basename(skill.path)) || request.skills.includes(sanitizeName(skill.name))).map((skill) => ({
|
|
6487
|
+
...skill,
|
|
6488
|
+
via: declarer.name
|
|
6489
|
+
})));
|
|
6490
|
+
declarers.push({
|
|
6491
|
+
name: request.package,
|
|
6492
|
+
dir: target,
|
|
6493
|
+
depth: declarer.depth + 1
|
|
6494
|
+
});
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
return {
|
|
6498
|
+
skills,
|
|
6499
|
+
remote: [...remote.values()],
|
|
6500
|
+
warnings,
|
|
6501
|
+
errors
|
|
6219
6502
|
};
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6503
|
+
}
|
|
6504
|
+
function isUnderNodeModules(path) {
|
|
6505
|
+
return path.split(sep).includes("node_modules");
|
|
6506
|
+
}
|
|
6507
|
+
function installedBySync(entry) {
|
|
6508
|
+
return entry?.sourceType === "node_modules" || entry?.via !== void 0;
|
|
6509
|
+
}
|
|
6510
|
+
async function foreignDestination(dest, canonicalDir, lockEntry, cwd) {
|
|
6511
|
+
let stats;
|
|
6512
|
+
try {
|
|
6513
|
+
stats = await lstat(dest);
|
|
6514
|
+
} catch {
|
|
6515
|
+
return null;
|
|
6516
|
+
}
|
|
6517
|
+
if (stats.isSymbolicLink()) {
|
|
6518
|
+
const target = resolve(dirname(dest), await readlink(dest));
|
|
6519
|
+
if (isUnderNodeModules(target) || target === canonicalDir) return null;
|
|
6520
|
+
return `${shortenPath$1(dest, cwd)} is a symlink to ${shortenPath$1(target, cwd)}`;
|
|
6521
|
+
}
|
|
6522
|
+
if (installedBySync(lockEntry)) return null;
|
|
6523
|
+
return `${shortenPath$1(dest, cwd)} already exists and was not installed by sync`;
|
|
6524
|
+
}
|
|
6525
|
+
async function linksIntoNodeModules(path) {
|
|
6526
|
+
try {
|
|
6527
|
+
if (!(await lstat(path)).isSymbolicLink()) return false;
|
|
6528
|
+
return isUnderNodeModules(resolve(dirname(path), await readlink(path)));
|
|
6529
|
+
} catch {
|
|
6530
|
+
return false;
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
async function pruneStaleSkills(cwd, keep, keepRemote, lock, dryRun) {
|
|
6534
|
+
const canonicalBase = getCanonicalSkillsDir(false, cwd);
|
|
6535
|
+
const lockKeys = /* @__PURE__ */ new Map();
|
|
6536
|
+
const explicit = /* @__PURE__ */ new Set();
|
|
6537
|
+
for (const [key, entry] of Object.entries(lock.skills)) {
|
|
6538
|
+
const remote = entry.sourceType !== "node_modules";
|
|
6539
|
+
if (installedBySync(entry) && !(remote && keepRemote(entry))) lockKeys.set(sanitizeName(key), key);
|
|
6540
|
+
else explicit.add(sanitizeName(key));
|
|
6541
|
+
}
|
|
6542
|
+
const candidates = new Set(lockKeys.keys());
|
|
6543
|
+
for (const name of await readdir(canonicalBase).catch(() => [])) if (await linksIntoNodeModules(join(canonicalBase, name))) candidates.add(name);
|
|
6544
|
+
const stale = [...candidates].filter((name) => !keep.has(name) && !explicit.has(name)).sort();
|
|
6545
|
+
if (dryRun || stale.length === 0) return stale;
|
|
6546
|
+
const allAgents = Object.keys(agents);
|
|
6547
|
+
for (const name of stale) {
|
|
6548
|
+
const canonicalDir = join(canonicalBase, name);
|
|
6549
|
+
const key = lockKeys.get(name);
|
|
6550
|
+
const lockEntry = key ? lock.skills[key] : void 0;
|
|
6551
|
+
const destinations = /* @__PURE__ */ new Set([canonicalDir, ...allAgents.map((agent) => getInstallPath(name, agent, { cwd }))]);
|
|
6552
|
+
for (const dest of destinations) if (!await foreignDestination(dest, canonicalDir, lockEntry, cwd)) await rm(dest, {
|
|
6553
|
+
recursive: true,
|
|
6554
|
+
force: true
|
|
6555
|
+
});
|
|
6556
|
+
if (key) delete lock.skills[key];
|
|
6557
|
+
}
|
|
6558
|
+
await writeLocalLock(lock, cwd);
|
|
6559
|
+
return stale;
|
|
6560
|
+
}
|
|
6561
|
+
async function blockedReason(name, targetAgents, lockEntry, cwd) {
|
|
6562
|
+
if (lockEntry && !installedBySync(lockEntry)) return `installed with \`skills add\` from ${lockEntry.source}`;
|
|
6563
|
+
const canonicalDir = getCanonicalPath(name, { cwd });
|
|
6564
|
+
const destinations = /* @__PURE__ */ new Set([canonicalDir, ...targetAgents.map((agent) => getInstallPath(name, agent, { cwd }))]);
|
|
6565
|
+
for (const dest of destinations) {
|
|
6566
|
+
const reason = await foreignDestination(dest, canonicalDir, lockEntry, cwd);
|
|
6567
|
+
if (reason) return reason;
|
|
6568
|
+
}
|
|
6569
|
+
return null;
|
|
6570
|
+
}
|
|
6571
|
+
async function resolveConflicts(skills, targetAgents, lockSkills, cwd) {
|
|
6572
|
+
const lockBySanitizedName = new Map(Object.entries(lockSkills).map(([name, entry]) => [sanitizeName(name), entry]));
|
|
6573
|
+
const byName = Map.groupBy(skills, (skill) => sanitizeName(skill.name));
|
|
6574
|
+
const install = [];
|
|
6575
|
+
const skipped = [];
|
|
6576
|
+
for (const [name, all] of byName) {
|
|
6577
|
+
const depth = Math.min(...all.map((c) => c.depth));
|
|
6578
|
+
const candidates = all.filter((c) => c.depth === depth);
|
|
6579
|
+
for (const skill of all.filter((c) => c.depth !== depth)) skipped.push({
|
|
6580
|
+
skill,
|
|
6581
|
+
reason: `${candidates[0].packageName} is closer to the project`
|
|
6582
|
+
});
|
|
6583
|
+
if (candidates.length > 1) {
|
|
6584
|
+
const packages = candidates.map((c) => c.packageName).join(", ");
|
|
6585
|
+
for (const skill of candidates) skipped.push({
|
|
6586
|
+
skill,
|
|
6587
|
+
reason: `shipped by ${packages}; drop this one with --exclude ${skill.packageName}#${name}`
|
|
6588
|
+
});
|
|
6589
|
+
continue;
|
|
6227
6590
|
}
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6591
|
+
const skill = candidates[0];
|
|
6592
|
+
const reason = await blockedReason(name, targetAgents, lockBySanitizedName.get(name), cwd);
|
|
6593
|
+
if (reason) skipped.push({
|
|
6594
|
+
skill,
|
|
6595
|
+
reason
|
|
6596
|
+
});
|
|
6597
|
+
else install.push(skill);
|
|
6598
|
+
}
|
|
6599
|
+
return {
|
|
6600
|
+
install,
|
|
6601
|
+
skipped
|
|
6602
|
+
};
|
|
6603
|
+
}
|
|
6604
|
+
async function promptForAgentChoice(choices, fallback) {
|
|
6605
|
+
const universalAgents = getUniversalAgents();
|
|
6606
|
+
const visibleUniversalAgents = getVisibleUniversalAgents();
|
|
6607
|
+
const last = await getLastSelectedAgents().catch(() => void 0);
|
|
6608
|
+
const remembered = choices.filter((a) => last?.includes(a));
|
|
6609
|
+
const selected = await searchMultiselect({
|
|
6610
|
+
message: "Which agents do you want to install to?",
|
|
6611
|
+
items: choices.map((a) => ({
|
|
6612
|
+
value: a,
|
|
6613
|
+
label: agents[a].displayName,
|
|
6614
|
+
hint: agents[a].skillsDir
|
|
6615
|
+
})),
|
|
6616
|
+
initialSelected: remembered.length > 0 ? remembered : fallback,
|
|
6617
|
+
lockedSection: {
|
|
6618
|
+
title: "Universal (.agents/skills)",
|
|
6619
|
+
items: visibleUniversalAgents.map((a) => ({
|
|
6620
|
+
value: a,
|
|
6621
|
+
label: agents[a].displayName
|
|
6622
|
+
})),
|
|
6623
|
+
hiddenCount: universalAgents.length - visibleUniversalAgents.length
|
|
6624
|
+
}
|
|
6625
|
+
});
|
|
6626
|
+
if (!isCancelled(selected)) await saveSelectedAgents(selected).catch(() => {});
|
|
6627
|
+
return selected;
|
|
6628
|
+
}
|
|
6629
|
+
function isFromRequest(entry, request) {
|
|
6630
|
+
return entry.via !== void 0 && entry.source === getProjectLockSource(request.parsed) && entry.ref === request.parsed.ref;
|
|
6631
|
+
}
|
|
6632
|
+
function isRemoteInstalled(request, lock, cwd) {
|
|
6633
|
+
const installed = Object.entries(lock.skills).filter(([name, entry]) => isFromRequest(entry, request) && existsSync(getCanonicalPath(name, { cwd }))).map(([name]) => sanitizeName(name));
|
|
6634
|
+
return request.skills.length === 0 ? installed.length > 0 : request.skills.every((name) => installed.includes(sanitizeName(name)));
|
|
6243
6635
|
}
|
|
6244
6636
|
async function runSync(args, options = {}) {
|
|
6245
6637
|
const cwd = process.cwd();
|
|
@@ -6260,46 +6652,37 @@ async function runSync(args, options = {}) {
|
|
|
6260
6652
|
if (agentResult.isAgent) log.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively");
|
|
6261
6653
|
const spinner$2 = spinner();
|
|
6262
6654
|
spinner$2.start("Scanning node_modules for skills…");
|
|
6263
|
-
const
|
|
6264
|
-
if (
|
|
6265
|
-
spinner$2.stop(import_picocolors.default.
|
|
6266
|
-
|
|
6655
|
+
const discovery = await discoverNodeModuleSkills(cwd, options.recursive ?? false);
|
|
6656
|
+
if (discovery.errors.length > 0) {
|
|
6657
|
+
spinner$2.stop(import_picocolors.default.red("Invalid skills field"));
|
|
6658
|
+
for (const error of discovery.errors) log.error(error);
|
|
6659
|
+
outro(import_picocolors.default.red("Fix the \"skills\" field in package.json and run sync again."));
|
|
6660
|
+
process.exitCode = 1;
|
|
6661
|
+
return;
|
|
6662
|
+
}
|
|
6663
|
+
const { include, exclude } = options;
|
|
6664
|
+
const discoveredSkills = discovery.skills.filter((skill) => (!include?.length || matchesSkill(skill, include)) && !(exclude?.length && matchesSkill(skill, exclude)));
|
|
6665
|
+
spinner$2.stop(discoveredSkills.length === 0 ? import_picocolors.default.yellow("No skills found") : `Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
|
|
6666
|
+
for (const warning of discovery.warnings) log.warn(warning);
|
|
6667
|
+
const localLock = await readLocalLock(cwd);
|
|
6668
|
+
if (options.cleanup !== false) {
|
|
6669
|
+
const keep = new Set(discoveredSkills.map((skill) => sanitizeName(skill.name)));
|
|
6670
|
+
const keepRemote = (entry) => options.remote === false || discovery.remote.some((r) => isFromRequest(entry, r));
|
|
6671
|
+
const stale = await pruneStaleSkills(cwd, keep, keepRemote, localLock, options.dryRun ?? false);
|
|
6672
|
+
for (const name of stale) log.info(`${options.dryRun ? "Would remove" : "Removed"} ${import_picocolors.default.cyan(name)} ${import_picocolors.default.dim("(no longer provided by a dependency)")}`);
|
|
6673
|
+
}
|
|
6674
|
+
const remoteRequests = (options.remote === false ? [] : discovery.remote).filter((request) => !isRemoteInstalled(request, localLock, cwd));
|
|
6675
|
+
if (discoveredSkills.length === 0 && remoteRequests.length === 0) {
|
|
6676
|
+
outro(import_picocolors.default.dim("Nothing to sync."));
|
|
6267
6677
|
return;
|
|
6268
6678
|
}
|
|
6269
|
-
spinner$2.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
|
|
6270
6679
|
for (const skill of discoveredSkills) {
|
|
6271
6680
|
log.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
|
|
6272
6681
|
if (skill.description) log.message(import_picocolors.default.dim(` ${skill.description}`));
|
|
6273
6682
|
}
|
|
6274
|
-
const localLock = await readLocalLock(cwd);
|
|
6275
|
-
const toInstall = [];
|
|
6276
|
-
const upToDate = [];
|
|
6277
|
-
if (options.force) {
|
|
6278
|
-
toInstall.push(...discoveredSkills);
|
|
6279
|
-
log.info(import_picocolors.default.dim("Force mode: reinstalling all skills"));
|
|
6280
|
-
} else {
|
|
6281
|
-
for (const skill of discoveredSkills) {
|
|
6282
|
-
const existingEntry = localLock.skills[skill.name];
|
|
6283
|
-
if (existingEntry) {
|
|
6284
|
-
if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) {
|
|
6285
|
-
upToDate.push(skill.name);
|
|
6286
|
-
continue;
|
|
6287
|
-
}
|
|
6288
|
-
}
|
|
6289
|
-
toInstall.push(skill);
|
|
6290
|
-
}
|
|
6291
|
-
if (upToDate.length > 0) log.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`));
|
|
6292
|
-
if (toInstall.length === 0) {
|
|
6293
|
-
console.log();
|
|
6294
|
-
outro(import_picocolors.default.green("All skills are up to date."));
|
|
6295
|
-
return;
|
|
6296
|
-
}
|
|
6297
|
-
}
|
|
6298
|
-
log.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`);
|
|
6299
6683
|
let targetAgents;
|
|
6300
6684
|
const validAgents = Object.keys(agents);
|
|
6301
6685
|
const universalAgents = getUniversalAgents();
|
|
6302
|
-
const visibleUniversalAgents = getVisibleUniversalAgents();
|
|
6303
6686
|
if (options.agent?.includes("*")) {
|
|
6304
6687
|
targetAgents = validAgents;
|
|
6305
6688
|
log.info(`Installing to all ${targetAgents.length} agents`);
|
|
@@ -6316,54 +6699,14 @@ async function runSync(args, options = {}) {
|
|
|
6316
6699
|
const installedAgents = await detectInstalledAgents();
|
|
6317
6700
|
const totalAgents = Object.keys(agents).length;
|
|
6318
6701
|
spinner$2.stop(`${totalAgents} agents`);
|
|
6319
|
-
if (installedAgents.length === 0
|
|
6702
|
+
if (installedAgents.length === 0 && options.yes) {
|
|
6320
6703
|
targetAgents = universalAgents;
|
|
6321
6704
|
log.info("Installing to universal agents");
|
|
6322
|
-
} else {
|
|
6323
|
-
const selected = await searchMultiselect({
|
|
6324
|
-
message: "Which agents do you want to install to?",
|
|
6325
|
-
items: getNonUniversalAgents().map((a) => ({
|
|
6326
|
-
value: a,
|
|
6327
|
-
label: agents[a].displayName,
|
|
6328
|
-
hint: agents[a].skillsDir
|
|
6329
|
-
})),
|
|
6330
|
-
initialSelected: [],
|
|
6331
|
-
lockedSection: {
|
|
6332
|
-
title: "Universal (.agents/skills)",
|
|
6333
|
-
items: visibleUniversalAgents.map((a) => ({
|
|
6334
|
-
value: a,
|
|
6335
|
-
label: agents[a].displayName
|
|
6336
|
-
})),
|
|
6337
|
-
hiddenCount: universalAgents.length - visibleUniversalAgents.length
|
|
6338
|
-
}
|
|
6339
|
-
});
|
|
6340
|
-
if (isCancelled(selected)) {
|
|
6341
|
-
cancel("Sync cancelled");
|
|
6342
|
-
process.exit(0);
|
|
6343
|
-
}
|
|
6344
|
-
targetAgents = selected;
|
|
6345
|
-
}
|
|
6346
|
-
else if (installedAgents.length === 1 || options.yes) {
|
|
6705
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
6347
6706
|
targetAgents = [...installedAgents];
|
|
6348
6707
|
for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
|
|
6349
6708
|
} else {
|
|
6350
|
-
const selected = await
|
|
6351
|
-
message: "Which agents do you want to install to?",
|
|
6352
|
-
items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({
|
|
6353
|
-
value: a,
|
|
6354
|
-
label: agents[a].displayName,
|
|
6355
|
-
hint: agents[a].skillsDir
|
|
6356
|
-
})),
|
|
6357
|
-
initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
|
|
6358
|
-
lockedSection: {
|
|
6359
|
-
title: "Universal (.agents/skills)",
|
|
6360
|
-
items: visibleUniversalAgents.map((a) => ({
|
|
6361
|
-
value: a,
|
|
6362
|
-
label: agents[a].displayName
|
|
6363
|
-
})),
|
|
6364
|
-
hiddenCount: universalAgents.length - visibleUniversalAgents.length
|
|
6365
|
-
}
|
|
6366
|
-
});
|
|
6709
|
+
const selected = await promptForAgentChoice(getNonUniversalAgents().filter((a) => installedAgents.length === 0 || installedAgents.includes(a)), installedAgents.filter((a) => !universalAgents.includes(a)));
|
|
6367
6710
|
if (isCancelled(selected)) {
|
|
6368
6711
|
cancel("Sync cancelled");
|
|
6369
6712
|
process.exit(0);
|
|
@@ -6371,14 +6714,27 @@ async function runSync(args, options = {}) {
|
|
|
6371
6714
|
targetAgents = selected;
|
|
6372
6715
|
}
|
|
6373
6716
|
}
|
|
6717
|
+
const { install: toInstall, skipped } = await resolveConflicts(discoveredSkills, targetAgents, localLock.skills, cwd);
|
|
6718
|
+
for (const { skill, reason } of skipped) log.warn(`Skipped ${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}: ${reason}`);
|
|
6719
|
+
if (toInstall.length === 0 && remoteRequests.length === 0) {
|
|
6720
|
+
console.log();
|
|
6721
|
+
outro(import_picocolors.default.yellow("Nothing to sync."));
|
|
6722
|
+
return;
|
|
6723
|
+
}
|
|
6724
|
+
const mode = options.copy ? "copy" : "link";
|
|
6374
6725
|
const summaryLines = [];
|
|
6375
6726
|
for (const skill of toInstall) {
|
|
6376
|
-
const
|
|
6727
|
+
const canonicalPath = getCanonicalPath(skill.name, { cwd });
|
|
6377
6728
|
summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
|
|
6378
|
-
summaryLines.push(` ${import_picocolors.default.dim(
|
|
6729
|
+
summaryLines.push(` ${import_picocolors.default.dim(`${shortenPath$1(canonicalPath, cwd)} ${mode === "link" ? "→" : "copied from"} ${shortenPath$1(skill.path, cwd)}`)}`);
|
|
6379
6730
|
}
|
|
6731
|
+
for (const request of remoteRequests) summaryLines.push(`${import_picocolors.default.cyan(getProjectLockSource(request.parsed))} ${import_picocolors.default.dim(`← ${request.via} (remote)`)}`);
|
|
6380
6732
|
console.log();
|
|
6381
6733
|
note(summaryLines.join("\n"), "Sync Summary");
|
|
6734
|
+
if (options.dryRun) {
|
|
6735
|
+
outro(import_picocolors.default.dim("Dry run — nothing changed."));
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6382
6738
|
if (!options.yes) {
|
|
6383
6739
|
const confirmed = await confirm({ message: "Proceed with sync?" });
|
|
6384
6740
|
if (isCancel(confirmed) || !confirmed) {
|
|
@@ -6392,7 +6748,7 @@ async function runSync(args, options = {}) {
|
|
|
6392
6748
|
const result = await installSkillForAgent(skill, agent, {
|
|
6393
6749
|
global: false,
|
|
6394
6750
|
cwd,
|
|
6395
|
-
mode: "
|
|
6751
|
+
mode: agent === "eve" ? "copy" : mode
|
|
6396
6752
|
});
|
|
6397
6753
|
results.push({
|
|
6398
6754
|
skill: skill.name,
|
|
@@ -6413,9 +6769,44 @@ async function runSync(args, options = {}) {
|
|
|
6413
6769
|
await addSkillToLocalLock(skill.name, {
|
|
6414
6770
|
source: skill.packageName,
|
|
6415
6771
|
sourceType: "node_modules",
|
|
6772
|
+
skillPath: skill.skillPath,
|
|
6773
|
+
...skill.packageVersion && { version: skill.packageVersion },
|
|
6774
|
+
...skill.via && { via: skill.via },
|
|
6416
6775
|
computedHash
|
|
6417
6776
|
}, cwd);
|
|
6418
6777
|
} catch {}
|
|
6778
|
+
const claimed = new Set(discoveredSkills.map((skill) => sanitizeName(skill.name)));
|
|
6779
|
+
const lockEntryFor = (name) => Object.entries(localLock.skills).find(([key]) => sanitizeName(key) === name)?.[1];
|
|
6780
|
+
for (const request of remoteRequests) {
|
|
6781
|
+
const label = getProjectLockSource(request.parsed);
|
|
6782
|
+
const result = await installFromSource(request.parsed, {
|
|
6783
|
+
skills: request.skills,
|
|
6784
|
+
agents: targetAgents,
|
|
6785
|
+
via: request.via,
|
|
6786
|
+
select: async (skills) => {
|
|
6787
|
+
const selected = [];
|
|
6788
|
+
for (const skill of skills) {
|
|
6789
|
+
const name = sanitizeName(skill.name);
|
|
6790
|
+
const reason = claimed.has(name) ? "another source in this sync provides it" : await blockedReason(name, targetAgents, lockEntryFor(name), cwd);
|
|
6791
|
+
if (reason) {
|
|
6792
|
+
log.warn(`Skipped ${import_picocolors.default.cyan(name)} from ${label}: ${reason}`);
|
|
6793
|
+
continue;
|
|
6794
|
+
}
|
|
6795
|
+
claimed.add(name);
|
|
6796
|
+
selected.push(skill);
|
|
6797
|
+
}
|
|
6798
|
+
return selected;
|
|
6799
|
+
}
|
|
6800
|
+
});
|
|
6801
|
+
if (result.error) {
|
|
6802
|
+
log.error(`Failed to install from ${import_picocolors.default.cyan(label)}: ${result.error}`);
|
|
6803
|
+
process.exitCode = 1;
|
|
6804
|
+
continue;
|
|
6805
|
+
}
|
|
6806
|
+
if (result.installed.length > 0) log.success(`Installed ${result.installed.map((name) => import_picocolors.default.cyan(name)).join(", ")} from ${label}`);
|
|
6807
|
+
for (const failure of result.failed) log.error(failure);
|
|
6808
|
+
if (result.failed.length > 0) process.exitCode = 1;
|
|
6809
|
+
}
|
|
6419
6810
|
console.log();
|
|
6420
6811
|
if (successful.length > 0) {
|
|
6421
6812
|
const bySkill = /* @__PURE__ */ new Map();
|
|
@@ -6454,21 +6845,23 @@ async function runSync(args, options = {}) {
|
|
|
6454
6845
|
}
|
|
6455
6846
|
function parseSyncOptions(args) {
|
|
6456
6847
|
const options = {};
|
|
6457
|
-
|
|
6848
|
+
let i = 0;
|
|
6849
|
+
const takeValues = () => {
|
|
6850
|
+
const values = [];
|
|
6851
|
+
while (args[i + 1] !== void 0 && !args[i + 1].startsWith("-")) values.push(args[++i]);
|
|
6852
|
+
return values;
|
|
6853
|
+
};
|
|
6854
|
+
for (; i < args.length; i++) {
|
|
6458
6855
|
const arg = args[i];
|
|
6459
6856
|
if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
6460
|
-
else if (arg === "
|
|
6461
|
-
else if (arg === "-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
nextArg = args[i];
|
|
6469
|
-
}
|
|
6470
|
-
i--;
|
|
6471
|
-
}
|
|
6857
|
+
else if (arg === "--copy") options.copy = true;
|
|
6858
|
+
else if (arg === "--dry-run") options.dryRun = true;
|
|
6859
|
+
else if (arg === "--no-cleanup") options.cleanup = false;
|
|
6860
|
+
else if (arg === "--no-remote") options.remote = false;
|
|
6861
|
+
else if (arg === "-r" || arg === "--recursive") options.recursive = true;
|
|
6862
|
+
else if (arg === "-a" || arg === "--agent") options.agent = [...options.agent ?? [], ...takeValues()];
|
|
6863
|
+
else if (arg === "--include") options.include = [...options.include ?? [], ...takeValues()];
|
|
6864
|
+
else if (arg === "--exclude") options.exclude = [...options.exclude ?? [], ...takeValues()];
|
|
6472
6865
|
}
|
|
6473
6866
|
return { options };
|
|
6474
6867
|
}
|
|
@@ -6565,14 +6958,27 @@ async function runInstallFromLock(args) {
|
|
|
6565
6958
|
}
|
|
6566
6959
|
const remoteCount = skillEntries.length - nodeModuleSkills.length;
|
|
6567
6960
|
if (remoteCount > 0) log.info(`Restoring ${import_picocolors.default.cyan(String(remoteCount))} skill${remoteCount !== 1 ? "s" : ""} from skills-lock.json into ${import_picocolors.default.dim(".agents/skills/")}`);
|
|
6568
|
-
for (const [source, { skills }] of bySource)
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6961
|
+
for (const [source, { sourceType, skills }] of bySource) {
|
|
6962
|
+
if (sourceType === "well-known") {
|
|
6963
|
+
await runAdd([source], {
|
|
6964
|
+
skill: skills,
|
|
6965
|
+
agent: universalAgentNames,
|
|
6966
|
+
yes: true
|
|
6967
|
+
});
|
|
6968
|
+
continue;
|
|
6969
|
+
}
|
|
6970
|
+
const result = await installFromSource(parseSource(source), {
|
|
6971
|
+
skills,
|
|
6972
|
+
agents: universalAgentNames
|
|
6573
6973
|
});
|
|
6574
|
-
|
|
6575
|
-
|
|
6974
|
+
if (result.error) {
|
|
6975
|
+
log.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${result.error}`);
|
|
6976
|
+
process.exitCode = 1;
|
|
6977
|
+
continue;
|
|
6978
|
+
}
|
|
6979
|
+
log.success(`Restored ${result.installed.map((name) => import_picocolors.default.cyan(name)).join(", ")}`);
|
|
6980
|
+
for (const failure of result.failed) log.error(failure);
|
|
6981
|
+
if (result.failed.length > 0) process.exitCode = 1;
|
|
6576
6982
|
}
|
|
6577
6983
|
if (nodeModuleSkills.length > 0) {
|
|
6578
6984
|
log.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
|
|
@@ -6736,14 +7142,18 @@ async function runList(args) {
|
|
|
6736
7142
|
console.log();
|
|
6737
7143
|
}
|
|
6738
7144
|
}
|
|
6739
|
-
function resolveSkillsToRemove(requested, folderNames, lockKeys = []) {
|
|
7145
|
+
function resolveSkillsToRemove(requested, folderNames, lockKeys = [], sourceByKey = {}) {
|
|
6740
7146
|
const identityBySanitized = /* @__PURE__ */ new Map();
|
|
6741
7147
|
for (const folder of folderNames) identityBySanitized.set(sanitizeName(folder), folder);
|
|
6742
7148
|
for (const key of lockKeys) identityBySanitized.set(sanitizeName(key), key);
|
|
6743
7149
|
const matched = /* @__PURE__ */ new Set();
|
|
6744
7150
|
for (const name of requested) {
|
|
6745
7151
|
const hit = identityBySanitized.get(sanitizeName(name));
|
|
6746
|
-
if (hit)
|
|
7152
|
+
if (hit) {
|
|
7153
|
+
matched.add(hit);
|
|
7154
|
+
continue;
|
|
7155
|
+
}
|
|
7156
|
+
for (const key of lockKeys) if (sourceByKey[key] === name) matched.add(key);
|
|
6747
7157
|
}
|
|
6748
7158
|
return Array.from(matched);
|
|
6749
7159
|
}
|
|
@@ -6791,9 +7201,11 @@ async function removeCommand(skillNames, options) {
|
|
|
6791
7201
|
}
|
|
6792
7202
|
const installedSkills = Array.from(skillNamesSet).sort();
|
|
6793
7203
|
spinner$1.stop(`Found ${installedSkills.length} unique installed skill(s)`);
|
|
6794
|
-
const
|
|
7204
|
+
const lockEntries = isGlobal ? (await readSkillLock()).skills : (await readLocalLock(cwd)).skills;
|
|
7205
|
+
const lockSkillsKeys = Object.keys(lockEntries);
|
|
7206
|
+
const sourceByKey = Object.fromEntries(lockSkillsKeys.map((key) => [key, lockEntries[key]?.source]));
|
|
6795
7207
|
const requestedSkills = options.all ? [...installedSkills, ...lockSkillsKeys] : skillNames;
|
|
6796
|
-
const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys) : [];
|
|
7208
|
+
const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys, sourceByKey) : [];
|
|
6797
7209
|
if (installedSkills.length === 0 && resolvedRequestedSkills.length === 0) {
|
|
6798
7210
|
outro(import_picocolors.default.yellow("No skills found to remove."));
|
|
6799
7211
|
return;
|
|
@@ -6925,7 +7337,7 @@ async function removeCommand(skillNames, options) {
|
|
|
6925
7337
|
if (successful.length > 0) {
|
|
6926
7338
|
const bySource = /* @__PURE__ */ new Map();
|
|
6927
7339
|
for (const r of successful) {
|
|
6928
|
-
const source = r.source || "local";
|
|
7340
|
+
const source = r.sourceType === "local" ? "local" : r.source || "local";
|
|
6929
7341
|
const existing = bySource.get(source) || { skills: [] };
|
|
6930
7342
|
existing.skills.push(r.skill);
|
|
6931
7343
|
existing.sourceType = r.sourceType;
|
|
@@ -6990,7 +7402,7 @@ function normalizeSkillName(name) {
|
|
|
6990
7402
|
function normalizeSkillPath(path) {
|
|
6991
7403
|
return path.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
6992
7404
|
}
|
|
6993
|
-
function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
|
|
7405
|
+
function resolveSkillLocations(lockedSkillNames, lockSkills, discovered, options = {}) {
|
|
6994
7406
|
const discoveredPaths = new Set(discovered.map((skill) => normalizeSkillPath(skill.skillPath)));
|
|
6995
7407
|
const pathsByName = /* @__PURE__ */ new Map();
|
|
6996
7408
|
for (const skill of discovered) {
|
|
@@ -7007,11 +7419,16 @@ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
|
|
|
7007
7419
|
if (!lockedPath) continue;
|
|
7008
7420
|
const normalizedLockedPath = normalizeSkillPath(lockedPath);
|
|
7009
7421
|
const candidates = [...pathsByName.get(normalizeSkillName(name)) ?? []];
|
|
7422
|
+
const lockedPathStillExists = discoveredPaths.has(normalizedLockedPath);
|
|
7423
|
+
if (lockedPathStillExists && options.exactPathDisambiguates) {
|
|
7424
|
+
resolvedPaths.set(name, normalizedLockedPath);
|
|
7425
|
+
continue;
|
|
7426
|
+
}
|
|
7010
7427
|
if (candidates.length > 1) {
|
|
7011
7428
|
ambiguousSkills.push(name);
|
|
7012
7429
|
continue;
|
|
7013
7430
|
}
|
|
7014
|
-
if (
|
|
7431
|
+
if (lockedPathStillExists) {
|
|
7015
7432
|
resolvedPaths.set(name, normalizedLockedPath);
|
|
7016
7433
|
continue;
|
|
7017
7434
|
}
|
|
@@ -7170,8 +7587,8 @@ async function promptDeletions(source, deletedSkills, isGlobal, options) {
|
|
|
7170
7587
|
});
|
|
7171
7588
|
}
|
|
7172
7589
|
}
|
|
7173
|
-
async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered) {
|
|
7174
|
-
const resolution = resolveSkillLocations(allLockedForSource, lockSkills, discovered);
|
|
7590
|
+
async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered, locationOptions = {}) {
|
|
7591
|
+
const resolution = resolveSkillLocations(allLockedForSource, lockSkills, discovered, locationOptions);
|
|
7175
7592
|
if (resolution.ambiguousSkills.length > 0) {
|
|
7176
7593
|
console.log();
|
|
7177
7594
|
console.log(`${DIM$1}Warning:${RESET$1} Multiple current paths match these skills from ${DIM$1}${source}${RESET$1}; skipping them rather than deleting or migrating the wrong skill:`);
|
|
@@ -7384,7 +7801,7 @@ async function updateGlobalSkills(options = {}) {
|
|
|
7384
7801
|
name: skill.name,
|
|
7385
7802
|
skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
|
|
7386
7803
|
}));
|
|
7387
|
-
const resolution = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name), lock.skills, true, options, discoveredLocations);
|
|
7804
|
+
const resolution = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name), lock.skills, true, options, discoveredLocations, { exactPathDisambiguates: !shouldUseFullDepthForUpdate(firstEntry) });
|
|
7388
7805
|
const deletedSkillSet = new Set(resolution.deletedSkills);
|
|
7389
7806
|
for (const { name: skillName, entry } of itemsForSource) {
|
|
7390
7807
|
if (deletedSkillSet.has(skillName)) continue;
|
|
@@ -7545,7 +7962,7 @@ async function updateProjectSkills(options = {}) {
|
|
|
7545
7962
|
if (hasUniversal) targetParts.push("Universal");
|
|
7546
7963
|
targetParts.push(...targetAgentNames);
|
|
7547
7964
|
if (targetParts.length > 0) console.log(`${TEXT$1}Updating for: ${targetParts.join(", ")}${RESET$1}`);
|
|
7548
|
-
console.log(`${TEXT$1}
|
|
7965
|
+
console.log(`${TEXT$1}Checking ${updatable.length + wellKnownCount} skill(s)…${RESET$1}`);
|
|
7549
7966
|
console.log();
|
|
7550
7967
|
const { successCount: wkSuccessCount, failCount: wkFailCount } = await processWellKnownUpdates(wellKnownGroups, false, options);
|
|
7551
7968
|
successCount += wkSuccessCount;
|
|
@@ -7559,6 +7976,7 @@ async function updateProjectSkills(options = {}) {
|
|
|
7559
7976
|
}
|
|
7560
7977
|
const localLock = await readLocalLock();
|
|
7561
7978
|
const cliEntry = join(__dirname$1, "..", "bin", "cli.mjs");
|
|
7979
|
+
let upToDateCount = 0;
|
|
7562
7980
|
if (updatable.length > 0 && !existsSync(cliEntry)) {
|
|
7563
7981
|
console.log(`${DIM$1}✗ CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
7564
7982
|
return {
|
|
@@ -7576,6 +7994,7 @@ async function updateProjectSkills(options = {}) {
|
|
|
7576
7994
|
let tempDir = null;
|
|
7577
7995
|
let deletedSkills = [];
|
|
7578
7996
|
let resolvedPaths = null;
|
|
7997
|
+
const latestHashes = /* @__PURE__ */ new Map();
|
|
7579
7998
|
if (cloneSource === null) {
|
|
7580
7999
|
failCount += skillsForSource.length;
|
|
7581
8000
|
console.log(`${DIM$1}✗ Cannot update ${source}: skills-lock.json is missing sourceUrl for this generic Git source${RESET$1}`);
|
|
@@ -7590,9 +8009,13 @@ async function updateProjectSkills(options = {}) {
|
|
|
7590
8009
|
name: skill.name,
|
|
7591
8010
|
skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
|
|
7592
8011
|
}));
|
|
7593
|
-
const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations);
|
|
8012
|
+
const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations, { exactPathDisambiguates: !shouldUseFullDepthForUpdate(firstEntry) });
|
|
7594
8013
|
deletedSkills = resolution.deletedSkills;
|
|
7595
8014
|
resolvedPaths = resolution.resolvedPaths;
|
|
8015
|
+
for (const [name, skillPath] of resolution.resolvedPaths) try {
|
|
8016
|
+
const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
|
|
8017
|
+
if (hash) latestHashes.set(name, hash);
|
|
8018
|
+
} catch {}
|
|
7596
8019
|
} catch (error) {
|
|
7597
8020
|
console.log(`${DIM$1}✗ Failed to check for deleted skills from ${source}${RESET$1}`);
|
|
7598
8021
|
} finally {
|
|
@@ -7611,6 +8034,12 @@ async function updateProjectSkills(options = {}) {
|
|
|
7611
8034
|
...skill.entry,
|
|
7612
8035
|
skillPath: resolvedPath
|
|
7613
8036
|
} : skill.entry;
|
|
8037
|
+
const relocated = entry.skillPath !== skill.entry.skillPath;
|
|
8038
|
+
const latestHash = latestHashes.get(skill.name);
|
|
8039
|
+
if (!relocated && latestHash && latestHash === skill.entry.computedHash) {
|
|
8040
|
+
upToDateCount++;
|
|
8041
|
+
continue;
|
|
8042
|
+
}
|
|
7614
8043
|
console.log(`${TEXT$1}Updating ${safeName}…${RESET$1}`);
|
|
7615
8044
|
const installUrl = buildLocalUpdateSource(entry);
|
|
7616
8045
|
if (!installUrl) {
|
|
@@ -7647,6 +8076,8 @@ async function updateProjectSkills(options = {}) {
|
|
|
7647
8076
|
}
|
|
7648
8077
|
}
|
|
7649
8078
|
}
|
|
8079
|
+
if (upToDateCount > 0) if (upToDateCount === updatable.length && wkSuccessCount === 0 && wkFailCount === 0) console.log(`${TEXT$1}✓ All project skills are up to date${RESET$1}`);
|
|
8080
|
+
else console.log(`${DIM$1}${upToDateCount} project skill(s) already up to date${RESET$1}`);
|
|
7650
8081
|
printLegacyProjectSkills(legacy);
|
|
7651
8082
|
return {
|
|
7652
8083
|
successCount,
|
|
@@ -8210,6 +8641,13 @@ ${BOLD}Remove Options:${RESET}
|
|
|
8210
8641
|
${BOLD}Experimental Sync Options:${RESET}
|
|
8211
8642
|
-a, --agent <agents> Specify agents to install to (use '*' for all agents)
|
|
8212
8643
|
-y, --yes Skip confirmation prompts
|
|
8644
|
+
--copy Copy skills instead of linking them to node_modules
|
|
8645
|
+
--dry-run Show what would change without changing anything
|
|
8646
|
+
--no-cleanup Keep skills whose package no longer ships them
|
|
8647
|
+
--no-remote Skip git sources listed in package.json skills fields
|
|
8648
|
+
-r, --recursive Also sync the dependencies of each workspace package
|
|
8649
|
+
--include <patterns> Only sync matching packages (<pkg>) or skills (<pkg>#<skill>)
|
|
8650
|
+
--exclude <patterns> Skip matching packages (<pkg>) or skills (<pkg>#<skill>)
|
|
8213
8651
|
|
|
8214
8652
|
${BOLD}List Options:${RESET}
|
|
8215
8653
|
-g, --global List global skills (default: project)
|