@fitlab-ai/agent-infra 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +200 -8
  2. package/README.zh-CN.md +176 -8
  3. package/bin/{cli.js → cli.ts} +23 -19
  4. package/dist/bin/cli.js +116 -0
  5. package/dist/lib/defaults.json +61 -0
  6. package/dist/lib/init.js +238 -0
  7. package/dist/lib/log.js +18 -0
  8. package/dist/lib/merge.js +747 -0
  9. package/dist/lib/paths.js +18 -0
  10. package/dist/lib/prompt.js +85 -0
  11. package/dist/lib/render.js +139 -0
  12. package/dist/lib/sandbox/commands/create.js +1173 -0
  13. package/dist/lib/sandbox/commands/enter.js +98 -0
  14. package/dist/lib/sandbox/commands/ls.js +93 -0
  15. package/dist/lib/sandbox/commands/rebuild.js +101 -0
  16. package/dist/lib/sandbox/commands/refresh.js +85 -0
  17. package/dist/lib/sandbox/commands/rm.js +226 -0
  18. package/dist/lib/sandbox/commands/vm.js +144 -0
  19. package/dist/lib/sandbox/config.js +85 -0
  20. package/dist/lib/sandbox/constants.js +104 -0
  21. package/dist/lib/sandbox/credentials.js +437 -0
  22. package/dist/lib/sandbox/dockerfile.js +76 -0
  23. package/dist/lib/sandbox/dotfiles.js +170 -0
  24. package/dist/lib/sandbox/engine.js +155 -0
  25. package/dist/lib/sandbox/engines/colima.js +64 -0
  26. package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
  27. package/dist/lib/sandbox/engines/index.js +25 -0
  28. package/dist/lib/sandbox/engines/native.js +96 -0
  29. package/dist/lib/sandbox/engines/orbstack.js +63 -0
  30. package/dist/lib/sandbox/engines/selinux.js +48 -0
  31. package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
  32. package/dist/lib/sandbox/engines/wsl2.js +57 -0
  33. package/dist/lib/sandbox/index.js +70 -0
  34. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
  35. package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
  36. package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
  37. package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
  38. package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
  39. package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
  40. package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
  41. package/dist/lib/sandbox/shell.js +148 -0
  42. package/dist/lib/sandbox/task-resolver.js +35 -0
  43. package/dist/lib/sandbox/tools.js +115 -0
  44. package/dist/lib/update.js +186 -0
  45. package/dist/lib/version.js +5 -0
  46. package/dist/package.json +5 -0
  47. package/lib/{init.js → init.ts} +64 -20
  48. package/lib/{log.js → log.ts} +4 -4
  49. package/lib/{merge.js → merge.ts} +129 -63
  50. package/lib/paths.ts +18 -0
  51. package/lib/{prompt.js → prompt.ts} +12 -12
  52. package/lib/{render.js → render.ts} +30 -17
  53. package/lib/sandbox/commands/create.ts +1507 -0
  54. package/lib/sandbox/commands/enter.ts +115 -0
  55. package/lib/sandbox/commands/{ls.js → ls.ts} +41 -10
  56. package/lib/sandbox/commands/rebuild.ts +135 -0
  57. package/lib/sandbox/commands/refresh.ts +128 -0
  58. package/lib/sandbox/commands/{rm.js → rm.ts} +71 -21
  59. package/lib/sandbox/commands/{vm.js → vm.ts} +62 -15
  60. package/lib/sandbox/config.ts +133 -0
  61. package/lib/sandbox/{constants.js → constants.ts} +41 -17
  62. package/lib/sandbox/credentials.ts +634 -0
  63. package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
  64. package/lib/sandbox/dotfiles.ts +236 -0
  65. package/lib/sandbox/engine.ts +231 -0
  66. package/lib/sandbox/engines/colima.ts +81 -0
  67. package/lib/sandbox/engines/docker-desktop.ts +36 -0
  68. package/lib/sandbox/engines/index.ts +74 -0
  69. package/lib/sandbox/engines/native.ts +131 -0
  70. package/lib/sandbox/engines/orbstack.ts +78 -0
  71. package/lib/sandbox/engines/selinux.ts +66 -0
  72. package/lib/sandbox/engines/wsl2-paths.ts +65 -0
  73. package/lib/sandbox/engines/wsl2.ts +74 -0
  74. package/lib/sandbox/{index.js → index.ts} +17 -8
  75. package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
  76. package/lib/sandbox/runtimes/base.dockerfile +116 -1
  77. package/lib/sandbox/shell.ts +186 -0
  78. package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
  79. package/lib/sandbox/{tools.js → tools.ts} +33 -29
  80. package/lib/{update.js → update.ts} +33 -10
  81. package/package.json +22 -12
  82. package/templates/.agents/rules/create-issue.github.en.md +2 -4
  83. package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
  84. package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
  85. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
  86. package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
  87. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
  88. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  89. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  90. package/templates/.agents/skills/import-issue/SKILL.en.md +6 -8
  91. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
  92. package/lib/paths.js +0 -9
  93. package/lib/sandbox/commands/create.js +0 -1174
  94. package/lib/sandbox/commands/enter.js +0 -79
  95. package/lib/sandbox/commands/rebuild.js +0 -102
  96. package/lib/sandbox/config.js +0 -84
  97. package/lib/sandbox/engine.js +0 -256
  98. package/lib/sandbox/shell.js +0 -122
  99. package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
  100. /package/lib/{version.js → version.ts} +0 -0
