From db336be23ed93622b4ce0f0961e465a106ec89dd Mon Sep 17 00:00:00 2001 From: lacasoft Date: Sun, 18 Oct 2026 23:14:41 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20--update=20recuerda=20el=20stack=20de=20?= =?UTF-8?q?la=20instalaci=C3=B3n=20anterior=20+=20v3.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit En un proyecto sin framework detectable (TypeScript puro) o en un monorepo, --update --yes fallaba si no se repetía --stack con todas sus carpetas. El manifiesto guarda ahora los stacks; para instalaciones 3.0.0/3.0.1 se deducen de .claude/rules/kit/ (nombre y paths:). Un --stack explícito sigue mandando. Release v3.0.2: versión, CHANGELOG y pins del README/CONTRIBUTING. --- CHANGELOG.md | 6 ++++ CONTRIBUTING.md | 2 +- README.md | 8 ++++-- install.js | 48 +++++++++++++++++++++++++++---- package.json | 2 +- test/install-update.test.cjs | 56 ++++++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb76854..5080d84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Formato basado en [Keep a Changelog](https://keepachangelog.com/) y [SemVer](htt ## [No publicado] +## [3.0.2] - 2026-10-18 + +### Fixed +- **`--update --yes` fallaba en proyectos sin framework detectable.** Un proyecto TypeScript puro (o cualquiera instalado con `--stack` porque la detección no lo reconoce) obligaba a repetir `--stack` en cada actualización; sin él, salía con "No se pudo detectar el stack". En un monorepo había que repetir además todas las carpetas. Ahora el manifiesto guarda los stacks y sus carpetas, y el instalador los reutiliza (pide confirmar; con `--yes` los acepta). Para las instalaciones 3.0.0 y 3.0.1, que no los guardaban, se deducen de `.claude/rules/kit/`: el nombre de cada regla dice el stack y su `paths:` las carpetas. Un `--stack` explícito sigue mandando. +- 4 tests (100 en total). + ## [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`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dccabb0..bad64e5 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.1`. +- Para fijar versiones en un proyecto concreto: `npx github:lacasoft/dev-starter-kit#v3.0.2`. ## Probar localmente diff --git a/README.md b/README.md index c02afae..63a3268 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.1 # fijar una versión (recomendado) +npx -y github:lacasoft/dev-starter-kit#v3.0.2 # fijar una versión (recomendado) ``` Toma siempre la última versión de `master` (o el tag indicado). Repo **público (MIT)**. @@ -261,6 +261,8 @@ cubre lo que usábamos. Si un proyecto lo tenía: npx -y github:lacasoft/dev-starter-kit --update --yes ``` +No hace falta repetir `--stack`: el instalador **recuerda los stacks** de la instalación anterior (y sus carpetas, en un monorepo) y solo pide confirmar. Para cambiarlo, pasa `--stack` de nuevo. + Por defecto el instalador es **aditivo** (añade lo nuevo, no pisa lo existente). Con **`--update`** (alias `--force`) trae además la última versión de agentes, skills y helpers — **sin que pierdas nada tuyo**, que es justo lo que un starter kit debe evitar. @@ -305,7 +307,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.1 --update --yes # por tag +npx -y github:lacasoft/dev-starter-kit#v3.0.2 --update --yes # por tag npx -y github:lacasoft/dev-starter-kit# --update --yes # por commit (el SHA del tag) ``` @@ -326,4 +328,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.1`). Instala siempre pineando (`#v3.0.1`): es reproducible y evita la cache de npx. + (`v3.0.2`). Instala siempre pineando (`#v3.0.2`): es reproducible y evita la cache de npx. diff --git a/install.js b/install.js index 075883d..ad6c910 100644 --- a/install.js +++ b/install.js @@ -292,6 +292,35 @@ function parseStacks(specs) { } const describeStacks = (stacks) => stacks.map((s) => (s.dirs ? `${s.id} (${s.dirs.map((d) => `${d}/`).join(", ")})` : s.id)).join(" · "); +// Stacks con los que se instaló el kit la última vez, para que --update no obligue a repetir +// --stack (en un proyecto sin framework detectable, o en un monorepo con todas sus carpetas). +// Desde 3.0.2 van en el manifiesto; las instalaciones 3.0.0/3.0.1 no los guardaban, pero sus +// reglas de .claude/rules/kit/ llevan el stack en el nombre y las carpetas en `paths:`. +function previousStacks(manifestJson) { + const guardados = (manifestJson && Array.isArray(manifestJson.stacks) ? manifestJson.stacks : []).filter( + (st) => st && STACKS[st.id] && (st.dirs === null || Array.isArray(st.dirs)) + ); + if (guardados.length) return guardados.map((st) => ({ id: st.id, dirs: st.dirs })); + const dir = path.join(CWD, ".claude", RULES_DIR); + let archivos = []; + try { + archivos = fs.readdirSync(dir).filter((f) => f.endsWith(".md")); + } catch (_) { + return null; + } + const deducidos = []; + for (const f of archivos) { + const id = f.replace(/\.md$/, "").replace("-", "/"); + if (!STACKS[id]) continue; // backend.md (común) u otra regla + const fm = readMaybe(path.join(dir, f)).match(/^---\npaths:\n([\s\S]*?)\n---/); + const dirs = fm + ? [...fm[1].matchAll(/^\s*-\s*"(.+)\/\*\*"\s*$/gm)].map((m) => m[1].replace(/\\(.)/g, "$1")) + : null; + deducidos.push({ id, dirs: dirs && dirs.length ? dirs : null }); + } + return deducidos.length ? deducidos : null; +} + const STACKS = { "backend/nestjs": 1, "backend/express": 1, "backend/fastapi": 1, "backend/django": 1, "backend/php": 1, "backend/spring": 1, "backend/dotnet": 1, @@ -1074,8 +1103,18 @@ async function main() { console.log(`║ Dev Starter Kit v${VERSION} — instalador `.slice(0, 45) + "║"); console.log("╚════════════════════════════════════════════╝"); + const claudeDir = path.join(CWD, ".claude"); + const manifestPath = path.join(claudeDir, MANIFEST_FILE); + const prevManifest = readJSON(manifestPath); + const anteriores = FLAGS.stackArgs.length ? null : previousStacks(prevManifest); + let stacks = null; - if (FLAGS.stackArgs.length) { + if (anteriores) { + stacks = anteriores; + console.log(`\n✅ Stack de la instalación anterior: ${describeStacks(stacks)}`); + console.log(" Para cambiarlo: --stack [:carpeta][,…]"); + if (!(await confirm("¿Es correcto?", true))) stacks = null; + } else if (FLAGS.stackArgs.length) { const r = parseStacks(FLAGS.stackArgs); if (r.error) console.log(`⚠️ ${r.error}.`); else { @@ -1099,7 +1138,7 @@ async function main() { if (stacks && !(await confirm("¿Es correcto?", true))) stacks = null; } if (!stacks) { - if (!FLAGS.stackArgs.length) console.log(`\n🔍 ${detectionHint()}`); + if (!FLAGS.stackArgs.length && !anteriores) console.log(`\n🔍 ${detectionHint()}`); console.log("\nStacks disponibles:\n " + Object.keys(STACKS).join("\n ")); const answer = await ask("\nElige stack (category/stack; en monorepo, category/stack:carpeta separados por coma): "); const r = parseStacks((answer || "").split(",").map((x) => x.trim()).filter(Boolean)); @@ -1116,7 +1155,6 @@ async function main() { stacks = r.stacks; } // backup - const claudeDir = path.join(CWD, ".claude"); if (fs.existsSync(claudeDir) && !FLAGS.dryRun) { const bk = `.claude.backup.${Date.now()}`; fs.cpSync(claudeDir, path.join(CWD, bk), { recursive: true }); @@ -1129,8 +1167,6 @@ async function main() { // 1) capa base console.log("\n📄 Aplicando capa base (shared/.claude)..."); - const manifestPath = path.join(claudeDir, MANIFEST_FILE); - const prevManifest = readJSON(manifestPath); if (FLAGS.update) { console.log(" 🔄 Modo actualización: se actualizan los archivos del kit que nadie ha tocado."); console.log( @@ -1152,7 +1188,7 @@ async function main() { const remembered = retireFiles(layer, claudeDir); reportLayer(layer); if (!FLAGS.dryRun) { - fs.writeFileSync(manifestPath, JSON.stringify({ version: VERSION, files: { ...remembered, ...layer.next } }, null, 2) + "\n"); + fs.writeFileSync(manifestPath, JSON.stringify({ version: VERSION, stacks, files: { ...remembered, ...layer.next } }, null, 2) + "\n"); } // 2) Definición del proyecto + CLAUDE.md raíz (añade/actualiza nuestro bloque; respeta lo demás) diff --git a/package.json b/package.json index dc2e0d7..c4ae11f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lacasoft/dev-starter-kit", - "version": "3.0.1", + "version": "3.0.2", "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 4b177cd..796f74d 100644 --- a/test/install-update.test.cjs +++ b/test/install-update.test.cjs @@ -355,6 +355,62 @@ test("el resumen avisa si tsconfig y package.json no permiten import/export", () fs.rmSync(conModule, { recursive: true, force: true }); }); +// ---- --update recuerda el stack ---- +// Un proyecto sin framework detectable (TypeScript puro) se instala con --stack; antes, cada +// --update --yes fallaba con "No se pudo detectar el stack" si no se repetía. +function sinFramework() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "kit-sinfw-")); + fs.writeFileSync(path.join(dir, "package.json"), '{"name":"x"}'); + return dir; +} + +test("--update sin --stack reutiliza el stack de la instalación anterior", () => { + const dir = sinFramework(); + assert.equal(correr(dir, ["--stack", "backend/express"]).status, 0); + assert.deepEqual(leerJSON(path.join(dir, ".claude", ".kit-manifest.json")).stacks, [{ id: "backend/express", dirs: null }]); + const r = correr(dir, ["--update"]); + assert.equal(r.status, 0, r.out); + assert.match(r.out, /Stack de la instalación anterior: backend\/express/); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("instalación 3.0.x sin stacks en el manifiesto: se deducen de .claude/rules/kit/", () => { + const dir = sinFramework(); + correr(dir, ["--stack", "backend/express"]); + const mp = path.join(dir, ".claude", ".kit-manifest.json"); + const m = leerJSON(mp); + delete m.stacks; // así lo dejaban 3.0.0 y 3.0.1 + fs.writeFileSync(mp, JSON.stringify(m)); + const r = correr(dir, ["--update"]); + assert.equal(r.status, 0, r.out); + assert.match(r.out, /instalación anterior: backend\/express/); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("monorepo: --update conserva los stacks y sus carpetas sin repetir --stack", () => { + const dir = sinFramework(); + correr(dir, ["--stack", "backend/nestjs:apps/api,frontend/nextjs:apps/web,frontend/nextjs:apps/admin"]); + const mp = path.join(dir, ".claude", ".kit-manifest.json"); + const m = leerJSON(mp); + delete m.stacks; // también desde las reglas (paths:) + fs.writeFileSync(mp, JSON.stringify(m)); + const antes = regla(dir, "frontend-nextjs.md"); + const r = correr(dir, ["--update"]); + assert.equal(r.status, 0, r.out); + assert.match(r.out, /backend\/nestjs \(apps\/api\/\)/); + assert.equal(regla(dir, "frontend-nextjs.md"), antes, "las rutas de cada regla no cambian"); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +test("--stack explícito gana sobre el stack recordado", () => { + const dir = sinFramework(); + correr(dir, ["--stack", "backend/express"]); + const r = correr(dir, ["--update", "--stack", "backend/fastapi"]); + assert.match(r.out, /Stack indicado: backend\/fastapi/); + assert.deepEqual(leerJSON(path.join(dir, ".claude", ".kit-manifest.json")).stacks, [{ id: "backend/fastapi", dirs: null }]); + fs.rmSync(dir, { 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");