@kairyou/agent-tools 0.7.1 → 0.7.4
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 +23 -23
- package/README.zh-CN.md +22 -20
- package/dist/statusline/claude-statusline.mjs +0 -0
- package/dist/usage/cli.mjs +0 -0
- package/dist/usage/codex-hook.mjs +0 -0
- package/dist/usage/core.mjs +0 -0
- package/dist/vision/mcp-server.mjs +0 -0
- package/package.json +5 -1
- package/scripts/install.mjs +114 -22
- package/scripts/release.mjs +11 -76
package/README.md
CHANGED
|
@@ -75,8 +75,8 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
|
|
|
75
75
|
|
|
76
76
|
`--dry-run` previews, `--uninstall` unwires the integration from the agent, and
|
|
77
77
|
re-running the install command updates. The installer only touches config
|
|
78
|
-
entries it wrote itself
|
|
79
|
-
|
|
78
|
+
entries it wrote itself, and `config.jsonc` updates only add missing default
|
|
79
|
+
keys without touching your edits or comments.
|
|
80
80
|
|
|
81
81
|
| Capability | Claude Code | Codex | OpenCode |
|
|
82
82
|
| --- | --- | --- | --- |
|
|
@@ -90,19 +90,19 @@ uninstall.
|
|
|
90
90
|
npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
The installer writes `statusLine` to `~/.claude/settings.json`.
|
|
94
|
-
output is:
|
|
93
|
+
The installer writes `statusLine` to `~/.claude/settings.json`. Example output:
|
|
95
94
|
|
|
96
95
|
```text
|
|
96
|
+
# Pick and order the fields via statusline.fields in ~/.agent-tools/config.jsonc:
|
|
97
97
|
⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
|
|
98
|
-
```
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
# With a compatible API relay, its quota info is shown instead, e.g.:
|
|
100
|
+
⎇ main | Opus 4.8 | API | balance $362 | today $61.7 | 30d $566
|
|
101
|
+
```
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
and
|
|
103
|
+
Here `5h` and `w` are Claude's rolling usage windows and `⟳` is the reset
|
|
104
|
+
countdown; see [Provider usage](#provider-usage) below for relay quota
|
|
105
|
+
compatibility and configuration.
|
|
106
106
|
|
|
107
107
|
### Provider usage
|
|
108
108
|
|
|
@@ -149,6 +149,19 @@ Keep `preset` set to `auto` for automatic detection. Select a specific protocol
|
|
|
149
149
|
only when you know which usage endpoint the gateway exposes; a configured
|
|
150
150
|
custom route id is also accepted.
|
|
151
151
|
|
|
152
|
+
Output examples:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
# Relay plan quota.
|
|
156
|
+
API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
157
|
+
|
|
158
|
+
# Wallet balance.
|
|
159
|
+
API | balance $362 | today $61.7 | 30d $566
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Fields: `D/W/M` are daily/weekly/monthly spend against plan limits; `Exp` is
|
|
163
|
+
the plan expiry; `balance` is wallet credit; `today` and `30d` are API spend.
|
|
164
|
+
|
|
152
165
|
#### Custom gateway routes
|
|
153
166
|
|
|
154
167
|
For gateways the built-in probes cannot reach (e.g. cookie-authenticated
|
|
@@ -198,19 +211,6 @@ export async function run(context, { requestJson, agentConfig }) {
|
|
|
198
211
|
through to the next route (with `providerUsage.debug` enabled, failures are
|
|
199
212
|
logged to `~/.agent-tools/logs/usage-debug.log`).
|
|
200
213
|
|
|
201
|
-
Output examples:
|
|
202
|
-
|
|
203
|
-
```text
|
|
204
|
-
# Relay plan quota.
|
|
205
|
-
API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
206
|
-
|
|
207
|
-
# Wallet balance.
|
|
208
|
-
API | balance $362 | today $61.7 | 30d $566
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Fields: `D/W/M` are daily/weekly/monthly spend against plan limits; `Exp` is
|
|
212
|
-
the plan expiry; `balance` is wallet credit; `today` and `30d` are API spend.
|
|
213
|
-
|
|
214
214
|
### Vision (cross-model image understanding)
|
|
215
215
|
|
|
216
216
|
Lets a main model that cannot see images ask a multimodal model specific questions about an image (local path or http(s) URL) and reason on from the answers. Typical uses: reading error screenshots, implementing UI from design mockups, locating the glitch in a bug-report screenshot. One installer capability bundling three parts: the `inspect_image` MCP stdio server, the `at-vision` policy skill, and a human diagnostic CLI.
|
package/README.zh-CN.md
CHANGED
|
@@ -72,7 +72,8 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
`--dry-run` 预览, `--uninstall` 解除对应集成, 重新执行安装命令即为更新.
|
|
75
|
-
|
|
75
|
+
安装器只改动自己写入的配置项, 更新 `config.jsonc` 时只补充缺失的默认键,
|
|
76
|
+
不会动你的修改和注释.
|
|
76
77
|
|
|
77
78
|
| Capability | Claude Code | Codex | OpenCode |
|
|
78
79
|
| --- | --- | --- | --- |
|
|
@@ -86,17 +87,18 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
|
|
|
86
87
|
npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
87
88
|
```
|
|
88
89
|
|
|
89
|
-
安装器会把 `statusLine` 写入 `~/.claude/settings.json`.
|
|
90
|
+
安装器会把 `statusLine` 写入 `~/.claude/settings.json`. 输出示例:
|
|
90
91
|
|
|
91
92
|
```text
|
|
93
|
+
# 显示项与顺序由 ~/.agent-tools/config.jsonc 的 statusline.fields 控制:
|
|
92
94
|
⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
|
|
93
|
-
```
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
# 使用兼容的 API 中转时, 显示中转的额度信息, 比如:
|
|
97
|
+
⎇ main | Opus 4.8 | API | balance $362 | today $61.7 | 30d $566
|
|
98
|
+
```
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
其中 `5h` / `w` 是 Claude 的滚动用量窗口, `⟳` 后面是重置倒计时;
|
|
101
|
+
中转额度信息的兼容性与配置见下方 [Provider usage](#provider-usage).
|
|
100
102
|
|
|
101
103
|
### Provider usage
|
|
102
104
|
|
|
@@ -138,6 +140,19 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
|
|
|
138
140
|
保持 `preset: "auto"` 即可自动探测. 只有明确知道网关开放的是哪种用量协议时,
|
|
139
141
|
才指定相应的内置 preset 或已配置的自定义 route id.
|
|
140
142
|
|
|
143
|
+
显示效果示例:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
# 中转套餐额度.
|
|
147
|
+
API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
148
|
+
|
|
149
|
+
# 钱包余额.
|
|
150
|
+
API | balance $362 | today $61.7 | 30d $566
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
字段含义: `D/W/M` 是日/周/月套餐消耗与上限, `Exp` 是套餐到期日,
|
|
154
|
+
`balance` 是钱包余额, `today` / `30d` 是今日与近 30 天 API 消耗.
|
|
155
|
+
|
|
141
156
|
#### 自定义网关路由
|
|
142
157
|
|
|
143
158
|
内置探测覆盖不到的网关(比如 cookie 认证的中转), 可以自己写路由模块并在
|
|
@@ -184,19 +199,6 @@ export async function run(context, { requestJson, agentConfig }) {
|
|
|
184
199
|
`text` 是自由字符串; 成功返回 `{ text }`, 抛错则回落到下一条路由
|
|
185
200
|
(开启 `providerUsage.debug` 后, 失败会记录到 `~/.agent-tools/logs/usage-debug.log`).
|
|
186
201
|
|
|
187
|
-
显示效果示例:
|
|
188
|
-
|
|
189
|
-
```text
|
|
190
|
-
# 中转套餐额度.
|
|
191
|
-
API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
192
|
-
|
|
193
|
-
# 钱包余额.
|
|
194
|
-
API | balance $362 | today $61.7 | 30d $566
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
字段含义: `D/W/M` 是日/周/月套餐消耗与上限, `Exp` 是套餐到期日,
|
|
198
|
-
`balance` 是钱包余额, `today` / `30d` 是今日与近 30 天 API 消耗.
|
|
199
|
-
|
|
200
202
|
### Vision(跨模型识图)
|
|
201
203
|
|
|
202
204
|
让不支持图片的主模型借助多模态模型识图: 针对图片(本地路径或 http(s) URL)提出具体问题, 拿到答案后继续自己的推理. 常见场景: 读取报错截图, 按设计稿还原 UI, 定位测试反馈截图里的界面问题. 一个安装器 capability 包含三部分: `inspect_image` MCP stdio server, `at-vision` 策略 skill, 以及人工诊断 CLI.
|
|
File without changes
|
package/dist/usage/cli.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/dist/usage/core.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kairyou/agent-tools",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Reusable Agent Skills and installable integrations (statusline, provider usage, vision) for Codex, Claude Code, and opencode.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kairyou/agent-tools.git"
|
|
9
|
+
},
|
|
6
10
|
"engines": {
|
|
7
11
|
"node": ">=22"
|
|
8
12
|
},
|
package/scripts/install.mjs
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
// -h, --help Show this help.
|
|
46
46
|
|
|
47
47
|
import { spawnSync } from "node:child_process";
|
|
48
|
+
import { createHash } from "node:crypto";
|
|
48
49
|
import fs from "node:fs";
|
|
49
50
|
import os from "node:os";
|
|
50
51
|
import path from "node:path";
|
|
@@ -93,6 +94,7 @@ const USAGE_SKILL_NAME = "at-usage";
|
|
|
93
94
|
const USAGE_SKILL_SRC = path.join(REPO_ROOT, "integrations", "usage", "skills", USAGE_SKILL_NAME);
|
|
94
95
|
const VISION_BUNDLED_MCP_SERVER = path.join(REPO_ROOT, "dist", "vision", "mcp-server.mjs");
|
|
95
96
|
const VISION_BUNDLED_CLI = path.join(REPO_ROOT, "dist", "vision", "cli.mjs");
|
|
97
|
+
const INSTALL_STATE_PATH = path.join(INSTALL_ROOT, "install-state.json");
|
|
96
98
|
|
|
97
99
|
function fwd(p) {
|
|
98
100
|
return p.replace(/\\/g, "/");
|
|
@@ -242,7 +244,9 @@ function installRuntimeAssets(opts) {
|
|
|
242
244
|
}
|
|
243
245
|
console.log(`runtime: ${INSTALL_ROOT}`);
|
|
244
246
|
for (const [src, dest, options] of files) copyRuntimeFile(src, dest, opts.dryRun, options);
|
|
245
|
-
|
|
247
|
+
// The statusline refreshes via the usage engine, so it needs packaged
|
|
248
|
+
// routes too.
|
|
249
|
+
if (wants(opts, "usage") || wants(opts, "statusline")) syncUsageRoutesDir(opts.dryRun);
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
// Repo-shipped usage routes are replaced wholesale so routes removed from the
|
|
@@ -443,13 +447,12 @@ const VISION_RATE_LIMIT_STATE = path.join(INSTALL_ROOT, "cache", "vision-rate-li
|
|
|
443
447
|
const VISION_DIST_DIR = path.join(REPO_ROOT, "dist", "vision");
|
|
444
448
|
const VISION_RUNTIME_SERVER = path.join(VISION_RUNTIME_DIR, "mcp-server.mjs");
|
|
445
449
|
const VISION_RUNTIME_CLI = path.join(VISION_RUNTIME_DIR, "cli.mjs");
|
|
446
|
-
const
|
|
447
|
-
const VISION_SKILL_MARKER_DATA = Object.freeze({
|
|
450
|
+
const VISION_SKILL_IDENTITY = Object.freeze({
|
|
448
451
|
owner: "@kairyou/agent-tools",
|
|
449
452
|
capability: "vision",
|
|
450
453
|
artifact: "skill",
|
|
451
454
|
});
|
|
452
|
-
const
|
|
455
|
+
const USAGE_SKILL_IDENTITY = Object.freeze({
|
|
453
456
|
owner: "@kairyou/agent-tools",
|
|
454
457
|
capability: "usage",
|
|
455
458
|
artifact: "skill",
|
|
@@ -556,26 +559,110 @@ function installVisionRuntime(opts) {
|
|
|
556
559
|
}
|
|
557
560
|
}
|
|
558
561
|
|
|
559
|
-
function
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
+
function skillManifestKey(dest) {
|
|
563
|
+
const resolved = fwd(path.resolve(dest));
|
|
564
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function skillHash(skillFile) {
|
|
568
|
+
if (!fs.existsSync(skillFile)) return "";
|
|
569
|
+
return createHash("sha256").update(fs.readFileSync(skillFile)).digest("hex");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function matchesIdentity(value, identity) {
|
|
573
|
+
return Object.entries(identity).every(([key, expected]) => value?.[key] === expected);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function readInstallState() {
|
|
577
|
+
const value = readJson(INSTALL_STATE_PATH);
|
|
578
|
+
if (Object.keys(value).length === 0) return { version: 1, artifacts: {} };
|
|
579
|
+
if (
|
|
580
|
+
value.version !== 1 ||
|
|
581
|
+
!value.artifacts ||
|
|
582
|
+
typeof value.artifacts !== "object" ||
|
|
583
|
+
Array.isArray(value.artifacts)
|
|
584
|
+
) {
|
|
585
|
+
throw new Error(`Cannot parse ${INSTALL_STATE_PATH}: unsupported install state schema`);
|
|
586
|
+
}
|
|
587
|
+
return value;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function writeInstallState(state) {
|
|
591
|
+
if (Object.keys(state.artifacts).length === 0) {
|
|
592
|
+
fs.rmSync(INSTALL_STATE_PATH, { force: true });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const text = JSON.stringify(state, null, 2) + "\n";
|
|
596
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
597
|
+
const stage = `${INSTALL_STATE_PATH}.stage-${suffix}`;
|
|
598
|
+
const backup = `${INSTALL_STATE_PATH}.backup-${suffix}`;
|
|
599
|
+
let movedCurrent = false;
|
|
600
|
+
fs.mkdirSync(path.dirname(INSTALL_STATE_PATH), { recursive: true });
|
|
562
601
|
try {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
602
|
+
fs.writeFileSync(stage, text);
|
|
603
|
+
if (fs.existsSync(INSTALL_STATE_PATH)) {
|
|
604
|
+
fs.renameSync(INSTALL_STATE_PATH, backup);
|
|
605
|
+
movedCurrent = true;
|
|
606
|
+
}
|
|
607
|
+
fs.renameSync(stage, INSTALL_STATE_PATH);
|
|
608
|
+
fs.rmSync(backup, { force: true });
|
|
609
|
+
} catch (error) {
|
|
610
|
+
fs.rmSync(stage, { force: true });
|
|
611
|
+
if (movedCurrent && !fs.existsSync(INSTALL_STATE_PATH) && fs.existsSync(backup)) {
|
|
612
|
+
fs.renameSync(backup, INSTALL_STATE_PATH);
|
|
613
|
+
}
|
|
614
|
+
throw error;
|
|
615
|
+
} finally {
|
|
616
|
+
fs.rmSync(stage, { force: true });
|
|
617
|
+
if (fs.existsSync(INSTALL_STATE_PATH)) fs.rmSync(backup, { force: true });
|
|
567
618
|
}
|
|
619
|
+
console.log(` wrote ${INSTALL_STATE_PATH}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function managedSkillStatus(dest, identity) {
|
|
623
|
+
const entry = readInstallState().artifacts[skillManifestKey(dest)];
|
|
624
|
+
if (entry) {
|
|
625
|
+
if (!matchesIdentity(entry, identity)) return "other";
|
|
626
|
+
const currentHash = skillHash(path.join(dest, "SKILL.md"));
|
|
627
|
+
return currentHash && currentHash === entry.sha256 ? "managed" : "modified";
|
|
628
|
+
}
|
|
629
|
+
return "unmanaged";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function recordManagedSkill(dest, identity) {
|
|
633
|
+
const state = readInstallState();
|
|
634
|
+
state.artifacts[skillManifestKey(dest)] = {
|
|
635
|
+
path: fwd(path.resolve(dest)),
|
|
636
|
+
...identity,
|
|
637
|
+
sha256: skillHash(path.join(dest, "SKILL.md")),
|
|
638
|
+
};
|
|
639
|
+
writeInstallState(state);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function forgetManagedSkill(dest) {
|
|
643
|
+
const state = readInstallState();
|
|
644
|
+
if (!state.artifacts[skillManifestKey(dest)]) return;
|
|
645
|
+
delete state.artifacts[skillManifestKey(dest)];
|
|
646
|
+
writeInstallState(state);
|
|
568
647
|
}
|
|
569
648
|
|
|
570
649
|
function installManagedSkillDir(
|
|
571
650
|
skillsRoot,
|
|
572
|
-
{ name, source,
|
|
651
|
+
{ name, source, identity, replacements, remove, dryRun }
|
|
573
652
|
) {
|
|
574
653
|
const dest = path.join(skillsRoot, name);
|
|
575
|
-
const
|
|
654
|
+
const status = fs.existsSync(dest) ? managedSkillStatus(dest, identity) : "missing";
|
|
576
655
|
if (remove) {
|
|
577
|
-
if (!fs.existsSync(dest))
|
|
578
|
-
|
|
656
|
+
if (!fs.existsSync(dest)) {
|
|
657
|
+
if (!dryRun) forgetManagedSkill(dest);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (status === "modified") {
|
|
661
|
+
if (!dryRun) forgetManagedSkill(dest);
|
|
662
|
+
console.log(` kept modified skill and released ownership ${dest}`);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (status !== "managed") {
|
|
579
666
|
console.log(` kept unmanaged ${dest}`);
|
|
580
667
|
return;
|
|
581
668
|
}
|
|
@@ -584,10 +671,18 @@ function installManagedSkillDir(
|
|
|
584
671
|
return;
|
|
585
672
|
}
|
|
586
673
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
674
|
+
forgetManagedSkill(dest);
|
|
587
675
|
console.log(` removed ${dest}`);
|
|
588
676
|
return;
|
|
589
677
|
}
|
|
590
|
-
if (
|
|
678
|
+
if (status === "modified") {
|
|
679
|
+
console.error(
|
|
680
|
+
` Refusing to overwrite modified managed skill directory ${dest}. ` +
|
|
681
|
+
`Move or restore it, then re-run the install.`
|
|
682
|
+
);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
if (fs.existsSync(dest) && status !== "managed") {
|
|
591
686
|
console.error(
|
|
592
687
|
` Refusing to overwrite existing unowned skill directory ${dest}. ` +
|
|
593
688
|
`Move or remove it, then re-run the install.`
|
|
@@ -610,10 +705,7 @@ function installManagedSkillDir(
|
|
|
610
705
|
skill = skill.replaceAll(token, value);
|
|
611
706
|
}
|
|
612
707
|
fs.writeFileSync(skillFile, skill);
|
|
613
|
-
|
|
614
|
-
path.join(dest, SKILL_MARKER),
|
|
615
|
-
JSON.stringify(markerData, null, 2) + "\n"
|
|
616
|
-
);
|
|
708
|
+
recordManagedSkill(dest, identity);
|
|
617
709
|
console.log(` wrote ${dest}`);
|
|
618
710
|
}
|
|
619
711
|
|
|
@@ -621,7 +713,7 @@ function installVisionSkillDir(skillsRoot, { remove, dryRun }) {
|
|
|
621
713
|
installManagedSkillDir(skillsRoot, {
|
|
622
714
|
name: VISION_SKILL_NAME,
|
|
623
715
|
source: VISION_SKILL_SRC,
|
|
624
|
-
|
|
716
|
+
identity: VISION_SKILL_IDENTITY,
|
|
625
717
|
replacements: { "{{VISION_CLI_PATH}}": fwd(VISION_RUNTIME_CLI) },
|
|
626
718
|
remove,
|
|
627
719
|
dryRun,
|
|
@@ -632,7 +724,7 @@ function installUsageSkillDir(skillsRoot, agent, { remove, dryRun }) {
|
|
|
632
724
|
installManagedSkillDir(skillsRoot, {
|
|
633
725
|
name: USAGE_SKILL_NAME,
|
|
634
726
|
source: USAGE_SKILL_SRC,
|
|
635
|
-
|
|
727
|
+
identity: USAGE_SKILL_IDENTITY,
|
|
636
728
|
replacements: {
|
|
637
729
|
"{{USAGE_CLI_PATH}}": fwd(RUNTIME.usageCli),
|
|
638
730
|
"{{USAGE_AGENT}}": agent,
|
package/scripts/release.mjs
CHANGED
|
@@ -3,52 +3,20 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
5
|
// npm run release [-- minor|major] # default: patch
|
|
6
|
-
// npm run release -- --dry-run # npm test + preview tarball, no bump/
|
|
6
|
+
// npm run release -- --dry-run # npm test + preview tarball, no bump/push
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// Local steps: clean-tree check -> npm test -> npm version -> git push --follow-tags.
|
|
9
|
+
// Publishing runs on GitHub Actions via npm trusted publishing (OIDC, no
|
|
10
|
+
// tokens, no npm login): .github/workflows/release.yml publishes when the
|
|
11
|
+
// version tag lands.
|
|
9
12
|
//
|
|
10
13
|
// Release when scripts/ integrations/ config.default.jsonc or
|
|
11
14
|
// package.json deps change — npx users only get these from the published package.
|
|
12
15
|
// (skills/ ships from GitHub via `npx skills add`; no release needed.)
|
|
13
16
|
|
|
14
17
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
16
18
|
|
|
17
19
|
const BUMPS = ["patch", "minor", "major"];
|
|
18
|
-
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
|
-
const PACKAGE_NAME = packageJson.name;
|
|
20
|
-
|
|
21
|
-
if (typeof PACKAGE_NAME !== "string" || PACKAGE_NAME.trim() === "") {
|
|
22
|
-
console.error("package.json must define a non-empty name before releasing.");
|
|
23
|
-
process.exit(2);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function readNpmConfig(key) {
|
|
27
|
-
const npmExecPath = process.env.npm_execpath;
|
|
28
|
-
const result = npmExecPath
|
|
29
|
-
? spawnSync(process.execPath, [npmExecPath, "config", "get", key], { encoding: "utf8" })
|
|
30
|
-
: spawnSync("npm", ["config", "get", key], { encoding: "utf8", shell: true });
|
|
31
|
-
|
|
32
|
-
if (result.status !== 0) {
|
|
33
|
-
const detail = result.stderr?.trim() || result.error?.message || "npm config exited unexpectedly";
|
|
34
|
-
console.error(`Unable to read npm config ${key}:\n${detail}`);
|
|
35
|
-
process.exit(result.status ?? 2);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const value = result.stdout?.trim();
|
|
39
|
-
return value && value !== "undefined" && value !== "null" ? value : undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const packageScope = PACKAGE_NAME.match(/^(@[^/]+)\//)?.[1];
|
|
43
|
-
const REGISTRY = (packageScope && readNpmConfig(`${packageScope}:registry`)) || readNpmConfig("registry");
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const registryUrl = new URL(REGISTRY);
|
|
47
|
-
if (!["http:", "https:"].includes(registryUrl.protocol)) throw new Error("unsupported protocol");
|
|
48
|
-
} catch {
|
|
49
|
-
console.error("npm registry config must be a valid HTTP(S) URL before releasing.");
|
|
50
|
-
process.exit(2);
|
|
51
|
-
}
|
|
52
20
|
|
|
53
21
|
let bump = "patch";
|
|
54
22
|
let dryRun = false;
|
|
@@ -61,66 +29,33 @@ for (const arg of process.argv.slice(2)) {
|
|
|
61
29
|
}
|
|
62
30
|
}
|
|
63
31
|
|
|
64
|
-
function run(command
|
|
65
|
-
const { onFailure, ...spawnOptions } = opts;
|
|
32
|
+
function run(command) {
|
|
66
33
|
console.log(`\n> ${command}`);
|
|
67
|
-
const result = spawnSync(command, { stdio: "inherit", shell: true
|
|
34
|
+
const result = spawnSync(command, { stdio: "inherit", shell: true });
|
|
68
35
|
if (result.status !== 0) {
|
|
69
36
|
console.error(`\nAborted: \`${command}\` exited with ${result.status}.`);
|
|
70
|
-
onFailure?.();
|
|
71
37
|
process.exit(result.status ?? 1);
|
|
72
38
|
}
|
|
73
39
|
}
|
|
74
40
|
|
|
75
|
-
function printCommands(message, commands) {
|
|
76
|
-
console.error(`\n${message}\n`);
|
|
77
|
-
for (const command of commands) console.error(` ${command}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
41
|
const status = spawnSync("git status --porcelain", { shell: true, encoding: "utf8" });
|
|
81
42
|
if (!dryRun && status.stdout.trim() !== "") {
|
|
82
43
|
console.error("Working tree is not clean — commit or stash first:\n" + status.stdout);
|
|
83
44
|
process.exit(1);
|
|
84
45
|
}
|
|
85
46
|
|
|
86
|
-
if (!dryRun) {
|
|
87
|
-
// Fail before npm version creates a commit and tag when the saved token is stale.
|
|
88
|
-
run(`npm whoami --registry=${REGISTRY}`, {
|
|
89
|
-
onFailure() {
|
|
90
|
-
printCommands("npm authentication is missing or expired. Log in, verify the account, then retry:", [
|
|
91
|
-
`npm login --registry=${REGISTRY}`,
|
|
92
|
-
`npm whoami --registry=${REGISTRY}`,
|
|
93
|
-
`npm run release -- ${bump}`,
|
|
94
|
-
]);
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
47
|
run("npm test");
|
|
100
48
|
|
|
101
49
|
if (dryRun) {
|
|
102
50
|
// Shows exactly which files would ship; verify nothing is missing/extra.
|
|
103
51
|
run("npm pack --dry-run");
|
|
104
|
-
console.log("\nDry run complete. No version bump, nothing
|
|
52
|
+
console.log("\nDry run complete. No version bump, nothing pushed.");
|
|
105
53
|
process.exit(0);
|
|
106
54
|
}
|
|
107
55
|
|
|
108
56
|
run(`npm version ${bump}`);
|
|
109
|
-
run("npm publish", {
|
|
110
|
-
onFailure() {
|
|
111
|
-
printCommands(
|
|
112
|
-
"npm publish failed after the version commit and tag were created. Do not run the release command again. Fix authentication/permissions, then resume:",
|
|
113
|
-
[
|
|
114
|
-
`npm login --registry=${REGISTRY}`,
|
|
115
|
-
`npm whoami --registry=${REGISTRY}`,
|
|
116
|
-
`npm owner ls ${PACKAGE_NAME}`,
|
|
117
|
-
"npm publish",
|
|
118
|
-
`npm view ${PACKAGE_NAME} version`,
|
|
119
|
-
"git push --follow-tags",
|
|
120
|
-
],
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
57
|
run("git push --follow-tags");
|
|
125
58
|
|
|
126
|
-
console.log(
|
|
59
|
+
console.log(
|
|
60
|
+
"\nTag pushed. GitHub Actions publishes it: https://github.com/kairyou/agent-tools/actions"
|
|
61
|
+
);
|