@quickchatai/quickreview 1.2.0 → 1.3.2

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 (350) hide show
  1. package/README.md +116 -53
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +25 -7
  4. package/deploy/ansible/README.md +20 -12
  5. package/deploy/ansible/inventory.example.ini +1 -1
  6. package/deploy/collect-secrets.sh +1 -1
  7. package/deploy/qcr-update.sh +1 -1
  8. package/dist/extensions/cli/home.js +14 -0
  9. package/dist/extensions/cli/home.js.map +1 -0
  10. package/dist/extensions/cli/selfimprove.js +18 -0
  11. package/dist/extensions/cli/selfimprove.js.map +1 -0
  12. package/dist/extensions/home.js +350 -0
  13. package/dist/extensions/home.js.map +1 -0
  14. package/dist/extensions/login-route.js +1 -1
  15. package/dist/extensions/login-route.js.map +1 -1
  16. package/dist/extensions/onboarding.js +8 -7
  17. package/dist/extensions/onboarding.js.map +1 -1
  18. package/dist/extensions/selfimprove.js +49 -0
  19. package/dist/extensions/selfimprove.js.map +1 -0
  20. package/dist/extensions/setup-scan.js +23 -592
  21. package/dist/extensions/setup-scan.js.map +1 -1
  22. package/dist/extensions/setup.js +2 -47
  23. package/dist/extensions/setup.js.map +1 -1
  24. package/dist/extensions/vendor/openviking-pi/index.js +4 -0
  25. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
  26. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
  27. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
  28. package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
  29. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -1
  30. package/dist/src/archive/restore.js +18 -15
  31. package/dist/src/archive/restore.js.map +1 -1
  32. package/dist/src/botworker.js +13 -2
  33. package/dist/src/botworker.js.map +1 -1
  34. package/dist/src/channelfs.js +3 -26
  35. package/dist/src/channelfs.js.map +1 -1
  36. package/dist/src/cli.js +14 -50
  37. package/dist/src/cli.js.map +1 -1
  38. package/dist/src/config.js +16 -0
  39. package/dist/src/config.js.map +1 -1
  40. package/dist/src/daemon.js +33 -4
  41. package/dist/src/daemon.js.map +1 -1
  42. package/dist/src/debuground.js +1 -0
  43. package/dist/src/debuground.js.map +1 -1
  44. package/dist/src/debugroundpi.js +1 -0
  45. package/dist/src/debugroundpi.js.map +1 -1
  46. package/dist/src/engine/caserunner-sdk.js +15 -251
  47. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  48. package/dist/src/home/adapters.js +349 -0
  49. package/dist/src/home/adapters.js.map +1 -0
  50. package/dist/src/home/advanced.js +318 -0
  51. package/dist/src/home/advanced.js.map +1 -0
  52. package/dist/src/home/advancedtools.js +57 -0
  53. package/dist/src/home/advancedtools.js.map +1 -0
  54. package/dist/src/home/autoreview.js +325 -0
  55. package/dist/src/home/autoreview.js.map +1 -0
  56. package/dist/src/home/daemonstatus.js +32 -0
  57. package/dist/src/home/daemonstatus.js.map +1 -0
  58. package/dist/src/home/devicelogin.js +61 -0
  59. package/dist/src/home/devicelogin.js.map +1 -0
  60. package/dist/src/home/launch.js +202 -0
  61. package/dist/src/home/launch.js.map +1 -0
  62. package/dist/src/home/loginowner.js +205 -0
  63. package/dist/src/home/loginowner.js.map +1 -0
  64. package/dist/src/home/memory.js +122 -0
  65. package/dist/src/home/memory.js.map +1 -0
  66. package/dist/src/home/model.js +811 -0
  67. package/dist/src/home/model.js.map +1 -0
  68. package/dist/src/home/panel.js +520 -0
  69. package/dist/src/home/panel.js.map +1 -0
  70. package/dist/src/home/ports.js +19 -0
  71. package/dist/src/home/ports.js.map +1 -0
  72. package/dist/src/home/review.js +163 -0
  73. package/dist/src/home/review.js.map +1 -0
  74. package/dist/src/home/reviews.js +478 -0
  75. package/dist/src/home/reviews.js.map +1 -0
  76. package/dist/src/home/setup.js +507 -0
  77. package/dist/src/home/setup.js.map +1 -0
  78. package/dist/src/home/setupadapters.js +214 -0
  79. package/dist/src/home/setupadapters.js.map +1 -0
  80. package/dist/src/home/setupcard.js +199 -0
  81. package/dist/src/home/setupcard.js.map +1 -0
  82. package/dist/src/home/setuptools.js +45 -0
  83. package/dist/src/home/setuptools.js.map +1 -0
  84. package/dist/src/home/spend.js +160 -0
  85. package/dist/src/home/spend.js.map +1 -0
  86. package/dist/src/home/switches.js +209 -0
  87. package/dist/src/home/switches.js.map +1 -0
  88. package/dist/src/home/switchon.js +439 -0
  89. package/dist/src/home/switchon.js.map +1 -0
  90. package/dist/src/home/toolnames.js +41 -0
  91. package/dist/src/home/toolnames.js.map +1 -0
  92. package/dist/src/home/tune.js +341 -0
  93. package/dist/src/home/tune.js.map +1 -0
  94. package/dist/src/home/words.js +7 -0
  95. package/dist/src/home/words.js.map +1 -0
  96. package/dist/src/homes.js +14 -0
  97. package/dist/src/homes.js.map +1 -1
  98. package/dist/src/install.js +1 -1
  99. package/dist/src/install.js.map +1 -1
  100. package/dist/src/ipc/workerspec.js +8 -0
  101. package/dist/src/ipc/workerspec.js.map +1 -1
  102. package/dist/src/ledgercheck.js +39 -10
  103. package/dist/src/ledgercheck.js.map +1 -1
  104. package/dist/src/live/publish.js +1 -1
  105. package/dist/src/live/publish.js.map +1 -1
  106. package/dist/src/livecli.js +1 -1
  107. package/dist/src/livecli.js.map +1 -1
  108. package/dist/src/loginrefresh/handover.js +121 -0
  109. package/dist/src/loginrefresh/handover.js.map +1 -0
  110. package/dist/src/loginrefresh/lock.js +54 -3
  111. package/dist/src/loginrefresh/lock.js.map +1 -1
  112. package/dist/src/loginrefresh/refresher.js +33 -5
  113. package/dist/src/loginrefresh/refresher.js.map +1 -1
  114. package/dist/src/loginrefresh/supervisor.js +18 -1
  115. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  116. package/dist/src/npmverbs.js +42 -12
  117. package/dist/src/npmverbs.js.map +1 -1
  118. package/dist/src/onboard/choices.js +6 -3
  119. package/dist/src/onboard/choices.js.map +1 -1
  120. package/dist/src/onboard/engine.js +33 -292
  121. package/dist/src/onboard/engine.js.map +1 -1
  122. package/dist/src/onboard/steps/bringup.js +3 -2
  123. package/dist/src/onboard/steps/bringup.js.map +1 -1
  124. package/dist/src/onboard/steps/configfill.js +18 -7
  125. package/dist/src/onboard/steps/configfill.js.map +1 -1
  126. package/dist/src/onboard/steps/loginshare.js +213 -0
  127. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  128. package/dist/src/onboard/steps/memory.js +14 -2
  129. package/dist/src/onboard/steps/memory.js.map +1 -1
  130. package/dist/src/onboard/steps/repoconfig.js +72 -0
  131. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  132. package/dist/src/onboard/toolnames.js +2 -2
  133. package/dist/src/onboard/toolnames.js.map +1 -1
  134. package/dist/src/piagentdir.js +13 -0
  135. package/dist/src/piagentdir.js.map +1 -0
  136. package/dist/src/pilogin.js +31 -16
  137. package/dist/src/pilogin.js.map +1 -1
  138. package/dist/src/qcrbin.js +16 -11
  139. package/dist/src/qcrbin.js.map +1 -1
  140. package/dist/src/reviewargs.js +122 -0
  141. package/dist/src/reviewargs.js.map +1 -0
  142. package/dist/src/reviewcli.js +7 -161
  143. package/dist/src/reviewcli.js.map +1 -1
  144. package/dist/src/reviewdetach.js +76 -11
  145. package/dist/src/reviewdetach.js.map +1 -1
  146. package/dist/src/scancli.js +39 -691
  147. package/dist/src/scancli.js.map +1 -1
  148. package/dist/src/selfimprove.js +66 -0
  149. package/dist/src/selfimprove.js.map +1 -0
  150. package/dist/src/session-command-supervisor.js +38 -9
  151. package/dist/src/session-command-supervisor.js.map +1 -1
  152. package/dist/src/setup/adapters.js +5 -223
  153. package/dist/src/setup/adapters.js.map +1 -1
  154. package/dist/src/setup/caserunner.js +15 -249
  155. package/dist/src/setup/caserunner.js.map +1 -1
  156. package/dist/src/setup/engine.js +32 -341
  157. package/dist/src/setup/engine.js.map +1 -1
  158. package/dist/src/setup/facts.js +3 -592
  159. package/dist/src/setup/facts.js.map +1 -1
  160. package/dist/src/setup/findings.js +3 -947
  161. package/dist/src/setup/findings.js.map +1 -1
  162. package/dist/src/setup/presentation.js +3 -8
  163. package/dist/src/setup/presentation.js.map +1 -1
  164. package/dist/src/setup/scan.js +3 -724
  165. package/dist/src/setup/scan.js.map +1 -1
  166. package/dist/src/setup/scanbuild.js +9 -92
  167. package/dist/src/setup/scanbuild.js.map +1 -1
  168. package/dist/src/setup/scanlease.js +3 -250
  169. package/dist/src/setup/scanlease.js.map +1 -1
  170. package/dist/src/setup/scanreport.js +3 -288
  171. package/dist/src/setup/scanreport.js.map +1 -1
  172. package/dist/src/setup/scanrun.js +3 -1732
  173. package/dist/src/setup/scanrun.js.map +1 -1
  174. package/dist/src/setup/scantree.js +3 -256
  175. package/dist/src/setup/scantree.js.map +1 -1
  176. package/dist/src/setup/stage2request.js +3 -258
  177. package/dist/src/setup/stage2request.js.map +1 -1
  178. package/dist/src/setup/unitcut.js +3 -436
  179. package/dist/src/setup/unitcut.js.map +1 -1
  180. package/dist/src/state.js +75 -9
  181. package/dist/src/state.js.map +1 -1
  182. package/dist/src/takeover.js +1 -0
  183. package/dist/src/takeover.js.map +1 -1
  184. package/dist/src/takeoverpi.js +2 -1
  185. package/dist/src/takeoverpi.js.map +1 -1
  186. package/dist/src/tui/alerts.js +4 -1
  187. package/dist/src/tui/alerts.js.map +1 -1
  188. package/dist/src/tui/board.js +2 -2
  189. package/dist/src/tui/board.js.map +1 -1
  190. package/dist/src/tui/companion.js +2 -1
  191. package/dist/src/tui/companion.js.map +1 -1
  192. package/dist/src/tui/deploytab.js +2 -2
  193. package/dist/src/tui/deploytab.js.map +1 -1
  194. package/dist/src/tui/index.js +9 -6
  195. package/dist/src/tui/index.js.map +1 -1
  196. package/dist/src/tui/memory.js +3 -16
  197. package/dist/src/tui/memory.js.map +1 -1
  198. package/dist/src/tui/memoryurl.js +22 -0
  199. package/dist/src/tui/memoryurl.js.map +1 -0
  200. package/dist/src/tui/remote.js +1 -1
  201. package/dist/src/tui/remote.js.map +1 -1
  202. package/dist/src/tui/route.js +2 -2
  203. package/dist/src/tui/route.js.map +1 -1
  204. package/dist/src/tui/setupstate.js +8 -5
  205. package/dist/src/tui/setupstate.js.map +1 -1
  206. package/dist/src/tui/sshtriage.js +1 -1
  207. package/dist/src/tui/sshtriage.js.map +1 -1
  208. package/dist/src/verdicts.js +12 -0
  209. package/dist/src/verdicts.js.map +1 -1
  210. package/dist/src/wire.js +9 -4
  211. package/dist/src/wire.js.map +1 -1
  212. package/dist/src/workerbreaker.js +32 -5
  213. package/dist/src/workerbreaker.js.map +1 -1
  214. package/dist/src/workerworld.js +4 -0
  215. package/dist/src/workerworld.js.map +1 -1
  216. package/docker/README.md +1 -1
  217. package/docker/entrypoint.sh +40 -6
  218. package/docker-compose.yml +16 -1
  219. package/extensions/cli/home.ts +14 -0
  220. package/extensions/cli/selfimprove.ts +18 -0
  221. package/extensions/home.ts +484 -0
  222. package/extensions/login-route.ts +1 -1
  223. package/extensions/onboarding.ts +8 -7
  224. package/extensions/selfimprove.ts +65 -0
  225. package/extensions/setup-scan.ts +79 -766
  226. package/extensions/setup.ts +14 -70
  227. package/extensions/vendor/openviking-pi/NOTICE +5 -0
  228. package/extensions/vendor/openviking-pi/README.md +39 -5
  229. package/extensions/vendor/openviking-pi/index.ts +4 -0
  230. package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
  231. package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
  232. package/extensions/vendor/openviking-pi/sync.ts +80 -0
  233. package/extensions/vendor/openviking-pi/upstream.json +2 -1
  234. package/package.json +6 -4
  235. package/prompts/onboarding.md +8 -7
  236. package/schemas/review-result.v1.json +60 -0
  237. package/scripts/deploy.sh +8 -6
  238. package/scripts/qcr-host.sh +28 -5
  239. package/scripts/vm-ghcr-login.sh +7 -4
  240. package/src/archive/restore.ts +18 -21
  241. package/src/botworker.ts +13 -2
  242. package/src/channelfs.ts +3 -26
  243. package/src/cli.ts +13 -54
  244. package/src/config.ts +38 -0
  245. package/src/daemon.ts +34 -4
  246. package/src/debuground.ts +3 -0
  247. package/src/debugroundpi.ts +1 -0
  248. package/src/engine/caserunner-sdk.ts +29 -314
  249. package/src/home/adapters.ts +418 -0
  250. package/src/home/advanced.ts +421 -0
  251. package/src/home/advancedtools.ts +96 -0
  252. package/src/home/autoreview.ts +385 -0
  253. package/src/home/daemonstatus.ts +55 -0
  254. package/src/home/devicelogin.ts +107 -0
  255. package/src/home/launch.ts +257 -0
  256. package/src/home/loginowner.ts +248 -0
  257. package/src/home/memory.ts +176 -0
  258. package/src/home/model.ts +965 -0
  259. package/src/home/panel.ts +666 -0
  260. package/src/home/ports.ts +338 -0
  261. package/src/home/review.ts +206 -0
  262. package/src/home/reviews.ts +565 -0
  263. package/src/home/setup.ts +689 -0
  264. package/src/home/setupadapters.ts +217 -0
  265. package/src/home/setupcard.ts +288 -0
  266. package/src/home/setuptools.ts +57 -0
  267. package/src/home/spend.ts +194 -0
  268. package/src/home/switches.ts +293 -0
  269. package/src/home/switchon.ts +554 -0
  270. package/src/home/toolnames.ts +46 -0
  271. package/src/home/tune.ts +405 -0
  272. package/src/home/words.ts +8 -0
  273. package/src/homes.ts +17 -0
  274. package/src/install.ts +1 -1
  275. package/src/ipc/workerspec.ts +15 -0
  276. package/src/ledgercheck.ts +45 -9
  277. package/src/live/publish.ts +4 -1
  278. package/src/livecli.ts +6 -1
  279. package/src/loginrefresh/handover.ts +138 -0
  280. package/src/loginrefresh/lock.ts +72 -3
  281. package/src/loginrefresh/refresher.ts +34 -5
  282. package/src/loginrefresh/supervisor.ts +25 -2
  283. package/src/npmverbs.ts +51 -11
  284. package/src/onboard/choices.ts +6 -3
  285. package/src/onboard/engine.ts +64 -434
  286. package/src/onboard/steps/bringup.ts +3 -2
  287. package/src/onboard/steps/configfill.ts +23 -7
  288. package/src/onboard/steps/loginshare.ts +258 -0
  289. package/src/onboard/steps/memory.ts +18 -2
  290. package/src/onboard/steps/repoconfig.ts +85 -0
  291. package/src/onboard/toolnames.ts +2 -2
  292. package/src/piagentdir.ts +12 -0
  293. package/src/pilogin.ts +32 -16
  294. package/src/qcrbin.ts +16 -11
  295. package/src/reviewargs.ts +119 -0
  296. package/src/reviewcli.ts +6 -167
  297. package/src/reviewdetach.ts +107 -13
  298. package/src/scancli.ts +75 -826
  299. package/src/selfimprove.ts +90 -0
  300. package/src/session-command-supervisor.ts +41 -8
  301. package/src/setup/adapters.ts +13 -221
  302. package/src/setup/caserunner.ts +34 -273
  303. package/src/setup/engine.ts +62 -496
  304. package/src/setup/facts.ts +3 -627
  305. package/src/setup/findings.ts +3 -1264
  306. package/src/setup/presentation.ts +3 -18
  307. package/src/setup/scan.ts +3 -1033
  308. package/src/setup/scanbuild.ts +20 -114
  309. package/src/setup/scanlease.ts +3 -297
  310. package/src/setup/scanreport.ts +3 -373
  311. package/src/setup/scanrun.ts +3 -2268
  312. package/src/setup/scantree.ts +3 -259
  313. package/src/setup/stage2request.ts +3 -347
  314. package/src/setup/unitcut.ts +3 -523
  315. package/src/state.ts +126 -11
  316. package/src/takeover.ts +5 -0
  317. package/src/takeoverpi.ts +3 -2
  318. package/src/tui/alerts.ts +9 -2
  319. package/src/tui/board.ts +2 -2
  320. package/src/tui/companion.ts +2 -1
  321. package/src/tui/deploytab.ts +2 -2
  322. package/src/tui/index.ts +9 -6
  323. package/src/tui/memory.ts +3 -15
  324. package/src/tui/memoryurl.ts +22 -0
  325. package/src/tui/remote.ts +1 -1
  326. package/src/tui/route.ts +2 -2
  327. package/src/tui/setupstate.ts +20 -16
  328. package/src/tui/sshtriage.ts +1 -1
  329. package/src/verdicts.ts +13 -0
  330. package/src/wire.ts +9 -4
  331. package/src/workerbreaker.ts +41 -5
  332. package/src/workerworld.ts +9 -1
  333. package/dist/src/live/projection.js +0 -450
  334. package/dist/src/live/projection.js.map +0 -1
  335. package/dist/src/live/read.js +0 -60
  336. package/dist/src/live/read.js.map +0 -1
  337. package/dist/src/onboard/launch.js +0 -75
  338. package/dist/src/onboard/launch.js.map +0 -1
  339. package/dist/src/setup/ledger.js +0 -207
  340. package/dist/src/setup/ledger.js.map +0 -1
  341. package/dist/src/setup/stage2.js +0 -617
  342. package/dist/src/setup/stage2.js.map +0 -1
  343. package/dist/src/setup/textsniff.js +0 -89
  344. package/dist/src/setup/textsniff.js.map +0 -1
  345. package/src/live/projection.ts +0 -491
  346. package/src/live/read.ts +0 -86
  347. package/src/onboard/launch.ts +0 -103
  348. package/src/setup/ledger.ts +0 -324
  349. package/src/setup/stage2.ts +0 -926
  350. package/src/setup/textsniff.ts +0 -95
