diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4b1fa9..832b810 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ["main"] + branches: ["main", "v2-rebrand"] pull_request: jobs: @@ -35,7 +35,7 @@ jobs: # Core checks on all matrices (since it's cheap) - name: Install CLI dependencies - run: npm ci + run: npm install - name: Run Biome Lint run: npm run lint @@ -49,17 +49,30 @@ jobs: - name: Build CLI run: npm run build - - name: Test Project Generation (${{ matrix.pm }}) + - name: Test Project Generation — stack next (${{ matrix.pm }}) run: | mkdir -p /tmp/test-gen cd /tmp/test-gen - node $GITHUB_WORKSPACE/dist/index.js test-app --pm ${{ matrix.pm }} --no-git --no-openspec + node $GITHUB_WORKSPACE/dist/index.js next test-app --pm ${{ matrix.pm }} --no-git --no-openspec cd test-app - # Verificar que instaló dependencias (node_modules existe) if [ ! -d "node_modules" ]; then echo "❌ node_modules no fue creado por ${{ matrix.pm }}" exit 1 fi - echo "✅ Generación e instalación exitosa con ${{ matrix.pm }}" + echo "✅ Generación e instalación exitosa con ${{ matrix.pm }} (stack next)" + + - name: Test Project Generation — stack api (${{ matrix.pm }}) + run: | + mkdir -p /tmp/test-gen-api + cd /tmp/test-gen-api + node $GITHUB_WORKSPACE/dist/index.js api test-api --pm ${{ matrix.pm }} --no-git --no-openspec + cd test-api + + if [ ! -d "node_modules" ]; then + echo "❌ node_modules no fue creado por ${{ matrix.pm }}" + exit 1 + fi + + echo "✅ Generación e instalación exitosa con ${{ matrix.pm }} (stack api)" diff --git a/.knowledge/BRIEF.md b/.knowledge/BRIEF.md new file mode 100644 index 0000000..e046bd7 --- /dev/null +++ b/.knowledge/BRIEF.md @@ -0,0 +1,26 @@ +# BRIEF: @gonzoblasco/create-stack + +## Qué es + +Scaffolder opinado multi-stack. Un solo comando y tenés un proyecto moderno, testeado, lintado y listo para que un AI agent lo habite. + +## Por qué existe + +Gonzo arranca proyectos constantemente y se cansó de repetir las mismas 40 decisiones. `create-stack-next` demostró el concepto pero quedó atado a Next.js. Esta es la evolución: un CLI que permite elegir el stack como argumento posicional, con arquitectura modular para agregar stacks sin tocar el core. + +## Stack inicial + +- **next:** Next.js 15 + React 19 + TypeScript estricto + Biome + Vitest + Playwright + Zod + OpenSpec + AI agent config +- **api:** Next.js 15 App Router (API-only) + Drizzle ORM + SQLite + middleware Bearer + tests + +## Stack futuros (en consideración) + +- tanstack, astro, cli + +## Diferenciador + +No es solo un scaffolder de código. Es un scaffolder de **entorno de desarrollo**: tests, lint, CI, AI agent config, OpenSpec, todo desde el día 1. El proyecto generado está listo para que un agente IA lo habite productivamente. + +## Público + +Gonzo (uso primario), y cualquier dev que quiera arrancar proyectos rápido con buena base técnica y AI-ready. diff --git a/.knowledge/STATUS.md b/.knowledge/STATUS.md new file mode 100644 index 0000000..d62f536 --- /dev/null +++ b/.knowledge/STATUS.md @@ -0,0 +1,28 @@ +# STATUS: @gonzoblasco/create-stack + +**Fecha:** 2026-07-20 +**Fase:** Reidentificación (v2-rebrand) +**Última versión publicada:** 0.7.2 (como create-stack-next) + +## Estado actual + +En plena reidentificación de `create-stack-next` → `@gonzoblasco/create-stack`. Se está refactorizando la arquitectura a stacks modulares, renombrando el paquete, y documentando con ADRs + OpenSpec specs. + +## Lo que está pasando ahora + +- [x] ADRs de decisiones arquitectónicas (3/3) +- [x] OpenSpec specs del producto (7 specs) +- [x] BRIEF definido +- [ ] Refactor de `src/` a arquitectura modular +- [ ] Migración de templates a `src/stacks/` +- [ ] Renombre de package.json y bin +- [ ] Tests actualizados +- [ ] Documentación actualizada + +## Próximos pasos inmediatos + +1. Refactorizar `src/` (parse-args, cli, index) +2. Mover templates a `src/stacks/next/template/` y `src/stacks/api/template/` +3. Renombrar package.json +4. Actualizar tests +5. Verificar build + lint + typecheck + tests diff --git a/.knowledge/adr/0001-use-openspec-for-product-definition.md b/.knowledge/adr/0001-use-openspec-for-product-definition.md new file mode 100644 index 0000000..e843006 --- /dev/null +++ b/.knowledge/adr/0001-use-openspec-for-product-definition.md @@ -0,0 +1,10 @@ +# ADR 0001: Usar OpenSpec para definición de producto + +- **Fecha:** 2026-07-20 +- **Contexto:** Reidentificación de `create-stack-next` → `@gonzoblasco/create-stack`. Necesitamos documentar decisiones de producto y especificaciones de manera estructurada y ejecutable por IA. +- **Decisión:** Usar OpenSpec como formato de especificación. Las specs viven en `.knowledge/specs/` y se sincronizan con el workspace git (no con el repo del proyecto). Los ADRs documentan decisiones arquitectónicas. +- **Consecuencias:** + - Las features se definen como OpenSpec specs antes de implementar + - Los ADRs capturan el "por qué" de cada decisión + - El workflow es: spec → implementación → spec actualizada +- **Estado:** Aceptado diff --git a/.knowledge/adr/0002-rename-to-create-stack-with-positional-stacks.md b/.knowledge/adr/0002-rename-to-create-stack-with-positional-stacks.md new file mode 100644 index 0000000..76c061f --- /dev/null +++ b/.knowledge/adr/0002-rename-to-create-stack-with-positional-stacks.md @@ -0,0 +1,16 @@ +# ADR 0002: Renombrar a `@gonzoblasco/create-stack` con stacks posicionales + +- **Fecha:** 2026-07-20 +- **Contexto:** `create-stack-next` está atado semánticamente a Next.js, pero ya tiene `--template api` y se planean más stacks (TanStack, Astro). El nombre actual es engañoso y no escala. +- **Decisión:** + 1. **Nombre npm:** `@gonzoblasco/create-stack` (scoped package público) + 2. **Nombre repo:** `create-stack` (se renombrará en GitHub al estabilizar) + 3. **CLI:** `npx @gonzoblasco/create-stack [nombre]` donde `` es el primer argumento posicional + 4. **Stacks iniciales:** `next` (full app), `api` (Next.js API-only) + 5. **Stacks futuros:** `tanstack`, `astro`, `cli` +- **Consecuencias:** + - Breaking change en la interfaz CLI (de `npx create-stack-next nombre` a `npx @gonzoblasco/create-stack next nombre`) + - El comando es más verboso pero más explícito y extensible + - El flag `--template` se depreca; el stack se pasa como argumento + - Se mantiene compatibilidad hacia atrás con `--template` por un tiempo +- **Estado:** Aceptado diff --git a/.knowledge/adr/0003-modular-stack-architecture.md b/.knowledge/adr/0003-modular-stack-architecture.md new file mode 100644 index 0000000..9fbd32f --- /dev/null +++ b/.knowledge/adr/0003-modular-stack-architecture.md @@ -0,0 +1,14 @@ +# ADR 0003: Arquitectura modular de stacks + +- **Fecha:** 2026-07-20 +- **Contexto:** Necesitamos que agregar un nuevo stack sea trivial: crear una carpeta con template + config, sin tocar el core del CLI. +- **Decisión:** Cada stack es un módulo independiente en `src/stacks//` con: + - `index.ts` — configuración del stack (nombre, descripción, template dir, package.json base, prompts) + - `template/` — archivos del template (copiados al proyecto destino) + - El core del CLI descubre stacks por convención de directorios +- **Consecuencias:** + - Agregar un stack = crear `src/stacks//` con su `index.ts` y `template/` + - El core no necesita saber qué stacks existen + - Los tests de cada stack pueden ser independientes + - Los templates actuales (`template/` y `template-api/`) se migran a `src/stacks/next/template/` y `src/stacks/api/template/` +- **Estado:** Aceptado diff --git a/.knowledge/specs/product.md b/.knowledge/specs/product.md new file mode 100644 index 0000000..b3d3312 --- /dev/null +++ b/.knowledge/specs/product.md @@ -0,0 +1,119 @@ +# OpenSpec: @gonzoblasco/create-stack + +## Summary + +Scaffolder opinado multi-stack. Un solo comando y tenés un proyecto moderno, testeado, lintado y listo para que un AI agent lo habite. El stack se elige como primer argumento posicional. + +## Specs + +### spec-001: CLI entrypoint + +- **ID:** spec-001 +- **Title:** CLI entrypoint con argumento posicional de stack +- **Status:** proposed +- **Priority:** P0 + +**Description:** +El CLI se ejecuta con `npx @gonzoblasco/create-stack [nombre]`. El stack es obligatorio. El nombre del proyecto es opcional (si no se pasa, prompt interactivo). + +**Acceptance Criteria:** +- [ ] `npx @gonzoblasco/create-stack next my-app` genera un proyecto Next.js full en `./my-app` +- [ ] `npx @gonzoblasco/create-stack api my-app` genera un proyecto Next.js API-only en `./my-app` +- [ ] `npx @gonzoblasco/create-stack` sin argumentos muestra help con stacks disponibles +- [ ] `npx @gonzoblasco/create-stack --help` muestra help +- [ ] `npx @gonzoblasco/create-stack inexistente` muestra error con stacks válidos +- [ ] El flag `--template` legacy sigue funcionando (compatibilidad) + +### spec-002: Stacks modulares + +- **ID:** spec-002 +- **Title:** Stacks como módulos independientes +- **Status:** proposed +- **Priority:** P0 + +**Description:** +Cada stack vive en `src/stacks//` con su propia configuración y template. El core descubre stacks automáticamente. + +**Acceptance Criteria:** +- [ ] `src/stacks/next/index.ts` exporta config del stack Next.js full +- [ ] `src/stacks/api/index.ts` exporta config del stack Next.js API-only +- [ ] El core itera `src/stacks/` para listar stacks disponibles +- [ ] Agregar un stack nuevo no requiere modificar el core + +### spec-003: Template copiado con personalización + +- **ID:** spec-003 +- **Title:** Template copiado con personalización por stack +- **Status:** proposed +- **Priority:** P0 + +**Description:** +Cada stack tiene su carpeta `template/` que se copia al directorio destino. Durante la copia se personaliza el `package.json` (name, version) y se ejecutan hooks post-copia. + +**Acceptance Criteria:** +- [ ] `src/stacks/next/template/` contiene el template Next.js full +- [ ] `src/stacks/api/template/` contiene el template Next.js API-only +- [ ] El `package.json` del proyecto destino tiene el nombre correcto +- [ ] Los hooks post-copia se ejecutan (OpenSpec init, etc.) + +### spec-004: OpenSpec integrado + +- **ID:** spec-004 +- **Title:** OpenSpec init post-scaffold +- **Status:** proposed +- **Priority:** P1 + +**Description:** +Después de copiar el template e instalar deps, se ejecuta `openspec init` con las herramientas IA seleccionadas por el usuario. + +**Acceptance Criteria:** +- [ ] Select interactivo de herramientas IA (Claude Code, Cursor, etc.) +- [ ] `openspec init` se ejecuta con las herramientas seleccionadas +- [ ] Flag `--no-openspec` para saltear +- [ ] Fallback si `openspec init` falla (estructura base ya copiada del template) + +### spec-005: Git init inteligente + +- **ID:** spec-005 +- **Title:** Git init con detección de workspace +- **Status:** proposed +- **Priority:** P1 + +**Description:** +Se inicializa git automáticamente a menos que se detecte un workspace (Turborepo, pnpm workspace) o se pase `--no-git`. + +**Acceptance Criteria:** +- [ ] `git init -b main` + commit inicial +- [ ] Detección de workspace salta git init +- [ ] `--no-git` salta git init +- [ ] Si falta user.name/user.email, usa valores temporales y advierte + +### spec-006: Package manager selection + +- **ID:** spec-006 +- **Title:** Selección de package manager +- **Status:** proposed +- **Priority:** P1 + +**Description:** +El usuario puede elegir el package manager con `--pm`. Default: npm. + +**Acceptance Criteria:** +- [ ] `--pm npm|pnpm|yarn|bun` funciona +- [ ] Default: npm +- [ ] `npm install` se ejecuta post-copia + +### spec-007: Help y discoverability + +- **ID:** spec-007 +- **Title:** Help con stacks disponibles +- **Status:** proposed +- **Priority:** P1 + +**Description:** +El comando `--help` muestra todos los stacks disponibles con su descripción, y ejemplos de uso. + +**Acceptance Criteria:** +- [ ] `--help` lista stacks con nombre y descripción +- [ ] `--help` muestra ejemplos de uso +- [ ] Los stacks se listan dinámicamente (no hardcodeados) diff --git a/AGENT_TASKS.md b/AGENT_TASKS.md index a264caf..8e29564 100644 --- a/AGENT_TASKS.md +++ b/AGENT_TASKS.md @@ -9,8 +9,9 @@ Antes de escribir una sola línea de código, el agente DEBE: 2. Si la tarea implica una decisión arquitectónica nueva o cambio de diseño, documentarla como ADR en `docs/decisions.md`. 3. Planificar la implementación paso a paso. 4. Ejecutar cambios atómicos. -5. Correr linting (`npm run lint`), typecheck (`npm run typecheck`) y tests (`npm run test:run`). -6. Actualizar `CHANGELOG.md` en la sección `[Unreleased]` y, si corresponde, `ROADMAP.md`. +5. Correr `npm run lint:fix` (lint + format automático), `npm run typecheck` y `npm run test:run`. +6. **Solo si todo pasa**, hacer commit. Si algo falla, arreglarlo antes del commit — no hay commits de fixes. +7. Actualizar `CHANGELOG.md` en la sección `[Unreleased]` y, si corresponde, `ROADMAP.md`. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index b52f8b6..17d34b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,200 +1,52 @@ # Changelog -Todas las versiones notables de este proyecto se documentan en este archivo. +## [Unreleased] — v0.8.0 (Reidentificación) -El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +### 🚀 Cambios mayores ---- +- **Renombre:** `create-stack-next` → `@gonzoblasco/create-stack` +- **Nuevo CLI:** `npx @gonzoblasco/create-stack [nombre]` +- **Arquitectura modular:** cada stack es un módulo independiente en `src/stacks//` +- **Compatibilidad hacia atrás:** `npx create-stack-next my-app` y `--template api` siguen funcionando -## [Unreleased] +### ✨ Nuevas features -### Changed +- Stack posicional como primer argumento (`next`, `api`) +- Help dinámico que lista stacks disponibles +- Stacks `next` (full app) y `api` (API-only) como módulos -- **Templates actualizados para compatibilidad con Biome 2.5+ y TypeScript 5.x/7.x:** - - Schema de Biome actualizado de 1.9.0 a 2.5.4 - - `organizeImports.enabled` → `assist.actions.source.organizeImports.level: "on"` - - `linter.rules.recommended: true` → `linter.rules.preset: "recommended"` - - `files.ignore` → `files.includes` (formato Biome 2.5+) - - Agregado `css.parser.tailwindDirectives: true` para Tailwind v4 - - Bump de `@biomejs/biome` de `^1.9.0` a `^2.5.0` - - Bump de `vitest` de `^2.1.0` a `^4.0.0` - - Agregado `src/global.d.ts` con declaraciones de módulos para CSS/imágenes - - `@types/react` y `@types/react-dom` actualizados a `^19.0.0` en template-api -- **Proyecto CSN actualizado a Biome 2.5+ y TypeScript 7+:** - - Schema de Biome actualizado - - `@biomejs/biome` bump a `^2.5.0`, `typescript` a `^7.0.0`, `vitest` a `^4.0.0` - - Agregado `"types": ["node"]` en tsconfig para compatibilidad con TS 7 +### 🔧 Internals -### Fixed - -- Error de schema en biome.json de templates (schema URL desactualizada) -- Error de `files.ignore` deprecado en Biome 2.5+ -- Error de `organizeImports.enabled` → `level: "on"` en Biome 2.5.4 -- Error de módulo CSS no encontrado en TypeScript (agregado `global.d.ts`) -- Error de `baseUrl` deprecado en TS 6+ (agregado `ignoreDeprecations: "6.0"` en templates) - ---- - -## [0.7.1] - 2026-07-14 - -### Added - -- Opción "Todas las herramientas (30+)" en el select de IA: si el usuario usa herramientas menos comunes (Antigravity, OpenClaw, Gemini, etc.) puede seleccionar "Todas" y se pasa `--tools all` a `openspec init`. - -### Changed - -- Removido "Primera utilidad publicada. M4 Lite en progreso." del README. -- ROADMAP reescrito: lista de logros actuales en vez de fases rígidas. - -## [0.7.0] - 2026-07-14 - -### Added - -- **Spec-Driven Development integrado con OpenSpec**: todos los proyectos generados ahora incluyen [OpenSpec](https://github.com/Fission-AI/OpenSpec) pre-configurado. El scaffolder pregunta qué herramientas de IA usás (Claude Code, Cursor, Windsurf, GitHub Copilot, Cline, Codex) e instala los skills y slash commands correspondientes. - - Flujo `/opsx:propose` → `/opsx:apply` → `/opsx:archive` listo para usar desde el día 1. - - Estructura `openspec/` pre-armada en el template como fallback si `openspec init` falla (sin internet, npm caído, etc.). - - `@fission-ai/openspec` agregado como `devDependency` en los proyectos generados. - - AGENTS.md actualizado con sección SDD: specs como fuente de verdad, delta specs, flujo explore→propose→apply→archive. - - README de los proyectos generados incluye sección "Desarrollo por Especificaciones". - - Nuevo flag `--no-openspec` para saltear la inicialización de OpenSpec. - - Próximos pasos en el CLI ahora incluye `/opsx:propose "tu primera feature"`. - -### Changed +- `src/stacks/` con registry, types y módulos por stack +- Templates movidos a `src/stacks/*/template/` +- `parse-args.ts` refactorizado para nueva API +- `cli.ts` refactorizado para usar `getStack()` y `listStacks()` +- ADRs documentados en `.knowledge/adr/` +- OpenSpec specs en `.knowledge/specs/` -- OpenSpec init se ejecuta **antes** de `git init` para que los skills y comandos queden en el commit inicial. -- README principal del repo actualizado con tabla comparativa (Spec-Driven Development: Ninguno vs OpenSpec integrado). +### 📚 Documentación -## [0.6.2] - 2026-07-14 +- README actualizado con nueva API y arquitectura +- ROADMAP actualizado +- BRIEF, STATUS, ADRs y specs agregados -### Fixed - -- `copy-template.ts`: agregado `dotfiles: true` al `cp()` para que el scaffolding copie archivos ocultos del template (`.env.example`, `.gitignore`, etc.). Sin este flag, los dotfiles del template se ignoraban silenciosamente al generar un proyecto nuevo. - -## [0.6.1] - 2026-07-01 - -### Added - -- Publicación en npm con soporte oficial del paquete para el template API (`files`: `dist`, `template`, `template-api`). -- Documentación del flag `--template api` en README. +## [0.7.2] — 2026-07-15 ### Fixed +- Templates actualizados para Biome 2.5.4, Vitest 4, TS 5.x compat +- Agregado `src/global.d.ts` con declaraciones de módulos +- `@types/react` y `@types/react-dom` actualizados a ^19.0.0 en template-api -- Fix del infame error `ERESOLVE` de `npm install` al fijar la dependencia de React de forma armónica para Next.js 15 en los proyectos generados. - -## [0.6.0] - 2026-07-01 +## [0.7.1] — 2026-07-14 ### Added +- Opción "Todas las herramientas (30+)" en el select de IA +- Limpieza de docs -- **Template API (`--template api`)**: arquitectura robusta de backend puro usando Next.js App Router. - - **Base de Datos**: integración de Drizzle ORM + `@libsql/client` (SQLite) con tabla de usuarios y `drizzle.config.ts`. - - **Manejo de Errores y Respuestas**: utilidades estandarizadas (`src/lib/errors.ts` y `src/lib/api-response.ts`) para respuestas de API consistentes. - - **Middleware**: interceptor de autenticación base vía Bearer Token en `src/middleware.ts`. - - **Testing**: integración con `node-mocks-http` y Vitest para pruebas de Route Handlers ultra veloces sin levantar servidores. -- **AI Workflow Injection**: todos los proyectos generados (`template/` y `template-api/`) ahora incluyen de fábrica archivos semilla para orquestar agentes de IA (`ROADMAP.md`, `AGENT_TASKS.md`, `HANDOFF.md`). -- **Prompt Kickoff**: prompt pre-configurado en `AGENTS.md` del template para que el usuario inicie de inmediato la Fase 1 del proyecto usando cualquier LLM (Cursor, OpenClaw, etc.). -- Decisiones técnicas fundacionales (ADR D004 y D005) inyectadas en los templates generados para prevenir amnesia contextual en la IA. -- **Soporte de Workspaces**: detección automática si el CLI se ejecuta dentro de un monorepo (Turborepo, pnpm workspaces, etc.), desactivando `git init` para evitar anidamiento accidental. -- **Pulido Visual del CLI**: `@clack/prompts` con spinners en tiempo real, colores semánticos, manejo limpio de cancelaciones (Ctrl+C) y bloque estilizado de "Próximos pasos". -- **CI/CD Pipeline**: flujo de trabajo de GitHub Actions nativo en el repositorio central para validar Linting, Tipos y tests de Playwright. - -### Changed - -- Tests de Vitest re-arquitectados para discriminar el directorio `src/` del directorio de plantillas `template/`, evitando colisiones fantasma. - -### Fixed - -- Se añadió `vitest.config.ts` en la raíz para incluir estrictamente `src/` e impedir que Vitest corra tests anidados dentro de `template/` y carpetas de prueba locales, arreglando conflictos con Playwright. - -## [0.5.0] - 2026-06-30 +## [0.7.0] — 2026-07-13 ### Added - -- `AGENT_TASKS.md` — guía de proceso para agentes de IA (Fase 1 del roadmap). -- `src/test/git.test.ts` — tests dedicados para funciones de Git (`isGitAvailable`, `getGitConfig`, `execCapture`). -- Tests exhaustivos en `cli.test.ts`: 27 tests (antes 13) cubriendo `validateProjectName` directo, edge cases de FS, combinaciones de flags. -- Tests exhaustivos en `copy-template.test.ts`: 11 tests (antes 2) cubriendo estructura completa de archivos, dotfiles, paths anidados, archivos sin placeholders. -- ADRs D002, D003, D004 en `docs/decisions.md`. - -### Changed - -- `cli.ts` refactorizado: funciones `validateProjectName`, `execInDir`, `execCapture`, `isGitAvailable`, `getGitConfig` ahora son exportadas para testabilidad directa. -- `validateProjectName` ahora acepta directorios existentes vacíos (antes rechazaba cualquier directorio existente). -- `validateProjectName` valida permisos de escritura en el directorio padre. -- `runGitInit` ahora detecta si Git está instalado y muestra warning amigable si no. -- `runGitInit` usa valores genéricos para el commit inicial cuando `user.name`/`user.email` no están configurados. -- Output de "Próximos pasos" ahora muestra el package manager seleccionado (antes siempre mostraba `npm`). - -## [0.3.1] - 2026-06-29 - -### Added - -- `CHANGELOG.md` base dentro de la carpeta `template/` para que los nuevos proyectos lo incluyan por defecto. -- Instrucción en `template/AGENTS.md` obligando a los agentes de IA a mantener actualizado el `CHANGELOG.md` de los proyectos generados. - -## [0.3.0] - 2026-06-28 - -### Changed - -- README completamente reescrito y pulido (M4 Lite). -- Mejor estructura, badges, tabla de diferencias con create-next-app. -- Sección de casos de uso y AI agents más visibles. -- Roadmap actualizado (M3 pausado + M4 Lite). - -### Added - -- `FUTURE.md` con proyectos futuros. - -## [0.2.0] - 2026-06-28 - -### Added - -- Soporte completo para AI agents (M2): - - Comando `npm run agent`. - - Carpeta `.openclaw/` con config y prompts (feature, refactor, bugfix, tests). - - Carpeta `.agents/` con config genérica y prompts compartidos. - - Carpeta `docs/` con `architecture.md`, `decisions.md` y `contributing.md`. -- Tests del scaffolder: - - `cli.test.ts` (parseo de flags y validación de nombres). - - `copy-template.test.ts` (reemplazo de placeholders). - - `integration.test.ts` (generación completa + checks de calidad). -- `AGENTS.md` actualizado con instrucciones para agentes. -- Formateo completo del template con Biome. - -### Changed - -- `README.md` actualizado para reflejar M2 cerrado. -- Justificación de `.openclaw/` y `.agents/` actualizada como "buena práctica". - -### Fixed - -- Varios archivos del template ahora pasan `biome check` correctamente. - ---- - -## [0.1.0] - 2026-06-28 - -### Added - -- Primera versión pública del scaffolder. -- Template de Next.js 15 con: - - TypeScript estricto. - - Biome. - - Vitest + Playwright. - - Zod. - - GitHub Actions (CI + e2e). - - `AGENTS.md`. -- Publicación en npm. -- Repo público en GitHub. - -[Unreleased]: https://github.com/gonzoblasco/create-stack-next/compare/v0.7.1...HEAD -[0.7.1]: https://github.com/gonzoblasco/create-stack-next/compare/v0.7.0...v0.7.1 -[0.7.0]: https://github.com/gonzoblasco/create-stack-next/compare/v0.6.2...v0.7.0 -[0.6.2]: https://github.com/gonzoblasco/create-stack-next/compare/v0.6.1...v0.6.2 -[0.6.1]: https://github.com/gonzoblasco/create-stack-next/compare/v0.6.0...v0.6.1 -[0.6.0]: https://github.com/gonzoblasco/create-stack-next/compare/v0.5.0...v0.6.0 -[0.5.0]: https://github.com/gonzoblasco/create-stack-next/compare/v0.3.1...v0.5.0 -[0.3.1]: https://github.com/gonzoblasco/create-stack-next/compare/v0.3.0...v0.3.1 -[0.3.0]: https://github.com/gonzoblasco/create-stack-next/compare/v0.2.0...v0.3.0 -[0.2.0]: https://github.com/gonzoblasco/create-stack-next/compare/v0.1.0...v0.2.0 -[0.1.0]: https://github.com/gonzoblasco/create-stack-next/releases/tag/v0.1.0 +- Spec-Driven Development integrado con OpenSpec +- Select interactivo para elegir herramientas IA +- Flag `--no-openspec` +- Próximos pasos incluye `/opsx:propose` diff --git a/FUTURE.md b/FUTURE.md index 2ecdbb0..fa7e7e2 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -1,26 +1,31 @@ # Proyectos futuros -Este documento lista ideas y posibles expansiones del ecosistema `create-stack-*` que **no están en el alcance actual**. +Este documento lista ideas y posibles expansiones del ecosistema `@gonzoblasco/create-stack` que **no están en el alcance actual**. --- -## Nuevos templates +## Nuevos stacks -- `--template cli` (scaffolder de CLIs en Node.js) -- `--template tanstack` (Next.js + TanStack Query + TanStack Start) +- `tanstack` — Next.js + TanStack Query + TanStack Start +- `astro` — Astro + Content Collections + integraciones +- `cli` — Scaffolder de CLIs en Node.js +- `remix` — Remix Run +- `sveltekit` — SvelteKit ## Expansión del ecosistema -- Familia de scaffolders: `create-stack-remix`, `create-stack-astro`, `create-stack-sveltekit`. - Sub-agentes de OpenClaw que ejecuten tareas de scaffolding o migración automáticamente. +- Plugins de IDE/extensiones para generar proyectos desde UI. +- Integración con registries de componentes (shadcn/ui, etc.). ## Otras ideas -- Plugins de IDE/extensiones para generar proyectos desde UI. -- Integración con registries de componentes (shadcn/ui, etc.). +- `create-stack` como CLI global instalable +- Modo interactivo sin argumentos (seleccionar stack + nombre con prompts) +- Template versionado (elegir versión de Next.js, etc.) --- -**Última actualización:** 2026-07-01 +**Última actualización:** 2026-07-20 Este documento es vivo. Se actualiza cuando surge una idea que no entra en el roadmap inmediato. diff --git a/HANDOFF.md b/HANDOFF.md index f3b79e0..f3dfcd7 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,98 +1,59 @@ -# Handover — create-stack-next +# Handover — @gonzoblasco/create-stack -**Fecha:** 2026-07-15 -**Última versión:** `0.7.1` +**Fecha:** 2026-07-20 +**Última versión:** `0.8.0` (v2-rebrand) +**Branch:** `v2-rebrand` --- -## Últimos cambios (2026-07-15) +## Resumen de la reidentificación -### Templates actualizados para compatibilidad con versiones modernas +`create-stack-next` → `@gonzoblasco/create-stack`. Se refactorizó la arquitectura a stacks modulares, se renombró el paquete, y se documentó con ADRs + OpenSpec specs. -Se actualizaron los templates (`template/` y `template-api/`) para resolver los pitfalls encontrados al scaffoldear SoporteML v2: +### Cambios principales -- **Biome 1.9 → 2.5.4:** - - Schema URL actualizada - - `organizeImports.enabled` → `assist.actions.source.organizeImports.level: "on"` - - `linter.rules.recommended: true` → `linter.rules.preset: "recommended"` - - `files.ignore` → `files.includes` (formato Biome 2.5+) - - Agregado `css.parser.tailwindDirectives: true` para Tailwind v4 -- **TypeScript:** templates mantienen `^5.6.x` (Next.js 15 no es compatible con TS 7) -- **Vitest:** bump de `^2.1.0` a `^4.0.0` -- **Agregado `src/global.d.ts`** con declaraciones de módulos para CSS/imágenes -- **`@types/react` y `@types/react-dom`** actualizados a `^19.0.0` en template-api +1. **Nombre npm:** `@gonzoblasco/create-stack` (scoped package público) +2. **CLI:** `npx @gonzoblasco/create-stack [nombre]` +3. **Arquitectura:** cada stack es un módulo en `src/stacks//` +4. **Stacks:** `next` (full app) y `api` (API-only) +5. **Compatibilidad:** `npx create-stack-next my-app` y `--template api` siguen funcionando -### Proyecto CSN actualizado +### Archivos modificados/creados -- `@biomejs/biome` bump a `^2.5.0`, `typescript` a `^7.0.0`, `vitest` a `^4.0.0` -- Agregado `"types": ["node"]` en tsconfig para compatibilidad con TS 7 -- Todos los 64 tests pasando ✅ +- `package.json` — renombrado, bin actualizado +- `src/parse-args.ts` — nueva API con stack posicional +- `src/cli.ts` — refactorizado para usar registry de stacks +- `src/stacks/` — nuevo: types, registry, next, api +- `src/stacks/next/template/` — template Next.js full (movido de `template/`) +- `src/stacks/api/template/` — template API-only (movido de `template-api/`) +- `src/test/*.test.ts` — tests actualizados para nueva API +- `vitest.config.ts` — exclude templates del runner +- `tsconfig.json` — exclude templates del build +- `README.md`, `ROADMAP.md`, `CHANGELOG.md`, `FUTURE.md` — actualizados +- `.knowledge/` — nuevo: ADRs, specs, BRIEF, STATUS -### Nota importante +### Tests -Los templates usan TypeScript 5.x porque Next.js 15 no es compatible con TS 7 (falla `next build` con `Cannot read properties of undefined (reading 'fileExists')`). El proyecto CSN en sí usa TS 7 sin problemas porque no depende de Next.js. +- 67 tests pasando (5 test files) +- Build compila sin errores +- Lint: Biome check ---- - -## Estado actual del proyecto - -### Versiones cerradas -- **Fase 1 (Robustez Absoluta):** ✅ Completada en v0.5.x -- **Fase 2 (Flexibilidad Interna y DX):** ✅ Completada en v0.6.x -- **Fase 3 (Adopción y Documentación):** ⏳ En progreso (SDD integrado) - -### Entregables de v0.7.0 completados -- ✅ **Spec-Driven Development integrado con OpenSpec**: todos los proyectos generados incluyen OpenSpec pre-configurado. -- ✅ Select interactivo para elegir herramientas IA (Claude Code, Cursor, Windsurf, GitHub Copilot, Cline, Codex) con Claude y Cursor pre-seleccionados. -- ✅ `openspec init --tools --force` se ejecuta post-install, antes de `git init`. -- ✅ Estructura `openspec/` pre-armada en `template/` y `template-api/` como fallback. -- ✅ `@fission-ai/openspec` agregado como `devDependency` en ambos templates. -- ✅ `AGENTS.md` de ambos templates actualizado con sección SDD. -- ✅ `README.md` de ambos templates + repo principal actualizado con sección SDD. -- ✅ Nuevo flag `--no-openspec` para saltear la inicialización. -- ✅ Próximos pasos del CLI incluye `/opsx:propose "tu primera feature"`. -- ✅ Fix de v0.6.2: `dotfiles: true` en `copy-template.ts` para copiar archivos ocultos. - -### Entregables de v0.6.x completados -- ✅ Template `--template api` (backend puro Next.js App Router, Drizzle ORM + SQLite, Zod, middleware Bearer/API Key, tests con `node-mocks-http`). -- ✅ AI-Native workflow: `AGENTS.md`, `ROADMAP.md`, `AGENT_TASKS.md`, `HANDOFF.md` y ADRs semilla inyectados en ambos templates. -- ✅ Soporte de workspaces con detección automática y desactivación inteligente de `git init`. -- ✅ Pulido visual del CLI con `@clack/prompts`. -- ✅ CI/CD de GitHub Actions en el repositorio central. - -### Tests del scaffolder -- 64 tests pasando con `npm run test:run`. -- 9 tests nuevos de OpenSpec (estructura, config, devDeps, runOpenSpecInit). -- `vitest.config.ts` aísla estrictamente `src/` para evitar colisiones con tests anidados en `template/`. - ---- - -## Dónde retomar +### Pendientes -Cuando se retome el proyecto, se recomienda revisar: +- [ ] Publicar v0.8.0 en npm (`npm login` + `npm publish`) +- [ ] Renombrar repo en GitHub a `create-stack` +- [ ] Mergear `v2-rebrand` a `main` cuando esté estable +- [ ] Sitio de documentación +- [ ] Video demo -1. `ROADMAP.md` — estado actual de fases y épicas pendientes. -2. `AGENT_TASKS.md` — sprint plan para la Fase 3. -3. `CHANGELOG.md` — historial de versiones. -4. `FUTURE.md` — ideas post-v1.0.0. +### Notas -### Pendientes de la Fase 3 -- [ ] Publicar v0.7.0 en npm (requiere `npm login` de Gonzo) -- [ ] Sitio Web de Documentación (VitePress o Nextra) -- [ ] Material de Onboarding (video demo de 2-4 min) -- [ ] Tests de integración multi-package manager (`pnpm`, `yarn`, `bun`) -- [ ] Polish de Release Candidate: beta testing comunitario +- Los templates usan TypeScript 5.x (Next.js 15 no es compatible con TS 7) +- El proyecto CSN en sí usa TS 7 sin problemas +- OpenSpec se ejecuta con `OPENSPEC_TELEMETRY=0` +- El flag `--template` legacy se mapea: `app → next`, `api → api` +- Si se pasa `--template` y también un stack posicional, gana `--template` ---- - -## Notas - -- OpenSpec se ejecuta con `OPENSPEC_TELEMETRY=0` para desactivar telemetría. -- El flujo es: copiar template → personalizar → select herramientas IA → npm install → openspec init → git init. -- Si `openspec init` falla (sin internet, npm caído), la estructura base ya está copiada del template como fallback. -- Los prompts de agentes están en español (`.openclaw/prompts/` y `.agents/prompts/`). -- CSN es 100% en español — release notes, docs, todo. - ---- +### Lecciones aprendidas -**Última actualización:** 2026-07-14 \ No newline at end of file +- **Siempre correr `lint:fix` + `typecheck` + `test:run` antes de commitear.** No hay commits de fixes. Si algo falla, se arregla antes del commit. Esto evita ruido en el historial y CI failures evitables. diff --git a/README.md b/README.md index 2133355..55e3014 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# create-stack-next +# @gonzoblasco/create-stack -> Scaffolder opinado para Next.js 15. Un solo comando y tenés un proyecto moderno, testeado, lintado y listo para que un AI agent lo habite. +> Scaffolder opinado multi-stack. Un solo comando y tenés un proyecto moderno, testeado, lintado y listo para que un AI agent lo habite. -[![npm version](https://img.shields.io/npm/v/create-stack-next.svg)](https://www.npmjs.com/package/create-stack-next) +[![npm version](https://img.shields.io/npm/v/@gonzoblasco/create-stack.svg)](https://www.npmjs.com/package/@gonzoblasco/create-stack) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) --- @@ -10,17 +10,25 @@ ## Instalación ```bash -npx create-stack-next my-app +# Stack Next.js full (app + API routes + tests + AI agent) +npx @gonzoblasco/create-stack next my-app -# Si solo necesitás un Backend (API Route Handlers): -npx create-stack-next my-api --template api +# Stack Next.js API-only (Drizzle ORM + SQLite + middleware) +npx @gonzoblasco/create-stack api my-api ``` -Eso es todo. El comando genera un proyecto Next.js 15 listo para empezar a desarrollar. +Eso es todo. El comando genera un proyecto listo para empezar a desarrollar. --- -## Qué incluye +## Stacks disponibles + +| Stack | Descripción | +|-------|-------------| +| `next` | Next.js 15 + React 19 + TypeScript estricto + Biome + Vitest + Playwright + Zod + OpenSpec + AI agent config | +| `api` | Next.js 15 App Router (API-only) + Drizzle ORM + SQLite + middleware Bearer + tests con `node-mocks-http` | + +### Stack Next.js Full (`next`) El proyecto generado viene con todo preconfigurado: @@ -33,7 +41,8 @@ El proyecto generado viene con todo preconfigurado: - **AGENTS.md** + workflow de IA inyectado para desarrollo asistido - `docs/` con arquitectura, decisiones y guía de contribución -### Template API (`--template api`) +### Stack API-only (`api`) + Si preferís generar un backend puramente funcional sin React components: - **Next.js 15 App Router** (Rutas de API). - **Drizzle ORM** + SQLite pre-configurado para base de datos. @@ -45,8 +54,8 @@ Si preferís generar un backend puramente funcional sin React components: ## ¿Por qué este y no create-next-app? -| Aspecto | create-next-app | create-stack-next | -|---------|-----------------|-------------------| +| Aspecto | create-next-app | @gonzoblasco/create-stack | +|---------|-----------------|---------------------------| | Testing | Ninguno | Vitest + Playwright configurados | | Linter | ESLint + Prettier | Biome (un solo binario) | | TypeScript | Básico | Estricto + `noUncheckedIndexedAccess` | @@ -54,16 +63,33 @@ Si preferís generar un backend puramente funcional sin React components: | Spec-Driven Development | Ninguno | OpenSpec integrado (specs + slash commands) | | CI | Ninguno | GitHub Actions incluido | | Filosofía | Mínimo | Opinado con defaults 2026 | +| Multi-stack | Solo Next.js | `next`, `api`, y más por venir | **Ideal si:** arrancás proyectos con frecuencia y no querés repetir las mismas 40 decisiones cada vez. --- -## Casos de uso recomendados +## Opciones + +```bash +npx @gonzoblasco/create-stack [nombre] [opciones] + +Opciones: + --no-git No inicializa git ni hace commit inicial + --no-install No corre npm install después de generar + --no-openspec No inicializa OpenSpec (Spec-Driven Development) + --pm Package manager: npm, pnpm, yarn, bun (default: npm) + +Ejemplos: + npx @gonzoblasco/create-stack next my-app + npx @gonzoblasco/create-stack api my-api + npx @gonzoblasco/create-stack next my-app --pm pnpm --no-git +``` -- Proyectos personales o de equipo donde querés arrancar rápido con buena base -- Cuando usás (o querés usar) AI agents para desarrollar -- Cuando valorás tener tests, lint y CI desde el día 1 +### Compatibilidad hacia atrás + +El formato legacy `npx create-stack-next my-app` sigue funcionando (asume stack `next`). +El flag `--template api` también funciona, mapeado al stack `api`. --- @@ -79,7 +105,7 @@ El proyecto incluye: ### Spec-Driven Development con OpenSpec -Desde la v0.7.0, todos los proyectos generados incluyen [OpenSpec](https://github.com/Fission-AI/OpenSpec) pre-configurado. Esto significa que tu IA trabaja con especificaciones en vez de prompts vagos: +Todos los proyectos generados incluyen [OpenSpec](https://github.com/Fission-AI/OpenSpec) pre-configurado. Esto significa que tu IA trabaja con especificaciones en vez de prompts vagos: ```bash /opsx:propose "agregar autenticación" # la IA crea propuesta + specs + tasks @@ -91,18 +117,6 @@ Durante el scaffolding, el CLI te pregunta qué herramientas de IA usás (Claude --- -## Roadmap - -El proyecto se encuentra en desarrollo activo hacia la versión estable (v1.0.0). Podés ver el detalle completo y estado actual en [`ROADMAP.md`](ROADMAP.md). - -- **Fase 1: Robustez Absoluta** ✅ (Completada en v0.5.0) -- **Fase 2: Flexibilidad Interna y DX** ✅ (Completada en v0.6.0) -- **Fase 3: Adopción y Documentación** ⏳ En progreso - -Ver [`FUTURE.md`](FUTURE.md) para el registro de ideas futuras. - ---- - ## Comandos del proyecto generado ```bash @@ -118,9 +132,34 @@ npm run agent # sesión con OpenClaw --- -## Contribuir +## Arquitectura del CLI + +``` +src/ +├── index.ts # Entrypoint +├── cli.ts # Lógica principal del CLI +├── parse-args.ts # Parser de argumentos +├── copy-template.ts # Copia y personaliza templates +├── openspec-init.ts # Inicialización de OpenSpec +├── workspace.ts # Detección de workspaces +└── stacks/ + ├── types.ts # Tipos de StackConfig + ├── index.ts # Registry de stacks + ├── next/ + │ ├── index.ts # Config del stack next + │ └── template/ # Template Next.js full + └── api/ + ├── index.ts # Config del stack api + └── template/ # Template Next.js API-only +``` + +Cada stack es un módulo independiente. Para agregar uno nuevo, creá `src/stacks//` con su `index.ts` y `template/`, y registralo en `src/stacks/index.ts`. + +--- + +## Roadmap -Si querés contribuir, mirá [`docs/contributing.md`](docs/contributing.md) en el template generado. +Ver [`ROADMAP.md`](ROADMAP.md) y [`FUTURE.md`](FUTURE.md). --- @@ -130,4 +169,4 @@ MIT --- -**Creado con ❤️ por Gonzo y Kanam (OpenClaw)** \ No newline at end of file +**Creado con ❤️ por Gonzo y Kanam (OpenClaw)** diff --git a/ROADMAP.md b/ROADMAP.md index 6800484..c49c86f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,19 +1,23 @@ # Roadmap -## Estado actual (v0.7.0) +## Estado actual (v0.8.0 — Reidentificación) -- ✅ CLI robusto con validación de nombres, edge cases de FS y flujo Git -- ✅ Template app (Next.js 15 + React 19 + TS estricto + Biome + Vitest + Playwright + Zod) -- ✅ Template API (`--template api`) con Drizzle ORM + SQLite, middleware, error handling -- ✅ AI-Native: `AGENTS.md`, `.openclaw/`, `.agents/`, prompts y workflow inyectados +- ✅ CLI con stack posicional (`npx @gonzoblasco/create-stack next my-app`) +- ✅ Arquitectura modular de stacks (`src/stacks//`) +- ✅ Stacks: `next` (Next.js full) y `api` (Next.js API-only) +- ✅ Compatibilidad hacia atrás con `create-stack-next` y `--template` +- ✅ Template Next.js 15 + React 19 + TS estricto + Biome + Vitest + Playwright + Zod +- ✅ Template API con Drizzle ORM + SQLite + middleware + tests +- ✅ AI-Native: AGENTS.md, .openclaw/, .agents/, prompts y workflow inyectados - ✅ Soporte de workspaces (Turborepo, pnpm workspaces) con detección automática - ✅ CI/CD con GitHub Actions (lint, typecheck, tests, build en 4 package managers) -- ✅ Spec-Driven Development integrado con OpenSpec (v0.7.0) -- ✅ 64 tests automáticos +- ✅ Spec-Driven Development integrado con OpenSpec +- ✅ 67 tests automáticos ## Próximos pasos -- [ ] Publicar v0.7.0 en npm +- [ ] Publicar v0.8.0 en npm como `@gonzoblasco/create-stack` +- [ ] Renombrar repo en GitHub a `create-stack` - [ ] Sitio de documentación (VitePress o Nextra) - [ ] Video demo de onboarding (2-4 min) - [ ] Tests de integración multi-package manager en CI @@ -21,4 +25,4 @@ ## Ideas futuras -Ver [`FUTURE.md`](FUTURE.md). \ No newline at end of file +Ver [`FUTURE.md`](FUTURE.md). diff --git a/package-lock.json b/package-lock.json index 298d273..b5bdc98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { - "name": "create-stack-next", - "version": "0.7.1", + "name": "@gonzoblasco/create-stack", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "create-stack-next", - "version": "0.7.1", + "name": "@gonzoblasco/create-stack", + "version": "0.8.0", "license": "MIT", "dependencies": { "@clack/prompts": "^0.9.0", "picocolors": "^1.1.0" }, "bin": { - "create-stack-next": "dist/index.js" + "create-stack": "dist/index.js" }, "devDependencies": { "@biomejs/biome": "^2.5.0", @@ -209,29 +209,6 @@ "sisteransi": "^1.0.5" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -587,11 +564,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", - "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1147,9 +1125,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -1163,23 +1141,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -1198,9 +1176,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -1219,9 +1197,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -1240,9 +1218,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -1261,9 +1239,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -1282,9 +1260,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -1303,9 +1281,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -1324,9 +1302,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -1345,9 +1323,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -1366,9 +1344,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -1387,9 +1365,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -1437,9 +1415,9 @@ } }, "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -1469,6 +1447,7 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1477,9 +1456,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", "dev": true, "funding": [ { @@ -1497,7 +1476,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1671,16 +1650,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", - "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { diff --git a/package.json b/package.json index 759136e..47ffc1f 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { - "name": "create-stack-next", - "version": "0.7.2", - "description": "Scaffolder opinado para proyectos Next.js 15 con TypeScript estricto, Biome, Vitest, Playwright y AI agent config preinstalado.", + "name": "@gonzoblasco/create-stack", + "version": "0.8.0", + "description": "Scaffolder opinado multi-stack. Un solo comando y tenés un proyecto moderno, testeado, lintado y listo para que un AI agent lo habite.", "type": "module", "bin": { - "create-stack-next": "dist/index.js" + "create-stack": "dist/index.js", + "@gonzoblasco/create-stack": "dist/index.js" }, "files": [ "dist", - "template", - "template-api" + "scripts" ], "scripts": { - "build": "tsc", + "build": "tsc && node scripts/copy-templates.mjs", "dev": "tsc --watch", "start": "node ./dist/index.js", "lint": "biome check .", @@ -26,15 +26,17 @@ "node": ">=20.0.0" }, "keywords": [ - "create-stack-next", + "create-stack", "scaffolder", "next.js", "nextjs", + "tanstack", "typescript", "biome", "vitest", "playwright", - "ai-agent" + "ai-agent", + "openspec" ], "license": "MIT", "dependencies": { diff --git a/scripts/copy-templates.mjs b/scripts/copy-templates.mjs new file mode 100644 index 0000000..0c687e1 --- /dev/null +++ b/scripts/copy-templates.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node +// Copy templates from src/stacks/*/template/ to dist/stacks/*/template/ +// after TypeScript build. Templates are not compiled but need to be in dist/ +// for runtime resolution (__dirname points to dist/). + +import { cp, readdir } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = resolve(__dirname, ".."); +const srcStacksDir = join(rootDir, "src", "stacks"); +const distStacksDir = join(rootDir, "dist", "stacks"); + +async function copyTemplates() { + const stackDirs = await readdir(srcStacksDir, { withFileTypes: true }); + + for (const entry of stackDirs) { + if (!entry.isDirectory()) continue; + + const srcTemplateDir = join(srcStacksDir, entry.name, "template"); + const distTemplateDir = join(distStacksDir, entry.name, "template"); + + try { + await cp(srcTemplateDir, distTemplateDir, { + recursive: true, + dotfiles: true, + }); + console.log(` ✓ Copiado template: ${entry.name}`); + } catch (err) { + const nodeErr = /** @type {import('node:fs').ErrnoException} */ (err); + if (nodeErr.code !== "ENOENT") { + console.error(` ✗ Error copiando template ${entry.name}:`, err); + } + } + } +} + +copyTemplates().catch((err) => { + console.error("Error copiando templates:", err); + process.exit(1); +}); diff --git a/src/cli.ts b/src/cli.ts index 42a7e50..59a0efa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,6 @@ import { spawn } from "node:child_process"; import { accessSync, constants, existsSync, readdirSync } from "node:fs"; -import { mkdir, writeFile } from "node:fs/promises"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import * as p from "@clack/prompts"; @@ -8,6 +8,7 @@ import pc from "picocolors"; import { copyTemplate } from "./copy-template.js"; import { runOpenSpecInit } from "./openspec-init.js"; import { type Args, parseArgs } from "./parse-args.js"; +import { getStack, listStacks } from "./stacks/index.js"; import { isInsideWorkspace } from "./workspace.js"; /** Herramientas IA soportadas por OpenSpec para el select interactivo. */ @@ -31,18 +32,22 @@ const DEFAULT_TOOLS = ["claude", "cursor"]; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// En dist/ queda en dist/index.js, así que subimos un nivel -function getTemplateDir(templateType = "app"): string { - const folderName = templateType === "api" ? "template-api" : "template"; - return resolve(__dirname, "..", folderName); -} +const CLI_NAME = "@gonzoblasco/create-stack"; function printUsage(): void { + const stacks = listStacks(); + const stacksList = stacks + .map((s) => ` ${pc.green(s.id.padEnd(12))} ${s.label} — ${s.description}`) + .join("\n"); + console.log(` -${pc.cyan(pc.bold("create-stack-next"))} — scaffolder opinado para Next.js 15 +${pc.cyan(pc.bold(CLI_NAME))} — scaffolder opinado multi-stack ${pc.bold("Uso:")} - ${pc.cyan("npx create-stack-next")} ${pc.green("")} [opciones] + ${pc.cyan(`npx ${CLI_NAME}`)} ${pc.green("")} [nombre-del-proyecto] [opciones] + +${pc.bold("Stacks disponibles:")} +${stacksList} ${pc.bold("Opciones:")} ${pc.yellow("--no-git")} No inicializa git ni hace commit inicial @@ -51,8 +56,9 @@ ${pc.bold("Opciones:")} ${pc.yellow("--pm ")} Package manager: ${pc.cyan("npm")}, ${pc.cyan("pnpm")}, ${pc.cyan("yarn")}, ${pc.cyan("bun")} (default: ${pc.cyan("npm")}) ${pc.bold("Ejemplos:")} - ${pc.cyan("npx create-stack-next")} ${pc.green("my-app")} - ${pc.cyan("npx create-stack-next")} ${pc.green("my-app")} ${pc.yellow("--pm pnpm --no-git")} + ${pc.cyan(`npx ${CLI_NAME}`)} ${pc.green("next")} ${pc.green("my-app")} + ${pc.cyan(`npx ${CLI_NAME}`)} ${pc.green("api")} ${pc.green("my-app")} + ${pc.cyan(`npx ${CLI_NAME}`)} ${pc.green("next")} ${pc.green("my-app")} ${pc.yellow("--pm pnpm --no-git")} `); } @@ -74,7 +80,6 @@ export function validateProjectName( return { ok: false, reason: "Falta el nombre del proyecto" }; } - // Mismas reglas que npm init: lowercase, puede tener guión/underscore, no arrancar con . if (!/^[a-z0-9-_]+$/.test(name)) { return { ok: false, @@ -89,11 +94,9 @@ export function validateProjectName( }; } - // Si el directorio ya existe, verificar si está vacío if (existsSync(name)) { try { const entries = readdirSync(name); - // Ignorar archivos del sistema como .DS_Store const meaningfulEntries = entries.filter( (entry) => entry !== ".DS_Store" && entry !== "Thumbs.db", ); @@ -108,7 +111,6 @@ export function validateProjectName( } } - // Verificar permisos de escritura en el directorio padre const parentDir = dirname(resolve(name)); try { accessSync(parentDir, constants.W_OK); @@ -119,13 +121,8 @@ export function validateProjectName( return { ok: true }; } -function _logStep(emoji: string, message: string): void { - console.log(` ${emoji} ${message}`); -} - /** - * Ejecuta un comando en un directorio. Hereda stdout/stderr del proceso padre - * para que el output de npm/git sea visible. + * Ejecuta un comando en un directorio. Hereda stdout/stderr del proceso padre. */ export function execInDir( cmd: string, @@ -144,7 +141,7 @@ export function execInDir( } /** - * Ejecuta un comando y captura su stdout. Útil para leer valores de git config. + * Ejecuta un comando y captura su stdout. */ export function execCapture( cmd: string, @@ -180,7 +177,6 @@ export async function isGitAvailable(): Promise { /** * Lee un valor de la configuración global de git. - * Retorna null si no está configurado o si git no está disponible. */ export async function getGitConfig( key: string, @@ -195,7 +191,6 @@ export async function getGitConfig( } async function runGitInit(projectDir: string): Promise { - // 1. Verificar que git está disponible if (!(await isGitAvailable())) { p.log.warn("Git no encontrado. Saltando inicialización git."); p.log.info(`Instalá Git y corré ${pc.cyan("git init")} manualmente.`); @@ -205,7 +200,6 @@ async function runGitInit(projectDir: string): Promise { await execInDir("git", ["init", "-b", "main"], projectDir); await execInDir("git", ["add", "."], projectDir); - // 2. Verificar user.name/user.email const userName = await getGitConfig("user.name", projectDir); const userEmail = await getGitConfig("user.email", projectDir); @@ -217,12 +211,12 @@ async function runGitInit(projectDir: string): Promise { "git", [ "-c", - "user.name=create-stack-next", + "user.name=create-stack", "-c", - "user.email=noreply@create-stack-next", + "user.email=noreply@create-stack", "commit", "-m", - "chore: initial commit from create-stack-next", + "chore: initial commit from @gonzoblasco/create-stack", ], projectDir, ); @@ -232,7 +226,7 @@ async function runGitInit(projectDir: string): Promise { } else { await execInDir( "git", - ["commit", "-m", "chore: initial commit from create-stack-next"], + ["commit", "-m", "chore: initial commit from @gonzoblasco/create-stack"], projectDir, ); } @@ -244,7 +238,6 @@ async function runInstall(projectDir: string, pm: string): Promise { } export async function run(): Promise { - // Sin args o --help/-h → mostrar usage if ( process.argv.length <= 2 || process.argv.includes("--help") || @@ -256,16 +249,42 @@ export async function run(): Promise { const args: Args = parseArgs(process.argv.slice(2)); - const projectName = args._[0]; + // Validar stack + const stack = args.stack; + if (!stack) { + console.error(pc.red("❌"), "Falta el stack."); + console.error( + ` Uso: ${pc.cyan(`npx ${CLI_NAME}`)} ${pc.green("")} ${pc.green("[nombre]")}`, + ); + console.error( + ` Stacks: ${listStacks() + .map((s) => pc.cyan(s.id)) + .join(", ")}`, + ); + process.exit(1); + } + + const stackConfig = getStack(stack); + if (!stackConfig) { + console.error( + pc.red("❌"), + `Stack "${stack}" no encontrado. Stacks disponibles: ${listStacks() + .map((s) => pc.cyan(s.id)) + .join(", ")}`, + ); + process.exit(1); + } + + // Validar nombre del proyecto + const projectName = args.projectName; if (!projectName) { console.error(pc.red("❌"), "Falta el nombre del proyecto."); console.error( - ` Uso: ${pc.cyan("npx create-stack-next")} ${pc.green("")}`, + ` Uso: ${pc.cyan(`npx ${CLI_NAME}`)} ${pc.green(stack)} ${pc.green("")}`, ); process.exit(1); } - // Validar nombre const validation = validateProjectName(projectName); if (!validation.ok) { console.error(pc.red("❌"), validation.reason); @@ -273,12 +292,12 @@ export async function run(): Promise { } const projectDir = resolve(process.cwd(), projectName); - const pm = args.pm ?? "npm"; + const pm = args.pm ?? stackConfig.defaultPM; const git = args.git !== false; const install = args.install !== false; const openspec = args.openspec !== false; - p.intro(pc.bgCyan(pc.black(" create-stack-next "))); + p.intro(pc.bgCyan(pc.black(` ${CLI_NAME} — ${stackConfig.label} `))); try { const s = p.spinner(); @@ -288,16 +307,17 @@ export async function run(): Promise { await mkdir(projectDir, { recursive: true }); s.stop(`Directorio creado: ${pc.cyan(projectName)}`); - // 2. Copiar template - s.start("Copiando template"); - const templateDir = getTemplateDir(args.template); - await copyTemplate(templateDir, projectDir, { projectName, pm }); + // 2. Copiar template del stack + s.start(`Copiando template: ${stackConfig.label}`); + await copyTemplate(stackConfig.templateDir, projectDir, { + projectName, + pm, + }); s.stop("Template copiado exitosamente"); - // 3. Personalizar package.json con el nombre correcto del proyecto + // 3. Personalizar package.json s.start("Configurando proyecto"); const packageJsonPath = join(projectDir, "package.json"); - const { readFile } = await import("node:fs/promises"); const packageJsonRaw = await readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonRaw); packageJson.name = projectName; @@ -308,10 +328,9 @@ export async function run(): Promise { ); s.stop("Configuración lista"); - // 4. Seleccionar herramientas IA para OpenSpec (antes de install) + // 4. Seleccionar herramientas IA para OpenSpec let selectedTools: string[] = DEFAULT_TOOLS; if (openspec) { - // En entornos no interactivos (CI, pipes, etc.), saltar el select y usar defaults if (!process.stdin.isTTY) { p.log.info( `Modo no interactivo detectado. Usando herramientas por defecto: ${DEFAULT_TOOLS.join(", ")}`, @@ -353,9 +372,8 @@ export async function run(): Promise { } } - // 6. OpenSpec init (Spec-Driven Development) + // 6. OpenSpec init if (openspec && selectedTools.length > 0) { - // Si el usuario seleccionó "all", ignorar las demás y pasar solo "all" const toolsFlag = selectedTools.includes("all") ? ["all"] : selectedTools; s.start("Configurando OpenSpec (Spec-Driven Development)"); try { diff --git a/src/parse-args.ts b/src/parse-args.ts index dbd76ed..6abec49 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -1,11 +1,10 @@ -/** - * Parser de argumentos minimalista. No usamos commander/yargs porque solo - * necesitamos 3 flags. process.argv a mano es más simple y menos deps. - */ +import { isValidStack } from "./stacks/index.js"; export type Args = { - /** Positional args: primer elemento es el nombre del proyecto */ - _: string[]; + /** Stack a usar (primer argumento posicional) */ + stack?: string; + /** Nombre del proyecto (segundo argumento posicional, opcional) */ + projectName?: string; /** Inicializar git (default: true). false si se pasa --no-git */ git?: boolean; /** Correr npm install (default: true). false si se pasa --no-install */ @@ -14,7 +13,7 @@ export type Args = { pm?: string; /** Inicializar OpenSpec (default: true). false si se pasa --no-openspec */ openspec?: boolean; - /** Template to use: app | api (default: app) */ + /** Template legacy (deprecado, usar stack posicional) */ template?: string; }; @@ -32,12 +31,18 @@ function isValidTemplate(value: string): value is ValidTemplate { return (VALID_TEMPLATES as readonly string[]).includes(value); } +/** Mapeo de templates legacy a stacks */ +const TEMPLATE_TO_STACK: Record = { + app: "next", + api: "api", +}; + export function parseArgs(argv: string[]): Args { - const args: Args = { _: [] }; + const args: Args = {}; + const positional: string[] = []; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; - if (arg === undefined) continue; switch (arg) { @@ -74,19 +79,37 @@ export function parseArgs(argv: string[]): Args { `--template inválido: "${next}". Valores válidos: ${VALID_TEMPLATES.join(", ")}`, ); } + // Mapear template legacy a stack args.template = next; + args.stack = TEMPLATE_TO_STACK[next]; i++; break; } case "--help": case "-h": - // El caller ya maneja esto antes + // El caller maneja esto break; default: if (arg.startsWith("--")) { throw new Error(`Flag desconocida: ${arg}`); } - args._.push(arg); + positional.push(arg); + } + } + + // El primer argumento posicional es el stack + // Solo si no fue ya seteado por --template legacy + if (positional.length > 0 && args.stack === undefined) { + const first = positional[0]; + if (first && isValidStack(first)) { + args.stack = first; + // El segundo argumento posicional es el nombre del proyecto + args.projectName = positional[1]; + } else { + // Compatibilidad hacia atrás: si el primer arg no es un stack válido, + // asumimos que es el nombre del proyecto con stack "next" (legacy) + args.stack = "next"; + args.projectName = first; } } diff --git a/src/stacks/api/index.ts b/src/stacks/api/index.ts new file mode 100644 index 0000000..24ff43d --- /dev/null +++ b/src/stacks/api/index.ts @@ -0,0 +1,16 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { StackConfig } from "../types.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export const apiStack: StackConfig = { + id: "api", + label: "Next.js API", + description: + "Next.js 15 App Router (API-only) + Drizzle ORM + SQLite + middleware Bearer + tests con node-mocks-http", + templateDir: resolve(__dirname, "template"), + defaultPM: "npm", + minNodeVersion: ">=20.0.0", +}; diff --git a/template-api/.vscode/extensions.json b/src/stacks/api/template/.vscode/extensions.json similarity index 100% rename from template-api/.vscode/extensions.json rename to src/stacks/api/template/.vscode/extensions.json diff --git a/template-api/.vscode/settings.json b/src/stacks/api/template/.vscode/settings.json similarity index 100% rename from template-api/.vscode/settings.json rename to src/stacks/api/template/.vscode/settings.json diff --git a/template-api/AGENTS.md b/src/stacks/api/template/AGENTS.md similarity index 100% rename from template-api/AGENTS.md rename to src/stacks/api/template/AGENTS.md diff --git a/template-api/AGENT_TASKS.md b/src/stacks/api/template/AGENT_TASKS.md similarity index 100% rename from template-api/AGENT_TASKS.md rename to src/stacks/api/template/AGENT_TASKS.md diff --git a/template-api/CHANGELOG.md b/src/stacks/api/template/CHANGELOG.md similarity index 100% rename from template-api/CHANGELOG.md rename to src/stacks/api/template/CHANGELOG.md diff --git a/template-api/HANDOFF.md b/src/stacks/api/template/HANDOFF.md similarity index 100% rename from template-api/HANDOFF.md rename to src/stacks/api/template/HANDOFF.md diff --git a/template-api/README.md b/src/stacks/api/template/README.md similarity index 100% rename from template-api/README.md rename to src/stacks/api/template/README.md diff --git a/template-api/ROADMAP.md b/src/stacks/api/template/ROADMAP.md similarity index 100% rename from template-api/ROADMAP.md rename to src/stacks/api/template/ROADMAP.md diff --git a/template-api/biome.json b/src/stacks/api/template/biome.json similarity index 100% rename from template-api/biome.json rename to src/stacks/api/template/biome.json diff --git a/template-api/docs/architecture.md b/src/stacks/api/template/docs/architecture.md similarity index 100% rename from template-api/docs/architecture.md rename to src/stacks/api/template/docs/architecture.md diff --git a/template-api/docs/contributing.md b/src/stacks/api/template/docs/contributing.md similarity index 100% rename from template-api/docs/contributing.md rename to src/stacks/api/template/docs/contributing.md diff --git a/template-api/docs/decisions.md b/src/stacks/api/template/docs/decisions.md similarity index 100% rename from template-api/docs/decisions.md rename to src/stacks/api/template/docs/decisions.md diff --git a/template-api/docs/workflow_ai.md b/src/stacks/api/template/docs/workflow_ai.md similarity index 100% rename from template-api/docs/workflow_ai.md rename to src/stacks/api/template/docs/workflow_ai.md diff --git a/template-api/drizzle.config.ts b/src/stacks/api/template/drizzle.config.ts similarity index 100% rename from template-api/drizzle.config.ts rename to src/stacks/api/template/drizzle.config.ts diff --git a/template-api/gitignore b/src/stacks/api/template/gitignore similarity index 100% rename from template-api/gitignore rename to src/stacks/api/template/gitignore diff --git a/template-api/openspec/changes/archive/.gitkeep b/src/stacks/api/template/openspec/changes/archive/.gitkeep similarity index 100% rename from template-api/openspec/changes/archive/.gitkeep rename to src/stacks/api/template/openspec/changes/archive/.gitkeep diff --git a/template-api/openspec/config.yaml b/src/stacks/api/template/openspec/config.yaml similarity index 100% rename from template-api/openspec/config.yaml rename to src/stacks/api/template/openspec/config.yaml diff --git a/template-api/openspec/specs/.gitkeep b/src/stacks/api/template/openspec/specs/.gitkeep similarity index 100% rename from template-api/openspec/specs/.gitkeep rename to src/stacks/api/template/openspec/specs/.gitkeep diff --git a/template-api/package.json b/src/stacks/api/template/package.json similarity index 100% rename from template-api/package.json rename to src/stacks/api/template/package.json diff --git a/template-api/src/app/api/health/route.ts b/src/stacks/api/template/src/app/api/health/route.ts similarity index 100% rename from template-api/src/app/api/health/route.ts rename to src/stacks/api/template/src/app/api/health/route.ts diff --git a/template-api/src/app/api/users/route.ts b/src/stacks/api/template/src/app/api/users/route.ts similarity index 100% rename from template-api/src/app/api/users/route.ts rename to src/stacks/api/template/src/app/api/users/route.ts diff --git a/template-api/src/lib/api-response.ts b/src/stacks/api/template/src/lib/api-response.ts similarity index 100% rename from template-api/src/lib/api-response.ts rename to src/stacks/api/template/src/lib/api-response.ts diff --git a/template-api/src/lib/db.ts b/src/stacks/api/template/src/lib/db.ts similarity index 100% rename from template-api/src/lib/db.ts rename to src/stacks/api/template/src/lib/db.ts diff --git a/template-api/src/lib/errors.ts b/src/stacks/api/template/src/lib/errors.ts similarity index 100% rename from template-api/src/lib/errors.ts rename to src/stacks/api/template/src/lib/errors.ts diff --git a/template-api/src/lib/schema.ts b/src/stacks/api/template/src/lib/schema.ts similarity index 100% rename from template-api/src/lib/schema.ts rename to src/stacks/api/template/src/lib/schema.ts diff --git a/template-api/src/middleware.ts b/src/stacks/api/template/src/middleware.ts similarity index 94% rename from template-api/src/middleware.ts rename to src/stacks/api/template/src/middleware.ts index 76d3a1b..f2eb308 100644 --- a/template-api/src/middleware.ts +++ b/src/stacks/api/template/src/middleware.ts @@ -10,7 +10,7 @@ export function middleware(request: NextRequest) { request.nextUrl.pathname.startsWith('/api') && !request.nextUrl.pathname.startsWith('/api/health') ) { - if (!authHeader || !authHeader.startsWith('Bearer ')) { + if (!authHeader?.startsWith('Bearer ')) { return NextResponse.json( { success: false, diff --git a/template-api/src/test/health.test.ts b/src/stacks/api/template/src/test/health.test.ts similarity index 95% rename from template-api/src/test/health.test.ts rename to src/stacks/api/template/src/test/health.test.ts index 488ea1b..789dcbb 100644 --- a/template-api/src/test/health.test.ts +++ b/src/stacks/api/template/src/test/health.test.ts @@ -4,7 +4,7 @@ import { GET } from '@/app/api/health/route' describe('Healthcheck API', () => { it('should return 200 and ok status', async () => { - const { req } = createMocks({ + createMocks({ method: 'GET', }) diff --git a/template-api/tsconfig.json b/src/stacks/api/template/tsconfig.json similarity index 100% rename from template-api/tsconfig.json rename to src/stacks/api/template/tsconfig.json diff --git a/template-api/vitest.config.ts b/src/stacks/api/template/vitest.config.ts similarity index 100% rename from template-api/vitest.config.ts rename to src/stacks/api/template/vitest.config.ts diff --git a/src/stacks/index.ts b/src/stacks/index.ts new file mode 100644 index 0000000..4f36630 --- /dev/null +++ b/src/stacks/index.ts @@ -0,0 +1,34 @@ +import { apiStack } from "./api/index.js"; +import { nextStack } from "./next/index.js"; +import type { StackConfig } from "./types.js"; + +/** + * Registry de todos los stacks disponibles. + * Para agregar un stack nuevo, importalo acá y agregalo al array. + */ +const STACKS: StackConfig[] = [nextStack, apiStack]; + +/** Mapa id → stack para lookup rápido */ +const stackMap = new Map(STACKS.map((s) => [s.id, s])); + +/** + * Obtiene la config de un stack por su id. + * Retorna undefined si no existe. + */ +export function getStack(id: string): StackConfig | undefined { + return stackMap.get(id); +} + +/** + * Lista todos los stacks disponibles. + */ +export function listStacks(): StackConfig[] { + return STACKS; +} + +/** + * Valida que un stack id exista. + */ +export function isValidStack(id: string): boolean { + return stackMap.has(id); +} diff --git a/src/stacks/next/index.ts b/src/stacks/next/index.ts new file mode 100644 index 0000000..280a284 --- /dev/null +++ b/src/stacks/next/index.ts @@ -0,0 +1,30 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export interface StackConfig { + /** Identificador del stack (ej: "next", "api") */ + id: string; + /** Nombre legible para mostrar en help */ + label: string; + /** Descripción breve */ + description: string; + /** Directorio del template (relativo a este archivo) */ + templateDir: string; + /** Package manager por defecto */ + defaultPM: string; + /** Versión de Node mínima */ + minNodeVersion: string; +} + +export const nextStack: StackConfig = { + id: "next", + label: "Next.js Full", + description: + "Next.js 15 + React 19 + TypeScript estricto + Biome + Vitest + Playwright + Zod + OpenSpec + AI agent config", + templateDir: resolve(__dirname, "template"), + defaultPM: "npm", + minNodeVersion: ">=20.0.0", +}; diff --git a/template/.agents/config.json b/src/stacks/next/template/.agents/config.json similarity index 100% rename from template/.agents/config.json rename to src/stacks/next/template/.agents/config.json diff --git a/template/.agents/prompts/bugfix.md b/src/stacks/next/template/.agents/prompts/bugfix.md similarity index 100% rename from template/.agents/prompts/bugfix.md rename to src/stacks/next/template/.agents/prompts/bugfix.md diff --git a/template/.agents/prompts/feature.md b/src/stacks/next/template/.agents/prompts/feature.md similarity index 100% rename from template/.agents/prompts/feature.md rename to src/stacks/next/template/.agents/prompts/feature.md diff --git a/template/.agents/prompts/refactor.md b/src/stacks/next/template/.agents/prompts/refactor.md similarity index 100% rename from template/.agents/prompts/refactor.md rename to src/stacks/next/template/.agents/prompts/refactor.md diff --git a/template/.agents/prompts/tests.md b/src/stacks/next/template/.agents/prompts/tests.md similarity index 100% rename from template/.agents/prompts/tests.md rename to src/stacks/next/template/.agents/prompts/tests.md diff --git a/template/.github/workflows/ci.yml b/src/stacks/next/template/.github/workflows/ci.yml similarity index 100% rename from template/.github/workflows/ci.yml rename to src/stacks/next/template/.github/workflows/ci.yml diff --git a/template/.github/workflows/playwright.yml b/src/stacks/next/template/.github/workflows/playwright.yml similarity index 100% rename from template/.github/workflows/playwright.yml rename to src/stacks/next/template/.github/workflows/playwright.yml diff --git a/template/.openclaw/README.md b/src/stacks/next/template/.openclaw/README.md similarity index 100% rename from template/.openclaw/README.md rename to src/stacks/next/template/.openclaw/README.md diff --git a/template/.openclaw/config.json b/src/stacks/next/template/.openclaw/config.json similarity index 100% rename from template/.openclaw/config.json rename to src/stacks/next/template/.openclaw/config.json diff --git a/template/.openclaw/prompts/bugfix.md b/src/stacks/next/template/.openclaw/prompts/bugfix.md similarity index 100% rename from template/.openclaw/prompts/bugfix.md rename to src/stacks/next/template/.openclaw/prompts/bugfix.md diff --git a/template/.openclaw/prompts/feature.md b/src/stacks/next/template/.openclaw/prompts/feature.md similarity index 100% rename from template/.openclaw/prompts/feature.md rename to src/stacks/next/template/.openclaw/prompts/feature.md diff --git a/template/.openclaw/prompts/refactor.md b/src/stacks/next/template/.openclaw/prompts/refactor.md similarity index 100% rename from template/.openclaw/prompts/refactor.md rename to src/stacks/next/template/.openclaw/prompts/refactor.md diff --git a/template/.openclaw/prompts/tests.md b/src/stacks/next/template/.openclaw/prompts/tests.md similarity index 100% rename from template/.openclaw/prompts/tests.md rename to src/stacks/next/template/.openclaw/prompts/tests.md diff --git a/template/.vscode/extensions.json b/src/stacks/next/template/.vscode/extensions.json similarity index 100% rename from template/.vscode/extensions.json rename to src/stacks/next/template/.vscode/extensions.json diff --git a/template/.vscode/settings.json b/src/stacks/next/template/.vscode/settings.json similarity index 100% rename from template/.vscode/settings.json rename to src/stacks/next/template/.vscode/settings.json diff --git a/template/AGENTS.md b/src/stacks/next/template/AGENTS.md similarity index 100% rename from template/AGENTS.md rename to src/stacks/next/template/AGENTS.md diff --git a/template/AGENT_TASKS.md b/src/stacks/next/template/AGENT_TASKS.md similarity index 100% rename from template/AGENT_TASKS.md rename to src/stacks/next/template/AGENT_TASKS.md diff --git a/template/CHANGELOG.md b/src/stacks/next/template/CHANGELOG.md similarity index 100% rename from template/CHANGELOG.md rename to src/stacks/next/template/CHANGELOG.md diff --git a/template/HANDOFF.md b/src/stacks/next/template/HANDOFF.md similarity index 100% rename from template/HANDOFF.md rename to src/stacks/next/template/HANDOFF.md diff --git a/template/LICENSE b/src/stacks/next/template/LICENSE similarity index 100% rename from template/LICENSE rename to src/stacks/next/template/LICENSE diff --git a/template/README.md b/src/stacks/next/template/README.md similarity index 100% rename from template/README.md rename to src/stacks/next/template/README.md diff --git a/template/ROADMAP.md b/src/stacks/next/template/ROADMAP.md similarity index 100% rename from template/ROADMAP.md rename to src/stacks/next/template/ROADMAP.md diff --git a/template/biome.json b/src/stacks/next/template/biome.json similarity index 100% rename from template/biome.json rename to src/stacks/next/template/biome.json diff --git a/template/docs/architecture.md b/src/stacks/next/template/docs/architecture.md similarity index 100% rename from template/docs/architecture.md rename to src/stacks/next/template/docs/architecture.md diff --git a/template/docs/contributing.md b/src/stacks/next/template/docs/contributing.md similarity index 100% rename from template/docs/contributing.md rename to src/stacks/next/template/docs/contributing.md diff --git a/template/docs/decisions.md b/src/stacks/next/template/docs/decisions.md similarity index 100% rename from template/docs/decisions.md rename to src/stacks/next/template/docs/decisions.md diff --git a/template/docs/workflow_ai.md b/src/stacks/next/template/docs/workflow_ai.md similarity index 100% rename from template/docs/workflow_ai.md rename to src/stacks/next/template/docs/workflow_ai.md diff --git a/template/gitignore b/src/stacks/next/template/gitignore similarity index 100% rename from template/gitignore rename to src/stacks/next/template/gitignore diff --git a/template/next.config.ts b/src/stacks/next/template/next.config.ts similarity index 100% rename from template/next.config.ts rename to src/stacks/next/template/next.config.ts diff --git a/template/openspec/changes/archive/.gitkeep b/src/stacks/next/template/openspec/changes/archive/.gitkeep similarity index 100% rename from template/openspec/changes/archive/.gitkeep rename to src/stacks/next/template/openspec/changes/archive/.gitkeep diff --git a/template/openspec/config.yaml b/src/stacks/next/template/openspec/config.yaml similarity index 100% rename from template/openspec/config.yaml rename to src/stacks/next/template/openspec/config.yaml diff --git a/template/openspec/specs/.gitkeep b/src/stacks/next/template/openspec/specs/.gitkeep similarity index 100% rename from template/openspec/specs/.gitkeep rename to src/stacks/next/template/openspec/specs/.gitkeep diff --git a/template/package.json b/src/stacks/next/template/package.json similarity index 100% rename from template/package.json rename to src/stacks/next/template/package.json diff --git a/template/playwright.config.ts b/src/stacks/next/template/playwright.config.ts similarity index 100% rename from template/playwright.config.ts rename to src/stacks/next/template/playwright.config.ts diff --git a/template/public/.gitkeep b/src/stacks/next/template/public/.gitkeep similarity index 100% rename from template/public/.gitkeep rename to src/stacks/next/template/public/.gitkeep diff --git a/template/src/app/api/health/route.ts b/src/stacks/next/template/src/app/api/health/route.ts similarity index 100% rename from template/src/app/api/health/route.ts rename to src/stacks/next/template/src/app/api/health/route.ts diff --git a/template/src/app/globals.css b/src/stacks/next/template/src/app/globals.css similarity index 100% rename from template/src/app/globals.css rename to src/stacks/next/template/src/app/globals.css diff --git a/template/src/app/layout.tsx b/src/stacks/next/template/src/app/layout.tsx similarity index 100% rename from template/src/app/layout.tsx rename to src/stacks/next/template/src/app/layout.tsx diff --git a/template/src/app/page.tsx b/src/stacks/next/template/src/app/page.tsx similarity index 100% rename from template/src/app/page.tsx rename to src/stacks/next/template/src/app/page.tsx diff --git a/template/src/components/ui/.gitkeep b/src/stacks/next/template/src/components/ui/.gitkeep similarity index 100% rename from template/src/components/ui/.gitkeep rename to src/stacks/next/template/src/components/ui/.gitkeep diff --git a/template/src/global.d.ts b/src/stacks/next/template/src/global.d.ts similarity index 100% rename from template/src/global.d.ts rename to src/stacks/next/template/src/global.d.ts diff --git a/template/src/lib/env.ts b/src/stacks/next/template/src/lib/env.ts similarity index 100% rename from template/src/lib/env.ts rename to src/stacks/next/template/src/lib/env.ts diff --git a/template/src/lib/utils.ts b/src/stacks/next/template/src/lib/utils.ts similarity index 100% rename from template/src/lib/utils.ts rename to src/stacks/next/template/src/lib/utils.ts diff --git a/template/src/test/example.test.ts b/src/stacks/next/template/src/test/example.test.ts similarity index 100% rename from template/src/test/example.test.ts rename to src/stacks/next/template/src/test/example.test.ts diff --git a/template/src/test/setup.ts b/src/stacks/next/template/src/test/setup.ts similarity index 100% rename from template/src/test/setup.ts rename to src/stacks/next/template/src/test/setup.ts diff --git a/template/tests/e2e/home.spec.ts b/src/stacks/next/template/tests/e2e/home.spec.ts similarity index 100% rename from template/tests/e2e/home.spec.ts rename to src/stacks/next/template/tests/e2e/home.spec.ts diff --git a/template/tsconfig.json b/src/stacks/next/template/tsconfig.json similarity index 100% rename from template/tsconfig.json rename to src/stacks/next/template/tsconfig.json diff --git a/template/vitest.config.ts b/src/stacks/next/template/vitest.config.ts similarity index 100% rename from template/vitest.config.ts rename to src/stacks/next/template/vitest.config.ts diff --git a/src/stacks/types.ts b/src/stacks/types.ts new file mode 100644 index 0000000..89d7e2c --- /dev/null +++ b/src/stacks/types.ts @@ -0,0 +1,14 @@ +export interface StackConfig { + /** Identificador del stack (ej: "next", "api") */ + id: string; + /** Nombre legible para mostrar en help */ + label: string; + /** Descripción breve */ + description: string; + /** Directorio del template */ + templateDir: string; + /** Package manager por defecto */ + defaultPM: string; + /** Versión de Node mínima */ + minNodeVersion: string; +} diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts index 188c769..fbd53f8 100644 --- a/src/test/cli.test.ts +++ b/src/test/cli.test.ts @@ -6,63 +6,79 @@ import { validateProjectName } from "../cli.js"; import { parseArgs } from "../parse-args.js"; // ═══════════════════════════════════════════════════════════ -// Tests de parseArgs +// Tests de parseArgs (nueva API con stack posicional) // ═══════════════════════════════════════════════════════════ -describe("parseArgs", () => { - it("parsea el nombre del proyecto como argumento posicional", () => { +describe("parseArgs — nueva API con stack posicional", () => { + it("parsea stack + nombre del proyecto como argumentos posicionales", () => { + const args = parseArgs(["next", "my-app"]); + expect(args.stack).toBe("next"); + expect(args.projectName).toBe("my-app"); + }); + + it("parsea stack api + nombre", () => { + const args = parseArgs(["api", "my-api"]); + expect(args.stack).toBe("api"); + expect(args.projectName).toBe("my-api"); + }); + + it("compatibilidad hacia atrás: primer arg no-stack se asume nombre con stack next", () => { const args = parseArgs(["my-app"]); - expect(args._).toEqual(["my-app"]); + expect(args.stack).toBe("next"); + expect(args.projectName).toBe("my-app"); }); it("parsea --no-git correctamente", () => { - const args = parseArgs(["my-app", "--no-git"]); + const args = parseArgs(["next", "my-app", "--no-git"]); expect(args.git).toBe(false); }); it("parsea --no-install correctamente", () => { - const args = parseArgs(["my-app", "--no-install"]); + const args = parseArgs(["next", "my-app", "--no-install"]); expect(args.install).toBe(false); }); it("parsea --pm pnpm correctamente", () => { - const args = parseArgs(["my-app", "--pm", "pnpm"]); + const args = parseArgs(["next", "my-app", "--pm", "pnpm"]); expect(args.pm).toBe("pnpm"); }); it("parsea --pm yarn correctamente", () => { - const args = parseArgs(["my-app", "--pm", "yarn"]); + const args = parseArgs(["next", "my-app", "--pm", "yarn"]); expect(args.pm).toBe("yarn"); }); it("parsea --pm bun correctamente", () => { - const args = parseArgs(["my-app", "--pm", "bun"]); + const args = parseArgs(["next", "my-app", "--pm", "bun"]); expect(args.pm).toBe("bun"); }); it("parsea múltiples flags combinadas", () => { const args = parseArgs([ + "next", "my-app", "--no-git", "--no-install", "--pm", "pnpm", ]); - expect(args._).toEqual(["my-app"]); + expect(args.stack).toBe("next"); + expect(args.projectName).toBe("my-app"); expect(args.git).toBe(false); expect(args.install).toBe(false); expect(args.pm).toBe("pnpm"); }); it("lanza error si --pm tiene un valor inválido", () => { - expect(() => parseArgs(["my-app", "--pm", "deno"])).toThrow( + expect(() => parseArgs(["next", "my-app", "--pm", "deno"])).toThrow( /--pm inválido/, ); }); - it("parsea --template api correctamente", () => { + it("parsea --template api (legacy) correctamente", () => { const args = parseArgs(["my-app", "--template", "api"]); expect(args.template).toBe("api"); + expect(args.stack).toBe("api"); }); it("lanza error si --template tiene un valor inválido", () => { @@ -78,31 +94,34 @@ describe("parseArgs", () => { }); it("lanza error si --pm no tiene valor", () => { - expect(() => parseArgs(["my-app", "--pm"])).toThrow( + expect(() => parseArgs(["next", "my-app", "--pm"])).toThrow( /--pm requiere un valor/, ); }); it("lanza error con flag desconocida", () => { - expect(() => parseArgs(["my-app", "--foo"])).toThrow(/Flag desconocida/); + expect(() => parseArgs(["next", "my-app", "--foo"])).toThrow( + /Flag desconocida/, + ); }); - it("sin argumentos retorna array vacío de posicionales", () => { + it("sin argumentos retorna stack y projectName undefined", () => { const args = parseArgs([]); - expect(args._).toEqual([]); + expect(args.stack).toBeUndefined(); + expect(args.projectName).toBeUndefined(); }); it("ignora --help y -h sin error", () => { const args = parseArgs(["--help"]); - expect(args._).toEqual([]); + expect(args.stack).toBeUndefined(); const args2 = parseArgs(["-h"]); - expect(args2._).toEqual([]); + expect(args2.stack).toBeUndefined(); }); }); // ═══════════════════════════════════════════════════════════ -// Tests de validateProjectName (importación directa) +// Tests de validateProjectName // ═══════════════════════════════════════════════════════════ describe("validateProjectName", () => { @@ -116,8 +135,6 @@ describe("validateProjectName", () => { await rm(tempDir, { recursive: true, force: true }); }); - // --- Nombres válidos --- - it("acepta nombres válidos simples", () => { const result = validateProjectName("my-app"); expect(result.ok).toBe(true); @@ -138,8 +155,6 @@ describe("validateProjectName", () => { expect(result.ok).toBe(true); }); - // --- Nombres inválidos --- - it("rechaza nombre vacío", () => { const result = validateProjectName(""); expect(result.ok).toBe(false); @@ -189,14 +204,11 @@ describe("validateProjectName", () => { } }); - // --- Edge cases de file system --- - it("rechaza directorio existente NO vacío", async () => { const existingDir = join(tempDir, "existing-project"); await mkdir(existingDir, { recursive: true }); await writeFile(join(existingDir, "file.txt"), "content"); - // Cambiar cwd temporalmente al tempDir const originalCwd = process.cwd(); process.chdir(tempDir); try { diff --git a/src/test/copy-template.test.ts b/src/test/copy-template.test.ts index 764beba..036b497 100644 --- a/src/test/copy-template.test.ts +++ b/src/test/copy-template.test.ts @@ -4,10 +4,10 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { copyTemplate, listFiles } from "../copy-template.js"; -describe("copyTemplate", () => { +describe("copyTemplate — stack next", () => { let tempDir: string; let targetDir: string; - const templateDir = join(process.cwd(), "template"); + const templateDir = join(process.cwd(), "src", "stacks", "next", "template"); beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "create-stack-test-")); @@ -18,8 +18,6 @@ describe("copyTemplate", () => { await rm(tempDir, { recursive: true, force: true }); }); - // ─── Reemplazo de placeholders ─────────────────────── - it("copia el template y reemplaza el nombre del proyecto en package.json", async () => { await copyTemplate(templateDir, targetDir, { projectName: "my-test-app", @@ -57,8 +55,6 @@ describe("copyTemplate", () => { expect(readme).toContain("custom-name"); }); - // ─── Estructura completa de archivos ───────────────── - it("copia todos los archivos del template (verificación exhaustiva)", async () => { await copyTemplate(templateDir, targetDir, { projectName: "my-test-app", @@ -68,8 +64,6 @@ describe("copyTemplate", () => { const templateFiles = await listFiles(templateDir); const targetFiles = await listFiles(targetDir); - // Cada archivo del template debe existir en el destino, - // con excepción de los dotfiles que se renombran (gitignore → .gitignore) const renames: Record = { gitignore: ".gitignore" }; for (const file of templateFiles) { const expectedFile = renames[file] ?? file; @@ -134,7 +128,6 @@ describe("copyTemplate", () => { pm: "npm", }); - // biome.json no debería tener placeholders ni ser modificado const biomeOriginal = await readFile( join(templateDir, "biome.json"), "utf-8", @@ -163,21 +156,70 @@ describe("copyTemplate", () => { }); }); +describe("copyTemplate — stack api", () => { + let tempDir: string; + let targetDir: string; + const templateDir = join(process.cwd(), "src", "stacks", "api", "template"); + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "create-stack-api-test-")); + targetDir = join(tempDir, "my-test-api"); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + it("copia el template API correctamente", async () => { + await copyTemplate(templateDir, targetDir, { + projectName: "my-test-api", + pm: "npm", + }); + + const packageJsonPath = join(targetDir, "package.json"); + const raw = await readFile(packageJsonPath, "utf-8"); + const pkg = JSON.parse(raw); + + expect(pkg.name).toBe("my-test-api"); + }); + + it("copia drizzle.config.ts en template API", async () => { + await copyTemplate(templateDir, targetDir, { + projectName: "my-test-api", + pm: "npm", + }); + + const drizzleConfig = await stat(join(targetDir, "drizzle.config.ts")); + expect(drizzleConfig.isFile()).toBe(true); + }); +}); + // ─── Tests de listFiles helper ─────────────────────────── describe("listFiles", () => { - it("devuelve la lista ordenada de archivos del template", async () => { - const templateDir = join(process.cwd(), "template"); + it("devuelve la lista ordenada de archivos del template next", async () => { + const templateDir = join( + process.cwd(), + "src", + "stacks", + "next", + "template", + ); const files = await listFiles(templateDir); expect(files.length).toBeGreaterThan(0); - // Verificar que la lista está ordenada const sorted = [...files].sort(); expect(files).toEqual(sorted); }); it("incluye dotfiles en la lista", async () => { - const templateDir = join(process.cwd(), "template"); + const templateDir = join( + process.cwd(), + "src", + "stacks", + "next", + "template", + ); const files = await listFiles(templateDir); expect(files).toContain("gitignore"); diff --git a/src/test/git.test.ts b/src/test/git.test.ts index d42e982..5dea459 100644 --- a/src/test/git.test.ts +++ b/src/test/git.test.ts @@ -1,13 +1,8 @@ import { describe, expect, it } from "vitest"; import { execCapture, getGitConfig, isGitAvailable } from "../cli.js"; -// ═══════════════════════════════════════════════════════════ -// Tests de utilidades Git (funciones exportadas de cli.ts) -// ═══════════════════════════════════════════════════════════ - describe("isGitAvailable", () => { it("retorna true cuando git está instalado", async () => { - // En el entorno de desarrollo, git siempre debería estar disponible const result = await isGitAvailable(); expect(result).toBe(true); }); @@ -16,14 +11,13 @@ describe("isGitAvailable", () => { describe("getGitConfig", () => { it("retorna null para una clave de git config inexistente", async () => { const result = await getGitConfig( - "create-stack-next.nonexistent-key-test", + "create-stack.nonexistent-key-test", process.cwd(), ); expect(result).toBeNull(); }); it("retorna un valor string para claves configuradas (si existen)", async () => { - // Solo validamos que la función no crashea y retorna string o null const userName = await getGitConfig("user.name", process.cwd()); expect(typeof userName === "string" || userName === null).toBe(true); }); diff --git a/src/test/integration.test.ts b/src/test/integration.test.ts index eabd83a..63900dc 100644 --- a/src/test/integration.test.ts +++ b/src/test/integration.test.ts @@ -20,7 +20,7 @@ function runCommand(cmd: string, cwd: string): Promise { }); } -describe("Integration: full project generation", () => { +describe("Integration: full project generation — stack next", () => { let tempDir: string; let targetDir: string; @@ -34,26 +34,26 @@ describe("Integration: full project generation", () => { }); it("generates a project that passes lint, typecheck, test and build", async () => { - const templateDir = join(process.cwd(), "template"); + const templateDir = join( + process.cwd(), + "src", + "stacks", + "next", + "template", + ); await copyTemplate(templateDir, targetDir, { projectName: "my-test-app", pm: "npm", }); - // Instalar dependencias await runCommand("npm install", targetDir); - - // Primero formateamos con Biome (el template puede tener archivos sin formatear) await runCommand("npm run lint:fix", targetDir); - - // Luego corremos los checks await runCommand("npm run lint", targetDir); await runCommand("npm run typecheck", targetDir); await runCommand("npm run test:run", targetDir); await runCommand("npm run build", targetDir); - // Si llegamos acá sin errores, el test pasa expect(true).toBe(true); - }, 120_000); // timeout de 2 minutos + }, 120_000); }); diff --git a/src/test/openspec.test.ts b/src/test/openspec.test.ts index 801a12e..b830016 100644 --- a/src/test/openspec.test.ts +++ b/src/test/openspec.test.ts @@ -5,10 +5,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { copyTemplate, listFiles } from "../copy-template.js"; import { runOpenSpecInit } from "../openspec-init.js"; -describe("OpenSpec — estructura en el template", () => { +describe("OpenSpec — estructura en el template next", () => { let tempDir: string; let targetDir: string; - const templateDir = join(process.cwd(), "template"); + const templateDir = join(process.cwd(), "src", "stacks", "next", "template"); beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "csn-openspec-test-")); @@ -87,21 +87,16 @@ describe("runOpenSpecInit", () => { }); it("falla graceful si no hay proyecto válido", async () => { - // Pasamos un directorio inexistente — debería fallar sin crashear await expect( runOpenSpecInit(join(tempDir, "no-existe"), ["claude"]), ).rejects.toThrow(); }); - - // Nota: el test de integración real (correr openspec init en un proyecto - // con internet) está en integration.test.ts como test opcional que - // requiere conexión. }); -describe("OpenSpec — template-api", () => { +describe("OpenSpec — template api", () => { let tempDir: string; let targetDir: string; - const templateDir = join(process.cwd(), "template-api"); + const templateDir = join(process.cwd(), "src", "stacks", "api", "template"); beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "csn-openspec-api-test-")); @@ -133,9 +128,15 @@ describe("OpenSpec — template-api", () => { }); }); -describe("OpenSpec — template app", () => { - it("template app tiene @fission-ai/openspec en devDependencies", async () => { - const templateDir = join(process.cwd(), "template"); +describe("OpenSpec — template app (next)", () => { + it("template next tiene @fission-ai/openspec en devDependencies", async () => { + const templateDir = join( + process.cwd(), + "src", + "stacks", + "next", + "template", + ); const pkg = JSON.parse( await readFile(join(templateDir, "package.json"), "utf-8"), ); diff --git a/tsconfig.json b/tsconfig.json index 942c43a..da3db9c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,5 @@ "types": ["node"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "template"] + "exclude": ["node_modules", "dist", "src/stacks/*/template"] } diff --git a/vitest.config.ts b/vitest.config.ts index e036b1a..0b2e7fe 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,5 +3,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { include: ["src/**/*.{test,spec}.ts"], + exclude: [ + "src/stacks/*/template/**/*.{test,spec}.ts", + "node_modules", + "dist", + ], }, });