@@ -0,0 +1,186 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import type { ExecFileSyncOptions, StdioOptions, SpawnSyncOptions, SpawnSyncReturns } from 'node:child_process';
5
+
6
+ const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
7
+
8
+ type CommandOptions = {
9
+ cwd?: string;
10
+ encoding?: BufferEncoding;
11
+ stdio?: StdioOptions;
12
+ timeout?: number;
13
+ shell?: boolean;
14
+ };
15
+
16
+ type RunProbeOptions = CommandOptions & {
17
+ spawnFn?: typeof spawnSync;
18
+ };
19
+
20
+ function normalizeOptions(opts: CommandOptions = {}, stdio: StdioOptions): CommandOptions {
21
+ return {
22
+ cwd: opts.cwd,
23
+ encoding: opts.encoding,
24
+ stdio,
25
+ timeout: opts.timeout ?? DEFAULT_TIMEOUT_MS
26
+ };
27
+ }
28
+
29
+ function resolveCommand(cmd: string): string {
30
+ if (process.platform !== 'win32' || path.extname(cmd)) {
31
+ return cmd;
32
+ }
33
+
34
+ const pathValue = process.env.Path || process.env.PATH || '';
35
+ const extensions = (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
36
+ .split(';')
37
+ .filter(Boolean);
38
+
39
+ for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
40
+ for (const extension of extensions) {
41
+ const candidate = path.join(dir, `${cmd}${extension.toLowerCase()}`);
42
+ if (fs.existsSync(candidate)) {
43
+ return candidate;
44
+ }
45
+ const upperCandidate = path.join(dir, `${cmd}${extension.toUpperCase()}`);
46
+ if (fs.existsSync(upperCandidate)) {
47
+ return upperCandidate;
48
+ }
49
+ }
50
+ }
51
+
52
+ return cmd;
53
+ }
54
+
55
+ function commandOptions<T extends CommandOptions>(cmd: string, opts: T): T | (T & { shell: true }) {
56
+ if (process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(cmd)) {
57
+ return { ...opts, shell: true };
58
+ }
59
+ return opts;
60
+ }
61
+
62
+ export function run(cmd: string, args: string[], opts: CommandOptions = {}): string {
63
+ const resolved = resolveCommand(cmd);
64
+ return execFileSync(resolved, args, commandOptions(resolved, {
65
+ ...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
66
+ encoding: 'utf8'
67
+ })).trim();
68
+ }
69
+
70
+ export function runOk(cmd: string, args: string[], opts: CommandOptions = {}): boolean {
71
+ const resolved = resolveCommand(cmd);
72
+ const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'pipe')));
73
+ return result.status === 0;
74
+ }
75
+
76
+ export function restoreTerminal(): void {
77
+ if (!process.stdout.isTTY) {
78
+ return;
79
+ }
80
+
81
+ try {
82
+ process.stdout.write([
83
+ '\x1b[?1049l',
84
+ '\x1b[?25h',
85
+ '\x1b>',
86
+ '\x1b[?1000l',
87
+ '\x1b[?1002l',
88
+ '\x1b[?1003l',
89
+ '\x1b[?1006l'
90
+ ].join(''));
91
+ } catch {
92
+ // Best-effort cleanup only; preserve the original command result.
93
+ }
94
+
95
+ if (process.platform === 'win32') {
96
+ return;
97
+ }
98
+
99
+ try {
100
+ execFileSync('stty', ['sane'], { stdio: 'inherit' });
101
+ } catch {
102
+ // Some environments do not provide stty or reject sane; ANSI reset still helps.
103
+ }
104
+ }
105
+
106
+ export function runInteractive(cmd: string, args: string[], opts: CommandOptions = {}): number {
107
+ const resolved = resolveCommand(cmd);
108
+ try {
109
+ const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
110
+ return result.status ?? 1;
111
+ } finally {
112
+ restoreTerminal();
113
+ }
114
+ }
115
+
116
+ export function runVerbose(cmd: string, args: string[], opts: CommandOptions = {}): void {
117
+ const resolved = resolveCommand(cmd);
118
+ const result = spawnSync(resolved, args, commandOptions(resolved, normalizeOptions(opts, 'inherit')));
119
+
120
+ if (result.status !== 0) {
121
+ if (result.signal === 'SIGTERM') {
122
+ throw new Error(`Command timed out after ${opts.timeout ?? DEFAULT_TIMEOUT_MS}ms: ${cmd}`);
123
+ }
124
+ throw new Error(`Command failed with exit code ${result.status}: ${cmd}`);
125
+ }
126
+ }
127
+
128
+ export function runSafe(cmd: string, args: string[], opts: CommandOptions = {}): string {
129
+ const resolved = resolveCommand(cmd);
130
+ const result = spawnSync(resolved, args, commandOptions(resolved, {
131
+ ...normalizeOptions(opts, ['pipe', 'pipe', 'pipe']),
132
+ encoding: 'utf8',
133
+ }));
134
+ if (result.status !== 0 && result.stderr) {
135
+ process.stderr.write(result.stderr);
136
+ }
137
+ return (result.stdout ?? '').trim();
138
+ }
139
+
140
+ export function commandForEngine(engine: string, cmd: string, args: string[] = []): { cmd: string; args: string[] } {
141
+ if (engine === 'wsl2') {
142
+ const resolvedWrapper = resolveCommand('wsl.exe');
143
+ return { cmd: resolvedWrapper, args: ['--', cmd, ...args] };
144
+ }
145
+
146
+ return { cmd, args };
147
+ }
148
+
149
+ export function runEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): string {
150
+ const command = commandForEngine(engine, cmd, args);
151
+ return run(command.cmd, command.args, opts);
152
+ }
153
+
154
+ export function execEngine(engine: string, cmd: string, args: string[], opts: ExecFileSyncOptions = {}) {
155
+ const command = commandForEngine(engine, cmd, args);
156
+ return execFileSync(command.cmd, command.args, opts);
157
+ }
158
+
159
+ export function runOkEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): boolean {
160
+ const command = commandForEngine(engine, cmd, args);
161
+ return runOk(command.cmd, command.args, opts);
162
+ }
163
+
164
+ export function runSafeEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): string {
165
+ const command = commandForEngine(engine, cmd, args);
166
+ return runSafe(command.cmd, command.args, opts);
167
+ }
168
+
169
+ export function runVerboseEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): void {
170
+ const command = commandForEngine(engine, cmd, args);
171
+ return runVerbose(command.cmd, command.args, opts);
172
+ }
173
+
174
+ export function runInteractiveEngine(engine: string, cmd: string, args: string[], opts: CommandOptions = {}): number {
175
+ const command = commandForEngine(engine, cmd, args);
176
+ return runInteractive(command.cmd, command.args, opts);
177
+ }
178
+
179
+ export function runProbe(cmd: string, args: string[], opts: RunProbeOptions = {}): SpawnSyncReturns<string | Buffer> {
180
+ const { spawnFn = spawnSync, ...commandOpts } = opts;
181
+ const resolved = resolveCommand(cmd);
182
+ return spawnFn(resolved, args, commandOptions(resolved, normalizeOptions(
183
+ { encoding: 'utf8', ...commandOpts },
184
+ commandOpts.stdio ?? ['pipe', 'pipe', 'pipe']
185
+ )));
186
+ }
@@ -4,11 +4,11 @@ import path from 'node:path';
4
4
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
5
5
  const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
