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