@easybits.cloud/cli 0.15.5 → 0.17.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 +150 -13
  2. package/dist/index.js +2875 -266
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@
4
4
  import { parseArgs } from "util";
5
5
 
6
6
  // src/commands/account.ts
7
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
8
+ import { basename } from "path";
7
9
  import { resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
8
10
 
9
11
  // src/args.ts
@@ -277,10 +279,33 @@ var HEADERS_ES = {
277
279
  "URL / COMMAND": "URL / COMANDO",
278
280
  VERSION: "VERSI\xD3N",
279
281
  COLUMN: "COLUMNA",
282
+ EVENTS: "EVENTOS",
283
+ FAILS: "FALLAS",
284
+ SCOPES: "PERMISOS",
285
+ ROOT: "RA\xCDZ",
286
+ PREFIX: "PREFIJO",
280
287
  PERMANENT: "PERMANENTES",
281
288
  EXPIRING: "CADUCAN",
282
289
  EXTERNAL: "DE FUERA",
283
- EMPTY: "VAC\xCDAS"
290
+ EMPTY: "VAC\xCDAS",
291
+ VALUE: "VALOR",
292
+ DAY: "D\xCDA",
293
+ CONSISTENCY: "CONSISTENCIA",
294
+ RESERVED: "RESERVADA",
295
+ MEMORY: "MEMORIA",
296
+ DISK: "DISCO",
297
+ "LAST USED": "\xDALTIMO USO",
298
+ EXIT: "C\xD3DIGO",
299
+ STARTED: "INICI\xD3",
300
+ OUTPUT: "SALIDA",
301
+ COMMAND: "COMANDO",
302
+ DATE: "FECHA",
303
+ AUTHOR: "AUTOR",
304
+ PAGES: "P\xC1GINAS",
305
+ SCENES: "ESCENAS",
306
+ DURATION: "DURACI\xD3N",
307
+ NARRATION: "NARRACI\xD3N",
308
+ PROMPT: "PROMPT"
284
309
  };
285
310
  function table(rows, cols, empty = t("Nothing here yet.", "Todav\xEDa no hay nada.")) {
286
311
  if (lang() === "es") cols = cols.map(([k, l]) => [k, HEADERS_ES[l] ?? l]);
@@ -533,6 +558,113 @@ async function api(ctx, method, path, body, opts = {}) {
533
558
  }
534
559
  var encodePath = (p) => p.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
535
560
 
561
+ // src/prompt.ts
562
+ import { createInterface } from "readline/promises";
563
+ var YES_OPTION = {
564
+ yes: { type: "boolean", short: "y", description: "Skip the confirmation (required without a terminal or with --json)" }
565
+ };
566
+ function sameCommand(argv2 = process.argv.slice(2)) {
567
+ const quoted = argv2.map((a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`);
568
+ return `easybits ${quoted.join(" ")}`;
569
+ }
570
+ function applyCommand(argv2, opts) {
571
+ const rest = argv2.filter((a) => a !== "--dry-run");
572
+ const hasYes = rest.some((a) => a === "--yes" || a === "-y");
573
+ return `${sameCommand(rest)}${opts.needsYes && opts.headless && !hasYes ? " --yes" : ""}`;
574
+ }
575
+ function applyHint(ctx, opts = {}) {
576
+ const cmd = applyCommand(process.argv.slice(2), { headless: !interactive(ctx), needsYes: opts.needsYes });
577
+ return t(`To apply: ${cmd}`, `Para aplicarlo: ${cmd}`);
578
+ }
579
+ async function ask(question) {
580
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
581
+ try {
582
+ return (await rl.question(question)).trim();
583
+ } catch {
584
+ process.stderr.write("\n");
585
+ return "";
586
+ } finally {
587
+ rl.close();
588
+ }
589
+ }
590
+ function requireYesIfHeadless(ctx) {
591
+ if (bool(ctx, "yes") || interactive(ctx)) return;
592
+ throw new CliError(t("--yes required when not running interactively.", "Sin terminal hace falta --yes."), EXIT.USAGE, `${t("Run", "Corre")}: ${sameCommand()} --yes`, "usage");
593
+ }
594
+ async function confirm(ctx, action, opts = {}) {
595
+ if (bool(ctx, "yes")) return;
596
+ requireYesIfHeadless(ctx);
597
+ if (opts.typeName) {
598
+ process.stderr.write(`${action}
599
+ ${t("This cannot be undone.", "No se puede deshacer.")}
600
+ `);
601
+ const answer2 = await ask(t(`Type "${opts.typeName}" to confirm: `, `Teclea "${opts.typeName}" para confirmar: `));
602
+ if (answer2 !== opts.typeName) throw new CliError(t("Aborted: the name did not match.", "Cancelado: el nombre no coincide."), EXIT.USAGE, void 0, "aborted");
603
+ return;
604
+ }
605
+ const answer = await ask(`${action} ${t("[y/N]", "[s/N]")} `);
606
+ if (!/^(y(es)?|s[ií]?)$/i.test(answer)) throw new CliError(t("Aborted.", "Cancelado."), EXIT.USAGE, void 0, "aborted");
607
+ }
608
+
609
+ // src/sitefiles.ts
610
+ import { readdirSync, statSync } from "fs";
611
+ import { extname, join as join2, relative, sep } from "path";
612
+ var MIME = {
613
+ html: "text/html",
614
+ htm: "text/html",
615
+ css: "text/css",
616
+ js: "text/javascript",
617
+ mjs: "text/javascript",
618
+ json: "application/json",
619
+ map: "application/json",
620
+ webmanifest: "application/manifest+json",
621
+ xml: "application/xml",
622
+ txt: "text/plain",
623
+ md: "text/markdown",
624
+ csv: "text/csv",
625
+ svg: "image/svg+xml",
626
+ png: "image/png",
627
+ jpg: "image/jpeg",
628
+ jpeg: "image/jpeg",
629
+ gif: "image/gif",
630
+ webp: "image/webp",
631
+ avif: "image/avif",
632
+ ico: "image/x-icon",
633
+ woff: "font/woff",
634
+ woff2: "font/woff2",
635
+ ttf: "font/ttf",
636
+ otf: "font/otf",
637
+ mp4: "video/mp4",
638
+ webm: "video/webm",
639
+ mp3: "audio/mpeg",
640
+ wav: "audio/wav",
641
+ pdf: "application/pdf",
642
+ zip: "application/zip",
643
+ wasm: "application/wasm"
644
+ };
645
+ function mimeOf(path) {
646
+ return MIME[extname(path).slice(1).toLowerCase()] ?? "application/octet-stream";
647
+ }
648
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
649
+ function walkSite(dir) {
650
+ const out = [];
651
+ const visit = (d) => {
652
+ for (const e of readdirSync(d, { withFileTypes: true })) {
653
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
654
+ const full = join2(d, e.name);
655
+ if (e.isDirectory()) visit(full);
656
+ else if (e.isFile()) out.push({ path: relative(dir, full).split(sep).join("/"), local: full, size: statSync(full).size });
657
+ }
658
+ };
659
+ visit(dir);
660
+ return out.sort((a, b) => a.path.localeCompare(b.path));
661
+ }
662
+ function sitePath(p) {
663
+ const clean = p.replace(/\\/g, "/").replace(/^\/+/, "");
664
+ if (!clean || clean.split("/").some((s) => s === ".." || s === "")) return null;
665
+ return clean;
666
+ }
667
+
536
668
  // src/commands/account.ts
537
669
  var login = {
538
670
  name: "login",
@@ -625,8 +757,8 @@ Webhooks: ${u.counts.webhooks}`
625
757
  var websites = {
626
758
  name: "websites",
627
759
  group: "Storage",
628
- summary: "Static websites: list, create, change slug",
629
- synopsis: "websites <ls|create|slug>",
760
+ summary: "Static websites: list, create, upload, deploy a folder, delete",
761
+ synopsis: "websites",
630
762
  subs: {
631
763
  ls: {
632
764
  aliases: ["list"],
@@ -670,10 +802,136 @@ var websites = {
670
802
  const { website } = await api(ctx, "PATCH", `/websites/${websiteId}`, { slug });
671
803
  emit(ctx, website, () => console.log(t(`Slug changed \u2192 ${website.url}`, `Slug cambiado \u2192 ${website.url}`)));
672
804
  }
805
+ },
806
+ get: {
807
+ aliases: ["show"],
808
+ summary: "Show one website (by slug or id)",
809
+ usage: "easybits websites get <website>",
810
+ examples: ["easybits websites get mi-tienda", "easybits websites get mi-tienda --json"],
811
+ async run(ctx) {
812
+ const w = await api(ctx, "GET", `/websites/${await resolveWebsiteId(ctx, need(ctx, 0, "website", this.usage))}`);
813
+ emit(ctx, w, () => {
814
+ console.log(`ID: ${w.id}`);
815
+ console.log(`${t("Name: ", "Nombre: ")} ${w.name}`);
816
+ console.log(`Slug: ${w.slug}`);
817
+ console.log(`${t("Status: ", "Estado: ")} ${w.status}`);
818
+ console.log(`${t("Files: ", "Archivos: ")} ${w.fileCount ?? 0} (${fmtBytes(w.totalSize ?? 0)})`);
819
+ console.log(`URL: ${w.url}`);
820
+ console.log(`${t("Created: ", "Creado: ")} ${fmtDate(w.createdAt)}`);
821
+ });
822
+ }
823
+ },
824
+ files: {
825
+ summary: "List a website's files (paths inside the site)",
826
+ usage: "easybits websites files [ls] <website>",
827
+ examples: ["easybits websites files ls mi-tienda", "easybits websites files mi-tienda --json"],
828
+ async run(ctx) {
829
+ if (ctx.args[0] === "ls" || ctx.args[0] === "list") ctx.args.shift();
830
+ const id = await resolveWebsiteId(ctx, need(ctx, 0, "website", this.usage));
831
+ const w = await api(ctx, "GET", `/websites/${id}`);
832
+ const items = [];
833
+ for (let cursor; ; ) {
834
+ const page = await api(ctx, "GET", `/websites/${id}/files?limit=100${cursor ? `&cursor=${cursor}` : ""}`);
835
+ items.push(...page.items);
836
+ if (!page.hasMore || !page.nextCursor) break;
837
+ cursor = page.nextCursor;
838
+ }
839
+ const rows = items.map((f) => ({ ...f, path: w.prefix && f.name.startsWith(w.prefix) ? f.name.slice(w.prefix.length) : f.name })).sort((a, b) => a.path.localeCompare(b.path));
840
+ emit(
841
+ ctx,
842
+ rows,
843
+ () => table(
844
+ rows.map((f) => ({ ...f, size: fmtBytes(f.size), createdAt: fmtDate(f.createdAt) })),
845
+ [["path", "PATH"], ["size", "SIZE"], ["contentType", "TYPE"], ["createdAt", "CREATED"]],
846
+ t(`No files yet. Upload a folder: easybits websites deploy ${w.slug} ./dist`, `Todav\xEDa no hay archivos. Sube una carpeta: easybits websites deploy ${w.slug} ./dist`)
847
+ )
848
+ );
849
+ }
850
+ },
851
+ upload: {
852
+ summary: "Upload one file to a website (public; replaces the same path)",
853
+ usage: "easybits websites upload <website> <local-file> [--path <path-in-site>]",
854
+ options: { path: { type: "string", value: "path", description: "Path inside the site (default: the file name), e.g. img/hero.jpg" } },
855
+ examples: ["easybits websites upload mi-tienda ./index.html", "easybits websites upload mi-tienda ./hero.jpg --path img/hero.jpg"],
856
+ async run(ctx) {
857
+ const ref = need(ctx, 0, "website", this.usage);
858
+ const local = need(ctx, 1, "local-file", this.usage);
859
+ if (!existsSync2(local) || !statSync2(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), this.usage);
860
+ const path = sitePath(str(ctx, "path") ?? basename(local));
861
+ 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);
862
+ const size = statSync2(local).size;
863
+ if (size === 0) throw usageError(t(`${local} is empty.`, `${local} est\xE1 vac\xEDo.`), this.usage);
864
+ const id = await resolveWebsiteId(ctx, ref);
865
+ const r = await putSiteFile(ctx, id, path, local, size);
866
+ emit(ctx, r, () => console.log(t(`Uploaded ${path} (${fmtBytes(size)}) \u2192 ${r.url}`, `Subido ${path} (${fmtBytes(size)}) \u2192 ${r.url}`)));
867
+ }
868
+ },
869
+ deploy: {
870
+ summary: "Upload a whole folder (e.g. your build) keeping its paths; same paths are replaced",
871
+ usage: "easybits websites deploy <website> <dir> [--dry-run]",
872
+ options: { "dry-run": { type: "boolean", description: "List what would be uploaded; upload nothing" } },
873
+ examples: ["easybits websites deploy mi-tienda ./dist", "easybits websites deploy mi-tienda ./dist --dry-run"],
874
+ async run(ctx) {
875
+ const ref = need(ctx, 0, "website", this.usage);
876
+ const dir = need(ctx, 1, "dir", this.usage);
877
+ if (!existsSync2(dir) || !statSync2(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
878
+ const all = walkSite(dir);
879
+ const files3 = all.filter((f) => f.size > 0);
880
+ const empty = all.filter((f) => f.size === 0).map((f) => f.path);
881
+ 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);
882
+ 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."));
883
+ const id = await resolveWebsiteId(ctx, ref);
884
+ if (bool(ctx, "dry-run")) {
885
+ emit(ctx, { websiteId: id, files: files3.map(({ path, size }) => ({ path, size })), skippedEmpty: empty }, () => {
886
+ for (const f of files3) console.log(` ${f.path} ${fmtBytes(f.size)}`);
887
+ console.log(t(`${files3.length} files, ${fmtBytes(files3.reduce((s, f) => s + f.size, 0))}. Nothing uploaded (--dry-run).`, `${files3.length} archivos, ${fmtBytes(files3.reduce((s, f) => s + f.size, 0))}. No se subi\xF3 nada (--dry-run).`));
888
+ });
889
+ return;
890
+ }
891
+ const done = [];
892
+ let next = 0;
893
+ const worker = async () => {
894
+ while (next < files3.length) {
895
+ const f = files3[next++];
896
+ done.push(await putSiteFile(ctx, id, f.path, f.local, f.size));
897
+ if (!ctx.json) process.stderr.write(` ${f.path}
898
+ `);
899
+ }
900
+ };
901
+ await Promise.all(Array.from({ length: Math.min(4, files3.length) }, worker));
902
+ const w = await api(ctx, "GET", `/websites/${id}`);
903
+ emit(ctx, { websiteId: id, url: w.url, uploaded: done.sort((a, b) => a.path.localeCompare(b.path)), skippedEmpty: empty }, () => {
904
+ if (empty.length) console.error(t(`Skipped empty files: ${empty.join(", ")}`, `Se saltaron archivos vac\xEDos: ${empty.join(", ")}`));
905
+ console.log(t(`Deployed ${done.length} files \u2192 ${w.url}`, `Publicados ${done.length} archivos \u2192 ${w.url}`));
906
+ });
907
+ }
908
+ },
909
+ rm: {
910
+ aliases: ["delete", "remove"],
911
+ summary: "Delete a website and its files (asks you to type the slug)",
912
+ usage: "easybits websites rm <website> [--yes]",
913
+ options: { ...YES_OPTION },
914
+ examples: ["easybits websites rm mi-tienda", "easybits websites rm mi-tienda --yes"],
915
+ async run(ctx) {
916
+ const ref = need(ctx, 0, "website", this.usage);
917
+ requireYesIfHeadless(ctx);
918
+ const id = await resolveWebsiteId(ctx, ref);
919
+ const w = await api(ctx, "GET", `/websites/${id}`);
920
+ 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 });
921
+ const r = await api(ctx, "DELETE", `/websites/${id}`);
922
+ emit(ctx, { ...r, id, slug: w.slug }, () => console.log(t(`Deleted ${w.slug}`, `Borrado ${w.slug}`)));
923
+ }
673
924
  }
674
925
  },
675
926
  defaultSub: "ls"
676
927
  };
928
+ async function putSiteFile(ctx, websiteId, path, local, size) {
929
+ const contentType = mimeOf(path);
930
+ const r = await api(ctx, "POST", `/websites/${websiteId}/files`, { fileName: path, contentType, size });
931
+ const res = await fetch(r.putUrl, { method: "PUT", body: readFileSync3(local), headers: { "Content-Type": contentType } });
932
+ 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);
933
+ return { path, fileId: r.file.id, url: r.file.url, size, contentType };
934
+ }
677
935
  function matchWebsite(ref, items) {
678
936
  if (/^[0-9a-f]{24}$/i.test(ref)) return ref;
679
937
  const r = ref.trim().toLowerCase();
@@ -772,59 +1030,9 @@ var whoami = {
772
1030
  };
773
1031
 
774
1032
  // src/commands/files.ts
775
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
776
- import { basename, extname } from "path";
777
-
778
- // src/prompt.ts
779
- import { createInterface } from "readline/promises";
780
- var YES_OPTION = {
781
- yes: { type: "boolean", short: "y", description: "Skip the confirmation (required without a terminal or with --json)" }
782
- };
783
- function sameCommand(argv2 = process.argv.slice(2)) {
784
- const quoted = argv2.map((a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`);
785
- return `easybits ${quoted.join(" ")}`;
786
- }
787
- function applyCommand(argv2, opts) {
788
- const rest = argv2.filter((a) => a !== "--dry-run");
789
- const hasYes = rest.some((a) => a === "--yes" || a === "-y");
790
- return `${sameCommand(rest)}${opts.needsYes && opts.headless && !hasYes ? " --yes" : ""}`;
791
- }
792
- function applyHint(ctx, opts = {}) {
793
- const cmd = applyCommand(process.argv.slice(2), { headless: !interactive(ctx), needsYes: opts.needsYes });
794
- return t(`To apply: ${cmd}`, `Para aplicarlo: ${cmd}`);
795
- }
796
- async function ask(question) {
797
- const rl = createInterface({ input: process.stdin, output: process.stderr });
798
- try {
799
- return (await rl.question(question)).trim();
800
- } catch {
801
- process.stderr.write("\n");
802
- return "";
803
- } finally {
804
- rl.close();
805
- }
806
- }
807
- function requireYesIfHeadless(ctx) {
808
- if (bool(ctx, "yes") || interactive(ctx)) return;
809
- throw new CliError(t("--yes required when not running interactively.", "Sin terminal hace falta --yes."), EXIT.USAGE, `${t("Run", "Corre")}: ${sameCommand()} --yes`, "usage");
810
- }
811
- async function confirm(ctx, action, opts = {}) {
812
- if (bool(ctx, "yes")) return;
813
- requireYesIfHeadless(ctx);
814
- if (opts.typeName) {
815
- process.stderr.write(`${action}
816
- ${t("This cannot be undone.", "No se puede deshacer.")}
817
- `);
818
- const answer2 = await ask(t(`Type "${opts.typeName}" to confirm: `, `Teclea "${opts.typeName}" para confirmar: `));
819
- if (answer2 !== opts.typeName) throw new CliError(t("Aborted: the name did not match.", "Cancelado: el nombre no coincide."), EXIT.USAGE, void 0, "aborted");
820
- return;
821
- }
822
- const answer = await ask(`${action} ${t("[y/N]", "[s/N]")} `);
823
- if (!/^(y(es)?|s[ií]?)$/i.test(answer)) throw new CliError(t("Aborted.", "Cancelado."), EXIT.USAGE, void 0, "aborted");
824
- }
825
-
826
- // src/commands/files.ts
827
- var MIME = {
1033
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
1034
+ import { basename as basename2, extname as extname2 } from "path";
1035
+ var MIME2 = {
828
1036
  pdf: "application/pdf",
829
1037
  png: "image/png",
830
1038
  jpg: "image/jpeg",
@@ -877,15 +1085,15 @@ var files = {
877
1085
  examples: ["easybits files upload ./report.pdf", "easybits files upload ./logo.png --public --json"],
878
1086
  async run(ctx) {
879
1087
  const path = need(ctx, 0, "path", this.usage);
880
- if (!existsSync2(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), this.usage);
881
- const size = statSync(path).size;
882
- const contentType = MIME[extname(path).slice(1).toLowerCase()] || "application/octet-stream";
1088
+ if (!existsSync3(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), this.usage);
1089
+ const size = statSync3(path).size;
1090
+ const contentType = MIME2[extname2(path).slice(1).toLowerCase()] || "application/octet-stream";
883
1091
  const eb = await getClient(ctx);
884
1092
  const access = bool(ctx, "public") ? "public" : void 0;
885
- const data = await eb.uploadFile({ fileName: basename(path), contentType, size, access });
1093
+ const data = await eb.uploadFile({ fileName: basename2(path), contentType, size, access });
886
1094
  const res = await fetch(data.putUrl, {
887
1095
  method: "PUT",
888
- body: readFileSync3(path),
1096
+ body: readFileSync4(path),
889
1097
  headers: { "Content-Type": contentType }
890
1098
  });
891
1099
  if (!res.ok) throw new CliError(t(`Upload failed: storage answered ${res.status}`, `Fall\xF3 la subida: el almacenamiento contest\xF3 ${res.status}`), 1, void 0, "upload_failed", res.status);
@@ -915,7 +1123,7 @@ var files = {
915
1123
  };
916
1124
 
917
1125
  // src/commands/init.ts
918
- import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
1126
+ import { existsSync as existsSync4, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
919
1127
  var init = {
920
1128
  name: "init",
921
1129
  group: "Hosting",
@@ -923,16 +1131,16 @@ var init = {
923
1131
  synopsis: "init",
924
1132
  leaf: {
925
1133
  summary: "Write the GitHub Actions workflow that deploys this repo on every push to main",
926
- usage: "easybits init [--app-dir /srv/app] [--port 3000]",
1134
+ usage: "easybits init [--app-dir /app] [--port 3000]",
927
1135
  options: {
928
- "app-dir": { type: "string", value: "dir", description: "Where the app lives in the machine (default /srv/app)" },
1136
+ "app-dir": { type: "string", value: "dir", description: "Where the app lives in the machine (default /app)" },
929
1137
  port: { type: "string", value: "port", description: "Port your app listens on (default 3000)" }
930
1138
  },
931
- examples: ["easybits init", "easybits init --app-dir /srv/app --port 8080"],
1139
+ examples: ["easybits init", "easybits init --port 8080"],
932
1140
  async run(ctx) {
933
- const appDir = str(ctx, "app-dir") ?? "/srv/app";
1141
+ const appDir = str(ctx, "app-dir") ?? "/app";
934
1142
  const port = str(ctx, "port") ?? "3000";
935
- if (!existsSync3("package.json")) {
1143
+ if (!existsSync4("package.json")) {
936
1144
  throw usageError(t("No package.json here. Run this at the root of your app's repo.", "Aqu\xED no hay package.json. C\xF3rrelo en la ra\xEDz del repo de tu app."), this.usage);
937
1145
  }
938
1146
  const workflow = `# Despliegue a EasyBits en cada push a main.
@@ -1138,8 +1346,8 @@ Desde ah\xED, cada push a main despliega.`)));
1138
1346
 
1139
1347
  // src/commands/ssh.ts
1140
1348
  import { homedir as homedir2 } from "os";
1141
- import { join as join2 } from "path";
1142
- import { existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
1349
+ import { join as join3 } from "path";
1350
+ import { existsSync as existsSync5, readFileSync as readFileSync5, mkdirSync as mkdirSync2 } from "fs";
1143
1351
  import { execFileSync } from "child_process";
1144
1352
  var SSH_CONFIG_SNIPPET = `Host *.ghosty
1145
1353
  ProxyCommand easybits ssh-proxy %h
@@ -1154,17 +1362,17 @@ var sshKey = {
1154
1362
  usage: "easybits ssh-key",
1155
1363
  examples: ["easybits ssh-key"],
1156
1364
  async run(ctx) {
1157
- const dir = join2(homedir2(), ".ssh");
1158
- const key = join2(dir, "easybits_ed25519");
1365
+ const dir = join3(homedir2(), ".ssh");
1366
+ const key = join3(dir, "easybits_ed25519");
1159
1367
  const pub = `${key}.pub`;
1160
- if (!existsSync4(pub)) {
1368
+ if (!existsSync5(pub)) {
1161
1369
  mkdirSync2(dir, { recursive: true, mode: 448 });
1162
1370
  execFileSync("ssh-keygen", ["-t", "ed25519", "-N", "", "-f", key, "-C", "easybits"], {
1163
1371
  stdio: ["ignore", "ignore", "inherit"]
1164
1372
  });
1165
1373
  console.error(t(`easybits: key created at ${key}`, `easybits: llave creada en ${key}`));
1166
1374
  }
1167
- const publicKey = readFileSync4(pub, "utf8").trim();
1375
+ const publicKey = readFileSync5(pub, "utf8").trim();
1168
1376
  if (ctx.json) process.stdout.write(JSON.stringify({ publicKey, path: pub }, null, 2) + "\n");
1169
1377
  else process.stdout.write(publicKey + "\n");
1170
1378
  }
@@ -1216,7 +1424,234 @@ var sshProxy = {
1216
1424
  };
1217
1425
 
1218
1426
  // src/commands/sandboxes.ts
1219
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1427
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1428
+
1429
+ // src/commands/domains.ts
1430
+ import { EasybitsError as EasybitsError3 } from "@easybits.cloud/sdk";
1431
+ function matchDomain(ref, items) {
1432
+ if (/^[0-9a-f]{24}$/i.test(ref)) return ref;
1433
+ const r = ref.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
1434
+ return items.find((d) => d.domain === r)?.id ?? null;
1435
+ }
1436
+ async function listDomains(ctx) {
1437
+ return (await api(ctx, "GET", "/domains")).domains;
1438
+ }
1439
+ async function resolveDomain(ctx, ref) {
1440
+ if (/^[0-9a-f]{24}$/i.test(ref)) return { id: ref };
1441
+ const items = await listDomains(ctx);
1442
+ const id = matchDomain(ref, items);
1443
+ if (id) return { id, row: items.find((d) => d.id === id) };
1444
+ throw new CliError(
1445
+ t(`No domain "${ref}" in your account.`, `No hay dominio "${ref}" en tu cuenta.`),
1446
+ EXIT.API,
1447
+ `${t("List them with", "Enl\xEDstalos con")}: easybits domains ls`,
1448
+ "not_found",
1449
+ 404
1450
+ );
1451
+ }
1452
+ function dnsLines(records, apexNames) {
1453
+ return records.map((r) => {
1454
+ const opt = apexNames.has(r.name) ? t(" (only to serve a site at the root)", " (s\xF3lo para servir un sitio en la ra\xEDz)") : "";
1455
+ return ` ${r.type.padEnd(6)}${r.name.padEnd(40)}${r.values.join(" ")}${opt}`;
1456
+ });
1457
+ }
1458
+ function printDns(d) {
1459
+ if (!d.dns?.length) return;
1460
+ const apex = /* @__PURE__ */ new Set([d.domain, `www.${d.domain}`]);
1461
+ console.log(t("Create these DNS records at your DNS provider:", "Crea estos registros DNS en tu proveedor de DNS:"));
1462
+ for (const l of dnsLines(d.dns, apex)) console.log(l);
1463
+ }
1464
+ var domains = {
1465
+ name: "domains",
1466
+ aliases: ["domain"],
1467
+ group: "Hosting",
1468
+ summary: "Your own domains for your websites (sandbox ports: sandboxes domains)",
1469
+ synopsis: "domains",
1470
+ subs: {
1471
+ ls: {
1472
+ aliases: ["list"],
1473
+ summary: "List the custom domains of your account",
1474
+ usage: "easybits domains ls",
1475
+ examples: ["easybits domains ls", "easybits domains ls --json"],
1476
+ async run(ctx) {
1477
+ const items = await listDomains(ctx);
1478
+ emit(
1479
+ ctx,
1480
+ items,
1481
+ () => table(
1482
+ items.map((d) => ({
1483
+ ...d,
1484
+ status: d.verified ? t("verified", "verificado") : t("pending", "pendiente"),
1485
+ root: d.apexWebsite?.slug ?? "-",
1486
+ dnsBy: d.dnsProvider ?? t("manual", "a mano"),
1487
+ createdAt: fmtDate(d.createdAt)
1488
+ })),
1489
+ [["id", "ID"], ["domain", "DOMAIN"], ["status", "STATUS"], ["root", "ROOT"], ["dnsBy", "DNS"], ["createdAt", "CREATED"]],
1490
+ t("No custom domains. Add one: easybits domains add example.com", "Sin dominios propios. Agrega uno: easybits domains add midominio.com")
1491
+ )
1492
+ );
1493
+ }
1494
+ },
1495
+ add: {
1496
+ summary: "Register a domain; prints the DNS records to create",
1497
+ usage: "easybits domains add <domain>",
1498
+ examples: ["easybits domains add example.com", "easybits domains add example.com --json | jq .dns"],
1499
+ async run(ctx) {
1500
+ const name = need(ctx, 0, "domain", this.usage);
1501
+ const { domain } = await api(ctx, "POST", "/domains", { domain: name });
1502
+ emit(ctx, domain, () => {
1503
+ console.log(t(`Added ${domain.domain} (${domain.id})`, `Agregado ${domain.domain} (${domain.id})`));
1504
+ printDns(domain);
1505
+ console.log(
1506
+ t(
1507
+ `Then run: easybits domains verify ${domain.domain} (DNS in Mailmask? easybits domains dns-auto ${domain.domain})`,
1508
+ `Luego corre: easybits domains verify ${domain.domain} (\xBFDNS en Mailmask? easybits domains dns-auto ${domain.domain})`
1509
+ )
1510
+ );
1511
+ });
1512
+ }
1513
+ },
1514
+ verify: {
1515
+ summary: "Check the TXT record and issue the certificate",
1516
+ usage: "easybits domains verify <domain>",
1517
+ examples: ["easybits domains verify example.com"],
1518
+ async run(ctx) {
1519
+ const { id } = await resolveDomain(ctx, need(ctx, 0, "domain", this.usage));
1520
+ const { domain } = await api(ctx, "POST", `/domains/${id}/verify`);
1521
+ emit(ctx, domain, () => console.log(t(`${domain.domain}: verified. Your sites answer at <slug>.${domain.domain}`, `${domain.domain}: verificado. Tus sitios responden en <slug>.${domain.domain}`)));
1522
+ }
1523
+ },
1524
+ "dns-auto": {
1525
+ summary: "Write the DNS records into your Mailmask zone and verify (--undo restores)",
1526
+ usage: "easybits domains dns-auto <domain> [--undo]",
1527
+ options: { undo: { type: "boolean", description: "Restore the records EasyBits wrote (keeps the ones you changed)" } },
1528
+ examples: ["easybits domains dns-auto example.com", "easybits domains dns-auto example.com --undo"],
1529
+ async run(ctx) {
1530
+ const { id } = await resolveDomain(ctx, need(ctx, 0, "domain", this.usage));
1531
+ const undo = bool(ctx, "undo");
1532
+ try {
1533
+ const r = await api(ctx, undo ? "DELETE" : "POST", `/domains/${id}/dns-auto`);
1534
+ emit(ctx, r, () => {
1535
+ if (undo) {
1536
+ console.log(r.nothing ? t("Nothing to undo.", "Nada que deshacer.") : t("Records restored.", "Registros restaurados."));
1537
+ if (r.skipped?.length) console.log(t(`Left alone (you changed them): ${r.skipped.join(", ")}`, `Sin tocar (los cambiaste t\xFA): ${r.skipped.join(", ")}`));
1538
+ } else {
1539
+ console.log(t(`Wrote ${r.records?.join(", ")} in zone ${r.zone}`, `Escrib\xED ${r.records?.join(", ")} en la zona ${r.zone}`));
1540
+ console.log(r.verified ? t("Verified.", "Verificado.") : t("Not verified yet (DNS still propagating): run easybits domains verify in a minute.", "Todav\xEDa no verifica (el DNS sigue propagando): corre easybits domains verify en un minuto."));
1541
+ }
1542
+ });
1543
+ } catch (e) {
1544
+ const body = e instanceof EasybitsError3 && e.status === 409 ? String(e.body ?? "") : "";
1545
+ const m = /"connectUrl"\s*:\s*"([^"]+)"/.exec(body);
1546
+ if (m) {
1547
+ throw new CliError(
1548
+ t("Mailmask is not connected to your account.", "Mailmask no est\xE1 conectado a tu cuenta."),
1549
+ EXIT.API,
1550
+ t(`Open this once in your browser to connect it, then run the command again:
1551
+ ${m[1]}`, `Abre esto una vez en tu navegador para conectarlo y vuelve a correr el comando:
1552
+ ${m[1]}`),
1553
+ "mailmask_not_connected",
1554
+ 409
1555
+ );
1556
+ }
1557
+ throw e;
1558
+ }
1559
+ }
1560
+ },
1561
+ apex: {
1562
+ aliases: ["assign"],
1563
+ summary: "Serve one of your websites at the root domain and www (--clear removes it)",
1564
+ usage: "easybits domains apex <domain> <website|--clear>",
1565
+ options: { clear: { type: "boolean", description: "Stop serving a website at the root" } },
1566
+ examples: ["easybits domains apex example.com mi-tienda", "easybits domains apex example.com --clear"],
1567
+ async run(ctx) {
1568
+ const ref = need(ctx, 0, "domain", this.usage);
1569
+ const clear = bool(ctx, "clear");
1570
+ if (!clear && !ctx.args[1]) throw usageError(t("Missing <website> (or pass --clear).", "Falta <website> (o pasa --clear)."), this.usage);
1571
+ if (clear && ctx.args[1]) throw usageError(t("Pass a <website> or --clear, not both.", "Pasa un <website> o --clear, no ambos."), this.usage);
1572
+ const { id } = await resolveDomain(ctx, ref);
1573
+ const websiteId = clear ? null : await resolveWebsiteId(ctx, ctx.args[1]);
1574
+ const { domain } = await api(ctx, "POST", `/domains/${id}/apex`, { websiteId });
1575
+ emit(
1576
+ ctx,
1577
+ domain,
1578
+ () => console.log(
1579
+ domain.apexWebsite ? t(`${domain.domain} and www.${domain.domain} \u2192 ${domain.apexWebsite.slug}`, `${domain.domain} y www.${domain.domain} \u2192 ${domain.apexWebsite.slug}`) : t(`${domain.domain}: no website at the root.`, `${domain.domain}: sin sitio en la ra\xEDz.`)
1580
+ )
1581
+ );
1582
+ }
1583
+ },
1584
+ rm: {
1585
+ aliases: ["remove", "delete"],
1586
+ summary: "Remove a domain: unlinks its websites and deletes its certificates",
1587
+ usage: "easybits domains rm <domain> [--yes]",
1588
+ options: { ...YES_OPTION },
1589
+ examples: ["easybits domains rm example.com", "easybits domains rm example.com --yes"],
1590
+ async run(ctx) {
1591
+ const ref = need(ctx, 0, "domain", this.usage);
1592
+ requireYesIfHeadless(ctx);
1593
+ const { id, row } = await resolveDomain(ctx, ref);
1594
+ const name = row?.domain ?? ref;
1595
+ 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.`));
1596
+ const r = await api(ctx, "DELETE", "/domains", { domainId: id });
1597
+ emit(ctx, { ...r, domainId: id, domain: name }, () => console.log(t(`Removed ${name}`, `Quitado ${name}`)));
1598
+ }
1599
+ }
1600
+ }
1601
+ };
1602
+ var SANDBOX_DOMAINS_USAGE = "easybits sandboxes domains <ls|add|verify|rm> <sandbox> [domain] [--port <port>] [--yes]";
1603
+ var sandboxDomains = {
1604
+ summary: "Custom domains with auto TLS on a sandbox or machine port",
1605
+ usage: SANDBOX_DOMAINS_USAGE,
1606
+ options: {
1607
+ port: { type: "string", value: "port", description: "add: port inside the sandbox (required)" },
1608
+ ...YES_OPTION
1609
+ },
1610
+ examples: [
1611
+ "easybits sandboxes domains ls sb_abc123",
1612
+ "easybits sb domains add sb_abc123 shop.example.com --port 3000",
1613
+ "easybits sb domains verify sb_abc123 shop.example.com",
1614
+ "easybits sb domains rm sb_abc123 shop.example.com --yes"
1615
+ ],
1616
+ async run(ctx) {
1617
+ const action = need(ctx, 0, "ls|add|verify|rm", SANDBOX_DOMAINS_USAGE);
1618
+ const id = need(ctx, 1, "sandbox", SANDBOX_DOMAINS_USAGE);
1619
+ if (action === "ls" || action === "list") {
1620
+ const sb = await (await getClient(ctx)).sandboxes.get(id);
1621
+ const items = await sb.listDomains();
1622
+ emit(ctx, items, () => table(items, [["domain", "DOMAIN"], ["port", "PORT"]], t("No custom domains.", "Sin dominios propios.")));
1623
+ return;
1624
+ }
1625
+ const domain = need(ctx, 2, "domain", SANDBOX_DOMAINS_USAGE);
1626
+ if (action === "add") {
1627
+ const port = int(ctx, "port", SANDBOX_DOMAINS_USAGE);
1628
+ if (port == null) throw usageError(t("Missing --port.", "Falta --port."), SANDBOX_DOMAINS_USAGE);
1629
+ const r = await (await (await getClient(ctx)).sandboxes.get(id)).addDomain(domain, port);
1630
+ emit(ctx, r, () => {
1631
+ console.log(t(`Attached ${r.domain} \u2192 port ${r.port}`, `Ligado ${r.domain} \u2192 puerto ${r.port}`));
1632
+ console.log(t(`Create this DNS record: ${JSON.stringify(r.dns)}`, `Crea este registro DNS: ${JSON.stringify(r.dns)}`));
1633
+ console.log(t(`Then run: easybits sandboxes domains verify ${id} ${r.domain}`, `Luego corre: easybits sandboxes domains verify ${id} ${r.domain}`));
1634
+ });
1635
+ } else if (action === "verify") {
1636
+ const r = await (await (await getClient(ctx)).sandboxes.get(id)).verifyDomain(domain);
1637
+ emit(ctx, r, () => {
1638
+ console.log(`${r.domain}: ${r.ready ? t("ready", "listo") : t("not ready yet", "todav\xEDa no est\xE1 listo")}`);
1639
+ console.log(t(`DNS resolved: ${r.dns.resolved} HTTPS ok: ${r.https.ok}`, `DNS resuelve: ${r.dns.resolved} HTTPS ok: ${r.https.ok}`));
1640
+ if (r.hint) console.log(r.hint);
1641
+ });
1642
+ if (!r.ready) process.exitCode = 1;
1643
+ } else if (action === "rm" || action === "remove" || action === "delete") {
1644
+ requireYesIfHeadless(ctx);
1645
+ await confirm(ctx, t(`Detach ${domain} from ${id}? It stops serving right away.`, `\xBFDesligar ${domain} de ${id}? Deja de servir de inmediato.`));
1646
+ const r = await (await (await getClient(ctx)).sandboxes.get(id)).removeDomain(domain);
1647
+ emit(ctx, r, () => console.log(t(`Removed ${domain}`, `Quitado ${domain}`)));
1648
+ } else {
1649
+ throw usageError(t(`Unknown domains action "${action}".`, `Acci\xF3n de domains desconocida "${action}".`), SANDBOX_DOMAINS_USAGE);
1650
+ }
1651
+ }
1652
+ };
1653
+
1654
+ // src/commands/sandboxes.ts
1220
1655
  function sandboxRecord(s) {
1221
1656
  return {
1222
1657
  sandboxId: s.sandboxId,
@@ -1258,7 +1693,60 @@ function checkNotTruncated(size, path) {
1258
1693
  "truncated"
1259
1694
  );
1260
1695
  }
1261
- var FILES_USAGE = "easybits sandboxes files <ls|read|write> <sandbox> <path> [local-file]";
1696
+ var BG_USAGE = "easybits sandboxes bg <start|ls|status|kill> <sandbox> [exec-id] [--cwd <dir>] [--dotenv <path>] [--grace <s>] [-- <command...>]";
1697
+ var GIT_OPS = ["clone", "status", "commit", "push", "pull", "checkout", "log"];
1698
+ var GIT_USAGE = `easybits sandboxes git <${GIT_OPS.join("|")}> <sandbox> --dir <dir> [flags]`;
1699
+ function gitParams(op, f) {
1700
+ if (!GIT_OPS.includes(op)) throw usageError(t(`Unknown git operation "${op}".`, `Operaci\xF3n de git desconocida "${op}".`), GIT_USAGE);
1701
+ if (!f.dir) throw usageError(t("Missing --dir (the repo folder inside the sandbox).", "Falta --dir (la carpeta del repo dentro del sandbox)."), GIT_USAGE);
1702
+ if (f.token && !f.token.startsWith("$secret:")) {
1703
+ process.stderr.write(t("Warning: a literal token in argv shows up in ps and shell history; prefer --git-token '$secret:NAME'.\n", "Aviso: un token literal en argv se ve en ps y el historial; mejor --git-token '$secret:NOMBRE'.\n"));
1704
+ }
1705
+ const clean = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== false && !(Array.isArray(v) && !v.length)));
1706
+ const auth = { token: f.token, username: f.username };
1707
+ switch (op) {
1708
+ case "clone":
1709
+ if (!f.repo) throw usageError(t("clone needs --repo <url>.", "clone necesita --repo <url>."), GIT_USAGE);
1710
+ return clean({ repo: f.repo, dir: f.dir, branch: f.branch, depth: f.depth, commit: f.sha, ...auth });
1711
+ case "status":
1712
+ return { dir: f.dir };
1713
+ case "commit":
1714
+ if (!f.message) throw usageError(t("commit needs -m <message>.", "commit necesita -m <mensaje>."), GIT_USAGE);
1715
+ return clean({ dir: f.dir, message: f.message, addAll: f.all, paths: f.paths, authorName: f.authorName, authorEmail: f.authorEmail });
1716
+ case "push":
1717
+ return clean({ dir: f.dir, remote: f.remote, branch: f.branch, setUpstream: f.setUpstream, force: f.force, ...auth });
1718
+ case "pull":
1719
+ return clean({ dir: f.dir, rebase: f.rebase, ...auth });
1720
+ case "checkout":
1721
+ if (!f.branch) throw usageError(t("checkout needs --branch <name>.", "checkout necesita --branch <nombre>."), GIT_USAGE);
1722
+ return clean({ dir: f.dir, branch: f.branch, create: f.create, from: f.from });
1723
+ default:
1724
+ return clean({ dir: f.dir, limit: f.limit, path: f.paths?.[0] });
1725
+ }
1726
+ }
1727
+ function printGit(op, r) {
1728
+ if (op === "clone") console.log(t(`Cloned ${r.remote} \u2192 ${r.dir} (${r.detached ? "detached" : r.branch} @ ${String(r.head).slice(0, 7)})`, `Clonado ${r.remote} \u2192 ${r.dir} (${r.detached ? "desprendido" : r.branch} @ ${String(r.head).slice(0, 7)})`));
1729
+ else if (op === "status") {
1730
+ console.log(`${r.branch}${r.upstream ? ` \u2192 ${r.upstream}` : ""}${r.ahead || r.behind ? ` +${r.ahead} -${r.behind}` : ""}${r.clean ? t(" (clean)", " (limpio)") : ""}`);
1731
+ for (const [k, label] of [["staged", "staged"], ["modified", "modified"], ["untracked", "untracked"], ["conflicted", "conflicted"]]) {
1732
+ if (r[k]?.length) console.log(` ${label}: ${r[k].join(", ")}`);
1733
+ }
1734
+ } else if (op === "commit") console.log(r.nothingToCommit ? t("Nothing to commit.", "Nada que commitear.") : `${t("Committed", "Commit")} ${String(r.sha).slice(0, 7)}`);
1735
+ else if (op === "push") console.log(r.pushed ? t(`Pushed ${r.branch} to ${r.remote}`, `Subido ${r.branch} a ${r.remote}`) : r.output);
1736
+ else if (op === "pull") {
1737
+ console.log(r.updated ? t(`Updated to ${String(r.head).slice(0, 7)}`, `Actualizado a ${String(r.head).slice(0, 7)}`) : t("Already up to date.", "Ya estaba al d\xEDa."));
1738
+ if (r.conflicts?.length) console.log(t(`Conflicts: ${r.conflicts.join(", ")}`, `Conflictos: ${r.conflicts.join(", ")}`));
1739
+ } else if (op === "checkout") console.log(t(`On ${r.branch}${r.created ? " (new)" : ""}`, `En ${r.branch}${r.created ? " (nueva)" : ""}`));
1740
+ else
1741
+ table(
1742
+ r.items.map((e) => ({ ...e, sha: String(e.sha).slice(0, 7), date: fmtDate(e.date) })),
1743
+ [["sha", "SHA"], ["date", "DATE"], ["author", "AUTHOR"], ["message", "MESSAGE"]]
1744
+ );
1745
+ }
1746
+ async function sandboxAt(ctx, i, usage2) {
1747
+ return (await getClient(ctx)).sandboxes.get(need(ctx, i, "sandbox", usage2));
1748
+ }
1749
+ var FILES_USAGE = "easybits sandboxes files <ls|read|write|rm|mv|mkdir> <sandbox> <path> [local-file|new-path]";
1262
1750
  var sandboxes = {
1263
1751
  name: "sandboxes",
1264
1752
  aliases: ["sandbox", "sb"],
@@ -1389,21 +1877,27 @@ var sandboxes = {
1389
1877
  }
1390
1878
  },
1391
1879
  files: {
1392
- summary: "List, read or write files inside a sandbox",
1880
+ summary: "List, read, write, delete, move files and make folders inside a sandbox",
1393
1881
  usage: FILES_USAGE,
1394
1882
  options: {
1395
1883
  content: { type: "string", value: "text", description: "write: inline content instead of a local file" },
1396
- out: { type: "string", value: "file", description: "read: save to a local file (binary-safe)" }
1884
+ out: { type: "string", value: "file", description: "read: save to a local file (binary-safe)" },
1885
+ recursive: { type: "boolean", short: "r", description: "rm: a folder and everything in it" },
1886
+ ...YES_OPTION
1397
1887
  },
1398
1888
  examples: [
1399
1889
  "easybits sandboxes files ls sb_abc123 /data/work",
1400
1890
  "easybits sb files read sb_abc123 /etc/os-release",
1401
1891
  "easybits sb files read sb_abc123 /data/out.png --out out.png",
1402
1892
  "easybits sb files write sb_abc123 /data/work/app.js ./app.js",
1403
- "echo hello | easybits sb files write sb_abc123 /tmp/hello.txt"
1893
+ "echo hello | easybits sb files write sb_abc123 /tmp/hello.txt",
1894
+ "easybits sb files mkdir sb_abc123 /data/work/out",
1895
+ "easybits sb files mv sb_abc123 /data/work/a.txt /data/work/out/a.txt",
1896
+ "easybits sb files rm sb_abc123 /data/work/tmp -r --yes"
1404
1897
  ],
1405
1898
  async run(ctx) {
1406
- const action = need(ctx, 0, "ls|read|write", FILES_USAGE);
1899
+ const action = need(ctx, 0, "ls|read|write|rm|mv|mkdir", FILES_USAGE);
1900
+ if (action === "rm" || action === "delete") requireYesIfHeadless(ctx);
1407
1901
  const id = need(ctx, 1, "sandbox", FILES_USAGE);
1408
1902
  const path = need(ctx, 2, "path", FILES_USAGE);
1409
1903
  const eb = await getClient(ctx);
@@ -1437,17 +1931,30 @@ var sandboxes = {
1437
1931
  let buf;
1438
1932
  if (inline != null) buf = Buffer.from(inline, "utf8");
1439
1933
  else if (local) {
1440
- if (!existsSync5(local)) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE);
1441
- buf = readFileSync5(local);
1934
+ if (!existsSync6(local)) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE);
1935
+ buf = readFileSync6(local);
1442
1936
  } else if (!process.stdin.isTTY) buf = await readStdin();
1443
1937
  else throw usageError(t("Give a local file, --content, or pipe data on stdin.", "Pasa un archivo local, --content, o datos por stdin."), FILES_USAGE);
1444
1938
  const r = await sb.files.write(path, buf.toString("base64"), { encoding: "base64" });
1445
1939
  emit(ctx, { path, ...r }, () => console.log(t(`Wrote ${fmtBytes(r.bytes)} to ${path}`, `Escrib\xED ${fmtBytes(r.bytes)} en ${path}`)));
1940
+ } else if (action === "rm" || action === "delete") {
1941
+ const recursive = bool(ctx, "recursive");
1942
+ await confirm(ctx, t(`Delete ${path}${recursive ? " and everything in it" : ""} in ${id}?`, `\xBFBorrar ${path}${recursive ? " y todo lo que tiene" : ""} en ${id}?`));
1943
+ const r = await sb.files.delete(path, recursive ? { recursive } : void 0);
1944
+ emit(ctx, { path, ...r }, () => console.log(t(`Deleted ${path}`, `Borrado ${path}`)));
1945
+ } else if (action === "mv" || action === "move") {
1946
+ const to = need(ctx, 3, "new-path", FILES_USAGE);
1947
+ const r = await sb.files.move(path, to);
1948
+ emit(ctx, { from: path, to, ...r }, () => console.log(`${path} \u2192 ${to}`));
1949
+ } else if (action === "mkdir") {
1950
+ const r = await sb.files.mkdir(path);
1951
+ emit(ctx, { path, ...r }, () => console.log(t(`Created ${path}`, `Creada ${path}`)));
1446
1952
  } else {
1447
1953
  throw usageError(t(`Unknown files action "${action}".`, `Acci\xF3n de files desconocida "${action}".`), FILES_USAGE);
1448
1954
  }
1449
1955
  }
1450
1956
  },
1957
+ domains: sandboxDomains,
1451
1958
  suspend: {
1452
1959
  summary: "Suspend to disk (frees CPU; TTL paused)",
1453
1960
  usage: "easybits sandboxes suspend <sandbox>",
@@ -1490,6 +1997,129 @@ var sandboxes = {
1490
1997
  emit(ctx, { ...r, sandboxId: ctx.args[0] }, () => console.log(t(`Destroyed ${ctx.args[0]}`, `Destruido ${ctx.args[0]}`)));
1491
1998
  }
1492
1999
  },
2000
+ bg: {
2001
+ summary: "Long-running processes: start one in the background, list, poll its output, kill it",
2002
+ usage: BG_USAGE,
2003
+ options: {
2004
+ cwd: { type: "string", value: "dir", description: "Working directory" },
2005
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" },
2006
+ ...ENV_FILE_OPTION,
2007
+ grace: { type: "string", value: "seconds", description: "kill: seconds between SIGTERM and SIGKILL (default 5)" }
2008
+ },
2009
+ examples: [
2010
+ "easybits sb bg start sb_abc123 --cwd /data/work -- npm run dev",
2011
+ "easybits sb bg ls sb_abc123",
2012
+ "easybits sb bg status sb_abc123 exec_123 # status, exit code, stdout/stderr",
2013
+ "easybits sb bg kill sb_abc123 exec_123"
2014
+ ],
2015
+ async run(ctx) {
2016
+ const action = need(ctx, 0, "start|ls|status|kill", BG_USAGE);
2017
+ need(ctx, 1, "sandbox", BG_USAGE);
2018
+ if (action === "start") {
2019
+ const command = ctx.args.slice(2).join(" ");
2020
+ if (!command) throw usageError(t("Missing <command> after --.", "Falta <command> despu\xE9s de --."), BG_USAGE);
2021
+ const env = await envFrom(ctx, BG_USAGE);
2022
+ const sb = await sandboxAt(ctx, 1, BG_USAGE);
2023
+ const r = await sb.execBackground(command, { cwd: str(ctx, "cwd"), env: Object.keys(env).length ? env : void 0 });
2024
+ emit(ctx, r, () => {
2025
+ console.log(t(`Started ${r.execId} (${r.status})`, `Arranc\xF3 ${r.execId} (${r.status})`));
2026
+ console.log(t(`Poll it: easybits sb bg status ${ctx.args[1]} ${r.execId}`, `Rev\xEDsalo: easybits sb bg status ${ctx.args[1]} ${r.execId}`));
2027
+ });
2028
+ } else if (action === "ls" || action === "list") {
2029
+ const r = await (await sandboxAt(ctx, 1, BG_USAGE)).bgList();
2030
+ emit(
2031
+ ctx,
2032
+ r,
2033
+ () => table(
2034
+ r.processes.map((p) => ({ ...p, startedAt: fmtDate(p.startedAt), exit: p.exitCode ?? "-", output: fmtBytes(p.stdoutBytes + p.stderrBytes) })),
2035
+ [["execId", "ID"], ["status", "STATUS"], ["exit", "EXIT"], ["startedAt", "STARTED"], ["output", "OUTPUT"], ["command", "COMMAND"]],
2036
+ t("No background processes.", "Sin procesos en segundo plano.")
2037
+ )
2038
+ );
2039
+ } else if (action === "status") {
2040
+ const execId = need(ctx, 2, "exec-id", BG_USAGE);
2041
+ const r = await (await sandboxAt(ctx, 1, BG_USAGE)).bgStatus(execId);
2042
+ if (ctx.json) return emit(ctx, r, () => {
2043
+ });
2044
+ console.error(`${execId}: ${r.status}${r.exitCode != null ? ` (exit ${r.exitCode})` : ""}${r.logsExpired ? t(" \u2014 logs expired", " \u2014 logs vencidos") : ""}`);
2045
+ if (r.stdout) process.stdout.write(r.stdout);
2046
+ if (r.stderr) process.stderr.write(r.stderr);
2047
+ } else if (action === "kill") {
2048
+ const execId = need(ctx, 2, "exec-id", BG_USAGE);
2049
+ const grace = int(ctx, "grace", BG_USAGE);
2050
+ const r = await (await sandboxAt(ctx, 1, BG_USAGE)).bgKill(execId, grace != null ? { graceSeconds: grace } : void 0);
2051
+ emit(
2052
+ ctx,
2053
+ { execId, ...r },
2054
+ () => console.log(r.alreadyExited ? t(`${execId} had already exited.`, `${execId} ya hab\xEDa terminado.`) : t(`Killed ${execId} (${r.signal ?? "SIGTERM"})`, `Detenido ${execId} (${r.signal ?? "SIGTERM"})`))
2055
+ );
2056
+ } else {
2057
+ throw usageError(t(`Unknown bg action "${action}".`, `Acci\xF3n de bg desconocida "${action}".`), BG_USAGE);
2058
+ }
2059
+ }
2060
+ },
2061
+ git: {
2062
+ summary: "Git inside a sandbox: clone, status, commit, push, pull, checkout, log",
2063
+ usage: GIT_USAGE,
2064
+ options: {
2065
+ dir: { type: "string", value: "dir", description: "Repo folder inside the sandbox (required)" },
2066
+ repo: { type: "string", value: "url", description: "clone: repo URL (clean; the token goes in --git-token)" },
2067
+ branch: { type: "string", value: "name", description: "clone/push/checkout: branch" },
2068
+ depth: { type: "string", value: "n", description: "clone: shallow depth (default 1)" },
2069
+ sha: { type: "string", value: "sha", description: "clone: a specific commit (full clone, detached HEAD)" },
2070
+ "git-token": { type: "string", value: "$secret:NAME", description: "Token for a private repo; best as $secret:NAME from your vault" },
2071
+ "git-user": { type: "string", value: "name", description: "Username for the token (default: x-access-token)" },
2072
+ message: { type: "string", short: "m", value: "text", description: "commit: message" },
2073
+ all: { type: "boolean", description: "commit: stage everything first (git add -A)" },
2074
+ path: { type: "string", multiple: true, value: "path", description: "commit: only these paths (repeatable); log: only this path" },
2075
+ "author-name": { type: "string", value: "name", description: "commit: author name" },
2076
+ "author-email": { type: "string", value: "email", description: "commit: author email" },
2077
+ remote: { type: "string", value: "name", description: "push: remote (default origin)" },
2078
+ "set-upstream": { type: "boolean", description: "push: set the upstream branch" },
2079
+ force: { type: "boolean", description: "push: --force-with-lease (fails if the remote moved)" },
2080
+ rebase: { type: "boolean", description: "pull: rebase instead of merge" },
2081
+ create: { type: "boolean", description: "checkout: create the branch (-B, safe to repeat)" },
2082
+ from: { type: "string", value: "ref", description: "checkout --create: start point" },
2083
+ limit: { type: "string", value: "n", description: "log: how many" }
2084
+ },
2085
+ examples: [
2086
+ "easybits sb git clone sb_abc123 --repo https://github.com/you/app.git --dir /data/work/app --git-token '$secret:GITHUB_TOKEN'",
2087
+ "easybits sb git status sb_abc123 --dir /data/work/app",
2088
+ 'easybits sb git commit sb_abc123 --dir /data/work/app --all -m "fix: checkout"',
2089
+ "easybits sb git push sb_abc123 --dir /data/work/app --git-token '$secret:GITHUB_TOKEN'",
2090
+ "easybits sb git checkout sb_abc123 --dir /data/work/app --branch feature/x --create",
2091
+ "easybits sb git log sb_abc123 --dir /data/work/app --limit 5"
2092
+ ],
2093
+ async run(ctx) {
2094
+ const op = need(ctx, 0, GIT_OPS.join("|"), GIT_USAGE);
2095
+ need(ctx, 1, "sandbox", GIT_USAGE);
2096
+ const params = gitParams(op, {
2097
+ dir: str(ctx, "dir"),
2098
+ repo: str(ctx, "repo"),
2099
+ branch: str(ctx, "branch"),
2100
+ depth: int(ctx, "depth", GIT_USAGE),
2101
+ sha: str(ctx, "sha"),
2102
+ token: str(ctx, "git-token"),
2103
+ username: str(ctx, "git-user"),
2104
+ message: str(ctx, "message"),
2105
+ all: bool(ctx, "all"),
2106
+ paths: list(ctx, "path"),
2107
+ authorName: str(ctx, "author-name"),
2108
+ authorEmail: str(ctx, "author-email"),
2109
+ remote: str(ctx, "remote"),
2110
+ setUpstream: bool(ctx, "set-upstream"),
2111
+ force: bool(ctx, "force"),
2112
+ rebase: bool(ctx, "rebase"),
2113
+ create: bool(ctx, "create"),
2114
+ from: str(ctx, "from"),
2115
+ limit: int(ctx, "limit", GIT_USAGE)
2116
+ });
2117
+ const { git } = await sandboxAt(ctx, 1, GIT_USAGE);
2118
+ const r = op === "status" ? await git.status(params.dir) : await git[op](params);
2119
+ emit(ctx, r, () => printGit(op, r));
2120
+ if (op === "pull" && r.conflicts?.length) process.exitCode = 1;
2121
+ }
2122
+ },
1493
2123
  snapshot: {
1494
2124
  summary: "Capture a copy-on-write snapshot without stopping the box",
1495
2125
  usage: "easybits sandboxes snapshot <sandbox> [--name <name>]",
@@ -1504,14 +2134,18 @@ var sandboxes = {
1504
2134
  };
1505
2135
 
1506
2136
  // src/commands/machines.ts
1507
- import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
1508
- import { basename as basename2 } from "path";
2137
+ import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
2138
+ import { basename as basename3 } from "path";
1509
2139
 
1510
2140
  // src/refs.ts
1511
2141
  var ID_FORMAT = {
1512
2142
  agent: /^[0-9a-f]{24}$/i,
1513
2143
  db: /^[0-9a-f]{24}$/i,
1514
- sandbox: /^sb_[0-9a-z-]+$/i
2144
+ sandbox: /^sb_[0-9a-z-]+$/i,
2145
+ site: /^[0-9a-f]{24}$/i,
2146
+ fleet: /^[0-9a-f]{24}$/i,
2147
+ document: /^[0-9a-f]{24}$/i,
2148
+ video: /^[0-9a-f]{24}$/i
1515
2149
  };
1516
2150
  var looksLikeId = (kind, ref) => ID_FORMAT[kind].test(ref);
1517
2151
  var REF_ARGS = {
@@ -1531,6 +2165,9 @@ var REF_ARGS = {
1531
2165
  "sandboxes exec": { index: 0, kind: "sandbox" },
1532
2166
  "sandboxes logs": { index: 0, kind: "sandbox" },
1533
2167
  "sandboxes files": { index: 1, kind: "sandbox" },
2168
+ "sandboxes domains": { index: 1, kind: "sandbox" },
2169
+ "sandboxes bg": { index: 1, kind: "sandbox" },
2170
+ "sandboxes git": { index: 1, kind: "sandbox" },
1534
2171
  "sandboxes suspend": { index: 0, kind: "sandbox" },
1535
2172
  "sandboxes resume": { index: 0, kind: "sandbox" },
1536
2173
  "sandboxes destroy": { index: 0, kind: "sandbox" },
@@ -1540,15 +2177,35 @@ var REF_ARGS = {
1540
2177
  "machines logs": { index: 0, kind: "sandbox" },
1541
2178
  "machines rollback": { index: 0, kind: "sandbox" },
1542
2179
  "machines secrets": { index: 1, kind: "sandbox" },
1543
- "domains ls": { index: 0, kind: "sandbox" },
1544
- "domains add": { index: 0, kind: "sandbox" },
1545
- "domains verify": { index: 0, kind: "sandbox" },
1546
- "domains rm": { index: 0, kind: "sandbox" },
2180
+ "machines get": { index: 0, kind: "sandbox" },
2181
+ "machines restart": { index: 0, kind: "sandbox" },
2182
+ "machines redeploy": { index: 0, kind: "sandbox" },
2183
+ "machines backups": { index: 0, kind: "sandbox" },
2184
+ "machines backup": { index: 0, kind: "sandbox" },
2185
+ "machines runspec": { index: 1, kind: "sandbox" },
2186
+ "machines rm": { index: 0, kind: "sandbox" },
1547
2187
  "db rm": { index: 0, kind: "db" },
1548
2188
  "db rename": { index: 0, kind: "db" },
1549
2189
  "db tables": { index: 0, kind: "db" },
1550
2190
  "db query": { index: 0, kind: "db" },
1551
- "db photos": { index: 1, kind: "db" }
2191
+ "db photos": { index: 1, kind: "db" },
2192
+ "sites theme": { index: 1, kind: "site" },
2193
+ "fleet get": { index: 0, kind: "fleet" },
2194
+ "fleet capabilities": { index: 0, kind: "fleet" },
2195
+ "fleet configure": { index: 0, kind: "fleet" },
2196
+ "fleet message": { index: 0, kind: "fleet" },
2197
+ "documents get": { index: 0, kind: "document" },
2198
+ "documents pages": { index: 1, kind: "document" },
2199
+ "documents add-page": { index: 0, kind: "document" },
2200
+ "documents rm-page": { index: 0, kind: "document" },
2201
+ "documents reorder": { index: 0, kind: "document" },
2202
+ "documents deploy": { index: 0, kind: "document" },
2203
+ "documents export": { index: 1, kind: "document" },
2204
+ "documents rm": { index: 0, kind: "document" },
2205
+ "video projects": { index: 1, kind: "video" },
2206
+ "video scenes": { index: 1, kind: "video" },
2207
+ "video music": { index: 1, kind: "video" },
2208
+ "video render": { index: 0, kind: "video" }
1552
2209
  };
1553
2210
  function matchRef(kind, ref, items) {
1554
2211
  if (looksLikeId(kind, ref)) return { ok: true, id: ref };
@@ -1565,28 +2222,37 @@ function matchRef(kind, ref, items) {
1565
2222
  var LS_HINT = {
1566
2223
  agent: "easybits agents ls",
1567
2224
  sandbox: "easybits sandboxes ls",
1568
- db: "easybits db ls"
2225
+ db: "easybits db ls",
2226
+ site: "easybits sites ls",
2227
+ fleet: "easybits fleet ls",
2228
+ document: "easybits documents ls",
2229
+ video: "easybits video projects ls"
1569
2230
  };
1570
- var NOUN = () => ({ agent: t("agent", "agente"), sandbox: t("sandbox or machine", "sandbox o m\xE1quina"), db: t("database", "base de datos") });
1571
- var NOUNS = () => ({ agent: t("agents", "agentes"), sandbox: t("sandboxes", "sandboxes"), db: t("databases", "bases de datos") });
2231
+ 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") });
2232
+ 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") });
1572
2233
  var cache = /* @__PURE__ */ new Map();
1573
- function listRefs(ctx, kind) {
1574
- let p = cache.get(kind);
2234
+ function listRefs(ctx, kind, ref) {
2235
+ const key = kind === "document" ? `document:${ref}` : kind;
2236
+ let p = cache.get(key);
1575
2237
  if (!p) {
1576
2238
  p = (async () => {
1577
2239
  const eb = await getClient(ctx);
1578
2240
  if (kind === "agent") return (await eb.listAgents()).map((a) => ({ id: a.agentId, name: a.name, status: a.status }));
2241
+ if (kind === "fleet") return (await api(ctx, "GET", "/fleet-agents")).pools.map((p2) => ({ id: p2.id, name: p2.name }));
2242
+ if (kind === "document") return (await api(ctx, "GET", `/documents?limit=100&search=${encodeURIComponent(ref ?? "")}`)).items.map((d) => ({ id: d.id, name: d.name }));
2243
+ if (kind === "video") return (await api(ctx, "GET", "/video-projects?limit=100")).items.map((v) => ({ id: v.id, name: v.name, status: v.status }));
2244
+ if (kind === "site") return (await api(ctx, "GET", "/sites")).items.map((s) => ({ id: s.id, name: s.name }));
1579
2245
  if (kind === "db") return (await eb.listDatabases()).items.map((d) => ({ id: d.id, name: d.name }));
1580
2246
  return (await eb.sandboxes.list()).map((s) => ({ id: s.sandboxId, name: s.name, status: s.status }));
1581
2247
  })();
1582
- p.catch(() => cache.delete(kind));
1583
- cache.set(kind, p);
2248
+ p.catch(() => cache.delete(key));
2249
+ cache.set(key, p);
1584
2250
  }
1585
2251
  return p;
1586
2252
  }
1587
2253
  async function resolveRef(ctx, kind, ref) {
1588
2254
  if (looksLikeId(kind, ref)) return ref;
1589
- const r = matchRef(kind, ref, await listRefs(ctx, kind));
2255
+ const r = matchRef(kind, ref, await listRefs(ctx, kind, ref));
1590
2256
  if (r.ok) return r.id;
1591
2257
  if (r.reason === "not_found") {
1592
2258
  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);
@@ -1610,6 +2276,59 @@ async function resolveRefs(ctx, key) {
1610
2276
 
1611
2277
  // src/commands/machines.ts
1612
2278
  var SECRETS_USAGE = "easybits machines secrets <ls|set|unset> <machine> [--dotenv <path> | NAME]";
2279
+ var RUNSPEC_USAGE = "easybits machines runspec <get|set> <machine> [--file <file.json|->] [--app-dir <dir>] [--build <cmd>] [--start <cmd>] [--unit <name>] [--port <port>] [--prebuilt on|off] [--data <path>]... [--exclude <path>]... [--env K=V]...";
2280
+ function printMachine(m) {
2281
+ console.log(`ID: ${m.sandboxId}`);
2282
+ if (m.name) console.log(`${t("Name: ", "Nombre: ")} ${m.name}`);
2283
+ console.log(`${t("Status: ", "Estado: ")} ${m.status}${m.runtimeStatus ? ` (${m.runtimeStatus})` : ""}`);
2284
+ console.log(`Tier: ${m.tier} \xB7 ${m.vcpus} vCPU ${m.cpuMode} \xB7 ${fmtBytes(m.memoryMb * 1024 * 1024)} RAM \xB7 ${fmtBytes(m.diskMb * 1024 * 1024)}${m.diskAddonsGB ? ` + ${m.diskAddonsGB} GB` : ""}`);
2285
+ console.log(`${t("Price: ", "Precio: ")} $${m.monthlyMxn} MXN/${t("month", "mes")}`);
2286
+ console.log(`${t("Sleeps: ", "Duerme: ")} ${m.sleep ? t("yes, without visits", "s\xED, sin visitas") : t("no, always on", "no, siempre prendida")}`);
2287
+ console.log(`${t("Created: ", "Creada: ")} ${fmtDate(m.createdAt)}`);
2288
+ if (m.shared) console.log(t("Shared with you by its owner.", "Su due\xF1o la comparti\xF3 contigo."));
2289
+ if (m.deletionScheduledAt) console.log(t(`Scheduled for deletion: ${fmtDate(m.deletionScheduledAt)}`, `Programada para borrarse: ${fmtDate(m.deletionScheduledAt)}`));
2290
+ }
2291
+ function printRunspec(r) {
2292
+ for (const [k, v] of Object.entries(r)) {
2293
+ if (v == null) continue;
2294
+ const shown = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? Object.keys(v).join(", ") : String(v);
2295
+ console.log(`${k.padEnd(16)}${shown}`);
2296
+ }
2297
+ }
2298
+ async function runspecFrom(ctx) {
2299
+ let spec = {};
2300
+ const file = str(ctx, "file");
2301
+ if (file) {
2302
+ const text = file === "-" ? (await readStdin()).toString("utf8") : existsSync7(file) ? readFileSync7(file, "utf8") : null;
2303
+ if (text == null) throw usageError(t(`File not found: ${file}`, `No existe el archivo: ${file}`), RUNSPEC_USAGE);
2304
+ try {
2305
+ const parsed = JSON.parse(text);
2306
+ spec = parsed && typeof parsed === "object" && "runspec" in parsed ? parsed.runspec ?? {} : parsed;
2307
+ } catch {
2308
+ throw usageError(t(`${file === "-" ? "stdin" : file} is not valid JSON.`, `${file === "-" ? "stdin" : file} no es JSON v\xE1lido.`), RUNSPEC_USAGE);
2309
+ }
2310
+ }
2311
+ const set2 = (k, v) => {
2312
+ if (v !== void 0) spec[k] = v;
2313
+ };
2314
+ set2("appDir", str(ctx, "app-dir"));
2315
+ set2("buildCommand", str(ctx, "build"));
2316
+ set2("startCommand", str(ctx, "start"));
2317
+ set2("unit", str(ctx, "unit"));
2318
+ set2("port", int(ctx, "port", RUNSPEC_USAGE));
2319
+ const prebuilt = str(ctx, "prebuilt");
2320
+ if (prebuilt != null) {
2321
+ if (prebuilt !== "on" && prebuilt !== "off") throw usageError(t("--prebuilt is on or off.", "--prebuilt es on u off."), RUNSPEC_USAGE);
2322
+ spec.prebuilt = prebuilt === "on";
2323
+ }
2324
+ const data = list(ctx, "data");
2325
+ if (data.length) spec.dataPaths = data;
2326
+ const excludes = list(ctx, "exclude");
2327
+ if (excludes.length) spec.excludes = excludes;
2328
+ const env = pairs(list(ctx, "env"), RUNSPEC_USAGE);
2329
+ if (Object.keys(env).length) spec.env = env;
2330
+ return spec;
2331
+ }
1613
2332
  var machines = {
1614
2333
  name: "machines",
1615
2334
  aliases: ["machine"],
@@ -1673,16 +2392,19 @@ var machines = {
1673
2392
  }
1674
2393
  if (repo && archiveUrl) throw usageError(t("--repo and --archive are exclusive.", "--repo y --archive se excluyen."), this.usage);
1675
2394
  if (machineRef && str(ctx, "tier")) {
1676
- throw usageError(t("--tier is only for a new machine (resize = redeploy_machine).", "--tier s\xF3lo aplica a una m\xE1quina nueva (cambiar tama\xF1o = redeploy_machine)."), this.usage);
2395
+ throw usageError(
2396
+ t("--tier is only for a new machine. To resize: easybits machines redeploy <machine> --tier <tier> --replace", "--tier s\xF3lo aplica a una m\xE1quina nueva. Para cambiar tama\xF1o: easybits machines redeploy <m\xE1quina> --tier <tier> --replace"),
2397
+ this.usage
2398
+ );
1677
2399
  }
1678
2400
  const eb = await getClient(ctx);
1679
2401
  const sandboxId = machineRef ? await resolveRef(ctx, "sandbox", machineRef) : void 0;
1680
2402
  if (archiveUrl && !/^https?:\/\//.test(archiveUrl)) {
1681
- if (!existsSync6(archiveUrl)) throw usageError(t(`File not found: ${archiveUrl}`, `No existe el archivo: ${archiveUrl}`), this.usage);
1682
- const size = statSync2(archiveUrl).size;
2403
+ if (!existsSync7(archiveUrl)) throw usageError(t(`File not found: ${archiveUrl}`, `No existe el archivo: ${archiveUrl}`), this.usage);
2404
+ const size = statSync4(archiveUrl).size;
1683
2405
  const contentType = archiveUrl.endsWith(".zip") ? "application/zip" : "application/gzip";
1684
- const up = await eb.uploadFile({ fileName: basename2(archiveUrl), contentType, size, access: "public" });
1685
- const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync6(archiveUrl), headers: { "Content-Type": contentType } });
2406
+ const up = await eb.uploadFile({ fileName: basename3(archiveUrl), contentType, size, access: "public" });
2407
+ const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync7(archiveUrl), headers: { "Content-Type": contentType } });
1686
2408
  if (!res.ok) throw new CliError(t(`Upload failed: storage answered ${res.status}`, `Fall\xF3 la subida: el almacenamiento contest\xF3 ${res.status}`), 1, void 0, "upload_failed", res.status);
1687
2409
  archiveUrl = up.file.url;
1688
2410
  if (!archiveUrl) throw new CliError(t("The upload returned no public URL.", "La subida no devolvi\xF3 una URL p\xFAblica."), 1, void 0, "upload_failed");
@@ -1794,6 +2516,184 @@ var machines = {
1794
2516
  if (r.exitCode !== 0) process.exitCode = 1;
1795
2517
  }
1796
2518
  },
2519
+ get: {
2520
+ summary: "Show a machine: tier, size, price, status and whether it sleeps",
2521
+ usage: "easybits machines get <machine>",
2522
+ examples: ["easybits machines get shop", "easybits machines get shop --json"],
2523
+ async run(ctx) {
2524
+ const id = need(ctx, 0, "machine", this.usage);
2525
+ const m = await api(ctx, "GET", `/machines/${encodeURIComponent(id)}`);
2526
+ emit(ctx, m, () => printMachine(m));
2527
+ }
2528
+ },
2529
+ restart: {
2530
+ summary: "Restart the app's process with the current secrets and runspec (seconds, no build)",
2531
+ usage: "easybits machines restart <machine>",
2532
+ examples: ["easybits machines restart shop", "easybits machines secrets set shop --dotenv .env && easybits machines restart shop"],
2533
+ async run(ctx) {
2534
+ const id = need(ctx, 0, "machine", this.usage);
2535
+ const r = await api(ctx, "POST", `/machines/${encodeURIComponent(id)}/restart`);
2536
+ emit(ctx, r, () => {
2537
+ console.log(r.exitCode === 0 ? t(`Restarted ${id}`, `Reiniciada ${id}`) : t(`Restart failed (exit ${r.exitCode})`, `Fall\xF3 el reinicio (salida ${r.exitCode})`));
2538
+ if (r.exitCode !== 0 && r.startOutput) console.log(r.startOutput.slice(-2e3));
2539
+ });
2540
+ if (r.exitCode !== 0) process.exitCode = 1;
2541
+ }
2542
+ },
2543
+ redeploy: {
2544
+ summary: "Build a NEW machine from a release: recover a lost one, or change its tier (resize)",
2545
+ usage: "easybits machines redeploy <machine> [--release <id>] [--tier <tier>] [--name <name>] [--cpu-mode shared|reserved] [--replace] [--yes]",
2546
+ options: {
2547
+ release: { type: "string", value: "id", description: "Release to build from (default: the machine's newest)" },
2548
+ tier: { type: "string", value: "tier", description: "Size of the new machine (default: same as the release's)" },
2549
+ name: { type: "string", value: "name", description: "Label for the new machine" },
2550
+ "cpu-mode": { type: "string", value: "mode", description: "shared (default) or reserved" },
2551
+ replace: { type: "boolean", description: "Release the old machine once the new one runs (otherwise you pay for both)" },
2552
+ ...YES_OPTION
2553
+ },
2554
+ examples: [
2555
+ "easybits machines redeploy shop --tier focus --replace # resize",
2556
+ "easybits machines redeploy sb_abc123 --release rel_789 # recover from a release"
2557
+ ],
2558
+ async run(ctx) {
2559
+ const id = need(ctx, 0, "machine", this.usage);
2560
+ const replace = bool(ctx, "replace");
2561
+ if (replace) requireYesIfHeadless(ctx);
2562
+ const eb = await getClient(ctx);
2563
+ let releaseId = str(ctx, "release");
2564
+ if (!releaseId) {
2565
+ const { items } = await eb.machines.releases(id, { limit: 1 });
2566
+ releaseId = items[0]?.releaseId;
2567
+ if (!releaseId) throw new CliError(t(`${id} has no releases to build from.`, `${id} no tiene releases de d\xF3nde construir.`), 1, t("Publish one first: easybits machines deploy <machine>", "Publica uno antes: easybits machines deploy <m\xE1quina>"), "no_release");
2568
+ }
2569
+ const cpuMode = str(ctx, "cpu-mode");
2570
+ if (cpuMode && cpuMode !== "shared" && cpuMode !== "reserved") throw usageError(t("--cpu-mode is shared or reserved.", "--cpu-mode es shared o reserved."), this.usage);
2571
+ if (replace) {
2572
+ await confirm(
2573
+ ctx,
2574
+ t(`Build a new machine from ${releaseId} and RELEASE ${id} once it runs? The new one starts with no data (${id}'s data stays in its backups).`, `\xBFConstruir una m\xE1quina nueva desde ${releaseId} y LIBERAR ${id} cuando corra? La nueva nace sin datos (los de ${id} quedan en sus respaldos).`),
2575
+ { typeName: id }
2576
+ );
2577
+ }
2578
+ const r = await eb.machines.redeploy(releaseId, {
2579
+ tier: str(ctx, "tier"),
2580
+ name: str(ctx, "name"),
2581
+ cpuMode,
2582
+ replaceSandboxId: replace ? id : void 0
2583
+ });
2584
+ emit(ctx, r, () => {
2585
+ console.log(t(`New machine ${r.sandboxId} from ${r.releaseId} (exit ${r.exitCode})`, `M\xE1quina nueva ${r.sandboxId} desde ${r.releaseId} (salida ${r.exitCode})`));
2586
+ if (r.replaced) console.log(t(`Released ${r.replaced}.`, `Liberada ${r.replaced}.`));
2587
+ console.log(t("It starts with no data: restore a backup if the app keeps any.", "Nace sin datos: restaura un respaldo si la app guarda alguno."));
2588
+ });
2589
+ if (r.exitCode !== 0) process.exitCode = 1;
2590
+ }
2591
+ },
2592
+ backups: {
2593
+ summary: "List the daily data backups, newest first (kept 7 days)",
2594
+ usage: "easybits machines backups <machine> [--limit <n>]",
2595
+ options: { limit: { type: "string", value: "n", description: "How many" } },
2596
+ examples: ["easybits machines backups shop", "easybits machines backups shop --json"],
2597
+ async run(ctx) {
2598
+ const id = need(ctx, 0, "machine", this.usage);
2599
+ const eb = await getClient(ctx);
2600
+ const r = await eb.machines.backups(id, { limit: int(ctx, "limit", this.usage) });
2601
+ emit(
2602
+ ctx,
2603
+ r,
2604
+ () => table(
2605
+ r.items.map((b) => ({ ...b, size: fmtBytes(b.bytes), createdAt: fmtDate(b.createdAt), expiresAt: fmtDate(b.expiresAt) })),
2606
+ [["id", "ID"], ["stamp", "DAY"], ["status", "STATUS"], ["size", "SIZE"], ["consistency", "CONSISTENCY"], ["expiresAt", "EXPIRES"]],
2607
+ t("No backups yet. Declare what to copy (runspec dataPaths), then: easybits machines backup <machine>", "Todav\xEDa no hay respaldos. Declara qu\xE9 copiar (dataPaths del runspec) y luego: easybits machines backup <m\xE1quina>")
2608
+ )
2609
+ );
2610
+ }
2611
+ },
2612
+ backup: {
2613
+ summary: "Take a data backup now instead of waiting for tonight's",
2614
+ usage: "easybits machines backup <machine>",
2615
+ examples: ["easybits machines backup shop"],
2616
+ async run(ctx) {
2617
+ const id = need(ctx, 0, "machine", this.usage);
2618
+ const eb = await getClient(ctx);
2619
+ const b = await eb.machines.backup(id);
2620
+ emit(ctx, b, () => console.log(t(`Backup ${b.id} (${b.status}, ${fmtBytes(b.bytes)}, ${b.consistency})`, `Respaldo ${b.id} (${b.status}, ${fmtBytes(b.bytes)}, ${b.consistency})`)));
2621
+ }
2622
+ },
2623
+ runspec: {
2624
+ summary: "How the app is built, started and backed up (set merges with what is there)",
2625
+ usage: RUNSPEC_USAGE,
2626
+ options: {
2627
+ file: { type: "string", value: "file.json|-", description: "Runspec as JSON (- reads stdin); flags win over it" },
2628
+ "app-dir": { type: "string", value: "dir", description: "Where the app lives in the machine" },
2629
+ build: { type: "string", value: "cmd", description: "Build command" },
2630
+ start: { type: "string", value: "cmd", description: "Start command (used when there is no systemd unit)" },
2631
+ unit: { type: "string", value: "name", description: "systemd unit to restart (preferred over --start)" },
2632
+ port: { type: "string", value: "port", description: "Port the app listens on" },
2633
+ prebuilt: { type: "string", value: "on|off", description: "on: releases ship the build and a deploy only starts it" },
2634
+ data: { type: "string", multiple: true, value: "path", description: "Path the nightly backup copies (repeatable). Without it nothing is backed up" },
2635
+ exclude: { type: "string", multiple: true, value: "path", description: "Path kept out of the release (repeatable)" },
2636
+ env: { type: "string", multiple: true, value: "K=V", description: "Environment variable, non-secret (repeatable)" }
2637
+ },
2638
+ examples: [
2639
+ "easybits machines runspec get shop",
2640
+ "easybits machines runspec set shop --data /app/data --data /app/uploads",
2641
+ "easybits machines runspec set shop --start 'node server.js' --port 8080 --prebuilt on",
2642
+ "easybits machines runspec get shop --json | jq .runspec > rs.json # edit, then:",
2643
+ "easybits machines runspec set shop --file rs.json"
2644
+ ],
2645
+ async run(ctx) {
2646
+ const action = need(ctx, 0, "get|set", RUNSPEC_USAGE);
2647
+ const id = need(ctx, 1, "machine", RUNSPEC_USAGE);
2648
+ const eb = await getClient(ctx);
2649
+ if (action === "get") {
2650
+ const r2 = await eb.machines.runspec(id);
2651
+ emit(ctx, r2, () => r2.runspec ? printRunspec(r2.runspec) : console.log(t("No runspec yet: a machine made with launch has one.", "Todav\xEDa sin runspec: una m\xE1quina hecha con launch ya lo trae.")));
2652
+ return;
2653
+ }
2654
+ if (action !== "set") throw usageError(t(`Unknown runspec action "${action}".`, `Acci\xF3n de runspec desconocida "${action}".`), RUNSPEC_USAGE);
2655
+ const spec = await runspecFrom(ctx);
2656
+ if (!Object.keys(spec).length) throw usageError(t("Nothing to set: pass --file or a flag (--data, --start, --port\u2026).", "Nada que cambiar: pasa --file o una bandera (--data, --start, --port\u2026)."), RUNSPEC_USAGE);
2657
+ const r = await eb.machines.setRunspec(id, spec);
2658
+ emit(ctx, r, () => {
2659
+ console.log(t("Runspec saved. It applies on the next deploy or restart.", "Runspec guardado. Aplica en el siguiente deploy o reinicio."));
2660
+ printRunspec(r);
2661
+ });
2662
+ }
2663
+ },
2664
+ rm: {
2665
+ aliases: ["destroy", "delete"],
2666
+ summary: "Release a machine: stops billing, takes a last backup and destroys it",
2667
+ usage: "easybits machines rm <machine> [--yes]",
2668
+ options: { ...YES_OPTION },
2669
+ examples: ["easybits machines rm shop", "easybits machines rm sb_abc123 --yes --json"],
2670
+ async run(ctx) {
2671
+ const id = need(ctx, 0, "machine", this.usage);
2672
+ requireYesIfHeadless(ctx);
2673
+ await confirm(ctx, t(`Release machine ${id}? Billing stops and the box is destroyed (a last backup is kept).`, `\xBFLiberar la m\xE1quina ${id}? Se deja de cobrar y la caja se destruye (queda un \xFAltimo respaldo).`), { typeName: id });
2674
+ const r = await api(ctx, "DELETE", `/machines/${encodeURIComponent(id)}`);
2675
+ emit(ctx, { ...r, sandboxId: id }, () => {
2676
+ console.log(t(`Released ${id}.`, `Liberada ${id}.`));
2677
+ console.log(r.backupId ? t(`Last backup: ${r.backupId}`, `\xDAltimo respaldo: ${r.backupId}`) : t("No final backup (nothing declared in dataPaths, or it failed).", "Sin respaldo final (nada declarado en dataPaths, o fall\xF3)."));
2678
+ });
2679
+ }
2680
+ },
2681
+ tiers: {
2682
+ summary: "The hosting catalog: sizes and monthly prices in MXN",
2683
+ usage: "easybits machines tiers",
2684
+ examples: ["easybits machines tiers", "easybits machines tiers --json"],
2685
+ async run(ctx) {
2686
+ const eb = await getClient(ctx);
2687
+ const r = await eb.machines.tiers();
2688
+ emit(ctx, r, () => {
2689
+ table(
2690
+ r.tiers.map((x) => ({ ...x, memory: fmtBytes(x.memoryMb * 1024 * 1024), disk: fmtBytes(x.diskMb * 1024 * 1024), reserved: x.priceReserved ?? "-" })),
2691
+ [["key", "TIER"], ["vcpus", "VCPUS"], ["memory", "MEMORY"], ["disk", "DISK"], ["priceShared", "MXN/MONTH"], ["reserved", "RESERVED"], ["minPlan", "PLAN"]]
2692
+ );
2693
+ console.log(t(`Extra disk: ${r.diskAddon.gb} GB for $${r.diskAddon.price} MXN/month each.`, `Disco extra: ${r.diskAddon.gb} GB por $${r.diskAddon.price} MXN/mes cada uno.`));
2694
+ });
2695
+ }
2696
+ },
1797
2697
  secrets: {
1798
2698
  summary: "App env vars stored encrypted in your vault",
1799
2699
  usage: SECRETS_USAGE,
@@ -1832,85 +2732,165 @@ var machines = {
1832
2732
  }
1833
2733
  };
1834
2734
 
1835
- // src/commands/domains.ts
1836
- async function sandbox2(ctx, usage2) {
1837
- const id = need(ctx, 0, "sandbox", usage2);
1838
- return (await getClient(ctx)).sandboxes.get(id);
2735
+ // src/commands/sites.ts
2736
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2737
+ import { resolveBaseUrl as resolveBaseUrl5 } from "@easybits.cloud/sdk";
2738
+ var SITE_STACKS = ["tanstack", "next", "react-router", "remix", "nuxt", "sveltekit", "astro", "elysia"];
2739
+ var SITE_TEMPLATES = ["consultorio", "despacho", "portafolio", "lanzamiento"];
2740
+ var TEMPLATE_STACKS = ["tanstack", "react-router"];
2741
+ function siteRows(items) {
2742
+ return items.map((s) => ({
2743
+ ...s,
2744
+ kind: s.kind === "webapp" ? `webapp${s.stack ? ` (${s.stack})` : ""}` : "static",
2745
+ url: s.url ?? (s.archivedAt ? t("(archived)", "(archivada)") : t("(not published)", "(sin publicar)")),
2746
+ createdAt: fmtDate(s.createdAt)
2747
+ }));
1839
2748
  }
1840
- var domains = {
1841
- name: "domains",
1842
- aliases: ["domain"],
2749
+ function createBody(o, usage2) {
2750
+ const kind = o.kind ?? "webapp";
2751
+ if (kind !== "static" && kind !== "webapp") throw usageError(t(`--kind is static or webapp, not "${kind}".`, `--kind es static o webapp, no "${kind}".`), usage2);
2752
+ if (kind === "static" && (o.stack || o.template)) {
2753
+ throw usageError(t("--stack and --template are only for --kind webapp.", "--stack y --template s\xF3lo aplican a --kind webapp."), usage2);
2754
+ }
2755
+ if (o.stack && !SITE_STACKS.includes(o.stack)) {
2756
+ throw usageError(t(`Unknown stack "${o.stack}". One of: ${SITE_STACKS.join(", ")}.`, `Stack desconocido "${o.stack}". Uno de: ${SITE_STACKS.join(", ")}.`), usage2);
2757
+ }
2758
+ if (o.template) {
2759
+ if (!SITE_TEMPLATES.includes(o.template)) {
2760
+ throw usageError(t(`Unknown template "${o.template}". One of: ${SITE_TEMPLATES.join(", ")}.`, `Plantilla desconocida "${o.template}". Una de: ${SITE_TEMPLATES.join(", ")}.`), usage2);
2761
+ }
2762
+ if (!TEMPLATE_STACKS.includes(o.stack ?? "tanstack")) {
2763
+ throw usageError(t(`--template only works with stack ${TEMPLATE_STACKS.join(" or ")}.`, `--template s\xF3lo funciona con el stack ${TEMPLATE_STACKS.join(" o ")}.`), usage2);
2764
+ }
2765
+ }
2766
+ if (!o.prompt && !o.name) throw usageError(t("Give --prompt (what to build) or --name.", "Pasa --prompt (qu\xE9 construir) o --name."), usage2);
2767
+ const body = { kind };
2768
+ for (const k of ["prompt", "name", "stack", "template"]) if (o[k]) body[k] = o[k];
2769
+ return body;
2770
+ }
2771
+ var THEME_USAGE = "easybits sites theme <get|set> <site> [--theme <id> | --colors <file.json|->]";
2772
+ async function readColors(path) {
2773
+ let text;
2774
+ if (path === "-") text = (await readStdin()).toString("utf8");
2775
+ else {
2776
+ if (!existsSync8(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), THEME_USAGE);
2777
+ text = readFileSync8(path, "utf8");
2778
+ }
2779
+ try {
2780
+ return JSON.parse(text);
2781
+ } catch {
2782
+ throw usageError(t(`${path === "-" ? "stdin" : path} is not valid JSON.`, `${path === "-" ? "stdin" : path} no es JSON v\xE1lido.`), THEME_USAGE);
2783
+ }
2784
+ }
2785
+ function printColors(colors) {
2786
+ table(Object.entries(colors ?? {}).map(([token, value]) => ({ token, value })), [["token", "TOKEN"], ["value", "VALUE"]]);
2787
+ }
2788
+ var sites = {
2789
+ name: "sites",
2790
+ aliases: ["site"],
1843
2791
  group: "Hosting",
1844
- summary: "Custom domains with auto TLS on a sandbox or machine",
1845
- synopsis: "domains",
2792
+ summary: "Your site-builder sites (/dash/sitios): static and web apps",
2793
+ synopsis: "sites <ls|create|theme>",
1846
2794
  subs: {
1847
2795
  ls: {
1848
2796
  aliases: ["list"],
1849
- summary: "List domains attached to a sandbox",
1850
- usage: "easybits domains ls <sandbox>",
1851
- examples: ["easybits domains ls sb_abc123"],
2797
+ summary: "List your sites with their kind, engine and public URL",
2798
+ usage: "easybits sites ls",
2799
+ examples: ["easybits sites ls", "easybits sites ls --json"],
1852
2800
  async run(ctx) {
1853
- const items = await (await sandbox2(ctx, this.usage)).listDomains();
1854
- emit(ctx, items, () => table(items, [["domain", "DOMAIN"], ["port", "PORT"]], t("No custom domains.", "Sin dominios propios.")));
2801
+ const { items } = await api(ctx, "GET", "/sites");
2802
+ emit(
2803
+ ctx,
2804
+ items,
2805
+ () => table(
2806
+ siteRows(items),
2807
+ [["id", "ID"], ["name", "NAME"], ["kind", "KIND"], ["engine", "ENGINE"], ["url", "URL"]],
2808
+ t('No sites yet. Create one: easybits sites create --prompt "\u2026"', 'Todav\xEDa no hay sitios. Crea uno: easybits sites create --prompt "\u2026"')
2809
+ )
2810
+ );
1855
2811
  }
1856
2812
  },
1857
- add: {
1858
- summary: "Attach a domain to a port; prints the DNS record to create",
1859
- usage: "easybits domains add <sandbox> <domain> --port <port>",
1860
- options: { port: { type: "string", value: "port", description: "Port inside the sandbox (required)" } },
1861
- examples: ["easybits domains add sb_abc123 shop.example.com --port 3000"],
2813
+ create: {
2814
+ aliases: ["new"],
2815
+ summary: "Create a site from a prompt; its builder agent picks it up in the editor",
2816
+ usage: "easybits sites create (--prompt <text> | --name <name>) [--kind webapp|static] [--stack <stack>] [--template <key>]",
2817
+ options: {
2818
+ prompt: { type: "string", value: "text", description: "What to build; also names the site if --name is missing" },
2819
+ name: { type: "string", value: "name", description: "Site name" },
2820
+ kind: { type: "string", value: "kind", description: "webapp (default: its own machine, published later) or static (CDN website)" },
2821
+ stack: { type: "string", value: "stack", description: "Web app stack: tanstack (default), next, react-router, remix, nuxt, sveltekit, astro, elysia" },
2822
+ template: { type: "string", value: "key", description: "Starting template (tanstack/react-router): consultorio, despacho, portafolio, lanzamiento" }
2823
+ },
2824
+ examples: [
2825
+ 'easybits sites create --prompt "Landing for my dental clinic in Puebla"',
2826
+ 'easybits sites create --prompt "Task tracker with login" --stack next --json',
2827
+ 'easybits sites create --name "Promo" --kind static'
2828
+ ],
1862
2829
  async run(ctx) {
1863
- const domain = need(ctx, 1, "domain", this.usage);
1864
- const port = int(ctx, "port", this.usage);
1865
- if (port == null) throw usageError(t("Missing --port.", "Falta --port."), this.usage);
1866
- const r = await (await sandbox2(ctx, this.usage)).addDomain(domain, port);
1867
- emit(ctx, r, () => {
1868
- console.log(t(`Attached ${r.domain} \u2192 port ${r.port}`, `Ligado ${r.domain} \u2192 puerto ${r.port}`));
1869
- console.log(t(`Create this DNS record: ${JSON.stringify(r.dns)}`, `Crea este registro DNS: ${JSON.stringify(r.dns)}`));
1870
- console.log(t(`Then run: easybits domains verify ${ctx.args[0]} ${r.domain}`, `Luego corre: easybits domains verify ${ctx.args[0]} ${r.domain}`));
2830
+ const body = createBody(
2831
+ { prompt: str(ctx, "prompt"), name: str(ctx, "name"), kind: str(ctx, "kind"), stack: str(ctx, "stack"), template: str(ctx, "template") },
2832
+ this.usage
2833
+ );
2834
+ const site = await api(ctx, "POST", "/sites", body);
2835
+ const base = (await resolveBaseUrl5()).replace(/\/$/, "");
2836
+ const editorUrl2 = site.editorUrl.startsWith("http") ? site.editorUrl : `${base}${site.editorUrl}`;
2837
+ const out = { ...site, editorUrl: editorUrl2 };
2838
+ emit(ctx, out, () => {
2839
+ console.log(t(`Created ${site.name} (${site.id}) \u2014 ${site.kind}${site.stack ? ` ${site.stack}` : ""}`, `Creado ${site.name} (${site.id}) \u2014 ${site.kind}${site.stack ? ` ${site.stack}` : ""}`));
2840
+ console.log(`${t("Editor", "Editor")}: ${editorUrl2}`);
2841
+ if (site.previewUrl) console.log(`${t("Preview", "Vista previa")}: ${site.previewUrl}`);
2842
+ console.log(site.url ? `URL: ${site.url}` : t("Not published yet: publish it from the editor.", "Todav\xEDa sin publicar: publ\xEDcalo desde el editor."));
1871
2843
  });
1872
2844
  }
1873
2845
  },
1874
- verify: {
1875
- summary: "Check DNS + HTTPS for a domain",
1876
- usage: "easybits domains verify <sandbox> <domain>",
1877
- examples: ["easybits domains verify sb_abc123 shop.example.com"],
2846
+ theme: {
2847
+ summary: "Read or change a site's color theme without a model turn (the \xABTema\xBB button)",
2848
+ usage: THEME_USAGE,
2849
+ options: {
2850
+ theme: { type: "string", value: "id", description: "Palette id: minimal, calido, oceano, noche, dracula, nord, tokyo-night, github-light\u2026" },
2851
+ colors: { type: "string", value: "file.json|-", description: "JSON with every token as #rrggbb (- reads stdin)" }
2852
+ },
2853
+ examples: [
2854
+ "easybits sites theme get my-shop",
2855
+ "easybits sites theme set my-shop --theme dracula",
2856
+ "easybits sites theme get my-shop --json > theme.json # edit, then:",
2857
+ "jq .colors theme.json | easybits sites theme set my-shop --colors -"
2858
+ ],
1878
2859
  async run(ctx) {
1879
- const domain = need(ctx, 1, "domain", this.usage);
1880
- const r = await (await sandbox2(ctx, this.usage)).verifyDomain(domain);
2860
+ const action = need(ctx, 0, "get|set", THEME_USAGE);
2861
+ const id = need(ctx, 1, "site", THEME_USAGE);
2862
+ const path = `/sites/${encodeURIComponent(id)}/theme`;
2863
+ if (action === "get") {
2864
+ const r2 = await api(ctx, "GET", path);
2865
+ emit(ctx, r2, () => {
2866
+ if (!r2.supported) console.log(t("This site predates themes (literal colors): ask its builder agent to move it to tokens.", "Este sitio es anterior a los temas (colores literales): p\xEDdele a su agente que lo pase a tokens."));
2867
+ else printColors(r2.colors);
2868
+ });
2869
+ return;
2870
+ }
2871
+ if (action !== "set") throw usageError(t(`Unknown theme action "${action}".`, `Acci\xF3n de theme desconocida "${action}".`), THEME_USAGE);
2872
+ const themeId = str(ctx, "theme");
2873
+ const file = str(ctx, "colors");
2874
+ if (!themeId === !file) throw usageError(t("Give --theme <id> or --colors <file.json|->, one of them.", "Pasa --theme <id> o --colors <archivo.json|->, uno de los dos."), THEME_USAGE);
2875
+ const body = themeId ? { themeId } : { colors: await readColors(file) };
2876
+ const r = await api(ctx, "POST", path, body);
1881
2877
  emit(ctx, r, () => {
1882
- console.log(`${r.domain}: ${r.ready ? t("ready", "listo") : t("not ready yet", "todav\xEDa no est\xE1 listo")}`);
1883
- console.log(t(`DNS resolved: ${r.dns.resolved} HTTPS ok: ${r.https.ok}`, `DNS resuelve: ${r.dns.resolved} HTTPS ok: ${r.https.ok}`));
1884
- if (r.hint) console.log(r.hint);
2878
+ console.log(t("Theme changed. The preview updates by itself.", "Tema cambiado. La vista previa se actualiza sola."));
2879
+ printColors(r.colors);
1885
2880
  });
1886
- if (!r.ready) process.exitCode = 1;
1887
- }
1888
- },
1889
- rm: {
1890
- aliases: ["remove", "delete"],
1891
- summary: "Detach a domain",
1892
- usage: "easybits domains rm <sandbox> <domain> [--yes]",
1893
- options: { ...YES_OPTION },
1894
- examples: ["easybits domains rm sb_abc123 shop.example.com", "easybits domains rm sb_abc123 shop.example.com --yes"],
1895
- async run(ctx) {
1896
- need(ctx, 0, "sandbox", this.usage);
1897
- const domain = need(ctx, 1, "domain", this.usage);
1898
- requireYesIfHeadless(ctx);
1899
- await confirm(ctx, t(`Detach ${domain} from ${ctx.args[0]}? It stops serving right away.`, `\xBFDesligar ${domain} de ${ctx.args[0]}? Deja de servir de inmediato.`));
1900
- const r = await (await sandbox2(ctx, this.usage)).removeDomain(domain);
1901
- emit(ctx, r, () => console.log(t(`Removed ${domain}`, `Quitado ${domain}`)));
1902
2881
  }
1903
2882
  }
1904
- }
2883
+ },
2884
+ defaultSub: "ls"
1905
2885
  };
1906
2886
 
1907
2887
  // src/commands/db.ts
1908
- import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
2888
+ import { existsSync as existsSync10, readFileSync as readFileSync11, statSync as statSync7 } from "fs";
1909
2889
 
1910
2890
  // src/photos.ts
1911
2891
  import { createHash as createHash2 } from "crypto";
1912
- import { readFileSync as readFileSync7, readdirSync, statSync as statSync3 } from "fs";
1913
- import { extname as extname2, join as join3 } from "path";
2892
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
2893
+ import { extname as extname3, join as join4 } from "path";
1914
2894
  var PHOTO_MAX_BYTES = 10 * 1024 * 1024;
1915
2895
  var WHATSAPP_MAX_BYTES = 5 * 1024 * 1024;
1916
2896
  function imageTypeOf(b) {
@@ -1926,10 +2906,10 @@ function imageTypeOf(b) {
1926
2906
  function photoFiles(dir) {
1927
2907
  const photos = [];
1928
2908
  const rejected = [];
1929
- for (const name of readdirSync(dir).sort()) {
2909
+ for (const name of readdirSync2(dir).sort()) {
1930
2910
  if (name.startsWith(".")) continue;
1931
- const path = join3(dir, name);
1932
- const st = statSync3(path);
2911
+ const path = join4(dir, name);
2912
+ const st = statSync5(path);
1933
2913
  if (!st.isFile()) continue;
1934
2914
  if (st.size === 0) {
1935
2915
  rejected.push({ file: name, reason: "empty" });
@@ -1939,13 +2919,13 @@ function photoFiles(dir) {
1939
2919
  rejected.push({ file: name, reason: "too_large" });
1940
2920
  continue;
1941
2921
  }
1942
- const bytes = readFileSync7(path);
2922
+ const bytes = readFileSync9(path);
1943
2923
  const type = imageTypeOf(bytes);
1944
2924
  if (!type) {
1945
2925
  rejected.push({ file: name, reason: "not_an_image" });
1946
2926
  continue;
1947
2927
  }
1948
- const key = name.slice(0, name.length - extname2(name).length);
2928
+ const key = name.slice(0, name.length - extname3(name).length);
1949
2929
  photos.push({ key, file: name, path, ...type, bytes: st.size, sha: createHash2("sha256").update(bytes).digest("hex") });
1950
2930
  }
1951
2931
  return { photos, rejected };
@@ -2000,8 +2980,8 @@ function parseLinkStats(columns, row) {
2000
2980
  }
2001
2981
 
2002
2982
  // src/commands/agents-config.ts
2003
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
2004
- import { basename as basename3, dirname, join as join4, relative, resolve } from "path";
2983
+ import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync6, writeFileSync as writeFileSync4 } from "fs";
2984
+ import { basename as basename4, dirname, join as join5, relative as relative2, resolve } from "path";
2005
2985
  var WORK_DIR = "/data/work";
2006
2986
  var MACHINE_TEMPLATES = /* @__PURE__ */ new Set(["ghosty-lite", "goose"]);
2007
2987
  var SKILLS_DIR = "/data/agent/skills";
@@ -2080,8 +3060,8 @@ function readLocalText(path, usage2) {
2080
3060
  if (process.stdin.isTTY) throw usageError(t("Expected the content on stdin.", "Se esperaba el contenido por stdin."), usage2);
2081
3061
  return readStdin().then((b) => b.toString("utf8"));
2082
3062
  }
2083
- if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2084
- return readFileSync8(path, "utf8");
3063
+ if (!existsSync9(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
3064
+ return readFileSync10(path, "utf8");
2085
3065
  }
2086
3066
  function parseMcpFile(text, usage2) {
2087
3067
  let j;
@@ -2297,8 +3277,8 @@ var files2 = {
2297
3277
  if (!rest.length) throw usageError(t("Give one or more local files.", "Pasa uno o m\xE1s archivos locales."), FILES_USAGE2);
2298
3278
  const to = (str(ctx, "to") ?? "").replace(/^\/+|\/+$/g, "");
2299
3279
  const plan = rest.map((local) => {
2300
- if (!existsSync7(local) || !statSync4(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
2301
- return { local, path: to ? `${to}/${basename3(local)}` : basename3(local), bytes: statSync4(local).size };
3280
+ if (!existsSync9(local) || !statSync6(local).isFile()) throw usageError(t(`File not found: ${local}`, `No existe el archivo: ${local}`), FILES_USAGE2);
3281
+ return { local, path: to ? `${to}/${basename4(local)}` : basename4(local), bytes: statSync6(local).size };
2302
3282
  });
2303
3283
  if (bool(ctx, "dry-run")) {
2304
3284
  emit(ctx, { agentId: id, dryRun: true, upload: plan }, () => {
@@ -2310,7 +3290,7 @@ var files2 = {
2310
3290
  }
2311
3291
  const done = [];
2312
3292
  for (const p of plan) {
2313
- const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync8(p.local), { raw: true });
3293
+ const r = await api(ctx, "PUT", `/agents/${id}/files/${encodePath(p.path)}`, readFileSync10(p.local), { raw: true });
2314
3294
  done.push({ local: p.local, ...r });
2315
3295
  if (!ctx.json) console.log(`${t("Uploaded", "Subido")} ${p.local} \u2192 ${WORK_DIR}/${p.path} (${fmtBytes(p.bytes)})`);
2316
3296
  }
@@ -2337,11 +3317,11 @@ var SKILLS_USAGE = "easybits agents skills <ls|get|add|rm> <agent> [slug...] [--
2337
3317
  function readSkillDir(dir) {
2338
3318
  const assets = [];
2339
3319
  const visit = (d) => {
2340
- for (const e of readdirSync2(d, { withFileTypes: true })) {
3320
+ for (const e of readdirSync3(d, { withFileTypes: true })) {
2341
3321
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2342
- const p = join4(d, e.name);
3322
+ const p = join5(d, e.name);
2343
3323
  if (e.isDirectory()) visit(p);
2344
- else if (e.isFile() && relative(dir, p) !== "SKILL.md") assets.push({ name: relative(dir, p).split("\\").join("/"), contentBase64: readFileSync8(p).toString("base64") });
3324
+ else if (e.isFile() && relative2(dir, p) !== "SKILL.md") assets.push({ name: relative2(dir, p).split("\\").join("/"), contentBase64: readFileSync10(p).toString("base64") });
2345
3325
  }
2346
3326
  };
2347
3327
  visit(dir);
@@ -2394,22 +3374,22 @@ var skills = {
2394
3374
  }
2395
3375
  const s = await readSkill(box, entry);
2396
3376
  mkdirSync3(out, { recursive: true });
2397
- writeFileSync4(join4(out, "SKILL.md"), s.markdown);
3377
+ writeFileSync4(join5(out, "SKILL.md"), s.markdown);
2398
3378
  for (const a of s.assets) {
2399
- mkdirSync3(dirname(join4(out, a.name)), { recursive: true });
2400
- writeFileSync4(join4(out, a.name), Buffer.from(a.contentBase64, "base64"));
3379
+ mkdirSync3(dirname(join5(out, a.name)), { recursive: true });
3380
+ writeFileSync4(join5(out, a.name), Buffer.from(a.contentBase64, "base64"));
2401
3381
  }
2402
3382
  emit(ctx, { slug, out: resolve(out), files: entry.files }, () => console.log(t(`Saved ${entry.files.length} file(s) to ${resolve(out)}`, `Guard\xE9 ${entry.files.length} archivo(s) en ${resolve(out)}`)));
2403
3383
  } else if (action === "add" || action === "put") {
2404
3384
  const file = str(ctx, "file");
2405
3385
  const dir = str(ctx, "dir");
2406
3386
  if (!!file === !!dir) throw usageError(t("Pass --file SKILL.md or --dir <folder>.", "Pasa --file SKILL.md o --dir <carpeta>."), SKILLS_USAGE);
2407
- const mdPath = file ?? join4(dir, "SKILL.md");
2408
- if (!existsSync7(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
2409
- const markdown = readFileSync8(mdPath, "utf8");
3387
+ const mdPath = file ?? join5(dir, "SKILL.md");
3388
+ if (!existsSync9(mdPath)) throw usageError(t(`File not found: ${mdPath}`, `No existe el archivo: ${mdPath}`), SKILLS_USAGE);
3389
+ const markdown = readFileSync10(mdPath, "utf8");
2410
3390
  const fm = skillFrontmatter(markdown);
2411
3391
  if (fm.error) throw usageError(`${mdPath}: ${fm.error}`, SKILLS_USAGE);
2412
- const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename3(resolve(dir ?? dirname(mdPath))).toLowerCase());
3392
+ const slug = str(ctx, "slug") ?? (fm.name && SLUG_RE.test(fm.name) ? fm.name : basename4(resolve(dir ?? dirname(mdPath))).toLowerCase());
2413
3393
  if (!SLUG_RE.test(slug)) throw usageError(t(`Invalid slug "${slug}" ([a-z0-9-], max 64). Pass --slug.`, `Slug inv\xE1lido "${slug}" ([a-z0-9-], m\xE1x 64). Pasa --slug.`), SKILLS_USAGE);
2414
3394
  const assets = dir ? readSkillDir(dir) : [];
2415
3395
  const plan = { agentId: id, slug, files: ["SKILL.md", ...assets.map((a) => a.name)], restart: bool(ctx, "restart") };
@@ -2673,10 +3653,10 @@ var exportAgent = {
2673
3653
  }
2674
3654
  };
2675
3655
  function readExport(path, usage2) {
2676
- if (!existsSync7(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
3656
+ if (!existsSync9(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
2677
3657
  let x;
2678
3658
  try {
2679
- x = JSON.parse(readFileSync8(path, "utf8"));
3659
+ x = JSON.parse(readFileSync10(path, "utf8"));
2680
3660
  } catch (e) {
2681
3661
  throw usageError(t(`${path} is not JSON: ${e.message}`, `${path} no es JSON: ${e.message}`), usage2);
2682
3662
  }
@@ -2978,7 +3958,7 @@ var db = {
2978
3958
  const keyCol = str(ctx, "key-column");
2979
3959
  const dir = str(ctx, "dir");
2980
3960
  if (!tableName || !keyCol || !dir) throw usageError(t("Missing --table, --key-column or --dir.", "Faltan --table, --key-column o --dir."), this.usage);
2981
- if (!existsSync8(dir) || !statSync5(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
3961
+ if (!existsSync10(dir) || !statSync7(dir).isDirectory()) throw usageError(t(`Not a folder: ${dir}`, `No es una carpeta: ${dir}`), this.usage);
2982
3962
  const { photos, rejected } = photoFiles(dir);
2983
3963
  const eb = await getClient(ctx);
2984
3964
  const hit = await findDb(ctx, ref);
@@ -2992,8 +3972,8 @@ var db = {
2992
3972
  if (!cols.includes(column)) throw usageError(t(`No column "${column}" in ${tableName} (${cols.join(", ")}).`, `No hay columna "${column}" en ${tableName} (${cols.join(", ")}).`), this.usage);
2993
3973
  const current2 = /* @__PURE__ */ new Map();
2994
3974
  for (let i = 0; i < photos.length; i += 100) {
2995
- const keys = photos.slice(i, i + 100).map((p) => p.key);
2996
- const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys.map(() => "?").join(",")})`, keys);
3975
+ const keys2 = photos.slice(i, i + 100).map((p) => p.key);
3976
+ const r = await q(`SELECT ${ident2(keyCol)}, ${ident2(column)} FROM ${ident2(tableName)} WHERE ${ident2(keyCol)} IN (${keys2.map(() => "?").join(",")})`, keys2);
2997
3977
  for (const row of r.rows) current2.set(String(row[0]), row[1]);
2998
3978
  }
2999
3979
  const plan = planPhotos(photos, current2, bool(ctx, "replace"));
@@ -3024,7 +4004,7 @@ var db = {
3024
4004
  let url = urls.get(photo.sha);
3025
4005
  if (!url) {
3026
4006
  const up = await eb.uploadFile({ fileName: `${photo.sha.slice(0, 32)}.${photo.ext}`, contentType: photo.mime, size: photo.bytes, access: "public" });
3027
- const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync9(photo.path), headers: { "Content-Type": photo.mime } });
4007
+ const res = await fetch(up.putUrl, { method: "PUT", body: readFileSync11(photo.path), headers: { "Content-Type": photo.mime } });
3028
4008
  if (!res.ok) throw new Error(t(`storage answered ${res.status}`, `el almacenamiento contest\xF3 ${res.status}`));
3029
4009
  url = up.file.url;
3030
4010
  if (!url) throw new Error(t("the API returned no public link", "la API no devolvi\xF3 liga p\xFAblica"));
@@ -3086,48 +4066,48 @@ var db = {
3086
4066
  };
3087
4067
 
3088
4068
  // src/commands/agents.ts
3089
- import { existsSync as existsSync11, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
4069
+ import { existsSync as existsSync13, readFileSync as readFileSync14, statSync as statSync9 } from "fs";
3090
4070
 
3091
4071
  // src/commands/spec.ts
3092
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync11, statSync as statSync6, writeFileSync as writeFileSync5 } from "fs";
4072
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync13, statSync as statSync8, writeFileSync as writeFileSync5 } from "fs";
3093
4073
  import { homedir as homedir3 } from "os";
3094
- import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
4074
+ import { dirname as dirname2, join as join7, resolve as resolve2 } from "path";
3095
4075
 
3096
4076
  // src/specfile.ts
3097
- import { existsSync as existsSync9, readFileSync as readFileSync10, readdirSync as readdirSync3 } from "fs";
3098
- import { join as join5, relative as relative2 } from "path";
4077
+ import { existsSync as existsSync11, readFileSync as readFileSync12, readdirSync as readdirSync4 } from "fs";
4078
+ import { join as join6, relative as relative3 } from "path";
3099
4079
  function walkLocal(root) {
3100
4080
  const out = [];
3101
4081
  const visit = (d) => {
3102
- for (const e of readdirSync3(d, { withFileTypes: true })) {
4082
+ for (const e of readdirSync4(d, { withFileTypes: true })) {
3103
4083
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
3104
- const p = join5(d, e.name);
4084
+ const p = join6(d, e.name);
3105
4085
  if (e.isDirectory()) visit(p);
3106
- else if (e.isFile()) out.push(relative2(root, p).split("\\").join("/"));
4086
+ else if (e.isFile()) out.push(relative3(root, p).split("\\").join("/"));
3107
4087
  }
3108
4088
  };
3109
- if (existsSync9(root)) visit(root);
4089
+ if (existsSync11(root)) visit(root);
3110
4090
  return out;
3111
4091
  }
3112
4092
  function readUploads(dir) {
3113
4093
  const skills2 = {};
3114
- const skillsRoot = join5(dir, "skills");
3115
- if (existsSync9(skillsRoot)) {
3116
- for (const e of readdirSync3(skillsRoot, { withFileTypes: true })) {
4094
+ const skillsRoot = join6(dir, "skills");
4095
+ if (existsSync11(skillsRoot)) {
4096
+ for (const e of readdirSync4(skillsRoot, { withFileTypes: true })) {
3117
4097
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
3118
- skills2[e.name] = walkLocal(join5(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync10(join5(skillsRoot, e.name, path)).toString("base64") }));
4098
+ skills2[e.name] = walkLocal(join6(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync12(join6(skillsRoot, e.name, path)).toString("base64") }));
3119
4099
  }
3120
4100
  }
3121
- const filesRoot = join5(dir, "files");
3122
- const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync10(join5(filesRoot, p)).toString("base64")]));
4101
+ const filesRoot = join6(dir, "files");
4102
+ const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync12(join6(filesRoot, p)).toString("base64")]));
3123
4103
  return { ...Object.keys(skills2).length ? { skills: skills2 } : {}, ...Object.keys(files3).length ? { files: files3 } : {} };
3124
4104
  }
3125
4105
  function secretsFor(text, env = process.env) {
3126
4106
  const names = [...new Set([...text.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]))];
3127
- const secrets = {};
4107
+ const secrets2 = {};
3128
4108
  const missing = [];
3129
- for (const n of names) env[n] !== void 0 ? secrets[n] = env[n] : missing.push(n);
3130
- return { secrets, missing };
4109
+ for (const n of names) env[n] !== void 0 ? secrets2[n] = env[n] : missing.push(n);
4110
+ return { secrets: secrets2, missing };
3131
4111
  }
3132
4112
  function specId(text) {
3133
4113
  return /^id:\s*["']?([^"'\s#]+)/m.exec(text)?.[1] ?? /^\s{0,2}"id"\s*:\s*"([^"]+)"/m.exec(text)?.[1];
@@ -3172,17 +4152,17 @@ Ed\xEDtalo y luego: easybits apply ${out} --dry-run`)));
3172
4152
  const yaml = await getYaml();
3173
4153
  const dir = resolve2(out);
3174
4154
  mkdirSync4(dir, { recursive: true });
3175
- writeFileSync5(join6(dir, "agent.yaml"), yaml);
4155
+ writeFileSync5(join7(dir, "agent.yaml"), yaml);
3176
4156
  const x = await buildExport(ctx, id, { secrets: false, fileContents: true });
3177
4157
  const put = (path, b64) => {
3178
4158
  mkdirSync4(dirname2(path), { recursive: true });
3179
4159
  writeFileSync5(path, Buffer.from(b64, "base64"));
3180
4160
  };
3181
4161
  for (const s of x.skills ?? []) {
3182
- writeFileSync5((mkdirSync4(join6(dir, "skills", s.slug), { recursive: true }), join6(dir, "skills", s.slug, "SKILL.md")), s.markdown);
3183
- for (const a of s.assets) put(join6(dir, "skills", s.slug, a.name), a.contentBase64);
4162
+ writeFileSync5((mkdirSync4(join7(dir, "skills", s.slug), { recursive: true }), join7(dir, "skills", s.slug, "SKILL.md")), s.markdown);
4163
+ for (const a of s.assets) put(join7(dir, "skills", s.slug, a.name), a.contentBase64);
3184
4164
  }
3185
- for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join6(dir, "files", f.path), f.contentBase64);
4165
+ for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join7(dir, "files", f.path), f.contentBase64);
3186
4166
  const summary = { saved: dir, skills: (x.skills ?? []).map((s) => s.slug), files: (x.files ?? []).map((f) => f.path) };
3187
4167
  emit(
3188
4168
  ctx,
@@ -3195,30 +4175,30 @@ ${t("Edit it, then", "Ed\xEDtalo y luego")}: easybits apply ${out} --dry-run`
3195
4175
  }
3196
4176
  };
3197
4177
  function readTarget(target, usage2) {
3198
- if (!existsSync10(target)) throw usageError(t(`Not found: ${target}`, `No existe: ${target}`), usage2);
3199
- if (statSync6(target).isDirectory()) {
3200
- const name = SPEC_NAMES.find((n) => existsSync10(join6(target, n)));
4178
+ if (!existsSync12(target)) throw usageError(t(`Not found: ${target}`, `No existe: ${target}`), usage2);
4179
+ if (statSync8(target).isDirectory()) {
4180
+ const name = SPEC_NAMES.find((n) => existsSync12(join7(target, n)));
3201
4181
  if (!name) throw usageError(t(`${target} has no agent.yaml.`, `${target} no tiene agent.yaml.`), usage2);
3202
- return { text: readFileSync11(join6(target, name), "utf8"), dir: target };
4182
+ return { text: readFileSync13(join7(target, name), "utf8"), dir: target };
3203
4183
  }
3204
- return { text: readFileSync11(target, "utf8") };
4184
+ return { text: readFileSync13(target, "utf8") };
3205
4185
  }
3206
4186
  var line = (c) => `${c.op} ${c.what}`;
3207
4187
  var printPlan = (plan) => console.log(plan.map(line).join("\n"));
3208
4188
  function saveBackup(id, yaml) {
3209
- const dir = join6(homedir3(), ".easybits", "backups");
4189
+ const dir = join7(homedir3(), ".easybits", "backups");
3210
4190
  mkdirSync4(dir, { recursive: true });
3211
- const path = join6(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
4191
+ const path = join7(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
3212
4192
  writeFileSync5(path, yaml);
3213
4193
  return path;
3214
4194
  }
3215
4195
  async function applyTarget(ctx, target, o) {
3216
4196
  const { text, dir } = readTarget(target, o.usage);
3217
- const { secrets, missing } = secretsFor(text);
4197
+ const { secrets: secrets2, missing } = secretsFor(text);
3218
4198
  const uploads = dir ? readUploads(dir) : {};
3219
4199
  if (o.create) {
3220
4200
  if (o.agent) throw usageError(t("Use --agent or --create, not both.", "Usa --agent o --create, no los dos."), o.usage);
3221
- const body2 = { spec: text, secrets, ...o.name ? { name: o.name } : {} };
4201
+ const body2 = { spec: text, secrets: secrets2, ...o.name ? { name: o.name } : {} };
3222
4202
  const plan2 = await api(ctx, "POST", "/agents/apply", { ...body2, dryRun: true });
3223
4203
  if (o.dryRun) {
3224
4204
  emit(ctx, { create: true, ...plan2, ...uploads.skills ? { skills: Object.keys(uploads.skills) } : {}, ...uploads.files ? { files: Object.keys(uploads.files) } : {} }, () => {
@@ -3239,7 +4219,7 @@ async function applyTarget(ctx, target, o) {
3239
4219
  if (/\n?(skills|files):/m.test(text) || uploads.skills || uploads.files) {
3240
4220
  await waitMachine(ctx, id2).catch(() => {
3241
4221
  });
3242
- rest = await api(ctx, "POST", `/agents/${id2}/apply`, { spec: text, secrets, ...uploads, ...o.name ? { name: o.name } : {} });
4222
+ rest = await api(ctx, "POST", `/agents/${id2}/apply`, { spec: text, secrets: secrets2, ...uploads, ...o.name ? { name: o.name } : {} });
3243
4223
  }
3244
4224
  const extra = (rest?.plan ?? []).filter((c) => c.op !== "!" && !/^(prompt|env)\b/.test(c.what));
3245
4225
  const res = { agentId: id2, sandboxId: created.sandboxId, plan: [...created.plan, ...extra], applied: rest?.applied, failed: rest?.failed };
@@ -3258,7 +4238,7 @@ async function applyTarget(ctx, target, o) {
3258
4238
  `, `No est\xE1n en tu entorno (conservan su valor de hoy): ${missing.join(", ")}
3259
4239
  `));
3260
4240
  const path = `/agents/${id}/apply`;
3261
- const body = { spec: text, secrets, ...uploads, ...o.prune ? { prune: true } : {} };
4241
+ const body = { spec: text, secrets: secrets2, ...uploads, ...o.prune ? { prune: true } : {} };
3262
4242
  const plan = await api(ctx, "POST", path, { ...body, dryRun: true });
3263
4243
  const actionable = plan.plan.filter((c) => c.op !== "!");
3264
4244
  if (o.dryRun || !actionable.length) {
@@ -3324,7 +4304,7 @@ var apply = {
3324
4304
  // src/commands/agents.ts
3325
4305
  function isLegacyExport(path) {
3326
4306
  try {
3327
- return existsSync11(path) && !statSync7(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync12(path, "utf8"));
4307
+ return existsSync13(path) && !statSync9(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync14(path, "utf8"));
3328
4308
  } catch {
3329
4309
  return false;
3330
4310
  }
@@ -3391,9 +4371,9 @@ var agents = {
3391
4371
  if (promptInline != null && promptFile) throw usageError(t("Use --prompt or --prompt-file, not both.", "Usa --prompt o --prompt-file, no los dos."), this.usage);
3392
4372
  const mode = str(ctx, "prompt-mode");
3393
4373
  if (mode && mode !== "append" && mode !== "replace") throw usageError(t("--prompt-mode must be append or replace.", "--prompt-mode debe ser append o replace."), this.usage);
3394
- if (promptFile && !existsSync11(promptFile)) throw usageError(t(`File not found: ${promptFile}`, `No existe el archivo: ${promptFile}`), this.usage);
4374
+ if (promptFile && !existsSync13(promptFile)) throw usageError(t(`File not found: ${promptFile}`, `No existe el archivo: ${promptFile}`), this.usage);
3395
4375
  const mcpFile = str(ctx, "mcp-file");
3396
- if (mcpFile && !existsSync11(mcpFile)) throw usageError(t(`File not found: ${mcpFile}`, `No existe el archivo: ${mcpFile}`), this.usage);
4376
+ if (mcpFile && !existsSync13(mcpFile)) throw usageError(t(`File not found: ${mcpFile}`, `No existe el archivo: ${mcpFile}`), this.usage);
3397
4377
  if (bool(ctx, "copy-files") && !like && !from) throw usageError(t("--copy-files needs --like or --from.", "--copy-files necesita --like o --from."), this.usage);
3398
4378
  if (from && !isLegacyExport(from)) {
3399
4379
  await applyTarget(ctx, from, { create: true, name: str(ctx, "name"), dryRun: bool(ctx, "dry-run"), usage: this.usage });
@@ -3403,7 +4383,7 @@ var agents = {
3403
4383
  const template = str(ctx, "template") ?? source?.template;
3404
4384
  if (!template) throw usageError(t("Missing --template (or --like / --from).", "Falta --template (o --like / --from)."), this.usage);
3405
4385
  const env = await envFrom(ctx, this.usage);
3406
- const prompt = promptInline ?? (promptFile ? readFileSync12(promptFile, "utf8") : source?.systemPrompt);
4386
+ const prompt = promptInline ?? (promptFile ? readFileSync14(promptFile, "utf8") : source?.systemPrompt);
3407
4387
  const promptMode = mode ?? source?.systemPromptMode;
3408
4388
  const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
3409
4389
  const promptAfter = multiline ? prompt : void 0;
@@ -3412,7 +4392,7 @@ var agents = {
3412
4392
  }
3413
4393
  if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
3414
4394
  if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
3415
- const mcpServers = mcpFile ? parseMcpFile(readFileSync12(mcpFile, "utf8"), this.usage) : source?.mcpServers;
4395
+ const mcpServers = mcpFile ? parseMcpFile(readFileSync14(mcpFile, "utf8"), this.usage) : source?.mcpServers;
3416
4396
  if (mcpServers && hasMasked(mcpServers)) {
3417
4397
  throw usageError(t(`${from ?? "the source"} has masked MCP values (\u2026***).`, `${from ?? "la fuente"} trae valores del MCP tapados (\u2026***).`), t("Re-export with --show-secrets, or pass --mcp-file", "Vuelve a exportar con --show-secrets, o pasa --mcp-file"));
3418
4398
  }
@@ -3508,10 +4488,201 @@ var agents = {
3508
4488
  }
3509
4489
  };
3510
4490
 
4491
+ // src/commands/fleet.ts
4492
+ import { randomUUID } from "crypto";
4493
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
4494
+ var fleetRecord = (p) => ({
4495
+ id: p.id,
4496
+ name: p.name ?? null,
4497
+ assistantName: p.assistantName ?? null,
4498
+ workerTemplate: p.workerTemplate ?? null,
4499
+ hasOwnNumber: !!p.hasOwnNumber,
4500
+ createdAt: p.createdAt ?? null
4501
+ });
4502
+ var CONFIGURE_USAGE = "easybits fleet configure <agent> [--name <name>] [--prompt-file <file|->] [--engine <id>] [--model <id>] [--effort <level>] [--own-number on|off] [--recycle]";
4503
+ function configureActions(f) {
4504
+ const out = [];
4505
+ if (f.name != null) out.push({ action: "set-name", name: f.name });
4506
+ if (f.prompt != null) out.push({ action: "set-agent-prompt", systemPrompt: f.prompt });
4507
+ if (f.engine != null) out.push({ action: "set-engine", engine: f.engine });
4508
+ if (f.model != null) out.push({ action: "set-model", model: f.model });
4509
+ if (f.effort != null) {
4510
+ if (!["low", "medium", "high", "xhigh", "max"].includes(f.effort)) throw usageError(t("--effort is low, medium, high, xhigh or max.", "--effort es low, medium, high, xhigh o max."), CONFIGURE_USAGE);
4511
+ out.push({ action: "set-effort", effort: f.effort });
4512
+ }
4513
+ if (f.ownNumber != null) {
4514
+ if (f.ownNumber !== "on" && f.ownNumber !== "off") throw usageError(t("--own-number is on or off.", "--own-number es on u off."), CONFIGURE_USAGE);
4515
+ out.push({ action: "toggle-own-number", on: f.ownNumber === "on" });
4516
+ }
4517
+ if (f.recycle) out.push({ action: "recycle-box" });
4518
+ if (!out.length) throw usageError(t("Nothing to change: pass a flag (--name, --prompt-file, --model\u2026).", "Nada que cambiar: pasa una bandera (--name, --prompt-file, --model\u2026)."), CONFIGURE_USAGE);
4519
+ return out;
4520
+ }
4521
+ async function readText(path, usage2) {
4522
+ if (path === "-") return (await readStdin()).toString("utf8");
4523
+ if (!existsSync14(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
4524
+ return readFileSync15(path, "utf8");
4525
+ }
4526
+ var caps = (ctx, id, q) => api(ctx, "GET", `/fleet-agents/${encodeURIComponent(id)}/capabilities${q ? `?q=${encodeURIComponent(q)}` : ""}`);
4527
+ var fleet = {
4528
+ name: "fleet",
4529
+ aliases: ["flota"],
4530
+ group: "Compute",
4531
+ summary: "Fleet agents: one agent, many conversations (WhatsApp, WABA, Teams, web)",
4532
+ synopsis: "fleet <ls|get|\u2026>",
4533
+ subs: {
4534
+ ls: {
4535
+ aliases: ["list"],
4536
+ summary: "List your fleet agents",
4537
+ usage: "easybits fleet ls",
4538
+ examples: ["easybits fleet ls", "easybits fleet ls --json"],
4539
+ async run(ctx) {
4540
+ const { pools } = await api(ctx, "GET", "/fleet-agents");
4541
+ const rows = pools.map(fleetRecord);
4542
+ emit(
4543
+ ctx,
4544
+ rows,
4545
+ () => table(
4546
+ rows.map((r) => ({ ...r, createdAt: fmtDate(r.createdAt) })),
4547
+ [["id", "ID"], ["name", "NAME"], ["workerTemplate", "ENGINE"], ["createdAt", "CREATED"]],
4548
+ t("No fleet agents yet. Create one at https://www.easybits.cloud/dash/flota", "Todav\xEDa no hay agentes de flota. Crea uno en https://www.easybits.cloud/dash/flota")
4549
+ )
4550
+ );
4551
+ }
4552
+ },
4553
+ get: {
4554
+ summary: "Show a fleet agent: engine, model, effort, prompt and channels",
4555
+ usage: "easybits fleet get <agent> [--prompt-out <file>]",
4556
+ options: { "prompt-out": { type: "string", value: "file", description: "Write the system prompt to a file instead of printing it" } },
4557
+ examples: ["easybits fleet get soporte", "easybits fleet get soporte --json", "easybits fleet get soporte --prompt-out PROMPT.md"],
4558
+ async run(ctx) {
4559
+ const id = need(ctx, 0, "agent", this.usage);
4560
+ const [{ pools }, view] = await Promise.all([api(ctx, "GET", "/fleet-agents"), caps(ctx, id)]);
4561
+ const rec = fleetRecord(pools.find((p) => p.id === id) ?? { id });
4562
+ const out = { ...rec, ...view.agent, engine: view.engine, channels: view.channels.map((c) => c.key), skills: view.skills, customMcps: view.customMcps };
4563
+ const promptOut = str(ctx, "prompt-out");
4564
+ if (promptOut) {
4565
+ const { writeFileSync: writeFileSync11 } = await import("fs");
4566
+ writeFileSync11(promptOut, view.agent.systemPrompt);
4567
+ out.systemPrompt = { file: promptOut, bytes: Buffer.byteLength(view.agent.systemPrompt) };
4568
+ }
4569
+ emit(ctx, out, () => {
4570
+ console.log(`ID: ${rec.id}${rec.name ? ` (${rec.name})` : ""}`);
4571
+ console.log(`${t("Engine: ", "Motor: ")} ${view.engine ?? view.agent.workerTemplate}${view.agent.modelLabel ? ` \xB7 ${view.agent.modelLabel}` : ""} \xB7 effort ${view.agent.effort}`);
4572
+ console.log(`${t("Number: ", "N\xFAmero: ")} ${view.agent.hasOwnNumber ? t("dedicated", "dedicado") : t("shared (replies carry the name prefix)", "compartido (las respuestas llevan el nombre)")}`);
4573
+ console.log(`${t("Channels:", "Canales: ")} ${view.channels.map((c) => c.key).join(", ")}`);
4574
+ if (view.skills.length) console.log(`Skills: ${view.skills.map((s) => `${s.name}${s.enabled ? "" : " (off)"}`).join(", ")}`);
4575
+ if (view.customMcps.length) console.log(`MCP: ${view.customMcps.map((m) => m.name).join(", ")}`);
4576
+ if (promptOut) console.log(t(`Prompt written to ${promptOut}`, `Prompt escrito en ${promptOut}`));
4577
+ else console.log(`
4578
+ ${view.agent.systemPrompt || t("(no system prompt)", "(sin system prompt)")}`);
4579
+ });
4580
+ }
4581
+ },
4582
+ capabilities: {
4583
+ aliases: ["caps"],
4584
+ summary: "The catalog and current state: engines, models, tool buckets, MCPs, skills, channels",
4585
+ usage: "easybits fleet capabilities <agent> [--q <text>]",
4586
+ options: { q: { type: "string", value: "text", description: "Filter the capabilities catalog" } },
4587
+ examples: ["easybits fleet capabilities soporte --json", "easybits fleet caps soporte --q calendar"],
4588
+ async run(ctx) {
4589
+ const id = need(ctx, 0, "agent", this.usage);
4590
+ const view = await caps(ctx, id, str(ctx, "q"));
4591
+ emit(ctx, view, () => {
4592
+ console.log(`${t("Engines:", "Motores:")} ${view.engines.map((e) => `${e.id}${e.id === view.engine ? "*" : ""}${e.ready ? "" : " (soon)"}`).join(", ")}`);
4593
+ console.log(`${t("Models: ", "Modelos:")} ${view.models.map((m) => `${m.key}${m.key === view.agent.model ? "*" : ""}`).join(", ") || "-"}`);
4594
+ console.log(`Effort: ${view.efforts.map((e) => `${e}${e === view.agent.effort ? "*" : ""}`).join(", ")}`);
4595
+ console.log(`Buckets: ${view.agent.buckets.join(", ") || "-"}`);
4596
+ console.log(`${t("Built-in:", "Incluidos:")} ${view.builtins.map((b) => b.name).join(", ")}`);
4597
+ console.log(`${t("Channels:", "Canales:")} ${view.channels.map((c) => c.key).join(", ")}`);
4598
+ console.log(`${t("Secrets: ", "Secretos:")} ${view.secretsPresent.join(", ") || "-"}`);
4599
+ console.log(t("(* = current. --json has the full view: per-channel config, buckets and their tools.)", "(* = el actual. --json trae la vista completa: config por canal, buckets y sus tools.)"));
4600
+ });
4601
+ }
4602
+ },
4603
+ configure: {
4604
+ aliases: ["set"],
4605
+ summary: "Change name, system prompt, engine, model, effort or number mode; --recycle applies it now",
4606
+ usage: CONFIGURE_USAGE,
4607
+ options: {
4608
+ name: { type: "string", value: "name", description: "Display name and the name the agent goes by" },
4609
+ "prompt-file": { type: "string", value: "file|-", description: "System prompt for every channel (- reads stdin)" },
4610
+ engine: { type: "string", value: "id", description: "Engine (claude, deepseek, codex\u2026); recycles its boxes by itself" },
4611
+ model: { type: "string", value: "id", description: "Model of the current engine (see fleet capabilities)" },
4612
+ effort: { type: "string", value: "level", description: "low, medium, high, xhigh or max" },
4613
+ "own-number": { type: "string", value: "on|off", description: "on: dedicated number (no name prefix on replies)" },
4614
+ recycle: { type: "boolean", description: "Recycle its running boxes so the change applies on the next turn" }
4615
+ },
4616
+ examples: [
4617
+ "easybits fleet configure soporte --prompt-file PROMPT.md",
4618
+ "easybits fleet configure soporte --model claude-sonnet-5-5 --effort high --recycle",
4619
+ "easybits fleet configure soporte --name Nik --own-number on"
4620
+ ],
4621
+ async run(ctx) {
4622
+ const id = need(ctx, 0, "agent", CONFIGURE_USAGE);
4623
+ const promptFile = str(ctx, "prompt-file");
4624
+ const actions = configureActions({
4625
+ name: str(ctx, "name"),
4626
+ prompt: promptFile ? await readText(promptFile, CONFIGURE_USAGE) : void 0,
4627
+ engine: str(ctx, "engine"),
4628
+ model: str(ctx, "model"),
4629
+ effort: str(ctx, "effort"),
4630
+ ownNumber: str(ctx, "own-number"),
4631
+ recycle: bool(ctx, "recycle")
4632
+ });
4633
+ const applied = [];
4634
+ for (const a of actions) {
4635
+ await api(ctx, "POST", `/fleet-agents/${encodeURIComponent(id)}/capabilities`, a);
4636
+ applied.push(String(a.action));
4637
+ }
4638
+ emit(ctx, { ok: true, applied }, () => {
4639
+ console.log(t(`Applied: ${applied.join(", ")}`, `Aplicado: ${applied.join(", ")}`));
4640
+ if (!applied.includes("recycle-box") && !applied.includes("set-engine")) {
4641
+ console.log(t("Sleeping boxes pick it up when they wake; a running one when it sleeps (or pass --recycle).", "Las cajas dormidas lo toman al despertar; una corriendo, al dormirse (o pasa --recycle)."));
4642
+ }
4643
+ });
4644
+ }
4645
+ },
4646
+ message: {
4647
+ summary: "Send one message to a fleet agent and print its reply",
4648
+ usage: "easybits fleet message <agent> <text|-> [--group <id>] [--config-group <id>] [--sender <who>] [--timezone <tz>]",
4649
+ options: {
4650
+ group: { type: "string", value: "id", description: "Conversation id; reuse it to continue (default: a new cli-<uuid>)" },
4651
+ "config-group": { type: "string", value: "id", description: "Channel whose config (tools, MCP, prompt) applies, e.g. web or waba:<id>" },
4652
+ sender: { type: "string", value: "who", description: "Who is writing (phone, user id)" },
4653
+ timezone: { type: "string", value: "tz", description: "IANA timezone for the turn, e.g. America/Mexico_City" }
4654
+ },
4655
+ examples: [
4656
+ 'easybits fleet message soporte "\xBFqu\xE9 horarios tienen?" --config-group web',
4657
+ 'easybits fleet message soporte "y el s\xE1bado?" --group cli-1234 --config-group web # same conversation',
4658
+ "cat pregunta.txt | easybits fleet message soporte - --json"
4659
+ ],
4660
+ async run(ctx) {
4661
+ const id = need(ctx, 0, "agent", this.usage);
4662
+ let text = ctx.args.slice(1).join(" ");
4663
+ if (text === "-") text = (await readStdin()).toString("utf8").trim();
4664
+ if (!text) throw usageError(t("Missing <text>.", "Falta <text>."), this.usage);
4665
+ const groupId = str(ctx, "group") ?? `cli-${randomUUID()}`;
4666
+ const r = await api(ctx, "POST", `/fleet-agents/${encodeURIComponent(id)}/message`, {
4667
+ groupId,
4668
+ text,
4669
+ configGroupId: str(ctx, "config-group"),
4670
+ sender: str(ctx, "sender"),
4671
+ timezone: str(ctx, "timezone")
4672
+ });
4673
+ emit(ctx, { ...r, groupId }, () => {
4674
+ console.log(r.reply);
4675
+ if (!str(ctx, "group")) console.error(t(`(continue with --group ${groupId})`, `(sigue con --group ${groupId})`));
4676
+ });
4677
+ }
4678
+ }
4679
+ }
4680
+ };
4681
+
3511
4682
  // src/commands/completion.ts
3512
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
4683
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync16, writeFileSync as writeFileSync6 } from "fs";
3513
4684
  import { homedir as homedir4 } from "os";
3514
- import { dirname as dirname3, join as join7 } from "path";
4685
+ import { dirname as dirname3, join as join8 } from "path";
3515
4686
  var GLOBAL_FLAGS = ["--json", "--token", "--lang", "--help", "--version"];
3516
4687
  var AGENT_FLAGS = /* @__PURE__ */ new Set(["--agent", "--like"]);
3517
4688
  var findSub = (c, name) => Object.entries(c.subs ?? {}).find(([n, l]) => n === name || l.aliases?.includes(name));
@@ -3548,11 +4719,11 @@ function completionFor(words, current2, commands) {
3548
4719
  if (spec && spec.index === n) return { kind: "names", ref: spec.kind };
3549
4720
  return { kind: "words", words: [] };
3550
4721
  }
3551
- var cacheFile = () => join7(process.env.XDG_CACHE_HOME || join7(homedir4(), ".cache"), "easybits", "complete.json");
4722
+ var cacheFile = () => join8(process.env.XDG_CACHE_HOME || join8(homedir4(), ".cache"), "easybits", "complete.json");
3552
4723
  async function namesOf(ctx, kind) {
3553
4724
  let cache2 = {};
3554
4725
  try {
3555
- cache2 = JSON.parse(readFileSync13(cacheFile(), "utf8"));
4726
+ cache2 = JSON.parse(readFileSync16(cacheFile(), "utf8"));
3556
4727
  const c = cache2[kind];
3557
4728
  if (c && Date.now() - c.at < 5 * 6e4) return c.names;
3558
4729
  } catch {
@@ -3634,7 +4805,7 @@ var completion = {
3634
4805
 
3635
4806
  // src/commands/docs.ts
3636
4807
  import { spawn as spawn2 } from "child_process";
3637
- import { resolveBaseUrl as resolveBaseUrl5 } from "@easybits.cloud/sdk";
4808
+ import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
3638
4809
  var docs = {
3639
4810
  name: "docs",
3640
4811
  group: "Help",
@@ -3649,7 +4820,7 @@ var docs = {
3649
4820
  },
3650
4821
  examples: ["easybits docs", "easybits docs cli --en", "easybits docs hosting", "easybits docs --open"],
3651
4822
  async run(ctx) {
3652
- const base = await resolveBaseUrl5();
4823
+ const base = await resolveBaseUrl6();
3653
4824
  const prefix = bool(ctx, "en") ? "/en/docs" : "/docs";
3654
4825
  const section = ctx.args[0];
3655
4826
  if (!section) {
@@ -3679,16 +4850,16 @@ var docs = {
3679
4850
  };
3680
4851
 
3681
4852
  // src/commands/doctor.ts
3682
- import { existsSync as existsSync12, statSync as statSync8 } from "fs";
4853
+ import { existsSync as existsSync15, statSync as statSync10 } from "fs";
3683
4854
  import { homedir as homedir6 } from "os";
3684
- import { join as join9 } from "path";
3685
- import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
4855
+ import { join as join10 } from "path";
4856
+ import { resolveBaseUrl as resolveBaseUrl7 } from "@easybits.cloud/sdk";
3686
4857
 
3687
4858
  // src/update.ts
3688
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
4859
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync17, writeFileSync as writeFileSync7 } from "fs";
3689
4860
  import { homedir as homedir5 } from "os";
3690
- import { dirname as dirname4, join as join8 } from "path";
3691
- var FILE = join8(process.env.XDG_CONFIG_HOME || join8(homedir5(), ".config"), "easybits", "update.json");
4861
+ import { dirname as dirname4, join as join9 } from "path";
4862
+ var FILE = join9(process.env.XDG_CONFIG_HOME || join9(homedir5(), ".config"), "easybits", "update.json");
3692
4863
  var TTL = 60 * 60 * 1e3;
3693
4864
  var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
3694
4865
  function newer(a, b) {
@@ -3700,7 +4871,7 @@ function newer(a, b) {
3700
4871
  var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
3701
4872
  function readCache() {
3702
4873
  try {
3703
- return JSON.parse(readFileSync14(FILE, "utf8"));
4874
+ return JSON.parse(readFileSync17(FILE, "utf8"));
3704
4875
  } catch {
3705
4876
  return {};
3706
4877
  }
@@ -3739,7 +4910,7 @@ async function updateNotice(current2) {
3739
4910
  }
3740
4911
 
3741
4912
  // src/commands/doctor.ts
3742
- var VERSION = true ? "0.15.5" : "dev";
4913
+ var VERSION = true ? "0.17.0" : "dev";
3743
4914
  var doctor2 = {
3744
4915
  name: "doctor",
3745
4916
  group: "Account",
@@ -3764,7 +4935,7 @@ var doctor2 = {
3764
4935
  ...old ? { hint: `${t("Run", "Corre")}: ${UPGRADE}` } : {}
3765
4936
  });
3766
4937
  }
3767
- const base = await resolveBaseUrl6();
4938
+ const base = await resolveBaseUrl7();
3768
4939
  const health = await fetch(`${base.replace(/\/+$/, "")}/api/health`, { signal: AbortSignal.timeout(5e3) }).then((r) => ({ ok: r.ok, detail: `${base} (${r.status})` })).catch((e) => ({ ok: false, detail: `${base}: ${e.message}` }));
3769
4940
  checks.push({ check: "api", ...health, ...health.ok ? {} : { hint: t("Check your network or EASYBITS_URL.", "Revisa tu red o EASYBITS_URL.") } });
3770
4941
  const rc = readRc();
@@ -3782,9 +4953,9 @@ var doctor2 = {
3782
4953
  checks.push({ check: "credential-env", ok: true, detail: t("EASYBITS_API_KEY wins over the saved login in ~/.easybitsrc", "EASYBITS_API_KEY le gana a la sesi\xF3n guardada en ~/.easybitsrc") });
3783
4954
  }
3784
4955
  }
3785
- const rcPath = join9(homedir6(), ".easybitsrc");
3786
- if (existsSync12(rcPath)) {
3787
- const mode = statSync8(rcPath).mode & 511;
4956
+ const rcPath = join10(homedir6(), ".easybitsrc");
4957
+ if (existsSync15(rcPath)) {
4958
+ const mode = statSync10(rcPath).mode & 511;
3788
4959
  const ok2 = (mode & 63) === 0;
3789
4960
  checks.push({ check: "rc-file", ok: ok2, detail: `${rcPath} ${mode.toString(8).padStart(3, "0")}`, ...ok2 ? {} : { hint: t(`It holds tokens. Run: chmod 600 ${rcPath}`, `Guarda tokens. Corre: chmod 600 ${rcPath}`) } });
3790
4961
  }
@@ -3801,23 +4972,1254 @@ var doctor2 = {
3801
4972
  }
3802
4973
  };
3803
4974
 
3804
- // src/commands/index.ts
3805
- var COMMANDS = [
3806
- login,
3807
- logout,
3808
- whoami,
4975
+ // src/commands/web.ts
4976
+ import { writeFileSync as writeFileSync8 } from "fs";
4977
+ import { EasybitsError as EasybitsError4 } from "@easybits.cloud/sdk";
4978
+ var ENGINES = ["google", "bing", "yandex", "duckduckgo"];
4979
+ var PACKS_URL = "https://www.easybits.cloud/dash/packs?tab=web";
4980
+ async function web(fn) {
4981
+ try {
4982
+ return await fn();
4983
+ } catch (e) {
4984
+ if (e instanceof EasybitsError4 && e.status === 402) {
4985
+ const buy = /"buy"\s*:\s*"([^"]+)"/.exec(String(e.body ?? ""))?.[1] ?? PACKS_URL;
4986
+ throw new CliError(t("No web queries left.", "Ya no te quedan consultas web."), EXIT.API, t(`Buy a pack: ${buy}`, `Compra un paquete: ${buy}`), "quota_exceeded", 402);
4987
+ }
4988
+ if (e instanceof EasybitsError4 && e.status === 429) {
4989
+ throw new CliError(t("The source is cooling down that query.", "La fuente puso esa consulta en pausa."), EXIT.API, t("Try again in about a minute.", "Vuelve a intentar en un minuto."), "upstream_cooldown", 429);
4990
+ }
4991
+ throw e;
4992
+ }
4993
+ }
4994
+ function organicResults(results) {
4995
+ const r = results ?? {};
4996
+ const items = Array.isArray(r.organic) ? r.organic : Array.isArray(results) ? results : [];
4997
+ return items.map((o, i) => ({ rank: i + 1, title: (o.title ?? "").trim(), url: o.link ?? o.url ?? "" })).filter((o) => o.url);
4998
+ }
4999
+ function extractInput(inputJson, urls, usage2) {
5000
+ if (inputJson && urls.length) throw usageError(t("Pass --input or --url, not both.", "Pasa --input o --url, no ambos."), usage2);
5001
+ if (urls.length) return urls.map((url) => ({ url }));
5002
+ if (!inputJson) throw usageError(t("Missing --input <json> (or --url <url>).", "Falta --input <json> (o --url <url>)."), usage2);
5003
+ try {
5004
+ const v = JSON.parse(inputJson);
5005
+ if (v && typeof v === "object") return v;
5006
+ } catch {
5007
+ }
5008
+ throw usageError(t(`--input must be a JSON object or array, got: ${inputJson}`, `--input debe ser un objeto o arreglo JSON, lleg\xF3: ${inputJson}`), usage2);
5009
+ }
5010
+ function printJob(ctx, job) {
5011
+ emit(ctx, job, () => {
5012
+ if (job.status === "running") {
5013
+ console.log(t(`Job ${job.jobId} running (${job.source}). Check it with: easybits web extract-status ${job.jobId}`, `Trabajo ${job.jobId} corriendo (${job.source}). Rev\xEDsalo con: easybits web extract-status ${job.jobId}`));
5014
+ } else if (job.status === "error") {
5015
+ console.log(t(`Job ${job.jobId} failed: ${job.error ?? "unknown error"}`, `El trabajo ${job.jobId} fall\xF3: ${job.error ?? "error desconocido"}`));
5016
+ } else {
5017
+ for (const r of job.records ?? []) console.log(JSON.stringify(r));
5018
+ console.error(t(`${job.records?.length ?? 0} records (${job.source}).`, `${job.records?.length ?? 0} registros (${job.source}).`));
5019
+ }
5020
+ });
5021
+ if (job.status === "error") process.exitCode = EXIT.API;
5022
+ }
5023
+ var webCmd = {
5024
+ name: "web",
5025
+ group: "Web",
5026
+ summary: "Search, read, extract and crawl the web (metered in web queries)",
5027
+ synopsis: "web",
5028
+ subs: {
5029
+ search: {
5030
+ summary: "Search Google, Bing, Yandex or DuckDuckGo (1 query)",
5031
+ usage: "easybits web search <query...> [--engine google|bing|yandex|duckduckgo] [--country <cc>]",
5032
+ options: {
5033
+ engine: { type: "string", value: "name", description: "google (default), bing, yandex or duckduckgo" },
5034
+ country: { type: "string", value: "cc", description: "ISO country for localized results (mx, us\u2026)" }
5035
+ },
5036
+ examples: ["easybits web search dentista polanco --country mx", `easybits web search "react router 7 loaders" --json | jq '.results.organic[0]'`],
5037
+ async run(ctx) {
5038
+ const query = ctx.args.join(" ").trim();
5039
+ if (!query) throw usageError(t("Missing <query>.", "Falta <query>."), this.usage);
5040
+ const engine = str(ctx, "engine");
5041
+ if (engine && !ENGINES.includes(engine)) throw usageError(t(`--engine must be one of: ${ENGINES.join(", ")}.`, `--engine debe ser uno de: ${ENGINES.join(", ")}.`), this.usage);
5042
+ const eb = await getClient(ctx);
5043
+ const r = await web(() => eb.webSearch({ query, engine, country: str(ctx, "country") }));
5044
+ emit(ctx, r, () => table(organicResults(r.results), [["rank", "#"], ["title", "TITLE"], ["url", "URL"]], t("No results.", "Sin resultados.")));
5045
+ }
5046
+ },
5047
+ fetch: {
5048
+ summary: "Read a page even if it blocks bots: raw HTML or clean markdown (1 query)",
5049
+ usage: "easybits web fetch <url> [--markdown] [--main] [--country <cc>] [--out <file>]",
5050
+ options: {
5051
+ markdown: { type: "boolean", description: "Clean markdown instead of raw HTML" },
5052
+ main: { type: "boolean", description: "With --markdown: drop nav, footer and icons" },
5053
+ country: { type: "string", value: "cc", description: "ISO country to fetch from (mx, us\u2026)" },
5054
+ out: { type: "string", value: "file", description: "Save the body to a file instead of stdout" }
5055
+ },
5056
+ examples: ["easybits web fetch https://example.com --markdown --main", "easybits web fetch https://example.com --out page.html"],
5057
+ async run(ctx) {
5058
+ const url = need(ctx, 0, "url", this.usage);
5059
+ const asMarkdown = bool(ctx, "markdown");
5060
+ if (bool(ctx, "main") && !asMarkdown) throw usageError(t("--main only works with --markdown.", "--main s\xF3lo funciona con --markdown."), this.usage);
5061
+ const eb = await getClient(ctx);
5062
+ const r = await web(() => eb.webFetch({ url, asMarkdown, onlyMainContent: bool(ctx, "main") || void 0, country: str(ctx, "country") }));
5063
+ const out = str(ctx, "out");
5064
+ if (out) {
5065
+ writeFileSync8(out, r.body);
5066
+ const { body: _b, ...meta } = r;
5067
+ emit(ctx, { ...meta, out, bytes: Buffer.byteLength(r.body) }, () => console.error(t(`Saved ${fmtBytes(Buffer.byteLength(r.body))} to ${out} (HTTP ${r.statusCode})`, `Guard\xE9 ${fmtBytes(Buffer.byteLength(r.body))} en ${out} (HTTP ${r.statusCode})`)));
5068
+ return;
5069
+ }
5070
+ emit(ctx, r, () => process.stdout.write(r.body.endsWith("\n") ? r.body : r.body + "\n"));
5071
+ }
5072
+ },
5073
+ extract: {
5074
+ summary: "Structured records from a known source (Google Maps, Mercado Libre\u2026); 1 query per record",
5075
+ usage: "easybits web extract <source> (--input <json> | --url <url>...) [--limit <n>] [--wait]",
5076
+ options: {
5077
+ input: { type: "string", value: "json", description: 'Input for the source: [{"keyword":"\u2026","country":"MX"}], {"query":"\u2026"} or [{"url":"\u2026"}]' },
5078
+ url: { type: "string", multiple: true, value: "url", description: `Shortcut for --input '[{"url":\u2026}]' (repeatable)` },
5079
+ limit: { type: "string", value: "n", description: "Max records per input (default 20, max 200)" },
5080
+ dataset: { type: "string", value: "id", description: "Catalog dataset id, for sources not on the list (source becomes optional)" },
5081
+ wait: { type: "boolean", description: "Wait for the job to finish (polls every 15 s, up to 5 min)" }
5082
+ },
5083
+ examples: [
5084
+ `easybits web extract google_maps --input '[{"keyword":"dentista Polanco CDMX","country":"MX"}]' --limit 50 --wait`,
5085
+ `easybits web extract mercadolibre --input '{"query":"iphone 15"}' --json`,
5086
+ "easybits web extract instagram_profiles --url https://instagram.com/easybits --wait"
5087
+ ],
5088
+ async run(ctx) {
5089
+ const dataset = str(ctx, "dataset");
5090
+ const source = ctx.args[0];
5091
+ if (!source && !dataset) throw usageError(t("Missing <source> (or --dataset <id>).", "Falta <source> (o --dataset <id>)."), this.usage);
5092
+ const input = extractInput(str(ctx, "input"), list(ctx, "url"), this.usage);
5093
+ const limit = int(ctx, "limit", this.usage);
5094
+ const params = { source, datasetId: dataset, input, limit };
5095
+ const eb = await getClient(ctx);
5096
+ const job = await web(() => bool(ctx, "wait") ? eb.webExtractAndWait(params) : eb.webExtract(params));
5097
+ printJob(ctx, job);
5098
+ }
5099
+ },
5100
+ "extract-status": {
5101
+ summary: "Check an extract job; when done, prints its records",
5102
+ usage: "easybits web extract-status <jobId>",
5103
+ examples: ["easybits web extract-status j_abc123", "easybits web extract-status j_abc123 --json | jq '.records | length'"],
5104
+ async run(ctx) {
5105
+ const jobId = need(ctx, 0, "jobId", this.usage);
5106
+ const eb = await getClient(ctx);
5107
+ printJob(ctx, await web(() => eb.webExtractStatus(jobId)));
5108
+ }
5109
+ },
5110
+ crawl: {
5111
+ summary: "Read a page and follow its internal links as markdown (1 query per page)",
5112
+ usage: "easybits web crawl <url> [--max-pages <n>] [--main] [--country <cc>]",
5113
+ options: {
5114
+ "max-pages": { type: "string", value: "n", description: "Pages to read, 1-20 (default 10)" },
5115
+ main: { type: "boolean", description: "Drop nav, footer and icons from each page (good for RAG)" },
5116
+ country: { type: "string", value: "cc", description: "ISO country to fetch from (mx, us\u2026)" }
5117
+ },
5118
+ examples: ["easybits web crawl https://docs.example.com --max-pages 5 --main", "easybits web crawl https://example.com --json | jq '.pages[].url'"],
5119
+ async run(ctx) {
5120
+ const url = need(ctx, 0, "url", this.usage);
5121
+ const maxPages = int(ctx, "max-pages", this.usage);
5122
+ if (maxPages != null && (maxPages < 1 || maxPages > 20)) throw usageError(t("--max-pages must be 1-20.", "--max-pages debe ser de 1 a 20."), this.usage);
5123
+ const eb = await getClient(ctx);
5124
+ const r = await web(() => eb.webCrawl({ url, maxPages, onlyMainContent: bool(ctx, "main") || void 0, country: str(ctx, "country") }));
5125
+ emit(ctx, r, () => {
5126
+ for (const p of r.pages) console.log(`<!-- ${p.url} -->
5127
+ ${p.markdown.trim()}
5128
+ `);
5129
+ console.error(
5130
+ t(`${r.pages.length} pages; ${r.pending.length} links not visited (--json to get them).`, `${r.pages.length} p\xE1ginas; ${r.pending.length} links sin visitar (--json para verlos).`)
5131
+ );
5132
+ });
5133
+ }
5134
+ }
5135
+ }
5136
+ };
5137
+
5138
+ // src/commands/webhooks.ts
5139
+ function eventList(values) {
5140
+ return [...new Set(values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter(Boolean))];
5141
+ }
5142
+ function scopeList(value) {
5143
+ if (value == null) return void 0;
5144
+ const out = [...new Set(value.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean))];
5145
+ return out.length ? out : void 0;
5146
+ }
5147
+ function matchKey(ref, keys2) {
5148
+ const byId = keys2.find((k) => k.id === ref);
5149
+ if (byId) return { id: byId.id };
5150
+ const r = ref.trim();
5151
+ const found = keys2.filter((k) => k.status !== "REVOKED" && (k.prefix === r || k.name.toLowerCase() === r.toLowerCase()));
5152
+ return found.length === 1 ? { id: found[0].id } : { matches: found };
5153
+ }
5154
+ var EVENT_OPTION = {
5155
+ event: { type: "string", multiple: true, value: "name", description: "Event to deliver (repeatable or comma-separated): file.created, website.deleted, form.submitted\u2026" }
5156
+ };
5157
+ var webhooks = {
5158
+ name: "webhooks",
5159
+ aliases: ["webhook"],
5160
+ group: "Account",
5161
+ summary: "Webhooks: HTTPS POSTs signed with HMAC when your files, websites or forms change",
5162
+ synopsis: "webhooks",
5163
+ subs: {
5164
+ ls: {
5165
+ aliases: ["list"],
5166
+ summary: "List your webhooks",
5167
+ usage: "easybits webhooks ls",
5168
+ examples: ["easybits webhooks ls", "easybits webhooks ls --json"],
5169
+ async run(ctx) {
5170
+ const { items } = await (await getClient(ctx)).listWebhooks();
5171
+ emit(
5172
+ ctx,
5173
+ items,
5174
+ () => table(
5175
+ items.map((w) => ({ ...w, events: w.events.join(","), fails: w.failCount })),
5176
+ [["id", "ID"], ["url", "URL"], ["events", "EVENTS"], ["status", "STATUS"], ["fails", "FAILS"]],
5177
+ t("No webhooks. Create one: easybits webhooks create https://example.com/hook --event file.created", "Sin webhooks. Crea uno: easybits webhooks create https://ejemplo.com/hook --event file.created")
5178
+ )
5179
+ );
5180
+ }
5181
+ },
5182
+ create: {
5183
+ summary: "Create a webhook; prints its signing secret ONCE",
5184
+ usage: "easybits webhooks create <https-url> --event <name>...",
5185
+ options: { ...EVENT_OPTION },
5186
+ examples: [
5187
+ "easybits webhooks create https://example.com/hook --event file.created --event file.deleted",
5188
+ "easybits webhooks create https://example.com/hook --event form.submitted --json | jq -r .secret"
5189
+ ],
5190
+ async run(ctx) {
5191
+ const url = need(ctx, 0, "https-url", this.usage);
5192
+ const events = eventList(list(ctx, "event"));
5193
+ if (!events.length) throw usageError(t("Missing --event (at least one).", "Falta --event (al menos uno)."), this.usage);
5194
+ const w = await (await getClient(ctx)).createWebhook({ url, events });
5195
+ emit(ctx, w, () => {
5196
+ console.log(t(`Created ${w.id} \u2192 ${w.url} (${w.events.join(", ")})`, `Creado ${w.id} \u2192 ${w.url} (${w.events.join(", ")})`));
5197
+ console.log(`${t("Signing secret", "Secreto de firma")}: ${w.secret}`);
5198
+ console.error(
5199
+ t(
5200
+ "Save it now: it is not shown again. Verify X-Easybits-Signature (HMAC-SHA256 of the body) with it.",
5201
+ "Gu\xE1rdalo ya: no se vuelve a mostrar. Con \xE9l verificas X-Easybits-Signature (HMAC-SHA256 del cuerpo)."
5202
+ )
5203
+ );
5204
+ });
5205
+ }
5206
+ },
5207
+ update: {
5208
+ summary: "Change a webhook's URL or events, or pause/resume it",
5209
+ usage: "easybits webhooks update <id> [--url <https-url>] [--event <name>...] [--pause|--resume]",
5210
+ options: {
5211
+ url: { type: "string", value: "https-url", description: "New destination URL" },
5212
+ ...EVENT_OPTION,
5213
+ pause: { type: "boolean", description: "Stop deliveries (status PAUSED)" },
5214
+ resume: { type: "boolean", description: "Deliver again and reset the failure count (status ACTIVE)" }
5215
+ },
5216
+ examples: ["easybits webhooks update 66f0c2a1b2c3d4e5f6a7b8c9 --event file.created,file.updated", "easybits webhooks update 66f0c2a1b2c3d4e5f6a7b8c9 --resume"],
5217
+ async run(ctx) {
5218
+ const id = need(ctx, 0, "id", this.usage);
5219
+ if (bool(ctx, "pause") && bool(ctx, "resume")) throw usageError(t("Pass --pause or --resume, not both.", "Pasa --pause o --resume, no ambos."), this.usage);
5220
+ const events = eventList(list(ctx, "event"));
5221
+ const params = {
5222
+ ...str(ctx, "url") ? { url: str(ctx, "url") } : {},
5223
+ ...events.length ? { events } : {},
5224
+ ...bool(ctx, "pause") ? { status: "PAUSED" } : bool(ctx, "resume") ? { status: "ACTIVE" } : {}
5225
+ };
5226
+ if (!Object.keys(params).length) throw usageError(t("Nothing to change: pass --url, --event, --pause or --resume.", "Nada que cambiar: pasa --url, --event, --pause o --resume."), this.usage);
5227
+ const w = await (await getClient(ctx)).updateWebhook(id, params);
5228
+ emit(ctx, w, () => console.log(t(`Updated ${w.id}: ${w.url} (${w.events.join(", ")}) ${w.status}`, `Actualizado ${w.id}: ${w.url} (${w.events.join(", ")}) ${w.status}`)));
5229
+ }
5230
+ },
5231
+ rm: {
5232
+ aliases: ["remove", "delete"],
5233
+ summary: "Delete a webhook",
5234
+ usage: "easybits webhooks rm <id> [--yes]",
5235
+ options: { ...YES_OPTION },
5236
+ examples: ["easybits webhooks rm 66f0c2a1b2c3d4e5f6a7b8c9 --yes"],
5237
+ async run(ctx) {
5238
+ const id = need(ctx, 0, "id", this.usage);
5239
+ requireYesIfHeadless(ctx);
5240
+ await confirm(ctx, t(`Delete webhook ${id}? Deliveries stop right away.`, `\xBFBorrar el webhook ${id}? Las entregas paran de inmediato.`));
5241
+ const r = await (await getClient(ctx)).deleteWebhook(id);
5242
+ emit(ctx, { ...r, id }, () => console.log(t(`Deleted ${id}`, `Borrado ${id}`)));
5243
+ }
5244
+ }
5245
+ }
5246
+ };
5247
+ async function resolveKey(ctx, ref) {
5248
+ const { keys: keys2 } = await (await getClient(ctx)).listKeys();
5249
+ const m = matchKey(ref, keys2);
5250
+ if ("id" in m) return keys2.find((k) => k.id === m.id);
5251
+ if (!m.matches.length) {
5252
+ 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);
5253
+ }
5254
+ throw new CliError(
5255
+ t(`"${ref}" matches ${m.matches.length} keys. Use the id:`, `"${ref}" coincide con ${m.matches.length} keys. Usa el id:`),
5256
+ EXIT.USAGE,
5257
+ m.matches.map((k) => ` ${k.id} ${k.prefix}\u2026 ${k.name}`).join("\n"),
5258
+ "ambiguous"
5259
+ );
5260
+ }
5261
+ var keys = {
5262
+ name: "keys",
5263
+ aliases: ["key"],
5264
+ group: "Account",
5265
+ summary: "API keys of your account: list, create, revoke",
5266
+ synopsis: "keys",
5267
+ subs: {
5268
+ ls: {
5269
+ aliases: ["list"],
5270
+ summary: "List your API keys (prefix only; the full key is never shown again)",
5271
+ usage: "easybits keys ls",
5272
+ examples: ["easybits keys ls", "easybits keys ls --json"],
5273
+ async run(ctx) {
5274
+ const { keys: items } = await (await getClient(ctx)).listKeys();
5275
+ emit(
5276
+ ctx,
5277
+ items,
5278
+ () => table(
5279
+ items.map((k) => ({ ...k, prefix: `${k.prefix}\u2026`, scopes: k.scopes.join(","), createdAt: fmtDate(k.createdAt), expiresAt: k.expiresAt ? fmtDate(k.expiresAt) : t("never", "nunca") })),
5280
+ [["id", "ID"], ["name", "NAME"], ["prefix", "PREFIX"], ["scopes", "SCOPES"], ["status", "STATUS"], ["expiresAt", "EXPIRES"]],
5281
+ t("No API keys.", "Sin API keys.")
5282
+ )
5283
+ );
5284
+ }
5285
+ },
5286
+ create: {
5287
+ summary: "Create an API key; prints it ONCE",
5288
+ usage: "easybits keys create <name> [--scopes READ,WRITE,DELETE] [--expires <date>]",
5289
+ options: {
5290
+ scopes: { type: "string", value: "list", description: "Comma-separated: READ, WRITE, DELETE, ADMIN (default READ,WRITE,DELETE; never more than your own credential has)" },
5291
+ expires: { type: "string", value: "date", description: "Expiry date (ISO, e.g. 2027-01-31); default never" }
5292
+ },
5293
+ examples: [
5294
+ "easybits keys create ci-deploy",
5295
+ "easybits keys create dashboard-readonly --scopes READ --expires 2027-01-31",
5296
+ "KEY=$(easybits keys create ci --json | jq -r .raw)"
5297
+ ],
5298
+ async run(ctx) {
5299
+ const name = need(ctx, 0, "name", this.usage);
5300
+ const expires = str(ctx, "expires");
5301
+ if (expires && Number.isNaN(new Date(expires).getTime())) throw usageError(t(`--expires must be a date (2027-01-31), got "${expires}".`, `--expires debe ser una fecha (2027-01-31), lleg\xF3 "${expires}".`), this.usage);
5302
+ const k = await api(ctx, "POST", "/keys", { name, scopes: scopeList(str(ctx, "scopes")), ...expires ? { expiresAt: new Date(expires).toISOString() } : {} });
5303
+ emit(ctx, k, () => {
5304
+ console.log(t(`Created ${k.name} (${k.id}) \u2014 ${k.scopes.join(", ")}`, `Creada ${k.name} (${k.id}) \u2014 ${k.scopes.join(", ")}`));
5305
+ console.log(k.raw);
5306
+ console.error(t("Save it now: it is not shown again (keys ls only shows the prefix).", "Gu\xE1rdala ya: no se vuelve a mostrar (keys ls s\xF3lo muestra el prefijo)."));
5307
+ });
5308
+ }
5309
+ },
5310
+ revoke: {
5311
+ aliases: ["rm", "delete"],
5312
+ summary: "Revoke an API key (by id, name or prefix); anything using it stops working",
5313
+ usage: "easybits keys revoke <id|name|prefix> [--yes]",
5314
+ options: { ...YES_OPTION },
5315
+ examples: ["easybits keys revoke ci-deploy", "easybits keys revoke eb_sk_live_abcd1234 --yes"],
5316
+ async run(ctx) {
5317
+ const ref = need(ctx, 0, "key", this.usage);
5318
+ requireYesIfHeadless(ctx);
5319
+ const k = await resolveKey(ctx, ref);
5320
+ const self = (await resolveCredential(ctx)).startsWith(k.prefix);
5321
+ await confirm(
5322
+ ctx,
5323
+ self ? t(`Revoke ${k.name} (${k.prefix}\u2026)? It is the key THIS CLI is using: you will need easybits login after.`, `\xBFRevocar ${k.name} (${k.prefix}\u2026)? Es la key que usa ESTE CLI: despu\xE9s tendr\xE1s que correr easybits login.`) : t(`Revoke ${k.name} (${k.prefix}\u2026)? Anything using it stops working right away.`, `\xBFRevocar ${k.name} (${k.prefix}\u2026)? Lo que la use deja de funcionar de inmediato.`)
5324
+ );
5325
+ const r = await api(ctx, "DELETE", `/keys/${encodeURIComponent(k.id)}`);
5326
+ emit(ctx, { ...r, id: k.id, name: k.name, prefix: k.prefix }, () => console.log(t(`Revoked ${k.name} (${k.prefix}\u2026)`, `Revocada ${k.name} (${k.prefix}\u2026)`)));
5327
+ }
5328
+ }
5329
+ }
5330
+ };
5331
+
5332
+ // src/commands/secrets.ts
5333
+ var NAME = /^[A-Z_][A-Z0-9_]*$/;
5334
+ var SET_USAGE = "easybits secrets set (--dotenv <path|-> | <NAME> < value)";
5335
+ async function secretsToSet(args, dotenv, stdin) {
5336
+ if (args.some((a) => a.includes("="))) {
5337
+ throw usageError(t("Values never go in argv (ps and shell history see them). Use --dotenv or stdin.", "Los valores nunca van en argv (los ven ps y el historial). Usa --dotenv o stdin."), SET_USAGE);
5338
+ }
5339
+ if (dotenv && args.length) throw usageError(t("Give --dotenv or a NAME, not both.", "Pasa --dotenv o un NOMBRE, no los dos."), SET_USAGE);
5340
+ let values;
5341
+ if (dotenv) values = await readEnvFile(dotenv, SET_USAGE);
5342
+ else if (args.length === 1) {
5343
+ const v = (await stdin()).replace(/\r?\n$/, "");
5344
+ values = { [args[0]]: v };
5345
+ } else throw usageError(t("Give --dotenv <path|-> or one NAME with the value on stdin.", "Pasa --dotenv <ruta|-> o un NOMBRE con el valor por stdin."), SET_USAGE);
5346
+ for (const [k, v] of Object.entries(values)) {
5347
+ if (!NAME.test(k)) throw usageError(t(`Bad name "${k}": use [A-Z_][A-Z0-9_]* (e.g. GITHUB_TOKEN).`, `Nombre inv\xE1lido "${k}": usa [A-Z_][A-Z0-9_]* (p. ej. GITHUB_TOKEN).`), SET_USAGE);
5348
+ if (!v) throw usageError(t(`Empty value for ${k}.`, `Valor vac\xEDo para ${k}.`), SET_USAGE);
5349
+ }
5350
+ if (!Object.keys(values).length) throw usageError(t("Nothing to save.", "Nada que guardar."), SET_USAGE);
5351
+ return values;
5352
+ }
5353
+ var secrets = {
5354
+ name: "secrets",
5355
+ aliases: ["secret"],
5356
+ group: "Account",
5357
+ summary: "Your account's secrets vault: names only; used as $secret:NAME",
5358
+ synopsis: "secrets <ls|set|rm>",
5359
+ subs: {
5360
+ ls: {
5361
+ aliases: ["list"],
5362
+ summary: "List the names in your vault (values never come back)",
5363
+ usage: "easybits secrets ls",
5364
+ examples: ["easybits secrets ls", "easybits secrets ls --json"],
5365
+ async run(ctx) {
5366
+ const { items } = await api(ctx, "GET", "/secrets");
5367
+ emit(
5368
+ ctx,
5369
+ { items },
5370
+ () => table(
5371
+ items.map((s) => ({ ...s, createdAt: fmtDate(s.createdAt), lastUsedAt: fmtDate(s.lastUsedAt) })),
5372
+ [["name", "NAME"], ["createdAt", "CREATED"], ["lastUsedAt", "LAST USED"]],
5373
+ t("No secrets yet. Save one: easybits secrets set --dotenv .env", "Todav\xEDa no hay secretos. Guarda uno: easybits secrets set --dotenv .env")
5374
+ )
5375
+ );
5376
+ }
5377
+ },
5378
+ set: {
5379
+ summary: "Save or replace secrets from a dotenv file, or one value from stdin",
5380
+ usage: SET_USAGE,
5381
+ options: { ...ENV_FILE_OPTION },
5382
+ examples: [
5383
+ "easybits secrets set --dotenv .env.production",
5384
+ `printf '%s' "$TOKEN" | easybits secrets set GITHUB_TOKEN`,
5385
+ "op read op://vault/stripe/key | easybits secrets set STRIPE_SECRET_KEY # from a password manager"
5386
+ ],
5387
+ async run(ctx) {
5388
+ if (ctx.args.length === 1 && !str(ctx, "dotenv") && process.stdin.isTTY) {
5389
+ throw usageError(t(`Pipe the value on stdin: printf '%s' "$VALUE" | easybits secrets set ${ctx.args[0]}`, `Pasa el valor por stdin: printf '%s' "$VALOR" | easybits secrets set ${ctx.args[0]}`), SET_USAGE);
5390
+ }
5391
+ const values = await secretsToSet(ctx.args, str(ctx, "dotenv"), async () => (await readStdin()).toString("utf8"));
5392
+ for (const [name, value] of Object.entries(values)) await api(ctx, "POST", "/secrets", { name, value });
5393
+ const saved = Object.keys(values);
5394
+ emit(ctx, { saved }, () => console.log(t(`Saved ${saved.join(", ")}. Use it as $secret:${saved[0]}`, `Guardado ${saved.join(", ")}. \xDAsalo como $secret:${saved[0]}`)));
5395
+ }
5396
+ },
5397
+ rm: {
5398
+ aliases: ["remove", "delete"],
5399
+ summary: "Delete secrets by name; whatever references them stops resolving",
5400
+ usage: "easybits secrets rm <NAME>... [--yes]",
5401
+ options: { ...YES_OPTION },
5402
+ examples: ["easybits secrets rm GITHUB_TOKEN", "easybits secrets rm OLD_A OLD_B --yes --json"],
5403
+ async run(ctx) {
5404
+ if (!ctx.args.length) throw usageError(t("Missing <NAME>.", "Falta <NAME>."), this.usage);
5405
+ requireYesIfHeadless(ctx);
5406
+ await confirm(ctx, t(`Delete ${ctx.args.join(", ")} from your vault? The value cannot be recovered.`, `\xBFBorrar ${ctx.args.join(", ")} de tu b\xF3veda? El valor no se puede recuperar.`));
5407
+ const deleted = [];
5408
+ for (const name of ctx.args) {
5409
+ await api(ctx, "DELETE", `/secrets/${encodeURIComponent(name)}`);
5410
+ deleted.push(name);
5411
+ }
5412
+ emit(ctx, { deleted }, () => console.log(t(`Deleted ${deleted.join(", ")}`, `Borrado ${deleted.join(", ")}`)));
5413
+ }
5414
+ }
5415
+ },
5416
+ defaultSub: "ls"
5417
+ };
5418
+
5419
+ // src/commands/documents.ts
5420
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync18, statSync as statSync11, writeFileSync as writeFileSync10 } from "fs";
5421
+ import { join as join11 } from "path";
5422
+ import { EasybitsError as EasybitsError5, resolveBaseUrl as resolveBaseUrl8 } from "@easybits.cloud/sdk";
5423
+
5424
+ // src/progress.ts
5425
+ import { writeFileSync as writeFileSync9 } from "fs";
5426
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
5427
+ function progress(ctx, label) {
5428
+ const started = Date.now();
5429
+ const secs = () => Math.round((Date.now() - started) / 1e3);
5430
+ if (ctx.json) return { update() {
5431
+ }, stop() {
5432
+ } };
5433
+ if (!interactive(ctx)) {
5434
+ let last = "";
5435
+ const line2 = (l) => {
5436
+ if (l !== last) process.stderr.write(`${l}
5437
+ `);
5438
+ last = l;
5439
+ };
5440
+ line2(label);
5441
+ return { update: line2, stop: (final) => final && process.stderr.write(`${final}
5442
+ `) };
5443
+ }
5444
+ let i = 0;
5445
+ let current2 = label;
5446
+ const draw = () => process.stderr.write(`\r\x1B[2K${FRAMES[i++ % FRAMES.length]} ${current2} (${secs()}s)`);
5447
+ draw();
5448
+ const timer = setInterval(draw, 100);
5449
+ return {
5450
+ update(l) {
5451
+ current2 = l;
5452
+ },
5453
+ stop(final) {
5454
+ clearInterval(timer);
5455
+ process.stderr.write(`\r\x1B[2K${final ? `${final}
5456
+ ` : ""}`);
5457
+ }
5458
+ };
5459
+ }
5460
+ async function withProgress(ctx, label, work) {
5461
+ const p = progress(ctx, label);
5462
+ try {
5463
+ return await work(p);
5464
+ } finally {
5465
+ p.stop();
5466
+ }
5467
+ }
5468
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5469
+ async function download(url, out, headers = {}) {
5470
+ const res = await fetch(url, { headers });
5471
+ if (!res.ok) throw new CliError(t(`Download failed (${res.status}): ${url}`, `Fall\xF3 la descarga (${res.status}): ${url}`), EXIT.API);
5472
+ const buf = Buffer.from(await res.arrayBuffer());
5473
+ writeFileSync9(out, buf);
5474
+ return buf.length;
5475
+ }
5476
+
5477
+ // src/commands/documents.ts
5478
+ var DOC_PRESETS = {
5479
+ "ig-square": { width: 1080, height: 1080 },
5480
+ "fb-square": { width: 1080, height: 1080 },
5481
+ "ig-feed": { width: 1080, height: 1350 },
5482
+ "li-feed": { width: 1080, height: 1350 },
5483
+ "ig-story": { width: 1080, height: 1920 },
5484
+ "wsp-status": { width: 1080, height: 1920 },
5485
+ tiktok: { width: 1080, height: 1920 },
5486
+ "slide-16-9": { width: 1920, height: 1080 },
5487
+ letter: null,
5488
+ "1080x1080": { width: 1080, height: 1080 },
5489
+ "1080x1350": { width: 1080, height: 1350 }
5490
+ };
5491
+ function parseSize(s) {
5492
+ const m = /^(\d{2,5})x(\d{2,5})$/i.exec(s.trim());
5493
+ return m ? { width: Number(m[1]), height: Number(m[2]) } : null;
5494
+ }
5495
+ function docFormat(input, usage2) {
5496
+ if (!input) return { dims: null };
5497
+ if (input in DOC_PRESETS) return { body: { preset: input }, dims: DOC_PRESETS[input] };
5498
+ const size = parseSize(input);
5499
+ if (!size) {
5500
+ throw usageError(
5501
+ t(`Unknown --format "${input}". One of: ${Object.keys(DOC_PRESETS).join(", ")}, or WxH in px.`, `--format desconocido "${input}". Uno de: ${Object.keys(DOC_PRESETS).join(", ")}, o WxH en px.`),
5502
+ usage2
5503
+ );
5504
+ }
5505
+ if ([size.width, size.height].some((n) => n < 100 || n > 1e4)) {
5506
+ throw usageError(t("--format WxH goes from 100 to 10000 px per side.", "--format WxH va de 100 a 10000 px por lado."), usage2);
5507
+ }
5508
+ return { body: size, dims: size };
5509
+ }
5510
+ function blankPageHtml(dims) {
5511
+ if (!dims) return void 0;
5512
+ return `<section class="w-[${dims.width}px] h-[${dims.height}px] flex flex-col relative overflow-hidden bg-surface"></section>`;
5513
+ }
5514
+ var pagesOf = (doc) => [...doc.sections ?? []].sort((a, b) => a.order - b.order);
5515
+ function pageRef(pages, ref, usage2) {
5516
+ const byId = pages.find((p) => p.id === ref);
5517
+ if (byId) return byId;
5518
+ if (/^\d+$/.test(ref)) {
5519
+ const n = Number(ref);
5520
+ if (n >= 1 && n <= pages.length) return pages[n - 1];
5521
+ }
5522
+ throw usageError(
5523
+ 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).`),
5524
+ usage2
5525
+ );
5526
+ }
5527
+ function reorderIds(pages, refs, usage2) {
5528
+ const ids = refs.map((r) => pageRef(pages, r, usage2).id);
5529
+ if (ids.length !== pages.length || new Set(ids).size !== ids.length) {
5530
+ throw usageError(t(`Give every page exactly once (${pages.length} pages).`, `Pasa cada p\xE1gina exactamente una vez (${pages.length} p\xE1ginas).`), usage2);
5531
+ }
5532
+ return ids;
5533
+ }
5534
+ function pageRows(pages) {
5535
+ return pages.map((p, i) => ({ n: i + 1, id: p.id, label: p.label || p.name || "-", bytes: (p.html ?? "").length }));
5536
+ }
5537
+ var sized = (rows) => rows.map((r) => ({ ...r, size: fmtBytes(r.bytes) }));
5538
+ var safeName = (s) => s.replace(/[^\w\-. ]+/g, "_").trim().slice(0, 80) || "document";
5539
+ function readHtml(path, usage2) {
5540
+ if (path === "-") {
5541
+ if (process.stdin.isTTY) throw usageError(t("Expected the HTML on stdin.", "Se esperaba el HTML por stdin."), usage2);
5542
+ return readStdin().then((b) => b.toString("utf8"));
5543
+ }
5544
+ if (!existsSync16(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
5545
+ return readFileSync18(path, "utf8");
5546
+ }
5547
+ var getDoc = (ctx, id) => api(ctx, "GET", `/documents/${encodeURIComponent(id)}`);
5548
+ var docPath = (id) => `/documents/${encodeURIComponent(id)}`;
5549
+ async function editorUrl(id) {
5550
+ return `${(await resolveBaseUrl8()).replace(/\/$/, "")}/dash/documents/${id}`;
5551
+ }
5552
+ async function getBytes(ctx, path) {
5553
+ const token = await resolveCredential(ctx);
5554
+ const base = (await resolveBaseUrl8()).replace(/\/$/, "");
5555
+ const res = await fetch(`${base}/api/v2${path}`, { headers: { Authorization: `Bearer ${token}` } });
5556
+ if (!res.ok) throw new EasybitsError5(res.status, await res.text());
5557
+ return Buffer.from(await res.arrayBuffer());
5558
+ }
5559
+ var PAGES_USAGE = "easybits documents pages <ls|get|set> <document> [<page>] [<file.html|->] [--out FILE]";
5560
+ var EXPORT_USAGE = "easybits documents export <pdf|png> <document> [--out PATH] [--pages 1,3]";
5561
+ var documents = {
5562
+ name: "documents",
5563
+ aliases: ["document", "doc"],
5564
+ group: "Content",
5565
+ summary: "Documents (/dash/documents): pages of HTML you write, published and exported to PDF/PNG",
5566
+ synopsis: "documents <ls|get|create|\u2026>",
5567
+ subs: {
5568
+ ls: {
5569
+ aliases: ["list"],
5570
+ summary: "List your documents, newest first",
5571
+ usage: "easybits documents ls [--search TEXT] [--limit N]",
5572
+ options: {
5573
+ search: { type: "string", value: "text", description: "Only documents whose name contains this" },
5574
+ limit: { type: "string", value: "n", description: "How many (default 20, max 100)" }
5575
+ },
5576
+ examples: ["easybits documents ls", "easybits documents ls --search cotizaci\xF3n --json"],
5577
+ async run(ctx) {
5578
+ const q = new URLSearchParams();
5579
+ const search = str(ctx, "search");
5580
+ const limit = str(ctx, "limit");
5581
+ if (search) q.set("search", search);
5582
+ if (limit) q.set("limit", limit);
5583
+ const r = await api(ctx, "GET", `/documents${q.size ? `?${q}` : ""}`);
5584
+ emit(ctx, r, () => {
5585
+ table(
5586
+ r.items.map((d) => ({ ...d, url: d.shareUrl ?? t("(not published)", "(sin publicar)"), updatedAt: fmtDate(d.updatedAt) })),
5587
+ [["id", "ID"], ["name", "NAME"], ["pageCount", "PAGES"], ["updatedAt", "MODIFIED"], ["url", "URL"]],
5588
+ t('No documents yet. Create one: easybits documents create --name "\u2026" --html page.html', 'Todav\xEDa no hay documentos. Crea uno: easybits documents create --name "\u2026" --html pagina.html')
5589
+ );
5590
+ if (r.total > r.items.length) console.log(t(`(${r.items.length} of ${r.total}; --limit up to 100 or --search)`, `(${r.items.length} de ${r.total}; --limit hasta 100 o --search)`));
5591
+ });
5592
+ }
5593
+ },
5594
+ get: {
5595
+ summary: "Show one document: format, URL and its pages",
5596
+ usage: "easybits documents get <document>",
5597
+ examples: ["easybits documents get 'Cotizaci\xF3n ACME'", "easybits documents get 6650f0c2a1b2c3d4e5f60718 --json"],
5598
+ async run(ctx) {
5599
+ const id = need(ctx, 0, "document", this.usage);
5600
+ const doc = await getDoc(ctx, id);
5601
+ emit(ctx, doc, () => {
5602
+ const f = doc.metadata?.format;
5603
+ console.log(`${doc.name} (${doc.id})`);
5604
+ console.log(`${t("Format", "Formato")}: ${f ? `${f.width}\xD7${f.height}px` : t("letter", "carta")} ${t("Theme", "Tema")}: ${doc.metadata?.theme || doc.theme}`);
5605
+ console.log(`URL: ${doc.shareUrl ?? t("(not published: easybits documents deploy)", "(sin publicar: easybits documents deploy)")}`);
5606
+ if (doc.pdfUrl) console.log(`PDF: ${doc.pdfUrl}`);
5607
+ console.log("");
5608
+ table(sized(pageRows(pagesOf(doc))), [["n", "#"], ["id", "ID"], ["label", "NAME"], ["size", "SIZE"]], t("No pages.", "Sin p\xE1ginas."));
5609
+ });
5610
+ }
5611
+ },
5612
+ create: {
5613
+ aliases: ["new"],
5614
+ summary: "Create a document from HTML files (one page each) or a blank page",
5615
+ usage: "easybits documents create --name <name> [--html <file>\u2026] [--format <preset|WxH>] [--theme <theme>] [--brand-kit <id>]",
5616
+ options: {
5617
+ name: { type: "string", value: "name", description: "Document name" },
5618
+ html: { type: "string", multiple: true, value: "file", description: "A page's HTML, in order (repeatable; - reads stdin). Without it: one blank page" },
5619
+ format: { type: "string", value: "preset|WxH", description: "letter (default), ig-feed, ig-square, ig-story, li-feed, slide-16-9, tiktok\u2026 or WxH in px" },
5620
+ theme: { type: "string", value: "theme", description: "Theme name (default: your brand kit or default)" },
5621
+ "brand-kit": { type: "string", value: "id", description: "Brand kit to apply (default: your default kit)" }
5622
+ },
5623
+ examples: [
5624
+ 'easybits documents create --name "Cotizaci\xF3n ACME" --html portada.html --html precios.html',
5625
+ 'easybits documents create --name "Carrusel" --format ig-feed --json # one blank 1080x1350 page'
5626
+ ],
5627
+ async run(ctx) {
5628
+ const name = str(ctx, "name");
5629
+ if (!name?.trim()) throw usageError(t("Missing --name.", "Falta --name."), this.usage);
5630
+ const { body: format, dims } = docFormat(str(ctx, "format"), this.usage);
5631
+ const pagesHtml = [];
5632
+ for (const f of list(ctx, "html")) pagesHtml.push(await readHtml(f, this.usage));
5633
+ const body = { name };
5634
+ if (format) body.format = format;
5635
+ const theme = str(ctx, "theme");
5636
+ const kit = str(ctx, "brand-kit");
5637
+ if (theme) body.theme = theme;
5638
+ if (kit) body.brandKitId = kit;
5639
+ const { document: doc } = await api(ctx, "POST", "/documents", body);
5640
+ const htmls = pagesHtml.length ? pagesHtml : [blankPageHtml(dims)];
5641
+ const pages = [];
5642
+ for (const html of htmls) pages.push(await api(ctx, "POST", `${docPath(doc.id)}/pages`, html ? { html } : {}));
5643
+ const out = { id: doc.id, name: doc.name, format: dims, pages: pages.map((p) => ({ id: p.id, label: p.label })), editorUrl: await editorUrl(doc.id) };
5644
+ emit(ctx, out, () => {
5645
+ console.log(t(`Created ${doc.name} (${doc.id}) with ${pages.length} page(s).`, `Creado ${doc.name} (${doc.id}) con ${pages.length} p\xE1gina(s).`));
5646
+ console.log(`${t("Editor", "Editor")}: ${out.editorUrl}`);
5647
+ console.log(t(`Next: easybits documents pages set ${doc.id} 1 page.html \xB7 easybits documents deploy ${doc.id}`, `Sigue: easybits documents pages set ${doc.id} 1 pagina.html \xB7 easybits documents deploy ${doc.id}`));
5648
+ });
5649
+ }
5650
+ },
5651
+ pages: {
5652
+ summary: "List pages, print one page's HTML, or replace it with a file",
5653
+ usage: PAGES_USAGE,
5654
+ options: {
5655
+ out: { type: "string", value: "file", description: "get: write the HTML to this file instead of stdout" }
5656
+ },
5657
+ examples: [
5658
+ "easybits documents pages ls 'Cotizaci\xF3n ACME'",
5659
+ "easybits documents pages get 'Cotizaci\xF3n ACME' 2 --out precios.html",
5660
+ "easybits documents pages set 'Cotizaci\xF3n ACME' 2 precios.html",
5661
+ "cat page.html | easybits documents pages set 'Cotizaci\xF3n ACME' 1 -"
5662
+ ],
5663
+ async run(ctx) {
5664
+ const action = need(ctx, 0, "ls|get|set", PAGES_USAGE);
5665
+ const id = need(ctx, 1, "document", PAGES_USAGE);
5666
+ if (!["ls", "get", "set"].includes(action)) throw usageError(t(`Unknown pages action "${action}".`, `Acci\xF3n de pages desconocida "${action}".`), PAGES_USAGE);
5667
+ const doc = await getDoc(ctx, id);
5668
+ const pages = pagesOf(doc);
5669
+ if (action === "ls") {
5670
+ const rows = pageRows(pages);
5671
+ emit(ctx, rows, () => table(sized(rows), [["n", "#"], ["id", "ID"], ["label", "NAME"], ["size", "SIZE"]], t("No pages.", "Sin p\xE1ginas.")));
5672
+ return;
5673
+ }
5674
+ const page = pageRef(pages, need(ctx, 2, "page", PAGES_USAGE), PAGES_USAGE);
5675
+ if (action === "get") {
5676
+ const p = await api(ctx, "GET", `${docPath(id)}/pages/${encodeURIComponent(page.id)}`);
5677
+ const out = str(ctx, "out");
5678
+ if (out) {
5679
+ writeFileSync10(out, p.html ?? "");
5680
+ emit(ctx, { id: p.id, out, bytes: (p.html ?? "").length }, () => console.log(t(`Wrote ${fmtBytes((p.html ?? "").length)} to ${out}`, `Escrib\xED ${fmtBytes((p.html ?? "").length)} en ${out}`)));
5681
+ } else emit(ctx, p, () => process.stdout.write((p.html ?? "") + "\n"));
5682
+ return;
5683
+ }
5684
+ const html = await readHtml(need(ctx, 3, "file.html|-", PAGES_USAGE), PAGES_USAGE);
5685
+ const r = await api(ctx, "PATCH", `${docPath(id)}/pages/${encodeURIComponent(page.id)}`, { html });
5686
+ emit(
5687
+ ctx,
5688
+ r,
5689
+ () => console.log(r.noop ? t("No change: the page already had that HTML.", "Sin cambios: la p\xE1gina ya ten\xEDa ese HTML.") : t(`Page ${pages.indexOf(page) + 1} updated (${page.id}).`, `P\xE1gina ${pages.indexOf(page) + 1} actualizada (${page.id}).`))
5690
+ );
5691
+ }
5692
+ },
5693
+ "add-page": {
5694
+ summary: "Add a page from an HTML file (blank without one)",
5695
+ usage: "easybits documents add-page <document> [<file.html|->] [--after N] [--label NAME]",
5696
+ options: {
5697
+ after: { type: "string", value: "n", description: "Insert after page N (0 = first; default: at the end)" },
5698
+ label: { type: "string", value: "name", description: "Page name in the editor" }
5699
+ },
5700
+ examples: ["easybits documents add-page 'Cotizaci\xF3n ACME' anexo.html", "easybits documents add-page 'Cotizaci\xF3n ACME' portada.html --after 0 --label Portada"],
5701
+ async run(ctx) {
5702
+ const id = need(ctx, 0, "document", this.usage);
5703
+ const file = ctx.args[1];
5704
+ const body = {};
5705
+ if (file) body.html = await readHtml(file, this.usage);
5706
+ const after = str(ctx, "after");
5707
+ if (after != null) {
5708
+ if (!/^\d+$/.test(after)) throw usageError(t("--after must be a page number (0 = first).", "--after debe ser un n\xFAmero de p\xE1gina (0 = al inicio)."), this.usage);
5709
+ body.afterPageIndex = Number(after) - 1;
5710
+ }
5711
+ const label = str(ctx, "label");
5712
+ if (label) body.label = label;
5713
+ if (!file) {
5714
+ const doc = await getDoc(ctx, id);
5715
+ const html = blankPageHtml(doc.metadata?.format ?? null);
5716
+ if (html) body.html = html;
5717
+ }
5718
+ const p = await api(ctx, "POST", `${docPath(id)}/pages`, body);
5719
+ emit(ctx, p, () => console.log(t(`Added page ${p.label ?? ""} (${p.id}).`, `Agregu\xE9 la p\xE1gina ${p.label ?? ""} (${p.id}).`)));
5720
+ }
5721
+ },
5722
+ "rm-page": {
5723
+ summary: "Delete one page (a document keeps at least one)",
5724
+ usage: "easybits documents rm-page <document> <page> [--yes]",
5725
+ options: { ...YES_OPTION },
5726
+ examples: ["easybits documents rm-page 'Cotizaci\xF3n ACME' 3 --yes"],
5727
+ async run(ctx) {
5728
+ const id = need(ctx, 0, "document", this.usage);
5729
+ const ref = need(ctx, 1, "page", this.usage);
5730
+ requireYesIfHeadless(ctx);
5731
+ const pages = pagesOf(await getDoc(ctx, id));
5732
+ const page = pageRef(pages, ref, this.usage);
5733
+ 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})?`));
5734
+ const r = await api(ctx, "DELETE", `${docPath(id)}/pages/${encodeURIComponent(page.id)}`);
5735
+ emit(ctx, r, () => console.log(t(`Deleted. ${r.remainingPages} page(s) left.`, `Borrada. Quedan ${r.remainingPages} p\xE1gina(s).`)));
5736
+ }
5737
+ },
5738
+ reorder: {
5739
+ summary: "Put the pages in a new order (every page, by number or id)",
5740
+ usage: "easybits documents reorder <document> <page>\u2026",
5741
+ examples: ["easybits documents reorder 'Cotizaci\xF3n ACME' 1 3 2 # swap pages 2 and 3"],
5742
+ async run(ctx) {
5743
+ const id = need(ctx, 0, "document", this.usage);
5744
+ if (ctx.args.length < 2) throw usageError(t("Give the new order: every page by number or id.", "Pasa el orden nuevo: cada p\xE1gina por n\xFAmero o id."), this.usage);
5745
+ const pageIds = reorderIds(pagesOf(await getDoc(ctx, id)), ctx.args.slice(1), this.usage);
5746
+ const r = await api(ctx, "PUT", `${docPath(id)}/pages/reorder`, { pageIds });
5747
+ emit(ctx, r, () => table(r.map((p) => ({ n: p.order + 1, id: p.id, label: p.label ?? "-" })), [["n", "#"], ["id", "ID"], ["label", "NAME"]]));
5748
+ }
5749
+ },
5750
+ deploy: {
5751
+ aliases: ["publish"],
5752
+ summary: "Publish (or republish) the document at its public URL",
5753
+ usage: "easybits documents deploy <document>",
5754
+ examples: ["easybits documents deploy 'Cotizaci\xF3n ACME'"],
5755
+ async run(ctx) {
5756
+ const id = need(ctx, 0, "document", this.usage);
5757
+ const r = await withProgress(
5758
+ ctx,
5759
+ t("Publishing\u2026", "Publicando\u2026"),
5760
+ () => api(ctx, "POST", `${docPath(id)}/deploy`)
5761
+ );
5762
+ emit(ctx, r, () => {
5763
+ console.log(`URL: ${r.customUrl ?? r.url}`);
5764
+ if (r.pdfUrl) console.log(`PDF: ${r.pdfUrl}`);
5765
+ });
5766
+ }
5767
+ },
5768
+ export: {
5769
+ summary: "Render to a PDF file or to one PNG per page (rendered on the server)",
5770
+ usage: EXPORT_USAGE,
5771
+ options: {
5772
+ out: { type: "string", value: "path", description: "pdf: the file (default <name>.pdf); png: the folder (default .)" },
5773
+ pages: { type: "string", value: "1,3", description: "Only these pages (numbers or ids, comma-separated)" }
5774
+ },
5775
+ examples: [
5776
+ "easybits documents export pdf 'Cotizaci\xF3n ACME' --out cotizacion.pdf",
5777
+ "easybits documents export png Carrusel --out ./slides # one PNG per page at its exact size"
5778
+ ],
5779
+ async run(ctx) {
5780
+ const kind = need(ctx, 0, "pdf|png", EXPORT_USAGE);
5781
+ if (kind !== "pdf" && kind !== "png") throw usageError(t(`Export as pdf or png, not "${kind}".`, `Exporta como pdf o png, no "${kind}".`), EXPORT_USAGE);
5782
+ const id = need(ctx, 1, "document", EXPORT_USAGE);
5783
+ const doc = await getDoc(ctx, id);
5784
+ const pages = pagesOf(doc);
5785
+ const only = str(ctx, "pages");
5786
+ const ids = only ? only.split(",").map((r) => pageRef(pages, r.trim(), EXPORT_USAGE).id) : [];
5787
+ const qs = ids.length ? `?sections=${ids.map(encodeURIComponent).join(",")}` : "";
5788
+ const out = str(ctx, "out");
5789
+ if (kind === "pdf") {
5790
+ const file = out ?? `${safeName(doc.name)}.pdf`;
5791
+ const pdf = await withProgress(ctx, t("Rendering the PDF\u2026", "Renderizando el PDF\u2026"), () => getBytes(ctx, `${docPath(id)}/pdf${qs}`));
5792
+ writeFileSync10(file, pdf);
5793
+ emit(ctx, { file, bytes: pdf.length }, () => console.log(t(`Wrote ${fmtBytes(pdf.length)} to ${file}`, `Escrib\xED ${fmtBytes(pdf.length)} en ${file}`)));
5794
+ return;
5795
+ }
5796
+ const dir = out ?? ".";
5797
+ if (existsSync16(dir) && !statSync11(dir).isDirectory()) throw usageError(t(`--out for png is a folder; ${dir} is a file.`, `--out para png es una carpeta; ${dir} es un archivo.`), EXPORT_USAGE);
5798
+ mkdirSync7(dir, { recursive: true });
5799
+ const files3 = await withProgress(ctx, t(`Rendering ${ids.length || pages.length} page(s)\u2026`, `Renderizando ${ids.length || pages.length} p\xE1gina(s)\u2026`), async (p) => {
5800
+ const r = await api(ctx, "GET", `${docPath(id)}/images${qs}`);
5801
+ const saved = [];
5802
+ for (const [i, f] of r.files.entries()) {
5803
+ p.update(t(`Downloading ${i + 1}/${r.files.length}\u2026`, `Bajando ${i + 1}/${r.files.length}\u2026`));
5804
+ const page = pages.findIndex((x) => x.id === f.sectionId) + 1 || i + 1;
5805
+ const file = join11(dir, `${safeName(doc.name)}-${String(page).padStart(2, "0")}.png`);
5806
+ saved.push({ page, file, url: f.url, fileId: f.id, bytes: await download(f.url, file) });
5807
+ }
5808
+ return saved;
5809
+ });
5810
+ emit(ctx, { files: files3 }, () => {
5811
+ for (const f of files3) console.log(`${f.file} ${fmtBytes(f.bytes)}`);
5812
+ });
5813
+ }
5814
+ },
5815
+ rm: {
5816
+ aliases: ["delete"],
5817
+ summary: "Delete a document (irreversible)",
5818
+ usage: "easybits documents rm <document> [--yes]",
5819
+ options: { ...YES_OPTION },
5820
+ examples: ["easybits documents rm 'Borrador viejo' --yes"],
5821
+ async run(ctx) {
5822
+ const id = need(ctx, 0, "document", this.usage);
5823
+ requireYesIfHeadless(ctx);
5824
+ const doc = await getDoc(ctx, id);
5825
+ await confirm(ctx, t(`Delete the document "${doc.name}" (${pagesOf(doc).length} pages)? This cannot be undone.`, `\xBFBorrar el documento "${doc.name}" (${pagesOf(doc).length} p\xE1ginas)? No se puede deshacer.`));
5826
+ const r = await api(ctx, "DELETE", docPath(id));
5827
+ emit(ctx, { ...r, id }, () => console.log(t(`Deleted ${doc.name}.`, `Borr\xE9 ${doc.name}.`)));
5828
+ }
5829
+ }
5830
+ }
5831
+ };
5832
+
5833
+ // src/commands/video.ts
5834
+ import { existsSync as existsSync17, readFileSync as readFileSync19 } from "fs";
5835
+ import { EasybitsError as EasybitsError6, resolveBaseUrl as resolveBaseUrl9 } from "@easybits.cloud/sdk";
5836
+ var VIDEO_PRESETS = {
5837
+ portrait: [1080, 1920],
5838
+ story: [1080, 1920],
5839
+ reel: [1080, 1920],
5840
+ tiktok: [1080, 1920],
5841
+ square: [1080, 1080],
5842
+ landscape: [1920, 1080],
5843
+ "16-9": [1920, 1080],
5844
+ youtube: [1920, 1080]
5845
+ };
5846
+ var VIDEO_THEMES = ["default", "dark", "light", "brand"];
5847
+ var VOICES = ["em_santa", "em_alex", "ef_dora"];
5848
+ function projectBody(o, usage2) {
5849
+ const body = {};
5850
+ if (o.name) body.name = o.name;
5851
+ if (o.format) {
5852
+ if (o.format in VIDEO_PRESETS) body.format = { preset: o.format };
5853
+ else {
5854
+ const m = /^(\d{3,4})x(\d{3,4})$/i.exec(o.format);
5855
+ if (!m || [m[1], m[2]].some((n) => Number(n) < 100 || Number(n) > 4096)) {
5856
+ throw usageError(t(`Unknown --format "${o.format}". One of: ${Object.keys(VIDEO_PRESETS).join(", ")}, or WxH (100-4096 px).`, `--format desconocido "${o.format}". Uno de: ${Object.keys(VIDEO_PRESETS).join(", ")}, o WxH (100-4096 px).`), usage2);
5857
+ }
5858
+ body.width = Number(m[1]);
5859
+ body.height = Number(m[2]);
5860
+ }
5861
+ }
5862
+ if (o.fps != null) {
5863
+ const n = Number(o.fps);
5864
+ if (!Number.isInteger(n) || n < 1 || n > 60) throw usageError(t("--fps goes from 1 to 60.", "--fps va de 1 a 60."), usage2);
5865
+ body.fps = n;
5866
+ }
5867
+ if (o.theme) {
5868
+ if (!VIDEO_THEMES.includes(o.theme)) throw usageError(t(`--theme is one of: ${VIDEO_THEMES.join(", ")}.`, `--theme es uno de: ${VIDEO_THEMES.join(", ")}.`), usage2);
5869
+ body.theme = o.theme;
5870
+ }
5871
+ return body;
5872
+ }
5873
+ var scenesOf = (p) => [...p.scenes ?? []].sort((a, b) => a.order - b.order);
5874
+ function sceneRef(scenes, ref, usage2) {
5875
+ const byId = scenes.find((s) => s.id === ref);
5876
+ if (byId) return byId;
5877
+ if (/^\d+$/.test(ref)) {
5878
+ const n = Number(ref);
5879
+ if (n >= 1 && n <= scenes.length) return scenes[n - 1];
5880
+ }
5881
+ throw usageError(
5882
+ 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).`),
5883
+ usage2
5884
+ );
5885
+ }
5886
+ function sceneRows(scenes) {
5887
+ 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) : "-" }));
5888
+ }
5889
+ function sceneBody(o, usage2, readFile = (p) => existsSync17(p) ? readFileSync19(p, "utf8") : null) {
5890
+ const body = {};
5891
+ if (o.timeline != null) body.timeline = readFile(o.timeline) ?? o.timeline;
5892
+ if (o.duration != null) {
5893
+ const n = Number(o.duration);
5894
+ if (!Number.isFinite(n) || n < 0.3 || n > 120) throw usageError(t("--duration goes from 0.3 to 120 seconds.", "--duration va de 0.3 a 120 segundos."), usage2);
5895
+ body.durationSec = n;
5896
+ }
5897
+ if (o.label != null) body.label = o.label;
5898
+ if (o.narration != null) body.narration = o.narration;
5899
+ if (o.voice != null) {
5900
+ if (!VOICES.includes(o.voice)) throw usageError(t(`--voice is one of: ${VOICES.join(", ")}.`, `--voice es una de: ${VOICES.join(", ")}.`), usage2);
5901
+ body.narrationVoice = o.voice;
5902
+ }
5903
+ return body;
5904
+ }
5905
+ function renderErrorIsFinal(e) {
5906
+ if (!(e instanceof EasybitsError6)) return false;
5907
+ if (e.status < 500) return true;
5908
+ try {
5909
+ return typeof JSON.parse(e.body)?.error === "string";
5910
+ } catch {
5911
+ return false;
5912
+ }
5913
+ }
5914
+ var SIZE = (p) => `${p.width}x${p.height}`;
5915
+ var projectPath = (id) => `/video-projects/${encodeURIComponent(id)}`;
5916
+ var getProject = (ctx, id) => api(ctx, "GET", projectPath(id));
5917
+ function readHtml2(path, usage2) {
5918
+ if (path === "-") {
5919
+ if (process.stdin.isTTY) throw usageError(t("Expected the HTML on stdin.", "Se esperaba el HTML por stdin."), usage2);
5920
+ return readStdin().then((b) => b.toString("utf8"));
5921
+ }
5922
+ if (!existsSync17(path)) throw usageError(t(`File not found: ${path}`, `No existe el archivo: ${path}`), usage2);
5923
+ return readFileSync19(path, "utf8");
5924
+ }
5925
+ async function postRender(ctx, id, signal) {
5926
+ const token = await resolveCredential(ctx);
5927
+ const base = (await resolveBaseUrl9()).replace(/\/$/, "");
5928
+ const res = await fetch(`${base}/api/v2${projectPath(id)}/render`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, signal });
5929
+ const text = await res.text();
5930
+ if (!res.ok) throw new EasybitsError6(res.status, text);
5931
+ return JSON.parse(text);
5932
+ }
5933
+ var SCENE_FLAGS = {
5934
+ timeline: { type: "string", value: "file|code", description: "GSAP against the paused timeline `tl`: a file, or the code itself" },
5935
+ duration: { type: "string", value: "sec", description: "Seconds on screen (default: fits the narration, else 3)" },
5936
+ label: { type: "string", value: "name", description: "Scene name" },
5937
+ narration: { type: "string", value: "text", description: 'Voiceover text, synthesized at render time ("" removes it)' },
5938
+ voice: { type: "string", value: "id", description: "Narration voice: em_santa (default), em_alex, ef_dora" }
5939
+ };
5940
+ var PROJECTS_USAGE = "easybits video projects <ls|get|create|rm> [<project>] [--name N] [--format F] [--fps N] [--theme T]";
5941
+ var SCENES_USAGE = "easybits video scenes <ls|add|set|rm|reorder> <project> [<scene>] [<file.html|->] [flags]";
5942
+ var MUSIC_USAGE = "easybits video music set <project> <url|none> [--name FILE]";
5943
+ var video = {
5944
+ name: "video",
5945
+ aliases: ["videos"],
5946
+ group: "Content",
5947
+ summary: "Video projects: HTML + GSAP scenes with narration and music, rendered to MP4",
5948
+ synopsis: "video <projects|scenes|\u2026>",
5949
+ subs: {
5950
+ projects: {
5951
+ summary: "List, show, create or delete video projects",
5952
+ usage: PROJECTS_USAGE,
5953
+ options: {
5954
+ name: { type: "string", value: "name", description: "create: project name" },
5955
+ format: { type: "string", value: "preset|WxH", description: "create: portrait (default 1080x1920), square, landscape, story, reel, tiktok, youtube, 16-9, or WxH" },
5956
+ fps: { type: "string", value: "n", description: "create: frames per second (default 30)" },
5957
+ theme: { type: "string", value: "theme", description: "create: background theme: default, dark, light, brand" },
5958
+ ...YES_OPTION
5959
+ },
5960
+ examples: [
5961
+ "easybits video projects ls",
5962
+ 'easybits video projects create --name "Promo oto\xF1o" --format square --json',
5963
+ "easybits video projects get 'Promo oto\xF1o' # scenes, music, last render",
5964
+ "easybits video projects rm 'Promo oto\xF1o' --yes"
5965
+ ],
5966
+ async run(ctx) {
5967
+ const action = need(ctx, 0, "ls|get|create|rm", PROJECTS_USAGE);
5968
+ if (action === "ls" || action === "list") {
5969
+ const r = await api(ctx, "GET", "/video-projects?limit=100");
5970
+ emit(
5971
+ ctx,
5972
+ r,
5973
+ () => table(
5974
+ r.items.map((p2) => ({ ...p2, size: SIZE(p2), duration: `${p2.durationSec.toFixed(1)}s`, render: p2.lastRenderUrl ?? "-", updatedAt: fmtDate(p2.updatedAt) })),
5975
+ [["id", "ID"], ["name", "NAME"], ["sceneCount", "SCENES"], ["duration", "DURATION"], ["size", "SIZE"], ["status", "STATUS"], ["render", "URL"]],
5976
+ t('No video projects yet. Create one: easybits video projects create --name "\u2026"', 'Todav\xEDa no hay proyectos de video. Crea uno: easybits video projects create --name "\u2026"')
5977
+ )
5978
+ );
5979
+ return;
5980
+ }
5981
+ if (action === "create" || action === "new") {
5982
+ const body = projectBody({ name: str(ctx, "name"), format: str(ctx, "format"), fps: str(ctx, "fps"), theme: str(ctx, "theme") }, PROJECTS_USAGE);
5983
+ const { project } = await api(ctx, "POST", "/video-projects", body);
5984
+ emit(ctx, project, () => {
5985
+ console.log(t(`Created ${project.name} (${project.id}) \u2014 ${SIZE(project)} @ ${project.fps} fps.`, `Creado ${project.name} (${project.id}) \u2014 ${SIZE(project)} a ${project.fps} fps.`));
5986
+ console.log(t(`Next: easybits video scenes add ${project.id} scene.html --narration "\u2026"`, `Sigue: easybits video scenes add ${project.id} escena.html --narration "\u2026"`));
5987
+ });
5988
+ return;
5989
+ }
5990
+ if (action !== "get" && action !== "rm") throw usageError(t(`Unknown projects action "${action}".`, `Acci\xF3n de projects desconocida "${action}".`), PROJECTS_USAGE);
5991
+ const id = need(ctx, 1, "project", PROJECTS_USAGE);
5992
+ if (action === "rm") {
5993
+ requireYesIfHeadless(ctx);
5994
+ const p2 = await getProject(ctx, id);
5995
+ await confirm(ctx, t(`Delete the video project "${p2.name}" (${p2.sceneCount} scenes)? Rendered MP4s stay in your files.`, `\xBFBorrar el proyecto de video "${p2.name}" (${p2.sceneCount} escenas)? Los MP4 renderizados se quedan en tus archivos.`));
5996
+ const r = await api(ctx, "DELETE", projectPath(id));
5997
+ emit(ctx, { ...r, id }, () => console.log(t(`Deleted ${p2.name}.`, `Borr\xE9 ${p2.name}.`)));
5998
+ return;
5999
+ }
6000
+ const p = await getProject(ctx, id);
6001
+ emit(ctx, p, () => {
6002
+ console.log(`${p.name} (${p.id})`);
6003
+ console.log(`${SIZE(p)} @ ${p.fps} fps ${t("Theme", "Tema")}: ${p.theme} ${t("Status", "Estado")}: ${p.status}${p.failReason ? ` \u2014 ${p.failReason}` : ""}`);
6004
+ console.log(`${t("Music", "M\xFAsica")}: ${p.audioAssetName ?? "-"} ${t("Duration", "Duraci\xF3n")}: ${p.durationSec.toFixed(1)}s`);
6005
+ if (p.lastRenderUrl) console.log(`MP4: ${p.lastRenderUrl}`);
6006
+ console.log("");
6007
+ table(sceneRows(scenesOf(p)), [["n", "#"], ["id", "ID"], ["label", "NAME"], ["durationSec", "DURATION"], ["narration", "NARRATION"]], t("No scenes yet.", "Todav\xEDa no hay escenas."));
6008
+ });
6009
+ }
6010
+ },
6011
+ scenes: {
6012
+ summary: "List, add, edit, delete or reorder a project's scenes",
6013
+ usage: SCENES_USAGE,
6014
+ options: {
6015
+ ...SCENE_FLAGS,
6016
+ after: { type: "string", value: "n", description: "add: insert after scene N (0 = first; default: at the end)" },
6017
+ ...YES_OPTION
6018
+ },
6019
+ examples: [
6020
+ "easybits video scenes ls 'Promo oto\xF1o'",
6021
+ `easybits video scenes add 'Promo oto\xF1o' intro.html --timeline intro.js --narration "Lleg\xF3 el oto\xF1o"`,
6022
+ "easybits video scenes set 'Promo oto\xF1o' 2 --duration 4 --voice ef_dora",
6023
+ "easybits video scenes set 'Promo oto\xF1o' 2 escena2.html # replace its HTML",
6024
+ "easybits video scenes reorder 'Promo oto\xF1o' 2 1 3",
6025
+ "easybits video scenes rm 'Promo oto\xF1o' 3 --yes"
6026
+ ],
6027
+ async run(ctx) {
6028
+ const action = need(ctx, 0, "ls|add|set|rm|reorder", SCENES_USAGE);
6029
+ if (!["ls", "add", "set", "rm", "reorder"].includes(action)) throw usageError(t(`Unknown scenes action "${action}".`, `Acci\xF3n de scenes desconocida "${action}".`), SCENES_USAGE);
6030
+ const id = need(ctx, 1, "project", SCENES_USAGE);
6031
+ const flags = () => sceneBody({ timeline: str(ctx, "timeline"), duration: str(ctx, "duration"), label: str(ctx, "label"), narration: str(ctx, "narration"), voice: str(ctx, "voice") }, SCENES_USAGE);
6032
+ if (action === "add") {
6033
+ const body2 = { ...flags(), html: await readHtml2(need(ctx, 2, "file.html|-", SCENES_USAGE), SCENES_USAGE) };
6034
+ const after = str(ctx, "after");
6035
+ if (after != null) {
6036
+ if (!/^\d+$/.test(after)) throw usageError(t("--after must be a scene number (0 = first).", "--after debe ser un n\xFAmero de escena (0 = al inicio)."), SCENES_USAGE);
6037
+ body2.afterIndex = Number(after) - 1;
6038
+ }
6039
+ const r2 = await api(ctx, "POST", `${projectPath(id)}/scenes`, body2);
6040
+ emit(ctx, r2, () => console.log(t(`Added ${r2.scene.label ?? r2.scene.id} (${r2.scene.durationSec}s). ${r2.sceneCount} scene(s).`, `Agregu\xE9 ${r2.scene.label ?? r2.scene.id} (${r2.scene.durationSec}s). ${r2.sceneCount} escena(s).`)));
6041
+ return;
6042
+ }
6043
+ if (action === "rm") requireYesIfHeadless(ctx);
6044
+ const scenes = scenesOf(await getProject(ctx, id));
6045
+ if (action === "ls") {
6046
+ const rows = sceneRows(scenes);
6047
+ emit(ctx, rows, () => table(rows, [["n", "#"], ["id", "ID"], ["label", "NAME"], ["durationSec", "DURATION"], ["narration", "NARRATION"]], t("No scenes yet.", "Todav\xEDa no hay escenas.")));
6048
+ return;
6049
+ }
6050
+ if (action === "reorder") {
6051
+ const refs = ctx.args.slice(2);
6052
+ const ids = refs.map((r3) => sceneRef(scenes, r3, SCENES_USAGE).id);
6053
+ if (ids.length !== scenes.length || new Set(ids).size !== ids.length) {
6054
+ throw usageError(t(`Give every scene exactly once (${scenes.length} scenes).`, `Pasa cada escena exactamente una vez (${scenes.length} escenas).`), SCENES_USAGE);
6055
+ }
6056
+ const r2 = await api(ctx, "PUT", `${projectPath(id)}/scenes`, { sceneIds: ids });
6057
+ emit(ctx, r2, () => table(r2.scenes.map((s) => ({ n: s.order + 1, id: s.id, label: s.label ?? "-" })), [["n", "#"], ["id", "ID"], ["label", "NAME"]]));
6058
+ return;
6059
+ }
6060
+ const scene = sceneRef(scenes, need(ctx, 2, "scene", SCENES_USAGE), SCENES_USAGE);
6061
+ const n = scenes.indexOf(scene) + 1;
6062
+ if (action === "rm") {
6063
+ await confirm(ctx, t(`Delete scene ${n} (${scene.label ?? scene.id})?`, `\xBFBorrar la escena ${n} (${scene.label ?? scene.id})?`));
6064
+ const r2 = await api(ctx, "DELETE", `${projectPath(id)}/scenes/${encodeURIComponent(scene.id)}`);
6065
+ emit(ctx, r2, () => console.log(t(`Deleted. ${r2.sceneCount} scene(s) left.`, `Borrada. Quedan ${r2.sceneCount} escena(s).`)));
6066
+ return;
6067
+ }
6068
+ const body = flags();
6069
+ if (ctx.args[3]) body.html = await readHtml2(ctx.args[3], SCENES_USAGE);
6070
+ if (Object.keys(body).length === 0) throw usageError(t("Nothing to change: pass a file.html and/or --timeline, --duration, --label, --narration, --voice.", "Nada que cambiar: pasa un archivo.html y/o --timeline, --duration, --label, --narration, --voice."), SCENES_USAGE);
6071
+ const r = await api(ctx, "PATCH", `${projectPath(id)}/scenes/${encodeURIComponent(scene.id)}`, body);
6072
+ emit(ctx, r, () => console.log(r.changed ? t(`Scene ${n} updated.`, `Escena ${n} actualizada.`) : t("No change: the scene already had that.", "Sin cambios: la escena ya ten\xEDa eso.")));
6073
+ }
6074
+ },
6075
+ music: {
6076
+ summary: "Set or remove the background music (a public audio URL)",
6077
+ usage: MUSIC_USAGE,
6078
+ options: { name: { type: "string", value: "file", description: "File name inside the bundle (default: from the URL)" } },
6079
+ examples: [
6080
+ "easybits video music set 'Promo oto\xF1o' https://cdn.example.com/bed.mp3",
6081
+ "easybits video music set 'Promo oto\xF1o' none # remove it"
6082
+ ],
6083
+ async run(ctx) {
6084
+ const action = need(ctx, 0, "set", MUSIC_USAGE);
6085
+ if (action !== "set") throw usageError(t(`Unknown music action "${action}".`, `Acci\xF3n de music desconocida "${action}".`), MUSIC_USAGE);
6086
+ const id = need(ctx, 1, "project", MUSIC_USAGE);
6087
+ const url = need(ctx, 2, "url|none", MUSIC_USAGE);
6088
+ const clear = url === "none";
6089
+ if (!clear && !/^https?:\/\//.test(url)) throw usageError(t("Give a public http(s) URL (upload a local file first: easybits files upload song.mp3 --public).", "Pasa una URL p\xFAblica http(s) (sube primero el archivo local: easybits files upload cancion.mp3 --public)."), MUSIC_USAGE);
6090
+ const r = await api(ctx, "POST", `${projectPath(id)}/audio`, { url: clear ? null : url, name: str(ctx, "name") });
6091
+ emit(ctx, r, () => console.log(r.audio ? t(`Music: ${r.audio.name}`, `M\xFAsica: ${r.audio.name}`) : t("Music removed.", "M\xFAsica quitada.")));
6092
+ }
6093
+ },
6094
+ render: {
6095
+ summary: "Render the project to MP4 on the server; --wait follows it to the end",
6096
+ usage: "easybits video render <project> [--wait] [--out FILE.mp4] [--timeout MIN]",
6097
+ options: {
6098
+ wait: { type: "boolean", description: "Wait for the MP4 (spinner in a terminal); without it, start and return" },
6099
+ out: { type: "string", value: "file", description: "With --wait: also download the MP4 here" },
6100
+ timeout: { type: "string", value: "min", description: "With --wait: give up after this many minutes (default 15)" }
6101
+ },
6102
+ examples: [
6103
+ "easybits video render 'Promo oto\xF1o' --wait --out promo.mp4",
6104
+ "easybits video render 'Promo oto\xF1o' # starts it; then:",
6105
+ "easybits video projects get 'Promo oto\xF1o' --json # status: rendering \u2192 ready | failed"
6106
+ ],
6107
+ async run(ctx) {
6108
+ const id = need(ctx, 0, "project", this.usage);
6109
+ const wait = bool(ctx, "wait");
6110
+ const out = str(ctx, "out");
6111
+ if (out && !wait) throw usageError(t("--out needs --wait.", "--out necesita --wait."), this.usage);
6112
+ const minutes = Number(str(ctx, "timeout") ?? 15);
6113
+ if (!Number.isFinite(minutes) || minutes <= 0) throw usageError(t("--timeout is minutes, > 0.", "--timeout son minutos, > 0."), this.usage);
6114
+ const before = await getProject(ctx, id);
6115
+ if (before.sceneCount === 0) throw usageError(t("The project has no scenes: easybits video scenes add \u2026", "El proyecto no tiene escenas: easybits video scenes add \u2026"), this.usage);
6116
+ const p = progress(ctx, t(`Rendering ${before.name} (${before.durationSec.toFixed(1)}s of video)\u2026`, `Renderizando ${before.name} (${before.durationSec.toFixed(1)}s de video)\u2026`));
6117
+ const ctrl = new AbortController();
6118
+ let post = null;
6119
+ let postDone = false;
6120
+ let postResult = null;
6121
+ let postError = null;
6122
+ if (before.status !== "rendering") {
6123
+ post = postRender(ctx, id, ctrl.signal).then(
6124
+ (r) => (postDone = true, postResult = r),
6125
+ (e) => (postDone = true, postError = e)
6126
+ );
6127
+ }
6128
+ try {
6129
+ if (!wait) {
6130
+ for (let i = 0; i < 30 && !postDone; i++) {
6131
+ if ((await getProject(ctx, id)).status === "rendering") break;
6132
+ await sleep(1e3);
6133
+ }
6134
+ if (postError && renderErrorIsFinal(postError)) throw postError;
6135
+ if (!postDone) ctrl.abort();
6136
+ p.stop();
6137
+ const state2 = postResult ? postResult : await getProject(ctx, id);
6138
+ emit(ctx, state2, () => {
6139
+ if (state2.status === "ready" && state2.lastRenderUrl) console.log(`MP4: ${state2.lastRenderUrl}`);
6140
+ else console.log(t(`Rendering on the server. Follow it: easybits video render ${id} --wait`, `Renderizando en el servidor. S\xEDguelo: easybits video render ${id} --wait`));
6141
+ });
6142
+ return;
6143
+ }
6144
+ if (post) await post;
6145
+ if (postError && renderErrorIsFinal(postError)) throw postError;
6146
+ let final = null;
6147
+ const deadline = Date.now() + minutes * 6e4;
6148
+ while (!postResult) {
6149
+ final = await getProject(ctx, id);
6150
+ if (final.status !== "rendering") break;
6151
+ if (Date.now() > deadline) throw new CliError(t(`Still rendering after ${minutes} min.`, `Sigue renderizando despu\xE9s de ${minutes} min.`), EXIT.API, t(`Check later: easybits video projects get ${id}`, `Revisa luego: easybits video projects get ${id}`));
6152
+ p.update(t(`Rendering ${before.name}\u2026 (status: ${final.status})`, `Renderizando ${before.name}\u2026 (estado: ${final.status})`));
6153
+ await sleep(3e3);
6154
+ }
6155
+ const state = postResult ?? final;
6156
+ if (state.status === "failed" || !state.lastRenderUrl) {
6157
+ const reason = state.failReason ?? (await getProject(ctx, id)).failReason;
6158
+ throw new CliError(t(`Render failed: ${reason ?? "unknown"}`, `Fall\xF3 el render: ${reason ?? "desconocido"}`), EXIT.API);
6159
+ }
6160
+ let bytes;
6161
+ if (out) {
6162
+ p.update(t("Downloading the MP4\u2026", "Bajando el MP4\u2026"));
6163
+ bytes = await download(state.lastRenderUrl, out);
6164
+ }
6165
+ p.stop();
6166
+ emit(ctx, { ...state, ...out ? { out, bytes } : {} }, () => {
6167
+ console.log(`MP4: ${state.lastRenderUrl}`);
6168
+ if (out) console.log(t(`Saved to ${out}`, `Guardado en ${out}`));
6169
+ });
6170
+ } finally {
6171
+ p.stop();
6172
+ }
6173
+ }
6174
+ },
6175
+ ls: {
6176
+ aliases: ["list"],
6177
+ summary: "List AI-generated videos (prompt \u2192 clip), newest first",
6178
+ usage: "easybits video ls [--limit N]",
6179
+ options: { limit: { type: "string", value: "n", description: "How many (default 50, max 100)" } },
6180
+ examples: ["easybits video ls", "easybits video ls --limit 5 --json"],
6181
+ async run(ctx) {
6182
+ const limit = str(ctx, "limit");
6183
+ const r = await api(ctx, "GET", `/videos${limit ? `?limit=${encodeURIComponent(limit)}` : ""}`);
6184
+ emit(
6185
+ ctx,
6186
+ r.videos,
6187
+ () => table(
6188
+ r.videos.map((v) => ({ ...v, prompt: v.prompt.slice(0, 40), duration: `${v.duration}s`, createdAt: fmtDate(v.createdAt) })),
6189
+ [["id", "ID"], ["prompt", "PROMPT"], ["status", "STATUS"], ["duration", "DURATION"], ["createdAt", "CREATED"]],
6190
+ t("No generated videos yet.", "Todav\xEDa no hay videos generados.")
6191
+ )
6192
+ );
6193
+ }
6194
+ }
6195
+ }
6196
+ };
6197
+
6198
+ // src/commands/index.ts
6199
+ var COMMANDS = [
6200
+ login,
6201
+ logout,
6202
+ whoami,
3809
6203
  doctor2,
3810
6204
  usage,
6205
+ secrets,
3811
6206
  sandboxes,
3812
6207
  agents,
6208
+ fleet,
3813
6209
  apply,
3814
6210
  machines,
6211
+ sites,
3815
6212
  domains,
3816
6213
  init,
3817
6214
  db,
3818
6215
  files,
3819
6216
  websites,
6217
+ documents,
6218
+ video,
3820
6219
  providers,
6220
+ webCmd,
6221
+ webhooks,
6222
+ keys,
3821
6223
  mcp,
3822
6224
  sshKey,
3823
6225
  sshProxy,
@@ -3892,7 +6294,7 @@ var HELP_ES = {
3892
6294
  "Last N lines (default 200)": "\xDAltimos N renglones (default 200)",
3893
6295
  'journalctl time spec, e.g. "10 min ago"': 'Tiempo en sintaxis de journalctl, p. ej. "10 min ago"',
3894
6296
  "Only lines matching": "S\xF3lo los renglones que coincidan",
3895
- "List, read or write files inside a sandbox": "Enlista, lee o escribe archivos dentro de un sandbox",
6297
+ "List, read, write, delete, move files and make folders inside a sandbox": "Enlista, lee, escribe, borra y mueve archivos y crea carpetas dentro de un sandbox",
3896
6298
  "write: inline content instead of a local file": "write: contenido en l\xEDnea en vez de un archivo local",
3897
6299
  "read: save to a local file (binary-safe)": "read: guarda en un archivo local (seguro con binarios)",
3898
6300
  "Suspend to disk (frees CPU; TTL paused)": "Lo suspende a disco (libera CPU; el TTL se pausa)",
@@ -3965,6 +6367,8 @@ var HELP_ES = {
3965
6367
  "a copy": "una copia",
3966
6368
  "what would be removed": "lo que se quitar\xEDa",
3967
6369
  "Hosting": "Hosting",
6370
+ "Your site-builder sites (/dash/sitios): static and web apps": "Tus sitios del creador (/dash/sitios): est\xE1ticos y web apps",
6371
+ "List your sites with their kind, engine and public URL": "Enlista tus sitios con su tipo, motor y URL p\xFAblica",
3968
6372
  "Permanent machines and their releases (easybits deploy <machine> publishes)": "M\xE1quinas permanentes y sus releases (easybits deploy <m\xE1quina> publica)",
3969
6373
  "List your permanent machines": "Enlista tus m\xE1quinas permanentes",
3970
6374
  "Publish the machine's current app code as a new release": "Publica el c\xF3digo actual de la app como un release nuevo",
@@ -3993,15 +6397,54 @@ var HELP_ES = {
3993
6397
  "Last N lines": "\xDAltimos N renglones",
3994
6398
  "Put a previous release back on the same machine (data untouched)": "Vuelve a poner un release anterior en la misma m\xE1quina (datos intactos)",
3995
6399
  "App env vars stored encrypted in your vault": "Variables de entorno de la app, cifradas en tu vault",
3996
- "Custom domains with auto TLS on a sandbox or machine": "Dominios propios con TLS autom\xE1tico en un sandbox o m\xE1quina",
3997
- "List domains attached to a sandbox": "Enlista los dominios de un sandbox",
3998
- "Attach a domain to a port; prints the DNS record to create": "Liga un dominio a un puerto; imprime el registro DNS que hay que crear",
3999
- "Port inside the sandbox (required)": "Puerto dentro del sandbox (obligatorio)",
4000
- "Check DNS + HTTPS for a domain": "Revisa DNS + HTTPS de un dominio",
4001
- "Detach a domain": "Desliga un dominio",
6400
+ "Your own domains for your websites (sandbox ports: sandboxes domains)": "Dominios propios para tus sitios (puertos de una caja: sandboxes domains)",
6401
+ "List the custom domains of your account": "Enlista los dominios propios de tu cuenta",
6402
+ "Register a domain; prints the DNS records to create": "Registra un dominio; imprime los registros DNS que hay que crear",
6403
+ "Check the TXT record and issue the certificate": "Revisa el registro TXT y emite el certificado",
6404
+ "Write the DNS records into your Mailmask zone and verify (--undo restores)": "Escribe los registros DNS en tu zona de Mailmask y verifica (--undo los restaura)",
6405
+ "Restore the records EasyBits wrote (keeps the ones you changed)": "Restaura los registros que escribi\xF3 EasyBits (respeta los que cambiaste)",
6406
+ "Serve one of your websites at the root domain and www (--clear removes it)": "Sirve uno de tus sitios en la ra\xEDz del dominio y en www (--clear lo quita)",
6407
+ "Stop serving a website at the root": "Deja de servir un sitio en la ra\xEDz",
6408
+ "Remove a domain: unlinks its websites and deletes its certificates": "Quita un dominio: desliga sus sitios y borra sus certificados",
6409
+ "Custom domains with auto TLS on a sandbox or machine port": "Dominios propios con TLS autom\xE1tico en un puerto de un sandbox o m\xE1quina",
6410
+ "add: port inside the sandbox (required)": "add: puerto dentro del sandbox (obligatorio)",
6411
+ "Web": "Web",
6412
+ "Search, read, extract and crawl the web (metered in web queries)": "Busca, lee, extrae y rastrea la web (se mide en consultas web)",
6413
+ "Search Google, Bing, Yandex or DuckDuckGo (1 query)": "Busca en Google, Bing, Yandex o DuckDuckGo (1 consulta)",
6414
+ "google (default), bing, yandex or duckduckgo": "google (default), bing, yandex o duckduckgo",
6415
+ "ISO country for localized results (mx, us\u2026)": "Pa\xEDs ISO para resultados locales (mx, us\u2026)",
6416
+ "Read a page even if it blocks bots: raw HTML or clean markdown (1 query)": "Lee una p\xE1gina aunque bloquee bots: HTML crudo o markdown limpio (1 consulta)",
6417
+ "Clean markdown instead of raw HTML": "Markdown limpio en vez de HTML crudo",
6418
+ "With --markdown: drop nav, footer and icons": "Con --markdown: quita men\xFA, pie e iconos",
6419
+ "ISO country to fetch from (mx, us\u2026)": "Pa\xEDs ISO desde donde leer (mx, us\u2026)",
6420
+ "Save the body to a file instead of stdout": "Guarda el contenido en un archivo en vez de stdout",
6421
+ "Structured records from a known source (Google Maps, Mercado Libre\u2026); 1 query per record": "Registros estructurados de una fuente conocida (Google Maps, Mercado Libre\u2026); 1 consulta por registro",
6422
+ 'Input for the source: [{"keyword":"\u2026","country":"MX"}], {"query":"\u2026"} or [{"url":"\u2026"}]': 'Entrada seg\xFAn la fuente: [{"keyword":"\u2026","country":"MX"}], {"query":"\u2026"} o [{"url":"\u2026"}]',
6423
+ "Shortcut for --input '[{\"url\":\u2026}]' (repeatable)": `Atajo de --input '[{"url":\u2026}]' (repetible)`,
6424
+ "Max records per input (default 20, max 200)": "Registros m\xE1ximos por entrada (default 20, m\xE1x. 200)",
6425
+ "Catalog dataset id, for sources not on the list (source becomes optional)": "Id de dataset del cat\xE1logo, para fuentes fuera de la lista (source se vuelve opcional)",
6426
+ "Wait for the job to finish (polls every 15 s, up to 5 min)": "Espera a que termine el trabajo (revisa cada 15 s, hasta 5 min)",
6427
+ "Check an extract job; when done, prints its records": "Revisa un trabajo de extract; si ya termin\xF3, imprime sus registros",
6428
+ "Read a page and follow its internal links as markdown (1 query per page)": "Lee una p\xE1gina y sigue sus links internos, en markdown (1 consulta por p\xE1gina)",
6429
+ "Pages to read, 1-20 (default 10)": "P\xE1ginas a leer, 1-20 (default 10)",
6430
+ "Drop nav, footer and icons from each page (good for RAG)": "Quita men\xFA, pie e iconos de cada p\xE1gina (bueno para RAG)",
6431
+ "Webhooks: HTTPS POSTs signed with HMAC when your files, websites or forms change": "Webhooks: POSTs HTTPS firmados con HMAC cuando cambian tus archivos, sitios o formularios",
6432
+ "List your webhooks": "Enlista tus webhooks",
6433
+ "Create a webhook; prints its signing secret ONCE": "Crea un webhook; imprime su secreto de firma UNA vez",
6434
+ "Event to deliver (repeatable or comma-separated): file.created, website.deleted, form.submitted\u2026": "Evento a entregar (repetible o separado por comas): file.created, website.deleted, form.submitted\u2026",
6435
+ "Change a webhook's URL or events, or pause/resume it": "Cambia la URL o los eventos de un webhook, o lo pausa/reanuda",
6436
+ "New destination URL": "URL de destino nueva",
6437
+ "Stop deliveries (status PAUSED)": "Detiene las entregas (estado PAUSED)",
6438
+ "Deliver again and reset the failure count (status ACTIVE)": "Vuelve a entregar y reinicia el conteo de fallas (estado ACTIVE)",
6439
+ "Delete a webhook": "Borra un webhook",
6440
+ "API keys of your account: list, create, revoke": "API keys de tu cuenta: enlistar, crear, revocar",
6441
+ "List your API keys (prefix only; the full key is never shown again)": "Enlista tus API keys (s\xF3lo el prefijo; la key completa no se vuelve a mostrar)",
6442
+ "Create an API key; prints it ONCE": "Crea una API key; la imprime UNA vez",
6443
+ "Comma-separated: READ, WRITE, DELETE, ADMIN (default READ,WRITE,DELETE; never more than your own credential has)": "Separados por comas: READ, WRITE, DELETE, ADMIN (default READ,WRITE,DELETE; nunca m\xE1s de los que tiene tu credencial)",
6444
+ "Expiry date (ISO, e.g. 2027-01-31); default never": "Fecha de vencimiento (ISO, p. ej. 2027-01-31); default nunca",
6445
+ "Revoke an API key (by id, name or prefix); anything using it stops working": "Revoca una API key (por id, nombre o prefijo); lo que la use deja de funcionar",
4002
6446
  "Write the GitHub Actions deploy workflow for this repo": "Escribe el workflow de GitHub Actions que despliega este repo",
4003
6447
  "Write the GitHub Actions workflow that deploys this repo on every push to main": "Escribe el workflow de GitHub Actions que despliega este repo en cada push a main",
4004
- "Where the app lives in the machine (default /srv/app)": "D\xF3nde vive la app en la m\xE1quina (default /srv/app)",
4005
6448
  "Port your app listens on (default 3000)": "Puerto en el que escucha tu app (default 3000)",
4006
6449
  "Data": "Datos",
4007
6450
  "SQL databases (libSQL)": "Bases de datos SQL (libSQL)",
@@ -4027,7 +6470,14 @@ var HELP_ES = {
4027
6470
  "Public file with an open CDN URL (default: private)": "Archivo p\xFAblico con URL abierta en el CDN (por defecto: privado)",
4028
6471
  "Delete a file (soft delete, 7-day trash)": "Borra un archivo (a la papelera 7 d\xEDas)",
4029
6472
  "List your static websites": "Enlista tus sitios est\xE1ticos",
4030
- "Static websites: list, create, change slug": "Sitios est\xE1ticos: enlistar, crear, cambiar slug",
6473
+ "Static websites: list, create, upload, deploy a folder, delete": "Sitios est\xE1ticos: enlistar, crear, subir, publicar una carpeta, borrar",
6474
+ "Show one website (by slug or id)": "Muestra un sitio (por slug o id)",
6475
+ "List a website's files (paths inside the site)": "Enlista los archivos de un sitio (rutas dentro del sitio)",
6476
+ "Upload one file to a website (public; replaces the same path)": "Sube un archivo a un sitio (p\xFAblico; reemplaza la misma ruta)",
6477
+ "Path inside the site (default: the file name), e.g. img/hero.jpg": "Ruta dentro del sitio (default: el nombre del archivo), p. ej. img/hero.jpg",
6478
+ "Upload a whole folder (e.g. your build) keeping its paths; same paths are replaced": "Sube una carpeta entera (p. ej. tu build) con sus rutas; las mismas rutas se reemplazan",
6479
+ "List what would be uploaded; upload nothing": "Enlista lo que se subir\xEDa; no sube nada",
6480
+ "Delete a website and its files (asks you to type the slug)": "Borra un sitio y sus archivos (pide teclear el slug)",
4031
6481
  "Create an empty static website (optionally with your own slug)": "Crea un sitio est\xE1tico vac\xEDo (con tu propio slug si quieres)",
4032
6482
  "Public slug: /s/<slug>/ (a-z, 0-9, hyphens, 3-60 chars)": "Slug p\xFAblico: /s/<slug>/ (a-z, 0-9, guiones, 3-60 caracteres)",
4033
6483
  "Change a website's slug (the old URL redirects 301 to the new one)": "Cambia el slug de un sitio (la URL vieja redirige 301 a la nueva)",
@@ -4057,7 +6507,147 @@ var HELP_ES = {
4057
6507
  "Column for the link (default: image_url, photo_url, imagen\u2026)": "Columna de la liga (default: image_url, photo_url, imagen\u2026)",
4058
6508
  "Also replace rows that already have a permanent link": "Reemplaza tambi\xE9n las filas que ya tienen liga permanente",
4059
6509
  "Show the plan; upload nothing": "Ense\xF1a el plan; no sube nada",
4060
- "how many links are permanent, external or empty": "cu\xE1ntas ligas son permanentes, de fuera o vac\xEDas"
6510
+ "how many links are permanent, external or empty": "cu\xE1ntas ligas son permanentes, de fuera o vac\xEDas",
6511
+ // sites create / theme
6512
+ "Create a site from a prompt; its builder agent picks it up in the editor": "Crea un sitio a partir de un prompt; su agente lo sigue construyendo en el editor",
6513
+ "What to build; also names the site if --name is missing": "Qu\xE9 construir; tambi\xE9n nombra el sitio si falta --name",
6514
+ "Site name": "Nombre del sitio",
6515
+ "webapp (default: its own machine, published later) or static (CDN website)": "webapp (default: su propia m\xE1quina, se publica despu\xE9s) o static (website en el CDN)",
6516
+ "Web app stack: tanstack (default), next, react-router, remix, nuxt, sveltekit, astro, elysia": "Stack de la web app: tanstack (default), next, react-router, remix, nuxt, sveltekit, astro, elysia",
6517
+ "Starting template (tanstack/react-router): consultorio, despacho, portafolio, lanzamiento": "Plantilla de partida (tanstack/react-router): consultorio, despacho, portafolio, lanzamiento",
6518
+ "Read or change a site's color theme without a model turn (the \xABTema\xBB button)": "Lee o cambia el tema de colores de un sitio sin turno del modelo (el bot\xF3n \xABTema\xBB)",
6519
+ "Palette id: minimal, calido, oceano, noche, dracula, nord, tokyo-night, github-light\u2026": "Id de la paleta: minimal, calido, oceano, noche, dracula, nord, tokyo-night, github-light\u2026",
6520
+ "JSON with every token as #rrggbb (- reads stdin)": "JSON con cada token en #rrggbb (- lee stdin)",
6521
+ "edit, then:": "ed\xEDtalo y luego:",
6522
+ // machines get/restart/redeploy/backups/backup/runspec/rm/tiers
6523
+ "Show a machine: tier, size, price, status and whether it sleeps": "Muestra una m\xE1quina: tier, tama\xF1o, precio, estado y si duerme",
6524
+ "Restart the app's process with the current secrets and runspec (seconds, no build)": "Reinicia el proceso de la app con los secretos y el runspec actuales (segundos, sin build)",
6525
+ "Build a NEW machine from a release: recover a lost one, or change its tier (resize)": "Construye una m\xE1quina NUEVA desde un release: recupera una perdida o cambia su tier (resize)",
6526
+ "Release to build from (default: the machine's newest)": "Release de d\xF3nde construir (default: el m\xE1s nuevo de la m\xE1quina)",
6527
+ "Size of the new machine (default: same as the release's)": "Tama\xF1o de la m\xE1quina nueva (default: el mismo del release)",
6528
+ "Label for the new machine": "Nombre de la m\xE1quina nueva",
6529
+ "shared (default) or reserved": "shared (default) o reserved",
6530
+ "Release the old machine once the new one runs (otherwise you pay for both)": "Libera la m\xE1quina vieja cuando la nueva corra (si no, pagas las dos)",
6531
+ "resize": "cambiar tama\xF1o",
6532
+ "recover from a release": "recuperar desde un release",
6533
+ "List the daily data backups, newest first (kept 7 days)": "Enlista los respaldos diarios de datos, el m\xE1s nuevo primero (se guardan 7 d\xEDas)",
6534
+ "Take a data backup now instead of waiting for tonight's": "Toma un respaldo de datos ya, sin esperar al de la noche",
6535
+ "How the app is built, started and backed up (set merges with what is there)": "C\xF3mo se construye, arranca y respalda la app (set se mezcla con lo que ya hay)",
6536
+ "Runspec as JSON (- reads stdin); flags win over it": "Runspec en JSON (- lee stdin); las banderas ganan sobre \xE9l",
6537
+ "Where the app lives in the machine": "D\xF3nde vive la app en la m\xE1quina",
6538
+ "Build command": "Comando de build",
6539
+ "Start command (used when there is no systemd unit)": "Comando de arranque (se usa cuando no hay unit de systemd)",
6540
+ "systemd unit to restart (preferred over --start)": "Unit de systemd a reiniciar (preferida sobre --start)",
6541
+ "Port the app listens on": "Puerto donde escucha la app",
6542
+ "on: releases ship the build and a deploy only starts it": "on: los releases llevan el build y un deploy s\xF3lo lo arranca",
6543
+ "Path kept out of the release (repeatable)": "Ruta que no entra al release (repetible)",
6544
+ "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",
6545
+ "The hosting catalog: sizes and monthly prices in MXN": "El cat\xE1logo de hosting: tama\xF1os y precio mensual en MXN",
6546
+ // secrets (vault de la cuenta)
6547
+ "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",
6548
+ "List the names in your vault (values never come back)": "Enlista los nombres de tu b\xF3veda (los valores nunca regresan)",
6549
+ "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",
6550
+ "from a password manager": "desde un gestor de contrase\xF1as",
6551
+ "Delete secrets by name; whatever references them stops resolving": "Borra secretos por nombre; lo que los use deja de resolverlos",
6552
+ // sandboxes files rm|mv|mkdir, bg, git
6553
+ "rm: a folder and everything in it": "rm: una carpeta y todo lo que tiene",
6554
+ "Long-running processes: start one in the background, list, poll its output, kill it": "Procesos largos: arranca uno en segundo plano, enl\xEDstalos, lee su salida, detenlo",
6555
+ "kill: seconds between SIGTERM and SIGKILL (default 5)": "kill: segundos entre SIGTERM y SIGKILL (default 5)",
6556
+ "status, exit code, stdout/stderr": "estado, c\xF3digo de salida, stdout/stderr",
6557
+ "Git inside a sandbox: clone, status, commit, push, pull, checkout, log": "Git dentro de un sandbox: clone, status, commit, push, pull, checkout, log",
6558
+ "Repo folder inside the sandbox (required)": "Carpeta del repo dentro del sandbox (obligatoria)",
6559
+ "clone: repo URL (clean; the token goes in --git-token)": "clone: URL del repo (limpia; el token va en --git-token)",
6560
+ "clone/push/checkout: branch": "clone/push/checkout: rama",
6561
+ "clone: shallow depth (default 1)": "clone: profundidad del clone superficial (default 1)",
6562
+ "clone: a specific commit (full clone, detached HEAD)": "clone: un commit concreto (clone completo, HEAD desprendido)",
6563
+ "Username for the token (default: x-access-token)": "Usuario del token (default: x-access-token)",
6564
+ "commit: message": "commit: mensaje",
6565
+ "commit: stage everything first (git add -A)": "commit: agrega todo antes (git add -A)",
6566
+ "commit: only these paths (repeatable); log: only this path": "commit: s\xF3lo estas rutas (repetible); log: s\xF3lo esta ruta",
6567
+ "commit: author name": "commit: nombre del autor",
6568
+ "commit: author email": "commit: correo del autor",
6569
+ "push: remote (default origin)": "push: remoto (default origin)",
6570
+ "push: set the upstream branch": "push: fija la rama upstream",
6571
+ "push: --force-with-lease (fails if the remote moved)": "push: --force-with-lease (falla si el remoto se movi\xF3)",
6572
+ "pull: rebase instead of merge": "pull: rebase en vez de merge",
6573
+ "checkout: create the branch (-B, safe to repeat)": "checkout: crea la rama (-B, seguro de repetir)",
6574
+ "checkout --create: start point": "checkout --create: punto de partida",
6575
+ "log: how many": "log: cu\xE1ntos",
6576
+ // fleet
6577
+ "Fleet agents: one agent, many conversations (WhatsApp, WABA, Teams, web)": "Agentes de flota: un agente, muchas conversaciones (WhatsApp, WABA, Teams, web)",
6578
+ "List your fleet agents": "Enlista tus agentes de flota",
6579
+ "Show a fleet agent: engine, model, effort, prompt and channels": "Muestra un agente de flota: motor, modelo, esfuerzo, prompt y canales",
6580
+ "The catalog and current state: engines, models, tool buckets, MCPs, skills, channels": "El cat\xE1logo y el estado actual: motores, modelos, grupos de tools, MCPs, skills, canales",
6581
+ "Filter the capabilities catalog": "Filtra el cat\xE1logo de capacidades",
6582
+ "Change name, system prompt, engine, model, effort or number mode; --recycle applies it now": "Cambia nombre, system prompt, motor, modelo, esfuerzo o modo de n\xFAmero; --recycle lo aplica ya",
6583
+ "Display name and the name the agent goes by": "Nombre visible y el nombre con el que se presenta el agente",
6584
+ "System prompt for every channel (- reads stdin)": "System prompt para todos los canales (- lee stdin)",
6585
+ "Engine (claude, deepseek, codex\u2026); recycles its boxes by itself": "Motor (claude, deepseek, codex\u2026); recicla sus cajas solo",
6586
+ "Model of the current engine (see fleet capabilities)": "Modelo del motor actual (mira fleet capabilities)",
6587
+ "low, medium, high, xhigh or max": "low, medium, high, xhigh o max",
6588
+ "on: dedicated number (no name prefix on replies)": "on: n\xFAmero dedicado (las respuestas no llevan el nombre al inicio)",
6589
+ "Recycle its running boxes so the change applies on the next turn": "Recicla sus cajas vivas para que el cambio aplique en el siguiente turno",
6590
+ "Send one message to a fleet agent and print its reply": "Manda un mensaje a un agente de flota e imprime su respuesta",
6591
+ "Conversation id; reuse it to continue (default: a new cli-<uuid>)": "Id de la conversaci\xF3n; rep\xEDtelo para seguirla (default: una nueva cli-<uuid>)",
6592
+ "Channel whose config (tools, MCP, prompt) applies, e.g. web or waba:<id>": "Canal cuya config (tools, MCP, prompt) aplica, p. ej. web o waba:<id>",
6593
+ "Who is writing (phone, user id)": "Qui\xE9n escribe (tel\xE9fono, id de usuario)",
6594
+ "IANA timezone for the turn, e.g. America/Mexico_City": "Zona horaria IANA del turno, p. ej. America/Mexico_City",
6595
+ "same conversation": "la misma conversaci\xF3n",
6596
+ // documents
6597
+ "Content": "Contenido",
6598
+ "Documents (/dash/documents): pages of HTML you write, published and exported to PDF/PNG": "Documentos (/dash/documents): p\xE1ginas de HTML que t\xFA escribes, publicadas y exportadas a PDF/PNG",
6599
+ "List your documents, newest first": "Enlista tus documentos, los m\xE1s nuevos primero",
6600
+ "Only documents whose name contains this": "S\xF3lo los documentos cuyo nombre contenga esto",
6601
+ "How many (default 20, max 100)": "Cu\xE1ntos (default 20, m\xE1ximo 100)",
6602
+ "Show one document: format, URL and its pages": "Muestra un documento: formato, URL y sus p\xE1ginas",
6603
+ "Create a document from HTML files (one page each) or a blank page": "Crea un documento desde archivos HTML (una p\xE1gina cada uno) o con una p\xE1gina en blanco",
6604
+ "Document name": "Nombre del documento",
6605
+ "A page's HTML, in order (repeatable; - reads stdin). Without it: one blank page": "El HTML de una p\xE1gina, en orden (repetible; - lee stdin). Sin \xE9l: una p\xE1gina en blanco",
6606
+ "letter (default), ig-feed, ig-square, ig-story, li-feed, slide-16-9, tiktok\u2026 or WxH in px": "letter (default), ig-feed, ig-square, ig-story, li-feed, slide-16-9, tiktok\u2026 o WxH en px",
6607
+ "Theme name (default: your brand kit or default)": "Nombre del tema (default: tu brand kit o default)",
6608
+ "Brand kit to apply (default: your default kit)": "Brand kit a aplicar (default: tu kit predeterminado)",
6609
+ "one blank 1080x1350 page": "una p\xE1gina en blanco de 1080x1350",
6610
+ "List pages, print one page's HTML, or replace it with a file": "Enlista las p\xE1ginas, imprime el HTML de una o lo reemplaza con un archivo",
6611
+ "get: write the HTML to this file instead of stdout": "get: escribe el HTML en este archivo en vez de stdout",
6612
+ "Add a page from an HTML file (blank without one)": "Agrega una p\xE1gina desde un archivo HTML (en blanco sin \xE9l)",
6613
+ "Insert after page N (0 = first; default: at the end)": "Inserta despu\xE9s de la p\xE1gina N (0 = al inicio; default: al final)",
6614
+ "Page name in the editor": "Nombre de la p\xE1gina en el editor",
6615
+ "Delete one page (a document keeps at least one)": "Borra una p\xE1gina (un documento conserva al menos una)",
6616
+ "Put the pages in a new order (every page, by number or id)": "Pone las p\xE1ginas en otro orden (todas, por n\xFAmero o id)",
6617
+ "swap pages 2 and 3": "intercambia las p\xE1ginas 2 y 3",
6618
+ "Publish (or republish) the document at its public URL": "Publica (o republica) el documento en su URL p\xFAblica",
6619
+ "Render to a PDF file or to one PNG per page (rendered on the server)": "Lo renderiza a un PDF o a un PNG por p\xE1gina (en el servidor)",
6620
+ "pdf: the file (default <name>.pdf); png: the folder (default .)": "pdf: el archivo (default <nombre>.pdf); png: la carpeta (default .)",
6621
+ "Only these pages (numbers or ids, comma-separated)": "S\xF3lo estas p\xE1ginas (n\xFAmeros o ids, separados por coma)",
6622
+ "one PNG per page at its exact size": "un PNG por p\xE1gina a su tama\xF1o exacto",
6623
+ "Delete a document (irreversible)": "Borra un documento (irreversible)",
6624
+ // video
6625
+ "Video projects: HTML + GSAP scenes with narration and music, rendered to MP4": "Proyectos de video: escenas de HTML + GSAP con narraci\xF3n y m\xFAsica, renderizadas a MP4",
6626
+ "List, show, create or delete video projects": "Enlista, muestra, crea o borra proyectos de video",
6627
+ "create: project name": "create: nombre del proyecto",
6628
+ "create: portrait (default 1080x1920), square, landscape, story, reel, tiktok, youtube, 16-9, or WxH": "create: portrait (default 1080x1920), square, landscape, story, reel, tiktok, youtube, 16-9, o WxH",
6629
+ "create: frames per second (default 30)": "create: cuadros por segundo (default 30)",
6630
+ "create: background theme: default, dark, light, brand": "create: tema del fondo: default, dark, light, brand",
6631
+ "scenes, music, last render": "escenas, m\xFAsica, \xFAltimo render",
6632
+ "List, add, edit, delete or reorder a project's scenes": "Enlista, agrega, edita, borra o reordena las escenas de un proyecto",
6633
+ "GSAP against the paused timeline `tl`: a file, or the code itself": "GSAP sobre el timeline pausado `tl`: un archivo, o el c\xF3digo tal cual",
6634
+ "Seconds on screen (default: fits the narration, else 3)": "Segundos en pantalla (default: lo que dure la narraci\xF3n, si no 3)",
6635
+ "Scene name": "Nombre de la escena",
6636
+ 'Voiceover text, synthesized at render time ("" removes it)': 'Texto de la voz en off, se sintetiza al renderizar ("" la quita)',
6637
+ "Narration voice: em_santa (default), em_alex, ef_dora": "Voz de la narraci\xF3n: em_santa (default), em_alex, ef_dora",
6638
+ "add: insert after scene N (0 = first; default: at the end)": "add: inserta despu\xE9s de la escena N (0 = al inicio; default: al final)",
6639
+ "replace its HTML": "reemplaza su HTML",
6640
+ "Set or remove the background music (a public audio URL)": "Pone o quita la m\xFAsica de fondo (una URL p\xFAblica de audio)",
6641
+ "File name inside the bundle (default: from the URL)": "Nombre del archivo dentro del bundle (default: el de la URL)",
6642
+ "remove it": "la quita",
6643
+ "Render the project to MP4 on the server; --wait follows it to the end": "Renderiza el proyecto a MP4 en el servidor; --wait lo sigue hasta el final",
6644
+ "Wait for the MP4 (spinner in a terminal); without it, start and return": "Espera el MP4 (spinner en una terminal); sin ella, lo arranca y regresa",
6645
+ "With --wait: also download the MP4 here": "Con --wait: adem\xE1s baja el MP4 aqu\xED",
6646
+ "With --wait: give up after this many minutes (default 15)": "Con --wait: se rinde tras estos minutos (default 15)",
6647
+ "starts it; then:": "lo arranca; luego:",
6648
+ "status: rendering \u2192 ready | failed": "status: rendering \u2192 ready | failed",
6649
+ "List AI-generated videos (prompt \u2192 clip), newest first": "Enlista los videos generados con IA (prompt \u2192 clip), los m\xE1s nuevos primero",
6650
+ "How many (default 50, max 100)": "Cu\xE1ntos (default 50, m\xE1ximo 100)"
4061
6651
  };
4062
6652
 
4063
6653
  // src/help.ts
@@ -4077,15 +6667,15 @@ var GLOBAL_FLAGS_EN = `Global flags:
4077
6667
  Naming (like gh): noun then verb \u2014 easybits <noun> <verb>. Old spellings still work
4078
6668
  and print the new one on stderr (never with --json).
4079
6669
 
4080
- Names or ids: agents, sandboxes/machines and databases take either
6670
+ Names or ids: agents, sandboxes/machines, databases and sites take either
4081
6671
  (exact, case-insensitive; two with the same name \u2192 exit 2 with the ids).
4082
6672
 
4083
- Deletes (sandboxes destroy, agents destroy, db rm, domains rm, files rm, agents files|skills rm)
6673
+ Deletes (every rm, destroy and revoke: sandboxes, machines, agents, db, domains, files, webhooks, keys, websites, documents\u2026)
4084
6674
  ask first; without a terminal or with --json pass -y/--yes, or they exit 2.
4085
6675
  Agent writes (agents set|create|files|skills|mcp) take --dry-run: show the change, touch nothing.
4086
6676
 
4087
6677
  Env: EASYBITS_API_KEY (API key), EASYBITS_URL (API server, default https://www.easybits.cloud),
4088
- EASYBITS_NO_UPDATE_CHECK=1 (no daily "update available" notice on stderr)
6678
+ EASYBITS_NO_UPDATE_CHECK=1 (no "update available" notice on stderr; npm is checked hourly)
4089
6679
 
4090
6680
  Exit codes: 0 ok, 1 API error, 2 usage error, 3 not logged in or bad key`;
4091
6681
  var GLOBAL_FLAGS_ES = `Flags globales:
@@ -4098,16 +6688,16 @@ var GLOBAL_FLAGS_ES = `Flags globales:
4098
6688
  Nombres (como gh): sustantivo y luego verbo \u2014 easybits <sustantivo> <verbo>. Las formas
4099
6689
  viejas siguen funcionando y avisan la nueva en stderr (nunca con --json).
4100
6690
 
4101
- Nombres o ids: agentes, sandboxes/m\xE1quinas y bases aceptan cualquiera de los dos
6691
+ Nombres o ids: agentes, sandboxes/m\xE1quinas, bases y sitios aceptan cualquiera de los dos
4102
6692
  (exacto, sin distinguir may\xFAsculas; dos con el mismo nombre \u2192 sale con 2 y los ids).
4103
6693
 
4104
- Los borrados (sandboxes destroy, agents destroy, db rm, domains rm, files rm, agents files|skills rm)
6694
+ Los borrados (todo rm, destroy y revoke: sandboxes, m\xE1quinas, agentes, db, dominios, archivos, webhooks, llaves, websites, documentos\u2026)
4105
6695
  preguntan antes; sin terminal o con --json pasa -y/--yes, o salen con 2.
4106
6696
  Lo que escribe en un agente (agents set|create|files|skills|mcp) acepta --dry-run: ense\xF1a el
4107
6697
  cambio y no toca nada.
4108
6698
 
4109
6699
  Env: EASYBITS_API_KEY (API key), EASYBITS_URL (servidor de la API, default https://www.easybits.cloud),
4110
- EASYBITS_NO_UPDATE_CHECK=1 (sin el aviso diario de versi\xF3n nueva en stderr)
6700
+ EASYBITS_NO_UPDATE_CHECK=1 (sin el aviso de versi\xF3n nueva en stderr; npm se revisa cada hora)
4111
6701
 
4112
6702
  C\xF3digos de salida: 0 ok, 1 error de la API, 2 error de uso, 3 sin sesi\xF3n o key inv\xE1lida`;
4113
6703
  var globalFlags = () => lang() === "es" ? GLOBAL_FLAGS_ES : GLOBAL_FLAGS_EN;
@@ -4178,7 +6768,7 @@ function indent(s, n) {
4178
6768
  var BANNER = " \x1B[38;2;244;245;251m\u2860\u2812\u2812\u28E4\x1B[38;2;152;112;237m\u28C0\u28C0\u2840 \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28E4\u28FF\x1B[0m\n \x1B[38;2;244;245;251m\u28A0\u2854\u2809\u2809\u2809\u2822\u2840\u285E\x1B[38;2;152;112;237m\u2880\u2874\u288B\u282D\x1B[38;2;244;245;251m\u2809\u28AF\x1B[38;2;152;112;237m\u28FD\u28F7\u2844 \x1B[38;2;244;245;251m\u28FF\u28FF \u28E4\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28E4\u28E4 \u28E4\u28FF\u28FF\u28E4\u28E4 \u28E4\u28FF\u28FF\u28FF\u28FF\u28E4\x1B[0m\n \x1B[38;2;152;112;237m\u2880\x1B[38;2;244;245;251m\u28FF\x1B[38;2;152;112;237m\u289B\u280D\u2809\u2899\u2873\u28FC\u2857\u28FE\u28B0\x1B[38;2;244;245;251m\u28A1\u28F6\u28E4\u2818\u2846 \x1B[38;2;152;112;237m\u28BB \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF \u28E4\u28E4\u28E4\u28E4\u28FF\u28FF \u28FF\u28FF\u28E4\u28E4\u28E4 \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E4 \u28FF\u28FF \u281B\u28FF\u28FF\u281B\u281B \u28FF\u28FF\u28E4\u28E4\u28E4\x1B[0m\n\x1B[38;2;152;112;237m\u28E4\u28C4\u28C0\u28FF\x1B[38;2;244;245;251m\u28DF\x1B[38;2;152;112;237m\u2801 \x1B[38;2;244;245;251m\u28FE\u28FF\u280E\x1B[38;2;152;112;237m\u28E7\u28BF\u284C\x1B[38;2;244;245;251m\u2808\u2819\u280B \u2847\x1B[38;2;152;112;237m\u2880\u287E \x1B[38;2;244;245;251m\u28FF\u28FF \u28FF\u28FF\u281B\u281B\u281B\u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28E4\u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u28FF\u28FF \u281B\u281B\u281B\u28FF\u28FF\x1B[0m\n\x1B[38;2;152;112;237m\u2801 \u2808\u28E7\x1B[38;2;244;245;251m\u28BF\u28C6 \u2809\x1B[38;2;152;112;237m\u2880\u285F\x1B[38;2;244;245;251m\u28E7\x1B[38;2;152;112;237m\u2859\u2832\u28A4\u28E4\u2874\u2835\u280B \x1B[38;2;244;245;251m\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u281B\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B \u28E4\u28FF\u28FF\u281B \u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u281B \u28FF\u28FF \u281B\u28FF\u28FF\u28FF \u281B\u28FF\u28FF\u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;152;112;237m\u2808\u283B\x1B[38;2;244;245;251m\u28BF\u28E6\x1B[38;2;152;112;237m\u28C0\u28E0\u28F4\u281F\x1B[38;2;244;245;251m\u2801\u2808\u2819\u2833\u2836\u280A \u28FF\u28FF\u281B\x1B[0m\n \x1B[38;2;244;245;251m\u2808\u2809\u2809\u2801\x1B[0m";
4179
6769
 
4180
6770
  // src/index.ts
4181
- import { EasybitsError as EasybitsError3 } from "@easybits.cloud/sdk";
6771
+ import { EasybitsError as EasybitsError7 } from "@easybits.cloud/sdk";
4182
6772
 
4183
6773
  // src/aliases.ts
4184
6774
  function positionals(argv2) {
@@ -4198,6 +6788,13 @@ function positionals(argv2) {
4198
6788
  var MACHINE_SUBS = ["ls", "list", "deploy", "release", "releases", "logs", "rollback", "secrets"];
4199
6789
  var SANDBOX_NAMES = ["sandboxes", "sandbox", "sb"];
4200
6790
  var AGENT_NAMES = ["agents", "agent"];
6791
+ var DOMAIN_NAMES = ["domains", "domain"];
6792
+ function oldSandboxDomains(sub, rest, hasPort) {
6793
+ if (sub === "ls" || sub === "list") return rest >= 1;
6794
+ if (sub === "add") return hasPort || rest >= 2;
6795
+ if (sub === "verify" || sub === "rm" || sub === "remove" || sub === "delete") return rest >= 2;
6796
+ return false;
6797
+ }
4201
6798
  function normalizeArgs(input) {
4202
6799
  const argv2 = [...input];
4203
6800
  const notes = [];
@@ -4231,6 +6828,18 @@ function normalizeArgs(input) {
4231
6828
  argv2[pos[1]] = "deploy";
4232
6829
  notes.push(["`machines release` is now `machines deploy`.", "`machines release` ahora es `machines deploy`."]);
4233
6830
  }
6831
+ if (DOMAIN_NAMES.includes(at(0) ?? "")) {
6832
+ const hasPort = head.some((a) => a === "--port" || a.startsWith("--port="));
6833
+ const sub = at(1);
6834
+ if (oldSandboxDomains(sub, pos.length - 2, hasPort)) {
6835
+ argv2.splice(pos[0], 1, "sandboxes", "domains");
6836
+ notes.push([
6837
+ `\`easybits domains ${sub} <sandbox> \u2026\` is now \`easybits sandboxes domains ${sub} <sandbox> \u2026\` (\`domains\` is for your account's domains).`,
6838
+ `\`easybits domains ${sub} <caja> \u2026\` ahora es \`easybits sandboxes domains ${sub} <caja> \u2026\` (\`domains\` es para los dominios de tu cuenta).`
6839
+ ]);
6840
+ pos = positionals(argv2);
6841
+ }
6842
+ }
4234
6843
  const noun = SANDBOX_NAMES.includes(at(0) ?? "") ? "sandboxes" : AGENT_NAMES.includes(at(0) ?? "") ? "agents" : null;
4235
6844
  if (noun && (at(1) === "create" || at(1) === "new")) {
4236
6845
  const stop = argv2.indexOf("--");
@@ -4248,7 +6857,7 @@ function normalizeArgs(input) {
4248
6857
  }
4249
6858
 
4250
6859
  // src/index.ts
4251
- var VERSION2 = true ? "0.15.5" : "dev";
6860
+ var VERSION2 = true ? "0.17.0" : "dev";
4252
6861
  var GLOBAL_OPTIONS = {
4253
6862
  json: { type: "boolean" },
4254
6863
  token: { type: "string" },
@@ -4394,7 +7003,7 @@ async function main(input) {
4394
7003
  try {
4395
7004
  await run();
4396
7005
  } catch (e) {
4397
- if (e instanceof EasybitsError3 && e.status === 401 && usedSession() && await forceRefresh()) {
7006
+ if (e instanceof EasybitsError7 && e.status === 401 && usedSession() && await forceRefresh()) {
4398
7007
  await run();
4399
7008
  return;
4400
7009
  }