@treeport/treeport 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,402 +1,12 @@
1
+ import { Ct as projectsResponseSchema, U as decodeUnknownOrNull, ct as healthResponseSchema } from "./dist-BsLn2Gbc.js";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { z } from "zod";
4
4
  import { spawn } from "node:child_process";
5
5
  import crypto from "node:crypto";
6
6
  import fsSync, { constants } from "node:fs";
7
7
  import os from "node:os";
8
+ import { z } from "zod";
8
9
  import { fileURLToPath, pathToFileURL } from "node:url";
9
- //#region ../../packages/shared/dist/terminal-protocol.js
10
- const SOCKET_IO_PATH = "/api/socket.io/";
11
- const TERMINAL_CONTROLLER_GRACE_MS = 1e4;
12
- const TERMINAL_OUTPUT_HIGH_WATERMARK = 256 * 1024;
13
- const TERMINAL_OUTPUT_LOW_WATERMARK = 64 * 1024;
14
- const TERMINAL_OUTPUT_STALL_TIMEOUT_MS = 3e4;
15
- const TERMINAL_MAX_CLIENT_MESSAGE_BYTES = 128 * 1024;
16
- const TERMINAL_MAX_INPUT_BYTES = 64 * 1024;
17
- const TERMINAL_SCROLL_EXIT_SEQUENCE = "\x1B[9000~";
18
- const TERMINAL_SELECTION_START_SEQUENCE = "\x1B[9001~";
19
- const TERMINAL_SELECTION_STOP_SEQUENCE = "\x1B[9002~";
20
- const TERMINAL_SELECTION_CLEAR_SEQUENCE = "\x1B[9003~";
21
- const TERMINAL_SELECTION_RESTORE_SEQUENCE = "\x1B[9004~";
22
- z.unknown();
23
- const terminalId = z.string().min(1).max(128);
24
- const clientId = z.string().min(1).max(128);
25
- const streamId = z.string().min(1).max(128);
26
- const generation = z.number().int().nonnegative();
27
- const dimensions = {
28
- cols: z.number().int().min(2).max(1e3),
29
- rows: z.number().int().min(2).max(500)
30
- };
31
- const terminalSizeSchema = z.strictObject(dimensions);
32
- const terminalProgressSchema = z.strictObject({
33
- state: z.enum([
34
- "normal",
35
- "error",
36
- "indeterminate",
37
- "paused"
38
- ]),
39
- value: z.number().int().min(0).max(100).nullable()
40
- });
41
- const terminalProgramSchema = z.enum([
42
- "pi",
43
- "claude",
44
- "codex"
45
- ]);
46
- const terminalRuntimeMetadataSchema = z.strictObject({
47
- terminalId: z.string().min(1),
48
- title: z.string().max(256).nullable(),
49
- program: terminalProgramSchema.nullable().default(null),
50
- hasForegroundProcess: z.boolean().nullable().optional(),
51
- progress: terminalProgressSchema.nullable(),
52
- progressStartedAt: z.string().datetime().nullable().default(null),
53
- progressClearedAt: z.string().datetime().nullable().default(null),
54
- bell: z.strictObject({
55
- sequence: z.number().int().positive(),
56
- at: z.string().datetime(),
57
- unread: z.boolean()
58
- }).nullable().default(null)
59
- });
60
- const terminalBellAcknowledgementSchema = z.strictObject({ sequence: z.number().int().positive() });
61
- function parseTerminalProgress(data) {
62
- const [command, rawState, rawValue, ...extra] = data.split(";");
63
- if (command !== "4" || extra.length > 0 || !/^[0-4]$/.test(rawState ?? "")) return;
64
- const state = Number(rawState);
65
- if (state === 0) return null;
66
- if (rawValue !== void 0 && rawValue !== "" && !/^\d{1,3}$/.test(rawValue)) return;
67
- const value = rawValue === void 0 || rawValue === "" ? null : Number(rawValue);
68
- if (value !== null && value > 100) return;
69
- return {
70
- state: [
71
- void 0,
72
- "normal",
73
- "error",
74
- "indeterminate",
75
- "paused"
76
- ][state],
77
- value
78
- };
79
- }
80
- const terminalAuthSchema = z.strictObject({
81
- terminalId,
82
- clientId,
83
- ...dimensions
84
- });
85
- const terminalInputSchema = z.strictObject({
86
- generation,
87
- data: z.string().max(TERMINAL_MAX_INPUT_BYTES)
88
- });
89
- const terminalBinarySchema = z.strictObject({
90
- generation,
91
- data: z.string().max(TERMINAL_MAX_INPUT_BYTES)
92
- });
93
- const terminalResizeSchema = z.strictObject({
94
- generation,
95
- ...dimensions
96
- });
97
- const terminalTakeControlSchema = z.strictObject({
98
- generation,
99
- ...dimensions
100
- });
101
- const terminalLegacyTakeControlSchema = z.strictObject({ generation });
102
- const terminalOutputAckSchema = z.strictObject({
103
- streamId,
104
- sequence: z.number().int().nonnegative()
105
- });
106
- const terminalReadyBase = {
107
- connectionId: z.string().min(1).max(128),
108
- streamId,
109
- generation,
110
- controller: z.boolean(),
111
- reset: z.literal("full")
112
- };
113
- const terminalLegacyReadySchema = z.strictObject(terminalReadyBase);
114
- const terminalReadyV2Schema = z.strictObject({
115
- ...terminalReadyBase,
116
- ...dimensions,
117
- revision: z.number().int().positive()
118
- });
119
- z.union([terminalLegacyReadySchema, terminalReadyV2Schema]);
120
- z.strictObject({
121
- ...dimensions,
122
- revision: z.number().int().positive()
123
- });
124
- z.strictObject({
125
- streamId,
126
- sequence: z.number().int().positive(),
127
- data: z.string()
128
- });
129
- z.strictObject({ title: z.string().max(256) });
130
- z.strictObject({ progress: terminalProgressSchema.nullable() });
131
- z.strictObject({ viewing: z.boolean() });
132
- z.strictObject({
133
- generation,
134
- controller: z.boolean()
135
- });
136
- z.strictObject({ exitCode: z.number().int().nullable() });
137
- z.strictObject({
138
- code: z.string().min(1).max(80),
139
- message: z.string().min(1).max(1e3),
140
- retryable: z.boolean()
141
- });
142
- function parseTerminalAuth(value) {
143
- const parsed = terminalAuthSchema.safeParse(value);
144
- return parsed.success ? parsed.data : null;
145
- }
146
- //#endregion
147
- //#region ../../packages/shared/dist/socket-protocol.js
148
- const identifierSchema = z.string().min(1).max(128);
149
- const eventEnvelope = (type, data) => z.strictObject({
150
- id: identifierSchema,
151
- type: z.literal(type),
152
- at: z.string().datetime(),
153
- data
154
- });
155
- const projectEventDataSchema = z.strictObject({
156
- projectId: identifierSchema,
157
- worktreeId: z.null()
158
- });
159
- const worktreeEventDataSchema = z.strictObject({ worktreeId: identifierSchema });
160
- const projectWorktreeEventDataSchema = z.strictObject({
161
- projectId: identifierSchema,
162
- worktreeId: identifierSchema
163
- });
164
- const operationEventDataSchema = z.strictObject({
165
- operationId: identifierSchema,
166
- worktreeId: identifierSchema
167
- });
168
- const productEventSchema = z.discriminatedUnion("type", [
169
- eventEnvelope("project.created", projectEventDataSchema),
170
- eventEnvelope("project.updated", projectEventDataSchema),
171
- eventEnvelope("project.removed", projectEventDataSchema),
172
- eventEnvelope("worktree.created", projectWorktreeEventDataSchema),
173
- eventEnvelope("worktree.updated", worktreeEventDataSchema),
174
- eventEnvelope("worktree.removed", projectWorktreeEventDataSchema),
175
- eventEnvelope("create.started", z.strictObject({
176
- projectId: identifierSchema,
177
- operationId: identifierSchema,
178
- worktreeId: z.null()
179
- })),
180
- eventEnvelope("create.completed", z.strictObject({
181
- projectId: identifierSchema,
182
- operationId: identifierSchema,
183
- worktreeId: identifierSchema
184
- })),
185
- eventEnvelope("create.failed", z.strictObject({
186
- projectId: identifierSchema,
187
- operationId: identifierSchema,
188
- worktreeId: z.null()
189
- })),
190
- eventEnvelope("terminal.created", z.strictObject({
191
- projectId: identifierSchema.optional(),
192
- worktreeId: identifierSchema,
193
- terminalId: identifierSchema
194
- })),
195
- eventEnvelope("terminal.updated", z.strictObject({
196
- worktreeId: identifierSchema,
197
- terminalId: identifierSchema
198
- })),
199
- eventEnvelope("terminal.removed", z.strictObject({
200
- worktreeId: identifierSchema,
201
- terminalId: identifierSchema
202
- })),
203
- eventEnvelope("terminal.metadata", terminalRuntimeMetadataSchema.extend({ worktreeId: z.null() })),
204
- eventEnvelope("terminal.controller_changed", z.strictObject({
205
- terminalId: identifierSchema,
206
- controlled: z.boolean(),
207
- worktreeId: z.null()
208
- })),
209
- eventEnvelope("panel.created", z.strictObject({
210
- worktreeId: identifierSchema,
211
- panelId: identifierSchema
212
- })),
213
- eventEnvelope("panel.updated", z.strictObject({
214
- worktreeId: identifierSchema,
215
- panelId: identifierSchema
216
- })),
217
- eventEnvelope("panel.open_requested", z.strictObject({
218
- worktreeId: identifierSchema,
219
- panelId: identifierSchema,
220
- sourceTerminalId: identifierSchema.nullable()
221
- })),
222
- eventEnvelope("panel.removed", z.strictObject({
223
- worktreeId: identifierSchema,
224
- panelId: identifierSchema
225
- })),
226
- eventEnvelope("workspace.open_requested", z.strictObject({
227
- worktreeId: identifierSchema,
228
- sourceTerminalId: identifierSchema
229
- })),
230
- eventEnvelope("remove.started", operationEventDataSchema.extend({ kind: z.literal("remove") })),
231
- eventEnvelope("remove.completed", operationEventDataSchema),
232
- eventEnvelope("remove.failed", operationEventDataSchema.extend({ error: z.string() }))
233
- ]);
234
- const webPanelSnapshotSchema = z.strictObject({
235
- id: z.string().min(1),
236
- kind: z.literal("web"),
237
- worktreeId: z.string().min(1),
238
- definitionId: z.string().min(1),
239
- title: z.string().min(1),
240
- launch: z.strictObject({
241
- input: z.record(z.string(), z.json()).nullable(),
242
- cwd: z.string().nullable()
243
- }),
244
- sandbox: z.strictObject({ allowSameOrigin: z.boolean() }),
245
- createdAt: z.string(),
246
- updatedAt: z.string()
247
- });
248
- const eventsSnapshotSchema = z.strictObject({
249
- at: z.string().datetime(),
250
- terminalMetadata: z.array(terminalRuntimeMetadataSchema),
251
- webPanels: z.array(webPanelSnapshotSchema)
252
- });
253
- z.unknown();
254
- function parseEventsSnapshot(value) {
255
- const parsed = eventsSnapshotSchema.safeParse(value);
256
- return parsed.success ? parsed.data : null;
257
- }
258
- function parseProductEvent(value) {
259
- const parsed = productEventSchema.safeParse(value);
260
- return parsed.success ? parsed.data : null;
261
- }
262
- //#endregion
263
- //#region ../../packages/shared/dist/index.js
264
- const TERMINAL_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
265
- const TERMINAL_EXECUTABLE_MAX_LENGTH = 4096;
266
- const TERMINAL_ARGUMENT_MAX_LENGTH = 4096;
267
- const TERMINAL_CAPTURE_MAX_LINES = 5e3;
268
- const WEB_PANEL_INPUT_MAX_BYTES = 64 * 1024;
269
- function formatCommandLine(argv) {
270
- return argv.map((value) => {
271
- if (value === "") return "\"\"";
272
- if (!/[\s"'\\]/.test(value)) return value;
273
- return `"${value.replace(/["\\]/g, "\\$&")}"`;
274
- }).join(" ");
275
- }
276
- const PROJECT_COLORS = [
277
- "rose",
278
- "orange",
279
- "amber",
280
- "emerald",
281
- "cyan",
282
- "blue",
283
- "violet",
284
- "pink"
285
- ];
286
- const browseDirectoryQuerySchema = z.object({
287
- input: z.string().trim().min(1).max(4096),
288
- hidden: z.enum(["true", "false"]).optional().default("false").transform((value) => value === "true")
289
- });
290
- const terminalCaptureQuerySchema = z.object({ lines: z.coerce.number().int().min(1).max(TERMINAL_CAPTURE_MAX_LINES).optional().default(200) });
291
- const registerProjectSchema = z.object({
292
- path: z.string().trim().min(1),
293
- name: z.string().trim().min(1).max(120).optional()
294
- });
295
- const updateProjectSchema = z.object({ color: z.enum(PROJECT_COLORS).nullable() });
296
- const terminalNameSchema = z.string().trim().min(1).max(120);
297
- const terminalArgvSchema = z.array(z.string()).min(1).max(128);
298
- const terminalPresetArgumentSchema = z.string().max(TERMINAL_ARGUMENT_MAX_LENGTH);
299
- const terminalPresetFields = {
300
- name: terminalNameSchema,
301
- executable: z.string().min(1).max(TERMINAL_EXECUTABLE_MAX_LENGTH).refine((value) => value.trim().length > 0, { message: "Executable cannot be blank" }),
302
- args: z.array(terminalPresetArgumentSchema).max(127),
303
- closeOnSuccess: z.boolean().default(false)
304
- };
305
- const repositoryTerminalPresetSchema = z.strictObject(terminalPresetFields);
306
- const repositoryTerminalPresetIdSchema = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,119}$/, { message: "Preset IDs must contain only lowercase letters, numbers, dots, underscores, and hyphens" });
307
- const repositoryTerminalPresetsFileSchema = z.strictObject({
308
- version: z.literal(1),
309
- presets: z.record(repositoryTerminalPresetIdSchema, z.unknown())
310
- });
311
- const terminalPresetRevisionSchema = z.string().min(1).max(64);
312
- const initialTerminalSchema = z.object({
313
- name: terminalNameSchema,
314
- argv: terminalArgvSchema.optional(),
315
- returnToShell: z.boolean().optional(),
316
- initialSize: terminalSizeSchema.optional()
317
- });
318
- const createWorktreeSchema = z.object({
319
- name: z.string().trim().min(1).max(120),
320
- base: z.enum(["default", "current"]).default("default"),
321
- sourceWorktreeId: z.string().min(1).optional(),
322
- initialTerminal: initialTerminalSchema.optional()
323
- }).superRefine((value, context) => {
324
- if (value.base === "current" && !value.sourceWorktreeId) context.addIssue({
325
- code: "custom",
326
- path: ["sourceWorktreeId"],
327
- message: "A source tree is required when starting from current"
328
- });
329
- });
330
- const terminalCwdSchema = z.string().min(1).max(4096).refine((value) => value.trim().length > 0 && !value.includes("\0"), { message: "Working directory cannot be blank or contain NUL" });
331
- const terminalEnvironmentKeySchema = z.string().min(1).max(256).refine((value) => !value.includes("=") && !value.includes("\0"), { message: "Environment keys cannot contain equals or NUL" });
332
- const terminalShellCommandSchema = z.string().min(1).max(TERMINAL_ARGUMENT_MAX_LENGTH).refine((value) => value.trim().length > 0 && !value.includes("\0"), { message: "Shell command cannot be blank or contain NUL" });
333
- const terminalEnvironmentSchema = z.record(terminalEnvironmentKeySchema, z.string().max(TERMINAL_ARGUMENT_MAX_LENGTH).refine((value) => !value.includes("\0"), { message: "Environment values cannot contain NUL" })).refine((value) => Object.keys(value).length <= 128, { message: "Environment cannot contain more than 128 variables" });
334
- const createTerminalSchema = z.object({
335
- name: terminalNameSchema,
336
- argv: terminalArgvSchema.optional(),
337
- shellCommand: terminalShellCommandSchema.optional(),
338
- cwd: terminalCwdSchema.optional(),
339
- env: terminalEnvironmentSchema.optional(),
340
- returnToShell: z.boolean().optional(),
341
- closeOnSuccess: z.boolean().optional(),
342
- initialSize: terminalSizeSchema.optional()
343
- }).refine((value) => !(value.argv && value.shellCommand), { message: "A terminal cannot have both argv and a shell command" }).refine((value) => !(value.returnToShell && value.closeOnSuccess), { message: "A terminal cannot return to a shell and close on success" });
344
- const updateTerminalSchema = z.object({ name: terminalNameSchema });
345
- const webPanelInputSchema = z.record(z.string(), z.json());
346
- const createWebPanelSchema = z.object({
347
- definitionId: z.string().min(1).max(256),
348
- input: webPanelInputSchema.nullable().optional(),
349
- launchCwd: z.string().max(4096).nullable().optional()
350
- });
351
- const openWebPanelSchema = createWebPanelSchema.extend({
352
- newInstance: z.boolean().optional(),
353
- sourceTerminalId: z.string().min(1).max(128).nullable().optional()
354
- });
355
- const requestWorkspaceOpenSchema = z.object({ sourceTerminalId: z.string().min(1).max(128) });
356
- const webPanelStorageKeySchema = z.string().min(1).max(128);
357
- const getWebPanelStorageSchema = z.object({ key: webPanelStorageKeySchema });
358
- const setWebPanelStorageSchema = z.object({
359
- key: webPanelStorageKeySchema,
360
- value: z.json()
361
- });
362
- const deleteWebPanelStorageSchema = z.object({ key: webPanelStorageKeySchema });
363
- const createTerminalPresetSchema = z.object(terminalPresetFields);
364
- const updateTerminalPresetSchema = z.object({
365
- ...terminalPresetFields,
366
- closeOnSuccess: z.boolean().optional(),
367
- expectedUpdatedAt: terminalPresetRevisionSchema
368
- });
369
- const deleteTerminalPresetSchema = z.object({ expectedUpdatedAt: terminalPresetRevisionSchema });
370
- const packageProjectQuerySchema = z.object({ path: z.string().trim().min(1).max(4096) });
371
- const packageInstallSchema = z.object({
372
- source: z.string().trim().min(1).max(4096),
373
- projectId: z.string().min(1).optional()
374
- });
375
- const packageRemoveSchema = z.object({
376
- source: z.string().trim().min(1).max(4096),
377
- projectId: z.string().min(1).optional()
378
- });
379
- const packageUpdateSchema = z.object({ source: z.string().trim().min(1).max(4096).optional() });
380
- const packageReloadSchema = z.object({ projectId: z.string().min(1).optional() });
381
- const removeWorktreeSchema = z.object({
382
- confirmationToken: z.string().length(64),
383
- confirmDestructive: z.boolean()
384
- });
385
- z.object({
386
- project: z.string().min(1),
387
- worktreeName: z.string().trim().min(1).max(120),
388
- name: terminalNameSchema,
389
- argv: terminalArgvSchema.optional(),
390
- base: z.enum(["default", "current"]).default("default"),
391
- sourceWorktreeId: z.string().min(1).optional()
392
- }).superRefine((value, context) => {
393
- if (value.base === "current" && !value.sourceWorktreeId) context.addIssue({
394
- code: "custom",
395
- path: ["sourceWorktreeId"],
396
- message: "A source tree is required when starting from current"
397
- });
398
- });
399
- //#endregion
400
10
  //#region src/duration.ts
