@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/cli-runner.ts
CHANGED
|
@@ -2,19 +2,22 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import
|
|
5
|
+
import { Writable } from "node:stream";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
REVIEW_CLI_INSTALL_TARGET_LABELS,
|
|
10
|
+
REVIEW_REMOTE_ATTACH_BEGIN,
|
|
11
|
+
REVIEW_REMOTE_ATTACH_END,
|
|
12
|
+
} from "@dev.fast/review-protocol";
|
|
8
13
|
import {
|
|
9
14
|
StoreApiError,
|
|
10
|
-
processIsAlive,
|
|
11
15
|
readStoreAuth,
|
|
12
16
|
withStoreAuthorization,
|
|
13
17
|
} from "@dev.fast/trace-core";
|
|
14
18
|
import {
|
|
15
19
|
type CliInputStream,
|
|
16
20
|
DEFAULT_STORE_ORIGIN,
|
|
17
|
-
DEV_REVIEW_HOME_ENV,
|
|
18
21
|
emitJsonEvent,
|
|
19
22
|
humanStream,
|
|
20
23
|
jsonRequestedInArgv,
|
|
@@ -42,10 +45,16 @@ import {
|
|
|
42
45
|
windowsInstallerCommand,
|
|
43
46
|
} from "./cli-install";
|
|
44
47
|
import { cliRuntimeInfo, describeCliRuntime } from "./cli-runtime-info";
|
|
48
|
+
import {
|
|
49
|
+
AGENT_CONNECT_TARGETS,
|
|
50
|
+
type AgentConnectTarget,
|
|
51
|
+
connectAgents,
|
|
52
|
+
detectAgents,
|
|
53
|
+
} from "./connect-agents";
|
|
45
54
|
import { connectPrompts } from "./connect-prompts";
|
|
46
55
|
import {
|
|
47
56
|
ReviewInstanceUnavailableError,
|
|
48
|
-
|
|
57
|
+
desktopUsesHome,
|
|
49
58
|
selectReviewInstance,
|
|
50
59
|
} from "./desktop-discovery";
|
|
51
60
|
import {
|
|
@@ -86,6 +95,8 @@ import {
|
|
|
86
95
|
type ReviewTelemetryErrorName,
|
|
87
96
|
} from "./review-telemetry";
|
|
88
97
|
import {
|
|
98
|
+
DESKTOP_TAKEOVER_SIGNAL,
|
|
99
|
+
type ReviewServerDiscovery,
|
|
89
100
|
readReviewServerDiscovery,
|
|
90
101
|
readReviewServerHealth,
|
|
91
102
|
reviewServerIsHealthy,
|
|
@@ -265,6 +276,16 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
265
276
|
? { ...env, DEV_REVIEW_SERVER_DIR: path.resolve(cwd, stateDir) }
|
|
266
277
|
: env;
|
|
267
278
|
|
|
279
|
+
const refuse = (message: string, json?: boolean) => {
|
|
280
|
+
if (json)
|
|
281
|
+
emitReviewEvent(input.stdout, {
|
|
282
|
+
event: "error",
|
|
283
|
+
error: { name: "Error", message },
|
|
284
|
+
});
|
|
285
|
+
else input.stderr.write(`${message}\n`);
|
|
286
|
+
state.exitCode = 1;
|
|
287
|
+
};
|
|
288
|
+
|
|
268
289
|
const serverCommand = configureOutput(
|
|
269
290
|
program
|
|
270
291
|
.command("server")
|
|
@@ -291,6 +312,16 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
291
312
|
"--software-maps",
|
|
292
313
|
"allow the authoring skill to generate optional software maps",
|
|
293
314
|
)
|
|
315
|
+
.option(
|
|
316
|
+
"--detach",
|
|
317
|
+
"start the server in the background, logging to review-server/server.log",
|
|
318
|
+
)
|
|
319
|
+
.addOption(
|
|
320
|
+
new Option("--started-by <who>")
|
|
321
|
+
.choices(["user", "cli", "desktop"])
|
|
322
|
+
.default("user")
|
|
323
|
+
.hideHelp(),
|
|
324
|
+
)
|
|
294
325
|
// Batch authoring was removed; name that instead of "unknown option".
|
|
295
326
|
.addOption(new Option("--authoring-mode <mode>").hideHelp()),
|
|
296
327
|
"plain",
|
|
@@ -299,6 +330,8 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
299
330
|
stateDir?: string;
|
|
300
331
|
port: string;
|
|
301
332
|
softwareMaps?: boolean;
|
|
333
|
+
detach?: boolean;
|
|
334
|
+
startedBy: "user" | "cli" | "desktop";
|
|
302
335
|
authoringMode?: string;
|
|
303
336
|
json?: boolean;
|
|
304
337
|
}>();
|
|
@@ -314,17 +347,49 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
314
347
|
"--port must be an integer between 0 and 65535.",
|
|
315
348
|
);
|
|
316
349
|
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
350
|
+
|
|
351
|
+
if (options.detach) {
|
|
352
|
+
const { ensureBackgroundServer } =
|
|
353
|
+
await import("./server/background-server.js");
|
|
354
|
+
|
|
355
|
+
const { discovery, started } = await ensureBackgroundServer({
|
|
356
|
+
stateDir,
|
|
357
|
+
env,
|
|
358
|
+
args: [
|
|
359
|
+
"--port",
|
|
360
|
+
`${port}`,
|
|
361
|
+
...(options.softwareMaps ? ["--software-maps"] : []),
|
|
362
|
+
],
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await writeServerStatus(discovery, stateDir, options.json, { started });
|
|
366
|
+
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
317
370
|
const controller = new AbortController();
|
|
318
371
|
const stop = () => controller.abort();
|
|
372
|
+
|
|
373
|
+
const takeover = () => {
|
|
374
|
+
input.stderr.write(
|
|
375
|
+
`Whiteboard Desktop took over ${stateDir}; stopping this server.\n`,
|
|
376
|
+
);
|
|
377
|
+
stop();
|
|
378
|
+
};
|
|
379
|
+
|
|
319
380
|
process.once("SIGINT", stop);
|
|
320
381
|
process.once("SIGTERM", stop);
|
|
382
|
+
process.once(DESKTOP_TAKEOVER_SIGNAL, takeover);
|
|
383
|
+
|
|
384
|
+
const { HeadlessServerBusyError, runHeadlessServer } =
|
|
385
|
+
await import("./server/headless-host.js");
|
|
321
386
|
|
|
322
387
|
try {
|
|
323
|
-
const { runHeadlessServer } = await import("./server/headless-host.js");
|
|
324
388
|
await runHeadlessServer({
|
|
325
389
|
stateDir,
|
|
326
390
|
port,
|
|
327
391
|
softwareMapEnabled: options.softwareMaps,
|
|
392
|
+
startedBy: options.startedBy,
|
|
328
393
|
signal: controller.signal,
|
|
329
394
|
telemetry,
|
|
330
395
|
onReady: ({ url, serverPid }) => {
|
|
@@ -335,9 +400,13 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
335
400
|
);
|
|
336
401
|
},
|
|
337
402
|
});
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (!(error instanceof HeadlessServerBusyError)) throw error;
|
|
405
|
+
refuse(error.message, options.json);
|
|
338
406
|
} finally {
|
|
339
407
|
process.off("SIGINT", stop);
|
|
340
408
|
process.off("SIGTERM", stop);
|
|
409
|
+
process.off(DESKTOP_TAKEOVER_SIGNAL, takeover);
|
|
341
410
|
}
|
|
342
411
|
});
|
|
343
412
|
|
|
@@ -358,15 +427,64 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
358
427
|
|
|
359
428
|
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
360
429
|
const discovery = await readReviewServerDiscovery(stateDir);
|
|
361
|
-
const health = discovery && (await readReviewServerHealth(discovery));
|
|
362
430
|
|
|
363
|
-
if (!discovery
|
|
364
|
-
|
|
365
|
-
|
|
431
|
+
if (!discovery) throw serverNotReady(stateDir);
|
|
432
|
+
await writeServerStatus(discovery, stateDir, options.json);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
configureJsonOutput(
|
|
436
|
+
serverCommand
|
|
437
|
+
.command("stop")
|
|
438
|
+
.description(
|
|
439
|
+
"Stop a background server started by --detach, the CLI, or Desktop",
|
|
440
|
+
)
|
|
441
|
+
.option(
|
|
442
|
+
"--state-dir <path>",
|
|
443
|
+
"directory selected when starting the server",
|
|
444
|
+
),
|
|
445
|
+
"plain",
|
|
446
|
+
).action(async (_options, command: Command) => {
|
|
447
|
+
const options = command.optsWithGlobals<{
|
|
448
|
+
stateDir?: string;
|
|
449
|
+
json?: boolean;
|
|
450
|
+
}>();
|
|
451
|
+
|
|
452
|
+
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
453
|
+
|
|
454
|
+
const { headlessServerOwner, stopBackgroundServer } =
|
|
455
|
+
await import("./server/background-server.js");
|
|
456
|
+
|
|
457
|
+
const owner = await headlessServerOwner(stateDir);
|
|
458
|
+
const discovery = await readReviewServerDiscovery(stateDir);
|
|
459
|
+
|
|
460
|
+
if (owner === undefined) {
|
|
461
|
+
input.stdout.write(
|
|
462
|
+
options.json
|
|
463
|
+
? `${JSON.stringify({ event: "server.stop", stopped: false, stateDir })}\n`
|
|
464
|
+
: `No Whiteboard server is running in ${stateDir}.\n`,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const serverPid = owner;
|
|
471
|
+
|
|
472
|
+
if (discovery?.serverPid !== owner)
|
|
473
|
+
throw new Error(
|
|
474
|
+
`Process ${owner} holds the Whiteboard server's lock in ${stateDir} but has published no server; it may be starting. Try again, or end that process.`,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
if (discovery.startedBy === "user")
|
|
478
|
+
throw new Error(
|
|
479
|
+
`The Whiteboard server in ${stateDir} (process ${serverPid}) runs in the foreground of \`whiteboard server start\`. Stop it there with Ctrl-C.`,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
await stopBackgroundServer(discovery);
|
|
483
|
+
|
|
366
484
|
input.stdout.write(
|
|
367
485
|
options.json
|
|
368
|
-
? `${JSON.stringify({ event: "server.
|
|
369
|
-
: `Whiteboard server
|
|
486
|
+
? `${JSON.stringify({ event: "server.stop", stopped: true, serverPid, stateDir })}\n`
|
|
487
|
+
: `Stopped the Whiteboard server (process ${serverPid}).\n`,
|
|
370
488
|
);
|
|
371
489
|
});
|
|
372
490
|
|
|
@@ -394,32 +512,10 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
394
512
|
`No saved reviews in ${stateDir}, so there is no server id to reset. Check --state-dir.`,
|
|
395
513
|
);
|
|
396
514
|
|
|
397
|
-
|
|
398
|
-
// process counts as stopped: a paused or busy one may not answer /health.
|
|
399
|
-
const desktops = await readReviewInstances({
|
|
400
|
-
env: { ...env, [DEV_REVIEW_HOME_ENV]: stateDir },
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
const [problem] = desktops.broken.values();
|
|
404
|
-
|
|
405
|
-
if (problem)
|
|
406
|
-
throw new Error(
|
|
407
|
-
`Cannot tell whether a Whiteboard Desktop is using ${stateDir}: ${problem.message}`,
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
const inUse = () =>
|
|
411
|
-
new Error(
|
|
412
|
-
`A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`,
|
|
413
|
-
);
|
|
515
|
+
const inUse = `A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`;
|
|
414
516
|
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
[discovery.appPid, discovery.serverPid].some((pid) =>
|
|
418
|
-
processIsAlive(pid),
|
|
419
|
-
),
|
|
420
|
-
)
|
|
421
|
-
)
|
|
422
|
-
throw inUse();
|
|
517
|
+
if (await desktopUsesHome(stateDir, env))
|
|
518
|
+
return refuse(inUse, options.json);
|
|
423
519
|
|
|
424
520
|
const [{ openReviewProfile }, { withHeadlessServerLock }] =
|
|
425
521
|
await Promise.all([
|
|
@@ -445,7 +541,7 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
445
541
|
},
|
|
446
542
|
);
|
|
447
543
|
|
|
448
|
-
if (!reset.acquired)
|
|
544
|
+
if (!reset.acquired) return refuse(inUse, options.json);
|
|
449
545
|
const serverId = reset.result;
|
|
450
546
|
|
|
451
547
|
input.stdout.write(
|
|
@@ -455,6 +551,239 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
455
551
|
);
|
|
456
552
|
});
|
|
457
553
|
|
|
554
|
+
const remote = configureOutput(
|
|
555
|
+
program
|
|
556
|
+
.command("remote")
|
|
557
|
+
.description("Serve reviews to Whiteboard Desktop over SSH"),
|
|
558
|
+
"plain",
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
configureJsonOutput(
|
|
562
|
+
remote
|
|
563
|
+
.command("attach")
|
|
564
|
+
.description(
|
|
565
|
+
"Start a background server if none is healthy and report how to reach it",
|
|
566
|
+
)
|
|
567
|
+
.option(
|
|
568
|
+
"--state-dir <path>",
|
|
569
|
+
"directory for saved reviews and server discovery",
|
|
570
|
+
)
|
|
571
|
+
.option(
|
|
572
|
+
"--groups <groups>",
|
|
573
|
+
"comma-separated optional extension groups the Desktop has enabled, such as go",
|
|
574
|
+
)
|
|
575
|
+
.option(
|
|
576
|
+
"--replace",
|
|
577
|
+
"stop a running server of another version that the CLI or Desktop started, and start this one",
|
|
578
|
+
),
|
|
579
|
+
"plain",
|
|
580
|
+
).action(async (_options, command: Command) => {
|
|
581
|
+
const options = command.optsWithGlobals<{
|
|
582
|
+
stateDir?: string;
|
|
583
|
+
json?: boolean;
|
|
584
|
+
groups?: string;
|
|
585
|
+
replace?: boolean;
|
|
586
|
+
}>();
|
|
587
|
+
|
|
588
|
+
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
589
|
+
|
|
590
|
+
const { remoteAttach } = await import("./remote-attach.js");
|
|
591
|
+
|
|
592
|
+
let attach: Awaited<ReturnType<typeof remoteAttach>>;
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
attach = await remoteAttach({
|
|
596
|
+
stateDir,
|
|
597
|
+
env,
|
|
598
|
+
groups: options.groups?.split(",").map((group) => group.trim()),
|
|
599
|
+
replace: options.replace,
|
|
600
|
+
});
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (!options.json) throw error;
|
|
603
|
+
|
|
604
|
+
let line = "";
|
|
605
|
+
|
|
606
|
+
emitReviewEvent(
|
|
607
|
+
new Writable({
|
|
608
|
+
write(chunk, _encoding, done) {
|
|
609
|
+
line += chunk;
|
|
610
|
+
done();
|
|
611
|
+
},
|
|
612
|
+
}),
|
|
613
|
+
{ event: "error", error: serializeReviewError(error) },
|
|
614
|
+
);
|
|
615
|
+
input.stdout.write(
|
|
616
|
+
`${REVIEW_REMOTE_ATTACH_BEGIN}\n${ensureTrailingNewline(line)}${REVIEW_REMOTE_ATTACH_END}\n`,
|
|
617
|
+
);
|
|
618
|
+
state.exitCode = 1;
|
|
619
|
+
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
input.stdout.write(
|
|
624
|
+
options.json
|
|
625
|
+
? `${REVIEW_REMOTE_ATTACH_BEGIN}\n${JSON.stringify(attach)}\n${REVIEW_REMOTE_ATTACH_END}\n`
|
|
626
|
+
: `Whiteboard server ${attach.startedServer ? "started" : "already running"} at ${attach.url}\nLanguage features: ${attach.languageServer ? `VS Code server on port ${attach.languageServer.port}` : `unavailable (${attach.languageServerDetail})`}\n`,
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
configureJsonOutput(
|
|
631
|
+
remote
|
|
632
|
+
.command("extensions")
|
|
633
|
+
.description("Language extensions for the VS Code server on a remote")
|
|
634
|
+
.command("ensure")
|
|
635
|
+
.description(
|
|
636
|
+
"Download the curated extensions for this machine and list them for the VS Code server",
|
|
637
|
+
)
|
|
638
|
+
.option(
|
|
639
|
+
"--groups <groups>",
|
|
640
|
+
"comma-separated optional extension groups the Desktop has enabled, such as go",
|
|
641
|
+
),
|
|
642
|
+
"plain",
|
|
643
|
+
).action(async (_options, command: Command) => {
|
|
644
|
+
const { json, groups } = command.optsWithGlobals<{
|
|
645
|
+
json?: boolean;
|
|
646
|
+
groups?: string;
|
|
647
|
+
}>();
|
|
648
|
+
|
|
649
|
+
const { ensureRemoteExtensions } = await import("./remote-extensions.js");
|
|
650
|
+
|
|
651
|
+
const result = await ensureRemoteExtensions({
|
|
652
|
+
env,
|
|
653
|
+
groups: groups?.split(",").map((group) => group.trim()),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
input.stdout.write(
|
|
657
|
+
json
|
|
658
|
+
? `${JSON.stringify(result)}\n`
|
|
659
|
+
: [
|
|
660
|
+
...result.installed.map((id) => `Installed ${id}`),
|
|
661
|
+
...result.skipped.map(
|
|
662
|
+
({ id, reason }) => `Skipped ${id}: ${reason}`,
|
|
663
|
+
),
|
|
664
|
+
...result.failed.map(({ id, error }) => `Failed ${id}: ${error}`),
|
|
665
|
+
]
|
|
666
|
+
.map((line) => `${line}\n`)
|
|
667
|
+
.join(""),
|
|
668
|
+
);
|
|
669
|
+
state.exitCode = result.failed.length > 0 ? 1 : 0;
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
configureJsonOutput(
|
|
673
|
+
remote
|
|
674
|
+
.command("uninstall")
|
|
675
|
+
.description(
|
|
676
|
+
"Remove what Whiteboard Desktop installed on this host, stopping a server it started",
|
|
677
|
+
)
|
|
678
|
+
.addOption(
|
|
679
|
+
new Option(
|
|
680
|
+
"--keep-reviews",
|
|
681
|
+
"leave the saved reviews in place",
|
|
682
|
+
).conflicts("deleteReviews"),
|
|
683
|
+
)
|
|
684
|
+
.addOption(
|
|
685
|
+
new Option("--delete-reviews", "also delete the saved reviews"),
|
|
686
|
+
)
|
|
687
|
+
.option(
|
|
688
|
+
"--state-dir <path>",
|
|
689
|
+
"directory for saved reviews and server discovery",
|
|
690
|
+
),
|
|
691
|
+
"plain",
|
|
692
|
+
).action(async (_options, command: Command) => {
|
|
693
|
+
const options = command.optsWithGlobals<{
|
|
694
|
+
stateDir?: string;
|
|
695
|
+
json?: boolean;
|
|
696
|
+
keepReviews?: boolean;
|
|
697
|
+
deleteReviews?: boolean;
|
|
698
|
+
}>();
|
|
699
|
+
|
|
700
|
+
const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
|
|
701
|
+
const { remoteUninstall } = await import("./remote-uninstall.js");
|
|
702
|
+
|
|
703
|
+
let deleteReviews = options.deleteReviews
|
|
704
|
+
? true
|
|
705
|
+
: options.keepReviews
|
|
706
|
+
? false
|
|
707
|
+
: undefined;
|
|
708
|
+
|
|
709
|
+
if (deleteReviews === undefined && !options.json && input.stdin?.isTTY) {
|
|
710
|
+
const prompt = createInterface({
|
|
711
|
+
input: input.stdin,
|
|
712
|
+
output: input.stderr,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
deleteReviews = /^y(es)?$/i.test(
|
|
717
|
+
(
|
|
718
|
+
await prompt.question(
|
|
719
|
+
`Also delete the saved reviews in ${stateDir}? [y/N] `,
|
|
720
|
+
)
|
|
721
|
+
).trim(),
|
|
722
|
+
);
|
|
723
|
+
} finally {
|
|
724
|
+
prompt.close();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const result =
|
|
729
|
+
deleteReviews === undefined
|
|
730
|
+
? {
|
|
731
|
+
event: "remote.uninstall" as const,
|
|
732
|
+
ok: false as const,
|
|
733
|
+
reason:
|
|
734
|
+
"Say what happens to the saved reviews: pass --keep-reviews or --delete-reviews.",
|
|
735
|
+
}
|
|
736
|
+
: await remoteUninstall({
|
|
737
|
+
home: env.HOME ?? "",
|
|
738
|
+
env,
|
|
739
|
+
stateDir,
|
|
740
|
+
deleteReviews,
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
if (!result.ok) {
|
|
744
|
+
if (!options.json) throw new Error(result.reason);
|
|
745
|
+
input.stdout.write(`${JSON.stringify(result)}\n`);
|
|
746
|
+
state.exitCode = 1;
|
|
747
|
+
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
input.stdout.write(
|
|
752
|
+
options.json
|
|
753
|
+
? `${JSON.stringify(result)}\n`
|
|
754
|
+
: [
|
|
755
|
+
...(result.stoppedServer
|
|
756
|
+
? [
|
|
757
|
+
`Stopped the Whiteboard server (process ${result.stoppedServer.pid}).`,
|
|
758
|
+
]
|
|
759
|
+
: []),
|
|
760
|
+
...result.removed.map((removed) => `Removed ${removed}`),
|
|
761
|
+
result.keptReviews
|
|
762
|
+
? `Saved reviews kept in ${stateDir}`
|
|
763
|
+
: "Saved reviews deleted.",
|
|
764
|
+
"",
|
|
765
|
+
].join("\n"),
|
|
766
|
+
);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
async function writeServerStatus(
|
|
770
|
+
discovery: ReviewServerDiscovery,
|
|
771
|
+
stateDir: string,
|
|
772
|
+
json: boolean | undefined,
|
|
773
|
+
extra: { started?: boolean } = {},
|
|
774
|
+
) {
|
|
775
|
+
const health = await readReviewServerHealth(discovery);
|
|
776
|
+
|
|
777
|
+
if (!health) throw serverNotReady(stateDir);
|
|
778
|
+
const { url, serverPid } = discovery;
|
|
779
|
+
const { version, serverId } = health;
|
|
780
|
+
input.stdout.write(
|
|
781
|
+
json
|
|
782
|
+
? `${JSON.stringify({ event: "server.status", ready: true, url, serverPid, stateDir, version, serverId, ...extra })}\n`
|
|
783
|
+
: `Whiteboard server ${extra.started === false ? "already running" : "ready"} at ${url}\nSaved reviews: ${stateDir}\n`,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
458
787
|
configureJsonOutput(
|
|
459
788
|
program
|
|
460
789
|
.command("version")
|
|
@@ -662,53 +991,125 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
662
991
|
"copilot",
|
|
663
992
|
"all",
|
|
664
993
|
]),
|
|
994
|
+
)
|
|
995
|
+
.addOption(
|
|
996
|
+
new Option(
|
|
997
|
+
"--detect",
|
|
998
|
+
"list the agents on this machine and whether each is connected; changes nothing",
|
|
999
|
+
).conflicts("yes"),
|
|
1000
|
+
)
|
|
1001
|
+
.option(
|
|
1002
|
+
"--yes",
|
|
1003
|
+
"run the commands the prompt gives, without an agent: claude, codex, opencode or pi",
|
|
665
1004
|
),
|
|
666
1005
|
"plain",
|
|
667
1006
|
);
|
|
668
1007
|
|
|
669
|
-
connect.action(
|
|
670
|
-
|
|
1008
|
+
connect.action(
|
|
1009
|
+
async (
|
|
1010
|
+
targets: string[],
|
|
1011
|
+
options: { json?: boolean; detect?: boolean; yes?: boolean },
|
|
1012
|
+
) => {
|
|
1013
|
+
const output = {
|
|
1014
|
+
json: options.json,
|
|
1015
|
+
stdout: input.stdout,
|
|
1016
|
+
stderr: input.stderr,
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
if (options.detect) {
|
|
1020
|
+
const agents = await detectAgents({ homeDir: scope.homeDir, env });
|
|
1021
|
+
|
|
1022
|
+
if (options.json) {
|
|
1023
|
+
emitJsonEvent(output, { event: "connect.detect", agents });
|
|
1024
|
+
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
humanStream(output).write(
|
|
1029
|
+
agents.length
|
|
1030
|
+
? `${agents.map((agent) => `${agent.name}: ${agent.connected ? "connected" : "not connected"}${agent.manual ? " (its command is not on PATH)" : ""}`).join("\n")}\n`
|
|
1031
|
+
: "No agents found.\n",
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
671
1036
|
|
|
672
|
-
|
|
1037
|
+
if (options.yes) {
|
|
1038
|
+
const agents = parseTargets(targets);
|
|
673
1039
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1040
|
+
if (
|
|
1041
|
+
targets.length === 0 ||
|
|
1042
|
+
!agents.every((target): target is AgentConnectTarget =>
|
|
1043
|
+
AGENT_CONNECT_TARGETS.some((known) => known === target),
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
"connect --yes takes one or more of claude, codex, opencode or pi.",
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
const results = await connectAgents({
|
|
1051
|
+
agents,
|
|
1052
|
+
homeDir: scope.homeDir,
|
|
680
1053
|
env,
|
|
681
|
-
)
|
|
682
|
-
traceEnabled: await traceMachineEnabled({ homeDir, env }),
|
|
683
|
-
fffBinaryPath: path.join(homeDir, ".local", "bin", "fff-mcp"),
|
|
684
|
-
fffCorpusRoot: path.join(devHome, "trace-search"),
|
|
685
|
-
});
|
|
1054
|
+
});
|
|
686
1055
|
|
|
687
|
-
|
|
688
|
-
json: options.json,
|
|
689
|
-
stdout: input.stdout,
|
|
690
|
-
stderr: input.stderr,
|
|
691
|
-
};
|
|
1056
|
+
state.exitCode = results.every((result) => result.connected) ? 0 : 1;
|
|
692
1057
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1058
|
+
if (options.json) {
|
|
1059
|
+
emitJsonEvent(output, { event: "connect.run", agents: results });
|
|
1060
|
+
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
humanStream(output).write(
|
|
1065
|
+
results
|
|
1066
|
+
.map(
|
|
1067
|
+
(result) =>
|
|
1068
|
+
`${result.output}${result.name}: ${result.connected ? "connected" : "not connected"}\n`,
|
|
1069
|
+
)
|
|
1070
|
+
.join(""),
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const selected = parseTargets(targets);
|
|
1077
|
+
|
|
1078
|
+
const { homeDir, devHome } = scope;
|
|
1079
|
+
|
|
1080
|
+
const prompts = connectPrompts({
|
|
1081
|
+
legacyPaths: await scanLegacySkills(homeDir),
|
|
1082
|
+
hasShim:
|
|
1083
|
+
(await isOwnedShim(pathShimPath(homeDir))) ||
|
|
1084
|
+
(await windowsInstallerCommand(
|
|
1085
|
+
findReviewPackageRoot(import.meta.url),
|
|
1086
|
+
env,
|
|
1087
|
+
)) !== undefined,
|
|
1088
|
+
traceEnabled: await traceMachineEnabled({ homeDir, env }),
|
|
1089
|
+
fffBinaryPath: path.join(homeDir, ".local", "bin", "fff-mcp"),
|
|
1090
|
+
fffCorpusRoot: path.join(devHome, "trace-search"),
|
|
699
1091
|
});
|
|
700
1092
|
|
|
701
|
-
|
|
702
|
-
|
|
1093
|
+
if (options.json) {
|
|
1094
|
+
emitJsonEvent(output, {
|
|
1095
|
+
event: "connect",
|
|
1096
|
+
prompts: Object.fromEntries(
|
|
1097
|
+
selected.map((target) => [target, prompts[target]]),
|
|
1098
|
+
),
|
|
1099
|
+
});
|
|
703
1100
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
? `## ${TARGET_LABELS[target]}\n\n${prompts[target]}`
|
|
707
|
-
: prompts[target],
|
|
708
|
-
);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
709
1103
|
|
|
710
|
-
|
|
711
|
-
|
|
1104
|
+
const sections = selected.map((target) =>
|
|
1105
|
+
selected.length > 1
|
|
1106
|
+
? `## ${REVIEW_CLI_INSTALL_TARGET_LABELS[target]}\n\n${prompts[target]}`
|
|
1107
|
+
: prompts[target],
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
humanStream(output).write(`${sections.join("\n\n")}\n`);
|
|
1111
|
+
},
|
|
1112
|
+
);
|
|
712
1113
|
|
|
713
1114
|
const migrate = configureOutput(
|
|
714
1115
|
program.command("migrate", { hidden: true }),
|
|
@@ -1137,16 +1538,6 @@ export async function runReviewCli(input: ReviewCliInput): Promise<number> {
|
|
|
1137
1538
|
}
|
|
1138
1539
|
}
|
|
1139
1540
|
|
|
1140
|
-
const TARGET_LABELS: Record<InstallTarget, string> = {
|
|
1141
|
-
claude: "Claude Code",
|
|
1142
|
-
codex: "Codex",
|
|
1143
|
-
cursor: "Cursor",
|
|
1144
|
-
opencode: "OpenCode",
|
|
1145
|
-
pi: "Pi",
|
|
1146
|
-
omp: "oh-my-pi",
|
|
1147
|
-
copilot: "Copilot CLI",
|
|
1148
|
-
};
|
|
1149
|
-
|
|
1150
1541
|
function parseTargets(targets: readonly string[]): InstallTarget[] {
|
|
1151
1542
|
if (targets.length === 0 || targets.includes("all")) {
|
|
1152
1543
|
return [...ALL_INSTALL_TARGETS];
|
package/src/cli.ts
CHANGED
|
@@ -71,7 +71,7 @@ async function maybeDelegateToDesktopCli(
|
|
|
71
71
|
if (
|
|
72
72
|
argv.some(
|
|
73
73
|
(argument) =>
|
|
74
|
-
["api", "mcp", "server", "instances"].includes(argument) ||
|
|
74
|
+
["api", "mcp", "server", "remote", "instances"].includes(argument) ||
|
|
75
75
|
/^--state-dir(?:=|$)/.test(argument),
|
|
76
76
|
) ||
|
|
77
77
|
env.DEV_REVIEW_SERVER_DIR?.trim()
|