@treeport/treeport 0.6.1 → 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,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 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";
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) {
@@ -146,7 +149,7 @@ async function ensureServiceDaemon() {
146
149
  pid: state.pid
147
150
  };
148
151
  }
149
- async function request(pathname, options = {}) {
152
+ async function request(pathname, schema, options = {}) {
150
153
  const controller = new AbortController();
151
154
  const externalSignal = options.signal;
152
155
  const abort = () => controller.abort();
@@ -164,10 +167,12 @@ async function request(pathname, options = {}) {
164
167
  });
165
168
  const body = await response.json().catch(() => ({}));
166
169
  if (!response.ok) {
167
- const error = body.error;
168
- 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);
169
172
  }
170
- 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;
171
176
  } catch (error) {
172
177
  if (error instanceof CliError) throw error;
173
178
  throw new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error instanceof Error ? error.message : String(error)}`, 3, "DAEMON_UNREACHABLE");
@@ -177,19 +182,19 @@ async function request(pathname, options = {}) {
177
182
  }
178
183
  }
179
184
  async function createWorktree(projectId, input) {
180
- let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, {
185
+ let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, operationResponseSchema, {
181
186
  method: "POST",
182
187
  body: JSON.stringify(input)
183
188
  })).operation;
184
189
  while (operation.status === "pending" || operation.status === "running") {
185
190
  await new Promise((resolve) => setTimeout(resolve, 100));
186
- operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
191
+ operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
187
192
  }
188
193
  if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
189
194
  if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
190
195
  const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
191
196
  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);
197
+ const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project.worktrees.find((item) => item.id === worktreeId);
193
198
  if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
194
199
  const terminalId = operation.result?.terminalId ?? null;
195
200
  return {
@@ -212,7 +217,7 @@ async function canonical(value) {
212
217
  return fs.realpath(resolved).catch(() => resolved);
213
218
  }
214
219
  async function projects() {
215
- return (await request("/api/projects")).projects;
220
+ return (await request("/api/projects", projectsResponseSchema)).projects;
216
221
  }
217
222
  function pathContains(candidate, parent) {
218
223
  const relative = path.relative(parent, candidate);
@@ -240,7 +245,7 @@ async function packageSource(value) {
240
245
  return value;
241
246
  }
242
247
  async function localPackageProjectId() {
243
- 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;
244
249
  }
245
250
  async function resolveWorktree(identifier) {
246
251
  const all = (await projects()).flatMap((project) => project.worktrees);
@@ -264,12 +269,12 @@ function parseWebPanelInput(value) {
264
269
  } catch (error) {
265
270
  throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
266
271
  }
267
- const validated = webPanelInputSchema.safeParse(parsed);
268
- if (!validated.success) throw new CliError("--input must contain a JSON object", 2);
269
- 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;
270
275
  }
271
276
  async function webPanelDefinition(worktreeId, identifier) {
272
- 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;
273
278
  const exact = definitions.find((definition) => definition.id === identifier);
274
279
  if (exact) return exact;
275
280
  const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
@@ -304,7 +309,7 @@ async function resolveBrowserPanel(panelId) {
304
309
  }
305
310
  async function runBrowserAgentCommand(command, args, panelId) {
306
311
  const { panel } = await resolveBrowserPanel(panelId);
307
- const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, {
312
+ const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, browserAgentResponseSchema, {
308
313
  method: "POST",
309
314
  body: JSON.stringify({
310
315
  command,
@@ -346,7 +351,7 @@ function parseDuration(value) {
346
351
  }
347
352
  }
348
353
  async function inspectTerminal(terminalId, signal) {
349
- return request(`/api/terminals/${encodeURIComponent(terminalId)}`, signal ? { signal } : {});
354
+ return request(`/api/terminals/${encodeURIComponent(terminalId)}`, terminalObservationResponseSchema, signal ? { signal } : {});
350
355
  }
351
356
  async function waitForTerminal(terminalId, condition, timeoutMs) {
352
357
  let observation = null;
@@ -378,16 +383,9 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
378
383
  if (condition === "bell") bellBaseline = observation.metadata.bell?.sequence ?? 0;
379
384
  const immediate = matched((/* @__PURE__ */ new Date()).toISOString());
380
385
  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
386
  return await new Promise((resolve, reject) => {
390
387
  let settled = false;
388
+ let connected = false;
391
389
  let queue = Promise.resolve();
392
390
  const finish = (result) => {
393
391
  if (!settled) {
@@ -405,16 +403,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
405
403
  queue = queue.then(task);
406
404
  queue.catch(fail);
407
405
  };
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);
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);
413
410
  if (metadata) observation = {
414
411
  ...observation,
415
412
  metadata
416
413
  };
417
- const snapshotMatch = matched(snapshot.at);
414
+ const snapshotMatch = matched(value.at);
418
415
  if (snapshotMatch) {
419
416
  finish(snapshotMatch);
420
417
  return;
@@ -423,10 +420,8 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
423
420
  if (cancellation) throw new Error("Terminal wait cancelled");
424
421
  const refreshedMatch = matched((/* @__PURE__ */ new Date()).toISOString());
425
422
  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");
423
+ };
424
+ const productEvent = async (event) => {
430
425
  if (event.type !== "terminal.metadata" && event.type !== "terminal.updated" && event.type !== "terminal.removed") return;
431
426
  if (event.data.terminalId !== terminalId) return;
432
427
  if (event.type === "terminal.removed") throw new CliError(`Terminal ${terminalId} was removed while waiting`, 5, "TERMINAL_REMOVED", {
@@ -446,10 +441,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
446
441
  }
447
442
  const result = matched(event.at);
448
443
  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();
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);
453
453
  });
454
454
  } catch (error) {
455
455
  if (cancellation === "timeout") throw new CliError(`Timed out waiting for terminal ${terminalId} to reach ${condition}`, 4, "WAIT_TIMEOUT", {
@@ -465,8 +465,7 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
465
465
  if (timeout) clearTimeout(timeout);
466
466
  process.off("SIGINT", interrupt);
467
467
  controller.abort();
468
- events?.removeAllListeners();
469
- events?.disconnect();
468
+ if (events) Effect.runFork(Fiber.interrupt(events));
470
469
  }
471
470
  }
472
471
  function print(value, human) {
@@ -499,7 +498,7 @@ async function main(args) {
499
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");
500
499
  } else if (lifecycle === "service") await ensureServiceDaemon();
501
500
  else await daemonUp({});
502
- const registered = await request("/api/projects", {
501
+ const registered = await request("/api/projects", projectResponseSchema, {
503
502
  method: "POST",
504
503
  body: JSON.stringify({ path: canonicalFolder })
505
504
  });
@@ -512,7 +511,7 @@ async function main(args) {
512
511
  target.pathname = `/projects/${encodeURIComponent(registered.project.id)}/worktrees/${encodeURIComponent(targetWorktree.id)}`;
513
512
  target.search = "";
514
513
  target.hash = "";
515
- const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, {
514
+ const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, okResponseSchema, {
516
515
  method: "POST",
517
516
  body: JSON.stringify({ sourceTerminalId: contextTerminalId })
518
517
  }).then(() => ({ client: "current" })) : await openWorkspace(target.href).catch((error) => {
@@ -549,13 +548,13 @@ async function main(args) {
549
548
  if (options.foreground) return;
550
549
  print(result, () => `Treeport is running\n${result.apiUrl}`);
551
550
  });
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");
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");
553
552
  stopCommand.action(async () => {
554
553
  const lifecycle = await resolveDaemonLifecycle();
555
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");
556
555
  const options = stopCommand.opts();
557
556
  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" });
557
+ if (options.terminateTerminals) await request("/api/admin/terminate-terminals", terminatedTerminalsResponseSchema, { method: "POST" });
559
558
  if (lifecycle === "service") {
560
559
  const result = await serviceStop();
561
560
  print(result, () => formatServiceStatus(result.status));
@@ -690,7 +689,7 @@ async function main(args) {
690
689
  ...!terminalId ? [`${contextPrefix}_TERMINAL_ID`] : []
691
690
  ];
692
691
  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;
692
+ const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project;
694
693
  const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
695
694
  if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
696
695
  projectId,
@@ -701,7 +700,7 @@ async function main(args) {
701
700
  worktreeId,
702
701
  terminalId
703
702
  });
704
- const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`)).context;
703
+ const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`, treeContextResponseSchema)).context;
705
704
  const context = {
706
705
  managed: true,
707
706
  apiUrl,
@@ -758,7 +757,7 @@ async function main(args) {
758
757
  url,
759
758
  sourceTerminalId: contextWorktreeId === worktree.id ? contextTerminalId ?? null : null
760
759
  };
761
- const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, {
760
+ const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, openBrowserPanelResponseSchema, {
762
761
  method: "POST",
763
762
  body: JSON.stringify(body)
764
763
  });
@@ -769,15 +768,15 @@ async function main(args) {
769
768
  print(output, () => `Opened ${result.panel.title} (${result.panel.id})\n${output.url}`);
770
769
  });
771
770
  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" });
771
+ const result = await request("/api/browser/install", browserInstallResponseSchema, { method: "POST" });
773
772
  print(result, () => result.message);
774
773
  });
775
774
  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");
775
+ const result = await request("/api/browser/status", browserInstallStatusSchema);
777
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}` : ""}`);
778
777
  });
779
778
  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" });
779
+ await request("/api/browser/install", okResponseSchema, { method: "DELETE" });
781
780
  print({ removed: true }, () => "Removed Treeport hosted Chromium");
782
781
  });
783
782
  browserCommand.command("list").description("List open Browser sessions").option("--json", "emit machine-readable JSON").action(async () => {
@@ -842,7 +841,7 @@ async function main(args) {
842
841
  const options = installCommand.opts();
843
842
  const body = { source: await packageSource(source) };
844
843
  if (options.local) body.projectId = await localPackageProjectId();
845
- const result = (await request("/api/packages/install", {
844
+ const result = (await request("/api/packages/install", packageOperationResponseSchema, {
846
845
  method: "POST",
847
846
  body: JSON.stringify(body)
848
847
  })).result;
@@ -853,14 +852,14 @@ async function main(args) {
853
852
  const options = removePackageCommand.opts();
854
853
  const body = { source: await packageSource(source) };
855
854
  if (options.local) body.projectId = await localPackageProjectId();
856
- const result = (await request("/api/packages/remove", {
855
+ const result = (await request("/api/packages/remove", packageOperationResponseSchema, {
857
856
  method: "POST",
858
857
  body: JSON.stringify(body)
859
858
  })).result;
860
859
  print(result, () => `Removed ${result.source}`);
861
860
  });
862
861
  program.command("list").description("List configured Treeport packages").option("--json", "emit machine-readable JSON").action(async () => {
863
- const result = await request("/api/packages");
862
+ const result = await request("/api/packages", packageListingResponseSchema);
864
863
  print(result, () => {
865
864
  const lines = result.packages.map((pkg) => {
866
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`;
@@ -878,7 +877,7 @@ async function main(args) {
878
877
  const selfUpdateOptions = { environment: cliEnvironment };
879
878
  if (!jsonOutput) selfUpdateOptions.progress = (message) => writeStderr(`${message}\n`);
880
879
  const result = await runLocalUpdate(selfUpdateOptions).catch((error) => {
881
- 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);
882
881
  throw error;
883
882
  });
