@dforce2055/dai 0.6.0 → 0.8.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 +9 -3
- package/CHANGELOG.md +79 -0
- package/README.md +38 -9
- package/VERSION +1 -1
- package/cli/dai.mjs +221 -53
- package/cli/lib/args.mjs +13 -2
- package/cli/lib/bootstrap.mjs +77 -15
- package/cli/lib/http.mjs +61 -0
- package/cli/lib/jira-fields.mjs +143 -0
- package/cli/lib/pm-jira.mjs +52 -12
- package/cli/lib/skills-source.mjs +26 -0
- package/docs/adr/0013-skills-externas-install-from.md +56 -0
- package/docs/adr/0014-copilot-agent-skills.md +99 -0
- package/docs/adr/0015-jira-corporativo.md +104 -0
- package/docs/adr/README.md +4 -0
- package/package.json +1 -1
- package/skills/dai-review/SKILL.md +1 -1
- package/skills/doc-to-backlog/SKILL.md +1 -1
- package/skills/grill-epic/SKILL.md +13 -5
- package/skills/grill-intent/SKILL.md +1 -1
- package/skills/grill-user-story/SKILL.md +16 -3
- package/skills/link-us/SKILL.md +1 -1
- package/skills/tdd/SKILL.md +1 -1
- package/templates/jira-fields.example.json +49 -0
- package/templates/skill.md +49 -0
package/.env.example
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Las variables ya exportadas en la shell/CI ganan sobre el .env.
|
|
4
4
|
|
|
5
5
|
# ── Backend del tracker ──────────────────────────────────────────────────────
|
|
6
|
-
# md | jira | clickup
|
|
6
|
+
# md | jira | clickup
|
|
7
7
|
DAI_PM=md
|
|
8
8
|
|
|
9
9
|
# Plantilla del link al ticket (para `dai ls` / stamp). {id} se reemplaza.
|
|
@@ -17,8 +17,14 @@ DAI_MD_US_DIR=.dai/us
|
|
|
17
17
|
# DAI_JIRA_BASE_URL=https://miempresa.atlassian.net
|
|
18
18
|
# DAI_JIRA_EMAIL=usuario@miempresa.com
|
|
19
19
|
# DAI_JIRA_TOKEN= # ← SECRETO. api_token de id.atlassian.com. Nunca commitear.
|
|
20
|
-
# DAI_JIRA_PROJECT= # clave del
|
|
21
|
-
# DAI_JIRA_ISSUETYPE=Story # tipo de issue a crear (default Story)
|
|
20
|
+
# DAI_JIRA_PROJECT= # clave del PROYECTO (ej. PROJ), NO la de un ticket (PROJ-123) — para `dai publish`
|
|
21
|
+
# DAI_JIRA_ISSUETYPE=Story # tipo de issue a crear (default Story). Se pisa con `dai publish --issuetype Epic`
|
|
22
|
+
# DAI_JIRA_FIELDS_FILE=.dai/jira-fields.json # campos propios que tu Jira exige al crear
|
|
23
|
+
# # (ADR-0015). Si el archivo no existe, se ignora. Molde en templates/.
|
|
24
|
+
#
|
|
25
|
+
# ¿El proxy de tu empresa intercepta TLS? Node no usa el trust store del sistema, así que
|
|
26
|
+
# falla donde el navegador anda. Declará la CA — NUNCA NODE_TLS_REJECT_UNAUTHORIZED=0:
|
|
27
|
+
# NODE_EXTRA_CA_CERTS=/ruta/ca-empresa.pem
|
|
22
28
|
|
|
23
29
|
# ── Backend clickup (REST v2) — activar con DAI_PM=clickup ───────────────────
|
|
24
30
|
# DAI_CLICKUP_TOKEN= # ← SECRETO. token personal scopeado.
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,83 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.8.0] — 2026-07-15
|
|
7
|
+
|
|
8
|
+
**El primer analista funcional real usó dai contra un Jira corporativo, y encontró el
|
|
9
|
+
hueco entre "anda en mi Jira de juguete" y "anda en el de una empresa".**
|
|
10
|
+
|
|
11
|
+
### Agregado
|
|
12
|
+
- **Campos propios de Jira**, declarados por issuetype en `.dai/jira-fields.json` (o
|
|
13
|
+
`DAI_JIRA_FIELDS_FILE`) con nombre humano, forma, default y opciones válidas. dai valida
|
|
14
|
+
**antes** de llamar a la red: un typo da `'Mejraa' no es opción de 'clasificacion' —
|
|
15
|
+
válidas: Mejora | Corrección`, no un 400 críptico. El valor se elige por US con
|
|
16
|
+
`dai publish us.md --field clasificacion=Corrección` (repetible), porque la clasificación
|
|
17
|
+
cambia según la historia y un default fijo publicaría todas iguales ([ADR-0015](docs/adr/0015-jira-corporativo.md)).
|
|
18
|
+
- `dai publish --parent <KEY>` — cuelga la US de su épica (antes `createUS` nunca mandaba
|
|
19
|
+
`parent`, así que las US quedaban sueltas).
|
|
20
|
+
- `dai publish --issuetype <T>` — **`grill-epic` gana su fallback por CLI**: hasta ahora, sin
|
|
21
|
+
MCP, una épica quedaba en un `.md` para pegar a mano.
|
|
22
|
+
- **Diagnóstico de TLS**: un fallo de certificado ahora enseña `NODE_EXTRA_CA_CERTS` (el caso
|
|
23
|
+
típico es el proxy corporativo: Node no usa el trust store del sistema) y explica por qué
|
|
24
|
+
`NODE_TLS_REJECT_UNAUTHORIZED=0` no es una alternativa — apaga la verificación entera y por
|
|
25
|
+
ahí viaja tu token.
|
|
26
|
+
- **Constitución**: dos reglas nuevas para todos los asistentes — no bajar la seguridad para
|
|
27
|
+
avanzar, y parar y avisar si el CLI no llega en vez de improvisar la llamada por fuera.
|
|
28
|
+
|
|
29
|
+
### Cambiado
|
|
30
|
+
- **Copilot lee `SKILL.md` nativo** ([ADR-0014](docs/adr/0014-copilot-agent-skills.md)).
|
|
31
|
+
`dai init --for copilot` genera `.github/skills/` (copia cruda, **con los `templates/`**) en
|
|
32
|
+
vez de `.github/prompts/*.prompt.md`, y borra los prompts viejos de dai para que no dupliquen
|
|
33
|
+
cada `/comando`. Los prompt files propios del equipo no se tocan.
|
|
34
|
+
- **`dai skills install --for copilot` ya existe** → `~/.copilot/skills/`. Antes warneaba
|
|
35
|
+
*"Copilot no tiene skills instalables"*: era cierto hasta que GitHub adoptó Agent Skills, y
|
|
36
|
+
por eso una skill "instalada global" no le aparecía a nadie que usara la app o el CLI de
|
|
37
|
+
Copilot (`~/.claude/skills` solo lo mira VS Code).
|
|
38
|
+
- **`dai doctor` reporta solo los asistentes que el repo usa.** Antes listaba los tres siempre:
|
|
39
|
+
quien configuraba uno veía 14 warnings de los otros dos y leía "está todo roto". Ahora
|
|
40
|
+
también chequea Copilot, valida `DAI_JIRA_PROJECT` y que el archivo de campos parsee.
|
|
41
|
+
- **Un flag repetido acumula** (`--field a=1 --field b=2`) en vez de que gane el último en
|
|
42
|
+
silencio.
|
|
43
|
+
|
|
44
|
+
### Arreglado
|
|
45
|
+
- **`doc-to-backlog` y `grill-epic` no cargaban en Copilot.** Sus descripciones tenían un `: `
|
|
46
|
+
suelto (*"…épicas finales: extrae…"*), que un parser YAML lee como el arranque de un mapa y
|
|
47
|
+
descarta la skill entera — justo las dos que más necesita un analista funcional. El defecto
|
|
48
|
+
siempre estuvo en la fuente: lo tapaban nuestro `parseFrontmatter` (que es un regex, no YAML)
|
|
49
|
+
y la conversión a `.prompt.md` (que citaba el valor al serializarlo). Ahora las 7
|
|
50
|
+
descripciones van citadas, **`validateSkill` valida que `name` y `description` sean escalares
|
|
51
|
+
YAML válidos**, y el molde de `templates/skill.md` cita por defecto.
|
|
52
|
+
- `DAI_JIRA_PROJECT=PROJ-42` (la clave de un **ticket**, el error de config más común) daba
|
|
53
|
+
un 400 de Jira que no lo explicaba. Ahora falla **antes de la red**, con los dos caminos:
|
|
54
|
+
`DAI_JIRA_PROJECT=PROJ`, o `--parent PROJ-42` si querías colgarla de esa épica.
|
|
55
|
+
|
|
56
|
+
### Quitado
|
|
57
|
+
- `skillToPrompt()` y el adaptador `.github/prompts/` de Copilot. Cuando el formato de una
|
|
58
|
+
skill es un estándar abierto, el mejor adaptador es ninguno.
|
|
59
|
+
|
|
60
|
+
### Interno
|
|
61
|
+
- **158 tests** (+39 desde 0.7.0): `jira-fields` ×19, `http`/TLS ×7, contrato YAML del
|
|
62
|
+
frontmatter ×7, `assertProjectKey` y el payload de `createUS` ×6.
|
|
63
|
+
- **Primera verificación en Windows.** Hasta ahora dai no se había instalado nunca en Windows
|
|
64
|
+
(no hay CI de esa plataforma): se probó el ciclo completo — install, `dai init --for copilot`,
|
|
65
|
+
skills globales en `~/.copilot/skills`, y Copilot cargando las 7. Los campos propios de Jira
|
|
66
|
+
y el diagnóstico de TLS están cubiertos por tests contra un Jira simulado, **todavía no
|
|
67
|
+
contra un Jira corporativo real**.
|
|
68
|
+
- `lib/jira-fields.mjs` y `lib/http.mjs` nuevos, cero dependencias (como el resto del CLI).
|
|
69
|
+
|
|
70
|
+
## [0.7.0] — 2026-07-14
|
|
71
|
+
|
|
72
|
+
**dai es el distribuidor de skills de cualquier stack, sin opinar sobre su contenido.**
|
|
73
|
+
|
|
74
|
+
### Agregado
|
|
75
|
+
- `dai skills install` — namespace de skills (`dai install` queda como alias). Con
|
|
76
|
+
`--from <git-url|path>[#ref]` instala **skills externas** por-stack (.NET, Java,
|
|
77
|
+
Rust…) desde un repo git (público o privado por SSH) o un path local, convertidas
|
|
78
|
+
para los 3 asistentes (Claude/Cursor/Copilot).
|
|
79
|
+
Self-service, one-off, sin registro; `dai sync` no las toca ([ADR-0013](docs/adr/0013-skills-externas-install-from.md)).
|
|
80
|
+
Valida el contrato mínimo (`SKILL.md` con `name` + `description`) y saltea con aviso
|
|
81
|
+
las malformadas; molde en [`templates/skill.md`](templates/skill.md). No valida el contenido.
|
|
82
|
+
|
|
6
83
|
## [0.6.0] — 2026-07-12
|
|
7
84
|
|
|
8
85
|
Self-update del CLI y blindaje de autoría del repo.
|
|
@@ -181,6 +258,8 @@ ClickUp y Jira Cloud.
|
|
|
181
258
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
182
259
|
`files` de npm sin tests ni secretos.
|
|
183
260
|
|
|
261
|
+
[0.8.0]: https://github.com/dforce2055/dai/releases/tag/v0.8.0
|
|
262
|
+
[0.7.0]: https://github.com/dforce2055/dai/releases/tag/v0.7.0
|
|
184
263
|
[0.6.0]: https://github.com/dforce2055/dai/releases/tag/v0.6.0
|
|
185
264
|
[0.5.0]: https://github.com/dforce2055/dai/releases/tag/v0.5.0
|
|
186
265
|
[0.4.0]: https://github.com/dforce2055/dai/releases/tag/v0.4.0
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ deja las skills disponibles:
|
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
npm i -g @dforce2055/dai # el CLI
|
|
52
|
-
dai install
|
|
52
|
+
dai skills install # skills de IA → Claude y Cursor (global por defecto)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Y después, en el chat del asistente, según lo que tengas:
|
|
@@ -157,8 +157,10 @@ flowchart TD
|
|
|
157
157
|
| Comando | Qué hace |
|
|
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
|
-
| `dai install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|cursor\|all]` | instala/actualiza skills de
|
|
160
|
+
| `dai skills install [--global \| --local <repo>] [--force] [--dry-run] [--for claude\|cursor\|all]` | instala/actualiza las skills de dai en Claude y/o Cursor (`--for all` por defecto). `--force` re-copia. Alias: **`dai install`**. Ej: `dai skills install --local . --for cursor --force` |
|
|
161
|
+
| `dai skills install --from <git-url\|path>[#ref] [--for …]` | instala **skills externas** (por-stack: .NET, Java, …) desde un repo/dir, **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` |
|
|
161
162
|
| `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)) |
|
|
163
|
+
| `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)) |
|
|
162
164
|
| `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 |
|
|
163
165
|
| `dai link-us <ID> [--us <md>]` | crea branch + `implements.yaml`; sin `--us` trae la US del tracker |
|
|
164
166
|
| `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
|
|
@@ -173,12 +175,33 @@ flowchart TD
|
|
|
173
175
|
| `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) |
|
|
174
176
|
|
|
175
177
|
> **🆕 Mantené tu repo al día — `dai sync`.** Las skills, la constitución y los templates son un
|
|
176
|
-
> *caché derivable* del CLI. Cuando actualizás `dai` (`
|
|
178
|
+
> *caché derivable* del CLI. Cuando actualizás `dai` (`dai upgrade`), **`dai doctor` y
|
|
177
179
|
> `dai version` te avisan solos** si tu scaffold quedó atrás — con color y un `⬆️` —, y **`dai sync`**
|
|
178
180
|
> lo refresca: **aditivo** (conserva tu `CLAUDE.md` propio), sin tocar el `.env` ni OpenSpec. Probá sin
|
|
179
181
|
> riesgo con `dai sync --dry-run`. El versionado es semver: patch/minor no rompen nada; solo un major
|
|
180
182
|
> pediría migración. ([ADR-0010](docs/adr/0010-versionado-y-upgrade.md))
|
|
181
183
|
|
|
184
|
+
> **🧩 Skills de cualquier stack — `dai skills install --from`.** Además de las skills de
|
|
185
|
+
> dai, cada equipo suma las suyas (por-stack: .NET, Java, Rust…) desde su propio repo:
|
|
186
|
+
> `dai skills install --from github.com/tu-org/net-skills`. dai las **convierte para los 3
|
|
187
|
+
> asistentes** (Claude/Cursor/Copilot) e instala. **dai es el distribuidor de skills de
|
|
188
|
+
> cualquier stack, sin opinar sobre su contenido** — self-service, sin registro; `dai sync`
|
|
189
|
+
> sigue siendo solo de dai. ([ADR-0013](docs/adr/0013-skills-externas-install-from.md))
|
|
190
|
+
>
|
|
191
|
+
> La fuente puede ser **pública, privada (por SSH) o un path local**: dai no hace auth
|
|
192
|
+
> propia, delega en git — **si podés `git clone` el repo, dai instala desde ahí**. Para
|
|
193
|
+
> privados usá la forma SSH (`git@github.com:tu-org/net-skills.git`), consistente con el
|
|
194
|
+
> modelo de auth de dai ([ADR-0007](docs/adr/0007-modelo-de-autenticacion.md)). Público =
|
|
195
|
+
> cero fricción entre equipos/máquinas.
|
|
196
|
+
>
|
|
197
|
+
> **Cómo armar una skill que dai ingiera.** Cada skill es un directorio con un `SKILL.md`
|
|
198
|
+
> en **formato Agent Skills de Claude**: frontmatter con **`name`** y **`description`**
|
|
199
|
+
> (obligatorios) + el cuerpo con las instrucciones. dai **valida ese contrato**: si a un
|
|
200
|
+
> `SKILL.md` le falta `name`/`description`, o un dir no tiene `SKILL.md`, lo **saltea con
|
|
201
|
+
> un warn** (no instala una skill rota). El **contenido** no lo valida — bajo tu criterio.
|
|
202
|
+
> Molde: [`templates/skill.md`](templates/skill.md) · ejemplos reales: las skills de dai
|
|
203
|
+
> en [`skills/`](skills/).
|
|
204
|
+
|
|
182
205
|
Skills (se invocan en el asistente): `/doc-to-backlog` · `/grill-intent` · `/grill-epic` · `/grill-user-story` · `/link-us` ·
|
|
183
206
|
`/tdd` · `/dai-review`. Config del tracker (`md`\|`jira`\|`clickup`) y tokens: en `.env` —
|
|
184
207
|
ver [`.env.example`](.env.example). Auth (SSH + tokens): [ADR-0007](docs/adr/0007-modelo-de-autenticacion.md).
|
|
@@ -243,12 +266,18 @@ Las skills se invocan en el asistente — **no en todas sus superficies** (lími
|
|
|
243
266
|
| **Claude Desktop** | ✅ | `~/.claude/skills/` (global) — sin IDE ni consola |
|
|
244
267
|
| **Claude Code** | ✅ | `~/.claude/skills/` + `.claude/skills/` del repo |
|
|
245
268
|
| **Cursor Agent (IDE)** | ✅ | `.cursor/skills/*/SKILL.md` + `.cursor/rules/*.mdc` |
|
|
246
|
-
| **Copilot
|
|
247
|
-
| **Copilot
|
|
248
|
-
| **Copilot
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
269
|
+
| **Copilot app / CLI** | ✅ | `~/.copilot/skills/` (global) + `.github/skills/` del repo |
|
|
270
|
+
| **Copilot en VS Code / JetBrains** | ✅ | ídem (modo agente) |
|
|
271
|
+
| **Copilot code review · cloud agent** | ✅ | `.github/skills/` del repo |
|
|
272
|
+
| **Chat de github.com** | ❌ | no carga skills |
|
|
273
|
+
|
|
274
|
+
**El analista no necesita IDE**: cierra el ciclo con **Claude Desktop** o con la **app / CLI de
|
|
275
|
+
Copilot**. El CLI `dai` corre en cualquier terminal, con cualquier asistente o ninguno.
|
|
276
|
+
|
|
277
|
+
> Copilot lee el `SKILL.md` **tal cual**, sin conversión ([ADR-0014](docs/adr/0014-copilot-agent-skills.md)).
|
|
278
|
+
> Si vienes de dai ≤ 0.7, corre `dai init --for copilot` una vez: cambia los
|
|
279
|
+
> `.github/prompts/*.prompt.md` por `.github/skills/` (con los `templates/`, que antes se
|
|
280
|
+
> perdían) y limpia los viejos para que no te dupliquen cada `/comando`.
|
|
252
281
|
|
|
253
282
|
## Se adapta a cualquier escala
|
|
254
283
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.8.0
|
package/cli/dai.mjs
CHANGED
|
@@ -27,9 +27,12 @@ import { branchUrl, commitUrl, parseRemote, detectForge } from "./lib/forge-url.
|
|
|
27
27
|
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
|
-
import { parseFlags, parseAssistants, isAssistantToken } from "./lib/args.mjs";
|
|
30
|
+
import { parseFlags, parseAssistants, isAssistantToken, asList } from "./lib/args.mjs";
|
|
31
31
|
import { versionDrift, planUpgrade } from "./lib/semver.mjs";
|
|
32
|
-
import {
|
|
32
|
+
import { parseSource } from "./lib/skills-source.mjs";
|
|
33
|
+
import { skillToCursor, validateSkill, constitution, constitutionCursorRule, envFor, mergeEnv, upsertBlock, reconcileGitignore, stalePromptFiles } from "./lib/bootstrap.mjs";
|
|
34
|
+
import { parseFieldsFile, parseFieldOverrides, resolveJiraFields } from "./lib/jira-fields.mjs";
|
|
35
|
+
import { assertProjectKey } from "./lib/pm-jira.mjs";
|
|
33
36
|
|
|
34
37
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
35
38
|
|
|
@@ -47,6 +50,9 @@ const C = { y: (m) => paint("33", m), r: (m) => paint("31", m), cy: (m) => paint
|
|
|
47
50
|
const ROOT = join(HERE, ".."); // raíz del paquete dai (cli/ está adentro)
|
|
48
51
|
const CLAUDE_SKILLS_DIR = process.env.CLAUDE_SKILLS_DIR || join(homedir(), ".claude", "skills");
|
|
49
52
|
const CURSOR_SKILLS_DIR = process.env.CURSOR_SKILLS_DIR || join(homedir(), ".cursor", "skills");
|
|
53
|
+
// Copilot lee las skills personales de ~/.copilot/skills (NO de ~/.claude/skills, que
|
|
54
|
+
// solo mira VS Code). Por eso una skill "instalada global" no le aparecía (ADR-0014).
|
|
55
|
+
const COPILOT_SKILLS_DIR = process.env.COPILOT_SKILLS_DIR || join(homedir(), ".copilot", "skills");
|
|
50
56
|
function git(args, opts = {}) {
|
|
51
57
|
// stderr en 'pipe' (no 'inherit') para no filtrar errores de git a la salida;
|
|
52
58
|
// quedan en e.stderr para quien quiera inspeccionarlos.
|
|
@@ -237,16 +243,45 @@ async function cmdForge(sub, ref, opts) {
|
|
|
237
243
|
}
|
|
238
244
|
|
|
239
245
|
// ── publish: crea la US en el tracker desde un .md (fallback del MCP) ──────────
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
|
|
247
|
+
// Los campos propios que exige el proyecto, declarados en .dai/jira-fields.json
|
|
248
|
+
// (o donde apunte DAI_JIRA_FIELDS_FILE). Sin archivo → {}: un Jira sin campos
|
|
249
|
+
// obligatorios publica igual que siempre.
|
|
250
|
+
function loadJiraFieldsSpec() {
|
|
251
|
+
const path = process.env.DAI_JIRA_FIELDS_FILE || join(".dai", "jira-fields.json");
|
|
252
|
+
if (!existsSync(path)) return {};
|
|
253
|
+
return parseFieldsFile(readFileSync(path, "utf8"), path);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function cmdPublish(file, opts = {}) {
|
|
257
|
+
if (!file) fail("uso: dai publish <archivo-us.md> [--parent KEY] [--issuetype T] [--field alias=valor]", 1);
|
|
242
258
|
loadEnv();
|
|
243
259
|
const md = readFileSync(file, "utf8");
|
|
244
260
|
const title = extractTitle(md);
|
|
245
261
|
if (!title) fail("no pude extraer el título de la US (falta un '# Título').", 1);
|
|
246
262
|
const adapter = getAdapter(process.env);
|
|
247
263
|
if (typeof adapter.createUS !== "function") fail(`el backend '${adapter.kind}' no soporta crear US.`, 1);
|
|
248
|
-
|
|
264
|
+
|
|
265
|
+
const parent = typeof opts.parent === "string" ? opts.parent : undefined;
|
|
266
|
+
const issuetype = typeof opts.issuetype === "string" ? opts.issuetype : undefined;
|
|
267
|
+
const project = typeof opts.project === "string" ? opts.project : undefined;
|
|
268
|
+
|
|
269
|
+
// Los campos propios son cosa de Jira; los otros backends no los usan.
|
|
270
|
+
let fields;
|
|
271
|
+
if (adapter.kind === "jira") {
|
|
272
|
+
const type = issuetype || process.env.DAI_JIRA_ISSUETYPE || "Story";
|
|
273
|
+
fields = resolveJiraFields({
|
|
274
|
+
spec: loadJiraFieldsSpec(),
|
|
275
|
+
issuetype: type,
|
|
276
|
+
overrides: parseFieldOverrides(asList(opts.field)),
|
|
277
|
+
});
|
|
278
|
+
} else if (opts.field !== undefined) {
|
|
279
|
+
fail(`--field es solo para jira (DAI_PM=${adapter.kind}).`, 2);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const r = await adapter.createUS({ title, descriptionMarkdown: md, parent, issuetype, project, fields });
|
|
249
283
|
ok(`US publicada en ${adapter.kind}: ${r.id}${r.url ? ` → ${r.url}` : ""}`);
|
|
284
|
+
if (parent) info(`colgada de ${parent}`);
|
|
250
285
|
info(`Próximo paso (el dev abre el CÓMO): dai link-us ${r.id}`);
|
|
251
286
|
}
|
|
252
287
|
|
|
@@ -416,17 +451,15 @@ async function cmdPr(opts) {
|
|
|
416
451
|
|
|
417
452
|
// ── install: skills → ~/.claude/skills o <repo>/.claude/skills ────────────────
|
|
418
453
|
async function cmdInstall(opts) {
|
|
454
|
+
if (opts.from !== undefined) return cmdInstallFrom(opts); // skills externas (ADR-0013)
|
|
419
455
|
const skillsSrc = join(ROOT, "skills");
|
|
420
456
|
const skills = readdirSync(skillsSrc).filter((n) => statSync(join(skillsSrc, n)).isDirectory());
|
|
421
457
|
let want;
|
|
422
458
|
try { want = parseAssistants(typeof opts.for === "string" ? opts.for : "all"); }
|
|
423
459
|
catch (e) { fail(`--for ${e.message}`); }
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const wantClaude = want.claude, wantCursor = want.cursor;
|
|
428
|
-
if (!wantClaude && !wantCursor) fail("nada para instalar: pasá --for claude|cursor|all.");
|
|
429
|
-
const installFor = [wantClaude && "claude", wantCursor && "cursor"].filter(Boolean).join("+");
|
|
460
|
+
const wantClaude = want.claude, wantCursor = want.cursor, wantCopilot = want.copilot;
|
|
461
|
+
if (!wantClaude && !wantCursor && !wantCopilot) fail("nada para instalar: pasá --for claude|copilot|cursor|all.");
|
|
462
|
+
const installFor = [wantClaude && "claude", wantCopilot && "copilot", wantCursor && "cursor"].filter(Boolean).join("+");
|
|
430
463
|
const interactive = process.stdin.isTTY && !opts.global && opts.local === undefined;
|
|
431
464
|
const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
432
465
|
|
|
@@ -437,17 +470,21 @@ async function cmdInstall(opts) {
|
|
|
437
470
|
return a === "l" ? { scope: "local", repo: process.cwd() } : a === "s" ? { scope: "skip" } : { scope: "global" };
|
|
438
471
|
};
|
|
439
472
|
|
|
473
|
+
// Cada asistente tiene SU dir: Claude ~/.claude/skills · Copilot ~/.copilot/skills
|
|
474
|
+
// (ADR-0014) · Cursor ~/.cursor/skills. Local, el de Copilot es .github/skills.
|
|
475
|
+
const LOCAL_DIR = { claude: [".claude", "skills"], copilot: [".github", "skills"], cursor: [".cursor", "skills"] };
|
|
476
|
+
const GLOBAL_DIR = { claude: CLAUDE_SKILLS_DIR, copilot: COPILOT_SKILLS_DIR, cursor: CURSOR_SKILLS_DIR };
|
|
477
|
+
|
|
440
478
|
const installOne = async ({ name, scope, repo, kind }) => {
|
|
441
479
|
const src = join(skillsSrc, name);
|
|
442
|
-
const targetDir = scope === "local"
|
|
443
|
-
? join(repo, kind === "cursor" ? ".cursor" : ".claude", "skills")
|
|
444
|
-
: kind === "cursor" ? CURSOR_SKILLS_DIR : CLAUDE_SKILLS_DIR;
|
|
480
|
+
const targetDir = scope === "local" ? join(repo, ...LOCAL_DIR[kind]) : GLOBAL_DIR[kind];
|
|
445
481
|
const target = join(targetDir, name);
|
|
446
|
-
const label =
|
|
482
|
+
const label = `${name} (${kind})`;
|
|
447
483
|
|
|
448
484
|
let sameAsSource = false;
|
|
449
485
|
if (existsSync(target)) {
|
|
450
|
-
if (kind
|
|
486
|
+
if (kind !== "cursor") {
|
|
487
|
+
// claude y copilot son copia cruda del SKILL.md: comparación directa.
|
|
451
488
|
sameAsSource = dirsEqual(src, target);
|
|
452
489
|
} else {
|
|
453
490
|
const tempRoot = mkdtempSync(join(tmpdir(), "dai-cursor-skill-"));
|
|
@@ -484,11 +521,95 @@ async function cmdInstall(opts) {
|
|
|
484
521
|
const { scope, repo } = await scopeFor(name);
|
|
485
522
|
if (scope === "skip") { warn(`salto ${name}`); continue; }
|
|
486
523
|
if (wantClaude) await installOne({ name, scope, repo, kind: "claude" });
|
|
524
|
+
if (wantCopilot) await installOne({ name, scope, repo, kind: "copilot" });
|
|
487
525
|
if (wantCursor) await installOne({ name, scope, repo, kind: "cursor" });
|
|
488
526
|
}
|
|
489
527
|
if (rl) rl.close();
|
|
490
528
|
}
|
|
491
529
|
|
|
530
|
+
// ── skills install --from: skills EXTERNAS (por-stack) desde un repo/dir ───────
|
|
531
|
+
// Self-service, one-off, sin registro ni sync (ADR-0013). Convierte cada skill
|
|
532
|
+
// para los 3 asistentes y la instala en el repo. No pisa las skills built-in de
|
|
533
|
+
// dai (colisión → warn + skip). `dai sync` NO las toca: es solo de dai.
|
|
534
|
+
function cmdInstallFrom(opts) {
|
|
535
|
+
if (typeof opts.from !== "string" || !opts.from.trim())
|
|
536
|
+
fail("--from necesita una fuente: un git URL (github.com/org/skills[#ref]) o un path local", 2);
|
|
537
|
+
let want;
|
|
538
|
+
try { want = parseAssistants(typeof opts.for === "string" ? opts.for : "all"); }
|
|
539
|
+
catch (e) { fail(`--for ${e.message}`); }
|
|
540
|
+
|
|
541
|
+
let src;
|
|
542
|
+
try { src = parseSource(opts.from); } catch (e) { fail(`--from ${e.message}`, 2); }
|
|
543
|
+
|
|
544
|
+
// Resolver la fuente a un directorio local.
|
|
545
|
+
let root, tmp = null;
|
|
546
|
+
if (src.type === "git") {
|
|
547
|
+
tmp = mkdtempSync(join(tmpdir(), "dai-skills-"));
|
|
548
|
+
info(`clonando ${src.location}${src.ref ? " @ " + src.ref : ""} …`);
|
|
549
|
+
const args = ["clone", "--depth", "1"];
|
|
550
|
+
if (src.ref) args.push("--branch", src.ref);
|
|
551
|
+
args.push(src.location, tmp);
|
|
552
|
+
try { git(args); }
|
|
553
|
+
catch (e) { rmSync(tmp, { recursive: true, force: true }); fail(`no pude clonar la fuente: ${String(e.message).split("\n")[0]}`, 1); }
|
|
554
|
+
root = tmp;
|
|
555
|
+
} else {
|
|
556
|
+
root = src.location;
|
|
557
|
+
if (!existsSync(root)) fail(`no existe la fuente: ${root}`, 2);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const cleanup = () => { if (tmp) rmSync(tmp, { recursive: true, force: true }); };
|
|
561
|
+
|
|
562
|
+
// El dir de skills: <root>/skills, o <root> si ya contiene <name>/SKILL.md.
|
|
563
|
+
const skillsDir = existsSync(join(root, "skills")) ? join(root, "skills") : root;
|
|
564
|
+
let entries;
|
|
565
|
+
try {
|
|
566
|
+
entries = readdirSync(skillsDir).filter((n) => {
|
|
567
|
+
try { return statSync(join(skillsDir, n)).isDirectory(); } catch { return false; }
|
|
568
|
+
});
|
|
569
|
+
} catch { cleanup(); fail(`no pude leer la fuente: ${skillsDir}`, 2); }
|
|
570
|
+
if (entries.length === 0) { cleanup(); fail("la fuente no tiene directorios de skill", 2); }
|
|
571
|
+
|
|
572
|
+
// Destino: repo local (default cwd), o --local <repo> / --global.
|
|
573
|
+
const repo = opts.global ? homedir() : (typeof opts.local === "string" ? opts.local : process.cwd());
|
|
574
|
+
const builtins = new Set(readdirSync(join(ROOT, "skills")));
|
|
575
|
+
const forStr = ["claude", "copilot", "cursor"].filter((a) => want[a]).join("+");
|
|
576
|
+
info(`skills install --from ${opts.from} · ${entries.length} candidata(s) · asistentes: ${forStr}${opts.dryRun ? " [dry-run]" : ""}`);
|
|
577
|
+
|
|
578
|
+
let installed = 0, skipped = 0;
|
|
579
|
+
for (const name of entries) {
|
|
580
|
+
const srcDir = join(skillsDir, name);
|
|
581
|
+
const skillPath = join(srcDir, "SKILL.md");
|
|
582
|
+
// Validación estructural (contrato mínimo, no contenido — ADR-0013).
|
|
583
|
+
if (!existsSync(skillPath)) { warn(`'${name}' no tiene SKILL.md — salto`); skipped++; continue; }
|
|
584
|
+
if (builtins.has(name)) { warn(`'${name}' choca con una skill de dai — salto (renombrala, p.ej. ${name}-<stack>)`); skipped++; continue; }
|
|
585
|
+
const md = readFileSync(skillPath, "utf8");
|
|
586
|
+
const bad = validateSkill(md);
|
|
587
|
+
if (bad) { warn(`'${name}' inválida: ${bad} — salto (ver templates/skill.md)`); skipped++; continue; }
|
|
588
|
+
if (opts.dryRun) { info(`[dry-run] ${name} → ${forStr}`); continue; }
|
|
589
|
+
if (want.claude) {
|
|
590
|
+
const t = join(repo, ".claude", "skills", name);
|
|
591
|
+
rmSync(t, { recursive: true, force: true }); mkdirSync(dirname(t), { recursive: true }); cpSync(srcDir, t, { recursive: true });
|
|
592
|
+
}
|
|
593
|
+
if (want.cursor) {
|
|
594
|
+
const t = join(repo, ".cursor", "skills", name);
|
|
595
|
+
rmSync(t, { recursive: true, force: true }); cpSync(srcDir, t, { recursive: true }); writeFileSync(join(t, "SKILL.md"), skillToCursor(md));
|
|
596
|
+
}
|
|
597
|
+
if (want.copilot) {
|
|
598
|
+
const pdir = join(repo, ".github", "prompts"); mkdirSync(pdir, { recursive: true });
|
|
599
|
+
writeFileSync(join(pdir, `${name}.prompt.md`), skillToPrompt(md));
|
|
600
|
+
}
|
|
601
|
+
ok(name); installed++;
|
|
602
|
+
}
|
|
603
|
+
cleanup();
|
|
604
|
+
if (opts.dryRun) { info("dry-run: nada escrito."); return; }
|
|
605
|
+
process.stdout.write("\n");
|
|
606
|
+
if (installed === 0) {
|
|
607
|
+
fail(`0 skills instaladas${skipped ? ` (${skipped} salteada(s))` : ""} — revisá el formato: cada skill es un dir con SKILL.md y frontmatter (name + description). Molde en templates/skill.md`, 1);
|
|
608
|
+
}
|
|
609
|
+
ok(`${installed} skill(s) externa(s) instalada(s)${skipped ? `, ${skipped} salteada(s)` : ""} desde ${opts.from}`);
|
|
610
|
+
warn("skills externas — bajo tu criterio: dai las convierte e instala, no las vetea ni las trackea. `dai sync` no las toca.");
|
|
611
|
+
}
|
|
612
|
+
|
|
492
613
|
// ── helpers de prompt interactivo ─────────────────────────────────────────────
|
|
493
614
|
async function askMenu(rl, title, choices, def) {
|
|
494
615
|
process.stdout.write(`\n ${title}\n`);
|
|
@@ -607,12 +728,20 @@ async function cmdInit(repo, opts) {
|
|
|
607
728
|
ok(`Claude: .claude/skills/ (${skills.length}) + CLAUDE.md${cCur ? " (bloque dai, aditivo)" : ""} → conoce el método y las skills`);
|
|
608
729
|
}
|
|
609
730
|
if (wantCopilot) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
731
|
+
// Copilot lee SKILL.md nativo (Agent Skills, ADR-0014): copia cruda, sin convertir.
|
|
732
|
+
// Así viajan también los templates/ que la conversión a .prompt.md perdía.
|
|
733
|
+
const dir = join(repo, ".github", "skills");
|
|
734
|
+
mkdirSync(dir, { recursive: true });
|
|
735
|
+
for (const name of skills) cpSync(join(skillsSrc, name), join(dir, name), { recursive: true });
|
|
613
736
|
const ciPath = join(repo, ".github", "copilot-instructions.md"), ciCur = existsSync(ciPath) ? readFileSync(ciPath, "utf8") : "";
|
|
614
737
|
writeFileSync(ciPath, upsertBlock(ciCur, constitution("copilot")));
|
|
615
|
-
ok(`Copilot: .github/
|
|
738
|
+
ok(`Copilot: .github/skills/ (${skills.length}) + copilot-instructions.md${ciCur ? " (bloque dai, aditivo)" : ""}`);
|
|
739
|
+
// Los .prompt.md de versiones previas duplicarían cada /comando con una copia
|
|
740
|
+
// vieja y sin templates. Solo borramos los que generó dai, nunca los del equipo.
|
|
741
|
+
const pdir = join(repo, ".github", "prompts");
|
|
742
|
+
const stale = existsSync(pdir) ? stalePromptFiles(skills).filter((f) => existsSync(join(pdir, f))) : [];
|
|
743
|
+
for (const f of stale) rmSync(join(pdir, f), { force: true });
|
|
744
|
+
if (stale.length) info(` quité ${stale.length} .prompt.md viejo(s) de .github/prompts/ — ahora son skills`);
|
|
616
745
|
}
|
|
617
746
|
if (wantCursor) {
|
|
618
747
|
const dir = join(repo, ".cursor", "skills");
|
|
@@ -864,35 +993,52 @@ function cmdDoctor() {
|
|
|
864
993
|
loadEnv();
|
|
865
994
|
info(`dai doctor — versión v${readFileSync(join(ROOT, "VERSION"), "utf8").trim()}`);
|
|
866
995
|
|
|
867
|
-
// Una skill sirve si está en el repo actual (
|
|
868
|
-
//
|
|
996
|
+
// Una skill sirve si está en el repo actual (la puso `dai init`) o global (la puso
|
|
997
|
+
// `dai install`). Reportamos dónde — y SOLO de los asistentes que este repo usa: antes
|
|
998
|
+
// se listaban los tres siempre, así que quien configuró uno veía 14 warnings de los
|
|
999
|
+
// otros dos y leía "está todo roto" cuando estaba todo bien.
|
|
869
1000
|
const skillsRoot = join(ROOT, "skills");
|
|
870
1001
|
const skillNames = readdirSync(skillsRoot).filter((n) => statSync(join(skillsRoot, n)).isDirectory());
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1002
|
+
const cwd = process.cwd();
|
|
1003
|
+
const ASSISTANTS = [
|
|
1004
|
+
{ kind: "claude", label: "Claude", local: join(cwd, ".claude", "skills"), global: CLAUDE_SKILLS_DIR, home: "~/.claude/skills" },
|
|
1005
|
+
{ kind: "copilot", label: "Copilot", local: join(cwd, ".github", "skills"), global: COPILOT_SKILLS_DIR, home: "~/.copilot/skills" },
|
|
1006
|
+
{ kind: "cursor", label: "Cursor", local: join(cwd, ".cursor", "skills"), global: CURSOR_SKILLS_DIR, home: "~/.cursor/skills" },
|
|
1007
|
+
];
|
|
1008
|
+
const present = (a) => skillNames.some((n) => existsSync(join(a.local, n)) || existsSync(join(a.global, n)));
|
|
1009
|
+
const active = ASSISTANTS.filter(present);
|
|
1010
|
+
|
|
1011
|
+
if (active.length === 0) {
|
|
1012
|
+
warn("no encontré skills de dai para ningún asistente.");
|
|
1013
|
+
process.stdout.write(" En un repo: dai init --for claude|copilot|cursor|all\n");
|
|
1014
|
+
process.stdout.write(" O global: dai install --for all\n");
|
|
1015
|
+
}
|
|
1016
|
+
for (const a of active) {
|
|
1017
|
+
info(`skills ${a.label} (repo local / global ${a.home}):`);
|
|
1018
|
+
for (const name of skillNames) {
|
|
1019
|
+
const local = existsSync(join(a.local, name)), global = existsSync(join(a.global, name));
|
|
1020
|
+
if (local && global) ok(`${name} (local + global)`);
|
|
1021
|
+
else if (local) ok(`${name} (local, este repo)`);
|
|
1022
|
+
else if (global) ok(`${name} (global)`);
|
|
1023
|
+
else warn(`${name} — falta para ${a.label} (dai init --for ${a.kind}, o dai install --for ${a.kind})`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (active.some((a) => a.kind === "copilot")) {
|
|
1027
|
+
const ci = join(cwd, ".github", "copilot-instructions.md");
|
|
1028
|
+
existsSync(ci) ? ok("constitución Copilot (.github/copilot-instructions.md)") : warn("falta la constitución de Copilot (dai init --for copilot)");
|
|
1029
|
+
// Los .prompt.md viejos duplican cada /comando con una copia sin templates.
|
|
1030
|
+
const pdir = join(cwd, ".github", "prompts");
|
|
1031
|
+
const stale = existsSync(pdir) ? stalePromptFiles(skillNames).filter((f) => existsSync(join(pdir, f))) : [];
|
|
1032
|
+
if (stale.length) warn(`${stale.length} .prompt.md viejo(s) en .github/prompts/ — duplican las skills. Corré \`dai init --for copilot\` para limpiarlos.`);
|
|
1033
|
+
}
|
|
1034
|
+
if (active.some((a) => a.kind === "cursor")) {
|
|
1035
|
+
const localRule = join(cwd, ".cursor", "rules", "dai-constitution.mdc");
|
|
1036
|
+
const globalRule = join(homedir(), ".cursor", "rules", "dai-constitution.mdc");
|
|
1037
|
+
if (existsSync(localRule) && existsSync(globalRule)) ok("constitución Cursor (local + global)");
|
|
1038
|
+
else if (existsSync(localRule)) ok("constitución Cursor (local)");
|
|
1039
|
+
else if (existsSync(globalRule)) ok("constitución Cursor (global)");
|
|
1040
|
+
else warn("falta constitución Cursor (dai-constitution.mdc)");
|
|
1041
|
+
}
|
|
896
1042
|
|
|
897
1043
|
info("adaptador de PM:");
|
|
898
1044
|
const pm = process.env.DAI_PM || "md";
|
|
@@ -900,9 +1046,24 @@ function cmdDoctor() {
|
|
|
900
1046
|
if (pm === "jira") {
|
|
901
1047
|
if (!process.env.DAI_JIRA_BASE_URL) warn("falta DAI_JIRA_BASE_URL en el .env");
|
|
902
1048
|
if (!process.env.DAI_JIRA_EMAIL) warn("falta DAI_JIRA_EMAIL en el .env");
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1049
|
+
// Ojo: solo miramos que el token ESTÉ, no que sirva — uno vencido pasa este chequeo
|
|
1050
|
+
// y recién falla al publicar. Verificarlo de verdad es pegarle a la red.
|
|
1051
|
+
if (!process.env.DAI_JIRA_TOKEN) warn("falta DAI_JIRA_TOKEN en el .env"); else ok("token de Jira presente (no verificado: eso lo dice `dai publish`)");
|
|
1052
|
+
if (!process.env.DAI_JIRA_PROJECT) warn("DAI_JIRA_PROJECT vacío — solo hace falta para `dai publish` (crear issues)");
|
|
1053
|
+
else {
|
|
1054
|
+
try { ok(`proyecto=${assertProjectKey(process.env.DAI_JIRA_PROJECT)} (para dai publish)`); }
|
|
1055
|
+
catch (e) { warn(String(e.message)); }
|
|
1056
|
+
}
|
|
1057
|
+
// Campos propios del proyecto: que el archivo parsee ANTES de necesitarlo.
|
|
1058
|
+
const fpath = process.env.DAI_JIRA_FIELDS_FILE || join(".dai", "jira-fields.json");
|
|
1059
|
+
if (!existsSync(fpath)) info(`sin campos propios declarados (${fpath} no existe — normal si tu Jira no los exige)`);
|
|
1060
|
+
else {
|
|
1061
|
+
try {
|
|
1062
|
+
const spec = parseFieldsFile(readFileSync(fpath, "utf8"), fpath);
|
|
1063
|
+
const types = Object.keys(spec);
|
|
1064
|
+
ok(`campos propios: ${fpath} — issuetypes: ${types.length ? types.join(", ") : "(ninguno)"}`);
|
|
1065
|
+
} catch (e) { warn(String(e.message)); }
|
|
1066
|
+
}
|
|
906
1067
|
}
|
|
907
1068
|
if (pm === "clickup") {
|
|
908
1069
|
if (!process.env.DAI_CLICKUP_TOKEN) warn("falta DAI_CLICKUP_TOKEN en el .env"); else ok("token de ClickUp presente");
|
|
@@ -931,11 +1092,15 @@ switch (cmd) {
|
|
|
931
1092
|
case "check": cmdCheck().catch((e) => fail(String(e.message))); break;
|
|
932
1093
|
case "stamp": cmdStamp().catch((e) => fail(String(e.message))); break;
|
|
933
1094
|
case "forge": cmdForge(pos[0], pos[1], opts).catch((e) => fail(String(e.message))); break;
|
|
934
|
-
case "publish": cmdPublish(pos[0]).catch((e) => fail(String(e.message))); break;
|
|
1095
|
+
case "publish": cmdPublish(pos[0], opts).catch((e) => fail(String(e.message))); break;
|
|
935
1096
|
case "pr": cmdPr(opts).catch((e) => fail(String(e.message))); break;
|
|
936
1097
|
case "done": cmdDone(opts); break;
|
|
937
1098
|
case "archive": cmdArchive(pos[0], opts); break;
|
|
938
|
-
case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break;
|
|
1099
|
+
case "install": cmdInstall(opts).catch((e) => fail(String(e.message))); break; // alias de `dai skills install`
|
|
1100
|
+
case "skills":
|
|
1101
|
+
if (pos[0] === "install" || pos[0] === undefined) cmdInstall(opts).catch((e) => fail(String(e.message)));
|
|
1102
|
+
else fail(`subcomando de skills desconocido: '${pos[0]}' (por ahora: install)`, 2);
|
|
1103
|
+
break;
|
|
939
1104
|
case "init": cmdInit(pos[0], opts).catch((e) => fail(String(e.message))); break;
|
|
940
1105
|
case "sync": cmdSync(pos[0], opts); break;
|
|
941
1106
|
case "upgrade":
|
|
@@ -950,6 +1115,8 @@ switch (cmd) {
|
|
|
950
1115
|
" ac-hash <us.md> calcula el ac_hash (ADR-0001)\n" +
|
|
951
1116
|
" ls [--json] lista lo que implementa el repo (ADR-0005)\n" +
|
|
952
1117
|
" publish <us.md> crea la US en el tracker (Jira/ClickUp/md) y devuelve el key\n" +
|
|
1118
|
+
" [--parent KEY] la cuelga de su épica · [--issuetype T] p. ej. Epic\n" +
|
|
1119
|
+
" [--field alias=valor] campos propios que exige tu Jira (.dai/jira-fields.json); repetible\n" +
|
|
953
1120
|
" link-us <KEY> [--us <md>] crea branch + implements.yaml; sin --us trae la US del tracker (ADR-0004)\n" +
|
|
954
1121
|
" link-us <KEY> --resync re-estampa el ac_hash contra la US viva (tras un ⚠️ de check)\n" +
|
|
955
1122
|
" check compara vs la US viva → atrasado (ADR-0003)\n" +
|
|
@@ -959,7 +1126,8 @@ switch (cmd) {
|
|
|
959
1126
|
" pr [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
960
1127
|
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n\n" +
|
|
961
1128
|
"Instalación:\n" +
|
|
962
|
-
" install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] skills
|
|
1129
|
+
" skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] instala las skills de dai (alias: `install`)\n" +
|
|
1130
|
+
" skills install --from <git-url|path>[#ref] [--for <asistentes>] instala skills EXTERNAS (por-stack), convertidas para los 3 asistentes (ADR-0013)\n" +
|
|
963
1131
|
" init [<repo>] scaffolder interactivo del repo (asistente, gestor, OpenSpec)\n" +
|
|
964
1132
|
" --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
|
|
965
1133
|
" ej: --for claude,cursor · --for copilot · --for all\n" +
|
package/cli/lib/args.mjs
CHANGED
|
@@ -28,6 +28,17 @@ export function parseAssistants(str) {
|
|
|
28
28
|
return want;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Un flag REPETIDO acumula en lista en vez de pisarse (`--field a=1 --field b=2`).
|
|
32
|
+
// Antes ganaba el último en silencio, que para --field sería perder datos sin avisar.
|
|
33
|
+
const push = (opts, key, val) => {
|
|
34
|
+
if (!(key in opts)) { opts[key] = val; return; }
|
|
35
|
+
if (Array.isArray(opts[key])) opts[key].push(val);
|
|
36
|
+
else opts[key] = [opts[key], val];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Normaliza un flag que puede venir 0, 1 o N veces → siempre lista.
|
|
40
|
+
export const asList = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
|
|
41
|
+
|
|
31
42
|
export function parseFlags(argv) {
|
|
32
43
|
const opts = {};
|
|
33
44
|
const pos = [];
|
|
@@ -36,8 +47,8 @@ export function parseFlags(argv) {
|
|
|
36
47
|
if (a.startsWith("--")) {
|
|
37
48
|
const key = camel(a.slice(2));
|
|
38
49
|
const next = argv[i + 1];
|
|
39
|
-
if (next !== undefined && !next.startsWith("--")) { opts
|
|
40
|
-
else opts
|
|
50
|
+
if (next !== undefined && !next.startsWith("--")) { push(opts, key, next); i++; }
|
|
51
|
+
else push(opts, key, true); // flag booleano
|
|
41
52
|
} else {
|
|
42
53
|
pos.push(a);
|
|
43
54
|
}
|