@treeport/treeport 0.6.1 → 0.8.3

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,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { B as parseEventsSnapshot, H as SOCKET_IO_PATH, R as webPanelInputSchema, V as parseProductEvent, t as TERMINAL_CAPTURE_MAX_LINES } from "../../dist-Crk_Xr82.js";
3
- import { A as treeportVersion, C as disableTailscaleRemote, D as resolvePackagePath, E as resolveLocalApiUrl, M as parseDurationMs, O as runDoctor, S as daemonUp, T as readDaemonLogs, _ as serviceStatus, b as daemonHealth, d as serviceDisable, f as serviceDoctorCheck, g as serviceStart, h as serviceRun, k as tailscaleRemoteStatus, l as readServiceLogs, m as serviceInstalled, p as serviceEnable, s as runLocalUpdate, t as LocalUpdateError, u as serviceApply, v as serviceStop, w as enableTailscaleRemote, x as daemonStatus, y as daemonDown } from "../../update-qVp7yL5D.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 runDoctor, C as daemonStatus, D as readDaemonLogs, E as enableTailscaleRemote, F as formatServiceStatus, I as humanOutput, L as stateName, M as treeportVersion, N as formatLocalUpdateError, O as resolveLocalApiUrl, P as formatLocalUpdateResult, S as daemonHealth, T as disableTailscaleRemote, _ as serviceRun, b as serviceStop, d as readServiceLogs, f as serviceApply, g as serviceInstalled, h as serviceEnable, j as tailscaleRemoteStatus, k as resolvePackagePath, l as runLocalUpdate, m as serviceDoctorCheck, p as serviceDisable, r as confirmLocalUpdate, t as LocalUpdateError, v as serviceStart, w as daemonUp, x as daemonDown, y as serviceStatus, z as parseDurationMs } from "../../update-BYHlwpAq.js";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { Command, CommanderError } from "commander";
7
- 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";
8
11
  import { spawn } from "node:child_process";
9
12
  //#region src/cli/args.ts
