@easybits.cloud/cli 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -1
  2. package/dist/index.js +638 -140
  3. 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((resolve3) => server.listen(0, "127.0.0.1", resolve3));
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((resolve3, reject) => {
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
- resolve3(got);
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 ref = need(ctx, 0, "website", this.usage);
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, ref);
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 ref = need(ctx, 0, "website", this.usage);
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, ref);
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 ref = need(ctx, 0, "website", this.usage);
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, ref);
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 ref = need(ctx, 0, "website", this.usage);
924
+ const ref2 = need(ctx, 0, "website", this.usage);
925
925
  requireYesIfHeadless(ctx);
926
- const id = await resolveWebsiteId(ctx, ref);
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(ref, items) {
944
- if (/^[0-9a-f]{24}$/i.test(ref)) return ref;
945
- const r = ref.trim().toLowerCase();
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, ref) {
949
- if (/^[0-9a-f]{24}$/i.test(ref)) return ref;
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(ref, page.items);
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 "${ref}".`, `No hay sitio con id o slug "${ref}".`),
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((resolve3, reject) => {
1414
- ws.onopen = () => resolve3();
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(ref, items) {
1440
- if (/^[0-9a-f]{24}$/i.test(ref)) return ref;
1441
- const r = ref.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
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, ref) {
1448
- if (/^[0-9a-f]{24}$/i.test(ref)) return { id: ref };
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(ref, items);
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 "${ref}" in your account.`, `No hay dominio "${ref}" en tu cuenta.`),
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 ref = need(ctx, 0, "domain", this.usage);
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, ref);
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 ref = need(ctx, 0, "domain", this.usage);
1599
+ const ref2 = need(ctx, 0, "domain", this.usage);
1600
1600
  requireYesIfHeadless(ctx);
1601
- const { id, row } = await resolveDomain(ctx, ref);
1602
- const name = row?.domain ?? ref;
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, ref) => ID_FORMAT[kind].test(ref);
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, ref, items) {
2219
- if (looksLikeId(kind, ref)) return { ok: true, id: ref };
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 === ref)) return { ok: true, id: ref };
2222
- const want = ref.trim().toLowerCase();
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, ref) {
2243
- const key = kind === "document" ? `document:${ref}` : kind;
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(ref ?? "")}`)).items.map((d) => ({ id: d.id, name: d.name }));
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, ref) {
2262
- if (looksLikeId(kind, ref)) return ref;
2263
- const r = matchRef(kind, ref, await listRefs(ctx, kind, ref));
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 "${ref}".`, `No hay ${NOUN()[kind]} con id o nombre "${ref}".`), EXIT.API, `${t("List them with", "Enl\xEDstalos con")}: ${LS_HINT[kind]}`, "not_found", 404);
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(`"${ref}" matches ${r.matches.length} ${NOUNS()[kind]}. Use the id:`, `"${ref}" coincide con ${r.matches.length} ${NOUNS()[kind]}. Usa el id:`),
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 ref = spec ? ctx.args[spec.index] : void 0;
2281
- if (!spec || !ref) return;
2282
- ctx.args[spec.index] = await resolveRef(ctx, spec.kind, ref);
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 ref = need(ctx, 0, "db-id|name", this.usage);
3811
+ const ref2 = need(ctx, 0, "db-id|name", this.usage);
3812
3812
  if (bool(ctx, "dry-run")) {
3813
- const hit2 = await findDb(ctx, ref);
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, ref);
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 ref = need(ctx, 0, "db-id|name", this.usage);
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, ref);
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 ref = need(ctx, 0, "db-id|name", this.usage);
3906
+ const ref2 = need(ctx, 0, "db-id|name", this.usage);
3907
3907
  const eb = await getClient(ctx);
