@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.
- package/README.md +2 -2
- package/bin/treeport.mjs +3 -1
- package/dist/{loopback-4XVZbAD1.js → loopback-D7k_J_Wl.js} +16 -11
- package/dist/node/cli/index.js +1257 -104
- package/dist/node/server/core/launcher.js +19 -2
- package/dist/node/server/index.js +469 -377
- package/dist/{shell-integration-7aBNr-p0.js → shell-integration-Be_c91lw.js} +17 -15
- package/dist/web/assets/{index-CUy0IkGL.js → index-DCtptjcH.js} +5 -5
- package/dist/web/assets/index-Wj0w0nWP.css +2 -0
- package/dist/web/index.html +2 -2
- package/dist/web/manifest.webmanifest +2 -2
- package/package.json +4 -4
- package/skills/treeport/SKILL.md +20 -20
- package/dist/web/assets/index-0q5frbNy.css +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as
|
|
2
|
-
import { n as prepareShellIntegration } from "../../shell-integration-
|
|
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(
|
|
114
|
-
return
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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-${
|
|
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
|
|
326
|
-
|
|
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("
|
|
513
|
-
if (name.length > 120) throw new Error("
|
|
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
|
|
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
|
-
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
const argsInput =
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
if (
|
|
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
|
|
589
|
-
|
|
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
|
-
|
|
595
|
-
if (
|
|
596
|
-
|
|
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
|
-
|
|
599
|
-
if (
|
|
600
|
-
env
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
617
|
-
const
|
|
618
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2194
|
+
let input;
|
|
2158
2195
|
try {
|
|
2159
|
-
|
|
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
|
-
|
|
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
|
|
2171
|
-
|
|
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
|
-
|
|
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
|
|
2182
|
-
|
|
2183
|
-
|
|
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
|
-
|
|
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
|
|
2191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
2356
|
-
|
|
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 [...
|
|
2419
|
+
return [...parsed.data];
|
|
2361
2420
|
}
|
|
2362
2421
|
validateWebPanelManifest(entries, packageJsonPath) {
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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:
|
|
2431
|
+
source: parsedSource.data,
|
|
2369
2432
|
allowSameOrigin: false
|
|
2370
2433
|
};
|
|
2371
2434
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2484
|
-
|
|
2485
|
-
if (treeport !== void 0) {
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2935
|
-
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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,
|
|
3506
|
-
const raw =
|
|
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
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
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
|
-
|
|
3666
|
-
|
|
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
|
|
3744
|
-
bind-key -T copy-mode-vi
|
|
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
|
-
|
|
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
|
|
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
|
|
4286
|
-
if (
|
|
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
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
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
|
|
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", "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5744
|
-
|
|
5745
|
-
|
|
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: `
|
|
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 ??= "
|
|
5836
|
+
if (setup.tasks.length > 0 || setupError) if (!terminal) setupError ??= "Tree setup: no persistent terminal could be started";
|
|
5771
5837
|
else try {
|
|
5772
|
-
|
|
5838
|
+
const setupOptions = {
|
|
5773
5839
|
setup: {
|
|
5774
5840
|
tasks: setup.tasks,
|
|
5775
5841
|
error: setupError
|
|
5776
5842
|
},
|
|
5777
|
-
closeOnSuccess: true
|
|
5778
|
-
|
|
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 = `
|
|
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: "
|
|
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", "
|
|
5902
|
-
if (this.projectLocks.has(worktree.projectId) || this.worktreeLocks.has(worktreeId) || worktree.prunable) throw new DomainError("WORKTREE_BUSY", "Cannot create a terminal while the
|
|
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", "
|
|
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
|
|
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", "
|
|
6016
|
-
if (this.worktreeLocks.has(worktreeId)) throw new DomainError("WORKTREE_UNAVAILABLE", "Cannot refresh a pull request while the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6119
|
-
if (request.confirmationToken !== preview.confirmationToken) throw new DomainError("REMOVE_PREVIEW_STALE", "The
|
|
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
|
|
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
|
|
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 :
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
6669
|
+
const lifecycleType = lifecycle?.[1];
|
|
6670
|
+
const lifecyclePaneId = lifecycle?.[2];
|
|
6671
|
+
if ((lifecycleType === "pause" || lifecycleType === "continue") && lifecyclePaneId) {
|
|
6602
6672
|
events.push({
|
|
6603
|
-
type:
|
|
6604
|
-
paneId:
|
|
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"
|
|
7510
|
-
"image/gif"
|
|
7511
|
-
"image/jpeg"
|
|
7512
|
-
"image/png"
|
|
7513
|
-
"image/svg+xml"
|
|
7514
|
-
"image/webp"
|
|
7515
|
-
"text/plain"
|
|
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)
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
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
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
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
|
-
|
|
7716
|
-
".css"
|
|
7717
|
-
".gif"
|
|
7718
|
-
".html"
|
|
7719
|
-
".jpeg"
|
|
7720
|
-
".jpg"
|
|
7721
|
-
".js"
|
|
7722
|
-
".json"
|
|
7723
|
-
".map"
|
|
7724
|
-
".mjs"
|
|
7725
|
-
".png"
|
|
7726
|
-
".svg"
|
|
7727
|
-
".webp"
|
|
7728
|
-
".woff"
|
|
7729
|
-
".woff2"
|
|
7730
|
-
|
|
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
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
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
|
|
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
|
|
7834
|
-
|
|
7835
|
-
|
|
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
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
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) =>
|
|
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"
|
|
8018
|
-
|
|
8019
|
-
|
|
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(
|
|
8095
|
-
return ((
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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([
|
|
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);
|