6
6
 
7
- function stripQuotes(value) {
7
+ function stripQuotes(value: string): string {
8
8
  return value.replace(/^(["'])(.*)\1$/, '$2');
9
9
  }
10
10
 
11
- function readTaskContent(repoRoot, taskId) {
11
+ function readTaskContent(repoRoot: string, taskId: string): string {
12
12
  for (const dir of WORKSPACE_DIRS) {
13
13
  const taskPath = path.join(repoRoot, '.agents', 'workspace', dir, taskId, 'task.md');
14
14
  if (fs.existsSync(taskPath)) {
@@ -18,21 +18,21 @@ function readTaskContent(repoRoot, taskId) {
18
18
  throw new Error(`Task not found: ${taskId}`);
19
19
  }
20
20
 
21
- function resolveBranchFromTaskContent(content, taskId) {
21
+ function resolveBranchFromTaskContent(content: string, taskId: string): string {
22
22
  const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
23
- if (frontmatterBranch && frontmatterBranch[1].trim()) {
23
+ if (frontmatterBranch?.[1]?.trim()) {
24
24
  return stripQuotes(frontmatterBranch[1].trim());
25
25
  }
26
26
 
27
27
  const contextBranch = content.match(/^- \*\*(?:分支|Branch)\*\*:[ \t]*`?([^`\n]+)`?$/m);
28
- if (contextBranch && contextBranch[1].trim()) {
28
+ if (contextBranch?.[1]?.trim()) {
29
29
  return stripQuotes(contextBranch[1].trim());
30
30
  }
31
31
 
32
32
  throw new Error(`Task ${taskId} has no branch field in task.md`);
33
33
  }
34
34
 
35
- export function resolveTaskBranch(arg, repoRoot) {
35
+ export function resolveTaskBranch(arg: string, repoRoot: string): string {
36
36
  if (!TASK_ID_RE.test(arg)) {
37
37
  return arg;
38
38
  }
@@ -1,29 +1,29 @@
1
- import path from 'node:path';
2
- import { safeNameCandidates, sanitizeBranchName } from './constants.js';
1
+ import { safeNameCandidates, sanitizeBranchName } from './constants.ts';
2
+ import { hostJoin } from './engines/wsl2-paths.ts';
3
3
 
4
- /**
5
- * @typedef {Object} SandboxTool
6
- * @property {string} id
7
- * @property {string} name
8
- * @property {string} npmPackage
9
- * @property {string} sandboxBase
10
- * @property {string} containerMount
11
- * @property {string} versionCmd
12
- * @property {string} setupHint
13
- * @property {Record<string, string>=} envVars
14
- * @property {Array<{ hostPath: string, sandboxName: string }>=} hostPreSeedFiles
15
- * @property {Array<{ hostDir: string, sandboxSubdir: string }>=} hostPreSeedDirs
16
- * @property {string[]=} pathRewriteFiles
17
- * @property {Array<{ hostPath: string, containerSubpath: string }>=} hostLiveMounts
18
- * @property {string[]=} postSetupCmds
19
- */
4
+ export type SandboxTool = {
5
+ id: string;
6
+ name: string;
7
+ npmPackage: string;
8
+ sandboxBase: string;
9
+ containerMount: string;
10
+ versionCmd: string;
11
+ setupHint: string;
12
+ envVars?: Record<string, string>;
13
+ hostPreSeedFiles?: Array<{ hostPath: string; sandboxName: string }>;
14
+ hostPreSeedDirs?: Array<{ hostDir: string; sandboxSubdir: string }>;
15
+ pathRewriteFiles?: string[];
16
+ hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
17
+ postSetupCmds?: string[];
18
+ };
20
19
 
21
- function hostJoin(basePath, ...segments) {
22
- return basePath.startsWith('/') ? path.posix.join(basePath, ...segments) : path.join(basePath, ...segments);
23
- }
20
+ type ToolsConfig = {
21
+ home: string;
22
+ project: string;
23
+ tools: string[];
24
+ };
24
25
 
25
- function createBuiltinTools(home, project) {
26
- /** @type {Record<string, SandboxTool>} */
26
+ function createBuiltinTools(home: string, project: string): Record<string, SandboxTool> {
27
27
  return {
28
28
  'claude-code': {
29
29
  id: 'claude-code',
@@ -78,6 +78,10 @@ function createBuiltinTools(home, project) {
78
78
  containerMount: '/home/devuser/.local/share/opencode',
79
79
  versionCmd: 'opencode version',
80
80
  setupHint: 'Configure OpenCode credentials inside the container before first use.',
81
+ // OpenCode reads opencode.json from $XDG_CONFIG_HOME/opencode by default,
82
+ // outside this tool mount. Pin the config file path so the inherited
83
+ // sandbox opencode.json is the one the TUI actually reads.
84
+ envVars: { OPENCODE_CONFIG: '/home/devuser/.local/share/opencode/opencode.json' },
81
85
  hostLiveMounts: [
82
86
  {
83
87
  hostPath: hostJoin(home, '.local', 'share', 'opencode', 'auth.json'),
@@ -104,13 +108,13 @@ function createBuiltinTools(home, project) {
104
108
  };
105
109
  }
106
110
 
107
- function validateTool(tool) {
111
+ function validateTool(tool: SandboxTool): void {
108
112
  if (!tool.npmPackage || !tool.containerMount.startsWith('/')) {
109
113
  throw new Error(`Invalid sandbox tool descriptor: ${tool.id}`);
110
114
  }
111
115
  }
112
116
 
113
- export function resolveTools(config) {
117
+ export function resolveTools(config: ToolsConfig): SandboxTool[] {
114
118
  const builtins = createBuiltinTools(config.home, config.project);
115
119
  return config.tools.map((id) => {
116
120
  const tool = builtins[id];
@@ -122,18 +126,18 @@ export function resolveTools(config) {
122
126
  });
123
127
  }
124
128
 
125
- export function toolConfigDir(tool, project, branch) {
129
+ export function toolConfigDir(tool: SandboxTool, project: string, branch: string): string {
126
130
  return hostJoin(tool.sandboxBase, project, sanitizeBranchName(branch));
127
131
  }
128
132
 
129
- export function toolConfigDirCandidates(tool, project, branch) {
133
+ export function toolConfigDirCandidates(tool: SandboxTool, project: string, branch: string): string[] {
130
134
  return safeNameCandidates(branch).map((name) => hostJoin(tool.sandboxBase, project, name));
131
135
  }
132
136
 
133
- export function toolProjectDirCandidates(tool, project) {
137
+ export function toolProjectDirCandidates(tool: SandboxTool, project: string): string[] {
134
138
  return [hostJoin(tool.sandboxBase, project)];
135
139
  }
136
140
 
137
- export function toolNpmPackagesArg(tools) {
141
+ export function toolNpmPackagesArg(tools: SandboxTool[]): string {
138
142
  return tools.map((tool) => tool.npmPackage).join(' ');
139
143
  }
@@ -1,18 +1,41 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { info, ok, err } from './log.js';
4
- import { resolveTemplateDir } from './paths.js';
5
- import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.js';
3
+ import { info, ok, err } from './log.ts';
4
+ import { resolveTemplateDir } from './paths.ts';
5
+ import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.ts';
6
+
7
+ type FileRegistry = {
8
+ managed: string[];
9
+ merged: string[];
10
+ ejected: string[];
11
+ };
12
+
13
+ type UpdateConfig = {
14
+ project: string;
15
+ org: string;
16
+ language: string;
17
+ platform?: { type?: string };
18
+ sandbox?: Record<string, unknown>;
19
+ labels?: Record<string, unknown>;
20
+ files?: Partial<FileRegistry>;
21
+ };
22
+
23
+ type Defaults = {
24
+ platform: { type: string };
25
+ sandbox: Record<string, unknown>;
26
+ labels: Record<string, unknown>;
27
+ files: FileRegistry;
28
+ };
6
29
 
7
30
  const defaults = JSON.parse(
8
31
  fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8')
9
- );
32
+ ) as Defaults;
10
33
 
11
34
  const CONFIG_DIR = '.agents';
12
35
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
13
36
 
14
- function isPathOwnedByOtherPlatform(relativePath, platformType) {
15
- const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0];
37
+ function isPathOwnedByOtherPlatform(relativePath: string, platformType: string): boolean {
38
+ const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
16
39
  if (!top.startsWith('.')) return false;
17
40
 
18
41
  const candidate = top.slice(1);
@@ -20,7 +43,7 @@ function isPathOwnedByOtherPlatform(relativePath, platformType) {
20
43
  return candidate !== platformType;
21
44
  }
22
45
 
23
- function syncFileRegistry(config, platformType) {
46
+ function syncFileRegistry(config: UpdateConfig, platformType: string) {
24
47
  config.files ||= {};
25
48
  const before = JSON.stringify({
26
49
  files: {
@@ -38,7 +61,7 @@ function syncFileRegistry(config, platformType) {
38
61
  ...config.files.merged,
39
62
  ...config.files.ejected
40
63
  ];
41
- const added = { managed: [], merged: [] };
64
+ const added: Pick<FileRegistry, 'managed' | 'merged'> = { managed: [], merged: [] };
42
65
 
43
66
  for (const entry of defaults.files.managed) {
44
67
  if (isPathOwnedByOtherPlatform(entry, platformType)) continue;
@@ -66,7 +89,7 @@ function syncFileRegistry(config, platformType) {
66
89
  return { added, changed: before !== after };
67
90
  }
68
91
 
69
- async function cmdUpdate() {
92
+ async function cmdUpdate(): Promise<void> {
70
93
  console.log('');
71
94
  console.log(' agent-infra update');
72
95
  console.log(' ==================================');
@@ -90,7 +113,7 @@ async function cmdUpdate() {
90
113
  }
91
114
 
92
115
  // read project config
93
- const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
116
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as UpdateConfig;
94
117
  const { project, org, language } = config;
95
118
  const platformType = config.platform?.type || defaults.platform.type;
96
119
  const replacements = { project, org };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
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",
@@ -18,16 +18,18 @@
18
18
  "registry": "https://registry.npmjs.org/"
19
19
  },
20
20
  "bin": {
21
- "agent-infra": "./bin/cli.js",
22
- "ai": "./bin/cli.js"
21
+ "agent-infra": "./dist/bin/cli.js",
22
+ "ai": "./dist/bin/cli.js"
23
23
  },
24
24
  "files": [
25
- "bin/cli.js",
25
+ "dist/",
26
+ "!dist/**/*.map",
27
+ "bin/cli.ts",
26
28
  "lib/",
27
29
  "templates/"
28
30
  ],
29
31
  "engines": {
30
- "node": ">=18"
32
+ "node": ">=22"
31
33
  },
32
34
  "keywords": [
33
35
  "ai",
@@ -41,16 +43,24 @@
41
43
  "installer"
42
44
  ],
43
45
  "dependencies": {
44
- "@clack/prompts": "1.2.0",
45
- "picocolors": "1.1.1"
46
+ "@clack/prompts": "1.4.0",
47
+ "cross-spawn": "^7.0.6",
48
+ "picocolors": "1.1.1",
49
+ "smol-toml": "^1.6.1"
46
50
  },
47
51
  "scripts": {
48
- "build": "node scripts/build-inline.js",
52
+ "build": "tsc -p tsconfig.json && node scripts/build.js && node scripts/build-inline.js",
49
53
  "demo:regen": "sh scripts/demo-regen.sh",
50
54
  "prepare": "git config core.hooksPath .git-hooks || true",
51
- "test:smoke": "node scripts/build-inline.js --check && node --test tests/templates/*.test.js tests/core/airc.test.js tests/core/release.test.js tests/core/metadata-sync-workflow.test.js tests/core/pr-label-workflow.test.js tests/core/status-label-workflow.test.js tests/core/test-tier-coverage.test.js tests/cli/lib.test.js tests/cli/sync-templates.test.js tests/scripts/sync-templates-platform-gating.test.js",
52
- "test:core": "node scripts/build-inline.js --check && node --test tests/templates/*.test.js tests/core/airc.test.js tests/core/release.test.js tests/core/metadata-sync-workflow.test.js tests/core/pr-label-workflow.test.js tests/core/status-label-workflow.test.js tests/core/test-tier-coverage.test.js tests/cli/lib.test.js tests/cli/sync-templates.test.js tests/scripts/sync-templates-platform-gating.test.js tests/cli/cli.test.js tests/cli/merge.test.js tests/cli/sandbox.test.js tests/core/custom-skills.test.js tests/core/custom-tuis.test.js tests/core/demo-regen.test.js tests/scripts/find-existing-task.test.js tests/scripts/platform-adapter-defaults.test.js",
53
- "test": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js tests/scripts/*.test.js",
54
- "prepublishOnly": "node scripts/build-inline.js --check && node --test tests/cli/*.test.js tests/templates/*.test.js tests/core/*.test.js tests/scripts/*.test.js"
55
+ "typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
56
+ "test:smoke": "npm run build && node --experimental-strip-types --no-warnings --test tests/templates/*.test.ts tests/core/airc.test.ts tests/core/release.test.ts tests/core/metadata-sync-workflow.test.ts tests/core/pr-label-workflow.test.ts tests/core/status-label-workflow.test.ts tests/core/test-tier-coverage.test.ts tests/cli/lib.test.ts tests/cli/sync-templates.test.ts tests/scripts/sync-templates-platform-gating.test.ts",
57
+ "test:core": "npm run build && node --experimental-strip-types --no-warnings --test tests/templates/*.test.ts tests/core/airc.test.ts tests/core/release.test.ts tests/core/metadata-sync-workflow.test.ts tests/core/pr-label-workflow.test.ts tests/core/status-label-workflow.test.ts tests/core/test-tier-coverage.test.ts tests/cli/lib.test.ts tests/cli/sync-templates.test.ts tests/scripts/sync-templates-platform-gating.test.ts tests/cli/cli.test.ts tests/cli/merge.test.ts tests/cli/sandbox.test.ts tests/core/custom-skills.test.ts tests/core/custom-tuis.test.ts tests/core/demo-regen.test.ts tests/scripts/find-existing-task.test.ts tests/scripts/platform-adapter-defaults.test.ts",
58
+ "test": "npm run build && node --experimental-strip-types --no-warnings --test tests/cli/*.test.ts tests/templates/*.test.ts tests/core/*.test.ts tests/scripts/*.test.ts",
59
+ "prepublishOnly": "npm run build && node --experimental-strip-types --no-warnings --test tests/cli/*.test.ts tests/templates/*.test.ts tests/core/*.test.ts tests/scripts/*.test.ts"
60
+ },
61
+ "devDependencies": {
62
+ "@types/cross-spawn": "^6.0.6",
63
+ "@types/node": "^22.19.19",
64
+ "typescript": "~5.9"
55
65
  }
56
66
  }
@@ -155,10 +155,8 @@ Update task.md:
155
155
 
156
156
  - Write `issue_number: {n}` into the frontmatter (replace if it exists; append at the end of the frontmatter otherwise)
157
157
  - Update `updated_at` to the current time (command: `date "+%Y-%m-%d %H:%M:%S%:z"`)
158
- - Append to the `## 活动日志` / `## Activity Log` section:
159
- ```
160
- - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Create Issue** by {agent} — Created GitHub Issue #{n}
161
- ```
158
+
159
+ > Do NOT append an Activity Log entry here. The Issue creation event is already captured by the GitHub Issue itself and by the frontmatter `issue_number` field; the Activity Log only records the single `create-task` skill execution anchor (`Task Created`), written by the caller SKILL step 3.
162
160
 
163
161
  ### 8. Return the Result
164
162
 
@@ -155,10 +155,8 @@ gh api "repos/$upstream_repo/issues/{issue-number}" -X PATCH \
155
155
 
156
156
  - 把 `issue_number: {n}` 写入 frontmatter(已存在则替换;不存在则在 frontmatter 末尾追加)
157
157
  - 更新 `updated_at` 为当前时间(命令:`date "+%Y-%m-%d %H:%M:%S%:z"`)
158
- - 在 `## 活动日志` / `## Activity Log` 段落追加:
159
- ```
160
- - {YYYY-MM-DD HH:mm:ss±HH:MM} — **Create Issue** by {agent} — Created GitHub Issue #{n}
161
- ```
158
+
159
+ > 不要在此追加 Activity Log 条目。Issue 创建事件已由 GitHub Issue 自身和 frontmatter `issue_number` 承载;Activity Log 仅记录 `create-task` skill 一次执行的整体锚点(`Task Created`),由调用方 SKILL 步骤 3 写入。
162
160
 
163
161
  ### 8. 返回结果
164
162
 
@@ -120,6 +120,35 @@ Read Issue comments or search for existing hidden markers:
120
120
  gh api "repos/$upstream_repo/issues/{issue-number}/comments" --paginate
121
121
  ```
122
122
 
123
+ ## Historical Task Comment Scan
124
+
125
+ `find-existing-task.js` only consumes stdin and does not call `gh` directly. The AI selects the pipeline command for the host OS.
126
+
127
+ POSIX (bash / zsh):
128
+
129
+ ```bash
130
+ set -o pipefail
131
+ gh api "repos/$upstream_repo/issues/{issue-number}/comments" \
132
+ --paginate --jq '.[] | @json' \
133
+ | node .agents/scripts/find-existing-task.js
134
+ ```
135
+
136
+ Windows (PowerShell 7+ / pwsh):
137
+
138
+ ```powershell
139
+ $ErrorActionPreference = 'Stop'
140
+ gh api "repos/$upstream_repo/issues/{issue-number}/comments" `
141
+ --paginate --jq '.[] | @json' |
142
+ node .agents/scripts/find-existing-task.js
143
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
144
+ ```
145
+
146
+ On PowerShell 5.1, explicitly enable UTF-8 stdio first; otherwise the pipe may corrupt multibyte characters:
147
+
148
+ ```powershell
149
+ [Console]::OutputEncoding = $OutputEncoding = [System.Text.UTF8Encoding]::new()
150
+ ```
151
+
123
152
  ## PR Template and Metadata Helpers
124
153
 
125
154
  Read a repository PR template when present:
@@ -120,6 +120,35 @@ gh issue close {issue-number} -R "$upstream_repo" --reason "{reason}"
120
120
  gh api "repos/$upstream_repo/issues/{issue-number}/comments" --paginate
121
121
  ```
122
122
 
123
+ ## 历史任务评论扫描
124
+
125
+ `find-existing-task.js` 仅消费 stdin,不直接调用 `gh`。由 AI 按宿主 OS 选择下面的 pipeline 命令。
126
+
127
+ POSIX(bash / zsh):
128
+
129
+ ```bash
130
+ set -o pipefail
131
+ gh api "repos/$upstream_repo/issues/{issue-number}/comments" \
132
+ --paginate --jq '.[] | @json' \
133
+ | node .agents/scripts/find-existing-task.js
134
+ ```
135
+
136
+ Windows(PowerShell 7+ / pwsh):
137
+
138
+ ```powershell
139
+ $ErrorActionPreference = 'Stop'
140
+ gh api "repos/$upstream_repo/issues/{issue-number}/comments" `
141
+ --paginate --jq '.[] | @json' |
142
+ node .agents/scripts/find-existing-task.js
143
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
144
+ ```
145
+
146
+ 在 PowerShell 5.1 上需先显式启用 UTF-8 stdio,否则 pipe 可能损坏多字节字符:
147
+
148
+ ```powershell
149
+ [Console]::OutputEncoding = $OutputEncoding = [System.Text.UTF8Encoding]::new()
150
+ ```
151
+
123
152
  ## PR 模板与元数据辅助命令
124
153
 
125
154
  存在仓库 PR 模板时读取: