plugins 1.3.0 → 1.3.1

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 (3) hide show
  1. package/README.md +22 -22
  2. package/dist/index.js +168 -30
  3. package/package.json +17 -2
package/README.md CHANGED
@@ -42,31 +42,31 @@ npx plugins targets
42
42
 
43
43
  ## Commands
44
44
 
45
- | Command | Description |
46
- |---|---|
47
- | `plugins add <source>` | Discover and install plugins from a source |
48
- | `plugins discover <source>` | Inspect plugins without installing (dry run) |
49
- | `plugins targets` | List available agent tools and their detection status |
45
+ | Command | Description |
46
+ | --------------------------- | ----------------------------------------------------- |
47
+ | `plugins add <source>` | Discover and install plugins from a source |
48
+ | `plugins discover <source>` | Inspect plugins without installing (dry run) |
49
+ | `plugins targets` | List available agent tools and their detection status |
50
50
 
51
51
  If no subcommand is given, `plugins <source>` defaults to `add`.
52
52
 
53
53
  ## Flags
54
54
 
55
- | Flag | Short | Default | Description |
56
- |---|---|---|---|
57
- | `--target` | `-t` | auto-detect | Install to a specific agent tool (`claude-code`, `cursor`) |
58
- | `--scope` | `-s` | `user` | Installation scope: `user`, `project`, or `local` |
59
- | `--yes` | `-y` | `false` | Skip the confirmation prompt |
60
- | `--help` | `-h` | | Show usage information |
55
+ | Flag | Short | Default | Description |
56
+ | ---------- | ----- | ----------- | ---------------------------------------------------------- |
57
+ | `--target` | `-t` | auto-detect | Install to a specific agent tool (`claude-code`, `cursor`) |
58
+ | `--scope` | `-s` | `user` | Installation scope: `user`, `project`, or `local` |
59
+ | `--yes` | `-y` | `false` | Skip the confirmation prompt |
60
+ | `--help` | `-h` | | Show usage information |
61
61
 
62
62
  ## Supported targets
63
63
 
64
64
  The CLI auto-detects which agent tools are installed and installs to all of them.
65
65
 
