@repo-os/repoos 0.5.57 → 0.5.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +9 -1
  2. package/dist/.build-info.json +2 -2
  3. package/dist/.build-stamp.json +1 -1
  4. package/dist/cli/index.js +20 -19
  5. package/dist/commands/check.js +5 -0
  6. package/dist/commands/init.d.ts +7 -1
  7. package/dist/commands/init.js +147 -13
  8. package/dist/commands/serve-banner.test.d.ts +1 -0
  9. package/dist/commands/serve-banner.test.js +26 -0
  10. package/dist/commands/serve.d.ts +12 -0
  11. package/dist/commands/serve.js +72 -21
  12. package/dist/commands/shot.d.ts +31 -0
  13. package/dist/commands/shot.js +427 -0
  14. package/dist/commands/tasks.js +2 -2
  15. package/dist/core/agent-compatibility.json +20 -0
  16. package/dist/core/agent-contract.js +67 -11
  17. package/dist/core/agent-updates.js +3 -0
  18. package/dist/core/areas.d.ts +102 -0
  19. package/dist/core/areas.js +145 -0
  20. package/dist/core/canary-repo.d.ts +12 -0
  21. package/dist/core/canary-repo.js +111 -0
  22. package/dist/core/canary-repo.test.d.ts +1 -0
  23. package/dist/core/canary-repo.test.js +71 -0
  24. package/dist/core/canary.d.ts +21 -7
  25. package/dist/core/canary.js +80 -12
  26. package/dist/core/canary.test.d.ts +1 -0
  27. package/dist/core/canary.test.js +73 -0
  28. package/dist/core/check-failure-summary.d.ts +11 -0
  29. package/dist/core/check-failure-summary.js +29 -0
  30. package/dist/core/check-store.d.ts +5 -0
  31. package/dist/core/check-store.js +29 -14
  32. package/dist/core/config.d.ts +13 -2
  33. package/dist/core/config.js +90 -0
  34. package/dist/core/context-pack.js +9 -4
  35. package/dist/core/detect.js +13 -0
  36. package/dist/core/doctor.js +106 -0
  37. package/dist/core/git-activity.d.ts +35 -0
  38. package/dist/core/git-activity.js +26 -0
  39. package/dist/core/git.d.ts +53 -6
  40. package/dist/core/git.js +135 -27
  41. package/dist/core/models.js +27 -1
  42. package/dist/core/repoos.d.ts +6 -1
  43. package/dist/core/repoos.js +20 -1
  44. package/dist/core/shot-targets.d.ts +90 -0
  45. package/dist/core/shot-targets.js +191 -0
  46. package/dist/core/tailscale.d.ts +15 -0
  47. package/dist/core/tailscale.js +76 -1
  48. package/dist/core/task.js +35 -2
  49. package/dist/core/types.d.ts +45 -1
  50. package/dist/core/version.d.ts +2 -0
  51. package/dist/core/version.js +16 -0
  52. package/dist/server/agents.d.ts +69 -3
  53. package/dist/server/agents.js +280 -9
  54. package/dist/server/area-migration.d.ts +24 -0
  55. package/dist/server/area-migration.js +131 -0
  56. package/dist/server/freeform.d.ts +4 -1
  57. package/dist/server/freeform.js +52 -33
  58. package/dist/server/handoff.js +5 -0
  59. package/dist/server/integration-orchestrator.js +4 -0
  60. package/dist/server/live-index.d.ts +11 -0
  61. package/dist/server/live-index.js +1 -0
  62. package/dist/server/local-token.d.ts +10 -0
  63. package/dist/server/local-token.js +62 -0
  64. package/dist/server/preview.js +30 -10
  65. package/dist/server/repo-status.d.ts +95 -0
  66. package/dist/server/repo-status.js +313 -0
  67. package/dist/server/review.js +1 -1
  68. package/dist/server/routes/config.js +91 -1
  69. package/dist/server/routes/info.js +4 -2
  70. package/dist/server/routes/repo-log.d.ts +11 -0
  71. package/dist/server/routes/repo-log.js +15 -0
  72. package/dist/server/routes/tasks.d.ts +3 -0
  73. package/dist/server/routes/tasks.js +67 -3
  74. package/dist/server/server.js +119 -30
  75. package/dist/server/shot-context.d.ts +21 -0
  76. package/dist/server/shot-context.js +30 -0
  77. package/dist/server/shots.d.ts +42 -0
  78. package/dist/server/shots.js +170 -0
  79. package/dist/server/watcher.d.ts +38 -0
  80. package/dist/server/watcher.js +109 -2
  81. package/dist/server/write.d.ts +6 -1
  82. package/dist/server/write.js +20 -2
  83. package/dist/ui/assets/{AgentModelModal-Ced4h4sp.js → AgentModelModal-DzP177gt.js} +1 -1
  84. package/dist/ui/assets/{AgentsView-fJc8ZQRN.js → AgentsView-CA6vNzB_.js} +1 -1
  85. package/dist/ui/assets/{ChecksView-DK2wcClf.css → ChecksView-DXaRYvbW.css} +1 -1
  86. package/dist/ui/assets/ChecksView-k-3IMvCn.js +0 -0
  87. package/dist/ui/assets/{ContextView-CTGAeIvz.css → ContextView-CE7UCqYM.css} +1 -1
  88. package/dist/ui/assets/ContextView-DIn7440o.js +4 -0
  89. package/dist/ui/assets/{DashboardView-BlSDOM4G.js → DashboardView-Dycyb_k9.js} +1 -1
  90. package/dist/ui/assets/{DiffView-nH7u0JwJ.js → DiffView-DMGq4a2J.js} +1 -1
  91. package/dist/ui/assets/{InputsView-BZ3T5jsY.js → InputsView-DLdABoFS.js} +1 -1
  92. package/dist/ui/assets/{LoginView-CX6blOC4.js → LoginView-DJRB0cU8.js} +1 -1
  93. package/dist/ui/assets/ReleasesView-CURY5err.css +1 -0
  94. package/dist/ui/assets/{ReleasesView-C5PaQX6n.js → ReleasesView-UavG0AWn.js} +2 -2
  95. package/dist/ui/assets/SettingsView-B0CnWYfh.js +4 -0
  96. package/dist/ui/assets/StoriesView-D1Rm_igY.js +1 -0
  97. package/dist/ui/assets/{VoiceDictate-CifZUkzt.js → VoiceDictate-CCJfAGJk.js} +1 -1
  98. package/dist/ui/assets/WorkView-DoN6rK5X.js +2 -0
  99. package/dist/ui/assets/{abnfDiagram-VCTEODGH-CxKkcu03.js → abnfDiagram-VCTEODGH-DNJeaBue.js} +1 -1
  100. package/dist/ui/assets/architecture-7GRP2DOG-CE8k1klh.js +1 -0
  101. package/dist/ui/assets/{architectureDiagram-5GKGNRK7-B7yFyWIW.js → architectureDiagram-5GKGNRK7-CFYttSDP.js} +1 -1
  102. package/dist/ui/assets/{blockDiagram-I7D4REHJ-Dqn5piZv.js → blockDiagram-I7D4REHJ-D4x6ObYp.js} +1 -1
  103. package/dist/ui/assets/{c4Diagram-7LVT6UL2-BDourwGX.js → c4Diagram-7LVT6UL2-CxtP3-uN.js} +1 -1
  104. package/dist/ui/assets/channel-YsXE3acZ.js +1 -0
  105. package/dist/ui/assets/{chunk-4HAMMTFA-BGxQ0vmo.js → chunk-4HAMMTFA-CuuPSpo5.js} +1 -1
  106. package/dist/ui/assets/{chunk-75Z2AOVW-CoD1HiQQ.js → chunk-75Z2AOVW-DA5zPl9A.js} +1 -1
  107. package/dist/ui/assets/{chunk-DU6HZSFF-WQx_PGDE.js → chunk-DU6HZSFF-_OVfIMg9.js} +1 -1
  108. package/dist/ui/assets/{chunk-F27PBJKO-BmtJFWYx.js → chunk-F27PBJKO-DmAfAXfg.js} +1 -1
  109. package/dist/ui/assets/{chunk-GMAD6QVW-kRmn1tqd.js → chunk-GMAD6QVW-7rYiJcfH.js} +1 -1
  110. package/dist/ui/assets/{chunk-GVQU2GXP-C2SMD4HM.js → chunk-GVQU2GXP-DUvM8o5c.js} +1 -1
  111. package/dist/ui/assets/{chunk-IMKFNOWR-DRm68yqu.js → chunk-IMKFNOWR-Dug5EN3L.js} +1 -1
  112. package/dist/ui/assets/{chunk-L3NEJ4N5-Bdd0UO7i.js → chunk-L3NEJ4N5-B__JCALV.js} +1 -1
  113. package/dist/ui/assets/{chunk-OSK3NFVY-DzFP_5D1.js → chunk-OSK3NFVY-DNSy5va-.js} +1 -1
  114. package/dist/ui/assets/{chunk-P2QGCYS3-CxTbTe1z.js → chunk-P2QGCYS3-jZiRM4I9.js} +1 -1
  115. package/dist/ui/assets/{chunk-POPQ4Y6H-1sMFzZlT.js → chunk-POPQ4Y6H-DUE_WVHL.js} +1 -1
  116. package/dist/ui/assets/{chunk-PWAF6VOD-qwo7mGDG.js → chunk-PWAF6VOD-CbMevpLH.js} +1 -1
  117. package/dist/ui/assets/{chunk-SHT3W25Y-t7arLGqg.js → chunk-SHT3W25Y-MuvL_buO.js} +1 -1
  118. package/dist/ui/assets/{chunk-SVP7TREG-CDGoMrDa.js → chunk-SVP7TREG-lOHx_pKr.js} +1 -1
  119. package/dist/ui/assets/{chunk-TICWLB2K-DdCRvOgF.js → chunk-TICWLB2K-11z-MDx9.js} +1 -1
  120. package/dist/ui/assets/classDiagram-ZZMXUADV-DpeLlC2e.js +1 -0
  121. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-DpeLlC2e.js +1 -0
  122. package/dist/ui/assets/config-j80hL8dN.js +1 -0
  123. package/dist/ui/assets/{cynefin-OW5HDTMX-Dphr1hkJ.js → cynefin-OW5HDTMX-D8Tbtioe.js} +1 -1
  124. package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-BmNfqRyY.js → cynefinDiagram-5FMLGOSQ-DykcYZa1.js} +1 -1
  125. package/dist/ui/assets/{dagre-GXQ25YYZ-0__5BzkZ.js → dagre-GXQ25YYZ-BCjGpohz.js} +1 -1
  126. package/dist/ui/assets/{diagram-S7CK7UJ4-BCSWodGD.js → diagram-S7CK7UJ4-DrO95VpH.js} +1 -1
  127. package/dist/ui/assets/{diagram-UQ7AKVKN-DNY_hqmH.js → diagram-UQ7AKVKN-Bl2XIcjq.js} +1 -1
  128. package/dist/ui/assets/{diagram-VSXAHHWV-DgEipWnL.js → diagram-VSXAHHWV-D88EBlA1.js} +1 -1
  129. package/dist/ui/assets/{diagram-VX7I27RA-CXqwFFDY.js → diagram-VX7I27RA-Dd9fRIfD.js} +1 -1
  130. package/dist/ui/assets/{diagram-Z3DM3KII-DqGMJic1.js → diagram-Z3DM3KII-CDvHYGee.js} +1 -1
  131. package/dist/ui/assets/{ebnfDiagram-PWID7BFC-O7OLADLR.js → ebnfDiagram-PWID7BFC-ROLkMsCj.js} +1 -1
  132. package/dist/ui/assets/{erDiagram-RLTQ6QDP-BLybCuiJ.js → erDiagram-RLTQ6QDP-hgEdVaDk.js} +1 -1
  133. package/dist/ui/assets/eventmodeling-NTZA5JFV-D4JrdfJN.js +1 -0
  134. package/dist/ui/assets/flowDiagram-HODETNUW-BuBvXaQp.js +1 -0
  135. package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-PP7LUoJk.js → ganttDiagram-EL5Y4UJY-DyNYXsDm.js} +1 -1
  136. package/dist/ui/assets/{gitGraph-4MIJSDKK-jLh6GWq3.js → gitGraph-4MIJSDKK-DrwHooXn.js} +1 -1
  137. package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-BpQqVPAf.js → gitGraphDiagram-WWUBYQGX-_DOOuskB.js} +1 -1
  138. package/dist/ui/assets/index-BqREmSY8.js +46 -0
  139. package/dist/ui/assets/index-RS-D6B89.css +1 -0
  140. package/dist/ui/assets/{info-A6RAGUB7-MKojffEb.js → info-A6RAGUB7-Bp4WG9KV.js} +1 -1
  141. package/dist/ui/assets/{infoDiagram-27XIBGKW-CDsY5bPn.js → infoDiagram-27XIBGKW-Bp9BY3BX.js} +1 -1
  142. package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-DlkSFpW_.js → ishikawaDiagram-5VMMS53U-Dmxsu_I6.js} +1 -1
  143. package/dist/ui/assets/{journeyDiagram-3NMN7TZE-BQerIcFQ.js → journeyDiagram-3NMN7TZE-DyCFShcT.js} +1 -1
  144. package/dist/ui/assets/{kanban-definition-UXKFOSKX-DcOUAWHk.js → kanban-definition-UXKFOSKX-BY6mL0nV.js} +1 -1
  145. package/dist/ui/assets/{line-CWe6TPQq.js → line-CFvZJFGP.js} +1 -1
  146. package/dist/ui/assets/{mermaid-parser.core-B6lGkbHf.js → mermaid-parser.core-BlWQycY8.js} +3 -3
  147. package/dist/ui/assets/{mermaid.core-usGEYfph.js → mermaid.core-kuwSwalt.js} +4 -4
  148. package/dist/ui/assets/{mindmap-definition-YA3MSWOX-CRr1hJ6k.js → mindmap-definition-YA3MSWOX-Da8T6fiA.js} +1 -1
  149. package/dist/ui/assets/{packet-AYTQ26CC-Vrb5eklN.js → packet-AYTQ26CC-uA_F87fL.js} +1 -1
  150. package/dist/ui/assets/{pegDiagram-XKGWAZYB-DWgbaT0F.js → pegDiagram-XKGWAZYB-DlT4Tua4.js} +1 -1
  151. package/dist/ui/assets/{pie-WAS4IAKB-EBSi28Ou.js → pie-WAS4IAKB-CMlL534n.js} +1 -1
  152. package/dist/ui/assets/{pieDiagram-E7YTZNPT-CY7kTdf6.js → pieDiagram-E7YTZNPT-Dc1lWF3q.js} +1 -1
  153. package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-DapXMfW1.js → quadrantDiagram-AXDQQJYC-BqVcYO8d.js} +1 -1
  154. package/dist/ui/assets/{radar-RG4KPBEZ-B4g4t9_Y.js → radar-RG4KPBEZ-Dolxtcln.js} +1 -1
  155. package/dist/ui/assets/{railroad-74A4TZTK-DbL2DZlM.js → railroad-74A4TZTK-WQOQGRUO.js} +1 -1
  156. package/dist/ui/assets/railroad-abnf-HS5TGJTU-oeSeN2iQ.js +1 -0
  157. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-CXhtQ5k4.js +1 -0
  158. package/dist/ui/assets/railroad-peg-WCYAUIDC-CZQ_zKCz.js +1 -0
  159. package/dist/ui/assets/{railroadDiagram-O6MQD6OU-Bq4-JY0m.js → railroadDiagram-O6MQD6OU-BvlFISOo.js} +1 -1
  160. package/dist/ui/assets/repo-CPWzR4oq.js +1 -0
  161. package/dist/ui/assets/{requirementDiagram-BXWQKSXE-CVI43xef.js → requirementDiagram-BXWQKSXE-BYKoXbW1.js} +1 -1
  162. package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-6igeb8Iu.js → sankeyDiagram-P5KCCOFB-ClkaPNXM.js} +1 -1
  163. package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-LYhNaHRI.js → sequenceDiagram-WJ2MYXX4-CnLXWS23.js} +1 -1
  164. package/dist/ui/assets/{stateDiagram-D77RDMKH-VQcHODiY.js → stateDiagram-D77RDMKH-SURchdbJ.js} +1 -1
  165. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-3QeI6zO7.js +1 -0
  166. package/dist/ui/assets/{swimlanes-42K2YHIH-C5U1l1mo.js → swimlanes-42K2YHIH-ETHZRcl1.js} +1 -1
  167. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-2inBpL8s.js +8 -0
  168. package/dist/ui/assets/{timeline-definition-24CTP7MA-C8YMVaeD.js → timeline-definition-24CTP7MA-CzmcMnEC.js} +1 -1
  169. package/dist/ui/assets/{treeView-Q6P3EWNA-B4Opgu8L.js → treeView-Q6P3EWNA-DpYbsVnL.js} +1 -1
  170. package/dist/ui/assets/{treemap-WGGIJYW6-BWpYq891.js → treemap-WGGIJYW6-CprmPbH2.js} +1 -1
  171. package/dist/ui/assets/{vennDiagram-4TSXK5OY-BZ7_jhEK.js → vennDiagram-4TSXK5OY-h5jxvp23.js} +1 -1
  172. package/dist/ui/assets/{wardley-WFR3VGLG-DSF6DKt8.js → wardley-WFR3VGLG-C9hsSVZz.js} +1 -1
  173. package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-ChfzNSSI.js → wardleyDiagram-VM6X3IG4-D58m5Tfi.js} +1 -1
  174. package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-CwDvFw4R.js → xychartDiagram-S5SC5T6Z-WeCot50K.js} +1 -1
  175. package/dist/ui/index.html +6 -6
  176. package/dist/ui/sw.js +2 -2
  177. package/package.json +1 -1
  178. package/dist/ui/assets/ChecksView-CpGuGL1P.js +0 -0
  179. package/dist/ui/assets/ContextView-C2ElRI0s.js +0 -4
  180. package/dist/ui/assets/ReleasesView-DaX_TFoT.css +0 -1
  181. package/dist/ui/assets/SettingsView-C1xiIbbq.js +0 -4
  182. package/dist/ui/assets/StoriesView-BCJnrDOc.js +0 -1
  183. package/dist/ui/assets/WorkView-CrQ8R4xr.js +0 -2
  184. package/dist/ui/assets/architecture-7GRP2DOG-BKXNQ9fY.js +0 -1
  185. package/dist/ui/assets/channel-DXJt6aEW.js +0 -1
  186. package/dist/ui/assets/classDiagram-ZZMXUADV-CuXQhOCc.js +0 -1
  187. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-CuXQhOCc.js +0 -1
  188. package/dist/ui/assets/config-CxRkBtUj.js +0 -1
  189. package/dist/ui/assets/eventmodeling-NTZA5JFV-BXXaF1fS.js +0 -1
  190. package/dist/ui/assets/flowDiagram-HODETNUW-HB2tiq05.js +0 -1
  191. package/dist/ui/assets/index-C9k_vHM5.js +0 -46
  192. package/dist/ui/assets/index-DKDq8ZAh.css +0 -1
  193. package/dist/ui/assets/railroad-abnf-HS5TGJTU-SDQCarTw.js +0 -1
  194. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-D9NI_tLh.js +0 -1
  195. package/dist/ui/assets/railroad-peg-WCYAUIDC-kSMa3Mxd.js +0 -1
  196. package/dist/ui/assets/repo-OUIxzvAy.js +0 -1
  197. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-L8aM9etd.js +0 -1
  198. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-Dj94QI7O.js +0 -8
