vskill 1.2.4 → 1.2.7
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 +51 -3
- 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/audit/fix-suggestions.js +6 -0
- package/dist/audit/fix-suggestions.js.map +1 -1
- 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 +5 -7
- package/dist/commands/add.js +207 -111
- 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/scan.js +1 -1
- package/dist/commands/scan.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/{CreateSkillPage-BUrgworx.js → CreateSkillPage-CXDd6gDN.js} +1 -1
- package/dist/eval-ui/assets/{FindSkillsPalette-BstJ22TI.js → FindSkillsPalette-DQ6uGhFP.js} +2 -2
- package/dist/eval-ui/assets/{SearchPaletteCore-Dagnc-E7.js → SearchPaletteCore-DpHCf0mf.js} +1 -1
- package/dist/eval-ui/assets/{SkillDetailPanel-DnujVk6g.js → SkillDetailPanel-BjHFjGxg.js} +1 -1
- package/dist/eval-ui/assets/{UpdateDropdown-B7D2vXrm.js → UpdateDropdown-Cn4Vsq5d.js} +1 -1
- package/dist/eval-ui/assets/main-BmB7Zj9I.js +86 -0
- package/dist/eval-ui/index.html +1 -1
- 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/scanner/hidden-content.d.ts +47 -0
- package/dist/scanner/hidden-content.js +904 -0
- package/dist/scanner/hidden-content.js.map +1 -0
- package/dist/scanner/installer-hosts.d.ts +26 -0
- package/dist/scanner/installer-hosts.js +92 -0
- package/dist/scanner/installer-hosts.js.map +1 -0
- package/dist/scanner/judge-guard.d.ts +119 -0
- package/dist/scanner/judge-guard.js +481 -0
- package/dist/scanner/judge-guard.js.map +1 -0
- package/dist/scanner/patterns.d.ts +12 -0
- package/dist/scanner/patterns.js +353 -113
- package/dist/scanner/patterns.js.map +1 -1
- package/dist/scanner/repo-scanner.d.ts +4 -0
- package/dist/scanner/repo-scanner.js +4 -1
- package/dist/scanner/repo-scanner.js.map +1 -1
- package/dist/scanner/tier1.js +37 -7
- package/dist/scanner/tier1.js.map +1 -1
- package/dist/scanner/tier2-llm.d.ts +22 -4
- package/dist/scanner/tier2-llm.js +163 -23
- package/dist/scanner/tier2-llm.js.map +1 -1
- package/dist/scanner/types.d.ts +8 -0
- 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 +1 -1
- package/dist/eval-ui/assets/main-DjA5gpjV.js +0 -86
package/dist/commands/add.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
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";
|
|
16
|
+
import { isBinary } from "../scanner/repo-scanner.js";
|
|
14
17
|
import { getAvailablePlugins, getPluginSource, getPluginVersion, hasPlugin, discoverUnregisteredPlugins } from "../marketplace/index.js";
|
|
15
18
|
import { checkInstallSafety, checkLocalInstallSafety } from "../blocklist/blocklist.js";
|
|
16
19
|
import { getSkill, searchSkills } from "../api/client.js";
|
|
@@ -36,30 +39,9 @@ import { extractFrontmatterVersion } from "../utils/version.js";
|
|
|
36
39
|
import { claudePluginInstall, claudePluginUninstall, claudePluginMarketplaceAdd, claudePluginMarketplaceList, } from "../utils/claude-plugin.js";
|
|
37
40
|
import { buildPerAgentReport, resolvePluginId, } from "../lib/skill-lifecycle.js";
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
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
|
-
}
|
|
42
|
+
// 0894: a GitHub skill's complete folder is fetched by
|
|
43
|
+
// prepareGitHubSkillBundle (./add-github-bundle.ts) — every nested file,
|
|
44
|
+
// binaries as bytes, exec bits kept, limits that fail instead of truncating.
|
|
63
45
|
/** Validate that a download_url from GitHub Contents API points to a trusted GitHub domain. */
|
|
64
46
|
function isGitHubDownloadUrl(url) {
|
|
65
47
|
try {
|
|
@@ -340,24 +322,45 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
340
322
|
const toUninstall = usedInteractiveCheckbox
|
|
341
323
|
? [...installedSet].filter((name) => !selectedNames.has(name))
|
|
342
324
|
: [];
|
|
325
|
+
// 0894: --dry-run stops here, before the deselected-plugin uninstall, the
|
|
326
|
+
// registry submissions and every file or lockfile write below.
|
|
327
|
+
if (opts.dryRun) {
|
|
328
|
+
const dry = await promptInstallOptions(await detectInstalledAgents(), opts);
|
|
329
|
+
if (dry.global)
|
|
330
|
+
opts.global = true;
|
|
331
|
+
printDryRunPlan([...selectedNames], dry.agents, opts, toUninstall);
|
|
332
|
+
for (const p of selectedPlugins) {
|
|
333
|
+
enableAfterInstall(p.name, { marketplace: marketplaceName || undefined }, opts);
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
343
337
|
if (toUninstall.length > 0) {
|
|
344
338
|
console.log(dim(`\nUninstalling ${toUninstall.length} deselected plugin${toUninstall.length === 1 ? "" : "s"}...\n`));
|
|
345
339
|
const agents = await detectInstalledAgents();
|
|
346
340
|
for (const skillName of toUninstall) {
|
|
347
341
|
// Remove skill directories from all agents
|
|
348
342
|
let removedCount = 0;
|
|
343
|
+
const dirs = new Set();
|
|
349
344
|
for (const agent of agents) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
345
|
+
dirs.add(join(process.cwd(), agent.localSkillsDir, skillName));
|
|
346
|
+
// 0894: every global dir vskill may have installed into
|
|
347
|
+
// (Antigravity's legacy ~/.gemini/antigravity/skills included).
|
|
348
|
+
for (const globalDir of agentGlobalInstallDirs(agent, resolveTilde)) {
|
|
349
|
+
dirs.add(join(globalDir, skillName));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// 0894: shared readers are installed through the canonical
|
|
353
|
+
// .agents/skills folder alone, so the canonical copy for the scope this
|
|
354
|
+
// lockfile tracks goes too (symmetric with install).
|
|
355
|
+
dirs.add(join(lockDir, lockDir === join(os.homedir(), ".agents") ? "" : ".agents", "skills", skillName));
|
|
356
|
+
for (const dir of dirs) {
|
|
357
|
+
if (pathPresent(dir)) {
|
|
358
|
+
try {
|
|
359
|
+
rmSync(dir, { recursive: true, force: true });
|
|
360
|
+
removedCount++;
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
console.warn(`[vskill] Could not remove ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
361
364
|
}
|
|
362
365
|
}
|
|
363
366
|
}
|
|
@@ -518,8 +521,7 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
518
521
|
// not mutate already-valid frontmatter (no-touch principle).
|
|
519
522
|
const isRenaming = sd.name !== plugin.name;
|
|
520
523
|
const processedContent = ensureFrontmatter(content, namespacedName, isRenaming);
|
|
521
|
-
for (const
|
|
522
|
-
const baseDir = resolveInstallBase(opts, agent);
|
|
524
|
+
for (const baseDir of uniqueInstallBases(opts, agents)) {
|
|
523
525
|
const skillDir = join(baseDir, sd.name);
|
|
524
526
|
mkdirSync(skillDir, { recursive: true });
|
|
525
527
|
writeFileSync(join(skillDir, "SKILL.md"), processedContent, "utf-8");
|
|
@@ -535,8 +537,7 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
535
537
|
if (contentRes.ok) {
|
|
536
538
|
const content = await contentRes.text();
|
|
537
539
|
const processedContent = ensureFrontmatter(content, plugin.name);
|
|
538
|
-
for (const
|
|
539
|
-
const baseDir = resolveInstallBase(opts, agent);
|
|
540
|
+
for (const baseDir of uniqueInstallBases(opts, agents)) {
|
|
540
541
|
const skillDir = join(baseDir, plugin.name);
|
|
541
542
|
mkdirSync(skillDir, { recursive: true });
|
|
542
543
|
writeFileSync(join(skillDir, "SKILL.md"), processedContent, "utf-8");
|
|
@@ -613,9 +614,9 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
|
|
|
613
614
|
// unenabled. The user saw the skill on disk but Claude Code didn't load
|
|
614
615
|
// it. Treat --global as opt-in to the enable hook (scope = user, since
|
|
615
616
|
// --global is per-machine, not per-repo).
|
|
617
|
+
// (--dry-run, also an opt-in, returned earlier with its own preview.)
|
|
616
618
|
const userOptedIn = opts.scope !== undefined ||
|
|
617
619
|
opts.global === true ||
|
|
618
|
-
opts.dryRun === true ||
|
|
619
620
|
opts.enable === false;
|
|
620
621
|
if (userOptedIn) {
|
|
621
622
|
const enabledSoFar = [];
|
|
@@ -861,15 +862,46 @@ function resolveSkillsPath(base, localSkillsDir) {
|
|
|
861
862
|
* Resolve the base directory for local skill installation.
|
|
862
863
|
*
|
|
863
864
|
* Priority:
|
|
864
|
-
* 1.
|
|
865
|
-
*
|
|
865
|
+
* 1. 0894: an agent that reads the shared `.agents/skills` in this scope ->
|
|
866
|
+
* that shared folder (same rule as the canonical installer)
|
|
867
|
+
* 2. `--global` -> agent's global skills dir (Antigravity: legacy-aware)
|
|
868
|
+
* 3. default -> process.cwd() + agent's localSkillsDir
|
|
866
869
|
*/
|
|
867
870
|
function resolveInstallBase(opts, agent) {
|
|
868
|
-
|
|
869
|
-
|
|
871
|
+
const global = !!opts.global;
|
|
872
|
+
if (agent.readsSharedSkills?.[global ? "global" : "project"] === true) {
|
|
873
|
+
return global ? resolveTilde("~/.agents/skills") : join(safeProjectRoot(), ".agents", "skills");
|
|
874
|
+
}
|
|
875
|
+
if (global) {
|
|
876
|
+
return resolveAgentGlobalSkillsDir({ id: agent.id ?? "", globalSkillsDir: agent.globalSkillsDir }, resolveTilde);
|
|
870
877
|
}
|
|
871
878
|
return resolveSkillsPath(safeProjectRoot(), agent.localSkillsDir);
|
|
872
879
|
}
|
|
880
|
+
/** 0894: each distinct install base once — shared readers (Cursor, Codex,
|
|
881
|
+
* ...) all resolve to `.agents/skills`, which must get one copy, not one
|
|
882
|
+
* per agent. Keeps the first agent seen for each base, in order. */
|
|
883
|
+
function uniqueInstallBases(opts, agents) {
|
|
884
|
+
return [...new Set(agents.map((a) => resolveInstallBase(opts, a)))];
|
|
885
|
+
}
|
|
886
|
+
/** 0894: true for anything at `p`, including a link whose target is gone. */
|
|
887
|
+
function pathPresent(p) {
|
|
888
|
+
if (existsSync(p))
|
|
889
|
+
return true;
|
|
890
|
+
try {
|
|
891
|
+
return lstatSync(p).isSymbolicLink();
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/** 0894: the --dry-run report every install path prints instead of writing. */
|
|
898
|
+
function printDryRunPlan(names, agents, opts, uninstall = []) {
|
|
899
|
+
if (uninstall.length > 0) {
|
|
900
|
+
console.log(`\nDry run: would uninstall ${uninstall.map((n) => bold(n)).join(", ")}.`);
|
|
901
|
+
}
|
|
902
|
+
console.log(`\nDry run: would install ${names.map((n) => bold(n)).join(", ")} ` +
|
|
903
|
+
`(${opts.global ? "global" : "project"}) for ${agents.map((a) => a.id).join(", ")}. Nothing was written.`);
|
|
904
|
+
}
|
|
873
905
|
/**
|
|
874
906
|
* Prompt the user for agent selection and install method.
|
|
875
907
|
*
|
|
@@ -963,9 +995,8 @@ async function promptInstallOptions(agents, opts) {
|
|
|
963
995
|
console.log(dim(` Scope: ${bold(scopeLabel)} | Method: ${bold(methodLabel)}`));
|
|
964
996
|
console.log(dim(` Agents (${selectedAgents.length}):`));
|
|
965
997
|
for (const agent of selectedAgents) {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
: resolveSkillsPath(safeProjectRoot(), agent.localSkillsDir);
|
|
998
|
+
// 0894: same resolver the installs use, so the preview cannot drift.
|
|
999
|
+
const base = resolveInstallBase({ ...opts, global: useGlobal }, agent);
|
|
969
1000
|
console.log(dim(` ${agent.displayName}: ${base}/`));
|
|
970
1001
|
}
|
|
971
1002
|
console.log(dim(" ─────────────────────────────────────────────────"));
|
|
@@ -1087,17 +1118,22 @@ function listPluginDirPlugins(basePath) {
|
|
|
1087
1118
|
* Collect all file contents from a plugin directory for security scanning.
|
|
1088
1119
|
* Concatenates content from all readable files.
|
|
1089
1120
|
*/
|
|
1090
|
-
function collectPluginContent(pluginDir) {
|
|
1121
|
+
export function collectPluginContent(pluginDir) {
|
|
1091
1122
|
const files = readdirSync(pluginDir, { recursive: true });
|
|
1092
1123
|
const parts = [];
|
|
1093
1124
|
for (const file of files) {
|
|
1094
1125
|
const fullPath = join(pluginDir, file);
|
|
1095
1126
|
try {
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1127
|
+
const buffer = readFileSync(fullPath);
|
|
1128
|
+
// Images and other binaries decode to junk that looks like hidden
|
|
1129
|
+
// Unicode (a GIF header can spell tag characters); they are not text
|
|
1130
|
+
// an agent reads, so they are not scanned as text.
|
|
1131
|
+
if (isBinary(buffer))
|
|
1132
|
+
continue;
|
|
1133
|
+
parts.push(`--- ${file} ---\n${buffer.toString("utf-8")}`);
|
|
1098
1134
|
}
|
|
1099
1135
|
catch {
|
|
1100
|
-
// Skip unreadable files (directories,
|
|
1136
|
+
// Skip unreadable files (directories, etc.)
|
|
1101
1137
|
}
|
|
1102
1138
|
}
|
|
1103
1139
|
return parts.join("\n");
|
|
@@ -1220,11 +1256,23 @@ async function installPluginDir(basePath, pluginName, opts, repoUrl) {
|
|
|
1220
1256
|
const version = existsSync(mktPath)
|
|
1221
1257
|
? getPluginVersion(pluginName, readFileSync(mktPath, "utf-8")) || "0.0.0"
|
|
1222
1258
|
: "0.0.0";
|
|
1259
|
+
if (opts.dryRun) {
|
|
1260
|
+
printDryRunPlan([pluginName], selectedAgents, opts);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1223
1263
|
// Install: recursively copy plugin directory to each agent
|
|
1224
1264
|
const sha = computeSha(content);
|
|
1225
1265
|
const locations = [];
|
|
1266
|
+
const doneBases = new Set();
|
|
1226
1267
|
for (const agent of selectedAgents) {
|
|
1227
|
-
const
|
|
1268
|
+
const base = resolveInstallBase(opts, agent);
|
|
1269
|
+
const cacheDir = join(base, pluginName);
|
|
1270
|
+
// 0894: shared readers share one copy in .agents/skills.
|
|
1271
|
+
if (doneBases.has(base)) {
|
|
1272
|
+
locations.push(`${agent.displayName}: ${cacheDir}`);
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
doneBases.add(base);
|
|
1228
1276
|
try {
|
|
1229
1277
|
// Full clean before copy: removes stale files from older installs
|
|
1230
1278
|
if (existsSync(cacheDir)) {
|
|
@@ -1294,7 +1342,17 @@ async function installOneGitHubSkill(owner, repo, skillName, rawUrl, opts, agent
|
|
|
1294
1342
|
* through to the lockfile so the studio detail-header anchor resolves to
|
|
1295
1343
|
* the actual file (not /blob/HEAD/SKILL.md at the repo root).
|
|
1296
1344
|
*/
|
|
1297
|
-
sourceSkillPath
|
|
1345
|
+
sourceSkillPath,
|
|
1346
|
+
/**
|
|
1347
|
+
* 0894: the ONE ref (commit SHA when known) that SKILL.md, the bundle and
|
|
1348
|
+
* the lockfile all use, so they can never come from different commits.
|
|
1349
|
+
*/
|
|
1350
|
+
ref,
|
|
1351
|
+
/** 0894: lockfile `files` of the previous install of this skill. */
|
|
1352
|
+
previousFiles) {
|
|
1353
|
+
if (ref && sourceSkillPath) {
|
|
1354
|
+
rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${sourceSkillPath}`;
|
|
1355
|
+
}
|
|
1298
1356
|
// Blocklist + rejection check BEFORE fetching (prevents misleading 404)
|
|
1299
1357
|
const repoUrl = `https://github.com/${owner}/${repo}`;
|
|
1300
1358
|
const safety = await checkRepoInstallSafety(skillName, owner, repo);
|
|
@@ -1360,9 +1418,29 @@ sourceSkillPath) {
|
|
|
1360
1418
|
}
|
|
1361
1419
|
return { skillName, installed: false, verdict: scanResult.verdict, score: scanResult.score };
|
|
1362
1420
|
}
|
|
1363
|
-
//
|
|
1421
|
+
// 0894: the complete skill folder (nested dirs, binaries, exec bits).
|
|
1422
|
+
let bundleFiles;
|
|
1423
|
+
let bundleExecutables;
|
|
1424
|
+
let installedFiles;
|
|
1425
|
+
if (sourceSkillPath) {
|
|
1426
|
+
const prepared = await prepareGitHubSkillBundle({
|
|
1427
|
+
owner,
|
|
1428
|
+
repo,
|
|
1429
|
+
ref: ref || (await getDefaultBranch(owner, repo)),
|
|
1430
|
+
skillMdPath: sourceSkillPath,
|
|
1431
|
+
force: !!opts.force,
|
|
1432
|
+
});
|
|
1433
|
+
if (!prepared.ok) {
|
|
1434
|
+
console.log(red(` ${prepared.message}`));
|
|
1435
|
+
return { skillName, installed: false, verdict: prepared.verdict, score: scanResult.score };
|
|
1436
|
+
}
|
|
1437
|
+
bundleFiles = Object.keys(prepared.bundle.files).length > 0 ? prepared.bundle.files : undefined;
|
|
1438
|
+
bundleExecutables = prepared.bundle.executables;
|
|
1439
|
+
installedFiles = prepared.bundle.fileList;
|
|
1440
|
+
}
|
|
1441
|
+
// Fetch agent files (agents/*.md) if discovered and the path is unknown
|
|
1364
1442
|
let agentFiles;
|
|
1365
|
-
if (agentRawUrls && Object.keys(agentRawUrls).length > 0) {
|
|
1443
|
+
if (!sourceSkillPath && agentRawUrls && Object.keys(agentRawUrls).length > 0) {
|
|
1366
1444
|
agentFiles = {};
|
|
1367
1445
|
const fetches = Object.entries(agentRawUrls).map(async ([relPath, url]) => {
|
|
1368
1446
|
try {
|
|
@@ -1389,7 +1467,9 @@ sourceSkillPath) {
|
|
|
1389
1467
|
scope: opts.global ? "user" : "project",
|
|
1390
1468
|
projectRoot,
|
|
1391
1469
|
copy: !!opts.copy,
|
|
1392
|
-
bundleFiles: agentFiles,
|
|
1470
|
+
bundleFiles: bundleFiles ?? agentFiles,
|
|
1471
|
+
bundleExecutables,
|
|
1472
|
+
previousFiles,
|
|
1393
1473
|
});
|
|
1394
1474
|
}
|
|
1395
1475
|
catch (err) {
|
|
@@ -1397,7 +1477,7 @@ sourceSkillPath) {
|
|
|
1397
1477
|
return { skillName, installed: false, verdict: "WRITE_ERROR", score: scanResult.score };
|
|
1398
1478
|
}
|
|
1399
1479
|
const skillVersion = extractFrontmatterVersion(content) || "1.0.0";
|
|
1400
|
-
return { skillName, installed: true, verdict: scanResult.verdict, score: scanResult.score, sha, version: skillVersion, sourceSkillPath };
|
|
1480
|
+
return { skillName, installed: true, verdict: scanResult.verdict, score: scanResult.score, sha, version: skillVersion, sourceSkillPath, files: installedFiles };
|
|
1401
1481
|
}
|
|
1402
1482
|
// ---------------------------------------------------------------------------
|
|
1403
1483
|
// Bulk repo plugin installation: --repo with --all
|
|
@@ -1649,9 +1729,29 @@ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
|
|
|
1649
1729
|
const sha = computeSha(combined);
|
|
1650
1730
|
const skillFileNames = skills.map((s) => s.name === pluginName ? "SKILL.md" : `${s.name}/SKILL.md`).sort();
|
|
1651
1731
|
const locations = [];
|
|
1732
|
+
if (opts.dryRun) {
|
|
1733
|
+
printDryRunPlan([pluginName], selectedAgents, opts);
|
|
1734
|
+
const enableResult = enableAfterInstall(pluginName, {}, opts);
|
|
1735
|
+
const report = buildPerAgentReport({
|
|
1736
|
+
skill: pluginName,
|
|
1737
|
+
scope: enableResult.scope,
|
|
1738
|
+
action: "not-applicable",
|
|
1739
|
+
agents: selectedAgents,
|
|
1740
|
+
});
|
|
1741
|
+
for (const line of report)
|
|
1742
|
+
console.log(` ${dim(">")} ${line.line}`);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
const doneBases = new Set();
|
|
1652
1746
|
for (const agent of selectedAgents) {
|
|
1653
1747
|
const baseDir = resolveInstallBase(opts, agent);
|
|
1654
1748
|
const plugDir = basename(baseDir) === pluginName ? baseDir : join(baseDir, pluginName);
|
|
1749
|
+
// 0894: shared readers share one copy in .agents/skills.
|
|
1750
|
+
if (doneBases.has(baseDir)) {
|
|
1751
|
+
locations.push(`${agent.displayName}: ${plugDir}`);
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
doneBases.add(baseDir);
|
|
1655
1755
|
try {
|
|
1656
1756
|
// Skills: {agent-dir}/{plugin-name}/{skill-name}/SKILL.md
|
|
1657
1757
|
for (const skill of skills) {
|
|
@@ -1702,9 +1802,9 @@ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
|
|
|
1702
1802
|
// hence no rollback wiring here) — the gate only surfaces the same
|
|
1703
1803
|
// acknowledgement + per-agent report as the marketplace path. The identical
|
|
1704
1804
|
// opt-in condition keeps the default flow unchanged (NFR-005).
|
|
1805
|
+
// (--dry-run, also an opt-in, returned earlier with its own preview.)
|
|
1705
1806
|
const userOptedIn = opts.scope !== undefined ||
|
|
1706
1807
|
opts.global === true ||
|
|
1707
|
-
opts.dryRun === true ||
|
|
1708
1808
|
opts.enable === false;
|
|
1709
1809
|
if (userOptedIn) {
|
|
1710
1810
|
const enableResult = enableAfterInstall(pluginName, lock.skills[pluginName], opts);
|
|
@@ -1989,12 +2089,20 @@ async function addCommandInner(source, opts) {
|
|
|
1989
2089
|
return process.exit(0);
|
|
1990
2090
|
}
|
|
1991
2091
|
}
|
|
2092
|
+
if (opts.dryRun) {
|
|
2093
|
+
printDryRunPlan(selectedSkills.map((s) => s.name), selectedAgents, opts);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
// 0894: resolve the commit ONCE; SKILL.md, bundle and lockfile all use it.
|
|
2097
|
+
const sourceBranch = await getDefaultBranch(owner, repo);
|
|
2098
|
+
const sourceCommitSha = await getBranchHeadSha(owner, repo, sourceBranch);
|
|
2099
|
+
const priorSkills = readLockfile(lockfileRoot(opts))?.skills ?? {};
|
|
1992
2100
|
// Install selected skills
|
|
1993
2101
|
const results = [];
|
|
1994
2102
|
for (const skill of selectedSkills) {
|
|
1995
2103
|
console.log(dim(`\nInstalling skill: ${bold(skill.name)}...`));
|
|
1996
2104
|
try {
|
|
1997
|
-
const result = await installOneGitHubSkill(owner, repo, skill.name, skill.rawUrl, opts, selectedAgents, projectRoot, skill.agentRawUrls, skill.path);
|
|
2105
|
+
const result = await installOneGitHubSkill(owner, repo, skill.name, skill.rawUrl, opts, selectedAgents, projectRoot, skill.agentRawUrls, skill.path, sourceCommitSha || sourceBranch, priorSkills[skill.name]?.files);
|
|
1998
2106
|
results.push(result);
|
|
1999
2107
|
}
|
|
2000
2108
|
catch (err) {
|
|
@@ -2007,8 +2115,6 @@ async function addCommandInner(source, opts) {
|
|
|
2007
2115
|
// `sourceSkillPath` are persisted alongside the legacy `source` string.
|
|
2008
2116
|
const lockDir = lockfileRoot(opts);
|
|
2009
2117
|
const lock = ensureLockfile(lockDir);
|
|
2010
|
-
const sourceBranch = await getDefaultBranch(owner, repo);
|
|
2011
|
-
const sourceCommitSha = await getBranchHeadSha(owner, repo, sourceBranch);
|
|
2012
2118
|
for (const r of results) {
|
|
2013
2119
|
if (r.installed && r.sha) {
|
|
2014
2120
|
lock.skills[r.skillName] = buildGitHubInstallLockEntry({
|
|
@@ -2021,6 +2127,7 @@ async function addCommandInner(source, opts) {
|
|
|
2021
2127
|
commitSha: sourceCommitSha,
|
|
2022
2128
|
pluginName: pluginNameFromSkillPath(r.sourceSkillPath),
|
|
2023
2129
|
global: !!opts.global,
|
|
2130
|
+
files: r.files,
|
|
2024
2131
|
});
|
|
2025
2132
|
}
|
|
2026
2133
|
}
|
|
@@ -2384,10 +2491,21 @@ async function installFromRegistry(skillName, opts) {
|
|
|
2384
2491
|
// Install to each agent
|
|
2385
2492
|
const sha = computeSha(content);
|
|
2386
2493
|
const locations = [];
|
|
2494
|
+
if (opts.dryRun) {
|
|
2495
|
+
printDryRunPlan([skillName], selectedAgents, opts);
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2387
2498
|
const processedContent = ensureFrontmatter(content, skillName);
|
|
2499
|
+
const doneBases = new Set();
|
|
2388
2500
|
for (const agent of selectedAgents) {
|
|
2389
2501
|
const baseDir = resolveInstallBase(opts, agent);
|
|
2390
2502
|
const skillDir = join(baseDir, skillName);
|
|
2503
|
+
// 0894: shared readers share one copy in .agents/skills.
|
|
2504
|
+
if (doneBases.has(baseDir)) {
|
|
2505
|
+
locations.push(`${agent.displayName}: ${skillDir}`);
|
|
2506
|
+
continue;
|
|
2507
|
+
}
|
|
2508
|
+
doneBases.add(baseDir);
|
|
2391
2509
|
try {
|
|
2392
2510
|
mkdirSync(skillDir, { recursive: true });
|
|
2393
2511
|
writeFileSync(join(skillDir, "SKILL.md"), processedContent, "utf-8");
|
|
@@ -2465,6 +2583,8 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2465
2583
|
}
|
|
2466
2584
|
const branch = refOverride || (await getDefaultBranch(owner, repo));
|
|
2467
2585
|
const sourceCommitSha = await getBranchHeadSha(owner, repo, branch);
|
|
2586
|
+
// 0894: SKILL.md, the bundle and the lockfile all read this one commit.
|
|
2587
|
+
const contentRef = sourceCommitSha || branch;
|
|
2468
2588
|
// Candidate subpaths: explicit override wins; otherwise try the conventional
|
|
2469
2589
|
// skills/{name}/ layout first, then root-level {name}/ (e.g. higgsfield-ai/skills).
|
|
2470
2590
|
const subpathCandidates = skillSubpathOverride
|
|
@@ -2477,7 +2597,7 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2477
2597
|
let content;
|
|
2478
2598
|
for (let i = 0; i < subpathCandidates.length; i++) {
|
|
2479
2599
|
const candidate = subpathCandidates[i];
|
|
2480
|
-
const candidateUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${
|
|
2600
|
+
const candidateUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${contentRef}/${candidate}`;
|
|
2481
2601
|
const isLast = i === subpathCandidates.length - 1;
|
|
2482
2602
|
if (isLast) {
|
|
2483
2603
|
content = await fetchSkillContent(candidateUrl);
|
|
@@ -2493,53 +2613,6 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2493
2613
|
}
|
|
2494
2614
|
if (content === undefined)
|
|
2495
2615
|
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
2616
|
// Platform security check (best-effort, non-blocking on network error).
|
|
2544
2617
|
// Skipped for private repos: the registry looks skills up by name, so it
|
|
2545
2618
|
// would leak the name and could match an unrelated public skill.
|
|
@@ -2624,6 +2697,26 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2624
2697
|
opts.global = true;
|
|
2625
2698
|
if (!selections.symlink)
|
|
2626
2699
|
opts.copy = true;
|
|
2700
|
+
if (opts.dryRun) {
|
|
2701
|
+
printDryRunPlan([skillName], selectedAgents, opts);
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
// 0894: the complete skill folder — every nested file, binaries as bytes,
|
|
2705
|
+
// exec bits kept — read from the same commit, scanned, never truncated; not
|
|
2706
|
+
// downloaded for --dry-run.
|
|
2707
|
+
const prepared = await prepareGitHubSkillBundle({
|
|
2708
|
+
owner,
|
|
2709
|
+
repo,
|
|
2710
|
+
ref: contentRef,
|
|
2711
|
+
skillMdPath: skillSubpath,
|
|
2712
|
+
force: !!opts.force,
|
|
2713
|
+
});
|
|
2714
|
+
if (!prepared.ok) {
|
|
2715
|
+
console.error(red(`\n${prepared.message}`));
|
|
2716
|
+
return process.exit(1);
|
|
2717
|
+
}
|
|
2718
|
+
const bundle = prepared.bundle;
|
|
2719
|
+
const previousFiles = readLockfile(lockfileRoot(opts))?.skills[skillName]?.files;
|
|
2627
2720
|
// Install to each agent using canonical installer (flat directory, namespaced frontmatter name)
|
|
2628
2721
|
const namespacedName = pluginNamespace && skillName !== pluginNamespace
|
|
2629
2722
|
? `${pluginNamespace}/${skillName}`
|
|
@@ -2648,7 +2741,9 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2648
2741
|
scope: opts.global ? "user" : "project",
|
|
2649
2742
|
projectRoot,
|
|
2650
2743
|
copy: !!opts.copy,
|
|
2651
|
-
bundleFiles:
|
|
2744
|
+
bundleFiles: Object.keys(bundle.files).length > 0 ? bundle.files : undefined,
|
|
2745
|
+
bundleExecutables: bundle.executables,
|
|
2746
|
+
previousFiles,
|
|
2652
2747
|
});
|
|
2653
2748
|
// Update lockfile (global → ~/.agents/, project → project root).
|
|
2654
2749
|
// 0743: persist `sourceSkillPath` from the locally-derived `skillSubpath`
|
|
@@ -2667,6 +2762,7 @@ async function installSingleSkillLegacy(owner, repo, skill, opts, skillSubpathOv
|
|
|
2667
2762
|
commitSha: sourceCommitSha,
|
|
2668
2763
|
pluginName: pluginNamespace ?? pluginNameFromSkillPath(skillSubpath),
|
|
2669
2764
|
global: !!opts.global,
|
|
2765
|
+
files: bundle.fileList,
|
|
2670
2766
|
});
|
|
2671
2767
|
lock.agents = [...new Set([...(lock.agents || []), ...selectedAgents.map((a) => a.id)])];
|
|
2672
2768
|
writeLockfile(lock, lockDir);
|