@fitlab-ai/agent-infra 0.6.5 → 0.7.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.
Files changed (208) hide show
  1. package/README.md +51 -25
  2. package/README.zh-CN.md +49 -23
  3. package/bin/cli.ts +1 -1
  4. package/dist/bin/cli.js +1 -1
  5. package/dist/lib/builtin-tuis.js +45 -0
  6. package/dist/lib/defaults.json +4 -0
  7. package/dist/lib/init.js +65 -23
  8. package/dist/lib/prompt.js +49 -1
  9. package/dist/lib/sandbox/commands/create.js +4 -2
  10. package/dist/lib/sandbox/commands/enter.js +15 -4
  11. package/dist/lib/sandbox/commands/list-running.js +153 -0
  12. package/dist/lib/sandbox/commands/ls.js +24 -45
  13. package/dist/lib/sandbox/commands/rebuild.js +7 -13
  14. package/dist/lib/sandbox/commands/rm.js +2 -0
  15. package/dist/lib/sandbox/config.js +3 -0
  16. package/dist/lib/sandbox/image-prune.js +18 -0
  17. package/dist/lib/sandbox/index.js +2 -1
  18. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
  19. package/dist/lib/sandbox/task-resolver.js +18 -0
  20. package/dist/lib/sandbox/tools.js +213 -8
  21. package/dist/lib/update.js +70 -18
  22. package/lib/builtin-tuis.ts +55 -0
  23. package/lib/defaults.json +4 -0
  24. package/lib/init.ts +97 -35
  25. package/lib/prompt.ts +54 -1
  26. package/lib/sandbox/commands/create.ts +10 -2
  27. package/lib/sandbox/commands/enter.ts +14 -4
  28. package/lib/sandbox/commands/list-running.ts +188 -0
  29. package/lib/sandbox/commands/ls.ts +28 -49
  30. package/lib/sandbox/commands/rebuild.ts +12 -14
  31. package/lib/sandbox/commands/rm.ts +3 -0
  32. package/lib/sandbox/config.ts +7 -0
  33. package/lib/sandbox/image-prune.ts +23 -0
  34. package/lib/sandbox/index.ts +2 -1
  35. package/lib/sandbox/runtimes/ai-tools.dockerfile +10 -6
  36. package/lib/sandbox/task-resolver.ts +23 -1
  37. package/lib/sandbox/tools.ts +248 -9
  38. package/lib/update.ts +85 -30
  39. package/package.json +1 -1
  40. package/templates/.agents/QUICKSTART.en.md +1 -1
  41. package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
  42. package/templates/.agents/README.en.md +111 -2
  43. package/templates/.agents/README.zh-CN.md +111 -2
  44. package/templates/.agents/rules/create-issue.en.md +1 -1
  45. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  46. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  47. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  48. package/templates/.agents/rules/issue-sync.github.en.md +6 -5
  49. package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
  50. package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
  51. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
  52. package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
  53. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
  54. package/templates/.agents/rules/pr-sync.github.en.md +4 -5
  55. package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
  56. package/templates/.agents/rules/task-management.en.md +9 -6
  57. package/templates/.agents/rules/task-management.zh-CN.md +9 -6
  58. package/templates/.agents/rules/task-short-id.en.md +141 -0
  59. package/templates/.agents/rules/task-short-id.zh-CN.md +124 -0
  60. package/templates/.agents/rules/testing-discipline.en.md +2 -2
  61. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
  62. package/templates/.agents/scripts/task-short-id.js +713 -0
  63. package/templates/.agents/scripts/validate-artifact.js +1 -1
  64. package/templates/.agents/skills/analyze-task/SKILL.en.md +20 -4
  65. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +20 -5
  66. package/templates/.agents/skills/block-task/SKILL.en.md +12 -0
  67. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +12 -1
  68. package/templates/.agents/skills/cancel-task/SKILL.en.md +12 -0
  69. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +12 -1
  70. package/templates/.agents/skills/check-task/SKILL.en.md +47 -32
  71. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +46 -32
  72. package/templates/.agents/skills/close-codescan/SKILL.en.md +11 -0
  73. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +11 -0
  74. package/templates/.agents/skills/close-dependabot/SKILL.en.md +11 -0
  75. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +11 -0
  76. package/templates/.agents/skills/code-task/SKILL.en.md +121 -0
  77. package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +55 -25
  78. package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
  79. package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
  80. package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
  81. package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
  82. package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
  83. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
  84. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
  85. package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
  86. package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
  87. package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
  88. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
  89. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
  90. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
  91. package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
  92. package/templates/.agents/skills/commit/SKILL.en.md +6 -2
  93. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -2
  94. package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
  95. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
  96. package/templates/.agents/skills/complete-task/SKILL.en.md +17 -3
  97. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +17 -4
  98. package/templates/.agents/skills/create-pr/SKILL.en.md +21 -1
  99. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +21 -1
  100. package/templates/.agents/skills/create-task/SKILL.en.md +14 -0
  101. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +14 -1
  102. package/templates/.agents/skills/import-codescan/SKILL.en.md +15 -1
  103. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +15 -1
  104. package/templates/.agents/skills/import-dependabot/SKILL.en.md +16 -2
  105. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +16 -2
  106. package/templates/.agents/skills/import-issue/SKILL.en.md +17 -3
  107. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +17 -3
  108. package/templates/.agents/skills/plan-task/SKILL.en.md +8 -4
  109. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +8 -5
  110. package/templates/.agents/skills/restore-task/SKILL.en.md +16 -3
  111. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +16 -4
  112. package/templates/.agents/skills/review-analysis/SKILL.en.md +80 -0
  113. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +105 -0
  114. package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
  115. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
  116. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
  117. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
  118. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
  119. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
  120. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
  121. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
  122. package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +15 -9
  123. package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +19 -10
  124. package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
  125. package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
  126. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
  127. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
  128. package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
  129. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
  130. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
  131. package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
  132. package/templates/.agents/skills/review-plan/SKILL.en.md +80 -0
  133. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +105 -0
  134. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
  135. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
  136. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
  137. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
  138. package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
  139. package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
  140. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
  141. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
  142. package/templates/.agents/skills/test/SKILL.en.md +2 -2
  143. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
  144. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +1 -0
  145. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -0
  146. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +113 -21
  147. package/templates/.agents/templates/task.en.md +4 -3
  148. package/templates/.agents/templates/task.zh-CN.md +3 -2
  149. package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
  150. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
  151. package/templates/.agents/workflows/feature-development.en.yaml +115 -70
  152. package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
  153. package/templates/.agents/workflows/refactoring.en.yaml +123 -78
  154. package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
  155. package/templates/.claude/commands/code-task.en.md +8 -0
  156. package/templates/.claude/commands/code-task.zh-CN.md +8 -0
  157. package/templates/.claude/commands/review-analysis.en.md +8 -0
  158. package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
  159. package/templates/.claude/commands/review-code.en.md +8 -0
  160. package/templates/.claude/commands/review-code.zh-CN.md +8 -0
  161. package/templates/.claude/commands/review-plan.en.md +8 -0
  162. package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
  163. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
  164. package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
  165. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
  166. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
  167. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
  168. package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
  169. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
  170. package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
  171. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
  172. package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
  173. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
  174. package/templates/.opencode/commands/code-task.en.md +11 -0
  175. package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
  176. package/templates/.opencode/commands/review-analysis.en.md +11 -0
  177. package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
  178. package/templates/.opencode/commands/review-code.en.md +11 -0
  179. package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
  180. package/templates/.opencode/commands/review-plan.en.md +11 -0
  181. package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
  182. package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
  183. package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
  184. package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
  185. package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
  186. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
  187. package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
  188. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
  189. package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
  190. package/templates/.claude/commands/implement-task.en.md +0 -8
  191. package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
  192. package/templates/.claude/commands/refine-task.en.md +0 -8
  193. package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
  194. package/templates/.claude/commands/review-task.en.md +0 -8
  195. package/templates/.claude/commands/review-task.zh-CN.md +0 -8
  196. package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
  197. package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
  198. package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
  199. package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
  200. package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
  201. package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
  202. package/templates/.opencode/commands/implement-task.en.md +0 -11
  203. package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
  204. package/templates/.opencode/commands/refine-task.en.md +0 -11
  205. package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
  206. package/templates/.opencode/commands/review-task.en.md +0 -11
  207. package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
  208. /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