66
- | Target | Detection |
67
- |---|---|
68
- | [Claude Code](https://code.claude.com) | `claude` binary on PATH |
69
- | [Cursor](https://cursor.com) | `cursor` + `claude` binaries on PATH |
66
+ | Target | Detection |
67
+ | -------------------------------------- | ------------------------------------ |
68
+ | [Claude Code](https://code.claude.com) | `claude` binary on PATH |
69
+ | [Cursor](https://cursor.com) | `cursor` + `claude` binaries on PATH |
70
70
 
71
71
  ## How it works
72
72
 
@@ -90,12 +90,12 @@ The CLI translates the vendor-neutral `.plugin/` format into target-specific for
90
90
 
91
91
  ## Environment variables
92
92
 
93
- | Variable | Purpose |
94
- |---|---|
95
- | `DISABLE_TELEMETRY` | Disable anonymous install telemetry |
96
- | `DO_NOT_TRACK` | Disable anonymous install telemetry ([standard](https://consoledonottrack.com/)) |
97
- | `NO_COLOR` | Disable color output |
98
- | `FORCE_COLOR` | Force color output |
93
+ | Variable | Purpose |
94
+ | ------------------- | -------------------------------------------------------------------------------- |
95
+ | `DISABLE_TELEMETRY` | Disable anonymous install telemetry |
96
+ | `DO_NOT_TRACK` | Disable anonymous install telemetry ([standard](https://consoledonottrack.com/)) |
97
+ | `NO_COLOR` | Disable color output |
98
+ | `FORCE_COLOR` | Force color output |
99
99
 
100
100
  ## Development
101
101
 
@@ -109,4 +109,4 @@ Zero runtime dependencies. Built with [tsup](https://tsup.egoist.dev/) as a sing
109
109
 
110
110
  ## License
111
111
 
112
- [Apache 2.0](../../LICENSE)
112
+ [Apache 2.0](./LICENSE)
package/dist/index.js CHANGED
@@ -540,7 +540,9 @@ async function multiSelect(title, options, maxVisible = 8) {
540
540
  if (state === "active") {
541
541
  const blockCursor = isColorSupported ? `\x1B[7m \x1B[0m` : "_";
542
542
  lines.push(`${c.gray(S.bar)} ${c.dim("Search:")} ${query}${blockCursor}`);
543
- lines.push(`${c.gray(S.bar)} ${c.dim("\u2191\u2193 move, space toggle, a all, n none, enter confirm")}`);
543
+ lines.push(
544
+ `${c.gray(S.bar)} ${c.dim("\u2191\u2193 move, space toggle, a all, n none, enter confirm")}`
545
+ );
544
546
  lines.push(`${c.gray(S.bar)}`);
545
547
  const visibleStart = Math.max(
546
548
  0,
@@ -689,6 +691,15 @@ var cachePopulated = false;
689
691
  async function installPlugins(plugins, target, scope, repoPath, source) {
690
692
  switch (target.id) {
691
693
  case "claude-code": {
694
+ const officialRef = getOfficialPluginRef(source);
695
+ if (officialRef) {
696
+ const ok = await installViaClaudeCli(officialRef, scope);
697
+ if (ok) {
698
+ cachePopulated = true;
699
+ break;
700
+ }
701
+ barDebug(c.dim("Falling back to direct file-based install"));
702
+ }
692
703
  const workspace = await stageInstallWorkspace(plugins, repoPath, target.id);
693
704
  await installToClaudeCode(workspace.plugins, scope, workspace.repoPath, source);
694
705
  break;
@@ -727,6 +738,47 @@ async function stageInstallWorkspace(plugins, repoPath, targetId, stagingBaseDir
727
738
  plugins: stagedPlugins
728
739
  };
729
740
  }
741
+ var OFFICIAL_MARKETPLACE_SOURCE = "anthropics/claude-plugins-official";
742
+ function getOfficialPluginRef(source) {
743
+ let repo = null;
744
+ const shorthand = source.match(/^([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
745
+ if (shorthand) repo = shorthand[1].toLowerCase();
746
+ if (!repo) {
747
+ const https = source.match(/^https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
748
+ if (https) repo = https[1].toLowerCase();
749
+ }
750
+ if (!repo) {
751
+ const ssh = source.match(/^git@github\.com:([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
752
+ if (ssh) repo = ssh[1].toLowerCase();
753
+ }
754
+ if (repo === "vercel/vercel-plugin") {
755
+ return "vercel@claude-plugins-official";
756
+ }
757
+ return null;
758
+ }
759
+ async function installViaClaudeCli(pluginRef, scope) {
760
+ const claude = findClaudeOrNull();
761
+ if (!claude) return false;
762
+ try {
763
+ step("Registering official Claude marketplace");
764
+ execSync2(`${claude} plugin marketplace add ${OFFICIAL_MARKETPLACE_SOURCE}`, {
765
+ stdio: "pipe",
766
+ timeout: 12e4
767
+ });
768
+ stepDone("Official marketplace registered");
769
+ step(`Installing ${c.cyan(pluginRef)} via Claude CLI`);
770
+ execSync2(`${claude} plugin install "${pluginRef}" --scope ${scope}`, {
771
+ stdio: "pipe",
772
+ timeout: 12e4
773
+ });
774
+ stepDone(`Installed ${c.cyan(pluginRef)} via Claude CLI`);
775
+ return true;
776
+ } catch (err) {
777
+ const msg = err instanceof Error ? err.message : String(err);
778
+ barDebug(c.dim(`Claude CLI install failed: ${msg}`));
779
+ return false;
780
+ }
781
+ }
730
782
  async function installToClaudeCode(plugins, scope, repoPath, source) {
731
783
  await installToPluginCache(plugins, scope, repoPath, source);
732
784
  }
@@ -773,7 +825,10 @@ async function installToPluginCache(plugins, scope, repoPath, source) {
773
825
  marketplaceSource = { source: "github", repo: githubRepo };
774
826
  } else if (isRemoteSource(source)) {
775
827
  const gitUrl = normalizeGitUrl(source);
776
- marketplaceSource = { source: "git", url: gitUrl.endsWith(".git") ? gitUrl : gitUrl + ".git" };
828
+ marketplaceSource = {
829
+ source: "git",
830
+ url: gitUrl.endsWith(".git") ? gitUrl : gitUrl + ".git"
831
+ };
777
832
  } else {
778
833
  marketplaceSource = { source: "directory", path: repoPath };
779
834
  }
@@ -787,16 +842,25 @@ async function installToPluginCache(plugins, scope, repoPath, source) {
787
842
  }
788
843
  barEmpty();
789
844
  const installedPath = join3(pluginsDir, "installed_plugins.json");
790
- let installedData = { version: 2, plugins: {} };
845
+ let installedData = {
846
+ version: 2,
847
+ plugins: {}
848
+ };
791
849
  if (existsSync2(installedPath)) {
792
850
  try {
793
- installedData = JSON.parse(await readFile2(installedPath, "utf-8"));
851
+ const parsed = JSON.parse(await readFile2(installedPath, "utf-8"));
852
+ installedData.version = parsed.version ?? 2;
853
+ installedData.plugins = parsed.plugins ?? {};
794
854
  } catch {
795
855
  }
796
856
  }
797
857
  let gitSha;
798
858
  try {
799
- gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim();
859
+ gitSha = execSync2("git rev-parse HEAD", {
860
+ cwd: repoPath,
861
+ encoding: "utf-8",
862
+ stdio: "pipe"
863
+ }).trim();
800
864
  } catch {
801
865
  }
802
866
  for (const plugin of plugins) {
@@ -834,7 +898,9 @@ async function installToPluginCache(plugins, scope, repoPath, source) {
834
898
  }
835
899
  }
836
900
  if (settingsCorrupted) {
837
- warn("Could not parse ~/.claude/settings.json \u2014 skipping enabledPlugins update to avoid overwriting existing settings.");
901
+ warn(
902
+ "Could not parse ~/.claude/settings.json \u2014 skipping enabledPlugins update to avoid overwriting existing settings."
903
+ );
838
904
  barLine(c.dim("You may need to manually enable the plugins in Claude Code settings."));
839
905
  } else {
840
906
  const enabled = settings.enabledPlugins ?? {};
@@ -867,7 +933,11 @@ async function installToCursorExtensions(plugins, scope, repoPath, source) {
867
933
  }
868
934
  let gitSha;
869
935
  try {
870
- gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim();
936
+ gitSha = execSync2("git rev-parse HEAD", {
937
+ cwd: repoPath,
938
+ encoding: "utf-8",
939
+ stdio: "pipe"
940
+ }).trim();
871
941
  } catch {
872
942
  }
873
943
  for (const plugin of plugins) {
@@ -881,9 +951,7 @@ async function installToCursorExtensions(plugins, scope, repoPath, source) {
881
951
  await cp(plugin.path, destDir, { recursive: true });
882
952
  barDebug(c.dim(`Copied to ${destDir}`));
883
953
  const identifier = `${marketplaceName}.${plugin.name}`;
884
- extensions = extensions.filter(
885
- (e) => e?.identifier?.id !== identifier
886
- );
954
+ extensions = extensions.filter((e) => e?.identifier?.id !== identifier);
887
955
  const uriPath = "/" + destDir.replace(/\\/g, "/");
888
956
  extensions.push({
889
957
  identifier: { id: identifier },
@@ -917,7 +985,11 @@ async function installToCodex(plugins, scope, repoPath, source) {
917
985
  }
918
986
  let gitSha;
919
987
  try {
920
- gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim();
988
+ gitSha = execSync2("git rev-parse HEAD", {
989
+ cwd: repoPath,
990
+ encoding: "utf-8",
991
+ stdio: "pipe"
992
+ }).trim();
921
993
  } catch {
922
994
  }
923
995
  const versionKey = gitSha ?? "local";
@@ -1031,7 +1103,12 @@ async function enrichForCodex(plugin) {
1031
1103
  };
1032
1104
  if (manifest.homepage) iface.websiteURL = manifest.homepage;
1033
1105
  else if (manifest.repository) iface.websiteURL = manifest.repository;
1034
- const assetCandidates = ["assets/app-icon.png", "assets/icon.png", "assets/logo.png", "assets/logo.svg"];
1106
+ const assetCandidates = [
1107
+ "assets/app-icon.png",
1108
+ "assets/icon.png",
1109
+ "assets/logo.png",
1110
+ "assets/logo.svg"
1111
+ ];
1035
1112
  for (const candidate of assetCandidates) {
1036
1113
  if (existsSync2(join3(plugin.path, candidate))) {
1037
1114
  iface.logo = `./${candidate}`;
@@ -1076,6 +1153,23 @@ async function prepareForClaudeCode(plugins, repoPath, marketplaceName) {
1076
1153
  await preparePluginDirForVendor(plugin, ".claude-plugin", "CLAUDE_PLUGIN_ROOT");
1077
1154
  }
1078
1155
  }
1156
+ function findClaudeOrNull() {
1157
+ try {
1158
+ const path = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
1159
+ if (path) return path;
1160
+ } catch {
1161
+ }
1162
+ const home = homedir2();
1163
+ const candidates = [
1164
+ join3(home, ".local", "bin", "claude"),
1165
+ join3(home, ".bun", "bin", "claude"),
1166
+ "/usr/local/bin/claude"
1167
+ ];
1168
+ for (const candidate of candidates) {
1169
+ if (existsSync2(candidate)) return candidate;
1170
+ }
1171
+ return null;
1172
+ }
1079
1173
  async function preparePluginDirForVendor(plugin, vendorDir, envVar) {
1080
1174
  const pluginPath = plugin.path;
1081
1175
  const openPluginDir = join3(pluginPath, ".plugin");
@@ -1131,7 +1225,9 @@ async function translateEnvVars(pluginPath, pluginName, envVar) {
1131
1225
  if (changed) {
1132
1226
  await writeFile(filePath, content);
1133
1227
  barDebug(
1134
- c.dim(`${pluginName}: translated plugin root \u2192 \${${envVar}} in ${filePath.split("/").pop()}`)
1228
+ c.dim(
1229
+ `${pluginName}: translated plugin root \u2192 \${${envVar}} in ${filePath.split("/").pop()}`
1230
+ )
1135
1231
  );
1136
1232
  }
1137
1233
  }
@@ -1183,6 +1279,29 @@ function normalizeGitUrl(source) {
1183
1279
  }
1184
1280
  return source;
1185
1281
  }
1282
+ async function isMarketplaceNew(marketplaceName) {
1283
+ const knownPath = join3(homedir2(), ".claude", "plugins", "known_marketplaces.json");
1284
+ if (!existsSync2(knownPath)) return true;
1285
+ try {
1286
+ const data = JSON.parse(await readFile2(knownPath, "utf-8"));
1287
+ return !data[marketplaceName];
1288
+ } catch {
1289
+ return true;
1290
+ }
1291
+ }
1292
+ async function setAutoUpdate(marketplaceName, enabled) {
1293
+ const knownPath = join3(homedir2(), ".claude", "plugins", "known_marketplaces.json");
1294
+ if (!existsSync2(knownPath)) return;
1295
+ let data = {};
1296
+ try {
1297
+ data = JSON.parse(await readFile2(knownPath, "utf-8"));
1298
+ } catch {
1299
+ return;
1300
+ }
1301
+ if (!data[marketplaceName]) return;
1302
+ data[marketplaceName].autoUpdate = enabled;
1303
+ await writeFile(knownPath, JSON.stringify(data, null, 2));
1304
+ }
1186
1305
 
1187
1306
  // lib/telemetry.ts
1188
1307
  var TELEMETRY_URL = "https://plugins-telemetry.labs.vercel.dev/t";
@@ -1294,14 +1413,14 @@ async function cmdDiscover(source) {
1294
1413
  if (remotePlugins.length > 0) {
1295
1414
  if (values.remote) {
1296
1415
  barEmpty();
1297
- step(`${c.bold(String(remotePlugins.length))} remote plugin(s) ${c.dim("(hosted in external repos)")}`);
1416
+ step(
1417
+ `${c.bold(String(remotePlugins.length))} remote plugin(s) ${c.dim("(hosted in external repos)")}`
1418
+ );
1298
1419
  barEmpty();
1299
1420
  printRemotePluginTable(remotePlugins);
1300
1421
  } else {
1301
1422
  barEmpty();
1302
- barLine(
1303
- c.dim(`${remotePlugins.length} remote plugin(s) not shown. Run:`)
1304
- );
1423
+ barLine(c.dim(`${remotePlugins.length} remote plugin(s) not shown. Run:`));
1305
1424
  barLine(` ${c.cyan(`npx plugins discover ${source} --remote`)}`);
1306
1425
  }
1307
1426
  printMissingPaths(missingPaths);
@@ -1343,9 +1462,7 @@ async function cmdInstall(source, opts) {
1343
1462
  barEmpty();
1344
1463
  step("No plugins found.");
1345
1464
  if (remotePlugins.length > 0) {
1346
- barLine(
1347
- c.dim(`${remotePlugins.length} remote plugin(s) not shown. Run:`)
1348
- );
1465
+ barLine(c.dim(`${remotePlugins.length} remote plugin(s) not shown. Run:`));
1349
1466
  barLine(` ${c.cyan(`npx plugins discover ${source} --remote`)}`);
1350
1467
  printMissingPaths(missingPaths);
1351
1468
  }
@@ -1387,9 +1504,7 @@ async function cmdInstall(source, opts) {
1387
1504
  printPluginTable(plugins);
1388
1505
  if (remotePlugins.length > 0) {
1389
1506
  barEmpty();
1390
- barLine(
1391
- c.dim(`+ ${remotePlugins.length} remote plugin(s) not included. Run:`)
1392
- );
1507
+ barLine(c.dim(`+ ${remotePlugins.length} remote plugin(s) not included. Run:`));
1393
1508
  barLine(` ${c.cyan(`npx plugins discover ${source} --remote`)}`);
1394
1509
  printMissingPaths(missingPaths);
1395
1510
  }
@@ -1422,9 +1537,7 @@ async function cmdInstall(source, opts) {
1422
1537
  }
1423
1538
  selectedPlugins = plugins.filter((p) => selected.includes(p.name));
1424
1539
  if (remotePlugins.length > 0) {
1425
- barLine(
1426
- c.dim(`+ ${remotePlugins.length} remote plugin(s) not included. Run:`)
1427
- );
1540
+ barLine(c.dim(`+ ${remotePlugins.length} remote plugin(s) not included. Run:`));
1428
1541
  barLine(` ${c.cyan(`npx plugins discover ${source} --remote`)}`);
1429
1542
  printMissingPaths(missingPaths);
1430
1543
  }
@@ -1434,9 +1547,29 @@ async function cmdInstall(source, opts) {
1434
1547
  barEmpty();
1435
1548
  }
1436
1549
  const scope = opts.scope ?? "user";
1550
+ const marketplaceName = selectedPlugins[0]?.marketplace ?? deriveMarketplaceName(source);
1551
+ const willInstallToClaude = installTargets.some(
1552
+ (t) => t.id === "claude-code" || t.id === "cursor" && process.platform !== "win32"
1553
+ );
1554
+ const isNew = willInstallToClaude && await isMarketplaceNew(marketplaceName);
1437
1555
  for (const target of installTargets) {
1438
1556
  await installPlugins(selectedPlugins, target, scope, repoPath, source);
1439
1557
  }
1558
+ const installedViaOfficialCli = !!getOfficialPluginRef(source);
1559
+ if (isNew && willInstallToClaude && !installedViaOfficialCli) {
1560
+ let enableAutoUpdate = true;
1561
+ if (!opts.yes && process.stdin.isTTY) {
1562
+ barEmpty();
1563
+ const response = await readLine(
1564
+ `${c.cyan(S.stepActive)} Enable auto-updates? ${c.dim("[Y/n]")} `
1565
+ );
1566
+ enableAutoUpdate = response.trim().toLowerCase() !== "n";
1567
+ }
1568
+ if (enableAutoUpdate) {
1569
+ await setAutoUpdate(marketplaceName, true);
1570
+ stepDone("Auto-updates enabled");
1571
+ }
1572
+ }
1440
1573
  track({
1441
1574
  event: "install",
1442
1575
  source,
@@ -1451,9 +1584,12 @@ async function cmdInstall(source, opts) {
1451
1584
  }
1452
1585
  function pluginComponents(p) {
1453
1586
  const parts = [];
1454
- if (p.skills.length) parts.push(`${p.skills.length} ${p.skills.length === 1 ? "skill" : "skills"}`);
1455
- if (p.commands.length) parts.push(`${p.commands.length} ${p.commands.length === 1 ? "cmd" : "cmds"}`);
1456
- if (p.agents.length) parts.push(`${p.agents.length} ${p.agents.length === 1 ? "agent" : "agents"}`);
1587
+ if (p.skills.length)
1588
+ parts.push(`${p.skills.length} ${p.skills.length === 1 ? "skill" : "skills"}`);
1589
+ if (p.commands.length)
1590
+ parts.push(`${p.commands.length} ${p.commands.length === 1 ? "cmd" : "cmds"}`);
1591
+ if (p.agents.length)
1592
+ parts.push(`${p.agents.length} ${p.agents.length === 1 ? "agent" : "agents"}`);
1457
1593
  if (p.rules.length) parts.push(`${p.rules.length} ${p.rules.length === 1 ? "rule" : "rules"}`);
1458
1594
  if (p.hasHooks) parts.push("hooks");
1459
1595
  if (p.hasMcp) parts.push("mcp");
@@ -1471,7 +1607,9 @@ function printPluginTable(plugins) {
1471
1607
  const name = p.name.padEnd(nameWidth);
1472
1608
  const comp = compStrs[i];
1473
1609
  const desc = truncate(p.description ?? "", descWidth);
1474
- barLine(`${c.bold(name)} ${comp ? c.cyan(comp.padEnd(compWidth)) : " ".repeat(compWidth)} ${c.dim(desc)}`);
1610
+ barLine(
1611
+ `${c.bold(name)} ${comp ? c.cyan(comp.padEnd(compWidth)) : " ".repeat(compWidth)} ${c.dim(desc)}`
1612
+ );
1475
1613
  }
1476
1614
  }
1477
1615
  function printRemotePluginTable(plugins) {
package/package.json CHANGED
@@ -1,20 +1,35 @@
1
1
  {
2
2
  "name": "plugins",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Install open-plugin format plugins into agent tools",
5
5
  "type": "module",
6
+ "packageManager": "pnpm@10.28.1",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
6
10
  "bin": {
7
11
  "plugins": "dist/index.js"
8
12
  },
9
13
  "files": [
10
14
  "dist"
11
15
  ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/vercel-labs/plugins.git"
19
+ },
20
+ "homepage": "https://github.com/vercel-labs/plugins#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/vercel-labs/plugins/issues"
23
+ },
12
24
  "scripts": {
13
25
  "build": "tsup",
14
26
  "start": "node dist/index.js",
15
- "test": "tsx --test test/*.test.ts"
27
+ "test": "tsx --test test/*.test.ts",
28
+ "format:check": "prettier --check \"index.ts\" \"lib/**/*.ts\" \"test/**/*.ts\"",
29
+ "publish:snapshot": "npm version prerelease --preid=snapshot --no-git-tag-version && npm publish --tag snapshot"
16
30
  },
17
31
  "devDependencies": {
32
+ "prettier": "^3",
18
33
  "tsx": "^4.20.6",
19
34
  "tsup": "^8",
20
35
  "typescript": "^5"