@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,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-level tunnel app registry.
|
|
3
|
+
*
|
|
4
|
+
* `cloudflared` on a machine runs as ONE system service reading ONE ingress
|
|
5
|
+
* config — but each RepoOS repo checkout only knows about its own
|
|
6
|
+
* `repoos.toml [tunnel.apps.*]`. Without a shared machine view, the second
|
|
7
|
+
* repo to `repoos tunnel install` clobbers the first repo's ingress routes.
|
|
8
|
+
*
|
|
9
|
+
* This registry (`~/.config/repoos/tunnel-apps.toml` by default) is the
|
|
10
|
+
* union of every repo's published apps on this box, keyed by app name, each
|
|
11
|
+
* entry tagged with the repo checkout (`ownerRoot`) that contributed it.
|
|
12
|
+
* `repoos.toml [tunnel.apps.*]` in each repo stays the per-repo, git-tracked
|
|
13
|
+
* source of truth for that repo's own apps — this file is a derived,
|
|
14
|
+
* machine-local cache that `repoos tunnel install`/`start` render the actual
|
|
15
|
+
* `cloudflared` ingress from, so bringing up the service from ANY repo on the
|
|
16
|
+
* machine serves every app published by any repo.
|
|
17
|
+
*/
|
|
18
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { parseToml, tomlQuote } from "./tunnel.js";
|
|
22
|
+
export function emptyRegistry() {
|
|
23
|
+
return { tunnelId: "", tunnelName: "", domain: "", apps: {} };
|
|
24
|
+
}
|
|
25
|
+
/** Default location — one registry per machine, outside any repo. */
|
|
26
|
+
export function registryPath() {
|
|
27
|
+
return join(homedir(), ".config", "repoos", "tunnel-apps.toml");
|
|
28
|
+
}
|
|
29
|
+
export function parseRegistry(text) {
|
|
30
|
+
const root = parseToml(text);
|
|
31
|
+
const reg = emptyRegistry();
|
|
32
|
+
if (typeof root.tunnel_id === "string")
|
|
33
|
+
reg.tunnelId = root.tunnel_id;
|
|
34
|
+
if (typeof root.tunnel_name === "string")
|
|
35
|
+
reg.tunnelName = root.tunnel_name;
|
|
36
|
+
if (typeof root.domain === "string")
|
|
37
|
+
reg.domain = root.domain;
|
|
38
|
+
const rawApps = (root.apps ?? {});
|
|
39
|
+
for (const [name, a] of Object.entries(rawApps)) {
|
|
40
|
+
if (typeof a !== "object" || a === null)
|
|
41
|
+
continue;
|
|
42
|
+
const hostname = String(a.hostname ?? "");
|
|
43
|
+
const service = String(a.service ?? "");
|
|
44
|
+
// A corrupt/partial entry (hand-edited despite the "do not" header, or
|
|
45
|
+
// written by some future bug) must never reach `renderCloudflaredConfig`
|
|
46
|
+
// — an empty hostname/service would render a blank ingress rule into
|
|
47
|
+
// cloudflared's config. Drop it rather than pass it through silently.
|
|
48
|
+
if (!hostname || !service)
|
|
49
|
+
continue;
|
|
50
|
+
reg.apps[name] = {
|
|
51
|
+
hostname,
|
|
52
|
+
service,
|
|
53
|
+
access: Array.isArray(a.access) ? a.access.map(String) : [],
|
|
54
|
+
...(a.noAccess === true ? { noAccess: true } : {}),
|
|
55
|
+
ownerRoot: String(a.ownerRoot ?? ""),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return reg;
|
|
59
|
+
}
|
|
60
|
+
export function serializeRegistry(reg) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push("# Generated by repoos tunnel — machine-level registry of every repo's published apps on this box.");
|
|
63
|
+
lines.push("# Do not hand-edit; see docs/tunnel-registry.md.");
|
|
64
|
+
if (reg.tunnelId)
|
|
65
|
+
lines.push(`tunnel_id = ${tomlQuote(reg.tunnelId)}`);
|
|
66
|
+
if (reg.tunnelName)
|
|
67
|
+
lines.push(`tunnel_name = ${tomlQuote(reg.tunnelName)}`);
|
|
68
|
+
if (reg.domain)
|
|
69
|
+
lines.push(`domain = ${tomlQuote(reg.domain)}`);
|
|
70
|
+
for (const name of Object.keys(reg.apps).sort()) {
|
|
71
|
+
const app = reg.apps[name];
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push(`[apps.${name}]`);
|
|
74
|
+
lines.push(`hostname = ${tomlQuote(app.hostname)}`);
|
|
75
|
+
lines.push(`service = ${tomlQuote(app.service)}`);
|
|
76
|
+
lines.push(`access = [${app.access.map(tomlQuote).join(", ")}]`);
|
|
77
|
+
if (app.noAccess)
|
|
78
|
+
lines.push(`noAccess = true`);
|
|
79
|
+
lines.push(`ownerRoot = ${tomlQuote(app.ownerRoot)}`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n") + "\n";
|
|
82
|
+
}
|
|
83
|
+
/** Read the machine registry. Missing file reads as an empty registry. */
|
|
84
|
+
export function readRegistry(path = registryPath()) {
|
|
85
|
+
const text = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
86
|
+
return parseRegistry(text);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Persist the machine registry (0600 — same treatment as the derived ingress
|
|
90
|
+
* config). Writes to a sibling temp file and renames it over the real path
|
|
91
|
+
* (atomic on POSIX) so a concurrent reader (another repo's `tunnel list`, or
|
|
92
|
+
* this same read done mid-write) never observes a truncated/partial file.
|
|
93
|
+
*/
|
|
94
|
+
export function writeRegistry(reg, path = registryPath()) {
|
|
95
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
96
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
97
|
+
writeFileSync(tmp, serializeRegistry(reg), { encoding: "utf8", mode: 0o600 });
|
|
98
|
+
renameSync(tmp, path);
|
|
99
|
+
}
|
|
100
|
+
const LOCK_RETRY_MS = 25;
|
|
101
|
+
const LOCK_STALE_MS = 5_000;
|
|
102
|
+
/** Block the calling thread for `ms` without busy-spinning (a real syscall wait). */
|
|
103
|
+
function sleepSync(ms) {
|
|
104
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Hold an exclusive lock on the registry while running `fn`. Two `repoos
|
|
108
|
+
* tunnel` processes on the same machine — the exact scenario this registry
|
|
109
|
+
* exists for — can otherwise race a read-modify-write and silently drop one
|
|
110
|
+
* another's change (last write wins). A sibling `<path>.lock` file created
|
|
111
|
+
* with the atomic create-if-absent (`wx`) flag serves as a simple
|
|
112
|
+
* cross-process mutex: a lock older than {@link LOCK_STALE_MS} is assumed to
|
|
113
|
+
* be left behind by a crashed process and is stolen rather than waited on
|
|
114
|
+
* forever.
|
|
115
|
+
*/
|
|
116
|
+
export function withRegistryLock(path, fn) {
|
|
117
|
+
const lockPath = `${path}.lock`;
|
|
118
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
119
|
+
const start = Date.now();
|
|
120
|
+
for (;;) {
|
|
121
|
+
try {
|
|
122
|
+
closeSync(openSync(lockPath, "wx"));
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
if (e.code !== "EEXIST")
|
|
127
|
+
throw e;
|
|
128
|
+
if (Date.now() - start > LOCK_STALE_MS) {
|
|
129
|
+
try {
|
|
130
|
+
unlinkSync(lockPath);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// another waiter already stole/removed it — just retry the create
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
sleepSync(LOCK_RETRY_MS);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return fn();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(lockPath);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// already gone (shouldn't happen — we're the holder)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reconcile a repo's tunnel identity against the registry's. A machine runs
|
|
154
|
+
* ONE Cloudflare Tunnel, so if the registry already has an identity and this
|
|
155
|
+
* repo's disagrees, refuse rather than silently picking one — returns an
|
|
156
|
+
* error message. When the registry has no identity yet, adopts the repo's
|
|
157
|
+
* (mutates `reg` in place). No-ops when the repo itself isn't set up yet.
|
|
158
|
+
*/
|
|
159
|
+
export function reconcileIdentity(reg, tunnel) {
|
|
160
|
+
if (!tunnel.tunnelId)
|
|
161
|
+
return null;
|
|
162
|
+
if (!reg.tunnelId) {
|
|
163
|
+
reg.tunnelId = tunnel.tunnelId;
|
|
164
|
+
reg.tunnelName = tunnel.name;
|
|
165
|
+
if (!reg.domain)
|
|
166
|
+
reg.domain = tunnel.domain;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (reg.tunnelId !== tunnel.tunnelId) {
|
|
170
|
+
return (`This repo's tunnel (${tunnel.name || "unnamed"}, ${tunnel.tunnelId}) does not match ` +
|
|
171
|
+
`the machine's registered tunnel (${reg.tunnelName || "unnamed"}, ${reg.tunnelId}). ` +
|
|
172
|
+
`A machine runs ONE Cloudflare Tunnel — \`repoos tunnel setup\` reuses a tunnel by NAME, ` +
|
|
173
|
+
`so it won't join this one unless the names happen to match. To join it, edit this repo's ` +
|
|
174
|
+
`repoos.toml [tunnel] block by hand: set tunnel_id = ${tomlQuote(reg.tunnelId)}` +
|
|
175
|
+
(reg.tunnelName ? ` and name = ${tomlQuote(reg.tunnelName)}` : "") +
|
|
176
|
+
`, or resolve the conflict the other way in ${registryPath()}.`);
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
/** Add or replace a registry entry, tagging it with its owning repo. */
|
|
181
|
+
export function upsertApp(reg, name, app, ownerRoot) {
|
|
182
|
+
reg.apps[name] = { ...app, ownerRoot };
|
|
183
|
+
}
|
|
184
|
+
/** Remove a registry entry (no-ops when absent). */
|
|
185
|
+
export function removeApp(reg, name) {
|
|
186
|
+
delete reg.apps[name];
|
|
187
|
+
}
|
|
188
|
+
/** Strip `ownerRoot` back to plain `TunnelApp`s — the shape `renderCloudflaredConfig` expects. */
|
|
189
|
+
export function unionApps(reg) {
|
|
190
|
+
const out = {};
|
|
191
|
+
for (const [name, app] of Object.entries(reg.apps)) {
|
|
192
|
+
out[name] = {
|
|
193
|
+
hostname: app.hostname,
|
|
194
|
+
service: app.service,
|
|
195
|
+
access: app.access,
|
|
196
|
+
...(app.noAccess ? { noAccess: true } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Reconcile the registry against THIS repo's own `repoos.toml [tunnel.apps]`
|
|
203
|
+
* — still the git-tracked, hand-edited source of truth for the apps this
|
|
204
|
+
* repo publishes. Mutates `reg` in place and returns the names that were
|
|
205
|
+
* newly seeded (for the same one-time log line callers have always printed).
|
|
206
|
+
* Three cases, keyed entirely on `ownerRoot` so another repo's entries are
|
|
207
|
+
* never touched:
|
|
208
|
+
*
|
|
209
|
+
* - in `tunnel.apps`, missing from the registry → seed it (first run after
|
|
210
|
+
* upgrade, or a brand-new `create` on an otherwise-synced registry).
|
|
211
|
+
* - in `tunnel.apps` AND already registered under THIS repo's `ownerRoot`,
|
|
212
|
+
* but with different fields (hostname/port/access edited by hand in
|
|
213
|
+
* repoos.toml rather than through `repoos tunnel`) → overwrite the
|
|
214
|
+
* registry entry so it can't drift from the file that's supposed to be
|
|
215
|
+
* authoritative for it.
|
|
216
|
+
* - registered under THIS repo's `ownerRoot` but no longer in `tunnel.apps`
|
|
217
|
+
* (deleted by hand rather than via `repoos tunnel destroy`) → drop it,
|
|
218
|
+
* since this repo no longer declares it.
|
|
219
|
+
*
|
|
220
|
+
* An entry owned by a DIFFERENT repo is left alone in every case, even when
|
|
221
|
+
* this repo's own `tunnel.apps` happens to declare the same name (a
|
|
222
|
+
* pre-existing collision — surfaced to the user elsewhere, not silently
|
|
223
|
+
* resolved here).
|
|
224
|
+
*
|
|
225
|
+
* Deliberately per-app rather than gated on "registry is totally empty": on
|
|
226
|
+
* a machine with multiple PRE-EXISTING repos (each already publishing its
|
|
227
|
+
* own apps before this registry existed), each repo seeds its own apps into
|
|
228
|
+
* the registry the first time IT runs `create`/`install`/`start`, regardless
|
|
229
|
+
* of whether another repo got there first and seeded its own — that's
|
|
230
|
+
* exactly the scenario (celleris + dev on one tunnel) this registry exists
|
|
231
|
+
* to fix. For the common single-repo case this still behaves like a
|
|
232
|
+
* one-time full migration, since every app is missing on the first run and
|
|
233
|
+
* every run after that is a no-op unless repoos.toml itself changed.
|
|
234
|
+
*/
|
|
235
|
+
export function syncFromRepo(reg, tunnel, root) {
|
|
236
|
+
const seeded = [];
|
|
237
|
+
for (const [name, app] of Object.entries(tunnel.apps)) {
|
|
238
|
+
const existing = reg.apps[name];
|
|
239
|
+
if (!existing) {
|
|
240
|
+
reg.apps[name] = { ...app, ownerRoot: root };
|
|
241
|
+
seeded.push(name);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (existing.ownerRoot !== root)
|
|
245
|
+
continue; // another repo's entry — never touched here
|
|
246
|
+
const { ownerRoot: _ownerRoot, ...current } = existing;
|
|
247
|
+
if (JSON.stringify(current) !== JSON.stringify(app)) {
|
|
248
|
+
reg.apps[name] = { ...app, ownerRoot: root };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const [name, existing] of Object.entries(reg.apps)) {
|
|
252
|
+
if (existing.ownerRoot === root && !tunnel.apps[name]) {
|
|
253
|
+
delete reg.apps[name];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return seeded;
|
|
257
|
+
}
|
|
258
|
+
/** Every registry app, sorted by name, annotated with staleness. */
|
|
259
|
+
export function annotateStale(reg) {
|
|
260
|
+
return Object.entries(reg.apps)
|
|
261
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
262
|
+
.map(([name, app]) => ({
|
|
263
|
+
name,
|
|
264
|
+
app,
|
|
265
|
+
stale: !!app.ownerRoot && !existsSync(app.ownerRoot),
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { emptyTunnelConfig, renderCloudflaredConfig } from "./tunnel.js";
|
|
6
|
+
import { annotateStale, emptyRegistry, parseRegistry, readRegistry, reconcileIdentity, removeApp, serializeRegistry, syncFromRepo, unionApps, upsertApp, withRegistryLock, writeRegistry, } from "./tunnel-registry.js";
|
|
7
|
+
const dirs = [];
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
for (const d of dirs)
|
|
10
|
+
rmSync(d, { recursive: true, force: true });
|
|
11
|
+
dirs.length = 0;
|
|
12
|
+
});
|
|
13
|
+
function tmpDir(prefix) {
|
|
14
|
+
const d = mkdtempSync(join(tmpdir(), prefix));
|
|
15
|
+
dirs.push(d);
|
|
16
|
+
return d;
|
|
17
|
+
}
|
|
18
|
+
function tunnelWithApps(tunnelId, name, apps) {
|
|
19
|
+
return { ...emptyTunnelConfig(), tunnelId, name, apps };
|
|
20
|
+
}
|
|
21
|
+
describe("registry parse/serialize round-trip", () => {
|
|
22
|
+
it("round-trips identity and apps through TOML", () => {
|
|
23
|
+
const reg = {
|
|
24
|
+
tunnelId: "tunnel-abc",
|
|
25
|
+
tunnelName: "repoos-bee",
|
|
26
|
+
domain: "repoos.org",
|
|
27
|
+
apps: {
|
|
28
|
+
dashboard: {
|
|
29
|
+
hostname: "dashboard.repoos.org",
|
|
30
|
+
service: "http://localhost:3000",
|
|
31
|
+
access: ["alice@example.com"],
|
|
32
|
+
ownerRoot: "/repos/a",
|
|
33
|
+
},
|
|
34
|
+
dev: {
|
|
35
|
+
hostname: "dev.repoos.org",
|
|
36
|
+
service: "http://localhost:7171",
|
|
37
|
+
access: [],
|
|
38
|
+
noAccess: true,
|
|
39
|
+
ownerRoot: "/repos/b",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const parsed = parseRegistry(serializeRegistry(reg));
|
|
44
|
+
expect(parsed).toEqual(reg);
|
|
45
|
+
});
|
|
46
|
+
it("reads a missing file as an empty registry", () => {
|
|
47
|
+
const dir = tmpDir("repoos-registry-missing-");
|
|
48
|
+
const reg = readRegistry(join(dir, "does-not-exist.toml"));
|
|
49
|
+
expect(reg).toEqual(emptyRegistry());
|
|
50
|
+
});
|
|
51
|
+
it("persists and re-reads via an explicit path", () => {
|
|
52
|
+
const dir = tmpDir("repoos-registry-io-");
|
|
53
|
+
const path = join(dir, "tunnel-apps.toml");
|
|
54
|
+
const reg = emptyRegistry();
|
|
55
|
+
reg.tunnelId = "t-1";
|
|
56
|
+
reg.tunnelName = "repoos-box";
|
|
57
|
+
upsertApp(reg, "dev", { hostname: "dev.repoos.org", service: "http://localhost:7171", access: [] }, "/repos/a");
|
|
58
|
+
writeRegistry(reg, path);
|
|
59
|
+
expect(readRegistry(path)).toEqual(reg);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("reconcileIdentity", () => {
|
|
63
|
+
it("adopts the repo's identity when the registry has none yet", () => {
|
|
64
|
+
const reg = emptyRegistry();
|
|
65
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {});
|
|
66
|
+
tunnel.domain = "repoos.org";
|
|
67
|
+
const conflict = reconcileIdentity(reg, tunnel);
|
|
68
|
+
expect(conflict).toBeNull();
|
|
69
|
+
expect(reg.tunnelId).toBe("tunnel-1");
|
|
70
|
+
expect(reg.tunnelName).toBe("repoos-box");
|
|
71
|
+
expect(reg.domain).toBe("repoos.org");
|
|
72
|
+
});
|
|
73
|
+
it("no-ops when the repo isn't set up yet (no tunnelId)", () => {
|
|
74
|
+
const reg = emptyRegistry();
|
|
75
|
+
reg.tunnelId = "existing";
|
|
76
|
+
const tunnel = tunnelWithApps("", "", {});
|
|
77
|
+
expect(reconcileIdentity(reg, tunnel)).toBeNull();
|
|
78
|
+
expect(reg.tunnelId).toBe("existing");
|
|
79
|
+
});
|
|
80
|
+
it("passes silently when the repo's identity matches the registry's", () => {
|
|
81
|
+
const reg = emptyRegistry();
|
|
82
|
+
reg.tunnelId = "tunnel-1";
|
|
83
|
+
reg.tunnelName = "repoos-box";
|
|
84
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {});
|
|
85
|
+
expect(reconcileIdentity(reg, tunnel)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
it("refuses (never silently picks one) when tunnelIds disagree", () => {
|
|
88
|
+
const reg = emptyRegistry();
|
|
89
|
+
reg.tunnelId = "tunnel-1";
|
|
90
|
+
reg.tunnelName = "repoos-box";
|
|
91
|
+
const tunnel = tunnelWithApps("tunnel-2", "repoos-other", {});
|
|
92
|
+
const conflict = reconcileIdentity(reg, tunnel);
|
|
93
|
+
expect(conflict).toMatch(/does not match/);
|
|
94
|
+
expect(conflict).toContain("tunnel-1");
|
|
95
|
+
expect(conflict).toContain("tunnel-2");
|
|
96
|
+
// registry identity must be left untouched
|
|
97
|
+
expect(reg.tunnelId).toBe("tunnel-1");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("syncFromRepo (seed / update / drop)", () => {
|
|
101
|
+
it("seeds every app from an empty registry (single pre-existing repo)", () => {
|
|
102
|
+
const reg = emptyRegistry();
|
|
103
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
104
|
+
dashboard: { hostname: "dashboard.repoos.org", service: "http://localhost:3000", access: [] },
|
|
105
|
+
api: { hostname: "api.repoos.org", service: "http://localhost:4000", access: [] },
|
|
106
|
+
});
|
|
107
|
+
const seeded = syncFromRepo(reg, tunnel, "/repos/a");
|
|
108
|
+
expect(seeded.sort()).toEqual(["api", "dashboard"]);
|
|
109
|
+
expect(reg.apps.dashboard).toMatchObject({
|
|
110
|
+
hostname: "dashboard.repoos.org",
|
|
111
|
+
ownerRoot: "/repos/a",
|
|
112
|
+
});
|
|
113
|
+
expect(reg.apps.api).toMatchObject({ hostname: "api.repoos.org", ownerRoot: "/repos/a" });
|
|
114
|
+
});
|
|
115
|
+
it("is a no-op on a second call — never re-seeds or duplicates", () => {
|
|
116
|
+
const reg = emptyRegistry();
|
|
117
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
118
|
+
dashboard: { hostname: "dashboard.repoos.org", service: "http://localhost:3000", access: [] },
|
|
119
|
+
});
|
|
120
|
+
syncFromRepo(reg, tunnel, "/repos/a");
|
|
121
|
+
const secondPass = syncFromRepo(reg, tunnel, "/repos/a");
|
|
122
|
+
expect(secondPass).toEqual([]);
|
|
123
|
+
expect(Object.keys(reg.apps)).toEqual(["dashboard"]);
|
|
124
|
+
});
|
|
125
|
+
it("seeds a SECOND pre-existing repo's own apps without touching the first repo's", () => {
|
|
126
|
+
// The exact "celleris + dev" bug scenario: two repos already publishing
|
|
127
|
+
// apps before this registry existed. Repo A migrates first...
|
|
128
|
+
const reg = emptyRegistry();
|
|
129
|
+
const repoA = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
130
|
+
celleris: { hostname: "celleris.repoos.org", service: "http://localhost:3000", access: [] },
|
|
131
|
+
});
|
|
132
|
+
syncFromRepo(reg, repoA, "/repos/celleris");
|
|
133
|
+
// ...then repo B, independently, also has its own pre-existing app that
|
|
134
|
+
// was never in the registry. It must get folded in too, not skipped
|
|
135
|
+
// just because the registry is no longer empty.
|
|
136
|
+
const repoB = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
137
|
+
dev: { hostname: "dev.repoos.org", service: "http://localhost:7171", access: [] },
|
|
138
|
+
});
|
|
139
|
+
const seeded = syncFromRepo(reg, repoB, "/repos/dev");
|
|
140
|
+
expect(seeded).toEqual(["dev"]);
|
|
141
|
+
expect(reg.apps.celleris).toMatchObject({ ownerRoot: "/repos/celleris" });
|
|
142
|
+
expect(reg.apps.dev).toMatchObject({ ownerRoot: "/repos/dev" });
|
|
143
|
+
});
|
|
144
|
+
it("never overwrites an entry owned by a different repo", () => {
|
|
145
|
+
const reg = emptyRegistry();
|
|
146
|
+
upsertApp(reg, "dev", { hostname: "dev.repoos.org", service: "http://localhost:7171", access: [] }, "/repos/a");
|
|
147
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
148
|
+
dev: { hostname: "dev-imposter.repoos.org", service: "http://localhost:9999", access: [] },
|
|
149
|
+
});
|
|
150
|
+
const seeded = syncFromRepo(reg, tunnel, "/repos/b");
|
|
151
|
+
expect(seeded).toEqual([]);
|
|
152
|
+
expect(reg.apps.dev).toMatchObject({ hostname: "dev.repoos.org", ownerRoot: "/repos/a" });
|
|
153
|
+
});
|
|
154
|
+
it("re-upserts an entry it owns when repoos.toml changed (port/hostname edited by hand)", () => {
|
|
155
|
+
const reg = emptyRegistry();
|
|
156
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
157
|
+
dashboard: { hostname: "dashboard.repoos.org", service: "http://localhost:3000", access: [] },
|
|
158
|
+
});
|
|
159
|
+
syncFromRepo(reg, tunnel, "/repos/a");
|
|
160
|
+
// repoos.toml is hand-edited (or re-created via `create`) to a new port —
|
|
161
|
+
// a stale registry copy must not survive a second sync from its own repo.
|
|
162
|
+
tunnel.apps.dashboard.service = "http://localhost:4000";
|
|
163
|
+
const seeded = syncFromRepo(reg, tunnel, "/repos/a");
|
|
164
|
+
expect(seeded).toEqual([]); // not newly seeded — updated in place
|
|
165
|
+
expect(reg.apps.dashboard).toMatchObject({
|
|
166
|
+
service: "http://localhost:4000",
|
|
167
|
+
ownerRoot: "/repos/a",
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
it("drops an entry it owns once repoos.toml no longer declares it", () => {
|
|
171
|
+
const reg = emptyRegistry();
|
|
172
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
173
|
+
dashboard: { hostname: "dashboard.repoos.org", service: "http://localhost:3000", access: [] },
|
|
174
|
+
});
|
|
175
|
+
syncFromRepo(reg, tunnel, "/repos/a");
|
|
176
|
+
delete tunnel.apps.dashboard; // hand-deleted from repoos.toml, not via `destroy`
|
|
177
|
+
syncFromRepo(reg, tunnel, "/repos/a");
|
|
178
|
+
expect(reg.apps.dashboard).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
it("never touches or drops another repo's entries even when this repo's toml is empty", () => {
|
|
181
|
+
const reg = emptyRegistry();
|
|
182
|
+
upsertApp(reg, "other", { hostname: "other.repoos.org", service: "http://localhost:9000", access: [] }, "/repos/b");
|
|
183
|
+
const tunnel = tunnelWithApps("tunnel-1", "repoos-box", {});
|
|
184
|
+
syncFromRepo(reg, tunnel, "/repos/a");
|
|
185
|
+
expect(reg.apps.other).toMatchObject({ ownerRoot: "/repos/b" });
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe("parseRegistry — corrupt entries", () => {
|
|
189
|
+
it("drops an entry with an empty hostname or service rather than rendering a blank rule", () => {
|
|
190
|
+
const text = [
|
|
191
|
+
'tunnel_id = "tunnel-1"',
|
|
192
|
+
"",
|
|
193
|
+
"[apps.good]",
|
|
194
|
+
'hostname = "good.repoos.org"',
|
|
195
|
+
'service = "http://localhost:3000"',
|
|
196
|
+
"access = []",
|
|
197
|
+
'ownerRoot = "/repos/a"',
|
|
198
|
+
"",
|
|
199
|
+
"[apps.corrupt]",
|
|
200
|
+
'hostname = ""',
|
|
201
|
+
'service = "http://localhost:4000"',
|
|
202
|
+
"access = []",
|
|
203
|
+
'ownerRoot = "/repos/a"',
|
|
204
|
+
].join("\n");
|
|
205
|
+
const reg = parseRegistry(text);
|
|
206
|
+
expect(Object.keys(reg.apps)).toEqual(["good"]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe("withRegistryLock", () => {
|
|
210
|
+
it("runs the callback and releases the lock file afterward", () => {
|
|
211
|
+
const dir = tmpDir("repoos-registry-lock-");
|
|
212
|
+
const path = join(dir, "tunnel-apps.toml");
|
|
213
|
+
const result = withRegistryLock(path, () => 42);
|
|
214
|
+
expect(result).toBe(42);
|
|
215
|
+
expect(existsSync(`${path}.lock`)).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
it("releases the lock even when the callback throws", () => {
|
|
218
|
+
const dir = tmpDir("repoos-registry-lock-throw-");
|
|
219
|
+
const path = join(dir, "tunnel-apps.toml");
|
|
220
|
+
expect(() => withRegistryLock(path, () => {
|
|
221
|
+
throw new Error("boom");
|
|
222
|
+
})).toThrow("boom");
|
|
223
|
+
expect(existsSync(`${path}.lock`)).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
it("serializes a read-modify-write so neither writer's entry is lost", () => {
|
|
226
|
+
// The exact race this lock exists to prevent: two "processes" (here,
|
|
227
|
+
// just two sequential calls, since a true concurrent-process test would
|
|
228
|
+
// need real subprocesses) each read, add their own app, and write —
|
|
229
|
+
// without the lock this pattern is exactly how a naive read/mutate/write
|
|
230
|
+
// loses an entry. Locking each one's full cycle assures nothing overlaps.
|
|
231
|
+
const dir = tmpDir("repoos-registry-lock-race-");
|
|
232
|
+
const path = join(dir, "tunnel-apps.toml");
|
|
233
|
+
writeRegistry(emptyRegistry(), path);
|
|
234
|
+
function addOne(name, root) {
|
|
235
|
+
withRegistryLock(path, () => {
|
|
236
|
+
const reg = readRegistry(path);
|
|
237
|
+
upsertApp(reg, name, { hostname: `${name}.repoos.org`, service: "http://localhost:1", access: [] }, root);
|
|
238
|
+
writeRegistry(reg, path);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
addOne("a", "/repos/a");
|
|
242
|
+
addOne("b", "/repos/b");
|
|
243
|
+
const final = readRegistry(path);
|
|
244
|
+
expect(Object.keys(final.apps).sort()).toEqual(["a", "b"]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe("unionApps + renderCloudflaredConfig (the actual fix)", () => {
|
|
248
|
+
it("two repos' apps both survive a single render — neither clobbers the other", () => {
|
|
249
|
+
const reg = emptyRegistry();
|
|
250
|
+
reg.tunnelId = "tunnel-1";
|
|
251
|
+
upsertApp(reg, "celleris", { hostname: "celleris.repoos.org", service: "http://localhost:3000", access: [] }, "/repos/celleris");
|
|
252
|
+
upsertApp(reg, "dev", { hostname: "dev.repoos.org", service: "http://localhost:7171", access: [] }, "/repos/dev");
|
|
253
|
+
const yaml = renderCloudflaredConfig({ ...emptyTunnelConfig(), tunnelId: reg.tunnelId, apps: unionApps(reg) }, "/home/user/.cloudflared/tunnel-1.json");
|
|
254
|
+
expect(yaml).toContain("hostname: celleris.repoos.org");
|
|
255
|
+
expect(yaml).toContain("hostname: dev.repoos.org");
|
|
256
|
+
expect(yaml.trim().endsWith("- service: http_status:404")).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
it("installing from repo B after repo A no longer drops repo A's route", () => {
|
|
259
|
+
// Simulates: repo A creates its app (registry seeded with A's app), then
|
|
260
|
+
// repo B runs `install` — B's own syncFromRepo pass must not remove A's
|
|
261
|
+
// entry, and the union render must still contain both.
|
|
262
|
+
const reg = emptyRegistry();
|
|
263
|
+
reg.tunnelId = "tunnel-1";
|
|
264
|
+
const repoA = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
265
|
+
a: { hostname: "a.repoos.org", service: "http://localhost:1111", access: [] },
|
|
266
|
+
});
|
|
267
|
+
syncFromRepo(reg, repoA, "/repos/a");
|
|
268
|
+
const repoB = tunnelWithApps("tunnel-1", "repoos-box", {
|
|
269
|
+
b: { hostname: "b.repoos.org", service: "http://localhost:2222", access: [] },
|
|
270
|
+
});
|
|
271
|
+
syncFromRepo(reg, repoB, "/repos/b");
|
|
272
|
+
const apps = unionApps(reg);
|
|
273
|
+
expect(Object.keys(apps).sort()).toEqual(["a", "b"]);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
describe("removeApp", () => {
|
|
277
|
+
it("removes an entry and leaves the rest untouched", () => {
|
|
278
|
+
const reg = emptyRegistry();
|
|
279
|
+
upsertApp(reg, "a", { hostname: "a.repoos.org", service: "http://localhost:1", access: [] }, "/repos/a");
|
|
280
|
+
upsertApp(reg, "b", { hostname: "b.repoos.org", service: "http://localhost:2", access: [] }, "/repos/b");
|
|
281
|
+
removeApp(reg, "a");
|
|
282
|
+
expect(Object.keys(reg.apps)).toEqual(["b"]);
|
|
283
|
+
});
|
|
284
|
+
it("no-ops on an absent name", () => {
|
|
285
|
+
const reg = emptyRegistry();
|
|
286
|
+
expect(() => removeApp(reg, "missing")).not.toThrow();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
describe("annotateStale", () => {
|
|
290
|
+
it("flags entries whose ownerRoot no longer exists on disk", () => {
|
|
291
|
+
const dir = tmpDir("repoos-registry-stale-");
|
|
292
|
+
const liveRoot = join(dir, "live-repo");
|
|
293
|
+
mkdirSync(liveRoot);
|
|
294
|
+
const goneRoot = join(dir, "gone-repo"); // never created
|
|
295
|
+
const reg = emptyRegistry();
|
|
296
|
+
upsertApp(reg, "live", { hostname: "live.repoos.org", service: "http://localhost:1", access: [] }, liveRoot);
|
|
297
|
+
upsertApp(reg, "gone", { hostname: "gone.repoos.org", service: "http://localhost:2", access: [] }, goneRoot);
|
|
298
|
+
const rows = annotateStale(reg);
|
|
299
|
+
const byName = Object.fromEntries(rows.map((r) => [r.name, r.stale]));
|
|
300
|
+
expect(byName.live).toBe(false);
|
|
301
|
+
expect(byName.gone).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
it("sorts by app name", () => {
|
|
304
|
+
const reg = emptyRegistry();
|
|
305
|
+
upsertApp(reg, "zebra", { hostname: "z.repoos.org", service: "http://localhost:1", access: [] }, "/repos/z");
|
|
306
|
+
upsertApp(reg, "alpha", { hostname: "a.repoos.org", service: "http://localhost:2", access: [] }, "/repos/a");
|
|
307
|
+
expect(annotateStale(reg).map((r) => r.name)).toEqual(["alpha", "zebra"]);
|
|
308
|
+
});
|
|
309
|
+
});
|