@ariasbruno/skillbase 0.2.0 → 1.1.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/src/core.js CHANGED
@@ -5,9 +5,38 @@ import * as readline from 'node:readline';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
6
  import { execFile } from 'node:child_process';
7
7
  import { promisify } from 'node:util';
8
- import { getGlobalSkillsDir, getProjectRoot, getProjectSkillsDir, PROJECT_AGENTS_DIR, PROJECT_SKILLS_DIR } from './config.js';
8
+ import {
9
+ getGlobalSkillsDir,
10
+ getProjectRoot,
11
+ getProjectSkillsDir,
12
+ PROJECT_AGENTS_DIR,
13
+ PROJECT_SKILLS_DIR,
14
+ exists
15
+ } from './config.js';
9
16
  import { readManifest, removeSkillFromManifest, upsertSkill, writeJson, writeManifest } from './manifest.js';
10
17
  import { detectProjectTechnologies } from './recommendations.js';
18
+ import { t } from './i18n.js';
19
+ import {
20
+ bold,
21
+ dim,
22
+ dim2,
23
+ text,
24
+ cyan,
25
+ green,
26
+ yellow,
27
+ S_DIAMOND,
28
+ S_POINTER,
29
+ S_STEP,
30
+ S_SQUARE,
31
+ S_SQUARE_FILL,
32
+ S_BAR,
33
+ S_BAR_START,
34
+ S_BAR_END,
35
+ H_HIDE_CURSOR,
36
+ H_SHOW_CURSOR,
37
+ H_CLEAR_DOWN,
38
+ H_MOVE_UP
39
+ } from './styles.js';
11
40
  const execFileAsync = promisify(execFile);
12
41
 
