@treeport/treeport 0.2.2 → 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.
@@ -1,5 +1,5 @@
1
- import { B as TERMINAL_SELECTION_RESTORE_SEQUENCE, C as repositoryTerminalPresetsFileSchema, D as updateTerminalPresetSchema, E as updateProjectSchema, G as terminalBellAcknowledgementSchema, H as TERMINAL_SELECTION_STOP_SEQUENCE, J as terminalLegacyTakeControlSchema, K as terminalBinarySchema, L as TERMINAL_OUTPUT_STALL_TIMEOUT_MS, M as TERMINAL_CONTROLLER_GRACE_MS, N as TERMINAL_MAX_CLIENT_MESSAGE_BYTES, O as updateTerminalSchema, Q as terminalTakeControlSchema, R as TERMINAL_SCROLL_EXIT_SEQUENCE, S as repositoryTerminalPresetSchema, T as terminalCaptureQuerySchema, U as parseTerminalAuth, V as TERMINAL_SELECTION_START_SEQUENCE, W as parseTerminalProgress, X as terminalResizeSchema, Y as terminalOutputAckSchema, Z as terminalSizeSchema, _ as packageReloadSchema, b as registerProjectSchema, c as createTerminalSchema, d as deleteTerminalPresetSchema, f as deleteWebPanelStorageSchema, g as packageProjectQuerySchema, h as packageInstallSchema, i as TERMINAL_MAX_UPLOAD_BYTES, j as SOCKET_IO_PATH, l as createWebPanelSchema, m as openWebPanelSchema, n as parseDurationMs, o as browseDirectoryQuerySchema, p as getWebPanelStorageSchema, q as terminalInputSchema, s as createTerminalPresetSchema, t as assertLoopbackHost, u as createWorktreeSchema, v as packageRemoveSchema, w as setWebPanelStorageSchema, x as removeWorktreeSchema, y as packageUpdateSchema, z as TERMINAL_SELECTION_CLEAR_SEQUENCE } from "../../loopback-4XVZbAD1.js";
2
- import { n as prepareShellIntegration } from "../../shell-integration-7aBNr-p0.js";
1
+ import { $ as terminalTakeControlSchema, B as TERMINAL_SELECTION_CLEAR_SEQUENCE, C as repositoryTerminalPresetsFileSchema, D as updateTerminalPresetSchema, E as updateProjectSchema, G as parseTerminalProgress, H as TERMINAL_SELECTION_START_SEQUENCE, J as terminalInputSchema, K as terminalBellAcknowledgementSchema, M as SOCKET_IO_PATH, N as TERMINAL_CONTROLLER_GRACE_MS, O as updateTerminalSchema, P as TERMINAL_MAX_CLIENT_MESSAGE_BYTES, Q as terminalSizeSchema, R as TERMINAL_OUTPUT_STALL_TIMEOUT_MS, S as repositoryTerminalPresetSchema, T as terminalCaptureQuerySchema, U as TERMINAL_SELECTION_STOP_SEQUENCE, V as TERMINAL_SELECTION_RESTORE_SEQUENCE, W as parseTerminalAuth, X as terminalOutputAckSchema, Y as terminalLegacyTakeControlSchema, Z as terminalResizeSchema, _ as packageReloadSchema, b as registerProjectSchema, c as createTerminalSchema, d as deleteTerminalPresetSchema, f as deleteWebPanelStorageSchema, g as packageProjectQuerySchema, h as packageInstallSchema, i as TERMINAL_MAX_UPLOAD_BYTES, k as webPanelInputSchema, l as createWebPanelSchema, m as openWebPanelSchema, n as parseDurationMs, o as browseDirectoryQuerySchema, p as getWebPanelStorageSchema, q as terminalBinarySchema, s as createTerminalPresetSchema, t as assertLoopbackHost, u as createWorktreeSchema, v as packageRemoveSchema, w as setWebPanelStorageSchema, x as removeWorktreeSchema, y as packageUpdateSchema, z as TERMINAL_SCROLL_EXIT_SEQUENCE } from "../../loopback-D7k_J_Wl.js";
2
+ import { n as prepareShellIntegration } from "../../shell-integration-Be_c91lw.js";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { z } from "zod";
@@ -110,8 +110,8 @@ var ExternalCommandError = class extends Data.TaggedError("ExternalCommandError"
110
110
  });
111
111
  }
112
112
  };
