blun-king-cli 9.1.60 → 9.1.62

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.
@@ -0,0 +1,389 @@
1
+ 'use strict';
2
+
3
+ const { createHash, randomUUID } = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const https = require('node:https');
6
+ const path = require('node:path');
7
+ const { spawn } = require('node:child_process');
8
+
9
+ const { compareSemver, requestTrustedJson } = require('./update-notice');
10
+ const { ensurePrivateDirectory, securePrivateFile, writePrivateFile } = require('./private-paths');
11
+
12
+ const PACKAGE_NAME = 'blun-king-cli';
13
+ const MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
14
+ const REGISTRY_URL = 'https://registry.npmjs.org/blun-king-cli';
15
+ const ACTIVE_RUNTIME_FILE = 'active-runtime.json';
16
+ const RUNNING_UPDATE_HANDOFF_EXIT_CODE = 76;
17
+ const RUNNING_UPDATE_PREPARED_MESSAGE = 'blun-running-update-prepared';
18
+ const RUNNING_UPDATE_HANDOFF_MESSAGE = 'blun-running-update-handoff';
19
+ const RUNTIME_READY_MESSAGE = 'blun-runtime-session-ready';
20
+ const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
21
+
22
+ function ownData(value, key) {
23
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
24
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
25
+ return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
26
+ }
27
+
28
+ function validIntegrity(value) {
29
+ if (typeof value !== 'string') return false;
30
+ const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
31
+ if (!match) return false;
32
+ try {
33
+ return Buffer.from(match[1], 'base64').length === 64;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function selectRunningUpdateTarget(currentVersion, manifest) {
40
+ const name = ownData(manifest, 'name');
41
+ const version = ownData(manifest, 'latest');
42
+ const integrity = ownData(manifest, 'integrity');
43
+ const releasedAt = ownData(manifest, 'releasedAt');
44
+ if (name !== PACKAGE_NAME
45
+ || typeof version !== 'string'
46
+ || compareSemver(currentVersion, version) !== -1
47
+ || !validIntegrity(integrity)
48
+ || typeof releasedAt !== 'string'
49
+ || !Number.isFinite(Date.parse(releasedAt))) {
50
+ return null;
51
+ }
52
+ return Object.freeze({ version, integrity });
53
+ }
54
+
55
+ function isSafeRuntimeBoundary(state) {
56
+ return state?.isShuttingDown === false
57
+ && state.streamingPhase === 'idle'
58
+ && state.isCompacting === false
59
+ && state.queuedMessages === 0
60
+ && state.activeToolCalls === 0
61
+ && state.shellCommands === 0
62
+ && state.queueCommandRunning === false;
63
+ }
64
+
65
+ function resumeArgsForHandoff(args, sessionId) {
66
+ const filtered = [];
67
+ for (let index = 0; index < args.length; index += 1) {
68
+ const arg = args[index];
69
+ if (arg === '-c' || arg === '--continue') continue;
70
+ if (arg === '-r' || arg === '--resume' || arg === '--session') {
71
+ index += 1;
72
+ continue;
73
+ }
74
+ if (/^(?:--resume|--session)=/u.test(arg)) continue;
75
+ filtered.push(arg);
76
+ }
77
+ return [...filtered, '--resume', sessionId];
78
+ }
79
+
80
+ async function handoffRuntime(options) {
81
+ const handoffArgs = resumeArgsForHandoff(options.args, options.sessionId);
82
+ if (!await options.probeTarget(options.target)) {
83
+ return { kind: 'probe-failed', runtime: options.previous };
84
+ }
85
+ await options.stopOld();
86
+ const targetCore = await options.startCore({
87
+ packageRoot: options.target.packageRoot,
88
+ args: handoffArgs,
89
+ });
90
+ if (targetCore.ready === true) {
91
+ await options.activateTarget(options.target);
92
+ return { kind: 'activated', runtime: options.target, core: targetCore };
93
+ }
94
+ const fallbackCore = await options.startCore({
95
+ packageRoot: options.previous.packageRoot,
96
+ args: handoffArgs,
97
+ });
98
+ return { kind: 'rolled-back', runtime: options.previous, core: fallbackCore };
99
+ }
100
+
101
+ function releasesRoot(sharedHome) {
102
+ return path.join(path.resolve(sharedHome), 'updates', 'releases');
103
+ }
104
+
105
+ function activeRuntimePath(sharedHome) {
106
+ return path.join(path.resolve(sharedHome), 'updates', ACTIVE_RUNTIME_FILE);
107
+ }
108
+
109
+ function pathWithin(root, candidate) {
110
+ const relative = path.relative(root, candidate);
111
+ return relative === '' || (relative !== '..'
112
+ && !relative.startsWith(`..${path.sep}`)
113
+ && !path.isAbsolute(relative));
114
+ }
115
+
116
+ function verifyRuntimePackage(packageRoot, version, options = {}) {
117
+ try {
118
+ const root = path.resolve(packageRoot);
119
+ const allowedRoot = path.resolve(options.allowedRoot || releasesRoot(options.sharedHome));
120
+ if (!pathWithin(allowedRoot, root)) return false;
121
+ const manifestPath = path.join(root, 'package.json');
122
+ const bundlePath = path.join(root, 'blun.mjs');
123
+ const bootstrapPath = path.join(root, 'bin', 'core-bootstrap.js');
124
+ for (const filePath of [manifestPath, bundlePath, bootstrapPath]) {
125
+ const stat = fs.lstatSync(filePath);
126
+ if (!stat.isFile() || stat.isSymbolicLink()) return false;
127
+ }
128
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
129
+ return ownData(manifest, 'name') === PACKAGE_NAME && ownData(manifest, 'version') === version;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ function readActiveRuntime(sharedHome) {
136
+ try {
137
+ const filePath = activeRuntimePath(sharedHome);
138
+ const stat = fs.lstatSync(filePath);
139
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024) return null;
140
+ const record = JSON.parse(fs.readFileSync(filePath, 'utf8'));
141
+ const version = ownData(record, 'version');
142
+ const packageRoot = ownData(record, 'packageRoot');
143
+ const integrity = ownData(record, 'integrity');
144
+ if (typeof version !== 'string'
145
+ || typeof packageRoot !== 'string'
146
+ || !path.isAbsolute(packageRoot)
147
+ || !validIntegrity(integrity)
148
+ || !verifyRuntimePackage(packageRoot, version, { sharedHome })) {
149
+ return null;
150
+ }
151
+ return Object.freeze({ version, packageRoot: path.resolve(packageRoot), integrity });
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function activateRuntime(sharedHome, target) {
158
+ if (!verifyRuntimePackage(target.packageRoot, target.version, { sharedHome })
159
+ || !validIntegrity(target.integrity)) {
160
+ throw new Error('RUNNING_UPDATE_TARGET_INVALID');
161
+ }
162
+ const updateRoot = path.join(path.resolve(sharedHome), 'updates');
163
+ ensurePrivateDirectory(path.resolve(sharedHome));
164
+ ensurePrivateDirectory(updateRoot);
165
+ const filePath = activeRuntimePath(sharedHome);
166
+ const temporaryPath = path.join(updateRoot, `.${ACTIVE_RUNTIME_FILE}.${process.pid}.${randomUUID()}.tmp`);
167
+ try {
168
+ writePrivateFile(temporaryPath, `${JSON.stringify({
169
+ version: target.version,
170
+ packageRoot: path.resolve(target.packageRoot),
171
+ integrity: target.integrity,
172
+ activatedAt: new Date().toISOString(),
173
+ })}\n`);
174
+ fs.renameSync(temporaryPath, filePath);
175
+ securePrivateFile(filePath);
176
+ } catch (error) {
177
+ fs.rmSync(temporaryPath, { force: true });
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ function registryRelease(registry, version) {
183
+ const versions = ownData(registry, 'versions');
184
+ const entry = ownData(versions, version);
185
+ const dist = ownData(entry, 'dist');
186
+ const integrity = ownData(dist, 'integrity');
187
+ const tarball = ownData(dist, 'tarball');
188
+ if (!validIntegrity(integrity) || typeof tarball !== 'string') return null;
189
+ let url;
190
+ try {
191
+ url = new URL(tarball);
192
+ } catch {
193
+ return null;
194
+ }
195
+ if (url.protocol !== 'https:'
196
+ || url.hostname !== 'registry.npmjs.org'
197
+ || url.username !== ''
198
+ || url.password !== '') {
199
+ return null;
200
+ }
201
+ return { integrity, tarball: url.href };
202
+ }
203
+
204
+ function downloadTarball(url, destination, options = {}) {
205
+ const httpsImpl = options.httpsImpl || https;
206
+ return new Promise((resolve, reject) => {
207
+ const request = httpsImpl.get(url, {
208
+ headers: { 'user-agent': 'blun-king-running-update' },
209
+ timeout: options.timeoutMs || 30_000,
210
+ }, (response) => {
211
+ if (response.statusCode !== 200) {
212
+ response.resume();
213
+ reject(new Error(`RUNNING_UPDATE_DOWNLOAD_${String(response.statusCode)}`));
214
+ return;
215
+ }
216
+ let bytes = 0;
217
+ const handle = fs.openSync(destination, 'wx', 0o600);
218
+ let settled = false;
219
+ const fail = (error) => {
220
+ if (settled) return;
221
+ settled = true;
222
+ try { fs.closeSync(handle); } catch {}
223
+ fs.rmSync(destination, { force: true });
224
+ reject(error);
225
+ };
226
+ response.on('data', (chunk) => {
227
+ bytes += chunk.length;
228
+ if (bytes > MAX_TARBALL_BYTES) {
229
+ response.destroy(new Error('RUNNING_UPDATE_TARBALL_TOO_LARGE'));
230
+ return;
231
+ }
232
+ try {
233
+ fs.writeSync(handle, chunk);
234
+ } catch (error) {
235
+ response.destroy(error);
236
+ }
237
+ });
238
+ response.once('error', fail);
239
+ response.once('end', () => {
240
+ if (settled) return;
241
+ settled = true;
242
+ fs.fsyncSync(handle);
243
+ fs.closeSync(handle);
244
+ resolve();
245
+ });
246
+ });
247
+ request.once('timeout', () => request.destroy(new Error('RUNNING_UPDATE_DOWNLOAD_TIMEOUT')));
248
+ request.once('error', reject);
249
+ });
250
+ }
251
+
252
+ function resolveNpmCliPath(execPath = process.execPath) {
253
+ const executableDirectory = path.dirname(path.resolve(execPath));
254
+ const candidates = [
255
+ path.join(executableDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
256
+ path.resolve(executableDirectory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
257
+ path.resolve(executableDirectory, '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
258
+ path.resolve(__dirname, '..', '..', 'npm', 'bin', 'npm-cli.js'),
259
+ ];
260
+ for (const candidate of candidates) {
261
+ try {
262
+ const resolved = fs.realpathSync(candidate);
263
+ if (fs.statSync(resolved).isFile() && path.basename(resolved) === 'npm-cli.js') return resolved;
264
+ } catch {}
265
+ }
266
+ throw new Error('TRUSTED_NPM_CLI_NOT_FOUND');
267
+ }
268
+
269
+ function runProcess(command, args, options = {}) {
270
+ return new Promise((resolve, reject) => {
271
+ const child = (options.spawnImpl || spawn)(command, args, {
272
+ cwd: options.cwd,
273
+ env: options.env,
274
+ stdio: options.stdio || 'ignore',
275
+ windowsHide: true,
276
+ });
277
+ child.once('error', reject);
278
+ child.once('exit', (code, signal) => {
279
+ if (code === 0) resolve();
280
+ else reject(new Error(`RUNNING_UPDATE_PROCESS_FAILED:${code ?? signal ?? 'unknown'}`));
281
+ });
282
+ });
283
+ }
284
+
285
+ async function defaultInstallTarball(tarballPath, installPrefix, options = {}) {
286
+ const npmCliPath = resolveNpmCliPath(options.execPath);
287
+ const env = {
288
+ ...process.env,
289
+ TEMP: installPrefix,
290
+ TMP: installPrefix,
291
+ TMPDIR: installPrefix,
292
+ npm_config_registry: 'https://registry.npmjs.org/',
293
+ npm_config_strict_ssl: 'true',
294
+ };
295
+ await runProcess(options.execPath || process.execPath, [
296
+ npmCliPath,
297
+ 'install',
298
+ `--prefix=${installPrefix}`,
299
+ '--ignore-scripts=false',
300
+ '--package-lock=false',
301
+ '--audit=false',
302
+ '--fund=false',
303
+ tarballPath,
304
+ ], { cwd: installPrefix, env, spawnImpl: options.spawnImpl });
305
+ }
306
+
307
+ async function defaultProbeRuntime(packageRoot, options = {}) {
308
+ try {
309
+ await runProcess(options.execPath || process.execPath, ['--check', path.join(packageRoot, 'blun.mjs')], options);
310
+ await runProcess(options.execPath || process.execPath, ['--check', path.join(packageRoot, 'bin', 'core-bootstrap.js')], options);
311
+ return true;
312
+ } catch {
313
+ return false;
314
+ }
315
+ }
316
+
317
+ async function prepareRunningUpdate(options) {
318
+ if (String(options.env?.BLUN_NO_AUTO_UPDATE || '') === '1') return null;
319
+ const manifest = await (options.loadManifest
320
+ ? options.loadManifest()
321
+ : requestTrustedJson(MANIFEST_URL));
322
+ const target = selectRunningUpdateTarget(options.currentVersion, manifest);
323
+ if (target === null) return null;
324
+ const registry = await (options.loadRegistry
325
+ ? options.loadRegistry()
326
+ : requestTrustedJson(REGISTRY_URL));
327
+ const release = registryRelease(registry, target.version);
328
+ if (release === null || release.integrity !== target.integrity) {
329
+ throw new Error('RUNNING_UPDATE_INTEGRITY_SOURCE_MISMATCH');
330
+ }
331
+ const digest = createHash('sha256').update(target.integrity).digest('hex').slice(0, 16);
332
+ const releaseDirectory = path.join(releasesRoot(options.sharedHome), target.version, digest);
333
+ const packageRoot = path.join(releaseDirectory, 'node_modules', PACKAGE_NAME);
334
+ if (verifyRuntimePackage(packageRoot, target.version, { sharedHome: options.sharedHome })) {
335
+ return Object.freeze({ ...target, packageRoot });
336
+ }
337
+
338
+ const updateRoot = path.join(path.resolve(options.sharedHome), 'updates');
339
+ const stagingRoot = path.join(updateRoot, 'staging');
340
+ ensurePrivateDirectory(path.resolve(options.sharedHome));
341
+ ensurePrivateDirectory(updateRoot);
342
+ ensurePrivateDirectory(stagingRoot);
343
+ const temporaryRoot = fs.mkdtempSync(path.join(stagingRoot, 'runtime-'));
344
+ ensurePrivateDirectory(temporaryRoot);
345
+ const tarballPath = path.join(temporaryRoot, `${PACKAGE_NAME}.tgz`);
346
+ const installPrefix = path.join(temporaryRoot, 'install');
347
+ ensurePrivateDirectory(installPrefix);
348
+ try {
349
+ await (options.downloadTarball || downloadTarball)(release.tarball, tarballPath);
350
+ const actualIntegrity = `sha512-${createHash('sha512').update(fs.readFileSync(tarballPath)).digest('base64')}`;
351
+ if (actualIntegrity !== target.integrity) throw new Error('RUNNING_UPDATE_TARBALL_INTEGRITY_FAILED');
352
+ await (options.installTarball || defaultInstallTarball)(tarballPath, installPrefix, options);
353
+ const installedRoot = path.join(installPrefix, 'node_modules', PACKAGE_NAME);
354
+ if (!verifyRuntimePackage(installedRoot, target.version, { allowedRoot: installPrefix })) {
355
+ throw new Error('RUNNING_UPDATE_PACKAGE_INVALID');
356
+ }
357
+ if (!await (options.probeRuntime || defaultProbeRuntime)(installedRoot, options)) {
358
+ throw new Error('RUNNING_UPDATE_PROBE_FAILED');
359
+ }
360
+ ensurePrivateDirectory(releasesRoot(options.sharedHome));
361
+ ensurePrivateDirectory(path.dirname(releaseDirectory));
362
+ if (fs.existsSync(releaseDirectory)) fs.rmSync(releaseDirectory, { recursive: true, force: true });
363
+ fs.renameSync(installPrefix, releaseDirectory);
364
+ if (!verifyRuntimePackage(packageRoot, target.version, { sharedHome: options.sharedHome })) {
365
+ throw new Error('RUNNING_UPDATE_ACTIVATION_PACKAGE_INVALID');
366
+ }
367
+ return Object.freeze({ ...target, packageRoot });
368
+ } finally {
369
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
370
+ }
371
+ }
372
+
373
+ module.exports = {
374
+ ACTIVE_RUNTIME_FILE,
375
+ RUNNING_UPDATE_HANDOFF_EXIT_CODE,
376
+ RUNNING_UPDATE_HANDOFF_MESSAGE,
377
+ RUNNING_UPDATE_PREPARED_MESSAGE,
378
+ RUNTIME_READY_MESSAGE,
379
+ activateRuntime,
380
+ activeRuntimePath,
381
+ defaultProbeRuntime,
382
+ handoffRuntime,
383
+ isSafeRuntimeBoundary,
384
+ prepareRunningUpdate,
385
+ readActiveRuntime,
386
+ resumeArgsForHandoff,
387
+ selectRunningUpdateTarget,
388
+ verifyRuntimePackage,
389
+ };
@@ -54,8 +54,18 @@ const LEGACY_MANIFEST_KEYS = Object.freeze([
54
54
  'notesUrl',
55
55
  'releasedAt',
56
56
  ]);
57
+ const RELEASE_NOTES_MANIFEST_KEYS = Object.freeze([
58
+ 'install',
59
+ 'latest',
60
+ 'minSupported',
61
+ 'name',
62
+ 'notesUrl',
63
+ 'releaseNotes',
64
+ 'releasedAt',
65
+ ]);
57
66
  const MANIFEST_KEYS = Object.freeze([
58
67
  'install',
68
+ 'integrity',
59
69
  'latest',
60
70
  'minSupported',
61
71
  'name',
@@ -214,8 +224,20 @@ function parseReleaseNotes(value) {
214
224
  return Object.freeze(notes);
215
225
  }
216
226
 
227
+ function validIntegrity(value) {
228
+ if (typeof value !== 'string') return false;
229
+ const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
230
+ if (!match) return false;
231
+ try {
232
+ return Buffer.from(match[1], 'base64').length === 64;
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+
217
238
  function parseFallbackManifest(value) {
218
239
  const record = exactDataRecord(value, MANIFEST_KEYS)
240
+ || exactDataRecord(value, RELEASE_NOTES_MANIFEST_KEYS)
219
241
  || exactDataRecord(value, LEGACY_MANIFEST_KEYS);
220
242
  const notesUrl = record ? canonicalOfficialNotesUrl(record.notesUrl) : undefined;
221
243
  const releaseNotes = record && Object.hasOwn(record, 'releaseNotes')
@@ -228,6 +250,7 @@ function parseFallbackManifest(value) {
228
250
  || compareSemver(record.minSupported, record.latest) === 1
229
251
  || !isIsoTimestamp(record.releasedAt)
230
252
  || !notesUrl
253
+ || (Object.hasOwn(record, 'integrity') && !validIntegrity(record.integrity))
231
254
  || (Object.hasOwn(record, 'releaseNotes') && releaseNotes === undefined)
232
255
  || !isBoundedText(record.install, 256)) {
233
256
  return undefined;
@@ -238,6 +261,7 @@ function parseFallbackManifest(value) {
238
261
  minSupported: record.minSupported,
239
262
  releasedAt: record.releasedAt,
240
263
  notesUrl,
264
+ ...(Object.hasOwn(record, 'integrity') ? { integrity: record.integrity } : {}),
241
265
  ...(releaseNotes === undefined ? {} : { releaseNotes }),
242
266
  install: record.install,
243
267
  });