g360-cli 1.12.0 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  import { manifest } from '../lib/manifest.js';
6
6
  import { progress } from '../lib/progress.js';
7
7
  import { setSkill } from './set-skill.js';
8
+ import { bring } from './bring.js';
8
9
  import inquirer from 'inquirer';
9
10
 
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -35,7 +36,8 @@ export async function init(name, options) {
35
36
  dir = '.',
36
37
  dryRun = false,
37
38
  force = false,
38
- portable = null
39
+ portable = null,
40
+ brand = false,
39
41
  } = options;
40
42
 
41
43
  const targetDir = path.join(process.cwd(), dir, name);
@@ -127,16 +129,56 @@ export async function init(name, options) {
127
129
  if (wantPortable) {
128
130
  console.log(chalk.yellow('📦 Versión portable habilitada\n'));
129
131
  }
130
- console.log(chalk.gray('Next steps:'));
132
+
133
+ let appliedBrand = false;
134
+ if (brand) {
135
+ appliedBrand = true;
136
+ console.log(chalk.bold.cyan('\n🔖 Aplicando marca G360\n'));
137
+ await applyBrandToProject(targetDir, skill, dryRun);
138
+ } else if (!dryRun) {
139
+ const answers = await inquirer.prompt([
140
+ {
141
+ type: 'confirm',
142
+ name: 'applyBrand',
143
+ message: '¿Deseas aplicar la marca G360 (logo, colores, firma) al proyecto?',
144
+ default: true,
145
+ },
146
+ ]);
147
+ if (answers.applyBrand) {
148
+ appliedBrand = true;
149
+ console.log(chalk.bold.cyan('\n🔖 Aplicando marca G360\n'));
150
+ await applyBrandToProject(targetDir, skill, dryRun);
151
+ }
152
+ }
153
+
154
+ console.log(chalk.gray('\nNext steps:'));
131
155
  console.log(` ${chalk.cyan('cd')} ${name}`);
132
- console.log(` ${chalk.cyan('g360 bring')}`);
133
- console.log(` ${chalk.cyan('g360 present')}\n`);
156
+ console.log(` ${chalk.cyan('g360 present')}`);
157
+ if (!appliedBrand) {
158
+ console.log(` ${chalk.cyan('g360 bring brand')} # Aplicar marca G360`);
159
+ }
160
+ console.log(` ${chalk.cyan('g360 audit')}`);
161
+ console.log();
134
162
  } catch (error) {
135
163
  progressBar.stop();
136
164
  console.error(chalk.red(`\n❌ Error: ${error.message}`));
137
165
  }
138
166
  }
139
167
 
