kooni-bot 0.2.0 → 0.2.1
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 +166 -128
- 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.1";
|
|
26
26
|
|
|
27
27
|
const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
|
|
28
28
|
const BRANCH = process.env.KOONI_BRANCH || "main";
|
|
@@ -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,49 @@ 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 SIN `shell: true`:
|
|
498
|
+
// - En Windows se resuelve el shim `.cmd` (npx.cmd / pnpm.cmd) y se lanza con
|
|
499
|
+
// `spawnSync` (así Node no concatena argumentos ni emite DEP0190).
|
|
500
|
+
// - `stdio` hereda por defecto para que el usuario vea el progreso en vivo.
|
|
501
|
+
// - Devuelve stdout como string; si el proceso falla, lanza el error con stderr.
|
|
502
|
+
function run(file, args = [], opts = {}) {
|
|
503
|
+
const cwd = opts.cwd;
|
|
504
|
+
const input = opts.input != null ? String(opts.input) : undefined;
|
|
505
|
+
const stdio = opts.stdio || (opts.capture ? ["pipe", "pipe", "pipe"] : "inherit");
|
|
506
|
+
|
|
507
|
+
let cmd = file;
|
|
508
|
+
if (isWin && !/\.(cmd|bat|exe)$/i.test(file)) {
|
|
509
|
+
// pnpm/npx en Windows son shims .cmd; ejecutarlos directo falla con ENOENT/EINVAL.
|
|
510
|
+
if (file === "pnpm") cmd = "pnpm.cmd";
|
|
511
|
+
else if (file === "npx") cmd = "npx.cmd";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const r = spawnSync(cmd, args, { cwd, input, stdio, encoding: "utf8" });
|
|
515
|
+
if (r.error) throw r.error;
|
|
516
|
+
if (r.status !== 0) {
|
|
517
|
+
const err = new Error(`Command failed: ${[cmd, ...args].join(" ")}`);
|
|
518
|
+
err.stdout = r.stdout;
|
|
519
|
+
err.stderr = r.stderr;
|
|
520
|
+
err.status = r.status;
|
|
521
|
+
throw err;
|
|
522
|
+
}
|
|
523
|
+
return (r.stdout || "").toString();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Ejecuta `npx wrangler ...`. `capture` captura stdout/stderr (para parsear);
|
|
527
|
+
// por defecto hereda la terminal (progreso visible).
|
|
503
528
|
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
|
-
});
|
|
529
|
+
return run("npx", ["wrangler", ...args], { cwd: dir, ...opts });
|
|
512
530
|
}
|
|
513
531
|
|
|
514
532
|
// Ejecuta `pnpm ...`, habilitando corepack si no existe pnpm.
|
|
515
533
|
function runPnpm(dir, args, opts = {}) {
|
|
516
|
-
const pnpm = process.env.KOONI_PNPM || "pnpm";
|
|
517
534
|
try {
|
|
518
|
-
return
|
|
519
|
-
cwd: dir,
|
|
520
|
-
encoding: "utf8",
|
|
521
|
-
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
|
|
522
|
-
...(isWin ? { shell: true } : {}),
|
|
523
|
-
...opts.extra,
|
|
524
|
-
});
|
|
535
|
+
return run("pnpm", args, { cwd: dir, ...opts });
|
|
525
536
|
} 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
|
-
});
|
|
537
|
+
if (e && /ENOENT|not found|pnpm/.test(String(e.message || ""))) {
|
|
538
|
+
try { run("corepack", ["enable", "pnpm"], { stdio: "ignore" }); } catch {}
|
|
539
|
+
return run("pnpm", args, { cwd: dir, ...opts });
|
|
536
540
|
}
|
|
537
541
|
throw e;
|
|
538
542
|
}
|
|
@@ -553,9 +557,10 @@ function stampWrangler(dir, answers) {
|
|
|
553
557
|
if (!existsSync(wt)) return null;
|
|
554
558
|
let s = readFileSync(wt, "utf8");
|
|
555
559
|
const slug = answers.slug;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
560
|
+
// Nombres canónicos del proyecto (coinciden con `pnpm db:apply:remote`, que
|
|
561
|
+
// apunta a `kooni_db`, y con la instalación probada del template).
|
|
562
|
+
const dbName = "kooni_db";
|
|
563
|
+
const kbName = "kooni_kb";
|
|
559
564
|
const R = REGIONS[answers.lang] || REGIONS["es-MX"];
|
|
560
565
|
|
|
561
566
|
const set = (re, val) => { s = s.replace(re, val); };
|
|
@@ -567,7 +572,6 @@ function stampWrangler(dir, answers) {
|
|
|
567
572
|
set(/BOT_TIER\s*=\s*"[^"]*"/g, `BOT_TIER = "${answers.tier}"`);
|
|
568
573
|
set(/BUFFER_SECONDS\s*=\s*"[^"]*"/g, `BUFFER_SECONDS = "${answers.bufferSeconds || "15"}"`);
|
|
569
574
|
set(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = ""`);
|
|
570
|
-
// D1 / Vectorize namespaced por bot.
|
|
571
575
|
set(/database_name\s*=\s*"[^"]*"/, `database_name = "${dbName}"`);
|
|
572
576
|
set(/index_name\s*=\s*"[^"]*"/, `index_name = "${kbName}"`);
|
|
573
577
|
// El id del demo no sirve: placeholder hasta que `deploy` lo reemplace.
|
|
@@ -671,44 +675,49 @@ function writeDevVars(dir, answers, kbToken) {
|
|
|
671
675
|
writeFileSync(join(dir, ".dev.vars"), lines.join("\n") + "\n");
|
|
672
676
|
}
|
|
673
677
|
|
|
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()] ||
|
|
678
|
+
function collectAnswers(flags) {
|
|
679
|
+
const rawBrain = String(flags.cerebro || flags.brain || "").trim().toLowerCase();
|
|
680
|
+
const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", grok: "grok", xai: "grok", gateway: "gateway" })[rawBrain] || null;
|
|
681
|
+
const tone = ({ cercano: "cercano", friendly: "cercano", formal: "formal", divertido: "divertido", playful: "divertido" })[String(flags.tono || "").trim().toLowerCase()] || null;
|
|
682
|
+
|
|
683
|
+
// Lo que vino por flag se conserva; lo que NO vino queda undefined para que
|
|
684
|
+
// `onboarding()` lo pregunte (interactivo) o use el default (no-interactivo).
|
|
678
685
|
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,
|
|
686
|
+
slug: flags.slug ? sanitizeSlug(flags.slug) : undefined,
|
|
687
|
+
businessName: String(flags.negocio || flags.nombre || flags.name || "").trim() || undefined,
|
|
688
|
+
botName: String(flags["bot-name"] || "").trim() || undefined,
|
|
689
|
+
lang: flags.lang ? normBotLang(flags.lang) : undefined,
|
|
690
|
+
tier: flags.tier ? (String(flags.tier).trim().toLowerCase() === "pro" ? "pro" : "free") : undefined,
|
|
691
|
+
provider: brainKey ? BRAINS[brainKey].provider : undefined,
|
|
685
692
|
brainKey,
|
|
686
|
-
baseUrl: String(flags["base-url"] || "").trim() ||
|
|
687
|
-
secret: BRAINS[brainKey].secret,
|
|
688
|
-
what: String(flags.que || "").trim(),
|
|
689
|
-
offer: String(flags.ofrece || "").trim(),
|
|
690
|
-
hours: String(flags.horario || "").trim(),
|
|
691
|
-
location: String(flags.ubicacion || "").trim(),
|
|
692
|
-
phone: String(flags.telefono || "").trim(),
|
|
693
|
-
web: String(flags.web || flags.redes || "").trim(),
|
|
694
|
-
pagos: String(flags.pagos || "").trim(),
|
|
695
|
-
faq: String(flags.faq || "").trim(),
|
|
696
|
-
reglas: String(flags.reglas || "").trim(),
|
|
693
|
+
baseUrl: String(flags["base-url"] || "").trim() || undefined,
|
|
694
|
+
secret: brainKey ? BRAINS[brainKey].secret : undefined,
|
|
695
|
+
what: String(flags.que || "").trim() || undefined,
|
|
696
|
+
offer: String(flags.ofrece || "").trim() || undefined,
|
|
697
|
+
hours: String(flags.horario || "").trim() || undefined,
|
|
698
|
+
location: String(flags.ubicacion || "").trim() || undefined,
|
|
699
|
+
phone: String(flags.telefono || "").trim() || undefined,
|
|
700
|
+
web: String(flags.web || flags.redes || "").trim() || undefined,
|
|
701
|
+
pagos: String(flags.pagos || "").trim() || undefined,
|
|
702
|
+
faq: String(flags.faq || "").trim() || undefined,
|
|
703
|
+
reglas: String(flags.reglas || "").trim() || undefined,
|
|
697
704
|
tone,
|
|
698
|
-
email: String(flags.email || "").trim(),
|
|
705
|
+
email: String(flags.email || "").trim() || undefined,
|
|
699
706
|
};
|
|
700
707
|
}
|
|
701
708
|
|
|
702
|
-
// Pregunta lo que falte, uno a uno. En
|
|
703
|
-
|
|
709
|
+
// Pregunta lo que falte, uno a uno. En interactivo muestra menús reales; en
|
|
710
|
+
// no-interactivo (`--yes`/agente) usa defaults. `defaultDir` alimenta el slug.
|
|
711
|
+
async function onboarding(rl, answers, defaultDir) {
|
|
704
712
|
console.log("\n " + C.dim(t().prep));
|
|
705
713
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (b) answers.botName = b;
|
|
714
|
+
if (!answers.slug) {
|
|
715
|
+
const dflt = sanitizeSlug(defaultDir || "mi-negocio");
|
|
716
|
+
const v = await ask(rl, t().qSlug, undefined);
|
|
717
|
+
answers.slug = v ? sanitizeSlug(v) : dflt;
|
|
711
718
|
}
|
|
719
|
+
if (!answers.businessName) answers.businessName = await ask(rl, t().qBusiness, undefined) || "";
|
|
720
|
+
if (!answers.botName) answers.botName = await ask(rl, t().qBotName, undefined) || "Asistente";
|
|
712
721
|
|
|
713
722
|
const langKeys = Object.keys(REGIONS);
|
|
714
723
|
const langIdx = await select(rl, t().qLang, langKeys.map((k) => ({ key: k, label: k })), { value: answers.lang, default: 0 });
|
|
@@ -729,35 +738,53 @@ async function onboarding(rl, answers) {
|
|
|
729
738
|
answers.provider = BRAINS[answers.brainKey].provider;
|
|
730
739
|
answers.secret = BRAINS[answers.brainKey].secret;
|
|
731
740
|
if (answers.brainKey === "gateway" && !answers.baseUrl) {
|
|
732
|
-
answers.baseUrl = await ask(rl, t().qBaseUrl,
|
|
741
|
+
answers.baseUrl = await ask(rl, t().qBaseUrl, undefined) || "https://api.aisa.one/v1";
|
|
733
742
|
}
|
|
734
743
|
|
|
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
|
|
744
|
+
answers.what = answers.what ?? (await ask(rl, t().qWhat, undefined) || "");
|
|
745
|
+
answers.offer = answers.offer ?? (await ask(rl, t().qOffer, undefined) || "");
|
|
746
|
+
answers.hours = answers.hours ?? (await ask(rl, t().qHours, undefined) || "");
|
|
747
|
+
answers.location = answers.location ?? (await ask(rl, t().qLoc, undefined) || "");
|
|
748
|
+
answers.phone = answers.phone ?? (await ask(rl, t().qPhone, undefined) || "");
|
|
749
|
+
answers.web = answers.web ?? (await ask(rl, t().qWeb, undefined) || "");
|
|
750
|
+
answers.pagos = answers.pagos ?? (await ask(rl, t().qPagos, undefined) || "");
|
|
751
|
+
answers.faq = answers.faq ?? (await ask(rl, t().qFaq, undefined) || "");
|
|
752
|
+
answers.reglas = answers.reglas ?? (await ask(rl, t().qReglas, undefined) || "");
|
|
744
753
|
|
|
745
754
|
const toneIdx = await select(rl, t().qTone, [
|
|
746
755
|
{ key: "cercano", label: t().toneFriendly, desc: t().tone1 },
|
|
747
756
|
{ key: "formal", label: t().toneFormal, desc: t().tone2 },
|
|
748
757
|
{ key: "divertido", label: t().tonePlayful, desc: t().tone3 },
|
|
749
|
-
], { value: answers.tone
|
|
758
|
+
], { value: answers.tone, default: 0 });
|
|
750
759
|
answers.tone = ["cercano", "formal", "divertido"][toneIdx] || "cercano";
|
|
751
760
|
|
|
752
761
|
return answers;
|
|
753
762
|
}
|
|
754
763
|
|
|
755
764
|
// ── deploy ───────────────────────────────────────────────────────────────────
|
|
765
|
+
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
766
|
+
|
|
756
767
|
function parseD1Id(raw) {
|
|
757
|
-
const m = raw.match(
|
|
768
|
+
const m = String(raw || "").match(UUID_RE);
|
|
758
769
|
return m ? m[0] : "";
|
|
759
770
|
}
|
|
760
771
|
|
|
772
|
+
// Extrae el id de una D1 por nombre desde la salida de `wrangler d1 list`. Sabe
|
|
773
|
+
// leer el JSON de `--json` (objeto por db) y, si no es JSON, cae a regex.
|
|
774
|
+
function findD1IdByName(raw, name) {
|
|
775
|
+
const s = String(raw || "");
|
|
776
|
+
try {
|
|
777
|
+
const parsed = JSON.parse(s);
|
|
778
|
+
const arr = Array.isArray(parsed) ? parsed : parsed.result;
|
|
779
|
+
if (Array.isArray(arr)) {
|
|
780
|
+
const hit = arr.find((d) => d && (d.name === name || d.database_name === name));
|
|
781
|
+
if (hit && hit.uuid) return hit.uuid;
|
|
782
|
+
}
|
|
783
|
+
} catch { /* no es JSON → regex */ }
|
|
784
|
+
const m = s.match(new RegExp(`${name}[\\s\\S]{0,200}?(${UUID_RE.source})`, "i"));
|
|
785
|
+
return m ? m[1] : "";
|
|
786
|
+
}
|
|
787
|
+
|
|
761
788
|
function patchWranglerFile(dir, fn) {
|
|
762
789
|
const wt = join(dir, "wrangler.toml");
|
|
763
790
|
if (!existsSync(wt)) return;
|
|
@@ -774,38 +801,43 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
774
801
|
|
|
775
802
|
// login
|
|
776
803
|
process.stdout.write(C.dim(" " + t().login + "\n"));
|
|
777
|
-
wrangler(dir, ["login"]
|
|
804
|
+
wrangler(dir, ["login"]);
|
|
778
805
|
console.log(" " + C.green("✓") + " " + t().loginOk);
|
|
779
806
|
|
|
780
|
-
// recursos
|
|
807
|
+
// recursos (nombres canónicos del proyecto: kooni_db / kooni_kb)
|
|
781
808
|
console.log("\n " + C.dim(t().creatingResources));
|
|
782
|
-
const dbName =
|
|
783
|
-
const kbName =
|
|
809
|
+
const dbName = "kooni_db";
|
|
810
|
+
const kbName = "kooni_kb";
|
|
784
811
|
|
|
785
812
|
let d1Id = "";
|
|
786
813
|
try {
|
|
787
|
-
const out = wrangler(dir, ["d1", "create", dbName]);
|
|
814
|
+
const out = wrangler(dir, ["d1", "create", dbName], { capture: true });
|
|
788
815
|
d1Id = parseD1Id(out);
|
|
789
816
|
} catch {}
|
|
790
817
|
if (!d1Id) {
|
|
791
818
|
try {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
819
|
+
// `--json` da un JSON limpio; si falla, caemos al texto plano.
|
|
820
|
+
let list = "";
|
|
821
|
+
try { list = wrangler(dir, ["d1", "list", "--json"], { capture: true }); }
|
|
822
|
+
catch { list = wrangler(dir, ["d1", "list"], { capture: true }); }
|
|
823
|
+
d1Id = findD1IdByName(list, dbName);
|
|
795
824
|
} catch {}
|
|
796
825
|
}
|
|
797
|
-
if (d1Id) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
826
|
+
if (!d1Id) {
|
|
827
|
+
// Sin id real NO podemos migrar ni desplegar: el placeholder rompe wrangler.
|
|
828
|
+
throw new Error(m(
|
|
829
|
+
`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.`,
|
|
830
|
+
`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.`,
|
|
831
|
+
));
|
|
802
832
|
}
|
|
833
|
+
patchWranglerFile(dir, (s) => s.replace(/database_id\s*=\s*"[^"]*"/, `database_id = "${d1Id}"`));
|
|
834
|
+
console.log(" " + C.green("✓") + " " + t().d1Ok(d1Id));
|
|
803
835
|
|
|
804
|
-
try { wrangler(dir, ["vectorize", "create", kbName, "--dimensions=1024", "--metric=cosine"]); } catch {}
|
|
836
|
+
try { wrangler(dir, ["vectorize", "create", kbName, "--dimensions=1024", "--metric=cosine"], { capture: true }); } catch {}
|
|
805
837
|
console.log(" " + C.green("✓") + " " + t().vectorOk);
|
|
806
838
|
|
|
807
839
|
try {
|
|
808
|
-
wrangler(dir, ["r2", "bucket", "create", "kooni-bot-catalog"]);
|
|
840
|
+
wrangler(dir, ["r2", "bucket", "create", "kooni-bot-catalog"], { capture: true });
|
|
809
841
|
console.log(" " + C.green("✓") + " " + t().r2Ok);
|
|
810
842
|
} catch {
|
|
811
843
|
console.log(" " + C.yellow("⚠") + " " + t().r2Warn);
|
|
@@ -821,10 +853,10 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
821
853
|
// API key: interactiva por defecto; en no-interactivo se delega al agente.
|
|
822
854
|
let apiKey = flags["api-key"] || "";
|
|
823
855
|
if (!apiKey && interactive()) {
|
|
824
|
-
apiKey = await
|
|
856
|
+
apiKey = await promptSecret(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
857
|
}
|
|
826
858
|
if (apiKey) {
|
|
827
|
-
wrangler(dir, ["secret", "put", BRAINS[flags.brainKey || "claude"].secret], {
|
|
859
|
+
wrangler(dir, ["secret", "put", BRAINS[flags.brainKey || "claude"].secret], { input: apiKey, capture: true });
|
|
828
860
|
console.log(" " + C.green("✓") + " " + t().secretOk(BRAINS[flags.brainKey || "claude"].secret));
|
|
829
861
|
} else if (!interactive()) {
|
|
830
862
|
console.log(C.yellow("\n ── PARA EL AGENTE ── falta la API key de IA."));
|
|
@@ -833,9 +865,9 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
833
865
|
}
|
|
834
866
|
|
|
835
867
|
let dash = "";
|
|
836
|
-
if (interactive()) dash = await
|
|
868
|
+
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
869
|
if (!dash) dash = "kooni-local-password";
|
|
838
|
-
wrangler(dir, ["secret", "put", "DASHBOARD_PASSWORD"], {
|
|
870
|
+
wrangler(dir, ["secret", "put", "DASHBOARD_PASSWORD"], { input: dash, capture: true });
|
|
839
871
|
console.log(" " + C.green("✓") + " " + t().secretOk("DASHBOARD_PASSWORD"));
|
|
840
872
|
|
|
841
873
|
// KB token desde .dev.vars
|
|
@@ -843,28 +875,34 @@ async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
|
843
875
|
const dv = readFileSync(join(dir, ".dev.vars"), "utf8");
|
|
844
876
|
const m = dv.match(/^KB_REINDEX_TOKEN=(.+)$/m);
|
|
845
877
|
if (m && m[1]) {
|
|
846
|
-
wrangler(dir, ["secret", "put", "KB_REINDEX_TOKEN"], {
|
|
878
|
+
wrangler(dir, ["secret", "put", "KB_REINDEX_TOKEN"], { input: m[1], capture: true });
|
|
847
879
|
console.log(" " + C.green("✓") + " " + t().secretOk("KB_REINDEX_TOKEN"));
|
|
848
880
|
}
|
|
849
881
|
} catch {}
|
|
850
882
|
|
|
851
|
-
// dependencias + migraciones + deploy
|
|
883
|
+
// dependencias + migraciones + deploy (progreso visible en vivo)
|
|
852
884
|
console.log("\n " + C.dim(t().installing));
|
|
885
|
+
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
886
|
runPnpm(dir, ["install"]);
|
|
854
887
|
console.log(" " + C.green("✓") + " " + m("dependencias listas", "dependencies ready"));
|
|
855
888
|
|
|
856
889
|
console.log(" " + C.dim(t().migrations));
|
|
857
|
-
|
|
890
|
+
runPnpm(dir, ["db:apply:remote"]);
|
|
858
891
|
console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
|
|
859
892
|
|
|
860
893
|
console.log(" " + C.dim(t().deploying));
|
|
861
|
-
let
|
|
862
|
-
|
|
894
|
+
let url = "";
|
|
895
|
+
try {
|
|
896
|
+
const dep = runPnpm(dir, ["run", "deploy"], { capture: true });
|
|
897
|
+
url = (dep.match(/https:\/\/[a-z0-9-]+\.workers\.dev/) || [])[0] || "";
|
|
898
|
+
} catch {
|
|
899
|
+
// el deploy-check imprime el detalle; si falló, no seguimos.
|
|
900
|
+
throw new Error(t().deployFailed);
|
|
901
|
+
}
|
|
863
902
|
if (url) {
|
|
864
903
|
patchWranglerFile(dir, (s) => s.replace(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = "${url}"`));
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
if (!url) {
|
|
904
|
+
try { runPnpm(dir, ["run", "deploy"], { capture: true }); } catch {}
|
|
905
|
+
} else {
|
|
868
906
|
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
907
|
}
|
|
870
908
|
return url;
|
|
@@ -1018,8 +1056,8 @@ async function cmdInit(flags, rest) {
|
|
|
1018
1056
|
|
|
1019
1057
|
// config
|
|
1020
1058
|
console.log("\n " + C.cyan("◇ ") + C.b(t().runInit));
|
|
1021
|
-
const answers = collectAnswers(flags
|
|
1022
|
-
await onboarding(rl, answers);
|
|
1059
|
+
const answers = collectAnswers(flags);
|
|
1060
|
+
await onboarding(rl, answers, basename(dir));
|
|
1023
1061
|
|
|
1024
1062
|
const meta = stampWrangler(dir, answers);
|
|
1025
1063
|
const kbToken = "kooni-reindex-" + randomUUID().replace(/-/g, "").slice(0, 12);
|
package/package.json
CHANGED