blun-king-cli 9.0.0 → 9.0.1

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.
Files changed (55) hide show
  1. package/LIESMICH.txt +1 -7
  2. package/README.md +4 -16
  3. package/bin/blun.js +248 -160
  4. package/bin/core-bootstrap.js +47 -0
  5. package/bin/king.js +277 -1
  6. package/bin/launcher-mode.js +2 -1
  7. package/bin/launcher-runtime.js +221 -0
  8. package/bin/plugin-bootstrap.js +0 -0
  9. package/bin/private-paths.js +0 -0
  10. package/bin/update-lease.js +399 -0
  11. package/bin/update-notice.js +1094 -0
  12. package/blun.mjs +4060 -6667
  13. package/package.json +3 -10
  14. package/skills/screenshot-lesen/SKILL.md +0 -1
  15. package/skills/web-lesen/SKILL.md +0 -1
  16. package/telegram-plugin/dist/bridge.mjs +1 -21
  17. package/mnemo/access_routes.js +0 -692
  18. package/mnemo/agent_governance.js +0 -4242
  19. package/mnemo/agent_mail.js +0 -901
  20. package/mnemo/bootstrap_auto.js +0 -137
  21. package/mnemo/brief_coordination.js +0 -226
  22. package/mnemo/code_read_tools.js +0 -375
  23. package/mnemo/context_preview_tools.js +0 -603
  24. package/mnemo/embeddings.js +0 -66
  25. package/mnemo/external_repo_ops.js +0 -575
  26. package/mnemo/facts/example-project-rules.json +0 -90
  27. package/mnemo/facts/example.json +0 -34
  28. package/mnemo/identity_schema.sql +0 -139
  29. package/mnemo/journal_schema.js +0 -561
  30. package/mnemo/loop_doctor_tools.js +0 -661
  31. package/mnemo/mail_secret_refs.js +0 -150
  32. package/mnemo/mcp.js +0 -9309
  33. package/mnemo/memory_consolidation.js +0 -1914
  34. package/mnemo/memory_health_tools.js +0 -165
  35. package/mnemo/package.json +0 -79
  36. package/mnemo/protected_scope_gate.js +0 -627
  37. package/mnemo/resource_access_control.js +0 -684
  38. package/mnemo/runtime_governance.js +0 -1256
  39. package/mnemo/runtime_turn_gate.js +0 -862
  40. package/mnemo/sandbox.js +0 -143
  41. package/mnemo/schema.sql +0 -389
  42. package/mnemo/shared_utils.js +0 -763
  43. package/mnemo/skills/agent-auto-resume/SKILL.md +0 -56
  44. package/mnemo/skills/agent_hand/SKILL.md +0 -43
  45. package/mnemo/skills/agent_hand/run.js +0 -63
  46. package/mnemo/skills/book_flight/SKILL.md +0 -34
  47. package/mnemo/skills/external_repo_review/SKILL.md +0 -43
  48. package/mnemo/skills/external_repo_review/run.js +0 -73
  49. package/mnemo/skills/pay_invoice/SKILL.md +0 -34
  50. package/mnemo/team_quality_ops.js +0 -944
  51. package/mnemo/timeline_report_tools.js +0 -810
  52. package/mnemo/write_gate_risk.js +0 -80
  53. package/mnemo/writer_health.js +0 -152
  54. package/skills/doku-ingestion/SKILL.md +0 -48
  55. package/skills/doku-ingestion/ingest_docs.py +0 -133
