auriga-cli 1.9.4 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,11 +13,19 @@ This repo itself is a fully configured harness project. You can clone it to see
13
13
  | **Workflow** | `CLAUDE.md` auriga workflow: requirement clarification -> TDD -> Review, Harness principles, Subagent usage guide |
14
14
  | **Skills** | Development process + orchestration skills — brainstorming, systematic-debugging, TDD, verification, planning, playwright, deep-review, test-designer, parallel-implementation |
15
15
  | **Recommended Skills** | Optional utility skills (e.g. `codex-agent`, `claude-code-agent`) you can add on top of the workflow skills |
16
- | **Plugins** | Recommended Claude Code plugins — skill-creator, claude-md-management, codex, auriga-go |
17
- | **Hooks** | Claude Code hooks: `notify` (macOS notification, focus-aware sound-only when terminal is frontmost — **opt-in**: not installed by `install --all`, requires `install hooks --hook notify`), `pr-create-guard` (PostToolUse body snapshot after `gh pr create`), `pr-ready-guard` (PreToolUse block on stray planning docs / active specs in `docs/specs/` / unpushed commits before `gh pr ready`) |
16
+ | **Plugins** | Recommended Claude Code and Codex plugins — skill-creator, claude-md-management, codex, auriga-go, auriga-pr-guards, session-instructions-loader |
17
+ | **Hooks** | Claude Code hooks: `notify` (macOS notification, focus-aware sound-only when terminal is frontmost — **opt-in**: not installed by `install --all`, requires `install hooks --hook notify`) |
18
18
 
19
19
  ## Quick Start
20
20
 
21
+ ### Ask your Agent to install
22
+
23
+ The easiest path is to let your current Agent read the install guide and follow it:
24
+
25
+ > Run `npx -y auriga-cli guide`, read the guide, then install the Auriga harness into this repository by following the steps it prints.
26
+
27
+ The guide command is intentionally non-interactive. It gives the Agent the prerequisite checks, catalog inspection commands, install commands, reload step, and verification checklist in one place.
28
+
21
29
  ### Agent Bootstrap (non-TTY)
22
30
 
23
31
  Running inside `claude -p`, `claude -p --worktree`, or any non-interactive Agent session? Start here:
@@ -35,11 +43,12 @@ Non-interactive install commands:
35
43
  ```bash
36
44
  npx -y auriga-cli install --all # workflow + skills + plugins + hooks (atomic)
37
45
  npx -y auriga-cli install recommended # opt-in utility skills (not in --all)
46
+ npx -y auriga-cli install plugins --agent codex --plugin session-instructions-loader
38
47
  npx -y auriga-cli install <type> [--flags] # one of: workflow | skills | recommended | plugins | hooks
39
48
  npx -y auriga-cli --help # full catalog + flags
40
49
  ```
41
50
 
42
- Exit codes: `0` success, `1` fatal (precheck / parse / fetch), `2` partial success — `stderr` lists per-category `[OK]/[FAIL]` and a `Retry:` hint. After install, reload the Claude Code session so the new `CLAUDE.md` / skills / plugins / hook registrations are picked up.
51
+ Exit codes: `0` success, `1` fatal (precheck / parse / fetch), `2` partial success — `stderr` lists per-category `[OK]/[FAIL]` and a `Retry:` hint. After install, reload the Claude Code or Codex session so the new `CLAUDE.md` / skills / plugins / hook registrations are picked up.
43
52
 
44
53
  ### Interactive menu
45
54
 
@@ -54,11 +63,11 @@ Interactive menu — select what to install:
54
63
  ◉ Workflow — CLAUDE.md + AGENTS.md
55
64
  ◉ Skills — Development process skills
56
65
  ◉ Recommended Skills — Extra utility skills
57
- ◉ Plugins — Claude Code plugins
66
+ ◉ Plugins — Claude Code / Codex plugins
58
67
  ◉ Hooks — Claude Code hooks
59
68
  ```
60
69
 
61
- Each module supports scope selection (Skills: project/global, Plugins: user/project, Hooks: project local / project / user).
70
+ Each module supports scope selection where applicable (Skills: project/global, Claude Code Plugins: user/project, Hooks: project local / project / user). Plugin installation also asks which runtime to target: Claude Code, Codex, or both.
62
71
 
63
72
  ## Module Details
64
73
 
@@ -99,14 +108,24 @@ Supports both project and global installation scopes.
99
108
 
100
109
  ### Plugins
101
110
 
102
- Installs selected plugins via `claude plugins install`, automatically adding required marketplaces.
111
+ 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` and enables selected plugins in `~/.codex/config.toml`.
103
112
 
