@fitlab-ai/agent-infra 0.5.6 → 0.5.7

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 +92 -4
  2. package/README.zh-CN.md +92 -4
  3. package/bin/cli.js +28 -4
  4. package/lib/defaults.json +1 -0
  5. package/lib/init.js +68 -4
  6. package/lib/prompt.js +28 -1
  7. package/lib/render.js +1 -1
  8. package/lib/sandbox/commands/rm.js +6 -4
  9. package/lib/sandbox/commands/vm.js +43 -16
  10. package/lib/sandbox/config.js +5 -0
  11. package/lib/sandbox/engine.js +125 -16
  12. package/lib/sandbox/task-resolver.js +13 -6
  13. package/package.json +2 -2
  14. package/templates/.agents/QUICKSTART.en.md +17 -0
  15. package/templates/.agents/QUICKSTART.zh-CN.md +17 -0
  16. package/templates/.agents/README.en.md +70 -1
  17. package/templates/.agents/README.zh-CN.md +70 -1
  18. package/templates/.agents/rules/issue-pr-commands.en.md +5 -0
  19. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -0
  20. package/templates/.agents/rules/issue-sync.en.md +5 -0
  21. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -0
  22. package/templates/.agents/rules/label-milestone-setup.en.md +5 -0
  23. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +5 -0
  24. package/templates/.agents/rules/milestone-inference.en.md +5 -0
  25. package/templates/.agents/rules/milestone-inference.zh-CN.md +5 -0
  26. package/templates/.agents/rules/pr-sync.en.md +5 -0
  27. package/templates/.agents/rules/pr-sync.zh-CN.md +5 -0
  28. package/templates/.agents/rules/release-commands.en.md +5 -0
  29. package/templates/.agents/rules/release-commands.zh-CN.md +5 -0
  30. package/templates/.agents/rules/security-alerts.en.md +5 -0
  31. package/templates/.agents/rules/security-alerts.zh-CN.md +5 -0
  32. package/templates/.agents/scripts/platform-adapters/platform-sync.js +6 -0
  33. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  34. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  35. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  36. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  37. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  38. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  39. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  40. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  41. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  42. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  43. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  44. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  45. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  46. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  47. package/templates/.agents/skills/create-issue/SKILL.en.md +2 -2
  48. package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +2 -2
  49. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  50. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  51. package/templates/.agents/skills/create-release-note/SKILL.en.md +8 -1
  52. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +8 -1
  53. package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
  54. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +2 -2
  55. package/templates/.agents/skills/implement-task/SKILL.en.md +2 -2
  56. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +2 -2
  57. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
  58. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +2 -2
  59. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  60. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  61. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -2
  62. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +2 -2
  63. package/templates/.agents/skills/init-labels/SKILL.en.md +1 -1
  64. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
  65. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +6 -0
  66. package/templates/.agents/skills/init-milestones/SKILL.en.md +1 -1
  67. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +1 -1
  68. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +6 -0
  69. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  70. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  71. package/templates/.agents/skills/post-release/SKILL.en.md +95 -0
  72. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +95 -0
  73. package/templates/.agents/skills/refine-task/SKILL.en.md +1 -1
  74. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +1 -1
  75. package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
  76. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
  77. package/templates/.agents/skills/release/SKILL.en.md +6 -1
  78. package/templates/.agents/skills/release/SKILL.zh-CN.md +6 -1
  79. package/templates/.agents/skills/release/scripts/manage-milestones.sh +6 -0
  80. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  81. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  82. package/templates/.agents/skills/review-task/SKILL.en.md +2 -2
  83. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +2 -2
  84. package/templates/.agents/skills/test/SKILL.en.md +1 -1
  85. package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
  86. package/templates/.agents/skills/test-integration/SKILL.en.md +1 -1
  87. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +1 -1
  88. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +10 -2
  89. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -2
  90. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +289 -12
  91. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +1 -1
  92. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +1 -1
  93. package/templates/.agents/templates/task.en.md +2 -2
  94. package/templates/.agents/templates/task.zh-CN.md +2 -2
  95. package/templates/.claude/commands/post-release.en.md +8 -0
  96. package/templates/.claude/commands/post-release.zh-CN.md +8 -0
  97. package/templates/.gemini/commands/_project_/post-release.en.toml +6 -0
  98. package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +6 -0
  99. package/templates/.opencode/commands/post-release.en.md +9 -0
  100. package/templates/.opencode/commands/post-release.zh-CN.md +9 -0
