harnessbrew 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +88 -9
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +184 -26
- package/dist/cli.js.map +1 -1
- package/dist/core/adapter-plugins.d.ts +12 -0
- package/dist/core/adapter-plugins.d.ts.map +1 -0
- package/dist/core/adapter-plugins.js +151 -0
- package/dist/core/adapter-plugins.js.map +1 -0
- package/dist/core/bundle.d.ts +33 -5
- package/dist/core/bundle.d.ts.map +1 -1
- package/dist/core/bundle.js +329 -60
- package/dist/core/bundle.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -2
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +2 -0
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/git.d.ts +1 -0
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js +17 -0
- package/dist/core/git.js.map +1 -1
- package/dist/core/installations.d.ts +1 -0
- package/dist/core/installations.d.ts.map +1 -1
- package/dist/core/installations.js +135 -14
- package/dist/core/installations.js.map +1 -1
- package/dist/core/journal.d.ts +26 -0
- package/dist/core/journal.d.ts.map +1 -0
- package/dist/core/journal.js +363 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/locks.d.ts +10 -0
- package/dist/core/locks.d.ts.map +1 -0
- package/dist/core/locks.js +90 -0
- package/dist/core/locks.js.map +1 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +3 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/state.d.ts +2 -0
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +21 -2
- package/dist/core/state.js.map +1 -1
- package/dist/core/taps.d.ts +7 -1
- package/dist/core/taps.d.ts.map +1 -1
- package/dist/core/taps.js +74 -16
- package/dist/core/taps.js.map +1 -1
- package/dist/core/targets/claude-code.d.ts.map +1 -1
- package/dist/core/targets/claude-code.js +11 -1
- package/dist/core/targets/claude-code.js.map +1 -1
- package/dist/core/targets/codex.d.ts.map +1 -1
- package/dist/core/targets/codex.js +11 -1
- package/dist/core/targets/codex.js.map +1 -1
- package/dist/core/targets/planner.d.ts +2 -3
- package/dist/core/targets/planner.d.ts.map +1 -1
- package/dist/core/targets/planner.js +43 -3
- package/dist/core/targets/planner.js.map +1 -1
- package/dist/core/targets/registry.d.ts +7 -3
- package/dist/core/targets/registry.d.ts.map +1 -1
- package/dist/core/targets/registry.js +55 -0
- package/dist/core/targets/registry.js.map +1 -1
- package/dist/core/targets/transaction.d.ts.map +1 -1
- package/dist/core/targets/transaction.js +46 -10
- package/dist/core/targets/transaction.js.map +1 -1
- package/dist/core/targets/types.d.ts +8 -4
- package/dist/core/targets/types.d.ts.map +1 -1
- package/dist/core/targets/types.js +1 -1
- package/dist/core/targets/types.js.map +1 -1
- package/dist/core/targets.d.ts +6 -7
- package/dist/core/targets.d.ts.map +1 -1
- package/dist/core/targets.js +33 -22
- package/dist/core/targets.js.map +1 -1
- package/dist/core/upgrades.d.ts.map +1 -1
- package/dist/core/upgrades.js +10 -1
- package/dist/core/upgrades.js.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.6.0 - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- add a versioned JSON CLI envelope with command-level results and structured errors
|
|
10
|
+
- add transactional `--dry-run` previews that report path fingerprints and roll back all managed changes
|
|
11
|
+
- add Target Adapter API v1 with validated registration, Cellar-bounded symlink plans, transaction-managed execution, and lockfile version fingerprints
|
|
12
|
+
- add explicit CLI management for trusted Adapter modules with persisted identity verification and transactional previews
|
|
13
|
+
|
|
14
|
+
### Security
|
|
15
|
+
|
|
16
|
+
- validate complete Receipt structures, canonical Cellar locations, operation metadata, owned keys, and all destructive paths before use
|
|
17
|
+
- detect added, removed, modified, unsupported, and permission-changed Cellar files through exact inventory comparison
|
|
18
|
+
- prevent forced cleanup from recursively deleting a real directory that replaced a managed directory symlink
|
|
19
|
+
- serialize CLI mutations per HarnessBrew home and protect shared Target configuration updates across processes
|
|
20
|
+
- recover interrupted mutations from durable write-ahead journals, including abandoned process locks and external Target files
|
|
21
|
+
- add Harnessfile and lockfile v2 with structured scopes, portable paths, content digests, explicit lock refresh, and Target convergence
|
|
22
|
+
- require explicit Tap trust before Target activation, enforce fast-forward updates by default, and roll back invalid Tap candidates
|
|
23
|
+
|
|
5
24
|
## 0.5.2 - 2026-08-13
|
|
6
25
|
|
|
7
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ HarnessBrew 默认将受管状态保存到 `~/.harnessbrew`。测试或隔离环
|
|
|
41
41
|
注册自己的资产 Tap:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
harnessbrew tap add xiejinheng/agents git@github.com:xiejinheng/agent-assets.git
|
|
44
|
+
harnessbrew tap add xiejinheng/agents git@github.com:xiejinheng/agent-assets.git --trust
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
搜索并查看 Formula:
|
|
@@ -64,7 +64,7 @@ harnessbrew install xiejinheng/agents/code-review \
|
|
|
64
64
|
更新和升级:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
harnessbrew update
|
|
67
|
+
harnessbrew update [--allow-rewind]
|
|
68
68
|
harnessbrew outdated
|
|
69
69
|
harnessbrew upgrade code-review
|
|
70
70
|
```
|
|
@@ -140,7 +140,8 @@ Git commit 是安装版本的唯一事实来源;Formula 不维护 `.snapshots`
|
|
|
140
140
|
|
|
141
141
|
MCP Formula 的入口是统一 JSON。stdio 配置使用 `command`、可选 `args` 和只包含环境变量名称的 `envVars`;HTTP 配置使用 `transport: "http"`、`url`、可选 `bearerTokenEnvVar` 与 `headersFromEnv`。HarnessBrew 不接受入口中的明文 `env` 密钥值。
|
|
142
142
|
|
|
143
|
-
`adapter` Formula
|
|
143
|
+
`adapter` Formula 是可由 Git/Cellar 管理的扩展资产,但不会作为代码自动执行。Target Adapter 运行时使用单独的可信模块机制:先审查并安装 npm 包,
|
|
144
|
+
再通过 `harnessbrew adapter add <module>` 显式授权。这样 Tap 仍保持声明式、不执行任意代码,而第三方 Target 可以接入版本化 Adapter SDK。
|
|
144
145
|
|
|
145
146
|
## Target
|
|
146
147
|
|
|
@@ -187,26 +188,41 @@ harnessbrew unlink code-review --target openai-codex
|
|
|
187
188
|
`Harnessfile` 适合提交到个人 dotfiles 或项目仓库:
|
|
188
189
|
|
|
189
190
|
```yaml
|
|
190
|
-
schemaVersion:
|
|
191
|
+
schemaVersion: 2
|
|
191
192
|
taps:
|
|
192
193
|
- name: xiejinheng/agents
|
|
193
194
|
git: git@github.com:xiejinheng/agent-assets.git
|
|
194
195
|
ref: main
|
|
196
|
+
trust: true
|
|
195
197
|
|
|
196
198
|
assets:
|
|
197
199
|
- formula: xiejinheng/agents/code-review
|
|
198
|
-
targets:
|
|
200
|
+
targets:
|
|
201
|
+
- target: openai-codex
|
|
202
|
+
scope: user
|
|
203
|
+
- target: claude-code
|
|
204
|
+
scope: project
|
|
205
|
+
project: .
|
|
199
206
|
```
|
|
200
207
|
|
|
208
|
+
v2 的 Target placement 可以声明 `user` 或 `project` scope,并可使用相对于 `Harnessfile` 的 `project` 和 `root` 路径。
|
|
209
|
+
schema v1 的 `targets: [openai-codex]` 仍然兼容,并按 user scope 解释。
|
|
210
|
+
|
|
201
211
|
安装并生成 `Harnessfile.lock`:
|
|
202
212
|
|
|
203
213
|
```bash
|
|
204
214
|
harnessbrew bundle install
|
|
205
215
|
```
|
|
206
216
|
|
|
207
|
-
lockfile
|
|
217
|
+
v2 lockfile 会记录 Manifest 摘要、HarnessBrew Adapter 版本、每个 Tap 的准确 commit、Formula 内容摘要、依赖闭包和完整 Target placement,
|
|
218
|
+
应与 `Harnessfile` 一起提交到 Git。
|
|
208
219
|
|
|
209
220
|
在其他设备运行同一命令时,HarnessBrew 会检出 lockfile 固定的 commit,而不是未经确认地使用 Tap 最新版本。
|
|
221
|
+
修改 v2 Harnessfile 后需要显式更新 lockfile:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
harnessbrew bundle install --update-lock
|
|
225
|
+
```
|
|
210
226
|
|
|
211
227
|
清理清单之外的受管资产:
|
|
212
228
|
|
|
@@ -223,9 +239,11 @@ harnessbrew bundle install --file ./config/Harnessfile
|
|
|
223
239
|
## CLI
|
|
224
240
|
|
|
225
241
|
```text
|
|
226
|
-
harnessbrew tap add <owner/name> <git-url> [--ref <ref>]
|
|
242
|
+
harnessbrew tap add <owner/name> <git-url> [--ref <ref>] [--trust]
|
|
227
243
|
harnessbrew tap list
|
|
228
|
-
harnessbrew tap update [owner/name]
|
|
244
|
+
harnessbrew tap update [owner/name] [--allow-rewind]
|
|
245
|
+
harnessbrew tap trust <owner/name>
|
|
246
|
+
harnessbrew tap untrust <owner/name>
|
|
229
247
|
harnessbrew tap remove <owner/name>
|
|
230
248
|
harnessbrew untap <owner/name>
|
|
231
249
|
harnessbrew search [query] [--kind <kind>] [--target <target>]
|
|
@@ -240,10 +258,20 @@ harnessbrew update
|
|
|
240
258
|
harnessbrew outdated
|
|
241
259
|
harnessbrew upgrade [formula]
|
|
242
260
|
harnessbrew uninstall <formula> [--force]
|
|
243
|
-
harnessbrew bundle install [--file <path>]
|
|
261
|
+
harnessbrew bundle install [--file <path>] [--update-lock]
|
|
244
262
|
harnessbrew bundle cleanup [--file <path>]
|
|
245
263
|
```
|
|
246
264
|
|
|
265
|
+
所有命令都可追加 `--json`,stdout 将只包含一个 schema v1 JSON envelope:`result` 是命令级结构化结果,`output` 保留人类文本,
|
|
266
|
+
失败时包含 `error.code`、`error.message`、`diagnostics` 和非零 `exitCode`。
|
|
267
|
+
|
|
268
|
+
变更命令可追加 `--dry-run`。HarnessBrew 会在同一 Home 写锁下完整执行校验和安装事务,收集每个路径的 before/after 类型与摘要,
|
|
269
|
+
随后回滚 Cellar、Receipt、Tap checkout 和 Agent Target;与 `--json` 组合时,预览位于 `changes` 数组。dry-run 可能执行 Git fetch/clone 等只读网络操作。
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
harnessbrew install code-review --target openai-codex --dry-run --json
|
|
273
|
+
```
|
|
274
|
+
|
|
247
275
|
## 本地目录
|
|
248
276
|
|
|
249
277
|
```text
|
|
@@ -259,6 +287,8 @@ Tap 工作树和 Cellar 内容都由 HarnessBrew 管理,不应直接编辑。
|
|
|
259
287
|
## 安全边界
|
|
260
288
|
|
|
261
289
|
- Formula 是声明式 JSON;HarnessBrew 不执行 Tap 中的任意脚本。
|
|
290
|
+
- 新注册的 Tap 默认不受信任:可以搜索和安装到 Cellar,但必须通过 `tap trust`、`tap add --trust` 或 Harnessfile v2 的 `trust: true` 才能链接或渲染到 Agent Target。旧状态记录按兼容策略视为已信任。
|
|
291
|
+
- Tap 更新默认只接受 Git fast-forward;仓库历史被重写时必须人工检查后使用 `--allow-rewind`。候选 commit 校验失败会恢复原 checkout 和状态。
|
|
262
292
|
- Formula 入口不能逃逸所属目录。
|
|
263
293
|
- 安装前会检查依赖、冲突和 target 兼容性。
|
|
264
294
|
- HarnessBrew 不覆盖未由 Receipt 管理的目标文件。
|
|
@@ -278,6 +308,55 @@ npm run check
|
|
|
278
308
|
|
|
279
309
|
`npm run check` 会执行 TypeScript 编译、全部 Node.js 测试、安装包冒烟测试和 `npm pack --dry-run`。
|
|
280
310
|
|
|
311
|
+
## Target Adapter SDK
|
|
312
|
+
|
|
313
|
+
Node.js/TypeScript 宿主可以通过公开 API 注册第三方 Agent Target。Adapter API v1 只接收 Receipt 与 Target Context,并返回一条声明式安装计划;
|
|
314
|
+
实际写入、冲突检测、Receipt、doctor、relink、upgrade、dry-run 和回滚仍由 HarnessBrew transaction layer 负责。
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { registerTargetAdapter, type TargetAdapter } from "harnessbrew";
|
|
318
|
+
|
|
319
|
+
const adapter: TargetAdapter = {
|
|
320
|
+
apiVersion: 1,
|
|
321
|
+
name: "cursor",
|
|
322
|
+
version: "1.0.0",
|
|
323
|
+
capabilities: {
|
|
324
|
+
skill: "symlink-directory",
|
|
325
|
+
agent: "symlink-file",
|
|
326
|
+
workflow: "symlink-file",
|
|
327
|
+
instruction: "symlink-file",
|
|
328
|
+
prompt: "symlink-file",
|
|
329
|
+
mcp: "unsupported",
|
|
330
|
+
adapter: "unsupported"
|
|
331
|
+
},
|
|
332
|
+
plan(receipt, context = {}) {
|
|
333
|
+
// Return one absolute destination and a source inside receipt.cellarPath.
|
|
334
|
+
return { target: "cursor", coordinate: receipt.coordinate, operations: [/* ... */] };
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const unregister = registerTargetAdapter(adapter);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
SDK 会校验 API 版本、名称、版本、完整能力矩阵、计划身份、绝对目标路径和 Cellar source 边界。v1 的第三方 Adapter 计划只允许
|
|
342
|
+
`symlink-file`、`symlink-directory` 和 `unsupported`,不提供直接写文件或生成共享配置的操作。注册是显式、进程内操作;HarnessBrew 不会从 Tap
|
|
343
|
+
自动执行 Adapter Formula。第三方 Adapter 本身是具有宿主进程权限的可信代码,只应加载经过审查的 npm 包。
|
|
344
|
+
|
|
345
|
+
独立 CLI 可以持久化管理可信 Adapter 模块。模块需已通过 npm 安装并可被 `harnessbrew` 解析,也可以使用绝对路径或 `file://` URL;它必须默认导出
|
|
346
|
+
一个 Adapter,或提供名为 `adapter` 的导出:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
harnessbrew adapter add @harnessbrew/adapter-cursor
|
|
350
|
+
harnessbrew adapter list
|
|
351
|
+
harnessbrew install review --target cursor
|
|
352
|
+
harnessbrew adapter remove cursor
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`adapter add` 是一次显式的代码执行授权。HarnessBrew 将模块标识及审核时的 name、version、API version 写入
|
|
356
|
+
`~/.harnessbrew/adapters.json`;后续只在 install/link/unlink/relink/upgrade/bundle 需要 Target 时加载,并在每次加载时核对身份。
|
|
357
|
+
如果包升级改变身份,命令会关闭失败,要求先 remove、审查后再 add。`adapter list/remove` 本身不执行插件,HarnessBrew 也不会自动运行 `npm install`。
|
|
358
|
+
CLI 加载的 Target 同样进入 Harnessfile v2 lock 的 Adapter 签名。
|
|
359
|
+
|
|
281
360
|
## 架构
|
|
282
361
|
|
|
283
362
|
完整设计见 [docs/architecture.md](docs/architecture.md)。
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TransactionChange } from "./core/journal.js";
|
|
1
2
|
export interface CliIO {
|
|
2
3
|
stdout: (message: string) => void;
|
|
3
4
|
stderr: (message: string) => void;
|
|
@@ -5,5 +6,21 @@ export interface CliIO {
|
|
|
5
6
|
export interface CliOptions {
|
|
6
7
|
home?: string;
|
|
7
8
|
}
|
|
9
|
+
export interface CliJsonError {
|
|
10
|
+
code: "COMMAND_FAILED" | "HARNESSBREW_ERROR";
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CliJsonEnvelope {
|
|
14
|
+
schemaVersion: 1;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
command: string;
|
|
17
|
+
exitCode: number;
|
|
18
|
+
dryRun: boolean;
|
|
19
|
+
result: unknown;
|
|
20
|
+
output: string[];
|
|
21
|
+
diagnostics: string[];
|
|
22
|
+
error?: CliJsonError;
|
|
23
|
+
changes?: TransactionChange[];
|
|
24
|
+
}
|
|
8
25
|
export declare function runCli(args: readonly string[], io?: CliIO, options?: CliOptions): Promise<number>;
|
|
9
26
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAyBA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,CAAC,CAAC;IACjB,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AA4XD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,GAAE,KAAiB,EACrB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,MAAM,CAAC,CA6FjB"}
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,20 @@ import path from "node:path";
|
|
|
2
2
|
import { HarnessBrewError } from "./core/errors.js";
|
|
3
3
|
import { formulaKinds, getFormula, searchFormulas } from "./core/formulas.js";
|
|
4
4
|
import { installFormula, listInstalled, uninstallFormula } from "./core/installations.js";
|
|
5
|
-
import {
|
|
5
|
+
import { installForTarget, linkFormula, unlinkFormula } from "./core/targets.js";
|
|
6
|
+
import { hasTargetAdapter } from "./core/targets/registry.js";
|
|
6
7
|
import { findOutdated, upgradeFormulas } from "./core/upgrades.js";
|
|
7
8
|
import { bundleCleanup, bundleInstall } from "./core/bundle.js";
|
|
8
9
|
import { resolveHarnessHome } from "./core/paths.js";
|
|
9
|
-
import { addTap, listTaps, removeTap, updateTaps } from "./core/taps.js";
|
|
10
|
+
import { addTap, listTaps, removeTap, setTapTrust, updateTaps } from "./core/taps.js";
|
|
10
11
|
import { VERSION } from "./version.js";
|
|
11
12
|
import { doctor, relinkFormula } from "./core/doctor.js";
|
|
13
|
+
import { addAdapterPlugin, listAdapterPlugins, loadAdapterPlugins, removeAdapterPlugin } from "./core/adapter-plugins.js";
|
|
14
|
+
import { withHomeLock } from "./core/locks.js";
|
|
15
|
+
import { withJournalPreview, withJournalTransaction } from "./core/journal.js";
|
|
16
|
+
function setResult(io, result) {
|
|
17
|
+
io.setResult?.(result);
|
|
18
|
+
}
|
|
12
19
|
const defaultIO = {
|
|
13
20
|
stdout: (message) => console.log(message),
|
|
14
21
|
stderr: (message) => console.error(message)
|
|
@@ -18,12 +25,12 @@ const helpText = `HarnessBrew ${VERSION}
|
|
|
18
25
|
A Git-native package manager for AI Agent assets.
|
|
19
26
|
|
|
20
27
|
Usage:
|
|
21
|
-
harnessbrew <command> [options]
|
|
28
|
+
harnessbrew <command> [options] [--json] [--dry-run]
|
|
22
29
|
|
|
23
30
|
Commands:
|
|
24
31
|
help Show this help
|
|
25
32
|
version Show the installed version
|
|
26
|
-
tap Add, list, update, or remove Git taps
|
|
33
|
+
tap Add, trust, list, update, or remove Git taps
|
|
27
34
|
untap Remove a Git tap
|
|
28
35
|
search Search formulas across registered taps
|
|
29
36
|
info Show formula metadata
|
|
@@ -38,11 +45,16 @@ Commands:
|
|
|
38
45
|
outdated List installed formulas with available changes
|
|
39
46
|
upgrade Upgrade formulas while preserving target links
|
|
40
47
|
bundle Rebuild or clean an environment from Harnessfile
|
|
48
|
+
adapter Add, list, or remove trusted Target Adapter modules
|
|
41
49
|
|
|
42
50
|
Target placement options:
|
|
43
51
|
--scope <user|project> Select user or project installation scope
|
|
44
52
|
--project <path> Project root (implies --scope project)
|
|
45
53
|
--target-root <path> Override the target's native root
|
|
54
|
+
|
|
55
|
+
Automation options:
|
|
56
|
+
--json Emit one versioned JSON result envelope
|
|
57
|
+
--dry-run Execute a mutation transaction, report changes, then roll it back
|
|
46
58
|
`;
|
|
47
59
|
function optionValue(args, name) {
|
|
48
60
|
const index = args.indexOf(name);
|
|
@@ -77,65 +89,118 @@ async function runTapCommand(args, home, io) {
|
|
|
77
89
|
const [action = "list", ...rest] = args;
|
|
78
90
|
if (action === "list") {
|
|
79
91
|
const taps = await listTaps(home);
|
|
80
|
-
taps.forEach((tap) => io.stdout(`${tap.name}\t${tap.commit.slice(0, 12)}\t${tap.url}`));
|
|
92
|
+
taps.forEach((tap) => io.stdout(`${tap.name}\t${tap.trusted ? "trusted" : "untrusted"}\t${tap.commit.slice(0, 12)}\t${tap.url}`));
|
|
93
|
+
setResult(io, taps);
|
|
81
94
|
return 0;
|
|
82
95
|
}
|
|
83
96
|
if (action === "add") {
|
|
84
97
|
const [name, url] = rest;
|
|
85
98
|
if (name === undefined || url === undefined) {
|
|
86
|
-
throw new HarnessBrewError("Usage: harnessbrew tap add <owner/name> <git-url> [--ref <ref>]");
|
|
99
|
+
throw new HarnessBrewError("Usage: harnessbrew tap add <owner/name> <git-url> [--ref <ref>] [--trust]");
|
|
87
100
|
}
|
|
88
101
|
const ref = optionValue(rest, "--ref");
|
|
89
|
-
const record = await addTap(home, name, url,
|
|
102
|
+
const record = await addTap(home, name, url, {
|
|
103
|
+
...(ref === undefined ? {} : { ref }),
|
|
104
|
+
trust: rest.includes("--trust")
|
|
105
|
+
});
|
|
90
106
|
io.stdout(`Tapped ${record.name} at ${record.commit.slice(0, 12)}.`);
|
|
107
|
+
setResult(io, record);
|
|
91
108
|
return 0;
|
|
92
109
|
}
|
|
93
110
|
if (action === "update") {
|
|
94
|
-
const
|
|
111
|
+
const requested = rest.find((value) => !value.startsWith("--"));
|
|
112
|
+
const updates = await updateTaps(home, requested, { allowRewind: rest.includes("--allow-rewind") });
|
|
95
113
|
updates.forEach((update) => {
|
|
96
114
|
io.stdout(update.changed
|
|
97
115
|
? `Updated ${update.name}: ${update.before.slice(0, 12)} -> ${update.after.slice(0, 12)}.`
|
|
98
116
|
: `${update.name} is already up-to-date.`);
|
|
99
117
|
});
|
|
118
|
+
setResult(io, updates);
|
|
100
119
|
return 0;
|
|
101
120
|
}
|
|
102
121
|
if (action === "remove") {
|
|
103
122
|
const [name] = rest;
|
|
104
123
|
if (name === undefined)
|
|
105
124
|
throw new HarnessBrewError("Usage: harnessbrew tap remove <owner/name>");
|
|
106
|
-
await removeTap(home, name);
|
|
125
|
+
const record = await removeTap(home, name);
|
|
107
126
|
io.stdout(`Untapped ${name}.`);
|
|
127
|
+
setResult(io, record);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
if (action === "trust" || action === "untrust") {
|
|
131
|
+
const [name] = rest;
|
|
132
|
+
if (name === undefined)
|
|
133
|
+
throw new HarnessBrewError(`Usage: harnessbrew tap ${action} <owner/name>`);
|
|
134
|
+
const record = await setTapTrust(home, name, action === "trust");
|
|
135
|
+
io.stdout(`${record.name} is now ${record.trusted ? "trusted" : "untrusted"}.`);
|
|
136
|
+
setResult(io, record);
|
|
108
137
|
return 0;
|
|
109
138
|
}
|
|
110
139
|
throw new HarnessBrewError(`Unknown tap action: ${action}`);
|
|
111
140
|
}
|
|
141
|
+
async function runAdapterCommand(args, home, io) {
|
|
142
|
+
const [action = "list", ...rest] = args;
|
|
143
|
+
if (action === "list") {
|
|
144
|
+
const records = await listAdapterPlugins(home);
|
|
145
|
+
records.forEach((record) => io.stdout(`${record.name}\t${record.version}\t${record.module}`));
|
|
146
|
+
setResult(io, records);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
if (action === "add") {
|
|
150
|
+
const [moduleSpecifier] = rest;
|
|
151
|
+
if (moduleSpecifier === undefined) {
|
|
152
|
+
throw new HarnessBrewError("Usage: harnessbrew adapter add <npm-package|absolute-path|file-url>");
|
|
153
|
+
}
|
|
154
|
+
const record = await addAdapterPlugin(home, moduleSpecifier);
|
|
155
|
+
io.stdout(`Added trusted Adapter ${record.name}@${record.version} from ${record.module}.`);
|
|
156
|
+
setResult(io, record);
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
if (action === "remove") {
|
|
160
|
+
const [name] = rest;
|
|
161
|
+
if (name === undefined)
|
|
162
|
+
throw new HarnessBrewError("Usage: harnessbrew adapter remove <target>");
|
|
163
|
+
const record = await removeAdapterPlugin(home, name);
|
|
164
|
+
io.stdout(`Removed Adapter ${record.name}@${record.version}.`);
|
|
165
|
+
setResult(io, record);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
throw new HarnessBrewError(`Unknown adapter action: ${action}`);
|
|
169
|
+
}
|
|
112
170
|
async function execute(args, io, options) {
|
|
113
171
|
const [command] = args;
|
|
114
172
|
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
115
173
|
io.stdout(helpText.trimEnd());
|
|
174
|
+
setResult(io, { help: helpText.trimEnd() });
|
|
116
175
|
return 0;
|
|
117
176
|
}
|
|
118
177
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
119
178
|
io.stdout(VERSION);
|
|
179
|
+
setResult(io, { version: VERSION });
|
|
120
180
|
return 0;
|
|
121
181
|
}
|
|
122
182
|
const home = resolveHarnessHome(options.home);
|
|
183
|
+
if (command === "adapter") {
|
|
184
|
+
return runAdapterCommand(args.slice(1), home, io);
|
|
185
|
+
}
|
|
123
186
|
if (command === "tap") {
|
|
124
187
|
return runTapCommand(args.slice(1), home, io);
|
|
125
188
|
}
|
|
126
189
|
if (command === "update") {
|
|
127
|
-
const updates = await updateTaps(home);
|
|
190
|
+
const updates = await updateTaps(home, undefined, { allowRewind: args.includes("--allow-rewind") });
|
|
128
191
|
updates.forEach((update) => io.stdout(update.changed
|
|
129
192
|
? `Updated ${update.name}: ${update.before.slice(0, 12)} -> ${update.after.slice(0, 12)}.`
|
|
130
193
|
: `${update.name} is already up-to-date.`));
|
|
194
|
+
setResult(io, updates);
|
|
131
195
|
return 0;
|
|
132
196
|
}
|
|
133
197
|
if (command === "untap") {
|
|
134
198
|
const name = args[1];
|
|
135
199
|
if (name === undefined)
|
|
136
200
|
throw new HarnessBrewError("Usage: harnessbrew untap <owner/name>");
|
|
137
|
-
await removeTap(home, name);
|
|
201
|
+
const record = await removeTap(home, name);
|
|
138
202
|
io.stdout(`Untapped ${name}.`);
|
|
203
|
+
setResult(io, record);
|
|
139
204
|
return 0;
|
|
140
205
|
}
|
|
141
206
|
if (command === "search") {
|
|
@@ -154,6 +219,7 @@ async function execute(args, io, options) {
|
|
|
154
219
|
...(target === undefined ? {} : { target })
|
|
155
220
|
});
|
|
156
221
|
formulas.forEach((formula) => io.stdout(`${formula.coordinate}\t${formula.kind}\t${formula.description}`));
|
|
222
|
+
setResult(io, formulas);
|
|
157
223
|
return 0;
|
|
158
224
|
}
|
|
159
225
|
if (command === "info") {
|
|
@@ -161,7 +227,7 @@ async function execute(args, io, options) {
|
|
|
161
227
|
if (name === undefined)
|
|
162
228
|
throw new HarnessBrewError("Usage: harnessbrew info <formula>");
|
|
163
229
|
const formula = await getFormula(home, name);
|
|
164
|
-
|
|
230
|
+
const result = {
|
|
165
231
|
coordinate: formula.coordinate,
|
|
166
232
|
kind: formula.kind,
|
|
167
233
|
description: formula.description,
|
|
@@ -169,7 +235,9 @@ async function execute(args, io, options) {
|
|
|
169
235
|
dependencies: formula.dependencies,
|
|
170
236
|
commit: formula.commit,
|
|
171
237
|
...(formula.deprecated === undefined ? {} : { deprecated: formula.deprecated })
|
|
172
|
-
}
|
|
238
|
+
};
|
|
239
|
+
io.stdout(JSON.stringify(result, null, 2));
|
|
240
|
+
setResult(io, result);
|
|
173
241
|
return 0;
|
|
174
242
|
}
|
|
175
243
|
if (command === "install") {
|
|
@@ -177,18 +245,20 @@ async function execute(args, io, options) {
|
|
|
177
245
|
if (name === undefined)
|
|
178
246
|
throw new HarnessBrewError("Usage: harnessbrew install <formula>");
|
|
179
247
|
const targetValue = optionValue(args, "--target");
|
|
180
|
-
if (targetValue !== undefined && !
|
|
181
|
-
throw new HarnessBrewError(`
|
|
248
|
+
if (targetValue !== undefined && !hasTargetAdapter(targetValue)) {
|
|
249
|
+
throw new HarnessBrewError(`Target adapter is not registered: ${targetValue}`);
|
|
182
250
|
}
|
|
183
251
|
const receipts = targetValue === undefined
|
|
184
252
|
? await installFormula(home, name)
|
|
185
253
|
: await installForTarget(home, name, targetValue, targetOptions(args));
|
|
186
254
|
receipts.forEach((receipt) => io.stdout(`Installed ${receipt.coordinate} at ${receipt.commit.slice(0, 12)}.`));
|
|
255
|
+
setResult(io, receipts);
|
|
187
256
|
return 0;
|
|
188
257
|
}
|
|
189
258
|
if (command === "list") {
|
|
190
259
|
const receipts = await listInstalled(home);
|
|
191
260
|
receipts.forEach((receipt) => io.stdout(`${receipt.coordinate}\t${receipt.commit.slice(0, 12)}`));
|
|
261
|
+
setResult(io, receipts);
|
|
192
262
|
return 0;
|
|
193
263
|
}
|
|
194
264
|
if (command === "uninstall") {
|
|
@@ -197,6 +267,7 @@ async function execute(args, io, options) {
|
|
|
197
267
|
throw new HarnessBrewError("Usage: harnessbrew uninstall <formula> [--force]");
|
|
198
268
|
const receipt = await uninstallFormula(home, name, { force: args.includes("--force") });
|
|
199
269
|
io.stdout(`Uninstalled ${receipt.coordinate}.`);
|
|
270
|
+
setResult(io, receipt);
|
|
200
271
|
return 0;
|
|
201
272
|
}
|
|
202
273
|
if (command === "link" || command === "unlink") {
|
|
@@ -205,25 +276,28 @@ async function execute(args, io, options) {
|
|
|
205
276
|
if (name === undefined || targetValue === undefined) {
|
|
206
277
|
throw new HarnessBrewError(`Usage: harnessbrew ${command} <formula> --target <target> [--scope <user|project>] [--project <path>]`);
|
|
207
278
|
}
|
|
208
|
-
if (!
|
|
209
|
-
throw new HarnessBrewError(`
|
|
279
|
+
if (!hasTargetAdapter(targetValue)) {
|
|
280
|
+
throw new HarnessBrewError(`Target adapter is not registered: ${targetValue}`);
|
|
210
281
|
}
|
|
211
282
|
if (command === "link") {
|
|
212
|
-
await linkFormula(home, name, targetValue, targetOptions(args));
|
|
283
|
+
const receipt = await linkFormula(home, name, targetValue, targetOptions(args));
|
|
213
284
|
io.stdout(`Linked ${name} to ${targetValue}.`);
|
|
285
|
+
setResult(io, receipt);
|
|
214
286
|
}
|
|
215
287
|
else {
|
|
216
|
-
await unlinkFormula(home, name, targetValue, {
|
|
288
|
+
const receipt = await unlinkFormula(home, name, targetValue, {
|
|
217
289
|
...(hasTargetPlacementOptions(args) ? targetOptions(args) : {}),
|
|
218
290
|
force: args.includes("--force")
|
|
219
291
|
});
|
|
220
292
|
io.stdout(`Unlinked ${name} from ${targetValue}.`);
|
|
293
|
+
setResult(io, receipt);
|
|
221
294
|
}
|
|
222
295
|
return 0;
|
|
223
296
|
}
|
|
224
297
|
if (command === "doctor") {
|
|
225
298
|
const name = args[1]?.startsWith("--") === true ? undefined : args[1];
|
|
226
299
|
const report = await doctor(home, name);
|
|
300
|
+
setResult(io, report);
|
|
227
301
|
if (report.healthy) {
|
|
228
302
|
io.stdout(`Doctor found no issues across ${report.checked} formula(s).`);
|
|
229
303
|
return 0;
|
|
@@ -241,14 +315,15 @@ async function execute(args, io, options) {
|
|
|
241
315
|
if (name === undefined)
|
|
242
316
|
throw new HarnessBrewError("Usage: harnessbrew relink <formula> [--target <target>]");
|
|
243
317
|
const targetValue = optionValue(args, "--target");
|
|
244
|
-
if (targetValue !== undefined && !
|
|
245
|
-
throw new HarnessBrewError(`
|
|
318
|
+
if (targetValue !== undefined && !hasTargetAdapter(targetValue)) {
|
|
319
|
+
throw new HarnessBrewError(`Target adapter is not registered: ${targetValue}`);
|
|
246
320
|
}
|
|
247
|
-
await relinkFormula(home, name, {
|
|
321
|
+
const receipt = await relinkFormula(home, name, {
|
|
248
322
|
...(hasTargetPlacementOptions(args) ? targetOptions(args) : {}),
|
|
249
323
|
...(targetValue === undefined ? {} : { target: targetValue })
|
|
250
324
|
});
|
|
251
325
|
io.stdout(`Relinked ${name}${targetValue === undefined ? "" : ` to ${targetValue}`}.`);
|
|
326
|
+
setResult(io, receipt);
|
|
252
327
|
return 0;
|
|
253
328
|
}
|
|
254
329
|
if (command === "outdated") {
|
|
@@ -256,6 +331,7 @@ async function execute(args, io, options) {
|
|
|
256
331
|
outdated.forEach((item) => io.stdout(item.available
|
|
257
332
|
? `${item.coordinate}\t${item.installedCommit.slice(0, 12)} -> ${item.availableCommit?.slice(0, 12)}`
|
|
258
333
|
: `${item.coordinate}\t${item.installedCommit.slice(0, 12)} -> unavailable`));
|
|
334
|
+
setResult(io, outdated);
|
|
259
335
|
return 0;
|
|
260
336
|
}
|
|
261
337
|
if (command === "upgrade") {
|
|
@@ -264,34 +340,116 @@ async function execute(args, io, options) {
|
|
|
264
340
|
if (results.length === 0)
|
|
265
341
|
io.stdout("All installed formulas are up-to-date.");
|
|
266
342
|
results.forEach((result) => io.stdout(`Upgraded ${result.coordinate}: ${result.before.slice(0, 12)} -> ${result.after.slice(0, 12)}.`));
|
|
343
|
+
setResult(io, results);
|
|
267
344
|
return 0;
|
|
268
345
|
}
|
|
269
346
|
if (command === "bundle") {
|
|
270
347
|
const action = args[1];
|
|
271
348
|
const filePath = optionValue(args, "--file") ?? path.resolve("Harnessfile");
|
|
272
349
|
if (action === "install") {
|
|
273
|
-
const lock = await bundleInstall(home, filePath);
|
|
350
|
+
const lock = await bundleInstall(home, filePath, { updateLock: args.includes("--update-lock") });
|
|
274
351
|
io.stdout(`Bundle installed ${lock.assets.length} formulas from ${lock.taps.length} taps.`);
|
|
352
|
+
setResult(io, lock);
|
|
275
353
|
return 0;
|
|
276
354
|
}
|
|
277
355
|
if (action === "cleanup") {
|
|
278
356
|
const result = await bundleCleanup(home, filePath);
|
|
279
357
|
result.removed.forEach((coordinate) => io.stdout(`Removed ${coordinate}.`));
|
|
358
|
+
setResult(io, result);
|
|
280
359
|
return 0;
|
|
281
360
|
}
|
|
282
|
-
throw new HarnessBrewError("Usage: harnessbrew bundle <install|cleanup> [--file <Harnessfile>]");
|
|
361
|
+
throw new HarnessBrewError("Usage: harnessbrew bundle <install|cleanup> [--file <Harnessfile>] [--update-lock]");
|
|
283
362
|
}
|
|
284
363
|
io.stderr(`Unknown command: ${command}`);
|
|
285
364
|
io.stderr("Run 'harnessbrew help' for usage.");
|
|
286
365
|
return 1;
|
|
287
366
|
}
|
|
288
367
|
export async function runCli(args, io = defaultIO, options = {}) {
|
|
368
|
+
const json = args.includes("--json");
|
|
369
|
+
const dryRun = args.includes("--dry-run");
|
|
370
|
+
const commandArgs = args.filter((argument) => argument !== "--json" && argument !== "--dry-run");
|
|
371
|
+
const output = [];
|
|
372
|
+
const diagnostics = [];
|
|
373
|
+
let result = null;
|
|
374
|
+
const commandIO = json || dryRun
|
|
375
|
+
? {
|
|
376
|
+
stdout: (message) => output.push(message),
|
|
377
|
+
stderr: (message) => diagnostics.push(message),
|
|
378
|
+
setResult: (value) => { result = value; }
|
|
379
|
+
}
|
|
380
|
+
: io;
|
|
381
|
+
const command = commandArgs[0];
|
|
382
|
+
const tapAction = command === "tap" ? (commandArgs[1] ?? "list") : undefined;
|
|
383
|
+
const adapterAction = command === "adapter" ? (commandArgs[1] ?? "list") : undefined;
|
|
384
|
+
const mutates = command === "update" || command === "untap" || command === "install"
|
|
385
|
+
|| command === "uninstall" || command === "link" || command === "unlink" || command === "relink"
|
|
386
|
+
|| command === "upgrade" || command === "bundle"
|
|
387
|
+
|| (command === "tap" && tapAction !== "list")
|
|
388
|
+
|| (command === "adapter" && adapterAction !== "list");
|
|
389
|
+
const emitJson = (exitCode, changes = [], errorCode) => {
|
|
390
|
+
const envelope = {
|
|
391
|
+
schemaVersion: 1,
|
|
392
|
+
ok: exitCode === 0,
|
|
393
|
+
command: command ?? "help",
|
|
394
|
+
exitCode,
|
|
395
|
+
dryRun,
|
|
396
|
+
result,
|
|
397
|
+
output,
|
|
398
|
+
diagnostics,
|
|
399
|
+
...(exitCode === 0 ? {} : {
|
|
400
|
+
error: {
|
|
401
|
+
code: errorCode ?? "COMMAND_FAILED",
|
|
402
|
+
message: diagnostics[0] ?? "Command failed."
|
|
403
|
+
}
|
|
404
|
+
}),
|
|
405
|
+
...(dryRun ? { changes } : {})
|
|
406
|
+
};
|
|
407
|
+
io.stdout(JSON.stringify(envelope));
|
|
408
|
+
};
|
|
289
409
|
try {
|
|
290
|
-
|
|
410
|
+
const home = resolveHarnessHome(options.home);
|
|
411
|
+
if (dryRun && !mutates)
|
|
412
|
+
throw new HarnessBrewError("--dry-run requires a mutating command.");
|
|
413
|
+
const invoke = async () => {
|
|
414
|
+
const unload = command !== undefined
|
|
415
|
+
&& ["install", "link", "unlink", "relink", "upgrade", "bundle"].includes(command)
|
|
416
|
+
? await loadAdapterPlugins(home)
|
|
417
|
+
: () => { };
|
|
418
|
+
try {
|
|
419
|
+
return await execute(commandArgs, commandIO, options);
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
unload();
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
let exitCode;
|
|
426
|
+
let changes = [];
|
|
427
|
+
if (dryRun) {
|
|
428
|
+
const preview = await withHomeLock(home, () => withJournalPreview(home, `preview:${command ?? "unknown"}`, invoke));
|
|
429
|
+
exitCode = preview.result;
|
|
430
|
+
changes = preview.changes;
|
|
431
|
+
if (!json) {
|
|
432
|
+
io.stdout(`Dry run completed: ${changes.length} path change(s) would be made.`);
|
|
433
|
+
changes.forEach((change) => io.stdout(`${change.before.kind === "missing" ? "create" : change.after.kind === "missing" ? "remove" : "modify"}\t${change.path}`));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
exitCode = mutates
|
|
438
|
+
? await withHomeLock(home, () => withJournalTransaction(home, `cli:${command ?? "unknown"}`, invoke))
|
|
439
|
+
: await invoke();
|
|
440
|
+
}
|
|
441
|
+
if (json)
|
|
442
|
+
emitJson(exitCode, changes);
|
|
443
|
+
return exitCode;
|
|
291
444
|
}
|
|
292
445
|
catch (error) {
|
|
293
446
|
if (error instanceof HarnessBrewError) {
|
|
294
|
-
|
|
447
|
+
if (json) {
|
|
448
|
+
diagnostics.push(error.message);
|
|
449
|
+
emitJson(1, [], "HARNESSBREW_ERROR");
|
|
450
|
+
}
|
|
451
|
+
else
|
|
452
|
+
io.stderr(error.message);
|
|
295
453
|
return 1;
|
|
296
454
|
}
|
|
297
455
|
throw error;
|