@ingeniomaps/cauce 0.27.0 → 0.29.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 +62 -0
- package/README.md +18 -3
- package/automatization/runners/antigravity/manifest.json +4 -0
- package/automatization/runners/antigravity/skills/onboard/SKILL.md +30 -0
- package/automatization/runners/claude/CLAUDE.md +3 -2
- package/automatization/runners/claude/manifest.json +4 -0
- package/automatization/runners/codex/AGENTS.md +14 -0
- package/automatization/runners/gemini/GEMINI.md +14 -0
- package/automatization/workflows/onboard.js +211 -0
- package/engine/cli/args.js +1 -0
- package/engine/cli/ops.js +53 -2
- package/engine/core/scan.js +120 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,68 @@ 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.29.0] - 2026-08-18
|
|
18
|
+
|
|
19
|
+
### Añadido
|
|
20
|
+
|
|
21
|
+
- **`ops scan`: qué hay en el workspace, resuelto por código.** Lista los subproyectos con manifiesto
|
|
22
|
+
propio, su runtime y los comandos de test, lint y build que cada uno declara, diciendo de qué archivo
|
|
23
|
+
salió cada comando. No corre ninguno y no inventa ninguno: un comando que nadie declaró se lee igual
|
|
24
|
+
que uno real, y el primer Verify de una tarea es donde eso se descubre. Saltea `node_modules` y todo
|
|
25
|
+
directorio oculto, que es lo que lo mantiene en milisegundos. Una instancia sidecar escanea su carpeta
|
|
26
|
+
madre, donde vive el código; cualquier otro modo, donde está parada.
|
|
27
|
+
|
|
28
|
+
### Cambiado
|
|
29
|
+
|
|
30
|
+
- **`/onboard` cuesta lo que encuentra.** Empezaba pidiéndole a un agente que explorara el repositorio y
|
|
31
|
+
terminaba corriendo la suite de tests de cada servicio: una corrida sobre una carpeta vacía gastó doce
|
|
32
|
+
minutos sin poder producir nada. Ahora arranca con `ops scan` —una llamada, milisegundos— y, si el
|
|
33
|
+
workspace no tiene código y nadie aportó contexto, termina ahí diciendo qué le falta, en vez de escribir
|
|
34
|
+
una empresa inventada.
|
|
35
|
+
|
|
36
|
+
El recorrido ya no ejecuta nada del proyecto. El mapa real anota cada comando **tal como está
|
|
37
|
+
declarado**, con su archivo de origen, y verificarlo corriéndolo pasa a ser una historia de la épica
|
|
38
|
+
001, donde tiene dueño y tiempo asignado. Lo que escribe y lo que deja a una persona no cambió:
|
|
39
|
+
borradores marcados como supuestos, credenciales y MCP en `HUMAN_ACTIONS.md`, épica sin promover.
|
|
40
|
+
|
|
41
|
+
Si ya tenés una instancia, el recorrido actualizado llega con `automation install`, no con `upgrade`:
|
|
42
|
+
los workflows viven en el runner.
|
|
43
|
+
|
|
44
|
+
## [0.28.0] - 2026-08-18
|
|
45
|
+
|
|
46
|
+
### Añadido
|
|
47
|
+
|
|
48
|
+
- **`/onboard`: el recorrido que llena una instancia recién creada.** `init` la deja funcionando y sin
|
|
49
|
+
enterar de nada —`organization/` es el molde y el roadmap está vacío—, y llenarlo exige leer el
|
|
50
|
+
repositorio y decidir qué es cada cosa, que es justo lo que un CLI determinista no puede hacer. El
|
|
51
|
+
recorrido inventaría los subproyectos con manifiesto propio, **corre** los comandos de test, lint y
|
|
52
|
+
build que cada uno declara, y escribe `organization/`, la sección «Mapa real» de `AGENTS.md` y las
|
|
53
|
+
raíces reales en `ops.config.json`.
|
|
54
|
+
|
|
55
|
+
Corre los comandos en vez de copiarlos del README porque un mapa copiado envejece sin avisar y el
|
|
56
|
+
primer Verify de una tarea real es donde aparece: cada comando queda como verificado, falla o ausente.
|
|
57
|
+
Lo que deduce va marcado «(supuesto)» y lo que nada sostiene queda «Por definir». No lee `.env` ni
|
|
58
|
+
ninguna credencial —de `.env.example` toma sólo los nombres de variable—, y las credenciales, los MCP
|
|
59
|
+
y el permiso de push salen como filas de `HUMAN_ACTIONS.md` con la acción concreta que las desbloquea,
|
|
60
|
+
sin proponer ningún valor. Cierra escribiendo la épica 001 —que una tarea pueda atravesar el ciclo
|
|
61
|
+
entero— y no la promueve.
|
|
62
|
+
|
|
63
|
+
Si la instancia ya tiene contexto escrito, para y pide `force`: reescribir un borrador que alguien
|
|
64
|
+
corrigió no deja rastro de lo que se perdió. Y un workspace todavía sin código no es un error: escribe
|
|
65
|
+
lo que el contexto permita y traer los repos pasa a ser la primera historia.
|
|
66
|
+
|
|
67
|
+
Claude y Antigravity lo reciben como recorrido ejecutable; Codex y Gemini lo operan desde la sección
|
|
68
|
+
«El arranque» que `automation install` deja en sus instrucciones. Si ya tenés una instancia, el
|
|
69
|
+
workflow llega con `automation install` —no con `upgrade`—: los workflows viven en el runner.
|
|
70
|
+
|
|
71
|
+
### Corregido
|
|
72
|
+
|
|
73
|
+
- **`init` dentro de una carpeta que ya nombra al toolkit deja de anidar.** Correrlo parado en
|
|
74
|
+
`acme-ops/` creaba `acme-ops/ops/` —una raíz ops dentro de otra— y llamaba «acme-ops» al proyecto.
|
|
75
|
+
Ahora la instancia es esa carpeta y el proyecto se llama «acme». Además un `.git` solo dejó de contar
|
|
76
|
+
como contenido, así que `mkdir acme-ops && git init && cauce init` no pide `--force` para no pisar
|
|
77
|
+
nada.
|
|
78
|
+
|
|
17
79
|
## [0.27.0] - 2026-08-18
|
|
18
80
|
|
|
19
81
|
### Añadido
|
package/README.md
CHANGED
|
@@ -96,9 +96,23 @@ falta igual —el motor, los guards y los workflows son JavaScript—.
|
|
|
96
96
|
|
|
97
97
|
### El primer ciclo
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
`init` deja la instancia funcionando, no enterada: `organization/` llega como molde y el roadmap está
|
|
100
|
+
vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es lo que un CLI determinista
|
|
101
|
+
no puede hacer, así que ese recorrido vive en el runner:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
/onboard parte del inventario de `ops scan` y escribe organization/, el «Mapa real» de
|
|
105
|
+
AGENTS.md —cada comando como está declarado y de qué archivo salió— y las raíces
|
|
106
|
+
de ops.config.json. Lo deducido queda marcado como supuesto; credenciales, MCP y
|
|
107
|
+
el permiso de push van a HUMAN_ACTIONS.md. Cierra con la épica 001, sin promoverla.
|
|
108
|
+
No corre nada del proyecto: verificar los comandos es una historia de esa épica.
|
|
109
|
+
/team evalúa si una intención posterior es viable y propone su épica.
|
|
110
|
+
/autobuild ejecuta una tarea ya promovida, fase por fase.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Claude y Antigravity lo traen como recorrido ejecutable; Codex y Gemini lo operan siguiendo las
|
|
114
|
+
instrucciones que `automation install` les deja. Sin runner, la [tabla de comandos](#comandos) y
|
|
115
|
+
[FLOW.md](template/planning/FLOW.md) hacen el mismo camino a mano.
|
|
102
116
|
|
|
103
117
|
Dentro del proyecto el CLI se invoca con `node tools/ops.js` —o `npx cauce`, que la dependencia deja
|
|
104
118
|
disponible—; desde este repositorio, con `node engine/cli/ops.js`. En la tabla de abajo `ops` representa
|
|
@@ -128,6 +142,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
|
|
|
128
142
|
| Comando | Función |
|
|
129
143
|
|---|---|
|
|
130
144
|
| `ops init [destino]` | Materializa una instancia y la deja usable; sin destino, en `ops/` y modo sidecar. |
|
|
145
|
+
| `ops scan [workspace]` | Inventaría servicios y comandos declarados, sin correr ninguno. |
|
|
131
146
|
| `ops check <planning>` | Valida contratos, unicidad, trazabilidad y estados. |
|
|
132
147
|
| `ops tree <planning>` | Muestra roadmap, backlog, WIP, inbox y done sin mutar nada. |
|
|
133
148
|
| `ops context <planning>` | Emite el contexto mínimo de la tarea vigente para un runner. |
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onboard
|
|
3
|
+
description: Escanea el repositorio y deja escrito el contexto de la empresa y la primera épica.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Es el arranque de una instancia recién creada: `init` la instaló, pero nadie le explicó todavía qué es
|
|
7
|
+
este proyecto. Antes de empezar comprobá que siga vacía —`{{OPS_DIR}}organization/company.md` con sus
|
|
8
|
+
«Por completar» y `{{OPS_DIR}}planning/roadmap/` sin épicas—: reescribir un contexto que alguien ya
|
|
9
|
+
corrigió no deja rastro de lo que se perdió.
|
|
10
|
+
|
|
11
|
+
El inventario no lo hagas a mano: `node {{OPS_DIR}}tools/ops.js scan --json` devuelve los subproyectos con
|
|
12
|
+
manifiesto propio, su runtime y los comandos que cada uno declara, con el archivo del que salieron.
|
|
13
|
+
Recorrer directorios es determinista y cuesta milisegundos; explorarlo vos cuesta minutos y encuentra lo
|
|
14
|
+
mismo. No corras ningún comando del proyecto: el mapa dice lo que está declarado y de dónde, y
|
|
15
|
+
verificarlo corriéndolo es una historia de la épica, con dueño y tiempo asignado.
|
|
16
|
+
|
|
17
|
+
Con eso escribí `{{OPS_DIR}}organization/company.md` y `product.md`, la sección «Mapa real» de
|
|
18
|
+
`{{OPS_DIR}}AGENTS.md` con el resultado que obtuviste por comando, y las raíces reales en
|
|
19
|
+
`workspaceRoots` de `{{OPS_DIR}}ops.config.json`, que es lo que un guard usa para bloquear una escritura
|
|
20
|
+
fuera de lugar. Lo deducido va marcado `(supuesto)` y lo que nada sostiene queda «Por definir»: no
|
|
21
|
+
inventes clientes, ingresos ni objetivos.
|
|
22
|
+
|
|
23
|
+
Credenciales, MCP y el permiso de push no te corresponden. Cada uno va como fila en
|
|
24
|
+
`{{OPS_DIR}}planning/HUMAN_ACTIONS.md` con la acción concreta que lo desbloquea y sin proponer ningún
|
|
25
|
+
valor; las preguntas abiertas, a la sección Ideas de `{{OPS_DIR}}planning/INBOX.md`.
|
|
26
|
+
|
|
27
|
+
Cerrá escribiendo `epic-001` en `{{OPS_DIR}}planning/roadmap/`: su resultado es que una tarea pueda
|
|
28
|
+
atravesar el ciclo entero, y sus criterios salen de lo que hoy falta —contexto sin supuestos, cada
|
|
29
|
+
comando en verde, el guard de límites probado en las dos direcciones, una tarea piloto en DONE—. Validá
|
|
30
|
+
con `node {{OPS_DIR}}tools/ops.js check planning`. **Nunca promuevas al BACKLOG**: esa firma es humana.
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
@{{OPS_DIR}}planning/rules/system/commits.md
|
|
8
8
|
@{{OPS_DIR}}planning/rules/system/conduct.md
|
|
9
9
|
|
|
10
|
-
Los hooks de `.claude/settings.json` son obligatorios.
|
|
11
|
-
y
|
|
10
|
+
Los hooks de `.claude/settings.json` son obligatorios. En una instancia recién creada, `/onboard` escanea el
|
|
11
|
+
repositorio y deja escrito el contexto de la empresa y la primera épica. Después, `/team` evalúa si una
|
|
12
|
+
intención es viable y propone una épica, y `/autobuild` ejecuta trabajo ya promovido; `/integration-sync` e
|
|
12
13
|
`/integration-promote` gestionan staging local sin escritura remota. Ninguno promueve al BACKLOG.
|
|
13
14
|
|
|
14
15
|
Antes de iniciar, respeta `{{OPS_DIR}}planning/AWAITING_REVIEW.md` y el mutex de `{{OPS_DIR}}planning/WIP.md`. Si el protocolo y
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"source": "../../workflows/team.js",
|
|
30
30
|
"target": ".claude/workflows/team.js"
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"source": "../../workflows/onboard.js",
|
|
34
|
+
"target": ".claude/workflows/onboard.js"
|
|
35
|
+
},
|
|
32
36
|
{
|
|
33
37
|
"source": "../../workflows/agent-eval.js",
|
|
34
38
|
"target": ".claude/workflows/agent-eval.js"
|
|
@@ -31,6 +31,20 @@ node {{OPS_DIR}}tools/ops.js agents list
|
|
|
31
31
|
Leé el `SKILL.md` del cargo que corresponda antes de actuar en su terreno, y respetá sus límites. Lo que
|
|
32
32
|
ese cargo debe saber de esta empresa está en `{{OPS_DIR}}organization/roles/<slug>.md`.
|
|
33
33
|
|
|
34
|
+
## El arranque
|
|
35
|
+
|
|
36
|
+
En una instancia recién creada nadie le explicó todavía al toolkit qué es este proyecto:
|
|
37
|
+
`{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una vez: reescribir un contexto
|
|
38
|
+
que alguien ya corrigió no deja rastro de lo que se perdió.
|
|
39
|
+
|
|
40
|
+
Empezá por `node {{OPS_DIR}}tools/ops.js scan --json`, que devuelve los subproyectos con manifiesto propio
|
|
41
|
+
y los comandos que cada uno declara: recorrer el árbol vos mismo cuesta minutos y encuentra lo mismo. Con
|
|
42
|
+
eso escribí `{{OPS_DIR}}organization/`, la sección «Mapa real» de `{{OPS_DIR}}AGENTS.md` con cada comando
|
|
43
|
+
tal como está declarado y de qué archivo salió —sin correrlo—, y las raíces reales en `workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
|
|
44
|
+
filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
|
|
45
|
+
`{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
|
|
46
|
+
lo que falta. Nunca la promuevas.
|
|
47
|
+
|
|
34
48
|
## Los equipos
|
|
35
49
|
|
|
36
50
|
Un equipo es una secuencia de cargos con etapas y exit gates, para evaluar una intención antes de que
|
|
@@ -24,6 +24,20 @@ node {{OPS_DIR}}tools/ops.js agents list
|
|
|
24
24
|
node {{OPS_DIR}}tools/ops.js team list
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
## El arranque
|
|
28
|
+
|
|
29
|
+
En una instancia recién creada nadie le explicó todavía al toolkit qué es este proyecto:
|
|
30
|
+
`{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una vez: reescribir un contexto
|
|
31
|
+
que alguien ya corrigió no deja rastro de lo que se perdió.
|
|
32
|
+
|
|
33
|
+
Empezá por `node {{OPS_DIR}}tools/ops.js scan --json`, que devuelve los subproyectos con manifiesto propio
|
|
34
|
+
y los comandos que cada uno declara: recorrer el árbol vos mismo cuesta minutos y encuentra lo mismo. Con
|
|
35
|
+
eso escribí `{{OPS_DIR}}organization/`, la sección «Mapa real» de `{{OPS_DIR}}AGENTS.md` con cada comando
|
|
36
|
+
tal como está declarado y de qué archivo salió —sin correrlo—, y las raíces reales en `workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
|
|
37
|
+
filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
|
|
38
|
+
`{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
|
|
39
|
+
lo que falta. Nunca la promuevas.
|
|
40
|
+
|
|
27
41
|
Nunca omitas aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones
|
|
28
42
|
trabajo desde INBOX.
|
|
29
43
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Arranque de una instancia recién creada: convierte un repositorio que nadie le explicó al toolkit en
|
|
2
|
+
// contexto escrito —`organization/`, el mapa real de `AGENTS.md`, las raíces de código— y en la primera
|
|
3
|
+
// épica.
|
|
4
|
+
//
|
|
5
|
+
// El orden importa y se pagó caro: la primera versión le pedía a un agente que «inventariara el
|
|
6
|
+
// repositorio», y en una carpeta vacía eso gastó doce minutos para no encontrar nada. Recorrer el árbol
|
|
7
|
+
// es determinista y lo hace `ops scan` en milisegundos; el modelo entra después, y sólo si hay algo
|
|
8
|
+
// sobre lo que decidir. Cuando no lo hay, este recorrido termina en una llamada.
|
|
9
|
+
//
|
|
10
|
+
// Escribe borradores y no decide por nadie: lo deducido queda marcado como supuesto, las credenciales y
|
|
11
|
+
// los sistemas externos van a HUMAN_ACTIONS —R12 se los prohíbe a un runner— y la épica queda sin
|
|
12
|
+
// promover, que sigue siendo la firma humana.
|
|
13
|
+
export const meta = {
|
|
14
|
+
name: 'onboard',
|
|
15
|
+
description: 'Inventaría el workspace y deja escrito el contexto de la empresa y la primera épica',
|
|
16
|
+
whenToUse: 'Primera corrida después de "cauce init", cuando organization/ y el roadmap están vacíos.',
|
|
17
|
+
phases: [
|
|
18
|
+
{ title: 'Scan', detail: 'Inventario determinista: ops scan, sin modelo recorriendo nada' },
|
|
19
|
+
{ title: 'Draft', detail: 'organization/, mapa real y raíces de código' },
|
|
20
|
+
{ title: 'Human', detail: 'Credenciales, MCP y permisos: lo que no le toca al runner' },
|
|
21
|
+
{ title: 'Epic', detail: 'La épica que deja al ciclo poder correr' },
|
|
22
|
+
{ title: 'Closing', detail: 'check y lo que queda esperando a una persona' },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// El prefijo lo completa `automation install`: el runtime no expone `process`, así que la ruta de la
|
|
27
|
+
// raíz ops viaja escrita, relativa a donde se abre la herramienta.
|
|
28
|
+
const ROOT = '{{OPS_DIR}}'.replace(/\/+$/, '') || '.'
|
|
29
|
+
const P = `${ROOT}/planning`
|
|
30
|
+
const ORG = `${ROOT}/organization`
|
|
31
|
+
const HUMAN = `${P}/HUMAN_ACTIONS.md`
|
|
32
|
+
const INBOX = `${P}/INBOX.md`
|
|
33
|
+
const ROADMAP = `${P}/roadmap`
|
|
34
|
+
|
|
35
|
+
// Lo que la persona ya sabe y el repositorio no puede decir: `/onboard vendemos ruteo a PYMEs de
|
|
36
|
+
// logística`. Entra como hecho; lo que se deduce, no.
|
|
37
|
+
const input = typeof args === 'string' ? { context: args } : (args || {})
|
|
38
|
+
const CONTEXT = String(input.context || '').trim()
|
|
39
|
+
const FORCE = Boolean(input.force)
|
|
40
|
+
|
|
41
|
+
const BASE = `Nunca inventes clientes, métricas, ingresos, plazos ni responsables. Distinguí lo que leíste ` +
|
|
42
|
+
`en un archivo de lo que estás suponiendo: lo segundo va marcado "(supuesto)" en el texto que escribas. ` +
|
|
43
|
+
`No leas archivos de credenciales —.env, *.pem, claves— ni copies su contenido a ningún lado; ` +
|
|
44
|
+
`.env.example sí, y sólo los nombres de las variables. No corras comandos del proyecto: este recorrido ` +
|
|
45
|
+
`no ejecuta nada, sólo lee. No escribas en ningún sistema externo y no promuevas trabajo al BACKLOG.`
|
|
46
|
+
|
|
47
|
+
function finish(result) {
|
|
48
|
+
log(`Fin: ${JSON.stringify(result)}`)
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stop = (reason, detail = '') => {
|
|
53
|
+
log(`Checkpoint: ${reason}${detail ? ` — ${detail}` : ''}`)
|
|
54
|
+
return finish({ stopped: true, reason, detail })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SCAN = {
|
|
58
|
+
type: 'object', additionalProperties: false, required: ['fresh', 'services'],
|
|
59
|
+
properties: {
|
|
60
|
+
fresh: { type: 'boolean' },
|
|
61
|
+
reason: { type: 'string' },
|
|
62
|
+
services: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
63
|
+
required: ['path'], properties: {
|
|
64
|
+
path: { type: 'string' },
|
|
65
|
+
runtimes: { type: 'array', items: { type: 'string' } },
|
|
66
|
+
// Comando declarado y de qué archivo salió. Verificar que además corra es una historia de la
|
|
67
|
+
// épica: correr la suite de cada servicio acá convertía el arranque en una espera larga.
|
|
68
|
+
commands: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
69
|
+
required: ['kind', 'command', 'source'], properties: {
|
|
70
|
+
kind: { type: 'string' }, command: { type: 'string' }, source: { type: 'string' },
|
|
71
|
+
} } },
|
|
72
|
+
} } },
|
|
73
|
+
externals: { type: 'array', items: { type: 'string' } },
|
|
74
|
+
secrets: { type: 'array', items: { type: 'string' } },
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const WRITTEN = {
|
|
79
|
+
type: 'object', additionalProperties: false, required: ['files'],
|
|
80
|
+
properties: {
|
|
81
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
82
|
+
assumptions: { type: 'array', items: { type: 'string' } },
|
|
83
|
+
humanActions: { type: 'array', items: { type: 'string' } },
|
|
84
|
+
openQuestions: { type: 'array', items: { type: 'string' } },
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
phase('Scan')
|
|
89
|
+
|
|
90
|
+
// Una sola llamada, y todo lo que hace es correr dos comandos y mirar dos archivos. Lo que sigue depende
|
|
91
|
+
// de lo que devuelva, así que gastar más antes de saberlo es gastar a ciegas.
|
|
92
|
+
const state = await agent(
|
|
93
|
+
`${BASE}\n\nFrom ${ROOT}, run exactly these two commands and report what they printed. Explore nothing ` +
|
|
94
|
+
`else and open no other file than the two named below.\n` +
|
|
95
|
+
`1. "node tools/ops.js scan --json": the workspace inventory. Copy each service with its path, its ` +
|
|
96
|
+
`runtimes and its declared commands, keeping the source file each command came from. Add nothing that ` +
|
|
97
|
+
`the command did not print.\n` +
|
|
98
|
+
`2. "node tools/ops.js check planning".\n` +
|
|
99
|
+
`Then read ${ORG}/company.md and list ${ROADMAP}. Set fresh=true only if the organization file still ` +
|
|
100
|
+
`carries the mold's "Por completar" placeholders and the roadmap holds nothing but its template and ` +
|
|
101
|
+
`README; otherwise fresh=false and say in reason what is already written. If .env.example exists at ` +
|
|
102
|
+
`the workspace root, report the variable names in secrets —names only— and the external services they ` +
|
|
103
|
+
`point at in externals.`,
|
|
104
|
+
{ schema: SCAN, label: 'inventario' },
|
|
105
|
+
)
|
|
106
|
+
if (!state) return stop('scan-unavailable', 'no se pudo leer el estado del workspace')
|
|
107
|
+
if (!state.fresh && !FORCE) {
|
|
108
|
+
return stop('ya-arrancado', `${state.reason || 'la instancia ya tiene contexto escrito'}. ` +
|
|
109
|
+
`Pasá force:true si querés reescribir los borradores.`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const services = state.services || []
|
|
113
|
+
const listado = services.map((service) => service.path).join(', ')
|
|
114
|
+
log(`${services.length} servicio(s) en el workspace${listado ? `: ${listado}` : ''}`)
|
|
115
|
+
|
|
116
|
+
// Sin código y sin contexto no hay nada que escribir que no sea inventado, y esa es exactamente la
|
|
117
|
+
// corrida que no puede costar nada: se termina acá, diciendo qué falta y cómo darlo.
|
|
118
|
+
if (!services.length && !CONTEXT) {
|
|
119
|
+
return stop('sin-contexto', `no hay servicios en el workspace y no me diste contexto, así que no hay ` +
|
|
120
|
+
`nada que escribir sin inventarlo. Volvé a correrlo cuando estén los repos, o dame el contexto en ` +
|
|
121
|
+
`una línea: /onboard qué vende la empresa, a quién, y cuál es el objetivo del trimestre.`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const INVENTARIO = { services, externals: state.externals || [], secrets: state.secrets || [] }
|
|
125
|
+
const EVIDENCE = `Inventario del workspace:\n${JSON.stringify(INVENTARIO)}` +
|
|
126
|
+
`${CONTEXT ? `\n\nContexto aportado por la persona, que vale como hecho: ${CONTEXT}` : ''}` +
|
|
127
|
+
`${services.length ? '' : '\n\nNo hay ningún servicio en el workspace: el código todavía no está acá.'}`
|
|
128
|
+
|
|
129
|
+
phase('Draft')
|
|
130
|
+
|
|
131
|
+
const drafted = await agent(
|
|
132
|
+
`${BASE}\n\n${EVIDENCE}\n\nEscribí los borradores de contexto de esta instancia, reemplazando el molde ` +
|
|
133
|
+
`en vez de comentarlo:\n` +
|
|
134
|
+
`1. ${ORG}/company.md y ${ORG}/product.md: lo que el contexto aportado y los nombres del repositorio ` +
|
|
135
|
+
`permiten afirmar. Lo que nada sostiene queda "Por definir" y su pregunta va a openQuestions.\n` +
|
|
136
|
+
`2. La sección "## Mapa real" de ${ROOT}/AGENTS.md: una entrada por servicio con su ruta, su runtime y ` +
|
|
137
|
+
`sus comandos **tal como los declara**, diciendo de qué archivo salió cada uno. No afirmes que ` +
|
|
138
|
+
`funcionan: nadie los corrió. Un servicio sin comandos declarados se escribe así, que es información.\n` +
|
|
139
|
+
`3. ${ROOT}/ops.config.json: dejá en workspaceRoots las raíces de código reales, que es lo que un guard ` +
|
|
140
|
+
`usa para bloquear una escritura fuera de lugar. Una raíz de más lo apaga.\n` +
|
|
141
|
+
`${services.length ? '' : 'Sin servicios, el mapa queda declarado como pendiente, diciendo qué lo ' +
|
|
142
|
+
'completa.\n'}` +
|
|
143
|
+
`Devolvé en files cada archivo que tocaste y en assumptions cada supuesto que dejaste marcado.`,
|
|
144
|
+
{ schema: WRITTEN, label: 'contexto' },
|
|
145
|
+
)
|
|
146
|
+
if (!drafted) return stop('draft-unavailable', 'los borradores no devolvieron resultado')
|
|
147
|
+
|
|
148
|
+
phase('Human')
|
|
149
|
+
|
|
150
|
+
// Credenciales, MCP y permiso de push no son trabajo del runner: R12 se los prohíbe y R13 exige dejar
|
|
151
|
+
// dicho quién los resuelve y con qué. La fila vale más que la negativa.
|
|
152
|
+
const pending = await agent(
|
|
153
|
+
`${BASE}\n\n${EVIDENCE}\n\nRegistrá en ${HUMAN} una fila por cada cosa que necesita a una persona, con ` +
|
|
154
|
+
`la tarea, el estado pendiente, el origen "onboard" y la acción concreta que la desbloquea. Como ` +
|
|
155
|
+
`mínimo: una por cada credencial que el proyecto espera —dónde se cargan y quién lo hace, sin proponer ` +
|
|
156
|
+
`ningún valor—, una por cada servicio externo o MCP a conectar —con su alcance y contra qué entorno— y ` +
|
|
157
|
+
`una por la autoridad del runner, que hoy declara runner.allowPush=false en ops.config.json. Las ` +
|
|
158
|
+
`preguntas abiertas van a la sección Ideas de ${INBOX}, sin promover: ` +
|
|
159
|
+
`${JSON.stringify(drafted.openQuestions || [])}`,
|
|
160
|
+
{ schema: WRITTEN, label: 'acciones-humanas' },
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
phase('Epic')
|
|
164
|
+
|
|
165
|
+
const epic = await agent(
|
|
166
|
+
`${BASE}\n\n${EVIDENCE}\n\nSupuestos que quedaron escritos: ${JSON.stringify(drafted.assumptions || [])}\n\n` +
|
|
167
|
+
`Escribí en ${ROADMAP} la épica epic-001-<slug>.md siguiendo el contrato de ${P}/PROTOCOL.md: ` +
|
|
168
|
+
`frontmatter epic/title/status/service con status open, criterios **CN** observables, "## Contexto ` +
|
|
169
|
+
`relevante" con rutas reales e historias con (→ CN) y (service: ruta), cada una de menos de cuatro ` +
|
|
170
|
+
`horas.\n\n` +
|
|
171
|
+
`Su resultado es que una tarea pueda atravesar el ciclo entero sin que nadie tenga que volver a ` +
|
|
172
|
+
`explicar este proyecto. Los criterios salen de lo que hoy falta y son verificables: que organization/ ` +
|
|
173
|
+
`no tenga supuestos sin confirmar, que cada comando del mapa esté verificado corriéndolo y anotado con ` +
|
|
174
|
+
`su resultado, que las raíces declaradas hagan que el guard de límites bloquee una escritura afuera y ` +
|
|
175
|
+
`deje pasar una adentro, y que una tarea piloto real llegue a DONE con evidencia. ` +
|
|
176
|
+
`${services.length
|
|
177
|
+
? 'Verificar los comandos es una historia: nadie los corrió todavía.'
|
|
178
|
+
: 'La primera historia es traer los repos y declararlos en workspaceRoots.'} ` +
|
|
179
|
+
`En "## Riesgos y decisiones humanas" citá las filas que quedaron en HUMAN_ACTIONS. No toques BACKLOG.md.`,
|
|
180
|
+
{ schema: { type: 'object', additionalProperties: false, required: ['file'],
|
|
181
|
+
properties: {
|
|
182
|
+
file: { type: 'string' }, title: { type: 'string' },
|
|
183
|
+
criteria: { type: 'array', items: { type: 'string' } },
|
|
184
|
+
stories: { type: 'array', items: { type: 'string' } },
|
|
185
|
+
} }, label: 'epica-001' },
|
|
186
|
+
)
|
|
187
|
+
if (!epic) return stop('epic-unavailable', 'la épica no devolvió resultado')
|
|
188
|
+
|
|
189
|
+
phase('Closing')
|
|
190
|
+
|
|
191
|
+
const closing = await agent(
|
|
192
|
+
`${BASE}\n\nFrom ${ROOT}, run "node tools/ops.js check planning" and report whether it passed. If it ` +
|
|
193
|
+
`failed, repair only what this run wrote —the epic, the config— so it satisfies the contract; never ` +
|
|
194
|
+
`weaken a criterion to force green.`,
|
|
195
|
+
{ schema: { type: 'object', additionalProperties: false, required: ['passed', 'details'],
|
|
196
|
+
properties: { passed: { type: 'boolean' }, details: { type: 'string' } } }, label: 'closing-check' },
|
|
197
|
+
)
|
|
198
|
+
if (!closing || !closing.passed) return stop('check-failed', closing ? closing.details : 'sin resultado')
|
|
199
|
+
|
|
200
|
+
const supuestos = (drafted.assumptions || []).length
|
|
201
|
+
const acciones = ((pending || {}).humanActions || []).length
|
|
202
|
+
log(`Contexto escrito con ${supuestos} supuesto(s) por confirmar y ${acciones} acción(es) humana(s) en ${HUMAN}.`)
|
|
203
|
+
log(`Épica en ${epic.file}, sin promover: revisala, promoví una historia a un hito del BACKLOG y corré /autobuild.`)
|
|
204
|
+
|
|
205
|
+
return finish({
|
|
206
|
+
services: services.length,
|
|
207
|
+
assumptions: supuestos,
|
|
208
|
+
humanActions: acciones,
|
|
209
|
+
epic: epic.file,
|
|
210
|
+
promoted: false,
|
|
211
|
+
})
|
package/engine/cli/args.js
CHANGED
|
@@ -14,6 +14,7 @@ const VALUED_FLAGS = new Set([
|
|
|
14
14
|
// —un agente, típicamente— recibía texto sin ninguna señal de que su bandera no existía.
|
|
15
15
|
const FLAGS = {
|
|
16
16
|
init: ['--name', '--mode', '--force', '--runner', '--integration', '--install', '--no-install'],
|
|
17
|
+
scan: ['--json'],
|
|
17
18
|
check: ['--json'],
|
|
18
19
|
tree: ['--json', '--no-color'],
|
|
19
20
|
context: ['--json'],
|
package/engine/cli/ops.js
CHANGED
|
@@ -14,6 +14,7 @@ const F = require('../core/files')
|
|
|
14
14
|
const O = require('../core/ownership')
|
|
15
15
|
const CL = require('../core/changelog')
|
|
16
16
|
const M = require('../core/manifest')
|
|
17
|
+
const SC = require('../core/scan')
|
|
17
18
|
const C = require('../config/validate')
|
|
18
19
|
const T = require('../teams/registry')
|
|
19
20
|
const AG = require('../agents/catalog')
|
|
@@ -35,6 +36,7 @@ function usage() {
|
|
|
35
36
|
console.log(`Uso:
|
|
36
37
|
ops init [destino] [--name <nombre>] [--mode embedded|sidecar] [--force]
|
|
37
38
|
[--runner claude|codex|gemini|antigravity] [--integration <proveedor>] [--install|--no-install]
|
|
39
|
+
ops scan [workspace] [--json]
|
|
38
40
|
ops check <planning-dir> [--json]
|
|
39
41
|
ops tree <planning-dir> [--no-color] [--json]
|
|
40
42
|
ops context <planning-dir> [--json]
|
|
@@ -226,6 +228,15 @@ function evaluationBench(root, agent, caso, force) {
|
|
|
226
228
|
return dir
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
// Dónde va la instancia cuando nadie eligió destino. Parada frecuente: el dev ya creó `acme-ops/` y
|
|
232
|
+
// corre `init` adentro. Sin esto la instancia caía en `acme-ops/ops/` —una carpeta del toolkit dentro
|
|
233
|
+
// de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
|
|
234
|
+
// instancia; no hay una segunda adentro.
|
|
235
|
+
function implicitTarget(cwd) {
|
|
236
|
+
const base = path.basename(cwd)
|
|
237
|
+
return base === DEFAULT_TARGET || base.endsWith('-ops') ? '.' : DEFAULT_TARGET
|
|
238
|
+
}
|
|
239
|
+
|
|
229
240
|
// El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
|
|
230
241
|
// nombran al toolkit, y quien lee `project` en la configuración espera leer «acme».
|
|
231
242
|
function defaultName(root) {
|
|
@@ -268,12 +279,15 @@ async function init(target, cli) {
|
|
|
268
279
|
// nivel deja de distinguir qué es suyo y qué llegó del toolkit. Es el layout que `automation
|
|
269
280
|
// install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
|
|
270
281
|
// único que faltaba era que fuera lo que pasa cuando no se elige nada.
|
|
271
|
-
const root = path.resolve(target ||
|
|
282
|
+
const root = path.resolve(target || implicitTarget(process.cwd()))
|
|
272
283
|
const mode = cli.value('--mode', target ? 'embedded' : 'sidecar')
|
|
273
284
|
if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
|
|
274
285
|
const name = cli.value('--name', defaultName(root))
|
|
275
286
|
const force = cli.has('--force')
|
|
276
|
-
|
|
287
|
+
// `.git` no cuenta como contenido: es lo único que hay en la carpeta que alguien acaba de crear y
|
|
288
|
+
// versionar para la instancia, y el toolkit no escribe nada adentro. Sin esta excepción el camino
|
|
289
|
+
// más natural —`mkdir acme-ops && git init && cauce init`— pedía `--force` para no pisar nada.
|
|
290
|
+
const existing = (fs.existsSync(root) ? fs.readdirSync(root) : []).filter((entry) => entry !== '.git')
|
|
277
291
|
if (existing.length && !force) {
|
|
278
292
|
fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
|
|
279
293
|
}
|
|
@@ -306,12 +320,48 @@ async function init(target, cli) {
|
|
|
306
320
|
|
|
307
321
|
if (resultado.instalado) {
|
|
308
322
|
check(path.join(root, 'planning'), SIN_BANDERAS)
|
|
323
|
+
// Una instancia recién instalada funciona y no sabe nada de este proyecto: `organization/` es el
|
|
324
|
+
// molde y el roadmap está vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que
|
|
325
|
+
// es justo lo que un CLI determinista no puede hacer; el recorrido vive en el runner, así que lo
|
|
326
|
+
// único útil acá es decir cuál es y con qué se abre.
|
|
327
|
+
if (resultado.runner !== BOOT.SIN_RUNNER) {
|
|
328
|
+
console.log(` siguiente: abrí ${resultado.runner} en este directorio y corré /onboard`)
|
|
329
|
+
console.log(' escanea el repositorio y deja escrito el contexto de la empresa y la primera épica')
|
|
330
|
+
}
|
|
309
331
|
console.log(` listo: el ciclo empieza en ${path.join(relative || '.', 'planning', 'FLOW.md')}`)
|
|
310
332
|
}
|
|
311
333
|
for (const paso of initSteps(enter, resultado)) console.log(paso)
|
|
312
334
|
if (resultado.error) fail(`${resultado.error}: la instancia quedó creada pero todavía no funciona.`)
|
|
313
335
|
}
|
|
314
336
|
|
|
337
|
+
// Qué hay en el workspace, antes de que nadie razone sobre él. La raíz ops se saltea: no es un servicio
|
|
338
|
+
// del proyecto, y su `package.json` sólo declara el motor.
|
|
339
|
+
function scan(target, cli) {
|
|
340
|
+
// Sólo el sidecar tiene su workspace afuera: la instancia es hija de la carpeta donde vive el código.
|
|
341
|
+
// Cualquier otro modo —embedded, y este mismo repositorio, que es `toolkit`— escanea donde está
|
|
342
|
+
// parado. Confundirlos hacía que un `scan` sin argumentos se fuera a recorrer la carpeta de al lado.
|
|
343
|
+
const sidecar = O.mode(process.cwd()) === 'sidecar'
|
|
344
|
+
const root = path.resolve(target || (sidecar ? path.join(process.cwd(), '..') : '.'))
|
|
345
|
+
const result = SC.scan(root, sidecar ? process.cwd() : '')
|
|
346
|
+
if (cli.has('--json')) return console.log(JSON.stringify(result, null, 2))
|
|
347
|
+
console.log(`workspace ${result.root}`)
|
|
348
|
+
if (result.rootManifests.length) {
|
|
349
|
+
console.log(`. ${result.rootManifests.join(', ')}${comandos(result.rootCommands)}`)
|
|
350
|
+
}
|
|
351
|
+
for (const service of result.services) {
|
|
352
|
+
console.log(`${service.path} [${service.runtimes.join(', ')}]${comandos(service.commands)}`)
|
|
353
|
+
}
|
|
354
|
+
const total = result.services.length + (result.rootManifests.length ? 1 : 0)
|
|
355
|
+
console.log(`${total} candidato(s). Cuál es el producto y cuál quedó muerto lo decide una persona.`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Sólo lo declarado y de dónde salió: un comando inventado se lee igual que uno real.
|
|
359
|
+
function comandos(commands) {
|
|
360
|
+
const entries = Object.entries(commands || {})
|
|
361
|
+
if (!entries.length) return ' — sin comandos declarados'
|
|
362
|
+
return ` — ${entries.map(([kind, value]) => `${kind}: ${value.command} (${value.source})`).join(', ')}`
|
|
363
|
+
}
|
|
364
|
+
|
|
315
365
|
function check(dir, cli) {
|
|
316
366
|
const root = path.resolve(dir || '.')
|
|
317
367
|
const errors = []
|
|
@@ -1118,6 +1168,7 @@ async function run(cli) {
|
|
|
1118
1168
|
}
|
|
1119
1169
|
const arg = cli.positional
|
|
1120
1170
|
if (command === 'init') await init(arg[1], cli)
|
|
1171
|
+
else if (command === 'scan') scan(arg[1], cli)
|
|
1121
1172
|
else if (command === 'check') check(arg[1], cli)
|
|
1122
1173
|
else if (command === 'tree') tree(arg[1], cli)
|
|
1123
1174
|
else if (command === 'context') context(arg[1], cli)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Qué hay en el workspace, resuelto por código y no por un modelo. Existe porque el arranque empezaba
|
|
4
|
+
// pidiéndole a un agente que «inventariara el repositorio»: en una carpeta vacía eso gastó doce minutos
|
|
5
|
+
// para no encontrar nada. Recorrer directorios y leer manifiestos es determinista, y lo que no lo es
|
|
6
|
+
// —qué de todo esto es el producto, qué está muerto— recién vale la pena preguntárselo a un modelo
|
|
7
|
+
// cuando esta lista existe.
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs')
|
|
10
|
+
const path = require('node:path')
|
|
11
|
+
|
|
12
|
+
// Lo que nunca es un servicio del proyecto. `node_modules` es el que hace la diferencia entre
|
|
13
|
+
// milisegundos y minutos: adentro hay un manifiesto por dependencia. Los directorios ocultos se saltean
|
|
14
|
+
// enteros —regla, no lista—: ahí viven la configuración del runner, el banco de evaluación y las cachés,
|
|
15
|
+
// y un servicio del producto no se esconde detrás de un punto.
|
|
16
|
+
const IGNORED = new Set([
|
|
17
|
+
'node_modules', 'vendor', 'dist', 'build', 'target', 'out', 'coverage', 'venv', '__pycache__', 'tmp',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
const skippable = (name) => name.startsWith('.') || IGNORED.has(name)
|
|
21
|
+
|
|
22
|
+
// Un servicio anidado más hondo que esto es una excepción, y recorrer el árbol entero para encontrarlo
|
|
23
|
+
// cuesta más que declararlo a mano en `AGENTS.md`.
|
|
24
|
+
const DEPTH = 3
|
|
25
|
+
|
|
26
|
+
const MANIFESTS = [
|
|
27
|
+
{ file: 'package.json', runtime: 'node' },
|
|
28
|
+
{ file: 'go.mod', runtime: 'go' },
|
|
29
|
+
{ file: 'pyproject.toml', runtime: 'python' },
|
|
30
|
+
{ file: 'requirements.txt', runtime: 'python' },
|
|
31
|
+
{ file: 'Cargo.toml', runtime: 'rust' },
|
|
32
|
+
{ file: 'composer.json', runtime: 'php' },
|
|
33
|
+
{ file: 'pom.xml', runtime: 'java' },
|
|
34
|
+
{ file: 'build.gradle', runtime: 'java' },
|
|
35
|
+
{ file: 'Gemfile', runtime: 'ruby' },
|
|
36
|
+
{ file: 'Makefile', runtime: 'make' },
|
|
37
|
+
{ file: 'docker-compose.yml', runtime: 'compose' },
|
|
38
|
+
{ file: 'docker-compose.yaml', runtime: 'compose' },
|
|
39
|
+
{ file: 'Dockerfile', runtime: 'docker' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
// Sólo lo declarado, con su archivo: un comando inventado se lee igual que uno real, y el primer Verify
|
|
43
|
+
// de una tarea es donde se descubre que no existe.
|
|
44
|
+
function npmScripts(file) {
|
|
45
|
+
try {
|
|
46
|
+
const scripts = JSON.parse(fs.readFileSync(file, 'utf8')).scripts || {}
|
|
47
|
+
return ['test', 'lint', 'build'].reduce((found, key) => (
|
|
48
|
+
scripts[key] ? { ...found, [key]: { command: `npm run ${key}`, source: 'package.json' } } : found
|
|
49
|
+
), {})
|
|
50
|
+
} catch { return {} }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeTargets(file) {
|
|
54
|
+
try {
|
|
55
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
56
|
+
return ['test', 'lint', 'build'].reduce((found, key) => (
|
|
57
|
+
new RegExp(`^${key}:`, 'm').test(text)
|
|
58
|
+
? { ...found, [key]: { command: `make ${key}`, source: 'Makefile' } }
|
|
59
|
+
: found
|
|
60
|
+
), {})
|
|
61
|
+
} catch { return {} }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function commandsOf(dir) {
|
|
65
|
+
const packageJson = path.join(dir, 'package.json')
|
|
66
|
+
const makefile = path.join(dir, 'Makefile')
|
|
67
|
+
return {
|
|
68
|
+
// El Makefile gana sobre los scripts cuando los dos existen: el que envuelve al otro es el que el
|
|
69
|
+
// proyecto quiere que se corra.
|
|
70
|
+
...(fs.existsSync(packageJson) ? npmScripts(packageJson) : {}),
|
|
71
|
+
...(fs.existsSync(makefile) ? makeTargets(makefile) : {}),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function manifestsOf(dir) {
|
|
76
|
+
return MANIFESTS.filter((entry) => fs.existsSync(path.join(dir, entry.file)))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Servicios candidatos bajo `root`, sin entrar en `skip` —típicamente la raíz ops, que no es un
|
|
80
|
+
// servicio del proyecto—. El resultado es una lista, no un veredicto: decidir cuál es el producto y
|
|
81
|
+
// cuál quedó muerto sigue siendo trabajo de una persona o de un cargo.
|
|
82
|
+
function services(root, skip = '') {
|
|
83
|
+
const found = []
|
|
84
|
+
const excluded = skip ? path.resolve(skip) : ''
|
|
85
|
+
const walk = (dir, depth) => {
|
|
86
|
+
const manifests = manifestsOf(dir)
|
|
87
|
+
if (manifests.length && path.resolve(dir) !== path.resolve(root)) {
|
|
88
|
+
found.push({
|
|
89
|
+
path: path.relative(root, dir).split(path.sep).join('/'),
|
|
90
|
+
runtimes: manifests.map((entry) => entry.runtime),
|
|
91
|
+
manifests: manifests.map((entry) => entry.file),
|
|
92
|
+
commands: commandsOf(dir),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
if (depth >= DEPTH) return
|
|
96
|
+
let entries = []
|
|
97
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (!entry.isDirectory() || skippable(entry.name)) continue
|
|
100
|
+
const child = path.join(dir, entry.name)
|
|
101
|
+
if (excluded && path.resolve(child) === excluded) continue
|
|
102
|
+
walk(child, depth + 1)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
walk(root, 0)
|
|
106
|
+
return found
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// El primer nivel del workspace también puede ser un solo proyecto sin subcarpetas: se reporta aparte
|
|
110
|
+
// para no confundir «un servicio en la raíz» con «no hay nada».
|
|
111
|
+
function scan(root, skip = '') {
|
|
112
|
+
return {
|
|
113
|
+
root: path.resolve(root),
|
|
114
|
+
rootManifests: manifestsOf(root).map((entry) => entry.file),
|
|
115
|
+
rootCommands: commandsOf(root),
|
|
116
|
+
services: services(root, skip),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { scan, services, IGNORED, DEPTH }
|