skills 1.7.1 → 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 -3
- package/dist/cli.mjs +719 -298
- 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
|
}
|
|
@@ -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 {
|
|
@@ -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,8 +5721,7 @@ 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
5727
|
if (successful.length > 0 && installGlobally && (normalizedSource || parsed.type === "local" && !directDownload)) {
|
|
@@ -5666,12 +5767,7 @@ async function runAdd(args, options = {}) {
|
|
|
5666
5767
|
if (computedHash === void 0) continue;
|
|
5667
5768
|
const skillPathValue = skillFiles[skill.name];
|
|
5668
5769
|
await addSkillToLocalLock(skill.name, {
|
|
5669
|
-
|
|
5670
|
-
...projectLockSourceUrl && { sourceUrl: projectLockSourceUrl },
|
|
5671
|
-
ref: parsed.ref,
|
|
5672
|
-
sourceType: parsed.type,
|
|
5673
|
-
...skillPathValue && { skillPath: skillPathValue },
|
|
5674
|
-
computedHash,
|
|
5770
|
+
...projectLockEntry(parsed, skillPathValue, computedHash),
|
|
5675
5771
|
...recordSubagents && { subagents: eveSubagents }
|
|
5676
5772
|
}, cwd);
|
|
5677
5773
|
} catch {}
|
|
@@ -6173,76 +6269,369 @@ ${DIM$3} 2) npx skills add <owner/repo@skill>${RESET$3}`;
|
|
|
6173
6269
|
else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
|
|
6174
6270
|
console.log();
|
|
6175
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
|
+
}
|
|
6176
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
|
+
}
|
|
6177
6326
|
function shortenPath$1(fullPath, cwd) {
|
|
6178
6327
|
const home = homedir();
|
|
6179
6328
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
6180
6329
|
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
6181
6330
|
return fullPath;
|
|
6182
6331
|
}
|
|
6183
|
-
async function
|
|
6184
|
-
const nodeModulesDir = join(cwd, "node_modules");
|
|
6185
|
-
const skills = [];
|
|
6186
|
-
let topNames;
|
|
6332
|
+
async function readPackageJson(dir) {
|
|
6187
6333
|
try {
|
|
6188
|
-
|
|
6334
|
+
return JSON.parse(await readFile(join(dir, "package.json"), "utf-8"));
|
|
6189
6335
|
} catch {
|
|
6190
|
-
return
|
|
6191
|
-
}
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
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
|
|
6198
6478
|
});
|
|
6199
|
-
return;
|
|
6200
6479
|
}
|
|
6201
|
-
const
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
for (const searchDir of searchDirs) try {
|
|
6207
|
-
const entries = await readdir(searchDir);
|
|
6208
|
-
for (const name of entries) {
|
|
6209
|
-
const skillDir = join(searchDir, name);
|
|
6210
|
-
try {
|
|
6211
|
-
if (!(await stat(skillDir)).isDirectory()) continue;
|
|
6212
|
-
} catch {
|
|
6213
|
-
continue;
|
|
6214
|
-
}
|
|
6215
|
-
const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
|
|
6216
|
-
if (skill) skills.push({
|
|
6217
|
-
...skill,
|
|
6218
|
-
packageName
|
|
6219
|
-
});
|
|
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;
|
|
6220
6485
|
}
|
|
6221
|
-
|
|
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
|
|
6222
6502
|
};
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
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;
|
|
6230
6590
|
}
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
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)));
|
|
6246
6635
|
}
|
|
6247
6636
|
async function runSync(args, options = {}) {
|
|
6248
6637
|
const cwd = process.cwd();
|
|
@@ -6263,46 +6652,37 @@ async function runSync(args, options = {}) {
|
|
|
6263
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");
|
|
6264
6653
|
const spinner$2 = spinner();
|
|
6265
6654
|
spinner$2.start("Scanning node_modules for skills…");
|
|
6266
|
-
const
|
|
6267
|
-
if (
|
|
6268
|
-
spinner$2.stop(import_picocolors.default.
|
|
6269
|
-
|
|
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."));
|
|
6270
6677
|
return;
|
|
6271
6678
|
}
|
|
6272
|
-
spinner$2.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
|
|
6273
6679
|
for (const skill of discoveredSkills) {
|
|
6274
6680
|
log.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
|
|
6275
6681
|
if (skill.description) log.message(import_picocolors.default.dim(` ${skill.description}`));
|
|
6276
6682
|
}
|
|
6277
|
-
const localLock = await readLocalLock(cwd);
|
|
6278
|
-
const toInstall = [];
|
|
6279
|
-
const upToDate = [];
|
|
6280
|
-
if (options.force) {
|
|
6281
|
-
toInstall.push(...discoveredSkills);
|
|
6282
|
-
log.info(import_picocolors.default.dim("Force mode: reinstalling all skills"));
|
|
6283
|
-
} else {
|
|
6284
|
-
for (const skill of discoveredSkills) {
|
|
6285
|
-
const existingEntry = localLock.skills[skill.name];
|
|
6286
|
-
if (existingEntry) {
|
|
6287
|
-
if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) {
|
|
6288
|
-
upToDate.push(skill.name);
|
|
6289
|
-
continue;
|
|
6290
|
-
}
|
|
6291
|
-
}
|
|
6292
|
-
toInstall.push(skill);
|
|
6293
|
-
}
|
|
6294
|
-
if (upToDate.length > 0) log.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`));
|
|
6295
|
-
if (toInstall.length === 0) {
|
|
6296
|
-
console.log();
|
|
6297
|
-
outro(import_picocolors.default.green("All skills are up to date."));
|
|
6298
|
-
return;
|
|
6299
|
-
}
|
|
6300
|
-
}
|
|
6301
|
-
log.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`);
|
|
6302
6683
|
let targetAgents;
|
|
6303
6684
|
const validAgents = Object.keys(agents);
|
|
6304
6685
|
const universalAgents = getUniversalAgents();
|
|
6305
|
-
const visibleUniversalAgents = getVisibleUniversalAgents();
|
|
6306
6686
|
if (options.agent?.includes("*")) {
|
|
6307
6687
|
targetAgents = validAgents;
|
|
6308
6688
|
log.info(`Installing to all ${targetAgents.length} agents`);
|
|
@@ -6319,54 +6699,14 @@ async function runSync(args, options = {}) {
|
|
|
6319
6699
|
const installedAgents = await detectInstalledAgents();
|
|
6320
6700
|
const totalAgents = Object.keys(agents).length;
|
|
6321
6701
|
spinner$2.stop(`${totalAgents} agents`);
|
|
6322
|
-
if (installedAgents.length === 0
|
|
6702
|
+
if (installedAgents.length === 0 && options.yes) {
|
|
6323
6703
|
targetAgents = universalAgents;
|
|
6324
6704
|
log.info("Installing to universal agents");
|
|
6325
|
-
} else {
|
|
6326
|
-
const selected = await searchMultiselect({
|
|
6327
|
-
message: "Which agents do you want to install to?",
|
|
6328
|
-
items: getNonUniversalAgents().map((a) => ({
|
|
6329
|
-
value: a,
|
|
6330
|
-
label: agents[a].displayName,
|
|
6331
|
-
hint: agents[a].skillsDir
|
|
6332
|
-
})),
|
|
6333
|
-
initialSelected: [],
|
|
6334
|
-
lockedSection: {
|
|
6335
|
-
title: "Universal (.agents/skills)",
|
|
6336
|
-
items: visibleUniversalAgents.map((a) => ({
|
|
6337
|
-
value: a,
|
|
6338
|
-
label: agents[a].displayName
|
|
6339
|
-
})),
|
|
6340
|
-
hiddenCount: universalAgents.length - visibleUniversalAgents.length
|
|
6341
|
-
}
|
|
6342
|
-
});
|
|
6343
|
-
if (isCancelled(selected)) {
|
|
6344
|
-
cancel("Sync cancelled");
|
|
6345
|
-
process.exit(0);
|
|
6346
|
-
}
|
|
6347
|
-
targetAgents = selected;
|
|
6348
|
-
}
|
|
6349
|
-
else if (installedAgents.length === 1 || options.yes) {
|
|
6705
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
6350
6706
|
targetAgents = [...installedAgents];
|
|
6351
6707
|
for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
|
|
6352
6708
|
} else {
|
|
6353
|
-
const selected = await
|
|
6354
|
-
message: "Which agents do you want to install to?",
|
|
6355
|
-
items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({
|
|
6356
|
-
value: a,
|
|
6357
|
-
label: agents[a].displayName,
|
|
6358
|
-
hint: agents[a].skillsDir
|
|
6359
|
-
})),
|
|
6360
|
-
initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
|
|
6361
|
-
lockedSection: {
|
|
6362
|
-
title: "Universal (.agents/skills)",
|
|
6363
|
-
items: visibleUniversalAgents.map((a) => ({
|
|
6364
|
-
value: a,
|
|
6365
|
-
label: agents[a].displayName
|
|
6366
|
-
})),
|
|
6367
|
-
hiddenCount: universalAgents.length - visibleUniversalAgents.length
|
|
6368
|
-
}
|
|
6369
|
-
});
|
|
6709
|
+
const selected = await promptForAgentChoice(getNonUniversalAgents().filter((a) => installedAgents.length === 0 || installedAgents.includes(a)), installedAgents.filter((a) => !universalAgents.includes(a)));
|
|
6370
6710
|
if (isCancelled(selected)) {
|
|
6371
6711
|
cancel("Sync cancelled");
|
|
6372
6712
|
process.exit(0);
|
|
@@ -6374,14 +6714,27 @@ async function runSync(args, options = {}) {
|
|
|
6374
6714
|
targetAgents = selected;
|
|
6375
6715
|
}
|
|
6376
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";
|
|
6377
6725
|
const summaryLines = [];
|
|
6378
6726
|
for (const skill of toInstall) {
|
|
6379
|
-
const
|
|
6727
|
+
const canonicalPath = getCanonicalPath(skill.name, { cwd });
|
|
6380
6728
|
summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
|
|
6381
|
-
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)}`)}`);
|
|
6382
6730
|
}
|
|
6731
|
+
for (const request of remoteRequests) summaryLines.push(`${import_picocolors.default.cyan(getProjectLockSource(request.parsed))} ${import_picocolors.default.dim(`← ${request.via} (remote)`)}`);
|
|
6383
6732
|
console.log();
|
|
6384
6733
|
note(summaryLines.join("\n"), "Sync Summary");
|
|
6734
|
+
if (options.dryRun) {
|
|
6735
|
+
outro(import_picocolors.default.dim("Dry run — nothing changed."));
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6385
6738
|
if (!options.yes) {
|
|
6386
6739
|
const confirmed = await confirm({ message: "Proceed with sync?" });
|
|
6387
6740
|
if (isCancel(confirmed) || !confirmed) {
|
|
@@ -6395,7 +6748,7 @@ async function runSync(args, options = {}) {
|
|
|
6395
6748
|
const result = await installSkillForAgent(skill, agent, {
|
|
6396
6749
|
global: false,
|
|
6397
6750
|
cwd,
|
|
6398
|
-
mode: "
|
|
6751
|
+
mode: agent === "eve" ? "copy" : mode
|
|
6399
6752
|
});
|
|
6400
6753
|
results.push({
|
|
6401
6754
|
skill: skill.name,
|
|
@@ -6416,9 +6769,44 @@ async function runSync(args, options = {}) {
|
|
|
6416
6769
|
await addSkillToLocalLock(skill.name, {
|
|
6417
6770
|
source: skill.packageName,
|
|
6418
6771
|
sourceType: "node_modules",
|
|
6772
|
+
skillPath: skill.skillPath,
|
|
6773
|
+
...skill.packageVersion && { version: skill.packageVersion },
|
|
6774
|
+
...skill.via && { via: skill.via },
|
|
6419
6775
|
computedHash
|
|
6420
6776
|
}, cwd);
|
|
6421
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
|
+
}
|
|
6422
6810
|
console.log();
|
|
6423
6811
|
if (successful.length > 0) {
|
|
6424
6812
|
const bySkill = /* @__PURE__ */ new Map();
|
|
@@ -6457,21 +6845,23 @@ async function runSync(args, options = {}) {
|
|
|
6457
6845
|
}
|
|
6458
6846
|
function parseSyncOptions(args) {
|
|
6459
6847
|
const options = {};
|
|
6460
|
-
|
|
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++) {
|
|
6461
6855
|
const arg = args[i];
|
|
6462
6856
|
if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
6463
|
-
else if (arg === "
|
|
6464
|
-
else if (arg === "-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
nextArg = args[i];
|
|
6472
|
-
}
|
|
6473
|
-
i--;
|
|
6474
|
-
}
|
|
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()];
|
|
6475
6865
|
}
|
|
6476
6866
|
return { options };
|
|
6477
6867
|
}
|
|
@@ -6568,14 +6958,27 @@ async function runInstallFromLock(args) {
|
|
|
6568
6958
|
}
|
|
6569
6959
|
const remoteCount = skillEntries.length - nodeModuleSkills.length;
|
|
6570
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/")}`);
|
|
6571
|
-
for (const [source, { skills }] of bySource)
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
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
|
|
6576
6973
|
});
|
|
6577
|
-
|
|
6578
|
-
|
|
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;
|
|
6579
6982
|
}
|
|
6580
6983
|
if (nodeModuleSkills.length > 0) {
|
|
6581
6984
|
log.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
|
|
@@ -6739,14 +7142,18 @@ async function runList(args) {
|
|
|
6739
7142
|
console.log();
|
|
6740
7143
|
}
|
|
6741
7144
|
}
|
|
6742
|
-
function resolveSkillsToRemove(requested, folderNames, lockKeys = []) {
|
|
7145
|
+
function resolveSkillsToRemove(requested, folderNames, lockKeys = [], sourceByKey = {}) {
|
|
6743
7146
|
const identityBySanitized = /* @__PURE__ */ new Map();
|
|
6744
7147
|
for (const folder of folderNames) identityBySanitized.set(sanitizeName(folder), folder);
|
|
6745
7148
|
for (const key of lockKeys) identityBySanitized.set(sanitizeName(key), key);
|
|
6746
7149
|
const matched = /* @__PURE__ */ new Set();
|
|
6747
7150
|
for (const name of requested) {
|
|
6748
7151
|
const hit = identityBySanitized.get(sanitizeName(name));
|
|
6749
|
-
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);
|
|
6750
7157
|
}
|
|
6751
7158
|
return Array.from(matched);
|
|
6752
7159
|
}
|
|
@@ -6794,9 +7201,11 @@ async function removeCommand(skillNames, options) {
|
|
|
6794
7201
|
}
|
|
6795
7202
|
const installedSkills = Array.from(skillNamesSet).sort();
|
|
6796
7203
|
spinner$1.stop(`Found ${installedSkills.length} unique installed skill(s)`);
|
|
6797
|
-
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]));
|
|
6798
7207
|
const requestedSkills = options.all ? [...installedSkills, ...lockSkillsKeys] : skillNames;
|
|
6799
|
-
const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys) : [];
|
|
7208
|
+
const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys, sourceByKey) : [];
|
|
6800
7209
|
if (installedSkills.length === 0 && resolvedRequestedSkills.length === 0) {
|
|
6801
7210
|
outro(import_picocolors.default.yellow("No skills found to remove."));
|
|
6802
7211
|
return;
|
|
@@ -6993,7 +7402,7 @@ function normalizeSkillName(name) {
|
|
|
6993
7402
|
function normalizeSkillPath(path) {
|
|
6994
7403
|
return path.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
6995
7404
|
}
|
|
6996
|
-
function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
|
|
7405
|
+
function resolveSkillLocations(lockedSkillNames, lockSkills, discovered, options = {}) {
|
|
6997
7406
|
const discoveredPaths = new Set(discovered.map((skill) => normalizeSkillPath(skill.skillPath)));
|
|
6998
7407
|
const pathsByName = /* @__PURE__ */ new Map();
|
|
6999
7408
|
for (const skill of discovered) {
|
|
@@ -7010,11 +7419,16 @@ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
|
|
|
7010
7419
|
if (!lockedPath) continue;
|
|
7011
7420
|
const normalizedLockedPath = normalizeSkillPath(lockedPath);
|
|
7012
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
|
+
}
|
|
7013
7427
|
if (candidates.length > 1) {
|
|
7014
7428
|
ambiguousSkills.push(name);
|
|
7015
7429
|
continue;
|
|
7016
7430
|
}
|
|
7017
|
-
if (
|
|
7431
|
+
if (lockedPathStillExists) {
|
|
7018
7432
|
resolvedPaths.set(name, normalizedLockedPath);
|
|
7019
7433
|
continue;
|
|
7020
7434
|
}
|
|
@@ -7173,8 +7587,8 @@ async function promptDeletions(source, deletedSkills, isGlobal, options) {
|
|
|
7173
7587
|
});
|
|
7174
7588
|
}
|
|
7175
7589
|
}
|
|
7176
|
-
async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered) {
|
|
7177
|
-
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);
|
|
7178
7592
|
if (resolution.ambiguousSkills.length > 0) {
|
|
7179
7593
|
console.log();
|
|
7180
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:`);
|
|
@@ -7387,7 +7801,7 @@ async function updateGlobalSkills(options = {}) {
|
|
|
7387
7801
|
name: skill.name,
|
|
7388
7802
|
skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
|
|
7389
7803
|
}));
|
|
7390
|
-
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) });
|
|
7391
7805
|
const deletedSkillSet = new Set(resolution.deletedSkills);
|
|
7392
7806
|
for (const { name: skillName, entry } of itemsForSource) {
|
|
7393
7807
|
if (deletedSkillSet.has(skillName)) continue;
|
|
@@ -7595,7 +8009,7 @@ async function updateProjectSkills(options = {}) {
|
|
|
7595
8009
|
name: skill.name,
|
|
7596
8010
|
skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
|
|
7597
8011
|
}));
|
|
7598
|
-
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) });
|
|
7599
8013
|
deletedSkills = resolution.deletedSkills;
|
|
7600
8014
|
resolvedPaths = resolution.resolvedPaths;
|
|
7601
8015
|
for (const [name, skillPath] of resolution.resolvedPaths) try {
|
|
@@ -8227,6 +8641,13 @@ ${BOLD}Remove Options:${RESET}
|
|
|
8227
8641
|
${BOLD}Experimental Sync Options:${RESET}
|
|
8228
8642
|
-a, --agent <agents> Specify agents to install to (use '*' for all agents)
|
|
8229
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>)
|
|
8230
8651
|
|
|
8231
8652
|
${BOLD}List Options:${RESET}
|
|
8232
8653
|
-g, --global List global skills (default: project)
|