@treeport/treeport 0.4.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 +1 -1
- package/dist/node/cli/index.js +47 -1589
- package/dist/node/server/core/launcher.js +16 -0
- package/dist/node/server/index.js +740 -210
- 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/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 +3 -3
- package/skills/treeport/SKILL.md +13 -4
- package/dist/loopback-D7k_J_Wl.js +0 -412
- package/dist/web/assets/index-DCtptjcH.js +0 -146
- package/dist/web/assets/index-Wj0w0nWP.css +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
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
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";
|
|
@@ -113,11 +113,17 @@ var ExternalCommandError = class extends Data.TaggedError("ExternalCommandError"
|
|
|
113
113
|
function errorMessage$1(cause) {
|
|
114
114
|
return cause instanceof Error ? cause.message : String(cause);
|
|
115
115
|
}
|
|
116
|
+
function signalProcessGroup(processGroupId, signal) {
|
|
117
|
+
try {
|
|
118
|
+
process.kill(-processGroupId, signal);
|
|
119
|
+
} catch (cause) {
|
|
120
|
+
if (cause.code !== "ESRCH") throw cause;
|
|
121
|
+
}
|
|
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,
|
|
@@ -249,16 +257,13 @@ function runCommandEffect(request) {
|
|
|
249
257
|
resource.terminalError ??= new TimeoutCommandError(request, timeoutMs);
|
|
250
258
|
return resource.terminalError;
|
|
251
259
|
})), Effect.flatMap(Effect.fail)));
|
|
252
|
-
}, (resource) => Deferred.isDone(resource.exit).pipe(Effect.flatMap((alreadyExited) => {
|
|
253
|
-
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;
|
|
254
262
|
return Effect.sync(() => {
|
|
255
|
-
resource.
|
|
256
|
-
}).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) => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
resource.child.kill("SIGKILL");
|
|
260
|
-
}).pipe(Effect.zipRight(Deferred.await(resource.exit)));
|
|
261
|
-
}));
|
|
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)))));
|
|
262
267
|
}), Effect.ensuring(Effect.sync(() => {
|
|
263
268
|
resource.child.removeListener("exit", resource.onExit);
|
|
264
269
|
resource.child.removeListener("error", resource.onProcessError);
|
|
@@ -359,6 +364,7 @@ var database_schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
359
364
|
const projects = sqliteTable("projects", {
|
|
360
365
|
id: text().primaryKey(),
|
|
361
366
|
name: text().notNull(),
|
|
367
|
+
kind: text("project_kind", { enum: ["repository", "folder"] }).notNull().default("repository"),
|
|
362
368
|
repositoryPath: text("repository_path").notNull().unique(),
|
|
363
369
|
mainWorktreePath: text("main_worktree_path").notNull(),
|
|
364
370
|
defaultBranch: text("default_branch").notNull(),
|
|
@@ -368,15 +374,18 @@ const projects = sqliteTable("projects", {
|
|
|
368
374
|
repositoryInode: text("repository_inode").notNull(),
|
|
369
375
|
nameIsCustom: integer("name_is_custom").notNull().default(0),
|
|
370
376
|
isOpen: integer("is_open").notNull().default(1),
|
|
377
|
+
showInRecents: integer("show_in_recents").notNull().default(0),
|
|
371
378
|
lastOpenedAt: text("last_opened_at").notNull(),
|
|
372
379
|
createdAt: text("created_at").notNull(),
|
|
373
380
|
updatedAt: text("updated_at").notNull()
|
|
374
381
|
}, (table) => [
|
|
382
|
+
check("projects_kind_check", sql`${table.kind} IN ('repository','folder')`),
|
|
375
383
|
check("projects_color_check", sql`${table.color} IS NULL OR ${table.color} IN ('rose','orange','amber','emerald','cyan','blue','violet','pink')`),
|
|
376
384
|
check("projects_name_is_custom_check", sql`${table.nameIsCustom} IN (0,1)`),
|
|
377
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)`),
|
|
378
387
|
uniqueIndex("projects_repository_identity_idx").on(table.repositoryIdentity).where(sql`${table.repositoryIdentity} IS NOT NULL`),
|
|
379
|
-
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)
|
|
380
389
|
]);
|
|
381
390
|
const worktrees = sqliteTable("worktrees", {
|
|
382
391
|
id: text().primaryKey(),
|
|
@@ -405,7 +414,7 @@ const worktrees = sqliteTable("worktrees", {
|
|
|
405
414
|
check("worktrees_detached_check", sql`${table.detached} IN (0,1)`),
|
|
406
415
|
check("worktrees_locked_check", sql`${table.locked} IN (0,1)`),
|
|
407
416
|
check("worktrees_prunable_check", sql`${table.prunable} IN (0,1)`),
|
|
408
|
-
check("worktrees_kind_check", sql`${table.kind} IN ('main','linked')`),
|
|
417
|
+
check("worktrees_kind_check", sql`${table.kind} IN ('main','linked','folder')`),
|
|
409
418
|
index("worktrees_project_idx").on(table.projectId),
|
|
410
419
|
uniqueIndex("worktrees_git_key_idx").on(table.projectId, table.gitWorktreeKey).where(sql`${table.gitWorktreeKey} IS NOT NULL`)
|
|
411
420
|
]);
|
|
@@ -517,6 +526,7 @@ function normalizeWorktreeName(input) {
|
|
|
517
526
|
}
|
|
518
527
|
function inferWorktreeName(mainWorktreePath, worktreePath, kind) {
|
|
519
528
|
if (kind === "main") return "main tree";
|
|
529
|
+
if (kind === "folder") return path.basename(worktreePath);
|
|
520
530
|
const checkoutName = path.basename(worktreePath);
|
|
521
531
|
return checkoutName === path.basename(mainWorktreePath) ? path.basename(path.dirname(worktreePath)) : checkoutName;
|
|
522
532
|
}
|
|
@@ -649,11 +659,8 @@ function resolveTask(task, input, protectCompatibilityEnvironment) {
|
|
|
649
659
|
const useShell = /[\s;&|<>`$()]/u.test(command);
|
|
650
660
|
return {
|
|
651
661
|
label: expand(task.label, compatibilityEnvironment),
|
|
652
|
-
argv: useShell ? [
|
|
653
|
-
|
|
654
|
-
"-lc",
|
|
655
|
-
[command, ...args.map(shellQuote)].join(" ")
|
|
656
|
-
] : [command, ...args],
|
|
662
|
+
argv: useShell ? null : [command, ...args],
|
|
663
|
+
shellCommand: useShell ? [command, ...args.map(shellQuote)].join(" ") : null,
|
|
657
664
|
cwd,
|
|
658
665
|
env: protectCompatibilityEnvironment ? {
|
|
659
666
|
...taskEnvironment,
|
|
@@ -712,8 +719,9 @@ async function loadZedTerminalPresetDefinitions(input) {
|
|
|
712
719
|
definitions.push({
|
|
713
720
|
id: `repository:${input.projectId}:zed-task:${index}`,
|
|
714
721
|
name: resolved.label,
|
|
715
|
-
executable: resolved.argv[0],
|
|
716
|
-
args: resolved.argv
|
|
722
|
+
executable: resolved.argv?.[0] ?? null,
|
|
723
|
+
args: resolved.argv?.slice(1) ?? [],
|
|
724
|
+
shellCommand: resolved.shellCommand,
|
|
717
725
|
cwd: resolved.cwd,
|
|
718
726
|
env: resolved.env,
|
|
719
727
|
closeOnSuccess: false,
|
|
@@ -731,9 +739,18 @@ async function loadZedTerminalPresetDefinitions(input) {
|
|
|
731
739
|
async function resolveZedCreateWorktreeSetupTasks(input) {
|
|
732
740
|
return (await loadCreateWorktreeTasks(input.mainWorktreePath)).map((task) => {
|
|
733
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
|
+
}
|
|
734
751
|
return {
|
|
735
752
|
label: task.label,
|
|
736
|
-
argv
|
|
753
|
+
argv,
|
|
737
754
|
cwd: resolved.cwd,
|
|
738
755
|
env: resolved.env,
|
|
739
756
|
timeoutMs: 30 * 6e4
|
|
@@ -874,6 +891,8 @@ async function openDatabase(filePath, options = {}) {
|
|
|
874
891
|
const databaseExists = fsSync.existsSync(absoluteFilePath);
|
|
875
892
|
let hasDurableSchema = false;
|
|
876
893
|
let hasLegacyMigrations = false;
|
|
894
|
+
let migrationsPending = !databaseExists;
|
|
895
|
+
const migrationSnapshotPaths = [];
|
|
877
896
|
let drizzleRows = [];
|
|
878
897
|
if (!databaseExists) await fsSync.promises.mkdir(path.dirname(absoluteFilePath), {
|
|
879
898
|
recursive: true,
|
|
@@ -919,7 +938,8 @@ async function openDatabase(filePath, options = {}) {
|
|
|
919
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.`);
|
|
920
939
|
}
|
|
921
940
|
if (hasDurableSchema && !hasLegacyMigrations && drizzleRows.length === 0) throw new Error(`Treeport database at ${absoluteFilePath} has no recognized migration history; refusing to modify it.`);
|
|
922
|
-
|
|
941
|
+
migrationsPending = drizzleRows.length === 0 || Number(drizzleRows.at(-1)?.createdAt) < latestMigration.folderMillis;
|
|
942
|
+
if (migrationsPending && hasDurableSchema) {
|
|
923
943
|
const backupDirectory = path.resolve(options.backupDirectory ?? path.join(path.dirname(absoluteFilePath), "database-backups"));
|
|
924
944
|
await fsSync.promises.mkdir(backupDirectory, {
|
|
925
945
|
recursive: true,
|
|
@@ -932,6 +952,7 @@ async function openDatabase(filePath, options = {}) {
|
|
|
932
952
|
try {
|
|
933
953
|
await db.run(sql.raw(`VACUUM INTO '${backupPath.replaceAll("'", "''")}'`));
|
|
934
954
|
await fsSync.promises.chmod(backupPath, 384);
|
|
955
|
+
migrationSnapshotPaths.push(backupPath);
|
|
935
956
|
} catch (error) {
|
|
936
957
|
await fsSync.promises.rm(backupPath, { force: true });
|
|
937
958
|
throw error;
|
|
@@ -965,6 +986,8 @@ async function openDatabase(filePath, options = {}) {
|
|
|
965
986
|
return {
|
|
966
987
|
filePath: absoluteFilePath,
|
|
967
988
|
db,
|
|
989
|
+
migrationState: migrationsPending ? "advanced" : "unchanged",
|
|
990
|
+
migrationSnapshotPaths,
|
|
968
991
|
close: () => client.close()
|
|
969
992
|
};
|
|
970
993
|
} catch (error) {
|
|
@@ -976,6 +999,8 @@ function mapProject(row, worktreeRows) {
|
|
|
976
999
|
return {
|
|
977
1000
|
id: row.id,
|
|
978
1001
|
name: row.name,
|
|
1002
|
+
kind: row.kind,
|
|
1003
|
+
rootPath: row.repositoryPath,
|
|
979
1004
|
repositoryPath: row.repositoryPath,
|
|
980
1005
|
mainWorktreePath: row.mainWorktreePath,
|
|
981
1006
|
defaultBranch: row.defaultBranch,
|
|
@@ -995,7 +1020,7 @@ function mapWorktree(row, mainWorktreePath) {
|
|
|
995
1020
|
projectId: row.projectId,
|
|
996
1021
|
name: inferWorktreeName(mainWorktreePath, row.path, row.kind),
|
|
997
1022
|
path: row.path,
|
|
998
|
-
head: row.head,
|
|
1023
|
+
head: row.kind === "folder" ? "" : row.head,
|
|
999
1024
|
branch: row.branch,
|
|
1000
1025
|
detached: Boolean(row.detached),
|
|
1001
1026
|
locked: Boolean(row.locked),
|
|
@@ -1284,10 +1309,34 @@ var GitAdapter = class {
|
|
|
1284
1309
|
timeoutMs: 3e4
|
|
1285
1310
|
});
|
|
1286
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
|
+
}
|
|
1287
1335
|
async canonicalizeRepositoryPath(inputPath) {
|
|
1336
|
+
const repositoryRoot = await this.findRepositoryRoot(inputPath);
|
|
1337
|
+
if (repositoryRoot) return repositoryRoot;
|
|
1288
1338
|
const canonicalInput = await fs.realpath(path.resolve(inputPath));
|
|
1289
|
-
|
|
1290
|
-
return fs.realpath(result.stdout.trim());
|
|
1339
|
+
throw new Error(`Not a Git repository: ${canonicalInput}`);
|
|
1291
1340
|
}
|
|
1292
1341
|
async repositoryIdentityValues(cwd) {
|
|
1293
1342
|
const result = await this.runner.run({
|
|
@@ -2155,7 +2204,7 @@ var PackageSystem = class {
|
|
|
2155
2204
|
if (scope === "global") return path.join(this.config.dataDir, "settings.json");
|
|
2156
2205
|
const project = projectId ? this.projectContexts.get(projectId) : void 0;
|
|
2157
2206
|
if (!project) throw new DomainError("PROJECT_NOT_FOUND", "Project not found for package operation", 404);
|
|
2158
|
-
return path.join(project.
|
|
2207
|
+
return path.join(project.rootPath, ".treeport", "settings.json");
|
|
2159
2208
|
}
|
|
2160
2209
|
async serialize(key, operation) {
|
|
2161
2210
|
const previous = this.operationTails.get(key) ?? Promise.resolve();
|
|
@@ -2312,7 +2361,7 @@ var PackageSystem = class {
|
|
|
2312
2361
|
};
|
|
2313
2362
|
}
|
|
2314
2363
|
npmRoot(scope, projectId) {
|
|
2315
|
-
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");
|
|
2316
2365
|
}
|
|
2317
2366
|
npmPackagePath(source, scope, projectId) {
|
|
2318
2367
|
return path.join(this.npmRoot(scope, projectId), "node_modules", source.name);
|
|
@@ -2628,6 +2677,7 @@ var PackageSystem = class {
|
|
|
2628
2677
|
name: result.data.name,
|
|
2629
2678
|
executable: result.data.executable,
|
|
2630
2679
|
args: [...result.data.args],
|
|
2680
|
+
shellCommand: null,
|
|
2631
2681
|
cwd: null,
|
|
2632
2682
|
env: {},
|
|
2633
2683
|
closeOnSuccess: result.data.closeOnSuccess,
|
|
@@ -2878,7 +2928,7 @@ var PackageSystem = class {
|
|
|
2878
2928
|
return {
|
|
2879
2929
|
id: project.id,
|
|
2880
2930
|
name: project.name,
|
|
2881
|
-
|
|
2931
|
+
rootPath: project.rootPath
|
|
2882
2932
|
};
|
|
2883
2933
|
}
|
|
2884
2934
|
syncProjects(projects) {
|
|
@@ -2886,7 +2936,7 @@ var PackageSystem = class {
|
|
|
2886
2936
|
const next = this.context(project);
|
|
2887
2937
|
const previous = this.projectContexts.get(project.id);
|
|
2888
2938
|
this.projectContexts.set(project.id, next);
|
|
2889
|
-
if (previous && (previous.
|
|
2939
|
+
if (previous && (previous.rootPath !== next.rootPath || previous.name !== next.name)) this.projectFingerprints.delete(project.id);
|
|
2890
2940
|
}
|
|
2891
2941
|
}
|
|
2892
2942
|
async initialize(projects) {
|
|
@@ -3210,6 +3260,7 @@ async function loadRepositoryTerminalPresets(projectId, worktreePath) {
|
|
|
3210
3260
|
name: preset.data.name,
|
|
3211
3261
|
executable: preset.data.executable,
|
|
3212
3262
|
args: [...preset.data.args],
|
|
3263
|
+
shellCommand: null,
|
|
3213
3264
|
cwd: null,
|
|
3214
3265
|
env: {},
|
|
3215
3266
|
closeOnSuccess: preset.data.closeOnSuccess,
|
|
@@ -3826,6 +3877,7 @@ var TmuxAdapter = class {
|
|
|
3826
3877
|
uid;
|
|
3827
3878
|
creationTails = /* @__PURE__ */ new Map();
|
|
3828
3879
|
configuredSockets = /* @__PURE__ */ new Set();
|
|
3880
|
+
socketConfigurationPromises = /* @__PURE__ */ new Map();
|
|
3829
3881
|
initializationPromise = null;
|
|
3830
3882
|
sshAuthSockPromise = null;
|
|
3831
3883
|
constructor(runner, runtimeDir, executable = "tmux", launcherPath, host = {}) {
|
|
@@ -3923,10 +3975,8 @@ var TmuxAdapter = class {
|
|
|
3923
3975
|
env: environment
|
|
3924
3976
|
};
|
|
3925
3977
|
if (input.fallbackArgv) spec.fallbackArgv = [...input.fallbackArgv];
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
spec.tmuxExecutable = resolveExecutablePath(this.executable, this.hostEnvironment);
|
|
3929
|
-
}
|
|
3978
|
+
spec.tmuxExecutable = resolveExecutablePath(this.executable, this.hostEnvironment);
|
|
3979
|
+
if (shellIntegrationReady) spec.shellIntegrationDir = this.shellIntegrationDir;
|
|
3930
3980
|
if (input.setupTasks?.length) spec.setupTasks = input.setupTasks.map((task) => ({
|
|
3931
3981
|
...task,
|
|
3932
3982
|
argv: [...task.argv],
|
|
@@ -3965,6 +4015,8 @@ var TmuxAdapter = class {
|
|
|
3965
4015
|
worktreeId: input.worktreeId,
|
|
3966
4016
|
name: input.name,
|
|
3967
4017
|
argv: input.argv,
|
|
4018
|
+
shellCommand: input.shellCommand,
|
|
4019
|
+
interactiveShell: input.interactiveShell,
|
|
3968
4020
|
closeOnSuccess: input.closeOnSuccess ?? false,
|
|
3969
4021
|
createdAt: input.createdAt,
|
|
3970
4022
|
updatedAt: input.createdAt
|
|
@@ -3992,7 +4044,10 @@ var TmuxAdapter = class {
|
|
|
3992
4044
|
}
|
|
3993
4045
|
}
|
|
3994
4046
|
async configureServer(socketName) {
|
|
3995
|
-
|
|
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, {
|
|
3996
4051
|
executable: this.executable,
|
|
3997
4052
|
args: [
|
|
3998
4053
|
...this.base(socketName),
|
|
@@ -4001,13 +4056,20 @@ var TmuxAdapter = class {
|
|
|
4001
4056
|
],
|
|
4002
4057
|
env: this.environment(),
|
|
4003
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);
|
|
4004
4065
|
});
|
|
4005
|
-
this.configuredSockets.add(socketName);
|
|
4006
4066
|
}
|
|
4007
4067
|
async configureSession(socketName, sessionName, metadata) {
|
|
4008
4068
|
const values = [
|
|
4009
4069
|
["@treeport-name", encodeMetadata(metadata.name)],
|
|
4010
4070
|
["@treeport-argv", encodeMetadata(metadata.argv)],
|
|
4071
|
+
["@treeport-shell-command", encodeMetadata(metadata.shellCommand)],
|
|
4072
|
+
["@treeport-interactive-shell", metadata.interactiveShell ? "1" : "0"],
|
|
4011
4073
|
["@treeport-close-on-success", metadata.closeOnSuccess ? "1" : "0"],
|
|
4012
4074
|
["@treeport-created-at", encodeMetadata(metadata.createdAt)],
|
|
4013
4075
|
["@treeport-updated-at", encodeMetadata(metadata.updatedAt)],
|
|
@@ -4060,7 +4122,7 @@ var TmuxAdapter = class {
|
|
|
4060
4122
|
"list-panes",
|
|
4061
4123
|
"-a",
|
|
4062
4124
|
"-F",
|
|
4063
|
-
"#{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}"
|
|
4064
4126
|
],
|
|
4065
4127
|
env: this.environment(),
|
|
4066
4128
|
timeoutMs: 1e4
|
|
@@ -4072,7 +4134,7 @@ var TmuxAdapter = class {
|
|
|
4072
4134
|
const sessions = /* @__PURE__ */ new Map();
|
|
4073
4135
|
for (const line of result.stdout.split("\n")) {
|
|
4074
4136
|
if (!line) continue;
|
|
4075
|
-
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(" ");
|
|
4076
4138
|
if (!sessionName || sessions.has(sessionName) || !terminalId || !worktreeId) continue;
|
|
4077
4139
|
let metadata;
|
|
4078
4140
|
try {
|
|
@@ -4081,12 +4143,15 @@ var TmuxAdapter = class {
|
|
|
4081
4143
|
worktreeId,
|
|
4082
4144
|
name: decodeMetadata(encodedName ?? "", z.string()),
|
|
4083
4145
|
argv: decodeMetadata(encodedArgv ?? "", z.array(z.string())),
|
|
4146
|
+
shellCommand: decodeMetadata(encodedShellCommand ?? "", z.string().nullable()),
|
|
4084
4147
|
createdAt: decodeMetadata(encodedCreatedAt ?? "", z.string()),
|
|
4085
4148
|
updatedAt: decodeMetadata(encodedUpdatedAt ?? "", z.string())
|
|
4086
4149
|
};
|
|
4087
4150
|
} catch {
|
|
4088
4151
|
continue;
|
|
4089
4152
|
}
|
|
4153
|
+
const interactiveShell = encodedInteractiveShell === "1" ? true : encodedInteractiveShell === "0" ? false : void 0;
|
|
4154
|
+
if (metadata.shellCommand === void 0 || interactiveShell === void 0) continue;
|
|
4090
4155
|
const fallbackCreatedAt = metadata.createdAt ?? (/* @__PURE__ */ new Date(Number(sessionCreated) * 1e3)).toISOString();
|
|
4091
4156
|
const dead = paneDead === "1";
|
|
4092
4157
|
const exitCode = dead && paneDeadStatus ? Number.parseInt(paneDeadStatus, 10) : null;
|
|
@@ -4096,6 +4161,8 @@ var TmuxAdapter = class {
|
|
|
4096
4161
|
name: metadata.name ?? sessionName,
|
|
4097
4162
|
sessionName,
|
|
4098
4163
|
argv: metadata.argv ?? [],
|
|
4164
|
+
shellCommand: metadata.shellCommand,
|
|
4165
|
+
interactiveShell,
|
|
4099
4166
|
closeOnSuccess: closeOnSuccess === "1",
|
|
4100
4167
|
status: dead ? "exited" : "running",
|
|
4101
4168
|
exitCode: Number.isNaN(exitCode) ? null : exitCode,
|
|
@@ -4248,7 +4315,7 @@ var TmuxAdapter = class {
|
|
|
4248
4315
|
"-p",
|
|
4249
4316
|
"-t",
|
|
4250
4317
|
sessionName,
|
|
4251
|
-
"#{@treeport-shell-title} #{pane_current_command} #{@treeport-command} #{pane_title}"
|
|
4318
|
+
"#{@treeport-fallback-shell} #{@treeport-shell-title} #{pane_current_command} #{@treeport-command} #{pane_title}"
|
|
4252
4319
|
],
|
|
4253
4320
|
env: this.environment(),
|
|
4254
4321
|
timeoutMs: 1e4
|
|
@@ -4257,19 +4324,22 @@ var TmuxAdapter = class {
|
|
|
4257
4324
|
const firstSeparator = result.stdout.indexOf(" ");
|
|
4258
4325
|
const secondSeparator = result.stdout.indexOf(" ", firstSeparator + 1);
|
|
4259
4326
|
const thirdSeparator = result.stdout.indexOf(" ", secondSeparator + 1);
|
|
4260
|
-
|
|
4261
|
-
|
|
4327
|
+
const fourthSeparator = result.stdout.indexOf(" ", thirdSeparator + 1);
|
|
4328
|
+
if (firstSeparator === -1 || secondSeparator === -1 || thirdSeparator === -1 || fourthSeparator === -1) return null;
|
|
4329
|
+
let fallbackShell = null;
|
|
4262
4330
|
let shellTitle = null;
|
|
4263
4331
|
try {
|
|
4264
|
-
|
|
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;
|
|
4265
4334
|
} catch {}
|
|
4266
|
-
const currentCommand = result.stdout.slice(
|
|
4267
|
-
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;
|
|
4268
4337
|
return {
|
|
4269
|
-
paneTitle: result.stdout.slice(
|
|
4338
|
+
paneTitle: result.stdout.slice(fourthSeparator + 1).trim() || null,
|
|
4270
4339
|
currentCommand,
|
|
4271
4340
|
commandLine,
|
|
4272
|
-
shellTitle
|
|
4341
|
+
shellTitle,
|
|
4342
|
+
fallbackShell
|
|
4273
4343
|
};
|
|
4274
4344
|
}
|
|
4275
4345
|
async setSessionShellTitle(socketName, sessionName, title) {
|
|
@@ -4403,6 +4473,7 @@ var TreeportService = class {
|
|
|
4403
4473
|
closeOnSuccessTerminalIds = /* @__PURE__ */ new Set();
|
|
4404
4474
|
terminalIdsByWorktree = /* @__PURE__ */ new Map();
|
|
4405
4475
|
projectObservationTails = /* @__PURE__ */ new Map();
|
|
4476
|
+
observedFolderIdentities = /* @__PURE__ */ new Map();
|
|
4406
4477
|
projectsSnapshotInFlight = null;
|
|
4407
4478
|
projectsSnapshotRevision = 0;
|
|
4408
4479
|
packages;
|
|
@@ -4600,9 +4671,11 @@ var TreeportService = class {
|
|
|
4600
4671
|
return this.deps.database.db.select({
|
|
4601
4672
|
id: projects.id,
|
|
4602
4673
|
name: projects.name,
|
|
4674
|
+
kind: projects.kind,
|
|
4675
|
+
rootPath: projects.repositoryPath,
|
|
4603
4676
|
repositoryPath: projects.repositoryPath,
|
|
4604
4677
|
lastOpenedAt: projects.lastOpenedAt
|
|
4605
|
-
}).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));
|
|
4606
4679
|
}
|
|
4607
4680
|
async collectCurrentProjectsSnapshot() {
|
|
4608
4681
|
while (true) {
|
|
@@ -4615,7 +4688,8 @@ var TreeportService = class {
|
|
|
4615
4688
|
return (await Promise.all((await this.storedProjects(true)).map(async (storedProject) => {
|
|
4616
4689
|
let project = storedProject;
|
|
4617
4690
|
try {
|
|
4618
|
-
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);
|
|
4619
4693
|
await this.ensureProjectTerminals(project.id);
|
|
4620
4694
|
project = await this.storedProject(project.id) ?? project;
|
|
4621
4695
|
} catch (error) {
|
|
@@ -4626,7 +4700,7 @@ var TreeportService = class {
|
|
|
4626
4700
|
}
|
|
4627
4701
|
if (await this.projectOpenState(project.id) !== true) return null;
|
|
4628
4702
|
await Promise.all(project.worktrees.map(async (worktree) => {
|
|
4629
|
-
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) => {
|
|
4630
4704
|
project.availability = {
|
|
4631
4705
|
state: "unavailable",
|
|
4632
4706
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -4667,6 +4741,8 @@ var TreeportService = class {
|
|
|
4667
4741
|
name: terminal.name,
|
|
4668
4742
|
tmuxSessionName: terminal.sessionName,
|
|
4669
4743
|
argv: terminal.argv,
|
|
4744
|
+
shellCommand: terminal.shellCommand,
|
|
4745
|
+
interactiveShell: terminal.interactiveShell,
|
|
4670
4746
|
status: terminal.status,
|
|
4671
4747
|
exitCode: terminal.exitCode,
|
|
4672
4748
|
createdAt: terminal.createdAt,
|
|
@@ -4730,7 +4806,7 @@ var TreeportService = class {
|
|
|
4730
4806
|
const direct = await this.storedProject(identifier);
|
|
4731
4807
|
if (direct) return direct;
|
|
4732
4808
|
const canonical = await fs.realpath(path.resolve(identifier)).catch(() => path.resolve(identifier));
|
|
4733
|
-
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)));
|
|
4734
4810
|
if (!match) throw new DomainError("PROJECT_NOT_FOUND", `No registered project contains ${identifier}`, 404);
|
|
4735
4811
|
return match;
|
|
4736
4812
|
}
|
|
@@ -4798,7 +4874,7 @@ var TreeportService = class {
|
|
|
4798
4874
|
definitions: [],
|
|
4799
4875
|
diagnostics: []
|
|
4800
4876
|
}),
|
|
4801
|
-
worktree && project ? loadZedTerminalPresetDefinitions({
|
|
4877
|
+
worktree && project?.kind === "repository" ? loadZedTerminalPresetDefinitions({
|
|
4802
4878
|
projectId: project.id,
|
|
4803
4879
|
shell: this.deps.config.shell,
|
|
4804
4880
|
mainWorktreePath: project.mainWorktreePath,
|
|
@@ -4820,6 +4896,7 @@ var TreeportService = class {
|
|
|
4820
4896
|
name: preset.name,
|
|
4821
4897
|
executable: preset.executable,
|
|
4822
4898
|
args: [...preset.args],
|
|
4899
|
+
shellCommand: null,
|
|
4823
4900
|
cwd: null,
|
|
4824
4901
|
env: {},
|
|
4825
4902
|
closeOnSuccess: preset.closeOnSuccess,
|
|
@@ -5069,18 +5146,21 @@ var TreeportService = class {
|
|
|
5069
5146
|
project: {
|
|
5070
5147
|
id: project.id,
|
|
5071
5148
|
name: project.name,
|
|
5072
|
-
|
|
5149
|
+
kind: project.kind,
|
|
5150
|
+
defaultBranch: project.kind === "repository" ? project.defaultBranch : null
|
|
5073
5151
|
},
|
|
5074
5152
|
worktree: {
|
|
5075
5153
|
id: worktree.id,
|
|
5076
5154
|
name: worktree.name,
|
|
5155
|
+
kind: worktree.kind,
|
|
5077
5156
|
branch: worktree.branch,
|
|
5078
|
-
head: worktree.head
|
|
5157
|
+
head: worktree.kind === "folder" ? null : worktree.head
|
|
5079
5158
|
}
|
|
5080
5159
|
};
|
|
5081
5160
|
}
|
|
5082
5161
|
async getWebPanelDiff(panelId) {
|
|
5083
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);
|
|
5084
5164
|
const worktree = await this.getWorktree(context.panel.worktreeId);
|
|
5085
5165
|
return this.deps.git.worktreeDiff(worktree.path, context.project.defaultBranch);
|
|
5086
5166
|
}
|
|
@@ -5145,6 +5225,14 @@ var TreeportService = class {
|
|
|
5145
5225
|
if (!worktree) throw new DomainError("WORKTREE_NOT_FOUND", "Tree not found", 404);
|
|
5146
5226
|
return worktree;
|
|
5147
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
|
+
}
|
|
5148
5236
|
async getTerminal(terminalId) {
|
|
5149
5237
|
const matches = (await this.listProjects()).flatMap((project) => project.worktrees).flatMap((worktree) => worktree.terminals).filter((terminal) => terminal.id === terminalId);
|
|
5150
5238
|
if (matches.length > 1) throw new DomainError("TERMINAL_ID_CONFLICT", "Terminal ID is present in more than one tmux server", 500);
|
|
@@ -5178,7 +5266,7 @@ var TreeportService = class {
|
|
|
5178
5266
|
const direct = await this.storedProject(identifier);
|
|
5179
5267
|
if (direct) return await this.requireOpenProject(direct.id);
|
|
5180
5268
|
const canonical = await fs.realpath(path.resolve(identifier)).catch(() => path.resolve(identifier));
|
|
5181
|
-
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)));
|
|
5182
5270
|
if (!match) throw new DomainError("PROJECT_NOT_FOUND", `No registered project contains ${identifier}`, 404);
|
|
5183
5271
|
await this.requireOpenProject(match.id);
|
|
5184
5272
|
return match;
|
|
@@ -5251,7 +5339,7 @@ var TreeportService = class {
|
|
|
5251
5339
|
path: breadcrumbPath
|
|
5252
5340
|
});
|
|
5253
5341
|
}
|
|
5254
|
-
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;
|
|
5255
5343
|
return {
|
|
5256
5344
|
input: inputPath,
|
|
5257
5345
|
exact,
|
|
@@ -5264,6 +5352,18 @@ var TreeportService = class {
|
|
|
5264
5352
|
entries,
|
|
5265
5353
|
truncated
|
|
5266
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
|
+
},
|
|
5267
5367
|
repository: repositoryPath ? {
|
|
5268
5368
|
state: "valid",
|
|
5269
5369
|
repositoryPath
|
|
@@ -5277,6 +5377,14 @@ var TreeportService = class {
|
|
|
5277
5377
|
};
|
|
5278
5378
|
}
|
|
5279
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) {
|
|
5280
5388
|
const checkout = await this.deps.git.canonicalizeRepositoryPath(inputPath).catch((error) => {
|
|
5281
5389
|
throw new DomainError("NOT_A_GIT_REPOSITORY", error instanceof Error ? error.message : "Not a Git repository", 400);
|
|
5282
5390
|
});
|
|
@@ -5331,17 +5439,18 @@ var TreeportService = class {
|
|
|
5331
5439
|
if (verifiedIdentity !== repositoryIdentity || verifiedStat.dev.toString() !== repositoryDevice || verifiedStat.ino.toString() !== repositoryInode) throw new DomainError("PROJECT_PATH_CONFLICT", "The repository changed during registration", 409);
|
|
5332
5440
|
await this.deps.database.db.run(sql`
|
|
5333
5441
|
INSERT INTO projects(
|
|
5334
|
-
id,name,repository_path,main_worktree_path,default_branch,
|
|
5442
|
+
id,name,project_kind,repository_path,main_worktree_path,default_branch,
|
|
5335
5443
|
repository_identity,repository_device,repository_inode,name_is_custom,
|
|
5336
|
-
is_open,last_opened_at,created_at,updated_at
|
|
5444
|
+
is_open,show_in_recents,last_opened_at,created_at,updated_at
|
|
5337
5445
|
) VALUES(
|
|
5338
|
-
${projectId},${name},${repositoryPath},${mainPath},${defaultBranch},
|
|
5446
|
+
${projectId},${name},'repository',${repositoryPath},${mainPath},${defaultBranch},
|
|
5339
5447
|
${repositoryIdentity},${repositoryDevice},${repositoryInode},
|
|
5340
|
-
${nameIsCustom ? 1 : 0},1,${timestamp},
|
|
5448
|
+
${nameIsCustom ? 1 : 0},1,0,${timestamp},
|
|
5341
5449
|
${existing?.createdAt ?? timestamp},${timestamp}
|
|
5342
5450
|
)
|
|
5343
5451
|
ON CONFLICT(id) DO UPDATE SET
|
|
5344
5452
|
name=excluded.name,
|
|
5453
|
+
project_kind=excluded.project_kind,
|
|
5345
5454
|
repository_path=excluded.repository_path,
|
|
5346
5455
|
main_worktree_path=excluded.main_worktree_path,
|
|
5347
5456
|
default_branch=excluded.default_branch,
|
|
@@ -5362,6 +5471,7 @@ var TreeportService = class {
|
|
|
5362
5471
|
const timestamp = now();
|
|
5363
5472
|
await this.deps.database.db.update(projects).set({
|
|
5364
5473
|
isOpen: 1,
|
|
5474
|
+
showInRecents: 0,
|
|
5365
5475
|
lastOpenedAt: timestamp,
|
|
5366
5476
|
updatedAt: timestamp
|
|
5367
5477
|
}).where(eq(projects.id, projectId));
|
|
@@ -5382,9 +5492,123 @@ var TreeportService = class {
|
|
|
5382
5492
|
this.events.publish("project.created", { projectId });
|
|
5383
5493
|
return this.getProjectSnapshot(projectId);
|
|
5384
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
|
+
}
|
|
5385
5587
|
async observeAvailableProject(project, allowClosed = false) {
|
|
5386
5588
|
try {
|
|
5387
|
-
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
|
+
});
|
|
5388
5612
|
} catch (error) {
|
|
5389
5613
|
throw new DomainError("PROJECT_UNAVAILABLE", error instanceof Error ? error.message : String(error), 503);
|
|
5390
5614
|
}
|
|
@@ -5397,12 +5621,14 @@ var TreeportService = class {
|
|
|
5397
5621
|
try {
|
|
5398
5622
|
const project = await this.observeAvailableProject(await this.getProject(projectId));
|
|
5399
5623
|
await this.ensureProjectTerminals(projectId);
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
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
|
+
}
|
|
5406
5632
|
await this.reconcile();
|
|
5407
5633
|
await this.packages.registerProject(await this.getProject(projectId));
|
|
5408
5634
|
this.invalidateProjectsSnapshot();
|
|
@@ -5421,6 +5647,7 @@ var TreeportService = class {
|
|
|
5421
5647
|
const timestamp = now();
|
|
5422
5648
|
await this.deps.database.db.update(projects).set({
|
|
5423
5649
|
isOpen: 1,
|
|
5650
|
+
showInRecents: 0,
|
|
5424
5651
|
lastOpenedAt: timestamp,
|
|
5425
5652
|
updatedAt: timestamp
|
|
5426
5653
|
}).where(eq(projects.id, projectId));
|
|
@@ -5462,6 +5689,7 @@ var TreeportService = class {
|
|
|
5462
5689
|
try {
|
|
5463
5690
|
await this.deps.database.db.update(projects).set({
|
|
5464
5691
|
isOpen: 0,
|
|
5692
|
+
showInRecents: 1,
|
|
5465
5693
|
updatedAt: now()
|
|
5466
5694
|
}).where(eq(projects.id, projectId));
|
|
5467
5695
|
} catch (error) {
|
|
@@ -5479,6 +5707,17 @@ var TreeportService = class {
|
|
|
5479
5707
|
}
|
|
5480
5708
|
});
|
|
5481
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
|
+
}
|
|
5482
5721
|
async serializeProjectObservation(projectId, operation) {
|
|
5483
5722
|
const observation = (this.projectObservationTails.get(projectId) ?? Promise.resolve()).then(operation);
|
|
5484
5723
|
const tail = observation.then(() => void 0, () => void 0);
|
|
@@ -5663,7 +5902,7 @@ var TreeportService = class {
|
|
|
5663
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);
|
|
5664
5903
|
}
|
|
5665
5904
|
async beginCreateWorktree(projectId, inputName, base, initialTerminal, sourceWorktreeId) {
|
|
5666
|
-
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);
|
|
5667
5906
|
let name;
|
|
5668
5907
|
try {
|
|
5669
5908
|
name = normalizeWorktreeName(inputName);
|
|
@@ -5734,7 +5973,7 @@ var TreeportService = class {
|
|
|
5734
5973
|
}
|
|
5735
5974
|
}
|
|
5736
5975
|
async createWorktree(projectId, inputName, base, initialTerminal, sourceWorktreeId) {
|
|
5737
|
-
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);
|
|
5738
5977
|
if (this.projectLocks.has(projectId) && !this.worktreeMutations.has(projectId)) throw new DomainError("PROJECT_BUSY", "Project is already being modified", 409);
|
|
5739
5978
|
return this.worktreeMutations.enqueue(projectId, () => this.executeCreateWorktree(projectId, inputName, base, initialTerminal, sourceWorktreeId));
|
|
5740
5979
|
}
|
|
@@ -5749,6 +5988,7 @@ var TreeportService = class {
|
|
|
5749
5988
|
let wrapperCreated = false;
|
|
5750
5989
|
try {
|
|
5751
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);
|
|
5752
5992
|
let name;
|
|
5753
5993
|
try {
|
|
5754
5994
|
name = normalizeWorktreeName(inputName);
|
|
@@ -5902,7 +6142,13 @@ var TreeportService = class {
|
|
|
5902
6142
|
const project = await this.requireOpenProject(worktree.projectId);
|
|
5903
6143
|
const terminalId = id("term");
|
|
5904
6144
|
const sessionName = generateTmuxSessionName();
|
|
5905
|
-
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"];
|
|
5906
6152
|
const timestamp = now();
|
|
5907
6153
|
const session = {
|
|
5908
6154
|
socketName: worktree.tmuxSocketName,
|
|
@@ -5913,6 +6159,8 @@ var TreeportService = class {
|
|
|
5913
6159
|
createdAt: timestamp,
|
|
5914
6160
|
cwd: options?.cwd ?? worktree.path,
|
|
5915
6161
|
argv: commandArgv,
|
|
6162
|
+
shellCommand,
|
|
6163
|
+
interactiveShell,
|
|
5916
6164
|
env: {
|
|
5917
6165
|
...options?.env ?? {},
|
|
5918
6166
|
TREEPORT_API_URL: this.deps.config.apiUrl,
|
|
@@ -5924,7 +6172,7 @@ var TreeportService = class {
|
|
|
5924
6172
|
TREEPORT_TERMINAL_ID: terminalId
|
|
5925
6173
|
}
|
|
5926
6174
|
};
|
|
5927
|
-
if (options?.returnToShell &&
|
|
6175
|
+
if (options?.returnToShell && !interactiveShell) session.fallbackArgv = [this.deps.config.shell, "-l"];
|
|
5928
6176
|
if (options?.closeOnSuccess) session.closeOnSuccess = true;
|
|
5929
6177
|
if (options?.initialSize) session.initialSize = options.initialSize;
|
|
5930
6178
|
if (options?.setup?.tasks.length) session.setupTasks = options.setup.tasks;
|
|
@@ -5940,6 +6188,8 @@ var TreeportService = class {
|
|
|
5940
6188
|
name,
|
|
5941
6189
|
tmuxSessionName: sessionName,
|
|
5942
6190
|
argv: commandArgv,
|
|
6191
|
+
shellCommand,
|
|
6192
|
+
interactiveShell,
|
|
5943
6193
|
status: "running",
|
|
5944
6194
|
exitCode: null,
|
|
5945
6195
|
createdAt: timestamp,
|
|
@@ -5980,7 +6230,7 @@ var TreeportService = class {
|
|
|
5980
6230
|
}
|
|
5981
6231
|
}
|
|
5982
6232
|
async refreshTerminalStatus(terminalId, observeGit = true) {
|
|
5983
|
-
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);
|
|
5984
6234
|
const worktree = await this.getWorktree(terminal.worktreeId);
|
|
5985
6235
|
const state = await this.deps.tmux.sessionState(worktree.tmuxSocketName, terminal.tmuxSessionName);
|
|
5986
6236
|
await this.requireOpenProject(worktree.projectId);
|
|
@@ -6098,6 +6348,7 @@ var TreeportService = class {
|
|
|
6098
6348
|
const worktree = await this.requireAvailableWorktree(worktreeId, true);
|
|
6099
6349
|
worktree.terminals = await this.listWorktreeTerminals(worktree);
|
|
6100
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);
|
|
6101
6352
|
const live = (await this.deps.git.listWorktrees(project.repositoryPath)).find((item) => item.path === worktree.path);
|
|
6102
6353
|
if (!live) throw new DomainError("WORKTREE_NOT_FOUND", "Git no longer reports this worktree", 404);
|
|
6103
6354
|
const head = live.head ?? worktree.head;
|
|
@@ -6433,6 +6684,7 @@ var TreeportService = class {
|
|
|
6433
6684
|
const terminalIdsByWorktree = /* @__PURE__ */ new Map();
|
|
6434
6685
|
for (const worktree of project.worktrees) terminalIdsByWorktree.set(worktree.id, await this.deps.tmux.killServer(worktree.tmuxSocketName));
|
|
6435
6686
|
await this.deps.database.db.run(sql`DELETE FROM projects WHERE id=${projectId}`);
|
|
6687
|
+
this.observedFolderIdentities.delete(projectId);
|
|
6436
6688
|
this.packages.forgetProject(projectId);
|
|
6437
6689
|
for (const worktree of project.worktrees) this.clearWorktreeTerminalState(worktree.id, terminalIdsByWorktree.get(worktree.id));
|
|
6438
6690
|
this.invalidateProjectsSnapshot();
|
|
@@ -6459,7 +6711,7 @@ var TreeportService = class {
|
|
|
6459
6711
|
async reconcile() {
|
|
6460
6712
|
const availableProjects = /* @__PURE__ */ new Set();
|
|
6461
6713
|
for (const project of await this.storedProjects(true)) try {
|
|
6462
|
-
await this.
|
|
6714
|
+
await this.observeAvailableProject(project);
|
|
6463
6715
|
availableProjects.add(project.id);
|
|
6464
6716
|
} catch {}
|
|
6465
6717
|
for (const project of await this.storedProjects(true)) {
|
|
@@ -6827,6 +7079,7 @@ async function terminateProcess(child, terminationStarted) {
|
|
|
6827
7079
|
var TmuxProgressObserver = class {
|
|
6828
7080
|
options;
|
|
6829
7081
|
spawnProcess;
|
|
7082
|
+
closed;
|
|
6830
7083
|
lifecycleFiber;
|
|
6831
7084
|
process = null;
|
|
6832
7085
|
metadataParser = null;
|
|
@@ -6841,6 +7094,7 @@ var TmuxProgressObserver = class {
|
|
|
6841
7094
|
this.options = options;
|
|
6842
7095
|
this.spawnProcess = spawnProcess;
|
|
6843
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);
|
|
6844
7098
|
}
|
|
6845
7099
|
dispose() {
|
|
6846
7100
|
if (this.disposed) return;
|
|
@@ -6997,22 +7251,6 @@ const PROGRAM_COMMANDS = /* @__PURE__ */ new Map([
|
|
|
6997
7251
|
["claude", "claude"],
|
|
6998
7252
|
["codex", "codex"]
|
|
6999
7253
|
]);
|
|
7000
|
-
const SHELL_COMMANDS = /* @__PURE__ */ new Set([
|
|
7001
|
-
"ash",
|
|
7002
|
-
"bash",
|
|
7003
|
-
"csh",
|
|
7004
|
-
"dash",
|
|
7005
|
-
"elvish",
|
|
7006
|
-
"fish",
|
|
7007
|
-
"ksh",
|
|
7008
|
-
"mksh",
|
|
7009
|
-
"nu",
|
|
7010
|
-
"pwsh",
|
|
7011
|
-
"sh",
|
|
7012
|
-
"tcsh",
|
|
7013
|
-
"xonsh",
|
|
7014
|
-
"zsh"
|
|
7015
|
-
]);
|
|
7016
7254
|
var TerminalMetadataRuntimeError = class {
|
|
7017
7255
|
phase;
|
|
7018
7256
|
terminalId;
|
|
@@ -7039,6 +7277,7 @@ var TerminalMetadataManager = class {
|
|
|
7039
7277
|
bellMutations = new KeyedTaskQueue();
|
|
7040
7278
|
bellDeletionVersions = /* @__PURE__ */ new Map();
|
|
7041
7279
|
persistedBells = /* @__PURE__ */ new Map();
|
|
7280
|
+
observerShutdowns = /* @__PURE__ */ new Set();
|
|
7042
7281
|
bellStateStore;
|
|
7043
7282
|
initializePromise = null;
|
|
7044
7283
|
unsubscribeEvents = null;
|
|
@@ -7104,8 +7343,10 @@ var TerminalMetadataManager = class {
|
|
|
7104
7343
|
entry = void 0;
|
|
7105
7344
|
}
|
|
7106
7345
|
if (!entry) {
|
|
7107
|
-
const launchCommand = path.basename(terminal.argv
|
|
7108
|
-
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;
|
|
7109
7350
|
this.bellDeletionVersions.set(terminal.id, (this.bellDeletionVersions.get(terminal.id) ?? 0) + 1);
|
|
7110
7351
|
const persistedBell = this.persistedBells.get(terminal.id);
|
|
7111
7352
|
const bell = persistedBell?.worktreeId === terminal.worktreeId ? {
|
|
@@ -7130,7 +7371,8 @@ var TerminalMetadataManager = class {
|
|
|
7130
7371
|
paneTitle: null,
|
|
7131
7372
|
currentCommand: null,
|
|
7132
7373
|
commandLine: null,
|
|
7133
|
-
|
|
7374
|
+
launchCommandLine,
|
|
7375
|
+
interactiveShellCommand,
|
|
7134
7376
|
launchProgram,
|
|
7135
7377
|
shellTitle: null,
|
|
7136
7378
|
persistedShellTitle: null,
|
|
@@ -7234,8 +7476,9 @@ var TerminalMetadataManager = class {
|
|
|
7234
7476
|
this.listeners.clear();
|
|
7235
7477
|
this.historyListeners.clear();
|
|
7236
7478
|
}
|
|
7237
|
-
drain() {
|
|
7238
|
-
|
|
7479
|
+
async drain() {
|
|
7480
|
+
await this.bellMutations.drain();
|
|
7481
|
+
await Promise.allSettled([...this.observerShutdowns]);
|
|
7239
7482
|
}
|
|
7240
7483
|
handleProductEvent(event) {
|
|
7241
7484
|
if (event.type === "terminal.removed") {
|
|
@@ -7403,7 +7646,7 @@ var TerminalMetadataManager = class {
|
|
|
7403
7646
|
this.update(entry, { progress: null });
|
|
7404
7647
|
}
|
|
7405
7648
|
});
|
|
7406
|
-
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);
|
|
7407
7650
|
else entry.observer = observer;
|
|
7408
7651
|
},
|
|
7409
7652
|
catch: (cause) => new TerminalMetadataRuntimeError("create_observer", entry.terminalId, cause)
|
|
@@ -7432,17 +7675,31 @@ var TerminalMetadataManager = class {
|
|
|
7432
7675
|
}
|
|
7433
7676
|
this.update(entry, { progress: null });
|
|
7434
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
|
+
}
|
|
7435
7685
|
releaseRuntimeResources(entry) {
|
|
7436
7686
|
entry.observerVersion += 1;
|
|
7437
|
-
entry.observer
|
|
7438
|
-
|
|
7687
|
+
if (entry.observer) {
|
|
7688
|
+
this.disposeObserver(entry.observer);
|
|
7689
|
+
entry.observer = null;
|
|
7690
|
+
}
|
|
7439
7691
|
this.clearProgressRuntime(entry);
|
|
7440
7692
|
entry.shellTitleWriting = false;
|
|
7441
7693
|
}
|
|
7442
7694
|
reconcileTitleState(entry, state) {
|
|
7443
7695
|
const paneTitle = state.paneTitle?.trim().slice(0, 256) || null;
|
|
7444
7696
|
const currentCommand = state.currentCommand?.trim().slice(0, 256) || null;
|
|
7445
|
-
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;
|
|
7446
7703
|
const previousCommand = entry.currentCommand;
|
|
7447
7704
|
const previousCommandLine = entry.commandLine;
|
|
7448
7705
|
const paneTitleChanged = paneTitle !== entry.paneTitle;
|
|
@@ -7462,12 +7719,8 @@ var TerminalMetadataManager = class {
|
|
|
7462
7719
|
const commandToken = commandLine?.match(/^(?:exec\s+|command\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))/);
|
|
7463
7720
|
const commandExecutable = commandToken ? commandToken[1] || commandToken[2] || commandToken[3] || null : null;
|
|
7464
7721
|
const observedProgram = PROGRAM_COMMANDS.get(path.basename(commandExecutable ?? "").replace(/^-/, "")) ?? PROGRAM_COMMANDS.get(path.basename(currentCommand ?? "").replace(/^-/, "")) ?? null;
|
|
7465
|
-
this.update(entry, { program: observedProgram ?? (!entry.
|
|
7466
|
-
if (
|
|
7467
|
-
this.update(entry, { title: paneTitle ?? commandLine ?? currentCommand });
|
|
7468
|
-
return;
|
|
7469
|
-
}
|
|
7470
|
-
if (currentCommand === entry.shellCommand) {
|
|
7722
|
+
this.update(entry, { program: observedProgram ?? (!entry.interactiveShellCommand ? entry.launchProgram : null) });
|
|
7723
|
+
if (entry.interactiveShellCommand && currentCommand === entry.interactiveShellCommand) {
|
|
7471
7724
|
const applicationTitleWasActive = entry.applicationTitleActive;
|
|
7472
7725
|
const freshShellTitle = observedTitlePending || previousCommand !== null && paneTitleChanged;
|
|
7473
7726
|
entry.applicationTitleActive = false;
|
|
@@ -7481,11 +7734,15 @@ var TerminalMetadataManager = class {
|
|
|
7481
7734
|
}
|
|
7482
7735
|
if (commandLine) {
|
|
7483
7736
|
if (observedTitlePending) entry.applicationTitleActive = paneTitle !== null && paneTitle !== commandLine;
|
|
7484
|
-
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;
|
|
7485
7738
|
else if (paneTitleChanged && paneTitle !== commandLine) entry.applicationTitleActive = true;
|
|
7486
7739
|
this.update(entry, { title: entry.applicationTitleActive ? paneTitle ?? entry.title ?? commandLine : commandLine });
|
|
7487
7740
|
return;
|
|
7488
7741
|
}
|
|
7742
|
+
if (!entry.interactiveShellCommand) {
|
|
7743
|
+
this.update(entry, { title: paneTitle ?? currentCommand });
|
|
7744
|
+
return;
|
|
7745
|
+
}
|
|
7489
7746
|
if (observedTitlePending) {
|
|
7490
7747
|
entry.applicationTitleActive = true;
|
|
7491
7748
|
this.update(entry, { title: entry.title ?? paneTitle ?? currentCommand });
|
|
@@ -7514,7 +7771,7 @@ var TerminalMetadataManager = class {
|
|
|
7514
7771
|
}
|
|
7515
7772
|
updateForegroundProcess(entry, currentCommand) {
|
|
7516
7773
|
const command = currentCommand?.trim().slice(0, 256) || null;
|
|
7517
|
-
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 });
|
|
7518
7775
|
}
|
|
7519
7776
|
persistShellTitle(entry, runtimeGeneration) {
|
|
7520
7777
|
return Effect.gen(this, function* () {
|
|
@@ -7638,7 +7895,7 @@ function queryInput(schema) {
|
|
|
7638
7895
|
if (!result.success) throw new DomainError("VALIDATION_ERROR", "Request validation failed", 400, z.flattenError(result.error));
|
|
7639
7896
|
});
|
|
7640
7897
|
}
|
|
7641
|
-
function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
7898
|
+
function createApp({ service, config, tmux, applicationUpdate, terminalMetadata, webDist }) {
|
|
7642
7899
|
const app = new Hono();
|
|
7643
7900
|
app.use("/api/*", requestId({
|
|
7644
7901
|
limitLength: 128,
|
|
@@ -7688,7 +7945,13 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7688
7945
|
installationMethod: config.installationMethod ?? "development",
|
|
7689
7946
|
daemonLifecycle: config.daemonLifecycle,
|
|
7690
7947
|
url: config.apiUrl
|
|
7691
|
-
})).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) => {
|
|
7692
7955
|
const body = context.req.valid("json");
|
|
7693
7956
|
return context.json({ preset: await service.createTerminalPreset(body) }, 201);
|
|
7694
7957
|
}).patch("/api/terminal-presets/:presetId", jsonInput(updateTerminalPresetSchema), async (context) => {
|
|
@@ -7720,6 +7983,9 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7720
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) => {
|
|
7721
7984
|
await service.closeProject(context.req.param("projectId"));
|
|
7722
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 });
|
|
7723
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) => {
|
|
7724
7990
|
const body = context.req.valid("json");
|
|
7725
7991
|
const projectId = context.req.param("projectId");
|
|
@@ -7746,6 +8012,9 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7746
8012
|
const worktreeId = context.req.param("worktreeId");
|
|
7747
8013
|
await service.refreshPr(worktreeId, false);
|
|
7748
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 });
|
|
7749
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) => {
|
|
7750
8019
|
const body = context.req.valid("json");
|
|
7751
8020
|
return context.json({ panel: await service.createWebPanel(context.req.param("worktreeId"), body.definitionId, {
|
|
@@ -7827,6 +8096,7 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7827
8096
|
if (body.initialSize) options.initialSize = body.initialSize;
|
|
7828
8097
|
if (body.cwd) options.cwd = body.cwd;
|
|
7829
8098
|
if (body.env) options.env = body.env;
|
|
8099
|
+
if (body.shellCommand) options.shellCommand = body.shellCommand;
|
|
7830
8100
|
const terminal = await service.createTerminal(context.req.param("worktreeId"), body.name, body.argv, Object.keys(options).length > 0 ? options : void 0);
|
|
7831
8101
|
return context.json({ terminal }, 201);
|
|
7832
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) => {
|
|
@@ -7947,6 +8217,247 @@ function createApp({ service, config, tmux, terminalMetadata, webDist }) {
|
|
|
7947
8217
|
return routes;
|
|
7948
8218
|
}
|
|
7949
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
|
|
7950
8461
|
//#region src/server/daemon-ownership.ts
|
|
7951
8462
|
function processExists(pid) {
|
|
7952
8463
|
try {
|
|
@@ -8361,7 +8872,7 @@ var TerminalAttachmentManager = class {
|
|
|
8361
8872
|
catch: (cause) => new AttachmentInitializationError(phase, cause)
|
|
8362
8873
|
});
|
|
8363
8874
|
return Effect.gen(this, function* () {
|
|
8364
|
-
const terminal = yield* promisePhase("refresh_terminal", () => this.service.refreshTerminalStatus(connection.terminalId));
|
|
8875
|
+
const terminal = yield* promisePhase("refresh_terminal", () => this.service.refreshTerminalStatus(connection.terminalId, false));
|
|
8365
8876
|
if (!isInitializing()) return;
|
|
8366
8877
|
if (terminal.status === "missing") return yield* Effect.fail(new AttachmentInitializationError("refresh_terminal", /* @__PURE__ */ new Error("The tmux session for this terminal is missing")));
|
|
8367
8878
|
const worktree = yield* promisePhase("resolve_worktree", () => this.service.getWorktree(terminal.worktreeId));
|
|
@@ -8370,12 +8881,12 @@ var TerminalAttachmentManager = class {
|
|
|
8370
8881
|
const initialDimensions = yield* Effect.tryPromise({
|
|
8371
8882
|
try: () => this.enqueueTerminal(connection.terminalId, async () => {
|
|
8372
8883
|
if (!isInitializing()) return null;
|
|
8884
|
+
const current = this.dimensions.get(connection.terminalId);
|
|
8885
|
+
if (current) return current;
|
|
8373
8886
|
await this.tmux.useManualWindowSize(worktree.tmuxSocketName, terminal.tmuxSessionName).catch((cause) => {
|
|
8374
8887
|
throw new AttachmentInitializationError("configure_window_size", cause);
|
|
8375
8888
|
});
|
|
8376
8889
|
if (!isInitializing()) return null;
|
|
8377
|
-
const current = this.dimensions.get(connection.terminalId);
|
|
8378
|
-
if (current) return current;
|
|
8379
8890
|
const sessionSize = await this.tmux.sessionSize(worktree.tmuxSocketName, terminal.tmuxSessionName).catch((cause) => {
|
|
8380
8891
|
throw new AttachmentInitializationError("read_session_size", cause);
|
|
8381
8892
|
});
|
|
@@ -8867,107 +9378,126 @@ function createSocketServer(httpServer, { service, config, tmux, terminalMetadat
|
|
|
8867
9378
|
}
|
|
8868
9379
|
//#endregion
|
|
8869
9380
|
//#region src/server/index.ts
|
|
8870
|
-
|
|
8871
|
-
const
|
|
8872
|
-
const
|
|
8873
|
-
|
|
8874
|
-
const
|
|
8875
|
-
const
|
|
8876
|
-
const
|
|
8877
|
-
|
|
8878
|
-
const
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
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
|
+
}
|
|
8905
9430
|
honoListener(request, response);
|
|
8906
9431
|
});
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
middlewareMode: true,
|
|
8926
|
-
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
|
+
});
|
|
8927
9450
|
}
|
|
8928
|
-
|
|
8929
|
-
}
|
|
8930
|
-
service
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
tmux,
|
|
8935
|
-
terminalMetadata
|
|
8936
|
-
});
|
|
8937
|
-
await new Promise((resolve, reject) => {
|
|
8938
|
-
server.once("error", reject);
|
|
8939
|
-
server.listen(config.port, config.host, () => {
|
|
8940
|
-
server.off("error", reject);
|
|
8941
|
-
resolve();
|
|
8942
|
-
});
|
|
8943
|
-
});
|
|
8944
|
-
await ownership.publish();
|
|
8945
|
-
console.log(`Treeport ${config.appVersion} listening on ${config.apiUrl}`);
|
|
8946
|
-
console.log(`database: ${config.databasePath}`);
|
|
8947
|
-
console.log(`git: ${prerequisites.gitVersion}`);
|
|
8948
|
-
console.log(`tmux: ${prerequisites.tmuxVersion}`);
|
|
8949
|
-
let shuttingDown = false;
|
|
8950
|
-
function shutdown() {
|
|
8951
|
-
if (shuttingDown) return;
|
|
8952
|
-
shuttingDown = true;
|
|
8953
|
-
attachments.dispose();
|
|
8954
|
-
terminalMetadata.dispose();
|
|
8955
|
-
const viteClosed = vite?.close();
|
|
8956
|
-
io.close(() => {
|
|
8957
|
-
Promise.all([
|
|
8958
|
-
service.drainMutations(),
|
|
8959
|
-
terminalMetadata.drain(),
|
|
8960
|
-
viteClosed
|
|
8961
|
-
]).then(async () => {
|
|
8962
|
-
await service.disposeWebPanelRuntime();
|
|
8963
|
-
database.close();
|
|
8964
|
-
await ownership.release();
|
|
8965
|
-
process.exit(0);
|
|
9451
|
+
service.attachHttpServer(server);
|
|
9452
|
+
const { io, attachments } = createSocketServer(server, {
|
|
9453
|
+
service,
|
|
9454
|
+
config,
|
|
9455
|
+
tmux,
|
|
9456
|
+
terminalMetadata
|
|
8966
9457
|
});
|
|
8967
|
-
|
|
8968
|
-
|
|
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
|
+
}
|
|
8969
9500
|
}
|
|
8970
|
-
|
|
8971
|
-
process.once("SIGTERM", shutdown);
|
|
9501
|
+
await main();
|
|
8972
9502
|
//#endregion
|
|
8973
9503
|
export {};
|