package/README.md CHANGED
@@ -314,6 +314,7 @@ agent-infra ships with **a rich set of built-in AI skills**. They are organized
314
314
  |-------|-------------|------------|----------------------|
315
315
  | `release` | Execute the version release workflow. | `version` (`X.Y.Z`) | Publish a new project release. |
316
316
  | `create-release-note` | Generate release notes from PRs and commits. | `version`, `previous-version` (optional) | Prepare a changelog before shipping. |
317
+ | `post-release` | Run post-release follow-up tasks (version bump, artifact rebuild, optional demo capture). | None | Finalize the release cycle after pushing a release tag. |
317
318
 
318
319
  <a id="security-skills"></a>
319
320
 
@@ -392,7 +393,7 @@ If you maintain reusable team skills outside the repository, declare them in `.a
392
393
  {
393
394
  "skills": {
394
395
  "sources": [
395
- { "type": "local", "path": "~/company-skills" },
396
+ { "type": "local", "path": "~/private-skills" },
396
397
  { "type": "local", "path": "~/team-skills" }
397
398
  ]
398
399
  }
@@ -402,7 +403,7 @@ If you maintain reusable team skills outside the repository, declare them in `.a
402
403
  Expected source layout:
403
404
 
404
405
  ```text
405
- ~/company-skills/
406
+ ~/private-skills/
406
407
  enforce-style/
407
408
  SKILL.md
408
409
  release-check/
@@ -427,6 +428,54 @@ When `update-agent-infra` runs:
427
428
  - Built-in skills always win over custom sources; if a source defines a skill with the same name as a built-in skill, agent-infra skips that custom source skill instead of overriding the built-in one
428
429
  - If you truly need to replace a built-in skill or command, use the existing `ejected` mechanism and own that file in the project
429
430
 
431
+ ## Custom TUI Configuration
432
+
433
+ Use the top-level `.agents/.airc.json` `customTUIs` array when your team uses an AI TUI that is not one of the built-in command targets. This config lets agent-infra show the correct next-step commands and generate command files for project custom skills by learning from an existing command in the custom TUI directory.
434
+
435
+ | Field | Required | Meaning |
436
+ |-------|----------|---------|
437
+ | `name` | Yes | Display name shown in reports and next-step guidance, for example `Acme TUI`. |
438
+ | `dir` | Yes | Command directory relative to the project root, for example `.acme/commands`. The path must stay inside the project root. |
439
+ | `invoke` | Yes | User-facing command template used in next-step guidance. |
440
+
441
+ Supported `invoke` placeholders:
442
+
443
+ | Placeholder | Replaced with | Example |
444
+ |-------------|---------------|---------|
445
+ | `${skillName}` | The skill command name, such as `review-task` or `commit`. | `acme ${skillName}` -> `acme review-task` |
446
+ | `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/agent-infra:review-task` |
447
+
448
+ Non-namespaced custom TUI:
449
+
450
+ ```json
451
+ {
452
+ "customTUIs": [
453
+ {
454
+ "name": "Acme TUI",
455
+ "dir": ".acme/commands",
456
+ "invoke": "acme ${skillName}"
457
+ }
458
+ ]
459
+ }
460
+ ```
461
+
462
+ Namespaced custom TUI:
463
+
464
+ ```json
465
+ {
466
+ "project": "agent-infra",
467
+ "customTUIs": [
468
+ {
469
+ "name": "Internal Gemini",
470
+ "dir": ".internal-gemini/commands",
471
+ "invoke": "/${projectName}:${skillName}"
472
+ }
473
+ ]
474
+ }
475
+ ```
476
+
477
+ `customTUIs` should contain one entry per custom TUI. To let `update-agent-infra` generate command files for custom skills, keep at least one existing command file in `dir` that references a built-in skill path such as `.agents/skills/analyze-task/SKILL.md`; agent-infra uses that file as the format reference.
478
+
430
479
  <a id="prebuilt-workflows"></a>
431
480
 
432
481
  ## Prebuilt Workflows
@@ -496,12 +545,24 @@ The generated `.agents/.airc.json` file is the central contract between the boot
496
545
  "project": "my-project",
497
546
  "org": "my-org",
498
547
  "language": "en",
499
- "templateVersion": "v0.5.6",
548
+ "templateVersion": "v0.5.7",
549
+ "templates": {
550
+ "sources": [
551
+ { "type": "local", "path": "~/private-templates" }
552
+ ]
553
+ },
500
554
  "skills": {
501
555
  "sources": [
502
- { "type": "local", "path": "~/company-skills" }
556
+ { "type": "local", "path": "~/private-skills" }
503
557
  ]
504
558
  },
559
+ "customTUIs": [
560
+ {
561
+ "name": "Acme TUI",
562
+ "dir": ".acme/commands",
563
+ "invoke": "acme ${skillName}"
564
+ }
565
+ ],
505
566
  "files": {
506
567
  "managed": [
507
568
  ".agents/workspace/README.md",
@@ -530,10 +591,37 @@ The generated `.agents/.airc.json` file is the central contract between the boot
530
591
  | `org` | GitHub organization or owner used by generated metadata and links. |
531
592
  | `language` | Primary project language or locale used by rendered templates. |
532
593
  | `templateVersion` | Installed template version for future upgrades and drift tracking. |
594
+ | `templates` | Optional external template overlay configuration. |
595
+ | `templates.sources` | Optional ordered list of external template sources. Only `type: "local"` is supported today. |
533
596
  | `skills` | Optional custom skill sync configuration. |
534
597
  | `skills.sources` | Optional ordered list of external custom skill sources. Only `type: "local"` is supported today. |
598
+ | `customTUIs` | Optional top-level list of custom AI TUI adapters. |
535
599
  | `files` | Per-path update strategy configuration for managed, merged, and ejected files. |
536
600
 
601
+ ### External template and skill sources
602
+
603
+ Use external sources when your team maintains private platform templates, private rules, or shared custom skills outside this repository. You can configure them during `agent-infra init` or later by editing `.agents/.airc.json`:
604
+
605
+ ```json
606
+ {
607
+ "templates": {
608
+ "sources": [
609
+ { "type": "local", "path": "~/private-templates" },
610
+ { "type": "local", "path": "~/team-overrides/templates" }
611
+ ]
612
+ },
613
+ "skills": {
614
+ "sources": [
615
+ { "type": "local", "path": "~/private-skills" }
616
+ ]
617
+ }
618
+ }
619
+ ```
620
+
621
+ Template source precedence is built-in templates first, then external sources as supplements. External files with the same path as built-in templates are ignored and reported in `templateSources.conflicts`; between external sources, later entries override earlier entries and conflicts are also reported. Skill sources use the same local-source shape, but custom skills cannot replace built-in skills.
622
+
623
+ External template files and skill scripts can include executable JavaScript or shell commands that AI workflows may run. Only use trusted local paths.
624
+
537
625
  <a id="file-management-strategies"></a>
538
626
 
539
627
  ## File Management Strategies
package/README.zh-CN.md CHANGED
@@ -314,6 +314,7 @@ agent-infra 提供 **丰富的内置 AI skills**。它们按使用场景分组
314
314
  |-------|------|------|---------|
315
315
  | `release` | 执行版本发布流程。 | `version`(`X.Y.Z`) | 发布新版本时。 |
316
316
  | `create-release-note` | 基于 PR 和 commit 生成发布说明。 | `version`、`previous-version`(可选) | 发布前准备 changelog 时。 |
317
+ | `post-release` | 执行版本发布后的收尾工作(版本 bump、产物重建、可选动图录制)。 | 无 | 推送发布标签后完成收尾。 |
317
318
 
318
319
  <a id="security-skills"></a>
319
320
 
@@ -392,7 +393,7 @@ args: "<task-id>" # 可选
392
393
  {
393
394
  "skills": {
394
395
  "sources": [
395
- { "type": "local", "path": "~/company-skills" },
396
+ { "type": "local", "path": "~/private-skills" },
396
397
  { "type": "local", "path": "~/team-skills" }
397
398
  ]
398
399
  }
@@ -402,7 +403,7 @@ args: "<task-id>" # 可选
402
403
  源目录结构示例:
403
404
 
404
405
  ```text
