@dev.fast/whiteboard 0.2.5-preview.20261013.113 → 0.2.5-preview.20261015.115
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/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
- package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
- package/dist/agent-cli-CRPPmS3l.js +2 -0
- package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
- package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
- package/dist/background-server-Cw7CcfaZ.js +156 -0
- package/dist/background-server-D2Rrr_e2.js +2 -0
- package/dist/build-info.json +1 -1
- package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
- package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
- package/dist/cli.js +3 -2
- package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
- package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
- package/dist/headless-host-Cp37C-v0.js +2 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
- package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
- package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
- package/dist/profile-BSOkA6A-.js +2 -0
- package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
- package/dist/remote-attach-BAt2eZx3.js +137 -0
- package/dist/remote-extensions-2Cxf3hMo.js +2 -0
- package/dist/remote-extensions-BiNx6NQK.js +323 -0
- package/dist/remote-language-server-DQgWUNeV.js +252 -0
- package/dist/remote-uninstall-DLtDG0o7.js +153 -0
- package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
- package/dist/review-app-launcher-D7I4DGoH.js +222 -0
- package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
- package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
- package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
- package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
- package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
- package/dist/server/desktop-host.js +1663 -266
- package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
- package/dist/sharing/index.d.ts +88 -32
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
- package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
- package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
- package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
- package/instructions/authoring.md +16 -12
- package/instructions/file-lenses.md +4 -2
- package/package.json +9 -14
- package/src/agent-connect.ts +507 -0
- package/src/agent-selection.ts +4 -0
- package/src/ask/agents.ts +59 -13
- package/src/ask/pi-mcp.ts +7 -3
- package/src/ask/thread-state.ts +9 -4
- package/src/ask/thread.ts +88 -50
- package/src/ask/threads.ts +53 -7
- package/src/cli-install.ts +3 -1
- package/src/cli-runner.ts +472 -81
- package/src/cli.ts +1 -1
- package/src/connect-agents.ts +285 -0
- package/src/connect-prompts.ts +60 -6
- package/src/desktop-discovery.ts +24 -0
- package/src/pinned-download.ts +118 -0
- package/src/remote-attach.ts +130 -0
- package/src/remote-extensions.ts +359 -0
- package/src/remote-language-server.ts +428 -0
- package/src/remote-toolchains.ts +107 -0
- package/src/remote-uninstall.ts +280 -0
- package/src/review-api/README.md +1 -3
- package/src/review-api/agent-cli.ts +4 -4
- package/src/review-api/agent-client.ts +23 -0
- package/src/review-api/ask-history.ts +31 -16
- package/src/review-api/authoring-tools.ts +1 -1
- package/src/review-api/blocks/index.ts +2 -4
- package/src/review-api/document.ts +1 -4
- package/src/review-api/http.ts +265 -237
- package/src/review-api/local-data.ts +18 -3
- package/src/review-api/onboarding.ts +478 -0
- package/src/review-api/origin.ts +7 -0
- package/src/review-api/recent-folders.ts +173 -0
- package/src/review-api/store.ts +56 -10
- package/src/review-api/workspaces.ts +56 -11
- package/src/review-app-launcher.ts +61 -3
- package/src/review-stack.ts +162 -36
- package/src/server/background-server.ts +250 -0
- package/src/server/desktop-host-shutdown.ts +24 -0
- package/src/server/desktop-host.ts +39 -24
- package/src/server/desktop-server.ts +68 -118
- package/src/server/global-verb-relay.ts +16 -0
- package/src/server/headless-host.ts +128 -3
- package/src/server/hono-http.ts +6 -1
- package/src/server/legacy-tutorial-cleanup.ts +38 -0
- package/src/server/review-gateway-hosts.ts +379 -0
- package/src/server/review-gateway-list.ts +109 -0
- package/src/server/review-gateway-memory.ts +130 -0
- package/src/server/review-gateway-pushes.ts +142 -0
- package/src/server/review-gateway-streams.ts +415 -0
- package/src/server/review-gateway-transport.ts +204 -0
- package/src/server/review-gateway.ts +433 -0
- package/src/server/review-server-core.ts +36 -6
- package/src/server-discovery.ts +11 -0
- package/src/sharing/import.ts +1 -0
- package/src/ui-telemetry-events.ts +5 -1
- package/src/vsix.ts +122 -0
- package/dist/agent-cli-BR_Jw5BL.js +0 -2
- package/dist/headless-host-BLLJ7Dyu.js +0 -95
- package/dist/profile-aMSw1oCM.js +0 -2
- package/dist/server-discovery-ZVi8Ay8J.js +0 -66
- package/src/fixtures/blocks/tutorial.json +0 -14
- package/src/review-api/blocks/tutorial.ts +0 -23
- package/src/server/tutorial-service.ts +0 -217
- package/tutorial/document.json +0 -216
- package/tutorial/lenses.json +0 -10
- package/tutorial/runtime-manifest.json +0 -11
- package/tutorial/sample-service/package.json +0 -9
- package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
- package/tutorial/sample-service/src/app.ts +0 -26
- package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
- package/tutorial/sample-service/src/database/schema.ts +0 -9
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
- package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
- package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
- package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
- package/tutorial/sample-service/src/orders/order-service.ts +0 -35
- package/tutorial/sample-service/src/orders/order.ts +0 -43
- package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
- package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
- package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
- package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
- package/tutorial/sample-service/tsconfig.json +0 -12
package/src/review-stack.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
|
+
import { defaultBranch, git } from "@dev.fast/local-vcs";
|
|
4
5
|
import type {
|
|
5
6
|
ReviewApiSummary,
|
|
6
7
|
ReviewStackLayer,
|
|
@@ -22,14 +23,71 @@ const GitHubStacksSchema = z.array(
|
|
|
22
23
|
|
|
23
24
|
export type RunGitHubApi = (host: string, endpoint: string) => Promise<string>;
|
|
24
25
|
|
|
26
|
+
type Subject = Pick<ReviewApiSummary, "origin"> & { reviewId?: string };
|
|
27
|
+
|
|
28
|
+
type Candidate = Pick<
|
|
29
|
+
ReviewApiSummary,
|
|
30
|
+
"reviewId" | "title" | "origin" | "pins" | "trashedAt" | "repositoryGroup"
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
interface StackEntry {
|
|
34
|
+
branch: string;
|
|
35
|
+
pullRequestNumber: number | null;
|
|
36
|
+
pullRequestUrl: string | null;
|
|
37
|
+
base: string | null;
|
|
38
|
+
subject: string | null;
|
|
39
|
+
current: boolean;
|
|
40
|
+
reviews: readonly Candidate[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The review's GitHub stack, else the chain of reviews through its base. */
|
|
25
44
|
export async function resolveReviewStackLayers(
|
|
26
|
-
subject:
|
|
27
|
-
reviews: readonly
|
|
28
|
-
|
|
29
|
-
"reviewId" | "title" | "origin" | "pins" | "trashedAt"
|
|
30
|
-
>[],
|
|
31
|
-
runGitHubApi: RunGitHubApi = defaultRunGitHubApi,
|
|
45
|
+
subject: Subject,
|
|
46
|
+
reviews: readonly Candidate[],
|
|
47
|
+
options: { runGitHubApi?: RunGitHubApi; repositoryPath?: string } = {},
|
|
32
48
|
): Promise<ReviewStackLayer[]> {
|
|
49
|
+
let entries = await gitHubStack(
|
|
50
|
+
subject,
|
|
51
|
+
reviews,
|
|
52
|
+
options.runGitHubApi ?? defaultRunGitHubApi,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!entries.length && options.repositoryPath)
|
|
56
|
+
entries = await localStack(subject, reviews, options.repositoryPath);
|
|
57
|
+
|
|
58
|
+
const currentIndex = entries.findIndex((entry) => entry.current);
|
|
59
|
+
|
|
60
|
+
return entries.map((entry, index) => {
|
|
61
|
+
const review =
|
|
62
|
+
entry.reviews.find(
|
|
63
|
+
(candidate) => candidate.reviewId === subject.reviewId,
|
|
64
|
+
) ??
|
|
65
|
+
entry.reviews.findLast((candidate) => !candidate.trashedAt) ??
|
|
66
|
+
entry.reviews.at(-1);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
branch: entry.branch,
|
|
70
|
+
pullRequestNumber: entry.pullRequestNumber,
|
|
71
|
+
pullRequestUrl: entry.pullRequestUrl,
|
|
72
|
+
base: entry.base,
|
|
73
|
+
subject: entry.subject,
|
|
74
|
+
reviewUuid: review?.reviewId ?? null,
|
|
75
|
+
reviewTitle: review?.title ?? null,
|
|
76
|
+
relation:
|
|
77
|
+
index < currentIndex
|
|
78
|
+
? "earlier"
|
|
79
|
+
: index === currentIndex
|
|
80
|
+
? "current"
|
|
81
|
+
: "later",
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function gitHubStack(
|
|
87
|
+
subject: Subject,
|
|
88
|
+
reviews: readonly Candidate[],
|
|
89
|
+
runGitHubApi: RunGitHubApi,
|
|
90
|
+
): Promise<StackEntry[]> {
|
|
33
91
|
const binding = subject.origin?.pullRequestUrl?.match(
|
|
34
92
|
/^https:\/\/([a-z0-9.-]+)\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/([1-9]\d*)$/,
|
|
35
93
|
);
|
|
@@ -59,43 +117,111 @@ export async function resolveReviewStackLayers(
|
|
|
59
117
|
candidate.pull_requests.some((pr) => pr.number === pullRequestNumber),
|
|
60
118
|
);
|
|
61
119
|
|
|
62
|
-
|
|
120
|
+
return (stack?.pull_requests ?? []).map((pr) => ({
|
|
121
|
+
branch: pr.head.ref,
|
|
122
|
+
pullRequestNumber: pr.number,
|
|
123
|
+
pullRequestUrl: `${repoKey}/pull/${pr.number}`,
|
|
124
|
+
base: null,
|
|
125
|
+
subject: null,
|
|
126
|
+
current: pr.number === pullRequestNumber,
|
|
127
|
+
reviews: reviews.filter(
|
|
128
|
+
(candidate) =>
|
|
129
|
+
(candidate.origin?.pullRequestUrl?.replace(/\/pull\/\d+.*$/, "") ??
|
|
130
|
+
candidate.pins?.repositoryId) === repoKey &&
|
|
131
|
+
candidate.origin?.pullRequestNumber === pr.number,
|
|
132
|
+
),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The chain of reviews through this one, linked by the base each names. */
|
|
137
|
+
async function localStack(
|
|
138
|
+
subject: Subject,
|
|
139
|
+
reviews: readonly Candidate[],
|
|
140
|
+
repositoryPath: string,
|
|
141
|
+
): Promise<StackEntry[]> {
|
|
142
|
+
const group = reviews.find((review) => review.reviewId === subject.reviewId)
|
|
143
|
+
?.repositoryGroup?.key;
|
|
144
|
+
|
|
145
|
+
const current = subject.origin?.branch;
|
|
146
|
+
|
|
147
|
+
if (!group || !current) return [];
|
|
148
|
+
|
|
149
|
+
const main = subject.origin?.baseBranch
|
|
150
|
+
? await defaultBranch(repositoryPath).catch(() => null)
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
const name = (ref: string) => ref.replace(/^origin\//, "");
|
|
63
154
|
|
|
64
|
-
const
|
|
65
|
-
(
|
|
155
|
+
const inGroup = reviews.filter(
|
|
156
|
+
(review) => review.repositoryGroup?.key === group,
|
|
66
157
|
);
|
|
67
158
|
|
|
68
|
-
|
|
69
|
-
const matches =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
candidate.pins?.repositoryId;
|
|
159
|
+
const latest = (field: "branch" | "baseBranch", branch: string) => {
|
|
160
|
+
const matches = inGroup.filter(
|
|
161
|
+
(review) => review.origin?.[field] === branch,
|
|
162
|
+
);
|
|
73
163
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
164
|
+
return matches.findLast((review) => !review.trashedAt) ?? matches.at(-1);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const branches = [current];
|
|
168
|
+
|
|
169
|
+
for (
|
|
170
|
+
let base = subject.origin?.baseBranch;
|
|
171
|
+
base && name(base) !== (main && name(main.ref)) && !branches.includes(base);
|
|
172
|
+
base = latest("branch", base)?.origin?.baseBranch
|
|
173
|
+
)
|
|
174
|
+
branches.unshift(base);
|
|
175
|
+
|
|
176
|
+
for (
|
|
177
|
+
let child = latest("baseBranch", current)?.origin?.branch;
|
|
178
|
+
child && !branches.includes(child);
|
|
179
|
+
child = latest("baseBranch", child)?.origin?.branch
|
|
180
|
+
)
|
|
181
|
+
branches.push(child);
|
|
182
|
+
|
|
183
|
+
const bottom = branches[0]!;
|
|
184
|
+
|
|
185
|
+
// A base nobody reviewed gets a row; a new review of it starts at its fork.
|
|
186
|
+
const start =
|
|
187
|
+
latest("branch", bottom) || !main
|
|
188
|
+
? null
|
|
189
|
+
: await forkPoint(repositoryPath, main.commit, bottom);
|
|
190
|
+
|
|
191
|
+
if (!latest("branch", bottom) && !start) branches.shift();
|
|
192
|
+
|
|
193
|
+
if (branches.length < 2) return [];
|
|
194
|
+
|
|
195
|
+
return branches.map((branch) => ({
|
|
196
|
+
branch,
|
|
197
|
+
pullRequestNumber: null,
|
|
198
|
+
pullRequestUrl: null,
|
|
199
|
+
base: (branch === bottom && start?.base) || null,
|
|
200
|
+
subject: (branch === bottom && start?.subject) || null,
|
|
201
|
+
current: branch === current,
|
|
202
|
+
reviews: inGroup.filter((review) => review.origin?.branch === branch),
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
79
205
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
matches.at(-1);
|
|
206
|
+
/** Where a local branch left `main`, and its oldest commit's subject. */
|
|
207
|
+
async function forkPoint(repositoryPath: string, main: string, branch: string) {
|
|
208
|
+
const ref = `refs/heads/${branch}`;
|
|
84
209
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
pullRequestNumber: pr.number,
|
|
88
|
-
pullRequestUrl: `${repoKey}/pull/${pr.number}`,
|
|
89
|
-
reviewUuid: review?.reviewId ?? null,
|
|
90
|
-
reviewTitle: review?.title ?? null,
|
|
91
|
-
relation:
|
|
92
|
-
index < currentIndex
|
|
93
|
-
? "earlier"
|
|
94
|
-
: index === currentIndex
|
|
95
|
-
? "current"
|
|
96
|
-
: "later",
|
|
97
|
-
};
|
|
210
|
+
const fork = await git(repositoryPath, ["merge-base", main, ref], {
|
|
211
|
+
allowFailure: true,
|
|
98
212
|
});
|
|
213
|
+
|
|
214
|
+
if (!fork.ok) return null;
|
|
215
|
+
|
|
216
|
+
const base = fork.stdout.trim();
|
|
217
|
+
|
|
218
|
+
const log = await git(
|
|
219
|
+
repositoryPath,
|
|
220
|
+
["log", "--reverse", "--format=%s", `${base}..${ref}`],
|
|
221
|
+
{ allowFailure: true },
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return { base, subject: log.stdout.split("\n")[0] || null };
|
|
99
225
|
}
|
|
100
226
|
|
|
101
227
|
async function defaultRunGitHubApi(
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, open, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
import { liveLockOwner, processIsAlive } from "@dev.fast/trace-core";
|
|
7
|
+
import {
|
|
8
|
+
type ReviewInstanceSelection,
|
|
9
|
+
desktopUsesHome,
|
|
10
|
+
} from "@review/desktop-discovery.js";
|
|
11
|
+
import { findReviewPackageRoot } from "@review/package-paths.js";
|
|
12
|
+
import { desktopApplicationInstalled } from "@review/review-app-launcher.js";
|
|
13
|
+
import {
|
|
14
|
+
type ReviewServerDiscovery,
|
|
15
|
+
desktopOwnsHome,
|
|
16
|
+
headlessServerLockPath,
|
|
17
|
+
readReviewServerDiscovery,
|
|
18
|
+
readReviewServerHealth,
|
|
19
|
+
reviewServerStateDir,
|
|
20
|
+
} from "@review/server-discovery.js";
|
|
21
|
+
|
|
22
|
+
export interface EnsureBackgroundServerInput {
|
|
23
|
+
stateDir: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
startedBy?: "cli" | "desktop";
|
|
27
|
+
args?: readonly string[];
|
|
28
|
+
cli?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function backgroundServerLogPath(stateDir: string) {
|
|
32
|
+
return path.join(stateDir, "review-server", "server.log");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function ensureBackgroundServer(
|
|
36
|
+
input: EnsureBackgroundServerInput,
|
|
37
|
+
): Promise<{ discovery: ReviewServerDiscovery; started: boolean }> {
|
|
38
|
+
const stateDir = path.resolve(input.stateDir);
|
|
39
|
+
const running = await healthyDiscovery(stateDir);
|
|
40
|
+
|
|
41
|
+
if (running) return { discovery: running, started: false };
|
|
42
|
+
|
|
43
|
+
if (await desktopUsesHome(stateDir, input.env))
|
|
44
|
+
throw new Error(desktopOwnsHome(stateDir));
|
|
45
|
+
|
|
46
|
+
const logPath = backgroundServerLogPath(stateDir);
|
|
47
|
+
await mkdir(path.dirname(logPath), { recursive: true, mode: 0o700 });
|
|
48
|
+
const logStart = (await stat(logPath).catch(() => null))?.size ?? 0;
|
|
49
|
+
const deadline = Date.now() + (input.timeoutMs ?? 15_000);
|
|
50
|
+
let child = await spawnServer(stateDir, logPath, input);
|
|
51
|
+
let respawned = false;
|
|
52
|
+
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
const discovery = await healthyDiscovery(stateDir);
|
|
55
|
+
|
|
56
|
+
if (discovery)
|
|
57
|
+
return { discovery, started: discovery.serverPid === child.pid };
|
|
58
|
+
|
|
59
|
+
if (child.error)
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Could not start the Whiteboard server: ${child.error.message}`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (child.exited && (await headlessServerOwner(stateDir)) === undefined) {
|
|
65
|
+
if (respawned) break;
|
|
66
|
+
respawned = true;
|
|
67
|
+
child = await spawnServer(stateDir, logPath, input);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await delay(100);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const owner = await headlessServerOwner(stateDir);
|
|
74
|
+
|
|
75
|
+
throw new Error(
|
|
76
|
+
`The Whiteboard server did not become ready${child.exited ? "" : ` within ${Math.round((input.timeoutMs ?? 15_000) / 1_000)} s; process ${child.pid} is still starting`}.${owner !== undefined && owner !== child.pid ? ` Process ${owner} holds its state directory without answering; \`whiteboard server stop\` ends it.` : ""} The end of ${logPath}:\n${await logTail(logPath, logStart)}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function ensureServerWithoutDesktop(input: {
|
|
81
|
+
selection: ReviewInstanceSelection;
|
|
82
|
+
env: NodeJS.ProcessEnv;
|
|
83
|
+
desktopInstalled?: () => boolean;
|
|
84
|
+
cli?: readonly string[];
|
|
85
|
+
}) {
|
|
86
|
+
const { selection, env } = input;
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
selection.source !== "fallback" ||
|
|
90
|
+
selection.instances.length !== 0 ||
|
|
91
|
+
selection.problem ||
|
|
92
|
+
(input.desktopInstalled ?? (() => desktopApplicationInstalled({ env })))()
|
|
93
|
+
)
|
|
94
|
+
return undefined;
|
|
95
|
+
|
|
96
|
+
const { discovery } = await ensureBackgroundServer({
|
|
97
|
+
stateDir: reviewServerStateDir(env),
|
|
98
|
+
env,
|
|
99
|
+
cli: input.cli,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return discovery;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function headlessServerOwner(stateDir: string) {
|
|
106
|
+
const resolved = await realpath(stateDir).catch(() => path.resolve(stateDir));
|
|
107
|
+
|
|
108
|
+
return liveLockOwner(headlessServerLockPath(resolved));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function recordedBackgroundServer(stateDir: string) {
|
|
112
|
+
const discovery = await readReviewServerDiscovery(stateDir).catch(() => null);
|
|
113
|
+
const health = discovery && (await readReviewServerHealth(discovery));
|
|
114
|
+
|
|
115
|
+
return discovery && health?.serverPid === discovery.serverPid
|
|
116
|
+
? { discovery, health }
|
|
117
|
+
: undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function stopBackgroundServer(
|
|
121
|
+
discovery: Pick<ReviewServerDiscovery, "serverPid">,
|
|
122
|
+
first: NodeJS.Signals = "SIGTERM",
|
|
123
|
+
) {
|
|
124
|
+
const { serverPid } = discovery;
|
|
125
|
+
|
|
126
|
+
for (const [signal, waitMs] of [
|
|
127
|
+
[first, 10_000],
|
|
128
|
+
["SIGKILL", 2_000],
|
|
129
|
+
] as const) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(serverPid, signal);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH")
|
|
134
|
+
return;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (let waited = 0; waited < waitMs; waited += 100) {
|
|
139
|
+
if (!processIsAlive(serverPid)) return;
|
|
140
|
+
await delay(100);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new Error(`The Whiteboard server (process ${serverPid}) did not stop.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface DetachedChild {
|
|
148
|
+
pid?: number;
|
|
149
|
+
exited: boolean;
|
|
150
|
+
error?: Error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function spawnDetached(input: {
|
|
154
|
+
command: string;
|
|
155
|
+
args: readonly string[];
|
|
156
|
+
cwd: string;
|
|
157
|
+
env: NodeJS.ProcessEnv;
|
|
158
|
+
log: string;
|
|
159
|
+
}) {
|
|
160
|
+
const log = await open(input.log, "a", 0o600);
|
|
161
|
+
|
|
162
|
+
const child = spawn(input.command, input.args, {
|
|
163
|
+
cwd: input.cwd,
|
|
164
|
+
detached: true,
|
|
165
|
+
env: input.env,
|
|
166
|
+
stdio: ["ignore", log.fd, log.fd],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const state: DetachedChild = { pid: child.pid, exited: false };
|
|
170
|
+
|
|
171
|
+
child.once("exit", () => (state.exited = true));
|
|
172
|
+
child.once("error", (error) => {
|
|
173
|
+
state.exited = true;
|
|
174
|
+
state.error = error;
|
|
175
|
+
});
|
|
176
|
+
child.unref();
|
|
177
|
+
await log.close();
|
|
178
|
+
|
|
179
|
+
return state;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function spawnServer(
|
|
183
|
+
stateDir: string,
|
|
184
|
+
logPath: string,
|
|
185
|
+
input: EnsureBackgroundServerInput,
|
|
186
|
+
) {
|
|
187
|
+
const { command, args, env } = cliSpawn(input.cli, input.env ?? process.env);
|
|
188
|
+
|
|
189
|
+
return spawnDetached({
|
|
190
|
+
command,
|
|
191
|
+
args: [
|
|
192
|
+
...args,
|
|
193
|
+
"server",
|
|
194
|
+
"start",
|
|
195
|
+
"--state-dir",
|
|
196
|
+
stateDir,
|
|
197
|
+
"--started-by",
|
|
198
|
+
input.startedBy ?? "cli",
|
|
199
|
+
...(input.args ?? []),
|
|
200
|
+
],
|
|
201
|
+
cwd: stateDir,
|
|
202
|
+
env,
|
|
203
|
+
log: logPath,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function cliSpawn(
|
|
208
|
+
cli: readonly string[] | undefined,
|
|
209
|
+
env: NodeJS.ProcessEnv,
|
|
210
|
+
) {
|
|
211
|
+
const [command, ...args] = cli ?? currentCli();
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
command: command!,
|
|
215
|
+
args,
|
|
216
|
+
// Run from source, tsx finds the path aliases only through this.
|
|
217
|
+
env:
|
|
218
|
+
args.at(-1)?.endsWith(".ts") && !env.TSX_TSCONFIG_PATH
|
|
219
|
+
? {
|
|
220
|
+
...env,
|
|
221
|
+
TSX_TSCONFIG_PATH: path.join(
|
|
222
|
+
findReviewPackageRoot(import.meta.url),
|
|
223
|
+
"tsconfig.json",
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
: env,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function currentCli() {
|
|
231
|
+
return [
|
|
232
|
+
process.execPath,
|
|
233
|
+
...process.execArgv,
|
|
234
|
+
path.resolve(process.argv[1]!),
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function healthyDiscovery(stateDir: string) {
|
|
239
|
+
const discovery = await readReviewServerDiscovery(stateDir).catch(() => null);
|
|
240
|
+
|
|
241
|
+
return discovery && (await readReviewServerHealth(discovery))
|
|
242
|
+
? discovery
|
|
243
|
+
: null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function logTail(logPath: string, from: number) {
|
|
247
|
+
const text = (await readFile(logPath)).subarray(from).toString("utf8");
|
|
248
|
+
|
|
249
|
+
return text.trimEnd().split("\n").slice(-20).join("\n");
|
|
250
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type JsonValue,
|
|
3
|
+
type ReviewGatewayHost,
|
|
4
|
+
ReviewGatewayHostSchema,
|
|
3
5
|
isJsonObject,
|
|
4
6
|
jsonBoolean,
|
|
5
7
|
jsonProperty,
|
|
@@ -9,12 +11,23 @@ import {
|
|
|
9
11
|
interface DesktopHostMessagePort {
|
|
10
12
|
on(event: "message", listener: (event: { data: JsonValue }) => void): void;
|
|
11
13
|
off(event: "message", listener: (event: { data: JsonValue }) => void): void;
|
|
14
|
+
postMessage?(message: JsonValue): void;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
interface DesktopHostProcess {
|
|
15
18
|
parentPort?: DesktopHostMessagePort;
|
|
16
19
|
on(event: "message", listener: (message: JsonValue) => void): void;
|
|
17
20
|
off(event: "message", listener: (message: JsonValue) => void): void;
|
|
21
|
+
send?(message: JsonValue): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function postToDesktop(
|
|
25
|
+
hostProcess: DesktopHostProcess,
|
|
26
|
+
message: JsonValue,
|
|
27
|
+
): void {
|
|
28
|
+
if (hostProcess.parentPort?.postMessage)
|
|
29
|
+
hostProcess.parentPort.postMessage(message);
|
|
30
|
+
else hostProcess.send?.(message);
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
export function listenForDesktopHostShutdown(
|
|
@@ -22,6 +35,7 @@ export function listenForDesktopHostShutdown(
|
|
|
22
35
|
onShutdown: () => void,
|
|
23
36
|
onTelemetrySetting?: (enabled: boolean) => void,
|
|
24
37
|
onStageRustAnalyzer?: (path: string) => void,
|
|
38
|
+
onRemoteHosts?: (hosts: ReviewGatewayHost[]) => void,
|
|
25
39
|
): () => void {
|
|
26
40
|
const handleMessage = (message: JsonValue) => {
|
|
27
41
|
if (!isJsonObject(message)) return;
|
|
@@ -45,6 +59,16 @@ export function listenForDesktopHostShutdown(
|
|
|
45
59
|
const path = jsonString(jsonProperty(message, "path"));
|
|
46
60
|
|
|
47
61
|
if (path) onStageRustAnalyzer?.(path);
|
|
62
|
+
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (type === "remote-hosts") {
|
|
67
|
+
const hosts = ReviewGatewayHostSchema.array().safeParse(
|
|
68
|
+
jsonProperty(message, "hosts"),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (hosts.success) onRemoteHosts?.(hosts.data);
|
|
48
72
|
}
|
|
49
73
|
};
|
|
50
74
|
|
|
@@ -12,8 +12,12 @@ import { ReviewTelemetry } from "@review/review-telemetry";
|
|
|
12
12
|
import { SharedReviewStore } from "@review/sharing/import.js";
|
|
13
13
|
import { reviewTelemetryChannel } from "@review/telemetry-config";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
listenForDesktopHostShutdown,
|
|
17
|
+
postToDesktop,
|
|
18
|
+
} from "./desktop-host-shutdown";
|
|
16
19
|
import { createGlobalReviewServer } from "./desktop-server";
|
|
20
|
+
import { takeOverServerHome } from "./headless-host";
|
|
17
21
|
import {
|
|
18
22
|
drainServerCrashReport,
|
|
19
23
|
installProcessErrorTelemetry,
|
|
@@ -74,34 +78,44 @@ export async function runDesktopHost(
|
|
|
74
78
|
const home = devReviewHome(env);
|
|
75
79
|
await mkdir(home, { recursive: true });
|
|
76
80
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const log = (message: string) =>
|
|
82
|
+
process.stdout.write(`[Whiteboard gateway] ${message}\n`);
|
|
83
|
+
|
|
84
|
+
const { local, shared, server } = await takeOverServerHome(home, async () => {
|
|
85
|
+
const local = await openReviewProfile(home, {
|
|
86
|
+
manageWorkspaces: true,
|
|
87
|
+
});
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
// JSON is the sole user-review store. A failure is surfaced, never replaced
|
|
90
|
+
// by a second catalog or an old document renderer.
|
|
91
|
+
|
|
92
|
+
const shared = new SharedReviewStore(path.join(home, "shared-reviews"));
|
|
93
|
+
shared.connect(local.store, local.data);
|
|
94
|
+
await shared.load();
|
|
95
|
+
|
|
96
|
+
const server = createGlobalReviewServer({
|
|
97
|
+
...serverInput,
|
|
98
|
+
log,
|
|
99
|
+
sharedReviews: shared,
|
|
100
|
+
reviewStore: local.store,
|
|
101
|
+
reviewData: local.data,
|
|
102
|
+
cliRuntimePath: env.DEV_FAST_REVIEW_CLI_RUNTIME,
|
|
103
|
+
crashDumpsDir: env.DEV_FAST_REVIEW_CRASH_DUMPS_DIR,
|
|
104
|
+
onRemoteHostRestarted: (alias) =>
|
|
105
|
+
postToDesktop(process, { type: "remote-host-restarted", alias }),
|
|
106
|
+
});
|
|
83
107
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
108
|
+
try {
|
|
109
|
+
await server.listen();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await local?.data.close();
|
|
112
|
+
await local?.store.close();
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
87
115
|
|
|
88
|
-
|
|
89
|
-
...serverInput,
|
|
90
|
-
sharedReviews: shared,
|
|
91
|
-
reviewStore: local.store,
|
|
92
|
-
reviewData: local.data,
|
|
93
|
-
cliRuntimePath: env.DEV_FAST_REVIEW_CLI_RUNTIME,
|
|
94
|
-
crashDumpsDir: env.DEV_FAST_REVIEW_CRASH_DUMPS_DIR,
|
|
116
|
+
return { local, shared, server };
|
|
95
117
|
});
|
|
96
118
|
|
|
97
|
-
try {
|
|
98
|
-
await server.listen();
|
|
99
|
-
} catch (error) {
|
|
100
|
-
await local?.data.close();
|
|
101
|
-
await local?.store.close();
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
119
|
process.stdout.write(
|
|
106
120
|
`${JSON.stringify({ event: "ready", ...server.discovery, installationId })}\n`,
|
|
107
121
|
);
|
|
@@ -142,6 +156,7 @@ export async function runDesktopHost(
|
|
|
142
156
|
env.DEV_FAST_REVIEW_RUST_ANALYZER = sourcePath;
|
|
143
157
|
void stageRustAnalyzer();
|
|
144
158
|
},
|
|
159
|
+
(hosts) => server.setRemoteHosts(hosts),
|
|
145
160
|
);
|
|
146
161
|
process.once("SIGINT", () => {
|
|
147
162
|
void stop().then(() => process.exit(0));
|