@kvbox/cli 0.5.24 → 0.5.25
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 +207 -8
- package/dist/kvbox.js.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @kvbox/cli
|
|
2
|
+
|
|
3
|
+
The `kvbox` command: Firecracker microVMs with Docker-like ergonomics. Every box is a real VM with its own kernel, boots in seconds, and can be snapshotted warm and cloned.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm i -g @kvbox/cli
|
|
7
|
+
kvbox login # use kvbox.sh, or point it at your own host
|
|
8
|
+
kvbox new dev --size m # boot a box
|
|
9
|
+
kvbox connect dev # shell in over SSH
|
|
10
|
+
kvbox exec dev -- uname -a
|
|
11
|
+
kvbox snapshots new dev base # memory + disk, the box keeps running
|
|
12
|
+
kvbox new api --from base # a warm clone
|
|
13
|
+
kvbox volume create data --size 50GB --box dev # a disk that outlives the box, at /mnt/data
|
|
14
|
+
kvbox ls / kvbox rm dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This is a thin client, any OS. It talks to [kvbox.sh](https://kvbox.sh) or to a host of your own running [`@kvbox/server`](https://www.npmjs.com/package/@kvbox/server). Every listing command takes `--json`.
|
|
18
|
+
|
|
19
|
+
Docs: [kvbox.sh/docs](https://kvbox.sh/docs). Agent skill: [kvbox.sh/kvbox/SKILL.md](https://kvbox.sh/kvbox/SKILL.md).
|
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.25" : void 0);
|
|
168
168
|
|
|
169
169
|
// src/commands/vm/index.ts
|
|
170
170
|
import { Command as Command26 } from "commander";
|
|
@@ -262,6 +262,9 @@ function paths(home = kvboxHome()) {
|
|
|
262
262
|
disk: join(dir, "disk.ext4")
|
|
263
263
|
};
|
|
264
264
|
},
|
|
265
|
+
/** Each owner's environment variables as the control plane last sent them (#88): env/<owner>.json, mode 600. */
|
|
266
|
+
env: join(home, "env"),
|
|
267
|
+
ownerEnv: (owner) => join(home, "env", `${owner}.json`),
|
|
265
268
|
snapshot: (owner, name) => {
|
|
266
269
|
const dir = join(home, "snapshots", owner, name);
|
|
267
270
|
return {
|
|
@@ -505,6 +508,26 @@ var CreateVolumeRequest = z2.object({
|
|
|
505
508
|
var AttachVolumeRequest = z2.object({
|
|
506
509
|
box: z2.string()
|
|
507
510
|
});
|
|
511
|
+
var EnvVarSchema = z2.object({
|
|
512
|
+
name: z2.string(),
|
|
513
|
+
value: z2.string().optional(),
|
|
514
|
+
secret: z2.boolean(),
|
|
515
|
+
updatedAt: z2.string()
|
|
516
|
+
});
|
|
517
|
+
var SetEnvRequest = z2.object({
|
|
518
|
+
value: z2.string(),
|
|
519
|
+
secret: z2.boolean().optional()
|
|
520
|
+
});
|
|
521
|
+
var EnvChangeSchema = z2.object({
|
|
522
|
+
owner: z2.string(),
|
|
523
|
+
name: z2.string(),
|
|
524
|
+
boxes: z2.number().int()
|
|
525
|
+
});
|
|
526
|
+
var HostEnvRequest = z2.object({
|
|
527
|
+
version: z2.number().int().min(0),
|
|
528
|
+
vars: z2.array(z2.object({ name: z2.string(), value: z2.string() }))
|
|
529
|
+
});
|
|
530
|
+
var HostEnvVersionsSchema = z2.array(z2.object({ owner: z2.string(), version: z2.number().int() }));
|
|
508
531
|
var CreateMachineRequest = z2.object({
|
|
509
532
|
name: z2.string().optional(),
|
|
510
533
|
/** Ignored over the local socket (the caller is the host owner); the control plane sets it over HTTPS. */
|
|
@@ -1137,6 +1160,16 @@ var ServerClient = class {
|
|
|
1137
1160
|
deleteSnapshot(name) {
|
|
1138
1161
|
return this.call("DELETE", `/snapshots/${encodeURIComponent(name)}`, z3.undefined());
|
|
1139
1162
|
}
|
|
1163
|
+
// ---- environment variables (#88), a control plane only: the caller's slug, or `--org`'s ----
|
|
1164
|
+
listEnv() {
|
|
1165
|
+
return this.call("GET", "/env", z3.array(EnvVarSchema));
|
|
1166
|
+
}
|
|
1167
|
+
setEnv(name, req) {
|
|
1168
|
+
return this.call("PUT", `/env/${encodeURIComponent(name)}`, EnvChangeSchema, req);
|
|
1169
|
+
}
|
|
1170
|
+
removeEnv(name) {
|
|
1171
|
+
return this.call("DELETE", `/env/${encodeURIComponent(name)}`, EnvChangeSchema);
|
|
1172
|
+
}
|
|
1140
1173
|
// ---- volumes (#52) ----
|
|
1141
1174
|
listVolumes(opts = {}) {
|
|
1142
1175
|
return this.call("GET", `/volumes${allQuery(opts)}`, z3.array(VolumeSchema), void 0, opts.org);
|
|
@@ -2419,7 +2452,13 @@ var cancelled = () => new KvboxError("cancelled", 130);
|
|
|
2419
2452
|
function keysIn(chunk) {
|
|
2420
2453
|
const keys = [];
|
|
2421
2454
|
for (let i = 0; i < chunk.length; ) {
|
|
2422
|
-
if (chunk[i] === ESC &&
|
|
2455
|
+
if (chunk[i] === ESC && chunk[i + 1] === "[" && i + 2 < chunk.length) {
|
|
2456
|
+
let j = i + 2;
|
|
2457
|
+
while (j < chunk.length && /[0-9;?]/.test(chunk[j])) j += 1;
|
|
2458
|
+
const end = j < chunk.length ? j + 1 : j;
|
|
2459
|
+
keys.push(chunk.slice(i, end));
|
|
2460
|
+
i = end;
|
|
2461
|
+
} else if (chunk[i] === ESC && chunk[i + 1] === "O" && i + 2 < chunk.length) {
|
|
2423
2462
|
keys.push(chunk.slice(i, i + 3));
|
|
2424
2463
|
i += 3;
|
|
2425
2464
|
} else {
|
|
@@ -2496,7 +2535,8 @@ async function text(message, opts = {}) {
|
|
|
2496
2535
|
const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
|
|
2497
2536
|
const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
|
|
2498
2537
|
const rl = createInterface({ input: process.stdin, output: out });
|
|
2499
|
-
const
|
|
2538
|
+
const line = await askLine(rl, question);
|
|
2539
|
+
const raw = opts.trim === false ? line : line.trim();
|
|
2500
2540
|
const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
|
|
2501
2541
|
const problem = opts.validate?.(answer);
|
|
2502
2542
|
out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
|
|
@@ -2509,6 +2549,44 @@ async function text(message, opts = {}) {
|
|
|
2509
2549
|
`);
|
|
2510
2550
|
}
|
|
2511
2551
|
}
|
|
2552
|
+
async function hiddenText(message) {
|
|
2553
|
+
const out = process.stderr;
|
|
2554
|
+
const inp = process.stdin;
|
|
2555
|
+
const question = `${color.green("?")} ${color.bold(message)} ${color.dim("(hidden) \u203A")} `;
|
|
2556
|
+
out.write(question);
|
|
2557
|
+
inp.setRawMode(true);
|
|
2558
|
+
inp.resume();
|
|
2559
|
+
inp.setEncoding("utf8");
|
|
2560
|
+
let chars = [];
|
|
2561
|
+
let done = false;
|
|
2562
|
+
try {
|
|
2563
|
+
return await new Promise((resolve3, reject) => {
|
|
2564
|
+
const onData = (chunk) => {
|
|
2565
|
+
for (const key of keysIn(chunk)) {
|
|
2566
|
+
if (key === ENTER || key === "\n") {
|
|
2567
|
+
inp.off("data", onData);
|
|
2568
|
+
done = true;
|
|
2569
|
+
resolve3(chars.join(""));
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (key === CTRL_C || key === CTRL_D) {
|
|
2573
|
+
inp.off("data", onData);
|
|
2574
|
+
reject(cancelled());
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
if (key === "\x7F" || key === "\b") chars = chars.slice(0, -1);
|
|
2578
|
+
else if (key.length === 1 && (key >= " " || key === " ")) chars.push(key);
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2581
|
+
inp.on("data", onData);
|
|
2582
|
+
});
|
|
2583
|
+
} finally {
|
|
2584
|
+
inp.setRawMode(false);
|
|
2585
|
+
inp.pause();
|
|
2586
|
+
out.write(`\r${ESC}[J${done ? `${settled(message, "\u2022\u2022\u2022\u2022\u2022")}
|
|
2587
|
+
` : "\n"}`);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2512
2590
|
async function askLine(rl, question) {
|
|
2513
2591
|
let onClose = () => {
|
|
2514
2592
|
};
|
|
@@ -3145,7 +3223,8 @@ var nextjsTemplate = {
|
|
|
3145
3223
|
* The source lands in a staging directory and is installed and built there while the old app
|
|
3146
3224
|
* keeps serving; only then is it swapped into place and the service restarted, so a failed
|
|
3147
3225
|
* 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+).
|
|
3226
|
+
* package manager or start script takes effect. /srv/app is the kvbox user's (image v0.8.0+). The
|
|
3227
|
+
* build runs in the deploy's SSH session, so it sees the owner's variables (`kvbox env`) too.
|
|
3149
3228
|
*/
|
|
3150
3229
|
remote(built) {
|
|
3151
3230
|
const c = commands(built.pm);
|
|
@@ -3157,8 +3236,12 @@ var nextjsTemplate = {
|
|
|
3157
3236
|
"[Service]",
|
|
3158
3237
|
"User=kvbox",
|
|
3159
3238
|
`WorkingDirectory=${APP_DIR}`,
|
|
3160
|
-
|
|
3161
|
-
|
|
3239
|
+
// PORT on the command line: the proxied address points at it, and nothing in the environment
|
|
3240
|
+
// file below may move the app off it (env's PORT, say, copied from a .env).
|
|
3241
|
+
`ExecStart=/usr/bin/env PORT=${PORT} ${c.start}`,
|
|
3242
|
+
"Environment=NODE_ENV=production",
|
|
3243
|
+
// The box owner's variables (#88, base v0.10.0+; the `-` makes an older box start without them).
|
|
3244
|
+
"EnvironmentFile=-/etc/kvbox/env",
|
|
3162
3245
|
"Restart=always",
|
|
3163
3246
|
"RestartSec=2",
|
|
3164
3247
|
"",
|
|
@@ -3677,12 +3760,128 @@ var rmCommand6 = new Command55("rm").alias("remove").description("delete a volum
|
|
|
3677
3760
|
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
3761
|
for (const cmd of volumeCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal volumes)");
|
|
3679
3762
|
|
|
3763
|
+
// src/commands/env/index.ts
|
|
3764
|
+
import { Command as Command60 } from "commander";
|
|
3765
|
+
|
|
3766
|
+
// src/commands/env/ls.ts
|
|
3767
|
+
import { Command as Command57 } from "commander";
|
|
3768
|
+
|
|
3769
|
+
// src/commands/env/shared.ts
|
|
3770
|
+
function requireCloud2(client) {
|
|
3771
|
+
if (client.target.kind === "local") throw new KvboxError("kvbox env needs kvbox.sh: kvbox login first");
|
|
3772
|
+
return client.target.org ?? client.target.user ?? "you";
|
|
3773
|
+
}
|
|
3774
|
+
function reach(boxes, verb) {
|
|
3775
|
+
if (boxes === 0) return verb === "get" ? "no boxes yet, every new one gets it" : "no boxes yet";
|
|
3776
|
+
return boxes === 1 ? `1 box ${verb}s it when it next starts` : `${boxes} boxes ${verb} it when they next start`;
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
// src/commands/env/ls.ts
|
|
3780
|
+
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) => {
|
|
3781
|
+
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
3782
|
+
try {
|
|
3783
|
+
const owner = requireCloud2(client);
|
|
3784
|
+
const vars = await client.listEnv();
|
|
3785
|
+
if (opts.json) {
|
|
3786
|
+
json(vars);
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
if (vars.length === 0) {
|
|
3790
|
+
info(`no variables for ${owner} yet; set one with: kvbox env set NAME value`);
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
table(vars.map((v) => ({ name: v.name, value: v.secret ? color.dim("(sealed)") : v.value, updated: age(v.updatedAt) })));
|
|
3794
|
+
} finally {
|
|
3795
|
+
await client.close();
|
|
3796
|
+
}
|
|
3797
|
+
});
|
|
3798
|
+
|
|
3799
|
+
// src/commands/env/rm.ts
|
|
3800
|
+
import { Command as Command58 } from "commander";
|
|
3801
|
+
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) => {
|
|
3802
|
+
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
3803
|
+
try {
|
|
3804
|
+
const owner = requireCloud2(client);
|
|
3805
|
+
if (!await confirm(`Remove ${names.join(", ")} from ${owner}'s variables?`, opts.yes ?? false)) {
|
|
3806
|
+
info("cancelled");
|
|
3807
|
+
process.exitCode = 1;
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
const changes = [];
|
|
3811
|
+
const failed = [];
|
|
3812
|
+
for (const name of names) {
|
|
3813
|
+
try {
|
|
3814
|
+
const c = await spin(`removing ${name}\u2026`, client.removeEnv(name));
|
|
3815
|
+
changes.push(c);
|
|
3816
|
+
if (!opts.json) info(`${color.green("\u2713")} removed ${c.name} from ${owner}: ${reach(c.boxes, "lose")}`);
|
|
3817
|
+
} catch (err) {
|
|
3818
|
+
failed.push({ name, err });
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
if (changes.length === 0 && failed.length > 0) throw failed[0].err;
|
|
3822
|
+
if (opts.json) json(changes);
|
|
3823
|
+
for (const f of failed) warn(`${color.red("\u2717")} ${f.name}: ${f.err.message}`);
|
|
3824
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
3825
|
+
} finally {
|
|
3826
|
+
await client.close();
|
|
3827
|
+
}
|
|
3828
|
+
});
|
|
3829
|
+
|
|
3830
|
+
// src/commands/env/set.ts
|
|
3831
|
+
import { Command as Command59 } from "commander";
|
|
3832
|
+
|
|
3833
|
+
// ../shared/src/env.ts
|
|
3834
|
+
var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3835
|
+
var ENV_RESERVED = ["PATH", "HOME", "USER", "SHELL"];
|
|
3836
|
+
var ENV_MAX_VALUE_BYTES = 4096;
|
|
3837
|
+
function envNameProblem(name) {
|
|
3838
|
+
if (!ENV_NAME_RE.test(name)) return `${JSON.stringify(name)} is not a variable name: letters, digits and _, not starting with a digit`;
|
|
3839
|
+
if (ENV_RESERVED.includes(name)) return `${name} is the box's own and cannot be set`;
|
|
3840
|
+
if (name.startsWith("KVBOX_")) return `${name}: names starting with KVBOX_ are kept for kvbox`;
|
|
3841
|
+
return null;
|
|
3842
|
+
}
|
|
3843
|
+
function envValueProblem(value) {
|
|
3844
|
+
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)";
|
|
3845
|
+
if (Buffer.byteLength(value, "utf8") > ENV_MAX_VALUE_BYTES) return `a value is at most ${ENV_MAX_VALUE_BYTES / 1024} KB`;
|
|
3846
|
+
return null;
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
// src/commands/env/set.ts
|
|
3850
|
+
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) => {
|
|
3851
|
+
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
3852
|
+
try {
|
|
3853
|
+
const owner = requireCloud2(client);
|
|
3854
|
+
const ask = interactive();
|
|
3855
|
+
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)");
|
|
3856
|
+
const name = nameArg ?? await text("Variable name", { validate: (a) => envNameProblem(a) ?? void 0 });
|
|
3857
|
+
const nameProblem = envNameProblem(name);
|
|
3858
|
+
if (nameProblem !== null) throw new KvboxError(nameProblem);
|
|
3859
|
+
const value = valueArg ?? (opts.secret ? await hiddenText(`Value for ${name}`) : await text(`Value for ${name}`, { trim: false }));
|
|
3860
|
+
const valueProblem = envValueProblem(value);
|
|
3861
|
+
if (valueProblem !== null) throw new KvboxError(valueProblem);
|
|
3862
|
+
const change = await spin(`setting ${name}\u2026`, client.setEnv(name, { value, secret: opts.secret ?? false }));
|
|
3863
|
+
if (opts.json) {
|
|
3864
|
+
json(change);
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
const what = opts.secret ? `${name} (secret)` : name;
|
|
3868
|
+
info(`${color.green("\u2713")} set ${what} for ${owner}: ${reach(change.boxes, "get")}`);
|
|
3869
|
+
} finally {
|
|
3870
|
+
await client.close();
|
|
3871
|
+
}
|
|
3872
|
+
});
|
|
3873
|
+
|
|
3874
|
+
// src/commands/env/index.ts
|
|
3875
|
+
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);
|
|
3876
|
+
for (const cmd of envCommand.commands) cmd.option("--org <slug>", "the org's variables instead of yours");
|
|
3877
|
+
|
|
3680
3878
|
// src/program.ts
|
|
3681
3879
|
function buildProgram() {
|
|
3682
|
-
const program = new
|
|
3880
|
+
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
3881
|
program.addCommand(vmCommand);
|
|
3684
3882
|
program.addCommand(snapshotsCommand);
|
|
3685
3883
|
program.addCommand(volumeCommand);
|
|
3884
|
+
program.addCommand(envCommand);
|
|
3686
3885
|
program.addCommand(serverCommand);
|
|
3687
3886
|
program.addCommand(loginCommand);
|
|
3688
3887
|
program.addCommand(logoutCommand);
|