@@ -0,0 +1,1094 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const https = require('node:https');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { fork } = require('node:child_process');
9
+ const { StringDecoder } = require('node:string_decoder');
10
+ const { types } = require('node:util');
11
+
12
+ const { tryAcquireUpdateLease } = require('./update-lease');
13
+
14
+ const {
15
+ ensurePrivateDirectory,
16
+ securePrivateFile,
17
+ writePrivateFile,
18
+ } = require('./private-paths');
19
+
20
+ const PACKAGE_NAME = 'blun-king-cli';
21
+ const REGISTRY_URL = 'https://registry.npmjs.org/blun-king-cli';
22
+ const INSTALL_REGISTRY_URL = 'https://registry.npmjs.org/';
23
+ const FALLBACK_MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
24
+ const SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
25
+ const REQUEST_TIMEOUT_MS = 2_500;
26
+ const STATE_FILE = 'update-notice.json';
27
+ const UPDATE_KEY_ESCAPE_TIMEOUT_MS = 500;
28
+ const UPDATE_SIGNAL_EXIT_CODES = Object.freeze({
29
+ SIGHUP: 129,
30
+ SIGINT: 130,
31
+ SIGTERM: 143,
32
+ });
33
+ const SEMVER_IDENTIFIER = '(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)';
34
+ const SEMVER_PATTERN = new RegExp(
35
+ `^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)`
36
+ + `(?:-(${SEMVER_IDENTIFIER}(?:\\.${SEMVER_IDENTIFIER})*))?`
37
+ + '(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$',
38
+ );
39
+ const MANIFEST_KEYS = Object.freeze([
40
+ 'install',
41
+ 'latest',
42
+ 'minSupported',
43
+ 'name',
44
+ 'notesUrl',
45
+ 'releasedAt',
46
+ ]);
47
+ const ENDPOINT_LIMITS = new Map([
48
+ [REGISTRY_URL, 1024 * 1024],
49
+ [FALLBACK_MANIFEST_URL, 64 * 1024],
50
+ ]);
51
+
52
+ function parseSemver(value) {
53
+ if (typeof value !== 'string' || value.length > 128) return undefined;
54
+ const match = SEMVER_PATTERN.exec(value);
55
+ if (!match) return undefined;
56
+ return {
57
+ raw: value,
58
+ core: [match[1], match[2], match[3]],
59
+ prerelease: match[4] === undefined ? undefined : match[4].split('.'),
60
+ };
61
+ }
62
+
63
+ function compareNumericIdentifier(left, right) {
64
+ if (left.length !== right.length) return left.length < right.length ? -1 : 1;
65
+ return left < right ? -1 : left > right ? 1 : 0;
66
+ }
67
+
68
+ function comparePrerelease(left, right) {
69
+ if (left === undefined && right === undefined) return 0;
70
+ if (left === undefined) return 1;
71
+ if (right === undefined) return -1;
72
+ const length = Math.max(left.length, right.length);
73
+ for (let index = 0; index < length; index += 1) {
74
+ if (left[index] === undefined) return -1;
75
+ if (right[index] === undefined) return 1;
76
+ const leftNumeric = /^\d+$/.test(left[index]);
77
+ const rightNumeric = /^\d+$/.test(right[index]);
78
+ if (leftNumeric && rightNumeric) {
79
+ const comparison = compareNumericIdentifier(left[index], right[index]);
80
+ if (comparison !== 0) return comparison;
81
+ } else if (leftNumeric !== rightNumeric) {
82
+ return leftNumeric ? -1 : 1;
83
+ } else if (left[index] !== right[index]) {
84
+ return left[index] < right[index] ? -1 : 1;
85
+ }
86
+ }
87
+ return 0;
88
+ }
89
+
90
+ function compareSemver(leftValue, rightValue) {
91
+ const left = parseSemver(leftValue);
92
+ const right = parseSemver(rightValue);
93
+ if (!left || !right) return undefined;
94
+ for (let index = 0; index < left.core.length; index += 1) {
95
+ const comparison = compareNumericIdentifier(left.core[index], right.core[index]);
96
+ if (comparison !== 0) return comparison;
97
+ }
98
+ return comparePrerelease(left.prerelease, right.prerelease);
99
+ }
100
+
101
+ function exactDataRecord(value, expectedKeys) {
102
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || types.isProxy(value)) {
103
+ return undefined;
104
+ }
105
+ const prototype = Object.getPrototypeOf(value);
106
+ if (prototype !== Object.prototype && prototype !== null) return undefined;
107
+ const descriptors = Object.getOwnPropertyDescriptors(value);
108
+ const keys = Object.keys(descriptors).toSorted();
109
+ if (keys.length !== expectedKeys.length
110
+ || keys.some((key, index) => key !== expectedKeys[index])) {
111
+ return undefined;
112
+ }
113
+ for (const key of keys) {
114
+ if (!Object.hasOwn(descriptors[key], 'value') || descriptors[key].enumerable !== true) {
115
+ return undefined;
116
+ }
117
+ }
118
+ return Object.fromEntries(keys.map((key) => [key, descriptors[key].value]));
119
+ }
120
+
121
+ function readDataProperty(value, key) {
122
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || types.isProxy(value)) {
123
+ return undefined;
124
+ }
125
+ const prototype = Object.getPrototypeOf(value);
126
+ if (prototype !== Object.prototype && prototype !== null) return undefined;
127
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
128
+ if (!descriptor || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
129
+ return undefined;
130
+ }
131
+ return descriptor.value;
132
+ }
133
+
134
+ function parseRegistryDocument(value) {
135
+ const tags = readDataProperty(value, 'dist-tags');
136
+ const latest = readDataProperty(tags, 'latest');
137
+ if (!parseSemver(latest)) return undefined;
138
+ return latest;
139
+ }
140
+
141
+ function isBoundedText(value, maxBytes) {
142
+ return typeof value === 'string'
143
+ && value.length > 0
144
+ && Buffer.byteLength(value, 'utf8') <= maxBytes
145
+ && !/[\u0000-\u001F\u007F-\u009F\u061C\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u.test(value);
146
+ }
147
+
148
+ function canonicalOfficialNotesUrl(value) {
149
+ if (!isBoundedText(value, 2_048)) return undefined;
150
+ try {
151
+ const url = new URL(value);
152
+ const officialHost = url.hostname === 'blun.ai' || url.hostname.endsWith('.blun.ai');
153
+ if (url.protocol !== 'https:'
154
+ || !officialHost
155
+ || url.username !== ''
156
+ || url.password !== ''
157
+ || url.port !== '') {
158
+ return undefined;
159
+ }
160
+ return url.href;
161
+ } catch {
162
+ return undefined;
163
+ }
164
+ }
165
+
166
+ function isIsoTimestamp(value) {
167
+ if (typeof value !== 'string') return false;
168
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?Z$/.exec(value);
169
+ if (!match) {
170
+ return false;
171
+ }
172
+ const timestamp = Date.parse(value);
173
+ if (!Number.isFinite(timestamp)) return false;
174
+ const date = new Date(timestamp);
175
+ return date.getUTCFullYear() === Number(match[1])
176
+ && date.getUTCMonth() + 1 === Number(match[2])
177
+ && date.getUTCDate() === Number(match[3])
178
+ && date.getUTCHours() === Number(match[4])
179
+ && date.getUTCMinutes() === Number(match[5])
180
+ && date.getUTCSeconds() === Number(match[6]);
181
+ }
182
+
183
+ function parseFallbackManifest(value) {
184
+ const record = exactDataRecord(value, MANIFEST_KEYS);
185
+ const notesUrl = record ? canonicalOfficialNotesUrl(record.notesUrl) : undefined;
186
+ if (!record
187
+ || record.name !== PACKAGE_NAME
188
+ || !parseSemver(record.latest)
189
+ || !parseSemver(record.minSupported)
190
+ || compareSemver(record.minSupported, record.latest) === 1
191
+ || !isIsoTimestamp(record.releasedAt)
192
+ || !notesUrl
193
+ || !isBoundedText(record.install, 256)) {
194
+ return undefined;
195
+ }
196
+ return Object.freeze({
197
+ name: record.name,
198
+ latest: record.latest,
199
+ minSupported: record.minSupported,
200
+ releasedAt: record.releasedAt,
201
+ notesUrl,
202
+ install: record.install,
203
+ });
204
+ }
205
+
206
+ function resolveEffectiveRelease(registryVersion, fallbackManifest) {
207
+ const validRegistryVersion = parseSemver(registryVersion) ? registryVersion : undefined;
208
+ const validFallback = parseFallbackManifest(fallbackManifest);
209
+ if (!validRegistryVersion && !validFallback) return undefined;
210
+
211
+ let version;
212
+ if (validRegistryVersion && validFallback) {
213
+ version = compareSemver(validRegistryVersion, validFallback.latest) <= 0
214
+ ? validRegistryVersion
215
+ : validFallback.latest;
216
+ } else {
217
+ version = validRegistryVersion || validFallback.latest;
218
+ }
219
+
220
+ if (validFallback && compareSemver(validFallback.latest, version) === 0) {
221
+ return { version, notesUrl: validFallback.notesUrl };
222
+ }
223
+ return { version };
224
+ }
225
+
226
+ function requestTrustedJson(url, options = {}) {
227
+ const maxBytes = ENDPOINT_LIMITS.get(url);
228
+ if (maxBytes === undefined) {
229
+ return Promise.reject(new Error('UNTRUSTED_UPDATE_ENDPOINT'));
230
+ }
231
+ const httpsImpl = options.httpsImpl || https;
232
+ const timeoutMs = options.timeoutMs || REQUEST_TIMEOUT_MS;
233
+
234
+ return new Promise((resolve, reject) => {
235
+ let request;
236
+ let response;
237
+ let settled = false;
238
+ const finish = (error, value) => {
239
+ if (settled) return;
240
+ settled = true;
241
+ clearTimeout(timer);
242
+ if (error) reject(error);
243
+ else resolve(value);
244
+ };
245
+ const timer = setTimeout(() => {
246
+ const error = new Error('UPDATE_REQUEST_TIMEOUT');
247
+ finish(error);
248
+ if (response && typeof response.destroy === 'function') response.destroy();
249
+ if (request && typeof request.destroy === 'function') request.destroy();
250
+ }, timeoutMs);
251
+
252
+ try {
253
+ request = httpsImpl.get(url, {
254
+ headers: { accept: 'application/vnd.npm.install-v1+json, application/json' },
255
+ }, (incoming) => {
256
+ response = incoming;
257
+ if (incoming.statusCode !== 200) {
258
+ finish(new Error(`UPDATE_HTTP_STATUS_${incoming.statusCode || 0}`));
259
+ if (typeof incoming.destroy === 'function') incoming.destroy();
260
+ if (request && typeof request.destroy === 'function') request.destroy();
261
+ return;
262
+ }
263
+
264
+ const rawLength = incoming.headers && incoming.headers['content-length'];
265
+ const declaredLength = typeof rawLength === 'string' && /^\d+$/.test(rawLength)
266
+ ? Number(rawLength)
267
+ : undefined;
268
+ if (declaredLength !== undefined && declaredLength > maxBytes) {
269
+ finish(new Error('UPDATE_BODY_TOO_LARGE'));
270
+ if (typeof incoming.destroy === 'function') incoming.destroy();
271
+ return;
272
+ }
273
+
274
+ const chunks = [];
275
+ let total = 0;
276
+ incoming.on('data', (chunk) => {
277
+ if (settled) return;
278
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
279
+ total += buffer.length;
280
+ if (total > maxBytes) {
281
+ finish(new Error('UPDATE_BODY_TOO_LARGE'));
282
+ if (typeof incoming.destroy === 'function') incoming.destroy();
283
+ return;
284
+ }
285
+ chunks.push(buffer);
286
+ });
287
+ incoming.on('error', (error) => finish(error));
288
+ incoming.on('end', () => {
289
+ if (settled) return;
290
+ try {
291
+ finish(undefined, JSON.parse(Buffer.concat(chunks).toString('utf8')));
292
+ } catch {
293
+ finish(new Error('UPDATE_INVALID_JSON'));
294
+ }
295
+ });
296
+ });
297
+ request.on('error', (error) => finish(error));
298
+ } catch (error) {
299
+ finish(error);
300
+ }
301
+ });
302
+ }
303
+
304
+ async function loadReleaseSources() {
305
+ const [registry, fallback] = await Promise.allSettled([
306
+ requestTrustedJson(REGISTRY_URL),
307
+ requestTrustedJson(FALLBACK_MANIFEST_URL),
308
+ ]);
309
+ return {
310
+ registryDocument: registry.status === 'fulfilled' ? registry.value : undefined,
311
+ fallbackDocument: fallback.status === 'fulfilled' ? fallback.value : undefined,
312
+ };
313
+ }
314
+
315
+ function statePath(blunDir) {
316
+ return path.join(blunDir, STATE_FILE);
317
+ }
318
+
319
+ function readSnoozeState(blunDir, now = Date.now()) {
320
+ try {
321
+ const filePath = statePath(blunDir);
322
+ const stat = fs.lstatSync(filePath);
323
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return false;
324
+ const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
325
+ 'snoozedUntil',
326
+ 'version',
327
+ ]);
328
+ if (!record
329
+ || record.version !== 1
330
+ || !Number.isSafeInteger(record.snoozedUntil)
331
+ || record.snoozedUntil <= now
332
+ || record.snoozedUntil > now + SNOOZE_MS) {
333
+ return false;
334
+ }
335
+ return true;
336
+ } catch {
337
+ return false;
338
+ }
339
+ }
340
+
341
+ function writeSnoozeState(blunDir, now = Date.now()) {
342
+ ensurePrivateDirectory(blunDir);
343
+ const filePath = statePath(blunDir);
344
+ const temporaryPath = path.join(
345
+ blunDir,
346
+ `.${STATE_FILE}.${process.pid}.${randomUUID()}.tmp`,
347
+ );
348
+ try {
349
+ writePrivateFile(temporaryPath, `${JSON.stringify({
350
+ version: 1,
351
+ snoozedUntil: now + SNOOZE_MS,
352
+ })}\n`);
353
+ const handle = fs.openSync(temporaryPath, 'r+');
354
+ try {
355
+ fs.fsyncSync(handle);
356
+ } finally {
357
+ fs.closeSync(handle);
358
+ }
359
+ fs.renameSync(temporaryPath, filePath);
360
+ securePrivateFile(filePath);
361
+ } catch (error) {
362
+ fs.rmSync(temporaryPath, { force: true });
363
+ throw error;
364
+ }
365
+ }
366
+
367
+ function isInteractiveUpdateStart({ argv, env, stdin, stdout }) {
368
+ return Array.isArray(argv)
369
+ && argv.length === 0
370
+ && (!env || env.BLUN_NO_AUTO_UPDATE !== '1')
371
+ && stdin?.isTTY === true
372
+ && stdout?.isTTY === true;
373
+ }
374
+
375
+ function installCommandText(version) {
376
+ return `npm i -g ${PACKAGE_NAME}@${version}`;
377
+ }
378
+
379
+ function promptUpdateAction({ currentVersion, release, input, output, processRef = process }) {
380
+ return new Promise((resolve, reject) => {
381
+ let selected = 1;
382
+ let settled = false;
383
+ let cursorHidden = false;
384
+ let escapeTimer;
385
+ let pendingInput = '';
386
+ const decoder = new StringDecoder('utf8');
387
+ const wasRaw = input.isRaw === true;
388
+ const actions = ['Herunterladen und installieren', 'Aussetzen'];
389
+ const signalHandlers = new Map();
390
+
391
+ const renderActions = (refresh) => {
392
+ if (refresh) output.write('\u001B[2F');
393
+ for (let index = 0; index < actions.length; index += 1) {
394
+ output.write(`\r\u001B[2K ${selected === index ? '>' : ' '} ${actions[index]}\n`);
395
+ }
396
+ };
397
+ const cleanup = () => {
398
+ if (escapeTimer !== undefined) clearTimeout(escapeTimer);
399
+ input.removeListener('data', onData);
400
+ input.removeListener('end', postpone);
401
+ input.removeListener('close', postpone);
402
+ input.removeListener('error', postpone);
403
+ for (const [signal, handler] of signalHandlers) {
404
+ processRef.removeListener(signal, handler);
405
+ }
406
+ signalHandlers.clear();
407
+ try {
408
+ if (typeof input.setRawMode === 'function') input.setRawMode(wasRaw);
409
+ } catch {}
410
+ try {
411
+ if (typeof input.pause === 'function') input.pause();
412
+ } catch {}
413
+ if (cursorHidden) {
414
+ try {
415
+ output.write('\u001B[?25h');
416
+ } catch {}
417
+ cursorHidden = false;
418
+ }
419
+ };
420
+ const finish = (choice) => {
421
+ if (settled) return;
422
+ settled = true;
423
+ cleanup();
424
+ try {
425
+ output.write('\n');
426
+ } catch {}
427
+ resolve(choice);
428
+ };
429
+ const fail = (error) => {
430
+ if (settled) return;
431
+ settled = true;
432
+ cleanup();
433
+ reject(error);
434
+ };
435
+ const handleSignal = (signal) => {
436
+ if (settled) return;
437
+ settled = true;
438
+ cleanup();
439
+ try {
440
+ output.write('\n');
441
+ } catch {}
442
+ const error = new Error(`UPDATE_PROMPT_${signal}`);
443
+ error.code = `UPDATE_PROMPT_${signal}`;
444
+ const exitCode = UPDATE_SIGNAL_EXIT_CODES[signal];
445
+ processRef.exitCode = exitCode;
446
+ if (typeof processRef.exit === 'function') processRef.exit(exitCode);
447
+ reject(error);
448
+ };
449
+ function postpone() {
450
+ finish('postpone');
451
+ }
452
+ const waitForEscapeSuffix = () => {
453
+ if (escapeTimer !== undefined) return;
454
+ escapeTimer = setTimeout(() => {
455
+ escapeTimer = undefined;
456
+ postpone();
457
+ }, UPDATE_KEY_ESCAPE_TIMEOUT_MS);
458
+ };
459
+ const clearEscapeTimer = () => {
460
+ if (escapeTimer === undefined) return;
461
+ clearTimeout(escapeTimer);
462
+ escapeTimer = undefined;
463
+ };
464
+ const readEscapeSequence = () => {
465
+ if (pendingInput.length < 2) return undefined;
466
+ if (pendingInput[1] === '[') {
467
+ for (let index = 2; index < pendingInput.length; index += 1) {
468
+ const code = pendingInput.codePointAt(index);
469
+ if (code >= 0x40 && code <= 0x7e) return pendingInput.slice(0, index + 1);
470
+ const isParameter = code >= 0x30 && code <= 0x3f;
471
+ const isIntermediate = code >= 0x20 && code <= 0x2f;
472
+ if (!isParameter && !isIntermediate) return pendingInput.slice(0, index + 1);
473
+ }
474
+ return pendingInput.length > 64 ? pendingInput : undefined;
475
+ }
476
+ if (pendingInput[1] === 'O') {
477
+ return pendingInput.length >= 3 ? pendingInput.slice(0, 3) : undefined;
478
+ }
479
+ return pendingInput.slice(0, 2);
480
+ };
481
+ const processPendingInput = () => {
482
+ while (pendingInput.length > 0) {
483
+ const first = pendingInput[0];
484
+ if (first === '\u0003') {
485
+ postpone();
486
+ return;
487
+ }
488
+ if (first === '\r' || first === '\n') {
489
+ pendingInput = pendingInput.slice(1);
490
+ finish(selected === 0 ? 'install' : 'postpone');
491
+ return;
492
+ }
493
+ if (first !== '\u001B') {
494
+ pendingInput = pendingInput.slice(1);
495
+ continue;
496
+ }
497
+ if (pendingInput.length > 1
498
+ && (pendingInput[1] === '\r'
499
+ || pendingInput[1] === '\n'
500
+ || pendingInput[1] === '\u0003')) {
501
+ postpone();
502
+ return;
503
+ }
504
+ const sequence = readEscapeSequence();
505
+ if (sequence === undefined) {
506
+ waitForEscapeSuffix();
507
+ return;
508
+ }
509
+ clearEscapeTimer();
510
+ if (sequence === '\u001B[A' || sequence === '\u001B[D') {
511
+ selected = 0;
512
+ pendingInput = pendingInput.slice(sequence.length);
513
+ renderActions(true);
514
+ continue;
515
+ }
516
+ if (sequence === '\u001B[B' || sequence === '\u001B[C') {
517
+ selected = 1;
518
+ pendingInput = pendingInput.slice(sequence.length);
519
+ renderActions(true);
520
+ continue;
521
+ }
522
+ pendingInput = pendingInput.slice(sequence.length);
523
+ }
524
+ };
525
+ const onData = (chunk) => {
526
+ try {
527
+ pendingInput += Buffer.isBuffer(chunk) ? decoder.write(chunk) : String(chunk);
528
+ processPendingInput();
529
+ } catch (error) {
530
+ fail(error);
531
+ }
532
+ };
533
+
534
+ try {
535
+ const supportedSignals = processRef.platform === 'win32'
536
+ ? ['SIGINT', 'SIGTERM']
537
+ : Object.keys(UPDATE_SIGNAL_EXIT_CODES);
538
+ for (const signal of supportedSignals) {
539
+ const handler = () => handleSignal(signal);
540
+ signalHandlers.set(signal, handler);
541
+ processRef.once(signal, handler);
542
+ }
543
+ output.write(`\nNeues Update verfügbar (${currentVersion} -> ${release.version})\n`);
544
+ if (release.notesUrl) output.write(`Versionshinweise: ${release.notesUrl}\n`);
545
+ output.write(`Installationsbefehl: ${installCommandText(release.version)}\n\n`);
546
+ cursorHidden = true;
547
+ output.write('\u001B[?25l');
548
+ renderActions(false);
549
+ input.on('data', onData);
550
+ input.once('end', postpone);
551
+ input.once('close', postpone);
552
+ input.once('error', postpone);
553
+ if (typeof input.setRawMode === 'function') input.setRawMode(true);
554
+ if (typeof input.resume === 'function') input.resume();
555
+ } catch (error) {
556
+ fail(error);
557
+ }
558
+ });
559
+ }
560
+
561
+ function resolveTrustedNpmCliPath(options = {}) {
562
+ const execPath = path.resolve(options.execPath || process.execPath);
563
+ const executableDirectory = path.dirname(execPath);
564
+ const packageRoot = path.resolve(__dirname, '..');
565
+ const candidates = options.npmCliPath ? [options.npmCliPath] : [
566
+ path.join(executableDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
567
+ path.resolve(executableDirectory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
568
+ path.resolve(executableDirectory, '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
569
+ path.resolve(packageRoot, '..', 'npm', 'bin', 'npm-cli.js'),
570
+ ];
571
+ for (const candidate of candidates) {
572
+ try {
573
+ if (!path.isAbsolute(candidate)) continue;
574
+ const resolved = fs.realpathSync(candidate);
575
+ const parts = resolved.replaceAll('\\', '/').split('/');
576
+ const npmIndex = parts.length - 3;
577
+ if (parts.at(-1) !== 'npm-cli.js'
578
+ || parts.at(-2) !== 'bin'
579
+ || parts[npmIndex]?.toLowerCase() !== 'npm'
580
+ || !fs.statSync(resolved).isFile()) {
581
+ continue;
582
+ }
583
+ return resolved;
584
+ } catch {}
585
+ }
586
+ throw new Error('TRUSTED_NPM_CLI_NOT_FOUND');
587
+ }
588
+
589
+ function createInstallInvocation(version, options = {}) {
590
+ if (!parseSemver(version)) throw new Error('INVALID_UPDATE_VERSION');
591
+ const packageSpec = `${PACKAGE_NAME}@${version}`;
592
+ const execPath = fs.realpathSync(path.resolve(options.execPath || process.execPath));
593
+ if (!fs.statSync(execPath).isFile()) throw new Error('INVALID_NODE_EXECUTABLE');
594
+ const npmCliPath = resolveTrustedNpmCliPath(options);
595
+ const packageRoot = fs.realpathSync(path.resolve(options.packageRoot));
596
+ const modulesRoot = path.dirname(packageRoot);
597
+ if (path.basename(modulesRoot).toLowerCase() !== 'node_modules') {
598
+ throw new Error('INVALID_GLOBAL_PACKAGE_ROOT');
599
+ }
600
+ let installPrefix = path.dirname(modulesRoot);
601
+ const platform = options.platform || process.platform;
602
+ if (platform !== 'win32' && path.basename(installPrefix) === 'lib') {
603
+ installPrefix = path.dirname(installPrefix);
604
+ }
605
+ let npmConfigPath;
606
+ if (options.npmConfigPath) {
607
+ npmConfigPath = fs.realpathSync(path.resolve(options.npmConfigPath));
608
+ if (!fs.statSync(npmConfigPath).isFile()) throw new Error('INVALID_NPM_CONFIG_PATH');
609
+ }
610
+ return {
611
+ command: execPath,
612
+ args: [
613
+ npmCliPath,
614
+ 'i',
615
+ '-g',
616
+ packageSpec,
617
+ `--registry=${INSTALL_REGISTRY_URL}`,
618
+ '--strict-ssl=true',
619
+ '--dry-run=false',
620
+ '--ignore-scripts=false',
621
+ '--package-lock-only=false',
622
+ `--prefix=${installPrefix}`,
623
+ ...(npmConfigPath ? [
624
+ `--userconfig=${npmConfigPath}`,
625
+ `--globalconfig=${npmConfigPath}`,
626
+ ] : []),
627
+ ],
628
+ windowsVerbatimArguments: false,
629
+ };
630
+ }
631
+
632
+ function createInstallEnvironment(sourceEnv, installDirectory, npmConfigPath) {
633
+ const allowedKeys = new Set([
634
+ 'appdata',
635
+ 'comspec',
636
+ 'home',
637
+ 'homedrive',
638
+ 'homepath',
639
+ 'lang',
640
+ 'lc_all',
641
+ 'lc_ctype',
642
+ 'localappdata',
643
+ 'logname',
644
+ 'path',
645
+ 'pathext',
646
+ 'programdata',
647
+ 'programfiles',
648
+ 'programfiles(x86)',
649
+ 'systemroot',
650
+ 'term',
651
+ 'tz',
652
+ 'user',
653
+ 'userprofile',
654
+ 'windir',
655
+ ]);
656
+ const result = {};
657
+ for (const [key, value] of Object.entries(sourceEnv || {})) {
658
+ const normalized = key.toLowerCase();
659
+ if (!allowedKeys.has(normalized) && !key.startsWith('BLUN_TEST_')) continue;
660
+ result[key] = value;
661
+ }
662
+ result.TEMP = installDirectory;
663
+ result.TMP = installDirectory;
664
+ result.TMPDIR = installDirectory;
665
+ result.npm_config_registry = INSTALL_REGISTRY_URL;
666
+ result.npm_config_strict_ssl = 'true';
667
+ result.npm_config_dry_run = 'false';
668
+ result.npm_config_ignore_scripts = 'false';
669
+ result.npm_config_package_lock_only = 'false';
670
+ result.npm_config_userconfig = npmConfigPath;
671
+ result.npm_config_globalconfig = npmConfigPath;
672
+ return result;
673
+ }
674
+
675
+ function createPrivateInstallDirectory(tempRoot = os.tmpdir()) {
676
+ const directory = fs.mkdtempSync(path.join(path.resolve(tempRoot), 'blun-update-'));
677
+ ensurePrivateDirectory(directory);
678
+ return directory;
679
+ }
680
+
681
+ function verifyInstalledPackageVersion(packageRoot, version) {
682
+ try {
683
+ const manifestPath = path.join(packageRoot, 'package.json');
684
+ const stat = fs.lstatSync(manifestPath);
685
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) return false;
686
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
687
+ return readDataProperty(manifest, 'name') === PACKAGE_NAME
688
+ && readDataProperty(manifest, 'version') === version;
689
+ } catch {
690
+ return false;
691
+ }
692
+ }
693
+
694
+ function preparePinnedInstaller(version, options = {}) {
695
+ const forkImpl = options.forkImpl || fork;
696
+ const preparation = Promise.withResolvers();
697
+ const resolvePreparation = preparation.resolve;
698
+ let child;
699
+ let invocation;
700
+ let leaseBusy = false;
701
+ let leaseUnavailable = false;
702
+ let sendError;
703
+ let childError;
704
+ let preparationSettled = false;
705
+ let closeSettled = false;
706
+ let selectedAction;
707
+ let installDirectory;
708
+ let npmConfigPath;
709
+ let disconnectTimer;
710
+ const close = Promise.withResolvers();
711
+ const closePromise = close.promise;
712
+ const resolveClose = close.resolve;
713
+ const cleanupDirectory = () => {
714
+ try {
715
+ if (installDirectory) {
716
+ (options.removeDirectory || fs.rmSync)(installDirectory, {
717
+ recursive: true,
718
+ force: true,
719
+ });
720
+ }
721
+ } catch {
722
+ // Cleanup cannot change a completed npm result.
723
+ }
724
+ };
725
+ const settlePreparation = (result) => {
726
+ if (preparationSettled) return;
727
+ preparationSettled = true;
728
+ clearTimeout(prepareTimeout);
729
+ resolvePreparation(result);
730
+ };
731
+ const finishClose = (result) => {
732
+ if (closeSettled) return;
733
+ closeSettled = true;
734
+ clearTimeout(disconnectTimer);
735
+ cleanupDirectory();
736
+ resolveClose(result);
737
+ if (!preparationSettled) {
738
+ settlePreparation({
739
+ ready: false,
740
+ reason: leaseBusy
741
+ ? 'busy'
742
+ : leaseUnavailable
743
+ ? 'unavailable'
744
+ : result.code === 75 || result.unknownState === true
745
+ ? 'unknown'
746
+ : 'failed',
747
+ result,
748
+ });
749
+ }
750
+ };
751
+ const finishDisconnected = () => {
752
+ if (closeSettled) return;
753
+ closeSettled = true;
754
+ const result = { code: undefined, error: childError, unknownState: true };
755
+ resolveClose(result);
756
+ if (!preparationSettled) {
757
+ settlePreparation({ ready: false, reason: 'unknown', result });
758
+ }
759
+ };
760
+ const scheduleUnknownState = (error) => {
761
+ if (error && childError === undefined) childError = error;
762
+ if (disconnectTimer !== undefined) return;
763
+ disconnectTimer = setTimeout(
764
+ finishDisconnected,
765
+ options.disconnectTimeoutMs || 1_000,
766
+ );
767
+ disconnectTimer.unref?.();
768
+ };
769
+ const sendControl = (type) => {
770
+ if (selectedAction) return closePromise;
771
+ selectedAction = type;
772
+ try {
773
+ child.send({ type }, (error) => {
774
+ if (!error) return;
775
+ sendError = error;
776
+ try {
777
+ child.kill();
778
+ } catch {}
779
+ });
780
+ } catch (error) {
781
+ sendError = error;
782
+ try {
783
+ child.kill();
784
+ } catch {}
785
+ }
786
+ return closePromise;
787
+ };
788
+ let prepareTimeout;
789
+ try {
790
+ installDirectory = createPrivateInstallDirectory(options.tempRoot);
791
+ npmConfigPath = path.join(installDirectory, 'npmrc');
792
+ writePrivateFile(npmConfigPath, '');
793
+ invocation = createInstallInvocation(version, { ...options, npmConfigPath });
794
+ child = forkImpl(path.join(__dirname, 'update-lease.js'), ['--installer-worker'], {
795
+ detached: true,
796
+ execPath: invocation.command,
797
+ cwd: installDirectory,
798
+ env: createInstallEnvironment(
799
+ options.env || process.env,
800
+ installDirectory,
801
+ npmConfigPath,
802
+ ),
803
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
804
+ windowsHide: true,
805
+ });
806
+ } catch (error) {
807
+ cleanupDirectory();
808
+ settlePreparation({ ready: false, reason: 'failed', result: { error } });
809
+ return preparation.promise;
810
+ }
811
+ prepareTimeout = setTimeout(() => {
812
+ sendError = new Error('UPDATE_INSTALLER_PREPARE_TIMEOUT');
813
+ settlePreparation({ ready: false, reason: 'unknown', result: { error: sendError } });
814
+ try {
815
+ child.kill();
816
+ } catch {}
817
+ }, options.prepareTimeoutMs || 5_000);
818
+ prepareTimeout.unref?.();
819
+ child.once('error', (error) => {
820
+ if (!preparationSettled && child.pid === undefined) {
821
+ finishClose({ code: undefined, error });
822
+ return;
823
+ }
824
+ scheduleUnknownState(error);
825
+ });
826
+ child.on('message', (message) => {
827
+ if (message?.type === 'blun-update-lease-busy') leaseBusy = true;
828
+ if (message?.type === 'blun-update-lease-unavailable') leaseUnavailable = true;
829
+ if (message?.type === 'blun-update-lease-adopted') {
830
+ settlePreparation(Object.freeze({
831
+ ready: true,
832
+ commit: () => sendControl('blun-update-lease-commit'),
833
+ cancel: () => sendControl('blun-update-lease-cancel'),
834
+ }));
835
+ }
836
+ });
837
+ const finishInstallerProcess = (code, signal) => {
838
+ if (sendError || childError) {
839
+ finishClose({
840
+ code: undefined,
841
+ error: sendError || childError,
842
+ unknownState: true,
843
+ });
844
+ return;
845
+ }
846
+ finishClose({
847
+ code: Number.isInteger(code) ? code : undefined,
848
+ signal: typeof signal === 'string' ? signal : undefined,
849
+ leaseBusy,
850
+ leaseUnavailable,
851
+ unknownState: !Number.isInteger(code),
852
+ verified: !leaseBusy
853
+ && !leaseUnavailable
854
+ && code === 0
855
+ && verifyInstalledPackageVersion(options.packageRoot, version),
856
+ });
857
+ };
858
+ child.once('exit', finishInstallerProcess);
859
+ child.once('close', finishInstallerProcess);
860
+ child.once('disconnect', () => scheduleUnknownState());
861
+ try {
862
+ child.send({
863
+ type: 'blun-update-lease-handoff',
864
+ packageRoot: path.resolve(options.packageRoot),
865
+ installer: {
866
+ npmCliPath: invocation.args[0],
867
+ npmArgs: invocation.args.slice(1),
868
+ installCwd: installDirectory,
869
+ },
870
+ }, (error) => {
871
+ if (!error) return;
872
+ sendError = error;
873
+ try {
874
+ child.kill();
875
+ } catch {
876
+ finishClose({ code: undefined, error, unknownState: true });
877
+ }
878
+ });
879
+ } catch (error) {
880
+ sendError = error;
881
+ try {
882
+ child.kill();
883
+ } catch {
884
+ finishClose({ code: undefined, error, unknownState: true });
885
+ }
886
+ }
887
+ return preparation.promise;
888
+ }
889
+
890
+ async function installPinnedVersion(version, options = {}) {
891
+ const prepared = await preparePinnedInstaller(version, options);
892
+ if (!prepared.ready) {
893
+ return {
894
+ ...prepared.result,
895
+ leaseBusy: prepared.reason === 'busy',
896
+ leaseUnavailable: prepared.reason === 'unavailable',
897
+ unknownState: prepared.reason === 'unknown' || prepared.result?.unknownState === true,
898
+ };
899
+ }
900
+ return prepared.commit();
901
+ }
902
+
903
+ async function probeInstallLease(options, packageRoot) {
904
+ let result;
905
+ try {
906
+ result = await (options.acquireInstallLease || tryAcquireUpdateLease)({
907
+ packageRoot,
908
+ role: 'install',
909
+ });
910
+ } catch {
911
+ return 'unavailable';
912
+ }
913
+ if (!result?.acquired) return result?.reason === 'busy' ? 'busy' : 'unavailable';
914
+ try {
915
+ result.lease.assertOwned();
916
+ } catch {
917
+ return 'busy';
918
+ } finally {
919
+ try {
920
+ await result.lease.release();
921
+ } catch {}
922
+ }
923
+ return 'clear';
924
+ }
925
+
926
+ async function runUpdateNotice(options) {
927
+ const {
928
+ currentVersion,
929
+ argv,
930
+ env,
931
+ stdin,
932
+ stdout,
933
+ stderr,
934
+ blunDir,
935
+ } = options;
936
+ if (!isInteractiveUpdateStart({ argv, env, stdin, stdout })) {
937
+ return { kind: 'continue', reason: 'not_interactive' };
938
+ }
939
+ if (!parseSemver(currentVersion)) {
940
+ return { kind: 'continue', reason: 'invalid_current_version' };
941
+ }
942
+
943
+ const now = (options.now || Date.now)();
944
+ if (readSnoozeState(blunDir, now)) {
945
+ return { kind: 'continue', reason: 'snoozed' };
946
+ }
947
+
948
+ const packageRoot = path.resolve(options.packageRoot || path.resolve(__dirname, '..'));
949
+ let leaseResult;
950
+ const ownsNoticeLease = options.noticeLease === undefined;
951
+ if (ownsNoticeLease) {
952
+ try {
953
+ leaseResult = await (options.acquireLease || tryAcquireUpdateLease)({ packageRoot });
954
+ } catch {
955
+ return { kind: 'continue', reason: 'lease_unavailable' };
956
+ }
957
+ } else {
958
+ leaseResult = { acquired: true, lease: options.noticeLease };
959
+ }
960
+ if (!leaseResult?.acquired) {
961
+ return leaseResult?.reason === 'busy'
962
+ ? { kind: 'update_in_progress' }
963
+ : { kind: 'continue', reason: 'lease_unavailable' };
964
+ }
965
+ const { lease } = leaseResult;
966
+ let installerSession;
967
+
968
+ try {
969
+ if (readSnoozeState(blunDir, (options.now || Date.now)())) {
970
+ return { kind: 'continue', reason: 'snoozed' };
971
+ }
972
+ const initialInstallLease = await probeInstallLease(options, packageRoot);
973
+ if (initialInstallLease === 'busy') return { kind: 'update_in_progress' };
974
+ if (initialInstallLease !== 'clear') {
975
+ return { kind: 'continue', reason: 'lease_unavailable' };
976
+ }
977
+ let sources;
978
+ try {
979
+ sources = await (options.loadReleaseSources || loadReleaseSources)();
980
+ } catch {
981
+ return { kind: 'continue', reason: 'unavailable' };
982
+ }
983
+ const registryVersion = parseRegistryDocument(sources?.registryDocument);
984
+ const fallbackManifest = parseFallbackManifest(sources?.fallbackDocument);
985
+ const release = resolveEffectiveRelease(registryVersion, fallbackManifest);
986
+ if (!release) return { kind: 'continue', reason: 'unavailable' };
987
+ if (compareSemver(release.version, currentVersion) <= 0) {
988
+ return { kind: 'continue', reason: 'current' };
989
+ }
990
+ try {
991
+ lease.assertOwned();
992
+ } catch {
993
+ return { kind: 'update_in_progress' };
994
+ }
995
+ const prePromptInstallLease = await probeInstallLease(options, packageRoot);
996
+ if (prePromptInstallLease === 'busy') return { kind: 'update_in_progress' };
997
+ if (prePromptInstallLease !== 'clear') {
998
+ return { kind: 'continue', reason: 'lease_unavailable' };
999
+ }
1000
+ if (!options.installVersion) {
1001
+ installerSession = await (options.prepareInstaller || preparePinnedInstaller)(
1002
+ release.version,
1003
+ { packageRoot },
1004
+ );
1005
+ if (!installerSession?.ready) {
1006
+ return installerSession?.reason === 'failed'
1007
+ ? { kind: 'continue', reason: 'installer_unavailable' }
1008
+ : { kind: 'update_in_progress' };
1009
+ }
1010
+ }
1011
+ let action;
1012
+ try {
1013
+ action = await (options.promptAction || promptUpdateAction)({
1014
+ currentVersion,
1015
+ release,
1016
+ input: stdin,
1017
+ output: stdout,
1018
+ });
1019
+ } catch {
1020
+ return { kind: 'continue', reason: 'prompt_failed' };
1021
+ }
1022
+ if (action !== 'install') {
1023
+ if (installerSession?.ready) await installerSession.cancel();
1024
+ try {
1025
+ writeSnoozeState(blunDir, (options.now || Date.now)());
1026
+ } catch {
1027
+ // A failed preference write must not block the existing console.
1028
+ }
1029
+ return { kind: 'continue', reason: 'postponed' };
1030
+ }
1031
+
1032
+ try {
1033
+ lease.assertOwned();
1034
+ } catch {
1035
+ return { kind: 'update_in_progress' };
1036
+ }
1037
+ let installResult;
1038
+ try {
1039
+ installResult = options.installVersion
1040
+ ? await options.installVersion(release.version)
1041
+ : await installerSession.commit();
1042
+ } catch {
1043
+ installResult = { code: undefined };
1044
+ }
1045
+ if (installResult?.leaseBusy === true
1046
+ || installResult?.leaseUnavailable === true
1047
+ || installResult?.unknownState === true) {
1048
+ return { kind: 'update_in_progress' };
1049
+ }
1050
+ if (installResult?.code === 0 && installResult?.verified === true) {
1051
+ stdout.write('Update erfolgreich installiert. Starte BLUN Code neu, um die neue Version zu verwenden.\n');
1052
+ return { kind: 'installed', version: release.version };
1053
+ }
1054
+
1055
+ if (Number.isInteger(installResult?.code) && installResult.code !== 0) {
1056
+ stderr.write(`Update fehlgeschlagen (Code ${installResult.code}). Die vorhandene Version wird gestartet.\n`);
1057
+ return { kind: 'continue', reason: 'install_failed', exitCode: installResult.code };
1058
+ }
1059
+ stderr.write('Update konnte nicht verifiziert werden. Starte BLUN Code neu.\n');
1060
+ return { kind: 'update_in_progress' };
1061
+ } finally {
1062
+ if (installerSession?.ready) {
1063
+ try {
1064
+ await installerSession.cancel();
1065
+ } catch {}
1066
+ }
1067
+ if (ownsNoticeLease) {
1068
+ try {
1069
+ await lease.release();
1070
+ } catch {}
1071
+ }
1072
+ }
1073
+ }
1074
+
1075
+ module.exports = {
1076
+ FALLBACK_MANIFEST_URL,
1077
+ PACKAGE_NAME,
1078
+ REGISTRY_URL,
1079
+ SNOOZE_MS,
1080
+ compareSemver,
1081
+ createInstallInvocation,
1082
+ installPinnedVersion,
1083
+ preparePinnedInstaller,
1084
+ isInteractiveUpdateStart,
1085
+ loadReleaseSources,
1086
+ parseFallbackManifest,
1087
+ parseRegistryDocument,
1088
+ promptUpdateAction,
1089
+ readSnoozeState,
1090
+ requestTrustedJson,
1091
+ resolveEffectiveRelease,
1092
+ runUpdateNotice,
1093
+ writeSnoozeState,
1094
+ };