g360-cli 1.3.0 → 1.5.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.
Files changed (86) hide show
  1. package/README.md +209 -5
  2. package/package.json +15 -8
  3. package/src/assets/brand/brand.json +43 -0
  4. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_borde.png +0 -0
  5. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_borde.svg +9 -0
  6. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_solid.png +0 -0
  7. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_solid.svg +9 -0
  8. package/src/assets/brand/g360/logotypes/logo-g360-dark.svg +19 -0
  9. package/src/assets/brand/g360/logotypes/logo-g360-light.svg +19 -0
  10. package/src/assets/brand/g360/logotypes/logo_g360_dark_b64.txt +1 -0
  11. package/src/assets/brand/g360/logotypes/logo_g360_light_b64.txt +1 -0
  12. package/src/assets/config/g360-skills.json +112 -0
  13. package/src/assets/config/project-types.json +16 -0
  14. package/src/assets/config/skills.json +12 -0
  15. package/src/assets/snippets/snippets.json +293 -24
  16. package/src/assets/templates/lit-web/src/core/skill.json +12 -5
  17. package/src/assets/templates/python-cli/build-portable.bat +43 -0
  18. package/src/assets/templates/python-cli/src/core/skill.json +25 -6
  19. package/src/assets/templates/python-cli/src/main.py +107 -3
  20. package/src/assets/templates/python-customtkinter/README.md +48 -0
  21. package/src/assets/templates/python-customtkinter/assets/images/logo-g360-dark.svg +19 -0
  22. package/src/assets/templates/python-customtkinter/assets/images/logo-g360-light.svg +19 -0
  23. package/src/assets/templates/python-customtkinter/build.bat +50 -0
  24. package/src/assets/templates/python-customtkinter/pyproject.toml +26 -0
  25. package/src/assets/templates/python-customtkinter/run.bat +20 -0
  26. package/src/assets/templates/python-customtkinter/src/core/g360_theme.py +101 -0
  27. package/src/assets/templates/python-customtkinter/src/core/skill.json +35 -0
  28. package/src/assets/templates/python-customtkinter/src/main.py +103 -0
  29. package/src/assets/templates/python-flet/README.md +89 -0
  30. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_borde.png +0 -0
  31. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_borde.svg +9 -0
  32. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_solid.png +0 -0
  33. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_solid.svg +9 -0
  34. package/src/assets/templates/python-flet/assets/images/logo-g360-dark.svg +19 -0
  35. package/src/assets/templates/python-flet/assets/images/logo-g360-light.svg +19 -0
  36. package/src/assets/templates/python-flet/build.bat +36 -0
  37. package/src/assets/templates/python-flet/pyproject.toml +26 -0
  38. package/src/assets/templates/python-flet/run.bat +20 -0
  39. package/src/assets/templates/python-flet/src/core/__init__.py +0 -0
  40. package/src/assets/templates/python-flet/src/core/g360_theme.py +186 -0
  41. package/src/assets/templates/python-flet/src/core/skill.json +41 -0
  42. package/src/assets/templates/python-flet/src/export/__init__.py +0 -0
  43. package/src/assets/templates/python-flet/src/main.py +146 -0
  44. package/src/assets/templates/python-flet/src/test_app.py +107 -0
  45. package/src/assets/templates/python-flet/src/ui/__init__.py +0 -0
  46. package/src/assets/templates/python-flet-migrate/README.md +70 -0
  47. package/src/assets/templates/python-flet-migrate/assets/images/logo-g360-dark.svg +19 -0
  48. package/src/assets/templates/python-flet-migrate/assets/images/logo-g360-light.svg +19 -0
  49. package/src/assets/templates/python-flet-migrate/build.bat +36 -0
  50. package/src/assets/templates/python-flet-migrate/pyproject.toml +26 -0
  51. package/src/assets/templates/python-flet-migrate/run.bat +20 -0
  52. package/src/assets/templates/python-flet-migrate/src/core/__init__.py +0 -0
  53. package/src/assets/templates/python-flet-migrate/src/core/g360_theme.py +186 -0
  54. package/src/assets/templates/python-flet-migrate/src/core/skill.json +34 -0
  55. package/src/assets/templates/python-flet-migrate/src/main.py +97 -0
  56. package/src/assets/templates/python-flet-migrate/src/migrate_tkinter.py +56 -0
  57. package/src/assets/templates/solid-web/src/core/skill.json +12 -5
  58. package/src/assets/templates/svelte-web/src/core/skill.json +12 -5
  59. package/src/assets/templates/web-pwa/index.html +2 -7
  60. package/src/assets/templates/web-pwa/public/favicon.svg +4 -0
  61. package/src/assets/templates/web-pwa/public/manifest.json +11 -0
  62. package/src/assets/templates/web-pwa/src/App.jsx +22 -0
  63. package/src/assets/templates/web-pwa/src/components/FooterSignature.jsx +9 -0
  64. package/src/assets/templates/web-pwa/src/components/Header.jsx +12 -0
  65. package/src/assets/templates/web-pwa/src/components/LoadingOverlay.jsx +10 -0
  66. package/src/assets/templates/web-pwa/src/core/skill.json +25 -0
  67. package/src/assets/templates/web-pwa/src/main.jsx +9 -0
  68. package/src/assets/templates/web-pwa/src/styles/main.css +133 -0
  69. package/src/assets/templates/web-pwa/vite.config.js +12 -0
  70. package/src/cli.js +8 -4
  71. package/src/commands/bring.js +81 -6
  72. package/src/commands/clean.js +51 -4
  73. package/src/commands/init.js +60 -8
  74. package/src/commands/list.js +79 -9
  75. package/src/commands/signature.js +12 -8
  76. package/src/lib/asset-validator.js +145 -0
  77. package/src/lib/auditor.js +48 -0
  78. package/src/lib/global-config.js +260 -0
  79. package/src/lib/i18n.js +238 -0
  80. package/src/lib/logger.js +100 -0
  81. package/src/lib/manifest.js +70 -1
  82. package/src/lib/validator.js +39 -0
  83. package/src/schemas/skills-schema.json +1 -0
  84. package/src/schemas/snippets-schema.json +1 -0
  85. package/src/assets/templates/web-pwa/app.js +0 -1
  86. package/src/assets/templates/web-pwa/styles.css +0 -28
