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.
Files changed (2) hide show
  1. package/bin/kooni.js +166 -128
  2. 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, chmodSync } from "node:fs";
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.0";
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 (raw mode, sin eco) para API keys / contraseñas.
343
- async function askSecret(rl, q) {
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
- process.stdout.write("\n " + C.b(q) + "\n " + C.cyan("› "));
346
- return await new Promise((resolve) => {
347
- const prev = input.isRaw;
348
- input.setRawMode(true);
349
- input.resume();
350
- let buf = "";
351
- const onData = (chunk) => {
352
- const s = chunk.toString();
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
- const cleanup = () => { input.removeListener("data", onData); input.pause(); if (!prev) input.setRawMode(false); };
366
- input.on("data", onData);
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 `npx wrangler ...` respetando el shim .cmd en Windows.
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
- const npx = isWin ? "npx.cmd" : "npx";
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 execFileSync(pnpm, args, {
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
- // pnpm ausente: habilítalo vía corepack y reintenta una vez.
527
- if (/pnpm/.test(e.message || "")) {
528
- try { execFileSync("corepack", ["enable", "pnpm"], { stdio: "ignore" }); } catch {}
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
- const resId = slug.replace(/-/g, "_");
557
- const dbName = `kooni_${resId}_db`;
558
- const kbName = `kooni_${resId}_kb`;
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, rl) {
675
- const slug = sanitizeSlug(flags.slug || "");
676
- const brainKey = ({ claude: "claude", anthropic: "claude", chatgpt: "chatgpt", openai: "chatgpt", gpt: "chatgpt", grok: "grok", xai: "grok", gateway: "gateway" })[String(flags.cerebro || flags.brain || "claude").trim().toLowerCase()] || "claude";
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"] || "Asistente").trim(),
682
- lang: normBotLang(flags.lang),
683
- tier: String(flags.tier || "free").trim().toLowerCase() === "pro" ? "pro" : "free",
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() || (brainKey === "gateway" ? "https://api.aisa.one/v1" : ""),
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 no-interactivo sin flag, devuelve default.
703
- async function onboarding(rl, answers) {
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
- answers.slug = answers.slug || sanitizeSlug(await ask(rl, t().qSlug, null) || "mi-negocio");
707
- if (!answers.businessName) answers.businessName = await ask(rl, t().qBusiness, null);
708
- if (!answers.botName || answers.botName === "Asistente") {
709
- const b = await ask(rl, t().qBotName, null);
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, null) || "https://api.aisa.one/v1";
741
+ answers.baseUrl = await ask(rl, t().qBaseUrl, undefined) || "https://api.aisa.one/v1";
733
742
  }
734
743
 
735
- answers.what = answers.what || await ask(rl, t().qWhat, null);
736
- answers.offer = answers.offer || await ask(rl, t().qOffer, null);
737
- answers.hours = answers.hours || await ask(rl, t().qHours, null);
738
- answers.location = answers.location || await ask(rl, t().qLoc, null);
739
- answers.phone = answers.phone || await ask(rl, t().qPhone, null);
740
- answers.web = answers.web || await ask(rl, t().qWeb, null);
741
- answers.pagos = answers.pagos || await ask(rl, t().qPagos, null);
742
- answers.faq = answers.faq || await ask(rl, t().qFaq, null);
743
- answers.reglas = answers.reglas || await ask(rl, t().qReglas, null);
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 || "cercano", default: 0 });
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(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
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"], { stdio: "inherit" });
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 = (readFileSync(wt, "utf8").match(/database_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_db";
783
- const kbName = (readFileSync(wt, "utf8").match(/index_name\s*=\s*"([^"]+)"/) || [])[1] || "kooni_kb";
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
- const list = wrangler(dir, ["d1", "list"]);
793
- const m = list.match(new RegExp(`${dbName}[\\s\\S]{0,160}?([0-9a-f-]{36})`));
794
- d1Id = m ? m[1] : "";
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
- patchWranglerFile(dir, (s) => s.replace(/database_id\s*=\s*"[^"]*"/, `database_id = "${d1Id}"`));
799
- console.log(" " + C.green("✓") + " " + t().d1Ok(d1Id));
800
- } else {
801
- console.log(" " + C.yellow("⚠") + " " + t().d1Warn);
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 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):`));
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], { stdio: ["pipe", "pipe", "pipe"], extra: { input: apiKey } });
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 askSecret(rl, m("Elige una contraseña para el panel /admin (usuario: admin):", "Choose a password for the /admin dashboard (user: admin):"));
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"], { stdio: ["pipe", "pipe", "pipe"], extra: { input: dash } });
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"], { stdio: ["pipe", "pipe", "pipe"], extra: { input: m[1] } });
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
- wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]);
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 dep = wrangler(dir, ["deploy"]);
862
- let url = (dep.match(/https:\/\/[a-z0-9-]+\.workers\.dev/) || [])[0] || "";
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
- wrangler(dir, ["deploy"]);
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, rl);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kooni-bot",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",