@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,689 @@
1
+ // The home's setup checklist: what a fresh machine walks through before the
2
+ // home opens on its pull requests. It is the plugin's step registry
3
+ // (@quickchatai/pi-quickreview/src/setupchecklist.ts) with the app's steps
4
+ // registered onto it, as ADR 0001 names the seam:
5
+ //
6
+ // pi_version, git the plugin's own first-run checks: the card shows one
7
+ // only while it fails
8
+ // login pi's own device-code login for the provider the chat
9
+ // runs on (openai-codex): it starts by itself, the card
10
+ // shows the URL and the code, and it retries cleanly
11
+ // github `gh` installed and signed in; one key checks again
12
+ // repo the repository: one of the person's recently pushed
13
+ // ones, or any owner/name or GitHub URL, written to the
14
+ // app config's `[github] repo` with the silent defaults
15
+ // (timezone, base branch, the owner's name and login)
16
+ //
17
+ // Each step's detect runs first, over what the home has just read (the
18
+ // HomeModel's slots), and a step already true is done with what was found.
19
+ // A step whose source cannot be read is Unknown: it stays pending, and is
20
+ // never a gap that holds the card up (invariant 9). The card shows while a
21
+ // step that a read showed to be missing is left; when the last one is done
22
+ // the panel turns into the home.
23
+ //
24
+ // Every step is one method here, called by a key (alt+n, ⏎ on the focused
25
+ // card) and by a `home_*` tool alike (src/home/setuptools.ts). The checklist
26
+ // lives in memory: every launch detects afresh.
27
+ import {
28
+ PLUGIN_SETUP_STEPS,
29
+ pluginSetupSteps,
30
+ } from "@quickchatai/pi-quickreview/src/plugin/setup.js";
31
+ import type { PluginHost } from "@quickchatai/pi-quickreview/src/plugin/session.js";
32
+ import {
33
+ SetupChecklist,
34
+ StepRegistry,
35
+ type Detection,
36
+ type SetupStep,
37
+ type StepReport,
38
+ } from "@quickchatai/pi-quickreview/src/setupchecklist.js";
39
+ import { providerLabel, type HomeModel, type SetupGap } from "./model.js";
40
+ import type { GithubPort, Known } from "./ports.js";
41
+
42
+ /** The app's step ids, in their order on the card. */
43
+ export const HOME_STEPS = { login: "login", github: "github", repo: "repo" } as const;
44
+ export type HomeStepId = (typeof HOME_STEPS)[keyof typeof HOME_STEPS];
45
+ const HOME_STEP_ORDER: readonly HomeStepId[] = [
46
+ HOME_STEPS.login,
47
+ HOME_STEPS.github,
48
+ HOME_STEPS.repo,
49
+ ];
50
+
51
+ /** The plugin's steps the home runs as its first-run checks: pi and git
52
+ * (gh is the github step's). The plugin's login, GitHub credential and repo
53
+ * profile steps are the home's own steps' business, or Setup's. */
54
+ export const FIRST_RUN_CHECKS: readonly string[] = [PLUGIN_SETUP_STEPS.pi, PLUGIN_SETUP_STEPS.git];
55
+
56
+ /** What the person types at a device-code page. */
57
+ export interface DeviceCode {
58
+ userCode: string;
59
+ verificationUri: string;
60
+ expiresInSeconds?: number;
61
+ }
62
+
63
+ export type LoginFailureKind =
64
+ "expired" | "denied" | "cancelled" | "not_enabled" | "unavailable" | "failed";
65
+
66
+ export type LoginOutcome = { ok: true } | { ok: false; kind: LoginFailureKind; why: string };
67
+
68
+ /** pi's own device-code login (src/home/devicelogin.ts). Never throws. */
69
+ export interface DeviceLoginPort {
70
+ login(o: {
71
+ provider: string;
72
+ signal: AbortSignal;
73
+ onCode(code: DeviceCode): void;
74
+ }): Promise<LoginOutcome>;
75
+ }
76
+
77
+ /** The repository pick and what it writes. */
78
+ export interface RepoChoice {
79
+ repo: string;
80
+ /** The repository's default branch, as GitHub says. */
81
+ baseBranch: string | null;
82
+ /** The machine's IANA timezone. */
83
+ timezone: string | null;
84
+ ownerName: string | null;
85
+ ownerLogin: string | null;
86
+ }
87
+
88
+ export type RepoWrite = { ok: true; path: string; wrote: string[] } | { ok: false; why: string };
89
+
90
+ export interface RepoPickPort {
91
+ /** The person's recently pushed repositories, owner/name. */
92
+ recent(): Promise<Known<readonly string[]>>;
93
+ /** What GitHub shows of `repo`: its canonical name and default branch. */
94
+ describe(
95
+ repo: string,
96
+ ): Promise<{ ok: true; repo: string; defaultBranch: string | null } | { ok: false; why: string }>;
97
+ /** The `gh` account's login and name. */
98
+ profile(): Promise<Known<{ login: string; name: string | null }>>;
99
+ /** Write the choice where the home's repo port reads it. */
100
+ write(choice: RepoChoice): Promise<RepoWrite>;
101
+ }
102
+
103
+ /** What the setup is built over, beside the home's own ports. */
104
+ export interface HomeSetupPorts {
105
+ /** The first-run checks' facts: the plugin's steps read the running pi's
106
+ * version and PATH from a PluginHost. Absent: no first-run checks. */
107
+ firstRun?: { host: PluginHost; range: string };
108
+ repos?: RepoPickPort;
109
+ /** The machine's IANA timezone, or null. */
110
+ timezone?: () => string | null;
111
+ /** pi's device login. Absent: the extension builds it from pi's session. */
112
+ deviceLogin?: DeviceLoginPort;
113
+ }
114
+
115
+ /** pi's dialogs, for the repository pick. */
116
+ export interface SetupUi {
117
+ select(title: string, options: string[]): Promise<string | undefined>;
118
+ input(title: string, placeholder: string): Promise<string | undefined>;
119
+ }
120
+
121
+ /** One row of the card, as the card draws it. */
122
+ export interface SetupStepView {
123
+ id: HomeStepId;
124
+ title: string;
125
+ state: "done" | "pending" | "running";
126
+ /** What was found, for a done step. */
127
+ found?: string;
128
+ /** Why it is not done yet, as its detect said. */
129
+ missing?: string;
130
+ /** Its source could not be read: neither done nor missing. */
131
+ unknown?: string;
132
+ /** The next step: the one alt+n runs. */
133
+ next: boolean;
134
+ }
135
+
136
+ export interface SetupView {
137
+ /** First-run checks that fail, in order. */
138
+ checks: Array<{ id: string; title: string; why: string; next: boolean }>;
139
+ steps: SetupStepView[];
140
+ /** The device login: its code once pi has one, the last failure. */
141
+ login: { running: boolean; code?: DeviceCode; failure?: { kind: LoginFailureKind; why: string } };
142
+ /** The provider the login is for, as a person names it. */
143
+ provider: string;
144
+ done: number;
145
+ total: number;
146
+ }
147
+
148
+ export const PASTE_REPO = "Paste any repository…";
149
+
150
+ /** The providers whose login the card runs as pi's device-code flow. Any
151
+ * other provider the chat runs on signs in with pi's own /login. */
152
+ export const DEVICE_LOGIN_PROVIDERS: ReadonlySet<string> = new Set(["openai-codex"]);
153
+
154
+ /** owner/name from what a person pastes: owner/name, a github.com URL (a PR
155
+ * or file URL too), or a git remote. Null when it is none of those. */
156
+ export function parseRepoInput(text: string): string | null {
157
+ let s = text.trim();
158
+ let wasUrl = false;
159
+ const strip = (re: RegExp): void => {
160
+ if (re.test(s)) {
161
+ s = s.replace(re, "");
162
+ wasUrl = true;
163
+ }
164
+ };
165
+ strip(/^git@github\.com:/i);
166
+ strip(/^(?:(?:https?|ssh|git):\/\/)?(?:[^@/]+@)?(?:www\.)?github\.com\//i);
167
+ s = s.replace(/[?#].*$/, "").replace(/\/+$/, "");
168
+ const parts = s.split("/");
169
+ if (parts.length < 2 || (parts.length > 2 && !wasUrl)) return null;
170
+ const owner = parts[0]!;
171
+ const name = parts[1]!.replace(/\.git$/i, "");
172
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(owner)) return null;
173
+ if (!/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..") return null;
174
+ return `${owner}/${name}`;
175
+ }
176
+
177
+ /** A detect's answer, or Unknown: its source could not be read. */
178
+ type Facts = Detection | { unknown: string };
179
+
180
+ const asDetection = (f: Facts): Detection => ("unknown" in f ? { missing: f.unknown } : f);
181
+
182
+ const report = (name: string, f: Facts): StepReport =>
183
+ "found" in f
184
+ ? { ok: true, checks: [{ name, ok: true, detail: f.found }] }
185
+ : {
186
+ ok: false,
187
+ checks: [{ name, ok: false, detail: "unknown" in f ? f.unknown : f.missing }],
188
+ };
189
+
190
+ const refused = (name: string, detail: string): StepReport => ({
191
+ ok: false,
192
+ checks: [{ name, ok: false, detail }],
193
+ });
194
+
195
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
196
+
197
+ /** A device login in flight. */
198
+ interface Signing {
199
+ ac: AbortController;
200
+ code?: DeviceCode;
201
+ onCode?: (c: DeviceCode) => void;
202
+ }
203
+
204
+ export interface HomeSetupOptions {
205
+ model: HomeModel;
206
+ /** The home's GitHub port: the github step's re-check reads it. */
207
+ github: GithubPort;
208
+ ports: HomeSetupPorts;
209
+ /** pi's device login, once pi's session exists. */
210
+ deviceLogin(): DeviceLoginPort | undefined;
211
+ /** Something the card shows changed. */
212
+ onChange?: () => void;
213
+ /** The card should give way to the home, or the home to the card. */
214
+ onPhase?: (phase: "setup" | "home") => void;
215
+ /** Dialogs and a terminal exist: the login may start by itself. */
216
+ interactive?: () => boolean;
217
+ }
218
+
219
+ export class HomeSetup {
220
+ private readonly checklist: SetupChecklist;
221
+ private readonly checkIds: readonly string[];
222
+ /** Steps whose last detect could not read its source, and why. */
223
+ private readonly unknownWhy = new Map<string, string>();
224
+ private signing: Signing | null = null;
225
+ private failure: { kind: LoginFailureKind; why: string } | null = null;
226
+ private stepping = 0;
227
+ private recentRepos: Promise<Known<readonly string[]>> | null = null;
228
+ private synced = false;
229
+ private autoStarted = false;
230
+ private lastPhase: "setup" | "home" = "home";
231
+ private closed = false;
232
+
233
+ constructor(private readonly o: HomeSetupOptions) {
234
+ const checks = o.ports.firstRun
235
+ ? pluginSetupSteps({ host: o.ports.firstRun.host, range: o.ports.firstRun.range }).filter(
236
+ (s) => FIRST_RUN_CHECKS.includes(s.id),
237
+ )
238
+ : [];
239
+ this.checkIds = checks.map((s) => s.id);
240
+ const registry = new StepRegistry(checks).register(...this.homeSteps());
241
+ this.checklist = new SetupChecklist(registry, { clock: () => new Date() }, { file: null });
242
+ }
243
+
244
+ private get m(): HomeModel {
245
+ return this.o.model;
246
+ }
247
+
248
+ private changed(): void {
249
+ this.o.onChange?.();
250
+ }
251
+
252
+ // ---- what each step reads ---------------------------------------------
253
+
254
+ private loginTitle(): string {
255
+ return `Sign in to ${providerLabel(this.m.chatProvider())}`;
256
+ }
257
+
258
+ private loginFacts(): Facts {
259
+ const label = providerLabel(this.m.chatProvider());
260
+ const signed = this.m.signedIn();
261
+ if (signed === true)
262
+ return { found: `${label === "OpenAI" ? "ChatGPT" : label} · signed in with pi` };
263
+ if (signed === false) return { missing: `pi has no ${label} login yet` };
264
+ const l = this.m.state.login;
265
+ return { unknown: l.kind === "unknown" ? `can't read pi's login: ${l.why}` : "still reading" };
266
+ }
267
+
268
+ private which(cmd: string): string | null | undefined {
269
+ return this.o.ports.firstRun?.host.runner.which(cmd);
270
+ }
271
+
272
+ private githubFacts(): Facts {
273
+ if (this.which("gh") === null)
274
+ return { missing: "gh is not installed: get it from https://cli.github.com" };
275
+ const g = this.m.state.github;
276
+ if (g.kind === "known")
277
+ return g.value === null
278
+ ? { missing: "gh is signed out: run `gh auth login` in a shell" }
279
+ : { found: `@${g.value} · via gh` };
280
+ return { unknown: g.kind === "unknown" ? `can't tell: ${g.why}` : "still reading" };
281
+ }
282
+
283
+ private repoFacts(): Facts {
284
+ const r = this.m.state.repo;
285
+ if (r.kind === "known")
286
+ return r.value === null
287
+ ? { missing: "no repository is chosen yet" }
288
+ : { found: `${r.value} · from your qcr config` };
289
+ return {
290
+ unknown: r.kind === "unknown" ? `can't read your qcr config: ${r.why}` : "still reading",
291
+ };
292
+ }
293
+
294
+ private facts(id: HomeStepId): Facts {
295
+ return id === "login"
296
+ ? this.loginFacts()
297
+ : id === "github"
298
+ ? this.githubFacts()
299
+ : this.repoFacts();
300
+ }
301
+
302
+ /** detect: the facts, noting an Unknown so it never counts as a gap. */
303
+ private detected(id: HomeStepId): Detection {
304
+ const f = this.facts(id);
305
+ if ("unknown" in f) this.unknownWhy.set(id, f.unknown);
306
+ else this.unknownWhy.delete(id);
307
+ return asDetection(f);
308
+ }
309
+
310
+ private homeSteps(): SetupStep[] {
311
+ const step = (
312
+ id: HomeStepId,
313
+ title: string,
314
+ order: number,
315
+ explain: string,
316
+ run: SetupStep["run"],
317
+ ): SetupStep => ({
318
+ id,
319
+ title,
320
+ order,
321
+ explain,
322
+ detect: async () => this.detected(id),
323
+ run,
324
+ verify: async () => report(title, this.facts(id)),
325
+ });
326
+ return [
327
+ step(
328
+ HOME_STEPS.login,
329
+ "Sign in",
330
+ 10,
331
+ "pi's own device-code login, for the provider the chat runs on.",
332
+ () => this.runLogin(),
333
+ ),
334
+ step(
335
+ HOME_STEPS.github,
336
+ "Connect GitHub",
337
+ 11,
338
+ "gh installed and signed in: the home reads the pull requests through it.",
339
+ () => this.runGithub(),
340
+ ),
341
+ step(
342
+ HOME_STEPS.repo,
343
+ "Choose a repository",
344
+ 12,
345
+ "The repository the home reviews, written to your qcr config's [github] repo.",
346
+ (ctx) => this.runRepo(ctx.input["repo"]),
347
+ ),
348
+ ];
349
+ }
350
+
351
+ // ---- the steps' runs --------------------------------------------------
352
+
353
+ private async runLogin(): Promise<StepReport> {
354
+ const provider = this.m.chatProvider();
355
+ if (!DEVICE_LOGIN_PROVIDERS.has(provider)) {
356
+ this.failure = {
357
+ kind: "unavailable",
358
+ why: `sign in to ${providerLabel(provider)} with pi's own /login`,
359
+ };
360
+ return refused("login", this.failure.why);
361
+ }
362
+ const port = this.o.deviceLogin();
363
+ const s = this.signing;
364
+ if (port === undefined || s === null) {
365
+ this.failure = {
366
+ kind: "unavailable",
367
+ why: "this home cannot run pi's device login: type /login to sign in with pi's own menu",
368
+ };
369
+ return refused("login", this.failure.why);
370
+ }
371
+ const r = await port.login({
372
+ provider,
373
+ signal: s.ac.signal,
374
+ onCode: (code) => {
375
+ s.code = code;
376
+ s.onCode?.(code);
377
+ this.changed();
378
+ },
379
+ });
380
+ if (r.ok)
381
+ return { ok: true, checks: [{ name: "login", ok: true, detail: "signed in with pi" }] };
382
+ this.failure = { kind: r.kind, why: r.why };
383
+ return refused("login", r.why);
384
+ }
385
+
386
+ private async runGithub(): Promise<StepReport> {
387
+ if (this.which("gh") === null)
388
+ return refused("github", "gh is not installed: get it from https://cli.github.com");
389
+ let u: Known<string | null>;
390
+ try {
391
+ u = await this.o.github.user();
392
+ } catch (err) {
393
+ u = { kind: "unknown", why: message(err) };
394
+ }
395
+ if (u.kind === "unknown") return refused("github", `can't tell: ${u.why}`);
396
+ return u.value === null
397
+ ? refused("github", "gh is still signed out: run `gh auth login` in a shell")
398
+ : { ok: true, checks: [{ name: "github", ok: true, detail: `@${u.value} · via gh` }] };
399
+ }
400
+
401
+ private async runRepo(raw: unknown): Promise<StepReport> {
402
+ const repos = this.o.ports.repos;
403
+ if (repos === undefined)
404
+ return refused("repo", "this home cannot write your qcr config: set [github] repo in it");
405
+ if (typeof raw !== "string" || raw.trim() === "")
406
+ return refused("repo", "which repository? Give owner/name or a GitHub URL");
407
+ const repo = parseRepoInput(raw);
408
+ if (repo === null)
409
+ return refused(
410
+ "repo",
411
+ `${JSON.stringify(raw.trim())} is not a repository: give owner/name or a GitHub URL`,
412
+ );
413
+ const seen = await repos.describe(repo);
414
+ if (!seen.ok) return refused("repo", `GitHub can't show ${repo}: ${seen.why}`);
415
+ const profile = await repos.profile();
416
+ const wrote = await repos.write({
417
+ repo: seen.repo,
418
+ baseBranch: seen.defaultBranch,
419
+ timezone: this.o.ports.timezone?.() ?? null,
420
+ ownerLogin: profile.kind === "known" ? profile.value.login : null,
421
+ ownerName: profile.kind === "known" ? profile.value.name : null,
422
+ });
423
+ if (!wrote.ok) return refused("repo", `couldn't write your qcr config: ${wrote.why}`);
424
+ return {
425
+ ok: true,
426
+ checks: [{ name: "repo", ok: true, detail: `${seen.repo}, written to ${wrote.path}` }],
427
+ };
428
+ }
429
+
430
+ // ---- what the home asks of it -----------------------------------------
431
+
432
+ /** Detect again over what the home has just read: a done step is
433
+ * re-verified, a pending one detected. The HomeModel calls it after every
434
+ * read, before its first notice. */
435
+ async sync(): Promise<void> {
436
+ await this.checklist.reconcile();
437
+ await this.checklist.detect();
438
+ // the session ended while the reads were out: their answer lands into nothing
439
+ if (this.closed) return;
440
+ this.synced = true;
441
+ if (this.isPending(HOME_STEPS.repo)) void this.recent();
442
+ const phase = this.phase();
443
+ if (phase !== this.lastPhase) {
444
+ this.lastPhase = phase;
445
+ this.o.onPhase?.(phase);
446
+ }
447
+ this.maybeStartLogin();
448
+ this.changed();
449
+ }
450
+
451
+ private isPending(id: string): boolean {
452
+ return this.checklist.list().find((l) => l.id === id)?.status === "pending";
453
+ }
454
+
455
+ /** The steps a read showed to be missing, in order: the setup checklist's
456
+ * gaps. An Unknown is none. Empty before the first read. */
457
+ gaps(): SetupGap[] {
458
+ if (!this.synced) return [];
459
+ return this.checklist
460
+ .list()
461
+ .filter((l) => l.status === "pending" && !l.optional && !this.unknownWhy.has(l.id))
462
+ .map((l) => l.id);
463
+ }
464
+
465
+ /** The card while a gap is left, then the home. */
466
+ phase(): "setup" | "home" {
467
+ return this.gaps().length > 0 ? "setup" : "home";
468
+ }
469
+
470
+ /** A step is running: the card's spinner turns. */
471
+ busy(): boolean {
472
+ return this.signing !== null || this.stepping > 0;
473
+ }
474
+
475
+ /** The first pending step: what alt+n runs. */
476
+ private nextId(): string | undefined {
477
+ return this.checklist.list().find((l) => l.status === "pending")?.id;
478
+ }
479
+
480
+ /** Read everything again after a step changed it. A read already in
481
+ * flight may have started before the change, so it is joined and then read
482
+ * once more. */
483
+ private async reread(): Promise<void> {
484
+ if (this.closed) return;
485
+ if (this.m.busy()) await this.m.refresh();
486
+ await this.m.refresh();
487
+ }
488
+
489
+ /** The login starts by itself when it is the first thing missing. */
490
+ private maybeStartLogin(): void {
491
+ if (this.autoStarted || this.closed || !(this.o.interactive?.() ?? false)) return;
492
+ const gaps = this.gaps();
493
+ if (gaps[0] !== HOME_STEPS.login || !DEVICE_LOGIN_PROVIDERS.has(this.m.chatProvider())) return;
494
+ this.autoStarted = true;
495
+ void this.signIn();
496
+ }
497
+
498
+ private async step(id: string, input?: Record<string, unknown>) {
499
+ this.stepping++;
500
+ this.changed();
501
+ try {
502
+ return await this.checklist.runStep(id, input);
503
+ } finally {
504
+ this.stepping--;
505
+ this.changed();
506
+ }
507
+ }
508
+
509
+ /** Run the next step (alt+n, ⏎ on the card, `home_setup_next`). */
510
+ async next(ui?: SetupUi): Promise<string> {
511
+ const id = this.nextId();
512
+ if (id === undefined) return "setup is done: every step is";
513
+ if (id === HOME_STEPS.login) return this.signIn();
514
+ if (id === HOME_STEPS.github) return this.checkGithub();
515
+ if (id === HOME_STEPS.repo) return this.chooseRepo(undefined, ui);
516
+ // a first-run check: look again
517
+ const r = await this.step(id);
518
+ if (r.ok) await this.reread();
519
+ const title = this.checklist.list().find((l) => l.id === id)?.title ?? id;
520
+ return r.ok ? `${title}: ok` : `${title}: ${r.failures.join("; ")}`;
521
+ }
522
+
523
+ /** pi's device login. The answer comes once the code is shown (or the
524
+ * login ended): the sign-in itself goes on in the background. */
525
+ async signIn(): Promise<string> {
526
+ const s = this.signing;
527
+ if (s !== null)
528
+ return s.code
529
+ ? `Still waiting: open ${s.code.verificationUri} and enter ${s.code.userCode}.`
530
+ : "The sign-in is starting: the code shows in a moment.";
531
+ if (!this.isPending(HOME_STEPS.login)) {
532
+ const f = this.loginFacts();
533
+ return `already signed in: ${"found" in f ? f.found : "done"}`;
534
+ }
535
+ const signing: Signing = { ac: new AbortController() };
536
+ this.signing = signing;
537
+ this.failure = null;
538
+ this.m.say(`Signing in to ${providerLabel(this.m.chatProvider())} with pi's device code.`);
539
+ let answer!: (text: string) => void;
540
+ const answered = new Promise<string>((r) => (answer = r));
541
+ signing.onCode = (c) =>
542
+ answer(
543
+ `Open ${c.verificationUri} and enter the code ${c.userCode}. The home finishes the sign-in once you approve it.`,
544
+ );
545
+ void this.checklist.runStep(HOME_STEPS.login).then(async (r) => {
546
+ this.signing = null;
547
+ const label = providerLabel(this.m.chatProvider());
548
+ if (r.ok) {
549
+ await this.reread();
550
+ this.m.say(`Signed in to ${label} with pi.`, "ok");
551
+ answer(`signed in to ${label}`);
552
+ } else {
553
+ const f = this.failure;
554
+ if (f?.kind !== "cancelled") this.m.say(loginFailureText(f, "alt+n"), "warn");
555
+ answer(`the sign-in did not finish: ${r.failures.join("; ")}`);
556
+ }
557
+ this.changed();
558
+ });
559
+ return answered;
560
+ }
561
+
562
+ /** gh: check again (one key). */
563
+ async checkGithub(): Promise<string> {
564
+ const r = await this.step(HOME_STEPS.github);
565
+ await this.reread();
566
+ if (r.ok) {
567
+ this.m.say(`Connected to GitHub as ${r.checks[0]?.detail ?? "you"}.`, "ok");
568
+ return `GitHub: ${r.checks[0]?.detail ?? "signed in"}`;
569
+ }
570
+ const why = r.failures.join("; ");
571
+ this.m.say(`${why.replace(/^github: /, "")}, then press alt+n to check again.`, "warn");
572
+ return why;
573
+ }
574
+
575
+ /** The person's recently pushed repositories, read once. */
576
+ recent(): Promise<Known<readonly string[]>> {
577
+ const repos = this.o.ports.repos;
578
+ if (repos === undefined)
579
+ return Promise.resolve({ kind: "unknown", why: "this home cannot list them" });
580
+ this.recentRepos ??= repos.recent().catch((err: unknown) => ({
581
+ kind: "unknown" as const,
582
+ why: message(err),
583
+ }));
584
+ return this.recentRepos;
585
+ }
586
+
587
+ /** Choose the repository: `repo` as given (owner/name or a URL), or, with
588
+ * pi's dialogs, picked from the recent ones or pasted. */
589
+ async chooseRepo(repo?: string, ui?: SetupUi): Promise<string> {
590
+ let text = repo;
591
+ if (text === undefined || text.trim() === "") {
592
+ const recent = await this.recent();
593
+ const names = recent.kind === "known" ? [...recent.value] : [];
594
+ if (ui === undefined)
595
+ return recent.kind === "known"
596
+ ? `Which repository? Give owner/name or a GitHub URL as \`repo\`.${names.length ? ` Recently pushed: ${names.join(", ")}.` : ""}`
597
+ : `Which repository? Give owner/name or a GitHub URL as \`repo\`. The recent ones are unknown: ${recent.why}.`;
598
+ const pick = await ui.select(
599
+ names.length ? "Choose a repository: your recently pushed ones" : "Choose a repository",
600
+ [...names, PASTE_REPO],
601
+ );
602
+ if (pick === undefined) return "no repository was chosen";
603
+ text =
604
+ pick === PASTE_REPO ? await ui.input("Repository", "owner/name or a GitHub URL") : pick;
605
+ if (text === undefined || text.trim() === "") return "no repository was chosen";
606
+ }
607
+ const r = await this.step(HOME_STEPS.repo, { repo: text });
608
+ if (!r.ok) {
609
+ const why = r.failures.join("; ").replace(/^repo: /, "");
610
+ this.m.say(`${why[0]?.toUpperCase() ?? ""}${why.slice(1)}.`, "warn");
611
+ return why;
612
+ }
613
+ await this.reread();
614
+ const name = this.m.repo() ?? "the repository";
615
+ const open = this.m.ordered().length;
616
+ if (this.phase() === "home")
617
+ this.m.say(`You're set. ${open} open pull request${open === 1 ? "" : "s"} in ${name}.`, "ok");
618
+ return r.checks[0]?.detail ?? `repository set to ${name}`;
619
+ }
620
+
621
+ /** The session ends: a login in flight stops with it. */
622
+ close(): void {
623
+ this.closed = true;
624
+ this.signing?.ac.abort();
625
+ }
626
+
627
+ /** What the card draws. */
628
+ view(): SetupView {
629
+ const lines = this.checklist.list();
630
+ const next = this.nextId();
631
+ const running = (id: string): boolean => id === HOME_STEPS.login && this.signing !== null;
632
+ const checks = lines
633
+ .filter((l) => this.checkIds.includes(l.id) && l.status === "pending")
634
+ .map((l) => ({ id: l.id, title: l.title, why: l.note ?? l.explain, next: l.id === next }));
635
+ const steps = HOME_STEP_ORDER.map((id): SetupStepView => {
636
+ const l = lines.find((x) => x.id === id);
637
+ const title = id === HOME_STEPS.login ? this.loginTitle() : (l?.title ?? id);
638
+ if (l?.status === "done") {
639
+ const f = this.facts(id);
640
+ return { id, title, state: "done", found: "found" in f ? f.found : "done", next: false };
641
+ }
642
+ const unknown = this.unknownWhy.get(id);
643
+ const f = this.facts(id);
644
+ return {
645
+ id,
646
+ title,
647
+ state: running(id) ? "running" : "pending",
648
+ ...(unknown !== undefined ? { unknown } : "missing" in f ? { missing: f.missing } : {}),
649
+ next: id === next && !running(id),
650
+ };
651
+ });
652
+ return {
653
+ checks,
654
+ steps,
655
+ login: {
656
+ running: this.signing !== null,
657
+ ...(this.signing?.code ? { code: this.signing.code } : {}),
658
+ ...(this.failure ? { failure: this.failure } : {}),
659
+ },
660
+ provider: providerLabel(this.m.chatProvider()),
661
+ done: steps.filter((s) => s.state === "done").length,
662
+ total: steps.length,
663
+ };
664
+ }
665
+ }
666
+
667
+ /** What a person reads after a device login that did not finish, with the
668
+ * key that tries again. */
669
+ export function loginFailureText(
670
+ f: { kind: LoginFailureKind; why: string } | null | undefined,
671
+ key: string,
672
+ ): string {
673
+ switch (f?.kind) {
674
+ case "expired":
675
+ return `The code expired before it was approved. ${key} gets a new one.`;
676
+ case "denied":
677
+ return `The sign-in was denied. ${key} tries again.`;
678
+ case "not_enabled":
679
+ return `Device-code login is off for this ChatGPT account: turn it on in ChatGPT → Settings → Security, then ${key} tries again.`;
680
+ case "cancelled":
681
+ return `The sign-in stopped. ${key} starts it again.`;
682
+ case "unavailable":
683
+ return `${f.why}.`;
684
+ case "failed":
685
+ return `The sign-in failed: ${f.why}. ${key} tries again.`;
686
+ default:
687
+ return `The sign-in did not finish. ${key} tries again.`;
688
+ }
689
+ }