@easybits.cloud/cli 0.12.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 +20 -1
  2. package/dist/index.js +410 -80
  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
@@ -68,7 +70,8 @@ easybits domains rm sb_abc123 shop.example.com --yes
68
70
  # Databases
69
71
  easybits db ls
70
72
  easybits db create leads
71
- easybits db tables leads # tables, row counts, columns
73
+ easybits db tables leads # tables, row counts, columns, permanent vs external links
74
+ easybits db photos put catalog --table products --key-column sku --dir photos/ # SKU-123.jpg → row sku=SKU-123
72
75
  easybits db query leads "SELECT * FROM leads" --json
73
76
  easybits db rm leads --yes
74
77
 
@@ -163,6 +166,22 @@ Host *.ghosty
163
166
 
164
167
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
165
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
+
177
+ ## Changes in 0.13.0
178
+
179
+ - Catalog photos: `db photos put <db> --table T --key-column sku --dir photos/ [--column C]
180
+ [--replace] [--dry-run]` uploads each photo as a public file with a permanent link and writes it
181
+ in the matching row (real type by bytes, JPEG/PNG/WebP, 10 MB; one upload per distinct photo).
182
+ - `db tables` shows, per text column with links, how many are permanent, expiring (signed),
183
+ external or empty (`--json`: `links`).
184
+
166
185
  ## Changes in 0.12.0
167
186
 
168
187
  - `easybits completion zsh|bash|fish`: TAB completes commands, subcommands, flags and the names of
package/dist/index.js CHANGED
@@ -252,7 +252,12 @@ var HEADERS_ES = {
252
252
  TABLE: "TABLA",
253
253
  TYPE: "TIPO",
254
254
  "URL / COMMAND": "URL / COMANDO",
255
- VERSION: "VERSI\xD3N"
255
+ VERSION: "VERSI\xD3N",
256
+ COLUMN: "COLUMNA",
257
+ PERMANENT: "PERMANENTES",
258
+ EXPIRING: "CADUCAN",
259
+ EXTERNAL: "DE FUERA",
260
+ EMPTY: "VAC\xCDAS"
256
261
  };
257
262
  function table(rows, cols, empty = t("Nothing here yet.", "Todav\xEDa no hay nada.")) {
258
263
  if (lang() === "es") cols = cols.map(([k, l]) => [k, HEADERS_ES[l] ?? l]);
@@ -1373,6 +1378,8 @@ var sandboxes = {
1373
1378
  };
1374
1379
 
1375
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";
1376
1383
  var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine> [--dotenv <path> | NAME]";
1377
1384
  var machines = {
1378
1385
  name: "machines",
@@ -1400,6 +1407,103 @@ var machines = {
1400
1407
  );
1401
1408
  }
1402
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
+ },
1403
1507
  deploy: {
1404
1508
  summary: "Publish the machine's current app code as a new release",
1405
1509
  usage: "easybits machines deploy <machine> [--message <text>]",
@@ -1570,6 +1674,101 @@ var domains = {
1570
1674
  }
1571
1675
  };
1572
1676
 
