@treeport/treeport 0.6.1 → 0.8.3
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 +172 -125
- package/dist/node/server/core/launcher.js +11 -49
- package/dist/node/server/index.js +9125 -7162
- package/dist/node/server/terminal-host-entry.js +979 -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-BYHlwpAq.js} +568 -147
- package/dist/web/assets/index-BWYDUD7N.css +2 -0
- package/dist/web/assets/index-BtAAdn1A.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 +19 -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 runDoctor, C as daemonStatus, D as readDaemonLogs, E as enableTailscaleRemote, F as formatServiceStatus, I as humanOutput, L as stateName, M as treeportVersion, N as formatLocalUpdateError, O as resolveLocalApiUrl, P as formatLocalUpdateResult, S as daemonHealth, T as disableTailscaleRemote, _ as serviceRun, b as serviceStop, d as readServiceLogs, f as serviceApply, g as serviceInstalled, h as serviceEnable, j as tailscaleRemoteStatus, k as resolvePackagePath, l as runLocalUpdate, m as serviceDoctorCheck, p as serviceDisable, r as confirmLocalUpdate, t as LocalUpdateError, v as serviceStart, w as daemonUp, x as daemonDown, y as serviceStatus, z as parseDurationMs } from "../../update-BYHlwpAq.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) {
|
|
@@ -96,6 +99,9 @@ let writeStderr = (value) => {
|
|
|
96
99
|
};
|
|
97
100
|
let requestedExitCode = 0;
|
|
98
101
|
let cliEnvironment = process.env;
|
|
102
|
+
let output = humanOutput();
|
|
103
|
+
let errorOutput = humanOutput();
|
|
104
|
+
let updateProgressShown = false;
|
|
99
105
|
var CliError = class extends Error {
|
|
100
106
|
exitCode;
|
|
101
107
|
code;
|
|
@@ -116,22 +122,6 @@ async function resolveDaemonLifecycle() {
|
|
|
116
122
|
}
|
|
117
123
|
return await serviceInstalled() ? "service" : "treeport";
|
|
118
124
|
}
|
|
119
|
-
function formatServiceStatus(status) {
|
|
120
|
-
const mode = status.mode === "headless" ? "advanced headless (starts before login)" : status.mode === "user" && status.manager === "launchd" ? "user/login (starts after login)" : status.mode === "user" ? "user service" : "not installed";
|
|
121
|
-
const lines = [
|
|
122
|
-
`Treeport service: ${status.state}`,
|
|
123
|
-
`Mode: ${mode}`,
|
|
124
|
-
`Manager: ${status.manager ?? "unsupported"}`,
|
|
125
|
-
`Starts before login: ${status.enabledAtBoot ? "yes" : "no"}`,
|
|
126
|
-
`Active: ${status.active ? "yes" : "no"}`,
|
|
127
|
-
`Definition: ${status.definitionPath ?? "not installed"}`
|
|
128
|
-
];
|
|
129
|
-
if (status.daemon?.state) lines.push(`PID: ${status.daemon.state.pid}`);
|
|
130
|
-
if (status.issues.length) lines.push(...status.issues.map((issue) => `Issue: ${issue}`));
|
|
131
|
-
if (status.administratorCommand) lines.push("Administrator action required:", status.administratorCommand, "Then run: treeport service status");
|
|
132
|
-
else if (status.recoveryCommands.length) lines.push(`Next: ${status.recoveryCommands[0]}`);
|
|
133
|
-
return lines.join("\n");
|
|
134
|
-
}
|
|
135
125
|
async function ensureServiceDaemon() {
|
|
136
126
|
const result = await serviceStart();
|
|
137
127
|
const state = result.status.daemon?.state;
|
|
@@ -146,7 +136,7 @@ async function ensureServiceDaemon() {
|
|
|
146
136
|
pid: state.pid
|
|
147
137
|
};
|
|
148
138
|
}
|
|
149
|
-
async function request(pathname, options = {}) {
|
|
139
|
+
async function request(pathname, schema, options = {}) {
|
|
150
140
|
const controller = new AbortController();
|
|
151
141
|
const externalSignal = options.signal;
|
|
152
142
|
const abort = () => controller.abort();
|
|
@@ -164,10 +154,12 @@ async function request(pathname, options = {}) {
|
|
|
164
154
|
});
|
|
165
155
|
const body = await response.json().catch(() => ({}));
|
|
166
156
|
if (!response.ok) {
|
|
167
|
-
const
|
|
168
|
-
throw new CliError(error
|
|
157
|
+
const failure = decodeUnknownOrNull(apiErrorBodySchema, body);
|
|
158
|
+
throw new CliError(failure?.error.message || `HTTP ${response.status}`, 5, failure?.error.code || "API_ERROR", failure?.error.details);
|
|
169
159
|
}
|
|
170
|
-
|
|
160
|
+
const decoded = decodeUnknownOrNull(schema, body);
|
|
161
|
+
if (decoded === null) throw new CliError(`Treeport daemon returned an invalid response for ${pathname}`, 3, "DAEMON_PROTOCOL_ERROR", { pathname });
|
|
162
|
+
return decoded;
|
|
171
163
|
} catch (error) {
|
|
172
164
|
if (error instanceof CliError) throw error;
|
|
173
165
|
throw new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error instanceof Error ? error.message : String(error)}`, 3, "DAEMON_UNREACHABLE");
|
|
@@ -177,19 +169,19 @@ async function request(pathname, options = {}) {
|
|
|
177
169
|
}
|
|
178
170
|
}
|
|
179
171
|
async function createWorktree(projectId, input) {
|
|
180
|
-
let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, {
|
|
172
|
+
let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, operationResponseSchema, {
|
|
181
173
|
method: "POST",
|
|
182
174
|
body: JSON.stringify(input)
|
|
183
175
|
})).operation;
|
|
184
176
|
while (operation.status === "pending" || operation.status === "running") {
|
|
185
177
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
186
|
-
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}
|
|
178
|
+
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
|
|
187
179
|
}
|
|
188
180
|
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
|
|
189
181
|
if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
|
|
190
182
|
const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
|
|
191
183
|
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)}
|
|
184
|
+
const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project.worktrees.find((item) => item.id === worktreeId);
|
|
193
185
|
if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
|
|
194
186
|
const terminalId = operation.result?.terminalId ?? null;
|
|
195
187
|
return {
|
|
@@ -212,7 +204,7 @@ async function canonical(value) {
|
|
|
212
204
|
return fs.realpath(resolved).catch(() => resolved);
|
|
213
205
|
}
|
|
214
206
|
async function projects() {
|
|
215
|
-
return (await request("/api/projects")).projects;
|
|
207
|
+
return (await request("/api/projects", projectsResponseSchema)).projects;
|
|
216
208
|
}
|
|
217
209
|
function pathContains(candidate, parent) {
|
|
218
210
|
const relative = path.relative(parent, candidate);
|
|
@@ -240,7 +232,7 @@ async function packageSource(value) {
|
|
|
240
232
|
return value;
|
|
241
233
|
}
|
|
242
234
|
async function localPackageProjectId() {
|
|
243
|
-
return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}
|
|
235
|
+
return (await request(`/api/packages/project?${new URLSearchParams({ path: await canonical(workingDirectory) }).toString()}`, packageProjectResponseSchema)).project.id;
|
|
244
236
|
}
|
|
245
237
|
async function resolveWorktree(identifier) {
|
|
246
238
|
const all = (await projects()).flatMap((project) => project.worktrees);
|
|
@@ -264,12 +256,12 @@ function parseWebPanelInput(value) {
|
|
|
264
256
|
} catch (error) {
|
|
265
257
|
throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
|
|
266
258
|
}
|
|
267
|
-
const validated = webPanelInputSchema
|
|
268
|
-
if (!validated
|
|
269
|
-
return validated
|
|
259
|
+
const validated = decodeUnknownOrNull(webPanelInputSchema, parsed);
|
|
260
|
+
if (!validated) throw new CliError("--input must contain a JSON object", 2);
|
|
261
|
+
return validated;
|
|
270
262
|
}
|
|
271
263
|
async function webPanelDefinition(worktreeId, identifier) {
|
|
272
|
-
const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions
|
|
264
|
+
const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`, webPanelDefinitionsResponseSchema)).definitions;
|
|
273
265
|
const exact = definitions.find((definition) => definition.id === identifier);
|
|
274
266
|
if (exact) return exact;
|
|
275
267
|
const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
|
|
@@ -304,7 +296,7 @@ async function resolveBrowserPanel(panelId) {
|
|
|
304
296
|
}
|
|
305
297
|
async function runBrowserAgentCommand(command, args, panelId) {
|
|
306
298
|
const { panel } = await resolveBrowserPanel(panelId);
|
|
307
|
-
const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, {
|
|
299
|
+
const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, browserAgentResponseSchema, {
|
|
308
300
|
method: "POST",
|
|
309
301
|
body: JSON.stringify({
|
|
310
302
|
command,
|
|
@@ -346,7 +338,7 @@ function parseDuration(value) {
|
|
|
346
338
|
}
|
|
347
339
|
}
|
|
348
340
|
async function inspectTerminal(terminalId, signal) {
|
|
349
|
-
return request(`/api/terminals/${encodeURIComponent(terminalId)}`, signal ? { signal } : {});
|
|
341
|
+
return request(`/api/terminals/${encodeURIComponent(terminalId)}`, terminalObservationResponseSchema, signal ? { signal } : {});
|
|
350
342
|
}
|
|
351
343
|
async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
352
344
|
let observation = null;
|
|
@@ -378,16 +370,9 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
378
370
|
if (condition === "bell") bellBaseline = observation.metadata.bell?.sequence ?? 0;
|
|
379
371
|
const immediate = matched((/* @__PURE__ */ new Date()).toISOString());
|
|
380
372
|
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
373
|
return await new Promise((resolve, reject) => {
|
|
390
374
|
let settled = false;
|
|
375
|
+
let connected = false;
|
|
391
376
|
let queue = Promise.resolve();
|
|
392
377
|
const finish = (result) => {
|
|
393
378
|
if (!settled) {
|
|
@@ -405,16 +390,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
405
390
|
queue = queue.then(task);
|
|
406
391
|
queue.catch(fail);
|
|
407
392
|
};
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const metadata = snapshot.terminalMetadata.find((item) => item.terminalId === terminalId);
|
|
393
|
+
const snapshot = async (value) => {
|
|
394
|
+
if (!observation) throw new CliError("Treeport daemon sent an invalid event snapshot", 3, "DAEMON_PROTOCOL_ERROR");
|
|
395
|
+
connected = true;
|
|
396
|
+
const metadata = value.terminalMetadata.find((item) => item.terminalId === terminalId);
|
|
413
397
|
if (metadata) observation = {
|
|
414
398
|
...observation,
|
|
415
399
|
metadata
|
|
416
400
|
};
|
|
417
|
-
const snapshotMatch = matched(
|
|
401
|
+
const snapshotMatch = matched(value.at);
|
|
418
402
|
if (snapshotMatch) {
|
|
419
403
|
finish(snapshotMatch);
|
|
420
404
|
return;
|
|
@@ -423,10 +407,8 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
423
407
|
if (cancellation) throw new Error("Terminal wait cancelled");
|
|
424
408
|
const refreshedMatch = matched((/* @__PURE__ */ new Date()).toISOString());
|
|
425
409
|
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");
|
|
410
|
+
};
|
|
411
|
+
const productEvent = async (event) => {
|
|
430
412
|
if (event.type !== "terminal.metadata" && event.type !== "terminal.updated" && event.type !== "terminal.removed") return;
|
|
431
413
|
if (event.data.terminalId !== terminalId) return;
|
|
432
414
|
if (event.type === "terminal.removed") throw new CliError(`Terminal ${terminalId} was removed while waiting`, 5, "TERMINAL_REMOVED", {
|
|
@@ -446,10 +428,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
446
428
|
}
|
|
447
429
|
const result = matched(event.at);
|
|
448
430
|
if (result) finish(result);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
431
|
+
};
|
|
432
|
+
controller.signal.addEventListener("abort", () => fail(/* @__PURE__ */ new Error("Terminal wait cancelled")), { once: true });
|
|
433
|
+
const program = Effect.scoped(Effect.gen(function* () {
|
|
434
|
+
yield* (yield* RpcClient.make(TreeportRpcs)).WatchProjectEvents({ protocol: 3 }).pipe(Stream.runForEach((item) => Effect.sync(() => {
|
|
435
|
+
if (item._tag === "Snapshot") enqueue(() => snapshot(item.snapshot));
|
|
436
|
+
else enqueue(() => productEvent(item.event));
|
|
437
|
+
})));
|
|
438
|
+
})).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")))));
|
|
439
|
+
events = Effect.runFork(program);
|
|
453
440
|
});
|
|
454
441
|
} catch (error) {
|
|
455
442
|
if (cancellation === "timeout") throw new CliError(`Timed out waiting for terminal ${terminalId} to reach ${condition}`, 4, "WAIT_TIMEOUT", {
|
|
@@ -465,8 +452,7 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
465
452
|
if (timeout) clearTimeout(timeout);
|
|
466
453
|
process.off("SIGINT", interrupt);
|
|
467
454
|
controller.abort();
|
|
468
|
-
events
|
|
469
|
-
events?.disconnect();
|
|
455
|
+
if (events) Effect.runFork(Fiber.interrupt(events));
|
|
470
456
|
}
|
|
471
457
|
}
|
|
472
458
|
function print(value, human) {
|
|
@@ -475,12 +461,17 @@ function print(value, human) {
|
|
|
475
461
|
async function main(args) {
|
|
476
462
|
const argv = args[0] === "spawn" || args[0] === "terminal" && args[1] === "create" ? commandArgv(args) : void 0;
|
|
477
463
|
let parserError = "";
|
|
478
|
-
const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, trees, and terminals.").argument("[folder]", "folder or folder inside a Git repository to open").option("--json", "emit machine-readable JSON").configureOutput({
|
|
464
|
+
const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, trees, and terminals.").version(output.heading(await treeportVersion()), "-v, --version", "show installed CLI version").argument("[folder]", "folder or folder inside a Git repository to open").option("--json", "emit machine-readable JSON").configureOutput({
|
|
465
|
+
getOutHasColors: () => output.enabled,
|
|
466
|
+
getErrHasColors: () => errorOutput.enabled,
|
|
479
467
|
writeOut: writeStdout,
|
|
480
468
|
writeErr: (value) => {
|
|
481
469
|
parserError += value;
|
|
482
470
|
}
|
|
483
|
-
}).showHelpAfterError().
|
|
471
|
+
}).showHelpAfterError(jsonOutput).configureHelp({
|
|
472
|
+
styleTitle: output.heading,
|
|
473
|
+
styleCommandText: output.heading
|
|
474
|
+
}).exitOverride();
|
|
484
475
|
program.action(async (folder) => {
|
|
485
476
|
if (folder === void 0) {
|
|
486
477
|
writeStdout(program.helpInformation());
|
|
@@ -499,7 +490,7 @@ async function main(args) {
|
|
|
499
490
|
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
491
|
} else if (lifecycle === "service") await ensureServiceDaemon();
|
|
501
492
|
else await daemonUp({});
|
|
502
|
-
const registered = await request("/api/projects", {
|
|
493
|
+
const registered = await request("/api/projects", projectResponseSchema, {
|
|
503
494
|
method: "POST",
|
|
504
495
|
body: JSON.stringify({ path: canonicalFolder })
|
|
505
496
|
});
|
|
@@ -512,7 +503,7 @@ async function main(args) {
|
|
|
512
503
|
target.pathname = `/projects/${encodeURIComponent(registered.project.id)}/worktrees/${encodeURIComponent(targetWorktree.id)}`;
|
|
513
504
|
target.search = "";
|
|
514
505
|
target.hash = "";
|
|
515
|
-
const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, {
|
|
506
|
+
const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, okResponseSchema, {
|
|
516
507
|
method: "POST",
|
|
517
508
|
body: JSON.stringify({ sourceTerminalId: contextTerminalId })
|
|
518
509
|
}).then(() => ({ client: "current" })) : await openWorkspace(target.href).catch((error) => {
|
|
@@ -536,7 +527,7 @@ async function main(args) {
|
|
|
536
527
|
if (lifecycle === "service") {
|
|
537
528
|
if (options.foreground || options.host || options.port) throw new CliError("An installed service owns the listener and process mode. Run `treeport service enable` to refresh its configuration, or `treeport service disable` to return to local background mode.", 5, "DAEMON_LIFECYCLE_SERVICE");
|
|
538
529
|
const result = await serviceStart();
|
|
539
|
-
print(result, () => formatServiceStatus(result.status));
|
|
530
|
+
print(result, () => formatServiceStatus(result.status, output));
|
|
540
531
|
if (result.administratorCommand || !result.status.healthy) requestedExitCode = 1;
|
|
541
532
|
return;
|
|
542
533
|
}
|
|
@@ -544,26 +535,29 @@ async function main(args) {
|
|
|
544
535
|
const daemonOptions = {};
|
|
545
536
|
if (options.host !== void 0) daemonOptions.host = options.host;
|
|
546
537
|
if (port !== void 0) daemonOptions.port = port;
|
|
547
|
-
if (options.foreground !== void 0)
|
|
538
|
+
if (options.foreground !== void 0) {
|
|
539
|
+
daemonOptions.foreground = options.foreground;
|
|
540
|
+
daemonOptions.output = output;
|
|
541
|
+
}
|
|
548
542
|
const result = await daemonUp(daemonOptions);
|
|
549
543
|
if (options.foreground) return;
|
|
550
|
-
print(result, () =>
|
|
544
|
+
print(result, () => output.blocks(output.summary("Treeport is running", "success"), output.rows([["URL", result.apiUrl]])));
|
|
551
545
|
});
|
|
552
|
-
const stopCommand = program.command("stop").description("Stop the local daemon and preserve terminal sessions").option("--terminate-terminals", "terminate every Treeport-owned
|
|
546
|
+
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
547
|
stopCommand.action(async () => {
|
|
554
548
|
const lifecycle = await resolveDaemonLifecycle();
|
|
555
549
|
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
550
|
const options = stopCommand.opts();
|
|
557
551
|
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" });
|
|
552
|
+
if (options.terminateTerminals) await request("/api/admin/terminate-terminals", terminatedTerminalsResponseSchema, { method: "POST" });
|
|
559
553
|
if (lifecycle === "service") {
|
|
560
554
|
const result = await serviceStop();
|
|
561
|
-
print(result, () => formatServiceStatus(result.status));
|
|
555
|
+
print(result, () => formatServiceStatus(result.status, output));
|
|
562
556
|
if (result.administratorCommand) requestedExitCode = 1;
|
|
563
557
|
return;
|
|
564
558
|
}
|
|
565
559
|
const result = await daemonDown();
|
|
566
|
-
print(result, () => result.wasRunning ? "Treeport is stopped" : "Treeport is already stopped");
|
|
560
|
+
print(result, () => output.summary(result.wasRunning ? "Treeport is stopped" : "Treeport is already stopped", "success"));
|
|
567
561
|
});
|
|
568
562
|
const serviceCommand = program.command("service").description("Manage opt-in OS service supervision");
|
|
569
563
|
serviceCommand.action(() => {
|
|
@@ -572,12 +566,12 @@ async function main(args) {
|
|
|
572
566
|
const serviceEnableCommand = serviceCommand.command("enable").description("Enable automatic startup and unexpected-exit restarts").option("--headless", "use advanced macOS startup before login (requires an administrator)").option("--json", "emit machine-readable JSON");
|
|
573
567
|
serviceEnableCommand.action(async () => {
|
|
574
568
|
const result = await serviceEnable(serviceEnableCommand.opts().headless ? "headless" : "user");
|
|
575
|
-
print(result, () => formatServiceStatus(result.status));
|
|
569
|
+
print(result, () => formatServiceStatus(result.status, output));
|
|
576
570
|
if (result.status.state === "action_required") requestedExitCode = 1;
|
|
577
571
|
});
|
|
578
572
|
serviceCommand.command("status").description("Show OS service supervision status").option("--json", "emit machine-readable JSON").action(async () => {
|
|
579
573
|
const result = await serviceStatus();
|
|
580
|
-
print(result, () => formatServiceStatus(result));
|
|
574
|
+
print(result, () => formatServiceStatus(result, output));
|
|
581
575
|
if (![
|
|
582
576
|
"disabled",
|
|
583
577
|
"healthy",
|
|
@@ -586,7 +580,7 @@ async function main(args) {
|
|
|
586
580
|
});
|
|
587
581
|
serviceCommand.command("disable").description("Stop and unregister OS service supervision").option("--json", "emit machine-readable JSON").action(async () => {
|
|
588
582
|
const result = await serviceDisable();
|
|
589
|
-
print(result, () => formatServiceStatus(result.status));
|
|
583
|
+
print(result, () => formatServiceStatus(result.status, output));
|
|
590
584
|
if (result.administratorCommand || result.status.state !== "disabled") requestedExitCode = 1;
|
|
591
585
|
});
|
|
592
586
|
serviceCommand.command("run", { hidden: true }).action(async () => serviceRun());
|
|
@@ -594,7 +588,7 @@ async function main(args) {
|
|
|
594
588
|
serviceApplyCommand.action(async () => {
|
|
595
589
|
const { request: requestPath } = serviceApplyCommand.opts();
|
|
596
590
|
const result = await serviceApply(requestPath);
|
|
597
|
-
print(result, () => `Applied Treeport service ${result.operation} request
|
|
591
|
+
print(result, () => output.summary(`Applied Treeport service ${result.operation} request.`, "success"));
|
|
598
592
|
});
|
|
599
593
|
const remoteCommand = program.command("remote").description("Expose Treeport privately through Tailscale Serve");
|
|
600
594
|
remoteCommand.action(() => {
|
|
@@ -612,37 +606,48 @@ async function main(args) {
|
|
|
612
606
|
if (port !== void 0) remoteOptions.port = port;
|
|
613
607
|
if (serviceDaemon !== void 0) remoteOptions.daemon = serviceDaemon;
|
|
614
608
|
const result = await enableTailscaleRemote(remoteOptions);
|
|
615
|
-
print(result, () => `Treeport remote access is ${result.alreadyEnabled ? "already enabled" : "enabled"}
|
|
609
|
+
print(result, () => output.blocks(output.summary(`Treeport remote access is ${result.alreadyEnabled ? "already enabled" : "enabled"}`, "success"), output.rows([["URL", result.url]]), "Tailscale authenticates each remote user. Access is limited by your Tailscale policy."));
|
|
616
610
|
});
|
|
617
611
|
remoteCommand.command("status").description("Show Tailscale remote access status").option("--json", "emit machine-readable JSON").action(async () => {
|
|
618
612
|
const result = await tailscaleRemoteStatus();
|
|
619
613
|
print(result, () => {
|
|
620
|
-
if (!result.configured) return "Treeport remote access is disabled";
|
|
621
|
-
return result.active ?
|
|
614
|
+
if (!result.configured) return output.summary("Treeport remote access is disabled");
|
|
615
|
+
return output.blocks(output.summary(result.active ? "Treeport remote access is enabled" : "Treeport remote access is unavailable", result.active ? "success" : "warning"), output.rows([[result.active ? "URL" : "Expected URL", result.url ?? "Unavailable"]]), !result.active && "The Tailscale Serve route no longer points to Treeport.");
|
|
622
616
|
});
|
|
623
617
|
});
|
|
624
618
|
remoteCommand.command("disable").description("Disable Treeport Tailscale remote access").option("--json", "emit machine-readable JSON").action(async () => {
|
|
625
619
|
const result = await disableTailscaleRemote();
|
|
626
620
|
print(result, () => {
|
|
627
|
-
if (result.changedTailscale) return "Treeport remote access is disabled";
|
|
628
|
-
return result.wasEnabled ? "Treeport remote access is disabled" : "Treeport remote access was already disabled; the current Tailscale route was left unchanged.";
|
|
621
|
+
if (result.changedTailscale) return output.summary("Treeport remote access is disabled", "success");
|
|
622
|
+
return output.summary(result.wasEnabled ? "Treeport remote access is disabled" : "Treeport remote access was already disabled; the current Tailscale route was left unchanged.", "success");
|
|
629
623
|
});
|
|
630
624
|
});
|
|
631
625
|
program.command("status").description("Show local daemon status").option("--json", "emit machine-readable JSON").action(async () => {
|
|
632
|
-
const status = await daemonStatus();
|
|
626
|
+
const [cliVersion, status] = await Promise.all([treeportVersion(), daemonStatus()]);
|
|
633
627
|
const supervision = await serviceInstalled() ? await serviceStatus() : null;
|
|
628
|
+
const observed = supervision?.daemon ?? status;
|
|
629
|
+
const daemonVersion = observed.running && observed.verified ? observed.health?.version ?? null : null;
|
|
634
630
|
const projectList = status.verified ? await projects() : [];
|
|
635
631
|
const result = {
|
|
636
632
|
...status,
|
|
633
|
+
cliVersion,
|
|
634
|
+
daemonVersion,
|
|
637
635
|
service: supervision,
|
|
638
636
|
projects: projectList.length,
|
|
639
637
|
worktrees: projectList.reduce((count, project) => count + project.worktrees.length, 0),
|
|
640
638
|
terminals: projectList.reduce((count, project) => count + project.worktrees.reduce((worktreeCount, worktree) => worktreeCount + worktree.terminals.length, 0), 0)
|
|
641
639
|
};
|
|
642
640
|
print(result, () => {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
641
|
+
const healthy = observed.running && observed.verified;
|
|
642
|
+
return output.blocks(output.heading("Treeport status"), output.summary(!observed.state ? "Treeport is stopped" : healthy ? "Treeport is running" : "Treeport is unhealthy", !observed.state ? "neutral" : healthy ? "success" : "failure"), output.rows([
|
|
643
|
+
["CLI version", cliVersion],
|
|
644
|
+
["Daemon version", daemonVersion ?? "Unavailable"],
|
|
645
|
+
observed.state ? ["URL", observed.state.apiUrl] : null
|
|
646
|
+
]), daemonVersion !== null && daemonVersion !== cliVersion && output.summary("Version mismatch: CLI and daemon versions differ", "warning"), healthy && output.detail(output.rows([
|
|
647
|
+
supervision ? null : ["Lifecycle", observed.health?.daemonLifecycle === "external" ? "Externally managed" : observed.health?.daemonLifecycle === "service" ? "OS service" : "Treeport"],
|
|
648
|
+
supervision ? null : ["PID", observed.state.pid],
|
|
649
|
+
status.verified ? ["Workspace", `${result.projects} projects · ${result.worktrees} trees · ${result.terminals} terminals`] : null
|
|
650
|
+
])), !healthy && observed.state !== null && output.rows([["PID", observed.state.pid], ["Logs", path.join(observed.state.dataDir, "logs", "daemon.log")]]), supervision !== null && formatServiceStatus(supervision, output));
|
|
646
651
|
});
|
|
647
652
|
});
|
|
648
653
|
const logsCommand = program.command("logs").description("Show recent daemon logs").option("--lines <count>", "number of lines", "100");
|
|
@@ -653,7 +658,7 @@ async function main(args) {
|
|
|
653
658
|
});
|
|
654
659
|
program.command("doctor").description("Diagnose local requirements and paths").option("--json", "emit machine-readable JSON").action(async () => {
|
|
655
660
|
const checks = [...await runDoctor(), await serviceDoctorCheck()];
|
|
656
|
-
print(checks, () => checks.
|
|
661
|
+
print(checks, () => output.blocks(output.heading("Treeport doctor"), output.summary(checks.every((check) => check.ok) ? "All checks passed" : "Some checks failed", checks.every((check) => check.ok) ? "success" : "failure"), ...checks.map((check) => `${output.summary(check.name, check.ok ? "success" : "failure")}\n${output.indent(check.detail.startsWith("state: ") ? stateName(check.detail.slice(7)) : check.ok ? output.detail(check.detail) : check.detail)}`)));
|
|
657
662
|
if (checks.some((check) => !check.ok)) requestedExitCode = 1;
|
|
658
663
|
});
|
|
659
664
|
program.command("version").description("Show CLI and daemon versions").option("--json", "emit machine-readable JSON").action(async () => {
|
|
@@ -662,7 +667,7 @@ async function main(args) {
|
|
|
662
667
|
cli,
|
|
663
668
|
daemon: status.verified ? status.health?.version ?? null : null
|
|
664
669
|
};
|
|
665
|
-
print(result, () =>
|
|
670
|
+
print(result, () => output.blocks(output.heading("Treeport version"), output.rows([["CLI", result.cli], ["Daemon", result.daemon ?? "Unavailable"]]), result.daemon !== null && result.daemon !== result.cli && output.summary("Version mismatch: CLI and daemon versions differ", "warning")));
|
|
666
671
|
});
|
|
667
672
|
program.command("skills").description("Print the Treeport usage guide for AI agents").action(async () => {
|
|
668
673
|
const skill = await fs.readFile(await resolvePackagePath("skills", "treeport", "SKILL.md"), "utf8");
|
|
@@ -680,7 +685,7 @@ async function main(args) {
|
|
|
680
685
|
print({
|
|
681
686
|
managed: false,
|
|
682
687
|
reason: "outside_treeport"
|
|
683
|
-
}, () => "Not running in a Treeport-managed terminal.");
|
|
688
|
+
}, () => output.blocks(output.heading("Treeport context"), "Not running in a Treeport-managed terminal."));
|
|
684
689
|
return;
|
|
685
690
|
}
|
|
686
691
|
const missing = [
|
|
@@ -690,7 +695,7 @@ async function main(args) {
|
|
|
690
695
|
...!terminalId ? [`${contextPrefix}_TERMINAL_ID`] : []
|
|
691
696
|
];
|
|
692
697
|
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)}
|
|
698
|
+
const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project;
|
|
694
699
|
const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
|
|
695
700
|
if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
|
|
696
701
|
projectId,
|
|
@@ -701,7 +706,7 @@ async function main(args) {
|
|
|
701
706
|
worktreeId,
|
|
702
707
|
terminalId
|
|
703
708
|
});
|
|
704
|
-
const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context
|
|
709
|
+
const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`, treeContextResponseSchema)).context;
|
|
705
710
|
const context = {
|
|
706
711
|
managed: true,
|
|
707
712
|
apiUrl,
|
|
@@ -744,7 +749,12 @@ async function main(args) {
|
|
|
744
749
|
}).join("").split("\n");
|
|
745
750
|
return ` ${key}: ${first}${rest.length > 0 ? `\n${rest.map((line) => ` ${line}`).join("\n")}` : ""}`;
|
|
746
751
|
}).join("\n");
|
|
747
|
-
return
|
|
752
|
+
return output.blocks(output.heading("Treeport context"), output.rows([
|
|
753
|
+
["Project", `${context.project.name} (${context.project.id})`],
|
|
754
|
+
["Tree", `${context.worktree.name} (${context.worktree.id})`],
|
|
755
|
+
["Path", context.worktree.path],
|
|
756
|
+
["Terminal", `${context.terminal.name} (${context.terminal.id}) — ${stateName(context.terminal.status)}`]
|
|
757
|
+
]), entries.length > 0 && `${output.heading("Tree context")}\n${treeContextText}`, output.detail(output.rows([["API", context.apiUrl], ["Lifecycle", context.daemonLifecycle === "external" ? "Externally managed" : context.daemonLifecycle === "service" ? "OS service" : "Treeport"]])));
|
|
748
758
|
});
|
|
749
759
|
});
|
|
750
760
|
const browserCommand = program.command("browser").description("Manage Browser and its hosted Chromium");
|
|
@@ -758,7 +768,7 @@ async function main(args) {
|
|
|
758
768
|
url,
|
|
759
769
|
sourceTerminalId: contextWorktreeId === worktree.id ? contextTerminalId ?? null : null
|
|
760
770
|
};
|
|
761
|
-
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, {
|
|
771
|
+
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, openBrowserPanelResponseSchema, {
|
|
762
772
|
method: "POST",
|
|
763
773
|
body: JSON.stringify(body)
|
|
764
774
|
});
|
|
@@ -769,15 +779,15 @@ async function main(args) {
|
|
|
769
779
|
print(output, () => `Opened ${result.panel.title} (${result.panel.id})\n${output.url}`);
|
|
770
780
|
});
|
|
771
781
|
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" });
|
|
782
|
+
const result = await request("/api/browser/install", browserInstallResponseSchema, { method: "POST" });
|
|
773
783
|
print(result, () => result.message);
|
|
774
784
|
});
|
|
775
785
|
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");
|
|
786
|
+
const result = await request("/api/browser/status", browserInstallStatusSchema);
|
|
777
787
|
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
788
|
});
|
|
779
789
|
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" });
|
|
790
|
+
await request("/api/browser/install", okResponseSchema, { method: "DELETE" });
|
|
781
791
|
print({ removed: true }, () => "Removed Treeport hosted Chromium");
|
|
782
792
|
});
|
|
783
793
|
browserCommand.command("list").description("List open Browser sessions").option("--json", "emit machine-readable JSON").action(async () => {
|
|
@@ -842,7 +852,7 @@ async function main(args) {
|
|
|
842
852
|
const options = installCommand.opts();
|
|
843
853
|
const body = { source: await packageSource(source) };
|
|
844
854
|
if (options.local) body.projectId = await localPackageProjectId();
|
|
845
|
-
const result = (await request("/api/packages/install", {
|
|
855
|
+
const result = (await request("/api/packages/install", packageOperationResponseSchema, {
|
|
846
856
|
method: "POST",
|
|
847
857
|
body: JSON.stringify(body)
|
|
848
858
|
})).result;
|
|
@@ -853,14 +863,14 @@ async function main(args) {
|
|
|
853
863
|
const options = removePackageCommand.opts();
|
|
854
864
|
const body = { source: await packageSource(source) };
|
|
855
865
|
if (options.local) body.projectId = await localPackageProjectId();
|
|
856
|
-
const result = (await request("/api/packages/remove", {
|
|
866
|
+
const result = (await request("/api/packages/remove", packageOperationResponseSchema, {
|
|
857
867
|
method: "POST",
|
|
858
868
|
body: JSON.stringify(body)
|
|
859
869
|
})).result;
|
|
860
870
|
print(result, () => `Removed ${result.source}`);
|
|
861
871
|
});
|
|
862
872
|
program.command("list").description("List configured Treeport packages").option("--json", "emit machine-readable JSON").action(async () => {
|
|
863
|
-
const result = await request("/api/packages");
|
|
873
|
+
const result = await request("/api/packages", packageListingResponseSchema);
|
|
864
874
|
print(result, () => {
|
|
865
875
|
const lines = result.packages.map((pkg) => {
|
|
866
876
|
return `${pkg.scope === "global" ? "global" : `project:${pkg.projectName ?? pkg.projectId}`}\t${pkg.source}\t${pkg.resources.webPanels} web panels, ${pkg.resources.terminalPresets} terminal presets`;
|
|
@@ -869,34 +879,41 @@ async function main(args) {
|
|
|
869
879
|
return lines.join("\n");
|
|
870
880
|
});
|
|
871
881
|
});
|
|
872
|
-
const updatePackagesCommand = program.command("update").description("Update Treeport or explicitly update configured packages").argument("[source]", "one configured npm: source").option("--packages", "update every eligible configured package").option("--json", "emit machine-readable JSON");
|
|
882
|
+
const updatePackagesCommand = program.command("update").description("Update Treeport or explicitly update configured packages").argument("[source]", "one configured npm: source").option("--packages", "update every eligible configured package").option("-y, --yes", "approve the Treeport self-update without prompting").option("--start", "start Treeport after a self-update if it was stopped").option("--json", "emit machine-readable JSON");
|
|
873
883
|
updatePackagesCommand.action(async (source) => {
|
|
874
884
|
const options = updatePackagesCommand.opts();
|
|
875
885
|
if (source && options.packages) throw new CliError("Specify a package source or --packages, not both.", 2);
|
|
886
|
+
if ((source || options.packages) && (options.yes || options.start)) throw new CliError("--yes and --start apply only to Treeport self-updates.", 2);
|
|
876
887
|
if (!source && !options.packages) {
|
|
877
888
|
if (await resolveDaemonLifecycle() === "external") throw new CliError("Cannot update Treeport because this daemon lifecycle is externally managed.", 5, "UPDATE_EXTERNAL_REFUSED");
|
|
878
|
-
const selfUpdateOptions = {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
889
|
+
const selfUpdateOptions = {
|
|
890
|
+
environment: cliEnvironment,
|
|
891
|
+
yes: options.yes ?? false,
|
|
892
|
+
start: options.start ?? false
|
|
893
|
+
};
|
|
894
|
+
if (!jsonOutput && process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY) selfUpdateOptions.confirm = (preview, signal) => confirmLocalUpdate(preview, signal, process.stdin, process.stderr, errorOutput);
|
|
895
|
+
if (!jsonOutput) selfUpdateOptions.progress = (message) => {
|
|
896
|
+
if (!updateProgressShown) {
|
|
897
|
+
writeStderr(`${errorOutput.heading("Treeport update")}\n\n`);
|
|
898
|
+
updateProgressShown = true;
|
|
899
|
+
}
|
|
900
|
+
writeStderr(`${errorOutput.indent(errorOutput.summary(message, "warning"))}\n`);
|
|
901
|
+
};
|
|
902
|
+
const result = await runLocalUpdate(selfUpdateOptions);
|
|
903
|
+
if (updateProgressShown) writeStderr("\n");
|
|
904
|
+
print(result, () => formatLocalUpdateResult(result, output));
|
|
888
905
|
return;
|
|
889
906
|
}
|
|
890
|
-
const results = (await request("/api/packages/update", {
|
|
907
|
+
const results = (await request("/api/packages/update", packageOperationsResponseSchema, {
|
|
891
908
|
method: "POST",
|
|
892
909
|
body: JSON.stringify(source ? { source: await packageSource(source) } : {})
|
|
893
910
|
})).results;
|
|
894
|
-
print(results, () => results.map((result) => `${result.status}
|
|
911
|
+
print(results, () => output.blocks(output.heading("Treeport package update"), ...results.map((result) => output.blocks(output.summary(`${stateName(result.status)} ${result.source ?? "packages"}`, result.status === "skipped" ? "warning" : "success"), output.detail(output.rows([["Scope", stateName(result.scope)]])), result.reason ? output.indent(result.reason) : null))));
|
|
895
912
|
});
|
|
896
913
|
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
914
|
reloadCommand.action(async () => {
|
|
898
915
|
const options = reloadCommand.opts();
|
|
899
|
-
const result = await request("/api/packages/reload", {
|
|
916
|
+
const result = await request("/api/packages/reload", packageReloadResponseSchema, {
|
|
900
917
|
method: "POST",
|
|
901
918
|
body: JSON.stringify(options.local ? { projectId: await localPackageProjectId() } : {})
|
|
902
919
|
});
|
|
@@ -909,7 +926,7 @@ async function main(args) {
|
|
|
909
926
|
throw new CliError(projectCommand.helpInformation(), 2);
|
|
910
927
|
});
|
|
911
928
|
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", {
|
|
929
|
+
const body = await request("/api/projects", projectResponseSchema, {
|
|
913
930
|
method: "POST",
|
|
914
931
|
body: JSON.stringify({ path: await canonical(repository) })
|
|
915
932
|
});
|
|
@@ -942,25 +959,46 @@ async function main(args) {
|
|
|
942
959
|
const result = await createWorktree(project.id, request);
|
|
943
960
|
print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
|
|
944
961
|
});
|
|
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");
|
|
962
|
+
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
963
|
worktreeRemoveCommand.action(async (identifier) => {
|
|
947
|
-
const { force: confirmed } = worktreeRemoveCommand.opts();
|
|
964
|
+
const { force: confirmed, skipCleanup: requestedSkipCleanup } = worktreeRemoveCommand.opts();
|
|
948
965
|
const worktree = await resolveWorktree(identifier);
|
|
949
|
-
const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview
|
|
966
|
+
const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`, removePreviewResponseSchema)).preview;
|
|
950
967
|
if (!preview.eligible) throw new CliError(preview.reasons.join("\n"), 5);
|
|
951
968
|
if (preview.warnings.length && !confirmed) throw new CliError(`${preview.warnings.join("\n")}\nRe-run with --force to confirm removal.`, 5);
|
|
952
|
-
|
|
969
|
+
const skipCleanup = Boolean(requestedSkipCleanup) && preview.cleanup.commands.length > 0;
|
|
970
|
+
if (skipCleanup && !confirmed) throw new CliError("Skipping project cleanup can leave project resources behind.\nRe-run with --force --skip-cleanup to confirm removal.", 5);
|
|
971
|
+
let operation = (await request(`/api/worktrees/${worktree.id}/remove`, operationResponseSchema, {
|
|
953
972
|
method: "POST",
|
|
954
973
|
body: JSON.stringify({
|
|
955
974
|
confirmationToken: preview.confirmationToken,
|
|
956
|
-
confirmDestructive: preview.warnings.length > 0
|
|
975
|
+
confirmDestructive: preview.warnings.length > 0 || skipCleanup,
|
|
976
|
+
skipCleanup
|
|
957
977
|
})
|
|
958
978
|
})).operation;
|
|
979
|
+
const displayedCleanupCommands = /* @__PURE__ */ new Set();
|
|
980
|
+
const displayFinishedCleanup = () => {
|
|
981
|
+
if (jsonOutput || operation.kind !== "remove") return;
|
|
982
|
+
operation.request.cleanupCommands.commands.forEach((command, index) => {
|
|
983
|
+
if (displayedCleanupCommands.has(index) || command.status !== "completed" && command.status !== "failed") return;
|
|
984
|
+
displayedCleanupCommands.add(index);
|
|
985
|
+
const lines = [`Cleanup: ${command.name}${command.status === "failed" ? " (failed)" : ""}`];
|
|
986
|
+
if (command.stdout) lines.push(command.stdout.replace(/\n$/u, ""));
|
|
987
|
+
if (command.stderr) lines.push(command.stderr.replace(/\n$/u, ""));
|
|
988
|
+
if (command.outputTruncated) lines.push("Cleanup output was truncated.");
|
|
989
|
+
writeStdout(`${lines.join("\n")}\n`);
|
|
990
|
+
});
|
|
991
|
+
};
|
|
992
|
+
displayFinishedCleanup();
|
|
959
993
|
while (operation.status === "pending" || operation.status === "running") {
|
|
960
994
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
961
|
-
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}
|
|
995
|
+
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
|
|
996
|
+
displayFinishedCleanup();
|
|
997
|
+
}
|
|
998
|
+
if (operation.status === "failed") {
|
|
999
|
+
const cleanup = operation.kind === "remove" ? operation.request.cleanupCommands.commands.find((command) => command.status === "failed") : null;
|
|
1000
|
+
throw new CliError(operation.error ?? "Tree removal failed; Git kept the tree.", 5, "WORKTREE_REMOVAL_FAILED", cleanup ?? void 0);
|
|
962
1001
|
}
|
|
963
|
-
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
|
|
964
1002
|
if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
|
|
965
1003
|
print(operation.result, () => {
|
|
966
1004
|
const warning = operation.result?.cleanup.warning;
|
|
@@ -976,7 +1014,7 @@ async function main(args) {
|
|
|
976
1014
|
const options = webPanelOpenCommand.opts();
|
|
977
1015
|
const worktree = await resolveWorktree(options.worktree);
|
|
978
1016
|
const definition = await webPanelDefinition(worktree.id, identifier);
|
|
979
|
-
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, {
|
|
1017
|
+
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, openWebPanelResponseSchema, {
|
|
980
1018
|
method: "POST",
|
|
981
1019
|
body: JSON.stringify({
|
|
982
1020
|
definitionId: definition.id,
|
|
@@ -1008,7 +1046,7 @@ async function main(args) {
|
|
|
1008
1046
|
const worktree = await resolveWorktree(options.worktree);
|
|
1009
1047
|
const body = { name: options.name };
|
|
1010
1048
|
if (argv) body.argv = argv;
|
|
1011
|
-
const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
|
|
1049
|
+
const result = await request(`/api/worktrees/${worktree.id}/terminals`, terminalResponseSchema, {
|
|
1012
1050
|
method: "POST",
|
|
1013
1051
|
body: JSON.stringify(body)
|
|
1014
1052
|
});
|
|
@@ -1028,7 +1066,7 @@ async function main(args) {
|
|
|
1028
1066
|
const { lines: rawLines } = terminalCaptureCommand.opts();
|
|
1029
1067
|
const lines = rawLines === void 0 ? 200 : parseCaptureLines(rawLines);
|
|
1030
1068
|
const terminalId = resolveTerminalId(identifier);
|
|
1031
|
-
const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}
|
|
1069
|
+
const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`, terminalCaptureResponseSchema);
|
|
1032
1070
|
if (jsonOutput) print(capture);
|
|
1033
1071
|
else {
|
|
1034
1072
|
writeStdout(capture.content);
|
|
@@ -1048,7 +1086,7 @@ async function main(args) {
|
|
|
1048
1086
|
print(result, () => `${result.terminal.name} (${result.terminal.id}) reached ${result.condition} at ${result.observedAt}`);
|
|
1049
1087
|
});
|
|
1050
1088
|
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" });
|
|
1089
|
+
await request(`/api/terminals/${terminalId}`, okResponseSchema, { method: "DELETE" });
|
|
1052
1090
|
print({
|
|
1053
1091
|
ok: true,
|
|
1054
1092
|
terminalId
|
|
@@ -1094,18 +1132,27 @@ async function runCliApplication(options) {
|
|
|
1094
1132
|
writeStdout = options.stdout ?? ((value) => process.stdout.write(value));
|
|
1095
1133
|
writeStderr = options.stderr ?? ((value) => process.stderr.write(value));
|
|
1096
1134
|
requestedExitCode = 0;
|
|
1135
|
+
updateProgressShown = false;
|
|
1136
|
+
output = humanOutput(environment, options.stdoutIsTTY ?? (!options.stdout && Boolean(process.stdout.isTTY)), jsonOutput);
|
|
1137
|
+
errorOutput = humanOutput(environment, options.stderrIsTTY ?? (!options.stderr && Boolean(process.stderr.isTTY)), jsonOutput);
|
|
1097
1138
|
try {
|
|
1098
1139
|
await main([...options.args]);
|
|
1099
1140
|
} catch (error) {
|
|
1100
|
-
const cliError = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
|
|
1141
|
+
const cliError = error instanceof CliError || error instanceof LocalUpdateError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
|
|
1101
1142
|
if (jsonOutput) {
|
|
1102
|
-
const body = { error: {
|
|
1143
|
+
const body = { error: cliError.details === void 0 ? {
|
|
1103
1144
|
code: cliError.code,
|
|
1104
1145
|
message: cliError.message
|
|
1146
|
+
} : {
|
|
1147
|
+
code: cliError.code,
|
|
1148
|
+
message: cliError.message,
|
|
1149
|
+
details: cliError.details
|
|
1105
1150
|
} };
|
|
1106
|
-
if (cliError.details !== void 0) body.error.details = cliError.details;
|
|
1107
1151
|
writeStderr(`${JSON.stringify(body)}\n`);
|
|
1108
|
-
} else
|
|
1152
|
+
} else {
|
|
1153
|
+
const text = error instanceof LocalUpdateError ? formatLocalUpdateError(error.message, error.details, errorOutput, error.code === "UPDATE_CANCELLED" || error.code === "UPDATE_INTERRUPTED") : errorOutput.blocks(errorOutput.summary(cliError.code === "USAGE_ERROR" ? "Invalid command" : "Command failed", "failure"), errorOutput.indent(cliError.message.replace(/^error: /, "")), cliError.code === "USAGE_ERROR" && errorOutput.next(["treeport --help"]));
|
|
1154
|
+
writeStderr(`${updateProgressShown ? "\n" : ""}${text}\n`);
|
|
1155
|
+
}
|
|
1109
1156
|
requestedExitCode = cliError.exitCode;
|
|
1110
1157
|
}
|
|
1111
1158
|
return requestedExitCode;
|