884
883
  print(result, () => {
@@ -887,7 +886,7 @@ async function main(args) {
887
886
  });
888
887
  return;
889
888
  }
890
- const results = (await request("/api/packages/update", {
889
+ const results = (await request("/api/packages/update", packageOperationsResponseSchema, {
891
890
  method: "POST",
892
891
  body: JSON.stringify(source ? { source: await packageSource(source) } : {})
893
892
  })).results;
@@ -896,7 +895,7 @@ async function main(args) {
896
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");
897
896
  reloadCommand.action(async () => {
898
897
  const options = reloadCommand.opts();
899
- const result = await request("/api/packages/reload", {
898
+ const result = await request("/api/packages/reload", packageReloadResponseSchema, {
900
899
  method: "POST",
901
900
  body: JSON.stringify(options.local ? { projectId: await localPackageProjectId() } : {})
902
901
  });
@@ -909,7 +908,7 @@ async function main(args) {
909
908
  throw new CliError(projectCommand.helpInformation(), 2);
910
909
  });
911
910
  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", {
911
+ const body = await request("/api/projects", projectResponseSchema, {
913
912
  method: "POST",
914
913
  body: JSON.stringify({ path: await canonical(repository) })
915
914
  });
@@ -942,25 +941,46 @@ async function main(args) {
942
941
  const result = await createWorktree(project.id, request);
943
942
  print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
944
943
  });
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");
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");
946
945
  worktreeRemoveCommand.action(async (identifier) => {
947
- const { force: confirmed } = worktreeRemoveCommand.opts();
946
+ const { force: confirmed, skipCleanup: requestedSkipCleanup } = worktreeRemoveCommand.opts();
948
947
  const worktree = await resolveWorktree(identifier);
949
- const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`)).preview;
948
+ const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`, removePreviewResponseSchema)).preview;
950
949
  if (!preview.eligible) throw new CliError(preview.reasons.join("\n"), 5);
951
950
  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`, {
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, {
953
954
  method: "POST",
954
955
  body: JSON.stringify({
955
956
  confirmationToken: preview.confirmationToken,
956
- confirmDestructive: preview.warnings.length > 0
957
+ confirmDestructive: preview.warnings.length > 0 || skipCleanup,
958
+ skipCleanup
957
959
  })
958
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();
959
975
  while (operation.status === "pending" || operation.status === "running") {
960
976
  await new Promise((resolve) => setTimeout(resolve, 100));
961
- 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);
962
983
  }
963
- if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
964
984
  if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
965
985
  print(operation.result, () => {
966
986
  const warning = operation.result?.cleanup.warning;
@@ -976,7 +996,7 @@ async function main(args) {
976
996
  const options = webPanelOpenCommand.opts();
977
997
  const worktree = await resolveWorktree(options.worktree);
978
998
  const definition = await webPanelDefinition(worktree.id, identifier);
979
- const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, {
999
+ const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, openWebPanelResponseSchema, {
980
1000
  method: "POST",
981
1001
  body: JSON.stringify({
982
1002
  definitionId: definition.id,
@@ -1008,7 +1028,7 @@ async function main(args) {
1008
1028
  const worktree = await resolveWorktree(options.worktree);
1009
1029
  const body = { name: options.name };
1010
1030
  if (argv) body.argv = argv;
1011
- const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
1031
+ const result = await request(`/api/worktrees/${worktree.id}/terminals`, terminalResponseSchema, {
1012
1032
  method: "POST",
1013
1033
  body: JSON.stringify(body)
1014
1034
  });
@@ -1028,7 +1048,7 @@ async function main(args) {
1028
1048
  const { lines: rawLines } = terminalCaptureCommand.opts();
1029
1049
  const lines = rawLines === void 0 ? 200 : parseCaptureLines(rawLines);
1030
1050
  const terminalId = resolveTerminalId(identifier);
1031
- const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`);
1051
+ const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`, terminalCaptureResponseSchema);
1032
1052
  if (jsonOutput) print(capture);
1033
1053
  else {
1034
1054
  writeStdout(capture.content);
@@ -1048,7 +1068,7 @@ async function main(args) {
1048
1068
  print(result, () => `${result.terminal.name} (${result.terminal.id}) reached ${result.condition} at ${result.observedAt}`);
1049
1069
  });
1050
1070
  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" });
1071
+ await request(`/api/terminals/${terminalId}`, okResponseSchema, { method: "DELETE" });
1052
1072
  print({
1053
1073
  ok: true,
1054
1074
  terminalId
@@ -1099,11 +1119,14 @@ async function runCliApplication(options) {
1099
1119
  } catch (error) {
1100
1120
  const cliError = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
1101
1121
  if (jsonOutput) {
1102
- const body = { error: {
1122
+ const body = { error: cliError.details === void 0 ? {
1103
1123
  code: cliError.code,
1104
1124
  message: cliError.message
1125
+ } : {
1126
+ code: cliError.code,
1127
+ message: cliError.message,
1128
+ details: cliError.details
1105
1129
  } };
1106
- if (cliError.details !== void 0) body.error.details = cliError.details;
1107
1130
  writeStderr(`${JSON.stringify(body)}\n`);
1108
1131
  } else writeStderr(`${cliError.message}\n`);
1109
1132
  requestedExitCode = cliError.exitCode;
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import "../../../dist-Crk_Xr82.js";
3
- import { t as integrateShellLaunch } from "../../../shell-integration-Be_c91lw.js";
2
+ import "../../../dist-BsLn2Gbc.js";
3
+ import { t as integrateShellLaunch } from "../../../shell-integration-CPmrVa3B.js";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
- import { z } from "zod";
7
6
  import { spawn } from "node:child_process";
7
+ import { z } from "zod";
8
8
  import { fileURLToPath } from "node:url";
9
9
  //#region src/server/core/launcher.ts
10
10
  const FORWARDED_SIGNALS = [
@@ -19,7 +19,6 @@ const launchSpecSchema = z.object({
19
19
  cwd: z.string(),
20
20
  env: z.record(z.string(), z.string()),
21
21
  shellIntegrationDir: z.string().optional(),
22
- tmuxExecutable: z.string().optional(),
23
22
  setupTasks: z.array(z.object({
24
23
  label: z.string(),
25
24
  argv: z.array(z.string()),
@@ -114,7 +113,6 @@ async function runLaunchSpec(spec, dependencies = {}) {
114
113
  const stdout = dependencies.stdout ?? process.stdout;
115
114
  const stderr = dependencies.stderr ?? process.stderr;
116
115
  const signalSource = dependencies.signalSource ?? process;
117
- const tmuxPane = dependencies.tmuxPane === void 0 ? process.env.TMUX_PANE : dependencies.tmuxPane ?? void 0;
118
116
  if (spec.setupError) {
119
117
  stderr.write(`[Treeport setup] ${safeDiagnostic(spec.setupError) || "setup preparation failed"}\n`);
120
118
  return 1;
@@ -160,25 +158,9 @@ async function runLaunchSpec(spec, dependencies = {}) {
160
158
  ...process.env,
161
159
  ...spec.env
162
160
  };
163
- const command = integrateShellLaunch(spec.argv, commandEnvironment, spec.shellIntegrationDir, spec.tmuxExecutable);
161
+ const command = integrateShellLaunch(spec.argv, commandEnvironment, spec.shellIntegrationDir, Boolean(spec.shellIntegrationDir));
164
162
  const initialTitle = spec.initialTitle ? safeDiagnostic(spec.initialTitle) : "";
165
- if (initialTitle && spec.tmuxExecutable && tmuxPane) {
166
- if ((await runChild([
167
- spec.tmuxExecutable,
168
- "set-option",
169
- "-p",
170
- "-t",
171
- tmuxPane,
172
- "--",
173
- "@treeport-command",
174
- initialTitle
175
- ], {
176
- cwd: spec.cwd,
177
- env: process.env,
178
- spawnProcess,
179
- signalSource
180
- })).forwardedSignal) return 1;
181
- }
163
+ if (initialTitle && spec.shellIntegrationDir) stdout.write(`\u001b]777;command;${initialTitle}\u001b\\`);
182
164
  const result = await runChild(command.argv, {
183
165
  cwd: spec.cwd,
184
166
  env: command.env,
@@ -188,31 +170,8 @@ async function runLaunchSpec(spec, dependencies = {}) {
188
170
  if (result.spawnError) stderr.write(`Treeport launcher: ${safeDiagnostic(result.spawnError.message) || "spawn error"}\n`);
189
171
  if (result.forwardedSignal && (!spec.fallbackArgv || result.forwardedSignal !== "SIGINT")) return 1;
190
172
  if (spec.fallbackArgv) {
191
- if (spec.tmuxExecutable && tmuxPane && spec.fallbackArgv[0]) {
192
- if ((await runChild([
193
- spec.tmuxExecutable,
194
- "set-option",
195
- "-p",
196
- "-u",
197
- "-t",
198
- tmuxPane,
199
- "@treeport-command",
200
- ";",
201
- "set-option",
202
- "-p",
203
- "-t",
204
- tmuxPane,
205
- "--",
206
- "@treeport-fallback-shell",
207
- Buffer.from(JSON.stringify(spec.fallbackArgv[0]), "utf8").toString("base64url")
208
- ], {
209
- cwd: spec.cwd,
210
- env: process.env,
211
- spawnProcess,
212
- signalSource
213
- })).forwardedSignal) return 1;
214
- }
215
- const fallback = integrateShellLaunch(spec.fallbackArgv, commandEnvironment, spec.shellIntegrationDir, spec.tmuxExecutable);
173
+ if (spec.shellIntegrationDir) stdout.write("\x1B]777;command;\x1B\\");
174
+ const fallback = integrateShellLaunch(spec.fallbackArgv, commandEnvironment, spec.shellIntegrationDir, Boolean(spec.shellIntegrationDir));
216
175
  const fallbackResult = await runChild(fallback.argv, {
217
176
  cwd: spec.cwd,
218
177
  env: fallback.env,
@@ -246,6 +205,9 @@ async function main() {
246
205
  process.exit(await runLaunchSpec(spec));
247
206
  }
248
207
  const invokedPath = process.argv[1];
249
- if (invokedPath && path.resolve(invokedPath) === path.resolve(fileURLToPath(import.meta.url))) main();
208
+ if (invokedPath && path.resolve(invokedPath) === path.resolve(fileURLToPath(import.meta.url))) main().catch((error) => {
209
+ process.stderr.write(`Treeport launcher: ${error instanceof Error ? error.message : String(error)}\n`);
210
+ process.exit(127);
211
+ });
250
212
  //#endregion
251
213
  export { runLaunchSpec };