13
42
  function nowISO() {
@@ -20,13 +49,13 @@ export async function ensureDir(dir) {
20
49
  await fs.mkdir(dir, { recursive: true });
21
50
  }
22
51
 
23
- async function exists(target) {
24
- try {
25
- await fs.access(target);
26
- return true;
27
- } catch {
28
- return false;
29
- }
52
+ function sanitizeSkillName(name) {
53
+ if (typeof name !== 'string') return '';
54
+
55
+ return name
56
+ .replace(/[\\/]/g, '_')
57
+ .replace(/\.\./g, '')
58
+ .replace(/^[.~/]+/, '');
30
59
  }
31
60
 
32
61
  export async function listGlobalSkills() {
@@ -68,14 +97,15 @@ function compareVersion(a, b) {
68
97
  }
69
98
 
70
99
  export async function addSkill(skillName, { sym = false, cwd = process.cwd() } = {}) {
71
- const globalPath = path.join(getGlobalSkillsDir(), skillName);
100
+ const safeName = sanitizeSkillName(skillName);
101
+ const globalPath = path.join(getGlobalSkillsDir(), safeName);
72
102
  if (!(await exists(globalPath))) {
73
- throw new Error(`La skill global "${skillName}" no existe en ${getGlobalSkillsDir()}`);
103
+ throw new Error(t('ERR_GLOBAL_NOT_FOUND', { name: skillName, dir: getGlobalSkillsDir() }));
74
104
  }
75
105
 
76
106
  const projectSkillsDir = getProjectSkillsDir(cwd);
77
107
  await ensureDir(projectSkillsDir);
78
- const target = path.join(projectSkillsDir, skillName);
108
+ const target = path.join(projectSkillsDir, safeName);
79
109
 
80
110
  if (await exists(target)) {
81
111
  await fs.rm(target, { recursive: true, force: true });
@@ -100,21 +130,21 @@ export async function addSkillsInteractive({ cwd = process.cwd(), sym = false }
100
130
  const skills = await listGlobalSkills();
101
131
  if (!skills.length) return { selected: [], cancelled: false };
102
132
  const selection = await selectSkillsFromList(skills, {
103
- title: 'Selecciona skills para instalar',
104
- requireTTYMessage: 'La selección interactiva requiere una terminal TTY. Usa: skillbase add <skill>.'
133
+ title: t('UI_SELECT_MULTIPLE'),
134
+ requireTTYMessage: t('UI_REQUIRED_TTY')
105
135
  });
106
136
  if (selection.cancelled) return { selected: [], cancelled: true };
107
137
  const selectedSkills = selection.selected;
108
138
  for (const skill of selectedSkills) {
109
139
  await addSkill(skill, { cwd, sym });
110
140
  }
111
- output.write(`\nInstaladas: ${selectedSkills.join(', ') || 'ninguna'}\n`);
141
+ output.write(`\n${t('INIT_INSTALLED', { list: selectedSkills.join(', ') || t('INIT_NONE') })}\n`);
112
142
  return { selected: selectedSkills, cancelled: false };
113
143
  }
114
144
 
115
- async function selectSkillsFromList(skills, { title, requireTTYMessage } = {}) {
145
+ async function selectSkillsFromList(skills, { title, subtitle, requireTTYMessage } = {}) {
116
146
  if (!input.isTTY || !output.isTTY) {
117
- throw new Error(requireTTYMessage || 'La selección interactiva requiere una terminal TTY.');
147
+ throw new Error(requireTTYMessage || t('UI_REQUIRED_TTY'));
118
148
  }
119
149
 
120
150
  const selected = new Set();
@@ -123,93 +153,96 @@ async function selectSkillsFromList(skills, { title, requireTTYMessage } = {}) {
123
153
  let renderedLines = 0;
124
154
  let firstRender = true;
125
155
 
126
- // Tamaño de página adaptativo (mínimo 5, máximo 15 por defecto)
127
156
  const getPageSize = () => {
128
157
  const terminalRows = output.rows || 24;
129
- const reservedRows = 8; // Header (5) + Footer (2) + Margen (1)
130
- return Math.max(5, Math.min(15, terminalRows - reservedRows));
158
+ const reservedRows = 10;
159
+ return Math.max(5, Math.min(12, terminalRows - reservedRows));
131
160
  };
132
161
 
133
162
  readline.emitKeypressEvents(input);
134
163
  input.resume();
135
164
  if (typeof input.setRawMode === 'function') input.setRawMode(true);
136
165
 
166
+ output.write(H_HIDE_CURSOR);
167
+
137
168
  const render = () => {
138
169
  const pageSize = getPageSize();
139
-
140
- // Ajustar ventana (offset) según el cursor
141
- if (cursor < offset) {
142
- offset = cursor;
143
- } else if (cursor >= offset + pageSize) {
144
- offset = cursor - pageSize + 1;
145
- }
170
+
171
+ if (cursor < offset) offset = cursor;
172
+ else if (cursor >= offset + pageSize) offset = cursor - pageSize + 1;
146
173
 
147
174
  const lines = [
148
- '\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m',
149
- `\x1b[1m${title || 'Selecciona skills'}\x1b[0m`,
150
- '\x1b[2m↑/↓ navegar · espacio seleccionar · enter confirmar · esc cancelar\x1b[0m',
151
- '\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m',
152
- ''
175
+ `${cyan(S_BAR_START)} ${bold(title || t('UI_SELECT_MULTIPLE'))}`,
176
+ `${cyan(S_BAR)} ${dim2(subtitle || t('UI_AVAILABLE', { count: skills.length }))}`,
177
+ `${cyan(S_BAR)}`
153
178
  ];
154
179
 
155
- // Indicador superior si hay más arriba
156
- if (offset > 0) {
157
- lines.push('\x1b[2m ▲ y más...\x1b[0m');
158
- } else {
159
- lines.push('');
160
- }
161
-
162
180
  const visibleSkills = skills.slice(offset, offset + pageSize);
163
181
  visibleSkills.forEach((skill, index) => {
164
182
  const realIndex = index + offset;
165
183
  const isSelected = selected.has(skill);
166
184
  const isCursor = realIndex === cursor;
167
- const mark = isSelected ? '[x]' : '[ ]';
168
- const pointer = isCursor ? '\x1b[32m❯\x1b[0m' : ' ';
169
- const line = `${mark} ${skill}`;
170
- lines.push(isCursor ? `${pointer} \x1b[1m${line}\x1b[0m` : `${pointer} ${line}`);
185
+
186
+ const check = isSelected
187
+ ? cyan(S_SQUARE_FILL)
188
+ : dim2(S_SQUARE);
189
+
190
+ const pointer = isCursor
191
+ ? cyan(S_POINTER)
192
+ : ' ';
193
+
194
+ // Formatear nombre: owner › name (si tiene /)
195
+ let label = skill;
196
+ if (skill.includes('/')) {
197
+ const [owner, ...nameParts] = skill.split('/');
198
+ const name = nameParts.join('/');
199
+ const ownerPart = isCursor ? bold(owner) : dim2(owner);
200
+ const namePart = isSelected ? cyan(name) : (isCursor ? bold(name) : text(name));
201
+ label = `${ownerPart} ${dim2(S_STEP)} ${namePart}`;
202
+ } else {
203
+ label = isCursor ? bold(skill) : (isSelected ? cyan(skill) : text(skill));
204
+ }
205
+
206
+ lines.push(`${cyan(S_BAR)} ${pointer} ${check} ${label}`);
171
207
  });
172
208
 
173
- // Rellenar hasta pageSize para mantener altura constante (evita parpadeos)
174
209
  for (let i = visibleSkills.length; i < pageSize; i += 1) {
175
- lines.push('');
210
+ lines.push(`${cyan(S_BAR)}`);
176
211
  }
177
212
 
178
- // Indicador inferior si hay más abajo
179
- if (offset + pageSize < skills.length) {
180
- lines.push('\x1b[2m ▼ y más...\x1b[0m');
181
- } else {
182
- lines.push('');
183
- }
213
+ lines.push(`${cyan(S_BAR)}`);
214
+ const status = `(${selected.size}/${skills.length})`;
215
+ const page = t('UI_PAGE', { current: Math.floor(offset / pageSize) + 1, total: Math.ceil(skills.length / pageSize) });
216
+ const controls = t('UI_CONTROLS', { status });
217
+ lines.push(`${cyan(S_BAR_END)} ${dim2(`${page} · ${controls}`)}`);
184
218
 
219
+ const outputContent = lines.join('\n');
185
220
  if (!firstRender) {
186
- readline.moveCursor(output, 0, -renderedLines);
221
+ output.write('\r' + H_MOVE_UP(renderedLines));
187
222
  }
223
+ output.write(H_CLEAR_DOWN + outputContent);
188
224
 
189
- for (let i = 0; i < lines.length; i += 1) {
190
- readline.clearLine(output, 0);
191
- readline.cursorTo(output, 0);
192
- output.write(lines[i]);
193
- if (i < lines.length - 1) output.write('\n');
194
- }
195
-
196
- // Limpiar líneas residuales si el tamaño de renderizado cambió (vía redimensionado de terminal)
197
- if (!firstRender && renderedLines > lines.length) {
198
- for (let i = lines.length; i < renderedLines; i += 1) {
199
- output.write('\n');
200
- readline.clearLine(output, 0);
201
- }
202
- }
203
-
204
- output.write('\n');
205
- renderedLines = lines.length + 1;
225
+ renderedLines = lines.length - 1;
206
226
  firstRender = false;
207
227
  };
208
228
 
209
229
  render();
230
+ const cleanup = () => {
231
+ output.write('\n' + H_SHOW_CURSOR);
232
+ if (typeof input.setRawMode === 'function') input.setRawMode(false);
233
+ input.pause();
234
+ input.removeListener('keypress', onKeypress);
235
+ };
236
+
210
237
  let onKeypress;
211
238
  const outcome = await new Promise((resolve) => {
212
239
  onKeypress = (_, key) => {
240
+ if (!key) return;
241
+ if (key.ctrl && key.name === 'c') {
242
+ cleanup();
243
+ resolve({ cancelled: true });
244
+ return;
245
+ }
213
246
  if (key.name === 'up') {
214
247
  cursor = (cursor - 1 + skills.length) % skills.length;
215
248
  render();
@@ -227,22 +260,28 @@ async function selectSkillsFromList(skills, { title, requireTTYMessage } = {}) {
227
260
  render();
228
261
  return;
229
262
  }
263
+ if (key.name === 'a') {
264
+ if (selected.size === skills.length) {
265
+ selected.clear();
266
+ } else {
267
+ skills.forEach((s) => selected.add(s));
268
+ }
269
+ render();
270
+ return;
271
+ }
230
272
  if (key.name === 'return' || key.name === 'enter') {
273
+ cleanup();
231
274
  resolve({ cancelled: false });
232
275
  return;
233
276
  }
234
- if (key.name === 'escape' || (key.name === 'c' && key.ctrl)) {
277
+ if (key.name === 'escape') {
278
+ cleanup();
235
279
  resolve({ cancelled: true });
236
280
  }
237
281
  };
238
282
  input.on('keypress', onKeypress);
239
283
  });
240
284
 
241
- if (onKeypress) input.off('keypress', onKeypress);
242
- if (typeof input.setRawMode === 'function') input.setRawMode(false);
243
- input.pause();
244
- output.write('\n');
245
-
246
285
  if (outcome.cancelled) {
247
286
  return { selected: [], cancelled: true };
248
287
  }
@@ -285,7 +324,7 @@ function parseRemoteSkillRef(skillRef) {
285
324
  async function fetchRemoteMetadata(skillRef) {
286
325
  const parsed = parseRemoteSkillRef(skillRef);
287
326
  if (!parsed.lookupKeys.length) {
288
- throw new Error('Debes indicar una skill remota válida.');
327
+ throw new Error(t('ERR_REMOTE_INVALID'));
289
328
  }
290
329
 
291
330
  for (const key of parsed.lookupKeys) {
@@ -308,7 +347,7 @@ async function fetchRemoteMetadata(skillRef) {
308
347
  }
309
348
 
310
349
  throw new Error(
311
- `No se pudo obtener metadata remota para "${skillRef}". Usa slug tipo "owner/skill" o URL de skills.sh.`
350
+ t('ERR_REMOTE_METADATA', { ref: skillRef })
312
351
  );
313
352
  }
314
353
 
@@ -319,12 +358,12 @@ async function downloadSkillFromRemote(skillRef, tmpDir) {
319
358
  const sourceUrl = metadata.downloadUrl || metadata.sourceUrl || metadata.repo || metadata.url;
320
359
 
321
360
  if (!sourceUrl) {
322
- throw new Error(`La metadata remota de "${skillRef}" no contiene downloadUrl/sourceUrl/repo/url`);
361
+ throw new Error(t('ERR_REMOTE_NO_URL', { ref: skillRef }));
323
362
  }
324
363
 
325
364
  if (metadata.archiveUrl) {
326
365
  const archiveResponse = await fetch(metadata.archiveUrl);
327
- if (!archiveResponse.ok) throw new Error(`No se pudo descargar archiveUrl (${archiveResponse.status})`);
366
+ if (!archiveResponse.ok) throw new Error(t('ERR_REMOTE_DOWNLOAD', { status: archiveResponse.status }));
328
367
  const archivePath = path.join(tmpDir, `${localName}.tgz`);
329
368
  const buffer = Buffer.from(await archiveResponse.arrayBuffer());
330
369
  await fs.writeFile(archivePath, buffer);
@@ -347,7 +386,7 @@ export async function installRemoteSkill(skillName, { cwd = process.cwd(), force
347
386
  await ensureDir(projectSkillsDir);
348
387
 
349
388
  if (/^https?:\/\/github\.com\//i.test(skillName)) {
350
- throw new Error('Para instalar desde GitHub usa: skillbase install <repo-url> --remote --skill <nombre-skill>.');
389
+ throw new Error(t('ERR_GITHUB_USAGE'));
351
390
  }
352
391
 
353
392
  const tempBase = await fs.mkdtemp(path.join(os.tmpdir(), 'skillbase-'));
@@ -355,7 +394,7 @@ export async function installRemoteSkill(skillName, { cwd = process.cwd(), force
355
394
  const downloaded = await downloadSkillFromRemote(skillName, tempBase);
356
395
  const target = path.join(projectSkillsDir, downloaded.localName);
357
396
  if ((await exists(target)) && !force) {
358
- throw new Error(`La skill "${downloaded.localName}" ya existe en el proyecto. Usa --force para reinstalar.`);
397
+ throw new Error(t('ERR_SKILL_EXISTS', { name: downloaded.localName }));
359
398
  }
360
399
  if (await exists(target)) await fs.rm(target, { recursive: true, force: true });
361
400
  await copyDir(downloaded.path, target);
@@ -378,7 +417,7 @@ export async function installRemoteSkill(skillName, { cwd = process.cwd(), force
378
417
 
379
418
  async function installRemoteFromGitHub(repoUrl, selectedSkill, { cwd = process.cwd(), force = false } = {}) {
380
419
  if (!selectedSkill) {
381
- throw new Error('Falta --skill <nombre>. Ejemplo: skillbase install <repo-url> --remote --skill find-skills');
420
+ throw new Error(t('ERR_SKILL_REQUIRED'));
382
421
  }
383
422
 
384
423
  const projectSkillsDir = getProjectSkillsDir(cwd);
@@ -397,12 +436,12 @@ async function installRemoteFromGitHub(repoUrl, selectedSkill, { cwd = process.c
397
436
  }
398
437
  }
399
438
  if (!sourcePath) {
400
- throw new Error(`No se encontró la skill "${selectedSkill}" en el repo remoto.`);
439
+ throw new Error(t('ERR_SKILL_NOT_FOUND_REMOTE', { name: selectedSkill }));
401
440
  }
402
441
 
403
442
  const target = path.join(projectSkillsDir, selectedSkill);
404
443
  if ((await exists(target)) && !force) {
405
- throw new Error(`La skill "${selectedSkill}" ya existe en el proyecto. Usa --force para reinstalar.`);
444
+ throw new Error(t('ERR_SKILL_EXISTS', { name: selectedSkill }));
406
445
  }
407
446
  if (await exists(target)) await fs.rm(target, { recursive: true, force: true });
408
447
  await copyDir(sourcePath, target);
@@ -433,7 +472,7 @@ export async function installRemoteSkillRef(skillRef, options = {}) {
433
472
  export async function installFromManifest({ cwd = process.cwd(), remote = false, force = false } = {}) {
434
473
  const manifest = await readManifest(cwd);
435
474
  if (!manifest.skills.length) {
436
- throw new Error('No hay skills en skillbase.json. Usa "skillbase add <skill>" o "skillbase install <skill> --remote".');
475
+ throw new Error(t('ERR_MANIFEST_EMPTY'));
437
476
  }
438
477
  for (const skill of manifest.skills) {
439
478
  if (remote || skill.source === 'remote') await installRemoteSkill(skill.name, { cwd, force });
@@ -442,12 +481,13 @@ export async function installFromManifest({ cwd = process.cwd(), remote = false,
442
481
  }
443
482
 
444
483
  export async function removeSkill(skillName, { cwd = process.cwd(), global = false } = {}) {
484
+ const safeName = sanitizeSkillName(skillName);
445
485
  if (global) {
446
- await fs.rm(path.join(getGlobalSkillsDir(), skillName), { recursive: true, force: true });
486
+ await fs.rm(path.join(getGlobalSkillsDir(), safeName), { recursive: true, force: true });
447
487
  return;
448
488
  }
449
489
 
450
- await fs.rm(path.join(getProjectSkillsDir(cwd), skillName), { recursive: true, force: true });
490
+ await fs.rm(path.join(getProjectSkillsDir(cwd), safeName), { recursive: true, force: true });
451
491
  const manifest = await readManifest(cwd);
452
492
  removeSkillFromManifest(manifest, skillName);
453
493
  await writeManifest(manifest, cwd);
@@ -542,8 +582,8 @@ export async function initProject({ cwd = process.cwd(), hard = false } = {}) {
542
582
 
543
583
  const selection = await selectSkillsFromList(suggested, {
544
584
  title: hard
545
- ? 'Init --hard: selecciona skills recomendadas por nombre y tags'
546
- : 'Init: selecciona skills recomendadas por nombre'
585
+ ? t('INIT_HARD_TITLE')
586
+ : t('INIT_TITLE')
547
587
  });
548
588
  if (selection.cancelled) return { technologies, suggested, installed: [], cancelled: true };
549
589
 
package/src/i18n.js ADDED
@@ -0,0 +1,34 @@
1
+ import { getConfig } from './config.js';
2
+ import { en } from './locales/en.js';
3
+ import { es } from './locales/es.js';
4
+
5
+ const locales = { en, es };
6
+
7
+ let currentLang = getConfig().lang || 'en';
8
+
9
+ /**
10
+ * Cambia el idioma actual en memoria
11
+ * @param {string} lang 'en' | 'es'
12
+ */
13
+ export function setLanguage(lang) {
14
+ if (locales[lang]) {
15
+ currentLang = lang;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Traduce una clave al idioma actual
21
+ * @param {string} key Clave del diccionario
22
+ * @param {Object} params Parámetros para reemplazar en la cadena ({key: value})
23
+ * @returns {string} Texto traducido
24
+ */
25
+ export function t(key, params = {}) {
26
+ const dict = locales[currentLang] || locales.en;
27
+ let textStr = dict[key] || locales.en[key] || key;
28
+
29
+ for (const [k, v] of Object.entries(params)) {
30
+ textStr = textStr.replace(new RegExp(`{${k}}`, 'g'), v);
31
+ }
32
+
33
+ return textStr;
34
+ }
@@ -0,0 +1,104 @@
1
+ export const en = {
2
+ // General
3
+ USAGE: 'Usage:',
4
+ COMMANDS: 'Commands:',
5
+ OPTIONS: 'Common Options:',
6
+ SHORTCUTS: 'Shortcuts: l=ls, h=-h, a=add, i=install, rm=remove, c=check, up=update, m=migrate',
7
+ HELP_FOOTER: 'Use {cmd} to see all options and commands.',
8
+ USE_HELP: 'Use {cmd} to see all options and commands.',
9
+ CANCELLED: 'Operation cancelled.',
10
+ COMMANDS_AVAILABLE: 'Available commands:',
11
+ SELECTION_CANCELLED: 'Selection cancelled.',
12
+ UNKNOWN_COMMAND: 'Unknown command: {cmd}. Use skillbase -h',
13
+
14
+ // Commands info
15
+ DESC_INIT: 'Initialize project and detect stack [--hard]',
16
+ DESC_INIT_SHORT: 'Configure project and detect stack',
17
+ DESC_ADD: 'Add one or more skills to the project',
18
+ DESC_ADD_SHORT: 'Add new skill (global/project)',
19
+ DESC_LS: 'List installed skills (local or global with -g)',
20
+ DESC_LS_SHORT: 'List installed skills',
21
+ DESC_INSTALL: 'Install from manifest or remote [-r] [-k <name>]',
22
+ DESC_INSTALL_SHORT: 'Install dependencies from manifest',
23
+ DESC_REMOVE: 'Remove a skill from project or global [-g]',
24
+ DESC_CHECK: 'Check for updates for installed skills',
25
+ DESC_UPDATE: 'Update one or all skills',
26
+ DESC_MIGRATE: 'Migrate (~/.agents) or promote (-p) local skills to global',
27
+ DESC_MIGRATE_SHORT: 'Migrate or promote local skills',
28
+ DESC_LANG: 'Change tool language (en|es)',
29
+ DESC_LANG_SHORT: 'Change CLI language',
30
+
31
+ // Options info
32
+ OPT_REMOTE: 'Operate with remote repositories (GitHub/GitLab)',
33
+ OPT_FORCE: 'Force operation (overwrite files)',
34
+ OPT_SYM: 'Create symbolic links instead of copying (add only)',
35
+ OPT_GLOBAL: 'Operate globally (for ls and remove)',
36
+ OPT_HELP: 'Show this help',
37
+
38
+ // Command-specific messages
39
+ LS_GLOBAL_EMPTY: 'No global skills installed in {dir}',
40
+ LS_GLOBAL_TITLE: 'Global skills ({dir}):',
41
+ LS_PROJECT_EMPTY: 'No local skills installed in {dir}',
42
+ LS_PROJECT_TITLE: 'Project skills ({dir}):',
43
+
44
+ INIT_NO_STACK: 'No stack detected in the project.',
45
+ INIT_ANALYSIS: 'Stack analysis:',
46
+ INIT_RESUMEN: 'Summary:',
47
+ INIT_INSTALLED: 'Installed: {list}',
48
+ INIT_NONE: 'No skills were installed.',
49
+
50
+ ADD_NO_SELECTED: 'No skills selected to install.',
51
+ ADD_RESUMEN: 'Summary:',
52
+ ADD_SUCCESS: 'Skill "{skill}" installed.',
53
+ ADD_REMOTE_SUCCESS: 'Remote skill "{skill}" installed.',
54
+
55
+ REMOVE_GLOBAL_SUCCESS: 'Global skill "{skill}" removed.',
56
+ REMOVE_PROJECT_SUCCESS: 'Project skill "{skill}" removed.',
57
+ REMOVE_NOT_FOUND: 'Skill "{skill}" is not installed.',
58
+
59
+ CHECK_SEARCHING: 'Checking for updates...',
60
+ CHECK_UP_TO_DATE: 'All skills are up to date.',
61
+ CHECK_UPDATES_FOUND: 'Updates available:',
62
+ CHECK_UPDATE_HINT: 'Use {cmd} to update.',
63
+
64
+ UPDATE_START: 'Updating skills...',
65
+ UPDATE_SUCCESS: 'All skills updated.',
66
+ UPDATE_SINGLE_SUCCESS: 'Skill "{skill}" updated.',
67
+
68
+ MIGRATE_TITLE: 'Skills Migration',
69
+ MIGRATE_SOURCE: 'Source: {dir}',
70
+ MIGRATE_FOUND: 'Skills found: {count}',
71
+ MIGRATE_SUCCESS_LIST: 'Skills migrated to {dir}:',
72
+ MIGRATE_SKIPPED_LIST: 'Skipped (already exist):',
73
+ MIGRATE_RESUMEN_TITLE: 'Summary:',
74
+ MIGRATE_RESUMEN_SUCCESS: '{count} skills migrated successfully.',
75
+ MIGRATE_RESUMEN_DEST: 'Destination: {dir}',
76
+ MIGRATE_NONE: 'No new skills were migrated.',
77
+
78
+ LANG_SUCCESS: 'Language changed to {lang} successfully.',
79
+ LANG_INVALID: 'Unsupported language: {lang}. Use "en" or "es".',
80
+
81
+ // Interactive UI
82
+ UI_SELECT_ONE: 'Select a skill',
83
+ UI_SELECT_MULTIPLE: 'Select skills',
84
+ UI_CONTROLS: '↑↓ navigate • [space] sel. {status} • [a] all • [enter] install • [esc] exit',
85
+ UI_SELECTED: 'selected',
86
+ UI_PAGE: 'page {current}/{total}',
87
+ UI_AVAILABLE: '{count} skills available',
88
+ UI_REQUIRED_TTY: 'Interactive selection requires a TTY terminal. Use: skillbase add <skill>.',
89
+
90
+ // Core / Errors
91
+ ERR_GLOBAL_NOT_FOUND: 'Global skill "{name}" does not exist in {dir}',
92
+ ERR_REMOTE_INVALID: 'You must specify a valid remote skill.',
93
+ ERR_REMOTE_METADATA: 'Could not fetch remote metadata for "{ref}". Use slug like "owner/skill" or skills.sh URL.',
94
+ ERR_REMOTE_NO_URL: 'Remote metadata for "{ref}" does not contain downloadUrl/sourceUrl/repo/url',
95
+ ERR_REMOTE_DOWNLOAD: 'Could not download archiveUrl ({status})',
96
+ ERR_GITHUB_USAGE: 'To install from GitHub use: skillbase install <repo-url> --remote --skill <skill-name>.',
97
+ ERR_SKILL_EXISTS: 'Skill "{name}" already exists in the project. Use --force to reinstall.',
98
+ ERR_SKILL_REQUIRED: 'Missing --skill <name>. Example: skillbase install <repo-url> --remote --skill find-skills',
99
+ ERR_SKILL_NOT_FOUND_REMOTE: 'Skill "{name}" not found in remote repo.',
100
+ ERR_MANIFEST_EMPTY: 'No skills in skillbase.json. Use "skillbase add <skill>" or "skillbase install <skill> --remote".',
101
+
102
+ INIT_HARD_TITLE: 'Init --hard: select skills recommended by name and tags',
103
+ INIT_TITLE: 'Init: select skills recommended by name',
104
+ };
@@ -0,0 +1,104 @@
1
+ export const es = {
2
+ // General
3
+ USAGE: 'Uso:',
4
+ COMMANDS: 'Comandos:',
5
+ OPTIONS: 'Opciones comunes:',
6
+ SHORTCUTS: 'Atajos: l=ls, h=-h, a=add, i=install, rm=remove, c=check, up=update, m=migrate',
7
+ HELP_FOOTER: 'Usa {cmd} para ver todas las opciones y comandos.',
8
+ USE_HELP: 'Usa {cmd} para ver todas las opciones y comandos.',
9
+ CANCELLED: 'Operación cancelada.',
10
+ COMMANDS_AVAILABLE: 'Comandos disponibles:',
11
+ SELECTION_CANCELLED: 'Selección cancelada.',
12
+ UNKNOWN_COMMAND: 'Comando desconocido: {cmd}. Usa skillbase -h',
13
+
14
+ // Commands info
15
+ DESC_INIT: 'Inicializa el proyecto y detecta el stack [--hard]',
16
+ DESC_INIT_SHORT: 'Configura proyecto y detecta stack',
17
+ DESC_ADD: 'Añade una o varias skills al proyecto',
18
+ DESC_ADD_SHORT: 'Añadir nueva skill (global/proyecto)',
19
+ DESC_LS: 'Lista las skills instaladas (local o global con -g)',
20
+ DESC_LS_SHORT: 'Listar skills instaladas',
21
+ DESC_INSTALL: 'Instala desde el manifiesto o remoto [-r] [-k <nombre>]',
22
+ DESC_INSTALL_SHORT: 'Instalar dependencias del manifiesto',
23
+ DESC_REMOVE: 'Elimina una skill del proyecto o global [-g]',
24
+ DESC_CHECK: 'Busca actualizaciones de las skills instaladas',
25
+ DESC_UPDATE: 'Actualiza una o todas las skills',
26
+ DESC_MIGRATE: 'Migra (~/.agents) o promueve (-p) skills locales a global',
27
+ DESC_MIGRATE_SHORT: 'Migrar o promover skills locales',
28
+ DESC_LANG: 'Cambia el idioma de la herramienta (en|es)',
29
+ DESC_LANG_SHORT: 'Cambiar idioma de la CLI',
30
+
31
+ // Options info
32
+ OPT_REMOTE: 'Operar con repositorios remotos (GitHub/GitLab)',
33
+ OPT_FORCE: 'Forzar la operación (sobrescribir archivos)',
34
+ OPT_SYM: 'Crear enlaces simbólicos en vez de copiar (solo add)',
35
+ OPT_GLOBAL: 'Operar de forma global (para ls y remove)',
36
+ OPT_HELP: 'Mostrar esta ayuda',
37
+
38
+ // Command-specific messages
39
+ LS_GLOBAL_EMPTY: 'No hay skills globales instaladas en {dir}',
40
+ LS_GLOBAL_TITLE: 'Skills globales ({dir}):',
41
+ LS_PROJECT_EMPTY: 'No hay skills instaladas localmente en {dir}',
42
+ LS_PROJECT_TITLE: 'Skills del proyecto ({dir}):',
43
+
44
+ INIT_NO_STACK: 'No se detectó stack en el proyecto.',
45
+ INIT_ANALYSIS: 'Análisis de stack:',
46
+ INIT_RESUMEN: 'Resumen:',
47
+ INIT_INSTALLED: 'Instaladas: {list}',
48
+ INIT_NONE: 'No se instaló ninguna skill.',
49
+
50
+ ADD_NO_SELECTED: 'No se seleccionaron skills para instalar.',
51
+ ADD_RESUMEN: 'Resumen:',
52
+ ADD_SUCCESS: 'Skill "{skill}" instalada.',
53
+ ADD_REMOTE_SUCCESS: 'Skill remota "{skill}" instalada.',
54
+
55
+ REMOVE_GLOBAL_SUCCESS: 'Skill global "{skill}" eliminada.',
56
+ REMOVE_PROJECT_SUCCESS: 'Skill del proyecto "{skill}" eliminada.',
57
+ REMOVE_NOT_FOUND: 'La skill "{skill}" no está instalada.',
58
+
59
+ CHECK_SEARCHING: 'Buscando actualizaciones...',
60
+ CHECK_UP_TO_DATE: 'Todas las skills están al día.',
61
+ CHECK_UPDATES_FOUND: 'Hay actualizaciones disponibles:',
62
+ CHECK_UPDATE_HINT: 'Usa {cmd} para actualizar.',
63
+
64
+ UPDATE_START: 'Actualizando skills...',
65
+ UPDATE_SUCCESS: 'Todas las skills actualizadas.',
66
+ UPDATE_SINGLE_SUCCESS: 'Skill "{skill}" actualizada.',
67
+
68
+ MIGRATE_TITLE: 'Migración de skills',
69
+ MIGRATE_SOURCE: 'Origen: {dir}',
70
+ MIGRATE_FOUND: 'Skills encontradas: {count}',
71
+ MIGRATE_SUCCESS_LIST: 'Skills migradas a {dir}:',
72
+ MIGRATE_SKIPPED_LIST: 'Omitidas (ya existen):',
73
+ MIGRATE_RESUMEN_TITLE: 'Resumen:',
74
+ MIGRATE_RESUMEN_SUCCESS: '{count} skills migradas con éxito.',
75
+ MIGRATE_RESUMEN_DEST: 'Destino: {dir}',
76
+ MIGRATE_NONE: 'No se migraron nuevas skills.',
77
+
78
+ LANG_SUCCESS: 'Idioma cambiado a {lang} con éxito.',
79
+ LANG_INVALID: 'Idioma no soportado: {lang}. Usa "en" o "es".',
80
+
81
+ // Interfaz Interactiva
82
+ UI_SELECT_ONE: 'Seleccionar una skill',
83
+ UI_SELECT_MULTIPLE: 'Seleccionar skills',
84
+ UI_CONTROLS: '↑↓ navegar • [espacio] sel. {status} • [a] todo • [enter] instalar • [esc] salir',
85
+ UI_SELECTED: 'seleccionadas',
86
+ UI_PAGE: 'pág. {current}/{total}',
87
+ UI_AVAILABLE: '{count} skills disponibles',
88
+ UI_REQUIRED_TTY: 'La selección interactiva requiere una terminal TTY. Usa: skillbase add <skill>.',
89
+
90
+ // Core / Errores
91
+ ERR_GLOBAL_NOT_FOUND: 'La skill global "{name}" no existe en {dir}',
92
+ ERR_REMOTE_INVALID: 'Debes indicar una skill remota válida.',
93
+ ERR_REMOTE_METADATA: 'No se pudo obtener metadata remota para "{ref}". Usa slug tipo "owner/skill" o URL de skills.sh.',
94
+ ERR_REMOTE_NO_URL: 'La metadata remota de "{ref}" no contiene downloadUrl/sourceUrl/repo/url',
95
+ ERR_REMOTE_DOWNLOAD: 'No se pudo descargar archiveUrl ({status})',
96
+ ERR_GITHUB_USAGE: 'Para instalar desde GitHub usa: skillbase install <repo-url> --remote --skill <nombre-skill>.',
97
+ ERR_SKILL_EXISTS: 'La skill "{name}" ya existe en el proyecto. Usa --force para reinstalar.',
98
+ ERR_SKILL_REQUIRED: 'Falta --skill <nombre>. Ejemplo: skillbase install <repo-url> --remote --skill find-skills',
99
+ ERR_SKILL_NOT_FOUND_REMOTE: 'No se encontró la skill "{name}" en el repo remoto.',
100
+ ERR_MANIFEST_EMPTY: 'No hay skills en skillbase.json. Usa "skillbase add <skill>" o "skillbase install <skill> --remote".',
101
+
102
+ INIT_HARD_TITLE: 'Init --hard: selecciona skills recomendadas por nombre y tags',
103
+ INIT_TITLE: 'Init: selecciona skills recomendadas por nombre',
104
+ };
package/src/manifest.js CHANGED
@@ -1,19 +1,15 @@
1
1
  import fs from 'node:fs/promises';
2
- import { getManifestPath } from './config.js';
3
-
4
- async function exists(target) {
5
- try {
6
- await fs.access(target);
7
- return true;
8
- } catch {
9
- return false;
10
- }
11
- }
2
+ import { getManifestPath, exists } from './config.js';
12
3
 
13
4
  async function safeReadJson(file, fallback) {
14
5
  if (!(await exists(file))) return fallback;
15
- const raw = await fs.readFile(file, 'utf8');
16
- return JSON.parse(raw);
6
+ try {
7
+ const raw = await fs.readFile(file, 'utf8');
8
+ return JSON.parse(raw);
9
+ } catch (e) {
10
+ console.error(`Warning: Failed to parse JSON at ${file}. Using fallback.`);
11
+ return fallback;
12
+ }
17
13
  }
18
14
 
19
15
  export async function writeJson(file, data) {