@dforce2055/dai 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
3
3
  Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
4
4
  (ver `VERSION`).
5
5
 
6
+ ## [0.4.0] — 2026-07-10
7
+
8
+ **Versionado y upgrade** ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)): mantené tu repo al
9
+ día con el CLI sin pisar nada. Las copias scaffoldeadas (skills, constitución, templates) son un
10
+ caché derivable — ahora la máquina te avisa cuando quedaron atrás y las refresca sola.
11
+
12
+ ### Agregado
13
+ - **`dai sync`**: refresca skills, constitución, templates y PR template a la versión del CLI —
14
+ **aditivo** (conserva tu `CLAUDE.md` propio vía bloque delimitado), sin tocar el `.env` ni OpenSpec.
15
+ Detecta los asistentes del repo o acepta `--for`; `--dry-run` muestra qué cambiaría.
16
+ - **`dai doctor` · version-drift**: compara `.dai/VERSION` (scaffold del repo) vs el CLI y avisa con
17
+ color + `⬆️` (misma major → refresh opcional con `dai sync`; major distinta → revisar migración;
18
+ repo más nuevo → actualizar el CLI).
19
+ - **`dai version`**: además de la versión, muestra el estado de drift si estás en un repo con dai
20
+ (chequeo liviano). `dai --version` fuera de un repo dai queda limpio (solo la versión).
21
+ - **`lib/semver.mjs`** (comparación de versiones, cero dependencias).
22
+
23
+ ### Interno
24
+ - **Golden vectors de `ac_hash`**: pineados como inmutables dentro de la línea major — blindan el
25
+ contrato ([ADR-0001](docs/adr/0001-contrato-ac-hash.md)) que hace seguros a los minors/patches y a `dai sync`.
26
+ - **109 tests** (+4 desde 0.3.1: semver ×3, golden vectors ×1).
27
+
28
+ > Diferido a un futuro major (ya diseñado en el ADR-0010): `dai migrate` + `MIGRATION.md` y estampar
29
+ > `schema:` en el `implements.yaml`.
30
+
6
31
  ## [0.3.1] — 2026-07-10
7
32
 
8
33
  Pulido de la experiencia de `dai init` y `dai link-us`, y un ejemplo de US listo para probar.
@@ -122,6 +147,7 @@ ClickUp y Jira Cloud.
122
147
  - Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
123
148
  `files` de npm sin tests ni secretos.
124
149
 
150
+ [0.4.0]: https://github.com/dforce2055/dai/releases/tag/v0.4.0
125
151
  [0.3.1]: https://github.com/dforce2055/dai/releases/tag/v0.3.1
126
152
  [0.3.0]: https://github.com/dforce2055/dai/releases/tag/v0.3.0
127
153
  [0.2.0]: https://github.com/dforce2055/dai/releases/tag/v0.2.0
package/README.md CHANGED
@@ -158,6 +158,7 @@ flowchart TD
158
158
  |---|---|
159
159
  | `dai init [<repo>]` | scaffolder interactivo del repo. Flags: `--for claude\|copilot\|both\|cursor\|all` (asistente, default `all`) · `--pm md\|jira\|clickup` (tracker) · `--openspec` |
160
160
  | `dai install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|cursor\|all]` | instala/actualiza skills de IA en Claude y/o Cursor (`--for all` por defecto). `--force` re-copia aunque ya existan. Ej: `dai install --local . --for cursor --force` · `dai install --global --for all --force` |
161
+ | `dai sync [--dry-run] [--for <asistentes>]` | **refresca** skills, constitución, templates y PR template a la versión del CLI — **aditivo** (no pisa tu `CLAUDE.md`), no toca el `.env` ni OpenSpec. Detecta los asistentes del repo o pasás `--for`. `--dry-run` muestra qué cambiaría ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md)) |
161
162
  | `dai publish <us.md>` | crea la US en el tracker (Jira/ClickUp/md) desde un `.md` y devuelve el key. Es el fallback del MCP para publicar sin el asistente |
162
163
  | `dai link-us <ID> [--us <md>]` | crea branch + `implements.yaml`; sin `--us` trae la US del tracker |
