@easybits.cloud/cli 0.17.1 → 0.18.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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/index.js +204 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -123,8 +123,19 @@ easybits secrets set --dotenv .env.production # KEY=VALUE lines; - reads stdin
123
123
  printf '%s' "$TOKEN" | easybits secrets set GITHUB_TOKEN # one value from stdin
124
124
  easybits secrets rm GITHUB_TOKEN --yes
125
125
 
126
- # Platform crons (platform admins only; key with the ADMIN scope)
126
+ # Your crons: call a public https URL or message one of your agents (at most every 5 min)
127
+ easybits crons create "Study reminder" --cron "30 8 * * *" --url https://www.ghosty.studio/api/v2/me/push \
128
+ --method POST --body '{"title":"Study time"}' --secret GHOSTY_PUSH_TOKEN
129
+ easybits crons create "Weekly report" --cron "0 9 * * 1" --agent <agentId> --prompt "Send me the sales summary"
127
130
  easybits crons ls
131
+ easybits crons update "Study reminder" --cron "0 7 * * 1-5"
132
+ easybits crons run "Study reminder" # id or exact name
133
+ easybits crons runs "Study reminder"
134
+ easybits crons pause "Study reminder"
135
+ easybits crons rm "Study reminder" --yes
136
+
137
+ # Platform crons (platform admins only; key with the ADMIN scope)
138
+ easybits crons ls --platform
128
139
  easybits crons runs backup-machines
129
140
  easybits crons run purge-notifications
130
141
  easybits crons pause check-hosting
package/dist/index.js CHANGED
@@ -4918,7 +4918,7 @@ async function updateNotice(current2) {
4918
4918
  }
4919
4919
 
4920
4920
  // src/commands/doctor.ts
