auriga-cli 1.29.1 → 1.30.0

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
@@ -113,7 +113,6 @@ Installs selected skills via `npx skills add`, targeting both Claude Code and Co
113
113
  | codex-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | Delegate to Codex sessions for cross-model coverage |
114
114
  | deprecation-and-migration | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | Sunset, replace, or migrate legacy code — deprecation discipline |
115
115
  | design-taste-frontend | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) | Senior UI/UX engineer with metric-based design rules and strict component architecture |
116
- | documentation-and-adrs | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | Record architectural decisions and the *why* — context for future engineers / agents |
117
116
  | frontend-design | [anthropics/skills](https://github.com/anthropics/skills) | Distinctive, production-grade frontend UI generation that avoids generic AI aesthetics |
118
117
  | make-interfaces-feel-better | [jakubkrehel/make-interfaces-feel-better](https://github.com/jakubkrehel/make-interfaces-feel-better) | Polish principles — animations, surfaces, typography, performance |
119
118
 
@@ -121,7 +120,7 @@ Supports both project and global installation scopes.
121
120
 
122
121
  ### Plugins
123
122
 
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`.
123
+ 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
124
 
126
125
  Examples:
127
126
 
@@ -137,7 +136,7 @@ npx -y auriga-cli install plugins --agent codex --plugin session-instructions-lo
137
136
  | claude-md-management | Claude Code / Codex | Audit and improve AGENTS.md / CLAUDE.md |
138
137
  | playground | Claude Code / Codex | Build interactive HTML playgrounds |
139
138
  | codex | Claude Code | Codex cross-model collaboration |
140
- | auriga-workflow | Claude Code / Codex | The auriga workflow plugin — workflow skills plus the git lifecycle hooks that enforce them. Skills: `incremental-impl`, `test-designer`, `spec-design`, `arch-design`, `code-simplify`, `session-compound`, `goalify` (plans an autonomous goal and dispatches it via the built-in `/goal` command), `deep-review` (multi-dimensional PR review orchestrator — parallel per-dimension reviewers synthesized into an actionable punch list), `reviewer-creator` (scaffolds project-level custom reviewers under `docs/rules/review/`), and `git-workflow` (git lifecycle skill). Hooks: `commit-reminder` (PostToolUse on file edits — `Edit` / `Write` / `MultiEdit` in Claude Code, `apply_patch` in Codex — nudges to commit at the next semantic boundary when uncommitted diff vs `HEAD` exceeds 200 lines or 8 files), `pr-create-guard` (PostToolUse on `gh pr create` → injects a PR-body snapshot for five-element self-verification and flags non-Conventional-Commits titles), `pr-ready-guard` (PreToolUse on `gh pr ready` and non-draft `gh pr create` → blocks on stray planning docs, unfinalized active specs under `docs/specs/`, or unpushed commits), and `pr-merge-guard` (PreToolUse on `gh pr merge` → blocks while the PR body's Acceptance criteria section still has unchecked checklist items). The two PostToolUse hooks reach full Claude Code / Codex parity; Codex currently fails open on `pr-ready-guard`'s PreToolUse `additionalContext` informational path (block path identical). Installed by default through the plugin path. |
139
+ | auriga-workflow | Claude Code / Codex | The auriga workflow plugin — workflow skills plus the git lifecycle hooks that enforce them. Skills: `incremental-impl`, `test-designer`, `spec-design`, `arch-design`, `code-simplify`, `session-compound`, `goalify` (plans an autonomous goal and dispatches it via the built-in `/goal` command), `deep-review` (multi-dimensional PR review orchestrator — parallel per-dimension reviewers synthesized into an actionable punch list), `reviewer-creator` (scaffolds project-level custom reviewers under `docs/rules/review/`), `git-workflow` (git lifecycle skill), and `documentation-and-adrs` (architecture decision records and project documentation discipline — ADRs archived under `docs/architecture/`). Hooks: `commit-reminder` (PostToolUse on file edits — `Edit` / `Write` / `MultiEdit` in Claude Code, `apply_patch` in Codex — nudges to commit at the next semantic boundary when uncommitted diff vs `HEAD` exceeds 200 lines or 8 files), `pr-create-guard` (PostToolUse on `gh pr create` → injects a PR-body snapshot for five-element self-verification and flags non-Conventional-Commits titles), `pr-ready-guard` (PreToolUse on `gh pr ready` and non-draft `gh pr create` → blocks on stray planning docs, unfinalized active specs under `docs/specs/`, or unpushed commits), and `pr-merge-guard` (PreToolUse on `gh pr merge` → blocks while the PR body's Acceptance criteria section still has unchecked checklist items). The two PostToolUse hooks reach full Claude Code / Codex parity; Codex currently fails open on `pr-ready-guard`'s PreToolUse `additionalContext` informational path (block path identical). Installed by default through the plugin path. |
141
140
  | auriga-notify *(opt-in)* | Claude Code | macOS native notification plugin for Claude Code `Notification` events. Focus-aware sound-only mode, click-to-activate, per-project notification grouping, and migrated `config.json` / `icon.png` support. Not installed by `install --all`; install explicitly with `install plugins --plugin auriga-notify`. |
142
141
  | session-instructions-loader | Codex | Codex-only SessionStart plugin that injects ancestor `AGENTS.md` files plus repo-configured extra instruction files. |
143
142
 
package/README.zh-CN.md CHANGED
@@ -113,7 +113,6 @@ npx auriga-cli
113
113
  | codex-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | 委派给 Codex 会话,做跨模型覆盖 |
114
114
  | deprecation-and-migration | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | 废弃与迁移流程 —— 安全地下线、替换或迁移遗留代码 |
115
115
  | design-taste-frontend | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) | 高阶 UI/UX 工程师 —— 度量化设计规则与严格的组件架构约束 |
116
- | documentation-and-adrs | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | 记录架构决策与"为什么" —— 为后来的工程师和 Agent 沉淀上下文 |
117
116
  | frontend-design | [anthropics/skills](https://github.com/anthropics/skills) | 生成有辨识度、production 级的前端界面,避开常见 AI 同质化美学 |
118
117
  | make-interfaces-feel-better | [jakubkrehel/make-interfaces-feel-better](https://github.com/jakubkrehel/make-interfaces-feel-better) | 界面打磨原则 —— 动画、表面、排版、性能 |
119
118
 
@@ -121,7 +120,7 @@ npx auriga-cli
121
120
 
122
121
  ### Plugins
123
122
 
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` 里启用选中的插件。
123
+ 可以把选中的插件安装到 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
124
 
126
125
  示例:
127
126
 
@@ -137,7 +136,7 @@ npx -y auriga-cli install plugins --agent codex --plugin session-instructions-lo
137
136
  | claude-md-management | Claude Code / Codex | 审计和改进 AGENTS.md / CLAUDE.md |
138
137
  | playground | Claude Code / Codex | 构建交互式 HTML playground |
139
138
  | codex | Claude Code | Codex 跨模型协作 |
140
- | auriga-workflow | Claude Code / Codex | auriga 工作流插件 —— 工作流 skill 加上强制执行工作流的 git 生命周期 hook。Skills:`incremental-impl`、`test-designer`、`spec-design`、`arch-design`、`code-simplify`、`session-compound`、`goalify`(plan 出自驱 goal 并通过内置 `/goal` 命令分发执行)、`deep-review`(多维度 PR review 编排器——并行派发各维度 reviewer,汇总成可执行的 punch list)、`reviewer-creator`(在 `docs/rules/review/` 下生成项目级自定义 reviewer)、`git-workflow`(git 生命周期 skill)。Hooks:`commit-reminder`(文件编辑的 PostToolUse —— Claude Code 匹配 `Edit` / `Write` / `MultiEdit`,Codex 匹配 `apply_patch` —— 未提交 diff 对比 `HEAD` 超过 200 行或 8 个文件时,提醒在下一个语义边界 commit)、`pr-create-guard`(`gh pr create` 的 PostToolUse —— 注入 PR body 快照供五要素自检,并对不符合 Conventional Commits 的标题提示)、`pr-ready-guard`(`gh pr ready` 与非 draft `gh pr create` 的 PreToolUse —— 拦截游离规划文档、`docs/specs/` 内未结案的活跃 spec、未 push commits)、`pr-merge-guard`(`gh pr merge` 的 PreToolUse —— PR body 的验收标准章节仍有未勾选清单项时拦截合并)。两个 PostToolUse hook 在 Claude Code / Codex 上完全对齐;Codex 仅对 `pr-ready-guard` 的 PreToolUse `additionalContext` 信息路径 fail-open(block 路径两边一致)。默认通过插件路径安装。 |
139
+ | auriga-workflow | Claude Code / Codex | auriga 工作流插件 —— 工作流 skill 加上强制执行工作流的 git 生命周期 hook。Skills:`incremental-impl`、`test-designer`、`spec-design`、`arch-design`、`code-simplify`、`session-compound`、`goalify`(plan 出自驱 goal 并通过内置 `/goal` 命令分发执行)、`deep-review`(多维度 PR review 编排器——并行派发各维度 reviewer,汇总成可执行的 punch list)、`reviewer-creator`(在 `docs/rules/review/` 下生成项目级自定义 reviewer)、`git-workflow`(git 生命周期 skill)、`documentation-and-adrs`(架构决策记录与项目文档规范,ADR 归档到 `docs/architecture/`)。Hooks:`commit-reminder`(文件编辑的 PostToolUse —— Claude Code 匹配 `Edit` / `Write` / `MultiEdit`,Codex 匹配 `apply_patch` —— 未提交 diff 对比 `HEAD` 超过 200 行或 8 个文件时,提醒在下一个语义边界 commit)、`pr-create-guard`(`gh pr create` 的 PostToolUse —— 注入 PR body 快照供五要素自检,并对不符合 Conventional Commits 的标题提示)、`pr-ready-guard`(`gh pr ready` 与非 draft `gh pr create` 的 PreToolUse —— 拦截游离规划文档、`docs/specs/` 内未结案的活跃 spec、未 push commits)、`pr-merge-guard`(`gh pr merge` 的 PreToolUse —— PR body 的验收标准章节仍有未勾选清单项时拦截合并)。两个 PostToolUse hook 在 Claude Code / Codex 上完全对齐;Codex 仅对 `pr-ready-guard` 的 PreToolUse `additionalContext` 信息路径 fail-open(block 路径两边一致)。默认通过插件路径安装。 |
141
140
  | auriga-notify *(opt-in)* | Claude Code | Claude Code `Notification` 事件的 macOS 原生通知插件。支持焦点感知仅提示音、点击唤起终端、按项目分组通知,并迁移旧 `config.json` / `icon.png`。不随 `install --all` 默认安装,需要显式执行 `install plugins --plugin auriga-notify`。 |
142
141
  | session-instructions-loader | Codex | Codex-only SessionStart 插件,注入上层目录的 `AGENTS.md` 和仓库配置的额外 instruction 文件。 |
143
142
 
package/dist/catalog.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-05-17T06:16:55.450Z",
2
+ "generatedAt": "2026-05-19T07:23:20.929Z",
3
3
  "workflowSkills": [
4
4
  {
5
5
  "name": "planning-with-files",
@@ -39,10 +39,6 @@
39
39
  "name": "design-taste-frontend",
40
40
  "description": "Senior UI/UX Engineer. Architect digital interfaces overriding default LLM biases. Enforces metric-based rules, strict component architecture, CSS hardware acceleration, and balanced design engineering."
41
41
  },
42
- {
43
- "name": "documentation-and-adrs",
44
- "description": "Records decisions and documentation. Use when making architectural decisions, changing public APIs, shipping features, or when you need to record context that future engineers and agents will need to understand the codebase."
45
- },
46
42
  {
47
43
  "name": "frontend-design",
48
44
  "description": "Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics."
@@ -55,7 +51,7 @@
55
51
  "plugins": [
56
52
  {
57
53
  "name": "auriga-workflow",
58
- "description": "(Claude/Codex) The auriga workflow plugin: workflow skills — incremental-impl (implementation slicing), test-designer (independent test design), spec-design (requirement clarification), arch-design (architecture design), code-simplify (code simplification), session-compound (session compounding), goalify (autonomous /goal planning), deep-review (multi-dimensional PR review orchestrator), reviewer-creator (custom reviewer scaffolding), git-workflow (git lifecycle skill) — plus the git lifecycle hooks that enforce them: commit-reminder, pr-create-guard, pr-ready-guard, pr-merge-guard. Dual-Agent compatible (Claude Code + Codex).",
54
+ "description": "(Claude/Codex) The auriga workflow plugin: workflow skills — incremental-impl (implementation slicing), test-designer (independent test design), spec-design (requirement clarification), arch-design (architecture design), code-simplify (code simplification), session-compound (session compounding), goalify (autonomous /goal planning), deep-review (multi-dimensional PR review orchestrator), reviewer-creator (custom reviewer scaffolding), git-workflow (git lifecycle skill), documentation-and-adrs (architecture decision records and documentation discipline) — plus the git lifecycle hooks that enforce them: commit-reminder, pr-create-guard, pr-ready-guard, pr-merge-guard. Dual-Agent compatible (Claude Code + Codex).",
59
55
  "agents": [
60
56
  "claude",
61
57
  "codex"
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auriga-cli",
3
- "version": "1.29.1",
3
+ "version": "1.30.0",
4
4
  "description": "Interactive CLI to install Claude Code harness modules (Workflow, Skills, Recommended Skills, Plugins)",
5
5
  "license": "MIT",
6
6
  "repository": {