package/README.md CHANGED
@@ -96,7 +96,7 @@ install, `repoos upgrade` prints the matching package-manager command instead.
96
96
  Then in any repo:
97
97
 
98
98
  ```bash
99
- repoos init # scaffold work/, docs/, repoos.toml, AGENTS.md (idempotent)
99
+ repoos init # scaffold repoos/work/, repoos/docs/, repoos.toml, AGENTS.md (idempotent)
100
100
  ```
101
101
 
102
102
  Zero config required. `repoos init` walks up to find your repo root, creates the
@@ -219,6 +219,14 @@ your other Tailscale devices and from localhost-only tools on the same machine
219
219
  (e.g. a Cloudflare Tunnel's local origin). Pass `--host 127.0.0.1` explicitly
220
220
  to restrict it to localhost only.
221
221
 
222
+ In that mode `repoos serve` also runs `tailscale serve` for you and prints an
223
+ `https://<machine>.<tailnet>.ts.net` URL. Use it: a plain-`http://` Tailscale IP
224
+ isn't a browser secure context, so some UI features can't work there. It is
225
+ tailnet-only (not Funnel), never overwrites another `tailscale serve` mapping
226
+ (it falls back to port 8443, then 10000), and needs MagicDNS + HTTPS
227
+ Certificates enabled in the Tailscale admin console (`serve` prints a hint if
228
+ not). Pass `--no-tailscale-https` to skip it.
229
+
222
230
  ### Endpoints
223
231
 
224
232
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "hash": "69cefc15aead79d33e1a4fcf9b4d8d1358152d8f214530f3af45226fbe121036",
3
- "version": "0.5.57",
2
+ "hash": "54bb7cbb6f6758af0e4594f62905c10fa0aa98c53e313ec47f2d21da7c028692",
3
+ "version": "0.5.59",
4
4
  "devUi": false
5
5
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "generatedAt": "2026-09-29T11:51:40.533Z"
2
+ "generatedAt": "2026-09-29T23:17:06.252Z"
3
3
  }
