@quickchatai/quickreview 1.2.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 +114 -50
- package/bin/qcr.js +1 -1
- package/config.example.toml +20 -6
- 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/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 +1 -1
- package/dist/extensions/login-route.js.map +1 -1
- package/dist/extensions/onboarding.js +8 -7
- 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/cli.js +12 -50
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.js +16 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +26 -0
- 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 +1 -0
- package/dist/src/debugroundpi.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/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/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/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 +42 -12
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +6 -3
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +33 -292
- package/dist/src/onboard/engine.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/configfill.js +18 -7
- package/dist/src/onboard/steps/configfill.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 +14 -2
- package/dist/src/onboard/steps/memory.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/toolnames.js +2 -2
- package/dist/src/onboard/toolnames.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/qcrbin.js +16 -11
- package/dist/src/qcrbin.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/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/takeover.js +1 -0
- package/dist/src/takeover.js.map +1 -1
- package/dist/src/takeoverpi.js +2 -1
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/board.js +1 -1
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/companion.js +2 -1
- package/dist/src/tui/companion.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/index.js +9 -6
- package/dist/src/tui/index.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/remote.js +1 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +2 -2
- package/dist/src/tui/route.js.map +1 -1
- package/dist/src/tui/setupstate.js +8 -5
- package/dist/src/tui/setupstate.js.map +1 -1
- package/dist/src/tui/sshtriage.js +1 -1
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/wire.js +6 -2
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerworld.js +4 -0
- package/dist/src/workerworld.js.map +1 -1
- package/docker/README.md +1 -1
- package/docker/entrypoint.sh +40 -6
- package/docker-compose.yml +16 -1
- package/extensions/cli/home.ts +14 -0
- package/extensions/cli/selfimprove.ts +18 -0
- package/extensions/home.ts +484 -0
- package/extensions/login-route.ts +1 -1
- package/extensions/onboarding.ts +8 -7
- 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 +8 -7
- package/schemas/review-result.v1.json +60 -0
- package/scripts/deploy.sh +8 -6
- 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/cli.ts +11 -54
- package/src/config.ts +38 -0
- package/src/daemon.ts +26 -0
- package/src/debuground.ts +3 -0
- package/src/debugroundpi.ts +1 -0
- package/src/engine/caserunner-sdk.ts +29 -314
- 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/loginrefresh/handover.ts +138 -0
- package/src/loginrefresh/lock.ts +72 -3
- package/src/loginrefresh/refresher.ts +34 -5
- package/src/loginrefresh/supervisor.ts +25 -2
- package/src/npmverbs.ts +51 -11
- package/src/onboard/choices.ts +6 -3
- package/src/onboard/engine.ts +64 -434
- package/src/onboard/steps/bringup.ts +3 -2
- package/src/onboard/steps/configfill.ts +23 -7
- package/src/onboard/steps/loginshare.ts +258 -0
- package/src/onboard/steps/memory.ts +18 -2
- package/src/onboard/steps/repoconfig.ts +85 -0
- package/src/onboard/toolnames.ts +2 -2
- package/src/piagentdir.ts +12 -0
- package/src/pilogin.ts +32 -16
- package/src/qcrbin.ts +16 -11
- package/src/reviewargs.ts +119 -0
- package/src/reviewcli.ts +6 -167
- package/src/reviewdetach.ts +107 -13
- 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 +25 -0
- package/src/takeover.ts +5 -0
- package/src/takeoverpi.ts +3 -2
- package/src/tui/board.ts +1 -1
- package/src/tui/companion.ts +2 -1
- package/src/tui/deploytab.ts +2 -2
- package/src/tui/index.ts +9 -6
- package/src/tui/memory.ts +3 -15
- package/src/tui/memoryurl.ts +22 -0
- package/src/tui/remote.ts +1 -1
- package/src/tui/route.ts +2 -2
- package/src/tui/setupstate.ts +20 -16
- package/src/tui/sshtriage.ts +1 -1
- package/src/wire.ts +6 -2
- package/src/workerworld.ts +9 -1
- 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 -75
- 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 -103
- package/src/setup/ledger.ts +0 -324
- package/src/setup/stage2.ts +0 -926
- package/src/setup/textsniff.ts +0 -95
package/src/scancli.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
// `qcr scan` — the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// `qcr scan` — the scan's verbs (src/setup/scanverbs.ts, the same ones the
|
|
2
|
+
// companion's scan tools in extensions/setup-scan.ts call) as a scriptable
|
|
3
|
+
// CLI verb, assembled over this machine's state home and config. Every
|
|
4
|
+
// subcommand renders twice: the human default, worded for the CLI, and a
|
|
5
5
|
// `--json` object on one line for scripts.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// (src/scanbox.ts); with `"host"` it is a detached process on this machine.
|
|
16
|
-
// Either way its records are the state home's, so every other subcommand
|
|
17
|
-
// reads them here, on the host. The driver runs the
|
|
18
|
-
// cases, answers the stop channel (the TUI's `[8] scan` stop and
|
|
7
|
+
// What is the app's here, around the verbs: where a run is driven. `start`
|
|
8
|
+
// (and `stage2 start`) launches a detached `qcr scan … --foreground` driver,
|
|
9
|
+
// waits until it has recorded the run (or refused), and returns. With
|
|
10
|
+
// `[scan] sandbox = "docker"` (the default) the driver runs in a throwaway
|
|
11
|
+
// container from the scan sandbox image (src/scanbox.ts); with `"host"` it is
|
|
12
|
+
// a detached process on this machine. Either way its records are the state
|
|
13
|
+
// home's, so every other subcommand reads them here, on the host. The driver
|
|
14
|
+
// runs the cases, answers the stop channel (the TUI's `[8] scan` stop and
|
|
19
15
|
// `qcr scan stop` alike), and holds the driver lease (src/setup/scanlease.ts)
|
|
20
16
|
// so that the companion and other drivers read its run as running and start
|
|
21
17
|
// nothing beside it. `--foreground` drives in the calling process instead,
|
|
@@ -23,39 +19,10 @@
|
|
|
23
19
|
// driven by anyone.
|
|
24
20
|
//
|
|
25
21
|
// Host-side beside them: `units` — the setup belt's unit-cut tools
|
|
26
|
-
// (extensions/setup.ts) — and the sandbox's `login` and `
|
|
27
|
-
//
|
|
28
|
-
// Exit codes: 0 done; 1 refused (or a waited-for run that did not end
|
|
29
|
-
// complete); 2 usage; 3 `status --wait` gave up at its `--timeout`.
|
|
30
|
-
import { dismissedLine } from "./setup/presentation.js";
|
|
22
|
+
// (extensions/setup.ts) — and the sandbox's `login`, `doctor` and `rebuild`.
|
|
31
23
|
import { spawn, spawnSync, type SpawnOptions } from "node:child_process";
|
|
32
|
-
import { closeSync, existsSync, openSync
|
|
24
|
+
import { closeSync, existsSync, openSync } from "node:fs";
|
|
33
25
|
import { join } from "node:path";
|
|
34
|
-
import {
|
|
35
|
-
CLI_VERBS,
|
|
36
|
-
FINDINGS_LIMIT,
|
|
37
|
-
startedDetails,
|
|
38
|
-
answerMatch,
|
|
39
|
-
findingsFilter,
|
|
40
|
-
listFindings,
|
|
41
|
-
previewDetails,
|
|
42
|
-
proposalDetails,
|
|
43
|
-
renderFindings,
|
|
44
|
-
renderPreview,
|
|
45
|
-
renderProposal,
|
|
46
|
-
renderQuestion,
|
|
47
|
-
renderStatus,
|
|
48
|
-
renderSuggestions,
|
|
49
|
-
stage2StartedLine,
|
|
50
|
-
startedLine,
|
|
51
|
-
stopScan,
|
|
52
|
-
} from "../extensions/setup-scan.js";
|
|
53
|
-
import {
|
|
54
|
-
CUT_CLI_VERBS,
|
|
55
|
-
renderCut,
|
|
56
|
-
renderDraft,
|
|
57
|
-
renderStatus as renderCutStatus,
|
|
58
|
-
} from "../extensions/setup.js";
|
|
59
26
|
import { loadConfig, sandboxRoot, type CompanionConfig } from "./config.js";
|
|
60
27
|
import { stateHome } from "./install.js";
|
|
61
28
|
import {
|
|
@@ -71,70 +38,81 @@ import {
|
|
|
71
38
|
DRIVER_LOG_MAX,
|
|
72
39
|
prepareDriverLog,
|
|
73
40
|
} from "./scandriverlog.js";
|
|
74
|
-
import type { SetupEngine } from "./setup/engine.js";
|
|
75
|
-
import type { FindingsStore, ScanFinding } from "./setup/findings.js";
|
|
76
|
-
import { dismissedLookalike, validityOf } from "./setup/scanreport.js";
|
|
77
|
-
import type { ScanRunner, ScanRun, ScanSettings, ScanStatus } from "./setup/scanrun.js";
|
|
78
|
-
import { lensTiers, stage2Lenses, MAX_STAGE2_ROWS } from "./setup/stage2.js";
|
|
79
|
-
import type { StoredStage2Proposal } from "./setup/stage2request.js";
|
|
80
41
|
import type { CloseFixedResult, ScanTriage } from "./setup/triage.js";
|
|
42
|
+
import type { ScanRunner, ScanRun } from "./setup/scanrun.js";
|
|
43
|
+
import type { UnitCutPort } from "@quickchatai/pi-quickreview/src/scan/unitcutstore.js";
|
|
44
|
+
import {
|
|
45
|
+
runScanVerb,
|
|
46
|
+
usage,
|
|
47
|
+
type DetachOutcome,
|
|
48
|
+
type ScanVerbDeps,
|
|
49
|
+
} from "@quickchatai/pi-quickreview/src/scan/scanverbs.js";
|
|
81
50
|
import {
|
|
82
51
|
SCAN_SCHEMA_VERSION,
|
|
83
52
|
withSchemaVersion,
|
|
84
53
|
} from "@quickchatai/pi-quickreview/src/contract.js";
|
|
85
54
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
55
|
+
// The verbs themselves, and their JSON shapes, are src/setup/scanverbs.ts's.
|
|
56
|
+
export {
|
|
57
|
+
POLL_MS,
|
|
58
|
+
SCAN_USAGE,
|
|
59
|
+
findingJson,
|
|
60
|
+
parseScanArgs,
|
|
61
|
+
runJson,
|
|
62
|
+
type DetachOutcome,
|
|
63
|
+
type FindingJson,
|
|
64
|
+
type FindingsQuery,
|
|
65
|
+
type ScanCommand,
|
|
66
|
+
} from "@quickchatai/pi-quickreview/src/scan/scanverbs.js";
|
|
67
|
+
export type { UnitCutPort };
|
|
68
|
+
|
|
69
|
+
/** What `qcr scan` runs over: the verbs' ports, with the app's triage, its
|
|
70
|
+
* tickets and the scan sandbox's doctor report. */
|
|
71
|
+
export interface ScanCliDeps extends Omit<
|
|
72
|
+
ScanVerbDeps,
|
|
73
|
+
"triage" | "afterMatch" | "doctor" | "driverLog"
|
|
74
|
+
> {
|
|
75
|
+
triage: ScanTriage | null;
|
|
76
|
+
/** After a match answer: close the tickets of the promoted findings it fixed. */
|
|
77
|
+
afterMatch: (() => Promise<CloseFixedResult>) | null;
|
|
78
|
+
/** `doctor`: the sandbox's readiness report. */
|
|
79
|
+
doctor?: () => Promise<DoctorReport>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Run one `qcr scan` subcommand; resolves its exit code. */
|
|
83
|
+
export function runScanCli(
|
|
84
|
+
rest: readonly string[],
|
|
85
|
+
json: boolean,
|
|
86
|
+
deps: ScanCliDeps,
|
|
87
|
+
): Promise<number> {
|
|
88
|
+
const { doctor, ...rest2 } = deps;
|
|
89
|
+
return runScanVerb(rest, json, {
|
|
90
|
+
...rest2,
|
|
91
|
+
driverLog: join(deps.home, DRIVER_LOG),
|
|
92
|
+
...(doctor
|
|
93
|
+
? {
|
|
94
|
+
doctor: async () => {
|
|
95
|
+
const r = await doctor();
|
|
96
|
+
return {
|
|
97
|
+
ok: r.ok,
|
|
98
|
+
human: renderDoctor(r),
|
|
99
|
+
data: r as unknown as Record<string, unknown>,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
94
106
|
|
|
95
107
|
export { DETACH_TIMEOUT_MS, DRIVER_LOG, DRIVER_LOG_MAX };
|
|
96
|
-
/** How often `status --wait` and `stop` look again. */
|
|
97
|
-
export const POLL_MS = 2000;
|
|
98
108
|
|
|
99
109
|
/** The SDK leaf host's private agent dir, under the state home: its own models
|
|
100
110
|
* list lives here, never in the operator's pi home. */
|
|
101
111
|
const SCAN_SDK_AGENT_DIR = "scan/sdk-agent";
|
|
102
112
|
|
|
103
|
-
export interface FindingsQuery {
|
|
104
|
-
state?: string;
|
|
105
|
-
lens?: string;
|
|
106
|
-
unit?: string;
|
|
107
|
-
severity?: string;
|
|
108
|
-
validity?: string;
|
|
109
|
-
doubted?: boolean;
|
|
110
|
-
limit?: number;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export type ScanCommand =
|
|
114
|
-
| { verb: "preview"; sha?: string; delta: boolean }
|
|
115
|
-
| { verb: "start"; key: string; sha?: string; delta: boolean; foreground: boolean }
|
|
116
|
-
| { verb: "status"; wait: boolean; timeout?: number }
|
|
117
|
-
| { verb: "stop" }
|
|
118
|
-
| { verb: "findings"; query: FindingsQuery }
|
|
119
|
-
| { verb: "matches" }
|
|
120
|
-
| { verb: "match"; key: string; pairs: Array<{ old: string; new: string }> }
|
|
121
|
-
| { verb: "suggestions" }
|
|
122
|
-
| { verb: "stage2-propose"; rows?: string; budget?: number; deep?: boolean; sha?: string }
|
|
123
|
-
| { verb: "stage2-start"; key: string; rows?: string[]; foreground: boolean }
|
|
124
|
-
| { verb: "dismiss"; fingerprints: string[]; reason: string }
|
|
125
|
-
| { verb: "units-show" }
|
|
126
|
-
| { verb: "units-propose"; file?: string }
|
|
127
|
-
| { verb: "units-adjust"; file: string }
|
|
128
|
-
| { verb: "units-save" }
|
|
129
|
-
| { verb: "login"; provider?: string }
|
|
130
|
-
| { verb: "doctor" }
|
|
131
|
-
| { verb: "rebuild" };
|
|
132
|
-
|
|
133
|
-
const usage = (why?: string): Error => new Error(why ? `${SCAN_USAGE}\n(${why})` : SCAN_USAGE);
|
|
134
|
-
|
|
135
113
|
/** `--sandbox host|docker`, taken off any subcommand's arguments: where this
|
|
136
|
-
* call runs a scan, over `[scan] sandbox`. The pi plugin's /scan
|
|
137
|
-
*
|
|
114
|
+
* call runs a scan, over `[scan] sandbox`. The pi plugin's /qcr-scan never
|
|
115
|
+
* comes here: it runs the plugin's own engine in its pi. */
|
|
138
116
|
export function takeSandboxFlag(rest: readonly string[]): {
|
|
139
117
|
rest: string[];
|
|
140
118
|
sandbox?: "host" | "docker";
|
|
@@ -154,735 +132,6 @@ export function takeSandboxFlag(rest: readonly string[]): {
|
|
|
154
132
|
return sandbox ? { rest: out, sandbox } : { rest: out };
|
|
155
133
|
}
|
|
156
134
|
|
|
157
|
-
/** `qcr scan`'s arguments (`--json` taken off before). An unknown token is
|
|
158
|
-
* refused, so a mistyped flag never runs something else. */
|
|
159
|
-
export function parseScanArgs(rest: readonly string[]): ScanCommand {
|
|
160
|
-
const [first, ...more] = rest;
|
|
161
|
-
let sub = first;
|
|
162
|
-
let args = more;
|
|
163
|
-
if (first === "stage2" || first === "units") {
|
|
164
|
-
sub = `${first}-${more[0] ?? ""}`;
|
|
165
|
-
args = more.slice(1);
|
|
166
|
-
}
|
|
167
|
-
const flags = new Map<string, string | true>();
|
|
168
|
-
const positional: string[] = [];
|
|
169
|
-
const VALUED = new Set([
|
|
170
|
-
"--sha",
|
|
171
|
-
"--key",
|
|
172
|
-
"--timeout",
|
|
173
|
-
"--state",
|
|
174
|
-
"--lens",
|
|
175
|
-
"--unit",
|
|
176
|
-
"--severity",
|
|
177
|
-
"--validity",
|
|
178
|
-
"--limit",
|
|
179
|
-
"--rows",
|
|
180
|
-
"--budget",
|
|
181
|
-
"--reason",
|
|
182
|
-
"--file",
|
|
183
|
-
"--provider",
|
|
184
|
-
]);
|
|
185
|
-
const BARE = new Set([
|
|
186
|
-
"--delta",
|
|
187
|
-
"--foreground",
|
|
188
|
-
"--wait",
|
|
189
|
-
"--doubted",
|
|
190
|
-
"--not-doubted",
|
|
191
|
-
"--deep",
|
|
192
|
-
"--standard",
|
|
193
|
-
]);
|
|
194
|
-
for (let i = 0; i < args.length; i++) {
|
|
195
|
-
const a = args[i]!;
|
|
196
|
-
if (VALUED.has(a)) {
|
|
197
|
-
const v = args[++i];
|
|
198
|
-
if (v === undefined || v.startsWith("--")) throw usage();
|
|
199
|
-
flags.set(a, v);
|
|
200
|
-
} else if (BARE.has(a)) flags.set(a, true);
|
|
201
|
-
else if (a.startsWith("--")) throw usage();
|
|
202
|
-
else positional.push(a);
|
|
203
|
-
}
|
|
204
|
-
const allowed = (...names: string[]): void => {
|
|
205
|
-
for (const k of flags.keys()) if (!names.includes(k)) throw usage();
|
|
206
|
-
};
|
|
207
|
-
const noPositional = (): void => {
|
|
208
|
-
if (positional.length > 0) throw usage();
|
|
209
|
-
};
|
|
210
|
-
const str = (k: string): string | undefined => {
|
|
211
|
-
const v = flags.get(k);
|
|
212
|
-
return typeof v === "string" ? v : undefined;
|
|
213
|
-
};
|
|
214
|
-
const num = (k: string, ok: (n: number) => boolean): number | undefined => {
|
|
215
|
-
const v = str(k);
|
|
216
|
-
if (v === undefined) return undefined;
|
|
217
|
-
const n = Number(v);
|
|
218
|
-
if (!Number.isFinite(n) || !ok(n)) throw usage();
|
|
219
|
-
return n;
|
|
220
|
-
};
|
|
221
|
-
const sha = (): { sha?: string } => (str("--sha") !== undefined ? { sha: str("--sha")! } : {});
|
|
222
|
-
switch (sub) {
|
|
223
|
-
case "preview":
|
|
224
|
-
allowed("--sha", "--delta");
|
|
225
|
-
noPositional();
|
|
226
|
-
return { verb: "preview", ...sha(), delta: flags.has("--delta") };
|
|
227
|
-
case "start": {
|
|
228
|
-
allowed("--key", "--sha", "--delta", "--foreground");
|
|
229
|
-
noPositional();
|
|
230
|
-
const key = str("--key");
|
|
231
|
-
if (key === undefined) throw usage("start needs --key — the key a preview showed");
|
|
232
|
-
return {
|
|
233
|
-
verb: "start",
|
|
234
|
-
key,
|
|
235
|
-
...sha(),
|
|
236
|
-
delta: flags.has("--delta"),
|
|
237
|
-
foreground: flags.has("--foreground"),
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
case "status": {
|
|
241
|
-
allowed("--wait", "--timeout");
|
|
242
|
-
noPositional();
|
|
243
|
-
const timeout = num("--timeout", (n) => n > 0);
|
|
244
|
-
if (timeout !== undefined && !flags.has("--wait")) throw usage("--timeout needs --wait");
|
|
245
|
-
return {
|
|
246
|
-
verb: "status",
|
|
247
|
-
wait: flags.has("--wait"),
|
|
248
|
-
...(timeout !== undefined ? { timeout } : {}),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
case "stop":
|
|
252
|
-
allowed();
|
|
253
|
-
noPositional();
|
|
254
|
-
return { verb: "stop" };
|
|
255
|
-
case "findings": {
|
|
256
|
-
allowed(
|
|
257
|
-
"--state",
|
|
258
|
-
"--lens",
|
|
259
|
-
"--unit",
|
|
260
|
-
"--severity",
|
|
261
|
-
"--validity",
|
|
262
|
-
"--doubted",
|
|
263
|
-
"--not-doubted",
|
|
264
|
-
"--limit",
|
|
265
|
-
);
|
|
266
|
-
noPositional();
|
|
267
|
-
if (flags.has("--doubted") && flags.has("--not-doubted")) throw usage();
|
|
268
|
-
const query: FindingsQuery = {};
|
|
269
|
-
for (const k of ["state", "lens", "unit", "severity", "validity"] as const) {
|
|
270
|
-
const v = str(`--${k}`);
|
|
271
|
-
if (v !== undefined) query[k] = v;
|
|
272
|
-
}
|
|
273
|
-
if (flags.has("--doubted")) query.doubted = true;
|
|
274
|
-
if (flags.has("--not-doubted")) query.doubted = false;
|
|
275
|
-
const limit = num("--limit", (n) => Number.isInteger(n) && n > 0);
|
|
276
|
-
if (limit !== undefined) query.limit = limit;
|
|
277
|
-
return { verb: "findings", query };
|
|
278
|
-
}
|
|
279
|
-
case "matches":
|
|
280
|
-
allowed();
|
|
281
|
-
noPositional();
|
|
282
|
-
return { verb: "matches" };
|
|
283
|
-
case "match": {
|
|
284
|
-
allowed("--key");
|
|
285
|
-
const key = str("--key");
|
|
286
|
-
if (key === undefined) throw usage("match needs --key — the key the match question showed");
|
|
287
|
-
const pairs = positional.map((p) => {
|
|
288
|
-
const m = /^([0-9a-f]+)=([0-9a-f]+)$/i.exec(p);
|
|
289
|
-
if (!m) throw usage(`a pair is OLD=NEW (two fingerprints), not ${JSON.stringify(p)}`);
|
|
290
|
-
return { old: m[1]!, new: m[2]! };
|
|
291
|
-
});
|
|
292
|
-
return { verb: "match", key, pairs };
|
|
293
|
-
}
|
|
294
|
-
case "suggestions":
|
|
295
|
-
allowed();
|
|
296
|
-
noPositional();
|
|
297
|
-
return { verb: "suggestions" };
|
|
298
|
-
case "stage2-propose": {
|
|
299
|
-
allowed("--rows", "--budget", "--deep", "--standard", "--sha");
|
|
300
|
-
noPositional();
|
|
301
|
-
if (flags.has("--deep") && flags.has("--standard")) throw usage();
|
|
302
|
-
const budget = num("--budget", (n) => n > 0);
|
|
303
|
-
return {
|
|
304
|
-
verb: "stage2-propose",
|
|
305
|
-
...(str("--rows") !== undefined ? { rows: str("--rows")! } : {}),
|
|
306
|
-
...(budget !== undefined ? { budget } : {}),
|
|
307
|
-
...(flags.has("--deep") ? { deep: true } : flags.has("--standard") ? { deep: false } : {}),
|
|
308
|
-
...sha(),
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
case "stage2-start": {
|
|
312
|
-
allowed("--key", "--rows", "--foreground");
|
|
313
|
-
noPositional();
|
|
314
|
-
const key = str("--key");
|
|
315
|
-
if (key === undefined)
|
|
316
|
-
throw usage("stage2 start needs --key — the key the stage 2 proposal showed");
|
|
317
|
-
const rows = str("--rows");
|
|
318
|
-
return {
|
|
319
|
-
verb: "stage2-start",
|
|
320
|
-
key,
|
|
321
|
-
...(rows !== undefined ? { rows: rows.split(",").map((r) => r.trim()) } : {}),
|
|
322
|
-
foreground: flags.has("--foreground"),
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
case "dismiss": {
|
|
326
|
-
allowed("--reason");
|
|
327
|
-
const reason = str("--reason");
|
|
328
|
-
if (reason === undefined) throw usage("dismiss needs --reason — it is what PR rounds read");
|
|
329
|
-
if (positional.length === 0) throw usage("dismiss needs at least one fingerprint");
|
|
330
|
-
return { verb: "dismiss", fingerprints: positional, reason };
|
|
331
|
-
}
|
|
332
|
-
case "login": {
|
|
333
|
-
allowed("--provider");
|
|
334
|
-
noPositional();
|
|
335
|
-
const provider = str("--provider");
|
|
336
|
-
return { verb: "login", ...(provider !== undefined ? { provider } : {}) };
|
|
337
|
-
}
|
|
338
|
-
case "units-show":
|
|
339
|
-
case "units-save":
|
|
340
|
-
case "doctor":
|
|
341
|
-
case "rebuild":
|
|
342
|
-
allowed();
|
|
343
|
-
noPositional();
|
|
344
|
-
return { verb: sub };
|
|
345
|
-
case "units-propose": {
|
|
346
|
-
allowed("--file");
|
|
347
|
-
noPositional();
|
|
348
|
-
const file = str("--file");
|
|
349
|
-
return { verb: "units-propose", ...(file !== undefined ? { file } : {}) };
|
|
350
|
-
}
|
|
351
|
-
case "units-adjust": {
|
|
352
|
-
allowed("--file");
|
|
353
|
-
noPositional();
|
|
354
|
-
const file = str("--file");
|
|
355
|
-
if (file === undefined)
|
|
356
|
-
throw usage("units adjust needs --file — a JSON array of adjust ops (or - for stdin)");
|
|
357
|
-
return { verb: "units-adjust", file };
|
|
358
|
-
}
|
|
359
|
-
default:
|
|
360
|
-
throw usage();
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// ---- the stable JSON shapes ----------------------------------------------------
|
|
365
|
-
|
|
366
|
-
/** One stored finding as `--json` carries it. */
|
|
367
|
-
export interface FindingJson {
|
|
368
|
-
fingerprint: string;
|
|
369
|
-
file: string;
|
|
370
|
-
range: { start: number; end: number };
|
|
371
|
-
lens: string;
|
|
372
|
-
lenses: string[];
|
|
373
|
-
unit: string;
|
|
374
|
-
kind: string;
|
|
375
|
-
severity: string;
|
|
376
|
-
validity: string;
|
|
377
|
-
state: string;
|
|
378
|
-
claim: string;
|
|
379
|
-
/** Marked "may not be valid" by the validator. */
|
|
380
|
-
doubted: boolean;
|
|
381
|
-
doubt: { word: string; reason: string; class: string | null } | null;
|
|
382
|
-
/** A dismissed finding it resembles — flagged, never auto-dismissed. */
|
|
383
|
-
looks_like_dismissed: { fingerprint: string; reason: string } | null;
|
|
384
|
-
dismissal_reason: string | null;
|
|
385
|
-
ticket: string | null;
|
|
386
|
-
first_seen: { run: string; sha: string };
|
|
387
|
-
last_seen: { run: string; sha: string };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export function findingJson(f: ScanFinding): FindingJson {
|
|
391
|
-
const look = dismissedLookalike(f);
|
|
392
|
-
return {
|
|
393
|
-
fingerprint: f.fingerprint,
|
|
394
|
-
file: f.path,
|
|
395
|
-
range: { start: f.start, end: f.end },
|
|
396
|
-
lens: f.lens,
|
|
397
|
-
lenses: f.lenses ?? [f.lens],
|
|
398
|
-
unit: f.unit,
|
|
399
|
-
kind: f.kind,
|
|
400
|
-
severity: f.severity,
|
|
401
|
-
validity: validityOf(f),
|
|
402
|
-
state: f.state,
|
|
403
|
-
claim: f.claim,
|
|
404
|
-
doubted: f.doubt !== null,
|
|
405
|
-
doubt: f.doubt
|
|
406
|
-
? { word: f.doubt.word, reason: f.doubt.reason, class: f.doubt.class ?? null }
|
|
407
|
-
: null,
|
|
408
|
-
looks_like_dismissed: look ? { fingerprint: look.fingerprint, reason: look.reason } : null,
|
|
409
|
-
dismissal_reason: f.dismissal?.reason ?? null,
|
|
410
|
-
ticket: f.ticket?.ref ?? null,
|
|
411
|
-
first_seen: { run: f.first_seen.run, sha: f.first_seen.sha },
|
|
412
|
-
last_seen: { run: f.last_seen.run, sha: f.last_seen.sha },
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** A run as `status --json` and a foreground start carry it. */
|
|
417
|
-
export function runJson(run: ScanRun & { display?: string }): Record<string, unknown> {
|
|
418
|
-
const n = (k: "findings" | "doubted"): number => run.cases.reduce((t, c) => t + c[k], 0);
|
|
419
|
-
return {
|
|
420
|
-
id: run.id,
|
|
421
|
-
mode: run.mode,
|
|
422
|
-
sha: run.sha,
|
|
423
|
-
state: run.state,
|
|
424
|
-
display: run.display ?? run.state,
|
|
425
|
-
depth: run.settings.depth ?? "standard",
|
|
426
|
-
started_at: run.started_at,
|
|
427
|
-
finished_at: run.finished_at,
|
|
428
|
-
cap_usd: run.cap_usd,
|
|
429
|
-
estimate_usd: run.estimate_usd,
|
|
430
|
-
spent_usd: run.spent_usd,
|
|
431
|
-
unpriced_cases: run.unpriced_cases,
|
|
432
|
-
cases_total: run.cases.length,
|
|
433
|
-
cases_done: run.cases.filter((c) => c.status === "done").length,
|
|
434
|
-
cases_failed: run.cases.filter((c) => c.status === "failed").length,
|
|
435
|
-
findings: n("findings"),
|
|
436
|
-
doubted: n("doubted"),
|
|
437
|
-
matching: run.matching ?? null,
|
|
438
|
-
tickets_closed: run.tickets_closed ?? 0,
|
|
439
|
-
problems: run.problems,
|
|
440
|
-
cases: run.cases.map((c) => ({
|
|
441
|
-
id: c.id,
|
|
442
|
-
unit: c.unit,
|
|
443
|
-
status: c.status,
|
|
444
|
-
findings: c.findings,
|
|
445
|
-
doubted: c.doubted,
|
|
446
|
-
cost_usd: c.cost_usd,
|
|
447
|
-
error: c.error,
|
|
448
|
-
...(c.lens !== undefined ? { lens: c.lens, tier: c.tier ?? null } : {}),
|
|
449
|
-
})),
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function proposalJson(p: StoredStage2Proposal): Record<string, unknown> {
|
|
454
|
-
return {
|
|
455
|
-
...proposalDetails(p),
|
|
456
|
-
budget_usd: p.budget_usd,
|
|
457
|
-
over_budget_usd: p.over_budget_usd,
|
|
458
|
-
validator: p.validator,
|
|
459
|
-
row_list: p.rows.map((r) => ({
|
|
460
|
-
id: r.id,
|
|
461
|
-
lens: r.lens,
|
|
462
|
-
tier: r.tier,
|
|
463
|
-
units: r.units,
|
|
464
|
-
files: r.files.map((f) => f.path),
|
|
465
|
-
tests: r.tests.map((f) => f.path),
|
|
466
|
-
lines: r.lines,
|
|
467
|
-
estimate_usd: r.estimate_usd,
|
|
468
|
-
status: r.status,
|
|
469
|
-
floor: r.floor === true,
|
|
470
|
-
reason: r.reason,
|
|
471
|
-
})),
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ---- the verb ------------------------------------------------------------------
|
|
476
|
-
|
|
477
|
-
/** What a detached driver answers once it has recorded its run (or refused);
|
|
478
|
-
* a sandboxed one names its container. */
|
|
479
|
-
export type DetachOutcome =
|
|
480
|
-
{ ok: true; run: ScanRun; log: string; container?: string } | { ok: false; errors: string[] };
|
|
481
|
-
|
|
482
|
-
/** The unit-cut verbs `qcr scan units` calls — the setup engine's. */
|
|
483
|
-
export type UnitCutPort = Pick<SetupEngine, "status" | "propose" | "adjust" | "save">;
|
|
484
|
-
|
|
485
|
-
export interface ScanCliDeps {
|
|
486
|
-
scan: ScanRunner;
|
|
487
|
-
findings: FindingsStore;
|
|
488
|
-
settingsOf: () => ScanSettings | null;
|
|
489
|
-
triage: ScanTriage | null;
|
|
490
|
-
/** After a match answer: close the tickets of the promoted findings it fixed. */
|
|
491
|
-
afterMatch: (() => Promise<CloseFixedResult>) | null;
|
|
492
|
-
/** The state home. */
|
|
493
|
-
home: string;
|
|
494
|
-
out: (line: string) => void;
|
|
495
|
-
/** Launch `qcr scan <argv> --foreground` detached; resolves once it started. */
|
|
496
|
-
detach: (argv: string[]) => Promise<DetachOutcome>;
|
|
497
|
-
sleep: (ms: number) => Promise<void>;
|
|
498
|
-
now: () => number;
|
|
499
|
-
/** While a foreground driver runs: answer the stop channel (and signals).
|
|
500
|
-
* Returns the stop. Absent: nothing is watched. */
|
|
501
|
-
watchStops?: (scan: ScanRunner) => () => void;
|
|
502
|
-
/** A foreground driver's start outcome, for the process that detached it. */
|
|
503
|
-
onStart?: (r: DetachOutcome) => void;
|
|
504
|
-
/** `units`: the setup engine over the reviewed checkout. Absent: refused. */
|
|
505
|
-
units?: UnitCutPort | null;
|
|
506
|
-
/** `doctor`: the sandbox's readiness report. */
|
|
507
|
-
doctor?: () => Promise<DoctorReport>;
|
|
508
|
-
/** `login`: pi, interactive, over the companion's pi home (in the running companion, or a throwaway container); its exit code. */
|
|
509
|
-
login?: (provider?: string) => Promise<number>;
|
|
510
|
-
/** `rebuild`: build `[scan] image` alone, stamped; no container touched. */
|
|
511
|
-
rebuild?: () => Promise<
|
|
512
|
-
{ ok: true; image: string; stamp: string } | { ok: false; errors: string[] }
|
|
513
|
-
>;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
type Emit = (code: number, human: string, data: Record<string, unknown>) => number;
|
|
517
|
-
|
|
518
|
-
/** Run one `qcr scan` subcommand; resolves its exit code. */
|
|
519
|
-
export async function runScanCli(
|
|
520
|
-
rest: readonly string[],
|
|
521
|
-
json: boolean,
|
|
522
|
-
deps: ScanCliDeps,
|
|
523
|
-
): Promise<number> {
|
|
524
|
-
const emit: Emit = (code, human, data) => {
|
|
525
|
-
deps.out(json ? JSON.stringify(withSchemaVersion(SCAN_SCHEMA_VERSION, data)) : human);
|
|
526
|
-
return code;
|
|
527
|
-
};
|
|
528
|
-
const refusal = (what: string, errors: readonly string[], code = 1): number =>
|
|
529
|
-
emit(code, `${what} refused:\n${errors.map((e) => `✗ ${e}`).join("\n")}`, {
|
|
530
|
-
ok: false,
|
|
531
|
-
errors,
|
|
532
|
-
});
|
|
533
|
-
let cmd: ScanCommand;
|
|
534
|
-
try {
|
|
535
|
-
cmd = parseScanArgs(rest);
|
|
536
|
-
} catch (err) {
|
|
537
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
538
|
-
return emit(2, msg, { ok: false, errors: [msg] });
|
|
539
|
-
}
|
|
540
|
-
const { scan } = deps;
|
|
541
|
-
switch (cmd.verb) {
|
|
542
|
-
case "preview": {
|
|
543
|
-
const r = await scan.preview({ ...(cmd.sha ? { sha: cmd.sha } : {}), delta: cmd.delta });
|
|
544
|
-
if (!r.ok) return refusal("Scan preview", r.errors);
|
|
545
|
-
const p = r.preview;
|
|
546
|
-
const start =
|
|
547
|
-
p.blockers.length > 0
|
|
548
|
-
? null
|
|
549
|
-
: ["--key", p.key, "--sha", p.sha, ...(p.mode === "delta" ? ["--delta"] : [])];
|
|
550
|
-
return emit(0, renderPreview(p, CLI_VERBS), {
|
|
551
|
-
ok: true,
|
|
552
|
-
preview: previewDetails(p),
|
|
553
|
-
start,
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
case "start":
|
|
557
|
-
case "stage2-start": {
|
|
558
|
-
const stage2 = cmd.verb === "stage2-start";
|
|
559
|
-
const argv =
|
|
560
|
-
cmd.verb === "start"
|
|
561
|
-
? [
|
|
562
|
-
"start",
|
|
563
|
-
"--key",
|
|
564
|
-
cmd.key,
|
|
565
|
-
...(cmd.sha ? ["--sha", cmd.sha] : []),
|
|
566
|
-
...(cmd.delta ? ["--delta"] : []),
|
|
567
|
-
]
|
|
568
|
-
: [
|
|
569
|
-
"stage2",
|
|
570
|
-
"start",
|
|
571
|
-
"--key",
|
|
572
|
-
cmd.key,
|
|
573
|
-
...(cmd.rows ? ["--rows", cmd.rows.join(",")] : []),
|
|
574
|
-
];
|
|
575
|
-
const what = stage2 ? "Stage 2 start" : "Scan start";
|
|
576
|
-
const line = (run: ScanRun): string =>
|
|
577
|
-
stage2
|
|
578
|
-
? stage2StartedLine(
|
|
579
|
-
run,
|
|
580
|
-
scan.stage2Proposal().proposal?.runnable ?? null,
|
|
581
|
-
cmd.verb === "stage2-start" && cmd.rows !== undefined,
|
|
582
|
-
CLI_VERBS,
|
|
583
|
-
)
|
|
584
|
-
: startedLine(run, CLI_VERBS);
|
|
585
|
-
if (!cmd.foreground) {
|
|
586
|
-
const d = await deps.detach(argv);
|
|
587
|
-
if (!d.ok) return refusal(what, d.errors);
|
|
588
|
-
return emit(
|
|
589
|
-
0,
|
|
590
|
-
`${line(d.run)}\nDriver output: ${d.log}${d.container ? `\nContainer: ${d.container}` : ""}`,
|
|
591
|
-
{
|
|
592
|
-
ok: true,
|
|
593
|
-
run: startedDetails(d.run),
|
|
594
|
-
log: d.log,
|
|
595
|
-
...(d.container ? { container: d.container } : {}),
|
|
596
|
-
},
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
const r =
|
|
600
|
-
cmd.verb === "start"
|
|
601
|
-
? await scan.start({
|
|
602
|
-
key: cmd.key,
|
|
603
|
-
...(cmd.sha ? { sha: cmd.sha } : {}),
|
|
604
|
-
delta: cmd.delta,
|
|
605
|
-
})
|
|
606
|
-
: await scan.startStage2({ key: cmd.key, ...(cmd.rows ? { rows: cmd.rows } : {}) });
|
|
607
|
-
if (!r.ok) {
|
|
608
|
-
deps.onStart?.({ ok: false, errors: r.errors });
|
|
609
|
-
return refusal(what, r.errors);
|
|
610
|
-
}
|
|
611
|
-
deps.onStart?.({ ok: true, run: r.run, log: join(deps.home, DRIVER_LOG) });
|
|
612
|
-
if (!json) deps.out(line(r.run));
|
|
613
|
-
// A pending promise alone keeps no process alive: hold it open until
|
|
614
|
-
// the run ends.
|
|
615
|
-
const keep = setInterval(() => {}, 60_000);
|
|
616
|
-
const unwatch = deps.watchStops?.(scan) ?? null;
|
|
617
|
-
try {
|
|
618
|
-
await scan.idle();
|
|
619
|
-
} finally {
|
|
620
|
-
unwatch?.();
|
|
621
|
-
clearInterval(keep);
|
|
622
|
-
}
|
|
623
|
-
const s = scan.status();
|
|
624
|
-
const complete = s.run?.display === "complete";
|
|
625
|
-
return emit(complete ? 0 : 1, renderStatus(s, CLI_VERBS), {
|
|
626
|
-
ok: complete,
|
|
627
|
-
run: s.run ? runJson(s.run) : null,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
case "status": {
|
|
631
|
-
let s = scan.status();
|
|
632
|
-
const t0 = deps.now();
|
|
633
|
-
while (cmd.wait && s.run?.display === "running") {
|
|
634
|
-
if (cmd.timeout !== undefined && deps.now() - t0 >= cmd.timeout * 1000) {
|
|
635
|
-
return emit(3, `${renderStatus(s, CLI_VERBS)}\nstill running after ${cmd.timeout}s`, {
|
|
636
|
-
...statusJson(s),
|
|
637
|
-
timed_out: true,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
await deps.sleep(POLL_MS);
|
|
641
|
-
s = scan.status();
|
|
642
|
-
}
|
|
643
|
-
const code = cmd.wait
|
|
644
|
-
? s.run === null || s.run.display === "complete"
|
|
645
|
-
? 0
|
|
646
|
-
: 1
|
|
647
|
-
: s.ok
|
|
648
|
-
? 0
|
|
649
|
-
: 1;
|
|
650
|
-
return emit(code, renderStatus(s, CLI_VERBS), statusJson(s));
|
|
651
|
-
}
|
|
652
|
-
case "stop":
|
|
653
|
-
return stop(deps, emit, refusal);
|
|
654
|
-
case "findings": {
|
|
655
|
-
const f = findingsFilter(cmd.query as Record<string, unknown>);
|
|
656
|
-
if (!f.ok) return emit(2, f.error, { ok: false, errors: [f.error] });
|
|
657
|
-
const listed = listFindings(deps.findings, f.filter);
|
|
658
|
-
if (!listed.ok) return refusal("Findings", [listed.error]);
|
|
659
|
-
const all = listed.findings;
|
|
660
|
-
const shown = all.slice(0, cmd.query.limit ?? (json ? all.length : FINDINGS_LIMIT));
|
|
661
|
-
return emit(0, renderFindings(shown, all.length), {
|
|
662
|
-
ok: true,
|
|
663
|
-
total: all.length,
|
|
664
|
-
validity: listed.validity,
|
|
665
|
-
findings: shown.map(findingJson),
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
case "matches": {
|
|
669
|
-
const { question, error } = deps.findings.question();
|
|
670
|
-
if (error !== null) return refusal("Match question", [error]);
|
|
671
|
-
return emit(0, renderQuestion(question, CLI_VERBS), {
|
|
672
|
-
ok: true,
|
|
673
|
-
question: question
|
|
674
|
-
? {
|
|
675
|
-
key: question.key,
|
|
676
|
-
run: question.run,
|
|
677
|
-
sha: question.sha,
|
|
678
|
-
unreported: question.unreported.map(findingJson),
|
|
679
|
-
fresh: question.fresh.map(findingJson),
|
|
680
|
-
}
|
|
681
|
-
: null,
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
case "match": {
|
|
685
|
-
const r = await answerMatch(
|
|
686
|
-
deps.findings,
|
|
687
|
-
{ key: cmd.key, pairs: cmd.pairs },
|
|
688
|
-
deps.afterMatch,
|
|
689
|
-
);
|
|
690
|
-
if (!r.ok) return refusal("Match", r.errors);
|
|
691
|
-
const { tickets, lines, ...result } = r;
|
|
692
|
-
return emit(0, lines.join("\n"), {
|
|
693
|
-
...result,
|
|
694
|
-
tickets_closed: tickets?.closed.length ?? 0,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
case "suggestions": {
|
|
698
|
-
const { suggestions, error } = scan.suggestions();
|
|
699
|
-
if (error !== null) return refusal("Suggestions", [error]);
|
|
700
|
-
const s = deps.settingsOf();
|
|
701
|
-
return emit(0, renderSuggestions(suggestions, s), {
|
|
702
|
-
ok: true,
|
|
703
|
-
suggestions,
|
|
704
|
-
stage2: s
|
|
705
|
-
? {
|
|
706
|
-
lenses: stage2Lenses(s.lenses).map((lens) => ({
|
|
707
|
-
lens,
|
|
708
|
-
tiers: lensTiers(s.lenses, lens, s.riskPasses ?? []),
|
|
709
|
-
})),
|
|
710
|
-
depth: s.scan.depth,
|
|
711
|
-
deep_lenses: s.scan.deep_lenses,
|
|
712
|
-
case_max_files: s.scan.case_max_files,
|
|
713
|
-
case_max_lines: s.scan.case_max_lines,
|
|
714
|
-
max_rows: MAX_STAGE2_ROWS,
|
|
715
|
-
}
|
|
716
|
-
: null,
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
case "stage2-propose": {
|
|
720
|
-
let rows: unknown = [];
|
|
721
|
-
if (cmd.rows !== undefined) {
|
|
722
|
-
try {
|
|
723
|
-
rows = JSON.parse(readFileSync(cmd.rows === "-" ? 0 : cmd.rows, "utf8"));
|
|
724
|
-
} catch (err) {
|
|
725
|
-
return refusal("Stage 2 proposal", [
|
|
726
|
-
`the rows (${cmd.rows}) cannot be read as JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
727
|
-
]);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
const r = await scan.proposeStage2({
|
|
731
|
-
rows,
|
|
732
|
-
...(cmd.budget !== undefined ? { budget_usd: cmd.budget } : {}),
|
|
733
|
-
...(cmd.sha ? { sha: cmd.sha } : {}),
|
|
734
|
-
...(cmd.deep !== undefined ? { deep: cmd.deep } : {}),
|
|
735
|
-
});
|
|
736
|
-
if (!r.ok) return refusal("Stage 2 proposal", r.errors);
|
|
737
|
-
return emit(0, renderProposal(r.proposal, CLI_VERBS), {
|
|
738
|
-
ok: true,
|
|
739
|
-
proposal: proposalJson(r.proposal),
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
case "dismiss": {
|
|
743
|
-
if (deps.triage === null) return refusal("Dismiss", ["no triage in this assembly"]);
|
|
744
|
-
const dismissed: Array<Record<string, unknown>> = [];
|
|
745
|
-
const errors: string[] = [];
|
|
746
|
-
const lines: string[] = [];
|
|
747
|
-
for (const fp of cmd.fingerprints) {
|
|
748
|
-
const r = await deps.triage.dismiss({ fingerprint: fp, reason: cmd.reason });
|
|
749
|
-
if (!r.ok) {
|
|
750
|
-
errors.push(`${fp}: ${r.errors.join("; ")}`);
|
|
751
|
-
lines.push(`✗ ${fp}: ${r.errors.join("; ")}`);
|
|
752
|
-
continue;
|
|
753
|
-
}
|
|
754
|
-
dismissed.push({
|
|
755
|
-
fingerprint: r.finding.fingerprint,
|
|
756
|
-
again: r.again,
|
|
757
|
-
memory:
|
|
758
|
-
r.memory === "written"
|
|
759
|
-
? { written: true }
|
|
760
|
-
: { written: false, problem: r.memory.problem },
|
|
761
|
-
});
|
|
762
|
-
lines.push(dismissedLine(r));
|
|
763
|
-
}
|
|
764
|
-
return emit(errors.length === 0 ? 0 : 1, lines.join("\n"), {
|
|
765
|
-
ok: errors.length === 0,
|
|
766
|
-
dismissed,
|
|
767
|
-
errors,
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
case "units-show":
|
|
771
|
-
case "units-propose":
|
|
772
|
-
case "units-adjust":
|
|
773
|
-
case "units-save":
|
|
774
|
-
return units(cmd, deps.units ?? null, emit, refusal);
|
|
775
|
-
case "doctor": {
|
|
776
|
-
if (!deps.doctor) return refusal("Doctor", ["no doctor in this assembly"]);
|
|
777
|
-
const r = await deps.doctor();
|
|
778
|
-
return emit(r.ok ? 0 : 1, renderDoctor(r), r as unknown as Record<string, unknown>);
|
|
779
|
-
}
|
|
780
|
-
case "login":
|
|
781
|
-
if (json)
|
|
782
|
-
return refusal("Login", ["login is interactive; run qcr scan login without --json"]);
|
|
783
|
-
if (!deps.login) return refusal("Login", ["no login in this assembly"]);
|
|
784
|
-
return deps.login(cmd.provider);
|
|
785
|
-
case "rebuild": {
|
|
786
|
-
if (!deps.rebuild) return refusal("Scan image rebuild", ["no rebuild in this assembly"]);
|
|
787
|
-
const r = await deps.rebuild();
|
|
788
|
-
if (!r.ok) return refusal("Scan image rebuild", r.errors);
|
|
789
|
-
return emit(
|
|
790
|
-
0,
|
|
791
|
-
`Scan image ${r.image} built (stamp ${r.stamp}); no container was stopped or replaced. ` +
|
|
792
|
-
"A companion on the same image keeps its container until `qcr companion rebuild`.",
|
|
793
|
-
{ ok: true, image: r.image, stamp: r.stamp },
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/** `qcr scan units …`: the setup belt's unit-cut tools, over the same
|
|
800
|
-
* engine and rendering, worded for the CLI. */
|
|
801
|
-
function units(
|
|
802
|
-
cmd: Extract<ScanCommand, { verb: `units-${string}` }>,
|
|
803
|
-
engine: UnitCutPort | null,
|
|
804
|
-
emit: Emit,
|
|
805
|
-
refusal: (what: string, errors: readonly string[]) => number,
|
|
806
|
-
): number {
|
|
807
|
-
if (engine === null) return refusal("Unit cut", ["no setup engine in this assembly"]);
|
|
808
|
-
const readJson = (file: string): { ok: true; value: unknown } | { ok: false; error: string } => {
|
|
809
|
-
try {
|
|
810
|
-
return { ok: true, value: JSON.parse(readFileSync(file === "-" ? 0 : file, "utf8")) };
|
|
811
|
-
} catch (err) {
|
|
812
|
-
return {
|
|
813
|
-
ok: false,
|
|
814
|
-
error: `${file === "-" ? "stdin" : file} cannot be read as JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
switch (cmd.verb) {
|
|
819
|
-
case "units-show": {
|
|
820
|
-
const s = engine.status();
|
|
821
|
-
return emit(0, renderCutStatus(s), { ok: true, status: s });
|
|
822
|
-
}
|
|
823
|
-
case "units-propose": {
|
|
824
|
-
let input: unknown;
|
|
825
|
-
if (cmd.file !== undefined) {
|
|
826
|
-
const j = readJson(cmd.file);
|
|
827
|
-
if (!j.ok) return refusal("Proposal", [j.error]);
|
|
828
|
-
input = j.value;
|
|
829
|
-
}
|
|
830
|
-
const r = engine.propose(input);
|
|
831
|
-
if (!r.ok) return refusal("Proposal", r.errors);
|
|
832
|
-
return emit(0, renderDraft("Draft stored", r.draft, CUT_CLI_VERBS), {
|
|
833
|
-
ok: true,
|
|
834
|
-
draft: r.draft,
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
case "units-adjust": {
|
|
838
|
-
const j = readJson(cmd.file);
|
|
839
|
-
if (!j.ok) return refusal("Adjustment", [j.error]);
|
|
840
|
-
// The tool's own parameter shape, `{ops: […]}`, or the ops alone.
|
|
841
|
-
const v = j.value;
|
|
842
|
-
const ops =
|
|
843
|
-
typeof v === "object" && v !== null && !Array.isArray(v) && "ops" in v ? v.ops : v;
|
|
844
|
-
const r = engine.adjust(ops);
|
|
845
|
-
if (!r.ok) return refusal("Adjustment", r.errors);
|
|
846
|
-
return emit(0, renderDraft("Draft adjusted", r.draft, CUT_CLI_VERBS), {
|
|
847
|
-
ok: true,
|
|
848
|
-
draft: r.draft,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
case "units-save": {
|
|
852
|
-
const r = engine.save();
|
|
853
|
-
if (!r.ok) return refusal("Save", r.errors);
|
|
854
|
-
return emit(0, renderCut("Unit cut saved", r.saved), { ok: true, saved: r.saved });
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function statusJson(s: ScanStatus): Record<string, unknown> {
|
|
860
|
-
return {
|
|
861
|
-
ok: s.ok,
|
|
862
|
-
problems: s.problems,
|
|
863
|
-
run: s.run ? runJson(s.run) : null,
|
|
864
|
-
active: s.active,
|
|
865
|
-
history: s.history,
|
|
866
|
-
ledger: s.ledger,
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/** `qcr scan stop`: the shared stop (`stopScan`), rendered. */
|
|
871
|
-
async function stop(
|
|
872
|
-
deps: ScanCliDeps,
|
|
873
|
-
emit: Emit,
|
|
874
|
-
refusal: (what: string, errors: readonly string[]) => number,
|
|
875
|
-
): Promise<number> {
|
|
876
|
-
const r = await stopScan(deps.scan, {
|
|
877
|
-
home: deps.home,
|
|
878
|
-
by: "cli",
|
|
879
|
-
sleep: deps.sleep,
|
|
880
|
-
now: deps.now,
|
|
881
|
-
});
|
|
882
|
-
if (!r.ok) return refusal("Scan stop", r.errors);
|
|
883
|
-
return emit(0, r.answer, { ok: true, run: r.run, answer: r.answer });
|
|
884
|
-
}
|
|
885
|
-
|
|
886
135
|
// ---- assembly ------------------------------------------------------------------
|
|
887
136
|
|
|
888
137
|
/** The env a host-side `qcr scan` assembles under: the reviewed checkout and
|