@dforce2055/dai 0.13.3 → 0.14.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.dai.example +15 -0
- package/CHANGELOG.md +62 -0
- package/README.md +3 -2
- package/VERSION +1 -1
- package/cli/dai.mjs +177 -65
- package/cli/lib/bootstrap.mjs +14 -3
- package/cli/lib/branch-flow.mjs +82 -0
- package/cli/lib/branch-scope.mjs +10 -1
- package/cli/lib/help.mjs +448 -0
- package/cli/lib/pr-remote.mjs +80 -0
- package/cli/lib/us-format.mjs +14 -3
- package/cli/lib/us.mjs +5 -2
- package/governance/branch-naming.md +15 -1
- package/package.json +1 -1
package/.env.dai.example
CHANGED
|
@@ -12,6 +12,21 @@ DAI_PM=md
|
|
|
12
12
|
# Si dai no puede saber el link, avisa y deja la PR sin él — nunca escribe el id pelado.
|
|
13
13
|
# DAI_TRACKER_URL_TEMPLATE=https://jira.miempresa.com/browse/{id}
|
|
14
14
|
|
|
15
|
+
# ── Flujo de branches (dai pr · dai done) ────────────────────────────────────
|
|
16
|
+
# Las DOS ramas de vida larga del repo. No se configura "la base" de las PR: la base sale
|
|
17
|
+
# del TIPO de branch, y con eso dai deja de adivinar.
|
|
18
|
+
# feature/ · fix/ → PR contra DAI_BRANCH_DEV
|
|
19
|
+
# release/ · hotfix/ → PR contra DAI_BRANCH_PROD, con confirmación explícita
|
|
20
|
+
#
|
|
21
|
+
# Rama que INTEGRA el desarrollo (la que se despliega a test). Sin declarar, dai usa la
|
|
22
|
+
# rama default del remoto (origin/HEAD) y avisa que la está adivinando.
|
|
23
|
+
# DAI_BRANCH_DEV=testing
|
|
24
|
+
|
|
25
|
+
# Rama que DESPLIEGA A PRODUCCIÓN. `dai pr` la marca en el preview y pide confirmación
|
|
26
|
+
# antes de publicar; con --yes hace falta --to-prod. Sin declarar, dai no marca ninguna
|
|
27
|
+
# rama como producción: no adivina cuál es.
|
|
28
|
+
# DAI_BRANCH_PROD=main
|
|
29
|
+
|
|
15
30
|
# ── Backend md (local, offline) ──────────────────────────────────────────────
|
|
16
31
|
# Carpeta donde viven las US como <ID>.md (p. ej. ABC-482.md).
|
|
17
32
|
DAI_MD_US_DIR=.dai/us
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,67 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.14.0] — 2026-09-08
|
|
7
|
+
|
|
8
|
+
**`dai pr` proponía mergear a `main` en un repo donde `main` despliega a producción, y el
|
|
9
|
+
preview no lo destacaba de ninguna forma. Tirando de ese hilo apareció que el problema no
|
|
10
|
+
era el default: era pedirle a alguien que configure "la base", cuando la base no es una
|
|
11
|
+
constante — es consecuencia del tipo de branch. Y de yapa, el hallazgo más caro de la
|
|
12
|
+
versión: `dai <comando> --help` no imprimía ayuda, ejecutaba el comando.**
|
|
13
|
+
|
|
14
|
+
### Cambiado
|
|
15
|
+
- **La base de una PR sale del mapa de ramas del repo, no de un `main` fijo.** Se declaran
|
|
16
|
+
las **dos ramas de vida larga** en el `.env.dai` —`DAI_BRANCH_DEV` (la que integra) y
|
|
17
|
+
`DAI_BRANCH_PROD` (la que despliega a producción)— y `dai pr` deriva la base del **tipo de
|
|
18
|
+
branch**: `feature/` y `fix/` integran, `release/` y `hotfix/` van contra producción.
|
|
19
|
+
`--base` gana siempre. Sin nada declarado, dai cae a la rama default del remoto
|
|
20
|
+
(`origin/HEAD`) **y avisa que la está adivinando** — antes decía `main` sin más.
|
|
21
|
+
`dai done` usa el mismo mapa (tenía el mismo `main` hardcodeado) y `dai doctor` lo reporta.
|
|
22
|
+
El preview ahora dice **de dónde salió** la base, que era la mitad que faltaba
|
|
23
|
+
([#46](https://github.com/dforce2055/dai/issues/46)).
|
|
24
|
+
- **Apuntarle a producción pide confirmación explícita.** Si la base es `DAI_BRANCH_PROD`,
|
|
25
|
+
el preview la marca `⚠️ DESPLIEGA A PRODUCCIÓN` y hay que **escribir el nombre de la rama**
|
|
26
|
+
para seguir; con `--yes` hace falta `--to-prod`. Sin la variable declarada dai no marca
|
|
27
|
+
ninguna rama como producción: no adivina cuál es, y un gate inventado sobre una suposición
|
|
28
|
+
es peor que no tenerlo.
|
|
29
|
+
- **`dai <comando> --help` imprime ayuda en vez de ejecutar el comando.** Vale para todos:
|
|
30
|
+
`dai help`, `dai --help`, `dai -h`, `dai help <cmd>`, `dai <cmd> --help`, `dai <cmd> -h` y
|
|
31
|
+
`dai <cmd> help`. Siempre por `stdout` y siempre con código 0; un comando desconocido sigue
|
|
32
|
+
saliendo por `stderr` con código ≠ 0, que es lo que deja `dai foo --help` usable dentro de
|
|
33
|
+
un script. Cada comando tiene ayuda propia (qué hace, uso, opciones, ejemplo).
|
|
34
|
+
|
|
35
|
+
### Corregido
|
|
36
|
+
- **`dai pr` dejaba una MR con el diff al día y la descripción vieja.** Con una MR ya abierta
|
|
37
|
+
pusheaba la branch, fallaba al crear porque la MR existía, y la única señal era el comando
|
|
38
|
+
crudo del forge. Quedaba una MR que **miente sobre lo que contiene**, que es peor que un
|
|
39
|
+
error porque parece que salió bien. Ahora la detecta **antes** de pushear
|
|
40
|
+
(`gh pr list --head` / `glab mr list --source-branch`), el preview dice
|
|
41
|
+
`── Pull Request a ACTUALIZAR (#12) ──` y actualiza título y descripción. Si la detección no
|
|
42
|
+
pudo correr y el forge responde *"already exists"*, la busca y la actualiza igual; si tampoco
|
|
43
|
+
puede, lo dice con todas las letras: *"NO toqué su descripción: quedó la vieja"*. También
|
|
44
|
+
avisa si la MR abierta apunta a otra base que la pedida ([#46](https://github.com/dforce2055/dai/issues/46)).
|
|
45
|
+
- **`dai link-us` estampaba `version: v1` en una US que declaraba `v4`.** El regex del
|
|
46
|
+
`spec_version` exigía separador y la US lo escribía pegado (`specversion`) — y estaba
|
|
47
|
+
**duplicado en dos módulos**, que es exactamente por qué se podía arreglar en uno y seguir
|
|
48
|
+
roto en el otro. Ahora vive en un solo lugar y tolera `spec_version`, `spec version`,
|
|
49
|
+
`spec-version` y `specversion`. Sin `spec_version` declarado **no se inventa un `v1`**: queda
|
|
50
|
+
`pendiente` con aviso, porque ese número se publica en el cuerpo de la PR y se estampa en el
|
|
51
|
+
tracker como si fuera un dato. `dai check` además avisa cuando el número del link no coincide
|
|
52
|
+
con el de la US viva ([#46](https://github.com/dforce2055/dai/issues/46)).
|
|
53
|
+
- **`dai pr` no podía abrir la PR de un repo sin US.** En un repo que no se trackea a sí mismo
|
|
54
|
+
con User Stories —el de dai, sin ir más lejos— una branch `fix/` sin ID moría pidiendo un
|
|
55
|
+
link que no puede existir, y aconsejaba renombrarla a `chore/`, que para un fix es el consejo
|
|
56
|
+
equivocado. Ahora, si la branch no exige link **y** el repo no declara ninguna US, la PR sale
|
|
57
|
+
"Sin US" con el motivo. Una `feature/` sin link sigue fallando: ahí falta de verdad.
|
|
58
|
+
- **`.env.dai` no estaba en el `.gitignore` de este repo**, aunque `dai init` lo agrega en todos
|
|
59
|
+
los que scaffoldea. Faltaba justo en el que se publica en npm.
|
|
60
|
+
|
|
61
|
+
### Interno
|
|
62
|
+
- **408 tests** (+41): el mapa de ramas y la derivación por tipo de branch, la detección y
|
|
63
|
+
actualización de una PR existente en los dos forges, las variantes del `spec_version`, y la
|
|
64
|
+
convención de ayuda — con un test que recorre los `case` del dispatcher y **falla si alguno se
|
|
65
|
+
agrega sin ayuda**, para que la convención no dependa de acordarse.
|
|
66
|
+
|
|
6
67
|
## [0.13.3] — 2026-09-02
|
|
7
68
|
|
|
8
69
|
**Una PR de dai se abría diciendo, en el mismo párrafo, dos cosas que no encajaban: que el
|
|
@@ -857,6 +918,7 @@ ClickUp y Jira Cloud.
|
|
|
857
918
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
858
919
|
`files` de npm sin tests ni secretos.
|
|
859
920
|
|
|
921
|
+
[0.14.0]: https://github.com/dforce2055/dai/releases/tag/v0.14.0
|
|
860
922
|
[0.13.3]: https://github.com/dforce2055/dai/releases/tag/v0.13.3
|
|
861
923
|
[0.13.2]: https://github.com/dforce2055/dai/releases/tag/v0.13.2
|
|
862
924
|
[0.13.1]: https://github.com/dforce2055/dai/releases/tag/v0.13.1
|
package/README.md
CHANGED
|
@@ -202,13 +202,14 @@ flowchart TD
|
|
|
202
202
|
| `dai link-us <ID> --resync` | re-estampa el `ac_hash` contra la US viva (tras un ⚠️ de check) |
|
|
203
203
|
| `dai check` | compara tu código vs la US viva → ✅ al día / ⚠️ atrasado (exit code = gate de PR) |
|
|
204
204
|
| `dai ls [--json]` | lista las US que implementa el repo + su link al tracker |
|
|
205
|
-
| `dai pr [--assignee u] [--base b] [--draft] [--yes] [--us ID] [--title t] [--description t\|--description-file f] [--changes t\|--changes-file f]` · alias **`dai mr`** | crea TU PR/MR precargada:
|
|
205
|
+
| `dai pr [--assignee u] [--base b] [--draft] [--yes] [--us ID] [--title t] [--description t\|--description-file f] [--changes t\|--changes-file f]` · alias **`dai mr`** | crea **o actualiza** TU PR/MR precargada: propone la branch base **según el tipo de rama** (`feature/`/`fix/` → `DAI_BRANCH_DEV` · `release/`/`hotfix/` → `DAI_BRANCH_PROD`; `--base` gana, y si no hay nada declarado cae a la rama default del remoto **avisando que adivina**), muestra el texto y confirma antes de publicar. Contra la rama de producción pide una **confirmación explícita** (escribir el nombre de la rama; con `--yes` hace falta `--to-prod`). Si la branch **ya tiene una PR/MR abierta**, actualiza su título y su descripción en lugar de fallar dejando el diff al día y el body viejo. Detecta el forge (GitHub→PR con `gh` · GitLab→MR con `glab`); `mr` es el mismo comando, más natural en GitLab. **La US la resuelve la branch** (la nombra `dai link-us`): si hay varias vivas y ninguna coincide, **pregunta** en vez de elegir por vos (sin TTY falla pidiendo `--us <ID>`), y una branch `chore/`/`docs/` sale **sin US** en lugar de heredar la de otro. **La descripción la escribís vos (o tu agente) con `--description`**: dai llena la US, los commits y los links, pero no inventa el propósito de un cambio — si "Descripción" o "Cambios realizados" quedarían con el molde del template, con `--yes` o sin TTY **no publica** y te dice qué falta |
|
|
206
206
|
| `dai stamp` | estampa la cobertura inversa en el tracker (branch + commit-ancla) |
|
|
207
|
-
| `dai done [--base
|
|
207
|
+
| `dai done [--base b] [--force]` | cierra la US: vuelve a la base (se resuelve igual que en `dai pr`), `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 |
|
|
208
208
|
| `dai archive [<change>] [--skip-specs]` | **funde los delta specs del change en las specs canónicas** (`openspec/specs/`) y lo archiva. Lo corre el **aprobador** de la PR (gate de aprobación, [ADR-0011](docs/adr/0011-archive-gate-de-aprobacion.md)); detecta el change activo o le pasás el nombre. Envuelve `openspec archive` |
|
|
209
209
|
| `dai forge review <ref> --from <review.json>` `[--dry-run\|--yes]` | **review inline**: un resumen + un comentario anclado a cada `archivo:línea`, clasificado low/medium/high. **Valida cada posición contra el diff** (descarta lo que el modelo inventó) antes de postear; sin `--yes` muestra el preview y no postea nada. Modo desatendido: `--min-severity`/`--min-confidence`/`--max-comments`. El review sale con `event: COMMENT`, nunca `APPROVE` ([ADR-0016](docs/adr/0016-review-inline.md)) |
|
|
210
210
|
| `dai forge comment <ref> --body-file <f>` · `dai forge pr <ref>` | comentar / leer una PR/MR (GitHub/GitLab) — el fallback simple, sin anclar |
|
|
211
211
|
| `dai ac-hash <us.md>` | calcula el hash de los criterios de aceptación de una US |
|
|
212
|
+
| `dai help [<comando>]` · `dai <comando> --help` | ayuda del CLI. Pedir ayuda **nunca ejecuta el comando**: sale por `stdout` y termina con 0. Valen `--help`, `-h` y `dai <comando> help` — las tres formas, en todos los comandos |
|
|
212
213
|
| `dai doctor` · `dai docs <dest>` · `dai version` | diagnóstico del entorno (incluye **version-drift** del scaffold) · copiar la doc (sin los assets del sitio; los links a las capturas apuntan al sitio publicado) · versión (`dai version` avisa si tu repo quedó atrás) |
|
|
213
214
|
|
|
214
215
|
> **🆕 Mantené tu repo al día — `dai sync`.** Las skills, la constitución y los templates son un
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.14.0
|
package/cli/dai.mjs
CHANGED
|
@@ -28,6 +28,8 @@ import { parsePrRef, getPR, postComment, postReview } from "./lib/forge-api.mjs"
|
|
|
28
28
|
import { trackerUrl } from "./lib/tracker-url.mjs";
|
|
29
29
|
import { parseFindings, diffPositions, validateFindings, filterFindings, renderFindingBody, renderReviewSummary } from "./lib/review-findings.mjs";
|
|
30
30
|
import { composePrBody, prTitle, forgeTool, bodyGaps } from "./lib/pr.mjs";
|
|
31
|
+
import { resolveBase, isProdBranch, branchFlow, baseHint, parseOriginHead } from "./lib/branch-flow.mjs";
|
|
32
|
+
import { listPrCmd, parsePrList, updatePrCmd, isAlreadyExistsError, describeUpdate } from "./lib/pr-remote.mjs";
|
|
31
33
|
import { absolutizeSiteLinks } from "./lib/docs-links.mjs";
|
|
32
34
|
import { diagnoseGitSsh, pushFailureHint, WINDOWS_OPENSSH } from "./lib/git-ssh.mjs";
|
|
33
35
|
import { dirsEqual } from "./lib/fsutil.mjs";
|
|
@@ -39,7 +41,8 @@ import { parseFieldsFile, parseFieldOverrides, resolveJiraFields } from "./lib/j
|
|
|
39
41
|
import { assertProjectKey } from "./lib/pm-jira.mjs";
|
|
40
42
|
import { flattenImplements, stampScope, prScope, matchBranchToImplements, requiresLink, trackerKeysIn } from "./lib/branch-scope.mjs";
|
|
41
43
|
import { describeForgeError, parseForgeError } from "./lib/forge-api.mjs";
|
|
42
|
-
import { validateUS, renderValidation, parseSpecVersion, bumpSpecVersion, setSpecVersion } from "./lib/us-format.mjs";
|
|
44
|
+
import { validateUS, renderValidation, parseSpecVersion, bumpSpecVersion, setSpecVersion, PENDING_VERSION } from "./lib/us-format.mjs";
|
|
45
|
+
import { isHelpToken, wantsHelp, helpTopic, helpFor, globalUsage } from "./lib/help.mjs";
|
|
43
46
|
|
|
44
47
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
48
|
|
|
@@ -150,13 +153,14 @@ function cmdLs(opts) {
|
|
|
150
153
|
async function cmdLinkUs(key, opts) {
|
|
151
154
|
if (!isValidKey(key)) fail(`key inválido: '${key}'. Sin espacios ni barras (ej.: ABC-482 o 86cxyz).`, 1);
|
|
152
155
|
|
|
153
|
-
let title, hash, version =
|
|
156
|
+
let title, hash, version = null;
|
|
154
157
|
if (opts.us) {
|
|
155
158
|
// Fuente local: un .md con la US.
|
|
156
159
|
const md = readFileSync(opts.us, "utf8");
|
|
157
160
|
hash = acHash(md);
|
|
158
161
|
if (hash == null) fail(`la US en ${opts.us} no tiene una sección 'Criterios de aceptación' con criterios testeables → sin ac_hash.\n Agregá los criterios bajo '## Criterios de aceptación', o corré /grill-user-story para pulir la US.`, 2);
|
|
159
162
|
title = opts.title || extractTitle(md);
|
|
163
|
+
version = parseSpecVersion(md);
|
|
160
164
|
} else {
|
|
161
165
|
// Fuente tracker: traer la US del adaptador (mismo hash que usará `dai check`).
|
|
162
166
|
loadDaiEnv();
|
|
@@ -166,7 +170,16 @@ async function cmdLinkUs(key, opts) {
|
|
|
166
170
|
hash = us.ac_hash;
|
|
167
171
|
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);
|
|
168
172
|
title = opts.title || us.title;
|
|
169
|
-
version = us.spec_version
|
|
173
|
+
version = us.spec_version;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Sin spec_version NO se inventa un `v1`: ese número se publica en la PR y se estampa en
|
|
177
|
+
// el tracker como si fuera un dato, y `dai check` lo reporta con un ✅ que da por buena
|
|
178
|
+
// una versión que no existe (issue #46). Un placeholder visible dice la verdad.
|
|
179
|
+
if (!version) {
|
|
180
|
+
version = PENDING_VERSION;
|
|
181
|
+
warn(`la US ${key} no declara spec_version — el link queda con 'version: ${PENDING_VERSION}'.`);
|
|
182
|
+
process.stdout.write(` Agregá la fila 'spec_version | v1' a la metadata de la US y re-estampá: dai link-us ${key} --resync\n`);
|
|
170
183
|
}
|
|
171
184
|
|
|
172
185
|
// ── modo resync: re-estampar el ac_hash en el implements.yaml existente ──────
|
|
@@ -222,6 +235,14 @@ function gitCommit() { try { return git(["rev-parse", "HEAD"]); } catch { return
|
|
|
222
235
|
function resolveRev(ref) {
|
|
223
236
|
try { git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]); return ref; } catch { return null; }
|
|
224
237
|
}
|
|
238
|
+
// La rama default del remoto (`origin/HEAD`). Es mejor fallback que un `main` fijo, pero
|
|
239
|
+
// sigue siendo un fallback: en un repo con ramas de ambiente, la default del remoto suele
|
|
240
|
+
// ser justo la de producción. Por eso el preview siempre dice de dónde salió la base.
|
|
241
|
+
function originHeadBranch() {
|
|
242
|
+
try { return parseOriginHead(git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"])); }
|
|
243
|
+
catch { return null; }
|
|
244
|
+
}
|
|
245
|
+
|
|
225
246
|
// Un texto que se puede pasar inline (`--description "…"`) o por archivo
|
|
226
247
|
// (`--description-file notas.md`). Un agente casi siempre quiere el archivo: markdown
|
|
227
248
|
// multilínea no sobrevive entero a la línea de comandos.
|
|
@@ -297,7 +318,7 @@ async function cmdCheckCi(opts = {}) {
|
|
|
297
318
|
try { live = await adapter.fetchUS(r.id); } catch (e) { netErr = String(e.message).split("\n")[0]; }
|
|
298
319
|
if (netErr) { warn(`${r.id}: no pude leer la US (${netErr}) — no bloqueo por un problema de red/credencial.`); continue; }
|
|
299
320
|
const status = coverageStatus(r.ac_hash, live?.ac_hash);
|
|
300
|
-
if (status === "al-dia") ok(`${r.id} al día (${r.version})`);
|
|
321
|
+
if (status === "al-dia") { ok(`${r.id} al día (${r.version})`); versionDriftHint(r, live); }
|
|
301
322
|
else if (status === "atrasado") {
|
|
302
323
|
process.stderr.write(`✗ gate: ${r.id} ATRASADO — implementaste ${r.ac_hash}, la US viva es ${live.ac_hash}.\n` +
|
|
303
324
|
` El QUÉ cambió. Resincronizá y revisá que lo cubras: dai link-us ${r.id} --resync\n`);
|
|
@@ -321,6 +342,18 @@ function ciBranch() {
|
|
|
321
342
|
(e.GITHUB_REF_NAME && !/^\d+\/merge$/.test(e.GITHUB_REF_NAME) ? e.GITHUB_REF_NAME : null) || null;
|
|
322
343
|
}
|
|
323
344
|
|
|
345
|
+
// El ac_hash dice si el QUÉ cambió; el `version` del link es el número que se PUBLICA
|
|
346
|
+
// (en el body de la PR, en el stamp del tracker). Que coincidan no es cosmético: un link
|
|
347
|
+
// al día con `version: v1` contra una US en v4 estampa una versión que no existe, y el ✅
|
|
348
|
+
// lo hace pasar por verificado (issue #46). No cambia el exit code — el hash manda.
|
|
349
|
+
function versionDriftHint(im, live) {
|
|
350
|
+
const declared = String(im?.version ?? "").trim();
|
|
351
|
+
const real = String(live?.spec_version ?? "").trim();
|
|
352
|
+
if (!real || declared === real) return;
|
|
353
|
+
warn(`${im.id}: el link declara version '${declared || "(vacío)"}' y la US viva dice '${real}'.`);
|
|
354
|
+
process.stdout.write(` Ese número se publica en la PR y se estampa en el tracker. Corregilo: dai link-us ${im.id} --resync\n`);
|
|
355
|
+
}
|
|
356
|
+
|
|
324
357
|
// ── check ──────────────────────────────────────────────────────────────────
|
|
325
358
|
// `process.exitCode` en vez de `process.exit()`: ver la nota en cmdCheckCi.
|
|
326
359
|
async function cmdCheck() {
|
|
@@ -334,7 +367,10 @@ async function cmdCheck() {
|
|
|
334
367
|
n++;
|
|
335
368
|
const { us: live, unreachable, reason } = await fetchLiveUS(adapter, im.id);
|
|
336
369
|
const status = coverageStatus(im.ac_hash, live?.ac_hash, { unreachable });
|
|
337
|
-
if (status === "al-dia")
|
|
370
|
+
if (status === "al-dia") {
|
|
371
|
+
process.stdout.write(`✅ ${im.id} al día (${im.version})\n`);
|
|
372
|
+
versionDriftHint(im, live);
|
|
373
|
+
}
|
|
338
374
|
else if (status === "atrasado") {
|
|
339
375
|
process.stdout.write(`⚠️ ${im.id} ATRASADO: implementaste ${im.ac_hash}, la US viva es ${live.ac_hash}${live.spec_version ? ` (${live.spec_version})` : ""}\n`);
|
|
340
376
|
atrasadas.push(im.id);
|
|
@@ -833,8 +869,13 @@ async function cmdUpdateUs(id, opts = {}) {
|
|
|
833
869
|
|
|
834
870
|
// ── done: cierra una US — vuelve a la base, actualiza y borra la branch local ──
|
|
835
871
|
function cmdDone(opts) {
|
|
836
|
-
|
|
872
|
+
loadDaiEnv();
|
|
873
|
+
// Misma resolución que `dai pr`: el `main` fijo mandaba a checkout+pull a la rama
|
|
874
|
+
// equivocada en cualquier repo que integre contra develop/testing (issue #46).
|
|
875
|
+
if (opts.base === true) fail("--base necesita el nombre de una branch (ej: --base develop).", 1);
|
|
876
|
+
const baseFlag = Array.isArray(opts.base) ? opts.base[opts.base.length - 1] : (typeof opts.base === "string" ? opts.base : null);
|
|
837
877
|
const branch = gitBranch();
|
|
878
|
+
const { base, source: baseSource } = resolveBase({ flag: baseFlag, branch, env: process.env, originHead: originHeadBranch() });
|
|
838
879
|
if (!branch || branch === "HEAD") fail("no estás en una branch.", 1);
|
|
839
880
|
if (branch === base) fail(`ya estás en '${base}' — nada que cerrar.`, 1);
|
|
840
881
|
|
|
@@ -857,7 +898,7 @@ function cmdDone(opts) {
|
|
|
857
898
|
).map((r) => r.id);
|
|
858
899
|
|
|
859
900
|
// Ir a la base y actualizar.
|
|
860
|
-
info(`Cambiando a '${base}' y actualizando
|
|
901
|
+
info(`Cambiando a '${base}' y actualizando… ${C.dim(`(base: ${baseSource})`)}`);
|
|
861
902
|
try { git(["checkout", base]); } catch (e) { fail(`no pude cambiar a '${base}': ${String(e.message).split("\n")[0]}`, 1); }
|
|
862
903
|
try { git(["fetch", "--prune"]); } catch { /* sin remoto */ }
|
|
863
904
|
try { git(["pull", "--ff-only"]); } catch { warn(`no pude hacer 'pull --ff-only' en '${base}' (¿divergió?). Revisa a mano.`); }
|
|
@@ -879,6 +920,23 @@ function cmdDone(opts) {
|
|
|
879
920
|
info("La branch remota (si existe) la maneja el forge (auto-delete on merge) o bórrala tú.");
|
|
880
921
|
}
|
|
881
922
|
|
|
923
|
+
// El comando del forge, listo para copiar y pegar. Cuando dai no llega, deja al dev
|
|
924
|
+
// parado exactamente donde estaba, no un paso atrás.
|
|
925
|
+
function shellHint(tool, cmd) {
|
|
926
|
+
return ` Comando listo para correr a mano:\n ${tool} ${cmd.map((c) => /\s/.test(c) ? `'${c}'` : c).join(" ")}\n`;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// La PR/MR abierta de esta branch, si la hay.
|
|
930
|
+
// { number, url, title, base } → existe · null → no hay · undefined → no se pudo saber
|
|
931
|
+
// El `undefined` importa: sin binario, sin auth o con un glab viejo, dai no puede afirmar
|
|
932
|
+
// que NO existe, así que sigue por el camino de crear (que también sabe reconocerla).
|
|
933
|
+
function findExistingPr(tool, branch) {
|
|
934
|
+
try {
|
|
935
|
+
const out = execFileSync(tool, listPrCmd(tool, branch), { encoding: "utf8", cwd: process.cwd(), stdio: ["ignore", "pipe", "pipe"] });
|
|
936
|
+
return parsePrList(tool, out);
|
|
937
|
+
} catch { return undefined; }
|
|
938
|
+
}
|
|
939
|
+
|
|
882
940
|
// ── pr: crea TU PROPIA PR/MR precargada desde el template + el link ────────────
|
|
883
941
|
// (Distinto de dai-review, que revisa la PR de OTRO. Tu PR la creas y revisas tú.)
|
|
884
942
|
async function cmdPr(opts) {
|
|
@@ -906,9 +964,18 @@ async function cmdPr(opts) {
|
|
|
906
964
|
};
|
|
907
965
|
const closeRl = () => { if (_rl) { _rl.close(); _rl = null; } };
|
|
908
966
|
|
|
909
|
-
// Elegir la branch base
|
|
910
|
-
|
|
911
|
-
|
|
967
|
+
// Elegir la branch base. El default NO es `main` fijo: sale de la config del repo
|
|
968
|
+
// (DAI_BRANCH_DEV / DAI_BRANCH_PROD) o de la rama default del remoto, y el preview lo dice.
|
|
969
|
+
// Con `main` hardcodeado, en un repo donde main DESPLIEGA A PRODUCCIÓN la MR quedaba
|
|
970
|
+
// proponiendo un merge a PRO y nada lo destacaba (issue #46).
|
|
971
|
+
if (opts.base === true) { closeRl(); fail("--base necesita el nombre de una branch (ej: --base develop).", 1); }
|
|
972
|
+
const baseFlag = Array.isArray(opts.base) ? opts.base[opts.base.length - 1] : (typeof opts.base === "string" ? opts.base : null);
|
|
973
|
+
let { base, source: baseSource, reason: baseWhy } = resolveBase({ flag: baseFlag, branch, env: process.env, originHead: originHeadBranch() });
|
|
974
|
+
if (!baseFlag) {
|
|
975
|
+
const ans = await ask(` ¿Contra qué branch va la PR? (${base}) `);
|
|
976
|
+
if (ans) { base = ans; baseSource = "lo respondiste vos"; baseWhy = null; }
|
|
977
|
+
}
|
|
978
|
+
const toProd = isProdBranch(base, process.env);
|
|
912
979
|
|
|
913
980
|
// La base puede no existir LOCAL (clones con --single-branch, repos donde el dev
|
|
914
981
|
// trabaja sobre develop y la base es main, corporativos con la base solo en origin).
|
|
@@ -1012,13 +1079,40 @@ async function cmdPr(opts) {
|
|
|
1012
1079
|
const forge = detectForge(parseRemote(remote)?.host);
|
|
1013
1080
|
const tool = forgeTool(forge);
|
|
1014
1081
|
|
|
1082
|
+
// ¿Ya hay una PR/MR abierta para esta branch? Si la hay, esto es una ACTUALIZACIÓN, y
|
|
1083
|
+
// hay que decirlo ANTES de confirmar: el bug del issue #46 era pushear (el diff quedaba
|
|
1084
|
+
// al día), fallar al crear, y dejar la MR con la descripción vieja sin avisar.
|
|
1085
|
+
// null → no hay ninguna abierta · undefined → no se pudo saber (sin binario, sin auth)
|
|
1086
|
+
const existing = findExistingPr(tool, branch);
|
|
1087
|
+
|
|
1015
1088
|
// 4. Mostrar y pedir confirmación (acción hacia afuera).
|
|
1016
|
-
|
|
1017
|
-
process.stdout.write(
|
|
1089
|
+
const accion = existing ? `a ACTUALIZAR (#${existing.number})` : "a crear";
|
|
1090
|
+
process.stdout.write(`\n ── Pull Request ${accion} ──────────────────────────────\n`);
|
|
1091
|
+
process.stdout.write(` título: ${title}\n de: ${branch}\n`);
|
|
1092
|
+
process.stdout.write(` a: ${base} ${C.dim(`(${baseSource}${baseWhy ? ` — ${baseWhy}` : ""})`)}${toProd ? ` ${C.b("⚠️ DESPLIEGA A PRODUCCIÓN")}` : ""}\n`);
|
|
1018
1093
|
process.stdout.write(` US: ${id || C.dim("(sin US)")} ${C.dim(`— ${usWhy}`)}\n`);
|
|
1019
1094
|
process.stdout.write(` forge: ${forge} (${tool})${opts.assignee ? `\n asignar: ${opts.assignee}` : ""}${opts.draft ? "\n draft: sí" : ""}\n`);
|
|
1020
1095
|
process.stdout.write(` ─────────────────────────────────────────────────────\n\n${body}\n`);
|
|
1021
1096
|
process.stdout.write(` ─────────────────────────────────────────────────────\n`);
|
|
1097
|
+
const hint = baseHint(baseSource, base);
|
|
1098
|
+
if (hint) info(hint);
|
|
1099
|
+
if (existing) for (const l of describeUpdate(existing, { base, tool })) warn(l);
|
|
1100
|
+
|
|
1101
|
+
// 4a. Gate de producción: proponer un merge a la rama que despliega a PRO no puede salir
|
|
1102
|
+
// de un default ni colarse con un `--yes` puesto por costumbre. Que lo diga alguien.
|
|
1103
|
+
if (toProd) {
|
|
1104
|
+
warn(`'${base}' está declarada como rama de PRODUCCIÓN (DAI_BRANCH_PROD).`);
|
|
1105
|
+
if (!opts.toProd) {
|
|
1106
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
1107
|
+
closeRl();
|
|
1108
|
+
fail(`no publico una PR contra producción sin que lo digas explícitamente.\n` +
|
|
1109
|
+
` Si es a propósito: dai pr --base ${base} --to-prod --yes\n` +
|
|
1110
|
+
` Si era otra la base: dai pr --base <rama-de-integracion>`, 1);
|
|
1111
|
+
}
|
|
1112
|
+
const a = await ask(` Escribí '${base}' para confirmar que esta PR va a PRODUCCIÓN (Enter = cancelar): `);
|
|
1113
|
+
if (String(a ?? "").trim() !== base) { closeRl(); info("Cancelado — no se creó la PR."); return; }
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1022
1116
|
|
|
1023
1117
|
// 4b. Gate: una PR con el molde del template sin llenar no se puede revisar.
|
|
1024
1118
|
// Pasaba en repos reales — "Descripción" con el comentario HTML (que no se renderiza:
|
|
@@ -1044,8 +1138,8 @@ async function cmdPr(opts) {
|
|
|
1044
1138
|
// Archivo de paso para gh/glab: en el temp del sistema, NO en el repo (no lo ensucia).
|
|
1045
1139
|
const bodyFile = join(mkdtempSync(join(tmpdir(), "dai-pr-")), "body.md");
|
|
1046
1140
|
if (!opts.yes) {
|
|
1047
|
-
if (!process.stdin.isTTY) { closeRl(); writeFileSync(bodyFile, body); info(`Body guardado en ${bodyFile}. Revisa y re-ejecuta con --yes para crear.`); return; }
|
|
1048
|
-
const a = (await ask(` ¿Publico la branch y creo el PR con ${tool}? (s/N) `) || "").toLowerCase();
|
|
1141
|
+
if (!process.stdin.isTTY) { closeRl(); writeFileSync(bodyFile, body); info(`Body guardado en ${bodyFile}. Revisa y re-ejecuta con --yes para ${existing ? "actualizar" : "crear"}.`); return; }
|
|
1142
|
+
const a = (await ask(` ¿Publico la branch y ${existing ? `actualizo la PR/MR #${existing.number}` : `creo el PR`} con ${tool}? (s/N) `) || "").toLowerCase();
|
|
1049
1143
|
closeRl();
|
|
1050
1144
|
if (!["s", "si", "sí", "y", "yes"].includes(a)) {
|
|
1051
1145
|
writeFileSync(bodyFile, body);
|
|
@@ -1081,6 +1175,29 @@ async function cmdPr(opts) {
|
|
|
1081
1175
|
fail(`no pude pushear la branch '${branch}'.`, 1);
|
|
1082
1176
|
}
|
|
1083
1177
|
|
|
1178
|
+
// Actualizar la PR/MR que ya está abierta: mismo body, misma disciplina. La alternativa
|
|
1179
|
+
// —fallar y dejarla con la descripción vieja— es la que rompía el issue #46.
|
|
1180
|
+
const doUpdate = (number) => {
|
|
1181
|
+
const ucmd = updatePrCmd(tool, { number, title, body, bodyFile });
|
|
1182
|
+
try {
|
|
1183
|
+
info(`Actualizando la PR/MR #${number} con ${tool}…`);
|
|
1184
|
+
const out = execFileSync(tool, ucmd, { encoding: "utf8", cwd: process.cwd() });
|
|
1185
|
+
process.stdout.write(out);
|
|
1186
|
+
ok(`PR/MR #${number} actualizada: título + descripción${existing?.url ? ` — ${existing.url}` : ""}.`);
|
|
1187
|
+
try { rmSync(bodyFile); } catch { /* noop */ }
|
|
1188
|
+
return true;
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
const msg = String(e.stderr || e.message || "");
|
|
1191
|
+
warn(`no pude actualizar la PR/MR #${number} con ${tool}. El body quedó en ${bodyFile}.`);
|
|
1192
|
+
if (msg.trim()) process.stdout.write(` ${tool} dijo:\n ${msg.trim().split("\n").join("\n ")}\n`);
|
|
1193
|
+
process.stdout.write(shellHint(tool, ucmd));
|
|
1194
|
+
process.stdout.write(` Si preferís no editarla: cerrá la PR/MR y volvé a correr \`dai pr\`.\n`);
|
|
1195
|
+
process.exitCode = 1;
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
if (existing) { doUpdate(existing.number); return; }
|
|
1200
|
+
|
|
1084
1201
|
const cmd = tool === "gh"
|
|
1085
1202
|
? ["pr", "create", "--title", title, "--body-file", bodyFile, "--base", base,
|
|
1086
1203
|
...(opts.assignee ? ["--assignee", opts.assignee] : []), ...(opts.draft ? ["--draft"] : [])]
|
|
@@ -1094,7 +1211,18 @@ async function cmdPr(opts) {
|
|
|
1094
1211
|
try { rmSync(bodyFile); } catch { /* noop */ }
|
|
1095
1212
|
} catch (e) {
|
|
1096
1213
|
const msg = String(e.stderr || e.message || "");
|
|
1097
|
-
const manual =
|
|
1214
|
+
const manual = shellHint(tool, cmd);
|
|
1215
|
+
if (isAlreadyExistsError(msg) || isAlreadyExistsError(String(e.stdout || ""))) {
|
|
1216
|
+
// La detección de arriba no la vio (glab viejo, `--output json` no soportado, sin
|
|
1217
|
+
// permiso de lectura). El forge sí sabe que existe: se busca de nuevo y se actualiza.
|
|
1218
|
+
warn("el forge dice que YA hay una PR/MR abierta para esta branch, así que no se creó otra.");
|
|
1219
|
+
const found = findExistingPr(tool, branch);
|
|
1220
|
+
if (found) { doUpdate(found.number); return; }
|
|
1221
|
+
warn(`tampoco pude averiguar su número con ${tool}, así que NO toqué su descripción: quedó la vieja.`);
|
|
1222
|
+
process.stdout.write(` Abrila y pegá el body de ${bodyFile}, o cerrala y volvé a correr \`dai pr\`.\n`);
|
|
1223
|
+
process.exitCode = 1;
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1098
1226
|
if (e.code === "ENOENT") {
|
|
1099
1227
|
// El binario del forge no está instalado (el caso más común detrás de "no salió la MR").
|
|
1100
1228
|
const doc = tool === "glab" ? "https://gitlab.com/gitlab-org/cli/-/releases" : "https://cli.github.com";
|
|
@@ -1869,6 +1997,20 @@ function cmdDoctor() {
|
|
|
1869
1997
|
}
|
|
1870
1998
|
}
|
|
1871
1999
|
|
|
2000
|
+
// ── flujo de branches: contra qué integra este repo, y qué rama es producción ──
|
|
2001
|
+
// Sin esto declarado, `dai pr` adivina la base — y en un repo con ramas de ambiente
|
|
2002
|
+
// adivinar significa proponer un merge a producción sin que nada lo destaque (issue #46).
|
|
2003
|
+
info("flujo de branches (dai pr · dai done):");
|
|
2004
|
+
const flow = branchFlow(process.env);
|
|
2005
|
+
if (flow.dev) ok(`integración: ${flow.dev} (DAI_BRANCH_DEV) — ahí van las PR de feature/ y fix/`);
|
|
2006
|
+
else {
|
|
2007
|
+
const { base: adivinada, source: src } = resolveBase({ env: process.env, originHead: originHeadBranch() });
|
|
2008
|
+
warn(`sin DAI_BRANCH_DEV: las PR van a '${adivinada}', que sale de ${src}, no de tu config.`);
|
|
2009
|
+
process.stdout.write(" Declaralo una vez en el .env.dai: DAI_BRANCH_DEV=<rama-que-integra>\n");
|
|
2010
|
+
}
|
|
2011
|
+
if (flow.prod) ok(`producción: ${flow.prod} (DAI_BRANCH_PROD) — ahí van release/ y hotfix/, con confirmación explícita`);
|
|
2012
|
+
else info("sin DAI_BRANCH_PROD: dai no marca ninguna rama como producción (no adivina cuál es)");
|
|
2013
|
+
|
|
1872
2014
|
// ── version-drift del scaffold vs el CLI (ADR-0010) ──────────────────────────
|
|
1873
2015
|
if (existsSync(join(process.cwd(), ".dai", "VERSION"))) { info("versión del scaffold:"); reportDrift(); }
|
|
1874
2016
|
}
|
|
@@ -1881,8 +2023,21 @@ function cmdVersion() {
|
|
|
1881
2023
|
|
|
1882
2024
|
let [cmd, ...rest] = process.argv.slice(2);
|
|
1883
2025
|
if (cmd === "--version" || cmd === "-v") cmd = "version";
|
|
1884
|
-
if (cmd === "--help" || cmd === "-h") cmd = "help";
|
|
1885
2026
|
const { opts, pos } = parseFlags(rest);
|
|
2027
|
+
|
|
2028
|
+
// ── Convención de ayuda (vale para TODOS los comandos) ────────────────────────
|
|
2029
|
+
// Pedir ayuda nunca ejecuta nada: sale por stdout y termina con 0. Antes el `--help`
|
|
2030
|
+
// caía en `opts` y el comando corría igual — `dai stamp --help` dejaba un comentario en
|
|
2031
|
+
// el tracker y `dai pr --help` publicaba una branch. Los agentes lo pisan seguido, porque
|
|
2032
|
+
// probar `<cmd> --help` antes de usar un comando es exactamente lo que hay que hacer.
|
|
2033
|
+
if (isHelpToken(cmd) || cmd === undefined || wantsHelp({ opts, pos })) {
|
|
2034
|
+
const { text, known } = helpFor(helpTopic(isHelpToken(cmd) ? null : cmd, pos));
|
|
2035
|
+
if (known) { process.stdout.write(text); process.exit(0); }
|
|
2036
|
+
process.stderr.write(`dai: no conozco el comando '${helpTopic(isHelpToken(cmd) ? null : cmd, pos)}'.\n\n`);
|
|
2037
|
+
process.stderr.write(text);
|
|
2038
|
+
process.exit(1);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1886
2041
|
switch (cmd) {
|
|
1887
2042
|
case "ac-hash": cmdAcHash(pos[0]); break;
|
|
1888
2043
|
case "ls": cmdLs(opts); break;
|
|
@@ -1910,53 +2065,10 @@ switch (cmd) {
|
|
|
1910
2065
|
case "doctor": cmdDoctor(); break;
|
|
1911
2066
|
case "version": cmdVersion(); break;
|
|
1912
2067
|
default:
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
" [--parent KEY] la cuelga de su épica · [--issuetype T] p. ej. Epic\n" +
|
|
1920
|
-
" [--field alias=valor] campos propios que exige tu Jira (.dai/jira-fields.json); repetible\n" +
|
|
1921
|
-
" link-us <KEY> [--us <md>] crea branch + implements.yaml; sin --us trae la US del tracker (ADR-0004)\n" +
|
|
1922
|
-
" link-us <KEY> --resync re-estampa el ac_hash contra la US viva (tras un ⚠️ de check)\n" +
|
|
1923
|
-
" edit-us <KEY> trae la US del tracker, la abrís en tu editor, valida el formato,\n" +
|
|
1924
|
-
" muestra qué cambia y la guarda (para el PO)\n" +
|
|
1925
|
-
" [--no-editor] no abre $EDITOR (para skills/scripts que ya escribieron el .md)\n" +
|
|
1926
|
-
" [--bump | --no-bump] decide el spec_version sin preguntar (sin TTY no se toca y avisa)\n" +
|
|
1927
|
-
" update-us <KEY> [--us <md>] empuja al tracker un .md que ya escribiste + re-estampa el ac_hash\n" +
|
|
1928
|
-
" [--dry-run] [--yes] sin --yes muestra el diff y pide confirmación · [--no-resync]\n" +
|
|
1929
|
-
" [--strict] las advertencias de formato también frenan · [--no-bump] no toca spec_version\n" +
|
|
1930
|
-
" check compara vs la US viva → atrasado (ADR-0003)\n" +
|
|
1931
|
-
" check --ci gate de CI: exige el link según branch-naming (chore/ y docs/ exentas)\n" +
|
|
1932
|
-
" [--branch b] la branch a evaluar (en CI se detecta sola) · [--no-network]\n" +
|
|
1933
|
-
" salidas: 0 pasa · 1 falta el link · 2 el QUÉ cambió\n" +
|
|
1934
|
-
" stamp [<ID>…] [--all] estampa la cobertura en el tracker (ADR-0005)\n" +
|
|
1935
|
-
" sin ID: la US de esta branch; si hay varias, pregunta\n" +
|
|
1936
|
-
" done [--base main] [--force] cierra la US: vuelve a la base, actualiza y borra la branch local (si está mergeada)\n" +
|
|
1937
|
-
" archive [<change>] [--skip-specs] funde los delta specs del change en las specs canónicas y lo archiva (lo corre el aprobador en la PR)\n" +
|
|
1938
|
-
" pr (alias mr) [--assignee u] [--base b] [--draft] [--yes] crea TU PR/MR precargada (muestra + confirma)\n" +
|
|
1939
|
-
" [--us <ID>] [--title t] la US la resuelve la branch; si hay varias, pregunta (sin TTY, falla)\n" +
|
|
1940
|
-
" --description <texto> QUÉ resuelve la PR y por qué → sección 'Descripción' (o --description-file <f>)\n" +
|
|
1941
|
-
" --changes <texto> detalle de 'Cambios realizados' (default: los commits) (o --changes-file <f>)\n" +
|
|
1942
|
-
" sin descripción y sin commits, con --yes o sin TTY, dai NO publica: la PR\n" +
|
|
1943
|
-
" saldría con el molde del template y no se podría revisar\n" +
|
|
1944
|
-
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n" +
|
|
1945
|
-
" forge review <ref> --from <review.json> [--dry-run|--yes] review inline: resumen + comentario por línea\n" +
|
|
1946
|
-
" --min-severity low|medium|high · --min-confidence 0..1 · --max-comments N · --base <branch>\n" +
|
|
1947
|
-
" Sin --yes no postea nada: muestra el preview y valida que cada hallazgo apunte al diff.\n\n" +
|
|
1948
|
-
"Instalación:\n" +
|
|
1949
|
-
" skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] instala las skills de dai (alias: `install`)\n" +
|
|
1950
|
-
" skills install --from <git-url|npm:pkg|path>[#ref] [--for <asistentes>] instala skills EXTERNAS (por-stack), convertidas para los 3 asistentes (ADR-0013)\n" +
|
|
1951
|
-
" init [<repo>] scaffolder interactivo del repo (asistente, gestor, OpenSpec)\n" +
|
|
1952
|
-
" --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
|
|
1953
|
-
" ej: --for claude,cursor · --for copilot · --for all\n" +
|
|
1954
|
-
" --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
|
|
1955
|
-
" 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" +
|
|
1956
|
-
" 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" +
|
|
1957
|
-
" docs <destino> documentación conceptual → <destino>\n" +
|
|
1958
|
-
" doctor diagnóstico del entorno\n\n" +
|
|
1959
|
-
" (config: .env.dai — ver .env.dai.example)\n"
|
|
1960
|
-
);
|
|
1961
|
-
process.exit(cmd && cmd !== "help" ? 1 : 0);
|
|
2068
|
+
// Comando desconocido: la ayuda global por stderr y salida ≠ 0 (la ayuda PEDIDA sale
|
|
2069
|
+
// por stdout y con 0, arriba). Distinguirlos es lo que deja `dai foo --help` usable
|
|
2070
|
+
// en un script sin tener que adivinar de dónde leer.
|
|
2071
|
+
process.stderr.write(`dai: no conozco el comando '${cmd}'.\n\n`);
|
|
2072
|
+
process.stderr.write(globalUsage());
|
|
2073
|
+
process.exit(1);
|
|
1962
2074
|
}
|
package/cli/lib/bootstrap.mjs
CHANGED
|
@@ -166,8 +166,19 @@ export function skillToCursor(md) {
|
|
|
166
166
|
// tapaba la de ClickUp con team_id. Queda como override manual para trackers raros.
|
|
167
167
|
export function envFor(pm) {
|
|
168
168
|
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";
|
|
169
|
+
// Flujo de branches: sin esto, `dai pr` tiene que adivinar la base, y en un repo con
|
|
170
|
+
// ramas de ambiente adivinar significa proponer un merge a producción (issue #46).
|
|
171
|
+
// Van vacías a propósito: vacío = "no declarada", y dai cae a la default del remoto.
|
|
172
|
+
const flujo =
|
|
173
|
+
"\n# ── Flujo de branches (dai pr · dai done) ─────────────────────────────────\n" +
|
|
174
|
+
"# Las DOS ramas de vida larga del repo. La base de una PR sale del TIPO de branch:\n" +
|
|
175
|
+
"# feature/ · fix/ → PR contra DAI_BRANCH_DEV\n" +
|
|
176
|
+
"# release/ · hotfix/ → PR contra DAI_BRANCH_PROD, con confirmación explícita\n" +
|
|
177
|
+
"# Vacías = no declaradas: dai cae a la rama default del remoto y avisa que adivina.\n" +
|
|
178
|
+
"DAI_BRANCH_DEV=\n" +
|
|
179
|
+
"DAI_BRANCH_PROD=\n";
|
|
169
180
|
if (pm === "clickup") {
|
|
170
|
-
return head + "DAI_PM=clickup\nDAI_CLICKUP_TOKEN=\nDAI_CLICKUP_LIST_ID=\n";
|
|
181
|
+
return head + "DAI_PM=clickup\nDAI_CLICKUP_TOKEN=\nDAI_CLICKUP_LIST_ID=\n" + flujo;
|
|
171
182
|
}
|
|
172
183
|
if (pm === "jira") {
|
|
173
184
|
return head +
|
|
@@ -181,9 +192,9 @@ export function envFor(pm) {
|
|
|
181
192
|
"# Solo si tu Jira exige campos propios AL CREAR una US (lo usa grill-user-story,\n" +
|
|
182
193
|
"# no hace falta para leerlas). El default ya es .dai/jira-fields.json; descomentá\n" +
|
|
183
194
|
"# solo para apuntar a otra ruta. Si el archivo no existe, se ignora.\n" +
|
|
184
|
-
"# DAI_JIRA_FIELDS_FILE=.dai/jira-fields.json\n";
|
|
195
|
+
"# DAI_JIRA_FIELDS_FILE=.dai/jira-fields.json\n" + flujo;
|
|
185
196
|
}
|
|
186
|
-
return head + "DAI_PM=md\nDAI_MD_US_DIR=.dai/us\n";
|
|
197
|
+
return head + "DAI_PM=md\nDAI_MD_US_DIR=.dai/us\n" + flujo;
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
// ── Helpers aditivos para `dai init` — no destruir la config de un repo vivo ──
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// dai · el mapa de ramas de vida larga del repo, y de ahí la base de cada PR (issue #46).
|
|
2
|
+
//
|
|
3
|
+
// El default `main` hardcodeado miente en cualquier repo cuyo flujo no sea el de GitHub:
|
|
4
|
+
// con ramas de ambiente (`testing` integra, `main` DESPLIEGA A PRODUCCIÓN), `dai pr`
|
|
5
|
+
// proponía mergear a producción y el preview no lo destacaba de ninguna forma. Había que
|
|
6
|
+
// acordarse de `--base testing` en cada invocación; el día que alguien se olvida, la MR
|
|
7
|
+
// queda apuntando a PRO y nada avisa.
|
|
8
|
+
//
|
|
9
|
+
// La primera versión pedía configurar "la base", y ese era el error de modelado: la base
|
|
10
|
+
// NO es una constante, es una consecuencia del TIPO de branch —
|
|
11
|
+
//
|
|
12
|
+
// feature/ · fix/ · lo que sea → rama de integración (DAI_BRANCH_DEV)
|
|
13
|
+
// release/ · hotfix/ → rama de producción (DAI_BRANCH_PROD)
|
|
14
|
+
//
|
|
15
|
+
// Por eso no se configura una base: se declaran las DOS ramas de vida larga del repo, que
|
|
16
|
+
// son dos hechos que dai no puede deducir de ningún lado, y la base sale del mapa.
|
|
17
|
+
// Lo que dai NO hace es adivinar: sin `DAI_BRANCH_PROD` declarada no marca nada como
|
|
18
|
+
// producción — inventar un gate sobre una suposición es peor que no tenerlo.
|
|
19
|
+
|
|
20
|
+
import { branchType } from "./branch-scope.mjs";
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_BASE = "main";
|
|
23
|
+
|
|
24
|
+
// Ramas de vida larga declaradas por el repo. `null` = no declarada (≠ vacía).
|
|
25
|
+
export function branchFlow(env = {}) {
|
|
26
|
+
const val = (k) => String(env[k] ?? "").trim() || null;
|
|
27
|
+
return { dev: val("DAI_BRANCH_DEV"), prod: val("DAI_BRANCH_PROD") };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Los tipos de branch que van contra producción: una release que se corta y un hotfix
|
|
31
|
+
// que sale del tag que está en PRO. El resto integra.
|
|
32
|
+
const HACIA_PROD = new Set(["release", "hotfix"]);
|
|
33
|
+
|
|
34
|
+
// Resuelve la base de una PR y —tan importante como el valor— POR QUÉ es esa.
|
|
35
|
+
// --base > el mapa de ramas según el tipo de branch > rama default del remoto > main
|
|
36
|
+
export function resolveBase({ flag, branch = null, env = {}, originHead = null } = {}) {
|
|
37
|
+
const explicit = typeof flag === "string" ? flag.trim() : "";
|
|
38
|
+
if (explicit) return { base: explicit, source: "--base", reason: null };
|
|
39
|
+
|
|
40
|
+
const flow = branchFlow(env);
|
|
41
|
+
const tipo = branchType(branch);
|
|
42
|
+
if (HACIA_PROD.has(tipo) && flow.prod) {
|
|
43
|
+
return { base: flow.prod, source: "DAI_BRANCH_PROD (.env.dai)", reason: `la branch es ${tipo}/` };
|
|
44
|
+
}
|
|
45
|
+
if (flow.dev) {
|
|
46
|
+
return { base: flow.dev, source: "DAI_BRANCH_DEV (.env.dai)", reason: null };
|
|
47
|
+
}
|
|
48
|
+
// Un repo puede declarar solo la de producción (flujo de una sola rama). Ahí la base es
|
|
49
|
+
// esa, y el gate de producción se dispara — que es exactamente lo que quiso quien la declaró.
|
|
50
|
+
if (flow.prod) return { base: flow.prod, source: "DAI_BRANCH_PROD (.env.dai)", reason: "es la única rama declarada" };
|
|
51
|
+
|
|
52
|
+
const head = String(originHead ?? "").trim();
|
|
53
|
+
if (head) return { base: head, source: "rama default de origin", reason: null };
|
|
54
|
+
return { base: DEFAULT_BASE, source: "default de dai", reason: null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ¿Esta base es la rama que despliega a producción? Solo si el repo lo declaró.
|
|
58
|
+
export function isProdBranch(base, env = {}) {
|
|
59
|
+
const b = String(base ?? "").trim();
|
|
60
|
+
return b !== "" && b === branchFlow(env).prod;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Las fuentes que YA son una decisión de alguien: no hay nada que avisar.
|
|
64
|
+
const DECIDIDAS = new Set(["--base", "DAI_BRANCH_DEV (.env.dai)", "DAI_BRANCH_PROD (.env.dai)", "lo respondiste vos"]);
|
|
65
|
+
|
|
66
|
+
// El aviso que va debajo del preview cuando la base salió de un default. Desaparece en
|
|
67
|
+
// cuanto el repo declara su mapa de ramas, que es justo lo que se le pide.
|
|
68
|
+
export function baseHint(source, base) {
|
|
69
|
+
if (DECIDIDAS.has(source)) return null;
|
|
70
|
+
return `la base '${base}' salió de ${source} — dai no sabe cuáles son las ramas de vida larga de este repo. Declaralas una vez en el .env.dai:\n` +
|
|
71
|
+
` DAI_BRANCH_DEV=<rama-que-integra> · DAI_BRANCH_PROD=<rama-que-despliega-a-PRO>\n` +
|
|
72
|
+
` Con eso: feature/ y fix/ van contra DEV; release/ y hotfix/ contra PROD (con confirmación).`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Normaliza lo que devuelve `git symbolic-ref refs/remotes/origin/HEAD` → nombre de rama.
|
|
76
|
+
// "origin/main" → "main" · "refs/remotes/origin/main" → "main" · basura → null
|
|
77
|
+
export function parseOriginHead(out) {
|
|
78
|
+
const s = String(out ?? "").trim();
|
|
79
|
+
if (!s) return null;
|
|
80
|
+
const m = s.match(/(?:^|\/)origin\/(.+)$/);
|
|
81
|
+
return m ? m[1].trim() || null : null;
|
|
82
|
+
}
|
package/cli/lib/branch-scope.mjs
CHANGED
|
@@ -194,7 +194,16 @@ export function prScope({ branch, rows, allRows = rows, ids = [] }) {
|
|
|
194
194
|
if (req.kind === "exempt") {
|
|
195
195
|
return { mode: "exempt", target: null, candidates: rows, reason: `${req.reason} y su nombre no nombra ninguna US` };
|
|
196
196
|
}
|
|
197
|
-
|
|
197
|
+
// El repo no tiene NINGUNA US viva. Exigirle un link a una branch que el propio
|
|
198
|
+
// branch-naming declara exenta es pedir algo que no existe: `fix/lo-que-sea` (sin ID en
|
|
199
|
+
// el nombre) terminaba con "corré dai link-us primero" y el consejo de renombrarla a
|
|
200
|
+
// `chore/`, que para un fix es directamente el consejo equivocado. Le pasa a cualquier
|
|
201
|
+
// repo de tooling — al de dai, sin ir más lejos, que no se trackea a sí mismo con US.
|
|
202
|
+
if (rows.length === 0) {
|
|
203
|
+
return req.required
|
|
204
|
+
? { mode: "none", target: null, candidates: [], reason: "no hay implements.yaml vivo en el repo" }
|
|
205
|
+
: { mode: "exempt", target: null, candidates: [], reason: `${req.reason}, y el repo no declara ninguna US` };
|
|
206
|
+
}
|
|
198
207
|
if (rows.length === 1) return { mode: "only", target: rows[0], candidates: rows, reason: "es la única US viva del repo" };
|
|
199
208
|
return { mode: "ambiguous", target: null, candidates: rows, reason: `hay ${rows.length} US vivas y la branch '${branch}' no dice cuál` };
|
|
200
209
|
}
|
package/cli/lib/help.mjs
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// dai · la ayuda del CLI, y la CONVENCIÓN de cómo se pide.
|
|
2
|
+
//
|
|
3
|
+
// El bug que originó este archivo: `dai <comando> --help` no imprimía ayuda — el flag caía
|
|
4
|
+
// en `opts` y el comando SE EJECUTABA igual. Con comandos que hablan hacia afuera eso no es
|
|
5
|
+
// una molestia: `dai stamp --help` dejaba un comentario en el tracker, `dai pr --help`
|
|
6
|
+
// publicaba una branch y abría una PR. Los agentes lo pisan seguido, porque probar
|
|
7
|
+
// `<cmd> --help` antes de usar un comando es exactamente lo que hay que hacer.
|
|
8
|
+
//
|
|
9
|
+
// La convención, para que valga en TODOS los comandos y no haya que recordar cuál la
|
|
10
|
+
// implementa: pedir ayuda NUNCA ejecuta nada, siempre sale por stdout y siempre con 0.
|
|
11
|
+
// dai help · dai --help · dai -h
|
|
12
|
+
// dai help <comando> · dai <comando> --help · dai <comando> -h · dai <comando> help
|
|
13
|
+
|
|
14
|
+
// Tokens que significan "quiero la ayuda", en cualquier posición razonable.
|
|
15
|
+
// `-h` entra como POSICIONAL (el parser solo entiende `--`), por eso está en la lista.
|
|
16
|
+
export const HELP_TOKENS = new Set(["help", "-h", "-help", "--help", "ayuda", "?"]);
|
|
17
|
+
|
|
18
|
+
export const isHelpToken = (t) => HELP_TOKENS.has(String(t ?? "").toLowerCase());
|
|
19
|
+
|
|
20
|
+
// ¿Esta invocación pide ayuda en vez de ejecutar?
|
|
21
|
+
export function wantsHelp({ opts = {}, pos = [] } = {}) {
|
|
22
|
+
if ("help" in opts || "h" in opts) return true;
|
|
23
|
+
return pos.some(isHelpToken);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Alias → comando canónico (para que `dai mr --help` no quede sin ayuda).
|
|
27
|
+
export const HELP_ALIAS = {
|
|
28
|
+
mr: "pr", update: "upgrade", install: "skills", "skills-install": "skills",
|
|
29
|
+
"link": "link-us", "us": "link-us",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// El tema del que se pide ayuda: el comando, o el primer positivo que no sea un token
|
|
33
|
+
// de ayuda (`dai help pr`). Devuelve null para la ayuda global.
|
|
34
|
+
export function helpTopic(cmd, pos = []) {
|
|
35
|
+
const c = String(cmd ?? "").toLowerCase();
|
|
36
|
+
if (c && !isHelpToken(c)) return HELP_ALIAS[c] || c;
|
|
37
|
+
const t = pos.map((p) => String(p).toLowerCase()).find((p) => !isHelpToken(p));
|
|
38
|
+
return t ? (HELP_ALIAS[t] || t) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Ayuda por comando ────────────────────────────────────────────────────────
|
|
42
|
+
// Cada entrada es autocontenida: qué hace, cómo se invoca, sus flags y un ejemplo real.
|
|
43
|
+
export const COMMAND_HELP = {
|
|
44
|
+
"ac-hash": `dai ac-hash — calcula el ac_hash de una US (ADR-0001)
|
|
45
|
+
|
|
46
|
+
Uso:
|
|
47
|
+
dai ac-hash <us.md>
|
|
48
|
+
|
|
49
|
+
Qué hace:
|
|
50
|
+
Extrae la sección "Criterios de aceptación", la normaliza y devuelve su SHA-256 truncado
|
|
51
|
+
a 8 hex. Es el mismo número que estampa \`dai link-us\` y compara \`dai check\`: si el QUÉ
|
|
52
|
+
cambia, cambia el hash, y los CÓMO que lo implementaban quedan marcados como atrasados.
|
|
53
|
+
|
|
54
|
+
Ejemplo:
|
|
55
|
+
dai ac-hash .dai/us/ABC-482.md
|
|
56
|
+
`,
|
|
57
|
+
|
|
58
|
+
ls: `dai ls — lista lo que este repo implementa (ADR-0005)
|
|
59
|
+
|
|
60
|
+
Uso:
|
|
61
|
+
dai ls [--json] [--root <dir>]
|
|
62
|
+
|
|
63
|
+
Qué hace:
|
|
64
|
+
Recorre los implements.yaml del repo y muestra cada US linkeada con su versión, su
|
|
65
|
+
ac_hash y el change de OpenSpec al que pertenece. Con --json sale la misma info
|
|
66
|
+
estructurada, para scripts y CI.
|
|
67
|
+
|
|
68
|
+
Ejemplo:
|
|
69
|
+
dai ls --json | jq '.[].id'
|
|
70
|
+
`,
|
|
71
|
+
|
|
72
|
+
"link-us": `dai link-us — crea la branch + el implements.yaml (el link QUÉ↔CÓMO, ADR-0004)
|
|
73
|
+
|
|
74
|
+
Uso:
|
|
75
|
+
dai link-us <KEY> [--us <us.md>] [--title <t>] [--change <c>] [--repo <r>] [--base <rama>]
|
|
76
|
+
dai link-us <KEY> --resync
|
|
77
|
+
dai link-us <KEY> --dry-run
|
|
78
|
+
|
|
79
|
+
Qué hace:
|
|
80
|
+
Trae la US del tracker (o la lee del .md que le pases), calcula el ac_hash, crea la
|
|
81
|
+
branch con el nombre canónico (governance/branch-naming.md) y escribe el implements.yaml.
|
|
82
|
+
El KEY nunca se tipea a mano en la branch: sale del argumento.
|
|
83
|
+
|
|
84
|
+
Opciones:
|
|
85
|
+
--us <us.md> fuente local en vez del tracker (útil sin red/token)
|
|
86
|
+
--resync re-estampa el ac_hash contra la US viva, sin crear branch (tras un ⚠ de check)
|
|
87
|
+
--title <t> pisa el título (y por lo tanto el slug de la branch)
|
|
88
|
+
--change <c> nombre del change de OpenSpec (default: el slug del título)
|
|
89
|
+
--base <rama> de qué rama sale la branch nueva (default: donde estás parado)
|
|
90
|
+
--dry-run muestra branch + yaml y no toca nada
|
|
91
|
+
|
|
92
|
+
Notas:
|
|
93
|
+
Si la US no declara \`spec_version\`, el link queda con \`version: pendiente\` y dai avisa:
|
|
94
|
+
un \`v1\` inventado se publica en la PR y se estampa en el tracker como si fuera un dato.
|
|
95
|
+
|
|
96
|
+
Ejemplo:
|
|
97
|
+
dai link-us ABC-482
|
|
98
|
+
dai link-us ABC-482 --resync
|
|
99
|
+
`,
|
|
100
|
+
|
|
101
|
+
check: `dai check — ¿tu implementación sigue cubriendo el QUÉ? (ADR-0003)
|
|
102
|
+
|
|
103
|
+
Uso:
|
|
104
|
+
dai check
|
|
105
|
+
dai check --ci [--branch <rama>] [--no-network]
|
|
106
|
+
|
|
107
|
+
Qué hace:
|
|
108
|
+
Compara el ac_hash estampado en cada implements.yaml contra la US viva del tracker.
|
|
109
|
+
Al día = el QUÉ no se movió. Atrasado = alguien cambió los criterios y tu CÓMO todavía
|
|
110
|
+
no los cubre.
|
|
111
|
+
|
|
112
|
+
Modo --ci (gate de governance/ci-rules.md):
|
|
113
|
+
Exige el link según el nombre de la branch — chore/, docs/, ci/, release/ y hotfix/
|
|
114
|
+
están exentas. Salidas: 0 pasa · 1 falta el link · 2 el QUÉ cambió.
|
|
115
|
+
--branch <rama> la branch a evaluar (en CI se detecta sola)
|
|
116
|
+
--no-network valida solo que el link exista (sin consultar al tracker)
|
|
117
|
+
|
|
118
|
+
Ejemplo:
|
|
119
|
+
dai check
|
|
120
|
+
dai check --ci --branch feature/ABC-482-checkout
|
|
121
|
+
`,
|
|
122
|
+
|
|
123
|
+
stamp: `dai stamp — estampa la cobertura en el tracker (ADR-0005, ADR-0018)
|
|
124
|
+
|
|
125
|
+
Uso:
|
|
126
|
+
dai stamp [<ID>…] [--all] [--dry-run]
|
|
127
|
+
|
|
128
|
+
Qué hace:
|
|
129
|
+
Deja en la US un comentario con qué repo/change/branch/commit la implementa y en qué
|
|
130
|
+
estado quedó. Sin ID estampa la US de ESTA branch; si hay varias candidatas pregunta,
|
|
131
|
+
porque un comentario en el tracker no se deshace.
|
|
132
|
+
|
|
133
|
+
Opciones:
|
|
134
|
+
--all estampa todas las US del repo (úsalo a sabiendas)
|
|
135
|
+
--dry-run muestra qué estamparía y no escribe nada
|
|
136
|
+
|
|
137
|
+
Ejemplo:
|
|
138
|
+
dai stamp --dry-run
|
|
139
|
+
`,
|
|
140
|
+
|
|
141
|
+
"update-us": `dai update-us — empuja al tracker un .md que ya escribiste
|
|
142
|
+
|
|
143
|
+
Uso:
|
|
144
|
+
dai update-us <ID> [--us <us.md>] [--dry-run] [--yes] [--strict] [--no-resync] [--no-bump]
|
|
145
|
+
|
|
146
|
+
Qué hace:
|
|
147
|
+
Valida el formato de la US, muestra el diff contra lo que hay en el tracker, propone
|
|
148
|
+
subir el spec_version si cambiaron los criterios, publica y re-estampa el ac_hash local.
|
|
149
|
+
|
|
150
|
+
Opciones:
|
|
151
|
+
--yes no pregunta (sin --yes muestra el diff y pide confirmación)
|
|
152
|
+
--strict las advertencias de formato también frenan
|
|
153
|
+
--no-resync no re-estampa el ac_hash en el implements.yaml
|
|
154
|
+
--no-bump no toca el spec_version
|
|
155
|
+
|
|
156
|
+
Ejemplo:
|
|
157
|
+
dai update-us ABC-482 --us borrador.md --dry-run
|
|
158
|
+
`,
|
|
159
|
+
|
|
160
|
+
"edit-us": `dai edit-us — editar el QUÉ con red de seguridad (para el PO)
|
|
161
|
+
|
|
162
|
+
Uso:
|
|
163
|
+
dai edit-us <ID> [--no-editor] [--bump | --no-bump] [--yes]
|
|
164
|
+
|
|
165
|
+
Qué hace:
|
|
166
|
+
Trae la US del tracker, la abre en tu $EDITOR, valida el formato al guardar, te muestra
|
|
167
|
+
qué cambia y recién entonces la escribe. Pregunta si el cambio es material para subir
|
|
168
|
+
el spec_version — esa decisión es de la persona, no de dai.
|
|
169
|
+
|
|
170
|
+
Opciones:
|
|
171
|
+
--no-editor no abre $EDITOR (para skills/scripts que ya escribieron el .md)
|
|
172
|
+
--bump / --no-bump decide el spec_version sin preguntar (sin TTY no se toca y avisa)
|
|
173
|
+
|
|
174
|
+
Ejemplo:
|
|
175
|
+
dai edit-us ABC-482
|
|
176
|
+
`,
|
|
177
|
+
|
|
178
|
+
publish: `dai publish — crea la US en el tracker y devuelve su key
|
|
179
|
+
|
|
180
|
+
Uso:
|
|
181
|
+
dai publish <us.md> [--parent <KEY>] [--issuetype <T>] [--field alias=valor]…
|
|
182
|
+
|
|
183
|
+
Qué hace:
|
|
184
|
+
Valida el formato y crea el issue/tarea en el backend configurado (DAI_PM). Devuelve el
|
|
185
|
+
key para que \`dai link-us\` lo use.
|
|
186
|
+
|
|
187
|
+
Opciones:
|
|
188
|
+
--parent <KEY> la cuelga de su épica
|
|
189
|
+
--issuetype <T> tipo de issue (p. ej. Epic; default DAI_JIRA_ISSUETYPE o Story)
|
|
190
|
+
--field alias=valor campos propios que exige tu Jira (.dai/jira-fields.json); repetible
|
|
191
|
+
|
|
192
|
+
Ejemplo:
|
|
193
|
+
dai publish us.md --parent ABC-100 --field clasificacion=Evolutiva
|
|
194
|
+
`,
|
|
195
|
+
|
|
196
|
+
pr: `dai pr — crea (o ACTUALIZA) TU Pull/Merge Request precargada
|
|
197
|
+
|
|
198
|
+
Uso:
|
|
199
|
+
dai pr [--base <rama>] [--us <ID>] [--title <t>] [--assignee <u>] [--draft] [--yes]
|
|
200
|
+
dai pr --description <texto> | --description-file <archivo>
|
|
201
|
+
dai pr --changes <texto> | --changes-file <archivo>
|
|
202
|
+
dai mr … alias para GitLab (merge request)
|
|
203
|
+
|
|
204
|
+
Qué hace:
|
|
205
|
+
Resuelve la US de esta branch, corre la verificación de trazabilidad, arma el body desde
|
|
206
|
+
el template del repo, TE MUESTRA el preview y pide confirmación. Si la branch ya tiene
|
|
207
|
+
una PR/MR abierta, la ACTUALIZA (título + descripción) en vez de fallar a mitad de camino.
|
|
208
|
+
|
|
209
|
+
La branch base sale del TIPO de branch, no de un default fijo:
|
|
210
|
+
feature/ · fix/ · el resto → DAI_BRANCH_DEV (la rama que integra)
|
|
211
|
+
release/ · hotfix/ → DAI_BRANCH_PROD (la que despliega a producción)
|
|
212
|
+
--base gana siempre; sin nada declarado cae a la rama default de origin, avisando.
|
|
213
|
+
El preview dice de dónde salió la base. Contra DAI_BRANCH_PROD pide confirmación
|
|
214
|
+
explícita — hay que escribir el nombre de la rama, y con --yes agregar --to-prod.
|
|
215
|
+
|
|
216
|
+
Opciones:
|
|
217
|
+
--base <rama> contra qué rama va la PR
|
|
218
|
+
--to-prod confirma que la base es producción (obligatorio junto a --yes)
|
|
219
|
+
--us <ID> con qué US titularla, si la branch toca varias
|
|
220
|
+
--title <t> pisa el título
|
|
221
|
+
--description[-file] QUÉ resuelve la PR y por qué → sección "Descripción"
|
|
222
|
+
--changes[-file] detalle de "Cambios realizados" (default: los commits)
|
|
223
|
+
--assignee <u> asigna la PR · --draft: la crea en borrador
|
|
224
|
+
--yes no pregunta (igual frena si el body saldría con el molde vacío)
|
|
225
|
+
|
|
226
|
+
Ejemplo:
|
|
227
|
+
dai pr --base develop --description-file notas.md
|
|
228
|
+
`,
|
|
229
|
+
|
|
230
|
+
done: `dai done — cierra la US: vuelve a la base, actualiza y borra la branch local
|
|
231
|
+
|
|
232
|
+
Uso:
|
|
233
|
+
dai done [--base <rama>] [--force]
|
|
234
|
+
|
|
235
|
+
Qué hace:
|
|
236
|
+
Verifica que no queden cambios sueltos ni commits sin pushear, vuelve a la base, hace
|
|
237
|
+
fetch + pull --ff-only y borra la branch local SOLO si ya está mergeada.
|
|
238
|
+
La base se resuelve igual que en \`dai pr\`: del tipo de branch (DAI_BRANCH_DEV /
|
|
239
|
+
DAI_BRANCH_PROD), o de la rama default de origin si el repo no las declaró.
|
|
240
|
+
|
|
241
|
+
Opciones:
|
|
242
|
+
--force borra la branch aunque no esté mergeada
|
|
243
|
+
|
|
244
|
+
Ejemplo:
|
|
245
|
+
dai done --base develop
|
|
246
|
+
`,
|
|
247
|
+
|
|
248
|
+
archive: `dai archive — funde los delta specs y archiva el change (ADR-0011)
|
|
249
|
+
|
|
250
|
+
Uso:
|
|
251
|
+
dai archive [<change>] [--skip-specs]
|
|
252
|
+
|
|
253
|
+
Qué hace:
|
|
254
|
+
Mueve el change de OpenSpec a archive/ y funde sus delta specs en las specs canónicas.
|
|
255
|
+
Lo corre QUIEN APRUEBA la PR, no quien la abre: es el gate de aprobación.
|
|
256
|
+
|
|
257
|
+
Ejemplo:
|
|
258
|
+
dai archive checkout-sin-duplicado
|
|
259
|
+
`,
|
|
260
|
+
|
|
261
|
+
forge: `dai forge — hablarle a una PR/MR AJENA (github/gitlab)
|
|
262
|
+
|
|
263
|
+
Uso:
|
|
264
|
+
dai forge pr <ref> lee la PR/MR
|
|
265
|
+
dai forge comment <ref> --body-file <f> comenta en el hilo
|
|
266
|
+
dai forge review <ref> --from <review.json> [--dry-run | --yes]
|
|
267
|
+
|
|
268
|
+
Qué hace:
|
|
269
|
+
\`review\` postea un review INLINE: un comentario de resumen más uno anclado a cada
|
|
270
|
+
archivo:línea. Sin --yes no postea nada: muestra el preview y valida que cada hallazgo
|
|
271
|
+
apunte de verdad al diff (descarta las líneas que el modelo inventó).
|
|
272
|
+
|
|
273
|
+
Opciones de review:
|
|
274
|
+
--min-severity low|medium|high · --min-confidence 0..1 · --max-comments N · --base <rama>
|
|
275
|
+
|
|
276
|
+
Auth:
|
|
277
|
+
GITHUB_TOKEN / GITLAB_TOKEN en el .env.dai (token scopeado; git sigue usando SSH).
|
|
278
|
+
|
|
279
|
+
Ejemplo:
|
|
280
|
+
dai forge review 42 --from review.json --dry-run
|
|
281
|
+
`,
|
|
282
|
+
|
|
283
|
+
skills: `dai skills install — instala las skills de dai (alias: dai install)
|
|
284
|
+
|
|
285
|
+
Uso:
|
|
286
|
+
dai skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>]
|
|
287
|
+
dai skills install --from <git-url|npm:pkg|path>[#ref] [--for <asistentes>]
|
|
288
|
+
|
|
289
|
+
Qué hace:
|
|
290
|
+
Copia las skills al asistente (Claude, Copilot, Cursor). Con --from instala skills
|
|
291
|
+
EXTERNAS por-stack, convertidas para los tres asistentes (ADR-0013).
|
|
292
|
+
|
|
293
|
+
Opciones:
|
|
294
|
+
--for <asistentes> claude|copilot|cursor (combinables con coma) · both|all (default all)
|
|
295
|
+
--global al home del asistente · --local <repo>: dentro de un repo
|
|
296
|
+
--force pisa lo que haya · --dry-run: muestra y no escribe
|
|
297
|
+
|
|
298
|
+
Ejemplo:
|
|
299
|
+
dai skills install --global --for claude,cursor
|
|
300
|
+
`,
|
|
301
|
+
|
|
302
|
+
init: `dai init — scaffolder del repo (asistente, gestor, OpenSpec)
|
|
303
|
+
|
|
304
|
+
Uso:
|
|
305
|
+
dai init [<repo>] [--for <asistentes>] [--pm md|jira|clickup] [--openspec]
|
|
306
|
+
|
|
307
|
+
Qué hace:
|
|
308
|
+
Interroga y deja el repo listo: skills del asistente, constitución, templates,
|
|
309
|
+
.env.dai.example, gitignore y el gate de CI. Con flags te saltea las preguntas.
|
|
310
|
+
|
|
311
|
+
Opciones:
|
|
312
|
+
--for <asistentes> claude|copilot|cursor (combinables con coma) · both|all (default all)
|
|
313
|
+
--pm <backend> backend del tracker · --openspec: además scaffoldea OpenSpec
|
|
314
|
+
|
|
315
|
+
Ejemplo:
|
|
316
|
+
dai init --for claude,copilot --pm jira --openspec
|
|
317
|
+
`,
|
|
318
|
+
|
|
319
|
+
sync: `dai sync — refresca el scaffolding a la versión del CLI (ADR-0010)
|
|
320
|
+
|
|
321
|
+
Uso:
|
|
322
|
+
dai sync [<repo>] [--dry-run] [--for <asistentes>]
|
|
323
|
+
|
|
324
|
+
Qué hace:
|
|
325
|
+
Actualiza skills, constitución y templates a los de esta versión de dai. Es ADITIVO:
|
|
326
|
+
no toca el .env.dai ni tus archivos de OpenSpec.
|
|
327
|
+
|
|
328
|
+
Ejemplo:
|
|
329
|
+
dai sync --dry-run
|
|
330
|
+
`,
|
|
331
|
+
|
|
332
|
+
upgrade: `dai upgrade — actualiza el CLI global (alias: dai update, ADR-0012)
|
|
333
|
+
|
|
334
|
+
Uso:
|
|
335
|
+
dai upgrade [--check] [--dry-run]
|
|
336
|
+
|
|
337
|
+
Qué hace:
|
|
338
|
+
Instala la última versión publicada (npm i -g …@latest) y avisa si el scaffolding del
|
|
339
|
+
repo quedó atrasado respecto del CLI.
|
|
340
|
+
|
|
341
|
+
Opciones:
|
|
342
|
+
--check solo dice si hay una versión nueva
|
|
343
|
+
--dry-run muestra el comando y no lo corre
|
|
344
|
+
|
|
345
|
+
Ejemplo:
|
|
346
|
+
dai upgrade --check
|
|
347
|
+
`,
|
|
348
|
+
|
|
349
|
+
docs: `dai docs — copia la documentación conceptual a un destino
|
|
350
|
+
|
|
351
|
+
Uso:
|
|
352
|
+
dai docs <destino>
|
|
353
|
+
|
|
354
|
+
Ejemplo:
|
|
355
|
+
dai docs ./docs/metodologia
|
|
356
|
+
`,
|
|
357
|
+
|
|
358
|
+
doctor: `dai doctor — diagnóstico del entorno
|
|
359
|
+
|
|
360
|
+
Uso:
|
|
361
|
+
dai doctor
|
|
362
|
+
|
|
363
|
+
Qué hace:
|
|
364
|
+
Revisa skills instaladas por asistente, constitución, comandos de OpenSpec, el backend
|
|
365
|
+
de PM configurado, el remoto/forge y el cliente ssh, y avisa si el scaffolding del repo
|
|
366
|
+
quedó atrasado respecto del CLI.
|
|
367
|
+
`,
|
|
368
|
+
|
|
369
|
+
version: `dai version — versión del CLI (alias: --version, -v)
|
|
370
|
+
|
|
371
|
+
Uso:
|
|
372
|
+
dai version
|
|
373
|
+
|
|
374
|
+
Además avisa si el scaffolding de este repo quedó atrasado respecto del CLI (ADR-0010).
|
|
375
|
+
`,
|
|
376
|
+
|
|
377
|
+
help: `dai help — ayuda del CLI
|
|
378
|
+
|
|
379
|
+
Uso:
|
|
380
|
+
dai help todos los comandos
|
|
381
|
+
dai help <comando> el detalle de uno
|
|
382
|
+
dai <comando> --help lo mismo (también -h, o \`dai <comando> help\`)
|
|
383
|
+
|
|
384
|
+
Pedir ayuda NUNCA ejecuta el comando: sale por stdout y termina con 0.
|
|
385
|
+
`,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// ── Ayuda global ─────────────────────────────────────────────────────────────
|
|
389
|
+
export function globalUsage() {
|
|
390
|
+
return (
|
|
391
|
+
"Uso: dai <comando> [args]\n\n" +
|
|
392
|
+
"Trazabilidad:\n" +
|
|
393
|
+
" ac-hash <us.md> calcula el ac_hash (ADR-0001)\n" +
|
|
394
|
+
" ls [--json] lista lo que implementa el repo (ADR-0005)\n" +
|
|
395
|
+
" publish <us.md> crea la US en el tracker (Jira/ClickUp/md) y devuelve el key\n" +
|
|
396
|
+
" [--parent KEY] la cuelga de su épica · [--issuetype T] p. ej. Epic\n" +
|
|
397
|
+
" [--field alias=valor] campos propios que exige tu Jira (.dai/jira-fields.json); repetible\n" +
|
|
398
|
+
" link-us <KEY> [--us <md>] crea branch + implements.yaml; sin --us trae la US del tracker (ADR-0004)\n" +
|
|
399
|
+
" link-us <KEY> --resync re-estampa el ac_hash contra la US viva (tras un ⚠️ de check)\n" +
|
|
400
|
+
" edit-us <KEY> trae la US del tracker, la abrís en tu editor, valida el formato,\n" +
|
|
401
|
+
" muestra qué cambia y la guarda (para el PO)\n" +
|
|
402
|
+
" [--no-editor] no abre $EDITOR (para skills/scripts que ya escribieron el .md)\n" +
|
|
403
|
+
" [--bump | --no-bump] decide el spec_version sin preguntar (sin TTY no se toca y avisa)\n" +
|
|
404
|
+
" update-us <KEY> [--us <md>] empuja al tracker un .md que ya escribiste + re-estampa el ac_hash\n" +
|
|
405
|
+
" [--dry-run] [--yes] sin --yes muestra el diff y pide confirmación · [--no-resync]\n" +
|
|
406
|
+
" [--strict] las advertencias de formato también frenan · [--no-bump] no toca spec_version\n" +
|
|
407
|
+
" check compara vs la US viva → atrasado (ADR-0003)\n" +
|
|
408
|
+
" check --ci gate de CI: exige el link según branch-naming (chore/ y docs/ exentas)\n" +
|
|
409
|
+
" [--branch b] la branch a evaluar (en CI se detecta sola) · [--no-network]\n" +
|
|
410
|
+
" salidas: 0 pasa · 1 falta el link · 2 el QUÉ cambió\n" +
|
|
411
|
+
" stamp [<ID>…] [--all] estampa la cobertura en el tracker (ADR-0005)\n" +
|
|
412
|
+
" sin ID: la US de esta branch; si hay varias, pregunta\n" +
|
|
413
|
+
" done [--base b] [--force] cierra la US: vuelve a la base, actualiza y borra la branch local\n" +
|
|
414
|
+
" archive [<change>] [--skip-specs] funde los delta specs del change en las specs canónicas y lo archiva (lo corre el aprobador en la PR)\n" +
|
|
415
|
+
" pr (alias mr) [--assignee u] [--base b] [--draft] [--yes] crea o ACTUALIZA TU PR/MR precargada (muestra + confirma)\n" +
|
|
416
|
+
" [--us <ID>] [--title t] la US la resuelve la branch; si hay varias, pregunta (sin TTY, falla)\n" +
|
|
417
|
+
" --description <texto> QUÉ resuelve la PR y por qué → sección 'Descripción' (o --description-file <f>)\n" +
|
|
418
|
+
" --changes <texto> detalle de 'Cambios realizados' (default: los commits) (o --changes-file <f>)\n" +
|
|
419
|
+
" [--to-prod] confirma una PR contra la rama de producción (DAI_BRANCH_PROD)\n" +
|
|
420
|
+
" sin descripción y sin commits, con --yes o sin TTY, dai NO publica: la PR\n" +
|
|
421
|
+
" saldría con el molde del template y no se podría revisar\n" +
|
|
422
|
+
" forge comment <ref> --body-file <f> · forge pr <ref> comentar/leer una PR ajena (github/gitlab)\n" +
|
|
423
|
+
" forge review <ref> --from <review.json> [--dry-run|--yes] review inline: resumen + comentario por línea\n" +
|
|
424
|
+
" --min-severity low|medium|high · --min-confidence 0..1 · --max-comments N · --base <branch>\n" +
|
|
425
|
+
" Sin --yes no postea nada: muestra el preview y valida que cada hallazgo apunte al diff.\n\n" +
|
|
426
|
+
"Instalación:\n" +
|
|
427
|
+
" skills install [--global | --local <repo>] [--force] [--dry-run] [--for <asistentes>] instala las skills de dai (alias: `install`)\n" +
|
|
428
|
+
" skills install --from <git-url|npm:pkg|path>[#ref] [--for <asistentes>] instala skills EXTERNAS (por-stack), convertidas para los 3 asistentes (ADR-0013)\n" +
|
|
429
|
+
" init [<repo>] scaffolder interactivo del repo (asistente, gestor, OpenSpec)\n" +
|
|
430
|
+
" --for <asistentes> claude|copilot|cursor (combinables con coma) · o both|all (default all)\n" +
|
|
431
|
+
" ej: --for claude,cursor · --for copilot · --for all\n" +
|
|
432
|
+
" --pm md|jira|clickup · --openspec (con flags salteas las preguntas)\n" +
|
|
433
|
+
" 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" +
|
|
434
|
+
" 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" +
|
|
435
|
+
" docs <destino> documentación conceptual → <destino>\n" +
|
|
436
|
+
" doctor diagnóstico del entorno\n" +
|
|
437
|
+
" help [<comando>] el detalle de un comando (también: dai <comando> --help)\n\n" +
|
|
438
|
+
" (config: .env.dai — ver .env.dai.example)\n"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// El texto a imprimir para un tema. null/desconocido → la ayuda global.
|
|
443
|
+
export function helpFor(topic) {
|
|
444
|
+
if (!topic) return { text: globalUsage(), known: true };
|
|
445
|
+
const key = HELP_ALIAS[topic] || topic;
|
|
446
|
+
const text = COMMAND_HELP[key];
|
|
447
|
+
return text ? { text, known: true } : { text: globalUsage(), known: false };
|
|
448
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// dai · la PR/MR que YA existe para esta branch: cómo buscarla, cómo actualizarla (issue #46).
|
|
2
|
+
//
|
|
3
|
+
// El bug: con una MR ya abierta, `dai pr` pusheaba la branch (el diff quedaba al día) y
|
|
4
|
+
// después `glab mr create` fallaba porque la MR existía. El body NO se actualizaba y lo
|
|
5
|
+
// único que se veía era el comando crudo del forge, sin un error legible. Resultado: una
|
|
6
|
+
// MR con el diff correcto y una descripción que MIENTE sobre lo que contiene — el peor de
|
|
7
|
+
// los dos mundos, porque parece que salió bien.
|
|
8
|
+
//
|
|
9
|
+
// Acá está la parte pura y testeable: armar los comandos del forge y leer lo que devuelven.
|
|
10
|
+
// Los efectos (execFileSync) viven en dai.mjs.
|
|
11
|
+
|
|
12
|
+
// ── Buscar la PR/MR abierta de una branch ────────────────────────────────────
|
|
13
|
+
export function listPrCmd(tool, branch) {
|
|
14
|
+
if (tool === "gh") {
|
|
15
|
+
return ["pr", "list", "--head", branch, "--state", "open", "--limit", "5",
|
|
16
|
+
"--json", "number,url,title,baseRefName"];
|
|
17
|
+
}
|
|
18
|
+
return ["mr", "list", "--source-branch", branch, "--per-page", "5", "--output", "json"];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Normaliza la respuesta de gh/glab a la MISMA forma: { number, url, title, base }.
|
|
22
|
+
// Devuelve null si no hay ninguna abierta. Tira si el JSON no parsea — quien llama decide
|
|
23
|
+
// si eso es fatal (no lo es: se sigue por el camino de crear, que también sabe fallar bien).
|
|
24
|
+
export function parsePrList(tool, stdout) {
|
|
25
|
+
const raw = String(stdout ?? "").trim();
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
const arr = JSON.parse(raw);
|
|
28
|
+
const list = Array.isArray(arr) ? arr : Array.isArray(arr?.items) ? arr.items : [];
|
|
29
|
+
for (const it of list) {
|
|
30
|
+
// glab devuelve TODAS las MR de la branch si no se filtra por estado: las cerradas y
|
|
31
|
+
// mergeadas no cuentan — reabrir una MR mergeada no es lo que pidió nadie.
|
|
32
|
+
const state = String(it.state ?? "opened").toLowerCase();
|
|
33
|
+
if (!["open", "opened"].includes(state)) continue;
|
|
34
|
+
const number = it.number ?? it.iid ?? null;
|
|
35
|
+
if (number == null) continue;
|
|
36
|
+
return {
|
|
37
|
+
number: Number(number),
|
|
38
|
+
url: it.url ?? it.web_url ?? null,
|
|
39
|
+
title: it.title ?? null,
|
|
40
|
+
base: it.baseRefName ?? it.target_branch ?? null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Actualizar el body/título de una PR/MR existente ─────────────────────────
|
|
47
|
+
// gh lee el body de un archivo; glab lo toma como string (igual que en `mr create`, que
|
|
48
|
+
// ya funciona así). Por eso la firma pide los dos y cada uno usa el que le sirve.
|
|
49
|
+
export function updatePrCmd(tool, { number, title, body, bodyFile }) {
|
|
50
|
+
if (tool === "gh") {
|
|
51
|
+
return ["pr", "edit", String(number), "--title", title, "--body-file", bodyFile];
|
|
52
|
+
}
|
|
53
|
+
return ["mr", "update", String(number), "--title", title, "--description", body, "--yes"];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── "ya existe una PR para esta branch" ──────────────────────────────────────
|
|
57
|
+
// El forge lo dice de formas distintas y en inglés. Se reconoce para poder pasar al camino
|
|
58
|
+
// de actualizar en vez de morir con el comando crudo en pantalla.
|
|
59
|
+
const ALREADY = [
|
|
60
|
+
/already exists/i,
|
|
61
|
+
/a merge request already exists/i,
|
|
62
|
+
/existing (?:pull request|merge request)/i,
|
|
63
|
+
/pull request for branch .* already exists/i,
|
|
64
|
+
/open merge request already exists/i,
|
|
65
|
+
];
|
|
66
|
+
export function isAlreadyExistsError(msg) {
|
|
67
|
+
const s = String(msg ?? "");
|
|
68
|
+
return ALREADY.some((re) => re.test(s));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Lo que se le muestra al dev cuando dai detecta la PR existente: qué va a pasar con ella.
|
|
72
|
+
export function describeUpdate(pr, { base, tool }) {
|
|
73
|
+
const lines = [`ya hay una PR/MR abierta para esta branch: #${pr.number}${pr.url ? ` — ${pr.url}` : ""}`];
|
|
74
|
+
if (pr.base && base && pr.base !== base) {
|
|
75
|
+
lines.push(`ojo: apunta a '${pr.base}' y vos pediste '${base}'. dai NO cambia la base de una PR abierta: ` +
|
|
76
|
+
`si querés otra base, cerrala y creá una nueva.`);
|
|
77
|
+
}
|
|
78
|
+
lines.push(`dai va a ACTUALIZAR su título y su descripción con ${tool} (el diff ya lo actualiza el push).`);
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
package/cli/lib/us-format.mjs
CHANGED
|
@@ -108,8 +108,19 @@ export function validateUS(md) {
|
|
|
108
108
|
// (METODOLOGIA §4). Sube cuando el cambio es material, y eso lo sabe el PO: dai
|
|
109
109
|
// mirando el hash no puede distinguir un criterio nuevo de un typo corregido.
|
|
110
110
|
|
|
111
|
+
// La US se escribe a mano en un tracker, así que el nombre del campo llega como salga:
|
|
112
|
+
// `spec_version`, `spec version`, `spec-version` y —visto en un Jira real— `specversion`
|
|
113
|
+
// pegado. Con el `[_ ]` obligatorio de antes, `| specversion | v4 |` no matcheaba y el
|
|
114
|
+
// link se estampaba en `v1` sin que nada avisara (issue #46). El separador es opcional.
|
|
115
|
+
export const SPEC_VERSION_RE = /spec[\s_-]*version[^\n]*?\b(v\d+)\b/i;
|
|
116
|
+
|
|
117
|
+
// El valor que se escribe cuando la US NO declara versión. Un placeholder VISIBLE es
|
|
118
|
+
// mejor que un `v1` que parece correcto: el `v1` se publica en la PR y se estampa en el
|
|
119
|
+
// tracker como si fuera un dato, cuando es una suposición de dai.
|
|
120
|
+
export const PENDING_VERSION = "pendiente";
|
|
121
|
+
|
|
111
122
|
export const parseSpecVersion = (md) => {
|
|
112
|
-
const m = String(md || "").match(
|
|
123
|
+
const m = String(md || "").match(SPEC_VERSION_RE);
|
|
113
124
|
return m ? m[1] : null;
|
|
114
125
|
};
|
|
115
126
|
|
|
@@ -123,8 +134,8 @@ export const bumpSpecVersion = (v) => {
|
|
|
123
134
|
// al final, donde nadie lo ve, ni en una tabla que no existe.
|
|
124
135
|
export function setSpecVersion(md, version) {
|
|
125
136
|
const text = String(md || "");
|
|
126
|
-
if (
|
|
127
|
-
return text.replace(/(spec[
|
|
137
|
+
if (SPEC_VERSION_RE.test(text)) {
|
|
138
|
+
return text.replace(/(spec[\s_-]*version[^\n]*?\b)v\d+\b/i, `$1${version}`);
|
|
128
139
|
}
|
|
129
140
|
const lines = text.split(/\r?\n/);
|
|
130
141
|
const i = lines.findIndex((l) => /^#\s+\S/.test(l));
|
package/cli/lib/us.mjs
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
import { acHash } from "./ac-hash.mjs";
|
|
5
5
|
import { extractTitle } from "./link-us.mjs";
|
|
6
|
+
import { parseSpecVersion } from "./us-format.mjs";
|
|
6
7
|
|
|
7
8
|
// Parseo puro de una US (markdown o texto) → identidad + hash vivo.
|
|
8
9
|
export function parseUS(raw) {
|
|
9
10
|
const title = extractTitle(raw);
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
// El regex vive en us-format.mjs: es UNO solo para leer, escribir y bumpear la versión.
|
|
12
|
+
// Duplicado acá, la variante `specversion` (sin separador) se arreglaba en un lado y
|
|
13
|
+
// seguía rota en el otro — que es exactamente como nació el issue #46.
|
|
14
|
+
return { title, spec_version: parseSpecVersion(raw), ac_hash: acHash(raw) };
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
// Compara el hash estampado (implements.yaml) con el hash vivo de la US.
|
|
@@ -20,7 +20,21 @@ Ejemplo: `feature/ABC-482-finalizar-compra-sin-duplicado`
|
|
|
20
20
|
- El **ID nunca se tipea a mano**: sale del argumento de `/link-us ABC-###`. Elimina
|
|
21
21
|
el error de tipeo que rompe el link ([Art. 8](../docs/MANIFIESTO.md#art-8), Art. 9).
|
|
22
22
|
- El **slug** deriva del título de la US: minúsculas, sin acentos ni ñ, espacios → `-`.
|
|
23
|
-
- La **base** de
|
|
23
|
+
- La **base** de una PR no se recuerda ni se configura una por una: sale del **tipo de
|
|
24
|
+
rama**, leyendo las dos ramas de vida larga que el repo declara en su `.env.dai`.
|
|
25
|
+
|
|
26
|
+
| Tipo de rama | PR contra | Variable |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `feature/`, `fix/`, el resto | la rama que integra | `DAI_BRANCH_DEV` |
|
|
29
|
+
| `release/`, `hotfix/` | la rama que despliega a producción | `DAI_BRANCH_PROD` |
|
|
30
|
+
|
|
31
|
+
`dai pr` y `dai done` usan ese mapa; `--base` siempre gana. Si `DAI_BRANCH_DEV` no está
|
|
32
|
+
declarada, dai usa la rama default del remoto y **avisa que la está adivinando**.
|
|
33
|
+
- Apuntarle a `DAI_BRANCH_PROD` pide una **confirmación explícita**: hay que escribir el
|
|
34
|
+
nombre de la rama, y con `--yes` hace falta `--to-prod`. En repos con ramas de ambiente
|
|
35
|
+
(`testing` integra, `main` va a producción) esto es lo que evita que una PR quede
|
|
36
|
+
proponiendo un despliegue que nadie pidió. Sin la variable declarada, dai no marca
|
|
37
|
+
ninguna rama como producción: no adivina cuál es.
|
|
24
38
|
- Una rama, una US. Si una US toca varios repos, es una rama por repo, **todas con el
|
|
25
39
|
mismo `ABC-###`** — así el índice las agrupa en una fila (federación).
|
|
26
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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/",
|