@saulwade/swl-ses 2.6.1 → 2.8.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/CLAUDE.md +14 -2
- package/README.md +65 -18
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/bin/swl-ses.js +10 -0
- package/comandos/swl/brainstorm.md +1 -0
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/deuda-codigo.md +97 -97
- package/comandos/swl/mcp-status.md +1 -0
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/agent-deep-links/SKILL.md +148 -148
- package/habilidades/backend-async-postgres-testing/SKILL.md +216 -216
- package/habilidades/backend-error-design/SKILL.md +221 -221
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/calidad-anti-patrones-universales/SKILL.md +105 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -165
- package/habilidades/calidad-mutation-testing/SKILL.md +25 -1
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
- package/habilidades/ci-cd-pipelines/SKILL.md +5 -1
- package/habilidades/css-moderno/SKILL.md +7 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
- package/habilidades/extractor-de-aprendizajes/SKILL.md +5 -1
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/SKILL.md +3 -2
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
- package/habilidades/proceso-dynamic-workflows/SKILL.md +60 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
- package/habilidades/proceso-modular-split/SKILL.md +256 -256
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
- package/hooks/calidad-pre-commit.js +159 -10
- package/hooks/ciclo-evolucion-subagente.js +26 -26
- package/hooks/ciclo-evolucion.js +26 -26
- package/hooks/contexto-subagente.js +68 -68
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/ciclo-evolucion.js +47 -47
- package/hooks/lib/deep-links.js +185 -185
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/notificacion-formato.js +45 -11
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/raiz-proyecto.js +35 -4
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/notificacion-telegram.js +5 -11
- package/hooks/session-briefing.js +12 -4
- package/instintos/autonomia.yaml +27 -27
- package/instintos/prompt-appendices.yaml +57 -57
- package/llms.txt +1 -1
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/canonical-hashes.json +662 -0
- package/manifiestos/harness-ir.json +47536 -0
- package/manifiestos/hooks-config.json +469 -469
- package/manifiestos/invariantes-criticos.json +30 -30
- package/manifiestos/policy-bundle.json +2065 -0
- package/manifiestos/policy-corpus-w2.json +3926 -0
- package/manifiestos/runtime-adapters-core3.json +208 -0
- package/manifiestos/runtime-conformance.json +139 -0
- package/manifiestos/skills-lock.json +43 -43
- package/package.json +2 -2
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +2 -2
- package/reglas/accesibilidad.md +10 -10
- package/reglas/auditorias-documentales-estructurales.md +7 -7
- package/reglas/cloud-infra.md +8 -8
- package/reglas/consultar-vault-primero.md +195 -195
- package/reglas/git-workflow.md +1 -0
- package/reglas/hooks.md +6 -6
- package/reglas/intent-engineering.md +218 -218
- package/reglas/markitdown.md +8 -8
- package/reglas/monitor-ci.md +12 -0
- package/reglas/patrones.md +6 -6
- package/reglas/testing.md +7 -7
- package/reglas/tests-cleanup.md +224 -224
- package/schemas/agent-message.schema.json +73 -73
- package/schemas/agent-output-implementacion.schema.json +114 -114
- package/schemas/agent-output-planificacion.schema.json +150 -150
- package/schemas/agent-output-review.schema.json +98 -98
- package/schemas/diary-entry.schema.json +112 -112
- package/schemas/gate-state.schema.json +76 -0
- package/schemas/harness-ir.schema.json +369 -0
- package/schemas/hook-profiles.schema.json +54 -54
- package/schemas/hooks-config.schema.json +89 -89
- package/schemas/legacy-gates.schema.json +45 -0
- package/schemas/modulos.schema.json +38 -38
- package/schemas/perfiles.schema.json +36 -36
- package/schemas/plugin.schema.json +77 -77
- package/schemas/policy-bundle.schema.json +140 -0
- package/schemas/policy-enforcement.schema.json +117 -0
- package/schemas/policy-operation.schema.json +261 -0
- package/schemas/runtime-adapter.schema.json +176 -0
- package/schemas/runtime-build-attestation.schema.json +100 -0
- package/schemas/runtime-conformance.schema.json +239 -0
- package/schemas/runtime-diagnostic.schema.json +395 -0
- package/schemas/skill-evals.schema.json +119 -119
- package/schemas/skill-frontmatter.schema.json +245 -245
- package/schemas/w4-certification-request.schema.json +72 -0
- package/schemas/w4-certification-verdict.schema.json +224 -0
- package/schemas/w4-corpus.schema.json +172 -0
- package/schemas/w4-mutation-report.schema.json +116 -0
- package/schemas/w4-replay-result.schema.json +164 -0
- package/schemas/w4-scoring-report.schema.json +89 -0
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/bootstrap-instintos.js +3 -0
- package/scripts/cli/aprobar-plan.js +73 -73
- package/scripts/cli/briefing.js +23 -23
- package/scripts/cli/ciclo-evolucion.js +26 -26
- package/scripts/cli/derivar-feature-list.js +25 -25
- package/scripts/cli/detectar-host.js +27 -27
- package/scripts/cli/diary-entry.js +69 -69
- package/scripts/cli/execution-state.js +18 -18
- package/scripts/cli/gateway-notify.js +41 -41
- package/scripts/cli/liberar-fase.js +42 -42
- package/scripts/cli/mark-evolved.js +56 -56
- package/scripts/cli/metricas-dora.js +26 -26
- package/scripts/cli/near-duplicate.js +55 -55
- package/scripts/cli/notificaciones.js +123 -123
- package/scripts/cli/propose-step.js +29 -29
- package/scripts/cli/schedule-parse.js +19 -19
- package/scripts/cli/sugerir-modelo.js +20 -20
- package/scripts/cli/verificar-plan.js +36 -36
- package/scripts/cli/verificar-trazabilidad.js +35 -35
- package/scripts/comandos/install-asistido.js +8 -7
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/doctor.js +61 -36
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-claims-runtime.js +1342 -0
- package/scripts/generar-harness-ir.js +257 -0
- package/scripts/generar-inventario.js +52 -54
- package/scripts/generar-policy-bundle.js +202 -0
- package/scripts/instalador.js +26 -7
- package/scripts/lib/approval-receipts.js +190 -0
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/certificacion-loop-state.js +421 -0
- package/scripts/lib/ci-reader.js +193 -193
- package/scripts/lib/ciclo-autonomo/yaml-instintos.js +56 -0
- package/scripts/lib/clasificar-directorio.js +92 -0
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-host-swl.js +175 -175
- package/scripts/lib/detectar-runtime.js +29 -20
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
- package/scripts/lib/detector-reglas-duplicadas.js +220 -220
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/evidence-verifier.js +192 -0
- package/scripts/lib/evidencia-release.js +322 -322
- package/scripts/lib/frontmatter-canonico.js +509 -0
- package/scripts/lib/gate-engine.js +871 -0
- package/scripts/lib/gate-hooks-requires.js +249 -249
- package/scripts/lib/gate-licencias.js +212 -212
- package/scripts/lib/git-config-preflight.js +48 -0
- package/scripts/lib/git-metricas.js +257 -257
- package/scripts/lib/harness-ir.js +778 -0
- package/scripts/lib/harness-source-snapshot.js +309 -0
- package/scripts/lib/integrity-ledger.js +1147 -0
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/legacy-gate-migration.js +324 -0
- package/scripts/lib/limpiar-basura-global.js +45 -2
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/metricas-dora.js +204 -204
- package/scripts/lib/notificaciones-telegram.js +1 -0
- package/scripts/lib/npm-version.js +1 -0
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/plan-lock.js +61 -13
- package/scripts/lib/policy-broker.js +338 -0
- package/scripts/lib/policy-bundle.js +342 -0
- package/scripts/lib/policy-context-provider.js +310 -0
- package/scripts/lib/policy-contract.js +479 -0
- package/scripts/lib/policy-verifier-utils.js +65 -0
- package/scripts/lib/pr-analyzer.js +399 -399
- package/scripts/lib/principal-verifier.js +178 -0
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/resolver-plan-fase.js +37 -37
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/runtime-adapter-contract.js +267 -0
- package/scripts/lib/runtime-artifact-verifier.js +426 -0
- package/scripts/lib/runtime-build-attestation.js +127 -0
- package/scripts/lib/runtime-bundle-installer.js +586 -0
- package/scripts/lib/runtime-compiler.js +327 -0
- package/scripts/lib/runtime-conformance.js +202 -0
- package/scripts/lib/runtime-doctor-core3.js +567 -0
- package/scripts/lib/runtime-doctor-input.js +59 -0
- package/scripts/lib/runtime-operation-adapter.js +267 -0
- package/scripts/lib/schema-version.js +164 -164
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/signed-envelope.js +545 -0
- package/scripts/lib/single-use-store.js +359 -0
- package/scripts/lib/skills-externas.js +31 -0
- package/scripts/lib/transformadores/codex.js +15 -8
- package/scripts/lib/transformadores/gemini.js +79 -5
- package/scripts/lib/w4-attestation-adapter.js +158 -0
- package/scripts/lib/w4-canario.js +337 -0
- package/scripts/lib/w4-claims.js +182 -0
- package/scripts/lib/w4-corpus-generador.js +542 -0
- package/scripts/lib/w4-gate-c5.js +115 -0
- package/scripts/lib/w4-harness-bajo-prueba.js +155 -0
- package/scripts/lib/w4-matriz-combos.js +55 -0
- package/scripts/lib/w4-motor-mutacion.js +1348 -0
- package/scripts/lib/w4-motor-replay.js +735 -0
- package/scripts/lib/w4-pin-origen.js +54 -0
- package/scripts/lib/w4-publicar-request.js +132 -0
- package/scripts/lib/w4-revocacion.js +62 -0
- package/scripts/lib/w4-runtimes-core3.js +38 -0
- package/scripts/lib/w4-scorer-certificacion.js +692 -0
- package/scripts/lib/w4-superficie-candidato.js +49 -0
- package/scripts/lib/w4-veredicto.js +452 -0
- package/scripts/lib/w4-verificar-veredicto.js +302 -0
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +200 -200
- package/scripts/migrar-gates-legacy.js +108 -0
- package/scripts/publicar-certification-request.js +115 -0
- package/scripts/runtime-doctor.js +107 -0
- package/scripts/tui/componentes/selector-multi.js +189 -189
- package/scripts/tui/componentes/selector-unico.js +158 -158
- package/scripts/tui/ejecutores.js +375 -375
- package/scripts/tui/lib/colores.js +129 -129
- package/scripts/tui/lib/render.js +264 -264
- package/scripts/tui/lib/teclas.js +113 -113
- package/scripts/tui/pantallas/install-wizard.js +12 -7
- package/scripts/tui/pantallas/menu-principal.js +52 -52
- package/scripts/tui/pantallas/progreso.js +274 -274
- package/scripts/tui/pantallas/resumen.js +132 -132
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-aislamiento-swl-eval.js +87 -0
- package/scripts/verificar-empaquetado-downstream.js +375 -0
- package/scripts/verificar-loop-constructor.js +215 -0
- package/scripts/verificar-trazabilidad.js +13 -6
- package/scripts/verificar-veredicto-real.js +84 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Único writer para instalar, actualizar y desinstalar bundles Core-3
|
|
5
|
+
* (T03) dentro de un home de runtime (`baseDir`). Journal transaccional
|
|
6
|
+
* por operación — backup -> stage (temp + fsync) -> swap (rename + fsync
|
|
7
|
+
* de directorio) -> marca `swapped` — con recuperación idempotente ante
|
|
8
|
+
* una corrida interrumpida (crash, kill, fault inyectada).
|
|
9
|
+
*
|
|
10
|
+
* Nunca activa hooks, grants ni autenticación: solo copia bytes ya
|
|
11
|
+
* verificados por el bundle (T01-T03) hacia las rutas que su propio
|
|
12
|
+
* locator declara, y solo borra rutas que un install anterior de este
|
|
13
|
+
* mismo módulo dejó registradas en `install-state.json`.
|
|
14
|
+
*
|
|
15
|
+
* @module scripts/lib/runtime-bundle-installer
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const crypto = require('node:crypto');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
const { esRuntimeBundleCompilado, leerArtefactoRuntime } = require('./runtime-compiler');
|
|
23
|
+
|
|
24
|
+
const ESTADO_DIRNAME = '.swl-runtime-state';
|
|
25
|
+
const TRANSACTION_FILE = 'transaction.json';
|
|
26
|
+
const STATE_FILE = 'install-state.json';
|
|
27
|
+
const BACKUPS_DIRNAME = 'backups';
|
|
28
|
+
const MODOS = new Set(['install', 'update', 'uninstall']);
|
|
29
|
+
|
|
30
|
+
function sha256(bytes) {
|
|
31
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function estadoDir(baseDir) {
|
|
35
|
+
return path.join(baseDir, ESTADO_DIRNAME);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function transactionPath(baseDir) {
|
|
39
|
+
return path.join(estadoDir(baseDir), TRANSACTION_FILE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function statePath(baseDir) {
|
|
43
|
+
return path.join(estadoDir(baseDir), STATE_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function backupsTransaccionPath(baseDir, transactionId) {
|
|
47
|
+
return path.join(estadoDir(baseDir), BACKUPS_DIRNAME, transactionId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function backupPath(baseDir, transactionId, rutaRelativa) {
|
|
51
|
+
return path.join(backupsTransaccionPath(baseDir, transactionId), rutaRelativa);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Los backups solo existen para revertir ESTA transacción; una vez cerrada (commit o rollback), sobran. */
|
|
55
|
+
function borrarBackupsTransaccion(baseDir, transactionId) {
|
|
56
|
+
fs.rmSync(backupsTransaccionPath(baseDir, transactionId), { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Punto de fallo determinístico para tests, mismo patrón que
|
|
61
|
+
* integrity-ledger.js/single-use-store.js: fuera de NODE_ENV=test la
|
|
62
|
+
* variable es ruido ignorado en producción salvo que además coincida,
|
|
63
|
+
* en cuyo caso es un error de configuración explícito (nunca una falla
|
|
64
|
+
* silenciosa en producción).
|
|
65
|
+
*/
|
|
66
|
+
function hitFault(punto) {
|
|
67
|
+
const objetivo = process.env.SWL_RUNTIME_INSTALLER_FAULT;
|
|
68
|
+
if (objetivo !== punto) return;
|
|
69
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
70
|
+
throw Object.assign(new Error('fault injection fuera de NODE_ENV=test'), { code: 'FAULT_CONFIG_INVALID' });
|
|
71
|
+
}
|
|
72
|
+
throw Object.assign(new Error(`FAULT_INJECTED: ${punto}`), { code: 'FAULT_INJECTED', punto });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Primitivas de filesystem — mismo patrón que scripts/generar-harness-ir.js.
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
function leerJSONOpcional(ruta) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(fs.readFileSync(ruta, 'utf8'));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.code === 'ENOENT') return null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escribirTemporalYFsync(ruta, contenido) {
|
|
89
|
+
fs.mkdirSync(path.dirname(ruta), { recursive: true });
|
|
90
|
+
const temporal = `${ruta}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
91
|
+
fs.writeFileSync(temporal, contenido, { flag: 'wx', mode: 0o600 });
|
|
92
|
+
const fd = fs.openSync(temporal, 'r+');
|
|
93
|
+
try {
|
|
94
|
+
fs.fsyncSync(fd);
|
|
95
|
+
} finally {
|
|
96
|
+
fs.closeSync(fd);
|
|
97
|
+
}
|
|
98
|
+
return temporal;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sincronizarDirectorio(directorio) {
|
|
102
|
+
const fd = fs.openSync(directorio, 'r');
|
|
103
|
+
try {
|
|
104
|
+
fs.fsyncSync(fd);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const noSoportado = process.platform === 'win32' && ['EPERM', 'EINVAL'].includes(error.code);
|
|
107
|
+
if (!noSoportado) throw error;
|
|
108
|
+
} finally {
|
|
109
|
+
fs.closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function limpiarSiExiste(ruta) {
|
|
114
|
+
try {
|
|
115
|
+
fs.unlinkSync(ruta);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error.code !== 'ENOENT') throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Escritura atómica: temp + fsync + rename + fsync de directorio. */
|
|
122
|
+
function publicarAtomico(destino, contenido) {
|
|
123
|
+
const temporal = escribirTemporalYFsync(destino, contenido);
|
|
124
|
+
fs.renameSync(temporal, destino);
|
|
125
|
+
sincronizarDirectorio(path.dirname(destino));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function adquirirLock(baseDir) {
|
|
129
|
+
fs.mkdirSync(estadoDir(baseDir), { recursive: true });
|
|
130
|
+
const lockPath = path.join(estadoDir(baseDir), 'transaction.lock');
|
|
131
|
+
let fd;
|
|
132
|
+
try {
|
|
133
|
+
hitFault('before-lock-open');
|
|
134
|
+
fd = fs.openSync(lockPath, 'wx', 0o600);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error.code === 'EEXIST') {
|
|
137
|
+
throw Object.assign(new Error('otra transacción del instalador está en curso'), { code: 'LOCK_HELD' });
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
// El PID queda escrito en el lock ANTES de que otro proceso pueda observarlo
|
|
142
|
+
// (la creación exclusiva ya se resolvió arriba): es lo único que permite
|
|
143
|
+
// distinguir después un lock huérfano (proceso muerto) de uno legítimamente
|
|
144
|
+
// en uso — sin esto ningún reclamo puede ser seguro (hallazgo del checker).
|
|
145
|
+
fs.writeSync(fd, String(process.pid));
|
|
146
|
+
fs.fsyncSync(fd);
|
|
147
|
+
return { fd, lockPath };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function pidDelLock(lockPath) {
|
|
151
|
+
let contenido;
|
|
152
|
+
try {
|
|
153
|
+
contenido = fs.readFileSync(lockPath, 'utf8').trim();
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const pid = Number(contenido);
|
|
158
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function procesoVive(pid) {
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pid, 0);
|
|
164
|
+
return true; // no lanzó: el proceso existe y es señalable
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return error.code === 'EPERM'; // ESRCH = no existe; EPERM = existe pero sin permiso (conservador: vivo)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Un lock sin PID legible (corrupto o de una versión anterior) se trata como huérfano recuperable. */
|
|
171
|
+
function lockEstaVivo(lockPath) {
|
|
172
|
+
const pid = pidDelLock(lockPath);
|
|
173
|
+
return pid !== null && procesoVive(pid);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function liberarLock(lock) {
|
|
177
|
+
try {
|
|
178
|
+
hitFault('before-lock-close');
|
|
179
|
+
fs.closeSync(lock.fd);
|
|
180
|
+
} catch {
|
|
181
|
+
/* best effort: un fd ya cerrado no debe impedir liberar el archivo del lock */
|
|
182
|
+
}
|
|
183
|
+
limpiarSiExiste(lock.lockPath);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Journal.
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
function escribirJournal(baseDir, journal) {
|
|
191
|
+
publicarAtomico(transactionPath(baseDir), JSON.stringify(journal, null, 2));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function leerJournal(baseDir) {
|
|
195
|
+
return leerJSONOpcional(transactionPath(baseDir));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function borrarJournal(baseDir) {
|
|
199
|
+
limpiarSiExiste(transactionPath(baseDir));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function crearTransactionId() {
|
|
203
|
+
return `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Estado instalado (install-state.json) — "el estado conoce cada archivo".
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function leerEstadoInstalado(baseDir) {
|
|
211
|
+
return leerJSONOpcional(statePath(baseDir)) || { schemaVersion: '1.0.0', runtimeId: null, files: [] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function escribirEstadoInstalado(baseDir, estado) {
|
|
215
|
+
publicarAtomico(statePath(baseDir), JSON.stringify(estado, null, 2));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function aplicarOperacionesAlEstado(estadoPrevio, runtimeId, operaciones) {
|
|
219
|
+
const porPath = new Map(estadoPrevio.files.map((f) => [f.path, f]));
|
|
220
|
+
for (const op of operaciones) {
|
|
221
|
+
if (op.accion === 'write') porPath.set(op.path, { path: op.path, sha256: op.nuevoSha256, bytes: op.bytes });
|
|
222
|
+
else porPath.delete(op.path);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
schemaVersion: '1.0.0',
|
|
226
|
+
runtimeId,
|
|
227
|
+
installedAt: new Date().toISOString(),
|
|
228
|
+
files: [...porPath.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Confinamiento de destino — el locator ya valida rutas relativas seguras
|
|
234
|
+
// (runtime-artifact-verifier.js), pero el instalador escribe a disco real:
|
|
235
|
+
// defensa en profundidad contra traversal si esa garantía llegara a fallar.
|
|
236
|
+
//
|
|
237
|
+
// El prefijo de string por sí solo NO basta: si un componente intermedio
|
|
238
|
+
// de baseDir ya existe como symlink/junction hacia fuera, `path.resolve`
|
|
239
|
+
// nunca lo detecta y la escritura/borrado real aterriza fuera de baseDir
|
|
240
|
+
// (hallazgo del checker independiente en la revisión de T08). Se camina
|
|
241
|
+
// cada componente con lstat ANTES de escribir y se rechaza cualquier
|
|
242
|
+
// enlace en la cadena; un componente que todavía no existe es seguro por
|
|
243
|
+
// construcción (mkdir/rename lo crean real, nunca siguen un enlace que
|
|
244
|
+
// no existe todavía).
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
function throwPathEscape(rutaRelativa, motivo) {
|
|
248
|
+
throw Object.assign(new Error(`${motivo}: ${rutaRelativa}`), { code: 'PATH_ESCAPE' });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function verificarSinEnlaces(raizReal, rutaRelativa) {
|
|
252
|
+
const segmentos = rutaRelativa.split('/');
|
|
253
|
+
let actual = raizReal;
|
|
254
|
+
for (let indice = 0; indice < segmentos.length; indice += 1) {
|
|
255
|
+
actual = path.join(actual, segmentos[indice]);
|
|
256
|
+
let stat;
|
|
257
|
+
try {
|
|
258
|
+
stat = fs.lstatSync(actual);
|
|
259
|
+
} catch {
|
|
260
|
+
return; // no existe aún: el resto de la ruta se crea fresca, sin enlaces que seguir
|
|
261
|
+
}
|
|
262
|
+
if (stat.isSymbolicLink()) throwPathEscape(rutaRelativa, 'ruta contiene un enlace simbólico');
|
|
263
|
+
const esUltimoSegmento = indice === segmentos.length - 1;
|
|
264
|
+
if (!esUltimoSegmento && !stat.isDirectory()) {
|
|
265
|
+
throwPathEscape(rutaRelativa, 'componente intermedio no es un directorio');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolverDestinoConfinado(baseDir, rutaRelativa) {
|
|
271
|
+
const raiz = path.resolve(baseDir);
|
|
272
|
+
const raizReal = fs.realpathSync(raiz); // baseDir siempre existe: el caller lo crea antes de instalar
|
|
273
|
+
verificarSinEnlaces(raizReal, rutaRelativa);
|
|
274
|
+
const destino = path.resolve(raizReal, rutaRelativa);
|
|
275
|
+
const prefijo = raizReal.endsWith(path.sep) ? raizReal : raizReal + path.sep;
|
|
276
|
+
// Exige un prefijo ESTRICTO: una ruta relativa válida siempre queda DENTRO
|
|
277
|
+
// de baseDir, nunca coincide con baseDir mismo (ese caso solo ocurre con
|
|
278
|
+
// "" o "." — entrada corrupta que jamás debe tratarse como archivo).
|
|
279
|
+
if (!destino.startsWith(prefijo)) throwPathEscape(rutaRelativa, 'ruta fuera de baseDir');
|
|
280
|
+
return destino;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function leerBytesDestino(destino) {
|
|
284
|
+
try {
|
|
285
|
+
const bytes = fs.readFileSync(destino);
|
|
286
|
+
return { existe: true, bytes, sha256: sha256(bytes) };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error.code === 'ENOENT') return { existe: false, bytes: null, sha256: null };
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Planificación (pura, sin I/O de escritura).
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
function planificarInstalacion(bundle, baseDir) {
|
|
298
|
+
const operaciones = [];
|
|
299
|
+
for (const entry of bundle.locator.entries) {
|
|
300
|
+
const destino = resolverDestinoConfinado(baseDir, entry.path);
|
|
301
|
+
const actual = leerBytesDestino(destino);
|
|
302
|
+
if (actual.existe && actual.sha256 === entry.sha256) continue;
|
|
303
|
+
operaciones.push({
|
|
304
|
+
path: entry.path, destino, accion: 'write',
|
|
305
|
+
hadPrior: actual.existe, nuevoSha256: entry.sha256, estado: 'pending',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return operaciones;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function planificarDesinstalacion(estadoInstalado, baseDir) {
|
|
312
|
+
return estadoInstalado.files.map((archivo) => ({
|
|
313
|
+
path: archivo.path,
|
|
314
|
+
destino: resolverDestinoConfinado(baseDir, archivo.path),
|
|
315
|
+
accion: 'delete',
|
|
316
|
+
hadPrior: true,
|
|
317
|
+
nuevoSha256: null,
|
|
318
|
+
estado: 'pending',
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Backup + stage + swap por operación.
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
function respaldarOperacion(baseDir, transactionId, op) {
|
|
327
|
+
if (!op.hadPrior) return;
|
|
328
|
+
const actual = leerBytesDestino(op.destino);
|
|
329
|
+
if (!actual.existe) return; // ya no existe (borrado manual entre plan y ejecución): nada que respaldar
|
|
330
|
+
publicarAtomico(backupPath(baseDir, transactionId, op.path), actual.bytes);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Escribe el temporal y persiste su nombre EXACTO en el journal antes del
|
|
335
|
+
* rename — la recuperación limpia exactamente ese archivo, nunca un patrón
|
|
336
|
+
* de prefijo sobre el directorio (un glob `${base}.tmp-*` puede coincidir
|
|
337
|
+
* con un archivo real del usuario, ej. `CLAUDE.md.tmp-notas-personales`;
|
|
338
|
+
* hallazgo del checker independiente).
|
|
339
|
+
*/
|
|
340
|
+
function aplicarEscritura(baseDir, journal, bundle, op) {
|
|
341
|
+
const bytes = leerArtefactoRuntime(bundle, op.path);
|
|
342
|
+
// Defensa final antes de tocar disco: si el bundle entregara bytes que no
|
|
343
|
+
// coinciden con el hash que el propio locator declaró para esa ruta, es
|
|
344
|
+
// preferible fallar cerrado (la transacción completa se revierte) a
|
|
345
|
+
// instalar contenido no verificado.
|
|
346
|
+
if (sha256(bytes) !== op.nuevoSha256) {
|
|
347
|
+
throw Object.assign(new Error(`bytes del bundle no coinciden con el locator: ${op.path}`), { code: 'HASH_MISMATCH' });
|
|
348
|
+
}
|
|
349
|
+
op.temporal = escribirTemporalYFsync(op.destino, bytes);
|
|
350
|
+
escribirJournal(baseDir, journal); // persiste el nombre del temporal ANTES del punto de falla simulado
|
|
351
|
+
hitFault('after-stage');
|
|
352
|
+
fs.renameSync(op.temporal, op.destino);
|
|
353
|
+
sincronizarDirectorio(path.dirname(op.destino));
|
|
354
|
+
op.temporal = null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function aplicarBorrado(op) {
|
|
358
|
+
limpiarSiExiste(op.destino);
|
|
359
|
+
sincronizarDirectorio(path.dirname(op.destino));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function ejecutarOperacion(bundle, baseDir, journal, op) {
|
|
363
|
+
respaldarOperacion(baseDir, journal.transactionId, op);
|
|
364
|
+
hitFault('after-backup');
|
|
365
|
+
op.estado = 'backed-up';
|
|
366
|
+
op.temporal = null;
|
|
367
|
+
escribirJournal(baseDir, journal);
|
|
368
|
+
|
|
369
|
+
if (op.accion === 'write') aplicarEscritura(baseDir, journal, bundle, op);
|
|
370
|
+
else aplicarBorrado(op);
|
|
371
|
+
hitFault('after-swap');
|
|
372
|
+
|
|
373
|
+
op.estado = 'swapped';
|
|
374
|
+
escribirJournal(baseDir, journal);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Rollback — reversible desde cualquier estado por-operación intermedio.
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
function restaurarDesdeBackup(baseDir, transactionId, op) {
|
|
382
|
+
const respaldo = backupPath(baseDir, transactionId, op.path);
|
|
383
|
+
let bytes;
|
|
384
|
+
try {
|
|
385
|
+
bytes = fs.readFileSync(respaldo);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error.code !== 'ENOENT') throw error;
|
|
388
|
+
// hadPrior=true se fijó en planificación, pero el archivo ya no existía
|
|
389
|
+
// al momento de ejecutar (respaldarOperacion no crea backup en ese
|
|
390
|
+
// caso): no hay nada que restaurar, así que la reversión segura es
|
|
391
|
+
// asegurar que el destino tampoco exista (Nemesis pass2, F-02). Sin
|
|
392
|
+
// esto, un ENOENT aquí bloqueaba el instalador para siempre — cada
|
|
393
|
+
// recuperar()/aplicarBundle() futuro repetía el mismo throw.
|
|
394
|
+
limpiarSiExiste(op.destino);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
publicarAtomico(op.destino, bytes);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function revertirOperacion(baseDir, transactionId, op) {
|
|
401
|
+
if (op.estado === 'pending') return;
|
|
402
|
+
if (op.estado === 'backed-up' || op.estado === 'swapped') {
|
|
403
|
+
if (op.hadPrior) restaurarDesdeBackup(baseDir, transactionId, op);
|
|
404
|
+
else limpiarSiExiste(op.destino);
|
|
405
|
+
}
|
|
406
|
+
if (op.temporal) limpiarSiExiste(op.temporal);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function revertirTransaccion(baseDir, journal) {
|
|
410
|
+
for (const op of journal.operaciones) revertirOperacion(baseDir, journal.transactionId, op);
|
|
411
|
+
journal.status = 'rolled-back';
|
|
412
|
+
escribirJournal(baseDir, journal);
|
|
413
|
+
borrarJournal(baseDir);
|
|
414
|
+
borrarBackupsTransaccion(baseDir, journal.transactionId);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Recuperación idempotente.
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Retoma cualquier transacción que haya quedado pendiente en `baseDir`
|
|
423
|
+
* (crash, kill, fault inyectada). Sin journal pendiente es un no-op —
|
|
424
|
+
* llamarla repetidamente es seguro por diseño. `status: 'in-progress'`
|
|
425
|
+
* (operaciones a medias) se REVIERTE; `status: 'operations-done'`
|
|
426
|
+
* (operaciones completas, falta cerrar el libro) se TERMINA — nunca al
|
|
427
|
+
* revés, o los archivos ya correctos en disco se revertirían a un estado
|
|
428
|
+
* viejo mientras install-state.json quedaría con el valor nuevo (o
|
|
429
|
+
* viceversa en uninstall), divergencia permanente (Nemesis pass2, F-01).
|
|
430
|
+
*/
|
|
431
|
+
function recuperar(baseDir) {
|
|
432
|
+
const journal = leerJournal(baseDir);
|
|
433
|
+
if (!journal) return { recuperado: false };
|
|
434
|
+
if (journal.status === 'operations-done') {
|
|
435
|
+
const lock = adquirirLockToleranteAPropio(baseDir);
|
|
436
|
+
try {
|
|
437
|
+
finalizarTransaccion(baseDir, journal);
|
|
438
|
+
} finally {
|
|
439
|
+
liberarLock(lock);
|
|
440
|
+
}
|
|
441
|
+
return { recuperado: true, transactionId: journal.transactionId };
|
|
442
|
+
}
|
|
443
|
+
if (journal.status !== 'in-progress') return { recuperado: false };
|
|
444
|
+
const lock = adquirirLockToleranteAPropio(baseDir);
|
|
445
|
+
try {
|
|
446
|
+
revertirTransaccion(baseDir, journal);
|
|
447
|
+
} finally {
|
|
448
|
+
liberarLock(lock);
|
|
449
|
+
}
|
|
450
|
+
return { recuperado: true, transactionId: journal.transactionId };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* `recuperar()` puede correr como parte de aplicarBundle() sobre un lock ya
|
|
455
|
+
* huérfano (el proceso que lo tomó murió sin liberarlo). Reclamar SOLO
|
|
456
|
+
* procede cuando el PID grabado en el lock ya no vive — un lock cuyo dueño
|
|
457
|
+
* sigue vivo debe seguir bloqueando (LOCK_HELD real), nunca robarse: sin
|
|
458
|
+
* esta verificación dos instalaciones concurrentes sobre el mismo baseDir
|
|
459
|
+
* corren en paralelo y corrompen el journal compartido (hallazgo del
|
|
460
|
+
* checker independiente).
|
|
461
|
+
*/
|
|
462
|
+
function adquirirLockToleranteAPropio(baseDir) {
|
|
463
|
+
try {
|
|
464
|
+
return adquirirLock(baseDir);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (error.code !== 'LOCK_HELD') throw error;
|
|
467
|
+
const lockPath = path.join(estadoDir(baseDir), 'transaction.lock');
|
|
468
|
+
if (lockEstaVivo(lockPath)) throw error;
|
|
469
|
+
limpiarSiExiste(lockPath);
|
|
470
|
+
return adquirirLock(baseDir);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// API pública.
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
function validarOpciones(opciones) {
|
|
479
|
+
if (!opciones || typeof opciones.baseDir !== 'string' || opciones.baseDir.length === 0) {
|
|
480
|
+
throw new TypeError('opciones.baseDir es requerido');
|
|
481
|
+
}
|
|
482
|
+
if (!MODOS.has(opciones.modo)) {
|
|
483
|
+
throw new TypeError(`opciones.modo debe ser uno de: ${[...MODOS].join(', ')}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function planificar(bundle, opciones) {
|
|
488
|
+
if (opciones.modo === 'uninstall') {
|
|
489
|
+
return planificarDesinstalacion(leerEstadoInstalado(opciones.baseDir), opciones.baseDir);
|
|
490
|
+
}
|
|
491
|
+
if (!esRuntimeBundleCompilado(bundle)) throw new TypeError('se requiere bundle verificado (T03)');
|
|
492
|
+
return planificarInstalacion(bundle, opciones.baseDir);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function resolverRuntimeId(bundle, opciones) {
|
|
496
|
+
return opciones.modo === 'uninstall'
|
|
497
|
+
? leerEstadoInstalado(opciones.baseDir).runtimeId
|
|
498
|
+
: bundle.runtimeId;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Cola de una transacción cuyas operaciones YA llegaron a 'swapped' (los
|
|
503
|
+
* archivos en disco son correctos): recalcula el estado instalado desde
|
|
504
|
+
* disco real y cierra el journal. Reejecutable sin efectos adicionales —
|
|
505
|
+
* `escribirEstadoInstalado`/`escribirJournal` son publicaciones atómicas
|
|
506
|
+
* idempotentes, así que llamarla dos veces sobre el mismo journal produce
|
|
507
|
+
* el mismo resultado (necesario para que `recuperar()` pueda retomarla
|
|
508
|
+
* tras un crash a mitad de esta cola, Nemesis pass2 F-01).
|
|
509
|
+
*/
|
|
510
|
+
function finalizarTransaccion(baseDir, journal) {
|
|
511
|
+
const estadoPrevio = leerEstadoInstalado(baseDir);
|
|
512
|
+
// El estado persistido refleja lo que REALMENTE quedó en disco (tamaño y
|
|
513
|
+
// hash recalculados del archivo ya swapped), no lo que el locator declaró
|
|
514
|
+
// — "el estado conoce cada archivo" exige verificar, no solo confiar.
|
|
515
|
+
const opsConBytes = journal.operaciones.map((op) => {
|
|
516
|
+
if (op.accion !== 'write') return op;
|
|
517
|
+
const bytesEnDisco = fs.readFileSync(op.destino);
|
|
518
|
+
return { ...op, bytes: bytesEnDisco.length, nuevoSha256: sha256(bytesEnDisco) };
|
|
519
|
+
});
|
|
520
|
+
escribirEstadoInstalado(baseDir, aplicarOperacionesAlEstado(estadoPrevio, journal.runtimeId, opsConBytes));
|
|
521
|
+
|
|
522
|
+
journal.status = 'completed';
|
|
523
|
+
escribirJournal(baseDir, journal);
|
|
524
|
+
borrarJournal(baseDir);
|
|
525
|
+
borrarBackupsTransaccion(baseDir, journal.transactionId);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function ejecutarTransaccion(bundle, opciones, operaciones) {
|
|
529
|
+
const baseDir = baseDirDe(opciones);
|
|
530
|
+
const journal = {
|
|
531
|
+
schemaVersion: '1.0.0', transactionId: crearTransactionId(),
|
|
532
|
+
runtimeId: resolverRuntimeId(bundle, opciones), modo: opciones.modo,
|
|
533
|
+
startedAt: new Date().toISOString(), status: 'in-progress', operaciones,
|
|
534
|
+
};
|
|
535
|
+
escribirJournal(baseDir, journal);
|
|
536
|
+
for (const op of journal.operaciones) ejecutarOperacion(bundle, baseDir, journal, op);
|
|
537
|
+
|
|
538
|
+
// Punto de conmutación: a partir de aquí los archivos en disco YA son
|
|
539
|
+
// correctos (todas las operaciones llegaron a 'swapped'). Un crash
|
|
540
|
+
// DESPUÉS de este punto debe TERMINAR la transacción (finalizarTransaccion
|
|
541
|
+
// es idempotente), nunca revertir los archivos — sin este estado
|
|
542
|
+
// intermedio, recuperar() no podía distinguir "operaciones a medias"
|
|
543
|
+
// (revertir) de "operaciones completas, falta cerrar el libro" (terminar),
|
|
544
|
+
// y un crash entre escribir install-state.json y cerrar el journal dejaba
|
|
545
|
+
// install-state.json desincronizado del contenido real en disco de forma
|
|
546
|
+
// permanente, peor aún en uninstall (Nemesis pass2, F-01).
|
|
547
|
+
journal.status = 'operations-done';
|
|
548
|
+
escribirJournal(baseDir, journal);
|
|
549
|
+
hitFault('after-operations');
|
|
550
|
+
|
|
551
|
+
finalizarTransaccion(baseDir, journal);
|
|
552
|
+
return journal;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function baseDirDe(opciones) {
|
|
556
|
+
return opciones.baseDir;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Instala, actualiza o desinstala un bundle Core-3 en `opciones.baseDir` de
|
|
561
|
+
* forma transaccional. `bundle` es obligatorio para install/update (debe
|
|
562
|
+
* venir de `compilarRuntimeBundle`, T03) e ignorado en uninstall (usa el
|
|
563
|
+
* install-state.json previo). `opciones.dryRun` calcula el plan sin tocar
|
|
564
|
+
* disco — ni siquiera intenta recuperar una transacción huérfana.
|
|
565
|
+
*/
|
|
566
|
+
function aplicarBundle(bundle, opciones) {
|
|
567
|
+
validarOpciones(opciones);
|
|
568
|
+
if (opciones.dryRun) return { dryRun: true, operaciones: planificar(bundle, opciones) };
|
|
569
|
+
|
|
570
|
+
const recuperacion = recuperar(opciones.baseDir);
|
|
571
|
+
const lock = adquirirLockToleranteAPropio(opciones.baseDir);
|
|
572
|
+
try {
|
|
573
|
+
const operaciones = planificar(bundle, opciones);
|
|
574
|
+
if (operaciones.length === 0) return { recuperacion, operaciones: [], sinCambios: true };
|
|
575
|
+
const journal = ejecutarTransaccion(bundle, opciones, operaciones);
|
|
576
|
+
return { recuperacion, operaciones: journal.operaciones, sinCambios: false };
|
|
577
|
+
} finally {
|
|
578
|
+
liberarLock(lock);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
module.exports = {
|
|
583
|
+
aplicarBundle,
|
|
584
|
+
recuperar,
|
|
585
|
+
leerEstadoInstalado,
|
|
586
|
+
};
|