405
- ~/company-skills/
406
+ ~/private-skills/
406
407
  enforce-style/
407
408
  SKILL.md
408
409
  release-check/
@@ -427,6 +428,54 @@ args: "<task-id>" # 可选
427
428
  - 内置 skill 始终优先于自定义 source;如果 source 里出现与内置 skill 同名的目录,agent-infra 会跳过该 source skill,而不是覆盖内置实现
428
429
  - 如果你确实需要替换内置 skill 或命令,请使用现有的 `ejected` 机制,让项目自己接管该文件
429
430
 
431
+ ## 自定义 TUI 配置
432
+
433
+ 当团队使用的 AI TUI 不属于内置命令目标时,可以在 `.agents/.airc.json` 顶层配置 `customTUIs` 数组。该配置用于让 agent-infra 输出正确的下一步命令,并通过学习自定义 TUI 目录中的既有命令文件,为项目自定义 skill 生成同格式命令。
434
+
435
+ | 字段 | 必填 | 含义 |
436
+ |------|------|------|
437
+ | `name` | 是 | 报告和下一步提示中展示的工具名称,例如 `Acme TUI`。 |
438
+ | `dir` | 是 | 相对项目根目录的命令目录,例如 `.acme/commands`。路径必须位于项目根目录内。 |
439
+ | `invoke` | 是 | 面向用户展示的命令模板,用于生成下一步提示。 |
440
+
441
+ `invoke` 支持的占位符:
442
+
443
+ | 占位符 | 替换为 | 示例 |
444
+ |--------|--------|------|
445
+ | `${skillName}` | skill 命令名,例如 `review-task` 或 `commit`。 | `acme ${skillName}` -> `acme review-task` |
446
+ | `${projectName}` | `.airc.json` 中的 `project` 值,适用于带命名空间的命令。 | `/${projectName}:${skillName}` -> `/agent-infra:review-task` |
447
+
448
+ 不带命名空间的自定义 TUI:
449
+
450
+ ```json
451
+ {
452
+ "customTUIs": [
453
+ {
454
+ "name": "Acme TUI",
455
+ "dir": ".acme/commands",
456
+ "invoke": "acme ${skillName}"
457
+ }
458
+ ]
459
+ }
460
+ ```
461
+
462
+ 带命名空间的自定义 TUI:
463
+
464
+ ```json
465
+ {
466
+ "project": "agent-infra",
467
+ "customTUIs": [
468
+ {
469
+ "name": "Internal Gemini",
470
+ "dir": ".internal-gemini/commands",
471
+ "invoke": "/${projectName}:${skillName}"
472
+ }
473
+ ]
474
+ }
475
+ ```
476
+
477
+ `customTUIs` 每个条目对应一个自定义 TUI。若希望 `update-agent-infra` 为自定义 skill 生成命令文件,请在 `dir` 中保留至少一个引用内置 skill 路径的既有命令文件,例如 `.agents/skills/analyze-task/SKILL.md`;agent-infra 会以该文件作为格式参考。
478
+
430
479
  <a id="prebuilt-workflows"></a>
