@treeport/treeport 0.5.0 → 0.7.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.
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { A as treeportVersion, C as disableTailscaleRemote, D as resolvePackagePath, E as resolveLocalApiUrl, M as parseDurationMs, N as TERMINAL_CAPTURE_MAX_LINES, O as runDoctor, S as daemonUp, T as readDaemonLogs, _ as serviceStatus, b as daemonHealth, ct as parseEventsSnapshot, d as serviceDisable, f as serviceDoctorCheck, g as serviceStart, h as serviceRun, k as tailscaleRemoteStatus, l as readServiceLogs, lt as parseProductEvent, m as serviceInstalled, p as serviceEnable, s as runLocalUpdate, st as webPanelInputSchema, t as LocalUpdateError, u as serviceApply, ut as SOCKET_IO_PATH, v as serviceStop, w as enableTailscaleRemote, x as daemonStatus, y as daemonDown } from "../../update-BW-a6Bd-.js";
2
+ import { Ct as projectsResponseSchema, Dt as terminalCaptureResponseSchema, G as treeportRpcClientLayer, Gt as webPanelDefinitionsResponseSchema, K as TreeportRpcs, Lt as treeContextResponseSchema, Nt as terminalResponseSchema, Ot as terminalObservationResponseSchema, Pt as terminatedTerminalsResponseSchema, St as projectResponseSchema, Tt as removePreviewResponseSchema, U as decodeUnknownOrNull, V as webPanelInputSchema, _t as packageOperationResponseSchema, bt as packageReloadResponseSchema, dt as openBrowserPanelResponseSchema, et as browserAgentResponseSchema, ft as openWebPanelResponseSchema, gt as packageListingResponseSchema, mt as operationResponseSchema, nt as browserInstallStatusSchema, o as apiErrorBodySchema, t as TERMINAL_CAPTURE_MAX_LINES, tt as browserInstallResponseSchema, ut as okResponseSchema, vt as packageOperationsResponseSchema, yt as packageProjectResponseSchema } from "../../dist-BsLn2Gbc.js";
3
+ import { A as tailscaleRemoteStatus, C as daemonUp, D as resolveLocalApiUrl, E as readDaemonLogs, N as parseDurationMs, O as resolvePackagePath, S as daemonStatus, T as enableTailscaleRemote, _ as serviceStart, b as daemonDown, c as runLocalUpdate, d as serviceApply, f as serviceDisable, g as serviceRun, h as serviceInstalled, j as treeportVersion, k as runDoctor, m as serviceEnable, p as serviceDoctorCheck, r as formatLocalUpdateError, t as LocalUpdateError, u as readServiceLogs, v as serviceStatus, w as disableTailscaleRemote, x as daemonHealth, y as serviceStop } from "../../update-UYS2lMdD.js";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import { Command, CommanderError } from "commander";
6
- import { io } from "socket.io-client";
7
+ import { RpcClient } from "@effect/rpc";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Fiber from "effect/Fiber";
10
+ import * as Stream from "effect/Stream";
7
11
  import { spawn } from "node:child_process";
8
12
  //#region src/cli/args.ts
9
13
  function extractJsonOutput(args) {
@@ -145,7 +149,7 @@ async function ensureServiceDaemon() {
145
149
  pid: state.pid
146
150
  };
147
151
  }
