@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.
- package/README.md +200 -8
- package/README.zh-CN.md +176 -8
- package/bin/{cli.js → cli.ts} +23 -19
- package/dist/bin/cli.js +116 -0
- package/dist/lib/defaults.json +61 -0
- package/dist/lib/init.js +238 -0
- package/dist/lib/log.js +18 -0
- package/dist/lib/merge.js +747 -0
- package/dist/lib/paths.js +18 -0
- package/dist/lib/prompt.js +85 -0
- package/dist/lib/render.js +139 -0
- package/dist/lib/sandbox/commands/create.js +1173 -0
- package/dist/lib/sandbox/commands/enter.js +98 -0
- package/dist/lib/sandbox/commands/ls.js +93 -0
- package/dist/lib/sandbox/commands/rebuild.js +101 -0
- package/dist/lib/sandbox/commands/refresh.js +85 -0
- package/dist/lib/sandbox/commands/rm.js +226 -0
- package/dist/lib/sandbox/commands/vm.js +144 -0
- package/dist/lib/sandbox/config.js +85 -0
- package/dist/lib/sandbox/constants.js +104 -0
- package/dist/lib/sandbox/credentials.js +437 -0
- package/dist/lib/sandbox/dockerfile.js +76 -0
- package/dist/lib/sandbox/dotfiles.js +170 -0
- package/dist/lib/sandbox/engine.js +155 -0
- package/dist/lib/sandbox/engines/colima.js +64 -0
- package/dist/lib/sandbox/engines/docker-desktop.js +27 -0
- package/dist/lib/sandbox/engines/index.js +25 -0
- package/dist/lib/sandbox/engines/native.js +96 -0
- package/dist/lib/sandbox/engines/orbstack.js +63 -0
- package/dist/lib/sandbox/engines/selinux.js +48 -0
- package/dist/lib/sandbox/engines/wsl2-paths.js +47 -0
- package/dist/lib/sandbox/engines/wsl2.js +57 -0
- package/dist/lib/sandbox/index.js +70 -0
- package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +39 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +178 -0
- package/dist/lib/sandbox/runtimes/java17.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/java21.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node20.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/node22.dockerfile +3 -0
- package/dist/lib/sandbox/runtimes/python3.dockerfile +3 -0
- package/dist/lib/sandbox/shell.js +148 -0
- package/dist/lib/sandbox/task-resolver.js +35 -0
- package/dist/lib/sandbox/tools.js +115 -0
- package/dist/lib/update.js +186 -0
- package/dist/lib/version.js +5 -0
- package/dist/package.json +5 -0
- package/lib/{init.js → init.ts} +64 -20
- package/lib/{log.js → log.ts} +4 -4
- package/lib/{merge.js → merge.ts} +129 -63
- package/lib/paths.ts +18 -0
- package/lib/{prompt.js → prompt.ts} +12 -12
- package/lib/{render.js → render.ts} +30 -17
- package/lib/sandbox/commands/create.ts +1507 -0
- package/lib/sandbox/commands/enter.ts +115 -0
- package/lib/sandbox/commands/{ls.js → ls.ts} +41 -10
- package/lib/sandbox/commands/rebuild.ts +135 -0
- package/lib/sandbox/commands/refresh.ts +128 -0
- package/lib/sandbox/commands/{rm.js → rm.ts} +71 -21
- package/lib/sandbox/commands/{vm.js → vm.ts} +62 -15
- package/lib/sandbox/config.ts +133 -0
- package/lib/sandbox/{constants.js → constants.ts} +41 -17
- package/lib/sandbox/credentials.ts +634 -0
- package/lib/sandbox/{dockerfile.js → dockerfile.ts} +13 -6
- package/lib/sandbox/dotfiles.ts +236 -0
- package/lib/sandbox/engine.ts +231 -0
- package/lib/sandbox/engines/colima.ts +81 -0
- package/lib/sandbox/engines/docker-desktop.ts +36 -0
- package/lib/sandbox/engines/index.ts +74 -0
- package/lib/sandbox/engines/native.ts +131 -0
- package/lib/sandbox/engines/orbstack.ts +78 -0
- package/lib/sandbox/engines/selinux.ts +66 -0
- package/lib/sandbox/engines/wsl2-paths.ts +65 -0
- package/lib/sandbox/engines/wsl2.ts +74 -0
- package/lib/sandbox/{index.js → index.ts} +17 -8
- package/lib/sandbox/runtimes/ai-tools.dockerfile +14 -1
- package/lib/sandbox/runtimes/base.dockerfile +116 -1
- package/lib/sandbox/shell.ts +186 -0
- package/lib/sandbox/{task-resolver.js → task-resolver.ts} +6 -6
- package/lib/sandbox/{tools.js → tools.ts} +33 -29
- package/lib/{update.js → update.ts} +33 -10
- package/package.json +22 -12
- package/templates/.agents/rules/create-issue.github.en.md +2 -4
- package/templates/.agents/rules/create-issue.github.zh-CN.md +2 -4
- package/templates/.agents/rules/issue-pr-commands.github.en.md +29 -0
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +29 -0
- package/templates/.agents/scripts/{platform-adapters/find-existing-task.github.js → find-existing-task.js} +22 -79
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +26 -41
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +6 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +6 -8
- package/lib/paths.js +0 -9
- package/lib/sandbox/commands/create.js +0 -1174
- package/lib/sandbox/commands/enter.js +0 -79
- package/lib/sandbox/commands/rebuild.js +0 -102
- package/lib/sandbox/config.js +0 -84
- package/lib/sandbox/engine.js +0 -256
- package/lib/sandbox/shell.js +0 -122
- package/templates/.agents/scripts/platform-adapters/find-existing-task.js +0 -5
- /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.
|
|
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
|
|
82
|
-
month
|
|
83
|
-
day
|
|
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
|
|
95
|
-
month
|
|
96
|
-
day
|
|
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
|
-
|
|
308
|
-
|
|
370
|
+
let monthEntries = monthGroups.get(monthKey);
|
|
371
|
+
if (!monthEntries) {
|
|
372
|
+
monthEntries = [];
|
|
373
|
+
monthGroups.set(monthKey, monthEntries);
|
|
309
374
|
}
|
|
310
|
-
|
|
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
|
|
807
|
+
const allEntries: Array<[MutableAction, string]> = [
|
|
743
808
|
['copied', '✓ Copied '],
|
|
744
809
|
['updated', '↑ Updated '],
|
|
745
810
|
['moved', '⇄ Moved '],
|
|
746
811
|
['skipped', '⊘ Skipped ']
|
|
747
|
-
]
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
}
|