blun-king-cli 9.1.616 → 9.1.618

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # BLUN King Changelog
2
2
 
3
+ ## 9.1.618
4
+
5
+ - Trifft eine Telegram-Nachricht beim Fortsetzen deiner Sitzung auf laufende Arbeit, bleibt sie in der Warteschlange und wird danach zugestellt. King zeigt dafür keine irreführende Meldung mehr an, dass bereits ein Zug läuft.
6
+ - Unter Linux x64 nutzt das Terminal eine mitgelieferte Binärdatei. Es funktioniert damit auch, wenn npm 12 Installationsskripte sperrt; ein Compiler ist dafür nicht nötig.
7
+
8
+ ## 9.1.617
9
+
10
+ King wartet beim Beenden auch auf Ereignisse, die beim Aufräumen laufender Arbeiten entstehen. Noch nicht abgeschlossene Arbeit wird nicht als beendet bestätigt.
11
+
3
12
  ## 9.1.616
4
13
 
5
14
  - Nach einer bestätigten Telegram-Antwort übernimmt King weitere an dich adressierte Nachrichten am nächsten sicheren Schritt, auch während eines laufenden Zugs. Eine bereits zugestellte Antwort hält die Warteschlange nicht mehr auf. Antworten bleiben ihrem jeweiligen Chat zugeordnet.
package/LIESMICH.txt CHANGED
@@ -26,7 +26,7 @@ Installation
26
26
  ------------
27
27
  Die geprüfte Version exakt global installieren:
28
28
 
29
- npm install -g blun-king-cli@9.1.616
29
+ npm install -g blun-king-cli@9.1.618
30
30
 
31
31
  Start
32
32
  -----
package/README.md CHANGED
@@ -9,7 +9,7 @@ Voraussetzung ist Node.js 24.15 oder neuer. Die geprüfte Version wird exakt
9
9
  installiert:
10
10
 
11
11
  ```powershell
12
- npm install -g blun-king-cli@9.1.616
12
+ npm install -g blun-king-cli@9.1.618
13
13
  ```
14
14
 
15
15
  ### Windows: Node.js, npm und Git Bash
@@ -4,7 +4,7 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { pathToFileURL } = require('node:url');
6
6
 
7
- const { acquireSharedRuntimeLease } = require('./update-lease');
7
+ const { acquireSharedRuntimeLease, tryAcquireUpdateLease } = require('./update-lease');
8
8
  const { installWindowsNodeCrashDump } = require('./windows-node-crash-dump.cjs');
9
9
  const { recordRuntimeExit } = require('./runtime-exit-ledger.cjs');
10
10
  const { isStandaloneHelpRequest } = require('./launcher-mode');
@@ -19,15 +19,25 @@ async function runCoreBootstrap() {
19
19
  }
20
20
  const packageRoot = path.resolve(__dirname, '..');
21
21
  const mainPath = path.join(packageRoot, 'blun.mjs');
22
- let leaseResult;
22
+ let leaseResult;
23
+ let profileLease;
23
24
 
24
25
  try {
25
26
  leaseResult = await acquireSharedRuntimeLease({ packageRoot });
26
27
  if (!leaseResult.acquired) throw new Error('RUNTIME_PROTECTION_UNAVAILABLE');
27
- leaseResult.lease.assertOwned();
28
+ leaseResult.lease.assertOwned();
29
+ if (process.env.BLUN_PROFILE_HANDOFF === '1') {
30
+ if (!path.isAbsolute(process.env.BLUN_HOME || '')) throw new Error('PROFILE_RUNTIME_UNAVAILABLE');
31
+ const result = await tryAcquireUpdateLease({ packageRoot: process.env.BLUN_HOME, role: 'profile' });
32
+ if (!result.acquired) throw Object.assign(new Error(`PROFILE_RUNTIME_${result.reason.toUpperCase()}`),
33
+ { code: result.reason === 'busy' ? 'PROFILE_RUNTIME_BUSY' : 'PROFILE_RUNTIME_UNAVAILABLE' });
34
+ profileLease = result.lease;
35
+ profileLease.assertOwned();
36
+ }
28
37
  process.argv = [process.execPath, mainPath, ...process.argv.slice(2)];
29
38
  await import(pathToFileURL(mainPath).href);
30
- leaseResult.lease.assertOwned();
39
+ leaseResult.lease.assertOwned();
40
+ profileLease?.assertOwned();
31
41
  } catch (error) {
32
42
  if (!informational) {
33
43
  try {
@@ -36,7 +46,8 @@ async function runCoreBootstrap() {
36
46
  kind: 'bootstrap-failure', phase: 'startup', error });
37
47
  } catch {}
38
48
  }
