@easybits.cloud/cli 0.11.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.
- package/README.md +17 -1
- package/dist/index.js +429 -85
- 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
|
|
|
@@ -119,6 +120,8 @@ easybits docs --open # open the docs in your browser
|
|
|
119
120
|
pass its name instead of the id (`easybits sb exec scratch -- ls`, `easybits domains ls shop`).
|
|
120
121
|
The match is exact and case-insensitive; if two share the name the CLI exits 2 and lists the ids.
|
|
121
122
|
|
|
123
|
+
Shell completion: `source <(easybits completion zsh)` (or `bash`, `fish`).
|
|
124
|
+
|
|
122
125
|
Help and messages follow your locale (`LANG=es_*` → Spanish) or `--lang es|en`.
|
|
123
126
|
|
|
124
127
|
Every command has help with examples: `easybits <command> <subcommand> --help`
|
|
@@ -161,6 +164,19 @@ Host *.ghosty
|
|
|
161
164
|
|
|
162
165
|
Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
|
|
163
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
|
+
|
|
175
|
+
## Changes in 0.12.0
|
|
176
|
+
|
|
177
|
+
- `easybits completion zsh|bash|fish`: TAB completes commands, subcommands, flags and the names of
|
|
178
|
+
your agents, sandboxes and databases (cached 5 min in `~/.cache/easybits/`).
|
|
179
|
+
|
|
164
180
|
## Changes in 0.11.0
|
|
165
181
|
|
|
166
182
|
- **Spanish or English, following your locale** (`LANG=es_*` → Spanish; otherwise English), or
|
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
|
|
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(${
|
|
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 ${
|
|
1751
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1838
|
-
import { basename as basename2, dirname, join as
|
|
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 (!
|
|
1918
|
-
return
|
|
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 (!
|
|
2134
|
-
return { local, path: to ? `${to}/${basename2(local)}` : basename2(local), bytes:
|
|
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)}`,
|
|
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
|
|
2377
|
+
for (const e of readdirSync2(d, { withFileTypes: true })) {
|
|
2172
2378
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2173
|
-
const p =
|
|
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:
|
|
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(
|
|
2434
|
+
writeFileSync4(join4(out, "SKILL.md"), s.markdown);
|
|
2229
2435
|
for (const a of s.assets) {
|
|
2230
|
-
mkdirSync3(dirname(
|
|
2231
|
-
writeFileSync4(
|
|
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 ??
|
|
2239
|
-
if (!
|
|
2240
|
-
const markdown =
|
|
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 (!
|
|
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(
|
|
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
|
|
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
|
|
2756
|
+
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
2551
2757
|
|
|
2552
2758
|
// src/specfile.ts
|
|
2553
|
-
import { existsSync as
|
|
2554
|
-
import { join as
|
|
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
|
|
2764
|
+
for (const e of readdirSync3(d, { withFileTypes: true })) {
|
|
2559
2765
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2560
|
-
const p =
|
|
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 (
|
|
2771
|
+
if (existsSync8(root)) visit(root);
|
|
2566
2772
|
return out;
|
|
2567
2773
|
}
|
|
2568
2774
|
function readUploads(dir) {
|
|
2569
2775
|
const skills2 = {};
|
|
2570
|
-
const skillsRoot =
|
|
2571
|
-
if (
|
|
2572
|
-
for (const e of
|
|
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(
|
|
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 =
|
|
2578
|
-
const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p,
|
|
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(
|
|
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(
|
|
2639
|
-
for (const a of s.assets) put(
|
|
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(
|
|
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 (!
|
|
2655
|
-
if (
|
|
2656
|
-
const name = SPEC_NAMES.find((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:
|
|
2864
|
+
return { text: readFileSync10(join6(target, name), "utf8"), dir: target };
|
|
2659
2865
|
}
|
|
2660
|
-
return { text:
|
|
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 =
|
|
2871
|
+
const dir = join6(homedir3(), ".easybits", "backups");
|
|
2666
2872
|
mkdirSync4(dir, { recursive: true });
|
|
2667
|
-
const path =
|
|
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
|
|
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 && !
|
|
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 && !
|
|
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 ?
|
|
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(
|
|
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
|
}
|
|
@@ -2957,6 +3163,130 @@ var agents = {
|
|
|
2957
3163
|
}
|
|
2958
3164
|
};
|
|
2959
3165
|
|
|
3166
|
+
// src/commands/completion.ts
|
|
3167
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
|
|
3168
|
+
import { homedir as homedir4 } from "os";
|
|
3169
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
3170
|
+
var GLOBAL_FLAGS = ["--json", "--token", "--lang", "--help", "--version"];
|
|
3171
|
+
var AGENT_FLAGS = /* @__PURE__ */ new Set(["--agent", "--like"]);
|
|
3172
|
+
var findSub = (c, name) => Object.entries(c.subs ?? {}).find(([n, l]) => n === name || l.aliases?.includes(name));
|
|
3173
|
+
function completionFor(words, current2, commands) {
|
|
3174
|
+
const names = commands.filter((c) => !c.name.startsWith("__")).map((c) => c.name);
|
|
3175
|
+
const prev = words[words.length - 1];
|
|
3176
|
+
if (prev && AGENT_FLAGS.has(prev)) return { kind: "names", ref: "agent" };
|
|
3177
|
+
if (!words.length) return { kind: "words", words: [...names, "help"] };
|
|
3178
|
+
const find = (n2) => commands.find((c) => c.name === n2 || c.aliases?.includes(n2));
|
|
3179
|
+
const cmd = find(words[0]);
|
|
3180
|
+
if (words[0] === "help") return { kind: "words", words: words.length === 1 ? names : Object.keys(find(words[1])?.subs ?? {}) };
|
|
3181
|
+
if (!cmd) return { kind: "words", words: [] };
|
|
3182
|
+
const sub = cmd.subs && words[1] ? findSub(cmd, words[1]) : void 0;
|
|
3183
|
+
const leaf = sub?.[1] ?? cmd.leaf;
|
|
3184
|
+
if (current2.startsWith("-")) {
|
|
3185
|
+
const own = Object.keys(leaf?.options ?? {}).map((n2) => `--${n2}`);
|
|
3186
|
+
return { kind: "words", words: [...own, ...GLOBAL_FLAGS] };
|
|
3187
|
+
}
|
|
3188
|
+
if (cmd.subs && !sub) return { kind: "words", words: words.length === 1 ? Object.keys(cmd.subs) : [] };
|
|
3189
|
+
if (cmd.name === "completion") return { kind: "words", words: words.length === 1 ? ["zsh", "bash", "fish"] : [] };
|
|
3190
|
+
const opts = leaf?.options ?? {};
|
|
3191
|
+
let n = 0;
|
|
3192
|
+
for (let i = sub ? 2 : 1; i < words.length; i++) {
|
|
3193
|
+
const w = words[i];
|
|
3194
|
+
if (w.startsWith("--")) {
|
|
3195
|
+
const o = opts[w.slice(2)];
|
|
3196
|
+
if ((o?.type === "string" || w === "--token" || w === "--lang") && !w.includes("=")) i++;
|
|
3197
|
+
continue;
|
|
3198
|
+
}
|
|
3199
|
+
if (w.startsWith("-") && w !== "-") continue;
|
|
3200
|
+
n++;
|
|
3201
|
+
}
|
|
3202
|
+
const spec = REF_ARGS[sub ? `${cmd.name} ${sub[0]}` : cmd.name];
|
|
3203
|
+
if (spec && spec.index === n) return { kind: "names", ref: spec.kind };
|
|
3204
|
+
return { kind: "words", words: [] };
|
|
3205
|
+
}
|
|
3206
|
+
var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "easybits", "complete.json");
|
|
3207
|
+
async function namesOf(ctx, kind) {
|
|
3208
|
+
let cache2 = {};
|
|
3209
|
+
try {
|
|
3210
|
+
cache2 = JSON.parse(readFileSync12(cacheFile(), "utf8"));
|
|
3211
|
+
const c = cache2[kind];
|
|
3212
|
+
if (c && Date.now() - c.at < 5 * 6e4) return c.names;
|
|
3213
|
+
} catch {
|
|
3214
|
+
}
|
|
3215
|
+
const names = [...new Set((await listRefs(ctx, kind)).map((x) => x.name).filter((x) => !!x))];
|
|
3216
|
+
try {
|
|
3217
|
+
mkdirSync5(dirname3(cacheFile()), { recursive: true });
|
|
3218
|
+
writeFileSync6(cacheFile(), JSON.stringify({ ...cache2, [kind]: { at: Date.now(), names } }));
|
|
3219
|
+
} catch {
|
|
3220
|
+
}
|
|
3221
|
+
return names;
|
|
3222
|
+
}
|
|
3223
|
+
async function completeCmd(args, commands) {
|
|
3224
|
+
const rest = args[0] === "--" ? args.slice(1) : args;
|
|
3225
|
+
const current2 = rest.length ? rest[rest.length - 1] : "";
|
|
3226
|
+
const words = rest.slice(0, -1);
|
|
3227
|
+
let out = [];
|
|
3228
|
+
try {
|
|
3229
|
+
const want = completionFor(words, current2, commands);
|
|
3230
|
+
const ctx = { json: true, args: [], opts: {} };
|
|
3231
|
+
out = want.kind === "words" ? want.words : await namesOf(ctx, want.ref);
|
|
3232
|
+
} catch {
|
|
3233
|
+
out = [];
|
|
3234
|
+
}
|
|
3235
|
+
const cur = current2.toLowerCase();
|
|
3236
|
+
process.stdout.write(out.filter((w) => w.toLowerCase().startsWith(cur)).join("\n") + "\n");
|
|
3237
|
+
}
|
|
3238
|
+
function completionScript(shell) {
|
|
3239
|
+
if (shell === "bash") {
|
|
3240
|
+
return `# easybits \u2014 bash completion: eval "$(easybits completion bash)" en ~/.bashrc
|
|
3241
|
+
_easybits() {
|
|
3242
|
+
# Las palabras ANTES de cambiar IFS: el bash 3.2 de macOS junta \${arr[@]:1:n} con un IFS propio.
|
|
3243
|
+
local words=("\${COMP_WORDS[@]:1:COMP_CWORD-1}") cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
3244
|
+
local IFS=$'\\n'
|
|
3245
|
+
COMPREPLY=( $(easybits __complete -- "\${words[@]}" "$cur" 2>/dev/null) )
|
|
3246
|
+
}
|
|
3247
|
+
complete -o default -F _easybits easybits
|
|
3248
|
+
`;
|
|
3249
|
+
}
|
|
3250
|
+
if (shell === "zsh") {
|
|
3251
|
+
return `#compdef easybits
|
|
3252
|
+
# easybits \u2014 zsh completion: source <(easybits completion zsh) en ~/.zshrc (despu\xE9s de compinit)
|
|
3253
|
+
_easybits() {
|
|
3254
|
+
local -a opts
|
|
3255
|
+
opts=("\${(@f)$(easybits __complete -- "\${(@)words[2,CURRENT-1]}" "\${words[CURRENT]}" 2>/dev/null)}")
|
|
3256
|
+
compadd -a opts
|
|
3257
|
+
}
|
|
3258
|
+
compdef _easybits easybits
|
|
3259
|
+
`;
|
|
3260
|
+
}
|
|
3261
|
+
if (shell === "fish") {
|
|
3262
|
+
return `# easybits \u2014 fish completion: easybits completion fish > ~/.config/fish/completions/easybits.fish
|
|
3263
|
+
complete -c easybits -f -a "(easybits __complete -- (commandline -opc)[2..-1] (commandline -ct) 2>/dev/null)"
|
|
3264
|
+
`;
|
|
3265
|
+
}
|
|
3266
|
+
return null;
|
|
3267
|
+
}
|
|
3268
|
+
var completion = {
|
|
3269
|
+
name: "completion",
|
|
3270
|
+
group: "Help",
|
|
3271
|
+
summary: "Shell completion script (zsh, bash, fish)",
|
|
3272
|
+
synopsis: "completion <shell>",
|
|
3273
|
+
leaf: {
|
|
3274
|
+
summary: "Print the completion script for your shell: commands, flags and your agents, sandboxes and databases",
|
|
3275
|
+
usage: "easybits completion <zsh|bash|fish>",
|
|
3276
|
+
examples: [
|
|
3277
|
+
"source <(easybits completion zsh) # add to ~/.zshrc, after compinit",
|
|
3278
|
+
'eval "$(easybits completion bash)" # add to ~/.bashrc',
|
|
3279
|
+
"easybits completion fish > ~/.config/fish/completions/easybits.fish"
|
|
3280
|
+
],
|
|
3281
|
+
async run(ctx) {
|
|
3282
|
+
const shell = need(ctx, 0, "zsh|bash|fish", this.usage);
|
|
3283
|
+
const script = completionScript(shell);
|
|
3284
|
+
if (!script) throw usageError(t(`Unknown shell "${shell}".`, `Shell desconocido "${shell}".`), this.usage);
|
|
3285
|
+
process.stdout.write(script);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
|
|
2960
3290
|
// src/commands/docs.ts
|
|
2961
3291
|
import { spawn as spawn2 } from "child_process";
|
|
2962
3292
|
import { resolveBaseUrl as resolveBaseUrl5 } from "@easybits.cloud/sdk";
|
|
@@ -3004,16 +3334,16 @@ var docs = {
|
|
|
3004
3334
|
};
|
|
3005
3335
|
|
|
3006
3336
|
// src/commands/doctor.ts
|
|
3007
|
-
import { existsSync as
|
|
3008
|
-
import { homedir as
|
|
3009
|
-
import { join as
|
|
3337
|
+
import { existsSync as existsSync11, statSync as statSync7 } from "fs";
|
|
3338
|
+
import { homedir as homedir6 } from "os";
|
|
3339
|
+
import { join as join9 } from "path";
|
|
3010
3340
|
import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
|
|
3011
3341
|
|
|
3012
3342
|
// src/update.ts
|
|
3013
|
-
import { mkdirSync as
|
|
3014
|
-
import { homedir as
|
|
3015
|
-
import { dirname as
|
|
3016
|
-
var FILE =
|
|
3343
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
|
|
3344
|
+
import { homedir as homedir5 } from "os";
|
|
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");
|
|
3017
3347
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
3018
3348
|
var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
|
|
3019
3349
|
function newer(a, b) {
|
|
@@ -3025,7 +3355,7 @@ function newer(a, b) {
|
|
|
3025
3355
|
var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
|
|
3026
3356
|
function readCache() {
|
|
3027
3357
|
try {
|
|
3028
|
-
return JSON.parse(
|
|
3358
|
+
return JSON.parse(readFileSync13(FILE, "utf8"));
|
|
3029
3359
|
} catch {
|
|
3030
3360
|
return {};
|
|
3031
3361
|
}
|
|
@@ -3035,8 +3365,8 @@ async function fetchLatest(timeoutMs = 1500) {
|
|
|
3035
3365
|
const r = await fetch("https://registry.npmjs.org/@easybits.cloud/cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
|
|
3036
3366
|
const latest = (await r.json()).version;
|
|
3037
3367
|
if (!latest) return null;
|
|
3038
|
-
|
|
3039
|
-
|
|
3368
|
+
mkdirSync6(dirname4(FILE), { recursive: true });
|
|
3369
|
+
writeFileSync7(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
|
|
3040
3370
|
return latest;
|
|
3041
3371
|
} catch {
|
|
3042
3372
|
return null;
|
|
@@ -3060,7 +3390,7 @@ async function updateNotice(current2) {
|
|
|
3060
3390
|
}
|
|
3061
3391
|
|
|
3062
3392
|
// src/commands/doctor.ts
|
|
3063
|
-
var VERSION = true ? "0.
|
|
3393
|
+
var VERSION = true ? "0.13.0" : "dev";
|
|
3064
3394
|
var doctor2 = {
|
|
3065
3395
|
name: "doctor",
|
|
3066
3396
|
group: "Account",
|
|
@@ -3103,9 +3433,9 @@ var doctor2 = {
|
|
|
3103
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") });
|
|
3104
3434
|
}
|
|
3105
3435
|
}
|
|
3106
|
-
const rcPath =
|
|
3107
|
-
if (
|
|
3108
|
-
const mode =
|
|
3436
|
+
const rcPath = join9(homedir6(), ".easybitsrc");
|
|
3437
|
+
if (existsSync11(rcPath)) {
|
|
3438
|
+
const mode = statSync7(rcPath).mode & 511;
|
|
3109
3439
|
const ok2 = (mode & 63) === 0;
|
|
3110
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}`) } });
|
|
3111
3441
|
}
|
|
@@ -3142,7 +3472,8 @@ var COMMANDS = [
|
|
|
3142
3472
|
mcp,
|
|
3143
3473
|
sshKey,
|
|
3144
3474
|
sshProxy,
|
|
3145
|
-
docs
|
|
3475
|
+
docs,
|
|
3476
|
+
completion
|
|
3146
3477
|
];
|
|
3147
3478
|
function findCommand(name) {
|
|
3148
3479
|
return COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
|
|
@@ -3314,7 +3645,7 @@ var HELP_ES = {
|
|
|
3314
3645
|
"Delete a database (irreversible)": "Borra una base de datos (irreversible)",
|
|
3315
3646
|
"asks you to type the name": "te pide teclear el nombre",
|
|
3316
3647
|
"scripts and agents": "scripts y agentes",
|
|
3317
|
-
"List tables with row counts and columns": "Enlista las tablas con
|
|
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",
|
|
3318
3649
|
"Run one SQL statement (by database id or name)": "Corre una sentencia SQL (por id o nombre de la base)",
|
|
3319
3650
|
"Positional ? parameter (repeatable)": "Par\xE1metro ? posicional (repetible)",
|
|
3320
3651
|
"Storage": "Almacenamiento",
|
|
@@ -3337,7 +3668,19 @@ var HELP_ES = {
|
|
|
3337
3668
|
"Help": "Ayuda",
|
|
3338
3669
|
"Print the docs URL, or a docs section as markdown": "Imprime la URL de los docs, o una secci\xF3n de los docs en markdown",
|
|
3339
3670
|
"English docs": "Docs en ingl\xE9s",
|
|
3340
|
-
"Open the docs in your browser": "Abre los docs en tu navegador"
|
|
3671
|
+
"Open the docs in your browser": "Abre los docs en tu navegador",
|
|
3672
|
+
"Shell completion script (zsh, bash, fish)": "Script de autocompletado para tu shell (zsh, bash, fish)",
|
|
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",
|
|
3674
|
+
"add to ~/.zshrc, after compinit": "agr\xE9galo a ~/.zshrc, despu\xE9s de compinit",
|
|
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"
|
|
3341
3684
|
};
|
|
3342
3685
|
|
|
3343
3686
|
// src/help.ts
|
|
@@ -3528,7 +3871,7 @@ function normalizeArgs(input) {
|
|
|
3528
3871
|
}
|
|
3529
3872
|
|
|
3530
3873
|
// src/index.ts
|
|
3531
|
-
var VERSION2 = true ? "0.
|
|
3874
|
+
var VERSION2 = true ? "0.13.0" : "dev";
|
|
3532
3875
|
var GLOBAL_OPTIONS = {
|
|
3533
3876
|
json: { type: "boolean" },
|
|
3534
3877
|
token: { type: "string" },
|
|
@@ -3572,7 +3915,7 @@ function parse(argv2, skip, leaf) {
|
|
|
3572
3915
|
throw usageError(e.message.split("\n")[0].split(". To specify")[0].replace(/\.?$/, "."), leaf?.usage);
|
|
3573
3916
|
}
|
|
3574
3917
|
}
|
|
3575
|
-
function
|
|
3918
|
+
function findSub2(cmd, name) {
|
|
3576
3919
|
return Object.entries(cmd.subs ?? {}).find(([n, l]) => n === name || l.aliases?.includes(name));
|
|
3577
3920
|
}
|
|
3578
3921
|
function unknownCommand(name) {
|
|
@@ -3586,7 +3929,7 @@ ${t("Run", "Corre")}: easybits --help`;
|
|
|
3586
3929
|
function unknownSub(cmd, name) {
|
|
3587
3930
|
const e = usageError(t(`Unknown subcommand "${cmd.name} ${name}".`, `Subcomando desconocido "${cmd.name} ${name}".`), `easybits ${cmd.name} <${Object.keys(cmd.subs ?? {}).join("|")}>`);
|
|
3588
3931
|
const names = Object.entries(cmd.subs ?? {}).flatMap(([n, l]) => [n, ...l.aliases ?? []]);
|
|
3589
|
-
const hint = didYouMean(name, names, `easybits ${cmd.name} `, (n) =>
|
|
3932
|
+
const hint = didYouMean(name, names, `easybits ${cmd.name} `, (n) => findSub2(cmd, n)?.[0] ?? n);
|
|
3590
3933
|
if (hint) e.hint = `${localHint(hint)}
|
|
3591
3934
|
${e.hint}`;
|
|
3592
3935
|
return e;
|
|
@@ -3600,6 +3943,7 @@ function aliasNote([en, es]) {
|
|
|
3600
3943
|
}
|
|
3601
3944
|
var localHint = (hint) => hint.replace("Did you mean this?", t("Did you mean this?", "\xBFQuisiste decir esto?"));
|
|
3602
3945
|
async function main(input) {
|
|
3946
|
+
if (input[0] === "__complete") return completeCmd(input.slice(1), COMMANDS);
|
|
3603
3947
|
const { argv: argv2, notes } = normalizeArgs(input);
|
|
3604
3948
|
if (!wantsJson(argv2)) notes.forEach(aliasNote);
|
|
3605
3949
|
const pos = positionalIndexes(argv2);
|
|
@@ -3609,7 +3953,7 @@ async function main(input) {
|
|
|
3609
3953
|
const topic = topicName ? findCommand(topicName) : void 0;
|
|
3610
3954
|
if (topicName && !topic) throw unknownCommand(topicName);
|
|
3611
3955
|
const subName = topic?.subs && pos[2] != null ? argv2[pos[2]] : void 0;
|
|
3612
|
-
const sub = topic && subName ?
|
|
3956
|
+
const sub = topic && subName ? findSub2(topic, subName) : void 0;
|
|
3613
3957
|
if (topic && subName && !sub) throw unknownSub(topic, subName);
|
|
3614
3958
|
const { values: values2 } = parse(argv2, pos.slice(0, sub ? 3 : 2), void 0);
|
|
3615
3959
|
if (values2.version) {
|
|
@@ -3635,7 +3979,7 @@ async function main(input) {
|
|
|
3635
3979
|
const skip = [pos[0]];
|
|
3636
3980
|
if (cmd.subs) {
|
|
3637
3981
|
const subName = pos[1] != null ? argv2[pos[1]] : void 0;
|
|
3638
|
-
const entry = subName ?
|
|
3982
|
+
const entry = subName ? findSub2(cmd, subName) : void 0;
|
|
3639
3983
|
if (entry) {
|
|
3640
3984
|
leaf = entry[1];
|
|
3641
3985
|
subKey = entry[0];
|
|
@@ -3682,7 +4026,7 @@ async function main(input) {
|
|
|
3682
4026
|
}
|
|
3683
4027
|
var argv = process.argv.slice(2);
|
|
3684
4028
|
setLang(detectLang(langFlag(argv)));
|
|
3685
|
-
var notice = process.stderr.isTTY && !wantsJson(argv) && !argv.includes("doctor") ? updateNotice(VERSION2) : null;
|
|
4029
|
+
var notice = process.stderr.isTTY && !wantsJson(argv) && !argv.includes("doctor") && argv[0] !== "__complete" ? updateNotice(VERSION2) : null;
|
|
3686
4030
|
main(argv).catch((err) => {
|
|
3687
4031
|
const e = toCliError(err);
|
|
3688
4032
|
const latest = e.code === "usage" ? cachedNewer(VERSION2) : null;
|
package/package.json
CHANGED