vskill 0.2.27 → 0.2.29

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.
Files changed (79) hide show
  1. package/dist/commands/__tests__/eval-router.test.d.ts +1 -0
  2. package/dist/commands/__tests__/eval-router.test.js +60 -0
  3. package/dist/commands/__tests__/eval-router.test.js.map +1 -0
  4. package/dist/commands/add.js +166 -24
  5. package/dist/commands/add.js.map +1 -1
  6. package/dist/commands/add.test.js +96 -1
  7. package/dist/commands/add.test.js.map +1 -1
  8. package/dist/commands/eval/__tests__/coverage.test.d.ts +1 -0
  9. package/dist/commands/eval/__tests__/coverage.test.js +122 -0
  10. package/dist/commands/eval/__tests__/coverage.test.js.map +1 -0
  11. package/dist/commands/eval/__tests__/generate-all.test.d.ts +1 -0
  12. package/dist/commands/eval/__tests__/generate-all.test.js +133 -0
  13. package/dist/commands/eval/__tests__/generate-all.test.js.map +1 -0
  14. package/dist/commands/eval/__tests__/init.test.d.ts +1 -0
  15. package/dist/commands/eval/__tests__/init.test.js +116 -0
  16. package/dist/commands/eval/__tests__/init.test.js.map +1 -0
  17. package/dist/commands/eval/__tests__/run.test.d.ts +1 -0
  18. package/dist/commands/eval/__tests__/run.test.js +149 -0
  19. package/dist/commands/eval/__tests__/run.test.js.map +1 -0
  20. package/dist/commands/eval/coverage.d.ts +1 -0
  21. package/dist/commands/eval/coverage.js +79 -0
  22. package/dist/commands/eval/coverage.js.map +1 -0
  23. package/dist/commands/eval/generate-all.d.ts +1 -0
  24. package/dist/commands/eval/generate-all.js +64 -0
  25. package/dist/commands/eval/generate-all.js.map +1 -0
  26. package/dist/commands/eval/init.d.ts +1 -0
  27. package/dist/commands/eval/init.js +38 -0
  28. package/dist/commands/eval/init.js.map +1 -0
  29. package/dist/commands/eval/run.d.ts +1 -0
  30. package/dist/commands/eval/run.js +107 -0
  31. package/dist/commands/eval/run.js.map +1 -0
  32. package/dist/commands/eval.d.ts +4 -0
  33. package/dist/commands/eval.js +48 -0
  34. package/dist/commands/eval.js.map +1 -0
  35. package/dist/eval/__tests__/benchmark.test.d.ts +1 -0
  36. package/dist/eval/__tests__/benchmark.test.js +65 -0
  37. package/dist/eval/__tests__/benchmark.test.js.map +1 -0
  38. package/dist/eval/__tests__/judge.test.d.ts +1 -0
  39. package/dist/eval/__tests__/judge.test.js +45 -0
  40. package/dist/eval/__tests__/judge.test.js.map +1 -0
  41. package/dist/eval/__tests__/llm.test.d.ts +1 -0
  42. package/dist/eval/__tests__/llm.test.js +85 -0
  43. package/dist/eval/__tests__/llm.test.js.map +1 -0
  44. package/dist/eval/__tests__/prompt-builder.test.d.ts +1 -0
  45. package/dist/eval/__tests__/prompt-builder.test.js +72 -0
  46. package/dist/eval/__tests__/prompt-builder.test.js.map +1 -0
  47. package/dist/eval/__tests__/schema.test.d.ts +1 -0
  48. package/dist/eval/__tests__/schema.test.js +209 -0
  49. package/dist/eval/__tests__/schema.test.js.map +1 -0
  50. package/dist/eval/__tests__/skill-scanner.test.d.ts +1 -0
  51. package/dist/eval/__tests__/skill-scanner.test.js +78 -0
  52. package/dist/eval/__tests__/skill-scanner.test.js.map +1 -0
  53. package/dist/eval/benchmark.d.ts +22 -0
  54. package/dist/eval/benchmark.js +24 -0
  55. package/dist/eval/benchmark.js.map +1 -0
  56. package/dist/eval/judge.d.ts +9 -0
  57. package/dist/eval/judge.js +40 -0
  58. package/dist/eval/judge.js.map +1 -0
  59. package/dist/eval/llm.d.ts +5 -0
  60. package/dist/eval/llm.js +34 -0
  61. package/dist/eval/llm.js.map +1 -0
  62. package/dist/eval/prompt-builder.d.ts +3 -0
  63. package/dist/eval/prompt-builder.js +155 -0
  64. package/dist/eval/prompt-builder.js.map +1 -0
  65. package/dist/eval/schema.d.ts +26 -0
  66. package/dist/eval/schema.js +128 -0
  67. package/dist/eval/schema.js.map +1 -0
  68. package/dist/eval/skill-scanner.d.ts +8 -0
  69. package/dist/eval/skill-scanner.js +44 -0
  70. package/dist/eval/skill-scanner.js.map +1 -0
  71. package/dist/index.js +9 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/marketplace/index.d.ts +2 -2
  74. package/dist/marketplace/index.js +1 -1
  75. package/dist/marketplace/index.js.map +1 -1
  76. package/dist/marketplace/marketplace.d.ts +13 -0
  77. package/dist/marketplace/marketplace.js +35 -0
  78. package/dist/marketplace/marketplace.js.map +1 -1
  79. package/package.json +2 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ // ---------------------------------------------------------------------------
