@easybits.cloud/cli 0.12.0 → 0.13.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 -1
  2. package/dist/index.js +293 -79
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -68,7 +68,8 @@ easybits domains rm sb_abc123 shop.example.com --yes
68
68
  # Databases
69
69
  easybits db ls
70
70
  easybits db create leads
71
- easybits db tables leads # tables, row counts, columns
71
+ easybits db tables leads # tables, row counts, columns, permanent vs external links
72
+ easybits db photos put catalog --table products --key-column sku --dir photos/ # SKU-123.jpg → row sku=SKU-123
72
73
  easybits db query leads "SELECT * FROM leads" --json
73
74
  easybits db rm leads --yes
74
75
 
@@ -163,6 +164,14 @@ Host *.ghosty
163
164
 
164
165
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
165
166
 
167
+ ## Changes in 0.13.0
168
+
169
+ - Catalog photos: `db photos put <db> --table T --key-column sku --dir photos/ [--column C]
170
+ [--replace] [--dry-run]` uploads each photo as a public file with a permanent link and writes it
171
+ in the matching row (real type by bytes, JPEG/PNG/WebP, 10 MB; one upload per distinct photo).
172
+ - `db tables` shows, per text column with links, how many are permanent, expiring (signed),
173
+ external or empty (`--json`: `links`).
174
+
166
175
  ## Changes in 0.12.0
167
176
 
168
177
  - `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]);
@@ -1570,6 +1575,101 @@ var domains = {
1570
1575
  }
1571
1576
  };
1572
1577
 
1578
+ // src/commands/db.ts
1579
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
1580
+
1581
+ // src/photos.ts
1582
+ import { createHash as createHash2 } from "crypto";
1583
+ import { readFileSync as readFileSync6, readdirSync, statSync as statSync2 } from "fs";
1584
+ import { extname as extname2, join as join3 } from "path";
1585
+ var PHOTO_MAX_BYTES = 10 * 1024 * 1024;
1586
+ var WHATSAPP_MAX_BYTES = 5 * 1024 * 1024;
1587
+ function imageTypeOf(b) {
1588
+ if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return { mime: "image/jpeg", ext: "jpg" };
1589
+ 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) {
1590
+ return { mime: "image/png", ext: "png" };
1591
+ }
1592
+ 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) {
1593
+ return { mime: "image/webp", ext: "webp" };
1594
+ }
1595
+ return null;
1596
+ }
1597
+ function photoFiles(dir) {
1598
+ const photos = [];
1599
+ const rejected = [];
1600
+ for (const name of readdirSync(dir).sort()) {
1601
+ if (name.startsWith(".")) continue;
1602
+ const path = join3(dir, name);
1603
+ const st = statSync2(path);
1604
+ if (!st.isFile()) continue;
1605
+ if (st.size === 0) {
1606
+ rejected.push({ file: name, reason: "empty" });
1607
+ continue;
1608
+ }
1609
+ if (st.size > PHOTO_MAX_BYTES) {
1610
+ rejected.push({ file: name, reason: "too_large" });
1611
+ continue;
1612
+ }
1613
+ const bytes = readFileSync6(path);
1614
+ const type = imageTypeOf(bytes);
1615
+ if (!type) {
1616
+ rejected.push({ file: name, reason: "not_an_image" });
1617
+ continue;
1618
+ }
1619
+ const key = name.slice(0, name.length - extname2(name).length);
1620
+ photos.push({ key, file: name, path, ...type, bytes: st.size, sha: createHash2("sha256").update(bytes).digest("hex") });
1621
+ }
1622
+ return { photos, rejected };
1623
+ }
1624
+ function linkKind(v) {
1625
+ if (v == null || String(v).trim() === "") return "empty";
1626
+ const s = String(v).trim();
1627
+ let u;
1628
+ try {
1629
+ u = new URL(s);
1630
+ } catch {
1631
+ return "other";
1632
+ }
1633
+ if (u.protocol !== "https:" && u.protocol !== "http:") return "other";
1634
+ if (/X-Amz-Signature=|[?&]Signature=|[?&]sig=/i.test(u.search)) return "expiring";
1635
+ if (/^easybits-public\./.test(u.hostname) || u.hostname === "easybits.cloud" || u.hostname.endsWith(".easybits.cloud")) return "permanent";
1636
+ return "external";
1637
+ }
1638
+ var PHOTO_COLUMNS = ["image_url", "photo_url", "imagen_url", "foto_url", "image", "photo", "imagen", "foto", "img"];
1639
+ function defaultPhotoColumn(columns) {
1640
+ const lower = new Map(columns.map((c) => [c.toLowerCase(), c]));
1641
+ for (const c of PHOTO_COLUMNS) if (lower.has(c)) return lower.get(c);
1642
+ return void 0;
1643
+ }
1644
+ function planPhotos(photos, current2, replace) {
1645
+ return photos.map((p) => {
1646
+ if (!current2.has(p.key)) return { key: p.key, file: p.file, action: "no_row" };
1647
+ const cur = current2.get(p.key);
1648
+ const keep = !replace && linkKind(cur) === "permanent";
1649
+ return { key: p.key, file: p.file, action: keep ? "keep" : "update", ...cur ? { current: String(cur) } : {} };
1650
+ });
1651
+ }
1652
+ var ident = (n) => `"${n.replace(/"/g, '""')}"`;
1653
+ function linkStatsSql(table2, columns) {
1654
+ const parts = columns.flatMap((c) => {
1655
+ const v = `trim(CAST(${ident(c)} AS TEXT))`;
1656
+ const url = `(${v} LIKE 'http://%' OR ${v} LIKE 'https://%')`;
1657
+ const exp = `(${v} LIKE '%X-Amz-Signature=%' OR ${v} LIKE '%?Signature=%' OR ${v} LIKE '%&Signature=%')`;
1658
+ const perm = `(${v} LIKE 'https://easybits-public.%' OR ${v} LIKE 'http%://easybits.cloud/%' OR ${v} LIKE 'http%.easybits.cloud/%')`;
1659
+ return [
1660
+ `SUM(CASE WHEN ${url} AND NOT ${exp} AND ${perm} THEN 1 ELSE 0 END)`,
1661
+ `SUM(CASE WHEN ${url} AND ${exp} THEN 1 ELSE 0 END)`,
1662
+ `SUM(CASE WHEN ${url} AND NOT ${exp} AND NOT ${perm} THEN 1 ELSE 0 END)`,
1663
+ `SUM(CASE WHEN ${ident(c)} IS NULL OR ${v} = '' THEN 1 ELSE 0 END)`
1664
+ ];
1665
+ });
1666
+ return `SELECT ${parts.join(", ")} FROM ${ident(table2)}`;
1667
+ }
1668
+ function parseLinkStats(columns, row) {
1669
+ const n = (i) => Number(row[i] ?? 0) || 0;
1670
+ 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);
1671
+ }
1672
+
1573
1673
  // src/refs.ts
