@treeport/treeport 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Treeport
2
2
 
3
- Treeport is a worktree-first terminal driver for persistent development workspaces.
3
+ Treeport is a tree-first terminal driver for persistent development workspaces.
4
4
 
5
5
  ```sh
6
6
  npm install --global @treeport/treeport
@@ -8,7 +8,7 @@ cd /path/to/repository
8
8
  treeport .
9
9
  ```
10
10
 
11
- Treeport starts its backend if needed, registers the repository and its worktrees, and opens the current worktree in the desktop app or browser. Run `treeport start` to start only the backend. Use `treeport service enable` when a host must start Treeport after reboot.
11
+ Treeport starts its backend if needed, registers the repository and its trees, and opens the current tree in the desktop app or browser. Run `treeport start` to start only the backend. Use `treeport service enable` when a host must start Treeport after reboot.
12
12
 
13
13
  Treeport supports macOS and Linux and requires Node.js 24 or newer, Git, and tmux 3.2 or newer.
14
14
 
@@ -12,6 +12,7 @@ const TERMINAL_SELECTION_START_SEQUENCE = "\x1B[9001~";
12
12
  const TERMINAL_SELECTION_STOP_SEQUENCE = "\x1B[9002~";
13
13
  const TERMINAL_SELECTION_CLEAR_SEQUENCE = "\x1B[9003~";
14
14
  const TERMINAL_SELECTION_RESTORE_SEQUENCE = "\x1B[9004~";
15
+ z.unknown();
15
16
  const terminalId = z.string().min(1).max(128);
16
17
  const clientId = z.string().min(1).max(128);
17
18
  const streamId = z.string().min(1).max(128);
@@ -238,6 +239,7 @@ const eventsSnapshotSchema = z.strictObject({
238
239
  terminalMetadata: z.array(terminalRuntimeMetadataSchema),
239
240
  webPanels: z.array(webPanelSnapshotSchema)
240
241
  });
242
+ z.unknown();
241
243
  function parseEventsSnapshot(value) {
242
244
  const parsed = eventsSnapshotSchema.safeParse(value);
243
245
  return parsed.success ? parsed.data : null;
@@ -304,7 +306,7 @@ const createWorktreeSchema = z.object({
304
306
  if (value.base === "current" && !value.sourceWorktreeId) context.addIssue({
305
307
  code: "custom",
306
308
  path: ["sourceWorktreeId"],
307
- message: "A source worktree is required when starting from current"
309
+ message: "A source tree is required when starting from current"
308
310
  });
309
311
  });
310
312
  const terminalCwdSchema = z.string().min(1).max(4096).refine((value) => value.trim().length > 0 && !value.includes("\0"), { message: "Working directory cannot be blank or contain NUL" });
@@ -370,22 +372,25 @@ z.object({
370
372
  if (value.base === "current" && !value.sourceWorktreeId) context.addIssue({
371
373
  code: "custom",
372
374
  path: ["sourceWorktreeId"],
373
- message: "A source worktree is required when starting from current"
375
+ message: "A source tree is required when starting from current"
374
376
  });
375
377
  });
376
378
  //#endregion
377
379
  //#region src/duration.ts
378
- const DURATION_UNITS = {
379
- ms: 1,
380
- s: 1e3,
381
- m: 6e4,
382
- h: 36e5
383
- };
380
+ const DURATION_UNITS = /* @__PURE__ */ new Map([
381
+ ["ms", 1],
382
+ ["s", 1e3],
383
+ ["m", 6e4],
384
+ ["h", 36e5]
385
+ ]);
384
386
  const MAX_DURATION_MS = 2147483647;
385
387
  function parseDurationMs(value) {
386
388
  const match = /^(\d+)(ms|s|m|h)$/.exec(value);
387
389
  if (!match) throw new Error("Timeout must be a positive duration such as 500ms, 30s, 5m, or 1h");
388
- const timeoutMs = Number(match[1]) * DURATION_UNITS[match[2]];
390
+ const amount = Number(match[1]);
391
+ const multiplier = DURATION_UNITS.get(match[2] ?? "");
392
+ if (multiplier === void 0) throw new Error("Timeout has an unsupported duration unit");
393
+ const timeoutMs = amount * multiplier;
389
394
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_DURATION_MS) throw new Error("Timeout must be between 1ms and 2147483647ms");
390
395
  return timeoutMs;
391
396
  }
@@ -404,4 +409,4 @@ function assertLoopbackHost(host) {
404
409
  throw new Error("Treeport supports only loopback listeners. Run `treeport start --host 127.0.0.1`, then use `treeport remote enable` for private remote access.");
405
410
  }
406
411
  //#endregion