3908
- const hit = await findDb(ctx, ref);
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 ref = need(ctx, 1, "db", this.usage);
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, ref);
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 ref = need(ctx, 0, "db-id|name", this.usage);
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, ref);
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 ref = o.agent ?? specId(text);
4243
- if (!ref) 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", ref);
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.18.0" : "dev";
4921
+ var VERSION = true ? "0.19.1" : "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(ref, keys2) {
5156
- const byId = keys2.find((k) => k.id === ref);
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 = ref.trim();
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, ref) {
5255
+ async function resolveKey(ctx, ref2) {
5256
5256
  const { keys: keys2 } = await (await getClient(ctx)).listKeys();
5257
- const m = matchKey(ref, keys2);
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 "${ref}".`, `No hay key activa con id, nombre o prefijo "${ref}".`), EXIT.API, `${t("List them with", "Enl\xEDstalas con")}: easybits keys ls`, "not_found", 404);
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(`"${ref}" matches ${m.matches.length} keys. Use the id:`, `"${ref}" coincide con ${m.matches.length} keys. Usa el id:`),
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 ref = need(ctx, 0, "key", this.usage);
5325
+ const ref2 = need(ctx, 0, "key", this.usage);
5326
5326
  requireYesIfHeadless(ctx);
5327
- const k = await resolveKey(ctx, ref);
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,35 +5425,98 @@ 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";
5428
5431
  var HEX24 = /^[a-f0-9]{24}$/;
5429
- var isUserCronId = (ref) => HEX24.test(ref);
5432
+ var isUserCronId = (ref2) => HEX24.test(ref2);
5430
5433
  function describeCronAction(a) {
5431
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
+ }
5432
5439
  return `agent ${a.fleetAgentId}: ${a.prompt.length > 40 ? a.prompt.slice(0, 39) + "\u2026" : a.prompt}`;
5433
5440
  }
5434
- async function resolveUserCron(ctx, ref) {
5441
+ function describeCron(c) {
5442
+ const n = c.notify;
5443
+ const chans = n ? [n.push ? "push" : "", n.email ? "email" : "", n.inbox ? `inbox:${n.inbox}` : ""].filter(Boolean).join(", ") : "";
5444
+ const state = c.enabled ? t("active", "activo") : c.done ? t("done", "terminado") : t("paused", "en pausa");
5445
+ return [
5446
+ `${c.id} \xAB${c.name}\xBB (${state})`,
5447
+ `${t("Schedule", "Horario")}: ${c.schedule} (${c.timezone})${c.until ? ` ${t("until", "hasta")} ${c.until}` : ""}${c.timesLeft != null ? ` \xB7 ${t("runs left", "corridas restantes")}: ${c.timesLeft}` : ""}`,
5448
+ `${t("Next run", "Pr\xF3xima")}: ${c.enabled && c.nextRunLocal ? `${c.nextRunLocal} (${c.in})` : "\u2014"}`,
5449
+ `${t("Does", "Hace")}: ${describeCronAction(c.action)}`,
5450
+ `${t("Notifies", "Avisa")}: ${chans ? `${chans} (${n.on === "always" ? t("always", "siempre") : t("on failure", "si falla")})` : "\u2014"}`,
5451
+ `${t("Last run", "\xDAltima")}: ${c.lastRunAt ? `${fmtDate(c.lastRunAt)} ${c.lastOk === false ? t("failed", "fall\xF3") : "ok"}${c.lastError ? ` \u2014 ${c.lastError}` : ""}` : "\u2014"}`
5452
+ ];
5453
+ }
5454
+ async function resolveUserCron(ctx, ref2) {
5435
5455
  const { items } = await api(ctx, "GET", "/crons");
5436
- const byId = items.find((c) => c.id === ref);
5456
+ const byId = items.find((c) => c.id === ref2);
5437
5457
  if (byId) return byId;
5438
- const byName = items.filter((c) => c.name.toLowerCase() === ref.trim().toLowerCase());
5458
+ const byName = items.filter((c) => c.name.toLowerCase() === ref2.trim().toLowerCase());
5439
5459
  if (byName.length > 1) {
5440
- throw new CliError(t(`"${ref}" matches ${byName.length} crons. Use the id.`, `"${ref}" coincide con ${byName.length} crons. Usa el id.`), EXIT.USAGE);
5460
+ throw new CliError(t(`"${ref2}" matches ${byName.length} crons. Use the id.`, `"${ref2}" coincide con ${byName.length} crons. Usa el id.`), EXIT.USAGE);
5441
5461
  }
5442
5462
  return byName[0] ?? null;
5443
5463
  }
5444
- async function userCronOrNull(ctx, ref) {
5445
- if (isUserCronId(ref)) return await resolveUserCron(ctx, ref) ?? { id: ref };
5446
- return resolveUserCron(ctx, ref);
5464
+ async function userCronOrNull(ctx, ref2) {
5465
+ if (isUserCronId(ref2)) return await resolveUserCron(ctx, ref2) ?? { id: ref2 };
5466
+ return resolveUserCron(ctx, ref2);
5447
5467
  }
5448
5468
  var ACTION_OPTIONS = {
5449
5469
  url: { type: "string", value: "https-url", description: "Call this public https URL on each run" },
5450
5470
  method: { type: "string", value: "GET|POST|PUT|PATCH|DELETE", description: "HTTP method (default GET)" },
5451
5471
  body: { type: "string", value: "json|text", description: "Request body (not with GET)" },
5452
- secret: { type: "string", value: "NAME", description: "Vault secret sent as a header (default Authorization: Bearer)" },
5472
+ secret: { type: "string", multiple: true, value: "NAME", description: "Vault secret: a header with --url (default Authorization: Bearer); an env var with --run (repeatable)" },
5453
5473
  "secret-header": { type: "string", value: "header", description: "Header for --secret" },
5454
5474
  agent: { type: "string", value: "id", description: "Send a turn to this fleet agent instead of calling a URL" },
5455
- prompt: { type: "string", value: "text", description: "What the agent is told on each run (with --agent)" }
5475
+ prompt: { type: "string", value: "text", description: "What the agent is told on each run (with --agent)" },
5476
+ run: { type: "string", value: "command", description: "Run this shell command in a throwaway box instead (python, files and secrets included; success = exit 0)" },
5477
+ file: { type: "string", multiple: true, value: "local[=remote]", description: "File mounted for --run, next to the command by default (repeatable)" },
5478
+ "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)" },
5479
+ template: { type: "string", value: "python|node|bun|ubuntu", description: "Box image for --run (default python)" },
5480
+ timeout: { type: "string", value: "seconds", description: "Time limit for --run (default 300, max 600)" }
5456
5481
  };
5482
+ function remotePathFor(local, home = homedir7()) {
5483
+ const abs = resolve3(local.replace(/^~(?=\/)/, home));
5484
+ return abs.startsWith(home + "/") ? `~/${abs.slice(home.length + 1)}` : basename5(abs);
5485
+ }
5486
+ function execFromFlags(f, read, home = homedir7()) {
5487
+ if (!f.run) return {};
5488
+ const expand = (p) => resolve3(p.replace(/^~(?=\/)/, home));
5489
+ const split = (spec) => {
5490
+ const eq = spec.indexOf("=");
5491
+ return eq < 0 ? [spec, void 0] : [spec.slice(0, eq), spec.slice(eq + 1)];
5492
+ };
5493
+ const files3 = f.files.map((spec) => {
5494
+ const [local, remote] = split(spec);
5495
+ return { path: remote || basename5(expand(local)), content: Buffer.from(read(expand(local)), "utf8").toString("base64"), encoding: "base64" };
5496
+ });
5497
+ const secretValues = {};
5498
+ const secrets2 = f.secrets.map((name) => ({ name }));
5499
+ for (const spec of f.secretFiles) {
5500
+ const colon = spec.indexOf(":");
5501
+ if (colon < 1) return { error: `--secret-file needs NAME:path, got "${spec}"` };
5502
+ const name = spec.slice(0, colon);
5503
+ const [local, remote] = split(spec.slice(colon + 1));
5504
+ secretValues[name] = read(expand(local));
5505
+ secrets2.push({ name, file: remote || remotePathFor(local, home) });
5506
+ }
5507
+ if (f.timeout != null && !/^\d+$/.test(f.timeout)) return { error: "--timeout must be whole seconds" };
5508
+ return {
5509
+ action: {
5510
+ type: "exec",
5511
+ command: f.run,
5512
+ ...files3.length ? { files: files3 } : {},
5513
+ ...secrets2.length ? { secrets: secrets2 } : {},
5514
+ ...f.template ? { template: f.template } : {},
5515
+ ...f.timeout ? { timeoutSeconds: Number(f.timeout) } : {}
5516
+ },
5517
+ ...Object.keys(secretValues).length ? { secretValues } : {}
5518
+ };
5519
+ }
5457
5520
  function actionFromFlags(f) {
5458
5521
  if (f.url && f.agent) return { error: "--url and --agent are exclusive" };
5459
5522
  if (f.url) {
@@ -5473,15 +5536,90 @@ function actionFromFlags(f) {
5473
5536
  }
5474
5537
  return {};
5475
5538
  }
5539
+ var NOTIFY_OPTIONS = {
5540
+ 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" },
5541
+ "notify-on": { type: "string", value: "failure|always", description: "When to notify (default failure)" }
5542
+ };
5543
+ function notifyFromFlags(f) {
5544
+ if (f.notify == null) return f.notifyOn ? { error: "--notify-on needs --notify" } : {};
5545
+ if (f.notify === "none") return { notify: null };
5546
+ const chans = f.notify.split(",").map((x) => x.trim()).filter(Boolean);
5547
+ const inboxChan = chans.find((x) => x.startsWith("inbox:"));
5548
+ const bad = chans.find((x) => x !== "push" && x !== "email" && !x.startsWith("inbox:"));
5549
+ if (bad || !chans.length || inboxChan === "inbox:") return { error: `--notify takes push, email, inbox:<name> or none (got "${f.notify}")` };
5550
+ if (f.notifyOn && f.notifyOn !== "failure" && f.notifyOn !== "always") return { error: "--notify-on is failure or always" };
5551
+ return {
5552
+ notify: {
5553
+ on: f.notifyOn ?? "failure",
5554
+ ...chans.includes("push") ? { push: true } : {},
5555
+ ...chans.includes("email") ? { email: true } : {},
5556
+ ...inboxChan ? { inbox: inboxChan.slice("inbox:".length) } : {}
5557
+ }
5558
+ };
5559
+ }
5560
+ async function resolveNotifyInbox(ctx, nf) {
5561
+ const name = nf.notify?.inbox;
5562
+ if (!name || /^[a-f0-9]{24}$/.test(name)) return nf;
5563
+ const b = await api(ctx, "GET", `/inboxes/${encodeURIComponent(name)}`);
5564
+ return { ...nf, notify: { ...nf.notify, inbox: b.id } };
5565
+ }
5566
+ var WHEN_OPTIONS = {
5567
+ at: { type: "string", value: "when", description: 'Run ONCE at this time: ISO ("2026-10-18T23:00", read in --tz) or "23:00" (today)' },
5568
+ in: { type: "string", value: "duration", description: "Run ONCE after this long: 10m, 2h, 1h30m, 1d" },
5569
+ until: { type: "string", value: "when", description: "A repeating cron stops after this date" },
5570
+ times: { type: "string", value: "N", description: "A repeating cron stops after N more runs" }
5571
+ };
5572
+ function scheduleFromFlags(f) {
5573
+ if (f.cron && (f.at || f.in)) return { error: "--cron repeats; --at/--in run once. Pick one." };
5574
+ if (f.at && f.in) return { error: "--at and --in are exclusive" };
5575
+ if (f.times != null && !/^\d+$/.test(f.times)) return { error: "--times must be a whole number" };
5576
+ return {
5577
+ fields: {
5578
+ ...f.cron ? { cron: f.cron } : {},
5579
+ ...f.at ? { at: f.at } : {},
5580
+ ...f.in ? { in: f.in } : {},
5581
+ ...f.until ? { until: f.until } : {},
5582
+ ...f.times != null ? { times: Number(f.times) } : {},
5583
+ ...f.tz ? { timezone: f.tz } : {}
5584
+ }
5585
+ };
5586
+ }
5587
+ var whenOf = (ctx) => ({
5588
+ cron: str(ctx, "cron"),
5589
+ at: str(ctx, "at"),
5590
+ in: str(ctx, "in"),
5591
+ until: str(ctx, "until"),
5592
+ times: str(ctx, "times"),
5593
+ tz: str(ctx, "tz")
5594
+ });
5595
+ 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");
5476
5596
  var flagsOf = (ctx) => ({
5477
5597
  url: str(ctx, "url"),
5478
5598
  method: str(ctx, "method"),
5479
5599
  body: str(ctx, "body"),
5480
- secret: str(ctx, "secret"),
5600
+ secret: list(ctx, "secret")[0],
5481
5601
  secretHeader: str(ctx, "secret-header"),
5482
5602
  agent: str(ctx, "agent"),
5483
5603
  prompt: str(ctx, "prompt")
5484
5604
  });
5605
+ function actionOf(ctx) {
5606
+ const run = str(ctx, "run");
5607
+ if (run && (str(ctx, "url") || str(ctx, "agent"))) return { error: "--run, --url and --agent are exclusive" };
5608
+ if (run) {
5609
+ return execFromFlags(
5610
+ { run, files: list(ctx, "file"), secrets: list(ctx, "secret"), secretFiles: list(ctx, "secret-file"), template: str(ctx, "template"), timeout: str(ctx, "timeout") },
5611
+ (p) => {
5612
+ try {
5613
+ return readFileSync18(p, "utf8");
5614
+ } catch {
5615
+ throw new CliError(t(`Can't read ${p}.`, `No se pudo leer ${p}.`), EXIT.USAGE);
5616
+ }
5617
+ }
5618
+ );
5619
+ }
5620
+ if (list(ctx, "secret").length > 1) return { error: "--url takes one --secret" };
5621
+ return actionFromFlags(flagsOf(ctx));
5622
+ }
5485
5623
  var STATUS = {
5486
5624
  "al-dia": ["up to date", "al d\xEDa"],
5487
5625
  tarde: ["late", "tarde"],
@@ -5494,6 +5632,14 @@ function cronStatus(c) {
5494
5632
  const s = STATUS[c.status];
5495
5633
  return s ? t(s[0], s[1]) : c.status;
5496
5634
  }
5635
+ var runExitCode = (r) => r.exitCode != null ? Math.min(Math.max(r.exitCode, 0), 255) : r.ok ? 0 : 1;
5636
+ function parseDurationMs(raw) {
5637
+ const s = raw.trim().toLowerCase();
5638
+ if (!/^(\d+(d|h|m|s))+$/.test(s)) return null;
5639
+ let ms = 0;
5640
+ 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];
5641
+ return ms;
5642
+ }
5497
5643
  function duration(ms) {
5498
5644
  if (ms == null) return "\u2014";
5499
5645
  return ms < 1e3 ? `${ms} ms` : `${(ms / 1e3).toFixed(1)} s`;
@@ -5502,8 +5648,8 @@ var crons = {
5502
5648
  name: "crons",
5503
5649
  aliases: ["cron"],
5504
5650
  group: "Account",
5505
- summary: "Your scheduled jobs (call a URL or message an agent) and, for admins, the platform's",
5506
- synopsis: "crons <ls|create|update|rm|run|runs|pause|resume>",
5651
+ summary: "Your scheduled jobs (call a URL, message an agent or run a command; repeating or once) and, for admins, the platform's",
5652
+ synopsis: "crons <ls|get|create|update|rm|run|runs|wait|pause|resume>",
5507
5653
  subs: {
5508
5654
  ls: {
5509
5655
  aliases: ["list"],
@@ -5521,10 +5667,10 @@ var crons = {
5521
5667
  items2.map((c) => ({
5522
5668
  id: c.id,
5523
5669
  name: c.name,
5524
- schedule: c.enabled ? c.schedule : `${c.schedule} (${t("paused", "en pausa")})`,
5670
+ schedule: c.enabled ? c.schedule : `${c.schedule} (${c.done ? t("done", "terminado") : t("paused", "en pausa")})`,
5525
5671
  does: describeCronAction(c.action),
5526
5672
  last: c.lastRunAt ? `${fmtDate(c.lastRunAt)} ${c.lastOk === false ? t("failed", "fall\xF3") : "ok"}` : "\u2014",
5527
- next: c.enabled ? fmtDate(c.nextRunAt) : "\u2014"
5673
+ next: c.enabled && c.nextRunLocal ? `${c.nextRunLocal} (${c.in})` : "\u2014"
5528
5674
  })),
5529
5675
  [["id", "ID"], ["name", "NAME"], ["schedule", "SCHEDULE"], ["does", "DOES"], ["last", "LAST RUN"], ["next", "NEXT RUN"]],
5530
5676
  t(
@@ -5552,21 +5698,42 @@ var crons = {
5552
5698
  );
5553
5699
  }
5554
5700
  },
5701
+ get: {
5702
+ aliases: ["show"],
5703
+ summary: "Show one of your crons: what it does, how it notifies you and when it runs (--json: the whole object)",
5704
+ usage: "easybits crons get <id|name> [--json]",
5705
+ examples: ['easybits crons get "Review 1.0.10"', "easybits crons get 66f0c2a1b2c3d4e5f6a7b8c9 --json"],
5706
+ async run(ctx) {
5707
+ const ref2 = need(ctx, 0, "id|name", this.usage);
5708
+ const c = await resolveUserCron(ctx, ref2);
5709
+ if (!c) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
5710
+ emit(ctx, c, () => {
5711
+ for (const line2 of describeCron(c)) console.log(line2);
5712
+ });
5713
+ }
5714
+ },
5555
5715
  runs: {
5556
5716
  summary: "Show the last runs of one of your crons (id or name) or of a platform job, with the full error",
5557
- usage: "easybits crons runs <id|name|job>",
5558
- examples: ["easybits crons runs 66f0c2a1b2c3d4e5f6a7b8c9", "easybits crons runs backup-machines --json"],
5717
+ usage: "easybits crons runs <id|name|job> [--limit N] [--logs]",
5718
+ options: {
5719
+ limit: { type: "string", value: "N", description: "At most N runs of one of your crons (1-50, default 50)" },
5720
+ logs: { type: "boolean", description: "Also print stdout/stderr of --run crons (last 64 KB per run)" }
5721
+ },
5722
+ examples: ["easybits crons runs 66f0c2a1b2c3d4e5f6a7b8c9", 'easybits crons runs "Review 1.0.10" --logs', "easybits crons runs backup-machines --json"],
5559
5723
  async run(ctx) {
5560
5724
  const key = need(ctx, 0, "id|name|job", this.usage);
5561
5725
  const mine = await userCronOrNull(ctx, key);
5562
- const path = mine ? `/crons/${mine.id}/runs?limit=50` : `/admin/crons/${encodeURIComponent(key)}/runs`;
5726
+ const withLogs = bool(ctx, "logs");
5727
+ const limit = str(ctx, "limit") ?? "50";
5728
+ if (!/^\d+$/.test(limit) || Number(limit) < 1 || Number(limit) > 50) throw usageError(t("--limit is 1 to 50.", "--limit va de 1 a 50."), this.usage);
5729
+ const path = mine ? `/crons/${mine.id}/runs?limit=${limit}${withLogs ? "&logs=1" : ""}` : `/admin/crons/${encodeURIComponent(key)}/runs`;
5563
5730
  const { items } = await api(ctx, "GET", path);
5564
5731
  emit(ctx, { items }, () => {
5565
5732
  table(
5566
5733
  items.map((r) => ({
5567
5734
  at: fmtDate(r.startedAt),
5568
5735
  ok: r.ok == null ? t("running", "corriendo") : r.ok ? "ok" : t("failed", "fall\xF3"),
5569
- status: r.status ?? "\u2014",
5736
+ status: r.exitCode != null ? `exit ${r.exitCode}` : r.status ?? "\u2014",
5570
5737
  took: duration(r.durationMs),
5571
5738
  trigger: r.trigger,
5572
5739
  summary: (r.error ?? r.summary ?? "").slice(0, 80)
@@ -5574,13 +5741,60 @@ var crons = {
5574
5741
  [["at", "STARTED"], ["ok", "RESULT"], ["status", "HTTP"], ["took", "TOOK"], ["trigger", "TRIGGER"], ["summary", "SUMMARY"]],
5575
5742
  t("No runs yet.", "Todav\xEDa no hay corridas.")
5576
5743
  );
5744
+ if (withLogs) {
5745
+ const last = items.find((r) => r.logs);
5746
+ if (last) console.log(`
5747
+ ${t("Output", "Salida")} (${fmtDate(last.startedAt)}):
5748
+ ${last.logs}`);
5749
+ }
5577
5750
  const failed = items.find((r) => r.ok === false && r.error);
5578
- if (failed) console.log(`
5751
+ if (failed && !withLogs) console.log(`
5579
5752
  ${t("Last error", "\xDAltimo error")} (${fmtDate(failed.startedAt)}):
5580
5753
  ${failed.error}`);
5581
5754
  });
5582
5755
  }
5583
5756
  },
5757
+ wait: {
5758
+ summary: "Wait for the next run of one of your crons to finish and exit with its result (exec: its exit code)",
5759
+ usage: "easybits crons wait <id|name> [--timeout 30m] [--json]",
5760
+ options: { timeout: { type: "string", value: "duration", description: "Give up after this long (default 30m; 10m, 2h\u2026)" } },
5761
+ examples: ['easybits crons wait "Review 1.0.10"', "easybits crons wait 66f0c2a1b2c3d4e5f6a7b8c9 --timeout 2h --json"],
5762
+ async run(ctx) {
5763
+ const ref2 = need(ctx, 0, "id|name", this.usage);
5764
+ const mine = await resolveUserCron(ctx, ref2);
5765
+ if (!mine) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
5766
+ const limitMs = parseDurationMs(str(ctx, "timeout") ?? "30m");
5767
+ if (limitMs == null) throw usageError(t("--timeout like 10m, 2h or 1d.", "--timeout como 10m, 2h o 1d."), this.usage);
5768
+ const deadline = Date.now() + limitMs;
5769
+ let after;
5770
+ for (; ; ) {
5771
+ const left = Math.ceil((deadline - Date.now()) / 1e3);
5772
+ const q = new URLSearchParams({ wait: String(Math.max(1, Math.min(50, left))) });
5773
+ if (after) q.set("after", after);
5774
+ const r = await api(ctx, "GET", `/crons/${mine.id}/runs/next?${q}`);
5775
+ after = r.after;
5776
+ if (r.run) {
5777
+ const code = runExitCode(r.run);
5778
+ emit(ctx, r, () => {
5779
+ 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)})`);
5780
+ if (r.run.logs) console.log(r.run.logs);
5781
+ else if (r.run.error) console.log(r.run.error);
5782
+ else if (r.run.summary) console.log(r.run.summary);
5783
+ });
5784
+ process.exitCode = code;
5785
+ return;
5786
+ }
5787
+ if (Date.now() >= deadline) {
5788
+ throw new CliError(
5789
+ 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})` : ""}.`),
5790
+ EXIT.API,
5791
+ `easybits crons wait ${mine.id} --timeout 2h`,
5792
+ "timeout"
5793
+ );
5794
+ }
5795
+ }
5796
+ }
5797
+ },
5584
5798
  run: {
5585
5799
  summary: "Run one of your crons or a platform job now, in the background (trigger manual)",
5586
5800
  usage: "easybits crons run <id|name|job>",
@@ -5620,54 +5834,81 @@ ${failed.error}`);
5620
5834
  },
5621
5835
  create: {
5622
5836
  aliases: ["add"],
5623
- summary: "Create a cron: call a public https URL or send a turn to one of your agents (at most every 5 min)",
5624
- usage: 'easybits crons create <name> --cron "<5 fields>" [--tz <IANA>] (--url <https-url> [--method] [--body] [--secret NAME] | --agent <id> --prompt <text>)',
5837
+ 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",
5838
+ 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]',
5625
5839
  options: {
5626
5840
  cron: { type: "string", value: "expr", description: 'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30' },
5841
+ ...WHEN_OPTIONS,
5627
5842
  tz: { type: "string", value: "zone", description: "IANA time zone (default America/Mexico_City)" },
5628
- ...ACTION_OPTIONS
5843
+ ...ACTION_OPTIONS,
5844
+ ...NOTIFY_OPTIONS,
5845
+ "dry-run": { type: "boolean", description: "Validate and show the next 3 runs without creating anything" }
5629
5846
  },
5630
5847
  examples: [
5631
5848
  `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`,
5632
- 'easybits crons create "Weekly report" --cron "0 9 * * 1" --agent 6a3c046d2cf7d7f4163f739d --prompt "Send me the weekly sales summary"'
5849
+ 'easybits crons create "Ping once" --at 23:00 --url https://example.com/ping --dry-run',
5850
+ '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',
5851
+ 'easybits crons create "Weekly report" --cron "0 9 * * 1" --times 4 --agent 6a3c046d2cf7d7f4163f739d --prompt "Send me the weekly sales summary"'
5633
5852
  ],
5634
5853
  async run(ctx) {
5635
5854
  const name = need(ctx, 0, "name", this.usage);
5636
- const cron = str(ctx, "cron");
5637
- if (!cron) throw usageError(t("Missing --cron.", "Falta --cron."), this.usage);
5638
- const a = actionFromFlags(flagsOf(ctx));
5855
+ const w = scheduleFromFlags(whenOf(ctx));
5856
+ if ("error" in w) throw usageError(w.error, this.usage);
5857
+ 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);
5858
+ const a = actionOf(ctx);
5639
5859
  if ("error" in a) throw usageError(a.error, this.usage);
5640
- if (!a.action) throw usageError(t("Missing --url or --agent.", "Falta --url o --agent."), this.usage);
5641
- const c = await api(ctx, "POST", "/crons", { name, cron, ...str(ctx, "tz") ? { timezone: str(ctx, "tz") } : {}, action: a.action });
5642
- emit(ctx, c, () => console.log(t(`Created ${c.id} \xAB${c.name}\xBB \u2014 ${c.schedule}; next ${fmtDate(c.nextRunAt)}`, `Creado ${c.id} \xAB${c.name}\xBB \u2014 ${c.schedule}; pr\xF3xima ${fmtDate(c.nextRunAt)}`)));
5860
+ if (!a.action) throw usageError(t("Missing --url, --agent or --run.", "Falta --url, --agent o --run."), this.usage);
5861
+ const nf0 = notifyFromFlags({ notify: str(ctx, "notify"), notifyOn: str(ctx, "notify-on") });
5862
+ if ("error" in nf0) throw usageError(nf0.error, this.usage);
5863
+ const nf = await resolveNotifyInbox(ctx, nf0);
5864
+ const body = { name, ...w.fields, action: a.action, ...a.secretValues ? { secretValues: a.secretValues } : {}, ...nf };
5865
+ if (bool(ctx, "dry-run")) {
5866
+ const p = await api(ctx, "POST", "/crons", { ...body, dryRun: true });
5867
+ emit(ctx, p, () => {
5868
+ 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}`));
5869
+ for (const r of p.nextRuns) console.log(` ${r.local} ${p.timezone} (${r.at})`);
5870
+ if (p.in) console.log(t(`First run in ${p.in}.`, `La primera corre en ${p.in}.`));
5871
+ });
5872
+ return;
5873
+ }
5874
+ const c = await api(ctx, "POST", "/crons", body);
5875
+ 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)));
5643
5876
  }