1574
1674
  var ID_FORMAT = {
1575
1675
  agent: /^[0-9a-f]{24}$/i,
@@ -1609,7 +1709,8 @@ var REF_ARGS = {
1609
1709
  "domains rm": { index: 0, kind: "sandbox" },
1610
1710
  "db rm": { index: 0, kind: "db" },
1611
1711
  "db tables": { index: 0, kind: "db" },
1612
- "db query": { index: 0, kind: "db" }
1712
+ "db query": { index: 0, kind: "db" },
1713
+ "db photos": { index: 1, kind: "db" }
1613
1714
  };
1614
1715
  function matchRef(kind, ref, items) {
1615
1716
  if (looksLikeId(kind, ref)) return { ok: true, id: ref };
@@ -1731,7 +1832,7 @@ var db = {
1731
1832
  },
1732
1833
  tables: {
1733
1834
  aliases: ["inspect", "schema"],
1734
- summary: "List tables with row counts and columns",
1835
+ summary: "List tables with row counts and columns, and how many links are permanent",
1735
1836
  usage: "easybits db tables <db-id|name>",
1736
1837
  examples: ["easybits db tables leads", "easybits db tables leads --json"],
1737
1838
  // Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
@@ -1741,28 +1842,133 @@ var db = {
1741
1842
  const hit = await findDb(ctx, ref);
1742
1843
  const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
1743
1844
  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, '""')}"`;
1845
+ const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
1745
1846
  const tables = [];
1746
1847
  for (const name of names) {
1747
- const info2 = await q(`PRAGMA table_info(${ident(name)})`);
1848
+ const info2 = await q(`PRAGMA table_info(${ident2(name)})`);
1748
1849
  const col = (k) => info2.cols.indexOf(k);
1749
1850
  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 });
1851
+ const count = await q(`SELECT count(*) FROM ${ident2(name)}`);
1852
+ const textCols = columns.filter((c) => !c.type || /CHAR|TEXT|CLOB/i.test(c.type)).map((c) => c.name);
1853
+ const links = textCols.length ? parseLinkStats(textCols, (await q(linkStatsSql(name, textCols))).rows[0] ?? []) : [];
1854
+ tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns, links });
1752
1855
  }