407
- export { parseProductEvent as A, TERMINAL_SELECTION_RESTORE_SEQUENCE as B, repositoryTerminalPresetsFileSchema as C, updateTerminalPresetSchema as D, updateProjectSchema as E, TERMINAL_OUTPUT_HIGH_WATERMARK as F, terminalBellAcknowledgementSchema as G, TERMINAL_SELECTION_STOP_SEQUENCE as H, TERMINAL_OUTPUT_LOW_WATERMARK as I, terminalLegacyTakeControlSchema as J, terminalBinarySchema as K, TERMINAL_OUTPUT_STALL_TIMEOUT_MS as L, TERMINAL_CONTROLLER_GRACE_MS as M, TERMINAL_MAX_CLIENT_MESSAGE_BYTES as N, updateTerminalSchema as O, TERMINAL_MAX_INPUT_BYTES as P, terminalTakeControlSchema as Q, TERMINAL_SCROLL_EXIT_SEQUENCE as R, repositoryTerminalPresetSchema as S, terminalCaptureQuerySchema as T, parseTerminalAuth as U, TERMINAL_SELECTION_START_SEQUENCE as V, parseTerminalProgress as W, terminalResizeSchema as X, terminalOutputAckSchema as Y, terminalSizeSchema as Z, packageReloadSchema as _, WEB_PANEL_INPUT_MAX_BYTES as a, registerProjectSchema as b, createTerminalSchema as c, deleteTerminalPresetSchema as d, deleteWebPanelStorageSchema as f, packageProjectQuerySchema as g, packageInstallSchema as h, TERMINAL_MAX_UPLOAD_BYTES as i, SOCKET_IO_PATH as j, parseEventsSnapshot as k, createWebPanelSchema as l, openWebPanelSchema as m, parseDurationMs as n, browseDirectoryQuerySchema as o, getWebPanelStorageSchema as p, terminalInputSchema as q, TERMINAL_CAPTURE_MAX_LINES as r, createTerminalPresetSchema as s, assertLoopbackHost as t, createWorktreeSchema as u, packageRemoveSchema as v, setWebPanelStorageSchema as w, removeWorktreeSchema as x, packageUpdateSchema as y, TERMINAL_SELECTION_CLEAR_SEQUENCE as z };
412
+ export { terminalTakeControlSchema as $, parseEventsSnapshot as A, TERMINAL_SELECTION_CLEAR_SEQUENCE as B, repositoryTerminalPresetsFileSchema as C, updateTerminalPresetSchema as D, updateProjectSchema as E, TERMINAL_MAX_INPUT_BYTES as F, parseTerminalProgress as G, TERMINAL_SELECTION_START_SEQUENCE as H, TERMINAL_OUTPUT_HIGH_WATERMARK as I, terminalInputSchema as J, terminalBellAcknowledgementSchema as K, TERMINAL_OUTPUT_LOW_WATERMARK as L, SOCKET_IO_PATH as M, TERMINAL_CONTROLLER_GRACE_MS as N, updateTerminalSchema as O, TERMINAL_MAX_CLIENT_MESSAGE_BYTES as P, terminalSizeSchema as Q, TERMINAL_OUTPUT_STALL_TIMEOUT_MS as R, repositoryTerminalPresetSchema as S, terminalCaptureQuerySchema as T, TERMINAL_SELECTION_STOP_SEQUENCE as U, TERMINAL_SELECTION_RESTORE_SEQUENCE as V, parseTerminalAuth as W, terminalOutputAckSchema as X, terminalLegacyTakeControlSchema as Y, terminalResizeSchema as Z, packageReloadSchema as _, WEB_PANEL_INPUT_MAX_BYTES as a, registerProjectSchema as b, createTerminalSchema as c, deleteTerminalPresetSchema as d, deleteWebPanelStorageSchema as f, packageProjectQuerySchema as g, packageInstallSchema as h, TERMINAL_MAX_UPLOAD_BYTES as i, parseProductEvent as j, webPanelInputSchema as k, createWebPanelSchema as l, openWebPanelSchema as m, parseDurationMs as n, browseDirectoryQuerySchema as o, getWebPanelStorageSchema as p, terminalBinarySchema as q, TERMINAL_CAPTURE_MAX_LINES as r, createTerminalPresetSchema as s, assertLoopbackHost as t, createWorktreeSchema as u, packageRemoveSchema as v, setWebPanelStorageSchema as w, removeWorktreeSchema as x, packageUpdateSchema as y, TERMINAL_SCROLL_EXIT_SEQUENCE as z };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { A as parseProductEvent, j as SOCKET_IO_PATH, k as parseEventsSnapshot, n as parseDurationMs, r as TERMINAL_CAPTURE_MAX_LINES, t as assertLoopbackHost } from "../../loopback-Dyv_owrb.js";
2
+ import { A as parseEventsSnapshot, M as SOCKET_IO_PATH, j as parseProductEvent, k as webPanelInputSchema, n as parseDurationMs, r as TERMINAL_CAPTURE_MAX_LINES, t as assertLoopbackHost } from "../../loopback-D7k_J_Wl.js";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { Command, CommanderError } from "commander";
@@ -601,6 +601,8 @@ const serviceRecordSchema = z.strictObject({
601
601
  logPath: z.string().min(1),
602
602
  apiUrl: z.string().min(1),
603
603
  cliEntrypoint: z.string().min(1),
604
+ runtimeExecutable: z.string().min(1).nullable().default(null),
605
+ runtimeEntrypoint: z.string().min(1).nullable().default(null),
604
606
  installationMethod: z.enum(["curl", "npm"]),
605
607
  definitionName: z.string().min(1),
606
608
  definitionPath: z.string().min(1),
@@ -635,7 +637,9 @@ const administratorRequestSchema = z.strictObject({
635
637
  stagedDefinitionPath: z.string().min(1),
636
638
  definitionHash: z.string().length(64),
637
639
  apiUrl: z.string().min(1),
638
- cliEntrypoint: z.string().min(1)
640
+ cliEntrypoint: z.string().min(1),
641
+ runtimeExecutable: z.string().min(1),
642
+ runtimeEntrypoint: z.string().min(1)
639
643
  });
640
644
  function managerForPlatform(platform = process.platform) {
641
645
  return platform === "darwin" ? "launchd" : platform === "linux" ? "systemd" : null;
@@ -664,7 +668,8 @@ async function writeJson(filePath, value) {
664
668
  await fs.rename(temporaryPath, filePath);
665
669
  }
666
670
  function fingerprint(value) {
667
- const source = typeof value === "string" ? value : JSON.stringify(Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))));
671
+ const parsed = z.string().safeParse(value);
672
+ const source = parsed.success ? parsed.data : JSON.stringify(Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))));
668
673
  return crypto.createHash("sha256").update(source).digest("hex");
669
674
  }
