@ingeniomaps/cauce 0.54.0 → 0.56.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 +61 -0
- package/README.md +4 -2
- package/engine/automation/config.js +43 -11
- package/engine/automation/index.js +19 -2
- package/engine/cli/dependency.js +73 -0
- package/engine/cli/instance.js +53 -51
- package/engine/cli/ops.js +14 -3
- package/package.json +1 -1
- package/template/Makefile +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,67 @@ 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.56.0] - 2026-09-03
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **El upgrade que documentaba el README rompía el pin exacto que puso `init`.** La razón para no
|
|
22
|
+
saltear su primer paso es que «`init` fija la versión exacta, así que `npm update` no la mueve», y
|
|
23
|
+
el comando de al lado la desarmaba: npm guarda con caret por defecto, así que
|
|
24
|
+
`npm install --save-dev @ingeniomaps/cauce@latest` dejaba `^0.55.0`, y dentro de `0.x` ese caret
|
|
25
|
+
alcanza a los patches —que Cauce publica—. Un `npm install` en otra máquina podía dejar el motor en
|
|
26
|
+
una versión que `ops.config.json` no decía.
|
|
27
|
+
|
|
28
|
+
Se arregla por los dos lados. El comando lleva `--save-exact` en los tres lugares que lo dictan —el
|
|
29
|
+
README, el `make upgrade` de tu instancia y la salida de `upgrade --check`—, y además **`upgrade`
|
|
30
|
+
repone la versión exacta al terminar**, así que una instancia que ya quedó con un rango se repara
|
|
31
|
+
sola en la próxima actualización. **Qué cambia para vos**: `upgrade` puede tocar ahora una línea de
|
|
32
|
+
tu `package.json`, y lo dice cuando lo hace. El resto del manifiesto queda como estaba, y una
|
|
33
|
+
instancia sin `package.json` no recibe uno.
|
|
34
|
+
|
|
35
|
+
## [0.55.0] - 2026-09-02
|
|
36
|
+
|
|
37
|
+
### Corregido
|
|
38
|
+
|
|
39
|
+
- **`upgrade` ya no pisa lo que `init --force` conservó.** Adoptar Cauce en un repositorio que ya tiene
|
|
40
|
+
contenido es `init --force` y después `upgrade`. El primero conserva tus archivos y lo cumple; el
|
|
41
|
+
segundo los reemplazaba sin avisar e imprimía que no había tocado nada propio. El registro se grababa
|
|
42
|
+
hasheando el disco, así que un archivo conservado quedaba anotado como entregado por Cauce con **tu**
|
|
43
|
+
contenido, y la comparación no veía ninguna diferencia. Ahora se anota el digest de lo que el molde
|
|
44
|
+
habría escrito: tu archivo se ve como lo que es —una edición local— y `upgrade` se detiene
|
|
45
|
+
nombrándolo. **Qué cambia para vos**: el primer `upgrade` después de una adopción con `--force` se
|
|
46
|
+
detiene listando esos archivos. Resolvelos a mano, o repetí con `--force` para descartarlos, que
|
|
47
|
+
ahora además los enumera en la salida.
|
|
48
|
+
- **Tu guard propio sobrevive a `automation install`.** `AGENTS.md` te dice que lo pongas en
|
|
49
|
+
`automatization/hooks/guard-<nombre>.sh` y promete que sobrevive a cada actualización. Sobrevivía al
|
|
50
|
+
`upgrade` y no a la reinstalación del runner: lo nuestro se reconocía por la carpeta, así que
|
|
51
|
+
cualquier `guard-*.sh` de ahí se desregistraba antes de fusionar. El archivo quedaba en disco, el
|
|
52
|
+
guard dejaba de correr y lo único que se veía era una línea diciendo que se había quitado una entrada
|
|
53
|
+
obsoleta. Ahora se reconoce por lo que efectivamente entregamos: los guards que el motor trae, más
|
|
54
|
+
los que esta instancia anotó haber recibido —el manifiesto gana una entrada por runner con el wiring
|
|
55
|
+
que dejó puesto, y por eso una entrada nuestra se sigue retirando el día que el motor deje de traer
|
|
56
|
+
ese guard—. **Si moviste tu guard fuera de esa carpeta para sortearlo, ya podés devolverlo.**
|
|
57
|
+
- **`automation doctor` deja de llamar divergente a una configuración completa.** Un hook propio sumado
|
|
58
|
+
*dentro* de un grupo que escribió el toolkit dejaba a `doctor` en rojo aunque estuvieran todas las
|
|
59
|
+
entradas esperadas: comparaba el grupo entero serializado, así que uno con un hook de más contaba
|
|
60
|
+
como ausente. Ahora compara por `matcher` y contenido. Lo que falta se sigue reportando igual.
|
|
61
|
+
- **`upgrade --check` avisa de lo que tenés editado aunque la versión coincida.** Salía por un camino
|
|
62
|
+
corto que informaba «al día» sin llegar a listarlo, que es el estado normal entre actualizaciones
|
|
63
|
+
—`init` fija la versión exacta—: el único modo que no toca nada era también el único que no podía
|
|
64
|
+
anticipar el conflicto. **Qué cambia para vos**: `--check` ahora sale 1 cuando hay archivos del
|
|
65
|
+
sistema editados, un caso donde antes salía 0. Si lo tenés en un script, revisá esa condición.
|
|
66
|
+
|
|
67
|
+
### Cambiado
|
|
68
|
+
|
|
69
|
+
- **`init` e `init .` eligen el mismo modo en el mismo directorio.** El default salía de si escribiste
|
|
70
|
+
el argumento, no de a dónde apunta: en `acme-ops/`, `init` daba `sidecar` e `init .` daba `embedded`.
|
|
71
|
+
No es cosmético — en `embedded` la raíz del workspace pasa a ser la carpeta ops misma, así que
|
|
72
|
+
`guard-workspace-boundary` deja de reconocer los repos hermanos y el wiring del runner se escribe
|
|
73
|
+
adentro en vez de al lado, donde abrís tu herramienta. Ahora lo decide el destino resuelto, con la
|
|
74
|
+
misma regla que ya elegía dónde aterrizar: una carpeta llamada `ops` o terminada en `-ops` **es** la
|
|
75
|
+
instancia. **Qué cambia para vos**: `init .` en una carpeta así ahora da `sidecar`, donde antes daba
|
|
76
|
+
`embedded`. El caso embebido sigue disponible pidiéndolo: `init . --mode embedded`.
|
|
77
|
+
|
|
17
78
|
## [0.54.0] - 2026-09-02
|
|
18
79
|
|
|
19
80
|
### Agregado
|
package/README.md
CHANGED
|
@@ -266,13 +266,15 @@ existente detiene el `upgrade` antes de pisarlo.
|
|
|
266
266
|
Son tres pasos y `make upgrade` hace los dos primeros:
|
|
267
267
|
|
|
268
268
|
```bash
|
|
269
|
-
npm install --save-dev @ingeniomaps/cauce@latest # trae el motor nuevo
|
|
269
|
+
npm install --save-exact --save-dev @ingeniomaps/cauce@latest # trae el motor nuevo
|
|
270
270
|
node tools/ops.js upgrade . # aplica system/ y el runtime
|
|
271
271
|
node tools/ops.js automation install . claude # el wiring del runner
|
|
272
272
|
```
|
|
273
273
|
|
|
274
274
|
El primero no se puede saltear: `init` fija la versión exacta, así que `npm update` no la mueve y
|
|
275
|
-
`upgrade` compara contra el motor instalado —lo dice en su salida—.
|
|
275
|
+
`upgrade` compara contra el motor instalado —lo dice en su salida—. Va con `--save-exact` porque npm
|
|
276
|
+
guarda con caret por defecto, y ese caret es justamente lo que volvería falsa la frase anterior; si
|
|
277
|
+
alguna vez se instaló sin él, `upgrade` repone la versión exacta al terminar. El tercero tampoco: los workflows y
|
|
276
278
|
las skills viven en el runner, no en la instancia, y `upgrade` no los toca. `upgrade` lo recuerda al
|
|
277
279
|
terminar.
|
|
278
280
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const fs = require('node:fs')
|
|
9
9
|
const path = require('node:path')
|
|
10
10
|
const F = require('../core/files')
|
|
11
|
-
const { supersededGuards } = require('./hooks')
|
|
11
|
+
const { supersededGuards, expectedHooks } = require('./hooks')
|
|
12
12
|
|
|
13
13
|
function mergeConfig(current, incoming) {
|
|
14
14
|
if (Array.isArray(incoming)) {
|
|
@@ -32,19 +32,40 @@ function mergeConfig(current, incoming) {
|
|
|
32
32
|
return incoming
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Una entrada de hook que puso Cauce se reconoce por el guard al que apunta
|
|
36
|
-
//
|
|
37
|
-
// el
|
|
38
|
-
|
|
35
|
+
// Una entrada de hook que puso Cauce se reconoce por el guard al que apunta, y lo nuestro es lo que
|
|
36
|
+
// efectivamente entregamos, no todo lo que vive en nuestra carpeta. Reconocer por directorio
|
|
37
|
+
// desregistraba el guard propio del proyecto en cada reinstalación: `template/AGENTS.md` invita a
|
|
38
|
+
// ponerlo justo ahí y promete que sobrevive a cada actualización, el archivo quedaba en disco, y lo
|
|
39
|
+
// único que se veía era una línea diciendo que se había quitado una entrada obsoleta.
|
|
40
|
+
// Se saca del archivo del usuario antes de fusionar para que el merge deje exactamente las de esta
|
|
41
|
+
// versión, ni una más.
|
|
42
|
+
const HOOK_PATH = /automatization\/hooks\/([a-z0-9-]+\.sh)(?:\s|$)/
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
// Dos fuentes, y la segunda es la que cierra el borde. `expectedHooks()` dice qué entrega el motor de
|
|
45
|
+
// hoy y alcanza para una instancia que nunca instaló; `delivered` es lo que esta instancia registró
|
|
46
|
+
// haber recibido la última vez, y es lo único que reconoce un guard que entregamos en una versión y
|
|
47
|
+
// retiramos en la siguiente: su nombre ya no está en la lista de hoy, así que sin el registro quedaría
|
|
48
|
+
// vivo en la configuración llamando a un guard que Cauce ya no mantiene. Un guard propio del proyecto
|
|
49
|
+
// no está en ninguna de las dos, que es exactamente por lo que sobrevive.
|
|
50
|
+
function isDelivered(command, delivered) {
|
|
51
|
+
const hit = String(command).match(HOOK_PATH)
|
|
52
|
+
if (!hit) return false
|
|
53
|
+
return expectedHooks().includes(hit[1]) || delivered.has(String(command))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Lo que de un conjunto de comandos es wiring de guards nuestro, para anotarlo como entregado.
|
|
57
|
+
function deliveredHookCommands(commands) {
|
|
58
|
+
return [...commands].filter((command) => HOOK_PATH.test(String(command)))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withoutDeliveredHooks(config, live, delivered = new Set()) {
|
|
41
62
|
const dropped = []
|
|
42
63
|
const walk = (node) => {
|
|
43
64
|
if (Array.isArray(node)) {
|
|
44
65
|
return node
|
|
45
66
|
.filter((item) => {
|
|
46
67
|
const command = item && typeof item === 'object' ? String(item.command || '') : ''
|
|
47
|
-
if (!
|
|
68
|
+
if (!isDelivered(command, delivered)) return true
|
|
48
69
|
// Sólo se anuncia lo que ya no vuelve: una entrada que el merge repone quedó igual, y decir
|
|
49
70
|
// que se quitó y se puso la misma línea es ruido que esconde el caso que sí importa.
|
|
50
71
|
if (!live.has(command)) dropped.push(command)
|
|
@@ -83,9 +104,20 @@ function reportRemoved(name, dropped, live, output) {
|
|
|
83
104
|
|
|
84
105
|
function includesConfig(actual, expected) {
|
|
85
106
|
if (Array.isArray(expected)) {
|
|
86
|
-
return Array.isArray(actual) && expected.every((item) => {
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
return Array.isArray(actual) && expected.every((item) => actual.some((value) => {
|
|
108
|
+
// Un grupo de hooks se compara por su contenido, no por su forma serializada. La comprobación es
|
|
109
|
+
// «contiene», que es lo correcto, pero con el ítem entero como unidad un grupo `{matcher, hooks}`
|
|
110
|
+
// con un hook de más dejaba de ser el mismo objeto y contaba como ausente: `doctor` llamaba
|
|
111
|
+
// divergente a una configuración que tenía todo lo esperado, y el anidamiento —donde vive la
|
|
112
|
+
// diferencia real— no se miraba nunca. La instalación promete conservar lo que el proyecto ya
|
|
113
|
+
// tenía, así que sumar un hook propio a un grupo nuestro es exactamente lo que permite.
|
|
114
|
+
if (item && typeof item === 'object' && Array.isArray(item.hooks)) {
|
|
115
|
+
return value && typeof value === 'object'
|
|
116
|
+
&& value.matcher === item.matcher
|
|
117
|
+
&& includesConfig(value.hooks, item.hooks)
|
|
118
|
+
}
|
|
119
|
+
return JSON.stringify(value) === JSON.stringify(item)
|
|
120
|
+
}))
|
|
89
121
|
}
|
|
90
122
|
if (expected && typeof expected === 'object') {
|
|
91
123
|
return actual && typeof actual === 'object' && Object.entries(expected).every(([key, value]) => {
|
|
@@ -170,6 +202,6 @@ function blockUpToDate(file, name, content) {
|
|
|
170
202
|
|
|
171
203
|
module.exports = {
|
|
172
204
|
blockStart,
|
|
173
|
-
mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
|
|
205
|
+
mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
|
|
174
206
|
unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
|
|
175
207
|
}
|
|
@@ -17,7 +17,7 @@ const {
|
|
|
17
17
|
legacyGuardWiring, staleHooks, listHooks,
|
|
18
18
|
} = require('./hooks')
|
|
19
19
|
const {
|
|
20
|
-
blockStart, mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
|
|
20
|
+
blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
|
|
21
21
|
unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
|
|
22
22
|
} = require('./config')
|
|
23
23
|
|
|
@@ -226,6 +226,11 @@ function deliveryKey(name, target) {
|
|
|
226
226
|
return `${name}/${target.split(path.sep).join('/')}`
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// La sección `runners` del manifiesto anota archivo → digest; ésta es la única entrada que guarda una
|
|
230
|
+
// lista, porque lo que registra no es un archivo sino las entradas que dejamos dentro del archivo de
|
|
231
|
+
// configuración del usuario. No colisiona con ningún `target`: ninguno se llama así.
|
|
232
|
+
const HOOKS_KEY = 'config.hooks'
|
|
233
|
+
|
|
229
234
|
// En qué estado quedó un archivo que este adaptador entregó alguna vez.
|
|
230
235
|
//
|
|
231
236
|
// nuevo no existe todavía
|
|
@@ -277,6 +282,7 @@ function uninstall(root, name, output = console) {
|
|
|
277
282
|
|
|
278
283
|
const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
|
|
279
284
|
const deliveredPaths = { ...recorded }
|
|
285
|
+
delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
|
|
280
286
|
for (const item of items) {
|
|
281
287
|
const resolved = { item, ...resolveItem(paths, root, name, item) }
|
|
282
288
|
const key = deliveryKey(name, item.target)
|
|
@@ -378,7 +384,12 @@ function install(root, name, output = console, options = {}) {
|
|
|
378
384
|
// guard que el runner intenta ejecutar y falla. Se quitan las nuestras y las vuelve a poner el merge.
|
|
379
385
|
const live = new Set((JSON.stringify(incoming).match(/"command":"[^"]*"/g) || [])
|
|
380
386
|
.map((entry) => JSON.parse(`{${entry}}`).command))
|
|
381
|
-
|
|
387
|
+
// Lo que este adaptador anotó haber entregado la última vez. Es lo único que reconoce una entrada
|
|
388
|
+
// nuestra cuyo guard el motor ya no trae: por nombre no se distingue de la que agregó el proyecto.
|
|
389
|
+
const previous = new Set(recorded[deliveryKey(name, HOOKS_KEY)] || [])
|
|
390
|
+
const clean = runner.config.owned
|
|
391
|
+
? { config: {}, dropped: [] }
|
|
392
|
+
: withoutDeliveredHooks(current, live, previous)
|
|
382
393
|
reportRemoved(name, clean.dropped, live, output)
|
|
383
394
|
F.atomicWriteJson(paths.configTarget, mergeConfig(clean.config, incoming))
|
|
384
395
|
// Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
|
|
@@ -388,6 +399,12 @@ function install(root, name, output = console, options = {}) {
|
|
|
388
399
|
}
|
|
389
400
|
output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
|
|
390
401
|
const deliveredPaths = { ...recorded }
|
|
402
|
+
// Qué wiring de guards dejamos puesto, para poder retirarlo el día que el motor deje de traerlo. Se
|
|
403
|
+
// anota lo que instalamos y no lo que quedó en el archivo: ahí conviven las entradas del proyecto, y
|
|
404
|
+
// registrarlas nos autorizaría a borrar lo ajeno en la instalación siguiente.
|
|
405
|
+
const ourHooks = deliveredHookCommands(live)
|
|
406
|
+
if (ourHooks.length) deliveredPaths[deliveryKey(name, HOOKS_KEY)] = ourHooks
|
|
407
|
+
else delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
|
|
391
408
|
for (const resolved of resolvedItems) {
|
|
392
409
|
const status = state.get(resolved)
|
|
393
410
|
const ownFile = runner.instructions.includes(resolved.item)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Cómo una instancia declara el motor en su `package.json`: ponerlo, reponerlo y sacarlo. Vive aparte
|
|
4
|
+
// del ciclo de vida porque su reloj es otro —npm y el versionado, no crear/actualizar/borrar— y porque
|
|
5
|
+
// sus tres consumidores no se solapan: `scaffold` declara, `upgrade` repone y `destroy` saca.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const F = require('../core/files')
|
|
10
|
+
const { fail } = require('./io')
|
|
11
|
+
|
|
12
|
+
// Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
|
|
13
|
+
// Conserva el manifiesto existente porque el repo anfitrión puede tener el suyo.
|
|
14
|
+
function declareEngine(manifest, version) {
|
|
15
|
+
let pkg = { name: path.basename(path.dirname(manifest)), private: true, version: '0.0.0' }
|
|
16
|
+
if (fs.existsSync(manifest)) {
|
|
17
|
+
try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch (error) {
|
|
18
|
+
fail(`package.json inválido en ${manifest}: ${error.message}`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
pkg.devDependencies = { ...pkg.devDependencies, '@ingeniomaps/cauce': version }
|
|
22
|
+
F.atomicWriteJson(manifest, pkg)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Repone la versión exacta que `declareEngine` había dejado, y devuelve la que había si cambió algo.
|
|
26
|
+
//
|
|
27
|
+
// El pin no sobrevive por sí solo: npm guarda con caret, así que el `npm install @latest` del primer
|
|
28
|
+
// paso del upgrade lo convierte en un rango, y con eso deja de valer la razón que el README da para no
|
|
29
|
+
// saltear ese paso —«init fija la versión exacta, así que npm update no la mueve»—. Repararlo acá y no
|
|
30
|
+
// sólo documentar el comando es lo que vuelve la propiedad independiente de cómo se haya instalado.
|
|
31
|
+
//
|
|
32
|
+
// Sin manifiesto no hay nada que reponer: una instancia que no lo tiene resuelve el motor de otra
|
|
33
|
+
// forma, y crearle uno sería otra operación que nadie pidió en un upgrade.
|
|
34
|
+
function pinEngine(root, version) {
|
|
35
|
+
const manifest = path.join(root, 'package.json')
|
|
36
|
+
if (!fs.existsSync(manifest)) return null
|
|
37
|
+
let pkg
|
|
38
|
+
try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch { return 'ilegible' }
|
|
39
|
+
const current = (pkg.devDependencies || {})['@ingeniomaps/cauce']
|
|
40
|
+
if (!current || current === version) return null
|
|
41
|
+
declareEngine(manifest, version)
|
|
42
|
+
return current
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// La inversa exacta de `declareEngine`: saca la clave que puso y nada más. El resto del manifiesto es
|
|
46
|
+
// del repo anfitrión aunque hoy no tenga otra cosa —un `package.json` vacío puede ser lo que alguien
|
|
47
|
+
// escribió para tener scripts— así que el archivo se borra sólo si es idéntico al que `declareEngine`
|
|
48
|
+
// habría creado desde cero, sin dependencias, sin scripts y con su `version: 0.0.0`.
|
|
49
|
+
//
|
|
50
|
+
// Lo que no se toca nunca es `node_modules/` ni el lockfile: los escribe npm, pueden tener dependencias
|
|
51
|
+
// del proyecto y borrarlos por nuestra cuenta destruye trabajo ajeno. Se nombran en la salida, que es
|
|
52
|
+
// la mitad que faltaba: `destroy` decía «tu repositorio queda donde está» y dejaba un `package.json`
|
|
53
|
+
// cuya única dependencia era Cauce. En un repo Rust eso es basura conspicua y nadie avisaba.
|
|
54
|
+
function undeclareEngine(manifest) {
|
|
55
|
+
if (!fs.existsSync(manifest)) return []
|
|
56
|
+
let pkg
|
|
57
|
+
try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch { return [] }
|
|
58
|
+
const dev = pkg.devDependencies || {}
|
|
59
|
+
if (!('@ingeniomaps/cauce' in dev)) return []
|
|
60
|
+
delete dev['@ingeniomaps/cauce']
|
|
61
|
+
pkg.devDependencies = dev
|
|
62
|
+
const ours = !Object.keys(dev).length && !Object.keys(pkg.dependencies || {}).length
|
|
63
|
+
&& !Object.keys(pkg.scripts || {}).length && pkg.private === true && pkg.version === '0.0.0'
|
|
64
|
+
if (ours) {
|
|
65
|
+
fs.rmSync(manifest, { force: true })
|
|
66
|
+
return ['package.json (lo había creado init: sin dependencias ni scripts propios)']
|
|
67
|
+
}
|
|
68
|
+
if (!Object.keys(dev).length) delete pkg.devDependencies
|
|
69
|
+
F.atomicWriteJson(manifest, pkg)
|
|
70
|
+
return ['package.json: se quitó la dependencia del motor y el resto queda como estaba']
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { declareEngine, pinEngine, undeclareEngine }
|
package/engine/cli/instance.js
CHANGED
|
@@ -16,6 +16,7 @@ const P = require('../planning/parser')
|
|
|
16
16
|
const ST = require('../planning/state')
|
|
17
17
|
const A = require('../automation')
|
|
18
18
|
const { fail } = require('./io')
|
|
19
|
+
const { declareEngine, pinEngine, undeclareEngine } = require('./dependency')
|
|
19
20
|
|
|
20
21
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
|
|
21
22
|
|
|
@@ -29,9 +30,14 @@ function providerNames() {
|
|
|
29
30
|
} catch { return [] }
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// Devuelve, por archivo conservado, el digest de lo que el molde **habría** escrito. Adoptar Cauce en
|
|
34
|
+
// un repositorio con contenido es `init --force`, y lo que se conserva ahí lo escribió la empresa: si
|
|
35
|
+
// el manifiesto lo registra hasheando el disco queda declarado como entregado por Cauce con contenido
|
|
36
|
+
// que Cauce nunca entregó, y el `upgrade` siguiente no ve ninguna edición local y lo reemplaza.
|
|
32
37
|
function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
|
|
33
38
|
F.assertNoSymlinkPath(path.dirname(target), target)
|
|
34
39
|
fs.mkdirSync(target, { recursive: true })
|
|
40
|
+
const preserved = {}
|
|
35
41
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
36
42
|
if (skip.includes(entry.name)) continue
|
|
37
43
|
const from = path.join(source, entry.name)
|
|
@@ -39,11 +45,14 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
|
|
|
39
45
|
// 11.16.0: lo deja afuera en la raíz y en subdirectorios—, así que viaja sin punto y se restituye
|
|
40
46
|
// acá. Sin esto el archivo existe en el repo del toolkit y desaparece para todo consumidor real.
|
|
41
47
|
const to = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name)
|
|
42
|
-
if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip, quiet)
|
|
48
|
+
if (entry.isDirectory()) Object.assign(preserved, copyTemplate(from, to, replacements, force, skip, quiet))
|
|
43
49
|
else {
|
|
44
50
|
if (fs.existsSync(to)) {
|
|
45
51
|
if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`)
|
|
46
52
|
if (!quiet) console.log(`= conservado ${to}`)
|
|
53
|
+
let would = fs.readFileSync(from, 'utf8')
|
|
54
|
+
for (const [key, value] of Object.entries(replacements)) would = would.replaceAll(key, value)
|
|
55
|
+
preserved[to] = M.digestText(would)
|
|
47
56
|
continue
|
|
48
57
|
}
|
|
49
58
|
let content = fs.readFileSync(from, 'utf8')
|
|
@@ -53,70 +62,36 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
|
|
|
53
62
|
if (!quiet) console.log(`+ ${to}`)
|
|
54
63
|
}
|
|
55
64
|
}
|
|
65
|
+
return preserved
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
// Devuelve lo conservado igual que `copyTemplate`, y por la misma razón: acá el runtime no lleva
|
|
69
|
+
// reemplazos, así que lo que habríamos escrito es el archivo del paquete tal cual.
|
|
58
70
|
function copyRuntime(source, target, preserve = false, boundary = target, skip = []) {
|
|
59
71
|
F.assertNoSymlinkPath(boundary, target)
|
|
60
72
|
fs.mkdirSync(target, { recursive: true })
|
|
73
|
+
const preserved = {}
|
|
61
74
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
62
75
|
if (skip.includes(entry.name)) continue
|
|
63
76
|
const from = path.join(source, entry.name)
|
|
64
77
|
const to = path.join(target, entry.name)
|
|
65
|
-
if (entry.isDirectory()) copyRuntime(from, to, preserve, boundary, skip)
|
|
66
|
-
else if (preserve && fs.existsSync(to))
|
|
67
|
-
|
|
78
|
+
if (entry.isDirectory()) Object.assign(preserved, copyRuntime(from, to, preserve, boundary, skip))
|
|
79
|
+
else if (preserve && fs.existsSync(to)) {
|
|
80
|
+
console.log(`= conservado ${to}`)
|
|
81
|
+
preserved[to] = M.digest(from)
|
|
82
|
+
} else {
|
|
68
83
|
F.assertNoSymlinkPath(boundary, to)
|
|
69
84
|
fs.copyFileSync(from, to)
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
|
|
75
|
-
// Conserva el manifiesto existente porque el repo anfitrión puede tener el suyo.
|
|
76
|
-
function declareEngine(manifest, version) {
|
|
77
|
-
let pkg = { name: path.basename(path.dirname(manifest)), private: true, version: '0.0.0' }
|
|
78
|
-
if (fs.existsSync(manifest)) {
|
|
79
|
-
try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch (error) {
|
|
80
|
-
fail(`package.json inválido en ${manifest}: ${error.message}`)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
pkg.devDependencies = { ...pkg.devDependencies, '@ingeniomaps/cauce': version }
|
|
84
|
-
F.atomicWriteJson(manifest, pkg)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// La inversa exacta de `declareEngine`: saca la clave que puso y nada más. El resto del manifiesto es
|
|
88
|
-
// del repo anfitrión aunque hoy no tenga otra cosa —un `package.json` vacío puede ser lo que alguien
|
|
89
|
-
// escribió para tener scripts— así que el archivo se borra sólo si es idéntico al que `declareEngine`
|
|
90
|
-
// habría creado desde cero, sin dependencias, sin scripts y con su `version: 0.0.0`.
|
|
91
|
-
//
|
|
92
|
-
// Lo que no se toca nunca es `node_modules/` ni el lockfile: los escribe npm, pueden tener dependencias
|
|
93
|
-
// del proyecto y borrarlos por nuestra cuenta destruye trabajo ajeno. Se nombran en la salida, que es
|
|
94
|
-
// la mitad que faltaba: `destroy` decía «tu repositorio queda donde está» y dejaba un `package.json`
|
|
95
|
-
// cuya única dependencia era Cauce. En un repo Rust eso es basura conspicua y nadie avisaba.
|
|
96
|
-
function undeclareEngine(manifest) {
|
|
97
|
-
if (!fs.existsSync(manifest)) return []
|
|
98
|
-
let pkg
|
|
99
|
-
try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch { return [] }
|
|
100
|
-
const dev = pkg.devDependencies || {}
|
|
101
|
-
if (!('@ingeniomaps/cauce' in dev)) return []
|
|
102
|
-
delete dev['@ingeniomaps/cauce']
|
|
103
|
-
pkg.devDependencies = dev
|
|
104
|
-
const ours = !Object.keys(dev).length && !Object.keys(pkg.dependencies || {}).length
|
|
105
|
-
&& !Object.keys(pkg.scripts || {}).length && pkg.private === true && pkg.version === '0.0.0'
|
|
106
|
-
if (ours) {
|
|
107
|
-
fs.rmSync(manifest, { force: true })
|
|
108
|
-
return ['package.json (lo había creado init: sin dependencias ni scripts propios)']
|
|
109
|
-
}
|
|
110
|
-
if (!Object.keys(dev).length) delete pkg.devDependencies
|
|
111
|
-
F.atomicWriteJson(manifest, pkg)
|
|
112
|
-
return ['package.json: se quitó la dependencia del motor y el resto queda como estaba']
|
|
87
|
+
return preserved
|
|
113
88
|
}
|
|
114
89
|
|
|
115
90
|
// El andamiaje de una instancia, sin leer argv. `init` es la cáscara que traduce banderas a esto, y
|
|
116
91
|
// el banco de evaluación lo llama directo: crear una instancia programáticamente no puede depender de
|
|
117
92
|
// cómo venga escrita la línea de comandos.
|
|
118
93
|
function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
119
|
-
copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
|
|
94
|
+
const preserved = copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
|
|
120
95
|
'{{PROJECT_NAME}}': name,
|
|
121
96
|
'{{MODE}}': mode,
|
|
122
97
|
'{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
|
|
@@ -124,12 +99,12 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
124
99
|
// No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
|
|
125
100
|
// el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando lo que no aplica dejaba
|
|
126
101
|
// `.github/workflows/` vacío en cada instancia.
|
|
127
|
-
copyRuntime(
|
|
102
|
+
Object.assign(preserved, copyRuntime(
|
|
128
103
|
path.join(PROJECT_ROOT, 'automatization', 'hooks'),
|
|
129
104
|
path.join(root, 'automatization', 'hooks'),
|
|
130
105
|
force,
|
|
131
106
|
root,
|
|
132
|
-
)
|
|
107
|
+
))
|
|
133
108
|
const version = require(path.join(PROJECT_ROOT, 'package.json')).version
|
|
134
109
|
// El motor llega como dependencia para que el lockfile fije su versión. El repo ops es un sidecar:
|
|
135
110
|
// declarar npm acá no convierte en Node al servicio de Go de al lado.
|
|
@@ -140,6 +115,14 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
140
115
|
if (fs.existsSync(dir)) deliveredPaths = M.record(root, relative, O.treeFiles(dir), deliveredPaths)
|
|
141
116
|
}
|
|
142
117
|
deliveredPaths = M.recordPaths(root, O.SYSTEM_FILES, deliveredPaths)
|
|
118
|
+
// Lo conservado entra con el digest del molde, no con el del disco: la diferencia entre los dos es
|
|
119
|
+
// justamente lo que `localChanges` tiene que ver para que `upgrade` se detenga antes de pisar el
|
|
120
|
+
// archivo de la empresa. Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce
|
|
121
|
+
// rastrea, y un archivo propio fuera de esa frontera no le incumbe.
|
|
122
|
+
for (const [file, hash] of Object.entries(preserved)) {
|
|
123
|
+
const relative = path.relative(root, file).split(path.sep).join('/')
|
|
124
|
+
if (relative in deliveredPaths) deliveredPaths[relative] = hash
|
|
125
|
+
}
|
|
143
126
|
M.write(root, deliveredPaths)
|
|
144
127
|
// La instancia recuerda de qué versión salió: sin esto no hay actualización posible.
|
|
145
128
|
const configFile = path.join(root, 'ops.config.json')
|
|
@@ -305,12 +288,23 @@ function upgrade(dir, cli) {
|
|
|
305
288
|
const overrides = O.overrides(root)
|
|
306
289
|
|
|
307
290
|
if (dry) {
|
|
291
|
+
// Lo editado localmente se informa siempre, antes de mirar versiones: son dos preguntas distintas
|
|
292
|
+
// —«¿hay algo más nuevo?» y «¿qué tengo editado que se perdería?»— y la segunda tiene respuesta
|
|
293
|
+
// útil aunque la primera sea que no. Sin esto, el único modo que no toca nada era también el único
|
|
294
|
+
// que no podía avisar del conflicto, justo en el estado normal entre actualizaciones: `init` fija
|
|
295
|
+
// la versión exacta, así que instancia y motor coinciden casi todo el tiempo.
|
|
296
|
+
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
308
297
|
if (from === to) {
|
|
309
298
|
// Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
|
|
310
299
|
// importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
|
|
311
300
|
// a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
|
|
312
301
|
console.log(`= ${to}: la instancia está al día con el motor instalado`)
|
|
313
|
-
|
|
302
|
+
// Con `--save-exact` porque npm guarda con caret por defecto, y el caret es lo que volvería
|
|
303
|
+
// falsa la línea de arriba: dentro de 0.x alcanza a los patches, y Cauce publica patches.
|
|
304
|
+
console.log(' para traer una versión más nueva:'
|
|
305
|
+
+ ' npm install --save-exact --save-dev @ingeniomaps/cauce@latest')
|
|
306
|
+
if (changed.length) process.exit(1)
|
|
307
|
+
return
|
|
314
308
|
}
|
|
315
309
|
// Hacia atrás también es legítimo —una versión rompió algo y se vuelve—, pero anunciarlo como «hay
|
|
316
310
|
// una versión más nueva» era mentir con el número a la vista. Y lo que corresponde imprimir es lo
|
|
@@ -322,7 +316,6 @@ function upgrade(dir, cli) {
|
|
|
322
316
|
console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
|
|
323
317
|
printChangelog(from, to)
|
|
324
318
|
}
|
|
325
|
-
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
326
319
|
process.exit(1)
|
|
327
320
|
}
|
|
328
321
|
|
|
@@ -416,18 +409,27 @@ function upgrade(dir, cli) {
|
|
|
416
409
|
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
417
410
|
config.cauceVersion = to
|
|
418
411
|
F.atomicWriteJson(path.join(root, 'ops.config.json'), config)
|
|
412
|
+
// Acá es donde las tres versiones se saben a la vez, así que es donde pueden quedar diciendo lo mismo.
|
|
413
|
+
const pinned = pinEngine(root, to)
|
|
419
414
|
|
|
420
415
|
console.log(`✓ Cauce ${from || '(previa)'} → ${to}`)
|
|
421
416
|
// Descartar con --force es legítimo; hacerlo sin dejar rastro no. Queda en la salida del comando,
|
|
422
417
|
// que es la evidencia que el protocolo pide para cualquier cambio.
|
|
423
418
|
for (const file of changed) console.log(`− descartado tu cambio en ${file}`)
|
|
424
419
|
for (const relative of retired) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
|
|
420
|
+
// Se dice porque cambia un archivo que la empresa versiona, y porque explica un diff que si no
|
|
421
|
+
// aparecería sin autor. Ilegible se avisa y no se toca: el manifiesto es del repo anfitrión.
|
|
422
|
+
if (pinned === 'ilegible') console.log(' ⚠ package.json no se pudo leer: su versión quedó como estaba')
|
|
423
|
+
else if (pinned) console.log(` package.json: ${pinned} → ${to}, la versión exacta que acabás de aplicar`)
|
|
425
424
|
printChangelog(from, to)
|
|
426
425
|
console.log(` ${system.length} ruta(s) del sistema y ${O.RUNTIME_PATHS.length} del runtime actualizadas`)
|
|
427
426
|
for (const override of overrides) {
|
|
428
427
|
console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
|
|
429
428
|
}
|
|
430
|
-
|
|
429
|
+
// Sólo cuando es cierto. Llegar acá con algo en `changed` significa que se corrió con --force y se
|
|
430
|
+
// descartó contenido de la empresa: las líneas de arriba lo enumeran, y afirmar a continuación que
|
|
431
|
+
// todo lo propio quedó intacto contradice a la única señal que recibe quien corrió el comando.
|
|
432
|
+
if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
|
|
431
433
|
// No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
|
|
432
434
|
// decide una persona.
|
|
433
435
|
if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
|
package/engine/cli/ops.js
CHANGED
|
@@ -21,8 +21,14 @@ const DEFAULT_TARGET = 'ops'
|
|
|
21
21
|
// de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
|
|
22
22
|
// instancia; no hay una segunda adentro.
|
|
23
23
|
function implicitTarget(cwd) {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return isInstanceDir(cwd) ? '.' : DEFAULT_TARGET
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// La misma heurística, aplicada al destino ya resuelto: una carpeta que ya nombra al toolkit **es** la
|
|
28
|
+
// instancia, y el código de la empresa vive al lado.
|
|
29
|
+
function isInstanceDir(dir) {
|
|
30
|
+
const base = path.basename(dir)
|
|
31
|
+
return base === DEFAULT_TARGET || base.endsWith('-ops')
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
// El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
|
|
@@ -73,7 +79,12 @@ async function init(target, cli) {
|
|
|
73
79
|
// install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
|
|
74
80
|
// único que faltaba era que fuera lo que pasa cuando no se elige nada.
|
|
75
81
|
const root = path.resolve(target || implicitTarget(process.cwd()))
|
|
76
|
-
|
|
82
|
+
// El modo lo decide a dónde apunta el destino, no si alguien escribió el argumento. `init` e `init .`
|
|
83
|
+
// resuelven el mismo directorio y elegían modos opuestos: escribir el punto —la forma más natural de
|
|
84
|
+
// decir «acá»— daba embedded, y ahí la raíz del workspace pasa a ser la carpeta ops misma, así que
|
|
85
|
+
// `guard-workspace-boundary` deja de reconocer los repos hermanos y el wiring del runner se escribe
|
|
86
|
+
// adentro en vez de al lado, donde el dev abre su herramienta.
|
|
87
|
+
const mode = cli.value('--mode', isInstanceDir(root) ? 'sidecar' : 'embedded')
|
|
77
88
|
if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
|
|
78
89
|
const name = cli.value('--name', defaultName(root))
|
|
79
90
|
const force = cli.has('--force')
|
package/package.json
CHANGED
package/template/Makefile
CHANGED
|
@@ -29,7 +29,7 @@ destroy: ## Muestra qué se pierde al borrar esta instancia (borrar exige --forc
|
|
|
29
29
|
@node tools/ops.js destroy .
|
|
30
30
|
|
|
31
31
|
upgrade: ## Trae la última versión de Cauce y la aplica, sin tocar lo del proyecto
|
|
32
|
-
@npm install --save-dev @ingeniomaps/cauce@latest
|
|
32
|
+
@npm install --save-exact --save-dev @ingeniomaps/cauce@latest
|
|
33
33
|
@node tools/ops.js upgrade .
|
|
34
34
|
|
|
35
35
|
automation-check: ## Comprueba hooks, workflows y adaptadores
|