@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
@@ -1,24 +1,78 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { info, ok } from './log.js';
3
+ import { info, ok } from './log.ts';
4
4
 
5
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
6
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
7
7
  const TITLE_RE = /^# (.+)$/m;
8
8
  const DATE_FROM_PATH_RE = /(?:^|[/\\])(\d{4})[/\\](\d{2})[/\\](\d{2})(?:[/\\]|$)/;
9
- const MUTABLE_SECTIONS = ['active', 'blocked', 'completed'];
10
- const ALL_SECTIONS = [...MUTABLE_SECTIONS, 'archive'];
9
+ const MUTABLE_SECTIONS = ['active', 'blocked', 'completed'] as const;
10
+ const ALL_SECTIONS = [...MUTABLE_SECTIONS, 'archive'] as const;
11
+ type MutableSection = typeof MUTABLE_SECTIONS[number];
12
+ type Section = typeof ALL_SECTIONS[number];
13
+ type MutableAction = 'copied' | 'updated' | 'moved' | 'skipped';
14
+ type ArchiveAction = 'copied' | 'skipped';
15
+ type SourceMode = 'workspace' | 'legacy-archive';
16
+ type DateParts = {
17
+ year: string;
18
+ month: string;
19
+ day: string;
20
+ };
21
+ type TimestampRecord = {
22
+ value: string;
23
+ source: string;
24
+ };
25
+ type TaskRecord = DateParts & {
26
+ taskId: string;
27
+ taskDir: string;
28
+ relativePath: string;
29
+ title: string;
30
+ type: string;
31
+ completedAt: string;
32
+ };
33
+ type ArchiveManifestEntry = Omit<TaskRecord, 'taskDir'>;
34
+ type WorkspaceRecord = {
35
+ taskId: string;
36
+ section: MutableSection;
37
+ taskDir: string;
38
+ timestamp: TimestampRecord;
39
+ };
40
+ type ReportEntry = {
41
+ action: MutableAction | ArchiveAction;
42
+ symbol: string;
43
+ taskId: string;
44
+ section?: Section;
45
+ detail: string;
46
+ fromSection?: MutableSection;
47
+ toSection?: MutableSection;
48
+ relativePath?: string;
49
+ };
50
+ type MutableCounts = Record<MutableAction, ReportEntry[]>;
51
+ type ArchiveCounts = Record<ArchiveAction, ReportEntry[]>;
52
+ type MergeReport = {
53
+ sourcePath: string;
54
+ backupRoot: string;
55
+ sections: Record<MutableSection, MutableCounts> & { archive: ArchiveCounts };
56
+ details: ReportEntry[];
57
+ backupCount: number;
58
+ };
59
+ type MergeMutableArgs = {
60
+ sourceWorkspace: string;
61
+ localWorkspace: string;
62
+ backupRoot: string;
63
+ report: MergeReport;
64
+ };
11
65
  const SECTION_LABELS = {
12
66
  active: 'Active',
13
67
  blocked: 'Blocked',
14
68
  completed: 'Completed',
15
69
  archive: 'Archive'
16
- };
70
+ } satisfies Record<Section, string>;
17
71
  const DIVIDER = '═'.repeat(55);
18
72
 
