@treeport/treeport 0.3.0 → 0.4.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 +2 -2
- package/dist/{loopback-Dyv_owrb.js → loopback-D7k_J_Wl.js} +15 -10
- package/dist/node/cli/index.js +135 -87
- package/dist/node/server/core/launcher.js +19 -2
- package/dist/node/server/index.js +466 -375
- package/dist/{shell-integration-7aBNr-p0.js → shell-integration-Be_c91lw.js} +17 -15
- package/dist/web/assets/{index-CUy0IkGL.js → index-DCtptjcH.js} +5 -5
- package/dist/web/assets/index-Wj0w0nWP.css +2 -0
- package/dist/web/index.html +2 -2
- package/dist/web/manifest.webmanifest +2 -2
- package/package.json +4 -4
- package/skills/treeport/SKILL.md +19 -19
- package/dist/web/assets/index-0q5frbNy.css +0 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Treeport
|
|
2
2
|
|
|
3
|
-
Treeport is a
|
|
3
|
+
Treeport is a tree-first terminal driver for persistent development workspaces.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install --global @treeport/treeport
|
|
@@ -8,7 +8,7 @@ cd /path/to/repository
|
|
|
8
8
|
treeport .
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Treeport starts its backend if needed, registers the repository and its
|
|
11
|
+
Treeport starts its backend if needed, registers the repository and its trees, and opens the current tree in the desktop app or browser. Run `treeport start` to start only the backend. Use `treeport service enable` when a host must start Treeport after reboot.
|
|
12
12
|
|
|
13
13
|
Treeport supports macOS and Linux and requires Node.js 24 or newer, Git, and tmux 3.2 or newer.
|
|
14
14
|
|
|
@@ -12,6 +12,7 @@ const TERMINAL_SELECTION_START_SEQUENCE = "\x1B[9001~";
|
|
|
12
12
|
const TERMINAL_SELECTION_STOP_SEQUENCE = "\x1B[9002~";
|
|
13
13
|
const TERMINAL_SELECTION_CLEAR_SEQUENCE = "\x1B[9003~";
|
|
14
14
|
const TERMINAL_SELECTION_RESTORE_SEQUENCE = "\x1B[9004~";
|
|
15
|
+
z.unknown();
|
|
15
16
|
const terminalId = z.string().min(1).max(128);
|
|
16
17
|
const clientId = z.string().min(1).max(128);
|
|
17
18
|
const streamId = z.string().min(1).max(128);
|
|
@@ -238,6 +239,7 @@ const eventsSnapshotSchema = z.strictObject({
|
|
|
238
239
|
terminalMetadata: z.array(terminalRuntimeMetadataSchema),
|
|
239
240
|
webPanels: z.array(webPanelSnapshotSchema)
|
|
240
241
|
});
|
|
242
|
+
z.unknown();
|
|
241
243
|
function parseEventsSnapshot(value) {
|
|
242
244
|
const parsed = eventsSnapshotSchema.safeParse(value);
|
|
243
245
|
return parsed.success ? parsed.data : null;
|
|
@@ -304,7 +306,7 @@ const createWorktreeSchema = z.object({
|
|
|
304
306
|
if (value.base === "current" && !value.sourceWorktreeId) context.addIssue({
|
|
305
307
|
code: "custom",
|
|
306
308
|
path: ["sourceWorktreeId"],
|
|
307
|
-
message: "A source
|
|
309
|
+
message: "A source tree is required when starting from current"
|
|
308
310
|
});
|
|
309
311
|
});
|
|
310
312
|
const terminalCwdSchema = z.string().min(1).max(4096).refine((value) => value.trim().length > 0 && !value.includes("\0"), { message: "Working directory cannot be blank or contain NUL" });
|
|
@@ -370,22 +372,25 @@ z.object({
|
|
|
370
372
|
if (value.base === "current" && !value.sourceWorktreeId) context.addIssue({
|
|
371
373
|
code: "custom",
|
|
372
374
|
path: ["sourceWorktreeId"],
|
|
373
|
-
message: "A source
|
|
375
|
+
message: "A source tree is required when starting from current"
|
|
374
376
|
});
|
|
375
377
|
});
|
|
376
378
|
//#endregion
|
|
377
379
|
//#region src/duration.ts
|
|
378
|
-
const DURATION_UNITS =
|
|
379
|
-
ms
|
|
380
|
-
s
|
|
381
|
-
m
|
|
382
|
-
h
|
|
383
|
-
|
|
380
|
+
const DURATION_UNITS = /* @__PURE__ */ new Map([
|
|
381
|
+
["ms", 1],
|
|
382
|
+
["s", 1e3],
|
|
383
|
+
["m", 6e4],
|
|
384
|
+
["h", 36e5]
|
|
385
|
+
]);
|
|
384
386
|
const MAX_DURATION_MS = 2147483647;
|
|
385
387
|
function parseDurationMs(value) {
|
|
386
388
|
const match = /^(\d+)(ms|s|m|h)$/.exec(value);
|
|
387
389
|
if (!match) throw new Error("Timeout must be a positive duration such as 500ms, 30s, 5m, or 1h");
|
|
388
|
-
const
|
|
390
|
+
const amount = Number(match[1]);
|
|
391
|
+
const multiplier = DURATION_UNITS.get(match[2] ?? "");
|
|
392
|
+
if (multiplier === void 0) throw new Error("Timeout has an unsupported duration unit");
|
|
393
|
+
const timeoutMs = amount * multiplier;
|
|
389
394
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_DURATION_MS) throw new Error("Timeout must be between 1ms and 2147483647ms");
|
|
390
395
|
return timeoutMs;
|
|
391
396
|
}
|
|
@@ -404,4 +409,4 @@ function assertLoopbackHost(host) {
|
|
|
404
409
|
throw new Error("Treeport supports only loopback listeners. Run `treeport start --host 127.0.0.1`, then use `treeport remote enable` for private remote access.");
|
|
405
410
|
}
|
|
406
411
|
//#endregion
|
|
407
|
-
export {
|
|
412
|
+
export { terminalTakeControlSchema as $, parseEventsSnapshot as A, TERMINAL_SELECTION_CLEAR_SEQUENCE as B, repositoryTerminalPresetsFileSchema as C, updateTerminalPresetSchema as D, updateProjectSchema as E, TERMINAL_MAX_INPUT_BYTES as F, parseTerminalProgress as G, TERMINAL_SELECTION_START_SEQUENCE as H, TERMINAL_OUTPUT_HIGH_WATERMARK as I, terminalInputSchema as J, terminalBellAcknowledgementSchema as K, TERMINAL_OUTPUT_LOW_WATERMARK as L, SOCKET_IO_PATH as M, TERMINAL_CONTROLLER_GRACE_MS as N, updateTerminalSchema as O, TERMINAL_MAX_CLIENT_MESSAGE_BYTES as P, terminalSizeSchema as Q, TERMINAL_OUTPUT_STALL_TIMEOUT_MS as R, repositoryTerminalPresetSchema as S, terminalCaptureQuerySchema as T, TERMINAL_SELECTION_STOP_SEQUENCE as U, TERMINAL_SELECTION_RESTORE_SEQUENCE as V, parseTerminalAuth as W, terminalOutputAckSchema as X, terminalLegacyTakeControlSchema as Y, terminalResizeSchema as Z, packageReloadSchema as _, WEB_PANEL_INPUT_MAX_BYTES as a, registerProjectSchema as b, createTerminalSchema as c, deleteTerminalPresetSchema as d, deleteWebPanelStorageSchema as f, packageProjectQuerySchema as g, packageInstallSchema as h, TERMINAL_MAX_UPLOAD_BYTES as i, parseProductEvent as j, webPanelInputSchema as k, createWebPanelSchema as l, openWebPanelSchema as m, parseDurationMs as n, browseDirectoryQuerySchema as o, getWebPanelStorageSchema as p, terminalBinarySchema as q, TERMINAL_CAPTURE_MAX_LINES as r, createTerminalPresetSchema as s, assertLoopbackHost as t, createWorktreeSchema as u, packageRemoveSchema as v, setWebPanelStorageSchema as w, removeWorktreeSchema as x, packageUpdateSchema as y, TERMINAL_SCROLL_EXIT_SEQUENCE as z };
|
package/dist/node/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as parseEventsSnapshot, M as SOCKET_IO_PATH, j as parseProductEvent, k as webPanelInputSchema, n as parseDurationMs, r as TERMINAL_CAPTURE_MAX_LINES, t as assertLoopbackHost } from "../../loopback-D7k_J_Wl.js";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Command, CommanderError } from "commander";
|
|
@@ -601,6 +601,8 @@ const serviceRecordSchema = z.strictObject({
|
|
|
601
601
|
logPath: z.string().min(1),
|
|
602
602
|
apiUrl: z.string().min(1),
|
|
603
603
|
cliEntrypoint: z.string().min(1),
|
|
604
|
+
runtimeExecutable: z.string().min(1).nullable().default(null),
|
|
605
|
+
runtimeEntrypoint: z.string().min(1).nullable().default(null),
|
|
604
606
|
installationMethod: z.enum(["curl", "npm"]),
|
|
605
607
|
definitionName: z.string().min(1),
|
|
606
608
|
definitionPath: z.string().min(1),
|
|
@@ -635,7 +637,9 @@ const administratorRequestSchema = z.strictObject({
|
|
|
635
637
|
stagedDefinitionPath: z.string().min(1),
|
|
636
638
|
definitionHash: z.string().length(64),
|
|
637
639
|
apiUrl: z.string().min(1),
|
|
638
|
-
cliEntrypoint: z.string().min(1)
|
|
640
|
+
cliEntrypoint: z.string().min(1),
|
|
641
|
+
runtimeExecutable: z.string().min(1),
|
|
642
|
+
runtimeEntrypoint: z.string().min(1)
|
|
639
643
|
});
|
|
640
644
|
function managerForPlatform(platform = process.platform) {
|
|
641
645
|
return platform === "darwin" ? "launchd" : platform === "linux" ? "systemd" : null;
|
|
@@ -664,7 +668,8 @@ async function writeJson(filePath, value) {
|
|
|
664
668
|
await fs.rename(temporaryPath, filePath);
|
|
665
669
|
}
|
|
666
670
|
function fingerprint(value) {
|
|
667
|
-
const
|
|
671
|
+
const parsed = z.string().safeParse(value);
|
|
672
|
+
const source = parsed.success ? parsed.data : JSON.stringify(Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))));
|
|
668
673
|
return crypto.createHash("sha256").update(source).digest("hex");
|
|
669
674
|
}
|
|
670
675
|
function xml(value) {
|
|
@@ -673,6 +678,9 @@ function xml(value) {
|
|
|
673
678
|
function shellQuote(value) {
|
|
674
679
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
675
680
|
}
|
|
681
|
+
function createAdministratorCommand(input) {
|
|
682
|
+
return `sudo ${input.installationMethod === "curl" ? shellQuote(input.cliEntrypoint) : `${shellQuote(input.runtimeExecutable)} ${shellQuote(input.runtimeEntrypoint)}`} service apply --request ${shellQuote(input.requestPath)}`;
|
|
683
|
+
}
|
|
676
684
|
function systemdValue(value) {
|
|
677
685
|
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("%", "%%").replaceAll("\n", "\\n");
|
|
678
686
|
}
|
|
@@ -833,6 +841,23 @@ async function ensureEntrypoint(installationMethod) {
|
|
|
833
841
|
}
|
|
834
842
|
return entrypoint;
|
|
835
843
|
}
|
|
844
|
+
async function currentAdministratorRuntime() {
|
|
845
|
+
const invokedEntrypoint = process.argv[1]?.trim();
|
|
846
|
+
if (!invokedEntrypoint) throw new Error("Treeport could not identify its Node entrypoint.");
|
|
847
|
+
const runtimeEntrypoint = path.resolve(invokedEntrypoint);
|
|
848
|
+
const [runtimeExecutable, actualEntrypoint, packageBinEntrypoint, packageCliEntrypoint] = await Promise.all([
|
|
849
|
+
fs.realpath(process.execPath),
|
|
850
|
+
fs.realpath(runtimeEntrypoint),
|
|
851
|
+
fs.realpath(await resolvePackagePath("bin", "treeport.mjs")),
|
|
852
|
+
fs.realpath(await resolvePackagePath("dist", "node", "cli", "index.js"))
|
|
853
|
+
]);
|
|
854
|
+
if (actualEntrypoint !== packageBinEntrypoint && actualEntrypoint !== packageCliEntrypoint) throw new Error(`Treeport cannot use an unrecognized package entrypoint for administrator commands: ${runtimeEntrypoint}`);
|
|
855
|
+
await Promise.all([fs.access(runtimeExecutable, constants.X_OK), fs.access(runtimeEntrypoint, constants.R_OK)]);
|
|
856
|
+
return {
|
|
857
|
+
runtimeExecutable,
|
|
858
|
+
runtimeEntrypoint
|
|
859
|
+
};
|
|
860
|
+
}
|
|
836
861
|
function cacheDirectory(home, env) {
|
|
837
862
|
const configured = env.TREEPORT_CACHE_DIR?.trim();
|
|
838
863
|
if (configured) return path.resolve(configured.replace(/^~(?=\/|$)/, home));
|
|
@@ -950,9 +975,15 @@ async function managerState(record) {
|
|
|
950
975
|
}
|
|
951
976
|
function administratorCommand(record) {
|
|
952
977
|
const requestId = record.pendingAdministratorRequestId;
|
|
953
|
-
if (!requestId) return null;
|
|
978
|
+
if (!requestId || !record.runtimeExecutable || !record.runtimeEntrypoint) return null;
|
|
954
979
|
const requestPath = path.join(servicePaths({ TREEPORT_DATA_DIR: record.dataDir }).requestsDirectory, `${requestId}.json`);
|
|
955
|
-
return
|
|
980
|
+
return createAdministratorCommand({
|
|
981
|
+
installationMethod: record.installationMethod,
|
|
982
|
+
cliEntrypoint: record.cliEntrypoint,
|
|
983
|
+
runtimeExecutable: record.runtimeExecutable,
|
|
984
|
+
runtimeEntrypoint: record.runtimeEntrypoint,
|
|
985
|
+
requestPath
|
|
986
|
+
});
|
|
956
987
|
}
|
|
957
988
|
async function untrackedDefinition() {
|
|
958
989
|
const manager = managerForPlatform();
|
|
@@ -1040,16 +1071,19 @@ async function serviceStatus() {
|
|
|
1040
1071
|
};
|
|
1041
1072
|
}
|
|
1042
1073
|
const paths = servicePaths({ TREEPORT_DATA_DIR: record.dataDir });
|
|
1043
|
-
const [managerStatus, definitionContent, entrypointExists, daemon] = await Promise.all([
|
|
1074
|
+
const [managerStatus, definitionContent, entrypointExists, runtimeExecutableExists, runtimeEntrypointExists, currentRuntime, daemon] = await Promise.all([
|
|
1044
1075
|
managerState(record),
|
|
1045
1076
|
fs.readFile(record.definitionPath, "utf8").catch(() => ""),
|
|
1046
1077
|
fs.access(record.cliEntrypoint, constants.X_OK).then(() => true).catch(() => false),
|
|
1078
|
+
record.runtimeExecutable ? fs.access(record.runtimeExecutable, constants.X_OK).then(() => true).catch(() => false) : Promise.resolve(false),
|
|
1079
|
+
record.runtimeEntrypoint ? fs.access(record.runtimeEntrypoint, constants.R_OK).then(() => true).catch(() => false) : Promise.resolve(false),
|
|
1080
|
+
currentAdministratorRuntime().catch(() => null),
|
|
1047
1081
|
daemonStatus()
|
|
1048
1082
|
]);
|
|
1049
1083
|
const definitionPresent = definitionContent !== "";
|
|
1050
1084
|
const definitionMatches = definitionPresent && fingerprint(definitionContent) === record.definitionHash;
|
|
1051
1085
|
const invokedEntrypoint = currentEntrypoint();
|
|
1052
|
-
const entrypointMatches = entrypointExists && (invokedEntrypoint === null || path.resolve(invokedEntrypoint) === path.resolve(record.cliEntrypoint));
|
|
1086
|
+
const entrypointMatches = Boolean(entrypointExists && runtimeExecutableExists && runtimeEntrypointExists && currentRuntime && record.runtimeExecutable === currentRuntime.runtimeExecutable && record.runtimeEntrypoint === currentRuntime.runtimeEntrypoint && (invokedEntrypoint === null || path.resolve(invokedEntrypoint) === path.resolve(record.cliEntrypoint)));
|
|
1053
1087
|
const environmentMatches = fingerprint(createServiceEnvironment({
|
|
1054
1088
|
user: {
|
|
1055
1089
|
uid: record.uid,
|
|
@@ -1082,7 +1116,7 @@ async function serviceStatus() {
|
|
|
1082
1116
|
recoveryCommands.push("treeport service enable");
|
|
1083
1117
|
}
|
|
1084
1118
|
if (!entrypointMatches) {
|
|
1085
|
-
issues.push(`The service CLI entrypoint is unavailable or moved: ${record.cliEntrypoint}`);
|
|
1119
|
+
issues.push(`The service CLI entrypoint or Node runtime is unavailable or moved: ${record.cliEntrypoint}`);
|
|
1086
1120
|
recoveryCommands.push("treeport service enable");
|
|
1087
1121
|
}
|
|
1088
1122
|
if (!environmentMatches) {
|
|
@@ -1134,7 +1168,7 @@ async function prepareRecord() {
|
|
|
1134
1168
|
if (listener.protocol !== "http:") throw new Error("Treeport service mode requires a local HTTP loopback URL.");
|
|
1135
1169
|
assertLoopbackHost(listener.hostname);
|
|
1136
1170
|
const installationMethod = process.env.TREEPORT_INSTALLATION_METHOD?.trim() === "curl" ? "curl" : "npm";
|
|
1137
|
-
const cliEntrypoint = await ensureEntrypoint(installationMethod);
|
|
1171
|
+
const [cliEntrypoint, administratorRuntime] = await Promise.all([ensureEntrypoint(installationMethod), currentAdministratorRuntime()]);
|
|
1138
1172
|
const group = await primaryGroup(user.username);
|
|
1139
1173
|
const definitionName = manager === "launchd" ? `app.treeport.daemon.${user.uid}` : "treeport.service";
|
|
1140
1174
|
const definitionPath = manager === "launchd" ? `/Library/LaunchDaemons/${definitionName}.plist` : path.join(process.env.XDG_CONFIG_HOME?.trim() || path.join(user.homedir, ".config"), "systemd", "user", definitionName);
|
|
@@ -1163,6 +1197,8 @@ async function prepareRecord() {
|
|
|
1163
1197
|
logPath: paths.logPath,
|
|
1164
1198
|
apiUrl,
|
|
1165
1199
|
cliEntrypoint,
|
|
1200
|
+
runtimeExecutable: administratorRuntime.runtimeExecutable,
|
|
1201
|
+
runtimeEntrypoint: administratorRuntime.runtimeEntrypoint,
|
|
1166
1202
|
installationMethod,
|
|
1167
1203
|
definitionName,
|
|
1168
1204
|
definitionPath,
|
|
@@ -1207,6 +1243,14 @@ async function writeServiceFiles(record, definition) {
|
|
|
1207
1243
|
await saveRecord(record);
|
|
1208
1244
|
}
|
|
1209
1245
|
async function prepareAdministratorRequest(record, operation) {
|
|
1246
|
+
const runtime = record.runtimeExecutable && record.runtimeEntrypoint ? {
|
|
1247
|
+
runtimeExecutable: record.runtimeExecutable,
|
|
1248
|
+
runtimeEntrypoint: record.runtimeEntrypoint
|
|
1249
|
+
} : await currentAdministratorRuntime();
|
|
1250
|
+
const requestRecord = {
|
|
1251
|
+
...record,
|
|
1252
|
+
...runtime
|
|
1253
|
+
};
|
|
1210
1254
|
const locations = servicePaths({ TREEPORT_DATA_DIR: record.dataDir });
|
|
1211
1255
|
const id = crypto.randomUUID();
|
|
1212
1256
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1228,11 +1272,13 @@ async function prepareAdministratorRequest(record, operation) {
|
|
|
1228
1272
|
stagedDefinitionPath: locations.stagedDefinitionPath,
|
|
1229
1273
|
definitionHash: record.definitionHash,
|
|
1230
1274
|
apiUrl: record.apiUrl,
|
|
1231
|
-
cliEntrypoint: record.cliEntrypoint
|
|
1275
|
+
cliEntrypoint: record.cliEntrypoint,
|
|
1276
|
+
runtimeExecutable: requestRecord.runtimeExecutable,
|
|
1277
|
+
runtimeEntrypoint: requestRecord.runtimeEntrypoint
|
|
1232
1278
|
};
|
|
1233
1279
|
await writeJson(path.join(locations.requestsDirectory, `${id}.json`), request);
|
|
1234
1280
|
const next = {
|
|
1235
|
-
...
|
|
1281
|
+
...requestRecord,
|
|
1236
1282
|
pendingAdministratorRequestId: id,
|
|
1237
1283
|
updatedAt: now.toISOString()
|
|
1238
1284
|
};
|
|
@@ -1459,13 +1505,16 @@ async function serviceApply(requestPath) {
|
|
|
1459
1505
|
if (!request) throw new Error("The service apply request is invalid.");
|
|
1460
1506
|
if (metadata.uid !== request.uid) throw new Error("The service apply request owner does not match its target user.");
|
|
1461
1507
|
if (Date.parse(request.expiresAt) <= Date.now()) throw new Error("The service apply request expired. Run the original Treeport command again.");
|
|
1508
|
+
const currentRuntime = await currentAdministratorRuntime().catch(() => null);
|
|
1509
|
+
const invokedRuntimeEntrypoint = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
1510
|
+
if (!currentRuntime || currentRuntime.runtimeExecutable !== request.runtimeExecutable || currentRuntime.runtimeEntrypoint !== request.runtimeEntrypoint || invokedRuntimeEntrypoint !== request.runtimeEntrypoint) throw new Error("The service apply command did not use the approved Treeport Node runtime and package entrypoint.");
|
|
1462
1511
|
const usedPath = `${requestPath}.used`;
|
|
1463
1512
|
if (await fs.access(usedPath).then(() => true).catch(() => false)) throw new Error("The service apply request was already used.");
|
|
1464
1513
|
const account = os.userInfo({ encoding: "utf8" });
|
|
1465
1514
|
const idResult = await runCommand(await executablePath("id"), ["-u", request.username]);
|
|
1466
1515
|
if (idResult.code !== 0 || Number(idResult.stdout.trim()) !== request.uid) throw new Error("The service apply target user no longer matches the host account.");
|
|
1467
1516
|
const record = await readJson(request.serviceRecordPath, serviceRecordSchema);
|
|
1468
|
-
if (!record || record.uid !== request.uid || record.username !== request.username || record.manager !== "launchd" || record.definitionName !== request.definitionName || record.definitionPath !== request.definitionPath || record.cliEntrypoint !== request.cliEntrypoint || record.definitionHash !== request.definitionHash || record.pendingAdministratorRequestId !== request.id) throw new Error("The service apply request does not match the current Treeport service record.");
|
|
1517
|
+
if (!record || record.uid !== request.uid || record.username !== request.username || record.manager !== "launchd" || record.definitionName !== request.definitionName || record.definitionPath !== request.definitionPath || record.cliEntrypoint !== request.cliEntrypoint || record.runtimeExecutable !== request.runtimeExecutable || record.runtimeEntrypoint !== request.runtimeEntrypoint || record.definitionHash !== request.definitionHash || record.pendingAdministratorRequestId !== request.id) throw new Error("The service apply request does not match the current Treeport service record.");
|
|
1469
1518
|
if (account.uid !== 0) throw new Error("Treeport service apply lost root privileges.");
|
|
1470
1519
|
const launchctl = await executablePath("launchctl");
|
|
1471
1520
|
const target = `system/${request.definitionName}`;
|
|
@@ -1668,14 +1717,13 @@ async function request(pathname, options = {}) {
|
|
|
1668
1717
|
else externalSignal?.addEventListener("abort", abort, { once: true });
|
|
1669
1718
|
const timeout = setTimeout(abort, 9e4);
|
|
1670
1719
|
try {
|
|
1720
|
+
const headers = new Headers({ accept: "application/json" });
|
|
1721
|
+
if (options.body) headers.set("content-type", "application/json");
|
|
1722
|
+
new Headers(options.headers).forEach((value, key) => headers.set(key, value));
|
|
1671
1723
|
const response = await fetch(`${apiUrl}${pathname}`, {
|
|
1672
1724
|
...options,
|
|
1673
1725
|
signal: controller.signal,
|
|
1674
|
-
headers
|
|
1675
|
-
accept: "application/json",
|
|
1676
|
-
...options.body ? { "content-type": "application/json" } : {},
|
|
1677
|
-
...options.headers
|
|
1678
|
-
}
|
|
1726
|
+
headers
|
|
1679
1727
|
});
|
|
1680
1728
|
const body = await response.json().catch(() => ({}));
|
|
1681
1729
|
if (!response.ok) {
|
|
@@ -1700,18 +1748,18 @@ async function createWorktree(projectId, input) {
|
|
|
1700
1748
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1701
1749
|
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
|
|
1702
1750
|
}
|
|
1703
|
-
if (operation.status === "failed") throw new CliError(operation.error ?? "
|
|
1704
|
-
if (operation.kind !== "create") throw new CliError("
|
|
1705
|
-
const worktreeId =
|
|
1706
|
-
if (!worktreeId) throw new CliError("Completed
|
|
1751
|
+
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree creation failed", 5, "WORKTREE_CREATION_FAILED");
|
|
1752
|
+
if (operation.kind !== "create") throw new CliError("Tree creation returned an unexpected operation kind", 5, "INVALID_OPERATION_RESULT");
|
|
1753
|
+
const worktreeId = operation.result?.worktreeId ?? operation.worktreeId;
|
|
1754
|
+
if (!worktreeId) throw new CliError("Completed tree creation did not identify its tree", 5, "INVALID_OPERATION_RESULT");
|
|
1707
1755
|
const worktree = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project.worktrees.find((item) => item.id === worktreeId);
|
|
1708
|
-
if (!worktree) throw new CliError(`Created
|
|
1709
|
-
const terminalId =
|
|
1756
|
+
if (!worktree) throw new CliError(`Created tree ${worktreeId} was not found`, 5, "INVALID_OPERATION_RESULT");
|
|
1757
|
+
const terminalId = operation.result?.terminalId ?? null;
|
|
1710
1758
|
return {
|
|
1711
1759
|
worktree,
|
|
1712
1760
|
terminal: worktree.terminals.find((item) => item.id === terminalId) ?? null,
|
|
1713
|
-
terminalError:
|
|
1714
|
-
setupError:
|
|
1761
|
+
terminalError: operation.result?.terminalError ?? null,
|
|
1762
|
+
setupError: operation.result?.setupError ?? null
|
|
1715
1763
|
};
|
|
1716
1764
|
}
|
|
1717
1765
|
function commandArgv(args) {
|
|
@@ -1764,7 +1812,7 @@ async function resolveWorktree(identifier) {
|
|
|
1764
1812
|
}
|
|
1765
1813
|
const candidate = await canonical(identifier);
|
|
1766
1814
|
const match = all.filter((worktree) => pathContains(candidate, worktree.path)).sort((a, b) => b.path.length - a.path.length)[0];
|
|
1767
|
-
if (!match) throw new CliError(`No registered
|
|
1815
|
+
if (!match) throw new CliError(`No registered tree matches ${identifier}`, 5);
|
|
1768
1816
|
return match;
|
|
1769
1817
|
}
|
|
1770
1818
|
function parseWebPanelInput(value) {
|
|
@@ -1776,8 +1824,9 @@ function parseWebPanelInput(value) {
|
|
|
1776
1824
|
} catch (error) {
|
|
1777
1825
|
throw new CliError(`--input must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`, 2);
|
|
1778
1826
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1827
|
+
const validated = webPanelInputSchema.safeParse(parsed);
|
|
1828
|
+
if (!validated.success) throw new CliError("--input must contain a JSON object", 2);
|
|
1829
|
+
return validated.data;
|
|
1781
1830
|
}
|
|
1782
1831
|
async function webPanelDefinition(worktreeId, identifier) {
|
|
1783
1832
|
const definitions = (await request(`/api/worktrees/${encodeURIComponent(worktreeId)}/web-panel-definitions`)).definitions;
|
|
@@ -1786,11 +1835,11 @@ async function webPanelDefinition(worktreeId, identifier) {
|
|
|
1786
1835
|
const matches = definitions.filter((definition) => decodeURIComponent(definition.id.split(":").at(-1) ?? "") === identifier);
|
|
1787
1836
|
if (matches.length === 1) return matches[0];
|
|
1788
1837
|
if (matches.length > 1) throw new CliError(`Web panel name ${identifier} is ambiguous: ${matches.map((match) => match.id).join(", ")}`, 5, "WEB_PANEL_DEFINITION_AMBIGUOUS", { definitionIds: matches.map((match) => match.id) });
|
|
1789
|
-
throw new CliError(`Web panel ${identifier} is not available in this
|
|
1838
|
+
throw new CliError(`Web panel ${identifier} is not available in this tree`, 5, "WEB_PANEL_DEFINITION_NOT_FOUND");
|
|
1790
1839
|
}
|
|
1791
1840
|
async function webPanelLaunchCwd(worktree) {
|
|
1792
1841
|
const [cwd, worktreeRoot] = await Promise.all([canonical(workingDirectory), canonical(worktree.path)]);
|
|
1793
|
-
if (!pathContains(cwd, worktreeRoot)) throw new CliError(`The current directory is outside
|
|
1842
|
+
if (!pathContains(cwd, worktreeRoot)) throw new CliError(`The current directory is outside tree ${worktree.name}`, 5, "INVALID_WEB_PANEL_LAUNCH_CWD", {
|
|
1794
1843
|
cwd,
|
|
1795
1844
|
worktreeId: worktree.id,
|
|
1796
1845
|
worktreePath: worktree.path
|
|
@@ -1876,10 +1925,10 @@ async function waitForTerminal(terminalId, condition, timeoutMs) {
|
|
|
1876
1925
|
resolve(result);
|
|
1877
1926
|
}
|
|
1878
1927
|
};
|
|
1879
|
-
const fail = (
|
|
1928
|
+
const fail = (cause) => {
|
|
1880
1929
|
if (!settled) {
|
|
1881
1930
|
settled = true;
|
|
1882
|
-
reject(
|
|
1931
|
+
reject(cause);
|
|
1883
1932
|
}
|
|
1884
1933
|
};
|
|
1885
1934
|
const enqueue = (task) => {
|
|
@@ -1959,7 +2008,7 @@ const agentGuidance = `AI agents:
|
|
|
1959
2008
|
async function main(args) {
|
|
1960
2009
|
const argv = args[0] === "spawn" || args[0] === "terminal" && args[1] === "create" ? commandArgv(args) : void 0;
|
|
1961
2010
|
let parserError = "";
|
|
1962
|
-
const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects,
|
|
2011
|
+
const program = new Command().name("treeport").usage("[options] [folder] [command]").description("Manage Treeport projects, trees, and terminals.").argument("[folder]", "folder inside a Git repository to open").option("--json", "emit machine-readable JSON").addHelpText("beforeAll", agentGuidance).configureOutput({
|
|
1963
2012
|
writeOut: writeStdout,
|
|
1964
2013
|
writeErr: (value) => {
|
|
1965
2014
|
parserError += value;
|
|
@@ -1972,7 +2021,7 @@ async function main(args) {
|
|
|
1972
2021
|
}
|
|
1973
2022
|
const absoluteFolder = path.resolve(workingDirectory, folder);
|
|
1974
2023
|
if (!(await fs.stat(absoluteFolder).catch((error) => {
|
|
1975
|
-
if (
|
|
2024
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new CliError(`Folder does not exist: ${absoluteFolder}`, 5, "FOLDER_NOT_FOUND", { path: absoluteFolder });
|
|
1976
2025
|
throw new CliError(`Cannot access folder ${absoluteFolder}: ${error instanceof Error ? error.message : String(error)}`, 5, "FOLDER_UNREADABLE", { path: absoluteFolder });
|
|
1977
2026
|
})).isDirectory()) throw new CliError(`Path is not a folder: ${absoluteFolder}`, 5, "FOLDER_NOT_DIRECTORY", { path: absoluteFolder });
|
|
1978
2027
|
const canonicalFolder = await fs.realpath(absoluteFolder).catch((error) => {
|
|
@@ -2024,11 +2073,11 @@ async function main(args) {
|
|
|
2024
2073
|
return;
|
|
2025
2074
|
}
|
|
2026
2075
|
const port = options.port === void 0 ? void 0 : Number(options.port);
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2076
|
+
const daemonOptions = {};
|
|
2077
|
+
if (options.host !== void 0) daemonOptions.host = options.host;
|
|
2078
|
+
if (port !== void 0) daemonOptions.port = port;
|
|
2079
|
+
if (options.foreground !== void 0) daemonOptions.foreground = options.foreground;
|
|
2080
|
+
const result = await daemonUp(daemonOptions);
|
|
2032
2081
|
if (options.foreground) return;
|
|
2033
2082
|
print(result, () => `Treeport is running\n${result.apiUrl}`);
|
|
2034
2083
|
});
|
|
@@ -2090,10 +2139,10 @@ async function main(args) {
|
|
|
2090
2139
|
const port = options.port === void 0 ? void 0 : Number(options.port);
|
|
2091
2140
|
if (port !== void 0 && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new CliError("--port must be an integer between 1 and 65535", 2);
|
|
2092
2141
|
const serviceDaemon = lifecycle === "service" ? await ensureServiceDaemon() : void 0;
|
|
2093
|
-
const
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2142
|
+
const remoteOptions = {};
|
|
2143
|
+
if (port !== void 0) remoteOptions.port = port;
|
|
2144
|
+
if (serviceDaemon !== void 0) remoteOptions.daemon = serviceDaemon;
|
|
2145
|
+
const result = await enableTailscaleRemote(remoteOptions);
|
|
2097
2146
|
print(result, () => `Treeport remote access is ${result.alreadyEnabled ? "already enabled" : "enabled"}\n${result.url}\nTailscale authenticates each remote user. Access is limited by your Tailscale policy.`);
|
|
2098
2147
|
});
|
|
2099
2148
|
remoteCommand.command("status").description("Show Tailscale remote access status").option("--json", "emit machine-readable JSON").action(async () => {
|
|
@@ -2124,7 +2173,7 @@ async function main(args) {
|
|
|
2124
2173
|
print(result, () => {
|
|
2125
2174
|
if (!status.state) return supervision ? formatServiceStatus(supervision) : "Treeport is stopped";
|
|
2126
2175
|
if (!status.running || !status.verified) return `Treeport is unhealthy (PID ${status.state.pid})\nLogs: ${path.join(status.state.dataDir, "logs", "daemon.log")}`;
|
|
2127
|
-
return `Treeport is running\n${status.state.apiUrl}\nLifecycle: ${status.health?.daemonLifecycle}\nVersion: ${status.health?.version}\nPID: ${status.state.pid}\nProjects: ${result.projects}\
|
|
2176
|
+
return `Treeport is running\n${status.state.apiUrl}\nLifecycle: ${status.health?.daemonLifecycle}\nVersion: ${status.health?.version}\nPID: ${status.state.pid}\nProjects: ${result.projects}\nTrees: ${result.worktrees}\nTerminals: ${result.terminals}`;
|
|
2128
2177
|
});
|
|
2129
2178
|
});
|
|
2130
2179
|
const logsCommand = program.command("logs").description("Show recent daemon logs").option("--lines <count>", "number of lines", "100");
|
|
@@ -2174,12 +2223,12 @@ async function main(args) {
|
|
|
2174
2223
|
if (missing.length) throw new CliError(`Incomplete Treeport context; missing ${missing.join(", ")}`, 5, "TREEPORT_CONTEXT_INCOMPLETE", { missing });
|
|
2175
2224
|
const project = (await request(`/api/projects/${encodeURIComponent(projectId)}`)).project;
|
|
2176
2225
|
const worktree = project.worktrees.find((candidate) => candidate.id === worktreeId);
|
|
2177
|
-
if (!worktree) throw new CliError("Treeport context
|
|
2226
|
+
if (!worktree) throw new CliError("Treeport context tree does not belong to the current project", 5, "TREEPORT_CONTEXT_INVALID", {
|
|
2178
2227
|
projectId,
|
|
2179
2228
|
worktreeId
|
|
2180
2229
|
});
|
|
2181
2230
|
const terminal = worktree.terminals.find((candidate) => candidate.id === terminalId);
|
|
2182
|
-
if (!terminal) throw new CliError("Treeport context terminal does not belong to the current
|
|
2231
|
+
if (!terminal) throw new CliError("Treeport context terminal does not belong to the current tree", 5, "TREEPORT_CONTEXT_INVALID", {
|
|
2183
2232
|
worktreeId,
|
|
2184
2233
|
terminalId
|
|
2185
2234
|
});
|
|
@@ -2213,29 +2262,27 @@ async function main(args) {
|
|
|
2213
2262
|
exitCode: terminal.exitCode
|
|
2214
2263
|
}
|
|
2215
2264
|
};
|
|
2216
|
-
print(context, () => `Treeport context\n\nProject: ${context.project.name} (${context.project.id})\
|
|
2265
|
+
print(context, () => `Treeport context\n\nProject: ${context.project.name} (${context.project.id})\nTree: ${context.worktree.name} (${context.worktree.id})\nPath: ${context.worktree.path}\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"}`);
|
|
2217
2266
|
});
|
|
2218
2267
|
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");
|
|
2219
2268
|
installCommand.action(async (source) => {
|
|
2220
2269
|
const options = installCommand.opts();
|
|
2270
|
+
const body = { source: await packageSource(source) };
|
|
2271
|
+
if (options.local) body.projectId = await localPackageProjectId();
|
|
2221
2272
|
const result = (await request("/api/packages/install", {
|
|
2222
2273
|
method: "POST",
|
|
2223
|
-
body: JSON.stringify(
|
|
2224
|
-
source: await packageSource(source),
|
|
2225
|
-
...options.local ? { projectId: await localPackageProjectId() } : {}
|
|
2226
|
-
})
|
|
2274
|
+
body: JSON.stringify(body)
|
|
2227
2275
|
})).result;
|
|
2228
2276
|
print(result, () => `Installed ${result.source}${result.scope === "project" ? ` for project ${result.projectId}` : " globally"}`);
|
|
2229
2277
|
});
|
|
2230
2278
|
const removePackageCommand = program.command("remove").alias("uninstall").description("Remove a configured Treeport package").argument("<source>", "npm: source or local directory").option("-l, --local", "remove from the registered project containing the current directory").option("--json", "emit machine-readable JSON");
|
|
2231
2279
|
removePackageCommand.action(async (source) => {
|
|
2232
2280
|
const options = removePackageCommand.opts();
|
|
2281
|
+
const body = { source: await packageSource(source) };
|
|
2282
|
+
if (options.local) body.projectId = await localPackageProjectId();
|
|
2233
2283
|
const result = (await request("/api/packages/remove", {
|
|
2234
2284
|
method: "POST",
|
|
2235
|
-
body: JSON.stringify(
|
|
2236
|
-
source: await packageSource(source),
|
|
2237
|
-
...options.local ? { projectId: await localPackageProjectId() } : {}
|
|
2238
|
-
})
|
|
2285
|
+
body: JSON.stringify(body)
|
|
2239
2286
|
})).result;
|
|
2240
2287
|
print(result, () => `Removed ${result.source}`);
|
|
2241
2288
|
});
|
|
@@ -2285,29 +2332,30 @@ async function main(args) {
|
|
|
2285
2332
|
const list = await projects();
|
|
2286
2333
|
print(list, () => list.map((project) => `${project.id}\t${project.name}\t${project.repositoryPath}`).join("\n"));
|
|
2287
2334
|
});
|
|
2288
|
-
const worktreeCommand = program.command("worktree").description("List, create, and remove
|
|
2335
|
+
const worktreeCommand = program.command("worktree").description("List, create, and remove trees");
|
|
2289
2336
|
worktreeCommand.action(() => {
|
|
2290
2337
|
throw new CliError(worktreeCommand.helpInformation(), 2);
|
|
2291
2338
|
});
|
|
2292
|
-
const worktreeListCommand = worktreeCommand.command("list").description("List discovered
|
|
2339
|
+
const worktreeListCommand = worktreeCommand.command("list").description("List discovered trees").option("--project <id-or-path>", "limit results to a project").option("--json", "emit machine-readable JSON");
|
|
2293
2340
|
worktreeListCommand.action(async () => {
|
|
2294
2341
|
const { project: projectIdentifier } = worktreeListCommand.opts();
|
|
2295
2342
|
const list = projectIdentifier ? (await resolveProject(projectIdentifier)).worktrees : (await projects()).flatMap((project) => project.worktrees);
|
|
2296
2343
|
print(list, () => list.map((worktree) => `${worktree.id}\t${worktree.name}\t${worktree.branch ?? `detached@${worktree.head.slice(0, 8)}`}\t${worktree.path}`).join("\n"));
|
|
2297
2344
|
});
|
|
2298
|
-
const worktreeCreateCommand = worktreeCommand.command("create").description("Create a linked
|
|
2345
|
+
const worktreeCreateCommand = worktreeCommand.command("create").description("Create a linked tree").requiredOption("--project <id-or-path>", "project to create from").requiredOption("--name <name>", "Tree name").option("--from-current", "base the tree on the current tree").option("--json", "emit machine-readable JSON");
|
|
2299
2346
|
worktreeCreateCommand.action(async () => {
|
|
2300
2347
|
const options = worktreeCreateCommand.opts();
|
|
2301
2348
|
const project = await resolveProject(options.project);
|
|
2302
2349
|
const sourceWorktreeId = options.fromCurrent ? (await resolveWorktree(".")).id : void 0;
|
|
2303
|
-
const
|
|
2350
|
+
const request = {
|
|
2304
2351
|
name: options.name,
|
|
2305
|
-
base: options.fromCurrent ? "current" : "default"
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2352
|
+
base: options.fromCurrent ? "current" : "default"
|
|
2353
|
+
};
|
|
2354
|
+
if (sourceWorktreeId) request.sourceWorktreeId = sourceWorktreeId;
|
|
2355
|
+
const result = await createWorktree(project.id, request);
|
|
2356
|
+
print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\n${result.worktree.path}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}`);
|
|
2309
2357
|
});
|
|
2310
|
-
const worktreeRemoveCommand = worktreeCommand.command("remove").description("Remove a linked
|
|
2358
|
+
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");
|
|
2311
2359
|
worktreeRemoveCommand.action(async (identifier) => {
|
|
2312
2360
|
const { force: confirmed } = worktreeRemoveCommand.opts();
|
|
2313
2361
|
const worktree = await resolveWorktree(identifier);
|
|
@@ -2325,18 +2373,18 @@ async function main(args) {
|
|
|
2325
2373
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2326
2374
|
operation = (await request(`/api/operations/${encodeURIComponent(operation.id)}`)).operation;
|
|
2327
2375
|
}
|
|
2328
|
-
if (operation.status === "failed") throw new CliError(operation.error ?? "
|
|
2329
|
-
if (operation.kind !== "remove" || !operation.result) throw new CliError("
|
|
2376
|
+
if (operation.status === "failed") throw new CliError(operation.error ?? "Tree removal failed", 5, "WORKTREE_REMOVAL_FAILED");
|
|
2377
|
+
if (operation.kind !== "remove" || !operation.result) throw new CliError("Tree removal returned an unexpected operation result", 5, "INVALID_OPERATION_RESULT");
|
|
2330
2378
|
print(operation.result, () => {
|
|
2331
2379
|
const warning = operation.result?.cleanup.warning;
|
|
2332
|
-
return `Removed ${worktree.name} (${worktree.id})${warning ? `\nWarning: ${warning}` : ""}`;
|
|
2380
|
+
return `Removed tree ${worktree.name} (${worktree.id})${warning ? `\nWarning: ${warning}` : ""}`;
|
|
2333
2381
|
});
|
|
2334
2382
|
});
|
|
2335
2383
|
const webPanelCommand = program.command("web-panel").description("Open persistent web panels");
|
|
2336
2384
|
webPanelCommand.action(() => {
|
|
2337
2385
|
throw new CliError(webPanelCommand.helpInformation(), 2);
|
|
2338
2386
|
});
|
|
2339
|
-
const webPanelOpenCommand = webPanelCommand.command("open").description("Create or reuse a web panel and request client navigation").argument("<definition>", "definition ID or unique short name").requiredOption("--worktree <id-or-path-or-dot>", "owning
|
|
2387
|
+
const webPanelOpenCommand = webPanelCommand.command("open").description("Create or reuse a web panel and request client navigation").argument("<definition>", "definition ID or unique short name").requiredOption("--worktree <id-or-path-or-dot>", "owning tree").option("--input <json>", "structured panel input as a JSON object").option("--new", "create a separate panel instance").option("--json", "emit machine-readable JSON");
|
|
2340
2388
|
webPanelOpenCommand.action(async (identifier) => {
|
|
2341
2389
|
const options = webPanelOpenCommand.opts();
|
|
2342
2390
|
const worktree = await resolveWorktree(options.worktree);
|
|
@@ -2357,25 +2405,25 @@ async function main(args) {
|
|
|
2357
2405
|
};
|
|
2358
2406
|
print(output, () => `${result.reused ? "Reused" : "Opened"} ${result.panel.title} (${result.panel.id})\n${output.url}`);
|
|
2359
2407
|
});
|
|
2360
|
-
const terminalCommand = program.command("terminal").description("Manage persistent
|
|
2408
|
+
const terminalCommand = program.command("terminal").description("Manage persistent tree terminals");
|
|
2361
2409
|
terminalCommand.action(() => {
|
|
2362
2410
|
throw new CliError(terminalCommand.helpInformation(), 2);
|
|
2363
2411
|
});
|
|
2364
|
-
const terminalListCommand = terminalCommand.command("list").description("List terminals").option("--worktree <id-or-path>", "limit results to a
|
|
2412
|
+
const terminalListCommand = terminalCommand.command("list").description("List terminals").option("--worktree <id-or-path>", "limit results to a tree").option("--json", "emit machine-readable JSON");
|
|
2365
2413
|
terminalListCommand.action(async () => {
|
|
2366
2414
|
const { worktree: identifier } = terminalListCommand.opts();
|
|
2367
2415
|
const list = identifier ? (await resolveWorktree(identifier)).terminals : (await projects()).flatMap((project) => project.worktrees.flatMap((worktree) => worktree.terminals));
|
|
2368
2416
|
print(list, () => list.map((terminal) => `${terminal.id}\t${terminal.name}\t${terminal.status}\t${JSON.stringify(terminal.argv)}`).join("\n"));
|
|
2369
2417
|
});
|
|
2370
|
-
const terminalCreateCommand = terminalCommand.command("create").description("Create a persistent terminal").usage("[options] [-- <command> args...]").requiredOption("--worktree <id-or-path-or-dot>", "owning
|
|
2418
|
+
const terminalCreateCommand = terminalCommand.command("create").description("Create a persistent terminal").usage("[options] [-- <command> args...]").requiredOption("--worktree <id-or-path-or-dot>", "owning tree").requiredOption("--name <name>", "terminal name").option("--json", "emit machine-readable JSON").addHelpText("after", "\nCommand arguments may be passed after --.\n");
|
|
2371
2419
|
terminalCreateCommand.action(async () => {
|
|
2372
2420
|
const options = terminalCreateCommand.opts();
|
|
2373
|
-
const
|
|
2421
|
+
const worktree = await resolveWorktree(options.worktree);
|
|
2422
|
+
const body = { name: options.name };
|
|
2423
|
+
if (argv) body.argv = argv;
|
|
2424
|
+
const result = await request(`/api/worktrees/${worktree.id}/terminals`, {
|
|
2374
2425
|
method: "POST",
|
|
2375
|
-
body: JSON.stringify(
|
|
2376
|
-
name: options.name,
|
|
2377
|
-
...argv ? { argv } : {}
|
|
2378
|
-
})
|
|
2426
|
+
body: JSON.stringify(body)
|
|
2379
2427
|
});
|
|
2380
2428
|
print(result.terminal, () => `Created ${result.terminal.name} (${result.terminal.id})`);
|
|
2381
2429
|
});
|
|
@@ -2419,21 +2467,21 @@ async function main(args) {
|
|
|
2419
2467
|
terminalId
|
|
2420
2468
|
}, () => `Deleted ${terminalId}`);
|
|
2421
2469
|
});
|
|
2422
|
-
const spawnCommand = program.command("spawn").description("Create a
|
|
2470
|
+
const spawnCommand = program.command("spawn").description("Create a tree and its first terminal").usage("[options] [-- <command> args...]").requiredOption("--project <id-or-path-or-dot>", "project to create from").requiredOption("--worktree-name <name>", "Tree name").requiredOption("--name <terminal-name>", "terminal name").option("--from-current", "base the tree on the current tree").option("--json", "emit machine-readable JSON").addHelpText("after", "\nCommand arguments may be passed after --.\n");
|
|
2423
2471
|
spawnCommand.action(async () => {
|
|
2424
2472
|
const options = spawnCommand.opts();
|
|
2425
2473
|
const project = await resolveProject(options.project);
|
|
2426
2474
|
const sourceWorktreeId = options.fromCurrent ? (await resolveWorktree(".")).id : void 0;
|
|
2427
|
-
const
|
|
2475
|
+
const initialTerminal = { name: options.name };
|
|
2476
|
+
if (argv) initialTerminal.argv = argv;
|
|
2477
|
+
const request = {
|
|
2428
2478
|
name: options.worktreeName,
|
|
2429
2479
|
base: options.fromCurrent ? "current" : "default",
|
|
2430
|
-
initialTerminal
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
});
|
|
2436
|
-
print(result, () => `Created worktree ${result.worktree.name} (${result.worktree.id})\nPath: ${result.worktree.path}\n${result.terminal ? `Terminal: ${result.terminal.name} (${result.terminal.id}) — ${result.terminal.status}` : "Terminal: not created"}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}${result.terminalError ? `\nTerminal error: ${result.terminalError}` : ""}`);
|
|
2480
|
+
initialTerminal
|
|
2481
|
+
};
|
|
2482
|
+
if (sourceWorktreeId) request.sourceWorktreeId = sourceWorktreeId;
|
|
2483
|
+
const result = await createWorktree(project.id, request);
|
|
2484
|
+
print(result, () => `Created tree ${result.worktree.name} (${result.worktree.id})\nPath: ${result.worktree.path}\n${result.terminal ? `Terminal: ${result.terminal.name} (${result.terminal.id}) — ${result.terminal.status}` : "Terminal: not created"}${result.setupError ? `\nSetup error: ${result.setupError}` : ""}${result.terminalError ? `\nTerminal error: ${result.terminalError}` : ""}`);
|
|
2437
2485
|
});
|
|
2438
2486
|
try {
|
|
2439
2487
|
await program.parseAsync(args, { from: "user" });
|
|
@@ -2465,9 +2513,9 @@ async function runCliApplication(options) {
|
|
|
2465
2513
|
if (jsonOutput) {
|
|
2466
2514
|
const body = { error: {
|
|
2467
2515
|
code: cliError.code,
|
|
2468
|
-
message: cliError.message
|
|
2469
|
-
...cliError.details === void 0 ? {} : { details: cliError.details }
|
|
2516
|
+
message: cliError.message
|
|
2470
2517
|
} };
|
|
2518
|
+
if (cliError.details !== void 0) body.error.details = cliError.details;
|
|
2471
2519
|
writeStderr(`${JSON.stringify(body)}\n`);
|
|
2472
2520
|
} else writeStderr(`${cliError.message}\n`);
|
|
2473
2521
|
requestedExitCode = cliError.exitCode;
|