@easybits.cloud/cli 0.13.0 → 0.14.0

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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +161 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -50,6 +50,8 @@ easybits sandboxes destroy sb_abc123 --yes
50
50
 
51
51
  # Hosting: permanent machines (alias: deploy)
52
52
  easybits machines ls
53
+ easybits machines launch --repo https://github.com/you/shop.git --tier micro --domain shop.example.com
54
+ easybits machines launch --machine shop --archive ./build.tgz --prebuilt -m v2 # redeploy
53
55
  easybits deploy sb_abc123 -m "v1.2" # = machines deploy
54
56
  easybits machines releases sb_abc123 --limit 5
55
57
  easybits machines logs sb_abc123 --lines 100 --grep ERROR
@@ -164,6 +166,14 @@ Host *.ghosty
164
166
 
165
167
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
166
168
 
169
+ ## Changes in 0.14.0
170
+
171
+ - `machines launch` (aliases `create`, `new`): create a permanent machine and put an app on it in
172
+ one step — the CLI side of `launch_app`. Source is `--repo`, `--archive <url|file>` (a local file
173
+ is uploaded first) or `--machine` (redeploy onto an existing one). `--tier`, `--prebuilt`,
174
+ `--start`, `--port`, `--env K=V`, `--secret NAME`, `--data <path>`, `--domain`.
175
+ Without a plan it prints the checkout link instead of failing.
176
+
167
177
  ## Changes in 0.13.0
168
178
 
