@kvbox/cli 0.5.24 → 0.5.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/kvbox.js +499 -139
- package/dist/kvbox.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox.js
CHANGED
|
@@ -149,7 +149,7 @@ async function spin(message, work, opts = {}) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// src/program.ts
|
|
152
|
-
import { Command as
|
|
152
|
+
import { Command as Command61 } from "commander";
|
|
153
153
|
|
|
154
154
|
// ../shared/src/version.ts
|
|
155
155
|
import { createRequire } from "module";
|
|
@@ -164,7 +164,7 @@ function versionOf(srcFileUrl, injected) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// src/version.ts
|
|
167
|
-
var VERSION = versionOf(import.meta.url, true ? "0.5.
|
|
167
|
+
var VERSION = versionOf(import.meta.url, true ? "0.5.26" : void 0);
|
|
168
168
|
|
|
169
169
|
// src/commands/vm/index.ts
|
|
170
170
|
import { Command as Command26 } from "commander";
|
|
@@ -193,6 +193,8 @@ function paths(home = kvboxHome()) {
|
|
|
193
193
|
serverLog: join(home, "server.log"),
|
|
194
194
|
/** The CLI's login to the control plane (`kvbox login`): url, bearer token, user. Mode 600. */
|
|
195
195
|
cli: join(home, "cli.json"),
|
|
196
|
+
/** The CLI's last look at npm for a newer @kvbox/cli: `{checkedAt, latest}`, at most once a day (cli update.ts). */
|
|
197
|
+
updateCheck: join(home, "update-check.json"),
|
|
196
198
|
ssh: join(home, "ssh"),
|
|
197
199
|
sshKey: join(home, "ssh", "id_ed25519"),
|
|
198
200
|
/** The device key signed by the control plane's CA (#51); ssh loads it by itself next to `-i id_ed25519`. */
|
|
@@ -262,6 +264,9 @@ function paths(home = kvboxHome()) {
|
|
|
262
264
|
disk: join(dir, "disk.ext4")
|
|
263
265
|
};
|
|
264
266
|
},
|
|
267
|
+
/** Each owner's environment variables as the control plane last sent them (#88): env/<owner>.json, mode 600. */
|
|
268
|
+
env: join(home, "env"),
|
|
269
|
+
ownerEnv: (owner) => join(home, "env", `${owner}.json`),
|
|
265
270
|
snapshot: (owner, name) => {
|
|
266
271
|
const dir = join(home, "snapshots", owner, name);
|
|
267
272
|
return {
|
|
@@ -492,6 +497,8 @@ var VolumeSchema = z2.object({
|
|
|
492
497
|
ownerKind: z2.enum(["user", "org"]).optional()
|
|
493
498
|
});
|
|
494
499
|
var MAX_VOLUME_GIB = 4096;
|
|
500
|
+
var VOLUME_NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
|
|
501
|
+
var VOLUME_NAME_RULE = "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit";
|
|
495
502
|
var CreateVolumeRequest = z2.object({
|
|
496
503
|
name: z2.string(),
|
|
497
504
|
sizeGib: z2.number().int().min(1).max(MAX_VOLUME_GIB),
|
|
@@ -505,6 +512,26 @@ var CreateVolumeRequest = z2.object({
|
|
|
505
512
|
var AttachVolumeRequest = z2.object({
|
|
506
513
|
box: z2.string()
|
|
507
514
|
});
|
|
515
|
+
var EnvVarSchema = z2.object({
|
|
516
|
+
name: z2.string(),
|
|
517
|
+
value: z2.string().optional(),
|
|
518
|
+
secret: z2.boolean(),
|
|
519
|
+
updatedAt: z2.string()
|
|
520
|
+
});
|
|
521
|
+
var SetEnvRequest = z2.object({
|
|
522
|
+
value: z2.string(),
|
|
523
|
+
secret: z2.boolean().optional()
|
|
524
|
+
});
|
|
525
|
+
var EnvChangeSchema = z2.object({
|
|
526
|
+
owner: z2.string(),
|
|
527
|
+
name: z2.string(),
|
|
528
|
+
boxes: z2.number().int()
|
|
529
|
+
});
|
|
530
|
+
var HostEnvRequest = z2.object({
|
|
531
|
+
version: z2.number().int().min(0),
|
|
532
|
+
vars: z2.array(z2.object({ name: z2.string(), value: z2.string() }))
|
|
533
|
+
});
|
|
534
|
+
var HostEnvVersionsSchema = z2.array(z2.object({ owner: z2.string(), version: z2.number().int() }));
|
|
508
535
|
var CreateMachineRequest = z2.object({
|
|
509
536
|
name: z2.string().optional(),
|
|
510
537
|
/** Ignored over the local socket (the caller is the host owner); the control plane sets it over HTTPS. */
|
|
@@ -802,10 +829,26 @@ var TopUpSchema = z2.object({
|
|
|
802
829
|
slug: z2.string(),
|
|
803
830
|
amount: z2.number(),
|
|
804
831
|
currency: z2.string(),
|
|
805
|
-
|
|
832
|
+
/** `transfer` (#93): an owner moving credit to an org they own, a negative row on their slug and a positive one on the org's. */
|
|
833
|
+
source: z2.enum(["admin", "card", "trial", "transfer"]),
|
|
806
834
|
note: z2.string().nullable(),
|
|
807
835
|
at: z2.string()
|
|
808
836
|
});
|
|
837
|
+
var TOP_UP_AMOUNTS = [10, 25, 50, 100];
|
|
838
|
+
var BillingInfoSchema = z2.object({
|
|
839
|
+
card: z2.object({ brand: z2.string(), last4: z2.string() }).nullable(),
|
|
840
|
+
cardInUse: z2.boolean(),
|
|
841
|
+
balance: BalanceSchema
|
|
842
|
+
});
|
|
843
|
+
var BillingRedirectSchema = z2.object({ url: z2.string() });
|
|
844
|
+
var TopUpCheckoutRequestSchema = z2.object({
|
|
845
|
+
amount: z2.literal(TOP_UP_AMOUNTS)
|
|
846
|
+
});
|
|
847
|
+
var TransferRequestSchema = z2.object({
|
|
848
|
+
org: z2.string().min(1),
|
|
849
|
+
amount: z2.number().positive().multipleOf(0.01, "amount is in cents at most")
|
|
850
|
+
});
|
|
851
|
+
var TransferResponseSchema = z2.object({ from: BalanceSchema, to: BalanceSchema, activated: z2.boolean() });
|
|
809
852
|
var LoginRequestSchema = z2.object({
|
|
810
853
|
email: z2.string().min(1),
|
|
811
854
|
password: z2.string().min(1),
|
|
@@ -1137,6 +1180,16 @@ var ServerClient = class {
|
|
|
1137
1180
|
deleteSnapshot(name) {
|
|
1138
1181
|
return this.call("DELETE", `/snapshots/${encodeURIComponent(name)}`, z3.undefined());
|
|
1139
1182
|
}
|
|
1183
|
+
// ---- environment variables (#88), a control plane only: the caller's slug, or `--org`'s ----
|
|
1184
|
+
listEnv() {
|
|
1185
|
+
return this.call("GET", "/env", z3.array(EnvVarSchema));
|
|
1186
|
+
}
|
|
1187
|
+
setEnv(name, req) {
|
|
1188
|
+
return this.call("PUT", `/env/${encodeURIComponent(name)}`, EnvChangeSchema, req);
|
|
1189
|
+
}
|
|
1190
|
+
removeEnv(name) {
|
|
1191
|
+
return this.call("DELETE", `/env/${encodeURIComponent(name)}`, EnvChangeSchema);
|
|
1192
|
+
}
|
|
1140
1193
|
// ---- volumes (#52) ----
|
|
1141
1194
|
listVolumes(opts = {}) {
|
|
1142
1195
|
return this.call("GET", `/volumes${allQuery(opts)}`, z3.array(VolumeSchema), void 0, opts.org);
|
|
@@ -1348,6 +1401,199 @@ async function serving(p, timeoutMs, check) {
|
|
|
1348
1401
|
throw new KvboxError(`kvbox-server did not start within ${timeoutMs / 1e3}s; see ${p.serverLog}`);
|
|
1349
1402
|
}
|
|
1350
1403
|
|
|
1404
|
+
// src/prompt.ts
|
|
1405
|
+
import { createInterface } from "readline/promises";
|
|
1406
|
+
function interactive() {
|
|
1407
|
+
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
1408
|
+
}
|
|
1409
|
+
var ESC = "\x1B";
|
|
1410
|
+
var ENTER = "\r";
|
|
1411
|
+
var CTRL_C = "";
|
|
1412
|
+
var CTRL_D = "";
|
|
1413
|
+
var cancelled = () => new KvboxError("cancelled", 130);
|
|
1414
|
+
function keysIn(chunk) {
|
|
1415
|
+
const keys = [];
|
|
1416
|
+
for (let i = 0; i < chunk.length; ) {
|
|
1417
|
+
if (chunk[i] === ESC && chunk[i + 1] === "[" && i + 2 < chunk.length) {
|
|
1418
|
+
let j = i + 2;
|
|
1419
|
+
while (j < chunk.length && /[0-9;?]/.test(chunk[j])) j += 1;
|
|
1420
|
+
const end = j < chunk.length ? j + 1 : j;
|
|
1421
|
+
keys.push(chunk.slice(i, end));
|
|
1422
|
+
i = end;
|
|
1423
|
+
} else if (chunk[i] === ESC && chunk[i + 1] === "O" && i + 2 < chunk.length) {
|
|
1424
|
+
keys.push(chunk.slice(i, i + 3));
|
|
1425
|
+
i += 3;
|
|
1426
|
+
} else {
|
|
1427
|
+
keys.push(chunk[i]);
|
|
1428
|
+
i += 1;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return keys;
|
|
1432
|
+
}
|
|
1433
|
+
var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
|
|
1434
|
+
var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
|
|
1435
|
+
var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
1436
|
+
function rows(line, columns = process.stderr.columns ?? 80) {
|
|
1437
|
+
const width2 = line.replace(ANSI2, "").length;
|
|
1438
|
+
return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
|
|
1439
|
+
}
|
|
1440
|
+
function settled(message, answer) {
|
|
1441
|
+
return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
|
|
1442
|
+
}
|
|
1443
|
+
async function select(message, choices, initial = 0) {
|
|
1444
|
+
const out = process.stderr;
|
|
1445
|
+
const inp = process.stdin;
|
|
1446
|
+
let index = Math.min(Math.max(initial, 0), choices.length - 1);
|
|
1447
|
+
let drawn = 0;
|
|
1448
|
+
const render = () => {
|
|
1449
|
+
if (drawn > 0) out.write(`${ESC}[${drawn}A`);
|
|
1450
|
+
const lines = [
|
|
1451
|
+
`${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
|
|
1452
|
+
...choices.map((c, i) => {
|
|
1453
|
+
const hint = c.hint ? color.dim(` ${c.hint}`) : "";
|
|
1454
|
+
return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
|
|
1455
|
+
})
|
|
1456
|
+
];
|
|
1457
|
+
out.write(`\r${ESC}[J${lines.join("\n")}
|
|
1458
|
+
`);
|
|
1459
|
+
drawn = lines.reduce((n, l) => n + rows(l), 0);
|
|
1460
|
+
};
|
|
1461
|
+
inp.setRawMode(true);
|
|
1462
|
+
inp.resume();
|
|
1463
|
+
inp.setEncoding("utf8");
|
|
1464
|
+
render();
|
|
1465
|
+
let done = false;
|
|
1466
|
+
try {
|
|
1467
|
+
return await new Promise((resolve3, reject) => {
|
|
1468
|
+
const onData = (chunk) => {
|
|
1469
|
+
for (const key of keysIn(chunk)) {
|
|
1470
|
+
if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
|
|
1471
|
+
else if (isDown(key)) index = (index + 1) % choices.length;
|
|
1472
|
+
else if (key === ENTER || key === "\n") {
|
|
1473
|
+
inp.off("data", onData);
|
|
1474
|
+
done = true;
|
|
1475
|
+
resolve3(choices[index].value);
|
|
1476
|
+
return;
|
|
1477
|
+
} else if (key === CTRL_C || key === CTRL_D) {
|
|
1478
|
+
inp.off("data", onData);
|
|
1479
|
+
reject(cancelled());
|
|
1480
|
+
return;
|
|
1481
|
+
} else continue;
|
|
1482
|
+
render();
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
inp.on("data", onData);
|
|
1486
|
+
});
|
|
1487
|
+
} finally {
|
|
1488
|
+
inp.setRawMode(false);
|
|
1489
|
+
inp.pause();
|
|
1490
|
+
out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
|
|
1491
|
+
` : ""}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async function text(message, opts = {}) {
|
|
1495
|
+
const out = process.stderr;
|
|
1496
|
+
for (; ; ) {
|
|
1497
|
+
const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
|
|
1498
|
+
const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
|
|
1499
|
+
const rl = createInterface({ input: process.stdin, output: out });
|
|
1500
|
+
const line = await askLine(rl, question);
|
|
1501
|
+
const raw = opts.trim === false ? line : line.trim();
|
|
1502
|
+
const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
|
|
1503
|
+
const problem = opts.validate?.(answer);
|
|
1504
|
+
out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
|
|
1505
|
+
if (problem === void 0) {
|
|
1506
|
+
out.write(`${settled(message, answer)}
|
|
1507
|
+
`);
|
|
1508
|
+
return answer;
|
|
1509
|
+
}
|
|
1510
|
+
out.write(`${color.yellow("!")} ${problem}
|
|
1511
|
+
`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async function hiddenText(message) {
|
|
1515
|
+
const out = process.stderr;
|
|
1516
|
+
const inp = process.stdin;
|
|
1517
|
+
const question = `${color.green("?")} ${color.bold(message)} ${color.dim("(hidden) \u203A")} `;
|
|
1518
|
+
out.write(question);
|
|
1519
|
+
inp.setRawMode(true);
|
|
1520
|
+
inp.resume();
|
|
1521
|
+
inp.setEncoding("utf8");
|
|
1522
|
+
let chars = [];
|
|
1523
|
+
let done = false;
|
|
1524
|
+
try {
|
|
1525
|
+
return await new Promise((resolve3, reject) => {
|
|
1526
|
+
const onData = (chunk) => {
|
|
1527
|
+
for (const key of keysIn(chunk)) {
|
|
1528
|
+
if (key === ENTER || key === "\n") {
|
|
1529
|
+
inp.off("data", onData);
|
|
1530
|
+
done = true;
|
|
1531
|
+
resolve3(chars.join(""));
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (key === CTRL_C || key === CTRL_D) {
|
|
1535
|
+
inp.off("data", onData);
|
|
1536
|
+
reject(cancelled());
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (key === "\x7F" || key === "\b") chars = chars.slice(0, -1);
|
|
1540
|
+
else if (key.length === 1 && (key >= " " || key === " ")) chars.push(key);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
inp.on("data", onData);
|
|
1544
|
+
});
|
|
1545
|
+
} finally {
|
|
1546
|
+
inp.setRawMode(false);
|
|
1547
|
+
inp.pause();
|
|
1548
|
+
out.write(`\r${ESC}[J${done ? `${settled(message, "\u2022\u2022\u2022\u2022\u2022")}
|
|
1549
|
+
` : "\n"}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async function askLine(rl, question) {
|
|
1553
|
+
let onClose = () => {
|
|
1554
|
+
};
|
|
1555
|
+
const closed = new Promise((_, reject) => {
|
|
1556
|
+
onClose = () => reject(cancelled());
|
|
1557
|
+
rl.once("close", onClose);
|
|
1558
|
+
});
|
|
1559
|
+
try {
|
|
1560
|
+
return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
|
|
1561
|
+
} finally {
|
|
1562
|
+
rl.off("close", onClose);
|
|
1563
|
+
rl.close();
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
function abortIsCancel(err) {
|
|
1567
|
+
if (err.code === "ABORT_ERR") throw cancelled();
|
|
1568
|
+
throw err;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/pick.ts
|
|
1572
|
+
function askOrUsage(usage) {
|
|
1573
|
+
if (!interactive()) throw new KvboxError(`${usage} (or run it in a terminal to be asked)`);
|
|
1574
|
+
}
|
|
1575
|
+
function refOf(item, user) {
|
|
1576
|
+
return user !== void 0 && slugOf(item.owner) !== user ? `${slugOf(item.owner)}/${item.name}` : item.name;
|
|
1577
|
+
}
|
|
1578
|
+
async function pickBox(client, message = "Box", extra = []) {
|
|
1579
|
+
const user = me(client);
|
|
1580
|
+
const boxes = await spin("listing your boxes\u2026", client.listMachines());
|
|
1581
|
+
const choices = boxes.map((b) => ({ value: refOf(b, user), label: refOf(b, user), hint: b.status }));
|
|
1582
|
+
if (choices.length === 0 && extra.length === 0) throw new KvboxError("you have no boxes yet: make one with kvbox new");
|
|
1583
|
+
return select(message, [...choices, ...extra]);
|
|
1584
|
+
}
|
|
1585
|
+
async function pickVolume(client, keep, none) {
|
|
1586
|
+
const user = me(client);
|
|
1587
|
+
const volumes = (await spin("listing your volumes\u2026", client.listVolumes())).filter(keep);
|
|
1588
|
+
if (volumes.length === 0) throw new KvboxError(none);
|
|
1589
|
+
const refs = volumes.map((v) => refOf(v, user));
|
|
1590
|
+
const ref = await select(
|
|
1591
|
+
"Volume",
|
|
1592
|
+
volumes.map((v, i) => ({ value: refs[i], label: refs[i], hint: `${v.sizeGib} GB, ${v.attachedTo === null ? "not attached" : `on ${v.attachedTo}`}` }))
|
|
1593
|
+
);
|
|
1594
|
+
return { ...volumes[refs.indexOf(ref)], ref };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1351
1597
|
// src/commands/vm/new.ts
|
|
1352
1598
|
import { Command, Option } from "commander";
|
|
1353
1599
|
var newCommand = new Command("new").description("boot a new box from the base image or a snapshot").argument("[name]", "box name (box1, box2, \u2026 when omitted)").addOption(new Option("-s, --size <size>", "box size (default: server config, s)").choices(SIZE_NAMES)).option("-f, --from <snapshot>", "start from a snapshot instead of the base image").option("--auto-hibernate <duration>", "hibernate the box after this long without network traffic (30m, 4h, 1d; off: never; default: server config, off)").option("--force", "skip the capacity guard").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
@@ -1400,12 +1646,14 @@ function parseIdleTimeout(text2) {
|
|
|
1400
1646
|
|
|
1401
1647
|
// src/commands/vm/connect.ts
|
|
1402
1648
|
var SSH_FAILED = 255;
|
|
1403
|
-
var connectCommand = new Command2("connect").alias("cn").description("open a shell in a box over SSH (or run a command: kvbox connect <name> -- <cmd>)").argument("
|
|
1649
|
+
var connectCommand = new Command2("connect").alias("cn").description("open a shell in a box over SSH (or run a command: kvbox connect <name> -- <cmd>)").argument("[name]", "box name (picked from a list when left out)").argument("[cmd...]", "command to run instead of a shell").passThroughOptions().action(async (nameArg, cmdArgs, _opts, cmd) => {
|
|
1650
|
+
if (nameArg === void 0) askOrUsage("give the box: kvbox connect <name>");
|
|
1404
1651
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1405
1652
|
let ep;
|
|
1406
1653
|
let cert;
|
|
1407
1654
|
let owner;
|
|
1408
1655
|
try {
|
|
1656
|
+
const name = nameArg ?? await pickBox(client);
|
|
1409
1657
|
let box = pickRef(await boxesFor(client, name), name, me(client));
|
|
1410
1658
|
if (!box) throw new KvboxError(`box ${name}: not found (kvbox ls)`);
|
|
1411
1659
|
cert = await ensureCert(client, void 0, void 0, { owner: box.owner });
|
|
@@ -1760,12 +2008,17 @@ function assertRouteName(text2) {
|
|
|
1760
2008
|
}
|
|
1761
2009
|
|
|
1762
2010
|
// src/commands/vm/proxy/claim.ts
|
|
1763
|
-
var claimCommand = new Command9("claim").description("give a box its HTTPS address, https://<subdomain>.kvbox.app, serving one of its ports").argument("
|
|
1764
|
-
|
|
1765
|
-
|
|
2011
|
+
var claimCommand = new Command9("claim").description("give a box its HTTPS address, https://<subdomain>.kvbox.app, serving one of its ports").argument("[box]", "box name (picked from a list when left out)").argument("[subdomain]", "3-63 lowercase letters, digits and '-'; must be free (asked for when left out)").option("--port <port>", "the box port the address serves (asked for when left out)").option("--json", "print the subdomain as JSON").action(async (nameArg, subArg, opts, cmd) => {
|
|
2012
|
+
if (nameArg === void 0 || subArg === void 0 || opts.port === void 0) askOrUsage("give the box, the subdomain and the port: kvbox vm proxy claim <box> <subdomain> --port <port>");
|
|
2013
|
+
if (subArg !== void 0) assertSubdomain(subArg);
|
|
2014
|
+
if (opts.port !== void 0) parsePorts([opts.port]);
|
|
1766
2015
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1767
2016
|
try {
|
|
1768
2017
|
requireCloud(client);
|
|
2018
|
+
const name = nameArg ?? await pickBox(client);
|
|
2019
|
+
const subdomain = subArg ?? await text("Subdomain", { validate: (a) => subdomainProblem(a) ?? void 0 });
|
|
2020
|
+
const portText = opts.port ?? await text("Port inside the box", { default: "3000", validate: (a) => /^\d+$/.test(a) && +a >= 1 && +a <= 65535 ? void 0 : "a port is 1-65535" });
|
|
2021
|
+
const [port] = parsePorts([portText]);
|
|
1769
2022
|
const user = me(client);
|
|
1770
2023
|
const found = await findBox(client, name);
|
|
1771
2024
|
const box = displayName(found, user);
|
|
@@ -2405,130 +2658,6 @@ import { hostname as osHostname } from "os";
|
|
|
2405
2658
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2406
2659
|
import { Writable } from "stream";
|
|
2407
2660
|
import { Command as Command35 } from "commander";
|
|
2408
|
-
|
|
2409
|
-
// src/prompt.ts
|
|
2410
|
-
import { createInterface } from "readline/promises";
|
|
2411
|
-
function interactive() {
|
|
2412
|
-
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
2413
|
-
}
|
|
2414
|
-
var ESC = "\x1B";
|
|
2415
|
-
var ENTER = "\r";
|
|
2416
|
-
var CTRL_C = "";
|
|
2417
|
-
var CTRL_D = "";
|
|
2418
|
-
var cancelled = () => new KvboxError("cancelled", 130);
|
|
2419
|
-
function keysIn(chunk) {
|
|
2420
|
-
const keys = [];
|
|
2421
|
-
for (let i = 0; i < chunk.length; ) {
|
|
2422
|
-
if (chunk[i] === ESC && (chunk[i + 1] === "[" || chunk[i + 1] === "O") && i + 2 < chunk.length) {
|
|
2423
|
-
keys.push(chunk.slice(i, i + 3));
|
|
2424
|
-
i += 3;
|
|
2425
|
-
} else {
|
|
2426
|
-
keys.push(chunk[i]);
|
|
2427
|
-
i += 1;
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
return keys;
|
|
2431
|
-
}
|
|
2432
|
-
var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
|
|
2433
|
-
var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
|
|
2434
|
-
var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
2435
|
-
function rows(line, columns = process.stderr.columns ?? 80) {
|
|
2436
|
-
const width2 = line.replace(ANSI2, "").length;
|
|
2437
|
-
return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
|
|
2438
|
-
}
|
|
2439
|
-
function settled(message, answer) {
|
|
2440
|
-
return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
|
|
2441
|
-
}
|
|
2442
|
-
async function select(message, choices, initial = 0) {
|
|
2443
|
-
const out = process.stderr;
|
|
2444
|
-
const inp = process.stdin;
|
|
2445
|
-
let index = Math.min(Math.max(initial, 0), choices.length - 1);
|
|
2446
|
-
let drawn = 0;
|
|
2447
|
-
const render = () => {
|
|
2448
|
-
if (drawn > 0) out.write(`${ESC}[${drawn}A`);
|
|
2449
|
-
const lines = [
|
|
2450
|
-
`${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
|
|
2451
|
-
...choices.map((c, i) => {
|
|
2452
|
-
const hint = c.hint ? color.dim(` ${c.hint}`) : "";
|
|
2453
|
-
return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
|
|
2454
|
-
})
|
|
2455
|
-
];
|
|
2456
|
-
out.write(`\r${ESC}[J${lines.join("\n")}
|
|
2457
|
-
`);
|
|
2458
|
-
drawn = lines.reduce((n, l) => n + rows(l), 0);
|
|
2459
|
-
};
|
|
2460
|
-
inp.setRawMode(true);
|
|
2461
|
-
inp.resume();
|
|
2462
|
-
inp.setEncoding("utf8");
|
|
2463
|
-
render();
|
|
2464
|
-
let done = false;
|
|
2465
|
-
try {
|
|
2466
|
-
return await new Promise((resolve3, reject) => {
|
|
2467
|
-
const onData = (chunk) => {
|
|
2468
|
-
for (const key of keysIn(chunk)) {
|
|
2469
|
-
if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
|
|
2470
|
-
else if (isDown(key)) index = (index + 1) % choices.length;
|
|
2471
|
-
else if (key === ENTER || key === "\n") {
|
|
2472
|
-
inp.off("data", onData);
|
|
2473
|
-
done = true;
|
|
2474
|
-
resolve3(choices[index].value);
|
|
2475
|
-
return;
|
|
2476
|
-
} else if (key === CTRL_C || key === CTRL_D) {
|
|
2477
|
-
inp.off("data", onData);
|
|
2478
|
-
reject(cancelled());
|
|
2479
|
-
return;
|
|
2480
|
-
} else continue;
|
|
2481
|
-
render();
|
|
2482
|
-
}
|
|
2483
|
-
};
|
|
2484
|
-
inp.on("data", onData);
|
|
2485
|
-
});
|
|
2486
|
-
} finally {
|
|
2487
|
-
inp.setRawMode(false);
|
|
2488
|
-
inp.pause();
|
|
2489
|
-
out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
|
|
2490
|
-
` : ""}`);
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
async function text(message, opts = {}) {
|
|
2494
|
-
const out = process.stderr;
|
|
2495
|
-
for (; ; ) {
|
|
2496
|
-
const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
|
|
2497
|
-
const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
|
|
2498
|
-
const rl = createInterface({ input: process.stdin, output: out });
|
|
2499
|
-
const raw = (await askLine(rl, question)).trim();
|
|
2500
|
-
const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
|
|
2501
|
-
const problem = opts.validate?.(answer);
|
|
2502
|
-
out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
|
|
2503
|
-
if (problem === void 0) {
|
|
2504
|
-
out.write(`${settled(message, answer)}
|
|
2505
|
-
`);
|
|
2506
|
-
return answer;
|
|
2507
|
-
}
|
|
2508
|
-
out.write(`${color.yellow("!")} ${problem}
|
|
2509
|
-
`);
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
|
-
async function askLine(rl, question) {
|
|
2513
|
-
let onClose = () => {
|
|
2514
|
-
};
|
|
2515
|
-
const closed = new Promise((_, reject) => {
|
|
2516
|
-
onClose = () => reject(cancelled());
|
|
2517
|
-
rl.once("close", onClose);
|
|
2518
|
-
});
|
|
2519
|
-
try {
|
|
2520
|
-
return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
|
|
2521
|
-
} finally {
|
|
2522
|
-
rl.off("close", onClose);
|
|
2523
|
-
rl.close();
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
function abortIsCancel(err) {
|
|
2527
|
-
if (err.code === "ABORT_ERR") throw cancelled();
|
|
2528
|
-
throw err;
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
// src/commands/login.ts
|
|
2532
2661
|
var loginCommand = new Command35("login").description("log in to the kvbox cloud (or your own control plane) and target it from now on").argument("[url]", `control plane URL (default: ${DEFAULT_CLOUD_URL})`).option("-e, --email <email>", "account email (prompted otherwise)").option("--password-stdin", "read the password from the first line of stdin (for scripts)").action(async (urlArg, opts) => {
|
|
2533
2662
|
const url = normalizeUrl(urlArg ?? DEFAULT_CLOUD_URL);
|
|
2534
2663
|
const creds = await readCredentials(opts);
|
|
@@ -3145,7 +3274,8 @@ var nextjsTemplate = {
|
|
|
3145
3274
|
* The source lands in a staging directory and is installed and built there while the old app
|
|
3146
3275
|
* keeps serving; only then is it swapped into place and the service restarted, so a failed
|
|
3147
3276
|
* install or build leaves the running app alone. The unit is rewritten every time, so a changed
|
|
3148
|
-
* package manager or start script takes effect. /srv/app is the kvbox user's (image v0.8.0+).
|
|
3277
|
+
* package manager or start script takes effect. /srv/app is the kvbox user's (image v0.8.0+). The
|
|
3278
|
+
* build runs in the deploy's SSH session, so it sees the owner's variables (`kvbox env`) too.
|
|
3149
3279
|
*/
|
|
3150
3280
|
remote(built) {
|
|
3151
3281
|
const c = commands(built.pm);
|
|
@@ -3157,8 +3287,12 @@ var nextjsTemplate = {
|
|
|
3157
3287
|
"[Service]",
|
|
3158
3288
|
"User=kvbox",
|
|
3159
3289
|
`WorkingDirectory=${APP_DIR}`,
|
|
3160
|
-
|
|
3161
|
-
|
|
3290
|
+
// PORT on the command line: the proxied address points at it, and nothing in the environment
|
|
3291
|
+
// file below may move the app off it (env's PORT, say, copied from a .env).
|
|
3292
|
+
`ExecStart=/usr/bin/env PORT=${PORT} ${c.start}`,
|
|
3293
|
+
"Environment=NODE_ENV=production",
|
|
3294
|
+
// The box owner's variables (#88, base v0.10.0+; the `-` makes an older box start without them).
|
|
3295
|
+
"EnvironmentFile=-/etc/kvbox/env",
|
|
3162
3296
|
"Restart=always",
|
|
3163
3297
|
"RestartSec=2",
|
|
3164
3298
|
"",
|
|
@@ -3560,9 +3694,12 @@ function describeMount(machine, volume, box) {
|
|
|
3560
3694
|
const path = machine.volumes?.find((v) => volume === v.name || volume.endsWith(`/${v.name}`))?.mountPath ?? volumeMountPath(volume.slice(volume.lastIndexOf("/") + 1));
|
|
3561
3695
|
return machine.status === "running" ? ` the box was restarted; the volume is at ${path} inside it` : ` box ${box} is ${machine.status}; the volume is at ${path} once it runs`;
|
|
3562
3696
|
}
|
|
3563
|
-
var attachCommand = new Command51("attach").description("attach a volume to a box at /mnt/<volume> (a running box is restarted to take it)").argument("
|
|
3697
|
+
var attachCommand = new Command51("attach").description("attach a volume to a box at /mnt/<volume> (a running box is restarted to take it)").argument("[volume]", "volume name (picked from a list when left out)").argument("[box]", "box name (picked from a list when left out)").option("--json", "print the box as JSON").action(async (volumeArg, boxArg, opts, cmd) => {
|
|
3698
|
+
if (volumeArg === void 0 || boxArg === void 0) askOrUsage("give the volume and the box: kvbox volume attach <volume> <box>");
|
|
3564
3699
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
3565
3700
|
try {
|
|
3701
|
+
const volume = volumeArg ?? (await pickVolume(client, (v) => v.attachedTo === null, "no volume is free to attach (kvbox volume ls); make one with kvbox volume new")).ref;
|
|
3702
|
+
const box = boxArg ?? await pickBox(client);
|
|
3566
3703
|
const machine = await spin(`attaching volume ${volume} to box ${box} (a running box is restarted)\u2026`, client.attachVolume(volume, { box }));
|
|
3567
3704
|
if (opts.json) {
|
|
3568
3705
|
json(machine);
|
|
@@ -3584,10 +3721,26 @@ function parseGib2(text2) {
|
|
|
3584
3721
|
if (gib > MAX_VOLUME_GIB) throw new KvboxError(`--size ${text2}: a volume is at most ${MAX_VOLUME_GIB} GB`);
|
|
3585
3722
|
return gib;
|
|
3586
3723
|
}
|
|
3587
|
-
|
|
3588
|
-
|
|
3724
|
+
function sizeProblem(answer) {
|
|
3725
|
+
try {
|
|
3726
|
+
parseGib2(answer);
|
|
3727
|
+
return void 0;
|
|
3728
|
+
} catch (err) {
|
|
3729
|
+
return err instanceof KvboxError ? err.message.replace(/^--size \S+: /, "") : String(err);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
var createCommand = new Command52("create").alias("new").description("create a volume for a box: an empty disk of its own at /mnt/<name> inside it (a running box is restarted)").argument("[name]", "volume name (1-12 chars; the box mounts it at /mnt/<name>; asked for when left out)").option("--size <gb>", "size in GB, like 50 or 50GB (whole GB; it only occupies what is written)").option("--box <box>", "the box it is for: made on that box's host and attached to it (required on kvbox.sh; optional with --local)").option("--json", "print the volume as JSON").action(async (nameArg, opts, cmd) => {
|
|
3733
|
+
const ask = interactive();
|
|
3734
|
+
if (nameArg === void 0 || opts.size === void 0) askOrUsage("give the name and the size: kvbox volume create <name> --size <gb> --box <box>");
|
|
3735
|
+
const name = nameArg ?? await text("Volume name", { validate: (a) => VOLUME_NAME_RE.test(a) ? void 0 : VOLUME_NAME_RULE });
|
|
3736
|
+
const sizeGib = opts.size !== void 0 ? parseGib2(opts.size) : parseGib2(await text("Size in GB", { default: "10", validate: sizeProblem }));
|
|
3589
3737
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
3590
3738
|
try {
|
|
3739
|
+
if (opts.box === void 0 && ask) {
|
|
3740
|
+
const none = client.target.kind === "cloud" ? [] : [{ value: "", label: "none", hint: "attach it later with kvbox volume attach" }];
|
|
3741
|
+
const picked = await pickBox(client, "Box", none);
|
|
3742
|
+
if (picked !== "") opts.box = picked;
|
|
3743
|
+
}
|
|
3591
3744
|
if (opts.box === void 0 && client.target.kind === "cloud") {
|
|
3592
3745
|
throw new KvboxError(`a volume is made for a box, on that box's host: kvbox volume create ${name} --size ${sizeGib} --box <box>`);
|
|
3593
3746
|
}
|
|
@@ -3610,9 +3763,19 @@ var createCommand = new Command52("create").alias("new").description("create a v
|
|
|
3610
3763
|
|
|
3611
3764
|
// src/commands/volume/detach.ts
|
|
3612
3765
|
import { Command as Command53 } from "commander";
|
|
3613
|
-
var detachCommand = new Command53("detach").description("detach a volume from a box; its data is kept (a running box is restarted)").argument("
|
|
3766
|
+
var detachCommand = new Command53("detach").description("detach a volume from a box; its data is kept (a running box is restarted)").argument("[volume]", "volume name (picked from a list when left out)").argument("[box]", "box name (the one the volume is on when left out)").option("--json", "print the box as JSON").action(async (volumeArg, boxArg, opts, cmd) => {
|
|
3767
|
+
if (volumeArg === void 0) askOrUsage("give the volume: kvbox volume detach <volume> [box]");
|
|
3614
3768
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
3615
3769
|
try {
|
|
3770
|
+
let volume = volumeArg;
|
|
3771
|
+
let box = boxArg;
|
|
3772
|
+
if (volume === void 0 || box === void 0) {
|
|
3773
|
+
const found = volume === void 0 ? await pickVolume(client, (v) => v.attachedTo !== null, "no volume is attached to a box (kvbox volume ls)") : pickRef(await spin("finding the volume\u2026", client.listVolumes()), volume, me(client), "volume");
|
|
3774
|
+
if (!found) throw new KvboxError(`volume ${volume}: not found (kvbox volume ls)`);
|
|
3775
|
+
if (found.attachedTo === null) throw new KvboxError(`volume ${volume}: not attached to a box`);
|
|
3776
|
+
volume ??= refOf(found, me(client));
|
|
3777
|
+
box ??= refOf({ name: found.attachedTo, owner: found.owner }, me(client));
|
|
3778
|
+
}
|
|
3616
3779
|
const machine = await spin(`detaching volume ${volume} from box ${box} (a running box is restarted)\u2026`, client.detachVolume(volume, { box }));
|
|
3617
3780
|
if (opts.json) {
|
|
3618
3781
|
json(machine);
|
|
@@ -3677,12 +3840,128 @@ var rmCommand6 = new Command55("rm").alias("remove").description("delete a volum
|
|
|
3677
3840
|
var volumeCommand = new Command56("volume").aliases(["volumes", "vol"]).description("disks of their own: create one, attach it to a box at /mnt/<name>, move it to another box later").addCommand(createCommand).addCommand(lsCommand6).addCommand(attachCommand).addCommand(detachCommand).addCommand(rmCommand6);
|
|
3678
3841
|
for (const cmd of volumeCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal volumes)");
|
|
3679
3842
|
|
|
3843
|
+
// src/commands/env/index.ts
|
|
3844
|
+
import { Command as Command60 } from "commander";
|
|
3845
|
+
|
|
3846
|
+
// src/commands/env/ls.ts
|
|
3847
|
+
import { Command as Command57 } from "commander";
|
|
3848
|
+
|
|
3849
|
+
// src/commands/env/shared.ts
|
|
3850
|
+
function requireCloud2(client) {
|
|
3851
|
+
if (client.target.kind === "local") throw new KvboxError("kvbox env needs kvbox.sh: kvbox login first");
|
|
3852
|
+
return client.target.org ?? client.target.user ?? "you";
|
|
3853
|
+
}
|
|
3854
|
+
function reach(boxes, verb) {
|
|
3855
|
+
if (boxes === 0) return verb === "get" ? "no boxes yet, every new one gets it" : "no boxes yet";
|
|
3856
|
+
return boxes === 1 ? `1 box ${verb}s it when it next starts` : `${boxes} boxes ${verb} it when they next start`;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
// src/commands/env/ls.ts
|
|
3860
|
+
var lsCommand7 = new Command57("ls").alias("list").description("list the variables (a secret shows as (sealed))").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
3861
|
+
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
3862
|
+
try {
|
|
3863
|
+
const owner = requireCloud2(client);
|
|
3864
|
+
const vars = await client.listEnv();
|
|
3865
|
+
if (opts.json) {
|
|
3866
|
+
json(vars);
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
if (vars.length === 0) {
|
|
3870
|
+
info(`no variables for ${owner} yet; set one with: kvbox env set NAME value`);
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3873
|
+
table(vars.map((v) => ({ name: v.name, value: v.secret ? color.dim("(sealed)") : v.value, updated: age(v.updatedAt) })));
|
|
3874
|
+
} finally {
|
|
3875
|
+
await client.close();
|
|
3876
|
+
}
|
|
3877
|
+
});
|
|
3878
|
+
|
|
3879
|
+
// src/commands/env/rm.ts
|
|
3880
|
+
import { Command as Command58 } from "commander";
|
|
3881
|
+
var rmCommand7 = new Command58("rm").alias("remove").description("remove variables; boxes lose them when they next start").argument("<names...>", "variable names").option("-y, --yes", "do not ask for confirmation").option("--json", "print as JSON").action(async (names, opts, cmd) => {
|
|
3882
|
+
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
3883
|
+
try {
|
|
3884
|
+
const owner = requireCloud2(client);
|
|
3885
|
+
if (!await confirm(`Remove ${names.join(", ")} from ${owner}'s variables?`, opts.yes ?? false)) {
|
|
3886
|
+
info("cancelled");
|
|
3887
|
+
process.exitCode = 1;
|
|
3888
|
+
return;
|
|
3889
|
+
}
|
|
3890
|
+
const changes = [];
|
|
3891
|
+
const failed = [];
|
|
3892
|
+
for (const name of names) {
|
|
3893
|
+
try {
|
|
3894
|
+
const c = await spin(`removing ${name}\u2026`, client.removeEnv(name));
|
|
3895
|
+
changes.push(c);
|
|
3896
|
+
if (!opts.json) info(`${color.green("\u2713")} removed ${c.name} from ${owner}: ${reach(c.boxes, "lose")}`);
|
|
3897
|
+
} catch (err) {
|
|
3898
|
+
failed.push({ name, err });
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
if (changes.length === 0 && failed.length > 0) throw failed[0].err;
|
|
3902
|
+
if (opts.json) json(changes);
|
|
3903
|
+
for (const f of failed) warn(`${color.red("\u2717")} ${f.name}: ${f.err.message}`);
|
|
3904
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
3905
|
+
} finally {
|
|
3906
|
+
await client.close();
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3909
|
+
|
|
3910
|
+
// src/commands/env/set.ts
|
|
3911
|
+
import { Command as Command59 } from "commander";
|
|
3912
|
+
|
|
3913
|
+
// ../shared/src/env.ts
|
|
3914
|
+
var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3915
|
+
var ENV_RESERVED = ["PATH", "HOME", "USER", "SHELL"];
|
|
3916
|
+
var ENV_MAX_VALUE_BYTES = 4096;
|
|
3917
|
+
function envNameProblem(name) {
|
|
3918
|
+
if (!ENV_NAME_RE.test(name)) return `${JSON.stringify(name)} is not a variable name: letters, digits and _, not starting with a digit`;
|
|
3919
|
+
if (ENV_RESERVED.includes(name)) return `${name} is the box's own and cannot be set`;
|
|
3920
|
+
if (name.startsWith("KVBOX_")) return `${name}: names starting with KVBOX_ are kept for kvbox`;
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
function envValueProblem(value) {
|
|
3924
|
+
if (/[\n\r\0]/.test(value)) return "a value is one line: no line breaks (base64 a multi-line value such as a key file)";
|
|
3925
|
+
if (Buffer.byteLength(value, "utf8") > ENV_MAX_VALUE_BYTES) return `a value is at most ${ENV_MAX_VALUE_BYTES / 1024} KB`;
|
|
3926
|
+
return null;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
// src/commands/env/set.ts
|
|
3930
|
+
var setCommand2 = new Command59("set").description("add or change a variable; boxes get it when they next start").argument("[name]", "variable name, e.g. DATABASE_URL (asked for when left out)").argument("[value]", "its value (asked for when left out; hidden with --secret)").option("--secret", "write-only: the value is never shown again, by any command").option("--json", "print as JSON").action(async (nameArg, valueArg, opts, cmd) => {
|
|
3931
|
+
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
3932
|
+
try {
|
|
3933
|
+
const owner = requireCloud2(client);
|
|
3934
|
+
const ask = interactive();
|
|
3935
|
+
if ((nameArg === void 0 || valueArg === void 0) && !ask) throw new KvboxError("give the name and the value: kvbox env set NAME VALUE (or run it in a terminal to be asked)");
|
|
3936
|
+
const name = nameArg ?? await text("Variable name", { validate: (a) => envNameProblem(a) ?? void 0 });
|
|
3937
|
+
const nameProblem = envNameProblem(name);
|
|
3938
|
+
if (nameProblem !== null) throw new KvboxError(nameProblem);
|
|
3939
|
+
const value = valueArg ?? (opts.secret ? await hiddenText(`Value for ${name}`) : await text(`Value for ${name}`, { trim: false }));
|
|
3940
|
+
const valueProblem = envValueProblem(value);
|
|
3941
|
+
if (valueProblem !== null) throw new KvboxError(valueProblem);
|
|
3942
|
+
const change = await spin(`setting ${name}\u2026`, client.setEnv(name, { value, secret: opts.secret ?? false }));
|
|
3943
|
+
if (opts.json) {
|
|
3944
|
+
json(change);
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
const what = opts.secret ? `${name} (secret)` : name;
|
|
3948
|
+
info(`${color.green("\u2713")} set ${what} for ${owner}: ${reach(change.boxes, "get")}`);
|
|
3949
|
+
} finally {
|
|
3950
|
+
await client.close();
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
|
|
3954
|
+
// src/commands/env/index.ts
|
|
3955
|
+
var envCommand = new Command60("env").description("environment variables every box of yours gets when it starts (secrets too)").addCommand(lsCommand7, { isDefault: true }).addCommand(setCommand2).addCommand(rmCommand7);
|
|
3956
|
+
for (const cmd of envCommand.commands) cmd.option("--org <slug>", "the org's variables instead of yours");
|
|
3957
|
+
|
|
3680
3958
|
// src/program.ts
|
|
3681
3959
|
function buildProgram() {
|
|
3682
|
-
const program = new
|
|
3960
|
+
const program = new Command61("kvbox").description("self-hosted Firecracker microVMs with Docker-like ergonomics").version(VERSION, "-v, --version").option("--local", "use this machine's kvbox-server over its socket, ignoring kvbox login").option("--org <slug>", "the org: where kvbox new creates, and which box a name means when it is not one of yours; before the command here, or after it").showHelpAfterError().enablePositionalOptions();
|
|
3683
3961
|
program.addCommand(vmCommand);
|
|
3684
3962
|
program.addCommand(snapshotsCommand);
|
|
3685
3963
|
program.addCommand(volumeCommand);
|
|
3964
|
+
program.addCommand(envCommand);
|
|
3686
3965
|
program.addCommand(serverCommand);
|
|
3687
3966
|
program.addCommand(loginCommand);
|
|
3688
3967
|
program.addCommand(logoutCommand);
|
|
@@ -3695,9 +3974,90 @@ function buildProgram() {
|
|
|
3695
3974
|
return program;
|
|
3696
3975
|
}
|
|
3697
3976
|
|
|
3977
|
+
// src/update.ts
|
|
3978
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
3979
|
+
import { dirname } from "path";
|
|
3980
|
+
var REGISTRY_URL = "https://registry.npmjs.org/@kvbox/cli/latest";
|
|
3981
|
+
var CHECK_EVERY_MS = 24 * 60 * 60 * 1e3;
|
|
3982
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
3983
|
+
var UPDATE_COMMAND = "npm install -g @kvbox/cli";
|
|
3984
|
+
function isNewer(a, b) {
|
|
3985
|
+
const pa = parse(a);
|
|
3986
|
+
const pb = parse(b);
|
|
3987
|
+
if (!pa || !pb) return false;
|
|
3988
|
+
for (let i = 0; i < 3; i++) {
|
|
3989
|
+
if (pa[i] !== pb[i]) return pa[i] > pb[i];
|
|
3990
|
+
}
|
|
3991
|
+
return false;
|
|
3992
|
+
}
|
|
3993
|
+
function parse(v) {
|
|
3994
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
3995
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
3996
|
+
}
|
|
3997
|
+
function readUpdateState(file = paths().updateCheck) {
|
|
3998
|
+
try {
|
|
3999
|
+
const raw = JSON.parse(readFileSync3(file, "utf8"));
|
|
4000
|
+
if (typeof raw.checkedAt !== "number" || typeof raw.latest !== "string") return null;
|
|
4001
|
+
return { checkedAt: raw.checkedAt, latest: raw.latest };
|
|
4002
|
+
} catch {
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
function writeUpdateState(state, file = paths().updateCheck) {
|
|
4007
|
+
try {
|
|
4008
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
4009
|
+
writeFileSync(file, JSON.stringify(state) + "\n");
|
|
4010
|
+
} catch {
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
async function fetchLatest(fetchImpl = fetch) {
|
|
4014
|
+
try {
|
|
4015
|
+
const res = await fetchImpl(REGISTRY_URL, {
|
|
4016
|
+
headers: { accept: "application/json" },
|
|
4017
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
4018
|
+
});
|
|
4019
|
+
if (!res.ok) return null;
|
|
4020
|
+
const body = await res.json();
|
|
4021
|
+
return typeof body.version === "string" ? body.version : null;
|
|
4022
|
+
} catch {
|
|
4023
|
+
return null;
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
async function refreshUpdateState(opts = {}) {
|
|
4027
|
+
const file = opts.file ?? paths().updateCheck;
|
|
4028
|
+
const now = opts.now ?? Date.now();
|
|
4029
|
+
const cached = readUpdateState(file);
|
|
4030
|
+
if (cached && now - cached.checkedAt < CHECK_EVERY_MS) return cached;
|
|
4031
|
+
const latest = await fetchLatest(opts.fetchImpl);
|
|
4032
|
+
const state = { checkedAt: now, latest: latest ?? cached?.latest ?? VERSION };
|
|
4033
|
+
writeUpdateState(state, file);
|
|
4034
|
+
return state;
|
|
4035
|
+
}
|
|
4036
|
+
function updateNotice(latest, current = VERSION) {
|
|
4037
|
+
if (!isNewer(latest, current)) return null;
|
|
4038
|
+
return `
|
|
4039
|
+
A new version of kvbox is available (v${latest.replace(/^v/, "")}). Update with:
|
|
4040
|
+
${color.bold(UPDATE_COMMAND)}
|
|
4041
|
+
`;
|
|
4042
|
+
}
|
|
4043
|
+
function enabled() {
|
|
4044
|
+
return process.stderr.isTTY === true && !process.env["CI"] && !process.env["KVBOX_NO_UPDATE_CHECK"];
|
|
4045
|
+
}
|
|
4046
|
+
function startUpdateCheck() {
|
|
4047
|
+
if (!enabled()) return Promise.resolve();
|
|
4048
|
+
process.on("exit", () => {
|
|
4049
|
+
const state = readUpdateState();
|
|
4050
|
+
const notice = state ? updateNotice(state.latest) : null;
|
|
4051
|
+
if (notice) process.stderr.write(notice);
|
|
4052
|
+
});
|
|
4053
|
+
return refreshUpdateState();
|
|
4054
|
+
}
|
|
4055
|
+
|
|
3698
4056
|
// src/index.ts
|
|
4057
|
+
var updateCheck = startUpdateCheck();
|
|
3699
4058
|
try {
|
|
3700
4059
|
await buildProgram().parseAsync(process.argv);
|
|
4060
|
+
await updateCheck;
|
|
3701
4061
|
} catch (err) {
|
|
3702
4062
|
if (err instanceof KvboxError) {
|
|
3703
4063
|
process.stderr.write(color.red(`error: ${err.message}`) + "\n");
|