@@ -1,11 +1,12 @@
1
1
  import { safeNameCandidates, sanitizeBranchName } from "./constants.js";
2
2
  import { hostJoin } from "./engines/wsl2-paths.js";
3
+ const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
3
4
  function createBuiltinTools(home, project) {
4
5
  return {
5
6
  'claude-code': {
6
7
  id: 'claude-code',
7
8
  name: 'Claude Code',
8
- npmPackage: '@anthropic-ai/claude-code@stable',
9
+ install: { type: 'npm', cmd: '@anthropic-ai/claude-code@stable' },
9
10
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
10
11
  containerMount: '/home/devuser/.claude',
11
12
  versionCmd: 'claude --version',
@@ -35,7 +36,7 @@ function createBuiltinTools(home, project) {
35
36
  codex: {
36
37
  id: 'codex',
37
38
  name: 'Codex',
38
- npmPackage: '@openai/codex',
39
+ install: { type: 'npm', cmd: '@openai/codex' },
39
40
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
40
41
  containerMount: '/home/devuser/.codex',
41
42
  versionCmd: 'codex --version',
@@ -50,7 +51,7 @@ function createBuiltinTools(home, project) {
50
51
  opencode: {
51
52
  id: 'opencode',
52
53
  name: 'OpenCode',
53
- npmPackage: 'opencode-ai',
54
+ install: { type: 'npm', cmd: 'opencode-ai' },
54
55
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
55
56
  containerMount: '/home/devuser/.local/share/opencode',
56
57
  versionCmd: 'opencode version',
@@ -69,7 +70,7 @@ function createBuiltinTools(home, project) {
69
70
  'gemini-cli': {
70
71
  id: 'gemini-cli',
71
72
  name: 'Gemini CLI',
72
- npmPackage: '@google/gemini-cli',
73
+ install: { type: 'npm', cmd: '@google/gemini-cli' },
73
74
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
74
75
  containerMount: '/home/devuser/.gemini',
75
76
  versionCmd: 'gemini --version',
@@ -84,15 +85,200 @@ function createBuiltinTools(home, project) {
84
85
  }
85
86
  };
86
87
  }
88
+ export function builtinToolIds() {
89
+ return Object.keys(createBuiltinTools('', ''));
90
+ }
87
91
  function validateTool(tool) {
88
- if (!tool.npmPackage || !tool.containerMount.startsWith('/')) {
89
- throw new Error(`Invalid sandbox tool descriptor: ${tool.id}`);
92
+ if (!tool.id || !TOOL_ID_PATTERN.test(tool.id)) {
93
+ throw new Error(`Invalid sandbox tool id: ${String(tool.id)}`);
94
+ }
95
+ if (!tool.install || (tool.install.type !== 'npm' && tool.install.type !== 'shell')) {
96
+ throw new Error(`Sandbox tool ${tool.id} has invalid install.type`);
97
+ }
98
+ if (!tool.install.cmd) {
99
+ throw new Error(`Sandbox tool ${tool.id} has empty install.cmd`);
100
+ }
101
+ if (!tool.containerMount || !tool.containerMount.startsWith('/')) {
102
+ throw new Error(`Sandbox tool ${tool.id} containerMount must be an absolute path`);
103
+ }
104
+ }
105
+ function isPlainObject(value) {
106
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
107
+ }
108
+ function asString(value, field, context) {
109
+ if (typeof value !== 'string') {
110
+ throw new Error(`${context}: field "${field}" must be a string`);
111
+ }
112
+ return value;
113
+ }
114
+ function asOptionalNonEmptyString(value, field, context) {
115
+ if (value === undefined) {
116
+ return undefined;
117
+ }
118
+ if (typeof value !== 'string') {
119
+ throw new Error(`${context}: field "${field}" must be a string when provided`);
120
+ }
121
+ if (value.length === 0) {
122
+ throw new Error(`${context}: field "${field}" must be non-empty when provided`);
123
+ }
124
+ return value;
125
+ }
126
+ function asStringRecord(value, field, context) {
127
+ if (value === undefined) {
128
+ return undefined;
129
+ }
130
+ if (!isPlainObject(value)) {
131
+ throw new Error(`${context}: field "${field}" must be an object when provided`);
132
+ }
133
+ const out = {};
134
+ for (const [key, val] of Object.entries(value)) {
135
+ if (typeof val !== 'string') {
136
+ throw new Error(`${context}: field "${field}.${key}" must be a string`);
137
+ }
138
+ out[key] = val;
139
+ }
140
+ return out;
141
+ }
142
+ function asStringArray(value, field, context) {
143
+ if (value === undefined) {
144
+ return undefined;
145
+ }
146
+ if (!Array.isArray(value)) {
147
+ throw new Error(`${context}: field "${field}" must be an array when provided`);
148
+ }
149
+ return value.map((item, index) => {
150
+ if (typeof item !== 'string') {
151
+ throw new Error(`${context}: field "${field}[${index}]" must be a string`);
152
+ }
153
+ return item;
154
+ });
155
+ }
156
+ function parseInstall(value, context) {
157
+ if (!isPlainObject(value)) {
158
+ throw new Error(`${context}: field "install" must be an object`);
159
+ }
160
+ const type = value.type;
161
+ if (type !== 'npm' && type !== 'shell') {
162
+ throw new Error(`${context}: field "install.type" must be "npm" or "shell"`);
163
+ }
164
+ const cmd = asString(value.cmd, 'install.cmd', context);
165
+ if (!cmd) {
166
+ throw new Error(`${context}: field "install.cmd" must be non-empty`);
167
+ }
168
+ return { type, cmd };
169
+ }
170
+ function parseHostPreSeedFiles(value, context) {
171
+ if (value === undefined) {
172
+ return undefined;
173
+ }
174
+ if (!Array.isArray(value)) {
175
+ throw new Error(`${context}: field "hostPreSeedFiles" must be an array when provided`);
90
176
  }
177
+ return value.map((item, index) => {
178
+ if (!isPlainObject(item)) {
179
+ throw new Error(`${context}: field "hostPreSeedFiles[${index}]" must be an object`);
180
+ }
181
+ return {
182
+ hostPath: asString(item.hostPath, `hostPreSeedFiles[${index}].hostPath`, context),
183
+ sandboxName: asString(item.sandboxName, `hostPreSeedFiles[${index}].sandboxName`, context)
184
+ };
185
+ });
186
+ }
187
+ function parseHostPreSeedDirs(value, context) {
188
+ if (value === undefined) {
189
+ return undefined;
190
+ }
191
+ if (!Array.isArray(value)) {
192
+ throw new Error(`${context}: field "hostPreSeedDirs" must be an array when provided`);
193
+ }
194
+ return value.map((item, index) => {
195
+ if (!isPlainObject(item)) {
196
+ throw new Error(`${context}: field "hostPreSeedDirs[${index}]" must be an object`);
197
+ }
198
+ return {
199
+ hostDir: asString(item.hostDir, `hostPreSeedDirs[${index}].hostDir`, context),
200
+ sandboxSubdir: asString(item.sandboxSubdir, `hostPreSeedDirs[${index}].sandboxSubdir`, context)
201
+ };
202
+ });
203
+ }
204
+ function parseHostLiveMounts(value, context) {
205
+ if (value === undefined) {
206
+ return undefined;
207
+ }
208
+ if (!Array.isArray(value)) {
209
+ throw new Error(`${context}: field "hostLiveMounts" must be an array when provided`);
210
+ }
211
+ return value.map((item, index) => {
212
+ if (!isPlainObject(item)) {
213
+ throw new Error(`${context}: field "hostLiveMounts[${index}]" must be an object`);
214
+ }
215
+ return {
216
+ hostPath: asString(item.hostPath, `hostLiveMounts[${index}].hostPath`, context),
217
+ containerSubpath: asString(item.containerSubpath, `hostLiveMounts[${index}].containerSubpath`, context)
218
+ };
219
+ });
220
+ }
221
+ export function parseCustomTool(entry, index, options) {
222
+ const context = `customTools[${index}]`;
223
+ if (!isPlainObject(entry)) {
224
+ throw new Error(`${context} must be an object`);
225
+ }
226
+ const id = asString(entry.id, 'id', context);
227
+ if (!TOOL_ID_PATTERN.test(id)) {
228
+ throw new Error(`${context}: field "id" must match ${TOOL_ID_PATTERN.source}`);
229
+ }
230
+ const containerMount = asOptionalNonEmptyString(entry.containerMount, 'containerMount', context)
231
+ ?? `/home/devuser/.${id}`;
232
+ if (!containerMount.startsWith('/')) {
233
+ throw new Error(`${context}: field "containerMount" must be an absolute path`);
234
+ }
235
+ const tool = {
236
+ id,
237
+ name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
238
+ install: parseInstall(entry.install, context),
239
+ sandboxBase: hostJoin(options.home, '.agent-infra', 'sandboxes', id),
240
+ containerMount,
241
+ versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
242
+ setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
243
+ ?? `Run \`${id}\` inside the container to set up.`,
244
+ envVars: asStringRecord(entry.envVars, 'envVars', context),
245
+ hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
246
+ hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
247
+ pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
248
+ hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
249
+ postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
250
+ };
251
+ validateTool(tool);
252
+ return tool;
253
+ }
254
+ export function parseCustomTools(value, options) {
255
+ if (value === undefined || value === null) {
256
+ return [];
257
+ }
258
+ if (!Array.isArray(value)) {
259
+ throw new Error('sandbox: "customTools" must be an array');
260
+ }
261
+ return value.map((entry, index) => parseCustomTool(entry, index, options));
91
262
  }
92
263
  export function resolveTools(config) {
93
264
  const builtins = createBuiltinTools(config.home, config.project);
265
+ const customs = config.customTools ?? [];
266
+ const seen = new Set();
267
+ for (const tool of customs) {
268
+ if (builtins[tool.id]) {
269
+ throw new Error(`Custom sandbox tool id "${tool.id}" collides with a built-in tool`);
270
+ }
271
+ if (seen.has(tool.id)) {
272
+ throw new Error(`Duplicate sandbox tool id "${tool.id}" in customTools`);
273
+ }
274
+ seen.add(tool.id);
275
+ }
276
+ const merged = { ...builtins };
277
+ for (const tool of customs) {
278
+ merged[tool.id] = tool;
279
+ }
94
280
  return config.tools.map((id) => {
95
- const tool = builtins[id];
281
+ const tool = merged[id];
96
282
  if (!tool) {
97
283
  throw new Error(`Unknown sandbox tool: ${id}`);
98
284
  }
@@ -110,6 +296,25 @@ export function toolProjectDirCandidates(tool, project) {
110
296
  return [hostJoin(tool.sandboxBase, project)];
111
297
  }
112
298
  export function toolNpmPackagesArg(tools) {
113
- return tools.map((tool) => tool.npmPackage).join(' ');
299
+ return tools
300
+ .filter((tool) => tool.install.type === 'npm')
301
+ .map((tool) => tool.install.cmd)
302
+ .join(' ');
303
+ }
304
+ export function toolShellInstallScript(tools) {
305
+ const blocks = tools
306
+ .filter((tool) => tool.install.type === 'shell')
307
+ .map((tool) => `# install: ${tool.id}\n${tool.install.cmd}`);
308
+ if (blocks.length === 0) {
309
+ return '';
310
+ }
311
+ return ['#!/bin/bash', 'set -e', '', ...blocks, ''].join('\n');
312
+ }
313
+ export function toolShellInstallScriptBase64(tools) {
314
+ const script = toolShellInstallScript(tools);
315
+ return script ? Buffer.from(script, 'utf8').toString('base64') : '';
316
+ }
317
+ export function imageSignatureFields(tools) {
318
+ return tools.map((tool) => ({ id: tool.id, install: tool.install }));
114
319
  }
115
320
  //# sourceMappingURL=tools.js.map
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { info, ok, err } from "./log.js";
4
4
  import { resolveTemplateDir } from "./paths.js";
5
5
  import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
6
+ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
6
7
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
7
8
  const CONFIG_DIR = '.agents';
8
9
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
@@ -15,7 +16,7 @@ function isPathOwnedByOtherPlatform(relativePath, platformType) {
15
16
  return false;
16
17
  return candidate !== platformType;
17
18
  }
18
- function syncFileRegistry(config, platformType) {
19
+ function syncFileRegistry(config, platformType, enabledTUIs) {
19
20
  config.files ||= {};
20
21
  const before = JSON.stringify({
21
22
  files: {
@@ -36,6 +37,8 @@ function syncFileRegistry(config, platformType) {
36
37
  for (const entry of defaults.files.managed) {
37
38
  if (isPathOwnedByOtherPlatform(entry, platformType))
38
39
  continue;
40
+ if (isPathOwnedByDisabledTUI(entry, enabledTUIs))
41
+ continue;
39
42
  if (!allExisting.includes(entry)) {
40
43
  config.files.managed.push(entry);
41
44
  added.managed.push(entry);
@@ -44,6 +47,8 @@ function syncFileRegistry(config, platformType) {
44
47
  for (const entry of defaults.files.merged) {
45
48
  if (isPathOwnedByOtherPlatform(entry, platformType))
46
49
  continue;
50
+ if (isPathOwnedByDisabledTUI(entry, enabledTUIs))
51
+ continue;
47
52
  if (!allExisting.includes(entry)) {
48
53
  config.files.merged.push(entry);
49
54
  added.merged.push(entry);
@@ -82,6 +87,7 @@ async function cmdUpdate() {
82
87
  const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
83
88
  const { project, org, language } = config;
84
89
  const platformType = config.platform?.type || defaults.platform.type;
90
+ const enabledTUIs = resolveEnabledTUIs(config.tuis);
85
91
  const replacements = { project, org };
86
92
  info(`Updating seed files for: ${project}`);
87
93
  console.log('');
@@ -106,21 +112,29 @@ async function cmdUpdate() {
106
112
  catch {
107
113
  // Ignore missing legacy script from pre-ESM installs.
108
114
  }
109
- // update Claude command
110
- renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
111
- ok('Updated .claude/commands/update-agent-infra.md');
112
- // update Gemini command
113
- renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
114
- ok(`Updated .gemini/commands/${project}/update-agent-infra.toml`);
115
- // update OpenCode command
116
- renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
117
- ok('Updated .opencode/commands/update-agent-infra.md');
115
+ // update Claude command (only if enabled)
116
+ if (enabledTUIs.has('claude-code')) {
117
+ renderFile(path.join(templateDir, '.claude', 'commands', claudeSrc), path.join('.claude', 'commands', 'update-agent-infra.md'), replacements);
118
+ ok('Updated .claude/commands/update-agent-infra.md');
119
+ }
120
+ // update Gemini command (only if enabled)
121
+ if (enabledTUIs.has('gemini-cli')) {
122
+ renderFile(path.join(templateDir, '.gemini', 'commands', '_project_', geminiSrc), path.join('.gemini', 'commands', project, 'update-agent-infra.toml'), replacements);
123
+ ok(`Updated .gemini/commands/${project}/update-agent-infra.toml`);
124
+ }
125
+ // update OpenCode command (only if enabled)
126
+ if (enabledTUIs.has('opencode')) {
127
+ renderFile(path.join(templateDir, '.opencode', 'commands', opencodeSrc), path.join('.opencode', 'commands', 'update-agent-infra.md'), replacements);
128
+ ok('Updated .opencode/commands/update-agent-infra.md');
129
+ }
118
130
  // sync file registry
119
- const { added, changed } = syncFileRegistry(config, platformType);
131
+ const { added, changed } = syncFileRegistry(config, platformType, enabledTUIs);
120
132
  const hasNewEntries = added.managed.length > 0 || added.merged.length > 0;
121
133
  const platformAdded = !config.platform;
122
134
  const sandboxAdded = !config.sandbox;
135
+ const taskAdded = !config.task;
123
136
  const labelsAdded = !config.labels;
137
+ const requiresPullRequestAdded = config.requiresPullRequest === undefined;
124
138
  let configChanged = changed;
125
139
  if (platformAdded) {
126
140
  config.platform = structuredClone(defaults.platform);
@@ -130,10 +144,18 @@ async function cmdUpdate() {
130
144
  config.sandbox = structuredClone(defaults.sandbox);
131
145
  configChanged = true;
132
146
  }
147
+ if (taskAdded) {
148
+ config.task = structuredClone(defaults.task);
149
+ configChanged = true;
150
+ }
133
151
  if (labelsAdded) {
134
152
  config.labels = structuredClone(defaults.labels);
135
153
  configChanged = true;
136
154
  }
155
+ if (requiresPullRequestAdded) {
156
+ config.requiresPullRequest = defaults.requiresPullRequest;
157
+ configChanged = true;
158
+ }
137
159
  if (configChanged) {
138
160
  console.log('');
139
161
  if (hasNewEntries) {
@@ -145,16 +167,22 @@ async function cmdUpdate() {
145
167
  ok(` merged: ${entry}`);
146
168
  }
147
169
  }
148
- else if (platformAdded || sandboxAdded || labelsAdded) {
170
+ else if (platformAdded || sandboxAdded || taskAdded || labelsAdded || requiresPullRequestAdded) {
149
171
  if (platformAdded) {
150
172
  info(`Default platform config added to ${CONFIG_PATH}.`);
151
173
  }
152
174
  if (sandboxAdded) {
153
175
  info(`Default sandbox config added to ${CONFIG_PATH}.`);
154
176
  }
177
+ if (taskAdded) {
178
+ info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
179
+ }
155
180
  if (labelsAdded) {
156
181
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
157
182
  }
183
+ if (requiresPullRequestAdded) {
184
+ info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
185
+ }
158
186
  }
159
187
  else {
160
188
  info(`File registry changed in ${CONFIG_PATH}.`);
@@ -162,12 +190,18 @@ async function cmdUpdate() {
162
190
  if (hasNewEntries && sandboxAdded) {
163
191
  info(`Default sandbox config added to ${CONFIG_PATH}.`);
164
192
  }
193
+ if (hasNewEntries && taskAdded) {
194
+ info(`Default task.shortIdLength=${defaults.task.shortIdLength} added to ${CONFIG_PATH}.`);
195
+ }
165
196
  if (hasNewEntries && labelsAdded) {
166
197
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
167
198
  }
168
199
  if (hasNewEntries && platformAdded) {
169
200
  info(`Default platform config added to ${CONFIG_PATH}.`);
170
201
  }
202
+ if (hasNewEntries && requiresPullRequestAdded) {
203
+ info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
204
+ }
171
205
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
172
206
  ok(`Updated ${CONFIG_PATH}`);
173
207
  }
@@ -175,12 +209,30 @@ async function cmdUpdate() {
175
209
  console.log('');
176
210
  ok('Seed files updated successfully!');
177
211
  console.log('');
178
- console.log(' Next step: run the full update in your AI TUI:');
179
- console.log('');
180
- console.log(' Claude Code / OpenCode: /update-agent-infra');
181
- console.log(` Gemini CLI: /${project}:update-agent-infra`);
182
- console.log(' Codex CLI: $update-agent-infra');
183
- console.log('');
212
+ if (enabledTUIs.size === 0) {
213
+ console.log(' No built-in TUI enabled (tuis: []).');
214
+ console.log(` Configure "customTUIs" in ${CONFIG_PATH} if needed.`);
215
+ console.log('');
216
+ }
217
+ else {
218
+ console.log(' Next step: run the full update in your AI TUI:');
219
+ console.log('');
220
+ const claudeOrOpencode = [];
221
+ if (enabledTUIs.has('claude-code'))
222
+ claudeOrOpencode.push('Claude Code');
223
+ if (enabledTUIs.has('opencode'))
224
+ claudeOrOpencode.push('OpenCode');
225
+ if (claudeOrOpencode.length > 0) {
226
+ console.log(` ${claudeOrOpencode.join(' / ')}: /update-agent-infra`);
227
+ }
228
+ if (enabledTUIs.has('gemini-cli')) {
229
+ console.log(` Gemini CLI: /${project}:update-agent-infra`);
230
+ }
231
+ if (enabledTUIs.has('codex')) {
232
+ console.log(' Codex CLI: $update-agent-infra');
233
+ }
234
+ console.log('');
235
+ }
184
236
  }
185
237
  export { cmdUpdate };
186
238
  //# sourceMappingURL=update.js.map
@@ -0,0 +1,55 @@
1
+ const BUILTIN_TUI_IDS = ['claude-code', 'codex', 'gemini-cli', 'opencode'] as const;
2
+ type BuiltinTUIId = (typeof BUILTIN_TUI_IDS)[number];
3
+
4
+ const BUILTIN_TUI_DISPLAY: Record<BuiltinTUIId, string> = {
5
+ 'claude-code': 'Claude Code',
6
+ 'codex': 'Codex',
7
+ 'gemini-cli': 'Gemini CLI',
8
+ 'opencode': 'OpenCode'
9
+ };
10
+
11
+ const BUILTIN_TUI_OWNED_PATH_PREFIXES: Record<BuiltinTUIId, string[]> = {
12
+ 'claude-code': ['.claude/'],
13
+ 'codex': ['.codex/'],
14
+ 'gemini-cli': ['.gemini/'],
15
+ 'opencode': ['.opencode/']
16
+ };
17
+
18
+ function isBuiltinTUIId(value: unknown): value is BuiltinTUIId {
19
+ return typeof value === 'string' && (BUILTIN_TUI_IDS as readonly string[]).includes(value);
20
+ }
21
+
22
+ function resolveEnabledTUIs(value: unknown): Set<BuiltinTUIId> {
23
+ // Missing field / null / non-array → full set (backward compat for legacy
24
+ // .airc.json predating the `tuis` field).
25
+ if (!Array.isArray(value)) return new Set(BUILTIN_TUI_IDS);
26
+ // Empty array is a meaningful, user-set value: "no built-in TUI managed".
27
+ // This supports the customTUI-only project layout.
28
+ const set = new Set<BuiltinTUIId>();
29
+ for (const v of value) {
30
+ if (isBuiltinTUIId(v)) set.add(v);
31
+ }
32
+ return set;
33
+ }
34
+
35
+ function isPathOwnedByDisabledTUI(rel: string, enabled: Set<BuiltinTUIId>): boolean {
36
+ const normalized = String(rel || '').replace(/\\/g, '/').replace(/^\.\//, '');
37
+ for (const tui of BUILTIN_TUI_IDS) {
38
+ if (enabled.has(tui)) continue;
39
+ for (const prefix of BUILTIN_TUI_OWNED_PATH_PREFIXES[tui]) {
40
+ const trimmed = prefix.replace(/\/$/, '');
41
+ if (normalized === trimmed || normalized.startsWith(prefix)) return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+
47
+ export {
48
+ BUILTIN_TUI_IDS,
49
+ BUILTIN_TUI_DISPLAY,
50
+ BUILTIN_TUI_OWNED_PATH_PREFIXES,
51
+ isBuiltinTUIId,
52
+ resolveEnabledTUIs,
53
+ isPathOwnedByDisabledTUI
54
+ };
55
+ export type { BuiltinTUIId };
package/lib/defaults.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "platform": {
3
3
  "type": "github"
4
4
  },
5
+ "requiresPullRequest": true,
5
6
  "sandbox": {
6
7
  "engine": null,
7
8
  "runtimes": [
@@ -20,6 +21,9 @@
20
21
  "disk": null
21
22
  }
22
23
  },
24
+ "task": {
25
+ "shortIdLength": 2
26
+ },
23
27
  "labels": {
24
28
  "in": {}
25
29
  },