10
13
  function extractJsonOutput(args) {
@@ -96,6 +99,9 @@ let writeStderr = (value) => {
96
99
  };
97
100
  let requestedExitCode = 0;
98
101
  let cliEnvironment = process.env;
102
+ let output = humanOutput();
103
+ let errorOutput = humanOutput();
104
+ let updateProgressShown = false;
99
105
  var CliError = class extends Error {
100
106
  exitCode;
101
107
  code;
@@ -116,22 +122,6 @@ async function resolveDaemonLifecycle() {
116
122
  }
117
123
  return await serviceInstalled() ? "service" : "treeport";
118
124
  }
119
- function formatServiceStatus(status) {
120
- const mode = status.mode === "headless" ? "advanced headless (starts before login)" : status.mode === "user" && status.manager === "launchd" ? "user/login (starts after login)" : status.mode === "user" ? "user service" : "not installed";
121
- const lines = [
122
- `Treeport service: ${status.state}`,
123
- `Mode: ${mode}`,
124
- `Manager: ${status.manager ?? "unsupported"}`,
125
- `Starts before login: ${status.enabledAtBoot ? "yes" : "no"}`,
126
- `Active: ${status.active ? "yes" : "no"}`,
127
- `Definition: ${status.definitionPath ?? "not installed"}`
128
- ];
129
- if (status.daemon?.state) lines.push(`PID: ${status.daemon.state.pid}`);
130
- if (status.issues.length) lines.push(...status.issues.map((issue) => `Issue: ${issue}`));
131
- if (status.administratorCommand) lines.push("Administrator action required:", status.administratorCommand, "Then run: treeport service status");
132
- else if (status.recoveryCommands.length) lines.push(`Next: ${status.recoveryCommands[0]}`);
133
- return lines.join("\n");
134
- }
135
125
  async function ensureServiceDaemon() {
136
126
  const result = await serviceStart();
137
127
  const state = result.status.daemon?.state;
@@ -146,7 +136,7 @@ async function ensureServiceDaemon() {
146
136
  pid: state.pid
147
137
  };
148
138
  }
149
- async function request(pathname, options = {}) {
139
+ async function request(pathname, schema, options = {}) {
150
140
  const controller = new AbortController();
151
141
  const externalSignal = options.signal;
152
142
  const abort = () => controller.abort();
@@ -164,10 +154,12 @@ async function request(pathname, options = {}) {
164
154
  });
165
155
  const body = await response.json().catch(() => ({}));
166
156
  if (!response.ok) {
167
- const error = body.error;
168
- throw new CliError(error?.message || `HTTP ${response.status}`, 5, error?.code || "API_ERROR", error?.details);
157
+ const failure = decodeUnknownOrNull(apiErrorBodySchema, body);
158
+ throw new CliError(failure?.error.message || `HTTP ${response.status}`, 5, failure?.error.code || "API_ERROR", failure?.error.details);
169
159
  }
170
- return body;
160
+ const decoded = decodeUnknownOrNull(schema, body);
161
+ if (decoded === null) throw new CliError(`Treeport daemon returned an invalid response for ${pathname}`, 3, "DAEMON_PROTOCOL_ERROR", { pathname });
162
+ return decoded;
171
163
  } catch (error) {
172
164
  if (error instanceof CliError) throw error;
173
165
  throw new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error instanceof Error ? error.message : String(error)}`, 3, "DAEMON_UNREACHABLE");
@@ -177,19 +169,19 @@ async function request(pathname, options = {}) {
177
169
  }
178
170
  }
179
171
  async function createWorktree(projectId, input) {
180
- let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, {
172
+ let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, operationResponseSchema, {
181
173
  method: "POST",
182
174
  body: JSON.stringify(input)
183
175
  })).operation;
184
176
  while (operation.status === "pending" || operation.status === "running") {
185
177
  await new Promise((resolve) => setTimeout(resolve, 100));
186
- operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
178
+ operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
187
179
  }
188
180
  if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
189
181
  if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
190
182
  const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
191
183
  if (!worktreeId) throw new CliError("Completed tree creation did not identify its tree", 5, "INVALID_OPERATION_RESULT");
192
- const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project.worktrees.find((item) => item.id === worktreeId);
184
+ const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project.worktrees.find((item) => item.id === worktreeId);
193
185
  if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
194
186
  const terminalId = operation.result?.terminalId ?? null;
195
187
  return {
@@ -212,7 +204,7 @@ async function canonical(value) {
212
204
  return fs.realpath(resolved).catch(() => resolved);
213
205
  }
214
206
  async function projects() {
215
- return (await request("/api/projects")).projects;
207
+ return (await request("/api/projects", projectsResponseSchema)).projects;
216
208
  }
217
209
  function pathContains(candidate, parent) {
218
210
  const relative = path.relative(parent, candidate);
@@ -240,7 +232,7 @@ async function packageSource(value) {
240
232
  return value;
241
233
  }
242
234
  async function localPackageProjectId() {
243
- return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}`)).project.id;
235
+ return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}`, packageProjectResponseSchema)).project.id;
244
236
  }
245
237
  async function resolveWorktree(identifier) {
246
238
  const all = (await projects()).flatMap((project) => project.worktrees);
@@ -264,12 +256,12 @@ function parseWebPanelInput(value) {
264
256
  } catch (error) {
265
257
  throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
266
258
  }
267
- const validated = webPanelInputSchema.safeParse(parsed);
268
- if (!validated.success) throw new CliError("--input must contain a JSON object", 2);
269
- return validated.data;
259
+ const validated = decodeUnknownOrNull(webPanelInputSchema, parsed);
260
+ if (!validated) throw new CliError("--input must contain a JSON object", 2);
261
+ return validated;
270
262
  }
271
263
  async function webPanelDefinition(worktreeId, identifier) {
272
- const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`)).definitions;
264
+ const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`, webPanelDefinitionsResponseSchema)).definitions;
273
265
  const exact = definitions.find((definition) => definition.id === identifier);
274
266
  if (exact) return exact;
275
267
  const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
@@ -304,7 +296,7 @@ async function resolveBrowserPanel(panelId) {
304
296
  }
305
297
  async function runBrowserAgentCommand(command, args, panelId) {
306
298
  const { panel } = await resolveBrowserPanel(panelId);
307
- const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, {
299
+ const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, browserAgentResponseSchema, {
308
300
  method: "POST",
309
301
  body: JSON.stringify({
310
302
  command,
@@ -346,7 +338,7 @@ function parseDuration(value) {
346
338
  }
347
339
  }
348
340
  async function inspectTerminal(terminalId, signal) {
349
- return request(`/api/terminals/${encodeURIComponent(terminalId)}`, signal ? { signal } : {});
341
+ return request(`/api/terminals/${encodeURIComponent(terminalId)}`, terminalObservationResponseSchema, signal ? { signal } : {});
350
342
  }
351
343
  async function waitForTerminal(terminalId, condition, timeoutMs) {
352
344
  let observation = null;
@@ -378,16 +370,9 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
378
370
  if (condition === "bell") bellBaseline = observation.metadata.bell?.sequence ?? 0;
379
371
  const immediate = matched((/* @__PURE__ */ new Date()).toISOString());
380
372
  if (immediate) return immediate;
381
- events = io(`${apiUrl}/events`, {
382
- path: SOCKET_IO_PATH,
383
- transports: ["websocket"],
384
- forceNew: true,
385
- autoConnect: false,
386
- reconnection: false,
387
- retries: 0
388
- });
389
373
  return await new Promise((resolve, reject) => {
390
374
  let settled = false;
375
+ let connected = false;
391
376
  let queue = Promise.resolve();
392
377
  const finish = (result) => {
393
378
  if (!settled) {
@@ -405,16 +390,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
405
390
  queue = queue.then(task);
406
391
  queue.catch(fail);
407
392
  };
408
- controller.signal.addEventListener("abort", () => fail(/* @__PURE__ */ new Error("Terminal wait cancelled")), { once: true });
409
- events.on("snapshot", (value) => enqueue(async () => {
410
- const snapshot = parseEventsSnapshot(value);
411
- if (!snapshot || !observation) throw new CliError("Treeport daemon sent an invalid event snapshot", 3, "DAEMON_PROTOCOL_ERROR");
412
- const metadata = snapshot.terminalMetadata.find((item) => item.terminalId === terminalId);
393
+ const snapshot = async (value) => {
394
+ if (!observation) throw new CliError("Treeport daemon sent an invalid event snapshot", 3, "DAEMON_PROTOCOL_ERROR");
395
+ connected = true;
396
+ const metadata = value.terminalMetadata.find((item) => item.terminalId === terminalId);
413
397
  if (metadata) observation = {
414
398
  ...observation,
415
399
  metadata
416
400
  };
417
- const snapshotMatch = matched(snapshot.at);
401
+ const snapshotMatch = matched(value.at);
418
402
  if (snapshotMatch) {
419
403
  finish(snapshotMatch);
420
404
  return;
@@ -423,10 +407,8 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
423
407
  if (cancellation) throw new Error("Terminal wait cancelled");
424
408
  const refreshedMatch = matched((/* @__PURE__ */ new Date()).toISOString());
425
409
  if (refreshedMatch) finish(refreshedMatch);
426
- }));
427
- events.on("product_event", (value) => enqueue(async () => {
428
- const event = parseProductEvent(value);
429
- if (!event) throw new CliError("Treeport daemon sent an invalid product event", 3, "DAEMON_PROTOCOL_ERROR");
410
+ };
411
+ const productEvent = async (event) => {
430
412
  if (event.type !== "terminal.metadata" && event.type !== "terminal.updated" && event.type !== "terminal.removed") return;
431
413
  if (event.data.terminalId !== terminalId) return;
432
414
  if (event.type === "terminal.removed") throw new CliError(`Terminal ${terminalId} was removed while waiting`, 5, "TERMINAL_REMOVED", {
@@ -446,10 +428,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
446
428
  }
447
429
  const result = matched(event.at);
448
430
  if (result) finish(result);
449
- }));
450
- events.on("connect_error", (error) => fail(new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error.message}`, 3, "DAEMON_UNREACHABLE")));
451
- events.on("disconnect", () => fail(new CliError("Treeport daemon event channel disconnected before the condition was observed", 3, "DAEMON_DISCONNECTED")));
452
- events.connect();
431
+ };
432
+ controller.signal.addEventListener("abort", () => fail(/* @__PURE__ */ new Error("Terminal wait cancelled")), { once: true });
433
+ const program = Effect.scoped(Effect.gen(function* () {
434
+ yield* (yield* RpcClient.make(TreeportRpcs)).WatchProjectEvents({ protocol: 3 }).pipe(Stream.runForEach((item) => Effect.sync(() => {
435
+ if (item._tag === "Snapshot") enqueue(() => snapshot(item.snapshot));
436
+ else enqueue(() => productEvent(item.event));
437
+ })));
438
+ })).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")))));
439
+ events = Effect.runFork(program);
453
440
  });
