Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**.
Expand All @@ -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/` (`<categoría>.md` + `<categoría>-<stack>.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.

Expand Down Expand Up @@ -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#<commit> --update --yes # por commit (el SHA del tag)
```

Expand All @@ -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.
88 changes: 80 additions & 8 deletions install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.*"));
Expand Down Expand Up @@ -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");
Expand All @@ -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}`);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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]);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
54 changes: 54 additions & 0 deletions test/install-update.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading