auriga-cli 1.29.0 → 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 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 uses `codex plugin marketplace add/upgrade` (the right one is picked by reading `~/.codex/config.toml`) and enables selected plugins in `~/.codex/config.toml`.
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`,并在 `~/.codex/config.toml` 里启用选中的插件。
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
 
@@ -95,8 +95,8 @@ export type ApplyScope = "project" | "user";
95
95
  /**
96
96
  * Workflow AGENTS.md language variant.
97
97
  *
98
- * - "zh-CN": Simplified Chinese AGENTS.md (the default).
99
- * - "en": English AGENTS.en.md.
98
+ * - "zh-CN": Simplified Chinese workflow template (the default).
99
+ * - "en": English workflow template.
100
100
  *
101
101
  * Only meaningful for `category === "workflow"`; rejected for other
102
102
  * categories so the API surface stays explicit.
package/dist/catalog.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-05-16T13:16:49.448Z",
2
+ "generatedAt": "2026-05-19T03:19:33.649Z",
3
3
  "workflowSkills": [
4
4
  {
5
5
  "name": "planning-with-files",
package/dist/cli.js CHANGED
@@ -693,8 +693,9 @@ async function runUi(p, version) {
693
693
  // - tarballRoot: where `dist/catalog.json` + the bundled DEV ui/dist live.
694
694
  // Always read from the installed npm package; can't be fetched because
695
695
  // dist/ is built artifact, not git content.
696
- // - contentRoot: where the runtime install recipes live (AGENTS.md,
697
- // marketplace manifests, extra_plugin_configs.json, skills-lock.json).
696
+ // - contentRoot: where the runtime install recipes live (workflow
697
+ // templates, marketplace manifests, extra_plugin_configs.json,
698
+ // skills-lock.json).
698
699
  // These files are
699
700
  // NOT in the npm tarball — the `files` allowlist only ships `dist/*`
700
701
  // + npm defaults. They are fetched from GitHub, pinned to the CLI
@@ -774,8 +775,9 @@ async function runUi(p, version) {
774
775
  pluginAgentsByName.set(name, def.agents);
775
776
  }
776
777
  const applyHandlers = buildDefaultApplyHandlers({
777
- // contentRoot: install handlers read AGENTS.md, marketplace manifests,
778
- // extra_plugin_configs.json, and skills-lock.json — all CONTENT_FILES.
778
+ // contentRoot: install handlers read workflow templates, marketplace
779
+ // manifests, extra_plugin_configs.json, and skills-lock.json — all
780
+ // CONTENT_FILES.
779
781
  // Routing them at tarballRoot fails ENOENT for npm-installed users.
780
782
  packageRoot: contentRoot,
781
783
  cwd,
@@ -800,8 +802,8 @@ async function runUi(p, version) {
800
802
  cwd,
801
803
  // server reads dist/catalog.json (tarball-shipped) via
802
804
  // buildScanCatalog on each /api/state call; install-time content
803
- // (marketplace manifests, extra plugin config, AGENTS.md, …) was already injected
804
- // into applyHandlers above with contentRoot.
805
+ // (workflow templates, marketplace manifests, extra plugin config, …)
806
+ // was already injected into applyHandlers above with contentRoot.
805
807
  packageRoot: tarballRoot,
806
808
  heartbeatTimeoutMs: UI_HEARTBEAT_TIMEOUT_MS,
807
809
  applyHandlers,
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: no `codex plugin uninstall` exists today (spec §10.4
58
- * flagged this as v0.1 needs-confirm). We mimic the install path
59
- * in reverse:
60
- * 1. Read + parse `~/.codex/config.toml`, delete `[plugins."<id>"]`,
61
- * atomic write back. Throws on parse error (don't half-corrupt).
62
- * 2. rm `~/.codex/plugins/cache/<marketplace>/<plugin>/` directory.
63
- * Both steps are idempotent missing config / missing cache dir is
64
- * a no-op (the user may have manually cleaned half of the install).
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
- * Caveat: we deliberately do NOT remove the marketplace itself. A
67
- * single marketplace may host multiple plugins; tearing it down
68
- * because one plugin left would break others. The user can
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, stringify as stringifyToml } from "smol-toml";
6
- import { codexLocalPluginPath, codexManifestPath, validateCodexMarketplace, } from "./codex-plugin-config.js";
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 `<name>@<marketplace>` config keys + decides whether
710
- // features.plugin_hooks needs to flip on. Local plugins resolve through
711
- // this repo's marketplace.json and require a manifest check + hooks
712
- // inspection; external plugins emit a key directly from extra_plugin_configs.json
713
- // (Codex CLI fetches the upstream manifest itself). External plugins do
714
- // NOT flip plugin_hooks today we don't have access to the upstream
715
- // manifest at install time. Acceptable while no external plugin ships
716
- // hooks; once one does, prefer fetching the manifest or adding an
717
- // explicit `requiresPluginHooks: true` field on the extra config entry.
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
- pluginKeys.push(`${plugin.name}@${localMarketplace.name}`);
724
- if (pluginHasHooks(pluginContentRoot, plugin))
725
- needsPluginHooks = true;
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
- pluginKeys.push(`${p.name}@${p.marketplace.name}`);
620
+ adds.push({
621
+ key: `${p.name}@${p.marketplace.name}`,
622
+ name: p.name,
623
+ hasHooks: false,
624
+ });
730
625
  }
731
- return { pluginKeys, needsPluginHooks };
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
- const localMarketplaceContentRoot = localMarketplace
803
- ? resolveCodexMarketplaceContentRoot(packageRoot, localMarketplace.name)
804
- : packageRoot;
805
- const effectiveLocalMarketplace = localMarketplace
806
- ? loadCodexMarketplace(localMarketplaceContentRoot) ?? localMarketplace
807
- : null;
808
- const selectedMarketplacePlugins = effectiveLocalMarketplace
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(localMarketplaceContentRoot, selectedMarketplacePlugins);
812
- if (effectiveLocalMarketplace) {
813
- materializeLocalCodexPluginCache(localMarketplaceContentRoot, effectiveLocalMarketplace.name, selectedMarketplacePlugins);
814
- }
815
- const { pluginKeys, needsPluginHooks } = await composeCodexPluginKeys(localMarketplaceContentRoot, effectiveLocalMarketplace, selectedMarketplacePlugins, externalSelected);
816
- enableCodexPluginConfig(path.join(codexHome(), "config.toml"), pluginKeys, needsPluginHooks);
817
- for (const plugin of [...localSelected, ...externalSelected]) {
818
- log.ok(`${plugin.name} enabled for Codex`);
819
- runPostInstallMigration(plugin.name, opts, ["codex"]);
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: no `codex plugin uninstall` exists today (spec §10.4
1075
- * flagged this as v0.1 needs-confirm). We mimic the install path
1076
- * in reverse:
1077
- * 1. Read + parse `~/.codex/config.toml`, delete `[plugins."<id>"]`,
1078
- * atomic write back. Throws on parse error (don't half-corrupt).
1079
- * 2. rm `~/.codex/plugins/cache/<marketplace>/<plugin>/` directory.
1080
- * Both steps are idempotent missing config / missing cache dir is
1081
- * a no-op (the user may have manually cleaned half of the install).
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
- * Validation happens before any I/Oa malformed id throws cleanly with
1089
- * no side effects, so retries are safe.
977
+ * `parsePluginId` validates the id shapeand 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
- const { plugin, marketplace } = parsePluginId(id);
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
- const home = codexHome();
1106
- const configPath = path.join(home, "config.toml");
1107
- if (fs.existsSync(configPath)) {
1108
- const content = fs.readFileSync(configPath, "utf-8");
1109
- // Parse-then-mutate: any parse failure aborts BEFORE we touch the
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
  }
package/dist/skills.js CHANGED
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { checkbox, select } from "@inquirer/prompts";
5
5
  import { atomicWriteFile, exec, execAsync, log, withEsc } from "./utils.js";
6
- // Curated default-on set: skills that the workflow in the root AGENTS.md
6
+ // Curated default-on set: skills that the shipped workflow template
7
7
  // directly references. Anything else in skills-lock.json is surfaced via
8
8
  // installRecommendedSkills as an opt-in utility.
9
9
  export const WORKFLOW_SKILLS = [
package/dist/utils.d.ts CHANGED
@@ -93,7 +93,7 @@ export interface LangOption {
93
93
  file: string;
94
94
  }
95
95
  export declare const DEFAULT_WORKFLOW_LANG = "zh-CN";
96
- export declare const DEFAULT_WORKFLOW_TEMPLATE_FILE = "AGENTS.md";
96
+ export declare const DEFAULT_WORKFLOW_TEMPLATE_FILE = "AGENTS.template.zh-CN.md";
97
97
  export declare const LANGUAGES: LangOption[];
98
98
  /**
99
99
  * Reads `version` from the packaged manifest. Throws when the package
package/dist/utils.js CHANGED
@@ -101,10 +101,10 @@ export function execAsync(cmd, opts) {
101
101
  });
102
102
  }
103
103
  export const DEFAULT_WORKFLOW_LANG = "zh-CN";
104
- export const DEFAULT_WORKFLOW_TEMPLATE_FILE = "AGENTS.md";
104
+ export const DEFAULT_WORKFLOW_TEMPLATE_FILE = "AGENTS.template.zh-CN.md";
105
105
  export const LANGUAGES = [
106
- { value: "zh-CN", label: "中文", file: "AGENTS.md" },
107
- { value: "en", label: "English", file: "AGENTS.en.md" },
106
+ { value: "zh-CN", label: "中文", file: "AGENTS.template.zh-CN.md" },
107
+ { value: "en", label: "English", file: "AGENTS.template.en.md" },
108
108
  ];
109
109
  // --- Remote content ---
110
110
  const REPO = "Ben2pc/auriga-cli";
@@ -149,12 +149,16 @@ function resolveContentRef() {
149
149
  }
150
150
  const CONTENT_FILES = [
151
151
  DEFAULT_WORKFLOW_TEMPLATE_FILE,
152
- "AGENTS.en.md",
152
+ "AGENTS.template.en.md",
153
153
  "skills-lock.json",
154
154
  ".claude-plugin/marketplace.json",
155
155
  ".agents/plugins/marketplace.json",
156
156
  "extra_plugin_configs.json",
157
157
  ];
158
+ const LEGACY_CONTENT_FILE_FALLBACKS = {
159
+ "AGENTS.template.zh-CN.md": "AGENTS.md",
160
+ "AGENTS.template.en.md": "AGENTS.en.md",
161
+ };
158
162
  async function fetchFile(file) {
159
163
  const ref = resolveContentRef();
160
164
  const url = `https://raw.githubusercontent.com/${REPO}/${ref}/${file}`;
@@ -163,6 +167,18 @@ async function fetchFile(file) {
163
167
  throw new Error(`Failed to fetch ${url}: ${res.status}`);
164
168
  return res.text();
165
169
  }
170
+ async function fetchContentFile(file) {
171
+ try {
172
+ return await fetchFile(file);
173
+ }
174
+ catch (err) {
175
+ const legacyFile = LEGACY_CONTENT_FILE_FALLBACKS[file];
176
+ if (!legacyFile || !(err instanceof Error) || !/: 404$/.test(err.message)) {
177
+ throw err;
178
+ }
179
+ return fetchFile(legacyFile);
180
+ }
181
+ }
166
182
  async function fetchFileBinary(file) {
167
183
  const ref = resolveContentRef();
168
184
  const url = `https://raw.githubusercontent.com/${REPO}/${ref}/${file}`;
@@ -177,7 +193,7 @@ export async function fetchContentRoot() {
177
193
  }
178
194
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "auriga-cli-"));
179
195
  for (const file of CONTENT_FILES) {
180
- const content = await fetchFile(file);
196
+ const content = await fetchContentFile(file);
181
197
  const dest = path.join(tmpDir, file);
182
198
  fs.mkdirSync(path.dirname(dest), { recursive: true });
183
199
  fs.writeFileSync(dest, content);
@@ -21,9 +21,9 @@ export const MARKER_SCHEMA = "v1";
21
21
  * START marker line, one per template language. Only the prose differs — the
22
22
  * structural `AURIGA:WORKFLOW:v1 START` token is language-independent, so the
23
23
  * parser (`START_LINE_RE`) keys on the token alone and never needs to know the
24
- * language. The English `AGENTS.en.md` gets the English marker; `AGENTS.md`
25
- * gets the Chinese one, so a downstream file never carries a comment in the
26
- * wrong language for its document.
24
+ * language. `AGENTS.template.en.md` gets the English marker;
25
+ * `AGENTS.template.zh-CN.md` gets the Chinese one, so a downstream file never
26
+ * carries a comment in the wrong language for its document.
27
27
  */