@@ -0,0 +1,133 @@
1
+ :root {
2
+ --g360-bg: #0b1220;
3
+ --g360-surface: #151e2e;
4
+ --g360-accent: #00d084;
5
+ --g360-text: #f0f4f8;
6
+ --g360-muted: #94a3b8;
7
+ --g360-glass: rgba(21, 30, 46, 0.75);
8
+ --g360-blur: 16px;
9
+ --g360-rounded: 16px;
10
+ --g360-font: system-ui, -apple-system, sans-serif;
11
+ }
12
+
13
+ * { box-sizing: border-box; margin: 0; padding: 0; }
14
+
15
+ body {
16
+ font-family: var(--g360-font);
17
+ background: var(--g360-bg);
18
+ color: var(--g360-text);
19
+ line-height: 1.6;
20
+ -webkit-font-smoothing: antialiased;
21
+ }
22
+
23
+ .app {
24
+ min-height: 100vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+
29
+ .container {
30
+ max-width: 480px;
31
+ margin: 0 auto;
32
+ padding: 24px 16px;
33
+ flex: 1;
34
+ }
35
+
36
+ .g360-header {
37
+ background: var(--g360-surface);
38
+ border-bottom: 1px solid rgba(255,255,255,0.08);
39
+ padding: 16px 24px;
40
+ }
41
+
42
+ .header-content {
43
+ max-width: 480px;
44
+ margin: 0 auto;
45
+ display: flex;
46
+ align-items: baseline;
47
+ gap: 12px;
48
+ }
49
+
50
+ .logo {
51
+ font-size: 24px;
52
+ font-weight: 800;
53
+ color: var(--g360-accent);
54
+ }
55
+
56
+ .subtitle {
57
+ font-size: 14px;
58
+ color: var(--g360-muted);
59
+ }
60
+
61
+ h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
62
+
63
+ .muted { color: var(--g360-muted); font-size: 14px; }
64
+
65
+ .g360-footer {
66
+ text-align: center;
67
+ padding: 12px;
68
+ font-size: 11px;
69
+ color: var(--g360-muted);
70
+ background: var(--g360-surface);
71
+ border-top: 1px solid rgba(255,255,255,0.08);
72
+ }
73
+
74
+ .g360-loading-overlay {
75
+ position: fixed;
76
+ inset: 0;
77
+ background: rgba(11, 18, 32, 0.9);
78
+ display: flex;
79
+ flex-direction: column;
80
+ align-items: center;
81
+ justify-content: center;
82
+ gap: 16px;
83
+ z-index: 1000;
84
+ }
85
+
86
+ .g360-loading-spinner {
87
+ width: 40px;
88
+ height: 40px;
89
+ border: 3px solid var(--g360-surface);
90
+ border-top-color: var(--g360-accent);
91
+ border-radius: 50%;
92
+ animation: spin 0.8s linear infinite;
93
+ }
94
+
95
+ @keyframes spin { to { transform: rotate(360deg); } }
96
+
97
+ .g360-card {
98
+ background: var(--g360-surface);
99
+ border-radius: var(--g360-rounded);
100
+ padding: 16px;
101
+ border: 1px solid rgba(255,255,255,0.08);
102
+ }
103
+
104
+ .g360-glass {
105
+ background: var(--g360-glass);
106
+ backdrop-filter: blur(var(--g360-blur));
107
+ -webkit-backdrop-filter: blur(var(--g360-blur));
108
+ border: 1px solid rgba(255,255,255,0.1);
109
+ }
110
+
111
+ .g360-btn {
112
+ background: var(--g360-accent);
113
+ color: var(--g360-bg);
114
+ border: none;
115
+ border-radius: 8px;
116
+ padding: 10px 20px;
117
+ font-size: 14px;
118
+ font-weight: 700;
119
+ cursor: pointer;
120
+ transition: opacity 0.2s;
121
+ }
122
+
123
+ .g360-btn:hover { opacity: 0.85; }
124
+
125
+ @media (prefers-color-scheme: light) {
126
+ :root {
127
+ --g360-bg: #ffffff;
128
+ --g360-surface: #f5f5f5;
129
+ --g360-muted: #6b7280;
130
+ --g360-text: #1f2937;
131
+ --g360-glass: rgba(255, 255, 255, 0.75);
132
+ }
133
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 3000,
8
+ },
9
+ build: {
10
+ outDir: 'dist',
11
+ },
12
+ });
package/src/cli.js CHANGED
@@ -31,10 +31,12 @@ program
31
31
  .command('init')
