blun-king-cli 9.1.616 → 9.1.617
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 +108 -104
- package/LIESMICH.txt +56 -56
- package/README.md +89 -89
- package/bin/core-bootstrap.js +24 -8
- package/bin/launcher-runtime.js +124 -48
- package/bin/profile-handoff.cjs +152 -0
- package/bin/update-lease.js +3 -1
- package/blun.mjs +3017 -1029
- package/package.json +1 -1
- package/worker-host.mjs +2185 -680
package/bin/launcher-runtime.js
CHANGED
|
@@ -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)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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 };
|
package/bin/update-lease.js
CHANGED
|
@@ -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 === '
|
|
37
|
+
const role = options.role === 'profile'
|
|
38
|
+
? 'profile'
|
|
39
|
+
: options.role === 'install'
|
|
38
40
|
? 'install'
|
|
39
41
|
: options.role === 'runtime'
|
|
40
42
|
? 'runtime'
|