@ingcrea/aumic-tailwind-killer 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # ⚔️ AUM-IC Tailwind Killer
2
+
3
+ **AUM-IC Tailwind Killer** es un orquestador CLI de alto rendimiento diseñado para **transmutar** proyectos basados en Tailwind CSS en ecosistemas de clases ofuscadas, deterministas y libres de dependencias (Zero-Bloat).
4
+
5
+ Desarrollado por el equipo de **Ingeniería Creativa (IngCrea)**, esta herramienta audita, extrae, compila (vía JIT) y purga tu código fuente en segundos gracias a su arquitectura multi-hilos (Piscina) e interceptores AST universales.
6
+
7
+ ---
8
+
9
+ ## ⚡ ¿Por qué usar AUM-IC Tailwind Killer?
10
+
11
+ 1. **Ofuscación y Seguridad Corporativa:** Transforma utilidades legibles (`flex items-center text-red-500`) en hashes seguros (`aumic-component-1b3a4f`), dificultando el scraping y el robo de diseño (UI/UX).
12
+ 2. **Independencia del Framework:** Erradica a Tailwind CSS de tu `package.json` y dependencias de build. Tu proyecto pasa a depender únicamente de un archivo `.css` nativo, estándar y ultra-optimizado.
13
+ 3. **Rendimiento Extremo (Build Time):** Al purgar el motor de Tailwind de tu flujo de trabajo, los tiempos de compilación de tu framework (Astro, Next.js, Vite) se reducen drásticamente.
14
+ 4. **Cero Riesgo de Corrupción:** Cuenta con escudos **Pre-Flight** que auditan los permisos NTFS/POSIX antes de tocar un solo archivo, evitando estados corruptos.
15
+
16
+ ---
17
+
18
+ ## 🚀 Instalación
19
+
20
+ Puedes ejecutarlo al vuelo usando `npx` (recomendado) o instalarlo globalmente:
21
+
22
+ ```bash
23
+ # Uso al vuelo mediante NPX
24
+ npx @ingcrea/aumic-tailwind-killer -m local -t ./mi-proyecto
25
+
26
+ # Instalación global
27
+ npm install -g @ingcrea/aumic-tailwind-killer
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 🛠 Casos de Uso y Ejemplos de Ejecución
33
+
34
+ La herramienta opera bajo distintos **modos** de destrucción y auditoría. Asegúrate de estar en una rama de Git limpia (ej. `git checkout -b build-aumic`) antes de realizar mutaciones destructivas.
35
+
36
+ ### 1. Modo Simulación (Dry-Run)
37
+ **Caso de uso:** Quieres auditar tu proyecto, ver cuántas clases de Tailwind usas y visualizar el impacto sin modificar tu código fuente. Ideal para auditorías previas.
38
+
39
+ ```bash
40
+ npx @ingcrea/aumic-tailwind-killer -m simulate -t ./
41
+ ```
42
+ * **Qué hace:** Escanea el código mediante el Motor Infrarrojo, extrae las utilidades, genera los hashes en RAM y emite un reporte visual detallado (`aumic-report.html`), pero **no altera** tus archivos.
43
+
44
+ ### 2. Modo Local (Transmutación Destructiva)
45
+ **Caso de uso:** Estás listo para preparar tu código para producción. Deseas reemplazar todo Tailwind por CSS puro, reescribir tus componentes (`.astro`, `.tsx`, `.vue`, `.py`) y eliminar el framework.
46
+
47
+ ```bash
48
+ npx @ingcrea/aumic-tailwind-killer -m local -t ./
49
+ ```
50
+ * **Qué hace:** Ejecuta las 5 Fases de AUM-IC. Reescribe físicamente todos los archivos soportados, inyecta el CSS nativo unificado, elimina dependencias en `package.json` y genera un mapa reverso (`aumic-lock.json`).
51
+
52
+ ### 3. Modo Quirúrgico (Surgical Scope)
53
+ **Caso de uso:** Tienes un monorepo gigante y solo quieres ofuscar una carpeta específica (ej. solo el frontend de marketing) ignorando el panel de administración.
54
+
55
+ ```bash
56
+ npx @ingcrea/aumic-tailwind-killer -m local -t ./ -s "src/frontend/**/*.{tsx,astro}"
57
+ ```
58
+ * **Qué hace:** Ignora el escáner global de Git y restringe la mutación **estrictamente** al patrón Glob (Regex) definido en el argumento `-s` (Scope).
59
+
60
+ ### 4. Modo Restauración (Rollback)
61
+ **Caso de uso:** Algo falló durante el QA post-mutación o necesitas volver a trabajar con Tailwind en tu entorno local.
62
+
63
+ ```bash
64
+ npx @ingcrea/aumic-tailwind-killer -m restore -t ./
65
+ ```
66
+ * **Qué hace:** Lee el mapa criptográfico `aumic-lock.json`, revierte los hashes en tus archivos a sus clases Tailwind originales y restaura el `package.json.aumic-bak`.
67
+
68
+ ---
69
+
70
+ ## ⚙️ Banderas y Variables (CLI Options)
71
+
72
+ | Bandera Corta | Bandera Larga | Descripción | Obligatorio |
73
+ | :--- | :--- | :--- | :--- |
74
+ | `-m` | `--mode` | Define el comportamiento del orquestador (`simulate`, `local`, `restore`, `clone`). | **Sí** |
75
+ | `-t` | `--target` | Ruta absoluta o relativa al directorio del proyecto a procesar. | **Sí** |
76
+ | `-s` | `--scope` | Patrón de búsqueda Glob para restringir la mutación a archivos específicos. | No |
77
+
78
+ ---
79
+
80
+ ## 🧠 Arquitectura de 5 Fases
81
+
82
+ 1. **Reconocimiento & Pre-Flight:** Validación exhaustiva de permisos W_OK (Anti-Zombie) y escaneo de árbol de dependencias mediante LS-Files de Git (Fast-Path).
83
+ 2. **Extracción Paralela (Babel + Regex):** Pool de workers en Node (Piscina) que analizan sintaxis de JSX, TSX y lenguajes agnósticos (Astro, Python, PHP) interceptando atributos de clase (`class`, `className`, `class:list`).
84
+ 3. **Criptografía de Nomenclatura:** Asignación de Hashes Deterministas (`aumic-component-[hash]`) optimizando la reusabilidad (deduplicación del 100%).
85
+ 4. **Motor JIT Embebido:** Generación virtual en memoria y renderizado de CSS de Tailwind usando compilación "Just In Time", fusionando todo en la base nativa.
86
+ 5. **Erradicación Total:** Purga de scripts, inyección del archivo CSS puro resultante y limpieza de configuraciones del framework original.
87
+
88
+ ---
89
+
90
+ ## 🛡 Consideraciones de Seguridad
91
+
92
+ * **Backup Automático:** Siempre que la herramienta interviene archivos de configuración (`package.json`, `tailwind.config.js`), genera respaldos con la extensión `.aumic-bak`.
93
+ * **Archivos Protegidos:** Por defecto, el motor ignora estrictamente agujeros negros como `node_modules`, `.git`, `dist`, y `.venv`.
94
+
95
+ ---
96
+
97
+ > Desarrollado bajo la doctrina tecnológica de Ingeniería Creativa. Excelencia, Determinismo y Zero-Trust.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const https = require('https');
7
+ const { spawnSync } = require('child_process');
8
+
9
+ // ==========================================
10
+ // AUM-IC NATIVE WRAPPER
11
+ // ==========================================
12
+ const VERSION = require('../package.json').version;
13
+ const GITHUB_REPO = 'ingcrea/aumic-tailwind-killer'; // <-- AJUSTA EL REPOSITORIO AQUÍ SI ES NECESARIO
14
+
15
+ const PLATFORM_MAP = {
16
+ win32: 'win',
17
+ linux: 'linux',
18
+ darwin: 'macos'
19
+ };
20
+
21
+ const ARCH_MAP = {
22
+ x64: 'x64',
23
+ arm64: 'arm64'
24
+ };
25
+
26
+ const osName = PLATFORM_MAP[os.platform()];
27
+ const osArch = ARCH_MAP[os.arch()];
28
+
29
+ if (!osName || !osArch) {
30
+ console.error(`❌ [AUM-IC] Arquitectura no soportada: ${os.platform()}-${os.arch()}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ const ext = osName === 'win' ? '.exe' : '';
35
+ const binName = `aumic-tailwind-killer-v${VERSION}-${osName}-${osArch}${ext}`;
36
+ const binDir = path.join(__dirname, '..', 'vendor');
37
+ const binPath = path.join(binDir, binName);
38
+
39
+ const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/${binName}`;
40
+
41
+ function downloadBinary(url, dest) {
42
+ return new Promise((resolve, reject) => {
43
+ https.get(url, (res) => {
44
+ if (res.statusCode === 301 || res.statusCode === 302) {
45
+ return downloadBinary(res.headers.location, dest).then(resolve).catch(reject);
46
+ }
47
+
48
+ if (res.statusCode !== 200) {
49
+ return reject(new Error(`Error HTTP ${res.statusCode} al descargar el binario nativo.`));
50
+ }
51
+
52
+ const file = fs.createWriteStream(dest);
53
+ res.pipe(file);
54
+ file.on('finish', () => {
55
+ file.close(resolve);
56
+ });
57
+ file.on('error', (err) => {
58
+ fs.unlink(dest, () => reject(err));
59
+ });
60
+ }).on('error', reject);
61
+ });
62
+ }
63
+
64
+ async function main() {
65
+ if (!fs.existsSync(binPath)) {
66
+ if (!fs.existsSync(binDir)) {
67
+ fs.mkdirSync(binDir, { recursive: true });
68
+ }
69
+
70
+ console.log(`\x1b[36m[AUM-IC]\x1b[0m Descargando motor nativo (v${VERSION}) para ${osName}-${osArch}...`);
71
+ console.log(`\x1b[90mDescargando desde: ${downloadUrl}\x1b[0m`);
72
+
73
+ try {
74
+ await downloadBinary(downloadUrl, binPath);
75
+ if (osName !== 'win') {
76
+ fs.chmodSync(binPath, 0o755); // Hacer ejecutable en Linux/Mac
77
+ }
78
+ console.log(`\x1b[32m✔ Motor nativo instalado con éxito.\x1b[0m\n`);
79
+ } catch (err) {
80
+ console.error(`\x1b[33m⚠️ No se pudo descargar el binario nativo: ${err.message}\x1b[0m`);
81
+ console.log(`\x1b[36m[AUM-IC]\x1b[0m Iniciando modo Fallback (Ejecutando motor de Node/TypeScript)...\n`);
82
+ require('../dist/index.js');
83
+ return;
84
+ }
85
+ }
86
+
87
+ // Ejecutar el binario y pasar todos los argumentos del CLI
88
+ const args = process.argv.slice(2);
89
+ const result = spawnSync(binPath, args, { stdio: 'inherit' });
90
+
91
+ if (result.error) {
92
+ console.error(`\x1b[33m⚠️ Falló la ejecución nativa. Iniciando modo Fallback...\x1b[0m\n`);
93
+ require('../dist/index.js');
94
+ return;
95
+ }
96
+
97
+ process.exit(result.status || 0);
98
+ }
99
+
100
+ main();
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AIEngine = void 0;
4
+ class AIEngine {
5
+ provider;
6
+ apiKey;
7
+ model;
8
+ constructor(provider, apiKey, model) {
9
+ this.provider = provider;
10
+ this.apiKey = apiKey;
11
+ this.model = model || this.getDefaultModel(provider);
12
+ }
13
+ getDefaultModel(provider) {
14
+ switch (provider) {
15
+ case 'openai': return 'gpt-4o-mini';
16
+ case 'deepseek': return 'deepseek-chat';
17
+ case 'xai': return 'grok-beta';
18
+ case 'alibaba': return 'qwen-turbo';
19
+ case 'claude': return 'claude-3-haiku-20240307';
20
+ case 'gemini': return 'gemini-1.5-flash';
21
+ case 'ollama': return 'llama3';
22
+ default: return 'gpt-4o-mini';
23
+ }
24
+ }
25
+ getProviderBaseUrl(provider) {
26
+ switch (provider) {
27
+ case 'openai': return 'https://api.openai.com/v1/chat/completions';
28
+ case 'deepseek': return 'https://api.deepseek.com/chat/completions';
29
+ case 'xai': return 'https://api.x.ai/v1/chat/completions';
30
+ case 'alibaba': return 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
31
+ case 'ollama': return 'http://127.0.0.1:11434/api/generate';
32
+ default: return '';
33
+ }
34
+ }
35
+ async generateSemanticNames(request) {
36
+ let systemPrompt = "Eres un Arquitecto CSS AUM-IC. Te daré un array JSON de cadenas de utilidades de Tailwind. Devuelve un objeto JSON donde la clave es la cadena exacta de Tailwind, y el valor es un nombre de clase semántico BEM corto que empiece con 'aumic-' (ej: 'aumic-btn--primary', 'aumic-card__header'). Responde ÚNICAMENTE con el objeto JSON válido, sin bloques de código markdown ni texto adicional.";
37
+ if (request.customRules) {
38
+ systemPrompt += `\n\nREGLAS PERSONALIZADAS DE LA EMPRESA/DESARROLLADOR:\n${request.customRules}\nDEBES DAR PRIORIDAD ABSOLUTA A ESTAS REGLAS EN EL RENOMBRAMIENTO.`;
39
+ }
40
+ const userContent = JSON.stringify(request.utilities);
41
+ let jsonRaw = '';
42
+ if (['openai', 'deepseek', 'xai', 'alibaba'].includes(this.provider)) {
43
+ jsonRaw = await this.callOpenAICompatible(systemPrompt, userContent);
44
+ }
45
+ else if (this.provider === 'gemini') {
46
+ jsonRaw = await this.callGemini(systemPrompt, userContent);
47
+ }
48
+ else if (this.provider === 'claude') {
49
+ jsonRaw = await this.callClaude(systemPrompt, userContent);
50
+ }
51
+ else if (this.provider === 'ollama') {
52
+ jsonRaw = await this.callOllama(systemPrompt, userContent);
53
+ }
54
+ else {
55
+ throw new Error(`Proveedor ${this.provider} no implementado.`);
56
+ }
57
+ try {
58
+ // Limpieza agresiva por si la IA devuelve markdown a pesar de las instrucciones
59
+ const cleanJson = jsonRaw.replace(/```json/g, '').replace(/```/g, '').trim();
60
+ const parsed = JSON.parse(cleanJson);
61
+ return { mapping: parsed };
62
+ }
63
+ catch (error) {
64
+ console.error("[IA] Fallo al parsear la respuesta semántica:", jsonRaw);
65
+ return { mapping: {} };
66
+ }
67
+ }
68
+ async callOpenAICompatible(system, user) {
69
+ const url = this.getProviderBaseUrl(this.provider);
70
+ const res = await fetch(url, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ 'Authorization': `Bearer ${this.apiKey}`
75
+ },
76
+ body: JSON.stringify({
77
+ model: this.model,
78
+ messages: [
79
+ { role: 'system', content: system },
80
+ { role: 'user', content: user }
81
+ ],
82
+ temperature: 0.1 // Temperatura baja para determinismo
83
+ })
84
+ });
85
+ if (!res.ok)
86
+ throw new Error(`[IA] Error HTTP ${res.status}: ${await res.text()}`);
87
+ const data = await res.json();
88
+ return data.choices[0].message.content;
89
+ }
90
+ async callGemini(system, user) {
91
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
92
+ const res = await fetch(url, {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify({
96
+ system_instruction: { parts: { text: system } },
97
+ contents: [{ parts: [{ text: user }] }],
98
+ generationConfig: { temperature: 0.1 }
99
+ })
100
+ });
101
+ if (!res.ok)
102
+ throw new Error(`[IA] Error HTTP ${res.status}: ${await res.text()}`);
103
+ const data = await res.json();
104
+ return data.candidates[0].content.parts[0].text;
105
+ }
106
+ async callClaude(system, user) {
107
+ const url = 'https://api.anthropic.com/v1/messages';
108
+ const res = await fetch(url, {
109
+ method: 'POST',
110
+ headers: {
111
+ 'Content-Type': 'application/json',
112
+ 'x-api-key': this.apiKey,
113
+ 'anthropic-version': '2023-06-01'
114
+ },
115
+ body: JSON.stringify({
116
+ model: this.model,
117
+ system: system,
118
+ messages: [{ role: 'user', content: user }],
119
+ temperature: 0.1,
120
+ max_tokens: 4096
121
+ })
122
+ });
123
+ if (!res.ok)
124
+ throw new Error(`[IA] Error HTTP ${res.status}: ${await res.text()}`);
125
+ const data = await res.json();
126
+ return data.content[0].text;
127
+ }
128
+ async callOllama(system, user) {
129
+ const url = this.getProviderBaseUrl(this.provider);
130
+ const res = await fetch(url, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({
134
+ model: this.model,
135
+ system: system,
136
+ prompt: user,
137
+ stream: false,
138
+ options: { temperature: 0.1 }
139
+ })
140
+ });
141
+ if (!res.ok)
142
+ throw new Error(`[IA] Error Ollama HTTP ${res.status}: Asegúrate de que Ollama esté corriendo en local.`);
143
+ const data = await res.json();
144
+ return data.response;
145
+ }
146
+ }
147
+ exports.AIEngine = AIEngine;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AstInterceptor = void 0;
7
+ const parser_1 = require("@babel/parser");
8
+ const traverse_1 = __importDefault(require("@babel/traverse"));
9
+ const generator_1 = __importDefault(require("@babel/generator"));
10
+ class AstInterceptor {
11
+ /**
12
+ * Destripa archivos TSX/JSX, localiza los atributos className/class
13
+ * e intercepta todos los StringLiterals dentro, sin importar su nivel
14
+ * de anidación o lógica condicional (ternarios, arrays, etc).
15
+ */
16
+ processFrameworkFile(code, fileName, replacerFn) {
17
+ const ext = fileName.split('.').pop()?.toLowerCase();
18
+ const jsExtensions = ['jsx', 'tsx', 'js', 'ts'];
19
+ if (jsExtensions.includes(ext || '')) {
20
+ return this.processWithBabel(code, replacerFn);
21
+ }
22
+ else {
23
+ return this.processWithUniversalRegex(code, replacerFn);
24
+ }
25
+ }
26
+ processWithUniversalRegex(code, replacerFn) {
27
+ const extracted = new Set();
28
+ // Regex letal que captura class="...", className='...', class:list={`...`} en cualquier motor de plantillas (Jinja, Blade, Tera, HTML)
29
+ const classRegex = /(class|className|class:list)\s*(=|:)\s*(["'`])(.*?)\3/gs;
30
+ let newCode = code;
31
+ // Fase 1: Extracción
32
+ let match;
33
+ while ((match = classRegex.exec(code)) !== null) {
34
+ const classString = match[4];
35
+ const cleanStr = classString.replace(/\s+/g, ' ').trim();
36
+ if (cleanStr && !cleanStr.includes('aumic-')) {
37
+ extracted.add(cleanStr);
38
+ }
39
+ }
40
+ // Fase 2: Mutación directa
41
+ newCode = code.replace(classRegex, (fullMatch, attr, separator, quote, classString) => {
42
+ const cleanStr = classString.replace(/\s+/g, ' ').trim();
43
+ if (!cleanStr || cleanStr.includes('aumic-'))
44
+ return fullMatch;
45
+ const newClass = replacerFn(cleanStr);
46
+ return `${attr}${separator}${quote}${newClass}${quote}`;
47
+ });
48
+ return { newCode, extractedClasses: Array.from(extracted) };
49
+ }
50
+ processWithBabel(code, replacerFn) {
51
+ const ast = (0, parser_1.parse)(code, {
52
+ sourceType: 'module',
53
+ plugins: ['jsx', 'typescript']
54
+ });
55
+ const extracted = new Set();
56
+ const traverseAst = traverse_1.default.default || traverse_1.default;
57
+ traverseAst(ast, {
58
+ CallExpression(path) {
59
+ const calleeName = path.node.callee.name;
60
+ if (['clsx', 'cva', 'cn', 'twMerge', 'classNames'].includes(calleeName)) {
61
+ path.traverse({
62
+ StringLiteral(strPath) {
63
+ const originalStr = strPath.node.value;
64
+ const cleanStr = originalStr.replace(/\s+/g, ' ').trim();
65
+ if (cleanStr && !cleanStr.includes('aumic-')) {
66
+ extracted.add(cleanStr);
67
+ strPath.node.value = replacerFn(cleanStr);
68
+ }
69
+ },
70
+ TemplateElement(tplPath) {
71
+ const originalRaw = tplPath.node.value.raw;
72
+ const cleanStr = originalRaw.replace(/\s+/g, ' ').trim();
73
+ if (cleanStr && !cleanStr.includes('aumic-')) {
74
+ extracted.add(cleanStr);
75
+ tplPath.node.value.raw = replacerFn(cleanStr);
76
+ tplPath.node.value.cooked = tplPath.node.value.raw;
77
+ }
78
+ }
79
+ });
80
+ }
81
+ },
82
+ JSXAttribute(path) {
83
+ const attrName = path.node.name.name;
84
+ if (attrName === 'className' || attrName === 'class') {
85
+ path.traverse({
86
+ StringLiteral(strPath) {
87
+ const originalStr = strPath.node.value;
88
+ const cleanStr = originalStr.replace(/\s+/g, ' ').trim();
89
+ if (cleanStr && !cleanStr.includes('aumic-')) {
90
+ extracted.add(cleanStr);
91
+ strPath.node.value = replacerFn(cleanStr);
92
+ }
93
+ },
94
+ TemplateElement(tplPath) {
95
+ const originalRaw = tplPath.node.value.raw;
96
+ const cleanStr = originalRaw.replace(/\s+/g, ' ').trim();
97
+ if (cleanStr && !cleanStr.includes('aumic-')) {
98
+ extracted.add(cleanStr);
99
+ tplPath.node.value.raw = replacerFn(cleanStr);
100
+ tplPath.node.value.cooked = tplPath.node.value.raw;
101
+ }
102
+ }
103
+ });
104
+ }
105
+ }
106
+ });
107
+ const generateCode = generator_1.default.default || generator_1.default;
108
+ const output = generateCode(ast, { retainLines: true }, code);
109
+ return {
110
+ newCode: output.code,
111
+ extractedClasses: Array.from(extracted)
112
+ };
113
+ }
114
+ }
115
+ exports.AstInterceptor = AstInterceptor;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WebCloner = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ // @ts-ignore
10
+ const website_scraper_1 = __importDefault(require("website-scraper"));
11
+ const picocolors_1 = __importDefault(require("picocolors"));
12
+ class WebCloner {
13
+ /**
14
+ * Clona una página web completa (HTML, CSS, JS, Fuentes, Imágenes),
15
+ * reescribe todas las rutas para que funcionen 100% en local.
16
+ */
17
+ static async cloneWebsite(url, outputDir, cloneDepth) {
18
+ // Limpiar el directorio si ya existe para evitar conflictos de clonación
19
+ if (await fs_extra_1.default.pathExists(outputDir)) {
20
+ await fs_extra_1.default.emptyDir(outputDir);
21
+ }
22
+ const options = {
23
+ urls: [url],
24
+ directory: outputDir,
25
+ sources: [
26
+ { selector: 'img', attr: 'src' },
27
+ { selector: 'link[rel="stylesheet"]', attr: 'href' },
28
+ { selector: 'script', attr: 'src' },
29
+ { selector: 'source', attr: 'src' },
30
+ { selector: 'source', attr: 'srcset' },
31
+ { selector: 'img', attr: 'srcset' }
32
+ ],
33
+ // Si es 'site', escanea y descarga recursivamente toda la web. Si es 'page', solo la URL dada y sus assets.
34
+ recursive: cloneDepth === 'site',
35
+ maxDepth: cloneDepth === 'site' ? 5 : 1, // Límite táctico para no desbordar el disco
36
+ ignoreErrors: true
37
+ };
38
+ try {
39
+ await (0, website_scraper_1.default)(options);
40
+ }
41
+ catch (error) {
42
+ console.error(picocolors_1.default.red(`\n[!] Error fatal durante la clonación cibernética: ${error}`));
43
+ throw error;
44
+ }
45
+ }
46
+ /**
47
+ * Detecta si la web clonada utiliza Tailwind CSS escaneando sus archivos HTML.
48
+ */
49
+ static async isTailwindPowered(outputDir) {
50
+ const files = await fs_extra_1.default.readdir(outputDir);
51
+ const htmlFiles = files.filter(f => f.endsWith('.html'));
52
+ let twScore = 0;
53
+ const twSignatures = ['flex', 'grid', 'w-full', 'h-full', 'absolute', 'relative', 'bg-', 'text-', 'p-4', 'm-'];
54
+ for (const file of htmlFiles) {
55
+ const content = await fs_extra_1.default.readFile(path_1.default.join(outputDir, file), 'utf-8');
56
+ for (const sig of twSignatures) {
57
+ if (content.includes(`"${sig}`))
58
+ twScore++;
59
+ if (content.includes(` ${sig}`))
60
+ twScore++;
61
+ }
62
+ }
63
+ // Si encontramos múltiples firmas atómicas, confirmamos que es Tailwind
64
+ return twScore >= 5;
65
+ }
66
+ }
67
+ exports.WebCloner = WebCloner;