169
179
  - Catalog photos: `db photos put <db> --table T --key-column sku --dir photos/ [--column C]
package/dist/index.js CHANGED
@@ -1378,6 +1378,8 @@ var sandboxes = {
1378
1378
  };
1379
1379
 
1380
1380
  // src/commands/machines.ts
1381
+ import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
1382
+ import { basename as basename2 } from "path";
1381
1383
  var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine> [--dotenv <path> | NAME]";
1382
1384
  var machines = {
1383
1385
  name: "machines",
@@ -1405,6 +1407,103 @@ var machines = {
1405
1407
  );
1406
1408
  }
1407
1409
  },
1410
+ launch: {
1411
+ aliases: ["create", "new"],
1412
+ summary: "Create a machine and put an app on it in one step (or redeploy onto an existing one)",
1413
+ usage: "easybits machines launch (--repo <url> | --archive <url|file> | --machine <machine>) [--tier micro] [--name <name>] [--prebuilt] [--start <cmd>] [--port 3000] [--env K=V]... [--secret NAME]... [--data <path>]... [--domain <host>]",
1414
+ options: {
1415
+ repo: { type: "string", value: "url", description: "Git repo to clone (clean URL; a token goes in --repo-token)" },
1416
+ branch: { type: "string", value: "name", description: "Branch to clone" },
1417
+ "repo-token": { type: "string", value: "$secret:NAME", description: "Token for a private repo; best as $secret:NAME from your vault" },
1418
+ archive: { type: "string", value: "url|file", description: "A .tar.gz/.zip of the app: URL, or a local file that gets uploaded first" },
1419
+ machine: { type: "string", value: "machine", description: "Deploy onto this existing machine instead of creating one" },
1420
+ tier: { type: "string", value: "tier", description: "Size for a new machine: nano, micro (default), estandar, focus, performance\u2026" },
1421
+ name: { type: "string", value: "name", description: "Human label" },
1422
+ "app-dir": { type: "string", value: "dir", description: "Where the app lives in the machine (default /app)" },
1423
+ build: { type: "string", value: "cmd", description: "Build command (default: npm ci && npm run build)" },
1424
+ start: { type: "string", value: "cmd", description: "Start command (default: npm start)" },
1425
+ port: { type: "string", value: "port", description: "Port the app listens on (default 3000)" },
1426
+ prebuilt: { type: "boolean", description: "The code is already built: skip the build, just start it" },
1427
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" },
1428
+ secret: { type: "string", multiple: true, value: "NAME", description: "Vault secret the app needs, by name (repeatable)" },
1429
+ data: { type: "string", multiple: true, value: "path", description: "Path the nightly backup copies (repeatable). Without it nothing is backed up" },
1430
+ domain: { type: "string", value: "host", description: "Domain to attach; the output says which DNS record to create" },
1431
+ message: { type: "string", short: "m", value: "text", description: "Release note" }
1432
+ },
1433
+ examples: [
1434
+ "easybits machines launch --repo https://github.com/you/shop.git --tier micro --domain shop.example.com",
1435
+ "easybits machines launch --archive ./build.tgz --prebuilt --tier nano --name shop --start 'npm start'",
1436
+ "easybits machines launch --machine shop --archive ./build.tgz --prebuilt -m v2 # redeploy"
1437
+ ],
1438
+ async run(ctx) {
1439
+ const repo = str(ctx, "repo");
1440
+ let archiveUrl = str(ctx, "archive");
1441
+ const sandboxId = str(ctx, "machine");
1442
+ if (!repo && !archiveUrl && !sandboxId) {
1443
+ throw usageError(t("Give a source: --repo, --archive or --machine.", "Pasa una fuente: --repo, --archive o --machine."), this.usage);
1444
+ }
1445
+ if (repo && archiveUrl) throw usageError(t("--repo and --archive are exclusive.", "--repo y --archive se excluyen."), this.usage);
1446
+ if (sandboxId && str(ctx, "tier")) {
1447
+ throw usageError(t("--tier is only for a new machine (resize = redeploy_machine).", "--tier s\xF3lo aplica a una m\xE1quina nueva (cambiar tama\xF1o = redeploy_machine)."), this.usage);
1448
+ }
1449
+ const eb = await getClient(ctx);
1450
+ if (archiveUrl && !/^https?:\/\//.test(archiveUrl)) {
1451
+ if (!existsSync6(archiveUrl)) throw usageError(t(`File not found: ${archiveUrl}`, `No existe el archivo: ${archiveUrl}`), this.usage);
1452
+ const size = statSync2(archiveUrl).size;
1453
+ const contentType = archiveUrl.endsWith(".zip") ? "application/zip" : "application/gzip";
1454
+ const up = await eb.uploadFile({ fileName: basename2(archiveUrl), contentType, size, access: "public" });
1455
+ const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync6(archiveUrl), headers: { "Content-Type": contentType } });
1456
+ if (!res.ok) throw new CliError(t(`Upload failed: storage answered ${res.status}`, `Fall\xF3 la subida: el almacenamiento contest\xF3 ${res.status}`), 1, void 0, "upload_failed", res.status);
1457
+ archiveUrl = up.file.url;
1458
+ if (!archiveUrl) throw new CliError(t("The upload returned no public URL.", "La subida no devolvi\xF3 una URL p\xFAblica."), 1, void 0, "upload_failed");
1459
+ }
1460
+ const env = pairs(list(ctx, "env"), this.usage);
1461
+ const secretNames = list(ctx, "secret");
1462
+ const dataPaths = list(ctx, "data");
1463
+ const r = await eb.machines.launch({
1464
+ repo,
1465
+ branch: str(ctx, "branch"),
1466
+ repoToken: str(ctx, "repo-token"),
1467
+ archiveUrl,
1468
+ sandboxId,
1469
+ tier: str(ctx, "tier"),
1470
+ name: str(ctx, "name"),
1471
+ appDir: str(ctx, "app-dir"),
1472
+ buildCommand: str(ctx, "build"),
1473
+ startCommand: str(ctx, "start"),
1474
+ port: int(ctx, "port", this.usage),
1475
+ prebuilt: bool(ctx, "prebuilt") || void 0,
1476
+ env: Object.keys(env).length ? env : void 0,
1477
+ secretNames: secretNames.length ? secretNames : void 0,
1478
+ dataPaths: dataPaths.length ? dataPaths : void 0,
1479
+ domain: str(ctx, "domain"),
1480
+ message: str(ctx, "message")
1481
+ });
1482
+ if (r.checkoutUrl) {
1483
+ emit(ctx, r, () => {
1484
+ console.log(t("Your account has no plan: pay the machine here and it is created by itself:", "Tu cuenta no tiene plan: paga la m\xE1quina aqu\xED y se crea sola:"));
1485
+ console.log(` ${r.checkoutUrl}`);
1486
+ console.log(t("Then deploy onto it: easybits machines launch --machine <id> \u2026", "Luego despliega encima: easybits machines launch --machine <id> \u2026"));
1487
+ });
1488
+ return;
1489
+ }
1490
+ if (r.exitCode !== 0) {
1491
+ throw new CliError(
1492
+ t(`Launch failed (exit ${r.exitCode}) on ${r.sandboxId}`, `Fall\xF3 el lanzamiento (exit ${r.exitCode}) en ${r.sandboxId}`),
1493
+ 1,
1494
+ r.buildOutput?.slice(-2e3),
1495
+ "launch_failed"
1496
+ );
1497
+ }
1498
+ emit(ctx, r, () => {
1499
+ console.log(`${r.sandboxId} v${r.version} ${r.url}`);
1500
+ if (r.domain) {
1501
+ console.log(`${t("Domain", "Dominio")}: ${r.domain.domain} \u2192 ${r.domain.url}`);
1502
+ console.log(`DNS: ${JSON.stringify(r.domain.dns)}`);
1503
+ }
1504
+ });
1505
+ }
1506
+ },
1408
1507
  deploy: {
1409
1508
  summary: "Publish the machine's current app code as a new release",
1410
1509
  usage: "easybits machines deploy <machine> [--message <text>]",
@@ -1576,11 +1675,11 @@ var domains = {
1576
1675
  };
1577
1676
 
1578
1677
  // src/commands/db.ts
1579
- import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
1678
+ import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
1580
1679
 
1581
1680
  // src/photos.ts
1582
1681
  import { createHash as createHash2 } from "crypto";
1583
- import { readFileSync as readFileSync6, readdirSync, statSync as statSync2 } from "fs";
1682
+ import { readFileSync as readFileSync7, readdirSync, statSync as statSync3 } from "fs";
1584
1683
  import { extname as extname2, join as join3 } from "path";
1585
1684
  var PHOTO_MAX_BYTES = 10 * 1024 * 1024;
1586
1685
  var WHATSAPP_MAX_BYTES = 5 * 1024 * 1024;
@@ -1600,7 +1699,7 @@ function photoFiles(dir) {
1600
1699
  for (const name of readdirSync(dir).sort()) {
1601
1700
  if (name.startsWith(".")) continue;
1602
1701
  const path = join3(dir, name);
1603
- const st = statSync2(path);
1702
+ const st = statSync3(path);
1604
1703
  if (!st.isFile()) continue;
1605
1704
  if (st.size === 0) {
1606
1705
  rejected.push({ file: name, reason: "empty" });
@@ -1610,7 +1709,7 @@ function photoFiles(dir) {
1610
1709
  rejected.push({ file: name, reason: "too_large" });
1611
1710
  continue;
1612
1711
  }
1613
- const bytes = readFileSync6(path);
1712
+ const bytes = readFileSync7(path);
1614
1713
  const type = imageTypeOf(bytes);
1615
1714
  if (!type) {
1616
1715
  rejected.push({ file: name, reason: "not_an_image" });
@@ -1901,7 +2000,7 @@ var db = {
1901
2000
  const keyCol = str(ctx, "key-column");
1902
2001
  const dir = str(ctx, "dir");
1903
2002
  if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
1904
- if (!existsSync6(dir) || !statSync3(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
2003
+ if (!existsSync7(dir) || !statSync4(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
1905
2004
  const { photos, rejected } = photoFiles(dir);
1906
2005
  const eb = await getClient(ctx);
1907
2006
  const hit = await findDb(ctx, ref);
@@ -1948,7 +2047,7 @@ var db = {
1948
2047
  let url = urls.get(photo.sha);
1949
2048
  if (!url) {
1950
2049
  const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
1951
- const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync7(photo.path), headers: { "Content-Type": photo.mime } });
2050
+ const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync8(photo.path), headers: { "Content-Type": photo.mime } });
1952
2051
  if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
1953
2052
  url = up.file.url;
1954
2053
  if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
@@ -2010,7 +2109,7 @@ var db = {
2010
2109
  };
2011
2110
 
2012
2111
  // src/commands/agents.ts
2013
- import { existsSync as existsSync10, readFileSync as readFileSync11, statSync as statSync6 } from "fs";
2112
+ import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
2014
2113
 
2015
2114
  // src/api.ts
2016
2115
  import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
@@ -2040,8 +2139,8 @@ async function api(ctx, method, path, body, opts = {}) {
2040
2139
  var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
2041
2140
 
2042
2141
  // src/commands/agents-config.ts
2043
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
2044
- import { basename as basename2, dirname, join as join4, relative, resolve } from "path";
2142
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync5, writeFileSync as writeFileSync4 } from "fs";
2143
+ import { basename as basename3, dirname, join as join4, relative, resolve } from "path";
2045
2144
  var WORK_DIR = "/data/work";
2046
2145
  var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
2047
2146
  var SKILLS_DIR = "/data/agent/skills";
@@ -2120,8 +2219,8 @@ function readLocalText(path, usage2) {
2120
2219
  if (process.stdin.isTTY) throw usageError(t("Expected the content on stdin.", "Se esperaba el contenido por stdin."), usage2);
2121
2220
  return readStdin().then((b) => b.toString("utf8"));
2122
2221
  }
2123
- if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2124
- return readFileSync8(path, "utf8");
2222
+ if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2223
+ return readFileSync9(path, "utf8");
2125
2224
  }
2126
2225
  function parseMcpFile(text, usage2) {
2127
2226
  let j;
@@ -2336,8 +2435,8 @@ var files2 = {
2336
2435
  if (!rest.length) throw usageError(t("Give one or more local files.", "Pasa uno o m\xE1s archivos locales."), FILES_USAGE2);
2337
2436
  const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
2338
2437
  const plan = rest.map((local) => {
2339
- if (!existsSync7(local) || !statSync4(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
2340
- return { local, path: to ? `${to}/${basename2(local)}` : basename2(local), bytes: statSync4(local).size };
2438
+ if (!existsSync8(local) || !statSync5(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
2439
+ return { local, path: to ? `${to}/${basename3(local)}` : basename3(local), bytes: statSync5(local).size };
2341
2440
  });
2342
2441
  if (bool(ctx, "dry-run")) {
2343
2442
  emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
@@ -2348,7 +2447,7 @@ var files2 = {
2348
2447
  }
2349
2448
  const done = [];
2350
2449
  for (const p of plan) {
2351
- const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync8(p.local), { raw: true });
2450
+ const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync9(p.local), { raw: true });
2352
2451
  done.push({ local: p.local, ...r });
2353
2452
  if (!ctx.json) console.log(`${t("Uploaded", "Subido")} ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2354
