g360-cli 1.0.0 → 1.1.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.
Files changed (30) hide show
  1. package/README.md +626 -28
  2. package/package.json +7 -2
  3. package/src/assets/config/g360-skills.json +172 -0
  4. package/src/assets/templates/lit-web/index.html +20 -0
  5. package/src/assets/templates/lit-web/package.json +19 -0
  6. package/src/assets/templates/lit-web/src/components/app-root.js +73 -0
  7. package/src/assets/templates/lit-web/src/core/skill.json +18 -0
  8. package/src/assets/templates/lit-web/src/index.js +8 -0
  9. package/src/assets/templates/lit-web/src/styles/main.css +91 -0
  10. package/src/assets/templates/lit-web/vite.config.js +10 -0
  11. package/src/assets/templates/solid-web/index.html +21 -0
  12. package/src/assets/templates/solid-web/package.json +20 -0
  13. package/src/assets/templates/solid-web/src/components/App.jsx +21 -0
  14. package/src/assets/templates/solid-web/src/core/skill.json +18 -0
  15. package/src/assets/templates/solid-web/src/index.jsx +11 -0
  16. package/src/assets/templates/solid-web/src/styles/main.css +115 -0
  17. package/src/assets/templates/solid-web/vite.config.js +12 -0
  18. package/src/assets/templates/svelte-web/package.json +21 -0
  19. package/src/assets/templates/svelte-web/src/app.css +77 -0
  20. package/src/assets/templates/svelte-web/src/app.html +19 -0
  21. package/src/assets/templates/svelte-web/src/core/skill.json +18 -0
  22. package/src/assets/templates/svelte-web/src/routes/+page.svelte +38 -0
  23. package/src/assets/templates/svelte-web/svelte.config.js +16 -0
  24. package/src/assets/templates/web-pwa/index.html +19 -21
  25. package/src/assets/templates/web-pwa/package.json +18 -2
  26. package/src/cli.js +26 -1
  27. package/src/commands/clean.js +288 -30
  28. package/src/commands/convert.js +388 -0
  29. package/src/commands/init.js +2 -1
  30. package/src/commands/set-skill.js +84 -0