148
- async function request(pathname, options = {}) {
152
+ async function request(pathname, schema, options = {}) {
149
153
  const controller = new AbortController();
150
154
  const externalSignal = options.signal;
151
155
  const abort = () => controller.abort();
@@ -163,10 +167,12 @@ async function request(pathname, options = {}) {
163
167
  });
164
168
  const body = await response.json().catch(() => ({}));
165
169
  if (!response.ok) {
166
- const error = body.error;
167
- throw new CliError(error?.message || `HTTP ${response.status}`, 5, error?.code || "API_ERROR", error?.details);
170
+ const failure = decodeUnknownOrNull(apiErrorBodySchema, body);
171
+ throw new CliError(failure?.error.message || `HTTP ${response.status}`, 5, failure?.error.code || "API_ERROR", failure?.error.details);
168
172
  }
169
- return body;
173
+ const decoded = decodeUnknownOrNull(schema, body);
174
+ if (decoded === null) throw new CliError(`Treeport daemon returned an invalid response for ${pathname}`, 3, "DAEMON_PROTOCOL_ERROR", { pathname });
175
+ return decoded;
170
176
  } catch (error) {
171
177
  if (error instanceof CliError) throw error;
172
178
  throw new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error instanceof Error ? error.message : String(error)}`, 3, "DAEMON_UNREACHABLE");
@@ -176,19 +182,19 @@ async function request(pathname, options = {}) {
176
182
  }
177
183
  }
178
184
  async function createWorktree(projectId, input) {
179
- let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, {
185
+ let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, operationResponseSchema, {
180
186
  method: "POST",
181
187
  body: JSON.stringify(input)
182
188
  })).operation;
183
189
  while (operation.status === "pending" || operation.status === "running") {
184
190
  await new Promise((resolve) => setTimeout(resolve, 100));
185
- operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
191
+ operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
186
192
  }
187
193
  if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
188
194
  if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
189
195
  const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
190
196
  if (!worktreeId) throw new CliError("Completed tree creation did not identify its tree", 5, "INVALID_OPERATION_RESULT");
191
- const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project.worktrees.find((item) => item.id === worktreeId);
197
+ const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project.worktrees.find((item) => item.id === worktreeId);
192
198
  if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
193
199
  const terminalId = operation.result?.terminalId ?? null;
194
200
  return {
@@ -211,7 +217,7 @@ async function canonical(value) {
211
217
  return fs.realpath(resolved).catch(() => resolved);
212
218
  }
213
219
  async function projects() {
214
- return (await request("/api/projects")).projects;
220
+ return (await request("/api/projects", projectsResponseSchema)).projects;
215
221
  }
216
222
  function pathContains(candidate, parent) {
217
223
  const relative = path.relative(parent, candidate);
@@ -239,7 +245,7 @@ async function packageSource(value) {
239
245
  return value;
240
246
  }
241
247
  async function localPackageProjectId() {
242
- return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}`)).project.id;
248
+ return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}`, packageProjectResponseSchema)).project.id;
243
249
  }
244
250
  async function resolveWorktree(identifier) {
245
251
  const all = (await projects()).flatMap((project) => project.worktrees);
@@ -263,12 +269,12 @@ function parseWebPanelInput(value) {
263
269
  } catch (error) {
264
270
  throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
265
271
  }
266
- const validated = webPanelInputSchema.safeParse(parsed);
267
- if (!validated.success) throw new CliError("--input must contain a JSON object", 2);
268
- return validated.data;
272
+ const validated = decodeUnknownOrNull(webPanelInputSchema, parsed);
273
+ if (!validated) throw new CliError("--input must contain a JSON object", 2);
274
+ return validated;
269
275
  }
270
276
  async function webPanelDefinition(worktreeId, identifier) {
271
- const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`)).definitions;
277
+ const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`, webPanelDefinitionsResponseSchema)).definitions;
272
278
  const exact = definitions.find((definition) => definition.id === identifier);
273
279
  if (exact) return exact;
274
280
  const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
@@ -285,7 +291,37 @@ async function webPanelLaunchCwd(worktree) {
285
291
  });
286
292
  return path.relative(worktreeRoot, cwd) || ".";
287
293
  }
288
- function webPanelUrl(worktree, panelId) {
294
+ async function resolveBrowserPanel(panelId) {
295
+ const candidates = (await projects()).flatMap((project) => project.worktrees.flatMap((worktree) => worktree.panels.filter((panel) => panel.kind === "browser").map((panel) => ({
296
+ panel,
297
+ worktree
298
+ }))));
299
+ if (panelId) {
300
+ const match = candidates.find((candidate) => candidate.panel.id === panelId);
301
+ if (!match) throw new CliError(`Browser ${panelId} was not found`, 5);
302
+ return match;
303
+ }
304
+ const worktree = await resolveWorktree(".");
305
+ const matches = candidates.filter((candidate) => candidate.worktree.id === worktree.id);
306
+ if (matches.length === 1) return matches[0];
307
+ if (matches.length === 0) throw new CliError(`No Browser is open in worktree ${worktree.name}`, 5, "BROWSER_PANEL_NOT_FOUND");
308
+ throw new CliError(`More than one Browser is open in worktree ${worktree.name}; specify --panel`, 5, "BROWSER_PANEL_AMBIGUOUS", { panelIds: matches.map((candidate) => candidate.panel.id) });
309
+ }
310
+ async function runBrowserAgentCommand(command, args, panelId) {
311
+ const { panel } = await resolveBrowserPanel(panelId);
312
+ const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, browserAgentResponseSchema, {
313
+ method: "POST",
314
+ body: JSON.stringify({
315
+ command,
316
+ args
317
+ })
318
+ });
319
+ return {
320
+ panelId: panel.id,
321
+ output: result.output
322
+ };
323
+ }
324
+ function panelUrl(worktree, panelId) {
289
325
  const target = new URL(apiUrl);
290
326
  target.pathname = `/projects/${encodeURIComponent(worktree.projectId)}/worktrees/${encodeURIComponent(worktree.id)}/panels/${encodeURIComponent(panelId)}`;
291
327
  target.search = "";
@@ -315,7 +351,7 @@ function parseDuration(value) {
315
351
  }
316
352
  }
317
353
  async function inspectTerminal(terminalId, signal) {
318
- return request(`/api/terminals/${encodeURIComponent(terminalId)}`, signal ? { signal } : {});
354
+ return request(`/api/terminals/${encodeURIComponent(terminalId)}`, terminalObservationResponseSchema, signal ? { signal } : {});
319
355
  }
320
356
  async function waitForTerminal(terminalId, condition, timeoutMs) {
321
357
  let observation = null;
@@ -347,16 +383,9 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
347
383
  if (condition === "bell") bellBaseline = observation.metadata.bell?.sequence ?? 0;
348
384
  const immediate = matched((/* @__PURE__ */ new Date()).toISOString());
349
385
  if (immediate) return immediate;
350
- events = io(`${apiUrl}/events`, {
351
- path: SOCKET_IO_PATH,
352
- transports: ["websocket"],
353
- forceNew: true,
354
- autoConnect: false,
355
- reconnection: false,
356
- retries: 0
357
- });
358
386
  return await new Promise((resolve, reject) => {
359
387
  let settled = false;
388
+ let connected = false;
360
389
  let queue = Promise.resolve();
361
390
  const finish = (result) => {
362
391
  if (!settled) {
@@ -374,16 +403,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
374
403
  queue = queue.then(task);
375
404
  queue.catch(fail);
376
405
  };
377
- controller.signal.addEventListener("abort", () => fail(/* @__PURE__ */ new Error("Terminal wait cancelled")), { once: true });
378
- events.on("snapshot", (value) => enqueue(async () => {
379
- const snapshot = parseEventsSnapshot(value);
380
- if (!snapshot || !observation) throw new CliError("Treeport daemon sent an invalid event snapshot", 3, "DAEMON_PROTOCOL_ERROR");
381
- const metadata = snapshot.terminalMetadata.find((item) => item.terminalId === terminalId);
406
+ const snapshot = async (value) => {
407
+ if (!observation) throw new CliError("Treeport daemon sent an invalid event snapshot", 3, "DAEMON_PROTOCOL_ERROR");
408
+ connected = true;
409
+ const metadata = value.terminalMetadata.find((item) => item.terminalId === terminalId);
382
410
  if (metadata) observation = {
383
411
  ...observation,
384
412
  metadata
385
413
  };
386
- const snapshotMatch = matched(snapshot.at);
414
+ const snapshotMatch = matched(value.at);
387
415
  if (snapshotMatch) {
388
416
  finish(snapshotMatch);
389
417
  return;
@@ -392,10 +420,8 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
392
420
  if (cancellation) throw new Error("Terminal wait cancelled");
393
421
  const refreshedMatch = matched((/* @__PURE__ */ new Date()).toISOString());
394
422
  if (refreshedMatch) finish(refreshedMatch);
395
- }));
396
- events.on("product_event", (value) => enqueue(async () => {
397
- const event = parseProductEvent(value);
398
- if (!event) throw new CliError("Treeport daemon sent an invalid product event", 3, "DAEMON_PROTOCOL_ERROR");
423
+ };
424
+ const productEvent = async (event) => {
399
425
  if (event.type !== "terminal.metadata" && event.type !== "terminal.updated" && event.type !== "terminal.removed") return;
400
426
  if (event.data.terminalId !== terminalId) return;
401
427
  if (event.type === "terminal.removed") throw new CliError(`Terminal ${terminalId} was removed while waiting`, 5, "TERMINAL_REMOVED", {
@@ -415,10 +441,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
415
441
  }
416
442
  const result = matched(event.at);
417
443
  if (result) finish(result);
418
- }));
419
- events.on("connect_error", (error) => fail(new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error.message}`, 3, "DAEMON_UNREACHABLE")));
420
- events.on("disconnect", () => fail(new CliError("Treeport daemon event channel disconnected before the condition was observed", 3, "DAEMON_DISCONNECTED")));
421
- events.connect();
444
+ };
445
+ controller.signal.addEventListener("abort", () => fail(/* @__PURE__ */ new Error("Terminal wait cancelled")), { once: true });
446
+ const program = Effect.scoped(Effect.gen(function* () {
447
+ yield* (yield* RpcClient.make(TreeportRpcs)).WatchProjectEvents({ protocol: 3 }).pipe(Stream.runForEach((item) => Effect.sync(() => {
448
+ if (item._tag === "Snapshot") enqueue(() => snapshot(item.snapshot));
449
+ else enqueue(() => productEvent(item.event));
450
+ })));
451
+ })).pipe(Effect.provide(treeportRpcClientLayer(`${apiUrl}/api/rpc`)), Effect.onExit(() => Effect.sync(() => fail(connected ? new CliError("Treeport daemon event channel disconnected before the condition was observed", 3, "DAEMON_DISCONNECTED") : new CliError(`Cannot reach Treeport daemon at ${apiUrl}`, 3, "DAEMON_UNREACHABLE")))));
452
+ events = Effect.runFork(program);
422
453
  });
