@fitlab-ai/agent-infra 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/bin/cli.js +1 -1
- package/lib/merge.js +442 -22
- package/lib/sandbox/commands/create.js +190 -67
- package/lib/sandbox/commands/enter.js +36 -3
- package/lib/sandbox/commands/ls.js +3 -2
- package/lib/sandbox/commands/rm.js +2 -2
- package/lib/sandbox/config.js +1 -1
- package/lib/sandbox/runtimes/base.dockerfile +1 -1
- package/lib/sandbox/tools.js +9 -5
- package/package.json +1 -1
- package/templates/.agents/rules/pr-sync.md +110 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +110 -0
- package/templates/.agents/scripts/validate-artifact.js +77 -1
- package/templates/.agents/skills/commit/SKILL.md +9 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +9 -1
- package/templates/.agents/skills/commit/config/verify.json +5 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.md +21 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +21 -0
- package/templates/.agents/skills/commit/reference/task-status-update.md +2 -0
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.md +2 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.md +7 -74
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +6 -73
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -1
package/README.md
CHANGED
|
@@ -181,10 +181,12 @@ This detects the packaged template version and renders all managed files. The sa
|
|
|
181
181
|
|
|
182
182
|
### Sandbox aliases and GitHub CLI
|
|
183
183
|
|
|
184
|
-
`ai sandbox create` now bootstraps the host-side aliases file at `~/.
|
|
184
|
+
`ai sandbox create` now bootstraps the host-side aliases file at `~/.agent-infra/aliases/sandbox.sh` on first run. The generated file includes ready-to-edit yolo shortcuts for Claude, Codex, Gemini CLI, and OpenCode, and every sandbox syncs that file into `/home/devuser/.bash_aliases`.
|
|
185
185
|
|
|
186
186
|
The sandbox image also preinstalls `gh`. When `gh auth token` succeeds on the host, `ai sandbox create` injects the token into the container as `GH_TOKEN`, so `gh` commands work inside the sandbox without extra setup.
|
|
187
187
|
|
|
188
|
+
`ai sandbox exec` also forwards a small terminal-detection whitelist (`TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `LC_TERMINAL`, `LC_TERMINAL_VERSION`) into the container. This keeps interactive TUIs aligned with the host terminal for behaviors such as Claude Code's Shift+Enter newline support, without passing through the full host environment.
|
|
189
|
+
|
|
188
190
|
<a id="architecture-overview"></a>
|
|
189
191
|
|
|
190
192
|
## Architecture Overview
|
|
@@ -394,7 +396,7 @@ The generated `.agents/.airc.json` file is the central contract between the boot
|
|
|
394
396
|
"project": "my-project",
|
|
395
397
|
"org": "my-org",
|
|
396
398
|
"language": "en",
|
|
397
|
-
"templateVersion": "v0.5.
|
|
399
|
+
"templateVersion": "v0.5.1",
|
|
398
400
|
"files": {
|
|
399
401
|
"managed": [
|
|
400
402
|
".agents/workspace/README.md",
|
package/README.zh-CN.md
CHANGED
|
@@ -181,10 +181,12 @@ CLI 会收集项目元数据,向所有支持的 AI TUI 安装 `update-agent-in
|
|
|
181
181
|
|
|
182
182
|
### 沙箱 aliases 与 GitHub CLI
|
|
183
183
|
|
|
184
|
-
`ai sandbox create` 在首次运行时会自动生成宿主机侧的 `~/.
|
|
184
|
+
`ai sandbox create` 在首次运行时会自动生成宿主机侧的 `~/.agent-infra/aliases/sandbox.sh`。该文件内置了 Claude、Codex、Gemini CLI 和 OpenCode 的 yolo 快捷命令模板,你可以直接修改;每次创建沙箱时,这个文件都会同步到容器内的 `/home/devuser/.bash_aliases`。
|
|
185
185
|
|
|
186
186
|
沙箱镜像也会预装 `gh`。如果宿主机上的 `gh auth token` 能成功返回 token,`ai sandbox create` 会把它以 `GH_TOKEN` 环境变量注入容器,让你在沙箱里直接使用 `gh`,无需额外登录配置。
|
|
187
187
|
|
|
188
|
+
`ai sandbox exec` 也会向容器透传一小组终端检测白名单变量(`TERM_PROGRAM`、`TERM_PROGRAM_VERSION`、`LC_TERMINAL`、`LC_TERMINAL_VERSION`)。这样可以让交互式 TUI 保持与宿主终端一致的行为,例如 Claude Code 的 `Shift+Enter` 换行支持,同时避免把整个宿主环境灌入容器。
|
|
189
|
+
|
|
188
190
|
<a id="architecture-overview"></a>
|
|
189
191
|
|
|
190
192
|
## 架构概览
|
|
@@ -394,7 +396,7 @@ import-issue #42 从 GitHub Issue 导入任务
|
|
|
394
396
|
"project": "my-project",
|
|
395
397
|
"org": "my-org",
|
|
396
398
|
"language": "en",
|
|
397
|
-
"templateVersion": "v0.5.
|
|
399
|
+
"templateVersion": "v0.5.1",
|
|
398
400
|
"files": {
|
|
399
401
|
"managed": [
|
|
400
402
|
".agents/workspace/README.md",
|
package/bin/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ const USAGE = `agent-infra - bootstrap AI collaboration infrastructure
|
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
16
|
agent-infra init Initialize a new project with update-agent-infra seed command
|
|
17
|
-
agent-infra merge Merge
|
|
17
|
+
agent-infra merge Merge tasks from another workspace directory (active/blocked/completed/archive)
|
|
18
18
|
agent-infra update Update seed files and sync file registry for an existing project
|
|
19
19
|
agent-infra sandbox Manage Docker-based AI sandboxes
|
|
20
20
|
agent-infra version Show version
|
package/lib/merge.js
CHANGED
|
@@ -6,6 +6,15 @@ 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'];
|
|
11
|
+
const SECTION_LABELS = {
|
|
12
|
+
active: 'Active',
|
|
13
|
+
blocked: 'Blocked',
|
|
14
|
+
completed: 'Completed',
|
|
15
|
+
archive: 'Archive'
|
|
16
|
+
};
|
|
17
|
+
const DIVIDER = '═'.repeat(55);
|
|
9
18
|
|
|
10
19
|
function extractField(content, fieldName) {
|
|
11
20
|
const match = content.match(FRONTMATTER_RE);
|
|
@@ -394,50 +403,305 @@ function removeManifestFiles(rootDir) {
|
|
|
394
403
|
}
|
|
395
404
|
}
|
|
396
405
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
406
|
+
function formatTimestamp(date) {
|
|
407
|
+
return [
|
|
408
|
+
date.getFullYear(),
|
|
409
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
410
|
+
String(date.getDate()).padStart(2, '0')
|
|
411
|
+
].join('-') + ' ' + [
|
|
412
|
+
String(date.getHours()).padStart(2, '0'),
|
|
413
|
+
String(date.getMinutes()).padStart(2, '0'),
|
|
414
|
+
String(date.getSeconds()).padStart(2, '0')
|
|
415
|
+
].join(':');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function formatBackupTimestamp(date) {
|
|
419
|
+
return [
|
|
420
|
+
date.getFullYear(),
|
|
421
|
+
String(date.getMonth() + 1).padStart(2, '0'),
|
|
422
|
+
String(date.getDate()).padStart(2, '0')
|
|
423
|
+
].join('') + `-${String(date.getHours()).padStart(2, '0')}${String(date.getMinutes()).padStart(2, '0')}${String(date.getSeconds()).padStart(2, '0')}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function toPosixPath(relativePath) {
|
|
427
|
+
return relativePath.split(path.sep).join('/');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function getLatestFileMtime(taskDir) {
|
|
431
|
+
let latestMs = null;
|
|
432
|
+
const stack = [taskDir];
|
|
433
|
+
|
|
434
|
+
while (stack.length > 0) {
|
|
435
|
+
const currentDir = stack.pop();
|
|
436
|
+
if (!currentDir || !fs.existsSync(currentDir)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
441
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
442
|
+
if (entry.isDirectory()) {
|
|
443
|
+
stack.push(entryPath);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const { mtimeMs } = fs.statSync(entryPath);
|
|
448
|
+
latestMs = latestMs === null ? mtimeMs : Math.max(latestMs, mtimeMs);
|
|
449
|
+
}
|
|
401
450
|
}
|
|
402
451
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
452
|
+
return latestMs;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function getTaskTimestamp(taskDir) {
|
|
456
|
+
const taskFile = path.join(taskDir, 'task.md');
|
|
457
|
+
|
|
458
|
+
if (fs.existsSync(taskFile)) {
|
|
459
|
+
const content = fs.readFileSync(taskFile, 'utf8');
|
|
460
|
+
const updatedAt = extractField(content, 'updated_at');
|
|
461
|
+
if (updatedAt) {
|
|
462
|
+
return { value: updatedAt, source: 'frontmatter' };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const taskFileStat = fs.statSync(taskFile);
|
|
466
|
+
return {
|
|
467
|
+
value: formatTimestamp(taskFileStat.mtime),
|
|
468
|
+
source: 'task-mtime'
|
|
469
|
+
};
|
|
406
470
|
}
|
|
407
471
|
|
|
408
|
-
|
|
409
|
-
|
|
472
|
+
const latestMs = getLatestFileMtime(taskDir);
|
|
473
|
+
if (latestMs !== null) {
|
|
474
|
+
return {
|
|
475
|
+
value: formatTimestamp(new Date(latestMs)),
|
|
476
|
+
source: 'dir-mtime'
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const dirStat = fs.statSync(taskDir);
|
|
481
|
+
return {
|
|
482
|
+
value: formatTimestamp(dirStat.mtime),
|
|
483
|
+
source: 'dir-mtime'
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function compareTimestamps(left, right) {
|
|
488
|
+
return left.value.localeCompare(right.value);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function scanWorkspaceSection(rootDir, sectionName) {
|
|
492
|
+
const sectionDir = path.join(rootDir, sectionName);
|
|
493
|
+
if (!fs.existsSync(sectionDir) || !fs.statSync(sectionDir).isDirectory()) {
|
|
494
|
+
return [];
|
|
410
495
|
}
|
|
411
496
|
|
|
412
|
-
const
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
497
|
+
const records = [];
|
|
498
|
+
for (const entry of fs.readdirSync(sectionDir, { withFileTypes: true })) {
|
|
499
|
+
if (!entry.isDirectory() || !TASK_ID_RE.test(entry.name)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
416
502
|
|
|
417
|
-
|
|
503
|
+
const taskDir = path.join(sectionDir, entry.name);
|
|
504
|
+
const taskFile = path.join(taskDir, 'task.md');
|
|
505
|
+
if (!fs.existsSync(taskFile)) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
records.push({
|
|
510
|
+
taskId: entry.name,
|
|
511
|
+
section: sectionName,
|
|
512
|
+
taskDir,
|
|
513
|
+
timestamp: getTaskTimestamp(taskDir)
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return records.sort((left, right) => left.taskId.localeCompare(right.taskId));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function buildWorkspaceIndex(workspaceDir) {
|
|
521
|
+
const index = new Map();
|
|
522
|
+
|
|
523
|
+
for (const section of MUTABLE_SECTIONS) {
|
|
524
|
+
for (const record of scanWorkspaceSection(workspaceDir, section)) {
|
|
525
|
+
index.set(record.taskId, record);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return index;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function backupTaskDir(backupRoot, section, taskDir, taskId) {
|
|
533
|
+
const backupDir = path.join(backupRoot, section, taskId);
|
|
534
|
+
fs.mkdirSync(path.dirname(backupDir), { recursive: true });
|
|
535
|
+
fs.cpSync(taskDir, backupDir, { recursive: true });
|
|
536
|
+
return backupDir;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function copyTaskToSection(sourceTask, workspaceDir) {
|
|
540
|
+
const destinationDir = path.join(workspaceDir, sourceTask.section, sourceTask.taskId);
|
|
541
|
+
fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
|
|
542
|
+
fs.cpSync(sourceTask.taskDir, destinationDir, { recursive: true });
|
|
543
|
+
return destinationDir;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function detectSourceMode(sourcePath) {
|
|
547
|
+
for (const section of ALL_SECTIONS) {
|
|
548
|
+
const sectionDir = path.join(sourcePath, section);
|
|
549
|
+
if (fs.existsSync(sectionDir) && fs.statSync(sectionDir).isDirectory()) {
|
|
550
|
+
return 'workspace';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return 'legacy-archive';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function createReport(sourcePath, backupRoot) {
|
|
558
|
+
return {
|
|
559
|
+
sourcePath,
|
|
560
|
+
backupRoot,
|
|
561
|
+
sections: {
|
|
562
|
+
active: { copied: [], updated: [], moved: [], skipped: [] },
|
|
563
|
+
blocked: { copied: [], updated: [], moved: [], skipped: [] },
|
|
564
|
+
completed: { copied: [], updated: [], moved: [], skipped: [] },
|
|
565
|
+
archive: { copied: [], skipped: [] }
|
|
566
|
+
},
|
|
567
|
+
details: [],
|
|
568
|
+
backupCount: 0
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function recordMutable(report, reportSection, action, entry) {
|
|
573
|
+
report.sections[reportSection][action].push(entry);
|
|
574
|
+
report.details.push(entry);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function recordArchive(report, action, entry) {
|
|
578
|
+
report.sections.archive[action].push(entry);
|
|
579
|
+
report.details.push(entry);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function mergeMutableSections({ sourceWorkspace, localWorkspace, backupRoot, report }) {
|
|
583
|
+
const localIndex = buildWorkspaceIndex(localWorkspace);
|
|
584
|
+
|
|
585
|
+
for (const sourceSection of MUTABLE_SECTIONS) {
|
|
586
|
+
const sourceTasks = scanWorkspaceSection(sourceWorkspace, sourceSection);
|
|
587
|
+
|
|
588
|
+
for (const sourceTask of sourceTasks) {
|
|
589
|
+
const localMatch = localIndex.get(sourceTask.taskId) || null;
|
|
590
|
+
|
|
591
|
+
if (!localMatch) {
|
|
592
|
+
const destinationDir = copyTaskToSection(sourceTask, localWorkspace);
|
|
593
|
+
localIndex.set(sourceTask.taskId, {
|
|
594
|
+
taskId: sourceTask.taskId,
|
|
595
|
+
section: sourceTask.section,
|
|
596
|
+
taskDir: destinationDir,
|
|
597
|
+
timestamp: getTaskTimestamp(destinationDir)
|
|
598
|
+
});
|
|
599
|
+
recordMutable(report, sourceTask.section, 'copied', {
|
|
600
|
+
action: 'copied',
|
|
601
|
+
symbol: '✓',
|
|
602
|
+
taskId: sourceTask.taskId,
|
|
603
|
+
section: sourceTask.section,
|
|
604
|
+
detail: 'copied'
|
|
605
|
+
});
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const comparison = compareTimestamps(sourceTask.timestamp, localMatch.timestamp);
|
|
610
|
+
if (comparison > 0) {
|
|
611
|
+
backupTaskDir(backupRoot, localMatch.section, localMatch.taskDir, localMatch.taskId);
|
|
612
|
+
report.backupCount += 1;
|
|
613
|
+
fs.rmSync(localMatch.taskDir, { recursive: true, force: true });
|
|
614
|
+
|
|
615
|
+
const destinationDir = copyTaskToSection(sourceTask, localWorkspace);
|
|
616
|
+
localIndex.set(sourceTask.taskId, {
|
|
617
|
+
taskId: sourceTask.taskId,
|
|
618
|
+
section: sourceTask.section,
|
|
619
|
+
taskDir: destinationDir,
|
|
620
|
+
timestamp: getTaskTimestamp(destinationDir)
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
if (localMatch.section === sourceTask.section) {
|
|
624
|
+
recordMutable(report, sourceTask.section, 'updated', {
|
|
625
|
+
action: 'updated',
|
|
626
|
+
symbol: '↑',
|
|
627
|
+
taskId: sourceTask.taskId,
|
|
628
|
+
section: sourceTask.section,
|
|
629
|
+
detail: `updated (source newer: ${sourceTask.timestamp.value} > ${localMatch.timestamp.value})`
|
|
630
|
+
});
|
|
631
|
+
} else {
|
|
632
|
+
recordMutable(report, localMatch.section, 'moved', {
|
|
633
|
+
action: 'moved',
|
|
634
|
+
symbol: '⇄',
|
|
635
|
+
taskId: sourceTask.taskId,
|
|
636
|
+
fromSection: localMatch.section,
|
|
637
|
+
toSection: sourceTask.section,
|
|
638
|
+
detail: `moved (source newer: ${sourceTask.timestamp.value} > ${localMatch.timestamp.value})`
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (comparison < 0) {
|
|
646
|
+
recordMutable(report, localMatch.section, 'skipped', {
|
|
647
|
+
action: 'skipped',
|
|
648
|
+
symbol: '⊘',
|
|
649
|
+
taskId: sourceTask.taskId,
|
|
650
|
+
section: localMatch.section,
|
|
651
|
+
detail: `skipped (local newer: ${localMatch.timestamp.value} > ${sourceTask.timestamp.value})`
|
|
652
|
+
});
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
recordMutable(report, localMatch.section, 'skipped', {
|
|
657
|
+
action: 'skipped',
|
|
658
|
+
symbol: '⊘',
|
|
659
|
+
taskId: sourceTask.taskId,
|
|
660
|
+
section: localMatch.section,
|
|
661
|
+
detail: `skipped (same timestamp: ${sourceTask.timestamp.value})`
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function mergeArchiveSection(sourceArchive, localArchive, report) {
|
|
668
|
+
const sourceTasks = scanSourceTasks(sourceArchive);
|
|
418
669
|
|
|
419
670
|
for (const task of sourceTasks) {
|
|
420
|
-
const existingTaskDir = taskExistsInArchive(
|
|
671
|
+
const existingTaskDir = taskExistsInArchive(localArchive, task.taskId);
|
|
421
672
|
if (existingTaskDir) {
|
|
422
|
-
skipped
|
|
673
|
+
recordArchive(report, 'skipped', {
|
|
674
|
+
action: 'skipped',
|
|
675
|
+
symbol: '⊘',
|
|
423
676
|
taskId: task.taskId,
|
|
424
|
-
|
|
677
|
+
section: 'archive',
|
|
678
|
+
relativePath: `${toPosixPath(path.relative(localArchive, existingTaskDir))}/`,
|
|
679
|
+
detail: `skipped (already exists at ${toPosixPath(path.relative(localArchive, existingTaskDir))}/)`
|
|
425
680
|
});
|
|
426
681
|
continue;
|
|
427
682
|
}
|
|
428
683
|
|
|
429
|
-
const destinationDir = path.join(
|
|
684
|
+
const destinationDir = path.join(localArchive, task.relativePath);
|
|
430
685
|
fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
|
|
431
686
|
fs.cpSync(task.taskDir, destinationDir, { recursive: true });
|
|
432
|
-
|
|
687
|
+
recordArchive(report, 'copied', {
|
|
688
|
+
action: 'copied',
|
|
689
|
+
symbol: '✓',
|
|
433
690
|
taskId: task.taskId,
|
|
434
|
-
|
|
691
|
+
section: 'archive',
|
|
692
|
+
relativePath: task.relativePath,
|
|
693
|
+
detail: 'copied'
|
|
435
694
|
});
|
|
436
695
|
}
|
|
437
696
|
|
|
438
|
-
|
|
697
|
+
return sourceTasks.length;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function printLegacyArchiveMessages(report, sourcePath) {
|
|
701
|
+
const merged = report.sections.archive.copied;
|
|
702
|
+
const skipped = report.sections.archive.skipped;
|
|
439
703
|
|
|
440
|
-
if (
|
|
704
|
+
if (merged.length === 0 && skipped.length === 0) {
|
|
441
705
|
info(`No archived tasks found in ${sourcePath}`);
|
|
442
706
|
}
|
|
443
707
|
|
|
@@ -453,13 +717,169 @@ async function cmdMerge(args) {
|
|
|
453
717
|
info('Merge summary');
|
|
454
718
|
info(`- Merged: ${merged.length}`);
|
|
455
719
|
info(`- Skipped: ${skipped.length}`);
|
|
720
|
+
process.stdout.write('\n');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function printSection(lines, name, counts) {
|
|
724
|
+
const title = `${SECTION_LABELS[name].padEnd(9, ' ')} (.agents/workspace/${name}/):`;
|
|
725
|
+
lines.push(title);
|
|
726
|
+
|
|
727
|
+
const entries = [
|
|
728
|
+
['copied', '✓ Copied '],
|
|
729
|
+
['updated', '↑ Updated '],
|
|
730
|
+
['moved', '⇄ Moved '],
|
|
731
|
+
['skipped', '⊘ Skipped ']
|
|
732
|
+
].filter(([key]) => Array.isArray(counts[key]));
|
|
733
|
+
|
|
734
|
+
const nonZeroEntries = entries.filter(([key]) => counts[key].length > 0);
|
|
735
|
+
if (nonZeroEntries.length === 0) {
|
|
736
|
+
lines.push(' (no changes)', '');
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
for (const [key, label] of nonZeroEntries) {
|
|
741
|
+
lines.push(` ${label}: ${counts[key].length}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
lines.push('');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function printArchiveSection(lines, counts) {
|
|
748
|
+
const title = `${SECTION_LABELS.archive.padEnd(9, ' ')} (.agents/workspace/archive/):`;
|
|
749
|
+
lines.push(title);
|
|
750
|
+
|
|
751
|
+
if (counts.copied.length === 0 && counts.skipped.length === 0) {
|
|
752
|
+
lines.push(' (no changes)', '');
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (counts.copied.length > 0) {
|
|
757
|
+
lines.push(` ✓ Copied : ${counts.copied.length}`);
|
|
758
|
+
}
|
|
759
|
+
if (counts.skipped.length > 0) {
|
|
760
|
+
lines.push(` ⊘ Skipped : ${counts.skipped.length}`);
|
|
761
|
+
}
|
|
762
|
+
lines.push('');
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function renderDetail(entry) {
|
|
766
|
+
if (entry.action === 'moved') {
|
|
767
|
+
return ` ${entry.symbol} ${entry.taskId} ${entry.fromSection}→${entry.toSection} ${entry.detail}`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const label = entry.section.padEnd(9, ' ');
|
|
771
|
+
return ` ${entry.symbol} ${entry.taskId} ${label} ${entry.detail}`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function printReport(report) {
|
|
775
|
+
const mutableTotals = MUTABLE_SECTIONS.reduce((acc, section) => {
|
|
776
|
+
acc.copied += report.sections[section].copied.length;
|
|
777
|
+
acc.updated += report.sections[section].updated.length;
|
|
778
|
+
acc.moved += report.sections[section].moved.length;
|
|
779
|
+
acc.skipped += report.sections[section].skipped.length;
|
|
780
|
+
return acc;
|
|
781
|
+
}, { copied: 0, updated: 0, moved: 0, skipped: 0 });
|
|
782
|
+
|
|
783
|
+
const archiveTotals = {
|
|
784
|
+
copied: report.sections.archive.copied.length,
|
|
785
|
+
skipped: report.sections.archive.skipped.length
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const lines = [
|
|
789
|
+
'Merge summary',
|
|
790
|
+
DIVIDER,
|
|
791
|
+
`Source: ${report.sourcePath}`,
|
|
792
|
+
`Backup: ${report.backupRoot}`,
|
|
793
|
+
''
|
|
794
|
+
];
|
|
795
|
+
|
|
796
|
+
for (const section of MUTABLE_SECTIONS) {
|
|
797
|
+
printSection(lines, section, report.sections[section]);
|
|
798
|
+
}
|
|
799
|
+
printArchiveSection(lines, report.sections.archive);
|
|
800
|
+
|
|
801
|
+
lines.push(
|
|
802
|
+
DIVIDER,
|
|
803
|
+
`Totals: ${mutableTotals.copied + archiveTotals.copied} copied, ${mutableTotals.updated} updated, ${mutableTotals.moved} moved, ${mutableTotals.skipped + archiveTotals.skipped} skipped`,
|
|
804
|
+
`Backup contains ${report.backupCount} task(s); review and remove when verified.`,
|
|
805
|
+
'',
|
|
806
|
+
'Detailed log:'
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
if (report.details.length === 0) {
|
|
810
|
+
lines.push(' (none)');
|
|
811
|
+
} else {
|
|
812
|
+
for (const detail of report.details) {
|
|
813
|
+
lines.push(renderDetail(detail));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function cmdMerge(args) {
|
|
821
|
+
const sourcePath = args[0];
|
|
822
|
+
if (!sourcePath) {
|
|
823
|
+
throw new Error('Usage: agent-infra merge <source-path>');
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const resolvedSource = path.resolve(sourcePath);
|
|
827
|
+
if (!fs.existsSync(resolvedSource)) {
|
|
828
|
+
throw new Error(`Source path does not exist: ${sourcePath}`);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (!fs.statSync(resolvedSource).isDirectory()) {
|
|
832
|
+
throw new Error(`Source path is not a directory: ${sourcePath}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const workspaceDir = path.join(process.cwd(), '.agents', 'workspace');
|
|
836
|
+
const archiveDir = path.join(workspaceDir, 'archive');
|
|
837
|
+
const backupStamp = formatBackupTimestamp(new Date());
|
|
838
|
+
const backupRootRelative = `.agents/workspace/.merge-backup/${backupStamp}/`;
|
|
839
|
+
const backupRoot = path.join(workspaceDir, '.merge-backup', backupStamp);
|
|
840
|
+
const report = createReport(resolvedSource, backupRootRelative);
|
|
841
|
+
const mode = detectSourceMode(resolvedSource);
|
|
842
|
+
|
|
843
|
+
for (const section of ALL_SECTIONS) {
|
|
844
|
+
fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (mode === 'legacy-archive') {
|
|
848
|
+
info('Detected legacy archive source; treating the input as archive-only for backward compatibility.');
|
|
849
|
+
mergeArchiveSection(resolvedSource, archiveDir, report);
|
|
850
|
+
} else {
|
|
851
|
+
mergeMutableSections({
|
|
852
|
+
sourceWorkspace: resolvedSource,
|
|
853
|
+
localWorkspace: workspaceDir,
|
|
854
|
+
backupRoot,
|
|
855
|
+
report
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const sourceArchive = path.join(resolvedSource, 'archive');
|
|
859
|
+
if (fs.existsSync(sourceArchive) && fs.statSync(sourceArchive).isDirectory()) {
|
|
860
|
+
mergeArchiveSection(sourceArchive, archiveDir, report);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
rebuildManifests(archiveDir);
|
|
865
|
+
|
|
866
|
+
if (mode === 'legacy-archive') {
|
|
867
|
+
printLegacyArchiveMessages(report, sourcePath);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
printReport(report);
|
|
456
871
|
}
|
|
457
872
|
|
|
458
873
|
export {
|
|
459
874
|
cmdMerge,
|
|
875
|
+
compareTimestamps,
|
|
876
|
+
detectSourceMode,
|
|
460
877
|
extractField,
|
|
461
878
|
extractTitle,
|
|
879
|
+
formatBackupTimestamp,
|
|
880
|
+
getTaskTimestamp,
|
|
462
881
|
rebuildManifests,
|
|
463
882
|
scanSourceTasks,
|
|
883
|
+
scanWorkspaceSection,
|
|
464
884
|
taskExistsInArchive
|
|
465
885
|
};
|