@easybits.cloud/cli 0.14.0 → 0.15.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 +16 -0
- package/dist/index.js +535 -397
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,8 @@ easybits db create leads
|
|
|
73
73
|
easybits db tables leads # tables, row counts, columns, permanent vs external links
|
|
74
74
|
easybits db photos put catalog --table products --key-column sku --dir photos/ # SKU-123.jpg → row sku=SKU-123
|
|
75
75
|
easybits db query leads "SELECT * FROM leads" --json
|
|
76
|
+
easybits db rename leads prospects --dry-run # which agent prompts name it
|
|
77
|
+
easybits db rm leads --dry-run # tables and rows that would be lost
|
|
76
78
|
easybits db rm leads --yes
|
|
77
79
|
|
|
78
80
|
# Agents
|
|
@@ -166,6 +168,20 @@ Host *.ghosty
|
|
|
166
168
|
|
|
167
169
|
Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
|
|
168
170
|
|
|
171
|
+
## Changes in 0.15.0
|
|
172
|
+
|
|
173
|
+
- `db rename <db> <new-name> [--skip-prompts] [--dry-run]` (alias `mv`): renames the database (the
|
|
174
|
+
id does not change, so the `db_*` tools keep working) and rewrites the name in your ghosty-lite /
|
|
175
|
+
goose agents' prompts where it appears as a standalone word — never inside `x.com`, `@x`, `x_test`
|
|
176
|
+
or in capitals. `--dry-run` lists each prompt and line; a prompt that fails is reported (exit 1)
|
|
177
|
+
and the rename stays. New API: `PATCH /api/v2/databases/:dbId { name?, description? }`.
|
|
178
|
+
- `db rm --dry-run`: the tables and row counts you would lose.
|
|
179
|
+
- `agents create --prompt/--prompt-file` prints which prompt was applied and its size.
|
|
180
|
+
|
|
181
|
+
## Changes in 0.14.1
|
|
182
|
+
|
|
183
|
+
- `machines launch --machine` takes a name too, like every other command.
|
|
184
|
+
|
|
169
185
|
## Changes in 0.14.0
|
|
170
186
|
|
|
171
187
|
- `machines launch` (aliases `create`, `new`): create a permanent machine and put an app on it in
|
package/dist/index.js
CHANGED
|
@@ -1380,6 +1380,109 @@ var sandboxes = {
|
|
|
1380
1380
|
// src/commands/machines.ts
|
|
1381
1381
|
import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
1382
1382
|
import { basename as basename2 } from "path";
|
|
1383
|
+
|
|
1384
|
+
// src/refs.ts
|
|
1385
|
+
var ID_FORMAT = {
|
|
1386
|
+
agent: /^[0-9a-f]{24}$/i,
|
|
1387
|
+
db: /^[0-9a-f]{24}$/i,
|
|
1388
|
+
sandbox: /^sb_[0-9a-z-]+$/i
|
|
1389
|
+
};
|
|
1390
|
+
var looksLikeId = (kind, ref) => ID_FORMAT[kind].test(ref);
|
|
1391
|
+
var REF_ARGS = {
|
|
1392
|
+
"agents get": { index: 0, kind: "agent" },
|
|
1393
|
+
"agents message": { index: 0, kind: "agent" },
|
|
1394
|
+
"agents destroy": { index: 0, kind: "agent" },
|
|
1395
|
+
"agents set": { index: 0, kind: "agent" },
|
|
1396
|
+
"agents files": { index: 1, kind: "agent" },
|
|
1397
|
+
"agents skills": { index: 1, kind: "agent" },
|
|
1398
|
+
"agents mcp": { index: 1, kind: "agent" },
|
|
1399
|
+
"agents restart": { index: 0, kind: "agent" },
|
|
1400
|
+
"agents try": { index: 0, kind: "agent" },
|
|
1401
|
+
"agents logs": { index: 0, kind: "agent" },
|
|
1402
|
+
"agents doctor": { index: 0, kind: "agent" },
|
|
1403
|
+
"agents export": { index: 0, kind: "agent" },
|
|
1404
|
+
"sandboxes get": { index: 0, kind: "sandbox" },
|
|
1405
|
+
"sandboxes exec": { index: 0, kind: "sandbox" },
|
|
1406
|
+
"sandboxes logs": { index: 0, kind: "sandbox" },
|
|
1407
|
+
"sandboxes files": { index: 1, kind: "sandbox" },
|
|
1408
|
+
"sandboxes suspend": { index: 0, kind: "sandbox" },
|
|
1409
|
+
"sandboxes resume": { index: 0, kind: "sandbox" },
|
|
1410
|
+
"sandboxes destroy": { index: 0, kind: "sandbox" },
|
|
1411
|
+
"sandboxes snapshot": { index: 0, kind: "sandbox" },
|
|
1412
|
+
"machines deploy": { index: 0, kind: "sandbox" },
|
|
1413
|
+
"machines releases": { index: 0, kind: "sandbox" },
|
|
1414
|
+
"machines logs": { index: 0, kind: "sandbox" },
|
|
1415
|
+
"machines rollback": { index: 0, kind: "sandbox" },
|
|
1416
|
+
"machines secrets": { index: 1, kind: "sandbox" },
|
|
1417
|
+
"domains ls": { index: 0, kind: "sandbox" },
|
|
1418
|
+
"domains add": { index: 0, kind: "sandbox" },
|
|
1419
|
+
"domains verify": { index: 0, kind: "sandbox" },
|
|
1420
|
+
"domains rm": { index: 0, kind: "sandbox" },
|
|
1421
|
+
"db rm": { index: 0, kind: "db" },
|
|
1422
|
+
"db rename": { index: 0, kind: "db" },
|
|
1423
|
+
"db tables": { index: 0, kind: "db" },
|
|
1424
|
+
"db query": { index: 0, kind: "db" },
|
|
1425
|
+
"db photos": { index: 1, kind: "db" }
|
|
1426
|
+
};
|
|
1427
|
+
function matchRef(kind, ref, items) {
|
|
1428
|
+
if (looksLikeId(kind, ref)) return { ok: true, id: ref };
|
|
1429
|
+
const list2 = items ?? [];
|
|
1430
|
+
if (list2.some((x) => x.id === ref)) return { ok: true, id: ref };
|
|
1431
|
+
const want = ref.trim().toLowerCase();
|
|
1432
|
+
const matches = list2.filter((x) => (x.name ?? "").trim().toLowerCase() === want);
|
|
1433
|
+
if (matches.length === 1) return { ok: true, id: matches[0].id };
|
|
1434
|
+
if (matches.length === 0) return { ok: false, reason: "not_found" };
|
|
1435
|
+
return { ok: false, reason: "ambiguous", matches };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/resolve.ts
|
|
1439
|
+
var LS_HINT = {
|
|
1440
|
+
agent: "easybits agents ls",
|
|
1441
|
+
sandbox: "easybits sandboxes ls",
|
|
1442
|
+
db: "easybits db ls"
|
|
1443
|
+
};
|
|
1444
|
+
var NOUN = () => ({ agent: t("agent", "agente"), sandbox: t("sandbox or machine", "sandbox o m\xE1quina"), db: t("database", "base de datos") });
|
|
1445
|
+
var NOUNS = () => ({ agent: t("agents", "agentes"), sandbox: t("sandboxes", "sandboxes"), db: t("databases", "bases de datos") });
|
|
1446
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1447
|
+
function listRefs(ctx, kind) {
|
|
1448
|
+
let p = cache.get(kind);
|
|
1449
|
+
if (!p) {
|
|
1450
|
+
p = (async () => {
|
|
1451
|
+
const eb = await getClient(ctx);
|
|
1452
|
+
if (kind === "agent") return (await eb.listAgents()).map((a) => ({ id: a.agentId, name: a.name, status: a.status }));
|
|
1453
|
+
if (kind === "db") return (await eb.listDatabases()).items.map((d) => ({ id: d.id, name: d.name }));
|
|
1454
|
+
return (await eb.sandboxes.list()).map((s) => ({ id: s.sandboxId, name: s.name, status: s.status }));
|
|
1455
|
+
})();
|
|
1456
|
+
p.catch(() => cache.delete(kind));
|
|
1457
|
+
cache.set(kind, p);
|
|
1458
|
+
}
|
|
1459
|
+
return p;
|
|
1460
|
+
}
|
|
1461
|
+
async function resolveRef(ctx, kind, ref) {
|
|
1462
|
+
if (looksLikeId(kind, ref)) return ref;
|
|
1463
|
+
const r = matchRef(kind, ref, await listRefs(ctx, kind));
|
|
1464
|
+
if (r.ok) return r.id;
|
|
1465
|
+
if (r.reason === "not_found") {
|
|
1466
|
+
throw new CliError(t(`No ${NOUN()[kind]} with id or name "${ref}".`, `No hay ${NOUN()[kind]} con id o nombre "${ref}".`), EXIT.API, `${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`, "not_found", 404);
|
|
1467
|
+
}
|
|
1468
|
+
const shown = r.matches.slice(0, 10).map((m) => ` ${m.id}${m.status ? ` (${m.status})` : ""}`);
|
|
1469
|
+
if (r.matches.length > 10) shown.push(t(` \u2026and ${r.matches.length - 10} more`, ` \u2026y ${r.matches.length - 10} m\xE1s`));
|
|
1470
|
+
throw new CliError(
|
|
1471
|
+
t(`"${ref}" matches ${r.matches.length} ${NOUNS()[kind]}. Use the id:`, `"${ref}" coincide con ${r.matches.length} ${NOUNS()[kind]}. Usa el id:`),
|
|
1472
|
+
EXIT.USAGE,
|
|
1473
|
+
`${shown.join("\n")}
|
|
1474
|
+
${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`,
|
|
1475
|
+
"ambiguous"
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
async function resolveRefs(ctx, key) {
|
|
1479
|
+
const spec = REF_ARGS[key];
|
|
1480
|
+
const ref = spec ? ctx.args[spec.index] : void 0;
|
|
1481
|
+
if (!spec || !ref) return;
|
|
1482
|
+
ctx.args[spec.index] = await resolveRef(ctx, spec.kind, ref);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/commands/machines.ts
|
|
1383
1486
|
var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine> [--dotenv <path> | NAME]";
|
|
1384
1487
|
var machines = {
|
|
1385
1488
|
name: "machines",
|
|
@@ -1438,15 +1541,16 @@ var machines = {
|
|
|
1438
1541
|
async run(ctx) {
|
|
1439
1542
|
const repo = str(ctx, "repo");
|
|
1440
1543
|
let archiveUrl = str(ctx, "archive");
|
|
1441
|
-
const
|
|
1442
|
-
if (!repo && !archiveUrl && !
|
|
1544
|
+
const machineRef = str(ctx, "machine");
|
|
1545
|
+
if (!repo && !archiveUrl && !machineRef) {
|
|
1443
1546
|
throw usageError(t("Give a source: --repo, --archive or --machine.", "Pasa una fuente: --repo, --archive o --machine."), this.usage);
|
|
1444
1547
|
}
|
|
1445
1548
|
if (repo && archiveUrl) throw usageError(t("--repo and --archive are exclusive.", "--repo y --archive se excluyen."), this.usage);
|
|
1446
|
-
if (
|
|
1549
|
+
if (machineRef && str(ctx, "tier")) {
|
|
1447
1550
|
throw usageError(t("--tier is only for a new machine (resize = redeploy_machine).", "--tier s\xF3lo aplica a una m\xE1quina nueva (cambiar tama\xF1o = redeploy_machine)."), this.usage);
|
|
1448
1551
|
}
|
|
1449
1552
|
const eb = await getClient(ctx);
|
|
1553
|
+
const sandboxId = machineRef ? await resolveRef(ctx, "sandbox", machineRef) : void 0;
|
|
1450
1554
|
if (archiveUrl && !/^https?:\/\//.test(archiveUrl)) {
|
|
1451
1555
|
if (!existsSync6(archiveUrl)) throw usageError(t(`File not found: ${archiveUrl}`, `No existe el archivo: ${archiveUrl}`), this.usage);
|
|
1452
1556
|
const size = statSync2(archiveUrl).size;
|
|
@@ -1675,7 +1779,7 @@ var domains = {
|
|
|
1675
1779
|
};
|
|
1676
1780
|
|
|
1677
1781
|
// src/commands/db.ts
|
|
1678
|
-
import { existsSync as
|
|
1782
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
|
|
1679
1783
|
|
|
1680
1784
|
// src/photos.ts
|
|
1681
1785
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1769,389 +1873,47 @@ function parseLinkStats(columns, row) {
|
|
|
1769
1873
|
return columns.map((column, i) => ({ column, permanent: n(i * 4), expiring: n(i * 4 + 1), external: n(i * 4 + 2), empty: n(i * 4 + 3) })).filter((s) => s.permanent + s.expiring + s.external > 0);
|
|
1770
1874
|
}
|
|
1771
1875
|
|
|
1772
|
-
// src/
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
};
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
"agents restart": { index: 0, kind: "agent" },
|
|
1788
|
-
"agents try": { index: 0, kind: "agent" },
|
|
1789
|
-
"agents logs": { index: 0, kind: "agent" },
|
|
1790
|
-
"agents doctor": { index: 0, kind: "agent" },
|
|
1791
|
-
"agents export": { index: 0, kind: "agent" },
|
|
1792
|
-
"sandboxes get": { index: 0, kind: "sandbox" },
|
|
1793
|
-
"sandboxes exec": { index: 0, kind: "sandbox" },
|
|
1794
|
-
"sandboxes logs": { index: 0, kind: "sandbox" },
|
|
1795
|
-
"sandboxes files": { index: 1, kind: "sandbox" },
|
|
1796
|
-
"sandboxes suspend": { index: 0, kind: "sandbox" },
|
|
1797
|
-
"sandboxes resume": { index: 0, kind: "sandbox" },
|
|
1798
|
-
"sandboxes destroy": { index: 0, kind: "sandbox" },
|
|
1799
|
-
"sandboxes snapshot": { index: 0, kind: "sandbox" },
|
|
1800
|
-
"machines deploy": { index: 0, kind: "sandbox" },
|
|
1801
|
-
"machines releases": { index: 0, kind: "sandbox" },
|
|
1802
|
-
"machines logs": { index: 0, kind: "sandbox" },
|
|
1803
|
-
"machines rollback": { index: 0, kind: "sandbox" },
|
|
1804
|
-
"machines secrets": { index: 1, kind: "sandbox" },
|
|
1805
|
-
"domains ls": { index: 0, kind: "sandbox" },
|
|
1806
|
-
"domains add": { index: 0, kind: "sandbox" },
|
|
1807
|
-
"domains verify": { index: 0, kind: "sandbox" },
|
|
1808
|
-
"domains rm": { index: 0, kind: "sandbox" },
|
|
1809
|
-
"db rm": { index: 0, kind: "db" },
|
|
1810
|
-
"db tables": { index: 0, kind: "db" },
|
|
1811
|
-
"db query": { index: 0, kind: "db" },
|
|
1812
|
-
"db photos": { index: 1, kind: "db" }
|
|
1813
|
-
};
|
|
1814
|
-
function matchRef(kind, ref, items) {
|
|
1815
|
-
if (looksLikeId(kind, ref)) return { ok: true, id: ref };
|
|
1816
|
-
const list2 = items ?? [];
|
|
1817
|
-
if (list2.some((x) => x.id === ref)) return { ok: true, id: ref };
|
|
1818
|
-
const want = ref.trim().toLowerCase();
|
|
1819
|
-
const matches = list2.filter((x) => (x.name ?? "").trim().toLowerCase() === want);
|
|
1820
|
-
if (matches.length === 1) return { ok: true, id: matches[0].id };
|
|
1821
|
-
if (matches.length === 0) return { ok: false, reason: "not_found" };
|
|
1822
|
-
return { ok: false, reason: "ambiguous", matches };
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
// src/resolve.ts
|
|
1826
|
-
var LS_HINT = {
|
|
1827
|
-
agent: "easybits agents ls",
|
|
1828
|
-
sandbox: "easybits sandboxes ls",
|
|
1829
|
-
db: "easybits db ls"
|
|
1830
|
-
};
|
|
1831
|
-
var NOUN = () => ({ agent: t("agent", "agente"), sandbox: t("sandbox or machine", "sandbox o m\xE1quina"), db: t("database", "base de datos") });
|
|
1832
|
-
var NOUNS = () => ({ agent: t("agents", "agentes"), sandbox: t("sandboxes", "sandboxes"), db: t("databases", "bases de datos") });
|
|
1833
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1834
|
-
function listRefs(ctx, kind) {
|
|
1835
|
-
let p = cache.get(kind);
|
|
1836
|
-
if (!p) {
|
|
1837
|
-
p = (async () => {
|
|
1838
|
-
const eb = await getClient(ctx);
|
|
1839
|
-
if (kind === "agent") return (await eb.listAgents()).map((a) => ({ id: a.agentId, name: a.name, status: a.status }));
|
|
1840
|
-
if (kind === "db") return (await eb.listDatabases()).items.map((d) => ({ id: d.id, name: d.name }));
|
|
1841
|
-
return (await eb.sandboxes.list()).map((s) => ({ id: s.sandboxId, name: s.name, status: s.status }));
|
|
1842
|
-
})();
|
|
1843
|
-
p.catch(() => cache.delete(kind));
|
|
1844
|
-
cache.set(kind, p);
|
|
1876
|
+
// src/api.ts
|
|
1877
|
+
import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
|
|
1878
|
+
async function api(ctx, method, path, body, opts = {}) {
|
|
1879
|
+
const token = await resolveCredential(ctx);
|
|
1880
|
+
const base = (await resolveBaseUrl4()).replace(/\/$/, "");
|
|
1881
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
1882
|
+
let payload;
|
|
1883
|
+
if (body !== void 0) {
|
|
1884
|
+
if (opts.raw) {
|
|
1885
|
+
payload = body;
|
|
1886
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
1887
|
+
} else {
|
|
1888
|
+
payload = JSON.stringify(body);
|
|
1889
|
+
headers["Content-Type"] = "application/json";
|
|
1890
|
+
}
|
|
1845
1891
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
throw new CliError(t(`No ${NOUN()[kind]} with id or name "${ref}".`, `No hay ${NOUN()[kind]} con id o nombre "${ref}".`), EXIT.API, `${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`, "not_found", 404);
|
|
1892
|
+
const res = await fetch(`${base}/api/v2${path}`, { method, headers, body: payload });
|
|
1893
|
+
const text = await res.text();
|
|
1894
|
+
if (!res.ok) throw new EasybitsError2(res.status, text);
|
|
1895
|
+
try {
|
|
1896
|
+
return JSON.parse(text);
|
|
1897
|
+
} catch {
|
|
1898
|
+
return text;
|
|
1854
1899
|
}
|
|
1855
|
-
const shown = r.matches.slice(0, 10).map((m) => ` ${m.id}${m.status ? ` (${m.status})` : ""}`);
|
|
1856
|
-
if (r.matches.length > 10) shown.push(t(` \u2026and ${r.matches.length - 10} more`, ` \u2026y ${r.matches.length - 10} m\xE1s`));
|
|
1857
|
-
throw new CliError(
|
|
1858
|
-
t(`"${ref}" matches ${r.matches.length} ${NOUNS()[kind]}. Use the id:`, `"${ref}" coincide con ${r.matches.length} ${NOUNS()[kind]}. Usa el id:`),
|
|
1859
|
-
EXIT.USAGE,
|
|
1860
|
-
`${shown.join("\n")}
|
|
1861
|
-
${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`,
|
|
1862
|
-
"ambiguous"
|
|
1863
|
-
);
|
|
1864
|
-
}
|
|
1865
|
-
async function resolveRefs(ctx, key) {
|
|
1866
|
-
const spec = REF_ARGS[key];
|
|
1867
|
-
const ref = spec ? ctx.args[spec.index] : void 0;
|
|
1868
|
-
if (!spec || !ref) return;
|
|
1869
|
-
ctx.args[spec.index] = await resolveRef(ctx, spec.kind, ref);
|
|
1870
1900
|
}
|
|
1901
|
+
var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
1871
1902
|
|
|
1872
|
-
// src/commands/
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
var
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
aliases: ["list"],
|
|
1887
|
-
summary: "List your databases",
|
|
1888
|
-
usage: "easybits db ls",
|
|
1889
|
-
examples: ["easybits db ls --json"],
|
|
1890
|
-
async run(ctx) {
|
|
1891
|
-
const eb = await getClient(ctx);
|
|
1892
|
-
const { items } = await eb.listDatabases();
|
|
1893
|
-
emit(
|
|
1894
|
-
ctx,
|
|
1895
|
-
items,
|
|
1896
|
-
() => table(
|
|
1897
|
-
items.map((d) => ({ ...d, createdAt: fmtDate(d.createdAt) })),
|
|
1898
|
-
[["id", "ID"], ["name", "NAME"], ["createdAt", "CREATED"], ["description", "DESCRIPTION"]],
|
|
1899
|
-
t("No databases. Create one: easybits db create <name>", "No hay bases. Crea una: easybits db create <nombre>")
|
|
1900
|
-
)
|
|
1901
|
-
);
|
|
1902
|
-
}
|
|
1903
|
-
},
|
|
1904
|
-
create: {
|
|
1905
|
-
summary: "Create a database",
|
|
1906
|
-
usage: "easybits db create <name> [--description <text>]",
|
|
1907
|
-
options: { description: { type: "string", value: "text", description: "What it is for" } },
|
|
1908
|
-
examples: ["easybits db create leads --description 'CRM leads'"],
|
|
1909
|
-
async run(ctx) {
|
|
1910
|
-
const name = need(ctx, 0, "name", this.usage);
|
|
1911
|
-
const eb = await getClient(ctx);
|
|
1912
|
-
const d = await eb.createDatabase({ name, description: str(ctx, "description") });
|
|
1913
|
-
emit(ctx, d, () => console.log(t(`Created ${d.name} (${d.id})`, `Creada ${d.name} (${d.id})`)));
|
|
1914
|
-
}
|
|
1915
|
-
},
|
|
1916
|
-
rm: {
|
|
1917
|
-
aliases: ["delete", "drop"],
|
|
1918
|
-
summary: "Delete a database (irreversible)",
|
|
1919
|
-
usage: "easybits db rm <db-id|name> [--yes]",
|
|
1920
|
-
options: { ...YES_OPTION },
|
|
1921
|
-
examples: ["easybits db rm leads # asks you to type the name", "easybits db rm leads --yes # scripts and agents"],
|
|
1922
|
-
async run(ctx) {
|
|
1923
|
-
const ref = need(ctx, 0, "db-id|name", this.usage);
|
|
1924
|
-
requireYesIfHeadless(ctx);
|
|
1925
|
-
const eb = await getClient(ctx);
|
|
1926
|
-
const hit = await findDb(ctx, ref);
|
|
1927
|
-
await confirm(ctx, t(`Delete database ${hit.name} (${hit.id}) and all its data?`, `\xBFBorrar la base ${hit.name} (${hit.id}) con todos sus datos?`), { typeName: hit.name });
|
|
1928
|
-
const r = await eb.deleteDatabase(hit.id);
|
|
1929
|
-
emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(t(`Deleted ${hit.name} (${hit.id})`, `Borrada ${hit.name} (${hit.id})`)));
|
|
1930
|
-
}
|
|
1931
|
-
},
|
|
1932
|
-
tables: {
|
|
1933
|
-
aliases: ["inspect", "schema"],
|
|
1934
|
-
summary: "List tables with row counts and columns, and how many links are permanent",
|
|
1935
|
-
usage: "easybits db tables <db-id|name>",
|
|
1936
|
-
examples: ["easybits db tables leads", "easybits db tables leads --json"],
|
|
1937
|
-
// Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
|
|
1938
|
-
async run(ctx) {
|
|
1939
|
-
const ref = need(ctx, 0, "db-id|name", this.usage);
|
|
1940
|
-
const eb = await getClient(ctx);
|
|
1941
|
-
const hit = await findDb(ctx, ref);
|
|
1942
|
-
const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
|
|
1943
|
-
const names = (await q("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' ORDER BY name")).rows.map((r) => String(r[0]));
|
|
1944
|
-
const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
|
|
1945
|
-
const tables = [];
|
|
1946
|
-
for (const name of names) {
|
|
1947
|
-
const info2 = await q(`PRAGMA table_info(${ident2(name)})`);
|
|
1948
|
-
const col = (k) => info2.cols.indexOf(k);
|
|
1949
|
-
const columns = info2.rows.map((r) => ({ name: String(r[col("name")]), type: String(r[col("type")] ?? ""), pk: Number(r[col("pk")]) > 0 }));
|
|
1950
|
-
const count = await q(`SELECT count(*) FROM ${ident2(name)}`);
|
|
1951
|
-
const textCols = columns.filter((c) => !c.type || /CHAR|TEXT|CLOB/i.test(c.type)).map((c) => c.name);
|
|
1952
|
-
const links = textCols.length ? parseLinkStats(textCols, (await q(linkStatsSql(name, textCols))).rows[0] ?? []) : [];
|
|
1953
|
-
tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns, links });
|
|
1954
|
-
}
|
|
1955
|
-
emit(ctx, tables, () => {
|
|
1956
|
-
table(
|
|
1957
|
-
tables.map((t2) => ({
|
|
1958
|
-
name: t2.name,
|
|
1959
|
-
rows: t2.rows,
|
|
1960
|
-
columns: t2.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
|
|
1961
|
-
})),
|
|
1962
|
-
[["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
|
|
1963
|
-
t(`No tables in ${hit.name}. Create one: easybits db query ${hit.name} 'CREATE TABLE \u2026'`, `No hay tablas en ${hit.name}. Crea una: easybits db query ${hit.name} 'CREATE TABLE \u2026'`)
|
|
1964
|
-
);
|
|
1965
|
-
const withLinks = tables.flatMap((tb) => tb.links.map((l) => ({ ...l, table: tb.name })));
|
|
1966
|
-
if (withLinks.length) {
|
|
1967
|
-
console.log("\n" + t("Links (permanent = EasyBits public storage; expiring = signed URL):", "Ligas (permanente = almacenamiento p\xFAblico de EasyBits; caduca = URL firmada):"));
|
|
1968
|
-
table(
|
|
1969
|
-
withLinks.map((l) => ({ where: `${l.table}.${l.column}`, permanent: l.permanent, expiring: l.expiring, external: l.external, empty: l.empty })),
|
|
1970
|
-
[["where", "COLUMN"], ["permanent", "PERMANENT"], ["expiring", "EXPIRING"], ["external", "EXTERNAL"], ["empty", "EMPTY"]]
|
|
1971
|
-
);
|
|
1972
|
-
if (withLinks.some((l) => l.external + l.expiring > 0)) {
|
|
1973
|
-
console.log(t("Make them permanent: easybits db photos put <db> --table T --key-column sku --dir photos/", "Hazlas permanentes: easybits db photos put <db> --table T --key-column sku --dir fotos/"));
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
1978
|
-
},
|
|
1979
|
-
photos: {
|
|
1980
|
-
summary: "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link",
|
|
1981
|
-
usage: "easybits db photos put <db> --table <table> --key-column <col> --dir <folder> [--column <col>] [--replace] [--dry-run]",
|
|
1982
|
-
options: {
|
|
1983
|
-
table: { type: "string", value: "table", description: "Table to update" },
|
|
1984
|
-
"key-column": { type: "string", value: "col", description: "Column that matches the file name (e.g. sku)" },
|
|
1985
|
-
dir: { type: "string", value: "folder", description: "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)" },
|
|
1986
|
-
column: { type: "string", value: "col", description: "Column for the link (default: image_url, photo_url, imagen\u2026)" },
|
|
1987
|
-
replace: { type: "boolean", description: "Also replace rows that already have a permanent link" },
|
|
1988
|
-
"dry-run": { type: "boolean", description: "Show the plan; upload nothing" }
|
|
1989
|
-
},
|
|
1990
|
-
examples: [
|
|
1991
|
-
"easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --dry-run",
|
|
1992
|
-
"easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --column foto",
|
|
1993
|
-
"easybits db tables catalogo # how many links are permanent, external or empty"
|
|
1994
|
-
],
|
|
1995
|
-
async run(ctx) {
|
|
1996
|
-
const action = need(ctx, 0, "put", this.usage);
|
|
1997
|
-
if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
|
|
1998
|
-
const ref = need(ctx, 1, "db", this.usage);
|
|
1999
|
-
const tableName = str(ctx, "table");
|
|
2000
|
-
const keyCol = str(ctx, "key-column");
|
|
2001
|
-
const dir = str(ctx, "dir");
|
|
2002
|
-
if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
|
|
2003
|
-
if (!existsSync7(dir) || !statSync4(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
|
|
2004
|
-
const { photos, rejected } = photoFiles(dir);
|
|
2005
|
-
const eb = await getClient(ctx);
|
|
2006
|
-
const hit = await findDb(ctx, ref);
|
|
2007
|
-
const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
|
|
2008
|
-
const ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
|
|
2009
|
-
const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
|
|
2010
|
-
const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
|
|
2011
|
-
if (!cols.length) throw new CliError(t(`No table "${tableName}" in ${hit.name}.`, `No hay tabla "${tableName}" en ${hit.name}.`), 1, t(`See: easybits db tables ${hit.name}`, `Mira: easybits db tables ${hit.name}`), "not_found", 404);
|
|
2012
|
-
if (!cols.includes(keyCol)) throw usageError(t(`No column "${keyCol}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${keyCol}" en ${tableName} (${cols.join(", ")}).`), this.usage);
|
|
2013
|
-
const column = str(ctx, "column") ?? defaultPhotoColumn(cols);
|
|
2014
|
-
if (!column) throw usageError(t(`${tableName} has no photo column: pass --column (${cols.join(", ")}).`, `${tableName} no tiene columna de foto: pasa --column (${cols.join(", ")}).`), this.usage);
|
|
2015
|
-
if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
|
|
2016
|
-
const current2 = /* @__PURE__ */ new Map();
|
|
2017
|
-
for (let i = 0; i < photos.length; i += 100) {
|
|
2018
|
-
const keys = photos.slice(i, i + 100).map((p) => p.key);
|
|
2019
|
-
const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
|
|
2020
|
-
for (const row of r.rows) current2.set(String(row[0]), row[1]);
|
|
2021
|
-
}
|
|
2022
|
-
const plan = planPhotos(photos, current2, bool(ctx, "replace"));
|
|
2023
|
-
const big = photos.filter((p) => p.bytes > WHATSAPP_MAX_BYTES).map((p) => p.file);
|
|
2024
|
-
const summary = { db: hit.name, table: tableName, keyColumn: keyCol, column, plan, rejected, overWhatsappLimit: big };
|
|
2025
|
-
const count = (a) => plan.filter((p) => p.action === a).length;
|
|
2026
|
-
const why = (r) => r === "not_an_image" ? t("not a JPEG/PNG/WebP", "no es JPEG/PNG/WebP") : r === "too_large" ? t("over 10 MB", "m\xE1s de 10 MB") : t("empty file", "archivo vac\xEDo");
|
|
2027
|
-
const printPlan2 = () => {
|
|
2028
|
-
for (const p of plan) {
|
|
2029
|
-
const what = p.action === "update" ? t("update", "actualizar") : p.action === "keep" ? t("keep (already permanent; --replace to redo)", "se queda (ya es permanente; --replace para rehacerla)") : t("no row with that key", "no hay fila con esa llave");
|
|
2030
|
-
console.log(`${p.action === "update" ? "~" : "!"} ${p.file} \u2192 ${keyCol}=${p.key}: ${what}`);
|
|
2031
|
-
}
|
|
2032
|
-
for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
|
|
2033
|
-
if (big.length) console.log(t(`Over 5 MB (WhatsApp won't send them by link): ${big.join(", ")}`, `M\xE1s de 5 MB (WhatsApp no las manda por liga): ${big.join(", ")}`));
|
|
2034
|
-
};
|
|
2035
|
-
if (bool(ctx, "dry-run") || !count("update")) {
|
|
2036
|
-
emit(ctx, { ...summary, dryRun: bool(ctx, "dry-run") }, () => {
|
|
2037
|
-
printPlan2();
|
|
2038
|
-
console.log(count("update") ? t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)") : t("Nothing to update.", "Nada que actualizar."));
|
|
2039
|
-
});
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
const urls = /* @__PURE__ */ new Map();
|
|
2043
|
-
const results = [];
|
|
2044
|
-
for (const item of plan.filter((p) => p.action === "update")) {
|
|
2045
|
-
const photo = photos.find((p) => p.key === item.key);
|
|
2046
|
-
try {
|
|
2047
|
-
let url = urls.get(photo.sha);
|
|
2048
|
-
if (!url) {
|
|
2049
|
-
const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
|
|
2050
|
-
const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync8(photo.path), headers: { "Content-Type": photo.mime } });
|
|
2051
|
-
if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
|
|
2052
|
-
url = up.file.url;
|
|
2053
|
-
if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
|
|
2054
|
-
urls.set(photo.sha, url);
|
|
2055
|
-
}
|
|
2056
|
-
await q(`UPDATE ${ident2(tableName)} SET ${ident2(column)} = ? WHERE ${ident2(keyCol)} = ?`, [url, item.key]);
|
|
2057
|
-
results.push({ key: item.key, file: item.file, url });
|
|
2058
|
-
if (!ctx.json) console.log(`~ ${item.file} \u2192 ${keyCol}=${item.key}: ${url}`);
|
|
2059
|
-
} catch (e) {
|
|
2060
|
-
results.push({ key: item.key, file: item.file, error: e.message });
|
|
2061
|
-
if (!ctx.json) console.error(`! ${item.file}: ${e.message}`);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
const failed = results.filter((r) => r.error).length;
|
|
2065
|
-
emit(ctx, { ...summary, results }, () => {
|
|
2066
|
-
for (const p of plan.filter((x) => x.action !== "update")) console.log(`! ${p.file} \u2192 ${keyCol}=${p.key}: ${p.action === "keep" ? t("kept", "se qued\xF3") : t("no row with that key", "no hay fila con esa llave")}`);
|
|
2067
|
-
for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
|
|
2068
|
-
console.log(t(`Updated ${results.length - failed} row(s)${failed ? `, ${failed} failed` : ""}.`, `${results.length - failed} fila(s) actualizadas${failed ? `, ${failed} fallaron` : ""}.`));
|
|
2069
|
-
});
|
|
2070
|
-
if (failed) process.exitCode = 1;
|
|
2071
|
-
}
|
|
2072
|
-
},
|
|
2073
|
-
query: {
|
|
2074
|
-
aliases: ["sql"],
|
|
2075
|
-
summary: "Run one SQL statement (by database id or name)",
|
|
2076
|
-
usage: "easybits db query <db-id|name> <sql> [--arg <value>]...",
|
|
2077
|
-
options: { arg: { type: "string", multiple: true, value: "value", description: "Positional ? parameter (repeatable)" } },
|
|
2078
|
-
examples: [
|
|
2079
|
-
"easybits db query leads 'SELECT * FROM leads LIMIT 10'",
|
|
2080
|
-
"easybits db query leads 'INSERT INTO leads(name) VALUES (?)' --arg Ana",
|
|
2081
|
-
"easybits db query leads 'SELECT count(*) AS n FROM leads' --json"
|
|
2082
|
-
],
|
|
2083
|
-
async run(ctx) {
|
|
2084
|
-
const ref = need(ctx, 0, "db-id|name", this.usage);
|
|
2085
|
-
const sql = need(ctx, 1, "sql", this.usage);
|
|
2086
|
-
const eb = await getClient(ctx);
|
|
2087
|
-
const hit = await findDb(ctx, ref);
|
|
2088
|
-
const args = list(ctx, "arg");
|
|
2089
|
-
const r = await eb.queryDatabase(hit.id, sql, args.length ? args : void 0);
|
|
2090
|
-
emit(ctx, r, () => {
|
|
2091
|
-
if (r.cols.length) {
|
|
2092
|
-
table(
|
|
2093
|
-
r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]]))),
|
|
2094
|
-
r.cols.map((c) => [c, c]),
|
|
2095
|
-
"(0 rows)"
|
|
2096
|
-
);
|
|
2097
|
-
} else {
|
|
2098
|
-
console.log(
|
|
2099
|
-
t(
|
|
2100
|
-
`OK, ${r.affected_row_count} row(s) affected${r.last_insert_rowid ? `, last id ${r.last_insert_rowid}` : ""}`,
|
|
2101
|
-
`OK, ${r.affected_row_count} fila(s) afectadas${r.last_insert_rowid ? `, \xFAltimo id ${r.last_insert_rowid}` : ""}`
|
|
2102
|
-
)
|
|
2103
|
-
);
|
|
2104
|
-
}
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
};
|
|
2110
|
-
|
|
2111
|
-
// src/commands/agents.ts
|
|
2112
|
-
import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
|
|
2113
|
-
|
|
2114
|
-
// src/api.ts
|
|
2115
|
-
import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
|
|
2116
|
-
async function api(ctx, method, path, body, opts = {}) {
|
|
2117
|
-
const token = await resolveCredential(ctx);
|
|
2118
|
-
const base = (await resolveBaseUrl4()).replace(/\/$/, "");
|
|
2119
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
2120
|
-
let payload;
|
|
2121
|
-
if (body !== void 0) {
|
|
2122
|
-
if (opts.raw) {
|
|
2123
|
-
payload = body;
|
|
2124
|
-
headers["Content-Type"] = "application/octet-stream";
|
|
2125
|
-
} else {
|
|
2126
|
-
payload = JSON.stringify(body);
|
|
2127
|
-
headers["Content-Type"] = "application/json";
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
const res = await fetch(`${base}/api/v2${path}`, { method, headers, body: payload });
|
|
2131
|
-
const text = await res.text();
|
|
2132
|
-
if (!res.ok) throw new EasybitsError2(res.status, text);
|
|
2133
|
-
try {
|
|
2134
|
-
return JSON.parse(text);
|
|
2135
|
-
} catch {
|
|
2136
|
-
return text;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
2140
|
-
|
|
2141
|
-
// src/commands/agents-config.ts
|
|
2142
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2143
|
-
import { basename as basename3, dirname, join as join4, relative, resolve } from "path";
|
|
2144
|
-
var WORK_DIR = "/data/work";
|
|
2145
|
-
var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
|
|
2146
|
-
var SKILLS_DIR = "/data/agent/skills";
|
|
2147
|
-
var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
2148
|
-
var DRY_RUN = { "dry-run": { type: "boolean", description: "Show what would change; change nothing" } };
|
|
2149
|
-
var getPrompt = (ctx, id) => api(ctx, "GET", `/agents/${id}/prompt`);
|
|
2150
|
-
var getMcp = async (ctx, id) => (await api(ctx, "GET", `/agents/${id}/mcp`)).servers;
|
|
2151
|
-
var listFiles = (ctx, id, sub) => api(ctx, "GET", `/agents/${id}/files${sub ? `/${encodePath(sub)}` : ""}`);
|
|
2152
|
-
async function agentBox(eb, id) {
|
|
2153
|
-
const a = await eb.getAgent(id);
|
|
2154
|
-
return { agent: a, box: await eb.sandboxes.get(a.sandboxId) };
|
|
1903
|
+
// src/commands/agents-config.ts
|
|
1904
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1905
|
+
import { basename as basename3, dirname, join as join4, relative, resolve } from "path";
|
|
1906
|
+
var WORK_DIR = "/data/work";
|
|
1907
|
+
var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
|
|
1908
|
+
var SKILLS_DIR = "/data/agent/skills";
|
|
1909
|
+
var SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
1910
|
+
var DRY_RUN = { "dry-run": { type: "boolean", description: "Show what would change; change nothing" } };
|
|
1911
|
+
var getPrompt = (ctx, id) => api(ctx, "GET", `/agents/${id}/prompt`);
|
|
1912
|
+
var getMcp = async (ctx, id) => (await api(ctx, "GET", `/agents/${id}/mcp`)).servers;
|
|
1913
|
+
var listFiles = (ctx, id, sub) => api(ctx, "GET", `/agents/${id}/files${sub ? `/${encodePath(sub)}` : ""}`);
|
|
1914
|
+
async function agentBox(eb, id) {
|
|
1915
|
+
const a = await eb.getAgent(id);
|
|
1916
|
+
return { agent: a, box: await eb.sandboxes.get(a.sandboxId) };
|
|
2155
1917
|
}
|
|
2156
1918
|
function skillFrontmatter(md) {
|
|
2157
1919
|
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
@@ -2219,8 +1981,8 @@ function readLocalText(path, usage2) {
|
|
|
2219
1981
|
if (process.stdin.isTTY) throw usageError(t("Expected the content on stdin.", "Se esperaba el contenido por stdin."), usage2);
|
|
2220
1982
|
return readStdin().then((b) => b.toString("utf8"));
|
|
2221
1983
|
}
|
|
2222
|
-
if (!
|
|
2223
|
-
return
|
|
1984
|
+
if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
|
|
1985
|
+
return readFileSync8(path, "utf8");
|
|
2224
1986
|
}
|
|
2225
1987
|
function parseMcpFile(text, usage2) {
|
|
2226
1988
|
let j;
|
|
@@ -2435,8 +2197,8 @@ var files2 = {
|
|
|
2435
2197
|
if (!rest.length) throw usageError(t("Give one or more local files.", "Pasa uno o m\xE1s archivos locales."), FILES_USAGE2);
|
|
2436
2198
|
const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
|
|
2437
2199
|
const plan = rest.map((local) => {
|
|
2438
|
-
if (!
|
|
2439
|
-
return { local, path: to ? `${to}/${basename3(local)}` : basename3(local), bytes:
|
|
2200
|
+
if (!existsSync7(local) || !statSync4(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
|
|
2201
|
+
return { local, path: to ? `${to}/${basename3(local)}` : basename3(local), bytes: statSync4(local).size };
|
|
2440
2202
|
});
|
|
2441
2203
|
if (bool(ctx, "dry-run")) {
|
|
2442
2204
|
emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
|
|
@@ -2447,7 +2209,7 @@ var files2 = {
|
|
|
2447
2209
|
}
|
|
2448
2210
|
const done = [];
|
|
2449
2211
|
for (const p of plan) {
|
|
2450
|
-
const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`,
|
|
2212
|
+
const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync8(p.local), { raw: true });
|
|
2451
2213
|
done.push({ local: p.local, ...r });
|
|
2452
2214
|
if (!ctx.json) console.log(`${t("Uploaded", "Subido")} ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
|
|
2453
2215
|
}
|
|
@@ -2477,7 +2239,7 @@ function readSkillDir(dir) {
|
|
|
2477
2239
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2478
2240
|
const p = join4(d, e.name);
|
|
2479
2241
|
if (e.isDirectory()) visit(p);
|
|
2480
|
-
else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64:
|
|
2242
|
+
else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync8(p).toString("base64") });
|
|
2481
2243
|
}
|
|
2482
2244
|
};
|
|
2483
2245
|
visit(dir);
|
|
@@ -2541,8 +2303,8 @@ var skills = {
|
|
|
2541
2303
|
const dir = str(ctx, "dir");
|
|
2542
2304
|
if (!!file === !!dir) throw usageError(t("Pass --file SKILL.md or --dir <folder>.", "Pasa --file SKILL.md o --dir <carpeta>."), SKILLS_USAGE);
|
|
2543
2305
|
const mdPath = file ?? join4(dir, "SKILL.md");
|
|
2544
|
-
if (!
|
|
2545
|
-
const markdown =
|
|
2306
|
+
if (!existsSync7(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
|
|
2307
|
+
const markdown = readFileSync8(mdPath, "utf8");
|
|
2546
2308
|
const fm = skillFrontmatter(markdown);
|
|
2547
2309
|
if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
|
|
2548
2310
|
const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename3(resolve(dir ?? dirname(mdPath))).toLowerCase());
|
|
@@ -2802,10 +2564,10 @@ var exportAgent = {
|
|
|
2802
2564
|
}
|
|
2803
2565
|
};
|
|
2804
2566
|
function readExport(path, usage2) {
|
|
2805
|
-
if (!
|
|
2567
|
+
if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
|
|
2806
2568
|
let x;
|
|
2807
2569
|
try {
|
|
2808
|
-
x = JSON.parse(
|
|
2570
|
+
x = JSON.parse(readFileSync8(path, "utf8"));
|
|
2809
2571
|
} catch (e) {
|
|
2810
2572
|
throw usageError(t(`${path} is not JSON: ${e.message}`, `${path} no es JSON: ${e.message}`), usage2);
|
|
2811
2573
|
}
|
|
@@ -2849,8 +2611,374 @@ async function applyExport(ctx, id, x, opts) {
|
|
|
2849
2611
|
return copied;
|
|
2850
2612
|
}
|
|
2851
2613
|
|
|
2852
|
-
// src/
|
|
2853
|
-
|
|
2614
|
+
// src/db-name-refs.ts
|
|
2615
|
+
var EDGE = "A-Za-z0-9_.@/-";
|
|
2616
|
+
var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2617
|
+
var nameRe = (name) => new RegExp(`(?<![${EDGE}])${escape2(name)}(?![${EDGE}])`, "g");
|
|
2618
|
+
function findNameRefs(text, name) {
|
|
2619
|
+
const re = nameRe(name);
|
|
2620
|
+
return text.split("\n").flatMap((l, i) => {
|
|
2621
|
+
re.lastIndex = 0;
|
|
2622
|
+
return re.test(l) ? [{ line: i + 1, text: l.trim().slice(0, 160) }] : [];
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
function replaceName(text, from, to) {
|
|
2626
|
+
return text.replace(nameRe(from), to);
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// src/commands/db.ts
|
|
2630
|
+
async function findDb(ctx, id) {
|
|
2631
|
+
const hit = (await listRefs(ctx, "db")).find((d) => d.id === id);
|
|
2632
|
+
if (!hit) throw new CliError(t(`No database "${id}".`, `No existe la base "${id}".`), 1, t("List them with: easybits db ls", "Enl\xEDstalas con: easybits db ls"), "not_found", 404);
|
|
2633
|
+
return { id, name: hit.name ?? id };
|
|
2634
|
+
}
|
|
2635
|
+
var ident2 = (n) => `"${n.replace(/"/g, '""')}"`;
|
|
2636
|
+
async function tableCounts(ctx, id) {
|
|
2637
|
+
const eb = await getClient(ctx);
|
|
2638
|
+
const q = (sql) => eb.queryDatabase(id, sql);
|
|
2639
|
+
const names = (await q("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_litestream_%' AND name NOT LIKE '_easybits_%' ORDER BY name")).rows.map((r) => String(r[0]));
|
|
2640
|
+
const out = [];
|
|
2641
|
+
for (const name of names) out.push({ name, rows: Number((await q(`SELECT count(*) FROM ${ident2(name)}`)).rows[0]?.[0] ?? 0) });
|
|
2642
|
+
return out;
|
|
2643
|
+
}
|
|
2644
|
+
async function promptRefs(ctx, name) {
|
|
2645
|
+
const eb = await getClient(ctx);
|
|
2646
|
+
const found = [];
|
|
2647
|
+
const skipped = [];
|
|
2648
|
+
for (const a of await eb.listAgents()) {
|
|
2649
|
+
if (!MACHINE_TEMPLATES.has(a.template)) continue;
|
|
2650
|
+
const agent = a.name ?? a.agentId;
|
|
2651
|
+
if (a.status === "lost" || a.status === "error") {
|
|
2652
|
+
skipped.push({ agentId: a.agentId, agent, reason: a.status });
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2656
|
+
const p = await api(ctx, "GET", `/agents/${a.agentId}/prompt`);
|
|
2657
|
+
const refs = findNameRefs(p.systemPrompt ?? "", name);
|
|
2658
|
+
if (refs.length) found.push({ agentId: a.agentId, agent, refs });
|
|
2659
|
+
} catch (e) {
|
|
2660
|
+
skipped.push({ agentId: a.agentId, agent, reason: e.message });
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return { found, skipped };
|
|
2664
|
+
}
|
|
2665
|
+
var db = {
|
|
2666
|
+
name: "db",
|
|
2667
|
+
aliases: ["databases", "database"],
|
|
2668
|
+
group: "Data",
|
|
2669
|
+
summary: "SQL databases (libSQL)",
|
|
2670
|
+
synopsis: "db",
|
|
2671
|
+
subs: {
|
|
2672
|
+
ls: {
|
|
2673
|
+
aliases: ["list"],
|
|
2674
|
+
summary: "List your databases",
|
|
2675
|
+
usage: "easybits db ls",
|
|
2676
|
+
examples: ["easybits db ls --json"],
|
|
2677
|
+
async run(ctx) {
|
|
2678
|
+
const eb = await getClient(ctx);
|
|
2679
|
+
const { items } = await eb.listDatabases();
|
|
2680
|
+
emit(
|
|
2681
|
+
ctx,
|
|
2682
|
+
items,
|
|
2683
|
+
() => table(
|
|
2684
|
+
items.map((d) => ({ ...d, createdAt: fmtDate(d.createdAt) })),
|
|
2685
|
+
[["id", "ID"], ["name", "NAME"], ["createdAt", "CREATED"], ["description", "DESCRIPTION"]],
|
|
2686
|
+
t("No databases. Create one: easybits db create <name>", "No hay bases. Crea una: easybits db create <nombre>")
|
|
2687
|
+
)
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
},
|
|
2691
|
+
create: {
|
|
2692
|
+
summary: "Create a database",
|
|
2693
|
+
usage: "easybits db create <name> [--description <text>]",
|
|
2694
|
+
options: { description: { type: "string", value: "text", description: "What it is for" } },
|
|
2695
|
+
examples: ["easybits db create leads --description 'CRM leads'"],
|
|
2696
|
+
async run(ctx) {
|
|
2697
|
+
const name = need(ctx, 0, "name", this.usage);
|
|
2698
|
+
const eb = await getClient(ctx);
|
|
2699
|
+
const d = await eb.createDatabase({ name, description: str(ctx, "description") });
|
|
2700
|
+
emit(ctx, d, () => console.log(t(`Created ${d.name} (${d.id})`, `Creada ${d.name} (${d.id})`)));
|
|
2701
|
+
}
|
|
2702
|
+
},
|
|
2703
|
+
rm: {
|
|
2704
|
+
aliases: ["delete", "drop"],
|
|
2705
|
+
summary: "Delete a database (irreversible)",
|
|
2706
|
+
usage: "easybits db rm <db-id|name> [--dry-run] [--yes]",
|
|
2707
|
+
options: { "dry-run": { type: "boolean", description: "Show its tables and rows; delete nothing" }, ...YES_OPTION },
|
|
2708
|
+
examples: [
|
|
2709
|
+
"easybits db rm leads --dry-run # what would be lost",
|
|
2710
|
+
"easybits db rm leads # asks you to type the name",
|
|
2711
|
+
"easybits db rm leads --yes # scripts and agents"
|
|
2712
|
+
],
|
|
2713
|
+
async run(ctx) {
|
|
2714
|
+
const ref = need(ctx, 0, "db-id|name", this.usage);
|
|
2715
|
+
if (bool(ctx, "dry-run")) {
|
|
2716
|
+
const hit2 = await findDb(ctx, ref);
|
|
2717
|
+
const tables = await tableCounts(ctx, hit2.id);
|
|
2718
|
+
const rows = tables.reduce((n, x) => n + x.rows, 0);
|
|
2719
|
+
emit(ctx, { id: hit2.id, name: hit2.name, dryRun: true, tables }, () => {
|
|
2720
|
+
console.log(t(`${hit2.name} (${hit2.id}): ${tables.length} table(s), ${rows} row(s)`, `${hit2.name} (${hit2.id}): ${tables.length} tabla(s), ${rows} fila(s)`));
|
|
2721
|
+
for (const x of tables) console.log(` ${x.name}: ${x.rows}`);
|
|
2722
|
+
console.log(t("(dry run: nothing deleted)", "(simulaci\xF3n: no se borr\xF3 nada)"));
|
|
2723
|
+
});
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
requireYesIfHeadless(ctx);
|
|
2727
|
+
const eb = await getClient(ctx);
|
|
2728
|
+
const hit = await findDb(ctx, ref);
|
|
2729
|
+
await confirm(ctx, t(`Delete database ${hit.name} (${hit.id}) and all its data?`, `\xBFBorrar la base ${hit.name} (${hit.id}) con todos sus datos?`), { typeName: hit.name });
|
|
2730
|
+
const r = await eb.deleteDatabase(hit.id);
|
|
2731
|
+
emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(t(`Deleted ${hit.name} (${hit.id})`, `Borrada ${hit.name} (${hit.id})`)));
|
|
2732
|
+
}
|
|
2733
|
+
},
|
|
2734
|
+
rename: {
|
|
2735
|
+
aliases: ["mv"],
|
|
2736
|
+
summary: "Rename a database and rewrite the agent prompts that name it",
|
|
2737
|
+
usage: "easybits db rename <db-id|name> <new-name> [--skip-prompts] [--dry-run]",
|
|
2738
|
+
options: {
|
|
2739
|
+
"skip-prompts": { type: "boolean", description: "Rename only; leave your agents' prompts as they are" },
|
|
2740
|
+
"dry-run": { type: "boolean", description: "Show the prompts that name it; change nothing" }
|
|
2741
|
+
},
|
|
2742
|
+
examples: [
|
|
2743
|
+
"easybits db rename totequim catalogo --dry-run # which prompts name it",
|
|
2744
|
+
"easybits db rename totequim catalogo",
|
|
2745
|
+
"easybits db rename totequim catalogo --skip-prompts --json"
|
|
2746
|
+
],
|
|
2747
|
+
// El id no cambia (las tools db_* van por id). Se reescribe el nombre como palabra suelta:
|
|
2748
|
+
// `totequim.com`, `@totequim`, `totequim_prueba` y «TOTEQUIM» se quedan (db-name-refs.ts).
|
|
2749
|
+
async run(ctx) {
|
|
2750
|
+
const ref = need(ctx, 0, "db-id|name", this.usage);
|
|
2751
|
+
const to = need(ctx, 1, "new-name", this.usage).trim();
|
|
2752
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(to)) throw usageError(t("The name takes letters, digits, - and _ (max 64).", "El nombre lleva letras, d\xEDgitos, - y _ (m\xE1x 64)."), this.usage);
|
|
2753
|
+
const hit = await findDb(ctx, ref);
|
|
2754
|
+
const from = hit.name;
|
|
2755
|
+
if (to === from) throw usageError(t(`${from} already has that name.`, `${from} ya se llama as\xED.`), this.usage);
|
|
2756
|
+
const taken = (await listRefs(ctx, "db")).find((d) => d.name === to);
|
|
2757
|
+
if (taken) throw new CliError(t(`A database named "${to}" already exists (${taken.id}).`, `Ya hay una base "${to}" (${taken.id}).`), 1, void 0, "conflict", 409);
|
|
2758
|
+
const scan = bool(ctx, "skip-prompts") ? { found: [], skipped: [] } : await promptRefs(ctx, from);
|
|
2759
|
+
const base = { id: hit.id, from, to, references: scan.found, skipped: scan.skipped };
|
|
2760
|
+
const printRefs = () => {
|
|
2761
|
+
if (bool(ctx, "skip-prompts")) return console.log(t("Prompts not checked (--skip-prompts).", "Prompts sin revisar (--skip-prompts)."));
|
|
2762
|
+
if (!scan.found.length) console.log(t("No agent prompt names it.", "Ning\xFAn prompt de tus agentes la nombra."));
|
|
2763
|
+
else console.log(t(`Named in ${scan.found.length} prompt(s):`, `La nombran ${scan.found.length} prompt(s):`));
|
|
2764
|
+
for (const r of scan.found) {
|
|
2765
|
+
console.log(` ${r.agent} (${r.agentId})`);
|
|
2766
|
+
for (const l of r.refs.slice(0, 3)) console.log(` ${l.line}: ${l.text}`);
|
|
2767
|
+
if (r.refs.length > 3) console.log(t(` \u2026 +${r.refs.length - 3} more`, ` \u2026 +${r.refs.length - 3} m\xE1s`));
|
|
2768
|
+
}
|
|
2769
|
+
for (const x of scan.skipped) console.log(t(` ! ${x.agent} (${x.agentId}) not checked: ${x.reason}`, ` ! ${x.agent} (${x.agentId}) sin revisar: ${x.reason}`));
|
|
2770
|
+
};
|
|
2771
|
+
if (bool(ctx, "dry-run")) {
|
|
2772
|
+
emit(ctx, { ...base, dryRun: true }, () => {
|
|
2773
|
+
console.log(`${from} \u2192 ${to}`);
|
|
2774
|
+
printRefs();
|
|
2775
|
+
console.log(t("(dry run: nothing changed; without --dry-run those prompts are rewritten too)", "(simulaci\xF3n: no cambi\xF3 nada; sin --dry-run tambi\xE9n se reescriben esos prompts)"));
|
|
2776
|
+
});
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
await api(ctx, "PATCH", `/databases/${hit.id}`, { name: to });
|
|
2780
|
+
const rewritten = [];
|
|
2781
|
+
const failed = [];
|
|
2782
|
+
for (const r of scan.found) {
|
|
2783
|
+
try {
|
|
2784
|
+
const cur = await api(ctx, "GET", `/agents/${r.agentId}/prompt`);
|
|
2785
|
+
await api(ctx, "PATCH", `/agents/${r.agentId}`, { systemPrompt: replaceName(cur.systemPrompt, from, to) });
|
|
2786
|
+
rewritten.push(r.agentId);
|
|
2787
|
+
} catch (e) {
|
|
2788
|
+
failed.push({ agentId: r.agentId, error: e.message });
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
emit(ctx, { ...base, dryRun: false, rewritten, failed }, () => {
|
|
2792
|
+
console.log(t(`Renamed ${from} \u2192 ${to} (${hit.id}). ${rewritten.length} prompt(s) rewritten.`, `Renombrada ${from} \u2192 ${to} (${hit.id}). ${rewritten.length} prompt(s) reescritos.`));
|
|
2793
|
+
for (const f of failed) console.error(`! ${f.agentId}: ${f.error}`);
|
|
2794
|
+
for (const x of scan.skipped) console.log(t(`! ${x.agent} (${x.agentId}) not checked: ${x.reason}`, `! ${x.agent} (${x.agentId}) sin revisar: ${x.reason}`));
|
|
2795
|
+
console.log(`${t("Undo", "Deshacer")}: easybits db rename ${to} ${from}`);
|
|
2796
|
+
});
|
|
2797
|
+
if (failed.length) process.exitCode = 1;
|
|
2798
|
+
}
|
|
2799
|
+
},
|
|
2800
|
+
tables: {
|
|
2801
|
+
aliases: ["inspect", "schema"],
|
|
2802
|
+
summary: "List tables with row counts and columns, and how many links are permanent",
|
|
2803
|
+
usage: "easybits db tables <db-id|name>",
|
|
2804
|
+
examples: ["easybits db tables leads", "easybits db tables leads --json"],
|
|
2805
|
+
// Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
|
|
2806
|
+
async run(ctx) {
|
|
2807
|
+
const ref = need(ctx, 0, "db-id|name", this.usage);
|
|
2808
|
+
const eb = await getClient(ctx);
|
|
2809
|
+
const hit = await findDb(ctx, ref);
|
|
2810
|
+
const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
|
|
2811
|
+
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]));
|
|
2812
|
+
const tables = [];
|
|
2813
|
+
for (const name of names) {
|
|
2814
|
+
const info2 = await q(`PRAGMA table_info(${ident2(name)})`);
|
|
2815
|
+
const col = (k) => info2.cols.indexOf(k);
|
|
2816
|
+
const columns = info2.rows.map((r) => ({ name: String(r[col("name")]), type: String(r[col("type")] ?? ""), pk: Number(r[col("pk")]) > 0 }));
|
|
2817
|
+
const count = await q(`SELECT count(*) FROM ${ident2(name)}`);
|
|
2818
|
+
const textCols = columns.filter((c) => !c.type || /CHAR|TEXT|CLOB/i.test(c.type)).map((c) => c.name);
|
|
2819
|
+
const links = textCols.length ? parseLinkStats(textCols, (await q(linkStatsSql(name, textCols))).rows[0] ?? []) : [];
|
|
2820
|
+
tables.push({ name, rows: Number(count.rows[0]?.[0] ?? 0), columns, links });
|
|
2821
|
+
}
|
|
2822
|
+
emit(ctx, tables, () => {
|
|
2823
|
+
table(
|
|
2824
|
+
tables.map((t2) => ({
|
|
2825
|
+
name: t2.name,
|
|
2826
|
+
rows: t2.rows,
|
|
2827
|
+
columns: t2.columns.map((c) => `${c.name}${c.type ? ` ${c.type}` : ""}${c.pk ? " PK" : ""}`).join(", ")
|
|
2828
|
+
})),
|
|
2829
|
+
[["name", "TABLE"], ["rows", "ROWS"], ["columns", "COLUMNS"]],
|
|
2830
|
+
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'`)
|
|
2831
|
+
);
|
|
2832
|
+
const withLinks = tables.flatMap((tb) => tb.links.map((l) => ({ ...l, table: tb.name })));
|
|
2833
|
+
if (withLinks.length) {
|
|
2834
|
+
console.log("\n" + t("Links (permanent = EasyBits public storage; expiring = signed URL):", "Ligas (permanente = almacenamiento p\xFAblico de EasyBits; caduca = URL firmada):"));
|
|
2835
|
+
table(
|
|
2836
|
+
withLinks.map((l) => ({ where: `${l.table}.${l.column}`, permanent: l.permanent, expiring: l.expiring, external: l.external, empty: l.empty })),
|
|
2837
|
+
[["where", "COLUMN"], ["permanent", "PERMANENT"], ["expiring", "EXPIRING"], ["external", "EXTERNAL"], ["empty", "EMPTY"]]
|
|
2838
|
+
);
|
|
2839
|
+
if (withLinks.some((l) => l.external + l.expiring > 0)) {
|
|
2840
|
+
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/"));
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
},
|
|
2846
|
+
photos: {
|
|
2847
|
+
summary: "Put catalog photos in a table: SKU-123.jpg \u2192 the row whose key is SKU-123, as a permanent public link",
|
|
2848
|
+
usage: "easybits db photos put <db> --table <table> --key-column <col> --dir <folder> [--column <col>] [--replace] [--dry-run]",
|
|
2849
|
+
options: {
|
|
2850
|
+
table: { type: "string", value: "table", description: "Table to update" },
|
|
2851
|
+
"key-column": { type: "string", value: "col", description: "Column that matches the file name (e.g. sku)" },
|
|
2852
|
+
dir: { type: "string", value: "folder", description: "Folder with the photos (JPEG, PNG or WebP, max 10 MB each)" },
|
|
2853
|
+
column: { type: "string", value: "col", description: "Column for the link (default: image_url, photo_url, imagen\u2026)" },
|
|
2854
|
+
replace: { type: "boolean", description: "Also replace rows that already have a permanent link" },
|
|
2855
|
+
"dry-run": { type: "boolean", description: "Show the plan; upload nothing" }
|
|
2856
|
+
},
|
|
2857
|
+
examples: [
|
|
2858
|
+
"easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --dry-run",
|
|
2859
|
+
"easybits db photos put catalogo --table productos --key-column sku --dir fotos/ --column foto",
|
|
2860
|
+
"easybits db tables catalogo # how many links are permanent, external or empty"
|
|
2861
|
+
],
|
|
2862
|
+
async run(ctx) {
|
|
2863
|
+
const action = need(ctx, 0, "put", this.usage);
|
|
2864
|
+
if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
|
|
2865
|
+
const ref = need(ctx, 1, "db", this.usage);
|
|
2866
|
+
const tableName = str(ctx, "table");
|
|
2867
|
+
const keyCol = str(ctx, "key-column");
|
|
2868
|
+
const dir = str(ctx, "dir");
|
|
2869
|
+
if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
|
|
2870
|
+
if (!existsSync8(dir) || !statSync5(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
|
|
2871
|
+
const { photos, rejected } = photoFiles(dir);
|
|
2872
|
+
const eb = await getClient(ctx);
|
|
2873
|
+
const hit = await findDb(ctx, ref);
|
|
2874
|
+
const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
|
|
2875
|
+
const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
|
|
2876
|
+
const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
|
|
2877
|
+
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);
|
|
2878
|
+
if (!cols.includes(keyCol)) throw usageError(t(`No column "${keyCol}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${keyCol}" en ${tableName} (${cols.join(", ")}).`), this.usage);
|
|
2879
|
+
const column = str(ctx, "column") ?? defaultPhotoColumn(cols);
|
|
2880
|
+
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);
|
|
2881
|
+
if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
|
|
2882
|
+
const current2 = /* @__PURE__ */ new Map();
|
|
2883
|
+
for (let i = 0; i < photos.length; i += 100) {
|
|
2884
|
+
const keys = photos.slice(i, i + 100).map((p) => p.key);
|
|
2885
|
+
const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
|
|
2886
|
+
for (const row of r.rows) current2.set(String(row[0]), row[1]);
|
|
2887
|
+
}
|
|
2888
|
+
const plan = planPhotos(photos, current2, bool(ctx, "replace"));
|
|
2889
|
+
const big = photos.filter((p) => p.bytes > WHATSAPP_MAX_BYTES).map((p) => p.file);
|
|
2890
|
+
const summary = { db: hit.name, table: tableName, keyColumn: keyCol, column, plan, rejected, overWhatsappLimit: big };
|
|
2891
|
+
const count = (a) => plan.filter((p) => p.action === a).length;
|
|
2892
|
+
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");
|
|
2893
|
+
const printPlan2 = () => {
|
|
2894
|
+
for (const p of plan) {
|
|
2895
|
+
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");
|
|
2896
|
+
console.log(`${p.action === "update" ? "~" : "!"} ${p.file} \u2192 ${keyCol}=${p.key}: ${what}`);
|
|
2897
|
+
}
|
|
2898
|
+
for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
|
|
2899
|
+
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(", ")}`));
|
|
2900
|
+
};
|
|
2901
|
+
if (bool(ctx, "dry-run") || !count("update")) {
|
|
2902
|
+
emit(ctx, { ...summary, dryRun: bool(ctx, "dry-run") }, () => {
|
|
2903
|
+
printPlan2();
|
|
2904
|
+
console.log(count("update") ? t("(dry run: nothing uploaded)", "(simulaci\xF3n: no se subi\xF3 nada)") : t("Nothing to update.", "Nada que actualizar."));
|
|
2905
|
+
});
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
const urls = /* @__PURE__ */ new Map();
|
|
2909
|
+
const results = [];
|
|
2910
|
+
for (const item of plan.filter((p) => p.action === "update")) {
|
|
2911
|
+
const photo = photos.find((p) => p.key === item.key);
|
|
2912
|
+
try {
|
|
2913
|
+
let url = urls.get(photo.sha);
|
|
2914
|
+
if (!url) {
|
|
2915
|
+
const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
|
|
2916
|
+
const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync9(photo.path), headers: { "Content-Type": photo.mime } });
|
|
2917
|
+
if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
|
|
2918
|
+
url = up.file.url;
|
|
2919
|
+
if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
|
|
2920
|
+
urls.set(photo.sha, url);
|
|
2921
|
+
}
|
|
2922
|
+
await q(`UPDATE ${ident2(tableName)} SET ${ident2(column)} = ? WHERE ${ident2(keyCol)} = ?`, [url, item.key]);
|
|
2923
|
+
results.push({ key: item.key, file: item.file, url });
|
|
2924
|
+
if (!ctx.json) console.log(`~ ${item.file} \u2192 ${keyCol}=${item.key}: ${url}`);
|
|
2925
|
+
} catch (e) {
|
|
2926
|
+
results.push({ key: item.key, file: item.file, error: e.message });
|
|
2927
|
+
if (!ctx.json) console.error(`! ${item.file}: ${e.message}`);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
const failed = results.filter((r) => r.error).length;
|
|
2931
|
+
emit(ctx, { ...summary, results }, () => {
|
|
2932
|
+
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")}`);
|
|
2933
|
+
for (const r of rejected) console.log(`! ${r.file}: ${why(r.reason)}`);
|
|
2934
|
+
console.log(t(`Updated ${results.length - failed} row(s)${failed ? `, ${failed} failed` : ""}.`, `${results.length - failed} fila(s) actualizadas${failed ? `, ${failed} fallaron` : ""}.`));
|
|
2935
|
+
});
|
|
2936
|
+
if (failed) process.exitCode = 1;
|
|
2937
|
+
}
|
|
2938
|
+
},
|
|
2939
|
+
query: {
|
|
2940
|
+
aliases: ["sql"],
|
|
2941
|
+
summary: "Run one SQL statement (by database id or name)",
|
|
2942
|
+
usage: "easybits db query <db-id|name> <sql> [--arg <value>]...",
|
|
2943
|
+
options: { arg: { type: "string", multiple: true, value: "value", description: "Positional ? parameter (repeatable)" } },
|
|
2944
|
+
examples: [
|
|
2945
|
+
"easybits db query leads 'SELECT * FROM leads LIMIT 10'",
|
|
2946
|
+
"easybits db query leads 'INSERT INTO leads(name) VALUES (?)' --arg Ana",
|
|
2947
|
+
"easybits db query leads 'SELECT count(*) AS n FROM leads' --json"
|
|
2948
|
+
],
|
|
2949
|
+
async run(ctx) {
|
|
2950
|
+
const ref = need(ctx, 0, "db-id|name", this.usage);
|
|
2951
|
+
const sql = need(ctx, 1, "sql", this.usage);
|
|
2952
|
+
const eb = await getClient(ctx);
|
|
2953
|
+
const hit = await findDb(ctx, ref);
|
|
2954
|
+
const args = list(ctx, "arg");
|
|
2955
|
+
const r = await eb.queryDatabase(hit.id, sql, args.length ? args : void 0);
|
|
2956
|
+
emit(ctx, r, () => {
|
|
2957
|
+
if (r.cols.length) {
|
|
2958
|
+
table(
|
|
2959
|
+
r.rows.map((row) => Object.fromEntries(r.cols.map((c, i) => [c, row[i]]))),
|
|
2960
|
+
r.cols.map((c) => [c, c]),
|
|
2961
|
+
"(0 rows)"
|
|
2962
|
+
);
|
|
2963
|
+
} else {
|
|
2964
|
+
console.log(
|
|
2965
|
+
t(
|
|
2966
|
+
`OK, ${r.affected_row_count} row(s) affected${r.last_insert_rowid ? `, last id ${r.last_insert_rowid}` : ""}`,
|
|
2967
|
+
`OK, ${r.affected_row_count} fila(s) afectadas${r.last_insert_rowid ? `, \xFAltimo id ${r.last_insert_rowid}` : ""}`
|
|
2968
|
+
)
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
|
|
2977
|
+
// src/commands/agents.ts
|
|
2978
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
|
|
2979
|
+
|
|
2980
|
+
// src/commands/spec.ts
|
|
2981
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2854
2982
|
import { homedir as homedir3 } from "os";
|
|
2855
2983
|
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
2856
2984
|
|
|
@@ -3209,6 +3337,10 @@ var agents = {
|
|
|
3209
3337
|
console.log(`${t("Agent: ", "Agente: ")} ${a.agentId}`);
|
|
3210
3338
|
console.log(`Sandbox: ${a.sandboxId}`);
|
|
3211
3339
|
console.log(`URL: ${a.agentUrl}`);
|
|
3340
|
+
if (prompt) {
|
|
3341
|
+
const origin = promptFile ?? (promptInline != null ? "--prompt" : `${t("from", "de")} ${source?.source.agentId ?? "export"}`);
|
|
3342
|
+
console.log(`Prompt: ${origin}, ${prompt.length.toLocaleString("en-US")} ${t("chars", "caracteres")}`);
|
|
3343
|
+
}
|
|
3212
3344
|
if (copied) console.log(t(`Copied: ${copied.skills.length} skill(s), ${copied.files.length} file(s)${copied.skippedFiles.length ? ` (${copied.skippedFiles.length} skipped: export without --with-files)` : ""}`, `Copiado: ${copied.skills.length} skill(s), ${copied.files.length} archivo(s)${copied.skippedFiles.length ? ` (${copied.skippedFiles.length} sin copiar: export sin --with-files)` : ""}`));
|
|
3213
3345
|
console.log(t(`Talk to it: easybits agents try ${a.agentId} "hello"`, `H\xE1blale: easybits agents try ${a.agentId} "hola"`));
|
|
3214
3346
|
});
|
|
@@ -3489,7 +3621,7 @@ async function updateNotice(current2) {
|
|
|
3489
3621
|
}
|
|
3490
3622
|
|
|
3491
3623
|
// src/commands/doctor.ts
|
|
3492
|
-
var VERSION = true ? "0.
|
|
3624
|
+
var VERSION = true ? "0.15.0" : "dev";
|
|
3493
3625
|
var doctor2 = {
|
|
3494
3626
|
name: "doctor",
|
|
3495
3627
|
group: "Account",
|
|
@@ -3759,6 +3891,12 @@ var HELP_ES = {
|
|
|
3759
3891
|
"Create a database": "Crea una base de datos",
|
|
3760
3892
|
"What it is for": "Para qu\xE9 es",
|
|
3761
3893
|
"Delete a database (irreversible)": "Borra una base de datos (irreversible)",
|
|
3894
|
+
"Show its tables and rows; delete nothing": "Ense\xF1a sus tablas y filas; no borra nada",
|
|
3895
|
+
"what would be lost": "lo que se perder\xEDa",
|
|
3896
|
+
"Rename a database and rewrite the agent prompts that name it": "Renombra una base y reescribe los prompts de agentes que la nombran",
|
|
3897
|
+
"Rename only; leave your agents' prompts as they are": "S\xF3lo renombra; deja los prompts de tus agentes como est\xE1n",
|
|
3898
|
+
"Show the prompts that name it; change nothing": "Ense\xF1a los prompts que la nombran; no cambia nada",
|
|
3899
|
+
"which prompts name it": "qu\xE9 prompts la nombran",
|
|
3762
3900
|
"asks you to type the name": "te pide teclear el nombre",
|
|
3763
3901
|
"scripts and agents": "scripts y agentes",
|
|
3764
3902
|
"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",
|
|
@@ -3987,7 +4125,7 @@ function normalizeArgs(input) {
|
|
|
3987
4125
|
}
|
|
3988
4126
|
|
|
3989
4127
|
// src/index.ts
|
|
3990
|
-
var VERSION2 = true ? "0.
|
|
4128
|
+
var VERSION2 = true ? "0.15.0" : "dev";
|
|
3991
4129
|
var GLOBAL_OPTIONS = {
|
|
3992
4130
|
json: { type: "boolean" },
|
|
3993
4131
|
token: { type: "string" },
|
package/package.json
CHANGED