113
- function errorMessage$1(error) {
114
- return error instanceof Error ? error.message : String(error);
113
+ function errorMessage$1(cause) {
114
+ return cause instanceof Error ? cause.message : String(cause);
115
115
  }
116
116
  /**
117
117
  * Runs a child command as an interruptible Effect. The child is acquired as a
@@ -243,9 +243,10 @@ function runCommandEffect(request) {
243
243
  removeUseListeners();
244
244
  });
245
245
  });
246
- if (request.timeoutMs === void 0 || request.timeoutMs <= 0) return awaitResult;
247
- return Effect.raceFirst(awaitResult, Effect.sleep(Duration.millis(request.timeoutMs)).pipe(Effect.flatMap(() => Effect.sync(() => {
248
- resource.terminalError ??= new TimeoutCommandError(request, request.timeoutMs);
246
+ const timeoutMs = request.timeoutMs;
247
+ if (timeoutMs === void 0 || timeoutMs <= 0) return awaitResult;
248
+ return Effect.raceFirst(awaitResult, Effect.sleep(Duration.millis(timeoutMs)).pipe(Effect.flatMap(() => Effect.sync(() => {
249
+ resource.terminalError ??= new TimeoutCommandError(request, timeoutMs);
249
250
  return resource.terminalError;
250
251
  })), Effect.flatMap(Effect.fail)));
251
252
  }, (resource) => Deferred.isDone(resource.exit).pipe(Effect.flatMap((alreadyExited) => {
@@ -311,7 +312,7 @@ function defaultCacheDir(env, platform = process.platform) {
311
312
  }
312
313
  function defaultRuntimeDir(env) {
313
314
  if (env.XDG_RUNTIME_DIR) return path.join(expandHome(env.XDG_RUNTIME_DIR), "treeport");
314
- return path.join(os.tmpdir(), `treeport-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
315
+ return path.join(os.tmpdir(), `treeport-${process.getuid?.() ?? "user"}`);
315
316
  }
316
317
  function loadConfig(env = process.env) {
317
318
  const host = env.TREEPORT_HOST?.trim() || env.HOST?.trim() || "127.0.0.1";
@@ -322,8 +323,8 @@ function loadConfig(env = process.env) {
322
323
  const dataDir = path.resolve(expandHome(env.TREEPORT_DATA_DIR?.trim() || defaultDataDir(env)));
323
324
  const runtimeDir = path.resolve(expandHome(env.TREEPORT_RUNTIME_DIR?.trim() || defaultRuntimeDir(env)));
324
325
  const daemonLifecycle = env.TREEPORT_DAEMON_LIFECYCLE?.trim() || "treeport";
325
- if (daemonLifecycle !== "treeport" && daemonLifecycle !== "external") throw new Error("TREEPORT_DAEMON_LIFECYCLE must be either treeport or external");
326
- return {
326
+ if (daemonLifecycle !== "treeport" && daemonLifecycle !== "service" && daemonLifecycle !== "external") throw new Error("TREEPORT_DAEMON_LIFECYCLE must be treeport, service, or external");
327
+ const config = {
327
328
  host,
328
329
  port: portValue,
329
330
  dataDir,
@@ -339,9 +340,10 @@ function loadConfig(env = process.env) {
339
340
  appVersion: env.TREEPORT_APP_VERSION?.trim() || "development",
340
341
  instanceId: env.TREEPORT_INSTANCE_ID?.trim() || crypto.randomUUID(),
341
342
  installationMethod: env.TREEPORT_INSTALLATION_METHOD?.trim() || "development",
342
- webDevelopment: env.TREEPORT_WEB_DEVELOPMENT?.trim() === "1",
343
- ...env.TREEPORT_WEB_DIST?.trim() ? { webDist: env.TREEPORT_WEB_DIST.trim() } : {}
343
+ webDevelopment: env.TREEPORT_WEB_DEVELOPMENT?.trim() === "1"
344
344
  };
345
+ if (env.TREEPORT_WEB_DIST?.trim()) config.webDist = env.TREEPORT_WEB_DIST.trim();
346
+ return config;
345
347
  }
346
348
  //#endregion
347
349
  //#region src/server/core/database-schema.ts
@@ -463,7 +465,7 @@ async function readOptionalJsonc(filePath) {
463
465
  try {
464
466
  source = await fs.readFile(filePath, "utf8");
465
467
  } catch (error) {
466
- if (error.code === "ENOENT") return { found: false };
468
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return { found: false };
467
469
  throw error;
468
470
  }
469
471
  const errors = [];
@@ -509,12 +511,12 @@ async function assertNoSymlinkComponents(parent, candidate) {
509
511
  }
510
512
  function normalizeWorktreeName(input) {
511
513
  const name = input.normalize("NFKD").replace(/\p{Mark}+/gu, "").toLowerCase().replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-+|-+$/gu, "");
512
- if (!name) throw new Error("Worktree name is required");
513
- if (name.length > 120) throw new Error("Worktree name must be 120 characters or fewer");
514
+ if (!name) throw new Error("Tree name is required");
515
+ if (name.length > 120) throw new Error("Tree name must be 120 characters or fewer");
514
516
  return name;
515
517
  }
516
518
  function inferWorktreeName(mainWorktreePath, worktreePath, kind) {
517
- if (kind === "main") return "main worktree";
519
+ if (kind === "main") return "main tree";
518
520
  const checkoutName = path.basename(worktreePath);
519
521
  return checkoutName === path.basename(mainWorktreePath) ? path.basename(path.dirname(worktreePath)) : checkoutName;
520
522
  }
@@ -564,58 +566,64 @@ async function prepareZedWorktreeWrapper(mainWorktreePath, wrapperPath) {
564
566
  };
565
567
  }
566
568
  const ZED_TASKS_CONFIG_PATH = path.join(".zed", "tasks.json");
569
+ z.unknown();
570
+ const zedTaskRecordSchema = z.looseObject({
571
+ command: z.unknown().optional(),
572
+ args: z.unknown().optional(),
573
+ env: z.unknown().optional(),
574
+ cwd: z.unknown().optional(),
575
+ label: z.unknown().optional(),
576
+ hooks: z.unknown().optional()
577
+ });
567
578
  function taskArray(value) {
568
- if (Array.isArray(value)) return value;
569
- if (value && typeof value === "object") {
570
- const tasks = Reflect.get(value, "tasks");
571
- if (Array.isArray(tasks)) return tasks;
572
- }
573
- return null;
579
+ const direct = z.array(z.unknown()).safeParse(value);
580
+ if (direct.success) return direct.data;
581
+ const wrapped = z.object({ tasks: z.array(z.unknown()) }).safeParse(value);
582
+ return wrapped.success ? wrapped.data.tasks : null;
574
583
  }
575
584
  function parseTask(entry, index, options) {
576
585
  const prefix = `Zed task ${index + 1}`;
577
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`${prefix} must be an object`);
578
- const command = Reflect.get(entry, "command");
579
- const argsInput = Reflect.get(entry, "args");
580
- const environmentInput = Reflect.get(entry, "env");
581
- const cwd = Reflect.get(entry, "cwd");
582
- const label = Reflect.get(entry, "label");
583
- if (typeof label !== "string" || !label.trim()) {
584
- if (options.requireLabel) throw new Error(`${prefix} is missing a label`);
585
- }
586
- if (typeof command !== "string" || !command.trim()) throw new Error(`${prefix} is missing a command`);
586
+ const parsedEntry = zedTaskRecordSchema.safeParse(entry);
587
+ if (!parsedEntry.success) throw new Error(`${prefix} must be an object`);
588
+ const { args: argsInput, command, cwd, env: environmentInput, label } = parsedEntry.data;
589
+ const parsedLabel = z.string().safeParse(label);
590
+ if ((!parsedLabel.success || !parsedLabel.data.trim()) && options.requireLabel) throw new Error(`${prefix} is missing a label`);
591
+ const parsedCommand = z.string().safeParse(command);
592
+ if (!parsedCommand.success || !parsedCommand.data.trim()) throw new Error(`${prefix} is missing a command`);
587
593
  if (argsInput !== void 0 && !Array.isArray(argsInput)) throw new Error(`${prefix} has invalid args`);
588
- const args = (argsInput ?? []).map((argument) => {
589
- if (typeof argument !== "string") throw new Error(`${prefix} has a non-string argument`);
590
- return argument;
591
- });
594
+ const parsedArgs = z.array(z.string()).safeParse(argsInput ?? []);
595
+ if (!parsedArgs.success) throw new Error(`${prefix} has a non-string argument`);
592
596
  const env = {};
593
597
  if (environmentInput !== void 0) {
594
- if (!environmentInput || typeof environmentInput !== "object" || Array.isArray(environmentInput)) throw new Error(`${prefix} has invalid env`);
595
- if (options.validateLaunchFields && Object.keys(environmentInput).length > 128) throw new Error(`${prefix} has more than 128 environment variables`);
596
- for (const [key, environmentValue] of Object.entries(environmentInput)) {
598
+ const parsedEnvironment = z.record(z.string(), z.unknown()).safeParse(environmentInput);
599
+ if (!parsedEnvironment.success) throw new Error(`${prefix} has invalid env`);
600
+ if (options.validateLaunchFields && Object.keys(parsedEnvironment.data).length > 128) throw new Error(`${prefix} has more than 128 environment variables`);
601
+ for (const [key, environmentValue] of Object.entries(parsedEnvironment.data)) {
597
602
  if (options.validateLaunchFields && (!key || key.length > 256 || key.includes("=") || key.includes("\0"))) throw new Error(`${prefix} has an invalid env key`);
598
- if (typeof environmentValue !== "string") throw new Error(`${prefix} has a non-string env value`);
599
- if (options.validateLaunchFields && (environmentValue.length > 4096 || environmentValue.includes("\0"))) throw new Error(`${prefix} has an invalid env value`);
600
- env[key] = environmentValue;
601
- }
602
- }
603
- if (cwd !== void 0 && typeof cwd !== "string") throw new Error(`${prefix} has invalid cwd`);
604
- if (options.validateLaunchFields && typeof cwd === "string" && (!cwd.trim() || cwd.length > 4096 || cwd.includes("\0"))) throw new Error(`${prefix} has invalid cwd`);
605
- return {
606
- label: typeof label === "string" && label.trim() ? label : `Task ${index + 1}`,
607
- command,
608
- args,
609
- ...typeof cwd === "string" ? { cwd } : {},
603
+ const parsedValue = z.string().safeParse(environmentValue);
604
+ if (!parsedValue.success) throw new Error(`${prefix} has a non-string env value`);
605
+ if (options.validateLaunchFields && (parsedValue.data.length > 4096 || parsedValue.data.includes("\0"))) throw new Error(`${prefix} has an invalid env value`);
606
+ env[key] = parsedValue.data;
607
+ }
608
+ }
609
+ const parsedCwd = z.string().safeParse(cwd);
610
+ if (cwd !== void 0 && !parsedCwd.success) throw new Error(`${prefix} has invalid cwd`);
611
+ if (options.validateLaunchFields && parsedCwd.success && (!parsedCwd.data.trim() || parsedCwd.data.length > 4096 || parsedCwd.data.includes("\0"))) throw new Error(`${prefix} has invalid cwd`);
612
+ const task = {
613
+ label: parsedLabel.success && parsedLabel.data.trim() ? parsedLabel.data : `Task ${index + 1}`,
614
+ command: parsedCommand.data,
615
+ args: parsedArgs.data,
610
616
  env
611
617
  };
618
+ if (parsedCwd.success) task.cwd = parsedCwd.data;
619
+ return task;
612
620
  }
613
621
  async function loadCreateWorktreeTasks(mainWorktreePath) {
614
622
  const tasksFile = await readOptionalJsonc(path.join(mainWorktreePath, ZED_TASKS_CONFIG_PATH));
615
623
  return (taskArray(tasksFile.found ? tasksFile.value : null) ?? []).flatMap((entry, index) => {
616
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
617
- const hooks = Reflect.get(entry, "hooks");
618
- if (!Array.isArray(hooks) || !hooks.includes("create_worktree")) return [];
624
+ const parsedEntry = zedTaskRecordSchema.safeParse(entry);
625
+ const parsedHooks = z.array(z.string()).safeParse(parsedEntry.success ? parsedEntry.data.hooks : void 0);
626
+ if (!parsedHooks.success || !parsedHooks.data.includes("create_worktree")) return [];
619
627
  return [parseTask(entry, index, {
620
628
  requireLabel: false,
621
629
  validateLaunchFields: false
@@ -1106,6 +1114,14 @@ var ProductEventBus = class {
1106
1114
  };
1107
1115
  //#endregion
1108
1116
  //#region src/server/core/gh.ts
1117
+ const ghPrSchema = z.object({
1118
+ number: z.number().optional(),
1119
+ state: z.string().optional(),
1120
+ url: z.string().optional(),
1121
+ baseRefName: z.string().optional(),
1122
+ headRefName: z.string().optional(),
1123
+ mergedAt: z.string().nullable().optional()
1124
+ }).strict();
1109
1125
  function mapPrState(pr) {
1110
1126
  if (!pr) return "no_pr";
1111
1127
  if (pr.mergedAt || pr.state?.toUpperCase() === "MERGED") return "merged";
@@ -1156,7 +1172,7 @@ var GhAdapter = class {
1156
1172
  timeoutMs: 3e4
1157
1173
  });
1158
1174
  if (result.exitCode !== 0) return unknownPr();
1159
- const pr = JSON.parse(result.stdout)[0] ?? null;
1175
+ const pr = z.array(ghPrSchema).parse(JSON.parse(result.stdout))[0] ?? null;
1160
1176
  return {
1161
1177
  state: mapPrState(pr),
1162
1178
  number: pr?.number ?? null,
@@ -1997,16 +2013,35 @@ async function checkRuntimePrerequisites(config) {
1997
2013
  }
1998
2014
  //#endregion
1999
2015
  //#region src/server/core/package-system.ts
2016
+ z.unknown();
2017
+ const manifestPatternsSchema = z.array(z.string());
2018
+ const webPanelManifestEntrySchema = z.union([z.string(), z.strictObject({
2019
+ source: z.string(),
2020
+ permissions: z.array(z.enum(["same-origin"])).optional()
2021
+ })]);
2000
2022
  const EMPTY_SETTINGS = {
2001
2023
  raw: {},
2002
2024
  packages: []
2003
2025
  };
2004
2026
  const PACKAGE_OPERATION_TIMEOUT_MS = 5 * 6e4;
2005
2027
  function sourceString(source) {
2006
- return typeof source === "string" ? source : source.source;
2028
+ const parsed = z.string().safeParse(source);
2029
+ if (parsed.success) return parsed.data;
2030
+ return z.object({ source: z.string() }).parse(source).source;
2007
2031
  }
2008
2032
  function packageFilter(source) {
2009
- return typeof source === "string" ? void 0 : source;
2033
+ if (z.string().safeParse(source).success) return;
2034
+ const data = z.object({
2035
+ source: z.string(),
2036
+ autoload: z.boolean().optional(),
2037
+ webPanels: z.array(z.string()).optional(),
2038
+ terminalPresets: z.array(z.string()).optional()
2039
+ }).parse(source);
2040
+ const result = { source: data.source };
2041
+ if (data.autoload !== void 0) result.autoload = data.autoload;
2042
+ if (data.webPanels !== void 0) result.webPanels = data.webPanels;
2043
+ if (data.terminalPresets !== void 0) result.terminalPresets = data.terminalPresets;
2044
+ return result;
2010
2045
  }
2011
2046
  function toPosix(value) {
2012
2047
  return value.split(path.sep).join("/");
@@ -2024,15 +2059,16 @@ function isWithin$1(candidate, root) {
2024
2059
  return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
2025
2060
  }
2026
2061
  function diagnostic(scope, message, options = {}) {
2027
- return {
2062
+ const result = {
2028
2063
  severity: options.severity ?? "error",
2029
2064
  scope,
2030
- message,
2031
- ...options.source === void 0 ? {} : { source: options.source },
2032
- ...options.projectId === void 0 ? {} : { projectId: options.projectId },
2033
- ...options.resourceType === void 0 ? {} : { resourceType: options.resourceType },
2034
- ...options.path === void 0 ? {} : { path: options.path }
2065
+ message
2035
2066
  };
2067
+ if (options.source !== void 0) result.source = options.source;
2068
+ if (options.projectId !== void 0) result.projectId = options.projectId;
2069
+ if (options.resourceType !== void 0) result.resourceType = options.resourceType;
2070
+ if (options.path !== void 0) result.path = options.path;
2071
+ return result;
2036
2072
  }
2037
2073
  function normalizePattern(value) {
2038
2074
  return toPosix(value.trim().replace(/^\.\//u, "").replace(/\/$/u, ""));
@@ -2139,85 +2175,103 @@ var PackageSystem = class {
2139
2175
  async readSettingsFile(settingsPath) {
2140
2176
  let readError;
2141
2177
  const content = await fs.readFile(settingsPath, "utf8").catch((error) => {
2142
- if (error.code === "ENOENT") return null;
2178
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
2143
2179
  readError = error instanceof Error ? error : new Error(String(error));
2144
2180
  return null;
2145
2181
  });
2146
- return {
2182
+ const result = {
2147
2183
  fingerprint: readError ? `error:${readError.message}` : content === null ? "missing" : crypto.createHash("sha256").update(content).digest("hex"),
2148
- content,
2149
- ...readError ? { error: readError } : {}
2184
+ content
2150
2185
  };
2186
+ if (readError) result.error = readError;
2187
+ return result;
2151
2188
  }
2152
2189
  parseSettings(content, settingsPath, scope, projectId) {
2153
2190
  if (content === null || content.trim() === "") return { settings: {
2154
2191
  raw: {},
2155
2192
  packages: []
2156
2193
  } };
2157
- let raw;
2194
+ let input;
2158
2195
  try {
2159
- raw = JSON.parse(content);
2196
+ input = JSON.parse(content);
2160
2197
  } catch (error) {
2161
2198
  return { diagnostic: diagnostic(scope, `Could not parse ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, {
2162
2199
  projectId,
2163
2200
  path: settingsPath
2164
2201
  }) };
2165
2202
  }
2166
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { diagnostic: diagnostic(scope, `${settingsPath} must contain a JSON object`, {
2203
+ const parsedRaw = z.looseObject({
2204
+ npmCommand: z.unknown().optional(),
2205
+ packages: z.unknown().optional()
2206
+ }).safeParse(input);
2207
+ if (!parsedRaw.success) return { diagnostic: diagnostic(scope, `${settingsPath} must contain a JSON object`, {
2167
2208
  projectId,
2168
2209
  path: settingsPath
2169
2210
  }) };
2170
- const npmCommand = Reflect.get(raw, "npmCommand");
2171
- const packageEntries = Reflect.get(raw, "packages");
2172
- if (npmCommand !== void 0 && (!Array.isArray(npmCommand) || npmCommand.length === 0 || npmCommand.some((value) => typeof value !== "string" || value.length === 0))) return { diagnostic: diagnostic(scope, `${settingsPath} npmCommand must be a non-empty argv string array`, {
2211
+ const parsedNpmCommand = z.array(z.string().min(1)).min(1).safeParse(parsedRaw.data.npmCommand);
2212
+ if (parsedRaw.data.npmCommand !== void 0 && !parsedNpmCommand.success) return { diagnostic: diagnostic(scope, `${settingsPath} npmCommand must be a non-empty argv string array`, {
2173
2213
  projectId,
2174
2214
  path: settingsPath
2175
2215
  }) };
2176
- if (packageEntries !== void 0 && !Array.isArray(packageEntries)) return { diagnostic: diagnostic(scope, `${settingsPath} packages must be an array`, {
2216
+ const parsedPackageEntries = z.array(z.unknown()).safeParse(parsedRaw.data.packages ?? []);
2217
+ if (!parsedPackageEntries.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages must be an array`, {
2177
2218
  projectId,
2178
2219
  path: settingsPath
2179
2220
  }) };
2221
+ const packageObjectSchema = z.looseObject({
2222
+ source: z.unknown().optional(),
2223
+ autoload: z.unknown().optional(),
2224
+ webPanels: z.unknown().optional(),
2225
+ terminalPresets: z.unknown().optional()
2226
+ });
2180
2227
  const packages = [];
2181
- for (const [index, entry] of (packageEntries ?? []).entries()) {
2182
- if (typeof entry === "string" && entry.trim()) {
2183
- packages.push(entry);
2228
+ for (const [index, entry] of parsedPackageEntries.data.entries()) {
2229
+ const parsedSourceString = z.string().safeParse(entry);
2230
+ if (parsedSourceString.success && parsedSourceString.data.trim()) {
2231
+ packages.push(parsedSourceString.data);
2184
2232
  continue;
2185
2233
  }
2186
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}] must be a source string or package object`, {
2234
+ const parsedEntry = packageObjectSchema.safeParse(entry);
2235
+ if (!parsedEntry.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}] must be a source string or package object`, {
2187
2236
  projectId,
2188
2237
  path: settingsPath
2189
2238
  }) };
2190
- const source = Reflect.get(entry, "source");
2191
- const autoload = Reflect.get(entry, "autoload");
2192
- const webPanels = Reflect.get(entry, "webPanels");
2193
- const terminalPresets = Reflect.get(entry, "terminalPresets");
2194
- if (typeof source !== "string" || !source.trim()) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].source must be a non-empty string`, {
2239
+ const parsedSource = z.string().safeParse(parsedEntry.data.source);
2240
+ if (!parsedSource.success || !parsedSource.data.trim()) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].source must be a non-empty string`, {
2195
2241
  projectId,
2196
2242
  path: settingsPath
2197
2243
  }) };
2198
- if (autoload !== void 0 && typeof autoload !== "boolean") return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].autoload must be a boolean`, {
2244
+ const parsedAutoload = z.boolean().safeParse(parsedEntry.data.autoload);
2245
+ if (parsedEntry.data.autoload !== void 0 && !parsedAutoload.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].autoload must be a boolean`, {
2199
2246
  projectId,
2200
2247
  path: settingsPath
2201
2248
  }) };
2202
- for (const [key, value] of [["webPanels", webPanels], ["terminalPresets", terminalPresets]]) if (value !== void 0 && (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string"))) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].${key} must be a string array`, {
2249
+ const parsedWebPanels = z.array(z.string()).safeParse(parsedEntry.data.webPanels);
2250
+ const parsedTerminalPresets = z.array(z.string()).safeParse(parsedEntry.data.terminalPresets);
2251
+ for (const [key, original, parsed] of [[
2252
+ "webPanels",
2253
+ parsedEntry.data.webPanels,
2254
+ parsedWebPanels
2255
+ ], [
2256
+ "terminalPresets",
2257
+ parsedEntry.data.terminalPresets,
2258
+ parsedTerminalPresets
2259
+ ]]) if (original !== void 0 && !parsed.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].${key} must be a string array`, {
2203
2260
  projectId,
2204
2261
  path: settingsPath
2205
2262
  }) };
2206
- const parsedWebPanels = Array.isArray(webPanels) ? webPanels.filter((pattern) => typeof pattern === "string") : void 0;
2207
- const parsedTerminalPresets = Array.isArray(terminalPresets) ? terminalPresets.filter((pattern) => typeof pattern === "string") : void 0;
2208
- packages.push({
2209
- source,
2210
- ...autoload === void 0 ? {} : { autoload },
2211
- ...parsedWebPanels ? { webPanels: parsedWebPanels } : {},
2212
- ...parsedTerminalPresets ? { terminalPresets: parsedTerminalPresets } : {}
2213
- });
2214
- }
2215
- const parsedNpmCommand = Array.isArray(npmCommand) ? npmCommand.filter((argument) => typeof argument === "string") : void 0;
2216
- return { settings: {
2217
- raw,
2218
- packages,
2219
- ...parsedNpmCommand ? { npmCommand: parsedNpmCommand } : {}
2220
- } };
2263
+ const configured = { source: parsedSource.data };
2264
+ if (parsedAutoload.success) configured.autoload = parsedAutoload.data;
2265
+ if (parsedWebPanels.success) configured.webPanels = parsedWebPanels.data;
2266
+ if (parsedTerminalPresets.success) configured.terminalPresets = parsedTerminalPresets.data;
2267
+ packages.push(configured);
2268
+ }
2269
+ const settings = {
2270
+ raw: parsedRaw.data,
2271
+ packages
2272
+ };
2273
+ if (parsedNpmCommand.success) settings.npmCommand = parsedNpmCommand.data;
2274
+ return { settings };
2221
2275
  }
2222
2276
  async parseSource(source, settingsPath) {
2223
2277
  const trimmed = source.trim();
@@ -2233,16 +2287,17 @@ var PackageSystem = class {
2233
2287
  const version = split > 0 ? spec.slice(split + 1) : void 0;
2234
2288
  if (!name || name.includes("..") || name.includes("\\") || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/iu.test(name) || version !== void 0 && !version) throw new DomainError("INVALID_PACKAGE_SOURCE", `Invalid npm package source: ${source}`, 400);
2235
2289
  const identity = `npm:${name}`;
2236
- return {
2290
+ const parsed = {
2237
2291
  type: "npm",
2238
2292
  source: trimmed,
2239
2293
  spec,
2240
2294
  name,
2241
- ...version === void 0 ? {} : { version },
2242
2295
  exact: isExactNpmVersion(version),
2243
2296
  identity,
2244
2297
  packageId: identity
2245
2298
  };
2299
+ if (version !== void 0) parsed.version = version;
2300
+ return parsed;
2246
2301
  }
2247
2302
  if (!path.isAbsolute(trimmed) && trimmed !== "." && trimmed !== ".." && !trimmed.startsWith("./") && !trimmed.startsWith("../") && trimmed !== "~" && !trimmed.startsWith("~/")) throw new DomainError("INVALID_PACKAGE_SOURCE", "Package sources must use npm: syntax or an explicit local path", 400);
2248
2303
  const expanded = trimmed === "~" || trimmed.startsWith("~/") ? path.join(os.homedir(), trimmed.slice(2)) : trimmed;
@@ -2346,40 +2401,47 @@ var PackageSystem = class {
2346
2401
  const installedPath = this.npmPackagePath(source, scope, projectId);
2347
2402
  let shouldInstall = forceInstall;
2348
2403
  if (!await fs.stat(installedPath).then((value) => value.isDirectory()).catch(() => false)) shouldInstall = true;
2349
- else if (source.exact) shouldInstall = await fs.readFile(path.join(installedPath, "package.json"), "utf8").then((content) => JSON.parse(content).version).catch(() => void 0) !== source.version;
2404
+ else if (source.exact) shouldInstall = await fs.readFile(path.join(installedPath, "package.json"), "utf8").then((content) => {
2405
+ const parsed = z.object({ version: z.string().optional() }).safeParse(JSON.parse(content));
2406
+ return parsed.success ? parsed.data.version : void 0;
2407
+ }).catch(() => void 0) !== source.version;
2350
2408
  if (shouldInstall) await this.runNpm("install", source, scope, projectId, settings);
2351
2409
  if (!await fs.stat(installedPath).then((value) => value.isDirectory()).catch(() => false)) throw new DomainError("PACKAGE_INSTALL_FAILED", `Package manager completed without installing ${source.name}`, 500);
2352
2410
  return installedPath;
2353
2411
  }
2354
2412
  validateManifestPatterns(patterns, field, packageJsonPath) {
2355
- if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== "string")) throw new Error(`${packageJsonPath} treeport.${field} must be a string array`);
2356
- for (const pattern of patterns) {
2413
+ const parsed = manifestPatternsSchema.safeParse(patterns);
2414
+ if (!parsed.success) throw new Error(`${packageJsonPath} treeport.${field} must be a string array`);
2415
+ for (const pattern of parsed.data) {
2357
2416
  const normalized = normalizePattern(pattern.startsWith("!") ? pattern.slice(1) : pattern);
2358
2417
  if (!normalized || path.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../") || pattern.startsWith("+") || pattern.startsWith("-")) throw new Error(`${packageJsonPath} treeport.${field} contains an invalid package-relative pattern: ${pattern}`);
2359
2418
  }
2360
- return [...patterns];
2419
+ return [...parsed.data];
2361
2420
  }
2362
2421
  validateWebPanelManifest(entries, packageJsonPath) {
2363
- if (!Array.isArray(entries)) throw new Error(`${packageJsonPath} treeport.webPanels must be an array`);
2364
- return entries.map((entry) => {
2365
- if (typeof entry === "string") {
2366
- this.validateManifestPatterns([entry], "webPanels", packageJsonPath);
2422
+ const parsedEntries = z.array(z.unknown()).safeParse(entries);
2423
+ if (!parsedEntries.success) throw new Error(`${packageJsonPath} treeport.webPanels must be an array`);
2424
+ return parsedEntries.data.map((entry) => {
2425
+ const parsed = webPanelManifestEntrySchema.safeParse(entry);
2426
+ if (!parsed.success) throw new Error(`${packageJsonPath} contains an invalid web panel definition`);
2427
+ const parsedSource = z.string().safeParse(parsed.data);
2428
+ if (parsedSource.success) {
2429
+ this.validateManifestPatterns([parsedSource.data], "webPanels", packageJsonPath);
2367
2430
  return {
2368
- source: entry,
2431
+ source: parsedSource.data,
2369
2432
  allowSameOrigin: false
2370
2433
  };
2371
2434
  }
2372
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`${packageJsonPath} treeport.webPanels entries must be package-relative patterns or panel definitions`);
2373
- const keys = Object.keys(entry);
2374
- const source = Reflect.get(entry, "source");
2375
- const permissions = Reflect.get(entry, "permissions") ?? [];
2376
- if (keys.some((key) => !["source", "permissions"].includes(key)) || typeof source !== "string" || source.startsWith("!") || !Array.isArray(permissions) || permissions.some((permission) => typeof permission !== "string")) throw new Error(`${packageJsonPath} contains an invalid web panel definition`);
2435
+ const { source, permissions = [] } = z.strictObject({
2436
+ source: z.string(),
2437
+ permissions: z.array(z.enum(["same-origin"])).optional()
2438
+ }).parse(parsed.data);
2439
+ if (source.startsWith("!")) throw new Error(`${packageJsonPath} contains an invalid web panel definition`);
2377
2440
  this.validateManifestPatterns([source], "webPanels", packageJsonPath);
2378
- const uniquePermissions = new Set(permissions);
2379
- if (uniquePermissions.size !== permissions.length || permissions.some((permission) => permission !== "same-origin")) throw new Error(`${packageJsonPath} contains an invalid web panel permission`);
2441
+ if (new Set(permissions).size !== permissions.length) throw new Error(`${packageJsonPath} contains an invalid web panel permission`);
2380
2442
  return {
2381
2443
  source,
2382
- allowSameOrigin: uniquePermissions.has("same-origin")
2444
+ allowSameOrigin: permissions.includes("same-origin")
2383
2445
  };
2384
2446
  });
2385
2447
  }
@@ -2469,7 +2531,7 @@ var PackageSystem = class {
2469
2531
  const source = sourceString(configured);
2470
2532
  const packageJsonPath = path.join(root, "package.json");
2471
2533
  const packageJsonContent = await fs.readFile(packageJsonPath, "utf8").catch((error) => {
2472
- if (error.code === "ENOENT") return null;
2534
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
2473
2535
  throw error;
2474
2536
  });
2475
2537
  let manifest;
@@ -2480,15 +2542,17 @@ var PackageSystem = class {
2480
2542
  } catch (error) {
2481
2543
  throw new Error(`Could not parse ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
2482
2544
  }
2483
- if (!packageJson || typeof packageJson !== "object" || Array.isArray(packageJson)) throw new Error(`${packageJsonPath} must contain a JSON object`);
2484
- const treeport = Reflect.get(packageJson, "treeport");
2485
- if (treeport !== void 0) {
2486
- if (!treeport || typeof treeport !== "object" || Array.isArray(treeport)) throw new Error(`${packageJsonPath} treeport manifest must be an object`);
2487
- const webPanels = Reflect.get(treeport, "webPanels");
2488
- const terminalPresets = Reflect.get(treeport, "terminalPresets");
2545
+ const parsedPackageJson = z.looseObject({ treeport: z.unknown().optional() }).safeParse(packageJson);
2546
+ if (!parsedPackageJson.success) throw new Error(`${packageJsonPath} must contain a JSON object`);
2547
+ if (parsedPackageJson.data.treeport !== void 0) {
2548
+ const parsedTreeport = z.looseObject({
2549
+ webPanels: z.unknown().optional(),
2550
+ terminalPresets: z.unknown().optional()
2551
+ }).safeParse(parsedPackageJson.data.treeport);
2552
+ if (!parsedTreeport.success) throw new Error(`${packageJsonPath} treeport manifest must be an object`);
2489
2553
  manifest = {
2490
- webPanels: this.validateWebPanelManifest(webPanels ?? [], packageJsonPath),
2491
- terminalPresets: this.validateManifestPatterns(terminalPresets ?? [], "terminalPresets", packageJsonPath)
2554
+ webPanels: this.validateWebPanelManifest(parsedTreeport.data.webPanels ?? [], packageJsonPath),
2555
+ terminalPresets: this.validateManifestPatterns(parsedTreeport.data.terminalPresets ?? [], "terminalPresets", packageJsonPath)
2492
2556
  };
2493
2557
  }
2494
2558
  }
@@ -2516,7 +2580,7 @@ var PackageSystem = class {
2516
2580
  const webPanels = applyNormalFilter(panelCandidates.map((candidate) => {
2517
2581
  const resourceId = encodeURIComponent(path.posix.basename(candidate.relativePath));
2518
2582
  const allowSameOrigin = manifest?.webPanels.some((entry) => entry.allowSameOrigin && this.manifestAllows(candidate.relativePath, [entry.source], "web-panel")) ?? false;
2519
- return {
2583
+ const resolved = {
2520
2584
  definition: {
2521
2585
  id: `package:${parsed.packageId}:web-panel:${resourceId}`,
2522
2586
  title: titleFromPath(candidate.relativePath),
@@ -2527,10 +2591,11 @@ var PackageSystem = class {
2527
2591
  entry: "index.html",
2528
2592
  packageRoot: root,
2529
2593
  development: parsed.type === "local",
2530
- ...packageLockPath ? { packageLockPath } : {},
2531
2594
  relativePath: candidate.relativePath,
2532
2595
  enabled: true
2533
2596
  };
2597
+ if (packageLockPath) resolved.packageLockPath = packageLockPath;
2598
+ return resolved;
2534
2599
  }), filter?.webPanels, autoload);
2535
2600
  const terminalPresets = [];
2536
2601
  for (const candidate of presetCandidates) {
@@ -2931,10 +2996,11 @@ var PackageSystem = class {
2931
2996
  const next = [];
2932
2997
  for (const configured of settings.packages) if ((await this.parseSource(sourceString(configured), settingsPath)).identity !== parsed.identity) next.push(configured);
2933
2998
  else if (!replaced) {
2934
- next.push(typeof configured === "string" ? persisted : {
2935
- ...configured,
2999
+ const filter = packageFilter(configured);
3000
+ next.push(filter ? {
3001
+ ...filter,
2936
3002
  source: persisted
2937
- });
3003
+ } : persisted);
2938
3004
  replaced = true;
2939
3005
  }
2940
3006
  if (!replaced) next.push(persisted);
@@ -3090,7 +3156,7 @@ const CONFIG_PATH = path.join(".treeport", "terminal-presets.json");
3090
3156
  async function loadRepositoryTerminalPresets(projectId, worktreePath) {
3091
3157
  const configPath = path.join(worktreePath, CONFIG_PATH);
3092
3158
  const content = await fs.readFile(configPath, "utf8").catch((error) => {
3093
- if ((typeof error === "object" && error !== null && "code" in error ? error.code : void 0) === "ENOENT") return null;
3159
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
3094
3160
  return error instanceof Error ? error : new Error(String(error));
3095
3161
  });
3096
3162
  if (content === null) return {
@@ -3163,7 +3229,7 @@ async function loadRepositoryTerminalPresets(projectId, worktreePath) {
3163
3229
  const DEFAULT_SETUP_TIMEOUT_MS = 30 * 6e4;
3164
3230
  const MAX_SETUP_OUTPUT = 4e3;
3165
3231
  const TREEPORT_SETUP_PATH = path.join(".treeport", "setup.json");
3166
- const TREEPORT_PATH_VARIABLES = ["TREEPORT_WORKTREE_PATH", "TREEPORT_MAIN_WORKTREE_PATH"];
3232
+ const TREEPORT_PATH_VARIABLE_NAMES = /* @__PURE__ */ new Set(["TREEPORT_WORKTREE_PATH", "TREEPORT_MAIN_WORKTREE_PATH"]);
3167
3233
  const environmentSchema = z.record(z.string(), z.string()).superRefine((environment, context) => {
3168
3234
  for (const [name, value] of Object.entries(environment)) {
3169
3235
  if (!name || name.includes("=") || name.includes("\0")) context.addIssue({
@@ -3171,7 +3237,7 @@ const environmentSchema = z.record(z.string(), z.string()).superRefine((environm
3171
3237
  path: [name],
3172
3238
  message: "Environment names must be non-empty and cannot contain = or NUL"
3173
3239
  });
3174
- if (TREEPORT_PATH_VARIABLES.includes(name)) context.addIssue({
3240
+ if (TREEPORT_PATH_VARIABLE_NAMES.has(name)) context.addIssue({
3175
3241
  code: "custom",
3176
3242
  path: [name],
3177
3243
  message: `${name} is reserved by Treeport`
@@ -3207,7 +3273,8 @@ const setupFileSchema = z.object({
3207
3273
  function formatIssuePath(issuePath) {
3208
3274
  if (!issuePath.length) return "configuration";
3209
3275
  return issuePath.reduce((formatted, component) => {
3210
- if (typeof component === "number") return `${formatted}[${component}]`;
3276
+ const parsedIndex = z.number().safeParse(component);
3277
+ if (parsedIndex.success) return `${formatted}[${parsedIndex.data}]`;
3211
3278
  return formatted ? `${formatted}.${String(component)}` : String(component);
3212
3279
  }, "");
3213
3280
  }
@@ -3239,7 +3306,7 @@ async function resolveWorktreeSetupTasks(input) {
3239
3306
  return parsed.data.commands.map((command, index) => {
3240
3307
  const expandedCwd = expandTreeportPaths(command.cwd ?? worktreePath, environment);
3241
3308
  const cwd = path.isAbsolute(expandedCwd) ? path.resolve(expandedCwd) : path.resolve(worktreePath, expandedCwd);
3242
- if (!isPathWithin$1(cwd, worktreePath)) throw new Error(`Invalid Treeport setup in ${filePath}: commands[${index}].cwd must stay inside the new worktree`);
3309
+ if (!isPathWithin$1(cwd, worktreePath)) throw new Error(`Invalid Treeport setup in ${filePath}: commands[${index}].cwd must stay inside the new tree`);
3243
3310
  const configuredEnvironment = Object.fromEntries(Object.entries(command.env ?? {}).map(([name, value]) => [name, expandTreeportPaths(value, environment)]));
3244
3311
  return {
3245
3312
  label: command.name,
@@ -3368,6 +3435,30 @@ var WebPanelViteRuntime = class {
3368
3435
  this.httpServer = server;
3369
3436
  }
3370
3437
  viteConfig(source, base, options = {}) {
3438
+ const server = {
3439
+ middlewareMode: true,
3440
+ headers: {
3441
+ "access-control-allow-origin": "*",
3442
+ "cache-control": "no-store",
3443
+ "x-content-type-options": "nosniff"
3444
+ },
3445
+ fs: {
3446
+ strict: true,
3447
+ allow: [source.packageRoot, PANEL_SDK_ROOT]
3448
+ }
3449
+ };
3450
+ if (options.server) server.hmr = {
3451
+ server: options.server,
3452
+ path: `${base}@vite-hmr`
3453
+ };
3454
+ const viteBuild = {
3455
+ sourcemap: true,
3456
+ rollupOptions: { input: path.join(source.root, source.entry) }
3457
+ };
3458
+ if (options.outDir) {
3459
+ viteBuild.outDir = options.outDir;
3460
+ viteBuild.emptyOutDir = true;
3461
+ }
3371
3462
  return {
3372
3463
  root: source.root,
3373
3464
  base,
@@ -3383,30 +3474,8 @@ var WebPanelViteRuntime = class {
3383
3474
  alias: { "@treeport/panel-sdk": PANEL_SDK_ENTRY },
3384
3475
  dedupe: ["react", "react-dom"]
3385
3476
  },
3386
- server: {
3387
- middlewareMode: true,
3388
- headers: {
3389
- "access-control-allow-origin": "*",
3390
- "cache-control": "no-store",
3391
- "x-content-type-options": "nosniff"
3392
- },
3393
- fs: {
3394
- strict: true,
3395
- allow: [source.packageRoot, PANEL_SDK_ROOT]
3396
- },
3397
- ...options.server ? { hmr: {
3398
- server: options.server,
3399
- path: `${base}@vite-hmr`
3400
- } } : {}
3401
- },
3402
- build: {
3403
- sourcemap: true,
3404
- rollupOptions: { input: path.join(source.root, source.entry) },
3405
- ...options.outDir ? {
3406
- outDir: options.outDir,
3407
- emptyOutDir: true
3408
- } : {}
3409
- }
3477
+ server,
3478
+ build: viteBuild
3410
3479
  };
3411
3480
  }
3412
3481
  async hashSource(source) {
@@ -3456,7 +3525,9 @@ var WebPanelViteRuntime = class {
3456
3525
  const parent = path.join(this.config.cacheDir, "web-panels", COMPILER_ABI);
3457
3526
  const directory = path.join(parent, hash);
3458
3527
  const metadata = path.join(directory, BUILD_METADATA);
3459
- if (await fs.readFile(metadata, "utf8").then((value) => JSON.parse(value)).then((value) => value.hash === hash).catch(() => false)) return {
3528
+ if (await fs.readFile(metadata, "utf8").then((value) => {
3529
+ return JSON.parse(value);
3530
+ }).then((value) => value.hash === hash).catch(() => false)) return {
3460
3531
  hash,
3461
3532
  directory
3462
3533
  };
@@ -3502,8 +3573,8 @@ var WebPanelViteRuntime = class {
3502
3573
  directory: await pending
3503
3574
  };
3504
3575
  }
3505
- errorPage(source, error) {
3506
- const raw = error instanceof Error ? error.message : String(error);
3576
+ errorPage(source, cause) {
3577
+ const raw = cause instanceof Error ? cause.message : String(cause);
3507
3578
  const diagnostic = raw.replaceAll(source.packageRoot, "<package>").replaceAll(source.root, "<panel>");
3508
3579
  const stage = /resolve|not found|cannot find|failed to load|import/iu.test(raw) ? "Dependency resolution" : "Source transformation";
3509
3580
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Panel build failed</title><style>body{font-family:system-ui,sans-serif;margin:2rem;line-height:1.5}pre{white-space:pre-wrap;background:#f4f4f5;padding:1rem;border-radius:.5rem}</style></head><body><h1>Web panel could not be compiled</h1><p><strong>${escapeHtml(source.definitionId)}</strong>${source.packageSource ? ` from ${escapeHtml(source.packageSource)}` : ""}</p><p>Stage: ${stage}</p><pre>${escapeHtml(diagnostic)}</pre><p>For a local panel package, install its <code>node_modules</code>. Put browser runtime imports in <code>dependencies</code>, not <code>devDependencies</code>.</p></body></html>`;
@@ -3642,11 +3713,13 @@ var KeyedTaskQueue = class {
3642
3713
  }
3643
3714
  const result = new Promise((resolve, reject) => {
3644
3715
  state.pending += 1;
3645
- Effect.runSync(Queue.offer(state.queue, {
3646
- run: task,
3647
- resolve: (value) => resolve(value),
3648
- reject
3649
- }));
3716
+ Effect.runSync(Queue.offer(state.queue, { run: async () => {
3717
+ try {
3718
+ resolve(await task());
3719
+ } catch (error) {
3720
+ reject(error);
3721
+ }
3722
+ } }));
3650
3723
  });
3651
3724
  if (!state.running) {
3652
3725
  state.running = true;
@@ -3662,14 +3735,8 @@ var KeyedTaskQueue = class {
3662
3735
  }
3663
3736
  async run(key, state) {
3664
3737
  while (state.pending > 0) {
3665
- const task = await Effect.runPromise(Queue.take(state.queue));
3666
- try {
3667
- task.resolve(await task.run());
3668
- } catch (error) {
3669
- task.reject(error);
3670
- } finally {
3671
- state.pending -= 1;
3672
- }
3738
+ await (await Effect.runPromise(Queue.take(state.queue))).run();
3739
+ state.pending -= 1;
3673
3740
  }
3674
3741
  state.running = false;
3675
3742
  if (state.pending > 0) {
@@ -3689,9 +3756,9 @@ const encodeMetadata = (value) => Buffer.from(JSON.stringify(value), "utf8").toS
3689
3756
  function isAbsentTmuxServer(stderr) {
3690
3757
  return /no server running|no sessions|no current target/i.test(stderr) || /(?:failed to connect|error connecting to).*(?:no such file or directory|connection refused)/i.test(stderr);
3691
3758
  }
3692
- function decodeMetadata(value) {
3759
+ function decodeMetadata(value, schema) {
3693
3760
  if (!value) return;
3694
- return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
3761
+ return schema.parse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
3695
3762
  }
3696
3763
  const TMUX_SCROLL_EXIT_SEQUENCE = TERMINAL_SCROLL_EXIT_SEQUENCE;
3697
3764
  const TMUX_SELECTION_CLEAR_SEQUENCE = TERMINAL_SELECTION_CLEAR_SEQUENCE;
@@ -3740,8 +3807,10 @@ bind-key -T copy-mode-vi User4 select-pane -t .
3740
3807
  bind-key -T copy-mode MouseDragEnd1Pane send-keys -X stop-selection
3741
3808
  bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X stop-selection
3742
3809
  # Keep these explicit so source-file replaces stale bindings in existing servers.
3743
- bind-key -T copy-mode WheelDownPane { select-pane ; send-keys -X -N 5 scroll-down ; if-shell -F '#{&&:#{==:#{scroll_position},0},#{==:#{selection_present},0}}' { send-keys -X cancel } }
3744
- bind-key -T copy-mode-vi WheelDownPane { select-pane ; send-keys -X -N 5 scroll-down ; if-shell -F '#{&&:#{==:#{scroll_position},0},#{==:#{selection_present},0}}' { send-keys -X cancel } }
3810
+ bind-key -T copy-mode WheelUpPane { select-pane ; send-keys -X -N 1 scroll-up }
3811
+ bind-key -T copy-mode-vi WheelUpPane { select-pane ; send-keys -X -N 1 scroll-up }
3812
+ bind-key -T copy-mode WheelDownPane { select-pane ; send-keys -X -N 1 scroll-down ; if-shell -F '#{&&:#{==:#{scroll_position},0},#{==:#{selection_present},0}}' { send-keys -X cancel } }
3813
+ bind-key -T copy-mode-vi WheelDownPane { select-pane ; send-keys -X -N 1 scroll-down ; if-shell -F '#{&&:#{==:#{scroll_position},0},#{==:#{selection_present},0}}' { send-keys -X cancel } }
3745
3814
  bind-key -T root WheelUpPane if-shell -F '#{||:#{alternate_on},#{pane_in_mode},#{mouse_any_flag}}' { send-keys -M } { copy-mode -H }
3746
3815
  `;
3747
3816
  var TmuxAdapter = class {
@@ -3846,25 +3915,24 @@ var TmuxAdapter = class {
3846
3915
  });
3847
3916
  await this.configureServer(input.socketName);
3848
3917
  }
3918
+ const environment = { ...input.env };
3919
+ if (sshAuthSock) environment.SSH_AUTH_SOCK = sshAuthSock;
3849
3920
  const spec = {
3850
3921
  argv: [...input.argv],
3851
- ...input.fallbackArgv ? { fallbackArgv: [...input.fallbackArgv] } : {},
3852
3922
  cwd: input.cwd,
3853
- env: {
3854
- ...sshAuthSock ? { SSH_AUTH_SOCK: sshAuthSock } : {},
3855
- ...input.env
3856
- },
3857
- ...shellIntegrationReady ? {
3858
- shellIntegrationDir: this.shellIntegrationDir,
3859
- tmuxExecutable: resolveExecutablePath(this.executable, this.hostEnvironment)
3860
- } : {},
3861
- ...input.setupTasks?.length ? { setupTasks: input.setupTasks.map((task) => ({
3862
- ...task,
3863
- argv: [...task.argv],
3864
- env: { ...task.env }
3865
- })) } : {},
3866
- ...input.setupError ? { setupError: input.setupError } : {}
3923
+ env: environment
3867
3924
  };
3925
+ if (input.fallbackArgv) spec.fallbackArgv = [...input.fallbackArgv];
3926
+ if (shellIntegrationReady) {
3927
+ spec.shellIntegrationDir = this.shellIntegrationDir;
3928
+ spec.tmuxExecutable = resolveExecutablePath(this.executable, this.hostEnvironment);
3929
+ }
3930
+ if (input.setupTasks?.length) spec.setupTasks = input.setupTasks.map((task) => ({
3931
+ ...task,
3932
+ argv: [...task.argv],
3933
+ env: { ...task.env }
3934
+ }));
3935
+ if (input.setupError) spec.setupError = input.setupError;
3868
3936
  await fs.writeFile(specPath, JSON.stringify(spec), { mode: 384 });
3869
3937
  wroteSpec = true;
3870
3938
  await runChecked(this.runner, {
@@ -4008,18 +4076,13 @@ var TmuxAdapter = class {
4008
4076
  if (!sessionName || sessions.has(sessionName) || !terminalId || !worktreeId) continue;
4009
4077
  let metadata;
4010
4078
  try {
4011
- const name = decodeMetadata(encodedName ?? "");
4012
- const argv = decodeMetadata(encodedArgv ?? "");
4013
- const createdAt = decodeMetadata(encodedCreatedAt ?? "");
4014
- const updatedAt = decodeMetadata(encodedUpdatedAt ?? "");
4015
- if (name !== void 0 && typeof name !== "string" || argv !== void 0 && (!Array.isArray(argv) || !argv.every((value) => typeof value === "string")) || createdAt !== void 0 && typeof createdAt !== "string" || updatedAt !== void 0 && typeof updatedAt !== "string") continue;
4016
4079
  metadata = {
4017
4080
  terminalId,
4018
4081
  worktreeId,
4019
- name,
4020
- argv,
4021
- createdAt,
4022
- updatedAt
4082
+ name: decodeMetadata(encodedName ?? "", z.string()),
4083
+ argv: decodeMetadata(encodedArgv ?? "", z.array(z.string())),
4084
+ createdAt: decodeMetadata(encodedCreatedAt ?? "", z.string()),
4085
+ updatedAt: decodeMetadata(encodedUpdatedAt ?? "", z.string())
4023
4086
  };
4024
4087
  } catch {
4025
4088
  continue;
@@ -4198,8 +4261,7 @@ var TmuxAdapter = class {
4198
4261
  const encodedShellTitle = result.stdout.slice(0, firstSeparator).trim();
4199
4262
  let shellTitle = null;
4200
4263
  try {
4201
- const decoded = decodeMetadata(encodedShellTitle);
4202
- shellTitle = typeof decoded === "string" ? decoded : null;
4264
+ shellTitle = decodeMetadata(encodedShellTitle, z.string()) ?? null;
4203
4265
  } catch {}
4204
4266
  const currentCommand = result.stdout.slice(firstSeparator + 1, secondSeparator).trim() || null;
4205
4267
  const commandLine = result.stdout.slice(secondSeparator + 1, thirdSeparator).trim() || null;
@@ -4268,7 +4330,7 @@ var TmuxAdapter = class {
4268
4330
  env: this.environment(),
4269
4331
  timeoutMs: 15e3
4270
4332
  });
4271
- if (result.exitCode !== 0 && !isAbsentTmuxServer(result.stderr)) throw new Error(result.stderr.trim() || "Failed to kill worktree tmux server");
4333
+ if (result.exitCode !== 0 && !isAbsentTmuxServer(result.stderr)) throw new Error(result.stderr.trim() || "Failed to kill tree tmux server");
4272
4334
  await Promise.all(terminalIds.map((terminalId) => fs.unlink(path.join(this.specsDir, `${terminalId}.json`)).catch(() => void 0)));
4273
4335
  this.configuredSockets.delete(socketName);
4274
4336
  return terminalIds;
@@ -4282,8 +4344,8 @@ const WEB_PANEL_STORAGE_MAX_ENTRIES = 256;
4282
4344
  const WEB_PANEL_STORAGE_MAX_TOTAL_BYTES = 1024 * 1024;
4283
4345
  const WEB_PANEL_STORAGE_MAX_VALUE_BYTES = 64 * 1024;
4284
4346
  function mapWebPanel(row, allowSameOrigin = false) {
4285
- const input = JSON.parse(row.inputJson);
4286
- if (input !== null && (typeof input !== "object" || Array.isArray(input))) throw new Error(`Web panel ${row.id} has invalid stored launch input`);
4347
+ const parsedInput = webPanelInputSchema.nullable().safeParse(JSON.parse(row.inputJson));
4348
+ if (!parsedInput.success) throw new Error(`Web panel ${row.id} has invalid stored launch input`);
4287
4349
  return {
4288
4350
  id: row.id,
4289
4351
  kind: "web",
@@ -4291,7 +4353,7 @@ function mapWebPanel(row, allowSameOrigin = false) {
4291
4353
  definitionId: row.definitionId,
4292
4354
  title: row.title,
4293
4355
  launch: {
4294
- input,
4356
+ input: parsedInput.data,
4295
4357
  cwd: row.launchCwd
4296
4358
  },
4297
4359
  sandbox: { allowSameOrigin },
@@ -4404,7 +4466,7 @@ var TreeportService = class {
4404
4466
  await tx.run(sql`
4405
4467
  UPDATE operations
4406
4468
  SET status = 'failed',
4407
- error = ${operation.kind === "create" ? "Daemon restarted before worktree creation completed; existing Git state will be discovered without replaying the creation" : "Daemon restarted before the operation completed; external state was preserved for retry"},
4469
+ error = ${operation.kind === "create" ? "Daemon restarted before tree creation completed; existing Git state will be discovered without replaying the creation" : "Daemon restarted before the operation completed; external state was preserved for retry"},
4408
4470
  updated_at = ${timestamp}
4409
4471
  WHERE id = ${operation.id}
4410
4472
  `);
@@ -4643,14 +4705,14 @@ var TreeportService = class {
4643
4705
  const binding = await this.getWorktree(worktreeId);
4644
4706
  await this.requireOpenProject(binding.projectId);
4645
4707
  const worktree = (await this.listProjects()).flatMap((project) => project.worktrees).find((candidate) => candidate.id === worktreeId);
4646
- if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Worktree not found", 404);
4708
+ if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
4647
4709
  return worktree;
4648
4710
  }
4649
4711
  async requireAvailableWorktree(worktreeId, allowPrunable = false) {
4650
4712
  const binding = await this.storedWorktree(worktreeId);
4651
- if (!binding) throw new DomainError("WORKTREE_NOT_FOUND", "Worktree not found", 404);
4713
+ if (!binding) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
4652
4714
  const worktree = (await this.observeAvailableProject(await this.requireOpenProject(binding.projectId))).worktrees.find((candidate) => candidate.id === worktreeId);
4653
- if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Worktree not found", 404);
4715
+ if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
4654
4716
  if (worktree.prunable && !allowPrunable) throw new DomainError("WORKTREE_UNAVAILABLE", "Git reports this worktree as prunable", 409);
4655
4717
  return worktree;
4656
4718
  }
@@ -4851,17 +4913,20 @@ var TreeportService = class {
4851
4913
  async effectiveWebPanelDefinitions(worktreeId) {
4852
4914
  const worktree = await this.getWorktree(worktreeId);
4853
4915
  this.packages.syncProjects([await this.getProject(worktree.projectId)]);
4854
- return [...await this.localWebPanelDefinitions(worktreeId), ...(await this.packages.webPanelDefinitions(worktree.projectId)).map(({ definition, root, entry, packageRoot, development, packageLockPath }) => ({
4855
- ...definition,
4856
- root,
4857
- entry,
4858
- packageRoot,
4859
- development,
4860
- ...packageLockPath ? { packageLockPath } : {},
4861
- definitionId: definition.id,
4862
- allowNetworkRequests: definition.sandbox.allowSameOrigin,
4863
- ...definition.source.type === "package" ? { packageSource: definition.source.source } : {}
4864
- }))];
4916
+ return [...await this.localWebPanelDefinitions(worktreeId), ...(await this.packages.webPanelDefinitions(worktree.projectId)).map(({ definition, root, entry, packageRoot, development, packageLockPath }) => {
4917
+ const resolved = {
4918
+ ...definition,
4919
+ root,
4920
+ entry,
4921
+ packageRoot,
4922
+ development,
4923
+ definitionId: definition.id,
4924
+ allowNetworkRequests: definition.sandbox.allowSameOrigin
4925
+ };
4926
+ if (packageLockPath) resolved.packageLockPath = packageLockPath;
4927
+ if (definition.source.type === "package") resolved.packageSource = definition.source.source;
4928
+ return resolved;
4929
+ })];
4865
4930
  }
4866
4931
  async listWebPanelDefinitions(worktreeId) {
4867
4932
  return (await this.effectiveWebPanelDefinitions(worktreeId)).map(({ root: _root, entry: _entry, packageRoot: _packageRoot, development: _development, packageLockPath: _packageLockPath, definitionId: _definitionId, packageSource: _packageSource, allowNetworkRequests: _allowNetworkRequests, ...definition }) => definition);
@@ -4879,7 +4944,7 @@ var TreeportService = class {
4879
4944
  const [worktreeRoot, requestedCwd] = await Promise.all([fs.realpath(worktree.path), fs.realpath(path.resolve(worktree.path, launch.cwd)).catch(() => null)]);
4880
4945
  if (!requestedCwd || !(await fs.stat(requestedCwd)).isDirectory()) throw new DomainError("INVALID_WEB_PANEL_LAUNCH_CWD", "Web panel launch directory does not exist", 400);
4881
4946
  const relativeCwd = path.relative(worktreeRoot, requestedCwd);
4882
- if (relativeCwd === ".." || relativeCwd.startsWith(`..${path.sep}`) || path.isAbsolute(relativeCwd)) throw new DomainError("INVALID_WEB_PANEL_LAUNCH_CWD", "Web panel launch directory must be inside the worktree", 400);
4947
+ if (relativeCwd === ".." || relativeCwd.startsWith(`..${path.sep}`) || path.isAbsolute(relativeCwd)) throw new DomainError("INVALID_WEB_PANEL_LAUNCH_CWD", "Web panel launch directory must be inside the tree", 400);
4883
4948
  return {
4884
4949
  launch: {
4885
4950
  input: launch.input,
@@ -5077,7 +5142,7 @@ var TreeportService = class {
5077
5142
  }
5078
5143
  async getWorktree(worktreeId) {
5079
5144
  const worktree = await this.storedWorktree(worktreeId);
5080
- if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Worktree not found", 404);
5145
+ if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
5081
5146
  return worktree;
5082
5147
  }
5083
5148
  async getTerminal(terminalId) {
@@ -5126,7 +5191,7 @@ var TreeportService = class {
5126
5191
  }
5127
5192
  const canonical = await fs.realpath(path.resolve(identifier)).catch(() => path.resolve(identifier));
5128
5193
  const match = (await this.storedProjects()).flatMap((project) => project.worktrees).filter((worktree) => isPathWithin(canonical, worktree.path)).sort((a, b) => b.path.length - a.path.length)[0];
5129
- if (!match) throw new DomainError("WORKTREE_NOT_FOUND", `No registered worktree contains ${identifier}`, 404);
5194
+ if (!match) throw new DomainError("WORKTREE_NOT_FOUND", `No registered tree contains ${identifier}`, 404);
5130
5195
  await this.requireOpenProject(match.projectId);
5131
5196
  return match;
5132
5197
  }
@@ -5373,7 +5438,7 @@ var TreeportService = class {
5373
5438
  const project = await this.getProject(projectId);
5374
5439
  if (await this.projectOpenState(projectId) !== true) return;
5375
5440
  if (this.projectLocks.has(projectId) || this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
5376
- if (project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project worktree is already being modified", 409);
5441
+ if (project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project tree is already being modified", 409);
5377
5442
  this.projectLocks.add(projectId);
5378
5443
  const lockedWorktreeIds = project.worktrees.map((worktree) => worktree.id);
5379
5444
  for (const worktreeId of lockedWorktreeIds) this.worktreeLocks.add(worktreeId);
@@ -5608,18 +5673,19 @@ var TreeportService = class {
5608
5673
  if (this.projectLocks.has(projectId) && !this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
5609
5674
  const operationId = id("op");
5610
5675
  const timestamp = now();
5676
+ const request = {
5677
+ name,
5678
+ base
5679
+ };
5680
+ if (initialTerminal) request.initialTerminal = initialTerminal;
5681
+ if (sourceWorktreeId) request.sourceWorktreeId = sourceWorktreeId;
5611
5682
  await this.deps.database.db.run(sql`
5612
5683
  INSERT INTO operations(
5613
5684
  id,kind,project_id,worktree_id,status,request_json,result_json,error,
5614
5685
  created_at,updated_at
5615
5686
  ) VALUES(
5616
5687
  ${operationId},'create',${projectId},NULL,'pending',
5617
- ${serializeOperation({
5618
- name,
5619
- base,
5620
- ...initialTerminal ? { initialTerminal } : {},
5621
- ...sourceWorktreeId ? { sourceWorktreeId } : {}
5622
- })},NULL,NULL,${timestamp},${timestamp}
5688
+ ${serializeOperation(request)},NULL,NULL,${timestamp},${timestamp}
5623
5689
  )
5624
5690
  `);
5625
5691
  const operation = await this.getOperation(operationId);
@@ -5689,7 +5755,7 @@ var TreeportService = class {
5689
5755
  } catch (error) {
5690
5756
  throw new DomainError("INVALID_WORKTREE_NAME", error instanceof Error ? error.message : String(error), 400);
5691
5757
  }
5692
- if (project.worktrees.some((worktree) => worktree.name.localeCompare(name, void 0, { sensitivity: "accent" }) === 0)) throw new DomainError("WORKTREE_EXISTS", `A worktree named ${name} already exists`, 409);
5758
+ if (project.worktrees.some((worktree) => worktree.name.localeCompare(name, void 0, { sensitivity: "accent" }) === 0)) throw new DomainError("WORKTREE_EXISTS", `A tree named ${name} already exists`, 409);
5693
5759
  const destination = await resolveZedWorktreePath(project.mainWorktreePath, name).catch((error) => {
5694
5760
  throw new DomainError("INVALID_WORKTREE_PATH", error instanceof Error ? error.message : String(error), 400);
5695
5761
  });
@@ -5698,9 +5764,9 @@ var TreeportService = class {
5698
5764
  if (await fs.access(worktreePath).then(() => true, () => false)) throw new DomainError("WORKTREE_PATH_EXISTS", `Destination already exists: ${worktreePath}`, 409);
5699
5765
  let commit;
5700
5766
  if (base === "current") {
5701
- if (!sourceWorktreeId) throw new DomainError("INVALID_SOURCE_WORKTREE", "A source worktree is required when starting from current", 400);
5767
+ if (!sourceWorktreeId) throw new DomainError("INVALID_SOURCE_WORKTREE", "A source tree is required when starting from current", 400);
5702
5768
  const source = await this.getWorktree(sourceWorktreeId);
5703
- if (source.projectId !== projectId || source.prunable) throw new DomainError("INVALID_SOURCE_WORKTREE", "The source worktree must be active and belong to the project", 400);
5769
+ if (source.projectId !== projectId || source.prunable) throw new DomainError("INVALID_SOURCE_WORKTREE", "The source tree must be active and belong to the project", 400);
5704
5770
  commit = await this.deps.git.resolveCommit(source.path);
5705
5771
  } else commit = await this.deps.git.resolveDefaultCommit(project.repositoryPath);
5706
5772
  let preparedWrapper;
@@ -5740,10 +5806,10 @@ var TreeportService = class {
5740
5806
  let terminalError = null;
5741
5807
  let setupError = null;
5742
5808
  if (initialTerminal) {
5743
- const initialTerminalCreation = this.executeCreateTerminal(worktree.id, initialTerminal.name, initialTerminal.argv, {
5744
- ...initialTerminal.returnToShell ? { returnToShell: true } : {},
5745
- ...initialTerminal.initialSize ? { initialSize: initialTerminal.initialSize } : {}
5746
- });
5809
+ const launchOptions = {};
5810
+ if (initialTerminal.returnToShell) launchOptions.returnToShell = true;
5811
+ if (initialTerminal.initialSize) launchOptions.initialSize = initialTerminal.initialSize;
5812
+ const initialTerminalCreation = this.executeCreateTerminal(worktree.id, initialTerminal.name, initialTerminal.argv, launchOptions);
5747
5813
  const setupResolution = resolveWorktreeSetupTasks({
5748
5814
  shell: this.deps.config.shell,
5749
5815
  mainWorktreePath: project.mainWorktreePath,
@@ -5753,7 +5819,7 @@ var TreeportService = class {
5753
5819
  error: null
5754
5820
  }), (error) => ({
5755
5821
  tasks: [],
5756
- error: `worktree setup: ${error instanceof Error ? error.message : String(error)}`.slice(0, 4096)
5822
+ error: `Tree setup: ${error instanceof Error ? error.message : String(error)}`.slice(0, 4096)
5757
5823
  }));
5758
5824
  try {
5759
5825
  terminal = await initialTerminalCreation;
@@ -5767,18 +5833,19 @@ var TreeportService = class {
5767
5833
  }
5768
5834
  const setup = await setupResolution;
5769
5835
  setupError = setup.error;
5770
- if (setup.tasks.length > 0 || setupError) if (!terminal) setupError ??= "worktree setup: no persistent terminal could be started";
5836
+ if (setup.tasks.length > 0 || setupError) if (!terminal) setupError ??= "Tree setup: no persistent terminal could be started";
5771
5837
  else try {
5772
- await this.executeCreateTerminal(worktree.id, "Setup", ["true"], {
5838
+ const setupOptions = {
5773
5839
  setup: {
5774
5840
  tasks: setup.tasks,
5775
5841
  error: setupError
5776
5842
  },
5777
- closeOnSuccess: true,
5778
- ...initialTerminal.initialSize ? { initialSize: initialTerminal.initialSize } : {}
5779
- });
5843
+ closeOnSuccess: true
5844
+ };
5845
+ if (initialTerminal.initialSize) setupOptions.initialSize = initialTerminal.initialSize;
5846
+ await this.executeCreateTerminal(worktree.id, "Setup", ["true"], setupOptions);
5780
5847
  } catch (error) {
5781
- const setupTerminalError = `worktree setup terminal${error instanceof DomainError ? ` [${error.code}]` : ""}: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2048);
5848
+ const setupTerminalError = `Tree setup terminal${error instanceof DomainError ? ` [${error.code}]` : ""}: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2048);
5782
5849
  setupError = setupError ? `${setupError.slice(0, 2047)}\n${setupTerminalError}` : setupTerminalError;
5783
5850
  }
5784
5851
  } else {
@@ -5790,7 +5857,7 @@ var TreeportService = class {
5790
5857
  runner: this.deps.runner,
5791
5858
  tasks
5792
5859
  })).catch((error) => [{
5793
- label: "worktree setup",
5860
+ label: "Tree setup",
5794
5861
  error: error instanceof Error ? error.message : String(error)
5795
5862
  }])).find((result) => result.error);
5796
5863
  setupError = setupFailure ? `${setupFailure.label}: ${setupFailure.error}`.slice(0, 4096) : null;
@@ -5837,32 +5904,33 @@ var TreeportService = class {
5837
5904
  const sessionName = generateTmuxSessionName();
5838
5905
  const commandArgv = argv ? [...argv] : [this.deps.config.shell, "-l"];
5839
5906
  const timestamp = now();
5907
+ const session = {
5908
+ socketName: worktree.tmuxSocketName,
5909
+ sessionName,
5910
+ terminalId,
5911
+ worktreeId: worktree.id,
5912
+ name,
5913
+ createdAt: timestamp,
5914
+ cwd: options?.cwd ?? worktree.path,
5915
+ argv: commandArgv,
5916
+ env: {
5917
+ ...options?.env ?? {},
5918
+ TREEPORT_API_URL: this.deps.config.apiUrl,
5919
+ TREEPORT_MANAGED_API_URL: this.deps.config.apiUrl,
5920
+ TREEPORT_DAEMON_RECORD: path.join(this.deps.config.runtimeDir, "daemon.json"),
5921
+ TREEPORT_DAEMON_LIFECYCLE: this.deps.config.daemonLifecycle,
5922
+ TREEPORT_PROJECT_ID: project.id,
5923
+ TREEPORT_WORKTREE_ID: worktree.id,
5924
+ TREEPORT_TERMINAL_ID: terminalId
5925
+ }
5926
+ };
5927
+ if (options?.returnToShell && argv) session.fallbackArgv = [this.deps.config.shell, "-l"];
5928
+ if (options?.closeOnSuccess) session.closeOnSuccess = true;
5929
+ if (options?.initialSize) session.initialSize = options.initialSize;
5930
+ if (options?.setup?.tasks.length) session.setupTasks = options.setup.tasks;
5931
+ if (options?.setup?.error) session.setupError = options.setup.error;
5840
5932
  try {
5841
- await this.deps.tmux.createSession({
5842
- socketName: worktree.tmuxSocketName,
5843
- sessionName,
5844
- terminalId,
5845
- worktreeId: worktree.id,
5846
- name,
5847
- createdAt: timestamp,
5848
- cwd: options?.cwd ?? worktree.path,
5849
- argv: commandArgv,
5850
- ...options?.returnToShell && argv ? { fallbackArgv: [this.deps.config.shell, "-l"] } : {},
5851
- ...options?.closeOnSuccess ? { closeOnSuccess: true } : {},
5852
- ...options?.initialSize ? { initialSize: options.initialSize } : {},
5853
- env: {
5854
- ...options?.env ?? {},
5855
- TREEPORT_API_URL: this.deps.config.apiUrl,
5856
- TREEPORT_MANAGED_API_URL: this.deps.config.apiUrl,
5857
- TREEPORT_DAEMON_RECORD: path.join(this.deps.config.runtimeDir, "daemon.json"),
5858
- TREEPORT_DAEMON_LIFECYCLE: this.deps.config.daemonLifecycle,
5859
- TREEPORT_PROJECT_ID: project.id,
5860
- TREEPORT_WORKTREE_ID: worktree.id,
5861
- TREEPORT_TERMINAL_ID: terminalId
5862
- },
5863
- ...options?.setup?.tasks.length ? { setupTasks: options.setup.tasks } : {},
5864
- ...options?.setup?.error ? { setupError: options.setup.error } : {}
5865
- });
5933
+ await this.deps.tmux.createSession(session);
5866
5934
  } catch (error) {
5867
5935
  throw new DomainError("TERMINAL_CREATE_FAILED", error instanceof Error ? error.message : String(error), 500);
5868
5936
  }
@@ -5898,8 +5966,8 @@ var TreeportService = class {
5898
5966
  await this.requireAvailableWorktree(worktreeId);
5899
5967
  try {
5900
5968
  const worktree = await this.storedWorktree(worktreeId);
5901
- if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Worktree not found", 404);
5902
- if (this.projectLocks.has(worktree.projectId) || this.worktreeLocks.has(worktreeId) || worktree.prunable) throw new DomainError("WORKTREE_BUSY", "Cannot create a terminal while the worktree is being modified", 409);
5969
+ if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
5970
+ if (this.projectLocks.has(worktree.projectId) || this.worktreeLocks.has(worktreeId) || worktree.prunable) throw new DomainError("WORKTREE_BUSY", "Cannot create a terminal while the tree is being modified", 409);
5903
5971
  this.worktreeLocks.add(worktreeId);
5904
5972
  try {
5905
5973
  return await this.createTerminalSession(worktree, name, argv, options);
@@ -5983,7 +6051,7 @@ var TreeportService = class {
5983
6051
  }
5984
6052
  async executeDeleteTerminal(terminalId, worktreeId) {
5985
6053
  const worktree = await this.storedWorktree(worktreeId);
5986
- if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Worktree not found", 404);
6054
+ if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
5987
6055
  await this.requireOpenProject(worktree.projectId);
5988
6056
  if (this.projectLocks.has(worktree.projectId) || this.worktreeLocks.has(worktree.id)) throw new DomainError("WORKTREE_BUSY", "Cannot delete a terminal during a destructive project operation", 409);
5989
6057
  this.worktreeLocks.add(worktree.id);
@@ -5991,7 +6059,7 @@ var TreeportService = class {
5991
6059
  const terminals = await this.listWorktreeTerminals(worktree);
5992
6060
  const terminal = terminals.find((candidate) => candidate.id === terminalId);
5993
6061
  if (!terminal) throw new DomainError("TERMINAL_NOT_FOUND", "Terminal not found", 404);
5994
- if (terminals.length <= 1 || terminals.every((candidate) => candidate.id === terminalId || this.closeOnSuccessTerminalIds.has(candidate.id))) throw new DomainError("LAST_TERMINAL", "Every open worktree must keep at least one terminal", 409);
6062
+ if (terminals.length <= 1 || terminals.every((candidate) => candidate.id === terminalId || this.closeOnSuccessTerminalIds.has(candidate.id))) throw new DomainError("LAST_TERMINAL", "Every open tree must keep at least one terminal", 409);
5995
6063
  await this.deps.tmux.killSession(worktree.tmuxSocketName, terminal.tmuxSessionName, terminal.id, { preserveServer: true });
5996
6064
  } finally {
5997
6065
  this.worktreeLocks.delete(worktree.id);
@@ -6012,8 +6080,8 @@ var TreeportService = class {
6012
6080
  if (!force && age < 6e4) return worktree.pr;
6013
6081
  await this.requireOpenProject(worktree.projectId);
6014
6082
  const pr = await this.deps.gh.pullRequest(worktree.path, worktree.branch);
6015
- if (!await this.storedWorktree(worktreeId)) throw new DomainError("WORKTREE_NOT_FOUND", "Worktree not found", 404);
6016
- if (this.worktreeLocks.has(worktreeId)) throw new DomainError("WORKTREE_UNAVAILABLE", "Cannot refresh a pull request while the worktree is being removed", 409);
6083
+ if (!await this.storedWorktree(worktreeId)) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
6084
+ if (this.worktreeLocks.has(worktreeId)) throw new DomainError("WORKTREE_UNAVAILABLE", "Cannot refresh a pull request while the tree is being removed", 409);
6017
6085
  await this.deps.database.db.run(sql`
6018
6086
  UPDATE worktrees
6019
6087
  SET pr_state=${pr.state},pr_number=${pr.number},pr_url=${pr.url},
@@ -6049,7 +6117,7 @@ var TreeportService = class {
6049
6117
  const reasons = [];
6050
6118
  const warnings = [];
6051
6119
  if (worktree.kind === "main") reasons.push("The main checkout cannot be removed");
6052
- if (live.locked) reasons.push(live.lockReason ? `The worktree is locked: ${live.lockReason}` : "The worktree is locked");
6120
+ if (live.locked) reasons.push(live.lockReason ? `The tree is locked: ${live.lockReason}` : "The tree is locked");
6053
6121
  if (dirty.staged) warnings.push(`${dirty.staged} staged change(s) will be lost`);
6054
6122
  if (dirty.unstaged) warnings.push(`${dirty.unstaged} unstaged change(s) will be lost`);
6055
6123
  if (dirty.untracked) warnings.push(`${dirty.untracked} untracked file(s) will be lost`);
@@ -6098,25 +6166,25 @@ var TreeportService = class {
6098
6166
  AND status IN ('pending','running')
6099
6167
  LIMIT 1
6100
6168
  `);
6101
- if (activeRemoval) throw new DomainError("REMOVE_IN_PROGRESS", "The worktree is already being removed", 409);
6169
+ if (activeRemoval) throw new DomainError("REMOVE_IN_PROGRESS", "The tree is already being removed", 409);
6102
6170
  if (this.terminalMutations.has(worktreeId)) return this.terminalMutations.enqueue(worktreeId, () => {
6103
6171
  if (this.worktreeMutations.has(worktree.projectId)) return this.worktreeMutations.enqueue(worktree.projectId, () => this.acceptRemove(worktreeId, request));
6104
6172
  return this.acceptRemove(worktreeId, request);
6105
6173
  });
6106
6174
  if (this.worktreeMutations.has(worktree.projectId)) return this.worktreeMutations.enqueue(worktree.projectId, () => this.acceptRemove(worktreeId, request));
6107
- if (this.projectLocks.has(worktree.projectId) || this.worktreeLocks.has(worktreeId)) throw new DomainError("REMOVE_IN_PROGRESS", "The worktree or project is already being modified", 409);
6175
+ if (this.projectLocks.has(worktree.projectId) || this.worktreeLocks.has(worktreeId)) throw new DomainError("REMOVE_IN_PROGRESS", "The tree or project is already being modified", 409);
6108
6176
  return this.acceptRemove(worktreeId, request);
6109
6177
  }
6110
6178
  async acceptRemove(worktreeId, request) {
6111
6179
  const worktree = await this.getWorktree(worktreeId);
6112
6180
  await this.requireOpenProject(worktree.projectId);
6113
- if (this.worktreeLocks.has(worktreeId) || this.projectLocks.has(worktree.projectId)) throw new DomainError("REMOVE_IN_PROGRESS", "The worktree or project is already being modified", 409);
6181
+ if (this.worktreeLocks.has(worktreeId) || this.projectLocks.has(worktree.projectId)) throw new DomainError("REMOVE_IN_PROGRESS", "The tree or project is already being modified", 409);
6114
6182
  this.worktreeLocks.add(worktreeId);
6115
6183
  let operationStarted = false;
6116
6184
  try {
6117
6185
  const { preview, prunable } = await this.prepareRemovePreview(worktreeId);
6118
- if (!preview.eligible) throw new DomainError("REMOVE_REFUSED", "The worktree cannot be removed", 409, preview);
6119
- if (request.confirmationToken !== preview.confirmationToken) throw new DomainError("REMOVE_PREVIEW_STALE", "The worktree changed after the removal preview; review it again", 409, preview);
6186
+ if (!preview.eligible) throw new DomainError("REMOVE_REFUSED", "The tree cannot be removed", 409, preview);
6187
+ if (request.confirmationToken !== preview.confirmationToken) throw new DomainError("REMOVE_PREVIEW_STALE", "The tree changed after the removal preview; review it again", 409, preview);
6120
6188
  if (preview.warnings.length > 0 && !request.confirmDestructive) throw new DomainError("REMOVE_CONFIRMATION_REQUIRED", "Confirm the destructive removal after reviewing its warnings", 409, preview);
6121
6189
  const checkout = await this.checkoutStat(preview.path);
6122
6190
  const [checkoutBinding] = await this.deps.database.db.all(sql`
@@ -6129,11 +6197,11 @@ var TreeportService = class {
6129
6197
  const operationId = id("op");
6130
6198
  let checkoutIdentity = null;
6131
6199
  if (prunable) {
6132
- if (!checkoutBinding?.git_worktree_key) throw new DomainError("REMOVE_PREVIEW_STALE", "The prunable worktree changed after the removal preview; review it again", 409, preview);
6200
+ if (!checkoutBinding?.git_worktree_key) throw new DomainError("REMOVE_PREVIEW_STALE", "The prunable tree changed after the removal preview; review it again", 409, preview);
6133
6201
  } else {
6134
6202
  const markerPath = path.join(preview.path, ".git");
6135
6203
  const gitMarker = (await this.checkoutStat(markerPath))?.isFile() ? await fs.readFile(markerPath, "utf8").catch(() => null) : null;
6136
- if (!checkout?.isDirectory() || !checkoutBinding?.git_worktree_key || gitMarker === null || !gitMarkerMatchesKey(preview.path, gitMarker, checkoutBinding.git_worktree_key)) throw new DomainError("REMOVE_PREVIEW_STALE", "The worktree checkout changed after the removal preview; review it again", 409, preview);
6204
+ if (!checkout?.isDirectory() || !checkoutBinding?.git_worktree_key || gitMarker === null || !gitMarkerMatchesKey(preview.path, gitMarker, checkoutBinding.git_worktree_key)) throw new DomainError("REMOVE_PREVIEW_STALE", "The tree checkout changed after the removal preview; review it again", 409, preview);
6137
6205
  checkoutIdentity = {
6138
6206
  path: preview.path,
6139
6207
  device: checkout.dev.toString(),
@@ -6212,12 +6280,12 @@ var TreeportService = class {
6212
6280
  try {
6213
6281
  const liveWorktrees = await this.deps.git.listWorktrees(project.repositoryPath);
6214
6282
  const acceptedKey = request.gitWorktreeKey;
6215
- const liveAccepted = liveWorktrees.find((item) => item.path === preview.path && (request.prunable ? item.prunable : typeof acceptedKey === "string" && item.gitWorktreeKey === acceptedKey));
6283
+ const liveAccepted = liveWorktrees.find((item) => item.path === preview.path && (request.prunable ? item.prunable : acceptedKey !== null && item.gitWorktreeKey === acceptedKey));
6216
6284
  const liveRepositoryIdentity = await this.deps.git.repositoryIdentity(project.repositoryPath);
6217
6285
  if (liveAccepted) {
6218
6286
  if (!request.repositoryIdentity || liveRepositoryIdentity !== request.repositoryIdentity) throw new Error("Removal revalidation failed before destructive effects: the repository identity changed after removal was accepted");
6219
6287
  if (request.prunable) {
6220
- if (!liveAccepted.prunable) throw new Error("Removal revalidation failed before destructive effects: the accepted worktree is no longer prunable");
6288
+ if (!liveAccepted.prunable) throw new Error("Removal revalidation failed before destructive effects: the accepted tree is no longer prunable");
6221
6289
  } else {
6222
6290
  const authorizationError = await this.authorizedCheckoutError(preview.path, request.checkoutIdentity);
6223
6291
  if (authorizationError) throw new Error(`Removal revalidation failed before destructive effects: ${authorizationError}`);
@@ -6350,18 +6418,18 @@ var TreeportService = class {
6350
6418
  async deleteProject(projectId) {
6351
6419
  if (this.projectLocks.has(projectId) || this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
6352
6420
  let project = await this.getProject(projectId);
6353
- if (project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project worktree is already being modified", 409);
6421
+ if (project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project tree is already being modified", 409);
6354
6422
  this.projectLocks.add(projectId);
6355
6423
  const lockedWorktrees = [];
6356
6424
  try {
6357
6425
  project = await this.observeAvailableProject(project, true);
6358
- if (this.worktreeMutations.has(projectId) || project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project worktree is already being modified", 409);
6426
+ if (this.worktreeMutations.has(projectId) || project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project tree is already being modified", 409);
6359
6427
  for (const worktree of project.worktrees) {
6360
6428
  this.worktreeLocks.add(worktree.id);
6361
6429
  lockedWorktrees.push(worktree.id);
6362
6430
  }
6363
6431
  project = await this.getProject(projectId);
6364
- if (project.worktrees.filter((worktree) => worktree.kind === "linked").length) throw new DomainError("PROJECT_HAS_WORKTREES", "Remove linked worktrees before unregistering the project", 409);
6432
+ if (project.worktrees.filter((worktree) => worktree.kind === "linked").length) throw new DomainError("PROJECT_HAS_WORKTREES", "Remove linked trees before unregistering the project", 409);
6365
6433
  const terminalIdsByWorktree = /* @__PURE__ */ new Map();
6366
6434
  for (const worktree of project.worktrees) terminalIdsByWorktree.set(worktree.id, await this.deps.tmux.killServer(worktree.tmuxSocketName));
6367
6435
  await this.deps.database.db.run(sql`DELETE FROM projects WHERE id=${projectId}`);
@@ -6598,10 +6666,12 @@ var TmuxControlParser = class {
6598
6666
  const text = ascii(line);
6599
6667
  if (/^%(?:begin|end|error)(?: |$)/.test(text)) throw new TmuxControlProtocolError("Unexpected or malformed command guard");
6600
6668
  const lifecycle = text.match(/^%(pause|continue) (%\d+)$/);
6601
- if (lifecycle) {
6669
+ const lifecycleType = lifecycle?.[1];
6670
+ const lifecyclePaneId = lifecycle?.[2];
6671
+ if ((lifecycleType === "pause" || lifecycleType === "continue") && lifecyclePaneId) {
6602
6672
  events.push({
6603
- type: lifecycle[1],
6604
- paneId: lifecycle[2]
6673
+ type: lifecycleType,
6674
+ paneId: lifecyclePaneId
6605
6675
  });
6606
6676
  return;
6607
6677
  }
@@ -7505,21 +7575,32 @@ var TerminalMetadataManager = class {
7505
7575
  };
7506
7576
  //#endregion
7507
7577
  //#region src/server/app.ts
7508
- const UPLOAD_MIME_EXTENSIONS = {
7509
- "application/pdf": "pdf",
7510
- "image/gif": "gif",
7511
- "image/jpeg": "jpg",
7512
- "image/png": "png",
7513
- "image/svg+xml": "svg",
7514
- "image/webp": "webp",
7515
- "text/plain": "txt"
7516
- };
7578
+ const UPLOAD_MIME_EXTENSIONS = /* @__PURE__ */ new Map([
7579
+ ["application/pdf", "pdf"],
7580
+ ["image/gif", "gif"],
7581
+ ["image/jpeg", "jpg"],
7582
+ ["image/png", "png"],
7583
+ ["image/svg+xml", "svg"],
7584
+ ["image/webp", "webp"],
7585
+ ["text/plain", "txt"]
7586
+ ]);
7517
7587
  const UPLOAD_RETENTION_MS = 1440 * 6e4;
7518
7588
  const UPLOAD_DIRECTORY_MAX_BYTES = 512 * 1024 * 1024;
7519
7589
  const terminalPresetDefinitionsQuerySchema = z.object({
7520
7590
  projectId: z.string().optional(),
7521
7591
  worktreeId: z.string().optional()
7522
7592
  });
7593
+ const operationQuerySchema = z.object({
7594
+ kind: z.enum([
7595
+ "create",
7596
+ "finish",
7597
+ "discard",
7598
+ "project_cleanup",
7599
+ "remove",
7600
+ "external_remove"
7601
+ ]).optional(),
7602
+ projectId: z.string().optional()
7603
+ });
7523
7604
  const discardStoredDataQuerySchema = z.object({ discardStoredData: z.string().optional() });
7524
7605
  async function pruneTerminalUploads(directory, preservePath) {
7525
7606
  const entries = await fs.readdir(directory, { withFileTypes: true });
@@ -7573,11 +7654,14 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
7573
7654
  code: "INVALID_JSON",
7574
7655
  message: "Request body must be valid JSON"
7575
7656
  } }, 400);
7576
- if (error instanceof DomainError) return context.json({ error: {
7577
- code: error.code,
7578
- message: error.message,
7579
- ...error.details === void 0 ? {} : { details: error.details }
7580
- } }, error.status);
7657
+ if (error instanceof DomainError) {
7658
+ const body = { error: {
7659
+ code: error.code,
7660
+ message: error.message
7661
+ } };
7662
+ if (error.details !== void 0) body.error.details = error.details;
7663
+ return context.json(body, error.status);
7664
+ }
7581
7665
  const requestIdentifier = context.get("requestId") || crypto.randomUUID();
7582
7666
  context.header("X-Request-Id", requestIdentifier);
7583
7667
  console.error("[Treeport] API request failed", {
@@ -7650,12 +7734,13 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
7650
7734
  return context.json({ ok: true });
7651
7735
  }).get("/api/projects/:projectId/worktrees", async (context) => context.json({ worktrees: (await service.getProjectSnapshot(context.req.param("projectId"))).worktrees })).post("/api/projects/:projectId/worktree-operations", jsonInput(createWorktreeSchema), async (context) => {
7652
7736
  const body = context.req.valid("json");
7653
- const initialTerminal = body.initialTerminal ? {
7654
- name: body.initialTerminal.name,
7655
- ...body.initialTerminal.argv ? { argv: body.initialTerminal.argv } : {},
7656
- ...body.initialTerminal.returnToShell ? { returnToShell: true } : {},
7657
- ...body.initialTerminal.initialSize ? { initialSize: body.initialTerminal.initialSize } : {}
7658
- } : void 0;
7737
+ let initialTerminal;
7738
+ if (body.initialTerminal) {
7739
+ initialTerminal = { name: body.initialTerminal.name };
7740
+ if (body.initialTerminal.argv) initialTerminal.argv = body.initialTerminal.argv;
7741
+ if (body.initialTerminal.returnToShell) initialTerminal.returnToShell = true;
7742
+ if (body.initialTerminal.initialSize) initialTerminal.initialSize = body.initialTerminal.initialSize;
7743
+ }
7659
7744
  return context.json({ operation: await service.beginCreateWorktree(context.req.param("projectId"), body.name, body.base, initialTerminal, body.sourceWorktreeId) }, 202);
7660
7745
  }).get("/api/worktrees/:worktreeId", async (context) => {
7661
7746
  const worktreeId = context.req.param("worktreeId");
@@ -7712,22 +7797,23 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
7712
7797
  }
7713
7798
  const extension = path.extname(resolution.path).toLowerCase();
7714
7799
  const body = await fs.readFile(resolution.path);
7715
- context.header("content-type", {
7716
- ".css": "text/css; charset=utf-8",
7717
- ".gif": "image/gif",
7718
- ".html": "text/html; charset=utf-8",
7719
- ".jpeg": "image/jpeg",
7720
- ".jpg": "image/jpeg",
7721
- ".js": "text/javascript; charset=utf-8",
7722
- ".json": "application/json; charset=utf-8",
7723
- ".map": "application/json; charset=utf-8",
7724
- ".mjs": "text/javascript; charset=utf-8",
7725
- ".png": "image/png",
7726
- ".svg": "image/svg+xml",
7727
- ".webp": "image/webp",
7728
- ".woff": "font/woff",
7729
- ".woff2": "font/woff2"
7730
- }[extension] ?? "application/octet-stream");
7800
+ const mimeTypes = /* @__PURE__ */ new Map([
7801
+ [".css", "text/css; charset=utf-8"],
7802
+ [".gif", "image/gif"],
7803
+ [".html", "text/html; charset=utf-8"],
7804
+ [".jpeg", "image/jpeg"],
7805
+ [".jpg", "image/jpeg"],
7806
+ [".js", "text/javascript; charset=utf-8"],
7807
+ [".json", "application/json; charset=utf-8"],
7808
+ [".map", "application/json; charset=utf-8"],
7809
+ [".mjs", "text/javascript; charset=utf-8"],
7810
+ [".png", "image/png"],
7811
+ [".svg", "image/svg+xml"],
7812
+ [".webp", "image/webp"],
7813
+ [".woff", "font/woff"],
7814
+ [".woff2", "font/woff2"]
7815
+ ]);
7816
+ context.header("content-type", mimeTypes.get(extension) ?? "application/octet-stream");
7731
7817
  context.header("cache-control", "public, max-age=31536000, immutable");
7732
7818
  context.header("access-control-allow-origin", "*");
7733
7819
  context.header("content-security-policy", webPanelContentSecurityPolicy("immutable", browserOrigin, resolution.allowNetworkRequests));
@@ -7735,13 +7821,13 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
7735
7821
  return context.body(body);
7736
7822
  }).post("/api/worktrees/:worktreeId/terminals", jsonInput(createTerminalSchema), async (context) => {
7737
7823
  const body = context.req.valid("json");
7738
- const terminal = await service.createTerminal(context.req.param("worktreeId"), body.name, body.argv, body.returnToShell || body.closeOnSuccess || body.initialSize || body.cwd || body.env ? {
7739
- ...body.returnToShell ? { returnToShell: true } : {},
7740
- ...body.closeOnSuccess ? { closeOnSuccess: true } : {},
7741
- ...body.initialSize ? { initialSize: body.initialSize } : {},
7742
- ...body.cwd ? { cwd: body.cwd } : {},
7743
- ...body.env ? { env: body.env } : {}
7744
- } : void 0);
7824
+ const options = {};
7825
+ if (body.returnToShell) options.returnToShell = true;
7826
+ if (body.closeOnSuccess) options.closeOnSuccess = true;
7827
+ if (body.initialSize) options.initialSize = body.initialSize;
7828
+ if (body.cwd) options.cwd = body.cwd;
7829
+ if (body.env) options.env = body.env;
7830
+ const terminal = await service.createTerminal(context.req.param("worktreeId"), body.name, body.argv, Object.keys(options).length > 0 ? options : void 0);
7745
7831
  return context.json({ terminal }, 201);
7746
7832
  }).get("/api/worktrees/:worktreeId/remove-preview", async (context) => context.json({ preview: await service.removePreview(context.req.param("worktreeId")) })).post("/api/worktrees/:worktreeId/remove", jsonInput(removeWorktreeSchema), async (context) => {
7747
7833
  const body = context.req.valid("json");
@@ -7792,7 +7878,7 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
7792
7878
  await waitForPreviousUpload;
7793
7879
  try {
7794
7880
  const contentType = context.req.header("content-type")?.split(";", 1)[0]?.toLowerCase() ?? "";
7795
- const extension = requestedExtension || UPLOAD_MIME_EXTENSIONS[contentType] || "";
7881
+ const extension = requestedExtension || UPLOAD_MIME_EXTENSIONS.get(contentType) || "";
7796
7882
  const uploadDirectory = path.join(config.runtimeDir, "uploads");
7797
7883
  await fs.mkdir(uploadDirectory, {
7798
7884
  recursive: true,
@@ -7830,26 +7916,15 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
7830
7916
  await service.deleteTerminal(context.req.param("terminalId"));
7831
7917
  return context.json({ ok: true });
7832
7918
  }).get("/api/operations", validator("query", (value) => {
7833
- const kind = typeof value.kind === "string" ? value.kind : void 0;
7834
- const projectId = typeof value.projectId === "string" ? value.projectId : void 0;
7835
- if (kind && ![
7836
- "create",
7837
- "finish",
7838
- "discard",
7839
- "project_cleanup",
7840
- "remove",
7841
- "external_remove"
7842
- ].includes(kind)) throw new DomainError("INVALID_OPERATION_KIND", "Invalid operation kind", 400);
7843
- return {
7844
- ...kind ? { kind } : {},
7845
- ...projectId ? { projectId } : {}
7846
- };
7919
+ const parsed = operationQuerySchema.safeParse(value);
7920
+ if (!parsed.success) throw new DomainError("INVALID_OPERATION_KIND", "Invalid operation query", 400);
7921
+ return parsed.data;
7847
7922
  }), async (context) => {
7848
- const { kind, projectId } = context.req.valid("query");
7849
- return context.json({ operations: await service.listActiveOperations({
7850
- ...projectId ? { projectId } : {},
7851
- ...kind ? { kind } : {}
7852
- }) });
7923
+ const query = context.req.valid("query");
7924
+ const filters = {};
7925
+ if (query.projectId) filters.projectId = query.projectId;
7926
+ if (query.kind) filters.kind = query.kind;
7927
+ return context.json({ operations: await service.listActiveOperations(filters) });
7853
7928
  }).get("/api/operations/:operationId", async (context) => context.json({ operation: await service.getOperation(context.req.param("operationId")) })).post("/api/admin/terminate-terminals", async (context) => context.json({ terminated: await service.terminateAllTerminals() })).all("/api/*", (context) => context.json({ error: {
7854
7929
  code: "NOT_FOUND",
7855
7930
  message: "API endpoint not found"
@@ -7900,7 +7975,8 @@ async function acquireDaemonOwnership(config) {
7900
7975
  apiUrl: config.apiUrl,
7901
7976
  dataDir: config.dataDir,
7902
7977
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7903
- installationMethod: config.installationMethod ?? "development"
7978
+ installationMethod: config.installationMethod ?? "development",
7979
+ daemonLifecycle: config.daemonLifecycle
7904
7980
  };
7905
7981
  const openLock = () => fs.open(lockPath, "wx", 384).then(async (file) => {
7906
7982
  await file.writeFile(`${JSON.stringify(record)}\n`);
@@ -7908,7 +7984,9 @@ async function acquireDaemonOwnership(config) {
7908
7984
  });
7909
7985
  if (!await openLock().then(() => true, async (error) => {
7910
7986
  if (error.code !== "EEXIST") throw error;
7911
- const existing = await fs.readFile(lockPath, "utf8").then((value) => JSON.parse(value)).catch(() => null);
7987
+ const existing = await fs.readFile(lockPath, "utf8").then((value) => {
7988
+ return JSON.parse(value);
7989
+ }).catch(() => null);
7912
7990
  if (existing?.pid && Number.isInteger(existing.pid) && processExists(existing.pid)) throw new Error(`Treeport is already running for ${config.dataDir} (PID ${existing.pid})`);
7913
7991
  await fs.rm(lockPath, { force: true });
7914
7992
  return false;
@@ -7962,7 +8040,7 @@ function singleHeader(request, name) {
7962
8040
  return {
7963
8041
  present: values.length > 0,
7964
8042
  valid: values.length <= 1,
7965
- value: values.length === 1 ? values[0] : null
8043
+ value: values.length === 1 ? values[0] ?? null : null
7966
8044
  };
7967
8045
  }
7968
8046
  function hasControlCharacters(value) {
@@ -8009,14 +8087,24 @@ function effectiveOriginFor(request, source, incomingHost) {
8009
8087
  if (!forwardedHost || forwardedProtocolHeader.value?.toLowerCase() !== "https") return null;
8010
8088
  return `https://${forwardedHost.host}`;
8011
8089
  }
8090
+ function allowsOpaqueWebPanelOrigin(request, socketUpgrade) {
8091
+ if (!["GET", "HEAD"].includes(request.method?.toUpperCase() ?? "")) return false;
8092
+ const pathname = new URL(request.url ?? "/", "http://treeport.local").pathname;
8093
+ if (socketUpgrade) return /^\/api\/web-panel-dev\/[a-f0-9]{24}\/@vite-hmr$/u.test(pathname);
8094
+ return /^\/api\/web-panels\/panel_[a-f0-9]{32}\/assets(?:\/|$)/u.test(pathname) || /^\/api\/web-panel-dev\/[a-f0-9]{24}\//u.test(pathname);
8095
+ }
8012
8096
  function originIsAllowed(request, effectiveOrigin, socketUpgrade) {
8013
8097
  const originHeader = singleHeader(request, "origin");
8014
8098
  if (!originHeader.valid) return false;
8015
8099
  if (originHeader.present) {
8016
8100
  const value = originHeader.value ?? "";
8017
- if (value === "null" || !URL.canParse(value)) return false;
8018
- const parsed = new URL(value);
8019
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.origin !== value || parsed.origin !== effectiveOrigin) return false;
8101
+ if (value === "null") {
8102
+ if (!allowsOpaqueWebPanelOrigin(request, socketUpgrade)) return false;
8103
+ } else {
8104
+ if (!URL.canParse(value)) return false;
8105
+ const parsed = new URL(value);
8106
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.origin !== value || parsed.origin !== effectiveOrigin) return false;
8107
+ }
8020
8108
  }
8021
8109
  const fetchSiteHeader = singleHeader(request, "sec-fetch-site");
8022
8110
  if (!fetchSiteHeader.valid) return false;
@@ -8091,8 +8179,8 @@ var AttachmentInitializationError = class {
8091
8179
  };
8092
8180
  const TERMINAL_MAX_QUEUED_INPUT_BYTES = 1024 * 1024;
8093
8181
  const TERMINAL_MAX_QUEUED_INPUT_MESSAGES = 256;
8094
- function errorMessage(error) {
8095
- return ((error instanceof Error ? error.message : String(error)).trim() || "Terminal attachment failed").slice(0, 1e3);
8182
+ function errorMessage(cause) {
8183
+ return ((cause instanceof Error ? cause.message : String(cause)).trim() || "Terminal attachment failed").slice(0, 1e3);
8096
8184
  }
8097
8185
  function tmuxEnvironment() {
8098
8186
  return Object.fromEntries(Object.entries(process.env).filter(([key, value]) => value !== void 0 && key !== "TMUX" && key !== "TMUX_PANE"));
@@ -8510,11 +8598,11 @@ var TerminalAttachmentManager = class {
8510
8598
  if (this.isActive(connection) && this.canControl(connection, generation)) connection.pty?.write(data);
8511
8599
  }).catch((error) => this.failInputWrite(connection, error));
8512
8600
  }
8513
- failInputWrite(connection, error) {
8601
+ failInputWrite(connection, cause) {
8514
8602
  if (!this.isActive(connection)) return;
8515
8603
  this.send(connection, "terminal_error", {
8516
8604
  code: "INPUT_FAILED",
8517
- message: errorMessage(error),
8605
+ message: errorMessage(cause),
8518
8606
  retryable: true
8519
8607
  });
8520
8608
  connection.transport.disconnect(true);
@@ -8550,13 +8638,13 @@ var TerminalAttachmentManager = class {
8550
8638
  for (const client of active) if (this.isActive(client)) client.pty?.resize(next.cols, next.rows);
8551
8639
  await this.tmux.resizeWindow(next.socketName, next.sessionName, next.cols, next.rows);
8552
8640
  }
8553
- failDimensionChange(terminalId, error) {
8641
+ failDimensionChange(terminalId, cause) {
8554
8642
  this.dimensions.delete(terminalId);
8555
8643
  for (const client of [...this.clients.values()]) {
8556
8644
  if (client.terminalId !== terminalId || !this.isActive(client)) continue;
8557
8645
  this.send(client, "terminal_error", {
8558
8646
  code: "RESIZE_FAILED",
8559
- message: errorMessage(error),
8647
+ message: errorMessage(cause),
8560
8648
  retryable: true
8561
8649
  });
8562
8650
  client.transport.disconnect(true);
@@ -8756,7 +8844,7 @@ function createSocketServer(httpServer, { service, config, tmux, terminalMetadat
8756
8844
  isConnected: () => socket.connected,
8757
8845
  send(event, payload) {
8758
8846
  if (!socket.connected) return false;
8759
- socket.emit(event, payload);
8847
+ socket.emit.bind(socket)(event, payload);
8760
8848
  return true;
8761
8849
  },
8762
8850
  disconnect(retryable) {
@@ -8864,10 +8952,14 @@ function shutdown() {
8864
8952
  shuttingDown = true;
8865
8953
  attachments.dispose();
8866
8954
  terminalMetadata.dispose();
8955
+ const viteClosed = vite?.close();
8867
8956
  io.close(() => {
8868
- Promise.all([service.drainMutations(), terminalMetadata.drain()]).then(async () => {
8957
+ Promise.all([
8958
+ service.drainMutations(),
8959
+ terminalMetadata.drain(),
8960
+ viteClosed
8961
+ ]).then(async () => {
8869
8962
  await service.disposeWebPanelRuntime();
8870
- await vite?.close();
8871
8963
  database.close();
8872
8964
  await ownership.release();
8873
8965
  process.exit(0);