blun-king-cli 9.1.626 → 9.1.628
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/bin/core-bootstrap.js +93 -92
- package/bin/launcher-mode.js +126 -93
- package/bin/launcher-runtime.js +73 -8
- package/bin/managed-plugin-selection.cjs +116 -115
- package/bin/native-command-runner.js +72 -0
- package/bin/native-module-repair.js +26 -20
- package/bin/plugin-bootstrap.js +163 -162
- package/bin/rename-private-staging.cjs +82 -0
- package/blun.mjs +371 -527
- package/package.json +1 -1
- package/worker-host.mjs +2 -2
|
@@ -1,115 +1,116 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const { createHash, randomUUID } = require('node:crypto');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
if (stat.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
length.
|
|
39
|
-
|
|
40
|
-
hash.update(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (records.
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
ensurePrivateDirectory(
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
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 { renamePrivateStaging } = require('./rename-private-staging.cjs');
|
|
6
|
+
const { ensurePrivateDirectory, securePrivateTree } = require('./private-paths');
|
|
7
|
+
const SCHEMA = 'blun.managed-plugin-selection/v1';
|
|
8
|
+
|
|
9
|
+
function snapshotPluginTree(root) {
|
|
10
|
+
const entries = [];
|
|
11
|
+
const rootStat = fs.lstatSync(root);
|
|
12
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return null;
|
|
13
|
+
const visit = (directory, relativeDirectory = '') => {
|
|
14
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })
|
|
15
|
+
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
16
|
+
const relativePath = path.join(relativeDirectory, entry.name);
|
|
17
|
+
const absolutePath = path.join(directory, entry.name);
|
|
18
|
+
const stat = fs.lstatSync(absolutePath);
|
|
19
|
+
if (stat.isSymbolicLink()) return false;
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
entries.push({ kind: 'directory', path: relativePath });
|
|
22
|
+
if (!visit(absolutePath, relativePath)) return false;
|
|
23
|
+
} else if (stat.isFile()) {
|
|
24
|
+
entries.push({ kind: 'file', path: relativePath, content: fs.readFileSync(absolutePath) });
|
|
25
|
+
} else return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
};
|
|
29
|
+
return visit(root) ? entries : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function digestPluginEntries(entries) {
|
|
33
|
+
if (entries === null) throw new Error('Invalid managed plugin tree');
|
|
34
|
+
const hash = createHash('sha256');
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
hash.update(Buffer.from(`${entry.kind}\0${entry.path}\0`, 'utf8'));
|
|
37
|
+
if (entry.kind === 'file') {
|
|
38
|
+
const length = Buffer.allocUnsafe(8);
|
|
39
|
+
length.writeBigUInt64BE(BigInt(entry.content.length));
|
|
40
|
+
hash.update(length);
|
|
41
|
+
hash.update(entry.content);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return hash.digest('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function samePath(left, right) {
|
|
48
|
+
const normalize = value => process.platform === 'win32' ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
49
|
+
return normalize(left) === normalize(right);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scopedPluginRoot(blunDir, pluginRoot) {
|
|
53
|
+
const managed = path.resolve(blunDir, 'plugins', 'managed');
|
|
54
|
+
const root = path.resolve(pluginRoot);
|
|
55
|
+
if (!samePath(path.dirname(root), managed)) throw new Error('Selected AgentSpine root is outside this profile');
|
|
56
|
+
for (const directory of [blunDir, path.dirname(managed), managed, root]) {
|
|
57
|
+
const stat = fs.lstatSync(directory);
|
|
58
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('Selected AgentSpine root contains a linked directory');
|
|
59
|
+
}
|
|
60
|
+
if (!samePath(path.dirname(fs.realpathSync(root)), fs.realpathSync(managed))) {
|
|
61
|
+
throw new Error('Selected AgentSpine root resolves outside this profile');
|
|
62
|
+
}
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function describeAgentSpineSelection({ blunDir, pluginRoot }) {
|
|
67
|
+
const root = scopedPluginRoot(blunDir, pluginRoot);
|
|
68
|
+
const digest = digestPluginEntries(snapshotPluginTree(root));
|
|
69
|
+
if (path.basename(root) !== `agent-spine-${digest}`) throw new Error('Selected AgentSpine digest mismatch');
|
|
70
|
+
return { schema: SCHEMA, digest };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function selectedAgentSpineRoot({ installed, blunDir }) {
|
|
74
|
+
const records = installed.plugins.filter(record => record.id === 'agent-spine');
|
|
75
|
+
if (!records.some(record => Object.hasOwn(record, 'managedSelection'))) return null;
|
|
76
|
+
if (records.length !== 1) throw new Error('Ambiguous AgentSpine selection');
|
|
77
|
+
const [record] = records;
|
|
78
|
+
const selection = record.managedSelection;
|
|
79
|
+
if (!selection || selection.schema !== SCHEMA || !/^[a-f0-9]{64}$/.test(selection.digest || '')
|
|
80
|
+
|| Object.keys(selection).sort().join(',') !== 'digest,schema' || typeof record.root !== 'string') {
|
|
81
|
+
throw new Error('Invalid AgentSpine selection');
|
|
82
|
+
}
|
|
83
|
+
const actual = describeAgentSpineSelection({ blunDir, pluginRoot: record.root });
|
|
84
|
+
if (actual.digest !== selection.digest) throw new Error('Selected AgentSpine digest mismatch');
|
|
85
|
+
return record.root;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function copySelectedAgentSpinePlugin({ blunDir, sourceRoot }) {
|
|
89
|
+
const plugins = path.resolve(blunDir, 'plugins');
|
|
90
|
+
const managed = path.join(plugins, 'managed');
|
|
91
|
+
ensurePrivateDirectory(plugins);
|
|
92
|
+
ensurePrivateDirectory(managed);
|
|
93
|
+
const digest = digestPluginEntries(snapshotPluginTree(sourceRoot));
|
|
94
|
+
const pluginRoot = path.join(managed, `agent-spine-${digest}`);
|
|
95
|
+
if (fs.existsSync(pluginRoot)) {
|
|
96
|
+
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
97
|
+
return pluginRoot;
|
|
98
|
+
}
|
|
99
|
+
const staging = path.join(managed, `.agent-spine-selected-${process.pid}-${randomUUID()}.tmp`);
|
|
100
|
+
try {
|
|
101
|
+
fs.cpSync(sourceRoot, staging, { recursive: true, errorOnExist: true, force: false });
|
|
102
|
+
securePrivateTree(staging);
|
|
103
|
+
if (digestPluginEntries(snapshotPluginTree(staging)) !== digest) throw new Error('AgentSpine source changed during installation');
|
|
104
|
+
renamePrivateStaging(staging, pluginRoot);
|
|
105
|
+
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
106
|
+
return pluginRoot;
|
|
107
|
+
} finally {
|
|
108
|
+
if (!samePath(path.dirname(staging), managed) || !path.basename(staging).startsWith('.agent-spine-selected-')) {
|
|
109
|
+
throw new Error('Invalid AgentSpine staging cleanup path');
|
|
110
|
+
}
|
|
111
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { snapshotPluginTree, digestPluginEntries, describeAgentSpineSelection,
|
|
116
|
+
selectedAgentSpineRoot, copySelectedAgentSpinePlugin };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { StringDecoder } = require('node:string_decoder');
|
|
6
|
+
|
|
7
|
+
const MAX_DIAGNOSTIC_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
async function stopOwnedProcessTree(child) {
|
|
10
|
+
if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
|
|
11
|
+
if (process.platform !== 'win32') {
|
|
12
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await new Promise(resolve => {
|
|
16
|
+
const fallback = () => {
|
|
17
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
18
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
19
|
+
}
|
|
20
|
+
resolve();
|
|
21
|
+
};
|
|
22
|
+
const killer = spawn(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'taskkill.exe'),
|
|
23
|
+
['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
24
|
+
killer.once('error', fallback);
|
|
25
|
+
killer.once('close', fallback);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runNativeCommand(command, args, { cwd, signal, spawnImpl = spawn }) {
|
|
30
|
+
signal?.throwIfAborted();
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const child = spawnImpl(command, args, { cwd, windowsHide: true,
|
|
33
|
+
detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
34
|
+
let error;
|
|
35
|
+
let stopping;
|
|
36
|
+
let cancelled = false;
|
|
37
|
+
const collect = stream => {
|
|
38
|
+
const decoder = new StringDecoder('utf8');
|
|
39
|
+
let bytes = 0;
|
|
40
|
+
let text = '';
|
|
41
|
+
let truncated = false;
|
|
42
|
+
stream.on('data', chunk => {
|
|
43
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
44
|
+
const kept = data.subarray(0, Math.max(0, MAX_DIAGNOSTIC_BYTES - bytes));
|
|
45
|
+
bytes += kept.length;
|
|
46
|
+
truncated ||= kept.length !== data.length;
|
|
47
|
+
if (kept.length) text += decoder.write(kept);
|
|
48
|
+
});
|
|
49
|
+
return () => text + decoder.end() + (truncated ? '\n[NATIVE_DIAGNOSTIC_OUTPUT_TRUNCATED]' : '');
|
|
50
|
+
};
|
|
51
|
+
const stdout = collect(child.stdout);
|
|
52
|
+
const stderr = collect(child.stderr);
|
|
53
|
+
const stop = () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
stopping ??= stopOwnedProcessTree(child).catch(stopError => {
|
|
56
|
+
error = stopError;
|
|
57
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
61
|
+
child.once('error', spawnError => { error = spawnError; });
|
|
62
|
+
child.once('close', async (status, exitSignal) => {
|
|
63
|
+
signal?.removeEventListener('abort', stop);
|
|
64
|
+
await stopping;
|
|
65
|
+
if (cancelled) reject(new DOMException('Native command cancelled', 'AbortError'));
|
|
66
|
+
else resolve({ status, signal: exitSignal, stdout: stdout(), stderr: stderr(), error });
|
|
67
|
+
});
|
|
68
|
+
if (signal?.aborted) stop();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { runNativeCommand };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { runNativeCommand } = require('./native-command-runner');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
|
|
@@ -21,10 +21,11 @@ function packageHasDependency(document, dependency) {
|
|
|
21
21
|
.some((field) => Object.hasOwn(document?.[field] || {}, dependency));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function nearestPackageDirectory(startDir) {
|
|
24
|
+
function nearestPackageDirectory(startDir, stopDir) {
|
|
25
25
|
let current = path.resolve(startDir);
|
|
26
26
|
while (true) {
|
|
27
27
|
if (fs.existsSync(path.join(current, 'package.json'))) return current;
|
|
28
|
+
if (stopDir !== undefined && current === stopDir) return null;
|
|
28
29
|
const parent = path.dirname(current);
|
|
29
30
|
if (parent === current) return null;
|
|
30
31
|
current = parent;
|
|
@@ -75,7 +76,7 @@ function configuredNativeProjectPaths({ blunDir, callerCwd }) {
|
|
|
75
76
|
path.join(root, '.mcp.json'),
|
|
76
77
|
path.join(callerCwd, '.blun', 'mcp.json'),
|
|
77
78
|
].map((entry) => path.resolve(entry)))];
|
|
78
|
-
const candidates = [nearestPackageDirectory(callerCwd)];
|
|
79
|
+
const candidates = [nearestPackageDirectory(callerCwd, root)];
|
|
79
80
|
for (const configPath of configPaths) {
|
|
80
81
|
const document = readMcpDocument(configPath);
|
|
81
82
|
const servers = document?.mcpServers;
|
|
@@ -98,12 +99,10 @@ function configuredNativeProjectPaths({ blunDir, callerCwd }) {
|
|
|
98
99
|
return projects;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function probeBetterSqlite({ nodeBinary, projectDir,
|
|
102
|
-
return
|
|
102
|
+
function probeBetterSqlite({ nodeBinary, projectDir, runCommand, signal }) {
|
|
103
|
+
return runCommand(nodeBinary, ['-e', SQLITE_PROBE], {
|
|
103
104
|
cwd: projectDir,
|
|
104
|
-
|
|
105
|
-
timeout: 30_000,
|
|
106
|
-
windowsHide: true,
|
|
105
|
+
signal,
|
|
107
106
|
});
|
|
108
107
|
}
|
|
109
108
|
|
|
@@ -119,13 +118,15 @@ function npmInvocation(nodeBinary) {
|
|
|
119
118
|
return { args: [npmCli], command: nodeBinary };
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
function repairNativeProject({
|
|
121
|
+
async function repairNativeProject({
|
|
123
122
|
nodeBinary = process.execPath,
|
|
124
123
|
npmCommand,
|
|
125
124
|
onRepairStart,
|
|
126
125
|
projectDir,
|
|
127
|
-
|
|
126
|
+
runCommand = runNativeCommand,
|
|
127
|
+
signal,
|
|
128
128
|
}) {
|
|
129
|
+
signal?.throwIfAborted();
|
|
129
130
|
if (typeof projectDir !== 'string' || !path.isAbsolute(projectDir)) {
|
|
130
131
|
throw new Error('Der Projektpfad fuer die native Ladeprobe muss absolut sein.');
|
|
131
132
|
}
|
|
@@ -141,26 +142,27 @@ function repairNativeProject({
|
|
|
141
142
|
return { kind: 'skipped', reason: 'dependency_missing' };
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
const before = probeBetterSqlite({ nodeBinary, projectDir: absoluteProjectDir,
|
|
145
|
+
const before = await probeBetterSqlite({ nodeBinary, projectDir: absoluteProjectDir, runCommand, signal });
|
|
146
|
+
signal?.throwIfAborted();
|
|
145
147
|
if (before.status === 0) return { kind: 'healthy' };
|
|
146
148
|
|
|
147
149
|
const npm = npmCommand === undefined
|
|
148
150
|
? npmInvocation(nodeBinary)
|
|
149
151
|
: { args: [], command: npmCommand };
|
|
150
152
|
onRepairStart?.(absoluteProjectDir);
|
|
151
|
-
const rebuilt =
|
|
153
|
+
const rebuilt = await runCommand(npm.command, [...npm.args, 'rebuild', 'better-sqlite3'], {
|
|
152
154
|
cwd: absoluteProjectDir,
|
|
153
|
-
|
|
154
|
-
timeout: 300_000,
|
|
155
|
-
windowsHide: true,
|
|
155
|
+
signal,
|
|
156
156
|
});
|
|
157
|
+
signal?.throwIfAborted();
|
|
157
158
|
if (rebuilt.status !== 0) {
|
|
158
159
|
throw new Error(
|
|
159
160
|
`Native Node.js-Module konnten nicht repariert werden: ${commandFailure(rebuilt) || 'ohne Fehlertext'}`,
|
|
160
161
|
);
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
const after = probeBetterSqlite({ nodeBinary, projectDir: absoluteProjectDir,
|
|
164
|
+
const after = await probeBetterSqlite({ nodeBinary, projectDir: absoluteProjectDir, runCommand, signal });
|
|
165
|
+
signal?.throwIfAborted();
|
|
164
166
|
if (after.status !== 0) {
|
|
165
167
|
throw new Error(
|
|
166
168
|
`Ladeprobe nach dem Rebuild fehlgeschlagen: ${commandFailure(after) || 'ohne Fehlertext'}`,
|
|
@@ -169,22 +171,26 @@ function repairNativeProject({
|
|
|
169
171
|
return { kind: 'repaired' };
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
function repairConfiguredNativeModules({
|
|
174
|
+
async function repairConfiguredNativeModules({
|
|
173
175
|
blunDir,
|
|
174
176
|
callerCwd,
|
|
175
177
|
nodeBinary = process.execPath,
|
|
176
178
|
onRepairStart,
|
|
177
179
|
projectPaths,
|
|
178
180
|
repairProject = repairNativeProject,
|
|
179
|
-
|
|
181
|
+
runCommand = runNativeCommand,
|
|
182
|
+
signal,
|
|
180
183
|
}) {
|
|
184
|
+
signal?.throwIfAborted();
|
|
181
185
|
const projects = projectPaths || configuredNativeProjectPaths({ blunDir, callerCwd });
|
|
182
186
|
const result = { checked: projects.length, healthy: [], repaired: [], skipped: [] };
|
|
183
187
|
for (const projectDir of projects) {
|
|
184
|
-
|
|
188
|
+
signal?.throwIfAborted();
|
|
189
|
+
const firstResult = await repairProject({
|
|
185
190
|
nodeBinary,
|
|
186
191
|
projectDir,
|
|
187
|
-
|
|
192
|
+
runCommand,
|
|
193
|
+
signal,
|
|
188
194
|
onRepairStart,
|
|
189
195
|
});
|
|
190
196
|
if (firstResult.kind === 'healthy') result.healthy.push(projectDir);
|