vskill 1.2.3 → 1.2.6
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 +49 -1
- package/agents.json +6 -1
- package/dist/agents/agents-registry.d.ts +43 -16
- package/dist/agents/agents-registry.js +119 -33
- package/dist/agents/agents-registry.js.map +1 -1
- package/dist/agents/global-dirs.d.ts +19 -0
- package/dist/agents/global-dirs.js +53 -0
- package/dist/agents/global-dirs.js.map +1 -0
- package/dist/commands/add-github-bundle.d.ts +29 -0
- package/dist/commands/add-github-bundle.js +135 -0
- package/dist/commands/add-github-bundle.js.map +1 -0
- package/dist/commands/add-lockfile.d.ts +2 -0
- package/dist/commands/add-lockfile.js +1 -1
- package/dist/commands/add-lockfile.js.map +1 -1
- package/dist/commands/add.d.ts +0 -7
- package/dist/commands/add.js +197 -107
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/audit-webmcp.d.ts +25 -0
- package/dist/commands/audit-webmcp.js +167 -0
- package/dist/commands/audit-webmcp.js.map +1 -0
- package/dist/commands/doctor.d.ts +69 -0
- package/dist/commands/doctor.js +760 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/remove.js +43 -8
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/update.js +12 -4
- package/dist/commands/update.js.map +1 -1
- package/dist/discovery/github-skill-bundle.d.ts +74 -0
- package/dist/discovery/github-skill-bundle.js +323 -0
- package/dist/discovery/github-skill-bundle.js.map +1 -0
- package/dist/eval/skill-scanner.js +7 -1
- package/dist/eval/skill-scanner.js.map +1 -1
- package/dist/eval-server/install-skill-routes.js +10 -3
- package/dist/eval-server/install-skill-routes.js.map +1 -1
- package/dist/eval-server/remove-skill-routes.js +262 -29
- package/dist/eval-server/remove-skill-routes.js.map +1 -1
- package/dist/eval-server/studio-privacy.js +2 -3
- package/dist/eval-server/studio-privacy.js.map +1 -1
- package/dist/eval-server/utils/scan-install-locations.js +22 -3
- package/dist/eval-server/utils/scan-install-locations.js.map +1 -1
- package/dist/eval-ui/assets/{AdvancedTab-CSWhj_MZ.js → AdvancedTab-6VlfqCuD.js} +1 -1
- package/dist/eval-ui/assets/{CreateSkillPage-lecFvrLO.js → CreateSkillPage-CXDd6gDN.js} +1 -1
- package/dist/eval-ui/assets/{FindSkillsPalette-Dtyd7VrA.js → FindSkillsPalette-DQ6uGhFP.js} +2 -2
- package/dist/eval-ui/assets/{GeneralTab-Gbc-K6UI.js → GeneralTab-BpFlJ2ue.js} +1 -1
- package/dist/eval-ui/assets/{PrivacyTab-BXCIX8cQ.js → PrivacyTab-UfO7QLy_.js} +1 -1
- package/dist/eval-ui/assets/{SearchPaletteCore-CneELTjb.js → SearchPaletteCore-DpHCf0mf.js} +1 -1
- package/dist/eval-ui/assets/{SkillDetailPanel-BMfMN6Wy.js → SkillDetailPanel-BjHFjGxg.js} +1 -1
- package/dist/eval-ui/assets/{UpdateDropdown-BlAAI6ah.js → UpdateDropdown-Cn4Vsq5d.js} +1 -1
- package/dist/eval-ui/assets/{UpdatesTab-BFGUaUtu.js → UpdatesTab-C38-n7V3.js} +1 -1
- package/dist/eval-ui/assets/globals-BWCNQsMa.css +1 -0
- package/dist/eval-ui/assets/globals-DmzftdEG.js +49 -0
- package/dist/eval-ui/assets/{index-DhhmQddr.js → index-DlQYQHDO.js} +1 -1
- package/dist/eval-ui/assets/{lifecycle-o_IRibOa.js → lifecycle-n00yue6r.js} +1 -1
- package/dist/eval-ui/assets/main-BmB7Zj9I.js +86 -0
- package/dist/eval-ui/assets/{preferences-D2BPT0OT.js → preferences-BN-jNWS0.js} +2 -2
- package/dist/eval-ui/assets/{useDesktopBridge-BKz-gcRg.js → useDesktopBridge-Cm4zPght.js} +1 -1
- package/dist/eval-ui/index.html +4 -4
- package/dist/eval-ui/lifecycle.html +3 -3
- package/dist/eval-ui/preferences.html +4 -4
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -1
- package/dist/installer/bundle-files.d.ts +39 -2
- package/dist/installer/bundle-files.js +80 -10
- package/dist/installer/bundle-files.js.map +1 -1
- package/dist/installer/canonical.d.ts +39 -4
- package/dist/installer/canonical.js +178 -24
- package/dist/installer/canonical.js.map +1 -1
- package/dist/installer/multi-install.d.ts +11 -2
- package/dist/installer/multi-install.js +41 -9
- package/dist/installer/multi-install.js.map +1 -1
- package/dist/installer/transformers/index.d.ts +8 -0
- package/dist/installer/yaml-safe-mutate.js +48 -2
- package/dist/installer/yaml-safe-mutate.js.map +1 -1
- package/dist/lib/github-fetch.js +2 -0
- package/dist/lib/github-fetch.js.map +1 -1
- package/dist/lib/private-source.d.ts +8 -0
- package/dist/lib/private-source.js +16 -2
- package/dist/lib/private-source.js.map +1 -1
- package/dist/webmcp/args.d.ts +34 -0
- package/dist/webmcp/args.js +87 -0
- package/dist/webmcp/args.js.map +1 -0
- package/dist/webmcp/attribution.d.ts +38 -0
- package/dist/webmcp/attribution.js +141 -0
- package/dist/webmcp/attribution.js.map +1 -0
- package/dist/webmcp/enumerate.d.ts +81 -0
- package/dist/webmcp/enumerate.js +436 -0
- package/dist/webmcp/enumerate.js.map +1 -0
- package/dist/webmcp/format.d.ts +9 -0
- package/dist/webmcp/format.js +105 -0
- package/dist/webmcp/format.js.map +1 -0
- package/dist/webmcp/merge.d.ts +49 -0
- package/dist/webmcp/merge.js +167 -0
- package/dist/webmcp/merge.js.map +1 -0
- package/dist/webmcp/score.d.ts +70 -0
- package/dist/webmcp/score.js +586 -0
- package/dist/webmcp/score.js.map +1 -0
- package/dist/webmcp/types.d.ts +92 -0
- package/dist/webmcp/types.js +8 -0
- package/dist/webmcp/types.js.map +1 -0
- package/package.json +6 -6
- package/dist/eval-ui/assets/globals-Dpf9KmYH.css +0 -1
- package/dist/eval-ui/assets/globals-hm1COkXX.js +0 -49
- package/dist/eval-ui/assets/main-DRhfwCeX.js +0 -86
package/dist/commands/add.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// vskill install -- install a skill from GitHub or local plugin directory
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, statSync, chmodSync, readdirSync, rmSync, } from "node:fs";
|
|
4
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, statSync, chmodSync, readdirSync, rmSync, lstatSync, } from "node:fs";
|
|
5
5
|
import { join, resolve, basename } from "node:path";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import { resolveTilde } from "../utils/paths.js";
|
|
8
8
|
import { reportInstall, reportInstallBatch, submitSkill } from "../api/client.js";
|
|
9
9
|
import { filterAgents } from "../utils/agent-filter.js";
|
|
10
10
|
import { detectInstalledAgents, AGENTS_REGISTRY } from "../agents/agents-registry.js";
|
|
11
|
+
import { agentGlobalInstallDirs, resolveAgentGlobalSkillsDir } from "../agents/global-dirs.js";
|
|
11
12
|
import { ensureLockfile, writeLockfile, readLockfile, removeSkillFromLock } from "../lockfile/index.js";
|
|
12
13
|
import { buildGitHubInstallLockEntry } from "./add-lockfile.js";
|
|
14
|
+
import { prepareGitHubSkillBundle } from "./add-github-bundle.js";
|
|
13
15
|
import { runTier1Scan } from "../scanner/index.js";
|
|
14
16
|
import { getAvailablePlugins, getPluginSource, getPluginVersion, hasPlugin, discoverUnregisteredPlugins } from "../marketplace/index.js";
|
|
15
17
|
import { checkInstallSafety, checkLocalInstallSafety } from "../blocklist/blocklist.js";
|
|
@@ -36,30 +38,9 @@ import { extractFrontmatterVersion } from "../utils/version.js";
|
|
|
36
38
|
import { claudePluginInstall, claudePluginUninstall, claudePluginMarketplaceAdd, claudePluginMarketplaceList, } from "../utils/claude-plugin.js";
|
|
37
39
|
import { buildPerAgentReport, resolvePluginId, } from "../lib/skill-lifecycle.js";
|
|
38
40
|
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* accepts in installer/bundle-files.ts, or the write is rejected.
|
|
43
|
-
*/
|
|
44
|
-
export const BUNDLE_SUBDIRS = ["agents", "references", "scripts", "assets", "tests"];
|
|
45
|
-
/**
|
|
46
|
-
* Extensions carried in a skill bundle. Bundle contents are transported as
|
|
47
|
-
* UTF-8 strings, so this is a text-only allowlist — binaries would be corrupted.
|
|
48
|
-
*/
|
|
49
|
-
const BUNDLE_TEXT_EXTENSIONS = [
|
|
50
|
-
".md", ".txt", ".json", ".jsonc", ".yaml", ".yml", ".toml", ".csv",
|
|
51
|
-
".py", ".sh", ".bash", ".zsh", ".js", ".mjs", ".cjs", ".ts", ".sql",
|
|
52
|
-
".excalidraw", ".svg", ".css", ".html",
|
|
53
|
-
];
|
|
54
|
-
/** Per-file and per-skill ceilings, so one fat repo cannot stall an install. */
|
|
55
|
-
const MAX_BUNDLE_FILE_BYTES = 512 * 1024;
|
|
56
|
-
const MAX_BUNDLE_FILES = 100;
|
|
57
|
-
export function isBundleTextFile(name) {
|
|
58
|
-
const lower = name.toLowerCase();
|
|
59
|
-
if (lower.startsWith("."))
|
|
60
|
-
return false; // .DS_Store, dotfiles
|
|
61
|
-
return BUNDLE_TEXT_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
62
|
-
}
|
|
41
|
+
// 0894: a GitHub skill's complete folder is fetched by
|
|
42
|
+
// prepareGitHubSkillBundle (./add-github-bundle.ts) — every nested file,
|
|
43
|
+
// binaries as bytes, exec bits kept, limits that fail instead of truncating.
|
|
63
44
|
/** Validate that a download_url from GitHub Contents API points to a trusted GitHub domain. */
|
|
64
45
|
function isGitHubDownloadUrl(url) {
|
|
65
46
|
try {
|
|
@@ -340,24 +321,45 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
340
321
|
const toUninstall = usedInteractiveCheckbox
|
|
341
322
|
? [...installedSet].filter((name) => !selectedNames.has(name))
|
|
342
323
|
: [];
|
|
324
|
+
// 0894: --dry-run stops here, before the deselected-plugin uninstall, the
|
|
325
|
+
// registry submissions and every file or lockfile write below.
|
|
326
|
+
if (opts.dryRun) {
|
|
327
|
+
const dry = await promptInstallOptions(await detectInstalledAgents(), opts);
|
|
328
|
+
if (dry.global)
|
|
329
|
+
opts.global = true;
|
|
330
|
+
printDryRunPlan([...selectedNames], dry.agents, opts, toUninstall);
|
|
331
|
+
for (const p of selectedPlugins) {
|
|
332
|
+
enableAfterInstall(p.name, { marketplace: marketplaceName || undefined }, opts);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
343
336
|
if (toUninstall.length > 0) {
|
|
344
337
|
console.log(dim(`\nUninstalling ${toUninstall.length} deselected plugin${toUninstall.length === 1 ? "" : "s"}...\n`));
|
|
345
338
|
const agents = await detectInstalledAgents();
|
|
346
339
|
for (const skillName of toUninstall) {
|
|
347
340
|
// Remove skill directories from all agents
|
|
348
341
|
let removedCount = 0;
|
|
342
|
+
const dirs = new Set();
|
|
349
343
|
for (const agent of agents) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
344
|
+
dirs.add(join(process.cwd(), agent.localSkillsDir, skillName));
|
|
345
|
+
// 0894: every global dir vskill may have installed into
|
|
346
|
+
// (Antigravity's legacy ~/.gemini/antigravity/skills included).
|
|
347
|
+
for (const globalDir of agentGlobalInstallDirs(agent, resolveTilde)) {
|
|
348
|
+
dirs.add(join(globalDir, skillName));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// 0894: shared readers are installed through the canonical
|
|
352
|
+
// .agents/skills folder alone, so the canonical copy for the scope this
|
|
353
|
+
// lockfile tracks goes too (symmetric with install).
|
|
354
|
+
dirs.add(join(lockDir, lockDir === join(os.homedir(), ".agents") ? "" : ".agents", "skills", skillName));
|
|
355
|
+
for (const dir of dirs) {
|
|
356
|
+
if (pathPresent(dir)) {
|
|
357
|
+
try {
|
|
358
|
+
rmSync(dir, { recursive: true, force: true });
|
|
359
|
+
removedCount++;
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
console.warn(`[vskill] Could not remove ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
361
363
|
}
|
|
362
364
|
}
|
|
363
365
|
}
|
|
@@ -518,8 +520,7 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
518
520
|
// not mutate already-valid frontmatter (no-touch principle).
|
|
519
521
|
const isRenaming = sd.name !== plugin.name;
|
|
520
522
|
const processedContent = ensureFrontmatter(content, namespacedName, isRenaming);
|
|
521
|
-
for (const
|
|
522
|
-
const baseDir = resolveInstallBase(opts, agent);
|
|
523
|
+
for (const baseDir of uniqueInstallBases(opts, agents)) {
|
|
523
524
|
const skillDir = join(baseDir, sd.name);
|
|
524
525
|
mkdirSync(skillDir, { recursive: true });
|
|
525
526
|
writeFileSync(join(skillDir, "SKILL.md"), processedContent, "utf-8");
|
|
@@ -535,8 +536,7 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
535
536
|
if (contentRes.ok) {
|
|
536
537
|
const content = await contentRes.text();
|
|
537
538
|
const processedContent = ensureFrontmatter(content, plugin.name);
|
|
538
|
-
for (const
|
|
539
|
-
const baseDir = resolveInstallBase(opts, agent);
|
|
539
|
+
for (const baseDir of uniqueInstallBases(opts, agents)) {
|
|
540
540
|
const skillDir = join(baseDir, plugin.name);
|
|
541
541
|
mkdirSync(skillDir, { recursive: true });
|
|
542
542
|
writeFileSync(join(skillDir, "SKILL.md"), processedContent, "utf-8");
|
|
@@ -613,9 +613,9 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
613
613
|
// unenabled. The user saw the skill on disk but Claude Code didn't load
|
|
614
614
|
// it. Treat --global as opt-in to the enable hook (scope = user, since
|
|
615
615
|
// --global is per-machine, not per-repo).
|
|
616
|
+
// (--dry-run, also an opt-in, returned earlier with its own preview.)
|
|
616
617
|
const userOptedIn = opts.scope !== undefined ||
|
|
617
618
|
opts.global === true ||
|
|
618
|
-
opts.dryRun === true ||
|
|
619
619
|
opts.enable === false;
|
|
620
620
|
if (userOptedIn) {
|
|
621
621
|
const enabledSoFar = [];
|
|
@@ -861,15 +861,46 @@ function resolveSkillsPath(base, localSkillsDir) {
|
|
|
861
861
|
* Resolve the base directory for local skill installation.
|
|
862
862
|
*
|
|
863
863
|
* Priority:
|
|
864
|
-
* 1.
|
|
865
|
-
*
|
|
864
|
+
* 1. 0894: an agent that reads the shared `.agents/skills` in this scope ->
|
|
865
|
+
* that shared folder (same rule as the canonical installer)
|
|
866
|
+
* 2. `--global` -> agent's global skills dir (Antigravity: legacy-aware)
|
|
867
|
+
* 3. default -> process.cwd() + agent's localSkillsDir
|
|
866
868
|
*/
|
|
867
869
|
function resolveInstallBase(opts, agent) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
+
const global = !!opts.global;
|
|
871
|
+
if (agent.readsSharedSkills?.[global ? "global" : "project"] === true) {
|
|
872
|
+
return global ? resolveTilde("~/.agents/skills") : join(safeProjectRoot(), ".agents", "skills");
|
|
873
|
+
}
|
|
874
|
+
if (global) {
|
|
875
|
+
return resolveAgentGlobalSkillsDir({ id: agent.id ?? "", globalSkillsDir: agent.globalSkillsDir }, resolveTilde);
|
|
870
876
|
}
|
|
871
877
|
return resolveSkillsPath(safeProjectRoot(), agent.localSkillsDir);
|
|
872
878
|
}
|
|
879
|
+
/** 0894: each distinct install base once — shared readers (Cursor, Codex,
|
|
880
|
+
* ...) all resolve to `.agents/skills`, which must get one copy, not one
|
|
881
|
+
* per agent. Keeps the first agent seen for each base, in order. */
|
|
882
|
+
function uniqueInstallBases(opts, agents) {
|
|
883
|
+
return [...new Set(agents.map((a) => resolveInstallBase(opts, a)))];
|
|
884
|
+
}
|
|
885
|
+
/** 0894: true for anything at `p`, including a link whose target is gone. */
|
|
886
|
+
function pathPresent(p) {
|
|
887
|
+
if (existsSync(p))
|
|
888
|
+
return true;
|
|
889
|
+
try {
|
|
890
|
+
return lstatSync(p).isSymbolicLink();
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/** 0894: the --dry-run report every install path prints instead of writing. */
|
|
897
|
+
function printDryRunPlan(names, agents, opts, uninstall = []) {
|
|
898
|
+
if (uninstall.length > 0) {
|
|
899
|
+
console.log(`\nDry run: would uninstall ${uninstall.map((n) => bold(n)).join(", ")}.`);
|
|
900
|
+
}
|
|
901
|
+
console.log(`\nDry run: would install ${names.map((n) => bold(n)).join(", ")} ` +
|
|
902
|
+
`(${opts.global ? "global" : "project"}) for ${agents.map((a) => a.id).join(", ")}. Nothing was written.`);
|
|
903
|
+
}
|
|
873
904
|
/**
|
|
874
905
|
* Prompt the user for agent selection and install method.
|
|
875
906
|
*
|
|
@@ -963,9 +994,8 @@ async function promptInstallOptions(agents, opts) {
|
|
|
963
994
|
console.log(dim(` Scope: ${bold(scopeLabel)} | Method: ${bold(methodLabel)}`));
|
|
964
995
|
console.log(dim(` Agents (${selectedAgents.length}):`));
|
|
965
996
|
for (const agent of selectedAgents) {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
: resolveSkillsPath(safeProjectRoot(), agent.localSkillsDir);
|
|
997
|
+
// 0894: same resolver the installs use, so the preview cannot drift.
|
|
998
|
+
const base = resolveInstallBase({ ...opts, global: useGlobal }, agent);
|
|
969
999
|
console.log(dim(` ${agent.displayName}: ${base}/`));
|
|
970
1000
|
}
|
|
971
1001
|
console.log(dim(" ─────────────────────────────────────────────────"));
|
|
@@ -1220,11 +1250,23 @@ async function installPluginDir(basePath, pluginName, opts, repoUrl) {
|
|
|
1220
1250
|
const version = existsSync(mktPath)
|
|
1221
1251
|
? getPluginVersion(pluginName, readFileSync(mktPath, "utf-8")) || "0.0.0"
|
|
1222
1252
|
: "0.0.0";
|
|
1253
|
+
if (opts.dryRun) {
|
|
1254
|
+
printDryRunPlan([pluginName], selectedAgents, opts);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1223
1257
|
// Install: recursively copy plugin directory to each agent
|
|
1224
1258
|
const sha = computeSha(content);
|
|
1225
1259
|
const locations = [];
|
|
1260
|
+
const doneBases = new Set();
|
|
1226
1261
|
for (const agent of selectedAgents) {
|
|
1227
|
-
const
|
|
1262
|
+
const base = resolveInstallBase(opts, agent);
|
|
1263
|
+
const cacheDir = join(base, pluginName);
|
|
1264
|
+
// 0894: shared readers share one copy in .agents/skills.
|
|
1265
|
+
if (doneBases.has(base)) {
|
|
1266
|
+
locations.push(`${agent.displayName}: ${cacheDir}`);
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
doneBases.add(base);
|
|
1228
1270
|
try {
|
|
1229
1271
|
// Full clean before copy: removes stale files from older installs
|
|
1230
1272
|
if (existsSync(cacheDir)) {
|
|
@@ -1294,7 +1336,17 @@ async function installOneGitHubSkill(owner, repo, skillName, rawUrl, opts, agent
|
|
|
1294
1336
|
* through to the lockfile so the studio detail-header anchor resolves to
|
|
1295
1337
|
* the actual file (not /blob/HEAD/SKILL.md at the repo root).
|
|
1296
1338
|
*/
|
|
1297
|
-
sourceSkillPath
|
|
1339
|
+
sourceSkillPath,
|
|
1340
|
+
/**
|
|
1341
|
+
* 0894: the ONE ref (commit SHA when known) that SKILL.md, the bundle and
|
|
1342
|
+
* the lockfile all use, so they can never come from different commits.
|
|
1343
|
+
*/
|
|
1344
|
+
ref,
|
|
1345
|
+
/** 0894: lockfile `files` of the previous install of this skill. */
|
|
1346
|
+
previousFiles) {
|
|
1347
|
+
if (ref && sourceSkillPath) {
|
|
1348
|
+
rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${sourceSkillPath}`;
|
|
1349
|
+
}
|
|
1298
1350
|
// Blocklist + rejection check BEFORE fetching (prevents misleading 404)
|
|
1299
1351
|
const repoUrl = `https://github.com/${owner}/${repo}`;
|
|
1300
1352
|
const safety = await checkRepoInstallSafety(skillName, owner, repo);
|
|
@@ -1360,9 +1412,29 @@ sourceSkillPath) {
|
|
|
1360
1412
|
}
|
|
1361
1413
|
return { skillName, installed: false, verdict: scanResult.verdict, score: scanResult.score };
|
|
1362
1414
|
}
|
|
1363
|
-
//
|
|
1415
|
+
// 0894: the complete skill folder (nested dirs, binaries, exec bits).
|
|
1416
|
+
let bundleFiles;
|
|
1417
|
+
let bundleExecutables;
|
|
1418
|
+
let installedFiles;
|
|
1419
|
+
if (sourceSkillPath) {
|
|
1420
|
+
const prepared = await prepareGitHubSkillBundle({
|
|
1421
|
+
owner,
|
|
1422
|
+
repo,
|
|
1423
|
+
ref: ref || (await getDefaultBranch(owner, repo)),
|
|
1424
|
+
skillMdPath: sourceSkillPath,
|
|
1425
|
+
force: !!opts.force,
|
|
1426
|
+
});
|
|
1427
|
+
if (!prepared.ok) {
|
|
1428
|
+
console.log(red(` ${prepared.message}`));
|
|
1429
|
+
return { skillName, installed: false, verdict: prepared.verdict, score: scanResult.score };
|
|
1430
|
+
}
|
|
1431
|
+
bundleFiles = Object.keys(prepared.bundle.files).length > 0 ? prepared.bundle.files : undefined;
|
|
1432
|
+
bundleExecutables = prepared.bundle.executables;
|
|
1433
|
+
installedFiles = prepared.bundle.fileList;
|
|
1434
|
+
}
|
|
1435
|
+
// Fetch agent files (agents/*.md) if discovered and the path is unknown
|
|
1364
1436
|
let agentFiles;
|
|
1365
|
-
if (agentRawUrls && Object.keys(agentRawUrls).length > 0) {
|
|
1437
|
+
if (!sourceSkillPath && agentRawUrls && Object.keys(agentRawUrls).length > 0) {
|
|
1366
1438
|
agentFiles = {};
|
|
1367
1439
|
const fetches = Object.entries(agentRawUrls).map(async ([relPath, url]) => {
|
|
1368
1440
|
try {
|
|
@@ -1389,7 +1461,9 @@ sourceSkillPath) {
|
|
|
1389
1461
|
scope: opts.global ? "user" : "project",
|
|
1390
1462
|
projectRoot,
|
|
1391
1463
|
copy: !!opts.copy,
|
|
1392
|
-
bundleFiles: agentFiles,
|
|
1464
|
+
bundleFiles: bundleFiles ?? agentFiles,
|
|
1465
|
+
bundleExecutables,
|
|
1466
|
+
previousFiles,
|
|
1393
1467
|
});
|
|
1394
1468
|
}
|
|
1395
1469
|
catch (err) {
|
|
@@ -1397,7 +1471,7 @@ sourceSkillPath) {
|
|
|
1397
1471
|
return { skillName, installed: false, verdict: "WRITE_ERROR", score: scanResult.score };
|
|
1398
1472
|
}
|
|
1399
1473
|
const skillVersion = extractFrontmatterVersion(content) || "1.0.0";
|
|
1400
|
-
return { skillName, installed: true, verdict: scanResult.verdict, score: scanResult.score, sha, version: skillVersion, sourceSkillPath };
|
|
1474
|
+
return { skillName, installed: true, verdict: scanResult.verdict, score: scanResult.score, sha, version: skillVersion, sourceSkillPath, files: installedFiles };
|
|
1401
1475
|
}
|
|
1402
1476
|
// ---------------------------------------------------------------------------
|
|
1403
1477
|
// Bulk repo plugin installation: --repo with --all
|
|
@@ -1649,9 +1723,29 @@ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
|
|
|
1649
1723
|
const sha = computeSha(combined);
|
|
1650
1724
|
const skillFileNames = skills.map((s) => s.name === pluginName ? "SKILL.md" : `${s.name}/SKILL.md`).sort();
|
|
1651
1725
|
const locations = [];
|
|
1726
|
+
if (opts.dryRun) {
|
|
1727
|
+
printDryRunPlan([pluginName], selectedAgents, opts);
|
|
1728
|
+
const enableResult = enableAfterInstall(pluginName, {}, opts);
|
|
1729
|
+
const report = buildPerAgentReport({
|
|
1730
|
+
skill: pluginName,
|
|
1731
|
+
scope: enableResult.scope,
|
|
1732
|
+
action: "not-applicable",
|
|
1733
|
+
agents: selectedAgents,
|
|
1734
|
+
});
|
|
1735
|
+
for (const line of report)
|
|
1736
|
+
console.log(` ${dim(">")} ${line.line}`);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const doneBases = new Set();
|
|
1652
1740
|
for (const agent of selectedAgents) {
|
|
1653
1741
|
const baseDir = resolveInstallBase(opts, agent);
|
|
1654
1742
|
const plugDir = basename(baseDir) === pluginName ? baseDir : join(baseDir, pluginName);
|
|
1743
|
+
// 0894: shared readers share one copy in .agents/skills.
|
|
1744
|
+
if (doneBases.has(baseDir)) {
|
|
1745
|
+
locations.push(`${agent.displayName}: ${plugDir}`);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
doneBases.add(baseDir);
|
|
1655
1749
|
try {
|
|
1656
1750
|
// Skills: {agent-dir}/{plugin-name}/{skill-name}/SKILL.md
|
|
1657
1751
|
for (const skill of skills) {
|
|
@@ -1702,9 +1796,9 @@ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
|
|
|
1702
1796
|
// hence no rollback wiring here) — the gate only surfaces the same
|
|
1703
1797
|
// acknowledgement + per-agent report as the marketplace path. The identical
|
|
1704
1798
|
// opt-in condition keeps the default flow unchanged (NFR-005).
|
|
1799
|
+
// (--dry-run, also an opt-in, returned earlier with its own preview.)
|
|
1705
1800
|
const userOptedIn = opts.scope !== undefined ||
|
|
1706
1801
|
opts.global === true ||
|
|
1707
|
-
opts.dryRun === true ||
|
|
1708
1802
|
opts.enable === false;
|
|
1709
1803
|
if (userOptedIn) {
|
|
1710
1804
|
const enableResult = enableAfterInstall(pluginName, lock.skills[pluginName], opts);
|
|
@@ -1989,12 +2083,20 @@ async function addCommandInner(source, opts) {
|
|
|
1989
2083
|
return process.exit(0);
|
|
1990
2084
|
}
|
|
1991
2085
|
}
|
|
2086
|
+
if (opts.dryRun) {
|
|
2087
|
+
printDryRunPlan(selectedSkills.map((s) => s.name), selectedAgents, opts);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
// 0894: resolve the commit ONCE; SKILL.md, bundle and lockfile all use it.
|
|
2091
|
+
const sourceBranch = await getDefaultBranch(owner, repo);
|
|
2092
|
+
const sourceCommitSha = await getBranchHeadSha(owner, repo, sourceBranch);
|
|
2093
|
+
const priorSkills = readLockfile(lockfileRoot(opts))?.skills ?? {};
|
|
1992
2094
|
// Install selected skills
|
|
1993
2095
|
const results = [];
|
|
1994
2096
|
for (const skill of selectedSkills) {
|
|
1995
2097
|
console.log(dim(`\nInstalling skill: ${bold(skill.name)}...`));
|
|
1996
2098
|
try {
|
|
1997
|
-
const result = await installOneGitHubSkill(owner, repo, skill.name, skill.rawUrl, opts, selectedAgents, projectRoot, skill.agentRawUrls, skill.path);
|
|
2099
|
+
const result = await installOneGitHubSkill(owner, repo, skill.name, skill.rawUrl, opts, selectedAgents, projectRoot, skill.agentRawUrls, skill.path, sourceCommitSha || sourceBranch, priorSkills[skill.name]?.files);
|
|
1998
2100
|
results.push(result);
|
|
1999
2101
|
}
|
|
2000
2102
|
catch (err) {
|
|
@@ -2007,8 +2109,6 @@ async function addCommandInner(source, opts) {
|
|
|
2007
2109
|
// `sourceSkillPath` are persisted alongside the legacy `source` string.
|
|
2008
2110
|
const lockDir = lockfileRoot(opts);
|
|
2009
2111
|
const lock = ensureLockfile(lockDir);
|
|
2010
|
-
const sourceBranch = await getDefaultBranch(owner, repo);
|
|
2011
|
-
const sourceCommitSha = await getBranchHeadSha(owner, repo, sourceBranch);
|
|
2012
2112
|
for (const r of results) {
|
|
2013
2113
|
if (r.installed && r.sha) {
|
|
2014
2114
|
lock.skills[r.skillName] = buildGitHubInstallLockEntry({
|
|
@@ -2021,6 +2121,7 @@ async function addCommandInner(source, opts) {
|
|
|
2021
2121
|
commitSha: sourceCommitSha,
|
|
2022
2122
|
pluginName: pluginNameFromSkillPath(r.sourceSkillPath),
|
|
2023
2123
|
global: !!opts.global,
|
|
2124
|
+
files: r.files,
|
|
2024
2125
|
});
|
|
2025
2126
|
}
|
|
2026
2127
|
}
|
|
@@ -2384,10 +2485,21 @@ async function installFromRegistry(skillName, opts) {
|
|
|
2384
2485
|
// Install to each agent
|
|
2385
2486
|
const sha = computeSha(content);
|
|
2386
2487
|
const locations = [];
|
|
2488
|
+
if (opts.dryRun) {
|
|
2489
|
+
printDryRunPlan([skillName], selectedAgents, opts);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2387
2492
|
const processedContent = ensureFrontmatter(content, skillName);
|
|
2493
|
+
const doneBases = new Set();
|
|
2388
2494
|
for (const agent of selectedAgents) {
|
|
2389
2495
|
const baseDir = resolveInstallBase(opts, agent);
|
|
2390
2496
|
const skillDir = join(baseDir, skillName);
|
|
2497
|
+
// 0894: shared readers share one copy in .agents/skills.
|
|
2498
|
+
if (doneBases.has(baseDir)) {
|
|
2499
|
+
locations.push(`${agent.displayName}: ${skillDir}`);
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
doneBases.add(baseDir);
|
|
2391
2503
|
try {
|
|
2392
2504
|
mkdirSync(skillDir, { recursive: true });
|
|
2393
2505
|
writeFileSync(join(skillDir, "SKILL.md"), processedContent, "utf-8");
|
|
@@ -2465,6 +2577,8 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2465
2577
|
}
|
|
2466
2578
|
const branch = refOverride || (await getDefaultBranch(owner, repo));
|
|
2467
2579
|
const sourceCommitSha = await getBranchHeadSha(owner, repo, branch);
|
|
2580
|
+
// 0894: SKILL.md, the bundle and the lockfile all read this one commit.
|
|
2581
|
+
const contentRef = sourceCommitSha || branch;
|
|
2468
2582
|
// Candidate subpaths: explicit override wins; otherwise try the conventional
|
|
2469
2583
|
// skills/{name}/ layout first, then root-level {name}/ (e.g. higgsfield-ai/skills).
|
|
2470
2584
|
const subpathCandidates = skillSubpathOverride
|
|
@@ -2477,7 +2591,7 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2477
2591
|
let content;
|
|
2478
2592
|
for (let i = 0; i < subpathCandidates.length; i++) {
|
|
2479
2593
|
const candidate = subpathCandidates[i];
|
|
2480
|
-
const candidateUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${
|
|
2594
|
+
const candidateUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${contentRef}/${candidate}`;
|
|
2481
2595
|
const isLast = i === subpathCandidates.length - 1;
|
|
2482
2596
|
if (isLast) {
|
|
2483
2597
|
content = await fetchSkillContent(candidateUrl);
|
|
@@ -2493,53 +2607,6 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2493
2607
|
}
|
|
2494
2608
|
if (content === undefined)
|
|
2495
2609
|
return process.exit(1);
|
|
2496
|
-
// Fetch the skill's bundled subdirectories when the skill lives in a
|
|
2497
|
-
// subdirectory of the repo. These are the skill-authoring convention and the
|
|
2498
|
-
// same prefixes the installer accepts (see installer/bundle-files.ts):
|
|
2499
|
-
// agents/ holds sub-agent prompts, references/ the docs SKILL.md links to,
|
|
2500
|
-
// scripts/ the executable tooling, assets/ static files, tests/ its tests.
|
|
2501
|
-
// A skill whose SKILL.md says "run scripts/foo.py" and installs without
|
|
2502
|
-
// scripts/ is not degraded — it is broken.
|
|
2503
|
-
let legacyAgentFiles;
|
|
2504
|
-
if (skill) {
|
|
2505
|
-
let fetched = 0;
|
|
2506
|
-
for (const subdir of BUNDLE_SUBDIRS) {
|
|
2507
|
-
try {
|
|
2508
|
-
const basePath = skillSubpath.replace(/\/SKILL\.md$/, `/${subdir}`);
|
|
2509
|
-
// ?ref= keeps the listing on the same branch as SKILL.md
|
|
2510
|
-
// (deep links may pin a non-default branch).
|
|
2511
|
-
const dirUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${basePath}?ref=${encodeURIComponent(branch)}`;
|
|
2512
|
-
const dirRes = await githubFetch(dirUrl, { headers: { Accept: "application/vnd.github.v3+json" } });
|
|
2513
|
-
if (!dirRes.ok)
|
|
2514
|
-
continue;
|
|
2515
|
-
const entries = (await dirRes.json());
|
|
2516
|
-
const wanted = entries.filter((e) => e.type !== "dir" &&
|
|
2517
|
-
isBundleTextFile(e.name) &&
|
|
2518
|
-
(e.size ?? 0) <= MAX_BUNDLE_FILE_BYTES &&
|
|
2519
|
-
e.download_url &&
|
|
2520
|
-
isGitHubDownloadUrl(e.download_url));
|
|
2521
|
-
if (wanted.length === 0)
|
|
2522
|
-
continue;
|
|
2523
|
-
legacyAgentFiles ??= {};
|
|
2524
|
-
const budget = wanted.slice(0, Math.max(0, MAX_BUNDLE_FILES - fetched));
|
|
2525
|
-
fetched += budget.length;
|
|
2526
|
-
const fetches = budget.map(async (entry) => {
|
|
2527
|
-
try {
|
|
2528
|
-
const res = await githubFetch(entry.download_url);
|
|
2529
|
-
if (res.ok)
|
|
2530
|
-
legacyAgentFiles[`${subdir}/${entry.name}`] = await res.text();
|
|
2531
|
-
}
|
|
2532
|
-
catch { /* skip */ }
|
|
2533
|
-
});
|
|
2534
|
-
await Promise.allSettled(fetches);
|
|
2535
|
-
if (fetched >= MAX_BUNDLE_FILES)
|
|
2536
|
-
break;
|
|
2537
|
-
}
|
|
2538
|
-
catch { /* subdir doesn't exist or API error — fine */ }
|
|
2539
|
-
}
|
|
2540
|
-
if (legacyAgentFiles && Object.keys(legacyAgentFiles).length === 0)
|
|
2541
|
-
legacyAgentFiles = undefined;
|
|
2542
|
-
}
|
|
2543
2610
|
// Platform security check (best-effort, non-blocking on network error).
|
|
2544
2611
|
// Skipped for private repos: the registry looks skills up by name, so it
|
|
2545
2612
|
// would leak the name and could match an unrelated public skill.
|
|
@@ -2624,6 +2691,26 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2624
2691
|
opts.global = true;
|
|
2625
2692
|
if (!selections.symlink)
|
|
2626
2693
|
opts.copy = true;
|
|
2694
|
+
if (opts.dryRun) {
|
|
2695
|
+
printDryRunPlan([skillName], selectedAgents, opts);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
// 0894: the complete skill folder — every nested file, binaries as bytes,
|
|
2699
|
+
// exec bits kept — read from the same commit, scanned, never truncated; not
|
|
2700
|
+
// downloaded for --dry-run.
|
|
2701
|
+
const prepared = await prepareGitHubSkillBundle({
|
|
2702
|
+
owner,
|
|
2703
|
+
repo,
|
|
2704
|
+
ref: contentRef,
|
|
2705
|
+
skillMdPath: skillSubpath,
|
|
2706
|
+
force: !!opts.force,
|
|
2707
|
+
});
|
|
2708
|
+
if (!prepared.ok) {
|
|
2709
|
+
console.error(red(`\n${prepared.message}`));
|
|
2710
|
+
return process.exit(1);
|
|
2711
|
+
}
|
|
2712
|
+
const bundle = prepared.bundle;
|
|
2713
|
+
const previousFiles = readLockfile(lockfileRoot(opts))?.skills[skillName]?.files;
|
|
2627
2714
|
// Install to each agent using canonical installer (flat directory, namespaced frontmatter name)
|
|
2628
2715
|
const namespacedName = pluginNamespace && skillName !== pluginNamespace
|
|
2629
2716
|
? `${pluginNamespace}/${skillName}`
|
|
@@ -2648,7 +2735,9 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2648
2735
|
scope: opts.global ? "user" : "project",
|
|
2649
2736
|
projectRoot,
|
|
2650
2737
|
copy: !!opts.copy,
|
|
2651
|
-
bundleFiles:
|
|
2738
|
+
bundleFiles: Object.keys(bundle.files).length > 0 ? bundle.files : undefined,
|
|
2739
|
+
bundleExecutables: bundle.executables,
|
|
2740
|
+
previousFiles,
|
|
2652
2741
|
});
|
|
2653
2742
|
// Update lockfile (global → ~/.agents/, project → project root).
|
|
2654
2743
|
// 0743: persist `sourceSkillPath` from the locally-derived `skillSubpath`
|
|
@@ -2667,6 +2756,7 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2667
2756
|
commitSha: sourceCommitSha,
|
|
2668
2757
|
pluginName: pluginNamespace ?? pluginNameFromSkillPath(skillSubpath),
|
|
2669
2758
|
global: !!opts.global,
|
|
2759
|
+
files: bundle.fileList,
|
|
2670
2760
|
});
|
|
2671
2761
|
lock.agents = [...new Set([...(lock.agents || []), ...selectedAgents.map((a) => a.id)])];
|
|
2672
2762
|
writeLockfile(lock, lockDir);
|