39
- try { diagnostics?.dispose(); } catch {}
49
+ try { diagnostics?.dispose(); } catch {}
50
+ try { await profileLease?.release(); } catch {}
40
51
  try { await leaseResult?.lease?.release(); }
41
52
  catch (leaseError) {
42
53
  if (!informational) {
@@ -52,11 +63,16 @@ async function runCoreBootstrap() {
52
63
  // The bundled CLI loads package assets lazily after module evaluation. Keep
53
64
  // the install lease for this process's lifetime, without keeping print/help
54
65
  // commands alive solely because of the owner server.
55
- leaseResult.lease.unref();
66
+ leaseResult.lease.unref();
67
+ profileLease?.unref();
56
68
 
57
69
  if (process.connected) {
58
70
  try {
59
- process.send({ type: CORE_LOADED_MESSAGE }, () => {
71
+ process.send({ type: CORE_LOADED_MESSAGE }, () => {
72
+ if (process.env.BLUN_PROFILE_HANDOFF === '1') {
73
+ process.channel?.unref();
74
+ return;
75
+ }
60
76
  try {
61
77
  process.disconnect();
62
78
  } catch {}
@@ -70,7 +86,7 @@ if (require.main === module) {
70
86
  void runCoreBootstrap().catch((error) => {
71
87
  const message = error && error.message ? error.message : String(error);
72
88
  fs.writeSync(process.stderr.fd, `BLUN Start fehlgeschlagen: ${message}\n`);
73
- process.exit(1);
89
+ process.exit(error?.code === 'PROFILE_RUNTIME_BUSY' ? 75 : 1);
74
90
  });
75
91
  }
76
92
 
@@ -39,6 +39,7 @@ const {
39
39
  writePrivateFile,
40
40
  } = require('./private-paths');
41
41
  const { parseProfileLaunchArgs, resolveSelectedProfile } = require('./profile-launch.cjs');
42
+ const { createProfileHandoff, PROFILE_HANDOFF_READY, PROFILE_HANDOFF_EXIT } = require('./profile-handoff.cjs');
42
43
 
43
44
  const PKG = path.resolve(__dirname, '..');
44
45
  const PROFILE = parseProfileLaunchArgs(process.argv.slice(2), launcherModeFromArgv(process.argv));
@@ -153,7 +154,7 @@ function shouldDetachProtectedCore(options = {}) {
153
154
  return platform !== 'win32' || stdinIsTTY !== true;
154
155
  }
155
156
 
156
- function spawnProtectedCore(args, env, cwd) {
157
+ function spawnProtectedCore(args, env, cwd, onProfileMessage) {
157
158
  const child = spawn(
158
159
  process.execPath,
159
160
  [path.join(PKG, 'bin', 'core-bootstrap.js'), ...args],
@@ -167,7 +168,8 @@ function spawnProtectedCore(args, env, cwd) {
167
168
  );
168
169
  let loadedSettled = false;
169
170
  let completedSettled = false;
170
- let operatorSignal;
171
+ let operatorSignal;
172
+ let profileReady = false;
171
173
  let resolveLoaded;
172
174
  let resolveCompleted;
173
175
  const loadTimer = setTimeout(() => {
@@ -201,16 +203,29 @@ function spawnProtectedCore(args, env, cwd) {
201
203
  signalHandlers.set(signal, handler);
202
204
  process.once(signal, handler);
203
205
  }
204
- child.on('message', (message) => {
205
- if (message?.type !== CORE_LOADED_MESSAGE) return;
206
- settleLoaded(true);
207
- try {
208
- child.disconnect();
209
- } catch {}
206
+ child.on('message', (message) => {
207
+ if (message?.type !== CORE_LOADED_MESSAGE) {
208
+ if (completedSettled || !onProfileMessage) return;
209
+ if (message?.type === PROFILE_HANDOFF_READY && Object.keys(message).length === 1) {
210
+ profileReady = true;
211
+ return;
212
+ }
213
+ if (!loadedSettled) return;
214
+ const reply = onProfileMessage(message);
215
+ if (reply) {
216
+ try { child.send(reply, () => {}); } catch {}
217
+ }
218
+ return;
219
+ }
220
+ settleLoaded(true);
221
+ if (onProfileMessage) child.channel?.unref();
222
+ else {
223
+ try { child.disconnect(); } catch {}
224
+ }
210
225
  });
211
226
  child.once('error', (error) => settleCompleted({ error }));
212
227
  child.once('exit', (code, signal) => settleCompleted({ code, signal }));
213
- return { child, completed, loaded };
228
+ return { child, completed, loaded, isProfileReady: () => profileReady };
214
229
  }
215
230
 
216
231
  async function runInformationalCore(args, env, cwd, releaseNotice) {
@@ -222,10 +237,14 @@ async function runInformationalCore(args, env, cwd, releaseNotice) {
222
237
  return Number.isInteger(result.code) ? result.code : exitCodeForChild(result, false);
223
238
  }
224
239
 
225
- async function superviseProtectedCore(args, env, cwd, releaseNotice) {
226
- let noticeReleased = false;
227
- for (;;) {
228
- const core = spawnProtectedCore(args, env, cwd);
240
+ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {}) {
241
+ let noticeReleased = false;
242
+ let rollback;
243
+ let recoveryAttempted = false;
244
+ for (;;) {
245
+ const handoff = options.prepareProfile
246
+ ? createProfileHandoff(env, target => runningTuiPid(target) !== null) : undefined;
247
+ const core = spawnProtectedCore(args, env, cwd, handoff && ((message) => handoff.prepare(message)));
229
248
  const loaded = await core.loaded;
230
249
  if (!noticeReleased) {
231
250
  noticeReleased = true;
@@ -236,12 +255,47 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice) {
236
255
  const intentionalStop = consumeIntentionalStopMarker({
237
256
  homeDir: env.BLUN_HOME, childPid: core.child.pid, nowMs: Date.now(),
238
257
  });
239
- recordRuntimeExit({
258
+ recordRuntimeExit({
240
259
  homeDir: env.BLUN_HOME, source: 'launcher', kind: result.error ? 'child-error' : 'child-exit',
241
260
  exitCode, signal: result.signal, phase: loaded ? 'runtime' : 'startup',
242
261
  cliVersion: env.BLUN_PUBLIC_PACKAGE_VERSION, profile: env.BLUN_PROFILE,
243
- loaded, childPid: core.child.pid, error: result.error,
244
- });
262
+ loaded, childPid: core.child.pid, error: result.error,
263
+ });
264
+ // A profile transition is committed only after the old process exits cleanly.
265
+ // Failed target startup restores the previous owner, never the previous PID.
266
+ if (rollback && (!loaded || !core.isProfileReady()) && !result.signal) {
267
+ ({ args, env } = rollback);
268
+ rollback = undefined;
269
+ continue;
270
+ }
271
+ if (loaded && core.isProfileReady()) {
272
+ rollback = undefined;
273
+ recoveryAttempted = false;
274
+ }
275
+ if (handoff && loaded && !intentionalStop && !result.error && !result.signal
276
+ && result.code === PROFILE_HANDOFF_EXIT) {
277
+ if (!handoff.hasPending()) return 1;
278
+ const sourceSessionId = handoff.getSourceSessionId();
279
+ const recoveryArgs = sourceSessionId === undefined ? ['-c'] : ['-r', sourceSessionId];
280
+ try {
281
+ const selected = handoff.commit();
282
+ const nextEnv = await options.prepareProfile(selected);
283
+ handoff.commit();
284
+ rollback = { args: recoveryArgs, env };
285
+ args = ['-c'];
286
+ env = { ...nextEnv, BLUN_PROFILE_HANDOFF: '1' };
287
+ continue;
288
+ } catch (error) {
289
+ recordRuntimeExit({ homeDir: env.BLUN_HOME, source: 'launcher', kind: 'profile-handoff-failure',
290
+ phase: 'startup', cliVersion: env.BLUN_PUBLIC_PACKAGE_VERSION, profile: env.BLUN_PROFILE,
291
+ exitCode: 1, error });
292
+ if (recoveryAttempted) return 1;
293
+ recoveryAttempted = true;
294
+ // Reopen the original owner when the selection became unavailable.
295
+ args = recoveryArgs;
296
+ continue;
297
+ }
298
+ }
245
299
  if (intentionalStop || !isAbnormalCoreExit(result, loaded)) {
246
300
  if (result.error) throw result.error;
247
301
  return exitCode;
@@ -272,7 +326,7 @@ function exitCodeForChild(result, loaded) {
272
326
  return 1;
273
327
  }
274
328
 
275
- function installTelegramForProfile(packageRoot, blunDir) {
329
+ function installTelegramForProfile(packageRoot, blunDir) {
276
330
  installManagedTelegramPlugin({ packageRoot, blunDir });
277
331
  const channelsDir = path.join(blunDir, 'channels');
278
332
  const channelDir = path.join(channelsDir, 'telegram');
@@ -284,7 +338,37 @@ function installTelegramForProfile(packageRoot, blunDir) {
284
338
  } else {
285
339
  securePrivateFile(envFile);
286
340
  }
287
- }
341
+ }
342
+
343
+ function prepareProfileRuntime(blunDir, callerCwd, mode) {
344
+ ensurePrivateDirectory(blunDir);
345
+ const cfgFile = path.join(blunDir, 'config.toml');
346
+ if (fs.existsSync(cfgFile)) securePrivateFile(cfgFile);
347
+ const standardMcpNames = availableStandardToolNames({ packageRoot: PKG })
348
+ .filter((name) => name !== 'telegram');
349
+ seedStandardTools({ packageRoot: PKG, blunDir, names: standardMcpNames });
350
+ seedStandardDesignSkill({ packageRoot: PKG, blunDir });
351
+ installManagedAgentSpinePlugin({ packageRoot: PKG, blunDir });
352
+ if (mode === LAUNCHER_MODES.KING) installTelegramForProfile(PKG, blunDir);
353
+ return repairConfiguredNativeModules({
354
+ blunDir, callerCwd,
355
+ onRepairStart() {
356
+ process.stdout.write('BLUN richtet native Node.js-Module fuer die neue Laufzeit ein.\n');
357
+ },
358
+ });
359
+ }
360
+
361
+ async function withProfileRuntimeReservation(homeDir, work) {
362
+ const result = await tryAcquireUpdateLease({ packageRoot: homeDir, role: 'profile' });
363
+ if (!result.acquired) throw Object.assign(new Error(`PROFILE_RUNTIME_${result.reason.toUpperCase()}`),
364
+ { code: result.reason === 'busy' ? 'PROFILE_RUNTIME_BUSY' : 'PROFILE_RUNTIME_UNAVAILABLE' });
365
+ try {
366
+ result.lease.assertOwned();
367
+ const value = await work();
368
+ result.lease.assertOwned();
369
+ return value;
370
+ } finally { await result.lease.release(); }
371
+ }
288
372
 
289
373
  function spawnManagedLauncher(binary, cwd) {
290
374
  return new Promise((resolve, reject) => {
@@ -511,51 +595,42 @@ async function runLauncher(options = {}) {
511
595
  // --- 2. Bestehende Konfiguration absichern -------------------------------
512
596
  // Ein frischer Start bleibt absichtlich ohne statischen Anbieter. /login
513
597
  // provisioniert King ueber den BLUN-OAuth-Server.
514
- const cfgFile = path.join(blunDir, 'config.toml');
515
- if (fs.existsSync(cfgFile)) securePrivateFile(cfgFile);
516
- require('./verify-bundled-agent-sources.cjs').verifyBundledAgentSources(PKG);
598
+ require('./verify-bundled-agent-sources.cjs').verifyBundledAgentSources(PKG);
599
+ const nativeModules = mode === LAUNCHER_MODES.KING && isInteractiveKingStart(ARGS)
600
+ ? await withProfileRuntimeReservation(blunDir, () => prepareProfileRuntime(blunDir, callerCwd, mode))
601
+ : prepareProfileRuntime(blunDir, callerCwd, mode);
517
602
 
518
- // --- 3. BLUN-Standard-MCPs additiv im Benutzerprofil einrichten ---------
519
- const standardMcpNames = availableStandardToolNames({ packageRoot: PKG })
520
- .filter((name) => name !== 'telegram');
521
- seedStandardTools({
522
- packageRoot: PKG,
523
- blunDir,
524
- names: standardMcpNames,
525
- });
526
-
527
- // --- 4. Eigenes BLUN-Grunddesign als einzigen Standard-Skill installieren
528
- seedStandardDesignSkill({ packageRoot: PKG, blunDir });
529
- installManagedAgentSpinePlugin({ packageRoot: PKG, blunDir });
530
-
531
- // --- 5. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
532
- if (mode === LAUNCHER_MODES.KING) {
533
- installTelegramForProfile(PKG, blunDir);
534
- }
535
-
536
- const nativeModules = repairConfiguredNativeModules({
537
- blunDir,
538
- callerCwd,
539
- onRepairStart() {
540
- process.stdout.write('BLUN richtet native Node.js-Module fuer die neue Laufzeit ein.\n');
541
- },
542
- });
543
603
  if (nativeModules.repaired.length > 0) {
544
604
  process.stdout.write('Native Node.js-Module sind wieder einsatzbereit.\n');
545
605
  }
546
606
 
547
- // --- 6. Start -----------------------------------------------------------
607
+ // --- 6. Start -----------------------------------------------------------
548
608
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion(), blunDir);
549
609
  env.BLUN_HOME = blunDir;
550
610
  if (PROFILE.name !== undefined) {
551
611
  env.BLUN_SHARED_HOME = getPrivatePaths().sharedHome;
552
612
  env.BLUN_PROFILE = PROFILE.name;
553
613
  }
614
+ const handoffOptions = mode === LAUNCHER_MODES.KING && isInteractiveKingStart(ARGS)
615
+ ? { prepareProfile: async (selected) => {
616
+ const target = selected.homeDir;
617
+ await withProfileRuntimeReservation(target, () => prepareProfileRuntime(target, callerCwd, mode));
618
+ const next = createLauncherEnvironment(process.env, mode, readPackageVersion(), target);
619
+ next.BLUN_HOME = target;
620
+ next.BLUN_SHARED_HOME = selected.sharedHome;
621
+ next.BLUN_PROFILE = selected.name;
622
+ delete next.BLUN_TELEGRAM_STATE_DIR;
623
+ delete next.BLUN_PLUGIN_ROOT;
624
+ return next;
625
+ } } : {};
626
+ if (handoffOptions.prepareProfile) env.BLUN_PROFILE_HANDOFF = '1';
627
+ else delete env.BLUN_PROFILE_HANDOFF;
554
628
  process.exitCode = await superviseProtectedCore(
555
629
  ARGS,
556
630
  env,
557
631
  callerCwd,
558
- releaseNotice,
632
+ releaseNotice,
633
+ handoffOptions,
559
634
  );
560
635
  } finally {
561
636
  await releaseNotice();
@@ -563,6 +638,7 @@ async function runLauncher(options = {}) {
563
638
  }
564
639
 
565
640
  module.exports = {
641
+ withProfileRuntimeReservation,
566
642
  resolveLauncherPrivatePaths,
567
643
  isInteractiveKingStart,
568
644
  runningTuiPid,
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { randomUUID } = require('node:crypto');
6
+ const { listExistingProfiles, resolveSelectedProfile } = require('./profile-launch.cjs');
7
+
8
+ const PROFILE_HANDOFF_REQUEST = 'blun-profile-handoff-request';
9
+ const PROFILE_HANDOFF_REPLY = 'blun-profile-handoff-reply';
10
+ const PROFILE_HANDOFF_CANCEL = 'blun-profile-handoff-cancel';
11
+ const PROFILE_HANDOFF_READY = 'blun-profile-handoff-ready';
12
+ const PROFILE_HANDOFF_EXIT = 76;
13
+ const REQUEST_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
14
+ const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u;
15
+ const samePath = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
16
+ const unavailable = () => Object.assign(new Error('PROFILE_HANDOFF_UNAVAILABLE'), { code: 'PROFILE_HANDOFF_UNAVAILABLE' });
17
+
18
+ function createProfileHandoff(env, isBusy = () => false) {
19
+ let pending;
20
+ const validate = (name) => {
21
+ if (typeof env.BLUN_HOME !== 'string' || !path.isAbsolute(env.BLUN_HOME)
22
+ || (env.BLUN_SHARED_HOME !== undefined && !path.isAbsolute(env.BLUN_SHARED_HOME))) throw unavailable();
23
+ const sharedHome = path.resolve(env.BLUN_SHARED_HOME || env.BLUN_HOME);
24
+ const homeDir = path.resolve(env.BLUN_HOME);
25
+ const current = env.BLUN_PROFILE;
26
+ if (current !== undefined) {
27
+ if (!env.BLUN_SHARED_HOME || path.basename(resolveSelectedProfile(sharedHome, current)) !== current
28
+ || !samePath(homeDir, resolveSelectedProfile(sharedHome, current))) throw unavailable();
29
+ } else if (!samePath(homeDir, sharedHome)) throw unavailable();
30
+ const names = listExistingProfiles(sharedHome);
31
+ if (typeof name !== 'string' || !names.includes(name) || name === current
32
+ || (current !== undefined && !names.includes(current))) throw unavailable();
33
+ const target = resolveSelectedProfile(sharedHome, name);
34
+ if (isBusy(target)) throw unavailable();
35
+ const identities = [sharedHome, path.join(sharedHome, 'profile'), homeDir, target]
36
+ .map(file => ({ file, stat: fs.lstatSync(file, { bigint: true }) }));
37
+ if (identities.some(({ stat }) => !stat.isDirectory() || stat.isSymbolicLink())) throw unavailable();
38
+ return { name, sharedHome, homeDir: target, identities };
39
+ };
40
+ return {
41
+ hasPending: () => pending !== undefined,
42
+ getSourceSessionId: () => pending?.sourceSessionId,
43
+ prepare(message) {
44
+ if (message?.type === PROFILE_HANDOFF_CANCEL && typeof message.requestId === 'string'
45
+ && REQUEST_ID.test(message.requestId) && Object.keys(message).sort().join(',') === 'requestId,type') {
46
+ if (pending?.requestId === message.requestId) pending = undefined;
47
+ return { type: PROFILE_HANDOFF_REPLY, requestId: message.requestId, status: 'cancelled' };
48
+ }
49
+ if (!message || message.type !== PROFILE_HANDOFF_REQUEST || typeof message.requestId !== 'string' || !REQUEST_ID.test(message.requestId)) return undefined;
50
+ const shape = Object.keys(message).sort().join(',');
51
+ if (shape !== 'name,requestId,type' && shape !== 'name,requestId,sourceSessionId,type') return undefined;
52
+ if (shape === 'name,requestId,sourceSessionId,type'
53
+ && (typeof message.sourceSessionId !== 'string' || !SESSION_ID.test(message.sourceSessionId))) return undefined;
54
+ let status = 'rejected';
55
+ try {
56
+ if (pending) {
57
+ if (pending.requestId === message.requestId && pending.name === message.name
58
+ && pending.sourceSessionId === message.sourceSessionId) status = 'prepared';
59
+ } else {
60
+ pending = { ...validate(message.name), requestId: message.requestId, sourceSessionId: message.sourceSessionId };
61
+ status = 'prepared';
62
+ }
63
+ } catch {}
64
+ return { type: PROFILE_HANDOFF_REPLY, requestId: message.requestId, status };
65
+ },
66
+ commit() {
67
+ try {
68
+ if (!pending) throw unavailable();
69
+ const fresh = validate(pending.name);
70
+ for (let index = 0; index < pending.identities.length; index++) {
71
+ const before = pending.identities[index];
72
+ const after = fresh.identities[index];
73
+ if (before.file !== after.file || before.stat.dev !== after.stat.dev || before.stat.ino !== after.stat.ino) throw unavailable();
74
+ }
75
+ return Object.freeze({ name: fresh.name, sharedHome: fresh.sharedHome, homeDir: fresh.homeDir,
76
+ ...(pending.sourceSessionId === undefined ? {} : { sourceSessionId: pending.sourceSessionId }) });
77
+ } catch {
78
+ throw unavailable();
79
+ }
80
+ },
81
+ };
82
+ }
83
+
84
+ function createProfileHandoffClient(channel = process) {
85
+ const enabled = channel.env?.BLUN_PROFILE_HANDOFF === '1';
86
+ let pending;
87
+ let disposed = false;
88
+ const available = () => !disposed && enabled && channel.connected === true
89
+ && typeof channel.send === 'function';
90
+ const cancel = () => {
91
+ const request = pending;
92
+ if (!request) return;
93
+ pending = undefined;
94
+ request.finish(false);
95
+ if (channel.connected === true) {
96
+ try { channel.send({ type: PROFILE_HANDOFF_CANCEL, requestId: request.id }, () => {}); } catch {}
97
+ }
98
+ };
99
+ return Object.freeze({
100
+ isPrepared: () => available() && pending?.prepared === true,
101
+ cancel,
102
+ dispose() { cancel(); disposed = true; },
103
+ /** @param {string} name @param {{ signal?: AbortSignal, timeoutMs?: number, sourceSessionId?: string }} [options] */
104
+ prepare(name, { signal, timeoutMs = 5000, sourceSessionId } = {}) {
105
+ try {
106
+ if (!available() || pending || signal?.aborted || typeof name !== 'string'
107
+ || path.basename(resolveSelectedProfile(path.parse(__dirname).root, name)) !== name
108
+ || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000
109
+ || (sourceSessionId !== undefined && (typeof sourceSessionId !== 'string' || !SESSION_ID.test(sourceSessionId)))) return Promise.resolve(false);
110
+ } catch { return Promise.resolve(false); }
111
+ return new Promise(resolve => {
112
+ const request = { id: randomUUID(), prepared: false, finish: undefined };
113
+ let settled = false;
114
+ let timer;
115
+ const aborted = () => cancel();
116
+ const disconnected = () => cancel();
117
+ const message = reply => {
118
+ if (pending !== request || reply?.type !== PROFILE_HANDOFF_REPLY || reply.requestId !== request.id
119
+ || Object.keys(reply).sort().join(',') !== 'requestId,status,type') return;
120
+ if (reply.status === 'prepared') request.finish(true);
121
+ else if (reply.status === 'rejected' || reply.status === 'cancelled') cancel();
122
+ };
123
+ request.finish = prepared => {
124
+ if (!prepared) signal?.removeEventListener('abort', aborted);
125
+ if (settled) return;
126
+ settled = true;
127
+ clearTimeout(timer);
128
+ channel.removeListener('message', message);
129
+ channel.removeListener('disconnect', disconnected);
130
+ try { channel.channel?.unref(); } catch {}
131
+ request.prepared = prepared;
132
+ resolve(prepared);
133
+ };
134
+ pending = request;
135
+ channel.on('message', message);
136
+ channel.once('disconnect', disconnected);
137
+ signal?.addEventListener('abort', aborted, { once: true });
138
+ timer = setTimeout(aborted, timeoutMs);
139
+ try {
140
+ channel.channel?.ref();
141
+ channel.send({ type: PROFILE_HANDOFF_REQUEST, requestId: request.id, name,
142
+ ...(sourceSessionId === undefined ? {} : { sourceSessionId }) }, error => {
143
+ if (error && pending === request) cancel();
144
+ });
145
+ } catch { cancel(); }
146
+ });
147
+ },
148
+ });
149
+ }
150
+
151
+ module.exports = { createProfileHandoff, createProfileHandoffClient, PROFILE_HANDOFF_CANCEL, PROFILE_HANDOFF_REQUEST, PROFILE_HANDOFF_REPLY,
152
+ PROFILE_HANDOFF_READY, PROFILE_HANDOFF_EXIT };
@@ -34,7 +34,9 @@ function canonicalPackageIdentity(packageRoot, options = {}) {
34
34
 
35
35
  function deriveUpdateLeaseEndpoint(packageRoot, options = {}) {
36
36
  const platform = options.platform || process.platform;
37
- const role = options.role === 'install'
37
+ const role = options.role === 'profile'
38
+ ? 'profile'
39
+ : options.role === 'install'
38
40
  ? 'install'
39
41
  : options.role === 'runtime'
40
42
  ? 'runtime'