kooni-bot 0.1.2 → 0.2.0
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 +1259 -224
- package/package.json +1 -1
package/bin/kooni.js
CHANGED
|
@@ -1,224 +1,1259 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// kooni-bot — instala y
|
|
3
|
-
// comando. Derivado de Forja (MIT,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// npx kooni-bot
|
|
7
|
-
// npx kooni-bot
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// kooni-bot — instala, configura, despliega y mantiene bots de IA de Kooni en TU
|
|
3
|
+
// propia infraestructura (Cloudflare), en un comando. Derivado de Forja (MIT,
|
|
4
|
+
// © Horizontes IA). Bilingüe (ES/EN), cero dependencias, Node >= 18.
|
|
5
|
+
//
|
|
6
|
+
// npx kooni-bot init [dir] → descarga el template, te configura el bot y (opcional) despliega
|
|
7
|
+
// npx kooni-bot deploy [dir] → provisiona Cloudflare y publica el worker
|
|
8
|
+
// npx kooni-bot update [dir] → trae la versión nueva SIN perder tu config ni datos
|
|
9
|
+
// npx kooni-bot doctor [dir] → diagnóstico del bot instalado
|
|
10
|
+
// npx kooni-bot version → versión del CLI
|
|
11
|
+
//
|
|
12
|
+
// Kooni es open source (MIT). No valida licencia contra servidores: el tier
|
|
13
|
+
// free/pro se controla con BOT_TIER en wrangler.toml y un código local en el panel.
|
|
14
|
+
import { createInterface } from "node:readline/promises";
|
|
15
|
+
import { emitKeypressEvents } from "node:readline";
|
|
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";
|
|
18
|
+
import { join, basename } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { realpathSync } from "node:fs";
|
|
24
|
+
|
|
25
|
+
const CLI_VERSION = "0.2.0";
|
|
26
|
+
|
|
27
|
+
const REPO = process.env.KOONI_REPO || "iamnocodeveloper/kooni-bot";
|
|
28
|
+
const BRANCH = process.env.KOONI_BRANCH || "main";
|
|
29
|
+
const TARBALL = `https://codeload.github.com/${REPO}/tar.gz/refs/heads/${BRANCH}`;
|
|
30
|
+
const RAW_VERSION_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/package.json`;
|
|
31
|
+
// Control del dueño: check-in al instalar (no bloquea; solo registra quién).
|
|
32
|
+
const CHECKIN_URL = process.env.KOONI_CHECKIN_URL || "https://f5gacw7g.function2.insforge.app/registrar-instalacion";
|
|
33
|
+
|
|
34
|
+
const CFG_DIR = join(homedir(), ".kooni");
|
|
35
|
+
const CFG_FILE = join(CFG_DIR, "config.json");
|
|
36
|
+
const MARKER = ".kooni-bot.json";
|
|
37
|
+
const SKILL_DIR = join(homedir(), ".claude", "skills", "kooni");
|
|
38
|
+
|
|
39
|
+
// ── color ─────────────────────────────────────────────────────────────────────
|
|
40
|
+
const C = {
|
|
41
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
42
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
43
|
+
b: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
44
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
45
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
46
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
47
|
+
};
|
|
48
|
+
const c256 = (n, s) => `\x1b[38;5;${n}m${s}\x1b[0m`;
|
|
49
|
+
|
|
50
|
+
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
51
|
+
const DICT = {
|
|
52
|
+
es: {
|
|
53
|
+
tagline: "asistentes de IA para tu negocio · open source",
|
|
54
|
+
chooseLang: "Idioma / Language",
|
|
55
|
+
optEs: "Español", optEn: "English",
|
|
56
|
+
commands: "Comandos:",
|
|
57
|
+
// init
|
|
58
|
+
whichDir: "¿En qué carpeta lo instalo? (vacío = actual)",
|
|
59
|
+
download: "Bajando el template de Kooni…",
|
|
60
|
+
templateOk: "template descargado",
|
|
61
|
+
runInit: "El template está listo. Ahora configuramos tu bot:",
|
|
62
|
+
needDir: "No encuentro una instalación de Kooni en:",
|
|
63
|
+
existingProject: "la carpeta ya tiene un proyecto. Usa `kooni-bot update` en su lugar.",
|
|
64
|
+
// onboarding
|
|
65
|
+
prep: "Vamos a preparar tu bot · unas preguntas rápidas (enter = saltar)",
|
|
66
|
+
qSlug: "¿Qué slug quieres para el bot? (corto, ej. mi-negocio)",
|
|
67
|
+
qBusiness: "¿Cómo se llama tu negocio?",
|
|
68
|
+
qBotName: "¿Cómo se llama tu asistente?",
|
|
69
|
+
qLang: "¿En qué idioma debe hablar tu bot?",
|
|
70
|
+
qTier: "¿Qué plan quieres? (free | pro)",
|
|
71
|
+
brainQ: "¿Con qué cerebro (modelo de IA) quieres que piense tu bot?",
|
|
72
|
+
brainDesc: "recomendado",
|
|
73
|
+
qBaseUrl: "URL base del gateway (ej. https://api.aisa.one/v1)",
|
|
74
|
+
qWhat: "En una frase, ¿a qué se dedica?",
|
|
75
|
+
qOffer: "¿Qué ofreces? (tus servicios o productos principales, con precios si quieres)",
|
|
76
|
+
qHours: "¿Cuál es tu horario de atención?",
|
|
77
|
+
qLoc: "¿Dónde estás? (dirección o 'en línea')",
|
|
78
|
+
qPhone: "¿Un teléfono/WhatsApp de contacto?",
|
|
79
|
+
qWeb: "¿Tienes sitio web o redes sociales? (pega los links, o enter para saltar)",
|
|
80
|
+
qPagos: "¿Qué métodos de pago aceptas? (efectivo, tarjeta, transferencia…)",
|
|
81
|
+
qFaq: "¿Qué es lo que MÁS te pregunta la gente? (separa con |)",
|
|
82
|
+
qReglas: "¿Algo que el bot NO deba hacer o decir? ¿Y cuándo debe pasarte la conversación?",
|
|
83
|
+
qTone: "¿Cómo quieres que suene?",
|
|
84
|
+
toneFriendly: "Cercano", toneFormal: "Formal", tonePlayful: "Divertido",
|
|
85
|
+
tone1: "cercano y amigable, como hablarle a un conocido",
|
|
86
|
+
tone2: "formal y profesional, claro y respetuoso",
|
|
87
|
+
tone3: "relajado y divertido, con chispa pero sin perder claridad",
|
|
88
|
+
configDone: "Config lista · tu bot ya sabe de tu negocio",
|
|
89
|
+
// deploy
|
|
90
|
+
deployAsk: "¿Desplegar en TU cuenta de Cloudflare ahora?",
|
|
91
|
+
login: "Autenticando Cloudflare (abre el navegador)…",
|
|
92
|
+
loginOk: "Cloudflare autenticado",
|
|
93
|
+
creatingResources: "Creando recursos en TU cuenta (D1 + Vectorize + R2)…",
|
|
94
|
+
d1Ok: (id) => `D1 listo (id ${id})`,
|
|
95
|
+
d1Warn: "no se detectó el id de D1 — pégalo a mano en wrangler.toml",
|
|
96
|
+
vectorOk: "Vectorize listo (o ya existía)",
|
|
97
|
+
r2Ok: "R2 listo (o ya existía)",
|
|
98
|
+
r2Warn: "R2 no está habilitado en tu cuenta — el bot funciona sin él (se comenta el binding)",
|
|
99
|
+
secretsTitle: "Guardando secrets en Cloudflare (sin mostrarlos)…",
|
|
100
|
+
secretOk: (k) => `secret ${k} guardado`,
|
|
101
|
+
installing: "Instalando dependencias…",
|
|
102
|
+
migrations: "Aplicando migraciones D1…",
|
|
103
|
+
deploying: "Desplegando el worker…",
|
|
104
|
+
panel: "Panel de administración:",
|
|
105
|
+
next: "Lo que sigue: conecta tu primer canal (Telegram ~5 min) desde el panel → Conexiones.",
|
|
106
|
+
// update
|
|
107
|
+
updRevalidating: "Buscando la versión nueva…",
|
|
108
|
+
updUpToDate: "Ya estás en la última versión.",
|
|
109
|
+
updDone: (v) => `Actualizado a v${v} (tu config y tus datos se conservaron)`,
|
|
110
|
+
updBackup: (p) => `Respaldé tu versión anterior en ${p} — por si quieres recuperar algo.`,
|
|
111
|
+
updPreserved: "Se conservaron: tu configuración (member/), tu wrangler.toml y tus datos.",
|
|
112
|
+
updReplaced: "Se actualizó: el motor del bot (todo lo demás).",
|
|
113
|
+
// doctor
|
|
114
|
+
doctorTitle: "Diagnóstico de Kooni",
|
|
115
|
+
okTag: "✓", warnTag: "⚠", badTag: "✗",
|
|
116
|
+
// generic
|
|
117
|
+
ok: "✓", err: "✗",
|
|
118
|
+
noInstallable: "Tu licencia está lista y guardada. El bot Starter llega en breve — te avisamos en la comunidad.",
|
|
119
|
+
// check-in
|
|
120
|
+
qEmail: "¿Tu email? (opcional, para soporte)",
|
|
121
|
+
// error
|
|
122
|
+
templateMissing: "no encontré el instalador en el template.",
|
|
123
|
+
deployFailed: "el deploy falló. Revisa los mensajes de arriba.",
|
|
124
|
+
unknown: "comando desconocido:",
|
|
125
|
+
helpIntro: "instala tu asistente de IA en Cloudflare",
|
|
126
|
+
},
|
|
127
|
+
en: {
|
|
128
|
+
tagline: "AI assistants for your business · open source",
|
|
129
|
+
chooseLang: "Language / Idioma",
|
|
130
|
+
optEs: "Spanish", optEn: "English",
|
|
131
|
+
commands: "Commands:",
|
|
132
|
+
whichDir: "Which folder should I install it in? (empty = current)",
|
|
133
|
+
download: "Downloading the Kooni template…",
|
|
134
|
+
templateOk: "template downloaded",
|
|
135
|
+
runInit: "Template ready. Now let's configure your bot:",
|
|
136
|
+
needDir: "No Kooni install found in:",
|
|
137
|
+
existingProject: "folder already has a project. Use `kooni-bot update` instead.",
|
|
138
|
+
prep: "Let's set up your bot · a few quick questions (enter = skip)",
|
|
139
|
+
qSlug: "What slug do you want? (short, e.g. my-business)",
|
|
140
|
+
qBusiness: "What's your business called?",
|
|
141
|
+
qBotName: "What's your assistant's name?",
|
|
142
|
+
qLang: "What language should your bot speak?",
|
|
143
|
+
qTier: "Which plan do you want? (free | pro)",
|
|
144
|
+
brainQ: "Which brain (AI model) should your bot think with?",
|
|
145
|
+
brainDesc: "recommended",
|
|
146
|
+
qBaseUrl: "Gateway base URL (e.g. https://api.aisa.one/v1)",
|
|
147
|
+
qWhat: "In one line, what does it do?",
|
|
148
|
+
qOffer: "What do you offer? (main services or products, with prices if you like)",
|
|
149
|
+
qHours: "What are your hours?",
|
|
150
|
+
qLoc: "Where are you? (address or 'online')",
|
|
151
|
+
qPhone: "A phone/WhatsApp contact?",
|
|
152
|
+
qWeb: "Do you have a website or social profiles? (paste links, or enter to skip)",
|
|
153
|
+
qPagos: "Which payment methods do you accept? (cash, card, transfer…)",
|
|
154
|
+
qFaq: "What do people ask you the MOST? (separate with |)",
|
|
155
|
+
qReglas: "Anything the bot should NOT do or say? And when should it hand the chat to you?",
|
|
156
|
+
qTone: "How should it sound?",
|
|
157
|
+
toneFriendly: "Friendly", toneFormal: "Formal", tonePlayful: "Playful",
|
|
158
|
+
tone1: "friendly and warm, like talking to someone you know",
|
|
159
|
+
tone2: "formal and professional, clear and respectful",
|
|
160
|
+
tone3: "relaxed and playful, with spark but still clear",
|
|
161
|
+
configDone: "Config ready · your bot already knows your business",
|
|
162
|
+
deployAsk: "Deploy to YOUR Cloudflare account now?",
|
|
163
|
+
login: "Authenticating Cloudflare (opens browser)…",
|
|
164
|
+
loginOk: "Cloudflare authenticated",
|
|
165
|
+
creatingResources: "Creating resources on YOUR account (D1 + Vectorize + R2)…",
|
|
166
|
+
d1Ok: (id) => `D1 ready (id ${id})`,
|
|
167
|
+
d1Warn: "couldn't detect the D1 id — paste it manually in wrangler.toml",
|
|
168
|
+
vectorOk: "Vectorize ready (or already existed)",
|
|
169
|
+
r2Ok: "R2 ready (or already existed)",
|
|
170
|
+
r2Warn: "R2 isn't enabled on your account — the bot works without it (binding commented)",
|
|
171
|
+
secretsTitle: "Saving secrets to Cloudflare (without showing them)…",
|
|
172
|
+
secretOk: (k) => `secret ${k} saved`,
|
|
173
|
+
installing: "Installing dependencies…",
|
|
174
|
+
migrations: "Applying D1 migrations…",
|
|
175
|
+
deploying: "Deploying the worker…",
|
|
176
|
+
panel: "Admin dashboard:",
|
|
177
|
+
next: "Next: connect your first channel (Telegram ~5 min) from the panel → Connections.",
|
|
178
|
+
updRevalidating: "Checking for a new version…",
|
|
179
|
+
updUpToDate: "You're on the latest version.",
|
|
180
|
+
updDone: (v) => `Updated to v${v} (your config and data were preserved)`,
|
|
181
|
+
updBackup: (p) => `Backed up your previous version to ${p} — in case you need to recover.`,
|
|
182
|
+
updPreserved: "Preserved: your config (member/), your wrangler.toml and your data.",
|
|
183
|
+
updReplaced: "Updated: the bot engine (everything else).",
|
|
184
|
+
doctorTitle: "Kooni diagnosis",
|
|
185
|
+
okTag: "✓", warnTag: "⚠", badTag: "✗",
|
|
186
|
+
ok: "✓", err: "✗",
|
|
187
|
+
noInstallable: "Your license is ready and saved. The Starter bot ships shortly.",
|
|
188
|
+
qEmail: "Your email? (optional, for support)",
|
|
189
|
+
templateMissing: "installer not found in template.",
|
|
190
|
+
deployFailed: "deploy failed. Check the messages above.",
|
|
191
|
+
unknown: "unknown command:",
|
|
192
|
+
helpIntro: "install your AI assistant on Cloudflare",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
let L = "es";
|
|
197
|
+
const t = () => DICT[L];
|
|
198
|
+
const m = (es, en) => (L === "en" ? en : es);
|
|
199
|
+
|
|
200
|
+
// Regiones del bot: idioma del panel + moneda + tz. `L` (arriba) es el idioma de
|
|
201
|
+
// ESTA CLI; `BOT_LANGUAGE`/memberConfig derivan de la región elegida por el usuario.
|
|
202
|
+
const REGIONS = {
|
|
203
|
+
"es-MX": { memberLang: "es", currency: "$", tz: "America/Mexico_City" },
|
|
204
|
+
"es-ES": { memberLang: "es", currency: "€", tz: "Europe/Madrid" },
|
|
205
|
+
"en": { memberLang: "en", currency: "$", tz: "America/New_York" },
|
|
206
|
+
"pt-BR": { memberLang: "pt", currency: "R$", tz: "America/Sao_Paulo" },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function normBotLang(v) {
|
|
210
|
+
const s = String(v || "").toLowerCase().replace("_", "-");
|
|
211
|
+
if (["es-mx", "es", "es-419", "latam", "mexico"].includes(s)) return "es-MX";
|
|
212
|
+
if (["es-es", "espana", "españa", "spain"].includes(s)) return "es-ES";
|
|
213
|
+
if (["en", "english", "us", "usa"].includes(s)) return "en";
|
|
214
|
+
if (s.startsWith("pt") || ["brasil", "brazil"].includes(s)) return "pt-BR";
|
|
215
|
+
return "es-MX";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const BRAINS = {
|
|
219
|
+
claude: { provider: "anthropic", secret: "ANTHROPIC_API_KEY", label: "Claude" },
|
|
220
|
+
chatgpt: { provider: "openai", secret: "OPENAI_API_KEY", label: "ChatGPT" },
|
|
221
|
+
grok: { provider: "xai", secret: "XAI_API_KEY", label: "Grok" },
|
|
222
|
+
gateway: { provider: "openai", secret: "OPENAI_API_KEY", label: "Gateway" },
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// ── splash ──────────────────────────────────────────────────────────────────
|
|
226
|
+
const KOONI_ART = [
|
|
227
|
+
"██╗ ██╗ ██████╗ ██████╗ ███╗ ██╗██╗",
|
|
228
|
+
"██║ ██╔╝██╔═══██╗██╔═══██╗████╗ ██║██║",
|
|
229
|
+
"█████╔╝ ██║ ██║██║ ██║██╔██╗ ██║██║",
|
|
230
|
+
"██╔═██╗ ██║ ██║██║ ██║██║╚██╗██║██║",
|
|
231
|
+
"██║ ██╗╚██████╔╝╚██████╔╝██║ ╚████║██║",
|
|
232
|
+
"╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝",
|
|
233
|
+
];
|
|
234
|
+
const KOONI_GRAD = [37, 43, 49, 48, 42, 36];
|
|
235
|
+
function kooniSplash() {
|
|
236
|
+
if (process.env.NO_COLOR || process.env.KOONI_NO_ART) {
|
|
237
|
+
console.log("\n " + C.b("◇ KOONI") + "\n");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const out = ["", c256(152, " · ˚ ✦ ˖ ✧")];
|
|
241
|
+
KOONI_ART.forEach((l, i) => out.push(" " + c256(KOONI_GRAD[i], l)));
|
|
242
|
+
out.push(c256(36, " ▂▃▄▅▆▇█ tu bot, tu infra █▇▆▅▄▃▂"), "");
|
|
243
|
+
console.log(out.join("\n"));
|
|
244
|
+
}
|
|
245
|
+
function banner() {
|
|
246
|
+
console.log(C.cyan("\n ◇ Kooni") + C.dim(" · " + t().tagline + "\n"));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── config ───────────────────────────────────────────────────────────────────
|
|
250
|
+
function loadCfg() { try { return JSON.parse(readFileSync(CFG_FILE, "utf8")); } catch { return {}; } }
|
|
251
|
+
function saveCfg(o) { mkdirSync(CFG_DIR, { recursive: true }); writeFileSync(CFG_FILE, JSON.stringify(o, null, 2)); }
|
|
252
|
+
|
|
253
|
+
// ── flags / interacción ──────────────────────────────────────────────────────
|
|
254
|
+
function parseFlags(args) {
|
|
255
|
+
const flags = {};
|
|
256
|
+
const rest = [];
|
|
257
|
+
for (let i = 0; i < args.length; i++) {
|
|
258
|
+
const a = args[i];
|
|
259
|
+
if (a.startsWith("--")) {
|
|
260
|
+
const key = a.slice(2);
|
|
261
|
+
const next = args[i + 1];
|
|
262
|
+
if (next === undefined || next.startsWith("--")) flags[key] = true;
|
|
263
|
+
else { flags[key] = next; i++; }
|
|
264
|
+
} else rest.push(a);
|
|
265
|
+
}
|
|
266
|
+
return { flags, rest };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let ASSUME_YES = false;
|
|
270
|
+
const interactive = () => !!(input.isTTY && output.isTTY) && !ASSUME_YES;
|
|
271
|
+
|
|
272
|
+
function agentBriefing(asks, retry) {
|
|
273
|
+
console.log(C.yellow("\n ── PARA EL AGENTE (Claude Code / Codex) ── [E-INPUT-REQUIRED]"));
|
|
274
|
+
console.log(" Falta información. Entrevista al usuario EN ESTE ORDEN — UNA pregunta por mensaje,");
|
|
275
|
+
console.log(" espera su respuesta antes de la siguiente:");
|
|
276
|
+
asks.forEach((a, i) => console.log(` ${i + 1}. ${a}`));
|
|
277
|
+
console.log(" Con sus respuestas, reintenta exactamente así:");
|
|
278
|
+
console.log(" " + C.cyan(retry));
|
|
279
|
+
console.log(C.yellow(" ──────────────────────────────────────────────\n"));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function select(rl, title, items, opts = {}) {
|
|
283
|
+
const def = Math.min(Math.max(opts.default || 0, 0), Math.max(items.length - 1, 0));
|
|
284
|
+
if (!items.length) return def;
|
|
285
|
+
if (opts.value != null && opts.value !== true) {
|
|
286
|
+
const v = String(opts.value).trim().toLowerCase();
|
|
287
|
+
const byKey = items.findIndex((it) => String(it.key || it.label || "").toLowerCase() === v);
|
|
288
|
+
if (byKey >= 0) return byKey;
|
|
289
|
+
const n = parseInt(v, 10);
|
|
290
|
+
if (Number.isInteger(n) && n >= 1 && n <= items.length) return n - 1;
|
|
291
|
+
}
|
|
292
|
+
if (!interactive()) return def;
|
|
293
|
+
let idx = def;
|
|
294
|
+
const hint = opts.hint || (L === "en" ? "↑/↓ move · enter to select" : "↑/↓ para moverte · enter para elegir");
|
|
295
|
+
emitKeypressEvents(input);
|
|
296
|
+
rl.pause();
|
|
297
|
+
const wasRaw = input.isRaw;
|
|
298
|
+
input.setRawMode(true);
|
|
299
|
+
input.resume();
|
|
300
|
+
output.write("\x1b[?25l");
|
|
301
|
+
let count = 0;
|
|
302
|
+
const render = (first) => {
|
|
303
|
+
const lines = [];
|
|
304
|
+
if (title) lines.push(C.b(" " + title));
|
|
305
|
+
items.forEach((it, i) => {
|
|
306
|
+
const on = i === idx;
|
|
307
|
+
const ptr = on ? c256(48, "❯") : " ";
|
|
308
|
+
const lab = on ? c256(48, it.label) : C.dim(it.label);
|
|
309
|
+
lines.push(` ${ptr} ${lab}${it.desc ? C.dim(" " + it.desc) : ""}`);
|
|
310
|
+
});
|
|
311
|
+
lines.push(C.dim(" " + hint));
|
|
312
|
+
if (!first) output.write(`\x1b[${count}A`);
|
|
313
|
+
output.write("\x1b[0J" + lines.join("\n") + "\n");
|
|
314
|
+
count = lines.length;
|
|
315
|
+
};
|
|
316
|
+
render(true);
|
|
317
|
+
return await new Promise((resolve) => {
|
|
318
|
+
const cleanup = () => {
|
|
319
|
+
input.removeListener("keypress", onKey);
|
|
320
|
+
if (!wasRaw) input.setRawMode(false);
|
|
321
|
+
output.write("\x1b[?25h");
|
|
322
|
+
rl.resume();
|
|
323
|
+
};
|
|
324
|
+
const onKey = (str, key) => {
|
|
325
|
+
key = key || {};
|
|
326
|
+
if (key.name === "up" || key.name === "k") { idx = (idx - 1 + items.length) % items.length; render(false); }
|
|
327
|
+
else if (key.name === "down" || key.name === "j" || key.name === "tab") { idx = (idx + 1) % items.length; render(false); }
|
|
328
|
+
else if (str && /^[1-9]$/.test(str) && Number(str) <= items.length) { idx = Number(str) - 1; render(false); }
|
|
329
|
+
else if (key.name === "return" || key.name === "enter") { cleanup(); resolve(idx); }
|
|
330
|
+
else if (key.ctrl && key.name === "c") { cleanup(); console.log(""); process.exit(130); }
|
|
331
|
+
};
|
|
332
|
+
input.on("keypress", onKey);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function ask(rl, q, val) {
|
|
337
|
+
if (val != null && val !== true) return String(val).trim();
|
|
338
|
+
if (!interactive()) return "";
|
|
339
|
+
return (await rl.question("\n " + C.b(q) + "\n " + C.cyan("› "))).trim();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Input oculto (raw mode, sin eco) para API keys / contraseñas.
|
|
343
|
+
async function askSecret(rl, q) {
|
|
344
|
+
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
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const cleanup = () => { input.removeListener("data", onData); input.pause(); if (!prev) input.setRawMode(false); };
|
|
366
|
+
input.on("data", onData);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function confirm(rl, q) {
|
|
371
|
+
if (!interactive()) return true;
|
|
372
|
+
const a = (await rl.question("\n " + C.b(q + " (s/N)") + "\n " + C.cyan("› "))).trim().toLowerCase();
|
|
373
|
+
return ["s", "si", "sí", "y", "yes"].includes(a);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── red / descarga ───────────────────────────────────────────────────────────
|
|
377
|
+
async function fetchTimeout(url, opts = {}, ms = 8000) {
|
|
378
|
+
const ctrl = new AbortController();
|
|
379
|
+
const to = setTimeout(() => ctrl.abort(), ms);
|
|
380
|
+
try { return await fetch(url, { ...opts, signal: ctrl.signal }); } finally { clearTimeout(to); }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function fetchRetry(url, opts = {}, { ms = 15000, tries = 3 } = {}) {
|
|
384
|
+
let lastErr;
|
|
385
|
+
for (let i = 0; i < tries; i++) {
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetchTimeout(url, opts, ms);
|
|
388
|
+
if (res.status < 500) return res;
|
|
389
|
+
lastErr = new Error(`HTTP ${res.status}`);
|
|
390
|
+
} catch (e) { lastErr = e; }
|
|
391
|
+
if (i < tries - 1) await new Promise((r) => setTimeout(r, 300 * (i + 1)));
|
|
392
|
+
}
|
|
393
|
+
throw lastErr || new Error("network");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function downloadTemplate(dest) {
|
|
397
|
+
const res = await fetchRetry(TARBALL, {}, { ms: 25000, tries: 3 });
|
|
398
|
+
if (!res.ok) {
|
|
399
|
+
throw new Error(m(`no pude bajar el template (HTTP ${res.status}). Revisa tu internet o el repo ${REPO}.`, `could not download template (HTTP ${res.status}). Check internet or repo ${REPO}.`));
|
|
400
|
+
}
|
|
401
|
+
writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── extracción (rutas NATIVAS: el tar de Windows no entiende /c/...) ────────
|
|
405
|
+
function extractFresh(tgz, dir) {
|
|
406
|
+
mkdirSync(dir, { recursive: true });
|
|
407
|
+
const tmp = join(dir, ".kooni-extract");
|
|
408
|
+
mkdirSync(tmp, { recursive: true });
|
|
409
|
+
execFileSync("tar", ["-xzf", tgz, "-C", tmp]);
|
|
410
|
+
const root = readdirSync(tmp)[0];
|
|
411
|
+
const src = join(tmp, root);
|
|
412
|
+
for (const e of readdirSync(src)) {
|
|
413
|
+
const to = join(dir, e);
|
|
414
|
+
try { rmSync(to, { recursive: true, force: true }); } catch {}
|
|
415
|
+
cpSync(join(src, e), to, { recursive: true });
|
|
416
|
+
}
|
|
417
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
418
|
+
rmSync(tgz, { force: true });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Extrae a un dir TEMP (sin pisar nada) para leer la versión y archivos del template.
|
|
422
|
+
function extractToTemp(tgz, outDir) {
|
|
423
|
+
mkdirSync(outDir, { recursive: true });
|
|
424
|
+
execFileSync("tar", ["-xzf", tgz, "-C", outDir]);
|
|
425
|
+
const root = readdirSync(outDir)[0];
|
|
426
|
+
return join(outDir, root);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const EXCLUDE_OVER = new Set([
|
|
430
|
+
"member", "wrangler.toml", ".dev.vars", ".dev.vars.example", ".dev.vars.local",
|
|
431
|
+
".env", ".env.example", ".bot-state.json", ".bot-setup.json", ".git",
|
|
432
|
+
"node_modules", ".wrangler", ".kooni-extract", ".kooni-backups", MARKER, ".bot-version",
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
function extractOver(tgz, dir) {
|
|
436
|
+
const tmp = join(dir, ".kooni-extract");
|
|
437
|
+
mkdirSync(tmp, { recursive: true });
|
|
438
|
+
const src = extractToTemp(tgz, tmp);
|
|
439
|
+
for (const e of readdirSync(src)) {
|
|
440
|
+
if (EXCLUDE_OVER.has(e)) continue;
|
|
441
|
+
const to = join(dir, e);
|
|
442
|
+
try { rmSync(to, { recursive: true, force: true }); } catch {}
|
|
443
|
+
cpSync(join(src, e), to, { recursive: true });
|
|
444
|
+
}
|
|
445
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
446
|
+
rmSync(tgz, { force: true });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function backupBeforeUpdate(dir, version) {
|
|
450
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
451
|
+
const backDir = join(dir, ".kooni-backups");
|
|
452
|
+
const dest = join(backDir, `${stamp}_v${version}.tgz`);
|
|
453
|
+
try {
|
|
454
|
+
mkdirSync(backDir, { recursive: true });
|
|
455
|
+
execFileSync("tar", ["-czf", dest, "-C", dir,
|
|
456
|
+
"--exclude=./node_modules", "--exclude=./.kooni-backups", "--exclude=./.kooni-extract",
|
|
457
|
+
"--exclude=./.git", "--exclude=./.wrangler", "--exclude=./.dev.vars", "--exclude=./.dev.vars.*",
|
|
458
|
+
"--exclude=./.env", "--exclude=./.env.*", "."]);
|
|
459
|
+
const olds = readdirSync(backDir).filter((f) => f.endsWith(".tgz")).sort();
|
|
460
|
+
for (const f of olds.slice(0, -5)) rmSync(join(backDir, f), { force: true });
|
|
461
|
+
return dest;
|
|
462
|
+
} catch { return null; }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── markers / detección ──────────────────────────────────────────────────────
|
|
466
|
+
function writeMarker(dir, { slug, version, lang }) {
|
|
467
|
+
writeFileSync(join(dir, MARKER), JSON.stringify({
|
|
468
|
+
slug, version, lang, updatedAt: new Date().toISOString(),
|
|
469
|
+
}, null, 2));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function readMarker(dir) {
|
|
473
|
+
try { return JSON.parse(readFileSync(join(dir, MARKER), "utf8")); } catch { return null; }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function readPkgVersion(dir) {
|
|
477
|
+
try {
|
|
478
|
+
const p = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
479
|
+
return p.version || null;
|
|
480
|
+
} catch { return null; }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function isKooni(dir) {
|
|
484
|
+
return existsSync(join(dir, "package.json")) && (existsSync(join(dir, "member")) || existsSync(join(dir, "src", "index.ts")));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function resolveBotDir(arg) {
|
|
488
|
+
if (arg && isKooni(arg)) return arg;
|
|
489
|
+
if (isKooni(process.cwd())) return process.cwd();
|
|
490
|
+
for (const e of readdirSync(process.cwd())) {
|
|
491
|
+
try {
|
|
492
|
+
const p = join(process.cwd(), e);
|
|
493
|
+
if (statSync(p).isDirectory() && isKooni(p)) return p;
|
|
494
|
+
} catch {}
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ── exec cross-platform ──────────────────────────────────────────────────────
|
|
500
|
+
const isWin = process.platform === "win32";
|
|
501
|
+
|
|
502
|
+
// Ejecuta `npx wrangler ...` respetando el shim .cmd en Windows.
|
|
503
|
+
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
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Ejecuta `pnpm ...`, habilitando corepack si no existe pnpm.
|
|
515
|
+
function runPnpm(dir, args, opts = {}) {
|
|
516
|
+
const pnpm = process.env.KOONI_PNPM || "pnpm";
|
|
517
|
+
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
|
+
});
|
|
525
|
+
} 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
|
+
});
|
|
536
|
+
}
|
|
537
|
+
throw e;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── config: escribir wrangler.toml / member/config.local.ts / .dev.vars ──────
|
|
542
|
+
function sanitizeSlug(s) {
|
|
543
|
+
return String(s || "mi-negocio").toLowerCase().replace(/ /g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "mi-negocio";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function stampWrangler(dir, answers) {
|
|
547
|
+
const wt = join(dir, "wrangler.toml");
|
|
548
|
+
const example = join(dir, "wrangler.toml.example");
|
|
549
|
+
// El template distribuye wrangler.toml.example; init genera el wrangler.toml real.
|
|
550
|
+
if (!existsSync(wt) && existsSync(example)) {
|
|
551
|
+
cpSync(example, wt);
|
|
552
|
+
}
|
|
553
|
+
if (!existsSync(wt)) return null;
|
|
554
|
+
let s = readFileSync(wt, "utf8");
|
|
555
|
+
const slug = answers.slug;
|
|
556
|
+
const resId = slug.replace(/-/g, "_");
|
|
557
|
+
const dbName = `kooni_${resId}_db`;
|
|
558
|
+
const kbName = `kooni_${resId}_kb`;
|
|
559
|
+
const R = REGIONS[answers.lang] || REGIONS["es-MX"];
|
|
560
|
+
|
|
561
|
+
const set = (re, val) => { s = s.replace(re, val); };
|
|
562
|
+
|
|
563
|
+
set(/^name\s*=\s*"[^"]*"/m, `name = "kooni-bot-${slug}"`);
|
|
564
|
+
set(/BOT_NAME\s*=\s*"[^"]*"/g, `BOT_NAME = "${String(answers.botName).replace(/"/g, "'")}"`);
|
|
565
|
+
set(/BUSINESS_NAME\s*=\s*"[^"]*"/g, `BUSINESS_NAME = "${String(answers.businessName).replace(/"/g, "'")}"`);
|
|
566
|
+
set(/BOT_LANGUAGE\s*=\s*"[^"]*"/g, `BOT_LANGUAGE = "${answers.lang}"`);
|
|
567
|
+
set(/BOT_TIER\s*=\s*"[^"]*"/g, `BOT_TIER = "${answers.tier}"`);
|
|
568
|
+
set(/BUFFER_SECONDS\s*=\s*"[^"]*"/g, `BUFFER_SECONDS = "${answers.bufferSeconds || "15"}"`);
|
|
569
|
+
set(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = ""`);
|
|
570
|
+
// D1 / Vectorize namespaced por bot.
|
|
571
|
+
set(/database_name\s*=\s*"[^"]*"/, `database_name = "${dbName}"`);
|
|
572
|
+
set(/index_name\s*=\s*"[^"]*"/, `index_name = "${kbName}"`);
|
|
573
|
+
// El id del demo no sirve: placeholder hasta que `deploy` lo reemplace.
|
|
574
|
+
set(/database_id\s*=\s*"[^"]*"[^\n]*/, `database_id = "{{D1_DATABASE_ID}}" # pon aquí el id real de: npx wrangler d1 create ${dbName}`);
|
|
575
|
+
|
|
576
|
+
// LLM_PROVIDER: en anthropic se omite (es default). Gateway/base-url se agregan.
|
|
577
|
+
// Trabajamos SOLO sobre la sección [vars] (entre "[vars]" y la siguiente "[…]").
|
|
578
|
+
const varsMatch = s.match(/\n\s*\[vars\][\s\S]*?(?=\n\s*\[[^\]]|\n\s*$)/);
|
|
579
|
+
const hasInVars = (key) => {
|
|
580
|
+
if (!varsMatch) return false;
|
|
581
|
+
return new RegExp(`^\\s*${key}\\s*=`, "m").test(varsMatch[0]);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
if (answers.provider !== "anthropic") {
|
|
585
|
+
if (hasInVars("LLM_PROVIDER")) {
|
|
586
|
+
set(/LLM_PROVIDER\s*=\s*"[^"]*"/g, `LLM_PROVIDER = "${answers.provider}"`);
|
|
587
|
+
} else {
|
|
588
|
+
s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1LLM_PROVIDER = "${answers.provider}"\n`);
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
s = s.replace(/^(\s*LLM_PROVIDER\s*=\s*"[^"]*"[^\n]*\n)/gm, "");
|
|
592
|
+
}
|
|
593
|
+
if (answers.baseUrl) {
|
|
594
|
+
if (hasInVars("OPENAI_API_BASE_URL")) {
|
|
595
|
+
set(/OPENAI_API_BASE_URL\s*=\s*"[^"]*"/g, `OPENAI_API_BASE_URL = "${answers.baseUrl}"`);
|
|
596
|
+
} else {
|
|
597
|
+
s = s.replace(/^(\s*\[vars\][^\n]*\n)/m, `$1OPENAI_API_BASE_URL = "${answers.baseUrl}"\n`);
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
s = s.replace(/^(\s*OPENAI_API_BASE_URL\s*=\s*"[^"]*"[^\n]*\n)/gm, "");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
writeFileSync(wt, s);
|
|
604
|
+
return { dbName, kbName, tz: R.tz, currency: R.currency };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function renderMemberConfig(answers, meta) {
|
|
608
|
+
const j = (v) => JSON.stringify(v ?? "");
|
|
609
|
+
const services = answers.offer
|
|
610
|
+
? [{ name: answers.offer, price: 0 }]
|
|
611
|
+
: [];
|
|
612
|
+
const paymentMethods = (answers.pagos || "").split(/[,;·]+/).map((x) => x.trim()).filter(Boolean);
|
|
613
|
+
const faqList = (answers.faq || "").split(/\|/).map((x) => x.trim()).filter(Boolean);
|
|
614
|
+
const customFields = {};
|
|
615
|
+
if (answers.what) customFields.queHacemos = answers.what;
|
|
616
|
+
if (answers.offer) customFields.ofrecemos = answers.offer;
|
|
617
|
+
if (faqList.length) customFields.preguntasFrecuentes = faqList.join(" | ");
|
|
618
|
+
if (answers.reglas) customFields.reglasYEscalacion = answers.reglas;
|
|
619
|
+
if (answers.web) customFields.sitioWebYRedes = answers.web;
|
|
620
|
+
if (answers.tone) customFields.tono = answers.tone;
|
|
621
|
+
|
|
622
|
+
const memberConfig = {
|
|
623
|
+
businessName: answers.businessName || "",
|
|
624
|
+
botName: answers.botName || "Asistente",
|
|
625
|
+
language: meta.memberLang,
|
|
626
|
+
tier: answers.tier === "pro" ? "pro" : "free",
|
|
627
|
+
timezone: meta.tz,
|
|
628
|
+
currency: meta.currency,
|
|
629
|
+
contactEmail: answers.email || "",
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const businessConfig = {
|
|
633
|
+
hours: answers.hours || "",
|
|
634
|
+
services,
|
|
635
|
+
location: answers.location || "",
|
|
636
|
+
paymentMethods,
|
|
637
|
+
contactPhone: answers.phone || "",
|
|
638
|
+
customFields,
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
return `// member/config.local.ts — config del negocio (generado por kooni-bot init)
|
|
642
|
+
// NUNCA se sobrescribe en updates. Edita aquí o desde el panel → Configuración.
|
|
643
|
+
|
|
644
|
+
export const memberConfig = ${JSON.stringify(memberConfig)};
|
|
645
|
+
|
|
646
|
+
export type MemberConfig = typeof memberConfig;
|
|
647
|
+
|
|
648
|
+
export const businessConfig = ${JSON.stringify(businessConfig)} as {
|
|
649
|
+
hours: string;
|
|
650
|
+
services: { name: string; price: number }[];
|
|
651
|
+
location: string;
|
|
652
|
+
paymentMethods: string[];
|
|
653
|
+
contactPhone: string;
|
|
654
|
+
customFields: Record<string, string>;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
export const catalog: { name: string; price: number; description?: string; sku?: string }[] = [];
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function writeDevVars(dir, answers, kbToken) {
|
|
662
|
+
const lines = [
|
|
663
|
+
"# KOONI — secrets locales (generados por kooni-bot init · NUNCA commitees)",
|
|
664
|
+
`LLM_PROVIDER=${answers.provider}`,
|
|
665
|
+
];
|
|
666
|
+
if (answers.baseUrl) lines.push(`OPENAI_API_BASE_URL=${answers.baseUrl}`);
|
|
667
|
+
// La API key NO se escribe en disco por defecto: se guarda como secret remoto.
|
|
668
|
+
lines.push("# " + answers.secret + "=<tu-api-key> ← para wrangler dev local, pégalo aquí (o usa el panel)");
|
|
669
|
+
lines.push(`DASHBOARD_PASSWORD=${answers.dashPassword || "kooni-local-password"}`);
|
|
670
|
+
lines.push(`KB_REINDEX_TOKEN=${kbToken}`);
|
|
671
|
+
writeFileSync(join(dir, ".dev.vars"), lines.join("\n") + "\n");
|
|
672
|
+
}
|
|
673
|
+
|
|
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
|
+
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,
|
|
685
|
+
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(),
|
|
697
|
+
tone,
|
|
698
|
+
email: String(flags.email || "").trim(),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Pregunta lo que falte, uno a uno. En no-interactivo sin flag, devuelve default.
|
|
703
|
+
async function onboarding(rl, answers) {
|
|
704
|
+
console.log("\n " + C.dim(t().prep));
|
|
705
|
+
|
|
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;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const langKeys = Object.keys(REGIONS);
|
|
714
|
+
const langIdx = await select(rl, t().qLang, langKeys.map((k) => ({ key: k, label: k })), { value: answers.lang, default: 0 });
|
|
715
|
+
answers.lang = langKeys[langIdx] || "es-MX";
|
|
716
|
+
|
|
717
|
+
const tierIdx = await select(rl, t().qTier, [
|
|
718
|
+
{ key: "free", label: "free", desc: m("Starter", "Starter") },
|
|
719
|
+
{ key: "pro", label: "pro", desc: m("panel completo + tools avanzadas", "full panel + advanced tools") },
|
|
720
|
+
], { value: answers.tier, default: 0 });
|
|
721
|
+
answers.tier = tierIdx === 1 ? "pro" : "free";
|
|
722
|
+
|
|
723
|
+
const brainKeys = ["claude", "chatgpt", "grok", "gateway"];
|
|
724
|
+
const brainIdx = await select(rl, t().brainQ, brainKeys.map((k) => ({
|
|
725
|
+
key: k, label: BRAINS[k].label,
|
|
726
|
+
desc: k === "claude" ? t().brainDesc : "",
|
|
727
|
+
})), { value: answers.brainKey, default: 0 });
|
|
728
|
+
answers.brainKey = brainKeys[brainIdx] || "claude";
|
|
729
|
+
answers.provider = BRAINS[answers.brainKey].provider;
|
|
730
|
+
answers.secret = BRAINS[answers.brainKey].secret;
|
|
731
|
+
if (answers.brainKey === "gateway" && !answers.baseUrl) {
|
|
732
|
+
answers.baseUrl = await ask(rl, t().qBaseUrl, null) || "https://api.aisa.one/v1";
|
|
733
|
+
}
|
|
734
|
+
|
|
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
|
+
|
|
745
|
+
const toneIdx = await select(rl, t().qTone, [
|
|
746
|
+
{ key: "cercano", label: t().toneFriendly, desc: t().tone1 },
|
|
747
|
+
{ key: "formal", label: t().toneFormal, desc: t().tone2 },
|
|
748
|
+
{ key: "divertido", label: t().tonePlayful, desc: t().tone3 },
|
|
749
|
+
], { value: answers.tone || "cercano", default: 0 });
|
|
750
|
+
answers.tone = ["cercano", "formal", "divertido"][toneIdx] || "cercano";
|
|
751
|
+
|
|
752
|
+
return answers;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ── deploy ───────────────────────────────────────────────────────────────────
|
|
756
|
+
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}/);
|
|
758
|
+
return m ? m[0] : "";
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function patchWranglerFile(dir, fn) {
|
|
762
|
+
const wt = join(dir, "wrangler.toml");
|
|
763
|
+
if (!existsSync(wt)) return;
|
|
764
|
+
const s = readFileSync(wt, "utf8");
|
|
765
|
+
writeFileSync(wt, fn(s));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function deployBot(dir, { flags = {}, rl } = {}) {
|
|
769
|
+
const wt = join(dir, "wrangler.toml");
|
|
770
|
+
if (!existsSync(wt)) throw new Error(m("no encuentro wrangler.toml en " + dir, "can't find wrangler.toml in " + dir));
|
|
771
|
+
|
|
772
|
+
const wantDeploy = await confirm(rl, t().deployAsk);
|
|
773
|
+
if (!wantDeploy) { console.log(C.dim(" " + m("puedes hacerlo luego con: npx kooni-bot deploy", "you can deploy later with: npx kooni-bot deploy"))); return null; }
|
|
774
|
+
|
|
775
|
+
// login
|
|
776
|
+
process.stdout.write(C.dim(" " + t().login + "\n"));
|
|
777
|
+
wrangler(dir, ["login"], { stdio: "inherit" });
|
|
778
|
+
console.log(" " + C.green("✓") + " " + t().loginOk);
|
|
779
|
+
|
|
780
|
+
// recursos
|
|
781
|
+
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";
|
|
784
|
+
|
|
785
|
+
let d1Id = "";
|
|
786
|
+
try {
|
|
787
|
+
const out = wrangler(dir, ["d1", "create", dbName]);
|
|
788
|
+
d1Id = parseD1Id(out);
|
|
789
|
+
} catch {}
|
|
790
|
+
if (!d1Id) {
|
|
791
|
+
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] : "";
|
|
795
|
+
} catch {}
|
|
796
|
+
}
|
|
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);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try { wrangler(dir, ["vectorize", "create", kbName, "--dimensions=1024", "--metric=cosine"]); } catch {}
|
|
805
|
+
console.log(" " + C.green("✓") + " " + t().vectorOk);
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
wrangler(dir, ["r2", "bucket", "create", "kooni-bot-catalog"]);
|
|
809
|
+
console.log(" " + C.green("✓") + " " + t().r2Ok);
|
|
810
|
+
} catch {
|
|
811
|
+
console.log(" " + C.yellow("⚠") + " " + t().r2Warn);
|
|
812
|
+
patchWranglerFile(dir, (s) => s
|
|
813
|
+
.replace(/\[\[r2_buckets\]\]/, "# [[r2_buckets]] (R2 no habilitado — opcional)")
|
|
814
|
+
.replace(/^binding\s*=\s*"CATALOG"/m, '# binding = "CATALOG"')
|
|
815
|
+
.replace(/^bucket_name\s*=\s*"[^"]*"/m, '# bucket_name = "kooni-bot-catalog"'));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// secrets (sin mostrarlos)
|
|
819
|
+
console.log("\n " + C.dim(t().secretsTitle));
|
|
820
|
+
|
|
821
|
+
// API key: interactiva por defecto; en no-interactivo se delega al agente.
|
|
822
|
+
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
|
+
if (apiKey) {
|
|
827
|
+
wrangler(dir, ["secret", "put", BRAINS[flags.brainKey || "claude"].secret], { stdio: ["pipe", "pipe", "pipe"], extra: { input: apiKey } });
|
|
828
|
+
console.log(" " + C.green("✓") + " " + t().secretOk(BRAINS[flags.brainKey || "claude"].secret));
|
|
829
|
+
} else if (!interactive()) {
|
|
830
|
+
console.log(C.yellow("\n ── PARA EL AGENTE ── falta la API key de IA."));
|
|
831
|
+
console.log(" " + C.cyan(`npx wrangler secret put ${BRAINS[flags.brainKey || "claude"].secret}`));
|
|
832
|
+
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)."));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
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):"));
|
|
837
|
+
if (!dash) dash = "kooni-local-password";
|
|
838
|
+
wrangler(dir, ["secret", "put", "DASHBOARD_PASSWORD"], { stdio: ["pipe", "pipe", "pipe"], extra: { input: dash } });
|
|
839
|
+
console.log(" " + C.green("✓") + " " + t().secretOk("DASHBOARD_PASSWORD"));
|
|
840
|
+
|
|
841
|
+
// KB token desde .dev.vars
|
|
842
|
+
try {
|
|
843
|
+
const dv = readFileSync(join(dir, ".dev.vars"), "utf8");
|
|
844
|
+
const m = dv.match(/^KB_REINDEX_TOKEN=(.+)$/m);
|
|
845
|
+
if (m && m[1]) {
|
|
846
|
+
wrangler(dir, ["secret", "put", "KB_REINDEX_TOKEN"], { stdio: ["pipe", "pipe", "pipe"], extra: { input: m[1] } });
|
|
847
|
+
console.log(" " + C.green("✓") + " " + t().secretOk("KB_REINDEX_TOKEN"));
|
|
848
|
+
}
|
|
849
|
+
} catch {}
|
|
850
|
+
|
|
851
|
+
// dependencias + migraciones + deploy
|
|
852
|
+
console.log("\n " + C.dim(t().installing));
|
|
853
|
+
runPnpm(dir, ["install"]);
|
|
854
|
+
console.log(" " + C.green("✓") + " " + m("dependencias listas", "dependencies ready"));
|
|
855
|
+
|
|
856
|
+
console.log(" " + C.dim(t().migrations));
|
|
857
|
+
wrangler(dir, ["d1", "execute", dbName, "--file=src/db/schema.sql", "--remote"]);
|
|
858
|
+
console.log(" " + C.green("✓") + " " + m("migraciones aplicadas", "migrations applied"));
|
|
859
|
+
|
|
860
|
+
console.log(" " + C.dim(t().deploying));
|
|
861
|
+
let dep = wrangler(dir, ["deploy"]);
|
|
862
|
+
let url = (dep.match(/https:\/\/[a-z0-9-]+\.workers\.dev/) || [])[0] || "";
|
|
863
|
+
if (url) {
|
|
864
|
+
patchWranglerFile(dir, (s) => s.replace(/DASHBOARD_BASE_URL\s*=\s*"[^"]*"/g, `DASHBOARD_BASE_URL = "${url}"`));
|
|
865
|
+
wrangler(dir, ["deploy"]);
|
|
866
|
+
}
|
|
867
|
+
if (!url) {
|
|
868
|
+
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
|
+
}
|
|
870
|
+
return url;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ── check-in (no bloqueante) ─────────────────────────────────────────────────
|
|
874
|
+
async function checkin(dir, answers, version) {
|
|
875
|
+
if (process.env.KOONI_NO_CHECKIN === "1" || process.env.KOONI_SILENT === "1") return;
|
|
876
|
+
try {
|
|
877
|
+
const slug = answers.slug || basename(dir);
|
|
878
|
+
await fetchTimeout(CHECKIN_URL, {
|
|
879
|
+
method: "POST",
|
|
880
|
+
headers: { "Content-Type": "application/json" },
|
|
881
|
+
body: JSON.stringify({
|
|
882
|
+
email: answers.email || undefined,
|
|
883
|
+
slug,
|
|
884
|
+
workerUrl: `https://kooni-bot-${slug}.workers.dev`,
|
|
885
|
+
cliVersion: CLI_VERSION,
|
|
886
|
+
botVersion: version,
|
|
887
|
+
tier: answers.tier,
|
|
888
|
+
provider: answers.provider,
|
|
889
|
+
platform: process.platform,
|
|
890
|
+
}),
|
|
891
|
+
}, 4000);
|
|
892
|
+
} catch { /* fire-and-forget */ }
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ── skill de agente ─────────────────────────────────────────────────────────
|
|
896
|
+
const AGENT_SKILL = `---
|
|
897
|
+
name: kooni
|
|
898
|
+
description: Guía para operar Kooni con el CLI \`kooni-bot\` — instalar, configurar, desplegar y mantener chatbots de IA open source (MIT) en la Cloudflare del usuario. Actívala cuando el usuario quiera "instalar Kooni", "crear/montar un chatbot", "actualizar mi bot", "diagnosticar mi bot", "configurar mi bot", o mencione kooni-bot, kooni o Kooni.
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
# Kooni — instalar y operar chatbots con el CLI \`kooni-bot\`
|
|
902
|
+
|
|
903
|
+
Eres el asistente que maneja Kooni POR el usuario. La persona probablemente **no programa**
|
|
904
|
+
y casi nunca verá la terminal: **tú corres los comandos y tú haces las preguntas en el chat**.
|
|
905
|
+
REGLA DE ORO: **una pregunta por mensaje** — espera la respuesta antes de la siguiente.
|
|
906
|
+
|
|
907
|
+
## Qué es Kooni
|
|
908
|
+
Un asistente de IA multicanal (WhatsApp, Instagram, Messenger, Telegram…) **open source
|
|
909
|
+
(MIT)**. El CLI \`kooni-bot\` instala el bot en la **cuenta de Cloudflare del usuario**, con
|
|
910
|
+
**sus llaves**. El bot y sus datos son del usuario. Tú NO eres el chatbot: tú eres el
|
|
911
|
+
constructor. No hay licencia ni servidor de Horizontes: el tier free/pro se controla con
|
|
912
|
+
\`BOT_TIER\` en \`wrangler.toml\` y un código local en el panel.
|
|
913
|
+
|
|
914
|
+
## Comandos del CLI
|
|
915
|
+
- \`npx kooni-bot init [dir]\` — descarga el template, configura (idioma, negocio, cerebro) y ofrece desplegar.
|
|
916
|
+
- \`npx kooni-bot deploy [dir]\` — provisiona Cloudflare (login, D1/Vectorize/R2, secrets, migraciones, deploy).
|
|
917
|
+
- \`npx kooni-bot update [dir]\` — trae la versión nueva conservando \`member/\`, \`wrangler.toml\` y datos.
|
|
918
|
+
- \`npx kooni-bot doctor [dir]\` — diagnostica el bot instalado.
|
|
919
|
+
- \`npx kooni-bot version\`.
|
|
920
|
+
|
|
921
|
+
## Regla de oro (memorízala)
|
|
922
|
+
| Carpeta / archivo | Qué pasa al actualizar |
|
|
923
|
+
|---|---|
|
|
924
|
+
| \`member/\` (config, KB) | **SE CONSERVA SIEMPRE.** |
|
|
925
|
+
| \`src/\` (motor del bot) | Se sobrescribe con la versión nueva. |
|
|
926
|
+
| Secrets de Cloudflare | **No se tocan.** |
|
|
927
|
+
| Datos en D1 | **No se borran.** |
|
|
928
|
+
| \`wrangler.toml\` | **Se conserva** en update (lo estampó init). |
|
|
929
|
+
|
|
930
|
+
Si dudas: **member/ es sagrado, src/ se actualiza.**
|
|
931
|
+
|
|
932
|
+
## Instalación de cero (resumen)
|
|
933
|
+
1. \`npx kooni-bot init\` (o \`init --yes --slug <slug> --negocio "…" --cerebro claude\` para agentes/CI).
|
|
934
|
+
2. \`npx kooni-bot deploy\` (login de Cloudflare, D1/Vectorize/R2, secrets, migraciones, deploy).
|
|
935
|
+
3. Abrir el panel en \`https://<worker>.workers.dev/admin\` (usuario \`admin\` + \`DASHBOARD_PASSWORD\`).
|
|
936
|
+
4. Conectar canales DESPUÉS del primer deploy (Telegram primero, ~5 min) desde \`/admin/conexiones\`.
|
|
937
|
+
|
|
938
|
+
## Secrets y vars (referencia rápida)
|
|
939
|
+
- Secrets: \`ANTHROPIC_API_KEY\` / \`OPENAI_API_KEY\` / \`XAI_API_KEY\` (cerebro), \`DASHBOARD_PASSWORD\` (panel), \`KB_REINDEX_TOKEN\` (reindex).
|
|
940
|
+
- Canales: \`TELEGRAM_BOT_TOKEN\`, \`MANYCHAT_API_KEY\`, \`TWILIO_ACCOUNT_SID\`+\`TWILIO_AUTH_TOKEN\`+\`TWILIO_WA_FROM\`, \`META_PAGE_ACCESS_TOKEN\`+\`META_VERIFY_TOKEN\`+\`META_APP_SECRET\`, \`ZERNIO_API_KEY\`.
|
|
941
|
+
- Avisos al dueño: \`OWNER_TELEGRAM_CHAT_ID\`, \`RESEND_API_KEY\`+\`OWNER_EMAIL\`, \`OWNER_WA_NUMBER\`.
|
|
942
|
+
- Vars en \`wrangler.toml\`: \`BOT_NAME\`, \`BUSINESS_NAME\`, \`BOT_LANGUAGE\`, \`BOT_TIER\`, \`BUFFER_SECONDS\`, \`DASHBOARD_BASE_URL\`, \`LLM_PROVIDER\` (\`anthropic\` default | \`openai\` | \`xai\`).
|
|
943
|
+
|
|
944
|
+
## Comandos del proyecto (dentro de la carpeta del bot, con pnpm)
|
|
945
|
+
- \`pnpm install\` — dependencias.
|
|
946
|
+
- \`pnpm run deploy\` — desplegar (corre el predeploy check).
|
|
947
|
+
- \`pnpm db:apply:remote\` — migraciones D1 en producción.
|
|
948
|
+
- \`pnpm kb:reindex\` — regenera fixtures desde \`member/kb/\`.
|
|
949
|
+
- \`pnpm typecheck\` / \`pnpm test\` — verificación.
|
|
950
|
+
|
|
951
|
+
## Reglas
|
|
952
|
+
- Habla español sencillo (LATAM), una pregunta a la vez.
|
|
953
|
+
- **Nunca pegues tokens/keys en el chat** — siempre \`wrangler secret put\`.
|
|
954
|
+
- No toques \`member/\` más allá de lo que indican los pasos.
|
|
955
|
+
- El panel no tiene login por email: solo Basic Auth (\`admin\` + \`DASHBOARD_PASSWORD\`).
|
|
956
|
+
|
|
957
|
+
## Skills del template (una vez instalado el bot)
|
|
958
|
+
- \`skill/configurar-mi-chatbot.md\` — instalación guiada en 4 fases.
|
|
959
|
+
- \`skill/actualizar-mi-bot.md\` — actualización paso a paso.
|
|
960
|
+
- \`skill/reporte.md\` / \`skill/exportar.md\` — operación diaria.
|
|
961
|
+
`;
|
|
962
|
+
|
|
963
|
+
function installAgentSkill(flags = {}) {
|
|
964
|
+
if (flags["no-agent-skill"] || process.env.KOONI_NO_AGENT_SKILL) return;
|
|
965
|
+
try {
|
|
966
|
+
const file = join(SKILL_DIR, "SKILL.md");
|
|
967
|
+
let prev = null;
|
|
968
|
+
try { prev = readFileSync(file, "utf8"); } catch {}
|
|
969
|
+
if (prev === AGENT_SKILL) return;
|
|
970
|
+
mkdirSync(SKILL_DIR, { recursive: true });
|
|
971
|
+
writeFileSync(file, AGENT_SKILL);
|
|
972
|
+
console.log(C.dim(prev == null
|
|
973
|
+
? " ✎ guía de Kooni instalada para tu agente → ~/.claude/skills/kooni/"
|
|
974
|
+
: " ✎ guía de tu agente actualizada"));
|
|
975
|
+
} catch { /* no romper el flujo por esto */ }
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ── comandos ─────────────────────────────────────────────────────────────────
|
|
979
|
+
async function cmdInit(flags, rest) {
|
|
980
|
+
const cfg = loadCfg();
|
|
981
|
+
ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
|
|
982
|
+
if (flags.lang === "en" || cfg.lang === "en") L = "en";
|
|
983
|
+
|
|
984
|
+
kooniSplash();
|
|
985
|
+
installAgentSkill(flags);
|
|
986
|
+
|
|
987
|
+
const rl = createInterface({ input, output });
|
|
988
|
+
try {
|
|
989
|
+
const langKeys = ["es", "en"];
|
|
990
|
+
if (!cfg.lang) {
|
|
991
|
+
const i = await select(rl, t().chooseLang, langKeys.map((k) => ({ key: k, label: k === "es" ? DICT.es.optEs : DICT.en.optEn })));
|
|
992
|
+
L = langKeys[i] || "es";
|
|
993
|
+
cfg.lang = L; saveCfg(cfg);
|
|
994
|
+
} else {
|
|
995
|
+
console.log("");
|
|
996
|
+
}
|
|
997
|
+
console.log(C.dim(" " + t().tagline + "\n"));
|
|
998
|
+
|
|
999
|
+
// directorio destino
|
|
1000
|
+
let dir = rest[0] ? join(process.cwd(), rest[0]) : process.cwd();
|
|
1001
|
+
if (!rest[0]) {
|
|
1002
|
+
const ans = await ask(rl, t().whichDir, flags.dir);
|
|
1003
|
+
if (ans) dir = join(process.cwd(), ans);
|
|
1004
|
+
}
|
|
1005
|
+
mkdirSync(dir, { recursive: true });
|
|
1006
|
+
if (!isKooni(dir) && existsSync(join(dir, "package.json"))) {
|
|
1007
|
+
console.log(C.red(" ✗ " + t().existingProject) + "\n");
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// descargar + extraer
|
|
1012
|
+
const tgz = join(dir, ".kooni-template.tgz");
|
|
1013
|
+
process.stdout.write(C.dim(" " + t().download + "\n"));
|
|
1014
|
+
await downloadTemplate(tgz);
|
|
1015
|
+
extractFresh(tgz, dir);
|
|
1016
|
+
console.log(" " + C.green("✓") + " " + t().templateOk);
|
|
1017
|
+
const version = readPkgVersion(dir) || "0.0.0";
|
|
1018
|
+
|
|
1019
|
+
// config
|
|
1020
|
+
console.log("\n " + C.cyan("◇ ") + C.b(t().runInit));
|
|
1021
|
+
const answers = collectAnswers(flags, rl);
|
|
1022
|
+
await onboarding(rl, answers);
|
|
1023
|
+
|
|
1024
|
+
const meta = stampWrangler(dir, answers);
|
|
1025
|
+
const kbToken = "kooni-reindex-" + randomUUID().replace(/-/g, "").slice(0, 12);
|
|
1026
|
+
if (existsSync(join(dir, "member"))) {
|
|
1027
|
+
const region = REGIONS[answers.lang] || REGIONS["es-MX"];
|
|
1028
|
+
writeFileSync(join(dir, "member", "config.local.ts"), renderMemberConfig(answers, {
|
|
1029
|
+
memberLang: region.memberLang,
|
|
1030
|
+
tz: (meta && meta.tz) || region.tz,
|
|
1031
|
+
currency: (meta && meta.currency) || region.currency,
|
|
1032
|
+
}));
|
|
1033
|
+
}
|
|
1034
|
+
writeDevVars(dir, answers, kbToken);
|
|
1035
|
+
writeMarker(dir, { slug: answers.slug, version, lang: L });
|
|
1036
|
+
console.log("\n " + C.green("✓") + " " + t().configDone);
|
|
1037
|
+
|
|
1038
|
+
// deploy (si no lo deshabilitan)
|
|
1039
|
+
if (!flags["no-deploy"] && !process.env.KOONI_NO_DEPLOY) {
|
|
1040
|
+
const deployFlags = { ...flags, brainKey: answers.brainKey };
|
|
1041
|
+
const url = await deployBot(dir, { flags: deployFlags, rl });
|
|
1042
|
+
if (url) {
|
|
1043
|
+
console.log("\n " + C.green(C.b(m("🎉 BOT EN LÍNEA", "🎉 BOT LIVE"))));
|
|
1044
|
+
console.log(" " + C.cyan(t().panel) + " " + C.b(url + "/admin"));
|
|
1045
|
+
console.log(" " + C.dim(t().next) + "\n");
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
await checkin(dir, answers, version);
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
console.log("\n " + C.red("✗ " + (e.message || e)) + "\n");
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
} finally {
|
|
1054
|
+
rl.close();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function cmdDeploy(flags, rest) {
|
|
1059
|
+
const cfg = loadCfg();
|
|
1060
|
+
ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
|
|
1061
|
+
if (flags.lang === "en" || cfg.lang === "en") L = "en";
|
|
1062
|
+
banner();
|
|
1063
|
+
installAgentSkill(flags);
|
|
1064
|
+
|
|
1065
|
+
const dir = resolveBotDir(rest[0]);
|
|
1066
|
+
if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
|
|
1067
|
+
|
|
1068
|
+
const rl = createInterface({ input, output });
|
|
1069
|
+
try {
|
|
1070
|
+
// provider para elegir el secret correcto
|
|
1071
|
+
let brainKey = "claude";
|
|
1072
|
+
try {
|
|
1073
|
+
const wt = readFileSync(join(dir, "wrangler.toml"), "utf8");
|
|
1074
|
+
const p = (wt.match(/LLM_PROVIDER\s*=\s*"([^"]+)"/) || [])[1] || "anthropic";
|
|
1075
|
+
brainKey = ({ anthropic: "claude", openai: "chatgpt", xai: "grok" })[p] || "claude";
|
|
1076
|
+
} catch {}
|
|
1077
|
+
flags.brainKey = brainKey;
|
|
1078
|
+
await deployBot(dir, { flags, rl });
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
console.log("\n " + C.red("✗ " + (e.message || e)) + "\n");
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
} finally {
|
|
1083
|
+
rl.close();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function cmdUpdate(flags, rest) {
|
|
1088
|
+
const cfg = loadCfg();
|
|
1089
|
+
ASSUME_YES = !!(flags.yes || process.env.KOONI_YES);
|
|
1090
|
+
if (flags.lang === "en" || cfg.lang === "en") L = "en";
|
|
1091
|
+
banner();
|
|
1092
|
+
installAgentSkill(flags);
|
|
1093
|
+
|
|
1094
|
+
const dir = resolveBotDir(rest[0]);
|
|
1095
|
+
if (!dir) { console.log(" " + C.red(t().needDir) + " " + (rest[0] || process.cwd()) + "\n"); process.exit(1); }
|
|
1096
|
+
|
|
1097
|
+
const marker = readMarker(dir) || {};
|
|
1098
|
+
const current = marker.version || readPkgVersion(dir) || "0.0.0";
|
|
1099
|
+
|
|
1100
|
+
const tgz = join(dir, ".kooni-template.tgz");
|
|
1101
|
+
process.stdout.write(C.dim(" " + t().updRevalidating + "\n"));
|
|
1102
|
+
await downloadTemplate(tgz);
|
|
1103
|
+
|
|
1104
|
+
// versión nueva desde el tarball
|
|
1105
|
+
const tmp = join(dir, ".kooni-extract");
|
|
1106
|
+
mkdirSync(tmp, { recursive: true });
|
|
1107
|
+
const src = extractToTemp(tgz, tmp);
|
|
1108
|
+
const next = readPkgVersion(src) || "0.0.0";
|
|
1109
|
+
|
|
1110
|
+
const verLt = (a, b) => {
|
|
1111
|
+
const pa = String(a).split(".").map(Number), pb = String(b).split(".").map(Number);
|
|
1112
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const x = pa[i] || 0, y = pb[i] || 0; if (x !== y) return x < y; }
|
|
1113
|
+
return false;
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
if (!verLt(current, next)) {
|
|
1117
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
1118
|
+
rmSync(tgz, { force: true });
|
|
1119
|
+
console.log(" " + C.green("✓") + " " + t().updUpToDate + " (v" + current + ")\n");
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// respaldo + extraer sobre la instalación
|
|
1124
|
+
const backupPath = backupBeforeUpdate(dir, current);
|
|
1125
|
+
extractOver(tgz, dir);
|
|
1126
|
+
writeMarker(dir, { slug: marker.slug || basename(dir), version: next, lang: marker.lang || L });
|
|
1127
|
+
|
|
1128
|
+
console.log(" " + C.green("✓") + " " + t().updDone(next));
|
|
1129
|
+
if (backupPath) console.log(" " + C.dim(t().updBackup(backupPath.slice(dir.length + 1))));
|
|
1130
|
+
console.log(" " + C.dim(t().updPreserved));
|
|
1131
|
+
console.log(" " + C.dim(t().updReplaced));
|
|
1132
|
+
|
|
1133
|
+
// dependencias + migraciones + reindex + deploy
|
|
1134
|
+
console.log("\n " + C.dim(t().installing));
|
|
1135
|
+
runPnpm(dir, ["install"]);
|
|
1136
|
+
try { runPnpm(dir, ["db:apply:remote"]); } catch { /* best-effort */ }
|
|
1137
|
+
try { runPnpm(dir, ["kb:reindex"]); } catch { /* best-effort */ }
|
|
1138
|
+
console.log(" " + C.dim(t().deploying));
|
|
1139
|
+
try { runPnpm(dir, ["run", "deploy"]); } catch { /* el deploy-check imprime el detalle */ }
|
|
1140
|
+
|
|
1141
|
+
// reindex del worker si hay estado con URL
|
|
1142
|
+
try {
|
|
1143
|
+
const st = JSON.parse(readFileSync(join(dir, ".bot-state.json"), "utf8"));
|
|
1144
|
+
if (st.worker_url) {
|
|
1145
|
+
const dv = readFileSync(join(dir, ".dev.vars"), "utf8");
|
|
1146
|
+
const tok = (dv.match(/^KB_REINDEX_TOKEN=(.+)$/m) || [])[1];
|
|
1147
|
+
if (tok) await fetchTimeout(`${st.worker_url}/kb/reindex`, { method: "POST", headers: { "X-Reindex-Token": tok } }, 15000);
|
|
1148
|
+
}
|
|
1149
|
+
} catch {}
|
|
1150
|
+
|
|
1151
|
+
console.log("");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function cmdDoctor(flags, rest) {
|
|
1155
|
+
const cfg = loadCfg();
|
|
1156
|
+
if (flags.lang === "en" || cfg.lang === "en") L = "en";
|
|
1157
|
+
banner();
|
|
1158
|
+
|
|
1159
|
+
const ok = (s) => console.log(" " + C.green("✓") + " " + s);
|
|
1160
|
+
const warn = (s, h) => { console.log(" " + C.yellow("⚠") + " " + s); if (h) console.log(" " + C.dim(h)); };
|
|
1161
|
+
const bad = (s, h) => { console.log(" " + C.red("✗") + " " + s); if (h) console.log(" " + C.dim(h)); };
|
|
1162
|
+
|
|
1163
|
+
const dir = resolveBotDir(rest[0]);
|
|
1164
|
+
if (!dir) { bad(t().needDir + " " + (rest[0] || process.cwd())); process.exit(1); }
|
|
1165
|
+
ok(m("Bot encontrado en ", "Bot found in ") + C.cyan(dir));
|
|
1166
|
+
|
|
1167
|
+
let marker = readMarker(dir) || {};
|
|
1168
|
+
const pkgVer = readPkgVersion(dir);
|
|
1169
|
+
const installed = marker.version || pkgVer || "?";
|
|
1170
|
+
if (marker.version) ok(m("Instalado: ", "Installed: ") + C.cyan(marker.slug) + " v" + installed);
|
|
1171
|
+
else if (pkgVer) warn(m("Sin marker; versión del package.json: ", "No marker; package.json version: ") + C.cyan("v" + pkgVer));
|
|
1172
|
+
|
|
1173
|
+
if (existsSync(join(dir, "wrangler.toml"))) ok("wrangler.toml " + m("presente", "present"));
|
|
1174
|
+
else { bad(m("Falta wrangler.toml", "wrangler.toml missing")); }
|
|
1175
|
+
if (existsSync(join(dir, "package.json"))) ok("package.json " + m("presente", "present"));
|
|
1176
|
+
else { warn(m("Falta package.json", "package.json missing")); }
|
|
1177
|
+
if (existsSync(join(dir, "node_modules"))) ok(m("Dependencias instaladas", "Dependencies installed"));
|
|
1178
|
+
else warn(m("Dependencias sin instalar", "Dependencies not installed"), "pnpm install");
|
|
1179
|
+
if (existsSync(join(dir, "member", "config.local.ts"))) ok(m("Negocio configurado (member/config.local.ts)", "Business configured (member/config.local.ts)"));
|
|
1180
|
+
else warn(m("El negocio aún no está configurado", "Business not configured yet"), "kooni-bot init");
|
|
1181
|
+
|
|
1182
|
+
let wt = "";
|
|
1183
|
+
try { wt = readFileSync(join(dir, "wrangler.toml"), "utf8"); } catch {}
|
|
1184
|
+
const val = (k) => { const mm = wt.match(new RegExp(`^\\s*${k}\\s*=\\s*["']([^"']*)`, "m")); return mm ? mm[1] : null; };
|
|
1185
|
+
const botName = val("BOT_NAME"), botLang = val("BOT_LANGUAGE"), tier = val("BOT_TIER"), baseUrl = val("DASHBOARD_BASE_URL");
|
|
1186
|
+
if (botName) ok(m("Nombre del bot: ", "Bot name: ") + C.cyan(botName));
|
|
1187
|
+
else warn(m("BOT_NAME sin definir", "BOT_NAME not set"));
|
|
1188
|
+
if (botLang) ok(m("Idioma: ", "Language: ") + C.cyan(botLang));
|
|
1189
|
+
if (tier) ok(m("Tier: ", "Tier: ") + C.cyan(tier));
|
|
1190
|
+
|
|
1191
|
+
// versión más reciente (best-effort)
|
|
1192
|
+
try {
|
|
1193
|
+
const r = await fetchTimeout(RAW_VERSION_URL, {}, 8000);
|
|
1194
|
+
if (r.ok) {
|
|
1195
|
+
const latest = (await r.json()).version;
|
|
1196
|
+
if (latest && latest !== installed) warn(m(`Hay una versión nueva: v${latest} (tienes v${installed})`, `New version: v${latest} (you have v${installed})`), "kooni-bot update");
|
|
1197
|
+
else if (latest === installed) ok(m("Estás en la última versión", "You're on the latest version"));
|
|
1198
|
+
}
|
|
1199
|
+
} catch { /* sin red */ }
|
|
1200
|
+
|
|
1201
|
+
if (baseUrl) {
|
|
1202
|
+
try {
|
|
1203
|
+
const r = await fetchTimeout(baseUrl.replace(/\/$/, "") + "/health", {}, 8000);
|
|
1204
|
+
if (r.ok) ok(m("El bot responde en línea (", "The bot is online (") + baseUrl + ")");
|
|
1205
|
+
else warn(m(`El bot respondió HTTP ${r.status}`, `Bot responded HTTP ${r.status}`));
|
|
1206
|
+
} catch { warn(m("El bot no respondió", "The bot didn't respond"), "¿Ya desplegaste? kooni-bot deploy"); }
|
|
1207
|
+
} else {
|
|
1208
|
+
warn(m("Sin DASHBOARD_BASE_URL", "No DASHBOARD_BASE_URL"), m("Se llena al desplegar", "Filled in on deploy"));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
console.log("");
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// ── ayuda ────────────────────────────────────────────────────────────────────
|
|
1215
|
+
function help() {
|
|
1216
|
+
console.log(`
|
|
1217
|
+
${C.cyan("kooni-bot")} — ${t().helpIntro}
|
|
1218
|
+
|
|
1219
|
+
${C.cyan("npx kooni-bot init [dir]")} ${m("instala (descarga template + config + deploy)", "install (download template + config + deploy)")}
|
|
1220
|
+
${C.cyan("npx kooni-bot deploy [dir]")} ${m("provisiona Cloudflare y publica el worker", "provision Cloudflare and publish the worker")}
|
|
1221
|
+
${C.cyan("npx kooni-bot update [dir]")} ${m("actualiza sin perder tu configuración", "update without losing config")}
|
|
1222
|
+
${C.cyan("npx kooni-bot doctor [dir]")} ${m("diagnóstico del bot instalado", "diagnose the installed bot")}
|
|
1223
|
+
${C.cyan("npx kooni-bot version")} ${m("versión del CLI", "CLI version")}
|
|
1224
|
+
|
|
1225
|
+
${C.dim(" Flags de init (modo no-interactivo, para agentes):")}
|
|
1226
|
+
${C.dim(" --yes --slug <slug> --negocio <nombre> --bot-name <nombre> --lang es-MX|es-ES|en|pt-BR")}
|
|
1227
|
+
${C.dim(" --tier free|pro --cerebro claude|chatgpt|grok|gateway --base-url <url>")}
|
|
1228
|
+
${C.dim(" --que --ofrece --horario --ubicacion --telefono --web --pagos --faq --reglas --tono")}
|
|
1229
|
+
${C.dim(" --no-deploy --no-agent-skill --email <correo>")}
|
|
1230
|
+
`);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
1234
|
+
const IS_MAIN = (() => {
|
|
1235
|
+
const argv1 = process.argv[1] || "";
|
|
1236
|
+
try { if (realpathSync(argv1) === fileURLToPath(import.meta.url)) return true; } catch {}
|
|
1237
|
+
const base = argv1.replace(/\\/g, "/").split("/").pop() || "";
|
|
1238
|
+
return base === "kooni.js" || base === "kooni-bot";
|
|
1239
|
+
})();
|
|
1240
|
+
|
|
1241
|
+
if (IS_MAIN) {
|
|
1242
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
1243
|
+
const { flags, rest } = parseFlags(args);
|
|
1244
|
+
(async () => {
|
|
1245
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") return help();
|
|
1246
|
+
if (cmd === "version" || cmd === "--version" || cmd === "-v") { console.log("kooni-bot " + CLI_VERSION); return; }
|
|
1247
|
+
if (cmd === "init") return cmdInit(flags, rest);
|
|
1248
|
+
if (cmd === "deploy") return cmdDeploy(flags, rest);
|
|
1249
|
+
if (cmd === "update") return cmdUpdate(flags, rest);
|
|
1250
|
+
if (cmd === "doctor") return cmdDoctor(flags, rest);
|
|
1251
|
+
console.log(t().unknown + " " + cmd);
|
|
1252
|
+
help();
|
|
1253
|
+
})().catch((e) => {
|
|
1254
|
+
console.error(C.red("✗ " + (e?.message || e)));
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
export { parseFlags, renderMemberConfig, stampWrangler, sanitizeSlug, normBotLang, collectAnswers };
|