2453
  }
@@ -2378,7 +2477,7 @@ function readSkillDir(dir) {
2378
2477
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2379
2478
  const p = join4(d, e.name);
2380
2479
  if (e.isDirectory()) visit(p);
2381
- else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync8(p).toString("base64") });
2480
+ else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync9(p).toString("base64") });
2382
2481
  }
2383
2482
  };
2384
2483
  visit(dir);
@@ -2442,11 +2541,11 @@ var skills = {
2442
2541
  const dir = str(ctx, "dir");
2443
2542
  if (!!file === !!dir) throw usageError(t("Pass --file SKILL.md or --dir <folder>.", "Pasa --file SKILL.md o --dir <carpeta>."), SKILLS_USAGE);
2444
2543
  const mdPath = file ?? join4(dir, "SKILL.md");
2445
- if (!existsSync7(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2446
- const markdown = readFileSync8(mdPath, "utf8");
2544
+ if (!existsSync8(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2545
+ const markdown = readFileSync9(mdPath, "utf8");
2447
2546
  const fm = skillFrontmatter(markdown);
2448
2547
  if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
2449
- const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename2(resolve(dir ?? dirname(mdPath))).toLowerCase());
2548
+ const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename3(resolve(dir ?? dirname(mdPath))).toLowerCase());
2450
2549
  if (!SLUG_RE.test(slug)) throw usageError(t(`Invalid slug "${slug}" ([a-z0-9-], max 64). Pass --slug.`, `Slug inv\xE1lido "${slug}" ([a-z0-9-], m\xE1x 64). Pasa --slug.`), SKILLS_USAGE);
2451
2550
  const assets = dir ? readSkillDir(dir) : [];
2452
2551
  const plan = { agentId: id, slug, files: ["SKILL.md", ...assets.map((a) => a.name)], restart: bool(ctx, "restart") };
@@ -2703,10 +2802,10 @@ var exportAgent = {
2703
2802
  }
2704
2803
  };
2705
2804
  function readExport(path, usage2) {
2706
- if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2805
+ if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2707
2806
  let x;
2708
2807
  try {
2709
- x = JSON.parse(readFileSync8(path, "utf8"));
2808
+ x = JSON.parse(readFileSync9(path, "utf8"));
2710
2809
  } catch (e) {
2711
2810
  throw usageError(t(`${path} is not JSON: ${e.message}`, `${path} no es JSON: ${e.message}`), usage2);
2712
2811
  }
@@ -2751,12 +2850,12 @@ async function applyExport(ctx, id, x, opts) {
2751
2850
  }
2752
2851
 
2753
2852
  // src/commands/spec.ts
2754
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
2853
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
2755
2854
  import { homedir as homedir3 } from "os";
2756
2855
  import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
2757
2856
 
2758
2857
  // src/specfile.ts
2759
- import { existsSync as existsSync8, readFileSync as readFileSync9, readdirSync as readdirSync3 } from "fs";
2858
+ import { existsSync as existsSync9, readFileSync as readFileSync10, readdirSync as readdirSync3 } from "fs";
2760
2859
  import { join as join5, relative as relative2 } from "path";
2761
2860
  function walkLocal(root) {
2762
2861
  const out = [];
@@ -2768,20 +2867,20 @@ function walkLocal(root) {
2768
2867
  else if (e.isFile()) out.push(relative2(root, p).split("\\").join("/"));
2769
2868
  }
2770
2869
  };
2771
- if (existsSync8(root)) visit(root);
2870
+ if (existsSync9(root)) visit(root);
2772
2871
  return out;
2773
2872
  }
2774
2873
  function readUploads(dir) {
2775
2874
  const skills2 = {};
2776
2875
  const skillsRoot = join5(dir, "skills");
2777
- if (existsSync8(skillsRoot)) {
2876
+ if (existsSync9(skillsRoot)) {
2778
2877
  for (const e of readdirSync3(skillsRoot, { withFileTypes: true })) {
2779
2878
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
2780
- skills2[e.name] = walkLocal(join5(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync9(join5(skillsRoot, e.name, path)).toString("base64") }));
2879
+ skills2[e.name] = walkLocal(join5(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync10(join5(skillsRoot, e.name, path)).toString("base64") }));
2781
2880
  }
2782
2881
  }
2783
2882
  const filesRoot = join5(dir, "files");
2784
- const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync9(join5(filesRoot, p)).toString("base64")]));
2883
+ const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync10(join5(filesRoot, p)).toString("base64")]));
2785
2884
  return { ...Object.keys(skills2).length ? { skills: skills2 } : {}, ...Object.keys(files3).length ? { files: files3 } : {} };
