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.
- package/bin/kooni.js +191 -48
- 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.
|
|
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: "
|
|
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: "
|
|
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,
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
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 = "
|
|
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
|
|
877
|
+
// recursos (nombres ÚNICOS por instalación, ya estampados en wrangler.toml)
|
|
820
878
|
console.log("\n " + C.dim(t().creatingResources));
|
|
821
|
-
const
|
|
822
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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