@quickchatai/quickreview 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -57
- package/bin/qcr.js +1 -1
- package/config.example.toml +60 -14
- package/deploy/ansible/README.md +20 -12
- package/deploy/ansible/inventory.example.ini +1 -1
- package/deploy/collect-secrets.sh +1 -1
- package/deploy/qcr-update.sh +1 -1
- package/dist/extensions/cli/home.js +14 -0
- package/dist/extensions/cli/home.js.map +1 -0
- package/dist/extensions/cli/login-route.js +5 -0
- package/dist/extensions/cli/login-route.js.map +1 -0
- package/dist/extensions/cli/selfimprove.js +18 -0
- package/dist/extensions/cli/selfimprove.js.map +1 -0
- package/dist/extensions/home.js +350 -0
- package/dist/extensions/home.js.map +1 -0
- package/dist/extensions/login-route.js +180 -0
- package/dist/extensions/login-route.js.map +1 -0
- package/dist/extensions/onboarding.js +35 -11
- package/dist/extensions/onboarding.js.map +1 -1
- package/dist/extensions/selfimprove.js +49 -0
- package/dist/extensions/selfimprove.js.map +1 -0
- package/dist/extensions/setup-scan.js +23 -592
- package/dist/extensions/setup-scan.js.map +1 -1
- package/dist/extensions/setup.js +2 -47
- package/dist/extensions/setup.js.map +1 -1
- package/dist/src/archive/restore.js +18 -15
- package/dist/src/archive/restore.js.map +1 -1
- package/dist/src/botworker.js +13 -2
- package/dist/src/botworker.js.map +1 -1
- package/dist/src/channelfs.js +3 -26
- package/dist/src/channelfs.js.map +1 -1
- package/dist/src/ciretry.js +2 -1
- package/dist/src/ciretry.js.map +1 -1
- package/dist/src/cli.js +54 -61
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliops.js +3 -2
- package/dist/src/cliops.js.map +1 -1
- package/dist/src/commands.js +25 -2
- package/dist/src/commands.js.map +1 -1
- package/dist/src/config.js +32 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +50 -8
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/debuground.js +1 -0
- package/dist/src/debuground.js.map +1 -1
- package/dist/src/debugroundpi.js +6 -1
- package/dist/src/debugroundpi.js.map +1 -1
- package/dist/src/dispatch.js +5 -4
- package/dist/src/dispatch.js.map +1 -1
- package/dist/src/engine/caserunner-sdk.js +15 -251
- package/dist/src/engine/caserunner-sdk.js.map +1 -1
- package/dist/src/engine/debugpi.js +17 -5
- package/dist/src/engine/debugpi.js.map +1 -1
- package/dist/src/execboundary.js +10 -3
- package/dist/src/execboundary.js.map +1 -1
- package/dist/src/github.js +51 -16
- package/dist/src/github.js.map +1 -1
- package/dist/src/githubtoken.js +346 -0
- package/dist/src/githubtoken.js.map +1 -0
- package/dist/src/home/adapters.js +349 -0
- package/dist/src/home/adapters.js.map +1 -0
- package/dist/src/home/advanced.js +318 -0
- package/dist/src/home/advanced.js.map +1 -0
- package/dist/src/home/advancedtools.js +57 -0
- package/dist/src/home/advancedtools.js.map +1 -0
- package/dist/src/home/autoreview.js +325 -0
- package/dist/src/home/autoreview.js.map +1 -0
- package/dist/src/home/daemonstatus.js +32 -0
- package/dist/src/home/daemonstatus.js.map +1 -0
- package/dist/src/home/devicelogin.js +61 -0
- package/dist/src/home/devicelogin.js.map +1 -0
- package/dist/src/home/launch.js +202 -0
- package/dist/src/home/launch.js.map +1 -0
- package/dist/src/home/loginowner.js +205 -0
- package/dist/src/home/loginowner.js.map +1 -0
- package/dist/src/home/memory.js +122 -0
- package/dist/src/home/memory.js.map +1 -0
- package/dist/src/home/model.js +811 -0
- package/dist/src/home/model.js.map +1 -0
- package/dist/src/home/panel.js +520 -0
- package/dist/src/home/panel.js.map +1 -0
- package/dist/src/home/ports.js +19 -0
- package/dist/src/home/ports.js.map +1 -0
- package/dist/src/home/review.js +163 -0
- package/dist/src/home/review.js.map +1 -0
- package/dist/src/home/reviews.js +478 -0
- package/dist/src/home/reviews.js.map +1 -0
- package/dist/src/home/setup.js +507 -0
- package/dist/src/home/setup.js.map +1 -0
- package/dist/src/home/setupadapters.js +214 -0
- package/dist/src/home/setupadapters.js.map +1 -0
- package/dist/src/home/setupcard.js +199 -0
- package/dist/src/home/setupcard.js.map +1 -0
- package/dist/src/home/setuptools.js +45 -0
- package/dist/src/home/setuptools.js.map +1 -0
- package/dist/src/home/spend.js +160 -0
- package/dist/src/home/spend.js.map +1 -0
- package/dist/src/home/switches.js +209 -0
- package/dist/src/home/switches.js.map +1 -0
- package/dist/src/home/switchon.js +439 -0
- package/dist/src/home/switchon.js.map +1 -0
- package/dist/src/home/toolnames.js +41 -0
- package/dist/src/home/toolnames.js.map +1 -0
- package/dist/src/home/tune.js +341 -0
- package/dist/src/home/tune.js.map +1 -0
- package/dist/src/home/words.js +7 -0
- package/dist/src/home/words.js.map +1 -0
- package/dist/src/homes.js +14 -0
- package/dist/src/homes.js.map +1 -1
- package/dist/src/install.js +1 -1
- package/dist/src/install.js.map +1 -1
- package/dist/src/ipc/workerspec.js +8 -0
- package/dist/src/ipc/workerspec.js.map +1 -1
- package/dist/src/live/publish.js +1 -1
- package/dist/src/live/publish.js.map +1 -1
- package/dist/src/livecli.js +1 -1
- package/dist/src/livecli.js.map +1 -1
- package/dist/src/loginhint.js +8 -3
- package/dist/src/loginhint.js.map +1 -1
- package/dist/src/loginrefresh/handover.js +121 -0
- package/dist/src/loginrefresh/handover.js.map +1 -0
- package/dist/src/loginrefresh/lock.js +54 -3
- package/dist/src/loginrefresh/lock.js.map +1 -1
- package/dist/src/loginrefresh/pirefresh.js +3 -0
- package/dist/src/loginrefresh/pirefresh.js.map +1 -1
- package/dist/src/loginrefresh/refresher.js +33 -5
- package/dist/src/loginrefresh/refresher.js.map +1 -1
- package/dist/src/loginrefresh/supervisor.js +18 -1
- package/dist/src/loginrefresh/supervisor.js.map +1 -1
- package/dist/src/npmverbs.js +46 -14
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +35 -4
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +32 -306
- package/dist/src/onboard/engine.js.map +1 -1
- package/dist/src/onboard/pipin.js +22 -0
- package/dist/src/onboard/pipin.js.map +1 -0
- package/dist/src/onboard/stepforward.js +41 -0
- package/dist/src/onboard/stepforward.js.map +1 -0
- package/dist/src/onboard/steps/appslug.js +4 -1
- package/dist/src/onboard/steps/appslug.js.map +1 -1
- package/dist/src/onboard/steps/bringup.js +3 -2
- package/dist/src/onboard/steps/bringup.js.map +1 -1
- package/dist/src/onboard/steps/compose.js +29 -0
- package/dist/src/onboard/steps/compose.js.map +1 -1
- package/dist/src/onboard/steps/configfill.js +120 -39
- package/dist/src/onboard/steps/configfill.js.map +1 -1
- package/dist/src/onboard/steps/githubaccount.js +595 -0
- package/dist/src/onboard/steps/githubaccount.js.map +1 -0
- package/dist/src/onboard/steps/githubapp.js +65 -7
- package/dist/src/onboard/steps/githubapp.js.map +1 -1
- package/dist/src/onboard/steps/golive.js +2 -0
- package/dist/src/onboard/steps/golive.js.map +1 -1
- package/dist/src/onboard/steps/index.js +11 -0
- package/dist/src/onboard/steps/index.js.map +1 -1
- package/dist/src/onboard/steps/ingress.js +80 -25
- package/dist/src/onboard/steps/ingress.js.map +1 -1
- package/dist/src/onboard/steps/llmauth.js +103 -30
- package/dist/src/onboard/steps/llmauth.js.map +1 -1
- package/dist/src/onboard/steps/loginshare.js +213 -0
- package/dist/src/onboard/steps/loginshare.js.map +1 -0
- package/dist/src/onboard/steps/memory.js +67 -3
- package/dist/src/onboard/steps/memory.js.map +1 -1
- package/dist/src/onboard/steps/proof.js +366 -56
- package/dist/src/onboard/steps/proof.js.map +1 -1
- package/dist/src/onboard/steps/repoconfig.js +72 -0
- package/dist/src/onboard/steps/repoconfig.js.map +1 -0
- package/dist/src/onboard/steps/topology.js +6 -6
- package/dist/src/onboard/steps/topology.js.map +1 -1
- package/dist/src/onboard/steps/transport.js +9 -1
- package/dist/src/onboard/steps/transport.js.map +1 -1
- package/dist/src/onboard/steps/tuiroute.js +102 -0
- package/dist/src/onboard/steps/tuiroute.js.map +1 -0
- package/dist/src/onboard/steps/verifyup.js +17 -1
- package/dist/src/onboard/steps/verifyup.js.map +1 -1
- package/dist/src/onboard/toolnames.js +30 -0
- package/dist/src/onboard/toolnames.js.map +1 -0
- package/dist/src/operatorwrite.js +23 -0
- package/dist/src/operatorwrite.js.map +1 -0
- package/dist/src/outcomes.js +4 -1
- package/dist/src/outcomes.js.map +1 -1
- package/dist/src/piagentdir.js +13 -0
- package/dist/src/piagentdir.js.map +1 -0
- package/dist/src/pilogin.js +31 -16
- package/dist/src/pilogin.js.map +1 -1
- package/dist/src/posting-journal.js +2 -1
- package/dist/src/posting-journal.js.map +1 -1
- package/dist/src/posting-outbox.js +6 -2
- package/dist/src/posting-outbox.js.map +1 -1
- package/dist/src/posting-receipt.js +14 -1
- package/dist/src/posting-receipt.js.map +1 -1
- package/dist/src/qcrbin.js +16 -11
- package/dist/src/qcrbin.js.map +1 -1
- package/dist/src/recheck.js +2 -1
- package/dist/src/recheck.js.map +1 -1
- package/dist/src/reconcile.js +22 -2
- package/dist/src/reconcile.js.map +1 -1
- package/dist/src/reviewargs.js +122 -0
- package/dist/src/reviewargs.js.map +1 -0
- package/dist/src/reviewcli.js +7 -161
- package/dist/src/reviewcli.js.map +1 -1
- package/dist/src/reviewdetach.js +76 -11
- package/dist/src/reviewdetach.js.map +1 -1
- package/dist/src/reviewhistory.js +32 -6
- package/dist/src/reviewhistory.js.map +1 -1
- package/dist/src/reviewnow.js +45 -0
- package/dist/src/reviewnow.js.map +1 -0
- package/dist/src/scancli.js +39 -691
- package/dist/src/scancli.js.map +1 -1
- package/dist/src/selfimprove.js +66 -0
- package/dist/src/selfimprove.js.map +1 -0
- package/dist/src/setup/adapters.js +5 -223
- package/dist/src/setup/adapters.js.map +1 -1
- package/dist/src/setup/caserunner.js +15 -249
- package/dist/src/setup/caserunner.js.map +1 -1
- package/dist/src/setup/engine.js +32 -341
- package/dist/src/setup/engine.js.map +1 -1
- package/dist/src/setup/facts.js +3 -592
- package/dist/src/setup/facts.js.map +1 -1
- package/dist/src/setup/findings.js +3 -947
- package/dist/src/setup/findings.js.map +1 -1
- package/dist/src/setup/presentation.js +3 -8
- package/dist/src/setup/presentation.js.map +1 -1
- package/dist/src/setup/scan.js +3 -724
- package/dist/src/setup/scan.js.map +1 -1
- package/dist/src/setup/scanbuild.js +9 -92
- package/dist/src/setup/scanbuild.js.map +1 -1
- package/dist/src/setup/scanlease.js +3 -250
- package/dist/src/setup/scanlease.js.map +1 -1
- package/dist/src/setup/scanreport.js +3 -288
- package/dist/src/setup/scanreport.js.map +1 -1
- package/dist/src/setup/scanrun.js +3 -1732
- package/dist/src/setup/scanrun.js.map +1 -1
- package/dist/src/setup/scantree.js +3 -256
- package/dist/src/setup/scantree.js.map +1 -1
- package/dist/src/setup/stage2request.js +3 -258
- package/dist/src/setup/stage2request.js.map +1 -1
- package/dist/src/setup/unitcut.js +3 -436
- package/dist/src/setup/unitcut.js.map +1 -1
- package/dist/src/state.js +2 -0
- package/dist/src/state.js.map +1 -1
- package/dist/src/statemachine.js +4 -1
- package/dist/src/statemachine.js.map +1 -1
- package/dist/src/takeover.js +1 -0
- package/dist/src/takeover.js.map +1 -1
- package/dist/src/takeoverpi.js +15 -1
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/app.js +49 -0
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/board.js +88 -12
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/cmdbar.js +4 -1
- package/dist/src/tui/cmdbar.js.map +1 -1
- package/dist/src/tui/companion.js +76 -11
- package/dist/src/tui/companion.js.map +1 -1
- package/dist/src/tui/connect.js +7 -1
- package/dist/src/tui/connect.js.map +1 -1
- package/dist/src/tui/deploytab.js +2 -2
- package/dist/src/tui/deploytab.js.map +1 -1
- package/dist/src/tui/filters.js +5 -2
- package/dist/src/tui/filters.js.map +1 -1
- package/dist/src/tui/ghread.js +61 -5
- package/dist/src/tui/ghread.js.map +1 -1
- package/dist/src/tui/index.js +109 -20
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/memforward.js +39 -2
- package/dist/src/tui/memforward.js.map +1 -1
- package/dist/src/tui/memory.js +3 -16
- package/dist/src/tui/memory.js.map +1 -1
- package/dist/src/tui/memoryurl.js +22 -0
- package/dist/src/tui/memoryurl.js.map +1 -0
- package/dist/src/tui/model.js +25 -0
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/prpause.js +7 -0
- package/dist/src/tui/prpause.js.map +1 -1
- package/dist/src/tui/remote.js +7 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +143 -0
- package/dist/src/tui/route.js.map +1 -0
- package/dist/src/tui/setupstate.js +32 -0
- package/dist/src/tui/setupstate.js.map +1 -0
- package/dist/src/tui/sshtriage.js +102 -5
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/tui/theme.js +2 -1
- package/dist/src/tui/theme.js.map +1 -1
- package/dist/src/tui/unwedge.js +2 -12
- package/dist/src/tui/unwedge.js.map +1 -1
- package/dist/src/verdicts.js +17 -5
- package/dist/src/verdicts.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/webhook.js +12 -1
- package/dist/src/webhook.js.map +1 -1
- package/dist/src/wire.js +34 -8
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerworld.js +11 -1
- package/dist/src/workerworld.js.map +1 -1
- package/docker/README.md +1 -1
- package/docker/companion/entrypoint.sh +20 -1
- package/docker/entrypoint.sh +57 -6
- package/docker-compose.yml +16 -1
- package/extensions/cli/home.ts +14 -0
- package/extensions/cli/login-route.ts +10 -0
- package/extensions/cli/selfimprove.ts +18 -0
- package/extensions/home.ts +484 -0
- package/extensions/login-route.ts +220 -0
- package/extensions/onboarding.ts +41 -11
- package/extensions/selfimprove.ts +65 -0
- package/extensions/setup-scan.ts +79 -766
- package/extensions/setup.ts +14 -70
- package/package.json +6 -4
- package/prompts/onboarding.md +58 -18
- package/schemas/review-result.v1.json +60 -0
- package/scripts/deploy.sh +8 -6
- package/scripts/mint-token.sh +25 -4
- package/scripts/qcr-host.sh +28 -5
- package/scripts/vm-ghcr-login.sh +7 -4
- package/src/archive/restore.ts +18 -21
- package/src/botworker.ts +13 -2
- package/src/channelfs.ts +3 -26
- package/src/ciretry.ts +2 -1
- package/src/cli.ts +59 -67
- package/src/cliops.ts +3 -2
- package/src/commands.ts +28 -2
- package/src/config.ts +62 -4
- package/src/daemon.ts +58 -7
- package/src/debuground.ts +3 -0
- package/src/debugroundpi.ts +12 -1
- package/src/dispatch.ts +5 -4
- package/src/engine/caserunner-sdk.ts +29 -314
- package/src/engine/debugpi.ts +17 -5
- package/src/execboundary.ts +16 -4
- package/src/github.ts +57 -15
- package/src/githubtoken.ts +435 -0
- package/src/home/adapters.ts +418 -0
- package/src/home/advanced.ts +421 -0
- package/src/home/advancedtools.ts +96 -0
- package/src/home/autoreview.ts +385 -0
- package/src/home/daemonstatus.ts +55 -0
- package/src/home/devicelogin.ts +107 -0
- package/src/home/launch.ts +257 -0
- package/src/home/loginowner.ts +248 -0
- package/src/home/memory.ts +176 -0
- package/src/home/model.ts +965 -0
- package/src/home/panel.ts +666 -0
- package/src/home/ports.ts +338 -0
- package/src/home/review.ts +206 -0
- package/src/home/reviews.ts +565 -0
- package/src/home/setup.ts +689 -0
- package/src/home/setupadapters.ts +217 -0
- package/src/home/setupcard.ts +288 -0
- package/src/home/setuptools.ts +57 -0
- package/src/home/spend.ts +194 -0
- package/src/home/switches.ts +293 -0
- package/src/home/switchon.ts +554 -0
- package/src/home/toolnames.ts +46 -0
- package/src/home/tune.ts +405 -0
- package/src/home/words.ts +8 -0
- package/src/homes.ts +17 -0
- package/src/install.ts +1 -1
- package/src/ipc/workerspec.ts +15 -0
- package/src/live/publish.ts +4 -1
- package/src/livecli.ts +6 -1
- package/src/loginhint.ts +8 -3
- package/src/loginrefresh/handover.ts +138 -0
- package/src/loginrefresh/lock.ts +72 -3
- package/src/loginrefresh/pirefresh.ts +3 -0
- package/src/loginrefresh/refresher.ts +34 -5
- package/src/loginrefresh/supervisor.ts +25 -2
- package/src/npmverbs.ts +55 -13
- package/src/onboard/choices.ts +40 -4
- package/src/onboard/engine.ts +70 -421
- package/src/onboard/pipin.ts +31 -0
- package/src/onboard/stepforward.ts +61 -0
- package/src/onboard/steps/appslug.ts +4 -1
- package/src/onboard/steps/bringup.ts +3 -2
- package/src/onboard/steps/compose.ts +31 -0
- package/src/onboard/steps/configfill.ts +144 -40
- package/src/onboard/steps/githubaccount.ts +716 -0
- package/src/onboard/steps/githubapp.ts +92 -8
- package/src/onboard/steps/golive.ts +2 -0
- package/src/onboard/steps/index.ts +11 -0
- package/src/onboard/steps/ingress.ts +98 -31
- package/src/onboard/steps/llmauth.ts +114 -29
- package/src/onboard/steps/loginshare.ts +258 -0
- package/src/onboard/steps/memory.ts +76 -3
- package/src/onboard/steps/proof.ts +473 -55
- package/src/onboard/steps/repoconfig.ts +85 -0
- package/src/onboard/steps/topology.ts +7 -6
- package/src/onboard/steps/transport.ts +15 -0
- package/src/onboard/steps/tuiroute.ts +119 -0
- package/src/onboard/steps/verifyup.ts +20 -1
- package/src/onboard/toolnames.ts +34 -0
- package/src/operatorwrite.ts +25 -0
- package/src/outcomes.ts +8 -1
- package/src/piagentdir.ts +12 -0
- package/src/pilogin.ts +32 -16
- package/src/posting-journal.ts +6 -1
- package/src/posting-outbox.ts +10 -2
- package/src/posting-receipt.ts +17 -0
- package/src/qcrbin.ts +16 -11
- package/src/recheck.ts +2 -1
- package/src/reconcile.ts +29 -3
- package/src/reviewargs.ts +119 -0
- package/src/reviewcli.ts +6 -167
- package/src/reviewdetach.ts +107 -13
- package/src/reviewhistory.ts +20 -6
- package/src/reviewnow.ts +76 -0
- package/src/scancli.ts +75 -826
- package/src/selfimprove.ts +90 -0
- package/src/setup/adapters.ts +13 -221
- package/src/setup/caserunner.ts +34 -273
- package/src/setup/engine.ts +62 -496
- package/src/setup/facts.ts +3 -627
- package/src/setup/findings.ts +3 -1264
- package/src/setup/presentation.ts +3 -18
- package/src/setup/scan.ts +3 -1033
- package/src/setup/scanbuild.ts +20 -114
- package/src/setup/scanlease.ts +3 -297
- package/src/setup/scanreport.ts +3 -373
- package/src/setup/scanrun.ts +3 -2268
- package/src/setup/scantree.ts +3 -259
- package/src/setup/stage2request.ts +3 -347
- package/src/setup/unitcut.ts +3 -523
- package/src/state.ts +43 -0
- package/src/statemachine.ts +4 -1
- package/src/takeover.ts +5 -0
- package/src/takeoverpi.ts +16 -2
- package/src/tui/app.ts +47 -0
- package/src/tui/board.ts +111 -14
- package/src/tui/cmdbar.ts +4 -1
- package/src/tui/companion.ts +80 -11
- package/src/tui/connect.ts +7 -1
- package/src/tui/deploytab.ts +2 -2
- package/src/tui/filters.ts +7 -3
- package/src/tui/ghread.ts +74 -7
- package/src/tui/index.ts +117 -23
- package/src/tui/memforward.ts +52 -3
- package/src/tui/memory.ts +3 -15
- package/src/tui/memoryurl.ts +22 -0
- package/src/tui/model.ts +36 -2
- package/src/tui/prpause.ts +11 -2
- package/src/tui/remote.ts +9 -1
- package/src/tui/route.ts +173 -0
- package/src/tui/setupstate.ts +56 -0
- package/src/tui/sshtriage.ts +127 -6
- package/src/tui/theme.ts +2 -1
- package/src/tui/unwedge.ts +2 -14
- package/src/verdicts.ts +23 -5
- package/src/version.ts +1 -1
- package/src/webhook.ts +10 -1
- package/src/wire.ts +42 -9
- package/src/workerworld.ts +16 -2
- package/dist/src/live/projection.js +0 -450
- package/dist/src/live/projection.js.map +0 -1
- package/dist/src/live/read.js +0 -60
- package/dist/src/live/read.js.map +0 -1
- package/dist/src/onboard/launch.js +0 -83
- package/dist/src/onboard/launch.js.map +0 -1
- package/dist/src/setup/ledger.js +0 -207
- package/dist/src/setup/ledger.js.map +0 -1
- package/dist/src/setup/stage2.js +0 -617
- package/dist/src/setup/stage2.js.map +0 -1
- package/dist/src/setup/textsniff.js +0 -89
- package/dist/src/setup/textsniff.js.map +0 -1
- package/src/live/projection.ts +0 -491
- package/src/live/read.ts +0 -86
- package/src/onboard/launch.ts +0 -115
- package/src/setup/ledger.ts +0 -324
- package/src/setup/stage2.ts +0 -926
- package/src/setup/textsniff.ts +0 -95
package/src/tui/ghread.ts
CHANGED
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
// `companion-guard` additionally vetoes write-shaped calls against the API
|
|
24
24
|
// (belt); the token's own scope is the brace.
|
|
25
25
|
//
|
|
26
|
+
// Under `[github] auth = "user"` (the person's own credential, SECURITY.md)
|
|
27
|
+
// there is no App to mint from. The companion then gets a COPY of
|
|
28
|
+
// the person's token file (`<secrets dir>/github_token`), re-copied whenever
|
|
29
|
+
// the host refresher rewrites it, in the same env file and the same
|
|
30
|
+
// read-only mount. That token is the person's and is NOT down-scoped: in this
|
|
31
|
+
// mode `companion-guard`'s veto of write-shaped calls is the only fence, and
|
|
32
|
+
// the env file's header says so.
|
|
33
|
+
//
|
|
26
34
|
// Everything here is total at the edge the TUI calls: `refresh()` answers an
|
|
27
35
|
// outcome, never throws — a companion without GitHub is under-equipped, never
|
|
28
36
|
// a companion that does not start.
|
|
@@ -160,7 +168,11 @@ const TOKEN_VALUE_RE = /^[A-Za-z0-9_.-]+$/;
|
|
|
160
168
|
/** The env file's text. Every value is checked against a character set that
|
|
161
169
|
* is inert in both a docker env file and a `source`d shell file. A token
|
|
162
170
|
* outside that set is refused, and the refusal never echoes it. */
|
|
163
|
-
export function renderTokenEnv(
|
|
171
|
+
export function renderTokenEnv(
|
|
172
|
+
tok: ReadToken,
|
|
173
|
+
repo: string,
|
|
174
|
+
source: "app" | "user" = "app",
|
|
175
|
+
): string {
|
|
164
176
|
if (!TOKEN_VALUE_RE.test(tok.token))
|
|
165
177
|
throw new Error(
|
|
166
178
|
"the minted token has a character an env file cannot carry safely (only A-Z a-z 0-9 _ . - are allowed)",
|
|
@@ -169,11 +181,16 @@ export function renderTokenEnv(tok: ReadToken, repo: string): string {
|
|
|
169
181
|
throw new Error("the token expiry is not a timestamp");
|
|
170
182
|
if (!REPO_RE.test(repo))
|
|
171
183
|
throw new Error(`[github] repo ${JSON.stringify(repo)} is not owner/name`);
|
|
184
|
+
const header =
|
|
185
|
+
source === "user"
|
|
186
|
+
? '# Copied on the host by qcr from the person\'s own GitHub token ([github] auth = "user").\n' +
|
|
187
|
+
"# It is NOT down-scoped: it can do what that person can. The companion uses it for reads\n" +
|
|
188
|
+
"# only, and companion-guard vetoes write-shaped calls. Re-copied when the host refreshes it.\n"
|
|
189
|
+
: "# Minted on the host by qcr: a READ-ONLY GitHub App installation token\n" +
|
|
190
|
+
"# (contents / issues / pull requests: read) for one repository. Re-minted before\n" +
|
|
191
|
+
"# it expires. It cannot write to GitHub.\n";
|
|
172
192
|
return (
|
|
173
|
-
|
|
174
|
-
"# (contents / issues / pull requests: read) for one repository. Re-minted before\n" +
|
|
175
|
-
"# it expires. It cannot write to GitHub.\n" +
|
|
176
|
-
`${TOKEN_VAR}=${tok.token}\n${EXPIRES_VAR}=${tok.expires_at}\n${REPO_VAR}=${repo}\n`
|
|
193
|
+
header + `${TOKEN_VAR}=${tok.token}\n${EXPIRES_VAR}=${tok.expires_at}\n${REPO_VAR}=${repo}\n`
|
|
177
194
|
);
|
|
178
195
|
}
|
|
179
196
|
|
|
@@ -227,10 +244,22 @@ export function tokenExpiry(dir: string, repo?: string): number | undefined {
|
|
|
227
244
|
return Number.isFinite(ms) ? ms : undefined;
|
|
228
245
|
}
|
|
229
246
|
|
|
247
|
+
/** The token the env file holds, or undefined. */
|
|
248
|
+
function heldToken(dir: string): string | undefined {
|
|
249
|
+
try {
|
|
250
|
+
const text = readFileSync(join(dir, TOKEN_FILE), "utf8");
|
|
251
|
+
return new RegExp(`^${TOKEN_VAR}=(.*)$`, "m").exec(text)?.[1]?.trim();
|
|
252
|
+
} catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
230
257
|
export type TokenOutcome =
|
|
231
258
|
/** The file holds a token with more than the margin left: nothing minted. */
|
|
232
259
|
| { kind: "fresh"; expiresAt: string }
|
|
233
260
|
| { kind: "minted"; expiresAt: string }
|
|
261
|
+
/** `auth = "user"`: the person's token file was copied in (it changed). */
|
|
262
|
+
| { kind: "copied"; expiresAt: string }
|
|
234
263
|
/** No App credential on this host: the companion simply has no GitHub. */
|
|
235
264
|
| { kind: "unavailable"; reason: string }
|
|
236
265
|
| { kind: "failed"; reason: string };
|
|
@@ -242,6 +271,8 @@ export function tokenOutcomeLine(o: TokenOutcome): string {
|
|
|
242
271
|
return `read-only GitHub token valid until ${o.expiresAt}`;
|
|
243
272
|
case "minted":
|
|
244
273
|
return `read-only GitHub token minted (valid until ${o.expiresAt})`;
|
|
274
|
+
case "copied":
|
|
275
|
+
return "GitHub token copied from the person's own token file (auth = user; used read-only)";
|
|
245
276
|
case "unavailable":
|
|
246
277
|
return `no read-only GitHub token — ${o.reason}`;
|
|
247
278
|
case "failed":
|
|
@@ -254,9 +285,12 @@ export interface CompanionGithubTokenOptions {
|
|
|
254
285
|
dir: string;
|
|
255
286
|
/** `[github] repo`, owner/name. */
|
|
256
287
|
repo: string;
|
|
257
|
-
/** The resolved
|
|
258
|
-
* host; nothing from it is
|
|
288
|
+
/** The resolved secrets dir (`app_id`, `app.pem`; or `github_token` under
|
|
289
|
+
* `auth = "user"`). Read here, on the host; under "app" nothing from it is
|
|
290
|
+
* ever written into `dir`. */
|
|
259
291
|
secretsDir: string;
|
|
292
|
+
/** `[github] auth`. Absent ⇒ "app". */
|
|
293
|
+
auth?: "app" | "user";
|
|
260
294
|
fetchFn?: FetchFn;
|
|
261
295
|
apiBase?: string;
|
|
262
296
|
now?: () => number;
|
|
@@ -276,6 +310,7 @@ export class CompanionGithubToken {
|
|
|
276
310
|
}
|
|
277
311
|
|
|
278
312
|
private async doRefresh(): Promise<TokenOutcome> {
|
|
313
|
+
if (this.opts.auth === "user") return this.copyUserToken();
|
|
279
314
|
const now = (this.opts.now ?? Date.now)();
|
|
280
315
|
const current = tokenExpiry(this.opts.dir, this.opts.repo);
|
|
281
316
|
if (current !== undefined && current - now > REFRESH_MARGIN_MS)
|
|
@@ -307,6 +342,38 @@ export class CompanionGithubToken {
|
|
|
307
342
|
}
|
|
308
343
|
}
|
|
309
344
|
|
|
345
|
+
/** `auth = "user"`: copy the person's token file in when it differs from
|
|
346
|
+
* what the env file holds (or the repo changed). The expiry the file
|
|
347
|
+
* carries is a re-check time, not GitHub's: a user token's own expiry is
|
|
348
|
+
* not known here. */
|
|
349
|
+
private copyUserToken(): TokenOutcome {
|
|
350
|
+
const source = join(this.opts.secretsDir, "github_token");
|
|
351
|
+
let token: string;
|
|
352
|
+
try {
|
|
353
|
+
token = readFileSync(source, "utf8").trim();
|
|
354
|
+
} catch {
|
|
355
|
+
return {
|
|
356
|
+
kind: "unavailable",
|
|
357
|
+
reason: `no user token at ${source} (run \`qcr github-token sync\` on the host)`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (token === "")
|
|
361
|
+
return { kind: "unavailable", reason: `the user token file ${source} is empty` };
|
|
362
|
+
const expiresAt = new Date((this.opts.now ?? Date.now)() + REFRESH_MARGIN_MS * 2).toISOString();
|
|
363
|
+
const heldUntil = tokenExpiry(this.opts.dir, this.opts.repo);
|
|
364
|
+
if (heldUntil !== undefined && heldToken(this.opts.dir) === token)
|
|
365
|
+
return { kind: "fresh", expiresAt: new Date(heldUntil).toISOString() };
|
|
366
|
+
try {
|
|
367
|
+
writeTokenEnv(
|
|
368
|
+
this.opts.dir,
|
|
369
|
+
renderTokenEnv({ token, expires_at: expiresAt, permissions: {} }, this.opts.repo, "user"),
|
|
370
|
+
);
|
|
371
|
+
return { kind: "copied", expiresAt };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
310
377
|
/** Check every `everyMs`; each outcome goes to `onOutcome`. Idempotent. */
|
|
311
378
|
start(onOutcome: (o: TokenOutcome) => void, everyMs: number = CHECK_EVERY_MS): void {
|
|
312
379
|
if (this.timer) return;
|
package/src/tui/index.ts
CHANGED
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
RELOAD_EXIT_CODE,
|
|
35
35
|
resumeStatePath,
|
|
36
36
|
} from "./reload.js";
|
|
37
|
-
import { killInFlightSsh, runnerFor, transportOf } from "./remote.js";
|
|
37
|
+
import { NO_SSH_HOST_STDERR, killInFlightSsh, runnerFor, transportOf } from "./remote.js";
|
|
38
|
+
import { diagnoseLocalMemory } from "./memforward.js";
|
|
39
|
+
import { realProbeRunner } from "./sshtriage.js";
|
|
40
|
+
import { setupLine } from "./setupstate.js";
|
|
38
41
|
import { chainOnPoll } from "./poll-hooks.js";
|
|
39
42
|
import { deliverMessage } from "./msg.js";
|
|
40
43
|
import type { ConnectionDiagnosis } from "./sshtriage.js";
|
|
@@ -60,7 +63,7 @@ export function parseTarget(
|
|
|
60
63
|
return { kind: "bad", reason: `unknown target '${arg}' — expected a PR number` };
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
export const TUI_HELP = `qcr tui — live board and operator console for the review daemon (over ssh)
|
|
66
|
+
export const TUI_HELP = `qcr tui — live board and operator console for the review daemon (over ssh, or local docker)
|
|
64
67
|
|
|
65
68
|
usage: qcr tui [<pr>] [--no-companion] [--no-reload]
|
|
66
69
|
|
|
@@ -95,7 +98,8 @@ keys: 1…8 the eight surfaces — board · memory · digest · tickets · deplo
|
|
|
95
98
|
board: j/k select · ⏎ focus the PR's pi pane · m quick-message ·
|
|
96
99
|
g github · u unwedge · e events overlay · I set up the repo ·
|
|
97
100
|
Ctrl-q release focus · Ctrl-c ×3 hard-kill the TUI ·
|
|
98
|
-
s
|
|
101
|
+
s re-check the connection (VM: ssh, the one passphrase prompt;
|
|
102
|
+
local: the daemon container) · r refresh ·
|
|
99
103
|
? full key map · q quit
|
|
100
104
|
|
|
101
105
|
troubleshooting:
|
|
@@ -106,6 +110,12 @@ troubleshooting:
|
|
|
106
110
|
not answering. Nothing is run for you; s re-checks and
|
|
107
111
|
reports, and offers the one GCP passphrase prompt when a
|
|
108
112
|
key is what is missing
|
|
113
|
+
local route [tui] target = "local" (or an empty ssh_host on a compose
|
|
114
|
+
project): every read is docker exec into the daemon
|
|
115
|
+
container, no ssh. The board names an absent or stopped
|
|
116
|
+
container with the docker compose command that starts it
|
|
117
|
+
setup not finished the setup checklist is not complete: run qcr (the board
|
|
118
|
+
names the next step)
|
|
109
119
|
▲ DATA STALE ssh works but status.json stopped updating — the
|
|
110
120
|
container is down or the daemon hung (qcr-side:
|
|
111
121
|
ssh <host> sudo docker ps / docker logs)
|
|
@@ -355,6 +365,8 @@ export interface RunTuiDeps {
|
|
|
355
365
|
input?: NodeJS.ReadStream;
|
|
356
366
|
config?: Config;
|
|
357
367
|
exit?: (code: number) => void;
|
|
368
|
+
/** The route facts (src/tui/route.ts); default: read from this checkout. */
|
|
369
|
+
routeFacts?: import("./route.js").RouteFacts;
|
|
358
370
|
}
|
|
359
371
|
|
|
360
372
|
/** First slice of the quit-time wait for an in-flight bring-up. */
|
|
@@ -440,7 +452,18 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
440
452
|
|
|
441
453
|
// ---- phase 0: local only, ending in the first frame -----------------------
|
|
442
454
|
|
|
443
|
-
const
|
|
455
|
+
const fileConfig = deps.config ?? loadConfig();
|
|
456
|
+
// The route — which daemon the board reads, and how — from [tui] plus the
|
|
457
|
+
// facts the stack holds locally (the compose project's .env, the
|
|
458
|
+
// onboarding checklist, the Docker socket's mode). The stack is the one the
|
|
459
|
+
// home sets up (src/homes.ts `stackRoot`), so an app installed by a package
|
|
460
|
+
// manager reads it from its state home. Two small file reads and one
|
|
461
|
+
// access(2): no docker, network or walk, so it may precede the frame.
|
|
462
|
+
const { readRouteFacts, resolveTuiRoute, pinnedVersion } = await import("./route.js");
|
|
463
|
+
const { stackRoot } = await import("../homes.js");
|
|
464
|
+
const routeFacts = deps.routeFacts ?? readRouteFacts(stackRoot(process.env, sandboxRoot()));
|
|
465
|
+
const route = resolveTuiRoute(fileConfig.tui, routeFacts);
|
|
466
|
+
const loaded: Config = { ...fileConfig, tui: route.cfg };
|
|
444
467
|
// In the containerised TUI the companion is a container the host wrapper
|
|
445
468
|
// runs, reached through a shared tmux socket. Its channel dirs are the
|
|
446
469
|
// host's, named by the wrapper in env; nothing here can run docker.
|
|
@@ -454,6 +477,11 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
454
477
|
const runner = runnerFor(cfg);
|
|
455
478
|
|
|
456
479
|
const app = new App(cfg, runner, out, input);
|
|
480
|
+
// A half-finished onboarding is stated on the first frame: the checklist is
|
|
481
|
+
// the file read above, and the step titles are the wizard's own registry.
|
|
482
|
+
const { setupStateOf } = await import("./setupstate.js");
|
|
483
|
+
app.view.setup = await setupStateOf(routeFacts.checklist);
|
|
484
|
+
for (const line of route.notes) app.notice(line);
|
|
457
485
|
|
|
458
486
|
// The terminal must come back whatever kills us — raw mode + alt screen
|
|
459
487
|
// surviving a crash is exactly the "only closing the terminal works" trap.
|
|
@@ -560,6 +588,17 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
560
588
|
// (nativecompanion.ts). The containerised TUI keeps its wrapper-run
|
|
561
589
|
// container: it has no Docker of its own to ask.
|
|
562
590
|
const containerLifecycle = new CompanionLifecycle(full.companion, realDockerRunner);
|
|
591
|
+
// On a compose box the release image is already pulled (QCR_VERSION in the
|
|
592
|
+
// project .env): a first start uses it rather than building one from this
|
|
593
|
+
// checkout. An explicit [companion] image, or a local build already
|
|
594
|
+
// present, still wins (CompanionLifecycle.ensureUp).
|
|
595
|
+
{
|
|
596
|
+
const version = pinnedVersion(routeFacts.projectEnv);
|
|
597
|
+
if (version !== "") {
|
|
598
|
+
const { releaseImage } = await import("../onboard/steps/compose.js");
|
|
599
|
+
containerLifecycle.preferredImage = releaseImage(version);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
563
602
|
const selecting = socketMode
|
|
564
603
|
? undefined
|
|
565
604
|
: new SelectingCompanion(containerLifecycle, realDockerRunner, {
|
|
@@ -593,6 +632,7 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
593
632
|
dir: companionGithubDir(socketMode ? laptopInstallEnv(process.env) : undefined),
|
|
594
633
|
repo: full.github.repo,
|
|
595
634
|
secretsDir: full.github.secrets_dir,
|
|
635
|
+
auth: full.github.auth ?? "app",
|
|
596
636
|
});
|
|
597
637
|
let configMirrorTimer: NodeJS.Timeout | undefined;
|
|
598
638
|
const CONFIG_MIRROR_EVERY_MS = 30_000;
|
|
@@ -620,8 +660,17 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
620
660
|
// never written to the normal screen, where the alt screen's clear would
|
|
621
661
|
// erase them (a visible flicker). With the companion off the same column
|
|
622
662
|
// carries the skipped steps, and stays as the place that says why it is off.
|
|
623
|
-
const { StartupProgress } = await import("./startup.js");
|
|
624
|
-
|
|
663
|
+
const { StartupProgress, STARTUP_STEPS } = await import("./startup.js");
|
|
664
|
+
// The connection step is named for the route it checks: ssh to the VM, or
|
|
665
|
+
// the daemon container on this machine.
|
|
666
|
+
const startupSteps = STARTUP_STEPS.map((step) =>
|
|
667
|
+
step.key === "ssh" && transportOf(cfg) === "local"
|
|
668
|
+
? { ...step, label: "daemon container" }
|
|
669
|
+
: step.key === "ssh" && route.unconfigured !== undefined
|
|
670
|
+
? { ...step, label: "connection" }
|
|
671
|
+
: step,
|
|
672
|
+
);
|
|
673
|
+
const progress = new StartupProgress(startupSteps, {
|
|
625
674
|
onUpdate: (p) => {
|
|
626
675
|
// Kept after the bring-up settles on purpose: a `✗ companion container ·
|
|
627
676
|
// docker is not running` is the most useful thing the column can show,
|
|
@@ -714,12 +763,20 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
714
763
|
// `[openviking] endpoint = "remote"`, else the daemon container.
|
|
715
764
|
container: tuiMemoryContainer(cfg),
|
|
716
765
|
// A local engine runs in Docker; with none here, memory is off — said
|
|
717
|
-
// once as the forward's state, never retried as a failure.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
:
|
|
766
|
+
// once as the forward's state, never retried as a failure. With no ssh
|
|
767
|
+
// host there is nowhere to forward to: off, with the one remedy, rather
|
|
768
|
+
// than a relay dialing the script's default host.
|
|
769
|
+
...(transportOf(cfg) === "ssh" && cfg.ssh_host.trim() === ""
|
|
770
|
+
? { offReason: () => route.unconfigured ?? NO_SSH_HOST_STDERR }
|
|
771
|
+
: !socketMode && transportOf(cfg) === "local"
|
|
772
|
+
? {
|
|
773
|
+
offReason: async () => ((await dockerUp()) ? undefined : MEMORY_OFF_NO_DOCKER),
|
|
774
|
+
// A forward that binds but reaches nothing: say whether the memory
|
|
775
|
+
// container is there at all, and how it is started.
|
|
776
|
+
diagnoseUpstream: () =>
|
|
777
|
+
diagnoseLocalMemory(cfg, routeFacts.projectDir, realProbeRunner),
|
|
778
|
+
}
|
|
779
|
+
: {}),
|
|
723
780
|
log: (line) => app.notice(line),
|
|
724
781
|
});
|
|
725
782
|
const { buildIssueSink, ticketProjection } = await import("./issuesink.js");
|
|
@@ -1005,12 +1062,26 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
1005
1062
|
// gating the first paint and, on a bad day, an interactive prompt. Nothing
|
|
1006
1063
|
// here is self-healing by design; the TUI reports the cause and the exact
|
|
1007
1064
|
// command, and `s` re-runs the check.
|
|
1008
|
-
const { diagnosisLine, triageConnection } = await import("./sshtriage.js");
|
|
1065
|
+
const { connectionUsable, diagnosisLine, triageConnection } = await import("./sshtriage.js");
|
|
1066
|
+
const triageDeps = {
|
|
1067
|
+
projectDir: routeFacts.projectDir,
|
|
1068
|
+
...(route.unconfigured !== undefined ? { unconfigured: route.unconfigured } : {}),
|
|
1069
|
+
};
|
|
1009
1070
|
const applyDiagnosis = (d: ConnectionDiagnosis): void => {
|
|
1010
1071
|
app.view.connection = d;
|
|
1011
|
-
|
|
1072
|
+
const usable = connectionUsable(d);
|
|
1073
|
+
// Under an unfinished onboarding a stack that is not up yet is expected,
|
|
1074
|
+
// not a fault: the step is skipped with the setup line, never failed.
|
|
1075
|
+
if (usable) progress.done("ssh", d.cause);
|
|
1076
|
+
else if (app.view.setup) progress.skip("ssh", setupLine(app.view.setup));
|
|
1012
1077
|
else progress.fail("ssh", diagnosisLine(d));
|
|
1013
|
-
app.notice(
|
|
1078
|
+
app.notice(
|
|
1079
|
+
usable
|
|
1080
|
+
? `✓ ${d.cause}`
|
|
1081
|
+
: app.view.setup
|
|
1082
|
+
? setupLine(app.view.setup)
|
|
1083
|
+
: `✖ ${diagnosisLine(d)}`,
|
|
1084
|
+
);
|
|
1014
1085
|
app.repaint();
|
|
1015
1086
|
};
|
|
1016
1087
|
// `s` starts its own async chain, independent of the bring-up. Tracked for
|
|
@@ -1018,9 +1089,13 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
1018
1089
|
let pendingReconnect: Promise<void> = Promise.resolve();
|
|
1019
1090
|
app.hooks.reconnect = () => {
|
|
1020
1091
|
progress.begin("ssh", "re-checking");
|
|
1021
|
-
app.notice(
|
|
1092
|
+
app.notice(
|
|
1093
|
+
transportOf(cfg) === "local"
|
|
1094
|
+
? "checking the daemon container…"
|
|
1095
|
+
: "checking the connection to the VM…",
|
|
1096
|
+
);
|
|
1022
1097
|
pendingReconnect = (async () => {
|
|
1023
|
-
let d = await triageConnection(cfg);
|
|
1098
|
+
let d = await triageConnection(cfg, triageDeps);
|
|
1024
1099
|
// A missing key is the one class the operator can clear from here: `s`
|
|
1025
1100
|
// is them asking for the single native passphrase prompt. A wedged
|
|
1026
1101
|
// master or a dead host is reported with its command instead — prompting
|
|
@@ -1029,7 +1104,7 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
1029
1104
|
onRealTerminal(() => {
|
|
1030
1105
|
ensureSshMaster(cfg);
|
|
1031
1106
|
});
|
|
1032
|
-
d = await triageConnection(cfg);
|
|
1107
|
+
d = await triageConnection(cfg, triageDeps);
|
|
1033
1108
|
}
|
|
1034
1109
|
applyDiagnosis(d);
|
|
1035
1110
|
// Awaited, not fired and forgotten: it is part of "re-run the check and
|
|
@@ -1397,7 +1472,7 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
1397
1472
|
// says "nobody has reached this yet", which would be a lie the moment the
|
|
1398
1473
|
// probes start.
|
|
1399
1474
|
progress.begin("ssh");
|
|
1400
|
-
const triaged = triageConnection(cfg)
|
|
1475
|
+
const triaged = triageConnection(cfg, triageDeps)
|
|
1401
1476
|
.then(applyDiagnosis)
|
|
1402
1477
|
.catch((err: unknown) =>
|
|
1403
1478
|
progress.fail("ssh", `probe failed: ${String(err instanceof Error ? err.message : err)}`),
|
|
@@ -1439,12 +1514,31 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
1439
1514
|
return;
|
|
1440
1515
|
}
|
|
1441
1516
|
if (step?.state === "running" && a.poller.state.consecutiveFailures >= 2) {
|
|
1442
|
-
progress.
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1517
|
+
if (app.view.setup) progress.skip("poll", setupLine(app.view.setup));
|
|
1518
|
+
else
|
|
1519
|
+
progress.fail(
|
|
1520
|
+
"poll",
|
|
1521
|
+
app.view.connection ? diagnosisLine(app.view.connection) : "status.json unreadable",
|
|
1522
|
+
);
|
|
1446
1523
|
}
|
|
1447
1524
|
});
|
|
1525
|
+
// The checklist moves while the wizard runs in another terminal: re-read it
|
|
1526
|
+
// each poll (one small file), and recompute the next step only when it
|
|
1527
|
+
// changed. Finishing onboarding clears the banner on its own.
|
|
1528
|
+
{
|
|
1529
|
+
const { readChecklist } = await import("../onboard/engine.js");
|
|
1530
|
+
let seen = routeFacts.checklist?.updated_at ?? null;
|
|
1531
|
+
chainOnPoll(app, () => {
|
|
1532
|
+
const now = readChecklist(join(routeFacts.projectDir, "state"));
|
|
1533
|
+
const stamp = now?.updated_at ?? null;
|
|
1534
|
+
if (stamp === seen) return;
|
|
1535
|
+
seen = stamp;
|
|
1536
|
+
void setupStateOf(now).then((next) => {
|
|
1537
|
+
app.view.setup = next;
|
|
1538
|
+
app.repaint();
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1448
1542
|
|
|
1449
1543
|
// Restore a hot-reload snapshot before run(), so the first paint is already
|
|
1450
1544
|
// on the right tab with the right selection.
|
package/src/tui/memforward.ts
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
// status` reads them) rather than painted as zero.
|
|
41
41
|
import { pathToFileURL } from "node:url";
|
|
42
42
|
import { join } from "node:path";
|
|
43
|
+
import type { TuiConfig } from "../config.js";
|
|
44
|
+
import { shellQuote } from "./remote.js";
|
|
45
|
+
import type { ProbeRunner } from "./sshtriage.js";
|
|
43
46
|
|
|
44
47
|
/** The container OpenViking runs in, as the TUI and the memory forward reach
|
|
45
48
|
* it: the memory sidecar when `[tui] memory_container` names one, else the
|
|
@@ -50,6 +53,33 @@ export function tuiMemoryContainer(tui: { container: string; memory_container?:
|
|
|
50
53
|
return named !== "" ? named : tui.container;
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The local route's account of the memory container, for a forward that
|
|
58
|
+
* binds but reaches nothing: absent, stopped, or running but not answering
|
|
59
|
+
* yet. One `docker inspect`; undefined when docker itself gave no usable
|
|
60
|
+
* answer (the generic line stands).
|
|
61
|
+
*/
|
|
62
|
+
export async function diagnoseLocalMemory(
|
|
63
|
+
cfg: Pick<TuiConfig, "container" | "memory_container" | "sudo_docker">,
|
|
64
|
+
projectDir: string,
|
|
65
|
+
run: ProbeRunner,
|
|
66
|
+
): Promise<string | undefined> {
|
|
67
|
+
const container = tuiMemoryContainer(cfg);
|
|
68
|
+
const inspect = ["inspect", "-f", "{{.State.Status}}", container];
|
|
69
|
+
const res = cfg.sudo_docker
|
|
70
|
+
? await run("sudo", ["-n", "docker", ...inspect], 5_000)
|
|
71
|
+
: await run("docker", inspect, 5_000);
|
|
72
|
+
const up = `docker compose --project-directory ${shellQuote(projectDir)} up -d memory`;
|
|
73
|
+
const state = res.stdout.trim();
|
|
74
|
+
if (res.status === 0 && state === "running")
|
|
75
|
+
return `the memory container ${container} is running but OpenViking is not answering yet (it takes a minute after a start) — r retries`;
|
|
76
|
+
if (res.status === 0)
|
|
77
|
+
return `the memory container ${container} is ${state || "not running"} — start it: ${up}`;
|
|
78
|
+
if (/no such (object|container)/i.test(`${res.stderr}\n${res.stdout}`))
|
|
79
|
+
return `the memory container ${container} is not running — it starts at onboarding's bring-up step, or: ${up}`;
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
/** What the forward's `stats()` reports — the fields painted. */
|
|
54
84
|
export interface ForwardStats {
|
|
55
85
|
maxRelays: number;
|
|
@@ -166,6 +196,10 @@ export interface MemForwardDeps {
|
|
|
166
196
|
* forward `off` with it as the detail — no probe, no bind, no retry, and
|
|
167
197
|
* nothing that reads as a failure. */
|
|
168
198
|
offReason?: () => string | undefined | Promise<string | undefined>;
|
|
199
|
+
/** Asked when the forward listens but nothing answers through it: a precise
|
|
200
|
+
* account of the upstream (the memory container absent, stopped, still
|
|
201
|
+
* starting), or undefined to keep the generic line. Never throws. */
|
|
202
|
+
diagnoseUpstream?: () => Promise<string | undefined>;
|
|
169
203
|
}
|
|
170
204
|
|
|
171
205
|
const DEFAULT_PROBE_TIMEOUT_MS = 1_500;
|
|
@@ -195,12 +229,13 @@ async function httpProbe(baseUrl: string, timeoutMs: number): Promise<boolean> {
|
|
|
195
229
|
*/
|
|
196
230
|
export class MemoryForward {
|
|
197
231
|
private readonly deps: Required<
|
|
198
|
-
Omit<MemForwardDeps, "env" | "sshHost" | "container" | "offReason">
|
|
232
|
+
Omit<MemForwardDeps, "env" | "sshHost" | "container" | "offReason" | "diagnoseUpstream">
|
|
199
233
|
> & {
|
|
200
234
|
env: NodeJS.ProcessEnv;
|
|
201
235
|
sshHost?: string;
|
|
202
236
|
container?: string;
|
|
203
237
|
offReason?: MemForwardDeps["offReason"];
|
|
238
|
+
diagnoseUpstream?: MemForwardDeps["diagnoseUpstream"];
|
|
204
239
|
};
|
|
205
240
|
private handle: ForwarderHandle | null = null;
|
|
206
241
|
/** How many of its addresses our forward bound, kept with the handle so
|
|
@@ -228,6 +263,7 @@ export class MemoryForward {
|
|
|
228
263
|
...(deps.sshHost ? { sshHost: deps.sshHost } : {}),
|
|
229
264
|
...(deps.container ? { container: deps.container } : {}),
|
|
230
265
|
...(deps.offReason ? { offReason: deps.offReason } : {}),
|
|
266
|
+
...(deps.diagnoseUpstream ? { diagnoseUpstream: deps.diagnoseUpstream } : {}),
|
|
231
267
|
};
|
|
232
268
|
this.status = {
|
|
233
269
|
state: "off",
|
|
@@ -295,7 +331,8 @@ export class MemoryForward {
|
|
|
295
331
|
alive ? "owned" : "failed",
|
|
296
332
|
alive
|
|
297
333
|
? this.ownedDetail(this.binds?.bound, this.binds?.wanted)
|
|
298
|
-
:
|
|
334
|
+
: ((await this.upstreamDetail()) ??
|
|
335
|
+
"the forward we started stopped answering — r retries"),
|
|
299
336
|
);
|
|
300
337
|
}
|
|
301
338
|
if (await this.deps.probe(this.deps.baseUrl, this.deps.probeTimeoutMs)) {
|
|
@@ -367,7 +404,10 @@ export class MemoryForward {
|
|
|
367
404
|
if (!(await this.deps.probe(this.deps.baseUrl, this.deps.probeTimeoutMs))) {
|
|
368
405
|
return this.settle(
|
|
369
406
|
"failed",
|
|
370
|
-
|
|
407
|
+
(await this.upstreamDetail()) ??
|
|
408
|
+
(this.deps.local
|
|
409
|
+
? "the forward we started is listening but upstream is not answering (the memory container or OpenViking) — r retries"
|
|
410
|
+
: "the forward we started is listening but upstream is not answering (ssh, VM container or OpenViking) — r retries"),
|
|
371
411
|
);
|
|
372
412
|
}
|
|
373
413
|
return this.settle("owned", this.ownedDetail(bound, cfg.binds.length));
|
|
@@ -400,6 +440,15 @@ export class MemoryForward {
|
|
|
400
440
|
return this.inFlight;
|
|
401
441
|
}
|
|
402
442
|
|
|
443
|
+
/** The upstream's own account, when one was wired; total. */
|
|
444
|
+
private async upstreamDetail(): Promise<string | undefined> {
|
|
445
|
+
try {
|
|
446
|
+
return await this.deps.diagnoseUpstream?.();
|
|
447
|
+
} catch {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
403
452
|
/** A partial bind is a working state, not a failure: loopback alone serves
|
|
404
453
|
* the TUI, and the docker-bridge bind only matters when the companion
|
|
405
454
|
* container is up. Say which, rather than reporting a bare "ok". */
|
package/src/tui/memory.ts
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
import { countOutcome, emptyRequestCounter, type RequestCounter } from "../memory/health.js";
|
|
53
53
|
import { OpenVikingStore } from "../openviking/store.js";
|
|
54
54
|
import { readRetiredUrisFile, writeRetiredUris } from "../memory/ovsession.js";
|
|
55
|
-
import { keysFilePath
|
|
55
|
+
import { keysFilePath } from "./companion.js";
|
|
56
56
|
|
|
57
57
|
export { matchesQuery, normalizeTag, refOf } from "../memory/store.js";
|
|
58
58
|
export type {
|
|
@@ -227,20 +227,8 @@ export function keysFileReader(
|
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
*
|
|
233
|
-
* `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
|
|
234
|
-
* the forward itself takes, so a non-default port only has to be spelled once.
|
|
235
|
-
* `MEMORY.port` is the shared constant the forward and the companion already
|
|
236
|
-
* agree on.
|
|
237
|
-
*/
|
|
238
|
-
export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
239
|
-
const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
|
|
240
|
-
if (explicit !== "") return explicit.replace(/\/+$/, "");
|
|
241
|
-
const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY.port) || MEMORY.port;
|
|
242
|
-
return `http://127.0.0.1:${port}`;
|
|
243
|
-
}
|
|
230
|
+
import { tuiMemoryBaseUrl } from "./memoryurl.js";
|
|
231
|
+
export { tuiMemoryBaseUrl };
|
|
244
232
|
|
|
245
233
|
// ---- the store ---------------------------------------------------------------
|
|
246
234
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Where this host reaches OpenViking: the memory forward's loopback
|
|
2
|
+
// listener. A leaf module, so a reader of the URL (the wizard's memory step,
|
|
3
|
+
// the home) never imports the TUI's memory store or the companion.
|
|
4
|
+
|
|
5
|
+
/** The port the memory forward and the companion agree on, the server's own
|
|
6
|
+
* in the VM container (`[openviking] port`). */
|
|
7
|
+
export const MEMORY_PORT = 1933;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where the TUI reaches OpenViking: the memory forward's loopback listener.
|
|
11
|
+
*
|
|
12
|
+
* `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
|
|
13
|
+
* the forward itself takes, so a non-default port only has to be spelled once.
|
|
14
|
+
* `MEMORY_PORT` is the shared constant the forward and the companion already
|
|
15
|
+
* agree on.
|
|
16
|
+
*/
|
|
17
|
+
export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
18
|
+
const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
|
|
19
|
+
if (explicit !== "") return explicit.replace(/\/+$/, "");
|
|
20
|
+
const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY_PORT) || MEMORY_PORT;
|
|
21
|
+
return `http://127.0.0.1:${port}`;
|
|
22
|
+
}
|
package/src/tui/model.ts
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
// selection movement.
|
|
4
4
|
import type { PrLifecycleState, StatusSnapshot } from "../state.js";
|
|
5
5
|
|
|
6
|
-
export type SnapshotPr = StatusSnapshot["prs"][number]
|
|
6
|
+
export type SnapshotPr = StatusSnapshot["prs"][number] & {
|
|
7
|
+
/** A board-only row: an open PR of the repo the daemon does not track yet
|
|
8
|
+
* (status.json `open_prs`). It is offered for review (`R`); nothing about it
|
|
9
|
+
* is known beyond what the PR list says, so no PR verb but `R` and `g`
|
|
10
|
+
* acts on it. */
|
|
11
|
+
offered?: true;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The glyph of an offered row: open on GitHub, not reviewed yet. */
|
|
15
|
+
export const OFFERED_GLYPH = "○";
|
|
7
16
|
|
|
8
17
|
export const STATE_GLYPHS: Record<PrLifecycleState, string> = {
|
|
9
18
|
queued: "●",
|
|
@@ -14,7 +23,8 @@ export const STATE_GLYPHS: Record<PrLifecycleState, string> = {
|
|
|
14
23
|
};
|
|
15
24
|
|
|
16
25
|
/** `↻` prefixes the state glyph when a stale recheck is pending. */
|
|
17
|
-
export function glyphFor(pr: Pick<SnapshotPr, "state" | "stale_recheck">): string {
|
|
26
|
+
export function glyphFor(pr: Pick<SnapshotPr, "state" | "stale_recheck" | "offered">): string {
|
|
27
|
+
if (pr.offered) return OFFERED_GLYPH;
|
|
18
28
|
const base = STATE_GLYPHS[pr.state] ?? "?";
|
|
19
29
|
return pr.stale_recheck ? `↻${base}` : base;
|
|
20
30
|
}
|
|
@@ -79,6 +89,30 @@ export function sortPrs(prs: SnapshotPr[]): SnapshotPr[] {
|
|
|
79
89
|
.sort((a, b) => rank(a.state) - rank(b.state) || a.number - b.number);
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
/** The repo's open PRs the daemon does not track (status.json `open_prs`),
|
|
93
|
+
* newest activity first, as offered board rows. A tracked PR is never offered
|
|
94
|
+
* twice: its own row is the one the verbs act on. */
|
|
95
|
+
export function offeredPrs(
|
|
96
|
+
snapshot: Pick<StatusSnapshot, "prs" | "open_prs"> | undefined,
|
|
97
|
+
): SnapshotPr[] {
|
|
98
|
+
const open = snapshot?.open_prs?.prs ?? [];
|
|
99
|
+
const tracked = new Set((snapshot?.prs ?? []).filter(onBoard).map((p) => p.number));
|
|
100
|
+
return open
|
|
101
|
+
.filter((p) => !tracked.has(p.number))
|
|
102
|
+
.slice()
|
|
103
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.number - a.number)
|
|
104
|
+
.map((p) => ({
|
|
105
|
+
number: p.number,
|
|
106
|
+
state: "tracked" as const,
|
|
107
|
+
title: p.draft ? `[draft] ${p.title}` : p.title,
|
|
108
|
+
head_sha: "",
|
|
109
|
+
stale_recheck: 0,
|
|
110
|
+
author: p.author,
|
|
111
|
+
updated_at: p.updated_at,
|
|
112
|
+
offered: true as const,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
82
116
|
export interface EventLine {
|
|
83
117
|
ts: string; // HH:MM:SS
|
|
84
118
|
text: string;
|
package/src/tui/prpause.ts
CHANGED
|
@@ -20,8 +20,10 @@ import type { SshRunner } from "./remote.js";
|
|
|
20
20
|
import { commandResultArgv, commandWriteArgv, resultReason } from "./unwedge.js";
|
|
21
21
|
import { resolveLayout } from "./layout.js";
|
|
22
22
|
|
|
23
|
-
/** The
|
|
24
|
-
|
|
23
|
+
/** The payloads, read from the daemon's own `Command`: the pause pair, and
|
|
24
|
+
* `review-now` (one PR queued for a human-asked round), which rides the same
|
|
25
|
+
* sealed channel and the same receipt. */
|
|
26
|
+
export type PrLeverCommand = Extract<Command, { command: "pause-pr" | "resume-pr" | "review-now" }>;
|
|
25
27
|
|
|
26
28
|
export interface PrLeverOutcome {
|
|
27
29
|
ok: boolean;
|
|
@@ -132,6 +134,13 @@ export function renderPrLever(
|
|
|
132
134
|
return { lines, code: 1 };
|
|
133
135
|
}
|
|
134
136
|
if (cmd.command === "resume-pr") return { lines: [`✓ ${pr} resumed`], code: 0 };
|
|
137
|
+
if (cmd.command === "review-now")
|
|
138
|
+
return {
|
|
139
|
+
lines: [
|
|
140
|
+
`✓ ${pr} queued for review${out.outcome ? ` (${out.outcome})` : ""} — follow it with \`qcr why ${cmd.pr}\` or on the board`,
|
|
141
|
+
],
|
|
142
|
+
code: 0,
|
|
143
|
+
};
|
|
135
144
|
if (out.issuer !== "operator") {
|
|
136
145
|
return {
|
|
137
146
|
lines: [
|