@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,217 @@
1
+ // The setup checklist's real ports (src/home/setup.ts), and the one function
2
+ // that turns a process's environment into them: the env shim
3
+ // (extensions/cli/home.ts) calls `homeSetupFromEnv(process.env, VERSION)`.
4
+ //
5
+ // first-run checks the plugin's pi-version and git steps, over a
6
+ // PluginHost of this process (its PATH, the running pi)
7
+ // repository pick `gh`: the person's recently pushed repositories, the
8
+ // chosen one's canonical name and default branch, and the
9
+ // account's login and name
10
+ // the write the app config the home's repo port reads
11
+ // (locateConfig: $QCR_CONFIG, else the install home's
12
+ // config.toml), patched the way the onboarding config
13
+ // fill patches it (src/onboard/steps/configfill.ts):
14
+ // comment-preserving, only the keys that change, never
15
+ // dry_run. A fresh config starts from the shipped
16
+ // template, whose dry_run = true it keeps (invariant 6).
17
+ // timezone the machine's IANA zone, as Intl resolves it
18
+ //
19
+ // The silent defaults ride on the repository write: the base branch is the
20
+ // chosen repository's (always: it belongs to that repository), and the
21
+ // timezone and the owner's name and login fill a value only while it is
22
+ // empty (or the config is new), so an owner's own choice is never overwritten.
23
+ import { readFileSync } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { join } from "node:path";
26
+ import { assetRoot } from "@quickchatai/pi-quickreview/src/assetroot.js";
27
+ import { nodeRunner } from "@quickchatai/pi-quickreview/src/plugin/runner.js";
28
+ import type { PluginHost } from "@quickchatai/pi-quickreview/src/plugin/session.js";
29
+ import { declaredPiRange } from "@quickchatai/pi-quickreview/src/plugin/versiongate.js";
30
+ import { writeAtomic } from "@quickchatai/pi-quickreview/src/setupchecklist.js";
31
+ import { locateConfig, stateHome } from "../homes.js";
32
+ import { piAgentDir } from "../piagentdir.js";
33
+ import { execRunner, type Runner, type RunResult } from "./adapters.js";
34
+ import { known, unknown, type Known } from "./ports.js";
35
+ import type { HomeSetupPorts, RepoPickPort, RepoWrite } from "./setup.js";
36
+
37
+ type Env = Readonly<Record<string, string | undefined>>;
38
+
39
+ const firstLine = (s: string): string => s.trim().split("\n")[0]?.trim() || "no output";
40
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
41
+
42
+ /** gh's words for "you are not signed in". */
43
+ const SIGNED_OUT = /gh auth login|not logged in|authentication required|HTTP 401|bad credentials/i;
44
+
45
+ function ghWhy(r: RunResult): string {
46
+ if (r.missing) return "gh is not installed";
47
+ if (SIGNED_OUT.test(r.stderr)) return "gh is signed out: run `gh auth login`";
48
+ return firstLine(r.stderr);
49
+ }
50
+
51
+ /** The machine's IANA timezone, or null when Intl cannot say. */
52
+ export function systemTimezone(): string | null {
53
+ try {
54
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
55
+ return typeof tz === "string" && tz !== "" ? tz : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /** A file's text; null only when it does not exist. Any other failure
62
+ * throws: a config that exists and cannot be read is never "no config", or
63
+ * the template would be written over it. */
64
+ function readIfThere(path: string): string | null {
65
+ try {
66
+ return readFileSync(path, "utf8");
67
+ } catch (err) {
68
+ if ((err as NodeJS.ErrnoException | null)?.code === "ENOENT") return null;
69
+ throw err;
70
+ }
71
+ }
72
+
73
+ export function repoPickPort(o: {
74
+ runner: Runner;
75
+ configPath: () => string;
76
+ templatePath: () => string;
77
+ read?: (path: string) => string | null;
78
+ write?: (path: string, text: string) => void;
79
+ }): RepoPickPort {
80
+ const read = o.read ?? readIfThere;
81
+ const write = o.write ?? writeAtomic;
82
+ return {
83
+ recent: async () => {
84
+ const r = await o.runner.run("gh", [
85
+ "repo",
86
+ "list",
87
+ "--limit",
88
+ "8",
89
+ "--json",
90
+ "nameWithOwner",
91
+ ]);
92
+ if (r.code !== 0) return unknown(ghWhy(r));
93
+ try {
94
+ const rows = JSON.parse(r.stdout) as unknown;
95
+ if (!Array.isArray(rows)) return unknown("gh's answer is not a list");
96
+ return known(
97
+ rows.flatMap((x: { nameWithOwner?: unknown }) =>
98
+ typeof x?.nameWithOwner === "string" ? [x.nameWithOwner] : [],
99
+ ),
100
+ );
101
+ } catch (err) {
102
+ return unknown(`gh's answer is not JSON: ${message(err)}`);
103
+ }
104
+ },
105
+ describe: async (repo) => {
106
+ const r = await o.runner.run("gh", [
107
+ "repo",
108
+ "view",
109
+ repo,
110
+ "--json",
111
+ "nameWithOwner,defaultBranchRef",
112
+ ]);
113
+ if (r.code !== 0) return { ok: false, why: ghWhy(r) };
114
+ try {
115
+ const v = JSON.parse(r.stdout) as {
116
+ nameWithOwner?: unknown;
117
+ defaultBranchRef?: { name?: unknown } | null;
118
+ };
119
+ const branch = v.defaultBranchRef?.name;
120
+ return {
121
+ ok: true,
122
+ repo: typeof v.nameWithOwner === "string" ? v.nameWithOwner : repo,
123
+ defaultBranch: typeof branch === "string" && branch !== "" ? branch : null,
124
+ };
125
+ } catch (err) {
126
+ return { ok: false, why: `gh's answer is not JSON: ${message(err)}` };
127
+ }
128
+ },
129
+ profile: async (): Promise<Known<{ login: string; name: string | null }>> => {
130
+ const r = await o.runner.run("gh", ["api", "user"]);
131
+ if (r.code !== 0) return unknown(ghWhy(r));
132
+ try {
133
+ const v = JSON.parse(r.stdout) as { login?: unknown; name?: unknown };
134
+ if (typeof v.login !== "string") return unknown("gh's account has no login");
135
+ return known({
136
+ login: v.login,
137
+ name: typeof v.name === "string" && v.name.trim() !== "" ? v.name.trim() : null,
138
+ });
139
+ } catch (err) {
140
+ return unknown(`gh's answer is not JSON: ${message(err)}`);
141
+ }
142
+ },
143
+ write: async (choice): Promise<RepoWrite> => {
144
+ let path: string;
145
+ try {
146
+ path = o.configPath();
147
+ } catch (err) {
148
+ return { ok: false, why: message(err) };
149
+ }
150
+ let existing: string | null;
151
+ let template: string | null = "";
152
+ try {
153
+ existing = read(path);
154
+ } catch (err) {
155
+ return { ok: false, why: `can't read ${path} (${message(err)}); nothing was written` };
156
+ }
157
+ try {
158
+ if (existing === null) template = read(o.templatePath());
159
+ } catch {
160
+ template = null;
161
+ }
162
+ if (template === null)
163
+ return { ok: false, why: `there is no ${path} and the shipped template is not readable` };
164
+ // the config fill's writer, loaded only when a repository is written
165
+ const { renderRepoConfig } = await import("../onboard/steps/repoconfig.js");
166
+ const r = renderRepoConfig(existing, template, choice);
167
+ if (!r.ok) return r;
168
+ try {
169
+ if (r.text !== existing) write(path, r.text);
170
+ } catch (err) {
171
+ return { ok: false, why: `can't write ${path}: ${message(err)}` };
172
+ }
173
+ return { ok: true, path, wrote: r.wrote };
174
+ },
175
+ };
176
+ }
177
+
178
+ /** The PluginHost the first-run checks read: this process's PATH and the
179
+ * running pi (its version, which the env shim reads off pi). Only the
180
+ * plugin's pi-version and git steps run over it, so its auth answers
181
+ * nothing. */
182
+ export function firstRunHost(env: Env, piVersion: string): PluginHost {
183
+ return {
184
+ cwd: join(stateHome(env), "home"),
185
+ env,
186
+ home: (env["HOME"] ?? "").trim() || homedir(),
187
+ piVersion,
188
+ agentDir: piAgentDir(env),
189
+ auth: {
190
+ logins: () => ({}),
191
+ configured: () => false,
192
+ refresh: () => Promise.reject(new Error("the home's first-run checks hold no login")),
193
+ },
194
+ runner: nodeRunner(env),
195
+ shutdown: new AbortController().signal,
196
+ };
197
+ }
198
+
199
+ /** The setup's options from a process's environment and the running pi's
200
+ * version: the env shim's one call. */
201
+ export function homeSetupFromEnv(env: Env, piVersion: string): HomeSetupPorts {
202
+ let range: string | null = null;
203
+ try {
204
+ range = declaredPiRange();
205
+ } catch {
206
+ range = null;
207
+ }
208
+ return {
209
+ ...(range !== null ? { firstRun: { host: firstRunHost(env, piVersion), range } } : {}),
210
+ repos: repoPickPort({
211
+ runner: execRunner,
212
+ configPath: () => locateConfig({ env }).path,
213
+ templatePath: () => join(assetRoot(), "config.example.toml"),
214
+ }),
215
+ timezone: systemTimezone,
216
+ };
217
+ }
@@ -0,0 +1,288 @@
1
+ // The setup card: on a machine that is not set up, the home opens on this,
2
+ // centred at half the terminal's width and about half its height, instead of the panel. It draws the setup checklist
3
+ // (src/home/setup.ts) in pi's theme tokens; when the last step is done the
4
+ // extension swaps it for the panel at the top right.
5
+ //
6
+ // ╭─ quickreview ──────────────────────────── setup ─╮
7
+ // │ Welcome to quickreview 1 of 3 done │
8
+ // │ ✓ Sign in to OpenAI ChatGPT · signed in with pi │
9
+ // │ ▸ Connect GitHub alt+n check │ the next step
10
+ // │ gh is signed out: run `gh auth login` … │
11
+ // │ ○ Choose a repository │
12
+ // ├──────────────────────────────────────────────────┤
13
+ // │ the notice, then the key hints │
14
+ // ╰──────────────────────────────────────────────────╯
15
+ import type { Theme } from "@earendil-works/pi-coding-agent";
16
+ import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
17
+ import {
18
+ matchesKey,
19
+ truncateToWidth,
20
+ visibleWidth,
21
+ wrapTextWithAnsi,
22
+ } from "@earendil-works/pi-tui";
23
+ import type { HomeModel } from "./model.js";
24
+ import { loginFailureText, type HomeSetup, type SetupStepView } from "./setup.js";
25
+
26
+ type Fg = Parameters<Theme["fg"]>[0];
27
+
28
+ /** Where the card sits: centred, half the width. */
29
+ export const SETUP_OVERLAY = {
30
+ anchor: "center",
31
+ width: "50%",
32
+ minWidth: 80,
33
+ nonCapturing: true,
34
+ } as const;
35
+
36
+ /** The share of the terminal's height the card fills. */
37
+ export const CARD_HEIGHT = 0.5;
38
+ /** Sub-rows (instructions, the device code) are indented under the label. */
39
+ const INDENT = " ";
40
+ const NOTICE_ROWS = 3;
41
+ const SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
42
+
43
+ export interface CardHost {
44
+ setup: HomeSetup;
45
+ model: HomeModel;
46
+ focused(): boolean;
47
+ /** The terminal's height. */
48
+ rows(): number;
49
+ unfocus(): void;
50
+ frame(): number;
51
+ /** Run the next step (⏎ while the card is focused). */
52
+ next(): void;
53
+ }
54
+
55
+ /** What each step's key does, in the hint beside it. */
56
+ const VERB: Record<SetupStepView["id"], string> = {
57
+ login: "sign in",
58
+ github: "check",
59
+ repo: "choose",
60
+ };
61
+
62
+ export class SetupCard implements Component {
63
+ constructor(
64
+ private readonly th: Theme,
65
+ private readonly host: CardHost,
66
+ ) {}
67
+
68
+ private c(color: Fg, s: string): string {
69
+ return this.th.fg(color, s);
70
+ }
71
+
72
+ private spin(): string {
73
+ return SPIN[this.host.frame() % SPIN.length]!;
74
+ }
75
+
76
+ private edge(s: string): string {
77
+ return this.c(this.host.focused() ? "borderAccent" : "borderMuted", s);
78
+ }
79
+
80
+ /** The key that runs the next step now. */
81
+ private key(): string {
82
+ return this.host.focused() ? "⏎" : "alt+n";
83
+ }
84
+
85
+ private row(w: number, left: string, right = "", selected = false): string {
86
+ const inner = w - 4;
87
+ const r = right ? ` ${right}` : "";
88
+ const l = truncateToWidth(left, Math.max(1, inner - visibleWidth(r)), "…");
89
+ const gap = Math.max(0, inner - visibleWidth(l) - visibleWidth(r));
90
+ let text = ` ${l}${" ".repeat(gap)}${r} `;
91
+ if (selected) text = this.th.bg("selectedBg", text);
92
+ return `${this.edge("│")}${text}${this.edge("│")}`;
93
+ }
94
+
95
+ /** Text under a step, wrapped inside the frame. */
96
+ private sub(w: number, text: string, color: Fg = "muted"): string[] {
97
+ const room = Math.max(10, w - 4 - INDENT.length);
98
+ return wrapTextWithAnsi(text, room).map((l) => this.row(w, `${INDENT}${this.c(color, l)}`));
99
+ }
100
+
101
+ private top(w: number): string {
102
+ const left = `${this.edge("╭─ ")}${this.th.bold(this.c("accent", "quickreview"))} `;
103
+ const right = ` ${this.c("muted", "setup")}${this.edge(" ─╮")}`;
104
+ const fill = Math.max(0, w - visibleWidth(left) - visibleWidth(right));
105
+ return `${left}${this.edge("─".repeat(fill))}${right}`;
106
+ }
107
+
108
+ private rule(w: number): string {
109
+ return this.edge(`├${"─".repeat(Math.max(0, w - 2))}┤`);
110
+ }
111
+
112
+ private bottom(w: number): string {
113
+ return this.edge(`╰${"─".repeat(Math.max(0, w - 2))}╯`);
114
+ }
115
+
116
+ private stepRows(w: number, s: SetupStepView): string[] {
117
+ const v = this.host.setup.view();
118
+ const out: string[] = [];
119
+ const k = this.key();
120
+ let mark: string;
121
+ let label: string;
122
+ let right = "";
123
+ if (s.state === "done") {
124
+ mark = this.c("success", "✓");
125
+ label = this.c("muted", s.title);
126
+ right = this.c("muted", s.found ?? "done");
127
+ } else if (s.state === "running") {
128
+ mark = this.c("accent", this.spin());
129
+ label = this.th.bold(this.c("text", s.title));
130
+ right = this.c("dim", v.login.code ? "waiting for you…" : "starting…");
131
+ } else if (s.unknown !== undefined) {
132
+ mark = this.c("dim", "?");
133
+ label = s.next ? this.th.bold(this.c("text", s.title)) : this.c("text", s.title);
134
+ right = s.next
135
+ ? `${this.c("accent", k)} ${this.c("muted", VERB[s.id])}`
136
+ : this.c("dim", "unknown");
137
+ } else if (s.next) {
138
+ mark = this.c("accent", "▸");
139
+ label = this.th.bold(this.c("text", s.title));
140
+ right = `${this.c("accent", k)} ${this.c("muted", VERB[s.id])}`;
141
+ } else {
142
+ mark = this.c("dim", "○");
143
+ label = this.c("dim", s.title);
144
+ }
145
+ out.push(this.row(w, ` ${mark} ${label}`, right, s.next));
146
+ if (s.state === "done") return out;
147
+ if (s.id === "login") out.push(...this.loginRows(w, s));
148
+ else if (s.unknown !== undefined)
149
+ out.push(...this.sub(w, `Unknown — ${s.unknown}.`, "warning"));
150
+ else if (s.next && s.id === "github" && s.missing)
151
+ out.push(...this.sub(w, `${capital(s.missing)}, then ${k} checks again.`, "warning"));
152
+ else if (s.next && s.id === "repo")
153
+ out.push(
154
+ ...this.sub(
155
+ w,
156
+ "One of your recently pushed repositories, or paste any owner/name or GitHub URL.",
157
+ ),
158
+ );
159
+ return out;
160
+ }
161
+
162
+ private loginRows(w: number, s: SetupStepView): string[] {
163
+ const v = this.host.setup.view();
164
+ const k = this.key();
165
+ const out: string[] = [];
166
+ const chatgpt = v.provider === "OpenAI";
167
+ const setting = "Device-code login must be on first: ChatGPT → Settings → Security.";
168
+ if (s.state === "running") {
169
+ const code = v.login.code;
170
+ if (!code) {
171
+ if (chatgpt) out.push(...this.sub(w, setting));
172
+ out.push(
173
+ this.row(
174
+ w,
175
+ `${INDENT}${this.c("accent", this.spin())} ${this.c("dim", "asking for a code…")}`,
176
+ ),
177
+ );
178
+ return out;
179
+ }
180
+ out.push(this.row(w, ""));
181
+ out.push(
182
+ this.row(
183
+ w,
184
+ `${INDENT}${this.c("muted", "1 Open ")} ${this.c("accent", code.verificationUri)}`,
185
+ ),
186
+ );
187
+ out.push(
188
+ this.row(
189
+ w,
190
+ `${INDENT}${this.c("muted", "2 Enter")} ${this.th.bold(this.c("text", code.userCode))}`,
191
+ ),
192
+ );
193
+ out.push(this.row(w, ""));
194
+ if (chatgpt) out.push(...this.sub(w, setting, "dim"));
195
+ out.push(
196
+ this.row(
197
+ w,
198
+ `${INDENT}${this.c("accent", this.spin())} ${this.c("dim", "waiting for you to approve…")}`,
199
+ ),
200
+ );
201
+ return out;
202
+ }
203
+ if (v.login.failure) out.push(...this.sub(w, loginFailureText(v.login.failure, k), "warning"));
204
+ if (s.unknown !== undefined) out.push(...this.sub(w, `Unknown — ${s.unknown}.`, "warning"));
205
+ if (s.next && chatgpt && v.login.failure?.kind !== "not_enabled")
206
+ out.push(...this.sub(w, setting));
207
+ return out;
208
+ }
209
+
210
+ private hints(w: number): string {
211
+ const k = (key: string, label: string): string =>
212
+ `${this.c("accent", key)} ${this.c("muted", label)}`;
213
+ return this.row(
214
+ w,
215
+ this.host.focused()
216
+ ? `${k("⏎", "next step")} ${k("esc", "back to the chat")}`
217
+ : `${k("alt+n", "next step")} ${k("f2", "focus the card")} ${this.c("dim", "· or just type to ask pi")}`,
218
+ );
219
+ }
220
+
221
+ render(width: number): string[] {
222
+ const w = Math.max(40, width);
223
+ const v = this.host.setup.view();
224
+ const body = [
225
+ this.row(w, ""),
226
+ this.row(
227
+ w,
228
+ ` ${this.th.bold(this.c("text", "Welcome to quickreview"))}`,
229
+ this.c("muted", `${v.done} of ${v.total} done `),
230
+ ),
231
+ this.row(
232
+ w,
233
+ ` ${this.c("muted", "Sign in once, choose a repository, and review your first pull request.")}`,
234
+ ),
235
+ this.row(w, ""),
236
+ ];
237
+ for (const c of v.checks) {
238
+ body.push(
239
+ this.row(
240
+ w,
241
+ ` ${this.c("warning", "✗")} ${this.th.bold(this.c("text", c.title))}`,
242
+ c.next ? `${this.c("accent", this.key())} ${this.c("muted", "check")}` : "",
243
+ c.next,
244
+ ),
245
+ );
246
+ body.push(
247
+ ...this.sub(
248
+ w,
249
+ `${capital(c.why)}${c.next ? `, then ${this.key()} checks again.` : "."}`,
250
+ "warning",
251
+ ),
252
+ );
253
+ }
254
+ for (const s of v.steps) body.push(...this.stepRows(w, s));
255
+ body.push(this.row(w, ""));
256
+ const foot = [this.rule(w)];
257
+ const n = this.host.model.notice;
258
+ if (n) {
259
+ const color: Fg = n.kind === "ok" ? "success" : n.kind === "warn" ? "warning" : "text";
260
+ for (const l of wrapTextWithAnsi(n.text, w - 4).slice(0, NOTICE_ROWS))
261
+ foot.push(this.row(w, this.c(color, l)));
262
+ }
263
+ foot.push(this.hints(w), this.bottom(w));
264
+ const target = Math.round(this.host.rows() * CARD_HEIGHT);
265
+ const spare = Math.max(0, target - 1 - body.length - foot.length);
266
+ const blank = (k: number): string[] => Array.from({ length: k }, () => this.row(w, ""));
267
+ return [
268
+ this.top(w),
269
+ ...blank(Math.floor(spare / 2)),
270
+ ...body,
271
+ ...blank(Math.ceil(spare / 2)),
272
+ ...foot,
273
+ ].map((l) => truncateToWidth(l, w));
274
+ }
275
+
276
+ handleInput(data: string): void {
277
+ if (matchesKey(data, "escape")) this.host.unfocus();
278
+ else if (matchesKey(data, "enter")) this.host.next();
279
+ }
280
+
281
+ handleMouse(e: TuiMouseEvent): TuiMouseEventResult | undefined {
282
+ return e.type === "click" ? { focus: true } : undefined;
283
+ }
284
+
285
+ invalidate(): void {}
286
+ }
287
+
288
+ const capital = (s: string): string => (s ? `${s[0]!.toUpperCase()}${s.slice(1)}` : s);
@@ -0,0 +1,57 @@
1
+ // The setup checklist's `home_*` tools: each step's action, as the model in
2
+ // the chat calls it. The keys call the same HomeSetup methods
3
+ // (extensions/home.ts), so a key and a tool never do two different things.
4
+ import { textResult, type QcrTool } from "@quickchatai/pi-quickreview/extensions/pi-tool.js";
5
+ import type { HomeSetup } from "./setup.js";
6
+ import type { HOME_SETUP_TOOL_NAMES } from "./toolnames.js";
7
+
8
+ export type HomeSetupToolName = (typeof HOME_SETUP_TOOL_NAMES)[number];
9
+
10
+ const NO_PARAMS = { type: "object", properties: {}, additionalProperties: false } as const;
11
+
12
+ /** One definition per HOME_SETUP_TOOL_NAMES entry. */
13
+ export function setupTools(setup: HomeSetup): {
14
+ [N in HomeSetupToolName]: QcrTool & { name: N };
15
+ } {
16
+ return {
17
+ home_setup_next: {
18
+ name: "home_setup_next",
19
+ label: "Home: next setup step",
20
+ description:
21
+ "Run the next step of the quickreview home's setup checklist (sign in, connect GitHub, choose a repository), as alt+n does. Answers what happened, or that setup is done.",
22
+ parameters: NO_PARAMS,
23
+ execute: async () => textResult(await setup.next()),
24
+ },
25
+ home_sign_in: {
26
+ name: "home_sign_in",
27
+ label: "Home: sign in",
28
+ description:
29
+ "Start pi's own device-code login for the provider the chat runs on. Answers with the URL and the code to enter once pi has them; the sign-in finishes in the background when the person approves it. Device-code login must be on in ChatGPT → Settings → Security.",
30
+ parameters: NO_PARAMS,
31
+ execute: async () => textResult(await setup.signIn()),
32
+ },
33
+ home_check_github: {
34
+ name: "home_check_github",
35
+ label: "Home: check GitHub",
36
+ description:
37
+ "Check again whether gh is installed and signed in. When it is not, the answer says what to run.",
38
+ parameters: NO_PARAMS,
39
+ execute: async () => textResult(await setup.checkGithub()),
40
+ },
41
+ home_choose_repo: {
42
+ name: "home_choose_repo",
43
+ label: "Home: choose a repository",
44
+ description:
45
+ "Set the repository the home reviews: owner/name or any GitHub URL of it. It is written to the qcr config's [github] repo. Without `repo`, answers the person's recently pushed repositories to choose from.",
46
+ parameters: {
47
+ type: "object",
48
+ properties: {
49
+ repo: { type: "string", description: "owner/name, or a GitHub URL of the repository." },
50
+ },
51
+ additionalProperties: false,
52
+ },
53
+ execute: async (_id, p) =>
54
+ textResult(await setup.chooseRepo(typeof p["repo"] === "string" ? p["repo"] : undefined)),
55
+ },
56
+ };
57
+ }