19
- function extractField(content, fieldName) {
73
+ function extractField(content: string, fieldName: string): string | null {
20
74
  const match = content.match(FRONTMATTER_RE);
21
- if (!match) {
75
+ if (!match || !match[1]) {
22
76
  return null;
23
77
  }
24
78
 
@@ -37,14 +91,14 @@ function extractField(content, fieldName) {
37
91
  return null;
38
92
  }
39
93
 
40
- function extractTitle(content) {
94
+ function extractTitle(content: string): string | null {
41
95
  const withoutFrontmatter = content.replace(FRONTMATTER_RE, '');
42
96
  const match = withoutFrontmatter.match(TITLE_RE);
43
97
  if (!match) {
44
98
  return null;
45
99
  }
46
100
 
47
- return match[1]
101
+ return (match[1] ?? '')
48
102
  .trim()
49
103
  .replace(/^任务:/, '')
50
104
  .replace(/^Task:\s*/, '')
@@ -52,7 +106,7 @@ function extractTitle(content) {
52
106
  .replace(/\|/g, '\\|') || null;
53
107
  }
54
108
 
55
- function normalizeTaskRecord(taskDir, taskFile, dateParts) {
109
+ function normalizeTaskRecord(taskDir: string, taskFile: string, dateParts: DateParts): TaskRecord {
56
110
  const taskId = path.basename(taskDir);
57
111
  const content = fs.readFileSync(taskFile, 'utf8');
58
112
  const completedAt = extractField(content, 'completed_at');
@@ -74,13 +128,17 @@ function normalizeTaskRecord(taskDir, taskFile, dateParts) {
74
128
  };
75
129
  }
76
130
 
77
- function fallbackDateParts(taskDir, content) {
131
+ function fallbackDateParts(taskDir: string, content: string): DateParts | null {
78
132
  const pathMatch = taskDir.match(DATE_FROM_PATH_RE);
79
133
  if (pathMatch) {
134
+ const [, year, month, day] = pathMatch;
135
+ if (!year || !month || !day) {
136
+ return null;
137
+ }
80
138
  return {
81
- year: pathMatch[1],
82
- month: pathMatch[2],
83
- day: pathMatch[3]
139
+ year,
140
+ month,
141
+ day
84
142
  };
85
143
  }
86
144
 
@@ -90,18 +148,22 @@ function fallbackDateParts(taskDir, content) {
90
148
  const dateMatch = source?.match(/^(\d{4})-(\d{2})-(\d{2})/);
91
149
 
92
150
  if (dateMatch) {
151
+ const [, year, month, day] = dateMatch;
152
+ if (!year || !month || !day) {
153
+ return null;
154
+ }
93
155
  return {
94
- year: dateMatch[1],
95
- month: dateMatch[2],
96
- day: dateMatch[3]
156
+ year,
157
+ month,
158
+ day
97
159
  };
98
160
  }
99
161
 
100
162
  return null;
101
163
  }
102
164
 
103
- function scanSourceTasks(sourceDir) {
104
- const tasks = [];
165
+ function scanSourceTasks(sourceDir: string): TaskRecord[] {
166
+ const tasks: TaskRecord[] = [];
105
167
  const years = fs.existsSync(sourceDir) ? fs.readdirSync(sourceDir, { withFileTypes: true }) : [];
106
168
 
107
169
  for (const yearEntry of years) {
@@ -184,7 +246,7 @@ function scanSourceTasks(sourceDir) {
184
246
  return tasks.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
185
247
  }
186
248
 
187
- function findTaskDirById(rootDir, taskId) {
249
+ function findTaskDirById(rootDir: string, taskId: string): string | null {
188
250
  if (!fs.existsSync(rootDir)) {
189
251
  return null;
190
252
  }
@@ -217,11 +279,11 @@ function findTaskDirById(rootDir, taskId) {
217
279
  return null;
218
280
  }
219
281
 
220
- function taskExistsInArchive(archiveDir, taskId) {
282
+ function taskExistsInArchive(archiveDir: string, taskId: string): string | null {
221
283
  return findTaskDirById(archiveDir, taskId);
222
284
  }
223
285
 
224
- function formatManifestHeader(generatedAt) {
286
+ function formatManifestHeader(generatedAt: string): string[] {
225
287
  return [
226
288
  '# Archive Manifest',
227
289
  '',
@@ -231,8 +293,8 @@ function formatManifestHeader(generatedAt) {
231
293
  ];
232
294
  }
233
295
 
234
- function collectArchiveEntries(archiveDir) {
235
- const entries = [];
296
+ function collectArchiveEntries(archiveDir: string): ArchiveManifestEntry[] {
297
+ const entries: ArchiveManifestEntry[] = [];
236
298
  if (!fs.existsSync(archiveDir)) {
237
299
  return entries;
238
300
  }
@@ -277,6 +339,7 @@ function collectArchiveEntries(archiveDir) {
277
339
  entries.push({
278
340
  year: yearEntry.name,
279
341
  month: monthEntry.name,
342
+ day: dayEntry.name,
280
343
  completedAt,
281
344
  taskId: taskEntry.name,
282
345
  title,
@@ -291,29 +354,31 @@ function collectArchiveEntries(archiveDir) {
291
354
  return entries;
292
355
  }
293
356
 
294
- function rebuildManifests(archiveDir) {
357
+ function rebuildManifests(archiveDir: string): void {
295
358
  fs.mkdirSync(archiveDir, { recursive: true });
296
359
  const entries = collectArchiveEntries(archiveDir);
297
360
  const generatedAt = new Date().toISOString().slice(0, 19).replace('T', ' ');
298
361
 
299
362
  removeManifestFiles(archiveDir);
300
363
 
301
- const monthGroups = new Map();
302
- const yearCounts = new Map();
303
- const monthCounts = new Map();
364
+ const monthGroups = new Map<string, ArchiveManifestEntry[]>();
365
+ const yearCounts = new Map<string, number>();
366
+ const monthCounts = new Map<string, number>();
304
367
 
305
368
  for (const entry of entries) {
306
369
  const monthKey = `${entry.year}\t${entry.month}`;
307
- if (!monthGroups.has(monthKey)) {
308
- monthGroups.set(monthKey, []);
370
+ let monthEntries = monthGroups.get(monthKey);
371
+ if (!monthEntries) {
372
+ monthEntries = [];
373
+ monthGroups.set(monthKey, monthEntries);
309
374
  }
310
- monthGroups.get(monthKey).push(entry);
375
+ monthEntries.push(entry);
311
376
  yearCounts.set(entry.year, (yearCounts.get(entry.year) || 0) + 1);
312
377
  monthCounts.set(monthKey, (monthCounts.get(monthKey) || 0) + 1);
313
378
  }
314
379
 
315
380
  for (const [monthKey, monthEntries] of [...monthGroups.entries()].sort()) {
316
- const [year, month] = monthKey.split('\t');
381
+ const [year = '', month = ''] = monthKey.split('\t');
317
382
  const monthManifestPath = path.join(archiveDir, year, month, 'manifest.md');
318
383
  fs.mkdirSync(path.dirname(monthManifestPath), { recursive: true });
319
384
 
@@ -356,7 +421,7 @@ function rebuildManifests(archiveDir) {
356
421
 
357
422
  for (const month of [...monthGroups.keys()]
358
423
  .filter((key) => key.startsWith(`${year}\t`))
359
- .map((key) => key.split('\t')[1])
424
+ .map((key) => key.split('\t')[1] ?? '')
360
425
  .sort()
361
426
  .reverse()) {
362
427
  lines.push(
@@ -382,7 +447,7 @@ function rebuildManifests(archiveDir) {
382
447
  fs.writeFileSync(path.join(archiveDir, 'manifest.md'), `${rootLines.join('\n')}\n`, 'utf8');
383
448
  }
384
449
 
385
- function removeManifestFiles(rootDir) {
450
+ function removeManifestFiles(rootDir: string): void {
386
451
  if (!fs.existsSync(rootDir)) {
387
452
  return;
388
453
  }
@@ -403,8 +468,8 @@ function removeManifestFiles(rootDir) {
403
468
  }
404
469
  }
405
470
 
406
- function formatTimestamp(date) {
407
- const pad = (value) => String(value).padStart(2, '0');
471
+ function formatTimestamp(date: Date): string {
472
+ const pad = (value: number): string => String(value).padStart(2, '0');
408
473
  const offsetMinutes = -date.getTimezoneOffset();
409
474
  const sign = offsetMinutes >= 0 ? '+' : '-';
410
475
  const absoluteOffsetMinutes = Math.abs(offsetMinutes);
@@ -422,7 +487,7 @@ function formatTimestamp(date) {
422
487
  ].join(':') + `${sign}${pad(offsetHours)}:${pad(offsetRemainderMinutes)}`;
423
488
  }
424
489
 
425
- function formatBackupTimestamp(date) {
490
+ function formatBackupTimestamp(date: Date): string {
426
491
  return [
427
492
  date.getFullYear(),
428
493
  String(date.getMonth() + 1).padStart(2, '0'),
@@ -430,12 +495,12 @@ function formatBackupTimestamp(date) {
430
495
  ].join('') + `-${String(date.getHours()).padStart(2, '0')}${String(date.getMinutes()).padStart(2, '0')}${String(date.getSeconds()).padStart(2, '0')}`;
431
496
  }
432
497
 
433
- function toPosixPath(relativePath) {
498
+ function toPosixPath(relativePath: string): string {
434
499
  return relativePath.split(path.sep).join('/');
435
500
  }
436
501
 
437
- function getLatestFileMtime(taskDir) {
438
- let latestMs = null;
502
+ function getLatestFileMtime(taskDir: string): number | null {
503
+ let latestMs: number | null = null;
439
504
  const stack = [taskDir];
440
505
 
441
506
  while (stack.length > 0) {
@@ -459,7 +524,7 @@ function getLatestFileMtime(taskDir) {
459
524
  return latestMs;
460
525
  }
461
526
 
462
- function getTaskTimestamp(taskDir) {
527
+ function getTaskTimestamp(taskDir: string): TimestampRecord {
463
528
  const taskFile = path.join(taskDir, 'task.md');
464
529
 
465
530
  if (fs.existsSync(taskFile)) {
@@ -491,8 +556,8 @@ function getTaskTimestamp(taskDir) {
491
556
  };
492
557
  }
493
558
 
494
- function compareTimestamps(left, right) {
495
- const normalizeTimestamp = (timestamp) => (timestamp.includes('T') ? timestamp : timestamp.replace(' ', 'T'));
559
+ function compareTimestamps(left: TimestampRecord, right: TimestampRecord): number {
560
+ const normalizeTimestamp = (timestamp: string): string => (timestamp.includes('T') ? timestamp : timestamp.replace(' ', 'T'));
496
561
  const leftMs = Date.parse(normalizeTimestamp(left.value));
497
562
  const rightMs = Date.parse(normalizeTimestamp(right.value));
498
563
 
@@ -503,13 +568,13 @@ function compareTimestamps(left, right) {
503
568
  return leftMs - rightMs;
504
569
  }
505
570
 
506
- function scanWorkspaceSection(rootDir, sectionName) {
571
+ function scanWorkspaceSection(rootDir: string, sectionName: MutableSection): WorkspaceRecord[] {
507
572
  const sectionDir = path.join(rootDir, sectionName);
508
573
  if (!fs.existsSync(sectionDir) || !fs.statSync(sectionDir).isDirectory()) {
509
574
  return [];
510
575
  }
511
576
 
512
- const records = [];
577
+ const records: WorkspaceRecord[] = [];
513
578
  for (const entry of fs.readdirSync(sectionDir, { withFileTypes: true })) {
514
579
  if (!entry.isDirectory() || !TASK_ID_RE.test(entry.name)) {
515
580
  continue;
@@ -532,8 +597,8 @@ function scanWorkspaceSection(rootDir, sectionName) {
532
597
  return records.sort((left, right) => left.taskId.localeCompare(right.taskId));
533
598
  }
534
599
 
535
- function buildWorkspaceIndex(workspaceDir) {
536
- const index = new Map();
600
+ function buildWorkspaceIndex(workspaceDir: string): Map<string, WorkspaceRecord> {
601
+ const index = new Map<string, WorkspaceRecord>();
537
602
 
538
603
  for (const section of MUTABLE_SECTIONS) {
539
604
  for (const record of scanWorkspaceSection(workspaceDir, section)) {
@@ -544,21 +609,21 @@ function buildWorkspaceIndex(workspaceDir) {
544
609
  return index;
545
610
  }
546
611
 
547
- function backupTaskDir(backupRoot, section, taskDir, taskId) {
612
+ function backupTaskDir(backupRoot: string, section: MutableSection, taskDir: string, taskId: string): string {
548
613
  const backupDir = path.join(backupRoot, section, taskId);
549
614
  fs.mkdirSync(path.dirname(backupDir), { recursive: true });
550
615
  fs.cpSync(taskDir, backupDir, { recursive: true });
551
616
  return backupDir;
552
617
  }
553
618
 
554
- function copyTaskToSection(sourceTask, workspaceDir) {
619
+ function copyTaskToSection(sourceTask: WorkspaceRecord, workspaceDir: string): string {
555
620
  const destinationDir = path.join(workspaceDir, sourceTask.section, sourceTask.taskId);
556
621
  fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
557
622
  fs.cpSync(sourceTask.taskDir, destinationDir, { recursive: true });
558
623
  return destinationDir;
559
624
  }
560
625
 
561
- function detectSourceMode(sourcePath) {
626
+ function detectSourceMode(sourcePath: string): SourceMode {
562
627
  for (const section of ALL_SECTIONS) {
563
628
  const sectionDir = path.join(sourcePath, section);
564
629
  if (fs.existsSync(sectionDir) && fs.statSync(sectionDir).isDirectory()) {
@@ -569,7 +634,7 @@ function detectSourceMode(sourcePath) {
569
634
  return 'legacy-archive';
570
635
  }
571
636
 
572
- function createReport(sourcePath, backupRoot) {
637
+ function createReport(sourcePath: string, backupRoot: string): MergeReport {
573
638
  return {
574
639
  sourcePath,
575
640
  backupRoot,
@@ -584,17 +649,17 @@ function createReport(sourcePath, backupRoot) {
584
649
  };
585
650
  }
586
651
 
587
- function recordMutable(report, reportSection, action, entry) {
652
+ function recordMutable(report: MergeReport, reportSection: MutableSection, action: MutableAction, entry: ReportEntry): void {
588
653
  report.sections[reportSection][action].push(entry);
589
654
  report.details.push(entry);
590
655
  }
591
656
 
592
- function recordArchive(report, action, entry) {
657
+ function recordArchive(report: MergeReport, action: ArchiveAction, entry: ReportEntry): void {
593
658
  report.sections.archive[action].push(entry);
594
659
  report.details.push(entry);
595
660
  }
596
661
 
597
- function mergeMutableSections({ sourceWorkspace, localWorkspace, backupRoot, report }) {
662
+ function mergeMutableSections({ sourceWorkspace, localWorkspace, backupRoot, report }: MergeMutableArgs): void {
598
663
  const localIndex = buildWorkspaceIndex(localWorkspace);
599
664
 
600
665
  for (const sourceSection of MUTABLE_SECTIONS) {
@@ -679,7 +744,7 @@ function mergeMutableSections({ sourceWorkspace, localWorkspace, backupRoot, rep
679
744
  }
680
745
  }
681
746
 
682
- function mergeArchiveSection(sourceArchive, localArchive, report) {
747
+ function mergeArchiveSection(sourceArchive: string, localArchive: string, report: MergeReport): number {
683
748
  const sourceTasks = scanSourceTasks(sourceArchive);
684
749
 
685
750
  for (const task of sourceTasks) {
@@ -712,7 +777,7 @@ function mergeArchiveSection(sourceArchive, localArchive, report) {
712
777
  return sourceTasks.length;
713
778
  }
714
779
 
715
- function printLegacyArchiveMessages(report, sourcePath) {
780
+ function printLegacyArchiveMessages(report: MergeReport, sourcePath: string): void {
716
781
  const merged = report.sections.archive.copied;
717
782
  const skipped = report.sections.archive.skipped;
718
783
 
@@ -735,16 +800,17 @@ function printLegacyArchiveMessages(report, sourcePath) {
735
800
  process.stdout.write('\n');
736
801
  }
737
802
 
738
- function printSection(lines, name, counts) {
803
+ function printSection(lines: string[], name: MutableSection, counts: MutableCounts): void {
739
804
  const title = `${SECTION_LABELS[name].padEnd(9, ' ')} (.agents/workspace/${name}/):`;
740
805
  lines.push(title);
741
806
 
742
- const entries = [
807
+ const allEntries: Array<[MutableAction, string]> = [
743
808
  ['copied', '✓ Copied '],
744
809
  ['updated', '↑ Updated '],
745
810
  ['moved', '⇄ Moved '],
746
811
  ['skipped', '⊘ Skipped ']
747
- ].filter(([key]) => Array.isArray(counts[key]));
812
+ ];
813
+ const entries = allEntries.filter(([key]) => Array.isArray(counts[key]));
748
814
 
749
815
  const nonZeroEntries = entries.filter(([key]) => counts[key].length > 0);
750
816
  if (nonZeroEntries.length === 0) {
@@ -759,7 +825,7 @@ function printSection(lines, name, counts) {
759
825
  lines.push('');
760
826
  }
761
827
 
762
- function printArchiveSection(lines, counts) {
828
+ function printArchiveSection(lines: string[], counts: ArchiveCounts): void {
763
829
  const title = `${SECTION_LABELS.archive.padEnd(9, ' ')} (.agents/workspace/archive/):`;
764
830
  lines.push(title);
765
831
 
@@ -777,16 +843,16 @@ function printArchiveSection(lines, counts) {
777
843
  lines.push('');
778
844
  }
779
845
 
780
- function renderDetail(entry) {
846
+ function renderDetail(entry: ReportEntry): string {
781
847
  if (entry.action === 'moved') {
782
848
  return ` ${entry.symbol} ${entry.taskId} ${entry.fromSection}→${entry.toSection} ${entry.detail}`;
783
849
  }
784
850
 
785
- const label = entry.section.padEnd(9, ' ');
851
+ const label = (entry.section ?? '').padEnd(9, ' ');
786
852
  return ` ${entry.symbol} ${entry.taskId} ${label} ${entry.detail}`;
787
853
  }
788
854
 
789
- function printReport(report) {
855
+ function printReport(report: MergeReport): void {
790
856
  const mutableTotals = MUTABLE_SECTIONS.reduce((acc, section) => {
791
857
  acc.copied += report.sections[section].copied.length;
792
858
  acc.updated += report.sections[section].updated.length;
@@ -832,7 +898,7 @@ function printReport(report) {
832
898
  process.stdout.write(`${lines.join('\n')}\n`);
833
899
  }
834
900
 
835
- async function cmdMerge(args) {
901
+ async function cmdMerge(args: string[]): Promise<void> {
836
902
  const sourcePath = args[0];
837
903
  if (!sourcePath) {
838
904
  throw new Error('Usage: agent-infra merge <source-path>');
package/lib/paths.ts ADDED
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ function resolveTemplateDir() {
5
+ const candidates = [
6
+ // Source checkout: lib/paths.ts -> repo-root/templates.
7
+ new URL('../templates', import.meta.url),
8
+ // Installed package: dist/lib/paths.js -> package-root/templates.
9
+ new URL('../../templates', import.meta.url)
10
+ ];
11
+ for (const candidate of candidates) {
12
+ const bundledDir = fileURLToPath(candidate);
13
+ if (fs.existsSync(bundledDir)) return bundledDir;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ export { resolveTemplateDir };
@@ -1,9 +1,9 @@
1
1
  import readline from 'node:readline';
2
- import { ask } from './log.js';
2
+ import { ask } from './log.ts';
3
3
 
4
- let _rl = null;
5
- let _lines = [];
6
- let _lineResolve = null;
4
+ let _rl: readline.Interface | null = null;
5
+ let _lines: string[] = [];
6
+ let _lineResolve: ((value: string | null) => void) | null = null;
7
7
  let _stdinDone = false;
8
8
 
9
9
  function setupInterface() {
@@ -34,10 +34,10 @@ function setupInterface() {
34
34
  });
35
35
  }
36
36
 
37
- function nextLine() {
37
+ function nextLine(): Promise<string | null> {
38
38
  return new Promise((resolve) => {
39
39
  if (_lines.length > 0) {
40
- resolve(_lines.shift());
40
+ resolve(_lines.shift() ?? null);
41
41
  } else if (_stdinDone) {
42
42
  resolve(null);
43
43
  } else {
@@ -46,7 +46,7 @@ function nextLine() {
46
46
  });
47
47
  }
48
48
 
49
- async function prompt(question, defaultValue) {
49
+ async function prompt(question: string, defaultValue: string): Promise<string> {
50
50
  const label = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
51
51
  ask(label);
52
52
 
@@ -59,8 +59,8 @@ async function prompt(question, defaultValue) {
59
59
  return line.trim() || defaultValue || '';
60
60
  }
61
61
 
62
- async function select(question, choices, defaultValue) {
63
- const defaultIndex = choices.indexOf(defaultValue);
62
+ async function select(question: string, choices: string[], defaultValue?: string): Promise<string> {
63
+ const defaultIndex = defaultValue === undefined ? -1 : choices.indexOf(defaultValue);
64
64
 
65
65
  process.stdout.write(` ${question}:\n`);
66
66
  choices.forEach((choice, index) => {
@@ -74,19 +74,19 @@ async function select(question, choices, defaultValue) {
74
74
 
75
75
  const line = await nextLine();
76
76
  if (line === null || line.trim() === '') {
77
- return defaultValue || choices[0];
77
+ return defaultValue || choices[0] || '';
78
78
  }
79
79
 
80
80
  const trimmed = line.trim();
81
81
  const selectedIndex = Number.parseInt(trimmed, 10);
82
82
  if (String(selectedIndex) === trimmed && selectedIndex >= 1 && selectedIndex <= choices.length) {
83
- return choices[selectedIndex - 1];
83
+ return choices[selectedIndex - 1] ?? '';
84
84
  }
85
85
 
86
86
  return trimmed;
87
87
  }
88
88
 
89
- function closePrompt() {
89
+ function closePrompt(): void {
90
90
  if (_rl) {
91
91
  _rl.close();
92
92
  _rl = null;
@@ -5,7 +5,14 @@ import path from 'node:path';
5
5
  const KNOWN_PLATFORMS = new Set(['github']);
6
6
  const KNOWN_LANGUAGES = new Set(['en', 'zh-CN']);
7
7
 
8
- function renderFile(src, dst, replacements) {
8
+ type Replacements = {
9
+ project: string;
10
+ org?: string;
11
+ };
12
+
13
+ type FileSelection = Map<string, string>;
14
+
15
+ function renderFile(src: string, dst: string, replacements: Replacements): void {
9
16
  if (!fs.existsSync(src)) {
10
17
  throw new Error(`Template file not found: ${src}`);
11
18
  }
@@ -20,7 +27,7 @@ function renderFile(src, dst, replacements) {
20
27
  fs.writeFileSync(dst, content, 'utf8');
21
28
  }
22
29
 
23
- function copyFile(src, dst) {
30
+ function copyFile(src: string, dst: string): void {
24
31
  if (!fs.existsSync(src)) {
25
32
  throw new Error(`Template file not found: ${src}`);
26
33
  }
@@ -36,8 +43,8 @@ function copyFile(src, dst) {
36
43
  }
37
44
  }
38
45
 
39
- function walkFiles(dir) {
40
- const results = [];
46
+ function walkFiles(dir: string): string[] {
47
+ const results: string[] = [];
41
48
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
42
49
  const entryPath = path.join(dir, entry.name);
43
50
  if (entry.isDirectory()) {
@@ -49,31 +56,31 @@ function walkFiles(dir) {
49
56
  return results;
50
57
  }
51
58
 
52
- function containsPlaceholders(src) {
59
+ function containsPlaceholders(src: string): boolean {
53
60
  const content = fs.readFileSync(src, 'utf8');
54
61
  return content.includes('{{project}}') || content.includes('{{org}}');
55
62
  }
56
63
 
57
- function variantExt(relativePath) {
64
+ function variantExt(relativePath: string): string {
58
65
  return path.extname(relativePath);
59
66
  }
60
67
 
61
- function variantBase(relativePath) {
68
+ function variantBase(relativePath: string): string {
62
69
  const ext = variantExt(relativePath);
63
70
  return relativePath.slice(0, -ext.length);
64
71
  }
65
72
 
66
- function withVariant(relativePath, variant) {
73
+ function withVariant(relativePath: string, variant: string): string {
67
74
  const ext = variantExt(relativePath);
68
75
  const base = variantBase(relativePath);
69
76
  return `${base}.${variant}${ext}`;
70
77
  }
71
78
 
72
- function stripVariant(relativePath, variant) {
79
+ function stripVariant(relativePath: string, variant: string): string {
73
80
  return relativePath.replace(new RegExp(`\\.${variant.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.`), '.');
74
81
  }
75
82
 
76
- function isPlatformVariant(relativePath, platform) {
83
+ function isPlatformVariant(relativePath: string, platform: string): boolean {
77
84
  const platforms = new Set([...KNOWN_PLATFORMS, platform]);
78
85
  for (const candidate of platforms) {
79
86
  if (relativePath.includes(`.${candidate}.`)) {
@@ -83,7 +90,7 @@ function isPlatformVariant(relativePath, platform) {
83
90
  return false;
84
91
  }
85
92
 
86
- function isLangVariant(relativePath) {
93
+ function isLangVariant(relativePath: string): boolean {
87
94
  for (const lang of KNOWN_LANGUAGES) {
88
95
  if (relativePath.includes(`.${lang}.`)) {
89
96
  return true;
@@ -92,8 +99,8 @@ function isLangVariant(relativePath) {
92
99
  return false;
93
100
  }
94
101
 
95
- function langSelect(relativePaths, language) {
96
- const selected = new Map();
102
+ function langSelect(relativePaths: string[], language: string): FileSelection {
103
+ const selected: FileSelection = new Map();
97
104
 
98
105
  for (const relativePath of relativePaths) {
99
106
  if (relativePath.includes(`.${language}.`)) {
@@ -108,8 +115,8 @@ function langSelect(relativePaths, language) {
108
115
  return selected;
109
116
  }
110
117
 
111
- function platformSelect(entries, platform) {
112
- const selected = new Map();
118
+ function platformSelect(entries: FileSelection, platform: string): FileSelection {
119
+ const selected: FileSelection = new Map();
113
120
 
114
121
  for (const [relativePath, src] of entries) {
115
122
  if (!relativePath.includes(`.${platform}.`)) {
@@ -131,12 +138,18 @@ function platformSelect(entries, platform) {
131
138
  return selected;
132
139
  }
133
140
 
134
- function selectLocalizedFiles(srcDir, language, platform = 'github') {
141
+ function selectLocalizedFiles(srcDir: string, language: string, platform = 'github'): FileSelection {
135
142
  const relativePaths = walkFiles(srcDir).map((src) => path.relative(srcDir, src));
136
143
  return platformSelect(langSelect(relativePaths, language), platform);
137
144
  }
138
145
 
139
- function copySkillDir(srcDir, dstDir, replacements, language, platform = 'github') {
146
+ function copySkillDir(
147
+ srcDir: string,
148
+ dstDir: string,
149
+ replacements: Replacements,
150
+ language: string,
151
+ platform = 'github'
152
+ ): void {
140
153
  if (!fs.existsSync(srcDir)) {
141
154
  throw new Error(`Template directory not found: ${srcDir}`);
142
155
  }