@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.
@@ -5,7 +5,7 @@
5
5
  "sandbox": {
6
6
  "engine": null,
7
7
  "runtimes": [
8
- "node20"
8
+ "node22"
9
9
  ],
10
10
  "tools": [
11
11
  "claude-code",
@@ -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: ['node20'],
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: Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
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: typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null,
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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.6.1",
3
+ "version": "0.6.2-alpha.1",
4
4
  "type": "module"
5
5
  }
package/lib/defaults.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "sandbox": {
6
6
  "engine": null,
7
7
  "runtimes": [
8
- "node20"
8
+ "node22"
9
9
  ],
10
10
  "tools": [
11
11
  "claude-code",
@@ -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: ['node20'],
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({ platformFn = platform }: { platformFn?: PlatformFn } = {}): SandboxConfig {
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: Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
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: typeof sandbox.dockerfile === 'string' ? sandbox.dockerfile : defaults.dockerfile ?? null,
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 creating a draft release.
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
- ## Create a Draft Release
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 create "v{version}" --draft --title "v{version}" --notes-file "{notes-file}"
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,或创建 draft release 前先读取本文件。
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
- ## 创建 Draft Release
40
+ ## 发布 Release notes
41
+
42
+ `v{version}` 的 GitHub Release 由 release 工作流自动创建并发布,为 Homebrew bottle 提供稳定的上传落点。本命令把精修后的 notes 写到这个已存在的 Release 上;若 Release 尚不存在则兜底创建。
41
43
 
42
44
  ```bash
43
- gh release create "v{version}" --draft --title "v{version}" --notes-file "{notes-file}"
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. Create a draft release?
159
+ 2. Write these notes onto the Release for this version?
160
160
 
161
- ### 9. Create Draft Release (If Confirmed)
161
+ ### 9. Publish the Release Notes (If Confirmed)
162
162
 
163
- Create the draft release by following `.agents/rules/release-commands.md`.
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
- Draft Release created.
167
+ Release notes updated.
168
168
 
169
- - URL: {draft-release-url}
169
+ - URL: {release-url}
170
170
  - Version: v{version}
171
- - Status: Draft
171
+ - Status: Published
172
172
 
173
- Please review and publish on the platform:
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. **Draft mode**: Creates a draft - won't auto-publish
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. 是否创建 draft release
159
+ 2. 是否把 notes 写入该版本的 Release
160
160
 
161
- ### 9. 创建 Draft Release(如确认)
161
+ ### 9. 发布 Release notes(如确认)
162
162
 
163
- 按 `.agents/rules/release-commands.md` Draft Release 创建命令执行。
163
+ 按 `.agents/rules/release-commands.md` 的「发布 Release notes」命令执行(写入已由 release 工作流自动创建/发布的 Release;不存在时兜底创建)。
164
164
 
165
165
  输出:
166
166
  ```
167
- Draft Release created.
167
+ Release notes 已更新。
168
168
 
169
- - URL: {draft-release-url}
169
+ - URL: {release-url}
170
170
  - Version: v{version}
171
- - Status: Draft
171
+ - Status: Published
172
172
 
173
- Please review and publish on the platform:
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
  ## 错误处理
@@ -26,7 +26,7 @@ const DEFAULTS = {
26
26
  "sandbox": {
27
27
  "engine": null,
28
28
  "runtimes": [
29
- "node20"
29
+ "node22"
30
30
  ],
31
31
  "tools": [
32
32
  "claude-code",