blun-king-cli 9.1.602 → 9.1.604
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 +118 -99
- package/LIESMICH.txt +74 -57
- package/README.md +119 -76
- package/bin/agentspine-king-goal-inbox.mjs +127 -127
- package/bin/agentspine-king-goal-intake.mjs +106 -106
- package/bin/agentspine-king-host-runner.mjs +109 -109
- package/bin/agentspine-king-snapshot-policy.cjs +80 -80
- package/bin/agentspine-king-status-policy.mjs +226 -226
- package/bin/agentspine-king-worker-host.mjs +160 -160
- package/bin/core-bootstrap.js +77 -77
- package/bin/launcher-mode.js +93 -93
- package/bin/launcher-restart-policy.cjs +131 -131
- package/bin/launcher-runtime.js +491 -491
- package/bin/managed-node.js +186 -186
- package/bin/managed-plugin-selection.cjs +115 -115
- package/bin/native-module-repair.js +201 -201
- package/bin/node-runtime.js +34 -34
- package/bin/node-version.js +49 -49
- package/bin/plugin-bootstrap.js +162 -162
- package/bin/private-paths.js +1606 -1606
- package/bin/runtime-exit-ledger.cjs +144 -144
- package/bin/runtime-exit-ledger.d.cts +23 -23
- package/bin/standard-tools-bootstrap.js +272 -272
- package/bin/turn-thinking-policy.cjs +82 -82
- package/bin/update-copy.js +200 -200
- package/bin/update-lease.js +597 -597
- package/bin/update-notice.js +1568 -1568
- package/bin/verify-agent-behavior.cjs +122 -122
- package/bin/verify-agent-components.cjs +104 -104
- package/bin/verify-bundled-agent-sources.cjs +98 -76
- package/bin/windows-node-crash-dump.cjs +289 -289
- package/blun.mjs +8041 -6407
- package/codebase-index/codebase_index.py +470 -470
- package/dist-web/assets/{CodeBlockNode-CXnTLMNr.js → CodeBlockNode-TKKkJX8L.js} +1 -1
- package/dist-web/assets/{CodeBlockShell.vue_vue_type_style_index_0_lang-CCF8Ntnq.js → CodeBlockShell.vue_vue_type_style_index_0_lang-CUj2Ds5x.js} +1 -1
- package/dist-web/assets/DesignSystemView-DwK1aQMo.js +12 -0
- package/dist-web/assets/DesignSystemView-szjTj7ns.css +1 -0
- package/dist-web/assets/{Tooltip-CFQEzqC-.js → Tooltip-CTWv7WCU.js} +1 -1
- package/dist-web/assets/{abnfDiagram-N423BO3Z-Z1APZG5m.js → abnfDiagram-N423BO3Z-DA8V8Zjh.js} +1 -1
- package/dist-web/assets/{arc-C4VyWloT.js → arc-YtddKbdY.js} +1 -1
- package/dist-web/assets/{architectureDiagram-T3A2C74G-DpddbUcM.js → architectureDiagram-T3A2C74G-CGEiObmo.js} +1 -1
- package/dist-web/assets/{blockDiagram-VBNYF7ZC-BqccOSMr.js → blockDiagram-VBNYF7ZC-Bd5Vt50e.js} +1 -1
- package/dist-web/assets/{c4Diagram-5PPSVZJV-OHAcJrTy.js → c4Diagram-5PPSVZJV-D2ApPM54.js} +1 -1
- package/dist-web/assets/channel-BnluexuN.js +1 -0
- package/dist-web/assets/{chunk-2GRJ4B5K-DqJHs8Fw.js → chunk-2GRJ4B5K-BDcIEZSl.js} +1 -1
- package/dist-web/assets/{chunk-2Q5K7J3B-B1nCd91o.js → chunk-2Q5K7J3B-6aSabCX7.js} +1 -1
- package/dist-web/assets/{chunk-5RXB4S5H-neG70kZx.js → chunk-5RXB4S5H-BFRQvIJS.js} +1 -1
- package/dist-web/assets/{chunk-5VM5RSS4-l7biGqeH.js → chunk-5VM5RSS4-ChAa-DNY.js} +1 -1
- package/dist-web/assets/{chunk-6Q2QTUOP-BO-F11Gs.js → chunk-6Q2QTUOP-BIfrb2Jc.js} +1 -1
- package/dist-web/assets/{chunk-GF5L2VYU-BtC69fw0.js → chunk-GF5L2VYU-BR0gvtWI.js} +1 -1
- package/dist-web/assets/{chunk-JWPE2WC7-DIOubEqr.js → chunk-JWPE2WC7-BgXKsx5E.js} +1 -1
- package/dist-web/assets/{chunk-KBJHAD2P-DDUeH_Ba.js → chunk-KBJHAD2P-I8mrZhpz.js} +1 -1
- package/dist-web/assets/{chunk-RYQCIY6F-B9gFMVbS.js → chunk-RYQCIY6F-BMPaem92.js} +1 -1
- package/dist-web/assets/{chunk-XXDRQBXY-Bn1Xz6TQ.js → chunk-XXDRQBXY-COy0J-0j.js} +1 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BliFdSD9.js +1 -0
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BliFdSD9.js +1 -0
- package/dist-web/assets/{cose-bilkent-JH36ORCC-DqigO3T4.js → cose-bilkent-JH36ORCC-MosTTRdo.js} +1 -1
- package/dist-web/assets/{cynefin-VYW2F7L2-rpD93tQ-.js → cynefin-VYW2F7L2-5tjPgrkP.js} +1 -1
- package/dist-web/assets/{cynefinDiagram-MW4NZA55-DbTqnkVJ.js → cynefinDiagram-MW4NZA55-CbDcdInR.js} +1 -1
- package/dist-web/assets/{dagre-VZM6K2ZE-DcM8aEVY.js → dagre-VZM6K2ZE-YaBom1ZH.js} +1 -1
- package/dist-web/assets/{diagram-7IWD3JNH-UymgsBNa.js → diagram-7IWD3JNH-Da4m-Hgz.js} +1 -1
- package/dist-web/assets/{diagram-B4RE2ZJO-BiYu5WPF.js → diagram-B4RE2ZJO-CAUUegxg.js} +1 -1
- package/dist-web/assets/{diagram-LBJQPF4R-BD3kkgOO.js → diagram-LBJQPF4R-o21mWKG4.js} +1 -1
- package/dist-web/assets/{diagram-Q27KOJAE-D1mYQ_cI.js → diagram-Q27KOJAE-BA3z3sXf.js} +1 -1
- package/dist-web/assets/{diagram-UB23O5K3-BtX8u6HG.js → diagram-UB23O5K3-CGRdSSDK.js} +1 -1
- package/dist-web/assets/{ebnfDiagram-BXEA7PRR-CBAnFkRF.js → ebnfDiagram-BXEA7PRR-Bw-iU9FP.js} +1 -1
- package/dist-web/assets/{erDiagram-JOGREHBK-3vKcWsxm.js → erDiagram-JOGREHBK-BwM1yiR0.js} +1 -1
- package/dist-web/assets/{flowDiagram-UKHOOZJN-D9wBbHm2.js → flowDiagram-UKHOOZJN-z05eOR1Q.js} +1 -1
- package/dist-web/assets/{ganttDiagram-PKOTCBZU-Cd_dViF-.js → ganttDiagram-PKOTCBZU-BMUubNTl.js} +1 -1
- package/dist-web/assets/{gitGraphDiagram-DS77QQ5N-BiEEtTo7.js → gitGraphDiagram-DS77QQ5N-Dccbi1wU.js} +1 -1
- package/dist-web/assets/{index-DdEakFJM.js → index-BBbC5LRF.js} +1 -1
- package/dist-web/assets/{index-BmKj7CB1.css → index-C9eDH3u-.css} +1 -1
- package/dist-web/assets/{index-Ba1OvLjf.js → index-D48p0M2S.js} +2 -2
- package/dist-web/assets/{index-CiPA8Kex.js → index-h0KEOmkU.js} +14 -14
- package/dist-web/assets/{index10-CNr4U-Nw.js → index10-Qbuw1N6u.js} +1 -1
- package/dist-web/assets/{index11-Dto_yZNz.js → index11-g8v025wN.js} +1 -1
- package/dist-web/assets/{index5-B2PldaO2.js → index5-BYNcV8BY.js} +1 -1
- package/dist-web/assets/{index6-CBdZmmyx.js → index6-BetANtnV.js} +1 -1
- package/dist-web/assets/{index7-Dz9oCsUF.js → index7-BcPbgCKN.js} +1 -1
- package/dist-web/assets/{index8-B6k25jtU.js → index8-B1lp-RvF.js} +2 -2
- package/dist-web/assets/{infoDiagram-6WML65LV-D4ph2sIH.js → infoDiagram-6WML65LV-BF_pbUnN.js} +1 -1
- package/dist-web/assets/{ishikawaDiagram-WSZJBQD7-DjKjbMm9.js → ishikawaDiagram-WSZJBQD7-Dedv4urS.js} +1 -1
- package/dist-web/assets/{journeyDiagram-NVQOT4AX-DttHDfPQ.js → journeyDiagram-NVQOT4AX-B1Ex3b6o.js} +1 -1
- package/dist-web/assets/{kanban-definition-27J2QSJJ-C5gB-W0d.js → kanban-definition-27J2QSJJ-BtCGliBH.js} +1 -1
- package/dist-web/assets/{linear-BRGe4-d6.js → linear-ByItc8Qr.js} +1 -1
- package/dist-web/assets/{mermaid.core-D-9Sx1No.js → mermaid.core-B1M2O7nN.js} +4 -4
- package/dist-web/assets/{mindmap-definition-FAOFIHXS-Cg_hoVIq.js → mindmap-definition-FAOFIHXS-D0UHKUyj.js} +1 -1
- package/dist-web/assets/{pegDiagram-VL7TDLO6-CSX9hitD.js → pegDiagram-VL7TDLO6-D7zYCVmG.js} +1 -1
- package/dist-web/assets/{pieDiagram-7S7Q4E2Y-0L0mxahZ.js → pieDiagram-7S7Q4E2Y-Bd0i9CHa.js} +1 -1
- package/dist-web/assets/{quadrantDiagram-CIZ2JOQS-U483z0er.js → quadrantDiagram-CIZ2JOQS-MG-Ug47d.js} +1 -1
- package/dist-web/assets/{railroadDiagram-AXF67PYL-D80a2_DJ.js → railroadDiagram-AXF67PYL-AZdl90Gj.js} +1 -1
- package/dist-web/assets/{requirementDiagram-LRYGKXZP-CeyHfq8t.js → requirementDiagram-LRYGKXZP-DjplF9kt.js} +1 -1
- package/dist-web/assets/{sankeyDiagram-W5VNT64P-CCISkkrR.js → sankeyDiagram-W5VNT64P-Bi9Zgfjs.js} +1 -1
- package/dist-web/assets/{sequenceDiagram-SI44F4Z6-jQfm2XS7.js → sequenceDiagram-SI44F4Z6-ChEpdCvb.js} +1 -1
- package/dist-web/assets/{sizeCapture-X5ZJPWSS-lRZsmCoS.js → sizeCapture-X5ZJPWSS-B5RFUdaa.js} +1 -1
- package/dist-web/assets/{stateDiagram-OKZ733FA-ClCgTdUm.js → stateDiagram-OKZ733FA-BkRblSKp.js} +1 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fVkQcACW.js +1 -0
- package/dist-web/assets/{swimlanes-SLNWSIFB-BCdOUN05.js → swimlanes-SLNWSIFB-DLXH6Zhc.js} +2 -2
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-Bg-BiZJQ.js +8 -0
- package/dist-web/assets/{timeline-definition-Z64GVDOM-DpWutSKa.js → timeline-definition-Z64GVDOM-C_N88siO.js} +1 -1
- package/dist-web/assets/{vennDiagram-T6HMQDX7-DVC5JVsc.js → vennDiagram-T6HMQDX7-DMsnvDt0.js} +1 -1
- package/dist-web/assets/{vue.runtime.esm-bundler-BwwRtQwQ.js → vue.runtime.esm-bundler-B-3uIy85.js} +1 -1
- package/dist-web/assets/{wardleyDiagram-T6FBY63Y-BK2mrq7L.js → wardleyDiagram-T6FBY63Y-C9SJIPzq.js} +1 -1
- package/dist-web/assets/{xychartDiagram-ELKLHX3M-DUJu9F7-.js → xychartDiagram-ELKLHX3M-DZLWA6Go.js} +1 -1
- package/dist-web/index.html +32 -32
- package/dist-web/vis/index.html.gz +0 -0
- package/package.json +1 -1
- package/scripts/fix-node-pty-perms.js +53 -53
- package/standard-skills/blun-app-design-system/SKILL.md +30 -30
- package/standard-skills/blun-app-design-system/provenance.json +17 -17
- package/standard-skills/blun-app-design-system/references/tokens.md +48 -48
- package/standard-skills/blun-app-design-system/scripts/check-blun-design.mjs +49 -49
- package/telegram-plugin/blun.plugin.json +20 -20
- package/telegram-plugin/commands/access.md +37 -37
- package/telegram-plugin/dist/bridge.mjs +117 -100
- package/telegram-plugin/dist/mcp-server.mjs +185 -161
- package/worker-host.mjs +1458 -1022
- package/dist-web/assets/DesignSystemView-6HgAUEmC.css +0 -1
- package/dist-web/assets/DesignSystemView-BbMOu-XO.js +0 -12
- package/dist-web/assets/channel-B2VOQOI-.js +0 -1
- package/dist-web/assets/classDiagram-JCYQIIEL-BA1TjAxj.js +0 -1
- package/dist-web/assets/classDiagram-v2-OCEON4UE-BA1TjAxj.js +0 -1
- package/dist-web/assets/stateDiagram-v2-UEYNNEHI-fi_lrTAx.js +0 -1
- package/dist-web/assets/swimlanesDiagram-ULZ7WXOC-BUd_uYK3.js +0 -8
package/bin/managed-node.js
CHANGED
|
@@ -1,186 +1,186 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('node:crypto');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const https = require('node:https');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
const { spawnSync } = require('node:child_process');
|
|
9
|
-
|
|
10
|
-
const NODE_VERSION = '24.15.0';
|
|
11
|
-
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
|
|
12
|
-
const DISTRIBUTIONS = {
|
|
13
|
-
'darwin-arm64': {
|
|
14
|
-
archive: `node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
|
|
15
|
-
binary: path.join('bin', 'node'),
|
|
16
|
-
format: 'tar.gz',
|
|
17
|
-
sha256: '372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4',
|
|
18
|
-
},
|
|
19
|
-
'darwin-x64': {
|
|
20
|
-
archive: `node-v${NODE_VERSION}-darwin-x64.tar.gz`,
|
|
21
|
-
binary: path.join('bin', 'node'),
|
|
22
|
-
format: 'tar.gz',
|
|
23
|
-
sha256: 'ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b',
|
|
24
|
-
},
|
|
25
|
-
'win32-arm64': {
|
|
26
|
-
archive: `node-v${NODE_VERSION}-win-arm64.zip`,
|
|
27
|
-
binary: 'node.exe',
|
|
28
|
-
format: 'zip',
|
|
29
|
-
sha256: 'c9eb7402eda26e2ba7e44b6727fc85a8de56c5095b1f71ebd3062892211aa116',
|
|
30
|
-
},
|
|
31
|
-
'win32-x64': {
|
|
32
|
-
archive: `node-v${NODE_VERSION}-win-x64.zip`,
|
|
33
|
-
binary: 'node.exe',
|
|
34
|
-
format: 'zip',
|
|
35
|
-
sha256: 'cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62',
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
function distributionFor(platform, arch) {
|
|
40
|
-
return DISTRIBUTIONS[`${platform}-${arch}`] || null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function sha256File(file) {
|
|
44
|
-
const hash = crypto.createHash('sha256');
|
|
45
|
-
hash.update(fs.readFileSync(file));
|
|
46
|
-
return hash.digest('hex');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function downloadFile(url, destination, redirectsLeft = 3) {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
const request = https.get(url, { timeout: 30_000 }, (response) => {
|
|
52
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
53
|
-
response.resume();
|
|
54
|
-
if (redirectsLeft <= 0) {
|
|
55
|
-
reject(new Error('Zu viele Weiterleitungen beim Node.js-Download.'));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const redirect = new URL(response.headers.location, url);
|
|
59
|
-
if (redirect.protocol !== 'https:') {
|
|
60
|
-
reject(new Error('Unsichere Weiterleitung beim Node.js-Download abgelehnt.'));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
downloadFile(redirect.href, destination, redirectsLeft - 1).then(resolve, reject);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (response.statusCode !== 200) {
|
|
68
|
-
response.resume();
|
|
69
|
-
reject(new Error(`Node.js-Download antwortete mit HTTP ${response.statusCode}.`));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const contentLength = Number(response.headers['content-length'] || 0);
|
|
74
|
-
if (contentLength > MAX_ARCHIVE_BYTES) {
|
|
75
|
-
response.destroy();
|
|
76
|
-
reject(new Error('Node.js-Download ist groesser als erwartet.'));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let received = 0;
|
|
81
|
-
const output = fs.createWriteStream(destination, { flags: 'wx' });
|
|
82
|
-
response.on('data', (chunk) => {
|
|
83
|
-
received += chunk.length;
|
|
84
|
-
if (received > MAX_ARCHIVE_BYTES) {
|
|
85
|
-
response.destroy(new Error('Node.js-Download ist groesser als erwartet.'));
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
response.on('error', reject);
|
|
89
|
-
output.on('error', reject);
|
|
90
|
-
output.on('finish', () => output.close(resolve));
|
|
91
|
-
response.pipe(output);
|
|
92
|
-
});
|
|
93
|
-
request.on('error', reject);
|
|
94
|
-
request.on('timeout', () => request.destroy(new Error('Zeitlimit beim Node.js-Download erreicht.')));
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function usableNode(binary) {
|
|
99
|
-
if (!fs.existsSync(binary)) return false;
|
|
100
|
-
const result = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 5_000 });
|
|
101
|
-
return result.status === 0 && result.stdout.trim() === `v${NODE_VERSION}`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function archiveRootName(distribution) {
|
|
105
|
-
const suffix = distribution.format === 'zip' ? '.zip' : '.tar.gz';
|
|
106
|
-
if (!distribution.archive.endsWith(suffix)) {
|
|
107
|
-
throw new Error('Unbekanntes Format der Node.js-Laufzeit.');
|
|
108
|
-
}
|
|
109
|
-
return distribution.archive.slice(0, -suffix.length);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function unpackArchive(archive, destination, distribution, platform) {
|
|
113
|
-
const command = platform === 'win32'
|
|
114
|
-
? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
|
115
|
-
: '/usr/bin/tar';
|
|
116
|
-
const args = distribution.format === 'zip'
|
|
117
|
-
? ['-xf', archive, '-C', destination]
|
|
118
|
-
: ['-xzf', archive, '-C', destination];
|
|
119
|
-
return spawnSync(command, args, {
|
|
120
|
-
encoding: 'utf8',
|
|
121
|
-
timeout: 120_000,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function ensureManagedNode(blunHome, platform, arch) {
|
|
126
|
-
const distribution = distributionFor(platform, arch);
|
|
127
|
-
if (!distribution) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Automatische Node.js-Einrichtung wird fuer ${platform}-${arch} noch nicht unterstuetzt.`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const installDir = path.join(
|
|
134
|
-
blunHome,
|
|
135
|
-
'runtime',
|
|
136
|
-
'node',
|
|
137
|
-
`v${NODE_VERSION}-${platform}-${arch}`,
|
|
138
|
-
);
|
|
139
|
-
const binary = path.join(installDir, distribution.binary);
|
|
140
|
-
if (usableNode(binary)) return binary;
|
|
141
|
-
|
|
142
|
-
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-node-runtime-'));
|
|
143
|
-
try {
|
|
144
|
-
const archive = path.join(staging, distribution.archive);
|
|
145
|
-
await downloadFile(
|
|
146
|
-
`https://nodejs.org/dist/v${NODE_VERSION}/${distribution.archive}`,
|
|
147
|
-
archive,
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const actualSha = sha256File(archive);
|
|
151
|
-
if (actualSha !== distribution.sha256) {
|
|
152
|
-
throw new Error('Die SHA-256-Pruefsumme der Node.js-Laufzeit stimmt nicht.');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const extractedName = archiveRootName(distribution);
|
|
156
|
-
const extractedDir = path.join(staging, extractedName);
|
|
157
|
-
const unpacked = unpackArchive(archive, staging, distribution, platform);
|
|
158
|
-
if (unpacked.status !== 0 || !usableNode(path.join(extractedDir, distribution.binary))) {
|
|
159
|
-
const detail = (unpacked.stderr || '').trim();
|
|
160
|
-
throw new Error(
|
|
161
|
-
`Die Node.js-Laufzeit konnte nicht entpackt werden${detail ? `: ${detail}` : '.'}`,
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
fs.mkdirSync(path.dirname(installDir), { recursive: true });
|
|
166
|
-
if (usableNode(binary)) return binary;
|
|
167
|
-
fs.rmSync(installDir, { recursive: true, force: true });
|
|
168
|
-
fs.renameSync(extractedDir, installDir);
|
|
169
|
-
if (!usableNode(binary)) throw new Error('Die installierte Node.js-Laufzeit startet nicht.');
|
|
170
|
-
return binary;
|
|
171
|
-
} finally {
|
|
172
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
module.exports = {
|
|
177
|
-
DISTRIBUTIONS,
|
|
178
|
-
MAX_ARCHIVE_BYTES,
|
|
179
|
-
NODE_VERSION,
|
|
180
|
-
distributionFor,
|
|
181
|
-
ensureManagedNode,
|
|
182
|
-
archiveRootName,
|
|
183
|
-
sha256File,
|
|
184
|
-
unpackArchive,
|
|
185
|
-
usableNode,
|
|
186
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const https = require('node:https');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const NODE_VERSION = '24.15.0';
|
|
11
|
+
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
|
|
12
|
+
const DISTRIBUTIONS = {
|
|
13
|
+
'darwin-arm64': {
|
|
14
|
+
archive: `node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
|
|
15
|
+
binary: path.join('bin', 'node'),
|
|
16
|
+
format: 'tar.gz',
|
|
17
|
+
sha256: '372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4',
|
|
18
|
+
},
|
|
19
|
+
'darwin-x64': {
|
|
20
|
+
archive: `node-v${NODE_VERSION}-darwin-x64.tar.gz`,
|
|
21
|
+
binary: path.join('bin', 'node'),
|
|
22
|
+
format: 'tar.gz',
|
|
23
|
+
sha256: 'ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b',
|
|
24
|
+
},
|
|
25
|
+
'win32-arm64': {
|
|
26
|
+
archive: `node-v${NODE_VERSION}-win-arm64.zip`,
|
|
27
|
+
binary: 'node.exe',
|
|
28
|
+
format: 'zip',
|
|
29
|
+
sha256: 'c9eb7402eda26e2ba7e44b6727fc85a8de56c5095b1f71ebd3062892211aa116',
|
|
30
|
+
},
|
|
31
|
+
'win32-x64': {
|
|
32
|
+
archive: `node-v${NODE_VERSION}-win-x64.zip`,
|
|
33
|
+
binary: 'node.exe',
|
|
34
|
+
format: 'zip',
|
|
35
|
+
sha256: 'cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function distributionFor(platform, arch) {
|
|
40
|
+
return DISTRIBUTIONS[`${platform}-${arch}`] || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sha256File(file) {
|
|
44
|
+
const hash = crypto.createHash('sha256');
|
|
45
|
+
hash.update(fs.readFileSync(file));
|
|
46
|
+
return hash.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function downloadFile(url, destination, redirectsLeft = 3) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const request = https.get(url, { timeout: 30_000 }, (response) => {
|
|
52
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
53
|
+
response.resume();
|
|
54
|
+
if (redirectsLeft <= 0) {
|
|
55
|
+
reject(new Error('Zu viele Weiterleitungen beim Node.js-Download.'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const redirect = new URL(response.headers.location, url);
|
|
59
|
+
if (redirect.protocol !== 'https:') {
|
|
60
|
+
reject(new Error('Unsichere Weiterleitung beim Node.js-Download abgelehnt.'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
downloadFile(redirect.href, destination, redirectsLeft - 1).then(resolve, reject);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (response.statusCode !== 200) {
|
|
68
|
+
response.resume();
|
|
69
|
+
reject(new Error(`Node.js-Download antwortete mit HTTP ${response.statusCode}.`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const contentLength = Number(response.headers['content-length'] || 0);
|
|
74
|
+
if (contentLength > MAX_ARCHIVE_BYTES) {
|
|
75
|
+
response.destroy();
|
|
76
|
+
reject(new Error('Node.js-Download ist groesser als erwartet.'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let received = 0;
|
|
81
|
+
const output = fs.createWriteStream(destination, { flags: 'wx' });
|
|
82
|
+
response.on('data', (chunk) => {
|
|
83
|
+
received += chunk.length;
|
|
84
|
+
if (received > MAX_ARCHIVE_BYTES) {
|
|
85
|
+
response.destroy(new Error('Node.js-Download ist groesser als erwartet.'));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
response.on('error', reject);
|
|
89
|
+
output.on('error', reject);
|
|
90
|
+
output.on('finish', () => output.close(resolve));
|
|
91
|
+
response.pipe(output);
|
|
92
|
+
});
|
|
93
|
+
request.on('error', reject);
|
|
94
|
+
request.on('timeout', () => request.destroy(new Error('Zeitlimit beim Node.js-Download erreicht.')));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function usableNode(binary) {
|
|
99
|
+
if (!fs.existsSync(binary)) return false;
|
|
100
|
+
const result = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 5_000 });
|
|
101
|
+
return result.status === 0 && result.stdout.trim() === `v${NODE_VERSION}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function archiveRootName(distribution) {
|
|
105
|
+
const suffix = distribution.format === 'zip' ? '.zip' : '.tar.gz';
|
|
106
|
+
if (!distribution.archive.endsWith(suffix)) {
|
|
107
|
+
throw new Error('Unbekanntes Format der Node.js-Laufzeit.');
|
|
108
|
+
}
|
|
109
|
+
return distribution.archive.slice(0, -suffix.length);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unpackArchive(archive, destination, distribution, platform) {
|
|
113
|
+
const command = platform === 'win32'
|
|
114
|
+
? path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
|
115
|
+
: '/usr/bin/tar';
|
|
116
|
+
const args = distribution.format === 'zip'
|
|
117
|
+
? ['-xf', archive, '-C', destination]
|
|
118
|
+
: ['-xzf', archive, '-C', destination];
|
|
119
|
+
return spawnSync(command, args, {
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
timeout: 120_000,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function ensureManagedNode(blunHome, platform, arch) {
|
|
126
|
+
const distribution = distributionFor(platform, arch);
|
|
127
|
+
if (!distribution) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Automatische Node.js-Einrichtung wird fuer ${platform}-${arch} noch nicht unterstuetzt.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const installDir = path.join(
|
|
134
|
+
blunHome,
|
|
135
|
+
'runtime',
|
|
136
|
+
'node',
|
|
137
|
+
`v${NODE_VERSION}-${platform}-${arch}`,
|
|
138
|
+
);
|
|
139
|
+
const binary = path.join(installDir, distribution.binary);
|
|
140
|
+
if (usableNode(binary)) return binary;
|
|
141
|
+
|
|
142
|
+
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-node-runtime-'));
|
|
143
|
+
try {
|
|
144
|
+
const archive = path.join(staging, distribution.archive);
|
|
145
|
+
await downloadFile(
|
|
146
|
+
`https://nodejs.org/dist/v${NODE_VERSION}/${distribution.archive}`,
|
|
147
|
+
archive,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const actualSha = sha256File(archive);
|
|
151
|
+
if (actualSha !== distribution.sha256) {
|
|
152
|
+
throw new Error('Die SHA-256-Pruefsumme der Node.js-Laufzeit stimmt nicht.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const extractedName = archiveRootName(distribution);
|
|
156
|
+
const extractedDir = path.join(staging, extractedName);
|
|
157
|
+
const unpacked = unpackArchive(archive, staging, distribution, platform);
|
|
158
|
+
if (unpacked.status !== 0 || !usableNode(path.join(extractedDir, distribution.binary))) {
|
|
159
|
+
const detail = (unpacked.stderr || '').trim();
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Die Node.js-Laufzeit konnte nicht entpackt werden${detail ? `: ${detail}` : '.'}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fs.mkdirSync(path.dirname(installDir), { recursive: true });
|
|
166
|
+
if (usableNode(binary)) return binary;
|
|
167
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
168
|
+
fs.renameSync(extractedDir, installDir);
|
|
169
|
+
if (!usableNode(binary)) throw new Error('Die installierte Node.js-Laufzeit startet nicht.');
|
|
170
|
+
return binary;
|
|
171
|
+
} finally {
|
|
172
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
DISTRIBUTIONS,
|
|
178
|
+
MAX_ARCHIVE_BYTES,
|
|
179
|
+
NODE_VERSION,
|
|
180
|
+
distributionFor,
|
|
181
|
+
ensureManagedNode,
|
|
182
|
+
archiveRootName,
|
|
183
|
+
sha256File,
|
|
184
|
+
unpackArchive,
|
|
185
|
+
usableNode,
|
|
186
|
+
};
|
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const { createHash, randomUUID } = require('node:crypto');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const { ensurePrivateDirectory, securePrivateTree } = require('./private-paths');
|
|
6
|
-
const SCHEMA = 'blun.managed-plugin-selection/v1';
|
|
7
|
-
|
|
8
|
-
function snapshotPluginTree(root) {
|
|
9
|
-
const entries = [];
|
|
10
|
-
const rootStat = fs.lstatSync(root);
|
|
11
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return null;
|
|
12
|
-
const visit = (directory, relativeDirectory = '') => {
|
|
13
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })
|
|
14
|
-
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
15
|
-
const relativePath = path.join(relativeDirectory, entry.name);
|
|
16
|
-
const absolutePath = path.join(directory, entry.name);
|
|
17
|
-
const stat = fs.lstatSync(absolutePath);
|
|
18
|
-
if (stat.isSymbolicLink()) return false;
|
|
19
|
-
if (stat.isDirectory()) {
|
|
20
|
-
entries.push({ kind: 'directory', path: relativePath });
|
|
21
|
-
if (!visit(absolutePath, relativePath)) return false;
|
|
22
|
-
} else if (stat.isFile()) {
|
|
23
|
-
entries.push({ kind: 'file', path: relativePath, content: fs.readFileSync(absolutePath) });
|
|
24
|
-
} else return false;
|
|
25
|
-
}
|
|
26
|
-
return true;
|
|
27
|
-
};
|
|
28
|
-
return visit(root) ? entries : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function digestPluginEntries(entries) {
|
|
32
|
-
if (entries === null) throw new Error('Invalid managed plugin tree');
|
|
33
|
-
const hash = createHash('sha256');
|
|
34
|
-
for (const entry of entries) {
|
|
35
|
-
hash.update(Buffer.from(`${entry.kind}\0${entry.path}\0`, 'utf8'));
|
|
36
|
-
if (entry.kind === 'file') {
|
|
37
|
-
const length = Buffer.allocUnsafe(8);
|
|
38
|
-
length.writeBigUInt64BE(BigInt(entry.content.length));
|
|
39
|
-
hash.update(length);
|
|
40
|
-
hash.update(entry.content);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return hash.digest('hex');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function samePath(left, right) {
|
|
47
|
-
const normalize = value => process.platform === 'win32' ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
48
|
-
return normalize(left) === normalize(right);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function scopedPluginRoot(blunDir, pluginRoot) {
|
|
52
|
-
const managed = path.resolve(blunDir, 'plugins', 'managed');
|
|
53
|
-
const root = path.resolve(pluginRoot);
|
|
54
|
-
if (!samePath(path.dirname(root), managed)) throw new Error('Selected AgentSpine root is outside this profile');
|
|
55
|
-
for (const directory of [blunDir, path.dirname(managed), managed, root]) {
|
|
56
|
-
const stat = fs.lstatSync(directory);
|
|
57
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('Selected AgentSpine root contains a linked directory');
|
|
58
|
-
}
|
|
59
|
-
if (!samePath(path.dirname(fs.realpathSync(root)), fs.realpathSync(managed))) {
|
|
60
|
-
throw new Error('Selected AgentSpine root resolves outside this profile');
|
|
61
|
-
}
|
|
62
|
-
return root;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function describeAgentSpineSelection({ blunDir, pluginRoot }) {
|
|
66
|
-
const root = scopedPluginRoot(blunDir, pluginRoot);
|
|
67
|
-
const digest = digestPluginEntries(snapshotPluginTree(root));
|
|
68
|
-
if (path.basename(root) !== `agent-spine-${digest}`) throw new Error('Selected AgentSpine digest mismatch');
|
|
69
|
-
return { schema: SCHEMA, digest };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function selectedAgentSpineRoot({ installed, blunDir }) {
|
|
73
|
-
const records = installed.plugins.filter(record => record.id === 'agent-spine');
|
|
74
|
-
if (!records.some(record => Object.hasOwn(record, 'managedSelection'))) return null;
|
|
75
|
-
if (records.length !== 1) throw new Error('Ambiguous AgentSpine selection');
|
|
76
|
-
const [record] = records;
|
|
77
|
-
const selection = record.managedSelection;
|
|
78
|
-
if (!selection || selection.schema !== SCHEMA || !/^[a-f0-9]{64}$/.test(selection.digest || '')
|
|
79
|
-
|| Object.keys(selection).sort().join(',') !== 'digest,schema' || typeof record.root !== 'string') {
|
|
80
|
-
throw new Error('Invalid AgentSpine selection');
|
|
81
|
-
}
|
|
82
|
-
const actual = describeAgentSpineSelection({ blunDir, pluginRoot: record.root });
|
|
83
|
-
if (actual.digest !== selection.digest) throw new Error('Selected AgentSpine digest mismatch');
|
|
84
|
-
return record.root;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function copySelectedAgentSpinePlugin({ blunDir, sourceRoot }) {
|
|
88
|
-
const plugins = path.resolve(blunDir, 'plugins');
|
|
89
|
-
const managed = path.join(plugins, 'managed');
|
|
90
|
-
ensurePrivateDirectory(plugins);
|
|
91
|
-
ensurePrivateDirectory(managed);
|
|
92
|
-
const digest = digestPluginEntries(snapshotPluginTree(sourceRoot));
|
|
93
|
-
const pluginRoot = path.join(managed, `agent-spine-${digest}`);
|
|
94
|
-
if (fs.existsSync(pluginRoot)) {
|
|
95
|
-
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
96
|
-
return pluginRoot;
|
|
97
|
-
}
|
|
98
|
-
const staging = path.join(managed, `.agent-spine-selected-${process.pid}-${randomUUID()}.tmp`);
|
|
99
|
-
try {
|
|
100
|
-
fs.cpSync(sourceRoot, staging, { recursive: true, errorOnExist: true, force: false });
|
|
101
|
-
securePrivateTree(staging);
|
|
102
|
-
if (digestPluginEntries(snapshotPluginTree(staging)) !== digest) throw new Error('AgentSpine source changed during installation');
|
|
103
|
-
fs.renameSync(staging, pluginRoot);
|
|
104
|
-
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
105
|
-
return pluginRoot;
|
|
106
|
-
} finally {
|
|
107
|
-
if (!samePath(path.dirname(staging), managed) || !path.basename(staging).startsWith('.agent-spine-selected-')) {
|
|
108
|
-
throw new Error('Invalid AgentSpine staging cleanup path');
|
|
109
|
-
}
|
|
110
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
module.exports = { snapshotPluginTree, digestPluginEntries, describeAgentSpineSelection,
|
|
115
|
-
selectedAgentSpineRoot, copySelectedAgentSpinePlugin };
|
|
1
|
+
'use strict';
|
|
2
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { ensurePrivateDirectory, securePrivateTree } = require('./private-paths');
|
|
6
|
+
const SCHEMA = 'blun.managed-plugin-selection/v1';
|
|
7
|
+
|
|
8
|
+
function snapshotPluginTree(root) {
|
|
9
|
+
const entries = [];
|
|
10
|
+
const rootStat = fs.lstatSync(root);
|
|
11
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return null;
|
|
12
|
+
const visit = (directory, relativeDirectory = '') => {
|
|
13
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })
|
|
14
|
+
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
15
|
+
const relativePath = path.join(relativeDirectory, entry.name);
|
|
16
|
+
const absolutePath = path.join(directory, entry.name);
|
|
17
|
+
const stat = fs.lstatSync(absolutePath);
|
|
18
|
+
if (stat.isSymbolicLink()) return false;
|
|
19
|
+
if (stat.isDirectory()) {
|
|
20
|
+
entries.push({ kind: 'directory', path: relativePath });
|
|
21
|
+
if (!visit(absolutePath, relativePath)) return false;
|
|
22
|
+
} else if (stat.isFile()) {
|
|
23
|
+
entries.push({ kind: 'file', path: relativePath, content: fs.readFileSync(absolutePath) });
|
|
24
|
+
} else return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
return visit(root) ? entries : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function digestPluginEntries(entries) {
|
|
32
|
+
if (entries === null) throw new Error('Invalid managed plugin tree');
|
|
33
|
+
const hash = createHash('sha256');
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
hash.update(Buffer.from(`${entry.kind}\0${entry.path}\0`, 'utf8'));
|
|
36
|
+
if (entry.kind === 'file') {
|
|
37
|
+
const length = Buffer.allocUnsafe(8);
|
|
38
|
+
length.writeBigUInt64BE(BigInt(entry.content.length));
|
|
39
|
+
hash.update(length);
|
|
40
|
+
hash.update(entry.content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return hash.digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function samePath(left, right) {
|
|
47
|
+
const normalize = value => process.platform === 'win32' ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
48
|
+
return normalize(left) === normalize(right);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scopedPluginRoot(blunDir, pluginRoot) {
|
|
52
|
+
const managed = path.resolve(blunDir, 'plugins', 'managed');
|
|
53
|
+
const root = path.resolve(pluginRoot);
|
|
54
|
+
if (!samePath(path.dirname(root), managed)) throw new Error('Selected AgentSpine root is outside this profile');
|
|
55
|
+
for (const directory of [blunDir, path.dirname(managed), managed, root]) {
|
|
56
|
+
const stat = fs.lstatSync(directory);
|
|
57
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('Selected AgentSpine root contains a linked directory');
|
|
58
|
+
}
|
|
59
|
+
if (!samePath(path.dirname(fs.realpathSync(root)), fs.realpathSync(managed))) {
|
|
60
|
+
throw new Error('Selected AgentSpine root resolves outside this profile');
|
|
61
|
+
}
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function describeAgentSpineSelection({ blunDir, pluginRoot }) {
|
|
66
|
+
const root = scopedPluginRoot(blunDir, pluginRoot);
|
|
67
|
+
const digest = digestPluginEntries(snapshotPluginTree(root));
|
|
68
|
+
if (path.basename(root) !== `agent-spine-${digest}`) throw new Error('Selected AgentSpine digest mismatch');
|
|
69
|
+
return { schema: SCHEMA, digest };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function selectedAgentSpineRoot({ installed, blunDir }) {
|
|
73
|
+
const records = installed.plugins.filter(record => record.id === 'agent-spine');
|
|
74
|
+
if (!records.some(record => Object.hasOwn(record, 'managedSelection'))) return null;
|
|
75
|
+
if (records.length !== 1) throw new Error('Ambiguous AgentSpine selection');
|
|
76
|
+
const [record] = records;
|
|
77
|
+
const selection = record.managedSelection;
|
|
78
|
+
if (!selection || selection.schema !== SCHEMA || !/^[a-f0-9]{64}$/.test(selection.digest || '')
|
|
79
|
+
|| Object.keys(selection).sort().join(',') !== 'digest,schema' || typeof record.root !== 'string') {
|
|
80
|
+
throw new Error('Invalid AgentSpine selection');
|
|
81
|
+
}
|
|
82
|
+
const actual = describeAgentSpineSelection({ blunDir, pluginRoot: record.root });
|
|
83
|
+
if (actual.digest !== selection.digest) throw new Error('Selected AgentSpine digest mismatch');
|
|
84
|
+
return record.root;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function copySelectedAgentSpinePlugin({ blunDir, sourceRoot }) {
|
|
88
|
+
const plugins = path.resolve(blunDir, 'plugins');
|
|
89
|
+
const managed = path.join(plugins, 'managed');
|
|
90
|
+
ensurePrivateDirectory(plugins);
|
|
91
|
+
ensurePrivateDirectory(managed);
|
|
92
|
+
const digest = digestPluginEntries(snapshotPluginTree(sourceRoot));
|
|
93
|
+
const pluginRoot = path.join(managed, `agent-spine-${digest}`);
|
|
94
|
+
if (fs.existsSync(pluginRoot)) {
|
|
95
|
+
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
96
|
+
return pluginRoot;
|
|
97
|
+
}
|
|
98
|
+
const staging = path.join(managed, `.agent-spine-selected-${process.pid}-${randomUUID()}.tmp`);
|
|
99
|
+
try {
|
|
100
|
+
fs.cpSync(sourceRoot, staging, { recursive: true, errorOnExist: true, force: false });
|
|
101
|
+
securePrivateTree(staging);
|
|
102
|
+
if (digestPluginEntries(snapshotPluginTree(staging)) !== digest) throw new Error('AgentSpine source changed during installation');
|
|
103
|
+
fs.renameSync(staging, pluginRoot);
|
|
104
|
+
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
105
|
+
return pluginRoot;
|
|
106
|
+
} finally {
|
|
107
|
+
if (!samePath(path.dirname(staging), managed) || !path.basename(staging).startsWith('.agent-spine-selected-')) {
|
|
108
|
+
throw new Error('Invalid AgentSpine staging cleanup path');
|
|
109
|
+
}
|
|
110
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { snapshotPluginTree, digestPluginEntries, describeAgentSpineSelection,
|
|
115
|
+
selectedAgentSpineRoot, copySelectedAgentSpinePlugin };
|