1753
- emit(
1754
- ctx,
1755
- tables,
1756
- () => table(
1856
+ emit(ctx, tables, () => {
1857
+ table(
1757
1858
  tables.map((t2) => ({
1758
1859
  name: t2.name,
1759
1860
  rows: t2.rows,
1760
1861
  columns: t2.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
1761
1862
  })),
1762
1863
  [["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
1763
- `No tables in ${hit.name}. Create one: easybits db query ${hit.name} 'CREATE TABLE \u2026'`
1764
- )
1765
- );
1864
+ 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'`)
1865
+ );
1866
+ const withLinks = tables.flatMap((tb) => tb.links.map((l) => ({ ...l, table: tb.name })));
1867
+ if (withLinks.length) {
1868
+ console.log("\n" + t("Links (permanent = EasyBits public storage; expiring = signed URL):", "Ligas (permanente = almacenamiento p\xFAblico de EasyBits; caduca = URL firmada):"));
1869
+ table(
1870
+ withLinks.map((l) => ({ where: `${l.table}.${l.column}`, permanent: l.permanent, expiring: l.expiring, external: l.external, empty: l.empty })),
1871
+ [["where", "COLUMN"], ["permanent", "PERMANENT"], ["expiring", "EXPIRING"], ["external", "EXTERNAL"], ["empty", "EMPTY"]]
1872
+ );
1873
+ if (withLinks.some((l) => l.external + l.expiring > 0)) {
1874
+ 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/"));
1875
+ }
1876
+ }
1877
+ });
1878
+ }
1879
+ },
1880
+ photos: {
1881
+ summary: "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link",
1882
+ usage: "easybits db photos put <db> --table <table> --key-column <col> --dir <folder> [--column <col>] [--replace] [--dry-run]",
1883
+ options: {
1884
+ table: { type: "string", value: "table", description: "Table to update" },
1885
+ "key-column": { type: "string", value: "col", description: "Column that matches the file name (e.g. sku)" },
1886
+ dir: { type: "string", value: "folder", description: "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)" },
1887
+ column: { type: "string", value: "col", description: "Column for the link (default: image_url, photo_url, imagen\u2026)" },
1888
+ replace: { type: "boolean", description: "Also replace rows that already have a permanent link" },
1889
+ "dry-run": { type: "boolean", description: "Show the plan; upload nothing" }
1890
+ },
1891
+ examples: [
1892
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --dry-run",
1893
+ "easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --column foto",
1894
+ "easybits db tables catalogo # how many links are permanent, external or empty"
1895
+ ],
1896
+ async run(ctx) {
1897
+ const action = need(ctx, 0, "put", this.usage);
1898
+ if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
1899
+ const ref = need(ctx, 1, "db", this.usage);
1900
+ const tableName = str(ctx, "table");
1901
+ const keyCol = str(ctx, "key-column");
1902
+ const dir = str(ctx, "dir");
1903
+ 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);
1905
+ const { photos, rejected } = photoFiles(dir);
1906
+ const eb = await getClient(ctx);
1907
+ const hit = await findDb(ctx, ref);
1908
+ const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
1909
+ const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
1910
+ const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
1911
+ const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
1912
+ 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);
1913
+ if (!cols.includes(keyCol)) throw usageError(t(`No column "${keyCol}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${keyCol}" en ${tableName} (${cols.join(", ")}).`), this.usage);
1914
+ const column = str(ctx, "column") ?? defaultPhotoColumn(cols);
1915
+ 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);
1916
+ if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
1917
+ const current2 = /* @__PURE__ */ new Map();
1918
+ for (let i = 0; i < photos.length; i += 100) {
1919
+ const keys = photos.slice(i, i + 100).map((p) => p.key);
1920
+ const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
1921
+ for (const row of r.rows) current2.set(String(row[0]), row[1]);
1922
+ }
1923
+ const plan = planPhotos(photos, current2, bool(ctx, "replace"));
1924
+ const big = photos.filter((p) => p.bytes > WHATSAPP_MAX_BYTES).map((p) => p.file);
1925
+ const summary = { db: hit.name, table: tableName, keyColumn: keyCol, column, plan, rejected, overWhatsappLimit: big };
1926
+ const count = (a) => plan.filter((p) => p.action === a).length;
1927
+ 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");
1928
+ const printPlan2 = () => {
1929
+ for (const p of plan) {
1930
+ 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");
1931
+ console.log(`${p.action === "update" ? "~" : "!"} ${p.file} \u2192 ${keyCol}=${p.key}: ${what}`);
1932
+ }
1933
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
1934
+ 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(", ")}`));
1935
+ };
1936
+ if (bool(ctx, "dry-run") || !count("update")) {
1937
+ emit(ctx, { ...summary, dryRun: bool(ctx, "dry-run") }, () => {
1938
+ printPlan2();
1939
+ console.log(count("update") ? t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)") : t("Nothing to update.", "Nada que actualizar."));
1940
+ });
1941
+ return;
1942
+ }
1943
+ const urls = /* @__PURE__ */ new Map();
1944
+ const results = [];
1945
+ for (const item of plan.filter((p) => p.action === "update")) {
1946
+ const photo = photos.find((p) => p.key === item.key);
1947
+ try {
1948
+ let url = urls.get(photo.sha);
1949
+ if (!url) {
1950
+ 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 } });
1952
+ if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
1953
+ url = up.file.url;
1954
+ if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
1955
+ urls.set(photo.sha, url);
1956
+ }
1957
+ await q(`UPDATE ${ident2(tableName)} SET ${ident2(column)} = ? WHERE ${ident2(keyCol)} = ?`, [url, item.key]);
1958
+ results.push({ key: item.key, file: item.file, url });
1959
+ if (!ctx.json) console.log(`~ ${item.file} \u2192 ${keyCol}=${item.key}: ${url}`);
1960
+ } catch (e) {
1961
+ results.push({ key: item.key, file: item.file, error: e.message });
1962
+ if (!ctx.json) console.error(`! ${item.file}: ${e.message}`);
1963
+ }
1964
+ }
1965
+ const failed = results.filter((r) => r.error).length;
1966
+ emit(ctx, { ...summary, results }, () => {
1967
+ 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")}`);
1968
+ for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
1969
+ console.log(t(`Updated ${results.length - failed} row(s)${failed ? `, ${failed} failed` : ""}.`, `${results.length - failed} fila(s) actualizadas${failed ? `, ${failed} fallaron` : ""}.`));
1970
+ });
1971
+ if (failed) process.exitCode = 1;
1766
1972
  }
