@easybits.cloud/cli 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/index.js +147 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,7 +131,10 @@ easybits crons create "Heads-up" --at 23:00 --url https://my-app.com/api/notice
|
|
|
131
131
|
easybits crons create "Heads-up" --in 2h --url https://my-app.com/api/notice --method POST # once; --until/--times end repeating ones
|
|
132
132
|
easybits crons create "Review" --at 23:00 --run "python3 asc.py submit" --file scripts/asc.py \
|
|
133
133
|
--secret-file ASC_P8:~/.appstoreconnect/private_keys/AuthKey_X.p8 # a command in a throwaway box; the .p8 only this cron reads
|
|
134
|
-
easybits crons
|
|
134
|
+
easybits crons create "Daily signups" --cron "0 9 * * *" --run "python3 count.py" --file count.py \
|
|
135
|
+
--machine my-app # runs with my-app's env and secrets (its DATABASE_URL), resolved each run
|
|
136
|
+
easybits crons get "Review" # what it does, how it notifies you, next run (--json: all of it)
|
|
137
|
+
easybits crons runs "Review" --logs --limit 5 # exit code + stdout/stderr
|
|
135
138
|
easybits crons wait "Review" --timeout 2h # blocks until it runs; exits with its exit code
|
|
136
139
|
easybits crons update "Review" --notify push,email --notify-on always # push to the Ghosty app / email to you
|
|
137
140
|
easybits crons ls
|
|
@@ -145,7 +148,9 @@ easybits crons rm "Study reminder" --yes
|
|
|
145
148
|
easybits inbox create builds # prints the URL (anyone with it can write)
|
|
146
149
|
easybits inbox ls
|
|
147
150
|
easybits inbox listen builds # one JSON line per event, live (Claude Code: run it with Monitor)
|
|
148
|
-
easybits inbox read builds # the unread ones, marked read
|
|
151
|
+
easybits inbox read builds # the unread ones, marked read (--since <seq>: from a cursor)
|
|
152
|
+
easybits inbox update builds --retention 30 # rename (--name) or keep events longer; the URL stays
|
|
153
|
+
easybits inbox rotate builds --yes # new URL; the old one stops working (if it leaked)
|
|
149
154
|
easybits inbox rm builds --yes
|
|
150
155
|
|
|
151
156
|
# Platform crons (platform admins only; key with the ADMIN scope)
|
|
@@ -164,6 +169,7 @@ easybits db query leads "SELECT * FROM leads" --json
|
|
|
164
169
|
easybits db rename leads prospects --dry-run # which agent prompts name it
|
|
165
170
|
easybits db rm leads --dry-run # tables and rows that would be lost
|
|
166
171
|
easybits db rm leads --yes
|
|
172
|
+
easybits db attach clave # the database your machine opens at /v1/db/clave/
|
|
167
173
|
|
|
168
174
|
# Agents
|
|
169
175
|
easybits agents ls
|
|
@@ -307,6 +313,14 @@ Host *.ghosty
|
|
|
307
313
|
|
|
308
314
|
Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
|
|
309
315
|
|
|
316
|
+
## Changes in 0.20.0
|
|
317
|
+
|
|
318
|
+
- `db attach <name> [--description <text>]`: adds to your account the database one of your machines
|
|
319
|
+
already uses (`DATABASE_URL=http://172.20.0.1:8090/v1/db/<name>/`). Nothing is created or copied;
|
|
320
|
+
from then on `db query|tables|ls` work on it. It does not count toward your plan's database limit,
|
|
321
|
+
and `db rm` only removes it from your list: the machine keeps its data. New API:
|
|
322
|
+
`POST /api/v2/databases { name, attach: true }`.
|
|
323
|
+
|
|
310
324
|
## Changes in 0.17.1
|
|
311
325
|
|
|
312
326
|
- `easybits crons ls|runs|run|pause|resume`: EasyBits' scheduled jobs, the same as the
|
package/dist/index.js
CHANGED
|
@@ -3797,6 +3797,22 @@ var db = {
|
|
|
3797
3797
|
emit(ctx, d, () => console.log(t(`Created ${d.name} (${d.id})`, `Creada ${d.name} (${d.id})`)));
|
|
3798
3798
|
}
|
|
3799
3799
|
},
|
|
3800
|
+
attach: {
|
|
3801
|
+
summary: "Add to your account the database one of your machines already uses",
|
|
3802
|
+
usage: "easybits db attach <name> [--description <text>]",
|
|
3803
|
+
options: { description: { type: "string", value: "text", description: "What it is for" } },
|
|
3804
|
+
examples: [
|
|
3805
|
+
"easybits db attach clave # the machine's DATABASE_URL ends in /v1/db/clave/",
|
|
3806
|
+
'easybits db query clave "SELECT count(*) FROM users" --json'
|
|
3807
|
+
],
|
|
3808
|
+
// No crea nada: el servidor deriva el namespace de tu cuenta + nombre y exige que exista.
|
|
3809
|
+
// Desde ahí sirven query/tables/export; `db rm` sólo la quita de la lista.
|
|
3810
|
+
async run(ctx) {
|
|
3811
|
+
const name = need(ctx, 0, "name", this.usage);
|
|
3812
|
+
const d = await api(ctx, "POST", "/databases", { name, attach: true, description: str(ctx, "description") });
|
|
3813
|
+
emit(ctx, d, () => console.log(t(`Attached ${d.name} (${d.id})`, `Adjunta ${d.name} (${d.id})`)));
|
|
3814
|
+
}
|
|
3815
|
+
},
|
|
3800
3816
|
rm: {
|
|
3801
3817
|
aliases: ["delete", "drop"],
|
|
3802
3818
|
summary: "Delete a database (irreversible)",
|
|
@@ -3826,7 +3842,13 @@ var db = {
|
|
|
3826
3842
|
const hit = await findDb(ctx, ref2);
|
|
3827
3843
|
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
3844
|
const r = await eb.deleteDatabase(hit.id);
|
|
3829
|
-
emit(
|
|
3845
|
+
emit(
|
|
3846
|
+
ctx,
|
|
3847
|
+
{ ...r, id: hit.id, name: hit.name },
|
|
3848
|
+
() => console.log(
|
|
3849
|
+
r.detached ? t(`Removed ${hit.name} from your list; its machine keeps the data`, `${hit.name} sali\xF3 de tu lista; su m\xE1quina conserva los datos`) : t(`Deleted ${hit.name} (${hit.id})`, `Borrada ${hit.name} (${hit.id})`)
|
|
3850
|
+
)
|
|
3851
|
+
);
|
|
3830
3852
|
}
|
|
3831
3853
|
},
|
|
3832
3854
|
rename: {
|
|
@@ -4918,7 +4940,7 @@ async function updateNotice(current2) {
|
|
|
4918
4940
|
}
|
|
4919
4941
|
|
|
4920
4942
|
// src/commands/doctor.ts
|
|
4921
|
-
var VERSION = true ? "0.
|
|
4943
|
+
var VERSION = true ? "0.20.0" : "dev";
|
|
4922
4944
|
var doctor2 = {
|
|
4923
4945
|
name: "doctor",
|
|
4924
4946
|
group: "Account",
|
|
@@ -5438,6 +5460,19 @@ function describeCronAction(a) {
|
|
|
5438
5460
|
}
|
|
5439
5461
|
return `agent ${a.fleetAgentId}: ${a.prompt.length > 40 ? a.prompt.slice(0, 39) + "\u2026" : a.prompt}`;
|
|
5440
5462
|
}
|
|
5463
|
+
function describeCron(c) {
|
|
5464
|
+
const n = c.notify;
|
|
5465
|
+
const chans = n ? [n.push ? "push" : "", n.email ? "email" : "", n.inbox ? `inbox:${n.inbox}` : ""].filter(Boolean).join(", ") : "";
|
|
5466
|
+
const state = c.enabled ? t("active", "activo") : c.done ? t("done", "terminado") : t("paused", "en pausa");
|
|
5467
|
+
return [
|
|
5468
|
+
`${c.id} \xAB${c.name}\xBB (${state})`,
|
|
5469
|
+
`${t("Schedule", "Horario")}: ${c.schedule} (${c.timezone})${c.until ? ` ${t("until", "hasta")} ${c.until}` : ""}${c.timesLeft != null ? ` \xB7 ${t("runs left", "ejecuciones restantes")}: ${c.timesLeft}` : ""}`,
|
|
5470
|
+
`${t("Next run", "Pr\xF3xima")}: ${c.enabled && c.nextRunLocal ? `${c.nextRunLocal} (${c.in})` : "\u2014"}`,
|
|
5471
|
+
`${t("Does", "Hace")}: ${describeCronAction(c.action)}`,
|
|
5472
|
+
`${t("Notifies", "Avisa")}: ${chans ? `${chans} (${n.on === "always" ? t("always", "siempre") : t("on failure", "si falla")})` : "\u2014"}`,
|
|
5473
|
+
`${t("Last run", "\xDAltima")}: ${c.lastRunAt ? `${fmtDate(c.lastRunAt)} ${c.lastOk === false ? t("failed", "fall\xF3") : "ok"}${c.lastError ? ` \u2014 ${c.lastError}` : ""}` : "\u2014"}`
|
|
5474
|
+
];
|
|
5475
|
+
}
|
|
5441
5476
|
async function resolveUserCron(ctx, ref2) {
|
|
5442
5477
|
const { items } = await api(ctx, "GET", "/crons");
|
|
5443
5478
|
const byId = items.find((c) => c.id === ref2);
|
|
@@ -5464,7 +5499,8 @@ var ACTION_OPTIONS = {
|
|
|
5464
5499
|
file: { type: "string", multiple: true, value: "local[=remote]", description: "File mounted for --run, next to the command by default (repeatable)" },
|
|
5465
5500
|
"secret-file": { type: "string", multiple: true, value: "NAME:local[=remote]", description: "Upload a local file as a secret only this cron can read, written at the same ~ path in the box (repeatable; e.g. a .p8)" },
|
|
5466
5501
|
template: { type: "string", value: "python|node|bun|ubuntu", description: "Box image for --run (default python)" },
|
|
5467
|
-
timeout: { type: "string", value: "seconds", description: "Time limit for --run (default 300, max 600)" }
|
|
5502
|
+
timeout: { type: "string", value: "seconds", description: "Time limit for --run (default 300, max 600)" },
|
|
5503
|
+
machine: { type: "string", value: "id|name", description: "Run --run with that machine's config (its env and secrets, e.g. DATABASE_URL), resolved on every run" }
|
|
5468
5504
|
};
|
|
5469
5505
|
function remotePathFor(local, home = homedir7()) {
|
|
5470
5506
|
const abs = resolve3(local.replace(/^~(?=\/)/, home));
|
|
@@ -5499,7 +5535,8 @@ function execFromFlags(f, read, home = homedir7()) {
|
|
|
5499
5535
|
...files3.length ? { files: files3 } : {},
|
|
5500
5536
|
...secrets2.length ? { secrets: secrets2 } : {},
|
|
5501
5537
|
...f.template ? { template: f.template } : {},
|
|
5502
|
-
...f.timeout ? { timeoutSeconds: Number(f.timeout) } : {}
|
|
5538
|
+
...f.timeout ? { timeoutSeconds: Number(f.timeout) } : {},
|
|
5539
|
+
...f.machine ? { machine: f.machine } : {}
|
|
5503
5540
|
},
|
|
5504
5541
|
...Object.keys(secretValues).length ? { secretValues } : {}
|
|
5505
5542
|
};
|
|
@@ -5579,7 +5616,7 @@ var whenOf = (ctx) => ({
|
|
|
5579
5616
|
times: str(ctx, "times"),
|
|
5580
5617
|
tz: str(ctx, "tz")
|
|
5581
5618
|
});
|
|
5582
|
-
var nextLine = (c) => c.nextRunLocal ? t(`next ${c.nextRunLocal} ${c.timezone} (in ${c.in})`, `pr\xF3xima ${c.nextRunLocal} ${c.timezone} (en ${c.in})`) : t("no next run", "sin pr\xF3xima
|
|
5619
|
+
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 ejecuci\xF3n");
|
|
5583
5620
|
var flagsOf = (ctx) => ({
|
|
5584
5621
|
url: str(ctx, "url"),
|
|
5585
5622
|
method: str(ctx, "method"),
|
|
@@ -5594,7 +5631,7 @@ function actionOf(ctx) {
|
|
|
5594
5631
|
if (run && (str(ctx, "url") || str(ctx, "agent"))) return { error: "--run, --url and --agent are exclusive" };
|
|
5595
5632
|
if (run) {
|
|
5596
5633
|
return execFromFlags(
|
|
5597
|
-
{ run, files: list(ctx, "file"), secrets: list(ctx, "secret"), secretFiles: list(ctx, "secret-file"), template: str(ctx, "template"), timeout: str(ctx, "timeout") },
|
|
5634
|
+
{ run, files: list(ctx, "file"), secrets: list(ctx, "secret"), secretFiles: list(ctx, "secret-file"), template: str(ctx, "template"), timeout: str(ctx, "timeout"), machine: str(ctx, "machine") },
|
|
5598
5635
|
(p) => {
|
|
5599
5636
|
try {
|
|
5600
5637
|
return readFileSync18(p, "utf8");
|
|
@@ -5612,10 +5649,10 @@ var STATUS = {
|
|
|
5612
5649
|
tarde: ["late", "tarde"],
|
|
5613
5650
|
fallo: ["failed", "fall\xF3"],
|
|
5614
5651
|
pausado: ["paused", "pausado"],
|
|
5615
|
-
nuevo: ["no runs yet", "sin
|
|
5652
|
+
nuevo: ["no runs yet", "sin ejecuciones"]
|
|
5616
5653
|
};
|
|
5617
5654
|
function cronStatus(c) {
|
|
5618
|
-
if (c.running) return t("running", "
|
|
5655
|
+
if (c.running) return t("running", "en ejecuci\xF3n");
|
|
5619
5656
|
const s = STATUS[c.status];
|
|
5620
5657
|
return s ? t(s[0], s[1]) : c.status;
|
|
5621
5658
|
}
|
|
@@ -5636,7 +5673,7 @@ var crons = {
|
|
|
5636
5673
|
aliases: ["cron"],
|
|
5637
5674
|
group: "Account",
|
|
5638
5675
|
summary: "Your scheduled jobs (call a URL, message an agent or run a command; repeating or once) and, for admins, the platform's",
|
|
5639
|
-
synopsis: "crons <ls|create|update|rm|run|runs|wait|pause|resume>",
|
|
5676
|
+
synopsis: "crons <ls|get|create|update|rm|run|runs|wait|pause|resume>",
|
|
5640
5677
|
subs: {
|
|
5641
5678
|
ls: {
|
|
5642
5679
|
aliases: ["list"],
|
|
@@ -5685,29 +5722,48 @@ var crons = {
|
|
|
5685
5722
|
);
|
|
5686
5723
|
}
|
|
5687
5724
|
},
|
|
5725
|
+
get: {
|
|
5726
|
+
aliases: ["show"],
|
|
5727
|
+
summary: "Show one of your crons: what it does, how it notifies you and when it runs (--json: the whole object)",
|
|
5728
|
+
usage: "easybits crons get <id|name> [--json]",
|
|
5729
|
+
examples: ['easybits crons get "Review 1.0.10"', "easybits crons get 66f0c2a1b2c3d4e5f6a7b8c9 --json"],
|
|
5730
|
+
async run(ctx) {
|
|
5731
|
+
const ref2 = need(ctx, 0, "id|name", this.usage);
|
|
5732
|
+
const c = await resolveUserCron(ctx, ref2);
|
|
5733
|
+
if (!c) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
|
|
5734
|
+
emit(ctx, c, () => {
|
|
5735
|
+
for (const line2 of describeCron(c)) console.log(line2);
|
|
5736
|
+
});
|
|
5737
|
+
}
|
|
5738
|
+
},
|
|
5688
5739
|
runs: {
|
|
5689
5740
|
summary: "Show the last runs of one of your crons (id or name) or of a platform job, with the full error",
|
|
5690
|
-
usage: "easybits crons runs <id|name|job> [--logs]",
|
|
5691
|
-
options: {
|
|
5741
|
+
usage: "easybits crons runs <id|name|job> [--limit N] [--logs]",
|
|
5742
|
+
options: {
|
|
5743
|
+
limit: { type: "string", value: "N", description: "At most N runs of one of your crons (1-50, default 50)" },
|
|
5744
|
+
logs: { type: "boolean", description: "Also print stdout/stderr of --run crons (last 64 KB per run)" }
|
|
5745
|
+
},
|
|
5692
5746
|
examples: ["easybits crons runs 66f0c2a1b2c3d4e5f6a7b8c9", 'easybits crons runs "Review 1.0.10" --logs', "easybits crons runs backup-machines --json"],
|
|
5693
5747
|
async run(ctx) {
|
|
5694
5748
|
const key = need(ctx, 0, "id|name|job", this.usage);
|
|
5695
5749
|
const mine = await userCronOrNull(ctx, key);
|
|
5696
5750
|
const withLogs = bool(ctx, "logs");
|
|
5697
|
-
const
|
|
5751
|
+
const limit = str(ctx, "limit") ?? "50";
|
|
5752
|
+
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);
|
|
5753
|
+
const path = mine ? `/crons/${mine.id}/runs?limit=${limit}${withLogs ? "&logs=1" : ""}` : `/admin/crons/${encodeURIComponent(key)}/runs`;
|
|
5698
5754
|
const { items } = await api(ctx, "GET", path);
|
|
5699
5755
|
emit(ctx, { items }, () => {
|
|
5700
5756
|
table(
|
|
5701
5757
|
items.map((r) => ({
|
|
5702
5758
|
at: fmtDate(r.startedAt),
|
|
5703
|
-
ok: r.ok == null ? t("running", "
|
|
5759
|
+
ok: r.ok == null ? t("running", "en ejecuci\xF3n") : r.ok ? "ok" : t("failed", "fall\xF3"),
|
|
5704
5760
|
status: r.exitCode != null ? `exit ${r.exitCode}` : r.status ?? "\u2014",
|
|
5705
5761
|
took: duration(r.durationMs),
|
|
5706
5762
|
trigger: r.trigger,
|
|
5707
5763
|
summary: (r.error ?? r.summary ?? "").slice(0, 80)
|
|
5708
5764
|
})),
|
|
5709
5765
|
[["at", "STARTED"], ["ok", "RESULT"], ["status", "HTTP"], ["took", "TOOK"], ["trigger", "TRIGGER"], ["summary", "SUMMARY"]],
|
|
5710
|
-
t("No runs yet.", "Todav\xEDa no hay
|
|
5766
|
+
t("No runs yet.", "Todav\xEDa no hay ejecuciones.")
|
|
5711
5767
|
);
|
|
5712
5768
|
if (withLogs) {
|
|
5713
5769
|
const last = items.find((r) => r.logs);
|
|
@@ -5754,7 +5810,7 @@ ${failed.error}`);
|
|
|
5754
5810
|
}
|
|
5755
5811
|
if (Date.now() >= deadline) {
|
|
5756
5812
|
throw new CliError(
|
|
5757
|
-
t(`Gave up waiting for \xAB${mine.name}\xBB${r.nextRunLocal ? ` (next run ${r.nextRunLocal})` : ""}.`, `Me cans\xE9 de esperar \xAB${mine.name}\xBB${r.nextRunLocal ? ` (pr\xF3xima
|
|
5813
|
+
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 ejecuci\xF3n ${r.nextRunLocal})` : ""}.`),
|
|
5758
5814
|
EXIT.API,
|
|
5759
5815
|
`easybits crons wait ${mine.id} --timeout 2h`,
|
|
5760
5816
|
"timeout"
|
|
@@ -5803,7 +5859,7 @@ ${failed.error}`);
|
|
|
5803
5859
|
create: {
|
|
5804
5860
|
aliases: ["add"],
|
|
5805
5861
|
summary: "Create a cron: call a public https URL, send a turn to one of your agents or run a command in a throwaway box, repeating (at most every 5 min) or once",
|
|
5806
|
-
usage: 'easybits crons create <name> (--cron "<5 fields>" [--until <when>] [--times N] | --at <when> | --in <duration>) [--tz <IANA>] (--url <https-url> [--method] [--body] [--secret NAME] | --agent <id> --prompt <text> | --run <cmd> [--file f]\u2026 [--secret NAME]\u2026 [--secret-file NAME:path]\u2026 [--template] [--timeout]) [--dry-run]',
|
|
5862
|
+
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] [--machine id|name]) [--dry-run]',
|
|
5807
5863
|
options: {
|
|
5808
5864
|
cron: { type: "string", value: "expr", description: 'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30' },
|
|
5809
5865
|
...WHEN_OPTIONS,
|
|
@@ -5835,7 +5891,7 @@ ${failed.error}`);
|
|
|
5835
5891
|
emit(ctx, p, () => {
|
|
5836
5892
|
console.log(t(`OK, nothing created. \xAB${name}\xBB \u2014 ${p.schedule}`, `V\xE1lido, no se cre\xF3 nada. \xAB${name}\xBB \u2014 ${p.schedule}`));
|
|
5837
5893
|
for (const r of p.nextRuns) console.log(` ${r.local} ${p.timezone} (${r.at})`);
|
|
5838
|
-
if (p.in) console.log(t(`First run in ${p.in}.`, `La primera
|
|
5894
|
+
if (p.in) console.log(t(`First run in ${p.in}.`, `La primera se ejecuta en ${p.in}.`));
|
|
5839
5895
|
});
|
|
5840
5896
|
return;
|
|
5841
5897
|
}
|
|
@@ -5846,7 +5902,7 @@ ${failed.error}`);
|
|
|
5846
5902
|
update: {
|
|
5847
5903
|
aliases: ["edit", "set"],
|
|
5848
5904
|
summary: "Change one of your crons (only what you pass)",
|
|
5849
|
-
usage: 'easybits crons update <id|name> [--name <n>] [--cron "<expr>" | --at <when> | --in <duration>] [--until <when>] [--times N] [--tz <IANA>] [--url \u2026 | --agent \u2026 --prompt \u2026]',
|
|
5905
|
+
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]',
|
|
5850
5906
|
options: {
|
|
5851
5907
|
name: { type: "string", value: "name", description: "New name" },
|
|
5852
5908
|
cron: { type: "string", value: "expr", description: "New schedule" },
|
|
@@ -5890,7 +5946,7 @@ ${failed.error}`);
|
|
|
5890
5946
|
const mine = await resolveUserCron(ctx, ref2);
|
|
5891
5947
|
if (!mine) throw new CliError(t(`You have no cron "${ref2}".`, `No tienes un cron "${ref2}".`), EXIT.API, "easybits crons ls", "not_found", 404);
|
|
5892
5948
|
requireYesIfHeadless(ctx);
|
|
5893
|
-
await confirm(ctx, t(`Delete cron \xAB${mine.name}\xBB? It stops running right away.`, `\xBFBorrar el cron \xAB${mine.name}\xBB? Deja de
|
|
5949
|
+
await confirm(ctx, t(`Delete cron \xAB${mine.name}\xBB? It stops running right away.`, `\xBFBorrar el cron \xAB${mine.name}\xBB? Deja de ejecutarse de inmediato.`));
|
|
5894
5950
|
const r = await api(ctx, "DELETE", `/crons/${mine.id}`);
|
|
5895
5951
|
emit(ctx, r, () => console.log(t(`Deleted \xAB${mine.name}\xBB`, `Borrado \xAB${mine.name}\xBB`)));
|
|
5896
5952
|
}
|
|
@@ -5987,7 +6043,7 @@ var inbox = {
|
|
|
5987
6043
|
aliases: ["inboxes"],
|
|
5988
6044
|
group: "Account",
|
|
5989
6045
|
summary: "Event inboxes: your own webhook URL whose events reach your terminal live or wait for you to read them",
|
|
5990
|
-
synopsis: "inbox <ls|create|listen|read|rm>",
|
|
6046
|
+
synopsis: "inbox <ls|create|listen|read|update|rotate|rm>",
|
|
5991
6047
|
subs: {
|
|
5992
6048
|
ls: {
|
|
5993
6049
|
aliases: ["list"],
|
|
@@ -6040,17 +6096,22 @@ var inbox = {
|
|
|
6040
6096
|
},
|
|
6041
6097
|
read: {
|
|
6042
6098
|
summary: "Show the events you haven't read yet and mark them read (the next read brings only new ones)",
|
|
6043
|
-
usage: "easybits inbox read <id|name> [--all] [--no-ack] [--limit N]",
|
|
6099
|
+
usage: "easybits inbox read <id|name> [--all | --since <seq>] [--no-ack] [--limit N]",
|
|
6044
6100
|
options: {
|
|
6045
6101
|
all: { type: "boolean", description: "Everything still kept, read or not" },
|
|
6102
|
+
since: { type: "string", value: "seq", description: "Events after this one, read or not (the cursor of a previous read)" },
|
|
6046
6103
|
"no-ack": { type: "boolean", description: "Just look; don't mark them read" },
|
|
6047
6104
|
limit: { type: "string", value: "N", description: "At most N events (default 50, max 200)" }
|
|
6048
6105
|
},
|
|
6049
|
-
examples: ["easybits inbox read builds", "easybits inbox read builds --all --no-ack --json"],
|
|
6106
|
+
examples: ["easybits inbox read builds", "easybits inbox read builds --all --no-ack --json", "easybits inbox read builds --since 12 --no-ack"],
|
|
6050
6107
|
async run(ctx) {
|
|
6051
6108
|
const name = need(ctx, 0, "id|name", this.usage);
|
|
6109
|
+
const since = str(ctx, "since");
|
|
6110
|
+
if (since != null && !/^\d+$/.test(since)) throw usageError(t("--since is an event number.", "--since es un n\xFAmero de evento."), this.usage);
|
|
6111
|
+
if (since != null && bool(ctx, "all")) throw usageError(t("--all and --since are exclusive.", "--all y --since no van juntos."), this.usage);
|
|
6052
6112
|
const q = new URLSearchParams();
|
|
6053
6113
|
if (bool(ctx, "all")) q.set("since", "0");
|
|
6114
|
+
if (since != null) q.set("since", since);
|
|
6054
6115
|
if (bool(ctx, "no-ack")) q.set("ack", "0");
|
|
6055
6116
|
if (str(ctx, "limit")) q.set("limit", str(ctx, "limit"));
|
|
6056
6117
|
const r = await api(ctx, "GET", `/inboxes/${ref(name)}/events${q.size ? `?${q}` : ""}`);
|
|
@@ -6063,6 +6124,41 @@ Quedan ${r.unread} sin leer: easybits inbox read ${name}`));
|
|
|
6063
6124
|
});
|
|
6064
6125
|
}
|
|
6065
6126
|
},
|
|
6127
|
+
update: {
|
|
6128
|
+
aliases: ["edit", "set"],
|
|
6129
|
+
summary: "Rename an inbox or change how long it keeps events (its URL stays the same)",
|
|
6130
|
+
usage: "easybits inbox update <id|name> [--name <new>] [--retention <days>]",
|
|
6131
|
+
options: {
|
|
6132
|
+
name: { type: "string", value: "new", description: "New name, unique in your account" },
|
|
6133
|
+
retention: { type: "string", value: "days", description: "Days events are kept (1-30)" }
|
|
6134
|
+
},
|
|
6135
|
+
examples: ["easybits inbox update builds --retention 30", "easybits inbox update builds --name ci --json"],
|
|
6136
|
+
async run(ctx) {
|
|
6137
|
+
const name = need(ctx, 0, "id|name", this.usage);
|
|
6138
|
+
const days = str(ctx, "retention");
|
|
6139
|
+
if (days != null && !/^\d+$/.test(days)) throw usageError(t("--retention is whole days.", "--retention son d\xEDas enteros."), this.usage);
|
|
6140
|
+
const patch = { ...str(ctx, "name") ? { name: str(ctx, "name") } : {}, ...days ? { retentionDays: Number(days) } : {} };
|
|
6141
|
+
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);
|
|
6142
|
+
const b = await api(ctx, "PATCH", `/inboxes/${ref(name)}`, patch);
|
|
6143
|
+
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.`)));
|
|
6144
|
+
}
|
|
6145
|
+
},
|
|
6146
|
+
rotate: {
|
|
6147
|
+
summary: "Give an inbox a new URL and print it; the old one stops accepting writes right away (stored events stay)",
|
|
6148
|
+
usage: "easybits inbox rotate <id|name> [--yes]",
|
|
6149
|
+
options: { ...YES_OPTION },
|
|
6150
|
+
examples: ["easybits inbox rotate builds --yes", "easybits inbox rotate builds --yes --json"],
|
|
6151
|
+
async run(ctx) {
|
|
6152
|
+
const name = need(ctx, 0, "id|name", this.usage);
|
|
6153
|
+
requireYesIfHeadless(ctx);
|
|
6154
|
+
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.`));
|
|
6155
|
+
const b = await api(ctx, "POST", `/inboxes/${ref(name)}/rotate`);
|
|
6156
|
+
emit(ctx, b, () => {
|
|
6157
|
+
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):`));
|
|
6158
|
+
console.log(` ${b.url}`);
|
|
6159
|
+
});
|
|
6160
|
+
}
|
|
6161
|
+
},
|
|
6066
6162
|
rm: {
|
|
6067
6163
|
aliases: ["remove", "delete"],
|
|
6068
6164
|
summary: "Delete an inbox and its events; its URL stops accepting writes",
|
|
@@ -6944,7 +7040,7 @@ var HELP_ES = {
|
|
|
6944
7040
|
"Compute": "C\xF3mputo",
|
|
6945
7041
|
"Firecracker microVMs (alias: sb)": "MicroVMs Firecracker (alias: sb)",
|
|
6946
7042
|
"List your sandboxes": "Enlista tus sandboxes",
|
|
6947
|
-
"Create a sandbox and wait until it is running": "Crea un sandbox y espera a que est\xE9
|
|
7043
|
+
"Create a sandbox and wait until it is running": "Crea un sandbox y espera a que est\xE9 en marcha",
|
|
6948
7044
|
"Base template (default ubuntu): ubuntu, python, node, code-interpreter\u2026": "Template base (default ubuntu): ubuntu, python, node, code-interpreter\u2026",
|
|
6949
7045
|
"Human label (lets you `ssh <name>.ghosty`)": "Nombre para personas (permite `ssh <nombre>.ghosty`)",
|
|
6950
7046
|
"Time to live before auto-destroy (was --timeout)": "Tiempo de vida antes de autodestruirse (antes --timeout)",
|
|
@@ -6953,7 +7049,7 @@ var HELP_ES = {
|
|
|
6953
7049
|
"KEY=VALUE lines (dotenv); - reads stdin. Use it for secrets": "Renglones KEY=VALUE (dotenv); - lee stdin. \xDAsalo para secretos",
|
|
6954
7050
|
"Return right away instead of waiting for running": "Regresa de inmediato en vez de esperar a que corra",
|
|
6955
7051
|
"Show one sandbox": "Muestra un sandbox",
|
|
6956
|
-
"Run a shell command inside a sandbox": "
|
|
7052
|
+
"Run a shell command inside a sandbox": "Ejecuta un comando de shell dentro de un sandbox",
|
|
6957
7053
|
"Working directory": "Directorio de trabajo",
|
|
6958
7054
|
"Kill the command after this long": "Mata el comando despu\xE9s de este tiempo",
|
|
6959
7055
|
"Read the sandbox journal (optionally one systemd unit)": "Lee el journal del sandbox (opcionalmente de una unit de systemd)",
|
|
@@ -6971,7 +7067,7 @@ var HELP_ES = {
|
|
|
6971
7067
|
"Skip the confirmation (required without a terminal or with --json)": "Salta la confirmaci\xF3n (obligatorio sin terminal o con --json)",
|
|
6972
7068
|
"Capture a copy-on-write snapshot without stopping the box": "Toma un snapshot copy-on-write sin detener la caja",
|
|
6973
7069
|
"Snapshot label": "Nombre del snapshot",
|
|
6974
|
-
"Agents running in their own sandbox": "Agentes que
|
|
7070
|
+
"Agents running in their own sandbox": "Agentes que viven en su propio sandbox",
|
|
6975
7071
|
"List your agents": "Enlista tus agentes",
|
|
6976
7072
|
"Show one agent: record, prompt, MCP servers, skills and files": "Muestra un agente: registro, prompt, servidores MCP, skills y archivos",
|
|
6977
7073
|
"Only these fields: agentId, name, template, status, lastError, sandboxId, agentUrl, embedToken, createdAt, expiresAt, systemPrompt, systemPromptMode, mcpServers, skills, files": "S\xF3lo estos campos: agentId, name, template, status, lastError, sandboxId, agentUrl, embedToken, createdAt, expiresAt, systemPrompt, systemPromptMode, mcpServers, skills, files",
|
|
@@ -7117,6 +7213,8 @@ var HELP_ES = {
|
|
|
7117
7213
|
"SQL databases (libSQL)": "Bases de datos SQL (libSQL)",
|
|
7118
7214
|
"List your databases": "Enlista tus bases de datos",
|
|
7119
7215
|
"Create a database": "Crea una base de datos",
|
|
7216
|
+
"Add to your account the database one of your machines already uses": "Agrega a tu cuenta la base que ya usa una de tus m\xE1quinas",
|
|
7217
|
+
"the machine's DATABASE_URL ends in /v1/db/clave/": "el DATABASE_URL de la m\xE1quina termina en /v1/db/clave/",
|
|
7120
7218
|
"What it is for": "Para qu\xE9 es",
|
|
7121
7219
|
"Delete a database (irreversible)": "Borra una base de datos (irreversible)",
|
|
7122
7220
|
"Show its tables and rows; delete nothing": "Ense\xF1a sus tablas y filas; no borra nada",
|
|
@@ -7128,7 +7226,7 @@ var HELP_ES = {
|
|
|
7128
7226
|
"asks you to type the name": "te pide teclear el nombre",
|
|
7129
7227
|
"scripts and agents": "scripts y agentes",
|
|
7130
7228
|
"List tables with row counts and columns, and how many links are permanent": "Enlista las tablas con sus filas y columnas, y cu\xE1ntas ligas son permanentes",
|
|
7131
|
-
"Run one SQL statement (by database id or name)": "
|
|
7229
|
+
"Run one SQL statement (by database id or name)": "Ejecuta una sentencia SQL (por id o nombre de la base)",
|
|
7132
7230
|
"Positional ? parameter (repeatable)": "Par\xE1metro ? posicional (repetible)",
|
|
7133
7231
|
"Storage": "Almacenamiento",
|
|
7134
7232
|
"Your files in EasyBits storage": "Tus archivos en el almacenamiento de EasyBits",
|
|
@@ -7211,24 +7309,25 @@ var HELP_ES = {
|
|
|
7211
7309
|
"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",
|
|
7212
7310
|
"The hosting catalog: sizes and monthly prices in MXN": "El cat\xE1logo de hosting: tama\xF1os y precio mensual en MXN",
|
|
7213
7311
|
// crons (tuyos y de la plataforma)
|
|
7214
|
-
"Your scheduled jobs (call a URL, message an agent or run a command; repeating or once) and, for admins, the platform's": "Tus tareas programadas (pegarle a una URL, escribirle a un agente o
|
|
7312
|
+
"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 ejecutar un comando; repetidas o de una sola vez) y, para admins, las de la plataforma",
|
|
7215
7313
|
"List your crons (or, with --platform, the platform's scheduled jobs)": "Enlista tus crons (o, con --platform, las tareas de la plataforma)",
|
|
7216
7314
|
"Platform jobs (backups, purges\u2026; platform admins only)": "Tareas de la plataforma (respaldos, purgas\u2026; s\xF3lo admins)",
|
|
7217
|
-
"Show the last runs of one of your crons (id or name) or of a platform job, with the full error": "Muestra las \xFAltimas
|
|
7218
|
-
"Run one of your crons or a platform job now, in the background (trigger manual)": "
|
|
7315
|
+
"Show the last runs of one of your crons (id or name) or of a platform job, with the full error": "Muestra las \xFAltimas ejecuciones de un cron tuyo (id o nombre) o de una tarea de la plataforma, con el error completo",
|
|
7316
|
+
"Run one of your crons or a platform job now, in the background (trigger manual)": "Ejecuta ya un cron tuyo o una tarea de la plataforma, en segundo plano (manual)",
|
|
7219
7317
|
"Pause one of your crons or a platform job until you resume it": "Pausa un cron tuyo o una tarea de la plataforma hasta que lo reanudes",
|
|
7220
7318
|
"Resume one of your crons or a platform job": "Reanuda un cron tuyo o una tarea de la plataforma",
|
|
7221
|
-
"Create a cron: call a public https URL, send a turn to one of your agents or run a command in a throwaway box, repeating (at most every 5 min) or once": "Crea un cron: pegarle a una URL https p\xFAblica, mandarle un turno a uno de tus agentes o
|
|
7319
|
+
"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 ejecutar un comando en una caja desechable, repetido (como mucho cada 5 min) o de una sola vez",
|
|
7222
7320
|
"Vault secret: a header with --url (default Authorization: Bearer); an env var with --run (repeatable)": "Secreto de la b\xF3veda: header con --url (default Authorization: Bearer); variable de entorno con --run (repetible)",
|
|
7223
|
-
"Run this shell command in a throwaway box instead (python, files and secrets included; success = exit 0)": "
|
|
7321
|
+
"Run this shell command in a throwaway box instead (python, files and secrets included; success = exit 0)": "Ejecutar este comando de shell en una caja desechable (con python, tus archivos y secretos; \xE9xito = exit 0)",
|
|
7224
7322
|
"File mounted for --run, next to the command by default (repeatable)": "Archivo que se monta para --run, junto al comando por default (repetible)",
|
|
7225
7323
|
"Upload a local file as a secret only this cron can read, written at the same ~ path in the box (repeatable; e.g. a .p8)": "Sube un archivo local como secreto que s\xF3lo este cron puede leer; se escribe en la misma ruta ~ dentro de la caja (repetible; p. ej. un .p8)",
|
|
7226
7324
|
"Box image for --run (default python)": "Imagen de la caja para --run (default python)",
|
|
7227
7325
|
"Time limit for --run (default 300, max 600)": "Tiempo l\xEDmite de --run (default 300, m\xE1ximo 600)",
|
|
7228
|
-
"
|
|
7229
|
-
"
|
|
7326
|
+
"Run --run with that machine's config (its env and secrets, e.g. DATABASE_URL), resolved on every run": "Corre --run con la configuraci\xF3n de esa m\xE1quina (su env y sus secretos, p. ej. DATABASE_URL), resuelta en cada ejecuci\xF3n",
|
|
7327
|
+
"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 ejecuci\xF3n)",
|
|
7328
|
+
"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 ejecuci\xF3n de un cron tuyo y sale con su resultado (exec: su c\xF3digo de salida)",
|
|
7230
7329
|
"Give up after this long (default 30m; 10m, 2h\u2026)": "Se rinde despu\xE9s de este tiempo (default 30m; 10m, 2h\u2026)",
|
|
7231
|
-
"Tell you after each run: push to the Ghosty app (vault secret GHOSTY_PUSH_TOKEN), email to your account and/or an event in one of your inboxes; none removes it": "Te avisa tras cada
|
|
7330
|
+
"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 ejecuci\xF3n: push a la app de Ghosty (secreto GHOSTY_PUSH_TOKEN de la b\xF3veda), correo a tu cuenta y/o un evento en uno de tus buzones; none lo quita",
|
|
7232
7331
|
// inbox (buzones de eventos)
|
|
7233
7332
|
"Event inboxes: your own webhook URL whose events reach your terminal live or wait for you to read them": "Buzones de eventos: tu propia URL de webhook; lo que llega te alcanza en la terminal en vivo o te espera para que lo leas",
|
|
7234
7333
|
"List your inboxes with their URL and unread events": "Enlista tus buzones con su URL y los eventos sin leer",
|
|
@@ -7241,20 +7340,27 @@ var HELP_ES = {
|
|
|
7241
7340
|
"Just look; don't mark them read": "S\xF3lo mirar; no marcarlos como le\xEDdos",
|
|
7242
7341
|
"At most N events (default 50, max 200)": "Como m\xE1ximo N eventos (default 50, tope 200)",
|
|
7243
7342
|
"Delete an inbox and its events; its URL stops accepting writes": "Borra un buz\xF3n y sus eventos; su URL deja de aceptar escrituras",
|
|
7343
|
+
"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)",
|
|
7344
|
+
"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)",
|
|
7345
|
+
"New name, unique in your account": "Nombre nuevo, \xFAnico en tu cuenta",
|
|
7346
|
+
"Days events are kept (1-30)": "D\xEDas que se guardan los eventos (1-30)",
|
|
7347
|
+
"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)",
|
|
7348
|
+
"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 se ejecuta (--json: el objeto completo)",
|
|
7349
|
+
"At most N runs of one of your crons (1-50, default 50)": "Como m\xE1ximo N ejecuciones de un cron tuyo (1-50, default 50)",
|
|
7244
7350
|
"When to notify (default failure)": "Cu\xE1ndo avisar (default failure: s\xF3lo si falla)",
|
|
7245
|
-
'Run ONCE at this time: ISO ("2026-10-18T23:00", read in --tz) or "23:00" (today)': '
|
|
7246
|
-
"Run ONCE after this long: 10m, 2h, 1h30m, 1d": "
|
|
7351
|
+
'Run ONCE at this time: ISO ("2026-10-18T23:00", read in --tz) or "23:00" (today)': 'Ejecutar UNA SOLA VEZ a esta hora: ISO ("2026-10-18T23:00", en la zona de --tz) o "23:00" (hoy)',
|
|
7352
|
+
"Run ONCE after this long: 10m, 2h, 1h30m, 1d": "Ejecutar UNA SOLA VEZ dentro de: 10m, 2h, 1h30m, 1d",
|
|
7247
7353
|
"A repeating cron stops after this date": "Un cron que se repite se detiene despu\xE9s de esta fecha",
|
|
7248
|
-
"A repeating cron stops after N more runs": "Un cron que se repite se detiene tras N
|
|
7249
|
-
"Validate and show the next 3 runs without creating anything": "Valida y muestra las pr\xF3ximas 3
|
|
7354
|
+
"A repeating cron stops after N more runs": "Un cron que se repite se detiene tras N ejecuciones m\xE1s",
|
|
7355
|
+
"Validate and show the next 3 runs without creating anything": "Valida y muestra las pr\xF3ximas 3 ejecuciones sin crear nada",
|
|
7250
7356
|
'Cron of 5 fields, e.g. "30 8 * * *" = daily 8:30': 'Cron de 5 campos, p. ej. "30 8 * * *" = diario 8:30',
|
|
7251
7357
|
"IANA time zone (default America/Mexico_City)": "Zona horaria IANA (default America/Mexico_City)",
|
|
7252
|
-
"Call this public https URL on each run": "Pegarle a esta URL https p\xFAblica en cada
|
|
7358
|
+
"Call this public https URL on each run": "Pegarle a esta URL https p\xFAblica en cada ejecuci\xF3n",
|
|
7253
7359
|
"HTTP method (default GET)": "M\xE9todo HTTP (default GET)",
|
|
7254
7360
|
"Request body (not with GET)": "Cuerpo de la petici\xF3n (no con GET)",
|
|
7255
7361
|
"Header for --secret": "Header para --secret",
|
|
7256
7362
|
"Send a turn to this fleet agent instead of calling a URL": "Mandarle un turno a este agente de la flota en vez de pegarle a una URL",
|
|
7257
|
-
"What the agent is told on each run (with --agent)": "Lo que se le dice al agente en cada
|
|
7363
|
+
"What the agent is told on each run (with --agent)": "Lo que se le dice al agente en cada ejecuci\xF3n (con --agent)",
|
|
7258
7364
|
"Change one of your crons (only what you pass)": "Cambia un cron tuyo (s\xF3lo lo que pases)",
|
|
7259
7365
|
"New name": "Nombre nuevo",
|
|
7260
7366
|
"New schedule": "Horario nuevo",
|
|
@@ -7574,7 +7680,7 @@ function normalizeArgs(input) {
|
|
|
7574
7680
|
}
|
|
7575
7681
|
|
|
7576
7682
|
// src/index.ts
|
|
7577
|
-
var VERSION2 = true ? "0.
|
|
7683
|
+
var VERSION2 = true ? "0.20.0" : "dev";
|
|
7578
7684
|
var GLOBAL_OPTIONS = {
|
|
7579
7685
|
json: { type: "boolean" },
|
|
7580
7686
|
token: { type: "string" },
|
package/package.json
CHANGED