@@ -0,0 +1,258 @@
1
+ // Switch-on's login step: one OpenAI login per box. The daemon's
2
+ // container reviews on the login the home signed in to, the host pi home's
3
+ // auth.json, instead of a login of its own.
4
+ //
5
+ // .env QCR_PI_LOGIN_DIR = the home's pi agent dir. docker-compose.yml
6
+ // mounts it at /opt/qcr/login and names it to the daemon
7
+ // (QCR_LOGIN_DIR), so the refresher keeps that one file and every
8
+ // worker borrows it; pi's settings stay in the container's own
9
+ // pi home, never the person's.
10
+ // config [login_refresh] enabled = true, so the daemon runs its single
11
+ // refresher on that file while auto-review runs. The config must
12
+ // still load with it (its margin outlasts the longest worker
13
+ // cap), or nothing is written.
14
+ // restart a stack brought up before is recreated, so the daemon has the
15
+ // mount and reads the config at boot (its volumes, so its data,
16
+ // are kept). A first switch-on's bring-up starts it this way.
17
+ // proof a stack brought up before keeps its image pin and its extracted
18
+ // docker-compose.yml, which may be an older release's. So the
19
+ // compose file must define the mount before anything is written,
20
+ // and the running daemon must show it reviews on the login (its
21
+ // QCR_LOGIN_DIR, the login in the mount, an image whose
22
+ // refresher reads that dir) before the step says so. Until then
23
+ // the step is not done, and the next switch-on runs it again.
24
+ //
25
+ // The hand-over needs nothing more: while the refresher runs it refreshes far
26
+ // ahead of pi's own window, so the home's pi, a borrower now, never has a
27
+ // token to refresh; switching auto-review off stops the daemon, the refresher
28
+ // with it, and the home's pi refreshes on its own again (src/home/loginowner.ts).
29
+ // The single-instance lock lives beside the shared auth.json, so a second
30
+ // refresher on this box is refused whoever starts it.
31
+ //
32
+ // A remote server (the advanced route) never gets this step: it signs in on
33
+ // its own device login, and the host's login is never copied there.
34
+ import { existsSync, readFileSync } from "node:fs";
35
+ import { join } from "node:path";
36
+ import { parse } from "smol-toml";
37
+ import { parseConfig } from "../../config.js";
38
+ import type {
39
+ CheckResult,
40
+ OnboardingStep,
41
+ ProcessRunner,
42
+ StepContext,
43
+ StepReport,
44
+ } from "../engine.js";
45
+ import { CONTAINER_LOGIN_DIR, LOGIN_DIR_ENV } from "../../loginrefresh/supervisor.js";
46
+ import { BROUGHT_UP_CHOICE_KEY } from "./bringup.js";
47
+ import {
48
+ DAEMON_SERVICE,
49
+ composeArgs,
50
+ daemonExec,
51
+ readEnvText,
52
+ releaseVersion,
53
+ rootOf,
54
+ writeAtomic,
55
+ } from "./compose.js";
56
+ import { assertNoDryRunEdit, patchToml, type TomlEdit } from "./configfill.js";
57
+ import { readEnvKey, setEnvKey } from "./ingress.js";
58
+ import { topologyOf } from "./topology.js";
59
+
60
+ export const LOGIN_SHARE_STEP_ID = "login_share";
61
+ /** The project .env key docker-compose.yml mounts the host's login from. */
62
+ export const PI_LOGIN_DIR_ENV = "QCR_PI_LOGIN_DIR";
63
+
64
+ export interface LoginShareOptions {
65
+ /** The home's pi agent dir, where its login is. Absent: nothing is shared
66
+ * and the daemon keeps a login of its own. */
67
+ loginDir?: string;
68
+ }
69
+
70
+ const firstLine = (s: string): string => s.trim().split("\n")[0]?.trim() || "no output";
71
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
72
+
73
+ /** Does the config run the refresher? null when it cannot be read. */
74
+ export function refresherEnabled(configText: string): boolean | null {
75
+ try {
76
+ const t = (parse(configText) as Record<string, unknown>)["login_refresh"];
77
+ return (
78
+ typeof t === "object" && t !== null && (t as Record<string, unknown>)["enabled"] === true
79
+ );
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /** The config with the refresher on; null when it is on already. */
86
+ export function refresherPatch(configText: string): string | null {
87
+ if (refresherEnabled(configText) === true) return null;
88
+ const edits: TomlEdit[] = [{ section: "login_refresh", key: "enabled", value: "true" }];
89
+ assertNoDryRunEdit(edits);
90
+ return patchToml(configText, edits);
91
+ }
92
+
93
+ const fail = (name: string, detail: string): StepReport => ({
94
+ ok: false,
95
+ checks: [{ name, ok: false, detail }],
96
+ });
97
+
98
+ /** The upgrade a stack too old for a shared login needs, said once. */
99
+ function upgradeFix(root: string): string {
100
+ const version = releaseVersion(root);
101
+ return (
102
+ `Fix: update the stack in ${root} to a release with the shared login` +
103
+ `${version ? ` (QCR_VERSION=${version} in its .env, and that release's docker-compose.yml)` : ""}, ` +
104
+ "then switch auto-review on again"
105
+ );
106
+ }
107
+
108
+ /** Does the project's docker-compose.yml mount a shared login? null when
109
+ * there is no compose file yet (the bring-up places it, this release's). */
110
+ export function composeMountsLogin(root: string): boolean | null {
111
+ let text: string;
112
+ try {
113
+ text = readFileSync(join(root, "docker-compose.yml"), "utf8");
114
+ } catch {
115
+ return null;
116
+ }
117
+ return text.includes(PI_LOGIN_DIR_ENV);
118
+ }
119
+
120
+ /** The running daemon, asked whether it reviews on the shared login: the
121
+ * compose definition hands it the dir, the mount holds the login, and its
122
+ * image's refresher reads that dir. Exit codes name which failed. */
123
+ export const DAEMON_LOGIN_PROBE = [
124
+ "sh",
125
+ "-c",
126
+ `test "$${LOGIN_DIR_ENV}" = ${CONTAINER_LOGIN_DIR} || exit 3; ` +
127
+ `test -s ${CONTAINER_LOGIN_DIR}/auth.json || exit 4; ` +
128
+ `grep -q ${LOGIN_DIR_ENV} dist/src/loginrefresh/supervisor.js 2>/dev/null || exit 5`,
129
+ ] as const;
130
+
131
+ /** Ask the running daemon (DAEMON_LOGIN_PROBE). A daemon that cannot be
132
+ * asked is not one that reviews on the login: never claimed. */
133
+ export async function daemonLoginCheck(runner: ProcessRunner, root: string): Promise<CheckResult> {
134
+ const r = await daemonExec(runner, root, DAEMON_LOGIN_PROBE);
135
+ if (r.code === 0)
136
+ return { name: "daemon", ok: true, detail: "the daemon reviews on your login, data kept" };
137
+ const why =
138
+ r.code === 3
139
+ ? `the running daemon is not handed the login (its ${LOGIN_DIR_ENV} is not ${CONTAINER_LOGIN_DIR}): the stack's docker-compose.yml is an older release's`
140
+ : r.code === 4
141
+ ? `the running daemon sees no login at ${CONTAINER_LOGIN_DIR}: the stack's docker-compose.yml does not mount it`
142
+ : r.code === 5
143
+ ? "the running daemon's image predates the shared login: it would keep reviewing on a login of its own"
144
+ : `can't ask the running daemon whether it reviews on your login: ${firstLine(r.stderr || r.stdout)}`;
145
+ return { name: "daemon", ok: false, detail: `${why}. ${upgradeFix(root)}` };
146
+ }
147
+
148
+ export function loginShareStep(o: LoginShareOptions = {}): OnboardingStep {
149
+ const state = async (ctx: StepContext): Promise<StepReport> => {
150
+ if (o.loginDir === undefined)
151
+ return {
152
+ ok: true,
153
+ checks: [{ name: "login", ok: true, detail: "no home login to share here" }],
154
+ };
155
+ const env = readEnvKey(readEnvText(rootOf(ctx.adapters)), PI_LOGIN_DIR_ENV);
156
+ let config: string;
157
+ try {
158
+ config = readFileSync(ctx.adapters.paths.configPath, "utf8");
159
+ } catch (err) {
160
+ return fail("login", `can't read ${ctx.adapters.paths.configPath}: ${message(err)}`);
161
+ }
162
+ const checks: CheckResult[] = [
163
+ env === o.loginDir
164
+ ? { name: "login", ok: true, detail: `auto-review reviews on your login in ${o.loginDir}` }
165
+ : {
166
+ name: "login",
167
+ ok: false,
168
+ detail: `the stack does not mount your login (${PI_LOGIN_DIR_ENV} is ${env === null ? "unset" : JSON.stringify(env)})`,
169
+ },
170
+ refresherEnabled(config) === true
171
+ ? { name: "refresher", ok: true, detail: "its refresher keeps the login fresh" }
172
+ : { name: "refresher", ok: false, detail: "[login_refresh] is not enabled" },
173
+ ];
174
+ return { ok: checks.every((c) => c.ok), checks };
175
+ };
176
+
177
+ return {
178
+ id: LOGIN_SHARE_STEP_ID,
179
+ title: "Share your login with auto-review",
180
+ explain:
181
+ "Auto-review reviews on the OpenAI login this home signed in to: one login per machine, which its refresher keeps fresh while it runs.",
182
+ // after the config steps (memory is 60), before the daemon starts (69)
183
+ order: 66,
184
+ enabled: (choices) => topologyOf(choices) === "local",
185
+ async run(ctx): Promise<StepReport> {
186
+ const dir = o.loginDir;
187
+ if (dir === undefined) return state(ctx);
188
+ const auth = join(dir, "auth.json");
189
+ if (!existsSync(auth))
190
+ return fail(
191
+ "login",
192
+ `there is no login at ${auth} yet: sign in in the home first, then switch auto-review on again`,
193
+ );
194
+ // compose expands `$` in an .env value and treats ` #` as a comment
195
+ if (/[$\n]|\s#/.test(dir))
196
+ return fail(
197
+ "login",
198
+ `${dir} can't be passed to Docker Compose through .env (a \`$\`, a newline or a space before #): set PI_CODING_AGENT_DIR to a plainer path`,
199
+ );
200
+ const configPath = ctx.adapters.paths.configPath;
201
+ let config: string;
202
+ try {
203
+ config = readFileSync(configPath, "utf8");
204
+ } catch (err) {
205
+ return fail("refresher", `can't read ${configPath}: ${message(err)}`);
206
+ }
207
+ const patched = refresherPatch(config);
208
+ if (patched !== null) {
209
+ try {
210
+ parseConfig(patched);
211
+ } catch (err) {
212
+ return fail(
213
+ "refresher",
214
+ `[login_refresh] enabled = true would not load in ${configPath}: ${message(err)} — nothing was written`,
215
+ );
216
+ }
217
+ }
218
+ const root = rootOf(ctx.adapters);
219
+ if (composeMountsLogin(root) === false)
220
+ return fail(
221
+ "daemon",
222
+ `${join(root, "docker-compose.yml")} is an older release's: it does not mount a shared login, so the daemon would keep a login of its own. Nothing was written. ${upgradeFix(root)}`,
223
+ );
224
+ const envText = readEnvText(root);
225
+ const nextEnv = setEnvKey(envText, PI_LOGIN_DIR_ENV, dir);
226
+ if (patched !== null) writeAtomic(configPath, patched);
227
+ if (nextEnv !== envText) writeAtomic(join(root, ".env"), nextEnv);
228
+ const checks: CheckResult[] = [
229
+ {
230
+ name: "login",
231
+ ok: true,
232
+ detail: `auto-review reviews on your login in ${dir}, and its refresher keeps it fresh while it runs`,
233
+ },
234
+ ];
235
+ const changed = patched !== null || nextEnv !== envText;
236
+ if (typeof ctx.choices[BROUGHT_UP_CHOICE_KEY] === "string") {
237
+ if (changed) {
238
+ // the mount and the config are read when the container starts
239
+ const r = await ctx.adapters.runner.run(
240
+ "docker",
241
+ composeArgs(root, ["up", "-d", "--no-deps", "--force-recreate", DAEMON_SERVICE]),
242
+ );
243
+ if (r.code !== 0)
244
+ return fail(
245
+ "daemon",
246
+ `\`docker compose up -d --force-recreate ${DAEMON_SERVICE}\` exited ${r.code}: ${firstLine(r.stderr || r.stdout)}`,
247
+ );
248
+ }
249
+ // a stack brought up before says so itself, or the step is not done
250
+ const daemon = await daemonLoginCheck(ctx.adapters.runner, root);
251
+ checks.push(daemon);
252
+ if (!daemon.ok) return { ok: false, checks };
253
+ }
254
+ return { ok: true, checks };
255
+ },
256
+ verify: state,
257
+ };
258
+ }
@@ -77,7 +77,7 @@ import { dirname, join, resolve, sep } from "node:path";
77
77
  import { pathToFileURL } from "node:url";
78
78
  import { OPENVIKING_DEFAULTS, parseConfig } from "../../config.js";
79
79
  import { configHome, locateKeysFile, locateSecrets } from "../../install.js";
80
- import { tuiMemoryBaseUrl } from "../../tui/memory.js";
80
+ import { tuiMemoryBaseUrl } from "../../tui/memoryurl.js";
81
81
  import { shellQuote } from "../../tui/remote.js";
82
82
  import { CHOICE_KEYS, MEMORY_CHOICE_IDS, type MemoryChoice } from "../choices.js";
83
83
  import { deliverConfigChange } from "./configdelivery.js";
@@ -481,6 +481,22 @@ export const DECLINED_CHECKS: readonly CheckResult[] = [
481
481
  /** The check row that reports the daemon's on/off switch. */
482
482
  export const MEMORY_SWITCH_CHECK = "memory-switch";
483
483
 
484
+ /** The check row that says phase B waits on a server not answering yet. */
485
+ export const MEMORY_SERVER_CHECK = "memory-server";
486
+
487
+ /** A run that landed phase A (the root key) and stopped only because the
488
+ * server is not up yet: on a stack not running memory, the bring-up starts
489
+ * it and the memory_user_keys step finishes phase B. Any other failure is a
490
+ * failure. */
491
+ export function waitsOnServer(checks: readonly CheckResult[]): boolean {
492
+ const failed = checks.filter((c) => !c.ok);
493
+ return (
494
+ failed.length > 0 &&
495
+ failed.every((c) => c.name === MEMORY_SERVER_CHECK) &&
496
+ checks.some((c) => c.name === "root-key" && c.ok)
497
+ );
498
+ }
499
+
484
500
  /**
485
501
  * Tell the DAEMON the operator's answer: `[openviking] enabled =
486
502
  * false` on a decline — its word for "off", under which memory health reports
@@ -1009,7 +1025,7 @@ export function memoryStep(opts: MemoryStepOptions = {}): OnboardingStep {
1009
1025
  const ready = await serverReady(ctx.adapters.fetch, base, prov);
1010
1026
  if (!ready.ok) {
1011
1027
  checks.push({
1012
- name: "memory-server",
1028
+ name: MEMORY_SERVER_CHECK,
1013
1029
  ok: false,
1014
1030
  detail: ready.answering
1015
1031
  ? `OpenViking answers at ${base} but is not ready (${ready.detail}). Check the engine and embedder logs, then run this step again; the memory forward already reaches it.`
@@ -0,0 +1,85 @@
1
+ // The repository the home's setup checklist picks, written into config.toml:
2
+ // the config fill's writer (configfill.ts) over the template or the
3
+ // install's config, never touching dry_run.
4
+ import { parse } from "smol-toml";
5
+ import { assertNoDryRunEdit, changedEdits, patchToml, tomlString } from "./configfill.js";
6
+
7
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
8
+
9
+ /** The repository pick, as the config takes it. */
10
+ export interface RepoConfigChoice {
11
+ repo: string;
12
+ baseBranch: string | null;
13
+ timezone: string | null;
14
+ ownerName: string | null;
15
+ ownerLogin: string | null;
16
+ }
17
+
18
+ /** A config value as text, "" when absent or not a string. */
19
+ function valueIn(doc: Record<string, unknown> | null, section: string, key: string): string {
20
+ const table = doc?.[section];
21
+ if (typeof table !== "object" || table === null) return "";
22
+ const v = (table as Record<string, unknown>)[key];
23
+ return typeof v === "string" ? v.trim() : "";
24
+ }
25
+
26
+ /** The config text with the choice in it, from `existing` (null: none yet,
27
+ * so the template). What changed is named; a config that is not TOML is
28
+ * refused, since what it holds cannot be seen. */
29
+ export function renderRepoConfig(
30
+ existing: string | null,
31
+ template: string,
32
+ choice: RepoConfigChoice,
33
+ ): { ok: true; text: string; wrote: string[] } | { ok: false; why: string } {
34
+ const fresh = existing === null;
35
+ const source = existing ?? template;
36
+ let doc: Record<string, unknown> | null = null;
37
+ try {
38
+ doc = parse(source);
39
+ } catch (err) {
40
+ return {
41
+ ok: false,
42
+ why: `${fresh ? "the shipped template" : "the config"} is not valid TOML (${message(err)}); nothing was written`,
43
+ };
44
+ }
45
+ const fill = (section: string, key: string): boolean =>
46
+ fresh || valueIn(doc, section, key) === "";
47
+ const wanted = [
48
+ { section: "github", key: "repo", value: tomlString(choice.repo) },
49
+ ...(choice.baseBranch
50
+ ? [{ section: "github", key: "base_branch", value: tomlString(choice.baseBranch) }]
51
+ : []),
52
+ ...(choice.timezone && fill("ops", "timezone")
53
+ ? [{ section: "ops", key: "timezone", value: tomlString(choice.timezone) }]
54
+ : []),
55
+ ...(choice.ownerName && fill("owner", "name")
56
+ ? [{ section: "owner", key: "name", value: tomlString(choice.ownerName) }]
57
+ : []),
58
+ ...(choice.ownerLogin && fill("owner", "github_login")
59
+ ? [{ section: "owner", key: "github_login", value: tomlString(choice.ownerLogin) }]
60
+ : []),
61
+ ];
62
+ assertNoDryRunEdit(wanted);
63
+ const edits = changedEdits(source, wanted);
64
+ const text = edits.length > 0 ? patchToml(source, edits) : source;
65
+ let after: Record<string, unknown>;
66
+ try {
67
+ after = parse(text);
68
+ } catch (err) {
69
+ return {
70
+ ok: false,
71
+ why: `the patched config would not parse (${message(err)}); nothing was written`,
72
+ };
73
+ }
74
+ if (valueIn(after, "github", "repo") !== choice.repo)
75
+ return {
76
+ ok: false,
77
+ why: "the patched config does not carry the repository; nothing was written",
78
+ };
79
+ if (fresh && after["dry_run"] !== true)
80
+ return {
81
+ ok: false,
82
+ why: "the shipped template no longer ships dry_run = true; nothing was written (invariant 6)",
83
+ };
84
+ return { ok: true, text, wrote: edits.map((e) => `[${e.section}] ${e.key}`) };
85
+ }
@@ -1,6 +1,6 @@
1
1
  // The onboarding session's tool names: one source for the extension that
2
- // registers them (extensions/onboarding.ts) and the argv that activates them
3
- // (launch.ts `onboardingPiArgv`).
2
+ // registers them (extensions/onboarding.ts) and the `--tools` allowlist that
3
+ // activates them (`onboardingToolAllowlist`).
4
4
  //
5
5
  // pi's `--tools <list>` is an allowlist over EVERY tool, extension tools
6
6
  // included (pi 1.4.0: createAgentSession({ tools }) leaves an extension tool
@@ -0,0 +1,12 @@
1
+ // pi's agent dir, the way pi itself finds it: a leaf, so the npm verbs
2
+ // (src/npmverbs.ts) and the home's login reader (src/home/adapters.ts) read
3
+ // one rule without importing each other.
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ /** pi's agent dir: `$PI_CODING_AGENT_DIR`, else `~/.pi/agent`, as pi reads it. */
8
+ export function piAgentDir(env: Readonly<Record<string, string | undefined>>): string {
9
+ const set = (env["PI_CODING_AGENT_DIR"] ?? "").trim();
10
+ if (set !== "") return set;
11
+ return join((env["HOME"] ?? "").trim() || homedir(), ".pi", "agent");
12
+ }
package/src/pilogin.ts CHANGED
@@ -4,15 +4,24 @@
4
4
  // (src/scanbox.ts) share it without one reaching the other.
5
5
 
6
6
  /** pi itself, with `/login` (or `/login <provider>`) typed and sent for the
7
- * user: pi runs in a fresh tmux session; once its input box is drawn (two
8
- * editor borders on screen) the keys go in, and again every 0.3 s until pi's
9
- * login menu shows — an Enter sent while pi still starts is lost, and a
10
- * `/login` left in the editor then only needs the Enter. After 30 s the
11
- * terminal attaches regardless, so every login pi offers is there and
12
- * quitting pi ends it. `pi` is the pi to start: the companion image's CLI on
13
- * its PATH by default; the release image's entrypoint passes its one pi,
14
- * node_modules/.bin/pi (src/picli.ts `piCliIn`), and `qcr login` the pinned
15
- * package's own bin (src/picli.ts `piCliPath`). */
7
+ * user: pi runs in a fresh tmux session, and qcr reads pi's input box, the
8
+ * lines between the last two editor borders on screen, every 0.3 s (a second
9
+ * after a key, which counts as three polls toward the 30 s). Empty, it gets the keys and an Enter; still holding exactly
10
+ * the keys, one more Enter (an Enter sent while pi still starts is lost, and
11
+ * the first one may only accept the autocomplete). Anything else in the box is
12
+ * one of pi's menus (the provider list, a provider's own login methods), and
13
+ * no key goes in again. pi draws every selection in that box, so this holds
14
+ * whatever title pi or a provider gives the menu, where an Enter would pick
15
+ * its default: a browser login, opened again on every poll. pi starts with
16
+ * `--no-approve`: a login needs nothing from the folder qcr was run in, so a
17
+ * project folder neither stops it at pi's trust prompt nor gets trusted. A pi
18
+ * that has already exited ends the wait at once. After 30 s the terminal
19
+ * attaches regardless, so every login pi offers is there and quitting pi ends
20
+ * it; a failed attach (no terminal) ends the session instead of leaving pi
21
+ * running where nobody can reach it. `pi` is the pi to start: the
22
+ * companion image's CLI on its PATH by default; the release image's entrypoint
23
+ * passes its one pi, node_modules/.bin/pi (src/picli.ts `piCliIn`), and
24
+ * `qcr login` the pinned package's own bin (src/picli.ts `piCliPath`). */
16
25
  export function loginInPi(provider?: string, pi: string = "pi"): string[] {
17
26
  if (provider !== undefined && !/^[A-Za-z0-9._-]+$/.test(provider))
18
27
  throw new Error(
@@ -22,16 +31,23 @@ export function loginInPi(provider?: string, pi: string = "pi"): string[] {
22
31
  if (!/^[A-Za-z0-9._/@+-]+$/.test(pi))
23
32
  throw new Error(`scan login: pi ${JSON.stringify(pi)} is not a plain path`);
24
33
  const keys = provider ? `/login ${provider}` : "/login";
34
+ // The input box, its side padding (pi's editorPaddingX) trimmed and blank
35
+ // lines dropped; awk fails while fewer than two borders are drawn (pi still
36
+ // starting), and the loop then only waits.
37
+ const box =
38
+ `tmux capture-pane -p -t "$s" 2>/dev/null | awk '/──────────/ { b[++n] = NR } { l[NR] = $0 } ` +
39
+ `END { if (n < 2) exit 1; for (i = b[n - 1] + 1; i < b[n]; i++) ` +
40
+ `{ sub(/^ +/, "", l[i]); sub(/ +$/, "", l[i]); if (l[i] != "") print l[i] } }'`;
25
41
  return [
26
42
  "sh",
27
43
  "-c",
28
- `s=qcr-login-$$; tmux new-session -d -s "$s" ${pi} || exit 1; i=0; ` +
29
- `while [ $i -lt 100 ]; do p=$(tmux capture-pane -p -t "$s"); ` +
30
- `case "$p" in *"Select authentication"*|*"Select provider"*) break;; esac; ` +
31
- `if [ "$(printf '%s\\n' "$p" | grep -cF '──────────')" -ge 2 ]; then ` +
32
- `if printf '%s\\n' "$p" | grep -qxF '${keys}'; then tmux send-keys -t "$s" Enter; ` +
33
- `else tmux send-keys -t "$s" -l '${keys}'; tmux send-keys -t "$s" Enter; fi; fi; ` +
44
+ `s=qcr-login-$$; tmux new-session -d -s "$s" ${pi} --no-approve || exit 1; i=0; ` +
45
+ `while [ $i -lt 100 ]; do tmux has-session -t "$s" 2>/dev/null || break; ` +
46
+ `if e=$(${box}); then ` +
47
+ `if [ -z "$e" ]; then tmux send-keys -t "$s" -l '${keys}'; tmux send-keys -t "$s" Enter; sleep 1; i=$((i+3)); ` +
48
+ `elif [ "$e" = '${keys}' ]; then tmux send-keys -t "$s" Enter; sleep 1; i=$((i+3)); ` +
49
+ `else break; fi; fi; ` +
34
50
  `sleep 0.3; i=$((i+1)); done; ` +
35
- `exec tmux attach -t "$s"`,
51
+ `tmux attach -t "$s" || { tmux kill-session -t "$s" 2>/dev/null; exit 1; }`,
36
52
  ];
37
53
  }
package/src/qcrbin.ts CHANGED
@@ -3,11 +3,12 @@
3
3
  // verbs a person runs from an npm install are routed here first and load only
4
4
  // what they use, so they work where that native driver cannot load:
5
5
  //
6
- // qcr the admin TUI (`qcr tui`)
6
+ // qcr the home: the app's own pi (src/home/launch.ts)
7
+ // qcr tui the admin TUI (src/cli.ts)
7
8
  // qcr version the install's identity (src/version.ts)
8
9
  // qcr review … a daemonless review (src/reviewcli.ts)
9
10
  // qcr login [p] pi's own /login in the pinned pi (src/npmverbs.ts)
10
- // qcr pi [args] the pinned pi with the qcr plugin preloaded
11
+ // qcr pi [args] the pinned pi with the qcr plugin and /selfimprove
11
12
  // qcr update the installing package manager updates qcr
12
13
  //
13
14
  // Every other verb goes to src/cli.ts `main` unchanged. The image's
@@ -22,10 +23,11 @@ import { MissingConfigError } from "./missingconfig.js";
22
23
  export const BIN_HELP = [
23
24
  "qcr — QuickReview",
24
25
  "",
25
- " qcr open the admin TUI",
26
+ " qcr open the home: pi with the plugin and your pull requests",
27
+ " qcr tui open the admin TUI",
26
28
  " qcr review [target] review code here, with no daemon (`qcr review --help`)",
27
29
  " qcr login [provider] log in with pi's own /login, for the review sessions",
28
- " qcr pi [args] pi with the qcr plugin (/review, /scan, /selfimprove, /qcr-setup)",
30
+ " qcr pi [args] pi with the qcr plugin (/qcr-review, /qcr-scan, /qcr-setup) and /selfimprove",
29
31
  " qcr update update qcr with the package manager that installed it",
30
32
  " qcr version [--json] this install's version, schemas and pi pin",
31
33
  "",
@@ -33,19 +35,19 @@ export const BIN_HELP = [
33
35
  ].join("\n");
34
36
 
35
37
  /** The verbs that never print the update notice: the ones that hand the
36
- * terminal over (the TUI, pi) or are about versions themselves. */
37
- const QUIET_VERBS = new Set(["tui", "pi", "update", "version", "login"]);
38
+ * terminal over (the home, the TUI, pi) or are about versions themselves. */
39
+ const QUIET_VERBS = new Set(["home", "tui", "pi", "update", "version", "login"]);
38
40
 
39
41
  /** What `runBin` would do with `argv`, before doing it. */
40
42
  export type BinRoute =
41
- | { route: "tui" }
43
+ | { route: "home" }
42
44
  | { route: "help" }
43
45
  | { route: "pi"; args: string[] }
44
46
  | { route: "version" | "update" | "login" | "review"; args: string[]; json: boolean }
45
47
  | { route: "app"; argv: string[] };
46
48
 
47
49
  export function binRoute(argv: readonly string[]): BinRoute {
48
- if (argv.length === 0) return { route: "tui" };
50
+ if (argv.length === 0 || (argv.length === 1 && argv[0] === "home")) return { route: "home" };
49
51
  // pi's arguments are pi's, `--json` included
50
52
  if (argv[0] === "pi") return { route: "pi", args: argv.slice(1) };
51
53
  // `qcr message` carries free text: src/cli.ts reads its argv as it is
@@ -115,8 +117,11 @@ async function maybeNotice(json: boolean): Promise<void> {
115
117
 
116
118
  async function route(r: BinRoute): Promise<void> {
117
119
  switch (r.route) {
118
- case "tui":
119
- return appMain(["tui"]);
120
+ case "home": {
121
+ const { homeRunDeps, runHome } = await import("./home/launch.js");
122
+ process.exitCode = runHome(homeRunDeps(await packageRoot()));
123
+ return;
124
+ }
120
125
  case "help":
121
126
  console.log(BIN_HELP);
122
127
  try {
@@ -181,6 +186,6 @@ export async function runBin(argv: readonly string[]): Promise<void> {
181
186
  return;
182
187
  }
183
188
  const verb = r.route === "app" ? r.argv.find((a) => a !== "--json") : r.route;
184
- if (r.route !== "tui" && r.route !== "help" && !QUIET_VERBS.has(verb ?? ""))
189
+ if (r.route !== "help" && !QUIET_VERBS.has(verb ?? ""))
185
190
  await maybeNotice("json" in r ? r.json : r.route === "app" && r.argv.includes("--json"));
186
191
  }