3
+ // Mocks
4
+ // ---------------------------------------------------------------------------
5
+ const mocks = vi.hoisted(() => ({
6
+ runEvalInit: vi.fn(),
7
+ runEvalRun: vi.fn(),
8
+ runEvalCoverage: vi.fn(),
9
+ runEvalGenerateAll: vi.fn(),
10
+ }));
11
+ vi.mock("../eval/init.js", () => ({ runEvalInit: mocks.runEvalInit }));
12
+ vi.mock("../eval/run.js", () => ({ runEvalRun: mocks.runEvalRun }));
13
+ vi.mock("../eval/coverage.js", () => ({
14
+ runEvalCoverage: mocks.runEvalCoverage,
15
+ }));
16
+ vi.mock("../eval/generate-all.js", () => ({
17
+ runEvalGenerateAll: mocks.runEvalGenerateAll,
18
+ }));
19
+ // ---------------------------------------------------------------------------
20
+ // Import module under test AFTER mocks
21
+ // ---------------------------------------------------------------------------
22
+ const { evalCommand } = await import("../eval.js");
23
+ // ---------------------------------------------------------------------------
24
+ // Tests
25
+ // ---------------------------------------------------------------------------
26
+ describe("evalCommand router", () => {
27
+ beforeEach(() => {
28
+ vi.resetAllMocks();
29
+ });
30
+ it("routes init subcommand to runEvalInit", async () => {
31
+ await evalCommand("init", "marketing/social-media-posting", {
32
+ root: "/tmp/test",
33
+ });
34
+ expect(mocks.runEvalInit).toHaveBeenCalledWith(expect.stringContaining("marketing/skills/social-media-posting"), false);
35
+ });
36
+ it("routes run subcommand to runEvalRun", async () => {
37
+ await evalCommand("run", "marketing/social-media-posting", {
38
+ root: "/tmp/test",
39
+ });
40
+ expect(mocks.runEvalRun).toHaveBeenCalledWith(expect.stringContaining("marketing/skills/social-media-posting"));
41
+ });
42
+ it("routes coverage subcommand to runEvalCoverage", async () => {
43
+ await evalCommand("coverage", undefined, { root: "/tmp/test" });
44
+ expect(mocks.runEvalCoverage).toHaveBeenCalledWith("/tmp/test");
45
+ });
46
+ it("routes generate-all subcommand to runEvalGenerateAll", async () => {
47
+ await evalCommand("generate-all", undefined, {
48
+ root: "/tmp/test",
49
+ force: true,
50
+ });
51
+ expect(mocks.runEvalGenerateAll).toHaveBeenCalledWith("/tmp/test", true);
52
+ });
53
+ it("prints error for unknown subcommand", async () => {
54
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
55
+ await evalCommand("unknown", undefined, { root: "/tmp/test" });
56
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown subcommand"));
57
+ consoleSpy.mockRestore();
58
+ });
59
+ });
60
+ //# sourceMappingURL=eval-router.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-router.test.js","sourceRoot":"","sources":["../../../src/commands/__tests__/eval-router.test.ts"],"names":[],"mappings":"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"}
@@ -7,12 +7,12 @@ import { createHash } from "node:crypto";
7
7
  import { execSync } from "node:child_process";
