@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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export interface RecentFoldersEnvironment {
|
|
9
|
+
homeDir?: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
platform?: NodeJS.Platform;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** VS Code forks whose recently opened folders count as projects. */
|
|
15
|
+
const EDITORS = ["Cursor", "Code"];
|
|
16
|
+
|
|
17
|
+
async function readText(file: string): Promise<string | undefined> {
|
|
18
|
+
try {
|
|
19
|
+
return await readFile(file, "utf8");
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A file's JSON as `schema` reads it, or undefined when it is missing,
|
|
26
|
+
* malformed, or shaped otherwise: another app's file, read on its terms. */
|
|
27
|
+
async function readJson<Schema extends z.ZodType>(
|
|
28
|
+
file: string,
|
|
29
|
+
schema: Schema,
|
|
30
|
+
): Promise<z.infer<Schema> | undefined> {
|
|
31
|
+
const text = await readText(file);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return text === undefined
|
|
35
|
+
? undefined
|
|
36
|
+
: schema.safeParse(JSON.parse(text)).data;
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const claudeConfigSchema = z.object({
|
|
43
|
+
projects: z.record(z.string(), z.object({}).loose()),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Claude Code keeps per-folder settings for every folder it ran in. */
|
|
47
|
+
async function claudeFolders(homeDir: string, env: NodeJS.ProcessEnv) {
|
|
48
|
+
const config = await readJson(
|
|
49
|
+
path.join(env.CLAUDE_CONFIG_DIR ?? homeDir, ".claude.json"),
|
|
50
|
+
claudeConfigSchema,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return Object.keys(config?.projects ?? {});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Codex records each folder it was trusted in as a `[projects."…"]` table. */
|
|
57
|
+
export function codexConfigFolders(toml: string): string[] {
|
|
58
|
+
const folders: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (const match of toml.matchAll(
|
|
61
|
+
/^\[projects\.(?:"((?:[^"\\]|\\.)*)"|'([^']*)')\]/gmu,
|
|
62
|
+
)) {
|
|
63
|
+
if (match[2] !== undefined) folders.push(match[2]);
|
|
64
|
+
else if (match[1] !== undefined)
|
|
65
|
+
try {
|
|
66
|
+
// A TOML basic string escapes as JSON does for paths.
|
|
67
|
+
folders.push(JSON.parse(`"${match[1]}"`));
|
|
68
|
+
} catch {
|
|
69
|
+
// An escape JSON lacks: not a path worth guessing at.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return folders;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function codexFolders(homeDir: string, env: NodeJS.ProcessEnv) {
|
|
77
|
+
const toml = await readText(
|
|
78
|
+
path.join(env.CODEX_HOME ?? path.join(homeDir, ".codex"), "config.toml"),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return toml === undefined ? [] : codexConfigFolders(toml);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function editorDataDir(
|
|
85
|
+
app: string,
|
|
86
|
+
homeDir: string,
|
|
87
|
+
env: NodeJS.ProcessEnv,
|
|
88
|
+
platform: NodeJS.Platform,
|
|
89
|
+
) {
|
|
90
|
+
if (platform === "darwin")
|
|
91
|
+
return path.join(homeDir, "Library", "Application Support", app);
|
|
92
|
+
|
|
93
|
+
if (platform === "win32")
|
|
94
|
+
return path.join(
|
|
95
|
+
env.APPDATA ?? path.join(homeDir, "AppData", "Roaming"),
|
|
96
|
+
app,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return path.join(env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config"), app);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The parts of an editor's storage.json that name folders; either can be
|
|
103
|
+
* missing or reshaped by another version without losing the other. */
|
|
104
|
+
const editorStorageSchema = z.object({
|
|
105
|
+
profileAssociations: z
|
|
106
|
+
.object({ workspaces: z.record(z.string(), z.unknown()) })
|
|
107
|
+
.optional()
|
|
108
|
+
.catch(undefined),
|
|
109
|
+
backupWorkspaces: z
|
|
110
|
+
.object({ folders: z.array(z.object({ folderUri: z.string() })) })
|
|
111
|
+
.optional()
|
|
112
|
+
.catch(undefined),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** The folders an editor opened: those with a profile, and those with
|
|
116
|
+
* unsaved work. Both live in its storage.json, unlike its sqlite history. */
|
|
117
|
+
function editorStorageFolders(
|
|
118
|
+
storage: z.infer<typeof editorStorageSchema>,
|
|
119
|
+
): string[] {
|
|
120
|
+
const uris = [
|
|
121
|
+
...Object.keys(storage.profileAssociations?.workspaces ?? {}),
|
|
122
|
+
...(storage.backupWorkspaces?.folders ?? []).map(
|
|
123
|
+
(folder) => folder.folderUri,
|
|
124
|
+
),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
return uris.flatMap((uri) => {
|
|
128
|
+
if (!uri.startsWith("file://")) return [];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
return [fileURLToPath(uri)];
|
|
132
|
+
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function editorFolders(
|
|
139
|
+
app: string,
|
|
140
|
+
homeDir: string,
|
|
141
|
+
env: NodeJS.ProcessEnv,
|
|
142
|
+
platform: NodeJS.Platform,
|
|
143
|
+
) {
|
|
144
|
+
const storage = await readJson(
|
|
145
|
+
path.join(
|
|
146
|
+
editorDataDir(app, homeDir, env, platform),
|
|
147
|
+
"User",
|
|
148
|
+
"globalStorage",
|
|
149
|
+
"storage.json",
|
|
150
|
+
),
|
|
151
|
+
editorStorageSchema,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return storage ? editorStorageFolders(storage) : [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Folders the user's agents and editors have worked in, unverified: some
|
|
158
|
+
* are gone, and many are not repositories or are inside one. */
|
|
159
|
+
export async function recentFolders(
|
|
160
|
+
input: RecentFoldersEnvironment = {},
|
|
161
|
+
): Promise<string[]> {
|
|
162
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
163
|
+
const env = input.env ?? process.env;
|
|
164
|
+
const platform = input.platform ?? process.platform;
|
|
165
|
+
|
|
166
|
+
const lists = await Promise.all([
|
|
167
|
+
claudeFolders(homeDir, env),
|
|
168
|
+
codexFolders(homeDir, env),
|
|
169
|
+
...EDITORS.map((app) => editorFolders(app, homeDir, env, platform)),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
return [...new Set(lists.flat().filter((folder) => path.isAbsolute(folder)))];
|
|
173
|
+
}
|
package/src/review-api/store.ts
CHANGED
|
@@ -150,12 +150,14 @@ export const commandSchema = z.strictObject({
|
|
|
150
150
|
|
|
151
151
|
/** Source identity displayed in the review header and Home, alongside immutable pins. */
|
|
152
152
|
export interface SnapshotOrigin {
|
|
153
|
-
/** Managed tutorial; readable by ID but excluded from the user catalog. */
|
|
154
|
-
tutorial?: boolean;
|
|
155
153
|
branch?: string;
|
|
156
154
|
baseRef?: string;
|
|
155
|
+
/** The base the review's target names, such as the branch below it. */
|
|
156
|
+
baseBranch?: string;
|
|
157
157
|
pullRequestNumber?: number;
|
|
158
158
|
pullRequestUrl?: string;
|
|
159
|
+
/** As the forge reported it when the PR was attached. */
|
|
160
|
+
pullRequestTitle?: string;
|
|
159
161
|
/** The legacy review revision this version was imported from. */
|
|
160
162
|
revision?: string;
|
|
161
163
|
}
|
|
@@ -216,6 +218,7 @@ export interface Result {
|
|
|
216
218
|
export interface ResolvedPullRequest {
|
|
217
219
|
target: ReviewTarget;
|
|
218
220
|
pins: Pins;
|
|
221
|
+
/** Empty when the PR has none. */
|
|
219
222
|
title: string;
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -228,6 +231,8 @@ export interface ReviewProviders {
|
|
|
228
231
|
url: string,
|
|
229
232
|
repository: { id?: string; preferred?: string },
|
|
230
233
|
): Promise<ResolvedPullRequest>;
|
|
234
|
+
/** Undefined when the forge can't be read. */
|
|
235
|
+
pullRequestTitle?(url: string): Promise<string | undefined>;
|
|
231
236
|
resolveTarget?(
|
|
232
237
|
target: ReviewTarget,
|
|
233
238
|
pinned?: Pins,
|
|
@@ -844,8 +849,8 @@ export class ReviewStore {
|
|
|
844
849
|
}
|
|
845
850
|
}
|
|
846
851
|
|
|
847
|
-
/**
|
|
848
|
-
|
|
852
|
+
/** Reviews of the tutorial earlier versions shipped, for their removal. */
|
|
853
|
+
legacyTutorialIds(): string[] {
|
|
849
854
|
return this.db
|
|
850
855
|
.prepare(
|
|
851
856
|
`SELECT reviews.id FROM reviews JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version WHERE json_extract(versions.snapshot,'$.origin.tutorial') = 1`,
|
|
@@ -877,8 +882,7 @@ export class ReviewStore {
|
|
|
877
882
|
JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
|
|
878
883
|
LEFT JOIN review_attention ON review_attention.review_id=reviews.id
|
|
879
884
|
LEFT JOIN repositories ON repositories.id=json_extract(versions.snapshot,'$.pins.repositoryId')
|
|
880
|
-
|
|
881
|
-
${id === undefined ? "" : "AND reviews.id=?"}
|
|
885
|
+
${id === undefined ? "" : "WHERE reviews.id=?"}
|
|
882
886
|
ORDER BY reviews.rowid`,
|
|
883
887
|
)
|
|
884
888
|
.all(...(id === undefined ? [] : [id]))
|
|
@@ -1004,7 +1008,7 @@ export class ReviewStore {
|
|
|
1004
1008
|
|
|
1005
1009
|
return inspectSnapshot(snapshot, targetId);
|
|
1006
1010
|
}
|
|
1007
|
-
/** The host can seed a
|
|
1011
|
+
/** The host can seed a document; transport callers only supply a command. */
|
|
1008
1012
|
execute(
|
|
1009
1013
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Command boundary: commandSchema.parse below rejects malformed input before mutation.
|
|
1010
1014
|
input: unknown,
|
|
@@ -1016,11 +1020,19 @@ export class ReviewStore {
|
|
|
1016
1020
|
|
|
1017
1021
|
if (initial && command.operation.type !== "create")
|
|
1018
1022
|
throw new ReviewInputError("Initial content requires a create command.");
|
|
1023
|
+
|
|
1019
1024
|
// Network and fetch time stay out of the write queue.
|
|
1020
1025
|
const pullRequest = this.startPullRequest(command);
|
|
1021
1026
|
|
|
1022
1027
|
pullRequest?.catch(() => {});
|
|
1023
1028
|
|
|
1029
|
+
const pullRequestTitle = pullRequest
|
|
1030
|
+
? pullRequest.then(
|
|
1031
|
+
({ title }) => title || undefined,
|
|
1032
|
+
() => undefined,
|
|
1033
|
+
)
|
|
1034
|
+
: this.startPullRequestTitle(command);
|
|
1035
|
+
|
|
1024
1036
|
const run = this.pending.then(async () => {
|
|
1025
1037
|
const op = command.operation;
|
|
1026
1038
|
this.purgeExpired();
|
|
@@ -1172,7 +1184,14 @@ export class ReviewStore {
|
|
|
1172
1184
|
|
|
1173
1185
|
let snapshot: Snapshot =
|
|
1174
1186
|
op.type === "create"
|
|
1175
|
-
? createdSnapshot(
|
|
1187
|
+
? createdSnapshot(
|
|
1188
|
+
id,
|
|
1189
|
+
op,
|
|
1190
|
+
resolvedTarget,
|
|
1191
|
+
fromPullRequest &&
|
|
1192
|
+
(fromPullRequest.title ||
|
|
1193
|
+
`PR #${op.pullRequestUrl!.split("/").at(-1)}`),
|
|
1194
|
+
)
|
|
1176
1195
|
: structuredClone(previous!);
|
|
1177
1196
|
|
|
1178
1197
|
delete snapshot.trashedAt;
|
|
@@ -1210,6 +1229,17 @@ export class ReviewStore {
|
|
|
1210
1229
|
}
|
|
1211
1230
|
}
|
|
1212
1231
|
|
|
1232
|
+
// A base named rather than given as a commit; a stacked review names
|
|
1233
|
+
// the branch below it.
|
|
1234
|
+
if (requestedTarget)
|
|
1235
|
+
snapshot.origin = {
|
|
1236
|
+
...snapshot.origin,
|
|
1237
|
+
baseBranch:
|
|
1238
|
+
requestedTarget.base === resolvedTarget?.pins.base
|
|
1239
|
+
? undefined
|
|
1240
|
+
: requestedTarget.base,
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1213
1243
|
switch (op.type) {
|
|
1214
1244
|
case "create":
|
|
1215
1245
|
if (initial) {
|
|
@@ -1223,7 +1253,7 @@ export class ReviewStore {
|
|
|
1223
1253
|
assignFreshIds(block, (prefix) => `${prefix}-${++nextId}`);
|
|
1224
1254
|
}
|
|
1225
1255
|
|
|
1226
|
-
setPullRequest(snapshot, op.pullRequestUrl);
|
|
1256
|
+
setPullRequest(snapshot, op.pullRequestUrl, await pullRequestTitle);
|
|
1227
1257
|
break;
|
|
1228
1258
|
case "rename":
|
|
1229
1259
|
snapshot.title = op.title;
|
|
@@ -1236,6 +1266,7 @@ export class ReviewStore {
|
|
|
1236
1266
|
snapshot.pins?.repositoryId !== resolvedTarget!.pins.repositoryId
|
|
1237
1267
|
? null
|
|
1238
1268
|
: undefined),
|
|
1269
|
+
await pullRequestTitle,
|
|
1239
1270
|
);
|
|
1240
1271
|
snapshot.staleSources = [];
|
|
1241
1272
|
snapshot.target = resolvedTarget!.target;
|
|
@@ -1425,6 +1456,22 @@ export class ReviewStore {
|
|
|
1425
1456
|
preferred: existing && this.read(existing).pins?.repositoryId,
|
|
1426
1457
|
});
|
|
1427
1458
|
}
|
|
1459
|
+
private startPullRequestTitle(
|
|
1460
|
+
command: z.infer<typeof commandSchema>,
|
|
1461
|
+
): Promise<string | undefined> {
|
|
1462
|
+
const op = command.operation;
|
|
1463
|
+
|
|
1464
|
+
if (
|
|
1465
|
+
(op.type !== "create" && op.type !== "set_target") ||
|
|
1466
|
+
!op.pullRequestUrl
|
|
1467
|
+
)
|
|
1468
|
+
return Promise.resolve(undefined);
|
|
1469
|
+
|
|
1470
|
+
return (
|
|
1471
|
+
this.providers.pullRequestTitle?.(op.pullRequestUrl) ??
|
|
1472
|
+
Promise.resolve(undefined)
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1428
1475
|
/** Reviews whose PR is this one, newest version first. Summaries only. */
|
|
1429
1476
|
private reviewsForPullRequest(url: string): string[] {
|
|
1430
1477
|
return this.db
|
|
@@ -1432,7 +1479,6 @@ export class ReviewStore {
|
|
|
1432
1479
|
`SELECT reviews.id FROM reviews
|
|
1433
1480
|
JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
|
|
1434
1481
|
WHERE lower(json_extract(versions.snapshot,'$.origin.pullRequestUrl'))=?
|
|
1435
|
-
AND COALESCE(json_extract(versions.snapshot,'$.origin.tutorial'), 0) = 0
|
|
1436
1482
|
ORDER BY json_extract(versions.snapshot,'$.createdAt') DESC, reviews.rowid DESC`,
|
|
1437
1483
|
)
|
|
1438
1484
|
.all(pullRequestKey(url))
|
|
@@ -48,9 +48,8 @@ interface Environment extends WorkspaceStatus {
|
|
|
48
48
|
role: "base" | "head";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// A lease lasts until the owning Desktop exits; closing the review does not release it.
|
|
52
51
|
const OWNED_ELSEWHERE =
|
|
53
|
-
"Another
|
|
52
|
+
"Another Whiteboard server is managing this review's language workspaces. Retry once it stops.";
|
|
54
53
|
|
|
55
54
|
/** Local lifecycle only: source and authored history never depend on preparation.
|
|
56
55
|
* Status/queue/process lifecycle follows #334, retaining the legacy prepare config
|
|
@@ -101,10 +100,23 @@ export class ReviewWorkspaces {
|
|
|
101
100
|
throw error;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
|
|
103
|
+
const stopCatalog = store.subscribeCatalog(() => {
|
|
105
104
|
this.collect();
|
|
106
105
|
this.releaseTrashed();
|
|
107
106
|
});
|
|
107
|
+
|
|
108
|
+
const stopDeleted = store.subscribe(({ reviewId, deleted }) => {
|
|
109
|
+
if (!deleted) return;
|
|
110
|
+
const repositories = this.repositoryDirs();
|
|
111
|
+
repositories.catch(() => undefined);
|
|
112
|
+
this.release(reviewId, repositories, () => !this.hasReview(reviewId));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.stop = () => {
|
|
116
|
+
stopCatalog();
|
|
117
|
+
stopDeleted();
|
|
118
|
+
};
|
|
119
|
+
|
|
108
120
|
this.collect();
|
|
109
121
|
this.releaseTrashed();
|
|
110
122
|
}
|
|
@@ -131,7 +143,13 @@ export class ReviewWorkspaces {
|
|
|
131
143
|
// Each release awaits this only after earlier cleanup and logs its failure.
|
|
132
144
|
repositories.catch(() => undefined);
|
|
133
145
|
|
|
134
|
-
for (const reviewId of released)
|
|
146
|
+
for (const reviewId of released)
|
|
147
|
+
this.release(reviewId, repositories, () => this.stillTrashed(reviewId));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Restored since the move to Trash: its checkouts may be in use. */
|
|
151
|
+
private stillTrashed(reviewId: string) {
|
|
152
|
+
return this.store.trashedIds().includes(reviewId);
|
|
135
153
|
}
|
|
136
154
|
|
|
137
155
|
private async repositoryDirs(): Promise<string[]> {
|
|
@@ -151,18 +169,26 @@ export class ReviewWorkspaces {
|
|
|
151
169
|
return this.releasing.get(reviewId) ?? Promise.resolve();
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
private release(
|
|
172
|
+
private release(
|
|
173
|
+
reviewId: string,
|
|
174
|
+
repositories: Promise<string[]>,
|
|
175
|
+
stillReleased: () => boolean,
|
|
176
|
+
) {
|
|
155
177
|
// Requests started later wait for this release; earlier ones finish first.
|
|
156
178
|
const requests = [...this.requests.values()];
|
|
157
179
|
|
|
158
180
|
const done = this.cleanup
|
|
159
181
|
.then(async () => {
|
|
160
182
|
await Promise.allSettled(requests);
|
|
161
|
-
await this.releaseCheckouts(
|
|
183
|
+
await this.releaseCheckouts(
|
|
184
|
+
reviewId,
|
|
185
|
+
await repositories,
|
|
186
|
+
stillReleased,
|
|
187
|
+
);
|
|
162
188
|
})
|
|
163
189
|
.catch((error) => {
|
|
164
190
|
console.error(
|
|
165
|
-
`Could not free the pinned checkouts of
|
|
191
|
+
`Could not free the pinned checkouts of review ${reviewId}:`,
|
|
166
192
|
error,
|
|
167
193
|
);
|
|
168
194
|
})
|
|
@@ -175,9 +201,12 @@ export class ReviewWorkspaces {
|
|
|
175
201
|
this.cleanup = done;
|
|
176
202
|
}
|
|
177
203
|
|
|
178
|
-
private async releaseCheckouts(
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
private async releaseCheckouts(
|
|
205
|
+
reviewId: string,
|
|
206
|
+
repositories: string[],
|
|
207
|
+
stillReleased: () => boolean,
|
|
208
|
+
) {
|
|
209
|
+
if (!stillReleased()) return;
|
|
181
210
|
|
|
182
211
|
const environments = this.all().filter(
|
|
183
212
|
(item) => item.reviewId === reviewId,
|
|
@@ -365,7 +394,9 @@ export class ReviewWorkspaces {
|
|
|
365
394
|
|
|
366
395
|
// A trashed review keeps its record, so collection would skip it.
|
|
367
396
|
if (this.store.has(environment.reviewId))
|
|
368
|
-
this.release(environment.reviewId)
|
|
397
|
+
this.release(environment.reviewId, this.repositoryDirs(), () =>
|
|
398
|
+
this.stillTrashed(environment.reviewId),
|
|
399
|
+
);
|
|
369
400
|
else this.collect(id);
|
|
370
401
|
await this.cleanup;
|
|
371
402
|
}
|
|
@@ -647,6 +678,20 @@ export class ReviewWorkspaces {
|
|
|
647
678
|
if (environment.rootPath)
|
|
648
679
|
await removeReviewPrepareArtifacts(environment.rootPath);
|
|
649
680
|
|
|
681
|
+
if (
|
|
682
|
+
environment.repository &&
|
|
683
|
+
!this.all().some(
|
|
684
|
+
(other) =>
|
|
685
|
+
other.id !== environment.id &&
|
|
686
|
+
other.reviewId === environment.reviewId &&
|
|
687
|
+
other.repository === environment.repository,
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
await removeReviewManagedCheckouts(
|
|
691
|
+
environment.repository,
|
|
692
|
+
environment.reviewId,
|
|
693
|
+
);
|
|
694
|
+
|
|
650
695
|
this.db
|
|
651
696
|
.prepare("DELETE FROM pinned_environments WHERE id=?")
|
|
652
697
|
.run(environment.id);
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { type SpawnOptions, spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { type SpawnOptions, execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fstatSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
11
|
import path from "node:path";
|
|
5
12
|
|
|
6
13
|
import {
|
|
@@ -19,10 +26,12 @@ import {
|
|
|
19
26
|
const RELEASE_APPS = {
|
|
20
27
|
stable: {
|
|
21
28
|
bundleId: "dev.fast.review",
|
|
29
|
+
macApp: "Whiteboard.app",
|
|
22
30
|
linuxLauncher: "/usr/bin/review-desktop",
|
|
23
31
|
},
|
|
24
32
|
preview: {
|
|
25
33
|
bundleId: "dev.fast.review.preview",
|
|
34
|
+
macApp: "Whiteboard Preview.app",
|
|
26
35
|
linuxLauncher: "/usr/bin/review-preview-desktop",
|
|
27
36
|
},
|
|
28
37
|
};
|
|
@@ -227,6 +236,55 @@ export async function focusReviewDesktop(
|
|
|
227
236
|
}
|
|
228
237
|
}
|
|
229
238
|
|
|
239
|
+
export function desktopApplicationInstalled(
|
|
240
|
+
input: Pick<
|
|
241
|
+
LaunchDesktopApplicationInput,
|
|
242
|
+
"platform" | "electron" | "env"
|
|
243
|
+
> & {
|
|
244
|
+
exists?: (path: string) => boolean;
|
|
245
|
+
spotlight?: (bundleId: string) => boolean;
|
|
246
|
+
} = {},
|
|
247
|
+
): boolean {
|
|
248
|
+
const env = input.env ?? process.env;
|
|
249
|
+
const platform = input.platform ?? process.platform;
|
|
250
|
+
const exists = input.exists ?? existsSync;
|
|
251
|
+
const apps = Object.values(RELEASE_APPS);
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
(platform !== "linux" && platform !== "darwin") ||
|
|
255
|
+
(input.electron ?? Boolean(process.versions.electron)) ||
|
|
256
|
+
env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()
|
|
257
|
+
)
|
|
258
|
+
return true;
|
|
259
|
+
|
|
260
|
+
if (platform === "linux")
|
|
261
|
+
return apps.some(({ linuxLauncher }) => exists(linuxLauncher));
|
|
262
|
+
|
|
263
|
+
// Spotlight can be off on a headless Mac, so the usual folders come first.
|
|
264
|
+
return (
|
|
265
|
+
apps.some(({ macApp }) =>
|
|
266
|
+
["/Applications", path.join(homedir(), "Applications")].some((dir) =>
|
|
267
|
+
exists(path.join(dir, macApp)),
|
|
268
|
+
),
|
|
269
|
+
) ||
|
|
270
|
+
apps.some(({ bundleId }) => (input.spotlight ?? spotlightFinds)(bundleId))
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function spotlightFinds(bundleId: string): boolean {
|
|
275
|
+
try {
|
|
276
|
+
return (
|
|
277
|
+
execFileSync(
|
|
278
|
+
"/usr/bin/mdfind",
|
|
279
|
+
[`kMDItemCFBundleIdentifier == '${bundleId}'`],
|
|
280
|
+
{ encoding: "utf8", timeout: 2_000 },
|
|
281
|
+
).trim() !== ""
|
|
282
|
+
);
|
|
283
|
+
} catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
230
288
|
export function launchDesktopApplication(
|
|
231
289
|
input: LaunchDesktopApplicationInput = {},
|
|
232
290
|
): DesktopLaunchAttempt {
|