104
- | Plugin | Description |
105
- |---|---|
106
- | skill-creator | Create and manage custom skills |
107
- | claude-md-management | Audit and improve CLAUDE.md |
108
- | codex | Codex cross-model collaboration |
109
- | auriga-go | Workflow autopilot for the auriga workflow. Reminder-based navigation across the `CLAUDE.md` phases with an Experimental hook-backed `ship` mode. Bundles a skill (description-based NL trigger + `/auriga-go`) plus a plugin-level Stop hook for ship mode. |
113
+ Examples:
114
+
115
+ ```bash
116
+ npx -y auriga-cli install plugins --plugin auriga-go
117
+ npx -y auriga-cli install plugins --agent codex --plugin session-instructions-loader
118
+ npx -y auriga-cli install plugins --agent both --plugin auriga-pr-guards
119
+ ```
120
+
121
+ | Plugin | Runtime | Description |
122
+ |---|---|---|
123
+ | skill-creator | Claude Code | Create and manage custom skills |
124
+ | claude-md-management | Claude Code | Audit and improve CLAUDE.md |
125
+ | codex | Claude Code | Codex cross-model collaboration |
126
+ | auriga-go | Claude Code / Codex | Workflow autopilot for the auriga workflow. Reminder-based navigation across the `CLAUDE.md` phases with an Experimental hook-backed `ship` mode. Bundles a skill (description-based NL trigger + `/auriga-go`) plus a plugin-level Stop hook for ship mode. |
127
+ | auriga-pr-guards | Claude Code / Codex | Two PR-workflow guardrails packaged as a dual-Agent plugin: `pr-create-guard` (PostToolUse for `gh pr create` → fetch the new PR's body via `gh pr view` and inject headings + TODO counts as `additionalContext` so the Agent can self-verify scope / acceptance / risks / TODO) and `pr-ready-guard` (PreToolUse for `gh pr ready` → block on stray planning docs at `findings.md` / `progress.md` / `task_plan.md` / `docs/superpowers/specs/*.md`, unfinalized active specs in `docs/specs/*.md`, or unpushed commits; otherwise inject the body snapshot). Codex currently fails open on the `additionalContext` field for PreToolUse but enforces blocks identically. |
128
+ | session-instructions-loader | Codex | Codex-only SessionStart plugin that injects ancestor `AGENTS.md` files plus repo-configured extra instruction files. |
110
129
 
111
130
  ### Hooks
112
131
 
@@ -115,8 +134,6 @@ Installs Claude Code hooks into a chosen scope. Each hook is self-contained unde
115
134
  | Hook | Description |
116
135
  |---|---|
117
136
  | notify *(opt-in)* | Native macOS notification when Claude needs your attention. Shows the brand mark in the small app-icon position; click brings the originating terminal back to focus. **Focus-aware**: when the launching terminal is already frontmost, drops the banner and plays the sound only (toggle via `soundOnlyWhenFocused` in `config.json`). **Per-project group ID**: new notifications cleanly replace older ones in Notification Center, no process accumulation, no cross-project interference. Auto-installs `alerter` via Homebrew (`vjeantet/tap/alerter`). Customize sound and icon by editing `.claude/hooks/notify/config.json` and `.claude/hooks/notify/icon.png`. macOS-only at runtime; silent no-op on other platforms. |
118
- | pr-create-guard | PostToolUse hook for `gh pr create`. Queries the newly-created PR via `gh pr view` and injects a body snapshot (headings found + TODO-checkbox counts) as `additionalContext` for the Agent to self-verify against the PR-readiness scope / acceptance / risks / TODO contract. Never blocks — PostToolUse runs after the fact. Graceful degradation when gh is unavailable. |
119
- | pr-ready-guard | PreToolUse hook for `gh pr ready`. Blocks on structural signals only: (1) stray planning docs at `findings.md` / `progress.md` / `task_plan.md` / `docs/superpowers/specs/*.md` — must be archived to `docs/worklog/worklog-<date>-<branch>/` (or deleted) per CLAUDE.md `Document Conventions`; (2) unfinalized active specs at `docs/specs/*.md` — must be promoted to `docs/architecture/`, archived, or deleted; (3) unpushed commits on the current branch. No text regex of PR content is ever used as a block signal. On pass, injects a PR body snapshot as `additionalContext`. |
120
137
 
121
138
  Scope choices:
122
139
 
@@ -129,7 +146,8 @@ Re-running the installer preserves your customized `config.json` and `icon.png`,
129
146
  ## Requirements
130
147
 
131
148
  - Node.js >= 18
132
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (required for Plugins and Hooks modules)
149
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (required for Claude Code Plugins and Hooks modules)
150
+ - Codex CLI (required only for `install plugins --agent codex|both`)
133
151
  - [Homebrew](https://brew.sh) (recommended for the `notify` hook to install `alerter`)
134
152
 
135
153
  ## Development
package/README.zh-CN.md CHANGED
@@ -13,11 +13,19 @@
13
13
  | **Workflow** | `CLAUDE.md` 里的 auriga 工作流:需求澄清 → TDD → Review,Harness 原则,Subagent 使用指南 |
14
14
  | **Skills** | 开发流程 + 编排类 skills —— brainstorming、systematic-debugging、TDD、verification、planning、playwright、deep-review、test-designer、parallel-implementation |
15
15
  | **Recommended Skills** | 可选的工具类 skills(如 `codex-agent`、`claude-code-agent`),在 workflow skills 之外按需追加 |
16
- | **Plugins** | 推荐的 Claude Code 插件 —— skill-creator、claude-md-management、codex、auriga-go |
17
- | **Hooks** | Claude Code hooks:`notify`(macOS 通知,终端在焦点时仅放声不弹横幅 —— **opt-in**:`install --all` 不装,需要 `install hooks --hook notify`)、`pr-create-guard`(`gh pr create` 后注入 PR body 快照的 PostToolUse)、`pr-ready-guard`(`gh pr ready` 前按游离 planning 文档 / `docs/specs/` 内未清理的 spec / 未 push commits 拦截的 PreToolUse) |
16
+ | **Plugins** | 推荐的 Claude Code 和 Codex 插件 —— skill-creator、claude-md-management、codex、auriga-go、auriga-pr-guards、session-instructions-loader |
17
+ | **Hooks** | Claude Code hooks:`notify`(macOS 通知,终端在焦点时仅放声不弹横幅 —— **opt-in**:`install --all` 不装,需要 `install hooks --hook notify`) |
18
18
 
19
19
  ## 快速开始
20
20
 
21
+ ### 让你的 Agent 负责安装
22
+
23
+ 最简单的方式是让当前 Agent 先读取安装指南,再按指南执行:
24
+
25
+ > 运行 `npx -y auriga-cli guide`,阅读指南,然后按输出步骤把 Auriga harness 安装到当前仓库。
26
+
27
+ `guide` 命令是非交互式的。它会把前置检查、catalog 查看命令、安装命令、重启会话步骤和验证清单一次性提供给 Agent。
28
+
21
29
  ### Agent Bootstrap(非交互)
22
30
 
23
31
  在 `claude -p`、`claude -p --worktree` 或任何非交互 Agent 会话里想装整套 harness?从这里开始:
@@ -35,11 +43,12 @@ npx -y auriga-cli guide
35
43
  ```bash
36
44
  npx -y auriga-cli install --all # workflow + skills + plugins + hooks(原子)
37
45
  npx -y auriga-cli install recommended # 可选工具 skills(不在 --all 内)
46
+ npx -y auriga-cli install plugins --agent codex --plugin session-instructions-loader
38
47
  npx -y auriga-cli install <type> [--flags] # 单类:workflow | skills | recommended | plugins | hooks
39
48
  npx -y auriga-cli --help # 完整 catalog + flag 说明
40
49
  ```
41
50
 
42
- 退出码:`0` 成功;`1` 致命错误(前置检查 / 解析 / 拉取失败);`2` 部分成功——`stderr` 会列出逐类 `[OK]/[FAIL]` 和 `Retry:` 提示。装完后请重启 Claude Code session,让新的 `CLAUDE.md` / skills / plugins / hook 注册 生效。
51
+ 退出码:`0` 成功;`1` 致命错误(前置检查 / 解析 / 拉取失败);`2` 部分成功——`stderr` 会列出逐类 `[OK]/[FAIL]` 和 `Retry:` 提示。装完后请重启 Claude Code Codex 会话,让新的 `CLAUDE.md` / skills / plugins / hook 注册生效。
43
52
 
44
53
  ### 交互式菜单
45
54
 
@@ -54,11 +63,11 @@ npx auriga-cli
54
63
  ◉ Workflow — CLAUDE.md + AGENTS.md
55
64
  ◉ Skills — 开发流程 skills
56
65
  ◉ Recommended Skills — 额外的工具 skills
57
- ◉ Plugins — Claude Code 插件
66
+ ◉ Plugins — Claude Code / Codex 插件
58
67
  ◉ Hooks — Claude Code hooks
59
68
  ```
60
69
 
61
- 每个模块支持作用域选择(Skills: project/global,Plugins: user/project,Hooks: project local / project / user)。
70
+ 每个模块在适用时支持作用域选择(Skills: project/global,Claude Code Plugins: user/project,Hooks: project local / project / user)。安装插件时还会先选择目标运行时:Claude Code、Codex 或两者都装。
62
71
 
63
72
  ## 模块详情
64
73
 
@@ -99,14 +108,24 @@ npx auriga-cli
99
108
 
100
109
  ### Plugins
101
110
 
102
- 通过 `claude plugins install` 安装选中的插件,自动添加所需的 marketplace
111
+ 可以把选中的插件安装到 Claude Code、Codex 或两者都装。Claude Code 路径使用 `claude plugins install`,并遵守 `--scope project|user`;Codex 路径使用 `codex plugin marketplace add`,并在 `~/.codex/config.toml` 里启用选中的插件。
103
112
 
104
- | 插件 | 说明 |
105
- |---|---|
106
- | skill-creator | 创建和管理自定义 skills |
107
- | claude-md-management | 审计和改进 CLAUDE.md |
108
- | codex | Codex 跨模型协作 |
109
- | auriga-go | auriga 工作流的自动驾驶:按 `CLAUDE.md` 的 phase 做 reminder-based 导航;包含 Experimental hook-backed `ship` 模式。内置一个 skill(按 description 的自然语言触发 + `/auriga-go` slash command)和一个 plugin 层面的 Stop hook。 |
113
+ 示例:
114
+
115
+ ```bash
116
+ npx -y auriga-cli install plugins --plugin auriga-go
117
+ npx -y auriga-cli install plugins --agent codex --plugin session-instructions-loader
118
+ npx -y auriga-cli install plugins --agent both --plugin auriga-pr-guards
119
+ ```
120
+
121
+ | 插件 | 运行时 | 说明 |
122
+ |---|---|---|
123
+ | skill-creator | Claude Code | 创建和管理自定义 skills |
124
+ | claude-md-management | Claude Code | 审计和改进 CLAUDE.md |
125
+ | codex | Claude Code | Codex 跨模型协作 |
126
+ | auriga-go | Claude Code / Codex | auriga 工作流的自动驾驶:按 `CLAUDE.md` 的 phase 做 reminder-based 导航;包含 Experimental 的 hook-backed `ship` 模式。内置一个 skill(按 description 的自然语言触发 + `/auriga-go` slash command)和一个 plugin 层面的 Stop hook。 |
127
+ | auriga-pr-guards | Claude Code / Codex | 两个 PR-workflow guardrail:`pr-create-guard`(`gh pr create` 的 PostToolUse —— 通过 `gh pr view` 拉真实 PR body,扫 `^##` / `^###` headings 并统计 `- [ ]` / `- [x]` 注入 `additionalContext`,让 Agent 对照范围 / 验收 / 风险 / 剩余 TODO 四要素)+ `pr-ready-guard`(`gh pr ready` 的 PreToolUse —— 仅按结构信号拦截:游离 `findings.md` / `progress.md` / `task_plan.md` / `docs/superpowers/specs/*.md`、`docs/specs/*.md` 内未结案的活跃 spec、未 push commits;放行时注入 body 快照)。Codex 当前对 PreToolUse 的 `additionalContext` 字段 fail-open(解析但不生效),block 路径两边一致。 |
128
+ | session-instructions-loader | Codex | Codex-only SessionStart 插件,注入上层目录的 `AGENTS.md` 和仓库配置的额外 instruction 文件。 |
110
129
 
111
130
  ### Hooks
112
131
 
@@ -115,8 +134,6 @@ npx auriga-cli
115
134
  | Hook | 说明 |
116
135
  |---|---|
117
136
  | notify *(opt-in)* | 当 Claude 需要你关注时弹一条原生 macOS 通知。在通知小图标位显示品牌图,点击通知可把发起 Claude 的终端拉回前台。**焦点感知**:发起 Claude 的终端正处于前台时,仅放提示音不弹横幅(通过 `config.json` 的 `soundOnlyWhenFocused` 切换)。**按项目分组**:新通知会干净地替换通知中心里的旧条目,不会进程堆积,也不会跨项目互相覆盖。会自动通过 Homebrew 安装 `alerter`(`vjeantet/tap/alerter`)。改 `.claude/hooks/notify/config.json` 即可换提示音、替换 `.claude/hooks/notify/icon.png` 即可换图标。仅 macOS 运行时生效,其它平台静默 no-op。 |
118
- | pr-create-guard | `gh pr create` 的 PostToolUse hook。创建成功后通过 `gh pr view` 拉真实 PR body,扫 `^##` / `^###` headings 并统计 `- [ ]` / `- [x]`,通过 `additionalContext` 注入快照让 Agent 对照 PR-readiness 阶段的"范围 / 验收标准 / 风险 / 剩余 TODO"四要素。不 block——PostToolUse 发生在动作之后。gh 不可用时静默降级。 |
119
- | pr-ready-guard | `gh pr ready` 的 PreToolUse hook。**只按结构信号**拦截:(1) 仓库根存在游离 planning 文档(`findings.md` / `progress.md` / `task_plan.md`)或 `docs/superpowers/specs/*.md` 未归档——按 CLAUDE.md 的"文档规范"迁到 `docs/worklog/worklog-<date>-<branch>/` 或删除;(2) `docs/specs/*.md` 内有未结案的活跃 spec——晋升到 `docs/architecture/`、归档或删除;(3) 本地有未 push commits。**不做 PR 正文文本 regex 匹配**。放行时注入 PR body 快照。 |
120
137
 
121
138
  作用域选择:
122
139
 
@@ -129,7 +146,8 @@ npx auriga-cli
129
146
  ## 环境要求
130
147
 
131
148
  - Node.js >= 18
132
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code)(Plugins 和 Hooks 模块需要)
149
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code)(Claude Code Plugins 和 Hooks 模块需要)
150
+ - Codex CLI(仅 `install plugins --agent codex|both` 需要)
133
151
  - [Homebrew](https://brew.sh)(`notify` hook 用来安装 `alerter`,可选)
134
152
 
135
153
  ## 开发
package/dist/catalog.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-05-06T14:58:34.641Z",
2
+ "generatedAt": "2026-05-07T13:10:22.979Z",
3
3
  "workflowSkills": [
4
4
  {
5
5
  "name": "brainstorming",
@@ -75,21 +75,21 @@
75
75
  },
76
76
  {
77
77
  "name": "auriga-go",
78
- "description": "Workflow autopilot for the auriga workflow (reminder-based navigation + Experimental ship mode)"
78
+ "description": "(Claude/Codex) Workflow autopilot for the auriga workflow (reminder-based navigation + Experimental ship mode)"
79
+ },
80
+ {
81
+ "name": "auriga-pr-guards",
82
+ "description": "(Claude/Codex) PR-create snapshot inject + PR-ready structural block guardrails (Claude Code + Codex)"
83
+ },
84
+ {
85
+ "name": "session-instructions-loader",
86
+ "description": "(Codex) Injects extra instruction files on session start"
79
87
  }
80
88
  ],
81
89
  "hooks": [
82
90
  {
83
91
  "name": "notify",
84
92
  "description": "(opt-in) Native macOS notification when Claude needs your attention (auto-installs alerter via Homebrew)"
85
- },
86
- {
87
- "name": "pr-create-guard",
88
- "description": "Workflow guard: after `gh pr create`, query the new PR and inject a body snapshot (headings + TODO counts) so the Agent can verify scope / acceptance / risks / TODO"
89
- },
90
- {
91
- "name": "pr-ready-guard",
92
- "description": "Workflow guard: block `gh pr ready` on unpushed commits, stray planning docs, or unfinalized active specs in docs/specs/; filter-inject PR body snapshot otherwise"
93
93
  }
94
94
  ]
95
95
  }
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { type PluginAgent } from "./utils.js";
2
3
  import { type CategoryName } from "./types.js";
