@fitlab-ai/agent-infra 0.6.1 → 0.6.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/defaults.json +1 -1
- package/dist/lib/sandbox/config.js +24 -6
- package/dist/lib/sandbox/runtime-engines.js +27 -0
- package/dist/package.json +1 -1
- package/lib/defaults.json +1 -1
- package/lib/sandbox/config.ts +34 -6
- package/lib/sandbox/runtime-engines.ts +39 -0
- package/package.json +3 -1
- package/templates/.agents/rules/release-commands.github.en.md +9 -3
- package/templates/.agents/rules/release-commands.github.zh-CN.md +9 -3
- package/templates/.agents/skills/create-release-note/SKILL.en.md +8 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +8 -11
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -1
package/dist/lib/defaults.json
CHANGED
|
@@ -2,11 +2,13 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir, platform } from 'node:os';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import pc from 'picocolors';
|
|
5
6
|
import { validateSandboxEngine } from "./engine.js";
|
|
6
7
|
import { hostJoin } from "./engines/wsl2-paths.js";
|
|
8
|
+
import { findRuntimeEngineMismatches } from "./runtime-engines.js";
|
|
7
9
|
const DEFAULTS = Object.freeze({
|
|
8
10
|
engine: null,
|
|
9
|
-
runtimes: ['
|
|
11
|
+
runtimes: ['node22'],
|
|
10
12
|
tools: ['claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
11
13
|
dockerfile: null,
|
|
12
14
|
vm: {
|
|
@@ -38,7 +40,7 @@ function cloneDefaults() {
|
|
|
38
40
|
vm: { ...DEFAULTS.vm }
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
|
-
export function loadConfig({ platformFn = platform } = {}) {
|
|
43
|
+
export function loadConfig({ platformFn = platform, writeStderr = (chunk) => process.stderr.write(chunk) } = {}) {
|
|
42
44
|
const repoRoot = detectRepoRoot();
|
|
43
45
|
const home = homedir();
|
|
44
46
|
if (!home) {
|
|
@@ -56,6 +58,24 @@ export function loadConfig({ platformFn = platform } = {}) {
|
|
|
56
58
|
if (!project || typeof project !== 'string') {
|
|
57
59
|
throw new Error('sandbox: .agents/.airc.json is missing a valid "project" field');
|
|
58
60
|
}
|
|
61
|
+
const runtimes = Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
|
|
62
|
+
? [...sandbox.runtimes]
|
|
63
|
+
: defaults.runtimes;
|
|
64
|
+
const dockerfile = typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null;
|
|
65
|
+
if (!dockerfile) {
|
|
66
|
+
let enginesNode;
|
|
67
|
+
try {
|
|
68
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
69
|
+
enginesNode = typeof pkg.engines?.node === 'string' ? pkg.engines.node : undefined;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
enginesNode = undefined;
|
|
73
|
+
}
|
|
74
|
+
for (const { runtimes: invalidRuntimes, enginesNode: range } of findRuntimeEngineMismatches(runtimes, enginesNode)) {
|
|
75
|
+
writeStderr(pc.yellow(`Warning: sandbox runtimes ${invalidRuntimes.map((runtime) => `"${runtime}"`).join(', ')} do not satisfy this project's package.json "engines.node" ("${range}").\n` +
|
|
76
|
+
' Update "sandbox.runtimes" in .agents/.airc.json (e.g. "node22"), or relax "engines.node".\n'));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
59
79
|
return {
|
|
60
80
|
repoRoot,
|
|
61
81
|
configPath,
|
|
@@ -68,13 +88,11 @@ export function loadConfig({ platformFn = platform } = {}) {
|
|
|
68
88
|
shareBase: hostJoin(home, '.agent-infra', 'share', project),
|
|
69
89
|
dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
|
|
70
90
|
engine,
|
|
71
|
-
runtimes
|
|
72
|
-
? [...sandbox.runtimes]
|
|
73
|
-
: defaults.runtimes,
|
|
91
|
+
runtimes,
|
|
74
92
|
tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
|
|
75
93
|
? [...sandbox.tools]
|
|
76
94
|
: defaults.tools,
|
|
77
|
-
dockerfile
|
|
95
|
+
dockerfile,
|
|
78
96
|
vm: {
|
|
79
97
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
80
98
|
memory: asPositiveNumberOrNull(sandbox.vm?.memory) ?? defaults.vm.memory,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
function nodeMajor(runtime) {
|
|
3
|
+
const match = /^node(\d+)$/.exec(runtime);
|
|
4
|
+
return match ? Number(match[1]) : null;
|
|
5
|
+
}
|
|
6
|
+
export function findRuntimeEngineMismatches(runtimes, enginesNode) {
|
|
7
|
+
if (!enginesNode) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const range = semver.validRange(enginesNode);
|
|
11
|
+
if (!range) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const nodeRuntimes = [];
|
|
15
|
+
for (const runtime of runtimes) {
|
|
16
|
+
const major = nodeMajor(runtime);
|
|
17
|
+
if (major === null) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
nodeRuntimes.push(runtime);
|
|
21
|
+
if (semver.intersects(`${major}.x`, range)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return nodeRuntimes.length > 0 ? [{ runtimes: nodeRuntimes, enginesNode }] : [];
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=runtime-engines.js.map
|
package/dist/package.json
CHANGED
package/lib/defaults.json
CHANGED
package/lib/sandbox/config.ts
CHANGED
|
@@ -2,12 +2,14 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir, platform } from 'node:os';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import pc from 'picocolors';
|
|
5
6
|
import { validateSandboxEngine } from './engine.ts';
|
|
6
7
|
import { hostJoin } from './engines/wsl2-paths.ts';
|
|
8
|
+
import { findRuntimeEngineMismatches } from './runtime-engines.ts';
|
|
7
9
|
|
|
8
10
|
const DEFAULTS = Object.freeze({
|
|
9
11
|
engine: null,
|
|
10
|
-
runtimes: ['
|
|
12
|
+
runtimes: ['node22'],
|
|
11
13
|
tools: ['claude-code', 'codex', 'gemini-cli', 'opencode'],
|
|
12
14
|
dockerfile: null,
|
|
13
15
|
vm: {
|
|
@@ -18,6 +20,7 @@ const DEFAULTS = Object.freeze({
|
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
type PlatformFn = typeof platform;
|
|
23
|
+
type WriteStderr = (chunk: string) => unknown;
|
|
21
24
|
|
|
22
25
|
type SandboxConfigInput = {
|
|
23
26
|
engine?: string | null;
|
|
@@ -82,7 +85,10 @@ function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes:
|
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
export function loadConfig({
|
|
88
|
+
export function loadConfig({
|
|
89
|
+
platformFn = platform,
|
|
90
|
+
writeStderr = (chunk) => process.stderr.write(chunk)
|
|
91
|
+
}: { platformFn?: PlatformFn; writeStderr?: WriteStderr } = {}): SandboxConfig {
|
|
86
92
|
const repoRoot = detectRepoRoot();
|
|
87
93
|
const home = homedir();
|
|
88
94
|
|
|
@@ -105,6 +111,30 @@ export function loadConfig({ platformFn = platform }: { platformFn?: PlatformFn
|
|
|
105
111
|
throw new Error('sandbox: .agents/.airc.json is missing a valid "project" field');
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
const runtimes = Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
|
|
115
|
+
? [...sandbox.runtimes]
|
|
116
|
+
: defaults.runtimes;
|
|
117
|
+
const dockerfile = typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null;
|
|
118
|
+
|
|
119
|
+
if (!dockerfile) {
|
|
120
|
+
let enginesNode: string | undefined;
|
|
121
|
+
try {
|
|
122
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as {
|
|
123
|
+
engines?: { node?: unknown };
|
|
124
|
+
};
|
|
125
|
+
enginesNode = typeof pkg.engines?.node === 'string' ? pkg.engines.node : undefined;
|
|
126
|
+
} catch {
|
|
127
|
+
enginesNode = undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const { runtimes: invalidRuntimes, enginesNode: range } of findRuntimeEngineMismatches(runtimes, enginesNode)) {
|
|
131
|
+
writeStderr(pc.yellow(
|
|
132
|
+
`Warning: sandbox runtimes ${invalidRuntimes.map((runtime) => `"${runtime}"`).join(', ')} do not satisfy this project's package.json "engines.node" ("${range}").\n` +
|
|
133
|
+
' Update "sandbox.runtimes" in .agents/.airc.json (e.g. "node22"), or relax "engines.node".\n'
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
return {
|
|
109
139
|
repoRoot,
|
|
110
140
|
configPath,
|
|
@@ -117,13 +147,11 @@ export function loadConfig({ platformFn = platform }: { platformFn?: PlatformFn
|
|
|
117
147
|
shareBase: hostJoin(home, '.agent-infra', 'share', project),
|
|
118
148
|
dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
|
|
119
149
|
engine,
|
|
120
|
-
runtimes
|
|
121
|
-
? [...sandbox.runtimes]
|
|
122
|
-
: defaults.runtimes,
|
|
150
|
+
runtimes,
|
|
123
151
|
tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
|
|
124
152
|
? [...sandbox.tools]
|
|
125
153
|
: defaults.tools,
|
|
126
|
-
dockerfile
|
|
154
|
+
dockerfile,
|
|
127
155
|
vm: {
|
|
128
156
|
cpu: asPositiveNumberOrNull(sandbox.vm?.cpu) ?? defaults.vm.cpu,
|
|
129
157
|
memory: asPositiveNumberOrNull(sandbox.vm?.memory) ?? defaults.vm.memory,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
3
|
+
export type RuntimeEngineMismatch = {
|
|
4
|
+
runtimes: string[];
|
|
5
|
+
enginesNode: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function nodeMajor(runtime: string): number | null {
|
|
9
|
+
const match = /^node(\d+)$/.exec(runtime);
|
|
10
|
+
return match ? Number(match[1]) : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function findRuntimeEngineMismatches(
|
|
14
|
+
runtimes: string[],
|
|
15
|
+
enginesNode: string | undefined
|
|
16
|
+
): RuntimeEngineMismatch[] {
|
|
17
|
+
if (!enginesNode) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const range = semver.validRange(enginesNode);
|
|
22
|
+
if (!range) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nodeRuntimes: string[] = [];
|
|
27
|
+
for (const runtime of runtimes) {
|
|
28
|
+
const major = nodeMajor(runtime);
|
|
29
|
+
if (major === null) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
nodeRuntimes.push(runtime);
|
|
33
|
+
if (semver.intersects(`${major}.x`, range)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return nodeRuntimes.length > 0 ? [{ runtimes: nodeRuntimes, enginesNode }] : [];
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.6.1",
|
|
3
|
+
"version": "0.6.2-alpha.1",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@clack/prompts": "1.4.0",
|
|
47
47
|
"cross-spawn": "^7.0.6",
|
|
48
48
|
"picocolors": "1.1.1",
|
|
49
|
+
"semver": "^7.8.1",
|
|
49
50
|
"smol-toml": "^1.6.1"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@types/cross-spawn": "^6.0.6",
|
|
63
64
|
"@types/node": "^25.9.1",
|
|
65
|
+
"@types/semver": "^7.7.1",
|
|
64
66
|
"typescript": "~6.0"
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release Platform Commands
|
|
2
2
|
|
|
3
|
-
Read this file before loading release history, querying merged PRs, or
|
|
3
|
+
Read this file before loading release history, querying merged PRs, or publishing the Release notes.
|
|
4
4
|
|
|
5
5
|
## Query Releases
|
|
6
6
|
|
|
@@ -37,10 +37,16 @@ gh issue view {issue-number} --json number,title,labels,url,author
|
|
|
37
37
|
|
|
38
38
|
Map GitHub no-reply emails with this rule: if `Name <email>` contains an email matching `(\d+\+)?(\S+?)@users\.noreply\.github\.com`, use the second capture group lowercased as the login. This covers both `{id}+{login}@users.noreply.github.com` and `{login}@users.noreply.github.com`.
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Publish the Release Notes
|
|
41
|
+
|
|
42
|
+
The GitHub Release for `v{version}` is created and published automatically by the release workflow so Homebrew bottles have a stable upload target. This command writes the curated notes onto that existing Release, falling back to creating it if it does not exist yet.
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
|
-
gh release
|
|
45
|
+
if gh release view "v{version}" >/dev/null 2>&1; then
|
|
46
|
+
gh release edit "v{version}" --notes-file "{notes-file}"
|
|
47
|
+
else
|
|
48
|
+
gh release create "v{version}" --title "v{version}" --notes-file "{notes-file}"
|
|
49
|
+
fi
|
|
44
50
|
```
|
|
45
51
|
|
|
46
52
|
If commands fail, stop or escalate according to the calling skill.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release 平台命令
|
|
2
2
|
|
|
3
|
-
在读取历史 release、查询已合并 PR
|
|
3
|
+
在读取历史 release、查询已合并 PR,或发布 Release notes 前先读取本文件。
|
|
4
4
|
|
|
5
5
|
## Release 查询
|
|
6
6
|
|
|
@@ -37,10 +37,16 @@ gh issue view {issue-number} --json number,title,labels,url,author
|
|
|
37
37
|
|
|
38
38
|
GitHub no-reply 邮箱映射规则:如果 `Name <email>` 中的 email 匹配 `(\d+\+)?(\S+?)@users\.noreply\.github\.com`,使用第二个捕获组的小写形式作为 login。该规则同时覆盖 `{id}+{login}@users.noreply.github.com` 和 `{login}@users.noreply.github.com`。
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## 发布 Release notes
|
|
41
|
+
|
|
42
|
+
`v{version}` 的 GitHub Release 由 release 工作流自动创建并发布,为 Homebrew bottle 提供稳定的上传落点。本命令把精修后的 notes 写到这个已存在的 Release 上;若 Release 尚不存在则兜底创建。
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
|
-
gh release
|
|
45
|
+
if gh release view "v{version}" >/dev/null 2>&1; then
|
|
46
|
+
gh release edit "v{version}" --notes-file "{notes-file}"
|
|
47
|
+
else
|
|
48
|
+
gh release create "v{version}" --title "v{version}" --notes-file "{notes-file}"
|
|
49
|
+
fi
|
|
44
50
|
```
|
|
45
51
|
|
|
46
52
|
失败时按调用方规则停止或提示人工介入。
|
|
@@ -156,31 +156,28 @@ Show the generated release notes to the user.
|
|
|
156
156
|
|
|
157
157
|
Ask:
|
|
158
158
|
1. Need any adjustments?
|
|
159
|
-
2.
|
|
159
|
+
2. Write these notes onto the Release for this version?
|
|
160
160
|
|
|
161
|
-
### 9.
|
|
161
|
+
### 9. Publish the Release Notes (If Confirmed)
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
Write the notes by following the "Publish the Release Notes" command in `.agents/rules/release-commands.md` (it updates the Release already created and published by the release workflow, falling back to creating it if missing).
|
|
164
164
|
|
|
165
165
|
Output:
|
|
166
166
|
```
|
|
167
|
-
|
|
167
|
+
Release notes updated.
|
|
168
168
|
|
|
169
|
-
- URL: {
|
|
169
|
+
- URL: {release-url}
|
|
170
170
|
- Version: v{version}
|
|
171
|
-
- Status:
|
|
171
|
+
- Status: Published
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
1. Open the URL above
|
|
175
|
-
2. Review the release notes
|
|
176
|
-
3. Click "Publish release"
|
|
173
|
+
The notes have been written to the Release. Edit further at the URL above if needed.
|
|
177
174
|
```
|
|
178
175
|
|
|
179
176
|
## Notes
|
|
180
177
|
|
|
181
178
|
1. **Requires the platform CLI**: Must have the platform CLI installed and authenticated
|
|
182
179
|
2. **Tags must exist**: Run the release skill first to create tags
|
|
183
|
-
3. **
|
|
180
|
+
3. **Release auto-published**: the `v{version}` Release is created and published by the release workflow (the upload target for Homebrew bottles); this skill writes/refreshes the notes on that Release
|
|
184
181
|
4. **Classification accuracy**: Auto-classification is based on title/scope/files; complex PRs may need manual adjustment
|
|
185
182
|
|
|
186
183
|
## Error Handling
|
|
@@ -156,31 +156,28 @@ git log v<prev-version>..v<version> \
|
|
|
156
156
|
|
|
157
157
|
询问:
|
|
158
158
|
1. 需要调整吗?
|
|
159
|
-
2.
|
|
159
|
+
2. 是否把 notes 写入该版本的 Release?
|
|
160
160
|
|
|
161
|
-
### 9.
|
|
161
|
+
### 9. 发布 Release notes(如确认)
|
|
162
162
|
|
|
163
|
-
按 `.agents/rules/release-commands.md`
|
|
163
|
+
按 `.agents/rules/release-commands.md` 的「发布 Release notes」命令执行(写入已由 release 工作流自动创建/发布的 Release;不存在时兜底创建)。
|
|
164
164
|
|
|
165
165
|
输出:
|
|
166
166
|
```
|
|
167
|
-
|
|
167
|
+
Release notes 已更新。
|
|
168
168
|
|
|
169
|
-
- URL: {
|
|
169
|
+
- URL: {release-url}
|
|
170
170
|
- Version: v{version}
|
|
171
|
-
- Status:
|
|
171
|
+
- Status: Published
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
1. Open the URL above
|
|
175
|
-
2. Review the release notes
|
|
176
|
-
3. Click "Publish release"
|
|
173
|
+
发布说明已写入该 Release。如需进一步调整,可在上面的 URL 直接编辑。
|
|
177
174
|
```
|
|
178
175
|
|
|
179
176
|
## 注意事项
|
|
180
177
|
|
|
181
178
|
1. **需要 the platform CLI**:必须安装并认证 the platform CLI
|
|
182
179
|
2. **标签必须存在**:先执行 release 技能创建标签
|
|
183
|
-
3.
|
|
180
|
+
3. **Release 已自动发布**:`v{version}` 的 Release 由 release 工作流自动创建并发布(给 Homebrew bottle 提供上传落点);本技能往该 Release 写入/刷新 notes
|
|
184
181
|
4. **分类准确性**:自动分类基于标题/scope/文件;复杂的 PR 可能需要手动调整
|
|
185
182
|
|
|
186
183
|
## 错误处理
|