@@ -0,0 +1,77 @@
1
+ /* ============================================
2
+ * G360 Theme - Estilos principales
3
+ * Sistema de diseño para proyectos G360
4
+ * ============================================ */
5
+
6
+ /* === VARIABLES G360 === */
7
+ :root {
8
+ /* Colores G360 - Oscuro (Default) */
9
+ --g360-bg: #0b1220;
10
+ --g360-surface: #151e2e;
11
+ --g360-accent: #00d084;
12
+ --g360-accent-dark: #00796B;
13
+ --g360-text: #f0f4f8;
14
+ --g360-muted: #94a3b8;
15
+ --g360-border: rgba(255, 255, 255, 0.1);
16
+
17
+ /* Efectos */
18
+ --g360-glass: rgba(21, 30, 46, 0.75);
19
+ --g360-blur: 16px;
20
+ --g360-rounded: 16px;
21
+
22
+ /* Espaciado */
23
+ --g360-space-xs: 4px;
24
+ --g360-space-sm: 8px;
25
+ --g360-space-md: 16px;
26
+ --g360-space-lg: 24px;
27
+ --g360-space-xl: 32px;
28
+
29
+ /* Tipografía */
30
+ --g360-font: 'Consolas', 'Monaco', 'Courier New', monospace;
31
+ --g360-font-size: 14px;
32
+ }
33
+
34
+ /* === ESTILOS BASE === */
35
+ * {
36
+ box-sizing: border-box;
37
+ margin: 0;
38
+ padding: 0;
39
+ }
40
+
41
+ body {
42
+ font-family: var(--g360-font);
43
+ font-size: var(--g360-font-size);
44
+ background: var(--g360-bg);
45
+ color: var(--g360-text);
46
+ line-height: 1.5;
47
+ -webkit-font-smoothing: antialiased;
48
+ }
49
+
50
+ /* === UTILIDADES === */
51
+ .g360-card {
52
+ background: var(--g360-surface);
53
+ border: 1px solid var(--g360-border);
54
+ border-radius: var(--g360-rounded);
55
+ padding: var(--g360-space-md);
56
+ }
57
+
58
+ .g360-btn {
59
+ background: var(--g360-accent);
60
+ color: var(--g360-bg);
61
+ border: none;
62
+ border-radius: var(--g360-rounded);
63
+ padding: var(--g360-space-sm) var(--g360-space-md);
64
+ font-weight: 700;
65
+ cursor: pointer;
66
+ }
67
+
68
+ /* === MODO CLARO === */
69
+ @media (prefers-color-scheme: light) {
70
+ :root {
71
+ --g360-bg: #ffffff;
72
+ --g360-surface: #f5f5f5;
73
+ --g360-text: #1f2937;
74
+ --g360-muted: #6b7280;
75
+ --g360-border: rgba(0, 0, 0, 0.1);
76
+ }
77
+ }
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="author" content="@carloscus" />
7
+ <meta name="description" content="Proyecto G360 desarrollado con SvelteKit" />
8
+ <meta name="theme-color" content="#00d084" />
9
+ <title>Mi Proyecto G360</title>
10
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
11
+ %sveltekit.head%
12
+ </head>
13
+ <body data-sveltekit-preload-data="hover">
14
+ <div style="display: contents">%sveltekit.body%</div>
15
+
16
+ <!-- Firma G360 -->
17
+ <g360-signature mode="powered" style="position: fixed; bottom: 16px; right: 16px; z-index: 100;"></g360-signature>
18
+ </body>
19
+ </html>
@@ -0,0 +1,18 @@
1
+ {
2
+ "skill": "corporativo",
3
+ "device": "movil",
4
+ "template": "svelte-web",
5
+ "version": "1.0.0",
6
+ "createdAt": "2024-01-01T00:00:00.000Z",
7
+ "colors": {
8
+ "bg": "#0b1220",
9
+ "surface": "#151e2e",
10
+ "accent": "#00d084",
11
+ "text": "#f0f4f8",
12
+ "muted": "#94a3b8"
13
+ },
14
+ "signature": {
15
+ "mode": "powered",
16
+ "text": "powered by G360"
17
+ }
18
+ }
@@ -0,0 +1,38 @@
1
+ <script>
2
+ let titulo = 'Mi Proyecto G360';
3
+ </script>
4
+
5
+ <div class="container">
6
+ <header>
7
+ <h1>{titulo}</h1>
8
+ </header>
9
+ <main>
10
+ <slot />
11
+ </main>
12
+ </div>
13
+
14
+ <style>
15
+ .container {
16
+ max-width: 480px;
17
+ margin: 0 auto;
18
+ padding: 16px;
19
+ min-height: 100vh;
20
+ background: var(--g360-bg);
21
+ }
22
+
23
+ header {
24
+ padding: 16px 0;
25
+ border-bottom: 1px solid var(--g360-surface);
26
+ }
27
+
28
+ h1 {
29
+ margin: 0;
30
+ font-size: 24px;
31
+ font-weight: 700;
32
+ color: var(--g360-accent);
33
+ }
34
+
35
+ main {
36
+ padding: 24px 0;
37
+ }
38
+ </style>
@@ -0,0 +1,16 @@
1
+ import adapter from '@sveltejs/adapter-static';
2
+
3
+ /** @type {import('@sveltejs/kit').Config} */
4
+ const config = {
5
+ kit: {
6
+ adapter: adapter({
7
+ pages: 'build',
8
+ assets: 'build',
9
+ fallback: 'index.html',
10
+ precompress: false,
11
+ strict: true
12
+ })
13
+ }
14
+ };
15
+
16
+ export default config;
@@ -1,24 +1,22 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="es">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="description" content="G360 Project">
7
- <meta name="theme-color" content="#3B82F6">
8
- <title>G360 Project</title>
9
- <link rel="manifest" href="/manifest.json">
10
- <link rel="stylesheet" href="/styles.css">
11
- </head>
12
- <body>
13
- <header class="g360-header">
14
- <h1>G360 Project</h1>
15
- </header>
16
- <main>
17
- <p>Your G360 project is ready!</p>
18
- </main>
19
- <footer class="g360-footer">
20
- <small>Powered by G360 Ecosystem</small>
21
- </footer>
22
- <script src="/app.js"></script>
23
- </body>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="author" content="@carloscus" />
7
+ <meta name="description" content="Proyecto G360 desarrollado con React + Vite" />
8
+ <meta name="theme-color" content="#00d084" />
9
+ <title>Mi Proyecto G360</title>
10
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
11
+ <link rel="manifest" href="/manifest.json" />
12
+ <link rel="stylesheet" href="/styles.css" />
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+
17
+ <!-- Firma G360 -->
18
+ <g360-signature mode="powered" style="position: fixed; bottom: 16px; right: 16px; z-index: 100;"></g360-signature>
19
+
20
+ <script type="module" src="/app.js"></script>
21
+ </body>
24
22
  </html>