package/dist/cli/index.js CHANGED
@@ -8,6 +8,7 @@ import { cmdList, cmdShow, cmdMv, cmdUpdate, cmdNew, cmdIndex, cmdNote, } from "
8
8
  import { cmdNewDoc } from "../commands/docs.js";
9
9
  import { cmdGc } from "../commands/gc.js";
10
10
  import { cmdCheck } from "../commands/check.js";
11
+ import { cmdShot } from "../commands/shot.js";
11
12
  import { cmdServe, serveProcessTitle, setServeProcessTitle } from "../commands/serve.js";
12
13
  import { cmdStop } from "../commands/stop.js";
13
14
  import { cmdTunnel } from "../commands/tunnel.js";
@@ -22,9 +23,7 @@ import { checkBuild } from "../core/build.js";
22
23
  import { loadConfig } from "../core/config.js";
23
24
  import { reexecAfterStaleBuild, reexecUnderBunIfRequested } from "../core/runtime.js";
24
25
  import { c } from "./colors.js";
25
- import { readFileSync } from "node:fs";
26
- import { dirname, join } from "node:path";
27
- import { fileURLToPath } from "node:url";
26
+ import { readVersion } from "../core/version.js";
28
27
  // node:sqlite (used by db.ts/auth-store.ts) is still marked experimental on
