@wabot-dev/create 0.0.3 → 0.0.4

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/lib/cli.mjs CHANGED
@@ -12,6 +12,7 @@ export function buildProgram() {
12
12
  .description('Create a new Wabot project and install packaged Wabot skills')
13
13
  .argument('[project-directory]', 'project name')
14
14
  .option('--db-url <dbUrl>', 'postgres connection string (used as default when prompting)')
15
+ .option('--lang <code>', 'language for wizard messages (en, es)', 'en')
15
16
  .action(async (projectDirectory, options) => {
16
17
  await runCreateProjectCommand(projectDirectory, options)
17
18
  })
@@ -5,97 +5,105 @@ import path from 'path'
5
5
  import fs from 'fs/promises'
6
6
  import { spawn } from 'child_process'
7
7
 
8
+ import { installSkillsInProject, listSupportedAgents } from './skills/install.mjs'
9
+ import { listSkills } from './skills/registry.mjs'
10
+ import { createTranslator } from './i18n.mjs'
11
+
8
12
  const REQUIRED_NODE_MAJOR = 22
9
13
 
10
14
  const AI_PROVIDERS = [
11
15
  {
12
16
  value: 'openrouter',
13
17
  title: 'OpenRouter',
14
- description: 'Used by the included Pixel example',
18
+ descriptionKey: 'providers.openrouter',
15
19
  envKey: 'OPENROUTER_API_KEY',
16
20
  },
17
21
  {
18
22
  value: 'openai',
19
23
  title: 'OpenAI',
20
- description: 'GPT models via the official OpenAI API',
24
+ descriptionKey: 'providers.openai',
21
25
  envKey: 'OPENAI_API_KEY',
22
26
  },
23
27
  {
24
28
  value: 'anthropic',
25
29
  title: 'Anthropic',
26
- description: 'Claude models',
30
+ descriptionKey: 'providers.anthropic',
27
31
  envKey: 'ANTHROPIC_API_KEY',
28
32
  },
29
33
  {
30
34
  value: 'google',
31
35
  title: 'Google',
32
- description: 'Gemini models',
36
+ descriptionKey: 'providers.google',
33
37
  envKey: 'GOOGLE_API_KEY',
34
38
  },
35
39
  ]
36
40
 
41
+ function agentLabel(t, agent) {
42
+ const key = `agent.label.${agent}`
43
+ const value = t(key)
44
+ return value === key ? agent : value
45
+ }
46
+
37
47
  export async function runCreateProjectCommand(projectDirectory, options) {
38
- printIntro()
39
- ensureNodeVersion()
48
+ const t = createTranslator(options?.lang)
49
+
50
+ printIntro(t)
51
+ ensureNodeVersion(t)
40
52
 
41
- const projectName = await resolveProjectName(projectDirectory)
42
- const setup = await collectSetup(options)
53
+ const projectName = await resolveProjectName(projectDirectory, t)
54
+ const setup = await collectSetup(options, t)
43
55
 
44
- await createProject(projectName, setup)
45
- await installDependencies(projectName)
56
+ await createProject(projectName, setup, t)
57
+ await installDependencies(projectName, t)
46
58
 
47
- printNextSteps(projectName, setup)
59
+ if (setup.skills.install) {
60
+ await installAgentSkills(projectName, setup.skills, t)
61
+ }
62
+
63
+ printNextSteps(projectName, setup, t)
48
64
  process.exit(0)
49
65
  }
50
66
 
51
- function printIntro() {
67
+ function printIntro(t) {
52
68
  const line = chalk.gray('─'.repeat(60))
53
69
  console.log()
54
70
  console.log(line)
55
- console.log(chalk.bold.cyan(' 🤖 Wabot project creator'))
71
+ console.log(chalk.bold.cyan(` ${t('intro.title')}`))
56
72
  console.log()
73
+ console.log(` ${t('intro.line1')}`)
57
74
  console.log(
58
- ' This wizard will scaffold a new chat-bot project from the',
59
- )
60
- console.log(
61
- ` ${chalk.cyan('@wabot-dev/template')} starter and configure your ${chalk.cyan('.env')}.`,
75
+ ` ${t('intro.line2', chalk.cyan('@wabot-dev/template'), chalk.cyan('.env'))}`,
62
76
  )
63
77
  console.log()
64
- console.log(' You will be asked about:')
65
- console.log('where to store data (in-memory or PostgreSQL)')
66
- console.log('which AI providers you want to use')
78
+ console.log(` ${t('intro.askAbout')}`)
79
+ console.log(`${t('intro.bullet.storage')}`)
80
+ console.log(`${t('intro.bullet.providers')}`)
67
81
  console.log(line)
68
82
  console.log()
69
83
  }
70
84
 
71
- function ensureNodeVersion() {
85
+ function ensureNodeVersion(t) {
72
86
  const major = Number(process.versions.node.split('.')[0])
73
87
  if (major === REQUIRED_NODE_MAJOR) {
74
88
  return
75
89
  }
76
90
 
77
- console.log(
78
- chalk.red(
79
- `✖ Node.js ${REQUIRED_NODE_MAJOR} is required (you are running ${process.versions.node}).`,
80
- ),
81
- )
91
+ console.log(chalk.red(t('node.required', REQUIRED_NODE_MAJOR, process.versions.node)))
82
92
  console.log()
83
- console.log(
84
- `The Wabot template pins Node ${REQUIRED_NODE_MAJOR} in its ${chalk.cyan('.nvmrc')}.`,
85
- )
86
- console.log(`We recommend installing it with ${chalk.cyan('nvm')}:`)
93
+ console.log(t('node.templatePin', REQUIRED_NODE_MAJOR, chalk.cyan('.nvmrc')))
94
+ console.log(t('node.installHint', chalk.cyan('nvm')))
87
95
  console.log()
88
96
  console.log(chalk.cyan(` nvm install ${REQUIRED_NODE_MAJOR}`))
89
97
  console.log(chalk.cyan(` nvm use ${REQUIRED_NODE_MAJOR}`))
90
98
  console.log()
91
99
  console.log(
92
- `Don't have nvm? See ${chalk.underline('https://github.com/nvm-sh/nvm#installing-and-updating')}`,
100
+ t('node.noNvm', chalk.underline('https://github.com/nvm-sh/nvm#installing-and-updating')),
93
101
  )
94
102
  console.log()
95
103
  process.exit(1)
96
104
  }
97
105
 
98
- async function resolveProjectName(projectDirectory) {
106
+ async function resolveProjectName(projectDirectory, t) {
99
107
  if (projectDirectory) {
100
108
  return projectDirectory
101
109
  }
@@ -103,36 +111,30 @@ async function resolveProjectName(projectDirectory) {
103
111
  const response = await prompts({
104
112
  type: 'text',
105
113
  name: 'projectName',
106
- message: 'What is your project named?',
114
+ message: t('project.namePrompt'),
107
115
  initial: 'my-wabot-app',
108
116
  })
109
117
 
110
118
  if (!response.projectName) {
111
- throw new Error('Project name is required')
119
+ throw new Error(t('project.nameRequired'))
112
120
  }
113
121
 
114
122
  return response.projectName
115
123
  }
116
124
 
117
- async function collectSetup(options) {
125
+ async function collectSetup(options, t) {
118
126
  const onCancel = () => {
119
- throw new Error('Setup cancelled')
127
+ throw new Error(t('project.cancelled'))
120
128
  }
121
129
 
122
130
  const { storage } = await prompts(
123
131
  {
124
132
  type: 'select',
125
133
  name: 'storage',
126
- message: 'How do you want to store data?',
134
+ message: t('storage.prompt'),
127
135
  choices: [
128
- {
129
- title: 'In-memory (quickest start, data resets on restart)',
130
- value: 'memory',
131
- },
132
- {
133
- title: 'PostgreSQL (persistent, recommended for real bots)',
134
- value: 'postgres',
135
- },
136
+ { title: t('storage.memory'), value: 'memory' },
137
+ { title: t('storage.postgres'), value: 'postgres' },
136
138
  ],
137
139
  initial: 0,
138
140
  },
@@ -145,7 +147,7 @@ async function collectSetup(options) {
145
147
  {
146
148
  type: 'text',
147
149
  name: 'databaseUrl',
148
- message: 'DATABASE_URL (leave blank to fill in later):',
150
+ message: t('storage.dbUrlPrompt'),
149
151
  initial: options.dbUrl ?? '',
150
152
  },
151
153
  { onCancel },
@@ -157,14 +159,14 @@ async function collectSetup(options) {
157
159
  {
158
160
  type: 'multiselect',
159
161
  name: 'providers',
160
- message: 'Which AI providers will you use? (space to toggle, enter to confirm)',
162
+ message: t('providers.prompt'),
161
163
  choices: AI_PROVIDERS.map((p) => ({
162
164
  title: p.title,
163
165
  value: p.value,
164
- description: p.description,
166
+ description: t(p.descriptionKey),
165
167
  selected: p.value === 'openrouter',
166
168
  })),
167
- hint: 'At least one is required to run the included Pixel example',
169
+ hint: t('providers.hint'),
168
170
  instructions: false,
169
171
  min: 1,
170
172
  },
@@ -178,22 +180,66 @@ async function collectSetup(options) {
178
180
  {
179
181
  type: 'password',
180
182
  name: 'apiKey',
181
- message: `${provider.envKey} (leave blank to fill in later):`,
183
+ message: t('providers.apiKeyPrompt', provider.envKey),
182
184
  },
183
185
  { onCancel },
184
186
  )
185
187
  providerKeys[provider.envKey] = apiKey ?? ''
186
188
  }
187
189
 
188
- return { storage, databaseUrl, providers, providerKeys }
190
+ const skills = await collectSkillsSetup({ onCancel }, t)
191
+
192
+ return { storage, databaseUrl, providers, providerKeys, skills }
189
193
  }
190
194
 
191
- async function createProject(projectName, setup) {
195
+ async function collectSkillsSetup({ onCancel }, t) {
196
+ const supportedAgents = listSupportedAgents()
197
+ const availableSkills = listSkills()
198
+
199
+ const { install } = await prompts(
200
+ {
201
+ type: 'confirm',
202
+ name: 'install',
203
+ message: t('skills.installPrompt', availableSkills.length),
204
+ initial: true,
205
+ },
206
+ { onCancel },
207
+ )
208
+
209
+ if (!install) {
210
+ return { install: false, agents: [], skills: [] }
211
+ }
212
+
213
+ const { agents } = await prompts(
214
+ {
215
+ type: 'multiselect',
216
+ name: 'agents',
217
+ message: t('skills.agentsPrompt'),
218
+ choices: supportedAgents.map((agent) => ({
219
+ title: agentLabel(t, agent),
220
+ value: agent,
221
+ selected: true,
222
+ })),
223
+ hint: t('skills.agentsHint'),
224
+ instructions: false,
225
+ min: 1,
226
+ },
227
+ { onCancel },
228
+ )
229
+
230
+ return {
231
+ install: true,
232
+ agents,
233
+ skills: availableSkills.map((skill) => skill.name),
234
+ }
235
+ }
236
+
237
+ async function createProject(projectName, setup, t) {
192
238
  const targetDir = path.resolve(process.cwd(), projectName)
193
239
 
194
- await ensureDirectoryDoesNotExist(targetDir, projectName)
240
+ await ensureDirectoryDoesNotExist(targetDir, projectName, t)
195
241
 
196
- console.log(chalk.blue(`\nCreating a new Wabot project in ${chalk.green(targetDir)}\n`))
242
+ console.log(chalk.blue(t('project.creating', chalk.green(targetDir))))
197
243
 
198
244
  const emitter = degit('wabot-dev/wabot-template', {
199
245
  cache: false,
@@ -208,13 +254,13 @@ async function createProject(projectName, setup) {
208
254
  packageJson.name = projectName
209
255
  await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n')
210
256
 
211
- await writeEnvFile(targetDir, setup)
257
+ await writeEnvFile(targetDir, setup, t)
212
258
  }
213
259
 
214
- async function ensureDirectoryDoesNotExist(targetDir, projectName) {
260
+ async function ensureDirectoryDoesNotExist(targetDir, projectName, t) {
215
261
  try {
216
262
  await fs.access(targetDir)
217
- throw new Error(`Directory ${projectName} already exists`)
263
+ throw new Error(t('project.exists', projectName))
218
264
  } catch (error) {
219
265
  if (error?.code === 'ENOENT') {
220
266
  return
@@ -224,22 +270,22 @@ async function ensureDirectoryDoesNotExist(targetDir, projectName) {
224
270
  }
225
271
  }
226
272
 
227
- async function writeEnvFile(targetDir, setup) {
273
+ async function writeEnvFile(targetDir, setup, t) {
228
274
  const lines = []
229
- lines.push('# debug important things')
275
+ lines.push(t('env.debugComment'))
230
276
  lines.push('DEBUG=wabot:*:error,wabot:*:warn,wabot:*:info')
231
277
  lines.push('')
232
278
 
233
279
  if (setup.storage === 'postgres') {
234
- lines.push('# postgres connection string')
280
+ lines.push(t('env.postgresComment'))
235
281
  lines.push(`DATABASE_URL=${setup.databaseUrl}`)
236
282
  } else {
237
- lines.push('# using in-memory storage (set DATABASE_URL to switch to postgres)')
283
+ lines.push(t('env.inMemoryComment'))
238
284
  lines.push('# DATABASE_URL=')
239
285
  }
240
286
  lines.push('')
241
287
 
242
- lines.push('# AI provider keys')
288
+ lines.push(t('env.providersComment'))
243
289
  for (const provider of AI_PROVIDERS) {
244
290
  const selected = setup.providers.includes(provider.value)
245
291
  if (selected) {
@@ -253,11 +299,11 @@ async function writeEnvFile(targetDir, setup) {
253
299
  await fs.writeFile(path.join(targetDir, '.env'), lines.join('\n'))
254
300
  }
255
301
 
256
- async function installDependencies(projectName) {
302
+ async function installDependencies(projectName, t) {
257
303
  const targetDir = path.resolve(process.cwd(), projectName)
258
304
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
259
305
 
260
- console.log(chalk.blue('\nInstalling dependencies with npm...\n'))
306
+ console.log(chalk.blue(t('deps.installing')))
261
307
 
262
308
  await new Promise((resolve, reject) => {
263
309
  const child = spawn(npmCmd, ['install'], {
@@ -270,30 +316,71 @@ async function installDependencies(projectName) {
270
316
  if (code === 0) {
271
317
  resolve()
272
318
  } else {
273
- reject(new Error(`npm install exited with code ${code}`))
319
+ reject(new Error(t('deps.exitCode', code)))
274
320
  }
275
321
  })
276
322
  })
277
323
  }
278
324
 
279
- function printNextSteps(projectName, setup) {
280
- console.log(chalk.green(`\n✔ Project ${chalk.bold(projectName)} is ready.\n`))
325
+ async function installAgentSkills(projectName, skillsSetup, t) {
326
+ const targetDir = path.resolve(process.cwd(), projectName)
327
+
328
+ console.log(chalk.blue(t('skills.installing')))
329
+
330
+ try {
331
+ const result = await installSkillsInProject(targetDir, skillsSetup.agents, skillsSetup.skills)
332
+
333
+ const installed = result.installations.filter((i) => i.status === 'installed')
334
+ const skipped = result.installations.filter((i) => i.status === 'skipped')
335
+
336
+ if (installed.length > 0) {
337
+ const byAgent = new Map()
338
+ for (const item of installed) {
339
+ const list = byAgent.get(item.agent) ?? []
340
+ list.push(item.skill)
341
+ byAgent.set(item.agent, list)
342
+ }
343
+ for (const [agent, names] of byAgent) {
344
+ const dir = path.join(projectName, ...resolveAgentDir(agent))
345
+ console.log(chalk.green(t('skills.installed', agent, names.length, dir)))
346
+ }
347
+ }
348
+ if (skipped.length > 0) {
349
+ console.log(chalk.yellow(t('skills.skipped', skipped.length)))
350
+ }
351
+ } catch (error) {
352
+ console.log(chalk.red(t('skills.failed', error.message)))
353
+ console.log(chalk.yellow(t('skills.installLater')))
354
+ }
355
+ }
356
+
357
+ function resolveAgentDir(agent) {
358
+ switch (agent) {
359
+ case 'claude': return ['.claude', 'skills']
360
+ case 'codex': return ['.codex', 'skills']
361
+ case 'agents': return ['.agents', 'skills']
362
+ default: return [`.${agent}`, 'skills']
363
+ }
364
+ }
365
+
366
+ function printNextSteps(projectName, setup, t) {
367
+ console.log(chalk.green(t('next.ready', chalk.bold(projectName))))
281
368
 
282
369
  const missingKeys = AI_PROVIDERS.filter(
283
370
  (p) => setup.providers.includes(p.value) && !setup.providerKeys[p.envKey],
284
371
  ).map((p) => p.envKey)
285
372
 
286
373
  if (setup.storage === 'postgres' && !setup.databaseUrl) {
287
- console.log(chalk.yellow(' ⚠ DATABASE_URL is empty — set it in .env before running.'))
374
+ console.log(chalk.yellow(t('next.dbUrlEmpty')))
288
375
  }
289
376
  if (missingKeys.length > 0) {
290
- console.log(
291
- chalk.yellow(` ⚠ Missing API keys in .env: ${missingKeys.join(', ')}`),
292
- )
377
+ console.log(chalk.yellow(t('next.missingKeys', missingKeys.join(', '))))
293
378
  }
294
379
 
295
- console.log('\nNext steps:')
380
+ console.log(t('next.heading'))
296
381
  console.log(chalk.cyan(`\n cd ${projectName}`))
297
- console.log(chalk.cyan(' npm run dev'))
298
- console.log('\nHappy hacking! 🚀\n')
382
+ console.log(chalk.cyan(' npm run dev:watch'))
383
+ console.log(t('next.cmdChannelHint'))
384
+ console.log(chalk.cyan('\n npm run cmd:channel'))
385
+ console.log(t('next.happy'))
299
386
  }
package/lib/i18n.mjs ADDED
@@ -0,0 +1,167 @@
1
+ const STRINGS = {
2
+ en: {
3
+ 'intro.title': '🤖 Wabot project creator',
4
+ 'intro.line1': 'This wizard will scaffold a new chat-bot project from the',
5
+ 'intro.line2': (template, env) => `${template} starter and configure your ${env}.`,
6
+ 'intro.askAbout': 'You will be asked about:',
7
+ 'intro.bullet.storage': 'where to store data (in-memory or PostgreSQL)',
8
+ 'intro.bullet.providers': 'which AI providers you want to use',
9
+
10
+ 'node.required': (required, current) =>
11
+ `✖ Node.js ${required} is required (you are running ${current}).`,
12
+ 'node.templatePin': (required, nvmrc) =>
13
+ `The Wabot template pins Node ${required} in its ${nvmrc}.`,
14
+ 'node.installHint': (nvm) => `We recommend installing it with ${nvm}:`,
15
+ 'node.noNvm': (url) => `Don't have nvm? See ${url}`,
16
+
17
+ 'project.namePrompt': 'What is your project named?',
18
+ 'project.nameRequired': 'Project name is required',
19
+ 'project.cancelled': 'Setup cancelled',
20
+ 'project.exists': (name) => `Directory ${name} already exists`,
21
+ 'project.creating': (dir) => `\nCreating a new Wabot project in ${dir}\n`,
22
+
23
+ 'storage.prompt': 'How do you want to store data?',
24
+ 'storage.memory': 'In-memory (quickest start, data resets on restart)',
25
+ 'storage.postgres': 'PostgreSQL (persistent, recommended for real bots)',
26
+ 'storage.dbUrlPrompt': 'DATABASE_URL (leave blank to fill in later):',
27
+
28
+ 'providers.prompt': 'Which AI providers will you use? (space to toggle, enter to confirm)',
29
+ 'providers.hint': 'At least one is required to run the included Pixel example',
30
+ 'providers.apiKeyPrompt': (envKey) => `${envKey} (leave blank to fill in later):`,
31
+ 'providers.openrouter': 'Used by the included Pixel example',
32
+ 'providers.openai': 'GPT models via the official OpenAI API',
33
+ 'providers.anthropic': 'Claude models',
34
+ 'providers.google': 'Gemini models',
35
+
36
+ 'skills.installPrompt': (count) =>
37
+ `Install Wabot agent skills (${count}) into the new project?`,
38
+ 'skills.agentsPrompt': 'Install for which agents? (space to toggle, enter to confirm)',
39
+ 'skills.agentsHint': 'Each adds a folder of <agent>/skills under the project root',
40
+ 'skills.installing': '\nInstalling Wabot agent skills...\n',
41
+ 'skills.installed': (agent, count, dir) =>
42
+ ` ✔ ${agent}: installed ${count} skill(s) into ${dir}`,
43
+ 'skills.skipped': (count) =>
44
+ ` ⚠ ${count} skill folder(s) already existed and were left untouched.`,
45
+ 'skills.failed': (msg) => ` ✖ Failed to install agent skills: ${msg}`,
46
+ 'skills.installLater':
47
+ ' You can install them later with `create-wabot skill add <skill>`.',
48
+
49
+ 'deps.installing': '\nInstalling dependencies with npm...\n',
50
+ 'deps.exitCode': (code) => `npm install exited with code ${code}`,
51
+
52
+ 'env.debugComment': '# debug important things',
53
+ 'env.postgresComment': '# postgres connection string',
54
+ 'env.inMemoryComment':
55
+ '# using in-memory storage (set DATABASE_URL to switch to postgres)',
56
+ 'env.providersComment': '# AI provider keys',
57
+
58
+ 'next.ready': (name) => `\n✔ Project ${name} is ready.\n`,
59
+ 'next.dbUrlEmpty': ' ⚠ DATABASE_URL is empty — set it in .env before running.',
60
+ 'next.missingKeys': (keys) => ` ⚠ Missing API keys in .env: ${keys}`,
61
+ 'next.heading': '\nNext steps:',
62
+ 'next.cmdChannelHint':
63
+ '\nIn another terminal, open a CLI to chat with your bot:',
64
+ 'next.happy': '\nHappy hacking! 🚀\n',
65
+
66
+ 'agent.label.claude': 'Claude Code (.claude/skills)',
67
+ 'agent.label.codex': 'Codex (.codex/skills)',
68
+ 'agent.label.agents': 'Generic agents (.agents/skills)',
69
+ },
70
+
71
+ es: {
72
+ 'intro.title': '🤖 Creador de proyectos Wabot',
73
+ 'intro.line1': 'Este asistente generará un nuevo proyecto de chat-bot a partir de',
74
+ 'intro.line2': (template, env) =>
75
+ `la plantilla ${template} y configurará tu archivo ${env}.`,
76
+ 'intro.askAbout': 'Te preguntaremos sobre:',
77
+ 'intro.bullet.storage': 'dónde almacenar los datos (en memoria o PostgreSQL)',
78
+ 'intro.bullet.providers': 'qué proveedores de IA deseas usar',
79
+
80
+ 'node.required': (required, current) =>
81
+ `✖ Se requiere Node.js ${required} (estás usando ${current}).`,
82
+ 'node.templatePin': (required, nvmrc) =>
83
+ `La plantilla de Wabot fija Node ${required} en su ${nvmrc}.`,
84
+ 'node.installHint': (nvm) => `Recomendamos instalarlo con ${nvm}:`,
85
+ 'node.noNvm': (url) => `¿No tienes nvm? Consulta ${url}`,
86
+
87
+ 'project.namePrompt': '¿Cómo se llama tu proyecto?',
88
+ 'project.nameRequired': 'El nombre del proyecto es obligatorio',
89
+ 'project.cancelled': 'Configuración cancelada',
90
+ 'project.exists': (name) => `El directorio ${name} ya existe`,
91
+ 'project.creating': (dir) => `\nCreando un nuevo proyecto Wabot en ${dir}\n`,
92
+
93
+ 'storage.prompt': '¿Cómo deseas almacenar los datos?',
94
+ 'storage.memory': 'En memoria (inicio más rápido, los datos se pierden al reiniciar)',
95
+ 'storage.postgres': 'PostgreSQL (persistente, recomendado para bots reales)',
96
+ 'storage.dbUrlPrompt': 'DATABASE_URL (déjalo en blanco para completarlo después):',
97
+
98
+ 'providers.prompt':
99
+ '¿Qué proveedores de IA usarás? (espacio para alternar, enter para confirmar)',
100
+ 'providers.hint':
101
+ 'Se requiere al menos uno para ejecutar el ejemplo Pixel incluido',
102
+ 'providers.apiKeyPrompt': (envKey) =>
103
+ `${envKey} (déjalo en blanco para completarlo después):`,
104
+ 'providers.openrouter': 'Usado por el ejemplo Pixel incluido',
105
+ 'providers.openai': 'Modelos GPT mediante la API oficial de OpenAI',
106
+ 'providers.anthropic': 'Modelos Claude',
107
+ 'providers.google': 'Modelos Gemini',
108
+
109
+ 'skills.installPrompt': (count) =>
110
+ `¿Instalar las skills de agente de Wabot (${count}) en el nuevo proyecto?`,
111
+ 'skills.agentsPrompt':
112
+ '¿Para qué agentes instalarlas? (espacio para alternar, enter para confirmar)',
113
+ 'skills.agentsHint':
114
+ 'Cada uno añade una carpeta <agente>/skills en la raíz del proyecto',
115
+ 'skills.installing': '\nInstalando skills de agente Wabot...\n',
116
+ 'skills.installed': (agent, count, dir) =>
117
+ ` ✔ ${agent}: se instalaron ${count} skill(s) en ${dir}`,
118
+ 'skills.skipped': (count) =>
119
+ ` ⚠ ${count} carpeta(s) de skill ya existían y no se modificaron.`,
120
+ 'skills.failed': (msg) => ` ✖ Fallo al instalar las skills de agente: ${msg}`,
121
+ 'skills.installLater':
122
+ ' Puedes instalarlas más tarde con `create-wabot skill add <skill>`.',
123
+
124
+ 'deps.installing': '\nInstalando dependencias con npm...\n',
125
+ 'deps.exitCode': (code) => `npm install terminó con código ${code}`,
126
+
127
+ 'env.debugComment': '# depurar lo importante',
128
+ 'env.postgresComment': '# cadena de conexión de postgres',
129
+ 'env.inMemoryComment':
130
+ '# usando almacenamiento en memoria (define DATABASE_URL para cambiar a postgres)',
131
+ 'env.providersComment': '# claves de proveedores de IA',
132
+
133
+ 'next.ready': (name) => `\n✔ El proyecto ${name} está listo.\n`,
134
+ 'next.dbUrlEmpty':
135
+ ' ⚠ DATABASE_URL está vacío — defínelo en .env antes de ejecutar.',
136
+ 'next.missingKeys': (keys) => ` ⚠ Faltan claves de API en .env: ${keys}`,
137
+ 'next.heading': '\nPróximos pasos:',
138
+ 'next.cmdChannelHint':
139
+ '\nEn otra terminal, abre una CLI para chatear con tu bot:',
140
+ 'next.happy': '\n¡Feliz desarrollo! 🚀\n',
141
+
142
+ 'agent.label.claude': 'Claude Code (.claude/skills)',
143
+ 'agent.label.codex': 'Codex (.codex/skills)',
144
+ 'agent.label.agents': 'Agentes genéricos (.agents/skills)',
145
+ },
146
+ }
147
+
148
+ export const SUPPORTED_LANGS = Object.keys(STRINGS)
149
+ export const DEFAULT_LANG = 'en'
150
+
151
+ export function resolveLang(lang) {
152
+ if (!lang) return DEFAULT_LANG
153
+ const normalized = String(lang).toLowerCase()
154
+ return SUPPORTED_LANGS.includes(normalized) ? normalized : DEFAULT_LANG
155
+ }
156
+
157
+ export function createTranslator(lang) {
158
+ const resolved = resolveLang(lang)
159
+ const dict = STRINGS[resolved]
160
+ const fallback = STRINGS[DEFAULT_LANG]
161
+
162
+ return function t(key, ...args) {
163
+ const value = dict[key] ?? fallback[key]
164
+ if (value === undefined) return key
165
+ return typeof value === 'function' ? value(...args) : value
166
+ }
167
+ }
@@ -2,7 +2,7 @@ import fs from 'fs/promises'
2
2
  import os from 'os'
3
3
  import path from 'path'
4
4
 
5
- import { getSkill, listSkillNames } from './registry.mjs'
5
+ import { getSkill, listSkillNames, listSkills } from './registry.mjs'
6
6
 
7
7
  const supportedAgents = {
8
8
  codex: ['.codex', 'skills'],
@@ -10,6 +10,65 @@ const supportedAgents = {
10
10
  agents: ['.agents', 'skills'],
11
11
  }
12
12
 
13
+ export function listSupportedAgents() {
14
+ return Object.keys(supportedAgents)
15
+ }
16
+
17
+ export async function installSkillsInProject(projectDir, agents, skillNames) {
18
+ if (!projectDir) {
19
+ throw new Error('installSkillsInProject: projectDir is required')
20
+ }
21
+
22
+ const resolvedAgents = (agents && agents.length > 0 ? agents : ['claude']).map((agent) => {
23
+ if (!supportedAgents[agent]) {
24
+ throw new Error(
25
+ `Unsupported agent "${agent}". Supported: ${Object.keys(supportedAgents).join(', ')}`,
26
+ )
27
+ }
28
+ return agent
29
+ })
30
+
31
+ const skills =
32
+ skillNames && skillNames.length > 0 ? skillNames.map((name) => requireSkill(name)) : listSkills()
33
+
34
+ const installations = []
35
+
36
+ for (const agent of resolvedAgents) {
37
+ const baseDir = path.join(projectDir, ...supportedAgents[agent])
38
+ await fs.mkdir(baseDir, { recursive: true })
39
+
40
+ for (const skill of skills) {
41
+ const installDir = path.join(baseDir, skill.name)
42
+ try {
43
+ await fs.cp(skill.sourceDir, installDir, {
44
+ recursive: true,
45
+ force: false,
46
+ errorOnExist: true,
47
+ })
48
+ installations.push({ agent, skill: skill.name, installDir, status: 'installed' })
49
+ } catch (error) {
50
+ if (error?.code === 'ERR_FS_CP_EEXIST' || error?.code === 'EEXIST') {
51
+ installations.push({ agent, skill: skill.name, installDir, status: 'skipped' })
52
+ continue
53
+ }
54
+ throw error
55
+ }
56
+ }
57
+ }
58
+
59
+ return { agents: resolvedAgents, skills: skills.map((skill) => skill.name), installations }
60
+ }
61
+
62
+ function requireSkill(name) {
63
+ const skill = getSkill(name)
64
+ if (!skill) {
65
+ throw new Error(
66
+ `Unknown skill "${name}". Available skills: ${listSkillNames().join(', ')}`,
67
+ )
68
+ }
69
+ return skill
70
+ }
71
+
13
72
  export async function installSkill(skillName, options, cwd = process.cwd()) {
14
73
  const skill = getSkill(skillName)
15
74