5644
5877
  },
5645
5878
  update: {
5646
5879
  aliases: ["edit", "set"],
5647
5880
  summary: "Change one of your crons (only what you pass)",
5648
- usage: 'easybits crons update <id|name> [--name <n>] [--cron "<expr>"] [--tz <IANA>] [--url \u2026 | --agent \u2026 --prompt \u2026]',
5881
+ 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 | --run <cmd> [--file f]\u2026 [--secret NAME]\u2026] [--notify push,email,inbox:<name>|none] [--notify-on failure|always]',
5649
5882
  options: {
5650
5883
  name: { type: "string", value: "name", description: "New name" },
5651
5884
  cron: { type: "string", value: "expr", description: "New schedule" },
5885
+ ...WHEN_OPTIONS,
5886
+ ...NOTIFY_OPTIONS,
5652
5887
  tz: { type: "string", value: "zone", description: "New IANA time zone" },
5653
5888
  ...ACTION_OPTIONS
5654
5889
  },
5655
5890
  examples: ['easybits crons update 66f0c2a1b2c3d4e5f6a7b8c9 --cron "0 7 * * 1-5"'],
5656
5891
  async run(ctx) {
5657
- const ref = need(ctx, 0, "id|name", this.usage);
5658
- const mine = await resolveUserCron(ctx, ref);
5659
- if (!mine) throw new CliError(t(`You have no cron "${ref}".`, `No tienes un cron "${ref}".`), EXIT.API, "easybits crons ls", "not_found", 404);
5660
- const a = actionFromFlags(flagsOf(ctx));
5892
+ const ref2 = need(ctx, 0, "id|name", this.usage);
5893
+ const mine = await resolveUserCron(ctx, ref2);
5894
+ if (!mine) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
5895
+ const a = actionOf(ctx);
5661
5896
  if ("error" in a) throw usageError(a.error, this.usage);
5897
+ const w = scheduleFromFlags(whenOf(ctx));
5898
+ if ("error" in w) throw usageError(w.error, this.usage);
5899
+ const nf0 = notifyFromFlags({ notify: str(ctx, "notify"), notifyOn: str(ctx, "notify-on") });
5900
+ if ("error" in nf0) throw usageError(nf0.error, this.usage);
5901
+ const nf = await resolveNotifyInbox(ctx, nf0);
5662
5902
  const patch = {
5903
+ ...nf,
5663
5904
  ...str(ctx, "name") ? { name: str(ctx, "name") } : {},
5664
- ...str(ctx, "cron") ? { cron: str(ctx, "cron") } : {},
5665
- ...str(ctx, "tz") ? { timezone: str(ctx, "tz") } : {},
5666
- ...a.action ? { action: a.action } : {}
5905
+ ...w.fields,
5906
+ ...a.action ? { action: a.action } : {},
5907
+ ...a.secretValues ? { secretValues: a.secretValues } : {}
5667
5908
  };
5668
5909
  if (!Object.keys(patch).length) throw usageError(t("Nothing to change.", "No hay nada que cambiar."), this.usage);
5669
5910
  const c = await api(ctx, "PATCH", `/crons/${mine.id}`, patch);
5670
- emit(ctx, c, () => console.log(t(`Updated \xAB${c.name}\xBB \u2014 ${c.schedule}`, `Actualizado \xAB${c.name}\xBB \u2014 ${c.schedule}`)));
5911
+ emit(ctx, c, () => console.log(t(`Updated \xAB${c.name}\xBB \u2014 ${c.schedule}; `, `Actualizado \xAB${c.name}\xBB \u2014 ${c.schedule}; `) + nextLine(c)));
5671
5912
  }