423
454
  } catch (error) {
424
455
  if (cancellation === "timeout") throw new CliError(`Timed out waiting for terminal ${terminalId} to reach ${condition}`, 4, "WAIT_TIMEOUT", {
@@ -434,20 +465,16 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
434
465
  if (timeout) clearTimeout(timeout);
435
466
  process.off("SIGINT", interrupt);
436
467
  controller.abort();
437
- events?.removeAllListeners();
438
- events?.disconnect();
468
+ if (events) Effect.runFork(Fiber.interrupt(events));
439
469
  }
440
470
  }
441
471
  function print(value, human) {
442
472
  writeStdout(`${jsonOutput ? JSON.stringify(value) : human ? human() : JSON.stringify(value, null, 2)}\n`);
443
473
  }
444
- const agentGuidance = `AI agents:
445
- If you're an AI agent, use \`treeport skills\` to see the usage guide.
446
- `;
447
474
  async function main(args) {
448
475
  const argv = args[0] === "spawn" || args[0] === "terminal" && args[1] === "create" ? commandArgv(args) : void 0;
449
476
  let parserError = "";
450
- const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, trees, and terminals.").argument("[folder]", "folder or folder inside a Git repository to open").option("--json", "emit machine-readable JSON").addHelpText("beforeAll", agentGuidance).configureOutput({
477
+ const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, trees, and terminals.").argument("[folder]", "folder or folder inside a Git repository to open").option("--json", "emit machine-readable JSON").configureOutput({
451
478
  writeOut: writeStdout,
452
479
  writeErr: (value) => {
453
480
  parserError += value;
@@ -471,7 +498,7 @@ async function main(args) {
471
498
  if (!await daemonHealth(apiUrl)) throw new CliError(`Cannot reach the externally managed Treeport daemon at ${apiUrl}. Start it through the process that owns its lifecycle and retry.`, 3, "DAEMON_UNREACHABLE");
472
499
  } else if (lifecycle === "service") await ensureServiceDaemon();
473
500
  else await daemonUp({});
474
- const registered = await request("/api/projects", {
501
+ const registered = await request("/api/projects", projectResponseSchema, {
475
502
  method: "POST",
476
503
  body: JSON.stringify({ path: canonicalFolder })
477
504
  });
@@ -484,7 +511,7 @@ async function main(args) {
484
511
  target.pathname = `/projects/${encodeURIComponent(registered.project.id)}/worktrees/${encodeURIComponent(targetWorktree.id)}`;
485
512
  target.search = "";
486
513
  target.hash = "";
487
- const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, {
514
+ const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, okResponseSchema, {
488
515
  method: "POST",
489
516
  body: JSON.stringify({ sourceTerminalId: contextTerminalId })
490
517
  }).then(() => ({ client: "current" })) : await openWorkspace(target.href).catch((error) => {
@@ -521,13 +548,13 @@ async function main(args) {
521
548
  if (options.foreground) return;
522
549
  print(result, () => `Treeport is running\n${result.apiUrl}`);
523
550
  });
524
- const stopCommand = program.command("stop").description("Stop the local daemon and preserve terminal sessions").option("--terminate-terminals", "terminate every Treeport-owned tmux server").option("--force", "confirm termination of all terminals").option("--json", "emit machine-readable JSON");
551
+ const stopCommand = program.command("stop").description("Stop the local daemon and preserve terminal sessions").option("--terminate-terminals", "terminate every Treeport-owned terminal process").option("--force", "confirm termination of all terminals").option("--json", "emit machine-readable JSON");
525
552
  stopCommand.action(async () => {
526
553
  const lifecycle = await resolveDaemonLifecycle();
527
554
  if (lifecycle === "external") throw new CliError("Cannot run `treeport stop` because the daemon lifecycle is externally managed. Control the process that started Treeport instead.", 5, "DAEMON_LIFECYCLE_EXTERNAL");
528
555
  const options = stopCommand.opts();
529
556
  if (options.terminateTerminals && !options.force) throw new CliError("Re-run with --terminate-terminals --force to confirm termination of every terminal.", 2);
530
- if (options.terminateTerminals) await request("/api/admin/terminate-terminals", { method: "POST" });
557
+ if (options.terminateTerminals) await request("/api/admin/terminate-terminals", terminatedTerminalsResponseSchema, { method: "POST" });
531
558
  if (lifecycle === "service") {
532
559
  const result = await serviceStop();
533
560
  print(result, () => formatServiceStatus(result.status));
@@ -662,7 +689,7 @@ async function main(args) {
662
689
  ...!terminalId ? [`${contextPrefix}_TERMINAL_ID`] : []
663
690
  ];
664
691
  if (missing.length) throw new CliError(`Incomplete Treeport context; missing ${missing.join(", ")}`, 5, "TREEPORT_CONTEXT_INCOMPLETE", { missing });
665
- const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project;
692
+ const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project;
666
693
  const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
667
694
  if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
668
695
  projectId,
@@ -673,6 +700,7 @@ async function main(args) {
673
700
  worktreeId,
674
701
  terminalId
675
702
  });
703
+ const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`, treeContextResponseSchema)).context;
676
704
  const context = {
677
705
  managed: true,
678
706
  apiUrl,
@@ -695,7 +723,8 @@ async function main(args) {
695
723
  head: worktree.head,
696
724
  branch: worktree.branch,
697
725
  detached: worktree.detached,
698
- kind: worktree.kind
726
+ kind: worktree.kind,
727
+ context: treeContext
699
728
  },
700
729
  terminal: {
701
730
  id: terminal.id,
@@ -705,14 +734,114 @@ async function main(args) {
705
734
  exitCode: terminal.exitCode
706
735
  }
707
736
  };
708
- print(context, () => `Treeport context\n\nProject: ${context.project.name} (${context.project.id})\nTree: ${context.worktree.name} (${context.worktree.id})\nPath: ${context.worktree.path}\nTerminal: ${context.terminal.name} (${context.terminal.id}) — ${context.terminal.status}\nAPI: ${context.apiUrl}\nLifecycle: ${context.daemonLifecycle === "external" ? "externally managed" : context.daemonLifecycle === "service" ? "managed by the OS service" : "managed by Treeport"}`);
737
+ print(context, () => {
738
+ const entries = Object.entries(context.worktree.context);
739
+ const treeContextText = entries.length === 0 ? "—" : entries.map(([key, value]) => {
740
+ const [first = "", ...rest] = [...value].map((character) => {
741
+ const code = character.charCodeAt(0);
742
+ return code !== 10 && (code <= 31 || code >= 127 && code <= 159) ? `\\u${code.toString(16).padStart(4, "0")}` : character;
743
+ }).join("").split("\n");
744
+ return ` ${key}: ${first}${rest.length > 0 ? `\n${rest.map((line) => ` ${line}`).join("\n")}` : ""}`;
745
+ }).join("\n");
746
+ return `Treeport context\n\nProject: ${context.project.name} (${context.project.id})\nTree: ${context.worktree.name} (${context.worktree.id})\nPath: ${context.worktree.path}\nContext:\n${treeContextText}\nTerminal: ${context.terminal.name} (${context.terminal.id}) — ${context.terminal.status}\nAPI: ${context.apiUrl}\nLifecycle: ${context.daemonLifecycle === "external" ? "externally managed" : context.daemonLifecycle === "service" ? "managed by the OS service" : "managed by Treeport"}`;
747
+ });
748
+ });
749
+ const browserCommand = program.command("browser").description("Manage Browser and its hosted Chromium");
750
+ browserCommand.action(() => {
751
+ writeStdout(browserCommand.helpInformation());
709
752
  });
753
+ const browserOpenCommand = browserCommand.command("open").description("Open Browser and request client navigation").argument("[url]", "absolute HTTP or HTTPS URL").requiredOption("--worktree <id-or-path-or-dot>", "owning tree").option("--json", "emit machine-readable JSON");
754
+ browserOpenCommand.action(async (url) => {
755
+ const worktree = await resolveWorktree(browserOpenCommand.opts().worktree);
756
+ const body = {
757
+ url,
758
+ sourceTerminalId: contextWorktreeId === worktree.id ? contextTerminalId ?? null : null
759
+ };
760
+ const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, openBrowserPanelResponseSchema, {
761
+ method: "POST",
762
+ body: JSON.stringify(body)
763
+ });
764
+ const output = {
765
+ ...result,
766
+ url: panelUrl(worktree, result.panel.id)
767
+ };
768
+ print(output, () => `Opened ${result.panel.title} (${result.panel.id})\n${output.url}`);
769
+ });
770
+ browserCommand.command("install").description("Install the Chromium build used by Browser").option("--json", "emit machine-readable JSON").action(async () => {
771
+ const result = await request("/api/browser/install", browserInstallResponseSchema, { method: "POST" });
772
+ print(result, () => result.message);
773
+ });
774
+ browserCommand.command("status").description("Show hosted browser installation status").option("--json", "emit machine-readable JSON").action(async () => {
775
+ const result = await request("/api/browser/status", browserInstallStatusSchema);
776
+ print(result, () => `${result.installed ? "Chromium is installed" : "Chromium is not installed"}\nLaunch ready: ${result.launchReady ? "yes" : "no"}\nPlaywright: ${result.playwrightVersion}\nBrowser: ${result.channel} ${result.browserRevision}\nExecutable: ${result.executablePath}${result.launchError ? `\nLaunch error: ${result.launchError}` : ""}`);
777
+ });
778
+ browserCommand.command("remove").description("Remove Treeport's hosted Chromium build").option("--json", "emit machine-readable JSON").action(async () => {
779
+ await request("/api/browser/install", okResponseSchema, { method: "DELETE" });
780
+ print({ removed: true }, () => "Removed Treeport hosted Chromium");
781
+ });
782
+ browserCommand.command("list").description("List open Browser sessions").option("--json", "emit machine-readable JSON").action(async () => {
783
+ const panels = (await projects()).flatMap((project) => project.worktrees.flatMap((worktree) => worktree.panels.filter((panel) => panel.kind === "browser").map((panel) => ({
784
+ panelId: panel.id,
785
+ title: panel.title,
786
+ worktreeId: worktree.id,
787
+ worktree: worktree.name,
788
+ projectId: project.id,
789
+ project: project.name
790
+ }))));
791
+ print(panels, () => panels.length ? panels.map((panel) => `${panel.panelId}\t${panel.project} / ${panel.worktree}\t${panel.title}`).join("\n") : "Browser is not open.");
792
+ });
793
+ const printAgentResult = async (command, args, panelId) => {
794
+ const result = await runBrowserAgentCommand(command, args, panelId);
795
+ print(result, () => result.output);
796
+ };
797
+ const browserSnapshotCommand = browserCommand.command("snapshot").description("Capture an accessibility snapshot of the hosted page").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
798
+ browserSnapshotCommand.action(async () => printAgentResult("snapshot", [], browserSnapshotCommand.opts().panel));
799
+ const browserClickCommand = browserCommand.command("click").description("Click an element from the latest browser snapshot").argument("<target>", "Playwright element reference or selector").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
800
+ browserClickCommand.action(async (target) => printAgentResult("click", [target], browserClickCommand.opts().panel));
801
+ const browserFillCommand = browserCommand.command("fill").description("Fill an editable element from the latest browser snapshot").argument("<target>", "Playwright element reference or selector").argument("<text>", "text to enter").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
802
+ browserFillCommand.action(async (target, text) => printAgentResult("fill", [target, text], browserFillCommand.opts().panel));
803
+ const browserPressCommand = browserCommand.command("press").description("Press a key in the hosted page").argument("<key>", "Playwright key name").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
804
+ browserPressCommand.action(async (key) => printAgentResult("press", [key], browserPressCommand.opts().panel));
805
+ const browserGotoCommand = browserCommand.command("goto").description("Navigate the hosted page").argument("<url>", "absolute HTTP or HTTPS URL").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
806
+ browserGotoCommand.action(async (url) => printAgentResult("goto", [url], browserGotoCommand.opts().panel));
807
+ const browserConsoleCommand = browserCommand.command("console").description("List page console messages").argument("[level]", "minimum console level").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
808
+ browserConsoleCommand.action(async (level) => printAgentResult("console", level ? [level] : [], browserConsoleCommand.opts().panel));
809
+ for (const [name, description, agentName] of [
810
+ [
811
+ "back",
812
+ "Go back in the hosted page",
813
+ "go-back"
814
+ ],
815
+ [
816
+ "forward",
817
+ "Go forward in the hosted page",
818
+ "go-forward"
819
+ ],
820
+ [
821
+ "reload",
822
+ "Reload the hosted page",
823
+ "reload"
824
+ ],
825
+ [
826
+ "network",
827
+ "List page network requests",
828
+ "requests"
829
+ ],
830
+ [
831
+ "screenshot",
832
+ "Capture a screenshot of the hosted page",
833
+ "screenshot"
834
+ ]
835
+ ]) {
836
+ const command = browserCommand.command(name).description(description).option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
837
+ command.action(async () => printAgentResult(agentName, [], command.opts().panel));
838
+ }
710
839
  const installCommand = program.command("install").description("Install and configure a Treeport package").argument("<source>", "npm: source or local directory").option("-l, --local", "configure the registered project containing the current directory").option("--json", "emit machine-readable JSON");
711
840
  installCommand.action(async (source) => {
712
841
  const options = installCommand.opts();
713
842
  const body = { source: await packageSource(source) };
714
843
  if (options.local) body.projectId = await localPackageProjectId();
715
- const result = (await request("/api/packages/install", {
844
+ const result = (await request("/api/packages/install", packageOperationResponseSchema, {
716
845
  method: "POST",
717
846
  body: JSON.stringify(body)
718
847
  })).result;
@@ -723,14 +852,14 @@ async function main(args) {
723
852
  const options = removePackageCommand.opts();
724
853
  const body = { source: await packageSource(source) };
725
854
  if (options.local) body.projectId = await localPackageProjectId();
726
- const result = (await request("/api/packages/remove", {
855
+ const result = (await request("/api/packages/remove", packageOperationResponseSchema, {
727
856
  method: "POST",
728
857
  body: JSON.stringify(body)
729
858
  })).result;
730
859
  print(result, () => `Removed ${result.source}`);
731
860
  });
732
861
  program.command("list").description("List configured Treeport packages").option("--json", "emit machine-readable JSON").action(async () => {
733
- const result = await request("/api/packages");
862
+ const result = await request("/api/packages", packageListingResponseSchema);
734
863
  print(result, () => {
735
864
  const lines = result.packages.map((pkg) => {
736
865
  return `${pkg.scope === "global" ? "global" : `project:${pkg.projectName ?? pkg.projectId}`}\t${pkg.source}\t${pkg.resources.webPanels} web panels, ${pkg.resources.terminalPresets} terminal presets`;
@@ -748,7 +877,7 @@ async function main(args) {
748
877
  const selfUpdateOptions = { environment: cliEnvironment };
749
878
  if (!jsonOutput) selfUpdateOptions.progress = (message) => writeStderr(`${message}\n`);
750
879
  const result = await runLocalUpdate(selfUpdateOptions).catch((error) => {
751
- if (error instanceof LocalUpdateError) throw new CliError(error.message, error.exitCode, error.code, error.details);
880
+ if (error instanceof LocalUpdateError) throw new CliError(jsonOutput ? error.message : formatLocalUpdateError(error.message, error.details), error.exitCode, error.code, error.details);
752
881
  throw error;
753
882
  });
754
883
  print(result, () => {
@@ -757,7 +886,7 @@ async function main(args) {
757
886
  });
758
887
  return;
759
888
  }
760
- const results = (await request("/api/packages/update", {
889
+ const results = (await request("/api/packages/update", packageOperationsResponseSchema, {
761
890
  method: "POST",
762
891
  body: JSON.stringify(source ? { source: await packageSource(source) } : {})
763
892
  })).results;
@@ -766,7 +895,7 @@ async function main(args) {
766
895
  const reloadCommand = program.command("reload").description("Reload package settings and resources without restarting").option("-l, --local", "reload only the registered project containing the current directory").option("--json", "emit machine-readable JSON");
767
896
  reloadCommand.action(async () => {
768
897
  const options = reloadCommand.opts();
769
- const result = await request("/api/packages/reload", {
898
+ const result = await request("/api/packages/reload", packageReloadResponseSchema, {
770
899
  method: "POST",
771
900
  body: JSON.stringify(options.local ? { projectId: await localPackageProjectId() } : {})
772
901
  });
@@ -779,7 +908,7 @@ async function main(args) {
779
908
  throw new CliError(projectCommand.helpInformation(), 2);
780
909
  });
781
910
  projectCommand.command("add").description("Register a folder or Git repository").argument("<path>", "folder path").option("--json", "emit machine-readable JSON").action(async (repository) => {
782
- const body = await request("/api/projects", {
911
+ const body = await request("/api/projects", projectResponseSchema, {
783
912
  method: "POST",
784
913
  body: JSON.stringify({ path: await canonical(repository) })
785
914
  });
@@ -812,25 +941,46 @@ async function main(args) {
812
941
  const result = await createWorktree(project.id, request);
813
942
  print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
814
943
  });
815
- const worktreeRemoveCommand = worktreeCommand.command("remove").description("Remove a linked tree").argument("<id-or-path-or-dot>", "Tree to remove").option("--force", "confirm destructive removal warnings").option("--json", "emit machine-readable JSON");
944
+ const worktreeRemoveCommand = worktreeCommand.command("remove").description("Remove a linked tree").argument("<id-or-path-or-dot>", "Tree to remove").option("--force", "confirm destructive removal warnings").option("--skip-cleanup", "remove without configured project cleanup").option("--json", "emit machine-readable JSON");
816
945
  worktreeRemoveCommand.action(async (identifier) => {
817
- const { force: confirmed } = worktreeRemoveCommand.opts();
946
+ const { force: confirmed, skipCleanup: requestedSkipCleanup } = worktreeRemoveCommand.opts();
818
947
  const worktree = await resolveWorktree(identifier);
819
- const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`)).preview;
948
+ const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`, removePreviewResponseSchema)).preview;
820
949
  if (!preview.eligible) throw new CliError(preview.reasons.join("\n"), 5);
821
950
  if (preview.warnings.length && !confirmed) throw new CliError(`${preview.warnings.join("\n")}\nRe-run with --force to confirm removal.`, 5);
822
- let operation = (await request(`/api/worktrees/${worktree.id}/remove`, {
951
+ const skipCleanup = Boolean(requestedSkipCleanup) && preview.cleanup.commands.length > 0;
952
+ if (skipCleanup && !confirmed) throw new CliError("Skipping project cleanup can leave project resources behind.\nRe-run with --force --skip-cleanup to confirm removal.", 5);
953
+ let operation = (await request(`/api/worktrees/${worktree.id}/remove`, operationResponseSchema, {
823
954
  method: "POST",
824
955
  body: JSON.stringify({
825
956
  confirmationToken: preview.confirmationToken,
826
- confirmDestructive: preview.warnings.length > 0
957
+ confirmDestructive: preview.warnings.length > 0 || skipCleanup,
958
+ skipCleanup
827
959
  })
828
960
  })).operation;
961
+ const displayedCleanupCommands = /* @__PURE__ */ new Set();
962
+ const displayFinishedCleanup = () => {
963
+ if (jsonOutput || operation.kind !== "remove") return;
964
+ operation.request.cleanupCommands.commands.forEach((command, index) => {
965
+ if (displayedCleanupCommands.has(index) || command.status !== "completed" && command.status !== "failed") return;
966
+ displayedCleanupCommands.add(index);
967
+ const lines = [`Cleanup: ${command.name}${command.status === "failed" ? " (failed)" : ""}`];
968
+ if (command.stdout) lines.push(command.stdout.replace(/\n$/u, ""));
969
+ if (command.stderr) lines.push(command.stderr.replace(/\n$/u, ""));
970
+ if (command.outputTruncated) lines.push("Cleanup output was truncated.");
971
+ writeStdout(`${lines.join("\n")}\n`);
972
+ });
973
+ };
974
+ displayFinishedCleanup();
829
975
  while (operation.status === "pending" || operation.status === "running") {
830
976
  await new Promise((resolve) => setTimeout(resolve, 100));
831
- operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
977
+ operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
978
+ displayFinishedCleanup();
979
+ }
980
+ if (operation.status === "failed") {
981
+ const cleanup = operation.kind === "remove" ? operation.request.cleanupCommands.commands.find((command) => command.status === "failed") : null;
982
+ throw new CliError(operation.error ?? "Tree removal failed; Git kept the tree.", 5, "WORKTREE_REMOVAL_FAILED", cleanup ?? void 0);
832
983
  }
833
- if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
834
984
  if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
835
985
  print(operation.result, () => {
836
986
  const warning = operation.result?.cleanup.warning;
@@ -846,7 +996,7 @@ async function main(args) {
846
996
  const options = webPanelOpenCommand.opts();
847
997
  const worktree = await resolveWorktree(options.worktree);
848
998
  const definition = await webPanelDefinition(worktree.id, identifier);
849
- const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, {
999
+ const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, openWebPanelResponseSchema, {
850
1000
  method: "POST",
851
1001
  body: JSON.stringify({
852
1002
  definitionId: definition.id,
@@ -858,7 +1008,7 @@ async function main(args) {
858
1008
  });
859
1009
  const output = {
860
1010
  ...result,
861
- url: webPanelUrl(worktree, result.panel.id)
1011
+ url: panelUrl(worktree, result.panel.id)
862
1012
  };
863
1013
  print(output, () => `${result.reused ? "Reused" : "Opened"} ${result.panel.title} (${result.panel.id})\n${output.url}`);
864
1014
  });
@@ -878,7 +1028,7 @@ async function main(args) {
878
1028
  const worktree = await resolveWorktree(options.worktree);
879
1029
  const body = { name: options.name };
880
1030
  if (argv) body.argv = argv;
881
- const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
1031
+ const result = await request(`/api/worktrees/${worktree.id}/terminals`, terminalResponseSchema, {
882
1032
  method: "POST",
883
1033
  body: JSON.stringify(body)
884
1034
  });
@@ -898,7 +1048,7 @@ async function main(args) {
898
1048
  const { lines: rawLines } = terminalCaptureCommand.opts();
899
1049
  const lines = rawLines === void 0 ? 200 : parseCaptureLines(rawLines);
900
1050
  const terminalId = resolveTerminalId(identifier);
901
- const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`);
1051
+ const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`, terminalCaptureResponseSchema);
902
1052
  if (jsonOutput) print(capture);
903
1053
  else {
904
1054
  writeStdout(capture.content);
@@ -918,7 +1068,7 @@ async function main(args) {
918
1068
  print(result, () => `${result.terminal.name} (${result.terminal.id}) reached ${result.condition} at ${result.observedAt}`);
919
1069
  });
920
1070
  terminalCommand.command("delete").description("Delete a terminal").argument("<terminal-id>", "terminal to delete").option("--json", "emit machine-readable JSON").action(async (terminalId) => {
921
- await request(`/api/terminals/${terminalId}`, { method: "DELETE" });
1071
+ await request(`/api/terminals/${terminalId}`, okResponseSchema, { method: "DELETE" });
922
1072
  print({
923
1073
  ok: true,
924
1074
  terminalId
@@ -969,11 +1119,14 @@ async function runCliApplication(options) {
969
1119
  } catch (error) {
970
1120
  const cliError = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
971
1121
  if (jsonOutput) {
972
- const body = { error: {
1122
+ const body = { error: cliError.details === void 0 ? {
973
1123
  code: cliError.code,
974
1124
  message: cliError.message
1125
+ } : {
1126
+ code: cliError.code,
1127
+ message: cliError.message,
1128
+ details: cliError.details
975
1129
  } };
976
- if (cliError.details !== void 0) body.error.details = cliError.details;
977
1130
  writeStderr(`${JSON.stringify(body)}\n`);
978
1131
  } else writeStderr(`${cliError.message}\n`);
979
1132
  requestedExitCode = cliError.exitCode;