1677
+ // src/commands/db.ts
1678
+ import { existsSync as existsSync7, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
1679
+
1680
+ // src/photos.ts
1681
+ import { createHash as createHash2 } from "crypto";
1682
+ import { readFileSync as readFileSync7, readdirSync, statSync as statSync3 } from "fs";
1683
+ import { extname as extname2, join as join3 } from "path";
1684
+ var PHOTO_MAX_BYTES = 10 * 1024 * 1024;
1685
+ var WHATSAPP_MAX_BYTES = 5 * 1024 * 1024;
1686
+ function imageTypeOf(b) {
1687
+ if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return { mime: "image/jpeg", ext: "jpg" };
1688
+ if (b.length >= 8 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71 && b[4] === 13 && b[5] === 10 && b[6] === 26 && b[7] === 10) {
1689
+ return { mime: "image/png", ext: "png" };
1690
+ }
1691
+ if (b.length >= 12 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) {
1692
+ return { mime: "image/webp", ext: "webp" };
1693
+ }
1694
+ return null;
1695
+ }
1696
+ function photoFiles(dir) {
1697
+ const photos = [];
1698
+ const rejected = [];
1699
+ for (const name of readdirSync(dir).sort()) {
1700
+ if (name.startsWith(".")) continue;
1701
+ const path = join3(dir, name);
1702
+ const st = statSync3(path);
1703
+ if (!st.isFile()) continue;
1704
+ if (st.size === 0) {
1705
+ rejected.push({ file: name, reason: "empty" });
1706
+ continue;
1707
+ }
1708
+ if (st.size > PHOTO_MAX_BYTES) {
1709
+ rejected.push({ file: name, reason: "too_large" });
1710
+ continue;
1711
+ }
1712
+ const bytes = readFileSync7(path);
1713
+ const type = imageTypeOf(bytes);
1714
+ if (!type) {
1715
+ rejected.push({ file: name, reason: "not_an_image" });
1716
+ continue;
1717
+ }
1718
+ const key = name.slice(0, name.length - extname2(name).length);
1719
+ photos.push({ key, file: name, path, ...type, bytes: st.size, sha: createHash2("sha256").update(bytes).digest("hex") });
1720
+ }
1721
+ return { photos, rejected };
1722
+ }
1723
+ function linkKind(v) {
1724
+ if (v == null || String(v).trim() === "") return "empty";
1725
+ const s = String(v).trim();
1726
+ let u;
1727
+ try {
1728
+ u = new URL(s);
1729
+ } catch {
1730
+ return "other";
1731
+ }
1732
+ if (u.protocol !== "https:" && u.protocol !== "http:") return "other";
1733
+ if (/X-Amz-Signature=|[?&]Signature=|[?&]sig=/i.test(u.search)) return "expiring";
1734
+ if (/^easybits-public\./.test(u.hostname) || u.hostname === "easybits.cloud" || u.hostname.endsWith(".easybits.cloud")) return "permanent";
1735
+ return "external";
1736
+ }
1737
+ var PHOTO_COLUMNS = ["image_url", "photo_url", "imagen_url", "foto_url", "image", "photo", "imagen", "foto", "img"];
1738
+ function defaultPhotoColumn(columns) {
1739
+ const lower = new Map(columns.map((c) => [c.toLowerCase(), c]));
1740
+ for (const c of PHOTO_COLUMNS) if (lower.has(c)) return lower.get(c);
1741
+ return void 0;
1742
+ }
1743
+ function planPhotos(photos, current2, replace) {
1744
+ return photos.map((p) => {
1745
+ if (!current2.has(p.key)) return { key: p.key, file: p.file, action: "no_row" };
1746
+ const cur = current2.get(p.key);
1747
+ const keep = !replace && linkKind(cur) === "permanent";
1748
+ return { key: p.key, file: p.file, action: keep ? "keep" : "update", ...cur ? { current: String(cur) } : {} };
1749
+ });
1750
+ }
1751
+ var ident = (n) => `"${n.replace(/"/g, '""')}"`;
1752
+ function linkStatsSql(table2, columns) {
1753
+ const parts = columns.flatMap((c) => {
1754
+ const v = `trim(CAST(${ident(c)} AS TEXT))`;
1755
+ const url = `(${v} LIKE 'http://%' OR ${v} LIKE 'https://%')`;
1756
+ const exp = `(${v} LIKE '%X-Amz-Signature=%' OR ${v} LIKE '%?Signature=%' OR ${v} LIKE '%&Signature=%')`;
1757
+ const perm = `(${v} LIKE 'https://easybits-public.%' OR ${v} LIKE 'http%://easybits.cloud/%' OR ${v} LIKE 'http%.easybits.cloud/%')`;
1758
+ return [
1759
+ `SUM(CASE WHEN ${url} AND NOT ${exp} AND ${perm} THEN 1 ELSE 0 END)`,
1760
+ `SUM(CASE WHEN ${url} AND ${exp} THEN 1 ELSE 0 END)`,
1761
+ `SUM(CASE WHEN ${url} AND NOT ${exp} AND NOT ${perm} THEN 1 ELSE 0 END)`,
1762
+ `SUM(CASE WHEN ${ident(c)} IS NULL OR ${v} = '' THEN 1 ELSE 0 END)`
1763
+ ];
1764
+ });
1765
+ return `SELECT ${parts.join(", ")} FROM ${ident(table2)}`;
1766
+ }
1767
+ function parseLinkStats(columns, row) {
1768
+ const n = (i) => Number(row[i] ?? 0) || 0;
1769
+ return columns.map((column, i) => ({ column, permanent: n(i * 4), expiring: n(i * 4 + 1), external: n(i * 4 + 2), empty: n(i * 4 + 3) })).filter((s) => s.permanent + s.expiring + s.external > 0);
1770
+ }
1771
+
1573
1772
  // src/refs.ts
1574
1773
  var ID_FORMAT = {
1575
1774
  agent: /^[0-9a-f]{24}$/i,
@@ -1609,7 +1808,8 @@ var REF_ARGS = {
1609
1808
  "domains rm": { index: 0, kind: "sandbox" },
1610
1809
  "db rm": { index: 0, kind: "db" },
1611
1810
  "db tables": { index: 0, kind: "db" },
1612
- "db query": { index: 0, kind: "db" }
1811
+ "db query": { index: 0, kind: "db" },
1812
+ "db photos": { index: 1, kind: "db" }
1613
1813
  };
1614
1814
  function matchRef(kind, ref, items) {
1615
1815
  if (looksLikeId(kind, ref)) return { ok: true, id: ref };
@@ -1731,7 +1931,7 @@ var db = {
1731
1931
  },
1732
1932
  tables: {
1733
1933
  aliases: ["inspect", "schema"],
1734
- summary: "List tables with row counts and columns",
1934
+ summary: "List tables with row counts and columns, and how many links are permanent",
1735
1935
  usage: "easybits db tables <db-id|name>",
1736
1936
  examples: ["easybits db tables leads", "easybits db tables leads --json"],
1737
1937
  // Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
@@ -1741,28 +1941,133 @@ var db = {
1741
1941
  const hit = await findDb(ctx, ref);
1742
1942
  const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
1743
1943
  const names = (await q("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name")).rows.map((r) => String(r[0]));
1744
- const ident = (n) => `"${n.replace(/"/g, '""')}"`;
1944
+ const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
1745
1945
  const tables = [];
1746
1946
  for (const name of names) {
1747
- const info2 = await q(`PRAGMA table_info(${ident(name)})`);
1947
+ const info2 = await q(`PRAGMA table_info(${ident2(name)})`);
1748
1948
  const col = (k) => info2.cols.indexOf(k);
1749
1949
  const columns = info2.rows.map((r) => ({ name: String(r[col("name")]), type: String(r[col("type")] ?? ""), pk: Number(r[col("pk")]) > 0 }));
1750
- const count = await q(`SELECT count(*) FROM ${ident(name)}`);
1751
- tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns });
1950
+ const count = await q(`SELECT count(*) FROM ${ident2(name)}`);
1951
+ const textCols = columns.filter((c) => !c.type || /CHAR|TEXT|CLOB/i.test(c.type)).map((c) => c.name);
1952
+ const links = textCols.length ? parseLinkStats(textCols, (await q(linkStatsSql(name, textCols))).rows[0] ?? []) : [];
1953
+ tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns, links });
1752
1954
  }
1753
- emit(
1754
- ctx,
1755
- tables,
1756
- () => table(
1955
+ emit(ctx, tables, () => {
1956
+ table(
1757
1957
  tables.map((t2) => ({
1758
1958
  name: t2.name,
1759
1959
  rows: t2.rows,
1760
1960
  columns: t2.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
1761
1961
  })),
1762
1962
  [["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
1763
- `No tables in ${hit.name}. Create one: easybits db query ${hit.name} 'CREATE TABLE \u2026'`
1764
- )
1765
- );
1963
+ t(`No tables in ${hit.name}. Create one: easybits db query ${hit.name} 'CREATE TABLE \u2026'`, `No hay tablas en ${hit.name}. Crea una: easybits db query ${hit.name} 'CREATE TABLE \u2026'`)
1964
+ );
1965
+ const withLinks = tables.flatMap((tb) => tb.links.map((l) => ({ ...l, table: tb.name })));
1966
+ if (withLinks.length) {
1967
+ console.log("\n" + t("Links (permanent = EasyBits public storage; expiring = signed URL):", "Ligas (permanente = almacenamiento p\xFAblico de EasyBits; caduca = URL firmada):"));
1968
+ table(
1969
+ withLinks.map((l) => ({ where: `${l.table}.${l.column}`, permanent: l.permanent, expiring: l.expiring, external: l.external, empty: l.empty })),
1970
+ [["where", "COLUMN"], ["permanent", "PERMANENT"], ["expiring", "EXPIRING"], ["external", "EXTERNAL"], ["empty", "EMPTY"]]
1971
+ );
1972
+ if (withLinks.some((l) => l.external + l.expiring > 0)) {
1973
+ console.log(t("Make them permanent: easybits db photos put <db> --table T --key-column sku --dir photos/", "Hazlas permanentes: easybits db photos put <db> --table T --key-column sku --dir fotos/"));
1974
+ }
1975
+ }
1976
+ });
1977
+ }
1978
+ },
1979
+ photos: {
1980
+ summary: "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link",
1981
+ usage: "easybits db photos put <db> --table <table> --key-column <col> --dir <folder> [--column <col>] [--replace] [--dry-run]",
1982
+ options: {
1983
+ table: { type: "string", value: "table", description: "Table to update" },
1984
+ "key-column": { type: "string", value: "col", description: "Column that matches the file name (e.g. sku)" },
1985
+ dir: { type: "string", value: "folder", description: "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)" },
1986
+ column: { type: "string", value: "col", description: "Column for the link (default: image_url, photo_url, imagen\u2026)" },
1987
+ replace: { type: "boolean", description: "Also replace rows that already have a permanent link" },
1988
+ "dry-run": { type: "boolean", description: "Show the plan; upload nothing" }
1989
+ },
1990
+ examples: [
1991
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --dry-run",
1992
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --column foto",
1993
+ "easybits db tables catalogo # how many links are permanent, external or empty"
1994
+ ],
1995
+ async run(ctx) {
1996
+ const action = need(ctx, 0, "put", this.usage);
1997
+ if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
1998
+ const ref = need(ctx, 1, "db", this.usage);
1999
+ const tableName = str(ctx, "table");
2000
+ const keyCol = str(ctx, "key-column");
2001
+ const dir = str(ctx, "dir");
2002
+ if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
2003
+ if (!existsSync7(dir) || !statSync4(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
2004
+ const { photos, rejected } = photoFiles(dir);
2005
+ const eb = await getClient(ctx);
2006
+ const hit = await findDb(ctx, ref);
2007
+ const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
2008
+ const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
2009
+ const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
2010
+ const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
2011
+ if (!cols.length) throw new CliError(t(`No table "${tableName}" in ${hit.name}.`, `No hay tabla "${tableName}" en ${hit.name}.`), 1, t(`See: easybits db tables ${hit.name}`, `Mira: easybits db tables ${hit.name}`), "not_found", 404);
2012
+ if (!cols.includes(keyCol)) throw usageError(t(`No column "${keyCol}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${keyCol}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2013
+ const column = str(ctx, "column") ?? defaultPhotoColumn(cols);
2014
+ if (!column) throw usageError(t(`${tableName} has no photo column: pass --column (${cols.join(", ")}).`, `${tableName} no tiene columna de foto: pasa --column (${cols.join(", ")}).`), this.usage);
2015
+ if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2016
+ const current2 = /* @__PURE__ */ new Map();
2017
+ for (let i = 0; i < photos.length; i += 100) {
2018
+ const keys = photos.slice(i, i + 100).map((p) => p.key);
2019
+ const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
2020
+ for (const row of r.rows) current2.set(String(row[0]), row[1]);
2021
+ }
2022
+ const plan = planPhotos(photos, current2, bool(ctx, "replace"));
2023
+ const big = photos.filter((p) => p.bytes > WHATSAPP_MAX_BYTES).map((p) => p.file);
2024
+ const summary = { db: hit.name, table: tableName, keyColumn: keyCol, column, plan, rejected, overWhatsappLimit: big };
2025
+ const count = (a) => plan.filter((p) => p.action === a).length;
2026
+ const why = (r) => r === "not_an_image" ? t("not a JPEG/PNG/WebP", "no es JPEG/PNG/WebP") : r === "too_large" ? t("over 10 MB", "m\xE1s de 10 MB") : t("empty file", "archivo vac\xEDo");
2027
+ const printPlan2 = () => {
2028
+ for (const p of plan) {
2029
+ const what = p.action === "update" ? t("update", "actualizar") : p.action === "keep" ? t("keep (already permanent; --replace to redo)", "se queda (ya es permanente; --replace para rehacerla)") : t("no row with that key", "no hay fila con esa llave");
2030
+ console.log(`${p.action === "update" ? "~" : "!"} ${p.file} \u2192 ${keyCol}=${p.key}: ${what}`);
2031
+ }
2032
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2033
+ if (big.length) console.log(t(`Over 5 MB (WhatsApp won't send them by link): ${big.join(", ")}`, `M\xE1s de 5 MB (WhatsApp no las manda por liga): ${big.join(", ")}`));
2034
+ };
2035
+ if (bool(ctx, "dry-run") || !count("update")) {
2036
+ emit(ctx, { ...summary, dryRun: bool(ctx, "dry-run") }, () => {
2037
+ printPlan2();
2038
+ console.log(count("update") ? t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)") : t("Nothing to update.", "Nada que actualizar."));
2039
+ });
2040
+ return;
2041
+ }
2042
+ const urls = /* @__PURE__ */ new Map();
2043
+ const results = [];
2044
+ for (const item of plan.filter((p) => p.action === "update")) {
2045
+ const photo = photos.find((p) => p.key === item.key);
2046
+ try {
2047
+ let url = urls.get(photo.sha);
2048
+ if (!url) {
2049
+ const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
2050
+ const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync8(photo.path), headers: { "Content-Type": photo.mime } });
2051
+ if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
2052
+ url = up.file.url;
2053
+ if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
2054
+ urls.set(photo.sha, url);
2055
+ }
2056
+ await q(`UPDATE ${ident2(tableName)} SET ${ident2(column)} = ? WHERE ${ident2(keyCol)} = ?`, [url, item.key]);
2057
+ results.push({ key: item.key, file: item.file, url });
2058
+ if (!ctx.json) console.log(`~ ${item.file} \u2192 ${keyCol}=${item.key}: ${url}`);
2059
+ } catch (e) {
2060
+ results.push({ key: item.key, file: item.file, error: e.message });
2061
+ if (!ctx.json) console.error(`! ${item.file}: ${e.message}`);
2062
+ }
2063
+ }
2064
+ const failed = results.filter((r) => r.error).length;
2065
+ emit(ctx, { ...summary, results }, () => {
2066
+ for (const p of plan.filter((x) => x.action !== "update")) console.log(`! ${p.file} \u2192 ${keyCol}=${p.key}: ${p.action === "keep" ? t("kept", "se qued\xF3") : t("no row with that key", "no hay fila con esa llave")}`);
2067
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
2068
+ console.log(t(`Updated ${results.length - failed} row(s)${failed ? `, ${failed} failed` : ""}.`, `${results.length - failed} fila(s) actualizadas${failed ? `, ${failed} fallaron` : ""}.`));
2069
+ });
2070
+ if (failed) process.exitCode = 1;
1766
2071
  }
1767
2072
  },
1768
2073
  query: {
@@ -1804,7 +2109,7 @@ var db = {
1804
2109
  };
1805
2110
 
1806
2111
  // src/commands/agents.ts
1807
- import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
2112
+ import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
1808
2113
 
1809
2114
  // src/api.ts
1810
2115
  import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
@@ -1834,8 +2139,8 @@ async function api(ctx, method, path, body, opts = {}) {
1834
2139
  var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
1835
2140
 
1836
2141
  // src/commands/agents-config.ts
1837
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1838
- import { basename as basename2, dirname, join as join3, 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";
1839
2144
  var WORK_DIR = "/data/work";
1840
2145
  var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
1841
2146
  var SKILLS_DIR = "/data/agent/skills";
@@ -1914,8 +2219,8 @@ function readLocalText(path, usage2) {
1914
2219
  if (process.stdin.isTTY) throw usageError(t("Expected the content on stdin.", "Se esperaba el contenido por stdin."), usage2);
1915
2220
  return readStdin().then((b) => b.toString("utf8"));
1916
2221
  }
1917
- if (!existsSync6(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
1918
- return readFileSync6(path, "utf8");
2222
+ if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2223
+ return readFileSync9(path, "utf8");
1919
2224
  }
1920
2225
  function parseMcpFile(text, usage2) {
1921
2226
  let j;
@@ -2130,8 +2435,8 @@ var files2 = {
2130
2435
  if (!rest.length) throw usageError(t("Give one or more local files.", "Pasa uno o m\xE1s archivos locales."), FILES_USAGE2);
2131
2436
  const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
2132
2437
  const plan = rest.map((local) => {
2133
- if (!existsSync6(local) || !statSync2(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
2134
- return { local, path: to ? `${to}/${basename2(local)}` : basename2(local), bytes: statSync2(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 };
2135
2440
  });
2136
2441
  if (bool(ctx, "dry-run")) {
2137
2442
  emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
@@ -2142,7 +2447,7 @@ var files2 = {
2142
2447
  }
2143
2448
  const done = [];
2144
2449
  for (const p of plan) {
2145
- const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync6(p.local), { raw: true });
2450
+ const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync9(p.local), { raw: true });
2146
2451
  done.push({ local: p.local, ...r });
2147
2452
  if (!ctx.json) console.log(`${t("Uploaded", "Subido")} ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2148
2453
  }
@@ -2168,11 +2473,11 @@ var SKILLS_USAGE = "easybits agents skills <ls|get|add|rm> <agent> [slug...] [--
2168
2473
  function readSkillDir(dir) {
2169
2474
  const assets = [];
2170
2475
  const visit = (d) => {
2171
- for (const e of readdirSync(d, { withFileTypes: true })) {
2476
+ for (const e of readdirSync2(d, { withFileTypes: true })) {
2172
2477
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2173
- const p = join3(d, e.name);
2478
+ const p = join4(d, e.name);
2174
2479
  if (e.isDirectory()) visit(p);
2175
- else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync6(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") });
2176
2481
  }
2177
2482
  };
2178
2483
  visit(dir);
@@ -2225,22 +2530,22 @@ var skills = {
2225
2530
  }
2226
2531
  const s = await readSkill(box, entry);
2227
2532
  mkdirSync3(out, { recursive: true });
2228
- writeFileSync4(join3(out, "SKILL.md"), s.markdown);
2533
+ writeFileSync4(join4(out, "SKILL.md"), s.markdown);
2229
2534
  for (const a of s.assets) {
2230
- mkdirSync3(dirname(join3(out, a.name)), { recursive: true });
2231
- writeFileSync4(join3(out, a.name), Buffer.from(a.contentBase64, "base64"));
2535
+ mkdirSync3(dirname(join4(out, a.name)), { recursive: true });
2536
+ writeFileSync4(join4(out, a.name), Buffer.from(a.contentBase64, "base64"));
2232
2537
  }
2233
2538
  emit(ctx, { slug, out: resolve(out), files: entry.files }, () => console.log(t(`Saved ${entry.files.length} file(s) to ${resolve(out)}`, `Guard\xE9 ${entry.files.length} archivo(s) en ${resolve(out)}`)));
2234
2539
  } else if (action === "add" || action === "put") {
2235
2540
  const file = str(ctx, "file");
2236
2541
  const dir = str(ctx, "dir");
2237
2542
  if (!!file === !!dir) throw usageError(t("Pass --file SKILL.md or --dir <folder>.", "Pasa --file SKILL.md o --dir <carpeta>."), SKILLS_USAGE);
2238
- const mdPath = file ?? join3(dir, "SKILL.md");
2239
- if (!existsSync6(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2240
- const markdown = readFileSync6(mdPath, "utf8");
2543
+ const mdPath = file ?? join4(dir, "SKILL.md");
2544
+ if (!existsSync8(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2545
+ const markdown = readFileSync9(mdPath, "utf8");
2241
2546
  const fm = skillFrontmatter(markdown);
2242
2547
  if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
2243
- 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());
2244
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);
2245
2550
  const assets = dir ? readSkillDir(dir) : [];
2246
2551
  const plan = { agentId: id, slug, files: ["SKILL.md", ...assets.map((a) => a.name)], restart: bool(ctx, "restart") };
@@ -2497,10 +2802,10 @@ var exportAgent = {
2497
2802
  }
2498
2803
  };
2499
2804
  function readExport(path, usage2) {
2500
- if (!existsSync6(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);
2501
2806
  let x;
2502
2807
  try {
2503
- x = JSON.parse(readFileSync6(path, "utf8"));
2808
+ x = JSON.parse(readFileSync9(path, "utf8"));
2504
2809
  } catch (e) {
2505
2810
  throw usageError(t(`${path} is not JSON: ${e.message}`, `${path} no es JSON: ${e.message}`), usage2);
2506
2811
  }
@@ -2545,37 +2850,37 @@ async function applyExport(ctx, id, x, opts) {
2545
2850
  }
2546
2851
 
2547
2852
  // src/commands/spec.ts
2548
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
2853
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
2549
2854
  import { homedir as homedir3 } from "os";
2550
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
2855
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
2551
2856
 
2552
2857
  // src/specfile.ts
2553
- import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
2554
- import { join as join4, relative as relative2 } from "path";
2858
+ import { existsSync as existsSync9, readFileSync as readFileSync10, readdirSync as readdirSync3 } from "fs";
2859
+ import { join as join5, relative as relative2 } from "path";
2555
2860
  function walkLocal(root) {
2556
2861
  const out = [];
2557
2862
  const visit = (d) => {
2558
- for (const e of readdirSync2(d, { withFileTypes: true })) {
2863
+ for (const e of readdirSync3(d, { withFileTypes: true })) {
2559
2864
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2560
- const p = join4(d, e.name);
2865
+ const p = join5(d, e.name);
2561
2866
  if (e.isDirectory()) visit(p);
2562
2867
  else if (e.isFile()) out.push(relative2(root, p).split("\\").join("/"));
2563
2868
  }
2564
2869
  };
2565
- if (existsSync7(root)) visit(root);
2870
+ if (existsSync9(root)) visit(root);
2566
2871
  return out;
2567
2872
  }
2568
2873
  function readUploads(dir) {
2569
2874
  const skills2 = {};
2570
- const skillsRoot = join4(dir, "skills");
2571
- if (existsSync7(skillsRoot)) {
2572
- for (const e of readdirSync2(skillsRoot, { withFileTypes: true })) {
2875
+ const skillsRoot = join5(dir, "skills");
2876
+ if (existsSync9(skillsRoot)) {
2877
+ for (const e of readdirSync3(skillsRoot, { withFileTypes: true })) {
2573
2878
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
2574
- skills2[e.name] = walkLocal(join4(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync7(join4(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") }));
2575
2880
  }
2576
2881
  }
2577
- const filesRoot = join4(dir, "files");
2578
- const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync7(join4(filesRoot, p)).toString("base64")]));
2882
+ const filesRoot = join5(dir, "files");
2883
+ const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync10(join5(filesRoot, p)).toString("base64")]));
2579
2884
  return { ...Object.keys(skills2).length ? { skills: skills2 } : {}, ...Object.keys(files3).length ? { files: files3 } : {} };
2580
2885
  }
2581
2886
  function secretsFor(text, env = process.env) {
@@ -2628,17 +2933,17 @@ Ed\xEDtalo y luego: easybits apply ${out} --dry-run`)));
2628
2933
  const yaml = await getYaml();
2629
2934
  const dir = resolve2(out);
2630
2935
  mkdirSync4(dir, { recursive: true });
2631
- writeFileSync5(join5(dir, "agent.yaml"), yaml);
2936
+ writeFileSync5(join6(dir, "agent.yaml"), yaml);
2632
2937
  const x = await buildExport(ctx, id, { secrets: false, fileContents: true });
2633
2938
  const put = (path, b64) => {
2634
2939
  mkdirSync4(dirname2(path), { recursive: true });
2635
2940
  writeFileSync5(path, Buffer.from(b64, "base64"));
2636
2941
  };
2637
2942
  for (const s of x.skills ?? []) {
2638
- writeFileSync5((mkdirSync4(join5(dir, "skills", s.slug), { recursive: true }), join5(dir, "skills", s.slug, "SKILL.md")), s.markdown);
2639
- for (const a of s.assets) put(join5(dir, "skills", s.slug, a.name), a.contentBase64);
2943
+ writeFileSync5((mkdirSync4(join6(dir, "skills", s.slug), { recursive: true }), join6(dir, "skills", s.slug, "SKILL.md")), s.markdown);
2944
+ for (const a of s.assets) put(join6(dir, "skills", s.slug, a.name), a.contentBase64);
2640
2945
  }
2641
- for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join5(dir, "files", f.path), f.contentBase64);
2946
+ for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join6(dir, "files", f.path), f.contentBase64);
2642
2947
  const summary = { saved: dir, skills: (x.skills ?? []).map((s) => s.slug), files: (x.files ?? []).map((f) => f.path) };
2643
2948
  emit(
2644
2949
  ctx,
@@ -2651,20 +2956,20 @@ ${t("Edit it, then", "Ed\xEDtalo y luego")}: easybits apply ${out} --dry-run`
2651
2956
  }
2652
2957
  };
2653
2958
  function readTarget(target, usage2) {
2654
- if (!existsSync8(target)) throw usageError(t(`Not found: ${target}`, `No existe: ${target}`), usage2);
2655
- if (statSync3(target).isDirectory()) {
2656
- const name = SPEC_NAMES.find((n) => existsSync8(join5(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)));
2657
2962
  if (!name) throw usageError(t(`${target} has no agent.yaml.`, `${target} no tiene agent.yaml.`), usage2);
2658
- return { text: readFileSync8(join5(target, name), "utf8"), dir: target };
2963
+ return { text: readFileSync11(join6(target, name), "utf8"), dir: target };
2659
2964
  }
2660
- return { text: readFileSync8(target, "utf8") };
2965
+ return { text: readFileSync11(target, "utf8") };
2661
2966
  }
2662
2967
  var line = (c) => `${c.op} ${c.what}`;
2663
2968
  var printPlan = (plan) => console.log(plan.map(line).join("\n"));
2664
2969
  function saveBackup(id, yaml) {
2665
- const dir = join5(homedir3(), ".easybits", "backups");
2970
+ const dir = join6(homedir3(), ".easybits", "backups");
2666
2971
  mkdirSync4(dir, { recursive: true });
2667
- const path = join5(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
2972
+ const path = join6(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
2668
2973
  writeFileSync5(path, yaml);
2669
2974
  return path;
2670
2975
  }
@@ -2778,7 +3083,7 @@ var apply = {
2778
3083
  // src/commands/agents.ts
2779
3084
  function isLegacyExport(path) {
2780
3085
  try {
2781
- return existsSync9(path) && !statSync4(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync9(path, "utf8"));
3086
+ return existsSync11(path) && !statSync7(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync12(path, "utf8"));
2782
3087
  } catch {
2783
3088
  return false;
2784
3089
  }
@@ -2845,9 +3150,9 @@ var agents = {
2845
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);
2846
3151
  const mode = str(ctx, "prompt-mode");
2847
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);
2848
- if (promptFile && !existsSync9(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);
2849
3154
  const mcpFile = str(ctx, "mcp-file");
2850
- if (mcpFile && !existsSync9(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);
2851
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);
2852
3157
  if (from && !isLegacyExport(from)) {
2853
3158
  await applyTarget(ctx, from, { create: true, name: str(ctx, "name"), dryRun: bool(ctx, "dry-run"), usage: this.usage });
@@ -2857,7 +3162,7 @@ var agents = {
2857
3162
  const template = str(ctx, "template") ?? source?.template;
2858
3163
  if (!template) throw usageError(t("Missing --template (or --like / --from).", "Falta --template (o --like / --from)."), this.usage);
2859
3164
  const env = await envFrom(ctx, this.usage);
2860
- const prompt = promptInline ?? (promptFile ? readFileSync9(promptFile, "utf8") : source?.systemPrompt);
3165
+ const prompt = promptInline ?? (promptFile ? readFileSync12(promptFile, "utf8") : source?.systemPrompt);
2861
3166
  const promptMode = mode ?? source?.systemPromptMode;
2862
3167
  const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
2863
3168
  const promptAfter = multiline ? prompt : void 0;
@@ -2866,7 +3171,7 @@ var agents = {
2866
3171
  }
2867
3172
  if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
2868
3173
  if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
2869
- const mcpServers = mcpFile ? parseMcpFile(readFileSync9(mcpFile, "utf8"), this.usage) : source?.mcpServers;
3174
+ const mcpServers = mcpFile ? parseMcpFile(readFileSync12(mcpFile, "utf8"), this.usage) : source?.mcpServers;
2870
3175
  if (mcpServers && hasMasked(mcpServers)) {
2871
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"));
2872
3177
  }
@@ -2958,9 +3263,9 @@ var agents = {
2958
3263
  };
2959
3264
 
2960
3265
  // src/commands/completion.ts
2961
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
3266
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
2962
3267
  import { homedir as homedir4 } from "os";
2963
- import { dirname as dirname3, join as join6 } from "path";
3268
+ import { dirname as dirname3, join as join7 } from "path";
2964
3269
  var GLOBAL_FLAGS = ["--json", "--token", "--lang", "--help", "--version"];
2965
3270
  var AGENT_FLAGS = /* @__PURE__ */ new Set(["--agent", "--like"]);
2966
3271
  var findSub = (c, name) => Object.entries(c.subs ?? {}).find(([n, l]) => n === name || l.aliases?.includes(name));
@@ -2997,11 +3302,11 @@ function completionFor(words, current2, commands) {
2997
3302
  if (spec && spec.index === n) return { kind: "names", ref: spec.kind };
2998
3303
  return { kind: "words", words: [] };
2999
3304
  }
3000
- var cacheFile = () => join6(process.env.XDG_CACHE_HOME || join6(homedir4(), ".cache"), "easybits", "complete.json");
3305
+ var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "easybits", "complete.json");
3001
3306
  async function namesOf(ctx, kind) {
3002
3307
  let cache2 = {};
3003
3308
  try {
3004
- cache2 = JSON.parse(readFileSync10(cacheFile(), "utf8"));
3309
+ cache2 = JSON.parse(readFileSync13(cacheFile(), "utf8"));
3005
3310
  const c = cache2[kind];
3006
3311
  if (c && Date.now() - c.at < 5 * 6e4) return c.names;
3007
3312
  } catch {
@@ -3128,16 +3433,16 @@ var docs = {
3128
3433
  };
3129
3434
 
3130
3435
  // src/commands/doctor.ts
3131
- import { existsSync as existsSync10, statSync as statSync5 } from "fs";
3436
+ import { existsSync as existsSync12, statSync as statSync8 } from "fs";
3132
3437
  import { homedir as homedir6 } from "os";
3133
- import { join as join8 } from "path";
3438
+ import { join as join9 } from "path";
3134
3439
  import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
3135
3440
 
3136
3441
  // src/update.ts
3137
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
3442
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
3138
3443
  import { homedir as homedir5 } from "os";
3139
- import { dirname as dirname4, join as join7 } from "path";
3140
- var FILE = join7(process.env.XDG_CONFIG_HOME || join7(homedir5(), ".config"), "easybits", "update.json");
3444
+ import { dirname as dirname4, join as join8 } from "path";
3445
+ var FILE = join8(process.env.XDG_CONFIG_HOME || join8(homedir5(), ".config"), "easybits", "update.json");
3141
3446
  var DAY = 24 * 60 * 60 * 1e3;
3142
3447
  var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
3143
3448
  function newer(a, b) {
@@ -3149,7 +3454,7 @@ function newer(a, b) {
3149
3454
  var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
3150
3455
  function readCache() {
3151
3456
  try {
3152
- return JSON.parse(readFileSync11(FILE, "utf8"));
3457
+ return JSON.parse(readFileSync14(FILE, "utf8"));
3153
3458
  } catch {
3154
3459
  return {};
3155
3460
  }
@@ -3184,7 +3489,7 @@ async function updateNotice(current2) {
3184
3489
  }
3185
3490
 
3186
3491
  // src/commands/doctor.ts
3187
- var VERSION = true ? "0.12.0" : "dev";
3492
+ var VERSION = true ? "0.14.0" : "dev";
3188
3493
  var doctor2 = {
3189
3494
  name: "doctor",
3190
3495
  group: "Account",
@@ -3227,9 +3532,9 @@ var doctor2 = {
3227
3532
  checks.push({ check: "credential-env", ok: true, detail: t("EASYBITS_API_KEY wins over the saved login in ~/.easybitsrc", "EASYBITS_API_KEY le gana a la sesi\xF3n guardada en ~/.easybitsrc") });
3228
3533
  }
3229
3534
  }
3230
- const rcPath = join8(homedir6(), ".easybitsrc");
3231
- if (existsSync10(rcPath)) {
3232
- const mode = statSync5(rcPath).mode & 511;
3535
+ const rcPath = join9(homedir6(), ".easybitsrc");
3536
+ if (existsSync12(rcPath)) {
3537
+ const mode = statSync8(rcPath).mode & 511;
3233
3538
  const ok2 = (mode & 63) === 0;
3234
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}`) } });
3235
3540
  }
@@ -3414,6 +3719,23 @@ var HELP_ES = {
3414
3719
  "List your permanent machines": "Enlista tus m\xE1quinas permanentes",
3415
3720
  "Publish the machine's current app code as a new release": "Publica el c\xF3digo actual de la app como un release nuevo",
3416
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",
3417
3739
  "same thing, shorter": "lo mismo, m\xE1s corto",
3418
3740
  "List releases, newest first": "Enlista los releases, el m\xE1s nuevo primero",
3419
3741
  "How many": "Cu\xE1ntos",
@@ -3439,7 +3761,7 @@ var HELP_ES = {
3439
3761
  "Delete a database (irreversible)": "Borra una base de datos (irreversible)",
3440
3762
  "asks you to type the name": "te pide teclear el nombre",
3441
3763
  "scripts and agents": "scripts y agentes",
3442
- "List tables with row counts and columns": "Enlista las tablas con su n\xFAmero de filas y columnas",
3764
+ "List tables with row counts and columns, and how many links are permanent": "Enlista las tablas con sus filas y columnas, y cu\xE1ntas ligas son permanentes",
3443
3765
  "Run one SQL statement (by database id or name)": "Corre una sentencia SQL (por id o nombre de la base)",
3444
3766
  "Positional ? parameter (repeatable)": "Par\xE1metro ? posicional (repetible)",
3445
3767
  "Storage": "Almacenamiento",
@@ -3466,7 +3788,15 @@ var HELP_ES = {
3466
3788
  "Shell completion script (zsh, bash, fish)": "Script de autocompletado para tu shell (zsh, bash, fish)",
3467
3789
  "Print the completion script for your shell: commands, flags and your agents, sandboxes and databases": "Imprime el script de autocompletado de tu shell: comandos, flags y tus agentes, sandboxes y bases",
3468
3790
  "add to ~/.zshrc, after compinit": "agr\xE9galo a ~/.zshrc, despu\xE9s de compinit",
3469
- "add to ~/.bashrc": "agr\xE9galo a ~/.bashrc"
3791
+ "add to ~/.bashrc": "agr\xE9galo a ~/.bashrc",
3792
+ "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link": "Pone las fotos del cat\xE1logo en una tabla: SKU-123.jpg \u2192 la fila cuya llave es SKU-123, con liga p\xFAblica permanente",
3793
+ "Table to update": "Tabla a actualizar",
3794
+ "Column that matches the file name (e.g. sku)": "Columna que coincide con el nombre del archivo (p. ej. sku)",
3795
+ "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)": "Carpeta con las fotos (JPEG, PNG o WebP, m\xE1x 10 MB cada una)",
3796
+ "Column for the link (default: image_url, photo_url, imagen\u2026)": "Columna de la liga (default: image_url, photo_url, imagen\u2026)",
3797
+ "Also replace rows that already have a permanent link": "Reemplaza tambi\xE9n las filas que ya tienen liga permanente",
3798
+ "Show the plan; upload nothing": "Ense\xF1a el plan; no sube nada",
3799
+ "how many links are permanent, external or empty": "cu\xE1ntas ligas son permanentes, de fuera o vac\xEDas"
3470
3800
  };
3471
3801
 
3472
3802
  // src/help.ts
@@ -3657,7 +3987,7 @@ function normalizeArgs(input) {
3657
3987
  }
3658
3988
 
3659
3989
  // src/index.ts
3660
- var VERSION2 = true ? "0.12.0" : "dev";
3990
+ var VERSION2 = true ? "0.14.0" : "dev";
3661
3991
  var GLOBAL_OPTIONS = {
3662
3992
  json: { type: "boolean" },
3663
3993
  token: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.12.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": {