blun-king-cli 9.1.628-pruefpaket.modules.r7 → 9.1.629

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/blun.js CHANGED
File without changes
File without changes
package/bin/king.js CHANGED
File without changes
File without changes
@@ -40,8 +40,142 @@ const {
40
40
  writePrivateFile,
41
41
  } = require('./private-paths');
42
42
 
43
+ const { writeTelegramTemplate } = require('./rename-private-staging.cjs');
43
44
  const PKG = path.resolve(__dirname, '..');
44
- const ARGS = process.argv.slice(2);
45
+ const { parseProfileLaunchArgs, resolveSelectedProfile } = require('./profile-launch.cjs');
46
+
47
+ // Generated from actual 628 Commander registrations; see GRAMMAR-628.json.
48
+ // No subcommand option arity table: subcommand tails belong to Core.
49
+ const ROOT_OPTION_ARITY = Object.freeze({
50
+ "-V": "flag",
51
+ "--version": "flag",
52
+ "--local": "flag",
53
+ "--message": "required",
54
+ "--history-limit": "required",
55
+ "--timeout-ms": "required",
56
+ "--thinking": "required",
57
+ "--url": "required",
58
+ "--token": "required",
59
+ "--password": "required",
60
+ "--tls-fingerprint": "required",
61
+ "-S": "optional",
62
+ "--session": "optional",
63
+ "-r": "optional",
64
+ "--resume": "optional",
65
+ "-c": "flag",
66
+ "--continue": "flag",
67
+ "-C": "flag",
68
+ "-y": "flag",
69
+ "--yolo": "flag",
70
+ "--auto": "flag",
71
+ "-p": "required",
72
+ "--prompt": "required",
73
+ "--output-format": "required",
74
+ "--skills-dir": "required",
75
+ "--add-dir": "required",
76
+ "--effort": "required",
77
+ "--permission-mode": "required",
78
+ "--fork-session": "required",
79
+ "--allowed-tools": "required",
80
+ "--settings": "required",
81
+ "--yes": "flag",
82
+ "--auto-approve": "flag",
83
+ "--plan": "flag",
84
+ "-h": "flag",
85
+ "--help": "flag"
86
+ });
87
+ const ROOT_COMMAND_NAMES = new Set(["export","proof","brief","handoff","replay","guard","demo","workspace","acp","server","web","login","mistakes","memory","doctor","config","agents","sessions","status","models","logs","vis","setup","docs","upgrade","update","__plugin_run_node","tui","chat","terminal"]);
88
+
89
+ function parseExplicitProfileArgs(args, mode) {
90
+ if (mode !== LAUNCHER_MODES.KING) return { args: [...args], name: undefined };
91
+ const remaining = [];
92
+ let name;
93
+ let firstOperandSeen = false;
94
+ let unknownDestination = false;
95
+ for (let i = 0; i < args.length; i++) {
96
+ const arg = args[i];
97
+ if (arg === '--') {
98
+ remaining.push(...args.slice(i));
99
+ break;
100
+ }
101
+ if (arg === '--profile' || arg.startsWith('--profile=')) {
102
+ if (name !== undefined) throw new Error('--profile darf nur einmal angegeben werden.');
103
+ if (arg !== '--profile') throw new Error('--profile braucht einen getrennten Profilnamen.');
104
+ const pair = i + 1 < args.length ? [arg, args[++i]] : [arg];
105
+ name = parseProfileLaunchArgs(pair, mode).name;
106
+ continue;
107
+ }
108
+ remaining.push(arg);
109
+ let token = arg;
110
+ let arity;
111
+ let recognized = false;
112
+ let attached = false;
113
+ let separator = false;
114
+ // Replay Commander's virtual tokens after each BOOLEAN short option.
115
+ // The remainder can itself become --, a long option, or a value option:
116
+ // -cy- -> -c -y --; -cy-prompt -> -c -y --prompt.
117
+ for (;;) {
118
+ if (token === '--') { separator = true; break; }
119
+ arity = Object.hasOwn(ROOT_OPTION_ARITY, token) ? ROOT_OPTION_ARITY[token] : undefined;
120
+ recognized = arity !== undefined;
121
+ if (recognized) break;
122
+ if (token.length > 2 && token[0] === '-' && token[1] !== '-') {
123
+ const short = '-' + token[1];
124
+ arity = ROOT_OPTION_ARITY[short];
125
+ if (arity !== undefined) {
126
+ if (arity !== 'flag') { recognized = true; attached = true; break; }
127
+ token = '-' + token.slice(2);
128
+ continue;
129
+ }
130
+ }
131
+ if (/^--[^=]+=/u.test(token)) {
132
+ arity = ROOT_OPTION_ARITY[token.slice(0, token.indexOf('='))];
133
+ recognized = arity === 'required' || arity === 'optional';
134
+ attached = recognized;
135
+ }
136
+ break;
137
+ }
138
+ if (separator) {
139
+ remaining.push(...args.slice(i + 1));
140
+ break;
141
+ }
142
+ if (recognized) {
143
+ if (!attached && i + 1 < args.length
144
+ && (arity === 'required'
145
+ || (arity === 'optional' && !(args[i + 1].length > 1 && args[i + 1][0] === '-')))) {
146
+ remaining.push(args[++i]);
147
+ }
148
+ continue;
149
+ }
150
+ const maybeOption = token.length > 1 && token[0] === '-';
151
+ if (maybeOption) unknownDestination = true;
152
+ // Match Commander's first operand/unknown split. A command-looking value
153
+ // behind -p/-S, a previous positional target or an unknown option is NOT
154
+ // a new command boundary.
155
+ if (!unknownDestination && !firstOperandSeen) {
156
+ if (ROOT_COMMAND_NAMES.has(arg)) {
157
+ remaining.push(...args.slice(i + 1));
158
+ break;
159
+ }
160
+ firstOperandSeen = true;
161
+ }
162
+ }
163
+ return { args: remaining, name };
164
+ }
165
+
166
+ const PROFILE = parseExplicitProfileArgs(process.argv.slice(2), launcherModeFromArgv(process.argv));
167
+ const ARGS = PROFILE.args;
168
+
169
+ function applyProfileEnvironment(env, paths) {
170
+ env.BLUN_HOME = paths.blunHome;
171
+ if (PROFILE.name !== undefined) {
172
+ env.BLUN_SHARED_HOME = paths.sharedHome;
173
+ env.BLUN_PROFILE = PROFILE.name;
174
+ } else if (String(env.BLUN_STATE_DIR || '').trim()) {
175
+ env.BLUN_SHARED_HOME = paths.blunHome;
176
+ }
177
+ return env;
178
+ }
45
179
 