@@ -1,5 +1,21 @@
1
1
  {
2
- "name": "web-pwa-template",
2
+ "name": "mi-react-proyecto",
3
3
  "version": "1.0.0",
4
- "private": true
4
+ "description": "Proyecto G360 creado con React + Vite",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.2.0",
17
+ "vite": "^5.0.0"
18
+ },
19
+ "author": "@carloscus",
20
+ "license": "MIT"
5
21
  }
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { Command } from 'commander';
4
4
  import chalk from 'chalk';
5
5
  import { init } from './commands/init.js';
6
+ import { setSkill } from './commands/set-skill.js';
6
7
  import { bring } from './commands/bring.js';
7
8
  import { list } from './commands/list.js';
8
9
  import { present } from './commands/present.js';
@@ -10,23 +11,32 @@ import { audit } from './commands/audit.js';
10
11
  import { clean } from './commands/clean.js';
11
12
  import { health } from './commands/health.js';
12
13
  import { update } from './commands/update.js';
14
+ import { convert } from './commands/convert.js';
13
15
 
14
16
  const program = new Command();
15
17
 
16
18
  program
17
19
  .name('g360')
18
20
  .description('CLI tool for bootstrapping G360 ecosystem projects')
19
- .version('1.0.0');
21
+ .version('1.1.1');
20
22
 
21
23
  program
22
24
  .command('init')
23
25
  .argument('<name>', 'Project name')
24
26
  .option('-t, --template <type>', 'Project template type', 'web-pwa')
27
+ .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom)', 'corporativo-movil')
25
28
  .option('-d, --dir <path>', 'Target directory', '.')
26
29
  .option('--dry-run', 'Preview without creating files')
27
30
  .option('--force', 'Overwrite existing files')
28
31
  .action(init);
29
32
 
33
+ program
34
+ .command('set-skill')
35
+ .argument('<skill>', 'Skill name to set')
36
+ .option('--verbose', 'Show detailed output')
37
+ .option('--force', 'Overwrite existing skill')
38
+ .action(setSkill);
39
+
30
40
  program
31
41
  .command('bring')
32
42
  .argument('[asset]', 'Specific asset to bring (template/component/skill/all)')
@@ -59,6 +69,11 @@ program
59
69
  .argument('[path]', 'Path to clean', '.')
60
70
  .option('--dry-run', 'Preview files to be removed')
61
71
  .option('--force', 'Skip confirmation')
72
+ .option('--dead', 'Remove dead/deprecated files')
73
+ .option('--duplicates', 'Remove duplicate files')
74
+ .option('--orphans', 'Remove orphan files (unreferenced)')
75
+ .option('--organize', 'Show misplaced files suggestions')
76
+ .option('--all', 'Run all cleanup tasks')
62
77
  .action(clean);
63
78
 
64
79
  program
@@ -71,4 +86,14 @@ program
71
86
  .option('--check', 'Check for updates without installing')
72
87
  .action(update);
73
88
 
89
+ program
90
+ .command('convert')
91
+ .argument('[path]', 'Path to convert', '.')
92
+ .option('-s, --skill <skill>', 'Skill to apply', 'corporativo-movil')
93
+ .option('--dry-run', 'Preview without applying')
94
+ .option('--restructure', 'Restructure project files')
95
+ .option('--force', 'Force dangerous changes')
96
+ .option('--backup', 'Create backup before converting')
97
+ .action(convert);
98
+
74
99
  program.parse();
@@ -1,65 +1,323 @@
1
+ /**
2
+ * @file clean.js
3
+ * @description Comando para limpiar código muerto, descontinuado y redundante
4
+ * @author @carloscus
5
+ */
6
+
1
7
  import chalk from 'chalk';
2
8
  import fs from 'fs-extra';
3
9
  import path from 'path';
4
- import { manifest } from '../lib/manifest.js';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ const DEAD_PATTERNS = [
15
+ 'backup', 'backup-old', 'backups',
16
+ 'temp', 'tmp', '.tmp',
17
+ 'old', 'deprecated', 'archive',
18
+ 'unused', 'trash', 'garbage'
19
+ ];
20
+
21
+ const DUPLICATE_EXTENSIONS = ['.bak', '.orig', '.swp', '.swo', '~'];
5
22
 