401
11
  const DURATION_UNITS = /* @__PURE__ */ new Map([
402
12
  ["ms", 1],
@@ -477,21 +87,6 @@ const daemonRecordSchema = z.strictObject({
477
87
  "external"
478
88
  ])
479
89
  });
480
- const healthRecordSchema = z.strictObject({
481
- ok: z.literal(true),
482
- version: z.string(),
483
- protocolVersion: z.number(),
484
- hostname: z.string().optional(),
485
- pid: z.number(),
486
- instanceId: z.string().nullable(),
487
- installationMethod: z.string(),
488
- daemonLifecycle: z.enum([
489
- "treeport",
490
- "service",
491
- "external"
492
- ]),
493
- url: z.string()
494
- });
495
90
  async function preferences(env = process.env) {
496
91
  return await readJson$1(localPaths(env).preferencesPath, preferencesSchema) ?? {};
497
92
  }
@@ -542,15 +137,18 @@ async function daemonHealth(apiUrl, timeoutMs = 1500) {
542
137
  const signal = AbortSignal.timeout(timeoutMs);
543
138
  return fetch(`${apiUrl}/api/health`, { signal }).then(async (response) => {
544
139
  if (!response.ok) return null;
545
- const result = healthRecordSchema.safeParse(await response.json());
546
- return result.success ? result.data : null;
140
+ return decodeUnknownOrNull(healthResponseSchema, await response.json());
547
141
  }).catch(() => null);
548
142
  }
