@ag-eco/agentplate-cli 0.13.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/LICENSE +21 -0
- package/README.md +462 -0
- package/agents/ap-co-creation.md +90 -0
- package/agents/builder.md +144 -0
- package/agents/coordinator.md +377 -0
- package/agents/lead.md +435 -0
- package/agents/merger.md +164 -0
- package/agents/monitor.md +214 -0
- package/agents/orchestrator.md +239 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +125 -0
- package/agents/supervisor.md +427 -0
- package/package.json +66 -0
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/copilot-hooks-deployer.test.ts +162 -0
- package/src/agents/copilot-hooks-deployer.ts +93 -0
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/headless-mail-injector.test.ts +709 -0
- package/src/agents/headless-mail-injector.ts +377 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +3119 -0
- package/src/agents/hooks-deployer.ts +804 -0
- package/src/agents/identity.test.ts +604 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/manifest.test.ts +1026 -0
- package/src/agents/manifest.ts +376 -0
- package/src/agents/overlay.test.ts +1058 -0
- package/src/agents/overlay.ts +490 -0
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +230 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +328 -0
- package/src/commands/agents.ts +299 -0
- package/src/commands/clean.test.ts +797 -0
- package/src/commands/clean.ts +791 -0
- package/src/commands/completions.test.ts +348 -0
- package/src/commands/completions.ts +981 -0
- package/src/commands/coordinator.test.ts +2975 -0
- package/src/commands/coordinator.ts +1841 -0
- package/src/commands/costs.test.ts +1183 -0
- package/src/commands/costs.ts +599 -0
- package/src/commands/dashboard.test.ts +954 -0
- package/src/commands/dashboard.ts +1212 -0
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.test.ts +303 -0
- package/src/commands/doctor.ts +311 -0
- package/src/commands/ecosystem.test.ts +226 -0
- package/src/commands/ecosystem.ts +248 -0
- package/src/commands/errors.test.ts +654 -0
- package/src/commands/errors.ts +197 -0
- package/src/commands/feed.test.ts +709 -0
- package/src/commands/feed.ts +260 -0
- package/src/commands/group.test.ts +475 -0
- package/src/commands/group.ts +546 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +263 -0
- package/src/commands/init.test.ts +1011 -0
- package/src/commands/init.ts +967 -0
- package/src/commands/inspect.test.ts +1239 -0
- package/src/commands/inspect.ts +648 -0
- package/src/commands/log.test.ts +1913 -0
- package/src/commands/log.ts +958 -0
- package/src/commands/logs.test.ts +801 -0
- package/src/commands/logs.ts +483 -0
- package/src/commands/mail.test.ts +1501 -0
- package/src/commands/mail.ts +848 -0
- package/src/commands/merge.test.ts +864 -0
- package/src/commands/merge.ts +381 -0
- package/src/commands/metrics.test.ts +458 -0
- package/src/commands/metrics.ts +129 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +409 -0
- package/src/commands/nudge.test.ts +579 -0
- package/src/commands/nudge.ts +646 -0
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +612 -0
- package/src/commands/prime.ts +359 -0
- package/src/commands/replay.test.ts +757 -0
- package/src/commands/replay.ts +231 -0
- package/src/commands/run.test.ts +469 -0
- package/src/commands/run.ts +353 -0
- package/src/commands/serve/agent-actions.test.ts +210 -0
- package/src/commands/serve/agent-actions.ts +192 -0
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +410 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1680 -0
- package/src/commands/serve/rest.ts +1130 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +654 -0
- package/src/commands/sling.test.ts +1583 -0
- package/src/commands/sling.ts +1351 -0
- package/src/commands/spec.test.ts +179 -0
- package/src/commands/spec.ts +105 -0
- package/src/commands/status.test.ts +614 -0
- package/src/commands/status.ts +403 -0
- package/src/commands/stop.test.ts +964 -0
- package/src/commands/stop.ts +319 -0
- package/src/commands/supervisor.test.ts +185 -0
- package/src/commands/supervisor.ts +537 -0
- package/src/commands/trace.test.ts +762 -0
- package/src/commands/trace.ts +205 -0
- package/src/commands/update.test.ts +466 -0
- package/src/commands/update.ts +263 -0
- package/src/commands/upgrade.test.ts +48 -0
- package/src/commands/upgrade.ts +240 -0
- package/src/commands/watch.test.ts +257 -0
- package/src/commands/watch.ts +308 -0
- package/src/commands/worktree.test.ts +1297 -0
- package/src/commands/worktree.ts +451 -0
- package/src/config.test.ts +1535 -0
- package/src/config.ts +1064 -0
- package/src/doctor/agents.test.ts +523 -0
- package/src/doctor/agents.ts +399 -0
- package/src/doctor/config-check.test.ts +191 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +807 -0
- package/src/doctor/consistency.ts +347 -0
- package/src/doctor/databases.test.ts +350 -0
- package/src/doctor/databases.ts +243 -0
- package/src/doctor/dependencies.test.ts +296 -0
- package/src/doctor/dependencies.ts +272 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +156 -0
- package/src/doctor/logs.test.ts +253 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +315 -0
- package/src/doctor/merge-queue.ts +167 -0
- package/src/doctor/providers.test.ts +409 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/structure.test.ts +423 -0
- package/src/doctor/structure.ts +285 -0
- package/src/doctor/types.ts +43 -0
- package/src/doctor/version.test.ts +241 -0
- package/src/doctor/version.ts +132 -0
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +214 -0
- package/src/e2e/init-sling-lifecycle.test.ts +283 -0
- package/src/errors.test.ts +350 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tailer.test.ts +719 -0
- package/src/events/tailer.ts +332 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +533 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.test.ts +72 -0
- package/src/json.ts +53 -0
- package/src/loam/client.test.ts +752 -0
- package/src/loam/client.ts +664 -0
- package/src/logging/color.test.ts +252 -0
- package/src/logging/color.ts +105 -0
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +255 -0
- package/src/logging/logger.test.ts +814 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +110 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/logging/theme.ts +140 -0
- package/src/mail/broadcast.test.ts +204 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +774 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +898 -0
- package/src/mail/store.ts +425 -0
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/queue.test.ts +426 -0
- package/src/merge/queue.ts +246 -0
- package/src/merge/resolver.test.ts +1993 -0
- package/src/merge/resolver.ts +926 -0
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/pricing.ts +135 -0
- package/src/metrics/store.test.ts +978 -0
- package/src/metrics/store.ts +501 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +483 -0
- package/src/metrics/transcript.ts +114 -0
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +1474 -0
- package/src/runtimes/claude.ts +579 -0
- package/src/runtimes/codex.test.ts +805 -0
- package/src/runtimes/codex.ts +273 -0
- package/src/runtimes/connections.test.ts +214 -0
- package/src/runtimes/connections.ts +103 -0
- package/src/runtimes/copilot.test.ts +707 -0
- package/src/runtimes/copilot.ts +316 -0
- package/src/runtimes/cursor.test.ts +497 -0
- package/src/runtimes/cursor.ts +205 -0
- package/src/runtimes/gemini.test.ts +537 -0
- package/src/runtimes/gemini.ts +243 -0
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/opencode.test.ts +325 -0
- package/src/runtimes/opencode.ts +188 -0
- package/src/runtimes/pi-guards.test.ts +486 -0
- package/src/runtimes/pi-guards.ts +367 -0
- package/src/runtimes/pi.test.ts +789 -0
- package/src/runtimes/pi.ts +305 -0
- package/src/runtimes/registry.test.ts +196 -0
- package/src/runtimes/registry.ts +99 -0
- package/src/runtimes/sapling.test.ts +1267 -0
- package/src/runtimes/sapling.ts +710 -0
- package/src/runtimes/types.ts +266 -0
- package/src/schema-consistency.test.ts +246 -0
- package/src/sessions/compat.test.ts +281 -0
- package/src/sessions/compat.ts +105 -0
- package/src/sessions/store.test.ts +1748 -0
- package/src/sessions/store.ts +858 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +145 -0
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/tools/loam/api.ts +368 -0
- package/src/tools/loam/cli.ts +278 -0
- package/src/tools/loam/commands/add.ts +52 -0
- package/src/tools/loam/commands/archive.ts +214 -0
- package/src/tools/loam/commands/audit.ts +276 -0
- package/src/tools/loam/commands/compact.ts +1062 -0
- package/src/tools/loam/commands/completions.ts +79 -0
- package/src/tools/loam/commands/config.ts +381 -0
- package/src/tools/loam/commands/delete-domain.ts +121 -0
- package/src/tools/loam/commands/delete.ts +316 -0
- package/src/tools/loam/commands/diff.ts +200 -0
- package/src/tools/loam/commands/doctor.ts +1113 -0
- package/src/tools/loam/commands/edit.ts +226 -0
- package/src/tools/loam/commands/init.ts +31 -0
- package/src/tools/loam/commands/learn.ts +179 -0
- package/src/tools/loam/commands/move.ts +323 -0
- package/src/tools/loam/commands/onboard.ts +374 -0
- package/src/tools/loam/commands/outcome.ts +185 -0
- package/src/tools/loam/commands/prime.ts +688 -0
- package/src/tools/loam/commands/prune.ts +614 -0
- package/src/tools/loam/commands/query.ts +218 -0
- package/src/tools/loam/commands/rank.ts +180 -0
- package/src/tools/loam/commands/ready.ts +189 -0
- package/src/tools/loam/commands/record.ts +1210 -0
- package/src/tools/loam/commands/restore.ts +166 -0
- package/src/tools/loam/commands/search.ts +327 -0
- package/src/tools/loam/commands/setup.ts +887 -0
- package/src/tools/loam/commands/status.ts +103 -0
- package/src/tools/loam/commands/sync.ts +298 -0
- package/src/tools/loam/commands/update.ts +19 -0
- package/src/tools/loam/commands/upgrade.ts +93 -0
- package/src/tools/loam/commands/validate.ts +190 -0
- package/src/tools/loam/index.ts +62 -0
- package/src/tools/loam/log.ts +127 -0
- package/src/tools/loam/registry/builtins.ts +409 -0
- package/src/tools/loam/registry/custom.ts +431 -0
- package/src/tools/loam/registry/init.ts +55 -0
- package/src/tools/loam/registry/template.ts +40 -0
- package/src/tools/loam/registry/type-registry.ts +113 -0
- package/src/tools/loam/schemas/config-schema.ts +489 -0
- package/src/tools/loam/schemas/config.ts +245 -0
- package/src/tools/loam/schemas/index.ts +18 -0
- package/src/tools/loam/schemas/record-schema.ts +191 -0
- package/src/tools/loam/schemas/record.ts +115 -0
- package/src/tools/loam/utils/active-work.ts +205 -0
- package/src/tools/loam/utils/anchor-validity.ts +80 -0
- package/src/tools/loam/utils/archive.ts +146 -0
- package/src/tools/loam/utils/audit.ts +667 -0
- package/src/tools/loam/utils/bm25.ts +238 -0
- package/src/tools/loam/utils/budget.ts +142 -0
- package/src/tools/loam/utils/config.ts +344 -0
- package/src/tools/loam/utils/dir-anchors.ts +62 -0
- package/src/tools/loam/utils/domain-rules.ts +114 -0
- package/src/tools/loam/utils/expertise.ts +393 -0
- package/src/tools/loam/utils/format-helpers.ts +96 -0
- package/src/tools/loam/utils/format.ts +1234 -0
- package/src/tools/loam/utils/git-context.ts +50 -0
- package/src/tools/loam/utils/git.ts +183 -0
- package/src/tools/loam/utils/hooks.ts +299 -0
- package/src/tools/loam/utils/index.ts +52 -0
- package/src/tools/loam/utils/json-output.ts +13 -0
- package/src/tools/loam/utils/lock.ts +76 -0
- package/src/tools/loam/utils/markers.ts +48 -0
- package/src/tools/loam/utils/numeric-flags.ts +20 -0
- package/src/tools/loam/utils/palette.ts +44 -0
- package/src/tools/loam/utils/prime-ranking.ts +135 -0
- package/src/tools/loam/utils/recipe-discovery.ts +195 -0
- package/src/tools/loam/utils/runtime-flags.ts +28 -0
- package/src/tools/loam/utils/scoring.ts +94 -0
- package/src/tools/loam/utils/version.ts +116 -0
- package/src/tools/sprout/commands/block.ts +64 -0
- package/src/tools/sprout/commands/blocked.ts +86 -0
- package/src/tools/sprout/commands/close.ts +129 -0
- package/src/tools/sprout/commands/completions.ts +198 -0
- package/src/tools/sprout/commands/config.ts +238 -0
- package/src/tools/sprout/commands/create.ts +164 -0
- package/src/tools/sprout/commands/dep.ts +148 -0
- package/src/tools/sprout/commands/doctor.ts +979 -0
- package/src/tools/sprout/commands/init.ts +83 -0
- package/src/tools/sprout/commands/label.ts +178 -0
- package/src/tools/sprout/commands/list.ts +210 -0
- package/src/tools/sprout/commands/migrate.ts +133 -0
- package/src/tools/sprout/commands/onboard.ts +207 -0
- package/src/tools/sprout/commands/plan-show.ts +278 -0
- package/src/tools/sprout/commands/plan.ts +2526 -0
- package/src/tools/sprout/commands/prime.ts +399 -0
- package/src/tools/sprout/commands/ready.ts +245 -0
- package/src/tools/sprout/commands/search.ts +221 -0
- package/src/tools/sprout/commands/show.ts +277 -0
- package/src/tools/sprout/commands/stats.ts +146 -0
- package/src/tools/sprout/commands/sync.ts +134 -0
- package/src/tools/sprout/commands/tpl.ts +364 -0
- package/src/tools/sprout/commands/unblock.ts +115 -0
- package/src/tools/sprout/commands/update.ts +257 -0
- package/src/tools/sprout/commands/upgrade.ts +91 -0
- package/src/tools/sprout/config-schema.ts +152 -0
- package/src/tools/sprout/config.ts +355 -0
- package/src/tools/sprout/filter.ts +107 -0
- package/src/tools/sprout/format.ts +43 -0
- package/src/tools/sprout/id.ts +22 -0
- package/src/tools/sprout/index.ts +204 -0
- package/src/tools/sprout/log.ts +76 -0
- package/src/tools/sprout/markers.ts +22 -0
- package/src/tools/sprout/output.ts +121 -0
- package/src/tools/sprout/plan-backref.ts +93 -0
- package/src/tools/sprout/plan-context.ts +81 -0
- package/src/tools/sprout/plan-domain.ts +139 -0
- package/src/tools/sprout/plan-lifecycle.ts +65 -0
- package/src/tools/sprout/plan-loam.ts +207 -0
- package/src/tools/sprout/plan-schema.ts +209 -0
- package/src/tools/sprout/sort.ts +31 -0
- package/src/tools/sprout/store.ts +172 -0
- package/src/tools/sprout/types.ts +118 -0
- package/src/tools/sprout/validation.ts +119 -0
- package/src/tools/sprout/version.ts +1 -0
- package/src/tools/sprout/yaml.ts +387 -0
- package/src/tools/trellis/commands/archive.ts +87 -0
- package/src/tools/trellis/commands/completions.ts +610 -0
- package/src/tools/trellis/commands/config.ts +382 -0
- package/src/tools/trellis/commands/create.ts +252 -0
- package/src/tools/trellis/commands/diff.ts +150 -0
- package/src/tools/trellis/commands/doctor.ts +771 -0
- package/src/tools/trellis/commands/emit.ts +365 -0
- package/src/tools/trellis/commands/history.ts +83 -0
- package/src/tools/trellis/commands/import.ts +198 -0
- package/src/tools/trellis/commands/init.ts +81 -0
- package/src/tools/trellis/commands/list.ts +103 -0
- package/src/tools/trellis/commands/onboard.ts +156 -0
- package/src/tools/trellis/commands/pin.ts +172 -0
- package/src/tools/trellis/commands/prime.ts +193 -0
- package/src/tools/trellis/commands/render.ts +122 -0
- package/src/tools/trellis/commands/schema.ts +353 -0
- package/src/tools/trellis/commands/show.ts +115 -0
- package/src/tools/trellis/commands/stats.ts +65 -0
- package/src/tools/trellis/commands/sync.ts +112 -0
- package/src/tools/trellis/commands/tree.ts +123 -0
- package/src/tools/trellis/commands/update.ts +330 -0
- package/src/tools/trellis/commands/upgrade.ts +95 -0
- package/src/tools/trellis/commands/validate.ts +166 -0
- package/src/tools/trellis/config-schema.ts +81 -0
- package/src/tools/trellis/config.ts +108 -0
- package/src/tools/trellis/frontmatter.ts +348 -0
- package/src/tools/trellis/id.ts +24 -0
- package/src/tools/trellis/index.ts +209 -0
- package/src/tools/trellis/markers.ts +28 -0
- package/src/tools/trellis/output.ts +84 -0
- package/src/tools/trellis/render.ts +212 -0
- package/src/tools/trellis/store.ts +144 -0
- package/src/tools/trellis/types.ts +82 -0
- package/src/tools/trellis/validate.ts +199 -0
- package/src/tools/trellis/yaml.ts +309 -0
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +90 -0
- package/src/tracker/factory.ts +65 -0
- package/src/tracker/sprout.test.ts +461 -0
- package/src/tracker/sprout.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/trellis/client.test.ts +107 -0
- package/src/trellis/client.ts +179 -0
- package/src/types.ts +970 -0
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/browser.test.ts +49 -0
- package/src/utils/browser.ts +48 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +152 -0
- package/src/utils/pid.ts +130 -0
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/version.ts +5 -0
- package/src/watchdog/daemon.test.ts +3721 -0
- package/src/watchdog/daemon.ts +1257 -0
- package/src/watchdog/health.test.ts +830 -0
- package/src/watchdog/health.ts +434 -0
- package/src/watchdog/triage.test.ts +205 -0
- package/src/watchdog/triage.ts +205 -0
- package/src/worktree/manager.test.ts +720 -0
- package/src/worktree/manager.ts +405 -0
- package/src/worktree/process.test.ts +172 -0
- package/src/worktree/process.ts +131 -0
- package/src/worktree/tmux.test.ts +1616 -0
- package/src/worktree/tmux.ts +721 -0
- package/templates/CLAUDE.md.tmpl +100 -0
- package/templates/copilot-hooks.json.tmpl +13 -0
- package/templates/hooks.json.tmpl +109 -0
- package/templates/overlay.md.tmpl +88 -0
- package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
- package/ui/dist/chunk-8s31f05k.css +1 -0
- package/ui/dist/chunk-vm5rz679.js +300 -0
- package/ui/dist/favicon-nzb39vza.svg +4 -0
- package/ui/dist/index.html +17 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { cleanupTempDir, createTempGitRepo, runGitInDir } from "../test-helpers.ts";
|
|
5
|
+
import type { Spawner } from "./init.ts";
|
|
6
|
+
import { AGENTPLATE_GITIGNORE, AGENTPLATE_README, initCommand, resolveToolSet } from "./init.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tests for `agentplate init` -- agent definition deployment.
|
|
10
|
+
*
|
|
11
|
+
* Uses real temp git repos. Suppresses stdout to keep test output clean.
|
|
12
|
+
* process.cwd() is saved/restored because initCommand uses it to find the project root.
|
|
13
|
+
*
|
|
14
|
+
* Tests that don't exercise ecosystem bootstrap pass a no-op spawner via _spawner
|
|
15
|
+
* so they don't require lm/sr/tl CLIs to be installed (they aren't available in CI).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** No-op spawner that treats all ecosystem tools as "not installed". */
|
|
19
|
+
const noopSpawner: Spawner = async () => ({ exitCode: 1, stdout: "", stderr: "not found" });
|
|
20
|
+
|
|
21
|
+
const AGENT_DEF_FILES = [
|
|
22
|
+
"scout.md",
|
|
23
|
+
"builder.md",
|
|
24
|
+
"reviewer.md",
|
|
25
|
+
"lead.md",
|
|
26
|
+
"merger.md",
|
|
27
|
+
"coordinator.md",
|
|
28
|
+
"monitor.md",
|
|
29
|
+
"orchestrator.md",
|
|
30
|
+
"ap-co-creation.md",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Resolve the source agents directory (same logic as init.ts). */
|
|
34
|
+
const SOURCE_AGENTS_DIR = join(import.meta.dir, "..", "..", "agents");
|
|
35
|
+
|
|
36
|
+
describe("initCommand: agent-defs deployment", () => {
|
|
37
|
+
let tempDir: string;
|
|
38
|
+
let originalCwd: string;
|
|
39
|
+
let originalWrite: typeof process.stdout.write;
|
|
40
|
+
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
tempDir = await createTempGitRepo();
|
|
43
|
+
originalCwd = process.cwd();
|
|
44
|
+
process.chdir(tempDir);
|
|
45
|
+
|
|
46
|
+
// Suppress stdout noise from initCommand
|
|
47
|
+
originalWrite = process.stdout.write;
|
|
48
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
process.chdir(originalCwd);
|
|
53
|
+
process.stdout.write = originalWrite;
|
|
54
|
+
await cleanupTempDir(tempDir);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("creates .agentplate/agent-defs/ with all 9 agent definition files (supervisor deprecated)", async () => {
|
|
58
|
+
await initCommand({ _spawner: noopSpawner });
|
|
59
|
+
|
|
60
|
+
const agentDefsDir = join(tempDir, ".agentplate", "agent-defs");
|
|
61
|
+
const files = await readdir(agentDefsDir);
|
|
62
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
63
|
+
|
|
64
|
+
expect(mdFiles).toEqual(AGENT_DEF_FILES.slice().sort());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("copied files match source content", async () => {
|
|
68
|
+
await initCommand({ _spawner: noopSpawner });
|
|
69
|
+
|
|
70
|
+
for (const fileName of AGENT_DEF_FILES) {
|
|
71
|
+
const sourcePath = join(SOURCE_AGENTS_DIR, fileName);
|
|
72
|
+
const targetPath = join(tempDir, ".agentplate", "agent-defs", fileName);
|
|
73
|
+
|
|
74
|
+
const sourceContent = await Bun.file(sourcePath).text();
|
|
75
|
+
const targetContent = await Bun.file(targetPath).text();
|
|
76
|
+
|
|
77
|
+
expect(targetContent).toBe(sourceContent);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("--force reinit overwrites existing agent def files", async () => {
|
|
82
|
+
// First init
|
|
83
|
+
await initCommand({ _spawner: noopSpawner });
|
|
84
|
+
|
|
85
|
+
// Tamper with one of the deployed files
|
|
86
|
+
const tamperPath = join(tempDir, ".agentplate", "agent-defs", "scout.md");
|
|
87
|
+
await Bun.write(tamperPath, "# tampered content\n");
|
|
88
|
+
|
|
89
|
+
// Verify tamper worked
|
|
90
|
+
const tampered = await Bun.file(tamperPath).text();
|
|
91
|
+
expect(tampered).toBe("# tampered content\n");
|
|
92
|
+
|
|
93
|
+
// Reinit with --force
|
|
94
|
+
await initCommand({ force: true, _spawner: noopSpawner });
|
|
95
|
+
|
|
96
|
+
// Verify the file was overwritten with the original source
|
|
97
|
+
const sourceContent = await Bun.file(join(SOURCE_AGENTS_DIR, "scout.md")).text();
|
|
98
|
+
const restored = await Bun.file(tamperPath).text();
|
|
99
|
+
expect(restored).toBe(sourceContent);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("Stop hook includes loam learn command", async () => {
|
|
103
|
+
await initCommand({ _spawner: noopSpawner });
|
|
104
|
+
|
|
105
|
+
const hooksPath = join(tempDir, ".agentplate", "hooks.json");
|
|
106
|
+
const content = await Bun.file(hooksPath).text();
|
|
107
|
+
const parsed = JSON.parse(content);
|
|
108
|
+
const stopHooks = parsed.hooks.Stop[0].hooks;
|
|
109
|
+
|
|
110
|
+
expect(stopHooks.length).toBe(2);
|
|
111
|
+
expect(stopHooks[0].command).toContain("ap log session-end");
|
|
112
|
+
expect(stopHooks[1].command).toBe("loam learn");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("PostToolUse hooks include Bash-matched loam diff hook", async () => {
|
|
116
|
+
await initCommand({ _spawner: noopSpawner });
|
|
117
|
+
|
|
118
|
+
const hooksPath = join(tempDir, ".agentplate", "hooks.json");
|
|
119
|
+
const content = await Bun.file(hooksPath).text();
|
|
120
|
+
const parsed = JSON.parse(content);
|
|
121
|
+
const postToolUseHooks = parsed.hooks.PostToolUse;
|
|
122
|
+
|
|
123
|
+
// Should have the generic tool-end logger plus the new Bash-specific hook
|
|
124
|
+
expect(postToolUseHooks.length).toBe(2);
|
|
125
|
+
|
|
126
|
+
const bashHookEntry = postToolUseHooks[1];
|
|
127
|
+
expect(bashHookEntry.matcher).toBe("Bash");
|
|
128
|
+
expect(bashHookEntry.hooks.length).toBe(1);
|
|
129
|
+
|
|
130
|
+
const command = bashHookEntry.hooks[0].command;
|
|
131
|
+
expect(command).toContain("git commit");
|
|
132
|
+
expect(command).toContain("loam diff HEAD~1");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("initCommand: .agentplate/.gitignore", () => {
|
|
137
|
+
let tempDir: string;
|
|
138
|
+
let originalCwd: string;
|
|
139
|
+
let originalWrite: typeof process.stdout.write;
|
|
140
|
+
|
|
141
|
+
beforeEach(async () => {
|
|
142
|
+
tempDir = await createTempGitRepo();
|
|
143
|
+
originalCwd = process.cwd();
|
|
144
|
+
process.chdir(tempDir);
|
|
145
|
+
|
|
146
|
+
// Suppress stdout noise from initCommand
|
|
147
|
+
originalWrite = process.stdout.write;
|
|
148
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterEach(async () => {
|
|
152
|
+
process.chdir(originalCwd);
|
|
153
|
+
process.stdout.write = originalWrite;
|
|
154
|
+
await cleanupTempDir(tempDir);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("creates .agentplate/.gitignore with wildcard+whitelist model", async () => {
|
|
158
|
+
await initCommand({ _spawner: noopSpawner });
|
|
159
|
+
|
|
160
|
+
const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
|
|
161
|
+
const content = await Bun.file(gitignorePath).text();
|
|
162
|
+
|
|
163
|
+
// Verify wildcard+whitelist pattern
|
|
164
|
+
expect(content).toContain("*\n");
|
|
165
|
+
expect(content).toContain("!.gitignore\n");
|
|
166
|
+
expect(content).toContain("!config.yaml\n");
|
|
167
|
+
expect(content).toContain("!agent-manifest.json\n");
|
|
168
|
+
expect(content).toContain("!hooks.json\n");
|
|
169
|
+
expect(content).toContain("!groups.json\n");
|
|
170
|
+
expect(content).toContain("!agent-defs/\n");
|
|
171
|
+
expect(content).toContain("!agent-defs/**\n");
|
|
172
|
+
|
|
173
|
+
// Verify it matches the exported constant
|
|
174
|
+
expect(content).toBe(AGENTPLATE_GITIGNORE);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("gitignore is always written when init completes", async () => {
|
|
178
|
+
// Init should write gitignore
|
|
179
|
+
await initCommand({ _spawner: noopSpawner });
|
|
180
|
+
|
|
181
|
+
const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
|
|
182
|
+
const content = await Bun.file(gitignorePath).text();
|
|
183
|
+
|
|
184
|
+
// Verify gitignore was written with correct content
|
|
185
|
+
expect(content).toBe(AGENTPLATE_GITIGNORE);
|
|
186
|
+
|
|
187
|
+
// Verify the file exists
|
|
188
|
+
const exists = await Bun.file(gitignorePath).exists();
|
|
189
|
+
expect(exists).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("--force reinit overwrites stale .agentplate/.gitignore", async () => {
|
|
193
|
+
// First init
|
|
194
|
+
await initCommand({ _spawner: noopSpawner });
|
|
195
|
+
|
|
196
|
+
const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
|
|
197
|
+
|
|
198
|
+
// Tamper with the gitignore file (simulate old deny-list format)
|
|
199
|
+
await Bun.write(gitignorePath, "# old format\nworktrees/\nlogs/\nmail.db\n");
|
|
200
|
+
|
|
201
|
+
// Verify tamper worked
|
|
202
|
+
const tampered = await Bun.file(gitignorePath).text();
|
|
203
|
+
expect(tampered).not.toContain("*\n");
|
|
204
|
+
expect(tampered).not.toContain("!.gitignore\n");
|
|
205
|
+
|
|
206
|
+
// Reinit with --force
|
|
207
|
+
await initCommand({ force: true, _spawner: noopSpawner });
|
|
208
|
+
|
|
209
|
+
// Verify the file was overwritten with the new wildcard+whitelist format
|
|
210
|
+
const restored = await Bun.file(gitignorePath).text();
|
|
211
|
+
expect(restored).toBe(AGENTPLATE_GITIGNORE);
|
|
212
|
+
expect(restored).toContain("*\n");
|
|
213
|
+
expect(restored).toContain("!.gitignore\n");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("subsequent init without --force does not overwrite gitignore", async () => {
|
|
217
|
+
// First init
|
|
218
|
+
await initCommand({ _spawner: noopSpawner });
|
|
219
|
+
|
|
220
|
+
const gitignorePath = join(tempDir, ".agentplate", ".gitignore");
|
|
221
|
+
|
|
222
|
+
// Tamper with the gitignore file
|
|
223
|
+
await Bun.write(gitignorePath, "# custom content\n");
|
|
224
|
+
|
|
225
|
+
// Verify tamper worked
|
|
226
|
+
const tampered = await Bun.file(gitignorePath).text();
|
|
227
|
+
expect(tampered).toBe("# custom content\n");
|
|
228
|
+
|
|
229
|
+
// Second init without --force should return early (not overwrite)
|
|
230
|
+
await initCommand({ _spawner: noopSpawner });
|
|
231
|
+
|
|
232
|
+
// Verify the file was NOT overwritten (early return prevented it)
|
|
233
|
+
const afterSecondInit = await Bun.file(gitignorePath).text();
|
|
234
|
+
expect(afterSecondInit).toBe("# custom content\n");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("initCommand: .agentplate/README.md", () => {
|
|
239
|
+
let tempDir: string;
|
|
240
|
+
let originalCwd: string;
|
|
241
|
+
let originalWrite: typeof process.stdout.write;
|
|
242
|
+
|
|
243
|
+
beforeEach(async () => {
|
|
244
|
+
tempDir = await createTempGitRepo();
|
|
245
|
+
originalCwd = process.cwd();
|
|
246
|
+
process.chdir(tempDir);
|
|
247
|
+
|
|
248
|
+
// Suppress stdout noise from initCommand
|
|
249
|
+
originalWrite = process.stdout.write;
|
|
250
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
afterEach(async () => {
|
|
254
|
+
process.chdir(originalCwd);
|
|
255
|
+
process.stdout.write = originalWrite;
|
|
256
|
+
await cleanupTempDir(tempDir);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("creates .agentplate/README.md with expected content", async () => {
|
|
260
|
+
await initCommand({ _spawner: noopSpawner });
|
|
261
|
+
|
|
262
|
+
const readmePath = join(tempDir, ".agentplate", "README.md");
|
|
263
|
+
const exists = await Bun.file(readmePath).exists();
|
|
264
|
+
expect(exists).toBe(true);
|
|
265
|
+
|
|
266
|
+
const content = await Bun.file(readmePath).text();
|
|
267
|
+
expect(content).toBe(AGENTPLATE_README);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("README.md is whitelisted in gitignore", () => {
|
|
271
|
+
expect(AGENTPLATE_GITIGNORE).toContain("!README.md\n");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("--force reinit overwrites README.md", async () => {
|
|
275
|
+
// First init
|
|
276
|
+
await initCommand({ _spawner: noopSpawner });
|
|
277
|
+
|
|
278
|
+
const readmePath = join(tempDir, ".agentplate", "README.md");
|
|
279
|
+
|
|
280
|
+
// Tamper with the README
|
|
281
|
+
await Bun.write(readmePath, "# tampered\n");
|
|
282
|
+
const tampered = await Bun.file(readmePath).text();
|
|
283
|
+
expect(tampered).toBe("# tampered\n");
|
|
284
|
+
|
|
285
|
+
// Reinit with --force
|
|
286
|
+
await initCommand({ force: true, _spawner: noopSpawner });
|
|
287
|
+
|
|
288
|
+
// Verify restored to canonical content
|
|
289
|
+
const restored = await Bun.file(readmePath).text();
|
|
290
|
+
expect(restored).toBe(AGENTPLATE_README);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("subsequent init without --force does not overwrite README.md", async () => {
|
|
294
|
+
// First init
|
|
295
|
+
await initCommand({ _spawner: noopSpawner });
|
|
296
|
+
|
|
297
|
+
const readmePath = join(tempDir, ".agentplate", "README.md");
|
|
298
|
+
|
|
299
|
+
// Tamper with the README
|
|
300
|
+
await Bun.write(readmePath, "# custom content\n");
|
|
301
|
+
const tampered = await Bun.file(readmePath).text();
|
|
302
|
+
expect(tampered).toBe("# custom content\n");
|
|
303
|
+
|
|
304
|
+
// Second init without --force returns early
|
|
305
|
+
await initCommand({ _spawner: noopSpawner });
|
|
306
|
+
|
|
307
|
+
// Verify tampered content preserved (early return)
|
|
308
|
+
const afterSecondInit = await Bun.file(readmePath).text();
|
|
309
|
+
expect(afterSecondInit).toBe("# custom content\n");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("initCommand: canonical branch detection", () => {
|
|
314
|
+
let tempDir: string;
|
|
315
|
+
let originalCwd: string;
|
|
316
|
+
let originalWrite: typeof process.stdout.write;
|
|
317
|
+
|
|
318
|
+
beforeEach(async () => {
|
|
319
|
+
tempDir = await createTempGitRepo();
|
|
320
|
+
originalCwd = process.cwd();
|
|
321
|
+
// Remove origin remote so detectCanonicalBranch falls through to
|
|
322
|
+
// current-branch check (otherwise remote HEAD resolves to main regardless)
|
|
323
|
+
await runGitInDir(tempDir, ["remote", "remove", "origin"]);
|
|
324
|
+
process.chdir(tempDir);
|
|
325
|
+
|
|
326
|
+
// Suppress stdout noise from initCommand
|
|
327
|
+
originalWrite = process.stdout.write;
|
|
328
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
afterEach(async () => {
|
|
332
|
+
process.chdir(originalCwd);
|
|
333
|
+
process.stdout.write = originalWrite;
|
|
334
|
+
await cleanupTempDir(tempDir);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("non-standard branch names are accepted as canonicalBranch", async () => {
|
|
338
|
+
// Switch to a non-standard branch name
|
|
339
|
+
await runGitInDir(tempDir, ["switch", "-c", "trunk"]);
|
|
340
|
+
|
|
341
|
+
await initCommand({ _spawner: noopSpawner });
|
|
342
|
+
|
|
343
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
344
|
+
const content = await Bun.file(configPath).text();
|
|
345
|
+
expect(content).toContain("canonicalBranch: trunk");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("standard branch names (main) still work as canonicalBranch", async () => {
|
|
349
|
+
// createTempGitRepo defaults to main branch
|
|
350
|
+
await initCommand({ _spawner: noopSpawner });
|
|
351
|
+
|
|
352
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
353
|
+
const content = await Bun.file(configPath).text();
|
|
354
|
+
expect(content).toContain("canonicalBranch: main");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("generated config opts into headless Claude by default (agentplate-caec)", async () => {
|
|
358
|
+
await initCommand({ _spawner: noopSpawner });
|
|
359
|
+
|
|
360
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
361
|
+
const content = await Bun.file(configPath).text();
|
|
362
|
+
expect(content).toContain("claudeHeadlessByDefault: true");
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("initCommand: --yes flag", () => {
|
|
367
|
+
let tempDir: string;
|
|
368
|
+
let originalCwd: string;
|
|
369
|
+
let originalWrite: typeof process.stdout.write;
|
|
370
|
+
|
|
371
|
+
beforeEach(async () => {
|
|
372
|
+
tempDir = await createTempGitRepo();
|
|
373
|
+
originalCwd = process.cwd();
|
|
374
|
+
process.chdir(tempDir);
|
|
375
|
+
|
|
376
|
+
// Suppress stdout noise from initCommand
|
|
377
|
+
originalWrite = process.stdout.write;
|
|
378
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
afterEach(async () => {
|
|
382
|
+
process.chdir(originalCwd);
|
|
383
|
+
process.stdout.write = originalWrite;
|
|
384
|
+
await cleanupTempDir(tempDir);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("--yes reinitializes when .agentplate/ already exists", async () => {
|
|
388
|
+
// First init
|
|
389
|
+
await initCommand({ _spawner: noopSpawner });
|
|
390
|
+
|
|
391
|
+
// Tamper with config to verify reinit happens
|
|
392
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
393
|
+
await Bun.write(configPath, "# tampered\n");
|
|
394
|
+
|
|
395
|
+
// Second init with --yes should reinitialize (not return early)
|
|
396
|
+
await initCommand({ yes: true, _spawner: noopSpawner });
|
|
397
|
+
|
|
398
|
+
// Verify config was regenerated (not the tampered content)
|
|
399
|
+
const content = await Bun.file(configPath).text();
|
|
400
|
+
expect(content).not.toBe("# tampered\n");
|
|
401
|
+
expect(content).toContain("# Agentplate configuration");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("--yes works on fresh project (no .agentplate/ yet)", async () => {
|
|
405
|
+
await initCommand({ yes: true, _spawner: noopSpawner });
|
|
406
|
+
|
|
407
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
408
|
+
const exists = await Bun.file(configPath).exists();
|
|
409
|
+
expect(exists).toBe(true);
|
|
410
|
+
|
|
411
|
+
const content = await Bun.file(configPath).text();
|
|
412
|
+
expect(content).toContain("# Agentplate configuration");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("--yes overwrites agent-defs on reinit", async () => {
|
|
416
|
+
// First init
|
|
417
|
+
await initCommand({ _spawner: noopSpawner });
|
|
418
|
+
|
|
419
|
+
// Tamper with an agent def
|
|
420
|
+
const scoutPath = join(tempDir, ".agentplate", "agent-defs", "scout.md");
|
|
421
|
+
await Bun.write(scoutPath, "TAMPERED CONTENT");
|
|
422
|
+
|
|
423
|
+
// Reinit with --yes should overwrite
|
|
424
|
+
await initCommand({ yes: true, _spawner: noopSpawner });
|
|
425
|
+
|
|
426
|
+
const restored = await Bun.file(scoutPath).text();
|
|
427
|
+
expect(restored).not.toBe("TAMPERED CONTENT");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("initCommand: --name flag", () => {
|
|
432
|
+
let tempDir: string;
|
|
433
|
+
let originalCwd: string;
|
|
434
|
+
let originalWrite: typeof process.stdout.write;
|
|
435
|
+
|
|
436
|
+
beforeEach(async () => {
|
|
437
|
+
tempDir = await createTempGitRepo();
|
|
438
|
+
originalCwd = process.cwd();
|
|
439
|
+
process.chdir(tempDir);
|
|
440
|
+
|
|
441
|
+
// Suppress stdout noise from initCommand
|
|
442
|
+
originalWrite = process.stdout.write;
|
|
443
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
afterEach(async () => {
|
|
447
|
+
process.chdir(originalCwd);
|
|
448
|
+
process.stdout.write = originalWrite;
|
|
449
|
+
await cleanupTempDir(tempDir);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("--name overrides auto-detected project name", async () => {
|
|
453
|
+
await initCommand({ name: "custom-project", _spawner: noopSpawner });
|
|
454
|
+
|
|
455
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
456
|
+
const content = await Bun.file(configPath).text();
|
|
457
|
+
expect(content).toContain("name: custom-project");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("--name combined with --yes works for fully non-interactive init", async () => {
|
|
461
|
+
await initCommand({ yes: true, name: "scripted-project", _spawner: noopSpawner });
|
|
462
|
+
|
|
463
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
464
|
+
const content = await Bun.file(configPath).text();
|
|
465
|
+
expect(content).toContain("name: scripted-project");
|
|
466
|
+
expect(content).toContain("# Agentplate configuration");
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// ---- Ecosystem Bootstrap Tests ----
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Build a Spawner that returns preset responses keyed by "arg0 arg1 ..." prefix.
|
|
474
|
+
* Records all calls for assertion.
|
|
475
|
+
*/
|
|
476
|
+
function createMockSpawner(
|
|
477
|
+
responses: Record<string, { exitCode: number; stdout: string; stderr: string }>,
|
|
478
|
+
): {
|
|
479
|
+
spawner: Spawner;
|
|
480
|
+
calls: string[][];
|
|
481
|
+
} {
|
|
482
|
+
const calls: string[][] = [];
|
|
483
|
+
const spawner: Spawner = async (args) => {
|
|
484
|
+
calls.push(args);
|
|
485
|
+
const key = args.join(" ");
|
|
486
|
+
// Longest prefix match
|
|
487
|
+
let bestMatch = "";
|
|
488
|
+
let bestResponse = { exitCode: 1, stdout: "", stderr: "not found" };
|
|
489
|
+
for (const [pattern, response] of Object.entries(responses)) {
|
|
490
|
+
if (key.startsWith(pattern) && pattern.length > bestMatch.length) {
|
|
491
|
+
bestMatch = pattern;
|
|
492
|
+
bestResponse = response;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return bestResponse;
|
|
496
|
+
};
|
|
497
|
+
return { spawner, calls };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
describe("resolveToolSet", () => {
|
|
501
|
+
test("default (no opts) returns all three tools in order", () => {
|
|
502
|
+
const tools = resolveToolSet({});
|
|
503
|
+
expect(tools.map((t) => t.name)).toEqual(["loam", "sprout", "trellis"]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("--skip-loam removes loam", () => {
|
|
507
|
+
const tools = resolveToolSet({ skipLoam: true });
|
|
508
|
+
expect(tools.map((t) => t.name)).toEqual(["sprout", "trellis"]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("--skip-sprout removes sprout", () => {
|
|
512
|
+
const tools = resolveToolSet({ skipSprout: true });
|
|
513
|
+
expect(tools.map((t) => t.name)).toEqual(["loam", "trellis"]);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("--skip-trellis removes trellis", () => {
|
|
517
|
+
const tools = resolveToolSet({ skipTrellis: true });
|
|
518
|
+
expect(tools.map((t) => t.name)).toEqual(["loam", "sprout"]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("multiple skip flags combine", () => {
|
|
522
|
+
const tools = resolveToolSet({ skipLoam: true, skipSprout: true });
|
|
523
|
+
expect(tools.map((t) => t.name)).toEqual(["trellis"]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("--tools overrides to specific tools", () => {
|
|
527
|
+
const tools = resolveToolSet({ tools: "loam,sprout" });
|
|
528
|
+
expect(tools.map((t) => t.name)).toEqual(["loam", "sprout"]);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("--tools single tool", () => {
|
|
532
|
+
const tools = resolveToolSet({ tools: "trellis" });
|
|
533
|
+
expect(tools.map((t) => t.name)).toEqual(["trellis"]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("--tools with unknown name filters it out", () => {
|
|
537
|
+
const tools = resolveToolSet({ tools: "loam,unknown" });
|
|
538
|
+
expect(tools.map((t) => t.name)).toEqual(["loam"]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("--tools overrides skip flags", () => {
|
|
542
|
+
// --tools takes precedence over --skip-* flags
|
|
543
|
+
const tools = resolveToolSet({ tools: "loam", skipLoam: true });
|
|
544
|
+
expect(tools.map((t) => t.name)).toEqual(["loam"]);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("all skip flags returns empty array", () => {
|
|
548
|
+
const tools = resolveToolSet({ skipLoam: true, skipSprout: true, skipTrellis: true });
|
|
549
|
+
expect(tools).toHaveLength(0);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
describe("initCommand: ecosystem bootstrap", () => {
|
|
554
|
+
let tempDir: string;
|
|
555
|
+
let originalCwd: string;
|
|
556
|
+
let originalWrite: typeof process.stdout.write;
|
|
557
|
+
|
|
558
|
+
beforeEach(async () => {
|
|
559
|
+
tempDir = await createTempGitRepo();
|
|
560
|
+
originalCwd = process.cwd();
|
|
561
|
+
process.chdir(tempDir);
|
|
562
|
+
originalWrite = process.stdout.write;
|
|
563
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
afterEach(async () => {
|
|
567
|
+
process.chdir(originalCwd);
|
|
568
|
+
process.stdout.write = originalWrite;
|
|
569
|
+
await cleanupTempDir(tempDir);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("all tools installed and init succeeds → status initialized", async () => {
|
|
573
|
+
const { spawner, calls } = createMockSpawner({
|
|
574
|
+
"lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
|
|
575
|
+
"lm init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
576
|
+
"lm onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
577
|
+
"sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
|
|
578
|
+
"sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
579
|
+
"sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
580
|
+
"tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
|
|
581
|
+
"tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
582
|
+
"tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
await initCommand({ _spawner: spawner });
|
|
586
|
+
|
|
587
|
+
// All three init commands were called
|
|
588
|
+
expect(calls).toContainEqual(["lm", "init"]);
|
|
589
|
+
expect(calls).toContainEqual(["sr", "init"]);
|
|
590
|
+
expect(calls).toContainEqual(["tl", "init"]);
|
|
591
|
+
|
|
592
|
+
// All three onboard commands were called
|
|
593
|
+
expect(calls).toContainEqual(["lm", "onboard"]);
|
|
594
|
+
expect(calls).toContainEqual(["sr", "onboard"]);
|
|
595
|
+
expect(calls).toContainEqual(["tl", "onboard"]);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("tool not installed → init and onboard not called", async () => {
|
|
599
|
+
const { spawner, calls } = createMockSpawner({
|
|
600
|
+
"lm --version": { exitCode: 1, stdout: "", stderr: "command not found" },
|
|
601
|
+
"sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
|
|
602
|
+
"sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
603
|
+
"sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
604
|
+
"tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
|
|
605
|
+
"tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
606
|
+
"tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
await initCommand({ _spawner: spawner });
|
|
610
|
+
|
|
611
|
+
// loam init should NOT have been called
|
|
612
|
+
expect(calls).not.toContainEqual(["lm", "init"]);
|
|
613
|
+
// sprout and trellis should still be called
|
|
614
|
+
expect(calls).toContainEqual(["sr", "init"]);
|
|
615
|
+
expect(calls).toContainEqual(["tl", "init"]);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("tool init non-zero + dir exists → already_initialized", async () => {
|
|
619
|
+
// Create .loam/ directory to simulate existing loam init
|
|
620
|
+
const { mkdir } = await import("node:fs/promises");
|
|
621
|
+
await mkdir(join(tempDir, ".loam"), { recursive: true });
|
|
622
|
+
|
|
623
|
+
const { spawner } = createMockSpawner({
|
|
624
|
+
"lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
|
|
625
|
+
"lm init": { exitCode: 1, stdout: "", stderr: "already initialized" },
|
|
626
|
+
"lm onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
627
|
+
"sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
|
|
628
|
+
"sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
629
|
+
"sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
630
|
+
"tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
|
|
631
|
+
"tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
632
|
+
"tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Should not throw — already_initialized is not an error
|
|
636
|
+
await initCommand({ _spawner: spawner });
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("--skip-onboard skips onboard calls", async () => {
|
|
640
|
+
const { spawner, calls } = createMockSpawner({
|
|
641
|
+
"lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
|
|
642
|
+
"lm init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
643
|
+
"sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
|
|
644
|
+
"sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
645
|
+
"tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
|
|
646
|
+
"tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await initCommand({ skipOnboard: true, _spawner: spawner });
|
|
650
|
+
|
|
651
|
+
expect(calls).not.toContainEqual(["lm", "onboard"]);
|
|
652
|
+
expect(calls).not.toContainEqual(["sr", "onboard"]);
|
|
653
|
+
expect(calls).not.toContainEqual(["tl", "onboard"]);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("--skip-loam skips loam entirely", async () => {
|
|
657
|
+
const { spawner, calls } = createMockSpawner({
|
|
658
|
+
"sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
|
|
659
|
+
"sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
660
|
+
"sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
661
|
+
"tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
|
|
662
|
+
"tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
663
|
+
"tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
await initCommand({ skipLoam: true, _spawner: spawner });
|
|
667
|
+
|
|
668
|
+
expect(calls.filter((c) => c[0] === "lm")).toHaveLength(0);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("--json outputs JSON envelope with tools and onboard status", async () => {
|
|
672
|
+
const { spawner } = createMockSpawner({
|
|
673
|
+
"lm --version": { exitCode: 0, stdout: "0.6.3", stderr: "" },
|
|
674
|
+
"lm init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
675
|
+
"lm onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
676
|
+
"sr --version": { exitCode: 0, stdout: "0.2.4", stderr: "" },
|
|
677
|
+
"sr init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
678
|
+
"sr onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
679
|
+
"tl --version": { exitCode: 0, stdout: "0.2.0", stderr: "" },
|
|
680
|
+
"tl init": { exitCode: 0, stdout: "initialized", stderr: "" },
|
|
681
|
+
"tl onboard": { exitCode: 0, stdout: "appended", stderr: "" },
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
let capturedOutput = "";
|
|
685
|
+
const restoreWrite = process.stdout.write;
|
|
686
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
687
|
+
capturedOutput += String(chunk);
|
|
688
|
+
return true;
|
|
689
|
+
}) as typeof process.stdout.write;
|
|
690
|
+
|
|
691
|
+
await initCommand({ json: true, _spawner: spawner });
|
|
692
|
+
|
|
693
|
+
process.stdout.write = restoreWrite;
|
|
694
|
+
|
|
695
|
+
// Find the JSON line (last line with JSON content)
|
|
696
|
+
const jsonLine = capturedOutput.split("\n").find((line) => line.startsWith('{"success":'));
|
|
697
|
+
|
|
698
|
+
expect(jsonLine).toBeDefined();
|
|
699
|
+
const parsed = JSON.parse(jsonLine ?? "{}") as Record<string, unknown>;
|
|
700
|
+
expect(parsed.success).toBe(true);
|
|
701
|
+
expect(parsed.command).toBe("init");
|
|
702
|
+
expect(parsed.tools).toBeDefined();
|
|
703
|
+
expect(parsed.onboard).toBeDefined();
|
|
704
|
+
expect(typeof parsed.gitattributes).toBe("boolean");
|
|
705
|
+
|
|
706
|
+
const tools = parsed.tools as Record<string, { status: string }>;
|
|
707
|
+
expect(tools.agentplate?.status).toBe("initialized");
|
|
708
|
+
expect(tools.loam?.status).toBe("initialized");
|
|
709
|
+
expect(tools.sprout?.status).toBe("initialized");
|
|
710
|
+
expect(tools.trellis?.status).toBe("initialized");
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
describe("initCommand: scaffold commit", () => {
|
|
715
|
+
let tempDir: string;
|
|
716
|
+
let originalCwd: string;
|
|
717
|
+
let originalWrite: typeof process.stdout.write;
|
|
718
|
+
|
|
719
|
+
beforeEach(async () => {
|
|
720
|
+
tempDir = await createTempGitRepo();
|
|
721
|
+
originalCwd = process.cwd();
|
|
722
|
+
process.chdir(tempDir);
|
|
723
|
+
originalWrite = process.stdout.write;
|
|
724
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
afterEach(async () => {
|
|
728
|
+
process.chdir(originalCwd);
|
|
729
|
+
process.stdout.write = originalWrite;
|
|
730
|
+
await cleanupTempDir(tempDir);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("git commit is called with scaffold message when git add succeeds and changes are staged", async () => {
|
|
734
|
+
const calls: string[][] = [];
|
|
735
|
+
const spawner: import("./init.ts").Spawner = async (args) => {
|
|
736
|
+
calls.push(args);
|
|
737
|
+
const key = args.join(" ");
|
|
738
|
+
// Sibling tool calls: all "not installed"
|
|
739
|
+
if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
740
|
+
// git add: success
|
|
741
|
+
if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
742
|
+
// git diff --cached --quiet: exit 1 means changes are staged
|
|
743
|
+
if (key.startsWith("git diff --cached --quiet"))
|
|
744
|
+
return { exitCode: 1, stdout: "", stderr: "" };
|
|
745
|
+
// git commit: success
|
|
746
|
+
if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
747
|
+
return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
await initCommand({ _spawner: spawner });
|
|
751
|
+
|
|
752
|
+
expect(calls).toContainEqual([
|
|
753
|
+
"git",
|
|
754
|
+
"commit",
|
|
755
|
+
"-m",
|
|
756
|
+
"chore: initialize agentplate and ecosystem tools",
|
|
757
|
+
]);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("git commit is NOT called when git diff reports nothing staged (exit 0)", async () => {
|
|
761
|
+
const calls: string[][] = [];
|
|
762
|
+
const spawner: import("./init.ts").Spawner = async (args) => {
|
|
763
|
+
calls.push(args);
|
|
764
|
+
const key = args.join(" ");
|
|
765
|
+
if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
766
|
+
if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
767
|
+
// exit 0 = nothing staged
|
|
768
|
+
if (key.startsWith("git diff --cached --quiet"))
|
|
769
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
770
|
+
if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
771
|
+
return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
await initCommand({ _spawner: spawner });
|
|
775
|
+
|
|
776
|
+
const commitCalls = calls.filter((c) => c[0] === "git" && c[1] === "commit");
|
|
777
|
+
expect(commitCalls).toHaveLength(0);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("git commit failure does not throw — init still succeeds", async () => {
|
|
781
|
+
const spawner: import("./init.ts").Spawner = async (args) => {
|
|
782
|
+
const key = args.join(" ");
|
|
783
|
+
if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
784
|
+
if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
785
|
+
if (key.startsWith("git diff --cached --quiet"))
|
|
786
|
+
return { exitCode: 1, stdout: "", stderr: "" };
|
|
787
|
+
// commit fails
|
|
788
|
+
if (key.startsWith("git commit"))
|
|
789
|
+
return { exitCode: 1, stdout: "", stderr: "nothing to commit" };
|
|
790
|
+
return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// Should not throw
|
|
794
|
+
await expect(initCommand({ _spawner: spawner })).resolves.toBeUndefined();
|
|
795
|
+
|
|
796
|
+
// .agentplate files should still be created
|
|
797
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
798
|
+
const exists = await Bun.file(configPath).exists();
|
|
799
|
+
expect(exists).toBe(true);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("git add failure skips commit without throwing", async () => {
|
|
803
|
+
const calls: string[][] = [];
|
|
804
|
+
const spawner: import("./init.ts").Spawner = async (args) => {
|
|
805
|
+
calls.push(args);
|
|
806
|
+
const key = args.join(" ");
|
|
807
|
+
if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
808
|
+
// git add fails
|
|
809
|
+
if (key.startsWith("git add")) return { exitCode: 1, stdout: "", stderr: "git add failed" };
|
|
810
|
+
if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
811
|
+
return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
await expect(initCommand({ _spawner: spawner })).resolves.toBeUndefined();
|
|
815
|
+
|
|
816
|
+
// commit should NOT have been called since add failed
|
|
817
|
+
const commitCalls = calls.filter((c) => c[0] === "git" && c[1] === "commit");
|
|
818
|
+
expect(commitCalls).toHaveLength(0);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("--json output includes scaffoldCommitted boolean", async () => {
|
|
822
|
+
const spawner: import("./init.ts").Spawner = async (args) => {
|
|
823
|
+
const key = args.join(" ");
|
|
824
|
+
if (key.endsWith("--version")) return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
825
|
+
if (key.startsWith("git add")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
826
|
+
if (key.startsWith("git diff --cached --quiet"))
|
|
827
|
+
return { exitCode: 1, stdout: "", stderr: "" };
|
|
828
|
+
if (key.startsWith("git commit")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
829
|
+
return { exitCode: 1, stdout: "", stderr: "not found" };
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
let capturedOutput = "";
|
|
833
|
+
const restoreWrite = process.stdout.write;
|
|
834
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
835
|
+
capturedOutput += String(chunk);
|
|
836
|
+
return true;
|
|
837
|
+
}) as typeof process.stdout.write;
|
|
838
|
+
|
|
839
|
+
await initCommand({ json: true, _spawner: spawner });
|
|
840
|
+
|
|
841
|
+
process.stdout.write = restoreWrite;
|
|
842
|
+
|
|
843
|
+
const jsonLine = capturedOutput.split("\n").find((line) => line.startsWith('{"success":'));
|
|
844
|
+
expect(jsonLine).toBeDefined();
|
|
845
|
+
const parsed = JSON.parse(jsonLine ?? "{}") as Record<string, unknown>;
|
|
846
|
+
expect(typeof parsed.scaffoldCommitted).toBe("boolean");
|
|
847
|
+
expect(parsed.scaffoldCommitted).toBe(true);
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
describe("initCommand: spawner error resilience", () => {
|
|
852
|
+
let tempDir: string;
|
|
853
|
+
let originalCwd: string;
|
|
854
|
+
let originalWrite: typeof process.stdout.write;
|
|
855
|
+
|
|
856
|
+
beforeEach(async () => {
|
|
857
|
+
tempDir = await createTempGitRepo();
|
|
858
|
+
originalCwd = process.cwd();
|
|
859
|
+
process.chdir(tempDir);
|
|
860
|
+
originalWrite = process.stdout.write;
|
|
861
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
afterEach(async () => {
|
|
865
|
+
process.chdir(originalCwd);
|
|
866
|
+
process.stdout.write = originalWrite;
|
|
867
|
+
await cleanupTempDir(tempDir);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("spawner that throws ENOENT does not crash init — degrades gracefully", async () => {
|
|
871
|
+
const throwingSpawner: Spawner = async (args) => {
|
|
872
|
+
const key = args.join(" ");
|
|
873
|
+
// Allow git operations through (git add, git diff, git commit)
|
|
874
|
+
if (key.startsWith("git")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
875
|
+
// Simulate ecosystem tool binary not found (ENOENT)
|
|
876
|
+
throw new Error(`spawn ENOENT: ${args[0]}: not found`);
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// Should not throw — graceful degradation
|
|
880
|
+
await expect(initCommand({ _spawner: throwingSpawner })).resolves.toBeUndefined();
|
|
881
|
+
|
|
882
|
+
// Core .agentplate files should still be created
|
|
883
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
884
|
+
expect(await Bun.file(configPath).exists()).toBe(true);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("throwing spawner causes all ecosystem tools to be skipped", async () => {
|
|
888
|
+
const calls: string[][] = [];
|
|
889
|
+
const throwingSpawner: Spawner = async (args) => {
|
|
890
|
+
calls.push(args);
|
|
891
|
+
const key = args.join(" ");
|
|
892
|
+
if (key.startsWith("git")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
893
|
+
throw new Error("spawn ENOENT");
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
await initCommand({ _spawner: throwingSpawner });
|
|
897
|
+
|
|
898
|
+
// init and onboard should NOT be called when --version throws
|
|
899
|
+
expect(calls).not.toContainEqual(["lm", "init"]);
|
|
900
|
+
expect(calls).not.toContainEqual(["sr", "init"]);
|
|
901
|
+
expect(calls).not.toContainEqual(["tl", "init"]);
|
|
902
|
+
expect(calls).not.toContainEqual(["lm", "onboard"]);
|
|
903
|
+
expect(calls).not.toContainEqual(["sr", "onboard"]);
|
|
904
|
+
expect(calls).not.toContainEqual(["tl", "onboard"]);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("spawner that throws only on init (not --version) still skips gracefully", async () => {
|
|
908
|
+
// --version succeeds (tool appears installed), but init itself throws
|
|
909
|
+
const throwingInitSpawner: Spawner = async (args) => {
|
|
910
|
+
const key = args.join(" ");
|
|
911
|
+
if (key.startsWith("git")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
912
|
+
if (key.endsWith("--version")) return { exitCode: 0, stdout: "1.0.0", stderr: "" };
|
|
913
|
+
if (key.endsWith("onboard")) return { exitCode: 0, stdout: "", stderr: "" };
|
|
914
|
+
// init itself throws
|
|
915
|
+
throw new Error("spawn ENOENT on init");
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
await expect(initCommand({ _spawner: throwingInitSpawner })).resolves.toBeUndefined();
|
|
919
|
+
|
|
920
|
+
const configPath = join(tempDir, ".agentplate", "config.yaml");
|
|
921
|
+
expect(await Bun.file(configPath).exists()).toBe(true);
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
describe("initCommand: .gitattributes setup", () => {
|
|
926
|
+
let tempDir: string;
|
|
927
|
+
let originalCwd: string;
|
|
928
|
+
let originalWrite: typeof process.stdout.write;
|
|
929
|
+
|
|
930
|
+
beforeEach(async () => {
|
|
931
|
+
tempDir = await createTempGitRepo();
|
|
932
|
+
originalCwd = process.cwd();
|
|
933
|
+
process.chdir(tempDir);
|
|
934
|
+
originalWrite = process.stdout.write;
|
|
935
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
afterEach(async () => {
|
|
939
|
+
process.chdir(originalCwd);
|
|
940
|
+
process.stdout.write = originalWrite;
|
|
941
|
+
await cleanupTempDir(tempDir);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test("creates .gitattributes with merge=union entries", async () => {
|
|
945
|
+
// Use a spawner that skips all ecosystem tools so only gitattributes step runs
|
|
946
|
+
const { spawner } = createMockSpawner({});
|
|
947
|
+
await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
|
|
948
|
+
|
|
949
|
+
const gitattrsPath = join(tempDir, ".gitattributes");
|
|
950
|
+
const exists = await Bun.file(gitattrsPath).exists();
|
|
951
|
+
expect(exists).toBe(true);
|
|
952
|
+
|
|
953
|
+
const content = await Bun.file(gitattrsPath).text();
|
|
954
|
+
expect(content).toContain(".loam/expertise/*.jsonl merge=union");
|
|
955
|
+
expect(content).toContain(".sprout/issues.jsonl merge=union");
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test("does not duplicate entries on reinit with --force", async () => {
|
|
959
|
+
const { spawner } = createMockSpawner({});
|
|
960
|
+
|
|
961
|
+
// First init
|
|
962
|
+
await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
|
|
963
|
+
|
|
964
|
+
// Second init with --force
|
|
965
|
+
await initCommand({
|
|
966
|
+
force: true,
|
|
967
|
+
skipLoam: true,
|
|
968
|
+
skipSprout: true,
|
|
969
|
+
skipTrellis: true,
|
|
970
|
+
_spawner: spawner,
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
const gitattrsPath = join(tempDir, ".gitattributes");
|
|
974
|
+
const content = await Bun.file(gitattrsPath).text();
|
|
975
|
+
|
|
976
|
+
// Count occurrences — should be exactly one each
|
|
977
|
+
const loamCount = (content.match(/\.loam\/expertise\/\*\.jsonl merge=union/g) ?? []).length;
|
|
978
|
+
const sproutCount = (content.match(/\.sprout\/issues\.jsonl merge=union/g) ?? []).length;
|
|
979
|
+
expect(loamCount).toBe(1);
|
|
980
|
+
expect(sproutCount).toBe(1);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
test("preserves existing .gitattributes content", async () => {
|
|
984
|
+
// Pre-create .gitattributes with existing content
|
|
985
|
+
const existingContent = "*.lock binary\n*.png binary\n";
|
|
986
|
+
await Bun.write(join(tempDir, ".gitattributes"), existingContent);
|
|
987
|
+
|
|
988
|
+
const { spawner } = createMockSpawner({});
|
|
989
|
+
await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
|
|
990
|
+
|
|
991
|
+
const content = await Bun.file(join(tempDir, ".gitattributes")).text();
|
|
992
|
+
expect(content).toContain("*.lock binary");
|
|
993
|
+
expect(content).toContain("*.png binary");
|
|
994
|
+
expect(content).toContain(".loam/expertise/*.jsonl merge=union");
|
|
995
|
+
expect(content).toContain(".sprout/issues.jsonl merge=union");
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test("no-op when entries already present", async () => {
|
|
999
|
+
// Pre-create .gitattributes with the entries already
|
|
1000
|
+
const existingContent =
|
|
1001
|
+
".loam/expertise/*.jsonl merge=union\n.sprout/issues.jsonl merge=union\n";
|
|
1002
|
+
await Bun.write(join(tempDir, ".gitattributes"), existingContent);
|
|
1003
|
+
|
|
1004
|
+
const { spawner } = createMockSpawner({});
|
|
1005
|
+
await initCommand({ skipLoam: true, skipSprout: true, skipTrellis: true, _spawner: spawner });
|
|
1006
|
+
|
|
1007
|
+
const content = await Bun.file(join(tempDir, ".gitattributes")).text();
|
|
1008
|
+
// Content should be unchanged
|
|
1009
|
+
expect(content).toBe(existingContent);
|
|
1010
|
+
});
|
|
1011
|
+
});
|