32
32
  .argument('<name>', 'Project name')
33
33
  .option('-t, --template <type>', 'Project template type', 'web-pwa')
34
- .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom)', 'corporativo-movil')
34
+ .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, cipsa, cipsa-movil)', 'corporativo-movil')
35
35
  .option('-d, --dir <path>', 'Target directory', '.')
36
36
  .option('--dry-run', 'Preview without creating files')
37
37
  .option('--force', 'Overwrite existing files')
38
+ .option('--portable', 'Create portable version (for python-flet, python-cli)')
39
+ .option('--no-portable', 'Skip portable version creation')
38
40
  .action(init);
39
41
 
40
42
  program
@@ -46,7 +48,7 @@ program
46
48
 
47
49
  program
48
50
  .command('bring')
49
- .argument('[asset]', 'Specific asset to bring (template/component/skill/all)')
51
+ .argument('[asset]', 'Asset to bring: brand, brand/cipsa, brand/g360, components, templates, skills, all')
50
52
  .option('-p, --path <path>', 'Target path', '.')
51
53
  .option('--dry-run', 'Preview without copying')
52
54
  .option('--force', 'Overwrite existing files')
@@ -54,7 +56,7 @@ program
54
56
 
55
57
  program
56
58
  .command('list')
57
- .argument('[type]', 'Type to list (templates/components/skills/all)', 'all')
59
+ .argument('[type]', 'Type to list (templates/components/skills/snippets/brands/all)', 'all')
58
60
  .option('--json', 'Output as JSON')
