@treeport/treeport 0.6.1 → 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.
- package/README.md +1 -1
- package/dist/dist-BsLn2Gbc.js +1630 -0
- package/dist/node/cli/index.js +95 -72
- package/dist/node/server/core/launcher.js +11 -49
- package/dist/node/server/index.js +10049 -8120
- package/dist/node/server/terminal-host-entry.js +962 -0
- package/dist/{shell-integration-Be_c91lw.js → shell-integration-CPmrVa3B.js} +46 -46
- package/dist/terminal-host-protocol-DZkQRAUF.js +378 -0
- package/dist/{update-qVp7yL5D.js → update-UYS2lMdD.js} +86 -56
- package/dist/web/assets/index-BWYDUD7N.css +2 -0
- package/dist/web/assets/index-C5cx0N4G.js +84 -0
- package/dist/web/index.html +2 -2
- package/drizzle/0012_terminal_host_cutover.sql +41 -0
- package/drizzle/0013_workspace_item_order.sql +13 -0
- package/drizzle/meta/0012_snapshot.json +919 -0
- package/drizzle/meta/0013_snapshot.json +987 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +18 -13
- package/skills/treeport/SKILL.md +4 -4
- package/dist/dist-Crk_Xr82.js +0 -735
- package/dist/web/assets/index-2LLiNn3-.js +0 -146
- package/dist/web/assets/index-DmDs47YU.css +0 -2
package/dist/node/cli/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { A as
|
|
2
|
+
import { Ct as projectsResponseSchema, Dt as terminalCaptureResponseSchema, G as treeportRpcClientLayer, Gt as webPanelDefinitionsResponseSchema, K as TreeportRpcs, Lt as treeContextResponseSchema, Nt as terminalResponseSchema, Ot as terminalObservationResponseSchema, Pt as terminatedTerminalsResponseSchema, St as projectResponseSchema, Tt as removePreviewResponseSchema, U as decodeUnknownOrNull, V as webPanelInputSchema, _t as packageOperationResponseSchema, bt as packageReloadResponseSchema, dt as openBrowserPanelResponseSchema, et as browserAgentResponseSchema, ft as openWebPanelResponseSchema, gt as packageListingResponseSchema, mt as operationResponseSchema, nt as browserInstallStatusSchema, o as apiErrorBodySchema, t as TERMINAL_CAPTURE_MAX_LINES, tt as browserInstallResponseSchema, ut as okResponseSchema, vt as packageOperationsResponseSchema, yt as packageProjectResponseSchema } from "../../dist-BsLn2Gbc.js";
|
|
3
|
+
import { A as tailscaleRemoteStatus, C as daemonUp, D as resolveLocalApiUrl, E as readDaemonLogs, N as parseDurationMs, O as resolvePackagePath, S as daemonStatus, T as enableTailscaleRemote, _ as serviceStart, b as daemonDown, c as runLocalUpdate, d as serviceApply, f as serviceDisable, g as serviceRun, h as serviceInstalled, j as treeportVersion, k as runDoctor, m as serviceEnable, p as serviceDoctorCheck, r as formatLocalUpdateError, t as LocalUpdateError, u as readServiceLogs, v as serviceStatus, w as disableTailscaleRemote, x as daemonHealth, y as serviceStop } from "../../update-UYS2lMdD.js";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { Command, CommanderError } from "commander";
|
|
7
|
-
import {
|
|
7
|
+
import { RpcClient } from "@effect/rpc";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Fiber from "effect/Fiber";
|
|
10
|
+
import * as Stream from "effect/Stream";
|
|
8
11
|
import { spawn } from "node:child_process";
|
|
9
12
|
//#region src/cli/args.ts
|
|
10
13
|
function extractJsonOutput(args) {
|
|
@@ -146,7 +149,7 @@ async function ensureServiceDaemon() {
|
|
|
146
149
|
pid: state.pid
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
|
-
async function request(pathname, options = {}) {
|
|
152
|
+
async function request(pathname, schema, options = {}) {
|
|
150
153
|
const controller = new AbortController();
|
|
151
154
|
const externalSignal = options.signal;
|
|
152
155
|
const abort = () => controller.abort();
|
|
@@ -164,10 +167,12 @@ async function request(pathname, options = {}) {
|
|
|
164
167
|
});
|
|
165
168
|
const body = await response.json().catch(() => ({}));
|
|
166
169
|
if (!response.ok) {
|
|
167
|
-
const
|
|
168
|
-
throw new CliError(error
|
|
170
|
+
const failure = decodeUnknownOrNull(apiErrorBodySchema, body);
|
|
171
|
+
throw new CliError(failure?.error.message || `HTTP ${response.status}`, 5, failure?.error.code || "API_ERROR", failure?.error.details);
|
|
169
172
|
}
|
|
170
|
-
|
|
173
|
+
const decoded = decodeUnknownOrNull(schema, body);
|
|
174
|
+
if (decoded === null) throw new CliError(`Treeport daemon returned an invalid response for ${pathname}`, 3, "DAEMON_PROTOCOL_ERROR", { pathname });
|
|
175
|
+
return decoded;
|
|
171
176
|
} catch (error) {
|
|
172
177
|
if (error instanceof CliError) throw error;
|
|
173
178
|
throw new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error instanceof Error ? error.message : String(error)}`, 3, "DAEMON_UNREACHABLE");
|
|
@@ -177,19 +182,19 @@ async function request(pathname, options = {}) {
|
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
async function createWorktree(projectId, input) {
|
|
180
|
-
let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, {
|
|
185
|
+
let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, operationResponseSchema, {
|
|
181
186
|
method: "POST",
|
|
182
187
|
body: JSON.stringify(input)
|
|
183
188
|
})).operation;
|
|
184
189
|
while (operation.status === "pending" || operation.status === "running") {
|
|
185
190
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
186
|
-
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}
|
|
191
|
+
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
|
|
187
192
|
}
|
|
188
193
|
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
|
|
189
194
|
if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
|
|
190
195
|
const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
|
|
191
196
|
if (!worktreeId) throw new CliError("Completed tree creation did not identify its tree", 5, "INVALID_OPERATION_RESULT");
|
|
192
|
-
const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}
|
|
197
|
+
const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project.worktrees.find((item) => item.id === worktreeId);
|
|
193
198
|
if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
|
|
194
199
|
const terminalId = operation.result?.terminalId ?? null;
|
|
195
200
|
return {
|
|
@@ -212,7 +217,7 @@ async function canonical(value) {
|
|
|
212
217
|
return fs.realpath(resolved).catch(() => resolved);
|
|
213
218
|
}
|
|
214
219
|
async function projects() {
|
|
215
|
-
return (await request("/api/projects")).projects;
|
|
220
|
+
return (await request("/api/projects", projectsResponseSchema)).projects;
|
|
216
221
|
}
|
|
217
222
|
function pathContains(candidate, parent) {
|
|
218
223
|
const relative = path.relative(parent, candidate);
|
|
@@ -240,7 +245,7 @@ async function packageSource(value) {
|
|
|
240
245
|
return value;
|
|
241
246
|
}
|
|
242
247
|
async function localPackageProjectId() {
|
|
243
|
-
return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}
|
|
248
|
+
return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}`, packageProjectResponseSchema)).project.id;
|
|
244
249
|
}
|
|
245
250
|
async function resolveWorktree(identifier) {
|
|
246
251
|
const all = (await projects()).flatMap((project) => project.worktrees);
|
|
@@ -264,12 +269,12 @@ function parseWebPanelInput(value) {
|
|
|
264
269
|
} catch (error) {
|
|
265
270
|
throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
|
|
266
271
|
}
|
|
267
|
-
const validated = webPanelInputSchema
|
|
268
|
-
if (!validated
|
|
269
|
-
return validated
|
|
272
|
+
const validated = decodeUnknownOrNull(webPanelInputSchema, parsed);
|
|
273
|
+
if (!validated) throw new CliError("--input must contain a JSON object", 2);
|
|
274
|
+
return validated;
|
|
270
275
|
}
|
|
271
276
|
async function webPanelDefinition(worktreeId, identifier) {
|
|
272
|
-
const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions
|
|
277
|
+
const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`, webPanelDefinitionsResponseSchema)).definitions;
|
|
273
278
|
const exact = definitions.find((definition) => definition.id === identifier);
|
|
274
279
|
if (exact) return exact;
|
|
275
280
|
const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
|
|
@@ -304,7 +309,7 @@ async function resolveBrowserPanel(panelId) {
|
|
|
304
309
|
}
|
|
305
310
|
async function runBrowserAgentCommand(command, args, panelId) {
|
|
306
311
|
const { panel } = await resolveBrowserPanel(panelId);
|
|
307
|
-
const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, {
|
|
312
|
+
const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, browserAgentResponseSchema, {
|
|
308
313
|
method: "POST",
|
|
309
314
|
body: JSON.stringify({
|
|
310
315
|
command,
|
|
@@ -346,7 +351,7 @@ function parseDuration(value) {
|
|
|
346
351
|
}
|
|
347
352
|
}
|
|
348
353
|
async function inspectTerminal(terminalId, signal) {
|
|
349
|
-
return request(`/api/terminals/${encodeURIComponent(terminalId)}`, signal ? { signal } : {});
|
|
354
|
+
return request(`/api/terminals/${encodeURIComponent(terminalId)}`, terminalObservationResponseSchema, signal ? { signal } : {});
|
|
350
355
|
}
|
|
351
356
|
async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
352
357
|
let observation = null;
|
|
@@ -378,16 +383,9 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
378
383
|
if (condition === "bell") bellBaseline = observation.metadata.bell?.sequence ?? 0;
|
|
379
384
|
const immediate = matched((/* @__PURE__ */ new Date()).toISOString());
|
|
380
385
|
if (immediate) return immediate;
|
|
381
|
-
events = io(`${apiUrl}/events`, {
|
|
382
|
-
path: SOCKET_IO_PATH,
|
|
383
|
-
transports: ["websocket"],
|
|
384
|
-
forceNew: true,
|
|
385
|
-
autoConnect: false,
|
|
386
|
-
reconnection: false,
|
|
387
|
-
retries: 0
|
|
388
|
-
});
|
|
389
386
|
return await new Promise((resolve, reject) => {
|
|
390
387
|
let settled = false;
|
|
388
|
+
let connected = false;
|
|
391
389
|
let queue = Promise.resolve();
|
|
392
390
|
const finish = (result) => {
|
|
393
391
|
if (!settled) {
|
|
@@ -405,16 +403,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
405
403
|
queue = queue.then(task);
|
|
406
404
|
queue.catch(fail);
|
|
407
405
|
};
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const metadata = snapshot.terminalMetadata.find((item) => item.terminalId === terminalId);
|
|
406
|
+
const snapshot = async (value) => {
|
|
407
|
+
if (!observation) throw new CliError("Treeport daemon sent an invalid event snapshot", 3, "DAEMON_PROTOCOL_ERROR");
|
|
408
|
+
connected = true;
|
|
409
|
+
const metadata = value.terminalMetadata.find((item) => item.terminalId === terminalId);
|
|
413
410
|
if (metadata) observation = {
|
|
414
411
|
...observation,
|
|
415
412
|
metadata
|
|
416
413
|
};
|
|
417
|
-
const snapshotMatch = matched(
|
|
414
|
+
const snapshotMatch = matched(value.at);
|
|
418
415
|
if (snapshotMatch) {
|
|
419
416
|
finish(snapshotMatch);
|
|
420
417
|
return;
|
|
@@ -423,10 +420,8 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
423
420
|
if (cancellation) throw new Error("Terminal wait cancelled");
|
|
424
421
|
const refreshedMatch = matched((/* @__PURE__ */ new Date()).toISOString());
|
|
425
422
|
if (refreshedMatch) finish(refreshedMatch);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const event = parseProductEvent(value);
|
|
429
|
-
if (!event) throw new CliError("Treeport daemon sent an invalid product event", 3, "DAEMON_PROTOCOL_ERROR");
|
|
423
|
+
};
|
|
424
|
+
const productEvent = async (event) => {
|
|
430
425
|
if (event.type !== "terminal.metadata" && event.type !== "terminal.updated" && event.type !== "terminal.removed") return;
|
|
431
426
|
if (event.data.terminalId !== terminalId) return;
|
|
432
427
|
if (event.type === "terminal.removed") throw new CliError(`Terminal ${terminalId} was removed while waiting`, 5, "TERMINAL_REMOVED", {
|
|
@@ -446,10 +441,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
446
441
|
}
|
|
447
442
|
const result = matched(event.at);
|
|
448
443
|
if (result) finish(result);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
444
|
+
};
|
|
445
|
+
controller.signal.addEventListener("abort", () => fail(/* @__PURE__ */ new Error("Terminal wait cancelled")), { once: true });
|
|
446
|
+
const program = Effect.scoped(Effect.gen(function* () {
|
|
447
|
+
yield* (yield* RpcClient.make(TreeportRpcs)).WatchProjectEvents({ protocol: 3 }).pipe(Stream.runForEach((item) => Effect.sync(() => {
|
|
448
|
+
if (item._tag === "Snapshot") enqueue(() => snapshot(item.snapshot));
|
|
449
|
+
else enqueue(() => productEvent(item.event));
|
|
450
|
+
})));
|
|
451
|
+
})).pipe(Effect.provide(treeportRpcClientLayer(`${apiUrl}/api/rpc`)), Effect.onExit(() => Effect.sync(() => fail(connected ? new CliError("Treeport daemon event channel disconnected before the condition was observed", 3, "DAEMON_DISCONNECTED") : new CliError(`Cannot reach Treeport daemon at ${apiUrl}`, 3, "DAEMON_UNREACHABLE")))));
|
|
452
|
+
events = Effect.runFork(program);
|
|
453
453
|
});
|
|
454
454
|
} catch (error) {
|
|
455
455
|
if (cancellation === "timeout") throw new CliError(`Timed out waiting for terminal ${terminalId} to reach ${condition}`, 4, "WAIT_TIMEOUT", {
|
|
@@ -465,8 +465,7 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
465
465
|
if (timeout) clearTimeout(timeout);
|
|
466
466
|
process.off("SIGINT", interrupt);
|
|
467
467
|
controller.abort();
|
|
468
|
-
events
|
|
469
|
-
events?.disconnect();
|
|
468
|
+
if (events) Effect.runFork(Fiber.interrupt(events));
|
|
470
469
|
}
|
|
471
470
|
}
|
|
472
471
|
function print(value, human) {
|
|
@@ -499,7 +498,7 @@ async function main(args) {
|
|
|
499
498
|
if (!await daemonHealth(apiUrl)) throw new CliError(`Cannot reach the externally managed Treeport daemon at ${apiUrl}. Start it through the process that owns its lifecycle and retry.`, 3, "DAEMON_UNREACHABLE");
|
|
500
499
|
} else if (lifecycle === "service") await ensureServiceDaemon();
|
|
501
500
|
else await daemonUp({});
|
|
502
|
-
const registered = await request("/api/projects", {
|
|
501
|
+
const registered = await request("/api/projects", projectResponseSchema, {
|
|
503
502
|
method: "POST",
|
|
504
503
|
body: JSON.stringify({ path: canonicalFolder })
|
|
505
504
|
});
|
|
@@ -512,7 +511,7 @@ async function main(args) {
|
|
|
512
511
|
target.pathname = `/projects/${encodeURIComponent(registered.project.id)}/worktrees/${encodeURIComponent(targetWorktree.id)}`;
|
|
513
512
|
target.search = "";
|
|
514
513
|
target.hash = "";
|
|
515
|
-
const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, {
|
|
514
|
+
const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, okResponseSchema, {
|
|
516
515
|
method: "POST",
|
|
517
516
|
body: JSON.stringify({ sourceTerminalId: contextTerminalId })
|
|
518
517
|
}).then(() => ({ client: "current" })) : await openWorkspace(target.href).catch((error) => {
|
|
@@ -549,13 +548,13 @@ async function main(args) {
|
|
|
549
548
|
if (options.foreground) return;
|
|
550
549
|
print(result, () => `Treeport is running\n${result.apiUrl}`);
|
|
551
550
|
});
|
|
552
|
-
const stopCommand = program.command("stop").description("Stop the local daemon and preserve terminal sessions").option("--terminate-terminals", "terminate every Treeport-owned
|
|
551
|
+
const stopCommand = program.command("stop").description("Stop the local daemon and preserve terminal sessions").option("--terminate-terminals", "terminate every Treeport-owned terminal process").option("--force", "confirm termination of all terminals").option("--json", "emit machine-readable JSON");
|
|
553
552
|
stopCommand.action(async () => {
|
|
554
553
|
const lifecycle = await resolveDaemonLifecycle();
|
|
555
554
|
if (lifecycle === "external") throw new CliError("Cannot run `treeport stop` because the daemon lifecycle is externally managed. Control the process that started Treeport instead.", 5, "DAEMON_LIFECYCLE_EXTERNAL");
|
|
556
555
|
const options = stopCommand.opts();
|
|
557
556
|
if (options.terminateTerminals && !options.force) throw new CliError("Re-run with --terminate-terminals --force to confirm termination of every terminal.", 2);
|
|
558
|
-
if (options.terminateTerminals) await request("/api/admin/terminate-terminals", { method: "POST" });
|
|
557
|
+
if (options.terminateTerminals) await request("/api/admin/terminate-terminals", terminatedTerminalsResponseSchema, { method: "POST" });
|
|
559
558
|
if (lifecycle === "service") {
|
|
560
559
|
const result = await serviceStop();
|
|
561
560
|
print(result, () => formatServiceStatus(result.status));
|
|
@@ -690,7 +689,7 @@ async function main(args) {
|
|
|
690
689
|
...!terminalId ? [`${contextPrefix}_TERMINAL_ID`] : []
|
|
691
690
|
];
|
|
692
691
|
if (missing.length) throw new CliError(`Incomplete Treeport context; missing ${missing.join(", ")}`, 5, "TREEPORT_CONTEXT_INCOMPLETE", { missing });
|
|
693
|
-
const project = (await request(`/api/projects/${encodeURIComponent(projectId)}
|
|
692
|
+
const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project;
|
|
694
693
|
const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
|
|
695
694
|
if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
|
|
696
695
|
projectId,
|
|
@@ -701,7 +700,7 @@ async function main(args) {
|
|
|
701
700
|
worktreeId,
|
|
702
701
|
terminalId
|
|
703
702
|
});
|
|
704
|
-
const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context
|
|
703
|
+
const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`, treeContextResponseSchema)).context;
|
|
705
704
|
const context = {
|
|
706
705
|
managed: true,
|
|
707
706
|
apiUrl,
|
|
@@ -758,7 +757,7 @@ async function main(args) {
|
|
|
758
757
|
url,
|
|
759
758
|
sourceTerminalId: contextWorktreeId === worktree.id ? contextTerminalId ?? null : null
|
|
760
759
|
};
|
|
761
|
-
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, {
|
|
760
|
+
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, openBrowserPanelResponseSchema, {
|
|
762
761
|
method: "POST",
|
|
763
762
|
body: JSON.stringify(body)
|
|
764
763
|
});
|
|
@@ -769,15 +768,15 @@ async function main(args) {
|
|
|
769
768
|
print(output, () => `Opened ${result.panel.title} (${result.panel.id})\n${output.url}`);
|
|
770
769
|
});
|
|
771
770
|
browserCommand.command("install").description("Install the Chromium build used by Browser").option("--json", "emit machine-readable JSON").action(async () => {
|
|
772
|
-
const result = await request("/api/browser/install", { method: "POST" });
|
|
771
|
+
const result = await request("/api/browser/install", browserInstallResponseSchema, { method: "POST" });
|
|
773
772
|
print(result, () => result.message);
|
|
774
773
|
});
|
|
775
774
|
browserCommand.command("status").description("Show hosted browser installation status").option("--json", "emit machine-readable JSON").action(async () => {
|
|
776
|
-
const result = await request("/api/browser/status");
|
|
775
|
+
const result = await request("/api/browser/status", browserInstallStatusSchema);
|
|
777
776
|
print(result, () => `${result.installed ? "Chromium is installed" : "Chromium is not installed"}\nLaunch ready: ${result.launchReady ? "yes" : "no"}\nPlaywright: ${result.playwrightVersion}\nBrowser: ${result.channel} ${result.browserRevision}\nExecutable: ${result.executablePath}${result.launchError ? `\nLaunch error: ${result.launchError}` : ""}`);
|
|
778
777
|
});
|
|
779
778
|
browserCommand.command("remove").description("Remove Treeport's hosted Chromium build").option("--json", "emit machine-readable JSON").action(async () => {
|
|
780
|
-
await request("/api/browser/install", { method: "DELETE" });
|
|
779
|
+
await request("/api/browser/install", okResponseSchema, { method: "DELETE" });
|
|
781
780
|
print({ removed: true }, () => "Removed Treeport hosted Chromium");
|
|
782
781
|
});
|
|
783
782
|
browserCommand.command("list").description("List open Browser sessions").option("--json", "emit machine-readable JSON").action(async () => {
|
|
@@ -842,7 +841,7 @@ async function main(args) {
|
|
|
842
841
|
const options = installCommand.opts();
|
|
843
842
|
const body = { source: await packageSource(source) };
|
|
844
843
|
if (options.local) body.projectId = await localPackageProjectId();
|
|
845
|
-
const result = (await request("/api/packages/install", {
|
|
844
|
+
const result = (await request("/api/packages/install", packageOperationResponseSchema, {
|
|
846
845
|
method: "POST",
|
|
847
846
|
body: JSON.stringify(body)
|
|
848
847
|
})).result;
|
|
@@ -853,14 +852,14 @@ async function main(args) {
|
|
|
853
852
|
const options = removePackageCommand.opts();
|
|
854
853
|
const body = { source: await packageSource(source) };
|
|
855
854
|
if (options.local) body.projectId = await localPackageProjectId();
|
|
856
|
-
const result = (await request("/api/packages/remove", {
|
|
855
|
+
const result = (await request("/api/packages/remove", packageOperationResponseSchema, {
|
|
857
856
|
method: "POST",
|
|
858
857
|
body: JSON.stringify(body)
|
|
859
858
|
})).result;
|
|
860
859
|
print(result, () => `Removed ${result.source}`);
|
|
861
860
|
});
|
|
862
861
|
program.command("list").description("List configured Treeport packages").option("--json", "emit machine-readable JSON").action(async () => {
|
|
863
|
-
const result = await request("/api/packages");
|
|
862
|
+
const result = await request("/api/packages", packageListingResponseSchema);
|
|
864
863
|
print(result, () => {
|
|
865
864
|
const lines = result.packages.map((pkg) => {
|
|
866
865
|
return `${pkg.scope === "global" ? "global" : `project:${pkg.projectName ?? pkg.projectId}`}\t${pkg.source}\t${pkg.resources.webPanels} web panels, ${pkg.resources.terminalPresets} terminal presets`;
|
|
@@ -878,7 +877,7 @@ async function main(args) {
|
|
|
878
877
|
const selfUpdateOptions = { environment: cliEnvironment };
|
|
879
878
|
if (!jsonOutput) selfUpdateOptions.progress = (message) => writeStderr(`${message}\n`);
|
|
880
879
|
const result = await runLocalUpdate(selfUpdateOptions).catch((error) => {
|
|
881
|
-
if (error instanceof LocalUpdateError) throw new CliError(error.message, error.exitCode, error.code, error.details);
|
|
880
|
+
if (error instanceof LocalUpdateError) throw new CliError(jsonOutput ? error.message : formatLocalUpdateError(error.message, error.details), error.exitCode, error.code, error.details);
|
|
882
881
|
throw error;
|
|
883
882
|
});
|
|
884
883
|
print(result, () => {
|
|
@@ -887,7 +886,7 @@ async function main(args) {
|
|
|
887
886
|
});
|
|
888
887
|
return;
|
|
889
888
|
}
|
|
890
|
-
const results = (await request("/api/packages/update", {
|
|
889
|
+
const results = (await request("/api/packages/update", packageOperationsResponseSchema, {
|
|
891
890
|
method: "POST",
|
|
892
891
|
body: JSON.stringify(source ? { source: await packageSource(source) } : {})
|
|
893
892
|
})).results;
|
|
@@ -896,7 +895,7 @@ async function main(args) {
|
|
|
896
895
|
const reloadCommand = program.command("reload").description("Reload package settings and resources without restarting").option("-l, --local", "reload only the registered project containing the current directory").option("--json", "emit machine-readable JSON");
|
|
897
896
|
reloadCommand.action(async () => {
|
|
898
897
|
const options = reloadCommand.opts();
|
|
899
|
-
const result = await request("/api/packages/reload", {
|
|
898
|
+
const result = await request("/api/packages/reload", packageReloadResponseSchema, {
|
|
900
899
|
method: "POST",
|
|
901
900
|
body: JSON.stringify(options.local ? { projectId: await localPackageProjectId() } : {})
|
|
902
901
|
});
|
|
@@ -909,7 +908,7 @@ async function main(args) {
|
|
|
909
908
|
throw new CliError(projectCommand.helpInformation(), 2);
|
|
910
909
|
});
|
|
911
910
|
projectCommand.command("add").description("Register a folder or Git repository").argument("<path>", "folder path").option("--json", "emit machine-readable JSON").action(async (repository) => {
|
|
912
|
-
const body = await request("/api/projects", {
|
|
911
|
+
const body = await request("/api/projects", projectResponseSchema, {
|
|
913
912
|
method: "POST",
|
|
914
913
|
body: JSON.stringify({ path: await canonical(repository) })
|
|
915
914
|
});
|
|
@@ -942,25 +941,46 @@ async function main(args) {
|
|
|
942
941
|
const result = await createWorktree(project.id, request);
|
|
943
942
|
print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
|
|
944
943
|
});
|
|
945
|
-
const worktreeRemoveCommand = worktreeCommand.command("remove").description("Remove a linked tree").argument("<id-or-path-or-dot>", "Tree to remove").option("--force", "confirm destructive removal warnings").option("--json", "emit machine-readable JSON");
|
|
944
|
+
const worktreeRemoveCommand = worktreeCommand.command("remove").description("Remove a linked tree").argument("<id-or-path-or-dot>", "Tree to remove").option("--force", "confirm destructive removal warnings").option("--skip-cleanup", "remove without configured project cleanup").option("--json", "emit machine-readable JSON");
|
|
946
945
|
worktreeRemoveCommand.action(async (identifier) => {
|
|
947
|
-
const { force: confirmed } = worktreeRemoveCommand.opts();
|
|
946
|
+
const { force: confirmed, skipCleanup: requestedSkipCleanup } = worktreeRemoveCommand.opts();
|
|
948
947
|
const worktree = await resolveWorktree(identifier);
|
|
949
|
-
const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview
|
|
948
|
+
const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`, removePreviewResponseSchema)).preview;
|
|
950
949
|
if (!preview.eligible) throw new CliError(preview.reasons.join("\n"), 5);
|
|
951
950
|
if (preview.warnings.length && !confirmed) throw new CliError(`${preview.warnings.join("\n")}\nRe-run with --force to confirm removal.`, 5);
|
|
952
|
-
|
|
951
|
+
const skipCleanup = Boolean(requestedSkipCleanup) && preview.cleanup.commands.length > 0;
|
|
952
|
+
if (skipCleanup && !confirmed) throw new CliError("Skipping project cleanup can leave project resources behind.\nRe-run with --force --skip-cleanup to confirm removal.", 5);
|
|
953
|
+
let operation = (await request(`/api/worktrees/${worktree.id}/remove`, operationResponseSchema, {
|
|
953
954
|
method: "POST",
|
|
954
955
|
body: JSON.stringify({
|
|
955
956
|
confirmationToken: preview.confirmationToken,
|
|
956
|
-
confirmDestructive: preview.warnings.length > 0
|
|
957
|
+
confirmDestructive: preview.warnings.length > 0 || skipCleanup,
|
|
958
|
+
skipCleanup
|
|
957
959
|
})
|
|
958
960
|
})).operation;
|
|
961
|
+
const displayedCleanupCommands = /* @__PURE__ */ new Set();
|
|
962
|
+
const displayFinishedCleanup = () => {
|
|
963
|
+
if (jsonOutput || operation.kind !== "remove") return;
|
|
964
|
+
operation.request.cleanupCommands.commands.forEach((command, index) => {
|
|
965
|
+
if (displayedCleanupCommands.has(index) || command.status !== "completed" && command.status !== "failed") return;
|
|
966
|
+
displayedCleanupCommands.add(index);
|
|
967
|
+
const lines = [`Cleanup: ${command.name}${command.status === "failed" ? " (failed)" : ""}`];
|
|
968
|
+
if (command.stdout) lines.push(command.stdout.replace(/\n$/u, ""));
|
|
969
|
+
if (command.stderr) lines.push(command.stderr.replace(/\n$/u, ""));
|
|
970
|
+
if (command.outputTruncated) lines.push("Cleanup output was truncated.");
|
|
971
|
+
writeStdout(`${lines.join("\n")}\n`);
|
|
972
|
+
});
|
|
973
|
+
};
|
|
974
|
+
displayFinishedCleanup();
|
|
959
975
|
while (operation.status === "pending" || operation.status === "running") {
|
|
960
976
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
961
|
-
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}
|
|
977
|
+
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
|
|
978
|
+
displayFinishedCleanup();
|
|
979
|
+
}
|
|
980
|
+
if (operation.status === "failed") {
|
|
981
|
+
const cleanup = operation.kind === "remove" ? operation.request.cleanupCommands.commands.find((command) => command.status === "failed") : null;
|
|
982
|
+
throw new CliError(operation.error ?? "Tree removal failed; Git kept the tree.", 5, "WORKTREE_REMOVAL_FAILED", cleanup ?? void 0);
|
|
962
983
|
}
|
|
963
|
-
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
|
|
964
984
|
if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
|
|
965
985
|
print(operation.result, () => {
|
|
966
986
|
const warning = operation.result?.cleanup.warning;
|
|
@@ -976,7 +996,7 @@ async function main(args) {
|
|
|
976
996
|
const options = webPanelOpenCommand.opts();
|
|
977
997
|
const worktree = await resolveWorktree(options.worktree);
|
|
978
998
|
const definition = await webPanelDefinition(worktree.id, identifier);
|
|
979
|
-
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, {
|
|
999
|
+
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, openWebPanelResponseSchema, {
|
|
980
1000
|
method: "POST",
|
|
981
1001
|
body: JSON.stringify({
|
|
982
1002
|
definitionId: definition.id,
|
|
@@ -1008,7 +1028,7 @@ async function main(args) {
|
|
|
1008
1028
|
const worktree = await resolveWorktree(options.worktree);
|
|
1009
1029
|
const body = { name: options.name };
|
|
1010
1030
|
if (argv) body.argv = argv;
|
|
1011
|
-
const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
|
|
1031
|
+
const result = await request(`/api/worktrees/${worktree.id}/terminals`, terminalResponseSchema, {
|
|
1012
1032
|
method: "POST",
|
|
1013
1033
|
body: JSON.stringify(body)
|
|
1014
1034
|
});
|
|
@@ -1028,7 +1048,7 @@ async function main(args) {
|
|
|
1028
1048
|
const { lines: rawLines } = terminalCaptureCommand.opts();
|
|
1029
1049
|
const lines = rawLines === void 0 ? 200 : parseCaptureLines(rawLines);
|
|
1030
1050
|
const terminalId = resolveTerminalId(identifier);
|
|
1031
|
-
const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}
|
|
1051
|
+
const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`, terminalCaptureResponseSchema);
|
|
1032
1052
|
if (jsonOutput) print(capture);
|
|
1033
1053
|
else {
|
|
1034
1054
|
writeStdout(capture.content);
|
|
@@ -1048,7 +1068,7 @@ async function main(args) {
|
|
|
1048
1068
|
print(result, () => `${result.terminal.name} (${result.terminal.id}) reached ${result.condition} at ${result.observedAt}`);
|
|
1049
1069
|
});
|
|
1050
1070
|
terminalCommand.command("delete").description("Delete a terminal").argument("<terminal-id>", "terminal to delete").option("--json", "emit machine-readable JSON").action(async (terminalId) => {
|
|
1051
|
-
await request(`/api/terminals/${terminalId}`, { method: "DELETE" });
|
|
1071
|
+
await request(`/api/terminals/${terminalId}`, okResponseSchema, { method: "DELETE" });
|
|
1052
1072
|
print({
|
|
1053
1073
|
ok: true,
|
|
1054
1074
|
terminalId
|
|
@@ -1099,11 +1119,14 @@ async function runCliApplication(options) {
|
|
|
1099
1119
|
} catch (error) {
|
|
1100
1120
|
const cliError = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
|
|
1101
1121
|
if (jsonOutput) {
|
|
1102
|
-
const body = { error: {
|
|
1122
|
+
const body = { error: cliError.details === void 0 ? {
|
|
1103
1123
|
code: cliError.code,
|
|
1104
1124
|
message: cliError.message
|
|
1125
|
+
} : {
|
|
1126
|
+
code: cliError.code,
|
|
1127
|
+
message: cliError.message,
|
|
1128
|
+
details: cliError.details
|
|
1105
1129
|
} };
|
|
1106
|
-
if (cliError.details !== void 0) body.error.details = cliError.details;
|
|
1107
1130
|
writeStderr(`${JSON.stringify(body)}\n`);
|
|
1108
1131
|
} else writeStderr(`${cliError.message}\n`);
|
|
1109
1132
|
requestedExitCode = cliError.exitCode;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "../../../dist-
|
|
3
|
-
import { t as integrateShellLaunch } from "../../../shell-integration-
|
|
2
|
+
import "../../../dist-BsLn2Gbc.js";
|
|
3
|
+
import { t as integrateShellLaunch } from "../../../shell-integration-CPmrVa3B.js";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { z } from "zod";
|
|
7
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { z } from "zod";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
//#region src/server/core/launcher.ts
|
|
10
10
|
const FORWARDED_SIGNALS = [
|
|
@@ -19,7 +19,6 @@ const launchSpecSchema = z.object({
|
|
|
19
19
|
cwd: z.string(),
|
|
20
20
|
env: z.record(z.string(), z.string()),
|
|
21
21
|
shellIntegrationDir: z.string().optional(),
|
|
22
|
-
tmuxExecutable: z.string().optional(),
|
|
23
22
|
setupTasks: z.array(z.object({
|
|
24
23
|
label: z.string(),
|
|
25
24
|
argv: z.array(z.string()),
|
|
@@ -114,7 +113,6 @@ async function runLaunchSpec(spec, dependencies = {}) {
|
|
|
114
113
|
const stdout = dependencies.stdout ?? process.stdout;
|
|
115
114
|
const stderr = dependencies.stderr ?? process.stderr;
|
|
116
115
|
const signalSource = dependencies.signalSource ?? process;
|
|
117
|
-
const tmuxPane = dependencies.tmuxPane === void 0 ? process.env.TMUX_PANE : dependencies.tmuxPane ?? void 0;
|
|
118
116
|
if (spec.setupError) {
|
|
119
117
|
stderr.write(`[Treeport setup] ${safeDiagnostic(spec.setupError) || "setup preparation failed"}\n`);
|
|
120
118
|
return 1;
|
|
@@ -160,25 +158,9 @@ async function runLaunchSpec(spec, dependencies = {}) {
|
|
|
160
158
|
...process.env,
|
|
161
159
|
...spec.env
|
|
162
160
|
};
|
|
163
|
-
const command = integrateShellLaunch(spec.argv, commandEnvironment, spec.shellIntegrationDir, spec.
|
|
161
|
+
const command = integrateShellLaunch(spec.argv, commandEnvironment, spec.shellIntegrationDir, Boolean(spec.shellIntegrationDir));
|
|
164
162
|
const initialTitle = spec.initialTitle ? safeDiagnostic(spec.initialTitle) : "";
|
|
165
|
-
if (initialTitle && spec.
|
|
166
|
-
if ((await runChild([
|
|
167
|
-
spec.tmuxExecutable,
|
|
168
|
-
"set-option",
|
|
169
|
-
"-p",
|
|
170
|
-
"-t",
|
|
171
|
-
tmuxPane,
|
|
172
|
-
"--",
|
|
173
|
-
"@treeport-command",
|
|
174
|
-
initialTitle
|
|
175
|
-
], {
|
|
176
|
-
cwd: spec.cwd,
|
|
177
|
-
env: process.env,
|
|
178
|
-
spawnProcess,
|
|
179
|
-
signalSource
|
|
180
|
-
})).forwardedSignal) return 1;
|
|
181
|
-
}
|
|
163
|
+
if (initialTitle && spec.shellIntegrationDir) stdout.write(`\u001b]777;command;${initialTitle}\u001b\\`);
|
|
182
164
|
const result = await runChild(command.argv, {
|
|
183
165
|
cwd: spec.cwd,
|
|
184
166
|
env: command.env,
|
|
@@ -188,31 +170,8 @@ async function runLaunchSpec(spec, dependencies = {}) {
|
|
|
188
170
|
if (result.spawnError) stderr.write(`Treeport launcher: ${safeDiagnostic(result.spawnError.message) || "spawn error"}\n`);
|
|
189
171
|
if (result.forwardedSignal && (!spec.fallbackArgv || result.forwardedSignal !== "SIGINT")) return 1;
|
|
190
172
|
if (spec.fallbackArgv) {
|
|
191
|
-
if (spec.
|
|
192
|
-
|
|
193
|
-
spec.tmuxExecutable,
|
|
194
|
-
"set-option",
|
|
195
|
-
"-p",
|
|
196
|
-
"-u",
|
|
197
|
-
"-t",
|
|
198
|
-
tmuxPane,
|
|
199
|
-
"@treeport-command",
|
|
200
|
-
";",
|
|
201
|
-
"set-option",
|
|
202
|
-
"-p",
|
|
203
|
-
"-t",
|
|
204
|
-
tmuxPane,
|
|
205
|
-
"--",
|
|
206
|
-
"@treeport-fallback-shell",
|
|
207
|
-
Buffer.from(JSON.stringify(spec.fallbackArgv[0]), "utf8").toString("base64url")
|
|
208
|
-
], {
|
|
209
|
-
cwd: spec.cwd,
|
|
210
|
-
env: process.env,
|
|
211
|
-
spawnProcess,
|
|
212
|
-
signalSource
|
|
213
|
-
})).forwardedSignal) return 1;
|
|
214
|
-
}
|
|
215
|
-
const fallback = integrateShellLaunch(spec.fallbackArgv, commandEnvironment, spec.shellIntegrationDir, spec.tmuxExecutable);
|
|
173
|
+
if (spec.shellIntegrationDir) stdout.write("\x1B]777;command;\x1B\\");
|
|
174
|
+
const fallback = integrateShellLaunch(spec.fallbackArgv, commandEnvironment, spec.shellIntegrationDir, Boolean(spec.shellIntegrationDir));
|
|
216
175
|
const fallbackResult = await runChild(fallback.argv, {
|
|
217
176
|
cwd: spec.cwd,
|
|
218
177
|
env: fallback.env,
|
|
@@ -246,6 +205,9 @@ async function main() {
|
|
|
246
205
|
process.exit(await runLaunchSpec(spec));
|
|
247
206
|
}
|
|
248
207
|
const invokedPath = process.argv[1];
|
|
249
|
-
if (invokedPath && path.resolve(invokedPath) === path.resolve(fileURLToPath(import.meta.url))) main()
|
|
208
|
+
if (invokedPath && path.resolve(invokedPath) === path.resolve(fileURLToPath(import.meta.url))) main().catch((error) => {
|
|
209
|
+
process.stderr.write(`Treeport launcher: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
210
|
+
process.exit(127);
|
|
211
|
+
});
|
|
250
212
|
//#endregion
|
|
251
213
|
export { runLaunchSpec };
|