@quickchatai/quickreview 1.2.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -53
- package/bin/qcr.js +1 -1
- package/config.example.toml +25 -7
- 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/extensions/vendor/openviking-pi/index.js +4 -0
- package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
- package/dist/extensions/vendor/openviking-pi/sync.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 +14 -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 +33 -4
- 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/ledgercheck.js +39 -10
- package/dist/src/ledgercheck.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/session-command-supervisor.js +38 -9
- package/dist/src/session-command-supervisor.js.map +1 -1
- 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 +75 -9
- 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/alerts.js +4 -1
- package/dist/src/tui/alerts.js.map +1 -1
- package/dist/src/tui/board.js +2 -2
- 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/verdicts.js +12 -0
- package/dist/src/verdicts.js.map +1 -1
- package/dist/src/wire.js +9 -4
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerbreaker.js +32 -5
- package/dist/src/workerbreaker.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/extensions/vendor/openviking-pi/NOTICE +5 -0
- package/extensions/vendor/openviking-pi/README.md +39 -5
- package/extensions/vendor/openviking-pi/index.ts +4 -0
- package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
- package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
- package/extensions/vendor/openviking-pi/sync.ts +80 -0
- package/extensions/vendor/openviking-pi/upstream.json +2 -1
- 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 +13 -54
- package/src/config.ts +38 -0
- package/src/daemon.ts +34 -4
- 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/ledgercheck.ts +45 -9
- 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/session-command-supervisor.ts +41 -8
- 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 +126 -11
- package/src/takeover.ts +5 -0
- package/src/takeoverpi.ts +3 -2
- package/src/tui/alerts.ts +9 -2
- package/src/tui/board.ts +2 -2
- 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/verdicts.ts +13 -0
- package/src/wire.ts +9 -4
- package/src/workerbreaker.ts +41 -5
- 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/onboard/engine.ts
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
|
-
// OnboardingEngine
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// tool per step); the
|
|
5
|
-
// itself is fully drivable from tests and from a non-LLM fallback
|
|
6
|
-
// nothing in here touches the network, Docker, or a real pi except
|
|
7
|
-
// the adapters.
|
|
1
|
+
// OnboardingEngine: the onboarding wizard's checklist, the plugin's setup
|
|
2
|
+
// checklist (@quickchatai/pi-quickreview/src/setupchecklist.ts, the step registry
|
|
3
|
+
// and its engine) bound to this app's adapters and state. pi is a thin
|
|
4
|
+
// driver over it (extensions/onboarding.ts registers one tool per step); the
|
|
5
|
+
// engine itself is fully drivable from tests and from a non-LLM fallback
|
|
6
|
+
// entry, and nothing in here touches the network, Docker, or a real pi except
|
|
7
|
+
// through the adapters.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
// What this module adds to the core's checklist:
|
|
10
|
+
// - the wizard's adapter set (OnboardAdapters) and its step type;
|
|
11
|
+
// - where the checklist lives: state/onboarding/checklist.json, tmp+rename
|
|
12
|
+
// atomic, re-verified on resume (the core's resumability contract);
|
|
13
|
+
// - the finish path's first-run marker, state/companion/first-run.json
|
|
14
|
+
// ({"completed_at": "<iso>"}), written exactly once: the contract the
|
|
15
|
+
// companion's first-run greeting consumes. Keep the path and shape
|
|
16
|
+
// stable.
|
|
17
|
+
import {
|
|
18
|
+
SetupChecklist,
|
|
19
|
+
checklistCompleteNow,
|
|
20
|
+
readChecklistFile,
|
|
21
|
+
writeAtomic,
|
|
22
|
+
type ChecklistAdapters,
|
|
23
|
+
type ChecklistFile,
|
|
24
|
+
type SetupStep,
|
|
25
|
+
type StepContext as ChecklistStepContext,
|
|
26
|
+
type StepRegistry,
|
|
27
|
+
type VerifyAllResult,
|
|
28
|
+
} from "@quickchatai/pi-quickreview/src/setupchecklist.js";
|
|
22
29
|
import type { StepForward, StepForwardRequest } from "./stepforward.js";
|
|
23
|
-
import { existsSync
|
|
24
|
-
import {
|
|
30
|
+
import { existsSync } from "node:fs";
|
|
31
|
+
import { join, resolve } from "node:path";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
CHECKLIST_VERSION,
|
|
35
|
+
failures,
|
|
36
|
+
readTextOrNull,
|
|
37
|
+
writeAtomic,
|
|
38
|
+
type CheckResult,
|
|
39
|
+
type ChecklistFile,
|
|
40
|
+
type ReconcileReport,
|
|
41
|
+
type StepRecord,
|
|
42
|
+
type StepReport,
|
|
43
|
+
type StepRunResult,
|
|
44
|
+
type StepStatusLine,
|
|
45
|
+
type VerifyAllResult,
|
|
46
|
+
} from "@quickchatai/pi-quickreview/src/setupchecklist.js";
|
|
25
47
|
|
|
26
48
|
// ---- adapters ---------------------------------------------------------------
|
|
27
49
|
|
|
@@ -41,7 +63,7 @@ export interface ProcessRunner {
|
|
|
41
63
|
* fake every field; extensions/onboarding.ts builds the real set. Steps use
|
|
42
64
|
* fetch (manifest exchange, health probes) and prompter (non-LLM fallback
|
|
43
65
|
* entry) as they need them; preflight needs neither. */
|
|
44
|
-
export interface OnboardAdapters {
|
|
66
|
+
export interface OnboardAdapters extends ChecklistAdapters {
|
|
45
67
|
/** HTTP seam (GitHub App manifest exchange, health endpoints, …). */
|
|
46
68
|
fetch: typeof fetch;
|
|
47
69
|
runner: ProcessRunner;
|
|
@@ -70,101 +92,17 @@ export interface OnboardAdapters {
|
|
|
70
92
|
|
|
71
93
|
// ---- step vocabulary --------------------------------------------------------
|
|
72
94
|
|
|
73
|
-
export
|
|
74
|
-
name: string;
|
|
75
|
-
ok: boolean;
|
|
76
|
-
detail: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** What run() and verify() both return. `data` (run only) is persisted onto
|
|
80
|
-
* the step's checklist record and handed back as ctx.data on later calls. */
|
|
81
|
-
export interface StepReport {
|
|
82
|
-
ok: boolean;
|
|
83
|
-
checks: CheckResult[];
|
|
84
|
-
data?: Record<string, unknown>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface StepContext {
|
|
88
|
-
adapters: OnboardAdapters;
|
|
89
|
-
/** The tool call's inputs — the operator's answer to a step that asks one
|
|
90
|
-
* (`{}` for a step that declares no `parameters`, and always on verify()).
|
|
91
|
-
* A step that needs an answer and does not get one must return a refusal
|
|
92
|
-
* naming the options, never a guess: the wizard offers, the operator picks. */
|
|
93
|
-
input: Readonly<Record<string, unknown>>;
|
|
94
|
-
/** Recorded operator choices (topology, auth route, captured uid, …). */
|
|
95
|
-
choices: Readonly<Record<string, unknown>>;
|
|
96
|
-
/** This step's persisted data from a previous run ({} on first run). */
|
|
97
|
-
data: Readonly<Record<string, unknown>>;
|
|
98
|
-
/** Record an operator choice / derived fact for later steps (persisted with
|
|
99
|
-
* the step result). */
|
|
100
|
-
recordChoice(key: string, value: unknown): void;
|
|
101
|
-
}
|
|
95
|
+
export type StepContext = ChecklistStepContext<OnboardAdapters>;
|
|
102
96
|
|
|
103
97
|
/**
|
|
104
98
|
* One onboarding step (the registered set is defaultSteps in
|
|
105
|
-
* src/onboard/steps/index.ts)
|
|
106
|
-
*
|
|
107
|
-
* registers each step as tool `onboard_<id>`;
|
|
108
|
-
* - `explain` is the teach-text the guide relays before running the step;
|
|
109
|
-
* - `enabled` gates on recorded choices (e.g. the VM route's steps are
|
|
110
|
-
* disabled on the laptop topology); absent ⇒ always enabled;
|
|
111
|
-
* - `run` performs the step; `verify` re-checks its outcome against reality
|
|
112
|
-
* and must be safe to call any number of times.
|
|
99
|
+
* src/onboard/steps/index.ts): the core's SetupStep over the wizard's
|
|
100
|
+
* adapters. The pi extension registers each step as tool `onboard_<id>`.
|
|
113
101
|
*/
|
|
114
|
-
export
|
|
115
|
-
id: string;
|
|
116
|
-
title: string;
|
|
117
|
-
explain: string;
|
|
118
|
-
/** Ascending display/registration order. */
|
|
119
|
-
order: number;
|
|
120
|
-
/** JSON-schema object the step's pi tool declares, for steps that need the
|
|
121
|
-
* operator's answer (a choice, a hostname). Absent ⇒ the tool takes no
|
|
122
|
-
* input, which is the common case. */
|
|
123
|
-
parameters?: unknown;
|
|
124
|
-
enabled?(choices: Readonly<Record<string, unknown>>): boolean;
|
|
125
|
-
/** After setup: the step still runs and verifies like any other, but the
|
|
126
|
-
* checklist completes without it — finish never waits on it. (The first
|
|
127
|
-
* review and the go-live flip: the board offers them once setup is done.) */
|
|
128
|
-
optional?: boolean;
|
|
129
|
-
run(ctx: StepContext): Promise<StepReport>;
|
|
130
|
-
verify(ctx: StepContext): Promise<StepReport>;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function failures(checks: readonly CheckResult[]): string[] {
|
|
134
|
-
return checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
|
|
135
|
-
}
|
|
102
|
+
export type OnboardingStep = SetupStep<OnboardAdapters>;
|
|
136
103
|
|
|
137
104
|
// ---- checklist file ---------------------------------------------------------
|
|
138
105
|
|
|
139
|
-
export const CHECKLIST_VERSION = 1;
|
|
140
|
-
|
|
141
|
-
export interface StepRecord {
|
|
142
|
-
status: "pending" | "done";
|
|
143
|
-
completed_at: string | null;
|
|
144
|
-
/** Last successful re-verification (resume / finish). */
|
|
145
|
-
verified_at: string | null;
|
|
146
|
-
/** Why a claimed-done step was demoted, or other engine notes. */
|
|
147
|
-
note: string | null;
|
|
148
|
-
data: Record<string, unknown>;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface ChecklistFile {
|
|
152
|
-
version: number;
|
|
153
|
-
created_at: string;
|
|
154
|
-
updated_at: string;
|
|
155
|
-
/** Set exactly once, by finish() when every enabled step verifies green.
|
|
156
|
-
* History — when onboarding first finished; it never moves and never clears. */
|
|
157
|
-
completed_at: string | null;
|
|
158
|
-
/** When a completed checklist stopped verifying (a demotion, a failed run,
|
|
159
|
-
* a refused finish, a newly-enabled unfinished step, a step run again —
|
|
160
|
-
* a run changes what the full verification checked); null/absent while the
|
|
161
|
-
* completion still holds. A green finish() clears it. Whether onboarding is
|
|
162
|
-
* complete now is `completed_at` set and this unset. */
|
|
163
|
-
reopened_at?: string | null;
|
|
164
|
-
choices: Record<string, unknown>;
|
|
165
|
-
steps: Record<string, StepRecord>;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
106
|
export function checklistPath(stateDir: string): string {
|
|
169
107
|
return join(stateDir, "onboarding", "checklist.json");
|
|
170
108
|
}
|
|
@@ -175,32 +113,9 @@ export function firstRunMarkerPath(stateDir: string): string {
|
|
|
175
113
|
return join(stateDir, "companion", "first-run.json");
|
|
176
114
|
}
|
|
177
115
|
|
|
178
|
-
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
179
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
116
|
/** Read the checklist; null when absent or unreadable (both mean "fresh"). */
|
|
183
117
|
export function readChecklist(stateDir: string): ChecklistFile | null {
|
|
184
|
-
|
|
185
|
-
if (!existsSync(path)) return null;
|
|
186
|
-
try {
|
|
187
|
-
const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
188
|
-
if (typeof raw !== "object" || raw === null) return null;
|
|
189
|
-
const file = raw as ChecklistFile;
|
|
190
|
-
// Every field the engine dereferences is checked here: a file that parses
|
|
191
|
-
// but lacks one (hand-edited, foreign) would otherwise throw out of a
|
|
192
|
-
// step predicate or record() on every call — a wedge, not a fresh start.
|
|
193
|
-
if (typeof file.version !== "number") return null;
|
|
194
|
-
if (!isPlainObject(file.choices)) return null;
|
|
195
|
-
if (file.completed_at !== null && typeof file.completed_at !== "string") return null;
|
|
196
|
-
if (!isPlainObject(file.steps)) return null;
|
|
197
|
-
for (const rec of Object.values(file.steps)) {
|
|
198
|
-
if (!isPlainObject(rec) || !isPlainObject(rec.data)) return null;
|
|
199
|
-
}
|
|
200
|
-
return file;
|
|
201
|
-
} catch {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
118
|
+
return readChecklistFile(checklistPath(stateDir));
|
|
204
119
|
}
|
|
205
120
|
|
|
206
121
|
/** Does the checklist file say onboarding finished? The CLI's post-exit
|
|
@@ -208,13 +123,7 @@ export function readChecklist(stateDir: string): ChecklistFile | null {
|
|
|
208
123
|
* re-verification, so "the file says so" is a verified claim). */
|
|
209
124
|
export function checklistComplete(stateDir: string): boolean {
|
|
210
125
|
const file = readChecklist(stateDir);
|
|
211
|
-
return file !== null &&
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Is onboarding complete NOW: finished once, and not reopened since. The one
|
|
215
|
-
* reading of the two fields, for the file on disk and the engine's copy. */
|
|
216
|
-
function completeNow(file: Pick<ChecklistFile, "completed_at" | "reopened_at">): boolean {
|
|
217
|
-
return typeof file.completed_at === "string" && !file.reopened_at;
|
|
126
|
+
return file !== null && checklistCompleteNow(file);
|
|
218
127
|
}
|
|
219
128
|
|
|
220
129
|
/** `<sandbox root>` — the parent of `paths.stateDir`, by the adapters'
|
|
@@ -223,315 +132,36 @@ export function sandboxRoot(paths: { readonly stateDir: string }): string {
|
|
|
223
132
|
return resolve(paths.stateDir, "..");
|
|
224
133
|
}
|
|
225
134
|
|
|
226
|
-
/** tmp+rename atomic write — the one writer every onboarding file write goes
|
|
227
|
-
* through (the checklist, config.toml, .env, the inventory): a reader, the
|
|
228
|
-
* daemon's own loader one bind mount away included, never sees a half-written
|
|
229
|
-
* file, and an interrupted write leaves the previous file whole. */
|
|
230
|
-
export function writeAtomic(path: string, content: string): void {
|
|
231
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
232
|
-
const tmp = join(dirname(path), `.${basename(path)}.onboard.tmp`);
|
|
233
|
-
writeFileSync(tmp, content);
|
|
234
|
-
renameSync(tmp, path);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** A file's text, or null when it cannot be read for any reason. For a read
|
|
238
|
-
* where only ABSENCE may be null, read it yourself. */
|
|
239
|
-
export function readTextOrNull(path: string): string | null {
|
|
240
|
-
try {
|
|
241
|
-
return readFileSync(path, "utf8");
|
|
242
|
-
} catch {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
135
|
// ---- engine -----------------------------------------------------------------
|
|
248
136
|
|
|
249
|
-
export interface StepStatusLine {
|
|
250
|
-
id: string;
|
|
251
|
-
title: string;
|
|
252
|
-
explain: string;
|
|
253
|
-
status: "pending" | "done" | "disabled";
|
|
254
|
-
note: string | null;
|
|
255
|
-
/** The step's `optional` flag: setup completes without it. */
|
|
256
|
-
optional?: boolean;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export interface ReconcileReport {
|
|
260
|
-
/** Claimed-done steps whose verify() held. */
|
|
261
|
-
verified: string[];
|
|
262
|
-
/** Claimed-done steps whose verify() failed — demoted to pending. */
|
|
263
|
-
demoted: Array<{ id: string; failures: string[] }>;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export interface StepRunResult {
|
|
267
|
-
id: string;
|
|
268
|
-
ok: boolean;
|
|
269
|
-
checks: CheckResult[];
|
|
270
|
-
failures: string[];
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export interface VerifyAllResult {
|
|
274
|
-
ok: boolean;
|
|
275
|
-
/** `optional` results are reported but never hold `ok` false. */
|
|
276
|
-
results: Array<{ id: string; ok: boolean; failures: string[]; optional?: boolean }>;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
137
|
export type FinishResult =
|
|
280
138
|
| { ok: true; completed_at: string; marker_written: boolean }
|
|
281
139
|
| { ok: false; results: VerifyAllResult["results"] };
|
|
282
140
|
|
|
283
|
-
export class OnboardingEngine {
|
|
284
|
-
private readonly registry: OnboardingStep[];
|
|
285
|
-
private checklist: ChecklistFile;
|
|
286
|
-
|
|
141
|
+
export class OnboardingEngine extends SetupChecklist<OnboardAdapters> {
|
|
287
142
|
constructor(
|
|
288
|
-
steps: OnboardingStep[],
|
|
289
|
-
|
|
143
|
+
steps: StepRegistry<OnboardAdapters> | readonly OnboardingStep[],
|
|
144
|
+
adapters: OnboardAdapters,
|
|
290
145
|
) {
|
|
291
|
-
|
|
292
|
-
const loaded = readChecklist(adapters.paths.stateDir);
|
|
293
|
-
if (loaded === null && existsSync(checklistPath(adapters.paths.stateDir))) {
|
|
294
|
-
adapters.log?.(
|
|
295
|
-
"onboarding: checklist file unreadable — starting fresh (recorded choices and step progress were reset)",
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
this.checklist = loaded ?? this.fresh();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private fresh(): ChecklistFile {
|
|
302
|
-
const now = this.adapters.clock().toISOString();
|
|
303
|
-
return {
|
|
304
|
-
version: CHECKLIST_VERSION,
|
|
305
|
-
created_at: now,
|
|
306
|
-
updated_at: now,
|
|
307
|
-
completed_at: null,
|
|
308
|
-
choices: {},
|
|
309
|
-
steps: {},
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
private save(): void {
|
|
314
|
-
this.checklist.updated_at = this.adapters.clock().toISOString();
|
|
315
|
-
writeAtomic(
|
|
316
|
-
checklistPath(this.adapters.paths.stateDir),
|
|
317
|
-
`${JSON.stringify(this.checklist, null, 2)}\n`,
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private record(id: string): StepRecord {
|
|
322
|
-
const existing = this.checklist.steps[id];
|
|
323
|
-
if (existing) return existing;
|
|
324
|
-
const created: StepRecord = {
|
|
325
|
-
status: "pending",
|
|
326
|
-
completed_at: null,
|
|
327
|
-
verified_at: null,
|
|
328
|
-
note: null,
|
|
329
|
-
data: {},
|
|
330
|
-
};
|
|
331
|
-
this.checklist.steps[id] = created;
|
|
332
|
-
return created;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
private ctxFor(id: string, input: Record<string, unknown> = {}): StepContext {
|
|
336
|
-
return {
|
|
337
|
-
adapters: this.adapters,
|
|
338
|
-
input,
|
|
339
|
-
choices: this.checklist.choices,
|
|
340
|
-
data: this.record(id).data,
|
|
341
|
-
recordChoice: (key, value) => {
|
|
342
|
-
this.checklist.choices[key] = value;
|
|
343
|
-
},
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/** The registered steps, in order (the pi tools register off this). */
|
|
348
|
-
stepDefs(): readonly OnboardingStep[] {
|
|
349
|
-
return this.registry;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
private enabled(step: OnboardingStep): boolean {
|
|
353
|
-
return step.enabled ? step.enabled(this.checklist.choices) : true;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
choices(): Readonly<Record<string, unknown>> {
|
|
357
|
-
return this.checklist.choices;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
isComplete(): boolean {
|
|
361
|
-
return completeNow(this.checklist);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** A completed checklist no longer verifies: withdraw the current-completion
|
|
365
|
-
* claim (the post-exit handoff reads it) without touching the history. */
|
|
366
|
-
private reopen(): void {
|
|
367
|
-
if (this.checklist.completed_at === null || this.checklist.reopened_at) return;
|
|
368
|
-
this.checklist.reopened_at = this.adapters.clock().toISOString();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
list(): StepStatusLine[] {
|
|
372
|
-
return this.registry.map((step) => {
|
|
373
|
-
const rec = this.checklist.steps[step.id];
|
|
374
|
-
return {
|
|
375
|
-
id: step.id,
|
|
376
|
-
title: step.title,
|
|
377
|
-
explain: step.explain,
|
|
378
|
-
status: this.enabled(step) ? (rec?.status ?? "pending") : "disabled",
|
|
379
|
-
note: rec?.note ?? null,
|
|
380
|
-
...(step.optional ? { optional: true } : {}),
|
|
381
|
-
};
|
|
382
|
-
});
|
|
146
|
+
super(steps, adapters, { file: checklistPath(adapters.paths.stateDir) });
|
|
383
147
|
}
|
|
384
148
|
|
|
385
149
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*/
|
|
391
|
-
async reconcile(): Promise<ReconcileReport> {
|
|
392
|
-
const report: ReconcileReport = { verified: [], demoted: [] };
|
|
393
|
-
for (const step of this.registry) {
|
|
394
|
-
if (!this.enabled(step)) continue;
|
|
395
|
-
const rec = this.checklist.steps[step.id];
|
|
396
|
-
if (!rec || rec.status !== "done") {
|
|
397
|
-
this.reopen();
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
const outcome = await this.safeVerify(step);
|
|
401
|
-
if (outcome.ok) {
|
|
402
|
-
rec.verified_at = this.adapters.clock().toISOString();
|
|
403
|
-
report.verified.push(step.id);
|
|
404
|
-
} else {
|
|
405
|
-
const fails = failures(outcome.checks);
|
|
406
|
-
rec.status = "pending";
|
|
407
|
-
rec.note = `re-verification failed: ${fails.join("; ") || "unknown"}`;
|
|
408
|
-
this.reopen();
|
|
409
|
-
report.demoted.push({ id: step.id, failures: fails });
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
this.save();
|
|
413
|
-
return report;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** Run one step, optionally with the operator's answer (`input`, the pi
|
|
417
|
-
* tool's params). Unknown/disabled ids are refusals-as-answers, never
|
|
418
|
-
* throws (the caller is an LLM tool belt). Persists after the step. */
|
|
419
|
-
async runStep(id: string, input: Record<string, unknown> = {}): Promise<StepRunResult> {
|
|
420
|
-
const step = this.registry.find((s) => s.id === id);
|
|
421
|
-
if (!step) {
|
|
422
|
-
return {
|
|
423
|
-
id,
|
|
424
|
-
ok: false,
|
|
425
|
-
checks: [],
|
|
426
|
-
failures: [`unknown step "${id}" — known: ${this.registry.map((s) => s.id).join(", ")}`],
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
if (!this.enabled(step)) {
|
|
430
|
-
return {
|
|
431
|
-
id,
|
|
432
|
-
ok: false,
|
|
433
|
-
checks: [],
|
|
434
|
-
failures: [`step "${id}" is disabled by the recorded choices`],
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
const rec = this.record(id);
|
|
438
|
-
rec.status = "pending";
|
|
439
|
-
this.reopen();
|
|
440
|
-
this.save();
|
|
441
|
-
let report: StepReport;
|
|
442
|
-
try {
|
|
443
|
-
report = await step.run(this.ctxFor(id, input));
|
|
444
|
-
} catch (err) {
|
|
445
|
-
report = {
|
|
446
|
-
ok: false,
|
|
447
|
-
checks: [{ name: id, ok: false, detail: `step threw: ${String(err)}` }],
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
if (report.data) rec.data = { ...rec.data, ...report.data };
|
|
451
|
-
if (report.ok) {
|
|
452
|
-
rec.status = "done";
|
|
453
|
-
rec.completed_at = this.adapters.clock().toISOString();
|
|
454
|
-
rec.verified_at = null;
|
|
455
|
-
rec.note = null;
|
|
456
|
-
// A run acts on the installation (choices, config, deploys) after the
|
|
457
|
-
// full verification that backed the completion claim: only the next
|
|
458
|
-
// green finish() may make that claim again.
|
|
459
|
-
this.reopen();
|
|
460
|
-
} else {
|
|
461
|
-
rec.status = "pending";
|
|
462
|
-
rec.note = `last run failed: ${failures(report.checks).join("; ") || "unknown"}`;
|
|
463
|
-
this.reopen();
|
|
464
|
-
}
|
|
465
|
-
this.save();
|
|
466
|
-
return { id, ok: report.ok, checks: report.checks, failures: failures(report.checks) };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
private async safeVerify(step: OnboardingStep): Promise<StepReport> {
|
|
470
|
-
try {
|
|
471
|
-
return await step.verify(this.ctxFor(step.id));
|
|
472
|
-
} catch (err) {
|
|
473
|
-
return {
|
|
474
|
-
ok: false,
|
|
475
|
-
checks: [{ name: step.id, ok: false, detail: `verify threw: ${String(err)}` }],
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Verify every enabled step — done or not. A pending step whose verify()
|
|
482
|
-
* holds is promoted to done (its outcome exists in reality, which is the
|
|
483
|
-
* only truth the checklist mirrors); a done step whose verify() fails
|
|
484
|
-
* reports red here (finish refuses) but demotion is reconcile()'s job.
|
|
485
|
-
*/
|
|
486
|
-
async verifyAll(): Promise<VerifyAllResult> {
|
|
487
|
-
const results: VerifyAllResult["results"] = [];
|
|
488
|
-
for (const step of this.registry) {
|
|
489
|
-
if (!this.enabled(step)) continue;
|
|
490
|
-
const outcome = await this.safeVerify(step);
|
|
491
|
-
const rec = this.record(step.id);
|
|
492
|
-
if (outcome.ok) {
|
|
493
|
-
rec.verified_at = this.adapters.clock().toISOString();
|
|
494
|
-
if (rec.status !== "done") {
|
|
495
|
-
rec.status = "done";
|
|
496
|
-
rec.completed_at = rec.completed_at ?? rec.verified_at;
|
|
497
|
-
rec.note = "verified green without a recorded run";
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
results.push({
|
|
501
|
-
id: step.id,
|
|
502
|
-
ok: outcome.ok,
|
|
503
|
-
failures: failures(outcome.checks),
|
|
504
|
-
...(step.optional ? { optional: true } : {}),
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
this.save();
|
|
508
|
-
return { ok: results.every((r) => r.ok || r.optional === true), results };
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* The finish tool's engine half: re-run the full verification checklist.
|
|
513
|
-
* Anything red ⇒ refuse with the failures (the caller returns them as the
|
|
514
|
-
* tool result and does not exit). All green ⇒ mark the checklist complete
|
|
515
|
-
* and write the first-run marker — once: a marker already on disk is left
|
|
150
|
+
* The finish tool's engine half: the checklist's full verification. Anything
|
|
151
|
+
* red ⇒ refuse with the failures (the caller returns them as the tool
|
|
152
|
+
* result and does not exit). All green ⇒ the checklist is complete, and the
|
|
153
|
+
* first-run marker is written — once: a marker already on disk is left
|
|
516
154
|
* untouched, so the completed_at the companion greets on never moves.
|
|
517
155
|
*/
|
|
518
|
-
async finish(): Promise<FinishResult> {
|
|
519
|
-
const
|
|
520
|
-
if (!
|
|
521
|
-
this.reopen();
|
|
522
|
-
this.save();
|
|
523
|
-
return { ok: false, results: all.results };
|
|
524
|
-
}
|
|
525
|
-
const completedAt = this.checklist.completed_at ?? this.adapters.clock().toISOString();
|
|
526
|
-
this.checklist.completed_at = completedAt;
|
|
527
|
-
this.checklist.reopened_at = null;
|
|
528
|
-
this.save();
|
|
156
|
+
override async finish(): Promise<FinishResult> {
|
|
157
|
+
const done = await super.finish();
|
|
158
|
+
if (!done.ok) return done;
|
|
529
159
|
const marker = firstRunMarkerPath(this.adapters.paths.stateDir);
|
|
530
160
|
let markerWritten = false;
|
|
531
161
|
if (!existsSync(marker)) {
|
|
532
|
-
writeAtomic(marker, `${JSON.stringify({ completed_at:
|
|
162
|
+
writeAtomic(marker, `${JSON.stringify({ completed_at: done.completed_at })}\n`);
|
|
533
163
|
markerWritten = true;
|
|
534
164
|
}
|
|
535
|
-
return { ok: true, completed_at:
|
|
165
|
+
return { ok: true, completed_at: done.completed_at, marker_written: markerWritten };
|
|
536
166
|
}
|
|
537
167
|
}
|
|
@@ -80,13 +80,14 @@ import {
|
|
|
80
80
|
embedderInputsCheck,
|
|
81
81
|
localEmbedderEnvEntries,
|
|
82
82
|
} from "./embeddings.js";
|
|
83
|
+
import { CHOICE_KEYS } from "../choices.js";
|
|
83
84
|
import { readEnvKey, readTomlKey, setEnvKey } from "./ingress.js";
|
|
84
85
|
import { UID_CHOICE_KEY } from "./preflight.js";
|
|
85
86
|
import { topologyOf } from "./topology.js";
|
|
86
87
|
|
|
87
88
|
export const BRINGUP_STEP_ID = "bringup";
|
|
88
89
|
/** Recorded timestamp of this step's successful bring-up. */
|
|
89
|
-
export const BROUGHT_UP_CHOICE_KEY =
|
|
90
|
+
export const BROUGHT_UP_CHOICE_KEY = CHOICE_KEYS.broughtUpAt;
|
|
90
91
|
|
|
91
92
|
/**
|
|
92
93
|
* The note, in the operator's words, emitted before the pull begins.
|
|
@@ -442,7 +443,7 @@ export function bringUpStep(opts: BringUpStepOptions = {}): OnboardingStep {
|
|
|
442
443
|
}),
|
|
443
444
|
);
|
|
444
445
|
const ok = checks.every((c) => c.ok);
|
|
445
|
-
if (ok) ctx.recordChoice(
|
|
446
|
+
if (ok) ctx.recordChoice(CHOICE_KEYS.broughtUpAt, ctx.adapters.clock().toISOString());
|
|
446
447
|
return { ok, checks, data: { profile, up_at: ctx.adapters.clock().toISOString() } };
|
|
447
448
|
},
|
|
448
449
|
|
|
@@ -121,9 +121,12 @@ function escapeRe(s: string): string {
|
|
|
121
121
|
* marker in place — the EXPLANATION after it is the part worth keeping. */
|
|
122
122
|
export function rewriteFillMarker(comment: string): string {
|
|
123
123
|
if (!/←\s*FILL/.test(comment)) return comment;
|
|
124
|
-
return comment.replace(/←\s*FILL(\s*\(\d+\))?\s*:?\s*/, "filled by qcr
|
|
124
|
+
return comment.replace(/←\s*FILL(\s*\(\d+\))?\s*:?\s*/, "filled by qcr: ");
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/** A line without the CR of its CRLF ending. */
|
|
128
|
+
const bare = (line: string): string => line.replace(/\r$/, "");
|
|
129
|
+
|
|
127
130
|
function sectionAt(lines: string[]): string[] {
|
|
128
131
|
const out: string[] = [];
|
|
129
132
|
let cur = "";
|
|
@@ -135,10 +138,12 @@ function sectionAt(lines: string[]): string[] {
|
|
|
135
138
|
if (splitTail(continuation) !== null) continuation = null;
|
|
136
139
|
continue;
|
|
137
140
|
}
|
|
138
|
-
const m = HEADER_RE.exec(line);
|
|
141
|
+
const m = HEADER_RE.exec(bare(line));
|
|
139
142
|
if (m) cur = m[1]!.trim();
|
|
140
143
|
out.push(cur);
|
|
141
|
-
const assignment = /^\s*(?:"(?:[^"\\]|\\.)*"|'[^']*'|[A-Za-z0-9_-]+)\s*=\s*(.*)$/.exec(
|
|
144
|
+
const assignment = /^\s*(?:"(?:[^"\\]|\\.)*"|'[^']*'|[A-Za-z0-9_-]+)\s*=\s*(.*)$/.exec(
|
|
145
|
+
bare(line),
|
|
146
|
+
);
|
|
142
147
|
if (assignment && splitTail(assignment[1]!) === null) continuation = assignment[1]!;
|
|
143
148
|
}
|
|
144
149
|
return out;
|
|
@@ -149,9 +154,18 @@ function sectionAt(lines: string[]): string[] {
|
|
|
149
154
|
* key order. An existing assignment has only its VALUE replaced (its trailing
|
|
150
155
|
* comment survives, with a spent `← FILL` marker rewritten); a commented-out
|
|
151
156
|
* assignment is uncommented in place; a key absent from its section is appended
|
|
152
|
-
* to the end of that section; a missing section is appended at EOF.
|
|
157
|
+
* to the end of that section; a missing section is appended at EOF. Line
|
|
158
|
+
* endings survive too: a CRLF file is patched as LF and given its CRs back, so
|
|
159
|
+
* an edited or added line ends the way every other line does, and in a file of
|
|
160
|
+
* mixed endings the edited line keeps its own.
|
|
153
161
|
*/
|
|
154
162
|
export function patchToml(text: string, edits: readonly TomlEdit[]): string {
|
|
163
|
+
if (text.includes("\r\n") && !/(^|[^\r])\n/.test(text))
|
|
164
|
+
return patchLines(text.replace(/\r\n/g, "\n"), edits).replace(/\n/g, "\r\n");
|
|
165
|
+
return patchLines(text, edits);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function patchLines(text: string, edits: readonly TomlEdit[]): string {
|
|
155
169
|
const lines = text.split("\n");
|
|
156
170
|
for (const edit of edits) {
|
|
157
171
|
const sections = sectionAt(lines);
|
|
@@ -162,7 +176,7 @@ export function patchToml(text: string, edits: readonly TomlEdit[]): string {
|
|
|
162
176
|
for (const re of [live, commented]) {
|
|
163
177
|
for (let i = 0; i < lines.length && !applied; i++) {
|
|
164
178
|
if (sections[i] !== edit.section) continue;
|
|
165
|
-
const m = re.exec(lines[i]!);
|
|
179
|
+
const m = re.exec(bare(lines[i]!));
|
|
166
180
|
if (!m) continue;
|
|
167
181
|
try {
|
|
168
182
|
if (!Object.hasOwn(parseToml(`${m[2]} = 0`), edit.key)) continue;
|
|
@@ -171,11 +185,13 @@ export function patchToml(text: string, edits: readonly TomlEdit[]): string {
|
|
|
171
185
|
}
|
|
172
186
|
let end = i;
|
|
173
187
|
let tail = m[4]!;
|
|
174
|
-
while (splitTail(tail) === null && end + 1 < lines.length)
|
|
188
|
+
while (splitTail(tail) === null && end + 1 < lines.length)
|
|
189
|
+
tail += "\n" + bare(lines[++end]!);
|
|
175
190
|
const parts = splitTail(tail);
|
|
176
191
|
if (!parts) throw new Error(`cannot patch unterminated TOML assignment ${edit.key}`);
|
|
177
192
|
const comment = parts.comment ? `${parts.gap}${rewriteFillMarker(parts.comment)}` : "";
|
|
178
|
-
lines
|
|
193
|
+
const cr = lines[end]!.endsWith("\r") ? "\r" : "";
|
|
194
|
+
lines.splice(i, end - i + 1, `${m[1]}${m[2]}${m[3]}${edit.value}${comment}${cr}`);
|
|
179
195
|
applied = true;
|
|
180
196
|
}
|
|
181
197
|
if (applied) break;
|