@repo-os/repoos 0.5.45
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.md +110 -0
- package/README.md +272 -0
- package/dist/.build-info.json +4 -0
- package/dist/.build-stamp.json +3 -0
- package/dist/cli/colors.d.ts +13 -0
- package/dist/cli/colors.js +37 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +221 -0
- package/dist/commands/check.d.ts +170 -0
- package/dist/commands/check.js +1227 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +70 -0
- package/dist/commands/gc.d.ts +1 -0
- package/dist/commands/gc.js +57 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +699 -0
- package/dist/commands/screenshots.d.ts +7 -0
- package/dist/commands/screenshots.js +77 -0
- package/dist/commands/serve.d.ts +55 -0
- package/dist/commands/serve.js +204 -0
- package/dist/commands/status.d.ts +140 -0
- package/dist/commands/status.js +569 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +59 -0
- package/dist/commands/tasks.d.ts +47 -0
- package/dist/commands/tasks.js +334 -0
- package/dist/commands/tunnel.d.ts +1 -0
- package/dist/commands/tunnel.js +1271 -0
- package/dist/commands/ui-harness.d.ts +53 -0
- package/dist/commands/ui-harness.js +62 -0
- package/dist/commands/ui-smoke.d.ts +6 -0
- package/dist/commands/ui-smoke.js +220 -0
- package/dist/commands/upgrade.d.ts +15 -0
- package/dist/commands/upgrade.js +188 -0
- package/dist/core/auth-store.d.ts +81 -0
- package/dist/core/auth-store.js +530 -0
- package/dist/core/auth.d.ts +75 -0
- package/dist/core/auth.js +124 -0
- package/dist/core/bootstrap.d.ts +41 -0
- package/dist/core/bootstrap.js +236 -0
- package/dist/core/build.d.ts +92 -0
- package/dist/core/build.js +249 -0
- package/dist/core/canary.d.ts +10 -0
- package/dist/core/canary.js +14 -0
- package/dist/core/check-failure-summary.d.ts +10 -0
- package/dist/core/check-failure-summary.js +34 -0
- package/dist/core/config.d.ts +160 -0
- package/dist/core/config.js +1288 -0
- package/dist/core/context-pack.d.ts +74 -0
- package/dist/core/context-pack.js +778 -0
- package/dist/core/db.d.ts +236 -0
- package/dist/core/db.js +747 -0
- package/dist/core/detect.d.ts +69 -0
- package/dist/core/detect.js +251 -0
- package/dist/core/docs.d.ts +76 -0
- package/dist/core/docs.js +225 -0
- package/dist/core/frontmatter.d.ts +29 -0
- package/dist/core/frontmatter.js +215 -0
- package/dist/core/git.d.ts +505 -0
- package/dist/core/git.js +1453 -0
- package/dist/core/indexer.d.ts +33 -0
- package/dist/core/indexer.js +282 -0
- package/dist/core/input.d.ts +66 -0
- package/dist/core/input.js +222 -0
- package/dist/core/logger.d.ts +70 -0
- package/dist/core/logger.js +138 -0
- package/dist/core/models.d.ts +45 -0
- package/dist/core/models.js +291 -0
- package/dist/core/net-probe.d.ts +4 -0
- package/dist/core/net-probe.js +20 -0
- package/dist/core/providers/deepinfra.d.ts +23 -0
- package/dist/core/providers/deepinfra.js +44 -0
- package/dist/core/providers/index.d.ts +24 -0
- package/dist/core/providers/index.js +59 -0
- package/dist/core/providers/openrouter.d.ts +28 -0
- package/dist/core/providers/openrouter.js +82 -0
- package/dist/core/providers/spend.d.ts +86 -0
- package/dist/core/providers/spend.js +286 -0
- package/dist/core/providers/types.d.ts +56 -0
- package/dist/core/providers/types.js +39 -0
- package/dist/core/repoos.d.ts +59 -0
- package/dist/core/repoos.js +221 -0
- package/dist/core/runtime.d.ts +35 -0
- package/dist/core/runtime.js +167 -0
- package/dist/core/skills-registry.d.ts +39 -0
- package/dist/core/skills-registry.js +148 -0
- package/dist/core/skills-registry.test.d.ts +1 -0
- package/dist/core/skills-registry.test.js +52 -0
- package/dist/core/sysmem.d.ts +7 -0
- package/dist/core/sysmem.js +55 -0
- package/dist/core/tailscale.d.ts +2 -0
- package/dist/core/tailscale.js +19 -0
- package/dist/core/task.d.ts +53 -0
- package/dist/core/task.js +310 -0
- package/dist/core/toml-line.d.ts +20 -0
- package/dist/core/toml-line.js +52 -0
- package/dist/core/toml-validate.d.ts +44 -0
- package/dist/core/toml-validate.js +335 -0
- package/dist/core/tunnel-assistant.d.ts +30 -0
- package/dist/core/tunnel-assistant.js +65 -0
- package/dist/core/tunnel-registry.d.ts +96 -0
- package/dist/core/tunnel-registry.js +267 -0
- package/dist/core/tunnel-registry.test.d.ts +1 -0
- package/dist/core/tunnel-registry.test.js +309 -0
- package/dist/core/tunnel.d.ts +103 -0
- package/dist/core/tunnel.js +320 -0
- package/dist/core/types.d.ts +811 -0
- package/dist/core/types.js +27 -0
- package/dist/core/worktree-gc.d.ts +57 -0
- package/dist/core/worktree-gc.js +194 -0
- package/dist/server/__tests__/integration-job.test.d.ts +1 -0
- package/dist/server/__tests__/integration-job.test.js +129 -0
- package/dist/server/agents.d.ts +1020 -0
- package/dist/server/agents.js +4070 -0
- package/dist/server/attachments.d.ts +51 -0
- package/dist/server/attachments.js +130 -0
- package/dist/server/auto-engineering.d.ts +45 -0
- package/dist/server/auto-engineering.js +275 -0
- package/dist/server/branchless-release.d.ts +22 -0
- package/dist/server/branchless-release.js +67 -0
- package/dist/server/built-in-agents.d.ts +305 -0
- package/dist/server/built-in-agents.js +1838 -0
- package/dist/server/cto-monitor.d.ts +35 -0
- package/dist/server/cto-monitor.js +281 -0
- package/dist/server/cto.d.ts +125 -0
- package/dist/server/cto.js +590 -0
- package/dist/server/deployments.d.ts +119 -0
- package/dist/server/deployments.js +325 -0
- package/dist/server/diff-snapshot.d.ts +20 -0
- package/dist/server/diff-snapshot.js +73 -0
- package/dist/server/done.d.ts +160 -0
- package/dist/server/done.js +488 -0
- package/dist/server/freeform.d.ts +55 -0
- package/dist/server/freeform.js +179 -0
- package/dist/server/handoff.d.ts +99 -0
- package/dist/server/handoff.js +575 -0
- package/dist/server/hetzner.d.ts +45 -0
- package/dist/server/hetzner.js +117 -0
- package/dist/server/icons.d.ts +8 -0
- package/dist/server/icons.js +195 -0
- package/dist/server/integration-job.d.ts +75 -0
- package/dist/server/integration-job.js +144 -0
- package/dist/server/integration-orchestrator.d.ts +211 -0
- package/dist/server/integration-orchestrator.js +1299 -0
- package/dist/server/integration-status.d.ts +48 -0
- package/dist/server/integration-status.js +71 -0
- package/dist/server/live-index.d.ts +295 -0
- package/dist/server/live-index.js +452 -0
- package/dist/server/model-test.d.ts +20 -0
- package/dist/server/model-test.js +100 -0
- package/dist/server/ntfy.d.ts +57 -0
- package/dist/server/ntfy.js +112 -0
- package/dist/server/pm-attachments.d.ts +35 -0
- package/dist/server/pm-attachments.js +181 -0
- package/dist/server/pm-runs.d.ts +58 -0
- package/dist/server/pm-runs.js +80 -0
- package/dist/server/preview.d.ts +174 -0
- package/dist/server/preview.js +641 -0
- package/dist/server/release.d.ts +88 -0
- package/dist/server/release.js +449 -0
- package/dist/server/reload.d.ts +194 -0
- package/dist/server/reload.js +586 -0
- package/dist/server/remote-validation.d.ts +132 -0
- package/dist/server/remote-validation.js +478 -0
- package/dist/server/repo-lock.d.ts +36 -0
- package/dist/server/repo-lock.js +141 -0
- package/dist/server/review-guard.d.ts +14 -0
- package/dist/server/review-guard.js +159 -0
- package/dist/server/review.d.ts +243 -0
- package/dist/server/review.js +1211 -0
- package/dist/server/routes/agents.d.ts +6 -0
- package/dist/server/routes/agents.js +28 -0
- package/dist/server/routes/auth.d.ts +60 -0
- package/dist/server/routes/auth.js +755 -0
- package/dist/server/routes/config.d.ts +20 -0
- package/dist/server/routes/config.js +347 -0
- package/dist/server/routes/debugger.d.ts +33 -0
- package/dist/server/routes/debugger.js +360 -0
- package/dist/server/routes/deployments.d.ts +3 -0
- package/dist/server/routes/deployments.js +16 -0
- package/dist/server/routes/docs.d.ts +8 -0
- package/dist/server/routes/docs.js +135 -0
- package/dist/server/routes/helpers.d.ts +21 -0
- package/dist/server/routes/helpers.js +171 -0
- package/dist/server/routes/index.d.ts +19 -0
- package/dist/server/routes/index.js +18 -0
- package/dist/server/routes/info.d.ts +17 -0
- package/dist/server/routes/info.js +177 -0
- package/dist/server/routes/inputs.d.ts +7 -0
- package/dist/server/routes/inputs.js +125 -0
- package/dist/server/routes/model-providers.d.ts +27 -0
- package/dist/server/routes/model-providers.js +94 -0
- package/dist/server/routes/models.d.ts +3 -0
- package/dist/server/routes/models.js +36 -0
- package/dist/server/routes/notify.d.ts +2 -0
- package/dist/server/routes/notify.js +17 -0
- package/dist/server/routes/playground.d.ts +29 -0
- package/dist/server/routes/playground.js +81 -0
- package/dist/server/routes/release.d.ts +20 -0
- package/dist/server/routes/release.js +86 -0
- package/dist/server/routes/router.d.ts +12 -0
- package/dist/server/routes/router.js +32 -0
- package/dist/server/routes/skill-registry.d.ts +5 -0
- package/dist/server/routes/skill-registry.js +51 -0
- package/dist/server/routes/tasks.d.ts +58 -0
- package/dist/server/routes/tasks.js +1442 -0
- package/dist/server/routes/transcribe.d.ts +2 -0
- package/dist/server/routes/transcribe.js +124 -0
- package/dist/server/routes/types.d.ts +68 -0
- package/dist/server/routes/types.js +1 -0
- package/dist/server/routes/ui.d.ts +5 -0
- package/dist/server/routes/ui.js +81 -0
- package/dist/server/routes/utils.d.ts +3 -0
- package/dist/server/routes/utils.js +23 -0
- package/dist/server/serve-reaper.d.ts +135 -0
- package/dist/server/serve-reaper.js +491 -0
- package/dist/server/server.d.ts +49 -0
- package/dist/server/server.js +2171 -0
- package/dist/server/supervisor.d.ts +89 -0
- package/dist/server/supervisor.js +231 -0
- package/dist/server/system.d.ts +198 -0
- package/dist/server/system.js +480 -0
- package/dist/server/task-check.d.ts +45 -0
- package/dist/server/task-check.js +71 -0
- package/dist/server/task-transitions.d.ts +51 -0
- package/dist/server/task-transitions.js +78 -0
- package/dist/server/task-watchdog.d.ts +158 -0
- package/dist/server/task-watchdog.js +681 -0
- package/dist/server/test-run.d.ts +28 -0
- package/dist/server/test-run.js +81 -0
- package/dist/server/watcher.d.ts +21 -0
- package/dist/server/watcher.js +194 -0
- package/dist/server/write.d.ts +94 -0
- package/dist/server/write.js +345 -0
- package/dist/ui/assets/AgentsView-CF4wRvHh.css +1 -0
- package/dist/ui/assets/AgentsView-D-myL-kA.js +1 -0
- package/dist/ui/assets/ContextView-D3TC-RSp.css +1 -0
- package/dist/ui/assets/ContextView-DsOqAdyE.js +4 -0
- package/dist/ui/assets/DashboardView-BOhsIJqO.js +1 -0
- package/dist/ui/assets/DashboardView-Ck9o0RP2.css +1 -0
- package/dist/ui/assets/DeploymentsView--iIUVfEH.css +1 -0
- package/dist/ui/assets/DeploymentsView-DXu_FYR2.js +1 -0
- package/dist/ui/assets/InputsView-24HVbVPk.js +1 -0
- package/dist/ui/assets/InputsView-BLb7GHtx.css +1 -0
- package/dist/ui/assets/LoginView-6QlGg6cx.css +1 -0
- package/dist/ui/assets/LoginView-BqupI0Lt.js +1 -0
- package/dist/ui/assets/ReleasesView-BEG1A0YH.css +1 -0
- package/dist/ui/assets/ReleasesView-CAgMuxpe.js +3 -0
- package/dist/ui/assets/SettingsView-CA-NYSMK.css +1 -0
- package/dist/ui/assets/SettingsView-O3iZSnjg.js +3 -0
- package/dist/ui/assets/WorkView-D8Tl6qwy.js +1 -0
- package/dist/ui/assets/WorkView-DMYL2s3W.css +1 -0
- package/dist/ui/assets/abnfDiagram-VCTEODGH-J5lkSnEE.js +1 -0
- package/dist/ui/assets/arc-BHZnmhhM.js +1 -0
- package/dist/ui/assets/architecture-7GRP2DOG-pLdFt-xS.js +1 -0
- package/dist/ui/assets/architectureDiagram-5GKGNRK7-C3k89uKX.js +36 -0
- package/dist/ui/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/assets/blockDiagram-I7D4REHJ-AWNoBzop.js +129 -0
- package/dist/ui/assets/button-lbikqSph.js +1 -0
- package/dist/ui/assets/c4Diagram-7LVT6UL2-DKG1D5gd.js +38 -0
- package/dist/ui/assets/channel-gA4qBv3A.js +1 -0
- package/dist/ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/assets/chunk-4HAMMTFA-D1YR8lF_.js +62 -0
- package/dist/ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/assets/chunk-75Z2AOVW-5GuBxfsL.js +2 -0
- package/dist/ui/assets/chunk-DU6HZSFF-DfdNNqwa.js +127 -0
- package/dist/ui/assets/chunk-F27PBJKO-BPZzinQ4.js +1 -0
- package/dist/ui/assets/chunk-FOHPRMQF-l2z4K2jD.js +161 -0
- package/dist/ui/assets/chunk-GMAD6QVW-DGWvSKj5.js +72 -0
- package/dist/ui/assets/chunk-GVQU2GXP-D6Aw75wJ.js +1 -0
- package/dist/ui/assets/chunk-IMKFNOWR-B8mMBKCW.js +231 -0
- package/dist/ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/assets/chunk-L3NEJ4N5-D5OJuXYE.js +1 -0
- package/dist/ui/assets/chunk-OSK3NFVY-B7r9w2UW.js +10 -0
- package/dist/ui/assets/chunk-P2QGCYS3-C74X4AQX.js +1 -0
- package/dist/ui/assets/chunk-POPQ4Y6H-C5_u1gUS.js +1 -0
- package/dist/ui/assets/chunk-PWAF6VOD-DfoTLtxn.js +1 -0
- package/dist/ui/assets/chunk-SHT3W25Y-D40m2eqz.js +168 -0
- package/dist/ui/assets/chunk-SVP7TREG-euxi0Kap.js +88 -0
- package/dist/ui/assets/chunk-TICWLB2K-fkdxngzN.js +206 -0
- package/dist/ui/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
- package/dist/ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/assets/classDiagram-ZZMXUADV-BzW8bq2P.js +1 -0
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BzW8bq2P.js +1 -0
- package/dist/ui/assets/cose-bilkent-JH36ORCC-BTcgMon1.js +1 -0
- package/dist/ui/assets/cynefin-OW5HDTMX-B5m_COsS.js +1 -0
- package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-C-AG6CGe.js +62 -0
- package/dist/ui/assets/cytoscape.esm-FBntH-9t.js +321 -0
- package/dist/ui/assets/dagre-GXQ25YYZ-DDfDnp8L.js +4 -0
- package/dist/ui/assets/dagre-PrKaheQc.js +1 -0
- package/dist/ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/assets/diagram-S7CK7UJ4-DjQZAlEY.js +30 -0
- package/dist/ui/assets/diagram-UQ7AKVKN-DAZ9yp1B.js +41 -0
- package/dist/ui/assets/diagram-VSXAHHWV-BWepUKJD.js +3 -0
- package/dist/ui/assets/diagram-VX7I27RA-EcV1J81b.js +24 -0
- package/dist/ui/assets/diagram-Z3DM3KII-Dbz7XU_K.js +24 -0
- package/dist/ui/assets/dist-DTg6UBE_.js +1 -0
- package/dist/ui/assets/ebnfDiagram-PWID7BFC-BUyQw8Mm.js +1 -0
- package/dist/ui/assets/erDiagram-RLTQ6QDP-WVf8ukKt.js +99 -0
- package/dist/ui/assets/eventmodeling-NTZA5JFV-DHqyLOAn.js +1 -0
- package/dist/ui/assets/flowDiagram-HODETNUW-CYk_ze5K.js +1 -0
- package/dist/ui/assets/ganttDiagram-EL5Y4UJY-rB4MZuB-.js +292 -0
- package/dist/ui/assets/gitGraph-4MIJSDKK-BOTWM8dY.js +1 -0
- package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-eYscuhtP.js +106 -0
- package/dist/ui/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/assets/index-BACBfnga.css +1 -0
- package/dist/ui/assets/index-BOQ-ust6.js +26 -0
- package/dist/ui/assets/info-A6RAGUB7-_67wOORe.js +1 -0
- package/dist/ui/assets/infoDiagram-27XIBGKW-XVXbddTA.js +2 -0
- package/dist/ui/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/assets/ishikawaDiagram-5VMMS53U-ClLNiEru.js +70 -0
- package/dist/ui/assets/journeyDiagram-3NMN7TZE-NRawjR6i.js +139 -0
- package/dist/ui/assets/kanban-definition-UXKFOSKX-BgSzBfIx.js +89 -0
- package/dist/ui/assets/katex-DolUETbr.js +257 -0
- package/dist/ui/assets/line-KAyfuul4.js +1 -0
- package/dist/ui/assets/linear-cCCPOpu7.js +1 -0
- package/dist/ui/assets/mermaid-parser.core-CgXZk1iN.js +7 -0
- package/dist/ui/assets/mermaid.core-BUUkChHW.js +44 -0
- package/dist/ui/assets/mindmap-definition-YA3MSWOX-Cpn7vW4r.js +96 -0
- package/dist/ui/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/assets/packet-AYTQ26CC-0lZwM-d6.js +1 -0
- package/dist/ui/assets/path-COt_16Va.js +1 -0
- package/dist/ui/assets/pegDiagram-XKGWAZYB-impy0inn.js +1 -0
- package/dist/ui/assets/pie-WAS4IAKB-CqfdGHYg.js +1 -0
- package/dist/ui/assets/pieDiagram-E7YTZNPT-CxjL4I1h.js +39 -0
- package/dist/ui/assets/quadrantDiagram-AXDQQJYC-Cjd1QMIe.js +7 -0
- package/dist/ui/assets/radar-RG4KPBEZ-CRkQIwiv.js +1 -0
- package/dist/ui/assets/railroad-74A4TZTK-i67XSWTB.js +1 -0
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-Cv9m5aS3.js +1 -0
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-C5ZIn-Jh.js +1 -0
- package/dist/ui/assets/railroad-peg-WCYAUIDC-CGra9Oq-.js +1 -0
- package/dist/ui/assets/railroadDiagram-O6MQD6OU-_pK0qyaY.js +1 -0
- package/dist/ui/assets/releases-B0WNV-SY.js +1 -0
- package/dist/ui/assets/repoos-cto-square.webp +0 -0
- package/dist/ui/assets/repoos-helpful-pm-square.webp +0 -0
- package/dist/ui/assets/repoos-orchestrator-square.webp +0 -0
- package/dist/ui/assets/repoos-performance-agent-square.webp +0 -0
- package/dist/ui/assets/repoos-ross-from-friends-square.webp +0 -0
- package/dist/ui/assets/repoos-tech-debt-agent-square.webp +0 -0
- package/dist/ui/assets/requirementDiagram-BXWQKSXE-CRrFX-yw.js +84 -0
- package/dist/ui/assets/rough.esm-By172zw-.js +1 -0
- package/dist/ui/assets/sankeyDiagram-P5KCCOFB-X_xFRj9E.js +40 -0
- package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-Byb49lKf.js +162 -0
- package/dist/ui/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dist/ui/assets/src-BH-TyZbA.js +1 -0
- package/dist/ui/assets/stateDiagram-D77RDMKH-CzaPrqYF.js +1 -0
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BCXckz7K.js +1 -0
- package/dist/ui/assets/swimlanes-42K2YHIH-Cx_1lnqr.js +1 -0
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-54qsKi6A.js +8 -0
- package/dist/ui/assets/timeline-definition-24CTP7MA-B1IbZPQ-.js +120 -0
- package/dist/ui/assets/treeView-Q6P3EWNA-DFWhVE3d.js +1 -0
- package/dist/ui/assets/treemap-WGGIJYW6-BmbYPThn.js +1 -0
- package/dist/ui/assets/vendor-ui-DPm7zLxi.js +3 -0
- package/dist/ui/assets/vendor-vue-CobgyEJp.js +1 -0
- package/dist/ui/assets/vennDiagram-4TSXK5OY-Ae2AtRpt.js +34 -0
- package/dist/ui/assets/wardley-WFR3VGLG-BtD4X4tL.js +1 -0
- package/dist/ui/assets/wardleyDiagram-VM6X3IG4-B3YN2fTd.js +78 -0
- package/dist/ui/assets/xychartDiagram-S5SC5T6Z-CIzJfMKA.js +7 -0
- package/dist/ui/favicon.svg +11 -0
- package/dist/ui/index.html +49 -0
- package/dist/ui/sw.js +48 -0
- package/package.json +91 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reap stale and orphaned repoos serve processes (task 0168).
|
|
3
|
+
*
|
|
4
|
+
* Manages a single lockfile under `.repoos/serve.lock` that tracks the PID of
|
|
5
|
+
* the serve process serving a given port. On startup, stale processes are
|
|
6
|
+
* detected and reaped. Port binding conflicts are detected before any bind
|
|
7
|
+
* attempt so serve refuses to coexist silently with a live server.
|
|
8
|
+
*
|
|
9
|
+
* Windows limitation: process alive-check is best-effort (kill -0 support
|
|
10
|
+
* varies). The reaper is primarily for Unix/Mac. On Windows, rely on the
|
|
11
|
+
* OS to reject EADDRINUSE if a port is already bound.
|
|
12
|
+
*
|
|
13
|
+
* Zero runtime deps: node:fs / node:child_process / node:path only.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, realpathSync, readFileSync, writeFileSync, rmSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { join, dirname, sep } from "node:path";
|
|
17
|
+
import { execFile, execSync } from "node:child_process";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import net from "node:net";
|
|
20
|
+
/** Concurrency cap for the boot-time orphan sweep's per-process cwd lookups. */
|
|
21
|
+
const ORPHAN_SWEEP_CONCURRENCY = 16;
|
|
22
|
+
/** Consecutive missing-root checks before a serve process closes itself. */
|
|
23
|
+
const WATCH_MISSES_REQUIRED = 3;
|
|
24
|
+
/**
|
|
25
|
+
* Manages a single lockfile that tracks the live serve process for a repo.
|
|
26
|
+
* At startup, detects and reaps stale processes. Prevents port conflicts.
|
|
27
|
+
* Skip cleanup when spawned as a reload replacement (REPOOS_RELOAD=1) so
|
|
28
|
+
* the parent can exit gracefully through its close handler.
|
|
29
|
+
*/
|
|
30
|
+
export class ServeReaper {
|
|
31
|
+
lockPath;
|
|
32
|
+
pid = process.pid;
|
|
33
|
+
repoRoot;
|
|
34
|
+
/** Ephemeral test/harness servers must never touch the control-plane lock. */
|
|
35
|
+
enabled;
|
|
36
|
+
registered = false;
|
|
37
|
+
rootWatch = null;
|
|
38
|
+
/**
|
|
39
|
+
* A preview child (`repoos serve --port <ephemeral>` rooted at a task
|
|
40
|
+
* worktree) is NOT the control-plane server, so the single-per-repo serve
|
|
41
|
+
* lockfile and its stale-reaping/conflict logic don't apply to it. If it ran
|
|
42
|
+
* `cleanupStale`, it would reap whatever sibling preview happens to be in the
|
|
43
|
+
* worktree's lockfile (killing a booting preview mid-health-check → "did not
|
|
44
|
+
* become ready"), and because ephemeral ports can be reused, `detectConflict`
|
|
45
|
+
* could even refuse to bind a port that is already stale in the lockfile.
|
|
46
|
+
* Disable the reaper entirely for preview children (#0183).
|
|
47
|
+
*/
|
|
48
|
+
previewChild = process.env.REPOOS_PREVIEW_CHILD === "1";
|
|
49
|
+
constructor(repoRoot, cacheDir = ".repoos", enabled = true, port) {
|
|
50
|
+
this.repoRoot = repoRoot;
|
|
51
|
+
// Per-PORT lockfile. A repo can legitimately run more than one control
|
|
52
|
+
// plane (the dogfood `just serve` on :7171 and Claude Code's Browser-pane
|
|
53
|
+
// preview on another port). With a single `serve.lock`, whichever booted
|
|
54
|
+
// last reaped the other via `cleanupStale` (it kills the lock's PID without
|
|
55
|
+
// checking the port). Scoping the lock by port means each server only ever
|
|
56
|
+
// sees — and only ever reaps a stale predecessor of — its own endpoint.
|
|
57
|
+
this.lockPath = join(repoRoot, cacheDir, Number.isInteger(port) && port > 0 ? `serve-${port}.lock` : "serve.lock");
|
|
58
|
+
this.enabled = enabled;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Close this server when the checkout it serves disappears. Fixture and
|
|
62
|
+
* preview roots are frequently removed before their child process receives
|
|
63
|
+
* its normal shutdown signal; a lockfile inside that deleted root can no
|
|
64
|
+
* longer help a later startup reap it. The watch is deliberately owned by
|
|
65
|
+
* the server process, so it also covers standalone fixture `serve` calls.
|
|
66
|
+
*
|
|
67
|
+
* A real repo root can be momentarily invisible (a rename while the dir is
|
|
68
|
+
* moved, a network-FS blip) without being gone — tearing the server down on
|
|
69
|
+
* such a transient miss would kill the live control plane. The root must
|
|
70
|
+
* be missing for `missesRequired` consecutive checks before `onMissing`
|
|
71
|
+
* fires; a deleted fixture/preview root stays gone, so this only delays the
|
|
72
|
+
* self-close by `missesRequired × intervalMs`.
|
|
73
|
+
*/
|
|
74
|
+
watchRoot(onMissing, intervalMs = 1_000, missesRequired = WATCH_MISSES_REQUIRED) {
|
|
75
|
+
this.stopWatchingRoot();
|
|
76
|
+
let misses = 0;
|
|
77
|
+
const check = () => {
|
|
78
|
+
if (existsSync(this.repoRoot)) {
|
|
79
|
+
misses = 0;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
misses += 1;
|
|
83
|
+
if (misses < missesRequired)
|
|
84
|
+
return;
|
|
85
|
+
this.stopWatchingRoot();
|
|
86
|
+
onMissing();
|
|
87
|
+
};
|
|
88
|
+
this.rootWatch = setInterval(check, intervalMs);
|
|
89
|
+
this.rootWatch.unref?.();
|
|
90
|
+
return () => this.stopWatchingRoot();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if a process with the given PID is still running and matches our
|
|
94
|
+
* expected serve characteristics (the repoos CLI binary in the command line).
|
|
95
|
+
* On Windows, the check is limited to existence (kill -0).
|
|
96
|
+
*/
|
|
97
|
+
isProcessAlive(pid, port) {
|
|
98
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
99
|
+
return false;
|
|
100
|
+
// For the current process, trust it's valid without checking
|
|
101
|
+
if (pid === this.pid)
|
|
102
|
+
return true;
|
|
103
|
+
try {
|
|
104
|
+
// Check if process exists (works on Unix and Windows)
|
|
105
|
+
execSync(`kill -0 ${pid}`, { stdio: "ignore", timeout: 2000 });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// On Windows, we can only verify existence, not the cmdline. Return true.
|
|
111
|
+
if (process.platform === "win32")
|
|
112
|
+
return true;
|
|
113
|
+
// On Unix/Mac, verify it's a repoos serve process on the expected port
|
|
114
|
+
try {
|
|
115
|
+
const cmdline = execSync(`ps -p ${pid} -o command=`, {
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
118
|
+
timeout: 2000,
|
|
119
|
+
});
|
|
120
|
+
return cmdline.includes("serve") && cmdline.includes(`--port ${port}`);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Boot-time cleanup: detect and reap stale processes from the lockfile.
|
|
128
|
+
* SKIPPED when spawned as a reload replacement (REPOOS_RELOAD=1) so the
|
|
129
|
+
* parent process can exit gracefully without being killed mid-shutdown.
|
|
130
|
+
* This is safe to call multiple times and always succeeds (best-effort).
|
|
131
|
+
*/
|
|
132
|
+
cleanupStale() {
|
|
133
|
+
if (!this.enabled)
|
|
134
|
+
return;
|
|
135
|
+
// A preview child must never reap other processes (see `previewChild`).
|
|
136
|
+
if (this.previewChild)
|
|
137
|
+
return;
|
|
138
|
+
// Skip cleanup when spawned as a reload replacement — the parent process
|
|
139
|
+
// is still running and will shut down gracefully. Killing it here would
|
|
140
|
+
// bypass previews.stopAll() and runner.flushAll(), orphaning children.
|
|
141
|
+
if (process.env.REPOOS_RELOAD === "1")
|
|
142
|
+
return;
|
|
143
|
+
if (!existsSync(this.lockPath))
|
|
144
|
+
return;
|
|
145
|
+
let old = null;
|
|
146
|
+
try {
|
|
147
|
+
old = JSON.parse(readFileSync(this.lockPath, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Corrupt lockfile — drop it
|
|
151
|
+
this.removeLock();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!old || typeof old.pid !== "number" || typeof old.port !== "number") {
|
|
155
|
+
this.removeLock();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// If the lockfile claims our own PID, it's stale from a prior launchd run
|
|
159
|
+
// that recycled this PID — don't kill ourselves.
|
|
160
|
+
if (old.pid === this.pid) {
|
|
161
|
+
this.removeLock();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// If there's a stale process, try to reap it
|
|
165
|
+
if (!this.isProcessAlive(old.pid, old.port)) {
|
|
166
|
+
this.removeLock();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Process is still running. Try to kill it since it's stale
|
|
170
|
+
this.killProcess(old.pid);
|
|
171
|
+
this.removeLock();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Reap historical fixture/preview servers whose deleted root also took their
|
|
175
|
+
* lockfile with it. This is intentionally narrower than a generic process
|
|
176
|
+
* sweep: it requires the RepoOS CLI's `serve` command shape AND a cwd under
|
|
177
|
+
* the system temp dir that no longer exists — the exact shape of every
|
|
178
|
+
* observed orphan (`/var/folders/.../repoos-release-*`, `.../repoos-autoprev-*`).
|
|
179
|
+
* A user's live control plane, every healthy preview, and a sibling project
|
|
180
|
+
* that happens to be briefly unmounted or renamed therefore remain untouched
|
|
181
|
+
* (a real repo never lives under tmpdir).
|
|
182
|
+
*
|
|
183
|
+
* Async and best-effort so a boot full of accumulated orphans can never
|
|
184
|
+
* block serve startup: the `ps` scan and the per-process `lsof` cwd lookups
|
|
185
|
+
* run off the event loop, with the lookups capped in flight. Returns the
|
|
186
|
+
* number of processes killed.
|
|
187
|
+
*/
|
|
188
|
+
async cleanupOrphanedRoots() {
|
|
189
|
+
if (!this.enabled)
|
|
190
|
+
return 0;
|
|
191
|
+
if (process.platform === "win32")
|
|
192
|
+
return 0;
|
|
193
|
+
let rows;
|
|
194
|
+
try {
|
|
195
|
+
rows = await execFileAsync("ps", ["-axo", "pid=,command="], 4_000);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
const candidates = [];
|
|
201
|
+
for (const row of rows.split("\n")) {
|
|
202
|
+
const match = row.trim().match(/^(\d+)\s+(.+)$/);
|
|
203
|
+
if (!match)
|
|
204
|
+
continue;
|
|
205
|
+
const pid = Number(match[1]);
|
|
206
|
+
const command = match[2];
|
|
207
|
+
if (pid === this.pid || !isOrphanServeCommand(command))
|
|
208
|
+
continue;
|
|
209
|
+
candidates.push(pid);
|
|
210
|
+
}
|
|
211
|
+
if (candidates.length === 0)
|
|
212
|
+
return 0;
|
|
213
|
+
let reaped = 0;
|
|
214
|
+
await runBounded(candidates, ORPHAN_SWEEP_CONCURRENCY, async (pid) => {
|
|
215
|
+
const cwd = await this.processCwdAsync(pid);
|
|
216
|
+
if (!cwd || !isOrphanRoot(cwd))
|
|
217
|
+
return;
|
|
218
|
+
// PID-recycling guard: the command-shape scan ran at sweep start, so a
|
|
219
|
+
// PID could have been reused by an unrelated process in the interim.
|
|
220
|
+
// Never signal a PID that no longer names a serve process at kill time.
|
|
221
|
+
const command = await this.commandOf(pid);
|
|
222
|
+
if (!command || !isOrphanServeCommand(command))
|
|
223
|
+
return;
|
|
224
|
+
this.killProcess(pid);
|
|
225
|
+
reaped += 1;
|
|
226
|
+
});
|
|
227
|
+
return reaped;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Detect if a port is already bound by a live serve process.
|
|
231
|
+
* Returns a human-readable error message if there's a conflict, null otherwise.
|
|
232
|
+
*
|
|
233
|
+
* Two independent signals are consulted:
|
|
234
|
+
* 1. The live port probe (new): is *something* actually listening? Consulted
|
|
235
|
+
* first so a missing/stale/corrupt lockfile can no longer mask a live
|
|
236
|
+
* listener — the single source of truth for "is the port taken" is the
|
|
237
|
+
* port itself, not the lockfile (#0284).
|
|
238
|
+
* 2. The lockfile (existing): when it names a live process for this exact
|
|
239
|
+
* port/host, that yields the most specific conflict (PID + start time).
|
|
240
|
+
*
|
|
241
|
+
* The probe matters because binding a port a lockfile-less-but-live process
|
|
242
|
+
* owns would otherwise either EADDRINUSE with a confusing error or, worse,
|
|
243
|
+
* silently steal a port mid-drain during another process's reload.
|
|
244
|
+
*/
|
|
245
|
+
async detectConflict(port, host) {
|
|
246
|
+
if (!this.enabled)
|
|
247
|
+
return null;
|
|
248
|
+
// Preview children bind distinct ephemeral ports; the control-plane
|
|
249
|
+
// conflict check (which reads the shared per-worktree lockfile) does not
|
|
250
|
+
// apply and can false-positive on a reused port (see `previewChild`).
|
|
251
|
+
if (this.previewChild)
|
|
252
|
+
return null;
|
|
253
|
+
// "0.0.0.0" is a valid bind address but not a connectable one — probe the
|
|
254
|
+
// loopback interface instead. Ephemeral (port 0) means "pick a free port",
|
|
255
|
+
// so there is never a listener to probe.
|
|
256
|
+
const probeHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
257
|
+
const listening = port !== 0 ? await isPortListening(port, probeHost) : false;
|
|
258
|
+
let info = null;
|
|
259
|
+
if (existsSync(this.lockPath)) {
|
|
260
|
+
try {
|
|
261
|
+
info = JSON.parse(readFileSync(this.lockPath, "utf8"));
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
info = null; // corrupt — fall through to the probe result
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// A lockfile that names a live process for this exact port/host is the most
|
|
268
|
+
// specific conflict: we can point the user at the offending PID.
|
|
269
|
+
if (info && info.port === port && info.host === host && this.isProcessAlive(info.pid, port)) {
|
|
270
|
+
return (`Port ${port} is already bound by another repoos serve process (PID ${info.pid} started at ${info.startedAt}). ` +
|
|
271
|
+
`Use 'kill ${info.pid}' to stop it, or choose a different port with --port.`);
|
|
272
|
+
}
|
|
273
|
+
// A lockfile that exists for this exact port/host but names a dead process
|
|
274
|
+
// is stale — clear it so a later startup (and our own register below)
|
|
275
|
+
// starts from a clean slate. Only when the process is actually gone.
|
|
276
|
+
if (info && info.port === port && info.host === host) {
|
|
277
|
+
this.removeLock();
|
|
278
|
+
}
|
|
279
|
+
// A live listener the lockfile doesn't explain is still a conflict: binding
|
|
280
|
+
// would either EADDRINUSE or steal a port some live process — possibly
|
|
281
|
+
// another serve — already holds (#0284). The message distinguishes the two
|
|
282
|
+
// reasons the lockfile can differ from reality so a present-and-valid
|
|
283
|
+
// lockfile (for a different port/host) isn't dismissed as "missing/stale".
|
|
284
|
+
if (listening) {
|
|
285
|
+
// We reach here only when the lockfile couldn't explain the live listener.
|
|
286
|
+
// Same endpoint + live PID was handled above; same endpoint + dead PID was
|
|
287
|
+
// just reaped (stale). Anything else means the lockfile points elsewhere,
|
|
288
|
+
// or is absent — so say which, rather than blanket-claiming "missing/stale".
|
|
289
|
+
const lockAccounted = info === null
|
|
290
|
+
? "the serve lockfile is missing or corrupt"
|
|
291
|
+
: info.port === port && info.host === host
|
|
292
|
+
? "the serve lockfile names a dead process on this port"
|
|
293
|
+
: `the serve lockfile names a different endpoint (${info.host}:${info.port})`;
|
|
294
|
+
return (`Port ${port} is already in use by a live process on ${probeHost}, but ${lockAccounted}. ` +
|
|
295
|
+
`Refusing to bind rather than steal a port a live process owns — choose a different port with --port.`);
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Register the current process as serving on the given port.
|
|
301
|
+
* Call this after successful bind. Idempotent.
|
|
302
|
+
*/
|
|
303
|
+
register(port, host) {
|
|
304
|
+
// Preview children don't own the control-plane lockfile; writing it would
|
|
305
|
+
// collide with the worktree's other preview cycles (see `previewChild`).
|
|
306
|
+
if (!this.enabled || this.previewChild)
|
|
307
|
+
return;
|
|
308
|
+
try {
|
|
309
|
+
mkdirSync(dirname(this.lockPath), { recursive: true });
|
|
310
|
+
const info = {
|
|
311
|
+
pid: this.pid,
|
|
312
|
+
port,
|
|
313
|
+
host,
|
|
314
|
+
startedAt: new Date().toISOString(),
|
|
315
|
+
};
|
|
316
|
+
writeFileSync(this.lockPath, JSON.stringify(info, null, 2));
|
|
317
|
+
this.registered = true;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Registration is best-effort — never crash the server
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Unregister the current process. Call on shutdown.
|
|
325
|
+
* Idempotent: unregistering when not registered is a no-op.
|
|
326
|
+
*/
|
|
327
|
+
unregister() {
|
|
328
|
+
this.stopWatchingRoot();
|
|
329
|
+
// An ephemeral server used the same repo root but never registered itself;
|
|
330
|
+
// it must not delete the long-lived control plane's lock on close.
|
|
331
|
+
if (!this.registered)
|
|
332
|
+
return;
|
|
333
|
+
this.removeLock();
|
|
334
|
+
this.registered = false;
|
|
335
|
+
}
|
|
336
|
+
// ---- internals ----
|
|
337
|
+
removeLock() {
|
|
338
|
+
try {
|
|
339
|
+
rmSync(this.lockPath, { force: true });
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
/* ignore */
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
stopWatchingRoot() {
|
|
346
|
+
if (this.rootWatch === null)
|
|
347
|
+
return;
|
|
348
|
+
clearInterval(this.rootWatch);
|
|
349
|
+
this.rootWatch = null;
|
|
350
|
+
}
|
|
351
|
+
/** Read a process cwd from lsof's machine-readable output, best-effort. */
|
|
352
|
+
async processCwdAsync(pid) {
|
|
353
|
+
try {
|
|
354
|
+
const out = await execFileAsync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], 2_000);
|
|
355
|
+
const line = out.split("\n").find((entry) => entry.startsWith("n"));
|
|
356
|
+
const cwd = line?.slice(1) || null;
|
|
357
|
+
// macOS lsof suffixes a deleted cwd with " (deleted)" — exactly the case
|
|
358
|
+
// the orphan sweep looks for, so strip it before the root checks.
|
|
359
|
+
return cwd ? cwd.replace(/ \(deleted\)$/, "") : null;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** The command line of `pid`, or null when the process is already gone. */
|
|
366
|
+
async commandOf(pid) {
|
|
367
|
+
try {
|
|
368
|
+
const out = await execFileAsync("ps", ["-p", String(pid), "-o", "command="], 2_000);
|
|
369
|
+
return out.trim() || null;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
killProcess(pid) {
|
|
376
|
+
try {
|
|
377
|
+
// Use process.kill() which is portable across Unix/Mac/Windows.
|
|
378
|
+
// Send SIGTERM first (graceful), then SIGKILL only if the process is
|
|
379
|
+
// still alive at the deadline. Never SIGKILL a PID that exited in the
|
|
380
|
+
// grace window — an unrelated process may have recycled it (#0216).
|
|
381
|
+
process.kill(pid, "SIGTERM");
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
/* ignore — process may already be gone */
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
try {
|
|
389
|
+
process.kill(pid, 0); // existence probe — throws ESRCH when gone
|
|
390
|
+
process.kill(pid, "SIGKILL");
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
/* ignore — already exited; do not SIGKILL a possibly-recycled PID */
|
|
394
|
+
}
|
|
395
|
+
}, 100);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* True when a TCP connection can be established to host:port — i.e. some
|
|
400
|
+
* process is actually listening there. This is the ground truth for "is the
|
|
401
|
+
* port taken", consulted alongside the lockfile so a missing/stale/corrupt
|
|
402
|
+
* lockfile can't mask a live listener (#0284). Best-effort and async: any
|
|
403
|
+
* connect error (connection refused, timeout, unroutable host) resolves false,
|
|
404
|
+
* and an invalid port resolves false immediately.
|
|
405
|
+
*/
|
|
406
|
+
export function isPortListening(port, host, timeoutMs = 1000) {
|
|
407
|
+
return new Promise((resolve) => {
|
|
408
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
409
|
+
resolve(false);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const socket = net.connect({ port, host });
|
|
413
|
+
let settled = false;
|
|
414
|
+
const done = (ok) => {
|
|
415
|
+
if (settled)
|
|
416
|
+
return;
|
|
417
|
+
settled = true;
|
|
418
|
+
socket.destroy();
|
|
419
|
+
resolve(ok);
|
|
420
|
+
};
|
|
421
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
422
|
+
socket.once("connect", () => done(true));
|
|
423
|
+
socket.once("error", () => done(false));
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* True when a process command line has the RepoOS CLI `serve` shape — the
|
|
428
|
+
* compiled `.../cli/index.js serve` or dev `.../src/cli/index.ts serve`. The
|
|
429
|
+
* orphan sweep requires this shape in addition to a deleted temp-dir root, so
|
|
430
|
+
* unrelated processes are never touched.
|
|
431
|
+
*/
|
|
432
|
+
export function isOrphanServeCommand(command) {
|
|
433
|
+
return /cli[/\\]index\.(?:js|ts)/.test(command) && /\sserve(?:\s|$)/.test(command);
|
|
434
|
+
}
|
|
435
|
+
let cachedTempDirs = null;
|
|
436
|
+
/**
|
|
437
|
+
* The system temp dir and its real path. macOS exposes `/var` as a symlink
|
|
438
|
+
* to `/private/var`, and lsof reports the resolved path, so both forms are
|
|
439
|
+
* kept and compared.
|
|
440
|
+
*/
|
|
441
|
+
function systemTempDirs() {
|
|
442
|
+
if (cachedTempDirs)
|
|
443
|
+
return cachedTempDirs;
|
|
444
|
+
const raw = tmpdir();
|
|
445
|
+
const dirs = new Set([raw]);
|
|
446
|
+
try {
|
|
447
|
+
dirs.add(realpathSync(raw));
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
/* the temp dir always exists */
|
|
451
|
+
}
|
|
452
|
+
cachedTempDirs = [...dirs];
|
|
453
|
+
return cachedTempDirs;
|
|
454
|
+
}
|
|
455
|
+
/** True when `cwd` is at or under the system temp dir. */
|
|
456
|
+
function isUnderSystemTempDir(cwd) {
|
|
457
|
+
return systemTempDirs().some((d) => cwd === d || cwd.startsWith(d + sep));
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* True when `cwd` is a deleted root under the system temp dir — the exact
|
|
461
|
+
* shape of every observed orphan (fixture/preview temp dirs). A real repo —
|
|
462
|
+
* the live control plane, a preview worktree, any sibling project — never
|
|
463
|
+
* lives under tmpdir, so a briefly-unmounted or renamed repo cannot be
|
|
464
|
+
* mistaken for an orphan and killed. Exported for testing.
|
|
465
|
+
*/
|
|
466
|
+
export function isOrphanRoot(cwd) {
|
|
467
|
+
if (existsSync(cwd))
|
|
468
|
+
return false;
|
|
469
|
+
return isUnderSystemTempDir(cwd);
|
|
470
|
+
}
|
|
471
|
+
/** `execFile` promisified for the boot-time orphan sweep. */
|
|
472
|
+
function execFileAsync(cmd, args, timeoutMs) {
|
|
473
|
+
return new Promise((resolve, reject) => {
|
|
474
|
+
execFile(cmd, args, { encoding: "utf8", timeout: timeoutMs }, (error, stdout) => {
|
|
475
|
+
if (error)
|
|
476
|
+
reject(error);
|
|
477
|
+
else
|
|
478
|
+
resolve(stdout);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/** Run `fn` over `items` with at most `concurrency` promises in flight. */
|
|
483
|
+
async function runBounded(items, concurrency, fn) {
|
|
484
|
+
const queue = [...items];
|
|
485
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
486
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
487
|
+
await fn(next);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
await Promise.all(workers);
|
|
491
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { LiveIndex } from "./live-index.js";
|
|
2
|
+
export interface ServeOptions {
|
|
3
|
+
root?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
host?: string;
|
|
6
|
+
/**
|
|
7
|
+
* True when this process is a reload replacement (REPOOS_RELOAD=1): retry
|
|
8
|
+
* EADDRINUSE until the old process releases the port, instead of failing.
|
|
9
|
+
*/
|
|
10
|
+
reloadReplacement?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Force auth off for this instance regardless of [auth] in repoos.toml.
|
|
13
|
+
* For ephemeral in-process servers (the UI smoke gate) that test generic
|
|
14
|
+
* rendering, not auth flows — a project with real auth enabled must not
|
|
15
|
+
* make `repoos check` itself unable to reach the dashboard.
|
|
16
|
+
*/
|
|
17
|
+
disableAuth?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Fired synchronously the instant the HTTP listener actually binds
|
|
20
|
+
* (`server.listen()`'s own callback), with the live index instance at that
|
|
21
|
+
* exact moment. Exists so a test can assert a STATE invariant instead of a
|
|
22
|
+
* timing one — see boot-timing.test.ts: comparing wall-clock timestamps
|
|
23
|
+
* (even in-process ones) can't reliably distinguish "genuinely concurrent"
|
|
24
|
+
* from "coincidentally fast" when the index build finishes within the same
|
|
25
|
+
* millisecond as listen() on a healthy/fast machine. `index.snapshot()`
|
|
26
|
+
* still reporting zero tasks at this instant is unambiguous regardless of
|
|
27
|
+
* speed: `refreshAllAsync()` only populates the index in one atomic swap
|
|
28
|
+
* at the end, so 0 here can only mean the listener bound before the async
|
|
29
|
+
* build touched anything — a real regression to the old synchronous boot
|
|
30
|
+
* (index built to completion, then listen()) would show the full count
|
|
31
|
+
* already present.
|
|
32
|
+
*/
|
|
33
|
+
onListening?: (index: LiveIndex) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Only a long-lived control-plane server may sweep machine-wide serve
|
|
37
|
+
* processes. Preview children deliberately run the same CLI on an ephemeral
|
|
38
|
+
* port, but must never classify their parent control plane (often detached
|
|
39
|
+
* with PPID 1 after a nohup/reload handoff) as an orphan and terminate it.
|
|
40
|
+
* Likewise, in-process test servers use port 0 and are never supervisors.
|
|
41
|
+
*/
|
|
42
|
+
export declare function shouldReapStrayServeProcesses(opts: Pick<ServeOptions, "port">, env?: NodeJS.ProcessEnv): boolean;
|
|
43
|
+
export interface ServerHandle {
|
|
44
|
+
url: string;
|
|
45
|
+
port: number;
|
|
46
|
+
close: (reason?: string) => Promise<void>;
|
|
47
|
+
index: LiveIndex;
|
|
48
|
+
}
|
|
49
|
+
export declare function startServer(opts?: ServeOptions): Promise<ServerHandle>;
|