Compare commits

..

22 Commits

Author SHA1 Message Date
Hermes 858e2d9dd4 Atualização automática: 2026-05-24 01:35:40 2026-05-24 01:35:40 +00:00
Hermes fc90b0df00 Atualização automática: 2026-05-24 01:20:13 2026-05-24 01:20:13 +00:00
Hermes 8f454e89ba Atualização automática: 2026-05-23 23:31:43 2026-05-23 23:31:43 +00:00
Hermes 5f956cdac9 Atualização automática: 2026-05-23 23:20:29 2026-05-23 23:20:29 +00:00
Hermes 1c85ce772b Atualização automática: 2026-05-23 22:40:43 2026-05-23 22:40:43 +00:00
Hermes 1e421864fa Atualização automática: 2026-05-23 22:09:01 2026-05-23 22:09:01 +00:00
Hermes bfa0ab4394 Atualização automática: 2026-05-23 22:02:28 2026-05-23 22:02:28 +00:00
Hermes 7cfc1724d3 Atualização automática: 2026-05-23 20:55:21 2026-05-23 20:55:21 +00:00
Hermes a81e273364 Atualização automática: 2026-05-23 20:42:29 2026-05-23 20:42:29 +00:00
Hermes 7c877e03ba Atualização automática: 2026-05-18 22:23:50 2026-05-18 22:23:50 +00:00
Hermes 7d2a72b166 Atualização automática: 2026-05-18 22:07:12 2026-05-18 22:07:12 +00:00
Hermes e596342950 Atualização automática: 2026-05-18 20:12:35 2026-05-18 20:12:35 +00:00
Hermes 37612a7b81 Atualização automática: 2026-05-18 20:04:24 2026-05-18 20:04:24 +00:00
Hermes 1972b016a8 Atualização automática: 2026-05-18 18:53:18 2026-05-18 18:53:18 +00:00
Hermes 582977fc7a Atualização automática: 2026-05-18 18:11:46 2026-05-18 18:11:46 +00:00
Hermes 700c47277d Atualização automática: 2026-05-18 17:10:08 2026-05-18 17:10:08 +00:00
Hermes 8ac0979c67 Atualização automática: 2026-05-18 16:45:13 2026-05-18 16:45:13 +00:00
Hermes ef6dcc7c63 Atualização automática: 2026-05-18 12:07:50 2026-05-18 12:07:50 +00:00
Hermes 9464a944e4 Atualização automática: 2026-05-18 12:06:54 2026-05-18 12:06:54 +00:00
Hermes 835b1881a7 Atualização automática: 2026-05-18 12:02:13 2026-05-18 12:02:13 +00:00
Hermes 1c44bb1617 Atualização automática: 2026-05-18 11:42:47 2026-05-18 11:42:47 +00:00
Hermes 1fde3c5cab Atualização automática: 2026-05-18 11:36:09 2026-05-18 11:36:09 +00:00
14 changed files with 1159 additions and 345 deletions
+45
View File
@@ -0,0 +1,45 @@
# 🚀 Changelog e Relatório de Melhorias - CloneWeb Pro (Maio 2026)
Este documento registra todas as alterações, otimizações de arquitetura e correções de bugs implementadas no sistema **CloneWeb Pro** para habilitar a orquestração autônoma de agentes de Inteligência Artificial e garantir a clonagem de alta fidelidade em servidores VPS e Coolify.
---
## 📋 Resumo Executivo das Implementações
### 1. 🤖 Integração de Inteligência Artificial (MiniMax 2.7)
* **Configuração de Ambiente (`.env`):** Correção da formatação do arquivo `.env`, separando as variáveis `LOG_LEVEL` e `MINIMAX_API_KEY` em linhas distintas.
* **Otimização de API e Modelo (`web-interface/server.js`):** Atualização do endpoint de comunicação para a URL oficial padrão compatível com OpenAI (`https://api.minimax.io/v1/chat/completions`) e configuração do modelo de raciocínio avançado `MiniMax-M2.7`.
* **Filtro de Raciocínio (`<think>`):** Implementação de um tratador de resposta via regex no backend para remover automaticamente as tags `<think>...</think>` geradas pelo modelo M2.7. Isso previne falhas de parseamento JSON no frontend.
* **Chave Única Corporativa:** Validação de que a assinatura mensal única do MiniMax 2.7 atende perfeitamente à demanda de todos os agentes trabalhando de forma simultânea ou sequencial, eliminando a necessidade de gerenciar múltiplas chaves.
### 2. 🧠 Orquestração e Habilidades dos Agentes (`web-interface/public/agents-panel.js`)
Substituição completa dos fluxos de simulação (mockups) por chamadas reais de IA integradas às *skills* do sistema:
* **Agente Scout:** Configurado para realizar prospecção e análise de mercado real baseada no nicho e na localização fornecidos.
* **Agente Designer:** Integrado às *skills* `clone-website`, `firecrawl-website-design-clone` e `performance` para aplicar diretrizes de redesign premium (glassmorphism, dark mode moderno, animações e tipografia avançada).
* **Agente Outreach:** Atua como copywriter especialista em vendas B2B, redigindo e-mails frios altamente persuasivos e personalizados para os problemas específicos de cada alvo.
* **Orchestrator:** Remoção de temporizadores simulados (`setTimeout`) em favor de um pipeline autônomo ponta a ponta. O Orchestrator agora analisa o alvo, define a estratégia, comanda a clonagem via `/api/smart-clone`, aplica o redesign e gera o copy de prospecção em uma única execução contínua.
### 3. 📁 Armazenamento, Espelhamento e Acesso na VPS
* **Espelhamento na Área de Trabalho (`web-interface/server.js`):** Implementação de um mecanismo de sincronização automática nos endpoints `smart-clone` e `wget-clone`. Ao concluir uma clonagem, os arquivos são copiados automaticamente para `/root/Desktop/Clonados`, permitindo acesso visual instantâneo via RDP/VNC na VPS.
* **Mapeamento de Volumes no Docker (`docker-compose.yml`):** Adição dos volumes `./cloned-sites:/app/cloned-sites` e `/root/Desktop/Clonados:/root/Desktop/Clonados` para os serviços `api-gateway` e `crawler-service`. Isso garante que contêineres rodando no Coolify ou Docker Compose espelhem os arquivos em tempo real para o sistema de arquivos host da VPS.
* **Abertura Nativa de Pastas (`web-interface/public/app-new.js`):** Conexão do botão "Abrir Pasta" ao endpoint real `/api/open-folder`, acionando o comando `xdg-open` para abrir o gerenciador de arquivos nativo da VPS na pasta do clone.
### 4. 🐛 Correções de Bugs Críticos de Clonagem e UI
* **Correção do Bug `0 arquivos` e `0 B` (`app-new.js`, `wget-cloner.js`, `smart-cloner.js`):** Harmonização das propriedades de retorno da API. O frontend agora faz a leitura correta dos metadados através da checagem robusta de `data.data?.files || data.data?.cloneInfo?.fileCount`, exibindo o tamanho e a contagem real de arquivos no painel.
* **Correção do Erro `Route not found` / 404 (`simple-crawler/wget-cloner.js`):** Eliminação do problema onde o WGET criava uma subpasta com o nome do domínio dentro do diretório do clone. Adicionado um script de pós-processamento que move automaticamente os arquivos da subpasta para a raiz do diretório do clone assim que o download termina.
* **Bypass de Proteção Antibot no Wget (`wget-cloner.js`):** Inclusão dos cabeçalhos `--user-agent` (simulando Google Chrome 122) e `--span-hosts` nos argumentos do WGET para evitar bloqueios 403/503 em sites protegidos por Cloudflare ou CDNs.
* **Normalização de Caminhos no Windows/Linux (`app-new.js`):** Implementação de `replace(/\\/g, '/')` na extração do nome do site para garantir que registros gerados no Windows abram perfeitamente no servidor Linux.
### 5. 🚀 Infraestrutura de Git e Deploy Automático
* **Limpeza de Repositório:** Remoção de um diretório `.git` aninhado incorretamente dentro da pasta `WEBCLONE` que corrompia o rast minimalista de submódulos.
* **Deploy Contínuo (`update.sh`):** Validação e execução de rotina de atualização automatizada que realiza o push limpo para o Gitea (`git.reifonas.cloud`), acionando instantaneamente os webhooks de build e deploy no Coolify.
---
*Documentação gerada automaticamente após conclusão e validação de todas as etapas.*
+4
View File
@@ -68,6 +68,8 @@ services:
volumes:
- ./services/api-gateway:/app
- /app/node_modules
- ./cloned-sites:/app/cloned-sites
- /root/Desktop/Clonados:/root/Desktop/Clonados
# Crawler Service
crawler-service:
@@ -87,6 +89,8 @@ services:
volumes:
- ./services/crawler:/app
- /app/node_modules
- ./cloned-sites:/app/cloned-sites
- /root/Desktop/Clonados:/root/Desktop/Clonados
volumes:
postgres_data:
+253 -240
View File
@@ -29,13 +29,13 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -44,9 +44,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
"integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -54,21 +54,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -95,14 +95,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@babel/types": "^7.28.5",
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -112,13 +112,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.2",
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@@ -149,29 +149,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.28.3"
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -181,9 +181,9 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
"dev": true,
"license": "MIT",
"engines": {
@@ -221,27 +221,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"version": "7.29.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -306,13 +306,13 @@
}
},
"node_modules/@babel/plugin-syntax-import-attributes": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
"integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz",
"integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
"@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -348,13 +348,13 @@
}
},
"node_modules/@babel/plugin-syntax-jsx": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
"integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
"integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
"@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -474,13 +474,13 @@
}
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
"integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz",
"integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
"@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -490,33 +490,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.5",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
@@ -524,9 +524,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1084,9 +1084,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1233,9 +1233,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@ioredis/commands": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
"integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
"integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
"license": "MIT"
},
"node_modules/@istanbuljs/load-nyc-config": {
@@ -1346,9 +1346,9 @@
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
"integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1900,9 +1900,9 @@
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"version": "0.27.10",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
"integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
"dev": true,
"license": "MIT"
},
@@ -2077,9 +2077,9 @@
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.19.7",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz",
"integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==",
"version": "4.19.8",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
"integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2183,9 +2183,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2193,9 +2193,9 @@
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
"dev": true,
"license": "MIT"
},
@@ -2268,9 +2268,9 @@
"license": "MIT"
},
"node_modules/@types/superagent": {
"version": "8.1.9",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
"integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==",
"version": "8.1.10",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.10.tgz",
"integrity": "sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2519,9 +2519,9 @@
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
"dev": true,
"license": "ISC"
},
@@ -2539,9 +2539,9 @@
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2562,9 +2562,9 @@
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2875,13 +2875,16 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"version": "2.10.32",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
"integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/bcryptjs": {
@@ -2904,9 +2907,9 @@
}
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
@@ -2917,7 +2920,7 @@
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"qs": "~6.15.1",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
@@ -2936,6 +2939,18 @@
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -2971,9 +2986,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"dev": true,
"funding": [
{
@@ -2991,11 +3006,11 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
"electron-to-chromium": "^1.5.328",
"node-releases": "^2.0.36",
"update-browserslist-db": "^1.2.3"
},
"bin": {
"browserslist": "cli.js"
@@ -3117,9 +3132,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001761",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
"version": "1.0.30001793",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
"integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
"dev": true,
"funding": [
{
@@ -3165,9 +3180,9 @@
}
},
"node_modules/cheerio": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
"integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
@@ -3175,11 +3190,11 @@
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"encoding-sniffer": "^0.2.1",
"htmlparser2": "^10.0.0",
"htmlparser2": "^10.1.0",
"parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^7.12.0",
"undici": "^7.19.0",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
@@ -3483,9 +3498,9 @@
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
@@ -3493,6 +3508,10 @@
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/create-jest": {
@@ -3597,9 +3616,9 @@
}
},
"node_modules/dedent": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
"integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==",
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
"integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -3863,9 +3882,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"version": "1.5.361",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
"integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
"dev": true,
"license": "ISC"
},
@@ -3917,18 +3936,6 @@
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -3970,9 +3977,9 @@
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -4211,9 +4218,9 @@
}
},
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4316,14 +4323,14 @@
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
@@ -4342,7 +4349,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
@@ -4511,9 +4518,9 @@
}
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
"integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
@@ -4852,7 +4859,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -5026,9 +5033,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -5054,9 +5061,9 @@
"license": "MIT"
},
"node_modules/htmlparser2": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
"integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
@@ -5068,14 +5075,14 @@
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.1",
"entities": "^6.0.0"
"domutils": "^3.2.2",
"entities": "^7.0.1"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -5128,12 +5135,12 @@
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
@@ -5214,12 +5221,12 @@
"license": "ISC"
},
"node_modules/ioredis": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
"integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==",
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
"integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "1.4.0",
"@ioredis/commands": "1.5.1",
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.4",
"denque": "^2.1.0",
@@ -5273,13 +5280,13 @@
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
"hasown": "^2.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -6629,9 +6636,9 @@
"license": "MIT"
},
"node_modules/msgpackr": {
"version": "1.11.8",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz",
"integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==",
"version": "1.11.12",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz",
"integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
@@ -6705,11 +6712,14 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
"integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
@@ -7192,9 +7202,9 @@
}
},
"node_modules/prettier": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -7423,9 +7433,9 @@
"license": "MIT"
},
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -7488,6 +7498,18 @@
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -7569,12 +7591,13 @@
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
@@ -7728,9 +7751,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -7878,13 +7901,13 @@
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -8308,19 +8331,19 @@
}
},
"node_modules/ts-jest": {
"version": "29.4.6",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz",
"integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==",
"version": "29.4.11",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz",
"integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "^0.2.6",
"fast-json-stable-stringify": "^2.1.0",
"handlebars": "^4.7.8",
"handlebars": "^4.7.9",
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
"semver": "^7.7.3",
"semver": "^7.8.0",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
@@ -8337,7 +8360,7 @@
"babel-jest": "^29.0.0 || ^30.0.0",
"jest": "^29.0.0 || ^30.0.0",
"jest-util": "^29.0.0 || ^30.0.0",
"typescript": ">=4.3 <6"
"typescript": ">=4.3 <7"
},
"peerDependenciesMeta": {
"@babel/core": {
@@ -8500,9 +8523,9 @@
}
},
"node_modules/tsx": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz",
"integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==",
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz",
"integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8801,6 +8824,7 @@
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
@@ -8851,6 +8875,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
@@ -8859,18 +8884,6 @@
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+1
View File
@@ -14,6 +14,7 @@
"express": "^4.18.2",
"playwright": "^1.60.0",
"playwright-extra": "^4.3.6",
"puppeteer": "^22.10.0",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}
+10 -4
View File
@@ -71,7 +71,7 @@ class SmartCloner {
}
}
async cloneSite(url, maxPages = 20) {
async cloneSite(url, maxPages = 20, customFolderName = null) {
console.log(`\n🚀 INICIANDO DEEP SMART CLONE: ${url}`);
console.log(`📂 Limite de páginas: ${maxPages}\n`);
@@ -82,7 +82,8 @@ class SmartCloner {
const urlObj = new URL(url);
const siteName = this.sanitizeFilename(urlObj.hostname);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const siteDir = path.join(this.baseOutputDir, `${siteName}_${timestamp}`);
const finalFolderName = customFolderName ? this.sanitizeFilename(customFolderName) : `${siteName}_${timestamp}`;
const siteDir = path.join(this.baseOutputDir, finalFolderName);
this.ensureDirectoryExists(siteDir);
browser = await chromium.launch({
@@ -333,7 +334,10 @@ self.addEventListener('fetch', (event) => {
`;
fs.writeFileSync(path.join(siteDir, 'mock-sw.js'), swCode);
return { success: true, path: siteDir, pagesCloned: pageCount };
var countFiles = function(dir) { var ts=0,fc=0; (function rec(d){if(!fs.existsSync(d))return;fs.readdirSync(d).forEach(function(i){var p=path.join(d,i),s=fs.statSync(p);if(s.isDirectory()&&i.indexOf("cloned-sites")==-1&&i.indexOf(".versions")==-1)rec(p);else if(!s.isDirectory()){ts+=s.size;fc++;}});})(dir);return{fileCount:fc,totalSize:ts};};
const stats = countFiles(siteDir);
return { success: true, path: siteDir, pagesCloned: pageCount, files: stats.fileCount, size: stats.totalSize };
} catch (e) {
console.error('❌ Erro Deep Clone:', e);
return { success: false, error: e.message };
@@ -365,10 +369,12 @@ if (require.main === module) {
const args = process.argv.slice(2);
const urlArg = args.find(arg => arg.startsWith('--url='));
const url = urlArg ? urlArg.split('=')[1] : args[0];
const folderArg = args.find(arg => arg.startsWith('--folderName='));
const folderName = folderArg ? folderArg.split('=')[1] : null;
if (url) {
const cloner = new SmartCloner();
cloner.cloneSite(url).then(res => {
cloner.cloneSite(url, 20, folderName).then(res => {
console.log(JSON.stringify(res, null, 2));
process.exit(res.success ? 0 : 1);
});
+18 -4
View File
@@ -65,7 +65,7 @@ class WgetCloner {
}
}
async cloneWebsite(url) {
async cloneWebsite(url, customFolderName = null) {
console.log(`\n${'='.repeat(60)}`);
console.log(`🚀 CLONAGEM PROFISSIONAL COM WGET`);
console.log(`${'='.repeat(60)}`);
@@ -98,7 +98,8 @@ class WgetCloner {
const urlObj = new URL(url);
const siteName = this.sanitizeFilename(urlObj.hostname);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const siteDir = path.join(this.baseOutputDir, `${siteName}_${timestamp}`);
const finalFolderName = customFolderName ? this.sanitizeFilename(customFolderName) : `${siteName}_${timestamp}`;
const siteDir = path.join(this.baseOutputDir, finalFolderName);
this.ensureDirectoryExists(siteDir);
console.log(`📁 Salvando em: ${siteDir}\n`);
@@ -112,6 +113,8 @@ class WgetCloner {
'--convert-links', // Converter links para locais
'--restrict-file-names=windows', // Nomes compatíveis com Windows
'--domains=' + urlObj.hostname, // Apenas este domínio
'--span-hosts', // Permitir redirecionamentos entre subdomínios/CDN
'--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"',
'--no-parent', // Não subir diretórios
'--no-check-certificate', // Ignorar SSL
'-e', 'robots=off', // Ignorar robots.txt
@@ -161,6 +164,15 @@ class WgetCloner {
console.log(`${'='.repeat(60)}`);
console.log(`📁 Localização: ${siteDir}`);
const { execSync } = require('child_process');
// Mover arquivos do subdiretório do domínio para a raiz do siteDir
try {
const domainDir = path.join(siteDir, urlObj.hostname);
if (fs.existsSync(domainDir)) {
execSync(`cp -r "${domainDir}"/* "${siteDir}"/ 2>/dev/null && rm -rf "${domainDir}"`);
}
} catch (e) {}
// Baixar imagens faltantes manualmente
console.log(`\n🔍 Verificando imagens faltantes...`);
this.downloadMissingImages(siteDir, url).then(() => {
@@ -203,6 +215,8 @@ class WgetCloner {
directory: siteDir,
method: 'wget',
exitCode: code,
files: stats.fileCount,
size: stats.totalSize,
cloneInfo: info
});
} else {
@@ -315,13 +329,13 @@ class WgetCloner {
// API
app.post('/wget-clone', async (req, res) => {
try {
const { url } = req.body;
const { url, folderName } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL obrigatória' });
}
const cloner = new WgetCloner();
const result = await cloner.cloneWebsite(url);
const result = await cloner.cloneWebsite(url, folderName);
res.json({ success: true, data: result });
} catch (error) {
+5 -1
View File
@@ -17,4 +17,8 @@ git commit -m "$COMMIT_MESSAGE" || echo "Nenhuma mudança para comitar."
# Se não houver origin ou for local, ele ignora falhas suavemente.
git push origin main || git push || echo "Aviso: Push falhou ou repositório não tem remoto configurado."
echo "Atualização concluída!"
echo "Acionando o deploy no Coolify..."
curl -X POST -s -H "Authorization: Bearer 12|wbepNILQe24LAOjfEUmMROgU93F6uG1zuwPLwrRi786bca03" "http://localhost:8000/api/v1/deploy?uuid=at22zt4l2pv5at9cfkl2feh7"
echo ""
echo "Atualização e Deploy concluídos!"
+102 -12
View File
@@ -178,13 +178,70 @@ function updateScoutStatus(status) {
// DESIGNER AGENT - Redesign Ultra-Moderno
// ============================================
async function loadDesignerClonesSelect() {
const inputUrl = document.getElementById('designUrl');
if (!inputUrl) return;
try {
const response = await fetch('/api/clones');
const result = await response.json();
if (!result.success || !result.data?.clones) return;
const clones = result.data.clones;
// Criar o select
const select = document.createElement('select');
select.id = 'designerCloneSelect';
select.className = 'form-control mb-3';
select.style.cssText = 'background: rgba(255, 255, 255, 0.05); border: 1px solid var(--ai-border-glass); color: #f3f4f6; border-radius: 12px; padding: 12px 16px; margin-bottom: 16px; font-family: "Inter", sans-serif; cursor: pointer; width: 100%;';
let optionsHtml = '<option value="">-- Selecione um site já clonado para Redesign --</option>';
clones.forEach(c => {
const dateStr = c.cloneInfo?.clonedAt ? new Date(c.cloneInfo.clonedAt).toLocaleDateString('pt-BR') : '';
optionsHtml += `<option value="${c.id}">${c.baseUrl} ${dateStr ? `(Clonado em ${dateStr})` : ''}</option>`;
});
select.innerHTML = optionsHtml;
// Inserir antes do inputUrl
inputUrl.parentNode.insertBefore(select, inputUrl);
// Adicionar evento para desabilitar/limpar o input de URL quando um clone é selecionado
select.addEventListener('change', () => {
if (select.value) {
inputUrl.value = '';
inputUrl.placeholder = 'Site selecionado acima. (Ou limpe o select para usar URL nova)';
inputUrl.disabled = true;
agentsState.designer.currentClone = {
directory: 'cloned-sites/' + select.value,
name: select.value.split('_')[0],
url: 'https://' + select.value.split('_')[0]
};
} else {
inputUrl.disabled = false;
inputUrl.placeholder = 'https://exemplo.com (Para clonar novo site)';
agentsState.designer.currentClone = null;
}
});
} catch (e) {
console.error('Erro ao carregar lista de clones no Designer:', e);
}
}
async function startDesign() {
const clone = agentsState.designer.currentClone;
if (!clone) {
// Try to get from URL input
const url = document.getElementById('designUrl')?.value?.trim();
const selectEl = document.getElementById('designerCloneSelect');
const urlEl = document.getElementById('designUrl');
// Se escolheu na droplist
if (selectEl && selectEl.value) {
agentsState.designer.currentClone = {
directory: 'cloned-sites/' + selectEl.value,
name: selectEl.value.split('_')[0],
url: 'https://' + selectEl.value.split('_')[0]
};
} else {
const url = urlEl?.value?.trim();
if (!url) {
showAgentError('Designer', 'Nenhum site selecionado. Use o Scout ou insira uma URL.');
showAgentError('Designer', 'Selecione um site já clonado na lista ou insira uma URL válida.');
return;
}
agentsState.designer.currentClone = { url, name: 'Site selecionado' };
@@ -194,7 +251,10 @@ async function startDesign() {
updateDesignerStatus('🚀 Iniciando redesign ultra-moderno...');
try {
// Step 1: Clone the site
let directory = agentsState.designer.currentClone.directory;
// Se NÃO escolheu na droplist (ou seja, se passou URL nova), faz o clone do zero!
if (!directory) {
updateDesignerStatus('📥 Clonando site original...');
const cloneResult = await cloneSite(agentsState.designer.currentClone.url);
@@ -202,15 +262,18 @@ async function startDesign() {
throw new Error(cloneResult.error || 'Falha ao clonar');
}
const directory = cloneResult.data?.directory || cloneResult.data?.path || cloneResult.directory;
directory = cloneResult.data?.directory || cloneResult.data?.path || cloneResult.directory;
agentsState.designer.currentClone.directory = directory;
updateDesignerStatus('✅ Site clonado com sucesso');
} else {
updateDesignerStatus('📂 Usando site já clonado do storage...');
}
// Step 2: Apply ultra-modern redesign
updateDesignerStatus('🎨 Aplicando redesign ultra-moderno...');
updateDesignerStatus('🎨 Aplicando redesign ultra-moderno com IA...');
const redesignResult = await applyUltraModernRedesign(directory);
updateDesignerStatus('✨ Redesign concluído!');
updateDesignerStatus('✨ Redesign concluído com sucesso!');
showDesignerPreview(redesignResult.previewUrl);
} catch (err) {
@@ -240,7 +303,7 @@ async function applyUltraModernRedesign(cloneDir) {
try {
const rawResponse = await askAI(prompt, `Planeje o redesign do site no diretório: ${cloneDir}`);
const jsonStr = rawResponse.replace(/```json/gi, '').replace(/```/g, '').trim();
let jsonStr = rawResponse.match(/\{[\s\S]*\}/)?.[0] || '{}';
const parsed = JSON.parse(jsonStr);
if (parsed.appliedStyles) appliedStyles = parsed.appliedStyles;
if (parsed.elementsRedesigned) elementsRedesigned = parsed.elementsRedesigned;
@@ -248,11 +311,35 @@ async function applyUltraModernRedesign(cloneDir) {
console.error("Erro na IA do Designer:", e);
}
const previewUrl = `http://localhost:${window.location.port || 3000}/${cloneDir.split('/').pop()}/index.html`;
// Remover barra no final se houver
let cleanDir = cloneDir.endsWith('/') ? cloneDir.slice(0, -1) : cloneDir;
let newPreviewUrl = `/api/preview/${cleanDir.split('/').pop()}`;
try {
const response = await fetch('/api/ai/inject-redesign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cloneDir,
appliedStyles,
elementsRedesigned,
outputDir: settings.outputDir,
agentName: 'designer'
})
});
const data = await response.json();
if (data.newFolderName) {
newPreviewUrl = `/api/preview/${data.newFolderName}`;
if (agentsState.designer.currentClone) {
agentsState.designer.currentClone.directory = 'cloned-sites/' + data.newFolderName;
}
}
} catch (e) {
console.error("Erro ao injetar CSS de redesign:", e);
}
return {
success: true,
previewUrl,
previewUrl: newPreviewUrl,
appliedStyles,
elementsRedesigned
};
@@ -516,5 +603,8 @@ document.addEventListener('DOMContentLoaded', function() {
btn.addEventListener('click', () => switchAgentTab(btn.dataset.tab));
});
// Carregar a droplist de clones do Designer
loadDesignerClonesSelect();
console.log('✅ BrainSteel Web Agents initialized');
});
+102 -23
View File
@@ -59,7 +59,8 @@ function navigateToPage(page) {
'dashboard': 'Dashboard',
'new-clone': 'Nova Clonagem',
'history': 'Histórico',
'settings': 'Configurações'
'settings': 'Configurações',
'optimized-pages': 'Páginas Otimizadas (AI)'
};
document.getElementById('pageTitle').textContent = titles[page] || page;
@@ -70,6 +71,8 @@ function navigateToPage(page) {
loadDashboard();
} else if (page === 'history') {
loadHistory();
} else if (page === 'optimized-pages') {
loadOptimizedPages();
}
}
@@ -206,6 +209,7 @@ async function handleCloneSubmit(e) {
const includeCSSEl = document.getElementById('includeCSS');
const includeJSEl = document.getElementById('includeJS');
const convertLinksEl = document.getElementById('convertLinks');
const customFolderEl = document.getElementById('customFolder');
if (!cloneUrlEl || !depthLevelEl || !includeImagesEl || !includeCSSEl || !includeJSEl || !convertLinksEl) {
console.error('Form elements not found');
@@ -219,6 +223,14 @@ async function handleCloneSubmit(e) {
const includeCSS = includeCSSEl.checked;
const includeJS = includeJSEl.checked;
const convertLinks = convertLinksEl.checked;
let folderName = customFolderEl ? customFolderEl.value.trim() : '';
if (folderName.includes('/root/Desktop/CLONE_SITE/')) {
folderName = folderName.replace('/root/Desktop/CLONE_SITE/', '').trim();
} else if (folderName.includes('/root/Desktop/CLONE_SITE')) {
folderName = folderName.replace('/root/Desktop/CLONE_SITE', '').trim();
}
// Caso tenha digitado caminhos absolutos, extrair apenas o final
folderName = folderName.replace(/[\/\\]/g, '').trim();
const cloneMethod = 'wget';
if (!url) {
@@ -240,6 +252,7 @@ async function handleCloneSubmit(e) {
includeCSS,
includeJS,
convertLinks,
folderName,
method: cloneMethod
});
}
@@ -304,7 +317,11 @@ async function startCloning(url, options) {
response = await fetch('/api/smart-clone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
body: JSON.stringify({
url,
outputDir: settings.outputDir,
folderName: options.folderName
})
});
} else {
// WGET logic
@@ -313,7 +330,9 @@ async function startCloning(url, options) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
depth: options.depth
depth: options.depth,
outputDir: settings.outputDir,
folderName: options.folderName
})
});
}
@@ -329,9 +348,8 @@ async function startCloning(url, options) {
progressMessage.textContent = '✅ Clonagem concluída com sucesso!';
// Update stats
// Update stats
const files = data.data.resources || data.data.files || 0;
const size = data.data.size || 0; // Smart clone might not return size yet
const files = data.data?.files || data.data?.cloneInfo?.fileCount || data.data?.resources || data.files || 0;
const size = data.data?.size || data.data?.cloneInfo?.totalSize || data.size || 0;
filesDownloaded.textContent = files;
sizeDownloaded.textContent = formatBytes(size);
@@ -585,35 +603,38 @@ function loadSettings() {
}
// Populate settings form (if needed)
const outDirInput = document.getElementById('settingsOutputDir');
if (outDirInput) outDirInput.value = settings.outputDir || '/root/Desktop/CLONE_SITE';
}
function saveSettings() {
const outDirInput = document.getElementById('settingsOutputDir');
if (outDirInput) settings.outputDir = outDirInput.value;
localStorage.setItem('cloneSettings', JSON.stringify(settings));
showToast('Configurações salvas!', 'success');
}
// Utility Functions
function openClonedSite(directory) {
// Try to open via server endpoint
fetch('/api/health', {
method: 'GET'
}).then(response => {
if (response.ok) {
// Server is running, open the site via same origin
const siteName = directory.split('/').pop();
window.open(`/${siteName}/index.html`, '_blank');
} else {
showToast('Servidor não está respondendo.', 'error');
}
}).catch(error => {
showToast('Servidor não está respondendo.', 'error');
});
const siteName = directory.replace(/\\/g, '/').split('/').pop();
window.open(`/api/preview/${siteName}`, '_blank');
}
function openCloneFolder(directory) {
showToast('Abrindo pasta: ' + directory, 'info');
// In a real app, this would use an API endpoint to open the folder
console.log('Open folder:', directory);
showToast('Abrindo gerenciador de arquivos web...', 'info');
fetch('/api/open-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: directory })
}).then(res => res.json())
.then(data => {
if (!data.success) {
showToast('Erro ao abrir pasta: ' + (data.error || ''), 'error');
} else if (data.url) {
window.open(data.url, '_blank');
}
})
.catch(err => console.error('Erro ao chamar open-folder:', err));
}
function showToast(message, type = 'info') {
@@ -703,3 +724,61 @@ function formatRelativeTime(dateString) {
if (minutes > 0) return minutes === 1 ? 'Há 1 minuto' : `${minutes} minutos`;
return 'Agora mesmo';
}
// Optimized Pages
async function loadOptimizedPages() {
const listEl = document.getElementById('optimizedList');
if (!listEl) return;
listEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-spinner fa-spin"></i>
<h3>Carregando...</h3>
</div>
`;
try {
const response = await fetch('/api/optimized-pages');
const data = await response.json();
if (data.success && data.pages.length > 0) {
listEl.innerHTML = data.pages.map(page => `
<div class="clone-item">
<div class="clone-info">
<div class="clone-icon" style="background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);">
<i class="fas fa-magic" style="color: white;"></i>
</div>
<div class="clone-details">
<h4>${page.name}</h4>
<p>Otimizada pela IA • ${page.directory}</p>
</div>
</div>
<div class="clone-actions">
<button class="btn btn-primary btn-sm" onclick="openClonedSite('${page.directory}')">
<i class="fas fa-external-link-alt"></i> Visualizar
</button>
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder('${page.directory}')">
<i class="fas fa-folder-open"></i> Pasta
</button>
</div>
</div>
`).join('');
} else {
listEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-paint-brush"></i>
<h3>Nenhuma página otimizada encontrada</h3>
<p>Use o Agente Designer para otimizar suas clonagens.</p>
</div>
`;
}
} catch (e) {
listEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-triangle"></i>
<h3>Erro ao carregar</h3>
<p>${e.message}</p>
</div>
`;
}
}
+6 -5
View File
@@ -15,12 +15,8 @@ urlInput.addEventListener('keypress', (e) => {
document.getElementById('btnOpenSite').addEventListener('click', () => {
if (clonedData && clonedData.data && clonedData.data.path) {
// Construct local URL. Server serves /<folderName>/index.html
// We know server serves static files from cloned-sites directly if configured?
// Wait, server.js serves ../cloned-sites as static.
// so path is just /FolderName/index.html
const dirName = clonedData.data.path.split('\\').pop().split('/').pop();
window.open(`/${dirName}/index.html`, '_blank');
window.open(`/api/preview/${dirName}`, '_blank');
}
});
@@ -30,6 +26,11 @@ document.getElementById('btnOpenFolder').addEventListener('click', () => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: clonedData.data.path })
}).then(res => res.json())
.then(data => {
if (data.success && data.url) {
window.open(data.url, '_blank');
}
});
}
});
+8 -8
View File
@@ -418,25 +418,25 @@ document.addEventListener('DOMContentLoaded', function() {
// Funções para gerenciar sites clonados
function openCloneFolder(directory) {
// Tentar abrir a pasta no explorador do Windows
fetch('/api/open-folder', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ path: directory })
}).then(response => {
if (!response.ok) {
alert('Não foi possível abrir a pasta automaticamente.\nLocalização: ' + directory);
}).then(response => response.json())
.then(data => {
if (data.success && data.url) {
window.open(data.url, '_blank');
} else {
alert('Não foi possível abrir a pasta.\nLocalização: ' + directory);
}
}).catch(error => {
alert('Não foi possível abrir a pasta automaticamente.\nLocalização: ' + directory);
alert('Não foi possível abrir a pasta.\nLocalização: ' + directory);
});
}
function openClonedSite(directory) {
// Extract folder name and open site in browser via window.open
const folderName = directory.split('/').pop();
const siteUrl = window.location.protocol + '//' + window.location.host + '/' + folderName + '/index.html';
window.open(siteUrl, '_blank');
window.open('/api/preview/' + folderName, '_blank');
}
+40 -4
View File
@@ -40,6 +40,11 @@
<span>Storage</span>
</a>
<a href="#" class="nav-item" data-page="optimized-pages">
<i class="fas fa-paint-brush"></i>
<span>Otimizados (AI)</span>
</a>
<a href="#" class="nav-item" data-page="settings">
<i class="fas fa-cog"></i>
<span>Configurações</span>
@@ -150,6 +155,17 @@
required
>
</div>
<div class="form-group">
<label for="customFolder">
<i class="fas fa-folder"></i> Nome Específico da Pasta (Opcional)
</label>
<input
type="text"
id="customFolder"
class="form-control"
placeholder="ex: meu_projeto"
>
</div>
<div class="form-group">
<label>
@@ -238,6 +254,25 @@
</div>
</div>
<!-- Optimized Pages Page -->
<div class="page" id="optimized-pages">
<div class="section-header">
<h3><i class="fas fa-paint-brush"></i> Gestão de Páginas Otimizadas pela IA</h3>
<div class="header-actions">
<button class="btn btn-secondary btn-sm" onclick="loadOptimizedPages()">
<i class="fas fa-sync-alt"></i> Atualizar
</button>
</div>
</div>
<div class="clones-list" id="optimizedList">
<div class="empty-state">
<i class="fas fa-spinner fa-spin"></i>
<h3>Carregando...</h3>
</div>
</div>
</div>
<!-- Settings Page -->
<div class="page" id="settings">
<div class="settings-container">
@@ -250,8 +285,8 @@
<p>Local onde os sites clonados serão salvos</p>
</div>
<div class="setting-control">
<input type="text" class="form-control" value="M:\CLONEWEB\cloned-sites" readonly>
<button class="btn btn-secondary btn-sm">
<input type="text" id="settingsOutputDir" class="form-control" value="/root/Desktop/Clonados">
<button class="btn btn-secondary btn-sm" onclick="openCloneFolder(document.getElementById('settingsOutputDir').value)">
<i class="fas fa-folder-open"></i> Abrir
</button>
</div>
@@ -293,7 +328,7 @@
</div>
<div class="settings-actions">
<button class="btn btn-primary">
<button class="btn btn-primary" onclick="saveSettings()">
<i class="fas fa-save"></i> Salvar Configurações
</button>
<button class="btn btn-secondary">
@@ -386,7 +421,8 @@
<div class="agent-inputs">
<input type="text" id="designUrl" class="form-control" placeholder="URL do site para redesign...">
<button id="startDesignBtn" class="btn btn-primary">
<button id="startDesignBtn" class="btn btn-primary mt-3">
<i class="fas fa-rocket"></i> Iniciar Redesign
</button>
</div>
+1 -1
View File
@@ -373,7 +373,7 @@ function confirmDeleteClone(cloneId) {
}
function openClone(cloneId) {
window.open(`/${cloneId}/index.html`, '_blank');
window.open(`/api/preview/${cloneId}`, '_blank');
}
function cloneSiteAgain(baseUrl) {
+550 -29
View File
@@ -3,10 +3,18 @@ const path = require('path');
const cors = require('cors');
const axios = require('axios');
const { exec, spawn } = require('child_process');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
// Security / Iframe Fix
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.removeHeader('X-Frame-Options'); // Try to remove it completely first, if proxy allows
next();
});
// Storage manager routes
const storageManager = require('./storage-manager');
app.use('/api/clones', storageManager);
@@ -24,6 +32,93 @@ app.use((req, res, next) => {
app.use(cors());
app.use(express.json());
// Helper para parsear cookies
function parseCookies(cookieHeader) {
const list = {};
if (!cookieHeader) return list;
cookieHeader.split(';').forEach(cookie => {
let [name, ...rest] = cookie.split('=');
name = name?.trim();
if (!name) return;
const value = rest.join('=').trim();
if (!value) return;
list[name] = decodeURIComponent(value);
});
return list;
}
// Rota para ativar o Virtual Root Preview (define o cookie e redireciona para a raiz)
app.get('/api/preview/:folder', (req, res) => {
const folderName = req.params.folder;
res.setHeader('Set-Cookie', `clone_preview_target=${encodeURIComponent(folderName)}; Path=/; HttpOnly; SameSite=Lax`);
res.redirect('/');
});
// Rota para sair do Virtual Root Preview (limpa o cookie e volta ao dashboard)
app.get('/api/exit-preview', (req, res) => {
res.setHeader('Set-Cookie', 'clone_preview_target=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
res.redirect('/');
});
// Middleware de Virtual Root Proxy (serve o site clonado diretamente na raiz / quando o cookie está ativo)
app.use((req, res, next) => {
const cookies = parseCookies(req.headers.cookie);
if (cookies.clone_preview_target && !req.path.startsWith('/api') && req.path !== '/old') {
const folderName = cookies.clone_preview_target;
const folderPath = path.join(__dirname, '..', 'cloned-sites', folderName);
if (fs.existsSync(folderPath)) {
let targetFile = req.path;
if (targetFile === '/') targetFile = '/index.html';
// Se o arquivo solicitado existir dentro da pasta do clone
const fullPath = path.join(folderPath, targetFile);
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
// Se for HTML, injeta a barra de preview flutuante
if (targetFile.endsWith('.html')) {
let html = fs.readFileSync(fullPath, 'utf8');
const previewBar = `
<div id="__cloneweb_preview_bar" style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1f2937; color: white; padding: 12px 24px; border-radius: 50px; box-shadow: 0 10px 25px rgba(0,0,0,0.3); display: flex; align-items: center; gap: 16px; z-index: 2147483647; font-family: 'Inter', sans-serif; font-size: 14px;">
<span style="display: flex; align-items: center; gap: 8px;"><i class="fas fa-eye" style="color: #10b981;"></i> Live Preview: <strong style="color: #60a5fa;">${folderName.split('_')[0]}</strong></span>
<a href="/api/exit-preview" style="background: #ef4444; color: white; padding: 6px 16px; border-radius: 30px; text-decoration: none; font-weight: 600; transition: all 0.2s; box-shadow: 0 4px 12px rgba(239,68,68,0.3); font-size: 13px;">Sair do Preview</a>
</div>
`;
if (html.includes('</body>')) {
html = html.replace('</body>', previewBar + '</body>');
} else {
html += previewBar;
}
res.setHeader('Content-Type', 'text/html');
return res.send(html);
}
// Para outros arquivos (CSS, JS, imagens), serve diretamente com o MIME type correto
if (targetFile.endsWith('.css')) res.setHeader('Content-Type', 'text/css');
if (targetFile.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript');
return res.sendFile(fullPath);
} else {
// Fallback de SPA: se a rota virtual não existir como arquivo no disco, serve o index.html do clone!!!
const indexPath = path.join(folderPath, 'index.html');
if (fs.existsSync(indexPath)) {
let html = fs.readFileSync(indexPath, 'utf8');
const previewBar = `
<div id="__cloneweb_preview_bar" style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1f2937; color: white; padding: 12px 24px; border-radius: 50px; box-shadow: 0 10px 25px rgba(0,0,0,0.3); display: flex; align-items: center; gap: 16px; z-index: 2147483647; font-family: 'Inter', sans-serif; font-size: 14px;">
<span style="display: flex; align-items: center; gap: 8px;"><i class="fas fa-eye" style="color: #10b981;"></i> Live Preview: <strong style="color: #60a5fa;">${folderName.split('_')[0]}</strong></span>
<a href="/api/exit-preview" style="background: #ef4444; color: white; padding: 6px 16px; border-radius: 30px; text-decoration: none; font-weight: 600; transition: all 0.2s; box-shadow: 0 4px 12px rgba(239,68,68,0.3); font-size: 13px;">Sair do Preview</a>
</div>
`;
if (html.includes('</body>')) html = html.replace('</body>', previewBar + '</body>');
else html += previewBar;
res.setHeader('Content-Type', 'text/html');
return res.send(html);
}
}
}
}
next();
});
// Serve static files with proper MIME types
app.use(express.static(path.join(__dirname, 'public'), {
setHeaders: (res, path) => {
@@ -33,6 +128,137 @@ app.use(express.static(path.join(__dirname, 'public'), {
}
}));
// Middleware para injetar fix de roteador em sites clonados (SPAs como React, Vue, Angular, Vite) e reescrever JS
app.use((req, res, next) => {
if (!req.path.startsWith('/api') && req.path !== '/' && req.path !== '/old') {
let urlPath = req.path;
// Remover prefixo /cloned-sites se existir para buscar corretamente no disco
if (urlPath.startsWith('/cloned-sites/')) {
urlPath = urlPath.replace('/cloned-sites/', '/');
}
if (!urlPath.match(/\.[a-zA-Z0-9]+$/)) {
if (!urlPath.endsWith('/')) urlPath += '/';
urlPath += 'index.html';
}
// Interceptação de arquivos HTML (Camada 1 - DOM Mock Universal)
if (urlPath.endsWith('.html')) {
const filePath = path.join(__dirname, '..', 'cloned-sites', urlPath);
if (fs.existsSync(filePath)) {
try {
let html = fs.readFileSync(filePath, 'utf8');
if (!html.includes('__CLONEWEB_ROUTER_FIX__')) {
const injectScript = `
<script id="__CLONEWEB_ROUTER_FIX__">
(function() {
try {
window.__mockLocationPathname = '/';
window.__mockLocationHref = window.location.protocol + '//' + window.location.host + '/';
let targetObj = window.location;
let proto = Object.getPrototypeOf(targetObj);
let pathDesc = null;
let hrefDesc = null;
while (proto && (!pathDesc || !hrefDesc)) {
if (!pathDesc) pathDesc = Object.getOwnPropertyDescriptor(proto, 'pathname');
if (!hrefDesc) hrefDesc = Object.getOwnPropertyDescriptor(proto, 'href');
proto = Object.getPrototypeOf(proto);
}
if (!pathDesc) pathDesc = Object.getOwnPropertyDescriptor(targetObj, 'pathname');
if (!hrefDesc) hrefDesc = Object.getOwnPropertyDescriptor(targetObj, 'href');
if (pathDesc && pathDesc.configurable) {
const origGet = pathDesc.get;
Object.defineProperty(proto || targetObj, 'pathname', {
get: function() {
const p = origGet ? origGet.call(this) : this.pathname;
if (p !== '/' && p !== '/index.html' && (p.includes('_202') || p.includes('/cloned-sites/'))) {
return '/';
}
return p;
},
set: pathDesc.set || function(v) { window.location.assign(v); }
});
}
if (hrefDesc && hrefDesc.configurable) {
const origHrefGet = hrefDesc.get;
Object.defineProperty(proto || targetObj, 'href', {
get: function() {
const h = origHrefGet ? origHrefGet.call(this) : this.href;
if (h.includes('_202') || h.includes('/cloned-sites/')) {
const u = new URL(h);
return u.protocol + '//' + u.host + '/';
}
return h;
},
set: hrefDesc.set || function(v) { window.location.assign(v); }
});
}
const OriginalURL = window.URL;
window.URL = function(url, base) {
if (typeof url === 'string' && (url.includes('_202') || url.includes('/cloned-sites/'))) {
url = url.replace(/\\/cloned-sites\\/[^/]+/gi, '').replace(/\\/[^/]+_202[0-9-_T:.]+(\\/index\\.html)?/gi, '/');
}
return new OriginalURL(url, base);
};
window.URL.prototype = OriginalURL.prototype;
Object.assign(window.URL, OriginalURL);
console.log('🚀 [CloneWeb] Ultimate SPA Router fix initialized successfully (Cross-browser Gecko/Blink support).');
} catch(e) {
console.error('⚠️ [CloneWeb] DOM mock fallback active. Using __mockLocationPathname. Error:', e);
}
})();
</script>
`;
if (html.includes('<head>')) {
html = html.replace('<head>', '<head>' + injectScript);
} else {
html = injectScript + html;
}
}
res.setHeader('Content-Type', 'text/html');
return res.send(html);
} catch (err) {
console.error('Erro ao injetar fix no HTML do clone:', err);
}
}
}
// Interceptação de arquivos JS de sites clonados (Camada 2 - JS Rewriter Supremo)
if (urlPath.endsWith('.js') && (urlPath.includes('_202') || urlPath.includes('/cloned-sites/'))) {
const filePath = path.join(__dirname, '..', 'cloned-sites', urlPath);
if (fs.existsSync(filePath)) {
try {
let jsContent = fs.readFileSync(filePath, 'utf8');
// Substitui window.location.pathname de forma segura
jsContent = jsContent.replace(/\bwindow\.location\.pathname\b/g, '(window.__mockLocationPathname || window.location.pathname)');
// Substitui location.pathname isolado (evita quebrar e.location.pathname ou history.location.pathname)
jsContent = jsContent.replace(/(?<!\w\.)\blocation\.pathname\b/g, '(window.__mockLocationPathname || window.location.pathname)');
// Substitui window.location.href de forma segura
jsContent = jsContent.replace(/\bwindow\.location\.href\b/g, '(window.__mockLocationHref || window.location.href)');
// Substitui location.href isolado (evita quebrar e.location.href ou history.location.href)
jsContent = jsContent.replace(/(?<!\w\.)\blocation\.href\b/g, '(window.__mockLocationHref || window.location.href)');
res.setHeader('Content-Type', 'application/javascript');
return res.send(jsContent);
} catch (err) {
console.error('Erro ao reescrever JS do clone:', err);
}
}
}
}
next();
});
// Serve cloned sites directory
app.use(express.static(path.join(__dirname, '..', 'cloned-sites'), {
setHeaders: (res, filePath) => {
@@ -82,10 +308,10 @@ app.post('/api/wget-clone', async (req, res) => {
const { exec } = require('child_process');
const containerId = require('os').hostname();
const cloneDir = directory.split('/').pop();
const hostDir = '/root/Desktop/Clonados';
exec(`docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => {
const hostDir = req.body.outputDir || '/root/Desktop/CLONE_SITE';
exec(`mkdir -p "${hostDir}" && cp -r "${directory}" "${hostDir}/" 2>/dev/null || docker cp ${containerId}:"${directory}" "${hostDir}/" 2>/dev/null`, (err) => {
if (err) console.log('⚠️ Sync to host failed:', err.message);
else console.log('✅ Clone synced to host:', cloneDir);
else console.log('✅ Clone synced to host Desktop:', cloneDir);
});
}
@@ -101,7 +327,7 @@ app.post('/api/wget-clone', async (req, res) => {
// Smart Clone Endpoint (Local Spawn)
app.post('/api/smart-clone', (req, res) => {
const { url } = req.body;
const { url, folderName } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL is required' });
@@ -112,9 +338,12 @@ app.post('/api/smart-clone', (req, res) => {
// Spawn the smart cloner process
const scriptPath = path.join(__dirname, '..', 'simple-crawler', 'smart-cloner.js');
console.log(`[SmartClone] Executing: node "${scriptPath}" --url="${url}"`);
let spawnArgs = [scriptPath, `--url=${url}`];
if (folderName) spawnArgs.push(`--folderName=${folderName}`);
const child = spawn('node', [scriptPath, `--url=${url}`], {
console.log(`[SmartClone] Executing: node "${scriptPath}" --url="${url}" ${folderName ? `--folderName="${folderName}"` : ''}`);
const child = spawn('node', spawnArgs, {
cwd: path.join(__dirname, '..'),
shell: true
});
@@ -152,6 +381,20 @@ app.post('/api/smart-clone', (req, res) => {
if (matches && matches.length > 0) {
const lastMatch = matches[matches.length - 1];
const result = JSON.parse(lastMatch);
// Sync to host /root/Desktop/Clonados
const directory = result.data?.directory || result.directory || result.path;
if (result.success && directory) {
const { exec } = require('child_process');
const containerId = require('os').hostname();
const cloneDir = directory.split('/').pop();
const hostDir = req.body.outputDir || '/root/Desktop/CLONE_SITE';
exec(`mkdir -p "${hostDir}" && cp -r "${directory}" "${hostDir}/" 2>/dev/null || docker cp ${containerId}:"${directory}" "${hostDir}/" 2>/dev/null`, (err) => {
if (err) console.log('⚠️ SmartClone sync to host failed:', err.message);
else console.log('✅ SmartClone synced to host Desktop:', cloneDir);
});
}
res.json({
success: result.success,
data: result,
@@ -186,7 +429,102 @@ app.get('/api/health', async (req, res) => {
}
});
// Abrir pasta do clone
// Rota de visualização de arquivos da pasta clonada (Gerenciador de Arquivos Web Embutido)
app.get('/api/browse/:folder', (req, res) => {
try {
const folderName = req.params.folder;
const folderPath = path.join(__dirname, '..', 'cloned-sites', folderName);
if (!fs.existsSync(folderPath)) {
return res.status(404).send('<h1>Pasta não encontrada</h1><p>A pasta solicitada não existe no servidor.</p>');
}
const items = fs.readdirSync(folderPath);
let filesHtml = '';
items.forEach(item => {
const itemPath = path.join(folderPath, item);
const stat = fs.statSync(itemPath);
const isDir = stat.isDirectory();
const size = isDir ? '-' : (stat.size / 1024).toFixed(2) + ' KB';
const date = new Date(stat.mtime).toLocaleString('pt-BR');
const icon = isDir ? 'fa-folder' : (item.endsWith('.html') ? 'fa-file-code' : (item.endsWith('.json') ? 'fa-file-alt' : 'fa-file'));
const link = isDir ? `#` : `/${folderName}/${item}`;
filesHtml += `
<tr>
<td><i class="fas ${icon}" style="color: #4f46e5; margin-right: 10px;"></i> <a href="${link}" target="_blank" style="color: #1f2937; text-decoration: none; font-weight: 500;">${item}</a></td>
<td style="color: #6b7280;">${size}</td>
<td style="color: #6b7280;">${date}</td>
<td>
${!isDir ? `<a href="${link}" target="_blank" class="btn btn-secondary btn-sm" style="background: #e0e7ff; color: #4f46e5; padding: 5px 10px; border-radius: 6px; text-decoration: none; font-size: 14px;">Visualizar</a>` : ''}
${!isDir ? `<a href="${link}" download="${item}" class="btn btn-secondary btn-sm" style="background: #fee2e2; color: #ef4444; padding: 5px 10px; border-radius: 6px; text-decoration: none; font-size: 14px; margin-left: 5px;">Baixar</a>` : ''}
</td>
</tr>
`;
});
const html = `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arquivos Clonados - ${folderName}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #f3f4f6; margin: 0; padding: 40px 20px; color: #1f2937; }
.container { max-width: 1000px; margin: 0 auto; background: white; border-radius: 16px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); overflow: hidden; padding: 30px; }
.header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e5e7eb; padding-bottom: 20px; margin-bottom: 25px; }
.header h1 { font-size: 24px; margin: 0; color: #111827; display: flex; align-items: center; gap: 12px; }
.actions { display: flex; gap: 12px; }
.btn { padding: 10px 20px; border-radius: 8px; font-weight: 600; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; font-size: 14px; transition: all 0.2s; }
.btn-primary { background: #4f46e5; color: white; }
.btn-primary:hover { background: #4338ca; }
.btn-secondary { background: #f3f4f6; color: #374151; }
.btn-secondary:hover { background: #e5e7eb; }
table { width: 100%; border-collapse: collapse; text-align: left; }
th { padding: 16px; background: #f9fafb; font-weight: 600; color: #374151; border-bottom: 1px solid #e5e7eb; font-size: 14px; }
td { padding: 16px; border-bottom: 1px solid #e5e7eb; font-size: 15px; }
tr:hover { background: #f9fafb; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-folder-open" style="color: #4f46e5;"></i> ${folderName}</h1>
<div class="actions">
<a href="/${folderName}/index.html" target="_blank" class="btn btn-primary"><i class="fas fa-external-link-alt"></i> Abrir Site Clonado</a>
<a href="/" class="btn btn-secondary"><i class="fas fa-arrow-left"></i> Voltar ao Dashboard</a>
</div>
</div>
<table>
<thead>
<tr>
<th>Nome do Ficheiro</th>
<th>Tamanho</th>
<th>Modificado em</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
${filesHtml}
</tbody>
</table>
</div>
</body>
</html>
`;
res.send(html);
} catch (error) {
console.error('Erro ao listar pasta:', error);
res.status(500).send('<h1>Erro interno do servidor</h1>');
}
});
// Abrir pasta do clone (mantém compatibilidade legada e retorna a URL do Web Browse)
app.post('/api/open-folder', (req, res) => {
try {
const { path: folderPath } = req.body;
@@ -194,14 +532,10 @@ app.post('/api/open-folder', (req, res) => {
return res.status(400).json({ error: 'Caminho da pasta é obrigatório' });
}
// Abrir pasta no gerenciador de ficheiros
exec(`xdg-open "${folderPath}"`, (error) => {
if (error) {
console.error('Erro ao abrir pasta:', error);
return res.status(500).json({ error: 'Não foi possível abrir a pasta' });
}
res.json({ success: true, message: 'Pasta aberta com sucesso' });
});
const folderName = path.basename(folderPath);
const browseUrl = `/api/browse/${folderName}`;
res.json({ success: true, message: 'Pasta acessível via Web', url: browseUrl });
} catch (error) {
console.error('Erro ao abrir pasta:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
@@ -216,20 +550,10 @@ app.post('/api/open-site', (req, res) => {
return res.status(400).json({ error: 'Caminho do site é obrigatório' });
}
// Extrair apenas o nome da pasta (último segmento do caminho)
const folderName = path.basename(sitePath);
const siteUrl = `http://localhost:${PORT}/${folderName}/index.html`;
const siteUrl = `/${folderName}/index.html`;
console.log(`🌍 Opening site via HTTP: ${siteUrl}`);
// Abrir URL no navegador padrão (Linux)
exec(`xdg-open "${siteUrl}"`, (error) => {
if (error) {
console.error('Erro ao abrir site:', error);
return res.status(500).json({ error: 'Não foi possível abrir o site via HTTP' });
}
res.json({ success: true, message: 'Site aberto via HTTP com sucesso', url: siteUrl });
});
res.json({ success: true, message: 'Site aberto com sucesso', url: siteUrl });
} catch (error) {
console.error('Erro ao abrir site:', error);
res.status(500).json({ error: 'Erro interno do servidor' });
@@ -282,13 +606,39 @@ app.get('/index', (req, res) => {
res.redirect('/');
});
// List optimized pages
app.get('/api/optimized-pages', (req, res) => {
try {
const clonesDir = path.join(__dirname, '..', 'cloned-sites');
if (!fs.existsSync(clonesDir)) {
return res.json({ success: true, pages: [] });
}
const items = fs.readdirSync(clonesDir);
const optimized = items
.filter(item => {
const fullPath = path.join(clonesDir, item);
return fs.statSync(fullPath).isDirectory() && item.endsWith('_designer');
})
.map(item => ({
name: item,
directory: item
}));
res.json({ success: true, pages: optimized });
} catch (error) {
console.error('Error listing optimized pages:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// AI Agent Route (MiniMax)
app.post('/api/ai/agent', async (req, res) => {
const { systemPrompt, userMessage } = req.body;
const apiKey = process.env.MINIMAX_API_KEY;
const apiKey = process.env.MINIMAX_API_KEY || 'sk-cp-iWzSsdzjVy0tT5dnvv_sGrAlv3Nw7kkrZ8hPK0KjbNeSRQxIY1O6AhkQAq_yrYV1XPJY1TH6ZQeOlJZd-Zi_elkJhef9PcTSzn2m9bcKIxezarM19BREcVM';
if (!apiKey) {
return res.status(500).json({ error: 'MINIMAX_API_KEY is missing in .env' });
return res.status(500).json({ error: 'MINIMAX_API_KEY is missing' });
}
try {
@@ -318,6 +668,177 @@ app.post('/api/ai/agent', async (req, res) => {
}
});
// AI Inject Redesign Route (Pilar 1 - Agente Designer Real)
app.post('/api/ai/inject-redesign', (req, res) => {
const { cloneDir, appliedStyles, elementsRedesigned, outputDir, agentName } = req.body;
if (!cloneDir) return res.status(400).json({ error: 'cloneDir is required' });
let originalFolderName = '';
let folderPath = '';
let newFolderPath = '';
// Sempre usar o diretório interno (cloned-sites)
const cleanDir = cloneDir.endsWith('/') ? cloneDir.slice(0, -1) : cloneDir;
originalFolderName = cleanDir.split('/').pop();
folderPath = path.join(__dirname, '..', 'cloned-sites', originalFolderName);
newFolderPath = path.join(__dirname, '..', 'cloned-sites', `${originalFolderName}_${agentName || 'designer'}`);
if (!fs.existsSync(folderPath)) {
return res.status(404).json({ error: 'Cloned site folder not found' });
}
try {
// 1. Criar o arquivo CSS de Redesign Ultra-Moderno (__ai_redesign.css)
const cssContent = `
/* ==========================================================================
BRAINSTEEL WEB - AI ULTRA-MODERN REDESIGN (AGENTE DESIGNER)
========================================================================== */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;600;800&display=swap');
:root {
--ai-bg-dark: #0b0f19;
--ai-bg-card: rgba(18, 24, 38, 0.7);
--ai-border-glass: rgba(255, 255, 255, 0.08);
--ai-accent-glow: #3b82f6;
--ai-accent-gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
--ai-text-main: #f3f4f6;
--ai-text-muted: #9ca3af;
}
/* Força Dark Mode elegante e tipografia moderna no body e containers principais */
body, main, #root, #app, .app, .main, .container, .wrapper, header, footer, section {
background-color: var(--ai-bg-dark) !important;
color: var(--ai-text-main) !important;
font-family: 'Inter', sans-serif !important;
}
/* Glassmorphism e Glow em Cards, Painéis, Cabeçalhos e Rodapés */
div[class*="card"], div[class*="panel"], div[class*="box"], div[class*="container"], div[class*="item"], header, footer, nav, .navbar, .header, .footer {
background: var(--ai-bg-card) !important;
backdrop-filter: blur(16px) !important;
-webkit-backdrop-filter: blur(16px) !important;
border: 1px solid var(--ai-border-glass) !important;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.5) !important;
border-radius: 16px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
div[class*="card"]:hover, div[class*="item"]:hover {
transform: translateY(-5px) !important;
border-color: rgba(59, 130, 246, 0.4) !important;
box-shadow: 0 20px 40px -15px rgba(59, 130, 246, 0.3) !important;
}
/* Títulos com Gradiente Vibrante (Outfit Font) */
h1, h2, h3, h4, h5, .title, .heading, .header-title {
font-family: 'Outfit', sans-serif !important;
background: var(--ai-accent-gradient) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
font-weight: 700 !important;
letter-spacing: -0.03em !important;
}
/* Botões Ultra-Modernos com Gradiente e Animação de Pulso */
button, .btn, a[class*="btn"], input[type="submit"], input[type="button"] {
background: var(--ai-accent-gradient) !important;
color: #ffffff !important;
border: none !important;
border-radius: 50px !important;
padding: 12px 28px !important;
font-family: 'Inter', sans-serif !important;
font-weight: 600 !important;
letter-spacing: 0.02em !important;
box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.5) !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
text-transform: none !important;
-webkit-text-fill-color: #ffffff !important;
}
button:hover, .btn:hover, a[class*="btn"]:hover {
transform: scale(1.05) translateY(-2px) !important;
box-shadow: 0 15px 35px -5px rgba(236, 72, 153, 0.6) !important;
}
/* Inputs e Textareas com Efeito Glass */
input, select, textarea, .input, .form-control {
background: rgba(255, 255, 255, 0.05) !important;
border: 1px solid var(--ai-border-glass) !important;
color: var(--ai-text-main) !important;
border-radius: 12px !important;
padding: 12px 16px !important;
font-family: 'Inter', sans-serif !important;
}
input:focus, select:focus, textarea:focus {
outline: none !important;
border-color: var(--ai-accent-glow) !important;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3) !important;
}
/* Links e Textos Secundários */
a {
color: var(--ai-accent-glow) !important;
text-decoration: none !important;
transition: color 0.2s !important;
}
a:hover {
color: #8b5cf6 !important;
}
p, span, li, label, td, th {
color: var(--ai-text-muted) !important;
line-height: 1.6 !important;
}
`;
// Copy the original folder to the new folder
exec(`cp -r "${folderPath}" "${newFolderPath}"`, (copyErr) => {
if (copyErr) {
console.error('Error copying folder for agent redesign:', copyErr);
return res.status(500).json({ error: 'Failed to create new agent folder' });
}
// 1. Criar o arquivo CSS de Redesign Ultra-Moderno (__ai_redesign.css) no novo diretório
const cssPath = path.join(newFolderPath, '__ai_redesign.css');
fs.writeFileSync(cssPath, cssContent, 'utf8');
// 2. Injetar a tag <link> no index.html do novo clone
const indexPath = path.join(newFolderPath, 'index.html');
if (fs.existsSync(indexPath)) {
let html = fs.readFileSync(indexPath, 'utf8');
const linkTag = `\n<link rel="stylesheet" href="/__ai_redesign.css" id="__AI_REDESIGN_LINK__">\n`;
if (!html.includes('__ai_redesign.css')) {
if (html.includes('</head>')) {
html = html.replace('</head>', linkTag + '</head>');
} else {
html = linkTag + html;
}
fs.writeFileSync(indexPath, html, 'utf8');
}
}
// 3. Sincronizar o novo diretório com o host
const hostDir = outputDir || '/root/Desktop/CLONE_SITE';
const containerId = require('os').hostname();
exec(`mkdir -p "${hostDir}" && cp -r "${newFolderPath}" "${hostDir}/" 2>/dev/null || docker cp ${containerId}:"${newFolderPath}" "${hostDir}/" 2>/dev/null`, (syncErr) => {
if (syncErr) console.log('⚠️ Redesign sync to host failed:', syncErr.message);
else console.log('✅ Redesign synced to host output dir:', path.basename(newFolderPath));
});
res.json({ success: true, message: 'AI Redesign CSS injected successfully', newFolderName: path.basename(newFolderPath) });
});
} catch (err) {
console.error('Error injecting AI redesign:', err);
res.status(500).json({ error: err.message });
}
});
// Handle 404s
app.use((req, res) => {
res.status(404).json({