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 CHANGED
File without changes
File without changes
package/bin/king.js CHANGED
File without changes
File without changes
@@ -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 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
+ }
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.BLUN_HOME = resolveLauncherPrivatePaths({ env: informationalEnv }).blunHome;
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 blunDir = resolveLauncherPrivatePaths().blunHome;
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.BLUN_HOME = blunDir;
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.BLUN_HOME = resolveLauncherPrivatePaths({ env: informationalEnv }).blunHome;
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.BLUN_HOME = blunDir;
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,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
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
File without changes
File without changes
File without changes
package/blun.mjs CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.628",
3
+ "version": "9.1.629",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {