g360-cli 1.10.0 → 1.11.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.
@@ -12,11 +12,13 @@ import { fileURLToPath } from 'url';
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
  const SKILLS_PATH = path.resolve(__dirname, '../assets/config/g360-skills.json');
14
14
 
15
- export async function setSkill(skillName, options) {
16
- const { verbose = false, cwd = process.cwd() } = options;
15
+ export async function setSkill(skillName, options = {}) {
16
+ const { verbose = false, cwd = process.cwd(), force = false } = options;
17
17
  const isInternalCall = options.cwd !== undefined;
18
18
 
19
- console.log(chalk.bold.cyan('\n🎨 G360 Skill Selector\n'));
19
+ if (!isInternalCall) {
20
+ console.log(chalk.bold.cyan('\n🎨 G360 Skill Selector\n'));
21
+ }
20
22
 
21
23
  // Cargar skills disponibles
22
24
  let skillsConfig;
@@ -43,9 +45,9 @@ export async function setSkill(skillName, options) {
43
45
  const skillJsonPath = path.join(cwd, 'skill.json');
44
46
 
45
47
  if (fs.existsSync(skillJsonPath)) {
46
- console.log(chalk.yellow('⚠️ El proyecto ya tiene un skill configurado.'));
47
- console.log(chalk.gray('Usar --force para sobrescribir'));
48
- if (!options.force) {
48
+ if (!force) {
49
+ console.log(chalk.yellow('⚠️ El proyecto ya tiene un skill configurado.'));
50
+ console.log(chalk.gray('Usar --force para sobrescribir'));
49
51
  console.log(chalk.cyan('\nPara cambiar el skill:'));
50
52
  console.log(chalk.cyan(' g360 set-skill ') + skillName + chalk.cyan(' --force'));
51
53
  return;
@@ -65,17 +67,19 @@ export async function setSkill(skillName, options) {
65
67
 
66
68
  await fs.writeJson(skillJsonPath, skillData, { spaces: 2 });
67
69
 
68
- console.log(chalk.green(`\n✅ Skill "${skillName}" configurado correctamente`));
69
- console.log(chalk.gray('\nDetalles:'));
70
- console.log(` Device: ${skill.device}`);
71
- console.log(` Accent: ${skill.colors.accent}`);
72
- console.log(` Signature: ${skill.signature.mode}`);
73
-
74
- if (verbose) {
75
- console.log(chalk.gray('\nColores:'));
76
- Object.entries(skill.colors).forEach(([key, value]) => {
77
- console.log(` ${key}: ${value}`);
78
- });
70
+ if (!isInternalCall) {
71
+ console.log(chalk.green(`\n✅ Skill "${skillName}" configurado correctamente`));
72
+ console.log(chalk.gray('\nDetalles:'));
73
+ console.log(` Device: ${skill.device}`);
74
+ console.log(` Accent: ${skill.colors.accent}`);
75
+ console.log(` Signature: ${skill.signature.mode}`);
76
+
77
+ if (verbose) {
78
+ console.log(chalk.gray('\nColores:'));
79
+ Object.entries(skill.colors).forEach(([key, value]) => {
80
+ console.log(` ${key}: ${value}`);
81
+ });
82
+ }
79
83
  }
80
84
 
81
85
  } catch (error) {
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @file set-skill.test.js
3
+ * @description Tests para el comando set-skill
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
7
+ import fs from 'fs-extra';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { setSkill } from '../commands/set-skill.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // Mock de fs-extra
15
+ vi.mock('fs-extra', () => ({
16
+ default: {
17
+ existsSync: vi.fn(),
18
+ readJson: vi.fn(),
19
+ readJsonSync: vi.fn(),
20
+ writeJson: vi.fn()
21
+ }
22
+ }));
23
+
24
+ describe('set-skill command', () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ // Mockear console.log para evitar output en los tests
28
+ vi.spyOn(console, 'log').mockImplementation(() => {});
29
+ vi.spyOn(console, 'error').mockImplementation(() => {});
30
+ });
31
+
32
+ afterEach(() => {
33
+ vi.restoreAllMocks();
34
+ });
35
+
36
+ describe('valid skill selection', () => {
37
+ it('should set skill successfully', async () => {
38
+ const mockSkills = {
39
+ skills: [
40
+ { name: 'corporativo', description: 'Proyectos corporativos', device: 'pc', colors: { accent: '#00796B' }, signature: { mode: 'powered' } }
41
+ ]
42
+ };
43
+
44
+ fs.readJson.mockResolvedValue(mockSkills);
45
+ fs.existsSync.mockReturnValue(false);
46
+ fs.writeJson.mockResolvedValue();
47
+
48
+ await setSkill('corporativo', { verbose: false, cwd: process.cwd() });
49
+
50
+ expect(fs.writeJson).toHaveBeenCalled();
51
+ });
52
+
53
+ it('should overwrite existing skill with --force', async () => {
54
+ const mockSkills = {
55
+ skills: [
56
+ { name: 'corporativo', description: 'Proyectos corporativos', device: 'pc', colors: { accent: '#00796B' }, signature: { mode: 'powered' } }
57
+ ]
58
+ };
59
+
60
+ fs.readJson.mockResolvedValue(mockSkills);
61
+ fs.existsSync.mockReturnValue(true);
62
+ fs.writeJson.mockResolvedValue();
63
+
64
+ await setSkill('corporativo', { verbose: false, force: true, cwd: process.cwd() });
65
+
66
+ expect(fs.writeJson).toHaveBeenCalled();
67
+ });
68
+ });
69
+
70
+ describe('invalid skill selection', () => {
71
+ it('should reject invalid skill name', async () => {
72
+ const mockSkills = {
73
+ skills: [
74
+ { name: 'corporativo', description: 'Proyectos corporativos', device: 'pc' }
75
+ ]
76
+ };
77
+
78
+ fs.readJson.mockResolvedValue(mockSkills);
79
+
80
+ await setSkill('invalid-skill', { verbose: false, cwd: process.cwd() });
81
+
82
+ expect(fs.writeJson).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('should handle missing skills config file', async () => {
86
+ fs.readJson.mockRejectedValue(new Error('File not found'));
87
+
88
+ await setSkill('corporativo', { verbose: false, cwd: process.cwd() });
89
+
90
+ expect(fs.writeJson).not.toHaveBeenCalled();
91
+ });
92
+ });
93
+
94
+ describe('skill already exists', () => {
95
+ it('should not overwrite without --force', async () => {
96
+ const mockSkills = {
97
+ skills: [
98
+ { name: 'corporativo', description: 'Proyectos corporativos', device: 'pc', colors: { accent: '#00796B' }, signature: { mode: 'powered' } }
99
+ ]
100
+ };
101
+
102
+ fs.readJsonSync.mockReturnValue(mockSkills);
103
+ fs.existsSync.mockReturnValue(true);
104
+
105
+ await setSkill('corporativo', { verbose: false, force: false, cwd: process.cwd() });
106
+
107
+ expect(fs.writeJson).not.toHaveBeenCalled();
108
+ });
109
+ });
110
+
111
+ describe('verbose output', () => {
112
+ it('should show detailed colors with --verbose', async () => {
113
+ const mockSkills = {
114
+ skills: [
115
+ {
116
+ name: 'corporativo',
117
+ description: 'Proyectos corporativos',
118
+ device: 'pc',
119
+ colors: { bg: '#0b1220', surface: '#151e2e', accent: '#00796B', text: '#f0f4f8', muted: '#94a3b8' },
120
+ signature: { mode: 'powered' }
121
+ }
122
+ ]
123
+ };
124
+
125
+ fs.readJson.mockResolvedValue(mockSkills);
126
+ fs.existsSync.mockReturnValue(false);
127
+ fs.writeJson.mockResolvedValue();
128
+
129
+ await setSkill('corporativo', { verbose: true, cwd: process.cwd() });
130
+
131
+ expect(fs.writeJson).toHaveBeenCalled();
132
+ });
133
+ });
134
+ });
@@ -1,11 +1,14 @@
1
1
  import chalk from 'chalk';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
+ import inquirer from 'inquirer';
4
5
  import { fileURLToPath } from 'url';
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
  const SIGNATURE_ASSETS = path.join(__dirname, '..', 'assets', 'signature');
8
9
 
10
+ const VALID_COMMANDS = ['install', 'positions'];
11
+
9
12
  const POSITIONS = {
10
13
  'bottom-right': 'position: fixed; bottom: 16px; right: 16px; z-index: 99999;',
11
14
  'bottom-left': 'position: fixed; bottom: 16px; left: 16px; z-index: 99999;',
@@ -22,6 +25,18 @@ const FLET_POSITIONS = {
22
25
  };
23
26
 
24
27
  export async function signature(command, options) {
28
+ // Validacion del comando
29
+ if (!VALID_COMMANDS.includes(command)) {
30
+ console.error(chalk.red(`❌ Comando invalido: "${command}"`));
31
+ console.log(chalk.gray('Comandos disponibles:'));
32
+ console.log(chalk.gray(' install - Instalar g360-signature en un proyecto'));
33
+ console.log(chalk.gray(' positions - Mostrar posiciones disponibles'));
34
+ console.log(chalk.gray('\nEjemplo:'));
35
+ console.log(chalk.gray(' g360 signature install'));
36
+ console.log(chalk.gray(' g360 signature positions'));
37
+ return;
38
+ }
39
+
25
40
  const {
26
41
  path: targetPath = '.',
27
42
  force = false,
@@ -43,22 +58,45 @@ export async function signature(command, options) {
43
58
  return;
44
59
  }
45
60
 
46
- // Modo interactivo: guiar al usuario
61
+ // Modo interactivo: guiar al usuario con inquirer
62
+ let resolvedPosition = position;
63
+ let resolvedMode = mode;
64
+
47
65
  if (interactive) {
48
- const selectedPosition = await interactivePosition(projectType);
49
- if (selectedPosition) {
50
- options.position = selectedPosition;
51
- }
66
+ const answers = await inquirer.prompt([
67
+ {
68
+ type: 'list',
69
+ name: 'mode',
70
+ message: 'Selecciona el modo de la firma:',
71
+ choices: [
72
+ { name: 'Powered by G360 (recomendado)', value: 'powered' },
73
+ { name: 'Own (G360 by ccusi)', value: 'own' },
74
+ ],
75
+ default: 'powered',
76
+ },
77
+ {
78
+ type: 'list',
79
+ name: 'position',
80
+ message: 'Selecciona la posicion de la firma:',
81
+ choices: projectType === 'flet'
82
+ ? Object.entries(FLET_POSITIONS).map(([key, desc]) => ({ name: `${key}: ${desc}`, value: key }))
83
+ : Object.entries(POSITIONS).map(([key, desc]) => ({ name: `${key}: ${desc}`, value: key })),
84
+ default: 'bottom-right',
85
+ },
86
+ ]);
87
+
88
+ resolvedMode = answers.mode;
89
+ resolvedPosition = answers.position;
52
90
  }
53
91
 
54
92
  if (projectType === 'flet') {
55
- await installFlet(targetDir, { force, mode, version, position: options.position });
93
+ await installFlet(targetDir, { force, mode: resolvedMode, version, position: resolvedPosition });
56
94
  } else if (projectType === 'web') {
57
- await installWeb(targetDir, { force, mode, version, position: options.position });
95
+ await installWeb(targetDir, { force, mode: resolvedMode, version, position: resolvedPosition });
58
96
  }
59
97
 
60
98
  console.log(chalk.green('\n✅ g360-signature instalado exitosamente!'));
61
- showUsageTips(projectType, options.position);
99
+ showUsageTips(projectType, resolvedPosition);
62
100
  }
63
101
 
64
102
  if (command === 'positions') {
@@ -96,24 +134,6 @@ function showPositions() {
96
134
  console.log(chalk.gray('\nEjemplo: g360 signature install --position bottom-left\n'));
97
135
  }
98
136
 
99
- async function interactivePosition(projectType) {
100
- console.log(chalk.bold.cyan('\n📍 Selecciona la posicion de la firma:\n'));
101
-
102
- const options = projectType === 'flet'
103
- ? Object.entries(FLET_POSITIONS)
104
- : Object.entries(POSITIONS);
105
-
106
- options.forEach(([key, value], index) => {
107
- console.log(chalk.white(` ${index + 1}. ${key.padEnd(18)} ${chalk.gray(value)}`));
108
- });
109
-
110
- console.log(chalk.gray('\n Presiona Enter para usar la posicion por defecto (bottom-right)'));
111
- console.log(chalk.gray(' O escribe el nombre de la posicion\n'));
112
-
113
- // En modo no-interactivo, retornar default
114
- return null;
115
- }
116
-
117
137
  function showUsageTips(projectType, position) {
118
138
  console.log(chalk.bold.cyan('\n💡 Tips de uso:\n'));
119
139
 
@@ -19,10 +19,22 @@ export async function update(options) {
19
19
  if (check) {
20
20
  try {
21
21
  console.log(chalk.gray(`Current version: ${currentVersion}`));
22
- console.log(chalk.gray('Check npm for latest version...'));
23
- console.log(chalk.yellow('\nUse: npm install -g g360-cli@latest to update'));
22
+ console.log(chalk.gray('Checking npm for latest version...\n'));
23
+
24
+ const latestVersion = execSync('npm view g360-cli version', { encoding: 'utf8' }).trim();
25
+
26
+ if (latestVersion === currentVersion) {
27
+ console.log(chalk.green(`✅ You have the latest version (${currentVersion})`));
28
+ } else {
29
+ console.log(chalk.yellow(`📦 Latest version: ${latestVersion}`));
30
+ console.log(chalk.yellow(`📦 Current version: ${currentVersion}`));
31
+ console.log(chalk.cyan('\nTo update:'));
32
+ console.log(chalk.gray(' npm install -g g360-cli@latest'));
33
+ }
24
34
  } catch (error) {
25
35
  console.error(chalk.red(`Error checking version: ${error.message}`));
36
+ console.log(chalk.gray('\nTry manually:'));
37
+ console.log(chalk.gray(' npm view g360-cli version'));
26
38
  }
27
39
  return;
28
40
  }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @file asset-validator.test.js
3
+ * @description Tests para el mĂłdulo asset-validator
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
7
+ import fs from 'fs-extra';
8
+ import { assetValidator } from '../lib/asset-validator.js';
9
+
10
+ // Mock de fs-extra
11
+ vi.mock('fs-extra', () => ({
12
+ default: {
13
+ existsSync: vi.fn(),
14
+ readJson: vi.fn()
15
+ }
16
+ }));
17
+
18
+ // Mock de logger
19
+ vi.mock('../lib/logger.js', () => ({
20
+ logger: {
21
+ debug: vi.fn(),
22
+ warn: vi.fn(),
23
+ error: vi.fn()
24
+ }
25
+ }));
26
+
27
+ describe('asset-validator module', () => {
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ assetValidator.clearCache();
31
+ });
32
+
33
+ describe('validateSkills', () => {
34
+ it('should validate valid skills data', async () => {
35
+ const validSkills = {
36
+ skills: [
37
+ {
38
+ name: 'corporativo',
39
+ description: 'Test skill',
40
+ device: 'pc',
41
+ colors: {
42
+ bg: '#0b1220',
43
+ surface: '#1a2332',
44
+ accent: '#00796B',
45
+ text: '#f0f4f8',
46
+ muted: '#94a3b8'
47
+ },
48
+ signature: {
49
+ mode: 'powered',
50
+ text: 'powered by G360'
51
+ }
52
+ }
53
+ ]
54
+ };
55
+
56
+ fs.existsSync.mockReturnValue(true);
57
+ fs.readJson.mockResolvedValue({
58
+ type: 'object',
59
+ properties: {
60
+ skills: {
61
+ type: 'array',
62
+ items: {
63
+ type: 'object',
64
+ required: ['name', 'description', 'device', 'colors', 'signature'],
65
+ properties: {
66
+ name: { type: 'string' },
67
+ description: { type: 'string' },
68
+ device: { type: 'string', enum: ['pc', 'movil', 'both'] },
69
+ colors: { type: 'object' },
70
+ signature: { type: 'object' }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ });
76
+
77
+ const result = await assetValidator.validateSkills(validSkills);
78
+
79
+ expect(result.valid).toBe(true);
80
+ expect(result.errors).toHaveLength(0);
81
+ });
82
+
83
+ it('should reject invalid skills data', async () => {
84
+ const invalidSkills = {
85
+ skills: [
86
+ {
87
+ name: 'test',
88
+ // Missing required fields
89
+ }
90
+ ]
91
+ };
92
+
93
+ fs.existsSync.mockReturnValue(true);
94
+ fs.readJson.mockResolvedValue({
95
+ type: 'object',
96
+ properties: {
97
+ skills: {
98
+ type: 'array',
99
+ items: {
100
+ type: 'object',
101
+ required: ['name', 'description', 'device', 'colors', 'signature']
102
+ }
103
+ }
104
+ }
105
+ });
106
+
107
+ const result = await assetValidator.validateSkills(invalidSkills);
108
+
109
+ expect(result.valid).toBe(false);
110
+ expect(result.errors.length).toBeGreaterThan(0);
111
+ });
112
+ });
113
+
114
+ describe('validateSnippets', () => {
115
+ it('should validate valid snippets data', async () => {
116
+ const validSnippets = {
117
+ snippets: [
118
+ {
119
+ name: 'test-snippet',
120
+ description: 'A test snippet',
121
+ code: 'console.log("test")'
122
+ }
123
+ ]
124
+ };
125
+
126
+ fs.existsSync.mockReturnValue(true);
127
+ fs.readJson.mockResolvedValue({
128
+ type: 'object',
129
+ properties: {
130
+ snippets: {
131
+ type: 'array',
132
+ items: {
133
+ type: 'object',
134
+ required: ['name', 'description', 'code']
135
+ }
136
+ }
137
+ }
138
+ });
139
+
140
+ const result = await assetValidator.validateSnippets(validSnippets);
141
+
142
+ expect(result.valid).toBe(true);
143
+ });
144
+ });
145
+
146
+ describe('validateAsset', () => {
147
+ it('should validate individual skill asset', async () => {
148
+ const skill = {
149
+ name: 'test-skill',
150
+ description: 'Test',
151
+ device: 'pc',
152
+ colors: { accent: '#00d084' },
153
+ signature: { mode: 'powered' }
154
+ };
155
+
156
+ fs.existsSync.mockReturnValue(true);
157
+ fs.readJson.mockResolvedValue({
158
+ type: 'object',
159
+ properties: {
160
+ skills: {
161
+ type: 'array',
162
+ items: { type: 'object' }
163
+ }
164
+ }
165
+ });
166
+
167
+ const result = await assetValidator.validateAsset('skill', skill);
168
+
169
+ expect(result).toHaveProperty('valid');
170
+ });
171
+
172
+ it('should validate individual snippet asset', async () => {
173
+ const snippet = {
174
+ name: 'test',
175
+ description: 'Test',
176
+ code: 'test'
177
+ };
178
+
179
+ fs.existsSync.mockReturnValue(true);
180
+ fs.readJson.mockResolvedValue({
181
+ type: 'object',
182
+ properties: {
183
+ snippets: {
184
+ type: 'array',
185
+ items: { type: 'object' }
186
+ }
187
+ }
188
+ });
189
+
190
+ const result = await assetValidator.validateAsset('snippet', snippet);
191
+
192
+ expect(result).toHaveProperty('valid');
193
+ });
194
+ });
195
+
196
+ describe('error handling', () => {
197
+ it('should handle missing schema file', async () => {
198
+ fs.existsSync.mockReturnValue(false);
199
+
200
+ const result = await assetValidator.validate('non-existent', {});
201
+
202
+ expect(result.valid).toBe(false);
203
+ expect(result.errors.length).toBeGreaterThan(0);
204
+ });
205
+
206
+ it('should handle invalid schema', async () => {
207
+ fs.existsSync.mockReturnValue(true);
208
+ fs.readJson.mockResolvedValue({ invalid: 'schema' });
209
+
210
+ const result = await assetValidator.validate('invalid-schema', {});
211
+
212
+ expect(result.valid).toBe(false);
213
+ });
214
+ });
215
+
216
+ describe('cache management', () => {
217
+ it('should cache compiled schemas', async () => {
218
+ fs.existsSync.mockReturnValue(true);
219
+ fs.readJson.mockResolvedValue({
220
+ type: 'object',
221
+ properties: {}
222
+ });
223
+
224
+ await assetValidator.validate('test', {});
225
+ await assetValidator.validate('test', {});
226
+
227
+ // Schema should be cached, so readJson should only be called once
228
+ expect(fs.readJson).toHaveBeenCalledTimes(1);
229
+ });
230
+
231
+ it('should clear cache when requested', async () => {
232
+ assetValidator.clearCache();
233
+
234
+ expect(assetValidator.schemaCache.size).toBe(0);
235
+ });
236
+ });
237
+ });