8
8
  import os from "node:os";
9
9
  import { resolveTilde } from "../utils/paths.js";
10
- import { reportInstall, reportInstallBatch } from "../api/client.js";
10
+ import { reportInstall, reportInstallBatch, submitSkill } from "../api/client.js";
11
11
  import { filterAgents } from "../utils/agent-filter.js";
12
12
  import { detectInstalledAgents, AGENTS_REGISTRY } from "../agents/agents-registry.js";
13
- import { ensureLockfile, writeLockfile, readLockfile } from "../lockfile/index.js";
13
+ import { ensureLockfile, writeLockfile, readLockfile, removeSkillFromLock } from "../lockfile/index.js";
14
14
  import { runTier1Scan } from "../scanner/index.js";
15
- import { getAvailablePlugins, getPluginSource, getPluginVersion, hasPlugin } from "../marketplace/index.js";
15
+ import { getAvailablePlugins, getPluginSource, getPluginVersion, hasPlugin, discoverUnregisteredPlugins } from "../marketplace/index.js";
16
16
  import { checkInstallSafety } from "../blocklist/blocklist.js";
17
17
  import { getSkill } from "../api/client.js";
18
18
  import { checkPlatformSecurity } from "../security/index.js";
@@ -22,7 +22,7 @@ import { parseSkillsShUrl, isCompleteParsed, isIncompleteParsed, } from "../reso
22
22
  import { bold, green, red, yellow, dim, cyan, spinner, } from "../utils/output.js";
23
23
  import { isTTY, createPrompter } from "../utils/prompts.js";
24
24
  import { installSymlink, installCopy } from "../installer/canonical.js";
25
- import { isClaudeCliAvailable, registerMarketplace, deregisterMarketplace, installNativePlugin, } from "../utils/claude-cli.js";
25
+ import { isClaudeCliAvailable, registerMarketplace, deregisterMarketplace, installNativePlugin, uninstallNativePlugin, } from "../utils/claude-cli.js";
26
26
  import { getMarketplaceName } from "../marketplace/index.js";
27
27
  async function parseManifestFromContentsApi(data) {
28
28
  // Prefer download_url for raw content
@@ -104,6 +104,24 @@ export async function detectMarketplaceRepo(owner, repo) {
104
104
  }
105
105
  return { isMarketplace: false };
106
106
  }
