auriga-cli 1.10.0 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -13
- package/README.zh-CN.md +32 -13
- package/dist/catalog.json +7 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +47 -12
- package/dist/codex-plugin-config.d.ts +23 -0
- package/dist/codex-plugin-config.js +75 -0
- package/dist/guide.js +8 -4
- package/dist/help.js +9 -4
- package/dist/hooks.js +1 -27
- package/dist/plugins.js +379 -63
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +28 -2
- package/package.json +6 -4
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, auriga-pr-guards |
|
|
16
|
+
| **Plugins** | Recommended Claude Code and Codex plugins — skill-creator, claude-md-management, codex, auriga-go, auriga-pr-guards, session-instructions-loader |
|
|
17
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,15 +108,24 @@ Supports both project and global installation scopes.
|
|
|
99
108
|
|
|
100
109
|
### Plugins
|
|
101
110
|
|
|
102
|
-
Installs selected plugins
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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. |
|
|
111
129
|
|
|
112
130
|
### Hooks
|
|
113
131
|
|
|
@@ -128,7 +146,8 @@ Re-running the installer preserves your customized `config.json` and `icon.png`,
|
|
|
128
146
|
## Requirements
|
|
129
147
|
|
|
130
148
|
- Node.js >= 18
|
|
131
|
-
- [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`)
|
|
132
151
|
- [Homebrew](https://brew.sh) (recommended for the `notify` hook to install `alerter`)
|
|
133
152
|
|
|
134
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、auriga-pr-guards |
|
|
16
|
+
| **Plugins** | 推荐的 Claude Code 和 Codex 插件 —— skill-creator、claude-md-management、codex、auriga-go、auriga-pr-guards、session-instructions-loader |
|
|
17
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
|
|
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
|
-
|
|
70
|
+
每个模块在适用时支持作用域选择(Skills: project/global,Claude Code Plugins: user/project,Hooks: project local / project / user)。安装插件时还会先选择目标运行时:Claude Code、Codex 或两者都装。
|
|
62
71
|
|
|
63
72
|
## 模块详情
|
|
64
73
|
|
|
@@ -99,15 +108,24 @@ npx auriga-cli
|
|
|
99
108
|
|
|
100
109
|
### Plugins
|
|
101
110
|
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 文件。 |
|
|
111
129
|
|
|
112
130
|
### Hooks
|
|
113
131
|
|
|
@@ -128,7 +146,8 @@ npx auriga-cli
|
|
|
128
146
|
## 环境要求
|
|
129
147
|
|
|
130
148
|
- Node.js >= 18
|
|
131
|
-
- [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` 需要)
|
|
132
151
|
- [Homebrew](https://brew.sh)(`notify` hook 用来安装 `alerter`,可选)
|
|
133
152
|
|
|
134
153
|
## 开发
|
package/dist/catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-05-
|
|
2
|
+
"generatedAt": "2026-05-07T14:28:08.068Z",
|
|
3
3
|
"workflowSkills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "brainstorming",
|
|
@@ -75,11 +75,15 @@
|
|
|
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
79
|
},
|
|
80
80
|
{
|
|
81
81
|
"name": "auriga-pr-guards",
|
|
82
|
-
"description": "PR-create snapshot inject + PR-ready structural block guardrails (Claude Code + Codex)"
|
|
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"
|
|
83
87
|
}
|
|
84
88
|
],
|
|
85
89
|
"hooks": [
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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 =
|
|
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,
|
|
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,
|
|
95
|
-
registrations are loaded at
|
|
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>]
|
|
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
|
|
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 {
|
|
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, 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
|
|
77
|
-
|
|
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,268 @@ 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
|
+
return "codex plugin marketplace add https://github.com/Ben2pc/auriga-cli.git";
|
|
138
|
+
}
|
|
139
|
+
function codexMarketplaceUpgradeCommand(marketplaceName) {
|
|
140
|
+
return `codex plugin marketplace upgrade ${shellQuote(marketplaceName)}`;
|
|
141
|
+
}
|
|
142
|
+
function commandErrorText(error) {
|
|
143
|
+
if (!(error instanceof Error))
|
|
144
|
+
return String(error);
|
|
145
|
+
const parts = [error.message];
|
|
146
|
+
const withOutput = error;
|
|
147
|
+
if (withOutput.stdout)
|
|
148
|
+
parts.push(String(withOutput.stdout));
|
|
149
|
+
if (withOutput.stderr)
|
|
150
|
+
parts.push(String(withOutput.stderr));
|
|
151
|
+
return parts.join("\n");
|
|
152
|
+
}
|
|
153
|
+
function escapeRegex(value) {
|
|
154
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
155
|
+
}
|
|
156
|
+
function isCodexMarketplaceAlreadyAdded(error, marketplaceName) {
|
|
157
|
+
const text = commandErrorText(error);
|
|
158
|
+
const marketplacePattern = new RegExp(`marketplace ['"]?${escapeRegex(marketplaceName)}['"]? is already added`, "i");
|
|
159
|
+
return marketplacePattern.test(text)
|
|
160
|
+
|| /already added from a different source/i.test(text);
|
|
161
|
+
}
|
|
162
|
+
function pluginHasHooks(packageRoot, plugin) {
|
|
163
|
+
const relativeManifestPath = codexManifestPath(plugin);
|
|
164
|
+
if (!relativeManifestPath)
|
|
165
|
+
return false;
|
|
166
|
+
const manifestPath = path.join(packageRoot, relativeManifestPath);
|
|
167
|
+
if (!fs.existsSync(manifestPath))
|
|
168
|
+
return false;
|
|
169
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
170
|
+
return typeof manifest.hooks === "string" || Array.isArray(manifest.hooks);
|
|
171
|
+
}
|
|
172
|
+
async function ensureCodexPluginManifests(packageRoot, plugins) {
|
|
173
|
+
for (const plugin of plugins) {
|
|
174
|
+
const manifestPath = codexManifestPath(plugin);
|
|
175
|
+
if (!manifestPath) {
|
|
176
|
+
throw new Error(`Codex marketplace.json: plugin ${plugin.name} must use a local source.path`);
|
|
177
|
+
}
|
|
178
|
+
if (fs.existsSync(path.join(packageRoot, manifestPath)))
|
|
179
|
+
continue;
|
|
180
|
+
await fetchExtraContent(packageRoot, manifestPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function ensureTomlBoolean(content, section, key, value) {
|
|
184
|
+
const line = `${key} = ${value ? "true" : "false"}`;
|
|
185
|
+
const header = `[${section}]`;
|
|
186
|
+
const lines = content.length > 0 ? content.split(/\r?\n/) : [];
|
|
187
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
188
|
+
if (start === -1) {
|
|
189
|
+
const prefix = content.trimEnd();
|
|
190
|
+
return `${prefix}${prefix ? "\n\n" : ""}${header}\n${line}\n`;
|
|
191
|
+
}
|
|
192
|
+
let end = lines.length;
|
|
193
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
194
|
+
if (/^\s*\[/.test(lines[i])) {
|
|
195
|
+
end = i;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const keyRe = new RegExp(`^\\s*${key}\\s*=`);
|
|
200
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
201
|
+
if (keyRe.test(lines[i])) {
|
|
202
|
+
lines[i] = line;
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
lines.splice(end, 0, line);
|
|
207
|
+
return lines.join("\n");
|
|
208
|
+
}
|
|
209
|
+
function parseCodexConfigToml(content, configPath) {
|
|
210
|
+
if (content.trim().length === 0)
|
|
211
|
+
return {};
|
|
212
|
+
try {
|
|
213
|
+
return parseToml(content);
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
throw new Error(`Codex config.toml is invalid TOML at ${configPath}: ${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function isTomlTable(value) {
|
|
220
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
221
|
+
}
|
|
222
|
+
function getOrCreateTomlTable(parent, key, pathLabel) {
|
|
223
|
+
const existing = parent[key];
|
|
224
|
+
if (existing === undefined) {
|
|
225
|
+
const table = {};
|
|
226
|
+
parent[key] = table;
|
|
227
|
+
return table;
|
|
228
|
+
}
|
|
229
|
+
if (!isTomlTable(existing)) {
|
|
230
|
+
throw new Error(`Codex config.toml: ${pathLabel} must be a TOML table`);
|
|
231
|
+
}
|
|
232
|
+
return existing;
|
|
233
|
+
}
|
|
234
|
+
function buildCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks) {
|
|
235
|
+
const parsed = parseCodexConfigToml(originalContent, configPath);
|
|
236
|
+
const features = getOrCreateTomlTable(parsed, "features", "features");
|
|
237
|
+
features.plugins = true;
|
|
238
|
+
if (needsPluginHooks) {
|
|
239
|
+
features.plugin_hooks = true;
|
|
240
|
+
}
|
|
241
|
+
const plugins = getOrCreateTomlTable(parsed, "plugins", "plugins");
|
|
242
|
+
for (const pluginKey of pluginKeys) {
|
|
243
|
+
const plugin = getOrCreateTomlTable(plugins, pluginKey, `plugins.${JSON.stringify(pluginKey)}`);
|
|
244
|
+
plugin.enabled = true;
|
|
245
|
+
}
|
|
246
|
+
return stringifyToml(parsed);
|
|
247
|
+
}
|
|
248
|
+
function tryMinimalCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks) {
|
|
249
|
+
let content = originalContent;
|
|
250
|
+
content = ensureTomlBoolean(content, "features", "plugins", true);
|
|
251
|
+
if (needsPluginHooks) {
|
|
252
|
+
content = ensureTomlBoolean(content, "features", "plugin_hooks", true);
|
|
253
|
+
}
|
|
254
|
+
for (const pluginKey of pluginKeys) {
|
|
255
|
+
content = ensureTomlBoolean(content, `plugins."${pluginKey}"`, "enabled", true);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
parseToml(content);
|
|
259
|
+
return content;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Existing configs may use legal TOML forms such as inline tables
|
|
263
|
+
// (`features = { plugins = false }`). In that case, a local section
|
|
264
|
+
// insertion would redefine the table, so fall back to structured output.
|
|
265
|
+
parseCodexConfigToml(originalContent, configPath);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function enableCodexPluginConfig(configPath, pluginKeys, needsPluginHooks) {
|
|
270
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
271
|
+
const originalContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf-8") : "";
|
|
272
|
+
const minimalContent = tryMinimalCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks);
|
|
273
|
+
const content = minimalContent ?? buildCodexPluginConfigToml(originalContent, configPath, pluginKeys, needsPluginHooks);
|
|
274
|
+
atomicWriteFile(configPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
275
|
+
}
|
|
276
|
+
async function installCodexPlugins(packageRoot, opts) {
|
|
277
|
+
const marketplace = loadCodexMarketplace(packageRoot);
|
|
278
|
+
if (!marketplace) {
|
|
279
|
+
const msg = "No .agents/plugins/marketplace.json found";
|
|
280
|
+
if (!opts.interactive)
|
|
281
|
+
throw new Error(msg);
|
|
282
|
+
log.warn(msg);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const installConfig = loadCodexInstallConfig(packageRoot);
|
|
286
|
+
if (!installConfig) {
|
|
287
|
+
const msg = "No .agents/plugins/install.json found";
|
|
288
|
+
if (!opts.interactive)
|
|
289
|
+
throw new Error(msg);
|
|
290
|
+
log.warn(msg);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const marketplaceByName = new Map(marketplace.plugins.map((p) => [p.name, p]));
|
|
294
|
+
const selected = opts.interactive
|
|
295
|
+
? await withEsc(checkbox({
|
|
296
|
+
message: "Select Codex plugins to install:",
|
|
297
|
+
choices: installConfig.plugins.map((p) => ({
|
|
298
|
+
name: p.description ? `${p.name} — ${p.description}` : p.name,
|
|
299
|
+
value: p,
|
|
300
|
+
checked: p.defaultOn !== false,
|
|
301
|
+
})),
|
|
302
|
+
}))
|
|
303
|
+
: resolveCodexPluginSelection(installConfig.plugins, opts.selected);
|
|
304
|
+
if (selected.length === 0) {
|
|
305
|
+
log.skip("No Codex plugins selected");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const failures = [];
|
|
309
|
+
const marketplaceExecOpts = opts.interactive ? { inherit: true } : undefined;
|
|
310
|
+
try {
|
|
311
|
+
exec(codexMarketplaceAddCommand(packageRoot), marketplaceExecOpts);
|
|
312
|
+
log.ok(`Codex marketplace ${marketplace.name} added`);
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
if (opts.interactive || isCodexMarketplaceAlreadyAdded(e, marketplace.name)) {
|
|
316
|
+
try {
|
|
317
|
+
exec(codexMarketplaceUpgradeCommand(marketplace.name), marketplaceExecOpts);
|
|
318
|
+
log.ok(`Codex marketplace ${marketplace.name} upgraded`);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
log.error(`Failed to upgrade Codex marketplace: ${marketplace.name}`);
|
|
322
|
+
failures.push(`codex marketplace ${marketplace.name}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
log.error(`Failed to add Codex marketplace: ${marketplace.name}`);
|
|
327
|
+
failures.push(`codex marketplace ${marketplace.name}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (failures.length === 0) {
|
|
331
|
+
const selectedMarketplacePlugins = selected.map((p) => {
|
|
332
|
+
const plugin = marketplaceByName.get(p.name);
|
|
333
|
+
if (!plugin) {
|
|
334
|
+
throw new Error(`Codex install.json: plugin ${p.name} is not present in marketplace.json`);
|
|
335
|
+
}
|
|
336
|
+
return plugin;
|
|
337
|
+
});
|
|
338
|
+
await ensureCodexPluginManifests(packageRoot, selectedMarketplacePlugins);
|
|
339
|
+
const pluginKeys = selectedMarketplacePlugins.map((p) => `${p.name}@${marketplace.name}`);
|
|
340
|
+
const needsPluginHooks = selectedMarketplacePlugins.some((p) => pluginHasHooks(packageRoot, p));
|
|
341
|
+
enableCodexPluginConfig(path.join(codexHome(), "config.toml"), pluginKeys, needsPluginHooks);
|
|
342
|
+
for (const plugin of selected) {
|
|
343
|
+
log.ok(`${plugin.name} enabled for Codex`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (failures.length > 0 && !opts.interactive) {
|
|
347
|
+
throw new Error(`${failures.length} Codex plugin operation(s) failed: ${failures.join(", ")}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
92
350
|
export async function installPlugins(packageRoot, opts) {
|
|
351
|
+
const agent = opts.interactive
|
|
352
|
+
? await withEsc(select({
|
|
353
|
+
message: "Plugins target runtime:",
|
|
354
|
+
choices: [
|
|
355
|
+
{ name: "Claude Code", value: "claude" },
|
|
356
|
+
{ name: "Codex", value: "codex" },
|
|
357
|
+
{ name: "Both", value: "both" },
|
|
358
|
+
],
|
|
359
|
+
}))
|
|
360
|
+
: opts.agent ?? "claude";
|
|
93
361
|
// Non-interactive path already ran `precheckExternal(["plugins"])` in
|
|
94
362
|
// cli.ts's runAll / runSingle before dispatching here, so rechecking
|
|
95
363
|
// `which claude` would be a redundant subprocess on every install.
|
|
@@ -97,25 +365,51 @@ export async function installPlugins(packageRoot, opts) {
|
|
|
97
365
|
// validate there — and fail soft (log-and-return) to match the menu's
|
|
98
366
|
// continue-on-failure ergonomics.
|
|
99
367
|
if (opts.interactive) {
|
|
100
|
-
|
|
101
|
-
|
|
368
|
+
if (agent === "claude" || agent === "both") {
|
|
369
|
+
try {
|
|
370
|
+
exec("which claude");
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
log.error("'claude' CLI not found. Please install Claude Code first.");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
102
376
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
377
|
+
if (agent === "codex" || agent === "both") {
|
|
378
|
+
try {
|
|
379
|
+
exec("which codex");
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
log.error("'codex' CLI not found. Please install Codex first.");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
106
385
|
}
|
|
107
386
|
}
|
|
387
|
+
if (agent === "codex") {
|
|
388
|
+
await installCodexPlugins(packageRoot, opts);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const failures = [];
|
|
108
392
|
const configPath = path.join(packageRoot, ".claude", "plugins.json");
|
|
393
|
+
let config = null;
|
|
109
394
|
if (!fs.existsSync(configPath)) {
|
|
110
395
|
log.warn("No .claude/plugins.json found");
|
|
111
|
-
|
|
396
|
+
if (agent === "both")
|
|
397
|
+
failures.push("Claude Code plugins config missing");
|
|
398
|
+
else
|
|
399
|
+
return;
|
|
112
400
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
401
|
+
else {
|
|
402
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
403
|
+
validatePluginsConfig(raw);
|
|
404
|
+
config = raw;
|
|
405
|
+
if (config.plugins.length === 0) {
|
|
406
|
+
log.warn("No plugins defined in plugins.json");
|
|
407
|
+
if (agent === "both")
|
|
408
|
+
failures.push("Claude Code plugins config empty");
|
|
409
|
+
else
|
|
410
|
+
return;
|
|
411
|
+
config = null;
|
|
412
|
+
}
|
|
119
413
|
}
|
|
120
414
|
const scope = opts.interactive
|
|
121
415
|
? await withEsc(select({
|
|
@@ -126,60 +420,82 @@ export async function installPlugins(packageRoot, opts) {
|
|
|
126
420
|
],
|
|
127
421
|
}))
|
|
128
422
|
: opts.scope ?? "project";
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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}...`);
|
|
423
|
+
if (config) {
|
|
424
|
+
const installed = getInstalledPlugins();
|
|
425
|
+
let selected;
|
|
159
426
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
427
|
+
selected = opts.interactive
|
|
428
|
+
? await withEsc(checkbox({
|
|
429
|
+
message: "Select plugins to install:",
|
|
430
|
+
choices: config.plugins.map((p) => {
|
|
431
|
+
const scopes = installed.get(p.package);
|
|
432
|
+
const suffix = scopes ? ` (installed: ${scopes.join(", ")})` : "";
|
|
433
|
+
return {
|
|
434
|
+
name: `${p.name} — ${p.description}${suffix}`,
|
|
435
|
+
value: p,
|
|
436
|
+
checked: !scopes || !(scopes.includes("user") && scopes.includes("project")),
|
|
437
|
+
};
|
|
438
|
+
}),
|
|
439
|
+
}))
|
|
440
|
+
: resolvePluginSelection(config.plugins, opts.selected);
|
|
162
441
|
}
|
|
163
|
-
catch {
|
|
164
|
-
|
|
165
|
-
|
|
442
|
+
catch (e) {
|
|
443
|
+
if (agent !== "both")
|
|
444
|
+
throw e;
|
|
445
|
+
selected = [];
|
|
446
|
+
failures.push(`Claude Code: ${e.message}`);
|
|
166
447
|
}
|
|
167
|
-
|
|
168
|
-
|
|
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`);
|
|
448
|
+
if (selected.length === 0) {
|
|
449
|
+
log.skip("No plugins selected");
|
|
176
450
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
451
|
+
else {
|
|
452
|
+
// Install required marketplaces
|
|
453
|
+
const existingMarketplaces = getInstalledMarketplaces();
|
|
454
|
+
const marketplacesToAdd = new Map();
|
|
455
|
+
for (const plugin of selected) {
|
|
456
|
+
if (plugin.marketplace && !existingMarketplaces.has(plugin.marketplace.name)) {
|
|
457
|
+
marketplacesToAdd.set(plugin.marketplace.name, plugin.marketplace.source);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const [name, source] of marketplacesToAdd) {
|
|
461
|
+
console.log(`\nAdding marketplace: ${name}...`);
|
|
462
|
+
try {
|
|
463
|
+
exec(`claude plugins marketplace add ${source}`, { inherit: true });
|
|
464
|
+
log.ok(`Marketplace ${name} added`);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
log.error(`Failed to add marketplace: ${name}`);
|
|
468
|
+
failures.push(`marketplace ${name}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Install plugins
|
|
472
|
+
for (const plugin of selected) {
|
|
473
|
+
console.log(`\nInstalling ${plugin.name}...`);
|
|
474
|
+
try {
|
|
475
|
+
exec(`claude plugins install ${plugin.package} --scope ${scope}`, {
|
|
476
|
+
inherit: true,
|
|
477
|
+
});
|
|
478
|
+
log.ok(`${plugin.name} installed`);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
log.error(`Failed to install: ${plugin.name}`);
|
|
482
|
+
failures.push(plugin.name);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
180
485
|
}
|
|
181
486
|
}
|
|
182
|
-
if (failures.length > 0 && !opts.interactive) {
|
|
487
|
+
if (failures.length > 0 && !opts.interactive && agent !== "both") {
|
|
183
488
|
throw new Error(`${failures.length} plugin operation(s) failed: ${failures.join(", ")}`);
|
|
184
489
|
}
|
|
490
|
+
if (agent === "both") {
|
|
491
|
+
try {
|
|
492
|
+
await installCodexPlugins(packageRoot, opts);
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
failures.push(`codex: ${e.message}`);
|
|
496
|
+
}
|
|
497
|
+
if (failures.length > 0 && !opts.interactive) {
|
|
498
|
+
throw new Error(`${failures.length} plugin operation(s) failed: ${failures.join(", ")}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
185
501
|
}
|
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
|
-
? `
|
|
298
|
-
: `${dim}
|
|
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.10.
|
|
3
|
+
"version": "1.10.2",
|
|
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,10 +25,11 @@
|
|
|
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
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",
|
|
32
33
|
"test:pr-guards": "node tests/pr-create-guard.test.mjs && node tests/pr-ready-guard.test.mjs"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@inquirer/prompts": "^8.0.0",
|
|
39
|
-
"gray-matter": "^4.0.3"
|
|
40
|
+
"gray-matter": "^4.0.3",
|
|
41
|
+
"smol-toml": "^1.6.1"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
44
|
"@types/node": "^22.0.0",
|