1767
1973
  },
1768
1974
  query: {
@@ -1804,7 +2010,7 @@ var db = {
1804
2010
  };
1805
2011
 
1806
2012
  // src/commands/agents.ts
1807
- import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
2013
+ import { existsSync as existsSync10, readFileSync as readFileSync11, statSync as statSync6 } from "fs";
1808
2014
 
1809
2015
  // src/api.ts
1810
2016
  import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
@@ -1834,8 +2040,8 @@ async function api(ctx, method, path, body, opts = {}) {
1834
2040
  var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
1835
2041
 
1836
2042
  // 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";
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";
1839
2045
  var WORK_DIR = "/data/work";
1840
2046
  var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
1841
2047
  var SKILLS_DIR = "/data/agent/skills";
@@ -1914,8 +2120,8 @@ function readLocalText(path, usage2) {
1914
2120
  if (process.stdin.isTTY) throw usageError(t("Expected the content on stdin.", "Se esperaba el contenido por stdin."), usage2);
1915
2121
  return readStdin().then((b) => b.toString("utf8"));
1916
2122
  }
1917
- if (!existsSync6(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
1918
- return readFileSync6(path, "utf8");
2123
+ if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2124
+ return readFileSync8(path, "utf8");
1919
2125
  }
1920
2126
  function parseMcpFile(text, usage2) {
1921
2127
  let j;
@@ -2130,8 +2336,8 @@ var files2 = {
2130
2336
  if (!rest.length) throw usageError(t("Give one or more local files.", "Pasa uno o m\xE1s archivos locales."), FILES_USAGE2);
2131
2337
  const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
2132
2338
  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 };
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 };
2135
2341
  });
2136
2342
  if (bool(ctx, "dry-run")) {
2137
2343
  emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
@@ -2142,7 +2348,7 @@ var files2 = {
2142
2348
  }
2143
2349
  const done = [];
2144
2350
  for (const p of plan) {
2145
- const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync6(p.local), { raw: true });
2351
+ const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync8(p.local), { raw: true });
2146
2352
  done.push({ local: p.local, ...r });
2147
2353
  if (!ctx.json) console.log(`${t("Uploaded", "Subido")} ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2148
2354
  }
@@ -2168,11 +2374,11 @@ var SKILLS_USAGE = "easybits agents skills <ls|get|add|rm> <agent> [slug...] [--
2168
2374
  function readSkillDir(dir) {
2169
2375
  const assets = [];
2170
2376
  const visit = (d) => {
2171
- for (const e of readdirSync(d, { withFileTypes: true })) {
2377
+ for (const e of readdirSync2(d, { withFileTypes: true })) {
2172
2378
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2173
- const p = join3(d, e.name);
2379
+ const p = join4(d, e.name);
2174
2380
  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") });
2381
+ else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync8(p).toString("base64") });
2176
2382
  }
2177
2383
  };
2178
2384
  visit(dir);
@@ -2225,19 +2431,19 @@ var skills = {
2225
2431
  }
2226
2432
  const s = await readSkill(box, entry);
2227
2433
  mkdirSync3(out, { recursive: true });
2228
- writeFileSync4(join3(out, "SKILL.md"), s.markdown);
2434
+ writeFileSync4(join4(out, "SKILL.md"), s.markdown);
2229
2435
  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"));
2436
+ mkdirSync3(dirname(join4(out, a.name)), { recursive: true });
2437
+ writeFileSync4(join4(out, a.name), Buffer.from(a.contentBase64, "base64"));
2232
2438
  }
2233
2439
  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
2440
  } else if (action === "add" || action === "put") {
2235
2441
  const file = str(ctx, "file");
2236
2442
  const dir = str(ctx, "dir");
2237
2443
  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");
2444
+ 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");
2241
2447
  const fm = skillFrontmatter(markdown);
2242
2448
  if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
2243
2449
  const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename2(resolve(dir ?? dirname(mdPath))).toLowerCase());
@@ -2497,10 +2703,10 @@ var exportAgent = {
2497
2703
  }
2498
2704
  };
2499
2705
  function readExport(path, usage2) {
2500
- if (!existsSync6(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2706
+ if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2501
2707
  let x;
2502
2708
  try {
2503
- x = JSON.parse(readFileSync6(path, "utf8"));
2709
+ x = JSON.parse(readFileSync8(path, "utf8"));
2504
2710
  } catch (e) {
2505
2711
  throw usageError(t(`${path} is not JSON: ${e.message}`, `${path} no es JSON: ${e.message}`), usage2);
2506
2712
  }
@@ -2545,37 +2751,37 @@ async function applyExport(ctx, id, x, opts) {
2545
2751
  }
2546
2752
 
2547
2753
  // src/commands/spec.ts
2548
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
2754
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
2549
2755
  import { homedir as homedir3 } from "os";
2550
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
2756
+ import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
2551
2757
 
2552
2758
  // 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";
2759
+ import { existsSync as existsSync8, readFileSync as readFileSync9, readdirSync as readdirSync3 } from "fs";
2760
+ import { join as join5, relative as relative2 } from "path";
2555
2761
  function walkLocal(root) {
2556
2762
  const out = [];
2557
2763
  const visit = (d) => {
2558
- for (const e of readdirSync2(d, { withFileTypes: true })) {
2764
+ for (const e of readdirSync3(d, { withFileTypes: true })) {
2559
2765
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2560
- const p = join4(d, e.name);
2766
+ const p = join5(d, e.name);
2561
2767
  if (e.isDirectory()) visit(p);
2562
2768
  else if (e.isFile()) out.push(relative2(root, p).split("\\").join("/"));
2563
2769
  }
2564
2770
  };
2565
- if (existsSync7(root)) visit(root);
2771
+ if (existsSync8(root)) visit(root);
2566
2772
  return out;
2567
2773
  }
2568
2774
  function readUploads(dir) {
2569
2775
  const skills2 = {};
2570
- const skillsRoot = join4(dir, "skills");
2571
- if (existsSync7(skillsRoot)) {
2572
- for (const e of readdirSync2(skillsRoot, { withFileTypes: true })) {
2776
+ const skillsRoot = join5(dir, "skills");
2777
+ if (existsSync8(skillsRoot)) {
2778
+ for (const e of readdirSync3(skillsRoot, { withFileTypes: true })) {
2573
2779
  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") }));
2780
+ skills2[e.name] = walkLocal(join5(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync9(join5(skillsRoot, e.name, path)).toString("base64") }));
2575
2781
  }
2576
2782
  }
2577
- const filesRoot = join4(dir, "files");
2578
- const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync7(join4(filesRoot, p)).toString("base64")]));
2783
+ const filesRoot = join5(dir, "files");
2784
+ const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync9(join5(filesRoot, p)).toString("base64")]));
2579
2785
  return { ...Object.keys(skills2).length ? { skills: skills2 } : {}, ...Object.keys(files3).length ? { files: files3 } : {} };
2580
2786
  }
2581
2787
  function secretsFor(text, env = process.env) {
@@ -2628,17 +2834,17 @@ Ed\xEDtalo y luego: easybits apply ${out} --dry-run`)));
2628
2834
  const yaml = await getYaml();
