@treeport/treeport 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/treeport.mjs +26 -1
- package/dist/dist-BsLn2Gbc.js +1630 -0
- package/dist/node/cli/index.js +226 -73
- package/dist/node/server/core/launcher.js +13 -22
- package/dist/node/server/index.js +10440 -5957
- 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-BW-a6Bd-.js → update-UYS2lMdD.js} +86 -447
- 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/0010_browser_panels_and_permissions.sql +21 -0
- package/drizzle/0011_free_magdalene.sql +1 -0
- package/drizzle/0012_terminal_host_cutover.sql +41 -0
- package/drizzle/0013_workspace_item_order.sql +13 -0
- package/drizzle/meta/0010_snapshot.json +923 -0
- package/drizzle/meta/0011_snapshot.json +931 -0
- package/drizzle/meta/0012_snapshot.json +919 -0
- package/drizzle/meta/0013_snapshot.json +987 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +21 -13
- package/skills/treeport/SKILL.md +62 -6
- package/dist/web/assets/index-Cr4UkmRD.js +0 -146
- package/dist/web/assets/index-he-SubzL.css +0 -2
package/dist/node/cli/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { Command, CommanderError } from "commander";
|
|
6
|
-
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";
|
|
7
11
|
import { spawn } from "node:child_process";
|
|
8
12
|
//#region src/cli/args.ts
|
|
9
13
|
function extractJsonOutput(args) {
|
|
@@ -145,7 +149,7 @@ async function ensureServiceDaemon() {
|
|
|
145
149
|
pid: state.pid
|
|
146
150
|
};
|
|
147
151
|
}
|
|
148
|
-
async function request(pathname, options = {}) {
|
|
152
|
+
async function request(pathname, schema, options = {}) {
|
|
149
153
|
const controller = new AbortController();
|
|
150
154
|
const externalSignal = options.signal;
|
|
151
155
|
const abort = () => controller.abort();
|
|
@@ -163,10 +167,12 @@ async function request(pathname, options = {}) {
|
|
|
163
167
|
});
|
|
164
168
|
const body = await response.json().catch(() => ({}));
|
|
165
169
|
if (!response.ok) {
|
|
166
|
-
const
|
|
167
|
-
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);
|
|
168
172
|
}
|
|
169
|
-
|
|
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;
|
|
170
176
|
} catch (error) {
|
|
171
177
|
if (error instanceof CliError) throw error;
|
|
172
178
|
throw new CliError(`Cannot reach Treeport daemon at ${apiUrl}: ${error instanceof Error ? error.message : String(error)}`, 3, "DAEMON_UNREACHABLE");
|
|
@@ -176,19 +182,19 @@ async function request(pathname, options = {}) {
|
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
184
|
async function createWorktree(projectId, input) {
|
|
179
|
-
let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, {
|
|
185
|
+
let operation = (await request(`/api/projects/${encodeURIComponent(projectId)}/worktree-operations`, operationResponseSchema, {
|
|
180
186
|
method: "POST",
|
|
181
187
|
body: JSON.stringify(input)
|
|
182
188
|
})).operation;
|
|
183
189
|
while (operation.status === "pending" || operation.status === "running") {
|
|
184
190
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
185
|
-
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}
|
|
191
|
+
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`, operationResponseSchema)).operation;
|
|
186
192
|
}
|
|
187
193
|
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
|
|
188
194
|
if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
|
|
189
195
|
const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
|
|
190
196
|
if (!worktreeId) throw new CliError("Completed tree creation did not identify its tree", 5, "INVALID_OPERATION_RESULT");
|
|
191
|
-
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);
|
|
192
198
|
if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
|
|
193
199
|
const terminalId = operation.result?.terminalId ?? null;
|
|
194
200
|
return {
|
|
@@ -211,7 +217,7 @@ async function canonical(value) {
|
|
|
211
217
|
return fs.realpath(resolved).catch(() => resolved);
|
|
212
218
|
}
|
|
213
219
|
async function projects() {
|
|
214
|
-
return (await request("/api/projects")).projects;
|
|
220
|
+
return (await request("/api/projects", projectsResponseSchema)).projects;
|
|
215
221
|
}
|
|
216
222
|
function pathContains(candidate, parent) {
|
|
217
223
|
const relative = path.relative(parent, candidate);
|
|
@@ -239,7 +245,7 @@ async function packageSource(value) {
|
|
|
239
245
|
return value;
|
|
240
246
|
}
|
|
241
247
|
async function localPackageProjectId() {
|
|
242
|
-
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;
|
|
243
249
|
}
|
|
244
250
|
async function resolveWorktree(identifier) {
|
|
245
251
|
const all = (await projects()).flatMap((project) => project.worktrees);
|
|
@@ -263,12 +269,12 @@ function parseWebPanelInput(value) {
|
|
|
263
269
|
} catch (error) {
|
|
264
270
|
throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
|
|
265
271
|
}
|
|
266
|
-
const validated = webPanelInputSchema
|
|
267
|
-
if (!validated
|
|
268
|
-
return validated
|
|
272
|
+
const validated = decodeUnknownOrNull(webPanelInputSchema, parsed);
|
|
273
|
+
if (!validated) throw new CliError("--input must contain a JSON object", 2);
|
|
274
|
+
return validated;
|
|
269
275
|
}
|
|
270
276
|
async function webPanelDefinition(worktreeId, identifier) {
|
|
271
|
-
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;
|
|
272
278
|
const exact = definitions.find((definition) => definition.id === identifier);
|
|
273
279
|
if (exact) return exact;
|
|
274
280
|
const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
|
|
@@ -285,7 +291,37 @@ async function webPanelLaunchCwd(worktree) {
|
|
|
285
291
|
});
|
|
286
292
|
return path.relative(worktreeRoot, cwd) || ".";
|
|
287
293
|
}
|
|
288
|
-
function
|
|
294
|
+
async function resolveBrowserPanel(panelId) {
|
|
295
|
+
const candidates = (await projects()).flatMap((project) => project.worktrees.flatMap((worktree) => worktree.panels.filter((panel) => panel.kind === "browser").map((panel) => ({
|
|
296
|
+
panel,
|
|
297
|
+
worktree
|
|
298
|
+
}))));
|
|
299
|
+
if (panelId) {
|
|
300
|
+
const match = candidates.find((candidate) => candidate.panel.id === panelId);
|
|
301
|
+
if (!match) throw new CliError(`Browser ${panelId} was not found`, 5);
|
|
302
|
+
return match;
|
|
303
|
+
}
|
|
304
|
+
const worktree = await resolveWorktree(".");
|
|
305
|
+
const matches = candidates.filter((candidate) => candidate.worktree.id === worktree.id);
|
|
306
|
+
if (matches.length === 1) return matches[0];
|
|
307
|
+
if (matches.length === 0) throw new CliError(`No Browser is open in worktree ${worktree.name}`, 5, "BROWSER_PANEL_NOT_FOUND");
|
|
308
|
+
throw new CliError(`More than one Browser is open in worktree ${worktree.name}; specify --panel`, 5, "BROWSER_PANEL_AMBIGUOUS", { panelIds: matches.map((candidate) => candidate.panel.id) });
|
|
309
|
+
}
|
|
310
|
+
async function runBrowserAgentCommand(command, args, panelId) {
|
|
311
|
+
const { panel } = await resolveBrowserPanel(panelId);
|
|
312
|
+
const result = await request(`/api/panels/${encodeURIComponent(panel.id)}/browser-agent`, browserAgentResponseSchema, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
command,
|
|
316
|
+
args
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
panelId: panel.id,
|
|
321
|
+
output: result.output
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function panelUrl(worktree, panelId) {
|
|
289
325
|
const target = new URL(apiUrl);
|
|
290
326
|
target.pathname = `/projects/${encodeURIComponent(worktree.projectId)}/worktrees/${encodeURIComponent(worktree.id)}/panels/${encodeURIComponent(panelId)}`;
|
|
291
327
|
target.search = "";
|
|
@@ -315,7 +351,7 @@ function parseDuration(value) {
|
|
|
315
351
|
}
|
|
316
352
|
}
|
|
317
353
|
async function inspectTerminal(terminalId, signal) {
|
|
318
|
-
return request(`/api/terminals/${encodeURIComponent(terminalId)}`, signal ? { signal } : {});
|
|
354
|
+
return request(`/api/terminals/${encodeURIComponent(terminalId)}`, terminalObservationResponseSchema, signal ? { signal } : {});
|
|
319
355
|
}
|
|
320
356
|
async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
321
357
|
let observation = null;
|
|
@@ -347,16 +383,9 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
347
383
|
if (condition === "bell") bellBaseline = observation.metadata.bell?.sequence ?? 0;
|
|
348
384
|
const immediate = matched((/* @__PURE__ */ new Date()).toISOString());
|
|
349
385
|
if (immediate) return immediate;
|
|
350
|
-
events = io(`${apiUrl}/events`, {
|
|
351
|
-
path: SOCKET_IO_PATH,
|
|
352
|
-
transports: ["websocket"],
|
|
353
|
-
forceNew: true,
|
|
354
|
-
autoConnect: false,
|
|
355
|
-
reconnection: false,
|
|
356
|
-
retries: 0
|
|
357
|
-
});
|
|
358
386
|
return await new Promise((resolve, reject) => {
|
|
359
387
|
let settled = false;
|
|
388
|
+
let connected = false;
|
|
360
389
|
let queue = Promise.resolve();
|
|
361
390
|
const finish = (result) => {
|
|
362
391
|
if (!settled) {
|
|
@@ -374,16 +403,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
374
403
|
queue = queue.then(task);
|
|
375
404
|
queue.catch(fail);
|
|
376
405
|
};
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
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);
|
|
382
410
|
if (metadata) observation = {
|
|
383
411
|
...observation,
|
|
384
412
|
metadata
|
|
385
413
|
};
|
|
386
|
-
const snapshotMatch = matched(
|
|
414
|
+
const snapshotMatch = matched(value.at);
|
|
387
415
|
if (snapshotMatch) {
|
|
388
416
|
finish(snapshotMatch);
|
|
389
417
|
return;
|
|
@@ -392,10 +420,8 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
392
420
|
if (cancellation) throw new Error("Terminal wait cancelled");
|
|
393
421
|
const refreshedMatch = matched((/* @__PURE__ */ new Date()).toISOString());
|
|
394
422
|
if (refreshedMatch) finish(refreshedMatch);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const event = parseProductEvent(value);
|
|
398
|
-
if (!event) throw new CliError("Treeport daemon sent an invalid product event", 3, "DAEMON_PROTOCOL_ERROR");
|
|
423
|
+
};
|
|
424
|
+
const productEvent = async (event) => {
|
|
399
425
|
if (event.type !== "terminal.metadata" && event.type !== "terminal.updated" && event.type !== "terminal.removed") return;
|
|
400
426
|
if (event.data.terminalId !== terminalId) return;
|
|
401
427
|
if (event.type === "terminal.removed") throw new CliError(`Terminal ${terminalId} was removed while waiting`, 5, "TERMINAL_REMOVED", {
|
|
@@ -415,10 +441,15 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
415
441
|
}
|
|
416
442
|
const result = matched(event.at);
|
|
417
443
|
if (result) finish(result);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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);
|
|
422
453
|
});
|
|
423
454
|
} catch (error) {
|
|
424
455
|
if (cancellation === "timeout") throw new CliError(`Timed out waiting for terminal ${terminalId} to reach ${condition}`, 4, "WAIT_TIMEOUT", {
|
|
@@ -434,20 +465,16 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
434
465
|
if (timeout) clearTimeout(timeout);
|
|
435
466
|
process.off("SIGINT", interrupt);
|
|
436
467
|
controller.abort();
|
|
437
|
-
events
|
|
438
|
-
events?.disconnect();
|
|
468
|
+
if (events) Effect.runFork(Fiber.interrupt(events));
|
|
439
469
|
}
|
|
440
470
|
}
|
|
441
471
|
function print(value, human) {
|
|
442
472
|
writeStdout(`${jsonOutput ? JSON.stringify(value) : human ? human() : JSON.stringify(value, null, 2)}\n`);
|
|
443
473
|
}
|
|
444
|
-
const agentGuidance = `AI agents:
|
|
445
|
-
If you're an AI agent, use \`treeport skills\` to see the usage guide.
|
|
446
|
-
`;
|
|
447
474
|
async function main(args) {
|
|
448
475
|
const argv = args[0] === "spawn" || args[0] === "terminal" && args[1] === "create" ? commandArgv(args) : void 0;
|
|
449
476
|
let parserError = "";
|
|
450
|
-
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").
|
|
477
|
+
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({
|
|
451
478
|
writeOut: writeStdout,
|
|
452
479
|
writeErr: (value) => {
|
|
453
480
|
parserError += value;
|
|
@@ -471,7 +498,7 @@ async function main(args) {
|
|
|
471
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");
|
|
472
499
|
} else if (lifecycle === "service") await ensureServiceDaemon();
|
|
473
500
|
else await daemonUp({});
|
|
474
|
-
const registered = await request("/api/projects", {
|
|
501
|
+
const registered = await request("/api/projects", projectResponseSchema, {
|
|
475
502
|
method: "POST",
|
|
476
503
|
body: JSON.stringify({ path: canonicalFolder })
|
|
477
504
|
});
|
|
@@ -484,7 +511,7 @@ async function main(args) {
|
|
|
484
511
|
target.pathname = `/projects/${encodeURIComponent(registered.project.id)}/worktrees/${encodeURIComponent(targetWorktree.id)}`;
|
|
485
512
|
target.search = "";
|
|
486
513
|
target.hash = "";
|
|
487
|
-
const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, {
|
|
514
|
+
const opened = contextTerminalId ? await request(`/api/worktrees/${encodeURIComponent(targetWorktree.id)}/open`, okResponseSchema, {
|
|
488
515
|
method: "POST",
|
|
489
516
|
body: JSON.stringify({ sourceTerminalId: contextTerminalId })
|
|
490
517
|
}).then(() => ({ client: "current" })) : await openWorkspace(target.href).catch((error) => {
|
|
@@ -521,13 +548,13 @@ async function main(args) {
|
|
|
521
548
|
if (options.foreground) return;
|
|
522
549
|
print(result, () => `Treeport is running\n${result.apiUrl}`);
|
|
523
550
|
});
|
|
524
|
-
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");
|
|
525
552
|
stopCommand.action(async () => {
|
|
526
553
|
const lifecycle = await resolveDaemonLifecycle();
|
|
527
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");
|
|
528
555
|
const options = stopCommand.opts();
|
|
529
556
|
if (options.terminateTerminals && !options.force) throw new CliError("Re-run with --terminate-terminals --force to confirm termination of every terminal.", 2);
|
|
530
|
-
if (options.terminateTerminals) await request("/api/admin/terminate-terminals", { method: "POST" });
|
|
557
|
+
if (options.terminateTerminals) await request("/api/admin/terminate-terminals", terminatedTerminalsResponseSchema, { method: "POST" });
|
|
531
558
|
if (lifecycle === "service") {
|
|
532
559
|
const result = await serviceStop();
|
|
533
560
|
print(result, () => formatServiceStatus(result.status));
|
|
@@ -662,7 +689,7 @@ async function main(args) {
|
|
|
662
689
|
...!terminalId ? [`${contextPrefix}_TERMINAL_ID`] : []
|
|
663
690
|
];
|
|
664
691
|
if (missing.length) throw new CliError(`Incomplete Treeport context; missing ${missing.join(", ")}`, 5, "TREEPORT_CONTEXT_INCOMPLETE", { missing });
|
|
665
|
-
const project = (await request(`/api/projects/${encodeURIComponent(projectId)}
|
|
692
|
+
const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`, projectResponseSchema)).project;
|
|
666
693
|
const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
|
|
667
694
|
if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
|
|
668
695
|
projectId,
|
|
@@ -673,6 +700,7 @@ async function main(args) {
|
|
|
673
700
|
worktreeId,
|
|
674
701
|
terminalId
|
|
675
702
|
});
|
|
703
|
+
const treeContext = (await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/context`, treeContextResponseSchema)).context;
|
|
676
704
|
const context = {
|
|
677
705
|
managed: true,
|
|
678
706
|
apiUrl,
|
|
@@ -695,7 +723,8 @@ async function main(args) {
|
|
|
695
723
|
head: worktree.head,
|
|
696
724
|
branch: worktree.branch,
|
|
697
725
|
detached: worktree.detached,
|
|
698
|
-
kind: worktree.kind
|
|
726
|
+
kind: worktree.kind,
|
|
727
|
+
context: treeContext
|
|
699
728
|
},
|
|
700
729
|
terminal: {
|
|
701
730
|
id: terminal.id,
|
|
@@ -705,14 +734,114 @@ async function main(args) {
|
|
|
705
734
|
exitCode: terminal.exitCode
|
|
706
735
|
}
|
|
707
736
|
};
|
|
708
|
-
print(context, () =>
|
|
737
|
+
print(context, () => {
|
|
738
|
+
const entries = Object.entries(context.worktree.context);
|
|
739
|
+
const treeContextText = entries.length === 0 ? "—" : entries.map(([key, value]) => {
|
|
740
|
+
const [first = "", ...rest] = [...value].map((character) => {
|
|
741
|
+
const code = character.charCodeAt(0);
|
|
742
|
+
return code !== 10 && (code <= 31 || code >= 127 && code <= 159) ? `\\u${code.toString(16).padStart(4, "0")}` : character;
|
|
743
|
+
}).join("").split("\n");
|
|
744
|
+
return ` ${key}: ${first}${rest.length > 0 ? `\n${rest.map((line) => ` ${line}`).join("\n")}` : ""}`;
|
|
745
|
+
}).join("\n");
|
|
746
|
+
return `Treeport context\n\nProject: ${context.project.name} (${context.project.id})\nTree: ${context.worktree.name} (${context.worktree.id})\nPath: ${context.worktree.path}\nContext:\n${treeContextText}\nTerminal: ${context.terminal.name} (${context.terminal.id}) — ${context.terminal.status}\nAPI: ${context.apiUrl}\nLifecycle: ${context.daemonLifecycle === "external" ? "externally managed" : context.daemonLifecycle === "service" ? "managed by the OS service" : "managed by Treeport"}`;
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
const browserCommand = program.command("browser").description("Manage Browser and its hosted Chromium");
|
|
750
|
+
browserCommand.action(() => {
|
|
751
|
+
writeStdout(browserCommand.helpInformation());
|
|
709
752
|
});
|
|
753
|
+
const browserOpenCommand = browserCommand.command("open").description("Open Browser and request client navigation").argument("[url]", "absolute HTTP or HTTPS URL").requiredOption("--worktree <id-or-path-or-dot>", "owning tree").option("--json", "emit machine-readable JSON");
|
|
754
|
+
browserOpenCommand.action(async (url) => {
|
|
755
|
+
const worktree = await resolveWorktree(browserOpenCommand.opts().worktree);
|
|
756
|
+
const body = {
|
|
757
|
+
url,
|
|
758
|
+
sourceTerminalId: contextWorktreeId === worktree.id ? contextTerminalId ?? null : null
|
|
759
|
+
};
|
|
760
|
+
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/browser-panels`, openBrowserPanelResponseSchema, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
body: JSON.stringify(body)
|
|
763
|
+
});
|
|
764
|
+
const output = {
|
|
765
|
+
...result,
|
|
766
|
+
url: panelUrl(worktree, result.panel.id)
|
|
767
|
+
};
|
|
768
|
+
print(output, () => `Opened ${result.panel.title} (${result.panel.id})\n${output.url}`);
|
|
769
|
+
});
|
|
770
|
+
browserCommand.command("install").description("Install the Chromium build used by Browser").option("--json", "emit machine-readable JSON").action(async () => {
|
|
771
|
+
const result = await request("/api/browser/install", browserInstallResponseSchema, { method: "POST" });
|
|
772
|
+
print(result, () => result.message);
|
|
773
|
+
});
|
|
774
|
+
browserCommand.command("status").description("Show hosted browser installation status").option("--json", "emit machine-readable JSON").action(async () => {
|
|
775
|
+
const result = await request("/api/browser/status", browserInstallStatusSchema);
|
|
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}` : ""}`);
|
|
777
|
+
});
|
|
778
|
+
browserCommand.command("remove").description("Remove Treeport's hosted Chromium build").option("--json", "emit machine-readable JSON").action(async () => {
|
|
779
|
+
await request("/api/browser/install", okResponseSchema, { method: "DELETE" });
|
|
780
|
+
print({ removed: true }, () => "Removed Treeport hosted Chromium");
|
|
781
|
+
});
|
|
782
|
+
browserCommand.command("list").description("List open Browser sessions").option("--json", "emit machine-readable JSON").action(async () => {
|
|
783
|
+
const panels = (await projects()).flatMap((project) => project.worktrees.flatMap((worktree) => worktree.panels.filter((panel) => panel.kind === "browser").map((panel) => ({
|
|
784
|
+
panelId: panel.id,
|
|
785
|
+
title: panel.title,
|
|
786
|
+
worktreeId: worktree.id,
|
|
787
|
+
worktree: worktree.name,
|
|
788
|
+
projectId: project.id,
|
|
789
|
+
project: project.name
|
|
790
|
+
}))));
|
|
791
|
+
print(panels, () => panels.length ? panels.map((panel) => `${panel.panelId}\t${panel.project} / ${panel.worktree}\t${panel.title}`).join("\n") : "Browser is not open.");
|
|
792
|
+
});
|
|
793
|
+
const printAgentResult = async (command, args, panelId) => {
|
|
794
|
+
const result = await runBrowserAgentCommand(command, args, panelId);
|
|
795
|
+
print(result, () => result.output);
|
|
796
|
+
};
|
|
797
|
+
const browserSnapshotCommand = browserCommand.command("snapshot").description("Capture an accessibility snapshot of the hosted page").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
|
|
798
|
+
browserSnapshotCommand.action(async () => printAgentResult("snapshot", [], browserSnapshotCommand.opts().panel));
|
|
799
|
+
const browserClickCommand = browserCommand.command("click").description("Click an element from the latest browser snapshot").argument("<target>", "Playwright element reference or selector").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
|
|
800
|
+
browserClickCommand.action(async (target) => printAgentResult("click", [target], browserClickCommand.opts().panel));
|
|
801
|
+
const browserFillCommand = browserCommand.command("fill").description("Fill an editable element from the latest browser snapshot").argument("<target>", "Playwright element reference or selector").argument("<text>", "text to enter").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
|
|
802
|
+
browserFillCommand.action(async (target, text) => printAgentResult("fill", [target, text], browserFillCommand.opts().panel));
|
|
803
|
+
const browserPressCommand = browserCommand.command("press").description("Press a key in the hosted page").argument("<key>", "Playwright key name").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
|
|
804
|
+
browserPressCommand.action(async (key) => printAgentResult("press", [key], browserPressCommand.opts().panel));
|
|
805
|
+
const browserGotoCommand = browserCommand.command("goto").description("Navigate the hosted page").argument("<url>", "absolute HTTP or HTTPS URL").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
|
|
806
|
+
browserGotoCommand.action(async (url) => printAgentResult("goto", [url], browserGotoCommand.opts().panel));
|
|
807
|
+
const browserConsoleCommand = browserCommand.command("console").description("List page console messages").argument("[level]", "minimum console level").option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
|
|
808
|
+
browserConsoleCommand.action(async (level) => printAgentResult("console", level ? [level] : [], browserConsoleCommand.opts().panel));
|
|
809
|
+
for (const [name, description, agentName] of [
|
|
810
|
+
[
|
|
811
|
+
"back",
|
|
812
|
+
"Go back in the hosted page",
|
|
813
|
+
"go-back"
|
|
814
|
+
],
|
|
815
|
+
[
|
|
816
|
+
"forward",
|
|
817
|
+
"Go forward in the hosted page",
|
|
818
|
+
"go-forward"
|
|
819
|
+
],
|
|
820
|
+
[
|
|
821
|
+
"reload",
|
|
822
|
+
"Reload the hosted page",
|
|
823
|
+
"reload"
|
|
824
|
+
],
|
|
825
|
+
[
|
|
826
|
+
"network",
|
|
827
|
+
"List page network requests",
|
|
828
|
+
"requests"
|
|
829
|
+
],
|
|
830
|
+
[
|
|
831
|
+
"screenshot",
|
|
832
|
+
"Capture a screenshot of the hosted page",
|
|
833
|
+
"screenshot"
|
|
834
|
+
]
|
|
835
|
+
]) {
|
|
836
|
+
const command = browserCommand.command(name).description(description).option("--panel <panel-id>", "Browser ID").option("--json", "emit machine-readable JSON");
|
|
837
|
+
command.action(async () => printAgentResult(agentName, [], command.opts().panel));
|
|
838
|
+
}
|
|
710
839
|
const installCommand = program.command("install").description("Install and configure a Treeport package").argument("<source>", "npm: source or local directory").option("-l, --local", "configure the registered project containing the current directory").option("--json", "emit machine-readable JSON");
|
|
711
840
|
installCommand.action(async (source) => {
|
|
712
841
|
const options = installCommand.opts();
|
|
713
842
|
const body = { source: await packageSource(source) };
|
|
714
843
|
if (options.local) body.projectId = await localPackageProjectId();
|
|
715
|
-
const result = (await request("/api/packages/install", {
|
|
844
|
+
const result = (await request("/api/packages/install", packageOperationResponseSchema, {
|
|
716
845
|
method: "POST",
|
|
717
846
|
body: JSON.stringify(body)
|
|
718
847
|
})).result;
|
|
@@ -723,14 +852,14 @@ async function main(args) {
|
|
|
723
852
|
const options = removePackageCommand.opts();
|
|
724
853
|
const body = { source: await packageSource(source) };
|
|
725
854
|
if (options.local) body.projectId = await localPackageProjectId();
|
|
726
|
-
const result = (await request("/api/packages/remove", {
|
|
855
|
+
const result = (await request("/api/packages/remove", packageOperationResponseSchema, {
|
|
727
856
|
method: "POST",
|
|
728
857
|
body: JSON.stringify(body)
|
|
729
858
|
})).result;
|
|
730
859
|
print(result, () => `Removed ${result.source}`);
|
|
731
860
|
});
|
|
732
861
|
program.command("list").description("List configured Treeport packages").option("--json", "emit machine-readable JSON").action(async () => {
|
|
733
|
-
const result = await request("/api/packages");
|
|
862
|
+
const result = await request("/api/packages", packageListingResponseSchema);
|
|
734
863
|
print(result, () => {
|
|
735
864
|
const lines = result.packages.map((pkg) => {
|
|
736
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`;
|
|
@@ -748,7 +877,7 @@ async function main(args) {
|
|
|
748
877
|
const selfUpdateOptions = { environment: cliEnvironment };
|
|
749
878
|
if (!jsonOutput) selfUpdateOptions.progress = (message) => writeStderr(`${message}\n`);
|
|
750
879
|
const result = await runLocalUpdate(selfUpdateOptions).catch((error) => {
|
|
751
|
-
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);
|
|
752
881
|
throw error;
|
|
753
882
|
});
|
|
754
883
|
print(result, () => {
|
|
@@ -757,7 +886,7 @@ async function main(args) {
|
|
|
757
886
|
});
|
|
758
887
|
return;
|
|
759
888
|
}
|
|
760
|
-
const results = (await request("/api/packages/update", {
|
|
889
|
+
const results = (await request("/api/packages/update", packageOperationsResponseSchema, {
|
|
761
890
|
method: "POST",
|
|
762
891
|
body: JSON.stringify(source ? { source: await packageSource(source) } : {})
|
|
763
892
|
})).results;
|
|
@@ -766,7 +895,7 @@ async function main(args) {
|
|
|
766
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");
|
|
767
896
|
reloadCommand.action(async () => {
|
|
768
897
|
const options = reloadCommand.opts();
|
|
769
|
-
const result = await request("/api/packages/reload", {
|
|
898
|
+
const result = await request("/api/packages/reload", packageReloadResponseSchema, {
|
|
770
899
|
method: "POST",
|
|
771
900
|
body: JSON.stringify(options.local ? { projectId: await localPackageProjectId() } : {})
|
|
772
901
|
});
|
|
@@ -779,7 +908,7 @@ async function main(args) {
|
|
|
779
908
|
throw new CliError(projectCommand.helpInformation(), 2);
|
|
780
909
|
});
|
|
781
910
|
projectCommand.command("add").description("Register a folder or Git repository").argument("<path>", "folder path").option("--json", "emit machine-readable JSON").action(async (repository) => {
|
|
782
|
-
const body = await request("/api/projects", {
|
|
911
|
+
const body = await request("/api/projects", projectResponseSchema, {
|
|
783
912
|
method: "POST",
|
|
784
913
|
body: JSON.stringify({ path: await canonical(repository) })
|
|
785
914
|
});
|
|
@@ -812,25 +941,46 @@ async function main(args) {
|
|
|
812
941
|
const result = await createWorktree(project.id, request);
|
|
813
942
|
print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
|
|
814
943
|
});
|
|
815
|
-
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");
|
|
816
945
|
worktreeRemoveCommand.action(async (identifier) => {
|
|
817
|
-
const { force: confirmed } = worktreeRemoveCommand.opts();
|
|
946
|
+
const { force: confirmed, skipCleanup: requestedSkipCleanup } = worktreeRemoveCommand.opts();
|
|
818
947
|
const worktree = await resolveWorktree(identifier);
|
|
819
|
-
const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview
|
|
948
|
+
const preview = (await request(`/api/worktrees/${worktree.id}/remove-preview`, removePreviewResponseSchema)).preview;
|
|
820
949
|
if (!preview.eligible) throw new CliError(preview.reasons.join("\n"), 5);
|
|
821
950
|
if (preview.warnings.length && !confirmed) throw new CliError(`${preview.warnings.join("\n")}\nRe-run with --force to confirm removal.`, 5);
|
|
822
|
-
|
|
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, {
|
|
823
954
|
method: "POST",
|
|
824
955
|
body: JSON.stringify({
|
|
825
956
|
confirmationToken: preview.confirmationToken,
|
|
826
|
-
confirmDestructive: preview.warnings.length > 0
|
|
957
|
+
confirmDestructive: preview.warnings.length > 0 || skipCleanup,
|
|
958
|
+
skipCleanup
|
|
827
959
|
})
|
|
828
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();
|
|
829
975
|
while (operation.status === "pending" || operation.status === "running") {
|
|
830
976
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
831
|
-
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);
|
|
832
983
|
}
|
|
833
|
-
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
|
|
834
984
|
if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
|
|
835
985
|
print(operation.result, () => {
|
|
836
986
|
const warning = operation.result?.cleanup.warning;
|
|
@@ -846,7 +996,7 @@ async function main(args) {
|
|
|
846
996
|
const options = webPanelOpenCommand.opts();
|
|
847
997
|
const worktree = await resolveWorktree(options.worktree);
|
|
848
998
|
const definition = await webPanelDefinition(worktree.id, identifier);
|
|
849
|
-
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, {
|
|
999
|
+
const result = await request(`/api/worktrees/${encodeURIComponent(worktree.id)}/panels/open`, openWebPanelResponseSchema, {
|
|
850
1000
|
method: "POST",
|
|
851
1001
|
body: JSON.stringify({
|
|
852
1002
|
definitionId: definition.id,
|
|
@@ -858,7 +1008,7 @@ async function main(args) {
|
|
|
858
1008
|
});
|
|
859
1009
|
const output = {
|
|
860
1010
|
...result,
|
|
861
|
-
url:
|
|
1011
|
+
url: panelUrl(worktree, result.panel.id)
|
|
862
1012
|
};
|
|
863
1013
|
print(output, () => `${result.reused ? "Reused" : "Opened"} ${result.panel.title} (${result.panel.id})\n${output.url}`);
|
|
864
1014
|
});
|
|
@@ -878,7 +1028,7 @@ async function main(args) {
|
|
|
878
1028
|
const worktree = await resolveWorktree(options.worktree);
|
|
879
1029
|
const body = { name: options.name };
|
|
880
1030
|
if (argv) body.argv = argv;
|
|
881
|
-
const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
|
|
1031
|
+
const result = await request(`/api/worktrees/${worktree.id}/terminals`, terminalResponseSchema, {
|
|
882
1032
|
method: "POST",
|
|
883
1033
|
body: JSON.stringify(body)
|
|
884
1034
|
});
|
|
@@ -898,7 +1048,7 @@ async function main(args) {
|
|
|
898
1048
|
const { lines: rawLines } = terminalCaptureCommand.opts();
|
|
899
1049
|
const lines = rawLines === void 0 ? 200 : parseCaptureLines(rawLines);
|
|
900
1050
|
const terminalId = resolveTerminalId(identifier);
|
|
901
|
-
const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}
|
|
1051
|
+
const capture = await request(`/api/terminals/${encodeURIComponent(terminalId)}/capture?lines=${lines}`, terminalCaptureResponseSchema);
|
|
902
1052
|
if (jsonOutput) print(capture);
|
|
903
1053
|
else {
|
|
904
1054
|
writeStdout(capture.content);
|
|
@@ -918,7 +1068,7 @@ async function main(args) {
|
|
|
918
1068
|
print(result, () => `${result.terminal.name} (${result.terminal.id}) reached ${result.condition} at ${result.observedAt}`);
|
|
919
1069
|
});
|
|
920
1070
|
terminalCommand.command("delete").description("Delete a terminal").argument("<terminal-id>", "terminal to delete").option("--json", "emit machine-readable JSON").action(async (terminalId) => {
|
|
921
|
-
await request(`/api/terminals/${terminalId}`, { method: "DELETE" });
|
|
1071
|
+
await request(`/api/terminals/${terminalId}`, okResponseSchema, { method: "DELETE" });
|
|
922
1072
|
print({
|
|
923
1073
|
ok: true,
|
|
924
1074
|
terminalId
|
|
@@ -969,11 +1119,14 @@ async function runCliApplication(options) {
|
|
|
969
1119
|
} catch (error) {
|
|
970
1120
|
const cliError = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error), 1);
|
|
971
1121
|
if (jsonOutput) {
|
|
972
|
-
const body = { error: {
|
|
1122
|
+
const body = { error: cliError.details === void 0 ? {
|
|
973
1123
|
code: cliError.code,
|
|
974
1124
|
message: cliError.message
|
|
1125
|
+
} : {
|
|
1126
|
+
code: cliError.code,
|
|
1127
|
+
message: cliError.message,
|
|
1128
|
+
details: cliError.details
|
|
975
1129
|
} };
|
|
976
|
-
if (cliError.details !== void 0) body.error.details = cliError.details;
|
|
977
1130
|
writeStderr(`${JSON.stringify(body)}\n`);
|
|
978
1131
|
} else writeStderr(`${cliError.message}\n`);
|
|
979
1132
|
requestedExitCode = cliError.exitCode;
|