kooni-bot 0.2.0 → 0.2.2
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 +185 -139
- package/package.json +1 -1
package/bin/kooni.js
CHANGED
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
15
15
|
import { emitKeypressEvents } from "node:readline";
|
|
16
16
|
import { stdin as input, stdout as output } from "node:process";
|
|
17
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, readdirSync, statSync, cpSync
|
|
17
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, readdirSync, statSync, cpSync } from "node:fs";
|
|
18
|
+
import { realpathSync } from "node:fs";
|
|
18
19
|
import { join, basename } from "node:path";
|
|
19
20
|
import { homedir } from "node:os";
|
|
20
|
-
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
21
22
|
import { randomUUID } from "node:crypto";
|
|
22
23
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { realpathSync } from "node:fs";
|
|
24
24
|
|
|
25
|
-
const CLI_VERSION = "0.2.
|
|
25
|
+
const CLI_VERSION = "0.2.2";
|
|
26
26
|
|
|
27
27
|
const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
|
|
28
28
|
const BRANCH = process.env.KOONI_BRANCH || "main";
|
|
@@ -67,9 +67,9 @@ const DICT = {
|
|
|
67
67
|
qBusiness: "¿Cómo se llama tu negocio?",
|
|
68
68
|
qBotName: "¿Cómo se llama tu asistente?",
|
|
69
69
|
qLang: "¿En qué idioma debe hablar tu bot?",
|
|
70
|
-
qTier: "¿Qué plan quieres? (free | pro)",
|
|
71
70
|
brainQ: "¿Con qué cerebro (modelo de IA) quieres que piense tu bot?",
|
|
72
71
|
brainDesc: "recomendado",
|
|
72
|
+
qApiKey: (label) => `Pega tu API key de ${label} (no se mostrará):`,
|
|
73
73
|
qBaseUrl: "URL base del gateway (ej. https://api.aisa.one/v1)",
|
|
74
74
|
qWhat: "En una frase, ¿a qué se dedica?",
|
|
75
75
|
qOffer: "¿Qué ofreces? (tus servicios o productos principales, con precios si quieres)",
|
|
@@ -140,9 +140,9 @@ const DICT = {
|
|
|
140
140
|
qBusiness: "What's your business called?",
|
|
141
141
|
qBotName: "What's your assistant's name?",
|
|
142
142
|
qLang: "What language should your bot speak?",
|
|
143
|
-
qTier: "Which plan do you want? (free | pro)",
|
|
144
143
|
brainQ: "Which brain (AI model) should your bot think with?",
|
|
145
144
|
brainDesc: "recommended",
|
|
145
|
+
qApiKey: (label) => `Paste your ${label} API key (hidden):`,
|
|
146
146
|
qBaseUrl: "Gateway base URL (e.g. https://api.aisa.one/v1)",
|
|
147
147
|
qWhat: "In one line, what does it do?",
|
|
148
148
|
qOffer: "What do you offer? (main services or products, with prices if you like)",
|
|
@@ -339,32 +339,27 @@ async function ask(rl, q, val) {
|
|
|
339
339
|
return (await rl.question("\n " + C.b(q) + "\n " + C.cyan("› "))).trim();
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
// Input oculto
|
|
343
|
-
|
|
342
|
+
// Input oculto para API keys / contraseñas. Usa una interfaz readline DEDICADA
|
|
343
|
+
// (no el rl compartido) con máscara de salida para no chocar con los listeners
|
|
344
|
+
// del selector y soportar pegado. Devuelve "" si no hay TTY.
|
|
345
|
+
async function promptSecret(q) {
|
|
344
346
|
if (!interactive()) return "";
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
for (const ch of s) {
|
|
354
|
-
if (ch === "\r" || ch === "\n") {
|
|
355
|
-
cleanup();
|
|
356
|
-
process.stdout.write("\n");
|
|
357
|
-
resolve(buf);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (ch === "\u0003") { cleanup(); process.stdout.write("\n"); process.exit(130); }
|
|
361
|
-
if (ch === "\u007f" || ch === "\b") { buf = buf.slice(0, -1); }
|
|
362
|
-
else if (ch >= " " && ch !== "\u001b") { buf += ch; }
|
|
363
|
-
}
|
|
347
|
+
const rl = createInterface({ input, output, terminal: true });
|
|
348
|
+
const answer = await new Promise((resolve) => {
|
|
349
|
+
// Enmascara lo que teclea el usuario: solo deja pasar el prompt y los saltos
|
|
350
|
+
// de línea (nunca la tecla), para que la API key quede oculta.
|
|
351
|
+
const orig = rl._writeToOutput;
|
|
352
|
+
rl._writeToOutput = (s) => {
|
|
353
|
+
if (s === "\r\n" || s === "\n" || s === "\r") process.stdout.write(s);
|
|
354
|
+
else if (typeof orig === "function" && s.includes("› ")) orig.call(rl, s);
|
|
364
355
|
};
|
|
365
|
-
|
|
366
|
-
|
|
356
|
+
rl.question("\n " + C.b(q) + "\n " + C.cyan("› "), (a) => {
|
|
357
|
+
rl.close();
|
|
358
|
+
resolve(a);
|
|
359
|
+
});
|
|
367
360
|
});
|
|
361
|
+
process.stdout.write("\n");
|
|
362
|
+
return answer.trim();
|
|
368
363
|
}
|
|
369
364
|
|
|
370
365
|
async function confirm(rl, q) {
|
|
@@ -499,40 +494,52 @@ function resolveBotDir(arg) {
|
|
|
499
494
|
// ── exec cross-platform ──────────────────────────────────────────────────────
|
|
500
495
|
const isWin = process.platform === "win32";
|
|
501
496
|
|
|
502
|
-
// Ejecuta
|
|
497
|
+
// Ejecuta un binario de forma segura en Windows/macOS/Linux:
|
|
498
|
+
// - Unix/mac: `spawnSync(file, args)` directo.
|
|
499
|
+
// - Windows: `npx`/`pnpm` son shims `.cmd`, que NO se pueden spawnear directo
|
|
500
|
+
// (EINVAL). Se lanzan con `cmd.exe /c <file> <args...>` pasando los argumentos
|
|
501
|
+
// como lista separada (nada de `shell: true` ni concatenación → sin DEP0190).
|
|
502
|
+
// - `stdio` hereda por defecto (progreso en vivo); `capture` captura para parsear.
|
|
503
|
+
function run(file, args = [], opts = {}) {
|
|
504
|
+
const cwd = opts.cwd;
|
|
505
|
+
const input = opts.input != null ? String(opts.input) : undefined;
|
|
506
|
+
const stdio = opts.stdio || (opts.capture ? ["pipe", "pipe", "pipe"] : "inherit");
|
|
507
|
+
|
|
508
|
+
let cmd = file;
|
|
509
|
+
let argv = args;
|
|
510
|
+
|
|
511
|
+
if (isWin) {
|
|
512
|
+
const shim = { npx: "npx.cmd", pnpm: "pnpm.cmd" }[file] || file;
|
|
513
|
+
cmd = "cmd.exe";
|
|
514
|
+
argv = ["/d", "/c", shim, ...args];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const r = spawnSync(cmd, argv, { cwd, input, stdio, encoding: "utf8" });
|
|
518
|
+
if (r.error) throw r.error;
|
|
519
|
+
if (r.status !== 0) {
|
|
520
|
+
const err = new Error(`Command failed: ${[cmd, ...args].join(" ")}`);
|
|
521
|
+
err.stdout = r.stdout;
|
|
522
|
+
err.stderr = r.stderr;
|
|
523
|
+
err.status = r.status;
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
return (r.stdout || "").toString();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Ejecuta `npx wrangler ...`. `capture` captura stdout/stderr (para parsear);
|
|
530
|
+
// por defecto hereda la terminal (progreso visible).
|
|
503
531
|
function wrangler(dir, args, opts = {}) {
|
|
504
|
-
|
|
505
|
-
return execFileSync(npx, ["wrangler", ...args], {
|
|
506
|
-
cwd: dir,
|
|
507
|
-
encoding: "utf8",
|
|
508
|
-
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
|
|
509
|
-
...(isWin ? { shell: true } : {}),
|
|
510
|
-
...opts.extra,
|
|
511
|
-
});
|
|
532
|
+
return run("npx", ["wrangler", ...args], { cwd: dir, ...opts });
|
|
512
533
|
}
|
|
513
534
|
|
|
514
535
|
// Ejecuta `pnpm ...`, habilitando corepack si no existe pnpm.
|
|
515
536
|
function runPnpm(dir, args, opts = {}) {
|
|
516
|
-
const pnpm = process.env.KOONI_PNPM || "pnpm";
|
|
517
537
|
try {
|
|
518
|
-
return
|
|
519
|
-
cwd: dir,
|
|
520
|
-
encoding: "utf8",
|
|
521
|
-
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
|
|
522
|
-
...(isWin ? { shell: true } : {}),
|
|
523
|
-
...opts.extra,
|
|
524
|
-
});
|
|
538
|
+
return run("pnpm", args, { cwd: dir, ...opts });
|
|
525
539
|
} catch (e) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return execFileSync(pnpm, args, {
|
|
530
|
-
cwd: dir,
|
|
531
|
-
encoding: "utf8",
|
|
532
|
-
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
|
|
533
|
-
...(isWin ? { shell: true } : {}),
|
|
534
|
-
...opts.extra,
|
|
535
|
-
});
|
|
540
|
+
if (e && /ENOENT|not found|pnpm/.test(String(e.message || ""))) {
|
|
541
|
+
try { run("corepack", ["enable", "pnpm"], { stdio: "ignore" }); } catch {}
|
|
542
|
+
return run("pnpm", args, { cwd: dir, ...opts });
|
|
536
543
|
}
|
|
537
544
|
throw e;
|
|
538
545
|
}
|
|
@@ -553,9 +560,10 @@ function stampWrangler(dir, answers) {
|
|
|
553
560
|
if (!existsSync(wt)) return null;
|
|
554
561
|
let s = readFileSync(wt, "utf8");
|
|
555
562
|
const slug = answers.slug;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
563
|
+
// Nombres canónicos del proyecto (coinciden con `pnpm db:apply:remote`, que
|
|
564
|
+
// apunta a `kooni_db`, y con la instalación probada del template).
|
|
565
|
+
const dbName = "kooni_db";
|
|
566
|
+
const kbName = "kooni_kb";
|
|
559
567
|
const R = REGIONS[answers.lang] || REGIONS["es-MX"];
|
|
560
568
|
|
|
561
569
|
const set = (re, val) => { s = s.replace(re, val); };
|
|
@@ -567,7 +575,6 @@ function stampWrangler(dir, answers) {
|
|
|
567
575
|
set(/BOT_TIER\s*=\s*"[^"]*"/g, `BOT_TIER = "${answers.tier}"`);
|
|
568
576
|
set(/BUFFER_SECONDS\s*=\s*"[^"]*"/g, `BUFFER_SECONDS = "${answers.bufferSeconds || "15"}"`);
|
|
569
577
|
set(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = ""`);
|
|
570
|
-
// D1 / Vectorize namespaced por bot.
|
|
571
578
|
set(/database_name\s*=\s*"[^"]*"/, `database_name = "${dbName}"`);
|
|
572
579
|
set(/index_name\s*=\s*"[^"]*"/, `index_name = "${kbName}"`);
|
|
573
580
|
// El id del demo no sirve: placeholder hasta que `deploy` lo reemplace.
|
|
@@ -671,54 +678,58 @@ function writeDevVars(dir, answers, kbToken) {
|
|
|
671
678
|
writeFileSync(join(dir, ".dev.vars"), lines.join("\n") + "\n");
|
|
672
679
|
}
|
|
673
680
|
|
|
674
|
-
function collectAnswers(flags
|
|
675
|
-
const
|
|
676
|
-
const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", grok: "grok", xai: "grok", gateway: "gateway" })[
|
|
677
|
-
const tone = ({ cercano: "cercano", friendly: "cercano", formal: "formal", divertido: "divertido", playful: "divertido" })[String(flags.tono || "").trim().toLowerCase()] ||
|
|
681
|
+
function collectAnswers(flags) {
|
|
682
|
+
const rawBrain = String(flags.cerebro || flags.brain || "").trim().toLowerCase();
|
|
683
|
+
const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", grok: "grok", xai: "grok", gateway: "gateway" })[rawBrain] || null;
|
|
684
|
+
const tone = ({ cercano: "cercano", friendly: "cercano", formal: "formal", divertido: "divertido", playful: "divertido" })[String(flags.tono || "").trim().toLowerCase()] || null;
|
|
685
|
+
|
|
686
|
+
// Lo que vino por flag se conserva; lo que NO vino queda undefined para que
|
|
687
|
+
// `onboarding()` lo pregunte (interactivo) o use el default (no-interactivo).
|
|
678
688
|
return {
|
|
679
|
-
slug,
|
|
680
|
-
businessName: String(flags.negocio || flags.nombre || flags.name || "").trim(),
|
|
681
|
-
botName: String(flags["bot-name"] || "
|
|
682
|
-
lang: normBotLang(flags.lang),
|
|
683
|
-
tier: String(flags.tier
|
|
684
|
-
provider: BRAINS[brainKey].provider,
|
|
689
|
+
slug: flags.slug ? sanitizeSlug(flags.slug) : undefined,
|
|
690
|
+
businessName: String(flags.negocio || flags.nombre || flags.name || "").trim() || undefined,
|
|
691
|
+
botName: String(flags["bot-name"] || "").trim() || undefined,
|
|
692
|
+
lang: flags.lang ? normBotLang(flags.lang) : undefined,
|
|
693
|
+
tier: flags.tier ? (String(flags.tier).trim().toLowerCase() === "pro" ? "pro" : "free") : undefined,
|
|
694
|
+
provider: brainKey ? BRAINS[brainKey].provider : undefined,
|
|
685
695
|
brainKey,
|
|
686
|
-
baseUrl: String(flags["base-url"] || "").trim() ||
|
|
687
|
-
secret: BRAINS[brainKey].secret,
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
696
|
+
baseUrl: String(flags["base-url"] || "").trim() || undefined,
|
|
697
|
+
secret: brainKey ? BRAINS[brainKey].secret : undefined,
|
|
698
|
+
apiKey: String(flags["api-key"] || "").trim() || undefined,
|
|
699
|
+
what: String(flags.que || "").trim() || undefined,
|
|
700
|
+
offer: String(flags.ofrece || "").trim() || undefined,
|
|
701
|
+
hours: String(flags.horario || "").trim() || undefined,
|
|
702
|
+
location: String(flags.ubicacion || "").trim() || undefined,
|
|
703
|
+
phone: String(flags.telefono || "").trim() || undefined,
|
|
704
|
+
web: String(flags.web || flags.redes || "").trim() || undefined,
|
|
705
|
+
pagos: String(flags.pagos || "").trim() || undefined,
|
|
706
|
+
faq: String(flags.faq || "").trim() || undefined,
|
|
707
|
+
reglas: String(flags.reglas || "").trim() || undefined,
|
|
697
708
|
tone,
|
|
698
|
-
email: String(flags.email || "").trim(),
|
|
709
|
+
email: String(flags.email || "").trim() || undefined,
|
|
699
710
|
};
|
|
700
711
|
}
|
|
701
712
|
|
|
702
|
-
// Pregunta lo que falte, uno a uno. En
|
|
703
|
-
|
|
713
|
+
// Pregunta lo que falte, uno a uno. En interactivo muestra menús reales; en
|
|
714
|
+
// no-interactivo (`--yes`/agente) usa defaults. `defaultDir` alimenta el slug.
|
|
715
|
+
async function onboarding(rl, answers, defaultDir) {
|
|
704
716
|
console.log("\n " + C.dim(t().prep));
|
|
705
717
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (b) answers.botName = b;
|
|
718
|
+
if (!answers.slug) {
|
|
719
|
+
const dflt = sanitizeSlug(defaultDir || "mi-negocio");
|
|
720
|
+
const v = await ask(rl, t().qSlug, undefined);
|
|
721
|
+
answers.slug = v ? sanitizeSlug(v) : dflt;
|
|
711
722
|
}
|
|
723
|
+
if (!answers.businessName) answers.businessName = await ask(rl, t().qBusiness, undefined) || "";
|
|
724
|
+
if (!answers.botName) answers.botName = await ask(rl, t().qBotName, undefined) || "Asistente";
|
|
712
725
|
|
|
713
726
|
const langKeys = Object.keys(REGIONS);
|
|
714
727
|
const langIdx = await select(rl, t().qLang, langKeys.map((k) => ({ key: k, label: k })), { value: answers.lang, default: 0 });
|
|
715
728
|
answers.lang = langKeys[langIdx] || "es-MX";
|
|
716
729
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
], { value: answers.tier, default: 0 });
|
|
721
|
-
answers.tier = tierIdx === 1 ? "pro" : "free";
|
|
730
|
+
// El plan NO se pregunta: se instala en free y la activación de plan/clave se
|
|
731
|
+
// hace después desde el dashboard. Aquí solo fijamos el default.
|
|
732
|
+
answers.tier = answers.tier || "free";
|
|
722
733
|
|
|
723
734
|
const brainKeys = ["claude", "chatgpt", "grok", "gateway"];
|
|
724
735
|
const brainIdx = await select(rl, t().brainQ, brainKeys.map((k) => ({
|
|
@@ -729,35 +740,59 @@ async function onboarding(rl, answers) {
|
|
|
729
740
|
answers.provider = BRAINS[answers.brainKey].provider;
|
|
730
741
|
answers.secret = BRAINS[answers.brainKey].secret;
|
|
731
742
|
if (answers.brainKey === "gateway" && !answers.baseUrl) {
|
|
732
|
-
answers.baseUrl = await ask(rl, t().qBaseUrl,
|
|
743
|
+
answers.baseUrl = await ask(rl, t().qBaseUrl, undefined) || "https://api.aisa.one/v1";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// La API key se pide aquí, apenas se elige el cerebro — y se conserva para el
|
|
747
|
+
// deploy (se guarda como secret sin mostrarla). En no-interactivo se salta.
|
|
748
|
+
if (!answers.apiKey) {
|
|
749
|
+
answers.apiKey = await promptSecret(t().qApiKey(BRAINS[answers.brainKey].label));
|
|
733
750
|
}
|
|
734
751
|
|
|
735
|
-
answers.what = answers.what
|
|
736
|
-
answers.offer = answers.offer
|
|
737
|
-
answers.hours = answers.hours
|
|
738
|
-
answers.location = answers.location
|
|
739
|
-
answers.phone = answers.phone
|
|
740
|
-
answers.web = answers.web
|
|
741
|
-
answers.pagos = answers.pagos
|
|
742
|
-
answers.faq = answers.faq
|
|
743
|
-
answers.reglas = answers.reglas
|
|
752
|
+
answers.what = answers.what ?? (await ask(rl, t().qWhat, undefined) || "");
|
|
753
|
+
answers.offer = answers.offer ?? (await ask(rl, t().qOffer, undefined) || "");
|
|
754
|
+
answers.hours = answers.hours ?? (await ask(rl, t().qHours, undefined) || "");
|
|
755
|
+
answers.location = answers.location ?? (await ask(rl, t().qLoc, undefined) || "");
|
|
756
|
+
answers.phone = answers.phone ?? (await ask(rl, t().qPhone, undefined) || "");
|
|
757
|
+
answers.web = answers.web ?? (await ask(rl, t().qWeb, undefined) || "");
|
|
758
|
+
answers.pagos = answers.pagos ?? (await ask(rl, t().qPagos, undefined) || "");
|
|
759
|
+
answers.faq = answers.faq ?? (await ask(rl, t().qFaq, undefined) || "");
|
|
760
|
+
answers.reglas = answers.reglas ?? (await ask(rl, t().qReglas, undefined) || "");
|
|
744
761
|
|
|
745
762
|
const toneIdx = await select(rl, t().qTone, [
|
|
746
763
|
{ key: "cercano", label: t().toneFriendly, desc: t().tone1 },
|
|
747
764
|
{ key: "formal", label: t().toneFormal, desc: t().tone2 },
|
|
748
765
|
{ key: "divertido", label: t().tonePlayful, desc: t().tone3 },
|
|
749
|
-
], { value: answers.tone
|
|
766
|
+
], { value: answers.tone, default: 0 });
|
|
750
767
|
answers.tone = ["cercano", "formal", "divertido"][toneIdx] || "cercano";
|
|
751
768
|
|
|
752
769
|
return answers;
|
|
753
770
|
}
|
|
754
771
|
|
|
755
772
|
// ── deploy ───────────────────────────────────────────────────────────────────
|
|
773
|
+
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
774
|
+
|
|
756
775
|
function parseD1Id(raw) {
|
|
757
|
-
const m = raw.match(
|
|
776
|
+
const m = String(raw || "").match(UUID_RE);
|
|
758
777
|
return m ? m[0] : "";
|
|
759
778
|
}
|
|
760
779
|
|
|
780
|
+
// Extrae el id de una D1 por nombre desde la salida de `wrangler d1 list`. Sabe
|
|
781
|
+
// leer el JSON de `--json` (objeto por db) y, si no es JSON, cae a regex.
|
|
782
|
+
function findD1IdByName(raw, name) {
|
|
783
|
+
const s = String(raw || "");
|
|
784
|
+
try {
|
|
785
|
+
const parsed = JSON.parse(s);
|
|
786
|
+
const arr = Array.isArray(parsed) ? parsed : parsed.result;
|
|
787
|
+
if (Array.isArray(arr)) {
|
|
788
|
+
const hit = arr.find((d) => d && (d.name === name || d.database_name === name));
|
|
789
|
+
if (hit && hit.uuid) return hit.uuid;
|
|
790
|
+
}
|
|
791
|
+
} catch { /* no es JSON → regex */ }
|
|
792
|
+
const m = s.match(new RegExp(`${name}[\\s\\S]{0,200}?(${UUID_RE.source})`, "i"));
|
|
793
|
+
return m ? m[1] : "";
|
|
794
|
+
}
|
|
795
|
+
|
|
761
796
|
function patchWranglerFile(dir, fn) {
|
|
762
797
|
const wt = join(dir, "wrangler.toml");
|
|
763
798
|
if (!existsSync(wt)) return;
|
|
@@ -774,38 +809,43 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
774
809
|
|
|
775
810
|
// login
|
|
776
811
|
process.stdout.write(C.dim(" " + t().login + "\n"));
|
|
777
|
-
wrangler(dir, ["login"]
|
|
812
|
+
wrangler(dir, ["login"]);
|
|
778
813
|
console.log(" " + C.green("✓") + " " + t().loginOk);
|
|
779
814
|
|
|
780
|
-
// recursos
|
|
815
|
+
// recursos (nombres canónicos del proyecto: kooni_db / kooni_kb)
|
|
781
816
|
console.log("\n " + C.dim(t().creatingResources));
|
|
782
|
-
const dbName =
|
|
783
|
-
const kbName =
|
|
817
|
+
const dbName = "kooni_db";
|
|
818
|
+
const kbName = "kooni_kb";
|
|
784
819
|
|
|
785
820
|
let d1Id = "";
|
|
786
821
|
try {
|
|
787
|
-
const out = wrangler(dir, ["d1", "create", dbName]);
|
|
822
|
+
const out = wrangler(dir, ["d1", "create", dbName], { capture: true });
|
|
788
823
|
d1Id = parseD1Id(out);
|
|
789
824
|
} catch {}
|
|
790
825
|
if (!d1Id) {
|
|
791
826
|
try {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
827
|
+
// `--json` da un JSON limpio; si falla, caemos al texto plano.
|
|
828
|
+
let list = "";
|
|
829
|
+
try { list = wrangler(dir, ["d1", "list", "--json"], { capture: true }); }
|
|
830
|
+
catch { list = wrangler(dir, ["d1", "list"], { capture: true }); }
|
|
831
|
+
d1Id = findD1IdByName(list, dbName);
|
|
795
832
|
} catch {}
|
|
796
833
|
}
|
|
797
|
-
if (d1Id) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
834
|
+
if (!d1Id) {
|
|
835
|
+
// Sin id real NO podemos migrar ni desplegar: el placeholder rompe wrangler.
|
|
836
|
+
throw new Error(m(
|
|
837
|
+
`no pude crear/encontrar la base D1 "${dbName}". Córrelo a mano (npx wrangler d1 create ${dbName}) y pega el id en wrangler.toml, luego reintenta kooni-bot deploy.`,
|
|
838
|
+
`couldn't create/find the D1 database "${dbName}". Run it manually (npx wrangler d1 create ${dbName}), paste the id into wrangler.toml, then retry kooni-bot deploy.`,
|
|
839
|
+
));
|
|
802
840
|
}
|
|
841
|
+
patchWranglerFile(dir, (s) => s.replace(/database_id\s*=\s*"[^"]*"/, `database_id = "${d1Id}"`));
|
|
842
|
+
console.log(" " + C.green("✓") + " " + t().d1Ok(d1Id));
|
|
803
843
|
|
|
804
|
-
try { wrangler(dir, ["vectorize", "create", kbName, "--dimensions=1024", "--metric=cosine"]); } catch {}
|
|
844
|
+
try { wrangler(dir, ["vectorize", "create", kbName, "--dimensions=1024", "--metric=cosine"], { capture: true }); } catch {}
|
|
805
845
|
console.log(" " + C.green("✓") + " " + t().vectorOk);
|
|
806
846
|
|
|
807
847
|
try {
|
|
808
|
-
wrangler(dir, ["r2", "bucket", "create", "kooni-bot-catalog"]);
|
|
848
|
+
wrangler(dir, ["r2", "bucket", "create", "kooni-bot-catalog"], { capture: true });
|
|
809
849
|
console.log(" " + C.green("✓") + " " + t().r2Ok);
|
|
810
850
|
} catch {
|
|
811
851
|
console.log(" " + C.yellow("⚠") + " " + t().r2Warn);
|
|
@@ -818,24 +858,24 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
818
858
|
// secrets (sin mostrarlos)
|
|
819
859
|
console.log("\n " + C.dim(t().secretsTitle));
|
|
820
860
|
|
|
821
|
-
// API key:
|
|
861
|
+
// API key: viene del onboarding (o de --api-key). Si no está y no-interactivo,
|
|
862
|
+
// se delega al agente (wrangler secret put). No se vuelve a preguntar aquí.
|
|
822
863
|
let apiKey = flags["api-key"] || "";
|
|
823
|
-
if (!apiKey && interactive()) {
|
|
824
|
-
apiKey = await askSecret(rl, m(`Pega tu API key de ${BRAINS[flags.brainKey || "claude"].label} (no se mostrará):`, `Paste your ${BRAINS[flags.brainKey || "claude"].label} API key (hidden):`));
|
|
825
|
-
}
|
|
826
864
|
if (apiKey) {
|
|
827
|
-
wrangler(dir, ["secret", "put", BRAINS[flags.brainKey || "claude"].secret], {
|
|
865
|
+
wrangler(dir, ["secret", "put", BRAINS[flags.brainKey || "claude"].secret], { input: apiKey, capture: true });
|
|
828
866
|
console.log(" " + C.green("✓") + " " + t().secretOk(BRAINS[flags.brainKey || "claude"].secret));
|
|
829
867
|
} else if (!interactive()) {
|
|
830
868
|
console.log(C.yellow("\n ── PARA EL AGENTE ── falta la API key de IA."));
|
|
831
869
|
console.log(" " + C.cyan(`npx wrangler secret put ${BRAINS[flags.brainKey || "claude"].secret}`));
|
|
832
870
|
console.log(" " + m("Pídele la llave al usuario y guárdala tú (entrada oculta).", "Ask the user for the key and save it (hidden input)."));
|
|
871
|
+
} else {
|
|
872
|
+
console.log(" " + C.yellow("⚠") + " " + m("sin API key — el bot no responderá hasta que la pongas desde el panel (Configuración → Modelo de IA).", "no API key — the bot won't reply until you set it from the panel (Settings → AI Model)."));
|
|
833
873
|
}
|
|
834
874
|
|
|
835
875
|
let dash = "";
|
|
836
|
-
if (interactive()) dash = await
|
|
876
|
+
if (interactive()) dash = await promptSecret(m("Elige una contraseña para el panel /admin (usuario: admin):", "Choose a password for the /admin dashboard (user: admin):"));
|
|
837
877
|
if (!dash) dash = "kooni-local-password";
|
|
838
|
-
wrangler(dir, ["secret", "put", "DASHBOARD_PASSWORD"], {
|
|
878
|
+
wrangler(dir, ["secret", "put", "DASHBOARD_PASSWORD"], { input: dash, capture: true });
|
|
839
879
|
console.log(" " + C.green("✓") + " " + t().secretOk("DASHBOARD_PASSWORD"));
|
|
840
880
|
|
|
841
881
|
// KB token desde .dev.vars
|
|
@@ -843,28 +883,34 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
843
883
|
const dv = readFileSync(join(dir, ".dev.vars"), "utf8");
|
|
844
884
|
const m = dv.match(/^KB_REINDEX_TOKEN=(.+)$/m);
|
|
845
885
|
if (m && m[1]) {
|
|
846
|
-
wrangler(dir, ["secret", "put", "KB_REINDEX_TOKEN"], {
|
|
886
|
+
wrangler(dir, ["secret", "put", "KB_REINDEX_TOKEN"], { input: m[1], capture: true });
|
|
847
887
|
console.log(" " + C.green("✓") + " " + t().secretOk("KB_REINDEX_TOKEN"));
|
|
848
888
|
}
|
|
849
889
|
} catch {}
|
|
850
890
|
|
|
851
|
-
// dependencias + migraciones + deploy
|
|
891
|
+
// dependencias + migraciones + deploy (progreso visible en vivo)
|
|
852
892
|
console.log("\n " + C.dim(t().installing));
|
|
893
|
+
console.log(" " + C.dim(m("esto puede tardar unos minutos la primera vez (descarga el runtime y compila dependencias).", "this may take a few minutes the first time (downloads the runtime and builds native deps).")));
|
|
853
894
|
runPnpm(dir, ["install"]);
|
|
854
895
|
console.log(" " + C.green("✓") + " " + m("dependencias listas", "dependencies ready"));
|
|
855
896
|
|
|
856
897
|
console.log(" " + C.dim(t().migrations));
|
|
857
|
-
|
|
898
|
+
runPnpm(dir, ["db:apply:remote"]);
|
|
858
899
|
console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
|
|
859
900
|
|
|
860
901
|
console.log(" " + C.dim(t().deploying));
|
|
861
|
-
let
|
|
862
|
-
|
|
902
|
+
let url = "";
|
|
903
|
+
try {
|
|
904
|
+
const dep = runPnpm(dir, ["run", "deploy"], { capture: true });
|
|
905
|
+
url = (dep.match(/https:\/\/[a-z0-9-]+\.workers\.dev/) || [])[0] || "";
|
|
906
|
+
} catch {
|
|
907
|
+
// el deploy-check imprime el detalle; si falló, no seguimos.
|
|
908
|
+
throw new Error(t().deployFailed);
|
|
909
|
+
}
|
|
863
910
|
if (url) {
|
|
864
911
|
patchWranglerFile(dir, (s) => s.replace(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = "${url}"`));
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
if (!url) {
|
|
912
|
+
try { runPnpm(dir, ["run", "deploy"], { capture: true }); } catch {}
|
|
913
|
+
} else {
|
|
868
914
|
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")));
|
|
869
915
|
}
|
|
870
916
|
return url;
|
|
@@ -1018,8 +1064,8 @@ async function cmdInit(flags, rest) {
|
|
|
1018
1064
|
|
|
1019
1065
|
// config
|
|
1020
1066
|
console.log("\n " + C.cyan("◇ ") + C.b(t().runInit));
|
|
1021
|
-
const answers = collectAnswers(flags
|
|
1022
|
-
await onboarding(rl, answers);
|
|
1067
|
+
const answers = collectAnswers(flags);
|
|
1068
|
+
await onboarding(rl, answers, basename(dir));
|
|
1023
1069
|
|
|
1024
1070
|
const meta = stampWrangler(dir, answers);
|
|
1025
1071
|
const kbToken = "kooni-reindex-" + randomUUID().replace(/-/g, "").slice(0, 12);
|
|
@@ -1037,7 +1083,7 @@ async function cmdInit(flags, rest) {
|
|
|
1037
1083
|
|
|
1038
1084
|
// deploy (si no lo deshabilitan)
|
|
1039
1085
|
if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
|
|
1040
|
-
const deployFlags = { ...flags, brainKey: answers.brainKey };
|
|
1086
|
+
const deployFlags = { ...flags, brainKey: answers.brainKey, "api-key": answers.apiKey || "" };
|
|
1041
1087
|
const url = await deployBot(dir, { flags: deployFlags, rl });
|
|
1042
1088
|
if (url) {
|
|
1043
1089
|
console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
|
package/package.json
CHANGED