2786
2885
  }
2787
2886
  function secretsFor(text, env = process.env) {
@@ -2857,13 +2956,13 @@ ${t("Edit it, then", "Ed\xEDtalo y luego")}: easybits apply ${out} --dry-run`
2857
2956
  }
2858
2957
  };
2859
2958
  function readTarget(target, usage2) {
2860
- if (!existsSync9(target)) throw usageError(t(`Not found: ${target}`, `No existe: ${target}`), usage2);
2861
- if (statSync5(target).isDirectory()) {
2862
- const name = SPEC_NAMES.find((n) => existsSync9(join6(target, n)));
2959
+ if (!existsSync10(target)) throw usageError(t(`Not found: ${target}`, `No existe: ${target}`), usage2);
2960
+ if (statSync6(target).isDirectory()) {
2961
+ const name = SPEC_NAMES.find((n) => existsSync10(join6(target, n)));
2863
2962
  if (!name) throw usageError(t(`${target} has no agent.yaml.`, `${target} no tiene agent.yaml.`), usage2);
2864
- return { text: readFileSync10(join6(target, name), "utf8"), dir: target };
2963
+ return { text: readFileSync11(join6(target, name), "utf8"), dir: target };
2865
2964
  }
2866
- return { text: readFileSync10(target, "utf8") };
2965
+ return { text: readFileSync11(target, "utf8") };
2867
2966
  }
2868
2967
  var line = (c) => `${c.op} ${c.what}`;
2869
2968
  var printPlan = (plan) => console.log(plan.map(line).join("\n"));
@@ -2984,7 +3083,7 @@ var apply = {
2984
3083
  // src/commands/agents.ts
2985
3084
  function isLegacyExport(path) {
2986
3085
  try {
2987
- return existsSync10(path) && !statSync6(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync11(path, "utf8"));
3086
+ return existsSync11(path) && !statSync7(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync12(path, "utf8"));
2988
3087
  } catch {
2989
3088
  return false;
2990
3089
  }
@@ -3051,9 +3150,9 @@ var agents = {
3051
3150
  if (promptInline != null && promptFile) throw usageError(t("Use --prompt or --prompt-file, not both.", "Usa --prompt o --prompt-file, no los dos."), this.usage);
3052
3151
  const mode = str(ctx, "prompt-mode");
3053
3152
  if (mode && mode !== "append" && mode !== "replace") throw usageError(t("--prompt-mode must be append or replace.", "--prompt-mode debe ser append o replace."), this.usage);
3054
- if (promptFile && !existsSync10(promptFile)) throw usageError(t(`File not found: ${promptFile}`, `No existe el archivo: ${promptFile}`), this.usage);
3153
+ if (promptFile && !existsSync11(promptFile)) throw usageError(t(`File not found: ${promptFile}`, `No existe el archivo: ${promptFile}`), this.usage);
3055
3154
  const mcpFile = str(ctx, "mcp-file");
3056
- if (mcpFile && !existsSync10(mcpFile)) throw usageError(t(`File not found: ${mcpFile}`, `No existe el archivo: ${mcpFile}`), this.usage);
3155
+ if (mcpFile && !existsSync11(mcpFile)) throw usageError(t(`File not found: ${mcpFile}`, `No existe el archivo: ${mcpFile}`), this.usage);
3057
3156
  if (bool(ctx, "copy-files") && !like && !from) throw usageError(t("--copy-files needs --like or --from.", "--copy-files necesita --like o --from."), this.usage);
3058
3157
  if (from && !isLegacyExport(from)) {
3059
3158
  await applyTarget(ctx, from, { create: true, name: str(ctx, "name"), dryRun: bool(ctx, "dry-run"), usage: this.usage });
@@ -3063,7 +3162,7 @@ var agents = {
3063
3162
  const template = str(ctx, "template") ?? source?.template;
3064
3163
  if (!template) throw usageError(t("Missing --template (or --like / --from).", "Falta --template (o --like / --from)."), this.usage);
3065
3164
  const env = await envFrom(ctx, this.usage);
3066
- const prompt = promptInline ?? (promptFile ? readFileSync11(promptFile, "utf8") : source?.systemPrompt);
3165
+ const prompt = promptInline ?? (promptFile ? readFileSync12(promptFile, "utf8") : source?.systemPrompt);
3067
3166
  const promptMode = mode ?? source?.systemPromptMode;
3068
3167
  const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
3069
3168
  const promptAfter = multiline ? prompt : void 0;
@@ -3072,7 +3171,7 @@ var agents = {
3072
3171
  }
3073
3172
  if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
3074
3173
  if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
3075
- const mcpServers = mcpFile ? parseMcpFile(readFileSync11(mcpFile, "utf8"), this.usage) : source?.mcpServers;
3174
+ const mcpServers = mcpFile ? parseMcpFile(readFileSync12(mcpFile, "utf8"), this.usage) : source?.mcpServers;
3076
3175
  if (mcpServers && hasMasked(mcpServers)) {
3077
3176
  throw usageError(t(`${from ?? "the source"} has masked MCP values (\u2026***).`, `${from ?? "la fuente"} trae valores del MCP tapados (\u2026***).`), t("Re-export with --show-secrets, or pass --mcp-file", "Vuelve a exportar con --show-secrets, o pasa --mcp-file"));
3078
3177
  }
@@ -3164,7 +3263,7 @@ var agents = {
3164
3263
  };
3165
3264
 
3166
3265
  // src/commands/completion.ts
3167
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
3266
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
3168
3267
  import { homedir as homedir4 } from "os";
3169
3268
  import { dirname as dirname3, join as join7 } from "path";
3170
3269
  var GLOBAL_FLAGS = ["--json", "--token", "--lang", "--help", "--version"];
@@ -3207,7 +3306,7 @@ var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".ca
3207
3306
  async function namesOf(ctx, kind) {
3208
3307
  let cache2 = {};
3209
3308
  try {
3210
- cache2 = JSON.parse(readFileSync12(cacheFile(), "utf8"));
3309
+ cache2 = JSON.parse(readFileSync13(cacheFile(), "utf8"));
3211
3310
  const c = cache2[kind];
3212
3311
  if (c && Date.now() - c.at < 5 * 6e4) return c.names;
3213
3312
  } catch {
@@ -3334,13 +3433,13 @@ var docs = {
3334
3433
  };
3335
3434
 
3336
3435
  // src/commands/doctor.ts
3337
- import { existsSync as existsSync11, statSync as statSync7 } from "fs";
3436
+ import { existsSync as existsSync12, statSync as statSync8 } from "fs";
3338
3437
  import { homedir as homedir6 } from "os";
3339
3438
  import { join as join9 } from "path";
3340
3439
  import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
3341
3440
 
3342
3441
  // src/update.ts
3343
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
3442
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
3344
3443
  import { homedir as homedir5 } from "os";
3345
3444
  import { dirname as dirname4, join as join8 } from "path";
3346
3445
  var FILE = join8(process.env.XDG_CONFIG_HOME || join8(homedir5(), ".config"), "easybits", "update.json");
@@ -3355,7 +3454,7 @@ function newer(a, b) {
3355
3454
  var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
3356
3455
  function readCache() {
3357
3456
  try {
3358
- return JSON.parse(readFileSync13(FILE, "utf8"));
3457
+ return JSON.parse(readFileSync14(FILE, "utf8"));
3359
3458
  } catch {
3360
3459
  return {};
3361
3460
  }
@@ -3390,7 +3489,7 @@ async function updateNotice(current2) {
3390
3489
  }
3391
3490
 
3392
3491
  // src/commands/doctor.ts
3393
- var VERSION = true ? "0.13.0" : "dev";
3492
+ var VERSION = true ? "0.14.0" : "dev";
3394
3493
  var doctor2 = {
3395
3494
  name: "doctor",
3396
3495
  group: "Account",
@@ -3434,8 +3533,8 @@ var doctor2 = {
3434
3533
  }
3435
3534
  }
3436
3535
  const rcPath = join9(homedir6(), ".easybitsrc");
3437
- if (existsSync11(rcPath)) {
3438
- const mode = statSync7(rcPath).mode & 511;
3536
+ if (existsSync12(rcPath)) {
3537
+ const mode = statSync8(rcPath).mode & 511;
3439
3538
  const ok2 = (mode & 63) === 0;
3440
3539
  checks.push({ check: "rc-file", ok: ok2, detail: `${rcPath} ${mode.toString(8).padStart(3, "0")}`, ...ok2 ? {} : { hint: t(`It holds tokens. Run: chmod 600 ${rcPath}`, `Guarda tokens. Corre: chmod 600 ${rcPath}`) } });
3441
3540
  }
@@ -3620,6 +3719,23 @@ var HELP_ES = {
3620
3719
  "List your permanent machines": "Enlista tus m\xE1quinas permanentes",
3621
3720
  "Publish the machine's current app code as a new release": "Publica el c\xF3digo actual de la app como un release nuevo",
3622
3721
  "Release note": "Nota del release",
3722
+ "Create a machine and put an app on it in one step (or redeploy onto an existing one)": "Crea una m\xE1quina y le pone la app en un paso (o redespliega sobre una existente)",
3723
+ "Git repo to clone (clean URL; a token goes in --repo-token)": "Repo git a clonar (URL limpia; el token va en --repo-token)",
3724
+ "Branch to clone": "Rama a clonar",
3725
+ "Token for a private repo; best as $secret:NAME from your vault": "Token de un repo privado; mejor como $secret:NOMBRE de tu vault",
3726
+ "A .tar.gz/.zip of the app: URL, or a local file that gets uploaded first": "Un .tar.gz/.zip de la app: URL, o un archivo local que se sube primero",
3727
+ "Deploy onto this existing machine instead of creating one": "Despliega sobre esta m\xE1quina existente en vez de crear una",
3728
+ "Size for a new machine: nano, micro (default), estandar, focus, performance\u2026": "Tama\xF1o de una m\xE1quina nueva: nano, micro (default), estandar, focus, performance\u2026",
3729
+ "Human label": "Nombre para personas",
3730
+ "Where the app lives in the machine (default /app)": "D\xF3nde vive la app en la m\xE1quina (default /app)",
3731
+ "Build command (default: npm ci && npm run build)": "Comando de build (default: npm ci && npm run build)",
3732
+ "Start command (default: npm start)": "Comando de arranque (default: npm start)",
3733
+ "Port the app listens on (default 3000)": "Puerto en el que escucha la app (default 3000)",
3734
+ "The code is already built: skip the build, just start it": "El c\xF3digo ya viene compilado: sin build, s\xF3lo arranca",
3735
+ "Vault secret the app needs, by name (repeatable)": "Secreto del vault que necesita la app, por nombre (repetible)",
3736
+ "Path the nightly backup copies (repeatable). Without it nothing is backed up": "Ruta que copia el respaldo nocturno (repetible). Sin ella no se respalda nada",
3737
+ "Domain to attach; the output says which DNS record to create": "Dominio a pegar; la salida dice qu\xE9 registro DNS crear",
3738
+ "redeploy": "redespliegue",
3623
3739
  "same thing, shorter": "lo mismo, m\xE1s corto",
3624
3740
  "List releases, newest first": "Enlista los releases, el m\xE1s nuevo primero",
3625
3741
  "How many": "Cu\xE1ntos",
@@ -3871,7 +3987,7 @@ function normalizeArgs(input) {
3871
3987
  }
3872
3988
 
3873
3989
  // src/index.ts
3874
- var VERSION2 = true ? "0.13.0" : "dev";
3990
+ var VERSION2 = true ? "0.14.0" : "dev";
3875
3991
  var GLOBAL_OPTIONS = {
3876
3992
  json: { type: "boolean" },
3877
3993
  token: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Official CLI for EasyBits, the cloud for AI agents — sandboxes, hosting, domains, databases, agents and files from your terminal",
5
5
  "type": "module",
6
6
  "bin": {