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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
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.1`.
- Para fijar versiones en un proyecto concreto: `npx github:lacasoft/dev-starter-kit#v3.0.2`.

## Probar localmente

Expand Down
8 changes: 5 additions & 3 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.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)**.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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#<commit> --update --yes # por commit (el SHA del tag)
```

Expand All @@ -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.
48 changes: 42 additions & 6 deletions install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <category/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 {
Expand All @@ -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));
Expand All @@ -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 });
Expand All @@ -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(
Expand All @@ -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)
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.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": {
Expand Down
56 changes: 56 additions & 0 deletions test/install-update.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading