From 4b68f91d3953f8c7f3e0e7d77d34f69dd96da02e Mon Sep 17 00:00:00 2001 From: lacasoft Date: Sun, 18 Oct 2026 23:00:38 +0200 Subject: [PATCH] fix: proyecto Node creado desde cero (lint-staged, node_modules, versiones exactas) + v3.0.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salió de instalar el kit en un proyecto TypeScript recién creado con npm init -y + npx tsc --init: - El pre-commit corría lint-staged sin configuración y fallaba en cada commit. Ahora se genera .lintstagedrc.mjs con lo que el proyecto ya tiene (prettier, eslint, tsc --noEmit); sin nada que revisar no se instala lint-staged y se retira el pre-commit de husky init. - node_modules/ no quedaba en .gitignore. - husky y compañía se instalaban con ^ y sin cooldown, contra §6.1: ahora -E y --before (7 días). - El resumen avisa del choque verbatimModuleSyntax + CommonJS que rompe todo import/export en un proyecto TS nuevo. Release v3.0.1: versión, CHANGELOG y pins del README/CONTRIBUTING. --- CHANGELOG.md | 13 ++++++ CONTRIBUTING.md | 2 +- README.md | 10 ++-- install.js | 88 ++++++++++++++++++++++++++++++++---- package.json | 2 +- test/install-update.test.cjs | 54 ++++++++++++++++++++++ 6 files changed, 155 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42269d5..fb76854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ Formato basado en [Keep a Changelog](https://keepachangelog.com/) y [SemVer](htt ## [No publicado] +## [3.0.1] - 2026-10-18 + +Correcciones salidas de instalar el kit en un proyecto TypeScript creado desde cero (`npm init -y` + `npx tsc --init`). + +### Fixed +- **El primer commit fallaba.** El instalador configuraba el `pre-commit` con lint-staged pero no escribía su configuración, y lint-staged sin configuración sale con error ("could not find any valid configuration"): ningún commit pasaba. Ahora genera `.lintstagedrc.mjs` con lo que el proyecto ya tiene (prettier, eslint y `tsc --noEmit` si hay `tsconfig.json`, en ese orden y sin correr a la vez). Si no hay nada que revisar, no instala lint-staged y retira el `pre-commit` que deja `husky init` (corre `npm test`, y el test por defecto de `npm init` sale con error). Una configuración de lint-staged existente se respeta. +- **`node_modules/` no quedaba ignorado.** `npm init` no crea `.gitignore` y el kit solo añadía sus entradas, pese a instalar devDeps: el primer `git add .` metía las dependencias al repo. Ahora se añade `node_modules/` en proyectos Node. +- **El instalador incumplía su propia §6.1.** husky, lint-staged y commitlint se instalaban con rangos `^` y sin filtro de antigüedad. Ahora con versión exacta (`-E`) y solo versiones con 7+ días publicadas (`--before`). + +### Added +- **Aviso en el resumen** cuando `tsconfig.json` tiene `verbatimModuleSyntax` con `module: nodenext` (lo que genera `tsc --init` desde TypeScript 5.9) y `package.json` está en CommonJS (lo que deja `npm init`): en esa combinación ningún `import`/`export` compila (TS1286/TS1295). Le pasa a todo proyecto TypeScript nuevo; el aviso dice el arreglo (`"type": "module"`). +- 5 tests (96 en total). Los del flujo de husky corren en dry-run, sin red. + ## [3.0.0] - 2026-10-17 Estándares de seguridad y calidad al día (OWASP Top 10:2025, ASVS 5.0, CWE Top 25 2025, listas OWASP de LLM y agentes 2026, ISO/IEC 25010:2023), memoria y contexto con las capacidades nativas de Claude Code, monorepos multi-stack con reglas por carpeta, y fuera Ruflo (claude-flow). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c128ecb..dccabb0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ Borra el archivo de `shared/.claude/` sin más: en `--update`, el instalador ret - SemVer en `package.json` + entrada en `CHANGELOG.md`. - Como se distribuye por `npx github:`, los proyectos toman la rama por defecto: **`master` siempre debe estar verde** (CI en cada PR). Usa ramas `feat/…`, `fix/…` y PRs. -- Para fijar versiones en un proyecto concreto: `npx github:lacasoft/dev-starter-kit#v3.0.0`. +- Para fijar versiones en un proyecto concreto: `npx github:lacasoft/dev-starter-kit#v3.0.1`. ## Probar localmente diff --git a/README.md b/README.md index b8e520d..c02afae 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Dentro de **cualquier proyecto** (nuevo o heredado), sin clonar nada: ```bash npx -y github:lacasoft/dev-starter-kit # interactivo npx -y github:lacasoft/dev-starter-kit --yes --all # desatendido (todo) -npx -y github:lacasoft/dev-starter-kit#v3.0.0 # fijar una versión (recomendado) +npx -y github:lacasoft/dev-starter-kit#v3.0.1 # fijar una versión (recomendado) ``` Toma siempre la última versión de `master` (o el tag indicado). Repo **público (MIT)**. @@ -60,10 +60,12 @@ El instalador: 2. Hace **backup** de tu `.claude/` si existe. 3. Aplica la **capa base** (`shared/.claude` → `.claude/`): agentes, skills, helpers, settings, comandos. 4. Scaffolda **`CLAUDE.project.md`**, compone **`./CLAUDE.md`** = `@CLAUDE.project.md` + `@.claude/CLAUDE.base.md` (bloque gestionado idempotente) y escribe los overlays del stack como **reglas** en `.claude/rules/kit/` (`.md` + `-.md`). -5. Actualiza **`.gitignore`** (memoria/runtime/backups/secretos), idempotente. Los ejemplos de env (`.env.example`, `.env.sample`, `.env.template`) quedan **versionados**: van negados después de `.env.*`. +5. Actualiza **`.gitignore`** (memoria/runtime/backups/secretos), idempotente. Los ejemplos de env (`.env.example`, `.env.sample`, `.env.template`) quedan **versionados**: van negados después de `.env.*`. En un proyecto Node añade también `node_modules/` (`npm init` no crea `.gitignore` y el kit instala devDeps). 6. **Avisa de restos de Ruflo** (claude-flow) si los encuentra: servidores en `.mcp.json`, `.claude-flow/`, `.swarm/`, archivos en `.claude/` o instrucciones en `CLAUDE.md`. Solo los enumera con el comando para quitarlos; no borra nada que no instaló el kit. 7. (opcional) Instala **agentes/skills externos** curados vía `claude-code-templates` y deps npm por stack. - Los **git hooks** (husky + lint-staged + commitlint) se omiten si el directorio no es un repositorio git: `npx husky init` fallaría y dejaría las devDeps instaladas para nada. + - Se instalan con **versión exacta** y solo versiones con 7+ días publicadas (`npm install -E --before=…`), la misma regla §6.1 que el kit exige al agente. + - El `pre-commit` corre lint-staged con lo que el proyecto **ya tiene** (prettier, eslint, `tsc --noEmit` si hay `tsconfig.json`) y se escribe `.lintstagedrc.mjs`. Si no hay nada que revisar, no se instala lint-staged ni se deja `pre-commit` (sin configuración bloquearía todos los commits); el mensaje se valida igual con commitlint. Una configuración de lint-staged existente se respeta. 8. Indica los **plugins de Claude Code** a añadir (shared + por-stack), incluido el de **code intelligence (LSP)** del stack: go-to-definition, referencias y errores de tipos en vivo para el agente. El binario del language server lo instalas tú; el comando exacto sale en pantalla. 9. Imprime un **resumen del proyecto**: stack y lenguaje, framework, paquetes, motores de datos, volumen de código, tests, calidad, entrega (CI/contenedores), git y entorno. Lo marcado con ⚠️ es un hueco frente a la baseline del kit. @@ -303,7 +305,7 @@ recibir la copia vieja. La solución buena es la misma que exige la baseline §6 versión** — y de paso hace la instalación reproducible: ```bash -npx -y github:lacasoft/dev-starter-kit#v3.0.0 --update --yes # por tag +npx -y github:lacasoft/dev-starter-kit#v3.0.1 --update --yes # por tag npx -y github:lacasoft/dev-starter-kit# --update --yes # por commit (el SHA del tag) ``` @@ -324,4 +326,4 @@ Repo **público (MIT)** distribuido por `npx github:` → la rama **`master` deb (dry-run) y en un monorepo multi-stack, en cada PR. La lista sale de `scripts/stacks.cjs`, que la deriva de `install.js`. - **Cómo añadir** agentes, skills, stacks o externos: ver [CONTRIBUTING.md](CONTRIBUTING.md). - **Versionado**: SemVer en `package.json` + `CHANGELOG.md`, con tag anotado por versión - (`v3.0.0`). Instala siempre pineando (`#v3.0.0`): es reproducible y evita la cache de npx. + (`v3.0.1`). Instala siempre pineando (`#v3.0.1`): es reproducible y evita la cache de npx. diff --git a/install.js b/install.js index b8c2b6c..075883d 100644 --- a/install.js +++ b/install.js @@ -621,7 +621,10 @@ function ensureGitignore() { const gi = path.join(CWD, ".gitignore"); const cur = fs.existsSync(gi) ? fs.readFileSync(gi, "utf8") : ""; const have = new Set(cur.split(/\r?\n/).map((l) => l.trim())); - const add = GITIGNORE.filter((n) => !have.has(n)); + // `npm init` no crea .gitignore y el propio kit instala devDeps: sin esto, el primer + // `git add .` mete node_modules/ al repo. + const node = fs.existsSync(path.join(CWD, "package.json")) && !have.has("node_modules") && !have.has("/node_modules") ? ["node_modules/"] : []; + const add = [...node, ...GITIGNORE].filter((n) => !have.has(n)); // Si acabamos de añadir ".env.*", reemitimos las negaciones aunque ya existan más arriba: // colocadas antes del patrón no harían nada, y la línea duplicada es inocua. const keep = GITIGNORE_KEEP.filter((n) => !have.has(n) || add.includes(".env.*")); @@ -738,6 +741,40 @@ function isGitRepo() { } } +// Configuración de lint-staged a partir de lo que el proyecto YA tiene (prettier, eslint, +// tsconfig). Sin ninguna tarea devuelve null: un pre-commit con lint-staged sin configuración +// falla en CADA commit ("could not find any valid configuration"), que es peor que no tenerlo. +// Una sola entrada por tipo de archivo para que los comandos corran en orden y no a la vez. +function lintStagedConfig(deps) { + const code = []; + if (deps.prettier) code.push("`prettier --write ${q(files)}`"); + if (deps.eslint) code.push("`eslint --fix ${q(files)}`"); + // tsc revisa el proyecto entero, no archivo por archivo: un error de tipos bloquea el commit. + if (fs.existsSync(path.join(CWD, "tsconfig.json"))) code.push('"tsc --noEmit"'); + const usesFiles = Boolean(deps.prettier || deps.eslint); + const entries = []; + if (code.length === 1 && !usesFiles) entries.push(` "*.{js,jsx,mjs,cjs,ts,tsx}": () => ${code[0]},`); + else if (code.length) entries.push(` "*.{js,jsx,mjs,cjs,ts,tsx}": (files) => [${code.join(", ")}],`); + if (deps.prettier) entries.push(' "*.{json,md,css,scss,yml,yaml}": "prettier --write",'); + if (!entries.length) return null; + return [ + "// lint-staged: lo que corre sobre los archivos en stage antes de cada commit (lo generó dev-starter-kit).", + ...(usesFiles ? ["const q = (files) => files.map((f) => JSON.stringify(f)).join(\" \");", ""] : []), + "export default {", + ...entries, + "};", + "", + ].join("\n"); +} +function hasLintStagedConfig(pkg) { + if (pkg["lint-staged"]) return true; + try { + return fs.readdirSync(CWD).some((f) => /^(\.lintstagedrc.*|lint-staged\.config\..+)$/.test(f)); + } catch (_) { + return false; + } +} + // Ofrece tooling de git que la baseline exige (husky + lint-staged + commitlint + plantilla de PR). async function maybeSetupHusky() { const pkgPath = path.join(CWD, "package.json"); @@ -754,23 +791,46 @@ async function maybeSetupHusky() { console.log(" → Omitido: esto no es un repositorio git. Ejecuta `git init` y relanza el instalador."); return; } + const lintStaged = hasLintStagedConfig(pkg) ? "existente" : lintStagedConfig(deps); + console.log( + lintStaged === "existente" + ? " pre-commit: lint-staged con la configuración que ya tiene el proyecto." + : lintStaged + ? ` pre-commit: lint-staged → ${[deps.prettier && "prettier", deps.eslint && "eslint", fs.existsSync(path.join(CWD, "tsconfig.json")) && "tsc --noEmit"].filter(Boolean).join(" + ")}.` + : " pre-commit: nada que revisar todavía (sin prettier, eslint ni tsconfig). Solo se valida el mensaje del commit; relanza cuando tengas linter." + ); if (!(await confirm("¿Configurarlos ahora? (instala devDeps)", FLAGS.all))) { console.log(" → Omitido. Hazlo luego o la regla aplica solo 'si el repo los tiene'."); return; } - if (run("npm install -D husky lint-staged @commitlint/cli @commitlint/config-conventional") && run("npx husky init")) { + // Baseline §6.1 aplicada al propio instalador: versiones exactas (-E) y solo versiones con al + // menos 7 días publicadas (--before), que es el plazo en que se suelen retirar los paquetes + // comprometidos. + const cooldown = new Date(Date.now() - 7 * 864e5).toISOString().slice(0, 10); + const paquetes = ["husky", ...(lintStaged ? ["lint-staged"] : []), "@commitlint/cli", "@commitlint/config-conventional"]; + if (run(`npm install -D -E --before=${cooldown} ${paquetes.join(" ")}`) && run("npx --no-install husky init")) { if (!FLAGS.dryRun) { try { + const creados = ["commitlint.config.cjs", ".husky/commit-msg"]; fs.writeFileSync(path.join(CWD, "commitlint.config.cjs"), 'module.exports = { extends: ["@commitlint/config-conventional"] };\n'); fs.writeFileSync(path.join(CWD, ".husky", "commit-msg"), 'npx --no-install commitlint --edit "$1"\n'); - fs.writeFileSync(path.join(CWD, ".husky", "pre-commit"), "npx --no-install lint-staged\n"); + if (lintStaged) { + fs.writeFileSync(path.join(CWD, ".husky", "pre-commit"), "npx --no-install lint-staged\n"); + creados.push(".husky/pre-commit"); + if (lintStaged !== "existente") { + fs.writeFileSync(path.join(CWD, ".lintstagedrc.mjs"), lintStaged); + creados.push(".lintstagedrc.mjs"); + } + } else { + // husky init deja un pre-commit con `npm test`, y el test por defecto de `npm init` sale + // con error: bloquearía todos los commits de un proyecto recién creado. + fs.rmSync(path.join(CWD, ".husky", "pre-commit"), { force: true }); + } const prDir = path.join(CWD, ".github"); fs.mkdirSync(prDir, { recursive: true }); - fs.writeFileSync( - path.join(prDir, "pull_request_template.md"), - "## Summary\n\n## Test plan\n- [ ] \n" - ); - console.log(" ➕ commitlint.config.cjs, .husky/{commit-msg,pre-commit}, .github/pull_request_template.md"); + fs.writeFileSync(path.join(prDir, "pull_request_template.md"), "## Summary\n\n## Test plan\n- [ ] \n"); + creados.push(".github/pull_request_template.md"); + console.log(` ➕ ${creados.join(", ")}`); } catch (e) { console.error(` ⚠️ ${e.message}`); } @@ -876,6 +936,15 @@ function envVars(file) { .filter(Boolean); } const plural = (n, sing, pl) => `${n} ${n === 1 ? sing : pl}`; +// `tsc --init` (TypeScript 5.9+) genera module nodenext + verbatimModuleSyntax, y `npm init` +// deja el paquete en CommonJS: la combinación rechaza todo import/export (TS1286/TS1295). Le +// pasa a cualquier proyecto TypeScript nuevo, así que se avisa en el resumen. +function tsModuleMismatch(pkg) { + const ts = readMaybe(path.join(CWD, "tsconfig.json")).replace(/^\s*\/\/.*$/gm, ""); + if (!/"verbatimModuleSyntax"\s*:\s*true/.test(ts)) return false; + if (!/"module"\s*:\s*"node(next|16|18|20)"/i.test(ts)) return false; + return pkg.type !== "module"; +} function summarizeProject(stacks) { const stackId = stacks[0].id; const monorepo = stacks.length > 1; @@ -894,6 +963,9 @@ function summarizeProject(stacks) { } if (pkg.engines?.node) lang.push(`Node ${pkg.engines.node}`); add("Stack", [describeStacks(stacks), ...lang].join(" · ")); + if (tsModuleMismatch(pkg)) { + add("Módulos", '⚠️ tsconfig con verbatimModuleSyntax y package.json en CommonJS: ningún import/export compila. Pon "type": "module" en package.json'); + } // En monorepo el framework vive en cada paquete, no en el package.json de la raíz. const fw = !monorepo && FRAMEWORK_DEP[stackId] && dep(FRAMEWORK_DEP[stackId]); diff --git a/package.json b/package.json index 3a302d6..dc2e0d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lacasoft/dev-starter-kit", - "version": "3.0.0", + "version": "3.0.1", "private": true, "description": "Base .claude coherente (agentes, skills, helpers, orquestación multiagente nativa) instalable en cualquier proyecto lacasoft (backend/frontend/mobile/blockchain).", "bin": { diff --git a/test/install-update.test.cjs b/test/install-update.test.cjs index 3de56a0..4b177cd 100644 --- a/test/install-update.test.cjs +++ b/test/install-update.test.cjs @@ -301,6 +301,60 @@ test("cambiar de stack retira la regla vieja intacta y respeta la que editaste", fs.rmSync(dir, { recursive: true, force: true }); }); +// ---- proyecto Node creado desde cero (npm init + tsc --init) ---- +// Salió de instalar el kit en un proyecto recién creado: el primer commit fallaba (lint-staged sin +// configuración), node_modules/ no estaba ignorado y los import no compilaban. +function nodeDesdeCero(archivos = {}) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kit-node0-")); + spawnSync("git", ["init", "-q"], { cwd: dir }); + for (const [f, c] of Object.entries({ "package.json": '{"name":"x","type":"commonjs"}', ...archivos })) { + fs.writeFileSync(path.join(dir, f), c); + } + return dir; +} +const TSCONFIG_NUEVO = '{\n "compilerOptions": {\n // "rootDir": "./src",\n "module": "nodenext",\n "verbatimModuleSyntax": true,\n "strict": true\n }\n}\n'; +// --all + --dry-run: recorre el flujo de husky sin instalar nada (la CI no depende de la red). +// Sin --no-external, que también se salta este paso. +const planHusky = (dir) => + spawnSync("node", [INSTALL, "--yes", "--stack", "backend/express", "--all", "--dry-run"], { cwd: dir, input: "", encoding: "utf8" }).stdout; + +test("husky: lint-staged con tsc si hay tsconfig, y dependencias con versión exacta y cooldown", () => { + const dir = nodeDesdeCero({ "tsconfig.json": TSCONFIG_NUEVO }); + const out = planHusky(dir); + assert.match(out, /pre-commit: lint-staged → tsc --noEmit/); + assert.match(out, /npm install -D -E --before=\d{4}-\d{2}-\d{2} husky lint-staged /, "-E y --before aplican §6.1 al propio kit"); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("husky: sin nada que revisar no instala lint-staged (un pre-commit sin config bloquea todos los commits)", () => { + const dir = nodeDesdeCero(); + const out = planHusky(dir); + assert.match(out, /nada que revisar todavía/); + assert.doesNotMatch(out, /npm install[^\n]*lint-staged/); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("husky: respeta una configuración de lint-staged que ya existe", () => { + const dir = nodeDesdeCero({ "package.json": '{"name":"x","lint-staged":{"*.js":"node --check"}}' }); + assert.match(planHusky(dir), /configuración que ya tiene el proyecto/); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test(".gitignore añade node_modules/ en un proyecto Node", () => { + const dir = proyecto(); + assert.ok(fs.readFileSync(path.join(dir, ".gitignore"), "utf8").split("\n").includes("node_modules/")); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("el resumen avisa si tsconfig y package.json no permiten import/export", () => { + const conCommonjs = nodeDesdeCero({ "tsconfig.json": TSCONFIG_NUEVO }); + assert.match(correr(conCommonjs, ["--stack", "backend/express", "--dry-run"]).out, /Módulos\s+⚠️/); + const conModule = nodeDesdeCero({ "package.json": '{"name":"x","type":"module"}', "tsconfig.json": TSCONFIG_NUEVO }); + assert.doesNotMatch(correr(conModule, ["--stack", "backend/express", "--dry-run"]).out, /Módulos/); + fs.rmSync(conCommonjs, { recursive: true, force: true }); + fs.rmSync(conModule, { recursive: true, force: true }); +}); + test("--update nunca toca la memoria ni CLAUDE.project.md", () => { const dir = proyecto(); const mem = path.join(dir, ".claude", "memory", "notas.md");