46
180
  function normalizeWindowsPath(value) {
47
181
  return path.win32.normalize(value).replace(/\\+$/u, '').toLowerCase();
@@ -51,6 +185,7 @@ function resolveLauncherPrivatePaths(options = {}) {
51
185
  const env = options.env || process.env;
52
186
  const platform = options.platform || process.platform;
53
187
  const configuredHome = String(env.BLUN_HOME || '').trim();
188
+ const selectedProfile = options.profileName ?? PROFILE.name;
54
189
  const stateDir = String(env.BLUN_STATE_DIR || '').trim();
55
190
  if (stateDir) {
56
191
  const paths = platform === 'win32' ? path.win32 : path.posix;
@@ -77,10 +212,35 @@ function resolveLauncherPrivatePaths(options = {}) {
77
212
  ancestor = parent;
78
213
  }
79
214
  }
215
+ if (selectedProfile !== undefined) {
216
+ // A state override cannot bypass the canonical OS-home/profile guards.
217
+ const profileEnv = { ...env };
218
+ delete profileEnv.BLUN_STATE_DIR;
219
+ const selected = resolveLauncherPrivatePaths({ ...options, env: profileEnv, profileName: selectedProfile });
220
+ const normalize = platform === 'win32' ? normalizeWindowsPath : paths.normalize;
221
+ if (normalize(blunHome) !== normalize(selected.blunHome)) {
222
+ throw new Error('BLUN_STATE_DIR widerspricht dem ausgewaehlten Profil.');
223
+ }
224
+ return selected;
225
+ }
80
226
  return { blunHome, userHome: (options.homedir || os.homedir)() };
81
227
  }
82
228
  if (platform !== 'win32') {
229
+ const posix = path.posix;
83
230
  const userHome = (options.homedir || os.homedir)();
231
+ if (selectedProfile !== undefined) {
232
+ const configuredShared = String(env.BLUN_SHARED_HOME || '').trim();
233
+ const sharedHome = configuredShared
234
+ ? posix.resolve(configuredShared)
235
+ : configuredHome && posix.basename(posix.dirname(configuredHome)) === 'profile'
236
+ ? posix.resolve(configuredHome, '..', '..')
237
+ : configuredHome ? posix.resolve(configuredHome) : posix.join(userHome, '.blun');
238
+ const blunHome = resolveSelectedProfile(sharedHome, selectedProfile, posix);
239
+ if (configuredShared && configuredHome && posix.resolve(configuredHome) !== blunHome) {
240
+ throw new Error(`BLUN_HOME passt nicht zum Profil ${selectedProfile}: ${blunHome}`);
241
+ }
242
+ return { blunHome, sharedHome, userHome };
243
+ }
84
244
  return {
85
245
  blunHome: configuredHome ? path.resolve(configuredHome) : path.join(userHome, '.blun'),
86
246
  userHome,
@@ -107,6 +267,19 @@ function resolveLauncherPrivatePaths(options = {}) {
107
267
  }
108
268
 
109
269
  const blunHome = path.win32.join(userHome, '.blun');
270
+ if (selectedProfile !== undefined) {
271
+ const profileHome = resolveSelectedProfile(blunHome, selectedProfile, path.win32);
272
+ const configuredShared = String(env.BLUN_SHARED_HOME || '').trim();
273
+ if (configuredShared && normalizeWindowsPath(path.win32.resolve(configuredShared)) !== normalizeWindowsPath(blunHome)) {
274
+ throw new Error(`BLUN_SHARED_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${blunHome}`);
275
+ }
276
+ if (configuredHome && normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(profileHome)
277
+ && normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(blunHome)) {
278
+ throw new Error(`BLUN_HOME passt nicht zum Profil ${selectedProfile}: ${profileHome}`);
279
+ }
280
+ return { blunHome: profileHome, sharedHome: blunHome, userHome };
281
+ }
282
+
110
283
  if (configuredHome
111
284
  && normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(blunHome)) {
112
285
  throw new Error(`BLUN_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${blunHome}`);
@@ -253,7 +426,7 @@ function installTelegramForProfile(packageRoot, blunDir) {
253
426
  ensurePrivateDirectory(channelDir);
254
427
  const envFile = path.join(channelDir, '.env');
255
428
  if (!fs.existsSync(envFile)) {
256
- writePrivateFile(envFile, 'BLUN_TELEGRAM_BOT_TOKEN=HIER_DEIN_BOT_TOKEN_EINFUEGEN\n');
429
+ writeTelegramTemplate(envFile, 'BLUN_TELEGRAM_BOT_TOKEN=HIER_DEIN_BOT_TOKEN_EINFUEGEN\n');
257
430
  } else {
258
431
  securePrivateFile(envFile);
259
432
  }
@@ -289,6 +462,9 @@ function spawnManagedLauncher(binary, cwd) {
289
462
  async function runLauncher(options = {}) {
290
463
  const callerCwd = process.cwd();
291
464
  const mode = options.mode || launcherModeFromArgv(process.argv);
465
+ if (PROFILE.name !== undefined) {
466
+ applyProfileEnvironment(process.env, resolveLauncherPrivatePaths());
467
+ }
292
468
  const explicitUpdateRequest = isNpmPackageUpdateRequest(ARGS);
293
469
  const explicitToolsRequest = String(ARGS[0] || '').toLowerCase() === 'tools';
294
470
  const noticeResult = options.skipUpdateNotice === true
@@ -393,7 +569,7 @@ async function runLauncher(options = {}) {
393
569
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
394
570
  const informationalEnv = { ...process.env };
395
571
  delete informationalEnv.BLUN_HOME;
396
- env.BLUN_HOME = resolveLauncherPrivatePaths({ env: informationalEnv }).blunHome;
572
+ applyProfileEnvironment(env, resolveLauncherPrivatePaths({ env: informationalEnv }));
397
573
  process.exitCode = await runInformationalCore(ARGS, env, callerCwd, releaseNotice);
398
574
  return;
399
575
  }
@@ -405,11 +581,11 @@ async function runLauncher(options = {}) {
405
581
  process.exitCode = 1;
406
582
  return;
407
583
  }
408
- const blunDir = resolveLauncherPrivatePaths().blunHome;
584
+ const inspectionPaths = resolveLauncherPrivatePaths();
585
+ const blunDir = inspectionPaths.blunHome;
409
586
  require('./verify-bundled-agent-sources.cjs').verifyBundledAgentSources(PKG);
410
587
  const env = createLauncherEnvironment(process.env, LAUNCHER_MODES.BLUN, readPackageVersion(), blunDir);
411
- env.BLUN_HOME = blunDir;
412
- if (String(process.env.BLUN_STATE_DIR || '').trim()) env.BLUN_SHARED_HOME = blunDir;
588
+ applyProfileEnvironment(env, inspectionPaths);
413
589
  process.exitCode = await runInformationalCore(ARGS, env, callerCwd, releaseNotice);
414
590
  return;
415
591
  }
@@ -439,7 +615,7 @@ async function runLauncher(options = {}) {
439
615
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
440
616
  const informationalEnv = { ...process.env };
441
617
  delete informationalEnv.BLUN_HOME;
442
- env.BLUN_HOME = resolveLauncherPrivatePaths({ env: informationalEnv }).blunHome;
618
+ applyProfileEnvironment(env, resolveLauncherPrivatePaths({ env: informationalEnv }));
443
619
  process.exitCode = await superviseProtectedCore(ARGS, env, callerCwd, releaseNotice);
444
620
  return;
445
621
  }
@@ -529,8 +705,7 @@ async function runLauncher(options = {}) {
529
705
 
530
706
  // --- 6. Start -----------------------------------------------------------
531
707
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion(), blunDir);
532
- env.BLUN_HOME = blunDir;
533
- if (String(process.env.BLUN_STATE_DIR || '').trim()) env.BLUN_SHARED_HOME = blunDir;
708
+ applyProfileEnvironment(env, getPrivatePaths());
534
709
  process.exitCode = await superviseProtectedCore(
535
710
  ARGS,
536
711
  env,
File without changes
@@ -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 };
File without changes
File without changes
File without changes
File without changes