ghostdock 0.0.16 → 0.0.18
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/dist/cli-reference.md +21 -2
- package/dist/cli.md +83 -4
- package/dist/index.js +2391 -1568
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -863,6 +863,14 @@ var GENERATED_TOOL_CATALOG = [
|
|
|
863
863
|
description: "Mark the sandbox ephemeral: every stop destroys the machine and the next start boots fresh, while the record, URLs, and settings stay. Omit to use the image label ghostdock.ephemeral, or false when that label is absent. Does not change idle-stop or auto-delete.",
|
|
864
864
|
type: "boolean"
|
|
865
865
|
},
|
|
866
|
+
imageBase: {
|
|
867
|
+
description: "How the disk is made for one of Ghostdock's VM images: auto (default) clones the host's image base, a disk with the image already on it, when there is one; off always starts from an empty disk that pulls the image. For tests and comparisons; other images always start empty.",
|
|
868
|
+
type: "string",
|
|
869
|
+
enum: [
|
|
870
|
+
"auto",
|
|
871
|
+
"off"
|
|
872
|
+
]
|
|
873
|
+
},
|
|
866
874
|
urls: {
|
|
867
875
|
description: "Changes to the image’s or snapshot’s URLs, keyed by port. Omit for the image URLs as they are; otherwise you get the image URLs plus yours: an entry on the same port replaces that URL completely (omitted fields take their defaults), and { port, remove: true } drops it; removing a port the image does not declare does nothing. A required port cannot be removed. TCP URLs are always public.",
|
|
868
876
|
maxItems: 100,
|
|
@@ -9175,8 +9183,25 @@ function getCliName(argv = process.argv) {
|
|
|
9175
9183
|
const invoked = basename(entry);
|
|
9176
9184
|
return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
|
|
9177
9185
|
}
|
|
9186
|
+
|
|
9187
|
+
// src/commands/docker.ts
|
|
9188
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
9189
|
+
|
|
9190
|
+
// ../../shared/lib/private-network.ts
|
|
9191
|
+
var ALL_BITS = (1n << 128n) - 1n;
|
|
9192
|
+
|
|
9193
|
+
// ../../shared/lib/docker-access.ts
|
|
9194
|
+
var DOCKER_CERTIFICATE_RENEW_DAYS = 10;
|
|
9195
|
+
var DOCKER_CLIENT_URI_PREFIX = {
|
|
9196
|
+
user: "urn:ghostdock:user:",
|
|
9197
|
+
device: "urn:ghostdock:device:",
|
|
9198
|
+
organization: "urn:ghostdock:org:"
|
|
9199
|
+
};
|
|
9200
|
+
function dockerContextName(orgSlug, sandboxName) {
|
|
9201
|
+
return `ghostdock-${orgSlug}-${sandboxName}`;
|
|
9202
|
+
}
|
|
9178
9203
|
// package.json
|
|
9179
|
-
var version = "0.0.
|
|
9204
|
+
var version = "0.0.18";
|
|
9180
9205
|
var package_default = {
|
|
9181
9206
|
name: "ghostdock",
|
|
9182
9207
|
description: "Ghostdock CLI — device login and agent tool invocation from the command line",
|
|
@@ -9284,6 +9309,380 @@ async function clearCredentials(path = getCredentialsPath()) {
|
|
|
9284
9309
|
await rm(path, { force: true });
|
|
9285
9310
|
}
|
|
9286
9311
|
|
|
9312
|
+
// ../../shared/lib/private-network-pause.ts
|
|
9313
|
+
function formatPauseLimit(seconds) {
|
|
9314
|
+
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
9315
|
+
if (minutes >= 2 * 1440 && minutes % 1440 === 0)
|
|
9316
|
+
return `${minutes / 1440} days`;
|
|
9317
|
+
const hours = Math.floor(minutes / 60);
|
|
9318
|
+
const rest = minutes % 60;
|
|
9319
|
+
if (hours === 0)
|
|
9320
|
+
return `${rest} min`;
|
|
9321
|
+
return rest === 0 ? `${hours} h` : `${hours} h ${rest} min`;
|
|
9322
|
+
}
|
|
9323
|
+
function devicePauseReasonText(reason, limits) {
|
|
9324
|
+
switch (reason) {
|
|
9325
|
+
case "idle":
|
|
9326
|
+
return `idle ${formatPauseLimit(limits.idleTimeoutSeconds)}`;
|
|
9327
|
+
case "max_session":
|
|
9328
|
+
return `${formatPauseLimit(limits.maxSessionSeconds)} session limit`;
|
|
9329
|
+
case "manual":
|
|
9330
|
+
return "paused in the web app";
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
// src/output.ts
|
|
9335
|
+
var SYMBOLS = {
|
|
9336
|
+
ready: "⚓",
|
|
9337
|
+
done: "✓",
|
|
9338
|
+
url: "↗",
|
|
9339
|
+
stopped: "◌",
|
|
9340
|
+
failed: "✗",
|
|
9341
|
+
progress: "▂▄▆",
|
|
9342
|
+
item: "•"
|
|
9343
|
+
};
|
|
9344
|
+
var jsonOutput = false;
|
|
9345
|
+
function setJsonOutput(on) {
|
|
9346
|
+
jsonOutput = on;
|
|
9347
|
+
}
|
|
9348
|
+
function isJsonOutput() {
|
|
9349
|
+
return jsonOutput;
|
|
9350
|
+
}
|
|
9351
|
+
function colorEnabled(stream, env = process.env) {
|
|
9352
|
+
if (env.FORCE_COLOR !== undefined)
|
|
9353
|
+
return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
|
|
9354
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
|
|
9355
|
+
return false;
|
|
9356
|
+
if (env.TERM === "dumb")
|
|
9357
|
+
return false;
|
|
9358
|
+
return stream.isTTY === true;
|
|
9359
|
+
}
|
|
9360
|
+
function trueColor(env = process.env) {
|
|
9361
|
+
return env.COLORTERM === "truecolor" || env.COLORTERM === "24bit";
|
|
9362
|
+
}
|
|
9363
|
+
function accent(text, enabled) {
|
|
9364
|
+
if (!enabled)
|
|
9365
|
+
return text;
|
|
9366
|
+
const open = trueColor() ? "\x1B[38;2;124;255;203m" : "\x1B[38;5;122m";
|
|
9367
|
+
return `${open}${text}\x1B[39m`;
|
|
9368
|
+
}
|
|
9369
|
+
function red(text, enabled) {
|
|
9370
|
+
return enabled ? `\x1B[31m${text}\x1B[39m` : text;
|
|
9371
|
+
}
|
|
9372
|
+
function dim(text, enabled) {
|
|
9373
|
+
return enabled ? `\x1B[2m${text}\x1B[22m` : text;
|
|
9374
|
+
}
|
|
9375
|
+
function eventLine(symbol, message, color = colorEnabled(process.stdout)) {
|
|
9376
|
+
return ` ${accent(SYMBOLS[symbol], color)} ${message}`;
|
|
9377
|
+
}
|
|
9378
|
+
function errorLine(message, color = colorEnabled(process.stderr)) {
|
|
9379
|
+
return red(` ${SYMBOLS.failed} ${message}`, color);
|
|
9380
|
+
}
|
|
9381
|
+
function printNotice(message) {
|
|
9382
|
+
process.stderr.write(`${message}
|
|
9383
|
+
`);
|
|
9384
|
+
}
|
|
9385
|
+
function printError(message) {
|
|
9386
|
+
process.stderr.write(`${errorLine(message)}
|
|
9387
|
+
`);
|
|
9388
|
+
}
|
|
9389
|
+
function printResult(data, human, log = console.log) {
|
|
9390
|
+
if (jsonOutput) {
|
|
9391
|
+
log(JSON.stringify(data, null, 2));
|
|
9392
|
+
return;
|
|
9393
|
+
}
|
|
9394
|
+
for (const line of human())
|
|
9395
|
+
log(line);
|
|
9396
|
+
}
|
|
9397
|
+
|
|
9398
|
+
// src/network/pause.ts
|
|
9399
|
+
var DEVICE_HEADER = "ghostdock-device";
|
|
9400
|
+
var DEVICE_STATE_HEADER = "ghostdock-device-state";
|
|
9401
|
+
function parseDeviceStateHeader(value) {
|
|
9402
|
+
if (!value)
|
|
9403
|
+
return null;
|
|
9404
|
+
const [state, ...params] = value.split(";").map((part) => part.trim());
|
|
9405
|
+
if (state !== "paused")
|
|
9406
|
+
return null;
|
|
9407
|
+
const fields = new Map(params.map((param) => {
|
|
9408
|
+
const [key, ...rest] = param.split("=");
|
|
9409
|
+
return [key?.trim() ?? "", rest.join("=").trim()];
|
|
9410
|
+
}));
|
|
9411
|
+
const reason = fields.get("reason");
|
|
9412
|
+
if (reason !== "idle" && reason !== "max_session" && reason !== "manual")
|
|
9413
|
+
return null;
|
|
9414
|
+
const limit = Number(fields.get("limit"));
|
|
9415
|
+
return { reason, limitSeconds: Number.isInteger(limit) && limit > 0 ? limit : null };
|
|
9416
|
+
}
|
|
9417
|
+
function pausedNotice(paused) {
|
|
9418
|
+
const limit = paused.limitSeconds ?? 0;
|
|
9419
|
+
const why = paused.reason === "manual" || limit > 0 ? ` (${devicePauseReasonText(paused.reason, { idleTimeoutSeconds: limit, maxSessionSeconds: limit })})` : "";
|
|
9420
|
+
return `Network access for this device is paused${why}. Run: ${getCliName()} network connect`;
|
|
9421
|
+
}
|
|
9422
|
+
var noticeShown = false;
|
|
9423
|
+
var noticeSuppressed = false;
|
|
9424
|
+
function notePausedDevice(header, write = (line) => process.stderr.write(`${line}
|
|
9425
|
+
`)) {
|
|
9426
|
+
if (noticeShown || noticeSuppressed)
|
|
9427
|
+
return;
|
|
9428
|
+
const paused = parseDeviceStateHeader(header);
|
|
9429
|
+
if (!paused)
|
|
9430
|
+
return;
|
|
9431
|
+
noticeShown = true;
|
|
9432
|
+
write(`! ${pausedNotice(paused)}`);
|
|
9433
|
+
}
|
|
9434
|
+
function suppressPausedNotice() {
|
|
9435
|
+
noticeSuppressed = true;
|
|
9436
|
+
}
|
|
9437
|
+
function clockTime(iso, now = new Date) {
|
|
9438
|
+
const at = new Date(iso);
|
|
9439
|
+
const time = at.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
9440
|
+
if (at.toDateString() === now.toDateString())
|
|
9441
|
+
return time;
|
|
9442
|
+
return `${at.toLocaleDateString("en-GB", { weekday: "short" })} ${time}`;
|
|
9443
|
+
}
|
|
9444
|
+
function setBy(slug) {
|
|
9445
|
+
return slug ? `, set by ${slug}` : "";
|
|
9446
|
+
}
|
|
9447
|
+
function pauseHint(state, now = new Date) {
|
|
9448
|
+
const idle = `after ${formatPauseLimit(state.idleTimeoutSeconds)} without traffic${setBy(state.setBy.idle)}`;
|
|
9449
|
+
const sessionEnd = sessionEndsAt(state, now);
|
|
9450
|
+
const session = `${formatPauseLimit(state.maxSessionSeconds)} after connecting${sessionEnd ? ` (at ${clockTime(sessionEnd, now)})` : ""}${setBy(state.setBy.maxSession)}`;
|
|
9451
|
+
return [
|
|
9452
|
+
"ℹ For security, this device's network access pauses automatically:",
|
|
9453
|
+
` ${idle}, and ${session}.`,
|
|
9454
|
+
` Resume any time: ${getCliName()} network connect, or Reconnect in the web app.`
|
|
9455
|
+
];
|
|
9456
|
+
}
|
|
9457
|
+
function sessionEndsAt(state, now) {
|
|
9458
|
+
if (state.status !== "active" || !state.pausesAt)
|
|
9459
|
+
return null;
|
|
9460
|
+
if (state.pauseCause === "max_session")
|
|
9461
|
+
return state.pausesAt;
|
|
9462
|
+
return new Date(now.getTime() + state.maxSessionSeconds * 1000).toISOString();
|
|
9463
|
+
}
|
|
9464
|
+
function bold2(text, enabled = colorEnabled(process.stderr)) {
|
|
9465
|
+
return enabled ? `\x1B[1m${text}\x1B[22m` : text;
|
|
9466
|
+
}
|
|
9467
|
+
function deviceStateText(state, now = new Date) {
|
|
9468
|
+
if (!state)
|
|
9469
|
+
return "Not connected (logged out)";
|
|
9470
|
+
if (state.status === "paused" && state.pauseReason) {
|
|
9471
|
+
const text = state.pauseReason === "manual" ? "Paused by you" : `Paused: ${devicePauseReasonText(state.pauseReason, state)}`;
|
|
9472
|
+
if (state.connected)
|
|
9473
|
+
return text;
|
|
9474
|
+
return state.connecting ? `${text}, connecting` : `Not connected (${text.toLowerCase()})`;
|
|
9475
|
+
}
|
|
9476
|
+
const left = state.pausesAt ? new Date(state.pausesAt).getTime() - now.getTime() : 0;
|
|
9477
|
+
const active = `Active, pauses in ${formatRemaining(Math.max(left, 0) / 1000)}`;
|
|
9478
|
+
if (state.connected)
|
|
9479
|
+
return active;
|
|
9480
|
+
return state.connecting ? `${active}, connecting` : `Not connected (armed, pauses in ${formatRemaining(Math.max(left, 0) / 1000)})`;
|
|
9481
|
+
}
|
|
9482
|
+
function formatRemaining(seconds) {
|
|
9483
|
+
const minutes = Math.floor(seconds / 60);
|
|
9484
|
+
const days = Math.floor(minutes / 1440);
|
|
9485
|
+
const hours = Math.floor(minutes % 1440 / 60);
|
|
9486
|
+
const rest = minutes % 60;
|
|
9487
|
+
const dayText = days === 1 ? "1 day" : `${days} days`;
|
|
9488
|
+
if (days > 0)
|
|
9489
|
+
return hours ? `${dayText} ${hours} h` : dayText;
|
|
9490
|
+
if (hours > 0)
|
|
9491
|
+
return rest ? `${hours} h ${rest} min` : `${hours} h`;
|
|
9492
|
+
return `${Math.max(rest, 1)} min`;
|
|
9493
|
+
}
|
|
9494
|
+
|
|
9495
|
+
// src/network/tunnel.ts
|
|
9496
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
9497
|
+
import { existsSync } from "node:fs";
|
|
9498
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
9499
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
9500
|
+
var INTERFACE = "gdock";
|
|
9501
|
+
function networkDir() {
|
|
9502
|
+
return join2(getConfigDir(), "network");
|
|
9503
|
+
}
|
|
9504
|
+
function configPath(dir = networkDir()) {
|
|
9505
|
+
return join2(dir, `${INTERFACE}.conf`);
|
|
9506
|
+
}
|
|
9507
|
+
function keyPath(dir) {
|
|
9508
|
+
return join2(dir, "private.key");
|
|
9509
|
+
}
|
|
9510
|
+
function statePath(dir) {
|
|
9511
|
+
return join2(dir, "device.json");
|
|
9512
|
+
}
|
|
9513
|
+
function isProvider(value) {
|
|
9514
|
+
const provider = value;
|
|
9515
|
+
return typeof provider?.id === "string" && typeof provider.name === "string";
|
|
9516
|
+
}
|
|
9517
|
+
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
9518
|
+
function keyPairFromPrivate(privateKey) {
|
|
9519
|
+
const raw = Buffer.from(privateKey, "base64");
|
|
9520
|
+
if (raw.length !== 32)
|
|
9521
|
+
throw new Error("The stored WireGuard key is not 32 bytes.");
|
|
9522
|
+
const key = createPrivateKey({
|
|
9523
|
+
key: Buffer.concat([X25519_PKCS8_PREFIX, raw]),
|
|
9524
|
+
format: "der",
|
|
9525
|
+
type: "pkcs8"
|
|
9526
|
+
});
|
|
9527
|
+
const spki = createPublicKey(key).export({ format: "der", type: "spki" });
|
|
9528
|
+
return { privateKey, publicKey: spki.subarray(spki.length - 32).toString("base64") };
|
|
9529
|
+
}
|
|
9530
|
+
async function loadOrCreateKey(dir = networkDir()) {
|
|
9531
|
+
try {
|
|
9532
|
+
return keyPairFromPrivate((await readFile2(keyPath(dir), "utf8")).trim());
|
|
9533
|
+
} catch (err) {
|
|
9534
|
+
if (err.code !== "ENOENT")
|
|
9535
|
+
throw err;
|
|
9536
|
+
}
|
|
9537
|
+
const { privateKey } = generateKeyPairSync("x25519");
|
|
9538
|
+
const der = privateKey.export({ format: "der", type: "pkcs8" });
|
|
9539
|
+
const raw = der.subarray(der.length - 32).toString("base64");
|
|
9540
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
9541
|
+
await writeFile2(keyPath(dir), `${raw}
|
|
9542
|
+
`, { mode: 384 });
|
|
9543
|
+
return keyPairFromPrivate(raw);
|
|
9544
|
+
}
|
|
9545
|
+
async function readDeviceState(dir = networkDir()) {
|
|
9546
|
+
try {
|
|
9547
|
+
const parsed = JSON.parse(await readFile2(statePath(dir), "utf8"));
|
|
9548
|
+
if (typeof parsed.deviceId === "string" && typeof parsed.baseUrl === "string") {
|
|
9549
|
+
return {
|
|
9550
|
+
baseUrl: parsed.baseUrl,
|
|
9551
|
+
deviceId: parsed.deviceId,
|
|
9552
|
+
address: typeof parsed.address === "string" ? parsed.address : "",
|
|
9553
|
+
gatewayProvider: isProvider(parsed.gatewayProvider) ? { id: parsed.gatewayProvider.id, name: parsed.gatewayProvider.name } : null,
|
|
9554
|
+
zones: Array.isArray(parsed.zones) ? parsed.zones.filter((zone) => typeof zone === "string" && isDnsZone(zone)) : []
|
|
9555
|
+
};
|
|
9556
|
+
}
|
|
9557
|
+
} catch {}
|
|
9558
|
+
return null;
|
|
9559
|
+
}
|
|
9560
|
+
async function writeDeviceState(state, dir = networkDir()) {
|
|
9561
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
9562
|
+
await writeFile2(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
9563
|
+
`, { mode: 384 });
|
|
9564
|
+
}
|
|
9565
|
+
function detectDnsMode(platform = process.platform, exists = existsSync) {
|
|
9566
|
+
if (platform === "darwin")
|
|
9567
|
+
return "macos-resolver";
|
|
9568
|
+
if (platform === "linux" && exists("/run/systemd/resolve"))
|
|
9569
|
+
return "resolved";
|
|
9570
|
+
return "none";
|
|
9571
|
+
}
|
|
9572
|
+
function renderConfig(input) {
|
|
9573
|
+
const { device, network, gateway } = input.registered;
|
|
9574
|
+
const zones = [network.zone, ...network.netZone ? [network.netZone] : []];
|
|
9575
|
+
for (const zone of zones) {
|
|
9576
|
+
if (!isDnsZone(zone))
|
|
9577
|
+
throw new Error(`The server sent a zone that is not a DNS name: ${zone}`);
|
|
9578
|
+
}
|
|
9579
|
+
if (!device.address || !isIPv6Address(device.address)) {
|
|
9580
|
+
throw new Error(`The server sent no IPv6 address for this device: ${device.address}`);
|
|
9581
|
+
}
|
|
9582
|
+
if (!isIPv6Address(network.gatewayAddress)) {
|
|
9583
|
+
throw new Error(`The server sent a gateway that is not an IPv6 address: ${network.gatewayAddress}`);
|
|
9584
|
+
}
|
|
9585
|
+
const ipv4 = device.address4 && network.ingress4 ? { address: device.address4, ingress: network.ingress4 } : null;
|
|
9586
|
+
if (ipv4 && (!isIPv4Address(ipv4.address) || !isIPv4Address(ipv4.ingress))) {
|
|
9587
|
+
throw new Error(`The server sent an IPv4 address that is not one: ${ipv4.address}, ${ipv4.ingress}`);
|
|
9588
|
+
}
|
|
9589
|
+
const lines = [
|
|
9590
|
+
"# Written by `ghostdock network connect`. Run it again rather than editing this file.",
|
|
9591
|
+
"[Interface]",
|
|
9592
|
+
`PrivateKey = ${input.privateKey}`,
|
|
9593
|
+
`Address = ${device.address}/128${ipv4 ? `, ${ipv4.address}/32` : ""}`,
|
|
9594
|
+
`MTU = ${network.mtu}`
|
|
9595
|
+
];
|
|
9596
|
+
if (input.dns === "macos-resolver") {
|
|
9597
|
+
for (const zone of zones) {
|
|
9598
|
+
const file = `/etc/resolver/${zone}`;
|
|
9599
|
+
lines.push(`PostUp = mkdir -p /etc/resolver && printf 'nameserver ${network.gatewayAddress}\\n' > ${file}`, `PostDown = rm -f ${file}`);
|
|
9600
|
+
}
|
|
9601
|
+
} else if (input.dns === "resolved") {
|
|
9602
|
+
const domains = zones.map((zone) => `'~${zone}'`).join(" ");
|
|
9603
|
+
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${domains}`);
|
|
9604
|
+
}
|
|
9605
|
+
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.prefix}${ipv4 ? `, ${ipv4.ingress}/32` : ""}`, "PersistentKeepalive = 25", "");
|
|
9606
|
+
return lines.join(`
|
|
9607
|
+
`);
|
|
9608
|
+
}
|
|
9609
|
+
var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
9610
|
+
function isDnsZone(zone) {
|
|
9611
|
+
const labels = zone.split(".");
|
|
9612
|
+
return zone.length <= 253 && labels.length >= 2 && labels.every((label) => DNS_LABEL.test(label));
|
|
9613
|
+
}
|
|
9614
|
+
function isIPv4Address(text) {
|
|
9615
|
+
return /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/.test(text);
|
|
9616
|
+
}
|
|
9617
|
+
function ipv4RouteOwner(platform, output) {
|
|
9618
|
+
if (platform === "darwin") {
|
|
9619
|
+
if (/^\s*destination: default$/m.test(output))
|
|
9620
|
+
return null;
|
|
9621
|
+
return /^\s*interface: (\S+)$/m.exec(output)?.[1] ?? null;
|
|
9622
|
+
}
|
|
9623
|
+
for (const line of output.split(`
|
|
9624
|
+
`).map((l) => l.trim())) {
|
|
9625
|
+
if (!line || /^(default|broadcast)\b/.test(line))
|
|
9626
|
+
continue;
|
|
9627
|
+
return /\bdev (\S+)/.exec(line)?.[1] ?? "another route";
|
|
9628
|
+
}
|
|
9629
|
+
return null;
|
|
9630
|
+
}
|
|
9631
|
+
function ipv4RouteCommand(platform, address) {
|
|
9632
|
+
return platform === "darwin" ? ["route", ["-n", "get", address]] : ["ip", ["-4", "route", "show", "table", "all", "match", address]];
|
|
9633
|
+
}
|
|
9634
|
+
function isIPv6Address(text) {
|
|
9635
|
+
return /^[0-9a-f:]{2,39}$/.test(text) && text.includes(":");
|
|
9636
|
+
}
|
|
9637
|
+
async function readConfig(path = configPath()) {
|
|
9638
|
+
try {
|
|
9639
|
+
return await readFile2(path, "utf8");
|
|
9640
|
+
} catch {
|
|
9641
|
+
return null;
|
|
9642
|
+
}
|
|
9643
|
+
}
|
|
9644
|
+
function configResolverZones(config) {
|
|
9645
|
+
if (!config)
|
|
9646
|
+
return [];
|
|
9647
|
+
const zones = [];
|
|
9648
|
+
for (const match of config.matchAll(/^PostDown = rm -f \/etc\/resolver\/(\S+)$/gm)) {
|
|
9649
|
+
const zone = match[1] ?? "";
|
|
9650
|
+
if (isDnsZone(zone) && !zones.includes(zone))
|
|
9651
|
+
zones.push(zone);
|
|
9652
|
+
}
|
|
9653
|
+
return zones;
|
|
9654
|
+
}
|
|
9655
|
+
function resolverFile(zone) {
|
|
9656
|
+
return `/etc/resolver/${zone}`;
|
|
9657
|
+
}
|
|
9658
|
+
async function readConfigMtu(path = configPath()) {
|
|
9659
|
+
let contents;
|
|
9660
|
+
try {
|
|
9661
|
+
contents = await readFile2(path, "utf8");
|
|
9662
|
+
} catch {
|
|
9663
|
+
return null;
|
|
9664
|
+
}
|
|
9665
|
+
const match = /^MTU = (\d+)$/m.exec(contents);
|
|
9666
|
+
return match ? Number(match[1]) : null;
|
|
9667
|
+
}
|
|
9668
|
+
async function writeConfig(contents, path = configPath()) {
|
|
9669
|
+
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
9670
|
+
await writeFile2(path, contents, { mode: 384 });
|
|
9671
|
+
await chmod2(path, 384);
|
|
9672
|
+
}
|
|
9673
|
+
function tunnelUp(platform = process.platform, exists = existsSync) {
|
|
9674
|
+
if (platform === "darwin")
|
|
9675
|
+
return exists(`/var/run/wireguard/${INTERFACE}.name`);
|
|
9676
|
+
return exists(`/sys/class/net/${INTERFACE}`);
|
|
9677
|
+
}
|
|
9678
|
+
function installHint(platform = process.platform) {
|
|
9679
|
+
if (platform === "darwin")
|
|
9680
|
+
return "brew install wireguard-tools";
|
|
9681
|
+
if (platform === "linux")
|
|
9682
|
+
return "sudo apt install wireguard-tools (or your distribution’s package)";
|
|
9683
|
+
return "Install wireguard-tools. On Windows, run the CLI inside WSL.";
|
|
9684
|
+
}
|
|
9685
|
+
|
|
9287
9686
|
// src/api-client.ts
|
|
9288
9687
|
var CLI_USER_AGENT = `ghostdock-cli/${version}`;
|
|
9289
9688
|
|
|
@@ -9325,6 +9724,7 @@ async function request(path, init = {}) {
|
|
|
9325
9724
|
...init.headers
|
|
9326
9725
|
}
|
|
9327
9726
|
});
|
|
9727
|
+
notePausedDevice(res.headers.get(DEVICE_STATE_HEADER));
|
|
9328
9728
|
if (!res.ok) {
|
|
9329
9729
|
const payload = await readErrorPayload(res);
|
|
9330
9730
|
throw new ApiError(res.status, payload.message, payload.data);
|
|
@@ -9356,6 +9756,11 @@ function deviceRevoke(body) {
|
|
|
9356
9756
|
body: JSON.stringify(body)
|
|
9357
9757
|
});
|
|
9358
9758
|
}
|
|
9759
|
+
var deviceIdOnce = null;
|
|
9760
|
+
function currentDeviceId() {
|
|
9761
|
+
deviceIdOnce ??= readDeviceState().then((state) => state && state.baseUrl === getBaseUrl() ? state.deviceId : null).catch(() => null);
|
|
9762
|
+
return deviceIdOnce;
|
|
9763
|
+
}
|
|
9359
9764
|
var refreshInFlight = null;
|
|
9360
9765
|
async function refreshStoredCredentials(refreshToken) {
|
|
9361
9766
|
if (refreshInFlight)
|
|
@@ -9378,10 +9783,12 @@ async function authorizedRequest(path, init = {}) {
|
|
|
9378
9783
|
if (!credentials) {
|
|
9379
9784
|
throw new AuthRequiredError;
|
|
9380
9785
|
}
|
|
9786
|
+
const deviceId = await currentDeviceId();
|
|
9381
9787
|
const withAuth = (accessToken) => ({
|
|
9382
9788
|
...init,
|
|
9383
9789
|
headers: {
|
|
9384
9790
|
...init.headers,
|
|
9791
|
+
...deviceId ? { [DEVICE_HEADER]: deviceId } : {},
|
|
9385
9792
|
authorization: `Bearer ${accessToken}`
|
|
9386
9793
|
}
|
|
9387
9794
|
});
|
|
@@ -9405,75 +9812,388 @@ async function authorizedRequest(path, init = {}) {
|
|
|
9405
9812
|
}
|
|
9406
9813
|
}
|
|
9407
9814
|
|
|
9408
|
-
// src/
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9815
|
+
// src/docker/certificates.ts
|
|
9816
|
+
import { spawnSync } from "node:child_process";
|
|
9817
|
+
|
|
9818
|
+
// src/docker/context.ts
|
|
9819
|
+
function contextDescription(sandbox, orgSlug) {
|
|
9820
|
+
return `Ghostdock sandbox ${sandbox} (${orgSlug}; needs ${getCliName()} network connect)`;
|
|
9821
|
+
}
|
|
9822
|
+
function csvField(field) {
|
|
9823
|
+
return /[",\r\n]/.test(field) ? `"${field.replaceAll('"', '""')}"` : field;
|
|
9824
|
+
}
|
|
9825
|
+
function contextArgs(verb, spec) {
|
|
9826
|
+
const endpoint = [
|
|
9827
|
+
`host=${spec.host}`,
|
|
9828
|
+
`ca=${spec.files.ca}`,
|
|
9829
|
+
`cert=${spec.files.cert}`,
|
|
9830
|
+
`key=${spec.files.key}`
|
|
9831
|
+
].map(csvField).join(",");
|
|
9832
|
+
return ["context", verb, spec.name, "--description", spec.description, "--docker", endpoint];
|
|
9833
|
+
}
|
|
9834
|
+
function contextHost(exec, name) {
|
|
9835
|
+
const result = exec("docker", [
|
|
9836
|
+
"context",
|
|
9837
|
+
"inspect",
|
|
9838
|
+
"--format",
|
|
9839
|
+
"{{.Endpoints.docker.Host}}",
|
|
9840
|
+
name
|
|
9841
|
+
]);
|
|
9842
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
9843
|
+
}
|
|
9844
|
+
function dockerFailure(args, result) {
|
|
9845
|
+
const said = result.stderr.trim() || result.stdout.trim();
|
|
9846
|
+
return new Error(`docker ${args.slice(0, 3).join(" ")} failed (exit ${result.status})${said ? `: ${said}` : "."}`);
|
|
9847
|
+
}
|
|
9848
|
+
function writeContext(exec, spec) {
|
|
9849
|
+
const created = contextHost(exec, spec.name) === null;
|
|
9850
|
+
const args = contextArgs(created ? "create" : "update", spec);
|
|
9851
|
+
const result = exec("docker", args);
|
|
9852
|
+
if (result.status !== 0)
|
|
9853
|
+
throw dockerFailure(args, result);
|
|
9854
|
+
return { created };
|
|
9855
|
+
}
|
|
9856
|
+
function useContext(exec, name) {
|
|
9857
|
+
const args = ["context", "use", name];
|
|
9858
|
+
const result = exec("docker", args);
|
|
9859
|
+
if (result.status !== 0)
|
|
9860
|
+
throw dockerFailure(args, result);
|
|
9861
|
+
}
|
|
9862
|
+
|
|
9863
|
+
// src/docker/csr.ts
|
|
9864
|
+
import { createPublicKey as createPublicKey2, sign } from "node:crypto";
|
|
9865
|
+
var TAG = {
|
|
9866
|
+
integer: 2,
|
|
9867
|
+
bitString: 3,
|
|
9868
|
+
utf8String: 12,
|
|
9869
|
+
sequence: 48,
|
|
9870
|
+
set: 49,
|
|
9871
|
+
context0: 160
|
|
9417
9872
|
};
|
|
9418
|
-
var
|
|
9419
|
-
|
|
9420
|
-
|
|
9873
|
+
var OID_COMMON_NAME = Buffer.from("0603550403", "hex");
|
|
9874
|
+
var OID_ECDSA_WITH_SHA256 = Buffer.from("06082a8648ce3d040302", "hex");
|
|
9875
|
+
function der(tag, content) {
|
|
9876
|
+
const length = content.length;
|
|
9877
|
+
if (length < 128)
|
|
9878
|
+
return Buffer.concat([Buffer.from([tag, length]), content]);
|
|
9879
|
+
const bytes = [];
|
|
9880
|
+
for (let rest = length;rest > 0; rest >>= 8)
|
|
9881
|
+
bytes.unshift(rest & 255);
|
|
9882
|
+
return Buffer.concat([Buffer.from([tag, 128 | bytes.length, ...bytes]), content]);
|
|
9883
|
+
}
|
|
9884
|
+
function csrSubject(deviceId) {
|
|
9885
|
+
return `ghostdock device ${deviceId}`;
|
|
9886
|
+
}
|
|
9887
|
+
function buildCsr(privateKey, deviceId) {
|
|
9888
|
+
const spki = createPublicKey2(privateKey).export({ type: "spki", format: "der" });
|
|
9889
|
+
const commonName = der(TAG.sequence, Buffer.concat([OID_COMMON_NAME, der(TAG.utf8String, Buffer.from(csrSubject(deviceId)))]));
|
|
9890
|
+
const subject = der(TAG.sequence, der(TAG.set, commonName));
|
|
9891
|
+
const info = der(TAG.sequence, Buffer.concat([
|
|
9892
|
+
der(TAG.integer, Buffer.from([0])),
|
|
9893
|
+
subject,
|
|
9894
|
+
spki,
|
|
9895
|
+
der(TAG.context0, Buffer.alloc(0))
|
|
9896
|
+
]));
|
|
9897
|
+
const signature = sign("sha256", info, { key: privateKey, dsaEncoding: "der" });
|
|
9898
|
+
return der(TAG.sequence, Buffer.concat([
|
|
9899
|
+
info,
|
|
9900
|
+
der(TAG.sequence, OID_ECDSA_WITH_SHA256),
|
|
9901
|
+
der(TAG.bitString, Buffer.concat([Buffer.from([0]), signature]))
|
|
9902
|
+
]));
|
|
9903
|
+
}
|
|
9904
|
+
function buildCsrPem(privateKey, deviceId) {
|
|
9905
|
+
const base64 = buildCsr(privateKey, deviceId).toString("base64");
|
|
9906
|
+
const lines = base64.match(/.{1,64}/g) ?? [];
|
|
9907
|
+
return `-----BEGIN CERTIFICATE REQUEST-----
|
|
9908
|
+
${lines.join(`
|
|
9909
|
+
`)}
|
|
9910
|
+
-----END CERTIFICATE REQUEST-----
|
|
9911
|
+
`;
|
|
9421
9912
|
}
|
|
9422
|
-
|
|
9423
|
-
|
|
9913
|
+
|
|
9914
|
+
// src/docker/store.ts
|
|
9915
|
+
import {
|
|
9916
|
+
createPrivateKey as createPrivateKey2,
|
|
9917
|
+
generateKeyPairSync as generateKeyPairSync2,
|
|
9918
|
+
randomBytes,
|
|
9919
|
+
X509Certificate
|
|
9920
|
+
} from "node:crypto";
|
|
9921
|
+
import { chmod as chmod3, link, mkdir as mkdir3, readdir, readFile as readFile3, rename, unlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
9922
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
9923
|
+
function dockerDirFor(networkDir) {
|
|
9924
|
+
return join3(dirname3(networkDir), "docker");
|
|
9424
9925
|
}
|
|
9425
|
-
function
|
|
9426
|
-
|
|
9427
|
-
return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
|
|
9428
|
-
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
|
|
9429
|
-
return false;
|
|
9430
|
-
if (env.TERM === "dumb")
|
|
9431
|
-
return false;
|
|
9432
|
-
return stream.isTTY === true;
|
|
9926
|
+
function isOrganizationId(id) {
|
|
9927
|
+
return /^[A-Za-z0-9_-]{1,128}$/.test(id);
|
|
9433
9928
|
}
|
|
9434
|
-
function
|
|
9435
|
-
|
|
9929
|
+
function organizationFiles(dockerDir, organizationId) {
|
|
9930
|
+
if (!isOrganizationId(organizationId)) {
|
|
9931
|
+
throw new Error(`Not an organization id: ${JSON.stringify(organizationId)}`);
|
|
9932
|
+
}
|
|
9933
|
+
const dir = join3(dockerDir, organizationId);
|
|
9934
|
+
return {
|
|
9935
|
+
dir,
|
|
9936
|
+
key: join3(dir, "key.pem"),
|
|
9937
|
+
cert: join3(dir, "cert.pem"),
|
|
9938
|
+
ca: join3(dir, "ca.pem"),
|
|
9939
|
+
meta: join3(dir, "certificate.json")
|
|
9940
|
+
};
|
|
9436
9941
|
}
|
|
9437
|
-
function
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9942
|
+
async function listOrganizationIds(dockerDir) {
|
|
9943
|
+
try {
|
|
9944
|
+
const entries = await readdir(dockerDir, { withFileTypes: true });
|
|
9945
|
+
return entries.filter((entry) => entry.isDirectory() && isOrganizationId(entry.name)).map((entry) => entry.name).sort();
|
|
9946
|
+
} catch (err) {
|
|
9947
|
+
if (err.code === "ENOENT")
|
|
9948
|
+
return [];
|
|
9949
|
+
throw err;
|
|
9950
|
+
}
|
|
9442
9951
|
}
|
|
9443
|
-
function
|
|
9444
|
-
return
|
|
9952
|
+
function tempPath(path) {
|
|
9953
|
+
return `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
9445
9954
|
}
|
|
9446
|
-
function
|
|
9447
|
-
|
|
9955
|
+
async function writeFileAtomic(path, contents, mode) {
|
|
9956
|
+
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
9957
|
+
const temp = tempPath(path);
|
|
9958
|
+
try {
|
|
9959
|
+
await writeFile3(temp, contents, { mode });
|
|
9960
|
+
await chmod3(temp, mode);
|
|
9961
|
+
await rename(temp, path);
|
|
9962
|
+
} catch (err) {
|
|
9963
|
+
await unlink(temp).catch(() => {});
|
|
9964
|
+
throw err;
|
|
9965
|
+
}
|
|
9448
9966
|
}
|
|
9449
|
-
function
|
|
9450
|
-
return
|
|
9967
|
+
function isP256(key) {
|
|
9968
|
+
return key.asymmetricKeyType === "ec" && key.asymmetricKeyDetails?.namedCurve === "prime256v1";
|
|
9451
9969
|
}
|
|
9452
|
-
function
|
|
9453
|
-
|
|
9970
|
+
async function loadOrCreateDockerKey(files) {
|
|
9971
|
+
const existing = await readDockerKey(files);
|
|
9972
|
+
if (existing)
|
|
9973
|
+
return existing;
|
|
9974
|
+
const { privateKey } = generateKeyPairSync2("ec", { namedCurve: "prime256v1" });
|
|
9975
|
+
const pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
9976
|
+
await mkdir3(files.dir, { recursive: true, mode: 448 });
|
|
9977
|
+
const temp = tempPath(files.key);
|
|
9978
|
+
try {
|
|
9979
|
+
await writeFile3(temp, pem, { mode: 384 });
|
|
9980
|
+
await chmod3(temp, 384);
|
|
9981
|
+
await link(temp, files.key);
|
|
9982
|
+
} catch (err) {
|
|
9983
|
+
if (err.code !== "EEXIST")
|
|
9984
|
+
throw err;
|
|
9985
|
+
const created = await readDockerKey(files);
|
|
9986
|
+
if (created)
|
|
9987
|
+
return created;
|
|
9988
|
+
throw err;
|
|
9989
|
+
} finally {
|
|
9990
|
+
await unlink(temp).catch(() => {});
|
|
9991
|
+
}
|
|
9992
|
+
return privateKey;
|
|
9454
9993
|
}
|
|
9455
|
-
function
|
|
9456
|
-
|
|
9457
|
-
|
|
9994
|
+
async function readDockerKey(files) {
|
|
9995
|
+
let pem;
|
|
9996
|
+
try {
|
|
9997
|
+
pem = await readFile3(files.key, "utf8");
|
|
9998
|
+
} catch (err) {
|
|
9999
|
+
if (err.code === "ENOENT")
|
|
10000
|
+
return null;
|
|
10001
|
+
throw err;
|
|
10002
|
+
}
|
|
10003
|
+
const key = createPrivateKey2(pem);
|
|
10004
|
+
if (!isP256(key))
|
|
10005
|
+
throw new Error(`${files.key} is not an ECDSA P-256 key.`);
|
|
10006
|
+
return key;
|
|
9458
10007
|
}
|
|
9459
|
-
function
|
|
9460
|
-
|
|
9461
|
-
`)
|
|
10008
|
+
function uriSan(certificate, prefix) {
|
|
10009
|
+
for (const name of (certificate.subjectAltName ?? "").split(/,\s*/)) {
|
|
10010
|
+
if (name.startsWith(`URI:${prefix}`))
|
|
10011
|
+
return name.slice(`URI:${prefix}`.length);
|
|
10012
|
+
}
|
|
10013
|
+
return null;
|
|
9462
10014
|
}
|
|
9463
|
-
function
|
|
9464
|
-
|
|
9465
|
-
|
|
10015
|
+
function parseClientCertificate(pem, key) {
|
|
10016
|
+
const certificate = new X509Certificate(pem);
|
|
10017
|
+
if (!certificate.checkPrivateKey(key)) {
|
|
10018
|
+
throw new Error("The certificate is not for this machine's Docker key.");
|
|
10019
|
+
}
|
|
10020
|
+
return {
|
|
10021
|
+
pem,
|
|
10022
|
+
serial: certificate.serialNumber.toLowerCase(),
|
|
10023
|
+
notAfter: new Date(certificate.validTo),
|
|
10024
|
+
deviceId: uriSan(certificate, DOCKER_CLIENT_URI_PREFIX.device),
|
|
10025
|
+
organizationId: uriSan(certificate, DOCKER_CLIENT_URI_PREFIX.organization)
|
|
10026
|
+
};
|
|
10027
|
+
}
|
|
10028
|
+
async function readClientCertificate(files, key) {
|
|
10029
|
+
try {
|
|
10030
|
+
const [pem] = await Promise.all([readFile3(files.cert, "utf8"), readFile3(files.ca, "utf8")]);
|
|
10031
|
+
return parseClientCertificate(pem, key);
|
|
10032
|
+
} catch {
|
|
10033
|
+
return null;
|
|
10034
|
+
}
|
|
10035
|
+
}
|
|
10036
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
10037
|
+
function certificateDue(certificate, deviceId, now) {
|
|
10038
|
+
return certificate.deviceId !== deviceId || certificate.notAfter.getTime() - now.getTime() < DOCKER_CERTIFICATE_RENEW_DAYS * DAY_MS;
|
|
10039
|
+
}
|
|
10040
|
+
async function readCertificateMeta(files) {
|
|
10041
|
+
try {
|
|
10042
|
+
const parsed = JSON.parse(await readFile3(files.meta, "utf8"));
|
|
10043
|
+
if (typeof parsed.baseUrl === "string" && typeof parsed.organization?.id === "string" && typeof parsed.organization.slug === "string") {
|
|
10044
|
+
return {
|
|
10045
|
+
baseUrl: parsed.baseUrl,
|
|
10046
|
+
organization: { id: parsed.organization.id, slug: parsed.organization.slug },
|
|
10047
|
+
serial: typeof parsed.serial === "string" ? parsed.serial : "",
|
|
10048
|
+
notAfter: typeof parsed.notAfter === "string" ? parsed.notAfter : ""
|
|
10049
|
+
};
|
|
10050
|
+
}
|
|
10051
|
+
} catch {}
|
|
10052
|
+
return null;
|
|
10053
|
+
}
|
|
10054
|
+
async function writeIssuedCertificate(files, issued) {
|
|
10055
|
+
await writeFileAtomic(files.ca, issued.caPem, 420);
|
|
10056
|
+
await writeFileAtomic(files.cert, issued.certificatePem, 420);
|
|
10057
|
+
await writeFileAtomic(files.meta, `${JSON.stringify(issued.meta, null, 2)}
|
|
10058
|
+
`, 420);
|
|
10059
|
+
}
|
|
10060
|
+
function contextsPath(dockerDir) {
|
|
10061
|
+
return join3(dockerDir, "contexts.json");
|
|
10062
|
+
}
|
|
10063
|
+
async function readContexts(dockerDir) {
|
|
10064
|
+
try {
|
|
10065
|
+
const parsed = JSON.parse(await readFile3(contextsPath(dockerDir), "utf8"));
|
|
10066
|
+
return (parsed.contexts ?? []).filter((c) => typeof c?.name === "string" && typeof c.organizationId === "string" && typeof c.sandbox === "string");
|
|
10067
|
+
} catch {
|
|
10068
|
+
return [];
|
|
10069
|
+
}
|
|
10070
|
+
}
|
|
10071
|
+
async function writeContexts(dockerDir, contexts) {
|
|
10072
|
+
const unique = [...new Map(contexts.map((c) => [c.name, c])).values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
10073
|
+
await writeFileAtomic(contextsPath(dockerDir), `${JSON.stringify({ contexts: unique }, null, 2)}
|
|
10074
|
+
`, 420);
|
|
10075
|
+
}
|
|
10076
|
+
async function recordContext(dockerDir, context) {
|
|
10077
|
+
const others = (await readContexts(dockerDir)).filter((c) => c.name !== context.name);
|
|
10078
|
+
await writeContexts(dockerDir, [...others, context]);
|
|
10079
|
+
}
|
|
10080
|
+
|
|
10081
|
+
// src/docker/certificates.ts
|
|
10082
|
+
function execDocker(command, args) {
|
|
10083
|
+
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
10084
|
+
return {
|
|
10085
|
+
status: result.status ?? 1,
|
|
10086
|
+
stdout: result.stdout ?? "",
|
|
10087
|
+
stderr: result.stderr ?? result.error?.message ?? ""
|
|
10088
|
+
};
|
|
10089
|
+
}
|
|
10090
|
+
async function issueCertificate(input, request) {
|
|
10091
|
+
const body = {
|
|
10092
|
+
organization: input.organizationId,
|
|
10093
|
+
deviceId: input.deviceId,
|
|
10094
|
+
csrPem: buildCsrPem(input.key, input.deviceId)
|
|
10095
|
+
};
|
|
10096
|
+
const issued = await request("/api/v1/docker/certificates", {
|
|
10097
|
+
method: "POST",
|
|
10098
|
+
body: JSON.stringify(body)
|
|
10099
|
+
});
|
|
10100
|
+
const certificate = parseClientCertificate(issued.certificatePem, input.key);
|
|
10101
|
+
if (certificate.deviceId !== input.deviceId || issued.organization.id !== input.organizationId || certificate.organizationId !== null && certificate.organizationId !== input.organizationId) {
|
|
10102
|
+
throw new Error("The app sent a certificate for another device or organization: not stored.");
|
|
10103
|
+
}
|
|
10104
|
+
const meta = {
|
|
10105
|
+
baseUrl: input.baseUrl,
|
|
10106
|
+
organization: issued.organization,
|
|
10107
|
+
serial: certificate.serial,
|
|
10108
|
+
notAfter: certificate.notAfter.toISOString()
|
|
10109
|
+
};
|
|
10110
|
+
await writeIssuedCertificate(input.files, {
|
|
10111
|
+
certificatePem: issued.certificatePem,
|
|
10112
|
+
caPem: issued.caPem,
|
|
10113
|
+
meta
|
|
10114
|
+
});
|
|
10115
|
+
return { certificate, meta };
|
|
10116
|
+
}
|
|
10117
|
+
async function ensureCertificate(input, request) {
|
|
10118
|
+
const key = await loadOrCreateDockerKey(input.files);
|
|
10119
|
+
const stored = await readClientCertificate(input.files, key);
|
|
10120
|
+
if (stored && !certificateDue(stored, input.deviceId, input.now)) {
|
|
10121
|
+
return { certificate: stored, issued: false };
|
|
10122
|
+
}
|
|
10123
|
+
const { certificate } = await issueCertificate({ ...input, key }, request);
|
|
10124
|
+
return { certificate, issued: true };
|
|
10125
|
+
}
|
|
10126
|
+
function message(err) {
|
|
10127
|
+
return err instanceof Error ? err.message : String(err);
|
|
10128
|
+
}
|
|
10129
|
+
function day(date) {
|
|
10130
|
+
return date.toISOString().slice(0, 10);
|
|
10131
|
+
}
|
|
10132
|
+
async function renewDockerCertificates(deps, options = {}) {
|
|
10133
|
+
const exec = deps.exec ?? execDocker;
|
|
10134
|
+
const dockerDir = deps.dockerDir ?? dockerDirFor(deps.dir);
|
|
10135
|
+
const now = (deps.now ?? (() => new Date))();
|
|
10136
|
+
let organizationIds;
|
|
10137
|
+
let state;
|
|
10138
|
+
try {
|
|
10139
|
+
organizationIds = (await listOrganizationIds(dockerDir)).filter((id) => id !== options.except);
|
|
10140
|
+
state = organizationIds.length > 0 ? await readDeviceState(deps.dir) : null;
|
|
10141
|
+
} catch (err) {
|
|
10142
|
+
deps.notice(`Could not check the Docker certificates: ${message(err)}`);
|
|
9466
10143
|
return;
|
|
9467
10144
|
}
|
|
9468
|
-
|
|
9469
|
-
|
|
10145
|
+
if (!state || !deps.has("docker"))
|
|
10146
|
+
return;
|
|
10147
|
+
const deviceId = state.deviceId;
|
|
10148
|
+
let contexts = await readContexts(dockerDir);
|
|
10149
|
+
let contextsChanged = false;
|
|
10150
|
+
for (const organizationId of organizationIds) {
|
|
10151
|
+
const files = organizationFiles(dockerDir, organizationId);
|
|
10152
|
+
const meta = await readCertificateMeta(files);
|
|
10153
|
+
const label = meta?.organization.slug ?? organizationId;
|
|
10154
|
+
try {
|
|
10155
|
+
if (meta && meta.baseUrl !== state.baseUrl)
|
|
10156
|
+
continue;
|
|
10157
|
+
const key = await readDockerKey(files);
|
|
10158
|
+
const stored = key ? await readClientCertificate(files, key) : null;
|
|
10159
|
+
if (!key || !stored || !certificateDue(stored, deviceId, now))
|
|
10160
|
+
continue;
|
|
10161
|
+
const { certificate } = await issueCertificate({ files, key, organizationId, deviceId, baseUrl: state.baseUrl }, deps.request);
|
|
10162
|
+
const updated = [];
|
|
10163
|
+
for (const context of contexts.filter((c) => c.organizationId === organizationId)) {
|
|
10164
|
+
const host = contextHost(exec, context.name);
|
|
10165
|
+
if (host === null) {
|
|
10166
|
+
contexts = contexts.filter((c) => c.name !== context.name);
|
|
10167
|
+
contextsChanged = true;
|
|
10168
|
+
continue;
|
|
10169
|
+
}
|
|
10170
|
+
writeContext(exec, {
|
|
10171
|
+
name: context.name,
|
|
10172
|
+
description: contextDescription(context.sandbox, label),
|
|
10173
|
+
host,
|
|
10174
|
+
files
|
|
10175
|
+
});
|
|
10176
|
+
updated.push(context.name);
|
|
10177
|
+
}
|
|
10178
|
+
deps.notice(`Renewed the Docker certificate for ${label}, valid until ${day(certificate.notAfter)}${updated.length > 0 ? `; updated ${updated.join(", ")}` : ""}.`);
|
|
10179
|
+
} catch (err) {
|
|
10180
|
+
deps.notice(`Could not renew the Docker certificate for ${label} (${message(err)}). Run \`${getCliName()} docker context <sandbox> --org ${label}\` to try again.`);
|
|
10181
|
+
}
|
|
10182
|
+
}
|
|
10183
|
+
if (contextsChanged) {
|
|
10184
|
+
try {
|
|
10185
|
+
await writeContexts(dockerDir, contexts);
|
|
10186
|
+
} catch (err) {
|
|
10187
|
+
deps.notice(`Could not update ${dockerDir}/contexts.json: ${message(err)}`);
|
|
10188
|
+
}
|
|
10189
|
+
}
|
|
9470
10190
|
}
|
|
9471
10191
|
|
|
9472
10192
|
// src/settings.ts
|
|
9473
|
-
import { mkdir as
|
|
9474
|
-
import { dirname as
|
|
10193
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
10194
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
9475
10195
|
function getSettingsPath() {
|
|
9476
|
-
return
|
|
10196
|
+
return join4(dirname4(getCredentialsPath()), "settings.json");
|
|
9477
10197
|
}
|
|
9478
10198
|
function isDefaultOrganization(value) {
|
|
9479
10199
|
if (!value || typeof value !== "object")
|
|
@@ -9483,15 +10203,15 @@ function isDefaultOrganization(value) {
|
|
|
9483
10203
|
}
|
|
9484
10204
|
async function readSettings(path = getSettingsPath()) {
|
|
9485
10205
|
try {
|
|
9486
|
-
const parsed = JSON.parse(await
|
|
10206
|
+
const parsed = JSON.parse(await readFile4(path, "utf8"));
|
|
9487
10207
|
return isDefaultOrganization(parsed.defaultOrganization) ? { defaultOrganization: parsed.defaultOrganization } : {};
|
|
9488
10208
|
} catch {
|
|
9489
10209
|
return {};
|
|
9490
10210
|
}
|
|
9491
10211
|
}
|
|
9492
10212
|
async function writeSettings(settings, path = getSettingsPath()) {
|
|
9493
|
-
await
|
|
9494
|
-
await
|
|
10213
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
10214
|
+
await writeFile4(path, `${JSON.stringify(settings, null, 2)}
|
|
9495
10215
|
`, "utf8");
|
|
9496
10216
|
}
|
|
9497
10217
|
|
|
@@ -9556,1667 +10276,1769 @@ async function resolveOrganizationId(flag) {
|
|
|
9556
10276
|
return (await findMyOrganization(await requireOrganizationRef(flag))).id;
|
|
9557
10277
|
}
|
|
9558
10278
|
|
|
9559
|
-
// src/commands/
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
10279
|
+
// src/commands/network.ts
|
|
10280
|
+
import { spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
10281
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10282
|
+
import { hostname } from "node:os";
|
|
10283
|
+
function defaultDeps() {
|
|
10284
|
+
return {
|
|
10285
|
+
request: authorizedRequest,
|
|
10286
|
+
run: (command, args) => new Promise((resolve, reject) => {
|
|
10287
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
10288
|
+
child.on("error", reject);
|
|
10289
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
10290
|
+
}),
|
|
10291
|
+
output: (command, args) => {
|
|
10292
|
+
const result = spawnSync2(command, args, { encoding: "utf8" });
|
|
10293
|
+
return result.status === 0 ? result.stdout : "";
|
|
10294
|
+
},
|
|
10295
|
+
platform: process.platform,
|
|
10296
|
+
has: (command) => spawnSync2("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
10297
|
+
dir: networkDir(),
|
|
10298
|
+
up: () => tunnelUp(),
|
|
10299
|
+
exists: existsSync2,
|
|
10300
|
+
log: console.log,
|
|
10301
|
+
error: printError,
|
|
10302
|
+
notice: printNotice
|
|
10303
|
+
};
|
|
9567
10304
|
}
|
|
9568
|
-
function
|
|
9569
|
-
return
|
|
9570
|
-
method: "POST",
|
|
9571
|
-
body: JSON.stringify(input)
|
|
9572
|
-
});
|
|
10305
|
+
function message2(err) {
|
|
10306
|
+
return err instanceof Error ? err.message : String(err);
|
|
9573
10307
|
}
|
|
9574
|
-
function
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
return `<${entry.valueLength} chars — read with \`${getCliName()} env get\`>`;
|
|
9579
|
-
}
|
|
9580
|
-
return entry.value ?? "";
|
|
10308
|
+
function connectCommandFor(provider) {
|
|
10309
|
+
const ref = provider.connectRef;
|
|
10310
|
+
const arg = /^[A-Za-z0-9._-]+$/.test(ref) ? ref : JSON.stringify(ref);
|
|
10311
|
+
return `${getCliName()} network connect ${arg}`;
|
|
9581
10312
|
}
|
|
9582
|
-
function
|
|
9583
|
-
const
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
displayValue(entry),
|
|
9587
|
-
entry.comment ?? ""
|
|
9588
|
-
]);
|
|
9589
|
-
const header = ["NAME", "KIND", "VALUE", "COMMENT"];
|
|
9590
|
-
const widths = header.map((_, column) => Math.max(header[column]?.length ?? 0, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
9591
|
-
const line = (cells) => cells.map((cell, column) => column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
9592
|
-
return [line(header), ...rows.map(line)].join(`
|
|
9593
|
-
`);
|
|
10313
|
+
async function wgQuick(deps, verb) {
|
|
10314
|
+
const args = ["wg-quick", verb, configPath(deps.dir)];
|
|
10315
|
+
deps.notice(`Running: sudo ${args.join(" ")}`);
|
|
10316
|
+
return deps.run("sudo", args);
|
|
9594
10317
|
}
|
|
9595
|
-
async function
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
10318
|
+
async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
10319
|
+
suppressPausedNotice();
|
|
10320
|
+
if (!deps.has("wg-quick")) {
|
|
10321
|
+
deps.error(`wg-quick is not installed. ${installHint()}`);
|
|
10322
|
+
return 1;
|
|
9599
10323
|
}
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
10324
|
+
let registered;
|
|
10325
|
+
let privateKey;
|
|
10326
|
+
const previous = await readDeviceState(deps.dir);
|
|
9603
10327
|
try {
|
|
9604
|
-
const
|
|
9605
|
-
|
|
10328
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10329
|
+
const provider = args.provider?.trim();
|
|
10330
|
+
const key = await loadOrCreateKey(deps.dir);
|
|
10331
|
+
privateKey = key.privateKey;
|
|
10332
|
+
registered = await deps.request("/api/v1/network/devices", {
|
|
10333
|
+
method: "POST",
|
|
10334
|
+
body: JSON.stringify({
|
|
10335
|
+
publicKey: key.publicKey,
|
|
10336
|
+
name: hostname().slice(0, 64) || "device",
|
|
10337
|
+
organization,
|
|
10338
|
+
...provider ? { provider } : {}
|
|
10339
|
+
})
|
|
9606
10340
|
});
|
|
9607
|
-
if (args.json) {
|
|
9608
|
-
log(JSON.stringify(result.entries, null, 2));
|
|
9609
|
-
return 0;
|
|
9610
|
-
}
|
|
9611
|
-
if (result.entries.length === 0) {
|
|
9612
|
-
log(" No variables or secrets.");
|
|
9613
|
-
return 0;
|
|
9614
|
-
}
|
|
9615
|
-
log(formatTable(result.entries));
|
|
9616
|
-
return 0;
|
|
9617
10341
|
} catch (err) {
|
|
9618
|
-
|
|
10342
|
+
deps.error(message2(err));
|
|
10343
|
+
return 1;
|
|
9619
10344
|
}
|
|
9620
|
-
|
|
9621
|
-
|
|
10345
|
+
const dns = detectDnsMode();
|
|
10346
|
+
let config;
|
|
9622
10347
|
try {
|
|
9623
|
-
|
|
9624
|
-
organization: args.organization,
|
|
9625
|
-
name: args.name
|
|
9626
|
-
});
|
|
9627
|
-
log(args.json ? JSON.stringify(result.entry, null, 2) : result.entry.value ?? "");
|
|
9628
|
-
return 0;
|
|
10348
|
+
config = renderConfig({ privateKey, registered, dns });
|
|
9629
10349
|
} catch (err) {
|
|
9630
|
-
|
|
10350
|
+
deps.error(message2(err));
|
|
10351
|
+
return 1;
|
|
9631
10352
|
}
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
10353
|
+
const previousConfig = await readConfig(configPath(deps.dir));
|
|
10354
|
+
await writeConfig(config, configPath(deps.dir));
|
|
10355
|
+
await writeDeviceState({
|
|
10356
|
+
baseUrl: getBaseUrl(),
|
|
10357
|
+
deviceId: registered.device.id,
|
|
10358
|
+
address: registered.device.address ?? "",
|
|
10359
|
+
gatewayProvider: registered.provider,
|
|
10360
|
+
zones: [
|
|
10361
|
+
registered.network.zone,
|
|
10362
|
+
...registered.network.netZone ? [registered.network.netZone] : []
|
|
10363
|
+
]
|
|
10364
|
+
}, deps.dir);
|
|
10365
|
+
const wasUp = deps.up();
|
|
10366
|
+
if (wasUp && previousConfig === config) {
|
|
10367
|
+
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
10368
|
+
printResult({
|
|
10369
|
+
device,
|
|
10370
|
+
network,
|
|
10371
|
+
provider,
|
|
10372
|
+
shortNameOrganizationChanged,
|
|
10373
|
+
interface: INTERFACE,
|
|
10374
|
+
dns,
|
|
10375
|
+
rearmed: true,
|
|
10376
|
+
pause: device.state
|
|
10377
|
+
}, () => [
|
|
10378
|
+
eventLine("done", rearmedLine(device.state)),
|
|
10379
|
+
...network.netZone && device.shortNameOrganization && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : []
|
|
10380
|
+
], deps.log);
|
|
10381
|
+
printPauseHint(device.state, deps);
|
|
10382
|
+
await renewDockerCertificates(deps);
|
|
10383
|
+
return 0;
|
|
9636
10384
|
}
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
10385
|
+
if (wasUp) {
|
|
10386
|
+
const down = await wgQuick(deps, "down");
|
|
10387
|
+
if (down !== 0) {
|
|
10388
|
+
deps.error(`Could not take the old tunnel down (wg-quick exited ${down}).`);
|
|
10389
|
+
return 1;
|
|
10390
|
+
}
|
|
9640
10391
|
}
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
error("Pass either --value or --value-stdin, not both.");
|
|
10392
|
+
const ipv4Clashes = ipv4Clashes4(registered, deps);
|
|
10393
|
+
const code = await wgQuick(deps, "up");
|
|
10394
|
+
if (code !== 0) {
|
|
10395
|
+
deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
|
|
9646
10396
|
return 1;
|
|
9647
10397
|
}
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
10398
|
+
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
10399
|
+
const pinned = device.shortNameOrganization;
|
|
10400
|
+
const switched = wasUp && previous?.gatewayProvider && previous.gatewayProvider.id !== provider.id ? previous.gatewayProvider : null;
|
|
10401
|
+
printResult({
|
|
10402
|
+
device,
|
|
10403
|
+
network,
|
|
10404
|
+
provider,
|
|
10405
|
+
shortNameOrganizationChanged,
|
|
10406
|
+
interface: INTERFACE,
|
|
10407
|
+
dns,
|
|
10408
|
+
rearmed: false,
|
|
10409
|
+
pause: device.state
|
|
10410
|
+
}, () => [
|
|
10411
|
+
eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
|
|
10412
|
+
...switched ? [
|
|
10413
|
+
` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
|
|
10414
|
+
] : [],
|
|
10415
|
+
` Reachable: the sandboxes on ${provider.name}. One provider at a time.`,
|
|
10416
|
+
` Names: <sandbox>.<org>.${network.zone}`,
|
|
10417
|
+
...network.netZone ? [` In a browser: https://<sandbox>--<org>.${network.netZone}:<port>`] : [],
|
|
10418
|
+
...network.netZone && pinned ? [
|
|
10419
|
+
` Short names: https://<sandbox>.${network.netZone}:<port> for ${pinned.slug}'s sandboxes, on this machine.`,
|
|
10420
|
+
` Another organization: ${getCliName()} network connect --org <org>.`
|
|
10421
|
+
] : [],
|
|
10422
|
+
...network.netZone && pinned && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : [],
|
|
10423
|
+
...network.ingress4 && !device.address4 ? [
|
|
10424
|
+
" No IPv4 address was free on this provider's host, so browser names work over IPv6 only:",
|
|
10425
|
+
" Chrome on a network without IPv6 cannot open them on this machine. Firefox and Safari",
|
|
10426
|
+
" can. Run connect again later: an address frees up when another device goes."
|
|
10427
|
+
] : [],
|
|
10428
|
+
...ipv4Clashes.map(({ address, owner }) => ` ${address} is already routed through ${owner} on this machine: browsers may not reach the private network's names over IPv4 while it is. Ask your Ghostdock admin about the IPv4 block.`),
|
|
10429
|
+
...dns === "none" ? [
|
|
10430
|
+
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
10431
|
+
` \`${getCliName()} network ls\`.`
|
|
10432
|
+
] : [],
|
|
10433
|
+
` → ${getCliName()} network ls`
|
|
10434
|
+
], deps.log);
|
|
10435
|
+
printPauseHint(device.state, deps);
|
|
10436
|
+
await renewDockerCertificates(deps);
|
|
10437
|
+
return 0;
|
|
10438
|
+
}
|
|
10439
|
+
function rearmedLine(state, now = new Date) {
|
|
10440
|
+
if (!state)
|
|
10441
|
+
return "Re-armed.";
|
|
10442
|
+
const until = clockTime(new Date(now.getTime() + state.maxSessionSeconds * 1000).toISOString(), now);
|
|
10443
|
+
return `Re-armed: network access for this device runs until ${until} (pauses after ${formatPauseLimit(state.idleTimeoutSeconds)} idle).`;
|
|
10444
|
+
}
|
|
10445
|
+
function printPauseHint(state, deps) {
|
|
10446
|
+
if (!state || isJsonOutput())
|
|
10447
|
+
return;
|
|
10448
|
+
const [first = "", ...rest] = pauseHint(state);
|
|
10449
|
+
deps.notice(["", bold2(first), ...rest].join(`
|
|
10450
|
+
`));
|
|
10451
|
+
}
|
|
10452
|
+
function ipv4Clashes4(registered, deps) {
|
|
10453
|
+
const addresses = [registered.network.ingress4, registered.device.address4].filter((a) => typeof a === "string" && isIPv4Address(a));
|
|
10454
|
+
const clashes = [];
|
|
10455
|
+
for (const address of addresses) {
|
|
10456
|
+
const [command, args] = ipv4RouteCommand(deps.platform, address);
|
|
10457
|
+
const owner = ipv4RouteOwner(deps.platform, deps.output(command, args));
|
|
10458
|
+
if (owner)
|
|
10459
|
+
clashes.push({ address, owner });
|
|
9651
10460
|
}
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
10461
|
+
return clashes;
|
|
10462
|
+
}
|
|
10463
|
+
function clearingNotice(cookieSite) {
|
|
10464
|
+
const site = cookieSite?.site ?? "the net domain";
|
|
10465
|
+
return [
|
|
10466
|
+
" Short names meant another organization's sandboxes before. The first short-name site each",
|
|
10467
|
+
` browser opens now clears its data once: the site's storage, and every cookie of ${site}.`,
|
|
10468
|
+
` That signs the browser out of every site under ${site}${cookieSite?.includesApp ? ", the Ghostdock app included" : ""}.`,
|
|
10469
|
+
" Reload or close browser tabs opened on short names before this connect."
|
|
10470
|
+
];
|
|
10471
|
+
}
|
|
10472
|
+
async function staleResolverFiles(deps) {
|
|
10473
|
+
if (deps.platform !== "darwin")
|
|
10474
|
+
return [];
|
|
10475
|
+
const state = await readDeviceState(deps.dir);
|
|
10476
|
+
const zones = new Set([
|
|
10477
|
+
...configResolverZones(await readConfig(configPath(deps.dir))),
|
|
10478
|
+
...state?.zones ?? []
|
|
10479
|
+
]);
|
|
10480
|
+
return [...zones].map(resolverFile).filter((file) => deps.exists(file));
|
|
10481
|
+
}
|
|
10482
|
+
async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
10483
|
+
const wasUp = deps.up();
|
|
10484
|
+
let failed = false;
|
|
10485
|
+
if (wasUp) {
|
|
10486
|
+
const code = await wgQuick(deps, "down");
|
|
10487
|
+
if (code !== 0) {
|
|
10488
|
+
deps.error(`wg-quick exited ${code}.`);
|
|
10489
|
+
failed = true;
|
|
10490
|
+
}
|
|
10491
|
+
}
|
|
10492
|
+
const stale = await staleResolverFiles(deps);
|
|
10493
|
+
if (stale.length) {
|
|
10494
|
+
const args = ["rm", "-f", ...stale];
|
|
10495
|
+
deps.notice(`Removing split DNS left from an earlier connection. Running: sudo ${args.join(" ")}`);
|
|
10496
|
+
const code = await deps.run("sudo", args);
|
|
10497
|
+
if (code !== 0) {
|
|
10498
|
+
deps.error(`Could not remove ${stale.join(", ")} (rm exited ${code}).`);
|
|
10499
|
+
return 1;
|
|
10500
|
+
}
|
|
9656
10501
|
}
|
|
9657
|
-
|
|
10502
|
+
if (failed)
|
|
10503
|
+
return 1;
|
|
10504
|
+
printResult({ connected: false, removedSplitDns: stale }, () => [
|
|
10505
|
+
wasUp ? eventLine("done", "Disconnected.") : eventLine(stale.length ? "done" : "item", stale.length ? "Not connected; removed the leftover split DNS." : "Not connected.")
|
|
10506
|
+
], deps.log);
|
|
10507
|
+
return 0;
|
|
10508
|
+
}
|
|
10509
|
+
async function readConnection(deps) {
|
|
10510
|
+
const state = await readDeviceState(deps.dir);
|
|
10511
|
+
const up = deps.up();
|
|
10512
|
+
if (!state)
|
|
10513
|
+
return { state, up, device: null, asked: true, provider: null, staleMtu: null };
|
|
9658
10514
|
try {
|
|
9659
|
-
const
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
});
|
|
9666
|
-
if (args.json) {
|
|
9667
|
-
log(JSON.stringify(result, null, 2));
|
|
9668
|
-
return 0;
|
|
10515
|
+
const { devices, mtu } = await deps.request("/api/v1/network/devices");
|
|
10516
|
+
const device = devices.find((d) => d.id === state.deviceId) ?? null;
|
|
10517
|
+
const config = await readConfigMtu(configPath(deps.dir));
|
|
10518
|
+
const staleMtu = config !== null && config !== mtu ? { config, current: mtu } : null;
|
|
10519
|
+
if (staleMtu) {
|
|
10520
|
+
deps.notice(`This machine's tunnel has MTU ${staleMtu.config}, the network now uses ${staleMtu.current}. ` + `Run \`${getCliName()} network connect\` again: until then, large packets other than TCP ` + "can be lost on the way.");
|
|
9669
10521
|
}
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
10522
|
+
return {
|
|
10523
|
+
state,
|
|
10524
|
+
up,
|
|
10525
|
+
device,
|
|
10526
|
+
asked: true,
|
|
10527
|
+
provider: up ? device?.gatewayProvider ?? null : null,
|
|
10528
|
+
staleMtu
|
|
10529
|
+
};
|
|
10530
|
+
} catch (err) {
|
|
10531
|
+
deps.notice(`Could not ask the app about this device: ${message2(err)}`);
|
|
10532
|
+
return {
|
|
10533
|
+
state,
|
|
10534
|
+
up,
|
|
10535
|
+
device: null,
|
|
10536
|
+
asked: false,
|
|
10537
|
+
provider: up ? state.gatewayProvider : null,
|
|
10538
|
+
staleMtu: null
|
|
10539
|
+
};
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10542
|
+
async function runNetworkStatus(deps = defaultDeps()) {
|
|
10543
|
+
const { state, up, device, asked, provider, staleMtu } = await readConnection(deps);
|
|
10544
|
+
const staleSplitDns = up ? [] : await staleResolverFiles(deps);
|
|
10545
|
+
for (const file of staleSplitDns) {
|
|
10546
|
+
deps.notice(`Split DNS from an earlier connection is still set (${file}): names of the private network will hang. Run: ${getCliName()} network disconnect`);
|
|
10547
|
+
}
|
|
10548
|
+
printResult({ connected: up, device, provider, staleMtu, staleSplitDns }, () => {
|
|
10549
|
+
if (!state) {
|
|
10550
|
+
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
10551
|
+
}
|
|
10552
|
+
const through = provider ? ` through ${provider.name}` : "";
|
|
10553
|
+
const lines = [
|
|
10554
|
+
eventLine("item", `${up ? `Connected${through}` : "Not connected"}: ${state.address}`)
|
|
10555
|
+
];
|
|
10556
|
+
if (device) {
|
|
10557
|
+
lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
|
|
10558
|
+
lines.push(` ${deviceStateText(device.state)}.`);
|
|
10559
|
+
if (device.shortNameOrganization) {
|
|
10560
|
+
lines.push(` Short names: ${device.shortNameOrganization.slug}'s sandboxes.`);
|
|
10561
|
+
}
|
|
10562
|
+
if (!device.gatewayProvider) {
|
|
10563
|
+
lines.push(` Its provider is gone, so no gateway serves it. Run \`${getCliName()} network connect\`.`);
|
|
10564
|
+
}
|
|
10565
|
+
} else if (asked) {
|
|
10566
|
+
lines.push(` The app no longer knows this device (removed, expired or logged out). Run \`${getCliName()} network connect\`.`);
|
|
9675
10567
|
}
|
|
10568
|
+
if (provider)
|
|
10569
|
+
lines.push(` Reachable: the sandboxes on ${provider.name}.`);
|
|
10570
|
+
return lines;
|
|
10571
|
+
}, deps.log);
|
|
10572
|
+
return 0;
|
|
10573
|
+
}
|
|
10574
|
+
function unreachableNote(sandbox, connected) {
|
|
10575
|
+
if (!sandbox.provider.privateNetwork) {
|
|
10576
|
+
return `not reachable: ${sandbox.provider.name} has no private network yet`;
|
|
10577
|
+
}
|
|
10578
|
+
if (connected?.id === sandbox.provider.id)
|
|
10579
|
+
return null;
|
|
10580
|
+
return `not reachable: on ${sandbox.provider.name} → ${connectCommandFor(sandbox.provider)}`;
|
|
10581
|
+
}
|
|
10582
|
+
async function runNetworkLs(args, deps = defaultDeps()) {
|
|
10583
|
+
try {
|
|
10584
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10585
|
+
const connection = await readConnection(deps);
|
|
10586
|
+
const deviceId = connection.device?.id;
|
|
10587
|
+
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes${deviceId ? `?device=${encodeURIComponent(deviceId)}` : ""}`);
|
|
10588
|
+
const connected = connection.provider;
|
|
10589
|
+
const sandboxes = listed.sandboxes.map((sandbox) => ({
|
|
10590
|
+
...sandbox,
|
|
10591
|
+
reachable: unreachableNote(sandbox, connected) === null
|
|
10592
|
+
}));
|
|
10593
|
+
printResult({ ...listed, connectedProvider: connected, sandboxes }, () => {
|
|
10594
|
+
if (listed.sandboxes.length === 0) {
|
|
10595
|
+
return [eventLine("item", `No sandboxes in ${listed.organization.slug}.`)];
|
|
10596
|
+
}
|
|
10597
|
+
const width = Math.max(...listed.sandboxes.map((s) => s.name.length));
|
|
10598
|
+
const canonicalWidth = Math.max(...listed.sandboxes.map((s) => s.canonicalName.length));
|
|
10599
|
+
const httpsWidth = Math.max(...listed.sandboxes.map((s) => (s.httpsName?.length ?? 0) + 8));
|
|
10600
|
+
const shortWidth = Math.max(...listed.sandboxes.map((s) => (s.shortName?.length ?? 0) + 8));
|
|
10601
|
+
const lines = [
|
|
10602
|
+
connected ? eventLine("item", `Connected through ${connected.name}.`) : eventLine("item", `Not connected: nothing is reachable. Run \`${getCliName()} network connect [provider]\`.`)
|
|
10603
|
+
];
|
|
10604
|
+
for (const s of listed.sandboxes) {
|
|
10605
|
+
const note = unreachableNote(s, connected);
|
|
10606
|
+
lines.push([
|
|
10607
|
+
` ${s.name.padEnd(width)}`,
|
|
10608
|
+
(s.address ?? "-").padEnd(28),
|
|
10609
|
+
s.canonicalName.padEnd(canonicalWidth),
|
|
10610
|
+
...s.httpsName ? [`https://${s.httpsName}`.padEnd(httpsWidth)] : [],
|
|
10611
|
+
...s.shortName ? [`https://${s.shortName}`.padEnd(shortWidth)] : [],
|
|
10612
|
+
s.status,
|
|
10613
|
+
...note ? [`(${note})`] : []
|
|
10614
|
+
].join(" "));
|
|
10615
|
+
}
|
|
10616
|
+
return lines;
|
|
10617
|
+
}, deps.log);
|
|
9676
10618
|
return 0;
|
|
9677
10619
|
} catch (err) {
|
|
9678
|
-
|
|
10620
|
+
deps.error(message2(err));
|
|
10621
|
+
return 1;
|
|
9679
10622
|
}
|
|
9680
10623
|
}
|
|
9681
|
-
async function
|
|
10624
|
+
async function runNetworkDevices(deps = defaultDeps()) {
|
|
9682
10625
|
try {
|
|
9683
|
-
await
|
|
9684
|
-
|
|
9685
|
-
|
|
10626
|
+
const { devices } = await deps.request("/api/v1/network/devices");
|
|
10627
|
+
const own = (await readDeviceState(deps.dir))?.deviceId;
|
|
10628
|
+
printResult({ devices }, () => devices.length === 0 ? [eventLine("item", "No devices.")] : devices.map((d) => ` ${d.id === own ? "*" : " "} ${d.id} ${d.name} ${d.address} until ${d.expiresAt.slice(0, 10)} ${deviceStateText(d.state)}`), deps.log);
|
|
10629
|
+
return 0;
|
|
10630
|
+
} catch (err) {
|
|
10631
|
+
deps.error(message2(err));
|
|
10632
|
+
return 1;
|
|
10633
|
+
}
|
|
10634
|
+
}
|
|
10635
|
+
async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
|
|
10636
|
+
try {
|
|
10637
|
+
await deps.request(`/api/v1/network/devices/${encodeURIComponent(args.id)}`, {
|
|
10638
|
+
method: "DELETE"
|
|
9686
10639
|
});
|
|
9687
|
-
|
|
10640
|
+
printResult({ removed: args.id }, () => [eventLine("done", `Removed device ${args.id}.`)], deps.log);
|
|
9688
10641
|
return 0;
|
|
9689
10642
|
} catch (err) {
|
|
9690
|
-
|
|
10643
|
+
deps.error(message2(err));
|
|
10644
|
+
return 1;
|
|
9691
10645
|
}
|
|
9692
10646
|
}
|
|
9693
|
-
var
|
|
9694
|
-
type: "positional",
|
|
9695
|
-
description: "Environment variable name.",
|
|
9696
|
-
required: true
|
|
9697
|
-
};
|
|
9698
|
-
var envCommand = defineCommand({
|
|
10647
|
+
var connectCommand = defineCommand({
|
|
9699
10648
|
meta: {
|
|
9700
|
-
name: "
|
|
9701
|
-
description: "
|
|
10649
|
+
name: "connect",
|
|
10650
|
+
description: "Put this machine into the private network through one provider: WireGuard and split DNS. The organization (--org, else the default) becomes this machine's short-name organization. Also re-arms a paused device: with the tunnel up and nothing changed, without sudo."
|
|
9702
10651
|
},
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
}));
|
|
9714
|
-
}
|
|
9715
|
-
}),
|
|
9716
|
-
get: defineCommand({
|
|
9717
|
-
meta: {
|
|
9718
|
-
name: "get",
|
|
9719
|
-
description: "Print one variable value in full (secrets cannot be read)."
|
|
9720
|
-
},
|
|
9721
|
-
args: {
|
|
9722
|
-
name: nameArg,
|
|
9723
|
-
org: orgArg
|
|
9724
|
-
},
|
|
9725
|
-
async run({ args }) {
|
|
9726
|
-
process.exitCode = await withOrganization(args.org, (organization) => runEnvGet({
|
|
9727
|
-
organization,
|
|
9728
|
-
name: args.name,
|
|
9729
|
-
json: isJsonOutput()
|
|
9730
|
-
}));
|
|
9731
|
-
}
|
|
9732
|
-
}),
|
|
9733
|
-
set: defineCommand({
|
|
9734
|
-
meta: { name: "set", description: "Create or replace a variable or secret." },
|
|
9735
|
-
args: {
|
|
9736
|
-
name: nameArg,
|
|
9737
|
-
org: orgArg,
|
|
9738
|
-
value: { type: "string", description: "Value to store." },
|
|
9739
|
-
"value-stdin": {
|
|
9740
|
-
type: "boolean",
|
|
9741
|
-
description: "Read the value from stdin — keeps secrets out of shell history.",
|
|
9742
|
-
default: false
|
|
9743
|
-
},
|
|
9744
|
-
secret: {
|
|
9745
|
-
type: "boolean",
|
|
9746
|
-
description: "Shorthand for --kind secret. Required when creating a secret.",
|
|
9747
|
-
default: false
|
|
9748
|
-
},
|
|
9749
|
-
kind: {
|
|
9750
|
-
type: "string",
|
|
9751
|
-
description: "variable or secret. Required when creating; on an update, omit to keep the existing kind or pass the other to convert."
|
|
9752
|
-
},
|
|
9753
|
-
comment: { type: "string", description: "Note about what this value is for." }
|
|
9754
|
-
},
|
|
9755
|
-
async run({ args }) {
|
|
9756
|
-
process.exitCode = await withOrganization(args.org, (organization) => runEnvSet({
|
|
9757
|
-
organization,
|
|
9758
|
-
name: args.name,
|
|
9759
|
-
value: args.value,
|
|
9760
|
-
valueStdin: args["value-stdin"],
|
|
9761
|
-
secret: args.secret,
|
|
9762
|
-
kind: args.kind,
|
|
9763
|
-
comment: args.comment,
|
|
9764
|
-
json: isJsonOutput()
|
|
9765
|
-
}));
|
|
9766
|
-
}
|
|
9767
|
-
}),
|
|
9768
|
-
delete: defineCommand({
|
|
9769
|
-
meta: { name: "delete", description: "Delete a variable or secret." },
|
|
9770
|
-
args: {
|
|
9771
|
-
name: nameArg,
|
|
9772
|
-
org: orgArg
|
|
9773
|
-
},
|
|
9774
|
-
async run({ args }) {
|
|
9775
|
-
process.exitCode = await withOrganization(args.org, (organization) => runEnvDelete({
|
|
9776
|
-
organization,
|
|
9777
|
-
name: args.name
|
|
9778
|
-
}));
|
|
9779
|
-
}
|
|
9780
|
-
})
|
|
9781
|
-
}
|
|
9782
|
-
});
|
|
9783
|
-
|
|
9784
|
-
// src/container-picker.ts
|
|
9785
|
-
import { createInterface } from "node:readline/promises";
|
|
9786
|
-
function notRunningContainerOf(error) {
|
|
9787
|
-
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
9788
|
-
return null;
|
|
9789
|
-
const { code, params } = error.data;
|
|
9790
|
-
if (code !== "container_not_running")
|
|
9791
|
-
return null;
|
|
9792
|
-
const container = params && typeof params === "object" ? params : {};
|
|
9793
|
-
return typeof container.container === "string" && container.container ? container.container : null;
|
|
9794
|
-
}
|
|
9795
|
-
function containerQuestionOf(error) {
|
|
9796
|
-
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
9797
|
-
return null;
|
|
9798
|
-
const { code, params } = error.data;
|
|
9799
|
-
if (code !== "container_choice_required" && code !== "container_default_missing")
|
|
9800
|
-
return null;
|
|
9801
|
-
const record = params && typeof params === "object" ? params : {};
|
|
9802
|
-
const running = typeof record.containers === "string" ? record.containers.split(",").map((name) => name.trim()).filter(Boolean) : [];
|
|
9803
|
-
if (running.length === 0)
|
|
9804
|
-
return null;
|
|
9805
|
-
return {
|
|
9806
|
-
running,
|
|
9807
|
-
...code === "container_default_missing" && typeof record.container === "string" ? { missingDefault: record.container } : {}
|
|
9808
|
-
};
|
|
9809
|
-
}
|
|
9810
|
-
var say = (line) => process.stderr.write(`${line}
|
|
9811
|
-
`);
|
|
9812
|
-
var chooseContainerOnTerminal = async ({ running, missingDefault }) => {
|
|
9813
|
-
if (missingDefault) {
|
|
9814
|
-
say(`
|
|
9815
|
-
The default container ${missingDefault} is gone.`);
|
|
9816
|
-
}
|
|
9817
|
-
say(missingDefault ? "Which container?" : `
|
|
9818
|
-
Several containers are running. Which one?`);
|
|
9819
|
-
running.forEach((name, index) => {
|
|
9820
|
-
say(` ${index + 1}. ${name}`);
|
|
9821
|
-
});
|
|
9822
|
-
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
9823
|
-
try {
|
|
9824
|
-
const answer = (await prompt.question("Number (Enter to cancel): ")).trim();
|
|
9825
|
-
const index = Number.parseInt(answer, 10) - 1;
|
|
9826
|
-
return Number.isInteger(index) && index >= 0 && index < running.length ? running[index] ?? null : null;
|
|
9827
|
-
} finally {
|
|
9828
|
-
prompt.close();
|
|
9829
|
-
}
|
|
9830
|
-
};
|
|
9831
|
-
var confirmDefaultOnTerminal = async (container) => {
|
|
9832
|
-
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
9833
|
-
try {
|
|
9834
|
-
const answer = (await prompt.question(`Make ${container} the default? [y/N] `)).trim();
|
|
9835
|
-
return /^y(es)?$/i.test(answer);
|
|
9836
|
-
} finally {
|
|
9837
|
-
prompt.close();
|
|
9838
|
-
}
|
|
9839
|
-
};
|
|
9840
|
-
var confirmStartOnTerminal = async (container) => {
|
|
9841
|
-
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
9842
|
-
try {
|
|
9843
|
-
const answer = (await prompt.question(`Container ${container} is not running. Start it? [Y/n] `)).trim();
|
|
9844
|
-
return answer === "" || /^y(es)?$/i.test(answer);
|
|
9845
|
-
} finally {
|
|
9846
|
-
prompt.close();
|
|
9847
|
-
}
|
|
9848
|
-
};
|
|
9849
|
-
function defaultContainerPickers() {
|
|
9850
|
-
return process.stdin.isTTY && process.stderr.isTTY ? {
|
|
9851
|
-
choose: chooseContainerOnTerminal,
|
|
9852
|
-
confirmDefault: confirmDefaultOnTerminal,
|
|
9853
|
-
confirmStart: confirmStartOnTerminal
|
|
9854
|
-
} : undefined;
|
|
9855
|
-
}
|
|
9856
|
-
|
|
9857
|
-
// src/progress.ts
|
|
9858
|
-
var PROGRESS_FRAMES = ["▂▄▆", "▄▆█", "▆█▆", "█▆▄", "▆▄▂", "▄▂▄"];
|
|
9859
|
-
var FRAME_MS = 120;
|
|
9860
|
-
function formatElapsed(ms) {
|
|
9861
|
-
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
9862
|
-
}
|
|
9863
|
-
function startProgress(message, options = {}) {
|
|
9864
|
-
const stream = options.stream ?? process.stderr;
|
|
9865
|
-
const now = options.now ?? Date.now;
|
|
9866
|
-
const started = now();
|
|
9867
|
-
const elapsed = () => formatElapsed(now() - started);
|
|
9868
|
-
if (isJsonOutput())
|
|
9869
|
-
return { stop: elapsed };
|
|
9870
|
-
const animate = options.animate ?? stream.isTTY === true;
|
|
9871
|
-
if (!animate) {
|
|
9872
|
-
stream.write(` ${SYMBOLS.progress} ${message}
|
|
9873
|
-
`);
|
|
9874
|
-
return { stop: elapsed };
|
|
9875
|
-
}
|
|
9876
|
-
const color = colorEnabled(stream);
|
|
9877
|
-
let frame = 0;
|
|
9878
|
-
const draw = () => {
|
|
9879
|
-
const bars = accent(PROGRESS_FRAMES[frame % PROGRESS_FRAMES.length] ?? SYMBOLS.progress, color);
|
|
9880
|
-
stream.write(`\r\x1B[2K ${bars} ${message} ${dim(elapsed(), color)}`);
|
|
9881
|
-
frame += 1;
|
|
9882
|
-
};
|
|
9883
|
-
const restore = () => stream.write("\r\x1B[2K\x1B[?25h");
|
|
9884
|
-
const onInterrupt = () => {
|
|
9885
|
-
restore();
|
|
9886
|
-
process.exit(130);
|
|
9887
|
-
};
|
|
9888
|
-
stream.write("\x1B[?25l");
|
|
9889
|
-
draw();
|
|
9890
|
-
const timer = setInterval(draw, FRAME_MS);
|
|
9891
|
-
timer.unref?.();
|
|
9892
|
-
process.once("SIGINT", onInterrupt);
|
|
9893
|
-
let stopped = false;
|
|
9894
|
-
return {
|
|
9895
|
-
stop() {
|
|
9896
|
-
if (!stopped) {
|
|
9897
|
-
stopped = true;
|
|
9898
|
-
clearInterval(timer);
|
|
9899
|
-
process.off("SIGINT", onInterrupt);
|
|
9900
|
-
restore();
|
|
9901
|
-
}
|
|
9902
|
-
return elapsed();
|
|
9903
|
-
}
|
|
9904
|
-
};
|
|
9905
|
-
}
|
|
9906
|
-
async function withProgress(message, work, options) {
|
|
9907
|
-
const progress = startProgress(message, options);
|
|
9908
|
-
try {
|
|
9909
|
-
const value = await work();
|
|
9910
|
-
return { value, elapsed: progress.stop() };
|
|
9911
|
-
} catch (err) {
|
|
9912
|
-
progress.stop();
|
|
9913
|
-
throw err;
|
|
9914
|
-
}
|
|
9915
|
-
}
|
|
9916
|
-
|
|
9917
|
-
// src/container-flow.ts
|
|
9918
|
-
async function withContainerPick(call, send, pickers, notice) {
|
|
9919
|
-
try {
|
|
9920
|
-
return await send();
|
|
9921
|
-
} catch (err) {
|
|
9922
|
-
const question = containerQuestionOf(err);
|
|
9923
|
-
if (!pickers || !question || !call.canPick)
|
|
9924
|
-
throw err;
|
|
9925
|
-
const container = await pickers.choose(question);
|
|
9926
|
-
if (!container)
|
|
9927
|
-
throw err;
|
|
9928
|
-
call.setContainer(container);
|
|
9929
|
-
const result = await send();
|
|
9930
|
-
if (call.sandboxId !== undefined && await pickers.confirmDefault(container)) {
|
|
9931
|
-
try {
|
|
9932
|
-
await authorizedRequest("/api/v1/tools/sandboxes_default_container", {
|
|
9933
|
-
method: "POST",
|
|
9934
|
-
body: JSON.stringify({
|
|
9935
|
-
organization: call.organization,
|
|
9936
|
-
sandboxId: call.sandboxId,
|
|
9937
|
-
container
|
|
9938
|
-
})
|
|
9939
|
-
});
|
|
9940
|
-
notice(eventLine("done", `Default container: ${container}.`));
|
|
9941
|
-
} catch (err) {
|
|
9942
|
-
notice(eventLine("failed", `The default container was not set: ${err instanceof Error ? err.message : String(err)}`));
|
|
9943
|
-
}
|
|
9944
|
-
}
|
|
9945
|
-
return result;
|
|
9946
|
-
}
|
|
9947
|
-
}
|
|
9948
|
-
async function withContainerStart(call, send, start, pickers, notice) {
|
|
9949
|
-
try {
|
|
9950
|
-
return await send();
|
|
9951
|
-
} catch (err) {
|
|
9952
|
-
const container = notRunningContainerOf(err);
|
|
9953
|
-
if (!container || call.sandboxId === undefined)
|
|
9954
|
-
throw err;
|
|
9955
|
-
if (!start && !(pickers && await pickers.confirmStart(container)))
|
|
9956
|
-
throw err;
|
|
9957
|
-
await withProgress("Starting container…", () => authorizedRequest("/api/v1/tools/sandboxes_containers_start", {
|
|
9958
|
-
method: "POST",
|
|
9959
|
-
body: JSON.stringify({
|
|
9960
|
-
organization: call.organization,
|
|
9961
|
-
sandboxId: call.sandboxId,
|
|
9962
|
-
container
|
|
9963
|
-
})
|
|
9964
|
-
}));
|
|
9965
|
-
notice(eventLine("done", `Started ${container}.`));
|
|
9966
|
-
return await send();
|
|
9967
|
-
}
|
|
9968
|
-
}
|
|
9969
|
-
|
|
9970
|
-
// src/container-logs.ts
|
|
9971
|
-
var FOLLOW_INTERVAL_MS = 2000;
|
|
9972
|
-
function defaultLogsDeps(follow) {
|
|
9973
|
-
let stop = false;
|
|
9974
|
-
if (follow) {
|
|
9975
|
-
process.once("SIGINT", () => {
|
|
9976
|
-
stop = true;
|
|
9977
|
-
});
|
|
9978
|
-
}
|
|
9979
|
-
return {
|
|
9980
|
-
request: (input) => authorizedRequest("/api/v1/tools/sandboxes_containers_logs", {
|
|
9981
|
-
method: "POST",
|
|
9982
|
-
body: JSON.stringify(input)
|
|
9983
|
-
}),
|
|
9984
|
-
out: (line) => process.stdout.write(`${line}
|
|
9985
|
-
`),
|
|
9986
|
-
err: (line) => process.stderr.write(`${line}
|
|
9987
|
-
`),
|
|
9988
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
9989
|
-
stopped: () => stop
|
|
9990
|
-
};
|
|
9991
|
-
}
|
|
9992
|
-
async function runContainerLogs(input, options, deps = defaultLogsDeps(options.follow)) {
|
|
9993
|
-
const json = isJsonOutput();
|
|
9994
|
-
const color = colorEnabled(process.stdout);
|
|
9995
|
-
const print = (line) => {
|
|
9996
|
-
if (json) {
|
|
9997
|
-
deps.out(JSON.stringify(line));
|
|
9998
|
-
return;
|
|
9999
|
-
}
|
|
10000
|
-
const text = options.timestamps && line.time ? `${dim(line.time, color)} ${line.text}` : line.text;
|
|
10001
|
-
(line.stream === "stderr" ? deps.err : deps.out)(text);
|
|
10002
|
-
};
|
|
10003
|
-
let result = await deps.request(input);
|
|
10004
|
-
if (!result.running) {
|
|
10005
|
-
deps.err("The sandbox is stopped: it has no logs to show.");
|
|
10006
|
-
return 1;
|
|
10007
|
-
}
|
|
10008
|
-
let last = null;
|
|
10009
|
-
for (;; ) {
|
|
10010
|
-
for (const line of result.lines) {
|
|
10011
|
-
print(line);
|
|
10012
|
-
if (line.time)
|
|
10013
|
-
last = line.time;
|
|
10014
|
-
}
|
|
10015
|
-
if (!options.follow) {
|
|
10016
|
-
if (result.truncated && json) {
|
|
10017
|
-
deps.out(JSON.stringify({ truncated: true }));
|
|
10018
|
-
} else if (result.truncated) {
|
|
10019
|
-
deps.err(dim("(older lines left out: use --tail for more)", colorEnabled(process.stderr)));
|
|
10020
|
-
}
|
|
10021
|
-
return 0;
|
|
10022
|
-
}
|
|
10023
|
-
if (!result.truncated)
|
|
10024
|
-
await deps.sleep(FOLLOW_INTERVAL_MS);
|
|
10025
|
-
if (deps.stopped())
|
|
10026
|
-
return 0;
|
|
10027
|
-
const { tail: _tail, ...rest } = input;
|
|
10028
|
-
try {
|
|
10029
|
-
result = await deps.request(last ? { ...rest, after: last } : input);
|
|
10030
|
-
} catch (error) {
|
|
10031
|
-
deps.err(red(error instanceof Error ? error.message : String(error), colorEnabled(process.stderr)));
|
|
10032
|
-
return 1;
|
|
10033
|
-
}
|
|
10034
|
-
if (!result.running) {
|
|
10035
|
-
deps.err("The sandbox stopped.");
|
|
10036
|
-
return 0;
|
|
10037
|
-
}
|
|
10038
|
-
}
|
|
10039
|
-
}
|
|
10040
|
-
|
|
10041
|
-
// src/human.ts
|
|
10042
|
-
var ID_KEYS = [
|
|
10043
|
-
"sandboxId",
|
|
10044
|
-
"siteId",
|
|
10045
|
-
"projectId",
|
|
10046
|
-
"deploymentId",
|
|
10047
|
-
"imageId",
|
|
10048
|
-
"snapshotId",
|
|
10049
|
-
"urlId",
|
|
10050
|
-
"mcpId",
|
|
10051
|
-
"commandId",
|
|
10052
|
-
"sessionId",
|
|
10053
|
-
"sandboxProviderId",
|
|
10054
|
-
"id"
|
|
10055
|
-
];
|
|
10056
|
-
var LABEL_KEYS = ["displayName", "name", "title", "slug", "label", "path", "uri", "ref"];
|
|
10057
|
-
var STATUS_KEYS = ["status", "outcome", "state"];
|
|
10058
|
-
var URL_KEYS = ["url", "liveUrl", "publicUrl", "sshCommand"];
|
|
10059
|
-
function isItem(value) {
|
|
10060
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10061
|
-
}
|
|
10062
|
-
function isScalar(value) {
|
|
10063
|
-
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
10064
|
-
}
|
|
10065
|
-
function firstKey(item, keys) {
|
|
10066
|
-
return keys.find((key) => {
|
|
10067
|
-
const value = item[key];
|
|
10068
|
-
return typeof value === "string" && value.trim() !== "";
|
|
10069
|
-
});
|
|
10070
|
-
}
|
|
10071
|
-
function firstString(item, keys) {
|
|
10072
|
-
const key = firstKey(item, keys);
|
|
10073
|
-
return key ? item[key] : undefined;
|
|
10074
|
-
}
|
|
10075
|
-
function symbolForStatus(status) {
|
|
10076
|
-
const s = (status ?? "").toLowerCase();
|
|
10077
|
-
if (/(fail|error|denied)/.test(s))
|
|
10078
|
-
return "failed";
|
|
10079
|
-
if (/^(running|active|ready|deployed|published|online|verified|ok)$/.test(s))
|
|
10080
|
-
return "ready";
|
|
10081
|
-
if (/^(stopped|deleted|unpublished|vanished|archived|revoked|closed|killed|exited|dead|paused)$/.test(s)) {
|
|
10082
|
-
return "stopped";
|
|
10083
|
-
}
|
|
10084
|
-
if (/(pending|starting|spawning|stopping|building|progress|awaiting|queued|creating|restarting|removing)/.test(s)) {
|
|
10085
|
-
return "progress";
|
|
10086
|
-
}
|
|
10087
|
-
return "item";
|
|
10088
|
-
}
|
|
10089
|
-
function isSandbox(item) {
|
|
10090
|
-
return typeof item.sandboxId === "string" && typeof item.name === "string" && !("id" in item);
|
|
10091
|
-
}
|
|
10092
|
-
function lineKeys(item) {
|
|
10093
|
-
if (isSandbox(item)) {
|
|
10094
|
-
return { id: "name", label: firstKey(item, ["displayName"]) };
|
|
10095
|
-
}
|
|
10096
|
-
const idKeys = "id" in item ? ID_KEYS.filter((key) => key !== "sandboxId") : ID_KEYS;
|
|
10097
|
-
return { id: firstKey(item, idKeys), label: firstKey(item, LABEL_KEYS) };
|
|
10098
|
-
}
|
|
10099
|
-
function shortId(id) {
|
|
10100
|
-
return /^[0-9a-f]{64}$/.test(id) ? id.slice(0, 12) : id;
|
|
10101
|
-
}
|
|
10102
|
-
function itemLine(item, color) {
|
|
10103
|
-
const keys = lineKeys(item);
|
|
10104
|
-
const id = keys.id ? item[keys.id] : undefined;
|
|
10105
|
-
const label = keys.label ? item[keys.label] : undefined;
|
|
10106
|
-
const status = firstString(item, STATUS_KEYS);
|
|
10107
|
-
const url = firstString(item, URL_KEYS);
|
|
10108
|
-
const parts = [label ?? id ?? "(unnamed)"];
|
|
10109
|
-
if (label && id && id !== label)
|
|
10110
|
-
parts.push(dim(shortId(id), color));
|
|
10111
|
-
if (status)
|
|
10112
|
-
parts.push(status);
|
|
10113
|
-
if (url)
|
|
10114
|
-
parts.push(`${SYMBOLS.url} ${url}`);
|
|
10115
|
-
return eventLine(symbolForStatus(status), parts.join(" "), color);
|
|
10116
|
-
}
|
|
10117
|
-
function scalarText(value) {
|
|
10118
|
-
if (value === null)
|
|
10119
|
-
return "—";
|
|
10120
|
-
if (isScalar(value))
|
|
10121
|
-
return String(value);
|
|
10122
|
-
if (Array.isArray(value) && value.every(isScalar))
|
|
10123
|
-
return value.join(", ");
|
|
10124
|
-
if (Array.isArray(value))
|
|
10125
|
-
return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
10126
|
-
return "…";
|
|
10127
|
-
}
|
|
10128
|
-
function fieldLines(item, color, skip) {
|
|
10129
|
-
const lines = [];
|
|
10130
|
-
for (const [key, value] of Object.entries(item)) {
|
|
10131
|
-
if (skip.has(key) || value === undefined)
|
|
10132
|
-
continue;
|
|
10133
|
-
if (Array.isArray(value) && value.some(isItem)) {
|
|
10134
|
-
lines.push(` ${dim(`${key}:`, color)}`);
|
|
10135
|
-
for (const entry of value) {
|
|
10136
|
-
lines.push(` ${isItem(entry) ? itemLine(entry, color) : eventLine("item", scalarText(entry), color)}`);
|
|
10137
|
-
}
|
|
10138
|
-
continue;
|
|
10139
|
-
}
|
|
10140
|
-
if (isItem(value)) {
|
|
10141
|
-
for (const [sub, subValue] of Object.entries(value)) {
|
|
10142
|
-
if (subValue === undefined || isItem(subValue))
|
|
10143
|
-
continue;
|
|
10144
|
-
lines.push(` ${dim(`${key}.${sub}:`, color)} ${scalarText(subValue)}`);
|
|
10145
|
-
}
|
|
10146
|
-
continue;
|
|
10147
|
-
}
|
|
10148
|
-
lines.push(` ${dim(`${key}:`, color)} ${scalarText(value)}`);
|
|
10652
|
+
args: {
|
|
10653
|
+
provider: {
|
|
10654
|
+
type: "positional",
|
|
10655
|
+
required: false,
|
|
10656
|
+
description: "Sandbox provider name or id. Omit for the organization's default provider (else the platform default). Only its sandboxes are reachable; connecting through another replaces the connection."
|
|
10657
|
+
},
|
|
10658
|
+
org: orgArg
|
|
10659
|
+
},
|
|
10660
|
+
async run({ args }) {
|
|
10661
|
+
process.exitCode = await runNetworkConnect({ provider: args.provider, org: args.org });
|
|
10149
10662
|
}
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
return isItem(value) && typeof value.ms === "number" && Number.isFinite(value.ms) ? { ms: value.ms } : null;
|
|
10159
|
-
}
|
|
10160
|
-
function bootLines(data, color) {
|
|
10161
|
-
if (!isItem(data) || !isItem(data.boot))
|
|
10162
|
-
return [];
|
|
10163
|
-
const sandbox = bootPhase(data.boot.sandbox);
|
|
10164
|
-
if (!sandbox)
|
|
10165
|
-
return [];
|
|
10166
|
-
const lines = [eventLine("ready", `sandbox started in ${formatElapsed(sandbox.ms)}`, color)];
|
|
10167
|
-
const container = bootPhase(data.boot.container);
|
|
10168
|
-
if (container) {
|
|
10169
|
-
lines.push(eventLine("done", `container started in ${formatElapsed(container.ms)}`, color));
|
|
10663
|
+
});
|
|
10664
|
+
var disconnectCommand = defineCommand({
|
|
10665
|
+
meta: {
|
|
10666
|
+
name: "disconnect",
|
|
10667
|
+
description: "Take this machine out of the private network, and remove split DNS an earlier connection left (macOS)."
|
|
10668
|
+
},
|
|
10669
|
+
async run() {
|
|
10670
|
+
process.exitCode = await runNetworkDisconnect();
|
|
10170
10671
|
}
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
return [eventLine("item", scalarText(data), color)];
|
|
10180
|
-
const entries = Object.entries(data).filter(([, value]) => value !== undefined);
|
|
10181
|
-
if (entries.length === 0 || entries.length === 1 && data.ok === true) {
|
|
10182
|
-
return [eventLine("done", "Done.", color)];
|
|
10672
|
+
});
|
|
10673
|
+
var statusCommand = defineCommand({
|
|
10674
|
+
meta: {
|
|
10675
|
+
name: "status",
|
|
10676
|
+
description: "Whether this machine is connected, through which provider, and as which device."
|
|
10677
|
+
},
|
|
10678
|
+
async run() {
|
|
10679
|
+
process.exitCode = await runNetworkStatus();
|
|
10183
10680
|
}
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10681
|
+
});
|
|
10682
|
+
var lsCommand = defineCommand({
|
|
10683
|
+
meta: {
|
|
10684
|
+
name: "ls",
|
|
10685
|
+
description: "Sandboxes of an organization with their private addresses and names, and which are reachable through the current connection."
|
|
10686
|
+
},
|
|
10687
|
+
args: { org: orgArg },
|
|
10688
|
+
async run({ args }) {
|
|
10689
|
+
process.exitCode = await runNetworkLs({ org: args.org });
|
|
10189
10690
|
}
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
}
|
|
10198
|
-
|
|
10199
|
-
// src/cli-help-text.ts
|
|
10200
|
-
var CLI_HELP_DESCRIPTION_MAX_LENGTH = 80;
|
|
10201
|
-
function cliHelpDescription(description, maxLength = CLI_HELP_DESCRIPTION_MAX_LENGTH) {
|
|
10202
|
-
const oneLine = description.replace(/\s+/g, " ").trim();
|
|
10203
|
-
if (oneLine.length <= maxLength) {
|
|
10204
|
-
return oneLine;
|
|
10691
|
+
});
|
|
10692
|
+
var devicesRemoveCommand = defineCommand({
|
|
10693
|
+
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
10694
|
+
args: {
|
|
10695
|
+
id: { type: "positional", description: "Device id, from `network devices`.", required: true }
|
|
10696
|
+
},
|
|
10697
|
+
async run({ args }) {
|
|
10698
|
+
process.exitCode = await runNetworkDevicesRemove({ id: args.id });
|
|
10205
10699
|
}
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10700
|
+
});
|
|
10701
|
+
var devicesCommand = defineCommand({
|
|
10702
|
+
meta: { name: "devices", description: "Your devices in the private network." },
|
|
10703
|
+
subCommands: { remove: devicesRemoveCommand },
|
|
10704
|
+
async run({ rawArgs }) {
|
|
10705
|
+
if (rawArgs[0] === "remove")
|
|
10706
|
+
return;
|
|
10707
|
+
process.exitCode = await runNetworkDevices();
|
|
10708
|
+
}
|
|
10709
|
+
});
|
|
10710
|
+
var networkCommand = defineCommand({
|
|
10711
|
+
meta: {
|
|
10712
|
+
name: "network",
|
|
10713
|
+
description: "The private network: reach your sandboxes by address and name from this machine."
|
|
10714
|
+
},
|
|
10715
|
+
subCommands: {
|
|
10716
|
+
connect: connectCommand,
|
|
10717
|
+
disconnect: disconnectCommand,
|
|
10718
|
+
status: statusCommand,
|
|
10719
|
+
ls: lsCommand,
|
|
10720
|
+
devices: devicesCommand
|
|
10721
|
+
}
|
|
10722
|
+
});
|
|
10212
10723
|
|
|
10213
|
-
// src/
|
|
10214
|
-
function
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10724
|
+
// src/commands/docker.ts
|
|
10725
|
+
function defaultDeps2() {
|
|
10726
|
+
const dir = networkDir();
|
|
10727
|
+
return {
|
|
10728
|
+
request: authorizedRequest,
|
|
10729
|
+
run: (command, args) => new Promise((resolve, reject) => {
|
|
10730
|
+
const child = spawn2(command, args, { stdio: "inherit" });
|
|
10731
|
+
child.on("error", reject);
|
|
10732
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
10733
|
+
}),
|
|
10734
|
+
output: (command, args) => {
|
|
10735
|
+
const result = spawnSync3(command, args, { encoding: "utf8" });
|
|
10736
|
+
return result.status === 0 ? result.stdout : "";
|
|
10737
|
+
},
|
|
10738
|
+
platform: process.platform,
|
|
10739
|
+
has: (command) => spawnSync3("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
10740
|
+
dir,
|
|
10741
|
+
up: () => tunnelUp(),
|
|
10742
|
+
log: console.log,
|
|
10743
|
+
error: printError,
|
|
10744
|
+
notice: printNotice,
|
|
10745
|
+
exec: execDocker,
|
|
10746
|
+
dockerDir: dockerDirFor(dir),
|
|
10747
|
+
now: () => new Date,
|
|
10748
|
+
env: process.env
|
|
10749
|
+
};
|
|
10221
10750
|
}
|
|
10222
|
-
function
|
|
10223
|
-
|
|
10224
|
-
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
10225
|
-
const group = prop[key];
|
|
10226
|
-
if (!Array.isArray(group))
|
|
10227
|
-
continue;
|
|
10228
|
-
for (const item of group) {
|
|
10229
|
-
if (isJsonSchemaProperty(item))
|
|
10230
|
-
variants.push(item);
|
|
10231
|
-
}
|
|
10232
|
-
}
|
|
10233
|
-
return variants;
|
|
10751
|
+
function message3(err) {
|
|
10752
|
+
return err instanceof Error ? err.message : String(err);
|
|
10234
10753
|
}
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
}
|
|
10240
|
-
return prop.type === "null" ? undefined : prop.type;
|
|
10754
|
+
var DNS_LABEL2 = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
10755
|
+
function noDockerHostReason(sandbox, orgSlug) {
|
|
10756
|
+
if (!sandbox.provider.privateNetwork) {
|
|
10757
|
+
return `${sandbox.name} runs on ${sandbox.provider.name}, which has no private network yet: no Docker context reaches it.`;
|
|
10241
10758
|
}
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
const type = primaryJsonType(variant);
|
|
10245
|
-
if (type)
|
|
10246
|
-
types.add(type);
|
|
10759
|
+
if (!sandbox.address) {
|
|
10760
|
+
return `${sandbox.name} has no address in the private network yet (${sandbox.provider.name} has no host on it).`;
|
|
10247
10761
|
}
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
return "array";
|
|
10252
|
-
if (types.size === 1) {
|
|
10253
|
-
const [only] = types;
|
|
10254
|
-
return only;
|
|
10762
|
+
const notLabel = [sandbox.name, orgSlug].find((name) => !DNS_LABEL2.test(name));
|
|
10763
|
+
if (notLabel) {
|
|
10764
|
+
return `${sandbox.name} has no Docker engine over the network: "${notLabel}" is not a DNS label (lower-case letters, digits and hyphens), so its Docker engine stays closed.`;
|
|
10255
10765
|
}
|
|
10256
|
-
return
|
|
10257
|
-
}
|
|
10258
|
-
function allowsNull(prop) {
|
|
10259
|
-
if (prop.type === "null")
|
|
10260
|
-
return true;
|
|
10261
|
-
if (Array.isArray(prop.type) && prop.type.includes("null"))
|
|
10262
|
-
return true;
|
|
10263
|
-
return schemaVariants(prop).some(allowsNull);
|
|
10766
|
+
return `${sandbox.name} has no Docker engine over the network.`;
|
|
10264
10767
|
}
|
|
10265
|
-
function
|
|
10266
|
-
const
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
if (!prop)
|
|
10272
|
-
continue;
|
|
10273
|
-
const jsonType = primaryJsonType(prop);
|
|
10274
|
-
args[toFlagName(name)] = {
|
|
10275
|
-
type: "positional",
|
|
10276
|
-
description: describe(prop.description ?? name),
|
|
10277
|
-
required: true,
|
|
10278
|
-
valueHint: jsonType
|
|
10279
|
-
};
|
|
10768
|
+
function sandboxWarnings(sandbox, gateway, up) {
|
|
10769
|
+
const warnings = [];
|
|
10770
|
+
if (gateway?.id !== sandbox.provider.id) {
|
|
10771
|
+
warnings.push(`${sandbox.name} runs on ${sandbox.provider.name}, and this machine connects through ${gateway?.name ?? "no provider"}: Docker reaches it only after \`${connectCommandFor(sandbox.provider)}\`.`);
|
|
10772
|
+
} else if (!up) {
|
|
10773
|
+
warnings.push(`This machine is not connected to the private network: run \`${connectCommandFor(sandbox.provider)}\` before using the context.`);
|
|
10280
10774
|
}
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
continue;
|
|
10284
|
-
const jsonType = primaryJsonType(prop);
|
|
10285
|
-
const isBoolean = jsonType === "boolean";
|
|
10286
|
-
const isJsonValue = jsonType === "object" || jsonType === "array";
|
|
10287
|
-
const typeNote = isJsonValue ? ` (JSON ${jsonType})` : "";
|
|
10288
|
-
const isOrganization = name === "organization";
|
|
10289
|
-
const isRequired = required.has(name) && prop.default === undefined && !isOrganization;
|
|
10290
|
-
const defaultNote = isOrganization ? " Defaults to GHOSTDOCK_ORG, then `organizations use`." : "";
|
|
10291
|
-
args[toFlagName(name)] = isBoolean ? {
|
|
10292
|
-
type: "boolean",
|
|
10293
|
-
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
10294
|
-
required: isRequired
|
|
10295
|
-
} : {
|
|
10296
|
-
type: "string",
|
|
10297
|
-
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
10298
|
-
required: isRequired,
|
|
10299
|
-
valueHint: jsonType
|
|
10300
|
-
};
|
|
10775
|
+
if (sandbox.dockerDoor === "none") {
|
|
10776
|
+
warnings.push(`${sandbox.name}: this sandbox's base image is too old for Docker over the network; restart it to update.`);
|
|
10301
10777
|
}
|
|
10302
|
-
return
|
|
10778
|
+
return warnings;
|
|
10303
10779
|
}
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
return
|
|
10780
|
+
var NO_DEVICE = () => `Run ${getCliName()} network connect first: the Docker context uses this machine's device.`;
|
|
10781
|
+
async function runDockerContext(args, deps = defaultDeps2()) {
|
|
10782
|
+
const state = await readDeviceStateFor(deps);
|
|
10783
|
+
if (!state) {
|
|
10784
|
+
deps.error(NO_DEVICE());
|
|
10785
|
+
return 1;
|
|
10310
10786
|
}
|
|
10311
|
-
if (
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10787
|
+
if (!deps.has("docker")) {
|
|
10788
|
+
deps.error("docker is not installed: the context is for Docker's CLI. Install Docker (Docker Desktop, OrbStack, or the docker CLI alone), then run this again.");
|
|
10789
|
+
return 1;
|
|
10790
|
+
}
|
|
10791
|
+
const use = args.use !== false;
|
|
10792
|
+
try {
|
|
10793
|
+
const organization = await requireOrganizationRef(args.org, deps.notice);
|
|
10794
|
+
const connection = await readConnection(deps);
|
|
10795
|
+
if (connection.asked && !connection.device) {
|
|
10796
|
+
deps.error(`The app no longer knows this machine's device (removed, expired or logged out). ${NO_DEVICE()}`);
|
|
10797
|
+
return 1;
|
|
10798
|
+
}
|
|
10799
|
+
const listed = await deps.request(`/api/v1/network/organizations/${encodeURIComponent(organization)}/sandboxes?device=${encodeURIComponent(state.deviceId)}`);
|
|
10800
|
+
const slug = listed.organization.slug;
|
|
10801
|
+
const wanted = args.sandbox.trim().toLowerCase();
|
|
10802
|
+
const sandbox = listed.sandboxes.find((s) => s.name.toLowerCase() === wanted);
|
|
10803
|
+
if (!sandbox) {
|
|
10804
|
+
deps.error(`No sandbox ${args.sandbox} in ${slug}. Run \`${getCliName()} network ls --org ${slug}\` for its sandboxes.`);
|
|
10805
|
+
return 1;
|
|
10806
|
+
}
|
|
10807
|
+
if (!sandbox.dockerHost) {
|
|
10808
|
+
deps.error(noDockerHostReason(sandbox, slug));
|
|
10809
|
+
return 1;
|
|
10315
10810
|
}
|
|
10316
|
-
|
|
10811
|
+
const gateway = connection.device?.gatewayProvider ?? connection.state?.gatewayProvider ?? null;
|
|
10812
|
+
for (const warning of sandboxWarnings(sandbox, gateway, connection.up))
|
|
10813
|
+
deps.notice(warning);
|
|
10814
|
+
const organizationId = listed.organization.id;
|
|
10815
|
+
if (!isOrganizationId(organizationId)) {
|
|
10816
|
+
throw new Error(`The app sent an organization id that is not one: ${organizationId}`);
|
|
10817
|
+
}
|
|
10818
|
+
const files = organizationFiles(deps.dockerDir, organizationId);
|
|
10819
|
+
const { certificate, issued } = await ensureCertificate({ files, organizationId, deviceId: state.deviceId, baseUrl: state.baseUrl, now: deps.now() }, deps.request);
|
|
10820
|
+
const name = dockerContextName(slug, sandbox.name);
|
|
10821
|
+
const { created } = writeContext(deps.exec, {
|
|
10822
|
+
name,
|
|
10823
|
+
description: contextDescription(sandbox.name, slug),
|
|
10824
|
+
host: sandbox.dockerHost,
|
|
10825
|
+
files
|
|
10826
|
+
});
|
|
10827
|
+
await recordContext(deps.dockerDir, { name, organizationId, sandbox: sandbox.name });
|
|
10828
|
+
if (use) {
|
|
10829
|
+
useContext(deps.exec, name);
|
|
10830
|
+
for (const variable of ["DOCKER_HOST", "DOCKER_CONTEXT"]) {
|
|
10831
|
+
if (deps.env[variable]) {
|
|
10832
|
+
deps.notice(`${variable} is set in this shell and wins over \`docker context use\`: unset it, or pass \`docker --context ${name}\`.`);
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
}
|
|
10836
|
+
printResult({
|
|
10837
|
+
context: { name, host: sandbox.dockerHost, created, inUse: use },
|
|
10838
|
+
sandbox: {
|
|
10839
|
+
name: sandbox.name,
|
|
10840
|
+
canonicalName: sandbox.canonicalName,
|
|
10841
|
+
provider: sandbox.provider,
|
|
10842
|
+
dockerDoor: sandbox.dockerDoor
|
|
10843
|
+
},
|
|
10844
|
+
organization: listed.organization,
|
|
10845
|
+
certificate: {
|
|
10846
|
+
serial: certificate.serial,
|
|
10847
|
+
notAfter: certificate.notAfter.toISOString(),
|
|
10848
|
+
issued,
|
|
10849
|
+
files: { ca: files.ca, cert: files.cert, key: files.key }
|
|
10850
|
+
}
|
|
10851
|
+
}, () => [
|
|
10852
|
+
eventLine("done", `Docker context ${name}${use ? " (in use)" : ""}`),
|
|
10853
|
+
` ${sandbox.dockerHost}`,
|
|
10854
|
+
` Certificate for this device, valid until ${day(certificate.notAfter)}.`,
|
|
10855
|
+
` → ${use ? "docker ps" : `docker --context ${name} ps`}`
|
|
10856
|
+
], deps.log);
|
|
10857
|
+
await renewDockerCertificates(deps, { except: organizationId });
|
|
10858
|
+
return 0;
|
|
10859
|
+
} catch (err) {
|
|
10860
|
+
deps.error(message3(err));
|
|
10861
|
+
return 1;
|
|
10317
10862
|
}
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10863
|
+
}
|
|
10864
|
+
async function readDeviceStateFor(deps) {
|
|
10865
|
+
const state = await readDeviceState(deps.dir);
|
|
10866
|
+
return state && state.baseUrl === getBaseUrl() ? state : null;
|
|
10867
|
+
}
|
|
10868
|
+
var contextCommand = defineCommand({
|
|
10869
|
+
meta: {
|
|
10870
|
+
name: "context",
|
|
10871
|
+
description: `Point Docker at a sandbox's Docker engine over the private network: plain \`docker\` and \`docker compose\` then run there. Creates (or updates) the Docker context ghostdock-<org>-<sandbox> for tcp://<sandbox>.<org>.ghostdock.internal:2376 with a client certificate of this machine's device, and makes it the current context. Needs \`${getCliName()} network connect\` first, and Docker's CLI. The certificate lives 30 days; \`network connect\` and this command renew it with 10 days left. An expired certificate fails Docker's TLS handshake with Docker's own error: run this command again. Docker's \`no such host\` on a ghostdock.internal address means this machine is not connected: run \`${getCliName()} network connect\`. After an organization rename the old name stops resolving: run it again to update the host.`
|
|
10872
|
+
},
|
|
10873
|
+
args: {
|
|
10874
|
+
sandbox: {
|
|
10875
|
+
type: "positional",
|
|
10876
|
+
required: true,
|
|
10877
|
+
description: "Sandbox name, from `network ls`."
|
|
10878
|
+
},
|
|
10879
|
+
org: orgArg,
|
|
10880
|
+
use: {
|
|
10881
|
+
type: "boolean",
|
|
10882
|
+
default: true,
|
|
10883
|
+
description: "Make it the current Docker context (`docker context use`). `--no-use` only creates or updates it.",
|
|
10884
|
+
negativeDescription: "Only create or update the context; then pass `docker --context <name>`."
|
|
10325
10885
|
}
|
|
10886
|
+
},
|
|
10887
|
+
async run({ args }) {
|
|
10888
|
+
process.exitCode = await runDockerContext({
|
|
10889
|
+
sandbox: args.sandbox,
|
|
10890
|
+
org: args.org,
|
|
10891
|
+
use: args.use
|
|
10892
|
+
});
|
|
10326
10893
|
}
|
|
10327
|
-
|
|
10894
|
+
});
|
|
10895
|
+
var dockerCommand = defineCommand({
|
|
10896
|
+
meta: {
|
|
10897
|
+
name: "docker",
|
|
10898
|
+
description: "Docker over the private network: a sandbox's Docker engine from this machine."
|
|
10899
|
+
},
|
|
10900
|
+
subCommands: { context: contextCommand }
|
|
10901
|
+
});
|
|
10902
|
+
|
|
10903
|
+
// src/commands/env.ts
|
|
10904
|
+
function reportError(err, error) {
|
|
10905
|
+
if (err instanceof AuthRequiredError || err instanceof Error) {
|
|
10906
|
+
error(err.message);
|
|
10907
|
+
} else {
|
|
10908
|
+
error(String(err));
|
|
10909
|
+
}
|
|
10910
|
+
return 1;
|
|
10328
10911
|
}
|
|
10329
|
-
function
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10912
|
+
function invokeTool(tool, input) {
|
|
10913
|
+
return authorizedRequest(`/api/v1/tools/${tool}`, {
|
|
10914
|
+
method: "POST",
|
|
10915
|
+
body: JSON.stringify(input)
|
|
10916
|
+
});
|
|
10917
|
+
}
|
|
10918
|
+
function displayValue(entry) {
|
|
10919
|
+
if (entry.valueOmittedReason === "secret")
|
|
10920
|
+
return "<secret>";
|
|
10921
|
+
if (entry.valueOmittedReason === "too_large") {
|
|
10922
|
+
return `<${entry.valueLength} chars — read with \`${getCliName()} env get\`>`;
|
|
10338
10923
|
}
|
|
10339
|
-
return
|
|
10924
|
+
return entry.value ?? "";
|
|
10340
10925
|
}
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10926
|
+
function formatTable(entries) {
|
|
10927
|
+
const rows = entries.map((entry) => [
|
|
10928
|
+
entry.name,
|
|
10929
|
+
entry.kind,
|
|
10930
|
+
displayValue(entry),
|
|
10931
|
+
entry.comment ?? ""
|
|
10932
|
+
]);
|
|
10933
|
+
const header = ["NAME", "KIND", "VALUE", "COMMENT"];
|
|
10934
|
+
const widths = header.map((_, column) => Math.max(header[column]?.length ?? 0, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
10935
|
+
const line = (cells) => cells.map((cell, column) => column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
10936
|
+
return [line(header), ...rows.map(line)].join(`
|
|
10937
|
+
`);
|
|
10938
|
+
}
|
|
10939
|
+
async function readStdin() {
|
|
10940
|
+
const chunks = [];
|
|
10941
|
+
for await (const chunk of process.stdin) {
|
|
10942
|
+
chunks.push(Buffer.from(chunk));
|
|
10943
|
+
}
|
|
10944
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
|
|
10945
|
+
}
|
|
10946
|
+
async function runEnvList(args, log = console.log, error = printError) {
|
|
10346
10947
|
try {
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10948
|
+
const result = await invokeTool("env_list", {
|
|
10949
|
+
organization: args.organization
|
|
10950
|
+
});
|
|
10951
|
+
if (args.json) {
|
|
10952
|
+
log(JSON.stringify(result.entries, null, 2));
|
|
10953
|
+
return 0;
|
|
10350
10954
|
}
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
}
|
|
10355
|
-
if (tool.name === "sandboxes_containers_logs") {
|
|
10356
|
-
try {
|
|
10357
|
-
return await runContainerLogs(input, {
|
|
10358
|
-
follow: rawArgs.follow === true,
|
|
10359
|
-
timestamps: rawArgs.timestamps === true
|
|
10360
|
-
});
|
|
10361
|
-
} catch (err) {
|
|
10362
|
-
error(err instanceof Error ? err.message : `${tool.name} failed.`);
|
|
10363
|
-
return 1;
|
|
10955
|
+
if (result.entries.length === 0) {
|
|
10956
|
+
log(" No variables or secrets.");
|
|
10957
|
+
return 0;
|
|
10364
10958
|
}
|
|
10959
|
+
log(formatTable(result.entries));
|
|
10960
|
+
return 0;
|
|
10961
|
+
} catch (err) {
|
|
10962
|
+
return reportError(err, error);
|
|
10365
10963
|
}
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : undefined,
|
|
10369
|
-
canPick: !!properties && "container" in properties && !input.container,
|
|
10370
|
-
setContainer: (container) => {
|
|
10371
|
-
input.container = container;
|
|
10372
|
-
}
|
|
10373
|
-
};
|
|
10964
|
+
}
|
|
10965
|
+
async function runEnvGet(args, log = console.log, error = printError) {
|
|
10374
10966
|
try {
|
|
10375
|
-
const
|
|
10376
|
-
|
|
10377
|
-
|
|
10967
|
+
const result = await invokeTool("env_get", {
|
|
10968
|
+
organization: args.organization,
|
|
10969
|
+
name: args.name
|
|
10378
10970
|
});
|
|
10379
|
-
|
|
10380
|
-
let elapsed;
|
|
10381
|
-
const send = async () => {
|
|
10382
|
-
if (!waitMessage)
|
|
10383
|
-
return post();
|
|
10384
|
-
const timed = await withProgress(waitMessage, post);
|
|
10385
|
-
elapsed = timed.elapsed;
|
|
10386
|
-
return timed.value;
|
|
10387
|
-
};
|
|
10388
|
-
const result = await withContainerPick(call, () => withContainerStart(call, send, rawArgs.start === true, pickers, notice), pickers, notice);
|
|
10389
|
-
printResult(result, () => {
|
|
10390
|
-
const color = colorEnabled(process.stdout);
|
|
10391
|
-
const lines = renderResult(result, color);
|
|
10392
|
-
return elapsed === undefined || bootLines(result, color).length > 0 ? lines : withElapsed(lines, elapsed);
|
|
10393
|
-
}, log);
|
|
10394
|
-
stopNote(result, notice);
|
|
10971
|
+
log(args.json ? JSON.stringify(result.entry, null, 2) : result.entry.value ?? "");
|
|
10395
10972
|
return 0;
|
|
10396
10973
|
} catch (err) {
|
|
10397
|
-
|
|
10398
|
-
return 1;
|
|
10974
|
+
return reportError(err, error);
|
|
10399
10975
|
}
|
|
10400
10976
|
}
|
|
10401
|
-
function
|
|
10402
|
-
if (
|
|
10403
|
-
return;
|
|
10404
|
-
const { action, note, container } = result;
|
|
10405
|
-
if (action !== "stop" || typeof container !== "string")
|
|
10406
|
-
return;
|
|
10407
|
-
if (note === "default") {
|
|
10408
|
-
notice(`${container} is the sandbox's default container: SSH, commands and files ask which container until it runs again.`);
|
|
10409
|
-
} else if (note === "last_running") {
|
|
10410
|
-
notice("No container is running now: SSH, commands and files have none to land in until one runs.");
|
|
10411
|
-
} else if (note === "sandbox_stopped") {
|
|
10412
|
-
notice(`The sandbox is stopped, so ${container} is too: nothing was done.`);
|
|
10977
|
+
function resolveKind(args) {
|
|
10978
|
+
if (args.kind !== undefined && args.kind !== "variable" && args.kind !== "secret") {
|
|
10979
|
+
return { error: `--kind must be "variable" or "secret", got "${args.kind}".` };
|
|
10413
10980
|
}
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
sandboxes_wake: "Waking sandbox…",
|
|
10418
|
-
sandboxes_stop: "Stopping sandbox…",
|
|
10419
|
-
sandboxes_delete: "Deleting sandbox…",
|
|
10420
|
-
sandboxes_resize: "Resizing sandbox…",
|
|
10421
|
-
sandboxes_resize_disk: "Resizing disk…",
|
|
10422
|
-
sandboxes_commands_run: "Running command…",
|
|
10423
|
-
sandboxes_containers_start: "Starting container…",
|
|
10424
|
-
sandboxes_containers_stop: "Stopping container…",
|
|
10425
|
-
sandboxes_containers_restart: "Restarting container…",
|
|
10426
|
-
snapshots_create: "Saving snapshot…",
|
|
10427
|
-
projects_redeploy: "Redeploying…",
|
|
10428
|
-
providers_verify_incus: "Checking the Incus host…"
|
|
10429
|
-
};
|
|
10430
|
-
function withElapsed(lines, elapsed) {
|
|
10431
|
-
const color = colorEnabled(process.stdout);
|
|
10432
|
-
const [first, ...rest] = lines;
|
|
10433
|
-
if (first === undefined || first.startsWith(" ")) {
|
|
10434
|
-
return [eventLine("done", `Done in ${elapsed}.`, color), ...lines];
|
|
10981
|
+
const fromKind = args.kind;
|
|
10982
|
+
if (args.secret && fromKind === "variable") {
|
|
10983
|
+
return { error: "Pass either --secret or --kind variable, not both." };
|
|
10435
10984
|
}
|
|
10436
|
-
return
|
|
10437
|
-
}
|
|
10438
|
-
function cliText(catalog, skills = BUILTIN_SKILL_NAMES) {
|
|
10439
|
-
const paths = new Map(catalog.filter((tool) => tool.cli).map((tool) => [tool.name, tool.cli]));
|
|
10440
|
-
const names = [...paths.keys()].sort((a, b) => b.length - a.length);
|
|
10441
|
-
const pattern = names.length > 0 ? new RegExp(`\\b(${names.join("|")})\\b`, "g") : null;
|
|
10442
|
-
return (text) => {
|
|
10443
|
-
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
10444
|
-
const commands = pattern ? oneLine.replace(pattern, (name) => paths.get(name)?.join(" ") ?? name) : oneLine;
|
|
10445
|
-
return commands.replace(/\bthe ([a-z][a-z0-9-]*) skill\b/g, (phrase, skill) => skills.includes(skill) ? `the ${skill} skill (\`${getCliName()} skills get ${skill}\`)` : phrase);
|
|
10446
|
-
};
|
|
10985
|
+
return { kind: fromKind ?? (args.secret ? "secret" : undefined) };
|
|
10447
10986
|
}
|
|
10448
|
-
function
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
args.start = {
|
|
10453
|
-
type: "boolean",
|
|
10454
|
-
description: "Start the container first when it is not running, without asking."
|
|
10455
|
-
};
|
|
10987
|
+
async function runEnvSet(args, log = console.log, error = printError, stdin = readStdin) {
|
|
10988
|
+
if (args.value !== undefined && args.valueStdin) {
|
|
10989
|
+
error("Pass either --value or --value-stdin, not both.");
|
|
10990
|
+
return 1;
|
|
10456
10991
|
}
|
|
10457
|
-
if (
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10992
|
+
if (args.value === undefined && !args.valueStdin) {
|
|
10993
|
+
error("Provide --value, or --value-stdin to read the value from stdin.");
|
|
10994
|
+
return 1;
|
|
10995
|
+
}
|
|
10996
|
+
const resolved = resolveKind(args);
|
|
10997
|
+
if ("error" in resolved) {
|
|
10998
|
+
error(resolved.error);
|
|
10999
|
+
return 1;
|
|
11000
|
+
}
|
|
11001
|
+
const value = args.valueStdin ? await stdin() : args.value ?? "";
|
|
11002
|
+
try {
|
|
11003
|
+
const result = await invokeTool("env_set", {
|
|
11004
|
+
organization: args.organization,
|
|
11005
|
+
name: args.name,
|
|
11006
|
+
value,
|
|
11007
|
+
...resolved.kind ? { kind: resolved.kind } : {},
|
|
11008
|
+
...args.comment ? { comment: args.comment } : {}
|
|
11009
|
+
});
|
|
11010
|
+
if (args.json) {
|
|
11011
|
+
log(JSON.stringify(result, null, 2));
|
|
11012
|
+
return 0;
|
|
11013
|
+
}
|
|
11014
|
+
log(eventLine("done", `${result.created ? "Created" : "Updated"} ${result.entry.kind} ${result.entry.name}.`));
|
|
11015
|
+
if (result.previousKind === "secret") {
|
|
11016
|
+
log(eventLine("item", `Warning: ${result.entry.name} was a secret and is now a variable — its value is readable by everyone in the organization.`));
|
|
11017
|
+
} else if (result.previousKind === "variable") {
|
|
11018
|
+
log(eventLine("item", `${result.entry.name} was a variable and is now a secret; its value is no longer shown.`));
|
|
11019
|
+
}
|
|
11020
|
+
return 0;
|
|
11021
|
+
} catch (err) {
|
|
11022
|
+
return reportError(err, error);
|
|
10464
11023
|
}
|
|
10465
|
-
return args;
|
|
10466
11024
|
}
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
11025
|
+
async function runEnvDelete(args, log = console.log, error = printError) {
|
|
11026
|
+
try {
|
|
11027
|
+
await invokeTool("env_delete", {
|
|
11028
|
+
organization: args.organization,
|
|
11029
|
+
name: args.name
|
|
11030
|
+
});
|
|
11031
|
+
log(eventLine("done", `Deleted ${args.name}.`));
|
|
11032
|
+
return 0;
|
|
11033
|
+
} catch (err) {
|
|
11034
|
+
return reportError(err, error);
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
var nameArg = {
|
|
11038
|
+
type: "positional",
|
|
11039
|
+
description: "Environment variable name.",
|
|
11040
|
+
required: true
|
|
11041
|
+
};
|
|
11042
|
+
var envCommand = defineCommand({
|
|
11043
|
+
meta: {
|
|
11044
|
+
name: "env",
|
|
11045
|
+
description: "Manage organization environment variables and secrets."
|
|
11046
|
+
},
|
|
11047
|
+
subCommands: {
|
|
11048
|
+
list: defineCommand({
|
|
11049
|
+
meta: { name: "list", description: "List variables and secrets." },
|
|
11050
|
+
args: {
|
|
11051
|
+
org: orgArg
|
|
11052
|
+
},
|
|
11053
|
+
async run({ args }) {
|
|
11054
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvList({
|
|
11055
|
+
organization,
|
|
11056
|
+
json: isJsonOutput()
|
|
11057
|
+
}));
|
|
11058
|
+
}
|
|
11059
|
+
}),
|
|
11060
|
+
get: defineCommand({
|
|
10473
11061
|
meta: {
|
|
10474
|
-
name:
|
|
10475
|
-
description:
|
|
11062
|
+
name: "get",
|
|
11063
|
+
description: "Print one variable value in full (secrets cannot be read)."
|
|
11064
|
+
},
|
|
11065
|
+
args: {
|
|
11066
|
+
name: nameArg,
|
|
11067
|
+
org: orgArg
|
|
10476
11068
|
},
|
|
10477
|
-
args: { ...buildToolArgs(tool.inputSchema, toCliText, tool.cliArgs), ...cliOnlyArgs(tool) },
|
|
10478
11069
|
async run({ args }) {
|
|
10479
|
-
process.exitCode = await
|
|
11070
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvGet({
|
|
11071
|
+
organization,
|
|
11072
|
+
name: args.name,
|
|
11073
|
+
json: isJsonOutput()
|
|
11074
|
+
}));
|
|
10480
11075
|
}
|
|
10481
|
-
})
|
|
10482
|
-
|
|
10483
|
-
|
|
11076
|
+
}),
|
|
11077
|
+
set: defineCommand({
|
|
11078
|
+
meta: { name: "set", description: "Create or replace a variable or secret." },
|
|
11079
|
+
args: {
|
|
11080
|
+
name: nameArg,
|
|
11081
|
+
org: orgArg,
|
|
11082
|
+
value: { type: "string", description: "Value to store." },
|
|
11083
|
+
"value-stdin": {
|
|
11084
|
+
type: "boolean",
|
|
11085
|
+
description: "Read the value from stdin — keeps secrets out of shell history.",
|
|
11086
|
+
default: false
|
|
11087
|
+
},
|
|
11088
|
+
secret: {
|
|
11089
|
+
type: "boolean",
|
|
11090
|
+
description: "Shorthand for --kind secret. Required when creating a secret.",
|
|
11091
|
+
default: false
|
|
11092
|
+
},
|
|
11093
|
+
kind: {
|
|
11094
|
+
type: "string",
|
|
11095
|
+
description: "variable or secret. Required when creating; on an update, omit to keep the existing kind or pass the other to convert."
|
|
11096
|
+
},
|
|
11097
|
+
comment: { type: "string", description: "Note about what this value is for." }
|
|
11098
|
+
},
|
|
11099
|
+
async run({ args }) {
|
|
11100
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvSet({
|
|
11101
|
+
organization,
|
|
11102
|
+
name: args.name,
|
|
11103
|
+
value: args.value,
|
|
11104
|
+
valueStdin: args["value-stdin"],
|
|
11105
|
+
secret: args.secret,
|
|
11106
|
+
kind: args.kind,
|
|
11107
|
+
comment: args.comment,
|
|
11108
|
+
json: isJsonOutput()
|
|
11109
|
+
}));
|
|
11110
|
+
}
|
|
11111
|
+
}),
|
|
11112
|
+
delete: defineCommand({
|
|
11113
|
+
meta: { name: "delete", description: "Delete a variable or secret." },
|
|
11114
|
+
args: {
|
|
11115
|
+
name: nameArg,
|
|
11116
|
+
org: orgArg
|
|
11117
|
+
},
|
|
11118
|
+
async run({ args }) {
|
|
11119
|
+
process.exitCode = await withOrganization(args.org, (organization) => runEnvDelete({
|
|
11120
|
+
organization,
|
|
11121
|
+
name: args.name
|
|
11122
|
+
}));
|
|
11123
|
+
}
|
|
11124
|
+
})
|
|
10484
11125
|
}
|
|
10485
|
-
|
|
10486
|
-
}
|
|
10487
|
-
|
|
10488
|
-
// src/commands/login.ts
|
|
10489
|
-
import { hostname } from "node:os";
|
|
11126
|
+
});
|
|
10490
11127
|
|
|
10491
|
-
// src/
|
|
10492
|
-
import { createInterface
|
|
10493
|
-
function
|
|
10494
|
-
|
|
11128
|
+
// src/container-picker.ts
|
|
11129
|
+
import { createInterface } from "node:readline/promises";
|
|
11130
|
+
function notRunningContainerOf(error) {
|
|
11131
|
+
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
11132
|
+
return null;
|
|
11133
|
+
const { code, params } = error.data;
|
|
11134
|
+
if (code !== "container_not_running")
|
|
11135
|
+
return null;
|
|
11136
|
+
const container = params && typeof params === "object" ? params : {};
|
|
11137
|
+
return typeof container.container === "string" && container.container ? container.container : null;
|
|
10495
11138
|
}
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
11139
|
+
function containerQuestionOf(error) {
|
|
11140
|
+
if (!(error instanceof ApiError) || !error.data || typeof error.data !== "object")
|
|
11141
|
+
return null;
|
|
11142
|
+
const { code, params } = error.data;
|
|
11143
|
+
if (code !== "container_choice_required" && code !== "container_default_missing")
|
|
11144
|
+
return null;
|
|
11145
|
+
const record = params && typeof params === "object" ? params : {};
|
|
11146
|
+
const running = typeof record.containers === "string" ? record.containers.split(",").map((name) => name.trim()).filter(Boolean) : [];
|
|
11147
|
+
if (running.length === 0)
|
|
11148
|
+
return null;
|
|
11149
|
+
return {
|
|
11150
|
+
running,
|
|
11151
|
+
...code === "container_default_missing" && typeof record.container === "string" ? { missingDefault: record.container } : {}
|
|
11152
|
+
};
|
|
11153
|
+
}
|
|
11154
|
+
var say = (line) => process.stderr.write(`${line}
|
|
11155
|
+
`);
|
|
11156
|
+
var chooseContainerOnTerminal = async ({ running, missingDefault }) => {
|
|
11157
|
+
if (missingDefault) {
|
|
11158
|
+
say(`
|
|
11159
|
+
The default container ${missingDefault} is gone.`);
|
|
11160
|
+
}
|
|
11161
|
+
say(missingDefault ? "Which container?" : `
|
|
11162
|
+
Several containers are running. Which one?`);
|
|
11163
|
+
running.forEach((name, index) => {
|
|
11164
|
+
say(` ${index + 1}. ${name}`);
|
|
10501
11165
|
});
|
|
10502
|
-
const prompt =
|
|
11166
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
10503
11167
|
try {
|
|
10504
|
-
const answer = (await prompt.question(
|
|
11168
|
+
const answer = (await prompt.question("Number (Enter to cancel): ")).trim();
|
|
10505
11169
|
const index = Number.parseInt(answer, 10) - 1;
|
|
10506
|
-
return Number.isInteger(index) && index >= 0 && index <
|
|
11170
|
+
return Number.isInteger(index) && index >= 0 && index < running.length ? running[index] ?? null : null;
|
|
10507
11171
|
} finally {
|
|
10508
11172
|
prompt.close();
|
|
10509
11173
|
}
|
|
10510
11174
|
};
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
}
|
|
10514
|
-
async function chooseDefaultOrganizationAfterLogin(org, log, choose = defaultChooser()) {
|
|
10515
|
-
const cli = getCliName();
|
|
10516
|
-
let organizations;
|
|
11175
|
+
var confirmDefaultOnTerminal = async (container) => {
|
|
11176
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
10517
11177
|
try {
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
if (!Array.isArray(organizations)) {
|
|
10523
|
-
log(eventLine("item", `Pick a default organization with \`${cli} organizations use <org>\`.`));
|
|
10524
|
-
return org ? 1 : 0;
|
|
10525
|
-
}
|
|
10526
|
-
const settings = await readSettings();
|
|
10527
|
-
const save = async (organization, why) => {
|
|
10528
|
-
settings.defaultOrganization = { id: organization.id, slug: organization.slug };
|
|
10529
|
-
await writeSettings(settings);
|
|
10530
|
-
log(eventLine("done", `Default organization: ${label(organization)}${why}.`));
|
|
10531
|
-
};
|
|
10532
|
-
const ref = org?.trim();
|
|
10533
|
-
if (ref) {
|
|
10534
|
-
const match = organizations.find((organization) => organization.id === ref) ?? organizations.find((organization) => organization.slug === ref);
|
|
10535
|
-
if (!match) {
|
|
10536
|
-
log(eventLine("failed", `You are not a member of an organization "${ref}"; no default set.`));
|
|
10537
|
-
return 1;
|
|
10538
|
-
}
|
|
10539
|
-
await save(match, "");
|
|
10540
|
-
return 0;
|
|
10541
|
-
}
|
|
10542
|
-
const current = organizations.find((organization) => organization.id === settings.defaultOrganization?.id);
|
|
10543
|
-
if (current) {
|
|
10544
|
-
log(eventLine("item", `Default organization: ${label(current)} (unchanged).`));
|
|
10545
|
-
return 0;
|
|
10546
|
-
}
|
|
10547
|
-
if (settings.defaultOrganization) {
|
|
10548
|
-
delete settings.defaultOrganization;
|
|
10549
|
-
await writeSettings(settings);
|
|
11178
|
+
const answer = (await prompt.question(`Make ${container} the default? [y/N] `)).trim();
|
|
11179
|
+
return /^y(es)?$/i.test(answer);
|
|
11180
|
+
} finally {
|
|
11181
|
+
prompt.close();
|
|
10550
11182
|
}
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
11183
|
+
};
|
|
11184
|
+
var confirmStartOnTerminal = async (container) => {
|
|
11185
|
+
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
11186
|
+
try {
|
|
11187
|
+
const answer = (await prompt.question(`Container ${container} is not running. Start it? [Y/n] `)).trim();
|
|
11188
|
+
return answer === "" || /^y(es)?$/i.test(answer);
|
|
11189
|
+
} finally {
|
|
11190
|
+
prompt.close();
|
|
10555
11191
|
}
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
11192
|
+
};
|
|
11193
|
+
function defaultContainerPickers() {
|
|
11194
|
+
return process.stdin.isTTY && process.stderr.isTTY ? {
|
|
11195
|
+
choose: chooseContainerOnTerminal,
|
|
11196
|
+
confirmDefault: confirmDefaultOnTerminal,
|
|
11197
|
+
confirmStart: confirmStartOnTerminal
|
|
11198
|
+
} : undefined;
|
|
11199
|
+
}
|
|
11200
|
+
|
|
11201
|
+
// src/progress.ts
|
|
11202
|
+
var PROGRESS_FRAMES = ["▂▄▆", "▄▆█", "▆█▆", "█▆▄", "▆▄▂", "▄▂▄"];
|
|
11203
|
+
var FRAME_MS = 120;
|
|
11204
|
+
function formatElapsed(ms) {
|
|
11205
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
11206
|
+
}
|
|
11207
|
+
function startProgress(message, options = {}) {
|
|
11208
|
+
const stream = options.stream ?? process.stderr;
|
|
11209
|
+
const now = options.now ?? Date.now;
|
|
11210
|
+
const started = now();
|
|
11211
|
+
const elapsed = () => formatElapsed(now() - started);
|
|
11212
|
+
if (isJsonOutput())
|
|
11213
|
+
return { stop: elapsed };
|
|
11214
|
+
const animate = options.animate ?? stream.isTTY === true;
|
|
11215
|
+
if (!animate) {
|
|
11216
|
+
stream.write(` ${SYMBOLS.progress} ${message}
|
|
11217
|
+
`);
|
|
11218
|
+
return { stop: elapsed };
|
|
10559
11219
|
}
|
|
10560
|
-
const
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
11220
|
+
const color = colorEnabled(stream);
|
|
11221
|
+
let frame = 0;
|
|
11222
|
+
const draw = () => {
|
|
11223
|
+
const bars = accent(PROGRESS_FRAMES[frame % PROGRESS_FRAMES.length] ?? SYMBOLS.progress, color);
|
|
11224
|
+
stream.write(`\r\x1B[2K ${bars} ${message} ${dim(elapsed(), color)}`);
|
|
11225
|
+
frame += 1;
|
|
11226
|
+
};
|
|
11227
|
+
const restore = () => stream.write("\r\x1B[2K\x1B[?25h");
|
|
11228
|
+
const onInterrupt = () => {
|
|
11229
|
+
restore();
|
|
11230
|
+
process.exit(130);
|
|
11231
|
+
};
|
|
11232
|
+
stream.write("\x1B[?25l");
|
|
11233
|
+
draw();
|
|
11234
|
+
const timer = setInterval(draw, FRAME_MS);
|
|
11235
|
+
timer.unref?.();
|
|
11236
|
+
process.once("SIGINT", onInterrupt);
|
|
11237
|
+
let stopped = false;
|
|
11238
|
+
return {
|
|
11239
|
+
stop() {
|
|
11240
|
+
if (!stopped) {
|
|
11241
|
+
stopped = true;
|
|
11242
|
+
clearInterval(timer);
|
|
11243
|
+
process.off("SIGINT", onInterrupt);
|
|
11244
|
+
restore();
|
|
11245
|
+
}
|
|
11246
|
+
return elapsed();
|
|
11247
|
+
}
|
|
11248
|
+
};
|
|
11249
|
+
}
|
|
11250
|
+
async function withProgress(message, work, options) {
|
|
11251
|
+
const progress = startProgress(message, options);
|
|
11252
|
+
try {
|
|
11253
|
+
const value = await work();
|
|
11254
|
+
return { value, elapsed: progress.stop() };
|
|
11255
|
+
} catch (err) {
|
|
11256
|
+
progress.stop();
|
|
11257
|
+
throw err;
|
|
10565
11258
|
}
|
|
10566
|
-
log(eventLine("item", `You belong to ${organizations.length} organizations: ${organizations.map((organization) => organization.slug ?? organization.id).join(", ")}. Pick a default with \`${cli} organizations use <org>\`, or pass --org.`));
|
|
10567
|
-
return 0;
|
|
10568
11259
|
}
|
|
10569
11260
|
|
|
10570
|
-
// src/
|
|
10571
|
-
function
|
|
11261
|
+
// src/container-flow.ts
|
|
11262
|
+
async function withContainerPick(call, send, pickers, notice) {
|
|
10572
11263
|
try {
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
11264
|
+
return await send();
|
|
11265
|
+
} catch (err) {
|
|
11266
|
+
const question = containerQuestionOf(err);
|
|
11267
|
+
if (!pickers || !question || !call.canPick)
|
|
11268
|
+
throw err;
|
|
11269
|
+
const container = await pickers.choose(question);
|
|
11270
|
+
if (!container)
|
|
11271
|
+
throw err;
|
|
11272
|
+
call.setContainer(container);
|
|
11273
|
+
const result = await send();
|
|
11274
|
+
if (call.sandboxId !== undefined && await pickers.confirmDefault(container)) {
|
|
11275
|
+
try {
|
|
11276
|
+
await authorizedRequest("/api/v1/tools/sandboxes_default_container", {
|
|
11277
|
+
method: "POST",
|
|
11278
|
+
body: JSON.stringify({
|
|
11279
|
+
organization: call.organization,
|
|
11280
|
+
sandboxId: call.sandboxId,
|
|
11281
|
+
container
|
|
11282
|
+
})
|
|
11283
|
+
});
|
|
11284
|
+
notice(eventLine("done", `Default container: ${container}.`));
|
|
11285
|
+
} catch (err) {
|
|
11286
|
+
notice(eventLine("failed", `The default container was not set: ${err instanceof Error ? err.message : String(err)}`));
|
|
11287
|
+
}
|
|
11288
|
+
}
|
|
11289
|
+
return result;
|
|
11290
|
+
}
|
|
10578
11291
|
}
|
|
10579
|
-
|
|
10580
|
-
|
|
11292
|
+
async function withContainerStart(call, send, start, pickers, notice) {
|
|
11293
|
+
try {
|
|
11294
|
+
return await send();
|
|
11295
|
+
} catch (err) {
|
|
11296
|
+
const container = notRunningContainerOf(err);
|
|
11297
|
+
if (!container || call.sandboxId === undefined)
|
|
11298
|
+
throw err;
|
|
11299
|
+
if (!start && !(pickers && await pickers.confirmStart(container)))
|
|
11300
|
+
throw err;
|
|
11301
|
+
await withProgress("Starting container…", () => authorizedRequest("/api/v1/tools/sandboxes_containers_start", {
|
|
11302
|
+
method: "POST",
|
|
11303
|
+
body: JSON.stringify({
|
|
11304
|
+
organization: call.organization,
|
|
11305
|
+
sandboxId: call.sandboxId,
|
|
11306
|
+
container
|
|
11307
|
+
})
|
|
11308
|
+
}));
|
|
11309
|
+
notice(eventLine("done", `Started ${container}.`));
|
|
11310
|
+
return await send();
|
|
11311
|
+
}
|
|
11312
|
+
}
|
|
11313
|
+
|
|
11314
|
+
// src/container-logs.ts
|
|
11315
|
+
var FOLLOW_INTERVAL_MS = 2000;
|
|
11316
|
+
function defaultLogsDeps(follow) {
|
|
11317
|
+
let stop = false;
|
|
11318
|
+
if (follow) {
|
|
11319
|
+
process.once("SIGINT", () => {
|
|
11320
|
+
stop = true;
|
|
11321
|
+
});
|
|
11322
|
+
}
|
|
10581
11323
|
return {
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
11324
|
+
request: (input) => authorizedRequest("/api/v1/tools/sandboxes_containers_logs", {
|
|
11325
|
+
method: "POST",
|
|
11326
|
+
body: JSON.stringify(input)
|
|
11327
|
+
}),
|
|
11328
|
+
out: (line) => process.stdout.write(`${line}
|
|
11329
|
+
`),
|
|
11330
|
+
err: (line) => process.stderr.write(`${line}
|
|
11331
|
+
`),
|
|
11332
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
11333
|
+
stopped: () => stop
|
|
10585
11334
|
};
|
|
10586
11335
|
}
|
|
10587
|
-
function
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
};
|
|
10595
|
-
if (args.code) {
|
|
10596
|
-
const result = await deviceToken({ pasteCode: args.code });
|
|
10597
|
-
if (result.status === "approved") {
|
|
10598
|
-
await writeCredentials(toCredentials(result));
|
|
10599
|
-
return loggedIn();
|
|
11336
|
+
async function runContainerLogs(input, options, deps = defaultLogsDeps(options.follow)) {
|
|
11337
|
+
const json = isJsonOutput();
|
|
11338
|
+
const color = colorEnabled(process.stdout);
|
|
11339
|
+
const print = (line) => {
|
|
11340
|
+
if (json) {
|
|
11341
|
+
deps.out(JSON.stringify(line));
|
|
11342
|
+
return;
|
|
10600
11343
|
}
|
|
10601
|
-
|
|
11344
|
+
const text = options.timestamps && line.time ? `${dim(line.time, color)} ${line.text}` : line.text;
|
|
11345
|
+
(line.stream === "stderr" ? deps.err : deps.out)(text);
|
|
11346
|
+
};
|
|
11347
|
+
let result = await deps.request(input);
|
|
11348
|
+
if (!result.running) {
|
|
11349
|
+
deps.err("The sandbox is stopped: it has no logs to show.");
|
|
10602
11350
|
return 1;
|
|
10603
11351
|
}
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
const timeoutSeconds = Number.parseInt(args.timeout ?? "", 10) || DEFAULT_TIMEOUT_SECONDS;
|
|
10611
|
-
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
10612
|
-
const intervalMs = start.interval * 1000;
|
|
10613
|
-
const progress = startProgress("Waiting for approval in the browser…");
|
|
10614
|
-
while (Date.now() < deadline) {
|
|
10615
|
-
await sleep(intervalMs);
|
|
10616
|
-
try {
|
|
10617
|
-
const result = await deviceToken({ deviceCode: start.deviceCode });
|
|
10618
|
-
if (result.status === "approved") {
|
|
10619
|
-
progress.stop();
|
|
10620
|
-
await writeCredentials(toCredentials(result));
|
|
10621
|
-
return loggedIn();
|
|
10622
|
-
}
|
|
10623
|
-
} catch (err) {
|
|
10624
|
-
progress.stop();
|
|
10625
|
-
if (err instanceof ApiError && err.status === 400) {
|
|
10626
|
-
error(`Login failed: ${err.message}`);
|
|
10627
|
-
return 1;
|
|
10628
|
-
}
|
|
10629
|
-
throw err;
|
|
11352
|
+
let last = null;
|
|
11353
|
+
for (;; ) {
|
|
11354
|
+
for (const line of result.lines) {
|
|
11355
|
+
print(line);
|
|
11356
|
+
if (line.time)
|
|
11357
|
+
last = line.time;
|
|
10630
11358
|
}
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
name: "login",
|
|
10639
|
-
description: "Sign in and store a device access token for this machine (~/.config/ghostdock/credentials.json)."
|
|
10640
|
-
},
|
|
10641
|
-
args: {
|
|
10642
|
-
wait: {
|
|
10643
|
-
type: "boolean",
|
|
10644
|
-
description: "Poll until the login is approved in the browser (default timeout 5 minutes).",
|
|
10645
|
-
default: false
|
|
10646
|
-
},
|
|
10647
|
-
code: {
|
|
10648
|
-
type: "string",
|
|
10649
|
-
description: "Redeem the one-time paste code shown in the browser instead of polling."
|
|
10650
|
-
},
|
|
10651
|
-
timeout: {
|
|
10652
|
-
type: "string",
|
|
10653
|
-
description: "Max seconds to poll when using --wait (default 300)."
|
|
10654
|
-
},
|
|
10655
|
-
org: {
|
|
10656
|
-
type: "string",
|
|
10657
|
-
description: "Set this organization (id or slug) as the default once logged in."
|
|
11359
|
+
if (!options.follow) {
|
|
11360
|
+
if (result.truncated && json) {
|
|
11361
|
+
deps.out(JSON.stringify({ truncated: true }));
|
|
11362
|
+
} else if (result.truncated) {
|
|
11363
|
+
deps.err(dim("(older lines left out: use --tail for more)", colorEnabled(process.stderr)));
|
|
11364
|
+
}
|
|
11365
|
+
return 0;
|
|
10658
11366
|
}
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
}
|
|
10664
|
-
|
|
10665
|
-
// src/commands/logout.ts
|
|
10666
|
-
async function runLogout(log = console.log) {
|
|
10667
|
-
const credentials = await readCredentials();
|
|
10668
|
-
if (credentials) {
|
|
11367
|
+
if (!result.truncated)
|
|
11368
|
+
await deps.sleep(FOLLOW_INTERVAL_MS);
|
|
11369
|
+
if (deps.stopped())
|
|
11370
|
+
return 0;
|
|
11371
|
+
const { tail: _tail, ...rest } = input;
|
|
10669
11372
|
try {
|
|
10670
|
-
await
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
}
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
}
|
|
10680
|
-
var logoutCommand = defineCommand({
|
|
10681
|
-
meta: {
|
|
10682
|
-
name: "logout",
|
|
10683
|
-
description: "Revoke the device session and remove stored credentials."
|
|
10684
|
-
},
|
|
10685
|
-
async run() {
|
|
10686
|
-
process.exitCode = await runLogout();
|
|
11373
|
+
result = await deps.request(last ? { ...rest, after: last } : input);
|
|
11374
|
+
} catch (error) {
|
|
11375
|
+
deps.err(red(error instanceof Error ? error.message : String(error), colorEnabled(process.stderr)));
|
|
11376
|
+
return 1;
|
|
11377
|
+
}
|
|
11378
|
+
if (!result.running) {
|
|
11379
|
+
deps.err("The sandbox stopped.");
|
|
11380
|
+
return 0;
|
|
11381
|
+
}
|
|
10687
11382
|
}
|
|
10688
|
-
}
|
|
10689
|
-
|
|
10690
|
-
// src/commands/network.ts
|
|
10691
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
10692
|
-
import { hostname as hostname2 } from "node:os";
|
|
11383
|
+
}
|
|
10693
11384
|
|
|
10694
|
-
// src/
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
11385
|
+
// src/human.ts
|
|
11386
|
+
var ID_KEYS = [
|
|
11387
|
+
"sandboxId",
|
|
11388
|
+
"siteId",
|
|
11389
|
+
"projectId",
|
|
11390
|
+
"deploymentId",
|
|
11391
|
+
"imageId",
|
|
11392
|
+
"snapshotId",
|
|
11393
|
+
"urlId",
|
|
11394
|
+
"mcpId",
|
|
11395
|
+
"commandId",
|
|
11396
|
+
"sessionId",
|
|
11397
|
+
"sandboxProviderId",
|
|
11398
|
+
"id"
|
|
11399
|
+
];
|
|
11400
|
+
var LABEL_KEYS = ["displayName", "name", "title", "slug", "label", "path", "uri", "ref"];
|
|
11401
|
+
var STATUS_KEYS = ["status", "outcome", "state"];
|
|
11402
|
+
var URL_KEYS = ["url", "liveUrl", "publicUrl", "sshCommand"];
|
|
11403
|
+
function isItem(value) {
|
|
11404
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10702
11405
|
}
|
|
10703
|
-
function
|
|
10704
|
-
return
|
|
11406
|
+
function isScalar(value) {
|
|
11407
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
10705
11408
|
}
|
|
10706
|
-
function
|
|
10707
|
-
return
|
|
11409
|
+
function firstKey(item, keys) {
|
|
11410
|
+
return keys.find((key) => {
|
|
11411
|
+
const value = item[key];
|
|
11412
|
+
return typeof value === "string" && value.trim() !== "";
|
|
11413
|
+
});
|
|
10708
11414
|
}
|
|
10709
|
-
function
|
|
10710
|
-
|
|
11415
|
+
function firstString(item, keys) {
|
|
11416
|
+
const key = firstKey(item, keys);
|
|
11417
|
+
return key ? item[key] : undefined;
|
|
10711
11418
|
}
|
|
10712
|
-
function
|
|
10713
|
-
const
|
|
10714
|
-
|
|
11419
|
+
function symbolForStatus(status) {
|
|
11420
|
+
const s = (status ?? "").toLowerCase();
|
|
11421
|
+
if (/(fail|error|denied)/.test(s))
|
|
11422
|
+
return "failed";
|
|
11423
|
+
if (/^(running|active|ready|deployed|published|online|verified|ok)$/.test(s))
|
|
11424
|
+
return "ready";
|
|
11425
|
+
if (/^(stopped|deleted|unpublished|vanished|archived|revoked|closed|killed|exited|dead|paused)$/.test(s)) {
|
|
11426
|
+
return "stopped";
|
|
11427
|
+
}
|
|
11428
|
+
if (/(pending|starting|spawning|stopping|building|progress|awaiting|queued|creating|restarting|removing)/.test(s)) {
|
|
11429
|
+
return "progress";
|
|
11430
|
+
}
|
|
11431
|
+
return "item";
|
|
10715
11432
|
}
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
const raw = Buffer.from(privateKey, "base64");
|
|
10719
|
-
if (raw.length !== 32)
|
|
10720
|
-
throw new Error("The stored WireGuard key is not 32 bytes.");
|
|
10721
|
-
const key = createPrivateKey({
|
|
10722
|
-
key: Buffer.concat([X25519_PKCS8_PREFIX, raw]),
|
|
10723
|
-
format: "der",
|
|
10724
|
-
type: "pkcs8"
|
|
10725
|
-
});
|
|
10726
|
-
const spki = createPublicKey(key).export({ format: "der", type: "spki" });
|
|
10727
|
-
return { privateKey, publicKey: spki.subarray(spki.length - 32).toString("base64") };
|
|
11433
|
+
function isSandbox(item) {
|
|
11434
|
+
return typeof item.sandboxId === "string" && typeof item.name === "string" && !("id" in item);
|
|
10728
11435
|
}
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
return
|
|
10732
|
-
} catch (err) {
|
|
10733
|
-
if (err.code !== "ENOENT")
|
|
10734
|
-
throw err;
|
|
11436
|
+
function lineKeys(item) {
|
|
11437
|
+
if (isSandbox(item)) {
|
|
11438
|
+
return { id: "name", label: firstKey(item, ["displayName"]) };
|
|
10735
11439
|
}
|
|
10736
|
-
const
|
|
10737
|
-
|
|
10738
|
-
const raw = der.subarray(der.length - 32).toString("base64");
|
|
10739
|
-
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
10740
|
-
await writeFile3(keyPath(dir), `${raw}
|
|
10741
|
-
`, { mode: 384 });
|
|
10742
|
-
return keyPairFromPrivate(raw);
|
|
11440
|
+
const idKeys = "id" in item ? ID_KEYS.filter((key) => key !== "sandboxId") : ID_KEYS;
|
|
11441
|
+
return { id: firstKey(item, idKeys), label: firstKey(item, LABEL_KEYS) };
|
|
10743
11442
|
}
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
11443
|
+
function shortId(id) {
|
|
11444
|
+
return /^[0-9a-f]{64}$/.test(id) ? id.slice(0, 12) : id;
|
|
11445
|
+
}
|
|
11446
|
+
function itemLine(item, color) {
|
|
11447
|
+
const keys = lineKeys(item);
|
|
11448
|
+
const id = keys.id ? item[keys.id] : undefined;
|
|
11449
|
+
const label = keys.label ? item[keys.label] : undefined;
|
|
11450
|
+
const status = firstString(item, STATUS_KEYS);
|
|
11451
|
+
const url = firstString(item, URL_KEYS);
|
|
11452
|
+
const parts = [label ?? id ?? "(unnamed)"];
|
|
11453
|
+
if (label && id && id !== label)
|
|
11454
|
+
parts.push(dim(shortId(id), color));
|
|
11455
|
+
if (status)
|
|
11456
|
+
parts.push(status);
|
|
11457
|
+
if (url)
|
|
11458
|
+
parts.push(`${SYMBOLS.url} ${url}`);
|
|
11459
|
+
return eventLine(symbolForStatus(status), parts.join(" "), color);
|
|
11460
|
+
}
|
|
11461
|
+
function scalarText(value) {
|
|
11462
|
+
if (value === null)
|
|
11463
|
+
return "—";
|
|
11464
|
+
if (isScalar(value))
|
|
11465
|
+
return String(value);
|
|
11466
|
+
if (Array.isArray(value) && value.every(isScalar))
|
|
11467
|
+
return value.join(", ");
|
|
11468
|
+
if (Array.isArray(value))
|
|
11469
|
+
return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
11470
|
+
return "…";
|
|
11471
|
+
}
|
|
11472
|
+
function fieldLines(item, color, skip) {
|
|
11473
|
+
const lines = [];
|
|
11474
|
+
for (const [key, value] of Object.entries(item)) {
|
|
11475
|
+
if (skip.has(key) || value === undefined)
|
|
11476
|
+
continue;
|
|
11477
|
+
if (Array.isArray(value) && value.some(isItem)) {
|
|
11478
|
+
lines.push(` ${dim(`${key}:`, color)}`);
|
|
11479
|
+
for (const entry of value) {
|
|
11480
|
+
lines.push(` ${isItem(entry) ? itemLine(entry, color) : eventLine("item", scalarText(entry), color)}`);
|
|
11481
|
+
}
|
|
11482
|
+
continue;
|
|
10754
11483
|
}
|
|
10755
|
-
|
|
10756
|
-
|
|
11484
|
+
if (isItem(value)) {
|
|
11485
|
+
for (const [sub, subValue] of Object.entries(value)) {
|
|
11486
|
+
if (subValue === undefined || isItem(subValue))
|
|
11487
|
+
continue;
|
|
11488
|
+
lines.push(` ${dim(`${key}.${sub}:`, color)} ${scalarText(subValue)}`);
|
|
11489
|
+
}
|
|
11490
|
+
continue;
|
|
11491
|
+
}
|
|
11492
|
+
lines.push(` ${dim(`${key}:`, color)} ${scalarText(value)}`);
|
|
11493
|
+
}
|
|
11494
|
+
return lines;
|
|
10757
11495
|
}
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
11496
|
+
function listLines(items, color) {
|
|
11497
|
+
if (items.length === 0)
|
|
11498
|
+
return [` ${dim("(none)", color)}`];
|
|
11499
|
+
return items.map((item) => isItem(item) ? itemLine(item, color) : eventLine("item", scalarText(item), color));
|
|
10762
11500
|
}
|
|
10763
|
-
function
|
|
10764
|
-
|
|
10765
|
-
return "macos-resolver";
|
|
10766
|
-
if (platform === "linux" && exists("/run/systemd/resolve"))
|
|
10767
|
-
return "resolved";
|
|
10768
|
-
return "none";
|
|
11501
|
+
function bootPhase(value) {
|
|
11502
|
+
return isItem(value) && typeof value.ms === "number" && Number.isFinite(value.ms) ? { ms: value.ms } : null;
|
|
10769
11503
|
}
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
11504
|
+
var DISK_SOURCE_TEXT = {
|
|
11505
|
+
fresh: "image pulled",
|
|
11506
|
+
"image-base": "image already on disk",
|
|
11507
|
+
"user-snapshot": "from a snapshot"
|
|
11508
|
+
};
|
|
11509
|
+
function bootLines(data, color) {
|
|
11510
|
+
if (!isItem(data) || !isItem(data.boot))
|
|
11511
|
+
return [];
|
|
11512
|
+
const sandbox = bootPhase(data.boot.sandbox);
|
|
11513
|
+
if (!sandbox)
|
|
11514
|
+
return [];
|
|
11515
|
+
const lines = [eventLine("ready", `sandbox started in ${formatElapsed(sandbox.ms)}`, color)];
|
|
11516
|
+
const container = bootPhase(data.boot.container);
|
|
11517
|
+
if (container) {
|
|
11518
|
+
lines.push(eventLine("done", `container started in ${formatElapsed(container.ms)}`, color));
|
|
10776
11519
|
}
|
|
10777
|
-
|
|
10778
|
-
|
|
11520
|
+
const provision = bootPhase(data.boot.provision);
|
|
11521
|
+
if (provision) {
|
|
11522
|
+
const disk = typeof data.boot.disk === "string" ? DISK_SOURCE_TEXT[data.boot.disk] : undefined;
|
|
11523
|
+
const how = disk ? ` (${disk})` : "";
|
|
11524
|
+
lines.push(eventLine("done", `ready in ${formatElapsed(provision.ms)}${how}`, color));
|
|
10779
11525
|
}
|
|
10780
|
-
|
|
10781
|
-
|
|
11526
|
+
return lines;
|
|
11527
|
+
}
|
|
11528
|
+
function renderResult(data, color) {
|
|
11529
|
+
if (data === null || data === undefined)
|
|
11530
|
+
return [eventLine("done", "Done.", color)];
|
|
11531
|
+
if (Array.isArray(data))
|
|
11532
|
+
return listLines(data, color);
|
|
11533
|
+
if (!isItem(data))
|
|
11534
|
+
return [eventLine("item", scalarText(data), color)];
|
|
11535
|
+
const entries = Object.entries(data).filter(([, value]) => value !== undefined);
|
|
11536
|
+
if (entries.length === 0 || entries.length === 1 && data.ok === true) {
|
|
11537
|
+
return [eventLine("done", "Done.", color)];
|
|
10782
11538
|
}
|
|
10783
|
-
const
|
|
10784
|
-
if (
|
|
10785
|
-
|
|
11539
|
+
const lists = entries.filter(([, value]) => Array.isArray(value) && (value.length === 0 || value.some(isItem)));
|
|
11540
|
+
if (lists.length === 1 && !firstString(data, ID_KEYS)) {
|
|
11541
|
+
const [listKey, list] = lists[0];
|
|
11542
|
+
const rest = fieldLines(data, color, new Set([listKey]));
|
|
11543
|
+
return [...rest, ...listLines(list, color)];
|
|
10786
11544
|
}
|
|
10787
|
-
const
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
];
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
11545
|
+
const { id: idKey, label: labelKey } = lineKeys(data);
|
|
11546
|
+
const used = [idKey, labelKey, firstKey(data, STATUS_KEYS), firstKey(data, URL_KEYS)].filter((key) => key !== undefined);
|
|
11547
|
+
const head = used.length > 0 ? [itemLine(data, color)] : [];
|
|
11548
|
+
const boot = bootLines(data, color);
|
|
11549
|
+
if ("boot" in data && (boot.length > 0 || data.boot === null))
|
|
11550
|
+
used.push("boot");
|
|
11551
|
+
return [...head, ...boot, ...fieldLines(data, color, new Set(used))];
|
|
11552
|
+
}
|
|
11553
|
+
|
|
11554
|
+
// src/cli-help-text.ts
|
|
11555
|
+
var CLI_HELP_DESCRIPTION_MAX_LENGTH = 80;
|
|
11556
|
+
function cliHelpDescription(description, maxLength = CLI_HELP_DESCRIPTION_MAX_LENGTH) {
|
|
11557
|
+
const oneLine = description.replace(/\s+/g, " ").trim();
|
|
11558
|
+
if (oneLine.length <= maxLength) {
|
|
11559
|
+
return oneLine;
|
|
11560
|
+
}
|
|
11561
|
+
const budget = Math.max(1, maxLength - 1);
|
|
11562
|
+
const slice = oneLine.slice(0, budget);
|
|
11563
|
+
const breakAt = slice.lastIndexOf(" ");
|
|
11564
|
+
const cut = breakAt >= Math.floor(budget * 0.6) ? slice.slice(0, breakAt) : slice;
|
|
11565
|
+
return `${cut.trimEnd()}…`;
|
|
11566
|
+
}
|
|
11567
|
+
|
|
11568
|
+
// src/schema-flags.ts
|
|
11569
|
+
function toFlagName(propertyName) {
|
|
11570
|
+
if (propertyName === "organization")
|
|
11571
|
+
return "org";
|
|
11572
|
+
return propertyName.replace(/([KMGTP])iB(?![a-z])/g, "$1ib").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
11573
|
+
}
|
|
11574
|
+
function isJsonSchemaProperty(value) {
|
|
11575
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11576
|
+
}
|
|
11577
|
+
function schemaVariants(prop) {
|
|
11578
|
+
const variants = [];
|
|
11579
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
11580
|
+
const group = prop[key];
|
|
11581
|
+
if (!Array.isArray(group))
|
|
11582
|
+
continue;
|
|
11583
|
+
for (const item of group) {
|
|
11584
|
+
if (isJsonSchemaProperty(item))
|
|
11585
|
+
variants.push(item);
|
|
10798
11586
|
}
|
|
10799
|
-
} else if (input.dns === "resolved") {
|
|
10800
|
-
const domains = zones.map((zone) => `'~${zone}'`).join(" ");
|
|
10801
|
-
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${domains}`);
|
|
10802
11587
|
}
|
|
10803
|
-
|
|
10804
|
-
return lines.join(`
|
|
10805
|
-
`);
|
|
11588
|
+
return variants;
|
|
10806
11589
|
}
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
11590
|
+
function primaryJsonType(prop) {
|
|
11591
|
+
if (prop.type !== undefined) {
|
|
11592
|
+
if (Array.isArray(prop.type)) {
|
|
11593
|
+
return prop.type.find((t) => t !== "null");
|
|
11594
|
+
}
|
|
11595
|
+
return prop.type === "null" ? undefined : prop.type;
|
|
11596
|
+
}
|
|
11597
|
+
const types = new Set;
|
|
11598
|
+
for (const variant of schemaVariants(prop)) {
|
|
11599
|
+
const type = primaryJsonType(variant);
|
|
11600
|
+
if (type)
|
|
11601
|
+
types.add(type);
|
|
11602
|
+
}
|
|
11603
|
+
if (types.has("object"))
|
|
11604
|
+
return "object";
|
|
11605
|
+
if (types.has("array"))
|
|
11606
|
+
return "array";
|
|
11607
|
+
if (types.size === 1) {
|
|
11608
|
+
const [only] = types;
|
|
11609
|
+
return only;
|
|
11610
|
+
}
|
|
11611
|
+
return;
|
|
10811
11612
|
}
|
|
10812
|
-
function
|
|
10813
|
-
|
|
11613
|
+
function allowsNull(prop) {
|
|
11614
|
+
if (prop.type === "null")
|
|
11615
|
+
return true;
|
|
11616
|
+
if (Array.isArray(prop.type) && prop.type.includes("null"))
|
|
11617
|
+
return true;
|
|
11618
|
+
return schemaVariants(prop).some(allowsNull);
|
|
10814
11619
|
}
|
|
10815
|
-
function
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
11620
|
+
function buildToolArgs(schema, describe = cliHelpDescription, positionals = []) {
|
|
11621
|
+
const properties = schema.properties ?? {};
|
|
11622
|
+
const required = new Set(schema.required ?? []);
|
|
11623
|
+
const args = {};
|
|
11624
|
+
for (const name of positionals) {
|
|
11625
|
+
const prop = properties[name];
|
|
11626
|
+
if (!prop)
|
|
11627
|
+
continue;
|
|
11628
|
+
const jsonType = primaryJsonType(prop);
|
|
11629
|
+
args[toFlagName(name)] = {
|
|
11630
|
+
type: "positional",
|
|
11631
|
+
description: describe(prop.description ?? name),
|
|
11632
|
+
required: true,
|
|
11633
|
+
valueHint: jsonType
|
|
11634
|
+
};
|
|
10820
11635
|
}
|
|
10821
|
-
for (const
|
|
10822
|
-
|
|
10823
|
-
if (!line || /^(default|broadcast)\b/.test(line))
|
|
11636
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
11637
|
+
if (positionals.includes(name))
|
|
10824
11638
|
continue;
|
|
10825
|
-
|
|
11639
|
+
const jsonType = primaryJsonType(prop);
|
|
11640
|
+
const isBoolean = jsonType === "boolean";
|
|
11641
|
+
const isJsonValue = jsonType === "object" || jsonType === "array";
|
|
11642
|
+
const typeNote = isJsonValue ? ` (JSON ${jsonType})` : "";
|
|
11643
|
+
const isOrganization = name === "organization";
|
|
11644
|
+
const isRequired = required.has(name) && prop.default === undefined && !isOrganization;
|
|
11645
|
+
const defaultNote = isOrganization ? " Defaults to GHOSTDOCK_ORG, then `organizations use`." : "";
|
|
11646
|
+
args[toFlagName(name)] = isBoolean ? {
|
|
11647
|
+
type: "boolean",
|
|
11648
|
+
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
11649
|
+
required: isRequired
|
|
11650
|
+
} : {
|
|
11651
|
+
type: "string",
|
|
11652
|
+
description: describe(`${prop.description ?? name}${typeNote}${defaultNote}`),
|
|
11653
|
+
required: isRequired,
|
|
11654
|
+
valueHint: jsonType
|
|
11655
|
+
};
|
|
10826
11656
|
}
|
|
10827
|
-
return
|
|
10828
|
-
}
|
|
10829
|
-
function ipv4RouteCommand(platform, address) {
|
|
10830
|
-
return platform === "darwin" ? ["route", ["-n", "get", address]] : ["ip", ["-4", "route", "show", "table", "all", "match", address]];
|
|
10831
|
-
}
|
|
10832
|
-
function isIPv6Address(text) {
|
|
10833
|
-
return /^[0-9a-f:]{2,39}$/.test(text) && text.includes(":");
|
|
11657
|
+
return args;
|
|
10834
11658
|
}
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
try {
|
|
10838
|
-
contents = await readFile3(path, "utf8");
|
|
10839
|
-
} catch {
|
|
11659
|
+
function coerceFlagValue(raw, prop, flagName) {
|
|
11660
|
+
if (raw === "null" && allowsNull(prop))
|
|
10840
11661
|
return null;
|
|
11662
|
+
const jsonType = primaryJsonType(prop);
|
|
11663
|
+
if (jsonType === "boolean") {
|
|
11664
|
+
return typeof raw === "boolean" ? raw : raw === "true";
|
|
10841
11665
|
}
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
11666
|
+
if (jsonType === "number" || jsonType === "integer") {
|
|
11667
|
+
const num = Number(raw);
|
|
11668
|
+
if (Number.isNaN(num)) {
|
|
11669
|
+
throw new Error(`Invalid number for --${flagName}: ${String(raw)}`);
|
|
11670
|
+
}
|
|
11671
|
+
return num;
|
|
11672
|
+
}
|
|
11673
|
+
if (jsonType === "object" || jsonType === "array") {
|
|
11674
|
+
if (typeof raw !== "string")
|
|
11675
|
+
return raw;
|
|
11676
|
+
try {
|
|
11677
|
+
return JSON.parse(raw);
|
|
11678
|
+
} catch {
|
|
11679
|
+
throw new Error(`Invalid JSON for --${flagName}: ${String(raw)}`);
|
|
11680
|
+
}
|
|
11681
|
+
}
|
|
11682
|
+
return raw;
|
|
10854
11683
|
}
|
|
10855
|
-
function
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
11684
|
+
function buildToolInput(schema, rawArgs) {
|
|
11685
|
+
const properties = schema.properties ?? {};
|
|
11686
|
+
const input = {};
|
|
11687
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
11688
|
+
const flagName = toFlagName(name);
|
|
11689
|
+
const raw = rawArgs[flagName] ?? rawArgs[name];
|
|
11690
|
+
if (raw === undefined)
|
|
11691
|
+
continue;
|
|
11692
|
+
input[name] = coerceFlagValue(raw, prop, flagName);
|
|
11693
|
+
}
|
|
11694
|
+
return input;
|
|
10861
11695
|
}
|
|
10862
11696
|
|
|
10863
|
-
// src/commands/
|
|
10864
|
-
function
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
run: (command, args) => new Promise((resolve, reject) => {
|
|
10868
|
-
const child = spawn(command, args, { stdio: "inherit" });
|
|
10869
|
-
child.on("error", reject);
|
|
10870
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
10871
|
-
}),
|
|
10872
|
-
output: (command, args) => {
|
|
10873
|
-
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
10874
|
-
return result.status === 0 ? result.stdout : "";
|
|
10875
|
-
},
|
|
10876
|
-
platform: process.platform,
|
|
10877
|
-
has: (command) => spawnSync("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
10878
|
-
dir: networkDir(),
|
|
10879
|
-
up: () => tunnelUp(),
|
|
10880
|
-
log: console.log,
|
|
10881
|
-
error: printError,
|
|
10882
|
-
notice: printNotice
|
|
10883
|
-
};
|
|
10884
|
-
}
|
|
10885
|
-
function message(err) {
|
|
10886
|
-
return err instanceof Error ? err.message : String(err);
|
|
10887
|
-
}
|
|
10888
|
-
function connectCommandFor(provider) {
|
|
10889
|
-
const ref = provider.connectRef;
|
|
10890
|
-
const arg = /^[A-Za-z0-9._-]+$/.test(ref) ? ref : JSON.stringify(ref);
|
|
10891
|
-
return `${getCliName()} network connect ${arg}`;
|
|
10892
|
-
}
|
|
10893
|
-
async function wgQuick(deps, verb) {
|
|
10894
|
-
const args = ["wg-quick", verb, configPath(deps.dir)];
|
|
10895
|
-
deps.notice(`Running: sudo ${args.join(" ")}`);
|
|
10896
|
-
return deps.run("sudo", args);
|
|
10897
|
-
}
|
|
10898
|
-
async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
10899
|
-
if (!deps.has("wg-quick")) {
|
|
10900
|
-
deps.error(`wg-quick is not installed. ${installHint()}`);
|
|
10901
|
-
return 1;
|
|
10902
|
-
}
|
|
10903
|
-
let registered;
|
|
10904
|
-
let privateKey;
|
|
10905
|
-
const previous = await readDeviceState(deps.dir);
|
|
11697
|
+
// src/commands/invoke.ts
|
|
11698
|
+
async function runToolInvoke(tool, rawArgs, log = console.log, error = printError, notice = printNotice, pickers = defaultContainerPickers()) {
|
|
11699
|
+
const properties = tool.inputSchema.properties;
|
|
11700
|
+
let input;
|
|
10906
11701
|
try {
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
registered = await deps.request("/api/v1/network/devices", {
|
|
10912
|
-
method: "POST",
|
|
10913
|
-
body: JSON.stringify({
|
|
10914
|
-
publicKey: key.publicKey,
|
|
10915
|
-
name: hostname2().slice(0, 64) || "device",
|
|
10916
|
-
organization,
|
|
10917
|
-
...provider ? { provider } : {}
|
|
10918
|
-
})
|
|
10919
|
-
});
|
|
11702
|
+
input = buildToolInput(tool.inputSchema, rawArgs);
|
|
11703
|
+
if (properties && "organization" in properties && input.organization === undefined) {
|
|
11704
|
+
input.organization = await requireOrganizationRef(undefined, notice);
|
|
11705
|
+
}
|
|
10920
11706
|
} catch (err) {
|
|
10921
|
-
|
|
11707
|
+
error(err instanceof Error ? err.message : String(err));
|
|
10922
11708
|
return 1;
|
|
10923
11709
|
}
|
|
10924
|
-
|
|
10925
|
-
|
|
11710
|
+
if (tool.name === "sandboxes_containers_logs") {
|
|
11711
|
+
try {
|
|
11712
|
+
return await runContainerLogs(input, {
|
|
11713
|
+
follow: rawArgs.follow === true,
|
|
11714
|
+
timestamps: rawArgs.timestamps === true
|
|
11715
|
+
});
|
|
11716
|
+
} catch (err) {
|
|
11717
|
+
error(err instanceof Error ? err.message : `${tool.name} failed.`);
|
|
11718
|
+
return 1;
|
|
11719
|
+
}
|
|
11720
|
+
}
|
|
11721
|
+
const call = {
|
|
11722
|
+
organization: typeof input.organization === "string" ? input.organization : undefined,
|
|
11723
|
+
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : undefined,
|
|
11724
|
+
canPick: !!properties && "container" in properties && !input.container,
|
|
11725
|
+
setContainer: (container) => {
|
|
11726
|
+
input.container = container;
|
|
11727
|
+
}
|
|
11728
|
+
};
|
|
10926
11729
|
try {
|
|
10927
|
-
|
|
11730
|
+
const post = () => authorizedRequest(`/api/v1/tools/${tool.name}`, {
|
|
11731
|
+
method: "POST",
|
|
11732
|
+
body: JSON.stringify(input)
|
|
11733
|
+
});
|
|
11734
|
+
const waitMessage = PROGRESS_MESSAGES[tool.name];
|
|
11735
|
+
let elapsed;
|
|
11736
|
+
const send = async () => {
|
|
11737
|
+
if (!waitMessage)
|
|
11738
|
+
return post();
|
|
11739
|
+
const timed = await withProgress(waitMessage, post);
|
|
11740
|
+
elapsed = timed.elapsed;
|
|
11741
|
+
return timed.value;
|
|
11742
|
+
};
|
|
11743
|
+
const result = await withContainerPick(call, () => withContainerStart(call, send, rawArgs.start === true, pickers, notice), pickers, notice);
|
|
11744
|
+
printResult(result, () => {
|
|
11745
|
+
const color = colorEnabled(process.stdout);
|
|
11746
|
+
const lines = renderResult(result, color);
|
|
11747
|
+
return elapsed === undefined || bootLines(result, color).length > 0 ? lines : withElapsed(lines, elapsed);
|
|
11748
|
+
}, log);
|
|
11749
|
+
stopNote(result, notice);
|
|
11750
|
+
return 0;
|
|
10928
11751
|
} catch (err) {
|
|
10929
|
-
|
|
10930
|
-
return 1;
|
|
10931
|
-
}
|
|
10932
|
-
await writeConfig(config, configPath(deps.dir));
|
|
10933
|
-
await writeDeviceState({
|
|
10934
|
-
baseUrl: getBaseUrl(),
|
|
10935
|
-
deviceId: registered.device.id,
|
|
10936
|
-
address: registered.device.address ?? "",
|
|
10937
|
-
gatewayProvider: registered.provider
|
|
10938
|
-
}, deps.dir);
|
|
10939
|
-
const wasUp = deps.up();
|
|
10940
|
-
if (wasUp) {
|
|
10941
|
-
const down = await wgQuick(deps, "down");
|
|
10942
|
-
if (down !== 0) {
|
|
10943
|
-
deps.error(`Could not take the old tunnel down (wg-quick exited ${down}).`);
|
|
10944
|
-
return 1;
|
|
10945
|
-
}
|
|
10946
|
-
}
|
|
10947
|
-
const ipv4Clashes = ipv4Clashes4(registered, deps);
|
|
10948
|
-
const code = await wgQuick(deps, "up");
|
|
10949
|
-
if (code !== 0) {
|
|
10950
|
-
deps.error(`wg-quick exited ${code}. The config is in ${configPath(deps.dir)}.`);
|
|
11752
|
+
error(err instanceof Error ? err.message : `${tool.name} failed.`);
|
|
10951
11753
|
return 1;
|
|
10952
11754
|
}
|
|
10953
|
-
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
10954
|
-
const pinned = device.shortNameOrganization;
|
|
10955
|
-
const switched = wasUp && previous?.gatewayProvider && previous.gatewayProvider.id !== provider.id ? previous.gatewayProvider : null;
|
|
10956
|
-
printResult({ device, network, provider, shortNameOrganizationChanged, interface: INTERFACE, dns }, () => [
|
|
10957
|
-
eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
|
|
10958
|
-
...switched ? [
|
|
10959
|
-
` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
|
|
10960
|
-
] : [],
|
|
10961
|
-
` Reachable: the sandboxes on ${provider.name}. One provider at a time.`,
|
|
10962
|
-
` Names: <sandbox>.<org>.${network.zone}`,
|
|
10963
|
-
...network.netZone ? [` In a browser: https://<sandbox>--<org>.${network.netZone}:<port>`] : [],
|
|
10964
|
-
...network.netZone && pinned ? [
|
|
10965
|
-
` Short names: https://<sandbox>.${network.netZone}:<port> for ${pinned.slug}'s sandboxes, on this machine.`,
|
|
10966
|
-
` Another organization: ${getCliName()} network connect --org <org>.`
|
|
10967
|
-
] : [],
|
|
10968
|
-
...network.netZone && pinned && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : [],
|
|
10969
|
-
...network.ingress4 && !device.address4 ? [
|
|
10970
|
-
" No IPv4 address was free on this provider's host, so browser names work over IPv6 only:",
|
|
10971
|
-
" Chrome on a network without IPv6 cannot open them on this machine. Firefox and Safari",
|
|
10972
|
-
" can. Run connect again later: an address frees up when another device goes."
|
|
10973
|
-
] : [],
|
|
10974
|
-
...ipv4Clashes.map(({ address, owner }) => ` ${address} is already routed through ${owner} on this machine: browsers may not reach the private network's names over IPv4 while it is. Ask your Ghostdock admin about the IPv4 block.`),
|
|
10975
|
-
...dns === "none" ? [
|
|
10976
|
-
" No split DNS on this machine (no systemd-resolved): use the addresses from",
|
|
10977
|
-
` \`${getCliName()} network ls\`.`
|
|
10978
|
-
] : [],
|
|
10979
|
-
` → ${getCliName()} network ls`
|
|
10980
|
-
], deps.log);
|
|
10981
|
-
return 0;
|
|
10982
11755
|
}
|
|
10983
|
-
function
|
|
10984
|
-
|
|
10985
|
-
|
|
10986
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
11756
|
+
function stopNote(result, notice) {
|
|
11757
|
+
if (!result || typeof result !== "object")
|
|
11758
|
+
return;
|
|
11759
|
+
const { action, note, container } = result;
|
|
11760
|
+
if (action !== "stop" || typeof container !== "string")
|
|
11761
|
+
return;
|
|
11762
|
+
if (note === "default") {
|
|
11763
|
+
notice(`${container} is the sandbox's default container: SSH, commands and files ask which container until it runs again.`);
|
|
11764
|
+
} else if (note === "last_running") {
|
|
11765
|
+
notice("No container is running now: SSH, commands and files have none to land in until one runs.");
|
|
11766
|
+
} else if (note === "sandbox_stopped") {
|
|
11767
|
+
notice(`The sandbox is stopped, so ${container} is too: nothing was done.`);
|
|
10991
11768
|
}
|
|
10992
|
-
return clashes;
|
|
10993
|
-
}
|
|
10994
|
-
function clearingNotice(cookieSite) {
|
|
10995
|
-
const site = cookieSite?.site ?? "the net domain";
|
|
10996
|
-
return [
|
|
10997
|
-
" Short names meant another organization's sandboxes before. The first short-name site each",
|
|
10998
|
-
` browser opens now clears its data once: the site's storage, and every cookie of ${site}.`,
|
|
10999
|
-
` That signs the browser out of every site under ${site}${cookieSite?.includesApp ? ", the Ghostdock app included" : ""}.`,
|
|
11000
|
-
" Reload or close browser tabs opened on short names before this connect."
|
|
11001
|
-
];
|
|
11002
11769
|
}
|
|
11003
|
-
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11770
|
+
var PROGRESS_MESSAGES = {
|
|
11771
|
+
sandboxes_create: "Creating sandbox…",
|
|
11772
|
+
sandboxes_wake: "Waking sandbox…",
|
|
11773
|
+
sandboxes_stop: "Stopping sandbox…",
|
|
11774
|
+
sandboxes_delete: "Deleting sandbox…",
|
|
11775
|
+
sandboxes_resize: "Resizing sandbox…",
|
|
11776
|
+
sandboxes_resize_disk: "Resizing disk…",
|
|
11777
|
+
sandboxes_commands_run: "Running command…",
|
|
11778
|
+
sandboxes_containers_start: "Starting container…",
|
|
11779
|
+
sandboxes_containers_stop: "Stopping container…",
|
|
11780
|
+
sandboxes_containers_restart: "Restarting container…",
|
|
11781
|
+
snapshots_create: "Saving snapshot…",
|
|
11782
|
+
projects_redeploy: "Redeploying…",
|
|
11783
|
+
providers_verify_incus: "Checking the Incus host…"
|
|
11784
|
+
};
|
|
11785
|
+
function withElapsed(lines, elapsed) {
|
|
11786
|
+
const color = colorEnabled(process.stdout);
|
|
11787
|
+
const [first, ...rest] = lines;
|
|
11788
|
+
if (first === undefined || first.startsWith(" ")) {
|
|
11789
|
+
return [eventLine("done", `Done in ${elapsed}.`, color), ...lines];
|
|
11012
11790
|
}
|
|
11013
|
-
|
|
11014
|
-
return 0;
|
|
11791
|
+
return [`${first} ${dim(`in ${elapsed}`, color)}`, ...rest];
|
|
11015
11792
|
}
|
|
11016
|
-
|
|
11017
|
-
const
|
|
11018
|
-
const
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
const
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
asked: true,
|
|
11034
|
-
provider: up ? device?.gatewayProvider ?? null : null,
|
|
11035
|
-
staleMtu
|
|
11793
|
+
function cliText(catalog, skills = BUILTIN_SKILL_NAMES) {
|
|
11794
|
+
const paths = new Map(catalog.filter((tool) => tool.cli).map((tool) => [tool.name, tool.cli]));
|
|
11795
|
+
const names = [...paths.keys()].sort((a, b) => b.length - a.length);
|
|
11796
|
+
const pattern = names.length > 0 ? new RegExp(`\\b(${names.join("|")})\\b`, "g") : null;
|
|
11797
|
+
return (text) => {
|
|
11798
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
11799
|
+
const commands = pattern ? oneLine.replace(pattern, (name) => paths.get(name)?.join(" ") ?? name) : oneLine;
|
|
11800
|
+
return commands.replace(/\bthe ([a-z][a-z0-9-]*) skill\b/g, (phrase, skill) => skills.includes(skill) ? `the ${skill} skill (\`${getCliName()} skills get ${skill}\`)` : phrase);
|
|
11801
|
+
};
|
|
11802
|
+
}
|
|
11803
|
+
function cliOnlyArgs(tool) {
|
|
11804
|
+
const schema = tool.inputSchema;
|
|
11805
|
+
const args = {};
|
|
11806
|
+
if (schema.properties && "container" in schema.properties && !schema.required?.includes("container")) {
|
|
11807
|
+
args.start = {
|
|
11808
|
+
type: "boolean",
|
|
11809
|
+
description: "Start the container first when it is not running, without asking."
|
|
11036
11810
|
};
|
|
11037
|
-
}
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
asked: false,
|
|
11044
|
-
provider: up ? state.gatewayProvider : null,
|
|
11045
|
-
staleMtu: null
|
|
11811
|
+
}
|
|
11812
|
+
if (tool.name === "sandboxes_containers_logs") {
|
|
11813
|
+
args.follow = {
|
|
11814
|
+
type: "boolean",
|
|
11815
|
+
alias: "f",
|
|
11816
|
+
description: "Keep printing new lines until Ctrl-C."
|
|
11046
11817
|
};
|
|
11818
|
+
args.timestamps = { type: "boolean", description: "Put each line's time in front of it." };
|
|
11047
11819
|
}
|
|
11820
|
+
return args;
|
|
11048
11821
|
}
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
lines.push(` Short names: ${device.shortNameOrganization.slug}'s sandboxes.`);
|
|
11063
|
-
}
|
|
11064
|
-
if (!device.gatewayProvider) {
|
|
11065
|
-
lines.push(` Its provider is gone, so no gateway serves it. Run \`${getCliName()} network connect\`.`);
|
|
11822
|
+
var generatedToolCommands = new WeakSet;
|
|
11823
|
+
function createToolCommands(catalog) {
|
|
11824
|
+
const commands = {};
|
|
11825
|
+
const toCliText = cliText(catalog);
|
|
11826
|
+
for (const tool of catalog) {
|
|
11827
|
+
const command = defineCommand({
|
|
11828
|
+
meta: {
|
|
11829
|
+
name: tool.cli?.at(-1) ?? tool.name,
|
|
11830
|
+
description: toCliText(tool.description)
|
|
11831
|
+
},
|
|
11832
|
+
args: { ...buildToolArgs(tool.inputSchema, toCliText, tool.cliArgs), ...cliOnlyArgs(tool) },
|
|
11833
|
+
async run({ args }) {
|
|
11834
|
+
process.exitCode = await runToolInvoke(tool, args);
|
|
11066
11835
|
}
|
|
11067
|
-
}
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
if (provider)
|
|
11071
|
-
lines.push(` Reachable: the sandboxes on ${provider.name}.`);
|
|
11072
|
-
return lines;
|
|
11073
|
-
}, deps.log);
|
|
11074
|
-
return 0;
|
|
11075
|
-
}
|
|
11076
|
-
function unreachableNote(sandbox, connected) {
|
|
11077
|
-
if (!sandbox.provider.privateNetwork) {
|
|
11078
|
-
return `not reachable: ${sandbox.provider.name} has no private network yet`;
|
|
11836
|
+
});
|
|
11837
|
+
generatedToolCommands.add(command);
|
|
11838
|
+
commands[tool.name] = command;
|
|
11079
11839
|
}
|
|
11080
|
-
|
|
11081
|
-
return null;
|
|
11082
|
-
return `not reachable: on ${sandbox.provider.name} → ${connectCommandFor(sandbox.provider)}`;
|
|
11840
|
+
return commands;
|
|
11083
11841
|
}
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
for (const s of listed.sandboxes) {
|
|
11107
|
-
const note = unreachableNote(s, connected);
|
|
11108
|
-
lines.push([
|
|
11109
|
-
` ${s.name.padEnd(width)}`,
|
|
11110
|
-
(s.address ?? "-").padEnd(28),
|
|
11111
|
-
s.canonicalName.padEnd(canonicalWidth),
|
|
11112
|
-
...s.httpsName ? [`https://${s.httpsName}`.padEnd(httpsWidth)] : [],
|
|
11113
|
-
...s.shortName ? [`https://${s.shortName}`.padEnd(shortWidth)] : [],
|
|
11114
|
-
s.status,
|
|
11115
|
-
...note ? [`(${note})`] : []
|
|
11116
|
-
].join(" "));
|
|
11117
|
-
}
|
|
11118
|
-
return lines;
|
|
11119
|
-
}, deps.log);
|
|
11120
|
-
return 0;
|
|
11121
|
-
} catch (err) {
|
|
11122
|
-
deps.error(message(err));
|
|
11123
|
-
return 1;
|
|
11842
|
+
|
|
11843
|
+
// src/commands/login.ts
|
|
11844
|
+
import { hostname as hostname2 } from "node:os";
|
|
11845
|
+
|
|
11846
|
+
// src/commands/login-organization.ts
|
|
11847
|
+
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
11848
|
+
function label(organization) {
|
|
11849
|
+
return `${organization.name} (${organization.slug ?? organization.id})`;
|
|
11850
|
+
}
|
|
11851
|
+
var chooseOrganizationOnTerminal = async (organizations) => {
|
|
11852
|
+
console.log(`
|
|
11853
|
+
Which organization should commands use by default?`);
|
|
11854
|
+
organizations.forEach((organization, index) => {
|
|
11855
|
+
console.log(` ${index + 1}. ${label(organization)}`);
|
|
11856
|
+
});
|
|
11857
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
11858
|
+
try {
|
|
11859
|
+
const answer = (await prompt.question(`Number (Enter to skip): `)).trim();
|
|
11860
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
11861
|
+
return Number.isInteger(index) && index >= 0 && index < organizations.length ? index : null;
|
|
11862
|
+
} finally {
|
|
11863
|
+
prompt.close();
|
|
11124
11864
|
}
|
|
11865
|
+
};
|
|
11866
|
+
function defaultChooser() {
|
|
11867
|
+
return process.stdin.isTTY && process.stdout.isTTY ? chooseOrganizationOnTerminal : undefined;
|
|
11125
11868
|
}
|
|
11126
|
-
async function
|
|
11869
|
+
async function chooseDefaultOrganizationAfterLogin(org, log, choose = defaultChooser()) {
|
|
11870
|
+
const cli = getCliName();
|
|
11871
|
+
let organizations;
|
|
11127
11872
|
try {
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11873
|
+
({ organizations } = await authorizedRequest("/api/v1/tools/organizations_list", { method: "POST", body: "{}" }));
|
|
11874
|
+
} catch {
|
|
11875
|
+
organizations = undefined;
|
|
11876
|
+
}
|
|
11877
|
+
if (!Array.isArray(organizations)) {
|
|
11878
|
+
log(eventLine("item", `Pick a default organization with \`${cli} organizations use <org>\`.`));
|
|
11879
|
+
return org ? 1 : 0;
|
|
11880
|
+
}
|
|
11881
|
+
const settings = await readSettings();
|
|
11882
|
+
const save = async (organization, why) => {
|
|
11883
|
+
settings.defaultOrganization = { id: organization.id, slug: organization.slug };
|
|
11884
|
+
await writeSettings(settings);
|
|
11885
|
+
log(eventLine("done", `Default organization: ${label(organization)}${why}.`));
|
|
11886
|
+
};
|
|
11887
|
+
const ref = org?.trim();
|
|
11888
|
+
if (ref) {
|
|
11889
|
+
const match = organizations.find((organization) => organization.id === ref) ?? organizations.find((organization) => organization.slug === ref);
|
|
11890
|
+
if (!match) {
|
|
11891
|
+
log(eventLine("failed", `You are not a member of an organization "${ref}"; no default set.`));
|
|
11892
|
+
return 1;
|
|
11893
|
+
}
|
|
11894
|
+
await save(match, "");
|
|
11895
|
+
return 0;
|
|
11896
|
+
}
|
|
11897
|
+
const current = organizations.find((organization) => organization.id === settings.defaultOrganization?.id);
|
|
11898
|
+
if (current) {
|
|
11899
|
+
log(eventLine("item", `Default organization: ${label(current)} (unchanged).`));
|
|
11900
|
+
return 0;
|
|
11901
|
+
}
|
|
11902
|
+
if (settings.defaultOrganization) {
|
|
11903
|
+
delete settings.defaultOrganization;
|
|
11904
|
+
await writeSettings(settings);
|
|
11905
|
+
}
|
|
11906
|
+
const [only, ...others] = organizations;
|
|
11907
|
+
if (!only) {
|
|
11908
|
+
log(eventLine("item", "You do not belong to an organization yet."));
|
|
11909
|
+
return 0;
|
|
11910
|
+
}
|
|
11911
|
+
if (others.length === 0) {
|
|
11912
|
+
await save(only, " (your only organization)");
|
|
11913
|
+
return 0;
|
|
11914
|
+
}
|
|
11915
|
+
const index = choose ? await choose(organizations) : null;
|
|
11916
|
+
const chosen = index === null ? undefined : organizations[index];
|
|
11917
|
+
if (chosen) {
|
|
11918
|
+
await save(chosen, "");
|
|
11131
11919
|
return 0;
|
|
11132
|
-
} catch (err) {
|
|
11133
|
-
deps.error(message(err));
|
|
11134
|
-
return 1;
|
|
11135
11920
|
}
|
|
11921
|
+
log(eventLine("item", `You belong to ${organizations.length} organizations: ${organizations.map((organization) => organization.slug ?? organization.id).join(", ")}. Pick a default with \`${cli} organizations use <org>\`, or pass --org.`));
|
|
11922
|
+
return 0;
|
|
11136
11923
|
}
|
|
11137
|
-
|
|
11924
|
+
|
|
11925
|
+
// src/commands/login.ts
|
|
11926
|
+
function defaultClientName() {
|
|
11138
11927
|
try {
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11928
|
+
const host = hostname2().trim();
|
|
11929
|
+
if (host)
|
|
11930
|
+
return host.slice(0, 64);
|
|
11931
|
+
} catch {}
|
|
11932
|
+
return "ghostdock-cli";
|
|
11933
|
+
}
|
|
11934
|
+
var DEFAULT_TIMEOUT_SECONDS = 300;
|
|
11935
|
+
function toCredentials(result) {
|
|
11936
|
+
return {
|
|
11937
|
+
accessToken: result.accessToken,
|
|
11938
|
+
refreshToken: result.refreshToken,
|
|
11939
|
+
accessExpiresAt: new Date(Date.now() + result.expiresIn * 1000).toISOString()
|
|
11940
|
+
};
|
|
11941
|
+
}
|
|
11942
|
+
function sleep(ms) {
|
|
11943
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11944
|
+
}
|
|
11945
|
+
async function runLogin(args, log = console.log, choose = defaultChooser(), error = printError) {
|
|
11946
|
+
const loggedIn = async () => {
|
|
11947
|
+
log(eventLine("ready", "Logged in."));
|
|
11948
|
+
return chooseDefaultOrganizationAfterLogin(args.org, log, choose);
|
|
11949
|
+
};
|
|
11950
|
+
if (args.code) {
|
|
11951
|
+
const result = await deviceToken({ pasteCode: args.code });
|
|
11952
|
+
if (result.status === "approved") {
|
|
11953
|
+
await writeCredentials(toCredentials(result));
|
|
11954
|
+
return loggedIn();
|
|
11955
|
+
}
|
|
11956
|
+
error("That code has not been approved yet. Approve it in the browser, then run this again.");
|
|
11146
11957
|
return 1;
|
|
11147
11958
|
}
|
|
11959
|
+
const start = await deviceStart({ client_name: defaultClientName() });
|
|
11960
|
+
log(eventLine("url", `Open this URL to sign in: ${start.verificationUrl}`));
|
|
11961
|
+
log(` Pass --wait to finish here once you approve, or approve on another device and run \`${getCliName()} login --code <paste-code>\` with the code shown there.`);
|
|
11962
|
+
if (!args.wait) {
|
|
11963
|
+
return 0;
|
|
11964
|
+
}
|
|
11965
|
+
const timeoutSeconds = Number.parseInt(args.timeout ?? "", 10) || DEFAULT_TIMEOUT_SECONDS;
|
|
11966
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
11967
|
+
const intervalMs = start.interval * 1000;
|
|
11968
|
+
const progress = startProgress("Waiting for approval in the browser…");
|
|
11969
|
+
while (Date.now() < deadline) {
|
|
11970
|
+
await sleep(intervalMs);
|
|
11971
|
+
try {
|
|
11972
|
+
const result = await deviceToken({ deviceCode: start.deviceCode });
|
|
11973
|
+
if (result.status === "approved") {
|
|
11974
|
+
progress.stop();
|
|
11975
|
+
await writeCredentials(toCredentials(result));
|
|
11976
|
+
return loggedIn();
|
|
11977
|
+
}
|
|
11978
|
+
} catch (err) {
|
|
11979
|
+
progress.stop();
|
|
11980
|
+
if (err instanceof ApiError && err.status === 400) {
|
|
11981
|
+
error(`Login failed: ${err.message}`);
|
|
11982
|
+
return 1;
|
|
11983
|
+
}
|
|
11984
|
+
throw err;
|
|
11985
|
+
}
|
|
11986
|
+
}
|
|
11987
|
+
progress.stop();
|
|
11988
|
+
error(`Timed out waiting for approval. Run \`${getCliName()} login\` again.`);
|
|
11989
|
+
return 1;
|
|
11148
11990
|
}
|
|
11149
|
-
var
|
|
11991
|
+
var loginCommand = defineCommand({
|
|
11150
11992
|
meta: {
|
|
11151
|
-
name: "
|
|
11152
|
-
description: "
|
|
11993
|
+
name: "login",
|
|
11994
|
+
description: "Sign in and store a device access token for this machine (~/.config/ghostdock/credentials.json)."
|
|
11153
11995
|
},
|
|
11154
11996
|
args: {
|
|
11155
|
-
|
|
11156
|
-
type: "
|
|
11157
|
-
|
|
11158
|
-
|
|
11997
|
+
wait: {
|
|
11998
|
+
type: "boolean",
|
|
11999
|
+
description: "Poll until the login is approved in the browser (default timeout 5 minutes).",
|
|
12000
|
+
default: false
|
|
11159
12001
|
},
|
|
11160
|
-
|
|
12002
|
+
code: {
|
|
12003
|
+
type: "string",
|
|
12004
|
+
description: "Redeem the one-time paste code shown in the browser instead of polling."
|
|
12005
|
+
},
|
|
12006
|
+
timeout: {
|
|
12007
|
+
type: "string",
|
|
12008
|
+
description: "Max seconds to poll when using --wait (default 300)."
|
|
12009
|
+
},
|
|
12010
|
+
org: {
|
|
12011
|
+
type: "string",
|
|
12012
|
+
description: "Set this organization (id or slug) as the default once logged in."
|
|
12013
|
+
}
|
|
11161
12014
|
},
|
|
11162
12015
|
async run({ args }) {
|
|
11163
|
-
process.exitCode = await
|
|
12016
|
+
process.exitCode = await runLogin(args);
|
|
11164
12017
|
}
|
|
11165
12018
|
});
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
12019
|
+
|
|
12020
|
+
// src/commands/logout.ts
|
|
12021
|
+
async function runLogout(log = console.log) {
|
|
12022
|
+
const credentials = await readCredentials();
|
|
12023
|
+
if (credentials) {
|
|
12024
|
+
try {
|
|
12025
|
+
await deviceRevoke({
|
|
12026
|
+
accessToken: credentials.accessToken,
|
|
12027
|
+
refreshToken: credentials.refreshToken
|
|
12028
|
+
});
|
|
12029
|
+
} catch {}
|
|
11170
12030
|
}
|
|
11171
|
-
|
|
11172
|
-
|
|
12031
|
+
await clearCredentials();
|
|
12032
|
+
log(eventLine("stopped", "Logged out."));
|
|
12033
|
+
return 0;
|
|
12034
|
+
}
|
|
12035
|
+
var logoutCommand = defineCommand({
|
|
11173
12036
|
meta: {
|
|
11174
|
-
name: "
|
|
11175
|
-
description: "
|
|
12037
|
+
name: "logout",
|
|
12038
|
+
description: "Revoke the device session and remove stored credentials."
|
|
11176
12039
|
},
|
|
11177
12040
|
async run() {
|
|
11178
|
-
process.exitCode = await
|
|
11179
|
-
}
|
|
11180
|
-
});
|
|
11181
|
-
var lsCommand = defineCommand({
|
|
11182
|
-
meta: {
|
|
11183
|
-
name: "ls",
|
|
11184
|
-
description: "Sandboxes of an organization with their private addresses and names, and which are reachable through the current connection."
|
|
11185
|
-
},
|
|
11186
|
-
args: { org: orgArg },
|
|
11187
|
-
async run({ args }) {
|
|
11188
|
-
process.exitCode = await runNetworkLs({ org: args.org });
|
|
11189
|
-
}
|
|
11190
|
-
});
|
|
11191
|
-
var devicesRemoveCommand = defineCommand({
|
|
11192
|
-
meta: { name: "remove", description: "Remove one of your devices from the private network." },
|
|
11193
|
-
args: {
|
|
11194
|
-
id: { type: "positional", description: "Device id, from `network devices`.", required: true }
|
|
11195
|
-
},
|
|
11196
|
-
async run({ args }) {
|
|
11197
|
-
process.exitCode = await runNetworkDevicesRemove({ id: args.id });
|
|
11198
|
-
}
|
|
11199
|
-
});
|
|
11200
|
-
var devicesCommand = defineCommand({
|
|
11201
|
-
meta: { name: "devices", description: "Your devices in the private network." },
|
|
11202
|
-
subCommands: { remove: devicesRemoveCommand },
|
|
11203
|
-
async run({ rawArgs }) {
|
|
11204
|
-
if (rawArgs[0] === "remove")
|
|
11205
|
-
return;
|
|
11206
|
-
process.exitCode = await runNetworkDevices();
|
|
11207
|
-
}
|
|
11208
|
-
});
|
|
11209
|
-
var networkCommand = defineCommand({
|
|
11210
|
-
meta: {
|
|
11211
|
-
name: "network",
|
|
11212
|
-
description: "The private network: reach your sandboxes by address and name from this machine."
|
|
11213
|
-
},
|
|
11214
|
-
subCommands: {
|
|
11215
|
-
connect: connectCommand,
|
|
11216
|
-
disconnect: disconnectCommand,
|
|
11217
|
-
status: statusCommand,
|
|
11218
|
-
ls: lsCommand,
|
|
11219
|
-
devices: devicesCommand
|
|
12041
|
+
process.exitCode = await runLogout();
|
|
11220
12042
|
}
|
|
11221
12043
|
});
|
|
11222
12044
|
|
|
@@ -11295,8 +12117,8 @@ var organizationsCurrentCommand = defineCommand({
|
|
|
11295
12117
|
});
|
|
11296
12118
|
|
|
11297
12119
|
// src/commands/publish-auto.ts
|
|
11298
|
-
import { readdir, readFile as
|
|
11299
|
-
import { basename as basename2, join as
|
|
12120
|
+
import { readdir as readdir2, readFile as readFile5, stat } from "node:fs/promises";
|
|
12121
|
+
import { basename as basename2, join as join5, relative, sep } from "node:path";
|
|
11300
12122
|
function publishAutoLines(result) {
|
|
11301
12123
|
const [summaryLine, ...rest] = formatPublishAutoResult(result).split(`
|
|
11302
12124
|
`);
|
|
@@ -11373,23 +12195,23 @@ function formatAttemptedSpec(spec) {
|
|
|
11373
12195
|
async function collectPath(path) {
|
|
11374
12196
|
const pathStats = await stat(path);
|
|
11375
12197
|
if (pathStats.isFile()) {
|
|
11376
|
-
return [{ path: basename2(path), data: await
|
|
12198
|
+
return [{ path: basename2(path), data: await readFile5(path) }];
|
|
11377
12199
|
}
|
|
11378
12200
|
if (!pathStats.isDirectory()) {
|
|
11379
12201
|
throw new Error(`Publish path must be a file or directory: ${path}`);
|
|
11380
12202
|
}
|
|
11381
12203
|
const files = [];
|
|
11382
12204
|
async function visit(currentPath) {
|
|
11383
|
-
const entries = await
|
|
12205
|
+
const entries = await readdir2(currentPath, { withFileTypes: true });
|
|
11384
12206
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
11385
12207
|
for (const entry of entries) {
|
|
11386
|
-
const entryPath =
|
|
12208
|
+
const entryPath = join5(currentPath, entry.name);
|
|
11387
12209
|
if (entry.isDirectory()) {
|
|
11388
12210
|
await visit(entryPath);
|
|
11389
12211
|
} else if (entry.isFile()) {
|
|
11390
12212
|
files.push({
|
|
11391
12213
|
path: relative(path, entryPath).split(sep).join("/"),
|
|
11392
|
-
data: await
|
|
12214
|
+
data: await readFile5(entryPath)
|
|
11393
12215
|
});
|
|
11394
12216
|
}
|
|
11395
12217
|
}
|
|
@@ -11432,7 +12254,7 @@ async function runPublishAuto(options, log = console.log, error = printError) {
|
|
|
11432
12254
|
}
|
|
11433
12255
|
}
|
|
11434
12256
|
if (options.zip) {
|
|
11435
|
-
form.append("zip", new Blob([await
|
|
12257
|
+
form.append("zip", new Blob([await readFile5(options.zip)]), basename2(options.zip));
|
|
11436
12258
|
}
|
|
11437
12259
|
const { value: result } = await withProgress("Planning and deploying… (this can take a few minutes)", () => authorizedRequest(`/api/v1/organizations/${encodeURIComponent(options.organizationId)}/publish-auto`, { method: "POST", body: form }));
|
|
11438
12260
|
printResult(result, () => publishAutoLines(result), log);
|
|
@@ -11555,28 +12377,28 @@ var publishAutoCommand = defineCommand({
|
|
|
11555
12377
|
});
|
|
11556
12378
|
|
|
11557
12379
|
// src/commands/publish-static.ts
|
|
11558
|
-
import { readdir as
|
|
11559
|
-
import { basename as basename3, join as
|
|
12380
|
+
import { readdir as readdir3, readFile as readFile6, stat as stat2 } from "node:fs/promises";
|
|
12381
|
+
import { basename as basename3, join as join6, relative as relative2, sep as sep2 } from "node:path";
|
|
11560
12382
|
async function collectPath2(path) {
|
|
11561
12383
|
const pathStats = await stat2(path);
|
|
11562
12384
|
if (pathStats.isFile()) {
|
|
11563
|
-
return [{ path: basename3(path), data: await
|
|
12385
|
+
return [{ path: basename3(path), data: await readFile6(path) }];
|
|
11564
12386
|
}
|
|
11565
12387
|
if (!pathStats.isDirectory()) {
|
|
11566
12388
|
throw new Error(`Publish path must be a file or directory: ${path}`);
|
|
11567
12389
|
}
|
|
11568
12390
|
const files = [];
|
|
11569
12391
|
async function visit(currentPath) {
|
|
11570
|
-
const entries = await
|
|
12392
|
+
const entries = await readdir3(currentPath, { withFileTypes: true });
|
|
11571
12393
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
11572
12394
|
for (const entry of entries) {
|
|
11573
|
-
const entryPath =
|
|
12395
|
+
const entryPath = join6(currentPath, entry.name);
|
|
11574
12396
|
if (entry.isDirectory()) {
|
|
11575
12397
|
await visit(entryPath);
|
|
11576
12398
|
} else if (entry.isFile()) {
|
|
11577
12399
|
files.push({
|
|
11578
12400
|
path: relative2(path, entryPath).split(sep2).join("/"),
|
|
11579
|
-
data: await
|
|
12401
|
+
data: await readFile6(entryPath)
|
|
11580
12402
|
});
|
|
11581
12403
|
}
|
|
11582
12404
|
}
|
|
@@ -11599,7 +12421,7 @@ async function runPublishStatic(options, log = console.log, error = printError)
|
|
|
11599
12421
|
}
|
|
11600
12422
|
}
|
|
11601
12423
|
if (options.zip) {
|
|
11602
|
-
form.append("zip", new Blob([await
|
|
12424
|
+
form.append("zip", new Blob([await readFile6(options.zip)]), basename3(options.zip));
|
|
11603
12425
|
}
|
|
11604
12426
|
const { value: result } = await withProgress("Publishing…", () => authorizedRequest(`/api/v1/organizations/${encodeURIComponent(options.organizationId)}/static-sites`, { method: "POST", body: form }));
|
|
11605
12427
|
printResult(result, () => [eventLine("url", result.url), eventLine("done", `Published site ${result.siteId}.`)], log);
|
|
@@ -11653,7 +12475,7 @@ var publishStaticCommand = defineCommand({
|
|
|
11653
12475
|
});
|
|
11654
12476
|
|
|
11655
12477
|
// src/commands/registry.ts
|
|
11656
|
-
import { spawn as
|
|
12478
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
11657
12479
|
function reportError2(err, error) {
|
|
11658
12480
|
if (err instanceof AuthRequiredError || err instanceof Error) {
|
|
11659
12481
|
error(err.message);
|
|
@@ -11670,7 +12492,7 @@ function defaultInvoke(input) {
|
|
|
11670
12492
|
}
|
|
11671
12493
|
function dockerLogin(input) {
|
|
11672
12494
|
return new Promise((resolve, reject) => {
|
|
11673
|
-
const child =
|
|
12495
|
+
const child = spawn3("docker", ["login", input.host, "-u", input.username, "--password-stdin"], {
|
|
11674
12496
|
stdio: ["pipe", "inherit", "pipe"]
|
|
11675
12497
|
});
|
|
11676
12498
|
let stderr = "";
|
|
@@ -11768,8 +12590,8 @@ var registryCommand = defineCommand({
|
|
|
11768
12590
|
});
|
|
11769
12591
|
|
|
11770
12592
|
// src/commands/sandbox-create.ts
|
|
11771
|
-
import { readdir as
|
|
11772
|
-
import { basename as basename4, join as
|
|
12593
|
+
import { readdir as readdir4, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
12594
|
+
import { basename as basename4, join as join7, relative as relative3, sep as sep3 } from "node:path";
|
|
11773
12595
|
|
|
11774
12596
|
// ../../node_modules/fflate/esm/index.mjs
|
|
11775
12597
|
import { createRequire } from "module";
|
|
@@ -12422,16 +13244,16 @@ async function collectDirectory(path) {
|
|
|
12422
13244
|
}
|
|
12423
13245
|
const files = [];
|
|
12424
13246
|
async function visit(currentPath) {
|
|
12425
|
-
const entries = await
|
|
13247
|
+
const entries = await readdir4(currentPath, { withFileTypes: true });
|
|
12426
13248
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
12427
13249
|
for (const entry of entries) {
|
|
12428
|
-
const entryPath =
|
|
13250
|
+
const entryPath = join7(currentPath, entry.name);
|
|
12429
13251
|
if (entry.isDirectory()) {
|
|
12430
13252
|
await visit(entryPath);
|
|
12431
13253
|
} else if (entry.isFile()) {
|
|
12432
13254
|
files.push({
|
|
12433
13255
|
path: relative3(path, entryPath).split(sep3).join("/"),
|
|
12434
|
-
data: await
|
|
13256
|
+
data: await readFile7(entryPath)
|
|
12435
13257
|
});
|
|
12436
13258
|
}
|
|
12437
13259
|
}
|
|
@@ -12464,7 +13286,7 @@ async function runSandboxCreate(options, log = console.log, error = printError)
|
|
|
12464
13286
|
return 1;
|
|
12465
13287
|
}
|
|
12466
13288
|
try {
|
|
12467
|
-
const zipBytes = options.zip ? new Uint8Array(await
|
|
13289
|
+
const zipBytes = options.zip ? new Uint8Array(await readFile7(options.zip)) : zipDirectory(await collectDirectory(options.paths[0] ?? ""));
|
|
12468
13290
|
const form = new FormData;
|
|
12469
13291
|
form.append("kind", options.kind);
|
|
12470
13292
|
if (options.displayName)
|
|
@@ -12725,28 +13547,28 @@ var toolsCommand = defineCommand({
|
|
|
12725
13547
|
});
|
|
12726
13548
|
|
|
12727
13549
|
// src/commands/upload-files.ts
|
|
12728
|
-
import { readdir as
|
|
12729
|
-
import { basename as basename5, join as
|
|
13550
|
+
import { readdir as readdir5, readFile as readFile8, stat as stat4 } from "node:fs/promises";
|
|
13551
|
+
import { basename as basename5, join as join8, relative as relative4, sep as sep4 } from "node:path";
|
|
12730
13552
|
async function collectPath3(path) {
|
|
12731
13553
|
const pathStats = await stat4(path);
|
|
12732
13554
|
if (pathStats.isFile()) {
|
|
12733
|
-
return [{ path: basename5(path), data: await
|
|
13555
|
+
return [{ path: basename5(path), data: await readFile8(path) }];
|
|
12734
13556
|
}
|
|
12735
13557
|
if (!pathStats.isDirectory()) {
|
|
12736
13558
|
throw new Error(`Upload path must be a file or directory: ${path}`);
|
|
12737
13559
|
}
|
|
12738
13560
|
const files = [];
|
|
12739
13561
|
async function visit(currentPath) {
|
|
12740
|
-
const entries = await
|
|
13562
|
+
const entries = await readdir5(currentPath, { withFileTypes: true });
|
|
12741
13563
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
12742
13564
|
for (const entry of entries) {
|
|
12743
|
-
const entryPath =
|
|
13565
|
+
const entryPath = join8(currentPath, entry.name);
|
|
12744
13566
|
if (entry.isDirectory()) {
|
|
12745
13567
|
await visit(entryPath);
|
|
12746
13568
|
} else if (entry.isFile()) {
|
|
12747
13569
|
files.push({
|
|
12748
13570
|
path: relative4(path, entryPath).split(sep4).join("/"),
|
|
12749
|
-
data: await
|
|
13571
|
+
data: await readFile8(entryPath)
|
|
12750
13572
|
});
|
|
12751
13573
|
}
|
|
12752
13574
|
}
|
|
@@ -12768,7 +13590,7 @@ async function runSandboxUpload(options, log = console.log, error = printError,
|
|
|
12768
13590
|
}
|
|
12769
13591
|
}
|
|
12770
13592
|
if (options.zip) {
|
|
12771
|
-
form.append("zip", new Blob([await
|
|
13593
|
+
form.append("zip", new Blob([await readFile8(options.zip)]), basename5(options.zip));
|
|
12772
13594
|
}
|
|
12773
13595
|
let container = options.container;
|
|
12774
13596
|
const call = {
|
|
@@ -12910,7 +13732,8 @@ var HANDWRITTEN_COMMANDS = [
|
|
|
12910
13732
|
[["sandboxes", "files", "upload"], sandboxUploadCommand],
|
|
12911
13733
|
[["sites", "publish"], publishStaticCommand],
|
|
12912
13734
|
[["projects", "publish"], publishAutoCommand],
|
|
12913
|
-
[["network"], networkCommand]
|
|
13735
|
+
[["network"], networkCommand],
|
|
13736
|
+
[["docker"], dockerCommand]
|
|
12914
13737
|
];
|
|
12915
13738
|
var HANDWRITTEN_PATHS = new Set(HANDWRITTEN_COMMANDS.map(([path]) => path.join(" ")));
|
|
12916
13739
|
function createMainCommand(catalog = OFFLINE_TOOL_CATALOG) {
|