5672
5913
  },
5673
5914
  rm: {
@@ -5677,9 +5918,9 @@ ${failed.error}`);
5677
5918
  options: { ...YES_OPTION },
5678
5919
  examples: ["easybits crons rm 66f0c2a1b2c3d4e5f6a7b8c9 --yes"],
5679
5920
  async run(ctx) {
5680
- const ref = need(ctx, 0, "id|name", this.usage);
5681
- const mine = await resolveUserCron(ctx, ref);
5682
- if (!mine) throw new CliError(t(`You have no cron "${ref}".`, `No tienes un cron "${ref}".`), EXIT.API, "easybits crons ls", "not_found", 404);
5921
+ const ref2 = need(ctx, 0, "id|name", this.usage);
5922
+ const mine = await resolveUserCron(ctx, ref2);
5923
+ if (!mine) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
5683
5924
  requireYesIfHeadless(ctx);
5684
5925
  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.`));
5685
5926
  const r = await api(ctx, "DELETE", `/crons/${mine.id}`);
@@ -5690,10 +5931,232 @@ ${failed.error}`);
5690
5931
  defaultSub: "ls"
5691
5932
  };
5692
5933
 
5934
+ // src/commands/inbox.ts
5935
+ import { resolveBaseUrl as resolveBaseUrl8 } from "@easybits.cloud/sdk";
5936
+ var ref = (r) => encodeURIComponent(r);
5937
+ function eventLine(e, width = 100) {
5938
+ const body = (e.json !== void 0 ? JSON.stringify(e.json) : e.body).replace(/\s+/g, " ");
5939
+ const head = `#${e.seq} ${fmtDate(e.receivedAt)} ${e.source}`;
5940
+ const room = Math.max(width - head.length - 1, 10);
5941
+ return `${head} ${body.length > room ? body.slice(0, room - 1) + "\u2026" : body}`;
5942
+ }
5943
+ function parseSse(buffer) {
5944
+ const blocks = buffer.split(/\r?\n\r?\n/);
5945
+ const rest = blocks.pop() ?? "";
5946
+ const events = [];
5947
+ for (const block of blocks) {
5948
+ let event = "message";
5949
+ let id;
5950
+ const data = [];
5951
+ for (const line2 of block.split(/\r?\n/)) {
5952
+ if (line2.startsWith(":")) continue;
5953
+ const i = line2.indexOf(":");
5954
+ const k = i < 0 ? line2 : line2.slice(0, i);
5955
+ const v = i < 0 ? "" : line2.slice(i + 1).replace(/^ /, "");
5956
+ if (k === "event") event = v;
5957
+ else if (k === "data") data.push(v);
5958
+ else if (k === "id") id = v;
5959
+ }
5960
+ if (data.length) events.push({ event, data: data.join("\n"), id });
5961
+ }
5962
+ return { events, rest };
5963
+ }
5964
+ async function listen(ctx, name, since) {
5965
+ const base = (await resolveBaseUrl8()).replace(/\/$/, "");
5966
+ let last = since;
5967
+ let backoff = 1e3;
5968
+ for (; ; ) {
5969
+ const token = await resolveCredential(ctx);
5970
+ const q = last != null ? `?since=${last}` : "";
5971
+ let res;
5972
+ try {
5973
+ res = await fetch(`${base}/api/v2/inboxes/${ref(name)}/events/stream${q}`, { headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" } });
5974
+ } catch {
5975
+ await new Promise((r) => setTimeout(r, backoff));
5976
+ backoff = Math.min(backoff * 2, 3e4);
5977
+ continue;
5978
+ }
5979
+ if (res.status === 401 || res.status === 403 || res.status === 404) {
5980
+ 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");
5981
+ }
5982
+ if (!res.ok || !res.body) {
5983
+ await new Promise((r) => setTimeout(r, backoff));
5984
+ backoff = Math.min(backoff * 2, 3e4);
5985
+ continue;
5986
+ }
5987
+ backoff = 1e3;
5988
+ const reader = res.body.getReader();
5989
+ const dec = new TextDecoder();
5990
+ let buf = "";
5991
+ try {
5992
+ for (; ; ) {
5993
+ const { value, done } = await reader.read();
5994
+ if (done) break;
5995
+ buf += dec.decode(value, { stream: true });
5996
+ const { events, rest } = parseSse(buf);
5997
+ buf = rest;
5998
+ for (const ev of events) {
5999
+ if (ev.event === "ready") {
6000
+ const r = JSON.parse(ev.data);
6001
+ if (last == null) last = r.since;
6002
+ process.stderr.write(t(`Listening to \xAB${r.name}\xBB from #${r.since}. One JSON line per event.
6003
+ `, `Escuchando \xAB${r.name}\xBB desde #${r.since}. Una l\xEDnea JSON por evento.
6004
+ `));
6005
+ } else if (ev.event === "inbox") {
6006
+ const e = JSON.parse(ev.data);
6007
+ last = e.seq;
6008
+ process.stdout.write(JSON.stringify(e) + "\n");
6009
+ }
6010
+ }
6011
+ }
6012
+ } catch {
6013
+ }
6014
+ process.stderr.write(t("Connection dropped; reconnecting\u2026\n", "Se cort\xF3 la conexi\xF3n; reconectando\u2026\n"));
6015
+ }
6016
+ }
6017
+ var inbox = {
6018
+ name: "inbox",
6019
+ aliases: ["inboxes"],
6020
+ group: "Account",
6021
+ summary: "Event inboxes: your own webhook URL whose events reach your terminal live or wait for you to read them",
6022
+ synopsis: "inbox <ls|create|listen|read|update|rotate|rm>",
6023
+ subs: {
6024
+ ls: {
6025
+ aliases: ["list"],
6026
+ summary: "List your inboxes with their URL and unread events",
6027
+ usage: "easybits inbox ls",
6028
+ examples: ["easybits inbox ls", "easybits inbox ls --json"],
6029
+ async run(ctx) {
6030
+ const { items } = await api(ctx, "GET", "/inboxes");
6031
+ emit(
6032
+ ctx,
6033
+ { items },
6034
+ () => table(
6035
+ items.map((b) => ({ name: b.name, unread: b.unread, last: b.lastEventAt ? fmtDate(b.lastEventAt) : "\u2014", url: b.url })),
6036
+ [["name", "NAME"], ["unread", "UNREAD"], ["last", "LAST EVENT"], ["url", "URL"]],
6037
+ t("No inboxes. Create one: easybits inbox create builds", "Sin buzones. Crea uno: easybits inbox create builds")
6038
+ )
6039
+ );
6040
+ }
6041
+ },
6042
+ create: {
6043
+ aliases: ["add"],
6044
+ summary: "Create an inbox and print its webhook URL (anyone with it can write: keep it private)",
6045
+ usage: "easybits inbox create <name> [--retention <days>]",
6046
+ options: { retention: { type: "string", value: "days", description: "Days events are kept (1-30, default 7)" } },
6047
+ examples: ["easybits inbox create builds", "easybits inbox create stripe --retention 30 --json"],
6048
+ async run(ctx) {
6049
+ const name = need(ctx, 0, "name", this.usage);
6050
+ const days = str(ctx, "retention");
6051
+ if (days != null && !/^\d+$/.test(days)) throw usageError(t("--retention is whole days.", "--retention son d\xEDas enteros."), this.usage);
6052
+ const b = await api(ctx, "POST", "/inboxes", { name, ...days ? { retentionDays: Number(days) } : {} });
6053
+ emit(ctx, b, () => {
6054
+ 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:`));
6055
+ console.log(` ${b.url}`);
6056
+ 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}`));
6057
+ });
6058
+ }
6059
+ },
6060
+ listen: {
6061
+ aliases: ["tail", "follow"],
6062
+ summary: "Stream an inbox: one JSON line per event as it arrives (reconnects on its own)",
6063
+ usage: "easybits inbox listen <id|name> [--since <seq>]",
6064
+ options: { since: { type: "string", value: "seq", description: "Start after this event (0 = everything kept; default: unread)" } },
6065
+ examples: ["easybits inbox listen builds", "easybits inbox listen builds --since 0 | jq .json"],
6066
+ async run(ctx) {
6067
+ const name = need(ctx, 0, "id|name", this.usage);
6068
+ const since = str(ctx, "since");
6069
+ if (since != null && !/^\d+$/.test(since)) throw usageError(t("--since is an event number.", "--since es un n\xFAmero de evento."), this.usage);
6070
+ await listen(ctx, name, since != null ? Number(since) : void 0);
6071
+ }
6072
+ },
6073
+ read: {
6074
+ summary: "Show the events you haven't read yet and mark them read (the next read brings only new ones)",
6075
+ usage: "easybits inbox read <id|name> [--all | --since <seq>] [--no-ack] [--limit N]",
6076
+ options: {
6077
+ all: { type: "boolean", description: "Everything still kept, read or not" },
6078
+ since: { type: "string", value: "seq", description: "Events after this one, read or not (the cursor of a previous read)" },
6079
+ "no-ack": { type: "boolean", description: "Just look; don't mark them read" },
6080
+ limit: { type: "string", value: "N", description: "At most N events (default 50, max 200)" }
6081
+ },
6082
+ examples: ["easybits inbox read builds", "easybits inbox read builds --all --no-ack --json", "easybits inbox read builds --since 12 --no-ack"],
6083
+ async run(ctx) {
6084
+ const name = need(ctx, 0, "id|name", this.usage);
6085
+ const since = str(ctx, "since");
6086
+ if (since != null && !/^\d+$/.test(since)) throw usageError(t("--since is an event number.", "--since es un n\xFAmero de evento."), this.usage);
6087
+ if (since != null && bool(ctx, "all")) throw usageError(t("--all and --since are exclusive.", "--all y --since no van juntos."), this.usage);
6088
+ const q = new URLSearchParams();
6089
+ if (bool(ctx, "all")) q.set("since", "0");
6090
+ if (since != null) q.set("since", since);
6091
+ if (bool(ctx, "no-ack")) q.set("ack", "0");
6092
+ if (str(ctx, "limit")) q.set("limit", str(ctx, "limit"));
6093
+ const r = await api(ctx, "GET", `/inboxes/${ref(name)}/events${q.size ? `?${q}` : ""}`);
6094
+ emit(ctx, r, () => {
6095
+ if (!r.items.length) return console.log(t("Nothing new.", "Nada nuevo."));
6096
+ for (const e of r.items) console.log(eventLine(e, process.stdout.columns || 100));
6097
+ if (r.unread) console.log(t(`
6098
+ ${r.unread} more unread: easybits inbox read ${name}`, `
6099
+ Quedan ${r.unread} sin leer: easybits inbox read ${name}`));
6100
+ });
6101
+ }
6102
+ },
6103
+ update: {
6104
+ aliases: ["edit", "set"],
6105
+ summary: "Rename an inbox or change how long it keeps events (its URL stays the same)",
6106
+ usage: "easybits inbox update <id|name> [--name <new>] [--retention <days>]",
6107
+ options: {
6108
+ name: { type: "string", value: "new", description: "New name, unique in your account" },
6109
+ retention: { type: "string", value: "days", description: "Days events are kept (1-30)" }
6110
+ },
6111
+ examples: ["easybits inbox update builds --retention 30", "easybits inbox update builds --name ci --json"],
6112
+ async run(ctx) {
6113
+ const name = need(ctx, 0, "id|name", this.usage);
6114
+ const days = str(ctx, "retention");
6115
+ if (days != null && !/^\d+$/.test(days)) throw usageError(t("--retention is whole days.", "--retention son d\xEDas enteros."), this.usage);
6116
+ const patch = { ...str(ctx, "name") ? { name: str(ctx, "name") } : {}, ...days ? { retentionDays: Number(days) } : {} };
6117
+ if (!Object.keys(patch).length) throw usageError(t("Nothing to change: pass --name or --retention.", "No hay nada que cambiar: pasa --name o --retention."), this.usage);
6118
+ const b = await api(ctx, "PATCH", `/inboxes/${ref(name)}`, patch);
6119
+ emit(ctx, b, () => console.log(t(`Updated \xAB${b.name}\xBB: keeps events ${b.retentionDays} days.`, `Actualizado \xAB${b.name}\xBB: guarda los eventos ${b.retentionDays} d\xEDas.`)));
6120
+ }
6121
+ },
6122
+ rotate: {
6123
+ summary: "Give an inbox a new URL and print it; the old one stops accepting writes right away (stored events stay)",
6124
+ usage: "easybits inbox rotate <id|name> [--yes]",
6125
+ options: { ...YES_OPTION },
6126
+ examples: ["easybits inbox rotate builds --yes", "easybits inbox rotate builds --yes --json"],
6127
+ async run(ctx) {
6128
+ const name = need(ctx, 0, "id|name", this.usage);
6129
+ requireYesIfHeadless(ctx);
6130
+ await confirm(ctx, t(`New URL for \xAB${name}\xBB? Whoever uses the old one stops getting through.`, `\xBFURL nueva para \xAB${name}\xBB? Quien use la vieja deja de poder escribir.`));
6131
+ const b = await api(ctx, "POST", `/inboxes/${ref(name)}/rotate`);
6132
+ emit(ctx, b, () => {
6133
+ console.log(t(`New URL for \xAB${b.name}\xBB (the old one no longer works):`, `URL nueva de \xAB${b.name}\xBB (la vieja ya no sirve):`));
6134
+ console.log(` ${b.url}`);
6135
+ });
6136
+ }
6137
+ },
6138
+ rm: {
6139
+ aliases: ["remove", "delete"],
6140
+ summary: "Delete an inbox and its events; its URL stops accepting writes",
6141
+ usage: "easybits inbox rm <id|name> [--yes]",
6142
+ options: { ...YES_OPTION },
6143
+ examples: ["easybits inbox rm builds --yes"],
6144
+ async run(ctx) {
6145
+ const name = need(ctx, 0, "id|name", this.usage);
6146
+ requireYesIfHeadless(ctx);
6147
+ await confirm(ctx, t(`Delete inbox \xAB${name}\xBB and its events?`, `\xBFBorrar el buz\xF3n \xAB${name}\xBB y sus eventos?`));
6148
+ const r = await api(ctx, "DELETE", `/inboxes/${ref(name)}`);
6149
+ emit(ctx, r, () => console.log(t(`Deleted \xAB${name}\xBB`, `Borrado \xAB${name}\xBB`)));
6150
+ }
6151
+ }
6152
+ },
6153
+ defaultSub: "ls"
6154
+ };
6155
+
5693
6156
  // src/commands/documents.ts