4921
- var VERSION = true ? "0.17.1" : "dev";
4921
+ var VERSION = true ? "0.18.0" : "dev";
4922
4922
  var doctor2 = {
4923
4923
  name: "doctor",
4924
4924
  group: "Account",
@@ -5425,6 +5425,63 @@ var secrets = {
5425
5425
  };
5426
5426
 
5427
5427
  // src/commands/crons.ts
5428
+ var HEX24 = /^[a-f0-9]{24}$/;
5429
+ var isUserCronId = (ref) => HEX24.test(ref);
5430
+ function describeCronAction(a) {
5431
+ if (a.type === "http") return `${a.method} ${a.url}${a.secretName ? ` (+${a.secretName})` : ""}`;
5432
+ return `agent ${a.fleetAgentId}: ${a.prompt.length > 40 ? a.prompt.slice(0, 39) + "\u2026" : a.prompt}`;
5433
+ }
5434
+ async function resolveUserCron(ctx, ref) {
5435
+ const { items } = await api(ctx, "GET", "/crons");
5436
+ const byId = items.find((c) => c.id === ref);
5437
+ if (byId) return byId;
5438
+ const byName = items.filter((c) => c.name.toLowerCase() === ref.trim().toLowerCase());
5439
+ 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);
5441
+ }
5442
+ return byName[0] ?? null;
5443
+ }
5444
+ async function userCronOrNull(ctx, ref) {
5445
+ if (isUserCronId(ref)) return await resolveUserCron(ctx, ref) ?? { id: ref };
5446
+ return resolveUserCron(ctx, ref);
5447
+ }
5448
+ var ACTION_OPTIONS = {
5449
+ url: { type: "string", value: "https-url", description: "Call this public https URL on each run" },
5450
+ method: { type: "string", value: "GET|POST|PUT|PATCH|DELETE", description: "HTTP method (default GET)" },
5451
+ 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)" },
5453
+ "secret-header": { type: "string", value: "header", description: "Header for --secret" },
5454
+ 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)" }
5456
+ };
5457
+ function actionFromFlags(f) {
5458
+ if (f.url && f.agent) return { error: "--url and --agent are exclusive" };
5459
+ if (f.url) {
5460
+ return {
5461
+ action: {
5462
+ type: "http",
5463
+ url: f.url,
5464
+ method: (f.method ?? "GET").toUpperCase(),
5465
+ ...f.body ? { body: f.body } : {},
5466
+ ...f.secret ? { secretName: f.secret, ...f.secretHeader ? { secretHeader: f.secretHeader } : {} } : {}
5467
+ }
5468
+ };
5469
+ }
5470
+ if (f.agent) {
5471
+ if (!f.prompt) return { error: "--agent needs --prompt" };
5472
+ return { action: { type: "agent_turn", fleetAgentId: f.agent, prompt: f.prompt } };
5473
+ }
5474
+ return {};
5475
+ }
5476
+ var flagsOf = (ctx) => ({
5477
+ url: str(ctx, "url"),
5478
+ method: str(ctx, "method"),
5479
+ body: str(ctx, "body"),
5480
+ secret: str(ctx, "secret"),
5481
+ secretHeader: str(ctx, "secret-header"),
5482
+ agent: str(ctx, "agent"),
5483
+ prompt: str(ctx, "prompt")
5484
+ });
5428
5485
  var STATUS = {
5429
5486
  "al-dia": ["up to date", "al d\xEDa"],
5430
5487
  tarde: ["late", "tarde"],
@@ -5445,15 +5502,39 @@ var crons = {
5445
5502
  name: "crons",
5446
5503
  aliases: ["cron"],
5447
5504
  group: "Account",
5448
- summary: "Platform scheduled jobs (admins only): status, history, run now, pause",
5449
- synopsis: "crons <ls|runs|run|pause|resume>",
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>",
5450
5507
  subs: {
5451
5508
  ls: {
5452
5509
  aliases: ["list"],
5453
- summary: "List the scheduled jobs with status, schedule, last and next run",
5454
- usage: "easybits crons ls",
5455
- examples: ["easybits crons ls", "easybits crons ls --json"],
5510
+ summary: "List your crons (or, with --platform, the platform's scheduled jobs)",
5511
+ usage: "easybits crons ls [--platform]",
5512
+ options: { platform: { type: "boolean", description: "Platform jobs (backups, purges\u2026; platform admins only)" } },
5513
+ examples: ["easybits crons ls", "easybits crons ls --json", "easybits crons ls --platform"],
5456
5514
  async run(ctx) {
5515
+ if (!bool(ctx, "platform")) {
5516
+ const { items: items2 } = await api(ctx, "GET", "/crons");
5517
+ emit(
5518
+ ctx,
5519
+ { items: items2 },
5520
+ () => table(
5521
+ items2.map((c) => ({
5522
+ id: c.id,
5523
+ name: c.name,
5524
+ schedule: c.enabled ? c.schedule : `${c.schedule} (${t("paused", "en pausa")})`,
5525
+ does: describeCronAction(c.action),
5526
+ last: c.lastRunAt ? `${fmtDate(c.lastRunAt)} ${c.lastOk === false ? t("failed", "fall\xF3") : "ok"}` : "\u2014",
5527
+ next: c.enabled ? fmtDate(c.nextRunAt) : "\u2014"
5528
+ })),
5529
+ [["id", "ID"], ["name", "NAME"], ["schedule", "SCHEDULE"], ["does", "DOES"], ["last", "LAST RUN"], ["next", "NEXT RUN"]],
5530
+ t(
5531
+ 'No crons. Create one: easybits crons create "Ping" --cron "*/15 * * * *" --url https://example.com/ping',
5532
+ 'Sin crons. Crea uno: easybits crons create "Ping" --cron "*/15 * * * *" --url https://ejemplo.com/ping'
5533
+ )
5534
+ )
5535
+ );
5536
+ return;
5537
+ }
5457
5538
  const { items } = await api(ctx, "GET", "/admin/crons");
5458
5539
  emit(
5459
5540
  ctx,
@@ -5472,12 +5553,14 @@ var crons = {
5472
5553
  }
5473
5554
  },
5474
5555
  runs: {
5475
- summary: "Show a job's last 50 runs, with the full error when one failed",
5476
- usage: "easybits crons runs <job>",
5477
- examples: ["easybits crons runs backup-machines", "easybits crons runs check-hosting --json"],
5556
+ 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"],
5478
5559
  async run(ctx) {
5479
- const key = need(ctx, 0, "job", this.usage);
5480
- const { items } = await api(ctx, "GET", `/admin/crons/${encodeURIComponent(key)}/runs`);
5560
+ const key = need(ctx, 0, "id|name|job", this.usage);
5561
+ const mine = await userCronOrNull(ctx, key);
5562
+ const path = mine ? `/crons/${mine.id}/runs?limit=50` : `/admin/crons/${encodeURIComponent(key)}/runs`;
5563
+ const { items } = await api(ctx, "GET", path);
5481
5564
  emit(ctx, { items }, () => {
5482
5565
  table(
5483
5566
  items.map((r) => ({
@@ -5499,12 +5582,13 @@ ${failed.error}`);
5499
5582
  }
5500
5583
  },
5501
5584
  run: {
5502
- summary: "Run a job now in the background (recorded with trigger manual)",
5503
- usage: "easybits crons run <job>",
5504
- examples: ["easybits crons run purge-notifications", "easybits crons run backup-machines --json"],
5585
+ summary: "Run one of your crons or a platform job now, in the background (trigger manual)",
5586
+ usage: "easybits crons run <id|name|job>",
5587
+ examples: ["easybits crons run 66f0c2a1b2c3d4e5f6a7b8c9", "easybits crons run purge-notifications --json"],
5505
5588
  async run(ctx) {
5506
- const key = need(ctx, 0, "job", this.usage);
5507
- const r = await api(ctx, "POST", `/admin/crons/${encodeURIComponent(key)}/run`);
5589
+ const key = need(ctx, 0, "id|name|job", this.usage);
5590
+ const mine = await userCronOrNull(ctx, key);
5591
+ const r = await api(ctx, "POST", mine ? `/crons/${mine.id}/run` : `/admin/crons/${encodeURIComponent(key)}/run`);
5508
5592
  emit(
5509
5593
  ctx,
5510
5594
  r,
@@ -5513,24 +5597,94 @@ ${failed.error}`);
5513
5597
  }
5514
5598
  },
5515
5599
  pause: {
5516
- summary: "Pause a job: scheduled runs answer skipped until you resume it",
5517
- usage: "easybits crons pause <job>",
5518
- examples: ["easybits crons pause check-hosting"],
5600
+ summary: "Pause one of your crons or a platform job until you resume it",
5601
+ usage: "easybits crons pause <id|name|job>",
5602
+ examples: ["easybits crons pause 66f0c2a1b2c3d4e5f6a7b8c9", "easybits crons pause check-hosting"],
5519
5603
  async run(ctx) {
5520
- const key = need(ctx, 0, "job", this.usage);
5521
- const r = await api(ctx, "POST", `/admin/crons/${encodeURIComponent(key)}/pause`, { paused: true });
5604
+ const key = need(ctx, 0, "id|name|job", this.usage);
5605
+ const mine = await userCronOrNull(ctx, key);
5606
+ const r = mine ? await api(ctx, "PATCH", `/crons/${mine.id}`, { enabled: false }) : await api(ctx, "POST", `/admin/crons/${encodeURIComponent(key)}/pause`, { paused: true });
5522
5607
  emit(ctx, r, () => console.log(t(`Paused ${key}.`, `${key} en pausa.`)));
5523
5608
  }
5524
5609
  },
5525
5610
  resume: {
5526
- summary: "Resume a paused job",
5527
- usage: "easybits crons resume <job>",
5528
- examples: ["easybits crons resume check-hosting"],
5611
+ summary: "Resume one of your crons or a platform job",
5612
+ usage: "easybits crons resume <id|name|job>",
5613
+ examples: ["easybits crons resume 66f0c2a1b2c3d4e5f6a7b8c9", "easybits crons resume check-hosting"],
5529
5614
  async run(ctx) {
5530
- const key = need(ctx, 0, "job", this.usage);
5531
- const r = await api(ctx, "POST", `/admin/crons/${encodeURIComponent(key)}/pause`, { paused: false });
5615
+ const key = need(ctx, 0, "id|name|job", this.usage);
5616
+ const mine = await userCronOrNull(ctx, key);
5617
+ const r = mine ? await api(ctx, "PATCH", `/crons/${mine.id}`, { enabled: true }) : await api(ctx, "POST", `/admin/crons/${encodeURIComponent(key)}/pause`, { paused: false });
5532
5618
  emit(ctx, r, () => console.log(t(`Resumed ${key}.`, `${key} reanudada.`)));
5533
5619
  }
5620
+ },
5621
+ create: {
5622
+ 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>)',
5625
+ options: {
5626
+ cron: { type: "string", value: "expr", description: 'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30' },
5627
+ tz: { type: "string", value: "zone", description: "IANA time zone (default America/Mexico_City)" },
5628
+ ...ACTION_OPTIONS
5629
+ },
5630
+ examples: [
5631
+ `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"'
5633
+ ],
5634
+ async run(ctx) {
5635
+ 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));
5639
+ 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)}`)));
5643
+ }
5644
+ },
5645
+ update: {
5646
+ aliases: ["edit", "set"],
5647
+ 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]',
5649
+ options: {
5650
+ name: { type: "string", value: "name", description: "New name" },
5651
+ cron: { type: "string", value: "expr", description: "New schedule" },
5652
+ tz: { type: "string", value: "zone", description: "New IANA time zone" },
5653
+ ...ACTION_OPTIONS
5654
+ },
5655
+ examples: ['easybits crons update 66f0c2a1b2c3d4e5f6a7b8c9 --cron "0 7 * * 1-5"'],
5656
+ 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));
5661
+ if ("error" in a) throw usageError(a.error, this.usage);
5662
+ const patch = {
5663
+ ...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 } : {}
5667
+ };
5668
+ if (!Object.keys(patch).length) throw usageError(t("Nothing to change.", "No hay nada que cambiar."), this.usage);
5669
+ 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}`)));
5671
+ }
5672
+ },
5673
+ rm: {
5674
+ aliases: ["remove", "delete"],
5675
+ summary: "Delete one of your crons",
5676
+ usage: "easybits crons rm <id|name> [--yes]",
5677
+ options: { ...YES_OPTION },
5678
+ examples: ["easybits crons rm 66f0c2a1b2c3d4e5f6a7b8c9 --yes"],
5679
+ 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);
5683
+ requireYesIfHeadless(ctx);
5684
+ 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
+ const r = await api(ctx, "DELETE", `/crons/${mine.id}`);
5686
+ emit(ctx, r, () => console.log(t(`Deleted \xAB${mine.name}\xBB`, `Borrado \xAB${mine.name}\xBB`)));
5687
+ }
5534
5688
  }
5535
5689
  },
5536
5690
  defaultSub: "ls"
@@ -6664,13 +6818,30 @@ var HELP_ES = {
6664
6818
  "Path kept out of the release (repeatable)": "Ruta que no entra al release (repetible)",
6665
6819
  "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
6820
  "The hosting catalog: sizes and monthly prices in MXN": "El cat\xE1logo de hosting: tama\xF1os y precio mensual en MXN",
6821
+ // 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",
6823
+ "List your crons (or, with --platform, the platform's scheduled jobs)": "Enlista tus crons (o, con --platform, las tareas de la plataforma)",
6824
+ "Platform jobs (backups, purges\u2026; platform admins only)": "Tareas de la plataforma (respaldos, purgas\u2026; s\xF3lo admins)",
6825
+ "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
+ "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
+ "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
+ "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)",
6830
+ 'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30': 'Cron de 5 campos, p. ej. "30 8 * * *" = diario 8:30',
6831
+ "IANA time zone (default America/Mexico_City)": "Zona horaria IANA (default America/Mexico_City)",
6832
+ "Call this public https URL on each run": "Pegarle a esta URL https p\xFAblica en cada corrida",
6833
+ "HTTP method (default GET)": "M\xE9todo HTTP (default GET)",
6834
+ "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
+ "Header for --secret": "Header para --secret",
6837
+ "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
+ "What the agent is told on each run (with --agent)": "Lo que se le dice al agente en cada corrida (con --agent)",
6839
+ "Change one of your crons (only what you pass)": "Cambia un cron tuyo (s\xF3lo lo que pases)",
6840
+ "New name": "Nombre nuevo",
6841
+ "New schedule": "Horario nuevo",
6842
+ "New IANA time zone": "Zona horaria IANA nueva",
6843
+ "Delete one of your crons": "Borra un cron tuyo",
6667
6844
  // 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
6845
  "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
6846
  "List the names in your vault (values never come back)": "Enlista los nombres de tu b\xF3veda (los valores nunca regresan)",
6676
6847
  "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 +7155,7 @@ function normalizeArgs(input) {
6984
7155
  }
6985
7156
 
6986
7157
  // src/index.ts
6987
- var VERSION2 = true ? "0.17.1" : "dev";
7158
+ var VERSION2 = true ? "0.18.0" : "dev";
6988
7159
  var GLOBAL_OPTIONS = {
6989
7160
  json: { type: "boolean" },
6990
7161
  token: { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Official CLI for EasyBits, the cloud for AI agents — sandboxes, hosting, domains, databases, agents and files from your terminal",
5
5
  "type": "module",
6
6
  "bin": {