549
143
  function matchesOwnership(state, observed) {
550
144
  return observed.pid === state.pid && observed.instanceId === state.instanceId && path.resolve(state.dataDir) === localPaths().dataDir;
551
145
  }
552
146
  async function readState() {
553
- return readJson$1(localPaths().statePath, daemonRecordSchema);
147
+ const paths = localPaths();
148
+ return await fs.readFile(paths.lockPath, "utf8").then((value) => daemonRecordSchema.parse(JSON.parse(value))).catch((error) => {
149
+ if (error.code === "ENOENT") return null;
150
+ throw new Error(`Cannot verify daemon ownership at ${paths.lockPath}. Inspect the daemon log before starting or stopping Treeport.`, { cause: error });
151
+ }) ?? readJson$1(paths.statePath, daemonRecordSchema);
554
152
  }
555
153
  async function removeStaleState(state) {
556
154
  const paths = localPaths();
@@ -764,11 +362,7 @@ async function disableTailscaleRemote() {
764
362
  }
765
363
  async function runDoctor() {
766
364
  const paths = localPaths();
767
- const gitPath = process.env.TREEPORT_GIT_PATH?.trim() || "git";
768
- const tmuxPath = process.env.TREEPORT_TMUX_PATH?.trim() || "tmux";
769
- const [git, tmux] = await Promise.all([executableCheck(gitPath, ["--version"]), executableCheck(tmuxPath, ["-V"])]);
770
- const tmuxMatch = /tmux\s+(\d+)\.(\d+)/i.exec(tmux.detail);
771
- const tmuxSupported = Boolean(tmux.ok && tmuxMatch && (Number(tmuxMatch[1]) > 3 || Number(tmuxMatch[1]) === 3 && Number(tmuxMatch[2]) >= 2));
365
+ const git = await executableCheck(process.env.TREEPORT_GIT_PATH?.trim() || "git", ["--version"]);
772
366
  const checkDirectory = (directoryPath) => fs.mkdir(directoryPath, {
773
367
  recursive: true,
774
368
  mode: 448
@@ -790,11 +384,6 @@ async function runDoctor() {
790
384
  name: "Git",
791
385
  ...git
792
386
  },
793
- {
794
- name: "tmux",
795
- ok: tmuxSupported,
796
- detail: tmuxSupported ? tmux.detail : `${tmux.detail}. Treeport requires tmux 3.2 or newer.`
797
- },
798
387
  {
799
388
  name: "Data directory",
800
389
  ...dataDirectory
@@ -1274,7 +863,6 @@ function createServiceEnvironment(input) {
1274
863
  TREEPORT_CACHE_DIR: cacheDirectory(input.user.homedir, env),
1275
864
  TREEPORT_DATABASE_PATH: env.TREEPORT_DATABASE_PATH?.trim() || path.join(input.paths.dataDir, "treeport.db"),
1276
865
  TREEPORT_SHELL: env.TREEPORT_SHELL?.trim() || env.SHELL?.trim() || "/bin/sh",
1277
- TREEPORT_TMUX_PATH: env.TREEPORT_TMUX_PATH?.trim() || "tmux",
1278
866
  TREEPORT_GIT_PATH: env.TREEPORT_GIT_PATH?.trim() || "git",
1279
867
  TREEPORT_GH_PATH: env.TREEPORT_GH_PATH?.trim() || "gh",
1280
868
  TREEPORT_DAEMON_LIFECYCLE: "service",
@@ -2238,16 +1826,18 @@ async function createUpdateStartupReporter(config) {
2238
1826
  const paths = updatePaths(config.dataDir);
2239
1827
  const pending = await fs.readFile(paths.pending, "utf8").then((value) => pendingSchema.safeParse(JSON.parse(value))).then((result) => result.success ? result.data : null).catch(() => null);
2240
1828
  const active = pending && pending.targetVersion === config.appVersion ? pending : null;
1829
+ const previous = await readUpdateStartupReport(config.dataDir);
1830
+ const previousState = active && previous?.operationId === active.operationId && previous.targetVersion === active.targetVersion ? previous.migrationState : "unknown";
2241
1831
  const report = active ? {
2242
1832
  schemaVersion: 1,
2243
1833
  operationId: active.operationId,
2244
1834
  targetVersion: active.targetVersion,
2245
1835
  instanceId: config.instanceId ?? null,
2246
- migrationState: "not_started",
1836
+ migrationState: previousState,
2247
1837
  ready: false,
2248
1838
  error: null,
2249
1839
  logPath: path.join(config.dataDir, "logs", "daemon.log"),
2250
- snapshotPaths: [],
1840
+ snapshotPaths: previous?.operationId === active.operationId && previous.targetVersion === active.targetVersion ? previous.snapshotPaths : [],
2251
1841
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2252
1842
  } : null;
2253
1843
  const save = async () => {
@@ -2259,14 +1849,20 @@ async function createUpdateStartupReporter(config) {
2259
1849
  return {
2260
1850
  async databaseOpening() {
2261
1851
  if (report) {
2262
- report.migrationState = "unknown";
1852
+ report.migrationState = previousState === "advanced" ? "advanced" : "unknown";
1853
+ await save();
1854
+ }
1855
+ },
1856
+ async snapshotCreated(snapshotPath) {
1857
+ if (report) {
1858
+ report.snapshotPaths = [.../* @__PURE__ */ new Set([...report.snapshotPaths, snapshotPath])];
2263
1859
  await save();
2264
1860
  }
2265
1861
  },
2266
1862
  async databaseOpened(input) {
2267
1863
  if (report) {
2268
- report.migrationState = input.migrationState;
2269
- report.snapshotPaths = input.snapshotPaths;
1864
+ report.migrationState = previousState === "advanced" || input.migrationState === "advanced" ? "advanced" : previousState === "unknown" ? "unknown" : "unchanged";
1865
+ report.snapshotPaths = [.../* @__PURE__ */ new Set([...report.snapshotPaths, ...input.snapshotPaths])];
2270
1866
  await save();
2271
1867
  }
2272
1868
  },
@@ -2378,6 +1974,15 @@ const packedReleaseSchema = z.tuple([z.looseObject({
2378
1974
  filename: z.string().min(1),
2379
1975
  integrity: z.string().min(1)
2380
1976
  })]);
1977
+ function formatLocalUpdateError(message, details = {}) {
1978
+ return [...new Set([
1979
+ message,
1980
+ details.cause,
1981
+ details.recovery,
1982
+ details.logPath ? `Daemon log: ${details.logPath}` : null,
1983
+ ...(details.snapshotPaths ?? []).map((snapshot) => `Pre-migration snapshot: ${snapshot}`)
1984
+ ].filter(Boolean))].join("\n");
1985
+ }
2381
1986
  var LocalUpdateError = class extends Error {
2382
1987
  code;
2383
1988
  details;
@@ -2498,10 +2103,24 @@ async function replaceSymlink(linkPath, target) {
2498
2103
  await fs.rename(temporaryPath, linkPath);
2499
2104
  }
2500
2105
  async function terminalIds(apiUrl) {
2501
- const result = await fetch(`${apiUrl}/api/projects`).then(async (response) => response.ok ? response.json() : null).catch(() => null);
2502
- const parsed = z.looseObject({ projects: z.array(z.looseObject({ worktrees: z.array(z.looseObject({ terminals: z.array(z.looseObject({ id: z.string() })) })) })) }).safeParse(result);
2503
- if (!parsed.success) throw new Error("Treeport could not read the terminal inventory.");
2504
- return parsed.data.projects.flatMap((project) => project.worktrees).flatMap((worktree) => worktree.terminals).map((terminal) => terminal.id).sort();
2106
+ const parsed = decodeUnknownOrNull(projectsResponseSchema, await fetch(`${apiUrl}/api/projects`).then(async (response) => response.ok ? response.json() : null).catch(() => null));
2107
+ if (!parsed) throw new Error("Treeport could not read the terminal inventory.");
2108
+ return parsed.projects.flatMap((project) => project.worktrees).flatMap((worktree) => worktree.terminals).map((terminal) => terminal.id).sort();
2109
+ }
2110
+ function updateMigrationState(operation, report) {
2111
+ if (operation.migrationState === "advanced") return "advanced";
2112
+ if (report?.operationId === operation.operationId && report.targetVersion === operation.toVersion) return report.migrationState;
2113
+ return ["stop", "activate"].includes(operation.phase) && operation.migrationState === "not_started" ? "not_started" : "unknown";
2114
+ }
2115
+ async function stopUpdateDaemon(lifecycle) {
2116
+ if (lifecycle === "service") {
2117
+ if ((await serviceStop()).administratorCommand) throw new LocalUpdateError("UPDATE_SERVICE_ADMINISTRATOR_ACTION_REQUIRED", "The service requires administrator action and was not stopped.", { phase: "stop" });
2118
+ const deadline = Date.now() + 7e3;
2119
+ while ((await daemonStatus()).state) {
2120
+ if (Date.now() >= deadline) throw new Error("Treeport could not verify that the service daemon stopped. Inspect the daemon log before changing the installed version.");
2121
+ await new Promise((resolve) => setTimeout(resolve, 100));
2122
+ }
2123
+ } else await daemonDown();
2505
2124
  }
2506
2125
  async function startThroughStableEntrypoint(entrypoint, environment) {
2507
2126
  const result = await runCommand(entrypoint, ["start", "--json"], environment);
@@ -2673,6 +2292,7 @@ async function runLocalUpdate(options = {}) {
2673
2292
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2674
2293
  };
2675
2294
  let recoveryOperation = null;
2295
+ let recoveryReport = null;
2676
2296
  const save = async (phase) => {
2677
2297
  operation = {
2678
2298
  ...operation,
@@ -2692,12 +2312,17 @@ async function runLocalUpdate(options = {}) {
2692
2312
  activeTarget: installation.managed ? await fs.realpath(installation.currentLink).catch(() => installation.prefix) : installation.prefix
2693
2313
  };
2694
2314
  if (staleOperation && staleOperation.daemonWasRunning && DESTRUCTIVE_PHASES.has(staleOperation.phase) && !(await daemonStatus()).running) {
2315
+ await stopUpdateDaemon(staleOperation.daemonLifecycle);
2695
2316
  const staleReport = await readUpdateStartupReport(paths.dataDir);
2696
- if (Boolean(staleReport?.operationId === staleOperation.operationId && ["advanced", "unknown"].includes(staleReport.migrationState))) {
2317
+ staleOperation.migrationState = updateMigrationState(staleOperation, staleReport);
2318
+ if (["advanced", "unknown"].includes(staleOperation.migrationState)) {
2319
+ recoveryReport = staleReport?.operationId === staleOperation.operationId && staleReport.targetVersion === staleOperation.toVersion ? staleReport : null;
2697
2320
  if (staleOperation.previousTarget && operation.activeTarget === staleOperation.previousTarget) throw new LocalUpdateError("UPDATE_RECOVERY_REQUIRED", "The older Treeport version is active after a database migration may have started. Treeport will not start it.", {
2698
2321
  phase: "recovery_required",
2699
2322
  operationId: staleOperation.operationId,
2700
- migrationState: staleReport?.migrationState ?? "unknown",
2323
+ migrationState: staleOperation.migrationState,
2324
+ logPath: recoveryReport?.logPath ?? paths.logPath,
2325
+ snapshotPaths: recoveryReport?.snapshotPaths ?? [],
2701
2326
  recovery: "Install the same or a newer Treeport release and inspect the daemon log."
2702
2327
  });
2703
2328
  recoveryOperation = staleOperation;
@@ -2779,6 +2404,8 @@ async function runLocalUpdate(options = {}) {
2779
2404
  phase: "recovery_required",
2780
2405
  operationId: recoveryOperation.operationId,
2781
2406
  migrationState: recoveryOperation.migrationState,
2407
+ logPath: recoveryReport?.logPath ?? paths.logPath,
2408
+ snapshotPaths: recoveryReport?.snapshotPaths ?? [],
2782
2409
  recovery: "Install the next Treeport release when it is available and run `treeport update` again."
2783
2410
  });
2784
2411
  if (comparison === 0) {
@@ -2920,6 +2547,7 @@ async function runLocalUpdate(options = {}) {
2920
2547
  phase: "verify",
2921
2548
  operationId
2922
2549
  });
2550
+ operation.migrationState = recoveryOperation?.migrationState ?? "not_started";
2923
2551
  operation.daemonWasRunning = daemonBefore.running && daemonBefore.verified || recoveryOperation !== null;
2924
2552
  operation.daemonLifecycle = recoveryOperation ? recoveryOperation.daemonLifecycle : operation.daemonWasRunning ? daemonBefore.health?.daemonLifecycle === "service" ? "service" : "treeport" : installedService ? "service" : "treeport";
2925
2553
  operation.serviceMode = recoveryOperation?.serviceMode ?? serviceBefore?.mode ?? null;
@@ -2930,12 +2558,7 @@ async function runLocalUpdate(options = {}) {
2930
2558
  });
2931
2559
  await save("stop");
2932
2560
  progress("Stopping the Treeport daemon and preserving terminals…");
2933
- if (operation.daemonWasRunning) if (operation.daemonLifecycle === "service") {
2934
- if ((await serviceStop()).administratorCommand) throw new LocalUpdateError("UPDATE_SERVICE_ADMINISTRATOR_ACTION_REQUIRED", "The service requires administrator action and was not stopped.", {
2935
- phase: "stop",
2936
- operationId
2937
- });
2938
- } else await daemonDown();
2561
+ if (operation.daemonWasRunning) await stopUpdateDaemon(operation.daemonLifecycle);
2939
2562
  await save("activate");
2940
2563
  progress(`Activating Treeport ${release.version}…`);
2941
2564
  await fs.rm(targetPath, {
@@ -2966,7 +2589,19 @@ async function runLocalUpdate(options = {}) {
2966
2589
  targetVersion: release.version,
2967
2590
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2968
2591
  });
2969
- await fs.rm(path.join(updateDirectory, "startup-report.json"), { force: true });
2592
+ await writeJson(path.join(updateDirectory, "startup-report.json"), {
2593
+ schemaVersion: 1,
2594
+ operationId,
2595
+ targetVersion: release.version,
2596
+ instanceId: null,
2597
+ migrationState: operation.migrationState,
2598
+ ready: false,
2599
+ error: null,
2600
+ logPath: paths.logPath,
2601
+ snapshotPaths: recoveryReport?.snapshotPaths ?? [],
2602
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2603
+ });
2604
+ operation.migrationState = operation.migrationState === "advanced" ? "advanced" : "unknown";
2970
2605
  await save("restart");
2971
2606
  progress(`Restarting the ${operation.daemonLifecycle === "service" ? "Treeport service" : "Treeport daemon"}…`);
2972
2607
  await startThroughStableEntrypoint(installation.entrypoint, environment);
@@ -2979,7 +2614,7 @@ async function runLocalUpdate(options = {}) {
2979
2614
  daemonAfter = await daemonStatus();
2980
2615
  report = await readUpdateStartupReport(paths.dataDir);
2981
2616
  }
2982
- operation.migrationState = report?.operationId === operationId ? report.migrationState : "unknown";
2617
+ operation.migrationState = updateMigrationState(operation, report);
2983
2618
  if (!daemonAfter.running || !daemonAfter.verified || daemonAfter.health?.version !== release.version || daemonAfter.health.daemonLifecycle !== operation.daemonLifecycle || path.resolve(daemonAfter.state.dataDir) !== paths.dataDir || report?.operationId !== operationId || !report.ready) throw new LocalUpdateError("UPDATE_HEALTH_VERIFICATION_FAILED", `Treeport ${release.version} did not pass startup verification.`, {
2984
2619
  phase: "health_check",
2985
2620
  operationId
@@ -3042,13 +2677,14 @@ async function runLocalUpdate(options = {}) {
3042
2677
  toVersion: operation.toVersion
3043
2678
  });
3044
2679
  }
3045
- const startupReport = await readUpdateStartupReport(paths.dataDir);
3046
- if (startupReport?.operationId === operationId) operation.migrationState = startupReport.migrationState;
3047
- if (!["not_started", "unchanged"].includes(operation.migrationState)) {
3048
- const serviceStopError = operation.daemonLifecycle === "service" ? await serviceStop().then(() => null, (cause) => cause instanceof Error ? cause.message : String(cause)) : null;
3049
- operation.recoveryAction = serviceStopError ? `Keep the new version installed. Stop the service, then inspect the daemon log. Service stop failed: ${serviceStopError}` : "Keep the new version installed. Inspect the daemon log and repair with the same or a newer Treeport release.";
2680
+ const stopError = operation.daemonWasRunning ? await stopUpdateDaemon(operation.daemonLifecycle).then(() => null, (cause) => cause instanceof Error ? cause.message : String(cause)) : null;
2681
+ const observedReport = await readUpdateStartupReport(paths.dataDir);
2682
+ const startupReport = observedReport?.operationId === operationId && observedReport.targetVersion === operation.toVersion ? observedReport : null;
2683
+ operation.migrationState = updateMigrationState(operation, startupReport);
2684
+ if (!(!stopError && ["not_started", "unchanged"].includes(operation.migrationState))) {
2685
+ operation.recoveryAction = stopError ? `Keep the new version installed. Stop the daemon, then inspect the daemon log. Stop failed: ${stopError}` : "Keep the new version installed. Inspect the daemon log and repair with the same or a newer Treeport release.";
3050
2686
  await save("recovery_required");
3051
- throw new LocalUpdateError("UPDATE_RECOVERY_REQUIRED", "The updated daemon did not become healthy after database migration began. Treeport did not start the older daemon.", {
2687
+ throw new LocalUpdateError("UPDATE_RECOVERY_REQUIRED", "Treeport could not prove that rollback is safe. Treeport did not start the older daemon.", {
3052
2688
  operationId,
3053
2689
  phase: failedPhase,
3054
2690
  fromVersion: operation.fromVersion,
@@ -3059,6 +2695,7 @@ async function runLocalUpdate(options = {}) {
3059
2695
  safe: false,
3060
2696
  succeeded: false
3061
2697
  },
2698
+ cause: startupReport?.error ?? (error instanceof Error ? error.message : String(error)),
3062
2699
  logPath: startupReport?.logPath ?? paths.logPath,
3063
2700
  snapshotPaths: startupReport?.snapshotPaths ?? [],
3064
2701
  recovery: operation.recoveryAction
@@ -3087,6 +2724,8 @@ async function runLocalUpdate(options = {}) {
3087
2724
  succeeded: rollbackError === null
3088
2725
  },
3089
2726
  cause: error instanceof Error ? error.message : String(error),
2727
+ logPath: startupReport?.logPath ?? paths.logPath,
2728
+ snapshotPaths: startupReport?.snapshotPaths ?? [],
3090
2729
  recovery: operation.recoveryAction
3091
2730
  });
3092
2731
  } finally {
@@ -3104,4 +2743,4 @@ async function runLocalUpdate(options = {}) {
3104
2743
  }
3105
2744
  }
3106
2745
  //#endregion
3107
- export { repositoryTerminalPresetSchema as $, treeportVersion as A, terminalSizeSchema as At, createWorktreeSchema as B, disableTailscaleRemote as C, parseTerminalProgress as Ct, resolvePackagePath as D, terminalLegacyTakeControlSchema as Dt, resolveLocalApiUrl as E, terminalInputSchema as Et, WEB_PANEL_INPUT_MAX_BYTES as F, openWebPanelSchema as G, deleteWebPanelStorageSchema as H, browseDirectoryQuerySchema as I, packageReloadSchema as J, packageInstallSchema as K, createTerminalPresetSchema as L, parseDurationMs as M, TERMINAL_CAPTURE_MAX_LINES as N, runDoctor as O, terminalOutputAckSchema as Ot, TERMINAL_MAX_UPLOAD_BYTES as P, removeWorktreeSchema as Q, createTerminalSchema as R, daemonUp as S, parseTerminalAuth as St, readDaemonLogs as T, terminalBinarySchema as Tt, formatCommandLine as U, deleteTerminalPresetSchema as V, getWebPanelStorageSchema as W, packageUpdateSchema as X, packageRemoveSchema as Y, registerProjectSchema as Z, serviceStatus as _, TERMINAL_SCROLL_EXIT_SEQUENCE as _t, readLocalUpdateProgress as a, updateTerminalPresetSchema as at, daemonHealth as b, TERMINAL_SELECTION_START_SEQUENCE as bt, createUpdateStartupReporter as c, parseEventsSnapshot as ct, serviceDisable as d, TERMINAL_CONTROLLER_GRACE_MS as dt, repositoryTerminalPresetsFileSchema as et, serviceDoctorCheck as f, TERMINAL_MAX_CLIENT_MESSAGE_BYTES as ft, serviceStart as g, TERMINAL_OUTPUT_STALL_TIMEOUT_MS as gt, serviceRun as h, TERMINAL_OUTPUT_LOW_WATERMARK as ht, isCanonicalTreeportVersion as i, updateProjectSchema as it, assertLoopbackHost as j, terminalTakeControlSchema as jt, tailscaleRemoteStatus as k, terminalResizeSchema as kt, readServiceLogs as l, parseProductEvent as lt, serviceInstalled as m, TERMINAL_OUTPUT_HIGH_WATERMARK as mt, compareTreeportVersions as n, setWebPanelStorageSchema as nt, resolveLatestTreeportRelease as o, updateTerminalSchema as ot, serviceEnable as p, TERMINAL_MAX_INPUT_BYTES as pt, packageProjectQuerySchema as q, inspectLocalUpdateInstallation as r, terminalCaptureQuerySchema as rt, runLocalUpdate as s, webPanelInputSchema as st, LocalUpdateError as t, requestWorkspaceOpenSchema as tt, serviceApply as u, SOCKET_IO_PATH as ut, serviceStop as v, TERMINAL_SELECTION_CLEAR_SEQUENCE as vt, enableTailscaleRemote as w, terminalBellAcknowledgementSchema as wt, daemonStatus as x, TERMINAL_SELECTION_STOP_SEQUENCE as xt, daemonDown as y, TERMINAL_SELECTION_RESTORE_SEQUENCE as yt, createWebPanelSchema as z };
2746
+ export { tailscaleRemoteStatus as A, daemonUp as C, resolveLocalApiUrl as D, readDaemonLogs as E, assertLoopbackHost as M, parseDurationMs as N, resolvePackagePath as O, daemonStatus as S, enableTailscaleRemote as T, serviceStart as _, isCanonicalTreeportVersion as a, daemonDown as b, runLocalUpdate as c, serviceApply as d, serviceDisable as f, serviceRun as g, serviceInstalled as h, inspectLocalUpdateInstallation as i, treeportVersion as j, runDoctor as k, createUpdateStartupReporter as l, serviceEnable as m, compareTreeportVersions as n, readLocalUpdateProgress as o, serviceDoctorCheck as p, formatLocalUpdateError as r, resolveLatestTreeportRelease as s, LocalUpdateError as t, readServiceLogs as u, serviceStatus as v, disableTailscaleRemote as w, daemonHealth as x, serviceStop as y };