6
23
  export async function clean(projectPath, options) {
7
- const { dryRun = false, force = false } = options;
24
+ const {
25
+ dryRun = false,
26
+ force = false,
27
+ dead = false,
28
+ duplicates = false,
29
+ orphans = false,
30
+ organize = false,
31
+ all = false
32
+ } = options;
33
+
8
34
  const targetDir = path.join(process.cwd(), projectPath);
9
35
 
10
- console.log(chalk.bold.cyan('\n🧹 G360 Clean\n'));
36
+ console.log(chalk.bold.cyan('\n🧹 G360 Clean - Limpeza de Proyecto\n'));
11
37
  console.log(chalk.gray(`Path: ${targetDir}\n`));
12
38
 
13
39
  if (!fs.existsSync(targetDir)) {
14
- console.error(chalk.red(`❌ Path not found: ${targetDir}`));
40
+ console.error(chalk.red(`❌ Path no encontrado: ${targetDir}`));
15
41
  return;
16
42
  }
17
43
 
18
- const g360Dir = path.join(targetDir, 'g360');
19
- if (!fs.existsSync(g360Dir)) {
20
- console.log(chalk.yellow('No g360 assets found to clean.'));
21
- return;
44
+ const allFiles = await getAllFiles(targetDir);
45
+ const issues = {
46
+ dead: [],
47
+ duplicates: [],
48
+ orphans: [],
49
+ misplaced: [],
50
+ emptyDirs: []
51
+ };
52
+
53
+ if (dead || all) {
54
+ console.log(chalk.cyan('🔍 Buscando archivos muertos/descontinuados...'));
55
+ issues.dead = findDeadFiles(allFiles);
56
+ console.log(chalk.gray(` Encontrados: ${issues.dead.length}`));
57
+ }
58
+
59
+ if (duplicates || all) {
60
+ console.log(chalk.cyan('🔍 Buscando duplicados...'));
61
+ issues.duplicates = findDuplicateFiles(allFiles);
62
+ console.log(chalk.gray(` Encontrados: ${issues.duplicates.length}`));
63
+ }
64
+
65
+ if (orphans || all) {
66
+ console.log(chalk.cyan('🔍 Buscando archivos huérfanos (sin referencias)...'));
67
+ issues.orphans = await findOrphanFiles(targetDir, allFiles);
68
+ console.log(chalk.gray(` Encontrados: ${issues.orphans.length}`));
22
69
  }
23
70
 
24
- const assetsToClean = getAssetsToClean(g360Dir);
71
+ if (organize || all) {
72
+ console.log(chalk.cyan('🔍 Analizando organización de componentes...'));
73
+ issues.misplaced = analyzeFileOrganization(allFiles);
74
+ console.log(chalk.gray(` Encontrados: ${issues.misplaced.length}`));
75
+ }
76
+
77
+ console.log(chalk.cyan('🔍 Buscando carpetas vacías...'));
78
+ issues.emptyDirs = findEmptyDirectories(targetDir);
79
+ console.log(chalk.gray(` Encontradas: ${issues.emptyDirs.length}`));
80
+
81
+ const totalIssues = Object.values(issues).reduce((sum, arr) => sum + arr.length, 0);
25
82
 
26
- console.log(chalk.yellow('Assets to be removed:'));
27
- assetsToClean.forEach(asset => console.log(chalk.gray(` - ${asset}`)));
28
- console.log();
83
+ if (totalIssues === 0) {
84
+ console.log(chalk.green('\n✅ ¡Proyecto limpio! No se encontraron problemas.\n'));
85
+ return;
86
+ }
87
+
88
+ console.log(chalk.bold.yellow(`\n⚠️ Problemas encontrados: ${totalIssues}\n`));
89
+
90
+ if (issues.dead.length > 0) {
91
+ console.log(chalk.bold.red('📁 Archivos muertos/descontinuados:'));
92
+ issues.dead.forEach(f => console.log(chalk.gray(` - ${f}`)));
93
+ }
94
+
95
+ if (issues.duplicates.length > 0) {
96
+ console.log(chalk.bold.red('📋 Duplicados:'));
97
+ issues.duplicates.forEach(f => console.log(chalk.gray(` - ${f}`)));
98
+ }
99
+
100
+ if (issues.orphans.length > 0) {
101
+ console.log(chalk.bold.yellow('🔗 Archivos huérfanos (sin referencias):'));
102
+ issues.orphans.forEach(f => console.log(chalk.gray(` - ${f}`)));
103
+ }
104
+
105
+ if (issues.misplaced.length > 0) {
106
+ console.log(chalk.bold.cyan('📍 Archivos descolocados (sugerencias):'));
107
+ issues.misplaced.forEach(f => console.log(chalk.gray(` - ${f.file} → mover a ${f.suggested}`)));
108
+ }
109
+
110
+ if (issues.emptyDirs.length > 0) {
111
+ console.log(chalk.bold.magenta('📂 Carpetas vacías:'));
112
+ issues.emptyDirs.forEach(f => console.log(chalk.gray(` - ${f}`)));
113
+ }
29
114
 
30
115
  if (dryRun) {
31
- console.log(chalk.yellow('📋 DRY RUN - No files will be removed\n'));
116
+ console.log(chalk.yellow('\n📋 DRY RUN - Usa --force para aplicar limpieza\n'));
32
117
  return;
33
118
  }
34
119
 
35
120
  if (!force) {
36
- console.log(chalk.gray('Run with --force to confirm deletion.'));
121
+ console.log(chalk.yellow('\n⚠️ Usa --force para confirmar limpieza'));
122
+ console.log(chalk.gray('O usa flags específicos: --dead, --duplicates, --orphans, --organize'));
37
123
  return;
38
124
  }
39
125
 
40
- try {
41
- await fs.remove(g360Dir);
42
- await manifest.remove(targetDir);
43
- console.log(chalk.green('\n✅ G360 assets cleaned successfully!\n'));
44
- } catch (error) {
45
- console.error(chalk.red(`\n❌ Error: ${error.message}`));
126
+ console.log(chalk.cyan('\n🚀 Aplicando limpieza...'));
127
+
128
+ let cleaned = 0;
129
+
130
+ for (const file of issues.dead) {
131
+ await fs.remove(path.join(targetDir, file));
132
+ cleaned++;
133
+ }
134
+
135
+ for (const file of issues.duplicates) {
136
+ await fs.remove(path.join(targetDir, file));
137
+ cleaned++;
138
+ }
139
+
140
+ for (const dir of issues.emptyDirs) {
141
+ await fs.remove(path.join(targetDir, dir));
142
+ cleaned++;
46
143
  }
144
+
145
+ console.log(chalk.green(`\n✅ Limpieza completada: ${cleaned} elementos eliminados\n`));
47
146
  }
48
147
 
49
- function getAssetsToClean(dir, baseDir = dir) {
50
- const items = [];
51
- const entries = fs.readdirSync(dir, { withFileTypes: true });
148
+ function findDeadFiles(files) {
149
+ const dead = [];
150
+
151
+ for (const file of files) {
152
+ const lower = file.toLowerCase();
153
+ const basename = path.basename(file).toLowerCase();
154
+
155
+ if (DEAD_PATTERNS.some(p => lower.includes(p))) {
156
+ dead.push(file);
157
+ continue;
158
+ }
159
+
160
+ const ext = path.extname(file);
161
+ if (DUPLICATE_EXTENSIONS.includes(ext)) {
162
+ dead.push(file);
163
+ }
164
+ }
52
165
 
53
- for (const entry of entries) {
54
- const fullPath = path.join(dir, entry.name);
55
- const relativePath = path.relative(baseDir, fullPath);
166
+ return dead;
167
+ }
168
+
169
+ function findDuplicateFiles(files) {
170
+ const byName = {};
171
+ const duplicates = [];
172
+
173
+ for (const file of files) {
174
+ const name = path.basename(file).toLowerCase();
175
+ const ext = path.extname(file);
176
+ const baseName = name.replace(ext, '');
56
177
 
57
- if (entry.isDirectory()) {
58
- items.push(...getAssetsToClean(fullPath, baseDir));
178
+ const key = `${baseName}${ext}`;
179
+ if (!byName[key]) {
180
+ byName[key] = [];
181
+ }
182
+ byName[key].push(file);
183
+ }
184
+
185
+ for (const [key, paths] of Object.entries(byName)) {
186
+ if (paths.length > 1) {
187
+ const isCommonFile = ['index.js', 'index.jsx', 'index.ts', 'index.tsx'].includes(key.toLowerCase());
188
+ if (!isCommonFile) {
189
+ paths.slice(1).forEach(p => duplicates.push(p));
190
+ }
191
+ }
192
+ }
193
+
194
+ return duplicates;
195
+ }
196
+
197
+ async function findOrphanFiles(projectDir, allFiles) {
198
+ const orphans = [];
199
+ const sourceFiles = allFiles.filter(f => {
200
+ const ext = path.extname(f);
201
+ return ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'].includes(ext);
202
+ });
203
+
204
+ const referencedFiles = new Set();
205
+
206
+ for (const file of sourceFiles) {
207
+ const fullPath = path.join(projectDir, file);
208
+ if (!fs.existsSync(fullPath)) continue;
209
+
210
+ try {
211
+ const content = await fs.readFile(fullPath, 'utf-8');
212
+
213
+ const importRegex = /import\s+.*?from\s+['"](.+?)['"]/g;
214
+ let match;
215
+ while ((match = importRegex.exec(content)) !== null) {
216
+ const imported = match[1].replace(/^\.\/|\.\.\//g, '');
217
+ const importedPath = path.join(path.dirname(file), imported);
218
+
219
+ for (const f of allFiles) {
220
+ if (f.includes(imported) || f.includes(imported + '.js') || f.includes(imported + '.jsx')) {
221
+ referencedFiles.add(f);
222
+ }
223
+ }
224
+ }
225
+ } catch (e) {}
226
+ }
227
+
228
+ const components = sourceFiles.filter(f => {
229
+ const name = path.basename(f);
230
+ return /^[A-Z][a-zA-Z]*\.jsx?$/.test(name) ||
231
+ /^[A-Z][a-zA-Z]*\.tsx?$/.test(name) ||
232
+ /^[A-Z][a-zA-Z]*\.svelte$/.test(name) ||
233
+ /^[A-Z][a-zA-Z]*\.vue$/.test(name);
234
+ });
235
+
236
+ for (const component of components) {
237
+ if (!referencedFiles.has(component)) {
238
+ orphans.push(component);
239
+ }
240
+ }
241
+
242
+ return orphans;
243
+ }
244
+
245
+ function analyzeFileOrganization(files) {
246
+ const misplaced = [];
247
+ const structureRules = {
248
+ 'src/components': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
249
+ 'src/pages': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
250
+ 'src/utils': ['.js', '.ts'],
251
+ 'src/hooks': ['.js', '.ts'],
252
+ 'src/services': ['.js', '.ts'],
253
+ 'src/styles': ['.css', '.scss', '.less'],
254
+ 'src/assets': ['.png', '.jpg', '.svg', '.gif', '.webp']
255
+ };
256
+
257
+ const components = files.filter(f => {
258
+ const name = path.basename(f);
259
+ return /^[A-Z][a-zA-Z]*\.jsx?$/.test(name) ||
260
+ /^[A-Z][a-zA-Z]*\.tsx?$/.test(name);
261
+ });
262
+
263
+ for (const comp of components) {
264
+ const dir = path.dirname(comp);
265
+ const parentDir = path.basename(dir);
266
+
267
+ if (!dir.includes('src/components') && !dir.includes('component')) {
268
+ misplaced.push({
269
+ file: comp,
270
+ suggested: 'src/components'
271
+ });
272
+ }
273
+ }
274
+
275
+ return misplaced;
276
+ }
277
+
278
+ function findEmptyDirectories(dir, baseDir = dir) {
279
+ const empty = [];
280
+ const items = fs.readdirSync(dir, { withFileTypes: true });
281
+
282
+ for (const item of items) {
283
+ if (!item.isDirectory()) continue;
284
+ if (item.name === 'node_modules' || item.name === '.git') continue;
285
+
286
+ const fullPath = path.join(dir, item.name);
287
+ const subItems = fs.readdirSync(fullPath);
288
+
289
+ if (subItems.length === 0) {
290
+ empty.push(path.relative(baseDir, fullPath));
59
291
  } else {
60
- items.push(relativePath);
292
+ empty.push(...findEmptyDirectories(fullPath, baseDir));
293
+ }
294
+ }
295
+
296
+ return empty;
297
+ }
298
+
299
+ async function getAllFiles(dir, baseDir = dir) {
300
+ const files = [];
301
+ const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '.next', 'out', '.nuxt', 'coverage', '.cache', '.svelte-kit'];
302
+
303
+ if (!fs.existsSync(dir)) return files;
304
+
305
+ const items = fs.readdirSync(dir, { withFileTypes: true });
306
+
307
+ for (const item of items) {
308
+ if (ignoreDirs.includes(item.name)) continue;
309
+
310
+ const fullPath = path.join(dir, item.name);
311
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
312
+
313
+ if (item.isDirectory()) {
314
+ files.push(...await getAllFiles(fullPath, baseDir));
315
+ } else {
316
+ files.push(relativePath);
61
317
  }
62
318
  }
63
319
 
64
- return items;
320
+ return files;
65
321
  }
322
+
323
+ export default { clean };