163
164
  | `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
@@ -168,7 +169,14 @@ flowchart TD
168
169
  | `dai done [--base main] [--force]` | cierra la US: vuelve a la base, `fetch --prune` + `pull`, y borra la branch local **si está mergeada** (chequeo estricto; `--force` la borra igual). Redes: no estar en la base, sin cambios sueltos, sin commits sin pushear |
169
170
  | `dai forge comment <ref> --body-file <f>` · `dai forge pr <ref>` | comentar / leer una PR/MR (GitHub/GitLab) |
170
171
  | `dai ac-hash <us.md>` | calcula el hash de los criterios de aceptación de una US |
171
- | `dai doctor` · `dai docs <dest>` · `dai --version` | diagnóstico del entorno · copiar la doc · versión |
172
+ | `dai doctor` · `dai docs <dest>` · `dai version` | diagnóstico del entorno (incluye **version-drift** del scaffold) · copiar la doc · versión (`dai version` avisa si tu repo quedó atrás) |
173
+
174
+ > **🆕 Mantené tu repo al día — `dai sync`.** Las skills, la constitución y los templates son un
175
+ > *caché derivable* del CLI. Cuando actualizás `dai` (`npm i -g @dforce2055/dai`), **`dai doctor` y
176
+ > `dai version` te avisan solos** si tu scaffold quedó atrás — con color y un `⬆️` —, y **`dai sync`**
177
+ > lo refresca: **aditivo** (conserva tu `CLAUDE.md` propio), sin tocar el `.env` ni OpenSpec. Probá sin
178
+ > riesgo con `dai sync --dry-run`. El versionado es semver: patch/minor no rompen nada; solo un major
179
+ > pediría migración. ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md))
172
180
 
173
181
  Skills (se invocan en el asistente): `/doc-to-backlog` · `/grill-intent` · `/grill-epic` · `/grill-user-story` · `/link-us` ·
174
182
  `/tdd` · `/dai-review`. Config del tracker (`md`\|`jira`\|`clickup`) y tokens: en `.env` —
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.4.0
package/cli/dai.mjs CHANGED
@@ -28,6 +28,7 @@ import { parsePrRef, getPR, postComment } from "./lib/forge-api.mjs";
28
28
  import { composePrBody, prTitle, forgeTool } from "./lib/pr.mjs";
29
29
  import { dirsEqual } from "./lib/fsutil.mjs";
30
30
  import { parseFlags, parseAssistants, isAssistantToken } from "./lib/args.mjs";
31
+ import { versionDrift } from "./lib/semver.mjs";
31
32
  import { skillToPrompt, skillToCursor, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore } from "./lib/bootstrap.mjs";
32
33
 
33
34
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -39,6 +40,10 @@ function fail(msg, code = 1) { process.stderr.write("dai: " + msg + "\n"); proce
39
40
  const ok = (m) => process.stdout.write(`✓ ${m}\n`);
40
41
  const info = (m) => process.stdout.write(`› ${m}\n`);
41
42
  const warn = (m) => process.stdout.write(`⚠ ${m}\n`);
43
+ // Color ANSI mínimo — solo si es TTY y no está NO_COLOR (así no ensucia pipes/CI).
44
+ const _color = process.stdout.isTTY && !process.env.NO_COLOR;
45
+ const paint = (code, m) => (_color ? `\x1b[${code}m${m}\x1b[0m` : m);
46
+ const C = { y: (m) => paint("33", m), r: (m) => paint("31", m), cy: (m) => paint("36", m), b: (m) => paint("1", m) };
42
47
  const ROOT = join(HERE, ".."); // raíz del paquete dai (cli/ está adentro)
43
48
  const CLAUDE_SKILLS_DIR = process.env.CLAUDE_SKILLS_DIR || join(homedir(), ".claude", "skills");
44
49
  const CURSOR_SKILLS_DIR = process.env.CURSOR_SKILLS_DIR || join(homedir(), ".cursor", "skills");
@@ -677,6 +682,113 @@ function cmdDocs(dest) {
677
682
  ok(`documentación copiada a ${dest}`);
678
683
  }
679
684
 
685
+ // ── sync: refresca las copias scaffoldeadas a la versión del CLI (ADR-0010) ───
686
+ // Las copias (skills, constitución, templates, PR template) son un CACHÉ derivable
687
+ // del CLI: `dai sync` las re-genera a la versión instalada, aditivo (no pisa la
688
+ // constitución propia del proyecto). NO toca el `.env` ni OpenSpec. Opt-in.
689
+ function cmdSync(repo, opts) {
690
+ repo = repo || ".";
691
+ if (!existsSync(repo)) fail(`no existe el directorio: ${repo}`, 2);
692
+ const daiDir = join(repo, ".dai");
693
+ if (!existsSync(daiDir)) fail("este repo no tiene dai (falta .dai/). Corré `dai init` primero.", 2);
694
+
695
+ // Asistentes: --for override, o detectar los que ya están en el repo.
696
+ let want;
697
+ if (typeof opts.for === "string") {
698
+ try { want = parseAssistants(opts.for); } catch (e) { fail(`--for ${e.message}`); }
699
+ } else {
700
+ want = {
701
+ claude: existsSync(join(repo, ".claude", "skills")),
702
+ copilot: existsSync(join(repo, ".github", "prompts")),
703
+ cursor: existsSync(join(repo, ".cursor", "skills")),
704
+ };
705
+ if (!want.claude && !want.copilot && !want.cursor)
706
+ fail("no detecté asistentes instalados (.claude/.cursor/.github/prompts). Pasá --for.", 2);
707
+ }
708
+
709
+ const cliV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
710
+ const repoV = existsSync(join(daiDir, "VERSION")) ? readFileSync(join(daiDir, "VERSION"), "utf8").trim() : "?";
711
+ const dry = !!opts.dryRun;
712
+ const forStr = [want.claude && "claude", want.copilot && "copilot", want.cursor && "cursor"].filter(Boolean).join("+");
713
+ info(`dai sync — ${repoV} → v${cliV} · asistentes: ${forStr}${dry ? " [dry-run]" : ""}`);
714
+
715
+ const skillsSrc = join(ROOT, "skills");
716
+ const skills = readdirSync(skillsSrc).filter((n) => statSync(join(skillsSrc, n)).isDirectory());
717
+ const step = (label, fn) => { if (dry) info(`[dry-run] ${label}`); else { fn(); ok(label); } };
718
+
719
+ // 1. .dai/ (templates + governance) + VERSION
720
+ step(`.dai/ moldes + governance → v${cliV}`, () => {
721
+ for (const sub of ["templates", "governance"]) {
722
+ const src = join(ROOT, sub);
723
+ if (existsSync(src)) { mkdirSync(join(daiDir, sub), { recursive: true }); cpSync(src, join(daiDir, sub), { recursive: true }); }
724
+ }
725
+ writeFileSync(join(daiDir, "VERSION"), readFileSync(join(ROOT, "VERSION"), "utf8"));
726
+ });
727
+
728
+ // 2. PR template
729
+ step(".github/ pull_request_template.md", () => {
730
+ mkdirSync(join(repo, ".github"), { recursive: true });
731
+ cpSync(join(ROOT, "templates", "pull-request.md"), join(repo, ".github", "pull_request_template.md"));
732
+ });
733
+
734
+ // 3. skills + constitución por asistente (aditivo: upsertBlock no pisa lo del proyecto)
735
+ if (want.claude) step(`Claude: .claude/skills/ (${skills.length}) + CLAUDE.md (bloque dai)`, () => {
736
+ const dir = join(repo, ".claude", "skills"); mkdirSync(dir, { recursive: true });
737
+ for (const name of skills) cpSync(join(skillsSrc, name), join(dir, name), { recursive: true });
738
+ const cPath = join(repo, "CLAUDE.md"), cCur = existsSync(cPath) ? readFileSync(cPath, "utf8") : "";
739
+ writeFileSync(cPath, upsertBlock(cCur, constitution("claude")));
740
+ });
741
+ if (want.copilot) step(`Copilot: .github/prompts/ (${skills.length}) + copilot-instructions.md`, () => {
742
+ const pdir = join(repo, ".github", "prompts"); mkdirSync(pdir, { recursive: true });
743
+ for (const name of skills) writeFileSync(join(pdir, `${name}.prompt.md`), skillToPrompt(readFileSync(join(skillsSrc, name, "SKILL.md"), "utf8")));
744
+ const ciPath = join(repo, ".github", "copilot-instructions.md"), ciCur = existsSync(ciPath) ? readFileSync(ciPath, "utf8") : "";
745
+ writeFileSync(ciPath, upsertBlock(ciCur, constitution("copilot")));
746
+ });
747
+ if (want.cursor) step(`Cursor: .cursor/skills/ (${skills.length}) + .cursor/rules/dai-constitution.mdc`, () => {
748
+ const dir = join(repo, ".cursor", "skills"); mkdirSync(dir, { recursive: true });
749
+ for (const name of skills) {
750
+ const src = join(skillsSrc, name), target = join(dir, name);
751
+ cpSync(src, target, { recursive: true });
752
+ writeFileSync(join(target, "SKILL.md"), skillToCursor(readFileSync(join(src, "SKILL.md"), "utf8")));
753
+ }
754
+ mkdirSync(join(repo, ".cursor", "rules"), { recursive: true });
755
+ writeFileSync(join(repo, ".cursor", "rules", "dai-constitution.mdc"), constitutionCursorRule());
756
+ });
757
+
758
+ // 4. .gitignore: versiona los artefactos, deja fuera solo lo personal
759
+ const giPath = join(repo, ".gitignore");
760
+ const gi = reconcileGitignore(existsSync(giPath) ? readFileSync(giPath, "utf8") : "", want);
761
+ if (gi.changed) step(".gitignore ajustado (artefactos versionados; settings.local.json fuera)",
762
+ () => writeFileSync(giPath, gi.text.endsWith("\n") ? gi.text : gi.text + "\n"));
763
+
764
+ if (dry) info("dry-run: nada escrito. Quitá --dry-run para aplicar.");
765
+ else { process.stdout.write("\n"); ok(`sync completo — .dai/ ahora en v${cliV}`); process.stdout.write(" (El .env y OpenSpec no se tocan: OpenSpec se actualiza aparte con `openspec`.)\n"); }
766
+ }
767
+
768
+ // Imprime el estado de version-drift del scaffold (ADR-0010) con color + ícono.
769
+ // Reutilizado por `dai doctor` y `dai version`. Devuelve el estado, o null si el
770
+ // directorio no tiene dai (.dai/VERSION). No imprime nada en ese caso.
771
+ function reportDrift(repo = process.cwd()) {
772
+ const vf = join(repo, ".dai", "VERSION");
773
+ if (!existsSync(vf)) return null;
774
+ const repoV = readFileSync(vf, "utf8").trim();
775
+ const cliV = readFileSync(join(ROOT, "VERSION"), "utf8").trim();
776
+ const status = versionDrift(repoV, cliV);
777
+ switch (status) {
778
+ case "current":
779
+ ok(`.dai/ al día con el CLI (v${repoV})`); break;
780
+ case "minor-behind":
781
+ process.stdout.write(`${C.b(C.y("⬆️ actualización disponible"))} — CLI ${C.b("v" + cliV)}, tu repo ${repoV}. Actualizá con ${C.cy("dai sync")} · probá con ${C.cy("dai sync --dry-run")}\n`); break;
782
+ case "major-behind":
783
+ process.stdout.write(`${C.b(C.r("⚠️ cambio MAYOR"))} — CLI ${C.b("v" + cliV)}, tu repo ${repoV}. Revisá el CHANGELOG/MIGRATION antes de ${C.cy("dai sync")}\n`); break;
784
+ case "cli-behind":
785
+ process.stdout.write(`${C.b(C.y("⚠️ CLI atrasado"))} — tu repo se scaffoldeó con v${repoV}, más nuevo que tu CLI (v${cliV}). Actualizá el CLI: ${C.cy("npm i -g @dforce2055/dai")}\n`); break;
786
+ default:
787
+ warn(`.dai/ VERSION ilegible: '${repoV}'`);
788
+ }
789
+ return status;
790
+ }
791
+
680
792
  // ── doctor: diagnóstico ───────────────────────────────────────────────────────
681
793
  function cmdDoctor() {
682
794
  loadEnv();
@@ -727,11 +839,15 @@ function cmdDoctor() {
727
839
  process.env.DAI_CLICKUP_LIST_ID ? ok(`lista=${process.env.DAI_CLICKUP_LIST_ID} (para dai publish)`)
728
840
  : warn("DAI_CLICKUP_LIST_ID vacío — solo hace falta para `dai publish` (crear tareas)");
729
841
  }
842
+
843
+ // ── version-drift del scaffold vs el CLI (ADR-0010) ──────────────────────────
844
+ if (existsSync(join(process.cwd(), ".dai", "VERSION"))) { info("versión del scaffold:"); reportDrift(); }
730
845
  }
731
846
 
732
847
  // ── version ───────────────────────────────────────────────────────────────────
733
848
  function cmdVersion() {
734
849
  process.stdout.write(`dai v${readFileSync(join(HERE, "..", "VERSION"), "utf8").trim()}\n`);
850
+ reportDrift(); // en un repo con dai, avisa si el scaffold está atrasado (ADR-0010)
735
851
  }
736
852
 
737
853
  let [cmd, ...rest] = process.argv.slice(2);
@@ -750,6 +866,7 @@ switch (cmd) {
750
866
  case "done": cmdDone(opts); break;
751
867
  case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break;
752
868
  case "init": cmdInit(pos[0], opts).catch((e) => fail(String(e.message))); break;
869
+ case "sync": cmdSync(pos[0], opts); break;
753
870
  case "docs": cmdDocs(pos[0]); break;
754
871
  case "doctor": cmdDoctor(); break;
755
872
  case "version": cmdVersion(); break;
@@ -773,6 +890,7 @@ switch (cmd) {
773
890
  " --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
774
891
  " ej: --for claude,cursor · --for copilot · --for all\n" +
775
892
  " --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
893
+ " sync [<repo>] [--dry-run] [--for <asistentes>] refresca skills/constitución/templates a la versión del CLI (aditivo; no toca .env ni OpenSpec)\n" +
776
894
  " docs <destino> documentación conceptual → <destino>\n" +
777
895
  " doctor diagnóstico del entorno\n\n" +
778
896
  " (config: .env — ver .env.example)\n"
@@ -0,0 +1,30 @@
1
+ // dai · comparación mínima de versiones semver (X.Y.Z). Cero dependencias.
2
+ // Se usa para el chequeo de version-drift (`dai doctor`) y `dai sync` (ADR-0010).
3
+
4
+ export function parseVersion(v) {
5
+ const m = String(v ?? "").trim().match(/^(\d+)\.(\d+)\.(\d+)/);
6
+ return m ? { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) } : null;
7
+ }
8
+
9
+ // -1 / 0 / 1 (a<b / a==b / a>b), o null si alguna no parsea.
10
+ export function compareVersions(a, b) {
11
+ const pa = parseVersion(a), pb = parseVersion(b);
12
+ if (!pa || !pb) return null;
13
+ const d = (pa.major - pb.major) || (pa.minor - pb.minor) || (pa.patch - pb.patch);
14
+ return d < 0 ? -1 : d > 0 ? 1 : 0;
15
+ }
16
+
17
+ // Estado del scaffold del repo (repoV, de `.dai/VERSION`) frente al CLI instalado (cliV):
18
+ // current → iguales
19
+ // minor-behind → CLI adelante, MISMA major (refresh opcional con `dai sync`, nada roto)
20
+ // major-behind → CLI adelante, major DISTINTA (revisar CHANGELOG/MIGRATION antes)
21
+ // cli-behind → el repo se scaffoldeó con una versión más nueva que el CLI (actualizá el CLI)
22
+ // unknown → alguna versión no parseable
23
+ export function versionDrift(repoV, cliV) {
24
+ const r = parseVersion(repoV), c = parseVersion(cliV);
25
+ if (!r || !c) return "unknown";
26
+ const cmp = compareVersions(cliV, repoV);
27
+ if (cmp === 0) return "current";
28
+ if (cmp < 0) return "cli-behind";
29
+ return c.major > r.major ? "major-behind" : "minor-behind";
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforce2055/dai",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Metodología de desarrollo asistido por IA — CLI de acciones deterministas (trazabilidad QUÉ↔CÓMO).",
5
5
  "repository": { "type": "git", "url": "git+https://github.com/dforce2055/dai.git" },
6
6
  "homepage": "https://dforce2055.github.io/dai/",