blun-king-cli 9.1.628 → 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 +0 -0
- package/bin/core-bootstrap.js +0 -0
- package/bin/king.js +0 -0
- package/bin/launcher-mode.js +0 -0
- package/bin/launcher-runtime.js +183 -9
- package/bin/managed-node.js +0 -0
- package/bin/native-command-runner.js +0 -0
- package/bin/native-module-repair.js +0 -0
- package/bin/node-runtime.js +0 -0
- package/bin/node-version.js +0 -0
- package/bin/plugin-bootstrap.js +0 -0
- package/bin/private-paths.js +0 -0
- package/bin/profile-handoff.cjs +152 -0
- package/bin/profile-launch.cjs +120 -0
- package/bin/standard-tools-bootstrap.js +0 -0
- package/bin/update-copy.js +0 -0
- package/bin/update-lease.js +0 -0
- package/bin/update-notice.js +0 -0
- package/blun.mjs +0 -0
- package/package.json +1 -1
package/bin/blun.js
CHANGED
|
File without changes
|
package/bin/core-bootstrap.js
CHANGED
|
File without changes
|
package/bin/king.js
CHANGED
|
File without changes
|
package/bin/launcher-mode.js
CHANGED
|
File without changes
|
package/bin/launcher-runtime.js
CHANGED
|
@@ -40,9 +40,142 @@ const {
|
|
|
40
40
|
writePrivateFile,
|
|
41
41
|
} = require('./private-paths');
|
|
42
42
|
|
|
43
|
-
const { writeTelegramTemplate } = require('./rename-private-staging.cjs');
|
|
43
|
+
const { writeTelegramTemplate } = require('./rename-private-staging.cjs');
|
|
44
44
|
const PKG = path.resolve(__dirname, '..');
|
|
45
|
-
const
|
|
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
|
+
}
|
|
46
179
|
|
|
47
180
|
function normalizeWindowsPath(value) {
|
|
48
181
|
return path.win32.normalize(value).replace(/\\+$/u, '').toLowerCase();
|
|
@@ -52,6 +185,7 @@ function resolveLauncherPrivatePaths(options = {}) {
|
|
|
52
185
|
const env = options.env || process.env;
|
|
53
186
|
const platform = options.platform || process.platform;
|
|
54
187
|
const configuredHome = String(env.BLUN_HOME || '').trim();
|
|
188
|
+
const selectedProfile = options.profileName ?? PROFILE.name;
|
|
55
189
|
const stateDir = String(env.BLUN_STATE_DIR || '').trim();
|
|
56
190
|
if (stateDir) {
|
|
57
191
|
const paths = platform === 'win32' ? path.win32 : path.posix;
|
|
@@ -78,10 +212,35 @@ function resolveLauncherPrivatePaths(options = {}) {
|
|
|
78
212
|
ancestor = parent;
|
|
79
213
|
}
|
|
80
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
|
+
}
|
|
81
226
|
return { blunHome, userHome: (options.homedir || os.homedir)() };
|
|
82
227
|
}
|
|
83
228
|
if (platform !== 'win32') {
|
|
229
|
+
const posix = path.posix;
|
|
84
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
|
+
}
|
|
85
244
|
return {
|
|
86
245
|
blunHome: configuredHome ? path.resolve(configuredHome) : path.join(userHome, '.blun'),
|
|
87
246
|
userHome,
|
|
@@ -108,6 +267,19 @@ function resolveLauncherPrivatePaths(options = {}) {
|
|
|
108
267
|
}
|
|
109
268
|
|
|
110
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
|
+
|
|
111
283
|
if (configuredHome
|
|
112
284
|
&& normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(blunHome)) {
|
|
113
285
|
throw new Error(`BLUN_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${blunHome}`);
|
|
@@ -290,6 +462,9 @@ function spawnManagedLauncher(binary, cwd) {
|
|
|
290
462
|
async function runLauncher(options = {}) {
|
|
291
463
|
const callerCwd = process.cwd();
|
|
292
464
|
const mode = options.mode || launcherModeFromArgv(process.argv);
|
|
465
|
+
if (PROFILE.name !== undefined) {
|
|
466
|
+
applyProfileEnvironment(process.env, resolveLauncherPrivatePaths());
|
|
467
|
+
}
|
|
293
468
|
const explicitUpdateRequest = isNpmPackageUpdateRequest(ARGS);
|
|
294
469
|
const explicitToolsRequest = String(ARGS[0] || '').toLowerCase() === 'tools';
|
|
295
470
|
const noticeResult = options.skipUpdateNotice === true
|
|
@@ -394,7 +569,7 @@ async function runLauncher(options = {}) {
|
|
|
394
569
|
const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
|
|
395
570
|
const informationalEnv = { ...process.env };
|
|
396
571
|
delete informationalEnv.BLUN_HOME;
|
|
397
|
-
env
|
|
572
|
+
applyProfileEnvironment(env, resolveLauncherPrivatePaths({ env: informationalEnv }));
|
|
398
573
|
process.exitCode = await runInformationalCore(ARGS, env, callerCwd, releaseNotice);
|
|
399
574
|
return;
|
|
400
575
|
}
|
|
@@ -406,11 +581,11 @@ async function runLauncher(options = {}) {
|
|
|
406
581
|
process.exitCode = 1;
|
|
407
582
|
return;
|
|
408
583
|
}
|
|
409
|
-
const
|
|
584
|
+
const inspectionPaths = resolveLauncherPrivatePaths();
|
|
585
|
+
const blunDir = inspectionPaths.blunHome;
|
|
410
586
|
require('./verify-bundled-agent-sources.cjs').verifyBundledAgentSources(PKG);
|
|
411
587
|
const env = createLauncherEnvironment(process.env, LAUNCHER_MODES.BLUN, readPackageVersion(), blunDir);
|
|
412
|
-
env
|
|
413
|
-
if (String(process.env.BLUN_STATE_DIR || '').trim()) env.BLUN_SHARED_HOME = blunDir;
|
|
588
|
+
applyProfileEnvironment(env, inspectionPaths);
|
|
414
589
|
process.exitCode = await runInformationalCore(ARGS, env, callerCwd, releaseNotice);
|
|
415
590
|
return;
|
|
416
591
|
}
|
|
@@ -440,7 +615,7 @@ async function runLauncher(options = {}) {
|
|
|
440
615
|
const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
|
|
441
616
|
const informationalEnv = { ...process.env };
|
|
442
617
|
delete informationalEnv.BLUN_HOME;
|
|
443
|
-
env
|
|
618
|
+
applyProfileEnvironment(env, resolveLauncherPrivatePaths({ env: informationalEnv }));
|
|
444
619
|
process.exitCode = await superviseProtectedCore(ARGS, env, callerCwd, releaseNotice);
|
|
445
620
|
return;
|
|
446
621
|
}
|
|
@@ -530,8 +705,7 @@ async function runLauncher(options = {}) {
|
|
|
530
705
|
|
|
531
706
|
// --- 6. Start -----------------------------------------------------------
|
|
532
707
|
const env = createLauncherEnvironment(process.env, mode, readPackageVersion(), blunDir);
|
|
533
|
-
env
|
|
534
|
-
if (String(process.env.BLUN_STATE_DIR || '').trim()) env.BLUN_SHARED_HOME = blunDir;
|
|
708
|
+
applyProfileEnvironment(env, getPrivatePaths());
|
|
535
709
|
process.exitCode = await superviseProtectedCore(
|
|
536
710
|
ARGS,
|
|
537
711
|
env,
|
package/bin/managed-node.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/bin/node-runtime.js
CHANGED
|
File without changes
|
package/bin/node-version.js
CHANGED
|
File without changes
|
package/bin/plugin-bootstrap.js
CHANGED
|
File without changes
|
package/bin/private-paths.js
CHANGED
|
File without changes
|
|
@@ -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 };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
|
|
7
|
+
const RESERVED = new Set(['doctor', 'export', 'help', 'login', 'mcp', 'server', 'tools', 'update', 'upgrade', 'version']);
|
|
8
|
+
|
|
9
|
+
function profileName(value) {
|
|
10
|
+
const name = String(value || '').trim().toLowerCase();
|
|
11
|
+
if (!PROFILE_NAME.test(name)) {
|
|
12
|
+
throw new Error('Profilnamen duerfen nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.');
|
|
13
|
+
}
|
|
14
|
+
return name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseProfileLaunchArgs(args, mode) {
|
|
18
|
+
const remaining = [...args];
|
|
19
|
+
if (mode !== 'king') return { args: remaining, name: undefined };
|
|
20
|
+
|
|
21
|
+
const flag = remaining.indexOf('--profile');
|
|
22
|
+
if (flag >= 0) {
|
|
23
|
+
if (flag + 1 >= remaining.length) throw new Error('--profile braucht einen Profilnamen.');
|
|
24
|
+
const name = profileName(remaining[flag + 1]);
|
|
25
|
+
remaining.splice(flag, 2);
|
|
26
|
+
return { args: remaining, name };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (remaining.length === 1 && !remaining[0].startsWith('-') && !RESERVED.has(remaining[0].toLowerCase())) {
|
|
30
|
+
return { args: [], name: profileName(remaining[0]), implicit: true };
|
|
31
|
+
}
|
|
32
|
+
if (remaining.length === 2 && remaining.includes('-c')) {
|
|
33
|
+
const candidate = remaining.find(value => value !== '-c');
|
|
34
|
+
if (candidate && !candidate.startsWith('-')) return { args: ['-c'], name: profileName(candidate), implicit: true };
|
|
35
|
+
}
|
|
36
|
+
return { args: remaining, name: undefined };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveSelectedProfile(sharedHome, name, pathImpl = path) {
|
|
40
|
+
return pathImpl.join(sharedHome, 'profile', profileName(name));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function catalogUnavailable() {
|
|
44
|
+
return Object.assign(new Error('PROFILE_CATALOG_UNAVAILABLE'), { code: 'PROFILE_CATALOG_UNAVAILABLE' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function directoryIdentity(target, fsImpl) {
|
|
48
|
+
let stat;
|
|
49
|
+
try { stat = fsImpl.lstatSync(target, { bigint: true }); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT') return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw catalogUnavailable();
|
|
55
|
+
return stat;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function homeIdentity(home, fsImpl) {
|
|
59
|
+
const root = path.parse(home).root;
|
|
60
|
+
let current = root;
|
|
61
|
+
let stat = directoryIdentity(current, fsImpl);
|
|
62
|
+
for (const part of home.slice(root.length).split(path.sep).filter(Boolean)) {
|
|
63
|
+
if (stat === undefined) return undefined;
|
|
64
|
+
current = path.join(current, part);
|
|
65
|
+
stat = directoryIdentity(current, fsImpl);
|
|
66
|
+
}
|
|
67
|
+
return stat;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sameDirectory(before, after) {
|
|
71
|
+
return before !== undefined && after !== undefined && before.dev === after.dev && before.ino === after.ino;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Directory names only; selection must revalidate them before changing owner. */
|
|
75
|
+
function listExistingProfiles(sharedHome, options = {}) {
|
|
76
|
+
const fsImpl = options.fsImpl || fs;
|
|
77
|
+
try {
|
|
78
|
+
if (typeof sharedHome !== 'string' || !path.isAbsolute(sharedHome)) throw catalogUnavailable();
|
|
79
|
+
const home = path.resolve(sharedHome);
|
|
80
|
+
if (process.platform === 'win32' && home.startsWith('\\\\')) throw catalogUnavailable();
|
|
81
|
+
const beforeHome = homeIdentity(home, fsImpl);
|
|
82
|
+
if (beforeHome === undefined) return [];
|
|
83
|
+
const profileRoot = path.join(home, 'profile');
|
|
84
|
+
const beforeProfiles = directoryIdentity(profileRoot, fsImpl);
|
|
85
|
+
if (beforeProfiles === undefined) {
|
|
86
|
+
if (!sameDirectory(beforeHome, homeIdentity(home, fsImpl))) throw catalogUnavailable();
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const rows = [];
|
|
90
|
+
const dir = fsImpl.opendirSync(profileRoot);
|
|
91
|
+
try {
|
|
92
|
+
let count = 0;
|
|
93
|
+
for (let entry; (entry = dir.readSync()) !== null;) {
|
|
94
|
+
if (++count > 4096) throw catalogUnavailable();
|
|
95
|
+
const name = entry.name;
|
|
96
|
+
if (!PROFILE_NAME.test(name)) continue;
|
|
97
|
+
const target = resolveSelectedProfile(home, name);
|
|
98
|
+
let stat;
|
|
99
|
+
try { stat = fsImpl.lstatSync(target, { bigint: true }); }
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') continue;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) rows.push({ name, target, stat });
|
|
105
|
+
}
|
|
106
|
+
} finally { dir.closeSync(); }
|
|
107
|
+
if (!sameDirectory(beforeHome, homeIdentity(home, fsImpl))
|
|
108
|
+
|| !sameDirectory(beforeProfiles, directoryIdentity(profileRoot, fsImpl))) throw catalogUnavailable();
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
if (!sameDirectory(row.stat, directoryIdentity(row.target, fsImpl))) throw catalogUnavailable();
|
|
111
|
+
}
|
|
112
|
+
if (!sameDirectory(beforeHome, homeIdentity(home, fsImpl))
|
|
113
|
+
|| !sameDirectory(beforeProfiles, directoryIdentity(profileRoot, fsImpl))) throw catalogUnavailable();
|
|
114
|
+
return rows.map(row => row.name).sort();
|
|
115
|
+
} catch {
|
|
116
|
+
throw catalogUnavailable();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { parseProfileLaunchArgs, resolveSelectedProfile, listExistingProfiles };
|
|
File without changes
|
package/bin/update-copy.js
CHANGED
|
File without changes
|
package/bin/update-lease.js
CHANGED
|
File without changes
|
package/bin/update-notice.js
CHANGED
|
File without changes
|
package/blun.mjs
CHANGED
|
File without changes
|