@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,10 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveAgentplateBin } from "./bin.ts";
|
|
3
|
+
|
|
4
|
+
describe("resolveAgentplateBin", () => {
|
|
5
|
+
test("returns a non-empty string", async () => {
|
|
6
|
+
const bin = await resolveAgentplateBin();
|
|
7
|
+
expect(typeof bin).toBe("string");
|
|
8
|
+
expect(bin.length).toBeGreaterThan(0);
|
|
9
|
+
});
|
|
10
|
+
});
|
package/src/utils/bin.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary resolution utilities.
|
|
3
|
+
*/
|
|
4
|
+
import { AgentplateError } from "../errors.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the path to the agentplate binary for re-launching.
|
|
8
|
+
* Uses `which ap` first, then falls back to process.argv.
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveAgentplateBin(): Promise<string> {
|
|
11
|
+
try {
|
|
12
|
+
const proc = Bun.spawn(["which", "ap"], {
|
|
13
|
+
stdout: "pipe",
|
|
14
|
+
stderr: "pipe",
|
|
15
|
+
});
|
|
16
|
+
const exitCode = await proc.exited;
|
|
17
|
+
if (exitCode === 0) {
|
|
18
|
+
const binPath = (await new Response(proc.stdout).text()).trim();
|
|
19
|
+
if (binPath.length > 0) {
|
|
20
|
+
return binPath;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// which not available or agentplate not on PATH
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fallback: use the script that's currently running (process.argv[1])
|
|
28
|
+
const scriptPath = process.argv[1];
|
|
29
|
+
if (scriptPath) {
|
|
30
|
+
return scriptPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new AgentplateError(
|
|
34
|
+
"Cannot resolve agentplate binary path for background launch",
|
|
35
|
+
"WATCH_ERROR",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { browserOpenCommand, openBrowser } from "./browser.ts";
|
|
3
|
+
|
|
4
|
+
describe("browserOpenCommand", () => {
|
|
5
|
+
test("uses `open` on darwin", () => {
|
|
6
|
+
expect(browserOpenCommand("http://localhost:7321", "darwin")).toEqual([
|
|
7
|
+
"open",
|
|
8
|
+
"http://localhost:7321",
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("uses `cmd /c start` with empty title on win32", () => {
|
|
13
|
+
expect(browserOpenCommand("http://localhost:7321", "win32")).toEqual([
|
|
14
|
+
"cmd",
|
|
15
|
+
"/c",
|
|
16
|
+
"start",
|
|
17
|
+
"",
|
|
18
|
+
"http://localhost:7321",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("uses `xdg-open` on linux/other", () => {
|
|
23
|
+
expect(browserOpenCommand("http://localhost:7321", "linux")).toEqual([
|
|
24
|
+
"xdg-open",
|
|
25
|
+
"http://localhost:7321",
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("openBrowser", () => {
|
|
31
|
+
test("spawns the resolved command and returns true", () => {
|
|
32
|
+
let received: string[] | null = null;
|
|
33
|
+
const fakeSpawn = ((cmd: string[]) => {
|
|
34
|
+
received = cmd;
|
|
35
|
+
return { unref() {} } as unknown as ReturnType<typeof Bun.spawn>;
|
|
36
|
+
}) as typeof Bun.spawn;
|
|
37
|
+
|
|
38
|
+
const ok = openBrowser("http://localhost:7321", fakeSpawn, "darwin");
|
|
39
|
+
expect(ok).toBe(true);
|
|
40
|
+
expect(received as string[] | null).toEqual(["open", "http://localhost:7321"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("returns false (never throws) when spawn fails", () => {
|
|
44
|
+
const throwingSpawn = (() => {
|
|
45
|
+
throw new Error("no GUI");
|
|
46
|
+
}) as typeof Bun.spawn;
|
|
47
|
+
expect(openBrowser("http://localhost:7321", throwingSpawn, "linux")).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort cross-platform browser launcher for `ap dashboard ui`.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the platform's "open a URL in the default browser" command and
|
|
5
|
+
* spawns it detached. Never throws — a failed launch (headless CI, no GUI,
|
|
6
|
+
* missing `xdg-open`) just means the operator opens the printed URL manually.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type SpawnFn = typeof Bun.spawn;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the argv used to open `url` in the default browser on `platform`.
|
|
13
|
+
* - darwin: `open <url>`
|
|
14
|
+
* - win32: `cmd /c start "" <url>` (empty title arg so a quoted URL isn't
|
|
15
|
+
* mistaken for the window title)
|
|
16
|
+
* - else: `xdg-open <url>` (Linux/BSD desktops)
|
|
17
|
+
*/
|
|
18
|
+
export function browserOpenCommand(
|
|
19
|
+
url: string,
|
|
20
|
+
platform: NodeJS.Platform = process.platform,
|
|
21
|
+
): string[] {
|
|
22
|
+
if (platform === "darwin") return ["open", url];
|
|
23
|
+
if (platform === "win32") return ["cmd", "/c", "start", "", url];
|
|
24
|
+
return ["xdg-open", url];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Open `url` in the default browser. Best-effort: returns `true` when the
|
|
29
|
+
* launcher was spawned, `false` on any failure. `spawn`/`platform` are
|
|
30
|
+
* injectable for tests.
|
|
31
|
+
*/
|
|
32
|
+
export function openBrowser(
|
|
33
|
+
url: string,
|
|
34
|
+
spawn: SpawnFn = Bun.spawn,
|
|
35
|
+
platform: NodeJS.Platform = process.platform,
|
|
36
|
+
): boolean {
|
|
37
|
+
try {
|
|
38
|
+
const proc = spawn(browserOpenCommand(url, platform), {
|
|
39
|
+
stdout: "ignore",
|
|
40
|
+
stderr: "ignore",
|
|
41
|
+
stdin: "ignore",
|
|
42
|
+
});
|
|
43
|
+
proc.unref();
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
7
|
+
import { clearDirectory, deleteFile, resetJsonFile, wipeSqliteDb } from "./fs.ts";
|
|
8
|
+
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tempDir = await mkdtemp(join(tmpdir(), "ap-fs-test-"));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await cleanupTempDir(tempDir);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("wipeSqliteDb", () => {
|
|
20
|
+
test("deletes main db and WAL/SHM companion files", async () => {
|
|
21
|
+
const dbPath = join(tempDir, "test-wipe.db");
|
|
22
|
+
const { Database } = await import("bun:sqlite");
|
|
23
|
+
const db = new Database(dbPath);
|
|
24
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
25
|
+
db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY)");
|
|
26
|
+
db.exec("INSERT INTO t VALUES (1)");
|
|
27
|
+
db.close();
|
|
28
|
+
|
|
29
|
+
expect(existsSync(dbPath)).toBe(true);
|
|
30
|
+
|
|
31
|
+
const result = await wipeSqliteDb(dbPath);
|
|
32
|
+
expect(result).toBe(true);
|
|
33
|
+
|
|
34
|
+
expect(existsSync(dbPath)).toBe(false);
|
|
35
|
+
expect(existsSync(`${dbPath}-wal`)).toBe(false);
|
|
36
|
+
expect(existsSync(`${dbPath}-shm`)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("returns false when db file does not exist", async () => {
|
|
40
|
+
const dbPath = join(tempDir, "nonexistent.db");
|
|
41
|
+
const result = await wipeSqliteDb(dbPath);
|
|
42
|
+
expect(result).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("resetJsonFile", () => {
|
|
47
|
+
test("resets existing JSON file to empty array", async () => {
|
|
48
|
+
const filePath = join(tempDir, "test-reset.json");
|
|
49
|
+
await Bun.write(filePath, '[{"id":"1"},{"id":"2"}]');
|
|
50
|
+
|
|
51
|
+
const result = await resetJsonFile(filePath);
|
|
52
|
+
expect(result).toBe(true);
|
|
53
|
+
|
|
54
|
+
const content = await Bun.file(filePath).text();
|
|
55
|
+
expect(content).toBe("[]\n");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("returns false for nonexistent file", async () => {
|
|
59
|
+
const filePath = join(tempDir, "nonexistent.json");
|
|
60
|
+
const result = await resetJsonFile(filePath);
|
|
61
|
+
expect(result).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("clearDirectory", () => {
|
|
66
|
+
test("clears files from a directory", async () => {
|
|
67
|
+
const dirPath = join(tempDir, "clear-test");
|
|
68
|
+
await mkdir(dirPath, { recursive: true });
|
|
69
|
+
await writeFile(join(dirPath, "file1.txt"), "hello");
|
|
70
|
+
await writeFile(join(dirPath, "file2.txt"), "world");
|
|
71
|
+
|
|
72
|
+
const result = await clearDirectory(dirPath);
|
|
73
|
+
expect(result).toBe(true);
|
|
74
|
+
|
|
75
|
+
const entries = await readdir(dirPath);
|
|
76
|
+
expect(entries).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("returns false for empty directory", async () => {
|
|
80
|
+
const dirPath = join(tempDir, "empty-dir");
|
|
81
|
+
await mkdir(dirPath, { recursive: true });
|
|
82
|
+
|
|
83
|
+
const result = await clearDirectory(dirPath);
|
|
84
|
+
expect(result).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns false for nonexistent directory", async () => {
|
|
88
|
+
const result = await clearDirectory(join(tempDir, "no-such-dir"));
|
|
89
|
+
expect(result).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("recursively removes subdirectories", async () => {
|
|
93
|
+
const dirPath = join(tempDir, "nested-clear");
|
|
94
|
+
await mkdir(join(dirPath, "sub", "deep"), { recursive: true });
|
|
95
|
+
await writeFile(join(dirPath, "sub", "deep", "file.txt"), "data");
|
|
96
|
+
|
|
97
|
+
const result = await clearDirectory(dirPath);
|
|
98
|
+
expect(result).toBe(true);
|
|
99
|
+
|
|
100
|
+
const entries = await readdir(dirPath);
|
|
101
|
+
expect(entries).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("deleteFile", () => {
|
|
106
|
+
test("deletes an existing file", async () => {
|
|
107
|
+
const filePath = join(tempDir, "to-delete.txt");
|
|
108
|
+
await writeFile(filePath, "delete me");
|
|
109
|
+
|
|
110
|
+
const result = await deleteFile(filePath);
|
|
111
|
+
expect(result).toBe(true);
|
|
112
|
+
expect(existsSync(filePath)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("returns false for nonexistent file", async () => {
|
|
116
|
+
const result = await deleteFile(join(tempDir, "no-such-file.txt"));
|
|
117
|
+
expect(result).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem utility functions for cleanup and state management.
|
|
3
|
+
*/
|
|
4
|
+
import { readdir, rm, unlink } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Delete a SQLite database file and its WAL/SHM companions.
|
|
9
|
+
*/
|
|
10
|
+
export async function wipeSqliteDb(dbPath: string): Promise<boolean> {
|
|
11
|
+
const extensions = ["", "-wal", "-shm"];
|
|
12
|
+
let wiped = false;
|
|
13
|
+
for (const ext of extensions) {
|
|
14
|
+
try {
|
|
15
|
+
await unlink(`${dbPath}${ext}`);
|
|
16
|
+
if (ext === "") wiped = true;
|
|
17
|
+
} catch {
|
|
18
|
+
// File may not exist
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return wiped;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reset a JSON file to an empty array.
|
|
26
|
+
*/
|
|
27
|
+
export async function resetJsonFile(path: string): Promise<boolean> {
|
|
28
|
+
const file = Bun.file(path);
|
|
29
|
+
if (await file.exists()) {
|
|
30
|
+
await Bun.write(path, "[]\n");
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clear all entries inside a directory but keep the directory itself.
|
|
38
|
+
*/
|
|
39
|
+
export async function clearDirectory(dirPath: string): Promise<boolean> {
|
|
40
|
+
try {
|
|
41
|
+
const entries = await readdir(dirPath);
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
await rm(join(dirPath, entry), { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
return entries.length > 0;
|
|
46
|
+
} catch {
|
|
47
|
+
// Directory may not exist
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Delete a single file if it exists.
|
|
54
|
+
*/
|
|
55
|
+
export async function deleteFile(path: string): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
await unlink(path);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
6
|
+
import { acquirePidLock, readPidFile, removePidFile, writePidFile } from "./pid.ts";
|
|
7
|
+
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tempDir = await mkdtemp(join(tmpdir(), "ap-pid-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await cleanupTempDir(tempDir);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("readPidFile", () => {
|
|
19
|
+
test("returns pid from valid file", async () => {
|
|
20
|
+
const pidPath = join(tempDir, "test.pid");
|
|
21
|
+
await Bun.write(pidPath, "12345\n");
|
|
22
|
+
const pid = await readPidFile(pidPath);
|
|
23
|
+
expect(pid).toBe(12345);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns null for nonexistent file", async () => {
|
|
27
|
+
const pid = await readPidFile(join(tempDir, "missing.pid"));
|
|
28
|
+
expect(pid).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns null for non-numeric content", async () => {
|
|
32
|
+
const pidPath = join(tempDir, "bad.pid");
|
|
33
|
+
await Bun.write(pidPath, "not-a-number\n");
|
|
34
|
+
const pid = await readPidFile(pidPath);
|
|
35
|
+
expect(pid).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("returns null for negative pid", async () => {
|
|
39
|
+
const pidPath = join(tempDir, "neg.pid");
|
|
40
|
+
await Bun.write(pidPath, "-1\n");
|
|
41
|
+
const pid = await readPidFile(pidPath);
|
|
42
|
+
expect(pid).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("writePidFile", () => {
|
|
47
|
+
test("roundtrip write then read", async () => {
|
|
48
|
+
const pidPath = join(tempDir, "roundtrip.pid");
|
|
49
|
+
await writePidFile(pidPath, 42);
|
|
50
|
+
const pid = await readPidFile(pidPath);
|
|
51
|
+
expect(pid).toBe(42);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("removePidFile", () => {
|
|
56
|
+
test("removes existing file", async () => {
|
|
57
|
+
const pidPath = join(tempDir, "remove.pid");
|
|
58
|
+
await Bun.write(pidPath, "99\n");
|
|
59
|
+
expect(await Bun.file(pidPath).exists()).toBe(true);
|
|
60
|
+
await removePidFile(pidPath);
|
|
61
|
+
expect(await Bun.file(pidPath).exists()).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("does not throw for nonexistent file", async () => {
|
|
65
|
+
await removePidFile(join(tempDir, "nope.pid"));
|
|
66
|
+
// No throw = pass
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("acquirePidLock", () => {
|
|
71
|
+
const alwaysAlive = (_pid: number) => true;
|
|
72
|
+
const alwaysDead = (_pid: number) => false;
|
|
73
|
+
|
|
74
|
+
test("acquires when no lock file exists", async () => {
|
|
75
|
+
const pidPath = join(tempDir, "lock.pid");
|
|
76
|
+
const result = await acquirePidLock(pidPath, 1234, alwaysAlive);
|
|
77
|
+
expect(result.acquired).toBe(true);
|
|
78
|
+
expect(await readPidFile(pidPath)).toBe(1234);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("creates parent directory if missing", async () => {
|
|
82
|
+
const pidPath = join(tempDir, "nested", "deeper", "lock.pid");
|
|
83
|
+
const result = await acquirePidLock(pidPath, 555, alwaysAlive);
|
|
84
|
+
expect(result.acquired).toBe(true);
|
|
85
|
+
expect(await readPidFile(pidPath)).toBe(555);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("refuses when a live foreign PID owns the lock", async () => {
|
|
89
|
+
const pidPath = join(tempDir, "lock.pid");
|
|
90
|
+
await Bun.write(pidPath, "9999\n");
|
|
91
|
+
const result = await acquirePidLock(pidPath, 1234, alwaysAlive);
|
|
92
|
+
expect(result.acquired).toBe(false);
|
|
93
|
+
if (!result.acquired) {
|
|
94
|
+
expect(result.existingPid).toBe(9999);
|
|
95
|
+
}
|
|
96
|
+
// File untouched.
|
|
97
|
+
expect(await readPidFile(pidPath)).toBe(9999);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("idempotent when file already contains caller's own PID", async () => {
|
|
101
|
+
const pidPath = join(tempDir, "lock.pid");
|
|
102
|
+
await Bun.write(pidPath, "1234\n");
|
|
103
|
+
// alwaysAlive would say 1234 is alive, but acquirePidLock should detect
|
|
104
|
+
// own-PID first and accept.
|
|
105
|
+
const result = await acquirePidLock(pidPath, 1234, alwaysAlive);
|
|
106
|
+
expect(result.acquired).toBe(true);
|
|
107
|
+
expect(await readPidFile(pidPath)).toBe(1234);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("reclaims stale lock with dead PID", async () => {
|
|
111
|
+
const pidPath = join(tempDir, "lock.pid");
|
|
112
|
+
await Bun.write(pidPath, "9999\n");
|
|
113
|
+
const result = await acquirePidLock(pidPath, 1234, alwaysDead);
|
|
114
|
+
expect(result.acquired).toBe(true);
|
|
115
|
+
expect(await readPidFile(pidPath)).toBe(1234);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("reclaims unreadable/corrupted lock file", async () => {
|
|
119
|
+
const pidPath = join(tempDir, "lock.pid");
|
|
120
|
+
await Bun.write(pidPath, "garbage-not-a-pid\n");
|
|
121
|
+
const result = await acquirePidLock(pidPath, 1234, alwaysAlive);
|
|
122
|
+
expect(result.acquired).toBe(true);
|
|
123
|
+
expect(await readPidFile(pidPath)).toBe(1234);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("two simultaneous acquirers — only one wins", async () => {
|
|
127
|
+
const pidPath = join(tempDir, "lock.pid");
|
|
128
|
+
const [a, b] = await Promise.all([
|
|
129
|
+
acquirePidLock(pidPath, 1111, alwaysAlive),
|
|
130
|
+
acquirePidLock(pidPath, 2222, alwaysAlive),
|
|
131
|
+
]);
|
|
132
|
+
const winners = [a, b].filter((r) => r.acquired);
|
|
133
|
+
const losers = [a, b].filter((r) => !r.acquired);
|
|
134
|
+
expect(winners.length).toBe(1);
|
|
135
|
+
expect(losers.length).toBe(1);
|
|
136
|
+
const loser = losers[0];
|
|
137
|
+
if (loser && !loser.acquired) {
|
|
138
|
+
expect([1111, 2222]).toContain(loser.existingPid);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("two simultaneous acquirers — file content matches the winner", async () => {
|
|
143
|
+
const pidPath = join(tempDir, "lock.pid");
|
|
144
|
+
const [a, b] = await Promise.all([
|
|
145
|
+
acquirePidLock(pidPath, 1111, alwaysAlive),
|
|
146
|
+
acquirePidLock(pidPath, 2222, alwaysAlive),
|
|
147
|
+
]);
|
|
148
|
+
const fileContent = await readPidFile(pidPath);
|
|
149
|
+
const winnerPid = a.acquired ? 1111 : b.acquired ? 2222 : -1;
|
|
150
|
+
expect(fileContent).toBe(winnerPid);
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/utils/pid.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID file management for daemon processes.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { link, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read the PID from a PID file.
|
|
10
|
+
* Returns null if the file doesn't exist or can't be parsed.
|
|
11
|
+
*/
|
|
12
|
+
export async function readPidFile(pidFilePath: string): Promise<number | null> {
|
|
13
|
+
const file = Bun.file(pidFilePath);
|
|
14
|
+
const exists = await file.exists();
|
|
15
|
+
if (!exists) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const text = await file.text();
|
|
21
|
+
const pid = Number.parseInt(text.trim(), 10);
|
|
22
|
+
if (Number.isNaN(pid) || pid <= 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return pid;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write a PID to a PID file.
|
|
33
|
+
*/
|
|
34
|
+
export async function writePidFile(pidFilePath: string, pid: number): Promise<void> {
|
|
35
|
+
await Bun.write(pidFilePath, `${pid}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Remove a PID file.
|
|
40
|
+
*/
|
|
41
|
+
export async function removePidFile(pidFilePath: string): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
await unlink(pidFilePath);
|
|
44
|
+
} catch {
|
|
45
|
+
// File may already be gone — not an error
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Result of acquirePidLock.
|
|
51
|
+
*
|
|
52
|
+
* `acquired: true` — caller owns the lock and is responsible for removing the
|
|
53
|
+
* PID file on shutdown.
|
|
54
|
+
*
|
|
55
|
+
* `acquired: false` — a live foreign process already owns the lock; caller
|
|
56
|
+
* must not start. `existingPid` is the live owner. `existingPid === -1` means
|
|
57
|
+
* the lock file existed but was unreadable and could not be reclaimed.
|
|
58
|
+
*/
|
|
59
|
+
export type AcquirePidLockResult = { acquired: true } | { acquired: false; existingPid: number };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Atomically acquire a PID-file lock.
|
|
63
|
+
*
|
|
64
|
+
* Uses the write-temp-then-link pattern so the lock file appears at its final
|
|
65
|
+
* path with PID contents already present (no empty-file window): a competing
|
|
66
|
+
* reader can never observe an in-flight write. Behavior:
|
|
67
|
+
*
|
|
68
|
+
* - Lock file does not exist → atomic create via link(). Caller owns the lock.
|
|
69
|
+
* - Lock file exists, contains the caller's own PID → idempotent acquire
|
|
70
|
+
* (caller already owns it; e.g. background-mode parent wrote child.pid
|
|
71
|
+
* before spawn).
|
|
72
|
+
* - Lock file exists with a live foreign PID → refuse; return existingPid.
|
|
73
|
+
* - Lock file exists with a dead PID (or unreadable) → reclaim by unlinking
|
|
74
|
+
* and retrying once. If the retry races and loses to a live foreign
|
|
75
|
+
* watchdog, the call returns acquired=false with that foreign PID.
|
|
76
|
+
*
|
|
77
|
+
* Parent directory is created if missing (matches the implicit Bun.write
|
|
78
|
+
* behavior the legacy writePidFile relied on).
|
|
79
|
+
*/
|
|
80
|
+
export async function acquirePidLock(
|
|
81
|
+
pidFilePath: string,
|
|
82
|
+
pid: number,
|
|
83
|
+
isAlive: (pid: number) => boolean,
|
|
84
|
+
): Promise<AcquirePidLockResult> {
|
|
85
|
+
await mkdir(dirname(pidFilePath), { recursive: true });
|
|
86
|
+
|
|
87
|
+
// Stage the PID content at a unique temp path. After link() succeeds, the
|
|
88
|
+
// lock path appears with full content already present.
|
|
89
|
+
const tempPath = `${pidFilePath}.tmp.${pid}.${randomUUID()}`;
|
|
90
|
+
await writeFile(tempPath, `${pid}\n`);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Two attempts: first try, then one stale-lock reclaim retry. A second
|
|
94
|
+
// EEXIST after reclaim means a live foreign process raced in.
|
|
95
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
96
|
+
try {
|
|
97
|
+
await link(tempPath, pidFilePath);
|
|
98
|
+
return { acquired: true };
|
|
99
|
+
} catch (err: unknown) {
|
|
100
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
101
|
+
if (code !== "EEXIST") {
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
const existing = await readPidFile(pidFilePath);
|
|
105
|
+
if (existing === null) {
|
|
106
|
+
// Unreadable/corrupted lock file — treat as stale.
|
|
107
|
+
await removePidFile(pidFilePath);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (existing === pid) {
|
|
111
|
+
// Idempotent: caller already owns it (parent pre-wrote child PID).
|
|
112
|
+
return { acquired: true };
|
|
113
|
+
}
|
|
114
|
+
if (isAlive(existing)) {
|
|
115
|
+
return { acquired: false, existingPid: existing };
|
|
116
|
+
}
|
|
117
|
+
// Stale: reclaim and retry once.
|
|
118
|
+
await removePidFile(pidFilePath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Two stale-then-retry attempts both failed. Another writer raced in
|
|
123
|
+
// between our reclaim and our retry — they own the lock now.
|
|
124
|
+
const existing = await readPidFile(pidFilePath);
|
|
125
|
+
return { acquired: false, existingPid: existing ?? -1 };
|
|
126
|
+
} finally {
|
|
127
|
+
// Drop the temp inode link (lock path retains the data via the second link).
|
|
128
|
+
await unlink(tempPath).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { findRunningWatchdogProcesses } from "./process-scan.ts";
|
|
3
|
+
|
|
4
|
+
describe("findRunningWatchdogProcesses", () => {
|
|
5
|
+
test("returns an array (does not throw)", async () => {
|
|
6
|
+
const results = await findRunningWatchdogProcesses();
|
|
7
|
+
expect(Array.isArray(results)).toBe(true);
|
|
8
|
+
// We can't assert specifics — depends on what's running on the host —
|
|
9
|
+
// but each entry should have a numeric pid and string command.
|
|
10
|
+
for (const proc of results) {
|
|
11
|
+
expect(typeof proc.pid).toBe("number");
|
|
12
|
+
expect(proc.pid).toBeGreaterThan(0);
|
|
13
|
+
expect(typeof proc.command).toBe("string");
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("excludes own process even if command matches", async () => {
|
|
18
|
+
// The test process itself runs `bun test ...` not `ap watch`, so it
|
|
19
|
+
// would not match anyway. But we still verify own-pid is filtered out
|
|
20
|
+
// by checking no result has our PID.
|
|
21
|
+
const results = await findRunningWatchdogProcesses();
|
|
22
|
+
const ownPid = process.pid;
|
|
23
|
+
for (const proc of results) {
|
|
24
|
+
expect(proc.pid).not.toBe(ownPid);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("matches `ap watch` and `bun run ap watch` invocations", async () => {
|
|
29
|
+
// Spawn a sleeper whose command line contains the `ap watch` substring,
|
|
30
|
+
// then verify the scanner finds it. We use `sh -c` so the argv string
|
|
31
|
+
// passed to ps contains our marker tokens.
|
|
32
|
+
const sleeper = Bun.spawn(["sh", "-c", "exec -a 'bun run ap watch' sleep 30"], {
|
|
33
|
+
stdout: "ignore",
|
|
34
|
+
stderr: "ignore",
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
// Give ps a moment to see the new process.
|
|
38
|
+
await Bun.sleep(150);
|
|
39
|
+
const results = await findRunningWatchdogProcesses();
|
|
40
|
+
const found = results.find((p) => p.pid === sleeper.pid);
|
|
41
|
+
// On macOS BSD ps, `exec -a` may or may not change the displayed
|
|
42
|
+
// argv depending on shell version. We accept either: if the
|
|
43
|
+
// command is detected, it must look right; if not, we don't fail
|
|
44
|
+
// the test (env-dependent).
|
|
45
|
+
if (found) {
|
|
46
|
+
expect(found.command).toMatch(/\b(ap|agentplate)\b.*\bwatch\b/);
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
sleeper.kill("SIGTERM");
|
|
50
|
+
await sleeper.exited.catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|