28
28
  const WORKFLOW_START_MARKERS = {
29
29
  en: `<!-- AURIGA:WORKFLOW:${MARKER_SCHEMA} START — Managed block, maintained by auriga-cli. Do not edit by hand; upgrades replace it wholesale. Put project-specific instructions after the END marker below. -->`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auriga-cli",
3
- "version": "1.29.0",
3
+ "version": "1.29.2",
4
4
  "description": "Interactive CLI to install Claude Code harness modules (Workflow, Skills, Recommended Skills, Plugins)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "dev": "tsc --watch",
26
26
  "start": "node dist/cli.js",
27
27
  "pretest": "npm run build",
28
- "test": "tsc -p tsconfig.test.json && DEV=1 node --test --experimental-test-module-mocks dist-test/tests/skills.test.js dist-test/tests/skills-uninstall.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/preset.test.js dist-test/tests/legacy-menu.test.js dist-test/tests/plugins.test.js dist-test/tests/plugins-uninstall.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js dist-test/tests/state.test.js dist-test/tests/server.test.js dist-test/tests/server-auth.test.js dist-test/tests/server-apply.test.js dist-test/tests/apply-handlers.test.js dist-test/tests/ui-fetch.test.js dist-test/tests/workflow-markers.test.js dist-test/tests/workflow-install.test.js dist-test/tests/workflow-uninstall.test.js dist-test/tests/tarball-shape.test.js dist-test/tests/spec-design.test.js dist-test/tests/goalify.test.js dist-test/tests/plugin-skill-frontmatter.test.js",
29
- "test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch --experimental-test-module-mocks dist-test/tests/skills.test.js dist-test/tests/skills-uninstall.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/preset.test.js dist-test/tests/legacy-menu.test.js dist-test/tests/plugins.test.js dist-test/tests/plugins-uninstall.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js dist-test/tests/state.test.js dist-test/tests/server.test.js dist-test/tests/server-auth.test.js dist-test/tests/server-apply.test.js dist-test/tests/apply-handlers.test.js dist-test/tests/ui-fetch.test.js dist-test/tests/workflow-markers.test.js dist-test/tests/workflow-install.test.js dist-test/tests/workflow-uninstall.test.js dist-test/tests/tarball-shape.test.js dist-test/tests/spec-design.test.js dist-test/tests/goalify.test.js dist-test/tests/plugin-skill-frontmatter.test.js",
28
+ "test": "tsc -p tsconfig.test.json && DEV=1 node --test --experimental-test-module-mocks dist-test/tests/skills.test.js dist-test/tests/skills-uninstall.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/preset.test.js dist-test/tests/legacy-menu.test.js dist-test/tests/plugins.test.js dist-test/tests/plugins-uninstall.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js dist-test/tests/state.test.js dist-test/tests/server.test.js dist-test/tests/server-auth.test.js dist-test/tests/server-apply.test.js dist-test/tests/apply-handlers.test.js dist-test/tests/ui-fetch.test.js dist-test/tests/workflow-markers.test.js dist-test/tests/workflow-install.test.js dist-test/tests/workflow-uninstall.test.js dist-test/tests/tarball-shape.test.js dist-test/tests/spec-design.test.js dist-test/tests/goalify.test.js dist-test/tests/plugin-skill-frontmatter.test.js dist-test/tests/auriga-workflow-skills.test.js",
29
+ "test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch --experimental-test-module-mocks dist-test/tests/skills.test.js dist-test/tests/skills-uninstall.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/preset.test.js dist-test/tests/legacy-menu.test.js dist-test/tests/plugins.test.js dist-test/tests/plugins-uninstall.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js dist-test/tests/state.test.js dist-test/tests/server.test.js dist-test/tests/server-auth.test.js dist-test/tests/server-apply.test.js dist-test/tests/apply-handlers.test.js dist-test/tests/ui-fetch.test.js dist-test/tests/workflow-markers.test.js dist-test/tests/workflow-install.test.js dist-test/tests/workflow-uninstall.test.js dist-test/tests/tarball-shape.test.js dist-test/tests/spec-design.test.js dist-test/tests/goalify.test.js dist-test/tests/plugin-skill-frontmatter.test.js dist-test/tests/auriga-workflow-skills.test.js",
30
30
  "pretest:e2e": "npm run build",
31
31
  "test:e2e": "tsc -p tsconfig.test.json && node --test dist-test/tests/e2e-install.test.js",
32
32
  "pretest:web-ui-e2e": "npm run build && npm --prefix ui ci && npm --prefix ui run build",