454
441
  } catch (error) {
455
442
  if (cancellation === "timeout") throw new CliError(`Timed out waiting for terminal ${terminalId} to reach ${condition}`, 4, "WAIT_TIMEOUT", {
@@ -465,8 +452,7 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
465
452
  if (timeout) clearTimeout(timeout);
466
453
  process.off("SIGINT", interrupt);
467
454
  controller.abort();
468
- events?.removeAllListeners();
469
- events?.disconnect();
455
+ if (events) Effect.runFork(Fiber.interrupt(events));
470
456
  }
471
457
  }
472
458
  function print(value, human) {
@@ -475,12 +461,17 @@ function print(value, human) {
475
461
  async function main(args) {
476
462
  const argv = args[0] === "spawn" || args[0] === "terminal" && args[1] === "create" ? commandArgv(args) : void 0;
477
463
  let parserError = "";
478
- 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({
464
+ const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, trees, and terminals.").version(output.heading(await treeportVersion()), "-v, --version", "show installed CLI version").argument("[folder]", "folder or folder inside a Git repository to open").option("--json", "emit machine-readable JSON").configureOutput({
465
+ getOutHasColors: () => output.enabled,
466
+ getErrHasColors: () => errorOutput.enabled,
479
467
  writeOut: writeStdout,
480
468
  writeErr: (value) => {
481
469
  parserError += value;
482
470
  }
483
- }).showHelpAfterError().exitOverride();
471
+ }).showHelpAfterError(jsonOutput).configureHelp({
472
+ styleTitle: output.heading,
473
+ styleCommandText: output.heading
474
+ }).exitOverride();
484
475
  program.action(async (folder) => {
485
476
  if (folder === void 0) {
486
477
  writeStdout(program.helpInformation());
@@ -499,7 +490,7 @@ async function main(args) {
499
490
  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");
500
491
  } else if (lifecycle === "service") await ensureServiceDaemon();
501
492
  else await daemonUp({});
502
- const registered = await request("/api/projects", {
493
+ const registered = await request("/api/projects", projectResponseSchema, {
503
494
  method: "POST",
504
495
  body: JSON.stringify({ path: canonicalFolder })
505
496
  });
@@ -512,7 +503,7 @@ async function main(args) {
512
503
  target.pathname = `/projects/${encodeURIComponent(registered.project.id)}/worktrees/${encodeURIComponent(targetWorktree.id)}`;
513
504
  target.search = "";
514
505
  target.hash = "";
515
- const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, {
506
+ const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, okResponseSchema, {
516
507
  method: "POST",
517
508
  body: JSON.stringify({ sourceTerminalId: contextTerminalId })
518
509
  }).then(() => ({ client: "current" })) : await openWorkspace(target.href).catch((error) => {
@@ -536,7 +527,7 @@ async function main(args) {
536
527
  if (lifecycle === "service") {
537
528
  if (options.foreground || options.host || options.port) throw new CliError("An installed service owns the listener and process mode. Run `treeport service enable` to refresh its configuration, or `treeport service disable` to return to local background mode.", 5, "DAEMON_LIFECYCLE_SERVICE");
538
529
  const result = await serviceStart();
539
- print(result, () => formatServiceStatus(result.status));
530
+ print(result, () => formatServiceStatus(result.status, output));
540
531
  if (result.administratorCommand || !result.status.healthy) requestedExitCode = 1;
541
532
  return;
542
533
  }
@@ -544,26 +535,29 @@ async function main(args) {
544
535
  const daemonOptions = {};
545
536
  if (options.host !== void 0) daemonOptions.host = options.host;
546
537
  if (port !== void 0) daemonOptions.port = port;
547
- if (options.foreground !== void 0) daemonOptions.foreground = options.foreground;
538
+ if (options.foreground !== void 0) {
539
+ daemonOptions.foreground = options.foreground;
540
+ daemonOptions.output = output;
541
+ }
548
542
  const result = await daemonUp(daemonOptions);
549
543
  if (options.foreground) return;
550
- print(result, () => `Treeport is running\n${result.apiUrl}`);
544
+ print(result, () => output.blocks(output.summary("Treeport is running", "success"), output.rows([["URL", result.apiUrl]])));
551
545
  });
552
- 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");
546
+ 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");
553
547
  stopCommand.action(async () => {
554
548
  const lifecycle = await resolveDaemonLifecycle();
555
549
  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");
556
550
  const options = stopCommand.opts();
557
551
  if (options.terminateTerminals && !options.force) throw new CliError("Re-run with --terminate-terminals --force to confirm termination of every terminal.", 2);
558
- if (options.terminateTerminals) await request("/api/admin/terminate-terminals", { method: "POST" });
552
+ if (options.terminateTerminals) await request("/api/admin/terminate-terminals", terminatedTerminalsResponseSchema, { method: "POST" });
559
553
  if (lifecycle === "service") {
560
554
  const result = await serviceStop();
561
- print(result, () => formatServiceStatus(result.status));
555
+ print(result, () => formatServiceStatus(result.status, output));
562
556
  if (result.administratorCommand) requestedExitCode = 1;
563
557
  return;
564
558
  }
565
559
  const result = await daemonDown();
566
- print(result, () => result.wasRunning ? "Treeport is stopped" : "Treeport is already stopped");
560
+ print(result, () => output.summary(result.wasRunning ? "Treeport is stopped" : "Treeport is already stopped", "success"));
567
561
  });
568
562
  const serviceCommand = program.command("service").description("Manage opt-in OS service supervision");
569
563
  serviceCommand.action(() => {
@@ -572,12 +566,12 @@ async function main(args) {
572
566
  const serviceEnableCommand = serviceCommand.command("enable").description("Enable automatic startup and unexpected-exit restarts").option("--headless", "use advanced macOS startup before login (requires an administrator)").option("--json", "emit machine-readable JSON");
573
567
  serviceEnableCommand.action(async () => {
574
568
  const result = await serviceEnable(serviceEnableCommand.opts().headless ? "headless" : "user");
575
- print(result, () => formatServiceStatus(result.status));
569
+ print(result, () => formatServiceStatus(result.status, output));
576
570
  if (result.status.state === "action_required") requestedExitCode = 1;
577
571
  });
578
572
  serviceCommand.command("status").description("Show OS service supervision status").option("--json", "emit machine-readable JSON").action(async () => {
579
573
  const result = await serviceStatus();
580
- print(result, () => formatServiceStatus(result));
574
+ print(result, () => formatServiceStatus(result, output));
581
575
  if (![
582
576
  "disabled",
583
577
  "healthy",
@@ -586,7 +580,7 @@ async function main(args) {
586
580
  });
587
581
  serviceCommand.command("disable").description("Stop and unregister OS service supervision").option("--json", "emit machine-readable JSON").action(async () => {
588
582
  const result = await serviceDisable();
589
- print(result, () => formatServiceStatus(result.status));
583
+ print(result, () => formatServiceStatus(result.status, output));
590
584
  if (result.administratorCommand || result.status.state !== "disabled") requestedExitCode = 1;
591
585
  });
592
586
  serviceCommand.command("run", { hidden: true }).action(async () => serviceRun());
@@ -594,7 +588,7 @@ async function main(args) {
594
588
  serviceApplyCommand.action(async () => {
595
589
  const { request: requestPath } = serviceApplyCommand.opts();
596
590
  const result = await serviceApply(requestPath);
597
- print(result, () => `Applied Treeport service ${result.operation} request.`);
591
+ print(result, () => output.summary(`Applied Treeport service ${result.operation} request.`, "success"));
598
592
  });
599
593
  const remoteCommand = program.command("remote").description("Expose Treeport privately through Tailscale Serve");
600
594
  remoteCommand.action(() => {
@@ -612,37 +606,48 @@ async function main(args) {
612
606
  if (port !== void 0) remoteOptions.port = port;
613
607
  if (serviceDaemon !== void 0) remoteOptions.daemon = serviceDaemon;
614
608
  const result = await enableTailscaleRemote(remoteOptions);
615
- print(result, () => `Treeport remote access is ${result.alreadyEnabled ? "already enabled" : "enabled"}\n${result.url}\nTailscale authenticates each remote user. Access is limited by your Tailscale policy.`);
609
+ print(result, () => output.blocks(output.summary(`Treeport remote access is ${result.alreadyEnabled ? "already enabled" : "enabled"}`, "success"), output.rows([["URL", result.url]]), "Tailscale authenticates each remote user. Access is limited by your Tailscale policy."));
616
610
  });
617
611
  remoteCommand.command("status").description("Show Tailscale remote access status").option("--json", "emit machine-readable JSON").action(async () => {
618
612
  const result = await tailscaleRemoteStatus();
619
613
  print(result, () => {
620
- if (!result.configured) return "Treeport remote access is disabled";
621
- return result.active ? `Treeport remote access is enabled\n${result.url}` : `Treeport remote access is unavailable\nExpected: ${result.url}\nThe Tailscale Serve route no longer points to Treeport.`;
614
+ if (!result.configured) return output.summary("Treeport remote access is disabled");
615
+ return output.blocks(output.summary(result.active ? "Treeport remote access is enabled" : "Treeport remote access is unavailable", result.active ? "success" : "warning"), output.rows([[result.active ? "URL" : "Expected URL", result.url ?? "Unavailable"]]), !result.active && "The Tailscale Serve route no longer points to Treeport.");
622
616
  });
623
617
  });
624
618
  remoteCommand.command("disable").description("Disable Treeport Tailscale remote access").option("--json", "emit machine-readable JSON").action(async () => {
625
619
  const result = await disableTailscaleRemote();
626
620
  print(result, () => {
627
- if (result.changedTailscale) return "Treeport remote access is disabled";
628
- return result.wasEnabled ? "Treeport remote access is disabled" : "Treeport remote access was already disabled; the current Tailscale route was left unchanged.";
621
+ if (result.changedTailscale) return output.summary("Treeport remote access is disabled", "success");
622
+ return output.summary(result.wasEnabled ? "Treeport remote access is disabled" : "Treeport remote access was already disabled; the current Tailscale route was left unchanged.", "success");
629
623
  });
630
624
  });
631
625
  program.command("status").description("Show local daemon status").option("--json", "emit machine-readable JSON").action(async () => {
632
- const status = await daemonStatus();
626
+ const [cliVersion, status] = await Promise.all([treeportVersion(), daemonStatus()]);
633
627
  const supervision = await serviceInstalled() ? await serviceStatus() : null;
628
+ const observed = supervision?.daemon ?? status;
629
+ const daemonVersion = observed.running && observed.verified ? observed.health?.version ?? null : null;
634
630
  const projectList = status.verified ? await projects() : [];
635
631
  const result = {
636
632
  ...status,
633
+ cliVersion,
634
+ daemonVersion,
637
635
  service: supervision,
638
636
  projects: projectList.length,
639
637
  worktrees: projectList.reduce((count, project) => count + project.worktrees.length, 0),
640
638
  terminals: projectList.reduce((count, project) => count + project.worktrees.reduce((worktreeCount, worktree) => worktreeCount + worktree.terminals.length, 0), 0)
641
639
  };
642
640
  print(result, () => {
643
- if (!status.state) return supervision ? formatServiceStatus(supervision) : "Treeport is stopped";
644
- if (!status.running || !status.verified) return `Treeport is unhealthy (PID ${status.state.pid})\nLogs: ${path.join(status.state.dataDir, "logs", "daemon.log")}`;
645
- return `Treeport is running\n${status.state.apiUrl}\nLifecycle: ${status.health?.daemonLifecycle}\nVersion: ${status.health?.version}\nPID: ${status.state.pid}\nProjects: ${result.projects}\nTrees: ${result.worktrees}\nTerminals: ${result.terminals}`;
641
+ const healthy = observed.running && observed.verified;
642
+ return output.blocks(output.heading("Treeport status"), output.summary(!observed.state ? "Treeport is stopped" : healthy ? "Treeport is running" : "Treeport is unhealthy", !observed.state ? "neutral" : healthy ? "success" : "failure"), output.rows([
643
+ ["CLI version", cliVersion],
644
+ ["Daemon version", daemonVersion ?? "Unavailable"],
645
+ observed.state ? ["URL", observed.state.apiUrl] : null
646
+ ]), daemonVersion !== null && daemonVersion !== cliVersion && output.summary("Version mismatch: CLI and daemon versions differ", "warning"), healthy && output.detail(output.rows([
647
+ supervision ? null : ["Lifecycle", observed.health?.daemonLifecycle === "external" ? "Externally managed" : observed.health?.daemonLifecycle === "service" ? "OS service" : "Treeport"],
648
+ supervision ? null : ["PID", observed.state.pid],
649
+ status.verified ? ["Workspace", `${result.projects} projects · ${result.worktrees} trees · ${result.terminals} terminals`] : null
650
+ ])), !healthy && observed.state !== null && output.rows([["PID", observed.state.pid], ["Logs", path.join(observed.state.dataDir, "logs", "daemon.log")]]), supervision !== null && formatServiceStatus(supervision, output));
646
651
  });
647
652
  });
648
653
  const logsCommand = program.command("logs").description("Show recent daemon logs").option("--lines <count>", "number of lines", "100");
@@ -653,7 +658,7 @@ async function main(args) {
653
658
  });
654
659
  program.command("doctor").description("Diagnose local requirements and paths").option("--json", "emit machine-readable JSON").action(async () => {
655
660
  const checks = [...await runDoctor(), await serviceDoctorCheck()];
656
- print(checks, () => checks.map((check) => `${check.ok ? "ok" : "error"}\t${check.name}\t${check.detail}`).join("\n"));
661
+ print(checks, () => output.blocks(output.heading("Treeport doctor"), output.summary(checks.every((check) => check.ok) ? "All checks passed" : "Some checks failed", checks.every((check) => check.ok) ? "success" : "failure"), ...checks.map((check) => `${output.summary(check.name, check.ok ? "success" : "failure")}\n${output.indent(check.detail.startsWith("state: ") ? stateName(check.detail.slice(7)) : check.ok ? output.detail(check.detail) : check.detail)}`)));
657
662
  if (checks.some((check) => !check.ok)) requestedExitCode = 1;
658
663
  });
659
664
  program.command("version").description("Show CLI and daemon versions").option("--json", "emit machine-readable JSON").action(async () => {
@@ -662,7 +667,7 @@ async function main(args) {
662
667
  cli,
663
668
  daemon: status.verified ? status.health?.version ?? null : null
664
669
  };
665
- print(result, () => `CLI: ${result.cli}\nDaemon: ${result.daemon ?? "not running"}`);
670
+ print(result, () => output.blocks(output.heading("Treeport version"), output.rows([["CLI", result.cli], ["Daemon", result.daemon ?? "Unavailable"]]), result.daemon !== null && result.daemon !== result.cli && output.summary("Version mismatch: CLI and daemon versions differ", "warning")));
666
671
  });
667
672
  program.command("skills").description("Print the Treeport usage guide for AI agents").action(async () => {
668
673
  const skill = await fs.readFile(await resolvePackagePath("skills", "treeport", "SKILL.md"), "utf8");
@@ -680,7 +685,7 @@ async function main(args) {
680
685
  print({
681
686
  managed: false,
682
687
  reason: "outside_treeport"
683
- }, () => "Not running in a Treeport-managed terminal.");
688
+ }, () => output.blocks(output.heading("Treeport context"), "Not running in a Treeport-managed terminal."));
684
689
  return;
685
690
  }
686
691
  const missing = [
@@ -690,7 +695,7 @@ async function main(args) {
690
695
  ...!terminalId ? [`${contextPrefix}_TERMINAL_ID`] : []
691
696
  ];
692
697
  if (missing.length) throw new CliError(`Incomplete Treeport context; missing ${missing.join(", ")}`, 5, "TREEPORT_CONTEXT_INCOMPLETE", { missing });
693
- const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project;
698
+ const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project;
694
699
  const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
695
700
  if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
696
701
  projectId,
@@ -701,7 +706,7 @@ async function main(args) {
701
706
  worktreeId,
702
707
  terminalId
703
708
  });
704
- const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`)).context;
709
+ const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`, treeContextResponseSchema)).context;
705
710
  const context = {
706
711
  managed: true,
707
712
  apiUrl,
@@ -744,7 +749,12 @@ async function main(args) {
744
749
  }).join("").split("\n");
745
750
  return ` ${key}: ${first}${rest.length > 0 ? `\n${rest.map((line) => ` ${line}`).join("\n")}` : ""}`;
746
751
  }).join("\n");
747
- 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"}`;
752
+ return output.blocks(output.heading("Treeport context"), output.rows([
753
+ ["Project", `${context.project.name} (${context.project.id})`],
754
+ ["Tree", `${context.worktree.name} (${context.worktree.id})`],
755
+ ["Path", context.worktree.path],
756
+ ["Terminal", `${context.terminal.name} (${context.terminal.id}) — ${stateName(context.terminal.status)}`]
757
+ ]), entries.length > 0 && `${output.heading("Tree context")}\n${treeContextText}`, output.detail(output.rows([["API", context.apiUrl], ["Lifecycle", context.daemonLifecycle === "external" ? "Externally managed" : context.daemonLifecycle === "service" ? "OS service" : "Treeport"]])));
748
758
  });
749
759
  });
750
760
  const browserCommand = program.command("browser").description("Manage Browser and its hosted Chromium");
@@ -758,7 +768,7 @@ async function main(args) {
758
768
  url,
759
769
  sourceTerminalId: contextWorktreeId === worktree.id ? contextTerminalId ?? null : null
760
770
  };
761
- const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, {
771
+ const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, openBrowserPanelResponseSchema, {
762
772
  method: "POST",
763
773
  body: JSON.stringify(body)
764
774
  });
@@ -769,15 +779,15 @@ async function main(args) {
769
779
  print(output, () => `Opened ${result.panel.title} (${result.panel.id})\n${output.url}`);
770
780
  });
771
781
  browserCommand.command("install").description("Install the Chromium build used by Browser").option("--json", "emit machine-readable JSON").action(async () => {
772
- const result = await request("/api/browser/install", { method: "POST" });
782
+ const result = await request("/api/browser/install", browserInstallResponseSchema, { method: "POST" });
773
783
  print(result, () => result.message);
774
784
  });
775
785
  browserCommand.command("status").description("Show hosted browser installation status").option("--json", "emit machine-readable JSON").action(async () => {
776
- const result = await request("/api/browser/status");
786
+ const result = await request("/api/browser/status", browserInstallStatusSchema);
777
787
  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}` : ""}`);
778
788
  });
779
789
  browserCommand.command("remove").description("Remove Treeport's hosted Chromium build").option("--json", "emit machine-readable JSON").action(async () => {
780
- await request("/api/browser/install", { method: "DELETE" });
790
+ await request("/api/browser/install", okResponseSchema, { method: "DELETE" });
781
791
  print({ removed: true }, () => "Removed Treeport hosted Chromium");
782
792
  });
783
793
  browserCommand.command("list").description("List open Browser sessions").option("--json", "emit machine-readable JSON").action(async () => {
@@ -842,7 +852,7 @@ async function main(args) {
842
852
  const options = installCommand.opts();
843
853
  const body = { source: await packageSource(source) };
844
854
  if (options.local) body.projectId = await localPackageProjectId();
845
- const result = (await request("/api/packages/install", {
855
+ const result = (await request("/api/packages/install", packageOperationResponseSchema, {
846
856
  method: "POST",
847
857
  body: JSON.stringify(body)
848
858
  })).result;
@@ -853,14 +863,14 @@ async function main(args) {
853
863
  const options = removePackageCommand.opts();
854
864
  const body = { source: await packageSource(source) };
855
865
  if (options.local) body.projectId = await localPackageProjectId();
856
- const result = (await request("/api/packages/remove", {
866
+ const result = (await request("/api/packages/remove", packageOperationResponseSchema, {
857
867
  method: "POST",
858
868
  body: JSON.stringify(body)
859
869
  })).result;
860
870
  print(result, () => `Removed ${result.source}`);
861
871
  });
862
872
  program.command("list").description("List configured Treeport packages").option("--json", "emit machine-readable JSON").action(async () => {
863
- const result = await request("/api/packages");
873
+ const result = await request("/api/packages", packageListingResponseSchema);
864
874
  print(result, () => {
865
875
  const lines = result.packages.map((pkg) => {
866
876
  return `${pkg.scope === "global" ? "global" : `project:${pkg.projectName ?? pkg.projectId}`}\t${pkg.source}\t${pkg.resources.webPanels} web panels, ${pkg.resources.terminalPresets} terminal presets`;
@@ -869,34 +879,41 @@ async function main(args) {
869
879
  return lines.join("\n");
870
880
  });
871
881
  });
872
- const updatePackagesCommand = program.command("update").description("Update Treeport or explicitly update configured packages").argument("[source]", "one configured npm: source").option("--packages", "update every eligible configured package").option("--json", "emit machine-readable JSON");
882
+ const updatePackagesCommand = program.command("update").description("Update Treeport or explicitly update configured packages").argument("[source]", "one configured npm: source").option("--packages", "update every eligible configured package").option("-y, --yes", "approve the Treeport self-update without prompting").option("--start", "start Treeport after a self-update if it was stopped").option("--json", "emit machine-readable JSON");
873
883
  updatePackagesCommand.action(async (source) => {
874
884
  const options = updatePackagesCommand.opts();
875
885
  if (source && options.packages) throw new CliError("Specify a package source or --packages, not both.", 2);
886
+ if ((source || options.packages) && (options.yes || options.start)) throw new CliError("--yes and --start apply only to Treeport self-updates.", 2);
876
887
  if (!source && !options.packages) {
877
888
  if (await resolveDaemonLifecycle() === "external") throw new CliError("Cannot update Treeport because this daemon lifecycle is externally managed.", 5, "UPDATE_EXTERNAL_REFUSED");
878
- const selfUpdateOptions = { environment: cliEnvironment };
879
- if (!jsonOutput) selfUpdateOptions.progress = (message) => writeStderr(`${message}\n`);
880
- const result = await runLocalUpdate(selfUpdateOptions).catch((error) => {
881
- if (error instanceof LocalUpdateError) throw new CliError(error.message, error.exitCode, error.code, error.details);
882
- throw error;
883
- });
884
- print(result, () => {
885
- if (result.status === "current") return `Treeport ${result.toVersion} is current`;
886
- return result.daemon.wasRunning ? `Updated Treeport from ${result.fromVersion} to ${result.toVersion} and restarted the ${result.daemon.lifecycle === "service" ? "service" : "daemon"}` : `Updated Treeport from ${result.fromVersion} to ${result.toVersion}; Treeport remains stopped`;
887
- });
889
+ const selfUpdateOptions = {
890
+ environment: cliEnvironment,
891
+ yes: options.yes ?? false,
892
+ start: options.start ?? false
893
+ };
894
+ if (!jsonOutput && process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY) selfUpdateOptions.confirm = (preview, signal) => confirmLocalUpdate(preview, signal, process.stdin, process.stderr, errorOutput);
895
+ if (!jsonOutput) selfUpdateOptions.progress = (message) => {
896
+ if (!updateProgressShown) {
897
+ writeStderr(`${errorOutput.heading("Treeport update")}\n\n`);
898
+ updateProgressShown = true;
899
+ }
900
+ writeStderr(`${errorOutput.indent(errorOutput.summary(message, "warning"))}\n`);
901
+ };
902
+ const result = await runLocalUpdate(selfUpdateOptions);
903
+ if (updateProgressShown) writeStderr("\n");
904
+ print(result, () => formatLocalUpdateResult(result, output));
888
905
  return;
889
906
  }
890
- const results = (await request("/api/packages/update", {
907
+ const results = (await request("/api/packages/update", packageOperationsResponseSchema, {
891
908
  method: "POST",
892
909
  body: JSON.stringify(source ? { source: await packageSource(source) } : {})
893
910
  })).results;
894
- print(results, () => results.map((result) => `${result.status}\t${result.scope}\t${result.source ?? "packages"}${result.reason ? `\t${result.reason}` : ""}`).join("\n"));
911
+ print(results, () => output.blocks(output.heading("Treeport package update"), ...results.map((result) => output.blocks(output.summary(`${stateName(result.status)} ${result.source ?? "packages"}`, result.status === "skipped" ? "warning" : "success"), output.detail(output.rows([["Scope", stateName(result.scope)]])), result.reason ? output.indent(result.reason) : null))));
895
912
  });
896
913
  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");
897
914
  reloadCommand.action(async () => {
898
915
  const options = reloadCommand.opts();
899
- const result = await request("/api/packages/reload", {
916
+ const result = await request("/api/packages/reload", packageReloadResponseSchema, {
900
917
  method: "POST",
901
918
  body: JSON.stringify(options.local ? { projectId: await localPackageProjectId() } : {})
902
919
  });
@@ -909,7 +926,7 @@ async function main(args) {
909
926
  throw new CliError(projectCommand.helpInformation(), 2);
910
927
  });
911
928
  projectCommand.command("add").description("Register a folder or Git repository").argument("<path>", "folder path").option("--json", "emit machine-readable JSON").action(async (repository) => {
912
- const body = await request("/api/projects", {
929
+ const body = await request("/api/projects", projectResponseSchema, {
913
930
  method: "POST",
914
931
  body: JSON.stringify({ path: await canonical(repository) })
915
932
  });
@@ -942,25 +959,46 @@ async function main(args) {
942
959
  const result = await createWorktree(project.id, request);
943
960
  print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
944
961
  });
945
- 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");
962
+ 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");
946
963
  worktreeRemoveCommand.action(async (identifier) => {
947
- const { force: confirmed } = worktreeRemoveCommand.opts();
964
+ const { force: confirmed, skipCleanup: requestedSkipCleanup } = worktreeRemoveCommand.opts();
948
965
  const worktree = await resolveWorktree(identifier);
949
- const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`)).preview;
966
+ const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`, removePreviewResponseSchema)).preview;
950
967
  if (!preview.eligible) throw new CliError(preview.reasons.join("\n"), 5);
951
968
  if (preview.warnings.length && !confirmed) throw new CliError(`${preview.warnings.join("\n")}\nRe-run with --force to confirm removal.`, 5);
952
- let operation = (await request(`/api/worktrees/${worktree.id}/remove`, {
969
+ const skipCleanup = Boolean(requestedSkipCleanup) && preview.cleanup.commands.length > 0;
970
+ if (skipCleanup && !confirmed) throw new CliError("Skipping project cleanup can leave project resources behind.\nRe-run with --force --skip-cleanup to confirm removal.", 5);
971
+ let operation = (await request(`/api/worktrees/${worktree.id}/remove`, operationResponseSchema, {
953
972
  method: "POST",
954
973
  body: JSON.stringify({
955
974
  confirmationToken: preview.confirmationToken,
956
- confirmDestructive: preview.warnings.length > 0
975
+ confirmDestructive: preview.warnings.length > 0 || skipCleanup,
976
+ skipCleanup
957
977
  })
958
978
  })).operation;
979
+ const displayedCleanupCommands = /* @__PURE__ */ new Set();
980
+ const displayFinishedCleanup = () => {
981
+ if (jsonOutput || operation.kind !== "remove") return;
982
+ operation.request.cleanupCommands.commands.forEach((command, index) => {
983
+ if (displayedCleanupCommands.has(index) || command.status !== "completed" && command.status !== "failed") return;
984
+ displayedCleanupCommands.add(index);
985
+ const lines = [`Cleanup: ${command.name}${command.status === "failed" ? " (failed)" : ""}`];
986
+ if (command.stdout) lines.push(command.stdout.replace(/\n$/u, ""));
987
+ if (command.stderr) lines.push(command.stderr.replace(/\n$/u, ""));
988
+ if (command.outputTruncated) lines.push("Cleanup output was truncated.");
989
+ writeStdout(`${lines.join("\n")}\n`);
990
+ });
991
+ };
992
+ displayFinishedCleanup();
959
993
  while (operation.status === "pending" || operation.status === "running") {
960
994
  await new Promise((resolve) => setTimeout(resolve, 100));
961
- operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
995
+ operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
996
+ displayFinishedCleanup();
997
+ }
998
+ if (operation.status === "failed") {
999
+ const cleanup = operation.kind === "remove" ? operation.request.cleanupCommands.commands.find((command) => command.status === "failed") : null;
1000
+ throw new CliError(operation.error ?? "Tree removal failed; Git kept the tree.", 5, "WORKTREE_REMOVAL_FAILED", cleanup ?? void 0);
962
1001
  }
963
- if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
964
1002
  if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
965
1003
  print(operation.result, () => {
966
1004
  const warning = operation.result?.cleanup.warning;
@@ -976,7 +1014,7 @@ async function main(args) {
976
1014
  const options = webPanelOpenCommand.opts();
977
1015
  const worktree = await resolveWorktree(options.worktree);
978
1016
  const definition = await webPanelDefinition(worktree.id, identifier);
979
- const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, {
1017
+ const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, openWebPanelResponseSchema, {
980
1018
  method: "POST",
981
1019
  body: JSON.stringify({
982
1020
  definitionId: definition.id,
@@ -1008,7 +1046,7 @@ async function main(args) {
1008
1046
  const worktree = await resolveWorktree(options.worktree);
1009
1047
  const body = { name: options.name };
1010
1048
  if (argv) body.argv = argv;
1011
- const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
1049
+ const result = await request(`/api/worktrees/${worktree.id}/terminals`, terminalResponseSchema, {
1012
1050
  method: "POST",
1013
1051
  body: JSON.stringify(body)
1014
1052
  });
@@ -1028,7 +1066,7 @@ async function main(args) {
1028
1066
  const { lines: rawLines } = terminalCaptureCommand.opts();
1029
1067
  const lines = rawLines === void 0 ? 200 : parseCaptureLines(rawLines);
1030
1068
  const terminalId = resolveTerminalId(identifier);
1031
- const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`);
1069
+ const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`, terminalCaptureResponseSchema);
1032
1070
  if (jsonOutput) print(capture);
1033
1071
  else {
1034
1072
  writeStdout(capture.content);
@@ -1048,7 +1086,7 @@ async function main(args) {
1048
1086
  print(result, () => `${result.terminal.name} (${result.terminal.id}) reached ${result.condition} at ${result.observedAt}`);
1049
1087
  });
1050
1088
  terminalCommand.command("delete").description("Delete a terminal").argument("<terminal-id>", "terminal to delete").option("--json", "emit machine-readable JSON").action(async (terminalId) => {
1051
- await request(`/api/terminals/${terminalId}`, { method: "DELETE" });
1089
+ await request(`/api/terminals/${terminalId}`, okResponseSchema, { method: "DELETE" });
1052
1090
  print({
1053
1091
  ok: true,
1054
1092
  terminalId
@@ -1094,18 +1132,27 @@ async function runCliApplication(options) {
1094
1132
  writeStdout = options.stdout ?? ((value) => process.stdout.write(value));
1095
1133
  writeStderr = options.stderr ?? ((value) => process.stderr.write(value));
1096
1134
  requestedExitCode = 0;
1135
+ updateProgressShown = false;
1136
+ output = humanOutput(environment, options.stdoutIsTTY ?? (!options.stdout && Boolean(process.stdout.isTTY)), jsonOutput);
1137
+ errorOutput = humanOutput(environment, options.stderrIsTTY ?? (!options.stderr && Boolean(process.stderr.isTTY)), jsonOutput);
1097
1138
  try {
1098
1139
  await main([...options.args]);
1099
1140
  } catch (error) {
1100
- const cliError = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
1141
+ const cliError = error instanceof CliError || error instanceof LocalUpdateError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
1101
1142
  if (jsonOutput) {
1102
- const body = { error: {
1143
+ const body = { error: cliError.details === void 0 ? {
1103
1144
  code: cliError.code,
1104
1145
  message: cliError.message
1146
+ } : {
1147
+ code: cliError.code,
1148
+ message: cliError.message,
1149
+ details: cliError.details
1105
1150
  } };
1106
- if (cliError.details !== void 0) body.error.details = cliError.details;
1107
1151
  writeStderr(`${JSON.stringify(body)}\n`);
1108
- } else writeStderr(`${cliError.message}\n`);
1152
+ } else {
1153
+ const text = error instanceof LocalUpdateError ? formatLocalUpdateError(error.message, error.details, errorOutput, error.code === "UPDATE_CANCELLED" || error.code === "UPDATE_INTERRUPTED") : errorOutput.blocks(errorOutput.summary(cliError.code === "USAGE_ERROR" ? "Invalid command" : "Command failed", "failure"), errorOutput.indent(cliError.message.replace(/^error: /, "")), cliError.code === "USAGE_ERROR" && errorOutput.next(["treeport --help"]));
1154
+ writeStderr(`${updateProgressShown ? "\n" : ""}${text}\n`);
1155
+ }
1109
1156
  requestedExitCode = cliError.exitCode;
1110
1157
  }
1111
1158
  return requestedExitCode;