@dforce2055/dai 0.13.2 → 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 +112 -0
- package/README.md +3 -2
- package/VERSION +1 -1
- package/cli/dai.mjs +198 -71
- 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/pm-adapter.mjs +19 -2
- package/cli/lib/pm-clickup.mjs +1 -0
- package/cli/lib/pm-jira.mjs +1 -0
- package/cli/lib/pr-remote.mjs +80 -0
- package/cli/lib/pr.mjs +20 -6
- package/cli/lib/us-format.mjs +14 -3
- package/cli/lib/us.mjs +39 -4
- package/governance/branch-naming.md +15 -1
- package/package.json +1 -1
- package/templates/pull-request.md +13 -9
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
|
+
}
|
package/cli/lib/pm-adapter.mjs
CHANGED
|
@@ -21,19 +21,36 @@
|
|
|
21
21
|
|
|
22
22
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
23
23
|
import { join, dirname } from "node:path";
|
|
24
|
-
import { parseUS, renderCoverage } from "./us.mjs";
|
|
24
|
+
import { parseUS, renderCoverage, explainFetchError } from "./us.mjs";
|
|
25
25
|
import { slugify } from "./link-us.mjs";
|
|
26
26
|
import { jiraAdapter } from "./pm-jira.mjs";
|
|
27
27
|
import { clickupAdapter } from "./pm-clickup.mjs";
|
|
28
28
|
|
|
29
29
|
// Re-export para compatibilidad (tests y CLI importan estos desde acá).
|
|
30
|
-
export { parseUS, coverageStatus, statusLabel, renderCoverage } from "./us.mjs";
|
|
30
|
+
export { parseUS, coverageStatus, statusLabel, renderCoverage, explainFetchError } from "./us.mjs";
|
|
31
|
+
|
|
32
|
+
// Consulta la US preservando la diferencia entre "no existe" y "no pude preguntar". Los
|
|
33
|
+
// adaptadores ya la hacen —404 → null, cualquier otro error → throw— pero se perdía en cada
|
|
34
|
+
// caller: `dai pr` la borraba con un `.catch(() => null)` y publicaba "sin US" en la PR, y
|
|
35
|
+
// los demás morían con el `fetch failed` pelado de undici. El gate de CI era el único que la
|
|
36
|
+
// respetaba, con su try/catch propio; esto es ese criterio, compartido.
|
|
37
|
+
//
|
|
38
|
+
// → { us, unreachable, reason } · unreachable: no hubo respuesta, no sabemos nada
|
|
39
|
+
export async function fetchLiveUS(adapter, id) {
|
|
40
|
+
if (!id) return { us: null, unreachable: false, reason: null };
|
|
41
|
+
try {
|
|
42
|
+
return { us: await adapter.fetchUS(id), unreachable: false, reason: null };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { us: null, unreachable: true, reason: explainFetchError(e, { kind: adapter.kind, endpoint: adapter.endpoint, id }) };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
31
47
|
|
|
32
48
|
// ── backend md (local, offline) ───────────────────────────────────────────────
|
|
33
49
|
function mdAdapter(env) {
|
|
34
50
|
const dir = env.DAI_MD_US_DIR || ".dai/us";
|
|
35
51
|
return {
|
|
36
52
|
kind: "md",
|
|
53
|
+
endpoint: dir,
|
|
37
54
|
fetchUS(id) {
|
|
38
55
|
const p = join(dir, `${id}.md`);
|
|
39
56
|
if (!existsSync(p)) return null;
|
package/cli/lib/pm-clickup.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export function clickupAdapter(env) {
|
|
|
25
25
|
if (!env.DAI_CLICKUP_TOKEN) throw new Error("falta DAI_CLICKUP_TOKEN en el .env.dai (backend clickup).");
|
|
26
26
|
return {
|
|
27
27
|
kind: "clickup",
|
|
28
|
+
endpoint: "api.clickup.com",
|
|
28
29
|
async fetchUS(id) {
|
|
29
30
|
const res = await fetch(clickupTaskUrl(id), { headers: clickupAuthHeaders(env) });
|
|
30
31
|
if (res.status === 404) return null;
|
package/cli/lib/pm-jira.mjs
CHANGED
|
@@ -139,6 +139,7 @@ export function jiraAdapter(env) {
|
|
|
139
139
|
if (!base) throw new Error("falta DAI_JIRA_BASE_URL en el .env.dai (backend jira).");
|
|
140
140
|
return {
|
|
141
141
|
kind: "jira",
|
|
142
|
+
endpoint: trim(base),
|
|
142
143
|
async fetchUS(id) {
|
|
143
144
|
const res = await daiFetch(jiraIssueUrl(base, id), { headers: jiraAuthHeaders(env) });
|
|
144
145
|
if (res.status === 404) return null;
|