107
+ /**
108
+ * Offer to submit a marketplace repo for re-scanning on the platform.
109
+ * Uses the existing submitSkill() API. Non-throwing — prints fallback URL on failure.
110
+ */
111
+ async function triggerResubmission(owner, repo) {
112
+ const repoUrl = `https://github.com/${owner}/${repo}`;
113
+ try {
114
+ const result = await submitSkill({ repoUrl });
115
+ console.log(green(" Submitted for re-scanning!"));
116
+ if (result.trackingUrl) {
117
+ console.log(dim(` Track progress: ${result.trackingUrl}`));
118
+ }
119
+ }
120
+ catch {
121
+ console.log(yellow(" Could not submit automatically."));
122
+ console.log(dim(` Submit manually: https://verified-skill.com/submit?repo=${owner}/${repo}`));
123
+ }
124
+ }
107
125
  /**
108
126
  * Install plugins from a Claude Code plugin marketplace repo.
109
127
  *
@@ -118,8 +136,20 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
118
136
  console.error(red("No plugins found in marketplace.json"));
119
137
  process.exit(1);
120
138
  }
121
- console.log(`\n${bold("Claude Code Plugin Marketplace")} detected: ${cyan(`${owner}/${repo}`)}\n` +
122
- dim(`Marketplace: ${marketplaceName || "unknown"} ${plugins.length} plugin${plugins.length === 1 ? "" : "s"} available\n`));
139
+ // Discover plugin directories not yet in marketplace.json
140
+ const unregistered = await discoverUnregisteredPlugins(owner, repo, manifestContent);
141
+ const headerParts = [
142
+ `\n${bold("Claude Code Plugin Marketplace")} detected: ${cyan(`${owner}/${repo}`)}\n`,
143
+ dim(`Marketplace: ${marketplaceName || "unknown"} — ${plugins.length} registered plugin${plugins.length === 1 ? "" : "s"}`),
144
+ ];
145
+ if (unregistered.length > 0) {
146
+ headerParts.push(dim(`, ${unregistered.length} unregistered`));
147
+ }
148
+ headerParts.push("\n");
149
+ if (unregistered.length > 0) {
150
+ headerParts.push(yellow(` ${unregistered.length} new plugin${unregistered.length === 1 ? "" : "s"} not yet in marketplace.json\n`));
151
+ }
152
+ console.log(headerParts.join(""));
123
153
  // Check lockfile for already-installed plugins
124
154
  const lockDir = lockfileRoot(opts);
125
155
  const lock = readLockfile(lockDir);
@@ -133,6 +163,8 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
133
163
  }
134
164
  // Select plugins
135
165
  let selectedPlugins;
166
+ let selectedUnregistered = [];
167
+ let usedInteractiveCheckbox = false;
136
168
  if (!isTTY() && !opts.yes && !opts.all) {
137
169
  // Non-TTY: list plugins and exit with guidance
138
170
  console.log("Available plugins:\n");
@@ -140,6 +172,13 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
140
172
  const installed = installedSet.has(p.name) ? dim(" (installed)") : "";
141
173
  console.log(` ${bold(p.name)}${installed}${p.description ? dim(` — ${p.description}`) : ""}`);
142
174
  }
175
+ if (unregistered.length > 0) {
176
+ console.log("\nUnregistered plugins (not in marketplace.json):\n");
177
+ for (const u of unregistered) {
178
+ console.log(` ${yellow(u.name)} ${dim("(new — not in marketplace.json)")}`);
179
+ }
180
+ console.log(dim("\nUse --force --plugin <name> to install unregistered plugins."));
181
+ }
143
182
  console.error(red("\nNon-interactive mode. Use --plugin <name> or --yes to select all."));
144
183
  process.exit(1);
145
184
  }
@@ -158,9 +197,14 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
158
197
  selectedPlugins = plugins;
159
198
  console.log(dim(`Auto-selecting all ${plugins.length} plugins (--yes/--all)`));
160
199
  }
200
+ if (unregistered.length > 0) {
201
+ console.log(dim(` Skipping ${unregistered.length} unregistered plugin${unregistered.length === 1 ? "" : "s"}: `) +
202
+ dim(unregistered.map((u) => u.name).join(", ")) +
203
+ dim(" (use --force to include)"));
204
+ }
161
205
  }
162
- else if (plugins.length === 1) {
163
- // Single plugin — show details and ask for confirmation
206
+ else if (plugins.length === 1 && unregistered.length === 0) {
207
+ // Single plugin, no unregistered — show details and ask for confirmation
164
208
  const p = plugins[0];
165
209
  const isInstalled = installedSet.has(p.name);
166
210
  const versionTag = p.version ? ` v${p.version}` : "";
@@ -185,20 +229,92 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
185
229
  selectedPlugins = plugins;
186
230
  }
187
231
  else {
232
+ // Build combined picker: registered plugins first, then unregistered
233
+ const combinedItems = [
234
+ ...plugins.map((p) => ({
235
+ label: p.name + (installedSet.has(p.name) ? dim(" (installed)") : ""),
236
+ description: p.description,
237
+ checked: preSelected ? preSelected.includes(p.name) : installedSet.has(p.name),
238
+ })),
239
+ ...unregistered.map((u) => ({
240
+ label: u.name + yellow(" (new — not in marketplace.json)"),
241
+ description: undefined,
242
+ checked: false,
243
+ })),
244
+ ];
188
245
  const prompter = createPrompter();
189
- const indices = await prompter.promptCheckboxList(plugins.map((p) => ({
190
- label: p.name + (installedSet.has(p.name) ? dim(" (installed)") : ""),
191
- description: p.description,
192
- checked: preSelected ? preSelected.includes(p.name) : installedSet.has(p.name),
193
- })), { title: "Select plugins to install" });
246
+ const indices = await prompter.promptCheckboxList(combinedItems, { title: "Select plugins to install" });
194
247
  if (indices.length === 0) {
195
248
  console.log(dim("No plugins selected. Aborting."));
196
249
  return;
197
250
  }
198
- selectedPlugins = indices.map((i) => plugins[i]);
251
+ // Partition selections into registered and unregistered
252
+ selectedPlugins = [];
253
+ selectedUnregistered = [];
254
+ for (const i of indices) {
255
+ if (i < plugins.length) {
256
+ selectedPlugins.push(plugins[i]);
257
+ }
258
+ else {
259
+ selectedUnregistered.push(unregistered[i - plugins.length]);
260
+ }
261
+ }
262
+ usedInteractiveCheckbox = true;
199
263
  }
200
264
  // Attempt native Claude Code install
201
265
  const hasClaude = !opts.copy && isClaudeCliAvailable();
266
+ // Uninstall plugins that were previously installed but now unchecked
267
+ const selectedNames = new Set(selectedPlugins.map((p) => p.name));
268
+ const toUninstall = usedInteractiveCheckbox
269
+ ? [...installedSet].filter((name) => !selectedNames.has(name))
270
+ : [];
271
+ if (toUninstall.length > 0) {
272
+ console.log(dim(`\nUninstalling ${toUninstall.length} deselected plugin${toUninstall.length === 1 ? "" : "s"}...\n`));
273
+ const agents = await detectInstalledAgents();
274
+ for (const skillName of toUninstall) {
275
+ // Remove skill directories from all agents
276
+ let removedCount = 0;
277
+ for (const agent of agents) {
278
+ const localDir = join(process.cwd(), agent.localSkillsDir, skillName);
279
+ const globalDir = resolveTilde(join(agent.globalSkillsDir, skillName));
280
+ for (const dir of [localDir, globalDir]) {
281
+ if (existsSync(dir)) {
282
+ try {
283
+ rmSync(dir, { recursive: true, force: true });
284
+ removedCount++;
285
+ }
286
+ catch { /* ignore removal errors */ }
287
+ }
288
+ }
289
+ }
290
+ // Uninstall from Claude Code native plugin system
291
+ if (hasClaude && marketplaceName) {
292
+ uninstallNativePlugin(skillName, marketplaceName);
293
+ }
294
+ // Remove from lockfile
295
+ removeSkillFromLock(skillName, lockDir);
296
+ console.log(red(` ✗ ${bold(skillName)} uninstalled`) + (removedCount > 0 ? dim(` (${removedCount} location${removedCount === 1 ? "" : "s"})`) : ""));
297
+ }
298
+ }
299
+ // Gate: unregistered plugins require --force
300
+ if (selectedUnregistered.length > 0 && !opts.force) {
301
+ console.log(yellow(`\n ${selectedUnregistered.length} unregistered plugin${selectedUnregistered.length === 1 ? "" : "s"} selected but --force not set.`) + "\n" +
302
+ dim(" Unregistered plugins have not been scanned or verified.") + "\n" +
303
+ dim(" Use --force to install them anyway, or submit the repo for re-scanning.") + "\n");
304
+ // Offer resubmission in interactive mode
305
+ if (isTTY()) {
306
+ const prompter = createPrompter();
307
+ const resubmit = await prompter.promptConfirm(`Submit ${bold(`${owner}/${repo}`)} for re-scanning?`, true);
308
+ if (resubmit) {
309
+ await triggerResubmission(owner, repo);
310
+ }
311
+ }
312
+ else {
313
+ console.log(dim(` Submit manually: https://verified-skill.com/submit?repo=${owner}/${repo}`));
314
+ }
315
+ // Clear unregistered selection — only install registered plugins
316
+ selectedUnregistered = [];
317
+ }
202
318
  let marketplaceRegistered = false;
