@kvbox/cli 0.5.20 → 0.5.21

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/kvbox.js CHANGED
@@ -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.20" : void 0);
167
+ var VERSION = versionOf(import.meta.url, true ? "0.5.21" : void 0);
168
168
 
169
169
  // src/commands/vm/index.ts
170
170
  import { Command as Command25 } from "commander";
@@ -370,12 +370,12 @@ import { z as z2 } from "zod";
370
370
 
371
371
  // ../shared/src/sizes.ts
372
372
  var SIZES = {
373
+ xs: { vcpu: 1, memMib: 2048, diskGib: 8 },
373
374
  s: { vcpu: 2, memMib: 4096, diskGib: 8 },
374
375
  m: { vcpu: 4, memMib: 8192, diskGib: 16 },
375
376
  l: { vcpu: 8, memMib: 16384, diskGib: 32 }
376
377
  };
377
378
  var SIZE_NAMES = Object.keys(SIZES);
378
- var DEFAULT_SIZE = "s";
379
379
  function parseMib(text2) {
380
380
  const m = /^\s*(\d+(?:\.\d+)?)\s*([gm]i?b?)?\s*$/i.exec(text2);
381
381
  if (!m) return void 0;
@@ -2891,14 +2891,12 @@ import { Command as Command49, Option as Option5 } from "commander";
2891
2891
 
2892
2892
  // src/commands/deploy/config.ts
2893
2893
  import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
2894
- import { join as join3 } from "path";
2894
+ import { join as join4 } from "path";
2895
2895
  import { z as z5 } from "zod";
2896
2896
 
2897
- // src/commands/deploy/templates/react.ts
2898
- import { spawn as spawn4 } from "child_process";
2897
+ // src/commands/deploy/templates/pm.ts
2899
2898
  import { access } from "fs/promises";
2900
2899
  import { join as join2 } from "path";
2901
- var OUT_DIRS = ["dist", "build"];
2902
2900
  async function packageManager(dir) {
2903
2901
  const has = (f) => access(join2(dir, f)).then(() => true, () => false);
2904
2902
  if (await has("pnpm-lock.yaml")) return "pnpm";
@@ -2906,8 +2904,100 @@ async function packageManager(dir) {
2906
2904
  if (await has("bun.lockb") || await has("bun.lock")) return "bun";
2907
2905
  return "npm";
2908
2906
  }
2907
+ function runScript(pm, script) {
2908
+ return pm === "yarn" ? [script] : ["run", script];
2909
+ }
2910
+
2911
+ // src/commands/deploy/templates/nextjs.ts
2912
+ var APP_DIR = "/srv/app/current";
2913
+ var SERVICE = "kvbox-app";
2914
+ var PORT = 3e3;
2915
+ function commands(pm) {
2916
+ switch (pm) {
2917
+ case "pnpm":
2918
+ return { setup: ["sudo -n corepack enable pnpm"], install: "pnpm install --frozen-lockfile", build: "pnpm run build", start: "pnpm run start" };
2919
+ case "yarn":
2920
+ return { setup: ["sudo -n corepack enable yarn"], install: "yarn install --frozen-lockfile", build: "yarn build", start: "yarn start" };
2921
+ case "bun":
2922
+ return { setup: [], install: "npm install", build: "npm run build", start: "npm run start" };
2923
+ case "npm":
2924
+ return { setup: [], install: "if [ -f package-lock.json ]; then npm ci; else npm install; fi", build: "npm run build", start: "npm run start" };
2925
+ }
2926
+ }
2927
+ var nextjsTemplate = {
2928
+ id: "nextjs",
2929
+ label: "Next.js",
2930
+ hint: "source shipped; installed, built and run in the box on port 3000",
2931
+ size: "s",
2932
+ port: PORT,
2933
+ detect(pkg) {
2934
+ if (!pkg) return false;
2935
+ return "next" in { ...pkg.dependencies, ...pkg.devDependencies };
2936
+ },
2937
+ /** No local build: the box does it. Only the scripts it will need are checked here. */
2938
+ async build(ctx) {
2939
+ for (const script of ["build", "start"]) {
2940
+ if (typeof ctx.pkg?.scripts?.[script] !== "string") throw new KvboxError(`${ctx.dir}: package.json has no "${script}" script; Next.js needs "build": "next build" and "start": "next start"`);
2941
+ }
2942
+ const pm = await packageManager(ctx.dir);
2943
+ return { outDir: ctx.dir, exclude: ["node_modules", ".next", ".git"], pm, what: "the source" };
2944
+ },
2945
+ /**
2946
+ * The source lands in a staging directory and is installed and built there while the old app
2947
+ * keeps serving; only then is it swapped into place and the service restarted, so a failed
2948
+ * install or build leaves the running app alone. The unit is rewritten every time, so a changed
2949
+ * package manager or start script takes effect. /srv/app is the kvbox user's (image v0.8.0+).
2950
+ */
2951
+ remote(built) {
2952
+ const c = commands(built.pm);
2953
+ const unit = [
2954
+ "[Unit]",
2955
+ "Description=kvbox deploy: the app in " + APP_DIR,
2956
+ "After=network.target",
2957
+ "",
2958
+ "[Service]",
2959
+ "User=kvbox",
2960
+ `WorkingDirectory=${APP_DIR}`,
2961
+ `ExecStart=/usr/bin/env ${c.start}`,
2962
+ `Environment=NODE_ENV=production PORT=${PORT}`,
2963
+ "Restart=always",
2964
+ "RestartSec=2",
2965
+ "",
2966
+ "[Install]",
2967
+ "WantedBy=multi-user.target"
2968
+ ];
2969
+ return [
2970
+ "set -e",
2971
+ "test -d /srv/app -a -w /srv/app || exit 3",
2972
+ "rm -rf /srv/app/.incoming /srv/app/.old",
2973
+ "mkdir -p /srv/app/.incoming",
2974
+ "tar -xf - -C /srv/app/.incoming",
2975
+ "cd /srv/app/.incoming",
2976
+ "export CI=1 NEXT_TELEMETRY_DISABLED=1 NODE_ENV=production",
2977
+ ...c.setup,
2978
+ c.install,
2979
+ c.build,
2980
+ "cd /",
2981
+ `sudo -n tee /etc/systemd/system/${SERVICE}.service >/dev/null <<'UNIT'`,
2982
+ ...unit,
2983
+ "UNIT",
2984
+ `if [ -e ${APP_DIR} ]; then mv ${APP_DIR} /srv/app/.old; fi`,
2985
+ `mv /srv/app/.incoming ${APP_DIR}`,
2986
+ "sudo -n systemctl daemon-reload",
2987
+ `sudo -n systemctl enable --quiet ${SERVICE}`,
2988
+ `sudo -n systemctl restart ${SERVICE}`,
2989
+ "rm -rf /srv/app/.old"
2990
+ ].join("\n");
2991
+ }
2992
+ };
2993
+
2994
+ // src/commands/deploy/templates/react.ts
2995
+ import { spawn as spawn4 } from "child_process";
2996
+ import { access as access2 } from "fs/promises";
2997
+ import { join as join3 } from "path";
2998
+ var OUT_DIRS = ["dist", "build"];
2909
2999
  async function runBuild(dir, pm) {
2910
- const args = pm === "yarn" ? ["build"] : ["run", "build"];
3000
+ const args = runScript(pm, "build");
2911
3001
  info(`${color.dim("$")} ${pm} ${args.join(" ")}`);
2912
3002
  const status2 = await new Promise((resolve3, reject) => {
2913
3003
  const child = spawn4(pm, args, { cwd: dir, stdio: ["ignore", process.stderr, process.stderr], shell: process.platform === "win32" });
@@ -2920,6 +3010,7 @@ var reactTemplate = {
2920
3010
  id: "react",
2921
3011
  label: "React",
2922
3012
  hint: "static site: built here, served by nginx in the box",
3013
+ size: "xs",
2923
3014
  port: 80,
2924
3015
  /** A Vite or Create React App project. A framework with its own server (Next.js, Remix, Expo) is not a static site. */
2925
3016
  detect(pkg) {
@@ -2930,11 +3021,12 @@ var reactTemplate = {
2930
3021
  },
2931
3022
  async build(ctx) {
2932
3023
  if (typeof ctx.pkg?.scripts?.["build"] !== "string") throw new KvboxError(`${ctx.dir}: package.json has no "build" script`);
2933
- await runBuild(ctx.dir, await packageManager(ctx.dir));
3024
+ const pm = await packageManager(ctx.dir);
3025
+ await runBuild(ctx.dir, pm);
2934
3026
  const candidates = ctx.dist !== void 0 ? [ctx.dist] : OUT_DIRS;
2935
3027
  for (const dist of candidates) {
2936
- const outDir = join2(ctx.dir, dist);
2937
- if (await access(join2(outDir, "index.html")).then(() => true, () => false)) return { outDir, dist };
3028
+ const outDir = join3(ctx.dir, dist);
3029
+ if (await access2(join3(outDir, "index.html")).then(() => true, () => false)) return { outDir, dist, exclude: [], pm, what: `${dist}/` };
2938
3030
  }
2939
3031
  throw new KvboxError(
2940
3032
  `the build left no ${candidates.join(" or ")}/index.html. The React template ships a directory of static files; set "dist" in kvbox.json to the build output directory, or pick the right template with --template`
@@ -2965,7 +3057,7 @@ var reactTemplate = {
2965
3057
  };
2966
3058
 
2967
3059
  // src/commands/deploy/templates/index.ts
2968
- var TEMPLATES = [reactTemplate];
3060
+ var TEMPLATES = [reactTemplate, nextjsTemplate];
2969
3061
  var TEMPLATE_IDS = TEMPLATES.map((t) => t.id);
2970
3062
  function templateById(id) {
2971
3063
  const t = TEMPLATES.find((x) => x.id === id);
@@ -2977,6 +3069,7 @@ function detectTemplate(pkg) {
2977
3069
  }
2978
3070
 
2979
3071
  // src/commands/deploy/config.ts
3072
+ var DEPLOY_AUTO_HIBERNATE_SEC = 3600;
2980
3073
  var CONFIG_FILE = "kvbox.json";
2981
3074
  var BOX_NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
2982
3075
  var DeployConfigSchema = z5.object({
@@ -2994,7 +3087,7 @@ var DeployConfigSchema = z5.object({
2994
3087
  dist: z5.string().optional()
2995
3088
  });
2996
3089
  async function readConfig(dir) {
2997
- const path = join3(dir, CONFIG_FILE);
3090
+ const path = join4(dir, CONFIG_FILE);
2998
3091
  let raw;
2999
3092
  try {
3000
3093
  raw = await readFile6(path, "utf8");
@@ -3016,7 +3109,7 @@ async function readConfig(dir) {
3016
3109
  return parsed.data;
3017
3110
  }
3018
3111
  async function writeConfig(dir, config) {
3019
- await writeFile4(join3(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
3112
+ await writeFile4(join4(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
3020
3113
  }
3021
3114
  function nameFromDir(dirName) {
3022
3115
  const slug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12).replace(/-+$/, "");
@@ -3029,21 +3122,27 @@ function subdomainFor(name) {
3029
3122
  // src/commands/deploy/ship.ts
3030
3123
  import { spawn as spawn5 } from "child_process";
3031
3124
  import { readdir, stat as stat2 } from "fs/promises";
3032
- import { join as join4 } from "path";
3125
+ import { join as join5 } from "path";
3033
3126
  var IMAGE_TOO_OLD = 3;
3034
3127
  var SSH_FAILED3 = 255;
3035
- async function measure(dir) {
3036
- let files = 0;
3037
- let bytes = 0;
3038
- for (const entry of await readdir(dir, { recursive: true, withFileTypes: true })) {
3039
- if (!entry.isFile()) continue;
3040
- files += 1;
3041
- bytes += (await stat2(join4(entry.parentPath, entry.name))).size;
3042
- }
3043
- return { files, bytes };
3044
- }
3045
- async function ship(ep, dir, remote) {
3046
- const tar = spawn5("tar", ["-cf", "-", "-C", dir, "."], { stdio: ["ignore", "pipe", "pipe"] });
3128
+ async function measure(dir, exclude = []) {
3129
+ const total = { files: 0, bytes: 0 };
3130
+ const walk = async (d) => {
3131
+ for (const entry of await readdir(d, { withFileTypes: true })) {
3132
+ if (exclude.includes(entry.name)) continue;
3133
+ const path = join5(d, entry.name);
3134
+ if (entry.isDirectory()) await walk(path);
3135
+ else if (entry.isFile()) {
3136
+ total.files += 1;
3137
+ total.bytes += (await stat2(path)).size;
3138
+ }
3139
+ }
3140
+ };
3141
+ await walk(dir);
3142
+ return total;
3143
+ }
3144
+ async function ship(ep, dir, remote, exclude = []) {
3145
+ const tar = spawn5("tar", ["-cf", "-", ...exclude.map((e) => `--exclude=${e}`), "-C", dir, "."], { stdio: ["ignore", "pipe", "pipe"] });
3047
3146
  const ssh = spawn5("ssh", [...sshArgs(ep), remote], { stdio: ["pipe", "pipe", "pipe"] });
3048
3147
  tar.stdout.pipe(ssh.stdin);
3049
3148
  const chunks = [];
@@ -3074,7 +3173,7 @@ async function ship(ep, dir, remote) {
3074
3173
  var shipping = { ship, measure, waitForSsh };
3075
3174
 
3076
3175
  // src/commands/deploy/index.ts
3077
- var deployCommand = new Command49("deploy").description("put the app in this directory on a box with its own HTTPS address (React for now); redeploy by running it again").argument("[dir]", "the app's directory (default: the current one)").addOption(new Option5("-t, --template <template>", "what kind of app it is (detected from package.json when not given)").choices(TEMPLATE_IDS)).option("-n, --name <name>", "the box's name (default: the directory's name)").option("--sub <subdomain>", "its HTTPS name, https://<subdomain>.kvbox.app (default: the box's name)").addOption(new Option5("-s, --size <size>", "size of the box when it has to be made").choices(SIZE_NAMES)).option("-y, --yes", "ask nothing: take the defaults for whatever the flags and kvbox.json leave open").option("--json", "print the result as JSON").option("--org <slug>", "deploy into this org's boxes (saved to kvbox.json)").action(async (dirArg, opts, cmd) => {
3176
+ var deployCommand = new Command49("deploy").description("put the app in this directory (React or Next.js) on a box with its own HTTPS address; redeploy by running it again").argument("[dir]", "the app's directory (default: the current one)").addOption(new Option5("-t, --template <template>", "what kind of app it is (detected from package.json when not given)").choices(TEMPLATE_IDS)).option("-n, --name <name>", "the box's name (default: the directory's name)").option("--sub <subdomain>", "its HTTPS name, https://<subdomain>.kvbox.app (default: the box's name)").addOption(new Option5("-s, --size <size>", "size of the box when it has to be made").choices(SIZE_NAMES)).option("-y, --yes", "ask nothing: take the defaults for whatever the flags and kvbox.json leave open").option("--json", "print the result as JSON").option("--org <slug>", "deploy into this org's boxes (saved to kvbox.json)").action(async (dirArg, opts, cmd) => {
3078
3177
  const dir = resolve2(dirArg ?? ".");
3079
3178
  const pkg = await readPackageJson(dir);
3080
3179
  const saved = await readConfig(dir);
@@ -3092,8 +3191,8 @@ var deployCommand = new Command49("deploy").description("put the app in this dir
3092
3191
  `);
3093
3192
  info(`${color.bold("Building")} ${color.cyan(config.name)} ${color.dim(`(${template.label})`)}`);
3094
3193
  const built = await template.build({ dir, pkg, dist: config.dist });
3095
- if (built.dist !== config.dist) await writeConfig(dir, { ...config, dist: built.dist });
3096
- const size = await shipping.measure(built.outDir);
3194
+ if (built.dist !== void 0 && built.dist !== config.dist) await writeConfig(dir, { ...config, dist: built.dist });
3195
+ const size = await shipping.measure(built.outDir, built.exclude);
3097
3196
  info("");
3098
3197
  const { box: found, ref, created } = await findOrCreate(client, config);
3099
3198
  const cert = await ensureCert(client, void 0, void 0, { owner: found.owner });
@@ -3101,17 +3200,18 @@ var deployCommand = new Command49("deploy").description("put the app in this dir
3101
3200
  const box = created || wasRunning ? found : await wakeUp(client, found, ref);
3102
3201
  const ep = sshEndpoint(box, client.target);
3103
3202
  if (!wasRunning) await spin(`waiting for ${ref} to accept SSH\u2026`, shipping.waitForSsh(ep));
3104
- const shipIt = () => shipping.ship(ep, built.outDir, template.remote());
3105
- let result = await spin(`shipping ${built.dist}/ (${size.files} files, ${gb(size.bytes)}) to ${ref}\u2026`, shipIt);
3203
+ const shipIt = () => shipping.ship(ep, built.outDir, template.remote(built), built.exclude);
3204
+ const doing = built.dist === void 0 ? `shipping ${built.what} (${size.files} files, ${gb(size.bytes)}) to ${ref} and building there, a minute or two\u2026` : `shipping ${built.what} (${size.files} files, ${gb(size.bytes)}) to ${ref}\u2026`;
3205
+ let result = await spin(doing, shipIt);
3106
3206
  if (result.status === SSH_FAILED3 && await renewAfterRefusal(client, cert, box.owner)) {
3107
3207
  info("renewed your SSH certificate, trying again\u2026");
3108
- result = await spin(`shipping ${built.dist}/ to ${ref}\u2026`, shipIt);
3208
+ result = await spin(doing, shipIt);
3109
3209
  }
3110
3210
  if (result.status === SSH_FAILED3) throw new KvboxError(`box ${ref}: could not reach it over SSH (still booting?)${result.output ? `
3111
3211
  ${result.output.trim()}` : ""}`, SSH_FAILED3);
3112
3212
  if (result.status === IMAGE_TOO_OLD) {
3113
3213
  throw new KvboxError(
3114
- `box ${ref} runs an image without nginx; ${template.label} needs base v0.8.0 or newer. Update the box (kvbox vm update ${ref}) once its host has the new image, or deploy under another name (--name) to get a fresh box`
3214
+ `box ${ref} runs an image too old for the ${template.label} template (base v0.8.0 or newer). Update the box (kvbox vm update ${ref}) once its host has the new image, or deploy under another name (--name) to get a fresh box`
3115
3215
  );
3116
3216
  }
3117
3217
  if (result.status !== 0) throw new KvboxError(`deploying to ${ref} failed (exit ${result.status}):
@@ -3120,10 +3220,10 @@ ${result.output.trim()}`);
3120
3220
  if (cloud) {
3121
3221
  const got = await address(client, box, ref, config, template);
3122
3222
  url = got.url;
3123
- if (got.subdomain !== config.subdomain) await writeConfig(dir, { ...config, subdomain: got.subdomain, ...built.dist !== config.dist ? { dist: built.dist } : {} });
3223
+ if (got.subdomain !== config.subdomain) await writeConfig(dir, { ...config, subdomain: got.subdomain, ...built.dist !== void 0 && built.dist !== config.dist ? { dist: built.dist } : {} });
3124
3224
  }
3125
3225
  if (opts.json) {
3126
- json({ box: ref, created, template: template.id, url, dist: built.dist, files: size.files, bytes: size.bytes });
3226
+ json({ box: ref, created, template: template.id, url, ...built.dist !== void 0 ? { dist: built.dist } : {}, files: size.files, bytes: size.bytes });
3127
3227
  return;
3128
3228
  }
3129
3229
  process.stdout.write(`${url}
@@ -3153,8 +3253,9 @@ async function settle(dir, pkg, saved, opts, orgFlag, cloud) {
3153
3253
  template: opts.template ?? saved?.template ?? detected?.id,
3154
3254
  name: opts.name ?? saved?.name ?? nameFromDir(basename(dir)),
3155
3255
  subdomain: opts.sub ?? savedSub,
3156
- size: opts.size ?? saved?.size ?? DEFAULT_SIZE
3256
+ size: opts.size ?? saved?.size
3157
3257
  };
3258
+ const sizeFor = (t) => defaults.size ?? t?.size ?? "xs";
3158
3259
  const open = {
3159
3260
  template: opts.template === void 0 && saved?.template === void 0,
3160
3261
  name: opts.name === void 0 && saved?.name === void 0,
@@ -3166,7 +3267,7 @@ async function settle(dir, pkg, saved, opts, orgFlag, cloud) {
3166
3267
  if (anyOpen && !opts.yes && !interactive()) {
3167
3268
  const sub = defaults.subdomain ?? subdomainFor(defaults.name);
3168
3269
  throw new KvboxError(
3169
- `kvbox deploy asks a few questions the first time, and this is not a terminal. Pass -y to take the defaults (${defaults.template ?? "template?"}, box ${defaults.name}${cloud ? `, https://${sub}.kvbox.app` : ""}, size ${defaults.size}), or answer with --template, --name${cloud ? ", --sub" : ""} and --size`
3270
+ `kvbox deploy asks a few questions the first time, and this is not a terminal. Pass -y to take the defaults (${defaults.template ?? "template?"}, box ${defaults.name}${cloud ? `, https://${sub}.kvbox.app` : ""}, size ${sizeFor(defaults.template !== void 0 ? templateById(defaults.template) : void 0)}), or answer with --template, --name${cloud ? ", --sub" : ""} and --size`
3170
3271
  );
3171
3272
  }
3172
3273
  const ask = anyOpen && !opts.yes;
@@ -3177,7 +3278,7 @@ async function settle(dir, pkg, saved, opts, orgFlag, cloud) {
3177
3278
  const initial = detected ? TEMPLATES.indexOf(detected) : 0;
3178
3279
  templateId = await select("Template", TEMPLATES.map((t) => ({ value: t.id, label: t.label, hint: t === detected ? `${t.hint} \xB7 detected` : t.hint })), initial);
3179
3280
  }
3180
- if (templateId === void 0) throw new KvboxError(`could not tell what kind of app ${dir} is (no package.json of a Vite or Create React App project): say it with --template ${TEMPLATE_IDS.join("|")}`);
3281
+ if (templateId === void 0) throw new KvboxError(`could not tell what kind of app ${dir} is (no package.json of a Vite, Create React App or Next.js project): say it with --template ${TEMPLATE_IDS.join("|")}`);
3181
3282
  const template = templateById(templateId);
3182
3283
  let name = defaults.name;
3183
3284
  if (open.name && ask) name = await text("Box name", { default: defaults.name, validate: (a) => BOX_NAME_RE.test(a) ? void 0 : NAME_RULE });
@@ -3192,9 +3293,9 @@ async function settle(dir, pkg, saved, opts, orgFlag, cloud) {
3192
3293
  });
3193
3294
  info(` ${color.dim(`https://${subdomain}.kvbox.app`)}`);
3194
3295
  }
3195
- let size = defaults.size;
3296
+ let size = sizeFor(template);
3196
3297
  if (open.size && ask) {
3197
- size = await select("Box size", SIZE_NAMES.map((s) => ({ value: s, label: s, hint: `${SIZES[s].vcpu} vCPU, ${SIZES[s].memMib / 1024} GB RAM, ${SIZES[s].diskGib} GB disk` })), SIZE_NAMES.indexOf(DEFAULT_SIZE));
3298
+ size = await select("Box size", SIZE_NAMES.map((s) => ({ value: s, label: s, hint: `${SIZES[s].vcpu} vCPU, ${SIZES[s].memMib / 1024} GB RAM, ${SIZES[s].diskGib} GB disk` })), SIZE_NAMES.indexOf(template.size));
3198
3299
  }
3199
3300
  const org = orgFlag ?? saved?.org;
3200
3301
  const config = {
@@ -3215,7 +3316,7 @@ async function findOrCreate(client, config) {
3215
3316
  let box = slug === void 0 ? named[0] : named.find((b) => slugOf(b.owner) === slug);
3216
3317
  let created = false;
3217
3318
  if (!box) {
3218
- box = await spin(`booting a new box ${config.name}\u2026`, client.createMachine({ name: config.name, ...config.size !== void 0 ? { size: config.size } : {} }));
3319
+ box = await spin(`booting a new box ${config.name}\u2026`, client.createMachine({ name: config.name, ...config.size !== void 0 ? { size: config.size } : {}, autoHibernateSec: DEPLOY_AUTO_HIBERNATE_SEC }));
3219
3320
  created = true;
3220
3321
  info(`${color.green("\u2713")} box ${color.bold(config.name)} is up: ${box.vcpu} vCPU, ${gb(box.memMib * 1024 ** 2)} RAM, ${gb(box.diskBytes)} disk`);
3221
3322
  }