5694
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync18, statSync as statSync11, writeFileSync as writeFileSync10 } from "fs";
6157
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync19, statSync as statSync11, writeFileSync as writeFileSync10 } from "fs";
5695
6158
  import { join as join11 } from "path";
5696
- import { EasybitsError as EasybitsError5, resolveBaseUrl as resolveBaseUrl8 } from "@easybits.cloud/sdk";
6159
+ import { EasybitsError as EasybitsError5, resolveBaseUrl as resolveBaseUrl9 } from "@easybits.cloud/sdk";
5697
6160
 
5698
6161
  // src/progress.ts
5699
6162
  import { writeFileSync as writeFileSync9 } from "fs";
@@ -5786,15 +6249,15 @@ function blankPageHtml(dims) {
5786
6249
  return `<section class="w-[${dims.width}px] h-[${dims.height}px] flex flex-col relative overflow-hidden bg-surface"></section>`;
5787
6250
  }
5788
6251
  var pagesOf = (doc) => [...doc.sections ?? []].sort((a, b) => a.order - b.order);
5789
- function pageRef(pages, ref, usage2) {
5790
- const byId = pages.find((p) => p.id === ref);
6252
+ function pageRef(pages, ref2, usage2) {
6253
+ const byId = pages.find((p) => p.id === ref2);
5791
6254
  if (byId) return byId;
5792
- if (/^\d+$/.test(ref)) {
5793
- const n = Number(ref);
6255
+ if (/^\d+$/.test(ref2)) {
6256
+ const n = Number(ref2);
5794
6257
  if (n >= 1 && n <= pages.length) return pages[n - 1];
5795
6258
  }
5796
6259
  throw usageError(
5797
- t(`No page "${ref}" in this document (${pages.length} pages: use 1-${pages.length} or a page id).`, `No hay p\xE1gina "${ref}" en este documento (${pages.length} p\xE1ginas: usa 1-${pages.length} o el id de la p\xE1gina).`),
6260
+ 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).`),
5798
6261
  usage2
5799
6262
  );
5800
6263
  }
@@ -5816,16 +6279,16 @@ function readHtml(path, usage2) {
5816
6279
  return readStdin().then((b) => b.toString("utf8"));
5817
6280
  }
5818
6281
  if (!existsSync16(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
5819
- return readFileSync18(path, "utf8");
6282
+ return readFileSync19(path, "utf8");
5820
6283
  }
5821
6284
  var getDoc = (ctx, id) => api(ctx, "GET", `/documents/${encodeURIComponent(id)}`);
5822
6285
  var docPath = (id) => `/documents/${encodeURIComponent(id)}`;
5823
6286
  async function editorUrl(id) {
5824
- return `${(await resolveBaseUrl8()).replace(/\/$/, "")}/dash/documents/${id}`;
6287
+ return `${(await resolveBaseUrl9()).replace(/\/$/, "")}/dash/documents/${id}`;
5825
6288
  }
5826
6289
  async function getBytes(ctx, path) {
5827
6290
  const token = await resolveCredential(ctx);
5828
- const base = (await resolveBaseUrl8()).replace(/\/$/, "");
6291
+ const base = (await resolveBaseUrl9()).replace(/\/$/, "");
5829
6292
  const res = await fetch(`${base}/api/v2${path}`, { headers: { Authorization: `Bearer ${token}` } });
5830
6293
  if (!res.ok) throw new EasybitsError5(res.status, await res.text());
5831
6294
  return Buffer.from(await res.arrayBuffer());
@@ -6000,10 +6463,10 @@ var documents = {
6000
6463
  examples: ["easybits documents rm-page 'Cotizaci\xF3n ACME' 3 --yes"],
6001
6464
  async run(ctx) {
6002
6465
  const id = need(ctx, 0, "document", this.usage);
6003
- const ref = need(ctx, 1, "page", this.usage);
6466
+ const ref2 = need(ctx, 1, "page", this.usage);
6004
6467
  requireYesIfHeadless(ctx);
6005
6468
  const pages = pagesOf(await getDoc(ctx, id));
6006
- const page = pageRef(pages, ref, this.usage);
6469
+ const page = pageRef(pages, ref2, this.usage);
6007
6470
  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})?`));
6008
6471
  const r = await api(ctx, "DELETE", `${docPath(id)}/pages/${encodeURIComponent(page.id)}`);
6009
6472
  emit(ctx, r, () => console.log(t(`Deleted. ${r.remainingPages} page(s) left.`, `Borrada. Quedan ${r.remainingPages} p\xE1gina(s).`)));
@@ -6105,8 +6568,8 @@ var documents = {
6105
6568
  };
6106
6569
 
6107
6570
  // src/commands/video.ts
6108
- import { existsSync as existsSync17, readFileSync as readFileSync19 } from "fs";
6109
- import { EasybitsError as EasybitsError6, resolveBaseUrl as resolveBaseUrl9 } from "@easybits.cloud/sdk";
6571
+ import { existsSync as existsSync17, readFileSync as readFileSync20 } from "fs";
6572
+ import { EasybitsError as EasybitsError6, resolveBaseUrl as resolveBaseUrl10 } from "@easybits.cloud/sdk";
6110
6573
  var VIDEO_PRESETS = {
6111
6574
  portrait: [1080, 1920],
6112
6575
  story: [1080, 1920],
@@ -6145,22 +6608,22 @@ function projectBody(o, usage2) {
6145
6608
  return body;
6146
6609
  }
6147
6610
  var scenesOf = (p) => [...p.scenes ?? []].sort((a, b) => a.order - b.order);
6148
- function sceneRef(scenes, ref, usage2) {
6149
- const byId = scenes.find((s) => s.id === ref);
6611
+ function sceneRef(scenes, ref2, usage2) {
6612
+ const byId = scenes.find((s) => s.id === ref2);
6150
6613
  if (byId) return byId;
6151
- if (/^\d+$/.test(ref)) {
6152
- const n = Number(ref);
6614
+ if (/^\d+$/.test(ref2)) {
6615
+ const n = Number(ref2);
6153
6616
  if (n >= 1 && n <= scenes.length) return scenes[n - 1];
6154
6617
  }
6155
6618
  throw usageError(
6156
- t(`No scene "${ref}" in this project (${scenes.length} scenes: use 1-${scenes.length} or a scene id).`, `No hay escena "${ref}" en este proyecto (${scenes.length} escenas: usa 1-${scenes.length} o el id de la escena).`),
6619
+ 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).`),
6157
6620
  usage2
6158
6621
  );
6159
6622
  }
6160
6623
  function sceneRows(scenes) {
6161
6624
  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) : "-" }));
6162
6625
  }
6163
- function sceneBody(o, usage2, readFile = (p) => existsSync17(p) ? readFileSync19(p, "utf8") : null) {
6626
+ function sceneBody(o, usage2, readFile = (p) => existsSync17(p) ? readFileSync20(p, "utf8") : null) {
6164
6627
  const body = {};
6165
6628
  if (o.timeline != null) body.timeline = readFile(o.timeline) ?? o.timeline;
6166
6629
  if (o.duration != null) {
@@ -6194,11 +6657,11 @@ function readHtml2(path, usage2) {
6194
6657
  return readStdin().then((b) => b.toString("utf8"));
6195
6658
  }
6196
6659
  if (!existsSync17(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
6197
- return readFileSync19(path, "utf8");
6660
+ return readFileSync20(path, "utf8");
6198
6661
  }
6199
6662
  async function postRender(ctx, id, signal) {
6200
6663
  const token = await resolveCredential(ctx);
6201
- const base = (await resolveBaseUrl9()).replace(/\/$/, "");
6664
+ const base = (await resolveBaseUrl10()).replace(/\/$/, "");
6202
6665
  const res = await fetch(`${base}/api/v2${projectPath(id)}/render`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, signal });
6203
6666
  const text = await res.text();
6204
6667
  if (!res.ok) throw new EasybitsError6(res.status, text);
@@ -6478,6 +6941,7 @@ var COMMANDS = [
6478
6941
  usage,
6479
6942
  secrets,
6480
6943
  crons,
6944
+ inbox,
6481
6945
  sandboxes,
6482
6946
  agents,
6483
6947
  fleet,
@@ -6819,20 +7283,54 @@ var HELP_ES = {
6819
7283
  "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",
6820
7284
  "The hosting catalog: sizes and monthly prices in MXN": "El cat\xE1logo de hosting: tama\xF1os y precio mensual en MXN",
6821
7285
  // crons (tuyos y de la plataforma)
6822
- "Your scheduled jobs (call a URL or message an agent) and, for admins, the platform's": "Tus tareas programadas (pegarle a una URL o escribirle a un agente) y, para admins, las de la plataforma",
7286
+ "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",
6823
7287
  "List your crons (or, with --platform, the platform's scheduled jobs)": "Enlista tus crons (o, con --platform, las tareas de la plataforma)",
6824
7288
  "Platform jobs (backups, purges\u2026; platform admins only)": "Tareas de la plataforma (respaldos, purgas\u2026; s\xF3lo admins)",
6825
7289
  "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",
6826
7290
  "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)",
6827
7291
  "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",
6828
7292
  "Resume one of your crons or a platform job": "Reanuda un cron tuyo o una tarea de la plataforma",
6829
- "Create a cron: call a public https URL or send a turn to one of your agents (at most every 5 min)": "Crea un cron: pegarle a una URL https p\xFAblica o mandarle un turno a uno de tus agentes (como mucho cada 5 min)",
7293
+ "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",
7294
+ "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)",
7295
+ "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)",
7296
+ "File mounted for --run, next to the command by default (repeatable)": "Archivo que se monta para --run, junto al comando por default (repetible)",
7297
+ "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)",
7298
+ "Box image for --run (default python)": "Imagen de la caja para --run (default python)",
7299
+ "Time limit for --run (default 300, max 600)": "Tiempo l\xEDmite de --run (default 300, m\xE1ximo 600)",
7300
+ "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)",
7301
+ "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)",
7302
+ "Give up after this long (default 30m; 10m, 2h\u2026)": "Se rinde despu\xE9s de este tiempo (default 30m; 10m, 2h\u2026)",
7303
+ "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",
7304
+ // inbox (buzones de eventos)
7305
+ "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",
7306
+ "List your inboxes with their URL and unread events": "Enlista tus buzones con su URL y los eventos sin leer",
7307
+ "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)",
7308
+ "Days events are kept (1-30, default 7)": "D\xEDas que se guardan los eventos (1-30, default 7)",
7309
+ "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)",
7310
+ "Start after this event (0 = everything kept; default: unread)": "Empieza despu\xE9s de este evento (0 = todo lo guardado; default: lo no le\xEDdo)",
7311
+ "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)",
7312
+ "Everything still kept, read or not": "Todo lo guardado, le\xEDdo o no",
7313
+ "Just look; don't mark them read": "S\xF3lo mirar; no marcarlos como le\xEDdos",
7314
+ "At most N events (default 50, max 200)": "Como m\xE1ximo N eventos (default 50, tope 200)",
7315
+ "Delete an inbox and its events; its URL stops accepting writes": "Borra un buz\xF3n y sus eventos; su URL deja de aceptar escrituras",
7316
+ "Events after this one, read or not (the cursor of a previous read)": "Los eventos despu\xE9s de \xE9ste, le\xEDdos o no (el cursor de una lectura anterior)",
7317
+ "Rename an inbox or change how long it keeps events (its URL stays the same)": "Renombra un buz\xF3n o cambia cu\xE1nto guarda los eventos (su URL no cambia)",
7318
+ "New name, unique in your account": "Nombre nuevo, \xFAnico en tu cuenta",
7319
+ "Days events are kept (1-30)": "D\xEDas que se guardan los eventos (1-30)",
7320
+ "Give an inbox a new URL and print it; the old one stops accepting writes right away (stored events stay)": "Le da al buz\xF3n una URL nueva y la imprime; la vieja deja de aceptar escrituras de inmediato (los eventos guardados se quedan)",
7321
+ "Show one of your crons: what it does, how it notifies you and when it runs (--json: the whole object)": "Muestra un cron tuyo: qu\xE9 hace, c\xF3mo te avisa y cu\xE1ndo corre (--json: el objeto completo)",
7322
+ "At most N runs of one of your crons (1-50, default 50)": "Como m\xE1ximo N corridas de un cron tuyo (1-50, default 50)",
7323
+ "When to notify (default failure)": "Cu\xE1ndo avisar (default failure: s\xF3lo si falla)",
7324
+ '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)',
7325
+ "Run ONCE after this long: 10m, 2h, 1h30m, 1d": "Correr UNA SOLA VEZ dentro de: 10m, 2h, 1h30m, 1d",
7326
+ "A repeating cron stops after this date": "Un cron que se repite se detiene despu\xE9s de esta fecha",
7327
+ "A repeating cron stops after N more runs": "Un cron que se repite se detiene tras N corridas m\xE1s",
7328
+ "Validate and show the next 3 runs without creating anything": "Valida y muestra las pr\xF3ximas 3 corridas sin crear nada",
6830
7329
  'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30': 'Cron de 5 campos, p. ej. "30 8 * * *" = diario 8:30',
6831
7330
  "IANA time zone (default America/Mexico_City)": "Zona horaria IANA (default America/Mexico_City)",
6832
7331
  "Call this public https URL on each run": "Pegarle a esta URL https p\xFAblica en cada corrida",
6833
7332
  "HTTP method (default GET)": "M\xE9todo HTTP (default GET)",
6834
7333
  "Request body (not with GET)": "Cuerpo de la petici\xF3n (no con GET)",
6835
- "Vault secret sent as a header (default Authorization: Bearer)": "Secreto de la b\xF3veda que viaja como header (default Authorization: Bearer)",
6836
7334
  "Header for --secret": "Header para --secret",
6837
7335
  "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",
6838
7336
  "What the agent is told on each run (with --agent)": "Lo que se le dice al agente en cada corrida (con --agent)",
@@ -7155,7 +7653,7 @@ function normalizeArgs(input) {
7155
7653
  }
7156
7654
 
7157
7655
  // src/index.ts
7158
- var VERSION2 = true ? "0.18.0" : "dev";
7656
+ var VERSION2 = true ? "0.19.1" : "dev";
7159
7657
  var GLOBAL_OPTIONS = {
7160
7658
  json: { type: "boolean" },
7161
7659
  token: { type: "string" },