@dforce2055/dai 0.9.0 → 0.10.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/{.env.example → .env.dai.example} +3 -3
- package/CHANGELOG.md +41 -0
- package/README.md +36 -25
- package/VERSION +1 -1
- package/cli/dai.mjs +97 -41
- package/cli/lib/bootstrap.mjs +39 -7
- package/cli/lib/env.mjs +12 -0
- package/cli/lib/pm-clickup.mjs +2 -2
- package/cli/lib/pm-jira.mjs +2 -2
- package/cli/lib/skills-source.mjs +8 -0
- package/docs/EJEMPLO-END-TO-END.md +52 -43
- package/docs/MANIFIESTO.md +2 -2
- package/docs/METODOLOGIA.md +20 -15
- package/docs/PROBAR.md +13 -14
- package/docs/SCRUM-CON-IA.md +10 -10
- package/docs/adr/0003-deteccion-y-estampado-son-comandos.md +1 -1
- package/docs/adr/0006-distribucion-y-licencia.md +1 -1
- package/docs/adr/0013-skills-externas-install-from.md +11 -4
- package/docs/adr/0015-jira-corporativo.md +1 -1
- package/docs/adr/0017-env-dai.md +64 -0
- package/docs/adr/README.md +1 -0
- package/docs/detalle/01-refinamiento.md +6 -5
- package/docs/detalle/03-ramas.md +2 -2
- package/docs/detalle/04-tdd.md +15 -9
- package/docs/detalle/06-code-review.md +8 -5
- package/docs/detalle/08-daily.md +1 -1
- package/docs/detalle/README.md +1 -1
- package/docs/glosario.md +2 -2
- package/docs/guias/dev.md +10 -7
- package/docs/guias/index.md +12 -0
- package/docs/guias/lead.md +1 -1
- package/docs/guias/po.md +13 -7
- package/docs/index.md +35 -0
- package/docs/public/favicon.svg +12 -0
- package/docs/public/logo-link.svg +12 -0
- package/docs/public/logo.svg +12 -0
- package/docs/public/tutoriales/clickup-1-settings.png +0 -0
- package/docs/public/tutoriales/clickup-2-api.png +0 -0
- package/docs/public/tutoriales/clickup-3-generate-copy.png +0 -0
- package/docs/public/tutoriales/jira-1-avatar.png +0 -0
- package/docs/public/tutoriales/jira-2-seguridad-tokens.png +0 -0
- package/docs/public/tutoriales/jira-3-crear-token.png +0 -0
- package/docs/public/tutoriales/jira-4-nombre-vencimiento.png +0 -0
- package/docs/public/tutoriales/jira-5-copiar.png +0 -0
- package/docs/tutoriales/claves-ssh.md +93 -0
- package/docs/tutoriales/configurar-git.md +53 -0
- package/docs/tutoriales/index.md +18 -0
- package/docs/tutoriales/instalar-glab.md +74 -0
- package/docs/tutoriales/token-clickup.md +73 -0
- package/docs/tutoriales/token-jira.md +74 -0
- package/package.json +9 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# dai · configuración del adaptador de PM (copiar a .env y completar).
|
|
2
|
-
# El .env
|
|
3
|
-
#
|
|
1
|
+
# dai · configuración del adaptador de PM (copiar a .env.dai y completar).
|
|
2
|
+
# El .env.dai NO se versiona (evita filtrar secretos) y el .env del equipo no se toca
|
|
3
|
+
# (ADR-0017). Este .example SÍ se versiona. Precedencia: shell/CI > .env.dai > .env.
|
|
4
4
|
|
|
5
5
|
# ── Backend del tracker ──────────────────────────────────────────────────────
|
|
6
6
|
# md | jira | clickup
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.10.0] — 2026-07-18
|
|
7
|
+
|
|
8
|
+
**La config de dai deja de vivir en el `.env` del equipo y pasa a un `.env.dai` propio (no
|
|
9
|
+
versionado). Resuelve el caso de las empresas que versionan el `.env` como política: dai no
|
|
10
|
+
toca ese archivo y guarda sus secretos donde git realmente los ignora. Y `dai init` estrena
|
|
11
|
+
una bienvenida con el Sol de Mayo en bloques.**
|
|
12
|
+
|
|
13
|
+
### Agregado
|
|
14
|
+
- **Banner de bienvenida en `dai init`**: el Sol de Mayo de dai en bloques (cuerpo y rayos
|
|
15
|
+
rectos en oro, rayos ondulados en celeste) junto al título, más un preview de lo que se va
|
|
16
|
+
a configurar. Cero dependencias (solo ANSI); degrada a ASCII sin color en no-TTY o con
|
|
17
|
+
`NO_COLOR`.
|
|
18
|
+
- **`dai skills install --from npm:@scope/pkg`** — nueva fuente para skills externas
|
|
19
|
+
([ADR-0013](docs/adr/0013-skills-externas-install-from.md)): además de git URL y path
|
|
20
|
+
local, ahora un **paquete npm**. dai hace `npm install` a un temp (respetando el `.npmrc`
|
|
21
|
+
del repo, así resuelve **registries privados con scope**; `npm pack` no sirve con los
|
|
22
|
+
registries de grupo de GitLab). Es común distribuir skills como paquete npm; antes había
|
|
23
|
+
que materializarlo a mano.
|
|
24
|
+
- **Descripciones en bloque YAML (`|` / `>`) en el frontmatter de las skills.** El parser
|
|
25
|
+
de frontmatter ahora lee bloques literales/plegados multilínea (antes tomaba solo la
|
|
26
|
+
primera línea `|` y el validador lo rechazaba). Es como se escriben las skills reales:
|
|
27
|
+
descripciones ricas con "USAR CUANDO / NO USAR CUANDO" que el agente usa para elegirlas.
|
|
28
|
+
|
|
29
|
+
### Cambiado
|
|
30
|
+
- **`dai init` escribe en `.env.dai` + `.env.dai.example`, no en `.env`/`.env.example`**
|
|
31
|
+
([ADR-0017](docs/adr/0017-env-dai.md)). El `.env` del equipo queda intacto (dai solo lo
|
|
32
|
+
lee). `.env.dai` (secretos) se gitignorea; `.env.dai.example` (plantilla) se versiona.
|
|
33
|
+
En un repo sin `.env`, dai ya no crea uno: es del equipo, no de dai.
|
|
34
|
+
- **El loader lee `.env.dai` y `.env`** con precedencia **shell/CI > `.env.dai` > `.env`**.
|
|
35
|
+
Seguir leyendo `.env` mantiene la compatibilidad: los repos que ya tenían los `DAI_*` ahí
|
|
36
|
+
no se rompen.
|
|
37
|
+
- **`.gitignore`**: dai ignora `.env.dai` (su archivo), no `.env`. Ya no fuerza un ignore
|
|
38
|
+
sobre un archivo que muchas orgs versionan a propósito.
|
|
39
|
+
- Se renombró el `.env.example` del paquete a `.env.dai.example`, y se actualizaron doctor,
|
|
40
|
+
mensajes de init/sync, tutoriales, constitución y README a la nueva convención.
|
|
41
|
+
|
|
42
|
+
### Interno
|
|
43
|
+
- **228 tests** (+4 desde 0.9.0): precedencia de `loadDaiEnv` (shell > `.env.dai` > `.env`),
|
|
44
|
+
compat con `.env`, y sin-archivos. Smokes de `dai init` con y sin `.env` preexistente.
|
|
45
|
+
|
|
6
46
|
## [0.9.0] — 2026-07-17
|
|
7
47
|
|
|
8
48
|
**El review de dai deja de ser un comentario al final del hilo y pasa a ser un review
|
|
@@ -405,6 +445,7 @@ ClickUp y Jira Cloud.
|
|
|
405
445
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
406
446
|
`files` de npm sin tests ni secretos.
|
|
407
447
|
|
|
448
|
+
[0.10.0]: https://github.com/dforce2055/dai/releases/tag/v0.10.0
|
|
408
449
|
[0.9.0]: https://github.com/dforce2055/dai/releases/tag/v0.9.0
|
|
409
450
|
[0.8.2]: https://github.com/dforce2055/dai/releases/tag/v0.8.2
|
|
410
451
|
[0.8.1]: https://github.com/dforce2055/dai/releases/tag/v0.8.1
|
package/README.md
CHANGED
|
@@ -63,10 +63,10 @@ Y después, en el chat del asistente, según lo que tengas:
|
|
|
63
63
|
|
|
64
64
|
> Las skills **te interrogan** hasta que la historia es testeable (nunca inventan
|
|
65
65
|
> requerimientos: los sacan a preguntas), y la **publican en el tracker** que configuraste
|
|
66
|
-
> (`DAI_PM` en el `.env`): por el **MCP** de Jira/ClickUp si está conectado, o con
|
|
66
|
+
> (`DAI_PM` en el `.env.dai`): por el **MCP** de Jira/ClickUp si está conectado, o con
|
|
67
67
|
> **`dai publish <us.md>`** si no (crea el issue vía token). Tú respondes y decides.
|
|
68
68
|
|
|
69
|
-
¿Sin MCP, o
|
|
69
|
+
¿Sin MCP, o quieres publicar a mano? `dai publish` sube el `.md` al tracker y te devuelve el key:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
dai publish historia.md # crea la US → devuelve el key
|
|
@@ -87,7 +87,7 @@ Una vez, preparas el repo:
|
|
|
87
87
|
```bash
|
|
88
88
|
npm i -g @dforce2055/dai
|
|
89
89
|
cd mi-repo && dai init # bootstrap: skills + tracker + OpenSpec + PR template
|
|
90
|
-
# completa el token del tracker en .env (o DAI_PM=md para probar sin credenciales)
|
|
90
|
+
# completa el token del tracker en .env.dai (o DAI_PM=md para probar sin credenciales)
|
|
91
91
|
dai doctor # verifica que todo quedó en su lugar
|
|
92
92
|
```
|
|
93
93
|
|
|
@@ -112,12 +112,12 @@ dai pr --assignee <compañero> # crea la PR precargada y se la asigna a un com
|
|
|
112
112
|
dai stamp # al mergear: estampa la cobertura en el tracker
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
> **Para que `dai pr` cree la PR/MR**
|
|
115
|
+
> **Para que `dai pr` cree la PR/MR** necesitas el CLI del forge instalado y autenticado, una
|
|
116
116
|
> sola vez por máquina:
|
|
117
117
|
> - **GitHub** → [`gh`](https://cli.github.com) · `gh auth login`
|
|
118
118
|
> - **GitLab** → [`glab`](https://gitlab.com/gitlab-org/cli) · `glab auth login --hostname <tu-gitlab>`
|
|
119
119
|
> — en un GitLab self-hosted/corporativo el `--hostname` es **obligatorio**, y el token va con
|
|
120
|
-
> scope `api`. (En Windows, tras instalar `glab` con `winget`,
|
|
120
|
+
> scope `api`. (En Windows, tras instalar `glab` con `winget`, abre una terminal nueva para que
|
|
121
121
|
> tome el PATH.)
|
|
122
122
|
>
|
|
123
123
|
> Sin el CLI del forge, `dai pr` **igual pushea tu branch** y te dice qué instalar — después
|
|
@@ -169,10 +169,10 @@ flowchart TD
|
|
|
169
169
|
| 14 | **Cerrar la US** | `dai done` → vuelve a la base, actualiza y borra la branch local (si está mergeada) | dev |
|
|
170
170
|
|
|
171
171
|
> **Paso 3b (publicar):** el MCP crea el issue interactivamente; `dai publish` necesita el
|
|
172
|
-
> token del tracker en `.env` (Jira además `DAI_JIRA_PROJECT`, ClickUp `DAI_CLICKUP_LIST_ID`).
|
|
172
|
+
> token del tracker en `.env.dai` (Jira además `DAI_JIRA_PROJECT`, ClickUp `DAI_CLICKUP_LIST_ID`).
|
|
173
173
|
>
|
|
174
174
|
> **Paso 7 (OpenSpec):** `dai init` te ofrece instalarlo **e inicializarlo** solo. Si los
|
|
175
|
-
> comandos `/opsx:*` no aparecen en el asistente, reinicia el IDE (se cargan al
|
|
175
|
+
> comandos `/opsx:*` no aparecen en el asistente, reinicia el IDE (se cargan al iniciar).
|
|
176
176
|
>
|
|
177
177
|
> **Paso 11 (PR):** necesita un remoto git (`origin`) y `gh`/`glab` autenticado. Sin remoto,
|
|
178
178
|
> dai no crea la PR (te deja el texto listo igual).
|
|
@@ -183,8 +183,8 @@ flowchart TD
|
|
|
183
183
|
|---|---|
|
|
184
184
|
| `dai init [<repo>]` | scaffolder interactivo del repo. Flags: `--for claude\|copilot\|both\|cursor\|all` (asistente, default `all`) · `--pm md\|jira\|clickup` (tracker) · `--openspec` |
|
|
185
185
|
| `dai skills install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|copilot\|cursor\|all]` | instala/actualiza las skills de dai en Claude, Copilot y/o Cursor (`--for all` por defecto; Copilot global → `~/.copilot/skills`, local → `.github/skills/`). `--force` re-copia. Alias: **`dai install`**. Ej: `dai skills install --local . --for cursor --force` |
|
|
186
|
-
| `dai skills install --from <git-url\|path>[#ref] [--for …]` | instala **skills externas** (por-stack: .NET, Java, …) desde un repo/dir
|
|
187
|
-
| `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)) |
|
|
186
|
+
| `dai skills install --from <git-url\|npm:pkg\|path>[#ref] [--for …]` | instala **skills externas** (por-stack: .NET, Java, …) desde un repo/dir/**paquete npm**, **convertidas para los 3 asistentes**. Self-service, one-off, sin registro; `dai sync` no las toca. Colisión con una skill de dai → salta ([ADR-0013](docs/adr/0013-skills-externas-install-from.md)). Ej: `dai skills install --from github.com/mi-org/net-skills` · `dai skills install --from npm:@mi-org/ui-skills` (usa el `.npmrc` del repo → registries privados OK) |
|
|
187
|
+
| `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.dai` 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)) |
|
|
188
188
|
| `dai upgrade [--check] [--dry-run]` · alias `dai update` | **actualiza el CLI global** a la última publicada (`npm i -g …@latest`) — self-update. **No toca el repo**: reporta el drift del scaffold pero deja el `dai sync` al mantenedor. `--check` solo informa · `--dry-run` muestra el comando ([ADR-0012](docs/adr/0012-upgrade-self-update-del-cli.md)) |
|
|
189
189
|
| `dai publish <us.md> [--parent KEY] [--issuetype T] [--field alias=valor]` | crea la US en el tracker (Jira/ClickUp/md) desde un `.md` y devuelve el key — el fallback del MCP para publicar sin el asistente. `--parent PROJ-42` la cuelga de su épica · `--issuetype Epic` publica una épica (fallback CLI de `grill-epic`) · `--field clasificacion=Mejora` (repetible) manda los campos propios que exige tu Jira, declarados en `.dai/jira-fields.json` ([ADR-0015](docs/adr/0015-jira-corporativo.md)) |
|
|
190
190
|
| `dai link-us <ID> [--us <md>]` | crea branch + `implements.yaml`; sin `--us` trae la US del tracker |
|
|
@@ -203,20 +203,22 @@ flowchart TD
|
|
|
203
203
|
> **🆕 Mantené tu repo al día — `dai sync`.** Las skills, la constitución y los templates son un
|
|
204
204
|
> *caché derivable* del CLI. Cuando actualizás `dai` (`dai upgrade`), **`dai doctor` y
|
|
205
205
|
> `dai version` te avisan solos** si tu scaffold quedó atrás — con color y un `⬆️` —, y **`dai sync`**
|
|
206
|
-
> lo refresca: **aditivo** (conserva tu `CLAUDE.md` propio), sin tocar el `.env` ni OpenSpec.
|
|
206
|
+
> lo refresca: **aditivo** (conserva tu `CLAUDE.md` propio), sin tocar el `.env.dai` ni OpenSpec. Prueba sin
|
|
207
207
|
> riesgo con `dai sync --dry-run`. El versionado es semver: patch/minor no rompen nada; solo un major
|
|
208
208
|
> pediría migración. ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md))
|
|
209
209
|
|
|
210
210
|
> **🧩 Skills de cualquier stack — `dai skills install --from`.** Además de las skills de
|
|
211
|
-
> dai, cada equipo suma las suyas (por-stack: .NET, Java, Rust…) desde su propio repo
|
|
212
|
-
> `dai skills install --from github.com/tu-org/net-skills
|
|
213
|
-
>
|
|
214
|
-
>
|
|
215
|
-
>
|
|
211
|
+
> dai, cada equipo suma las suyas (por-stack: .NET, Java, Rust…) desde su propio repo o
|
|
212
|
+
> **paquete npm**: `dai skills install --from github.com/tu-org/net-skills` · `dai skills
|
|
213
|
+
> install --from npm:@tu-org/ui-skills` (el paquete npm usa el `.npmrc` del repo, así
|
|
214
|
+
> resuelve registries privados con scope). dai las **convierte para los 3 asistentes**
|
|
215
|
+
> (Claude/Cursor/Copilot) e instala. **dai es el distribuidor de skills de cualquier stack,
|
|
216
|
+
> sin opinar sobre su contenido** — self-service, sin registro; `dai sync` sigue siendo solo
|
|
217
|
+
> de dai. ([ADR-0013](docs/adr/0013-skills-externas-install-from.md))
|
|
216
218
|
>
|
|
217
219
|
> La fuente puede ser **pública, privada (por SSH) o un path local**: dai no hace auth
|
|
218
|
-
> propia, delega en git — **si
|
|
219
|
-
> privados
|
|
220
|
+
> propia, delega en git — **si puedes `git clone` el repo, dai instala desde ahí**. Para
|
|
221
|
+
> privados usa la forma SSH (`git@github.com:tu-org/net-skills.git`), consistente con el
|
|
220
222
|
> modelo de auth de dai ([ADR-0007](docs/adr/0007-modelo-de-autenticacion.md)). Público =
|
|
221
223
|
> cero fricción entre equipos/máquinas.
|
|
222
224
|
>
|
|
@@ -229,8 +231,9 @@ flowchart TD
|
|
|
229
231
|
> en [`skills/`](skills/).
|
|
230
232
|
|
|
231
233
|
Skills (se invocan en el asistente): `/doc-to-backlog` · `/grill-intent` · `/grill-epic` · `/grill-user-story` · `/link-us` ·
|
|
232
|
-
`/tdd` · `/dai-review`. Config del tracker (`md`\|`jira`\|`clickup`) y tokens: en `.env`
|
|
233
|
-
|
|
234
|
+
`/tdd` · `/dai-review`. Config del tracker (`md`\|`jira`\|`clickup`) y tokens: en `.env.dai`
|
|
235
|
+
(no versionado; el `.env` del equipo no se toca — [ADR-0017](docs/adr/0017-env-dai.md)) —
|
|
236
|
+
ver [`.env.dai.example`](.env.dai.example). Auth (SSH + tokens): [ADR-0007](docs/adr/0007-modelo-de-autenticacion.md).
|
|
234
237
|
|
|
235
238
|
## El flag `--for` — ¿para qué asistente preparo el repo?
|
|
236
239
|
|
|
@@ -262,7 +265,7 @@ Después de `dai init` (con `--for all`):
|
|
|
262
265
|
```
|
|
263
266
|
mi-repo/
|
|
264
267
|
├── CLAUDE.md · Constitución del proyecto (auto-cargada por Claude)
|
|
265
|
-
├── .env
|
|
268
|
+
├── .env.dai · tu tracker (NO versionado, completa el token) + .env.dai.example (plantilla, sí versionada)
|
|
266
269
|
├── .claude/skills/ · Las skills, locales al repo (el equipo las hereda)
|
|
267
270
|
│ └── doc-to-backlog · grill-intent · grill-epic · grill-user-story · link-us · tdd · dai-review
|
|
268
271
|
├── .github/
|
|
@@ -334,7 +337,7 @@ dai/
|
|
|
334
337
|
├── skills/ 🤖 doc-to-backlog · grill-intent · grill-epic · grill-user-story · link-us · tdd · dai-review
|
|
335
338
|
├── governance/ 🛡️ branch-naming · ci-rules · commit-convention
|
|
336
339
|
├── index.html 📊 landing autocontenido (la historia); publicable por GitHub Pages
|
|
337
|
-
└── manifest.yaml · VERSION · install.sh (shim) · .env.example
|
|
340
|
+
└── manifest.yaml · VERSION · install.sh (shim) · .env.dai.example
|
|
338
341
|
```
|
|
339
342
|
|
|
340
343
|
## Licencia
|
|
@@ -351,9 +354,17 @@ Detalle en [ADR-0006](docs/adr/0006-distribucion-y-licencia.md).
|
|
|
351
354
|
|
|
352
355
|
## Agradecimientos
|
|
353
356
|
|
|
354
|
-
dai se apoya en ideas y
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
dai se apoya en ideas y herramientas de la comunidad.
|
|
358
|
+
|
|
359
|
+
Un agradecimiento especial a **[OpenSpec](https://github.com/Fission-AI/OpenSpec)** (de
|
|
360
|
+
[Fission AI](https://github.com/Fission-AI), `@fission-ai/openspec`): es la herramienta
|
|
361
|
+
sobre la que se construye todo el **CÓMO** de dai — explorar la solución, proponer el diseño
|
|
362
|
+
y las tareas, e implementar el change (`opsx:explore → propose → apply → archive`). dai no
|
|
363
|
+
la bundlea ni la reemplaza; se apoya en ella para convertir el QUÉ en un plan técnico
|
|
364
|
+
trazable. Sin OpenSpec, la mitad del método no existiría.
|
|
365
|
+
|
|
366
|
+
Y gracias a **[Matt Pocock](https://github.com/mattpocock)** por su aporte con las *skills*
|
|
367
|
+
y por su [canal de YouTube](https://www.youtube.com/@mattpocockuk) explicándolas — su
|
|
368
|
+
trabajo nos ayudó a construir esta herramienta.
|
|
358
369
|
|
|
359
370
|
Seguridad: [`SECURITY.md`](SECURITY.md) · Conducta: [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.10.0
|
package/cli/dai.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
21
21
|
import { acHash } from "./lib/ac-hash.mjs";
|
|
22
22
|
import { discoverImplements, isPlaceholderId } from "./lib/implements.mjs";
|
|
23
23
|
import { isValidKey, slugify, branchName, extractTitle, renderImplementsYaml } from "./lib/link-us.mjs";
|
|
24
|
-
import {
|
|
24
|
+
import { loadDaiEnv } from "./lib/env.mjs";
|
|
25
25
|
import { getAdapter, coverageStatus, statusLabel } from "./lib/pm-adapter.mjs";
|
|
26
26
|
import { branchUrl, commitUrl, parseRemote, detectForge } from "./lib/forge-url.mjs";
|
|
27
27
|
import { parsePrRef, getPR, postComment, postReview } from "./lib/forge-api.mjs";
|
|
@@ -48,8 +48,33 @@ const warn = (m) => process.stdout.write(`⚠ ${m}\n`);
|
|
|
48
48
|
// Color ANSI mínimo — solo si es TTY y no está NO_COLOR (así no ensucia pipes/CI).
|
|
49
49
|
const _color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
50
50
|
const paint = (code, m) => (_color ? `\x1b[${code}m${m}\x1b[0m` : m);
|
|
51
|
-
const C = { y: (m) => paint("33", m), r: (m) => paint("31", m), cy: (m) => paint("36", m), b: (m) => paint("1", m) };
|
|
51
|
+
const C = { y: (m) => paint("33", m), r: (m) => paint("31", m), cy: (m) => paint("36", m), b: (m) => paint("1", m), dim: (m) => paint("2", m) };
|
|
52
52
|
const ROOT = join(HERE, ".."); // raíz del paquete dai (cli/ está adentro)
|
|
53
|
+
|
|
54
|
+
// Banner de bienvenida de `dai init`: el Sol de Mayo en bloques (cuerpo y rayos rectos en
|
|
55
|
+
// oro; rayos ondulados en celeste) al lado del título, más el preview de lo que se configura.
|
|
56
|
+
// Todo con caracteres — cero-dep. Degrada a ASCII sin color si no hay TTY o con NO_COLOR.
|
|
57
|
+
const initBanner = () => {
|
|
58
|
+
const sun = [
|
|
59
|
+
" █ ",
|
|
60
|
+
" ▒ ▄███▄ ▒ ",
|
|
61
|
+
"██ █████ ██",
|
|
62
|
+
" ▒ ▀███▀ ▒ ",
|
|
63
|
+
" █ ",
|
|
64
|
+
];
|
|
65
|
+
const paintSun = (line) => [...line].map((ch) => (ch === "▒" ? C.cy(ch) : ch === " " ? " " : C.y(ch))).join("");
|
|
66
|
+
const aside = ["", paint("1;33", "dai") + " · Desarrollo Asistido por IA", C.dim("La IA asiste; la persona firma."), "", ""];
|
|
67
|
+
let out = "\n";
|
|
68
|
+
for (let i = 0; i < sun.length; i++) out += " " + paintSun(sun[i]) + (aside[i] ? " " + aside[i] : "") + "\n";
|
|
69
|
+
out += "\n " + C.b("Esto va a configurar el repo:") + "\n";
|
|
70
|
+
for (const b of [
|
|
71
|
+
"Skills del método (grill · link-us · tdd · dai-review) en tu asistente",
|
|
72
|
+
"La constitución del proyecto — las reglas del trabajo",
|
|
73
|
+
"OpenSpec para el CÓMO (design / tasks) — opcional",
|
|
74
|
+
"Plantilla de PR + .env.dai para el tracker",
|
|
75
|
+
]) out += " " + C.cy("▸") + " " + b + "\n";
|
|
76
|
+
return out;
|
|
77
|
+
};
|
|
53
78
|
const CLAUDE_SKILLS_DIR = process.env.CLAUDE_SKILLS_DIR || join(homedir(), ".claude", "skills");
|
|
54
79
|
const CURSOR_SKILLS_DIR = process.env.CURSOR_SKILLS_DIR || join(homedir(), ".cursor", "skills");
|
|
55
80
|
// Copilot lee las skills personales de ~/.copilot/skills (NO de ~/.claude/skills, que
|
|
@@ -119,10 +144,10 @@ async function cmdLinkUs(key, opts) {
|
|
|
119
144
|
title = opts.title || extractTitle(md);
|
|
120
145
|
} else {
|
|
121
146
|
// Fuente tracker: traer la US del adaptador (mismo hash que usará `dai check`).
|
|
122
|
-
|
|
147
|
+
loadDaiEnv();
|
|
123
148
|
const adapter = getAdapter(process.env);
|
|
124
149
|
const us = await adapter.fetchUS(key);
|
|
125
|
-
if (!us) fail(`no encontré la US ${key} en el backend ${adapter.kind}. Pasa --us <md> o revisa el .env.`, 2);
|
|
150
|
+
if (!us) fail(`no encontré la US ${key} en el backend ${adapter.kind}. Pasa --us <md> o revisa el .env.dai.`, 2);
|
|
126
151
|
hash = us.ac_hash;
|
|
127
152
|
if (hash == null) fail(`la US ${key} no tiene una sección 'Criterios de aceptación' con criterios testeables → sin ac_hash, no se puede linkear.\n Agregá la sección en el tracker, o corré /grill-user-story ${key} para pulir la US (te interroga y la re-publica).`, 2);
|
|
128
153
|
title = opts.title || us.title;
|
|
@@ -180,7 +205,7 @@ function gitCommit() { try { return git(["rev-parse", "HEAD"]); } catch { return
|
|
|
180
205
|
|
|
181
206
|
// ── check ──────────────────────────────────────────────────────────────────
|
|
182
207
|
async function cmdCheck() {
|
|
183
|
-
|
|
208
|
+
loadDaiEnv();
|
|
184
209
|
const adapter = getAdapter(process.env);
|
|
185
210
|
const found = discoverImplements(process.cwd(), { includeArchived: false });
|
|
186
211
|
let worst = 0, n = 0;
|
|
@@ -211,7 +236,7 @@ async function cmdCheck() {
|
|
|
211
236
|
|
|
212
237
|
// ── stamp ──────────────────────────────────────────────────────────────────
|
|
213
238
|
async function cmdStamp() {
|
|
214
|
-
|
|
239
|
+
loadDaiEnv();
|
|
215
240
|
const adapter = getAdapter(process.env);
|
|
216
241
|
const remote = gitRemote(), branch = gitBranch(), commit = gitCommit();
|
|
217
242
|
const found = discoverImplements(process.cwd());
|
|
@@ -233,7 +258,7 @@ async function cmdStamp() {
|
|
|
233
258
|
|
|
234
259
|
// ── forge (review) ───────────────────────────────────────────────────────────
|
|
235
260
|
async function cmdForge(sub, ref, opts) {
|
|
236
|
-
|
|
261
|
+
loadDaiEnv();
|
|
237
262
|
const pr = parsePrRef(ref, gitRemote());
|
|
238
263
|
if (!pr) fail("no pude resolver la PR/MR. Pasa la URL completa o el número (con remoto git).", 1);
|
|
239
264
|
if (sub === "pr") {
|
|
@@ -332,7 +357,7 @@ function loadJiraFieldsSpec() {
|
|
|
332
357
|
|
|
333
358
|
async function cmdPublish(file, opts = {}) {
|
|
334
359
|
if (!file) fail("uso: dai publish <archivo-us.md> [--parent KEY] [--issuetype T] [--field alias=valor]", 1);
|
|
335
|
-
|
|
360
|
+
loadDaiEnv();
|
|
336
361
|
const md = readFileSync(file, "utf8");
|
|
337
362
|
const title = extractTitle(md);
|
|
338
363
|
if (!title) fail("no pude extraer el título de la US (falta un '# Título').", 1);
|
|
@@ -409,7 +434,7 @@ function cmdDone(opts) {
|
|
|
409
434
|
// ── pr: crea TU PROPIA PR/MR precargada desde el template + el link ────────────
|
|
410
435
|
// (Distinto de dai-review, que revisa la PR de OTRO. Tu PR la creas y revisas tú.)
|
|
411
436
|
async function cmdPr(opts) {
|
|
412
|
-
|
|
437
|
+
loadDaiEnv();
|
|
413
438
|
const remote = gitRemote(), branch = gitBranch(), commit = gitCommit();
|
|
414
439
|
if (!remote) fail("no hay remoto git 'origin'. Configúralo para crear la PR.", 1);
|
|
415
440
|
if (!branch || branch === "HEAD") fail("no estás en una branch.", 1);
|
|
@@ -447,7 +472,7 @@ async function cmdPr(opts) {
|
|
|
447
472
|
for (const f of found) for (const im of f.implements || []) {
|
|
448
473
|
if (!isPlaceholderId(im.id)) { entry = { f, im }; break; }
|
|
449
474
|
}
|
|
450
|
-
if (!entry) fail("no
|
|
475
|
+
if (!entry) fail("no hay una US linkeada (implements.yaml). Si este PR implementa una US, corré `dai link-us` primero. Si es un chore/tooling (sin US), creá la PR con tu forge: `glab mr create` / `gh pr create`.", 1);
|
|
451
476
|
const { id, version, ac_hash } = entry.im;
|
|
452
477
|
|
|
453
478
|
// 2. Estado de trazabilidad (dai check) contra la US viva.
|
|
@@ -470,7 +495,7 @@ async function cmdPr(opts) {
|
|
|
470
495
|
const usUrl = usUrlFor(id, live?.url);
|
|
471
496
|
if (!usUrl) {
|
|
472
497
|
warn(`no sé la URL de ${id} en el tracker: la PR va a quedar sin link a la US.`);
|
|
473
|
-
warn(`configurá DAI_TRACKER_URL_TEMPLATE en el .env (p. ej. https://tu-tracker/browse/{id}).`);
|
|
498
|
+
warn(`configurá DAI_TRACKER_URL_TEMPLATE en el .env.dai (p. ej. https://tu-tracker/browse/{id}).`);
|
|
474
499
|
}
|
|
475
500
|
const body = composePrBody(readFileSync(tplPath, "utf8"), {
|
|
476
501
|
id, version, ac_hash, status, usUrl, usTitle: live?.title, commits,
|
|
@@ -632,7 +657,7 @@ async function cmdInstall(opts) {
|
|
|
632
657
|
// dai (colisión → warn + skip). `dai sync` NO las toca: es solo de dai.
|
|
633
658
|
function cmdInstallFrom(opts) {
|
|
634
659
|
if (typeof opts.from !== "string" || !opts.from.trim())
|
|
635
|
-
fail("--from necesita una fuente: un git URL (github.com/org/skills[#ref]) o un path local", 2);
|
|
660
|
+
fail("--from necesita una fuente: un git URL (github.com/org/skills[#ref]), un paquete npm (npm:@scope/pkg) o un path local", 2);
|
|
636
661
|
let want;
|
|
637
662
|
try { want = parseAssistants(typeof opts.for === "string" ? opts.for : "all"); }
|
|
638
663
|
catch (e) { fail(`--for ${e.message}`); }
|
|
@@ -651,6 +676,33 @@ function cmdInstallFrom(opts) {
|
|
|
651
676
|
try { git(args); }
|
|
652
677
|
catch (e) { rmSync(tmp, { recursive: true, force: true }); fail(`no pude clonar la fuente: ${String(e.message).split("\n")[0]}`, 1); }
|
|
653
678
|
root = tmp;
|
|
679
|
+
} else if (src.type === "npm") {
|
|
680
|
+
tmp = mkdtempSync(join(tmpdir(), "dai-skills-"));
|
|
681
|
+
info(`bajando el paquete npm ${src.location} …`);
|
|
682
|
+
try {
|
|
683
|
+
// `npm install` (no `npm pack`) en el temp. Dos decisiones a propósito:
|
|
684
|
+
// 1) install en vez de pack: los registries de GRUPO de GitLab devuelven una
|
|
685
|
+
// `dist.tarball` malformada (el scope duplicado en el nombre) que `npm pack` sigue
|
|
686
|
+
// literal y da 404; `npm install` —como `npx`— reconstruye la URL y resuelve.
|
|
687
|
+
// 2) copiamos el `.npmrc` del repo al temp y corremos con cwd=temp: así npm ve el
|
|
688
|
+
// registry/scope privado (con `--prefix` npm NO lee el `.npmrc` del cwd y se va al
|
|
689
|
+
// registry público). `--ignore-scripts`: no corremos scripts de un paquete de 3ros.
|
|
690
|
+
const repoNpmrc = join(process.cwd(), ".npmrc");
|
|
691
|
+
if (existsSync(repoNpmrc)) cpSync(repoNpmrc, join(tmp, ".npmrc"));
|
|
692
|
+
runNpmTool("npm", ["install", src.location,
|
|
693
|
+
"--no-save", "--no-package-lock", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
694
|
+
{ cwd: tmp, stdio: ["ignore", "ignore", "pipe"] });
|
|
695
|
+
// El paquete queda en <tmp>/node_modules/<name>. name = spec sin la versión
|
|
696
|
+
// (@scope/pkg@1.2.3 → @scope/pkg; el `@` inicial del scope no cuenta).
|
|
697
|
+
let name = src.location; const at = name.lastIndexOf("@");
|
|
698
|
+
if (at > 0) name = name.slice(0, at);
|
|
699
|
+
root = join(tmp, "node_modules", name);
|
|
700
|
+
if (!existsSync(root)) throw new Error(`npm install no dejó '${name}' en node_modules`);
|
|
701
|
+
} catch (e) {
|
|
702
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
703
|
+
const detail = String(e.stderr || e.stdout || e.message || "").split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 3).join("\n ");
|
|
704
|
+
fail(`no pude bajar el paquete npm '${src.location}':\n ${detail || "npm install falló — revisá el spec, el registry y el .npmrc del repo"}`, 1);
|
|
705
|
+
}
|
|
654
706
|
} else {
|
|
655
707
|
root = src.location;
|
|
656
708
|
if (!existsSync(root)) fail(`no existe la fuente: ${root}`, 2);
|
|
@@ -738,7 +790,7 @@ async function cmdInit(repo, opts) {
|
|
|
738
790
|
}
|
|
739
791
|
const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
740
792
|
|
|
741
|
-
process.stdout.write(
|
|
793
|
+
process.stdout.write(initBanner());
|
|
742
794
|
|
|
743
795
|
// Preguntas primero (después cerramos readline para liberar stdin a los instaladores).
|
|
744
796
|
let forOpt = typeof opts.for === "string" ? opts.for.toLowerCase() : null;
|
|
@@ -789,28 +841,32 @@ async function cmdInit(repo, opts) {
|
|
|
789
841
|
writeFileSync(join(dai, "VERSION"), readFileSync(join(ROOT, "VERSION"), "utf8"));
|
|
790
842
|
ok(".dai/ moldes (templates) + reglas (governance) del método");
|
|
791
843
|
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
|
|
795
|
-
|
|
844
|
+
// Config de dai en SUS PROPIOS archivos, sin tocar el `.env`/`.env.example` del equipo:
|
|
845
|
+
// muchas orgs versionan el `.env` como política, así que dai lo deja en paz (solo lo lee,
|
|
846
|
+
// por compat) y pone lo suyo en `.env.dai` (ver ADR-0017). `.env.dai.example` se versiona
|
|
847
|
+
// como plantilla; `.env.dai` (gitignored) es donde cada dev completa token y datos propios.
|
|
848
|
+
const envBlock = envFor(pm);
|
|
849
|
+
|
|
850
|
+
// .env.dai.example — plantilla VERSIONADA, mismas claves con valores VACÍOS (sin secretos).
|
|
851
|
+
const exPath = join(repo, ".env.dai.example");
|
|
796
852
|
if (existsSync(exPath)) {
|
|
797
|
-
const cur = readFileSync(exPath, "utf8"), merged = mergeEnv(cur,
|
|
798
|
-
if (merged !== cur) { writeFileSync(exPath, merged); ok(".env.example claves de dai agregadas (aditivo)"); }
|
|
799
|
-
else ok(".env.example ya tenía la config de dai");
|
|
800
|
-
} else { writeFileSync(exPath,
|
|
853
|
+
const cur = readFileSync(exPath, "utf8"), merged = mergeEnv(cur, envBlock);
|
|
854
|
+
if (merged !== cur) { writeFileSync(exPath, merged); ok(".env.dai.example claves de dai agregadas (aditivo)"); }
|
|
855
|
+
else ok(".env.dai.example ya tenía la config de dai");
|
|
856
|
+
} else { writeFileSync(exPath, envBlock); ok(".env.dai.example creado (plantilla versionada)"); }
|
|
801
857
|
|
|
802
|
-
// .env —
|
|
803
|
-
const envPath = join(repo, ".env")
|
|
858
|
+
// .env.dai — el real de cada dev (gitignored): aditivo; si no existe, lo crea.
|
|
859
|
+
const envPath = join(repo, ".env.dai");
|
|
804
860
|
if (existsSync(envPath)) {
|
|
805
861
|
const cur = readFileSync(envPath, "utf8"), merged = mergeEnv(cur, envBlock);
|
|
806
|
-
if (merged !== cur) { writeFileSync(envPath, merged); ok(`.env
|
|
807
|
-
else ok(".env
|
|
808
|
-
} else { writeFileSync(envPath, envBlock); ok(`.env
|
|
862
|
+
if (merged !== cur) { writeFileSync(envPath, merged); ok(`.env.dai claves de dai agregadas (aditivo, DAI_PM=${pm}${pm === "md" ? "" : " — completa el token"})`); }
|
|
863
|
+
else ok(".env.dai ya tenía la config de dai");
|
|
864
|
+
} else { writeFileSync(envPath, envBlock); ok(`.env.dai creado (no versionado), DAI_PM=${pm}${pm === "md" ? "" : " (completa el token)"}`); }
|
|
809
865
|
|
|
810
866
|
// .gitignore — versiona los artefactos de dai (según --for), deja fuera solo lo personal.
|
|
811
867
|
const giPath = join(repo, ".gitignore");
|
|
812
868
|
const gi = reconcileGitignore(existsSync(giPath) ? readFileSync(giPath, "utf8") : "", want);
|
|
813
|
-
if (gi.changed) { writeFileSync(giPath, gi.text.endsWith("\n") ? gi.text : gi.text + "\n"); ok(".gitignore ajustado (skills/constitución versionadas; .env y settings.local.json fuera)"); }
|
|
869
|
+
if (gi.changed) { writeFileSync(giPath, gi.text.endsWith("\n") ? gi.text : gi.text + "\n"); ok(".gitignore ajustado (skills/constitución versionadas; .env.dai y settings.local.json fuera)"); }
|
|
814
870
|
|
|
815
871
|
mkdirSync(join(repo, ".github"), { recursive: true });
|
|
816
872
|
cpSync(join(ROOT, "templates", "pull-request.md"), join(repo, ".github", "pull_request_template.md"));
|
|
@@ -894,12 +950,12 @@ async function cmdInit(repo, opts) {
|
|
|
894
950
|
}
|
|
895
951
|
|
|
896
952
|
// ── Próximos pasos ─────────────────────────────────────────────────────────
|
|
897
|
-
process.stdout.write("\n ✔ Repo configurado. Próximos pasos:\n");
|
|
953
|
+
process.stdout.write("\n " + C.y("✔") + " " + C.b("Repo configurado.") + " Próximos pasos:\n");
|
|
898
954
|
process.stdout.write(pm === "md"
|
|
899
|
-
?
|
|
900
|
-
: ` 1.
|
|
901
|
-
process.stdout.write(
|
|
902
|
-
process.stdout.write(
|
|
955
|
+
? ` 1. Crea tu primera US en ${C.cy(".dai/us/<ID>.md")} (criterios bajo '## Criterios de aceptación')\n`
|
|
956
|
+
: ` 1. Copia ${C.cy(".env.dai.example")} → ${C.cy(".env.dai")} y completa el token de ${pm}; verifica con ${C.y("dai doctor")}\n`);
|
|
957
|
+
process.stdout.write(` 2. ${C.y("dai link-us <ID>")} → crea la branch + el link a la US\n`);
|
|
958
|
+
process.stdout.write(` 3. Implementa con test primero, después: ${C.y("dai check")}\n`);
|
|
903
959
|
process.stdout.write(" Guía paso a paso: https://github.com/dforce2055/dai/blob/main/docs/PROBAR.md\n\n");
|
|
904
960
|
}
|
|
905
961
|
|
|
@@ -1031,7 +1087,7 @@ function cmdSync(repo, opts) {
|
|
|
1031
1087
|
() => writeFileSync(giPath, gi.text.endsWith("\n") ? gi.text : gi.text + "\n"));
|
|
1032
1088
|
|
|
1033
1089
|
if (dry) info("dry-run: nada escrito. Quitá --dry-run para aplicar.");
|
|
1034
|
-
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"); }
|
|
1090
|
+
else { process.stdout.write("\n"); ok(`sync completo — .dai/ ahora en v${cliV}`); process.stdout.write(" (El .env.dai y OpenSpec no se tocan: OpenSpec se actualiza aparte con `openspec`.)\n"); }
|
|
1035
1091
|
}
|
|
1036
1092
|
|
|
1037
1093
|
// Imprime el estado de version-drift del scaffold (ADR-0010) con color + ícono.
|
|
@@ -1097,7 +1153,7 @@ function cmdUpgrade(opts) {
|
|
|
1097
1153
|
|
|
1098
1154
|
// ── doctor: diagnóstico ───────────────────────────────────────────────────────
|
|
1099
1155
|
function cmdDoctor() {
|
|
1100
|
-
|
|
1156
|
+
loadDaiEnv();
|
|
1101
1157
|
info(`dai doctor — versión v${readFileSync(join(ROOT, "VERSION"), "utf8").trim()}`);
|
|
1102
1158
|
|
|
1103
1159
|
// Una skill sirve si está en el repo actual (la puso `dai init`) o global (la puso
|
|
@@ -1151,11 +1207,11 @@ function cmdDoctor() {
|
|
|
1151
1207
|
const pm = process.env.DAI_PM || "md";
|
|
1152
1208
|
ok(`DAI_PM=${pm}`);
|
|
1153
1209
|
if (pm === "jira") {
|
|
1154
|
-
if (!process.env.DAI_JIRA_BASE_URL) warn("falta DAI_JIRA_BASE_URL en
|
|
1155
|
-
if (!process.env.DAI_JIRA_EMAIL) warn("falta DAI_JIRA_EMAIL en
|
|
1210
|
+
if (!process.env.DAI_JIRA_BASE_URL) warn("falta DAI_JIRA_BASE_URL en .env.dai");
|
|
1211
|
+
if (!process.env.DAI_JIRA_EMAIL) warn("falta DAI_JIRA_EMAIL en .env.dai");
|
|
1156
1212
|
// Ojo: solo miramos que el token ESTÉ, no que sirva — uno vencido pasa este chequeo
|
|
1157
1213
|
// y recién falla al publicar. Verificarlo de verdad es pegarle a la red.
|
|
1158
|
-
if (!process.env.DAI_JIRA_TOKEN) warn("falta DAI_JIRA_TOKEN en
|
|
1214
|
+
if (!process.env.DAI_JIRA_TOKEN) warn("falta DAI_JIRA_TOKEN en .env.dai"); else ok("token de Jira presente (no verificado: eso lo dice `dai publish`)");
|
|
1159
1215
|
if (!process.env.DAI_JIRA_PROJECT) warn("DAI_JIRA_PROJECT vacío — solo hace falta para `dai publish` (crear issues)");
|
|
1160
1216
|
else {
|
|
1161
1217
|
try { ok(`proyecto=${assertProjectKey(process.env.DAI_JIRA_PROJECT)} (para dai publish)`); }
|
|
@@ -1173,7 +1229,7 @@ function cmdDoctor() {
|
|
|
1173
1229
|
}
|
|
1174
1230
|
}
|
|
1175
1231
|
if (pm === "clickup") {
|
|
1176
|
-
if (!process.env.DAI_CLICKUP_TOKEN) warn("falta DAI_CLICKUP_TOKEN en
|
|
1232
|
+
if (!process.env.DAI_CLICKUP_TOKEN) warn("falta DAI_CLICKUP_TOKEN en .env.dai"); else ok("token de ClickUp presente");
|
|
1177
1233
|
process.env.DAI_CLICKUP_LIST_ID ? ok(`lista=${process.env.DAI_CLICKUP_LIST_ID} (para dai publish)`)
|
|
1178
1234
|
: warn("DAI_CLICKUP_LIST_ID vacío — solo hace falta para `dai publish` (crear tareas)");
|
|
1179
1235
|
}
|
|
@@ -1238,16 +1294,16 @@ switch (cmd) {
|
|
|
1238
1294
|
" Sin --yes no postea nada: muestra el preview y valida que cada hallazgo apunte al diff.\n\n" +
|
|
1239
1295
|
"Instalación:\n" +
|
|
1240
1296
|
" skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] instala las skills de dai (alias: `install`)\n" +
|
|
1241
|
-
" skills install --from <git-url|path>[#ref] [--for <asistentes>] instala skills EXTERNAS (por-stack), convertidas para los 3 asistentes (ADR-0013)\n" +
|
|
1297
|
+
" skills install --from <git-url|npm:pkg|path>[#ref] [--for <asistentes>] instala skills EXTERNAS (por-stack), convertidas para los 3 asistentes (ADR-0013)\n" +
|
|
1242
1298
|
" init [<repo>] scaffolder interactivo del repo (asistente, gestor, OpenSpec)\n" +
|
|
1243
1299
|
" --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
|
|
1244
1300
|
" ej: --for claude,cursor · --for copilot · --for all\n" +
|
|
1245
1301
|
" --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
|
|
1246
|
-
" sync [<repo>] [--dry-run] [--for <asistentes>] refresca skills/constitución/templates a la versión del CLI (aditivo; no toca .env ni OpenSpec)\n" +
|
|
1302
|
+
" sync [<repo>] [--dry-run] [--for <asistentes>] refresca skills/constitución/templates a la versión del CLI (aditivo; no toca .env.dai ni OpenSpec)\n" +
|
|
1247
1303
|
" upgrade [--check] [--dry-run] (alias: update) actualiza el CLI global a la última (npm i -g …@latest) y avisa si el repo quedó atrasado (ADR-0012)\n" +
|
|
1248
1304
|
" docs <destino> documentación conceptual → <destino>\n" +
|
|
1249
1305
|
" doctor diagnóstico del entorno\n\n" +
|
|
1250
|
-
" (config: .env — ver .env.example)\n"
|
|
1306
|
+
" (config: .env.dai — ver .env.dai.example)\n"
|
|
1251
1307
|
);
|
|
1252
1308
|
process.exit(cmd && cmd !== "help" ? 1 : 0);
|
|
1253
1309
|
}
|
package/cli/lib/bootstrap.mjs
CHANGED
|
@@ -47,13 +47,38 @@ function unquoteScalar(s) {
|
|
|
47
47
|
// Parsea el frontmatter YAML de un SKILL.md → { name, description, body }.
|
|
48
48
|
// Devuelve los valores YA sin comillas, para que quien los reserialice (skillToCursor)
|
|
49
49
|
// no los cite dos veces.
|
|
50
|
+
// Lee un campo del frontmatter. Soporta escalar de una línea (con/sin comillas) y BLOQUE
|
|
51
|
+
// YAML (`|` literal, `>` plegado, con chomp `-`/`+`): junta las líneas indentadas siguientes
|
|
52
|
+
// hasta la próxima clave (columna 0). El parser de dai es regex, no un YAML completo — esto
|
|
53
|
+
// cubre lo común: descripciones multilínea, que es como se escriben las skills reales.
|
|
54
|
+
function readFmField(fm, key) {
|
|
55
|
+
const lines = fm.split(/\r?\n/);
|
|
56
|
+
const keyRe = new RegExp(`^${key}:\\s*(.*)$`);
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const m = lines[i].match(keyRe);
|
|
59
|
+
if (!m) continue;
|
|
60
|
+
const inline = m[1].trim();
|
|
61
|
+
const blk = inline.match(/^([|>])[-+]?\s*$/); // |, >, |-, |+, >-, >+
|
|
62
|
+
if (!blk) return unquoteScalar(inline) || null; // escalar de una línea
|
|
63
|
+
const buf = [];
|
|
64
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
65
|
+
if (lines[j].trim() === "") { buf.push(""); continue; }
|
|
66
|
+
if (!/^\s/.test(lines[j])) break; // sin indentar → empezó otra clave
|
|
67
|
+
buf.push(lines[j]);
|
|
68
|
+
}
|
|
69
|
+
const indent = ((buf.find((l) => l.trim() !== "") || "").match(/^\s*/) || [""])[0].length;
|
|
70
|
+
const text = buf.map((l) => l.slice(indent)).join("\n").replace(/\s+$/, "");
|
|
71
|
+
// `>` plegado: un salto simple → espacio; los dobles (párrafo) se conservan.
|
|
72
|
+
return (blk[1] === ">" ? text.replace(/([^\n])\n(?!\n)/g, "$1 ") : text) || null;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
export function parseFrontmatter(md) {
|
|
51
78
|
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
52
79
|
if (!m) return { name: null, description: null, body: md.trim() };
|
|
53
80
|
const fm = m[1];
|
|
54
|
-
|
|
55
|
-
const description = unquoteScalar((fm.match(/^description:\s*(.+)$/m) || [])[1]) || null;
|
|
56
|
-
return { name, description, body: m[2].trim() };
|
|
81
|
+
return { name: readFmField(fm, "name"), description: readFmField(fm, "description"), body: m[2].trim() };
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
// Valida el contrato MÍNIMO de un SKILL.md para que dai lo ingiera y los asistentes lo
|
|
@@ -66,7 +91,11 @@ export function validateSkill(md) {
|
|
|
66
91
|
if (!name) return "falta 'name' en el frontmatter";
|
|
67
92
|
if (!description) return "falta 'description' en el frontmatter";
|
|
68
93
|
for (const key of ["name", "description"]) {
|
|
69
|
-
const
|
|
94
|
+
const raw = rawFrontmatterValue(md, key);
|
|
95
|
+
// Un bloque YAML (`|`, `>`, con chomp) es literal → siempre válido; su contenido va en las
|
|
96
|
+
// líneas indentadas, no en el valor de la clave. No lo pasamos por el scalar-check.
|
|
97
|
+
if (/^[|>][-+]?\s*$/.test(String(raw ?? "").trim())) continue;
|
|
98
|
+
const issue = yamlScalarIssue(raw);
|
|
70
99
|
if (issue) return `'${key}' no es YAML válido: ${issue} — citá el valor con comillas dobles`;
|
|
71
100
|
}
|
|
72
101
|
return null;
|
|
@@ -98,7 +127,7 @@ export function skillToCursor(md) {
|
|
|
98
127
|
// la URL canónica que devuelve el tracker, así que el `/t/{id}` que escribíamos acá
|
|
99
128
|
// tapaba la de ClickUp con team_id. Queda como override manual para trackers raros.
|
|
100
129
|
export function envFor(pm) {
|
|
101
|
-
const head = "# Config de dai —
|
|
130
|
+
const head = "# Config de dai — va en .env.dai (NO versionado), no en el .env del equipo.\n# Completá lo que falte. NUNCA commitees tokens.\n";
|
|
102
131
|
if (pm === "clickup") {
|
|
103
132
|
return head + "DAI_PM=clickup\nDAI_CLICKUP_TOKEN=\nDAI_CLICKUP_LIST_ID=\n";
|
|
104
133
|
}
|
|
@@ -156,7 +185,10 @@ export function reconcileGitignore(text, want) {
|
|
|
156
185
|
// `.dai/` NO va acá: ahí vive config que SÍ se versiona (jira-fields.json). Solo se
|
|
157
186
|
// ignora `.dai/reviews/`, que son borradores de `dai forge review` — efímeros, con
|
|
158
187
|
// hallazgos a medio editar, y no tienen por qué viajar en un commit.
|
|
159
|
-
|
|
188
|
+
// `.env.dai` (config y secretos de dai) SÍ se ignora; el `.env` del equipo NO lo tocamos:
|
|
189
|
+
// es suyo y muchas orgs lo versionan como política (ADR-0017). La plantilla
|
|
190
|
+
// `.env.dai.example` sí se versiona (no matchea `.env.dai` exacto, así que no se ignora).
|
|
191
|
+
const ensure = [".env.dai", ".dai/reviews/"];
|
|
160
192
|
if (want.claude) { broad.add("CLAUDE.md"); broad.add(".claude"); ensure.push(".claude/settings.local.json"); }
|
|
161
193
|
if (want.cursor) { broad.add(".cursor"); }
|
|
162
194
|
let changed = false;
|
|
@@ -199,7 +231,7 @@ export function constitution(kind) {
|
|
|
199
231
|
- **El link se autora una vez** (\`implements.yaml\`); la cobertura se **deriva** (nunca a mano).
|
|
200
232
|
- **Verifica el comportamiento, no solo que compile:** que pase el chequeo estático o el build no prueba que funcione; ejercita el flujo real antes de darlo por hecho.
|
|
201
233
|
- **La IA confirma antes de construir:** el asistente declara que entendió esta constitución y la va a obedecer antes de generar código.
|
|
202
|
-
- **Secretos:** en \`.env\` (
|
|
234
|
+
- **Secretos:** en \`.env.dai\` (NO versionado; el \`.env\` del equipo no se toca). git por **SSH**, APIs por **token scopeado**.
|
|
203
235
|
- **No bajes la seguridad para avanzar:** si una llamada falla por el certificado, declara la CA (\`NODE_EXTRA_CA_CERTS\`). **Nunca** \`NODE_TLS_REJECT_UNAUTHORIZED=0\`, \`verify=False\`, \`-k\` ni equivalentes: apagan la verificación de toda la conexión, y por ahí viajan los tokens.
|
|
204
236
|
- **Si el CLI no llega, para y dilo:** cuando \`dai\` no cubre un caso, repórtalo — no improvises una llamada a la API por fuera. El atajo publica igual, pero rompe el link QUÉ↔CÓMO en silencio y nadie se entera hasta que la trazabilidad ya está mal.
|
|
205
237
|
- **Docs vivas:** una constitución o arquitectura desactualizada es un defecto, no documentación.
|
package/cli/lib/env.mjs
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
|
|
7
|
+
// Config de dai: `.env.dai` (propio, nunca versionado) tiene prioridad sobre `.env`.
|
|
8
|
+
// Pensado para equipos que versionan el `.env` (política de empresa): dai deja ese
|
|
9
|
+
// archivo en paz y pone sus claves y secretos en `.env.dai` (gitignored). Se carga
|
|
10
|
+
// `.env.dai` PRIMERO porque el loader es "primero-gana" (ver más abajo), así la
|
|
11
|
+
// precedencia queda: entorno (shell/CI) > .env.dai > .env. Leer también `.env`
|
|
12
|
+
// mantiene compatibilidad con repos previos que tienen los DAI_* ahí.
|
|
13
|
+
export function loadDaiEnv(env = process.env) {
|
|
14
|
+
loadEnv(".env.dai", env);
|
|
15
|
+
loadEnv(".env", env);
|
|
16
|
+
return env;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export function loadEnv(path = ".env", env = process.env) {
|
|
8
20
|
let text;
|
|
9
21
|
try { text = readFileSync(path, "utf8"); } catch { return env; } // sin .env: no pasa nada
|