@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/home/tune.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// "Tune to this repo": the home runs Setup for the repository's profile when
|
|
2
|
+
// the person chooses it (CONTEXT.md: Setup is the survey, the review history
|
|
3
|
+
// and the interview, and its result is the repo profile). Nothing waits on
|
|
4
|
+
// it: a review before it, the first one included, runs on the default
|
|
5
|
+
// profile, and the review button never asks for it.
|
|
6
|
+
//
|
|
7
|
+
// Setup is a conversation, so it runs with the agent the person is already
|
|
8
|
+
// talking to: the home hands the chat the plugin's own Setup brief
|
|
9
|
+
// (`profileSetupBrief`, the one `/qcr-setup profile` sends), over a checkout
|
|
10
|
+
// of the repository on this machine. The agent surveys the checkout with its
|
|
11
|
+
// read tools, reads the review history when it can, interviews the person in
|
|
12
|
+
// the chat, and writes the profile, `.qcr/` at the checkout's root, through
|
|
13
|
+
// `home_tune_write`: the one write the home lends it, which writes only a
|
|
14
|
+
// profile file, only while a tune runs, and only on the person's yes.
|
|
15
|
+
//
|
|
16
|
+
// A profile is several files (profile.toml and one addendum per house rule),
|
|
17
|
+
// so the tune stays open through all of them: it ends when the agent marks a
|
|
18
|
+
// write as the profile's last file (`last`), or with the session. Until the
|
|
19
|
+
// agent writes through the tool, the home also watches the profile (the
|
|
20
|
+
// plugin's own detect: `.qcr/profile.toml` in the checkout) and says when one
|
|
21
|
+
// is written another way. A review reads the profile from the base commit,
|
|
22
|
+
// so it applies once the person commits it there.
|
|
23
|
+
//
|
|
24
|
+
// The checkout is untrusted: its contributors choose what is in it, links
|
|
25
|
+
// included. A write creates its temporary file exclusively, never through a
|
|
26
|
+
// link, and refuses a `.qcr/` or `addenda/` that is a link or resolves
|
|
27
|
+
// outside the checkout, so a yes writes only the file it names.
|
|
28
|
+
//
|
|
29
|
+
// tune a key (t, alt+t) and the `home_tune` tool
|
|
30
|
+
// write the `home_tune_write` tool: the agent's draft, the person's yes
|
|
31
|
+
import { randomBytes } from "node:crypto";
|
|
32
|
+
import {
|
|
33
|
+
closeSync,
|
|
34
|
+
constants,
|
|
35
|
+
existsSync,
|
|
36
|
+
lstatSync,
|
|
37
|
+
mkdirSync,
|
|
38
|
+
openSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
realpathSync,
|
|
41
|
+
renameSync,
|
|
42
|
+
statSync,
|
|
43
|
+
unlinkSync,
|
|
44
|
+
writeSync,
|
|
45
|
+
type Stats,
|
|
46
|
+
} from "node:fs";
|
|
47
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
48
|
+
import { profileSetupBrief } from "@quickchatai/pi-quickreview/src/plugin/setup.js";
|
|
49
|
+
import { REPO_PROFILE_DIR } from "@quickchatai/pi-quickreview/src/profileoverrides.js";
|
|
50
|
+
import { parse } from "smol-toml";
|
|
51
|
+
import type { ProcessRunner } from "../onboard/engine.js";
|
|
52
|
+
import { known, unknown, type Known, type Outcome } from "./ports.js";
|
|
53
|
+
import { parseRepoInput } from "./setup.js";
|
|
54
|
+
import type { Ask } from "./switches.js";
|
|
55
|
+
|
|
56
|
+
/** Where the repository's Setup stands. */
|
|
57
|
+
export interface TuneStatus {
|
|
58
|
+
/** It has a profile of its own: Setup has run. */
|
|
59
|
+
tuned: boolean;
|
|
60
|
+
/** As the person reads it. */
|
|
61
|
+
detail: string;
|
|
62
|
+
/** When its profile was last written, ms since the epoch; null without one. */
|
|
63
|
+
at: number | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Setup, ready to hand the agent. */
|
|
67
|
+
export interface TuneStart {
|
|
68
|
+
/** The checkout Setup reads and writes the profile into. */
|
|
69
|
+
checkout: string;
|
|
70
|
+
/** The Setup brief. */
|
|
71
|
+
brief: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TunePort {
|
|
75
|
+
status(repo: string): Promise<Known<TuneStatus>>;
|
|
76
|
+
/** Find the checkout Setup runs on, and its brief. */
|
|
77
|
+
start(repo: string): Promise<Outcome<TuneStart>>;
|
|
78
|
+
/** Write one file of the profile, `file` relative to `.qcr/`. */
|
|
79
|
+
write(checkout: string, file: string, text: string): Promise<Outcome<{ path: string }>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** How often the home looks for the profile while a tune runs. */
|
|
83
|
+
export const TUNE_POLL_MS = 5000;
|
|
84
|
+
|
|
85
|
+
/** The files of a repo profile Setup may write: profile.toml, and one
|
|
86
|
+
* addendum per house rule. Null: not one of them. */
|
|
87
|
+
export function profileFile(file: string): string | null {
|
|
88
|
+
const f = file.trim().replace(/^\.qcr\//, "");
|
|
89
|
+
if (f === "profile.toml") return f;
|
|
90
|
+
return /^addenda\/[A-Za-z0-9][A-Za-z0-9._-]*\.md$/.test(f) && !f.includes("..") ? f : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** What the home adds to the plugin's brief: the write it lends, and what
|
|
94
|
+
* the reviewer-only home's agent cannot do. */
|
|
95
|
+
export function homeBriefNote(checkout: string): string {
|
|
96
|
+
return [
|
|
97
|
+
"",
|
|
98
|
+
`In this home, the checkout is ${checkout}: read it with your read tools. Write each file of the profile with the \`home_tune_write\` tool (\`file\` is relative to ${REPO_PROFILE_DIR}/: \`profile.toml\` or \`addenda/<nn>-<rule-id>.md\`); it shows me the file and writes it only on my yes. Pass \`last: true\` with the profile's final file: the tune ends with it. If you cannot reach \`gh\` from here, skip the review history and say so.`,
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const WRITE_NO = "No, don't write it";
|
|
103
|
+
export const WRITE_YES = "Yes, write it";
|
|
104
|
+
|
|
105
|
+
const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
106
|
+
|
|
107
|
+
export interface HomeTuneOptions {
|
|
108
|
+
port?: TunePort;
|
|
109
|
+
/** The repository the home reviews, or null. */
|
|
110
|
+
repo: () => string | null;
|
|
111
|
+
say: (text: string, kind?: "info" | "ok" | "warn") => void;
|
|
112
|
+
/** Run `fn` once after `ms`; the returned function cancels it. */
|
|
113
|
+
after: (ms: number, fn: () => void) => () => void;
|
|
114
|
+
onChange?: () => void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface Tuning {
|
|
118
|
+
repo: string;
|
|
119
|
+
checkout: string;
|
|
120
|
+
before: TuneStatus | null;
|
|
121
|
+
/** The files written through the tool; once there is one, only `last` ends it. */
|
|
122
|
+
wrote: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The tune: one at a time, from the person's choice to the profile's last file. */
|
|
126
|
+
export class HomeTune {
|
|
127
|
+
private tuning: Tuning | null = null;
|
|
128
|
+
private cancel: (() => void) | null = null;
|
|
129
|
+
private disposed = false;
|
|
130
|
+
|
|
131
|
+
constructor(private readonly o: HomeTuneOptions) {}
|
|
132
|
+
|
|
133
|
+
/** Is Setup under way? */
|
|
134
|
+
running(): boolean {
|
|
135
|
+
return this.tuning !== null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
dispose(): void {
|
|
139
|
+
this.disposed = true;
|
|
140
|
+
this.cancel?.();
|
|
141
|
+
this.cancel = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Run Setup for the repository: hand `tellAgent` the brief, then watch for
|
|
145
|
+
* the profile. Without an agent to tell (outside a session), nothing starts. */
|
|
146
|
+
async tune(tellAgent: ((text: string) => void) | undefined): Promise<string> {
|
|
147
|
+
const port = this.o.port;
|
|
148
|
+
if (!port) return "Setup can't be run from this home";
|
|
149
|
+
const repo = this.o.repo();
|
|
150
|
+
if (repo === null) return "no repository is chosen, so there is nothing to tune to";
|
|
151
|
+
if (this.tuning)
|
|
152
|
+
return `Setup of ${this.tuning.repo} is under way with your agent in the chat (${this.tuning.checkout}); it ends when your agent writes the profile's last file`;
|
|
153
|
+
if (!tellAgent) return "Setup is a conversation with the chat's agent, and there is none here";
|
|
154
|
+
let started: Outcome<TuneStart>;
|
|
155
|
+
try {
|
|
156
|
+
started = await port.start(repo);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
started = { ok: false, why: message(err) };
|
|
159
|
+
}
|
|
160
|
+
if (!started.ok) {
|
|
161
|
+
this.o.say(`Can't tune to ${repo} yet: ${started.why}`, "warn");
|
|
162
|
+
return `Setup did not start: ${started.why}`;
|
|
163
|
+
}
|
|
164
|
+
const before = await this.read(repo);
|
|
165
|
+
this.tuning = {
|
|
166
|
+
repo,
|
|
167
|
+
checkout: started.checkout,
|
|
168
|
+
before: before.kind === "known" ? before.value : null,
|
|
169
|
+
wrote: [],
|
|
170
|
+
};
|
|
171
|
+
tellAgent(`${started.brief}\n${homeBriefNote(started.checkout)}`);
|
|
172
|
+
this.o.say(
|
|
173
|
+
`Tuning to ${repo}: your agent surveys ${started.checkout}, reads its review history and asks you about it, here in the chat. Reviews run on the default profile until it is done.`,
|
|
174
|
+
);
|
|
175
|
+
this.o.onChange?.();
|
|
176
|
+
this.watch();
|
|
177
|
+
return `Setup of ${repo} started with the chat's agent, over ${started.checkout}; the home says when the profile is written`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The agent's draft of one profile file, written on the person's yes.
|
|
181
|
+
* `last`: it is the profile's final file, and the tune ends with it. */
|
|
182
|
+
async write(file: string, text: string, ask: Ask | undefined, last = false): Promise<string> {
|
|
183
|
+
const t = this.tuning;
|
|
184
|
+
const port = this.o.port;
|
|
185
|
+
if (!t || !port) return "no tune is running: start one first (home_tune)";
|
|
186
|
+
const f = profileFile(file);
|
|
187
|
+
if (f === null)
|
|
188
|
+
return `${JSON.stringify(file)} is not a profile file: write profile.toml or addenda/<nn>-<rule-id>.md`;
|
|
189
|
+
if (!ask) return "the person answers whether to write it, and no one can answer here";
|
|
190
|
+
const where = join(t.checkout, REPO_PROFILE_DIR, f);
|
|
191
|
+
let yes: string | undefined;
|
|
192
|
+
try {
|
|
193
|
+
const lines = text.split("\n").filter((l) => l.trim() !== "").length;
|
|
194
|
+
yes = await ask.select(`Write ${where} (${lines} lines, as your agent showed it)?`, [
|
|
195
|
+
WRITE_NO,
|
|
196
|
+
WRITE_YES,
|
|
197
|
+
]);
|
|
198
|
+
} catch {
|
|
199
|
+
yes = undefined;
|
|
200
|
+
}
|
|
201
|
+
if (yes !== WRITE_YES) return `the person said no: ${REPO_PROFILE_DIR}/${f} was not written`;
|
|
202
|
+
let r: Outcome<{ path: string }>;
|
|
203
|
+
try {
|
|
204
|
+
r = await port.write(t.checkout, f, text);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
r = { ok: false, why: message(err) };
|
|
207
|
+
}
|
|
208
|
+
if (!r.ok) return `${REPO_PROFILE_DIR}/${f} was not written: ${r.why}`;
|
|
209
|
+
if (this.tuning !== t) return `wrote ${r.path}`;
|
|
210
|
+
t.wrote.push(f);
|
|
211
|
+
if (!last)
|
|
212
|
+
return `wrote ${r.path}; the tune stays open: write the profile's other files, and pass last: true with the final one`;
|
|
213
|
+
const n = t.wrote.length === 1 ? "1 file" : `${t.wrote.length} files`;
|
|
214
|
+
this.finish(t, `${n} of ${REPO_PROFILE_DIR}/ in ${t.checkout}`);
|
|
215
|
+
return `wrote ${r.path}; Setup is done`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private async read(repo: string): Promise<Known<TuneStatus>> {
|
|
219
|
+
try {
|
|
220
|
+
return await this.o.port!.status(repo);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return unknown(message(err));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private watch(): void {
|
|
227
|
+
if (this.disposed || !this.tuning) return;
|
|
228
|
+
this.cancel = this.o.after(TUNE_POLL_MS, () => {
|
|
229
|
+
this.cancel = null;
|
|
230
|
+
void this.check().then((done) => {
|
|
231
|
+
if (!done) this.watch();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Has the profile been written another way since the tune started? Says
|
|
237
|
+
* so once. Once the agent writes through the tool, only `last` ends it: a
|
|
238
|
+
* profile.toml is the first file of several, not the end. */
|
|
239
|
+
private async check(): Promise<boolean> {
|
|
240
|
+
const t = this.tuning;
|
|
241
|
+
if (!t || t.wrote.length > 0) return true;
|
|
242
|
+
const s = await this.read(t.repo);
|
|
243
|
+
if (this.tuning !== t || t.wrote.length > 0) return true;
|
|
244
|
+
if (s.kind === "unknown" || !s.value.tuned) return false;
|
|
245
|
+
const b = t.before;
|
|
246
|
+
const fresh = !b?.tuned || (s.value.at ?? 0) > (b.at ?? 0);
|
|
247
|
+
if (!fresh) return false;
|
|
248
|
+
this.finish(t, s.value.detail);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private finish(t: Tuning, detail: string): void {
|
|
253
|
+
this.tuning = null;
|
|
254
|
+
this.cancel?.();
|
|
255
|
+
this.cancel = null;
|
|
256
|
+
this.o.say(
|
|
257
|
+
`Tuned to ${t.repo}: ${detail}. Reviews use it once it is committed to the base branch.`,
|
|
258
|
+
"ok",
|
|
259
|
+
);
|
|
260
|
+
this.o.onChange?.();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- the real port: a checkout on this machine ---------------------------
|
|
265
|
+
|
|
266
|
+
type Env = Readonly<Record<string, string | undefined>>;
|
|
267
|
+
|
|
268
|
+
export interface CheckoutTuneOptions {
|
|
269
|
+
env: Env;
|
|
270
|
+
/** The asset root: the shipped profiles and lenses the brief points at. */
|
|
271
|
+
root: () => string;
|
|
272
|
+
/** The app config, for `[companion] reviewed_repo_dir`. */
|
|
273
|
+
configPath: () => string;
|
|
274
|
+
runner: ProcessRunner;
|
|
275
|
+
read?: (p: string) => string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The checkout of `repo` on this machine: QCR_REVIEWED_REPO, the config's
|
|
279
|
+
* `[companion] reviewed_repo_dir`, then the directory the home was opened
|
|
280
|
+
* from, each only when git says its origin is `repo`. */
|
|
281
|
+
export async function findCheckout(
|
|
282
|
+
repo: string,
|
|
283
|
+
o: Pick<CheckoutTuneOptions, "env" | "configPath" | "runner" | "read">,
|
|
284
|
+
): Promise<string | null> {
|
|
285
|
+
const candidates: string[] = [];
|
|
286
|
+
const add = (v: unknown): void => {
|
|
287
|
+
if (typeof v === "string" && v.trim() !== "") candidates.push(v.trim());
|
|
288
|
+
};
|
|
289
|
+
add(o.env["QCR_REVIEWED_REPO"]);
|
|
290
|
+
try {
|
|
291
|
+
const read = o.read ?? ((p: string) => readFileSync(p, "utf8"));
|
|
292
|
+
const cfg = parse(read(o.configPath())) as Record<string, unknown>;
|
|
293
|
+
const companion = cfg["companion"];
|
|
294
|
+
if (typeof companion === "object" && companion !== null)
|
|
295
|
+
add((companion as Record<string, unknown>)["reviewed_repo_dir"]);
|
|
296
|
+
} catch {
|
|
297
|
+
// no config to read: the other places still count
|
|
298
|
+
}
|
|
299
|
+
add(o.env["PWD"]);
|
|
300
|
+
for (const dir of candidates) {
|
|
301
|
+
const top = await o.runner.run("git", ["-C", dir, "rev-parse", "--show-toplevel"]);
|
|
302
|
+
if (top.code !== 0) continue;
|
|
303
|
+
const root = top.stdout.trim();
|
|
304
|
+
const origin = await o.runner.run("git", ["-C", root, "remote", "get-url", "origin"]);
|
|
305
|
+
const named = origin.code === 0 ? parseRepoInput(origin.stdout.trim()) : null;
|
|
306
|
+
if (named !== null && named.toLowerCase() === repo.toLowerCase()) return root;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The tune over a checkout of the repository on this machine. */
|
|
312
|
+
export function checkoutTune(o: CheckoutTuneOptions): TunePort {
|
|
313
|
+
const profileAt = (top: string): string => join(top, REPO_PROFILE_DIR, "profile.toml");
|
|
314
|
+
const missing = (repo: string): string =>
|
|
315
|
+
`Setup reads a checkout of ${repo}, and there is none here: open the home from inside one (cd into it, then run qcr), or set QCR_REVIEWED_REPO to it`;
|
|
316
|
+
return {
|
|
317
|
+
status: async (repo) => {
|
|
318
|
+
const top = await findCheckout(repo, o);
|
|
319
|
+
if (top === null) return unknown(`no checkout of ${repo} on this machine`);
|
|
320
|
+
const file = profileAt(top);
|
|
321
|
+
if (!existsSync(file))
|
|
322
|
+
return known({ tuned: false, detail: "reviews run on the default profile", at: null });
|
|
323
|
+
let at: number | null = null;
|
|
324
|
+
try {
|
|
325
|
+
at = statSync(file).mtimeMs;
|
|
326
|
+
} catch {
|
|
327
|
+
// it is there: when it was written is all that is not known
|
|
328
|
+
}
|
|
329
|
+
return known({ tuned: true, detail: `${REPO_PROFILE_DIR}/profile.toml in ${top}`, at });
|
|
330
|
+
},
|
|
331
|
+
start: async (repo) => {
|
|
332
|
+
const top = await findCheckout(repo, o);
|
|
333
|
+
if (top === null) return { ok: false, why: missing(repo) };
|
|
334
|
+
return { ok: true, checkout: top, brief: profileSetupBrief({ top, root: o.root() }) };
|
|
335
|
+
},
|
|
336
|
+
write: async (checkout, file, text) => {
|
|
337
|
+
const f = profileFile(file);
|
|
338
|
+
if (f === null) return { ok: false, why: `${file} is not a profile file` };
|
|
339
|
+
const path = join(checkout, REPO_PROFILE_DIR, f);
|
|
340
|
+
try {
|
|
341
|
+
const body = text.endsWith("\n") ? text : `${text}\n`;
|
|
342
|
+
writeInCheckout(checkout, `${REPO_PROFILE_DIR}/${f}`, body);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return { ok: false, why: message(err) };
|
|
345
|
+
}
|
|
346
|
+
return { ok: true, path };
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Write `rel` (slash-separated, below `checkout`) into an untrusted checkout.
|
|
352
|
+
* Every directory on the way is a real directory, made here when missing,
|
|
353
|
+
* never a link, and resolves inside the checkout; the file itself, when there,
|
|
354
|
+
* is a regular file. The text goes to a fresh temporary file, created
|
|
355
|
+
* exclusively and never through a link, then renamed over the target, so no
|
|
356
|
+
* name a contributor planted is opened for writing. */
|
|
357
|
+
export function writeInCheckout(checkout: string, rel: string, text: string): void {
|
|
358
|
+
const top = realpathSync(checkout);
|
|
359
|
+
const parts = rel.split("/");
|
|
360
|
+
const name = parts.pop()!;
|
|
361
|
+
let dir = top;
|
|
362
|
+
for (const part of parts) {
|
|
363
|
+
dir = join(dir, part);
|
|
364
|
+
let st: Stats;
|
|
365
|
+
try {
|
|
366
|
+
st = lstatSync(dir);
|
|
367
|
+
} catch {
|
|
368
|
+
mkdirSync(dir);
|
|
369
|
+
st = lstatSync(dir);
|
|
370
|
+
}
|
|
371
|
+
if (st.isSymbolicLink()) throw new Error(`${relative(top, dir)} is a link, not a directory`);
|
|
372
|
+
if (!st.isDirectory()) throw new Error(`${relative(top, dir)} is not a directory`);
|
|
373
|
+
}
|
|
374
|
+
const real = realpathSync(dir);
|
|
375
|
+
if (real !== top && !real.startsWith(top + sep))
|
|
376
|
+
throw new Error(`${relative(top, dir)} resolves outside the checkout`);
|
|
377
|
+
const path = join(real, name);
|
|
378
|
+
try {
|
|
379
|
+
const st = lstatSync(path);
|
|
380
|
+
if (!st.isFile()) throw new Error(`${rel} is there and is not a plain file`);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
383
|
+
}
|
|
384
|
+
const tmp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
385
|
+
const fd = openSync(
|
|
386
|
+
tmp,
|
|
387
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
388
|
+
0o644,
|
|
389
|
+
);
|
|
390
|
+
try {
|
|
391
|
+
try {
|
|
392
|
+
writeSync(fd, text);
|
|
393
|
+
} finally {
|
|
394
|
+
closeSync(fd);
|
|
395
|
+
}
|
|
396
|
+
renameSync(tmp, path);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
try {
|
|
399
|
+
unlinkSync(tmp);
|
|
400
|
+
} catch {
|
|
401
|
+
// already renamed, or never there: nothing to clean up
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The home's words for an amount: one money and one plural, so the panel, the
|
|
2
|
+
// model's notices and the review message say a price or a count alike.
|
|
3
|
+
|
|
4
|
+
/** "$1.33". */
|
|
5
|
+
export const money = (n: number): string => `$${n.toFixed(2)}`;
|
|
6
|
+
|
|
7
|
+
/** "1 finding", "3 findings". */
|
|
8
|
+
export const plural = (n: number, word: string): string => `${n} ${word}${n === 1 ? "" : "s"}`;
|
package/src/homes.ts
CHANGED
|
@@ -104,6 +104,23 @@ export function operatorStateDir(
|
|
|
104
104
|
return kind === "global" || kind === "local" ? stateHome(env) : join(root, "state");
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* The compose project the home's switches and the admin TUI share, with the
|
|
109
|
+
* setup's state under it (`<root>/state`, `<root>/.env`): `QCR_ROOT` when
|
|
110
|
+
* set; a checkout or the image is its own; an app installed by a package
|
|
111
|
+
* manager gets one in its state home, never inside the package — for the
|
|
112
|
+
* same reasons as `operatorStateDir`.
|
|
113
|
+
*/
|
|
114
|
+
export function stackRoot(
|
|
115
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
116
|
+
pkgRoot: string,
|
|
117
|
+
): string {
|
|
118
|
+
const explicit = (env["QCR_ROOT"] ?? "").trim();
|
|
119
|
+
if (explicit !== "") return explicit;
|
|
120
|
+
const kind = detectInstall(pkgRoot, env).kind;
|
|
121
|
+
return kind === "global" || kind === "local" ? join(stateHome(env), "stack") : pkgRoot;
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
export function locateConfig(opts: LocateOptions = {}): LocatedConfig {
|
|
108
125
|
return core.locateConfig(optEnv(opts));
|
|
109
126
|
}
|
package/src/install.ts
CHANGED
|
@@ -190,7 +190,7 @@ export function migrateReport(outcome: MigrateOutcome): { code: number; lines: s
|
|
|
190
190
|
return {
|
|
191
191
|
code: 0,
|
|
192
192
|
lines: [
|
|
193
|
-
`no config.toml in the checkout or at ${outcome.to} — nothing to move (\`qcr
|
|
193
|
+
`no config.toml in the checkout or at ${outcome.to} — nothing to move (a bare \`qcr\` writes one there when you choose a repository)`,
|
|
194
194
|
],
|
|
195
195
|
};
|
|
196
196
|
case "override":
|
package/src/ipc/workerspec.ts
CHANGED
|
@@ -43,6 +43,14 @@ export interface WorkerSpec {
|
|
|
43
43
|
* in memory, never written back), and where models.json and the built lens
|
|
44
44
|
* agents are read from. */
|
|
45
45
|
agent_dir: string;
|
|
46
|
+
/** Where the login is borrowed from instead, when it is not in `agent_dir`:
|
|
47
|
+
* the single box's shared login (src/loginrefresh/supervisor.ts
|
|
48
|
+
* `loginDirOf`). Absent: `agent_dir`. */
|
|
49
|
+
login_dir?: string;
|
|
50
|
+
/** The providers the review's models name (the config's, plugin/login.ts
|
|
51
|
+
* `reviewProviders`): the login's admission checks only theirs. Omitted:
|
|
52
|
+
* every login in it. */
|
|
53
|
+
providers?: string[];
|
|
46
54
|
/** Where the PR's main JSONL lives (`state/pi-sessions/pr-<n>`). The worker
|
|
47
55
|
* resumes the newest one under it, or starts a session there at birth. */
|
|
48
56
|
session_dir: string;
|
|
@@ -169,6 +177,13 @@ export function parseWorkerSpec(raw: unknown): WorkerSpec {
|
|
|
169
177
|
spec.cap_usd = cap;
|
|
170
178
|
}
|
|
171
179
|
if (raw["lens_dir"] !== undefined) spec.lens_dir = abs(raw, "lens_dir");
|
|
180
|
+
if (raw["login_dir"] !== undefined) spec.login_dir = abs(raw, "login_dir");
|
|
181
|
+
if (raw["providers"] !== undefined) {
|
|
182
|
+
const p = raw["providers"];
|
|
183
|
+
if (!Array.isArray(p) || p.some((x) => typeof x !== "string" || x === ""))
|
|
184
|
+
return bad("`providers` must be a list of provider names");
|
|
185
|
+
spec.providers = p as string[];
|
|
186
|
+
}
|
|
172
187
|
if (raw["compact"] !== undefined) {
|
|
173
188
|
if (typeof raw["compact"] !== "boolean") return bad("`compact` must be a boolean");
|
|
174
189
|
spec.compact = raw["compact"];
|
package/src/live/publish.ts
CHANGED
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
progressLogPath,
|
|
18
18
|
} from "@quickchatai/pi-quickreview/src/round-progress.js";
|
|
19
19
|
import { readLiveEvents } from "@quickchatai/pi-quickreview/src/live/events.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
RoundProgressProjection,
|
|
22
|
+
type ProjectionIdentity,
|
|
23
|
+
} from "@quickchatai/pi-quickreview/src/live/projection.js";
|
|
21
24
|
|
|
22
25
|
/** `<root>/state/round-progress/pr-<n>.json`, where the supervisor reads it. */
|
|
23
26
|
export function roundProgressPath(root: string, pr: number): string {
|
package/src/livecli.ts
CHANGED
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
readLiveEvents,
|
|
16
16
|
type LiveEvent,
|
|
17
17
|
} from "@quickchatai/pi-quickreview/src/live/events.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
latestLiveRound,
|
|
20
|
+
lensLogIds,
|
|
21
|
+
listLiveRounds,
|
|
22
|
+
type LiveRoundRef,
|
|
23
|
+
} from "@quickchatai/pi-quickreview/src/live/read.js";
|
|
19
24
|
|
|
20
25
|
export const LIVE_USAGE =
|
|
21
26
|
"usage: qcr live [RUN_ID | DIR] [--lens ID] [--round N] [--follow] [--json]";
|
package/src/loginhint.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// provider the review's models run on? A login in pi's agent dir counts, and
|
|
3
3
|
// so does the provider's own API key in the environment. An API key for some
|
|
4
4
|
// OTHER provider never does: the review runs on the models its config names,
|
|
5
|
-
// or not at all, and it never switches models to use a key. The
|
|
6
|
-
// `
|
|
5
|
+
// or not at all, and it never switches models to use a key. The one exception
|
|
6
|
+
// is the OpenAI family: an `openai-codex/` model runs on an `openai` login or
|
|
7
|
+
// OPENAI_API_KEY (and back), the same model id on either
|
|
8
|
+
// (packages/pi-plugin/src/modelroute.ts), so either login counts for it. The
|
|
9
|
+
// refusal names `qcr login`, the way to get the login.
|
|
7
10
|
import { readFileSync } from "node:fs";
|
|
8
11
|
import { join } from "node:path";
|
|
9
12
|
import {
|
|
@@ -11,6 +14,7 @@ import {
|
|
|
11
14
|
RUNTIME_KEY_ENVS,
|
|
12
15
|
type LoginSummary,
|
|
13
16
|
} from "@quickchatai/pi-quickreview/src/engine/auth.js";
|
|
17
|
+
import { routeProvider } from "@quickchatai/pi-quickreview/src/modelroute.js";
|
|
14
18
|
|
|
15
19
|
/** pi's stored logins in `agentDir`; empty when it holds none. */
|
|
16
20
|
export function readLogins(
|
|
@@ -37,8 +41,9 @@ export function reviewLoginRefusal(o: {
|
|
|
37
41
|
const name = RUNTIME_KEY_ENVS[p];
|
|
38
42
|
return name !== undefined && (o.env[name] ?? "").trim() !== "" ? name : null;
|
|
39
43
|
};
|
|
44
|
+
const usable = (p: string): boolean => logins[p] !== undefined || keyOf(p) !== null;
|
|
40
45
|
for (const p of o.providers) {
|
|
41
|
-
if (
|
|
46
|
+
if (usable(routeProvider(p, "", { hasCredential: usable }))) continue;
|
|
42
47
|
const otherKeys = Object.keys(RUNTIME_KEY_ENVS)
|
|
43
48
|
.filter((q) => q !== p)
|
|
44
49
|
.map(keyOf)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The login handover, in Node: what docker/entrypoint.sh's login mode does
|
|
2
|
+
// around pi's /login (login_handover_begin / login_handover_end), for a login
|
|
3
|
+
// made outside the container, by the home's re-login (src/home/loginowner.ts).
|
|
4
|
+
//
|
|
5
|
+
// On a single box the daemon's refresher owns the host pi home's auth.json
|
|
6
|
+
// while auto-review runs (one login per box). A login that rewrites
|
|
7
|
+
// that file beside it would race its refresh: whichever wrote last wins, and a
|
|
8
|
+
// refresh landing after the login would put the old grant back. So a login
|
|
9
|
+
// first asks the refresher to pause: it writes `auth.json.pause` and waits for
|
|
10
|
+
// `"paused": true` in the refresher's status file. A paused refresher neither
|
|
11
|
+
// refreshes nor writes, and KEEPS its lock, so no second refresher can start
|
|
12
|
+
// during the login. Releasing removes the pause file; the refresher re-reads
|
|
13
|
+
// the new login at once and resumes. A pause file left behind (the home killed
|
|
14
|
+
// mid-login) is lifted by the refresher after 15 minutes; while the hold
|
|
15
|
+
// lasts, the file's time is renewed so a slow device login is never cut off.
|
|
16
|
+
//
|
|
17
|
+
// Where no refresher holds the lock (auto-review off) nothing is paused: the
|
|
18
|
+
// home's own pi owns the login then, and its /login runs under pi's own lock.
|
|
19
|
+
// Nothing here reads or writes auth.json.
|
|
20
|
+
import { readFileSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { loginLockHeld } from "./lock.js";
|
|
23
|
+
import { LOCK_FILE, PAUSE_FILE, STATUS_FILE } from "./refresher.js";
|
|
24
|
+
|
|
25
|
+
/** How long a login waits for the refresher to pause (the entrypoint's 60 s). */
|
|
26
|
+
export const PAUSE_WAIT_MS = 60_000;
|
|
27
|
+
/** How often the status file is read while waiting (the refresher looks at the
|
|
28
|
+
* pause file every 2 s). */
|
|
29
|
+
export const PAUSE_POLL_MS = 500;
|
|
30
|
+
/** How often a held pause file's time is renewed: well inside the
|
|
31
|
+
* refresher's 15 minutes. */
|
|
32
|
+
export const PAUSE_RENEW_MS = 5 * 60_000;
|
|
33
|
+
|
|
34
|
+
export type RefresherHold =
|
|
35
|
+
/** `held`: a refresher was running and is paused now; false: none runs. */
|
|
36
|
+
{ ok: true; held: boolean; release(): void } | { ok: false; why: string };
|
|
37
|
+
|
|
38
|
+
export interface HoldOptions {
|
|
39
|
+
flockBin?: string;
|
|
40
|
+
/** When the lock cannot be probed (no flock here): does a refresher run?
|
|
41
|
+
* null: unknown. */
|
|
42
|
+
refresherRuns?: () => Promise<boolean | null>;
|
|
43
|
+
waitMs?: number;
|
|
44
|
+
pollMs?: number;
|
|
45
|
+
renewMs?: number;
|
|
46
|
+
sleep?: (ms: number) => Promise<void>;
|
|
47
|
+
now?: () => number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const realSleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
51
|
+
|
|
52
|
+
/** Does a refresher hold the login in `agentDir` now? null: unknown. */
|
|
53
|
+
export async function refresherRuns(
|
|
54
|
+
agentDir: string,
|
|
55
|
+
o: Pick<HoldOptions, "flockBin" | "refresherRuns"> = {},
|
|
56
|
+
): Promise<boolean | null> {
|
|
57
|
+
const held = loginLockHeld(join(agentDir, LOCK_FILE), o.flockBin ? { flockBin: o.flockBin } : {});
|
|
58
|
+
if (held !== null) return held;
|
|
59
|
+
return o.refresherRuns ? o.refresherRuns() : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The refresher's status file says it is paused. */
|
|
63
|
+
function pausedNow(statusPath: string): boolean {
|
|
64
|
+
try {
|
|
65
|
+
const v = JSON.parse(readFileSync(statusPath, "utf8")) as { paused?: unknown } | null;
|
|
66
|
+
return v?.paused === true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function removeQuietly(path: string): void {
|
|
73
|
+
try {
|
|
74
|
+
unlinkSync(path);
|
|
75
|
+
} catch {
|
|
76
|
+
/* lifted already */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pause the refresher of the login in `agentDir` for a login, when one runs.
|
|
82
|
+
* Answers once it is paused (or none runs), with the release; refuses when it
|
|
83
|
+
* does not pause in time (the pause file is then removed again) or when
|
|
84
|
+
* whether one runs cannot be told.
|
|
85
|
+
*/
|
|
86
|
+
export async function holdRefresher(agentDir: string, o: HoldOptions = {}): Promise<RefresherHold> {
|
|
87
|
+
const runs = await refresherRuns(agentDir, o);
|
|
88
|
+
if (runs === null)
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
why: "whether auto-review's login refresher runs can't be told on this machine",
|
|
92
|
+
};
|
|
93
|
+
if (!runs) return { ok: true, held: false, release: () => {} };
|
|
94
|
+
const pause = join(agentDir, PAUSE_FILE);
|
|
95
|
+
const status = join(agentDir, STATUS_FILE);
|
|
96
|
+
try {
|
|
97
|
+
writeFileSync(pause, "", { mode: 0o660 });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
why: `can't write ${pause}: ${err instanceof Error ? err.message : String(err)}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const sleep = o.sleep ?? realSleep;
|
|
105
|
+
const now = o.now ?? Date.now;
|
|
106
|
+
const waitMs = o.waitMs ?? PAUSE_WAIT_MS;
|
|
107
|
+
const until = now() + waitMs;
|
|
108
|
+
while (!pausedNow(status)) {
|
|
109
|
+
if (now() >= until) {
|
|
110
|
+
removeQuietly(pause);
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
why: `auto-review's login refresher did not pause within ${Math.round(waitMs / 1000)} s (${status})`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
await sleep(o.pollMs ?? PAUSE_POLL_MS);
|
|
117
|
+
}
|
|
118
|
+
const renew = setInterval(() => {
|
|
119
|
+
try {
|
|
120
|
+
const t = new Date();
|
|
121
|
+
utimesSync(pause, t, t);
|
|
122
|
+
} catch {
|
|
123
|
+
/* lifted meanwhile */
|
|
124
|
+
}
|
|
125
|
+
}, o.renewMs ?? PAUSE_RENEW_MS);
|
|
126
|
+
renew.unref?.();
|
|
127
|
+
let released = false;
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
held: true,
|
|
131
|
+
release: () => {
|
|
132
|
+
if (released) return;
|
|
133
|
+
released = true;
|
|
134
|
+
clearInterval(renew);
|
|
135
|
+
removeQuietly(pause);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|