431
480
 
432
481
  ## 预置工作流
@@ -496,12 +545,24 @@ import-issue #42 从 GitHub Issue 导入任务
496
545
  "project": "my-project",
497
546
  "org": "my-org",
498
547
  "language": "en",
499
- "templateVersion": "v0.5.6",
548
+ "templateVersion": "v0.5.7",
549
+ "templates": {
550
+ "sources": [
551
+ { "type": "local", "path": "~/private-templates" }
552
+ ]
553
+ },
500
554
  "skills": {
501
555
  "sources": [
502
- { "type": "local", "path": "~/company-skills" }
556
+ { "type": "local", "path": "~/private-skills" }
503
557
  ]
504
558
  },
559
+ "customTUIs": [
560
+ {
561
+ "name": "Acme TUI",
562
+ "dir": ".acme/commands",
563
+ "invoke": "acme ${skillName}"
564
+ }
565
+ ],
505
566
  "files": {
506
567
  "managed": [
507
568
  ".agents/workspace/README.md",
@@ -530,10 +591,37 @@ import-issue #42 从 GitHub Issue 导入任务
530
591
  | `org` | 生成元数据和链接时使用的 GitHub 组织或拥有者。 |
531
592
  | `language` | 渲染模板时采用的项目主语言或区域设置。 |
532
593
  | `templateVersion` | 当前安装的模板版本,用于升级和差异追踪。 |
594
+ | `templates` | 可选的外部模板叠加配置。 |
595
+ | `templates.sources` | 可选的外部模板源列表,按顺序应用。当前仅支持 `type: "local"`。 |
533
596
  | `skills` | 可选的自定义 skill 同步配置。 |
534
597
  | `skills.sources` | 可选的外部自定义 skill 源列表,按顺序应用。当前仅支持 `type: "local"`。 |
598
+ | `customTUIs` | 可选的顶层自定义 AI TUI 适配配置列表。 |
535
599
  | `files` | 针对具体路径配置 `managed`、`merged`、`ejected` 三类更新策略。 |
536
600
 
601
+ ### 外部模板与 skill 源
602
+
603
+ 当团队在仓库外维护私有平台模板、私有规则或共享自定义 skill 时,可以使用外部源。你可以在 `agent-infra init` 时配置,也可以之后手动编辑 `.agents/.airc.json`:
604
+
605
+ ```json
606
+ {
607
+ "templates": {
608
+ "sources": [
609
+ { "type": "local", "path": "~/private-templates" },
610
+ { "type": "local", "path": "~/team-overrides/templates" }
611
+ ]
612
+ },
613
+ "skills": {
614
+ "sources": [
615
+ { "type": "local", "path": "~/private-skills" }
616
+ ]
617
+ }
618
+ }
619
+ ```
620
+
621
+ 模板源优先级是内置模板优先,外部源作为补充。外部源中与内置模板同路径的文件会被忽略,并记录到 `templateSources.conflicts`;多个外部源之间,后面的条目覆盖前面的条目,冲突同样会记录。Skill 源使用相同的本地源结构,但自定义 skill 不能替换内置 skill。
622
+
623
+ 外部模板文件和 skill 脚本可能包含 AI 工作流会执行的 JavaScript 或 shell 命令。只使用可信的本地路径。
624
+
537
625
  <a id="file-management-strategies"></a>
538
626
 
539
627
  ## 文件管理策略
package/bin/cli.js CHANGED
@@ -35,9 +35,27 @@ Examples:
35
35
 
36
36
  const command = process.argv[2] || '';
37
37
 
38
+ async function importCommand(importPath) {
39
+ try {
40
+ return await import(importPath);
41
+ } catch (error) {
42
+ if (error?.code === 'ERR_MODULE_NOT_FOUND') {
43
+ process.stderr.write(
44
+ 'Error: Missing npm dependency. Run npm install before using agent-infra from a development checkout.\n'
45
+ );
46
+ process.stderr.write(`${error.message}\n`);
47
+ process.exitCode = 1;
48
+ return null;
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+
38
54
  switch (command) {
39
55
  case 'init': {
40
- const { cmdInit } = await import('../lib/init.js');
56
+ const imported = await importCommand('../lib/init.js');
57
+ if (!imported) break;
58
+ const { cmdInit } = imported;
41
59
  await cmdInit().catch((e) => {
42
60
  process.stderr.write(`Error: ${e.message}\n`);
43
61
  process.exitCode = 1;
@@ -45,7 +63,9 @@ switch (command) {
45
63
  break;
46
64
  }
47
65
  case 'update': {
48
- const { cmdUpdate } = await import('../lib/update.js');
66
+ const imported = await importCommand('../lib/update.js');
67
+ if (!imported) break;
68
+ const { cmdUpdate } = imported;
49
69
  await cmdUpdate().catch((e) => {
50
70
  process.stderr.write(`Error: ${e.message}\n`);
51
71
  process.exitCode = 1;
@@ -53,7 +73,9 @@ switch (command) {
53
73
  break;
54
74
  }
55
75
  case 'merge': {
56
- const { cmdMerge } = await import('../lib/merge.js');
76
+ const imported = await importCommand('../lib/merge.js');
77
+ if (!imported) break;
78
+ const { cmdMerge } = imported;
57
79
  await cmdMerge(process.argv.slice(3)).catch((e) => {
58
80
  process.stderr.write(`Error: ${e.message}\n`);
59
81
  process.exitCode = 1;
@@ -61,7 +83,9 @@ switch (command) {
61
83
  break;
62
84
  }
63
85
  case 'sandbox': {
64
- const { runSandbox } = await import('../lib/sandbox/index.js');
86
+ const imported = await importCommand('../lib/sandbox/index.js');
87
+ if (!imported) break;
88
+ const { runSandbox } = imported;
65
89
  await runSandbox(process.argv.slice(3)).catch((e) => {
66
90
  process.stderr.write(`Error: ${e.message}\n`);
67
91
  process.exitCode = 1;
package/lib/defaults.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "type": "github"
4
4
  },
5
5
  "sandbox": {
6
+ "engine": null,
6
7
  "runtimes": [
7
8
  "node20"
8
9
  ],
package/lib/init.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
+ import { platform } from 'node:os';
4
5
  import { info, ok, err } from './log.js';
5
- import { prompt, closePrompt } from './prompt.js';
6
+ import { prompt, select, closePrompt } from './prompt.js';
6
7
  import { resolveTemplateDir } from './paths.js';
7
- import { renderFile, copySkillDir } from './render.js';
8
+ import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.js';
8
9
  import { VERSION } from './version.js';
9
10
 
10
11
  const defaults = JSON.parse(
@@ -39,10 +40,19 @@ function detectOrgName() {
39
40
 
40
41
  const VALID_NAME_RE = /^[a-zA-Z0-9_.@-]+$/;
41
42
 
43
+ function parseLocalSources(input) {
44
+ return input
45
+ .split(',')
46
+ .map((entry) => entry.trim())
47
+ .filter(Boolean)
48
+ .map((entry) => ({ type: 'local', path: entry }));
49
+ }
50
+
42
51
  async function cmdInit() {
43
52
  console.log('');
44
53
  console.log(' agent-infra init');
45
54
  console.log(' ================================');
55
+ console.log(' Optional template and skill sources can be added now or later in .agents/.airc.json.');
46
56
  console.log('');
47
57
 
48
58
  // resolve templates
@@ -100,14 +110,52 @@ async function cmdInit() {
100
110
  return;
101
111
  }
102
112
 
103
- const platformType = (await prompt('Platform type', 'github')).trim() || 'github';
104
- closePrompt();
113
+ let sandboxEngine = null;
114
+ if (platform() === 'darwin') {
115
+ sandboxEngine = await select(
116
+ 'Sandbox engine (macOS)',
117
+ ['colima', 'orbstack', 'docker-desktop'],
118
+ 'colima'
119
+ );
120
+ }
121
+
122
+ const platformChoices = [...KNOWN_PLATFORMS, 'other'];
123
+ let platformType = await select('Platform', platformChoices, 'github');
124
+
125
+ if (platformType === 'other') {
126
+ platformType = (await prompt('Custom platform type', '')).trim();
127
+ if (!platformType) {
128
+ closePrompt();
129
+ err('Custom platform type is required.');
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ }
134
+
105
135
  if (!/^[a-z0-9][a-z0-9-]*$/.test(platformType)) {
136
+ closePrompt();
106
137
  err(`Platform type must match /^[a-z0-9][a-z0-9-]*$/. Got: ${platformType}`);
107
138
  process.exitCode = 1;
108
139
  return;
109
140
  }
110
141
 
142
+ if (!KNOWN_PLATFORMS.has(platformType)) {
143
+ info(
144
+ `Custom platform '${platformType}' selected. Built-in templates are only complete for github;`
145
+ + ` provide matching '.${platformType}.' or generic templates before running update-agent-infra.`
146
+ );
147
+ }
148
+
149
+ const templateSources = parseLocalSources(await prompt(
150
+ 'Template sources (optional, comma-separated local paths, e.g. ~/my-templates; Enter to skip)',
151
+ ''
152
+ ));
153
+ const skillSources = parseLocalSources(await prompt(
154
+ 'Skill sources (optional, comma-separated local paths, e.g. ~/my-skills; Enter to skip)',
155
+ ''
156
+ ));
157
+ closePrompt();
158
+
111
159
  const project = projectName;
112
160
  const replacements = { project, org: orgName };
113
161
 
@@ -177,6 +225,22 @@ async function cmdInit() {
177
225
  files: structuredClone(defaults.files)
178
226
  };
179
227
 
228
+ if (sandboxEngine) {
229
+ config.sandbox.engine = sandboxEngine;
230
+ }
231
+
232
+ if (templateSources.length > 0) {
233
+ config.templates = {
234
+ sources: templateSources
235
+ };
236
+ }
237
+
238
+ if (skillSources.length > 0) {
239
+ config.skills = {
240
+ sources: skillSources
241
+ };
242
+ }
243
+
180
244
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
181
245
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
182
246
  ok(`Generated ${configPath}`);
package/lib/prompt.js CHANGED
@@ -59,6 +59,33 @@ 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);
64
+
65
+ process.stdout.write(` ${question}:\n`);
66
+ choices.forEach((choice, index) => {
67
+ const suffix = index === defaultIndex ? ' (default)' : '';
68
+ process.stdout.write(` ${index + 1}) ${choice}${suffix}\n`);
69
+ });
70
+
71
+ ask(defaultIndex >= 0 ? `Select [${defaultIndex + 1}]: ` : 'Select: ');
72
+
73
+ setupInterface();
74
+
75
+ const line = await nextLine();
76
+ if (line === null || line.trim() === '') {
77
+ return defaultValue || choices[0];
78
+ }
79
+
80
+ const trimmed = line.trim();
81
+ const selectedIndex = Number.parseInt(trimmed, 10);
82
+ if (String(selectedIndex) === trimmed && selectedIndex >= 1 && selectedIndex <= choices.length) {
83
+ return choices[selectedIndex - 1];
84
+ }
85
+
86
+ return trimmed;
87
+ }
88
+
62
89
  function closePrompt() {
63
90
  if (_rl) {
64
91
  _rl.close();
@@ -67,4 +94,4 @@ function closePrompt() {
67
94
  }
68
95
  }
69
96
 
70
- export { prompt, closePrompt };
97
+ export { prompt, select, closePrompt };
package/lib/render.js CHANGED
@@ -158,4 +158,4 @@ function copySkillDir(srcDir, dstDir, replacements, language, platform = 'github
158
158
  }
159
159
  }
160
160
 
161
- export { renderFile, copyFile, copySkillDir };
161
+ export { renderFile, copyFile, copySkillDir, KNOWN_PLATFORMS };
@@ -11,7 +11,7 @@ import {
11
11
  sandboxLabel,
12
12
  worktreeDirCandidates
13
13
  } from '../constants.js';
14
- import { isVmManaged } from '../engine.js';
14
+ import { detectEngine, engineDisplayName, isManagedEngine, stopManagedVm } from '../engine.js';
15
15
  import { run, runOk, runSafe } from '../shell.js';
16
16
  import { resolveTaskBranch } from '../task-resolver.js';
17
17
  import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
@@ -165,13 +165,15 @@ async function rmAll(config, tools) {
165
165
  runSafe('docker', ['rmi', config.imageName]);
166
166
  }
167
167
 
168
- if (isVmManaged()) {
168
+ const engine = detectEngine(config);
169
+ if (isManagedEngine(engine)) {
170
+ const name = engineDisplayName(engine);
169
171
  const shouldStopVm = await p.confirm({
170
- message: 'Stop Colima VM?',
172
+ message: `Stop ${name} VM?`,
171
173
  initialValue: false
172
174
  });
173
175
  if (!p.isCancel(shouldStopVm) && shouldStopVm) {
174
- runSafe('colima', ['stop']);
176
+ stopManagedVm(config);
175
177
  }
176
178
  }
177
179
 
@@ -3,31 +3,49 @@ import * as p from '@clack/prompts';
3
3
  import pc from 'picocolors';
4
4
  import { loadConfig } from '../config.js';
5
5
  import { parsePositiveIntegerOption } from '../constants.js';
6
- import { detectEngine, ensureDocker, isVmManaged } from '../engine.js';
7
- import { run, runOk, runSafe } from '../shell.js';
6
+ import {
7
+ ENGINES,
8
+ detectEngine,
9
+ engineDisplayName,
10
+ ensureDocker,
11
+ isManagedEngine,
12
+ stopManagedVm
13
+ } from '../engine.js';
14
+ import { runOk, runSafe } from '../shell.js';
8
15
 
9
16
  const USAGE = `Usage: ai sandbox vm <status|start|stop> [--cpu <n>] [--memory <n>]`;
10
17
 
11
- function ensureManagedVm() {
12
- if (!isVmManaged()) {
13
- throw new Error(`VM management is unavailable on ${detectEngine()}.`);
18
+ function ensureManagedVm(engine) {
19
+ if (!isManagedEngine(engine)) {
20
+ throw new Error(`VM management is unavailable for engine '${engineDisplayName(engine)}'.`);
14
21
  }
15
22
  }
16
23
 
17
24
  function status() {
18
- ensureManagedVm();
25
+ const config = loadConfig();
26
+ const engine = detectEngine(config);
27
+ const name = engineDisplayName(engine);
28
+ ensureManagedVm(engine);
19
29
  p.intro(pc.cyan('Sandbox VM status'));
20
30
 
21
- if (runOk('colima', ['status'])) {
22
- process.stdout.write(`${runSafe('colima', ['status'])}\n`);
23
- } else {
24
- p.log.warn('Colima VM is not running');
31
+ if (engine === ENGINES.COLIMA) {
32
+ if (runOk('colima', ['status'])) {
33
+ process.stdout.write(`${runSafe('colima', ['status'])}\n`);
34
+ } else {
35
+ p.log.warn('Colima VM is not running');
36
+ }
37
+ return;
38
+ }
39
+
40
+ if (!runOk('orb', ['status'])) {
41
+ p.log.warn(`${name} VM is not running`);
42
+ return;
25
43
  }
44
+
45
+ process.stdout.write(`${runSafe('orb', ['status'])}\n`);
26
46
  }
27
47
 
28
48
  async function start(args) {
29
- ensureManagedVm();
30
-
31
49
  const { values } = parseArgs({
32
50
  args,
33
51
  allowPositionals: true,
@@ -45,6 +63,8 @@ async function start(args) {
45
63
  }
46
64
 
47
65
  const config = loadConfig();
66
+ const engine = detectEngine(config);
67
+ ensureManagedVm(engine);
48
68
  const effectiveConfig = {
49
69
  ...config,
50
70
  vm: {
@@ -62,15 +82,22 @@ async function start(args) {
62
82
  }
63
83
 
64
84
  function stop() {
65
- ensureManagedVm();
85
+ const config = loadConfig();
86
+ const engine = detectEngine(config);
87
+ const name = engineDisplayName(engine);
88
+ ensureManagedVm(engine);
66
89
  p.intro(pc.cyan('Stopping sandbox VM'));
67
90
 
68
- if (!runOk('colima', ['status'])) {
69
- p.log.warn('Colima VM is not running');
91
+ if (engine === ENGINES.COLIMA && !runOk('colima', ['status'])) {
92
+ p.log.warn(`${name} VM is not running`);
93
+ return;
94
+ }
95
+ if (engine === ENGINES.ORBSTACK && !runOk('orb', ['status'])) {
96
+ p.log.warn(`${name} VM is not running`);
70
97
  return;
71
98
  }
72
99
 
73
- run('colima', ['stop']);
100
+ stopManagedVm(config);
74
101
  p.outro(pc.green('VM stopped'));
75
102
  }
76
103
 
@@ -1,8 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
+ import { validateSandboxEngine } from './engine.js';
4
5
 
5
6
  const DEFAULTS = Object.freeze({
7
+ engine: null,
6
8
  runtimes: ['node20'],
7
9
  tools: ['claude-code', 'codex', 'opencode', 'gemini-cli'],
8
10
  dockerfile: null,
@@ -26,6 +28,7 @@ function detectRepoRoot() {
26
28
 
27
29
  function cloneDefaults() {
28
30
  return {
31
+ engine: DEFAULTS.engine,
29
32
  runtimes: [...DEFAULTS.runtimes],
30
33
  tools: [...DEFAULTS.tools],
31
34
  dockerfile: DEFAULTS.dockerfile,
@@ -49,6 +52,7 @@ export function loadConfig() {
49
52
  const airc = JSON.parse(fs.readFileSync(configPath, 'utf8'));
50
53
  const defaults = cloneDefaults();
51
54
  const sandbox = airc.sandbox ?? {};
55
+ const engine = validateSandboxEngine(sandbox.engine ?? defaults.engine);
52
56
  const project = airc.project;
53
57
 
54
58
  if (!project || typeof project !== 'string') {
@@ -64,6 +68,7 @@ export function loadConfig() {
64
68
  containerPrefix: `${project}-dev`,
65
69
  imageName: `${project}-sandbox:latest`,
66
70
  worktreeBase: path.join(home, '.agent-infra', 'worktrees', project),
71
+ engine,
67
72
  runtimes: Array.isArray(sandbox.runtimes) && sandbox.runtimes.length > 0
68
73
  ? [...sandbox.runtimes]
69
74
  : defaults.runtimes,