auriga-cli 1.29.1 → 1.29.2
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/catalog.json +1 -1
- package/dist/plugins.d.ts +11 -15
- package/dist/plugins.js +102 -246
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ Supports both project and global installation scopes.
|
|
|
121
121
|
|
|
122
122
|
### Plugins
|
|
123
123
|
|
|
124
|
-
Installs selected plugins for Claude Code, Codex, or both. Claude Code uses `claude plugins install` and honors `--scope project|user`; Codex
|
|
124
|
+
Installs selected plugins for Claude Code, Codex, or both. Claude Code uses `claude plugins install` and honors `--scope project|user`; Codex registers the marketplace via `codex plugin marketplace add/upgrade` (the right one is picked by reading `~/.codex/config.toml`), then installs each selected plugin with the native `codex plugin add <plugin>@<marketplace>` command. The Codex path requires a Codex CLI new enough to expose `codex plugin add`; on older versions the Codex-side install aborts with an upgrade hint.
|
|
125
125
|
|
|
126
126
|
Examples:
|
|
127
127
|
|
package/README.zh-CN.md
CHANGED
|
@@ -121,7 +121,7 @@ npx auriga-cli
|
|
|
121
121
|
|
|
122
122
|
### Plugins
|
|
123
123
|
|
|
124
|
-
可以把选中的插件安装到 Claude Code、Codex 或两者都装。Claude Code 路径使用 `claude plugins install`,并遵守 `--scope project|user`;Codex 路径根据 `~/.codex/config.toml` 中是否已注册同名 marketplace 自动选择 `codex plugin marketplace add` 或 `upgrade
|
|
124
|
+
可以把选中的插件安装到 Claude Code、Codex 或两者都装。Claude Code 路径使用 `claude plugins install`,并遵守 `--scope project|user`;Codex 路径根据 `~/.codex/config.toml` 中是否已注册同名 marketplace 自动选择 `codex plugin marketplace add` 或 `upgrade` 注册 marketplace,再用原生的 `codex plugin add <plugin>@<marketplace>` 命令安装每个选中的插件。Codex 路径要求 Codex CLI 版本新到支持 `codex plugin add`;旧版本会中止 Codex 侧安装并提示升级。
|
|
125
125
|
|
|
126
126
|
示例:
|
|
127
127
|
|
package/dist/catalog.json
CHANGED
package/dist/plugins.d.ts
CHANGED
|
@@ -54,22 +54,18 @@ export declare function installPlugins(packageRoot: string, opts: InstallOpts):
|
|
|
54
54
|
* surfaces nuanced failure modes (marketplace gone, network) that
|
|
55
55
|
* the caller needs to see verbatim.
|
|
56
56
|
*
|
|
57
|
-
* Codex side:
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
57
|
+
* Codex side: shells out to `codex plugin remove <id>`, which deletes
|
|
58
|
+
* the plugin from Codex's local config and cache. We deliberately do
|
|
59
|
+
* NOT remove the marketplace itself — a single marketplace may host
|
|
60
|
+
* multiple plugins, and tearing it down because one plugin left would
|
|
61
|
+
* break the others. The user can `codex plugin marketplace remove`
|
|
62
|
+
* separately when they want. Errors are propagated like the Claude
|
|
63
|
+
* side; idempotency (removing an already-absent plugin) is the Codex
|
|
64
|
+
* CLI's responsibility.
|
|
65
65
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* `codex plugin marketplace remove` separately when they want.
|
|
70
|
-
*
|
|
71
|
-
* Validation happens before any I/O — a malformed id throws cleanly with
|
|
72
|
-
* no side effects, so retries are safe.
|
|
66
|
+
* `parsePluginId` validates the id shape — and rejects shell
|
|
67
|
+
* metacharacters in either segment — before any command runs, so a
|
|
68
|
+
* malformed id throws cleanly with no side effects and retries are safe.
|
|
73
69
|
*/
|
|
74
70
|
export declare function uninstallPlugin(id: string, agent: "claude" | "codex", opts: {
|
|
75
71
|
cwd: string;
|
package/dist/plugins.js
CHANGED
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { checkbox, select } from "@inquirer/prompts";
|
|
5
|
-
import { parse as parseToml
|
|
6
|
-
import {
|
|
5
|
+
import { parse as parseToml } from "smol-toml";
|
|
6
|
+
import { codexManifestPath, validateCodexMarketplace, } from "./codex-plugin-config.js";
|
|
7
7
|
import { validateMarketplaceField } from "./marketplace.js";
|
|
8
8
|
import { atomicWriteFile, exec, execAsync, log, withEsc } from "./utils.js";
|
|
9
9
|
// Plugin names and plugin-package names end up in `claude plugins ...`
|
|
@@ -24,7 +24,6 @@ const MIGRATED_WORKFLOW_SKILLS = [
|
|
|
24
24
|
const NOTIFY_PLUGIN_NAME = "auriga-notify";
|
|
25
25
|
const WORKFLOW_SKILLS_PLUGIN_NAME = "auriga-workflow";
|
|
26
26
|
const LEGACY_NOTIFY_MARKER = "auriga:notify";
|
|
27
|
-
const CODEX_PLUGIN_VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
28
27
|
function validateClaudeMarketplace(raw) {
|
|
29
28
|
if (!raw || typeof raw !== "object") {
|
|
30
29
|
throw new Error("Claude marketplace.json: root must be an object");
|
|
@@ -264,17 +263,6 @@ function resolveCodexPluginSelection(all, selected) {
|
|
|
264
263
|
function codexHome() {
|
|
265
264
|
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
266
265
|
}
|
|
267
|
-
function codexMarketplaceCacheRoot(marketplaceName) {
|
|
268
|
-
return path.join(codexHome(), ".tmp", "marketplaces", marketplaceName);
|
|
269
|
-
}
|
|
270
|
-
function resolveCodexMarketplaceContentRoot(packageRoot, marketplaceName) {
|
|
271
|
-
const cachedRoot = codexMarketplaceCacheRoot(marketplaceName);
|
|
272
|
-
if (fs.existsSync(path.join(cachedRoot, ".agents", "plugins", "marketplace.json"))) {
|
|
273
|
-
return cachedRoot;
|
|
274
|
-
}
|
|
275
|
-
throw new Error(`Codex marketplace ${marketplaceName} cache missing at ${cachedRoot}. ` +
|
|
276
|
-
"Run `codex plugin marketplace add/upgrade` successfully before materializing local plugins.");
|
|
277
|
-
}
|
|
278
266
|
function shellQuote(value) {
|
|
279
267
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
280
268
|
}
|
|
@@ -462,6 +450,35 @@ function codexExternalMarketplaceAddCommand(source) {
|
|
|
462
450
|
function codexMarketplaceUpgradeCommand(marketplaceName) {
|
|
463
451
|
return `codex plugin marketplace upgrade ${shellQuote(marketplaceName)}`;
|
|
464
452
|
}
|
|
453
|
+
// Capability probe for the native `codex plugin add` command. Older Codex
|
|
454
|
+
// versions expose `codex plugin marketplace` but not `add`; probing the
|
|
455
|
+
// subcommand's `--help` is version-number-agnostic — it needs no knowledge
|
|
456
|
+
// of which Codex release introduced `add`, so it can't rot the way a
|
|
457
|
+
// hardcoded minimum-version compare would. `--help` prints and exits 0
|
|
458
|
+
// when the subcommand exists, and exits non-zero (throwing here) when it
|
|
459
|
+
// doesn't.
|
|
460
|
+
function codexSupportsPluginAdd() {
|
|
461
|
+
try {
|
|
462
|
+
exec("codex plugin add --help");
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// `--enable plugins` turns on the global Codex plugins feature; `--enable
|
|
470
|
+
// plugin_hooks` is appended only for plugins that ship hooks. We pass the
|
|
471
|
+
// feature flags explicitly rather than relying on `codex plugin add` to
|
|
472
|
+
// flip them — both flags are idempotent, so re-passing them across
|
|
473
|
+
// multiple `add` calls is harmless. The plugin key (`<name>@<marketplace>`)
|
|
474
|
+
// is validated upstream (PLUGIN_NAME_RE / MARKETPLACE_NAME_RE), so no
|
|
475
|
+
// shell metacharacter can reach this interpolated command.
|
|
476
|
+
function codexPluginAddCommand(pluginKey, hasHooks) {
|
|
477
|
+
const enable = hasHooks
|
|
478
|
+
? "--enable plugins --enable plugin_hooks"
|
|
479
|
+
: "--enable plugins";
|
|
480
|
+
return `codex plugin add ${pluginKey} ${enable}`;
|
|
481
|
+
}
|
|
465
482
|
function commandErrorText(error) {
|
|
466
483
|
if (!(error instanceof Error))
|
|
467
484
|
return String(error);
|
|
@@ -538,131 +555,6 @@ function ensureCodexPluginManifests(packageRoot, plugins) {
|
|
|
538
555
|
throw new Error(`Codex plugin ${plugin.name} manifest missing at ${manifestPath}`);
|
|
539
556
|
}
|
|
540
557
|
}
|
|
541
|
-
function readCodexPluginVersion(packageRoot, plugin) {
|
|
542
|
-
const manifestPath = codexManifestPath(plugin);
|
|
543
|
-
if (!manifestPath) {
|
|
544
|
-
throw new Error(`Codex marketplace.json: plugin ${plugin.name} must use a local source.path`);
|
|
545
|
-
}
|
|
546
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, manifestPath), "utf-8"));
|
|
547
|
-
if (typeof manifest.version !== "string" || !CODEX_PLUGIN_VERSION_RE.test(manifest.version)) {
|
|
548
|
-
throw new Error(`Codex plugin ${plugin.name} manifest must include a safe string version`);
|
|
549
|
-
}
|
|
550
|
-
return manifest.version;
|
|
551
|
-
}
|
|
552
|
-
function materializeLocalCodexPluginCache(packageRoot, marketplaceName, plugins) {
|
|
553
|
-
const cacheRoot = path.join(codexHome(), "plugins", "cache");
|
|
554
|
-
for (const plugin of plugins) {
|
|
555
|
-
const sourcePath = codexLocalPluginPath(plugin);
|
|
556
|
-
if (!sourcePath) {
|
|
557
|
-
throw new Error(`Codex marketplace.json: plugin ${plugin.name} must use a local source.path`);
|
|
558
|
-
}
|
|
559
|
-
const version = readCodexPluginVersion(packageRoot, plugin);
|
|
560
|
-
const sourceDir = path.join(packageRoot, sourcePath);
|
|
561
|
-
const destDir = path.join(cacheRoot, marketplaceName, plugin.name, version);
|
|
562
|
-
const tmpDir = `${destDir}.tmp-${process.pid}-${Date.now()}`;
|
|
563
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
564
|
-
fs.mkdirSync(path.dirname(destDir), { recursive: true });
|
|
565
|
-
fs.cpSync(sourceDir, tmpDir, { recursive: true });
|
|
566
|
-
fs.rmSync(destDir, { recursive: true, force: true });
|
|
567
|
-
fs.renameSync(tmpDir, destDir);
|
|
568
|
-
if (!fs.existsSync(path.join(destDir, ".codex-plugin", "plugin.json"))) {
|
|
569
|
-
throw new Error(`Codex plugin ${plugin.name} cache materialization did not produce plugin.json`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
function ensureTomlBoolean(content, section, key, value) {
|
|
574
|
-
const line = `${key} = ${value ? "true" : "false"}`;
|
|
575
|
-
const header = `[${section}]`;
|
|
576
|
-
const lines = content.length > 0 ? content.split(/\r?\n/) : [];
|
|
577
|
-
const start = lines.findIndex((l) => l.trim() === header);
|
|
578
|
-
if (start === -1) {
|
|
579
|
-
const prefix = content.trimEnd();
|
|
580
|
-
return `${prefix}${prefix ? "\n\n" : ""}${header}\n${line}\n`;
|
|
581
|
-
}
|
|
582
|
-
let end = lines.length;
|
|
583
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
584
|
-
if (/^\s*\[/.test(lines[i])) {
|
|
585
|
-
end = i;
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
const keyRe = new RegExp(`^\\s*${key}\\s*=`);
|
|
590
|
-
for (let i = start + 1; i < end; i += 1) {
|
|
591
|
-
if (keyRe.test(lines[i])) {
|
|
592
|
-
lines[i] = line;
|
|
593
|
-
return lines.join("\n");
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
lines.splice(end, 0, line);
|
|
597
|
-
return lines.join("\n");
|
|
598
|
-
}
|
|
599
|
-
function parseCodexConfigToml(content, configPath) {
|
|
600
|
-
if (content.trim().length === 0)
|
|
601
|
-
return {};
|
|
602
|
-
try {
|
|
603
|
-
return parseToml(content);
|
|
604
|
-
}
|
|
605
|
-
catch (e) {
|
|
606
|
-
throw new Error(`Codex config.toml is invalid TOML at ${configPath}: ${e.message}`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
function isTomlTable(value) {
|
|
610
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
611
|
-
}
|
|
612
|
-
function getOrCreateTomlTable(parent, key, pathLabel) {
|
|
613
|
-
const existing = parent[key];
|
|
614
|
-
if (existing === undefined) {
|
|
615
|
-
const table = {};
|
|
616
|
-
parent[key] = table;
|
|
617
|
-
return table;
|
|
618
|
-
}
|
|
619
|
-
if (!isTomlTable(existing)) {
|
|
620
|
-
throw new Error(`Codex config.toml: ${pathLabel} must be a TOML table`);
|
|
621
|
-
}
|
|
622
|
-
return existing;
|
|
623
|
-
}
|
|
624
|
-
function buildCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks) {
|
|
625
|
-
const parsed = parseCodexConfigToml(originalContent, configPath);
|
|
626
|
-
const features = getOrCreateTomlTable(parsed, "features", "features");
|
|
627
|
-
features.plugins = true;
|
|
628
|
-
if (needsPluginHooks) {
|
|
629
|
-
features.plugin_hooks = true;
|
|
630
|
-
}
|
|
631
|
-
const plugins = getOrCreateTomlTable(parsed, "plugins", "plugins");
|
|
632
|
-
for (const pluginKey of pluginKeys) {
|
|
633
|
-
const plugin = getOrCreateTomlTable(plugins, pluginKey, `plugins.${JSON.stringify(pluginKey)}`);
|
|
634
|
-
plugin.enabled = true;
|
|
635
|
-
}
|
|
636
|
-
return stringifyToml(parsed);
|
|
637
|
-
}
|
|
638
|
-
function tryMinimalCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks) {
|
|
639
|
-
let content = originalContent;
|
|
640
|
-
content = ensureTomlBoolean(content, "features", "plugins", true);
|
|
641
|
-
if (needsPluginHooks) {
|
|
642
|
-
content = ensureTomlBoolean(content, "features", "plugin_hooks", true);
|
|
643
|
-
}
|
|
644
|
-
for (const pluginKey of pluginKeys) {
|
|
645
|
-
content = ensureTomlBoolean(content, `plugins."${pluginKey}"`, "enabled", true);
|
|
646
|
-
}
|
|
647
|
-
try {
|
|
648
|
-
parseToml(content);
|
|
649
|
-
return content;
|
|
650
|
-
}
|
|
651
|
-
catch {
|
|
652
|
-
// Existing configs may use legal TOML forms such as inline tables
|
|
653
|
-
// (`features = { plugins = false }`). In that case, a local section
|
|
654
|
-
// insertion would redefine the table, so fall back to structured output.
|
|
655
|
-
parseCodexConfigToml(originalContent, configPath);
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
function enableCodexPluginConfig(configPath, pluginKeys, needsPluginHooks) {
|
|
660
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
661
|
-
const originalContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf-8") : "";
|
|
662
|
-
const minimalContent = tryMinimalCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks);
|
|
663
|
-
const content = minimalContent ?? buildCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks);
|
|
664
|
-
atomicWriteFile(configPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
665
|
-
}
|
|
666
558
|
async function addCodexMarketplaceWithRetry(marketplaceName, addCommand, expectedSource, opts, marketplaceExecOpts, failures) {
|
|
667
559
|
const registeredSource = readCodexMarketplaceSource(marketplaceName);
|
|
668
560
|
if (registeredSource !== null) {
|
|
@@ -706,29 +598,32 @@ async function addCodexMarketplaceWithRetry(marketplaceName, addCommand, expecte
|
|
|
706
598
|
failures.push(`codex marketplace ${marketplaceName}`);
|
|
707
599
|
}
|
|
708
600
|
}
|
|
709
|
-
// Builds the
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
// manifest
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
async function composeCodexPluginKeys(pluginContentRoot, localMarketplace, selectedMarketplacePlugins, externalSelected) {
|
|
719
|
-
const pluginKeys = [];
|
|
720
|
-
let needsPluginHooks = false;
|
|
601
|
+
// Builds the `codex plugin add` work list: one entry per selected plugin.
|
|
602
|
+
// Local plugins resolve their hooks flag from this repo's manifest;
|
|
603
|
+
// external plugins emit a key straight from extra_plugin_configs.json
|
|
604
|
+
// (Codex CLI fetches the upstream manifest itself) and never set
|
|
605
|
+
// `hasHooks` — we don't have their manifest at install time. Acceptable
|
|
606
|
+
// while no external plugin ships hooks; once one does, prefer fetching the
|
|
607
|
+
// manifest or adding an explicit flag to the extra config entry.
|
|
608
|
+
function composeCodexPluginAdds(pluginContentRoot, localMarketplace, selectedMarketplacePlugins, externalSelected) {
|
|
609
|
+
const adds = [];
|
|
721
610
|
if (localMarketplace) {
|
|
722
611
|
for (const plugin of selectedMarketplacePlugins) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
612
|
+
adds.push({
|
|
613
|
+
key: `${plugin.name}@${localMarketplace.name}`,
|
|
614
|
+
name: plugin.name,
|
|
615
|
+
hasHooks: pluginHasHooks(pluginContentRoot, plugin),
|
|
616
|
+
});
|
|
726
617
|
}
|
|
727
618
|
}
|
|
728
619
|
for (const p of externalSelected) {
|
|
729
|
-
|
|
620
|
+
adds.push({
|
|
621
|
+
key: `${p.name}@${p.marketplace.name}`,
|
|
622
|
+
name: p.name,
|
|
623
|
+
hasHooks: false,
|
|
624
|
+
});
|
|
730
625
|
}
|
|
731
|
-
return
|
|
626
|
+
return adds;
|
|
732
627
|
}
|
|
733
628
|
async function installCodexPlugins(packageRoot, opts) {
|
|
734
629
|
const installConfig = loadCodexInstallConfig(packageRoot);
|
|
@@ -756,6 +651,20 @@ async function installCodexPlugins(packageRoot, opts) {
|
|
|
756
651
|
log.skip("No Codex plugins selected");
|
|
757
652
|
return;
|
|
758
653
|
}
|
|
654
|
+
// Version gate: native `codex plugin add` materializes the plugin cache
|
|
655
|
+
// and writes the enable config itself. Without it there is no supported
|
|
656
|
+
// install path — fail fast with an actionable upgrade hint rather than
|
|
657
|
+
// falling back to a hand-rolled cache/config mechanism. Under `--agent
|
|
658
|
+
// both` this throw is caught by installPlugins' aggregator, so the
|
|
659
|
+
// Claude-side install still completes (the Codex side is recorded as a
|
|
660
|
+
// failure).
|
|
661
|
+
if (!codexSupportsPluginAdd()) {
|
|
662
|
+
const msg = "Codex CLI does not support `codex plugin add` — upgrade the Codex CLI and retry";
|
|
663
|
+
if (!opts.interactive)
|
|
664
|
+
throw new Error(msg);
|
|
665
|
+
log.error(msg);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
759
668
|
// Local plugins are described by this repo's .agents/plugins/marketplace.json.
|
|
760
669
|
// External plugins come from extra_plugin_configs.json and are resolved by
|
|
761
670
|
// Codex CLI itself when their marketplace is added — we only need to
|
|
@@ -799,24 +708,26 @@ async function installCodexPlugins(packageRoot, opts) {
|
|
|
799
708
|
await addCodexMarketplaceWithRetry(mp.name, codexExternalMarketplaceAddCommand(mp.source), codexExternalMarketplaceSource(mp.source), opts, marketplaceExecOpts, failures);
|
|
800
709
|
}
|
|
801
710
|
if (failures.length === 0) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
? resolveSelectedCodexMarketplacePlugins(effectiveLocalMarketplace, localSelected)
|
|
711
|
+
// Hooks detection reads this repo's manifest directly: local plugins
|
|
712
|
+
// listed in .agents/plugins/marketplace.json are sourced from this
|
|
713
|
+
// repo, so packageRoot is the authoritative manifest location. The
|
|
714
|
+
// plugin payload itself is materialized by `codex plugin add` from the
|
|
715
|
+
// marketplace snapshot registered above — no manual cache copy.
|
|
716
|
+
const selectedMarketplacePlugins = localMarketplace
|
|
717
|
+
? resolveSelectedCodexMarketplacePlugins(localMarketplace, localSelected)
|
|
810
718
|
: [];
|
|
811
|
-
ensureCodexPluginManifests(
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
719
|
+
ensureCodexPluginManifests(packageRoot, selectedMarketplacePlugins);
|
|
720
|
+
const pluginAdds = composeCodexPluginAdds(packageRoot, localMarketplace, selectedMarketplacePlugins, externalSelected);
|
|
721
|
+
for (const entry of pluginAdds) {
|
|
722
|
+
try {
|
|
723
|
+
exec(codexPluginAddCommand(entry.key, entry.hasHooks), marketplaceExecOpts);
|
|
724
|
+
log.ok(`Codex plugin ${entry.key} added`);
|
|
725
|
+
runPostInstallMigration(entry.name, opts, ["codex"]);
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
log.error(`Failed to add Codex plugin ${entry.key}\n${commandErrorText(e)}`);
|
|
729
|
+
failures.push(`codex plugin ${entry.key}`);
|
|
730
|
+
}
|
|
820
731
|
}
|
|
821
732
|
}
|
|
822
733
|
if (failures.length > 0 && !opts.interactive) {
|
|
@@ -1046,23 +957,6 @@ function parsePluginId(id) {
|
|
|
1046
957
|
}
|
|
1047
958
|
return { plugin: m[1], marketplace: m[2] };
|
|
1048
959
|
}
|
|
1049
|
-
/**
|
|
1050
|
-
* Remove `[plugins."<id>"]` from a parsed Codex config TOML tree.
|
|
1051
|
-
* Returns true if anything was removed. Idempotent: missing key → false.
|
|
1052
|
-
*
|
|
1053
|
-
* Pure function operating on the parsed tree — no I/O. Lets the test
|
|
1054
|
-
* harness assert tree shape without touching disk + lets the I/O wrapper
|
|
1055
|
-
* skip the atomic write when nothing changed.
|
|
1056
|
-
*/
|
|
1057
|
-
function removeCodexPluginFromConfig(parsed, pluginId) {
|
|
1058
|
-
const plugins = parsed.plugins;
|
|
1059
|
-
if (!isTomlTable(plugins))
|
|
1060
|
-
return false;
|
|
1061
|
-
if (!(pluginId in plugins))
|
|
1062
|
-
return false;
|
|
1063
|
-
delete plugins[pluginId];
|
|
1064
|
-
return true;
|
|
1065
|
-
}
|
|
1066
960
|
/**
|
|
1067
961
|
* Uninstall a single plugin.
|
|
1068
962
|
*
|
|
@@ -1071,25 +965,21 @@ function removeCodexPluginFromConfig(parsed, pluginId) {
|
|
|
1071
965
|
* surfaces nuanced failure modes (marketplace gone, network) that
|
|
1072
966
|
* the caller needs to see verbatim.
|
|
1073
967
|
*
|
|
1074
|
-
* Codex side:
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
* Caveat: we deliberately do NOT remove the marketplace itself. A
|
|
1084
|
-
* single marketplace may host multiple plugins; tearing it down
|
|
1085
|
-
* because one plugin left would break others. The user can
|
|
1086
|
-
* `codex plugin marketplace remove` separately when they want.
|
|
968
|
+
* Codex side: shells out to `codex plugin remove <id>`, which deletes
|
|
969
|
+
* the plugin from Codex's local config and cache. We deliberately do
|
|
970
|
+
* NOT remove the marketplace itself — a single marketplace may host
|
|
971
|
+
* multiple plugins, and tearing it down because one plugin left would
|
|
972
|
+
* break the others. The user can `codex plugin marketplace remove`
|
|
973
|
+
* separately when they want. Errors are propagated like the Claude
|
|
974
|
+
* side; idempotency (removing an already-absent plugin) is the Codex
|
|
975
|
+
* CLI's responsibility.
|
|
1087
976
|
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
977
|
+
* `parsePluginId` validates the id shape — and rejects shell
|
|
978
|
+
* metacharacters in either segment — before any command runs, so a
|
|
979
|
+
* malformed id throws cleanly with no side effects and retries are safe.
|
|
1090
980
|
*/
|
|
1091
981
|
export async function uninstallPlugin(id, agent, opts) {
|
|
1092
|
-
|
|
982
|
+
parsePluginId(id);
|
|
1093
983
|
const emit = (line) => { opts.onLog?.(line); };
|
|
1094
984
|
if (agent === "claude") {
|
|
1095
985
|
// Note: scope is intentionally NOT specified. `claude plugins
|
|
@@ -1101,44 +991,10 @@ export async function uninstallPlugin(id, agent, opts) {
|
|
|
1101
991
|
emit(`uninstalled ${id} from Claude Code`);
|
|
1102
992
|
return;
|
|
1103
993
|
}
|
|
1104
|
-
// Codex path
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
// filesystem (cache dir removal also gets skipped) so a damaged
|
|
1111
|
-
// config doesn't end up half-uninstalled. The test "config.toml
|
|
1112
|
-
// damaged → throw before mutation" locks this in.
|
|
1113
|
-
const parsed = parseCodexConfigToml(content, configPath);
|
|
1114
|
-
const removed = removeCodexPluginFromConfig(parsed, id);
|
|
1115
|
-
if (removed) {
|
|
1116
|
-
const next = stringifyToml(parsed);
|
|
1117
|
-
atomicWriteFile(configPath, next.endsWith("\n") ? next : `${next}\n`);
|
|
1118
|
-
log.ok(`${id} disabled in Codex config.toml`);
|
|
1119
|
-
emit(`removed ${id} from Codex config.toml`);
|
|
1120
|
-
}
|
|
1121
|
-
else {
|
|
1122
|
-
log.skip(`${id} not present in Codex config.toml`);
|
|
1123
|
-
emit(`${id} not present in Codex config.toml`);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
else {
|
|
1127
|
-
log.skip(`Codex config.toml not present`);
|
|
1128
|
-
emit(`Codex config.toml not present`);
|
|
1129
|
-
}
|
|
1130
|
-
// Cache dir: ~/.codex/plugins/cache/<marketplace>/<plugin>/
|
|
1131
|
-
// PLUGIN_ID_RE constrains both segments to a safe charset, so the
|
|
1132
|
-
// path can't escape via injection. rmSync with recursive+force is
|
|
1133
|
-
// the standard rm-rf idiom; missing dir is a no-op.
|
|
1134
|
-
const cacheDir = path.join(home, "plugins", "cache", marketplace, plugin);
|
|
1135
|
-
if (fs.existsSync(cacheDir)) {
|
|
1136
|
-
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
1137
|
-
log.ok(`${id} cache directory removed`);
|
|
1138
|
-
emit(`removed Codex cache directory for ${id}`);
|
|
1139
|
-
}
|
|
1140
|
-
else {
|
|
1141
|
-
log.skip(`${id} cache directory not present`);
|
|
1142
|
-
emit(`Codex cache directory for ${id} not present`);
|
|
1143
|
-
}
|
|
994
|
+
// Codex path: `codex plugin remove` deletes the plugin from Codex's
|
|
995
|
+
// local config and cache. `id` was validated above, so it carries no
|
|
996
|
+
// shell metacharacter.
|
|
997
|
+
exec(`codex plugin remove ${id}`, { cwd: opts.cwd, inherit: true });
|
|
998
|
+
log.ok(`${id} removed from Codex`);
|
|
999
|
+
emit(`removed ${id} from Codex`);
|
|
1144
1000
|
}
|