@quickchatai/quickreview 1.1.0 → 1.2.0
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 +35 -19
- package/config.example.toml +41 -9
- package/dist/extensions/cli/login-route.js +5 -0
- package/dist/extensions/cli/login-route.js.map +1 -0
- package/dist/extensions/login-route.js +180 -0
- package/dist/extensions/login-route.js.map +1 -0
- package/dist/extensions/onboarding.js +29 -6
- package/dist/extensions/onboarding.js.map +1 -1
- package/dist/src/ciretry.js +2 -1
- package/dist/src/ciretry.js.map +1 -1
- package/dist/src/cli.js +43 -12
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliops.js +3 -2
- package/dist/src/cliops.js.map +1 -1
- package/dist/src/commands.js +25 -2
- package/dist/src/commands.js.map +1 -1
- package/dist/src/config.js +16 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +24 -8
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/debugroundpi.js +5 -1
- package/dist/src/debugroundpi.js.map +1 -1
- package/dist/src/dispatch.js +5 -4
- package/dist/src/dispatch.js.map +1 -1
- package/dist/src/engine/debugpi.js +17 -5
- package/dist/src/engine/debugpi.js.map +1 -1
- package/dist/src/execboundary.js +10 -3
- package/dist/src/execboundary.js.map +1 -1
- package/dist/src/github.js +51 -16
- package/dist/src/github.js.map +1 -1
- package/dist/src/githubtoken.js +346 -0
- package/dist/src/githubtoken.js.map +1 -0
- package/dist/src/loginhint.js +8 -3
- package/dist/src/loginhint.js.map +1 -1
- package/dist/src/loginrefresh/pirefresh.js +3 -0
- package/dist/src/loginrefresh/pirefresh.js.map +1 -1
- package/dist/src/npmverbs.js +4 -2
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +29 -1
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +8 -23
- package/dist/src/onboard/engine.js.map +1 -1
- package/dist/src/onboard/launch.js +12 -20
- package/dist/src/onboard/launch.js.map +1 -1
- package/dist/src/onboard/pipin.js +22 -0
- package/dist/src/onboard/pipin.js.map +1 -0
- package/dist/src/onboard/stepforward.js +41 -0
- package/dist/src/onboard/stepforward.js.map +1 -0
- package/dist/src/onboard/steps/appslug.js +4 -1
- package/dist/src/onboard/steps/appslug.js.map +1 -1
- package/dist/src/onboard/steps/compose.js +29 -0
- package/dist/src/onboard/steps/compose.js.map +1 -1
- package/dist/src/onboard/steps/configfill.js +102 -32
- package/dist/src/onboard/steps/configfill.js.map +1 -1
- package/dist/src/onboard/steps/githubaccount.js +595 -0
- package/dist/src/onboard/steps/githubaccount.js.map +1 -0
- package/dist/src/onboard/steps/githubapp.js +65 -7
- package/dist/src/onboard/steps/githubapp.js.map +1 -1
- package/dist/src/onboard/steps/golive.js +2 -0
- package/dist/src/onboard/steps/golive.js.map +1 -1
- package/dist/src/onboard/steps/index.js +11 -0
- package/dist/src/onboard/steps/index.js.map +1 -1
- package/dist/src/onboard/steps/ingress.js +80 -25
- package/dist/src/onboard/steps/ingress.js.map +1 -1
- package/dist/src/onboard/steps/llmauth.js +103 -30
- package/dist/src/onboard/steps/llmauth.js.map +1 -1
- package/dist/src/onboard/steps/memory.js +53 -1
- package/dist/src/onboard/steps/memory.js.map +1 -1
- package/dist/src/onboard/steps/proof.js +366 -56
- package/dist/src/onboard/steps/proof.js.map +1 -1
- package/dist/src/onboard/steps/topology.js +6 -6
- package/dist/src/onboard/steps/topology.js.map +1 -1
- package/dist/src/onboard/steps/transport.js +9 -1
- package/dist/src/onboard/steps/transport.js.map +1 -1
- package/dist/src/onboard/steps/tuiroute.js +102 -0
- package/dist/src/onboard/steps/tuiroute.js.map +1 -0
- package/dist/src/onboard/steps/verifyup.js +17 -1
- package/dist/src/onboard/steps/verifyup.js.map +1 -1
- package/dist/src/onboard/toolnames.js +30 -0
- package/dist/src/onboard/toolnames.js.map +1 -0
- package/dist/src/operatorwrite.js +23 -0
- package/dist/src/operatorwrite.js.map +1 -0
- package/dist/src/outcomes.js +4 -1
- package/dist/src/outcomes.js.map +1 -1
- package/dist/src/posting-journal.js +2 -1
- package/dist/src/posting-journal.js.map +1 -1
- package/dist/src/posting-outbox.js +6 -2
- package/dist/src/posting-outbox.js.map +1 -1
- package/dist/src/posting-receipt.js +14 -1
- package/dist/src/posting-receipt.js.map +1 -1
- package/dist/src/recheck.js +2 -1
- package/dist/src/recheck.js.map +1 -1
- package/dist/src/reconcile.js +22 -2
- package/dist/src/reconcile.js.map +1 -1
- package/dist/src/reviewhistory.js +32 -6
- package/dist/src/reviewhistory.js.map +1 -1
- package/dist/src/reviewnow.js +45 -0
- package/dist/src/reviewnow.js.map +1 -0
- package/dist/src/state.js.map +1 -1
- package/dist/src/statemachine.js +4 -1
- package/dist/src/statemachine.js.map +1 -1
- package/dist/src/takeoverpi.js +13 -0
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/app.js +49 -0
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/board.js +88 -12
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/cmdbar.js +4 -1
- package/dist/src/tui/cmdbar.js.map +1 -1
- package/dist/src/tui/companion.js +74 -10
- package/dist/src/tui/companion.js.map +1 -1
- package/dist/src/tui/connect.js +7 -1
- package/dist/src/tui/connect.js.map +1 -1
- package/dist/src/tui/filters.js +5 -2
- package/dist/src/tui/filters.js.map +1 -1
- package/dist/src/tui/ghread.js +61 -5
- package/dist/src/tui/ghread.js.map +1 -1
- package/dist/src/tui/index.js +106 -20
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/memforward.js +39 -2
- package/dist/src/tui/memforward.js.map +1 -1
- package/dist/src/tui/model.js +25 -0
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/prpause.js +7 -0
- package/dist/src/tui/prpause.js.map +1 -1
- package/dist/src/tui/remote.js +7 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +143 -0
- package/dist/src/tui/route.js.map +1 -0
- package/dist/src/tui/setupstate.js +29 -0
- package/dist/src/tui/setupstate.js.map +1 -0
- package/dist/src/tui/sshtriage.js +102 -5
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/tui/theme.js +2 -1
- package/dist/src/tui/theme.js.map +1 -1
- package/dist/src/tui/unwedge.js +2 -12
- package/dist/src/tui/unwedge.js.map +1 -1
- package/dist/src/verdicts.js +17 -5
- package/dist/src/verdicts.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/webhook.js +12 -1
- package/dist/src/webhook.js.map +1 -1
- package/dist/src/wire.js +28 -6
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerworld.js +7 -1
- package/dist/src/workerworld.js.map +1 -1
- package/docker/companion/entrypoint.sh +20 -1
- package/docker/entrypoint.sh +17 -0
- package/extensions/cli/login-route.ts +10 -0
- package/extensions/login-route.ts +220 -0
- package/extensions/onboarding.ts +35 -6
- package/package.json +2 -2
- package/prompts/onboarding.md +57 -18
- package/scripts/mint-token.sh +25 -4
- package/src/ciretry.ts +2 -1
- package/src/cli.ts +49 -14
- package/src/cliops.ts +3 -2
- package/src/commands.ts +28 -2
- package/src/config.ts +24 -4
- package/src/daemon.ts +32 -7
- package/src/debugroundpi.ts +11 -1
- package/src/dispatch.ts +5 -4
- package/src/engine/debugpi.ts +17 -5
- package/src/execboundary.ts +16 -4
- package/src/github.ts +57 -15
- package/src/githubtoken.ts +435 -0
- package/src/loginhint.ts +8 -3
- package/src/loginrefresh/pirefresh.ts +3 -0
- package/src/npmverbs.ts +4 -2
- package/src/onboard/choices.ts +34 -1
- package/src/onboard/engine.ts +22 -3
- package/src/onboard/launch.ts +17 -29
- package/src/onboard/pipin.ts +31 -0
- package/src/onboard/stepforward.ts +61 -0
- package/src/onboard/steps/appslug.ts +4 -1
- package/src/onboard/steps/compose.ts +31 -0
- package/src/onboard/steps/configfill.ts +121 -33
- package/src/onboard/steps/githubaccount.ts +716 -0
- package/src/onboard/steps/githubapp.ts +92 -8
- package/src/onboard/steps/golive.ts +2 -0
- package/src/onboard/steps/index.ts +11 -0
- package/src/onboard/steps/ingress.ts +98 -31
- package/src/onboard/steps/llmauth.ts +114 -29
- package/src/onboard/steps/memory.ts +58 -1
- package/src/onboard/steps/proof.ts +473 -55
- package/src/onboard/steps/topology.ts +7 -6
- package/src/onboard/steps/transport.ts +15 -0
- package/src/onboard/steps/tuiroute.ts +119 -0
- package/src/onboard/steps/verifyup.ts +20 -1
- package/src/onboard/toolnames.ts +34 -0
- package/src/operatorwrite.ts +25 -0
- package/src/outcomes.ts +8 -1
- package/src/posting-journal.ts +6 -1
- package/src/posting-outbox.ts +10 -2
- package/src/posting-receipt.ts +17 -0
- package/src/recheck.ts +2 -1
- package/src/reconcile.ts +29 -3
- package/src/reviewhistory.ts +20 -6
- package/src/reviewnow.ts +76 -0
- package/src/state.ts +18 -0
- package/src/statemachine.ts +4 -1
- package/src/takeoverpi.ts +13 -0
- package/src/tui/app.ts +47 -0
- package/src/tui/board.ts +111 -14
- package/src/tui/cmdbar.ts +4 -1
- package/src/tui/companion.ts +78 -10
- package/src/tui/connect.ts +7 -1
- package/src/tui/filters.ts +7 -3
- package/src/tui/ghread.ts +74 -7
- package/src/tui/index.ts +114 -23
- package/src/tui/memforward.ts +52 -3
- package/src/tui/model.ts +36 -2
- package/src/tui/prpause.ts +11 -2
- package/src/tui/remote.ts +9 -1
- package/src/tui/route.ts +173 -0
- package/src/tui/setupstate.ts +52 -0
- package/src/tui/sshtriage.ts +127 -6
- package/src/tui/theme.ts +2 -1
- package/src/tui/unwedge.ts +2 -14
- package/src/verdicts.ts +23 -5
- package/src/version.ts +1 -1
- package/src/webhook.ts +10 -1
- package/src/wire.ts +36 -7
- package/src/workerworld.ts +7 -1
package/src/onboard/choices.ts
CHANGED
|
@@ -41,6 +41,12 @@ export type IngressChoice = (typeof INGRESS_CHOICES)[number];
|
|
|
41
41
|
export const AUTH_ROUTES = ["oauth", "api"] as const;
|
|
42
42
|
export type AuthRoute = (typeof AUTH_ROUTES)[number];
|
|
43
43
|
|
|
44
|
+
/** How the daemon authenticates to GitHub (`config.toml [github] auth`):
|
|
45
|
+
* "user" is the person's own account (polling only, no App), "app" a GitHub
|
|
46
|
+
* App. Answered as the github_account step's input; absent ⇒ unanswered. */
|
|
47
|
+
export const GITHUB_AUTH_CHOICES = ["user", "app"] as const;
|
|
48
|
+
export type GithubAuth = (typeof GITHUB_AUTH_CHOICES)[number];
|
|
49
|
+
|
|
44
50
|
/** "owner/name" — the reviewed repo as the interview records it and the App
|
|
45
51
|
* step parses it: no URL, no trailing `.git`. */
|
|
46
52
|
export const REPO_SLUG_RE = /^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/;
|
|
@@ -71,6 +77,20 @@ export const CHOICE_KEYS = {
|
|
|
71
77
|
/** `AuthRoute`. Written by the LLM step; read by the interview's shape line. */
|
|
72
78
|
authRoute: "llm_auth_route",
|
|
73
79
|
|
|
80
|
+
/** `GithubAuth`. Written by the github_account step (the operator's answer,
|
|
81
|
+
* defaulted to "user" where `gh auth token` works); read by everything that
|
|
82
|
+
* differs between the own-account route and the App route — which of
|
|
83
|
+
* github_account / github_app / app_slug_reconcile is enabled, the
|
|
84
|
+
* interview's App questions, the ingress shapes on offer, verification. */
|
|
85
|
+
githubAuth: "github_auth",
|
|
86
|
+
/** The person's GitHub login, from `GET /user` with their own token (own-
|
|
87
|
+
* account route). The interview's owner-login answer and `[github]
|
|
88
|
+
* bot_login` both come from it. */
|
|
89
|
+
accountLogin: "github_account_login",
|
|
90
|
+
/** "owner/name" the person picked from the repos their account reaches
|
|
91
|
+
* (own-account route). The interview reads it as its repo answer. */
|
|
92
|
+
accountRepo: "github_account_repo",
|
|
93
|
+
|
|
74
94
|
/** "owner/name" of the reviewed repo. Written by the interview; read by the
|
|
75
95
|
* GitHub App step and the proof round. */
|
|
76
96
|
repo: "reviewed_repo",
|
|
@@ -144,6 +164,9 @@ export type ChoiceKey = (typeof CHOICE_KEYS)[ChoiceName];
|
|
|
144
164
|
*/
|
|
145
165
|
export const CHOICE_WRITERS: Record<ChoiceName, readonly string[]> = {
|
|
146
166
|
uid: ["preflight"],
|
|
167
|
+
githubAuth: ["github_account"],
|
|
168
|
+
accountLogin: ["github_account"],
|
|
169
|
+
accountRepo: ["github_account"],
|
|
147
170
|
topology: ["topology"],
|
|
148
171
|
sshAlias: ["topology"],
|
|
149
172
|
sshUser: ["ssh_preflight"],
|
|
@@ -158,7 +181,7 @@ export const CHOICE_WRITERS: Record<ChoiceName, readonly string[]> = {
|
|
|
158
181
|
dryRun: ["config_fill"],
|
|
159
182
|
labels: ["config_fill"],
|
|
160
183
|
appSlug: ["config_fill", "github_app", "app_slug_reconcile"],
|
|
161
|
-
botLogin: ["github_app", "app_slug_reconcile"],
|
|
184
|
+
botLogin: ["github_account", "github_app", "app_slug_reconcile"],
|
|
162
185
|
appId: ["github_app"],
|
|
163
186
|
appTarget: ["github_app"],
|
|
164
187
|
appName: ["github_app"],
|
|
@@ -216,6 +239,16 @@ export function authRouteOf(choices: Readonly<Record<string, unknown>>): AuthRou
|
|
|
216
239
|
return memberOf(AUTH_ROUTES, raw(choices, CHOICE_KEYS.authRoute));
|
|
217
240
|
}
|
|
218
241
|
|
|
242
|
+
/** The recorded GitHub auth route, or null while unanswered. */
|
|
243
|
+
export function githubAuthOf(choices: Readonly<Record<string, unknown>>): GithubAuth | null {
|
|
244
|
+
return memberOf(GITHUB_AUTH_CHOICES, raw(choices, CHOICE_KEYS.githubAuth));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Is the own-account route recorded? (Unanswered is NOT user mode.) */
|
|
248
|
+
export function userAuthRecorded(choices: Readonly<Record<string, unknown>>): boolean {
|
|
249
|
+
return githubAuthOf(choices) === "user";
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
/** The recorded ingress shape, or null when the ingress step has not run. */
|
|
220
253
|
export function ingressChoiceOf(choices: Readonly<Record<string, unknown>>): IngressChoice | null {
|
|
221
254
|
return memberOf(INGRESS_CHOICES, raw(choices, CHOICE_KEYS.ingress));
|
package/src/onboard/engine.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// The finish path writes the first-run marker state/companion/first-run.json
|
|
20
20
|
// ({"completed_at": "<iso>"}) exactly once — the contract the companion's
|
|
21
21
|
// first-run greeting consumes. Keep the path and shape stable.
|
|
22
|
+
import type { StepForward, StepForwardRequest } from "./stepforward.js";
|
|
22
23
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
23
24
|
import { basename, dirname, join, resolve } from "node:path";
|
|
24
25
|
|
|
@@ -60,6 +61,11 @@ export interface OnboardAdapters {
|
|
|
60
61
|
* so no step builds a config path itself. */
|
|
61
62
|
paths: { stateDir: string; configPath: string };
|
|
62
63
|
log?: (line: string) => void;
|
|
64
|
+
/** Opens the local memory forward for one step call (src/onboard/stepforward.ts):
|
|
65
|
+
* the compose route's memory engine is reachable from this machine only
|
|
66
|
+
* through it. The real assembly sets it; absent (every test), no forward is
|
|
67
|
+
* opened and the step sees whatever already listens. */
|
|
68
|
+
memoryForward?: (req: StepForwardRequest) => Promise<StepForward | null>;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
// ---- step vocabulary --------------------------------------------------------
|
|
@@ -116,6 +122,10 @@ export interface OnboardingStep {
|
|
|
116
122
|
* input, which is the common case. */
|
|
117
123
|
parameters?: unknown;
|
|
118
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;
|
|
119
129
|
run(ctx: StepContext): Promise<StepReport>;
|
|
120
130
|
verify(ctx: StepContext): Promise<StepReport>;
|
|
121
131
|
}
|
|
@@ -242,6 +252,8 @@ export interface StepStatusLine {
|
|
|
242
252
|
explain: string;
|
|
243
253
|
status: "pending" | "done" | "disabled";
|
|
244
254
|
note: string | null;
|
|
255
|
+
/** The step's `optional` flag: setup completes without it. */
|
|
256
|
+
optional?: boolean;
|
|
245
257
|
}
|
|
246
258
|
|
|
247
259
|
export interface ReconcileReport {
|
|
@@ -260,7 +272,8 @@ export interface StepRunResult {
|
|
|
260
272
|
|
|
261
273
|
export interface VerifyAllResult {
|
|
262
274
|
ok: boolean;
|
|
263
|
-
results
|
|
275
|
+
/** `optional` results are reported but never hold `ok` false. */
|
|
276
|
+
results: Array<{ id: string; ok: boolean; failures: string[]; optional?: boolean }>;
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
export type FinishResult =
|
|
@@ -364,6 +377,7 @@ export class OnboardingEngine {
|
|
|
364
377
|
explain: step.explain,
|
|
365
378
|
status: this.enabled(step) ? (rec?.status ?? "pending") : "disabled",
|
|
366
379
|
note: rec?.note ?? null,
|
|
380
|
+
...(step.optional ? { optional: true } : {}),
|
|
367
381
|
};
|
|
368
382
|
});
|
|
369
383
|
}
|
|
@@ -483,10 +497,15 @@ export class OnboardingEngine {
|
|
|
483
497
|
rec.note = "verified green without a recorded run";
|
|
484
498
|
}
|
|
485
499
|
}
|
|
486
|
-
results.push({
|
|
500
|
+
results.push({
|
|
501
|
+
id: step.id,
|
|
502
|
+
ok: outcome.ok,
|
|
503
|
+
failures: failures(outcome.checks),
|
|
504
|
+
...(step.optional ? { optional: true } : {}),
|
|
505
|
+
});
|
|
487
506
|
}
|
|
488
507
|
this.save();
|
|
489
|
-
return { ok: results.every((r) => r.ok), results };
|
|
508
|
+
return { ok: results.every((r) => r.ok || r.optional === true), results };
|
|
490
509
|
}
|
|
491
510
|
|
|
492
511
|
/**
|
package/src/onboard/launch.ts
CHANGED
|
@@ -7,35 +7,14 @@
|
|
|
7
7
|
// Everything effectful is injected (spawn, completion read, log), so tests
|
|
8
8
|
// drive the whole flow with fakes; src/cli.ts cmdOnboard is the one real
|
|
9
9
|
// assembly.
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
10
|
import { join } from "node:path";
|
|
11
|
+
import { defaultSteps } from "./steps/index.js";
|
|
12
|
+
import { onboardingToolAllowlist } from "./toolnames.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** The pin from package.json's "qcrPi" block — the mirror that exists on a bare
|
|
19
|
-
* public clone, where config.toml does not (the wizard writes it later).
|
|
20
|
-
* config.toml [pi] stays the preferred source when it loads (cmdOnboard tries
|
|
21
|
-
* it first); both mirror the same pinned version by rule 7's discipline. */
|
|
22
|
-
export function piPinFromPackageJson(
|
|
23
|
-
root: string,
|
|
24
|
-
readFile: (p: string) => string = (p) => readFileSync(p, "utf8"),
|
|
25
|
-
): PiPin | null {
|
|
26
|
-
try {
|
|
27
|
-
const pkg = JSON.parse(readFile(join(root, "package.json"))) as {
|
|
28
|
-
qcrPi?: { package?: unknown; version?: unknown };
|
|
29
|
-
};
|
|
30
|
-
const pi = pkg.qcrPi;
|
|
31
|
-
if (pi && typeof pi.package === "string" && typeof pi.version === "string") {
|
|
32
|
-
return { package: pi.package, version: pi.version };
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// fall through — the caller reports the missing pin
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
14
|
+
// The pin reader lives in a leaf module: src/version.ts (on the daemon's import
|
|
15
|
+
// graph) needs it, and this module imports the whole step registry.
|
|
16
|
+
export { piPinFromPackageJson, type PiPin } from "./pipin.js";
|
|
17
|
+
import type { PiPin } from "./pipin.js";
|
|
39
18
|
|
|
40
19
|
/** Dedicated onboarding session store — never the reviewer/companion dirs, so
|
|
41
20
|
* onboarding cannot pollute (or be polluted by) any other pi session store. */
|
|
@@ -55,8 +34,17 @@ export function onboardingSessionDir(root: string): string {
|
|
|
55
34
|
* the intent (no ambient extensions, exactly one onboarding extension, a
|
|
56
35
|
* read-only built-in belt) is the design decision; the exact flag spellings
|
|
57
36
|
* are unverified statically and belong to the named onboarding smoke step.
|
|
37
|
+
*
|
|
38
|
+
* `--tools` is an allowlist over extension tools too (pi 1.4.0; pinned by
|
|
39
|
+
* tests/onboard-tools-sdk.test.ts on the real SDK), so it names every wizard
|
|
40
|
+
* tool, derived from the step registry: a step added there is active here
|
|
41
|
+
* with no second edit. bash, write and edit stay off it.
|
|
58
42
|
*/
|
|
59
|
-
export function onboardingPiArgv(
|
|
43
|
+
export function onboardingPiArgv(
|
|
44
|
+
pin: PiPin,
|
|
45
|
+
root: string,
|
|
46
|
+
stepIds: readonly string[] = defaultSteps().map((s) => s.id),
|
|
47
|
+
): string[] {
|
|
60
48
|
return [
|
|
61
49
|
"npx",
|
|
62
50
|
"-y",
|
|
@@ -70,7 +58,7 @@ export function onboardingPiArgv(pin: PiPin, root: string): string[] {
|
|
|
70
58
|
"--system-prompt",
|
|
71
59
|
join(root, "prompts", "onboarding.md"),
|
|
72
60
|
"--tools",
|
|
73
|
-
"
|
|
61
|
+
onboardingToolAllowlist(stepIds).join(","),
|
|
74
62
|
"--session-dir",
|
|
75
63
|
onboardingSessionDir(root),
|
|
76
64
|
];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The pinned pi version, read from package.json. A leaf: the daemon reaches it
|
|
2
|
+
// through src/version.ts, so it imports nothing from the onboarding wizard.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface PiPin {
|
|
7
|
+
package: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The pin from package.json's "qcrPi" block — the mirror that exists on a bare
|
|
12
|
+
* public clone, where config.toml does not (the wizard writes it later).
|
|
13
|
+
* config.toml [pi] stays the preferred source when it loads (cmdOnboard tries
|
|
14
|
+
* it first); both mirror the same pinned version by rule 7's discipline. */
|
|
15
|
+
export function piPinFromPackageJson(
|
|
16
|
+
root: string,
|
|
17
|
+
readFile: (p: string) => string = (p) => readFileSync(p, "utf8"),
|
|
18
|
+
): PiPin | null {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(readFile(join(root, "package.json"))) as {
|
|
21
|
+
qcrPi?: { package?: unknown; version?: unknown };
|
|
22
|
+
};
|
|
23
|
+
const pi = pkg.qcrPi;
|
|
24
|
+
if (pi && typeof pi.package === "string" && typeof pi.version === "string") {
|
|
25
|
+
return { package: pi.package, version: pi.version };
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// fall through — the caller reports the missing pin
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The memory forward, held open for ONE onboarding step call.
|
|
2
|
+
//
|
|
3
|
+
// On the compose route the memory engine listens only on its own container's
|
|
4
|
+
// loopback; this machine reaches it through the memory forward, the same relay
|
|
5
|
+
// the TUI runs in-process (src/tui/memforward.ts over
|
|
6
|
+
// scripts/companion-memory-forward.mjs). The wizard runs before any TUI, so
|
|
7
|
+
// without this the memory steps' phase B (minting the user keys, the
|
|
8
|
+
// confinement matrix) found nothing listening and sent the operator to a
|
|
9
|
+
// second terminal — and a wizard driven in order never got memory working.
|
|
10
|
+
//
|
|
11
|
+
// A forward that is already listening (`qcr tui`, the systemd unit) is
|
|
12
|
+
// adopted and left alone; one this opens is closed when the step ends, so the
|
|
13
|
+
// wizard leaves nothing running behind it. The real adapter assembly
|
|
14
|
+
// (extensions/onboarding.ts) hands this to the steps; a test's adapters never
|
|
15
|
+
// do, so no test binds a port.
|
|
16
|
+
import { MemoryForward } from "../tui/memforward.js";
|
|
17
|
+
|
|
18
|
+
export interface StepForwardRequest {
|
|
19
|
+
/** Where the step reaches memory (the forward's loopback listener). */
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
/** The checkout holding scripts/. */
|
|
22
|
+
repoRoot: string;
|
|
23
|
+
/** The memory sidecar's container. */
|
|
24
|
+
container: string;
|
|
25
|
+
sudoDocker: boolean;
|
|
26
|
+
log?: (line: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StepForward {
|
|
30
|
+
/** true when this call started the forward (and stop() closes it). */
|
|
31
|
+
started: boolean;
|
|
32
|
+
detail: string;
|
|
33
|
+
stop(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Open (or adopt) the local memory forward; null when none could be made to
|
|
37
|
+
* answer, with nothing left bound. */
|
|
38
|
+
export async function openStepMemoryForward(
|
|
39
|
+
req: StepForwardRequest,
|
|
40
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
41
|
+
): Promise<StepForward | null> {
|
|
42
|
+
const forward = new MemoryForward({
|
|
43
|
+
baseUrl: req.baseUrl,
|
|
44
|
+
repoRoot: req.repoRoot,
|
|
45
|
+
env,
|
|
46
|
+
local: true,
|
|
47
|
+
sudoDocker: req.sudoDocker,
|
|
48
|
+
container: req.container,
|
|
49
|
+
...(req.log ? { log: req.log } : {}),
|
|
50
|
+
});
|
|
51
|
+
const status = await forward.ensure();
|
|
52
|
+
if (status.state === "owned")
|
|
53
|
+
return {
|
|
54
|
+
started: true,
|
|
55
|
+
detail: `opened a memory forward to ${req.container} for this step; it closes when the step ends, and \`qcr tui\` runs its own`,
|
|
56
|
+
stop: () => forward.stop(),
|
|
57
|
+
};
|
|
58
|
+
if (status.state === "adopted") return { started: false, detail: status.detail, stop: () => {} };
|
|
59
|
+
forward.stop();
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
tomlString,
|
|
51
51
|
type TomlEdit,
|
|
52
52
|
} from "./configfill.js";
|
|
53
|
-
import { CHOICE_KEYS, botLoginFor } from "../choices.js";
|
|
53
|
+
import { CHOICE_KEYS, botLoginFor, userAuthRecorded } from "../choices.js";
|
|
54
54
|
import { api, readCredentials } from "./githubapp.js";
|
|
55
55
|
|
|
56
56
|
export const APP_SLUG_STEP_ID = "app_slug_reconcile";
|
|
@@ -254,6 +254,9 @@ export function appSlugStep(opts: AppSlugStepOptions = {}): OnboardingStep {
|
|
|
254
254
|
"slug, so this step asks the App itself who it is and corrects [github] app_slug and bot_login to match. It matters more " +
|
|
255
255
|
"than it looks: bot_login is how the reviewer recognises its own reviews and comments, and a wrong one fails silently.",
|
|
256
256
|
order: 65,
|
|
257
|
+
// There is no App slug on the own-account route: `[github] app_slug` is ""
|
|
258
|
+
// and bot_login is the person's login (the github_account step writes both).
|
|
259
|
+
enabled: (choices) => !userAuthRecorded(choices),
|
|
257
260
|
async run(ctx): Promise<StepReport> {
|
|
258
261
|
return reconcile(ctx, true);
|
|
259
262
|
},
|
|
@@ -74,6 +74,9 @@ export const MEMORY_IMAGE_REPO = "ghcr.io/quickchatai/quickreview-memory";
|
|
|
74
74
|
|
|
75
75
|
export { CONTAINER_STATE_DIR };
|
|
76
76
|
export const CONTAINER_DRYRUN_DIR = `${CONTAINER_STATE_DIR}/dryrun`;
|
|
77
|
+
/** The daemon's command root in the release image (`<root>/commands`, with the
|
|
78
|
+
* root-sealed `operator/` under it and the result files in `results/`). */
|
|
79
|
+
export const CONTAINER_COMMANDS_DIR = "/opt/qcr/commands";
|
|
77
80
|
|
|
78
81
|
/** `<sandbox root>` — the engine's contract is stateDir = `<root>/state`. */
|
|
79
82
|
export function rootOf(adapters: OnboardAdapters): string {
|
|
@@ -157,6 +160,12 @@ export function daemonExecArgs(root: string, argv: readonly string[]): string[]
|
|
|
157
160
|
return composeArgs(root, ["exec", "-T", DAEMON_SERVICE, ...argv]);
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
/** The same exec as uid 0 — the operator seal on `commands/operator/`, which
|
|
164
|
+
* the daemon's own uid (every session's) can read but not write. */
|
|
165
|
+
export function daemonRootExecArgs(root: string, argv: readonly string[]): string[] {
|
|
166
|
+
return composeArgs(root, ["exec", "-T", "-u", "0", DAEMON_SERVICE, ...argv]);
|
|
167
|
+
}
|
|
168
|
+
|
|
160
169
|
export async function daemonExec(
|
|
161
170
|
runner: ProcessRunner,
|
|
162
171
|
root: string,
|
|
@@ -172,9 +181,31 @@ export interface PsReport {
|
|
|
172
181
|
detail: string;
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
/** docker-compose.yml's `name:` — the project label on its containers. */
|
|
185
|
+
export const COMPOSE_PROJECT = "quickreview";
|
|
186
|
+
|
|
187
|
+
/** `docker ps` over the compose project label, one compose-ps-shaped JSON row
|
|
188
|
+
* per running container: asks Docker without reading the compose file. */
|
|
189
|
+
export function projectPsArgs(): string[] {
|
|
190
|
+
return [
|
|
191
|
+
"ps",
|
|
192
|
+
"--filter",
|
|
193
|
+
`label=com.docker.compose.project=${COMPOSE_PROJECT}`,
|
|
194
|
+
"--format",
|
|
195
|
+
'{"Service":"{{.Label "com.docker.compose.service"}}","State":"{{.State}}"}',
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
175
199
|
export async function composePs(runner: ProcessRunner, root: string): Promise<PsReport> {
|
|
176
200
|
const ps = await runner.run("docker", composeArgs(root, ["ps", "--format", "json"]));
|
|
177
201
|
if (ps.code !== 0) {
|
|
202
|
+
// `docker compose ps` interpolates the compose file first, and before the
|
|
203
|
+
// bring-up has written the project .env (QCR_SECRETS_DIR and the rest) it
|
|
204
|
+
// refuses to run at all. That is a question about the file, not the
|
|
205
|
+
// stack: Docker itself still answers which of the project's containers
|
|
206
|
+
// run. Only when Docker cannot answer either is the state unknown.
|
|
207
|
+
const plain = await runner.run("docker", projectPsArgs());
|
|
208
|
+
if (plain.code === 0) return { ok: true, rows: parseComposePs(plain.stdout), detail: "" };
|
|
178
209
|
return {
|
|
179
210
|
ok: false,
|
|
180
211
|
rows: [],
|