@ingeniomaps/cauce 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +36 -6
- package/automatization/workflows/onboard.js +3 -2
- package/engine/automation/index.js +103 -0
- package/engine/cli/bootstrap.js +6 -2
- package/engine/cli/ops.js +37 -25
- package/engine/core/scan.js +32 -1
- package/package.json +1 -1
- package/template/Makefile +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,59 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.32.0] - 2026-08-19
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **Las credenciales de un multirepo no existían para el arranque.** `/onboard` leía un solo
|
|
22
|
+
`.env.example`, el de la raíz del workspace: en un monolito eso es todo, entre repositorios hermanos no
|
|
23
|
+
es nada. Una corrida real sobre tres repos dejó filas que decían «la credencial del proveedor» sin
|
|
24
|
+
nombrar una sola variable, y un externo que sólo ese archivo mencionaba —un certificado de organismo
|
|
25
|
+
fiscal— no apareció en ningún lado.
|
|
26
|
+
|
|
27
|
+
Ahora `ops scan` reporta, por servicio, los nombres que declara su ejemplo y de qué archivo salieron, y
|
|
28
|
+
el arranque los usa para nombrar cada fila. **Sólo los nombres**: el valor es de una persona, y el
|
|
29
|
+
ejemplo es la mitad pública del par. Se leen `.env.example`, `.env.sample`, `.env.template` y
|
|
30
|
+
`.env.dist`; un ejemplo de más de cuarenta variables es un archivo generado y no un contrato, así que
|
|
31
|
+
se corta ahí y lo dice.
|
|
32
|
+
|
|
33
|
+
- **`ops scan <ruta>` dejaba afuera al proyecto de la raíz.** Un monolito que declara sus comandos en el
|
|
34
|
+
nivel de arriba volvía listando todo menos a sí mismo, mientras que desde adentro de la instancia el
|
|
35
|
+
mismo árbol sí lo listaba: dos respuestas distintas sobre el mismo directorio. Los dos caminos comparten
|
|
36
|
+
una sola función y el proyecto de la raíz va primero.
|
|
37
|
+
|
|
38
|
+
## [0.31.0] - 2026-08-18
|
|
39
|
+
|
|
40
|
+
### Añadido
|
|
41
|
+
|
|
42
|
+
- **`ops automation uninstall <ops-root> <runner>`: sacar el wiring sin llevarse lo tuyo.** Hasta ahora
|
|
43
|
+
desinstalar era borrar la carpeta ops y descubrir después que cada llamada de herramienta ejecuta un
|
|
44
|
+
guard que ya no existe; la otra salida —borrar `.claude/` entero— se lleva puesto lo que hayas puesto
|
|
45
|
+
vos. El comando quita lo que Cauce entregó **y sigue igual que como lo entregó**: sus guards de la
|
|
46
|
+
configuración del runner, sus workflows, sus punteros a cargos. Tus hooks, tus workflows y tus skills
|
|
47
|
+
quedan donde están, y un archivo del toolkit que hayas editado se conserva y aparece nombrado en la
|
|
48
|
+
salida, porque decidir sobre él es tuyo. La instancia no se toca: borrarla es una decisión aparte.
|
|
49
|
+
También como `make uninstall-claude` y sus equivalentes.
|
|
50
|
+
|
|
51
|
+
### Corregido
|
|
52
|
+
|
|
53
|
+
- **Una instancia dejaba de enterarse de que había versiones nuevas.** `init` fija la versión exacta del
|
|
54
|
+
motor, así que `npm update` no la mueve y `upgrade` compara contra el que está instalado: quien
|
|
55
|
+
actualizaba con `make upgrade` recibía «la instancia está al día» en todas las versiones siguientes, sin
|
|
56
|
+
nada que le dijera que la comparación era local. Ahora la salida nombra contra qué comparó y da el
|
|
57
|
+
comando que trae un motor nuevo, y `make upgrade` lo corre antes de aplicar.
|
|
58
|
+
|
|
59
|
+
Actualizar son tres pasos y el tercero sigue siendo aparte: `npm install --save-dev
|
|
60
|
+
@ingeniomaps/cauce@latest`, `ops upgrade .` y `ops automation install . <runner>`. Los workflows y las
|
|
61
|
+
skills viven en el runner, no en la instancia, y `upgrade` lo recuerda al terminar.
|
|
62
|
+
|
|
63
|
+
### Cambiado
|
|
64
|
+
|
|
65
|
+
- **`init` termina en un solo cierre, y sus opciones se leen como una selección.** Las tres líneas finales
|
|
66
|
+
—cada una con tono de última— pasaron a ser una con la acción concreta, y las opciones de runner e
|
|
67
|
+
integración van una por línea con el default marcado donde se mira: `5) ninguno ← Enter`, en vez de un
|
|
68
|
+
`[ninguno]` pegado al prompt.
|
|
69
|
+
|
|
17
70
|
## [0.30.0] - 2026-08-18
|
|
18
71
|
|
|
19
72
|
### Añadido
|
package/README.md
CHANGED
|
@@ -31,12 +31,21 @@ instala la dependencia, deja el wiring del runner puesto y valida la instancia a
|
|
|
31
31
|
|
|
32
32
|
```text
|
|
33
33
|
¿Con qué runner vas a trabajar?
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
|
|
35
|
+
1) claude
|
|
36
|
+
2) codex
|
|
37
|
+
3) gemini
|
|
38
|
+
4) antigravity
|
|
39
|
+
5) ninguno ← Enter
|
|
40
|
+
|
|
41
|
+
> 1
|
|
36
42
|
|
|
37
43
|
¿Habilitar alguna integración?
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
|
|
45
|
+
1) jira
|
|
46
|
+
2) ninguna ← Enter
|
|
47
|
+
|
|
48
|
+
>
|
|
40
49
|
|
|
41
50
|
· npm install (el motor viene de la dependencia)
|
|
42
51
|
✓ claude: adaptador operativo (0 advertencia(s))
|
|
@@ -57,7 +66,8 @@ este proyecto, hasta cubrir lo que haga falta de esto:
|
|
|
57
66
|
|
|
58
67
|
Mientras tanto, esto es lo que hay: apps/api, apps/web
|
|
59
68
|
|
|
60
|
-
Abrí claude
|
|
69
|
+
→ Abrí claude acá y contestale esa pregunta.
|
|
70
|
+
Con tus respuestas escribe organization/, el mapa real de AGENTS.md y la primera épica.
|
|
61
71
|
El ciclo empieza en ops/planning/FLOW.md.
|
|
62
72
|
```
|
|
63
73
|
|
|
@@ -190,6 +200,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
|
|
|
190
200
|
| `ops automation list-hooks <ops-root>` | Describe los guards portables disponibles. |
|
|
191
201
|
| `ops automation check <ops-root>` | Valida guards, permisos y configuraciones. |
|
|
192
202
|
| `ops automation install <ops-root> <runner>` | Instala el wiring de Claude, Codex, Antigravity o Gemini. |
|
|
203
|
+
| `ops automation uninstall <ops-root> <runner>` | Quita ese wiring y conserva lo que no escribió Cauce. |
|
|
193
204
|
| `ops automation doctor <ops-root> <runner>` | Diagnostica una instalación materializada. |
|
|
194
205
|
|
|
195
206
|
`ops --help` lista las banderas de cada uno. En un proyecto generado, `make help` muestra los atajos
|
|
@@ -230,7 +241,20 @@ y actualizar no exige resolver conflictos: se reemplaza `system/` entero y nada
|
|
|
230
241
|
y sobrevive, desactivar uno del toolkit es quitarlo de la configuración del runner, y editar uno
|
|
231
242
|
existente detiene el `upgrade` antes de pisarlo.
|
|
232
243
|
|
|
233
|
-
###
|
|
244
|
+
### Actualizar
|
|
245
|
+
|
|
246
|
+
Son tres pasos y `make upgrade` hace los dos primeros:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npm install --save-dev @ingeniomaps/cauce@latest # trae el motor nuevo
|
|
250
|
+
node tools/ops.js upgrade . # aplica system/ y el runtime
|
|
251
|
+
node tools/ops.js automation install . claude # el wiring del runner
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
El primero no se puede saltear: `init` fija la versión exacta, así que `npm update` no la mueve y
|
|
255
|
+
`upgrade` compara contra el motor instalado —lo dice en su salida—. El tercero tampoco: los workflows y
|
|
256
|
+
las skills viven en el runner, no en la instancia, y `upgrade` no los toca. `upgrade` lo recuerda al
|
|
257
|
+
terminar.
|
|
234
258
|
|
|
235
259
|
Como `upgrade` reemplaza `system/` sin pedir confirmación, un cambio en el protocolo, en una regla del
|
|
236
260
|
sistema o en un guard es visible para el usuario y sube minor aunque no toque código. `upgrade` y
|
|
@@ -277,6 +301,12 @@ La instalación fusiona la configuración propia del runner y conserva las entra
|
|
|
277
301
|
reemplaza los guards que el propio toolkit había registrado sueltos por el grupo que ahora los cubre, y
|
|
278
302
|
lista cuáles quitó. Nada que no haya escrito el toolkit se toca.
|
|
279
303
|
|
|
304
|
+
Para sacarlo, `automation uninstall` quita exactamente lo que Cauce entregó y sigue igual que como lo
|
|
305
|
+
entregó: los guards de la configuración del runner, los workflows, los punteros a cargos. Lo que no
|
|
306
|
+
escribió —tus hooks, tus workflows, tus skills— queda donde está, y un archivo suyo que hayas editado se
|
|
307
|
+
conserva y se nombra en la salida. Borrar la carpeta ops sin esto deja al runner ejecutando guards que ya
|
|
308
|
+
no existen.
|
|
309
|
+
|
|
280
310
|
Qué comprueba cada guard, qué no puede comprobar y cómo se agrupan por evento está en
|
|
281
311
|
[automatization/hooks/README.md](automatization/hooks/README.md).
|
|
282
312
|
|
|
@@ -167,8 +167,9 @@ const drafted = await agent(
|
|
|
167
167
|
'completa.\n'}` +
|
|
168
168
|
`4. ${HUMAN}: una fila por cada cosa que necesita a una persona, con la tarea, el estado pendiente, el ` +
|
|
169
169
|
`origen "onboard" y la acción concreta que la desbloquea. Como mínimo, una por cada credencial que el ` +
|
|
170
|
-
`
|
|
171
|
-
`externo o MCP a conectar, y una por la autoridad del
|
|
170
|
+
`inventario nombra, diciendo la variable y el servicio que la espera —dónde se carga y quién lo hace, ` +
|
|
171
|
+
`sin proponer ningún valor—, una por cada sistema externo o MCP a conectar, y una por la autoridad del ` +
|
|
172
|
+
`runner, que hoy declara runner.allowPush=false.\n` +
|
|
172
173
|
`5. Las preguntas que queden abiertas, en la sección Ideas de ${INBOX}, sin promover.\n` +
|
|
173
174
|
`Devolvé en files cada archivo que tocaste y en assumptions cada supuesto que dejaste marcado.`,
|
|
174
175
|
{ schema: WRITTEN, label: 'contexto' },
|
|
@@ -478,6 +478,108 @@ function deliveryState(recorded, name, resolved, prefix = '') {
|
|
|
478
478
|
return delivered && delivered === current ? 'desactualizado' : 'ajeno'
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
// Quita de una estructura de configuración exactamente lo que este adaptador habría puesto, y nada más.
|
|
482
|
+
// Es el inverso de `mergeConfig`: una entrada del usuario nunca coincide literalmente con la nuestra, así
|
|
483
|
+
// que sobrevive; una que editó tampoco coincide, y por eso se conserva y se avisa en vez de borrarse.
|
|
484
|
+
function unmergeConfig(current, incoming) {
|
|
485
|
+
if (Array.isArray(incoming)) {
|
|
486
|
+
if (!Array.isArray(current)) return current
|
|
487
|
+
const nuestras = new Set(incoming.map((value) => JSON.stringify(value)))
|
|
488
|
+
return current.filter((value) => !nuestras.has(JSON.stringify(value)))
|
|
489
|
+
}
|
|
490
|
+
if (incoming && typeof incoming === 'object') {
|
|
491
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) return current
|
|
492
|
+
const result = { ...current }
|
|
493
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
494
|
+
if (!(key in result)) continue
|
|
495
|
+
const limpio = unmergeConfig(result[key], value)
|
|
496
|
+
// Una clave que queda vacía por habernos ido no es del usuario: la creamos nosotros al instalar.
|
|
497
|
+
const vacia = limpio === undefined
|
|
498
|
+
|| (Array.isArray(limpio) && !limpio.length)
|
|
499
|
+
|| (limpio && typeof limpio === 'object' && !Array.isArray(limpio) && !Object.keys(limpio).length)
|
|
500
|
+
if (vacia) delete result[key]
|
|
501
|
+
else result[key] = limpio
|
|
502
|
+
}
|
|
503
|
+
return result
|
|
504
|
+
}
|
|
505
|
+
return JSON.stringify(current) === JSON.stringify(incoming) ? undefined : current
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Borra el archivo y, de paso, los directorios que quedaron vacíos por haberlo sacado. Nunca sube más
|
|
509
|
+
// allá del límite: `.claude/` puede tener cosas del usuario aunque `.claude/workflows/` quede vacío.
|
|
510
|
+
function removeFile(file, boundary) {
|
|
511
|
+
fs.rmSync(file, { force: true })
|
|
512
|
+
let dir = path.dirname(file)
|
|
513
|
+
while (dir.startsWith(boundary) && dir !== boundary) {
|
|
514
|
+
try { if (fs.readdirSync(dir).length) return } catch { return }
|
|
515
|
+
fs.rmdirSync(dir)
|
|
516
|
+
dir = path.dirname(dir)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Saca el wiring de un runner dejando intacto lo que no escribimos nosotros.
|
|
521
|
+
//
|
|
522
|
+
// Existe porque desinstalar a mano es borrar `ops/` y descubrir después que cada llamada de herramienta
|
|
523
|
+
// ejecuta un guard que ya no está. Y porque la alternativa —borrar `.claude/` entero— se lleva puesto lo
|
|
524
|
+
// que el usuario haya puesto ahí, que es suyo y no tiene por qué desaparecer con el toolkit.
|
|
525
|
+
//
|
|
526
|
+
// La regla es una sola: se quita lo que Cauce entregó y sigue igual que como lo entregó. Un archivo con
|
|
527
|
+
// cambios propios se conserva y se nombra; decidir sobre él es de la persona, no de este comando.
|
|
528
|
+
function uninstall(root, name, output = console) {
|
|
529
|
+
if (O.mode(root) === 'toolkit') {
|
|
530
|
+
throw new Error('Acá se fabrica Cauce, no se lo consume.')
|
|
531
|
+
}
|
|
532
|
+
const runner = runnerManifest(root, name)
|
|
533
|
+
const paths = runnerPaths(root, name, runner)
|
|
534
|
+
const prefix = opsPrefix(root)
|
|
535
|
+
const recorded = M.readRunners(root)
|
|
536
|
+
const conservados = []
|
|
537
|
+
let quitados = 0
|
|
538
|
+
|
|
539
|
+
const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
|
|
540
|
+
const entregado = { ...recorded }
|
|
541
|
+
for (const item of items) {
|
|
542
|
+
const resolved = { item, ...resolveItem(paths, root, name, item) }
|
|
543
|
+
const key = deliveryKey(name, item.target)
|
|
544
|
+
if (!fs.existsSync(resolved.target)) { delete entregado[key]; continue }
|
|
545
|
+
const situacion = deliveryState(recorded, name, resolved, prefix)
|
|
546
|
+
if (situacion === 'ajeno') { conservados.push(item.target); continue }
|
|
547
|
+
removeFile(resolved.target, paths.install)
|
|
548
|
+
delete entregado[key]
|
|
549
|
+
quitados += 1
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Los punteros a cargos no se registran uno por uno —son cuarenta y siete y se regeneran enteros—,
|
|
553
|
+
// así que se reconocen por contenido: sólo se va el que sigue siendo el que generamos.
|
|
554
|
+
if (runner.capabilities.nativeSkills && runner.roleSkills) {
|
|
555
|
+
const base = path.resolve(paths.install, runner.roleSkills)
|
|
556
|
+
for (const role of roleCatalog(root)) {
|
|
557
|
+
const file = path.join(base, role.slug, 'SKILL.md')
|
|
558
|
+
if (!fs.existsSync(file)) continue
|
|
559
|
+
if (M.digest(file) !== M.digestText(roleSkill(role))) {
|
|
560
|
+
conservados.push(path.relative(paths.install, file))
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
563
|
+
removeFile(file, paths.install)
|
|
564
|
+
quitados += 1
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (fs.existsSync(paths.configTarget)) {
|
|
569
|
+
const current = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
|
|
570
|
+
const limpio = unmergeConfig(current, runnerConfig(paths, root))
|
|
571
|
+
if (limpio && Object.keys(limpio).length) F.atomicWriteJson(paths.configTarget, limpio)
|
|
572
|
+
else { removeFile(paths.configTarget, paths.install); quitados += 1 }
|
|
573
|
+
output.log(`✓ ${name}: ${runner.config.target} sin las entradas de Cauce`)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
M.write(root, undefined, entregado)
|
|
577
|
+
output.log(`✓ ${name}: ${quitados} archivo(s) del toolkit quitados de ${paths.install}`)
|
|
578
|
+
for (const file of conservados) output.log(`= ${name}: conservado ${file} (tiene cambios tuyos)`)
|
|
579
|
+
if (runner.activation) output.log(` ${name}: si lo habías registrado a mano, quitalo también.`)
|
|
580
|
+
return { removed: quitados, kept: conservados }
|
|
581
|
+
}
|
|
582
|
+
|
|
481
583
|
function install(root, name, output = console, options = {}) {
|
|
482
584
|
// `install` arma la superficie de consumo de una empresa: punteros a cada cargo, una copia de los
|
|
483
585
|
// workflows y los guards. Acá los cargos y los workflows son el producto —la copia divergiría— y
|
|
@@ -571,6 +673,7 @@ module.exports = {
|
|
|
571
673
|
check,
|
|
572
674
|
doctor,
|
|
573
675
|
install,
|
|
676
|
+
uninstall,
|
|
574
677
|
legacyGuardWiring,
|
|
575
678
|
roleCatalog,
|
|
576
679
|
roleSkill,
|
package/engine/cli/bootstrap.js
CHANGED
|
@@ -25,10 +25,14 @@ function terminal() {
|
|
|
25
25
|
// dejar que ese rechazo suba terminaba la corrida con «Aborted with Ctrl+D» y la instancia recién creada
|
|
26
26
|
// sin una línea que dijera cómo seguir. El default no hace nada, así que tomarlo no decide nada.
|
|
27
27
|
async function elegir(deps, texto, opciones, fallback) {
|
|
28
|
-
|
|
28
|
+
// Una opción por línea y el default señalado donde se mira: apretar Enter es la respuesta más común, y
|
|
29
|
+
// en una sola línea apretada el `[ninguno]` del final no se lee como «esto pasa si no elegís nada».
|
|
30
|
+
const listado = opciones
|
|
31
|
+
.map((opcion, indice) => ` ${indice + 1}) ${opcion}${opcion === fallback ? ' ← Enter' : ''}`)
|
|
32
|
+
.join('\n')
|
|
29
33
|
for (let intento = 0; intento < INTENTOS; intento += 1) {
|
|
30
34
|
let dicho
|
|
31
|
-
try { dicho = await deps.ask(`\n${texto}\n${listado}\n
|
|
35
|
+
try { dicho = await deps.ask(`\n${texto}\n\n${listado}\n\n> `) } catch {
|
|
32
36
|
deps.log(`\n sin respuesta: sigo con ${fallback}.`)
|
|
33
37
|
return fallback
|
|
34
38
|
}
|
package/engine/cli/ops.js
CHANGED
|
@@ -63,6 +63,7 @@ function usage() {
|
|
|
63
63
|
ops automation check <ops-root>
|
|
64
64
|
ops automation doctor <ops-root> claude|codex|gemini|antigravity
|
|
65
65
|
ops automation install <ops-root> claude|codex|gemini|antigravity
|
|
66
|
+
ops automation uninstall <ops-root> claude|codex|gemini|antigravity
|
|
66
67
|
ops learn <agent> [--proposal] [--applied [--period <AAAA-MM>]]
|
|
67
68
|
ops evaluate <agent> [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
|
|
68
69
|
ops agents list [ops-root] [--own|--system] [--json]
|
|
@@ -334,15 +335,8 @@ async function init(target, cli) {
|
|
|
334
335
|
// corriendo `init`, no cuesta nada, y es lo único que le dice a alguien qué hacer con lo que acaba de
|
|
335
336
|
// crear. Dejarlo adentro del camino feliz lo escondía justo de quien más lo necesita.
|
|
336
337
|
console.log('')
|
|
337
|
-
onboard(root, SIN_BANDERAS)
|
|
338
|
-
if (resultado.instalado && resultado.runner !== BOOT.SIN_RUNNER) {
|
|
339
|
-
console.log(`\nAbrí ${resultado.runner} en este directorio para que las escriba por vos.`)
|
|
340
|
-
}
|
|
341
|
-
console.log('')
|
|
338
|
+
onboard(root, SIN_BANDERAS, resultado.instalado ? resultado.runner : '')
|
|
342
339
|
for (const paso of initSteps(enter, resultado)) console.log(paso)
|
|
343
|
-
if (resultado.instalado) {
|
|
344
|
-
console.log(`El ciclo empieza en ${path.join(relative || '.', 'planning', 'FLOW.md')}.`)
|
|
345
|
-
}
|
|
346
340
|
if (resultado.error) fail(`${resultado.error}: la instancia quedó creada pero todavía no funciona.`)
|
|
347
341
|
}
|
|
348
342
|
|
|
@@ -359,29 +353,32 @@ function workspaceRoots(root) {
|
|
|
359
353
|
|
|
360
354
|
// Qué hay en las raíces declaradas, antes de que nadie razone sobre ello. La raíz ops se saltea: no es
|
|
361
355
|
// un servicio del proyecto, y su `package.json` sólo declara el motor.
|
|
356
|
+
// Los candidatos de una raíz, con el proyecto que vive en ella misma primero: un monolito declara sus
|
|
357
|
+
// comandos en el nivel de arriba, y dejarlo afuera hacía desaparecer justo al proyecto principal.
|
|
358
|
+
function candidates(workspace, skip = '') {
|
|
359
|
+
const result = SC.scan(workspace, skip)
|
|
360
|
+
const found = result.rootManifests.length
|
|
361
|
+
? [{ path: '.', root: workspace, runtimes: ['raíz'], commands: result.rootCommands, env: result.rootEnv }]
|
|
362
|
+
: []
|
|
363
|
+
return [...found, ...result.services.map((service) => ({ ...service, root: workspace }))]
|
|
364
|
+
}
|
|
365
|
+
|
|
362
366
|
function inventory(root) {
|
|
363
|
-
|
|
364
|
-
for (const workspace of workspaceRoots(root)) {
|
|
365
|
-
const result = SC.scan(workspace, root)
|
|
366
|
-
if (result.rootManifests.length) {
|
|
367
|
-
found.push({ path: '.', root: workspace, runtimes: ['raíz'], commands: result.rootCommands })
|
|
368
|
-
}
|
|
369
|
-
for (const service of result.services) found.push({ ...service, root: workspace })
|
|
370
|
-
}
|
|
371
|
-
return found
|
|
367
|
+
return workspaceRoots(root).flatMap((workspace) => candidates(workspace, root))
|
|
372
368
|
}
|
|
373
369
|
|
|
374
370
|
function scan(target, cli) {
|
|
375
371
|
const root = path.resolve(target || '.')
|
|
376
|
-
const result = target
|
|
377
|
-
? { root: path.resolve(target), services: SC.scan(path.resolve(target)).services }
|
|
378
|
-
: { root, services: inventory(root) }
|
|
372
|
+
const result = { root, services: target ? candidates(root) : inventory(root) }
|
|
379
373
|
if (cli.has('--json')) return console.log(JSON.stringify(result, null, 2))
|
|
380
374
|
// Un monorepo de sesenta paquetes no se lee en pantalla. Se recorta, y se dice cuánto: un corte que no
|
|
381
375
|
// se anuncia hace pasar lo listado por todo lo que hay.
|
|
382
376
|
for (const service of result.services.slice(0, LISTA)) {
|
|
383
377
|
const donde = service.root && service.root !== result.root ? `${path.basename(service.root)}/` : ''
|
|
384
|
-
|
|
378
|
+
const espera = service.env ? `\n espera ${service.env.names.join(', ')} (${service.env.file})` : ''
|
|
379
|
+
console.log(
|
|
380
|
+
`${donde}${service.path} [${(service.runtimes || []).join(', ')}]${comandos(service.commands)}${espera}`,
|
|
381
|
+
)
|
|
385
382
|
}
|
|
386
383
|
if (result.services.length > LISTA) {
|
|
387
384
|
console.log(`… y ${result.services.length - LISTA} más, todos en --json`)
|
|
@@ -399,7 +396,7 @@ function comandos(commands) {
|
|
|
399
396
|
|
|
400
397
|
// La guía de arranque: qué hay, qué falta y qué preguntar. Determinista y en milisegundos, porque es lo
|
|
401
398
|
// primero que ve alguien que acaba de instalar y todavía no sabe qué hace la herramienta.
|
|
402
|
-
function onboard(rootArg, cli) {
|
|
399
|
+
function onboard(rootArg, cli, runner = '') {
|
|
403
400
|
const root = path.resolve(rootArg || '.')
|
|
404
401
|
const services = inventory(root)
|
|
405
402
|
const state = OB.guide(root, services)
|
|
@@ -426,8 +423,12 @@ function onboard(rootArg, cli) {
|
|
|
426
423
|
console.log(`Esta instancia ya tiene ${escrito} escrito: el arranque no la va a pisar.`)
|
|
427
424
|
return
|
|
428
425
|
}
|
|
429
|
-
|
|
430
|
-
|
|
426
|
+
// Un solo cierre: tres líneas que suenan a final se leen como tres finales, y quien recién instaló
|
|
427
|
+
// termina sin saber cuál era el paso.
|
|
428
|
+
console.log(runner
|
|
429
|
+
? `\n→ Abrí ${runner} acá y contestale esa pregunta.`
|
|
430
|
+
: '\n→ Contestá esa pregunta cuando corras el arranque.')
|
|
431
|
+
console.log(' Con tus respuestas escribe organization/, el mapa real de AGENTS.md y la primera épica.')
|
|
431
432
|
}
|
|
432
433
|
|
|
433
434
|
function check(dir, cli) {
|
|
@@ -743,7 +744,13 @@ function upgrade(dir, cli) {
|
|
|
743
744
|
const overrides = O.overrides(root)
|
|
744
745
|
|
|
745
746
|
if (dry) {
|
|
746
|
-
if (from === to)
|
|
747
|
+
if (from === to) {
|
|
748
|
+
// Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
|
|
749
|
+
// importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
|
|
750
|
+
// a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
|
|
751
|
+
console.log(`= ${to}: la instancia está al día con el motor instalado`)
|
|
752
|
+
return console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
|
|
753
|
+
}
|
|
747
754
|
console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
|
|
748
755
|
printChangelog(from, to)
|
|
749
756
|
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
@@ -1112,6 +1119,11 @@ function automation(action, rootArg, runnerName, cli) {
|
|
|
1112
1119
|
console.log(`✓ ${runnerName}: adaptador operativo (${result.warnings.length} advertencia(s))`)
|
|
1113
1120
|
return
|
|
1114
1121
|
}
|
|
1122
|
+
if (action === 'uninstall') {
|
|
1123
|
+
try { A.uninstall(root, runnerName, console) } catch (error) { fail(error.message, 2) }
|
|
1124
|
+
console.log(' la instancia sigue en pie: borrar la carpeta ops es una decisión aparte.')
|
|
1125
|
+
return
|
|
1126
|
+
}
|
|
1115
1127
|
if (action === 'install') {
|
|
1116
1128
|
let runner
|
|
1117
1129
|
const force = cli.has('--force')
|
package/engine/core/scan.js
CHANGED
|
@@ -98,6 +98,35 @@ function manifestsOf(dir) {
|
|
|
98
98
|
return MANIFESTS.filter((entry) => fs.existsSync(path.join(dir, entry.file)))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Qué credenciales espera un servicio, por su nombre y nada más. El ejemplo es público y versionado —el
|
|
102
|
+
// `.env` de verdad no se toca nunca—, y los nombres son justo lo que hace falta para dejar una fila que
|
|
103
|
+
// diga qué cargar; el valor no le corresponde a nadie más que a una persona.
|
|
104
|
+
//
|
|
105
|
+
// Se lee por servicio y no sólo en la raíz porque en un multirepo cada repositorio trae el suyo: leyendo
|
|
106
|
+
// una sola raíz, las credenciales de tres repos se volvían invisibles y las acciones humanas terminaban
|
|
107
|
+
// diciendo «la credencial del proveedor» en vez de nombrarla.
|
|
108
|
+
const ENV_EXAMPLES = ['.env.example', '.env.sample', '.env.template', '.env.dist']
|
|
109
|
+
|
|
110
|
+
// Un ejemplo con cientos de variables es un archivo generado, no un contrato: se corta y se dice.
|
|
111
|
+
const ENV_MAX = 40
|
|
112
|
+
|
|
113
|
+
function expectedEnv(dir) {
|
|
114
|
+
for (const name of ENV_EXAMPLES) {
|
|
115
|
+
const file = path.join(dir, name)
|
|
116
|
+
if (!fs.existsSync(file)) continue
|
|
117
|
+
let text = ''
|
|
118
|
+
try { text = fs.readFileSync(file, 'utf8') } catch { return null }
|
|
119
|
+
const names = text.split('\n')
|
|
120
|
+
.map((line) => line.trim())
|
|
121
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
122
|
+
.map((line) => line.replace(/^export\s+/, '').split('=')[0].trim())
|
|
123
|
+
.filter((key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
124
|
+
if (!names.length) return null
|
|
125
|
+
return { file: name, names: names.slice(0, ENV_MAX), truncated: Math.max(0, names.length - ENV_MAX) }
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
101
130
|
// Servicios candidatos bajo `root`, sin entrar en `skip` —típicamente la raíz ops, que no es un
|
|
102
131
|
// servicio del proyecto—. El resultado es una lista, no un veredicto: decidir cuál es el producto y
|
|
103
132
|
// cuál quedó muerto sigue siendo trabajo de una persona o de un cargo.
|
|
@@ -113,6 +142,7 @@ function services(root, skip = '') {
|
|
|
113
142
|
runtimes: manifests.map((entry) => entry.runtime),
|
|
114
143
|
manifests: manifests.map((entry) => entry.file),
|
|
115
144
|
commands: commandsOf(dir),
|
|
145
|
+
env: expectedEnv(dir),
|
|
116
146
|
})
|
|
117
147
|
}
|
|
118
148
|
if (depth >= DEPTH) return
|
|
@@ -136,8 +166,9 @@ function scan(root, skip = '') {
|
|
|
136
166
|
root: path.resolve(root),
|
|
137
167
|
rootManifests: manifestsOf(root).map((entry) => entry.file),
|
|
138
168
|
rootCommands: commandsOf(root),
|
|
169
|
+
rootEnv: expectedEnv(root),
|
|
139
170
|
services: services(root, skip),
|
|
140
171
|
}
|
|
141
172
|
}
|
|
142
173
|
|
|
143
|
-
module.exports = { scan, services, IGNORED, DEPTH }
|
|
174
|
+
module.exports = { scan, services, expectedEnv, IGNORED, DEPTH, ENV_MAX }
|
package/package.json
CHANGED
package/template/Makefile
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
.PHONY: integration-check integration-sync require-key integration-promote
|
|
5
5
|
.PHONY: require-agent agent-learn agent-propose agent-evaluate require-team team-check team-show
|
|
6
6
|
.PHONY: install-claude install-codex install-gemini install-antigravity
|
|
7
|
+
.PHONY: uninstall-claude uninstall-codex uninstall-gemini uninstall-antigravity
|
|
7
8
|
.PHONY: doctor-claude doctor-codex doctor-gemini doctor-antigravity
|
|
8
9
|
|
|
9
10
|
# Jira es hoy el único proveedor; el día que haya otro se pasa PROVIDER=<slug>.
|
|
@@ -22,7 +23,10 @@ tree: ## Muestra roadmap, backlog, WIP y Done
|
|
|
22
23
|
context: ## Muestra el contexto mínimo de la tarea vigente
|
|
23
24
|
@node tools/ops.js context planning
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
# `init` fija la versión exacta del motor, así que `npm update` no la mueve: hay que pedir @latest.
|
|
27
|
+
# Sin este primer paso `upgrade` compara contra el motor instalado y contesta «al día» para siempre.
|
|
28
|
+
upgrade: ## Trae la última versión de Cauce y la aplica, sin tocar lo del proyecto
|
|
29
|
+
@npm install --save-dev @ingeniomaps/cauce@latest
|
|
26
30
|
@node tools/ops.js upgrade .
|
|
27
31
|
|
|
28
32
|
automation-check: ## Comprueba hooks, workflows y adaptadores
|
|
@@ -73,6 +77,18 @@ install-gemini: ## Instala contexto, comandos y configuración para Gemini
|
|
|
73
77
|
install-antigravity: ## Instala el plugin Cauce para Antigravity CLI
|
|
74
78
|
@node tools/ops.js automation install . antigravity
|
|
75
79
|
|
|
80
|
+
uninstall-claude: ## Quita de Claude el wiring de Cauce, conservando lo tuyo
|
|
81
|
+
@node tools/ops.js automation uninstall . claude
|
|
82
|
+
|
|
83
|
+
uninstall-codex: ## Quita de Codex el wiring de Cauce, conservando lo tuyo
|
|
84
|
+
@node tools/ops.js automation uninstall . codex
|
|
85
|
+
|
|
86
|
+
uninstall-gemini: ## Quita de Gemini el wiring de Cauce, conservando lo tuyo
|
|
87
|
+
@node tools/ops.js automation uninstall . gemini
|
|
88
|
+
|
|
89
|
+
uninstall-antigravity: ## Quita de Antigravity el wiring de Cauce, conservando lo tuyo
|
|
90
|
+
@node tools/ops.js automation uninstall . antigravity
|
|
91
|
+
|
|
76
92
|
doctor-claude: ## Diagnostica la instalación de Claude
|
|
77
93
|
@node tools/ops.js automation doctor . claude
|
|
78
94
|
|