2629
2835
  const dir = resolve2(out);
2630
2836
  mkdirSync4(dir, { recursive: true });
2631
- writeFileSync5(join5(dir, "agent.yaml"), yaml);
2837
+ writeFileSync5(join6(dir, "agent.yaml"), yaml);
2632
2838
  const x = await buildExport(ctx, id, { secrets: false, fileContents: true });
2633
2839
  const put = (path, b64) => {
2634
2840
  mkdirSync4(dirname2(path), { recursive: true });
2635
2841
  writeFileSync5(path, Buffer.from(b64, "base64"));
2636
2842
  };
2637
2843
  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);
2844
+ writeFileSync5((mkdirSync4(join6(dir, "skills", s.slug), { recursive: true }), join6(dir, "skills", s.slug, "SKILL.md")), s.markdown);
2845
+ for (const a of s.assets) put(join6(dir, "skills", s.slug, a.name), a.contentBase64);
2640
2846
  }
2641
- for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join5(dir, "files", f.path), f.contentBase64);
2847
+ for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join6(dir, "files", f.path), f.contentBase64);
2642
2848
  const summary = { saved: dir, skills: (x.skills ?? []).map((s) => s.slug), files: (x.files ?? []).map((f) => f.path) };
2643
2849
  emit(
2644
2850
  ctx,
@@ -2651,20 +2857,20 @@ ${t("Edit it, then", "Ed\xEDtalo y luego")}: easybits apply ${out} --dry-run`
2651
2857
  }
2652
2858
  };
2653
2859
  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)));
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)));
2657
2863
  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 };
2864
+ return { text: readFileSync10(join6(target, name), "utf8"), dir: target };
2659
2865
  }
2660
- return { text: readFileSync8(target, "utf8") };
2866
+ return { text: readFileSync10(target, "utf8") };
2661
2867
  }
2662
2868
  var line = (c) => `${c.op} ${c.what}`;
2663
2869
  var printPlan = (plan) => console.log(plan.map(line).join("\n"));
2664
2870
  function saveBackup(id, yaml) {
2665
- const dir = join5(homedir3(), ".easybits", "backups");
2871
+ const dir = join6(homedir3(), ".easybits", "backups");
2666
2872
  mkdirSync4(dir, { recursive: true });
2667
- const path = join5(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
2873
+ const path = join6(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
2668
2874
  writeFileSync5(path, yaml);
2669
2875
  return path;
2670
2876
  }
@@ -2778,7 +2984,7 @@ var apply = {
2778
2984
  // src/commands/agents.ts
2779
2985
  function isLegacyExport(path) {
2780
2986
  try {
2781
- return existsSync9(path) && !statSync4(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync9(path, "utf8"));
2987
+ return existsSync10(path) && !statSync6(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync11(path, "utf8"));
2782
2988
  } catch {
2783
2989
  return false;
2784
2990
  }
@@ -2845,9 +3051,9 @@ var agents = {
2845
3051
  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
3052
  const mode = str(ctx, "prompt-mode");
2847
3053
  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);
3054
+ if (promptFile && !existsSync10(promptFile)) throw usageError(t(`File not found: ${promptFile}`, `No existe el archivo: ${promptFile}`), this.usage);
2849
3055
  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);
3056
+ if (mcpFile && !existsSync10(mcpFile)) throw usageError(t(`File not found: ${mcpFile}`, `No existe el archivo: ${mcpFile}`), this.usage);
2851
3057
  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
3058
  if (from && !isLegacyExport(from)) {
2853
3059
  await applyTarget(ctx, from, { create: true, name: str(ctx, "name"), dryRun: bool(ctx, "dry-run"), usage: this.usage });
@@ -2857,7 +3063,7 @@ var agents = {
2857
3063
  const template = str(ctx, "template") ?? source?.template;
2858
3064
  if (!template) throw usageError(t("Missing --template (or --like / --from).", "Falta --template (o --like / --from)."), this.usage);
2859
3065
  const env = await envFrom(ctx, this.usage);
2860
- const prompt = promptInline ?? (promptFile ? readFileSync9(promptFile, "utf8") : source?.systemPrompt);
3066
+ const prompt = promptInline ?? (promptFile ? readFileSync11(promptFile, "utf8") : source?.systemPrompt);
2861
3067
  const promptMode = mode ?? source?.systemPromptMode;
2862
3068
  const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
2863
3069
  const promptAfter = multiline ? prompt : void 0;
@@ -2866,7 +3072,7 @@ var agents = {
2866
3072
  }
2867
3073
  if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
2868
3074
  if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
2869
- const mcpServers = mcpFile ? parseMcpFile(readFileSync9(mcpFile, "utf8"), this.usage) : source?.mcpServers;
3075
+ const mcpServers = mcpFile ? parseMcpFile(readFileSync11(mcpFile, "utf8"), this.usage) : source?.mcpServers;
2870
3076
  if (mcpServers && hasMasked(mcpServers)) {
2871
3077
  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
3078
  }
@@ -2958,9 +3164,9 @@ var agents = {
2958
3164
  };
2959
3165
 
2960
3166
  // src/commands/completion.ts
2961
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
3167
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
2962
3168
  import { homedir as homedir4 } from "os";
2963
- import { dirname as dirname3, join as join6 } from "path";
3169
+ import { dirname as dirname3, join as join7 } from "path";
2964
3170
  var GLOBAL_FLAGS = ["--json", "--token", "--lang", "--help", "--version"];
2965
3171
  var AGENT_FLAGS = /* @__PURE__ */ new Set(["--agent", "--like"]);
2966
3172
  var findSub = (c, name) => Object.entries(c.subs ?? {}).find(([n, l]) => n === name || l.aliases?.includes(name));
@@ -2997,11 +3203,11 @@ function completionFor(words, current2, commands) {
2997
3203
  if (spec && spec.index === n) return { kind: "names", ref: spec.kind };
2998
3204
  return { kind: "words", words: [] };
2999
3205
  }
3000
- var cacheFile = () => join6(process.env.XDG_CACHE_HOME || join6(homedir4(), ".cache"), "easybits", "complete.json");
3206
+ var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "easybits", "complete.json");
3001
3207
  async function namesOf(ctx, kind) {
3002
3208
  let cache2 = {};
3003
3209
  try {
3004
- cache2 = JSON.parse(readFileSync10(cacheFile(), "utf8"));
3210
+ cache2 = JSON.parse(readFileSync12(cacheFile(), "utf8"));
3005
3211
  const c = cache2[kind];
3006
3212
  if (c && Date.now() - c.at < 5 * 6e4) return c.names;
3007
3213
  } catch {
@@ -3128,16 +3334,16 @@ var docs = {
3128
3334
  };
3129
3335
 
3130
3336
  // src/commands/doctor.ts
3131
- import { existsSync as existsSync10, statSync as statSync5 } from "fs";
3337
+ import { existsSync as existsSync11, statSync as statSync7 } from "fs";
3132
3338
  import { homedir as homedir6 } from "os";
3133
- import { join as join8 } from "path";
3339
+ import { join as join9 } from "path";
3134
3340
  import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
3135
3341
 
3136
3342
  // src/update.ts
3137
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
3343
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
3138
3344
  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");
3345
+ import { dirname as dirname4, join as join8 } from "path";
3346
+ var FILE = join8(process.env.XDG_CONFIG_HOME || join8(homedir5(), ".config"), "easybits", "update.json");
3141
3347
  var DAY = 24 * 60 * 60 * 1e3;
3142
3348
  var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
3143
3349
  function newer(a, b) {
@@ -3149,7 +3355,7 @@ function newer(a, b) {
3149
3355
  var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
3150
3356
  function readCache() {
3151
3357
  try {
3152
- return JSON.parse(readFileSync11(FILE, "utf8"));
3358
+ return JSON.parse(readFileSync13(FILE, "utf8"));
3153
3359
  } catch {
3154
3360
  return {};
3155
3361
  }
@@ -3184,7 +3390,7 @@ async function updateNotice(current2) {
3184
3390
  }
3185
3391
 
3186
3392
  // src/commands/doctor.ts
3187
- var VERSION = true ? "0.12.0" : "dev";
3393
+ var VERSION = true ? "0.13.0" : "dev";
3188
3394
  var doctor2 = {
3189
3395
  name: "doctor",
3190
3396
  group: "Account",
@@ -3227,9 +3433,9 @@ var doctor2 = {
3227
3433
  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
3434
  }
3229
3435
  }
3230
- const rcPath = join8(homedir6(), ".easybitsrc");
3231
- if (existsSync10(rcPath)) {
3232
- const mode = statSync5(rcPath).mode & 511;
3436
+ const rcPath = join9(homedir6(), ".easybitsrc");
3437
+ if (existsSync11(rcPath)) {
3438
+ const mode = statSync7(rcPath).mode & 511;
3233
3439
  const ok2 = (mode & 63) === 0;
3234
3440
  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
3441
  }
@@ -3439,7 +3645,7 @@ var HELP_ES = {
3439
3645
  "Delete a database (irreversible)": "Borra una base de datos (irreversible)",
3440
3646
  "asks you to type the name": "te pide teclear el nombre",
3441
3647
  "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",
3648
+ "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
3649
  "Run one SQL statement (by database id or name)": "Corre una sentencia SQL (por id o nombre de la base)",
3444
3650
  "Positional ? parameter (repeatable)": "Par\xE1metro ? posicional (repetible)",
3445
3651
  "Storage": "Almacenamiento",
@@ -3466,7 +3672,15 @@ var HELP_ES = {
3466
3672
  "Shell completion script (zsh, bash, fish)": "Script de autocompletado para tu shell (zsh, bash, fish)",
3467
3673
  "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
3674
  "add to ~/.zshrc, after compinit": "agr\xE9galo a ~/.zshrc, despu\xE9s de compinit",
3469
- "add to ~/.bashrc": "agr\xE9galo a ~/.bashrc"
3675
+ "add to ~/.bashrc": "agr\xE9galo a ~/.bashrc",
3676
+ "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",
3677
+ "Table to update": "Tabla a actualizar",
3678
+ "Column that matches the file name (e.g. sku)": "Columna que coincide con el nombre del archivo (p. ej. sku)",
3679
+ "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)",
3680
+ "Column for the link (default: image_url, photo_url, imagen\u2026)": "Columna de la liga (default: image_url, photo_url, imagen\u2026)",
3681
+ "Also replace rows that already have a permanent link": "Reemplaza tambi\xE9n las filas que ya tienen liga permanente",
3682
+ "Show the plan; upload nothing": "Ense\xF1a el plan; no sube nada",
3683
+ "how many links are permanent, external or empty": "cu\xE1ntas ligas son permanentes, de fuera o vac\xEDas"
3470
3684
  };
3471
3685
 
3472
3686
  // src/help.ts
@@ -3657,7 +3871,7 @@ function normalizeArgs(input) {
3657
3871
  }
3658
3872
 
3659
3873
  // src/index.ts
3660
- var VERSION2 = true ? "0.12.0" : "dev";
3874
+ var VERSION2 = true ? "0.13.0" : "dev";
3661
3875
  var GLOBAL_OPTIONS = {
3662
3876
  json: { type: "boolean" },
3663
3877
  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.13.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": {