ghostdock 0.0.15 → 0.0.17
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 +5 -2
- package/dist/cli.md +30 -3
- package/dist/index.js +674 -395
- 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,
|
|
@@ -9176,7 +9184,7 @@ function getCliName(argv = process.argv) {
|
|
|
9176
9184
|
return CLI_NAMES.find((name) => name === invoked) ?? "ghostdock";
|
|
9177
9185
|
}
|
|
9178
9186
|
// package.json
|
|
9179
|
-
var version = "0.0.
|
|
9187
|
+
var version = "0.0.17";
|
|
9180
9188
|
var package_default = {
|
|
9181
9189
|
name: "ghostdock",
|
|
9182
9190
|
description: "Ghostdock CLI — device login and agent tool invocation from the command line",
|
|
@@ -9284,6 +9292,376 @@ async function clearCredentials(path = getCredentialsPath()) {
|
|
|
9284
9292
|
await rm(path, { force: true });
|
|
9285
9293
|
}
|
|
9286
9294
|
|
|
9295
|
+
// ../../shared/lib/private-network-pause.ts
|
|
9296
|
+
function formatPauseLimit(seconds) {
|
|
9297
|
+
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
9298
|
+
if (minutes >= 2 * 1440 && minutes % 1440 === 0)
|
|
9299
|
+
return `${minutes / 1440} days`;
|
|
9300
|
+
const hours = Math.floor(minutes / 60);
|
|
9301
|
+
const rest = minutes % 60;
|
|
9302
|
+
if (hours === 0)
|
|
9303
|
+
return `${rest} min`;
|
|
9304
|
+
return rest === 0 ? `${hours} h` : `${hours} h ${rest} min`;
|
|
9305
|
+
}
|
|
9306
|
+
function devicePauseReasonText(reason, limits) {
|
|
9307
|
+
switch (reason) {
|
|
9308
|
+
case "idle":
|
|
9309
|
+
return `idle ${formatPauseLimit(limits.idleTimeoutSeconds)}`;
|
|
9310
|
+
case "max_session":
|
|
9311
|
+
return `${formatPauseLimit(limits.maxSessionSeconds)} session limit`;
|
|
9312
|
+
case "manual":
|
|
9313
|
+
return "paused in the web app";
|
|
9314
|
+
}
|
|
9315
|
+
}
|
|
9316
|
+
|
|
9317
|
+
// src/output.ts
|
|
9318
|
+
var SYMBOLS = {
|
|
9319
|
+
ready: "⚓",
|
|
9320
|
+
done: "✓",
|
|
9321
|
+
url: "↗",
|
|
9322
|
+
stopped: "◌",
|
|
9323
|
+
failed: "✗",
|
|
9324
|
+
progress: "▂▄▆",
|
|
9325
|
+
item: "•"
|
|
9326
|
+
};
|
|
9327
|
+
var jsonOutput = false;
|
|
9328
|
+
function setJsonOutput(on) {
|
|
9329
|
+
jsonOutput = on;
|
|
9330
|
+
}
|
|
9331
|
+
function isJsonOutput() {
|
|
9332
|
+
return jsonOutput;
|
|
9333
|
+
}
|
|
9334
|
+
function colorEnabled(stream, env = process.env) {
|
|
9335
|
+
if (env.FORCE_COLOR !== undefined)
|
|
9336
|
+
return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
|
|
9337
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
|
|
9338
|
+
return false;
|
|
9339
|
+
if (env.TERM === "dumb")
|
|
9340
|
+
return false;
|
|
9341
|
+
return stream.isTTY === true;
|
|
9342
|
+
}
|
|
9343
|
+
function trueColor(env = process.env) {
|
|
9344
|
+
return env.COLORTERM === "truecolor" || env.COLORTERM === "24bit";
|
|
9345
|
+
}
|
|
9346
|
+
function accent(text, enabled) {
|
|
9347
|
+
if (!enabled)
|
|
9348
|
+
return text;
|
|
9349
|
+
const open = trueColor() ? "\x1B[38;2;124;255;203m" : "\x1B[38;5;122m";
|
|
9350
|
+
return `${open}${text}\x1B[39m`;
|
|
9351
|
+
}
|
|
9352
|
+
function red(text, enabled) {
|
|
9353
|
+
return enabled ? `\x1B[31m${text}\x1B[39m` : text;
|
|
9354
|
+
}
|
|
9355
|
+
function dim(text, enabled) {
|
|
9356
|
+
return enabled ? `\x1B[2m${text}\x1B[22m` : text;
|
|
9357
|
+
}
|
|
9358
|
+
function eventLine(symbol, message, color = colorEnabled(process.stdout)) {
|
|
9359
|
+
return ` ${accent(SYMBOLS[symbol], color)} ${message}`;
|
|
9360
|
+
}
|
|
9361
|
+
function errorLine(message, color = colorEnabled(process.stderr)) {
|
|
9362
|
+
return red(` ${SYMBOLS.failed} ${message}`, color);
|
|
9363
|
+
}
|
|
9364
|
+
function printNotice(message) {
|
|
9365
|
+
process.stderr.write(`${message}
|
|
9366
|
+
`);
|
|
9367
|
+
}
|
|
9368
|
+
function printError(message) {
|
|
9369
|
+
process.stderr.write(`${errorLine(message)}
|
|
9370
|
+
`);
|
|
9371
|
+
}
|
|
9372
|
+
function printResult(data, human, log = console.log) {
|
|
9373
|
+
if (jsonOutput) {
|
|
9374
|
+
log(JSON.stringify(data, null, 2));
|
|
9375
|
+
return;
|
|
9376
|
+
}
|
|
9377
|
+
for (const line of human())
|
|
9378
|
+
log(line);
|
|
9379
|
+
}
|
|
9380
|
+
|
|
9381
|
+
// src/network/pause.ts
|
|
9382
|
+
var DEVICE_HEADER = "ghostdock-device";
|
|
9383
|
+
var DEVICE_STATE_HEADER = "ghostdock-device-state";
|
|
9384
|
+
function parseDeviceStateHeader(value) {
|
|
9385
|
+
if (!value)
|
|
9386
|
+
return null;
|
|
9387
|
+
const [state, ...params] = value.split(";").map((part) => part.trim());
|
|
9388
|
+
if (state !== "paused")
|
|
9389
|
+
return null;
|
|
9390
|
+
const fields = new Map(params.map((param) => {
|
|
9391
|
+
const [key, ...rest] = param.split("=");
|
|
9392
|
+
return [key?.trim() ?? "", rest.join("=").trim()];
|
|
9393
|
+
}));
|
|
9394
|
+
const reason = fields.get("reason");
|
|
9395
|
+
if (reason !== "idle" && reason !== "max_session" && reason !== "manual")
|
|
9396
|
+
return null;
|
|
9397
|
+
const limit = Number(fields.get("limit"));
|
|
9398
|
+
return { reason, limitSeconds: Number.isInteger(limit) && limit > 0 ? limit : null };
|
|
9399
|
+
}
|
|
9400
|
+
function pausedNotice(paused) {
|
|
9401
|
+
const limit = paused.limitSeconds ?? 0;
|
|
9402
|
+
const why = paused.reason === "manual" || limit > 0 ? ` (${devicePauseReasonText(paused.reason, { idleTimeoutSeconds: limit, maxSessionSeconds: limit })})` : "";
|
|
9403
|
+
return `Network access for this device is paused${why}. Run: ${getCliName()} network connect`;
|
|
9404
|
+
}
|
|
9405
|
+
var noticeShown = false;
|
|
9406
|
+
var noticeSuppressed = false;
|
|
9407
|
+
function notePausedDevice(header, write = (line) => process.stderr.write(`${line}
|
|
9408
|
+
`)) {
|
|
9409
|
+
if (noticeShown || noticeSuppressed)
|
|
9410
|
+
return;
|
|
9411
|
+
const paused = parseDeviceStateHeader(header);
|
|
9412
|
+
if (!paused)
|
|
9413
|
+
return;
|
|
9414
|
+
noticeShown = true;
|
|
9415
|
+
write(`! ${pausedNotice(paused)}`);
|
|
9416
|
+
}
|
|
9417
|
+
function suppressPausedNotice() {
|
|
9418
|
+
noticeSuppressed = true;
|
|
9419
|
+
}
|
|
9420
|
+
function clockTime(iso, now = new Date) {
|
|
9421
|
+
const at = new Date(iso);
|
|
9422
|
+
const time = at.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
9423
|
+
if (at.toDateString() === now.toDateString())
|
|
9424
|
+
return time;
|
|
9425
|
+
return `${at.toLocaleDateString("en-GB", { weekday: "short" })} ${time}`;
|
|
9426
|
+
}
|
|
9427
|
+
function setBy(slug) {
|
|
9428
|
+
return slug ? `, set by ${slug}` : "";
|
|
9429
|
+
}
|
|
9430
|
+
function pauseHint(state, now = new Date) {
|
|
9431
|
+
const idle = `after ${formatPauseLimit(state.idleTimeoutSeconds)} without traffic${setBy(state.setBy.idle)}`;
|
|
9432
|
+
const sessionEnd = sessionEndsAt(state, now);
|
|
9433
|
+
const session = `${formatPauseLimit(state.maxSessionSeconds)} after connecting${sessionEnd ? ` (at ${clockTime(sessionEnd, now)})` : ""}${setBy(state.setBy.maxSession)}`;
|
|
9434
|
+
return [
|
|
9435
|
+
"ℹ For security, this device's network access pauses automatically:",
|
|
9436
|
+
` ${idle}, and ${session}.`,
|
|
9437
|
+
` Resume any time: ${getCliName()} network connect, or Reconnect in the web app.`
|
|
9438
|
+
];
|
|
9439
|
+
}
|
|
9440
|
+
function sessionEndsAt(state, now) {
|
|
9441
|
+
if (state.status !== "active" || !state.pausesAt)
|
|
9442
|
+
return null;
|
|
9443
|
+
if (state.pauseCause === "max_session")
|
|
9444
|
+
return state.pausesAt;
|
|
9445
|
+
return new Date(now.getTime() + state.maxSessionSeconds * 1000).toISOString();
|
|
9446
|
+
}
|
|
9447
|
+
function bold2(text, enabled = colorEnabled(process.stderr)) {
|
|
9448
|
+
return enabled ? `\x1B[1m${text}\x1B[22m` : text;
|
|
9449
|
+
}
|
|
9450
|
+
function deviceStateText(state, now = new Date) {
|
|
9451
|
+
if (!state)
|
|
9452
|
+
return "Not connected (logged out)";
|
|
9453
|
+
if (state.status === "paused" && state.pauseReason) {
|
|
9454
|
+
const text = state.pauseReason === "manual" ? "Paused by you" : `Paused: ${devicePauseReasonText(state.pauseReason, state)}`;
|
|
9455
|
+
return state.connected ? text : `Not connected (${text.toLowerCase()})`;
|
|
9456
|
+
}
|
|
9457
|
+
const left = state.pausesAt ? new Date(state.pausesAt).getTime() - now.getTime() : 0;
|
|
9458
|
+
const active = `Active, pauses in ${formatRemaining(Math.max(left, 0) / 1000)}`;
|
|
9459
|
+
return state.connected ? active : `Not connected (armed, pauses in ${formatRemaining(Math.max(left, 0) / 1000)})`;
|
|
9460
|
+
}
|
|
9461
|
+
function formatRemaining(seconds) {
|
|
9462
|
+
const minutes = Math.floor(seconds / 60);
|
|
9463
|
+
const days = Math.floor(minutes / 1440);
|
|
9464
|
+
const hours = Math.floor(minutes % 1440 / 60);
|
|
9465
|
+
const rest = minutes % 60;
|
|
9466
|
+
const dayText = days === 1 ? "1 day" : `${days} days`;
|
|
9467
|
+
if (days > 0)
|
|
9468
|
+
return hours ? `${dayText} ${hours} h` : dayText;
|
|
9469
|
+
if (hours > 0)
|
|
9470
|
+
return rest ? `${hours} h ${rest} min` : `${hours} h`;
|
|
9471
|
+
return `${Math.max(rest, 1)} min`;
|
|
9472
|
+
}
|
|
9473
|
+
|
|
9474
|
+
// src/network/tunnel.ts
|
|
9475
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
9476
|
+
import { existsSync } from "node:fs";
|
|
9477
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
9478
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
9479
|
+
var INTERFACE = "gdock";
|
|
9480
|
+
function networkDir() {
|
|
9481
|
+
return join2(getConfigDir(), "network");
|
|
9482
|
+
}
|
|
9483
|
+
function configPath(dir = networkDir()) {
|
|
9484
|
+
return join2(dir, `${INTERFACE}.conf`);
|
|
9485
|
+
}
|
|
9486
|
+
function keyPath(dir) {
|
|
9487
|
+
return join2(dir, "private.key");
|
|
9488
|
+
}
|
|
9489
|
+
function statePath(dir) {
|
|
9490
|
+
return join2(dir, "device.json");
|
|
9491
|
+
}
|
|
9492
|
+
function isProvider(value) {
|
|
9493
|
+
const provider = value;
|
|
9494
|
+
return typeof provider?.id === "string" && typeof provider.name === "string";
|
|
9495
|
+
}
|
|
9496
|
+
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
9497
|
+
function keyPairFromPrivate(privateKey) {
|
|
9498
|
+
const raw = Buffer.from(privateKey, "base64");
|
|
9499
|
+
if (raw.length !== 32)
|
|
9500
|
+
throw new Error("The stored WireGuard key is not 32 bytes.");
|
|
9501
|
+
const key = createPrivateKey({
|
|
9502
|
+
key: Buffer.concat([X25519_PKCS8_PREFIX, raw]),
|
|
9503
|
+
format: "der",
|
|
9504
|
+
type: "pkcs8"
|
|
9505
|
+
});
|
|
9506
|
+
const spki = createPublicKey(key).export({ format: "der", type: "spki" });
|
|
9507
|
+
return { privateKey, publicKey: spki.subarray(spki.length - 32).toString("base64") };
|
|
9508
|
+
}
|
|
9509
|
+
async function loadOrCreateKey(dir = networkDir()) {
|
|
9510
|
+
try {
|
|
9511
|
+
return keyPairFromPrivate((await readFile2(keyPath(dir), "utf8")).trim());
|
|
9512
|
+
} catch (err) {
|
|
9513
|
+
if (err.code !== "ENOENT")
|
|
9514
|
+
throw err;
|
|
9515
|
+
}
|
|
9516
|
+
const { privateKey } = generateKeyPairSync("x25519");
|
|
9517
|
+
const der = privateKey.export({ format: "der", type: "pkcs8" });
|
|
9518
|
+
const raw = der.subarray(der.length - 32).toString("base64");
|
|
9519
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
9520
|
+
await writeFile2(keyPath(dir), `${raw}
|
|
9521
|
+
`, { mode: 384 });
|
|
9522
|
+
return keyPairFromPrivate(raw);
|
|
9523
|
+
}
|
|
9524
|
+
async function readDeviceState(dir = networkDir()) {
|
|
9525
|
+
try {
|
|
9526
|
+
const parsed = JSON.parse(await readFile2(statePath(dir), "utf8"));
|
|
9527
|
+
if (typeof parsed.deviceId === "string" && typeof parsed.baseUrl === "string") {
|
|
9528
|
+
return {
|
|
9529
|
+
baseUrl: parsed.baseUrl,
|
|
9530
|
+
deviceId: parsed.deviceId,
|
|
9531
|
+
address: typeof parsed.address === "string" ? parsed.address : "",
|
|
9532
|
+
gatewayProvider: isProvider(parsed.gatewayProvider) ? { id: parsed.gatewayProvider.id, name: parsed.gatewayProvider.name } : null,
|
|
9533
|
+
zones: Array.isArray(parsed.zones) ? parsed.zones.filter((zone) => typeof zone === "string" && isDnsZone(zone)) : []
|
|
9534
|
+
};
|
|
9535
|
+
}
|
|
9536
|
+
} catch {}
|
|
9537
|
+
return null;
|
|
9538
|
+
}
|
|
9539
|
+
async function writeDeviceState(state, dir = networkDir()) {
|
|
9540
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
9541
|
+
await writeFile2(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
9542
|
+
`, { mode: 384 });
|
|
9543
|
+
}
|
|
9544
|
+
function detectDnsMode(platform = process.platform, exists = existsSync) {
|
|
9545
|
+
if (platform === "darwin")
|
|
9546
|
+
return "macos-resolver";
|
|
9547
|
+
if (platform === "linux" && exists("/run/systemd/resolve"))
|
|
9548
|
+
return "resolved";
|
|
9549
|
+
return "none";
|
|
9550
|
+
}
|
|
9551
|
+
function renderConfig(input) {
|
|
9552
|
+
const { device, network, gateway } = input.registered;
|
|
9553
|
+
const zones = [network.zone, ...network.netZone ? [network.netZone] : []];
|
|
9554
|
+
for (const zone of zones) {
|
|
9555
|
+
if (!isDnsZone(zone))
|
|
9556
|
+
throw new Error(`The server sent a zone that is not a DNS name: ${zone}`);
|
|
9557
|
+
}
|
|
9558
|
+
if (!device.address || !isIPv6Address(device.address)) {
|
|
9559
|
+
throw new Error(`The server sent no IPv6 address for this device: ${device.address}`);
|
|
9560
|
+
}
|
|
9561
|
+
if (!isIPv6Address(network.gatewayAddress)) {
|
|
9562
|
+
throw new Error(`The server sent a gateway that is not an IPv6 address: ${network.gatewayAddress}`);
|
|
9563
|
+
}
|
|
9564
|
+
const ipv4 = device.address4 && network.ingress4 ? { address: device.address4, ingress: network.ingress4 } : null;
|
|
9565
|
+
if (ipv4 && (!isIPv4Address(ipv4.address) || !isIPv4Address(ipv4.ingress))) {
|
|
9566
|
+
throw new Error(`The server sent an IPv4 address that is not one: ${ipv4.address}, ${ipv4.ingress}`);
|
|
9567
|
+
}
|
|
9568
|
+
const lines = [
|
|
9569
|
+
"# Written by `ghostdock network connect`. Run it again rather than editing this file.",
|
|
9570
|
+
"[Interface]",
|
|
9571
|
+
`PrivateKey = ${input.privateKey}`,
|
|
9572
|
+
`Address = ${device.address}/128${ipv4 ? `, ${ipv4.address}/32` : ""}`,
|
|
9573
|
+
`MTU = ${network.mtu}`
|
|
9574
|
+
];
|
|
9575
|
+
if (input.dns === "macos-resolver") {
|
|
9576
|
+
for (const zone of zones) {
|
|
9577
|
+
const file = `/etc/resolver/${zone}`;
|
|
9578
|
+
lines.push(`PostUp = mkdir -p /etc/resolver && printf 'nameserver ${network.gatewayAddress}\\n' > ${file}`, `PostDown = rm -f ${file}`);
|
|
9579
|
+
}
|
|
9580
|
+
} else if (input.dns === "resolved") {
|
|
9581
|
+
const domains = zones.map((zone) => `'~${zone}'`).join(" ");
|
|
9582
|
+
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${domains}`);
|
|
9583
|
+
}
|
|
9584
|
+
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.prefix}${ipv4 ? `, ${ipv4.ingress}/32` : ""}`, "PersistentKeepalive = 25", "");
|
|
9585
|
+
return lines.join(`
|
|
9586
|
+
`);
|
|
9587
|
+
}
|
|
9588
|
+
var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
9589
|
+
function isDnsZone(zone) {
|
|
9590
|
+
const labels = zone.split(".");
|
|
9591
|
+
return zone.length <= 253 && labels.length >= 2 && labels.every((label) => DNS_LABEL.test(label));
|
|
9592
|
+
}
|
|
9593
|
+
function isIPv4Address(text) {
|
|
9594
|
+
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);
|
|
9595
|
+
}
|
|
9596
|
+
function ipv4RouteOwner(platform, output) {
|
|
9597
|
+
if (platform === "darwin") {
|
|
9598
|
+
if (/^\s*destination: default$/m.test(output))
|
|
9599
|
+
return null;
|
|
9600
|
+
return /^\s*interface: (\S+)$/m.exec(output)?.[1] ?? null;
|
|
9601
|
+
}
|
|
9602
|
+
for (const line of output.split(`
|
|
9603
|
+
`).map((l) => l.trim())) {
|
|
9604
|
+
if (!line || /^(default|broadcast)\b/.test(line))
|
|
9605
|
+
continue;
|
|
9606
|
+
return /\bdev (\S+)/.exec(line)?.[1] ?? "another route";
|
|
9607
|
+
}
|
|
9608
|
+
return null;
|
|
9609
|
+
}
|
|
9610
|
+
function ipv4RouteCommand(platform, address) {
|
|
9611
|
+
return platform === "darwin" ? ["route", ["-n", "get", address]] : ["ip", ["-4", "route", "show", "table", "all", "match", address]];
|
|
9612
|
+
}
|
|
9613
|
+
function isIPv6Address(text) {
|
|
9614
|
+
return /^[0-9a-f:]{2,39}$/.test(text) && text.includes(":");
|
|
9615
|
+
}
|
|
9616
|
+
async function readConfig(path = configPath()) {
|
|
9617
|
+
try {
|
|
9618
|
+
return await readFile2(path, "utf8");
|
|
9619
|
+
} catch {
|
|
9620
|
+
return null;
|
|
9621
|
+
}
|
|
9622
|
+
}
|
|
9623
|
+
function configResolverZones(config) {
|
|
9624
|
+
if (!config)
|
|
9625
|
+
return [];
|
|
9626
|
+
const zones = [];
|
|
9627
|
+
for (const match of config.matchAll(/^PostDown = rm -f \/etc\/resolver\/(\S+)$/gm)) {
|
|
9628
|
+
const zone = match[1] ?? "";
|
|
9629
|
+
if (isDnsZone(zone) && !zones.includes(zone))
|
|
9630
|
+
zones.push(zone);
|
|
9631
|
+
}
|
|
9632
|
+
return zones;
|
|
9633
|
+
}
|
|
9634
|
+
function resolverFile(zone) {
|
|
9635
|
+
return `/etc/resolver/${zone}`;
|
|
9636
|
+
}
|
|
9637
|
+
async function readConfigMtu(path = configPath()) {
|
|
9638
|
+
let contents;
|
|
9639
|
+
try {
|
|
9640
|
+
contents = await readFile2(path, "utf8");
|
|
9641
|
+
} catch {
|
|
9642
|
+
return null;
|
|
9643
|
+
}
|
|
9644
|
+
const match = /^MTU = (\d+)$/m.exec(contents);
|
|
9645
|
+
return match ? Number(match[1]) : null;
|
|
9646
|
+
}
|
|
9647
|
+
async function writeConfig(contents, path = configPath()) {
|
|
9648
|
+
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
9649
|
+
await writeFile2(path, contents, { mode: 384 });
|
|
9650
|
+
await chmod2(path, 384);
|
|
9651
|
+
}
|
|
9652
|
+
function tunnelUp(platform = process.platform, exists = existsSync) {
|
|
9653
|
+
if (platform === "darwin")
|
|
9654
|
+
return exists(`/var/run/wireguard/${INTERFACE}.name`);
|
|
9655
|
+
return exists(`/sys/class/net/${INTERFACE}`);
|
|
9656
|
+
}
|
|
9657
|
+
function installHint(platform = process.platform) {
|
|
9658
|
+
if (platform === "darwin")
|
|
9659
|
+
return "brew install wireguard-tools";
|
|
9660
|
+
if (platform === "linux")
|
|
9661
|
+
return "sudo apt install wireguard-tools (or your distribution’s package)";
|
|
9662
|
+
return "Install wireguard-tools. On Windows, run the CLI inside WSL.";
|
|
9663
|
+
}
|
|
9664
|
+
|
|
9287
9665
|
// src/api-client.ts
|
|
9288
9666
|
var CLI_USER_AGENT = `ghostdock-cli/${version}`;
|
|
9289
9667
|
|
|
@@ -9325,6 +9703,7 @@ async function request(path, init = {}) {
|
|
|
9325
9703
|
...init.headers
|
|
9326
9704
|
}
|
|
9327
9705
|
});
|
|
9706
|
+
notePausedDevice(res.headers.get(DEVICE_STATE_HEADER));
|
|
9328
9707
|
if (!res.ok) {
|
|
9329
9708
|
const payload = await readErrorPayload(res);
|
|
9330
9709
|
throw new ApiError(res.status, payload.message, payload.data);
|
|
@@ -9356,6 +9735,11 @@ function deviceRevoke(body) {
|
|
|
9356
9735
|
body: JSON.stringify(body)
|
|
9357
9736
|
});
|
|
9358
9737
|
}
|
|
9738
|
+
var deviceIdOnce = null;
|
|
9739
|
+
function currentDeviceId() {
|
|
9740
|
+
deviceIdOnce ??= readDeviceState().then((state) => state && state.baseUrl === getBaseUrl() ? state.deviceId : null).catch(() => null);
|
|
9741
|
+
return deviceIdOnce;
|
|
9742
|
+
}
|
|
9359
9743
|
var refreshInFlight = null;
|
|
9360
9744
|
async function refreshStoredCredentials(refreshToken) {
|
|
9361
9745
|
if (refreshInFlight)
|
|
@@ -9378,10 +9762,12 @@ async function authorizedRequest(path, init = {}) {
|
|
|
9378
9762
|
if (!credentials) {
|
|
9379
9763
|
throw new AuthRequiredError;
|
|
9380
9764
|
}
|
|
9765
|
+
const deviceId = await currentDeviceId();
|
|
9381
9766
|
const withAuth = (accessToken) => ({
|
|
9382
9767
|
...init,
|
|
9383
9768
|
headers: {
|
|
9384
9769
|
...init.headers,
|
|
9770
|
+
...deviceId ? { [DEVICE_HEADER]: deviceId } : {},
|
|
9385
9771
|
authorization: `Bearer ${accessToken}`
|
|
9386
9772
|
}
|
|
9387
9773
|
});
|
|
@@ -9405,75 +9791,11 @@ async function authorizedRequest(path, init = {}) {
|
|
|
9405
9791
|
}
|
|
9406
9792
|
}
|
|
9407
9793
|
|
|
9408
|
-
// src/output.ts
|
|
9409
|
-
var SYMBOLS = {
|
|
9410
|
-
ready: "⚓",
|
|
9411
|
-
done: "✓",
|
|
9412
|
-
url: "↗",
|
|
9413
|
-
stopped: "◌",
|
|
9414
|
-
failed: "✗",
|
|
9415
|
-
progress: "▂▄▆",
|
|
9416
|
-
item: "•"
|
|
9417
|
-
};
|
|
9418
|
-
var jsonOutput = false;
|
|
9419
|
-
function setJsonOutput(on) {
|
|
9420
|
-
jsonOutput = on;
|
|
9421
|
-
}
|
|
9422
|
-
function isJsonOutput() {
|
|
9423
|
-
return jsonOutput;
|
|
9424
|
-
}
|
|
9425
|
-
function colorEnabled(stream, env = process.env) {
|
|
9426
|
-
if (env.FORCE_COLOR !== undefined)
|
|
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;
|
|
9433
|
-
}
|
|
9434
|
-
function trueColor(env = process.env) {
|
|
9435
|
-
return env.COLORTERM === "truecolor" || env.COLORTERM === "24bit";
|
|
9436
|
-
}
|
|
9437
|
-
function accent(text, enabled) {
|
|
9438
|
-
if (!enabled)
|
|
9439
|
-
return text;
|
|
9440
|
-
const open = trueColor() ? "\x1B[38;2;124;255;203m" : "\x1B[38;5;122m";
|
|
9441
|
-
return `${open}${text}\x1B[39m`;
|
|
9442
|
-
}
|
|
9443
|
-
function red(text, enabled) {
|
|
9444
|
-
return enabled ? `\x1B[31m${text}\x1B[39m` : text;
|
|
9445
|
-
}
|
|
9446
|
-
function dim(text, enabled) {
|
|
9447
|
-
return enabled ? `\x1B[2m${text}\x1B[22m` : text;
|
|
9448
|
-
}
|
|
9449
|
-
function eventLine(symbol, message, color = colorEnabled(process.stdout)) {
|
|
9450
|
-
return ` ${accent(SYMBOLS[symbol], color)} ${message}`;
|
|
9451
|
-
}
|
|
9452
|
-
function errorLine(message, color = colorEnabled(process.stderr)) {
|
|
9453
|
-
return red(` ${SYMBOLS.failed} ${message}`, color);
|
|
9454
|
-
}
|
|
9455
|
-
function printNotice(message) {
|
|
9456
|
-
process.stderr.write(`${message}
|
|
9457
|
-
`);
|
|
9458
|
-
}
|
|
9459
|
-
function printError(message) {
|
|
9460
|
-
process.stderr.write(`${errorLine(message)}
|
|
9461
|
-
`);
|
|
9462
|
-
}
|
|
9463
|
-
function printResult(data, human, log = console.log) {
|
|
9464
|
-
if (jsonOutput) {
|
|
9465
|
-
log(JSON.stringify(data, null, 2));
|
|
9466
|
-
return;
|
|
9467
|
-
}
|
|
9468
|
-
for (const line of human())
|
|
9469
|
-
log(line);
|
|
9470
|
-
}
|
|
9471
|
-
|
|
9472
9794
|
// src/settings.ts
|
|
9473
|
-
import { mkdir as
|
|
9474
|
-
import { dirname as
|
|
9795
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
9796
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
9475
9797
|
function getSettingsPath() {
|
|
9476
|
-
return
|
|
9798
|
+
return join3(dirname3(getCredentialsPath()), "settings.json");
|
|
9477
9799
|
}
|
|
9478
9800
|
function isDefaultOrganization(value) {
|
|
9479
9801
|
if (!value || typeof value !== "object")
|
|
@@ -9483,15 +9805,15 @@ function isDefaultOrganization(value) {
|
|
|
9483
9805
|
}
|
|
9484
9806
|
async function readSettings(path = getSettingsPath()) {
|
|
9485
9807
|
try {
|
|
9486
|
-
const parsed = JSON.parse(await
|
|
9808
|
+
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
9487
9809
|
return isDefaultOrganization(parsed.defaultOrganization) ? { defaultOrganization: parsed.defaultOrganization } : {};
|
|
9488
9810
|
} catch {
|
|
9489
9811
|
return {};
|
|
9490
9812
|
}
|
|
9491
9813
|
}
|
|
9492
9814
|
async function writeSettings(settings, path = getSettingsPath()) {
|
|
9493
|
-
await
|
|
9494
|
-
await
|
|
9815
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
9816
|
+
await writeFile3(path, `${JSON.stringify(settings, null, 2)}
|
|
9495
9817
|
`, "utf8");
|
|
9496
9818
|
}
|
|
9497
9819
|
|
|
@@ -9781,77 +10103,6 @@ var envCommand = defineCommand({
|
|
|
9781
10103
|
}
|
|
9782
10104
|
});
|
|
9783
10105
|
|
|
9784
|
-
// src/container-logs.ts
|
|
9785
|
-
var FOLLOW_INTERVAL_MS = 2000;
|
|
9786
|
-
function defaultLogsDeps(follow) {
|
|
9787
|
-
let stop = false;
|
|
9788
|
-
if (follow) {
|
|
9789
|
-
process.once("SIGINT", () => {
|
|
9790
|
-
stop = true;
|
|
9791
|
-
});
|
|
9792
|
-
}
|
|
9793
|
-
return {
|
|
9794
|
-
request: (input) => authorizedRequest("/api/v1/tools/sandboxes_containers_logs", {
|
|
9795
|
-
method: "POST",
|
|
9796
|
-
body: JSON.stringify(input)
|
|
9797
|
-
}),
|
|
9798
|
-
out: (line) => process.stdout.write(`${line}
|
|
9799
|
-
`),
|
|
9800
|
-
err: (line) => process.stderr.write(`${line}
|
|
9801
|
-
`),
|
|
9802
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
9803
|
-
stopped: () => stop
|
|
9804
|
-
};
|
|
9805
|
-
}
|
|
9806
|
-
async function runContainerLogs(input, options, deps = defaultLogsDeps(options.follow)) {
|
|
9807
|
-
const json = isJsonOutput();
|
|
9808
|
-
const color = colorEnabled(process.stdout);
|
|
9809
|
-
const print = (line) => {
|
|
9810
|
-
if (json) {
|
|
9811
|
-
deps.out(JSON.stringify(line));
|
|
9812
|
-
return;
|
|
9813
|
-
}
|
|
9814
|
-
const text = options.timestamps && line.time ? `${dim(line.time, color)} ${line.text}` : line.text;
|
|
9815
|
-
(line.stream === "stderr" ? deps.err : deps.out)(text);
|
|
9816
|
-
};
|
|
9817
|
-
let result = await deps.request(input);
|
|
9818
|
-
if (!result.running) {
|
|
9819
|
-
deps.err("The sandbox is stopped: it has no logs to show.");
|
|
9820
|
-
return 1;
|
|
9821
|
-
}
|
|
9822
|
-
let last = null;
|
|
9823
|
-
for (;; ) {
|
|
9824
|
-
for (const line of result.lines) {
|
|
9825
|
-
print(line);
|
|
9826
|
-
if (line.time)
|
|
9827
|
-
last = line.time;
|
|
9828
|
-
}
|
|
9829
|
-
if (!options.follow) {
|
|
9830
|
-
if (result.truncated && json) {
|
|
9831
|
-
deps.out(JSON.stringify({ truncated: true }));
|
|
9832
|
-
} else if (result.truncated) {
|
|
9833
|
-
deps.err(dim("(older lines left out: use --tail for more)", colorEnabled(process.stderr)));
|
|
9834
|
-
}
|
|
9835
|
-
return 0;
|
|
9836
|
-
}
|
|
9837
|
-
if (!result.truncated)
|
|
9838
|
-
await deps.sleep(FOLLOW_INTERVAL_MS);
|
|
9839
|
-
if (deps.stopped())
|
|
9840
|
-
return 0;
|
|
9841
|
-
const { tail: _tail, ...rest } = input;
|
|
9842
|
-
try {
|
|
9843
|
-
result = await deps.request(last ? { ...rest, after: last } : input);
|
|
9844
|
-
} catch (error) {
|
|
9845
|
-
deps.err(red(error instanceof Error ? error.message : String(error), colorEnabled(process.stderr)));
|
|
9846
|
-
return 1;
|
|
9847
|
-
}
|
|
9848
|
-
if (!result.running) {
|
|
9849
|
-
deps.err("The sandbox stopped.");
|
|
9850
|
-
return 0;
|
|
9851
|
-
}
|
|
9852
|
-
}
|
|
9853
|
-
}
|
|
9854
|
-
|
|
9855
10106
|
// src/container-picker.ts
|
|
9856
10107
|
import { createInterface } from "node:readline/promises";
|
|
9857
10108
|
function notRunningContainerOf(error) {
|
|
@@ -9985,6 +10236,130 @@ async function withProgress(message, work, options) {
|
|
|
9985
10236
|
}
|
|
9986
10237
|
}
|
|
9987
10238
|
|
|
10239
|
+
// src/container-flow.ts
|
|
10240
|
+
async function withContainerPick(call, send, pickers, notice) {
|
|
10241
|
+
try {
|
|
10242
|
+
return await send();
|
|
10243
|
+
} catch (err) {
|
|
10244
|
+
const question = containerQuestionOf(err);
|
|
10245
|
+
if (!pickers || !question || !call.canPick)
|
|
10246
|
+
throw err;
|
|
10247
|
+
const container = await pickers.choose(question);
|
|
10248
|
+
if (!container)
|
|
10249
|
+
throw err;
|
|
10250
|
+
call.setContainer(container);
|
|
10251
|
+
const result = await send();
|
|
10252
|
+
if (call.sandboxId !== undefined && await pickers.confirmDefault(container)) {
|
|
10253
|
+
try {
|
|
10254
|
+
await authorizedRequest("/api/v1/tools/sandboxes_default_container", {
|
|
10255
|
+
method: "POST",
|
|
10256
|
+
body: JSON.stringify({
|
|
10257
|
+
organization: call.organization,
|
|
10258
|
+
sandboxId: call.sandboxId,
|
|
10259
|
+
container
|
|
10260
|
+
})
|
|
10261
|
+
});
|
|
10262
|
+
notice(eventLine("done", `Default container: ${container}.`));
|
|
10263
|
+
} catch (err) {
|
|
10264
|
+
notice(eventLine("failed", `The default container was not set: ${err instanceof Error ? err.message : String(err)}`));
|
|
10265
|
+
}
|
|
10266
|
+
}
|
|
10267
|
+
return result;
|
|
10268
|
+
}
|
|
10269
|
+
}
|
|
10270
|
+
async function withContainerStart(call, send, start, pickers, notice) {
|
|
10271
|
+
try {
|
|
10272
|
+
return await send();
|
|
10273
|
+
} catch (err) {
|
|
10274
|
+
const container = notRunningContainerOf(err);
|
|
10275
|
+
if (!container || call.sandboxId === undefined)
|
|
10276
|
+
throw err;
|
|
10277
|
+
if (!start && !(pickers && await pickers.confirmStart(container)))
|
|
10278
|
+
throw err;
|
|
10279
|
+
await withProgress("Starting container…", () => authorizedRequest("/api/v1/tools/sandboxes_containers_start", {
|
|
10280
|
+
method: "POST",
|
|
10281
|
+
body: JSON.stringify({
|
|
10282
|
+
organization: call.organization,
|
|
10283
|
+
sandboxId: call.sandboxId,
|
|
10284
|
+
container
|
|
10285
|
+
})
|
|
10286
|
+
}));
|
|
10287
|
+
notice(eventLine("done", `Started ${container}.`));
|
|
10288
|
+
return await send();
|
|
10289
|
+
}
|
|
10290
|
+
}
|
|
10291
|
+
|
|
10292
|
+
// src/container-logs.ts
|
|
10293
|
+
var FOLLOW_INTERVAL_MS = 2000;
|
|
10294
|
+
function defaultLogsDeps(follow) {
|
|
10295
|
+
let stop = false;
|
|
10296
|
+
if (follow) {
|
|
10297
|
+
process.once("SIGINT", () => {
|
|
10298
|
+
stop = true;
|
|
10299
|
+
});
|
|
10300
|
+
}
|
|
10301
|
+
return {
|
|
10302
|
+
request: (input) => authorizedRequest("/api/v1/tools/sandboxes_containers_logs", {
|
|
10303
|
+
method: "POST",
|
|
10304
|
+
body: JSON.stringify(input)
|
|
10305
|
+
}),
|
|
10306
|
+
out: (line) => process.stdout.write(`${line}
|
|
10307
|
+
`),
|
|
10308
|
+
err: (line) => process.stderr.write(`${line}
|
|
10309
|
+
`),
|
|
10310
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
10311
|
+
stopped: () => stop
|
|
10312
|
+
};
|
|
10313
|
+
}
|
|
10314
|
+
async function runContainerLogs(input, options, deps = defaultLogsDeps(options.follow)) {
|
|
10315
|
+
const json = isJsonOutput();
|
|
10316
|
+
const color = colorEnabled(process.stdout);
|
|
10317
|
+
const print = (line) => {
|
|
10318
|
+
if (json) {
|
|
10319
|
+
deps.out(JSON.stringify(line));
|
|
10320
|
+
return;
|
|
10321
|
+
}
|
|
10322
|
+
const text = options.timestamps && line.time ? `${dim(line.time, color)} ${line.text}` : line.text;
|
|
10323
|
+
(line.stream === "stderr" ? deps.err : deps.out)(text);
|
|
10324
|
+
};
|
|
10325
|
+
let result = await deps.request(input);
|
|
10326
|
+
if (!result.running) {
|
|
10327
|
+
deps.err("The sandbox is stopped: it has no logs to show.");
|
|
10328
|
+
return 1;
|
|
10329
|
+
}
|
|
10330
|
+
let last = null;
|
|
10331
|
+
for (;; ) {
|
|
10332
|
+
for (const line of result.lines) {
|
|
10333
|
+
print(line);
|
|
10334
|
+
if (line.time)
|
|
10335
|
+
last = line.time;
|
|
10336
|
+
}
|
|
10337
|
+
if (!options.follow) {
|
|
10338
|
+
if (result.truncated && json) {
|
|
10339
|
+
deps.out(JSON.stringify({ truncated: true }));
|
|
10340
|
+
} else if (result.truncated) {
|
|
10341
|
+
deps.err(dim("(older lines left out: use --tail for more)", colorEnabled(process.stderr)));
|
|
10342
|
+
}
|
|
10343
|
+
return 0;
|
|
10344
|
+
}
|
|
10345
|
+
if (!result.truncated)
|
|
10346
|
+
await deps.sleep(FOLLOW_INTERVAL_MS);
|
|
10347
|
+
if (deps.stopped())
|
|
10348
|
+
return 0;
|
|
10349
|
+
const { tail: _tail, ...rest } = input;
|
|
10350
|
+
try {
|
|
10351
|
+
result = await deps.request(last ? { ...rest, after: last } : input);
|
|
10352
|
+
} catch (error) {
|
|
10353
|
+
deps.err(red(error instanceof Error ? error.message : String(error), colorEnabled(process.stderr)));
|
|
10354
|
+
return 1;
|
|
10355
|
+
}
|
|
10356
|
+
if (!result.running) {
|
|
10357
|
+
deps.err("The sandbox stopped.");
|
|
10358
|
+
return 0;
|
|
10359
|
+
}
|
|
10360
|
+
}
|
|
10361
|
+
}
|
|
10362
|
+
|
|
9988
10363
|
// src/human.ts
|
|
9989
10364
|
var ID_KEYS = [
|
|
9990
10365
|
"sandboxId",
|
|
@@ -10104,6 +10479,11 @@ function listLines(items, color) {
|
|
|
10104
10479
|
function bootPhase(value) {
|
|
10105
10480
|
return isItem(value) && typeof value.ms === "number" && Number.isFinite(value.ms) ? { ms: value.ms } : null;
|
|
10106
10481
|
}
|
|
10482
|
+
var DISK_SOURCE_TEXT = {
|
|
10483
|
+
fresh: "image pulled",
|
|
10484
|
+
"image-base": "image already on disk",
|
|
10485
|
+
"user-snapshot": "from a snapshot"
|
|
10486
|
+
};
|
|
10107
10487
|
function bootLines(data, color) {
|
|
10108
10488
|
if (!isItem(data) || !isItem(data.boot))
|
|
10109
10489
|
return [];
|
|
@@ -10115,6 +10495,12 @@ function bootLines(data, color) {
|
|
|
10115
10495
|
if (container) {
|
|
10116
10496
|
lines.push(eventLine("done", `container started in ${formatElapsed(container.ms)}`, color));
|
|
10117
10497
|
}
|
|
10498
|
+
const provision = bootPhase(data.boot.provision);
|
|
10499
|
+
if (provision) {
|
|
10500
|
+
const disk = typeof data.boot.disk === "string" ? DISK_SOURCE_TEXT[data.boot.disk] : undefined;
|
|
10501
|
+
const how = disk ? ` (${disk})` : "";
|
|
10502
|
+
lines.push(eventLine("done", `ready in ${formatElapsed(provision.ms)}${how}`, color));
|
|
10503
|
+
}
|
|
10118
10504
|
return lines;
|
|
10119
10505
|
}
|
|
10120
10506
|
function renderResult(data, color) {
|
|
@@ -10288,10 +10674,10 @@ function buildToolInput(schema, rawArgs) {
|
|
|
10288
10674
|
|
|
10289
10675
|
// src/commands/invoke.ts
|
|
10290
10676
|
async function runToolInvoke(tool, rawArgs, log = console.log, error = printError, notice = printNotice, pickers = defaultContainerPickers()) {
|
|
10677
|
+
const properties = tool.inputSchema.properties;
|
|
10291
10678
|
let input;
|
|
10292
10679
|
try {
|
|
10293
10680
|
input = buildToolInput(tool.inputSchema, rawArgs);
|
|
10294
|
-
const properties = tool.inputSchema.properties;
|
|
10295
10681
|
if (properties && "organization" in properties && input.organization === undefined) {
|
|
10296
10682
|
input.organization = await requireOrganizationRef(undefined, notice);
|
|
10297
10683
|
}
|
|
@@ -10310,24 +10696,33 @@ async function runToolInvoke(tool, rawArgs, log = console.log, error = printErro
|
|
|
10310
10696
|
return 1;
|
|
10311
10697
|
}
|
|
10312
10698
|
}
|
|
10699
|
+
const call = {
|
|
10700
|
+
organization: typeof input.organization === "string" ? input.organization : undefined,
|
|
10701
|
+
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : undefined,
|
|
10702
|
+
canPick: !!properties && "container" in properties && !input.container,
|
|
10703
|
+
setContainer: (container) => {
|
|
10704
|
+
input.container = container;
|
|
10705
|
+
}
|
|
10706
|
+
};
|
|
10313
10707
|
try {
|
|
10314
|
-
const
|
|
10708
|
+
const post = () => authorizedRequest(`/api/v1/tools/${tool.name}`, {
|
|
10315
10709
|
method: "POST",
|
|
10316
10710
|
body: JSON.stringify(input)
|
|
10317
10711
|
});
|
|
10318
|
-
const request = () => withContainerStart(input, send, rawArgs.start === true, pickers, notice);
|
|
10319
10712
|
const waitMessage = PROGRESS_MESSAGES[tool.name];
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10713
|
+
let elapsed;
|
|
10714
|
+
const send = async () => {
|
|
10715
|
+
if (!waitMessage)
|
|
10716
|
+
return post();
|
|
10717
|
+
const timed = await withProgress(waitMessage, post);
|
|
10718
|
+
elapsed = timed.elapsed;
|
|
10719
|
+
return timed.value;
|
|
10720
|
+
};
|
|
10721
|
+
const result = await withContainerPick(call, () => withContainerStart(call, send, rawArgs.start === true, pickers, notice), pickers, notice);
|
|
10327
10722
|
printResult(result, () => {
|
|
10328
10723
|
const color = colorEnabled(process.stdout);
|
|
10329
10724
|
const lines = renderResult(result, color);
|
|
10330
|
-
return bootLines(result, color).length > 0 ? lines : withElapsed(lines, elapsed);
|
|
10725
|
+
return elapsed === undefined || bootLines(result, color).length > 0 ? lines : withElapsed(lines, elapsed);
|
|
10331
10726
|
}, log);
|
|
10332
10727
|
stopNote(result, notice);
|
|
10333
10728
|
return 0;
|
|
@@ -10336,59 +10731,6 @@ async function runToolInvoke(tool, rawArgs, log = console.log, error = printErro
|
|
|
10336
10731
|
return 1;
|
|
10337
10732
|
}
|
|
10338
10733
|
}
|
|
10339
|
-
async function withContainerPick(tool, input, send, pickers, notice) {
|
|
10340
|
-
try {
|
|
10341
|
-
return await send();
|
|
10342
|
-
} catch (err) {
|
|
10343
|
-
const properties = tool.inputSchema.properties;
|
|
10344
|
-
const question = containerQuestionOf(err);
|
|
10345
|
-
if (!pickers || !question || !properties || !("container" in properties) || input.container) {
|
|
10346
|
-
throw err;
|
|
10347
|
-
}
|
|
10348
|
-
const container = await pickers.choose(question);
|
|
10349
|
-
if (!container)
|
|
10350
|
-
throw err;
|
|
10351
|
-
input.container = container;
|
|
10352
|
-
const result = await send();
|
|
10353
|
-
if (typeof input.sandboxId === "string" && await pickers.confirmDefault(container)) {
|
|
10354
|
-
try {
|
|
10355
|
-
await authorizedRequest("/api/v1/tools/sandboxes_default_container", {
|
|
10356
|
-
method: "POST",
|
|
10357
|
-
body: JSON.stringify({
|
|
10358
|
-
organization: input.organization,
|
|
10359
|
-
sandboxId: input.sandboxId,
|
|
10360
|
-
container
|
|
10361
|
-
})
|
|
10362
|
-
});
|
|
10363
|
-
notice(eventLine("done", `Default container: ${container}.`));
|
|
10364
|
-
} catch (err) {
|
|
10365
|
-
notice(eventLine("failed", `The default container was not set: ${err instanceof Error ? err.message : String(err)}`));
|
|
10366
|
-
}
|
|
10367
|
-
}
|
|
10368
|
-
return result;
|
|
10369
|
-
}
|
|
10370
|
-
}
|
|
10371
|
-
async function withContainerStart(input, send, start, pickers, notice) {
|
|
10372
|
-
try {
|
|
10373
|
-
return await send();
|
|
10374
|
-
} catch (err) {
|
|
10375
|
-
const container = notRunningContainerOf(err);
|
|
10376
|
-
if (!container || typeof input.sandboxId !== "string")
|
|
10377
|
-
throw err;
|
|
10378
|
-
if (!start && !(pickers && await pickers.confirmStart(container)))
|
|
10379
|
-
throw err;
|
|
10380
|
-
await authorizedRequest("/api/v1/tools/sandboxes_containers_start", {
|
|
10381
|
-
method: "POST",
|
|
10382
|
-
body: JSON.stringify({
|
|
10383
|
-
organization: input.organization,
|
|
10384
|
-
sandboxId: input.sandboxId,
|
|
10385
|
-
container
|
|
10386
|
-
})
|
|
10387
|
-
});
|
|
10388
|
-
notice(eventLine("done", `Started ${container}.`));
|
|
10389
|
-
return await send();
|
|
10390
|
-
}
|
|
10391
|
-
}
|
|
10392
10734
|
function stopNote(result, notice) {
|
|
10393
10735
|
if (!result || typeof result !== "object")
|
|
10394
10736
|
return;
|
|
@@ -10680,178 +11022,8 @@ var logoutCommand = defineCommand({
|
|
|
10680
11022
|
|
|
10681
11023
|
// src/commands/network.ts
|
|
10682
11024
|
import { spawn, spawnSync } from "node:child_process";
|
|
11025
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10683
11026
|
import { hostname as hostname2 } from "node:os";
|
|
10684
|
-
|
|
10685
|
-
// src/network/tunnel.ts
|
|
10686
|
-
import { createPrivateKey, createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
10687
|
-
import { existsSync } from "node:fs";
|
|
10688
|
-
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
10689
|
-
import { dirname as dirname3, join as join3 } from "node:path";
|
|
10690
|
-
var INTERFACE = "gdock";
|
|
10691
|
-
function networkDir() {
|
|
10692
|
-
return join3(getConfigDir(), "network");
|
|
10693
|
-
}
|
|
10694
|
-
function configPath(dir = networkDir()) {
|
|
10695
|
-
return join3(dir, `${INTERFACE}.conf`);
|
|
10696
|
-
}
|
|
10697
|
-
function keyPath(dir) {
|
|
10698
|
-
return join3(dir, "private.key");
|
|
10699
|
-
}
|
|
10700
|
-
function statePath(dir) {
|
|
10701
|
-
return join3(dir, "device.json");
|
|
10702
|
-
}
|
|
10703
|
-
function isProvider(value) {
|
|
10704
|
-
const provider = value;
|
|
10705
|
-
return typeof provider?.id === "string" && typeof provider.name === "string";
|
|
10706
|
-
}
|
|
10707
|
-
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
10708
|
-
function keyPairFromPrivate(privateKey) {
|
|
10709
|
-
const raw = Buffer.from(privateKey, "base64");
|
|
10710
|
-
if (raw.length !== 32)
|
|
10711
|
-
throw new Error("The stored WireGuard key is not 32 bytes.");
|
|
10712
|
-
const key = createPrivateKey({
|
|
10713
|
-
key: Buffer.concat([X25519_PKCS8_PREFIX, raw]),
|
|
10714
|
-
format: "der",
|
|
10715
|
-
type: "pkcs8"
|
|
10716
|
-
});
|
|
10717
|
-
const spki = createPublicKey(key).export({ format: "der", type: "spki" });
|
|
10718
|
-
return { privateKey, publicKey: spki.subarray(spki.length - 32).toString("base64") };
|
|
10719
|
-
}
|
|
10720
|
-
async function loadOrCreateKey(dir = networkDir()) {
|
|
10721
|
-
try {
|
|
10722
|
-
return keyPairFromPrivate((await readFile3(keyPath(dir), "utf8")).trim());
|
|
10723
|
-
} catch (err) {
|
|
10724
|
-
if (err.code !== "ENOENT")
|
|
10725
|
-
throw err;
|
|
10726
|
-
}
|
|
10727
|
-
const { privateKey } = generateKeyPairSync("x25519");
|
|
10728
|
-
const der = privateKey.export({ format: "der", type: "pkcs8" });
|
|
10729
|
-
const raw = der.subarray(der.length - 32).toString("base64");
|
|
10730
|
-
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
10731
|
-
await writeFile3(keyPath(dir), `${raw}
|
|
10732
|
-
`, { mode: 384 });
|
|
10733
|
-
return keyPairFromPrivate(raw);
|
|
10734
|
-
}
|
|
10735
|
-
async function readDeviceState(dir = networkDir()) {
|
|
10736
|
-
try {
|
|
10737
|
-
const parsed = JSON.parse(await readFile3(statePath(dir), "utf8"));
|
|
10738
|
-
if (typeof parsed.deviceId === "string" && typeof parsed.baseUrl === "string") {
|
|
10739
|
-
return {
|
|
10740
|
-
baseUrl: parsed.baseUrl,
|
|
10741
|
-
deviceId: parsed.deviceId,
|
|
10742
|
-
address: typeof parsed.address === "string" ? parsed.address : "",
|
|
10743
|
-
gatewayProvider: isProvider(parsed.gatewayProvider) ? { id: parsed.gatewayProvider.id, name: parsed.gatewayProvider.name } : null
|
|
10744
|
-
};
|
|
10745
|
-
}
|
|
10746
|
-
} catch {}
|
|
10747
|
-
return null;
|
|
10748
|
-
}
|
|
10749
|
-
async function writeDeviceState(state, dir = networkDir()) {
|
|
10750
|
-
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
10751
|
-
await writeFile3(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
10752
|
-
`, { mode: 384 });
|
|
10753
|
-
}
|
|
10754
|
-
function detectDnsMode(platform = process.platform, exists = existsSync) {
|
|
10755
|
-
if (platform === "darwin")
|
|
10756
|
-
return "macos-resolver";
|
|
10757
|
-
if (platform === "linux" && exists("/run/systemd/resolve"))
|
|
10758
|
-
return "resolved";
|
|
10759
|
-
return "none";
|
|
10760
|
-
}
|
|
10761
|
-
function renderConfig(input) {
|
|
10762
|
-
const { device, network, gateway } = input.registered;
|
|
10763
|
-
const zones = [network.zone, ...network.netZone ? [network.netZone] : []];
|
|
10764
|
-
for (const zone of zones) {
|
|
10765
|
-
if (!isDnsZone(zone))
|
|
10766
|
-
throw new Error(`The server sent a zone that is not a DNS name: ${zone}`);
|
|
10767
|
-
}
|
|
10768
|
-
if (!device.address || !isIPv6Address(device.address)) {
|
|
10769
|
-
throw new Error(`The server sent no IPv6 address for this device: ${device.address}`);
|
|
10770
|
-
}
|
|
10771
|
-
if (!isIPv6Address(network.gatewayAddress)) {
|
|
10772
|
-
throw new Error(`The server sent a gateway that is not an IPv6 address: ${network.gatewayAddress}`);
|
|
10773
|
-
}
|
|
10774
|
-
const ipv4 = device.address4 && network.ingress4 ? { address: device.address4, ingress: network.ingress4 } : null;
|
|
10775
|
-
if (ipv4 && (!isIPv4Address(ipv4.address) || !isIPv4Address(ipv4.ingress))) {
|
|
10776
|
-
throw new Error(`The server sent an IPv4 address that is not one: ${ipv4.address}, ${ipv4.ingress}`);
|
|
10777
|
-
}
|
|
10778
|
-
const lines = [
|
|
10779
|
-
"# Written by `ghostdock network connect`. Run it again rather than editing this file.",
|
|
10780
|
-
"[Interface]",
|
|
10781
|
-
`PrivateKey = ${input.privateKey}`,
|
|
10782
|
-
`Address = ${device.address}/128${ipv4 ? `, ${ipv4.address}/32` : ""}`,
|
|
10783
|
-
`MTU = ${network.mtu}`
|
|
10784
|
-
];
|
|
10785
|
-
if (input.dns === "macos-resolver") {
|
|
10786
|
-
for (const zone of zones) {
|
|
10787
|
-
const file = `/etc/resolver/${zone}`;
|
|
10788
|
-
lines.push(`PostUp = mkdir -p /etc/resolver && printf 'nameserver ${network.gatewayAddress}\\n' > ${file}`, `PostDown = rm -f ${file}`);
|
|
10789
|
-
}
|
|
10790
|
-
} else if (input.dns === "resolved") {
|
|
10791
|
-
const domains = zones.map((zone) => `'~${zone}'`).join(" ");
|
|
10792
|
-
lines.push(`PostUp = resolvectl dns %i ${network.gatewayAddress} && resolvectl domain %i ${domains}`);
|
|
10793
|
-
}
|
|
10794
|
-
lines.push("", "[Peer]", `PublicKey = ${gateway.publicKey}`, `Endpoint = ${gateway.endpoint}`, `AllowedIPs = ${network.prefix}${ipv4 ? `, ${ipv4.ingress}/32` : ""}`, "PersistentKeepalive = 25", "");
|
|
10795
|
-
return lines.join(`
|
|
10796
|
-
`);
|
|
10797
|
-
}
|
|
10798
|
-
var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
10799
|
-
function isDnsZone(zone) {
|
|
10800
|
-
const labels = zone.split(".");
|
|
10801
|
-
return zone.length <= 253 && labels.length >= 2 && labels.every((label) => DNS_LABEL.test(label));
|
|
10802
|
-
}
|
|
10803
|
-
function isIPv4Address(text) {
|
|
10804
|
-
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);
|
|
10805
|
-
}
|
|
10806
|
-
function ipv4RouteOwner(platform, output) {
|
|
10807
|
-
if (platform === "darwin") {
|
|
10808
|
-
if (/^\s*destination: default$/m.test(output))
|
|
10809
|
-
return null;
|
|
10810
|
-
return /^\s*interface: (\S+)$/m.exec(output)?.[1] ?? null;
|
|
10811
|
-
}
|
|
10812
|
-
for (const line of output.split(`
|
|
10813
|
-
`).map((l) => l.trim())) {
|
|
10814
|
-
if (!line || /^(default|broadcast)\b/.test(line))
|
|
10815
|
-
continue;
|
|
10816
|
-
return /\bdev (\S+)/.exec(line)?.[1] ?? "another route";
|
|
10817
|
-
}
|
|
10818
|
-
return null;
|
|
10819
|
-
}
|
|
10820
|
-
function ipv4RouteCommand(platform, address) {
|
|
10821
|
-
return platform === "darwin" ? ["route", ["-n", "get", address]] : ["ip", ["-4", "route", "show", "table", "all", "match", address]];
|
|
10822
|
-
}
|
|
10823
|
-
function isIPv6Address(text) {
|
|
10824
|
-
return /^[0-9a-f:]{2,39}$/.test(text) && text.includes(":");
|
|
10825
|
-
}
|
|
10826
|
-
async function readConfigMtu(path = configPath()) {
|
|
10827
|
-
let contents;
|
|
10828
|
-
try {
|
|
10829
|
-
contents = await readFile3(path, "utf8");
|
|
10830
|
-
} catch {
|
|
10831
|
-
return null;
|
|
10832
|
-
}
|
|
10833
|
-
const match = /^MTU = (\d+)$/m.exec(contents);
|
|
10834
|
-
return match ? Number(match[1]) : null;
|
|
10835
|
-
}
|
|
10836
|
-
async function writeConfig(contents, path = configPath()) {
|
|
10837
|
-
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
10838
|
-
await writeFile3(path, contents, { mode: 384 });
|
|
10839
|
-
await chmod2(path, 384);
|
|
10840
|
-
}
|
|
10841
|
-
function tunnelUp(platform = process.platform, exists = existsSync) {
|
|
10842
|
-
if (platform === "darwin")
|
|
10843
|
-
return exists(`/var/run/wireguard/${INTERFACE}.name`);
|
|
10844
|
-
return exists(`/sys/class/net/${INTERFACE}`);
|
|
10845
|
-
}
|
|
10846
|
-
function installHint(platform = process.platform) {
|
|
10847
|
-
if (platform === "darwin")
|
|
10848
|
-
return "brew install wireguard-tools";
|
|
10849
|
-
if (platform === "linux")
|
|
10850
|
-
return "sudo apt install wireguard-tools (or your distribution’s package)";
|
|
10851
|
-
return "Install wireguard-tools. On Windows, run the CLI inside WSL.";
|
|
10852
|
-
}
|
|
10853
|
-
|
|
10854
|
-
// src/commands/network.ts
|
|
10855
11027
|
function defaultDeps() {
|
|
10856
11028
|
return {
|
|
10857
11029
|
request: authorizedRequest,
|
|
@@ -10868,6 +11040,7 @@ function defaultDeps() {
|
|
|
10868
11040
|
has: (command) => spawnSync("sh", ["-c", `command -v ${command}`]).status === 0,
|
|
10869
11041
|
dir: networkDir(),
|
|
10870
11042
|
up: () => tunnelUp(),
|
|
11043
|
+
exists: existsSync2,
|
|
10871
11044
|
log: console.log,
|
|
10872
11045
|
error: printError,
|
|
10873
11046
|
notice: printNotice
|
|
@@ -10887,6 +11060,7 @@ async function wgQuick(deps, verb) {
|
|
|
10887
11060
|
return deps.run("sudo", args);
|
|
10888
11061
|
}
|
|
10889
11062
|
async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
11063
|
+
suppressPausedNotice();
|
|
10890
11064
|
if (!deps.has("wg-quick")) {
|
|
10891
11065
|
deps.error(`wg-quick is not installed. ${installHint()}`);
|
|
10892
11066
|
return 1;
|
|
@@ -10920,14 +11094,37 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
|
10920
11094
|
deps.error(message(err));
|
|
10921
11095
|
return 1;
|
|
10922
11096
|
}
|
|
11097
|
+
const previousConfig = await readConfig(configPath(deps.dir));
|
|
10923
11098
|
await writeConfig(config, configPath(deps.dir));
|
|
10924
11099
|
await writeDeviceState({
|
|
10925
11100
|
baseUrl: getBaseUrl(),
|
|
10926
11101
|
deviceId: registered.device.id,
|
|
10927
11102
|
address: registered.device.address ?? "",
|
|
10928
|
-
gatewayProvider: registered.provider
|
|
11103
|
+
gatewayProvider: registered.provider,
|
|
11104
|
+
zones: [
|
|
11105
|
+
registered.network.zone,
|
|
11106
|
+
...registered.network.netZone ? [registered.network.netZone] : []
|
|
11107
|
+
]
|
|
10929
11108
|
}, deps.dir);
|
|
10930
11109
|
const wasUp = deps.up();
|
|
11110
|
+
if (wasUp && previousConfig === config) {
|
|
11111
|
+
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
11112
|
+
printResult({
|
|
11113
|
+
device,
|
|
11114
|
+
network,
|
|
11115
|
+
provider,
|
|
11116
|
+
shortNameOrganizationChanged,
|
|
11117
|
+
interface: INTERFACE,
|
|
11118
|
+
dns,
|
|
11119
|
+
rearmed: true,
|
|
11120
|
+
pause: device.state
|
|
11121
|
+
}, () => [
|
|
11122
|
+
eventLine("done", rearmedLine(device.state)),
|
|
11123
|
+
...network.netZone && device.shortNameOrganization && shortNameOrganizationChanged ? clearingNotice(network.netCookieSite) : []
|
|
11124
|
+
], deps.log);
|
|
11125
|
+
printPauseHint(device.state, deps);
|
|
11126
|
+
return 0;
|
|
11127
|
+
}
|
|
10931
11128
|
if (wasUp) {
|
|
10932
11129
|
const down = await wgQuick(deps, "down");
|
|
10933
11130
|
if (down !== 0) {
|
|
@@ -10944,7 +11141,16 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
|
10944
11141
|
const { device, network, provider, shortNameOrganizationChanged } = registered;
|
|
10945
11142
|
const pinned = device.shortNameOrganization;
|
|
10946
11143
|
const switched = wasUp && previous?.gatewayProvider && previous.gatewayProvider.id !== provider.id ? previous.gatewayProvider : null;
|
|
10947
|
-
printResult({
|
|
11144
|
+
printResult({
|
|
11145
|
+
device,
|
|
11146
|
+
network,
|
|
11147
|
+
provider,
|
|
11148
|
+
shortNameOrganizationChanged,
|
|
11149
|
+
interface: INTERFACE,
|
|
11150
|
+
dns,
|
|
11151
|
+
rearmed: false,
|
|
11152
|
+
pause: device.state
|
|
11153
|
+
}, () => [
|
|
10948
11154
|
eventLine("done", `Connected through ${provider.name}. This machine is ${device.address} in the private network.`),
|
|
10949
11155
|
...switched ? [
|
|
10950
11156
|
` Replaced the connection through ${switched.name}: its sandboxes are out of reach now.`
|
|
@@ -10969,8 +11175,22 @@ async function runNetworkConnect(args = {}, deps = defaultDeps()) {
|
|
|
10969
11175
|
] : [],
|
|
10970
11176
|
` → ${getCliName()} network ls`
|
|
10971
11177
|
], deps.log);
|
|
11178
|
+
printPauseHint(device.state, deps);
|
|
10972
11179
|
return 0;
|
|
10973
11180
|
}
|
|
11181
|
+
function rearmedLine(state, now = new Date) {
|
|
11182
|
+
if (!state)
|
|
11183
|
+
return "Re-armed.";
|
|
11184
|
+
const until = clockTime(new Date(now.getTime() + state.maxSessionSeconds * 1000).toISOString(), now);
|
|
11185
|
+
return `Re-armed: network access for this device runs until ${until} (pauses after ${formatPauseLimit(state.idleTimeoutSeconds)} idle).`;
|
|
11186
|
+
}
|
|
11187
|
+
function printPauseHint(state, deps) {
|
|
11188
|
+
if (!state || isJsonOutput())
|
|
11189
|
+
return;
|
|
11190
|
+
const [first = "", ...rest] = pauseHint(state);
|
|
11191
|
+
deps.notice(["", bold2(first), ...rest].join(`
|
|
11192
|
+
`));
|
|
11193
|
+
}
|
|
10974
11194
|
function ipv4Clashes4(registered, deps) {
|
|
10975
11195
|
const addresses = [registered.network.ingress4, registered.device.address4].filter((a) => typeof a === "string" && isIPv4Address(a));
|
|
10976
11196
|
const clashes = [];
|
|
@@ -10991,17 +11211,41 @@ function clearingNotice(cookieSite) {
|
|
|
10991
11211
|
" Reload or close browser tabs opened on short names before this connect."
|
|
10992
11212
|
];
|
|
10993
11213
|
}
|
|
11214
|
+
async function staleResolverFiles(deps) {
|
|
11215
|
+
if (deps.platform !== "darwin")
|
|
11216
|
+
return [];
|
|
11217
|
+
const state = await readDeviceState(deps.dir);
|
|
11218
|
+
const zones = new Set([
|
|
11219
|
+
...configResolverZones(await readConfig(configPath(deps.dir))),
|
|
11220
|
+
...state?.zones ?? []
|
|
11221
|
+
]);
|
|
11222
|
+
return [...zones].map(resolverFile).filter((file) => deps.exists(file));
|
|
11223
|
+
}
|
|
10994
11224
|
async function runNetworkDisconnect(deps = defaultDeps()) {
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
11225
|
+
const wasUp = deps.up();
|
|
11226
|
+
let failed = false;
|
|
11227
|
+
if (wasUp) {
|
|
11228
|
+
const code = await wgQuick(deps, "down");
|
|
11229
|
+
if (code !== 0) {
|
|
11230
|
+
deps.error(`wg-quick exited ${code}.`);
|
|
11231
|
+
failed = true;
|
|
11232
|
+
}
|
|
11233
|
+
}
|
|
11234
|
+
const stale = await staleResolverFiles(deps);
|
|
11235
|
+
if (stale.length) {
|
|
11236
|
+
const args = ["rm", "-f", ...stale];
|
|
11237
|
+
deps.notice(`Removing split DNS left from an earlier connection. Running: sudo ${args.join(" ")}`);
|
|
11238
|
+
const code = await deps.run("sudo", args);
|
|
11239
|
+
if (code !== 0) {
|
|
11240
|
+
deps.error(`Could not remove ${stale.join(", ")} (rm exited ${code}).`);
|
|
11241
|
+
return 1;
|
|
11242
|
+
}
|
|
10998
11243
|
}
|
|
10999
|
-
|
|
11000
|
-
if (code !== 0) {
|
|
11001
|
-
deps.error(`wg-quick exited ${code}.`);
|
|
11244
|
+
if (failed)
|
|
11002
11245
|
return 1;
|
|
11003
|
-
}
|
|
11004
|
-
|
|
11246
|
+
printResult({ connected: false, removedSplitDns: stale }, () => [
|
|
11247
|
+
wasUp ? eventLine("done", "Disconnected.") : eventLine(stale.length ? "done" : "item", stale.length ? "Not connected; removed the leftover split DNS." : "Not connected.")
|
|
11248
|
+
], deps.log);
|
|
11005
11249
|
return 0;
|
|
11006
11250
|
}
|
|
11007
11251
|
async function readConnection(deps) {
|
|
@@ -11039,7 +11283,11 @@ async function readConnection(deps) {
|
|
|
11039
11283
|
}
|
|
11040
11284
|
async function runNetworkStatus(deps = defaultDeps()) {
|
|
11041
11285
|
const { state, up, device, asked, provider, staleMtu } = await readConnection(deps);
|
|
11042
|
-
|
|
11286
|
+
const staleSplitDns = up ? [] : await staleResolverFiles(deps);
|
|
11287
|
+
for (const file of staleSplitDns) {
|
|
11288
|
+
deps.notice(`Split DNS from an earlier connection is still set (${file}): names of the private network will hang. Run: ${getCliName()} network disconnect`);
|
|
11289
|
+
}
|
|
11290
|
+
printResult({ connected: up, device, provider, staleMtu, staleSplitDns }, () => {
|
|
11043
11291
|
if (!state) {
|
|
11044
11292
|
return [eventLine("item", `Not set up. Run \`${getCliName()} network connect\`.`)];
|
|
11045
11293
|
}
|
|
@@ -11049,6 +11297,7 @@ async function runNetworkStatus(deps = defaultDeps()) {
|
|
|
11049
11297
|
];
|
|
11050
11298
|
if (device) {
|
|
11051
11299
|
lines.push(` Device ${device.name}, valid until ${device.expiresAt.slice(0, 10)}.`);
|
|
11300
|
+
lines.push(` ${deviceStateText(device.state)}.`);
|
|
11052
11301
|
if (device.shortNameOrganization) {
|
|
11053
11302
|
lines.push(` Short names: ${device.shortNameOrganization.slug}'s sandboxes.`);
|
|
11054
11303
|
}
|
|
@@ -11118,7 +11367,7 @@ async function runNetworkDevices(deps = defaultDeps()) {
|
|
|
11118
11367
|
try {
|
|
11119
11368
|
const { devices } = await deps.request("/api/v1/network/devices");
|
|
11120
11369
|
const own = (await readDeviceState(deps.dir))?.deviceId;
|
|
11121
|
-
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)}`), deps.log);
|
|
11370
|
+
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);
|
|
11122
11371
|
return 0;
|
|
11123
11372
|
} catch (err) {
|
|
11124
11373
|
deps.error(message(err));
|
|
@@ -11140,7 +11389,7 @@ async function runNetworkDevicesRemove(args, deps = defaultDeps()) {
|
|
|
11140
11389
|
var connectCommand = defineCommand({
|
|
11141
11390
|
meta: {
|
|
11142
11391
|
name: "connect",
|
|
11143
|
-
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.
|
|
11392
|
+
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."
|
|
11144
11393
|
},
|
|
11145
11394
|
args: {
|
|
11146
11395
|
provider: {
|
|
@@ -11155,7 +11404,10 @@ var connectCommand = defineCommand({
|
|
|
11155
11404
|
}
|
|
11156
11405
|
});
|
|
11157
11406
|
var disconnectCommand = defineCommand({
|
|
11158
|
-
meta: {
|
|
11407
|
+
meta: {
|
|
11408
|
+
name: "disconnect",
|
|
11409
|
+
description: "Take this machine out of the private network, and remove split DNS an earlier connection left (macOS)."
|
|
11410
|
+
},
|
|
11159
11411
|
async run() {
|
|
11160
11412
|
process.exitCode = await runNetworkDisconnect();
|
|
11161
11413
|
}
|
|
@@ -12745,7 +12997,7 @@ async function collectPath3(path) {
|
|
|
12745
12997
|
await visit(path);
|
|
12746
12998
|
return files;
|
|
12747
12999
|
}
|
|
12748
|
-
async function runSandboxUpload(options, log = console.log, error = printError) {
|
|
13000
|
+
async function runSandboxUpload(options, log = console.log, error = printError, notice = printNotice, pickers = defaultContainerPickers()) {
|
|
12749
13001
|
if (options.paths.length === 0 && !options.zip) {
|
|
12750
13002
|
error("Provide at least one path or --zip.");
|
|
12751
13003
|
return 1;
|
|
@@ -12761,9 +13013,26 @@ async function runSandboxUpload(options, log = console.log, error = printError)
|
|
|
12761
13013
|
if (options.zip) {
|
|
12762
13014
|
form.append("zip", new Blob([await readFile7(options.zip)]), basename5(options.zip));
|
|
12763
13015
|
}
|
|
12764
|
-
|
|
13016
|
+
let container = options.container;
|
|
13017
|
+
const call = {
|
|
13018
|
+
organization,
|
|
13019
|
+
sandboxId: options.sandboxId,
|
|
13020
|
+
canPick: !container,
|
|
13021
|
+
setContainer: (picked) => {
|
|
13022
|
+
container = picked;
|
|
13023
|
+
}
|
|
13024
|
+
};
|
|
13025
|
+
const send = async () => {
|
|
13026
|
+
const query = new URLSearchParams({ organization });
|
|
13027
|
+
if (container)
|
|
13028
|
+
query.set("container", container);
|
|
13029
|
+
const { value } = await withProgress("Uploading…", () => authorizedRequest(`/api/v1/sandboxes/${encodeURIComponent(options.sandboxId)}/files?${query}`, { method: "POST", body: form }));
|
|
13030
|
+
return value;
|
|
13031
|
+
};
|
|
13032
|
+
const result = await withContainerPick(call, () => withContainerStart(call, send, options.start === true, pickers, notice), pickers, notice);
|
|
13033
|
+
const count = `${result.written.length} file${result.written.length === 1 ? "" : "s"}`;
|
|
12765
13034
|
printResult(result, () => [
|
|
12766
|
-
eventLine("done", `Uploaded ${
|
|
13035
|
+
eventLine("done", result.container ? `Uploaded ${count} to ${result.container} in ${options.sandboxId}.` : `Uploaded ${count} to ${options.sandboxId}.`)
|
|
12767
13036
|
], log);
|
|
12768
13037
|
return 0;
|
|
12769
13038
|
} catch (err) {
|
|
@@ -12787,6 +13056,14 @@ var sandboxUploadCommand = defineCommand({
|
|
|
12787
13056
|
required: true
|
|
12788
13057
|
},
|
|
12789
13058
|
org: orgArg,
|
|
13059
|
+
container: {
|
|
13060
|
+
type: "string",
|
|
13061
|
+
description: `Container the files land in, by name, from \`${getCliName()} sandboxes containers list\`. Default: the sandbox's default container, else the one running container.`
|
|
13062
|
+
},
|
|
13063
|
+
start: {
|
|
13064
|
+
type: "boolean",
|
|
13065
|
+
description: "Start the container first when it is not running, without asking."
|
|
13066
|
+
},
|
|
12790
13067
|
zip: {
|
|
12791
13068
|
type: "string",
|
|
12792
13069
|
description: "Local ZIP file to upload."
|
|
@@ -12802,7 +13079,9 @@ var sandboxUploadCommand = defineCommand({
|
|
|
12802
13079
|
sandboxId: args["sandbox-id"],
|
|
12803
13080
|
org: args.org,
|
|
12804
13081
|
paths: args._.slice(1),
|
|
12805
|
-
zip: args.zip
|
|
13082
|
+
zip: args.zip,
|
|
13083
|
+
container: args.container,
|
|
13084
|
+
start: args.start
|
|
12806
13085
|
});
|
|
12807
13086
|
}
|
|
12808
13087
|
});
|