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.
@@ -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 { 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 { 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 { spawnSync } = require('node:child_process');
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, spawnSyncImpl }) {
102
- return spawnSyncImpl(nodeBinary, ['-e', SQLITE_PROBE], {
102
+ function probeBetterSqlite({ nodeBinary, projectDir, runCommand, signal }) {
103
+ return runCommand(nodeBinary, ['-e', SQLITE_PROBE], {
103
104
  cwd: projectDir,
104
- encoding: 'utf8',
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
- spawnSyncImpl = spawnSync,
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, spawnSyncImpl });
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 = spawnSyncImpl(npm.command, [...npm.args, 'rebuild', 'better-sqlite3'], {
153
+ const rebuilt = await runCommand(npm.command, [...npm.args, 'rebuild', 'better-sqlite3'], {
152
154
  cwd: absoluteProjectDir,
153
- encoding: 'utf8',
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, spawnSyncImpl });
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
- spawnSyncImpl = spawnSync,
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
- const firstResult = repairProject({
188
+ signal?.throwIfAborted();
189
+ const firstResult = await repairProject({
185
190
  nodeBinary,
186
191
  projectDir,
187
- spawnSyncImpl,
192
+ runCommand,
193
+ signal,
188
194
  onRepairStart,
189
195
  });
190
196
  if (firstResult.kind === 'healthy') result.healthy.push(projectDir);