29
28
  // supported Node versions and prints a warning the first time it's loaded.
30
29
  // That's expected/stable usage here, not something a user needs to see —
@@ -39,19 +38,6 @@ process.on("warning", (warning) => {
39
38
  return;
40
39
  console.warn(warning);
41
40
  });
42
- /** Reads the version stamped into .build-info.json at build time; falls back for dev/source runs. */
43
- function readVersion() {
44
- try {
45
- const root = dirname(dirname(fileURLToPath(import.meta.url)));
46
- const info = JSON.parse(readFileSync(join(root, ".build-info.json"), "utf8"));
47
- if (info.version)
48
- return info.version;
49
- }
50
- catch {
51
- /* fall through */
52
- }
53
- return "0.0.0-dev";
54
- }
55
41
  const VERSION = readVersion();
56
42
  // Column where COMMANDS descriptions start, measured from the start of the
57
43
  // command text (not counting the leading 4-space indent). Wide enough for
@@ -89,6 +75,7 @@ function help() {
89
75
  cmdRow("check --profile full", "Run every declared step, including ones held back from a routine run"),
90
76
  cmdRow("check --changed main", "Fast pre-review pass over steps affected by changed paths"),
91
77
  cmdRow("check --print-plan", "Print the resolved plan as [[check.steps]] TOML to commit"),
78
+ cmdRow("shot [<route|url>]", `Capture preview screenshots into the task's shots folder ${c.dim("flags: --target --selector --task --base --wait --full-page")}`),
92
79
  cmdRow("init [name]", "Scaffold work/, repoos.toml, AGENTS.md; outside a git repo runs a guided flow that can launch the web console"),
93
80
  cmdRow("list [status]", `Show the board (or one column: ${c.dim("inbox|ready|active|review|done")})`),
94
81
  cmdRow("status [--json]", "One-screen health snapshot: server, build freshness, board, worktrees, tunnel, git"),
@@ -103,7 +90,7 @@ function help() {
103
90
  cmdRow('new-doc "<desc>"', "Create a document from a description via PM agent"),
104
91
  cmdRow("index [--json]", "Rebuild the derived index cache"),
105
92
  cmdRow("gc [--yes|--dry-run]", "Collect leaked task worktrees/branches (done/absent tasks, integrate candidates)"),
106
- cmdRow("serve [--port N]", `Start the local server (live API + SSE stream) ${c.dim("flags: --host, --preview-overrides, --no-preview-overrides")}`),
93
+ cmdRow("serve [--port N]", `Start the local server (live API + SSE stream) ${c.dim("flags: --host, --no-tailscale-https, --preview-overrides, --no-preview-overrides")}`),
107
94
  cmdRow("stop [--port N]", "Stop this repo's serve process (by its own lockfile)"),
108
95
  cmdRow("service [sub]", `Manage background services ${c.dim("list|status|install|start|stop|restart|enable|disable|remove")}`),
109
96
  cmdRow("tunnel <sub>", `Publish local apps via Cloudflare Tunnel + Zero Trust ${c.dim("(setup|create|allow|deny|rename|destroy|start|install|stop|list|status)")}`),
@@ -113,13 +100,16 @@ function help() {
113
100
  const exampleLines = [
114
101
  exRow("repoos init"),
115
102
  exRow("repoos init myproject", "guided new-project flow outside a git repo"),
116
- exRow('repoos new "Add company dashboard" --ai --type feature --area web --priority p1'),
103
+ exRow('repoos new "Add company dashboard" --ai --type feature --area web,server --priority p1'),
117
104
  exRow('repoos new-doc "API design doc for the payment system"'),
118
105
  exRow("repoos mv 0012 active"),
119
106
  exRow('repoos mv 0012 active --note "Fix the regression in checkout; see review"'),
120
107
  exRow('repoos note 0012 "Handle the reviewer\'s suggestions before the next review"'),
121
- exRow('repoos update 0012 --title "New title" --area web'),
108
+ exRow('repoos update 0012 --title "New title" --area web,core'),
122
109
  exRow("repoos list ready"),
110
+ exRow("repoos shot", "screenshot the task preview for the current worktree"),
111
+ exRow("repoos shot /repo/commits/abc123", "capture one route"),
112
+ exRow("repoos shot --target 'Docs site' --selector '.sidebar'"),
123
113
  exRow("repoos status", "one-screen health snapshot"),
124
114
  exRow("repoos status --json", "machine-readable, for agents/tools"),
125
115
  exRow("repoos index --json", "machine-readable, for agents/tools"),
@@ -264,6 +254,17 @@ function main() {
264
254
  case "check":
265
255
  void cmdCheck(rest);
266
256
  break;
257
+ case "shot":
258
+ void cmdShot(rest)
259
+ .then((code) => {
260
+ if (code !== 0)
261
+ process.exitCode = code;
262
+ })
263
+ .catch((err) => {
264
+ console.error(c.red(` ✗ shot failed: ${err.message}`));
265
+ process.exitCode = 1;
266
+ });
267
+ break;
267
268
  case "tunnel":
268
269
  void cmdTunnel(rest);
269
270
  break;
@@ -31,6 +31,7 @@ import { preferBunForDevTasks } from "../core/runtime.js";
31
31
  import { blockingFailures, DEFAULT_PROFILE, describeStep, formatPlanToml, resolveCheckPlan, selectSteps, } from "../core/check-plan.js";
32
32
  import { detectRepoMarkers, missingBinaries, prereqDetail, runCommand, stepCwd, } from "../core/check-runner.js";
33
33
  import { writeCheckRun } from "../core/check-results-store.js";
34
+ import { extractFailedTests } from "../core/check-failure-summary.js";
34
35
  import { envToRunContext, getCheckStore, localMachineName } from "../core/check-store.js";
35
36
  import { mainCheckoutRoot } from "../core/git.js";
36
37
  import { Logger } from "../core/logger.js";
@@ -1640,6 +1641,9 @@ export async function cmdCheck(argv = []) {
1640
1641
  failedStep: gatingFailures[0]?.name ?? null,
1641
1642
  skippedSteps: results.filter((r) => r.status === "skipped").map((r) => r.name),
1642
1643
  detail: gatingFailures[0]?.detail ?? null,
1644
+ // The stored detail is a short log tail that routinely loses the failing
1645
+ // test's name; keep the names themselves so "which test?" is a query.
1646
+ failedTests: gatingFailures.flatMap((r) => extractFailedTests(r.output ?? "")),
1643
1647
  });
1644
1648
  process.exit(gatingFailures.length > 0 ? 1 : 0);
1645
1649
  }
@@ -1672,6 +1676,7 @@ function recordRunHistoryRow(row) {
1672
1676
  failedStep: row.failedStep,
1673
1677
  skippedSteps: row.skippedSteps,
1674
1678
  detail: row.detail,
1679
+ failedTests: row.failedTests,
1675
1680
  });
1676
1681
  }
1677
1682
  catch {
@@ -1,3 +1,5 @@
1
+ /** Blockquote every line, so a multi-line (pasted markdown) description stays one quote. */
2
+ export declare function quoteBlock(text: string): string;
1
3
  /** Marker makes the optional existing-repo addition safe to offer repeatedly. */
2
4
  export declare const REPOOS_AGENTS_SECTION_MARKER = "<!-- repoos:managed-instructions -->";
3
5
  /**
@@ -19,7 +21,11 @@ export declare function repoOSAgentsSectionAddition(existing: string, workDir?:
19
21
  type ScaffoldLayout = string;
20
22
  /** Which starter task to seed beyond 0001 — a blank project or an existing repo. */
21
23
  type ScaffoldKind = "new" | "existing";
22
- export declare function scaffoldInto(root: string, description: string, layout?: ScaffoldLayout, kind?: ScaffoldKind): {
24
+ export declare function scaffoldInto(root: string, description: string, layout?: ScaffoldLayout, kind?: ScaffoldKind,
25
+ /** Areas collected at init time (#0583) — real `[[areas]]` rows when given. */
26
+ areas?: string[],
27
+ /** When true, scaffold commented `[[preview.targets]]` stubs for those areas. */
28
+ previewStub?: boolean): {
23
29
  created: string[];
24
30
  skipped: string[];
25
31
  };
@@ -6,7 +6,7 @@
6
6
  * guided, interactive flow that creates a brand-new RepoOS project from
7
7
  * scratch: optionally in a subdirectory, with a choice of layout (repo root
8
8
  * vs. a `repoos/` subfolder, config file at root either way), an optional
9
- * one-line project description (seeded into the sample task) and an optional
9
+ * project description, one or many lines (seeded into the sample task) and an optional
10
10
  * initial commit.
11
11
  */
12
12
  import { spawn } from "node:child_process";
@@ -15,6 +15,8 @@ import { createServer } from "node:net";
15
15
  import { join, resolve } from "node:path";
16
16
  import { createInterface } from "node:readline/promises";
17
17
  import { stdin as input, stdout as output } from "node:process";
18
+ import { canaryGitignoreIgnore, canaryGitignoreNegation, canaryRelPath } from "../core/canary.js";
19
+ import { scaffoldCanaryFile } from "../core/canary-repo.js";
18
20
  import { deriveServePort, findRepoRoot, loadConfig } from "../core/config.js";
19
21
  import { CHECK_PLAN_PROPOSAL_FILE, proposeCheckPlan } from "../core/check-plan-proposal.js";
20
22
  import { formatPlanToml } from "../core/check-plan.js";
@@ -58,12 +60,19 @@ running \`repoos init\` already satisfies everything above. There's nothing to
58
60
  Status is a frontmatter field — never move files between folders. Keep diffs
59
61
  small. Read AGENTS.md before starting any task.
60
62
  `;
63
+ /** Blockquote every line, so a multi-line (pasted markdown) description stays one quote. */
64
+ export function quoteBlock(text) {
65
+ return text
66
+ .split("\n")
67
+ .map((line) => (line.trim() === "" ? ">" : `> ${line}`))
68
+ .join("\n");
69
+ }
61
70
  /**
62
71
  * The first *workable* task a guided new-project init leaves on the board.
63
72
  * 0001 is deliberately `done` (scaffolding is the whole of it), so this is what
64
73
  * a new user — or their first agent — actually picks up. `repoos init` runs
65
74
  * before any server or agent exists, so the body itself is the prompt: it
66
- * embeds the one-line description collected at init and carries the questions
75
+ * embeds the description collected at init and carries the questions
67
76
  * that turn that description into docs and real tasks later.
68
77
  */
69
78
  const NEW_PROJECT_STARTER_TASK = (id, description, workDir = "work", docsDir = "docs") => `---
@@ -79,15 +88,15 @@ branch: ""
79
88
  ---
80
89
  ## Overview
81
90
 
82
- This project started from a one-line description. This task turns it into
91
+ This project started from a short description. This task turns it into
83
92
  something a team can actually build from — a shared vision, an initial
84
93
  architecture, and a first batch of concrete work.
85
94
 
86
95
  ## Project description
87
96
 
88
97
  ${description
89
- ? `> ${description}`
90
- : "_No description was given at init time. Start by writing one line that says what this project is and who it's for._"}
98
+ ? quoteBlock(description)
99
+ : "_No description was given at init time. Start by writing a short description that says what this project is and who it's for._"}
91
100
 
92
101
  ## What to do
93
102
 
@@ -202,6 +211,15 @@ a candidate merge. Do not erase a task's branch metadata to force this path.
202
211
 
203
212
  ## Rules
204
213
 
214
+ - **\`repoos.toml\` owns the layout.** \`workDir\` (\`${workDir}\`) and \`docsDir\`
215
+ (\`${docsDir}\`) are authoritative: tasks live under \`${workDir}/\` and project
216
+ docs under \`${docsDir}/\` because the config says so. Never move, rename or
217
+ relocate those directories, and never "fix" a layout you think is wrong by
218
+ moving files. If project docs or a person's own notes disagree with the
219
+ config, report the mismatch — the fix is to update \`repoos.toml\` with human
220
+ approval (or correct the docs), never to move directories on your own. Do not
221
+ invent ownership rules for RepoOS directories; the config is the only source
222
+ of truth for layout.
205
223
  - **Never** move task files between folders. Status lives in frontmatter.
206
224
  - **Never write directly to \`${workDir}/*.md\` files.** All task creation and
207
225
  manipulation goes through \`repoos\` commands or HTTP API endpoints
@@ -249,7 +267,10 @@ export const REPOOS_AGENTS_SECTION = (workDir) => `${REPOOS_AGENTS_SECTION_MARKE
249
267
  ## RepoOS
250
268
 
251
269
  RepoOS keeps tasks as Markdown under \`${workDir}/\` and runs task work in dedicated
252
- Git worktrees. Use the RepoOS UI or \`repoos\` commands to create and update
270
+ Git worktrees. The layout is set by \`repoos.toml\` (\`workDir\`/\`docsDir\`/\`cacheDir\`):
271
+ those paths are authoritative, so never move or rename them — if project docs
272
+ disagree with the config, ask the human instead of relocating directories.
273
+ Use the RepoOS UI or \`repoos\` commands to create and update
253
274
  tasks; do not hand-edit task files. Read the relevant project docs before
254
275
  starting work, run \`repoos check\` before handoff, and await human approval
255
276
  through RepoOS's **Move to done**. The reviewer is advisory. A Git remote does
@@ -267,7 +288,7 @@ export function repoOSAgentsSectionAddition(existing, workDir = "work") {
267
288
  }
268
289
  return (existing.endsWith("\n") ? "\n" : "\n\n") + REPOOS_AGENTS_SECTION(workDir);
269
290
  }
270
- function repoosToml(namespace) {
291
+ function repoosToml(namespace, areas = [], previewStub = false) {
271
292
  const ns = namespace
272
293
  ? `workDir = "${namespace}/work"\ndocsDir = "${namespace}/docs"\ncacheDir = "${namespace}/.repoos"\n`
273
294
  : "";
@@ -305,8 +326,45 @@ defaultAssignee = "unassigned"
305
326
  # active = "In progress"
306
327
  # review = "Code review"
307
328
  # done = "Shipped"
329
+ ${areaRows(areas)}
330
+ ${previewStub ? previewTargetRows(areas) : ""}
308
331
  `;
309
332
  }
333
+ /**
334
+ * The `[areas]` block: the vocabulary collected at init time as real
335
+ * `[[areas]]` rows, or the commented-out stub when skipped (#0583).
336
+ */
337
+ function areaRows(areas) {
338
+ if (!areas.length) {
339
+ return [
340
+ "# [[areas]] # the task-area vocabulary (#0583)",
341
+ '# name = "web" # offered in the area picker + the PM prompt',
342
+ '# description = "The main web app"',
343
+ "# Every [[preview.targets]] area is offered automatically, even when not",
344
+ "# declared here. With none declared the area field is free text only.",
345
+ ].join("\n");
346
+ }
347
+ return areas.map((a) => `[[areas]]\nname = "${a}"`).join("\n\n");
348
+ }
349
+ /**
350
+ * The commented `[[preview.targets]]` skeleton offered when an area vocabulary
351
+ * was seeded at init (#0583) — the wiring for when the project has something
352
+ * previewable, referencing the areas the picker now offers.
353
+ */
354
+ function previewTargetRows(areas) {
355
+ const areaList = areas.length ? JSON.stringify(areas) : '["web"]';
356
+ return [
357
+ "# [preview] # read-only task previews",
358
+ `# command = "bun run dev --port {port} --host {host}"`,
359
+ `# # default when no target matches`,
360
+ "",
361
+ `# [[preview.targets]]`,
362
+ `# name = "Main app"`,
363
+ `# areas = ${areaList}`,
364
+ `# command = "bun run dev --port {port} --host {host}"`,
365
+ `# readyTimeoutMs = 240000 # raise for a command that also builds`,
366
+ ].join("\n");
367
+ }
310
368
  const ENV_EXAMPLE = `# Copy to .env and fill in what you need — .env is gitignored, this file is
311
369
  # tracked so the repo documents which secrets a full setup expects.
312
370
  # repoos serve auto-loads .env at startup; real shell/process-supervisor env
@@ -380,7 +438,11 @@ function findStarter(root, workDir, slug) {
380
438
  return null;
381
439
  }
382
440
  }
383
- export function scaffoldInto(root, description, layout = "", kind = "new") {
441
+ export function scaffoldInto(root, description, layout = "", kind = "new",
442
+ /** Areas collected at init time (#0583) — real `[[areas]]` rows when given. */
443
+ areas = [],
444
+ /** When true, scaffold commented `[[preview.targets]]` stubs for those areas. */
445
+ previewStub = false) {
384
446
  const created = [];
385
447
  const skipped = [];
386
448
  let aborted = false;
@@ -425,7 +487,7 @@ export function scaffoldInto(root, description, layout = "", kind = "new") {
425
487
  // Write config first so workDir/docsDir/cacheDir overrides are in effect
426
488
  // before the dirs are created (namespace layout) — or so an existing
427
489
  // config is respected untouched (existing-repo path).
428
- ensureFile("repoos.toml", repoosToml(layout));
490
+ ensureFile("repoos.toml", repoosToml(layout, areas, previewStub));
429
491
  const config = loadConfig(root);
430
492
  ensureDir(config.workDir);
431
493
  ensureDir(config.docsDir);
@@ -449,10 +511,17 @@ export function scaffoldInto(root, description, layout = "", kind = "new") {
449
511
  ensureFile(join(config.workDir, `${starterId}-${starter.slug}.md`), starter.build(starterId, description, config.workDir, config.docsDir));
450
512
  }
451
513
  ensureFile(".env.example", ENV_EXAMPLE);
514
+ if (scaffoldCanaryFile(root, config.cacheDir)) {
515
+ created.push(canaryRelPath(config.cacheDir));
516
+ }
452
517
  // gitignore the derived cache and local secrets
453
518
  const giPath = join(root, ".gitignore");
454
519
  const ignoreLines = [
455
- { comment: "# RepoOS derived index cache", line: `${config.cacheDir}/` },
520
+ { comment: "# RepoOS derived index cache", line: canaryGitignoreIgnore(config.cacheDir) },
521
+ {
522
+ comment: "# RepoOS canary flow-test counter (tracked)",
523
+ line: canaryGitignoreNegation(config.cacheDir),
524
+ },
456
525
  { comment: "# Local secrets — see .env.example", line: ".env" },
457
526
  ];
458
527
  const existingLines = existsSync(giPath) ? readFileSync(giPath, "utf8").split(/\r?\n/) : [];
@@ -521,6 +590,46 @@ async function ask(question) {
521
590
  rl.close();
522
591
  }
523
592
  }
593
+ /**
594
+ * Like `ask`, but accepts multi-line input. On a TTY, a pasted block arrives as
595
+ * a burst of lines, so after each line we wait briefly for more; a typed line
596
+ * ending in `\\` also continues onto the next line. Non-TTY input (pipes, CI)
597
+ * stays strictly one line so later prompts' answers aren't swallowed.
598
+ */
599
+ async function askMultiline(question) {
600
+ if (!input.isTTY)
601
+ return ask(question);
602
+ const rl = createInterface({ input, output });
603
+ return new Promise((resolvePromise) => {
604
+ const lines = [];
605
+ let timer;
606
+ let done = false;
607
+ const finish = () => {
608
+ if (done)
609
+ return;
610
+ done = true;
611
+ if (timer)
612
+ clearTimeout(timer);
613
+ rl.close();
614
+ resolvePromise(lines.join("\n").trim());
615
+ };
616
+ rl.on("close", finish);
617
+ rl.on("line", (line) => {
618
+ if (timer)
619
+ clearTimeout(timer);
620
+ if (line.endsWith("\\")) {
621
+ lines.push(line.slice(0, -1));
622
+ rl.setPrompt(" … ");
623
+ rl.prompt();
624
+ return;
625
+ }
626
+ lines.push(line);
627
+ timer = setTimeout(finish, 75);
628
+ });
629
+ rl.setPrompt(question);
630
+ rl.prompt();
631
+ });
632
+ }
524
633
  async function confirm(question, dflt) {
525
634
  const hint = dflt ? " [Y/n]" : " [y/N]";
526
635
  const answer = (await ask(question + c.dim(hint) + " ")).toLowerCase();
@@ -854,9 +963,34 @@ async function guidedNewRepo(args) {
854
963
  console.log(c.yellow("\n Cancelled — nothing was created."));
855
964
  return;
856
965
  }
857
- const description = await ask(" Project description" +
858
- c.dim(" — one line, gives the AI context to suggest next steps (optional, Enter to skip)") +
966
+ const description = await askMultiline(" Project description" +
967
+ c.dim(" — gives the AI context to suggest next steps; paste multi-line markdown or end a line with \\ to continue (optional, Enter to skip)") +
968
+ ": ");
969
+ // The task-area vocabulary (#0583) — reinforced with preview targets, since
970
+ // a `[[preview.targets]]` area is offered in the picker automatically even
971
+ // when not declared here. Skipped means free text only; never blocking.
972
+ const areasInput = await ask(" Task areas to seed the area picker" +
973
+ c.dim(" — comma-separated (e.g. web, cli, api; Enter to skip)") +
859
974
  ": ");
975
+ const areas = areasInput
976
+ ? areasInput
977
+ .split(",")
978
+ .map((a) => a.trim())
979
+ .filter(Boolean)
980
+ : [];
981
+ if (areas.length) {
982
+ console.log(c.dim(" Preview targets route a task preview by its area — the picker and"));
983
+ console.log(c.dim(" the PM prompt offer target areas automatically; see user-docs/configuration.md's Previews section."));
984
+ }
985
+ // Preview targets reinforce areas ("define them together"): when an area
986
+ // vocabulary was given, offer to scaffold the commented `[[preview.targets]]`
987
+ // skeleton those areas feed, so the wiring sits where preview setup happens
988
+ // later. A brand-new project has nothing runnable yet — never a live
989
+ // preview command, just the commented shape to fill in.
990
+ let previewStub = false;
991
+ if (areas.length) {
992
+ previewStub = await confirm(" Scaffold commented preview-target stubs for these areas?", false);
993
+ }
860
994
  if (!existsSync(target))
861
995
  mkdirSync(target, { recursive: true });
862
996
  // git health warnings — fail-soft, scaffold regardless
@@ -869,7 +1003,7 @@ async function guidedNewRepo(args) {
869
1003
  console.log(c.dim(" git may auto-detect it, or the initial commit may fail. Set it with:"));
870
1004
  console.log(c.dim(' git config --global user.name "You" && git config --global user.email you@example.com'));
871
1005
  }
872
- const { created, skipped } = scaffoldInto(target, description, layout);
1006
+ const { created, skipped } = scaffoldInto(target, description, layout, "new", areas, previewStub);
873
1007
  reportInit(target, created, skipped);
874
1008
  await offerCheckPlanProposal(target);
875
1009
  if (!gitOk) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderServeBanner } from "./serve.js";
3
+ describe("renderServeBanner", () => {
4
+ const rows = [
5
+ { label: "Local", value: "http://127.0.0.1:7515" },
6
+ { label: "Tailnet HTTPS", value: "x" },
7
+ ];
8
+ it("shows the version and aligns labels", () => {
9
+ const out = renderServeBanner("1.2.3", rows);
10
+ expect(out).toContain("RepoOS v1.2.3");
11
+ expect(out).toContain("Local http://127.0.0.1:7515");
12
+ });
13
+ it("splits an HTTPS warning into problem, fix and link", () => {
14
+ const out = renderServeBanner("1.2.3", rows, "HTTPS certificates aren't enabled for your tailnet — turn on MagicDNS and HTTPS Certificates at https://login.tailscale.com/admin/dns");
15
+ expect(out).toContain("Tailscale HTTPS unavailable");
16
+ expect(out).toContain("HTTPS certificates aren't enabled for your tailnet");
17
+ expect(out).toContain("Fix: turn on MagicDNS and HTTPS Certificates at");
18
+ expect(out).toContain("https://login.tailscale.com/admin/dns");
19
+ expect(out).toContain("--no-tailscale-https");
20
+ });
21
+ it("handles a reason without a fix link", () => {
22
+ const out = renderServeBanner("1", rows, "all ports in use");
23
+ expect(out).toContain("all ports in use");
24
+ expect(out).not.toContain("Fix:");
25
+ });
26
+ });
@@ -59,6 +59,18 @@ export declare function resolvePreviewOverrides(args: readonly string[], env?: N
59
59
  * processes keep working. Exported for the defense-in-depth tests (#0096).
60
60
  */
61
61
  export declare function directServeBlockedByAgent(env?: NodeJS.ProcessEnv): boolean;
62
+ export interface BannerRow {
63
+ label: string;
64
+ /** May contain ANSI codes. */
65
+ value: string;
66
+ }
67
+ /**
68
+ * The startup banner: a title line with the running version, an aligned
69
+ * label/value table, and — when Tailscale HTTPS could not be set up — a
70
+ * highlighted warning block that separates the problem from its fix. Pure
71
+ * (returns a string) so tests can assert on it with colors off.
72
+ */
73
+ export declare function renderServeBanner(version: string, rows: readonly BannerRow[], httpsWarning?: string): string;
62
74
  export declare function cmdServe(args: string[], opts?: {
63
75
  onShutdown?: () => void;
64
76
  onReady?: (url: string) => void;
@@ -8,7 +8,8 @@ import { PREVIEW_REQUEST_SIGNAL } from "../server/agents.js";
8
8
  import { isBun } from "../core/runtime.js";
9
9
  import { findRepoRoot } from "../core/config.js";
10
10
  import { c, statusColor } from "../cli/colors.js";
11
- import { detectTailscaleIPv4 } from "../core/tailscale.js";
11
+ import { readVersion } from "../core/version.js";
12
+ import { detectTailscaleIPv4, ensureTailscaleHttps } from "../core/tailscale.js";
12
13
  /**
13
14
  * The OS-visible process title for a `repoos serve` instance:
14
15
  * `repoos-<project>`, where `<project>` is the managed root's directory name.
@@ -109,6 +110,39 @@ export function resolvePreviewOverrides(args, env = process.env) {
109
110
  export function directServeBlockedByAgent(env = process.env) {
110
111
  return env.REPOOS_AGENT === "1" && env.REPOOS_PREVIEW_CHILD !== "1" && env.REPOOS_RELOAD !== "1";
111
112
  }
113
+ /**
114
+ * The startup banner: a title line with the running version, an aligned
115
+ * label/value table, and — when Tailscale HTTPS could not be set up — a
116
+ * highlighted warning block that separates the problem from its fix. Pure
117
+ * (returns a string) so tests can assert on it with colors off.
118
+ */
119
+ export function renderServeBanner(version, rows, httpsWarning) {
120
+ const width = Math.max(...rows.map((r) => r.label.length));
121
+ const out = [
122
+ "",
123
+ ` ${c.bold(c.cyan("◆ RepoOS"))} ${c.dim("v" + version)} ${c.green("● running")}`,
124
+ "",
125
+ ];
126
+ for (const r of rows)
127
+ out.push(` ${c.dim(r.label.padEnd(width))} ${r.value}`);
128
+ if (httpsWarning) {
129
+ // Reasons read "<problem> — <fix> <url>": put the problem, the fix and the
130
+ // link on their own lines so the actionable part isn't buried.
131
+ const [problem, ...rest] = httpsWarning.split(" — ");
132
+ const fixText = rest.join(" — ");
133
+ const url = /https?:\/\/\S+/.exec(fixText)?.[0];
134
+ const fix = (url ? fixText.replace(url, "") : fixText).replace(/\s*\bat\s*$/, "").trim();
135
+ out.push("", ` ${c.yellow("▲")} ${c.bold(c.yellow("Tailscale HTTPS unavailable"))}`);
136
+ out.push(` ${c.dim(problem ?? "")}`);
137
+ if (fix)
138
+ out.push(` ${c.bold("Fix:")} ${fix}${url ? c.dim(" at") : ""}`);
139
+ if (url)
140
+ out.push(` ${c.cyan(url)}`);
141
+ out.push(` ${c.dim("or pass")} ${c.cyan("--no-tailscale-https")} ${c.dim("to silence this")}`);
142
+ }
143
+ out.push("", c.dim(" press ^C to stop"), "");
144
+ return out.join("\n");
145
+ }
112
146
  export async function cmdServe(args, opts = {}) {
113
147
  // Defense in depth (#0096): a managed agent process must never start its own
114
148
  // `repoos serve`. An agent that ignores the mission and runs `repoos serve`
@@ -128,6 +162,7 @@ export async function cmdServe(args, opts = {}) {
128
162
  let port;
129
163
  let explicitHost;
130
164
  let quiet = false;
165
+ let tailscaleHttps = true;
131
166
  for (let i = 0; i < args.length; i++) {
132
167
  if (args[i] === "--port" || args[i] === "-p")
133
168
  port = Number(args[++i]);
@@ -135,6 +170,8 @@ export async function cmdServe(args, opts = {}) {
135
170
  explicitHost = args[++i];
136
171
  else if (args[i] === "--quiet" || args[i] === "-q")
137
172
  quiet = true;
173
+ else if (args[i] === "--no-tailscale-https")
174
+ tailscaleHttps = false;
138
175
  }
139
176
  // Preview-only config overlay (#0464). A managed preview child
140
177
  // (REPOOS_PREVIEW_CHILD=1, set by PreviewManager) applies the repo's
@@ -171,32 +208,46 @@ export async function cmdServe(args, opts = {}) {
171
208
  return;
172
209
  }
173
210
  const snap = handle.index.snapshot();
174
- console.log(c.bold(c.cyan("\n RepoOS server")) + c.dim(" · ") + c.bold(handle.url));
175
- console.log(c.dim(" open ") + c.cyan(handle.url) + c.dim(" in your browser for the UI"));
176
- console.log(c.dim(" watching ") +
177
- snap.taskCount +
178
- c.dim(" tasks · SSE stream at ") +
179
- c.cyan(handle.url + "/api/events"));
211
+ const rt = isBun()
212
+ ? `Bun ${process.versions.bun ?? ""}`.trim()
213
+ : `Node ${process.versions.node}`;
214
+ const rows = [
215
+ { label: "Local", value: c.cyan(c.bold(handle.url)) },
216
+ { label: "Events", value: c.cyan(handle.url + "/api/events") + c.dim(" (SSE)") },
217
+ { label: "Watching", value: `${snap.taskCount} tasks` },
218
+ { label: "Runtime", value: rt },
219
+ ];
180
220
  // Report an active preview-only overlay and its effective keys (#0464), so a
181
221
  // preview that deliberately differs from the base config is never silent.
182
222
  if (handle.previewOverrides?.length) {
183
- console.log(c.yellow(" preview overrides active: ") + c.dim(handle.previewOverrides.join(", ")));
223
+ rows.push({ label: "Preview", value: c.yellow(handle.previewOverrides.join(", ")) });
184
224
  }
185
- const rt = isBun()
186
- ? `Bun ${process.versions.bun ?? ""}`.trim()
187
- : `Node ${process.versions.node}`;
188
- console.log(c.dim(" runtime ") + rt);
189
- console.log(c.dim(" api: ") + c.dim("/api/tasks /api/tasks/:id /api/counts /api/index /api/docs"));
190
225
  if (tailscaleDetected) {
191
- console.log(c.dim(" Tailscale detected — listening on all interfaces (") +
192
- c.cyan("0.0.0.0") +
193
- c.dim("), reachable from other Tailscale devices at ") +
194
- c.cyan(`http://${tailscaleIP}:${handle.port}`) +
195
- c.dim(". Use ") +
196
- c.cyan("--host 127.0.0.1") +
197
- c.dim(" to restrict to localhost only."));
226
+ rows.push({
227
+ label: "Tailnet",
228
+ value: c.cyan(`http://${tailscaleIP}:${handle.port}`) +
229
+ c.dim(" (all interfaces; ") +
230
+ c.cyan("--host 127.0.0.1") +
231
+ c.dim(" to restrict)"),
232
+ });
233
+ }
234
+ // HTTPS for tailnet devices (a plain-http IP isn't a secure context, so
235
+ // crypto.randomUUID etc. are missing). Skipped for managed previews, which
236
+ // must not touch the machine's `tailscale serve` config.
237
+ let httpsWarning;
238
+ if (tailscaleDetected && tailscaleHttps && process.env.REPOOS_PREVIEW_CHILD !== "1") {
239
+ const https = await ensureTailscaleHttps(handle.port);
240
+ if (https.url) {
241
+ rows.push({
242
+ label: "Tailnet HTTPS",
243
+ value: c.green(c.bold(https.url)) + c.dim(" (secure context)"),
244
+ });
245
+ }
246
+ else if (https.reason) {
247
+ httpsWarning = https.reason;
248
+ }
198
249
  }
199
- console.log(c.dim(" press ^C to stop\n"));
250
+ console.log(renderServeBanner(readVersion(), rows, httpsWarning));
200
251
  // Fires only now that the server is actually listening (handle.url is a
201
252
  // live, bound address) — a caller opening a browser tab any earlier races
202
253
  // the server's own startup and shows connection errors until a manual