From 61a398aa08f70afde3fe707fcd5e84c7f4417608 Mon Sep 17 00:00:00 2001 From: lacasoft Date: Mon, 17 Aug 2026 19:01:58 +0200 Subject: [PATCH] feat: cierra los huecos de Java, C# y Angular en el kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los tres stacks ya existían (backend/spring, backend/dotnet, frontend/angular), pero tenían huecos reales alrededor. Added: - Estándares de C# en el agente coder. 'Estándares por lenguaje' cubría TypeScript, Python, Go, Rust, Solidity, PHP, Java y Dart, pero no C#, pese a existir un stack backend/dotnet completo y a que la regla de nombres ya lo mencionaba. Un proyecto .NET recibía el overlay del stack pero ninguna guía de lenguaje. - Overlays de Spring (25→40), .NET (26→44) y Angular (26→47) profundizados con concurrencia, resiliencia, caché, observabilidad y calidad de proyecto. Fixed: - Detección de Spring incompleta: se exigía la cadena literal 'spring-boot' en el pom.xml, así que un proyecto que solo declara org.springframework (BOM o Spring Framework sin Boot) no se detectaba; y los settings.gradle(.kts) no se miraban. Los casos de build.gradle(.kts) sí funcionaban ya y quedan cubiertos como guardas de regresión. - Un proyecto Java sin Spring quedaba en un callejón sin salida: la detección devolvía null sin explicar nada. Ahora se imprime qué se vio y se aclara que el overlay de Java del kit es Spring Boot, avisando de que para Quarkus, Micronaut o Jakarta EE puro no encaja. Tests: 51 (10 nuevos de detección). --- CHANGELOG.md | 6 ++ README.md | 4 +- install.js | 26 +++++++- shared/.claude/agents/coder.md | 6 +- stacks/backend/dotnet/.claude/CLAUDE.md | 22 ++++++- stacks/backend/spring/.claude/CLAUDE.md | 17 ++++- stacks/frontend/angular/.claude/CLAUDE.md | 21 ++++++ test/install-detect.test.cjs | 79 +++++++++++++++++++++++ 8 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 test/install-detect.test.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c9e5e1a..31819b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Formato basado en [Keep a Changelog](https://keepachangelog.com/) y [SemVer](htt ## [No publicado] ### Added +- **Estándares de C# en el agente `coder`.** "Estándares por lenguaje" cubría TypeScript, Python, Go, Rust, Solidity, PHP, Java y Dart, pero **no C#**, pese a existir un stack `backend/dotnet` completo y a que la regla de nombres ya lo mencionaba. Se añade su sección (nullable reference types, `record`/`sealed`, `async` sin `.Result`/`async void`, `CancellationToken`, excepciones específicas, analyzers como error) y se incluye C# en la lista de lenguajes del agente. +- **Overlays de Spring, .NET y Angular más profundos.** Spring: virtual threads y `@Async` con executor explícito, Resilience4j, `@Cacheable` con clave y TTL, Actuator + Micrometer, `readOnly` en lecturas, la trampa del proxy de `@Transactional` en llamadas internas, y slices de test frente a `@SpringBootTest`. .NET: `CancellationToken` propagado, `IHttpClientFactory` con typed clients y resiliencia, `HybridCache`, OpenTelemetry y health checks, `MapGroup` con filtros, la captura de `Scoped` desde `Singleton`, `Nullable`/`TreatWarningsAsErrors`, y sustitución de dependencias vía `ConfigureTestServices`. Angular: `@defer`, `computed` frente a `effect`, `linkedSignal`, rutas lazy con guards funcionales y `withComponentInputBinding`, interceptores funcionales, formularios reactivos tipados, `NgOptimizedImage`, accesibilidad y presupuestos de bundle. + +### Fixed +- **Detección de Spring incompleta**: se exigía la cadena literal `spring-boot` en el `pom.xml`, así que un proyecto que solo declara `org.springframework` (BOM o Spring Framework sin Boot) no se detectaba; y los `settings.gradle`/`settings.gradle.kts` no se miraban en absoluto. Ahora se buscan varias señales en los cinco archivos de build. +- **Un proyecto Java sin Spring quedaba en un callejón sin salida**: la detección devolvía `null` sin explicar nada y, con `--yes`, el instalador salía con código 1 a secas. Ahora imprime qué vio (`pom.xml`/`build.gradle`), aclara que el overlay de Java del kit es Spring Boot y avisa de que para Quarkus, Micronaut o Jakarta EE puro ese overlay no encaja. Misma pista para ecosistemas sin overlay (Go, Rust) y para un `package.json` sin framework reconocido. - **`validate.cjs` verifica que el README no mienta.** El README se había desviado en silencio: listaba helpers borrados dos releases antes, 9 de los 13 stacks, "los 8 stacks" del CI y ejemplos pineados a `#v1.2.0`. Ahora se comprueban contra el repo los conteos de agentes, skills y stacks (los del árbol y los del CI), la lista de helpers, los stacks por categoría, el número de componentes descartados, y que todo ejemplo `#vX.Y.Z` apunte a la versión de `package.json`. Si una de esas frases desaparece, falla explícitamente en vez de dejar de comprobar: 38 → 49 comprobaciones. ## [2.1.0] - 2026-08-08 diff --git a/README.md b/README.md index 4f95fd1..91046f8 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,9 @@ Flags: `--stack backend/nestjs` (fuerza stack) · `--yes` (no interactivo, conse `--no-flow` · `--no-external` · `--update`/`--force` (trae la última capa base sin pisar lo que hayas editado) · `--dry-run` (simula) · `--help`. El instalador: -1. **Detecta el stack** (pubspec→flutter, react-native, next, angular, nest, foundry, .csproj→dotnet, spring, django, composer→php, fastapi, react, express). +1. **Detecta el stack** (pubspec→flutter, react-native, next, angular, nest, foundry, `.csproj`/`.sln`→dotnet, Spring, django, composer→php, fastapi, react, express). + - **Java/Kotlin**: se reconoce Spring por sus señales (`spring-boot`, `org.springframework`, el plugin de Gradle o `io.spring.dependency-management`) en `pom.xml`, `build.gradle(.kts)` y `settings.gradle(.kts)`. Un proyecto Java **sin** Spring no se fuerza al overlay de Spring: el instalador lo dice y te deja elegir. + - Si la detección falla, imprime **qué vio** en el directorio en vez de rendirse sin explicación. - En un proyecto **fullstack con un solo `package.json`** (p. ej. express + react juntos) gana el **frontend**; si el repo es principalmente API, fuerza con `--stack backend/express`. 2. Hace **backup** de tu `.claude/` si existe. 3. Aplica la **capa base** (`shared/.claude` → `.claude/`): agentes, skills, helpers, settings, comandos. diff --git a/install.js b/install.js index af75a50..db0cb81 100644 --- a/install.js +++ b/install.js @@ -170,6 +170,29 @@ function dirHasExt(re) { return false; } } +// Archivos de build de Java/Kotlin. Un proyecto Java NO es necesariamente Spring. +const JAVA_BUILD_FILES = ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"]; +const javaBuildFiles = () => JAVA_BUILD_FILES.filter((f) => fs.existsSync(path.join(CWD, f))); + +// Spring se declara de muchas formas: starters, BOM, plugin de Gradle, imports del framework. +// Antes solo se miraba /spring-boot/ en pom.xml y /spring/ en build.gradle, así que un Gradle +// con Kotlin DSL o un proyecto Spring sin la palabra exacta se quedaba sin detectar. +const SPRING_SIGNALS = /spring-boot|spring-framework|org\.springframework|io\.spring\.dependency-management/i; +const isSpringProject = () => javaBuildFiles().some((f) => fileHas(path.join(CWD, f), SPRING_SIGNALS)); + +// Qué se vio en el directorio cuando la detección falla. Sin esto el usuario solo recibe +// "no se pudo detectar" y tiene que adivinar por qué. +function detectionHint() { + const java = javaBuildFiles(); + if (java.length) { + return `Vi ${java.join(", ")}: es un proyecto Java/Kotlin, pero no encontré señales de Spring.\n El overlay de Java del kit es Spring Boot; úsalo con --stack backend/spring si aplica.\n Si es Quarkus, Micronaut o Jakarta EE puro, ese overlay no encaja: dinos cuál necesitas.`; + } + if (fs.existsSync(path.join(CWD, "go.mod"))) return "Vi go.mod: aún no hay overlay de Go en el kit."; + if (fs.existsSync(path.join(CWD, "Cargo.toml"))) return "Vi Cargo.toml: aún no hay overlay de Rust en el kit."; + if (fs.existsSync(path.join(CWD, "package.json"))) return "Vi package.json pero ningún framework reconocido en sus dependencias."; + return "No vi ningún archivo de proyecto reconocible en este directorio."; +} + function detectStack() { const pkg = readJSON(path.join(CWD, "package.json")) || {}; const deps = { ...pkg.dependencies, ...pkg.devDependencies }; @@ -185,7 +208,7 @@ function detectStack() { if (fs.existsSync(path.join(CWD, "nest-cli.json")) || deps["@nestjs/core"]) return "backend/nestjs"; if (fs.existsSync(path.join(CWD, "foundry.toml"))) return "blockchain/solidity"; if (dirHasExt(/\.(csproj|sln)$/)) return "backend/dotnet"; - if (fileHas(path.join(CWD, "pom.xml"), /spring-boot/i) || fileHas(path.join(CWD, "build.gradle"), /spring/i) || fileHas(path.join(CWD, "build.gradle.kts"), /spring/i)) return "backend/spring"; + if (isSpringProject()) return "backend/spring"; if (fs.existsSync(path.join(CWD, "manage.py")) || fileHas(req, /(^|\n)\s*django\b/i) || fileHas(pyproj, /\bdjango\b/i)) return "backend/django"; if (fs.existsSync(path.join(CWD, "composer.json"))) return "backend/php"; if (fileHas(req, /fastapi/i) || fileHas(pyproj, /fastapi/i)) return "backend/fastapi"; @@ -748,6 +771,7 @@ async function main() { if (!(await confirm("¿Es correcto?", true))) stackId = null; } if (!stackId) { + if (!FLAGS.stack) console.log(`\n🔍 ${detectionHint()}`); console.log("\nStacks disponibles:\n " + Object.keys(STACKS).join("\n ")); const answer = await ask("\nElige stack (category/stack): "); stackId = (answer || "").trim(); diff --git a/shared/.claude/agents/coder.md b/shared/.claude/agents/coder.md index 1f99fb9..602ce80 100644 --- a/shared/.claude/agents/coder.md +++ b/shared/.claude/agents/coder.md @@ -4,7 +4,7 @@ description: "Úsalo para implementar features o refactorizar con criterio senio tools: Read, Write, Edit, Bash, Glob, Grep --- -Eres un ingeniero de software senior con más de 20 años de experiencia construyendo y operando sistemas de producción en muchos lenguajes (TypeScript/JavaScript, Python, Go, Rust, Java, PHP, Solidity, Dart) y paradigmas (orientado a objetos, funcional, reactivo). Tu enfoque abarca correctitud, legibilidad, rendimiento y mantenibilidad a largo plazo, con un fuerte sesgo hacia el cambio mínimo y coherente y hacia código que se lee como prosa. Escribes código que tu yo del futuro —y cualquier colega— entenderá sin preguntar. +Eres un ingeniero de software senior con más de 20 años de experiencia construyendo y operando sistemas de producción en muchos lenguajes (TypeScript/JavaScript, Python, Go, Rust, Java, C#, PHP, Solidity, Dart) y paradigmas (orientado a objetos, funcional, reactivo). Tu enfoque abarca correctitud, legibilidad, rendimiento y mantenibilidad a largo plazo, con un fuerte sesgo hacia el cambio mínimo y coherente y hacia código que se lee como prosa. Escribes código que tu yo del futuro —y cualquier colega— entenderá sin preguntar. ## Preparación @@ -51,6 +51,10 @@ Al ser invocado, establece contexto primero: lee el archivo objetivo y sus vecin ### Java - Inyección por constructor (no `@Autowired` en campos). DTOs en la API, no entidades JPA. `Optional` en vez de null donde aplique; `final` por defecto. Maneja `checked exceptions` con intención. Evita N+1 (fetch joins/`@EntityGraph`). +### C# +- **Nullable reference types** habilitados (`enable`); el operador `!` no es una solución, es callar al compilador. `record`/`readonly struct` para datos inmutables; `sealed` por defecto. +- `async`/`await` en todo I/O: **nunca** `.Result`/`.Wait()` (deadlock), nada de `async void` salvo event handlers, propaga `CancellationToken` hasta el fondo y usa `ConfigureAwait(false)` en librerías. Excepciones específicas, jamás un `catch (Exception)` que se traga el error. Analyzers activos con `TreatWarningsAsErrors`. + ### Dart - Null-safety estricto; modelos inmutables (`freezed`) e igualdad por valor. `const` en todo lo posible. Un solo paradigma de estado (Riverpod o BLoC), no mezclar. `analysis_options.yaml` con lints fuertes. diff --git a/stacks/backend/dotnet/.claude/CLAUDE.md b/stacks/backend/dotnet/.claude/CLAUDE.md index aabec4c..4b70a2b 100644 --- a/stacks/backend/dotnet/.claude/CLAUDE.md +++ b/stacks/backend/dotnet/.claude/CLAUDE.md @@ -3,24 +3,42 @@ ## Estructura y DI - Minimal APIs o Controllers; capas con el **contenedor DI integrado** (constructor injection). Lifetimes correctos (`Scoped` para EF DbContext, no `Singleton`). - DTOs/`record` en la API, **nunca** entidades EF expuestas. Mapea con Mapster/AutoMapper o a mano. +- Con Minimal APIs, agrupa por dominio (`MapGroup`) y aplica filtros por grupo en vez de repetir validación endpoint por endpoint. +- **Nunca** captures un servicio `Scoped` desde uno `Singleton`: es el bug de lifetime clásico y explota tarde, en producción. ## Persistencia (EF Core) - Lecturas con `AsNoTracking()`. Evita **N+1**: `Include`/`ThenInclude` o proyecciones a DTO con `Select`. `await` en todo I/O. - Migraciones con `dotnet ef migrations`; transacciones explícitas para operaciones multi-tabla. Concurrencia optimista con `[Timestamp]`/rowversion. +- Proyecta a DTO **dentro** de la query (`Select` antes de materializar): traer la entidad entera para usar dos columnas es el desperdicio más común. + +## Async y cancelación +- `CancellationToken` en toda firma async, propagado hasta EF y `HttpClient`: sin él, el trabajo sigue corriendo después de que el cliente se fue. +- Nada de `.Result`/`.Wait()` (deadlock) ni `async void` fuera de event handlers. `IAsyncEnumerable` para streaming en vez de materializar listas grandes. ## API, validación, errores - Validación con **FluentValidation** o DataAnnotations en el boundary. Manejo global con middleware de excepciones → `ProblemDetails` (RFC 7807) consistente. -- Versionado de API y paginación. +- Versionado de API y paginación, con tope de tamaño de página en el servidor. + +## Resiliencia y caché +- `IHttpClientFactory` con **typed clients**: nunca `new HttpClient()` (agota sockets y no refresca DNS). Añade timeout + retry con backoff + circuit breaker (`Microsoft.Extensions.Http.Resilience` o Polly). +- `HybridCache`/`IMemoryCache` con TTL explícito; incluye en la clave todo lo que varíe la respuesta (usuario, tenant, cultura). ## Config y seguridad -- Config por `appsettings.{Environment}.json` + env vars + **user-secrets** en dev; secretos nunca versionados (usa Key Vault/secret manager en prod). **Options pattern** validado al boot. +- Config por `appsettings.{Environment}.json` + env vars + **user-secrets** en dev; secretos nunca versionados (usa Key Vault/secret manager en prod). **Options pattern** validado al boot (`ValidateOnStart`). - Auth con ASP.NET Core Identity/JWT; **authorization policies deny-by-default**. Logging estructurado con **Serilog** (sin secretos/PII). +## Observabilidad +- **OpenTelemetry** para trazas y métricas; health checks (`/health/live`, `/health/ready`) diferenciados y conectados al orquestador. + +## Calidad del proyecto +- `enable` y `true` en el `.csproj`; `.editorconfig` con analyzers. `dotnet format` en el hook de pre-commit. + ## Comandos - `dotnet run` · `dotnet build` · `dotnet test` · `dotnet format` - `dotnet ef migrations add ` · `dotnet ef database update` ## Tests - **xUnit** + Moq (unit); `WebApplicationFactory` (integración) + Testcontainers para DB real. +- Reemplaza dependencias externas en el `WebApplicationFactory` vía `ConfigureTestServices`, no con banderas de entorno dentro del código de producción. > Aplica también el playbook común de backend. diff --git a/stacks/backend/spring/.claude/CLAUDE.md b/stacks/backend/spring/.claude/CLAUDE.md index bb8e4dc..6aea12c 100644 --- a/stacks/backend/spring/.claude/CLAUDE.md +++ b/stacks/backend/spring/.claude/CLAUDE.md @@ -3,23 +3,38 @@ ## Estructura y DI - Capas: `Controller` → `Service` → `Repository` (Spring Data JPA). **Inyección por constructor** (nunca `@Autowired` en campos: rompe testabilidad e inmutabilidad). - DTOs en la API, **nunca** entidades JPA expuestas (evita fugas y problemas de serialización lazy). Mapea con MapStruct o a mano. +- DTOs como `record` (Java 17+): inmutables y con equals/hashCode correctos sin boilerplate. En Kotlin, `data class` con `val`. ## Persistencia (JPA) - Evita **N+1**: `JOIN FETCH`, `@EntityGraph` o proyecciones; cuidado con `FetchType.EAGER`. `@Transactional` con límites claros (en el service, no en el controller). +- `@Transactional(readOnly = true)` en lecturas: habilita optimizaciones del proveedor y documenta la intención. - Migraciones con **Flyway**/Liquibase (nunca `ddl-auto=update` en prod). Locking optimista con `@Version`. +- Cuidado con el proxy de `@Transactional`: una llamada interna al mismo bean **no** pasa por el proxy y se queda sin transacción. ## API, validación, errores - Bean Validation (`@Valid`, `@NotNull`...) en los DTOs de entrada. Manejo global con `@RestControllerAdvice` → envelope de error consistente. -- Versionado y paginación (`Pageable`). +- Versionado y paginación (`Pageable`). Limita el tamaño de página en el servidor: un `size` sin tope es un DoS gratis. + +## Concurrencia +- Java 21+: **virtual threads** (`spring.threads.virtual.enabled=true`) para I/O bloqueante; no los uses con código `synchronized` de bloqueo largo (pinning). +- `@Async` con un executor **explícito** (nunca el default sin acotar) y `CompletableFuture` para composición. Propaga el contexto de seguridad si lo necesitas. + +## Resiliencia y caché +- **Resilience4j** para llamadas a terceros: timeout + retry con backoff + circuit breaker. Un cliente HTTP sin timeout acaba tumbando el pool. +- `@Cacheable` con clave explícita y TTL; invalida en la escritura (`@CacheEvict`). Nunca cachees respuestas que dependan del usuario sin incluirlo en la clave. ## Config y seguridad - `application-{env}.yml` por perfil; secretos vía env vars / config server, **nunca** en el yml versionado. Fail-fast con `@ConfigurationProperties` validadas. - **Spring Security** deny-by-default; passwords con BCrypt; authz por método/endpoint. Actuator con endpoints sensibles protegidos. +## Observabilidad +- Actuator + **Micrometer** para métricas; `/health` y `/health/readiness` diferenciados. Logging estructurado (JSON) con `traceId` correlacionado, sin secretos ni PII. + ## Comandos - Maven: `mvn spring-boot:run` · `mvn verify` — Gradle: `./gradlew bootRun` · `./gradlew test` ## Tests - JUnit 5 + Mockito (unit), `@SpringBootTest` + **Testcontainers** (integración con DB real). `@DataJpaTest` para repos. +- Prefiere **slices** (`@WebMvcTest`, `@DataJpaTest`) al `@SpringBootTest` completo: arrancar todo el contexto en cada test es lo que vuelve lenta la suite. > Aplica también el playbook común de backend. diff --git a/stacks/frontend/angular/.claude/CLAUDE.md b/stacks/frontend/angular/.claude/CLAUDE.md index 20307c8..5af56b9 100644 --- a/stacks/frontend/angular/.claude/CLAUDE.md +++ b/stacks/frontend/angular/.claude/CLAUDE.md @@ -9,6 +9,22 @@ - **Standalone components** (sin NgModules nuevos). `ChangeDetectionStrategy.OnPush` por defecto. - `@if`/`@for`/`@switch` (control flow nuevo) con `track` en los `@for` (clave estable, no índice). - `inject()` sobre constructor cuando aporte; `DestroyRef`/`takeUntilDestroyed` para limpiar suscripciones. +- `@defer` para diferir lo que no es visible de entrada (bloques pesados, below the fold), con `@placeholder`/`@loading`. +- `effect()` es para sincronizar con el mundo exterior, **no** para derivar estado: si calculas un valor a partir de otros, eso es `computed`. + +## Estado con signals +- `computed` para todo lo derivado; nada de duplicar estado que puedas calcular. +- `linkedSignal` cuando un estado local deba reinicializarse al cambiar su origen (p. ej. la selección al cambiar de lista). +- Las APIs `resource`/`rxResource`/`httpResource` cubren carga asíncrona declarativa con estado y recarga; confirma su nivel de estabilidad contra la versión exacta de tu proyecto antes de apoyarte en ellas. + +## Rutas +- Lazy loading con `loadComponent`/`loadChildren` por ruta: el bundle inicial solo debe traer la primera pantalla. +- Guards y resolvers **funcionales** (`CanActivateFn`, `ResolveFn`) con `inject()`; los basados en clase son legado. +- `withComponentInputBinding()` para recibir parámetros de ruta como inputs en vez de leer `ActivatedRoute` a mano. + +## HTTP y formularios +- Interceptores **funcionales** (`HttpInterceptorFn`) para auth, correlación y reintentos; el manejo de errores va en el stream, no en cada componente. +- Formularios **reactivos y tipados**; template-driven solo para casos triviales. Valida en el `FormGroup`, no en el template. ## RxJS con criterio - Usa signals para estado; RxJS para flujos de eventos/async complejos. No subscribes manuales sin limpieza. @@ -18,9 +34,14 @@ - Todo texto visible vía `| translate` (ngx-translate); nunca hardcodeado. - Nada de acceso directo a `localStorage`: usa un `StorageService` inyectable. - Tailwind para estilos; evita CSS suelto duplicado. Sanitiza con `DomSanitizer` si inyectas HTML. +- Accesibilidad como requisito: HTML semántico, foco visible y navegable por teclado, `aria-*` solo cuando el elemento nativo no alcanza. +- `NgOptimizedImage` (`ngSrc`) para imágenes: dimensiona, prioriza el LCP y evita layout shift. ## Comandos - `ng serve` · `ng build` · `ng test` · `ng lint` ## Tests - Testing Library Angular o TestBed con `OnPush`. Mockea servicios HTTP (`HttpTestingController`). Prueba comportamiento visible, no internals. + +## Presupuesto de bundle +- Mantén los `budgets` del `angular.json` y trátalos como error en CI: sin tope, el bundle solo crece. diff --git a/test/install-detect.test.cjs b/test/install-detect.test.cjs new file mode 100644 index 0000000..01f21bd --- /dev/null +++ b/test/install-detect.test.cjs @@ -0,0 +1,79 @@ +// Tests de detección de stack. Antes, el pom.xml tenía que contener literalmente "spring-boot", +// así que un proyecto que solo declara `org.springframework` (BOM, spring-framework sin Boot) no +// se detectaba; y los `settings.gradle`/`settings.gradle.kts` no se miraban en absoluto. +// Los casos de `build.gradle`/`build.gradle.kts` sí funcionaban ya: aquí quedan como guardas de +// regresión, no como casos nuevos. +// Además, un proyecto Java sin Spring no daba ninguna pista de por qué no se detectaba nada. +// Correr: `npm test` o `node --test`. +const { test } = require("node:test"); +const assert = require("node:assert"); +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const INSTALL = path.join(__dirname, "..", "install.js"); + +// Crea un proyecto temporal con los archivos dados y corre el instalador en dry-run. +function detectar(archivos) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kit-detect-")); + for (const [nombre, contenido] of Object.entries(archivos)) { + fs.writeFileSync(path.join(dir, nombre), contenido); + } + const r = spawnSync("node", [INSTALL, "--yes", "--no-flow", "--no-external", "--dry-run"], { + cwd: dir, + input: "", + encoding: "utf8", + }); + fs.rmSync(dir, { recursive: true, force: true }); + const m = (r.stdout || "").match(/Stack detectado:\s*(\S+)/); + return { stack: m ? m[1] : null, salida: r.stdout || "", status: r.status }; +} + +test("detecta Spring por el starter en pom.xml", () => { + assert.equal(detectar({ "pom.xml": "spring-boot-starter-web" }).stack, "backend/spring"); +}); + +// NUEVO: el pom.xml sin la cadena "spring-boot" no se detectaba. +test("detecta Spring por el groupId org.springframework en pom.xml", () => { + assert.equal(detectar({ "pom.xml": "org.springframework" }).stack, "backend/spring"); +}); + +// NUEVO: los settings.gradle no se miraban. +test("detecta Spring declarado en settings.gradle.kts", () => { + assert.equal(detectar({ "settings.gradle.kts": 'id("org.springframework.boot")' }).stack, "backend/spring"); +}); + +// Guardas de regresión: estos ya funcionaban antes del cambio. +test("detecta Spring en Gradle con Kotlin DSL", () => { + assert.equal(detectar({ "build.gradle.kts": 'plugins { id("org.springframework.boot") version "3.3.0" }' }).stack, "backend/spring"); +}); + +test("detecta Spring por el plugin de dependency-management", () => { + assert.equal(detectar({ "build.gradle": 'apply plugin: "io.spring.dependency-management"' }).stack, "backend/spring"); +}); + +test("un proyecto Java SIN Spring no se marca como Spring y explica por qué", () => { + const r = detectar({ "pom.xml": "quarkus-resteasy" }); + assert.equal(r.stack, null, "no debe asumir Spring: el overlay no encajaría"); + assert.match(r.salida, /proyecto Java\/Kotlin/, "debe decir que vio un proyecto Java"); + assert.match(r.salida, /backend\/spring/, "debe sugerir el flag si aplica"); + assert.equal(r.status, 1, "sin stack no debe salir en verde"); +}); + +test("detecta C# por el .csproj", () => { + assert.equal(detectar({ "Api.csproj": "" }).stack, "backend/dotnet"); +}); + +test("detecta Angular por angular.json", () => { + assert.equal(detectar({ "angular.json": "{}" }).stack, "frontend/angular"); +}); + +test("detecta Angular por la dependencia @angular/core", () => { + assert.equal(detectar({ "package.json": '{"dependencies":{"@angular/core":"^20.0.0"}}' }).stack, "frontend/angular"); +}); + +test("la pista distingue otros ecosistemas sin overlay", () => { + assert.match(detectar({ "go.mod": "module x" }).salida, /Go/); + assert.match(detectar({ "Cargo.toml": "[package]" }).salida, /Rust/); +});