168
+ async function applyBrandToProject(targetDir, skill, dryRun) {
169
+ const brandName = skill?.includes('cipsa') ? 'cipsa' : 'g360';
170
+ if (dryRun) {
171
+ console.log(chalk.gray(` [dry-run] Would apply brand: ${brandName}`));
172
+ return;
173
+ }
174
+ try {
175
+ await bring(brandName, { path: targetDir, dryRun: false, force: false });
176
+ console.log(chalk.green(` ✅ Marca "${brandName}" aplicada`));
177
+ } catch (error) {
178
+ console.log(chalk.yellow(` ⚠ No se pudo aplicar la marca: ${error.message}`));
179
+ }
180
+ }
181
+
140
182
  async function createG360Structure(projectPath, assetsDir) {
141
183
  try {
142
184
  const g360Dir = path.join(projectPath, 'g360');
@@ -0,0 +1,578 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ const SEVERITY = {
9
+ CRITICAL: 'critical',
10
+ IMPORTANT: 'important',
11
+ MINOR: 'minor',
12
+ };
13
+
14
+ const JS_NAMING_RULES = {
15
+ function: { pattern: /^[a-z][a-zA-Z0-9]*$/, name: 'camelCase' },
16
+ class: { pattern: /^[A-Z][a-zA-Z0-9]*$/, name: 'PascalCase' },
17
+ variable: { pattern: /^[a-z][a-zA-Z0-9]*$/, name: 'camelCase' },
18
+ constant: { pattern: /^[A-Z][A-Z0-9_]*$/, name: 'UPPER_SNAKE_CASE' },
19
+ file: { pattern: /^[a-z][a-zA-Z0-9]*\.js$/, name: 'camelCase.js' },
20
+ };
21
+
22
+ const PYTHON_NAMING_RULES = {
23
+ function: { pattern: /^[a-z][a-z0-9_]*$/, name: 'snake_case' },
24
+ class: { pattern: /^[A-Z][a-zA-Z0-9]*$/, name: 'PascalCase' },
25
+ variable: { pattern: /^[a-z][a-z0-9_]*$/, name: 'snake_case' },
26
+ constant: { pattern: /^[A-Z][A-Z0-9_]*$/, name: 'UPPER_SNAKE_CASE' },
27
+ file: { pattern: /^[a-z][a-z0-9_]*\.py$/, name: 'snake_case.py' },
28
+ };
29
+
30
+ const GENERIC_NAMES = ['result', 'data', 'info', 'val', 'obj', 'tmp', 'aux', 'value', 'x', 'y', 'z', 'item', 'elem', 'entry', 'output', 'input', 'res', 'dt'];
31
+
32
+ export async function lint(targetPath, options) {
33
+ const { project = '.', level = 'all' } = options;
34
+ const targetDir = path.join(process.cwd(), project);
35
+
36
+ if (!fs.existsSync(targetDir)) {
37
+ console.error(chalk.red(`❌ Directorio no encontrado: ${targetDir}`));
38
+ return;
39
+ }
40
+
41
+ console.log(chalk.bold.cyan('\n🔍 G360 Lint — Naming & Consistency Review\n'));
42
+ console.log(chalk.gray(`Path: ${targetDir}\n`));
43
+
44
+ const findings = [];
45
+
46
+ if (level === 'all' || level === 'naming') {
47
+ findings.push(...checkNamingConventions(targetDir));
48
+ }
49
+
50
+ if (level === 'all' || level === 'duplicates') {
51
+ findings.push(...checkDuplicateFunctions(targetDir));
52
+ }
53
+
54
+ if (level === 'all' || level === 'syntax') {
55
+ findings.push(...checkSyntaxErrors(targetDir));
56
+ }
57
+
58
+ if (level === 'all' || level === 'structure') {
59
+ findings.push(...checkProjectStructure(targetDir));
60
+ }
61
+
62
+ if (findings.length === 0) {
63
+ console.log(chalk.green('✅ No se encontraron problemas.\n'));
64
+ return;
65
+ }
66
+
67
+ const critical = findings.filter(f => f.severity === SEVERITY.CRITICAL);
68
+ const important = findings.filter(f => f.severity === SEVERITY.IMPORTANT);
69
+ const minor = findings.filter(f => f.severity === SEVERITY.MINOR);
70
+
71
+ if (critical.length > 0) {
72
+ console.log(chalk.red(`\n🔴 Hallazgos críticos (${critical.length}):\n`));
73
+ critical.forEach(f => printFinding(f));
74
+ }
75
+
76
+ if (important.length > 0) {
77
+ console.log(chalk.yellow(`\n🟡 Hallazgos importantes (${important.length}):\n`));
78
+ important.forEach(f => printFinding(f));
79
+ }
80
+
81
+ if (minor.length > 0) {
82
+ console.log(chalk.blue(`\n🔵 Hallazgos menores (${minor.length}):\n`));
83
+ minor.forEach(f => printFinding(f));
84
+ }
85
+
86
+ const score = Math.max(0, 100 - (critical.length * 10) - (important.length * 3) - (minor.length * 1));
87
+ console.log(chalk.bold(`\n📊 Puntaje: ${score}/100`));
88
+ console.log(chalk.gray(` Críticos: ${critical.length} | Importantes: ${important.length} | Menores: ${minor.length}\n`));
89
+ }
90
+
91
+ function checkNamingConventions(dir) {
92
+ const findings = [];
93
+ const jsFiles = [];
94
+ const pyFiles = [];
95
+
96
+ function walk(d) {
97
+ const items = fs.readdirSync(d, { withFileTypes: true });
98
+ for (const item of items) {
99
+ if (item.name.startsWith('.')) continue;
100
+ if (item.name === 'node_modules') continue;
101
+ if (item.name === '__pycache__') continue;
102
+ if (item.name === '.pytest_cache') continue;
103
+ if (item.name === 'g360') continue;
104
+
105
+ const fullPath = path.join(d, item.name);
106
+ if (item.isDirectory()) {
107
+ walk(fullPath);
108
+ } else if (item.isFile()) {
109
+ if (item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
110
+ jsFiles.push(fullPath);
111
+ }
112
+ if (item.name.endsWith('.py') && !item.name.endsWith('__pycache__')) {
113
+ pyFiles.push(fullPath);
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ walk(dir);
120
+
121
+ for (const file of jsFiles) {
122
+ findings.push(...checkJsFileNaming(file, dir));
123
+ }
124
+
125
+ for (const file of pyFiles) {
126
+ findings.push(...checkPyFileNaming(file, dir));
127
+ }
128
+
129
+ return findings;
130
+ }
131
+
132
+ function checkJsFileNaming(filePath, projectDir) {
133
+ const findings = [];
134
+ const relPath = path.relative(projectDir, filePath);
135
+ const fileName = path.basename(filePath, '.js');
136
+
137
+ if (fileName.includes('-') && fileName !== 'g360-theme') {
138
+ findings.push({
139
+ severity: SEVERITY.MINOR,
140
+ file: relPath,
141
+ type: 'file-naming',
142
+ message: `El archivo JS "${fileName}.js" usa kebab-case. Se recomienda camelCase: "${toCamelCase(fileName)}.js"`,
143
+ current: `${fileName}.js`,
144
+ recommended: `${toCamelCase(fileName)}.js`,
145
+ });
146
+ }
147
+
148
+ const content = fs.readFileSync(filePath, 'utf8');
149
+
150
+ const functionMatches = content.match(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
151
+ if (functionMatches) {
152
+ for (const match of functionMatches) {
153
+ const name = match.replace(/^function\s+/, '');
154
+ if (!JS_NAMING_RULES.function.pattern.test(name)) {
155
+ findings.push({
156
+ severity: SEVERITY.IMPORTANT,
157
+ file: relPath,
158
+ type: 'function-naming',
159
+ message: `La función "${name}" no usa camelCase. Se recomienda: "${toCamelCase(name)}"`,
160
+ current: name,
161
+ recommended: toCamelCase(name),
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ const classMatches = content.match(/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
168
+ if (classMatches) {
169
+ for (const match of classMatches) {
170
+ const name = match.replace(/^class\s+/, '');
171
+ if (!JS_NAMING_RULES.class.pattern.test(name)) {
172
+ findings.push({
173
+ severity: SEVERITY.IMPORTANT,
174
+ file: relPath,
175
+ type: 'class-naming',
176
+ message: `La clase "${name}" no usa PascalCase. Se recomienda: "${toPascalCase(name)}"`,
177
+ current: name,
178
+ recommended: toPascalCase(name),
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ const varMatches = content.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
185
+ if (varMatches) {
186
+ for (const match of varMatches) {
187
+ const name = match.replace(/^(?:const|let|var)\s+/, '');
188
+ if (GENERIC_NAMES.includes(name)) {
189
+ findings.push({
190
+ severity: SEVERITY.MINOR,
191
+ file: relPath,
192
+ type: 'generic-variable',
193
+ message: `La variable "${name}" es genérica. Se recomienda un nombre descriptivo`,
194
+ current: name,
195
+ recommended: null,
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ return findings;
202
+ }
203
+
204
+ function checkPyFileNaming(filePath, projectDir) {
205
+ const findings = [];
206
+ const relPath = path.relative(projectDir, filePath);
207
+ const fileName = path.basename(filePath, '.py');
208
+
209
+ if (fileName.includes('-') || fileName.includes(' ')) {
210
+ findings.push({
211
+ severity: SEVERITY.IMPORTANT,
212
+ file: relPath,
213
+ type: 'file-naming',
214
+ message: `El archivo Python "${fileName}.py" no usa snake_case. Se recomienda: "${toSnakeCase(fileName)}.py"`,
215
+ current: `${fileName}.py`,
216
+ recommended: `${toSnakeCase(fileName)}.py`,
217
+ });
218
+ }
219
+
220
+ if (fileName !== '__init__' && /[A-Z]/.test(fileName)) {
221
+ findings.push({
222
+ severity: SEVERITY.IMPORTANT,
223
+ file: relPath,
224
+ type: 'file-naming',
225
+ message: `El archivo Python "${fileName}.py" contiene mayúsculas. Python usa snake_case: "${toSnakeCase(fileName)}.py"`,
226
+ current: `${fileName}.py`,
227
+ recommended: `${toSnakeCase(fileName)}.py`,
228
+ });
229
+ }
230
+
231
+ const content = fs.readFileSync(filePath, 'utf8');
232
+
233
+ const functionMatches = content.match(/^def\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm);
234
+ if (functionMatches) {
235
+ for (const match of functionMatches) {
236
+ const name = match.replace(/^def\s+/, '');
237
+ if (!PYTHON_NAMING_RULES.function.pattern.test(name)) {
238
+ findings.push({
239
+ severity: SEVERITY.IMPORTANT,
240
+ file: relPath,
241
+ type: 'function-naming',
242
+ message: `La función "${name}" no usa snake_case. Se recomienda: "${toSnakeCase(name)}"`,
243
+ current: name,
244
+ recommended: toSnakeCase(name),
245
+ });
246
+ }
247
+ }
248
+ }
249
+
250
+ const classMatches = content.match(/^class\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm);
251
+ if (classMatches) {
252
+ for (const match of classMatches) {
253
+ const name = match.replace(/^class\s+/, '');
254
+ if (!PYTHON_NAMING_RULES.class.pattern.test(name)) {
255
+ findings.push({
256
+ severity: SEVERITY.IMPORTANT,
257
+ file: relPath,
258
+ type: 'class-naming',
259
+ message: `La clase "${name}" no usa PascalCase. Se recomienda: "${toPascalCase(name)}"`,
260
+ current: name,
261
+ recommended: toPascalCase(name),
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ return findings;
268
+ }
269
+
270
+ function checkDuplicateFunctions(dir) {
271
+ const findings = [];
272
+ const functionMap = new Map();
273
+
274
+ function walk(d) {
275
+ const items = fs.readdirSync(d, { withFileTypes: true });
276
+ for (const item of items) {
277
+ if (item.name.startsWith('.')) continue;
278
+ if (item.name === 'node_modules') continue;
279
+ if (item.name === '__pycache__') continue;
280
+ if (item.name === '.pytest_cache') continue;
281
+ if (item.name === 'g360') continue;
282
+
283
+ const fullPath = path.join(d, item.name);
284
+ if (item.isDirectory()) {
285
+ walk(fullPath);
286
+ } else if (item.isFile()) {
287
+ if (item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
288
+ extractFunctions(fullPath, 'js', functionMap);
289
+ }
290
+ if (item.name.endsWith('.py') && !item.name.endsWith('__pycache__')) {
291
+ extractFunctions(fullPath, 'py', functionMap);
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ walk(dir);
298
+
299
+ for (const [name, locations] of functionMap) {
300
+ if (locations.length > 1) {
301
+ const uniqueBodies = new Set(locations.map(l => l.bodyHash));
302
+ if (uniqueBodies.size > 1) {
303
+ findings.push({
304
+ severity: SEVERITY.CRITICAL,
305
+ file: locations.map(l => l.file).join(', '),
306
+ type: 'duplicate-function',
307
+ message: `La función "${name}" está definida en ${locations.length} archivos con implementaciones diferentes`,
308
+ current: locations.map(l => `${l.file}:${l.line}`).join('\n'),
309
+ recommended: `Unificar en un solo archivo o renombrar para clarificar`,
310
+ });
311
+ } else {
312
+ findings.push({
313
+ severity: SEVERITY.IMPORTANT,
314
+ file: locations.map(l => l.file).join(', '),
315
+ type: 'duplicate-function',
316
+ message: `La función "${name}" está definida en ${locations.length} archivos con la misma implementación (código duplicado)`,
317
+ current: locations.map(l => `${l.file}:${l.line}`).join(', '),
318
+ recommended: `Extraer a un módulo compartido en src/lib/`,
319
+ });
320
+ }
321
+ }
322
+ }
323
+
324
+ return findings;
325
+ }
326
+
327
+ function extractFunctions(filePath, lang, functionMap) {
328
+ const content = fs.readFileSync(filePath, 'utf8');
329
+ const relPath = path.relative(process.cwd(), filePath);
330
+
331
+ if (lang === 'js') {
332
+ const funcMatches = content.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm);
333
+ if (funcMatches) {
334
+ const lines = content.split('\n');
335
+ for (let i = 0; i < lines.length; i++) {
336
+ const match = lines[i].match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
337
+ if (match) {
338
+ const name = match[1];
339
+ const bodyStart = i;
340
+ let bodyEnd = i;
341
+ let braceCount = 0;
342
+ let foundOpen = false;
343
+ for (let j = i; j < lines.length; j++) {
344
+ for (const ch of lines[j]) {
345
+ if (ch === '{') { braceCount++; foundOpen = true; }
346
+ if (ch === '}') { braceCount--; }
347
+ }
348
+ if (foundOpen && braceCount === 0) {
349
+ bodyEnd = j;
350
+ break;
351
+ }
352
+ }
353
+ const body = lines.slice(bodyStart, bodyEnd + 1).join('\n');
354
+ const bodyHash = hashString(body);
355
+
356
+ if (!functionMap.has(name)) {
357
+ functionMap.set(name, []);
358
+ }
359
+ functionMap.get(name).push({
360
+ file: relPath,
361
+ line: i + 1,
362
+ bodyHash,
363
+ body: body.substring(0, 100),
364
+ });
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ if (lang === 'py') {
371
+ const funcMatches = content.match(/^def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/gm);
372
+ if (funcMatches) {
373
+ const lines = content.split('\n');
374
+ for (let i = 0; i < lines.length; i++) {
375
+ const match = lines[i].match(/^def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
376
+ if (match) {
377
+ const name = match[1];
378
+ const bodyStart = i;
379
+ let bodyEnd = i;
380
+ let indent = null;
381
+ for (let j = i + 1; j < lines.length; j++) {
382
+ if (lines[j].trim() === '') continue;
383
+ const currentIndent = lines[j].match(/^(\s*)/)[1].length;
384
+ if (indent === null) {
385
+ indent = currentIndent;
386
+ }
387
+ if (currentIndent <= indent && lines[j].trim() !== '') {
388
+ bodyEnd = j - 1;
389
+ break;
390
+ }
391
+ bodyEnd = j;
392
+ }
393
+ const body = lines.slice(bodyStart, bodyEnd + 1).join('\n');
394
+ const bodyHash = hashString(body);
395
+
396
+ if (!functionMap.has(name)) {
397
+ functionMap.set(name, []);
398
+ }
399
+ functionMap.get(name).push({
400
+ file: relPath,
401
+ line: i + 1,
402
+ bodyHash,
403
+ body: body.substring(0, 100),
404
+ });
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ function checkSyntaxErrors(dir) {
412
+ const findings = [];
413
+
414
+ function walk(d) {
415
+ const items = fs.readdirSync(d, { withFileTypes: true });
416
+ for (const item of items) {
417
+ if (item.name.startsWith('.')) continue;
418
+ if (item.name === 'node_modules') continue;
419
+ if (item.name === '__pycache__') continue;
420
+ if (item.name === '.pytest_cache') continue;
421
+ if (item.name === 'g360') continue;
422
+
423
+ const fullPath = path.join(d, item.name);
424
+ if (item.isDirectory()) {
425
+ walk(fullPath);
426
+ } else if (item.isFile()) {
427
+ if (item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
428
+ checkJsSyntax(fullPath, dir, findings);
429
+ }
430
+ if (item.name.endsWith('.py')) {
431
+ checkPySyntax(fullPath, dir, findings);
432
+ }
433
+ }
434
+ }
435
+ }
436
+
437
+ walk(dir);
438
+ return findings;
439
+ }
440
+
441
+ function checkJsSyntax(filePath, projectDir, findings) {
442
+ const relPath = path.relative(projectDir, filePath);
443
+ try {
444
+ new Function(fs.readFileSync(filePath, 'utf8'));
445
+ } catch (error) {
446
+ findings.push({
447
+ severity: SEVERITY.CRITICAL,
448
+ file: relPath,
449
+ type: 'syntax-error',
450
+ message: `Error de sintaxis en JavaScript: ${error.message}`,
451
+ current: error.message,
452
+ recommended: 'Corregir el error de sintaxis',
453
+ });
454
+ }
455
+ }
456
+
457
+ function checkPySyntax(filePath, projectDir, findings) {
458
+ const relPath = path.relative(projectDir, filePath);
459
+ const content = fs.readFileSync(filePath, 'utf8');
460
+
461
+ const lines = content.split('\n');
462
+ let indentStack = [0];
463
+ let inTripleQuote = false;
464
+ let tripleQuoteChar = null;
465
+
466
+ for (let i = 0; i < lines.length; i++) {
467
+ const line = lines[i];
468
+ const trimmed = line.trim();
469
+
470
+ if (trimmed.startsWith('"""') || trimmed.startsWith("'''")) {
471
+ const quoteChar = trimmed.substring(0, 3);
472
+ if (!inTripleQuote) {
473
+ inTripleQuote = true;
474
+ tripleQuoteChar = quoteChar;
475
+ } else if (trimmed.endsWith(tripleQuoteChar) && trimmed.length > 3) {
476
+ inTripleQuote = false;
477
+ } else if (trimmed === tripleQuoteChar) {
478
+ inTripleQuote = false;
479
+ }
480
+ continue;
481
+ }
482
+
483
+ if (inTripleQuote) continue;
484
+
485
+ if (trimmed === '' || trimmed.startsWith('#')) continue;
486
+
487
+ const indent = line.match(/^(\s*)/)[1].length;
488
+
489
+ if (indent > indentStack[indentStack.length - 1]) {
490
+ indentStack.push(indent);
491
+ } else if (indent < indentStack[indentStack.length - 1]) {
492
+ while (indentStack.length > 1 && indentStack[indentStack.length - 1] > indent) {
493
+ indentStack.pop();
494
+ }
495
+ }
496
+ }
497
+ }
498
+
499
+ function checkProjectStructure(dir) {
500
+ const findings = [];
501
+
502
+ const manifestPath = path.join(dir, 'g360-manifest.json');
503
+ if (!fs.existsSync(manifestPath)) {
504
+ findings.push({
505
+ severity: SEVERITY.MINOR,
506
+ file: 'g360-manifest.json',
507
+ type: 'missing-file',
508
+ message: 'No se encontró g360-manifest.json. Se recomienda crear uno para proyectos G360',
509
+ current: 'ausente',
510
+ recommended: 'Crear g360-manifest.json con name, template, version',
511
+ });
512
+ }
513
+
514
+ const skillPath = path.join(dir, 'skill.json');
515
+ if (!fs.existsSync(skillPath)) {
516
+ const srcCoreSkill = path.join(dir, 'src', 'core', 'skill.json');
517
+ if (!fs.existsSync(srcCoreSkill)) {
518
+ findings.push({
519
+ severity: SEVERITY.MINOR,
520
+ file: 'skill.json',
521
+ type: 'missing-file',
522
+ message: 'No se encontró skill.json en la raíz ni en src/core/',
523
+ current: 'ausente',
524
+ recommended: 'Crear skill.json con name, description, framework, colors, signature',
525
+ });
526
+ }
527
+ }
528
+
529
+ const readmePath = path.join(dir, 'README.md');
530
+ if (!fs.existsSync(readmePath)) {
531
+ findings.push({
532
+ severity: SEVERITY.IMPORTANT,
533
+ file: 'README.md',
534
+ type: 'missing-file',
535
+ message: 'No se encontró README.md',
536
+ current: 'ausente',
537
+ recommended: 'Crear README.md con descripción, quick start y estructura del proyecto',
538
+ });
539
+ }
540
+
541
+ return findings;
542
+ }
543
+
544
+ function printFinding(finding) {
545
+ console.log(chalk.bold(`[${finding.type}]`));
546
+ console.log(chalk.red(` ${finding.message}`));
547
+ console.log(chalk.gray(` Archivo: ${finding.file}`));
548
+ if (finding.current) {
549
+ console.log(chalk.gray(` Actual: ${finding.current}`));
550
+ }
551
+ if (finding.recommended) {
552
+ console.log(chalk.green(` Recomendado: ${finding.recommended}`));
553
+ }
554
+ console.log();
555
+ }
556
+
557
+ function toCamelCase(str) {
558
+ return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
559
+ }
560
+
561
+ function toPascalCase(str) {
562
+ const camel = toCamelCase(str);
563
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
564
+ }
565
+
566
+ function toSnakeCase(str) {
567
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
568
+ }
569
+
570
+ function hashString(str) {
571
+ let hash = 0;
572
+ for (let i = 0; i < str.length; i++) {
573
+ const char = str.charCodeAt(i);
574
+ hash = ((hash << 5) - hash) + char;
575
+ hash |= 0;
576
+ }
577
+ return hash.toString();
578
+ }
@@ -0,0 +1,27 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', 'out', '.nuxt', 'coverage', '.cache', '.svelte-kit', '__pycache__', '.pytest_cache'];
5
+
6
+ export async function getAllFiles(dir, baseDir = dir) {
7
+ const files = [];
8
+
9
+ if (!fs.existsSync(dir)) return files;
10
+
11
+ const items = fs.readdirSync(dir, { withFileTypes: true });
12
+
13
+ for (const item of items) {
14
+ if (IGNORE_DIRS.includes(item.name)) continue;
15
+
16
+ const fullPath = path.join(dir, item.name);
17
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
18
+
19
+ if (item.isDirectory()) {
20
+ files.push(...await getAllFiles(fullPath, baseDir));
21
+ } else {
22
+ files.push(relativePath);
23
+ }
24
+ }
25
+
26
+ return files;
27
+ }
@@ -1 +0,0 @@
1
- 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
@@ -1 +0,0 @@
1
- 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