@treeport/treeport 0.3.0 → 0.5.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 +3 -3
- package/dist/node/cli/index.js +118 -1612
- package/dist/node/server/core/launcher.js +35 -2
- package/dist/node/server/index.js +1193 -572
- package/dist/{shell-integration-7aBNr-p0.js → shell-integration-Be_c91lw.js} +17 -15
- package/dist/update-BW-a6Bd-.js +3107 -0
- package/dist/web/assets/index-Cr4UkmRD.js +146 -0
- package/dist/web/assets/index-he-SubzL.css +2 -0
- package/dist/web/index.html +2 -2
- package/dist/web/manifest.webmanifest +2 -2
- package/drizzle/0008_recent_project_visibility.sql +4 -0
- package/drizzle/0009_open_folders.sql +148 -0
- package/drizzle/meta/0008_snapshot.json +792 -0
- package/drizzle/meta/0009_snapshot.json +804 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +4 -4
- package/skills/treeport/SKILL.md +31 -22
- package/dist/loopback-Dyv_owrb.js +0 -407
- package/dist/web/assets/index-0q5frbNy.css +0 -2
- package/dist/web/assets/index-CUy0IkGL.js +0 -146
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as
|
|
2
|
-
import { n as prepareShellIntegration } from "../../shell-integration-
|
|
1
|
+
import { $ as repositoryTerminalPresetSchema, At as terminalSizeSchema, B as createWorktreeSchema, Ct as parseTerminalProgress, Dt as terminalLegacyTakeControlSchema, Et as terminalInputSchema, G as openWebPanelSchema, H as deleteWebPanelStorageSchema, I as browseDirectoryQuerySchema, J as packageReloadSchema, K as packageInstallSchema, L as createTerminalPresetSchema, M as parseDurationMs, Ot as terminalOutputAckSchema, P as TERMINAL_MAX_UPLOAD_BYTES, Q as removeWorktreeSchema, R as createTerminalSchema, St as parseTerminalAuth, Tt as terminalBinarySchema, U as formatCommandLine, V as deleteTerminalPresetSchema, W as getWebPanelStorageSchema, X as packageUpdateSchema, Y as packageRemoveSchema, Z as registerProjectSchema, _ as serviceStatus, _t as TERMINAL_SCROLL_EXIT_SEQUENCE, a as readLocalUpdateProgress, at as updateTerminalPresetSchema, bt as TERMINAL_SELECTION_START_SEQUENCE, c as createUpdateStartupReporter, dt as TERMINAL_CONTROLLER_GRACE_MS, et as repositoryTerminalPresetsFileSchema, ft as TERMINAL_MAX_CLIENT_MESSAGE_BYTES, gt as TERMINAL_OUTPUT_STALL_TIMEOUT_MS, i as isCanonicalTreeportVersion, it as updateProjectSchema, j as assertLoopbackHost, jt as terminalTakeControlSchema, kt as terminalResizeSchema, n as compareTreeportVersions, nt as setWebPanelStorageSchema, o as resolveLatestTreeportRelease, ot as updateTerminalSchema, q as packageProjectQuerySchema, r as inspectLocalUpdateInstallation, rt as terminalCaptureQuerySchema, st as webPanelInputSchema, tt as requestWorkspaceOpenSchema, ut as SOCKET_IO_PATH, vt as TERMINAL_SELECTION_CLEAR_SEQUENCE, wt as terminalBellAcknowledgementSchema, xt as TERMINAL_SELECTION_STOP_SEQUENCE, yt as TERMINAL_SELECTION_RESTORE_SEQUENCE, z as createWebPanelSchema } from "../../update-BW-a6Bd-.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,14 +110,20 @@ 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
|
+
}
|
|
116
|
+
function signalProcessGroup(processGroupId, signal) {
|
|
117
|
+
try {
|
|
118
|
+
process.kill(-processGroupId, signal);
|
|
119
|
+
} catch (cause) {
|
|
120
|
+
if (cause.code !== "ESRCH") throw cause;
|
|
121
|
+
}
|
|
115
122
|
}
|
|
116
123
|
/**
|
|
117
|
-
* Runs a child command as an interruptible Effect. The child is acquired
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* exit. Descendant process groups are intentionally outside this contract.
|
|
124
|
+
* Runs a child command as an interruptible Effect. The child is acquired in an
|
|
125
|
+
* isolated process group, so interruption completes only after the finalizer
|
|
126
|
+
* has signaled the full group and observed the direct child exit.
|
|
121
127
|
*/
|
|
122
128
|
function runCommandEffect(request) {
|
|
123
129
|
const stdoutLimit = request.maxStdoutBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
@@ -134,7 +140,8 @@ function runCommandEffect(request) {
|
|
|
134
140
|
"pipe",
|
|
135
141
|
"pipe"
|
|
136
142
|
],
|
|
137
|
-
shell: false
|
|
143
|
+
shell: false,
|
|
144
|
+
detached: true
|
|
138
145
|
});
|
|
139
146
|
} catch (cause) {
|
|
140
147
|
resume(Effect.fail(new SpawnCommandError(request, cause)));
|
|
@@ -157,6 +164,7 @@ function runCommandEffect(request) {
|
|
|
157
164
|
child.removeListener("spawn", onSpawn);
|
|
158
165
|
resume(Effect.succeed({
|
|
159
166
|
child,
|
|
167
|
+
processGroupId: child.pid,
|
|
160
168
|
exit,
|
|
161
169
|
onExit,
|
|
162
170
|
onProcessError,
|
|
@@ -243,21 +251,19 @@ function runCommandEffect(request) {
|
|
|
243
251
|
removeUseListeners();
|
|
244
252
|
});
|
|
245
253
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
const timeoutMs = request.timeoutMs;
|
|
255
|
+
if (timeoutMs === void 0 || timeoutMs <= 0) return awaitResult;
|
|
256
|
+
return Effect.raceFirst(awaitResult, Effect.sleep(Duration.millis(timeoutMs)).pipe(Effect.flatMap(() => Effect.sync(() => {
|
|
257
|
+
resource.terminalError ??= new TimeoutCommandError(request, timeoutMs);
|
|
249
258
|
return resource.terminalError;
|
|
250
259
|
})), Effect.flatMap(Effect.fail)));
|
|
251
|
-
}, (resource) => Deferred.isDone(resource.exit).pipe(Effect.flatMap((alreadyExited) => {
|
|
252
|
-
if (alreadyExited) return Effect.void;
|
|
260
|
+
}, (resource, useExit) => Deferred.isDone(resource.exit).pipe(Effect.flatMap((alreadyExited) => {
|
|
261
|
+
if (alreadyExited && Exit.isSuccess(useExit)) return Effect.void;
|
|
253
262
|
return Effect.sync(() => {
|
|
254
|
-
resource.
|
|
255
|
-
}).pipe(Effect.zipRight(Effect.raceFirst(Deferred.await(resource.exit).pipe(Effect.as(true), Effect.interruptible), Effect.sleep(Duration.millis(killGraceMs)).pipe(Effect.as(false), Effect.interruptible))), Effect.flatMap((exited) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
resource.child.kill("SIGKILL");
|
|
259
|
-
}).pipe(Effect.zipRight(Deferred.await(resource.exit)));
|
|
260
|
-
}));
|
|
263
|
+
signalProcessGroup(resource.processGroupId, "SIGTERM");
|
|
264
|
+
}).pipe(Effect.zipRight(Effect.raceFirst(Deferred.await(resource.exit).pipe(Effect.as(true), Effect.interruptible), Effect.sleep(Duration.millis(killGraceMs)).pipe(Effect.as(false), Effect.interruptible))), Effect.flatMap((exited) => Effect.sync(() => {
|
|
265
|
+
signalProcessGroup(resource.processGroupId, "SIGKILL");
|
|
266
|
+
}).pipe(Effect.zipRight(exited ? Effect.void : Deferred.await(resource.exit)))));
|
|
261
267
|
}), Effect.ensuring(Effect.sync(() => {
|
|
262
268
|
resource.child.removeListener("exit", resource.onExit);
|
|
263
269
|
resource.child.removeListener("error", resource.onProcessError);
|
|
@@ -311,7 +317,7 @@ function defaultCacheDir(env, platform = process.platform) {
|
|
|
311
317
|
}
|
|
312
318
|
function defaultRuntimeDir(env) {
|
|
313
319
|
if (env.XDG_RUNTIME_DIR) return path.join(expandHome(env.XDG_RUNTIME_DIR), "treeport");
|
|
314
|
-
return path.join(os.tmpdir(), `treeport-${
|
|
320
|
+
return path.join(os.tmpdir(), `treeport-${process.getuid?.() ?? "user"}`);
|
|
315
321
|
}
|
|
316
322
|
function loadConfig(env = process.env) {
|
|
317
323
|
const host = env.TREEPORT_HOST?.trim() || env.HOST?.trim() || "127.0.0.1";
|
|
@@ -323,7 +329,7 @@ function loadConfig(env = process.env) {
|
|
|
323
329
|
const runtimeDir = path.resolve(expandHome(env.TREEPORT_RUNTIME_DIR?.trim() || defaultRuntimeDir(env)));
|
|
324
330
|
const daemonLifecycle = env.TREEPORT_DAEMON_LIFECYCLE?.trim() || "treeport";
|
|
325
331
|
if (daemonLifecycle !== "treeport" && daemonLifecycle !== "service" && daemonLifecycle !== "external") throw new Error("TREEPORT_DAEMON_LIFECYCLE must be treeport, service, or external");
|
|
326
|
-
|
|
332
|
+
const config = {
|
|
327
333
|
host,
|
|
328
334
|
port: portValue,
|
|
329
335
|
dataDir,
|
|
@@ -339,9 +345,10 @@ function loadConfig(env = process.env) {
|
|
|
339
345
|
appVersion: env.TREEPORT_APP_VERSION?.trim() || "development",
|
|
340
346
|
instanceId: env.TREEPORT_INSTANCE_ID?.trim() || crypto.randomUUID(),
|
|
341
347
|
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() } : {}
|
|
348
|
+
webDevelopment: env.TREEPORT_WEB_DEVELOPMENT?.trim() === "1"
|
|
344
349
|
};
|
|
350
|
+
if (env.TREEPORT_WEB_DIST?.trim()) config.webDist = env.TREEPORT_WEB_DIST.trim();
|
|
351
|
+
return config;
|
|
345
352
|
}
|
|
346
353
|
//#endregion
|
|
347
354
|
//#region src/server/core/database-schema.ts
|
|
@@ -357,6 +364,7 @@ var database_schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
357
364
|
const projects = sqliteTable("projects", {
|
|
358
365
|
id: text().primaryKey(),
|
|
359
366
|
name: text().notNull(),
|
|
367
|
+
kind: text("project_kind", { enum: ["repository", "folder"] }).notNull().default("repository"),
|
|
360
368
|
repositoryPath: text("repository_path").notNull().unique(),
|
|
361
369
|
mainWorktreePath: text("main_worktree_path").notNull(),
|
|
362
370
|
defaultBranch: text("default_branch").notNull(),
|
|
@@ -366,15 +374,18 @@ const projects = sqliteTable("projects", {
|
|
|
366
374
|
repositoryInode: text("repository_inode").notNull(),
|
|
367
375
|
nameIsCustom: integer("name_is_custom").notNull().default(0),
|
|
368
376
|
isOpen: integer("is_open").notNull().default(1),
|
|
377
|
+
showInRecents: integer("show_in_recents").notNull().default(0),
|
|
369
378
|
lastOpenedAt: text("last_opened_at").notNull(),
|
|
370
379
|
createdAt: text("created_at").notNull(),
|
|
371
380
|
updatedAt: text("updated_at").notNull()
|
|
372
381
|
}, (table) => [
|
|
382
|
+
check("projects_kind_check", sql`${table.kind} IN ('repository','folder')`),
|
|
373
383
|
check("projects_color_check", sql`${table.color} IS NULL OR ${table.color} IN ('rose','orange','amber','emerald','cyan','blue','violet','pink')`),
|
|
374
384
|
check("projects_name_is_custom_check", sql`${table.nameIsCustom} IN (0,1)`),
|
|
375
385
|
check("projects_is_open_check", sql`${table.isOpen} IN (0,1)`),
|
|
386
|
+
check("projects_show_in_recents_check", sql`${table.showInRecents} IN (0,1)`),
|
|
376
387
|
uniqueIndex("projects_repository_identity_idx").on(table.repositoryIdentity).where(sql`${table.repositoryIdentity} IS NOT NULL`),
|
|
377
|
-
index("projects_recent_idx").on(table.isOpen, desc(table.lastOpenedAt), table.id)
|
|
388
|
+
index("projects_recent_idx").on(table.isOpen, table.showInRecents, desc(table.lastOpenedAt), table.id)
|
|
378
389
|
]);
|
|
379
390
|
const worktrees = sqliteTable("worktrees", {
|
|
380
391
|
id: text().primaryKey(),
|
|
@@ -403,7 +414,7 @@ const worktrees = sqliteTable("worktrees", {
|
|
|
403
414
|
check("worktrees_detached_check", sql`${table.detached} IN (0,1)`),
|
|
404
415
|
check("worktrees_locked_check", sql`${table.locked} IN (0,1)`),
|
|
405
416
|
check("worktrees_prunable_check", sql`${table.prunable} IN (0,1)`),
|
|
406
|
-
check("worktrees_kind_check", sql`${table.kind} IN ('main','linked')`),
|
|
417
|
+
check("worktrees_kind_check", sql`${table.kind} IN ('main','linked','folder')`),
|
|
407
418
|
index("worktrees_project_idx").on(table.projectId),
|
|
408
419
|
uniqueIndex("worktrees_git_key_idx").on(table.projectId, table.gitWorktreeKey).where(sql`${table.gitWorktreeKey} IS NOT NULL`)
|
|
409
420
|
]);
|
|
@@ -463,7 +474,7 @@ async function readOptionalJsonc(filePath) {
|
|
|
463
474
|
try {
|
|
464
475
|
source = await fs.readFile(filePath, "utf8");
|
|
465
476
|
} catch (error) {
|
|
466
|
-
if (error.code === "ENOENT") return { found: false };
|
|
477
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return { found: false };
|
|
467
478
|
throw error;
|
|
468
479
|
}
|
|
469
480
|
const errors = [];
|
|
@@ -509,12 +520,13 @@ async function assertNoSymlinkComponents(parent, candidate) {
|
|
|
509
520
|
}
|
|
510
521
|
function normalizeWorktreeName(input) {
|
|
511
522
|
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("
|
|
523
|
+
if (!name) throw new Error("Tree name is required");
|
|
524
|
+
if (name.length > 120) throw new Error("Tree name must be 120 characters or fewer");
|
|
514
525
|
return name;
|
|
515
526
|
}
|
|
516
527
|
function inferWorktreeName(mainWorktreePath, worktreePath, kind) {
|
|
517
|
-
if (kind === "main") return "main
|
|
528
|
+
if (kind === "main") return "main tree";
|
|
529
|
+
if (kind === "folder") return path.basename(worktreePath);
|
|
518
530
|
const checkoutName = path.basename(worktreePath);
|
|
519
531
|
return checkoutName === path.basename(mainWorktreePath) ? path.basename(path.dirname(worktreePath)) : checkoutName;
|
|
520
532
|
}
|
|
@@ -564,58 +576,64 @@ async function prepareZedWorktreeWrapper(mainWorktreePath, wrapperPath) {
|
|
|
564
576
|
};
|
|
565
577
|
}
|
|
566
578
|
const ZED_TASKS_CONFIG_PATH = path.join(".zed", "tasks.json");
|
|
579
|
+
z.unknown();
|
|
580
|
+
const zedTaskRecordSchema = z.looseObject({
|
|
581
|
+
command: z.unknown().optional(),
|
|
582
|
+
args: z.unknown().optional(),
|
|
583
|
+
env: z.unknown().optional(),
|
|
584
|
+
cwd: z.unknown().optional(),
|
|
585
|
+
label: z.unknown().optional(),
|
|
586
|
+
hooks: z.unknown().optional()
|
|
587
|
+
});
|
|
567
588
|
function taskArray(value) {
|
|
568
|
-
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
return null;
|
|
589
|
+
const direct = z.array(z.unknown()).safeParse(value);
|
|
590
|
+
if (direct.success) return direct.data;
|
|
591
|
+
const wrapped = z.object({ tasks: z.array(z.unknown()) }).safeParse(value);
|
|
592
|
+
return wrapped.success ? wrapped.data.tasks : null;
|
|
574
593
|
}
|
|
575
594
|
function parseTask(entry, index, options) {
|
|
576
595
|
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`);
|
|
596
|
+
const parsedEntry = zedTaskRecordSchema.safeParse(entry);
|
|
597
|
+
if (!parsedEntry.success) throw new Error(`${prefix} must be an object`);
|
|
598
|
+
const { args: argsInput, command, cwd, env: environmentInput, label } = parsedEntry.data;
|
|
599
|
+
const parsedLabel = z.string().safeParse(label);
|
|
600
|
+
if ((!parsedLabel.success || !parsedLabel.data.trim()) && options.requireLabel) throw new Error(`${prefix} is missing a label`);
|
|
601
|
+
const parsedCommand = z.string().safeParse(command);
|
|
602
|
+
if (!parsedCommand.success || !parsedCommand.data.trim()) throw new Error(`${prefix} is missing a command`);
|
|
587
603
|
if (argsInput !== void 0 && !Array.isArray(argsInput)) throw new Error(`${prefix} has invalid args`);
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
return argument;
|
|
591
|
-
});
|
|
604
|
+
const parsedArgs = z.array(z.string()).safeParse(argsInput ?? []);
|
|
605
|
+
if (!parsedArgs.success) throw new Error(`${prefix} has a non-string argument`);
|
|
592
606
|
const env = {};
|
|
593
607
|
if (environmentInput !== void 0) {
|
|
594
|
-
|
|
595
|
-
if (
|
|
596
|
-
|
|
608
|
+
const parsedEnvironment = z.record(z.string(), z.unknown()).safeParse(environmentInput);
|
|
609
|
+
if (!parsedEnvironment.success) throw new Error(`${prefix} has invalid env`);
|
|
610
|
+
if (options.validateLaunchFields && Object.keys(parsedEnvironment.data).length > 128) throw new Error(`${prefix} has more than 128 environment variables`);
|
|
611
|
+
for (const [key, environmentValue] of Object.entries(parsedEnvironment.data)) {
|
|
597
612
|
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
|
-
|
|
613
|
+
const parsedValue = z.string().safeParse(environmentValue);
|
|
614
|
+
if (!parsedValue.success) throw new Error(`${prefix} has a non-string env value`);
|
|
615
|
+
if (options.validateLaunchFields && (parsedValue.data.length > 4096 || parsedValue.data.includes("\0"))) throw new Error(`${prefix} has an invalid env value`);
|
|
616
|
+
env[key] = parsedValue.data;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const parsedCwd = z.string().safeParse(cwd);
|
|
620
|
+
if (cwd !== void 0 && !parsedCwd.success) throw new Error(`${prefix} has invalid cwd`);
|
|
621
|
+
if (options.validateLaunchFields && parsedCwd.success && (!parsedCwd.data.trim() || parsedCwd.data.length > 4096 || parsedCwd.data.includes("\0"))) throw new Error(`${prefix} has invalid cwd`);
|
|
622
|
+
const task = {
|
|
623
|
+
label: parsedLabel.success && parsedLabel.data.trim() ? parsedLabel.data : `Task ${index + 1}`,
|
|
624
|
+
command: parsedCommand.data,
|
|
625
|
+
args: parsedArgs.data,
|
|
610
626
|
env
|
|
611
627
|
};
|
|
628
|
+
if (parsedCwd.success) task.cwd = parsedCwd.data;
|
|
629
|
+
return task;
|
|
612
630
|
}
|
|
613
631
|
async function loadCreateWorktreeTasks(mainWorktreePath) {
|
|
614
632
|
const tasksFile = await readOptionalJsonc(path.join(mainWorktreePath, ZED_TASKS_CONFIG_PATH));
|
|
615
633
|
return (taskArray(tasksFile.found ? tasksFile.value : null) ?? []).flatMap((entry, index) => {
|
|
616
|
-
|
|
617
|
-
const
|
|
618
|
-
if (!
|
|
634
|
+
const parsedEntry = zedTaskRecordSchema.safeParse(entry);
|
|
635
|
+
const parsedHooks = z.array(z.string()).safeParse(parsedEntry.success ? parsedEntry.data.hooks : void 0);
|
|
636
|
+
if (!parsedHooks.success || !parsedHooks.data.includes("create_worktree")) return [];
|
|
619
637
|
return [parseTask(entry, index, {
|
|
620
638
|
requireLabel: false,
|
|
621
639
|
validateLaunchFields: false
|
|
@@ -641,11 +659,8 @@ function resolveTask(task, input, protectCompatibilityEnvironment) {
|
|
|
641
659
|
const useShell = /[\s;&|<>`$()]/u.test(command);
|
|
642
660
|
return {
|
|
643
661
|
label: expand(task.label, compatibilityEnvironment),
|
|
644
|
-
argv: useShell ? [
|
|
645
|
-
|
|
646
|
-
"-lc",
|
|
647
|
-
[command, ...args.map(shellQuote)].join(" ")
|
|
648
|
-
] : [command, ...args],
|
|
662
|
+
argv: useShell ? null : [command, ...args],
|
|
663
|
+
shellCommand: useShell ? [command, ...args.map(shellQuote)].join(" ") : null,
|
|
649
664
|
cwd,
|
|
650
665
|
env: protectCompatibilityEnvironment ? {
|
|
651
666
|
...taskEnvironment,
|
|
@@ -704,8 +719,9 @@ async function loadZedTerminalPresetDefinitions(input) {
|
|
|
704
719
|
definitions.push({
|
|
705
720
|
id: `repository:${input.projectId}:zed-task:${index}`,
|
|
706
721
|
name: resolved.label,
|
|
707
|
-
executable: resolved.argv[0],
|
|
708
|
-
args: resolved.argv
|
|
722
|
+
executable: resolved.argv?.[0] ?? null,
|
|
723
|
+
args: resolved.argv?.slice(1) ?? [],
|
|
724
|
+
shellCommand: resolved.shellCommand,
|
|
709
725
|
cwd: resolved.cwd,
|
|
710
726
|
env: resolved.env,
|
|
711
727
|
closeOnSuccess: false,
|
|
@@ -723,9 +739,18 @@ async function loadZedTerminalPresetDefinitions(input) {
|
|
|
723
739
|
async function resolveZedCreateWorktreeSetupTasks(input) {
|
|
724
740
|
return (await loadCreateWorktreeTasks(input.mainWorktreePath)).map((task) => {
|
|
725
741
|
const resolved = resolveTask(task, input, false);
|
|
742
|
+
let argv = resolved.argv;
|
|
743
|
+
if (!argv) {
|
|
744
|
+
if (!resolved.shellCommand) throw new Error(`Zed task ${task.label} has no resolved command`);
|
|
745
|
+
argv = [
|
|
746
|
+
input.shell,
|
|
747
|
+
"-lc",
|
|
748
|
+
resolved.shellCommand
|
|
749
|
+
];
|
|
750
|
+
}
|
|
726
751
|
return {
|
|
727
752
|
label: task.label,
|
|
728
|
-
argv
|
|
753
|
+
argv,
|
|
729
754
|
cwd: resolved.cwd,
|
|
730
755
|
env: resolved.env,
|
|
731
756
|
timeoutMs: 30 * 6e4
|
|
@@ -866,6 +891,8 @@ async function openDatabase(filePath, options = {}) {
|
|
|
866
891
|
const databaseExists = fsSync.existsSync(absoluteFilePath);
|
|
867
892
|
let hasDurableSchema = false;
|
|
868
893
|
let hasLegacyMigrations = false;
|
|
894
|
+
let migrationsPending = !databaseExists;
|
|
895
|
+
const migrationSnapshotPaths = [];
|
|
869
896
|
let drizzleRows = [];
|
|
870
897
|
if (!databaseExists) await fsSync.promises.mkdir(path.dirname(absoluteFilePath), {
|
|
871
898
|
recursive: true,
|
|
@@ -911,7 +938,8 @@ async function openDatabase(filePath, options = {}) {
|
|
|
911
938
|
if (!knownMigration || knownMigration.hash !== row.hash) throw new Error(`Treeport database at ${absoluteFilePath} has an unrecognized migration history. Use a compatible Treeport version or restore a pre-migration snapshot.`);
|
|
912
939
|
}
|
|
913
940
|
if (hasDurableSchema && !hasLegacyMigrations && drizzleRows.length === 0) throw new Error(`Treeport database at ${absoluteFilePath} has no recognized migration history; refusing to modify it.`);
|
|
914
|
-
|
|
941
|
+
migrationsPending = drizzleRows.length === 0 || Number(drizzleRows.at(-1)?.createdAt) < latestMigration.folderMillis;
|
|
942
|
+
if (migrationsPending && hasDurableSchema) {
|
|
915
943
|
const backupDirectory = path.resolve(options.backupDirectory ?? path.join(path.dirname(absoluteFilePath), "database-backups"));
|
|
916
944
|
await fsSync.promises.mkdir(backupDirectory, {
|
|
917
945
|
recursive: true,
|
|
@@ -924,6 +952,7 @@ async function openDatabase(filePath, options = {}) {
|
|
|
924
952
|
try {
|
|
925
953
|
await db.run(sql.raw(`VACUUM INTO '${backupPath.replaceAll("'", "''")}'`));
|
|
926
954
|
await fsSync.promises.chmod(backupPath, 384);
|
|
955
|
+
migrationSnapshotPaths.push(backupPath);
|
|
927
956
|
} catch (error) {
|
|
928
957
|
await fsSync.promises.rm(backupPath, { force: true });
|
|
929
958
|
throw error;
|
|
@@ -957,6 +986,8 @@ async function openDatabase(filePath, options = {}) {
|
|
|
957
986
|
return {
|
|
958
987
|
filePath: absoluteFilePath,
|
|
959
988
|
db,
|
|
989
|
+
migrationState: migrationsPending ? "advanced" : "unchanged",
|
|
990
|
+
migrationSnapshotPaths,
|
|
960
991
|
close: () => client.close()
|
|
961
992
|
};
|
|
962
993
|
} catch (error) {
|
|
@@ -968,6 +999,8 @@ function mapProject(row, worktreeRows) {
|
|
|
968
999
|
return {
|
|
969
1000
|
id: row.id,
|
|
970
1001
|
name: row.name,
|
|
1002
|
+
kind: row.kind,
|
|
1003
|
+
rootPath: row.repositoryPath,
|
|
971
1004
|
repositoryPath: row.repositoryPath,
|
|
972
1005
|
mainWorktreePath: row.mainWorktreePath,
|
|
973
1006
|
defaultBranch: row.defaultBranch,
|
|
@@ -987,7 +1020,7 @@ function mapWorktree(row, mainWorktreePath) {
|
|
|
987
1020
|
projectId: row.projectId,
|
|
988
1021
|
name: inferWorktreeName(mainWorktreePath, row.path, row.kind),
|
|
989
1022
|
path: row.path,
|
|
990
|
-
head: row.head,
|
|
1023
|
+
head: row.kind === "folder" ? "" : row.head,
|
|
991
1024
|
branch: row.branch,
|
|
992
1025
|
detached: Boolean(row.detached),
|
|
993
1026
|
locked: Boolean(row.locked),
|
|
@@ -1106,6 +1139,14 @@ var ProductEventBus = class {
|
|
|
1106
1139
|
};
|
|
1107
1140
|
//#endregion
|
|
1108
1141
|
//#region src/server/core/gh.ts
|
|
1142
|
+
const ghPrSchema = z.object({
|
|
1143
|
+
number: z.number().optional(),
|
|
1144
|
+
state: z.string().optional(),
|
|
1145
|
+
url: z.string().optional(),
|
|
1146
|
+
baseRefName: z.string().optional(),
|
|
1147
|
+
headRefName: z.string().optional(),
|
|
1148
|
+
mergedAt: z.string().nullable().optional()
|
|
1149
|
+
}).strict();
|
|
1109
1150
|
function mapPrState(pr) {
|
|
1110
1151
|
if (!pr) return "no_pr";
|
|
1111
1152
|
if (pr.mergedAt || pr.state?.toUpperCase() === "MERGED") return "merged";
|
|
@@ -1156,7 +1197,7 @@ var GhAdapter = class {
|
|
|
1156
1197
|
timeoutMs: 3e4
|
|
1157
1198
|
});
|
|
1158
1199
|
if (result.exitCode !== 0) return unknownPr();
|
|
1159
|
-
const pr = JSON.parse(result.stdout)[0] ?? null;
|
|
1200
|
+
const pr = z.array(ghPrSchema).parse(JSON.parse(result.stdout))[0] ?? null;
|
|
1160
1201
|
return {
|
|
1161
1202
|
state: mapPrState(pr),
|
|
1162
1203
|
number: pr?.number ?? null,
|
|
@@ -1268,10 +1309,34 @@ var GitAdapter = class {
|
|
|
1268
1309
|
timeoutMs: 3e4
|
|
1269
1310
|
});
|
|
1270
1311
|
}
|
|
1312
|
+
async findRepositoryRoot(inputPath) {
|
|
1313
|
+
const canonicalInput = await fs.realpath(path.resolve(inputPath));
|
|
1314
|
+
const request = {
|
|
1315
|
+
executable: this.executable,
|
|
1316
|
+
args: ["rev-parse", "--show-toplevel"],
|
|
1317
|
+
cwd: canonicalInput,
|
|
1318
|
+
timeoutMs: 3e4
|
|
1319
|
+
};
|
|
1320
|
+
const result = await this.runner.run(request);
|
|
1321
|
+
if (result.exitCode === 0) return fs.realpath(result.stdout.trim());
|
|
1322
|
+
if (/not a git repository|outside repository/iu.test(result.stderr)) return null;
|
|
1323
|
+
throw new ExternalCommandError(`Could not inspect Git repository state: ${result.stderr.trim() || `Git exited with code ${result.exitCode}`}`, request, result);
|
|
1324
|
+
}
|
|
1325
|
+
async findProjectRepositoryRoot(inputPath) {
|
|
1326
|
+
const canonicalInput = await fs.realpath(path.resolve(inputPath));
|
|
1327
|
+
const repositoryRoot = await this.findRepositoryRoot(canonicalInput);
|
|
1328
|
+
if (!repositoryRoot || repositoryRoot === canonicalInput) return repositoryRoot;
|
|
1329
|
+
return (await this.checked(repositoryRoot, [
|
|
1330
|
+
"rev-list",
|
|
1331
|
+
"--all",
|
|
1332
|
+
"--max-count=1"
|
|
1333
|
+
])).stdout.trim() ? repositoryRoot : null;
|
|
1334
|
+
}
|
|
1271
1335
|
async canonicalizeRepositoryPath(inputPath) {
|
|
1336
|
+
const repositoryRoot = await this.findRepositoryRoot(inputPath);
|
|
1337
|
+
if (repositoryRoot) return repositoryRoot;
|
|
1272
1338
|
const canonicalInput = await fs.realpath(path.resolve(inputPath));
|
|
1273
|
-
|
|
1274
|
-
return fs.realpath(result.stdout.trim());
|
|
1339
|
+
throw new Error(`Not a Git repository: ${canonicalInput}`);
|
|
1275
1340
|
}
|
|
1276
1341
|
async repositoryIdentityValues(cwd) {
|
|
1277
1342
|
const result = await this.runner.run({
|
|
@@ -1997,16 +2062,35 @@ async function checkRuntimePrerequisites(config) {
|
|
|
1997
2062
|
}
|
|
1998
2063
|
//#endregion
|
|
1999
2064
|
//#region src/server/core/package-system.ts
|
|
2065
|
+
z.unknown();
|
|
2066
|
+
const manifestPatternsSchema = z.array(z.string());
|
|
2067
|
+
const webPanelManifestEntrySchema = z.union([z.string(), z.strictObject({
|
|
2068
|
+
source: z.string(),
|
|
2069
|
+
permissions: z.array(z.enum(["same-origin"])).optional()
|
|
2070
|
+
})]);
|
|
2000
2071
|
const EMPTY_SETTINGS = {
|
|
2001
2072
|
raw: {},
|
|
2002
2073
|
packages: []
|
|
2003
2074
|
};
|
|
2004
2075
|
const PACKAGE_OPERATION_TIMEOUT_MS = 5 * 6e4;
|
|
2005
2076
|
function sourceString(source) {
|
|
2006
|
-
|
|
2077
|
+
const parsed = z.string().safeParse(source);
|
|
2078
|
+
if (parsed.success) return parsed.data;
|
|
2079
|
+
return z.object({ source: z.string() }).parse(source).source;
|
|
2007
2080
|
}
|
|
2008
2081
|
function packageFilter(source) {
|
|
2009
|
-
|
|
2082
|
+
if (z.string().safeParse(source).success) return;
|
|
2083
|
+
const data = z.object({
|
|
2084
|
+
source: z.string(),
|
|
2085
|
+
autoload: z.boolean().optional(),
|
|
2086
|
+
webPanels: z.array(z.string()).optional(),
|
|
2087
|
+
terminalPresets: z.array(z.string()).optional()
|
|
2088
|
+
}).parse(source);
|
|
2089
|
+
const result = { source: data.source };
|
|
2090
|
+
if (data.autoload !== void 0) result.autoload = data.autoload;
|
|
2091
|
+
if (data.webPanels !== void 0) result.webPanels = data.webPanels;
|
|
2092
|
+
if (data.terminalPresets !== void 0) result.terminalPresets = data.terminalPresets;
|
|
2093
|
+
return result;
|
|
2010
2094
|
}
|
|
2011
2095
|
function toPosix(value) {
|
|
2012
2096
|
return value.split(path.sep).join("/");
|
|
@@ -2024,15 +2108,16 @@ function isWithin$1(candidate, root) {
|
|
|
2024
2108
|
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
2025
2109
|
}
|
|
2026
2110
|
function diagnostic(scope, message, options = {}) {
|
|
2027
|
-
|
|
2111
|
+
const result = {
|
|
2028
2112
|
severity: options.severity ?? "error",
|
|
2029
2113
|
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 }
|
|
2114
|
+
message
|
|
2035
2115
|
};
|
|
2116
|
+
if (options.source !== void 0) result.source = options.source;
|
|
2117
|
+
if (options.projectId !== void 0) result.projectId = options.projectId;
|
|
2118
|
+
if (options.resourceType !== void 0) result.resourceType = options.resourceType;
|
|
2119
|
+
if (options.path !== void 0) result.path = options.path;
|
|
2120
|
+
return result;
|
|
2036
2121
|
}
|
|
2037
2122
|
function normalizePattern(value) {
|
|
2038
2123
|
return toPosix(value.trim().replace(/^\.\//u, "").replace(/\/$/u, ""));
|
|
@@ -2119,7 +2204,7 @@ var PackageSystem = class {
|
|
|
2119
2204
|
if (scope === "global") return path.join(this.config.dataDir, "settings.json");
|
|
2120
2205
|
const project = projectId ? this.projectContexts.get(projectId) : void 0;
|
|
2121
2206
|
if (!project) throw new DomainError("PROJECT_NOT_FOUND", "Project not found for package operation", 404);
|
|
2122
|
-
return path.join(project.
|
|
2207
|
+
return path.join(project.rootPath, ".treeport", "settings.json");
|
|
2123
2208
|
}
|
|
2124
2209
|
async serialize(key, operation) {
|
|
2125
2210
|
const previous = this.operationTails.get(key) ?? Promise.resolve();
|
|
@@ -2139,85 +2224,103 @@ var PackageSystem = class {
|
|
|
2139
2224
|
async readSettingsFile(settingsPath) {
|
|
2140
2225
|
let readError;
|
|
2141
2226
|
const content = await fs.readFile(settingsPath, "utf8").catch((error) => {
|
|
2142
|
-
if (error.code === "ENOENT") return null;
|
|
2227
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
2143
2228
|
readError = error instanceof Error ? error : new Error(String(error));
|
|
2144
2229
|
return null;
|
|
2145
2230
|
});
|
|
2146
|
-
|
|
2231
|
+
const result = {
|
|
2147
2232
|
fingerprint: readError ? `error:${readError.message}` : content === null ? "missing" : crypto.createHash("sha256").update(content).digest("hex"),
|
|
2148
|
-
content
|
|
2149
|
-
...readError ? { error: readError } : {}
|
|
2233
|
+
content
|
|
2150
2234
|
};
|
|
2235
|
+
if (readError) result.error = readError;
|
|
2236
|
+
return result;
|
|
2151
2237
|
}
|
|
2152
2238
|
parseSettings(content, settingsPath, scope, projectId) {
|
|
2153
2239
|
if (content === null || content.trim() === "") return { settings: {
|
|
2154
2240
|
raw: {},
|
|
2155
2241
|
packages: []
|
|
2156
2242
|
} };
|
|
2157
|
-
let
|
|
2243
|
+
let input;
|
|
2158
2244
|
try {
|
|
2159
|
-
|
|
2245
|
+
input = JSON.parse(content);
|
|
2160
2246
|
} catch (error) {
|
|
2161
2247
|
return { diagnostic: diagnostic(scope, `Could not parse ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
2162
2248
|
projectId,
|
|
2163
2249
|
path: settingsPath
|
|
2164
2250
|
}) };
|
|
2165
2251
|
}
|
|
2166
|
-
|
|
2252
|
+
const parsedRaw = z.looseObject({
|
|
2253
|
+
npmCommand: z.unknown().optional(),
|
|
2254
|
+
packages: z.unknown().optional()
|
|
2255
|
+
}).safeParse(input);
|
|
2256
|
+
if (!parsedRaw.success) return { diagnostic: diagnostic(scope, `${settingsPath} must contain a JSON object`, {
|
|
2167
2257
|
projectId,
|
|
2168
2258
|
path: settingsPath
|
|
2169
2259
|
}) };
|
|
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`, {
|
|
2260
|
+
const parsedNpmCommand = z.array(z.string().min(1)).min(1).safeParse(parsedRaw.data.npmCommand);
|
|
2261
|
+
if (parsedRaw.data.npmCommand !== void 0 && !parsedNpmCommand.success) return { diagnostic: diagnostic(scope, `${settingsPath} npmCommand must be a non-empty argv string array`, {
|
|
2173
2262
|
projectId,
|
|
2174
2263
|
path: settingsPath
|
|
2175
2264
|
}) };
|
|
2176
|
-
|
|
2265
|
+
const parsedPackageEntries = z.array(z.unknown()).safeParse(parsedRaw.data.packages ?? []);
|
|
2266
|
+
if (!parsedPackageEntries.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages must be an array`, {
|
|
2177
2267
|
projectId,
|
|
2178
2268
|
path: settingsPath
|
|
2179
2269
|
}) };
|
|
2270
|
+
const packageObjectSchema = z.looseObject({
|
|
2271
|
+
source: z.unknown().optional(),
|
|
2272
|
+
autoload: z.unknown().optional(),
|
|
2273
|
+
webPanels: z.unknown().optional(),
|
|
2274
|
+
terminalPresets: z.unknown().optional()
|
|
2275
|
+
});
|
|
2180
2276
|
const packages = [];
|
|
2181
|
-
for (const [index, entry] of
|
|
2182
|
-
|
|
2183
|
-
|
|
2277
|
+
for (const [index, entry] of parsedPackageEntries.data.entries()) {
|
|
2278
|
+
const parsedSourceString = z.string().safeParse(entry);
|
|
2279
|
+
if (parsedSourceString.success && parsedSourceString.data.trim()) {
|
|
2280
|
+
packages.push(parsedSourceString.data);
|
|
2184
2281
|
continue;
|
|
2185
2282
|
}
|
|
2186
|
-
|
|
2283
|
+
const parsedEntry = packageObjectSchema.safeParse(entry);
|
|
2284
|
+
if (!parsedEntry.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}] must be a source string or package object`, {
|
|
2187
2285
|
projectId,
|
|
2188
2286
|
path: settingsPath
|
|
2189
2287
|
}) };
|
|
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`, {
|
|
2288
|
+
const parsedSource = z.string().safeParse(parsedEntry.data.source);
|
|
2289
|
+
if (!parsedSource.success || !parsedSource.data.trim()) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].source must be a non-empty string`, {
|
|
2195
2290
|
projectId,
|
|
2196
2291
|
path: settingsPath
|
|
2197
2292
|
}) };
|
|
2198
|
-
|
|
2293
|
+
const parsedAutoload = z.boolean().safeParse(parsedEntry.data.autoload);
|
|
2294
|
+
if (parsedEntry.data.autoload !== void 0 && !parsedAutoload.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].autoload must be a boolean`, {
|
|
2199
2295
|
projectId,
|
|
2200
2296
|
path: settingsPath
|
|
2201
2297
|
}) };
|
|
2202
|
-
|
|
2298
|
+
const parsedWebPanels = z.array(z.string()).safeParse(parsedEntry.data.webPanels);
|
|
2299
|
+
const parsedTerminalPresets = z.array(z.string()).safeParse(parsedEntry.data.terminalPresets);
|
|
2300
|
+
for (const [key, original, parsed] of [[
|
|
2301
|
+
"webPanels",
|
|
2302
|
+
parsedEntry.data.webPanels,
|
|
2303
|
+
parsedWebPanels
|
|
2304
|
+
], [
|
|
2305
|
+
"terminalPresets",
|
|
2306
|
+
parsedEntry.data.terminalPresets,
|
|
2307
|
+
parsedTerminalPresets
|
|
2308
|
+
]]) if (original !== void 0 && !parsed.success) return { diagnostic: diagnostic(scope, `${settingsPath} packages[${index}].${key} must be a string array`, {
|
|
2203
2309
|
projectId,
|
|
2204
2310
|
path: settingsPath
|
|
2205
2311
|
}) };
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
packages,
|
|
2219
|
-
...parsedNpmCommand ? { npmCommand: parsedNpmCommand } : {}
|
|
2220
|
-
} };
|
|
2312
|
+
const configured = { source: parsedSource.data };
|
|
2313
|
+
if (parsedAutoload.success) configured.autoload = parsedAutoload.data;
|
|
2314
|
+
if (parsedWebPanels.success) configured.webPanels = parsedWebPanels.data;
|
|
2315
|
+
if (parsedTerminalPresets.success) configured.terminalPresets = parsedTerminalPresets.data;
|
|
2316
|
+
packages.push(configured);
|
|
2317
|
+
}
|
|
2318
|
+
const settings = {
|
|
2319
|
+
raw: parsedRaw.data,
|
|
2320
|
+
packages
|
|
2321
|
+
};
|
|
2322
|
+
if (parsedNpmCommand.success) settings.npmCommand = parsedNpmCommand.data;
|
|
2323
|
+
return { settings };
|
|
2221
2324
|
}
|
|
2222
2325
|
async parseSource(source, settingsPath) {
|
|
2223
2326
|
const trimmed = source.trim();
|
|
@@ -2233,16 +2336,17 @@ var PackageSystem = class {
|
|
|
2233
2336
|
const version = split > 0 ? spec.slice(split + 1) : void 0;
|
|
2234
2337
|
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
2338
|
const identity = `npm:${name}`;
|
|
2236
|
-
|
|
2339
|
+
const parsed = {
|
|
2237
2340
|
type: "npm",
|
|
2238
2341
|
source: trimmed,
|
|
2239
2342
|
spec,
|
|
2240
2343
|
name,
|
|
2241
|
-
...version === void 0 ? {} : { version },
|
|
2242
2344
|
exact: isExactNpmVersion(version),
|
|
2243
2345
|
identity,
|
|
2244
2346
|
packageId: identity
|
|
2245
2347
|
};
|
|
2348
|
+
if (version !== void 0) parsed.version = version;
|
|
2349
|
+
return parsed;
|
|
2246
2350
|
}
|
|
2247
2351
|
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
2352
|
const expanded = trimmed === "~" || trimmed.startsWith("~/") ? path.join(os.homedir(), trimmed.slice(2)) : trimmed;
|
|
@@ -2257,7 +2361,7 @@ var PackageSystem = class {
|
|
|
2257
2361
|
};
|
|
2258
2362
|
}
|
|
2259
2363
|
npmRoot(scope, projectId) {
|
|
2260
|
-
return scope === "global" ? path.join(this.config.dataDir, "npm") : path.join(this.projectContexts.get(projectId).
|
|
2364
|
+
return scope === "global" ? path.join(this.config.dataDir, "npm") : path.join(this.projectContexts.get(projectId).rootPath, ".treeport", "npm");
|
|
2261
2365
|
}
|
|
2262
2366
|
npmPackagePath(source, scope, projectId) {
|
|
2263
2367
|
return path.join(this.npmRoot(scope, projectId), "node_modules", source.name);
|
|
@@ -2346,40 +2450,47 @@ var PackageSystem = class {
|
|
|
2346
2450
|
const installedPath = this.npmPackagePath(source, scope, projectId);
|
|
2347
2451
|
let shouldInstall = forceInstall;
|
|
2348
2452
|
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) =>
|
|
2453
|
+
else if (source.exact) shouldInstall = await fs.readFile(path.join(installedPath, "package.json"), "utf8").then((content) => {
|
|
2454
|
+
const parsed = z.object({ version: z.string().optional() }).safeParse(JSON.parse(content));
|
|
2455
|
+
return parsed.success ? parsed.data.version : void 0;
|
|
2456
|
+
}).catch(() => void 0) !== source.version;
|
|
2350
2457
|
if (shouldInstall) await this.runNpm("install", source, scope, projectId, settings);
|
|
2351
2458
|
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
2459
|
return installedPath;
|
|
2353
2460
|
}
|
|
2354
2461
|
validateManifestPatterns(patterns, field, packageJsonPath) {
|
|
2355
|
-
|
|
2356
|
-
|
|
2462
|
+
const parsed = manifestPatternsSchema.safeParse(patterns);
|
|
2463
|
+
if (!parsed.success) throw new Error(`${packageJsonPath} treeport.${field} must be a string array`);
|
|
2464
|
+
for (const pattern of parsed.data) {
|
|
2357
2465
|
const normalized = normalizePattern(pattern.startsWith("!") ? pattern.slice(1) : pattern);
|
|
2358
2466
|
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
2467
|
}
|
|
2360
|
-
return [...
|
|
2468
|
+
return [...parsed.data];
|
|
2361
2469
|
}
|
|
2362
2470
|
validateWebPanelManifest(entries, packageJsonPath) {
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2471
|
+
const parsedEntries = z.array(z.unknown()).safeParse(entries);
|
|
2472
|
+
if (!parsedEntries.success) throw new Error(`${packageJsonPath} treeport.webPanels must be an array`);
|
|
2473
|
+
return parsedEntries.data.map((entry) => {
|
|
2474
|
+
const parsed = webPanelManifestEntrySchema.safeParse(entry);
|
|
2475
|
+
if (!parsed.success) throw new Error(`${packageJsonPath} contains an invalid web panel definition`);
|
|
2476
|
+
const parsedSource = z.string().safeParse(parsed.data);
|
|
2477
|
+
if (parsedSource.success) {
|
|
2478
|
+
this.validateManifestPatterns([parsedSource.data], "webPanels", packageJsonPath);
|
|
2367
2479
|
return {
|
|
2368
|
-
source:
|
|
2480
|
+
source: parsedSource.data,
|
|
2369
2481
|
allowSameOrigin: false
|
|
2370
2482
|
};
|
|
2371
2483
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2484
|
+
const { source, permissions = [] } = z.strictObject({
|
|
2485
|
+
source: z.string(),
|
|
2486
|
+
permissions: z.array(z.enum(["same-origin"])).optional()
|
|
2487
|
+
}).parse(parsed.data);
|
|
2488
|
+
if (source.startsWith("!")) throw new Error(`${packageJsonPath} contains an invalid web panel definition`);
|
|
2377
2489
|
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`);
|
|
2490
|
+
if (new Set(permissions).size !== permissions.length) throw new Error(`${packageJsonPath} contains an invalid web panel permission`);
|
|
2380
2491
|
return {
|
|
2381
2492
|
source,
|
|
2382
|
-
allowSameOrigin:
|
|
2493
|
+
allowSameOrigin: permissions.includes("same-origin")
|
|
2383
2494
|
};
|
|
2384
2495
|
});
|
|
2385
2496
|
}
|
|
@@ -2469,7 +2580,7 @@ var PackageSystem = class {
|
|
|
2469
2580
|
const source = sourceString(configured);
|
|
2470
2581
|
const packageJsonPath = path.join(root, "package.json");
|
|
2471
2582
|
const packageJsonContent = await fs.readFile(packageJsonPath, "utf8").catch((error) => {
|
|
2472
|
-
if (error.code === "ENOENT") return null;
|
|
2583
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
2473
2584
|
throw error;
|
|
2474
2585
|
});
|
|
2475
2586
|
let manifest;
|
|
@@ -2480,15 +2591,17 @@ var PackageSystem = class {
|
|
|
2480
2591
|
} catch (error) {
|
|
2481
2592
|
throw new Error(`Could not parse ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2482
2593
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
if (treeport !== void 0) {
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2594
|
+
const parsedPackageJson = z.looseObject({ treeport: z.unknown().optional() }).safeParse(packageJson);
|
|
2595
|
+
if (!parsedPackageJson.success) throw new Error(`${packageJsonPath} must contain a JSON object`);
|
|
2596
|
+
if (parsedPackageJson.data.treeport !== void 0) {
|
|
2597
|
+
const parsedTreeport = z.looseObject({
|
|
2598
|
+
webPanels: z.unknown().optional(),
|
|
2599
|
+
terminalPresets: z.unknown().optional()
|
|
2600
|
+
}).safeParse(parsedPackageJson.data.treeport);
|
|
2601
|
+
if (!parsedTreeport.success) throw new Error(`${packageJsonPath} treeport manifest must be an object`);
|
|
2489
2602
|
manifest = {
|
|
2490
|
-
webPanels: this.validateWebPanelManifest(webPanels ?? [], packageJsonPath),
|
|
2491
|
-
terminalPresets: this.validateManifestPatterns(terminalPresets ?? [], "terminalPresets", packageJsonPath)
|
|
2603
|
+
webPanels: this.validateWebPanelManifest(parsedTreeport.data.webPanels ?? [], packageJsonPath),
|
|
2604
|
+
terminalPresets: this.validateManifestPatterns(parsedTreeport.data.terminalPresets ?? [], "terminalPresets", packageJsonPath)
|
|
2492
2605
|
};
|
|
2493
2606
|
}
|
|
2494
2607
|
}
|
|
@@ -2516,7 +2629,7 @@ var PackageSystem = class {
|
|
|
2516
2629
|
const webPanels = applyNormalFilter(panelCandidates.map((candidate) => {
|
|
2517
2630
|
const resourceId = encodeURIComponent(path.posix.basename(candidate.relativePath));
|
|
2518
2631
|
const allowSameOrigin = manifest?.webPanels.some((entry) => entry.allowSameOrigin && this.manifestAllows(candidate.relativePath, [entry.source], "web-panel")) ?? false;
|
|
2519
|
-
|
|
2632
|
+
const resolved = {
|
|
2520
2633
|
definition: {
|
|
2521
2634
|
id: `package:${parsed.packageId}:web-panel:${resourceId}`,
|
|
2522
2635
|
title: titleFromPath(candidate.relativePath),
|
|
@@ -2527,10 +2640,11 @@ var PackageSystem = class {
|
|
|
2527
2640
|
entry: "index.html",
|
|
2528
2641
|
packageRoot: root,
|
|
2529
2642
|
development: parsed.type === "local",
|
|
2530
|
-
...packageLockPath ? { packageLockPath } : {},
|
|
2531
2643
|
relativePath: candidate.relativePath,
|
|
2532
2644
|
enabled: true
|
|
2533
2645
|
};
|
|
2646
|
+
if (packageLockPath) resolved.packageLockPath = packageLockPath;
|
|
2647
|
+
return resolved;
|
|
2534
2648
|
}), filter?.webPanels, autoload);
|
|
2535
2649
|
const terminalPresets = [];
|
|
2536
2650
|
for (const candidate of presetCandidates) {
|
|
@@ -2563,6 +2677,7 @@ var PackageSystem = class {
|
|
|
2563
2677
|
name: result.data.name,
|
|
2564
2678
|
executable: result.data.executable,
|
|
2565
2679
|
args: [...result.data.args],
|
|
2680
|
+
shellCommand: null,
|
|
2566
2681
|
cwd: null,
|
|
2567
2682
|
env: {},
|
|
2568
2683
|
closeOnSuccess: result.data.closeOnSuccess,
|
|
@@ -2813,7 +2928,7 @@ var PackageSystem = class {
|
|
|
2813
2928
|
return {
|
|
2814
2929
|
id: project.id,
|
|
2815
2930
|
name: project.name,
|
|
2816
|
-
|
|
2931
|
+
rootPath: project.rootPath
|
|
2817
2932
|
};
|
|
2818
2933
|
}
|
|
2819
2934
|
syncProjects(projects) {
|
|
@@ -2821,7 +2936,7 @@ var PackageSystem = class {
|
|
|
2821
2936
|
const next = this.context(project);
|
|
2822
2937
|
const previous = this.projectContexts.get(project.id);
|
|
2823
2938
|
this.projectContexts.set(project.id, next);
|
|
2824
|
-
if (previous && (previous.
|
|
2939
|
+
if (previous && (previous.rootPath !== next.rootPath || previous.name !== next.name)) this.projectFingerprints.delete(project.id);
|
|
2825
2940
|
}
|
|
2826
2941
|
}
|
|
2827
2942
|
async initialize(projects) {
|
|
@@ -2931,10 +3046,11 @@ var PackageSystem = class {
|
|
|
2931
3046
|
const next = [];
|
|
2932
3047
|
for (const configured of settings.packages) if ((await this.parseSource(sourceString(configured), settingsPath)).identity !== parsed.identity) next.push(configured);
|
|
2933
3048
|
else if (!replaced) {
|
|
2934
|
-
|
|
2935
|
-
|
|
3049
|
+
const filter = packageFilter(configured);
|
|
3050
|
+
next.push(filter ? {
|
|
3051
|
+
...filter,
|
|
2936
3052
|
source: persisted
|
|
2937
|
-
});
|
|
3053
|
+
} : persisted);
|
|
2938
3054
|
replaced = true;
|
|
2939
3055
|
}
|
|
2940
3056
|
if (!replaced) next.push(persisted);
|
|
@@ -3090,7 +3206,7 @@ const CONFIG_PATH = path.join(".treeport", "terminal-presets.json");
|
|
|
3090
3206
|
async function loadRepositoryTerminalPresets(projectId, worktreePath) {
|
|
3091
3207
|
const configPath = path.join(worktreePath, CONFIG_PATH);
|
|
3092
3208
|
const content = await fs.readFile(configPath, "utf8").catch((error) => {
|
|
3093
|
-
if (
|
|
3209
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
3094
3210
|
return error instanceof Error ? error : new Error(String(error));
|
|
3095
3211
|
});
|
|
3096
3212
|
if (content === null) return {
|
|
@@ -3144,6 +3260,7 @@ async function loadRepositoryTerminalPresets(projectId, worktreePath) {
|
|
|
3144
3260
|
name: preset.data.name,
|
|
3145
3261
|
executable: preset.data.executable,
|
|
3146
3262
|
args: [...preset.data.args],
|
|
3263
|
+
shellCommand: null,
|
|
3147
3264
|
cwd: null,
|
|
3148
3265
|
env: {},
|
|
3149
3266
|
closeOnSuccess: preset.data.closeOnSuccess,
|
|
@@ -3163,7 +3280,7 @@ async function loadRepositoryTerminalPresets(projectId, worktreePath) {
|
|
|
3163
3280
|
const DEFAULT_SETUP_TIMEOUT_MS = 30 * 6e4;
|
|
3164
3281
|
const MAX_SETUP_OUTPUT = 4e3;
|
|
3165
3282
|
const TREEPORT_SETUP_PATH = path.join(".treeport", "setup.json");
|
|
3166
|
-
const
|
|
3283
|
+
const TREEPORT_PATH_VARIABLE_NAMES = /* @__PURE__ */ new Set(["TREEPORT_WORKTREE_PATH", "TREEPORT_MAIN_WORKTREE_PATH"]);
|
|
3167
3284
|
const environmentSchema = z.record(z.string(), z.string()).superRefine((environment, context) => {
|
|
3168
3285
|
for (const [name, value] of Object.entries(environment)) {
|
|
3169
3286
|
if (!name || name.includes("=") || name.includes("\0")) context.addIssue({
|
|
@@ -3171,7 +3288,7 @@ const environmentSchema = z.record(z.string(), z.string()).superRefine((environm
|
|
|
3171
3288
|
path: [name],
|
|
3172
3289
|
message: "Environment names must be non-empty and cannot contain = or NUL"
|
|
3173
3290
|
});
|
|
3174
|
-
if (
|
|
3291
|
+
if (TREEPORT_PATH_VARIABLE_NAMES.has(name)) context.addIssue({
|
|
3175
3292
|
code: "custom",
|
|
3176
3293
|
path: [name],
|
|
3177
3294
|
message: `${name} is reserved by Treeport`
|
|
@@ -3207,7 +3324,8 @@ const setupFileSchema = z.object({
|
|
|
3207
3324
|
function formatIssuePath(issuePath) {
|
|
3208
3325
|
if (!issuePath.length) return "configuration";
|
|
3209
3326
|
return issuePath.reduce((formatted, component) => {
|
|
3210
|
-
|
|
3327
|
+
const parsedIndex = z.number().safeParse(component);
|
|
3328
|
+
if (parsedIndex.success) return `${formatted}[${parsedIndex.data}]`;
|
|
3211
3329
|
return formatted ? `${formatted}.${String(component)}` : String(component);
|
|
3212
3330
|
}, "");
|
|
3213
3331
|
}
|
|
@@ -3239,7 +3357,7 @@ async function resolveWorktreeSetupTasks(input) {
|
|
|
3239
3357
|
return parsed.data.commands.map((command, index) => {
|
|
3240
3358
|
const expandedCwd = expandTreeportPaths(command.cwd ?? worktreePath, environment);
|
|
3241
3359
|
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
|
|
3360
|
+
if (!isPathWithin$1(cwd, worktreePath)) throw new Error(`Invalid Treeport setup in ${filePath}: commands[${index}].cwd must stay inside the new tree`);
|
|
3243
3361
|
const configuredEnvironment = Object.fromEntries(Object.entries(command.env ?? {}).map(([name, value]) => [name, expandTreeportPaths(value, environment)]));
|
|
3244
3362
|
return {
|
|
3245
3363
|
label: command.name,
|
|
@@ -3368,6 +3486,30 @@ var WebPanelViteRuntime = class {
|
|
|
3368
3486
|
this.httpServer = server;
|
|
3369
3487
|
}
|
|
3370
3488
|
viteConfig(source, base, options = {}) {
|
|
3489
|
+
const server = {
|
|
3490
|
+
middlewareMode: true,
|
|
3491
|
+
headers: {
|
|
3492
|
+
"access-control-allow-origin": "*",
|
|
3493
|
+
"cache-control": "no-store",
|
|
3494
|
+
"x-content-type-options": "nosniff"
|
|
3495
|
+
},
|
|
3496
|
+
fs: {
|
|
3497
|
+
strict: true,
|
|
3498
|
+
allow: [source.packageRoot, PANEL_SDK_ROOT]
|
|
3499
|
+
}
|
|
3500
|
+
};
|
|
3501
|
+
if (options.server) server.hmr = {
|
|
3502
|
+
server: options.server,
|
|
3503
|
+
path: `${base}@vite-hmr`
|
|
3504
|
+
};
|
|
3505
|
+
const viteBuild = {
|
|
3506
|
+
sourcemap: true,
|
|
3507
|
+
rollupOptions: { input: path.join(source.root, source.entry) }
|
|
3508
|
+
};
|
|
3509
|
+
if (options.outDir) {
|
|
3510
|
+
viteBuild.outDir = options.outDir;
|
|
3511
|
+
viteBuild.emptyOutDir = true;
|
|
3512
|
+
}
|
|
3371
3513
|
return {
|
|
3372
3514
|
root: source.root,
|
|
3373
3515
|
base,
|
|
@@ -3383,30 +3525,8 @@ var WebPanelViteRuntime = class {
|
|
|
3383
3525
|
alias: { "@treeport/panel-sdk": PANEL_SDK_ENTRY },
|
|
3384
3526
|
dedupe: ["react", "react-dom"]
|
|
3385
3527
|
},
|
|
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
|
-
}
|
|
3528
|
+
server,
|
|
3529
|
+
build: viteBuild
|
|
3410
3530
|
};
|
|
3411
3531
|
}
|
|
3412
3532
|
async hashSource(source) {
|
|
@@ -3456,7 +3576,9 @@ var WebPanelViteRuntime = class {
|
|
|
3456
3576
|
const parent = path.join(this.config.cacheDir, "web-panels", COMPILER_ABI);
|
|
3457
3577
|
const directory = path.join(parent, hash);
|
|
3458
3578
|
const metadata = path.join(directory, BUILD_METADATA);
|
|
3459
|
-
if (await fs.readFile(metadata, "utf8").then((value) =>
|
|
3579
|
+
if (await fs.readFile(metadata, "utf8").then((value) => {
|
|
3580
|
+
return JSON.parse(value);
|
|
3581
|
+
}).then((value) => value.hash === hash).catch(() => false)) return {
|
|
3460
3582
|
hash,
|
|
3461
3583
|
directory
|
|
3462
3584
|
};
|
|
@@ -3502,8 +3624,8 @@ var WebPanelViteRuntime = class {
|
|
|
3502
3624
|
directory: await pending
|
|
3503
3625
|
};
|
|
3504
3626
|
}
|
|
3505
|
-
errorPage(source,
|
|
3506
|
-
const raw =
|
|
3627
|
+
errorPage(source, cause) {
|
|
3628
|
+
const raw = cause instanceof Error ? cause.message : String(cause);
|
|
3507
3629
|
const diagnostic = raw.replaceAll(source.packageRoot, "<package>").replaceAll(source.root, "<panel>");
|
|
3508
3630
|
const stage = /resolve|not found|cannot find|failed to load|import/iu.test(raw) ? "Dependency resolution" : "Source transformation";
|
|
3509
3631
|
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 +3764,13 @@ var KeyedTaskQueue = class {
|
|
|
3642
3764
|
}
|
|
3643
3765
|
const result = new Promise((resolve, reject) => {
|
|
3644
3766
|
state.pending += 1;
|
|
3645
|
-
Effect.runSync(Queue.offer(state.queue, {
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3767
|
+
Effect.runSync(Queue.offer(state.queue, { run: async () => {
|
|
3768
|
+
try {
|
|
3769
|
+
resolve(await task());
|
|
3770
|
+
} catch (error) {
|
|
3771
|
+
reject(error);
|
|
3772
|
+
}
|
|
3773
|
+
} }));
|
|
3650
3774
|
});
|
|
3651
3775
|
if (!state.running) {
|
|
3652
3776
|
state.running = true;
|
|
@@ -3662,14 +3786,8 @@ var KeyedTaskQueue = class {
|
|
|
3662
3786
|
}
|
|
3663
3787
|
async run(key, state) {
|
|
3664
3788
|
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
|
-
}
|
|
3789
|
+
await (await Effect.runPromise(Queue.take(state.queue))).run();
|
|
3790
|
+
state.pending -= 1;
|
|
3673
3791
|
}
|
|
3674
3792
|
state.running = false;
|
|
3675
3793
|
if (state.pending > 0) {
|
|
@@ -3689,9 +3807,9 @@ const encodeMetadata = (value) => Buffer.from(JSON.stringify(value), "utf8").toS
|
|
|
3689
3807
|
function isAbsentTmuxServer(stderr) {
|
|
3690
3808
|
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
3809
|
}
|
|
3692
|
-
function decodeMetadata(value) {
|
|
3810
|
+
function decodeMetadata(value, schema) {
|
|
3693
3811
|
if (!value) return;
|
|
3694
|
-
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
3812
|
+
return schema.parse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
|
|
3695
3813
|
}
|
|
3696
3814
|
const TMUX_SCROLL_EXIT_SEQUENCE = TERMINAL_SCROLL_EXIT_SEQUENCE;
|
|
3697
3815
|
const TMUX_SELECTION_CLEAR_SEQUENCE = TERMINAL_SELECTION_CLEAR_SEQUENCE;
|
|
@@ -3740,8 +3858,10 @@ bind-key -T copy-mode-vi User4 select-pane -t .
|
|
|
3740
3858
|
bind-key -T copy-mode MouseDragEnd1Pane send-keys -X stop-selection
|
|
3741
3859
|
bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X stop-selection
|
|
3742
3860
|
# 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
|
|
3861
|
+
bind-key -T copy-mode WheelUpPane { select-pane ; send-keys -X -N 1 scroll-up }
|
|
3862
|
+
bind-key -T copy-mode-vi WheelUpPane { select-pane ; send-keys -X -N 1 scroll-up }
|
|
3863
|
+
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 } }
|
|
3864
|
+
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
3865
|
bind-key -T root WheelUpPane if-shell -F '#{||:#{alternate_on},#{pane_in_mode},#{mouse_any_flag}}' { send-keys -M } { copy-mode -H }
|
|
3746
3866
|
`;
|
|
3747
3867
|
var TmuxAdapter = class {
|
|
@@ -3757,6 +3877,7 @@ var TmuxAdapter = class {
|
|
|
3757
3877
|
uid;
|
|
3758
3878
|
creationTails = /* @__PURE__ */ new Map();
|
|
3759
3879
|
configuredSockets = /* @__PURE__ */ new Set();
|
|
3880
|
+
socketConfigurationPromises = /* @__PURE__ */ new Map();
|
|
3760
3881
|
initializationPromise = null;
|
|
3761
3882
|
sshAuthSockPromise = null;
|
|
3762
3883
|
constructor(runner, runtimeDir, executable = "tmux", launcherPath, host = {}) {
|
|
@@ -3846,25 +3967,22 @@ var TmuxAdapter = class {
|
|
|
3846
3967
|
});
|
|
3847
3968
|
await this.configureServer(input.socketName);
|
|
3848
3969
|
}
|
|
3970
|
+
const environment = { ...input.env };
|
|
3971
|
+
if (sshAuthSock) environment.SSH_AUTH_SOCK = sshAuthSock;
|
|
3849
3972
|
const spec = {
|
|
3850
3973
|
argv: [...input.argv],
|
|
3851
|
-
...input.fallbackArgv ? { fallbackArgv: [...input.fallbackArgv] } : {},
|
|
3852
3974
|
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 } : {}
|
|
3975
|
+
env: environment
|
|
3867
3976
|
};
|
|
3977
|
+
if (input.fallbackArgv) spec.fallbackArgv = [...input.fallbackArgv];
|
|
3978
|
+
spec.tmuxExecutable = resolveExecutablePath(this.executable, this.hostEnvironment);
|
|
3979
|
+
if (shellIntegrationReady) spec.shellIntegrationDir = this.shellIntegrationDir;
|
|
3980
|
+
if (input.setupTasks?.length) spec.setupTasks = input.setupTasks.map((task) => ({
|
|
3981
|
+
...task,
|
|
3982
|
+
argv: [...task.argv],
|
|
3983
|
+
env: { ...task.env }
|
|
3984
|
+
}));
|
|
3985
|
+
if (input.setupError) spec.setupError = input.setupError;
|
|
3868
3986
|
await fs.writeFile(specPath, JSON.stringify(spec), { mode: 384 });
|
|
3869
3987
|
wroteSpec = true;
|
|
3870
3988
|
await runChecked(this.runner, {
|
|
@@ -3897,6 +4015,8 @@ var TmuxAdapter = class {
|
|
|
3897
4015
|
worktreeId: input.worktreeId,
|
|
3898
4016
|
name: input.name,
|
|
3899
4017
|
argv: input.argv,
|
|
4018
|
+
shellCommand: input.shellCommand,
|
|
4019
|
+
interactiveShell: input.interactiveShell,
|
|
3900
4020
|
closeOnSuccess: input.closeOnSuccess ?? false,
|
|
3901
4021
|
createdAt: input.createdAt,
|
|
3902
4022
|
updatedAt: input.createdAt
|
|
@@ -3924,7 +4044,10 @@ var TmuxAdapter = class {
|
|
|
3924
4044
|
}
|
|
3925
4045
|
}
|
|
3926
4046
|
async configureServer(socketName) {
|
|
3927
|
-
|
|
4047
|
+
if (this.configuredSockets.has(socketName)) return;
|
|
4048
|
+
const existing = this.socketConfigurationPromises.get(socketName);
|
|
4049
|
+
if (existing) return existing;
|
|
4050
|
+
const configuring = runChecked(this.runner, {
|
|
3928
4051
|
executable: this.executable,
|
|
3929
4052
|
args: [
|
|
3930
4053
|
...this.base(socketName),
|
|
@@ -3933,13 +4056,20 @@ var TmuxAdapter = class {
|
|
|
3933
4056
|
],
|
|
3934
4057
|
env: this.environment(),
|
|
3935
4058
|
timeoutMs: 1e4
|
|
4059
|
+
}).then(() => {
|
|
4060
|
+
this.configuredSockets.add(socketName);
|
|
4061
|
+
});
|
|
4062
|
+
this.socketConfigurationPromises.set(socketName, configuring);
|
|
4063
|
+
return configuring.finally(() => {
|
|
4064
|
+
if (this.socketConfigurationPromises.get(socketName) === configuring) this.socketConfigurationPromises.delete(socketName);
|
|
3936
4065
|
});
|
|
3937
|
-
this.configuredSockets.add(socketName);
|
|
3938
4066
|
}
|
|
3939
4067
|
async configureSession(socketName, sessionName, metadata) {
|
|
3940
4068
|
const values = [
|
|
3941
4069
|
["@treeport-name", encodeMetadata(metadata.name)],
|
|
3942
4070
|
["@treeport-argv", encodeMetadata(metadata.argv)],
|
|
4071
|
+
["@treeport-shell-command", encodeMetadata(metadata.shellCommand)],
|
|
4072
|
+
["@treeport-interactive-shell", metadata.interactiveShell ? "1" : "0"],
|
|
3943
4073
|
["@treeport-close-on-success", metadata.closeOnSuccess ? "1" : "0"],
|
|
3944
4074
|
["@treeport-created-at", encodeMetadata(metadata.createdAt)],
|
|
3945
4075
|
["@treeport-updated-at", encodeMetadata(metadata.updatedAt)],
|
|
@@ -3992,7 +4122,7 @@ var TmuxAdapter = class {
|
|
|
3992
4122
|
"list-panes",
|
|
3993
4123
|
"-a",
|
|
3994
4124
|
"-F",
|
|
3995
|
-
"#{session_name} #{@treeport-terminal-id} #{@treeport-worktree-id} #{@treeport-name} #{@treeport-argv} #{@treeport-close-on-success} #{@treeport-created-at} #{@treeport-updated-at} #{session_created} #{pane_dead} #{pane_dead_status}"
|
|
4125
|
+
"#{session_name} #{@treeport-terminal-id} #{@treeport-worktree-id} #{@treeport-name} #{@treeport-argv} #{@treeport-shell-command} #{@treeport-interactive-shell} #{@treeport-close-on-success} #{@treeport-created-at} #{@treeport-updated-at} #{session_created} #{pane_dead} #{pane_dead_status}"
|
|
3996
4126
|
],
|
|
3997
4127
|
env: this.environment(),
|
|
3998
4128
|
timeoutMs: 1e4
|
|
@@ -4004,26 +4134,24 @@ var TmuxAdapter = class {
|
|
|
4004
4134
|
const sessions = /* @__PURE__ */ new Map();
|
|
4005
4135
|
for (const line of result.stdout.split("\n")) {
|
|
4006
4136
|
if (!line) continue;
|
|
4007
|
-
const [sessionName, terminalId, worktreeId, encodedName, encodedArgv, closeOnSuccess, encodedCreatedAt, encodedUpdatedAt, sessionCreated, paneDead, paneDeadStatus] = line.split(" ");
|
|
4137
|
+
const [sessionName, terminalId, worktreeId, encodedName, encodedArgv, encodedShellCommand, encodedInteractiveShell, closeOnSuccess, encodedCreatedAt, encodedUpdatedAt, sessionCreated, paneDead, paneDeadStatus] = line.split(" ");
|
|
4008
4138
|
if (!sessionName || sessions.has(sessionName) || !terminalId || !worktreeId) continue;
|
|
4009
4139
|
let metadata;
|
|
4010
4140
|
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
4141
|
metadata = {
|
|
4017
4142
|
terminalId,
|
|
4018
4143
|
worktreeId,
|
|
4019
|
-
name,
|
|
4020
|
-
argv,
|
|
4021
|
-
|
|
4022
|
-
|
|
4144
|
+
name: decodeMetadata(encodedName ?? "", z.string()),
|
|
4145
|
+
argv: decodeMetadata(encodedArgv ?? "", z.array(z.string())),
|
|
4146
|
+
shellCommand: decodeMetadata(encodedShellCommand ?? "", z.string().nullable()),
|
|
4147
|
+
createdAt: decodeMetadata(encodedCreatedAt ?? "", z.string()),
|
|
4148
|
+
updatedAt: decodeMetadata(encodedUpdatedAt ?? "", z.string())
|
|
4023
4149
|
};
|
|
4024
4150
|
} catch {
|
|
4025
4151
|
continue;
|
|
4026
4152
|
}
|
|
4153
|
+
const interactiveShell = encodedInteractiveShell === "1" ? true : encodedInteractiveShell === "0" ? false : void 0;
|
|
4154
|
+
if (metadata.shellCommand === void 0 || interactiveShell === void 0) continue;
|
|
4027
4155
|
const fallbackCreatedAt = metadata.createdAt ?? (/* @__PURE__ */ new Date(Number(sessionCreated) * 1e3)).toISOString();
|
|
4028
4156
|
const dead = paneDead === "1";
|
|
4029
4157
|
const exitCode = dead && paneDeadStatus ? Number.parseInt(paneDeadStatus, 10) : null;
|
|
@@ -4033,6 +4161,8 @@ var TmuxAdapter = class {
|
|
|
4033
4161
|
name: metadata.name ?? sessionName,
|
|
4034
4162
|
sessionName,
|
|
4035
4163
|
argv: metadata.argv ?? [],
|
|
4164
|
+
shellCommand: metadata.shellCommand,
|
|
4165
|
+
interactiveShell,
|
|
4036
4166
|
closeOnSuccess: closeOnSuccess === "1",
|
|
4037
4167
|
status: dead ? "exited" : "running",
|
|
4038
4168
|
exitCode: Number.isNaN(exitCode) ? null : exitCode,
|
|
@@ -4185,7 +4315,7 @@ var TmuxAdapter = class {
|
|
|
4185
4315
|
"-p",
|
|
4186
4316
|
"-t",
|
|
4187
4317
|
sessionName,
|
|
4188
|
-
"#{@treeport-shell-title} #{pane_current_command} #{@treeport-command} #{pane_title}"
|
|
4318
|
+
"#{@treeport-fallback-shell} #{@treeport-shell-title} #{pane_current_command} #{@treeport-command} #{pane_title}"
|
|
4189
4319
|
],
|
|
4190
4320
|
env: this.environment(),
|
|
4191
4321
|
timeoutMs: 1e4
|
|
@@ -4194,20 +4324,22 @@ var TmuxAdapter = class {
|
|
|
4194
4324
|
const firstSeparator = result.stdout.indexOf(" ");
|
|
4195
4325
|
const secondSeparator = result.stdout.indexOf(" ", firstSeparator + 1);
|
|
4196
4326
|
const thirdSeparator = result.stdout.indexOf(" ", secondSeparator + 1);
|
|
4197
|
-
|
|
4198
|
-
|
|
4327
|
+
const fourthSeparator = result.stdout.indexOf(" ", thirdSeparator + 1);
|
|
4328
|
+
if (firstSeparator === -1 || secondSeparator === -1 || thirdSeparator === -1 || fourthSeparator === -1) return null;
|
|
4329
|
+
let fallbackShell = null;
|
|
4199
4330
|
let shellTitle = null;
|
|
4200
4331
|
try {
|
|
4201
|
-
|
|
4202
|
-
shellTitle =
|
|
4332
|
+
fallbackShell = decodeMetadata(result.stdout.slice(0, firstSeparator).trim(), z.string()) ?? null;
|
|
4333
|
+
shellTitle = decodeMetadata(result.stdout.slice(firstSeparator + 1, secondSeparator).trim(), z.string()) ?? null;
|
|
4203
4334
|
} catch {}
|
|
4204
|
-
const currentCommand = result.stdout.slice(
|
|
4205
|
-
const commandLine = result.stdout.slice(
|
|
4335
|
+
const currentCommand = result.stdout.slice(secondSeparator + 1, thirdSeparator).trim() || null;
|
|
4336
|
+
const commandLine = result.stdout.slice(thirdSeparator + 1, fourthSeparator).trim() || null;
|
|
4206
4337
|
return {
|
|
4207
|
-
paneTitle: result.stdout.slice(
|
|
4338
|
+
paneTitle: result.stdout.slice(fourthSeparator + 1).trim() || null,
|
|
4208
4339
|
currentCommand,
|
|
4209
4340
|
commandLine,
|
|
4210
|
-
shellTitle
|
|
4341
|
+
shellTitle,
|
|
4342
|
+
fallbackShell
|
|
4211
4343
|
};
|
|
4212
4344
|
}
|
|
4213
4345
|
async setSessionShellTitle(socketName, sessionName, title) {
|
|
@@ -4268,7 +4400,7 @@ var TmuxAdapter = class {
|
|
|
4268
4400
|
env: this.environment(),
|
|
4269
4401
|
timeoutMs: 15e3
|
|
4270
4402
|
});
|
|
4271
|
-
if (result.exitCode !== 0 && !isAbsentTmuxServer(result.stderr)) throw new Error(result.stderr.trim() || "Failed to kill
|
|
4403
|
+
if (result.exitCode !== 0 && !isAbsentTmuxServer(result.stderr)) throw new Error(result.stderr.trim() || "Failed to kill tree tmux server");
|
|
4272
4404
|
await Promise.all(terminalIds.map((terminalId) => fs.unlink(path.join(this.specsDir, `${terminalId}.json`)).catch(() => void 0)));
|
|
4273
4405
|
this.configuredSockets.delete(socketName);
|
|
4274
4406
|
return terminalIds;
|
|
@@ -4282,8 +4414,8 @@ const WEB_PANEL_STORAGE_MAX_ENTRIES = 256;
|
|
|
4282
4414
|
const WEB_PANEL_STORAGE_MAX_TOTAL_BYTES = 1024 * 1024;
|
|
4283
4415
|
const WEB_PANEL_STORAGE_MAX_VALUE_BYTES = 64 * 1024;
|
|
4284
4416
|
function mapWebPanel(row, allowSameOrigin = false) {
|
|
4285
|
-
const
|
|
4286
|
-
if (
|
|
4417
|
+
const parsedInput = webPanelInputSchema.nullable().safeParse(JSON.parse(row.inputJson));
|
|
4418
|
+
if (!parsedInput.success) throw new Error(`Web panel ${row.id} has invalid stored launch input`);
|
|
4287
4419
|
return {
|
|
4288
4420
|
id: row.id,
|
|
4289
4421
|
kind: "web",
|
|
@@ -4291,7 +4423,7 @@ function mapWebPanel(row, allowSameOrigin = false) {
|
|
|
4291
4423
|
definitionId: row.definitionId,
|
|
4292
4424
|
title: row.title,
|
|
4293
4425
|
launch: {
|
|
4294
|
-
input,
|
|
4426
|
+
input: parsedInput.data,
|
|
4295
4427
|
cwd: row.launchCwd
|
|
4296
4428
|
},
|
|
4297
4429
|
sandbox: { allowSameOrigin },
|
|
@@ -4341,6 +4473,7 @@ var TreeportService = class {
|
|
|
4341
4473
|
closeOnSuccessTerminalIds = /* @__PURE__ */ new Set();
|
|
4342
4474
|
terminalIdsByWorktree = /* @__PURE__ */ new Map();
|
|
4343
4475
|
projectObservationTails = /* @__PURE__ */ new Map();
|
|
4476
|
+
observedFolderIdentities = /* @__PURE__ */ new Map();
|
|
4344
4477
|
projectsSnapshotInFlight = null;
|
|
4345
4478
|
projectsSnapshotRevision = 0;
|
|
4346
4479
|
packages;
|
|
@@ -4404,7 +4537,7 @@ var TreeportService = class {
|
|
|
4404
4537
|
await tx.run(sql`
|
|
4405
4538
|
UPDATE operations
|
|
4406
4539
|
SET status = 'failed',
|
|
4407
|
-
error = ${operation.kind === "create" ? "Daemon restarted before
|
|
4540
|
+
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
4541
|
updated_at = ${timestamp}
|
|
4409
4542
|
WHERE id = ${operation.id}
|
|
4410
4543
|
`);
|
|
@@ -4538,9 +4671,11 @@ var TreeportService = class {
|
|
|
4538
4671
|
return this.deps.database.db.select({
|
|
4539
4672
|
id: projects.id,
|
|
4540
4673
|
name: projects.name,
|
|
4674
|
+
kind: projects.kind,
|
|
4675
|
+
rootPath: projects.repositoryPath,
|
|
4541
4676
|
repositoryPath: projects.repositoryPath,
|
|
4542
4677
|
lastOpenedAt: projects.lastOpenedAt
|
|
4543
|
-
}).from(projects).where(eq(projects.isOpen, 0)).orderBy(desc(projects.lastOpenedAt), asc(projects.id));
|
|
4678
|
+
}).from(projects).where(and(eq(projects.isOpen, 0), eq(projects.showInRecents, 1))).orderBy(desc(projects.lastOpenedAt), asc(projects.id));
|
|
4544
4679
|
}
|
|
4545
4680
|
async collectCurrentProjectsSnapshot() {
|
|
4546
4681
|
while (true) {
|
|
@@ -4553,7 +4688,8 @@ var TreeportService = class {
|
|
|
4553
4688
|
return (await Promise.all((await this.storedProjects(true)).map(async (storedProject) => {
|
|
4554
4689
|
let project = storedProject;
|
|
4555
4690
|
try {
|
|
4556
|
-
await this.importWorktrees(project.id, project.repositoryPath, project.mainWorktreePath);
|
|
4691
|
+
if (project.kind === "repository") await this.importWorktrees(project.id, project.repositoryPath, project.mainWorktreePath);
|
|
4692
|
+
else project = await this.observeAvailableProject(project);
|
|
4557
4693
|
await this.ensureProjectTerminals(project.id);
|
|
4558
4694
|
project = await this.storedProject(project.id) ?? project;
|
|
4559
4695
|
} catch (error) {
|
|
@@ -4564,7 +4700,7 @@ var TreeportService = class {
|
|
|
4564
4700
|
}
|
|
4565
4701
|
if (await this.projectOpenState(project.id) !== true) return null;
|
|
4566
4702
|
await Promise.all(project.worktrees.map(async (worktree) => {
|
|
4567
|
-
const [dirty, terminals] = await Promise.all([project.availability.state === "available" && !worktree.prunable ? this.deps.git.dirtyState(worktree.path).catch(() => null) : null, this.listWorktreeTerminals(worktree).catch((error) => {
|
|
4703
|
+
const [dirty, terminals] = await Promise.all([project.kind === "repository" && project.availability.state === "available" && !worktree.prunable ? this.deps.git.dirtyState(worktree.path).catch(() => null) : null, this.listWorktreeTerminals(worktree).catch((error) => {
|
|
4568
4704
|
project.availability = {
|
|
4569
4705
|
state: "unavailable",
|
|
4570
4706
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -4605,6 +4741,8 @@ var TreeportService = class {
|
|
|
4605
4741
|
name: terminal.name,
|
|
4606
4742
|
tmuxSessionName: terminal.sessionName,
|
|
4607
4743
|
argv: terminal.argv,
|
|
4744
|
+
shellCommand: terminal.shellCommand,
|
|
4745
|
+
interactiveShell: terminal.interactiveShell,
|
|
4608
4746
|
status: terminal.status,
|
|
4609
4747
|
exitCode: terminal.exitCode,
|
|
4610
4748
|
createdAt: terminal.createdAt,
|
|
@@ -4643,14 +4781,14 @@ var TreeportService = class {
|
|
|
4643
4781
|
const binding = await this.getWorktree(worktreeId);
|
|
4644
4782
|
await this.requireOpenProject(binding.projectId);
|
|
4645
4783
|
const worktree = (await this.listProjects()).flatMap((project) => project.worktrees).find((candidate) => candidate.id === worktreeId);
|
|
4646
|
-
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "
|
|
4784
|
+
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
4647
4785
|
return worktree;
|
|
4648
4786
|
}
|
|
4649
4787
|
async requireAvailableWorktree(worktreeId, allowPrunable = false) {
|
|
4650
4788
|
const binding = await this.storedWorktree(worktreeId);
|
|
4651
|
-
if (!binding) throw new DomainError("WORKTREE_NOT_FOUND", "
|
|
4789
|
+
if (!binding) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
4652
4790
|
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", "
|
|
4791
|
+
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
4654
4792
|
if (worktree.prunable && !allowPrunable) throw new DomainError("WORKTREE_UNAVAILABLE", "Git reports this worktree as prunable", 409);
|
|
4655
4793
|
return worktree;
|
|
4656
4794
|
}
|
|
@@ -4668,7 +4806,7 @@ var TreeportService = class {
|
|
|
4668
4806
|
const direct = await this.storedProject(identifier);
|
|
4669
4807
|
if (direct) return direct;
|
|
4670
4808
|
const canonical = await fs.realpath(path.resolve(identifier)).catch(() => path.resolve(identifier));
|
|
4671
|
-
const match = (await this.storedProjects()).find((project) => isPathWithin(canonical, project.
|
|
4809
|
+
const match = (await this.storedProjects()).find((project) => isPathWithin(canonical, project.rootPath) || project.worktrees.some((worktree) => isPathWithin(canonical, worktree.path)));
|
|
4672
4810
|
if (!match) throw new DomainError("PROJECT_NOT_FOUND", `No registered project contains ${identifier}`, 404);
|
|
4673
4811
|
return match;
|
|
4674
4812
|
}
|
|
@@ -4736,7 +4874,7 @@ var TreeportService = class {
|
|
|
4736
4874
|
definitions: [],
|
|
4737
4875
|
diagnostics: []
|
|
4738
4876
|
}),
|
|
4739
|
-
worktree && project ? loadZedTerminalPresetDefinitions({
|
|
4877
|
+
worktree && project?.kind === "repository" ? loadZedTerminalPresetDefinitions({
|
|
4740
4878
|
projectId: project.id,
|
|
4741
4879
|
shell: this.deps.config.shell,
|
|
4742
4880
|
mainWorktreePath: project.mainWorktreePath,
|
|
@@ -4758,6 +4896,7 @@ var TreeportService = class {
|
|
|
4758
4896
|
name: preset.name,
|
|
4759
4897
|
executable: preset.executable,
|
|
4760
4898
|
args: [...preset.args],
|
|
4899
|
+
shellCommand: null,
|
|
4761
4900
|
cwd: null,
|
|
4762
4901
|
env: {},
|
|
4763
4902
|
closeOnSuccess: preset.closeOnSuccess,
|
|
@@ -4851,17 +4990,20 @@ var TreeportService = class {
|
|
|
4851
4990
|
async effectiveWebPanelDefinitions(worktreeId) {
|
|
4852
4991
|
const worktree = await this.getWorktree(worktreeId);
|
|
4853
4992
|
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
|
-
|
|
4993
|
+
return [...await this.localWebPanelDefinitions(worktreeId), ...(await this.packages.webPanelDefinitions(worktree.projectId)).map(({ definition, root, entry, packageRoot, development, packageLockPath }) => {
|
|
4994
|
+
const resolved = {
|
|
4995
|
+
...definition,
|
|
4996
|
+
root,
|
|
4997
|
+
entry,
|
|
4998
|
+
packageRoot,
|
|
4999
|
+
development,
|
|
5000
|
+
definitionId: definition.id,
|
|
5001
|
+
allowNetworkRequests: definition.sandbox.allowSameOrigin
|
|
5002
|
+
};
|
|
5003
|
+
if (packageLockPath) resolved.packageLockPath = packageLockPath;
|
|
5004
|
+
if (definition.source.type === "package") resolved.packageSource = definition.source.source;
|
|
5005
|
+
return resolved;
|
|
5006
|
+
})];
|
|
4865
5007
|
}
|
|
4866
5008
|
async listWebPanelDefinitions(worktreeId) {
|
|
4867
5009
|
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 +5021,7 @@ var TreeportService = class {
|
|
|
4879
5021
|
const [worktreeRoot, requestedCwd] = await Promise.all([fs.realpath(worktree.path), fs.realpath(path.resolve(worktree.path, launch.cwd)).catch(() => null)]);
|
|
4880
5022
|
if (!requestedCwd || !(await fs.stat(requestedCwd)).isDirectory()) throw new DomainError("INVALID_WEB_PANEL_LAUNCH_CWD", "Web panel launch directory does not exist", 400);
|
|
4881
5023
|
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
|
|
5024
|
+
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
5025
|
return {
|
|
4884
5026
|
launch: {
|
|
4885
5027
|
input: launch.input,
|
|
@@ -5004,18 +5146,21 @@ var TreeportService = class {
|
|
|
5004
5146
|
project: {
|
|
5005
5147
|
id: project.id,
|
|
5006
5148
|
name: project.name,
|
|
5007
|
-
|
|
5149
|
+
kind: project.kind,
|
|
5150
|
+
defaultBranch: project.kind === "repository" ? project.defaultBranch : null
|
|
5008
5151
|
},
|
|
5009
5152
|
worktree: {
|
|
5010
5153
|
id: worktree.id,
|
|
5011
5154
|
name: worktree.name,
|
|
5155
|
+
kind: worktree.kind,
|
|
5012
5156
|
branch: worktree.branch,
|
|
5013
|
-
head: worktree.head
|
|
5157
|
+
head: worktree.kind === "folder" ? null : worktree.head
|
|
5014
5158
|
}
|
|
5015
5159
|
};
|
|
5016
5160
|
}
|
|
5017
5161
|
async getWebPanelDiff(panelId) {
|
|
5018
5162
|
const context = await this.getWebPanelContext(panelId);
|
|
5163
|
+
if (context.project.kind !== "repository" || !context.project.defaultBranch) throw new DomainError("GIT_NOT_AVAILABLE", "Git diff is not available for a folder project", 409);
|
|
5019
5164
|
const worktree = await this.getWorktree(context.panel.worktreeId);
|
|
5020
5165
|
return this.deps.git.worktreeDiff(worktree.path, context.project.defaultBranch);
|
|
5021
5166
|
}
|
|
@@ -5077,9 +5222,17 @@ var TreeportService = class {
|
|
|
5077
5222
|
}
|
|
5078
5223
|
async getWorktree(worktreeId) {
|
|
5079
5224
|
const worktree = await this.storedWorktree(worktreeId);
|
|
5080
|
-
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "
|
|
5225
|
+
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
5081
5226
|
return worktree;
|
|
5082
5227
|
}
|
|
5228
|
+
async requestWorkspaceOpen(worktreeId, sourceTerminalId) {
|
|
5229
|
+
const worktree = await this.getWorktree(worktreeId);
|
|
5230
|
+
await this.requireOpenProject(worktree.projectId);
|
|
5231
|
+
this.events.publish("workspace.open_requested", {
|
|
5232
|
+
worktreeId,
|
|
5233
|
+
sourceTerminalId
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5083
5236
|
async getTerminal(terminalId) {
|
|
5084
5237
|
const matches = (await this.listProjects()).flatMap((project) => project.worktrees).flatMap((worktree) => worktree.terminals).filter((terminal) => terminal.id === terminalId);
|
|
5085
5238
|
if (matches.length > 1) throw new DomainError("TERMINAL_ID_CONFLICT", "Terminal ID is present in more than one tmux server", 500);
|
|
@@ -5113,7 +5266,7 @@ var TreeportService = class {
|
|
|
5113
5266
|
const direct = await this.storedProject(identifier);
|
|
5114
5267
|
if (direct) return await this.requireOpenProject(direct.id);
|
|
5115
5268
|
const canonical = await fs.realpath(path.resolve(identifier)).catch(() => path.resolve(identifier));
|
|
5116
|
-
const match = (await this.storedProjects()).find((project) => isPathWithin(canonical, project.
|
|
5269
|
+
const match = (await this.storedProjects()).find((project) => isPathWithin(canonical, project.rootPath) || project.worktrees.some((worktree) => isPathWithin(canonical, worktree.path)));
|
|
5117
5270
|
if (!match) throw new DomainError("PROJECT_NOT_FOUND", `No registered project contains ${identifier}`, 404);
|
|
5118
5271
|
await this.requireOpenProject(match.id);
|
|
5119
5272
|
return match;
|
|
@@ -5126,7 +5279,7 @@ var TreeportService = class {
|
|
|
5126
5279
|
}
|
|
5127
5280
|
const canonical = await fs.realpath(path.resolve(identifier)).catch(() => path.resolve(identifier));
|
|
5128
5281
|
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
|
|
5282
|
+
if (!match) throw new DomainError("WORKTREE_NOT_FOUND", `No registered tree contains ${identifier}`, 404);
|
|
5130
5283
|
await this.requireOpenProject(match.projectId);
|
|
5131
5284
|
return match;
|
|
5132
5285
|
}
|
|
@@ -5186,7 +5339,7 @@ var TreeportService = class {
|
|
|
5186
5339
|
path: breadcrumbPath
|
|
5187
5340
|
});
|
|
5188
5341
|
}
|
|
5189
|
-
const repositoryPath = exact ? await this.deps.git.
|
|
5342
|
+
const repositoryPath = exact ? await this.deps.git.findProjectRepositoryRoot(directoryPath).then((checkout) => checkout ? this.deps.git.resolveMainCheckout(checkout) : null).then((mainCheckout) => mainCheckout ? fs.realpath(mainCheckout) : null) : null;
|
|
5190
5343
|
return {
|
|
5191
5344
|
input: inputPath,
|
|
5192
5345
|
exact,
|
|
@@ -5199,6 +5352,18 @@ var TreeportService = class {
|
|
|
5199
5352
|
entries,
|
|
5200
5353
|
truncated
|
|
5201
5354
|
},
|
|
5355
|
+
project: exact ? repositoryPath ? {
|
|
5356
|
+
state: "valid",
|
|
5357
|
+
kind: "repository",
|
|
5358
|
+
path: repositoryPath
|
|
5359
|
+
} : {
|
|
5360
|
+
state: "valid",
|
|
5361
|
+
kind: "folder",
|
|
5362
|
+
path: directoryPath
|
|
5363
|
+
} : {
|
|
5364
|
+
state: "incomplete",
|
|
5365
|
+
message: "Choose a matching folder to continue."
|
|
5366
|
+
},
|
|
5202
5367
|
repository: repositoryPath ? {
|
|
5203
5368
|
state: "valid",
|
|
5204
5369
|
repositoryPath
|
|
@@ -5212,6 +5377,14 @@ var TreeportService = class {
|
|
|
5212
5377
|
};
|
|
5213
5378
|
}
|
|
5214
5379
|
async registerProject(inputPath, requestedName) {
|
|
5380
|
+
const canonicalPath = await fs.realpath(path.resolve(inputPath)).catch((error) => {
|
|
5381
|
+
throw new DomainError("FOLDER_UNREADABLE", error instanceof Error ? error.message : "Folder cannot be read", 400);
|
|
5382
|
+
});
|
|
5383
|
+
if (!(await fs.stat(canonicalPath, { bigint: true })).isDirectory()) throw new DomainError("FOLDER_NOT_DIRECTORY", `Path is not a folder: ${canonicalPath}`, 400);
|
|
5384
|
+
const repositoryRoot = await this.deps.git.findProjectRepositoryRoot(canonicalPath);
|
|
5385
|
+
return repositoryRoot ? this.registerRepositoryProject(repositoryRoot, requestedName) : this.registerFolderProject(canonicalPath, requestedName);
|
|
5386
|
+
}
|
|
5387
|
+
async registerRepositoryProject(inputPath, requestedName) {
|
|
5215
5388
|
const checkout = await this.deps.git.canonicalizeRepositoryPath(inputPath).catch((error) => {
|
|
5216
5389
|
throw new DomainError("NOT_A_GIT_REPOSITORY", error instanceof Error ? error.message : "Not a Git repository", 400);
|
|
5217
5390
|
});
|
|
@@ -5266,17 +5439,18 @@ var TreeportService = class {
|
|
|
5266
5439
|
if (verifiedIdentity !== repositoryIdentity || verifiedStat.dev.toString() !== repositoryDevice || verifiedStat.ino.toString() !== repositoryInode) throw new DomainError("PROJECT_PATH_CONFLICT", "The repository changed during registration", 409);
|
|
5267
5440
|
await this.deps.database.db.run(sql`
|
|
5268
5441
|
INSERT INTO projects(
|
|
5269
|
-
id,name,repository_path,main_worktree_path,default_branch,
|
|
5442
|
+
id,name,project_kind,repository_path,main_worktree_path,default_branch,
|
|
5270
5443
|
repository_identity,repository_device,repository_inode,name_is_custom,
|
|
5271
|
-
is_open,last_opened_at,created_at,updated_at
|
|
5444
|
+
is_open,show_in_recents,last_opened_at,created_at,updated_at
|
|
5272
5445
|
) VALUES(
|
|
5273
|
-
${projectId},${name},${repositoryPath},${mainPath},${defaultBranch},
|
|
5446
|
+
${projectId},${name},'repository',${repositoryPath},${mainPath},${defaultBranch},
|
|
5274
5447
|
${repositoryIdentity},${repositoryDevice},${repositoryInode},
|
|
5275
|
-
${nameIsCustom ? 1 : 0},1,${timestamp},
|
|
5448
|
+
${nameIsCustom ? 1 : 0},1,0,${timestamp},
|
|
5276
5449
|
${existing?.createdAt ?? timestamp},${timestamp}
|
|
5277
5450
|
)
|
|
5278
5451
|
ON CONFLICT(id) DO UPDATE SET
|
|
5279
5452
|
name=excluded.name,
|
|
5453
|
+
project_kind=excluded.project_kind,
|
|
5280
5454
|
repository_path=excluded.repository_path,
|
|
5281
5455
|
main_worktree_path=excluded.main_worktree_path,
|
|
5282
5456
|
default_branch=excluded.default_branch,
|
|
@@ -5297,6 +5471,7 @@ var TreeportService = class {
|
|
|
5297
5471
|
const timestamp = now();
|
|
5298
5472
|
await this.deps.database.db.update(projects).set({
|
|
5299
5473
|
isOpen: 1,
|
|
5474
|
+
showInRecents: 0,
|
|
5300
5475
|
lastOpenedAt: timestamp,
|
|
5301
5476
|
updatedAt: timestamp
|
|
5302
5477
|
}).where(eq(projects.id, projectId));
|
|
@@ -5317,9 +5492,123 @@ var TreeportService = class {
|
|
|
5317
5492
|
this.events.publish("project.created", { projectId });
|
|
5318
5493
|
return this.getProjectSnapshot(projectId);
|
|
5319
5494
|
}
|
|
5495
|
+
async registerFolderProject(folderPath, requestedName) {
|
|
5496
|
+
const folderStat = await fs.stat(folderPath, { bigint: true });
|
|
5497
|
+
const device = folderStat.dev.toString();
|
|
5498
|
+
const inode = folderStat.ino.toString();
|
|
5499
|
+
const [pathMatchRow] = await this.deps.database.db.select({ id: projects.id }).from(projects).where(eq(projects.repositoryPath, folderPath)).limit(1);
|
|
5500
|
+
const identityMatchId = [...this.observedFolderIdentities].find(([, identity]) => identity.device === device && identity.inode === inode)?.[0];
|
|
5501
|
+
const [pathMatch, identityMatch] = await Promise.all([pathMatchRow ? this.storedProject(pathMatchRow.id) : null, identityMatchId ? this.storedProject(identityMatchId) : null]);
|
|
5502
|
+
if (pathMatch?.kind === "repository") throw new DomainError("PROJECT_PATH_CONFLICT", "The selected folder is registered as a Git repository, but Git no longer recognizes it", 409);
|
|
5503
|
+
const observedPathIdentity = pathMatch ? this.observedFolderIdentities.get(pathMatch.id) : null;
|
|
5504
|
+
if (observedPathIdentity && (observedPathIdentity.device !== device || observedPathIdentity.inode !== inode)) throw new DomainError("PROJECT_PATH_CONFLICT", "The registered folder path now refers to a different folder", 409);
|
|
5505
|
+
if (pathMatch && identityMatch && pathMatch.id !== identityMatch.id) throw new DomainError("PROJECT_PATH_CONFLICT", "The folder identity and registered path belong to different projects", 409);
|
|
5506
|
+
const existing = identityMatch ?? pathMatch;
|
|
5507
|
+
const projectId = existing?.id ?? id("proj");
|
|
5508
|
+
const updateRegistration = async () => {
|
|
5509
|
+
const timestamp = now();
|
|
5510
|
+
const [metadata] = existing ? await this.deps.database.db.select({ nameIsCustom: projects.nameIsCustom }).from(projects).where(eq(projects.id, existing.id)).limit(1) : [];
|
|
5511
|
+
const requested = requestedName?.trim() || null;
|
|
5512
|
+
const nameIsCustom = requested ? true : Boolean(metadata?.nameIsCustom);
|
|
5513
|
+
const name = requested || (existing && !nameIsCustom && existing.name === path.basename(existing.rootPath) ? path.basename(folderPath) : existing?.name) || path.basename(folderPath);
|
|
5514
|
+
const [verifiedPath, verifiedStat] = await Promise.all([fs.realpath(folderPath), fs.stat(folderPath, { bigint: true })]);
|
|
5515
|
+
if (verifiedPath !== folderPath || !verifiedStat.isDirectory() || verifiedStat.dev.toString() !== device || verifiedStat.ino.toString() !== inode) throw new DomainError("PROJECT_PATH_CONFLICT", "The folder changed during registration", 409);
|
|
5516
|
+
const existingWorktreeRows = existing ? await this.deps.database.db.select().from(worktrees).where(eq(worktrees.projectId, projectId)) : [];
|
|
5517
|
+
if (existingWorktreeRows.length > 1 || existingWorktreeRows.some((worktree) => worktree.kind !== "folder")) throw new DomainError("PROJECT_PATH_CONFLICT", "The folder registration contains incompatible Git worktrees", 409);
|
|
5518
|
+
const existingWorktree = existingWorktreeRows[0];
|
|
5519
|
+
const worktreeId = existingWorktree?.id ?? id("wt");
|
|
5520
|
+
await this.deps.database.db.transaction(async (tx) => {
|
|
5521
|
+
await tx.run(sql`
|
|
5522
|
+
INSERT INTO projects(
|
|
5523
|
+
id,name,project_kind,repository_path,main_worktree_path,default_branch,
|
|
5524
|
+
repository_identity,repository_device,repository_inode,name_is_custom,
|
|
5525
|
+
is_open,show_in_recents,last_opened_at,created_at,updated_at
|
|
5526
|
+
) VALUES(
|
|
5527
|
+
${projectId},${name},'folder',${folderPath},${folderPath},'',
|
|
5528
|
+
NULL,${device},${inode},${nameIsCustom ? 1 : 0},1,0,${timestamp},
|
|
5529
|
+
${existing?.createdAt ?? timestamp},${timestamp}
|
|
5530
|
+
)
|
|
5531
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
5532
|
+
name=excluded.name,
|
|
5533
|
+
project_kind='folder',
|
|
5534
|
+
repository_path=excluded.repository_path,
|
|
5535
|
+
main_worktree_path=excluded.main_worktree_path,
|
|
5536
|
+
default_branch='',
|
|
5537
|
+
repository_identity=NULL,
|
|
5538
|
+
repository_device=excluded.repository_device,
|
|
5539
|
+
repository_inode=excluded.repository_inode,
|
|
5540
|
+
name_is_custom=excluded.name_is_custom,
|
|
5541
|
+
is_open=1,
|
|
5542
|
+
show_in_recents=0,
|
|
5543
|
+
last_opened_at=excluded.last_opened_at,
|
|
5544
|
+
updated_at=excluded.updated_at
|
|
5545
|
+
`);
|
|
5546
|
+
if (existingWorktree) await tx.run(sql`
|
|
5547
|
+
UPDATE worktrees
|
|
5548
|
+
SET path=${folderPath},git_worktree_key=NULL,head='',branch=NULL,
|
|
5549
|
+
detached=0,locked=0,lock_reason=NULL,prunable=0,kind='folder',
|
|
5550
|
+
managed_wrapper_path=NULL,pr_state='unknown',pr_number=NULL,
|
|
5551
|
+
pr_url=NULL,pr_base_branch=NULL,pr_head_branch=NULL,
|
|
5552
|
+
pr_merged_at=NULL,pr_refreshed_at=NULL,updated_at=${timestamp}
|
|
5553
|
+
WHERE id=${worktreeId}
|
|
5554
|
+
`);
|
|
5555
|
+
else await tx.run(sql`
|
|
5556
|
+
INSERT INTO worktrees(
|
|
5557
|
+
id,project_id,path,git_worktree_key,head,branch,detached,locked,
|
|
5558
|
+
lock_reason,prunable,kind,tmux_socket_name,created_at,updated_at
|
|
5559
|
+
) VALUES(
|
|
5560
|
+
${worktreeId},${projectId},${folderPath},NULL,'',NULL,0,0,NULL,0,
|
|
5561
|
+
'folder',${generateTmuxSocketName()},${timestamp},${timestamp}
|
|
5562
|
+
)
|
|
5563
|
+
`);
|
|
5564
|
+
});
|
|
5565
|
+
};
|
|
5566
|
+
const register = async () => {
|
|
5567
|
+
if (this.projectLocks.has(projectId) || this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
|
|
5568
|
+
this.projectLocks.add(projectId);
|
|
5569
|
+
try {
|
|
5570
|
+
await updateRegistration();
|
|
5571
|
+
} finally {
|
|
5572
|
+
this.projectLocks.delete(projectId);
|
|
5573
|
+
}
|
|
5574
|
+
};
|
|
5575
|
+
if (existing) await this.serializeProjectObservation(projectId, register);
|
|
5576
|
+
else await register();
|
|
5577
|
+
this.observedFolderIdentities.set(projectId, {
|
|
5578
|
+
device,
|
|
5579
|
+
inode
|
|
5580
|
+
});
|
|
5581
|
+
await this.packages.registerProject(await this.getProject(projectId));
|
|
5582
|
+
await this.ensureProjectTerminals(projectId).catch(() => void 0);
|
|
5583
|
+
this.invalidateProjectsSnapshot();
|
|
5584
|
+
this.events.publish(existing ? "project.updated" : "project.created", { projectId });
|
|
5585
|
+
return this.getProjectSnapshot(projectId);
|
|
5586
|
+
}
|
|
5320
5587
|
async observeAvailableProject(project, allowClosed = false) {
|
|
5321
5588
|
try {
|
|
5322
|
-
await this.importWorktrees(project.id, project.repositoryPath, project.mainWorktreePath, true, allowClosed);
|
|
5589
|
+
if (project.kind === "repository") await this.importWorktrees(project.id, project.repositoryPath, project.mainWorktreePath, true, allowClosed);
|
|
5590
|
+
else await this.serializeProjectObservation(project.id, async () => {
|
|
5591
|
+
if (!allowClosed && await this.projectOpenState(project.id) !== true || this.worktreeMutations.has(project.id)) return;
|
|
5592
|
+
const [metadata] = await this.deps.database.db.select({
|
|
5593
|
+
device: projects.repositoryDevice,
|
|
5594
|
+
inode: projects.repositoryInode
|
|
5595
|
+
}).from(projects).where(eq(projects.id, project.id)).limit(1);
|
|
5596
|
+
const [canonicalPath, folderStat] = await Promise.all([fs.realpath(project.rootPath), fs.stat(project.rootPath, { bigint: true })]);
|
|
5597
|
+
if (!metadata || canonicalPath !== project.rootPath || !folderStat.isDirectory()) throw new Error("The registered folder path is not an available directory");
|
|
5598
|
+
const device = folderStat.dev.toString();
|
|
5599
|
+
const inode = folderStat.ino.toString();
|
|
5600
|
+
const observedIdentity = this.observedFolderIdentities.get(project.id);
|
|
5601
|
+
if (observedIdentity && (observedIdentity.device !== device || observedIdentity.inode !== inode)) throw new Error("The registered folder path changed during this daemon session");
|
|
5602
|
+
if (project.worktrees.filter((worktree) => worktree.kind === "folder" && worktree.path === project.rootPath).length !== 1 || project.worktrees.length !== 1) throw new Error("The registered folder does not have one folder workspace");
|
|
5603
|
+
if (metadata.device !== device || metadata.inode !== inode) await this.deps.database.db.update(projects).set({
|
|
5604
|
+
repositoryDevice: device,
|
|
5605
|
+
repositoryInode: inode
|
|
5606
|
+
}).where(eq(projects.id, project.id));
|
|
5607
|
+
this.observedFolderIdentities.set(project.id, {
|
|
5608
|
+
device,
|
|
5609
|
+
inode
|
|
5610
|
+
});
|
|
5611
|
+
});
|
|
5323
5612
|
} catch (error) {
|
|
5324
5613
|
throw new DomainError("PROJECT_UNAVAILABLE", error instanceof Error ? error.message : String(error), 503);
|
|
5325
5614
|
}
|
|
@@ -5332,12 +5621,14 @@ var TreeportService = class {
|
|
|
5332
5621
|
try {
|
|
5333
5622
|
const project = await this.observeAvailableProject(await this.getProject(projectId));
|
|
5334
5623
|
await this.ensureProjectTerminals(projectId);
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5624
|
+
if (project.kind === "repository") {
|
|
5625
|
+
const defaultBranch = await this.deps.git.defaultBranch(project.repositoryPath);
|
|
5626
|
+
await this.deps.database.db.run(sql`
|
|
5627
|
+
UPDATE projects
|
|
5628
|
+
SET default_branch = ${defaultBranch}, updated_at = ${now()}
|
|
5629
|
+
WHERE id = ${projectId}
|
|
5630
|
+
`);
|
|
5631
|
+
}
|
|
5341
5632
|
await this.reconcile();
|
|
5342
5633
|
await this.packages.registerProject(await this.getProject(projectId));
|
|
5343
5634
|
this.invalidateProjectsSnapshot();
|
|
@@ -5356,6 +5647,7 @@ var TreeportService = class {
|
|
|
5356
5647
|
const timestamp = now();
|
|
5357
5648
|
await this.deps.database.db.update(projects).set({
|
|
5358
5649
|
isOpen: 1,
|
|
5650
|
+
showInRecents: 0,
|
|
5359
5651
|
lastOpenedAt: timestamp,
|
|
5360
5652
|
updatedAt: timestamp
|
|
5361
5653
|
}).where(eq(projects.id, projectId));
|
|
@@ -5373,7 +5665,7 @@ var TreeportService = class {
|
|
|
5373
5665
|
const project = await this.getProject(projectId);
|
|
5374
5666
|
if (await this.projectOpenState(projectId) !== true) return;
|
|
5375
5667
|
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
|
|
5668
|
+
if (project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project tree is already being modified", 409);
|
|
5377
5669
|
this.projectLocks.add(projectId);
|
|
5378
5670
|
const lockedWorktreeIds = project.worktrees.map((worktree) => worktree.id);
|
|
5379
5671
|
for (const worktreeId of lockedWorktreeIds) this.worktreeLocks.add(worktreeId);
|
|
@@ -5397,6 +5689,7 @@ var TreeportService = class {
|
|
|
5397
5689
|
try {
|
|
5398
5690
|
await this.deps.database.db.update(projects).set({
|
|
5399
5691
|
isOpen: 0,
|
|
5692
|
+
showInRecents: 1,
|
|
5400
5693
|
updatedAt: now()
|
|
5401
5694
|
}).where(eq(projects.id, projectId));
|
|
5402
5695
|
} catch (error) {
|
|
@@ -5414,6 +5707,17 @@ var TreeportService = class {
|
|
|
5414
5707
|
}
|
|
5415
5708
|
});
|
|
5416
5709
|
}
|
|
5710
|
+
async dismissRecentProject(projectId) {
|
|
5711
|
+
await this.serializeProjectObservation(projectId, async () => {
|
|
5712
|
+
await this.getProject(projectId);
|
|
5713
|
+
if (await this.projectOpenState(projectId) !== false) throw new DomainError("PROJECT_NOT_RECENT", "Project is open and cannot be removed from Recent projects", 409);
|
|
5714
|
+
await this.deps.database.db.update(projects).set({
|
|
5715
|
+
showInRecents: 0,
|
|
5716
|
+
updatedAt: now()
|
|
5717
|
+
}).where(and(eq(projects.id, projectId), eq(projects.isOpen, 0)));
|
|
5718
|
+
this.events.publish("project.updated", { projectId });
|
|
5719
|
+
});
|
|
5720
|
+
}
|
|
5417
5721
|
async serializeProjectObservation(projectId, operation) {
|
|
5418
5722
|
const observation = (this.projectObservationTails.get(projectId) ?? Promise.resolve()).then(operation);
|
|
5419
5723
|
const tail = observation.then(() => void 0, () => void 0);
|
|
@@ -5598,7 +5902,7 @@ var TreeportService = class {
|
|
|
5598
5902
|
return (await this.deps.database.db.select().from(operations).where(and(or(eq(operations.status, "pending"), eq(operations.status, "running")), ...filters.projectId ? [eq(operations.projectId, filters.projectId)] : [], ...filters.kind ? [eq(operations.kind, filters.kind)] : [])).orderBy(asc(operations.createdAt), asc(operations.id))).map(mapOperation);
|
|
5599
5903
|
}
|
|
5600
5904
|
async beginCreateWorktree(projectId, inputName, base, initialTerminal, sourceWorktreeId) {
|
|
5601
|
-
await this.requireOpenProject(projectId);
|
|
5905
|
+
if ((await this.requireOpenProject(projectId)).kind === "folder") throw new DomainError("PROJECT_HAS_NO_GIT_REPOSITORY", "Linked worktrees require a Git repository project", 409);
|
|
5602
5906
|
let name;
|
|
5603
5907
|
try {
|
|
5604
5908
|
name = normalizeWorktreeName(inputName);
|
|
@@ -5608,18 +5912,19 @@ var TreeportService = class {
|
|
|
5608
5912
|
if (this.projectLocks.has(projectId) && !this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
|
|
5609
5913
|
const operationId = id("op");
|
|
5610
5914
|
const timestamp = now();
|
|
5915
|
+
const request = {
|
|
5916
|
+
name,
|
|
5917
|
+
base
|
|
5918
|
+
};
|
|
5919
|
+
if (initialTerminal) request.initialTerminal = initialTerminal;
|
|
5920
|
+
if (sourceWorktreeId) request.sourceWorktreeId = sourceWorktreeId;
|
|
5611
5921
|
await this.deps.database.db.run(sql`
|
|
5612
5922
|
INSERT INTO operations(
|
|
5613
5923
|
id,kind,project_id,worktree_id,status,request_json,result_json,error,
|
|
5614
5924
|
created_at,updated_at
|
|
5615
5925
|
) VALUES(
|
|
5616
5926
|
${operationId},'create',${projectId},NULL,'pending',
|
|
5617
|
-
${serializeOperation({
|
|
5618
|
-
name,
|
|
5619
|
-
base,
|
|
5620
|
-
...initialTerminal ? { initialTerminal } : {},
|
|
5621
|
-
...sourceWorktreeId ? { sourceWorktreeId } : {}
|
|
5622
|
-
})},NULL,NULL,${timestamp},${timestamp}
|
|
5927
|
+
${serializeOperation(request)},NULL,NULL,${timestamp},${timestamp}
|
|
5623
5928
|
)
|
|
5624
5929
|
`);
|
|
5625
5930
|
const operation = await this.getOperation(operationId);
|
|
@@ -5668,7 +5973,7 @@ var TreeportService = class {
|
|
|
5668
5973
|
}
|
|
5669
5974
|
}
|
|
5670
5975
|
async createWorktree(projectId, inputName, base, initialTerminal, sourceWorktreeId) {
|
|
5671
|
-
await this.requireOpenProject(projectId);
|
|
5976
|
+
if ((await this.requireOpenProject(projectId)).kind === "folder") throw new DomainError("PROJECT_HAS_NO_GIT_REPOSITORY", "Linked worktrees require a Git repository project", 409);
|
|
5672
5977
|
if (this.projectLocks.has(projectId) && !this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
|
|
5673
5978
|
return this.worktreeMutations.enqueue(projectId, () => this.executeCreateWorktree(projectId, inputName, base, initialTerminal, sourceWorktreeId));
|
|
5674
5979
|
}
|
|
@@ -5683,13 +5988,14 @@ var TreeportService = class {
|
|
|
5683
5988
|
let wrapperCreated = false;
|
|
5684
5989
|
try {
|
|
5685
5990
|
project = await this.observeAvailableProject(await this.requireOpenProject(projectId));
|
|
5991
|
+
if (project.kind === "folder") throw new DomainError("PROJECT_HAS_NO_GIT_REPOSITORY", "Linked worktrees require a Git repository project", 409);
|
|
5686
5992
|
let name;
|
|
5687
5993
|
try {
|
|
5688
5994
|
name = normalizeWorktreeName(inputName);
|
|
5689
5995
|
} catch (error) {
|
|
5690
5996
|
throw new DomainError("INVALID_WORKTREE_NAME", error instanceof Error ? error.message : String(error), 400);
|
|
5691
5997
|
}
|
|
5692
|
-
if (project.worktrees.some((worktree) => worktree.name.localeCompare(name, void 0, { sensitivity: "accent" }) === 0)) throw new DomainError("WORKTREE_EXISTS", `A
|
|
5998
|
+
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
5999
|
const destination = await resolveZedWorktreePath(project.mainWorktreePath, name).catch((error) => {
|
|
5694
6000
|
throw new DomainError("INVALID_WORKTREE_PATH", error instanceof Error ? error.message : String(error), 400);
|
|
5695
6001
|
});
|
|
@@ -5698,9 +6004,9 @@ var TreeportService = class {
|
|
|
5698
6004
|
if (await fs.access(worktreePath).then(() => true, () => false)) throw new DomainError("WORKTREE_PATH_EXISTS", `Destination already exists: ${worktreePath}`, 409);
|
|
5699
6005
|
let commit;
|
|
5700
6006
|
if (base === "current") {
|
|
5701
|
-
if (!sourceWorktreeId) throw new DomainError("INVALID_SOURCE_WORKTREE", "A source
|
|
6007
|
+
if (!sourceWorktreeId) throw new DomainError("INVALID_SOURCE_WORKTREE", "A source tree is required when starting from current", 400);
|
|
5702
6008
|
const source = await this.getWorktree(sourceWorktreeId);
|
|
5703
|
-
if (source.projectId !== projectId || source.prunable) throw new DomainError("INVALID_SOURCE_WORKTREE", "The source
|
|
6009
|
+
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
6010
|
commit = await this.deps.git.resolveCommit(source.path);
|
|
5705
6011
|
} else commit = await this.deps.git.resolveDefaultCommit(project.repositoryPath);
|
|
5706
6012
|
let preparedWrapper;
|
|
@@ -5740,10 +6046,10 @@ var TreeportService = class {
|
|
|
5740
6046
|
let terminalError = null;
|
|
5741
6047
|
let setupError = null;
|
|
5742
6048
|
if (initialTerminal) {
|
|
5743
|
-
const
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
6049
|
+
const launchOptions = {};
|
|
6050
|
+
if (initialTerminal.returnToShell) launchOptions.returnToShell = true;
|
|
6051
|
+
if (initialTerminal.initialSize) launchOptions.initialSize = initialTerminal.initialSize;
|
|
6052
|
+
const initialTerminalCreation = this.executeCreateTerminal(worktree.id, initialTerminal.name, initialTerminal.argv, launchOptions);
|
|
5747
6053
|
const setupResolution = resolveWorktreeSetupTasks({
|
|
5748
6054
|
shell: this.deps.config.shell,
|
|
5749
6055
|
mainWorktreePath: project.mainWorktreePath,
|
|
@@ -5753,7 +6059,7 @@ var TreeportService = class {
|
|
|
5753
6059
|
error: null
|
|
5754
6060
|
}), (error) => ({
|
|
5755
6061
|
tasks: [],
|
|
5756
|
-
error: `
|
|
6062
|
+
error: `Tree setup: ${error instanceof Error ? error.message : String(error)}`.slice(0, 4096)
|
|
5757
6063
|
}));
|
|
5758
6064
|
try {
|
|
5759
6065
|
terminal = await initialTerminalCreation;
|
|
@@ -5767,18 +6073,19 @@ var TreeportService = class {
|
|
|
5767
6073
|
}
|
|
5768
6074
|
const setup = await setupResolution;
|
|
5769
6075
|
setupError = setup.error;
|
|
5770
|
-
if (setup.tasks.length > 0 || setupError) if (!terminal) setupError ??= "
|
|
6076
|
+
if (setup.tasks.length > 0 || setupError) if (!terminal) setupError ??= "Tree setup: no persistent terminal could be started";
|
|
5771
6077
|
else try {
|
|
5772
|
-
|
|
6078
|
+
const setupOptions = {
|
|
5773
6079
|
setup: {
|
|
5774
6080
|
tasks: setup.tasks,
|
|
5775
6081
|
error: setupError
|
|
5776
6082
|
},
|
|
5777
|
-
closeOnSuccess: true
|
|
5778
|
-
|
|
5779
|
-
|
|
6083
|
+
closeOnSuccess: true
|
|
6084
|
+
};
|
|
6085
|
+
if (initialTerminal.initialSize) setupOptions.initialSize = initialTerminal.initialSize;
|
|
6086
|
+
await this.executeCreateTerminal(worktree.id, "Setup", ["true"], setupOptions);
|
|
5780
6087
|
} catch (error) {
|
|
5781
|
-
const setupTerminalError = `
|
|
6088
|
+
const setupTerminalError = `Tree setup terminal${error instanceof DomainError ? ` [${error.code}]` : ""}: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2048);
|
|
5782
6089
|
setupError = setupError ? `${setupError.slice(0, 2047)}\n${setupTerminalError}` : setupTerminalError;
|
|
5783
6090
|
}
|
|
5784
6091
|
} else {
|
|
@@ -5790,7 +6097,7 @@ var TreeportService = class {
|
|
|
5790
6097
|
runner: this.deps.runner,
|
|
5791
6098
|
tasks
|
|
5792
6099
|
})).catch((error) => [{
|
|
5793
|
-
label: "
|
|
6100
|
+
label: "Tree setup",
|
|
5794
6101
|
error: error instanceof Error ? error.message : String(error)
|
|
5795
6102
|
}])).find((result) => result.error);
|
|
5796
6103
|
setupError = setupFailure ? `${setupFailure.label}: ${setupFailure.error}`.slice(0, 4096) : null;
|
|
@@ -5835,34 +6142,43 @@ var TreeportService = class {
|
|
|
5835
6142
|
const project = await this.requireOpenProject(worktree.projectId);
|
|
5836
6143
|
const terminalId = id("term");
|
|
5837
6144
|
const sessionName = generateTmuxSessionName();
|
|
5838
|
-
const
|
|
6145
|
+
const shellCommand = options?.shellCommand ?? null;
|
|
6146
|
+
const interactiveShell = !argv && shellCommand === null;
|
|
6147
|
+
const commandArgv = argv ? [...argv] : shellCommand ? [
|
|
6148
|
+
this.deps.config.shell,
|
|
6149
|
+
"-lc",
|
|
6150
|
+
shellCommand
|
|
6151
|
+
] : [this.deps.config.shell, "-l"];
|
|
5839
6152
|
const timestamp = now();
|
|
6153
|
+
const session = {
|
|
6154
|
+
socketName: worktree.tmuxSocketName,
|
|
6155
|
+
sessionName,
|
|
6156
|
+
terminalId,
|
|
6157
|
+
worktreeId: worktree.id,
|
|
6158
|
+
name,
|
|
6159
|
+
createdAt: timestamp,
|
|
6160
|
+
cwd: options?.cwd ?? worktree.path,
|
|
6161
|
+
argv: commandArgv,
|
|
6162
|
+
shellCommand,
|
|
6163
|
+
interactiveShell,
|
|
6164
|
+
env: {
|
|
6165
|
+
...options?.env ?? {},
|
|
6166
|
+
TREEPORT_API_URL: this.deps.config.apiUrl,
|
|
6167
|
+
TREEPORT_MANAGED_API_URL: this.deps.config.apiUrl,
|
|
6168
|
+
TREEPORT_DAEMON_RECORD: path.join(this.deps.config.runtimeDir, "daemon.json"),
|
|
6169
|
+
TREEPORT_DAEMON_LIFECYCLE: this.deps.config.daemonLifecycle,
|
|
6170
|
+
TREEPORT_PROJECT_ID: project.id,
|
|
6171
|
+
TREEPORT_WORKTREE_ID: worktree.id,
|
|
6172
|
+
TREEPORT_TERMINAL_ID: terminalId
|
|
6173
|
+
}
|
|
6174
|
+
};
|
|
6175
|
+
if (options?.returnToShell && !interactiveShell) session.fallbackArgv = [this.deps.config.shell, "-l"];
|
|
6176
|
+
if (options?.closeOnSuccess) session.closeOnSuccess = true;
|
|
6177
|
+
if (options?.initialSize) session.initialSize = options.initialSize;
|
|
6178
|
+
if (options?.setup?.tasks.length) session.setupTasks = options.setup.tasks;
|
|
6179
|
+
if (options?.setup?.error) session.setupError = options.setup.error;
|
|
5840
6180
|
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
|
-
});
|
|
6181
|
+
await this.deps.tmux.createSession(session);
|
|
5866
6182
|
} catch (error) {
|
|
5867
6183
|
throw new DomainError("TERMINAL_CREATE_FAILED", error instanceof Error ? error.message : String(error), 500);
|
|
5868
6184
|
}
|
|
@@ -5872,6 +6188,8 @@ var TreeportService = class {
|
|
|
5872
6188
|
name,
|
|
5873
6189
|
tmuxSessionName: sessionName,
|
|
5874
6190
|
argv: commandArgv,
|
|
6191
|
+
shellCommand,
|
|
6192
|
+
interactiveShell,
|
|
5875
6193
|
status: "running",
|
|
5876
6194
|
exitCode: null,
|
|
5877
6195
|
createdAt: timestamp,
|
|
@@ -5898,8 +6216,8 @@ var TreeportService = class {
|
|
|
5898
6216
|
await this.requireAvailableWorktree(worktreeId);
|
|
5899
6217
|
try {
|
|
5900
6218
|
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
|
|
6219
|
+
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
6220
|
+
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
6221
|
this.worktreeLocks.add(worktreeId);
|
|
5904
6222
|
try {
|
|
5905
6223
|
return await this.createTerminalSession(worktree, name, argv, options);
|
|
@@ -5912,7 +6230,7 @@ var TreeportService = class {
|
|
|
5912
6230
|
}
|
|
5913
6231
|
}
|
|
5914
6232
|
async refreshTerminalStatus(terminalId, observeGit = true) {
|
|
5915
|
-
const terminal = observeGit ? await this.getTerminal(terminalId) : await this.getTerminalFromBindings(terminalId);
|
|
6233
|
+
const terminal = observeGit ? await this.getTerminal(terminalId) : this.terminalStates.get(terminalId) ?? await this.getTerminalFromBindings(terminalId);
|
|
5916
6234
|
const worktree = await this.getWorktree(terminal.worktreeId);
|
|
5917
6235
|
const state = await this.deps.tmux.sessionState(worktree.tmuxSocketName, terminal.tmuxSessionName);
|
|
5918
6236
|
await this.requireOpenProject(worktree.projectId);
|
|
@@ -5983,7 +6301,7 @@ var TreeportService = class {
|
|
|
5983
6301
|
}
|
|
5984
6302
|
async executeDeleteTerminal(terminalId, worktreeId) {
|
|
5985
6303
|
const worktree = await this.storedWorktree(worktreeId);
|
|
5986
|
-
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "
|
|
6304
|
+
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
5987
6305
|
await this.requireOpenProject(worktree.projectId);
|
|
5988
6306
|
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
6307
|
this.worktreeLocks.add(worktree.id);
|
|
@@ -5991,7 +6309,7 @@ var TreeportService = class {
|
|
|
5991
6309
|
const terminals = await this.listWorktreeTerminals(worktree);
|
|
5992
6310
|
const terminal = terminals.find((candidate) => candidate.id === terminalId);
|
|
5993
6311
|
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
|
|
6312
|
+
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
6313
|
await this.deps.tmux.killSession(worktree.tmuxSocketName, terminal.tmuxSessionName, terminal.id, { preserveServer: true });
|
|
5996
6314
|
} finally {
|
|
5997
6315
|
this.worktreeLocks.delete(worktree.id);
|
|
@@ -6012,8 +6330,8 @@ var TreeportService = class {
|
|
|
6012
6330
|
if (!force && age < 6e4) return worktree.pr;
|
|
6013
6331
|
await this.requireOpenProject(worktree.projectId);
|
|
6014
6332
|
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
|
|
6333
|
+
if (!await this.storedWorktree(worktreeId)) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
6334
|
+
if (this.worktreeLocks.has(worktreeId)) throw new DomainError("WORKTREE_UNAVAILABLE", "Cannot refresh a pull request while the tree is being removed", 409);
|
|
6017
6335
|
await this.deps.database.db.run(sql`
|
|
6018
6336
|
UPDATE worktrees
|
|
6019
6337
|
SET pr_state=${pr.state},pr_number=${pr.number},pr_url=${pr.url},
|
|
@@ -6030,6 +6348,7 @@ var TreeportService = class {
|
|
|
6030
6348
|
const worktree = await this.requireAvailableWorktree(worktreeId, true);
|
|
6031
6349
|
worktree.terminals = await this.listWorktreeTerminals(worktree);
|
|
6032
6350
|
const project = await this.getProject(worktree.projectId);
|
|
6351
|
+
if (project.kind === "folder") throw new DomainError("FOLDER_WORKSPACE_NOT_REMOVABLE", "Remove the folder project instead of its folder workspace", 409);
|
|
6033
6352
|
const live = (await this.deps.git.listWorktrees(project.repositoryPath)).find((item) => item.path === worktree.path);
|
|
6034
6353
|
if (!live) throw new DomainError("WORKTREE_NOT_FOUND", "Git no longer reports this worktree", 404);
|
|
6035
6354
|
const head = live.head ?? worktree.head;
|
|
@@ -6049,7 +6368,7 @@ var TreeportService = class {
|
|
|
6049
6368
|
const reasons = [];
|
|
6050
6369
|
const warnings = [];
|
|
6051
6370
|
if (worktree.kind === "main") reasons.push("The main checkout cannot be removed");
|
|
6052
|
-
if (live.locked) reasons.push(live.lockReason ? `The
|
|
6371
|
+
if (live.locked) reasons.push(live.lockReason ? `The tree is locked: ${live.lockReason}` : "The tree is locked");
|
|
6053
6372
|
if (dirty.staged) warnings.push(`${dirty.staged} staged change(s) will be lost`);
|
|
6054
6373
|
if (dirty.unstaged) warnings.push(`${dirty.unstaged} unstaged change(s) will be lost`);
|
|
6055
6374
|
if (dirty.untracked) warnings.push(`${dirty.untracked} untracked file(s) will be lost`);
|
|
@@ -6098,25 +6417,25 @@ var TreeportService = class {
|
|
|
6098
6417
|
AND status IN ('pending','running')
|
|
6099
6418
|
LIMIT 1
|
|
6100
6419
|
`);
|
|
6101
|
-
if (activeRemoval) throw new DomainError("REMOVE_IN_PROGRESS", "The
|
|
6420
|
+
if (activeRemoval) throw new DomainError("REMOVE_IN_PROGRESS", "The tree is already being removed", 409);
|
|
6102
6421
|
if (this.terminalMutations.has(worktreeId)) return this.terminalMutations.enqueue(worktreeId, () => {
|
|
6103
6422
|
if (this.worktreeMutations.has(worktree.projectId)) return this.worktreeMutations.enqueue(worktree.projectId, () => this.acceptRemove(worktreeId, request));
|
|
6104
6423
|
return this.acceptRemove(worktreeId, request);
|
|
6105
6424
|
});
|
|
6106
6425
|
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
|
|
6426
|
+
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
6427
|
return this.acceptRemove(worktreeId, request);
|
|
6109
6428
|
}
|
|
6110
6429
|
async acceptRemove(worktreeId, request) {
|
|
6111
6430
|
const worktree = await this.getWorktree(worktreeId);
|
|
6112
6431
|
await this.requireOpenProject(worktree.projectId);
|
|
6113
|
-
if (this.worktreeLocks.has(worktreeId) || this.projectLocks.has(worktree.projectId)) throw new DomainError("REMOVE_IN_PROGRESS", "The
|
|
6432
|
+
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
6433
|
this.worktreeLocks.add(worktreeId);
|
|
6115
6434
|
let operationStarted = false;
|
|
6116
6435
|
try {
|
|
6117
6436
|
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
|
|
6437
|
+
if (!preview.eligible) throw new DomainError("REMOVE_REFUSED", "The tree cannot be removed", 409, preview);
|
|
6438
|
+
if (request.confirmationToken !== preview.confirmationToken) throw new DomainError("REMOVE_PREVIEW_STALE", "The tree changed after the removal preview; review it again", 409, preview);
|
|
6120
6439
|
if (preview.warnings.length > 0 && !request.confirmDestructive) throw new DomainError("REMOVE_CONFIRMATION_REQUIRED", "Confirm the destructive removal after reviewing its warnings", 409, preview);
|
|
6121
6440
|
const checkout = await this.checkoutStat(preview.path);
|
|
6122
6441
|
const [checkoutBinding] = await this.deps.database.db.all(sql`
|
|
@@ -6129,11 +6448,11 @@ var TreeportService = class {
|
|
|
6129
6448
|
const operationId = id("op");
|
|
6130
6449
|
let checkoutIdentity = null;
|
|
6131
6450
|
if (prunable) {
|
|
6132
|
-
if (!checkoutBinding?.git_worktree_key) throw new DomainError("REMOVE_PREVIEW_STALE", "The prunable
|
|
6451
|
+
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
6452
|
} else {
|
|
6134
6453
|
const markerPath = path.join(preview.path, ".git");
|
|
6135
6454
|
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
|
|
6455
|
+
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
6456
|
checkoutIdentity = {
|
|
6138
6457
|
path: preview.path,
|
|
6139
6458
|
device: checkout.dev.toString(),
|
|
@@ -6212,12 +6531,12 @@ var TreeportService = class {
|
|
|
6212
6531
|
try {
|
|
6213
6532
|
const liveWorktrees = await this.deps.git.listWorktrees(project.repositoryPath);
|
|
6214
6533
|
const acceptedKey = request.gitWorktreeKey;
|
|
6215
|
-
const liveAccepted = liveWorktrees.find((item) => item.path === preview.path && (request.prunable ? item.prunable :
|
|
6534
|
+
const liveAccepted = liveWorktrees.find((item) => item.path === preview.path && (request.prunable ? item.prunable : acceptedKey !== null && item.gitWorktreeKey === acceptedKey));
|
|
6216
6535
|
const liveRepositoryIdentity = await this.deps.git.repositoryIdentity(project.repositoryPath);
|
|
6217
6536
|
if (liveAccepted) {
|
|
6218
6537
|
if (!request.repositoryIdentity || liveRepositoryIdentity !== request.repositoryIdentity) throw new Error("Removal revalidation failed before destructive effects: the repository identity changed after removal was accepted");
|
|
6219
6538
|
if (request.prunable) {
|
|
6220
|
-
if (!liveAccepted.prunable) throw new Error("Removal revalidation failed before destructive effects: the accepted
|
|
6539
|
+
if (!liveAccepted.prunable) throw new Error("Removal revalidation failed before destructive effects: the accepted tree is no longer prunable");
|
|
6221
6540
|
} else {
|
|
6222
6541
|
const authorizationError = await this.authorizedCheckoutError(preview.path, request.checkoutIdentity);
|
|
6223
6542
|
if (authorizationError) throw new Error(`Removal revalidation failed before destructive effects: ${authorizationError}`);
|
|
@@ -6350,21 +6669,22 @@ var TreeportService = class {
|
|
|
6350
6669
|
async deleteProject(projectId) {
|
|
6351
6670
|
if (this.projectLocks.has(projectId) || this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
|
|
6352
6671
|
let project = await this.getProject(projectId);
|
|
6353
|
-
if (project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project
|
|
6672
|
+
if (project.worktrees.some((worktree) => this.worktreeLocks.has(worktree.id))) throw new DomainError("PROJECT_BUSY", "A project tree is already being modified", 409);
|
|
6354
6673
|
this.projectLocks.add(projectId);
|
|
6355
6674
|
const lockedWorktrees = [];
|
|
6356
6675
|
try {
|
|
6357
6676
|
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
|
|
6677
|
+
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
6678
|
for (const worktree of project.worktrees) {
|
|
6360
6679
|
this.worktreeLocks.add(worktree.id);
|
|
6361
6680
|
lockedWorktrees.push(worktree.id);
|
|
6362
6681
|
}
|
|
6363
6682
|
project = await this.getProject(projectId);
|
|
6364
|
-
if (project.worktrees.filter((worktree) => worktree.kind === "linked").length) throw new DomainError("PROJECT_HAS_WORKTREES", "Remove linked
|
|
6683
|
+
if (project.worktrees.filter((worktree) => worktree.kind === "linked").length) throw new DomainError("PROJECT_HAS_WORKTREES", "Remove linked trees before unregistering the project", 409);
|
|
6365
6684
|
const terminalIdsByWorktree = /* @__PURE__ */ new Map();
|
|
6366
6685
|
for (const worktree of project.worktrees) terminalIdsByWorktree.set(worktree.id, await this.deps.tmux.killServer(worktree.tmuxSocketName));
|
|
6367
6686
|
await this.deps.database.db.run(sql`DELETE FROM projects WHERE id=${projectId}`);
|
|
6687
|
+
this.observedFolderIdentities.delete(projectId);
|
|
6368
6688
|
this.packages.forgetProject(projectId);
|
|
6369
6689
|
for (const worktree of project.worktrees) this.clearWorktreeTerminalState(worktree.id, terminalIdsByWorktree.get(worktree.id));
|
|
6370
6690
|
this.invalidateProjectsSnapshot();
|
|
@@ -6391,7 +6711,7 @@ var TreeportService = class {
|
|
|
6391
6711
|
async reconcile() {
|
|
6392
6712
|
const availableProjects = /* @__PURE__ */ new Set();
|
|
6393
6713
|
for (const project of await this.storedProjects(true)) try {
|
|
6394
|
-
await this.
|
|
6714
|
+
await this.observeAvailableProject(project);
|
|
6395
6715
|
availableProjects.add(project.id);
|
|
6396
6716
|
} catch {}
|
|
6397
6717
|
for (const project of await this.storedProjects(true)) {
|
|
@@ -6598,10 +6918,12 @@ var TmuxControlParser = class {
|
|
|
6598
6918
|
const text = ascii(line);
|
|
6599
6919
|
if (/^%(?:begin|end|error)(?: |$)/.test(text)) throw new TmuxControlProtocolError("Unexpected or malformed command guard");
|
|
6600
6920
|
const lifecycle = text.match(/^%(pause|continue) (%\d+)$/);
|
|
6601
|
-
|
|
6921
|
+
const lifecycleType = lifecycle?.[1];
|
|
6922
|
+
const lifecyclePaneId = lifecycle?.[2];
|
|
6923
|
+
if ((lifecycleType === "pause" || lifecycleType === "continue") && lifecyclePaneId) {
|
|
6602
6924
|
events.push({
|
|
6603
|
-
type:
|
|
6604
|
-
paneId:
|
|
6925
|
+
type: lifecycleType,
|
|
6926
|
+
paneId: lifecyclePaneId
|
|
6605
6927
|
});
|
|
6606
6928
|
return;
|
|
6607
6929
|
}
|
|
@@ -6757,6 +7079,7 @@ async function terminateProcess(child, terminationStarted) {
|
|
|
6757
7079
|
var TmuxProgressObserver = class {
|
|
6758
7080
|
options;
|
|
6759
7081
|
spawnProcess;
|
|
7082
|
+
closed;
|
|
6760
7083
|
lifecycleFiber;
|
|
6761
7084
|
process = null;
|
|
6762
7085
|
metadataParser = null;
|
|
@@ -6771,6 +7094,7 @@ var TmuxProgressObserver = class {
|
|
|
6771
7094
|
this.options = options;
|
|
6772
7095
|
this.spawnProcess = spawnProcess;
|
|
6773
7096
|
this.lifecycleFiber = Effect.runFork(Effect.scoped(this.lifecycle()).pipe(Effect.catchAll(() => Effect.sync(() => this.notifyExit()))));
|
|
7097
|
+
this.closed = Effect.runPromise(Fiber.await(this.lifecycleFiber)).then(() => void 0);
|
|
6774
7098
|
}
|
|
6775
7099
|
dispose() {
|
|
6776
7100
|
if (this.disposed) return;
|
|
@@ -6927,22 +7251,6 @@ const PROGRAM_COMMANDS = /* @__PURE__ */ new Map([
|
|
|
6927
7251
|
["claude", "claude"],
|
|
6928
7252
|
["codex", "codex"]
|
|
6929
7253
|
]);
|
|
6930
|
-
const SHELL_COMMANDS = /* @__PURE__ */ new Set([
|
|
6931
|
-
"ash",
|
|
6932
|
-
"bash",
|
|
6933
|
-
"csh",
|
|
6934
|
-
"dash",
|
|
6935
|
-
"elvish",
|
|
6936
|
-
"fish",
|
|
6937
|
-
"ksh",
|
|
6938
|
-
"mksh",
|
|
6939
|
-
"nu",
|
|
6940
|
-
"pwsh",
|
|
6941
|
-
"sh",
|
|
6942
|
-
"tcsh",
|
|
6943
|
-
"xonsh",
|
|
6944
|
-
"zsh"
|
|
6945
|
-
]);
|
|
6946
7254
|
var TerminalMetadataRuntimeError = class {
|
|
6947
7255
|
phase;
|
|
6948
7256
|
terminalId;
|
|
@@ -6969,6 +7277,7 @@ var TerminalMetadataManager = class {
|
|
|
6969
7277
|
bellMutations = new KeyedTaskQueue();
|
|
6970
7278
|
bellDeletionVersions = /* @__PURE__ */ new Map();
|
|
6971
7279
|
persistedBells = /* @__PURE__ */ new Map();
|
|
7280
|
+
observerShutdowns = /* @__PURE__ */ new Set();
|
|
6972
7281
|
bellStateStore;
|
|
6973
7282
|
initializePromise = null;
|
|
6974
7283
|
unsubscribeEvents = null;
|
|
@@ -7034,8 +7343,10 @@ var TerminalMetadataManager = class {
|
|
|
7034
7343
|
entry = void 0;
|
|
7035
7344
|
}
|
|
7036
7345
|
if (!entry) {
|
|
7037
|
-
const launchCommand = path.basename(terminal.argv
|
|
7038
|
-
const launchProgram = PROGRAM_COMMANDS.get(launchCommand) ?? null;
|
|
7346
|
+
const launchCommand = path.basename(terminal.argv[0] ?? "").replace(/^-/, "");
|
|
7347
|
+
const launchProgram = !terminal.interactiveShell && terminal.shellCommand === null ? PROGRAM_COMMANDS.get(launchCommand) ?? null : null;
|
|
7348
|
+
const interactiveShellCommand = terminal.interactiveShell ? launchCommand : null;
|
|
7349
|
+
const launchCommandLine = terminal.interactiveShell ? null : (terminal.shellCommand?.replace(/\p{Cc}/gu, "") ?? formatCommandLine(terminal.argv.map((value) => value.replace(/\p{Cc}/gu, "")))).trim().slice(0, 256) || null;
|
|
7039
7350
|
this.bellDeletionVersions.set(terminal.id, (this.bellDeletionVersions.get(terminal.id) ?? 0) + 1);
|
|
7040
7351
|
const persistedBell = this.persistedBells.get(terminal.id);
|
|
7041
7352
|
const bell = persistedBell?.worktreeId === terminal.worktreeId ? {
|
|
@@ -7060,7 +7371,8 @@ var TerminalMetadataManager = class {
|
|
|
7060
7371
|
paneTitle: null,
|
|
7061
7372
|
currentCommand: null,
|
|
7062
7373
|
commandLine: null,
|
|
7063
|
-
|
|
7374
|
+
launchCommandLine,
|
|
7375
|
+
interactiveShellCommand,
|
|
7064
7376
|
launchProgram,
|
|
7065
7377
|
shellTitle: null,
|
|
7066
7378
|
persistedShellTitle: null,
|
|
@@ -7164,8 +7476,9 @@ var TerminalMetadataManager = class {
|
|
|
7164
7476
|
this.listeners.clear();
|
|
7165
7477
|
this.historyListeners.clear();
|
|
7166
7478
|
}
|
|
7167
|
-
drain() {
|
|
7168
|
-
|
|
7479
|
+
async drain() {
|
|
7480
|
+
await this.bellMutations.drain();
|
|
7481
|
+
await Promise.allSettled([...this.observerShutdowns]);
|
|
7169
7482
|
}
|
|
7170
7483
|
handleProductEvent(event) {
|
|
7171
7484
|
if (event.type === "terminal.removed") {
|
|
@@ -7333,7 +7646,7 @@ var TerminalMetadataManager = class {
|
|
|
7333
7646
|
this.update(entry, { progress: null });
|
|
7334
7647
|
}
|
|
7335
7648
|
});
|
|
7336
|
-
if (exited || this.entries.get(entry.terminalId) !== entry || entry.runtimeGeneration !== runtimeGeneration || entry.observerVersion !== version)
|
|
7649
|
+
if (exited || this.entries.get(entry.terminalId) !== entry || entry.runtimeGeneration !== runtimeGeneration || entry.observerVersion !== version) this.disposeObserver(observer);
|
|
7337
7650
|
else entry.observer = observer;
|
|
7338
7651
|
},
|
|
7339
7652
|
catch: (cause) => new TerminalMetadataRuntimeError("create_observer", entry.terminalId, cause)
|
|
@@ -7362,17 +7675,31 @@ var TerminalMetadataManager = class {
|
|
|
7362
7675
|
}
|
|
7363
7676
|
this.update(entry, { progress: null });
|
|
7364
7677
|
}
|
|
7678
|
+
disposeObserver(observer) {
|
|
7679
|
+
observer.dispose();
|
|
7680
|
+
if (!observer.closed) return;
|
|
7681
|
+
const shutdown = observer.closed;
|
|
7682
|
+
this.observerShutdowns.add(shutdown);
|
|
7683
|
+
shutdown.then(() => this.observerShutdowns.delete(shutdown), () => this.observerShutdowns.delete(shutdown));
|
|
7684
|
+
}
|
|
7365
7685
|
releaseRuntimeResources(entry) {
|
|
7366
7686
|
entry.observerVersion += 1;
|
|
7367
|
-
entry.observer
|
|
7368
|
-
|
|
7687
|
+
if (entry.observer) {
|
|
7688
|
+
this.disposeObserver(entry.observer);
|
|
7689
|
+
entry.observer = null;
|
|
7690
|
+
}
|
|
7369
7691
|
this.clearProgressRuntime(entry);
|
|
7370
7692
|
entry.shellTitleWriting = false;
|
|
7371
7693
|
}
|
|
7372
7694
|
reconcileTitleState(entry, state) {
|
|
7373
7695
|
const paneTitle = state.paneTitle?.trim().slice(0, 256) || null;
|
|
7374
7696
|
const currentCommand = state.currentCommand?.trim().slice(0, 256) || null;
|
|
7375
|
-
const
|
|
7697
|
+
const fallbackShellCommand = path.basename(state.fallbackShell ?? "").replace(/^-/, "").replace(/\p{Cc}/gu, "").trim().slice(0, 256);
|
|
7698
|
+
if (fallbackShellCommand) {
|
|
7699
|
+
entry.launchCommandLine = null;
|
|
7700
|
+
entry.interactiveShellCommand = fallbackShellCommand;
|
|
7701
|
+
}
|
|
7702
|
+
const commandLine = state.commandLine?.trim().slice(0, 256) || entry.launchCommandLine;
|
|
7376
7703
|
const previousCommand = entry.currentCommand;
|
|
7377
7704
|
const previousCommandLine = entry.commandLine;
|
|
7378
7705
|
const paneTitleChanged = paneTitle !== entry.paneTitle;
|
|
@@ -7392,12 +7719,8 @@ var TerminalMetadataManager = class {
|
|
|
7392
7719
|
const commandToken = commandLine?.match(/^(?:exec\s+|command\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))/);
|
|
7393
7720
|
const commandExecutable = commandToken ? commandToken[1] || commandToken[2] || commandToken[3] || null : null;
|
|
7394
7721
|
const observedProgram = PROGRAM_COMMANDS.get(path.basename(commandExecutable ?? "").replace(/^-/, "")) ?? PROGRAM_COMMANDS.get(path.basename(currentCommand ?? "").replace(/^-/, "")) ?? null;
|
|
7395
|
-
this.update(entry, { program: observedProgram ?? (!entry.
|
|
7396
|
-
if (
|
|
7397
|
-
this.update(entry, { title: paneTitle ?? commandLine ?? currentCommand });
|
|
7398
|
-
return;
|
|
7399
|
-
}
|
|
7400
|
-
if (currentCommand === entry.shellCommand) {
|
|
7722
|
+
this.update(entry, { program: observedProgram ?? (!entry.interactiveShellCommand ? entry.launchProgram : null) });
|
|
7723
|
+
if (entry.interactiveShellCommand && currentCommand === entry.interactiveShellCommand) {
|
|
7401
7724
|
const applicationTitleWasActive = entry.applicationTitleActive;
|
|
7402
7725
|
const freshShellTitle = observedTitlePending || previousCommand !== null && paneTitleChanged;
|
|
7403
7726
|
entry.applicationTitleActive = false;
|
|
@@ -7411,11 +7734,15 @@ var TerminalMetadataManager = class {
|
|
|
7411
7734
|
}
|
|
7412
7735
|
if (commandLine) {
|
|
7413
7736
|
if (observedTitlePending) entry.applicationTitleActive = paneTitle !== null && paneTitle !== commandLine;
|
|
7414
|
-
else if (commandLineChanged) entry.applicationTitleActive = previousCommand === null && paneTitle !== null && paneTitle !== commandLine && paneTitle !== entry.shellTitle;
|
|
7737
|
+
else if (commandLineChanged) entry.applicationTitleActive = entry.interactiveShellCommand !== null && previousCommand === null && paneTitle !== null && paneTitle !== commandLine && paneTitle !== entry.shellTitle;
|
|
7415
7738
|
else if (paneTitleChanged && paneTitle !== commandLine) entry.applicationTitleActive = true;
|
|
7416
7739
|
this.update(entry, { title: entry.applicationTitleActive ? paneTitle ?? entry.title ?? commandLine : commandLine });
|
|
7417
7740
|
return;
|
|
7418
7741
|
}
|
|
7742
|
+
if (!entry.interactiveShellCommand) {
|
|
7743
|
+
this.update(entry, { title: paneTitle ?? currentCommand });
|
|
7744
|
+
return;
|
|
7745
|
+
}
|
|
7419
7746
|
if (observedTitlePending) {
|
|
7420
7747
|
entry.applicationTitleActive = true;
|
|
7421
7748
|
this.update(entry, { title: entry.title ?? paneTitle ?? currentCommand });
|
|
@@ -7444,7 +7771,7 @@ var TerminalMetadataManager = class {
|
|
|
7444
7771
|
}
|
|
7445
7772
|
updateForegroundProcess(entry, currentCommand) {
|
|
7446
7773
|
const command = currentCommand?.trim().slice(0, 256) || null;
|
|
7447
|
-
this.update(entry, { hasForegroundProcess: entry.status !== "running" ? false : command === null ? null : command !== entry.
|
|
7774
|
+
this.update(entry, { hasForegroundProcess: entry.status !== "running" ? false : command === null ? null : command !== entry.interactiveShellCommand });
|
|
7448
7775
|
}
|
|
7449
7776
|
persistShellTitle(entry, runtimeGeneration) {
|
|
7450
7777
|
return Effect.gen(this, function* () {
|
|
@@ -7505,21 +7832,32 @@ var TerminalMetadataManager = class {
|
|
|
7505
7832
|
};
|
|
7506
7833
|
//#endregion
|
|
7507
7834
|
//#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
|
-
|
|
7835
|
+
const UPLOAD_MIME_EXTENSIONS = /* @__PURE__ */ new Map([
|
|
7836
|
+
["application/pdf", "pdf"],
|
|
7837
|
+
["image/gif", "gif"],
|
|
7838
|
+
["image/jpeg", "jpg"],
|
|
7839
|
+
["image/png", "png"],
|
|
7840
|
+
["image/svg+xml", "svg"],
|
|
7841
|
+
["image/webp", "webp"],
|
|
7842
|
+
["text/plain", "txt"]
|
|
7843
|
+
]);
|
|
7517
7844
|
const UPLOAD_RETENTION_MS = 1440 * 6e4;
|
|
7518
7845
|
const UPLOAD_DIRECTORY_MAX_BYTES = 512 * 1024 * 1024;
|
|
7519
7846
|
const terminalPresetDefinitionsQuerySchema = z.object({
|
|
7520
7847
|
projectId: z.string().optional(),
|
|
7521
7848
|
worktreeId: z.string().optional()
|
|
7522
7849
|
});
|
|
7850
|
+
const operationQuerySchema = z.object({
|
|
7851
|
+
kind: z.enum([
|
|
7852
|
+
"create",
|
|
7853
|
+
"finish",
|
|
7854
|
+
"discard",
|
|
7855
|
+
"project_cleanup",
|
|
7856
|
+
"remove",
|
|
7857
|
+
"external_remove"
|
|
7858
|
+
]).optional(),
|
|
7859
|
+
projectId: z.string().optional()
|
|
7860
|
+
});
|
|
7523
7861
|
const discardStoredDataQuerySchema = z.object({ discardStoredData: z.string().optional() });
|
|
7524
7862
|
async function pruneTerminalUploads(directory, preservePath) {
|
|
7525
7863
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
@@ -7557,7 +7895,7 @@ function queryInput(schema) {
|
|
|
7557
7895
|
if (!result.success) throw new DomainError("VALIDATION_ERROR", "Request validation failed", 400, z.flattenError(result.error));
|
|
7558
7896
|
});
|
|
7559
7897
|
}
|
|
7560
|
-
function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
7898
|
+
function createApp({ service, config, tmux, applicationUpdate, terminalMetadata, webDist }) {
|
|
7561
7899
|
const app = new Hono();
|
|
7562
7900
|
app.use("/api/*", requestId({
|
|
7563
7901
|
limitLength: 128,
|
|
@@ -7573,11 +7911,14 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7573
7911
|
code: "INVALID_JSON",
|
|
7574
7912
|
message: "Request body must be valid JSON"
|
|
7575
7913
|
} }, 400);
|
|
7576
|
-
if (error instanceof DomainError)
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7914
|
+
if (error instanceof DomainError) {
|
|
7915
|
+
const body = { error: {
|
|
7916
|
+
code: error.code,
|
|
7917
|
+
message: error.message
|
|
7918
|
+
} };
|
|
7919
|
+
if (error.details !== void 0) body.error.details = error.details;
|
|
7920
|
+
return context.json(body, error.status);
|
|
7921
|
+
}
|
|
7581
7922
|
const requestIdentifier = context.get("requestId") || crypto.randomUUID();
|
|
7582
7923
|
context.header("X-Request-Id", requestIdentifier);
|
|
7583
7924
|
console.error("[Treeport] API request failed", {
|
|
@@ -7604,7 +7945,13 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7604
7945
|
installationMethod: config.installationMethod ?? "development",
|
|
7605
7946
|
daemonLifecycle: config.daemonLifecycle,
|
|
7606
7947
|
url: config.apiUrl
|
|
7607
|
-
})).get("/api/
|
|
7948
|
+
})).get("/api/update", async (context) => {
|
|
7949
|
+
context.header("Cache-Control", "no-store");
|
|
7950
|
+
return context.json(await applicationUpdate.status());
|
|
7951
|
+
}).post("/api/update", async (context) => {
|
|
7952
|
+
context.header("Cache-Control", "no-store");
|
|
7953
|
+
return context.json(await applicationUpdate.start(), 202);
|
|
7954
|
+
}).get("/api/terminal-presets", async (context) => context.json({ presets: await service.listTerminalPresets() })).get("/api/terminal-preset-definitions", queryInput(terminalPresetDefinitionsQuerySchema), async (context) => context.json(await service.listTerminalPresetDefinitions(context.req.valid("query")))).post("/api/terminal-presets", jsonInput(createTerminalPresetSchema), async (context) => {
|
|
7608
7955
|
const body = context.req.valid("json");
|
|
7609
7956
|
return context.json({ preset: await service.createTerminalPreset(body) }, 201);
|
|
7610
7957
|
}).patch("/api/terminal-presets/:presetId", jsonInput(updateTerminalPresetSchema), async (context) => {
|
|
@@ -7636,6 +7983,9 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7636
7983
|
}).post("/api/projects/:projectId/open", async (context) => context.json({ project: await service.openProject(context.req.param("projectId")) })).post("/api/projects/:projectId/close", async (context) => {
|
|
7637
7984
|
await service.closeProject(context.req.param("projectId"));
|
|
7638
7985
|
return context.json({ ok: true });
|
|
7986
|
+
}).delete("/api/projects/:projectId/recent", async (context) => {
|
|
7987
|
+
await service.dismissRecentProject(context.req.param("projectId"));
|
|
7988
|
+
return context.json({ ok: true });
|
|
7639
7989
|
}).get("/api/projects/:projectId", async (context) => context.json({ project: await service.getProjectSnapshot(context.req.param("projectId")) })).patch("/api/projects/:projectId", jsonInput(updateProjectSchema), async (context) => {
|
|
7640
7990
|
const body = context.req.valid("json");
|
|
7641
7991
|
const projectId = context.req.param("projectId");
|
|
@@ -7650,17 +8000,21 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7650
8000
|
return context.json({ ok: true });
|
|
7651
8001
|
}).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
8002
|
const body = context.req.valid("json");
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
8003
|
+
let initialTerminal;
|
|
8004
|
+
if (body.initialTerminal) {
|
|
8005
|
+
initialTerminal = { name: body.initialTerminal.name };
|
|
8006
|
+
if (body.initialTerminal.argv) initialTerminal.argv = body.initialTerminal.argv;
|
|
8007
|
+
if (body.initialTerminal.returnToShell) initialTerminal.returnToShell = true;
|
|
8008
|
+
if (body.initialTerminal.initialSize) initialTerminal.initialSize = body.initialTerminal.initialSize;
|
|
8009
|
+
}
|
|
7659
8010
|
return context.json({ operation: await service.beginCreateWorktree(context.req.param("projectId"), body.name, body.base, initialTerminal, body.sourceWorktreeId) }, 202);
|
|
7660
8011
|
}).get("/api/worktrees/:worktreeId", async (context) => {
|
|
7661
8012
|
const worktreeId = context.req.param("worktreeId");
|
|
7662
8013
|
await service.refreshPr(worktreeId, false);
|
|
7663
8014
|
return context.json({ worktree: await service.getWorktreeSnapshot(worktreeId) });
|
|
8015
|
+
}).post("/api/worktrees/:worktreeId/open", jsonInput(requestWorkspaceOpenSchema), async (context) => {
|
|
8016
|
+
await service.requestWorkspaceOpen(context.req.param("worktreeId"), context.req.valid("json").sourceTerminalId);
|
|
8017
|
+
return context.json({ ok: true });
|
|
7664
8018
|
}).get("/api/worktrees/:worktreeId/web-panel-definitions", async (context) => context.json({ definitions: await service.listWebPanelDefinitions(context.req.param("worktreeId")) })).post("/api/worktrees/:worktreeId/panels", jsonInput(createWebPanelSchema), async (context) => {
|
|
7665
8019
|
const body = context.req.valid("json");
|
|
7666
8020
|
return context.json({ panel: await service.createWebPanel(context.req.param("worktreeId"), body.definitionId, {
|
|
@@ -7712,22 +8066,23 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7712
8066
|
}
|
|
7713
8067
|
const extension = path.extname(resolution.path).toLowerCase();
|
|
7714
8068
|
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
|
-
|
|
8069
|
+
const mimeTypes = /* @__PURE__ */ new Map([
|
|
8070
|
+
[".css", "text/css; charset=utf-8"],
|
|
8071
|
+
[".gif", "image/gif"],
|
|
8072
|
+
[".html", "text/html; charset=utf-8"],
|
|
8073
|
+
[".jpeg", "image/jpeg"],
|
|
8074
|
+
[".jpg", "image/jpeg"],
|
|
8075
|
+
[".js", "text/javascript; charset=utf-8"],
|
|
8076
|
+
[".json", "application/json; charset=utf-8"],
|
|
8077
|
+
[".map", "application/json; charset=utf-8"],
|
|
8078
|
+
[".mjs", "text/javascript; charset=utf-8"],
|
|
8079
|
+
[".png", "image/png"],
|
|
8080
|
+
[".svg", "image/svg+xml"],
|
|
8081
|
+
[".webp", "image/webp"],
|
|
8082
|
+
[".woff", "font/woff"],
|
|
8083
|
+
[".woff2", "font/woff2"]
|
|
8084
|
+
]);
|
|
8085
|
+
context.header("content-type", mimeTypes.get(extension) ?? "application/octet-stream");
|
|
7731
8086
|
context.header("cache-control", "public, max-age=31536000, immutable");
|
|
7732
8087
|
context.header("access-control-allow-origin", "*");
|
|
7733
8088
|
context.header("content-security-policy", webPanelContentSecurityPolicy("immutable", browserOrigin, resolution.allowNetworkRequests));
|
|
@@ -7735,13 +8090,14 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7735
8090
|
return context.body(body);
|
|
7736
8091
|
}).post("/api/worktrees/:worktreeId/terminals", jsonInput(createTerminalSchema), async (context) => {
|
|
7737
8092
|
const body = context.req.valid("json");
|
|
7738
|
-
const
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
8093
|
+
const options = {};
|
|
8094
|
+
if (body.returnToShell) options.returnToShell = true;
|
|
8095
|
+
if (body.closeOnSuccess) options.closeOnSuccess = true;
|
|
8096
|
+
if (body.initialSize) options.initialSize = body.initialSize;
|
|
8097
|
+
if (body.cwd) options.cwd = body.cwd;
|
|
8098
|
+
if (body.env) options.env = body.env;
|
|
8099
|
+
if (body.shellCommand) options.shellCommand = body.shellCommand;
|
|
8100
|
+
const terminal = await service.createTerminal(context.req.param("worktreeId"), body.name, body.argv, Object.keys(options).length > 0 ? options : void 0);
|
|
7745
8101
|
return context.json({ terminal }, 201);
|
|
7746
8102
|
}).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
8103
|
const body = context.req.valid("json");
|
|
@@ -7792,7 +8148,7 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7792
8148
|
await waitForPreviousUpload;
|
|
7793
8149
|
try {
|
|
7794
8150
|
const contentType = context.req.header("content-type")?.split(";", 1)[0]?.toLowerCase() ?? "";
|
|
7795
|
-
const extension = requestedExtension || UPLOAD_MIME_EXTENSIONS
|
|
8151
|
+
const extension = requestedExtension || UPLOAD_MIME_EXTENSIONS.get(contentType) || "";
|
|
7796
8152
|
const uploadDirectory = path.join(config.runtimeDir, "uploads");
|
|
7797
8153
|
await fs.mkdir(uploadDirectory, {
|
|
7798
8154
|
recursive: true,
|
|
@@ -7830,26 +8186,15 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7830
8186
|
await service.deleteTerminal(context.req.param("terminalId"));
|
|
7831
8187
|
return context.json({ ok: true });
|
|
7832
8188
|
}).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
|
-
};
|
|
8189
|
+
const parsed = operationQuerySchema.safeParse(value);
|
|
8190
|
+
if (!parsed.success) throw new DomainError("INVALID_OPERATION_KIND", "Invalid operation query", 400);
|
|
8191
|
+
return parsed.data;
|
|
7847
8192
|
}), async (context) => {
|
|
7848
|
-
const
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
8193
|
+
const query = context.req.valid("query");
|
|
8194
|
+
const filters = {};
|
|
8195
|
+
if (query.projectId) filters.projectId = query.projectId;
|
|
8196
|
+
if (query.kind) filters.kind = query.kind;
|
|
8197
|
+
return context.json({ operations: await service.listActiveOperations(filters) });
|
|
7853
8198
|
}).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
8199
|
code: "NOT_FOUND",
|
|
7855
8200
|
message: "API endpoint not found"
|
|
@@ -7872,6 +8217,247 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7872
8217
|
return routes;
|
|
7873
8218
|
}
|
|
7874
8219
|
//#endregion
|
|
8220
|
+
//#region src/server/application-update.ts
|
|
8221
|
+
const POLL_INTERVAL_MS = 10 * 6e4;
|
|
8222
|
+
const POLL_JITTER_MS = 6e4;
|
|
8223
|
+
const updateResultSchema = z.looseObject({
|
|
8224
|
+
schemaVersion: z.literal(1),
|
|
8225
|
+
operationId: z.string().uuid(),
|
|
8226
|
+
status: z.enum(["current", "updated"]),
|
|
8227
|
+
phase: z.literal("complete"),
|
|
8228
|
+
fromVersion: z.string(),
|
|
8229
|
+
toVersion: z.string()
|
|
8230
|
+
});
|
|
8231
|
+
const updateErrorSchema = z.looseObject({ error: z.looseObject({
|
|
8232
|
+
code: z.string(),
|
|
8233
|
+
message: z.string(),
|
|
8234
|
+
details: z.looseObject({
|
|
8235
|
+
operationId: z.string().optional(),
|
|
8236
|
+
recovery: z.string().optional()
|
|
8237
|
+
}).optional()
|
|
8238
|
+
}) });
|
|
8239
|
+
async function readValidatedJson(filePath, schema) {
|
|
8240
|
+
return fs.readFile(filePath, "utf8").then((value) => schema.safeParse(JSON.parse(value))).then((result) => result.success ? result.data : null).catch(() => null);
|
|
8241
|
+
}
|
|
8242
|
+
async function fileExists(filePath) {
|
|
8243
|
+
return fs.access(filePath).then(() => true).catch(() => false);
|
|
8244
|
+
}
|
|
8245
|
+
function createApplicationUpdateManager(config, dependencies = {}) {
|
|
8246
|
+
const environment = dependencies.environment ?? process.env;
|
|
8247
|
+
const resolveRelease = dependencies.resolveRelease ?? resolveLatestTreeportRelease;
|
|
8248
|
+
const inspectInstallation = dependencies.inspectInstallation ?? inspectLocalUpdateInstallation;
|
|
8249
|
+
const readProgress = dependencies.readProgress ?? readLocalUpdateProgress;
|
|
8250
|
+
const readServiceStatus = dependencies.readServiceStatus ?? serviceStatus;
|
|
8251
|
+
const spawnProcess = dependencies.spawnProcess ?? spawn;
|
|
8252
|
+
const random = dependencies.random ?? Math.random;
|
|
8253
|
+
const pollIntervalMs = dependencies.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
8254
|
+
const pollJitterMs = dependencies.pollJitterMs ?? POLL_JITTER_MS;
|
|
8255
|
+
const updateDirectory = path.join(config.dataDir, "updates");
|
|
8256
|
+
const resultPath = path.join(updateDirectory, "web-update-result.json");
|
|
8257
|
+
const errorPath = path.join(updateDirectory, "web-update-error.json");
|
|
8258
|
+
const currentVersion = config.appVersion ?? "development";
|
|
8259
|
+
const staticBlockedReason = config.installationMethod !== "npm" ? "This Treeport installation cannot update itself. Update it with its installation method." : config.daemonLifecycle === "external" ? "This Treeport daemon is managed by another process. Update it on the host." : !isCanonicalTreeportVersion(currentVersion) ? "Development and prerelease Treeport versions do not update from the stable npm channel." : null;
|
|
8260
|
+
let latestVersion = null;
|
|
8261
|
+
let checkedAt = null;
|
|
8262
|
+
let capabilityChecked = staticBlockedReason !== null;
|
|
8263
|
+
let canUpdate = false;
|
|
8264
|
+
let blockedReason = staticBlockedReason;
|
|
8265
|
+
let installation = null;
|
|
8266
|
+
let checking = false;
|
|
8267
|
+
let checkPromise = null;
|
|
8268
|
+
let pollingStarted = false;
|
|
8269
|
+
let disposed = false;
|
|
8270
|
+
let pollTimer = null;
|
|
8271
|
+
let launching = false;
|
|
8272
|
+
let launchError = null;
|
|
8273
|
+
const refreshCapability = async () => {
|
|
8274
|
+
if (staticBlockedReason) {
|
|
8275
|
+
capabilityChecked = true;
|
|
8276
|
+
canUpdate = false;
|
|
8277
|
+
blockedReason = staticBlockedReason;
|
|
8278
|
+
installation = null;
|
|
8279
|
+
return;
|
|
8280
|
+
}
|
|
8281
|
+
const [installationResult, serviceResult] = await Promise.all([inspectInstallation(environment).then((value) => ({
|
|
8282
|
+
value,
|
|
8283
|
+
error: null
|
|
8284
|
+
}), (cause) => ({
|
|
8285
|
+
value: null,
|
|
8286
|
+
error: cause
|
|
8287
|
+
})), config.daemonLifecycle === "service" ? readServiceStatus().then((value) => ({
|
|
8288
|
+
value,
|
|
8289
|
+
error: null
|
|
8290
|
+
}), (cause) => ({
|
|
8291
|
+
value: null,
|
|
8292
|
+
error: cause
|
|
8293
|
+
})) : Promise.resolve({
|
|
8294
|
+
value: null,
|
|
8295
|
+
error: null
|
|
8296
|
+
})]);
|
|
8297
|
+
capabilityChecked = true;
|
|
8298
|
+
installation = installationResult.value;
|
|
8299
|
+
if (!installation) {
|
|
8300
|
+
canUpdate = false;
|
|
8301
|
+
blockedReason = installationResult.error instanceof Error ? installationResult.error.message : "Treeport could not verify this npm installation.";
|
|
8302
|
+
return;
|
|
8303
|
+
}
|
|
8304
|
+
if (serviceResult.error) {
|
|
8305
|
+
canUpdate = false;
|
|
8306
|
+
blockedReason = "Treeport could not verify the service update lifecycle.";
|
|
8307
|
+
return;
|
|
8308
|
+
}
|
|
8309
|
+
if (serviceResult.value?.mode === "headless" && (serviceResult.value.active || serviceResult.value.daemon?.running)) {
|
|
8310
|
+
canUpdate = false;
|
|
8311
|
+
blockedReason = "Stop the advanced headless service with its administrator action before you update Treeport.";
|
|
8312
|
+
return;
|
|
8313
|
+
}
|
|
8314
|
+
canUpdate = true;
|
|
8315
|
+
blockedReason = null;
|
|
8316
|
+
};
|
|
8317
|
+
const status = async () => {
|
|
8318
|
+
const [progress, result, updateError, resultFileExists, errorFileExists] = await Promise.all([
|
|
8319
|
+
readProgress(config.dataDir),
|
|
8320
|
+
readValidatedJson(resultPath, updateResultSchema),
|
|
8321
|
+
readValidatedJson(errorPath, updateErrorSchema),
|
|
8322
|
+
fileExists(resultPath),
|
|
8323
|
+
fileExists(errorPath)
|
|
8324
|
+
]);
|
|
8325
|
+
if (progress.active) launching = false;
|
|
8326
|
+
const resultMatchesOperation = !result || !progress.operationId || result.operationId === progress.operationId;
|
|
8327
|
+
const errorOperationId = updateError?.error.details?.operationId ?? null;
|
|
8328
|
+
const errorMatchesOperation = !updateError || !errorOperationId || !progress.operationId || errorOperationId === progress.operationId;
|
|
8329
|
+
const available = Boolean(latestVersion && isCanonicalTreeportVersion(currentVersion) && compareTreeportVersions(latestVersion, currentVersion) > 0);
|
|
8330
|
+
const recoveryError = progress.recoveryAction;
|
|
8331
|
+
const cliError = errorMatchesOperation ? updateError?.error : null;
|
|
8332
|
+
const interrupted = Boolean(!progress.active && progress.phase && progress.phase !== "complete" && (resultFileExists || errorFileExists) && !result && !updateError);
|
|
8333
|
+
const error = launchError ?? (cliError ? [cliError.message, cliError.details?.recovery].filter((value, index, values) => Boolean(value && values.indexOf(value) === index)).join(" ") : recoveryError) ?? (interrupted ? "The update process stopped before it returned a result. Retry the update or run `treeport update` on the host." : null);
|
|
8334
|
+
const inactiveFailedPhase = interrupted || progress.phase === "rollback" || progress.phase === "recovery_required";
|
|
8335
|
+
const phase = progress.active ? progress.phase ?? "starting" : launching ? "starting" : launchError || cliError || inactiveFailedPhase ? progress.phase === "recovery_required" ? "recovery_required" : "failed" : result && resultMatchesOperation ? "complete" : progress.phase === "complete" ? "complete" : checking ? "checking" : "idle";
|
|
8336
|
+
return {
|
|
8337
|
+
currentVersion,
|
|
8338
|
+
latestVersion,
|
|
8339
|
+
updateAvailable: available,
|
|
8340
|
+
checkedAt,
|
|
8341
|
+
canUpdate: canUpdate && !progress.active && !launching,
|
|
8342
|
+
blockedReason: capabilityChecked ? blockedReason : "Treeport is checking whether this installation can update itself.",
|
|
8343
|
+
phase,
|
|
8344
|
+
operationId: progress.operationId ?? result?.operationId ?? null,
|
|
8345
|
+
targetVersion: progress.toVersion ?? result?.toVersion ?? latestVersion ?? null,
|
|
8346
|
+
error: error || null
|
|
8347
|
+
};
|
|
8348
|
+
};
|
|
8349
|
+
const check = async () => {
|
|
8350
|
+
if (staticBlockedReason || disposed) return;
|
|
8351
|
+
if ((await readProgress(config.dataDir)).active) return;
|
|
8352
|
+
if (checkPromise) return checkPromise;
|
|
8353
|
+
checking = true;
|
|
8354
|
+
checkPromise = Promise.all([resolveRelease(environment).then((value) => ({
|
|
8355
|
+
value,
|
|
8356
|
+
error: null
|
|
8357
|
+
}), (cause) => ({
|
|
8358
|
+
value: null,
|
|
8359
|
+
error: cause
|
|
8360
|
+
})), refreshCapability()]).then(([releaseResult]) => {
|
|
8361
|
+
if (releaseResult.value) {
|
|
8362
|
+
latestVersion = releaseResult.value.version;
|
|
8363
|
+
checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8364
|
+
} else console.warn("[Treeport] Application update check failed:", releaseResult.error instanceof Error ? releaseResult.error.message : String(releaseResult.error));
|
|
8365
|
+
});
|
|
8366
|
+
await checkPromise.finally(() => {
|
|
8367
|
+
checking = false;
|
|
8368
|
+
checkPromise = null;
|
|
8369
|
+
});
|
|
8370
|
+
};
|
|
8371
|
+
const scheduleNextCheck = () => {
|
|
8372
|
+
if (disposed || !pollingStarted || staticBlockedReason) return;
|
|
8373
|
+
const delay = pollIntervalMs + Math.floor(random() * pollJitterMs);
|
|
8374
|
+
pollTimer = setTimeout(() => {
|
|
8375
|
+
pollTimer = null;
|
|
8376
|
+
check().finally(scheduleNextCheck);
|
|
8377
|
+
}, delay);
|
|
8378
|
+
pollTimer.unref?.();
|
|
8379
|
+
};
|
|
8380
|
+
return {
|
|
8381
|
+
status,
|
|
8382
|
+
check,
|
|
8383
|
+
beginPolling() {
|
|
8384
|
+
if (pollingStarted || disposed || staticBlockedReason) return;
|
|
8385
|
+
pollingStarted = true;
|
|
8386
|
+
check().finally(scheduleNextCheck);
|
|
8387
|
+
},
|
|
8388
|
+
async start() {
|
|
8389
|
+
const currentStatus = await status();
|
|
8390
|
+
if (!currentStatus.updateAvailable) throw new DomainError("APPLICATION_UPDATE_NOT_AVAILABLE", "A newer stable Treeport release is not available.", 409);
|
|
8391
|
+
if (launching || [
|
|
8392
|
+
"starting",
|
|
8393
|
+
"inspect",
|
|
8394
|
+
"resolve",
|
|
8395
|
+
"stage",
|
|
8396
|
+
"verify",
|
|
8397
|
+
"stop",
|
|
8398
|
+
"activate",
|
|
8399
|
+
"restart",
|
|
8400
|
+
"health_check",
|
|
8401
|
+
"rollback"
|
|
8402
|
+
].includes(currentStatus.phase)) throw new DomainError("APPLICATION_UPDATE_IN_PROGRESS", "Another Treeport update is already running.", 409);
|
|
8403
|
+
launching = true;
|
|
8404
|
+
launchError = null;
|
|
8405
|
+
const launchResult = await (async () => {
|
|
8406
|
+
await refreshCapability();
|
|
8407
|
+
if (!canUpdate || !installation) throw new DomainError("APPLICATION_UPDATE_BLOCKED", blockedReason ?? "This Treeport installation cannot update itself.", 409);
|
|
8408
|
+
const entrypoint = installation.entrypoint;
|
|
8409
|
+
await fs.mkdir(updateDirectory, {
|
|
8410
|
+
recursive: true,
|
|
8411
|
+
mode: 448
|
|
8412
|
+
});
|
|
8413
|
+
await Promise.all([fs.rm(resultPath, { force: true }), fs.rm(errorPath, { force: true })]);
|
|
8414
|
+
const [resultFile, errorFile] = await Promise.all([fs.open(resultPath, "wx", 384), fs.open(errorPath, "wx", 384)]);
|
|
8415
|
+
const spawnResult = await new Promise((resolve, reject) => {
|
|
8416
|
+
const child = spawnProcess(entrypoint, ["update", "--json"], {
|
|
8417
|
+
env: environment,
|
|
8418
|
+
detached: true,
|
|
8419
|
+
shell: false,
|
|
8420
|
+
stdio: [
|
|
8421
|
+
"ignore",
|
|
8422
|
+
resultFile.fd,
|
|
8423
|
+
errorFile.fd
|
|
8424
|
+
]
|
|
8425
|
+
});
|
|
8426
|
+
child.once("spawn", () => resolve(child));
|
|
8427
|
+
child.once("error", reject);
|
|
8428
|
+
child.once("exit", () => {
|
|
8429
|
+
launching = false;
|
|
8430
|
+
});
|
|
8431
|
+
}).then((child) => ({
|
|
8432
|
+
child,
|
|
8433
|
+
error: null
|
|
8434
|
+
}), (cause) => ({
|
|
8435
|
+
child: null,
|
|
8436
|
+
error: cause
|
|
8437
|
+
}));
|
|
8438
|
+
await Promise.all([resultFile.close(), errorFile.close()]);
|
|
8439
|
+
if (!spawnResult.child) throw spawnResult.error instanceof Error ? spawnResult.error : /* @__PURE__ */ new Error("Treeport could not start the update process.");
|
|
8440
|
+
spawnResult.child.unref();
|
|
8441
|
+
})().then(() => ({ error: null }), (cause) => ({ error: cause }));
|
|
8442
|
+
if (launchResult.error) {
|
|
8443
|
+
launching = false;
|
|
8444
|
+
if (launchResult.error instanceof DomainError) throw launchResult.error;
|
|
8445
|
+
launchError = launchResult.error instanceof Error ? launchResult.error.message : "Treeport could not start the update process.";
|
|
8446
|
+
throw new DomainError("APPLICATION_UPDATE_START_FAILED", "Treeport could not start the update process.", 500);
|
|
8447
|
+
}
|
|
8448
|
+
return status();
|
|
8449
|
+
},
|
|
8450
|
+
dispose() {
|
|
8451
|
+
disposed = true;
|
|
8452
|
+
pollingStarted = false;
|
|
8453
|
+
if (pollTimer) {
|
|
8454
|
+
clearTimeout(pollTimer);
|
|
8455
|
+
pollTimer = null;
|
|
8456
|
+
}
|
|
8457
|
+
}
|
|
8458
|
+
};
|
|
8459
|
+
}
|
|
8460
|
+
//#endregion
|
|
7875
8461
|
//#region src/server/daemon-ownership.ts
|
|
7876
8462
|
function processExists(pid) {
|
|
7877
8463
|
try {
|
|
@@ -7909,7 +8495,9 @@ async function acquireDaemonOwnership(config) {
|
|
|
7909
8495
|
});
|
|
7910
8496
|
if (!await openLock().then(() => true, async (error) => {
|
|
7911
8497
|
if (error.code !== "EEXIST") throw error;
|
|
7912
|
-
const existing = await fs.readFile(lockPath, "utf8").then((value) =>
|
|
8498
|
+
const existing = await fs.readFile(lockPath, "utf8").then((value) => {
|
|
8499
|
+
return JSON.parse(value);
|
|
8500
|
+
}).catch(() => null);
|
|
7913
8501
|
if (existing?.pid && Number.isInteger(existing.pid) && processExists(existing.pid)) throw new Error(`Treeport is already running for ${config.dataDir} (PID ${existing.pid})`);
|
|
7914
8502
|
await fs.rm(lockPath, { force: true });
|
|
7915
8503
|
return false;
|
|
@@ -7963,7 +8551,7 @@ function singleHeader(request, name) {
|
|
|
7963
8551
|
return {
|
|
7964
8552
|
present: values.length > 0,
|
|
7965
8553
|
valid: values.length <= 1,
|
|
7966
|
-
value: values.length === 1 ? values[0] : null
|
|
8554
|
+
value: values.length === 1 ? values[0] ?? null : null
|
|
7967
8555
|
};
|
|
7968
8556
|
}
|
|
7969
8557
|
function hasControlCharacters(value) {
|
|
@@ -8010,14 +8598,24 @@ function effectiveOriginFor(request, source, incomingHost) {
|
|
|
8010
8598
|
if (!forwardedHost || forwardedProtocolHeader.value?.toLowerCase() !== "https") return null;
|
|
8011
8599
|
return `https://${forwardedHost.host}`;
|
|
8012
8600
|
}
|
|
8601
|
+
function allowsOpaqueWebPanelOrigin(request, socketUpgrade) {
|
|
8602
|
+
if (!["GET", "HEAD"].includes(request.method?.toUpperCase() ?? "")) return false;
|
|
8603
|
+
const pathname = new URL(request.url ?? "/", "http://treeport.local").pathname;
|
|
8604
|
+
if (socketUpgrade) return /^\/api\/web-panel-dev\/[a-f0-9]{24}\/@vite-hmr$/u.test(pathname);
|
|
8605
|
+
return /^\/api\/web-panels\/panel_[a-f0-9]{32}\/assets(?:\/|$)/u.test(pathname) || /^\/api\/web-panel-dev\/[a-f0-9]{24}\//u.test(pathname);
|
|
8606
|
+
}
|
|
8013
8607
|
function originIsAllowed(request, effectiveOrigin, socketUpgrade) {
|
|
8014
8608
|
const originHeader = singleHeader(request, "origin");
|
|
8015
8609
|
if (!originHeader.valid) return false;
|
|
8016
8610
|
if (originHeader.present) {
|
|
8017
8611
|
const value = originHeader.value ?? "";
|
|
8018
|
-
if (value === "null"
|
|
8019
|
-
|
|
8020
|
-
|
|
8612
|
+
if (value === "null") {
|
|
8613
|
+
if (!allowsOpaqueWebPanelOrigin(request, socketUpgrade)) return false;
|
|
8614
|
+
} else {
|
|
8615
|
+
if (!URL.canParse(value)) return false;
|
|
8616
|
+
const parsed = new URL(value);
|
|
8617
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.origin !== value || parsed.origin !== effectiveOrigin) return false;
|
|
8618
|
+
}
|
|
8021
8619
|
}
|
|
8022
8620
|
const fetchSiteHeader = singleHeader(request, "sec-fetch-site");
|
|
8023
8621
|
if (!fetchSiteHeader.valid) return false;
|
|
@@ -8092,8 +8690,8 @@ var AttachmentInitializationError = class {
|
|
|
8092
8690
|
};
|
|
8093
8691
|
const TERMINAL_MAX_QUEUED_INPUT_BYTES = 1024 * 1024;
|
|
8094
8692
|
const TERMINAL_MAX_QUEUED_INPUT_MESSAGES = 256;
|
|
8095
|
-
function errorMessage(
|
|
8096
|
-
return ((
|
|
8693
|
+
function errorMessage(cause) {
|
|
8694
|
+
return ((cause instanceof Error ? cause.message : String(cause)).trim() || "Terminal attachment failed").slice(0, 1e3);
|
|
8097
8695
|
}
|
|
8098
8696
|
function tmuxEnvironment() {
|
|
8099
8697
|
return Object.fromEntries(Object.entries(process.env).filter(([key, value]) => value !== void 0 && key !== "TMUX" && key !== "TMUX_PANE"));
|
|
@@ -8274,7 +8872,7 @@ var TerminalAttachmentManager = class {
|
|
|
8274
8872
|
catch: (cause) => new AttachmentInitializationError(phase, cause)
|
|
8275
8873
|
});
|
|
8276
8874
|
return Effect.gen(this, function* () {
|
|
8277
|
-
const terminal = yield* promisePhase("refresh_terminal", () => this.service.refreshTerminalStatus(connection.terminalId));
|
|
8875
|
+
const terminal = yield* promisePhase("refresh_terminal", () => this.service.refreshTerminalStatus(connection.terminalId, false));
|
|
8278
8876
|
if (!isInitializing()) return;
|
|
8279
8877
|
if (terminal.status === "missing") return yield* Effect.fail(new AttachmentInitializationError("refresh_terminal", /* @__PURE__ */ new Error("The tmux session for this terminal is missing")));
|
|
8280
8878
|
const worktree = yield* promisePhase("resolve_worktree", () => this.service.getWorktree(terminal.worktreeId));
|
|
@@ -8283,12 +8881,12 @@ var TerminalAttachmentManager = class {
|
|
|
8283
8881
|
const initialDimensions = yield* Effect.tryPromise({
|
|
8284
8882
|
try: () => this.enqueueTerminal(connection.terminalId, async () => {
|
|
8285
8883
|
if (!isInitializing()) return null;
|
|
8884
|
+
const current = this.dimensions.get(connection.terminalId);
|
|
8885
|
+
if (current) return current;
|
|
8286
8886
|
await this.tmux.useManualWindowSize(worktree.tmuxSocketName, terminal.tmuxSessionName).catch((cause) => {
|
|
8287
8887
|
throw new AttachmentInitializationError("configure_window_size", cause);
|
|
8288
8888
|
});
|
|
8289
8889
|
if (!isInitializing()) return null;
|
|
8290
|
-
const current = this.dimensions.get(connection.terminalId);
|
|
8291
|
-
if (current) return current;
|
|
8292
8890
|
const sessionSize = await this.tmux.sessionSize(worktree.tmuxSocketName, terminal.tmuxSessionName).catch((cause) => {
|
|
8293
8891
|
throw new AttachmentInitializationError("read_session_size", cause);
|
|
8294
8892
|
});
|
|
@@ -8511,11 +9109,11 @@ var TerminalAttachmentManager = class {
|
|
|
8511
9109
|
if (this.isActive(connection) && this.canControl(connection, generation)) connection.pty?.write(data);
|
|
8512
9110
|
}).catch((error) => this.failInputWrite(connection, error));
|
|
8513
9111
|
}
|
|
8514
|
-
failInputWrite(connection,
|
|
9112
|
+
failInputWrite(connection, cause) {
|
|
8515
9113
|
if (!this.isActive(connection)) return;
|
|
8516
9114
|
this.send(connection, "terminal_error", {
|
|
8517
9115
|
code: "INPUT_FAILED",
|
|
8518
|
-
message: errorMessage(
|
|
9116
|
+
message: errorMessage(cause),
|
|
8519
9117
|
retryable: true
|
|
8520
9118
|
});
|
|
8521
9119
|
connection.transport.disconnect(true);
|
|
@@ -8551,13 +9149,13 @@ var TerminalAttachmentManager = class {
|
|
|
8551
9149
|
for (const client of active) if (this.isActive(client)) client.pty?.resize(next.cols, next.rows);
|
|
8552
9150
|
await this.tmux.resizeWindow(next.socketName, next.sessionName, next.cols, next.rows);
|
|
8553
9151
|
}
|
|
8554
|
-
failDimensionChange(terminalId,
|
|
9152
|
+
failDimensionChange(terminalId, cause) {
|
|
8555
9153
|
this.dimensions.delete(terminalId);
|
|
8556
9154
|
for (const client of [...this.clients.values()]) {
|
|
8557
9155
|
if (client.terminalId !== terminalId || !this.isActive(client)) continue;
|
|
8558
9156
|
this.send(client, "terminal_error", {
|
|
8559
9157
|
code: "RESIZE_FAILED",
|
|
8560
|
-
message: errorMessage(
|
|
9158
|
+
message: errorMessage(cause),
|
|
8561
9159
|
retryable: true
|
|
8562
9160
|
});
|
|
8563
9161
|
client.transport.disconnect(true);
|
|
@@ -8757,7 +9355,7 @@ function createSocketServer(httpServer, { service, config, tmux, terminalMetadat
|
|
|
8757
9355
|
isConnected: () => socket.connected,
|
|
8758
9356
|
send(event, payload) {
|
|
8759
9357
|
if (!socket.connected) return false;
|
|
8760
|
-
socket.emit(event, payload);
|
|
9358
|
+
socket.emit.bind(socket)(event, payload);
|
|
8761
9359
|
return true;
|
|
8762
9360
|
},
|
|
8763
9361
|
disconnect(retryable) {
|
|
@@ -8780,103 +9378,126 @@ function createSocketServer(httpServer, { service, config, tmux, terminalMetadat
|
|
|
8780
9378
|
}
|
|
8781
9379
|
//#endregion
|
|
8782
9380
|
//#region src/server/index.ts
|
|
8783
|
-
|
|
8784
|
-
const
|
|
8785
|
-
const
|
|
8786
|
-
|
|
8787
|
-
const
|
|
8788
|
-
const
|
|
8789
|
-
const
|
|
8790
|
-
|
|
8791
|
-
const
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
9381
|
+
async function main() {
|
|
9382
|
+
const config = loadConfig();
|
|
9383
|
+
const updateStartup = await createUpdateStartupReporter(config);
|
|
9384
|
+
try {
|
|
9385
|
+
const ownership = await acquireDaemonOwnership(config);
|
|
9386
|
+
const prerequisites = await checkRuntimePrerequisites(config);
|
|
9387
|
+
const runner = new SpawnCommandRunner();
|
|
9388
|
+
await updateStartup.databaseOpening();
|
|
9389
|
+
const database = await openDatabase(config.databasePath, { backupDirectory: path.join(config.dataDir, "database-backups") });
|
|
9390
|
+
await updateStartup.databaseOpened({
|
|
9391
|
+
migrationState: database.migrationState,
|
|
9392
|
+
snapshotPaths: database.migrationSnapshotPaths
|
|
9393
|
+
});
|
|
9394
|
+
const git = new GitAdapter(runner, config.gitPath);
|
|
9395
|
+
const launcherPath = fileURLToPath(new URL("./core/launcher.js", import.meta.url));
|
|
9396
|
+
const tmux = new TmuxAdapter(runner, config.runtimeDir, config.tmuxPath, launcherPath);
|
|
9397
|
+
const service = new TreeportService({
|
|
9398
|
+
config,
|
|
9399
|
+
database,
|
|
9400
|
+
runner,
|
|
9401
|
+
git,
|
|
9402
|
+
tmux,
|
|
9403
|
+
gh: new GhAdapter(runner, config.ghPath)
|
|
9404
|
+
});
|
|
9405
|
+
await service.initialize();
|
|
9406
|
+
const terminalMetadata = new TerminalMetadataManager(service, tmux, config.tmuxPath);
|
|
9407
|
+
await terminalMetadata.initialize();
|
|
9408
|
+
const applicationUpdate = createApplicationUpdateManager(config);
|
|
9409
|
+
const honoListener = getRequestListener(createApp({
|
|
9410
|
+
service,
|
|
9411
|
+
config,
|
|
9412
|
+
tmux,
|
|
9413
|
+
applicationUpdate,
|
|
9414
|
+
terminalMetadata
|
|
9415
|
+
}).fetch);
|
|
9416
|
+
let vite = null;
|
|
9417
|
+
const server = createServer((request, response) => {
|
|
9418
|
+
const security = authorizeRequest(request);
|
|
9419
|
+
if (!security.allowed) {
|
|
9420
|
+
rejectHttpRequest(request, response, security);
|
|
9421
|
+
return;
|
|
9422
|
+
}
|
|
9423
|
+
service.handleWebPanelDevelopmentRequest(request, response, () => {
|
|
9424
|
+
if (vite && !request.url?.startsWith("/api")) {
|
|
9425
|
+
vite.middlewares(request, response, () => {
|
|
9426
|
+
honoListener(request, response);
|
|
9427
|
+
});
|
|
9428
|
+
return;
|
|
9429
|
+
}
|
|
8818
9430
|
honoListener(request, response);
|
|
8819
9431
|
});
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
middlewareMode: true,
|
|
8839
|
-
hmr: { server }
|
|
9432
|
+
});
|
|
9433
|
+
server.on("upgrade", (request, socket) => {
|
|
9434
|
+
const security = authorizeRequest(request, { socketUpgrade: true });
|
|
9435
|
+
if (security.allowed) return;
|
|
9436
|
+
const statusText = security.status === 400 ? "Bad Request" : security.status === 403 ? "Forbidden" : "Unauthorized";
|
|
9437
|
+
socket.write(`HTTP/1.1 ${security.status} ${statusText}\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n`);
|
|
9438
|
+
socket.destroy();
|
|
9439
|
+
});
|
|
9440
|
+
if (config.webDevelopment) {
|
|
9441
|
+
const { createServer: createViteServer } = await import("vite");
|
|
9442
|
+
vite = await createViteServer({
|
|
9443
|
+
configFile: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../vite.config.ts"),
|
|
9444
|
+
appType: "spa",
|
|
9445
|
+
server: {
|
|
9446
|
+
middlewareMode: true,
|
|
9447
|
+
hmr: { server }
|
|
9448
|
+
}
|
|
9449
|
+
});
|
|
8840
9450
|
}
|
|
8841
|
-
|
|
8842
|
-
}
|
|
8843
|
-
service
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
tmux,
|
|
8848
|
-
terminalMetadata
|
|
8849
|
-
});
|
|
8850
|
-
await new Promise((resolve, reject) => {
|
|
8851
|
-
server.once("error", reject);
|
|
8852
|
-
server.listen(config.port, config.host, () => {
|
|
8853
|
-
server.off("error", reject);
|
|
8854
|
-
resolve();
|
|
8855
|
-
});
|
|
8856
|
-
});
|
|
8857
|
-
await ownership.publish();
|
|
8858
|
-
console.log(`Treeport ${config.appVersion} listening on ${config.apiUrl}`);
|
|
8859
|
-
console.log(`database: ${config.databasePath}`);
|
|
8860
|
-
console.log(`git: ${prerequisites.gitVersion}`);
|
|
8861
|
-
console.log(`tmux: ${prerequisites.tmuxVersion}`);
|
|
8862
|
-
let shuttingDown = false;
|
|
8863
|
-
function shutdown() {
|
|
8864
|
-
if (shuttingDown) return;
|
|
8865
|
-
shuttingDown = true;
|
|
8866
|
-
attachments.dispose();
|
|
8867
|
-
terminalMetadata.dispose();
|
|
8868
|
-
io.close(() => {
|
|
8869
|
-
Promise.all([service.drainMutations(), terminalMetadata.drain()]).then(async () => {
|
|
8870
|
-
await service.disposeWebPanelRuntime();
|
|
8871
|
-
await vite?.close();
|
|
8872
|
-
database.close();
|
|
8873
|
-
await ownership.release();
|
|
8874
|
-
process.exit(0);
|
|
9451
|
+
service.attachHttpServer(server);
|
|
9452
|
+
const { io, attachments } = createSocketServer(server, {
|
|
9453
|
+
service,
|
|
9454
|
+
config,
|
|
9455
|
+
tmux,
|
|
9456
|
+
terminalMetadata
|
|
8875
9457
|
});
|
|
8876
|
-
|
|
8877
|
-
|
|
9458
|
+
await new Promise((resolve, reject) => {
|
|
9459
|
+
server.once("error", reject);
|
|
9460
|
+
server.listen(config.port, config.host, () => {
|
|
9461
|
+
server.off("error", reject);
|
|
9462
|
+
resolve();
|
|
9463
|
+
});
|
|
9464
|
+
});
|
|
9465
|
+
await ownership.publish();
|
|
9466
|
+
await updateStartup.ready();
|
|
9467
|
+
applicationUpdate.beginPolling();
|
|
9468
|
+
console.log(`Treeport ${config.appVersion} listening on ${config.apiUrl}`);
|
|
9469
|
+
console.log(`database: ${config.databasePath}`);
|
|
9470
|
+
console.log(`git: ${prerequisites.gitVersion}`);
|
|
9471
|
+
console.log(`tmux: ${prerequisites.tmuxVersion}`);
|
|
9472
|
+
let shuttingDown = false;
|
|
9473
|
+
function shutdown() {
|
|
9474
|
+
if (shuttingDown) return;
|
|
9475
|
+
shuttingDown = true;
|
|
9476
|
+
applicationUpdate.dispose();
|
|
9477
|
+
attachments.dispose();
|
|
9478
|
+
terminalMetadata.dispose();
|
|
9479
|
+
const viteClosed = vite?.close();
|
|
9480
|
+
io.close(() => {
|
|
9481
|
+
Promise.all([
|
|
9482
|
+
service.drainMutations(),
|
|
9483
|
+
terminalMetadata.drain(),
|
|
9484
|
+
viteClosed
|
|
9485
|
+
]).then(async () => {
|
|
9486
|
+
await service.disposeWebPanelRuntime();
|
|
9487
|
+
database.close();
|
|
9488
|
+
await ownership.release();
|
|
9489
|
+
process.exit(0);
|
|
9490
|
+
});
|
|
9491
|
+
});
|
|
9492
|
+
setTimeout(() => process.exit(1), 5e3).unref();
|
|
9493
|
+
}
|
|
9494
|
+
process.once("SIGINT", shutdown);
|
|
9495
|
+
process.once("SIGTERM", shutdown);
|
|
9496
|
+
} catch (error) {
|
|
9497
|
+
await updateStartup.failed(error instanceof Error ? error : new Error(String(error)));
|
|
9498
|
+
throw error;
|
|
9499
|
+
}
|
|
8878
9500
|
}
|
|
8879
|
-
|
|
8880
|
-
process.once("SIGTERM", shutdown);
|
|
9501
|
+
await main();
|
|
8881
9502
|
//#endregion
|
|
8882
9503
|
export {};
|