59
61
  .action(list);
60
62
 
@@ -81,6 +83,8 @@ program
81
83
  .option('--orphans', 'Remove orphan files (unreferenced)')
82
84
  .option('--organize', 'Show misplaced files suggestions')
83
85
  .option('--all', 'Run all cleanup tasks')
86
+ .option('--github', 'Prepare repo for GitHub (update .gitignore)')
87
+ .option('--pre-push', 'Alias for --github (prepare before push)')
84
88
  .action(clean);
85
89
 
86
90
  program
@@ -96,7 +100,7 @@ program
96
100
  program
97
101
  .command('convert')
98
102
  .argument('[path]', 'Path to convert', '.')
99
- .option('-s, --skill <skill>', 'Skill to apply', 'corporativo-movil')
103
+ .option('-s, --skill <skill>', 'Skill to apply (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, cipsa, cipsa-movil)', 'corporativo-movil')
100
104
  .option('--dry-run', 'Preview without applying')
101
105
  .option('--restructure', 'Restructure project files')
102
106
  .option('--force', 'Force dangerous changes')
@@ -10,7 +10,7 @@ export async function bring(asset, options) {
10
10
  const { path: targetPath = '.', dryRun = false, force = false } = options;
11
11
  const targetDir = path.join(process.cwd(), targetPath);
12
12
  const assetsPath = path.join(__dirname, '../assets');
13
-
13
+
14
14
  console.log(chalk.bold.cyan('\n📦 G360 Asset Manager\n'));
15
15
 
16
16
  if (!asset || asset === 'all') {
@@ -19,14 +19,89 @@ export async function bring(asset, options) {
19
19
  }
20
20
 
21
21
  const assetPath = path.join(assetsPath, asset);
22
-
22
+
23
23
  if (!fs.existsSync(assetPath)) {
24
24
  console.error(chalk.red(`❌ Asset "${asset}" not found.`));
25
- console.log(chalk.gray('\nRun "g360 list assets" to see available assets.'));
25
+ console.log(chalk.gray('\nRun "g360 list all" to see available assets.'));
26
26
  return;
27
27
  }
28
28
 
29
29
  await copyAssets(assetPath, targetDir, dryRun, force);
30
+
31
+ if (asset.startsWith('brand')) {
32
+ const brandName = asset.split('/')[1];
33
+ if (brandName) {
34
+ await applyBrand(targetDir, brandName, dryRun);
35
+ }
36
+ }
37
+ }
38
+
39
+ async function applyBrand(targetDir, brandName, dryRun) {
40
+ const brandConfigPath = path.join(targetDir, 'g360', 'brand', 'brand.json');
41
+ if (!fs.existsSync(brandConfigPath)) {
42
+ console.log(chalk.yellow(`\n⚠ brand.json not found. Run 'g360 bring brand' first.`));
43
+ return;
44
+ }
45
+
46
+ let brandConfig;
47
+ try {
48
+ brandConfig = fs.readJsonSync(brandConfigPath);
49
+ } catch {
50
+ console.log(chalk.yellow(`\n⚠ Could not parse brand.json.`));
51
+ return;
52
+ }
53
+
54
+ const brand = brandConfig.brands?.[brandName];
55
+ if (!brand) {
56
+ console.log(chalk.yellow(`\n⚠ Brand "${brandName}" not found in brand.json`));
57
+ return;
58
+ }
59
+
60
+ const skillCandidates = [
61
+ path.join(targetDir, 'skill.json'),
62
+ path.join(targetDir, 'src', 'core', 'skill.json'),
63
+ ];
64
+
65
+ let skillPath = null;
66
+ for (const c of skillCandidates) {
67
+ if (fs.existsSync(c)) {
68
+ skillPath = c;
69
+ break;
70
+ }
71
+ }
72
+
73
+ if (!skillPath) {
74
+ console.log(chalk.yellow(`\n⚠ No skill.json found. Create one with 'g360 init'.`));
75
+ return;
76
+ }
77
+
78
+ if (dryRun) {
79
+ console.log(chalk.yellow(`\n📋 DRY RUN - Would update ${skillPath}`));
80
+ return;
81
+ }
82
+
83
+ const skill = fs.readJsonSync(skillPath);
84
+ skill.brand = brandName;
85
+
86
+ if (!skill.guidelines) skill.guidelines = {};
87
+ skill.guidelines.logo = brand.default_logo;
88
+ skill.guidelines.description = brand.description;
89
+
90
+ if (brand.signature) {
91
+ if (!skill.signature) skill.signature = {};
92
+ skill.signature.mode = brand.signature.mode;
93
+ skill.signature.text = brand.signature.text;
94
+ }
95
+
96
+ if (brand.primary_color) {
97
+ if (!skill.colors) skill.colors = {};
98
+ skill.colors.accent = brand.primary_color;
99
+ }
100
+
101
+ fs.writeJsonSync(skillPath, skill, { spaces: 2 });
102
+ console.log(chalk.green(`\n✅ Brand "${brandName}" applied to ${skillPath}`));
103
+ console.log(chalk.gray(` Logo: ${brand.default_logo}`));
104
+ console.log(chalk.gray(` Signature: ${brand.signature?.text || 'none'}`));
30
105
  }
31
106
 
32
107
  async function copyAssets(src, dest, dryRun, force) {
@@ -36,18 +111,18 @@ async function copyAssets(src, dest, dryRun, force) {
36
111
  }
37
112
 
38
113
  const progressBar = progress('Copying assets...');
39
-
114
+
40
115
  try {
41
116
  const items = fs.readdirSync(src);
42
117
  for (const item of items) {
43
118
  const srcPath = path.join(src, item);
44
119
  const destPath = path.join(dest, 'g360', item);
45
-
120
+
46
121
  if (fs.statSync(srcPath).isDirectory()) {
47
122
  await fs.copy(srcPath, destPath, { overwrite: force });
48
123
  }
49
124
  }
50
-
125
+
51
126
  progressBar.stop();
52
127
  console.log(chalk.green('\n✅ Assets copied successfully!\n'));
53
128
  } catch (error) {
@@ -21,14 +21,16 @@ const DEAD_PATTERNS = [
21
21
  const DUPLICATE_EXTENSIONS = ['.bak', '.orig', '.swp', '.swo', '~'];
22
22
 
23
23
  export async function clean(projectPath, options) {
24
- const {
25
- dryRun = false,
24
+ const {
25
+ dryRun = false,
26
26
  force = false,
27
27
  dead = false,
28
28
  duplicates = false,
29
29
  orphans = false,
30
30
  organize = false,
31
- all = false
31
+ all = false,
32
+ github = false,
33
+ prePush = false
32
34
  } = options;
33
35
 
34
36
  const targetDir = path.join(process.cwd(), projectPath);
@@ -123,8 +125,53 @@ export async function clean(projectPath, options) {
123
125
  return;
124
126
  }
125
127
 
128
+ if (github || prePush) {
129
+ console.log(chalk.bold.cyan('\n📦 Modo Pre-Push GitHub\n'));
130
+ console.log(chalk.gray('Limpiando para subir a repositorio remoto...\n'));
131
+
132
+ const gitIgnore = path.join(targetDir, '.gitignore');
133
+ const requiredIgnores = [
134
+ 'node_modules/',
135
+ '__pycache__/',
136
+ '*.pyc',
137
+ '.env',
138
+ 'dist/',
139
+ 'build/',
140
+ '*.log',
141
+ '.cache/',
142
+ 'coverage/',
143
+ '*.egg-info/',
144
+ '.pytest_cache/'
145
+ ];
146
+
147
+ if (fs.existsSync(gitIgnore)) {
148
+ const currentIgnore = await fs.readFile(gitIgnore, 'utf-8');
149
+ let updatedIgnore = currentIgnore;
150
+
151
+ for (const pattern of requiredIgnores) {
152
+ if (!currentIgnore.includes(pattern)) {
153
+ updatedIgnore += `\n${pattern}`;
154
+ }
155
+ }
156
+
157
+ await fs.writeFile(gitIgnore, updatedIgnore);
158
+ console.log(chalk.green(' ✅ .gitignore actualizado'));
159
+ } else {
160
+ await fs.writeFile(gitIgnore, requiredIgnores.join('\n') + '\n');
161
+ console.log(chalk.green(' ✅ .gitignore creado'));
162
+ }
163
+
164
+ console.log(chalk.green('\n✅ Repo listo para GitHub!\n'));
165
+ console.log(chalk.gray('Para subir:'));
166
+ console.log(` ${chalk.cyan('git add .')}`);
167
+ console.log(` ${chalk.cyan('git commit -m "clean: preparing for push"')}`);
168
+ console.log(` ${chalk.cyan('git push')}\n`);
169
+
170
+ return;
171
+ }
172
+
126
173
  console.log(chalk.cyan('\n🚀 Aplicando limpieza...'));
127
-
174
+
128
175
  let cleaned = 0;
129
176
 
130
177
  for (const file of issues.dead) {
@@ -5,19 +5,61 @@ 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 inquirer from 'inquirer';
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
 
12
+ const PORTABLE_TEMPLATES = ['python-flet', 'python-flet-migrate', 'python-cli', 'python-customtkinter'];
13
+
14
+ async function askPortableOption(template) {
15
+ if (!PORTABLE_TEMPLATES.includes(template)) {
16
+ return false;
17
+ }
18
+
19
+ const answers = await inquirer.prompt([
20
+ {
21
+ type: 'confirm',
22
+ name: 'portable',
23
+ message: '¿Deseas crear una versión portable del proyecto? (ejecutable standalone)',
24
+ default: false
25
+ }
26
+ ]);
27
+
28
+ return answers.portable;
29
+ }
30
+
11
31
  export async function init(name, options) {
12
- const { template = 'web-pwa', skill = 'corporativo-movil', dir = '.', dryRun = false, force = false } = options;
32
+ const {
33
+ template = 'web-pwa',
34
+ skill = 'corporativo-movil',
35
+ dir = '.',
36
+ dryRun = false,
37
+ force = false,
38
+ portable = null
39
+ } = options;
40
+
13
41
  const targetDir = path.join(process.cwd(), dir, name);
14
-
42
+
15
43
  console.log(chalk.bold.cyan('\n🚀 G360 Project Initialization\n'));
16
44
  console.log(`Project: ${chalk.yellow(name)}`);
17
45
  console.log(`Template: ${chalk.blue(template)}`);
18
46
  console.log(`Skill: ${chalk.magenta(skill)}`);
19
47
  console.log(`Target: ${chalk.gray(targetDir)}\n`);
20
48
 
49
+ let wantPortable = false;
50
+
51
+ if (portable === true) {
52
+ wantPortable = true;
53
+ console.log(chalk.yellow('📦 Versión portable: SÍ (flag)\n'));
54
+ } else if (portable === false) {
55
+ console.log(chalk.gray('📦 Versión portable: NO\n'));
56
+ } else {
57
+ wantPortable = await askPortableOption(template);
58
+ if (wantPortable) {
59
+ console.log(chalk.yellow('📦 Versión portable: SÍ\n'));
60
+ }
61
+ }
62
+
21
63
  const templatesPath = path.join(__dirname, '../assets/templates');
22
64
 
23
65
  if (!fs.existsSync(templatesPath)) {
@@ -49,21 +91,31 @@ export async function init(name, options) {
49
91
  const progressBar = progress('Creating project...');
50
92
 
51
93
  try {
52
- // Asegurar que el directorio base existe
53
94
  await fs.ensureDir(path.dirname(targetDir));
54
95
 
55
96
  await fs.copy(templateDir, targetDir);
56
- await manifest.init(targetDir, { name, template, version: '1.0.0' });
57
-
58
- // Aplicar el skill pasando el directorio destino explícitamente
97
+ await manifest.init(targetDir, { name, template, version: '1.0.0', portable: wantPortable });
98
+
99
+ if (wantPortable) {
100
+ const portableDir = path.join(targetDir, 'portable');
101
+ await fs.ensureDir(portableDir);
102
+ const buildScript = path.join(targetDir, 'build-portable.bat');
103
+ if (fs.existsSync(buildScript)) {
104
+ await fs.copy(buildScript, path.join(portableDir, 'build.bat'));
105
+ }
106
+ console.log(chalk.gray(' 📦 Carpeta portable/ creada para builds\n'));
107
+ }
108
+
59
109
  await setSkill(skill, { force: true, verbose: options.verbose, cwd: targetDir });
60
110
 
61
- // Crear estructura estándar G360 para auditoría y desarrollo guiado
62
111
  await createG360Structure(targetDir, templatesPath);
63
112
 
64
113
  progressBar.stop();
65
-
114
+
66
115
  console.log(chalk.green('\n✅ Project created successfully!\n'));
116
+ if (wantPortable) {
117
+ console.log(chalk.yellow('📦 Versión portable habilitada\n'));
118
+ }
67
119
  console.log(chalk.gray('Next steps:'));
68
120
  console.log(` ${chalk.cyan('cd')} ${name}`);
69
121
  console.log(` ${chalk.cyan('g360 bring')}`);
@@ -14,13 +14,15 @@ export async function list(type, options) {
14
14
  const assets = {
15
15
  templates: [],
16
16
  components: [],
17
- skills: []
17
+ skills: [],
18
+ brands: []
18
19
  };
19
20
 
20
21
  const templatesPath = path.join(assetsPath, 'templates');
21
22
  const componentsPath = path.join(assetsPath, 'components');
22
- const skillsPath = path.join(assetsPath, 'skills');
23
- const enginePath = path.join(assetsPath, 'engine');
23
+ const configPath = path.join(assetsPath, 'config');
24
+ const snippetsPath = path.join(assetsPath, 'snippets');
25
+ const brandPath = path.join(assetsPath, 'brand');
24
26
 
25
27
  if (fs.existsSync(templatesPath)) {
26
28
  assets.templates = fs.readdirSync(templatesPath);
@@ -28,11 +30,53 @@ export async function list(type, options) {
28
30
  if (fs.existsSync(componentsPath)) {
29
31
  assets.components = fs.readdirSync(componentsPath).filter(f => f.endsWith('.jsx') || f.endsWith('.js'));
30
32
  }
31
- if (fs.existsSync(skillsPath)) {
32
- assets.skills = fs.readdirSync(skillsPath).filter(f => f.endsWith('.js') || f.endsWith('.mjs'));
33
+
34
+ if (fs.existsSync(configPath)) {
35
+ const g360SkillsPath = path.join(configPath, 'g360-skills.json');
36
+ if (fs.existsSync(g360SkillsPath)) {
37
+ try {
38
+ const skillsData = fs.readJsonSync(g360SkillsPath);
39
+ assets.skills = skillsData.skills.map(skill => ({
40
+ name: skill.name,
41
+ description: skill.description,
42
+ device: skill.device
43
+ }));
44
+ } catch (error) {
45
+ console.log(chalk.yellow(`Warning: Could not read skills config: ${error.message}`));
46
+ }
47
+ }
48
+ }
49
+
50
+ if (fs.existsSync(snippetsPath)) {
51
+ const snippetsJsonPath = path.join(snippetsPath, 'snippets.json');
52
+ if (fs.existsSync(snippetsJsonPath)) {
53
+ try {
54
+ const snippetsData = fs.readJsonSync(snippetsJsonPath);
55
+ assets.snippets = snippetsData.snippets.map(snippet => ({
56
+ name: snippet.name,
57
+ description: snippet.description,
58
+ language: snippet.language
59
+ }));
60
+ } catch (error) {
61
+ console.log(chalk.yellow(`Warning: Could not read snippets config: ${error.message}`));
62
+ }
63
+ }
33
64
  }
34
- if (fs.existsSync(enginePath)) {
35
- assets.engine = fs.readdirSync(enginePath).filter(f => f.startsWith('g360-skill-'));
65
+
66
+ if (fs.existsSync(brandPath)) {
67
+ const brandConfigPath = path.join(brandPath, 'brand.json');
68
+ if (fs.existsSync(brandConfigPath)) {
69
+ try {
70
+ const brandData = fs.readJsonSync(brandConfigPath);
71
+ assets.brands = Object.entries(brandData.brands || {}).map(([key, val]) => ({
72
+ name: key,
73
+ description: val.description,
74
+ logo: val.default_logo
75
+ }));
76
+ } catch (error) {
77
+ console.log(chalk.yellow(`Warning: Could not read brand config: ${error.message}`));
78
+ }
79
+ }
36
80
  }
37
81
 
38
82
  if (json) {
@@ -60,12 +104,38 @@ export async function list(type, options) {
60
104
 
61
105
  if (!type || type === 'all' || type === 'skills') {
62
106
  console.log(chalk.bold.yellow('\n⚡ Skills:'));
63
- if (assets.engine.length) {
64
- assets.engine.forEach(s => console.log(chalk.gray(` - ${s}`)));
107
+ if (assets.skills.length) {
108
+ assets.skills.forEach(skill => {
109
+ console.log(chalk.gray(` - ${skill.name}`));
110
+ console.log(chalk.gray(` ${skill.description}`));
111
+ console.log(chalk.gray(` Device: ${skill.device}`));
112
+ });
65
113
  } else {
66
114
  console.log(chalk.gray(' No skills found'));
67
115
  }
68
116
  }
69
117
 
118
+ if (!type || type === 'all' || type === 'brands') {
119
+ console.log(chalk.bold.yellow('\n🎨 Brands:'));
120
+ if (assets.brands.length) {
121
+ assets.brands.forEach(b => console.log(chalk.gray(` - ${b.name}`)));
122
+ } else {
123
+ console.log(chalk.gray(' No brands found'));
124
+ }
125
+ }
126
+
127
+ if (!type || type === 'all' || type === 'snippets') {
128
+ console.log(chalk.bold.yellow('\n📝 Snippets:'));
129
+ if (assets.snippets && assets.snippets.length) {
130
+ assets.snippets.forEach(snippet => {
131
+ console.log(chalk.gray(` - ${snippet.name}`));
132
+ console.log(chalk.gray(` ${snippet.description}`));
133
+ console.log(chalk.gray(` Language: ${snippet.language}`));
134
+ });
135
+ } else {
136
+ console.log(chalk.gray(' No snippets found'));
137
+ }
138
+ }
139
+
70
140
  console.log();
71
141
  }
@@ -21,20 +21,24 @@ export async function signature(command, options) {
21
21
 
22
22
  let htmlContent = fs.readFileSync(indexHtmlPath, 'utf8');
23
23
 
24
- // Verificar si ya esta instalado
25
- if (htmlContent.includes('g360-signature') && !force) {
26
- console.log(chalk.yellow('⚠️ g360-signature ya se encuentra instalado'));
27
- console.log(chalk.gray('Usa --force para reinstalar'));
28
- return;
29
- }
30
-
31
- // Agregar script CDN antes del cierre del body
32
24
  const scriptTag = ' <script type="module" src="https://unpkg.com/g360-signature@latest/index.js"></script>';
33
25
  const signatureComponent = `
34
26
  <!-- Firma Oficial G360 -->
35
27
  <g360-signature mode="powered" style="position: fixed; bottom: 16px; right: 16px; z-index: 99999;"></g360-signature>
36
28
  `;
37
29
 
30
+ // Si --force es true, eliminamos cualquier instancia existente para una reinstalación limpia.
31
+ // De lo contrario, si ya está instalado y no se fuerza, salimos.
32
+ if (force) {
33
+ htmlContent = htmlContent.replace(new RegExp(scriptTag.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
34
+ htmlContent = htmlContent.replace(new RegExp(signatureComponent.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
35
+ htmlContent = htmlContent.replace(/\n\s*\n/g, '\n'); // Limpiar posibles líneas vacías extra
36
+ } else if (htmlContent.includes('g360-signature')) {
37
+ console.log(chalk.yellow('⚠️ g360-signature ya se encuentra instalado'));
38
+ console.log(chalk.gray('Usa --force para reinstalar'));
39
+ return;
40
+ }
41
+
38
42
  if (!htmlContent.includes(scriptTag)) {
39
43
  htmlContent = htmlContent.replace('</body>', `${scriptTag}\n </body>`);
40
44
  }