@easybits.cloud/cli 0.17.1 → 0.19.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 +26 -1
- package/dist/index.js +718 -128
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -413,7 +413,7 @@ async function oauthLogin(ctx, opts = {}) {
|
|
|
413
413
|
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
414
414
|
const state = b64url(randomBytes(16));
|
|
415
415
|
const server = createServer();
|
|
416
|
-
await new Promise((
|
|
416
|
+
await new Promise((resolve4) => server.listen(0, "127.0.0.1", resolve4));
|
|
417
417
|
const port = server.address().port;
|
|
418
418
|
const redirectUri = `http://127.0.0.1:${port}/cb`;
|
|
419
419
|
try {
|
|
@@ -427,7 +427,7 @@ async function oauthLogin(ctx, opts = {}) {
|
|
|
427
427
|
code_challenge: challenge,
|
|
428
428
|
code_challenge_method: "S256"
|
|
429
429
|
});
|
|
430
|
-
const code = await new Promise((
|
|
430
|
+
const code = await new Promise((resolve4, reject) => {
|
|
431
431
|
const timer = setTimeout(
|
|
432
432
|
() => reject(new CliError(t("Login timed out after 5 minutes.", "El login se venci\xF3 a los 5 minutos."), EXIT.AUTH, t("Run: easybits login", "Corre: easybits login"), "login_timeout")),
|
|
433
433
|
LOGIN_TIMEOUT_MS
|
|
@@ -443,7 +443,7 @@ async function oauthLogin(ctx, opts = {}) {
|
|
|
443
443
|
clearTimeout(timer);
|
|
444
444
|
if (ok) {
|
|
445
445
|
res.writeHead(302, { location: `${base}/oauth/listo` }).end();
|
|
446
|
-
|
|
446
|
+
resolve4(got);
|
|
447
447
|
} else {
|
|
448
448
|
res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(FAIL_PAGE(base));
|
|
449
449
|
reject(
|
|
@@ -804,9 +804,9 @@ var websites = {
|
|
|
804
804
|
usage: "easybits websites slug <websiteId|slug> <new-slug>",
|
|
805
805
|
examples: ["easybits websites slug rio-durmiente mi-tienda"],
|
|
806
806
|
async run(ctx) {
|
|
807
|
-
const
|
|
807
|
+
const ref2 = need(ctx, 0, "website", this.usage);
|
|
808
808
|
const slug = need(ctx, 1, "new-slug", this.usage);
|
|
809
|
-
const websiteId = await resolveWebsiteId(ctx,
|
|
809
|
+
const websiteId = await resolveWebsiteId(ctx, ref2);
|
|
810
810
|
const { website } = await api(ctx, "PATCH", `/websites/${websiteId}`, { slug });
|
|
811
811
|
emit(ctx, website, () => console.log(t(`Slug changed \u2192 ${website.url}`, `Slug cambiado \u2192 ${website.url}`)));
|
|
812
812
|
}
|
|
@@ -862,14 +862,14 @@ var websites = {
|
|
|
862
862
|
options: { path: { type: "string", value: "path", description: "Path inside the site (default: the file name), e.g. img/hero.jpg" } },
|
|
863
863
|
examples: ["easybits websites upload mi-tienda ./index.html", "easybits websites upload mi-tienda ./hero.jpg --path img/hero.jpg"],
|
|
864
864
|
async run(ctx) {
|
|
865
|
-
const
|
|
865
|
+
const ref2 = need(ctx, 0, "website", this.usage);
|
|
866
866
|
const local = need(ctx, 1, "local-file", this.usage);
|
|
867
867
|
if (!existsSync2(local) || !statSync2(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), this.usage);
|
|
868
868
|
const path = sitePath(str(ctx, "path") ?? basename(local));
|
|
869
869
|
if (!path) throw usageError(t("--path must be a relative path inside the site (no ..).", "--path debe ser una ruta relativa dentro del sitio (sin ..)."), this.usage);
|
|
870
870
|
const size = statSync2(local).size;
|
|
871
871
|
if (size === 0) throw usageError(t(`${local} is empty.`, `${local} est\xE1 vac\xEDo.`), this.usage);
|
|
872
|
-
const id = await resolveWebsiteId(ctx,
|
|
872
|
+
const id = await resolveWebsiteId(ctx, ref2);
|
|
873
873
|
const r = await putSiteFile(ctx, id, path, local, size);
|
|
874
874
|
emit(ctx, r, () => console.log(t(`Uploaded ${path} (${fmtBytes(size)}) \u2192 ${r.url}`, `Subido ${path} (${fmtBytes(size)}) \u2192 ${r.url}`)));
|
|
875
875
|
}
|
|
@@ -880,7 +880,7 @@ var websites = {
|
|
|
880
880
|
options: { "dry-run": { type: "boolean", description: "List what would be uploaded; upload nothing" } },
|
|
881
881
|
examples: ["easybits websites deploy mi-tienda ./dist", "easybits websites deploy mi-tienda ./dist --dry-run"],
|
|
882
882
|
async run(ctx) {
|
|
883
|
-
const
|
|
883
|
+
const ref2 = need(ctx, 0, "website", this.usage);
|
|
884
884
|
const dir = need(ctx, 1, "dir", this.usage);
|
|
885
885
|
if (!existsSync2(dir) || !statSync2(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
|
|
886
886
|
const all = walkSite(dir);
|
|
@@ -888,7 +888,7 @@ var websites = {
|
|
|
888
888
|
const empty = all.filter((f) => f.size === 0).map((f) => f.path);
|
|
889
889
|
if (!files3.length) throw usageError(t(`Nothing to upload in ${dir} (hidden files and node_modules are skipped).`, `Nada que subir en ${dir} (se saltan los ocultos y node_modules).`), this.usage);
|
|
890
890
|
if (!files3.some((f) => f.path === "index.html")) console.error(t("Note: no index.html at the top of the folder; /s/<slug>/ will not have a home page.", "Ojo: no hay index.html en la ra\xEDz de la carpeta; /s/<slug>/ no tendr\xE1 p\xE1gina de inicio."));
|
|
891
|
-
const id = await resolveWebsiteId(ctx,
|
|
891
|
+
const id = await resolveWebsiteId(ctx, ref2);
|
|
892
892
|
if (bool(ctx, "dry-run")) {
|
|
893
893
|
emit(ctx, { websiteId: id, files: files3.map(({ path, size }) => ({ path, size })), skippedEmpty: empty }, () => {
|
|
894
894
|
for (const f of files3) console.log(` ${f.path} ${fmtBytes(f.size)}`);
|
|
@@ -921,9 +921,9 @@ var websites = {
|
|
|
921
921
|
options: { ...YES_OPTION },
|
|
922
922
|
examples: ["easybits websites rm mi-tienda", "easybits websites rm mi-tienda --yes"],
|
|
923
923
|
async run(ctx) {
|
|
924
|
-
const
|
|
924
|
+
const ref2 = need(ctx, 0, "website", this.usage);
|
|
925
925
|
requireYesIfHeadless(ctx);
|
|
926
|
-
const id = await resolveWebsiteId(ctx,
|
|
926
|
+
const id = await resolveWebsiteId(ctx, ref2);
|
|
927
927
|
const w = await api(ctx, "GET", `/websites/${id}`);
|
|
928
928
|
await confirm(ctx, t(`Delete website ${w.slug} (${w.fileCount ?? 0} files) at ${w.url}?`, `\xBFBorrar el sitio ${w.slug} (${w.fileCount ?? 0} archivos) en ${w.url}?`), { typeName: w.slug });
|
|
929
929
|
const r = await api(ctx, "DELETE", `/websites/${id}`);
|
|
@@ -940,21 +940,21 @@ async function putSiteFile(ctx, websiteId, path, local, size) {
|
|
|
940
940
|
if (!res.ok) throw new CliError(t(`Upload of ${path} failed: storage answered ${res.status}`, `Fall\xF3 la subida de ${path}: el almacenamiento contest\xF3 ${res.status}`), EXIT.API, void 0, "upload_failed", res.status);
|
|
941
941
|
return { path, fileId: r.file.id, url: r.file.url, size, contentType };
|
|
942
942
|
}
|
|
943
|
-
function matchWebsite(
|
|
944
|
-
if (/^[0-9a-f]{24}$/i.test(
|
|
945
|
-
const r =
|
|
943
|
+
function matchWebsite(ref2, items) {
|
|
944
|
+
if (/^[0-9a-f]{24}$/i.test(ref2)) return ref2;
|
|
945
|
+
const r = ref2.trim().toLowerCase();
|
|
946
946
|
return items.find((w) => w.slug === r)?.id ?? null;
|
|
947
947
|
}
|
|
948
|
-
async function resolveWebsiteId(ctx,
|
|
949
|
-
if (/^[0-9a-f]{24}$/i.test(
|
|
948
|
+
async function resolveWebsiteId(ctx, ref2) {
|
|
949
|
+
if (/^[0-9a-f]{24}$/i.test(ref2)) return ref2;
|
|
950
950
|
for (let offset = 0; ; offset += 100) {
|
|
951
951
|
const page = await api(ctx, "GET", `/websites?limit=100&offset=${offset}`);
|
|
952
|
-
const id = matchWebsite(
|
|
952
|
+
const id = matchWebsite(ref2, page.items);
|
|
953
953
|
if (id) return id;
|
|
954
954
|
if (page.items.length < 100 || offset + 100 >= page.total) break;
|
|
955
955
|
}
|
|
956
956
|
throw new CliError(
|
|
957
|
-
t(`No website with id or slug "${
|
|
957
|
+
t(`No website with id or slug "${ref2}".`, `No hay sitio con id o slug "${ref2}".`),
|
|
958
958
|
EXIT.API,
|
|
959
959
|
`${t("List them with", "Enl\xEDstalos con")}: easybits websites ls`,
|
|
960
960
|
"not_found",
|
|
@@ -1410,8 +1410,8 @@ var sshProxy = {
|
|
|
1410
1410
|
const { url } = await sb.sshTicket();
|
|
1411
1411
|
const ws = new WebSocket(url);
|
|
1412
1412
|
ws.binaryType = "arraybuffer";
|
|
1413
|
-
await new Promise((
|
|
1414
|
-
ws.onopen = () =>
|
|
1413
|
+
await new Promise((resolve4, reject) => {
|
|
1414
|
+
ws.onopen = () => resolve4();
|
|
1415
1415
|
ws.onerror = () => reject(new CliError(t("easybits ssh-proxy: could not open the tunnel", "easybits ssh-proxy: no se pudo abrir el t\xFAnel")));
|
|
1416
1416
|
});
|
|
1417
1417
|
ws.onerror = () => {
|
|
@@ -1436,21 +1436,21 @@ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync
|
|
|
1436
1436
|
|
|
1437
1437
|
// src/commands/domains.ts
|
|
1438
1438
|
import { EasybitsError as EasybitsError3 } from "@easybits.cloud/sdk";
|
|
1439
|
-
function matchDomain(
|
|
1440
|
-
if (/^[0-9a-f]{24}$/i.test(
|
|
1441
|
-
const r =
|
|
1439
|
+
function matchDomain(ref2, items) {
|
|
1440
|
+
if (/^[0-9a-f]{24}$/i.test(ref2)) return ref2;
|
|
1441
|
+
const r = ref2.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
1442
1442
|
return items.find((d) => d.domain === r)?.id ?? null;
|
|
1443
1443
|
}
|
|
1444
1444
|
async function listDomains(ctx) {
|
|
1445
1445
|
return (await api(ctx, "GET", "/domains")).domains;
|
|
1446
1446
|
}
|
|
1447
|
-
async function resolveDomain(ctx,
|
|
1448
|
-
if (/^[0-9a-f]{24}$/i.test(
|
|
1447
|
+
async function resolveDomain(ctx, ref2) {
|
|
1448
|
+
if (/^[0-9a-f]{24}$/i.test(ref2)) return { id: ref2 };
|
|
1449
1449
|
const items = await listDomains(ctx);
|
|
1450
|
-
const id = matchDomain(
|
|
1450
|
+
const id = matchDomain(ref2, items);
|
|
1451
1451
|
if (id) return { id, row: items.find((d) => d.id === id) };
|
|
1452
1452
|
throw new CliError(
|
|
1453
|
-
t(`No domain "${
|
|
1453
|
+
t(`No domain "${ref2}" in your account.`, `No hay dominio "${ref2}" en tu cuenta.`),
|
|
1454
1454
|
EXIT.API,
|
|
1455
1455
|
`${t("List them with", "Enl\xEDstalos con")}: easybits domains ls`,
|
|
1456
1456
|
"not_found",
|
|
@@ -1573,11 +1573,11 @@ var domains = {
|
|
|
1573
1573
|
options: { clear: { type: "boolean", description: "Stop serving a website at the root" } },
|
|
1574
1574
|
examples: ["easybits domains apex example.com mi-tienda", "easybits domains apex example.com --clear"],
|
|
1575
1575
|
async run(ctx) {
|
|
1576
|
-
const
|
|
1576
|
+
const ref2 = need(ctx, 0, "domain", this.usage);
|
|
1577
1577
|
const clear = bool(ctx, "clear");
|
|
1578
1578
|
if (!clear && !ctx.args[1]) throw usageError(t("Missing <website> (or pass --clear).", "Falta <website> (o pasa --clear)."), this.usage);
|
|
1579
1579
|
if (clear && ctx.args[1]) throw usageError(t("Pass a <website> or --clear, not both.", "Pasa un <website> o --clear, no ambos."), this.usage);
|
|
1580
|
-
const { id } = await resolveDomain(ctx,
|
|
1580
|
+
const { id } = await resolveDomain(ctx, ref2);
|
|
1581
1581
|
const websiteId = clear ? null : await resolveWebsiteId(ctx, ctx.args[1]);
|
|
1582
1582
|
const { domain } = await api(ctx, "POST", `/domains/${id}/apex`, { websiteId });
|
|
1583
1583
|
emit(
|
|
@@ -1596,10 +1596,10 @@ var domains = {
|
|
|
1596
1596
|
options: { ...YES_OPTION },
|
|
1597
1597
|
examples: ["easybits domains rm example.com", "easybits domains rm example.com --yes"],
|
|
1598
1598
|
async run(ctx) {
|
|
1599
|
-
const
|
|
1599
|
+
const ref2 = need(ctx, 0, "domain", this.usage);
|
|
1600
1600
|
requireYesIfHeadless(ctx);
|
|
1601
|
-
const { id, row } = await resolveDomain(ctx,
|
|
1602
|
-
const name = row?.domain ??
|
|
1601
|
+
const { id, row } = await resolveDomain(ctx, ref2);
|
|
1602
|
+
const name = row?.domain ?? ref2;
|
|
1603
1603
|
await confirm(ctx, t(`Remove ${name}? Its websites stop answering on it right away.`, `\xBFQuitar ${name}? Sus sitios dejan de responder en \xE9l de inmediato.`));
|
|
1604
1604
|
const r = await api(ctx, "DELETE", "/domains", { domainId: id });
|
|
1605
1605
|
emit(ctx, { ...r, domainId: id, domain: name }, () => console.log(t(`Removed ${name}`, `Quitado ${name}`)));
|
|
@@ -2155,7 +2155,7 @@ var ID_FORMAT = {
|
|
|
2155
2155
|
document: /^[0-9a-f]{24}$/i,
|
|
2156
2156
|
video: /^[0-9a-f]{24}$/i
|
|
2157
2157
|
};
|
|
2158
|
-
var looksLikeId = (kind,
|
|
2158
|
+
var looksLikeId = (kind, ref2) => ID_FORMAT[kind].test(ref2);
|
|
2159
2159
|
var REF_ARGS = {
|
|
2160
2160
|
"agents get": { index: 0, kind: "agent" },
|
|
2161
2161
|
"agents message": { index: 0, kind: "agent" },
|
|
@@ -2215,11 +2215,11 @@ var REF_ARGS = {
|
|
|
2215
2215
|
"video music": { index: 1, kind: "video" },
|
|
2216
2216
|
"video render": { index: 0, kind: "video" }
|
|
2217
2217
|
};
|
|
2218
|
-
function matchRef(kind,
|
|
2219
|
-
if (looksLikeId(kind,
|
|
2218
|
+
function matchRef(kind, ref2, items) {
|
|
2219
|
+
if (looksLikeId(kind, ref2)) return { ok: true, id: ref2 };
|
|
2220
2220
|
const list2 = items ?? [];
|
|
2221
|
-
if (list2.some((x) => x.id ===
|
|
2222
|
-
const want =
|
|
2221
|
+
if (list2.some((x) => x.id === ref2)) return { ok: true, id: ref2 };
|
|
2222
|
+
const want = ref2.trim().toLowerCase();
|
|
2223
2223
|
const matches = list2.filter((x) => (x.name ?? "").trim().toLowerCase() === want);
|
|
2224
2224
|
if (matches.length === 1) return { ok: true, id: matches[0].id };
|
|
2225
2225
|
if (matches.length === 0) return { ok: false, reason: "not_found" };
|
|
@@ -2239,15 +2239,15 @@ var LS_HINT = {
|
|
|
2239
2239
|
var NOUN = () => ({ agent: t("agent", "agente"), sandbox: t("sandbox or machine", "sandbox o m\xE1quina"), db: t("database", "base de datos"), site: t("site", "sitio"), fleet: t("fleet agent", "agente de flota"), document: t("document", "documento"), video: t("video project", "proyecto de video") });
|
|
2240
2240
|
var NOUNS = () => ({ agent: t("agents", "agentes"), sandbox: t("sandboxes", "sandboxes"), db: t("databases", "bases de datos"), site: t("sites", "sitios"), fleet: t("fleet agents", "agentes de flota"), document: t("documents", "documentos"), video: t("video projects", "proyectos de video") });
|
|
2241
2241
|
var cache = /* @__PURE__ */ new Map();
|
|
2242
|
-
function listRefs(ctx, kind,
|
|
2243
|
-
const key = kind === "document" ? `document:${
|
|
2242
|
+
function listRefs(ctx, kind, ref2) {
|
|
2243
|
+
const key = kind === "document" ? `document:${ref2}` : kind;
|
|
2244
2244
|
let p = cache.get(key);
|
|
2245
2245
|
if (!p) {
|
|
2246
2246
|
p = (async () => {
|
|
2247
2247
|
const eb = await getClient(ctx);
|
|
2248
2248
|
if (kind === "agent") return (await eb.listAgents()).map((a) => ({ id: a.agentId, name: a.name, status: a.status }));
|
|
2249
2249
|
if (kind === "fleet") return (await api(ctx, "GET", "/fleet-agents")).pools.map((p2) => ({ id: p2.id, name: p2.name }));
|
|
2250
|
-
if (kind === "document") return (await api(ctx, "GET", `/documents?limit=100&search=${encodeURIComponent(
|
|
2250
|
+
if (kind === "document") return (await api(ctx, "GET", `/documents?limit=100&search=${encodeURIComponent(ref2 ?? "")}`)).items.map((d) => ({ id: d.id, name: d.name }));
|
|
2251
2251
|
if (kind === "video") return (await api(ctx, "GET", "/video-projects?limit=100")).items.map((v) => ({ id: v.id, name: v.name, status: v.status }));
|
|
2252
2252
|
if (kind === "site") return (await api(ctx, "GET", "/sites")).items.map((s) => ({ id: s.id, name: s.name }));
|
|
2253
2253
|
if (kind === "db") return (await eb.listDatabases()).items.map((d) => ({ id: d.id, name: d.name }));
|
|
@@ -2258,17 +2258,17 @@ function listRefs(ctx, kind, ref) {
|
|
|
2258
2258
|
}
|
|
2259
2259
|
return p;
|
|
2260
2260
|
}
|
|
2261
|
-
async function resolveRef(ctx, kind,
|
|
2262
|
-
if (looksLikeId(kind,
|
|
2263
|
-
const r = matchRef(kind,
|
|
2261
|
+
async function resolveRef(ctx, kind, ref2) {
|
|
2262
|
+
if (looksLikeId(kind, ref2)) return ref2;
|
|
2263
|
+
const r = matchRef(kind, ref2, await listRefs(ctx, kind, ref2));
|
|
2264
2264
|
if (r.ok) return r.id;
|
|
2265
2265
|
if (r.reason === "not_found") {
|
|
2266
|
-
throw new CliError(t(`No ${NOUN()[kind]} with id or name "${
|
|
2266
|
+
throw new CliError(t(`No ${NOUN()[kind]} with id or name "${ref2}".`, `No hay ${NOUN()[kind]} con id o nombre "${ref2}".`), EXIT.API, `${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`, "not_found", 404);
|
|
2267
2267
|
}
|
|
2268
2268
|
const shown = r.matches.slice(0, 10).map((m) => ` ${m.id}${m.status ? ` (${m.status})` : ""}`);
|
|
2269
2269
|
if (r.matches.length > 10) shown.push(t(` \u2026and ${r.matches.length - 10} more`, ` \u2026y ${r.matches.length - 10} m\xE1s`));
|
|
2270
2270
|
throw new CliError(
|
|
2271
|
-
t(`"${
|
|
2271
|
+
t(`"${ref2}" matches ${r.matches.length} ${NOUNS()[kind]}. Use the id:`, `"${ref2}" coincide con ${r.matches.length} ${NOUNS()[kind]}. Usa el id:`),
|
|
2272
2272
|
EXIT.USAGE,
|
|
2273
2273
|
`${shown.join("\n")}
|
|
2274
2274
|
${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`,
|
|
@@ -2277,9 +2277,9 @@ ${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`,
|
|
|
2277
2277
|
}
|
|
2278
2278
|
async function resolveRefs(ctx, key) {
|
|
2279
2279
|
const spec = REF_ARGS[key];
|
|
2280
|
-
const
|
|
2281
|
-
if (!spec || !
|
|
2282
|
-
ctx.args[spec.index] = await resolveRef(ctx, spec.kind,
|
|
2280
|
+
const ref2 = spec ? ctx.args[spec.index] : void 0;
|
|
2281
|
+
if (!spec || !ref2) return;
|
|
2282
|
+
ctx.args[spec.index] = await resolveRef(ctx, spec.kind, ref2);
|
|
2283
2283
|
}
|
|
2284
2284
|
|
|
2285
2285
|
// src/commands/machines.ts
|
|
@@ -3808,9 +3808,9 @@ var db = {
|
|
|
3808
3808
|
"easybits db rm leads --yes # scripts and agents"
|
|
3809
3809
|
],
|
|
3810
3810
|
async run(ctx) {
|
|
3811
|
-
const
|
|
3811
|
+
const ref2 = need(ctx, 0, "db-id|name", this.usage);
|
|
3812
3812
|
if (bool(ctx, "dry-run")) {
|
|
3813
|
-
const hit2 = await findDb(ctx,
|
|
3813
|
+
const hit2 = await findDb(ctx, ref2);
|
|
3814
3814
|
const tables = await tableCounts(ctx, hit2.id);
|
|
3815
3815
|
const rows = tables.reduce((n, x) => n + x.rows, 0);
|
|
3816
3816
|
emit(ctx, { id: hit2.id, name: hit2.name, dryRun: true, tables }, () => {
|
|
@@ -3823,7 +3823,7 @@ var db = {
|
|
|
3823
3823
|
}
|
|
3824
3824
|
requireYesIfHeadless(ctx);
|
|
3825
3825
|
const eb = await getClient(ctx);
|
|
3826
|
-
const hit = await findDb(ctx,
|
|
3826
|
+
const hit = await findDb(ctx, ref2);
|
|
3827
3827
|
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 });
|
|
3828
3828
|
const r = await eb.deleteDatabase(hit.id);
|
|
3829
3829
|
emit(ctx, { ...r, id: hit.id, name: hit.name }, () => console.log(t(`Deleted ${hit.name} (${hit.id})`, `Borrada ${hit.name} (${hit.id})`)));
|
|
@@ -3845,10 +3845,10 @@ var db = {
|
|
|
3845
3845
|
// El id no cambia (las tools db_* van por id). Se reescribe el nombre como palabra suelta:
|
|
3846
3846
|
// `totequim.com`, `@totequim`, `totequim_prueba` y «TOTEQUIM» se quedan (db-name-refs.ts).
|
|
3847
3847
|
async run(ctx) {
|
|
3848
|
-
const
|
|
3848
|
+
const ref2 = need(ctx, 0, "db-id|name", this.usage);
|
|
3849
3849
|
const to = need(ctx, 1, "new-name", this.usage).trim();
|
|
3850
3850
|
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);
|
|
3851
|
-
const hit = await findDb(ctx,
|
|
3851
|
+
const hit = await findDb(ctx, ref2);
|
|
3852
3852
|
const from = hit.name;
|
|
3853
3853
|
if (to === from) throw usageError(t(`${from} already has that name.`, `${from} ya se llama as\xED.`), this.usage);
|
|
3854
3854
|
const taken = (await listRefs(ctx, "db")).find((d) => d.name === to);
|
|
@@ -3903,9 +3903,9 @@ var db = {
|
|
|
3903
3903
|
examples: ["easybits db tables leads", "easybits db tables leads --json"],
|
|
3904
3904
|
// Patrón de `turso db inspect` / `.tables` + `.schema` de sqlite3, en una sola vista.
|
|
3905
3905
|
async run(ctx) {
|
|
3906
|
-
const
|
|
3906
|
+
const ref2 = need(ctx, 0, "db-id|name", this.usage);
|
|
3907
3907
|
const eb = await getClient(ctx);
|
|
3908
|
-
const hit = await findDb(ctx,
|
|
3908
|
+
const hit = await findDb(ctx, ref2);
|
|
3909
3909
|
const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
|
|
3910
3910
|
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]));
|
|
3911
3911
|
const tables = [];
|
|
@@ -3961,7 +3961,7 @@ var db = {
|
|
|
3961
3961
|
async run(ctx) {
|
|
3962
3962
|
const action = need(ctx, 0, "put", this.usage);
|
|
3963
3963
|
if (action !== "put") throw usageError(t(`Unknown photos action "${action}".`, `Acci\xF3n de photos desconocida "${action}".`), this.usage);
|
|
3964
|
-
const
|
|
3964
|
+
const ref2 = need(ctx, 1, "db", this.usage);
|
|
3965
3965
|
const tableName = str(ctx, "table");
|
|
3966
3966
|
const keyCol = str(ctx, "key-column");
|
|
3967
3967
|
const dir = str(ctx, "dir");
|
|
@@ -3969,7 +3969,7 @@ var db = {
|
|
|
3969
3969
|
if (!existsSync10(dir) || !statSync7(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
|
|
3970
3970
|
const { photos, rejected } = photoFiles(dir);
|
|
3971
3971
|
const eb = await getClient(ctx);
|
|
3972
|
-
const hit = await findDb(ctx,
|
|
3972
|
+
const hit = await findDb(ctx, ref2);
|
|
3973
3973
|
const q = (sql, args) => eb.queryDatabase(hit.id, sql, args);
|
|
3974
3974
|
const info2 = await q(`PRAGMA table_info(${ident2(tableName)})`);
|
|
3975
3975
|
const cols = info2.rows.map((r) => String(r[info2.cols.indexOf("name")]));
|
|
@@ -4046,10 +4046,10 @@ var db = {
|
|
|
4046
4046
|
"easybits db query leads 'SELECT count(*) AS n FROM leads' --json"
|
|
4047
4047
|
],
|
|
4048
4048
|
async run(ctx) {
|
|
4049
|
-
const
|
|
4049
|
+
const ref2 = need(ctx, 0, "db-id|name", this.usage);
|
|
4050
4050
|
const sql = need(ctx, 1, "sql", this.usage);
|
|
4051
4051
|
const eb = await getClient(ctx);
|
|
4052
|
-
const hit = await findDb(ctx,
|
|
4052
|
+
const hit = await findDb(ctx, ref2);
|
|
4053
4053
|
const args = list(ctx, "arg");
|
|
4054
4054
|
const r = await eb.queryDatabase(hit.id, sql, args.length ? args : void 0);
|
|
4055
4055
|
emit(ctx, r, () => {
|
|
@@ -4239,9 +4239,9 @@ async function applyTarget(ctx, target, o) {
|
|
|
4239
4239
|
if (res.failed) process.exitCode = EXIT.API;
|
|
4240
4240
|
return;
|
|
4241
4241
|
}
|
|
4242
|
-
const
|
|
4243
|
-
if (!
|
|
4244
|
-
const id = await resolveRef(ctx, "agent",
|
|
4242
|
+
const ref2 = o.agent ?? specId(text);
|
|
4243
|
+
if (!ref2) throw usageError(t("The file has no agent id.", "El archivo no trae id de agente."), t("Point it at one with --agent <name|id>, or make a new one with --create.", "Ap\xFAntalo a uno con --agent <nombre|id>, o crea uno nuevo con --create."));
|
|
4244
|
+
const id = await resolveRef(ctx, "agent", ref2);
|
|
4245
4245
|
if (missing.length && !ctx.json) process.stderr.write(t(`Not in your environment (keep their current value): ${missing.join(", ")}
|
|
4246
4246
|
`, `No est\xE1n en tu entorno (conservan su valor de hoy): ${missing.join(", ")}
|
|
4247
4247
|
`));
|
|
@@ -4918,7 +4918,7 @@ async function updateNotice(current2) {
|
|
|
4918
4918
|
}
|
|
4919
4919
|
|
|
4920
4920
|
// src/commands/doctor.ts
|
|
4921
|
-
var VERSION = true ? "0.
|
|
4921
|
+
var VERSION = true ? "0.19.0" : "dev";
|
|
4922
4922
|
var doctor2 = {
|
|
4923
4923
|
name: "doctor",
|
|
4924
4924
|
group: "Account",
|
|
@@ -5152,10 +5152,10 @@ function scopeList(value) {
|
|
|
5152
5152
|
const out = [...new Set(value.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean))];
|
|
5153
5153
|
return out.length ? out : void 0;
|
|
5154
5154
|
}
|
|
5155
|
-
function matchKey(
|
|
5156
|
-
const byId = keys2.find((k) => k.id ===
|
|
5155
|
+
function matchKey(ref2, keys2) {
|
|
5156
|
+
const byId = keys2.find((k) => k.id === ref2);
|
|
5157
5157
|
if (byId) return { id: byId.id };
|
|
5158
|
-
const r =
|
|
5158
|
+
const r = ref2.trim();
|
|
5159
5159
|
const found = keys2.filter((k) => k.status !== "REVOKED" && (k.prefix === r || k.name.toLowerCase() === r.toLowerCase()));
|
|
5160
5160
|
return found.length === 1 ? { id: found[0].id } : { matches: found };
|
|
5161
5161
|
}
|
|
@@ -5252,15 +5252,15 @@ var webhooks = {
|
|
|
5252
5252
|
}
|
|
5253
5253
|
}
|
|
5254
5254
|
};
|
|
5255
|
-
async function resolveKey(ctx,
|
|
5255
|
+
async function resolveKey(ctx, ref2) {
|
|
5256
5256
|
const { keys: keys2 } = await (await getClient(ctx)).listKeys();
|
|
5257
|
-
const m = matchKey(
|
|
5257
|
+
const m = matchKey(ref2, keys2);
|
|
5258
5258
|
if ("id" in m) return keys2.find((k) => k.id === m.id);
|
|
5259
5259
|
if (!m.matches.length) {
|
|
5260
|
-
throw new CliError(t(`No active key with id, name or prefix "${
|
|
5260
|
+
throw new CliError(t(`No active key with id, name or prefix "${ref2}".`, `No hay key activa con id, nombre o prefijo "${ref2}".`), EXIT.API, `${t("List them with", "Enl\xEDstalas con")}: easybits keys ls`, "not_found", 404);
|
|
5261
5261
|
}
|
|
5262
5262
|
throw new CliError(
|
|
5263
|
-
t(`"${
|
|
5263
|
+
t(`"${ref2}" matches ${m.matches.length} keys. Use the id:`, `"${ref2}" coincide con ${m.matches.length} keys. Usa el id:`),
|
|
5264
5264
|
EXIT.USAGE,
|
|
5265
5265
|
m.matches.map((k) => ` ${k.id} ${k.prefix}\u2026 ${k.name}`).join("\n"),
|
|
5266
5266
|
"ambiguous"
|
|
@@ -5322,9 +5322,9 @@ var keys = {
|
|
|
5322
5322
|
options: { ...YES_OPTION },
|
|
5323
5323
|
examples: ["easybits keys revoke ci-deploy", "easybits keys revoke eb_sk_live_abcd1234 --yes"],
|
|
5324
5324
|
async run(ctx) {
|
|
5325
|
-
const
|
|
5325
|
+
const ref2 = need(ctx, 0, "key", this.usage);
|
|
5326
5326
|
requireYesIfHeadless(ctx);
|
|
5327
|
-
const k = await resolveKey(ctx,
|
|
5327
|
+
const k = await resolveKey(ctx, ref2);
|
|
5328
5328
|
const self = (await resolveCredential(ctx)).startsWith(k.prefix);
|
|
5329
5329
|
await confirm(
|
|
5330
5330
|
ctx,
|
|
@@ -5425,6 +5425,188 @@ var secrets = {
|
|
|
5425
5425
|
};
|
|
5426
5426
|
|
|
5427
5427
|
// src/commands/crons.ts
|
|
5428
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
5429
|
+
import { homedir as homedir7 } from "os";
|
|
5430
|
+
import { basename as basename5, resolve as resolve3 } from "path";
|
|
5431
|
+
var HEX24 = /^[a-f0-9]{24}$/;
|
|
5432
|
+
var isUserCronId = (ref2) => HEX24.test(ref2);
|
|
5433
|
+
function describeCronAction(a) {
|
|
5434
|
+
if (a.type === "http") return `${a.method} ${a.url}${a.secretName ? ` (+${a.secretName})` : ""}`;
|
|
5435
|
+
if (a.type === "exec") {
|
|
5436
|
+
const extra = [a.files?.length ? `${a.files.length} file(s)` : "", a.secrets?.length ? `+${a.secrets.map((x) => x.name).join(",")}` : ""].filter(Boolean).join(" ");
|
|
5437
|
+
return `run: ${a.command.length > 40 ? a.command.slice(0, 39) + "\u2026" : a.command}${extra ? ` (${extra})` : ""}`;
|
|
5438
|
+
}
|
|
5439
|
+
return `agent ${a.fleetAgentId}: ${a.prompt.length > 40 ? a.prompt.slice(0, 39) + "\u2026" : a.prompt}`;
|
|
5440
|
+
}
|
|
5441
|
+
async function resolveUserCron(ctx, ref2) {
|
|
5442
|
+
const { items } = await api(ctx, "GET", "/crons");
|
|
5443
|
+
const byId = items.find((c) => c.id === ref2);
|
|
5444
|
+
if (byId) return byId;
|
|
5445
|
+
const byName = items.filter((c) => c.name.toLowerCase() === ref2.trim().toLowerCase());
|
|
5446
|
+
if (byName.length > 1) {
|
|
5447
|
+
throw new CliError(t(`"${ref2}" matches ${byName.length} crons. Use the id.`, `"${ref2}" coincide con ${byName.length} crons. Usa el id.`), EXIT.USAGE);
|
|
5448
|
+
}
|
|
5449
|
+
return byName[0] ?? null;
|
|
5450
|
+
}
|
|
5451
|
+
async function userCronOrNull(ctx, ref2) {
|
|
5452
|
+
if (isUserCronId(ref2)) return await resolveUserCron(ctx, ref2) ?? { id: ref2 };
|
|
5453
|
+
return resolveUserCron(ctx, ref2);
|
|
5454
|
+
}
|
|
5455
|
+
var ACTION_OPTIONS = {
|
|
5456
|
+
url: { type: "string", value: "https-url", description: "Call this public https URL on each run" },
|
|
5457
|
+
method: { type: "string", value: "GET|POST|PUT|PATCH|DELETE", description: "HTTP method (default GET)" },
|
|
5458
|
+
body: { type: "string", value: "json|text", description: "Request body (not with GET)" },
|
|
5459
|
+
secret: { type: "string", multiple: true, value: "NAME", description: "Vault secret: a header with --url (default Authorization: Bearer); an env var with --run (repeatable)" },
|
|
5460
|
+
"secret-header": { type: "string", value: "header", description: "Header for --secret" },
|
|
5461
|
+
agent: { type: "string", value: "id", description: "Send a turn to this fleet agent instead of calling a URL" },
|
|
5462
|
+
prompt: { type: "string", value: "text", description: "What the agent is told on each run (with --agent)" },
|
|
5463
|
+
run: { type: "string", value: "command", description: "Run this shell command in a throwaway box instead (python, files and secrets included; success = exit 0)" },
|
|
5464
|
+
file: { type: "string", multiple: true, value: "local[=remote]", description: "File mounted for --run, next to the command by default (repeatable)" },
|
|
5465
|
+
"secret-file": { type: "string", multiple: true, value: "NAME:local[=remote]", description: "Upload a local file as a secret only this cron can read, written at the same ~ path in the box (repeatable; e.g. a .p8)" },
|
|
5466
|
+
template: { type: "string", value: "python|node|bun|ubuntu", description: "Box image for --run (default python)" },
|
|
5467
|
+
timeout: { type: "string", value: "seconds", description: "Time limit for --run (default 300, max 600)" }
|
|
5468
|
+
};
|
|
5469
|
+
function remotePathFor(local, home = homedir7()) {
|
|
5470
|
+
const abs = resolve3(local.replace(/^~(?=\/)/, home));
|
|
5471
|
+
return abs.startsWith(home + "/") ? `~/${abs.slice(home.length + 1)}` : basename5(abs);
|
|
5472
|
+
}
|
|
5473
|
+
function execFromFlags(f, read, home = homedir7()) {
|
|
5474
|
+
if (!f.run) return {};
|
|
5475
|
+
const expand = (p) => resolve3(p.replace(/^~(?=\/)/, home));
|
|
5476
|
+
const split = (spec) => {
|
|
5477
|
+
const eq = spec.indexOf("=");
|
|
5478
|
+
return eq < 0 ? [spec, void 0] : [spec.slice(0, eq), spec.slice(eq + 1)];
|
|
5479
|
+
};
|
|
5480
|
+
const files3 = f.files.map((spec) => {
|
|
5481
|
+
const [local, remote] = split(spec);
|
|
5482
|
+
return { path: remote || basename5(expand(local)), content: Buffer.from(read(expand(local)), "utf8").toString("base64"), encoding: "base64" };
|
|
5483
|
+
});
|
|
5484
|
+
const secretValues = {};
|
|
5485
|
+
const secrets2 = f.secrets.map((name) => ({ name }));
|
|
5486
|
+
for (const spec of f.secretFiles) {
|
|
5487
|
+
const colon = spec.indexOf(":");
|
|
5488
|
+
if (colon < 1) return { error: `--secret-file needs NAME:path, got "${spec}"` };
|
|
5489
|
+
const name = spec.slice(0, colon);
|
|
5490
|
+
const [local, remote] = split(spec.slice(colon + 1));
|
|
5491
|
+
secretValues[name] = read(expand(local));
|
|
5492
|
+
secrets2.push({ name, file: remote || remotePathFor(local, home) });
|
|
5493
|
+
}
|
|
5494
|
+
if (f.timeout != null && !/^\d+$/.test(f.timeout)) return { error: "--timeout must be whole seconds" };
|
|
5495
|
+
return {
|
|
5496
|
+
action: {
|
|
5497
|
+
type: "exec",
|
|
5498
|
+
command: f.run,
|
|
5499
|
+
...files3.length ? { files: files3 } : {},
|
|
5500
|
+
...secrets2.length ? { secrets: secrets2 } : {},
|
|
5501
|
+
...f.template ? { template: f.template } : {},
|
|
5502
|
+
...f.timeout ? { timeoutSeconds: Number(f.timeout) } : {}
|
|
5503
|
+
},
|
|
5504
|
+
...Object.keys(secretValues).length ? { secretValues } : {}
|
|
5505
|
+
};
|
|
5506
|
+
}
|
|
5507
|
+
function actionFromFlags(f) {
|
|
5508
|
+
if (f.url && f.agent) return { error: "--url and --agent are exclusive" };
|
|
5509
|
+
if (f.url) {
|
|
5510
|
+
return {
|
|
5511
|
+
action: {
|
|
5512
|
+
type: "http",
|
|
5513
|
+
url: f.url,
|
|
5514
|
+
method: (f.method ?? "GET").toUpperCase(),
|
|
5515
|
+
...f.body ? { body: f.body } : {},
|
|
5516
|
+
...f.secret ? { secretName: f.secret, ...f.secretHeader ? { secretHeader: f.secretHeader } : {} } : {}
|
|
5517
|
+
}
|
|
5518
|
+
};
|
|
5519
|
+
}
|
|
5520
|
+
if (f.agent) {
|
|
5521
|
+
if (!f.prompt) return { error: "--agent needs --prompt" };
|
|
5522
|
+
return { action: { type: "agent_turn", fleetAgentId: f.agent, prompt: f.prompt } };
|
|
5523
|
+
}
|
|
5524
|
+
return {};
|
|
5525
|
+
}
|
|
5526
|
+
var NOTIFY_OPTIONS = {
|
|
5527
|
+
notify: { type: "string", value: "push,email,inbox:<name>|none", description: "Tell you after each run: push to the Ghosty app (vault secret GHOSTY_PUSH_TOKEN), email to your account and/or an event in one of your inboxes; none removes it" },
|
|
5528
|
+
"notify-on": { type: "string", value: "failure|always", description: "When to notify (default failure)" }
|
|
5529
|
+
};
|
|
5530
|
+
function notifyFromFlags(f) {
|
|
5531
|
+
if (f.notify == null) return f.notifyOn ? { error: "--notify-on needs --notify" } : {};
|
|
5532
|
+
if (f.notify === "none") return { notify: null };
|
|
5533
|
+
const chans = f.notify.split(",").map((x) => x.trim()).filter(Boolean);
|
|
5534
|
+
const inboxChan = chans.find((x) => x.startsWith("inbox:"));
|
|
5535
|
+
const bad = chans.find((x) => x !== "push" && x !== "email" && !x.startsWith("inbox:"));
|
|
5536
|
+
if (bad || !chans.length || inboxChan === "inbox:") return { error: `--notify takes push, email, inbox:<name> or none (got "${f.notify}")` };
|
|
5537
|
+
if (f.notifyOn && f.notifyOn !== "failure" && f.notifyOn !== "always") return { error: "--notify-on is failure or always" };
|
|
5538
|
+
return {
|
|
5539
|
+
notify: {
|
|
5540
|
+
on: f.notifyOn ?? "failure",
|
|
5541
|
+
...chans.includes("push") ? { push: true } : {},
|
|
5542
|
+
...chans.includes("email") ? { email: true } : {},
|
|
5543
|
+
...inboxChan ? { inbox: inboxChan.slice("inbox:".length) } : {}
|
|
5544
|
+
}
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5547
|
+
async function resolveNotifyInbox(ctx, nf) {
|
|
5548
|
+
const name = nf.notify?.inbox;
|
|
5549
|
+
if (!name || /^[a-f0-9]{24}$/.test(name)) return nf;
|
|
5550
|
+
const b = await api(ctx, "GET", `/inboxes/${encodeURIComponent(name)}`);
|
|
5551
|
+
return { ...nf, notify: { ...nf.notify, inbox: b.id } };
|
|
5552
|
+
}
|
|
5553
|
+
var WHEN_OPTIONS = {
|
|
5554
|
+
at: { type: "string", value: "when", description: 'Run ONCE at this time: ISO ("2026-10-18T23:00", read in --tz) or "23:00" (today)' },
|
|
5555
|
+
in: { type: "string", value: "duration", description: "Run ONCE after this long: 10m, 2h, 1h30m, 1d" },
|
|
5556
|
+
until: { type: "string", value: "when", description: "A repeating cron stops after this date" },
|
|
5557
|
+
times: { type: "string", value: "N", description: "A repeating cron stops after N more runs" }
|
|
5558
|
+
};
|
|
5559
|
+
function scheduleFromFlags(f) {
|
|
5560
|
+
if (f.cron && (f.at || f.in)) return { error: "--cron repeats; --at/--in run once. Pick one." };
|
|
5561
|
+
if (f.at && f.in) return { error: "--at and --in are exclusive" };
|
|
5562
|
+
if (f.times != null && !/^\d+$/.test(f.times)) return { error: "--times must be a whole number" };
|
|
5563
|
+
return {
|
|
5564
|
+
fields: {
|
|
5565
|
+
...f.cron ? { cron: f.cron } : {},
|
|
5566
|
+
...f.at ? { at: f.at } : {},
|
|
5567
|
+
...f.in ? { in: f.in } : {},
|
|
5568
|
+
...f.until ? { until: f.until } : {},
|
|
5569
|
+
...f.times != null ? { times: Number(f.times) } : {},
|
|
5570
|
+
...f.tz ? { timezone: f.tz } : {}
|
|
5571
|
+
}
|
|
5572
|
+
};
|
|
5573
|
+
}
|
|
5574
|
+
var whenOf = (ctx) => ({
|
|
5575
|
+
cron: str(ctx, "cron"),
|
|
5576
|
+
at: str(ctx, "at"),
|
|
5577
|
+
in: str(ctx, "in"),
|
|
5578
|
+
until: str(ctx, "until"),
|
|
5579
|
+
times: str(ctx, "times"),
|
|
5580
|
+
tz: str(ctx, "tz")
|
|
5581
|
+
});
|
|
5582
|
+
var nextLine = (c) => c.nextRunLocal ? t(`next ${c.nextRunLocal} ${c.timezone} (in ${c.in})`, `pr\xF3xima ${c.nextRunLocal} ${c.timezone} (en ${c.in})`) : t("no next run", "sin pr\xF3xima corrida");
|
|
5583
|
+
var flagsOf = (ctx) => ({
|
|
5584
|
+
url: str(ctx, "url"),
|
|
5585
|
+
method: str(ctx, "method"),
|
|
5586
|
+
body: str(ctx, "body"),
|
|
5587
|
+
secret: list(ctx, "secret")[0],
|
|
5588
|
+
secretHeader: str(ctx, "secret-header"),
|
|
5589
|
+
agent: str(ctx, "agent"),
|
|
5590
|
+
prompt: str(ctx, "prompt")
|
|
5591
|
+
});
|
|
5592
|
+
function actionOf(ctx) {
|
|
5593
|
+
const run = str(ctx, "run");
|
|
5594
|
+
if (run && (str(ctx, "url") || str(ctx, "agent"))) return { error: "--run, --url and --agent are exclusive" };
|
|
5595
|
+
if (run) {
|
|
5596
|
+
return execFromFlags(
|
|
5597
|
+
{ run, files: list(ctx, "file"), secrets: list(ctx, "secret"), secretFiles: list(ctx, "secret-file"), template: str(ctx, "template"), timeout: str(ctx, "timeout") },
|
|
5598
|
+
(p) => {
|
|
5599
|
+
try {
|
|
5600
|
+
return readFileSync18(p, "utf8");
|
|
5601
|
+
} catch {
|
|
5602
|
+
throw new CliError(t(`Can't read ${p}.`, `No se pudo leer ${p}.`), EXIT.USAGE);
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
);
|
|
5606
|
+
}
|
|
5607
|
+
if (list(ctx, "secret").length > 1) return { error: "--url takes one --secret" };
|
|
5608
|
+
return actionFromFlags(flagsOf(ctx));
|
|
5609
|
+
}
|
|
5428
5610
|
var STATUS = {
|
|
5429
5611
|
"al-dia": ["up to date", "al d\xEDa"],
|
|
5430
5612
|
tarde: ["late", "tarde"],
|
|
@@ -5437,6 +5619,14 @@ function cronStatus(c) {
|
|
|
5437
5619
|
const s = STATUS[c.status];
|
|
5438
5620
|
return s ? t(s[0], s[1]) : c.status;
|
|
5439
5621
|
}
|
|
5622
|
+
var runExitCode = (r) => r.exitCode != null ? Math.min(Math.max(r.exitCode, 0), 255) : r.ok ? 0 : 1;
|
|
5623
|
+
function parseDurationMs(raw) {
|
|
5624
|
+
const s = raw.trim().toLowerCase();
|
|
5625
|
+
if (!/^(\d+(d|h|m|s))+$/.test(s)) return null;
|
|
5626
|
+
let ms = 0;
|
|
5627
|
+
for (const [, n, u] of s.matchAll(/(\d+)(d|h|m|s)/g)) ms += Number(n) * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[u];
|
|
5628
|
+
return ms;
|
|
5629
|
+
}
|
|
5440
5630
|
function duration(ms) {
|
|
5441
5631
|
if (ms == null) return "\u2014";
|
|
5442
5632
|
return ms < 1e3 ? `${ms} ms` : `${(ms / 1e3).toFixed(1)} s`;
|
|
@@ -5445,15 +5635,39 @@ var crons = {
|
|
|
5445
5635
|
name: "crons",
|
|
5446
5636
|
aliases: ["cron"],
|
|
5447
5637
|
group: "Account",
|
|
5448
|
-
summary: "
|
|
5449
|
-
synopsis: "crons <ls|
|
|
5638
|
+
summary: "Your scheduled jobs (call a URL, message an agent or run a command; repeating or once) and, for admins, the platform's",
|
|
5639
|
+
synopsis: "crons <ls|create|update|rm|run|runs|wait|pause|resume>",
|
|
5450
5640
|
subs: {
|
|
5451
5641
|
ls: {
|
|
5452
5642
|
aliases: ["list"],
|
|
5453
|
-
summary: "List
|
|
5454
|
-
usage: "easybits crons ls",
|
|
5455
|
-
|
|
5643
|
+
summary: "List your crons (or, with --platform, the platform's scheduled jobs)",
|
|
5644
|
+
usage: "easybits crons ls [--platform]",
|
|
5645
|
+
options: { platform: { type: "boolean", description: "Platform jobs (backups, purges\u2026; platform admins only)" } },
|
|
5646
|
+
examples: ["easybits crons ls", "easybits crons ls --json", "easybits crons ls --platform"],
|
|
5456
5647
|
async run(ctx) {
|
|
5648
|
+
if (!bool(ctx, "platform")) {
|
|
5649
|
+
const { items: items2 } = await api(ctx, "GET", "/crons");
|
|
5650
|
+
emit(
|
|
5651
|
+
ctx,
|
|
5652
|
+
{ items: items2 },
|
|
5653
|
+
() => table(
|
|
5654
|
+
items2.map((c) => ({
|
|
5655
|
+
id: c.id,
|
|
5656
|
+
name: c.name,
|
|
5657
|
+
schedule: c.enabled ? c.schedule : `${c.schedule} (${c.done ? t("done", "terminado") : t("paused", "en pausa")})`,
|
|
5658
|
+
does: describeCronAction(c.action),
|
|
5659
|
+
last: c.lastRunAt ? `${fmtDate(c.lastRunAt)} ${c.lastOk === false ? t("failed", "fall\xF3") : "ok"}` : "\u2014",
|
|
5660
|
+
next: c.enabled && c.nextRunLocal ? `${c.nextRunLocal} (${c.in})` : "\u2014"
|
|
5661
|
+
})),
|
|
5662
|
+
[["id", "ID"], ["name", "NAME"], ["schedule", "SCHEDULE"], ["does", "DOES"], ["last", "LAST RUN"], ["next", "NEXT RUN"]],
|
|
5663
|
+
t(
|
|
5664
|
+
'No crons. Create one: easybits crons create "Ping" --cron "*/15 * * * *" --url https://example.com/ping',
|
|
5665
|
+
'Sin crons. Crea uno: easybits crons create "Ping" --cron "*/15 * * * *" --url https://ejemplo.com/ping'
|
|
5666
|
+
)
|
|
5667
|
+
)
|
|
5668
|
+
);
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5457
5671
|
const { items } = await api(ctx, "GET", "/admin/crons");
|
|
5458
5672
|
emit(
|
|
5459
5673
|
ctx,
|
|
@@ -5472,18 +5686,22 @@ var crons = {
|
|
|
5472
5686
|
}
|
|
5473
5687
|
},
|
|
5474
5688
|
runs: {
|
|
5475
|
-
summary: "Show
|
|
5476
|
-
usage: "easybits crons runs <job>",
|
|
5477
|
-
|
|
5689
|
+
summary: "Show the last runs of one of your crons (id or name) or of a platform job, with the full error",
|
|
5690
|
+
usage: "easybits crons runs <id|name|job> [--logs]",
|
|
5691
|
+
options: { logs: { type: "boolean", description: "Also print stdout/stderr of --run crons (last 64 KB per run)" } },
|
|
5692
|
+
examples: ["easybits crons runs 66f0c2a1b2c3d4e5f6a7b8c9", 'easybits crons runs "Review 1.0.10" --logs', "easybits crons runs backup-machines --json"],
|
|
5478
5693
|
async run(ctx) {
|
|
5479
|
-
const key = need(ctx, 0, "job", this.usage);
|
|
5480
|
-
const
|
|
5694
|
+
const key = need(ctx, 0, "id|name|job", this.usage);
|
|
5695
|
+
const mine = await userCronOrNull(ctx, key);
|
|
5696
|
+
const withLogs = bool(ctx, "logs");
|
|
5697
|
+
const path = mine ? `/crons/${mine.id}/runs?limit=50${withLogs ? "&logs=1" : ""}` : `/admin/crons/${encodeURIComponent(key)}/runs`;
|
|
5698
|
+
const { items } = await api(ctx, "GET", path);
|
|
5481
5699
|
emit(ctx, { items }, () => {
|
|
5482
5700
|
table(
|
|
5483
5701
|
items.map((r) => ({
|
|
5484
5702
|
at: fmtDate(r.startedAt),
|
|
5485
5703
|
ok: r.ok == null ? t("running", "corriendo") : r.ok ? "ok" : t("failed", "fall\xF3"),
|
|
5486
|
-
status: r.status ?? "\u2014",
|
|
5704
|
+
status: r.exitCode != null ? `exit ${r.exitCode}` : r.status ?? "\u2014",
|
|
5487
5705
|
took: duration(r.durationMs),
|
|
5488
5706
|
trigger: r.trigger,
|
|
5489
5707
|
summary: (r.error ?? r.summary ?? "").slice(0, 80)
|
|
@@ -5491,20 +5709,68 @@ var crons = {
|
|
|
5491
5709
|
[["at", "STARTED"], ["ok", "RESULT"], ["status", "HTTP"], ["took", "TOOK"], ["trigger", "TRIGGER"], ["summary", "SUMMARY"]],
|
|
5492
5710
|
t("No runs yet.", "Todav\xEDa no hay corridas.")
|
|
5493
5711
|
);
|
|
5712
|
+
if (withLogs) {
|
|
5713
|
+
const last = items.find((r) => r.logs);
|
|
5714
|
+
if (last) console.log(`
|
|
5715
|
+
${t("Output", "Salida")} (${fmtDate(last.startedAt)}):
|
|
5716
|
+
${last.logs}`);
|
|
5717
|
+
}
|
|
5494
5718
|
const failed = items.find((r) => r.ok === false && r.error);
|
|
5495
|
-
if (failed) console.log(`
|
|
5719
|
+
if (failed && !withLogs) console.log(`
|
|
5496
5720
|
${t("Last error", "\xDAltimo error")} (${fmtDate(failed.startedAt)}):
|
|
5497
5721
|
${failed.error}`);
|
|
5498
5722
|
});
|
|
5499
5723
|
}
|
|
5500
5724
|
},
|
|
5725
|
+
wait: {
|
|
5726
|
+
summary: "Wait for the next run of one of your crons to finish and exit with its result (exec: its exit code)",
|
|
5727
|
+
usage: "easybits crons wait <id|name> [--timeout 30m] [--json]",
|
|
5728
|
+
options: { timeout: { type: "string", value: "duration", description: "Give up after this long (default 30m; 10m, 2h\u2026)" } },
|
|
5729
|
+
examples: ['easybits crons wait "Review 1.0.10"', "easybits crons wait 66f0c2a1b2c3d4e5f6a7b8c9 --timeout 2h --json"],
|
|
5730
|
+
async run(ctx) {
|
|
5731
|
+
const ref2 = need(ctx, 0, "id|name", this.usage);
|
|
5732
|
+
const mine = await resolveUserCron(ctx, ref2);
|
|
5733
|
+
if (!mine) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
|
|
5734
|
+
const limitMs = parseDurationMs(str(ctx, "timeout") ?? "30m");
|
|
5735
|
+
if (limitMs == null) throw usageError(t("--timeout like 10m, 2h or 1d.", "--timeout como 10m, 2h o 1d."), this.usage);
|
|
5736
|
+
const deadline = Date.now() + limitMs;
|
|
5737
|
+
let after;
|
|
5738
|
+
for (; ; ) {
|
|
5739
|
+
const left = Math.ceil((deadline - Date.now()) / 1e3);
|
|
5740
|
+
const q = new URLSearchParams({ wait: String(Math.max(1, Math.min(50, left))) });
|
|
5741
|
+
if (after) q.set("after", after);
|
|
5742
|
+
const r = await api(ctx, "GET", `/crons/${mine.id}/runs/next?${q}`);
|
|
5743
|
+
after = r.after;
|
|
5744
|
+
if (r.run) {
|
|
5745
|
+
const code = runExitCode(r.run);
|
|
5746
|
+
emit(ctx, r, () => {
|
|
5747
|
+
console.log(`${r.run.ok ? "\u2713" : "\u2717"} \xAB${mine.name}\xBB ${r.run.exitCode != null ? `exit ${r.run.exitCode}` : r.run.ok ? "ok" : t("failed", "fall\xF3")} (${duration(r.run.durationMs)})`);
|
|
5748
|
+
if (r.run.logs) console.log(r.run.logs);
|
|
5749
|
+
else if (r.run.error) console.log(r.run.error);
|
|
5750
|
+
else if (r.run.summary) console.log(r.run.summary);
|
|
5751
|
+
});
|
|
5752
|
+
process.exitCode = code;
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
if (Date.now() >= deadline) {
|
|
5756
|
+
throw new CliError(
|
|
5757
|
+
t(`Gave up waiting for \xAB${mine.name}\xBB${r.nextRunLocal ? ` (next run ${r.nextRunLocal})` : ""}.`, `Me cans\xE9 de esperar \xAB${mine.name}\xBB${r.nextRunLocal ? ` (pr\xF3xima corrida ${r.nextRunLocal})` : ""}.`),
|
|
5758
|
+
EXIT.API,
|
|
5759
|
+
`easybits crons wait ${mine.id} --timeout 2h`,
|
|
5760
|
+
"timeout"
|
|
5761
|
+
);
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
},
|
|
5501
5766
|
run: {
|
|
5502
|
-
summary: "Run a job now in the background (
|
|
5503
|
-
usage: "easybits crons run <job>",
|
|
5504
|
-
examples: ["easybits crons run
|
|
5767
|
+
summary: "Run one of your crons or a platform job now, in the background (trigger manual)",
|
|
5768
|
+
usage: "easybits crons run <id|name|job>",
|
|
5769
|
+
examples: ["easybits crons run 66f0c2a1b2c3d4e5f6a7b8c9", "easybits crons run purge-notifications --json"],
|
|
5505
5770
|
async run(ctx) {
|
|
5506
|
-
const key = need(ctx, 0, "job", this.usage);
|
|
5507
|
-
const
|
|
5771
|
+
const key = need(ctx, 0, "id|name|job", this.usage);
|
|
5772
|
+
const mine = await userCronOrNull(ctx, key);
|
|
5773
|
+
const r = await api(ctx, "POST", mine ? `/crons/${mine.id}/run` : `/admin/crons/${encodeURIComponent(key)}/run`);
|
|
5508
5774
|
emit(
|
|
5509
5775
|
ctx,
|
|
5510
5776
|
r,
|
|
@@ -5513,33 +5779,312 @@ ${failed.error}`);
|
|
|
5513
5779
|
}
|
|
5514
5780
|
},
|
|
5515
5781
|
pause: {
|
|
5516
|
-
summary: "Pause
|
|
5517
|
-
usage: "easybits crons pause <job>",
|
|
5518
|
-
examples: ["easybits crons pause check-hosting"],
|
|
5782
|
+
summary: "Pause one of your crons or a platform job until you resume it",
|
|
5783
|
+
usage: "easybits crons pause <id|name|job>",
|
|
5784
|
+
examples: ["easybits crons pause 66f0c2a1b2c3d4e5f6a7b8c9", "easybits crons pause check-hosting"],
|
|
5519
5785
|
async run(ctx) {
|
|
5520
|
-
const key = need(ctx, 0, "job", this.usage);
|
|
5521
|
-
const
|
|
5786
|
+
const key = need(ctx, 0, "id|name|job", this.usage);
|
|
5787
|
+
const mine = await userCronOrNull(ctx, key);
|
|
5788
|
+
const r = mine ? await api(ctx, "PATCH", `/crons/${mine.id}`, { enabled: false }) : await api(ctx, "POST", `/admin/crons/${encodeURIComponent(key)}/pause`, { paused: true });
|
|
5522
5789
|
emit(ctx, r, () => console.log(t(`Paused ${key}.`, `${key} en pausa.`)));
|
|
5523
5790
|
}
|
|
5524
5791
|
},
|
|
5525
5792
|
resume: {
|
|
5526
|
-
summary: "Resume a
|
|
5527
|
-
usage: "easybits crons resume <job>",
|
|
5528
|
-
examples: ["easybits crons resume check-hosting"],
|
|
5793
|
+
summary: "Resume one of your crons or a platform job",
|
|
5794
|
+
usage: "easybits crons resume <id|name|job>",
|
|
5795
|
+
examples: ["easybits crons resume 66f0c2a1b2c3d4e5f6a7b8c9", "easybits crons resume check-hosting"],
|
|
5529
5796
|
async run(ctx) {
|
|
5530
|
-
const key = need(ctx, 0, "job", this.usage);
|
|
5531
|
-
const
|
|
5797
|
+
const key = need(ctx, 0, "id|name|job", this.usage);
|
|
5798
|
+
const mine = await userCronOrNull(ctx, key);
|
|
5799
|
+
const r = mine ? await api(ctx, "PATCH", `/crons/${mine.id}`, { enabled: true }) : await api(ctx, "POST", `/admin/crons/${encodeURIComponent(key)}/pause`, { paused: false });
|
|
5532
5800
|
emit(ctx, r, () => console.log(t(`Resumed ${key}.`, `${key} reanudada.`)));
|
|
5533
5801
|
}
|
|
5802
|
+
},
|
|
5803
|
+
create: {
|
|
5804
|
+
aliases: ["add"],
|
|
5805
|
+
summary: "Create a cron: call a public https URL, send a turn to one of your agents or run a command in a throwaway box, repeating (at most every 5 min) or once",
|
|
5806
|
+
usage: 'easybits crons create <name> (--cron "<5 fields>" [--until <when>] [--times N] | --at <when> | --in <duration>) [--tz <IANA>] (--url <https-url> [--method] [--body] [--secret NAME] | --agent <id> --prompt <text> | --run <cmd> [--file f]\u2026 [--secret NAME]\u2026 [--secret-file NAME:path]\u2026 [--template] [--timeout]) [--dry-run]',
|
|
5807
|
+
options: {
|
|
5808
|
+
cron: { type: "string", value: "expr", description: 'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30' },
|
|
5809
|
+
...WHEN_OPTIONS,
|
|
5810
|
+
tz: { type: "string", value: "zone", description: "IANA time zone (default America/Mexico_City)" },
|
|
5811
|
+
...ACTION_OPTIONS,
|
|
5812
|
+
...NOTIFY_OPTIONS,
|
|
5813
|
+
"dry-run": { type: "boolean", description: "Validate and show the next 3 runs without creating anything" }
|
|
5814
|
+
},
|
|
5815
|
+
examples: [
|
|
5816
|
+
`easybits crons create "Study reminder" --cron "30 8 * * *" --url https://www.ghosty.studio/api/v2/me/push --method POST --body '{"title":"Study time"}' --secret GHOSTY_PUSH_TOKEN`,
|
|
5817
|
+
'easybits crons create "Ping once" --at 23:00 --url https://example.com/ping --dry-run',
|
|
5818
|
+
'easybits crons create "Review 1.0.10" --at 23:00 --run "pip install -q cryptography && python3 asc.py enviar-tienda" --file scripts/asc.py --secret-file ASC_P8:~/.appstoreconnect/private_keys/AuthKey_X.p8 --notify push --notify-on always',
|
|
5819
|
+
'easybits crons create "Weekly report" --cron "0 9 * * 1" --times 4 --agent 6a3c046d2cf7d7f4163f739d --prompt "Send me the weekly sales summary"'
|
|
5820
|
+
],
|
|
5821
|
+
async run(ctx) {
|
|
5822
|
+
const name = need(ctx, 0, "name", this.usage);
|
|
5823
|
+
const w = scheduleFromFlags(whenOf(ctx));
|
|
5824
|
+
if ("error" in w) throw usageError(w.error, this.usage);
|
|
5825
|
+
if (!w.fields.cron && !w.fields.at && !w.fields.in) throw usageError(t("Missing --cron, --at or --in.", "Falta --cron, --at o --in."), this.usage);
|
|
5826
|
+
const a = actionOf(ctx);
|
|
5827
|
+
if ("error" in a) throw usageError(a.error, this.usage);
|
|
5828
|
+
if (!a.action) throw usageError(t("Missing --url, --agent or --run.", "Falta --url, --agent o --run."), this.usage);
|
|
5829
|
+
const nf0 = notifyFromFlags({ notify: str(ctx, "notify"), notifyOn: str(ctx, "notify-on") });
|
|
5830
|
+
if ("error" in nf0) throw usageError(nf0.error, this.usage);
|
|
5831
|
+
const nf = await resolveNotifyInbox(ctx, nf0);
|
|
5832
|
+
const body = { name, ...w.fields, action: a.action, ...a.secretValues ? { secretValues: a.secretValues } : {}, ...nf };
|
|
5833
|
+
if (bool(ctx, "dry-run")) {
|
|
5834
|
+
const p = await api(ctx, "POST", "/crons", { ...body, dryRun: true });
|
|
5835
|
+
emit(ctx, p, () => {
|
|
5836
|
+
console.log(t(`OK, nothing created. \xAB${name}\xBB \u2014 ${p.schedule}`, `V\xE1lido, no se cre\xF3 nada. \xAB${name}\xBB \u2014 ${p.schedule}`));
|
|
5837
|
+
for (const r of p.nextRuns) console.log(` ${r.local} ${p.timezone} (${r.at})`);
|
|
5838
|
+
if (p.in) console.log(t(`First run in ${p.in}.`, `La primera corre en ${p.in}.`));
|
|
5839
|
+
});
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5842
|
+
const c = await api(ctx, "POST", "/crons", body);
|
|
5843
|
+
emit(ctx, c, () => console.log(t(`Created ${c.id} \xAB${c.name}\xBB \u2014 ${c.schedule}; `, `Creado ${c.id} \xAB${c.name}\xBB \u2014 ${c.schedule}; `) + nextLine(c)));
|
|
5844
|
+
}
|
|
5845
|
+
},
|
|
5846
|
+
update: {
|
|
5847
|
+
aliases: ["edit", "set"],
|
|
5848
|
+
summary: "Change one of your crons (only what you pass)",
|
|
5849
|
+
usage: 'easybits crons update <id|name> [--name <n>] [--cron "<expr>" | --at <when> | --in <duration>] [--until <when>] [--times N] [--tz <IANA>] [--url \u2026 | --agent \u2026 --prompt \u2026]',
|
|
5850
|
+
options: {
|
|
5851
|
+
name: { type: "string", value: "name", description: "New name" },
|
|
5852
|
+
cron: { type: "string", value: "expr", description: "New schedule" },
|
|
5853
|
+
...WHEN_OPTIONS,
|
|
5854
|
+
...NOTIFY_OPTIONS,
|
|
5855
|
+
tz: { type: "string", value: "zone", description: "New IANA time zone" },
|
|
5856
|
+
...ACTION_OPTIONS
|
|
5857
|
+
},
|
|
5858
|
+
examples: ['easybits crons update 66f0c2a1b2c3d4e5f6a7b8c9 --cron "0 7 * * 1-5"'],
|
|
5859
|
+
async run(ctx) {
|
|
5860
|
+
const ref2 = need(ctx, 0, "id|name", this.usage);
|
|
5861
|
+
const mine = await resolveUserCron(ctx, ref2);
|
|
5862
|
+
if (!mine) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
|
|
5863
|
+
const a = actionOf(ctx);
|
|
5864
|
+
if ("error" in a) throw usageError(a.error, this.usage);
|
|
5865
|
+
const w = scheduleFromFlags(whenOf(ctx));
|
|
5866
|
+
if ("error" in w) throw usageError(w.error, this.usage);
|
|
5867
|
+
const nf0 = notifyFromFlags({ notify: str(ctx, "notify"), notifyOn: str(ctx, "notify-on") });
|
|
5868
|
+
if ("error" in nf0) throw usageError(nf0.error, this.usage);
|
|
5869
|
+
const nf = await resolveNotifyInbox(ctx, nf0);
|
|
5870
|
+
const patch = {
|
|
5871
|
+
...nf,
|
|
5872
|
+
...str(ctx, "name") ? { name: str(ctx, "name") } : {},
|
|
5873
|
+
...w.fields,
|
|
5874
|
+
...a.action ? { action: a.action } : {},
|
|
5875
|
+
...a.secretValues ? { secretValues: a.secretValues } : {}
|
|
5876
|
+
};
|
|
5877
|
+
if (!Object.keys(patch).length) throw usageError(t("Nothing to change.", "No hay nada que cambiar."), this.usage);
|
|
5878
|
+
const c = await api(ctx, "PATCH", `/crons/${mine.id}`, patch);
|
|
5879
|
+
emit(ctx, c, () => console.log(t(`Updated \xAB${c.name}\xBB \u2014 ${c.schedule}; `, `Actualizado \xAB${c.name}\xBB \u2014 ${c.schedule}; `) + nextLine(c)));
|
|
5880
|
+
}
|
|
5881
|
+
},
|
|
5882
|
+
rm: {
|
|
5883
|
+
aliases: ["remove", "delete"],
|
|
5884
|
+
summary: "Delete one of your crons",
|
|
5885
|
+
usage: "easybits crons rm <id|name> [--yes]",
|
|
5886
|
+
options: { ...YES_OPTION },
|
|
5887
|
+
examples: ["easybits crons rm 66f0c2a1b2c3d4e5f6a7b8c9 --yes"],
|
|
5888
|
+
async run(ctx) {
|
|
5889
|
+
const ref2 = need(ctx, 0, "id|name", this.usage);
|
|
5890
|
+
const mine = await resolveUserCron(ctx, ref2);
|
|
5891
|
+
if (!mine) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
|
|
5892
|
+
requireYesIfHeadless(ctx);
|
|
5893
|
+
await confirm(ctx, t(`Delete cron \xAB${mine.name}\xBB? It stops running right away.`, `\xBFBorrar el cron \xAB${mine.name}\xBB? Deja de correr de inmediato.`));
|
|
5894
|
+
const r = await api(ctx, "DELETE", `/crons/${mine.id}`);
|
|
5895
|
+
emit(ctx, r, () => console.log(t(`Deleted \xAB${mine.name}\xBB`, `Borrado \xAB${mine.name}\xBB`)));
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
},
|
|
5899
|
+
defaultSub: "ls"
|
|
5900
|
+
};
|
|
5901
|
+
|
|
5902
|
+
// src/commands/inbox.ts
|
|
5903
|
+
import { resolveBaseUrl as resolveBaseUrl8 } from "@easybits.cloud/sdk";
|
|
5904
|
+
var ref = (r) => encodeURIComponent(r);
|
|
5905
|
+
function eventLine(e, width = 100) {
|
|
5906
|
+
const body = (e.json !== void 0 ? JSON.stringify(e.json) : e.body).replace(/\s+/g, " ");
|
|
5907
|
+
const head = `#${e.seq} ${fmtDate(e.receivedAt)} ${e.source}`;
|
|
5908
|
+
const room = Math.max(width - head.length - 1, 10);
|
|
5909
|
+
return `${head} ${body.length > room ? body.slice(0, room - 1) + "\u2026" : body}`;
|
|
5910
|
+
}
|
|
5911
|
+
function parseSse(buffer) {
|
|
5912
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
5913
|
+
const rest = blocks.pop() ?? "";
|
|
5914
|
+
const events = [];
|
|
5915
|
+
for (const block of blocks) {
|
|
5916
|
+
let event = "message";
|
|
5917
|
+
let id;
|
|
5918
|
+
const data = [];
|
|
5919
|
+
for (const line2 of block.split(/\r?\n/)) {
|
|
5920
|
+
if (line2.startsWith(":")) continue;
|
|
5921
|
+
const i = line2.indexOf(":");
|
|
5922
|
+
const k = i < 0 ? line2 : line2.slice(0, i);
|
|
5923
|
+
const v = i < 0 ? "" : line2.slice(i + 1).replace(/^ /, "");
|
|
5924
|
+
if (k === "event") event = v;
|
|
5925
|
+
else if (k === "data") data.push(v);
|
|
5926
|
+
else if (k === "id") id = v;
|
|
5927
|
+
}
|
|
5928
|
+
if (data.length) events.push({ event, data: data.join("\n"), id });
|
|
5929
|
+
}
|
|
5930
|
+
return { events, rest };
|
|
5931
|
+
}
|
|
5932
|
+
async function listen(ctx, name, since) {
|
|
5933
|
+
const base = (await resolveBaseUrl8()).replace(/\/$/, "");
|
|
5934
|
+
let last = since;
|
|
5935
|
+
let backoff = 1e3;
|
|
5936
|
+
for (; ; ) {
|
|
5937
|
+
const token = await resolveCredential(ctx);
|
|
5938
|
+
const q = last != null ? `?since=${last}` : "";
|
|
5939
|
+
let res;
|
|
5940
|
+
try {
|
|
5941
|
+
res = await fetch(`${base}/api/v2/inboxes/${ref(name)}/events/stream${q}`, { headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" } });
|
|
5942
|
+
} catch {
|
|
5943
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
5944
|
+
backoff = Math.min(backoff * 2, 3e4);
|
|
5945
|
+
continue;
|
|
5946
|
+
}
|
|
5947
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
5948
|
+
throw new CliError(t(`Can't listen to "${name}" (HTTP ${res.status}).`, `No se puede escuchar \xAB${name}\xBB (HTTP ${res.status}).`), res.status === 404 ? EXIT.API : EXIT.AUTH, "easybits inbox ls");
|
|
5949
|
+
}
|
|
5950
|
+
if (!res.ok || !res.body) {
|
|
5951
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
5952
|
+
backoff = Math.min(backoff * 2, 3e4);
|
|
5953
|
+
continue;
|
|
5954
|
+
}
|
|
5955
|
+
backoff = 1e3;
|
|
5956
|
+
const reader = res.body.getReader();
|
|
5957
|
+
const dec = new TextDecoder();
|
|
5958
|
+
let buf = "";
|
|
5959
|
+
try {
|
|
5960
|
+
for (; ; ) {
|
|
5961
|
+
const { value, done } = await reader.read();
|
|
5962
|
+
if (done) break;
|
|
5963
|
+
buf += dec.decode(value, { stream: true });
|
|
5964
|
+
const { events, rest } = parseSse(buf);
|
|
5965
|
+
buf = rest;
|
|
5966
|
+
for (const ev of events) {
|
|
5967
|
+
if (ev.event === "ready") {
|
|
5968
|
+
const r = JSON.parse(ev.data);
|
|
5969
|
+
if (last == null) last = r.since;
|
|
5970
|
+
process.stderr.write(t(`Listening to \xAB${r.name}\xBB from #${r.since}. One JSON line per event.
|
|
5971
|
+
`, `Escuchando \xAB${r.name}\xBB desde #${r.since}. Una l\xEDnea JSON por evento.
|
|
5972
|
+
`));
|
|
5973
|
+
} else if (ev.event === "inbox") {
|
|
5974
|
+
const e = JSON.parse(ev.data);
|
|
5975
|
+
last = e.seq;
|
|
5976
|
+
process.stdout.write(JSON.stringify(e) + "\n");
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
} catch {
|
|
5981
|
+
}
|
|
5982
|
+
process.stderr.write(t("Connection dropped; reconnecting\u2026\n", "Se cort\xF3 la conexi\xF3n; reconectando\u2026\n"));
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
var inbox = {
|
|
5986
|
+
name: "inbox",
|
|
5987
|
+
aliases: ["inboxes"],
|
|
5988
|
+
group: "Account",
|
|
5989
|
+
summary: "Event inboxes: your own webhook URL whose events reach your terminal live or wait for you to read them",
|
|
5990
|
+
synopsis: "inbox <ls|create|listen|read|rm>",
|
|
5991
|
+
subs: {
|
|
5992
|
+
ls: {
|
|
5993
|
+
aliases: ["list"],
|
|
5994
|
+
summary: "List your inboxes with their URL and unread events",
|
|
5995
|
+
usage: "easybits inbox ls",
|
|
5996
|
+
examples: ["easybits inbox ls", "easybits inbox ls --json"],
|
|
5997
|
+
async run(ctx) {
|
|
5998
|
+
const { items } = await api(ctx, "GET", "/inboxes");
|
|
5999
|
+
emit(
|
|
6000
|
+
ctx,
|
|
6001
|
+
{ items },
|
|
6002
|
+
() => table(
|
|
6003
|
+
items.map((b) => ({ name: b.name, unread: b.unread, last: b.lastEventAt ? fmtDate(b.lastEventAt) : "\u2014", url: b.url })),
|
|
6004
|
+
[["name", "NAME"], ["unread", "UNREAD"], ["last", "LAST EVENT"], ["url", "URL"]],
|
|
6005
|
+
t("No inboxes. Create one: easybits inbox create builds", "Sin buzones. Crea uno: easybits inbox create builds")
|
|
6006
|
+
)
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
},
|
|
6010
|
+
create: {
|
|
6011
|
+
aliases: ["add"],
|
|
6012
|
+
summary: "Create an inbox and print its webhook URL (anyone with it can write: keep it private)",
|
|
6013
|
+
usage: "easybits inbox create <name> [--retention <days>]",
|
|
6014
|
+
options: { retention: { type: "string", value: "days", description: "Days events are kept (1-30, default 7)" } },
|
|
6015
|
+
examples: ["easybits inbox create builds", "easybits inbox create stripe --retention 30 --json"],
|
|
6016
|
+
async run(ctx) {
|
|
6017
|
+
const name = need(ctx, 0, "name", this.usage);
|
|
6018
|
+
const days = str(ctx, "retention");
|
|
6019
|
+
if (days != null && !/^\d+$/.test(days)) throw usageError(t("--retention is whole days.", "--retention son d\xEDas enteros."), this.usage);
|
|
6020
|
+
const b = await api(ctx, "POST", "/inboxes", { name, ...days ? { retentionDays: Number(days) } : {} });
|
|
6021
|
+
emit(ctx, b, () => {
|
|
6022
|
+
console.log(t(`Inbox \xAB${b.name}\xBB ready. POST anything to:`, `Buz\xF3n \xAB${b.name}\xBB listo. Manda cualquier cosa con POST a:`));
|
|
6023
|
+
console.log(` ${b.url}`);
|
|
6024
|
+
console.log(t(`Listen live: easybits inbox listen ${b.name} \xB7 Read later: easybits inbox read ${b.name}`, `En vivo: easybits inbox listen ${b.name} \xB7 Despu\xE9s: easybits inbox read ${b.name}`));
|
|
6025
|
+
});
|
|
6026
|
+
}
|
|
6027
|
+
},
|
|
6028
|
+
listen: {
|
|
6029
|
+
aliases: ["tail", "follow"],
|
|
6030
|
+
summary: "Stream an inbox: one JSON line per event as it arrives (reconnects on its own)",
|
|
6031
|
+
usage: "easybits inbox listen <id|name> [--since <seq>]",
|
|
6032
|
+
options: { since: { type: "string", value: "seq", description: "Start after this event (0 = everything kept; default: unread)" } },
|
|
6033
|
+
examples: ["easybits inbox listen builds", "easybits inbox listen builds --since 0 | jq .json"],
|
|
6034
|
+
async run(ctx) {
|
|
6035
|
+
const name = need(ctx, 0, "id|name", this.usage);
|
|
6036
|
+
const since = str(ctx, "since");
|
|
6037
|
+
if (since != null && !/^\d+$/.test(since)) throw usageError(t("--since is an event number.", "--since es un n\xFAmero de evento."), this.usage);
|
|
6038
|
+
await listen(ctx, name, since != null ? Number(since) : void 0);
|
|
6039
|
+
}
|
|
6040
|
+
},
|
|
6041
|
+
read: {
|
|
6042
|
+
summary: "Show the events you haven't read yet and mark them read (the next read brings only new ones)",
|
|
6043
|
+
usage: "easybits inbox read <id|name> [--all] [--no-ack] [--limit N]",
|
|
6044
|
+
options: {
|
|
6045
|
+
all: { type: "boolean", description: "Everything still kept, read or not" },
|
|
6046
|
+
"no-ack": { type: "boolean", description: "Just look; don't mark them read" },
|
|
6047
|
+
limit: { type: "string", value: "N", description: "At most N events (default 50, max 200)" }
|
|
6048
|
+
},
|
|
6049
|
+
examples: ["easybits inbox read builds", "easybits inbox read builds --all --no-ack --json"],
|
|
6050
|
+
async run(ctx) {
|
|
6051
|
+
const name = need(ctx, 0, "id|name", this.usage);
|
|
6052
|
+
const q = new URLSearchParams();
|
|
6053
|
+
if (bool(ctx, "all")) q.set("since", "0");
|
|
6054
|
+
if (bool(ctx, "no-ack")) q.set("ack", "0");
|
|
6055
|
+
if (str(ctx, "limit")) q.set("limit", str(ctx, "limit"));
|
|
6056
|
+
const r = await api(ctx, "GET", `/inboxes/${ref(name)}/events${q.size ? `?${q}` : ""}`);
|
|
6057
|
+
emit(ctx, r, () => {
|
|
6058
|
+
if (!r.items.length) return console.log(t("Nothing new.", "Nada nuevo."));
|
|
6059
|
+
for (const e of r.items) console.log(eventLine(e, process.stdout.columns || 100));
|
|
6060
|
+
if (r.unread) console.log(t(`
|
|
6061
|
+
${r.unread} more unread: easybits inbox read ${name}`, `
|
|
6062
|
+
Quedan ${r.unread} sin leer: easybits inbox read ${name}`));
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
6065
|
+
},
|
|
6066
|
+
rm: {
|
|
6067
|
+
aliases: ["remove", "delete"],
|
|
6068
|
+
summary: "Delete an inbox and its events; its URL stops accepting writes",
|
|
6069
|
+
usage: "easybits inbox rm <id|name> [--yes]",
|
|
6070
|
+
options: { ...YES_OPTION },
|
|
6071
|
+
examples: ["easybits inbox rm builds --yes"],
|
|
6072
|
+
async run(ctx) {
|
|
6073
|
+
const name = need(ctx, 0, "id|name", this.usage);
|
|
6074
|
+
requireYesIfHeadless(ctx);
|
|
6075
|
+
await confirm(ctx, t(`Delete inbox \xAB${name}\xBB and its events?`, `\xBFBorrar el buz\xF3n \xAB${name}\xBB y sus eventos?`));
|
|
6076
|
+
const r = await api(ctx, "DELETE", `/inboxes/${ref(name)}`);
|
|
6077
|
+
emit(ctx, r, () => console.log(t(`Deleted \xAB${name}\xBB`, `Borrado \xAB${name}\xBB`)));
|
|
6078
|
+
}
|
|
5534
6079
|
}
|
|
5535
6080
|
},
|
|
5536
6081
|
defaultSub: "ls"
|
|
5537
6082
|
};
|
|
5538
6083
|
|
|
5539
6084
|
// src/commands/documents.ts
|
|
5540
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as
|
|
6085
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync19, statSync as statSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
5541
6086
|
import { join as join11 } from "path";
|
|
5542
|
-
import { EasybitsError as EasybitsError5, resolveBaseUrl as
|
|
6087
|
+
import { EasybitsError as EasybitsError5, resolveBaseUrl as resolveBaseUrl9 } from "@easybits.cloud/sdk";
|
|
5543
6088
|
|
|
5544
6089
|
// src/progress.ts
|
|
5545
6090
|
import { writeFileSync as writeFileSync9 } from "fs";
|
|
@@ -5632,15 +6177,15 @@ function blankPageHtml(dims) {
|
|
|
5632
6177
|
return `<section class="w-[${dims.width}px] h-[${dims.height}px] flex flex-col relative overflow-hidden bg-surface"></section>`;
|
|
5633
6178
|
}
|
|
5634
6179
|
var pagesOf = (doc) => [...doc.sections ?? []].sort((a, b) => a.order - b.order);
|
|
5635
|
-
function pageRef(pages,
|
|
5636
|
-
const byId = pages.find((p) => p.id ===
|
|
6180
|
+
function pageRef(pages, ref2, usage2) {
|
|
6181
|
+
const byId = pages.find((p) => p.id === ref2);
|
|
5637
6182
|
if (byId) return byId;
|
|
5638
|
-
if (/^\d+$/.test(
|
|
5639
|
-
const n = Number(
|
|
6183
|
+
if (/^\d+$/.test(ref2)) {
|
|
6184
|
+
const n = Number(ref2);
|
|
5640
6185
|
if (n >= 1 && n <= pages.length) return pages[n - 1];
|
|
5641
6186
|
}
|
|
5642
6187
|
throw usageError(
|
|
5643
|
-
t(`No page "${
|
|
6188
|
+
t(`No page "${ref2}" in this document (${pages.length} pages: use 1-${pages.length} or a page id).`, `No hay p\xE1gina "${ref2}" en este documento (${pages.length} p\xE1ginas: usa 1-${pages.length} o el id de la p\xE1gina).`),
|
|
5644
6189
|
usage2
|
|
5645
6190
|
);
|
|
5646
6191
|
}
|
|
@@ -5662,16 +6207,16 @@ function readHtml(path, usage2) {
|
|
|
5662
6207
|
return readStdin().then((b) => b.toString("utf8"));
|
|
5663
6208
|
}
|
|
5664
6209
|
if (!existsSync16(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
|
|
5665
|
-
return
|
|
6210
|
+
return readFileSync19(path, "utf8");
|
|
5666
6211
|
}
|
|
5667
6212
|
var getDoc = (ctx, id) => api(ctx, "GET", `/documents/${encodeURIComponent(id)}`);
|
|
5668
6213
|
var docPath = (id) => `/documents/${encodeURIComponent(id)}`;
|
|
5669
6214
|
async function editorUrl(id) {
|
|
5670
|
-
return `${(await
|
|
6215
|
+
return `${(await resolveBaseUrl9()).replace(/\/$/, "")}/dash/documents/${id}`;
|
|
5671
6216
|
}
|
|
5672
6217
|
async function getBytes(ctx, path) {
|
|
5673
6218
|
const token = await resolveCredential(ctx);
|
|
5674
|
-
const base = (await
|
|
6219
|
+
const base = (await resolveBaseUrl9()).replace(/\/$/, "");
|
|
5675
6220
|
const res = await fetch(`${base}/api/v2${path}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
5676
6221
|
if (!res.ok) throw new EasybitsError5(res.status, await res.text());
|
|
5677
6222
|
return Buffer.from(await res.arrayBuffer());
|
|
@@ -5846,10 +6391,10 @@ var documents = {
|
|
|
5846
6391
|
examples: ["easybits documents rm-page 'Cotizaci\xF3n ACME' 3 --yes"],
|
|
5847
6392
|
async run(ctx) {
|
|
5848
6393
|
const id = need(ctx, 0, "document", this.usage);
|
|
5849
|
-
const
|
|
6394
|
+
const ref2 = need(ctx, 1, "page", this.usage);
|
|
5850
6395
|
requireYesIfHeadless(ctx);
|
|
5851
6396
|
const pages = pagesOf(await getDoc(ctx, id));
|
|
5852
|
-
const page = pageRef(pages,
|
|
6397
|
+
const page = pageRef(pages, ref2, this.usage);
|
|
5853
6398
|
await confirm(ctx, t(`Delete page ${pages.indexOf(page) + 1} (${page.label || page.id})?`, `\xBFBorrar la p\xE1gina ${pages.indexOf(page) + 1} (${page.label || page.id})?`));
|
|
5854
6399
|
const r = await api(ctx, "DELETE", `${docPath(id)}/pages/${encodeURIComponent(page.id)}`);
|
|
5855
6400
|
emit(ctx, r, () => console.log(t(`Deleted. ${r.remainingPages} page(s) left.`, `Borrada. Quedan ${r.remainingPages} p\xE1gina(s).`)));
|
|
@@ -5951,8 +6496,8 @@ var documents = {
|
|
|
5951
6496
|
};
|
|
5952
6497
|
|
|
5953
6498
|
// src/commands/video.ts
|
|
5954
|
-
import { existsSync as existsSync17, readFileSync as
|
|
5955
|
-
import { EasybitsError as EasybitsError6, resolveBaseUrl as
|
|
6499
|
+
import { existsSync as existsSync17, readFileSync as readFileSync20 } from "fs";
|
|
6500
|
+
import { EasybitsError as EasybitsError6, resolveBaseUrl as resolveBaseUrl10 } from "@easybits.cloud/sdk";
|
|
5956
6501
|
var VIDEO_PRESETS = {
|
|
5957
6502
|
portrait: [1080, 1920],
|
|
5958
6503
|
story: [1080, 1920],
|
|
@@ -5991,22 +6536,22 @@ function projectBody(o, usage2) {
|
|
|
5991
6536
|
return body;
|
|
5992
6537
|
}
|
|
5993
6538
|
var scenesOf = (p) => [...p.scenes ?? []].sort((a, b) => a.order - b.order);
|
|
5994
|
-
function sceneRef(scenes,
|
|
5995
|
-
const byId = scenes.find((s) => s.id ===
|
|
6539
|
+
function sceneRef(scenes, ref2, usage2) {
|
|
6540
|
+
const byId = scenes.find((s) => s.id === ref2);
|
|
5996
6541
|
if (byId) return byId;
|
|
5997
|
-
if (/^\d+$/.test(
|
|
5998
|
-
const n = Number(
|
|
6542
|
+
if (/^\d+$/.test(ref2)) {
|
|
6543
|
+
const n = Number(ref2);
|
|
5999
6544
|
if (n >= 1 && n <= scenes.length) return scenes[n - 1];
|
|
6000
6545
|
}
|
|
6001
6546
|
throw usageError(
|
|
6002
|
-
t(`No scene "${
|
|
6547
|
+
t(`No scene "${ref2}" in this project (${scenes.length} scenes: use 1-${scenes.length} or a scene id).`, `No hay escena "${ref2}" en este proyecto (${scenes.length} escenas: usa 1-${scenes.length} o el id de la escena).`),
|
|
6003
6548
|
usage2
|
|
6004
6549
|
);
|
|
6005
6550
|
}
|
|
6006
6551
|
function sceneRows(scenes) {
|
|
6007
6552
|
return scenes.map((s, i) => ({ n: i + 1, id: s.id, label: s.label ?? "-", durationSec: s.durationSec, narration: s.narration ? s.narration.slice(0, 40) : "-" }));
|
|
6008
6553
|
}
|
|
6009
|
-
function sceneBody(o, usage2, readFile = (p) => existsSync17(p) ?
|
|
6554
|
+
function sceneBody(o, usage2, readFile = (p) => existsSync17(p) ? readFileSync20(p, "utf8") : null) {
|
|
6010
6555
|
const body = {};
|
|
6011
6556
|
if (o.timeline != null) body.timeline = readFile(o.timeline) ?? o.timeline;
|
|
6012
6557
|
if (o.duration != null) {
|
|
@@ -6040,11 +6585,11 @@ function readHtml2(path, usage2) {
|
|
|
6040
6585
|
return readStdin().then((b) => b.toString("utf8"));
|
|
6041
6586
|
}
|
|
6042
6587
|
if (!existsSync17(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
|
|
6043
|
-
return
|
|
6588
|
+
return readFileSync20(path, "utf8");
|
|
6044
6589
|
}
|
|
6045
6590
|
async function postRender(ctx, id, signal) {
|
|
6046
6591
|
const token = await resolveCredential(ctx);
|
|
6047
|
-
const base = (await
|
|
6592
|
+
const base = (await resolveBaseUrl10()).replace(/\/$/, "");
|
|
6048
6593
|
const res = await fetch(`${base}/api/v2${projectPath(id)}/render`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, signal });
|
|
6049
6594
|
const text = await res.text();
|
|
6050
6595
|
if (!res.ok) throw new EasybitsError6(res.status, text);
|
|
@@ -6324,6 +6869,7 @@ var COMMANDS = [
|
|
|
6324
6869
|
usage,
|
|
6325
6870
|
secrets,
|
|
6326
6871
|
crons,
|
|
6872
|
+
inbox,
|
|
6327
6873
|
sandboxes,
|
|
6328
6874
|
agents,
|
|
6329
6875
|
fleet,
|
|
@@ -6664,13 +7210,57 @@ var HELP_ES = {
|
|
|
6664
7210
|
"Path kept out of the release (repeatable)": "Ruta que no entra al release (repetible)",
|
|
6665
7211
|
"Release a machine: stops billing, takes a last backup and destroys it": "Libera una m\xE1quina: deja de cobrarse, toma un \xFAltimo respaldo y la destruye",
|
|
6666
7212
|
"The hosting catalog: sizes and monthly prices in MXN": "El cat\xE1logo de hosting: tama\xF1os y precio mensual en MXN",
|
|
7213
|
+
// crons (tuyos y de la plataforma)
|
|
7214
|
+
"Your scheduled jobs (call a URL, message an agent or run a command; repeating or once) and, for admins, the platform's": "Tus tareas programadas (pegarle a una URL, escribirle a un agente o correr un comando; repetidas o de una sola vez) y, para admins, las de la plataforma",
|
|
7215
|
+
"List your crons (or, with --platform, the platform's scheduled jobs)": "Enlista tus crons (o, con --platform, las tareas de la plataforma)",
|
|
7216
|
+
"Platform jobs (backups, purges\u2026; platform admins only)": "Tareas de la plataforma (respaldos, purgas\u2026; s\xF3lo admins)",
|
|
7217
|
+
"Show the last runs of one of your crons (id or name) or of a platform job, with the full error": "Muestra las \xFAltimas corridas de un cron tuyo (id o nombre) o de una tarea de la plataforma, con el error completo",
|
|
7218
|
+
"Run one of your crons or a platform job now, in the background (trigger manual)": "Corre ya un cron tuyo o una tarea de la plataforma, en segundo plano (manual)",
|
|
7219
|
+
"Pause one of your crons or a platform job until you resume it": "Pausa un cron tuyo o una tarea de la plataforma hasta que lo reanudes",
|
|
7220
|
+
"Resume one of your crons or a platform job": "Reanuda un cron tuyo o una tarea de la plataforma",
|
|
7221
|
+
"Create a cron: call a public https URL, send a turn to one of your agents or run a command in a throwaway box, repeating (at most every 5 min) or once": "Crea un cron: pegarle a una URL https p\xFAblica, mandarle un turno a uno de tus agentes o correr un comando en una caja desechable, repetido (como mucho cada 5 min) o de una sola vez",
|
|
7222
|
+
"Vault secret: a header with --url (default Authorization: Bearer); an env var with --run (repeatable)": "Secreto de la b\xF3veda: header con --url (default Authorization: Bearer); variable de entorno con --run (repetible)",
|
|
7223
|
+
"Run this shell command in a throwaway box instead (python, files and secrets included; success = exit 0)": "Correr este comando de shell en una caja desechable (con python, tus archivos y secretos; \xE9xito = exit 0)",
|
|
7224
|
+
"File mounted for --run, next to the command by default (repeatable)": "Archivo que se monta para --run, junto al comando por default (repetible)",
|
|
7225
|
+
"Upload a local file as a secret only this cron can read, written at the same ~ path in the box (repeatable; e.g. a .p8)": "Sube un archivo local como secreto que s\xF3lo este cron puede leer; se escribe en la misma ruta ~ dentro de la caja (repetible; p. ej. un .p8)",
|
|
7226
|
+
"Box image for --run (default python)": "Imagen de la caja para --run (default python)",
|
|
7227
|
+
"Time limit for --run (default 300, max 600)": "Tiempo l\xEDmite de --run (default 300, m\xE1ximo 600)",
|
|
7228
|
+
"Also print stdout/stderr of --run crons (last 64 KB per run)": "Muestra tambi\xE9n stdout/stderr de los crons --run (\xFAltimos 64 KB por corrida)",
|
|
7229
|
+
"Wait for the next run of one of your crons to finish and exit with its result (exec: its exit code)": "Espera a que termine la siguiente corrida de un cron tuyo y sale con su resultado (exec: su c\xF3digo de salida)",
|
|
7230
|
+
"Give up after this long (default 30m; 10m, 2h\u2026)": "Se rinde despu\xE9s de este tiempo (default 30m; 10m, 2h\u2026)",
|
|
7231
|
+
"Tell you after each run: push to the Ghosty app (vault secret GHOSTY_PUSH_TOKEN), email to your account and/or an event in one of your inboxes; none removes it": "Te avisa tras cada corrida: push a la app de Ghosty (secreto GHOSTY_PUSH_TOKEN de la b\xF3veda), correo a tu cuenta y/o un evento en uno de tus buzones; none lo quita",
|
|
7232
|
+
// inbox (buzones de eventos)
|
|
7233
|
+
"Event inboxes: your own webhook URL whose events reach your terminal live or wait for you to read them": "Buzones de eventos: tu propia URL de webhook; lo que llega te alcanza en la terminal en vivo o te espera para que lo leas",
|
|
7234
|
+
"List your inboxes with their URL and unread events": "Enlista tus buzones con su URL y los eventos sin leer",
|
|
7235
|
+
"Create an inbox and print its webhook URL (anyone with it can write: keep it private)": "Crea un buz\xF3n e imprime su URL de webhook (quien la tenga puede escribir: no la publiques)",
|
|
7236
|
+
"Days events are kept (1-30, default 7)": "D\xEDas que se guardan los eventos (1-30, default 7)",
|
|
7237
|
+
"Stream an inbox: one JSON line per event as it arrives (reconnects on its own)": "Escucha un buz\xF3n: una l\xEDnea JSON por evento en cuanto llega (se reconecta solo)",
|
|
7238
|
+
"Start after this event (0 = everything kept; default: unread)": "Empieza despu\xE9s de este evento (0 = todo lo guardado; default: lo no le\xEDdo)",
|
|
7239
|
+
"Show the events you haven't read yet and mark them read (the next read brings only new ones)": "Muestra los eventos que no has le\xEDdo y los marca como le\xEDdos (la siguiente lectura trae s\xF3lo lo nuevo)",
|
|
7240
|
+
"Everything still kept, read or not": "Todo lo guardado, le\xEDdo o no",
|
|
7241
|
+
"Just look; don't mark them read": "S\xF3lo mirar; no marcarlos como le\xEDdos",
|
|
7242
|
+
"At most N events (default 50, max 200)": "Como m\xE1ximo N eventos (default 50, tope 200)",
|
|
7243
|
+
"Delete an inbox and its events; its URL stops accepting writes": "Borra un buz\xF3n y sus eventos; su URL deja de aceptar escrituras",
|
|
7244
|
+
"When to notify (default failure)": "Cu\xE1ndo avisar (default failure: s\xF3lo si falla)",
|
|
7245
|
+
'Run ONCE at this time: ISO ("2026-10-18T23:00", read in --tz) or "23:00" (today)': 'Correr UNA SOLA VEZ a esta hora: ISO ("2026-10-18T23:00", en la zona de --tz) o "23:00" (hoy)',
|
|
7246
|
+
"Run ONCE after this long: 10m, 2h, 1h30m, 1d": "Correr UNA SOLA VEZ dentro de: 10m, 2h, 1h30m, 1d",
|
|
7247
|
+
"A repeating cron stops after this date": "Un cron que se repite se detiene despu\xE9s de esta fecha",
|
|
7248
|
+
"A repeating cron stops after N more runs": "Un cron que se repite se detiene tras N corridas m\xE1s",
|
|
7249
|
+
"Validate and show the next 3 runs without creating anything": "Valida y muestra las pr\xF3ximas 3 corridas sin crear nada",
|
|
7250
|
+
'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30': 'Cron de 5 campos, p. ej. "30 8 * * *" = diario 8:30',
|
|
7251
|
+
"IANA time zone (default America/Mexico_City)": "Zona horaria IANA (default America/Mexico_City)",
|
|
7252
|
+
"Call this public https URL on each run": "Pegarle a esta URL https p\xFAblica en cada corrida",
|
|
7253
|
+
"HTTP method (default GET)": "M\xE9todo HTTP (default GET)",
|
|
7254
|
+
"Request body (not with GET)": "Cuerpo de la petici\xF3n (no con GET)",
|
|
7255
|
+
"Header for --secret": "Header para --secret",
|
|
7256
|
+
"Send a turn to this fleet agent instead of calling a URL": "Mandarle un turno a este agente de la flota en vez de pegarle a una URL",
|
|
7257
|
+
"What the agent is told on each run (with --agent)": "Lo que se le dice al agente en cada corrida (con --agent)",
|
|
7258
|
+
"Change one of your crons (only what you pass)": "Cambia un cron tuyo (s\xF3lo lo que pases)",
|
|
7259
|
+
"New name": "Nombre nuevo",
|
|
7260
|
+
"New schedule": "Horario nuevo",
|
|
7261
|
+
"New IANA time zone": "Zona horaria IANA nueva",
|
|
7262
|
+
"Delete one of your crons": "Borra un cron tuyo",
|
|
6667
7263
|
// secrets (vault de la cuenta)
|
|
6668
|
-
"Platform scheduled jobs (admins only): status, history, run now, pause": "Tareas programadas de la plataforma (s\xF3lo admins): estado, historial, ejecutar ahora, pausar",
|
|
6669
|
-
"List the scheduled jobs with status, schedule, last and next run": "Enlista las tareas programadas con estado, horario, \xFAltima y pr\xF3xima corrida",
|
|
6670
|
-
"Show a job's last 50 runs, with the full error when one failed": "Muestra las \xFAltimas 50 corridas de una tarea, con el error completo si alguna fall\xF3",
|
|
6671
|
-
"Run a job now in the background (recorded with trigger manual)": "Corre una tarea ya, en segundo plano (queda registrada como manual)",
|
|
6672
|
-
"Pause a job: scheduled runs answer skipped until you resume it": "Pausa una tarea: sus corridas programadas responden skipped hasta que la reanudes",
|
|
6673
|
-
"Resume a paused job": "Reanuda una tarea en pausa",
|
|
6674
7264
|
"Your account's secrets vault: names only; used as $secret:NAME": "La b\xF3veda de secretos de tu cuenta: s\xF3lo nombres; se usan como $secret:NOMBRE",
|
|
6675
7265
|
"List the names in your vault (values never come back)": "Enlista los nombres de tu b\xF3veda (los valores nunca regresan)",
|
|
6676
7266
|
"Save or replace secrets from a dotenv file, or one value from stdin": "Guarda o reemplaza secretos desde un archivo dotenv, o un valor por stdin",
|
|
@@ -6984,7 +7574,7 @@ function normalizeArgs(input) {
|
|
|
6984
7574
|
}
|
|
6985
7575
|
|
|
6986
7576
|
// src/index.ts
|
|
6987
|
-
var VERSION2 = true ? "0.
|
|
7577
|
+
var VERSION2 = true ? "0.19.0" : "dev";
|
|
6988
7578
|
var GLOBAL_OPTIONS = {
|
|
6989
7579
|
json: { type: "boolean" },
|
|
6990
7580
|
token: { type: "string" },
|