670
675
  function xml(value) {
@@ -673,6 +678,9 @@ function xml(value) {
673
678
  function shellQuote(value) {
674
679
  return `'${value.replaceAll("'", `'\\''`)}'`;
675
680
  }
681
+ function createAdministratorCommand(input) {
682
+ return `sudo ${input.installationMethod === "curl" ? shellQuote(input.cliEntrypoint) : `${shellQuote(input.runtimeExecutable)} ${shellQuote(input.runtimeEntrypoint)}`} service apply --request ${shellQuote(input.requestPath)}`;
683
+ }
676
684
  function systemdValue(value) {
677
685
  return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("%", "%%").replaceAll("\n", "\\n");
678
686
  }
@@ -833,6 +841,23 @@ async function ensureEntrypoint(installationMethod) {
833
841
  }
834
842
  return entrypoint;
835
843
  }
844
+ async function currentAdministratorRuntime() {
845
+ const invokedEntrypoint = process.argv[1]?.trim();
846
+ if (!invokedEntrypoint) throw new Error("Treeport could not identify its Node entrypoint.");
847
+ const runtimeEntrypoint = path.resolve(invokedEntrypoint);
848
+ const [runtimeExecutable, actualEntrypoint, packageBinEntrypoint, packageCliEntrypoint] = await Promise.all([
849
+ fs.realpath(process.execPath),
850
+ fs.realpath(runtimeEntrypoint),
851
+ fs.realpath(await resolvePackagePath("bin", "treeport.mjs")),
852
+ fs.realpath(await resolvePackagePath("dist", "node", "cli", "index.js"))
853
+ ]);
854
+ if (actualEntrypoint !== packageBinEntrypoint && actualEntrypoint !== packageCliEntrypoint) throw new Error(`Treeport cannot use an unrecognized package entrypoint for administrator commands: ${runtimeEntrypoint}`);
855
+ await Promise.all([fs.access(runtimeExecutable, constants.X_OK), fs.access(runtimeEntrypoint, constants.R_OK)]);
856
+ return {
857
+ runtimeExecutable,
858
+ runtimeEntrypoint
859
+ };
860
+ }
836
861
  function cacheDirectory(home, env) {
837
862
  const configured = env.TREEPORT_CACHE_DIR?.trim();
838
863
  if (configured) return path.resolve(configured.replace(/^~(?=\/|$)/, home));
@@ -950,9 +975,15 @@ async function managerState(record) {
950
975
  }
951
976
  function administratorCommand(record) {
952
977
  const requestId = record.pendingAdministratorRequestId;
953
- if (!requestId) return null;
978
+ if (!requestId || !record.runtimeExecutable || !record.runtimeEntrypoint) return null;
954
979
  const requestPath = path.join(servicePaths({ TREEPORT_DATA_DIR: record.dataDir }).requestsDirectory, `${requestId}.json`);
955
- return `sudo ${shellQuote(record.cliEntrypoint)} service apply --request ${shellQuote(requestPath)}`;
980
+ return createAdministratorCommand({
981
+ installationMethod: record.installationMethod,
982
+ cliEntrypoint: record.cliEntrypoint,
983
+ runtimeExecutable: record.runtimeExecutable,
984
+ runtimeEntrypoint: record.runtimeEntrypoint,
985
+ requestPath
986
+ });
956
987
  }
957
988
  async function untrackedDefinition() {
958
989
  const manager = managerForPlatform();
@@ -1040,16 +1071,19 @@ async function serviceStatus() {
1040
1071
  };
1041
1072
  }
1042
1073
  const paths = servicePaths({ TREEPORT_DATA_DIR: record.dataDir });
1043
- const [managerStatus, definitionContent, entrypointExists, daemon] = await Promise.all([
1074
+ const [managerStatus, definitionContent, entrypointExists, runtimeExecutableExists, runtimeEntrypointExists, currentRuntime, daemon] = await Promise.all([
1044
1075
  managerState(record),
1045
1076
  fs.readFile(record.definitionPath, "utf8").catch(() => ""),
1046
1077
  fs.access(record.cliEntrypoint, constants.X_OK).then(() => true).catch(() => false),
1078
+ record.runtimeExecutable ? fs.access(record.runtimeExecutable, constants.X_OK).then(() => true).catch(() => false) : Promise.resolve(false),
1079
+ record.runtimeEntrypoint ? fs.access(record.runtimeEntrypoint, constants.R_OK).then(() => true).catch(() => false) : Promise.resolve(false),
1080
+ currentAdministratorRuntime().catch(() => null),
1047
1081
  daemonStatus()
1048
1082
  ]);
1049
1083
  const definitionPresent = definitionContent !== "";
1050
1084
  const definitionMatches = definitionPresent && fingerprint(definitionContent) === record.definitionHash;
1051
1085
  const invokedEntrypoint = currentEntrypoint();
1052
- const entrypointMatches = entrypointExists && (invokedEntrypoint === null || path.resolve(invokedEntrypoint) === path.resolve(record.cliEntrypoint));
1086
+ const entrypointMatches = Boolean(entrypointExists && runtimeExecutableExists && runtimeEntrypointExists && currentRuntime && record.runtimeExecutable === currentRuntime.runtimeExecutable && record.runtimeEntrypoint === currentRuntime.runtimeEntrypoint && (invokedEntrypoint === null || path.resolve(invokedEntrypoint) === path.resolve(record.cliEntrypoint)));
1053
1087
  const environmentMatches = fingerprint(createServiceEnvironment({
1054
1088
  user: {
1055
1089
  uid: record.uid,
@@ -1082,7 +1116,7 @@ async function serviceStatus() {
1082
1116
  recoveryCommands.push("treeport service enable");
1083
1117
  }
1084
1118
  if (!entrypointMatches) {
1085
- issues.push(`The service CLI entrypoint is unavailable or moved: ${record.cliEntrypoint}`);
1119
+ issues.push(`The service CLI entrypoint or Node runtime is unavailable or moved: ${record.cliEntrypoint}`);
1086
1120
  recoveryCommands.push("treeport service enable");
1087
1121
  }
1088
1122
  if (!environmentMatches) {
@@ -1134,7 +1168,7 @@ async function prepareRecord() {
1134
1168
  if (listener.protocol !== "http:") throw new Error("Treeport service mode requires a local HTTP loopback URL.");
1135
1169
  assertLoopbackHost(listener.hostname);
1136
1170
  const installationMethod = process.env.TREEPORT_INSTALLATION_METHOD?.trim() === "curl" ? "curl" : "npm";
1137
- const cliEntrypoint = await ensureEntrypoint(installationMethod);
1171
+ const [cliEntrypoint, administratorRuntime] = await Promise.all([ensureEntrypoint(installationMethod), currentAdministratorRuntime()]);
1138
1172
  const group = await primaryGroup(user.username);
1139
1173
  const definitionName = manager === "launchd" ? `app.treeport.daemon.${user.uid}` : "treeport.service";
1140
1174
  const definitionPath = manager === "launchd" ? `/Library/LaunchDaemons/${definitionName}.plist` : path.join(process.env.XDG_CONFIG_HOME?.trim() || path.join(user.homedir, ".config"), "systemd", "user", definitionName);
@@ -1163,6 +1197,8 @@ async function prepareRecord() {
1163
1197
  logPath: paths.logPath,
1164
1198
  apiUrl,
1165
1199
  cliEntrypoint,
1200
+ runtimeExecutable: administratorRuntime.runtimeExecutable,
1201
+ runtimeEntrypoint: administratorRuntime.runtimeEntrypoint,
1166
1202
  installationMethod,
1167
1203
  definitionName,
1168
1204
  definitionPath,
@@ -1207,6 +1243,14 @@ async function writeServiceFiles(record, definition) {
1207
1243
  await saveRecord(record);
1208
1244
  }
1209
1245
  async function prepareAdministratorRequest(record, operation) {
1246
+ const runtime = record.runtimeExecutable && record.runtimeEntrypoint ? {
1247
+ runtimeExecutable: record.runtimeExecutable,
1248
+ runtimeEntrypoint: record.runtimeEntrypoint
1249
+ } : await currentAdministratorRuntime();
1250
+ const requestRecord = {
1251
+ ...record,
1252
+ ...runtime
1253
+ };
1210
1254
  const locations = servicePaths({ TREEPORT_DATA_DIR: record.dataDir });
1211
1255
  const id = crypto.randomUUID();
1212
1256
  const now = /* @__PURE__ */ new Date();
@@ -1228,11 +1272,13 @@ async function prepareAdministratorRequest(record, operation) {
1228
1272
  stagedDefinitionPath: locations.stagedDefinitionPath,
1229
1273
  definitionHash: record.definitionHash,
1230
1274
  apiUrl: record.apiUrl,
1231
- cliEntrypoint: record.cliEntrypoint
1275
+ cliEntrypoint: record.cliEntrypoint,
1276
+ runtimeExecutable: requestRecord.runtimeExecutable,
1277
+ runtimeEntrypoint: requestRecord.runtimeEntrypoint
1232
1278
  };
1233
1279
  await writeJson(path.join(locations.requestsDirectory, `${id}.json`), request);
1234
1280
  const next = {
1235
- ...record,
1281
+ ...requestRecord,
1236
1282
  pendingAdministratorRequestId: id,
1237
1283
  updatedAt: now.toISOString()
1238
1284
  };
@@ -1459,13 +1505,16 @@ async function serviceApply(requestPath) {
1459
1505
  if (!request) throw new Error("The service apply request is invalid.");
1460
1506
  if (metadata.uid !== request.uid) throw new Error("The service apply request owner does not match its target user.");
1461
1507
  if (Date.parse(request.expiresAt) <= Date.now()) throw new Error("The service apply request expired. Run the original Treeport command again.");
1508
+ const currentRuntime = await currentAdministratorRuntime().catch(() => null);
1509
+ const invokedRuntimeEntrypoint = process.argv[1] ? path.resolve(process.argv[1]) : null;
1510
+ if (!currentRuntime || currentRuntime.runtimeExecutable !== request.runtimeExecutable || currentRuntime.runtimeEntrypoint !== request.runtimeEntrypoint || invokedRuntimeEntrypoint !== request.runtimeEntrypoint) throw new Error("The service apply command did not use the approved Treeport Node runtime and package entrypoint.");
1462
1511
  const usedPath = `${requestPath}.used`;
1463
1512
  if (await fs.access(usedPath).then(() => true).catch(() => false)) throw new Error("The service apply request was already used.");
1464
1513
  const account = os.userInfo({ encoding: "utf8" });
1465
1514
  const idResult = await runCommand(await executablePath("id"), ["-u", request.username]);
1466
1515
  if (idResult.code !== 0 || Number(idResult.stdout.trim()) !== request.uid) throw new Error("The service apply target user no longer matches the host account.");
1467
1516
  const record = await readJson(request.serviceRecordPath, serviceRecordSchema);
1468
- if (!record || record.uid !== request.uid || record.username !== request.username || record.manager !== "launchd" || record.definitionName !== request.definitionName || record.definitionPath !== request.definitionPath || record.cliEntrypoint !== request.cliEntrypoint || record.definitionHash !== request.definitionHash || record.pendingAdministratorRequestId !== request.id) throw new Error("The service apply request does not match the current Treeport service record.");
1517
+ if (!record || record.uid !== request.uid || record.username !== request.username || record.manager !== "launchd" || record.definitionName !== request.definitionName || record.definitionPath !== request.definitionPath || record.cliEntrypoint !== request.cliEntrypoint || record.runtimeExecutable !== request.runtimeExecutable || record.runtimeEntrypoint !== request.runtimeEntrypoint || record.definitionHash !== request.definitionHash || record.pendingAdministratorRequestId !== request.id) throw new Error("The service apply request does not match the current Treeport service record.");
1469
1518
  if (account.uid !== 0) throw new Error("Treeport service apply lost root privileges.");
1470
1519
  const launchctl = await executablePath("launchctl");
1471
1520
  const target = `system/${request.definitionName}`;
@@ -1668,14 +1717,13 @@ async function request(pathname, options = {}) {
1668
1717
  else externalSignal?.addEventListener("abort", abort, { once: true });
1669
1718
  const timeout = setTimeout(abort, 9e4);
1670
1719
  try {
1720
+ const headers = new Headers({ accept: "application/json" });
1721
+ if (options.body) headers.set("content-type", "application/json");
1722
+ new Headers(options.headers).forEach((value, key) => headers.set(key, value));
1671
1723
  const response = await fetch(`${apiUrl}${pathname}`, {
1672
1724
  ...options,
1673
1725
  signal: controller.signal,
1674
- headers: {
1675
- accept: "application/json",
1676
- ...options.body ? { "content-type": "application/json" } : {},
1677
- ...options.headers
1678
- }
1726
+ headers
1679
1727
  });
1680
1728
  const body = await response.json().catch(() => ({}));
1681
1729
  if (!response.ok) {
@@ -1700,18 +1748,18 @@ async function createWorktree(projectId, input) {
1700
1748
  await new Promise((resolve) => setTimeout(resolve, 100));
1701
1749
  operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
1702
1750
  }
1703
- if (operation.status === "failed") throw new CliError(operation.error ?? "Worktree creation failed", 5, "WORKTREE_CREATION_FAILED");
1704
- if (operation.kind !== "create") throw new CliError("Worktree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
1705
- const worktreeId = typeof operation.result?.worktreeId === "string" ? operation.result.worktreeId : operation.worktreeId;
1706
- if (!worktreeId) throw new CliError("Completed worktree creation did not identify its worktree", 5, "INVALID_OPERATION_RESULT");
1751
+ if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
1752
+ if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
1753
+ const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
1754
+ if (!worktreeId) throw new CliError("Completed tree creation did not identify its tree", 5, "INVALID_OPERATION_RESULT");
1707
1755
  const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project.worktrees.find((item) => item.id === worktreeId);
1708
- if (!worktree) throw new CliError(`Created worktree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
1709
- const terminalId = typeof operation.result?.terminalId === "string" ? operation.result.terminalId : null;
1756
+ if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
1757
+ const terminalId = operation.result?.terminalId ?? null;
1710
1758
  return {
1711
1759
  worktree,
1712
1760
  terminal: worktree.terminals.find((item) => item.id === terminalId) ?? null,
1713
- terminalError: typeof operation.result?.terminalError === "string" ? operation.result.terminalError : null,
1714
- setupError: typeof operation.result?.setupError === "string" ? operation.result.setupError : null
1761
+ terminalError: operation.result?.terminalError ?? null,
1762
+ setupError: operation.result?.setupError ?? null
1715
1763
  };
1716
1764
  }
1717
1765
  function commandArgv(args) {
@@ -1764,7 +1812,7 @@ async function resolveWorktree(identifier) {
1764
1812
  }
1765
1813
  const candidate = await canonical(identifier);
1766
1814
  const match = all.filter((worktree) => pathContains(candidate, worktree.path)).sort((a, b) => b.path.length - a.path.length)[0];
1767
- if (!match) throw new CliError(`No registered worktree matches ${identifier}`, 5);
1815
+ if (!match) throw new CliError(`No registered tree matches ${identifier}`, 5);
1768
1816
  return match;
1769
1817
  }
1770
1818
  function parseWebPanelInput(value) {
@@ -1776,8 +1824,9 @@ function parseWebPanelInput(value) {
1776
1824
  } catch (error) {
1777
1825
  throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
1778
1826
  }
1779
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError("--input must contain a JSON object", 2);
1780
- return parsed;
1827
+ const validated = webPanelInputSchema.safeParse(parsed);
1828
+ if (!validated.success) throw new CliError("--input must contain a JSON object", 2);
1829
+ return validated.data;
1781
1830
  }
1782
1831
  async function webPanelDefinition(worktreeId, identifier) {
1783
1832
  const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`)).definitions;
@@ -1786,11 +1835,11 @@ async function webPanelDefinition(worktreeId, identifier) {
1786
1835
  const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
1787
1836
  if (matches.length === 1) return matches[0];
1788
1837
  if (matches.length > 1) throw new CliError(`Web panel name ${identifier} is ambiguous: ${matches.map((match) => match.id).join(", ")}`, 5, "WEB_PANEL_DEFINITION_AMBIGUOUS", { definitionIds: matches.map((match) => match.id) });
1789
- throw new CliError(`Web panel ${identifier} is not available in this worktree`, 5, "WEB_PANEL_DEFINITION_NOT_FOUND");
1838
+ throw new CliError(`Web panel ${identifier} is not available in this tree`, 5, "WEB_PANEL_DEFINITION_NOT_FOUND");
1790
1839
  }
1791
1840
  async function webPanelLaunchCwd(worktree) {
1792
1841
  const [cwd, worktreeRoot] = await Promise.all([canonical(workingDirectory), canonical(worktree.path)]);
1793
- if (!pathContains(cwd, worktreeRoot)) throw new CliError(`The current directory is outside worktree ${worktree.name}`, 5, "INVALID_WEB_PANEL_LAUNCH_CWD", {
1842
+ if (!pathContains(cwd, worktreeRoot)) throw new CliError(`The current directory is outside tree ${worktree.name}`, 5, "INVALID_WEB_PANEL_LAUNCH_CWD", {
1794
1843
  cwd,
1795
1844
  worktreeId: worktree.id,
1796
1845
  worktreePath: worktree.path
@@ -1876,10 +1925,10 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
1876
1925
  resolve(result);
1877
1926
  }
1878
1927
  };
1879
- const fail = (error) => {
1928
+ const fail = (cause) => {
1880
1929
  if (!settled) {
1881
1930
  settled = true;
1882
- reject(error);
1931
+ reject(cause);
1883
1932
  }
1884
1933
  };
1885
1934
  const enqueue = (task) => {
@@ -1959,7 +2008,7 @@ const agentGuidance = `AI agents:
1959
2008
  async function main(args) {
1960
2009
  const argv = args[0] === "spawn" || args[0] === "terminal" && args[1] === "create" ? commandArgv(args) : void 0;
1961
2010
  let parserError = "";
1962
- const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, worktrees, and terminals.").argument("[folder]", "folder inside a Git repository to open").option("--json", "emit machine-readable JSON").addHelpText("beforeAll", agentGuidance).configureOutput({
2011
+ const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, trees, and terminals.").argument("[folder]", "folder inside a Git repository to open").option("--json", "emit machine-readable JSON").addHelpText("beforeAll", agentGuidance).configureOutput({
1963
2012
  writeOut: writeStdout,
1964
2013
  writeErr: (value) => {
1965
2014
  parserError += value;
@@ -1972,7 +2021,7 @@ async function main(args) {
1972
2021
  }
1973
2022
  const absoluteFolder = path.resolve(workingDirectory, folder);
1974
2023
  if (!(await fs.stat(absoluteFolder).catch((error) => {
1975
- if ((typeof error === "object" && error !== null && "code" in error ? error.code : void 0) === "ENOENT") throw new CliError(`Folder does not exist: ${absoluteFolder}`, 5, "FOLDER_NOT_FOUND", { path: absoluteFolder });
2024
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new CliError(`Folder does not exist: ${absoluteFolder}`, 5, "FOLDER_NOT_FOUND", { path: absoluteFolder });
1976
2025
  throw new CliError(`Cannot access folder ${absoluteFolder}: ${error instanceof Error ? error.message : String(error)}`, 5, "FOLDER_UNREADABLE", { path: absoluteFolder });
1977
2026
  })).isDirectory()) throw new CliError(`Path is not a folder: ${absoluteFolder}`, 5, "FOLDER_NOT_DIRECTORY", { path: absoluteFolder });
1978
2027
  const canonicalFolder = await fs.realpath(absoluteFolder).catch((error) => {
@@ -2024,11 +2073,11 @@ async function main(args) {
2024
2073
  return;
2025
2074
  }
2026
2075
  const port = options.port === void 0 ? void 0 : Number(options.port);
2027
- const result = await daemonUp({
2028
- ...options.host === void 0 ? {} : { host: options.host },
2029
- ...port === void 0 ? {} : { port },
2030
- ...options.foreground === void 0 ? {} : { foreground: options.foreground }
2031
- });
2076
+ const daemonOptions = {};
2077
+ if (options.host !== void 0) daemonOptions.host = options.host;
2078
+ if (port !== void 0) daemonOptions.port = port;
2079
+ if (options.foreground !== void 0) daemonOptions.foreground = options.foreground;
2080
+ const result = await daemonUp(daemonOptions);
2032
2081
  if (options.foreground) return;
2033
2082
  print(result, () => `Treeport is running\n${result.apiUrl}`);
2034
2083
  });
@@ -2090,10 +2139,10 @@ async function main(args) {
2090
2139
  const port = options.port === void 0 ? void 0 : Number(options.port);
2091
2140
  if (port !== void 0 && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new CliError("--port must be an integer between 1 and 65535", 2);
2092
2141
  const serviceDaemon = lifecycle === "service" ? await ensureServiceDaemon() : void 0;
2093
- const result = await enableTailscaleRemote({
2094
- ...port === void 0 ? {} : { port },
2095
- ...serviceDaemon === void 0 ? {} : { daemon: serviceDaemon }
2096
- });
2142
+ const remoteOptions = {};
2143
+ if (port !== void 0) remoteOptions.port = port;
2144
+ if (serviceDaemon !== void 0) remoteOptions.daemon = serviceDaemon;
2145
+ const result = await enableTailscaleRemote(remoteOptions);
2097
2146
  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.`);
2098
2147
  });
2099
2148
  remoteCommand.command("status").description("Show Tailscale remote access status").option("--json", "emit machine-readable JSON").action(async () => {
@@ -2124,7 +2173,7 @@ async function main(args) {
2124
2173
  print(result, () => {
2125
2174
  if (!status.state) return supervision ? formatServiceStatus(supervision) : "Treeport is stopped";
2126
2175
  if (!status.running || !status.verified) return `Treeport is unhealthy (PID ${status.state.pid})\nLogs: ${path.join(status.state.dataDir, "logs", "daemon.log")}`;
2127
- return `Treeport is running\n${status.state.apiUrl}\nLifecycle: ${status.health?.daemonLifecycle}\nVersion: ${status.health?.version}\nPID: ${status.state.pid}\nProjects: ${result.projects}\nWorktrees: ${result.worktrees}\nTerminals: ${result.terminals}`;
2176
+ 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}`;
2128
2177
  });
2129
2178
  });
2130
2179
  const logsCommand = program.command("logs").description("Show recent daemon logs").option("--lines <count>", "number of lines", "100");
@@ -2174,12 +2223,12 @@ async function main(args) {
2174
2223
  if (missing.length) throw new CliError(`Incomplete Treeport context; missing ${missing.join(", ")}`, 5, "TREEPORT_CONTEXT_INCOMPLETE", { missing });
2175
2224
  const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project;
2176
2225
  const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
2177
- if (!worktree) throw new CliError("Treeport context worktree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
2226
+ if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
2178
2227
  projectId,
2179
2228
  worktreeId
2180
2229
  });
2181
2230
  const terminal = worktree.terminals.find((candidate) => candidate.id === terminalId);
2182
- if (!terminal) throw new CliError("Treeport context terminal does not belong to the current worktree", 5, "TREEPORT_CONTEXT_INVALID", {
2231
+ if (!terminal) throw new CliError("Treeport context terminal does not belong to the current tree", 5, "TREEPORT_CONTEXT_INVALID", {
2183
2232
  worktreeId,
2184
2233
  terminalId
2185
2234
  });
@@ -2213,29 +2262,27 @@ async function main(args) {
2213
2262
  exitCode: terminal.exitCode
2214
2263
  }
2215
2264
  };
2216
- print(context, () => `Treeport context\n\nProject: ${context.project.name} (${context.project.id})\nWorktree: ${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"}`);
2265
+ 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"}`);
2217
2266
  });
2218
2267
  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");
2219
2268
  installCommand.action(async (source) => {
2220
2269
  const options = installCommand.opts();
2270
+ const body = { source: await packageSource(source) };
2271
+ if (options.local) body.projectId = await localPackageProjectId();
2221
2272
  const result = (await request("/api/packages/install", {
2222
2273
  method: "POST",
2223
- body: JSON.stringify({
2224
- source: await packageSource(source),
2225
- ...options.local ? { projectId: await localPackageProjectId() } : {}
2226
- })
2274
+ body: JSON.stringify(body)
2227
2275
  })).result;
2228
2276
  print(result, () => `Installed ${result.source}${result.scope === "project" ? ` for project ${result.projectId}` : " globally"}`);
2229
2277
  });
2230
2278
  const removePackageCommand = program.command("remove").alias("uninstall").description("Remove a configured Treeport package").argument("<source>", "npm: source or local directory").option("-l, --local", "remove from the registered project containing the current directory").option("--json", "emit machine-readable JSON");
2231
2279
  removePackageCommand.action(async (source) => {
2232
2280
  const options = removePackageCommand.opts();
2281
+ const body = { source: await packageSource(source) };
2282
+ if (options.local) body.projectId = await localPackageProjectId();
2233
2283
  const result = (await request("/api/packages/remove", {
2234
2284
  method: "POST",
2235
- body: JSON.stringify({
2236
- source: await packageSource(source),
2237
- ...options.local ? { projectId: await localPackageProjectId() } : {}
2238
- })
2285
+ body: JSON.stringify(body)
2239
2286
  })).result;
2240
2287
  print(result, () => `Removed ${result.source}`);
2241
2288
  });
@@ -2285,29 +2332,30 @@ async function main(args) {
2285
2332
  const list = await projects();
2286
2333
  print(list, () => list.map((project) => `${project.id}\t${project.name}\t${project.repositoryPath}`).join("\n"));
2287
2334
  });
2288
- const worktreeCommand = program.command("worktree").description("List, create, and remove worktrees");
2335
+ const worktreeCommand = program.command("worktree").description("List, create, and remove trees");
2289
2336
  worktreeCommand.action(() => {
2290
2337
  throw new CliError(worktreeCommand.helpInformation(), 2);
2291
2338
  });
2292
- const worktreeListCommand = worktreeCommand.command("list").description("List discovered worktrees").option("--project <id-or-path>", "limit results to a project").option("--json", "emit machine-readable JSON");
2339
+ const worktreeListCommand = worktreeCommand.command("list").description("List discovered trees").option("--project <id-or-path>", "limit results to a project").option("--json", "emit machine-readable JSON");
2293
2340
  worktreeListCommand.action(async () => {
2294
2341
  const { project: projectIdentifier } = worktreeListCommand.opts();
2295
2342
  const list = projectIdentifier ? (await resolveProject(projectIdentifier)).worktrees : (await projects()).flatMap((project) => project.worktrees);
2296
2343
  print(list, () => list.map((worktree) => `${worktree.id}\t${worktree.name}\t${worktree.branch ?? `detached@${worktree.head.slice(0, 8)}`}\t${worktree.path}`).join("\n"));
2297
2344
  });
2298
- const worktreeCreateCommand = worktreeCommand.command("create").description("Create a linked worktree").requiredOption("--project <id-or-path>", "project to create from").requiredOption("--name <name>", "worktree name").option("--from-current", "base the worktree on the current worktree").option("--json", "emit machine-readable JSON");
2345
+ const worktreeCreateCommand = worktreeCommand.command("create").description("Create a linked tree").requiredOption("--project <id-or-path>", "project to create from").requiredOption("--name <name>", "Tree name").option("--from-current", "base the tree on the current tree").option("--json", "emit machine-readable JSON");
2299
2346
  worktreeCreateCommand.action(async () => {
2300
2347
  const options = worktreeCreateCommand.opts();
2301
2348
  const project = await resolveProject(options.project);
2302
2349
  const sourceWorktreeId = options.fromCurrent ? (await resolveWorktree(".")).id : void 0;
2303
- const result = await createWorktree(project.id, {
2350
+ const request = {
2304
2351
  name: options.name,
2305
- base: options.fromCurrent ? "current" : "default",
2306
- ...sourceWorktreeId ? { sourceWorktreeId } : {}
2307
- });
2308
- print(result, () => `Created ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
2352
+ base: options.fromCurrent ? "current" : "default"
2353
+ };
2354
+ if (sourceWorktreeId) request.sourceWorktreeId = sourceWorktreeId;
2355
+ const result = await createWorktree(project.id, request);
2356
+ print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
2309
2357
  });
2310
- const worktreeRemoveCommand = worktreeCommand.command("remove").description("Remove a linked worktree").argument("<id-or-path-or-dot>", "worktree to remove").option("--force", "confirm destructive removal warnings").option("--json", "emit machine-readable JSON");
2358
+ 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");
2311
2359
  worktreeRemoveCommand.action(async (identifier) => {
2312
2360
  const { force: confirmed } = worktreeRemoveCommand.opts();
2313
2361
  const worktree = await resolveWorktree(identifier);
@@ -2325,18 +2373,18 @@ async function main(args) {
2325
2373
  await new Promise((resolve) => setTimeout(resolve, 100));
2326
2374
  operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
2327
2375
  }
2328
- if (operation.status === "failed") throw new CliError(operation.error ?? "Worktree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
2329
- if (operation.kind !== "remove" || !operation.result) throw new CliError("Worktree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
2376
+ if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
2377
+ if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
2330
2378
  print(operation.result, () => {
2331
2379
  const warning = operation.result?.cleanup.warning;
2332
- return `Removed ${worktree.name} (${worktree.id})${warning ? `\nWarning: ${warning}` : ""}`;
2380
+ return `Removed tree ${worktree.name} (${worktree.id})${warning ? `\nWarning: ${warning}` : ""}`;
2333
2381
  });
2334
2382
  });
2335
2383
  const webPanelCommand = program.command("web-panel").description("Open persistent web panels");
2336
2384
  webPanelCommand.action(() => {
2337
2385
  throw new CliError(webPanelCommand.helpInformation(), 2);
2338
2386
  });
2339
- const webPanelOpenCommand = webPanelCommand.command("open").description("Create or reuse a web panel and request client navigation").argument("<definition>", "definition ID or unique short name").requiredOption("--worktree <id-or-path-or-dot>", "owning worktree").option("--input <json>", "structured panel input as a JSON object").option("--new", "create a separate panel instance").option("--json", "emit machine-readable JSON");
2387
+ const webPanelOpenCommand = webPanelCommand.command("open").description("Create or reuse a web panel and request client navigation").argument("<definition>", "definition ID or unique short name").requiredOption("--worktree <id-or-path-or-dot>", "owning tree").option("--input <json>", "structured panel input as a JSON object").option("--new", "create a separate panel instance").option("--json", "emit machine-readable JSON");
2340
2388
  webPanelOpenCommand.action(async (identifier) => {
2341
2389
  const options = webPanelOpenCommand.opts();
2342
2390
  const worktree = await resolveWorktree(options.worktree);
@@ -2357,25 +2405,25 @@ async function main(args) {
2357
2405
  };
2358
2406
  print(output, () => `${result.reused ? "Reused" : "Opened"} ${result.panel.title} (${result.panel.id})\n${output.url}`);
2359
2407
  });
2360
- const terminalCommand = program.command("terminal").description("Manage persistent worktree terminals");
2408
+ const terminalCommand = program.command("terminal").description("Manage persistent tree terminals");
2361
2409
  terminalCommand.action(() => {
2362
2410
  throw new CliError(terminalCommand.helpInformation(), 2);
2363
2411
  });
2364
- const terminalListCommand = terminalCommand.command("list").description("List terminals").option("--worktree <id-or-path>", "limit results to a worktree").option("--json", "emit machine-readable JSON");
2412
+ const terminalListCommand = terminalCommand.command("list").description("List terminals").option("--worktree <id-or-path>", "limit results to a tree").option("--json", "emit machine-readable JSON");
2365
2413
  terminalListCommand.action(async () => {
2366
2414
  const { worktree: identifier } = terminalListCommand.opts();
2367
2415
  const list = identifier ? (await resolveWorktree(identifier)).terminals : (await projects()).flatMap((project) => project.worktrees.flatMap((worktree) => worktree.terminals));
2368
2416
  print(list, () => list.map((terminal) => `${terminal.id}\t${terminal.name}\t${terminal.status}\t${JSON.stringify(terminal.argv)}`).join("\n"));
2369
2417
  });
2370
- const terminalCreateCommand = terminalCommand.command("create").description("Create a persistent terminal").usage("[options] [-- <command> args...]").requiredOption("--worktree <id-or-path-or-dot>", "owning worktree").requiredOption("--name <name>", "terminal name").option("--json", "emit machine-readable JSON").addHelpText("after", "\nCommand arguments may be passed after --.\n");
2418
+ const terminalCreateCommand = terminalCommand.command("create").description("Create a persistent terminal").usage("[options] [-- <command> args...]").requiredOption("--worktree <id-or-path-or-dot>", "owning tree").requiredOption("--name <name>", "terminal name").option("--json", "emit machine-readable JSON").addHelpText("after", "\nCommand arguments may be passed after --.\n");
2371
2419
  terminalCreateCommand.action(async () => {
2372
2420
  const options = terminalCreateCommand.opts();
2373
- const result = await request(`/api/worktrees/${(await resolveWorktree(options.worktree)).id}/terminals`, {
2421
+ const worktree = await resolveWorktree(options.worktree);
2422
+ const body = { name: options.name };
2423
+ if (argv) body.argv = argv;
2424
+ const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
2374
2425
  method: "POST",
2375
- body: JSON.stringify({
2376
- name: options.name,
2377
- ...argv ? { argv } : {}
2378
- })
2426
+ body: JSON.stringify(body)
2379
2427
  });
2380
2428
  print(result.terminal, () => `Created ${result.terminal.name} (${result.terminal.id})`);
2381
2429
  });
@@ -2419,21 +2467,21 @@ async function main(args) {
2419
2467
  terminalId
2420
2468
  }, () => `Deleted ${terminalId}`);
2421
2469
  });
2422
- const spawnCommand = program.command("spawn").description("Create a worktree and its first terminal").usage("[options] [-- <command> args...]").requiredOption("--project <id-or-path-or-dot>", "project to create from").requiredOption("--worktree-name <name>", "worktree name").requiredOption("--name <terminal-name>", "terminal name").option("--from-current", "base the worktree on the current worktree").option("--json", "emit machine-readable JSON").addHelpText("after", "\nCommand arguments may be passed after --.\n");
2470
+ const spawnCommand = program.command("spawn").description("Create a tree and its first terminal").usage("[options] [-- <command> args...]").requiredOption("--project <id-or-path-or-dot>", "project to create from").requiredOption("--worktree-name <name>", "Tree name").requiredOption("--name <terminal-name>", "terminal name").option("--from-current", "base the tree on the current tree").option("--json", "emit machine-readable JSON").addHelpText("after", "\nCommand arguments may be passed after --.\n");
2423
2471
  spawnCommand.action(async () => {
2424
2472
  const options = spawnCommand.opts();
2425
2473
  const project = await resolveProject(options.project);
2426
2474
  const sourceWorktreeId = options.fromCurrent ? (await resolveWorktree(".")).id : void 0;
2427
- const result = await createWorktree(project.id, {
2475
+ const initialTerminal = { name: options.name };
2476
+ if (argv) initialTerminal.argv = argv;
2477
+ const request = {
2428
2478
  name: options.worktreeName,
2429
2479
  base: options.fromCurrent ? "current" : "default",
2430
- initialTerminal: {
2431
- name: options.name,
2432
- ...argv ? { argv } : {}
2433
- },
2434
- ...sourceWorktreeId ? { sourceWorktreeId } : {}
2435
- });
2436
- print(result, () => `Created worktree ${result.worktree.name} (${result.worktree.id})\nPath: ${result.worktree.path}\n${result.terminal ? `Terminal: ${result.terminal.name} (${result.terminal.id}) — ${result.terminal.status}` : "Terminal: not created"}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}${result.terminalError ? `\nTerminal error: ${result.terminalError}` : ""}`);
2480
+ initialTerminal
2481
+ };
2482
+ if (sourceWorktreeId) request.sourceWorktreeId = sourceWorktreeId;
2483
+ const result = await createWorktree(project.id, request);
2484
+ print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\nPath: ${result.worktree.path}\n${result.terminal ? `Terminal: ${result.terminal.name} (${result.terminal.id}) — ${result.terminal.status}` : "Terminal: not created"}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}${result.terminalError ? `\nTerminal error: ${result.terminalError}` : ""}`);
2437
2485
  });
2438
2486
  try {
2439
2487
  await program.parseAsync(args, { from: "user" });
@@ -2465,9 +2513,9 @@ async function runCliApplication(options) {
2465
2513
  if (jsonOutput) {
2466
2514
  const body = { error: {
2467
2515
  code: cliError.code,
2468
- message: cliError.message,
2469
- ...cliError.details === void 0 ? {} : { details: cliError.details }
2516
+ message: cliError.message
2470
2517
  } };
2518
+ if (cliError.details !== void 0) body.error.details = cliError.details;
2471
2519
  writeStderr(`${JSON.stringify(body)}\n`);
2472
2520
  } else writeStderr(`${cliError.message}\n`);
2473
2521
  requestedExitCode = cliError.exitCode;