plugins 1.2.5 → 1.2.9

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 (2) hide show
  1. package/dist/index.js +258 -15
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { parseArgs } from "util";
5
5
  import { resolve, join as join4 } from "path";
6
6
  import { execSync as execSync3 } from "child_process";
7
7
  import { existsSync as existsSync3, rmSync, mkdirSync } from "fs";
8
+ import { homedir as homedir3 } from "os";
8
9
  import { createInterface } from "readline";
9
10
 
10
11
  // lib/discover.ts
@@ -16,7 +17,8 @@ async function discover(repoPath) {
16
17
  join(repoPath, "marketplace.json"),
17
18
  join(repoPath, ".plugin", "marketplace.json"),
18
19
  join(repoPath, ".claude-plugin", "marketplace.json"),
19
- join(repoPath, ".cursor-plugin", "marketplace.json")
20
+ join(repoPath, ".cursor-plugin", "marketplace.json"),
21
+ join(repoPath, ".codex-plugin", "marketplace.json")
20
22
  ];
21
23
  for (const mp of marketplacePaths) {
22
24
  if (await fileExists(mp)) {
@@ -86,7 +88,7 @@ async function discoverFromMarketplace(repoPath, marketplace) {
86
88
  skills = await discoverSkills(sourcePath);
87
89
  }
88
90
  let manifest = null;
89
- for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin"]) {
91
+ for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin", ".codex-plugin"]) {
90
92
  const manifestPath = join(sourcePath, manifestDir, "plugin.json");
91
93
  if (await fileExists(manifestPath)) {
92
94
  manifest = await readJson(manifestPath);
@@ -127,6 +129,7 @@ async function isPluginDir(dirPath) {
127
129
  join(dirPath, ".plugin", "plugin.json"),
128
130
  join(dirPath, ".claude-plugin", "plugin.json"),
129
131
  join(dirPath, ".cursor-plugin", "plugin.json"),
132
+ join(dirPath, ".codex-plugin", "plugin.json"),
130
133
  join(dirPath, "skills"),
131
134
  join(dirPath, "commands"),
132
135
  join(dirPath, "agents"),
@@ -139,7 +142,7 @@ async function isPluginDir(dirPath) {
139
142
  }
140
143
  async function inspectPlugin(pluginPath) {
141
144
  let manifest = null;
142
- for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin"]) {
145
+ for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin", ".codex-plugin"]) {
143
146
  const manifestPath = join(pluginPath, manifestDir, "plugin.json");
144
147
  if (await fileExists(manifestPath)) {
145
148
  manifest = await readJson(manifestPath);
@@ -331,6 +334,12 @@ var TARGET_DEFS = [
331
334
  name: "Cursor",
332
335
  description: "AI-powered code editor",
333
336
  configPath: join2(HOME, ".cursor")
337
+ },
338
+ {
339
+ id: "codex",
340
+ name: "Codex",
341
+ description: "OpenAI's coding agent",
342
+ configPath: join2(HOME, ".codex")
334
343
  }
335
344
  // Future targets can be added here:
336
345
  // {
@@ -354,6 +363,8 @@ function detectTarget(def) {
354
363
  return detectBinary("claude");
355
364
  case "cursor":
356
365
  return detectBinary("cursor");
366
+ case "codex":
367
+ return detectBinary("codex");
357
368
  default:
358
369
  return false;
359
370
  }
@@ -369,10 +380,11 @@ function detectBinary(name) {
369
380
 
370
381
  // lib/install.ts
371
382
  import { join as join3, relative } from "path";
372
- import { mkdir, cp, readFile as readFile2, writeFile } from "fs/promises";
383
+ import { mkdir, cp, readFile as readFile2, writeFile, rm } from "fs/promises";
373
384
  import { existsSync as existsSync2 } from "fs";
374
385
  import { execSync as execSync2 } from "child_process";
375
386
  import { homedir as homedir2 } from "os";
387
+ import { createHash } from "crypto";
376
388
 
377
389
  // lib/ui.ts
378
390
  var isColorSupported = process.env.FORCE_COLOR !== "0" && !process.env.NO_COLOR && (process.env.FORCE_COLOR !== void 0 || process.stdout.isTTY);
@@ -673,16 +685,45 @@ function banner() {
673
685
  var cachePopulated = false;
674
686
  async function installPlugins(plugins, target, scope, repoPath, source) {
675
687
  switch (target.id) {
676
- case "claude-code":
677
- await installToClaudeCode(plugins, scope, repoPath, source);
688
+ case "claude-code": {
689
+ const workspace = await stageInstallWorkspace(plugins, repoPath, target.id);
690
+ await installToClaudeCode(workspace.plugins, scope, workspace.repoPath, source);
678
691
  break;
679
- case "cursor":
680
- await installToCursor(plugins, scope, repoPath, source);
692
+ }
693
+ case "cursor": {
694
+ if (cachePopulated) return;
695
+ const workspace = await stageInstallWorkspace(plugins, repoPath, target.id);
696
+ await installToCursor(workspace.plugins, scope, workspace.repoPath, source);
697
+ break;
698
+ }
699
+ case "codex": {
700
+ const workspace = await stageInstallWorkspace(plugins, repoPath, target.id);
701
+ await installToCodex(workspace.plugins, scope, workspace.repoPath, source);
681
702
  break;
703
+ }
682
704
  default:
683
705
  throw new Error(`Unsupported target: ${target.id}`);
684
706
  }
685
707
  }
708
+ async function stageInstallWorkspace(plugins, repoPath, targetId, stagingBaseDir = join3(homedir2(), ".cache", "plugins", ".install-staging")) {
709
+ const stageKey = createHash("sha1").update(repoPath).digest("hex");
710
+ const stageRoot = join3(stagingBaseDir, stageKey, targetId);
711
+ const stagedRepoPath = join3(stageRoot, "repo");
712
+ await mkdir(stageRoot, { recursive: true });
713
+ await rm(stagedRepoPath, { recursive: true, force: true });
714
+ await cp(repoPath, stagedRepoPath, { recursive: true });
715
+ const stagedPlugins = plugins.map((plugin) => {
716
+ const relPath = relative(repoPath, plugin.path);
717
+ return {
718
+ ...plugin,
719
+ path: relPath === "" ? stagedRepoPath : join3(stagedRepoPath, relPath)
720
+ };
721
+ });
722
+ return {
723
+ repoPath: stagedRepoPath,
724
+ plugins: stagedPlugins
725
+ };
726
+ }
686
727
  async function installToClaudeCode(plugins, scope, repoPath, source) {
687
728
  const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source);
688
729
  step("Preparing plugins for Claude Code...");
@@ -752,7 +793,7 @@ async function installToCursor(plugins, scope, repoPath, source) {
752
793
  await installToClaudeCode(plugins, scope, repoPath, source);
753
794
  return;
754
795
  }
755
- await installToPluginCache(plugins, scope, repoPath, source);
796
+ await installToCursorExtensions(plugins, scope, repoPath, source);
756
797
  }
757
798
  async function installToPluginCache(plugins, scope, repoPath, source) {
758
799
  const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source);
@@ -822,6 +863,207 @@ async function installToPluginCache(plugins, scope, repoPath, source) {
822
863
  barDebug(c.dim("Updated installed_plugins.json"));
823
864
  cachePopulated = true;
824
865
  }
866
+ async function installToCursorExtensions(plugins, scope, repoPath, source) {
867
+ if (process.platform !== "win32") {
868
+ await installToPluginCache(plugins, scope, repoPath, source);
869
+ return;
870
+ }
871
+ const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source);
872
+ const home = homedir2();
873
+ const extensionsDir = join3(home, ".cursor", "extensions");
874
+ step("Preparing plugins for Cursor...");
875
+ barEmpty();
876
+ await prepareForClaudeCode(plugins, repoPath, marketplaceName);
877
+ await mkdir(extensionsDir, { recursive: true });
878
+ const extensionsJsonPath = join3(extensionsDir, "extensions.json");
879
+ let extensions = [];
880
+ if (existsSync2(extensionsJsonPath)) {
881
+ try {
882
+ const parsed = JSON.parse(await readFile2(extensionsJsonPath, "utf-8"));
883
+ if (Array.isArray(parsed)) extensions = parsed;
884
+ } catch {
885
+ }
886
+ }
887
+ let gitSha;
888
+ try {
889
+ gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim();
890
+ } catch {
891
+ }
892
+ for (const plugin of plugins) {
893
+ const pluginRef = `${plugin.name}@${marketplaceName}`;
894
+ const version = plugin.version ?? "0.0.0";
895
+ const folderName = `${marketplaceName}.${plugin.name}-${version}`;
896
+ const destDir = join3(extensionsDir, folderName);
897
+ step(`Installing ${c.bold(pluginRef)}...`);
898
+ await mkdir(destDir, { recursive: true });
899
+ await cp(plugin.path, destDir, { recursive: true });
900
+ barDebug(c.dim(`Copied to ${destDir}`));
901
+ const identifier = `${marketplaceName}.${plugin.name}`;
902
+ extensions = extensions.filter(
903
+ (e) => e?.identifier?.id !== identifier
904
+ );
905
+ const uriPath = "/" + destDir.replace(/\\/g, "/");
906
+ extensions.push({
907
+ identifier: { id: identifier },
908
+ version,
909
+ location: { $mid: 1, path: uriPath, scheme: "file" },
910
+ relativeLocation: folderName,
911
+ metadata: {
912
+ installedTimestamp: Date.now(),
913
+ ...gitSha ? { gitCommitSha: gitSha } : {}
914
+ }
915
+ });
916
+ stepDone(`Installed ${c.cyan(pluginRef)}`);
917
+ }
918
+ await writeFile(extensionsJsonPath, JSON.stringify(extensions, null, 2));
919
+ barDebug(c.dim("Updated extensions.json"));
920
+ cachePopulated = true;
921
+ }
922
+ async function installToCodex(plugins, scope, repoPath, source) {
923
+ const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source);
924
+ const home = homedir2();
925
+ const cacheDir = join3(home, ".codex", "plugins", "cache");
926
+ const configPath = join3(home, ".codex", "config.toml");
927
+ const marketplaceDir = join3(home, ".agents", "plugins");
928
+ const marketplacePath = join3(marketplaceDir, "marketplace.json");
929
+ const marketplaceRoot = home;
930
+ step("Preparing plugins for Codex...");
931
+ barEmpty();
932
+ for (const plugin of plugins) {
933
+ await preparePluginDirForVendor(plugin, ".codex-plugin", "CODEX_PLUGIN_ROOT");
934
+ await enrichForCodex(plugin);
935
+ }
936
+ let gitSha;
937
+ try {
938
+ gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim();
939
+ } catch {
940
+ }
941
+ const versionKey = gitSha ?? "local";
942
+ const pluginPaths = {};
943
+ for (const plugin of plugins) {
944
+ const pluginRef = `${plugin.name}@${marketplaceName}`;
945
+ step(`Installing ${c.bold(pluginRef)}...`);
946
+ const cacheDest = join3(cacheDir, marketplaceName, plugin.name, versionKey);
947
+ await mkdir(cacheDest, { recursive: true });
948
+ await cp(plugin.path, cacheDest, { recursive: true });
949
+ pluginPaths[plugin.name] = cacheDest;
950
+ barDebug(c.dim(`Cached to ${cacheDest}`));
951
+ stepDone(`Installed ${c.cyan(pluginRef)}`);
952
+ }
953
+ step("Updating marketplace...");
954
+ await mkdir(marketplaceDir, { recursive: true });
955
+ let marketplace = {
956
+ name: "plugins-cli",
957
+ interface: { displayName: "Plugins CLI" },
958
+ plugins: []
959
+ };
960
+ if (existsSync2(marketplacePath)) {
961
+ try {
962
+ const existing = JSON.parse(await readFile2(marketplacePath, "utf-8"));
963
+ if (existing && typeof existing === "object" && Array.isArray(existing.plugins)) {
964
+ marketplace = existing;
965
+ }
966
+ } catch {
967
+ }
968
+ }
969
+ for (const plugin of plugins) {
970
+ const cacheDest = pluginPaths[plugin.name];
971
+ const relPath = relative(marketplaceRoot, cacheDest);
972
+ marketplace.plugins = marketplace.plugins.filter(
973
+ (e) => e.name !== plugin.name
974
+ );
975
+ marketplace.plugins.push({
976
+ name: plugin.name,
977
+ source: {
978
+ source: "local",
979
+ path: `./${relPath}`
980
+ },
981
+ policy: {
982
+ installation: "AVAILABLE",
983
+ authentication: "ON_INSTALL"
984
+ },
985
+ category: "Coding"
986
+ });
987
+ }
988
+ await writeFile(marketplacePath, JSON.stringify(marketplace, null, 2));
989
+ stepDone("Marketplace updated");
990
+ step("Updating config.toml...");
991
+ await mkdir(join3(home, ".codex"), { recursive: true });
992
+ let configContent = "";
993
+ if (existsSync2(configPath)) {
994
+ configContent = await readFile2(configPath, "utf-8");
995
+ }
996
+ let configChanged = false;
997
+ for (const plugin of plugins) {
998
+ const pluginKey = `${plugin.name}@plugins-cli`;
999
+ const tomlSection = `[plugins."${pluginKey}"]`;
1000
+ if (configContent.includes(tomlSection)) {
1001
+ barDebug(c.dim(`${pluginKey} already in config.toml`));
1002
+ continue;
1003
+ }
1004
+ const entry = `
1005
+ ${tomlSection}
1006
+ enabled = true
1007
+ `;
1008
+ configContent += entry;
1009
+ configChanged = true;
1010
+ barDebug(c.dim(`Added ${pluginKey} to config.toml`));
1011
+ }
1012
+ if (configChanged) {
1013
+ await writeFile(configPath, configContent);
1014
+ }
1015
+ stepDone("Config updated");
1016
+ }
1017
+ async function enrichForCodex(plugin) {
1018
+ const codexManifestPath = join3(plugin.path, ".codex-plugin", "plugin.json");
1019
+ if (!existsSync2(codexManifestPath)) return;
1020
+ let manifest;
1021
+ try {
1022
+ manifest = JSON.parse(await readFile2(codexManifestPath, "utf-8"));
1023
+ } catch {
1024
+ return;
1025
+ }
1026
+ if (manifest.interface) return;
1027
+ let changed = false;
1028
+ if (!manifest.skills && existsSync2(join3(plugin.path, "skills"))) {
1029
+ manifest.skills = "./skills/";
1030
+ changed = true;
1031
+ }
1032
+ if (!manifest.mcpServers && existsSync2(join3(plugin.path, ".mcp.json"))) {
1033
+ manifest.mcpServers = "./.mcp.json";
1034
+ changed = true;
1035
+ }
1036
+ if (!manifest.apps && existsSync2(join3(plugin.path, ".app.json"))) {
1037
+ manifest.apps = "./.app.json";
1038
+ changed = true;
1039
+ }
1040
+ const name = manifest.name ?? plugin.name;
1041
+ const description = manifest.description ?? plugin.description ?? "";
1042
+ const author = manifest.author;
1043
+ const iface = {
1044
+ displayName: name.charAt(0).toUpperCase() + name.slice(1),
1045
+ shortDescription: description,
1046
+ developerName: author?.name ?? "Unknown",
1047
+ category: "Coding",
1048
+ capabilities: ["Interactive", "Write"]
1049
+ };
1050
+ if (manifest.homepage) iface.websiteURL = manifest.homepage;
1051
+ else if (manifest.repository) iface.websiteURL = manifest.repository;
1052
+ const assetCandidates = ["assets/app-icon.png", "assets/icon.png", "assets/logo.png", "assets/logo.svg"];
1053
+ for (const candidate of assetCandidates) {
1054
+ if (existsSync2(join3(plugin.path, candidate))) {
1055
+ iface.logo = `./${candidate}`;
1056
+ iface.composerIcon = `./${candidate}`;
1057
+ break;
1058
+ }
1059
+ }
1060
+ manifest.interface = iface;
1061
+ changed = true;
1062
+ if (changed) {
1063
+ await writeFile(codexManifestPath, JSON.stringify(manifest, null, 2));
1064
+ barDebug(c.dim(`${plugin.name}: enriched .codex-plugin/plugin.json for Codex`));
1065
+ }
1066
+ }
825
1067
  async function prepareForClaudeCode(plugins, repoPath, marketplaceName) {
826
1068
  const claudePluginDir = join3(repoPath, ".claude-plugin");
827
1069
  await mkdir(claudePluginDir, { recursive: true });
@@ -903,7 +1145,8 @@ async function preparePluginDirForVendor(plugin, vendorDir, envVar) {
903
1145
  var KNOWN_PLUGIN_ROOT_VARS = [
904
1146
  "PLUGIN_ROOT",
905
1147
  "CLAUDE_PLUGIN_ROOT",
906
- "CURSOR_PLUGIN_ROOT"
1148
+ "CURSOR_PLUGIN_ROOT",
1149
+ "CODEX_PLUGIN_ROOT"
907
1150
  ];
908
1151
  async function translateEnvVars(pluginPath, pluginName, envVar) {
909
1152
  const configFiles = [
@@ -1004,7 +1247,7 @@ function track(data) {
1004
1247
  }
1005
1248
 
1006
1249
  // index.ts
1007
- setVersion("1.0.1");
1250
+ setVersion("1.2.8");
1008
1251
  var { values, positionals } = parseArgs({
1009
1252
  args: process.argv.slice(2),
1010
1253
  options: {
@@ -1158,7 +1401,7 @@ async function cmdInstall(source, opts) {
1158
1401
  } else if (detectedTargets.length === 0) {
1159
1402
  barEmpty();
1160
1403
  stepError("No supported targets detected.");
1161
- barLine(c.dim("Neither 'claude' nor 'cursor' binaries were found on PATH."));
1404
+ barLine(c.dim("Neither 'claude', 'cursor', nor 'codex' binaries were found on PATH."));
1162
1405
  barLine(c.dim("Use --target to specify one manually."));
1163
1406
  footer();
1164
1407
  process.exit(1);
@@ -1288,7 +1531,7 @@ function sshToHttps(sshUrl) {
1288
1531
  function resolveSource(source) {
1289
1532
  if (source.startsWith("https://") || source.startsWith("git@") || source.match(/^[\w-]+\/[\w.-]+$/)) {
1290
1533
  const url = source.match(/^[\w-]+\/[\w.-]+$/) ? `https://github.com/${source}` : source;
1291
- const cacheDir = join4(process.env.HOME ?? "~", ".cache", "plugins");
1534
+ const cacheDir = join4(homedir3(), ".cache", "plugins");
1292
1535
  mkdirSync(cacheDir, { recursive: true });
1293
1536
  const slug = url.replace(/^https?:\/\//, "").replace(/^git@/, "").replace(/\.git$/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
1294
1537
  const tmpDir = join4(cacheDir, slug);
@@ -1298,7 +1541,7 @@ function resolveSource(source) {
1298
1541
  step(`Source: ${c.dim(url)}`);
1299
1542
  barEmpty();
1300
1543
  try {
1301
- execSync3(`git clone --depth 1 -q ${url} ${tmpDir}`, { stdio: "pipe" });
1544
+ execSync3(`git clone --depth 1 -q "${url}" "${tmpDir}"`, { stdio: "pipe" });
1302
1545
  } catch (err) {
1303
1546
  const stderr = err.stderr?.toString() ?? "";
1304
1547
  if (url.startsWith("git@") && stderr.includes("Permission denied")) {
@@ -1308,7 +1551,7 @@ function resolveSource(source) {
1308
1551
  step(`Source: ${c.dim(httpsUrl)}`);
1309
1552
  barEmpty();
1310
1553
  try {
1311
- execSync3(`git clone --depth 1 -q ${httpsUrl} ${tmpDir}`, { stdio: "inherit" });
1554
+ execSync3(`git clone --depth 1 -q "${httpsUrl}" "${tmpDir}"`, { stdio: "inherit" });
1312
1555
  stepDone("Repository cloned");
1313
1556
  barEmpty();
1314
1557
  return tmpDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugins",
3
- "version": "1.2.5",
3
+ "version": "1.2.9",
4
4
  "description": "Install open-plugin format plugins into agent tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,9 +11,11 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "build": "tsup",
14
- "start": "node dist/index.js"
14
+ "start": "node dist/index.js",
15
+ "test": "tsx --test test/*.test.ts"
15
16
  },
16
17
  "devDependencies": {
18
+ "tsx": "^4.20.6",
17
19
  "tsup": "^8",
18
20
  "typescript": "^5"
19
21
  }