kooni-bot 0.2.4 → 0.2.8

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 (2) hide show
  1. package/bin/kooni.js +191 -48
  2. package/package.json +1 -1
package/bin/kooni.js CHANGED
@@ -22,7 +22,7 @@ import { execFileSync, spawnSync } from "node:child_process";
22
22
  import { randomUUID } from "node:crypto";
23
23
  import { fileURLToPath } from "node:url";
24
24
 
25
- const CLI_VERSION = "0.2.4";
25
+ const CLI_VERSION = "0.2.8";
26
26
 
27
27
  const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
28
28
  const BRANCH = process.env.KOONI_BRANCH || "main";
@@ -33,6 +33,7 @@ const CHECKIN_URL = process.env.KOONI_CHECKIN_URL || "https://f5gacw7g.function2
33
33
 
34
34
  const CFG_DIR = join(homedir(), ".kooni");
35
35
  const CFG_FILE = join(CFG_DIR, "config.json");
36
+ const INSTALLS_FILE = join(CFG_DIR, "installs.json");
36
37
  const MARKER = ".kooni-bot.json";
37
38
  const SKILL_DIR = join(homedir(), ".claude", "skills", "kooni");
38
39
 
@@ -102,7 +103,7 @@ const DICT = {
102
103
  migrations: "Aplicando migraciones D1…",
103
104
  deploying: "Desplegando el worker…",
104
105
  panel: "Panel de administración:",
105
- next: "Lo que sigue: conecta tu primer canal (Telegram ~5 min) desde el panel Conexiones.",
106
+ next: "Abre tu panel Conexiones para conectar canales (Telegram, Zernio, WhatsApp…) ahí mismo se activan y quedan en verde.",
106
107
  // update
107
108
  updRevalidating: "Buscando la versión nueva…",
108
109
  updUpToDate: "Ya estás en la última versión.",
@@ -174,7 +175,7 @@ const DICT = {
174
175
  migrations: "Applying D1 migrations…",
175
176
  deploying: "Deploying the worker…",
176
177
  panel: "Admin dashboard:",
177
- next: "Next: connect your first channel (Telegram ~5 min) from the panel Connections.",
178
+ next: "Open your panel Connections to connect channels (Telegram, Zernio, WhatsApp…) they activate and turn green right there.",
178
179
  updRevalidating: "Checking for a new version…",
179
180
  updUpToDate: "You're on the latest version.",
180
181
  updDone: (v) => `Updated to v${v} (your config and data were preserved)`,
@@ -250,6 +251,26 @@ function banner() {
250
251
  function loadCfg() { try { return JSON.parse(readFileSync(CFG_FILE, "utf8")); } catch { return {}; } }
251
252
  function saveCfg(o) { mkdirSync(CFG_DIR, { recursive: true }); writeFileSync(CFG_FILE, JSON.stringify(o, null, 2)); }
252
253
 
254
+ // Registro local de instalaciones (~/.kooni/installs.json): cada instalación de
255
+ // esta computadora queda identificada por su carpeta real + uid de Cloudflare.
256
+ // Sirve para que `deploy`/`update`/`doctor` sepan cuál elegir si hay varias.
257
+ function loadInstalls() {
258
+ try { return JSON.parse(readFileSync(INSTALLS_FILE, "utf8")); } catch { return []; }
259
+ }
260
+ function saveInstalls(list) {
261
+ mkdirSync(CFG_DIR, { recursive: true });
262
+ writeFileSync(INSTALLS_FILE, JSON.stringify(list, null, 2));
263
+ }
264
+ function recordInstall(dir, meta) {
265
+ const real = realpathSync(dir);
266
+ const list = loadInstalls().filter((x) => x && x.dir !== real);
267
+ list.push({ dir: real, ...meta, updatedAt: new Date().toISOString() });
268
+ saveInstalls(list);
269
+ }
270
+ function listInstalls() {
271
+ return loadInstalls().filter((x) => x && x.dir && existsSync(x.dir));
272
+ }
273
+
253
274
  // ── flags / interacción ──────────────────────────────────────────────────────
254
275
  function parseFlags(args) {
255
276
  const flags = {};
@@ -461,10 +482,9 @@ function backupBeforeUpdate(dir, version) {
461
482
  }
462
483
 
463
484
  // ── markers / detección ──────────────────────────────────────────────────────
464
- function writeMarker(dir, { slug, version, lang }) {
465
- writeFileSync(join(dir, MARKER), JSON.stringify({
466
- slug, version, lang, updatedAt: new Date().toISOString(),
467
- }, null, 2));
485
+ function writeMarker(dir, meta) {
486
+ const prev = readMarker(dir) || {};
487
+ writeFileSync(join(dir, MARKER), JSON.stringify({ ...prev, ...meta, updatedAt: new Date().toISOString() }, null, 2));
468
488
  }
469
489
 
470
490
  function readMarker(dir) {
@@ -482,9 +502,28 @@ function isKooni(dir) {
482
502
  return existsSync(join(dir, "package.json")) && (existsSync(join(dir, "member")) || existsSync(join(dir, "src", "index.ts")));
483
503
  }
484
504
 
485
- function resolveBotDir(arg) {
505
+ // Elige la instalación sobre la que actuar. `arg` es una ruta explícita (gana).
506
+ // Si no hay ruta, prioriza el cwd; después el registro local `installs.json`;
507
+ // y como último recurso escanea los hijos del cwd. Devuelve la ruta o null.
508
+ async function resolveBotDir(arg, rl) {
486
509
  if (arg && isKooni(arg)) return arg;
487
510
  if (isKooni(process.cwd())) return process.cwd();
511
+
512
+ const registered = listInstalls();
513
+ if (registered.length === 1) return registered[0].dir;
514
+ if (registered.length > 1) {
515
+ if (!interactive()) {
516
+ console.log(C.yellow("\n " + m("Hay varias instalaciones de Kooni. Pasa la carpeta explícita:", "Multiple Kooni installs found. Pass an explicit folder:")));
517
+ registered.forEach((x) => console.log(" " + C.cyan(`npx kooni-bot <comando> "${x.dir}"`)));
518
+ process.exit(1);
519
+ }
520
+ const idx = await select(rl, m("¿Cuál instalación?", "Which install?"), registered.map((x) => ({
521
+ key: x.dir,
522
+ label: `${x.slug || basename(x.dir)} · ${x.dir}`,
523
+ })));
524
+ return registered[idx]?.dir || null;
525
+ }
526
+
488
527
  for (const e of readdirSync(process.cwd())) {
489
528
  try {
490
529
  const p = join(process.cwd(), e);
@@ -527,6 +566,11 @@ function run(file, args = [], opts = {}) {
527
566
  err.status = r.status;
528
567
  throw err;
529
568
  }
569
+ // En modo capture, combinamos stdout+stderr: wrangler suele imprimir la URL del
570
+ // worker en stderr (progreso/banners), y necesitamos verla para parsearla.
571
+ if (opts.capture) {
572
+ return `${r.stdout || ""}\n${r.stderr || ""}`;
573
+ }
530
574
  return (r.stdout || "").toString();
531
575
  }
532
576
 
@@ -554,7 +598,7 @@ function sanitizeSlug(s) {
554
598
  return String(s || "mi-negocio").toLowerCase().replace(/ /g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "mi-negocio";
555
599
  }
556
600
 
557
- function stampWrangler(dir, answers) {
601
+ function stampWrangler(dir, answers, botUid) {
558
602
  const wt = join(dir, "wrangler.toml");
559
603
  const example = join(dir, "wrangler.toml.example");
560
604
  // El template distribuye wrangler.toml.example; init genera el wrangler.toml real.
@@ -564,15 +608,21 @@ function stampWrangler(dir, answers) {
564
608
  if (!existsSync(wt)) return null;
565
609
  let s = readFileSync(wt, "utf8");
566
610
  const slug = answers.slug;
567
- // Nombres canónicos del proyecto (coinciden con `pnpm db:apply:remote`, que
568
- // apunta a `kooni_db`, y con la instalación probada del template).
569
- const dbName = "kooni_db";
570
- const kbName = "kooni_kb";
611
+ // Identidad ÚNICA por instalación: cada bot genera su propio uid de 6 chars y
612
+ // lo usa en worker/D1/Vectorize. Así dos instalaciones en la MISMA cuenta de
613
+ // Cloudflare nunca comparten datos. Si el wrangler.toml ya trae un uid (reinstall
614
+ // en la misma carpeta), se reutiliza.
615
+ const existingUid = (s.match(/name\s*=\s*"kooni-bot-.+-([a-f0-9]{6})"/) || [])[1];
616
+ const uid = existingUid || botUid || randomUUID().replace(/-/g, "").slice(0, 6);
617
+ const resId = slug.replace(/-/g, "_");
618
+ const dbName = `kooni_${resId}_${uid}_db`;
619
+ const kbName = `kooni_${resId}_${uid}_kb`;
620
+ const workerName = `kooni-bot-${slug}-${uid}`;
571
621
  const R = REGIONS[answers.lang] || REGIONS["es-MX"];
572
622
 
573
623
  const set = (re, val) => { s = s.replace(re, val); };
574
624
 
575
- set(/^name\s*=\s*"[^"]*"/m, `name = "kooni-bot-${slug}"`);
625
+ set(/^name\s*=\s*"[^"]*"/m, `name = "${workerName}"`);
576
626
  set(/BOT_NAME\s*=\s*"[^"]*"/g, `BOT_NAME = "${String(answers.botName).replace(/"/g, "'")}"`);
577
627
  set(/BUSINESS_NAME\s*=\s*"[^"]*"/g, `BUSINESS_NAME = "${String(answers.businessName).replace(/"/g, "'")}"`);
578
628
  set(/BOT_LANGUAGE\s*=\s*"[^"]*"/g, `BOT_LANGUAGE = "${answers.lang}"`);
@@ -592,6 +642,13 @@ function stampWrangler(dir, answers) {
592
642
  return new RegExp(`^\\s*${key}\\s*=`, "m").test(varsMatch[0]);
593
643
  };
594
644
 
645
+ // Identidad de instalación para ligar licencias (ver src/license.ts + limits.ts).
646
+ if (hasInVars("BOT_INSTANCE_ID")) {
647
+ set(/BOT_INSTANCE_ID\s*=\s*"[^"]*"/g, `BOT_INSTANCE_ID = "${uid}"`);
648
+ } else {
649
+ s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1BOT_INSTANCE_ID = "${uid}"\n`);
650
+ }
651
+
595
652
  if (answers.provider !== "anthropic") {
596
653
  if (hasInVars("LLM_PROVIDER")) {
597
654
  set(/LLM_PROVIDER\s*=\s*"[^"]*"/g, `LLM_PROVIDER = "${answers.provider}"`);
@@ -612,7 +669,7 @@ function stampWrangler(dir, answers) {
612
669
  }
613
670
 
614
671
  writeFileSync(wt, s);
615
- return { dbName, kbName, tz: R.tz, currency: R.currency };
672
+ return { uid, dbName, kbName, workerName, tz: R.tz, currency: R.currency };
616
673
  }
617
674
 
618
675
  function renderMemberConfig(answers, meta) {
@@ -691,6 +748,7 @@ function collectAnswers(flags) {
691
748
  // `onboarding()` lo pregunte (interactivo) o use el default (no-interactivo).
692
749
  return {
693
750
  slug: flags.slug ? sanitizeSlug(flags.slug) : undefined,
751
+ uid: String(flags.uid || "").trim().toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 6) || undefined,
694
752
  businessName: String(flags.negocio || flags.nombre || flags.name || "").trim() || undefined,
695
753
  botName: String(flags["bot-name"] || "").trim() || undefined,
696
754
  lang: flags.lang ? normBotLang(flags.lang) : undefined,
@@ -816,10 +874,11 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
816
874
  wrangler(dir, ["login"]);
817
875
  console.log(" " + C.green("✓") + " " + t().loginOk);
818
876
 
819
- // recursos (nombres canónicos del proyecto: kooni_db / kooni_kb)
877
+ // recursos (nombres ÚNICOS por instalación, ya estampados en wrangler.toml)
820
878
  console.log("\n " + C.dim(t().creatingResources));
821
- const dbName = "kooni_db";
822
- const kbName = "kooni_kb";
879
+ const wtRaw = readFileSync(wt, "utf8");
880
+ const dbName = (wtRaw.match(/database_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_db";
881
+ const kbName = (wtRaw.match(/index_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_kb";
823
882
 
824
883
  let d1Id = "";
825
884
  try {
@@ -899,7 +958,7 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
899
958
  console.log(" " + C.green("✓") + " " + m("dependencias listas", "dependencies ready"));
900
959
 
901
960
  console.log(" " + C.dim(t().migrations));
902
- runPnpm(dir, ["db:apply:remote"]);
961
+ wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]);
903
962
  console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
904
963
 
905
964
  console.log(" " + C.dim(t().deploying));
@@ -907,8 +966,11 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
907
966
  try {
908
967
  const dep = runPnpm(dir, ["run", "deploy"], { capture: true });
909
968
  url = (dep.match(/https:\/\/[a-z0-9-]+\.workers\.dev/) || [])[0] || "";
910
- } catch {
911
- // el deploy-check imprime el detalle; si falló, no seguimos.
969
+ } catch (e) {
970
+ // Muestra el detalle real del deploy en vez de tragarlo: así el usuario ve
971
+ // qué falló (deploy-check, binding, auth…) y puede corregirlo.
972
+ const tail = ((e && (e.stderr || e.stdout || e.message)) || "").toString().trim().split("\n").slice(-12).join("\n");
973
+ if (tail) console.log(C.red(" ✗ " + tail));
912
974
  throw new Error(t().deployFailed);
913
975
  }
914
976
  if (url) {
@@ -917,6 +979,8 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
917
979
  } else {
918
980
  console.log(C.yellow(" ⚠ " + m("no se detectó la URL del worker — revisa la salida del deploy", "couldn't detect the worker URL — check the deploy output")));
919
981
  }
982
+ // Persistir identidad de Cloudflare en el marker (para update/doctor/selector).
983
+ writeMarker(dir, { databaseId: d1Id, workerUrl: url || undefined, dbName, kbName });
920
984
  return url;
921
985
  }
922
986
 
@@ -924,14 +988,19 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
924
988
  async function checkin(dir, answers, version) {
925
989
  if (process.env.KOONI_NO_CHECKIN === "1" || process.env.KOONI_SILENT === "1") return;
926
990
  try {
927
- const slug = answers.slug || basename(dir);
991
+ const marker = readMarker(dir) || {};
992
+ const slug = answers.slug || marker.slug || basename(dir);
928
993
  await fetchTimeout(CHECKIN_URL, {
929
994
  method: "POST",
930
995
  headers: { "Content-Type": "application/json" },
931
996
  body: JSON.stringify({
932
997
  email: answers.email || undefined,
933
998
  slug,
934
- workerUrl: `https://kooni-bot-${slug}.workers.dev`,
999
+ uid: marker.uid,
1000
+ workerName: marker.workerName,
1001
+ dbName: marker.dbName,
1002
+ kbName: marker.kbName,
1003
+ workerUrl: marker.workerUrl || `https://kooni-bot-${slug}.workers.dev`,
935
1004
  cliVersion: CLI_VERSION,
936
1005
  botVersion: version,
937
1006
  tier: answers.tier,
@@ -965,9 +1034,14 @@ constructor. No hay licencia ni servidor de Horizontes: el tier free/pro se cont
965
1034
  - \`npx kooni-bot init [dir]\` — descarga el template, configura (idioma, negocio, cerebro) y ofrece desplegar.
966
1035
  - \`npx kooni-bot deploy [dir]\` — provisiona Cloudflare (login, D1/Vectorize/R2, secrets, migraciones, deploy).
967
1036
  - \`npx kooni-bot update [dir]\` — trae la versión nueva conservando \`member/\`, \`wrangler.toml\` y datos.
1037
+ - \`npx kooni-bot update --all\` — actualiza TODAS las instalaciones registradas en esta computadora.
968
1038
  - \`npx kooni-bot doctor [dir]\` — diagnostica el bot instalado.
969
1039
  - \`npx kooni-bot version\`.
970
1040
 
1041
+ ## Conexión de canales (DESDE el panel, sin redeploy)
1042
+ - Telegram y Zernio se conectan pegando su token/API key en \`/admin/conexiones\`. Se guardan en D1 (\`settings\`) y el canal se pone verde al instante, SIN \`wrangler secret put\` ni redeploy.
1043
+ - La URL del webhook de cada canal se muestra en su propia card (con botón copiar).
1044
+
971
1045
  ## Regla de oro (memorízala)
972
1046
  | Carpeta / archivo | Qué pasa al actualizar |
973
1047
  |---|---|
@@ -1071,7 +1145,7 @@ async function cmdInit(flags, rest) {
1071
1145
  const answers = collectAnswers(flags);
1072
1146
  await onboarding(rl, answers, basename(dir));
1073
1147
 
1074
- const meta = stampWrangler(dir, answers);
1148
+ const meta = stampWrangler(dir, answers, answers.uid);
1075
1149
  const kbToken = "kooni-reindex-" + randomUUID().replace(/-/g, "").slice(0, 12);
1076
1150
  if (existsSync(join(dir, "member"))) {
1077
1151
  const region = REGIONS[answers.lang] || REGIONS["es-MX"];
@@ -1082,9 +1156,26 @@ async function cmdInit(flags, rest) {
1082
1156
  }));
1083
1157
  }
1084
1158
  writeDevVars(dir, answers, kbToken);
1085
- writeMarker(dir, { slug: answers.slug, version, lang: L });
1159
+ writeMarker(dir, {
1160
+ slug: answers.slug,
1161
+ version,
1162
+ lang: L,
1163
+ uid: (meta && meta.uid) || answers.uid,
1164
+ workerName: meta && meta.workerName,
1165
+ dbName: meta && meta.dbName,
1166
+ kbName: meta && meta.kbName,
1167
+ });
1086
1168
  console.log("\n " + C.green("✓") + " " + t().configDone);
1087
1169
 
1170
+ // Registrar la instalación local para el selector multi-bot.
1171
+ recordInstall(dir, {
1172
+ slug: answers.slug,
1173
+ uid: (meta && meta.uid) || answers.uid,
1174
+ workerName: meta && meta.workerName,
1175
+ dbName: meta && meta.dbName,
1176
+ kbName: meta && meta.kbName,
1177
+ });
1178
+
1088
1179
  // deploy (si no lo deshabilitan)
1089
1180
  if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
1090
1181
  const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
@@ -1093,6 +1184,8 @@ async function cmdInit(flags, rest) {
1093
1184
  console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
1094
1185
  console.log(" " + C.cyan(t().panel) + " " + C.b(url + "/admin"));
1095
1186
  console.log(" " + C.dim(t().next) + "\n");
1187
+ } else {
1188
+ console.log(" " + C.yellow(m("No detecté la URL automáticamente. Búscala en la salida de `pnpm run deploy` arriba (https://…workers.dev) y abre <url>/admin.", "Couldn't auto-detect the URL. Find it in the `pnpm run deploy` output above (https://…workers.dev) and open <url>/admin.")) + "\n");
1096
1189
  }
1097
1190
  }
1098
1191
 
@@ -1112,11 +1205,11 @@ async function cmdDeploy(flags, rest) {
1112
1205
  banner();
1113
1206
  installAgentSkill(flags);
1114
1207
 
1115
- const dir = resolveBotDir(rest[0]);
1116
- if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1117
-
1118
1208
  const rl = createInterface({ input, output });
1119
1209
  try {
1210
+ const dir = await resolveBotDir(rest[0], rl);
1211
+ if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1212
+
1120
1213
  // provider para elegir el secret correcto
1121
1214
  let brainKey = "claude";
1122
1215
  try {
@@ -1134,24 +1227,16 @@ async function cmdDeploy(flags, rest) {
1134
1227
  }
1135
1228
  }
1136
1229
 
1137
- async function cmdUpdate(flags, rest) {
1138
- const cfg = loadCfg();
1139
- ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
1140
- if (flags.lang === "en" || cfg.lang === "en") L = "en";
1141
- banner();
1142
- installAgentSkill(flags);
1143
-
1144
- const dir = resolveBotDir(rest[0]);
1145
- if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1146
-
1230
+ // Actualiza UNA instalación. Devuelve un resumen; NO hace process.exit (para que
1231
+ // `--all` pueda recorrer varias sin cortar el proceso).
1232
+ async function updateOne(dir, flags = {}) {
1147
1233
  const marker = readMarker(dir) || {};
1234
+ const slug = marker.slug || basename(dir);
1148
1235
  const current = marker.version || readPkgVersion(dir) || "0.0.0";
1149
1236
 
1150
1237
  const tgz = join(dir, ".kooni-template.tgz");
1151
- process.stdout.write(C.dim(" " + t().updRevalidating + "\n"));
1152
1238
  await downloadTemplate(tgz);
1153
1239
 
1154
- // versión nueva desde el tarball
1155
1240
  const tmp = join(dir, ".kooni-extract");
1156
1241
  mkdirSync(tmp, { recursive: true });
1157
1242
  const src = extractToTemp(tgz, tmp);
@@ -1166,29 +1251,26 @@ async function cmdUpdate(flags, rest) {
1166
1251
  if (!verLt(current, next)) {
1167
1252
  rmSync(tmp, { recursive: true, force: true });
1168
1253
  rmSync(tgz, { force: true });
1169
- console.log(" " + C.green("✓") + " " + t().updUpToDate + " (v" + current + ")\n");
1170
- return;
1254
+ return { dir, slug, updated: false, from: current, to: next };
1171
1255
  }
1172
1256
 
1173
- // respaldo + extraer sobre la instalación
1174
1257
  const backupPath = backupBeforeUpdate(dir, current);
1175
1258
  extractOver(tgz, dir);
1176
- writeMarker(dir, { slug: marker.slug || basename(dir), version: next, lang: marker.lang || L });
1259
+ writeMarker(dir, { slug, version: next, lang: marker.lang || L });
1177
1260
 
1178
1261
  console.log(" " + C.green("✓") + " " + t().updDone(next));
1179
1262
  if (backupPath) console.log(" " + C.dim(t().updBackup(backupPath.slice(dir.length + 1))));
1180
1263
  console.log(" " + C.dim(t().updPreserved));
1181
1264
  console.log(" " + C.dim(t().updReplaced));
1182
1265
 
1183
- // dependencias + migraciones + reindex + deploy
1184
1266
  console.log("\n " + C.dim(t().installing));
1185
1267
  runPnpm(dir, ["install"]);
1186
- try { runPnpm(dir, ["db:apply:remote"]); } catch { /* best-effort */ }
1268
+ const dbName = marker.dbName || (readFileSync(join(dir, "wrangler.toml"), "utf8").match(/database_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_db";
1269
+ try { wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]); } catch { /* best-effort */ }
1187
1270
  try { runPnpm(dir, ["kb:reindex"]); } catch { /* best-effort */ }
1188
1271
  console.log(" " + C.dim(t().deploying));
1189
1272
  try { runPnpm(dir, ["run", "deploy"]); } catch { /* el deploy-check imprime el detalle */ }
1190
1273
 
1191
- // reindex del worker si hay estado con URL
1192
1274
  try {
1193
1275
  const st = JSON.parse(readFileSync(join(dir, ".bot-state.json"), "utf8"));
1194
1276
  if (st.worker_url) {
@@ -1198,6 +1280,60 @@ async function cmdUpdate(flags, rest) {
1198
1280
  }
1199
1281
  } catch {}
1200
1282
 
1283
+ return { dir, slug, updated: true, from: current, to: next };
1284
+ }
1285
+
1286
+ async function cmdUpdate(flags, rest) {
1287
+ const cfg = loadCfg();
1288
+ ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
1289
+ if (flags.lang === "en" || cfg.lang === "en") L = "en";
1290
+ banner();
1291
+ installAgentSkill(flags);
1292
+
1293
+ // --all: actualiza todas las instalaciones registradas en ~/.kooni/installs.json.
1294
+ if (flags.all) {
1295
+ const dirs = listInstalls().map((x) => x.dir);
1296
+ if (dirs.length === 0) {
1297
+ console.log(" " + C.red(t().needDir) + " (sin instalaciones registradas)\n");
1298
+ process.exit(1);
1299
+ }
1300
+ console.log(" " + C.dim(m(`Actualizando ${dirs.length} instalaciones…`, `Updating ${dirs.length} installs…`)) + "\n");
1301
+ const results = [];
1302
+ for (const d of dirs) {
1303
+ const marker = readMarker(d) || {};
1304
+ const slug = marker.slug || basename(d);
1305
+ console.log(C.b("\n ◇ " + slug));
1306
+ try {
1307
+ const r = await updateOne(d, flags);
1308
+ results.push(r);
1309
+ console.log(r.updated
1310
+ ? " " + C.green(`✓ ${slug}: ${r.from} → ${r.to}`)
1311
+ : " " + C.green(`✓ ${slug}: ${t().updUpToDate} (v${r.to})`));
1312
+ } catch (e) {
1313
+ results.push({ dir: d, slug, error: e.message || String(e) });
1314
+ console.log(" " + C.red(`✗ ${slug}: ${e.message || e}`));
1315
+ }
1316
+ }
1317
+ console.log("");
1318
+ return;
1319
+ }
1320
+
1321
+ const rl = createInterface({ input, output });
1322
+ let dir;
1323
+ try {
1324
+ dir = await resolveBotDir(rest[0], rl);
1325
+ } finally {
1326
+ rl.close();
1327
+ }
1328
+ if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
1329
+
1330
+ const marker = readMarker(dir) || {};
1331
+ const slug = marker.slug || basename(dir);
1332
+ console.log(C.b("\n ◇ " + slug));
1333
+ const r = await updateOne(dir, flags);
1334
+ if (!r.updated) {
1335
+ console.log(" " + C.green("✓") + " " + t().updUpToDate + " (v" + r.to + ")\n");
1336
+ }
1201
1337
  console.log("");
1202
1338
  }
1203
1339
 
@@ -1210,7 +1346,13 @@ async function cmdDoctor(flags, rest) {
1210
1346
  const warn = (s, h) => { console.log(" " + C.yellow("⚠") + " " + s); if (h) console.log(" " + C.dim(h)); };
1211
1347
  const bad = (s, h) => { console.log(" " + C.red("✗") + " " + s); if (h) console.log(" " + C.dim(h)); };
1212
1348
 
1213
- const dir = resolveBotDir(rest[0]);
1349
+ const rl = createInterface({ input, output });
1350
+ let dir;
1351
+ try {
1352
+ dir = await resolveBotDir(rest[0], rl);
1353
+ } finally {
1354
+ rl.close();
1355
+ }
1214
1356
  if (!dir) { bad(t().needDir + " " + (rest[0] || process.cwd())); process.exit(1); }
1215
1357
  ok(m("Bot encontrado en ", "Bot found in ") + C.cyan(dir));
1216
1358
 
@@ -1269,6 +1411,7 @@ ${C.cyan("kooni-bot")} — ${t().helpIntro}
1269
1411
  ${C.cyan("npx kooni-bot init [dir]")} ${m("instala (descarga template + config + deploy)", "install (download template + config + deploy)")}
1270
1412
  ${C.cyan("npx kooni-bot deploy [dir]")} ${m("provisiona Cloudflare y publica el worker", "provision Cloudflare and publish the worker")}
1271
1413
  ${C.cyan("npx kooni-bot update [dir]")} ${m("actualiza sin perder tu configuración", "update without losing config")}
1414
+ ${C.cyan("npx kooni-bot update --all")} ${m("actualiza TODAS las instalaciones registradas", "update ALL registered installs")}
1272
1415
  ${C.cyan("npx kooni-bot doctor [dir]")} ${m("diagnóstico del bot instalado", "diagnose the installed bot")}
1273
1416
  ${C.cyan("npx kooni-bot version")} ${m("versión del CLI", "CLI version")}
1274
1417
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kooni-bot",
3
- "version": "0.2.4",
3
+ "version": "0.2.8",
4
4
  "description": "Kooni — instala tu asistente de IA multicanal (WhatsApp, Instagram, Messenger, Telegram) en TU Cloudflare, en un comando.",
5
5
  "license": "MIT",
6
6
  "type": "module",