203
319
  if (hasClaude) {
204
320
  // Register marketplace via git URL — Claude Code clones to its own
@@ -220,7 +336,7 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
220
336
  }
221
337
  }
222
338
  }
223
- // Install each plugin
339
+ // Install registered plugins
224
340
  const results = [];
225
341
  for (const plugin of selectedPlugins) {
226
342
  if (hasClaude && marketplaceRegistered && marketplaceName) {
@@ -256,15 +372,28 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
256
372
  }
257
373
  }
258
374
  }
375
+ // Install unregistered plugins via extraction (--force only)
376
+ for (const unreg of selectedUnregistered) {
377
+ console.log(yellow(` Installing unregistered plugin: ${bold(unreg.name)} (--force)`));
378
+ try {
379
+ await installRepoPlugin(`${owner}/${repo}`, unreg.name, opts, unreg.source);
380
+ results.push({ name: unreg.name, installed: true, method: "extraction-unregistered" });
381
+ }
382
+ catch (err) {
383
+ console.error(red(` ✗ ${unreg.name}: ${err.message}`));
384
+ results.push({ name: unreg.name, installed: false, method: "failed" });
385
+ }
386
+ }
259
387
  // Update lockfile
260
388
  const lockForWrite = ensureLockfile(lockDir);
261
389
  for (const r of results) {
262
390
  if (r.installed) {
263
- const pluginVersion = getPluginVersion(r.name, manifestContent) || "0.0.0";
391
+ const isUnregistered = r.method === "extraction-unregistered";
392
+ const pluginVersion = isUnregistered ? "0.0.0" : (getPluginVersion(r.name, manifestContent) || "0.0.0");
264
393
  lockForWrite.skills[r.name] = {
265
394
  version: pluginVersion,
266
395
  sha: "",
267
- tier: "VERIFIED",
396
+ tier: isUnregistered ? "UNSCANNED" : "VERIFIED",
268
397
  installedAt: new Date().toISOString(),
269
398
  source: `marketplace:${owner}/${repo}#${r.name}`,
270
399
  marketplace: marketplaceName || undefined,
@@ -285,9 +414,22 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
285
414
  // Summary
286
415
  const installed = results.filter((r) => r.installed);
287
416
  const failed = results.filter((r) => !r.installed);
288
- console.log(`\n${green(bold(`${installed.length} installed`))}` +
289
- (failed.length > 0 ? `, ${red(bold(`${failed.length} failed`))}` : "") +
290
- ` of ${results.length} plugins`);
417
+ const parts = [];
418
+ if (installed.length > 0 || results.length > 0) {
419
+ parts.push(`${green(bold(`${installed.length} installed`))}`);
420
+ }
421
+ if (failed.length > 0) {
422
+ parts.push(`${red(bold(`${failed.length} failed`))}`);
423
+ }
424
+ if (toUninstall.length > 0) {
425
+ parts.push(`${red(bold(`${toUninstall.length} uninstalled`))}`);
426
+ }
427
+ if (parts.length > 0) {
428
+ console.log(`\n${parts.join(", ")} of ${results.length + toUninstall.length} plugins`);
429
+ }
430
+ else {
431
+ console.log(dim("\nNo changes made."));
432
+ }
291
433
  if (hasClaude && marketplaceRegistered && marketplaceName) {
292
434
  console.log(dim(`\nManage: claude plugin list | claude plugin uninstall "<plugin>@${marketplaceName}"`));
293
435
  }
@@ -1028,7 +1170,7 @@ async function installAllRepoPlugins(ownerRepo, opts) {
1028
1170
  // ---------------------------------------------------------------------------
1029
1171
  // Remote plugin installation from a GitHub repository with marketplace.json
1030
1172
  // ---------------------------------------------------------------------------
1031
- async function installRepoPlugin(ownerRepo, pluginName, opts) {
1173
+ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
1032
1174
  const [owner, repo] = ownerRepo.split("/");
1033
1175
  if (!owner || !repo) {
1034
1176
  throw new Error("--repo must be in owner/repo format (e.g. anton-abyzov/vskill)");
@@ -1054,14 +1196,14 @@ async function installRepoPlugin(ownerRepo, pluginName, opts) {
1054
1196
  throw err;
1055
1197
  throw new Error(`Network error: ${err.message}`);
1056
1198
  }
1057
- // Find the plugin in the marketplace
1058
- const pluginSource = getPluginSource(pluginName, manifestContent);
1199
+ // Find the plugin in the marketplace (or use override for unregistered plugins)
1200
+ const pluginSource = overrideSource || getPluginSource(pluginName, manifestContent);
1059
1201
  if (!pluginSource) {
1060
1202
  const available = getAvailablePlugins(manifestContent).map((p) => p.name);
1061
1203
  throw new Error(`Plugin "${pluginName}" not found in marketplace.json. ` +
1062
1204
  `Available plugins: ${available.join(", ")}`);
1063
1205
  }
1064
- const pluginVersion = getPluginVersion(pluginName, manifestContent) || "0.0.0";
1206
+ const pluginVersion = overrideSource ? "0.0.0" : (getPluginVersion(pluginName, manifestContent) || "0.0.0");
1065
1207
  const pluginPath = pluginSource.replace(/^\.\//, "");
1066
1208
  // Blocklist + rejection check BEFORE fetching content
1067
1209
  const safety = await checkInstallSafety(pluginName);