3
4
  export type { CategoryName } from "./types.js";
4
5
  export interface InstallParsed {
@@ -8,6 +9,7 @@ export interface InstallParsed {
8
9
  lang?: string;
9
10
  cwd?: string;
10
11
  scope?: "project" | "user";
12
+ agent?: PluginAgent;
11
13
  }
12
14
  export type ParsedArgs = {
13
15
  command: "help";
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import { loadCatalog } from "./catalog.js";
11
11
  import { renderHelp, renderTypeHelp } from "./help.js";
12
12
  import { renderGuide } from "./guide.js";
13
13
  import { CATEGORY_NAMES } from "./types.js";
14
- const RELOAD_REMINDER = "\n⚠ Reload your Claude Code session to pick up the new harness (CLAUDE.md / skills / plugins are loaded at session startup).\n";
14
+ const RELOAD_REMINDER = "\n⚠ Reload your Claude Code or Codex session to pick up the new harness (CLAUDE.md / skills / plugins are loaded at session startup).\n";
15
15
  const CATEGORY_SET = new Set(CATEGORY_NAMES);
16
16
  const TYPE_FOR_FILTER = {
17
17
  "--skill": "skills",
@@ -153,6 +153,12 @@ function parseInstall(argv) {
153
153
  i += advance;
154
154
  continue;
155
155
  }
156
+ if (t === "--agent" || t.startsWith("--agent=")) {
157
+ const [v, advance] = readSingleValue(argv, i, "--agent");
158
+ out.agent = v;
159
+ i += advance;
160
+ continue;
161
+ }
156
162
  // Object.hasOwn (not `in`) so Object.prototype keys like `toString` /
157
163
  // `constructor` don't slip into the filter-flag branch and produce a
158
164
  // misleading error.
@@ -199,6 +205,8 @@ function validateInstall(out, filterFlag) {
199
205
  // --all may combine with --scope.
200
206
  if (out.scope !== undefined)
201
207
  validateScopeValue(out.scope);
208
+ if (out.agent !== undefined)
209
+ validateAgentValue(out.agent);
202
210
  return;
203
211
  }
204
212
  // Rule 3: filter flag requires matching type.
@@ -220,6 +228,12 @@ function validateInstall(out, filterFlag) {
220
228
  }
221
229
  validateScopeValue(out.scope);
222
230
  }
231
+ if (out.agent !== undefined) {
232
+ if (out.type !== "plugins") {
233
+ parseErr("--agent only applies to plugins or --all.");
234
+ }
235
+ validateAgentValue(out.agent);
236
+ }
223
237
  // Value validation for workflow.
224
238
  if (out.type === "workflow" && out.lang !== undefined) {
225
239
  const valid = LANGUAGES.map((l) => l.value);
@@ -269,6 +283,11 @@ function validateScopeValue(scope) {
269
283
  parseErr(`unknown --scope value '${scope}'; expected 'project' or 'user'.`);
270
284
  }
271
285
  }
286
+ function validateAgentValue(agent) {
287
+ if (agent !== "claude" && agent !== "codex" && agent !== "both") {
288
+ parseErr(`unknown --agent value '${agent}'; expected 'claude', 'codex', or 'both'.`);
289
+ }
290
+ }
272
291
  // ---------------------------------------------------------------------------
273
292
  // main — returns exit code (spec §5.3.1 / §7)
274
293
  // ---------------------------------------------------------------------------
@@ -341,13 +360,23 @@ async function runInstall(p) {
341
360
  * Precheck external prerequisites before touching any files.
342
361
  * Returns null if OK, or an error message.
343
362
  */
344
- function precheckExternal(need) {
363
+ function precheckExternal(need, agent = "claude") {
345
364
  if (need.includes("plugins")) {
346
- try {
347
- exec("which claude");
365
+ if (agent === "claude" || agent === "both") {
366
+ try {
367
+ exec("which claude");
368
+ }
369
+ catch {
370
+ return "'claude' CLI not in PATH. Install Claude Code first (https://docs.claude.com/claude-code), then re-run.";
371
+ }
348
372
  }
349
- catch {
350
- return "'claude' CLI not in PATH. Install Claude Code first (https://docs.claude.com/claude-code), then re-run.";
373
+ if (agent === "codex" || agent === "both") {
374
+ try {
375
+ exec("which codex");
376
+ }
377
+ catch {
378
+ return "'codex' CLI not in PATH. Install Codex first, then re-run.";
379
+ }
351
380
  }
352
381
  }
353
382
  return null;
@@ -368,8 +397,8 @@ async function safeFetchContentRoot() {
368
397
  * bubble up. Keeps runAll / runSingle from drifting apart as new
369
398
  * pre-install behavior accrues.
370
399
  */
371
- async function prepareInstall(needs) {
372
- const pre = precheckExternal(needs);
400
+ async function prepareInstall(needs, agent) {
401
+ const pre = precheckExternal(needs, agent ?? "claude");
373
402
  if (pre) {
374
403
  log.error(pre);
375
404
  return { exit: 1 };
@@ -382,7 +411,7 @@ async function prepareInstall(needs) {
382
411
  return { packageRoot: fetched.root };
383
412
  }
384
413
  async function runAll(p) {
385
- const prep = await prepareInstall(["plugins"]);
414
+ const prep = await prepareInstall(["plugins"], p.agent);
386
415
  if ("exit" in prep)
387
416
  return prep.exit;
388
417
  const { packageRoot } = prep;
@@ -395,6 +424,7 @@ async function runAll(p) {
395
424
  const opts = {
396
425
  interactive: false,
397
426
  scope: p.scope,
427
+ agent: p.agent,
398
428
  };
399
429
  try {
400
430
  await dispatchInstaller(category, packageRoot, opts);
@@ -422,9 +452,13 @@ async function runAll(p) {
422
452
  // the default project scope and leaves the intended user-scope
423
453
  // install incomplete.
424
454
  const scopeSuffix = p.scope ? ` --scope ${p.scope}` : "";
455
+ const agentSuffix = p.agent ? ` --agent ${p.agent}` : "";
425
456
  process.stderr.write("\nRetry:\n");
426
457
  for (const s of failed) {
427
- const suffix = scopeCategory(s.category) ? scopeSuffix : "";
458
+ const suffix = [
459
+ scopeCategory(s.category) ? scopeSuffix : "",
460
+ s.category === "plugins" ? agentSuffix : "",
461
+ ].join("");
428
462
  process.stderr.write(` npx -y auriga-cli install ${s.category}${suffix}\n`);
429
463
  }
430
464
  // Partial success still installed assets that need a session reload
@@ -442,7 +476,7 @@ function scopeCategory(c) {
442
476
  }
443
477
  async function runSingle(p) {
444
478
  const category = p.type;
445
- const prep = await prepareInstall(category === "plugins" ? ["plugins"] : []);
479
+ const prep = await prepareInstall(category === "plugins" ? ["plugins"] : [], p.agent);
446
480
  if ("exit" in prep)
447
481
  return prep.exit;
448
482
  const { packageRoot } = prep;
@@ -451,6 +485,7 @@ async function runSingle(p) {
451
485
  lang: p.lang,
452
486
  cwd: p.cwd,
453
487
  scope: p.scope,
488
+ agent: p.agent,
454
489
  selected: p.filter,
455
490
  };
456
491
  try {
@@ -499,7 +534,7 @@ async function runLegacyMenu() {
499
534
  { name: "Workflow — CLAUDE.md + AGENTS.md", value: "workflow", checked: true },
500
535
  { name: "Skills — Development process skills (brainstorming, TDD, debugging...)", value: "skills", checked: true },
501
536
  { name: "Recommended Skills — Extra utility skills (claude-code-agent, codex-agent...)", value: "recommended", checked: true },
502
- { name: "Plugins — Claude Code plugins (skill-creator, claude-md-management, codex...)", value: "plugins", checked: true },
537
+ { name: "Plugins — Claude Code / Codex plugins (skill-creator, codex, auriga-go...)", value: "plugins", checked: true },
503
538
  { name: "Hooks — Claude Code hooks (notifications, etc.)", value: "hooks", checked: true },
504
539
  ],
505
540
  }));
@@ -0,0 +1,23 @@
1
+ export interface CodexMarketplacePlugin {
2
+ name: string;
3
+ source?: {
4
+ source?: string;
5
+ path?: string;
6
+ } | string;
7
+ }
8
+ export interface CodexMarketplace {
9
+ name: string;
10
+ plugins: CodexMarketplacePlugin[];
11
+ }
12
+ export interface CodexInstallPlugin {
13
+ name: string;
14
+ description?: string;
15
+ defaultOn?: boolean;
16
+ }
17
+ export interface CodexInstallConfig {
18
+ plugins: CodexInstallPlugin[];
19
+ }
20
+ export declare function validateCodexMarketplace(raw: unknown): asserts raw is CodexMarketplace;
21
+ export declare function validateCodexInstallConfig(raw: unknown): asserts raw is CodexInstallConfig;
22
+ export declare function codexLocalPluginPath(plugin: CodexMarketplacePlugin): string | undefined;
23
+ export declare function codexManifestPath(plugin: CodexMarketplacePlugin): string | undefined;
@@ -0,0 +1,75 @@
1
+ import path from "node:path";
2
+ const PLUGIN_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
3
+ const MARKETPLACE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
4
+ export function validateCodexMarketplace(raw) {
5
+ if (!raw || typeof raw !== "object") {
6
+ throw new Error("Codex marketplace.json: root must be an object");
7
+ }
8
+ const cfg = raw;
9
+ if (typeof cfg.name !== "string" || !MARKETPLACE_NAME_RE.test(cfg.name)) {
10
+ throw new Error("Codex marketplace.json: root must include a safe name");
11
+ }
12
+ if (!Array.isArray(cfg.plugins)) {
13
+ throw new Error("Codex marketplace.json: .plugins must be an array");
14
+ }
15
+ for (const [i, plugin] of cfg.plugins.entries()) {
16
+ if (!plugin || typeof plugin !== "object") {
17
+ throw new Error(`Codex marketplace.json: plugins[${i}] must be an object`);
18
+ }
19
+ const p = plugin;
20
+ if (typeof p.name !== "string" || !PLUGIN_NAME_RE.test(p.name)) {
21
+ throw new Error(`Codex marketplace.json: plugins[${i}].name ${JSON.stringify(p.name)} does not match ${PLUGIN_NAME_RE}`);
22
+ }
23
+ }
24
+ }
25
+ export function validateCodexInstallConfig(raw) {
26
+ if (!raw || typeof raw !== "object") {
27
+ throw new Error("Codex install.json: root must be an object");
28
+ }
29
+ const cfg = raw;
30
+ if (!Array.isArray(cfg.plugins)) {
31
+ throw new Error("Codex install.json: .plugins must be an array");
32
+ }
33
+ for (const [i, plugin] of cfg.plugins.entries()) {
34
+ if (!plugin || typeof plugin !== "object") {
35
+ throw new Error(`Codex install.json: plugins[${i}] must be an object`);
36
+ }
37
+ const p = plugin;
38
+ if (typeof p.name !== "string" || !PLUGIN_NAME_RE.test(p.name)) {
39
+ throw new Error(`Codex install.json: plugins[${i}].name ${JSON.stringify(p.name)} does not match ${PLUGIN_NAME_RE}`);
40
+ }
41
+ if (p.description !== undefined && typeof p.description !== "string") {
42
+ throw new Error(`Codex install.json: plugins[${i}].description must be a string`);
43
+ }
44
+ if (p.defaultOn !== undefined && typeof p.defaultOn !== "boolean") {
45
+ throw new Error(`Codex install.json: plugins[${i}].defaultOn must be a boolean`);
46
+ }
47
+ }
48
+ }
49
+ export function codexLocalPluginPath(plugin) {
50
+ const sourcePath = typeof plugin.source === "object" && plugin.source?.source === "local"
51
+ ? plugin.source.path
52
+ : undefined;
53
+ if (typeof sourcePath !== "string" || sourcePath.length === 0)
54
+ return undefined;
55
+ if (sourcePath.startsWith("/") || sourcePath.startsWith("\\") || sourcePath.includes("\0")) {
56
+ throw new Error(`Codex marketplace.json: plugin ${plugin.name} has unsafe source.path`);
57
+ }
58
+ if (sourcePath.includes("\\")) {
59
+ throw new Error(`Codex marketplace.json: plugin ${plugin.name} source.path must use POSIX separators`);
60
+ }
61
+ const withoutDot = sourcePath.startsWith("./") ? sourcePath.slice(2) : sourcePath;
62
+ const normalized = path.posix.normalize(withoutDot);
63
+ if (normalized !== withoutDot ||
64
+ normalized === "." ||
65
+ normalized === ".." ||
66
+ normalized.startsWith("../") ||
67
+ normalized.includes("/../")) {
68
+ throw new Error(`Codex marketplace.json: plugin ${plugin.name} has unsafe source.path`);
69
+ }
70
+ return normalized;
71
+ }
72
+ export function codexManifestPath(plugin) {
73
+ const sourcePath = codexLocalPluginPath(plugin);
74
+ return sourcePath ? path.posix.join(sourcePath, ".codex-plugin", "plugin.json") : undefined;
75
+ }
package/dist/guide.js CHANGED
@@ -32,6 +32,7 @@ Ensure these CLIs are in PATH:
32
32
  - node (>= 18)
33
33
  - git
34
34
  - claude (required for plugins; see https://docs.claude.com/claude-code)
35
+ - codex (required only for plugins installed with --agent codex or --agent both)
35
36
 
36
37
  Optional (only if you'll push a PR): gh
37
38
 
@@ -39,6 +40,7 @@ Verify:
39
40
  ${cmd("node --version && git --version && claude --version")}
40
41
 
41
42
  If \`claude\` is missing: install Claude Code first, then re-run this guide.
43
+ If you plan to install Codex plugins, also verify \`codex --version\`.
42
44
 
43
45
  ${h("## Step 2 — Read --help BEFORE installing (do not skip)")}
44
46
 
@@ -68,6 +70,7 @@ Targeted — single category, picking from the catalog surfaced in Step 2:
68
70
  ${cmd("npx -y auriga-cli install workflow --lang en")}
69
71
  ${cmd("npx -y auriga-cli install skills --skill brainstorming test-driven-development")}
70
72
  ${cmd("npx -y auriga-cli install plugins --plugin skill-creator codex --scope user")}
73
+ ${cmd("npx -y auriga-cli install plugins --agent codex --plugin session-instructions-loader")}
71
74
  ${cmd("npx -y auriga-cli install hooks --hook pr-ready-guard")}
72
75
 
73
76
  Opt-in hooks: some hooks (e.g. \`notify\`) are NOT in the default set
@@ -91,10 +94,10 @@ Exit codes:
91
94
 
92
95
  ${h("## Step 4 — Reload session (REQUIRED when installed non-interactively)")}
93
96
 
94
- ${warn("⚠")} CLAUDE.md, .agents/skills/, .claude/plugins.json, and hook
95
- registrations are loaded at Claude Code session startup. If you ran
96
- \`npx -y auriga-cli install\` inside an existing Claude Code session
97
- (e.g., \`claude -p\` / \`claude -p --worktree\`), the current session
97
+ ${warn("⚠")} CLAUDE.md, .agents/skills/, .claude/plugins.json, Codex plugin
98
+ config, and hook registrations are loaded at session startup. If you ran
99
+ \`npx -y auriga-cli install\` inside an existing Claude Code or Codex session
100
+ (e.g., \`claude -p\` / \`claude -p --worktree\` / \`codex exec\`), the current session
98
101
  will NOT see the new harness.
99
102
 
100
103
  Action:
@@ -109,6 +112,7 @@ Expected artifacts:
109
112
  - AGENTS.md -> CLAUDE.md (symlink)
110
113
  - .agents/skills/<name>/ (one per installed skill)
111
114
  - .claude/plugins.json
115
+ - ~/.codex/config.toml (Codex plugin enablement, if Codex plugins selected)
112
116
  - .claude/settings.json (updated hook registrations, if hooks selected)
113
117
 
114
118
  ${h("## Troubleshooting")}
package/dist/help.js CHANGED
@@ -10,7 +10,8 @@ export function renderHelp(catalog, version) {
10
10
  USAGE
11
11
  npx auriga-cli guide Agent bootstrap SOP (start here)
12
12
  npx auriga-cli install (TTY only) checkbox menu
13
- npx auriga-cli install --all [--scope <s>] workflow + skills + plugins + hooks
13
+ npx auriga-cli install --all [--scope <s>] [--agent <a>]
14
+ workflow + skills + plugins + hooks
14
15
  (excludes recommended — install separately)
15
16
  npx auriga-cli install <type> [type-specific flags] single category
16
17
  npx auriga-cli install <type> --help per-category help + catalog subset
@@ -24,7 +25,7 @@ TYPES (exactly one with <type> form)
24
25
  workflow CLAUDE.md + AGENTS.md (workflow manifesto)
25
26
  skills Default-on workflow skills (listed below)
26
27
  recommended Opt-in utility skills (listed below)
27
- plugins Claude Code plugins (listed below)
28
+ plugins Claude Code and Codex plugins (listed below)
28
29
  hooks Project-level hooks for Claude Code (listed below)
29
30
 
30
31
  TYPE-SPECIFIC FLAGS
@@ -35,6 +36,7 @@ TYPE-SPECIFIC FLAGS
35
36
  recommended: --recommended-skill <names...>
36
37
  --scope <project|user> default project
37
38
  plugins: --plugin <names...>
39
+ --agent <claude|codex|both> default claude
38
40
  --scope <project|user> default project
39
41
  hooks: --hook <names...> non-interactive default installs every
40
42
  hook with defaultOn != false
@@ -117,12 +119,15 @@ ${col(catalog.recommendedSkills)}
117
119
  return `${header}
118
120
 
119
121
  USAGE
120
- npx auriga-cli install plugins [--plugin <names...>] [--scope <project|user>]
122
+ npx auriga-cli install plugins [--plugin <names...>] [--agent <claude|codex|both>] [--scope <project|user>]
121
123
 
122
124
  FLAGS
123
125
  --plugin <names...> space-separated; '*' = all
124
- omit → install every plugin listed below
126
+ omit → install every plugin available for the selected agent
127
+ --agent <...> target runtime: claude, codex, or both
128
+ default claude; codex enablement is user-level
125
129
  --scope <project|user> default project
130
+ applies to Claude Code; ignored for Codex
126
131
 
127
132
  CATALOG (plugins)
128
133
  ${col(catalog.plugins)}
package/dist/hooks.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import crypto from "node:crypto";
3
2
  import fs from "node:fs";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
5
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
7
- import { exec, fetchExtraContentBinary, log, withEsc, } from "./utils.js";
6
+ import { atomicWriteFile, exec, fetchExtraContentBinary, log, withEsc, } from "./utils.js";
8
7
  // --- Registry validation ---
9
8
  // hooks.json is fetched at runtime from raw.githubusercontent.com, so any
10
9
  // downstream code that interpolates registry values into shell commands or
@@ -539,31 +538,6 @@ function writeMergedSettings(resolved, hook, parsed) {
539
538
  }
540
539
  return { mutated };
541
540
  }
542
- /**
543
- * Write `content` to `filePath` atomically and TOCTOU-safely.
544
- *
545
- * A predictable tmp name like `settings.json.tmp` lets a local attacker
546
- * pre-create that path as a symlink pointing at, say, ~/.ssh/authorized_keys
547
- * — the next install would then clobber the link target. Defenses: random
548
- * suffix so the tmp name can't be predicted, plus O_CREAT|O_EXCL so we
549
- * refuse to open the path at all if anything (file or symlink) exists
550
- * there. Restrictive 0o600 perms in case the parent directory is
551
- * world-writable. Final rename(2) is the atomic step.
552
- */
553
- function atomicWriteFile(filePath, content) {
554
- const dir = path.dirname(filePath);
555
- const base = path.basename(filePath);
556
- const suffix = crypto.randomBytes(8).toString("hex");
557
- const tmp = path.join(dir, `.${base}.${suffix}.tmp`);
558
- const fd = fs.openSync(tmp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
559
- try {
560
- fs.writeSync(fd, content);
561
- }
562
- finally {
563
- fs.closeSync(fd);
564
- }
565
- fs.renameSync(tmp, filePath);
566
- }
567
541
  export function loadHooksConfig(packageRoot) {
568
542
  const configPath = path.join(packageRoot, ".claude", "hooks", "hooks.json");
569
543
  const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
package/dist/plugins.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { checkbox, select } from "@inquirer/prompts";
4
- import { exec, log, withEsc } from "./utils.js";
5
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
6
+ import { codexManifestPath, validateCodexInstallConfig, validateCodexMarketplace, } from "./codex-plugin-config.js";
7
+ import { atomicWriteFile, exec, fetchExtraContent, log, readPackageVersion, withEsc } from "./utils.js";
5
8
  // Plugin names, marketplace names/sources, and plugin-package names all
6
9
  // end up in `claude plugins ...` shell commands via string interpolation.
7
10
  // .claude/plugins.json is fetched from raw GitHub at runtime, so every
@@ -73,8 +76,12 @@ function getInstalledPlugins() {
73
76
  function resolvePluginSelection(all, selected) {
74
77
  if (!selected || (selected.length === 1 && selected[0] === "*"))
75
78
  return all;
76
- const wanted = new Set(selected);
77
- return all.filter((p) => wanted.has(p.name));
79
+ const byName = new Map(all.map((p) => [p.name, p]));
80
+ const missing = selected.filter((name) => !byName.has(name));
81
+ if (missing.length > 0) {
82
+ throw new Error(`${missing.join(", ")} not available for Claude Code plugins; available: ${all.map((p) => p.name).join(", ")}`);
83
+ }
84
+ return selected.map((name) => byName.get(name));
78
85
  }
79
86
  function getInstalledMarketplaces() {
80
87
  try {
@@ -89,7 +96,233 @@ function getInstalledMarketplaces() {
89
96
  return new Set();
90
97
  }
91
98
  }
99
+ function loadCodexMarketplace(packageRoot) {
100
+ const marketplacePath = path.join(packageRoot, ".agents", "plugins", "marketplace.json");
101
+ if (!fs.existsSync(marketplacePath))
102
+ return null;
103
+ const raw = JSON.parse(fs.readFileSync(marketplacePath, "utf-8"));
104
+ validateCodexMarketplace(raw);
105
+ return raw;
106
+ }
107
+ function loadCodexInstallConfig(packageRoot) {
108
+ const installPath = path.join(packageRoot, ".agents", "plugins", "install.json");
109
+ if (!fs.existsSync(installPath))
110
+ return null;
111
+ const raw = JSON.parse(fs.readFileSync(installPath, "utf-8"));
112
+ validateCodexInstallConfig(raw);
113
+ return raw;
114
+ }
115
+ function resolveCodexPluginSelection(all, selected) {
116
+ if (!selected)
117
+ return all.filter((p) => p.defaultOn !== false);
118
+ if (selected.length === 1 && selected[0] === "*")
119
+ return all;
120
+ const byName = new Map(all.map((p) => [p.name, p]));
121
+ const missing = selected.filter((name) => !byName.has(name));
122
+ if (missing.length > 0) {
123
+ throw new Error(`${missing.join(", ")} not available for Codex plugins; available: ${all.map((p) => p.name).join(", ")}`);
124
+ }
125
+ return selected.map((name) => byName.get(name));
126
+ }
127
+ function codexHome() {
128
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
129
+ }
130
+ function shellQuote(value) {
131
+ return `'${value.replace(/'/g, "'\\''")}'`;
132
+ }
133
+ function codexMarketplaceAddCommand(packageRoot) {
134
+ if (process.env.DEV === "1") {
135
+ return `codex plugin marketplace add ${shellQuote(packageRoot)}`;
136
+ }
137
+ const ref = process.env.AURIGA_CONTENT_REF || `v${readPackageVersion()}`;
138
+ return `codex plugin marketplace add Ben2pc/auriga-cli --ref ${shellQuote(ref)}`;
139
+ }
140
+ function pluginHasHooks(packageRoot, plugin) {
141
+ const relativeManifestPath = codexManifestPath(plugin);
142
+ if (!relativeManifestPath)
143
+ return false;
144
+ const manifestPath = path.join(packageRoot, relativeManifestPath);
145
+ if (!fs.existsSync(manifestPath))
146
+ return false;
147
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
148
+ return typeof manifest.hooks === "string" || Array.isArray(manifest.hooks);
149
+ }
150
+ async function ensureCodexPluginManifests(packageRoot, plugins) {
151
+ for (const plugin of plugins) {
152
+ const manifestPath = codexManifestPath(plugin);
153
+ if (!manifestPath) {
154
+ throw new Error(`Codex marketplace.json: plugin ${plugin.name} must use a local source.path`);
155
+ }
156
+ if (fs.existsSync(path.join(packageRoot, manifestPath)))
157
+ continue;
158
+ await fetchExtraContent(packageRoot, manifestPath);
159
+ }
160
+ }
161
+ function ensureTomlBoolean(content, section, key, value) {
162
+ const line = `${key} = ${value ? "true" : "false"}`;
163
+ const header = `[${section}]`;
164
+ const lines = content.length > 0 ? content.split(/\r?\n/) : [];
165
+ const start = lines.findIndex((l) => l.trim() === header);
166
+ if (start === -1) {
167
+ const prefix = content.trimEnd();
168
+ return `${prefix}${prefix ? "\n\n" : ""}${header}\n${line}\n`;
169
+ }
170
+ let end = lines.length;
171
+ for (let i = start + 1; i < lines.length; i += 1) {
172
+ if (/^\s*\[/.test(lines[i])) {
173
+ end = i;
174
+ break;
175
+ }
176
+ }
177
+ const keyRe = new RegExp(`^\\s*${key}\\s*=`);
178
+ for (let i = start + 1; i < end; i += 1) {
179
+ if (keyRe.test(lines[i])) {
180
+ lines[i] = line;
181
+ return lines.join("\n");
182
+ }
183
+ }
184
+ lines.splice(end, 0, line);
185
+ return lines.join("\n");
186
+ }
187
+ function parseCodexConfigToml(content, configPath) {
188
+ if (content.trim().length === 0)
189
+ return {};
190
+ try {
191
+ return parseToml(content);
192
+ }
193
+ catch (e) {
194
+ throw new Error(`Codex config.toml is invalid TOML at ${configPath}: ${e.message}`);
195
+ }
196
+ }
197
+ function isTomlTable(value) {
198
+ return typeof value === "object" && value !== null && !Array.isArray(value);
199
+ }
200
+ function getOrCreateTomlTable(parent, key, pathLabel) {
201
+ const existing = parent[key];
202
+ if (existing === undefined) {
203
+ const table = {};
204
+ parent[key] = table;
205
+ return table;
206
+ }
207
+ if (!isTomlTable(existing)) {
208
+ throw new Error(`Codex config.toml: ${pathLabel} must be a TOML table`);
209
+ }
210
+ return existing;
211
+ }
212
+ function buildCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks) {
213
+ const parsed = parseCodexConfigToml(originalContent, configPath);
214
+ const features = getOrCreateTomlTable(parsed, "features", "features");
215
+ features.plugins = true;
216
+ if (needsPluginHooks) {
217
+ features.plugin_hooks = true;
218
+ }
219
+ const plugins = getOrCreateTomlTable(parsed, "plugins", "plugins");
220
+ for (const pluginKey of pluginKeys) {
221
+ const plugin = getOrCreateTomlTable(plugins, pluginKey, `plugins.${JSON.stringify(pluginKey)}`);
222
+ plugin.enabled = true;
223
+ }
224
+ return stringifyToml(parsed);
225
+ }
226
+ function tryMinimalCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks) {
227
+ let content = originalContent;
228
+ content = ensureTomlBoolean(content, "features", "plugins", true);
229
+ if (needsPluginHooks) {
230
+ content = ensureTomlBoolean(content, "features", "plugin_hooks", true);
231
+ }
232
+ for (const pluginKey of pluginKeys) {
233
+ content = ensureTomlBoolean(content, `plugins."${pluginKey}"`, "enabled", true);
234
+ }
235
+ try {
236
+ parseToml(content);
237
+ return content;
238
+ }
239
+ catch {
240
+ // Existing configs may use legal TOML forms such as inline tables
241
+ // (`features = { plugins = false }`). In that case, a local section
242
+ // insertion would redefine the table, so fall back to structured output.
243
+ parseCodexConfigToml(originalContent, configPath);
244
+ return null;
245
+ }
246
+ }
247
+ function enableCodexPluginConfig(configPath, pluginKeys, needsPluginHooks) {
248
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
249
+ const originalContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf-8") : "";
250
+ const minimalContent = tryMinimalCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks);
251
+ const content = minimalContent ?? buildCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks);
252
+ atomicWriteFile(configPath, content.endsWith("\n") ? content : `${content}\n`);
253
+ }
254
+ async function installCodexPlugins(packageRoot, opts) {
255
+ const marketplace = loadCodexMarketplace(packageRoot);
256
+ if (!marketplace) {
257
+ const msg = "No .agents/plugins/marketplace.json found";
258
+ if (!opts.interactive)
259
+ throw new Error(msg);
260
+ log.warn(msg);
261
+ return;
262
+ }
263
+ const installConfig = loadCodexInstallConfig(packageRoot);
264
+ if (!installConfig) {
265
+ const msg = "No .agents/plugins/install.json found";
266
+ if (!opts.interactive)
267
+ throw new Error(msg);
268
+ log.warn(msg);
269
+ return;
270
+ }
271
+ const marketplaceByName = new Map(marketplace.plugins.map((p) => [p.name, p]));
272
+ const selected = opts.interactive
273
+ ? await withEsc(checkbox({
274
+ message: "Select Codex plugins to install:",
275
+ choices: installConfig.plugins.map((p) => ({
276
+ name: p.description ? `${p.name} — ${p.description}` : p.name,
277
+ value: p,
278
+ checked: p.defaultOn !== false,
279
+ })),
280
+ }))
281
+ : resolveCodexPluginSelection(installConfig.plugins, opts.selected);
282
+ if (selected.length === 0) {
283
+ log.skip("No Codex plugins selected");
284
+ return;
285
+ }
286
+ const failures = [];
287
+ try {
288
+ exec(codexMarketplaceAddCommand(packageRoot), { inherit: true });
289
+ log.ok(`Codex marketplace ${marketplace.name} added`);
290
+ }
291
+ catch {
292
+ log.error(`Failed to add Codex marketplace: ${marketplace.name}`);
293
+ failures.push(`codex marketplace ${marketplace.name}`);
294
+ }
295
+ if (failures.length === 0) {
296
+ const selectedMarketplacePlugins = selected.map((p) => {
297
+ const plugin = marketplaceByName.get(p.name);
298
+ if (!plugin) {
299
+ throw new Error(`Codex install.json: plugin ${p.name} is not present in marketplace.json`);
300
+ }
301
+ return plugin;
302
+ });
303
+ await ensureCodexPluginManifests(packageRoot, selectedMarketplacePlugins);
304
+ const pluginKeys = selectedMarketplacePlugins.map((p) => `${p.name}@${marketplace.name}`);
305
+ const needsPluginHooks = selectedMarketplacePlugins.some((p) => pluginHasHooks(packageRoot, p));
306
+ enableCodexPluginConfig(path.join(codexHome(), "config.toml"), pluginKeys, needsPluginHooks);
307
+ for (const plugin of selected) {
308
+ log.ok(`${plugin.name} enabled for Codex`);
309
+ }
310
+ }
311
+ if (failures.length > 0 && !opts.interactive) {
312
+ throw new Error(`${failures.length} Codex plugin operation(s) failed: ${failures.join(", ")}`);
313
+ }
314
+ }
92
315
  export async function installPlugins(packageRoot, opts) {
316
+ const agent = opts.interactive
317
+ ? await withEsc(select({
318
+ message: "Plugins target runtime:",
319
+ choices: [
320
+ { name: "Claude Code", value: "claude" },
321
+ { name: "Codex", value: "codex" },
322
+ { name: "Both", value: "both" },
323
+ ],
324
+ }))
325
+ : opts.agent ?? "claude";
93
326
  // Non-interactive path already ran `precheckExternal(["plugins"])` in
94
327
  // cli.ts's runAll / runSingle before dispatching here, so rechecking
95
328
  // `which claude` would be a redundant subprocess on every install.
@@ -97,25 +330,51 @@ export async function installPlugins(packageRoot, opts) {
97
330
  // validate there — and fail soft (log-and-return) to match the menu's
98
331
  // continue-on-failure ergonomics.
99
332
  if (opts.interactive) {
100
- try {
101
- exec("which claude");
333
+ if (agent === "claude" || agent === "both") {
334
+ try {
335
+ exec("which claude");
336
+ }
337
+ catch {
338
+ log.error("'claude' CLI not found. Please install Claude Code first.");
339
+ return;
340
+ }
102
341
  }
103
- catch {
104
- log.error("'claude' CLI not found. Please install Claude Code first.");
105
- return;
342
+ if (agent === "codex" || agent === "both") {
343
+ try {
344
+ exec("which codex");
345
+ }
346
+ catch {
347
+ log.error("'codex' CLI not found. Please install Codex first.");
348
+ return;
349
+ }
106
350
  }
107
351
  }
352
+ if (agent === "codex") {
353
+ await installCodexPlugins(packageRoot, opts);
354
+ return;
355
+ }
356
+ const failures = [];
108
357
  const configPath = path.join(packageRoot, ".claude", "plugins.json");
358
+ let config = null;
109
359
  if (!fs.existsSync(configPath)) {
110
360
  log.warn("No .claude/plugins.json found");
111
- return;
361
+ if (agent === "both")
362
+ failures.push("Claude Code plugins config missing");
363
+ else
364
+ return;
112
365
  }
113
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
114
- validatePluginsConfig(raw);
115
- const config = raw;
116
- if (config.plugins.length === 0) {
117
- log.warn("No plugins defined in plugins.json");
118
- return;
366
+ else {
367
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
368
+ validatePluginsConfig(raw);
369
+ config = raw;
370
+ if (config.plugins.length === 0) {
371
+ log.warn("No plugins defined in plugins.json");
372
+ if (agent === "both")
373
+ failures.push("Claude Code plugins config empty");
374
+ else
375
+ return;
376
+ config = null;
377
+ }
119
378
  }
120
379
  const scope = opts.interactive
121
380
  ? await withEsc(select({
@@ -126,60 +385,82 @@ export async function installPlugins(packageRoot, opts) {
126
385
  ],
127
386
  }))
128
387
  : opts.scope ?? "project";
129
- const installed = getInstalledPlugins();
130
- const selected = opts.interactive
131
- ? await withEsc(checkbox({
132
- message: "Select plugins to install:",
133
- choices: config.plugins.map((p) => {
134
- const scopes = installed.get(p.package);
135
- const suffix = scopes ? ` (installed: ${scopes.join(", ")})` : "";
136
- return {
137
- name: `${p.name} — ${p.description}${suffix}`,
138
- value: p,
139
- checked: !scopes || !(scopes.includes("user") && scopes.includes("project")),
140
- };
141
- }),
142
- }))
143
- : resolvePluginSelection(config.plugins, opts.selected);
144
- if (selected.length === 0) {
145
- log.skip("No plugins selected");
146
- return;
147
- }
148
- // Install required marketplaces
149
- const existingMarketplaces = getInstalledMarketplaces();
150
- const marketplacesToAdd = new Map();
151
- for (const plugin of selected) {
152
- if (plugin.marketplace && !existingMarketplaces.has(plugin.marketplace.name)) {
153
- marketplacesToAdd.set(plugin.marketplace.name, plugin.marketplace.source);
154
- }
155
- }
156
- const failures = [];
157
- for (const [name, source] of marketplacesToAdd) {
158
- console.log(`\nAdding marketplace: ${name}...`);
388
+ if (config) {
389
+ const installed = getInstalledPlugins();
390
+ let selected;
159
391
  try {
160
- exec(`claude plugins marketplace add ${source}`, { inherit: true });
161
- log.ok(`Marketplace ${name} added`);
392
+ selected = opts.interactive
393
+ ? await withEsc(checkbox({
394
+ message: "Select plugins to install:",
395
+ choices: config.plugins.map((p) => {
396
+ const scopes = installed.get(p.package);
397
+ const suffix = scopes ? ` (installed: ${scopes.join(", ")})` : "";
398
+ return {
399
+ name: `${p.name} — ${p.description}${suffix}`,
400
+ value: p,
401
+ checked: !scopes || !(scopes.includes("user") && scopes.includes("project")),
402
+ };
403
+ }),
404
+ }))
405
+ : resolvePluginSelection(config.plugins, opts.selected);
162
406
  }
163
- catch {
164
- log.error(`Failed to add marketplace: ${name}`);
165
- failures.push(`marketplace ${name}`);
407
+ catch (e) {
408
+ if (agent !== "both")
409
+ throw e;
410
+ selected = [];
411
+ failures.push(`Claude Code: ${e.message}`);
166
412
  }
167
- }
168
- // Install plugins
169
- for (const plugin of selected) {
170
- console.log(`\nInstalling ${plugin.name}...`);
171
- try {
172
- exec(`claude plugins install ${plugin.package} --scope ${scope}`, {
173
- inherit: true,
174
- });
175
- log.ok(`${plugin.name} installed`);
413
+ if (selected.length === 0) {
414
+ log.skip("No plugins selected");
176
415
  }
177
- catch {
178
- log.error(`Failed to install: ${plugin.name}`);
179
- failures.push(plugin.name);
416
+ else {
417
+ // Install required marketplaces
418
+ const existingMarketplaces = getInstalledMarketplaces();
419
+ const marketplacesToAdd = new Map();
420
+ for (const plugin of selected) {
421
+ if (plugin.marketplace && !existingMarketplaces.has(plugin.marketplace.name)) {
422
+ marketplacesToAdd.set(plugin.marketplace.name, plugin.marketplace.source);
423
+ }
424
+ }
425
+ for (const [name, source] of marketplacesToAdd) {
426
+ console.log(`\nAdding marketplace: ${name}...`);
427
+ try {
428
+ exec(`claude plugins marketplace add ${source}`, { inherit: true });
429
+ log.ok(`Marketplace ${name} added`);
430
+ }
431
+ catch {
432
+ log.error(`Failed to add marketplace: ${name}`);
433
+ failures.push(`marketplace ${name}`);
434
+ }
435
+ }
436
+ // Install plugins
437
+ for (const plugin of selected) {
438
+ console.log(`\nInstalling ${plugin.name}...`);
439
+ try {
440
+ exec(`claude plugins install ${plugin.package} --scope ${scope}`, {
441
+ inherit: true,
442
+ });
443
+ log.ok(`${plugin.name} installed`);
444
+ }
445
+ catch {
446
+ log.error(`Failed to install: ${plugin.name}`);
447
+ failures.push(plugin.name);
448
+ }
449
+ }
180
450
  }
181
451
  }
182
- if (failures.length > 0 && !opts.interactive) {
452
+ if (failures.length > 0 && !opts.interactive && agent !== "both") {
183
453
  throw new Error(`${failures.length} plugin operation(s) failed: ${failures.join(", ")}`);
184
454
  }
455
+ if (agent === "both") {
456
+ try {
457
+ await installCodexPlugins(packageRoot, opts);
458
+ }
459
+ catch (e) {
460
+ failures.push(`codex: ${e.message}`);
461
+ }
462
+ if (failures.length > 0 && !opts.interactive) {
463
+ throw new Error(`${failures.length} plugin operation(s) failed: ${failures.join(", ")}`);
464
+ }
465
+ }
185
466
  }
package/dist/utils.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface PluginDef {
19
19
  export interface PluginsConfig {
20
20
  plugins: PluginDef[];
21
21
  }
22
+ export type PluginAgent = "claude" | "codex" | "both";
22
23
  /**
23
24
  * Shared install function argument shape. Each installer consumes the
24
25
  * subset of fields meaningful to its category; irrelevant fields are
@@ -35,6 +36,8 @@ export interface InstallOpts {
35
36
  cwd?: string;
36
37
  /** skills / recommended / plugins / hooks — `"user"` means install globally. */
37
38
  scope?: "project" | "user";
39
+ /** plugins only — runtime to install plugins for. Defaults to Claude Code. */
40
+ agent?: PluginAgent;
38
41
  /**
39
42
  * sub-item filter. `undefined` = full set of this category.
40
43
  * Names are validated against the catalog by the CLI layer; installers
@@ -73,6 +76,16 @@ export declare function readPackageVersion(): string;
73
76
  export declare function fetchContentRoot(): Promise<string>;
74
77
  export declare function fetchExtraContent(tmpDir: string, file: string): Promise<void>;
75
78
  export declare function fetchExtraContentBinary(tmpDir: string, file: string): Promise<void>;
79
+ /**
80
+ * Write `content` to `filePath` atomically and TOCTOU-safely.
81
+ *
82
+ * A predictable tmp name like `settings.json.tmp` lets a local attacker
83
+ * pre-create that path as a symlink pointing at, say, ~/.ssh/authorized_keys.
84
+ * Defenses: random suffix so the tmp name can't be predicted, plus
85
+ * O_CREAT|O_EXCL so we refuse to open the path if anything already exists.
86
+ * Final rename(2) is the atomic step.
87
+ */
88
+ export declare function atomicWriteFile(filePath: string, content: string): void;
76
89
  export declare function withEsc<T>(prompt: Promise<T> & {
77
90
  cancel?: () => void;
78
91
  }): Promise<T>;
package/dist/utils.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
+ import crypto from "node:crypto";
3
4
  import fs from "node:fs";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
@@ -91,6 +92,8 @@ const CONTENT_FILES = [
91
92
  "CLAUDE.md",
92
93
  "skills-lock.json",
93
94
  ".claude/plugins.json",
95
+ ".agents/plugins/marketplace.json",
96
+ ".agents/plugins/install.json",
94
97
  ".claude/hooks/hooks.json",
95
98
  ];
96
99
  async function fetchFile(file) {
@@ -134,6 +137,29 @@ export async function fetchExtraContentBinary(tmpDir, file) {
134
137
  fs.mkdirSync(path.dirname(dest), { recursive: true });
135
138
  fs.writeFileSync(dest, buf);
136
139
  }
140
+ /**
141
+ * Write `content` to `filePath` atomically and TOCTOU-safely.
142
+ *
143
+ * A predictable tmp name like `settings.json.tmp` lets a local attacker
144
+ * pre-create that path as a symlink pointing at, say, ~/.ssh/authorized_keys.
145
+ * Defenses: random suffix so the tmp name can't be predicted, plus
146
+ * O_CREAT|O_EXCL so we refuse to open the path if anything already exists.
147
+ * Final rename(2) is the atomic step.
148
+ */
149
+ export function atomicWriteFile(filePath, content) {
150
+ const dir = path.dirname(filePath);
151
+ const base = path.basename(filePath);
152
+ const suffix = crypto.randomBytes(8).toString("hex");
153
+ const tmp = path.join(dir, `.${base}.${suffix}.tmp`);
154
+ const fd = fs.openSync(tmp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
155
+ try {
156
+ fs.writeSync(fd, content);
157
+ }
158
+ finally {
159
+ fs.closeSync(fd);
160
+ }
161
+ fs.renameSync(tmp, filePath);
162
+ }
137
163
  // --- ESC support ---
138
164
  export function withEsc(prompt) {
139
165
  const onKeypress = (_, key) => {
@@ -294,8 +320,8 @@ export function printBanner(version) {
294
320
  ? renderBannerPlain(pixels)
295
321
  : renderBannerWithShadow(pixels, SHADOW_DX, SHADOW_DY);
296
322
  const subtitle = noColor
297
- ? ` Claude Code Harness Installer v${version}`
298
- : `${dim} Claude Code Harness Installer v${version}${reset}`;
323
+ ? ` Auriga Harness Installer v${version}`
324
+ : `${dim} Auriga Harness Installer v${version}${reset}`;
299
325
  console.log("");
300
326
  console.log(art);
301
327
  console.log(subtitle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auriga-cli",
3
- "version": "1.9.4",
3
+ "version": "1.10.1",
4
4
  "description": "Interactive CLI to install Claude Code harness modules (Workflow, Skills, Recommended Skills, Plugins, Hooks)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,17 +25,20 @@
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/hooks.test.js dist-test/tests/skills.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/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js",
29
- "test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/skills.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/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js",
28
+ "test": "tsc -p tsconfig.test.json && DEV=1 node --test --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/skills.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/plugins.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",
29
+ "test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/skills.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/plugins.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",
30
30
  "pretest:e2e": "npm run build",
31
- "test:e2e": "tsc -p tsconfig.test.json && node --test dist-test/tests/e2e-install.test.js"
31
+ "test:e2e": "tsc -p tsconfig.test.json && node --test dist-test/tests/e2e-install.test.js",
32
+ "test:session-instructions-loader": "node tests/session-instructions-loader.test.mjs",
33
+ "test:pr-guards": "node tests/pr-create-guard.test.mjs && node tests/pr-ready-guard.test.mjs"
32
34
  },
33
35
  "engines": {
34
36
  "node": ">=18"
35
37
  },
36
38
  "dependencies": {
37
39
  "@inquirer/prompts": "^8.0.0",
38
- "gray-matter": "^4.0.3"
40
+ "gray-matter": "^4.0.3",
41
+ "smol-toml": "^1.6.1"
39
42
  },
40
43
  "devDependencies": {
41
44
  "@types/node": "^22.0.0",