blun-king-cli 9.1.19 → 9.1.21
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/update-notice.js +145 -21
- package/blun.mjs +2052 -176
- package/package.json +1 -1
- package/standard-tools/language-guard/blun_language_guard.py +22 -15
package/bin/update-notice.js
CHANGED
|
@@ -23,6 +23,7 @@ const FALLBACK_MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
|
|
|
23
23
|
const SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
24
24
|
const REQUEST_TIMEOUT_MS = 2_500;
|
|
25
25
|
const STATE_FILE = 'update-notice.json';
|
|
26
|
+
const UPDATE_SKIP_FILE = 'update-skip-version.json';
|
|
26
27
|
const UPDATE_DIRECTORY = 'updates';
|
|
27
28
|
const UPDATE_KEY_ESCAPE_TIMEOUT_MS = 500;
|
|
28
29
|
const UPDATE_SIGNAL_EXIT_CODES = Object.freeze({
|
|
@@ -382,6 +383,57 @@ function writeSnoozeState(blunDir, now = Date.now()) {
|
|
|
382
383
|
}
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
function skippedVersionPath(blunDir) {
|
|
387
|
+
return path.join(blunDir, UPDATE_SKIP_FILE);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function readSkippedVersion(blunDir) {
|
|
391
|
+
try {
|
|
392
|
+
const filePath = skippedVersionPath(blunDir);
|
|
393
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
394
|
+
securePrivateFile(filePath);
|
|
395
|
+
const stat = fs.lstatSync(filePath);
|
|
396
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_024) return undefined;
|
|
397
|
+
const record = exactDataRecord(JSON.parse(fs.readFileSync(filePath, 'utf8')), [
|
|
398
|
+
'skippedVersion',
|
|
399
|
+
'version',
|
|
400
|
+
]);
|
|
401
|
+
if (!record || record.version !== 1 || !parseSemver(record.skippedVersion)) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
return record.skippedVersion;
|
|
405
|
+
} catch {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function writeSkippedVersion(blunDir, skippedVersion) {
|
|
411
|
+
if (!parseSemver(skippedVersion)) throw new Error('INVALID_UPDATE_VERSION');
|
|
412
|
+
ensurePrivateDirectory(blunDir);
|
|
413
|
+
const filePath = skippedVersionPath(blunDir);
|
|
414
|
+
const temporaryPath = path.join(
|
|
415
|
+
blunDir,
|
|
416
|
+
`.${UPDATE_SKIP_FILE}.${process.pid}.${randomUUID()}.tmp`,
|
|
417
|
+
);
|
|
418
|
+
try {
|
|
419
|
+
writePrivateFile(temporaryPath, `${JSON.stringify({
|
|
420
|
+
version: 1,
|
|
421
|
+
skippedVersion,
|
|
422
|
+
})}\n`);
|
|
423
|
+
const handle = fs.openSync(temporaryPath, 'r+');
|
|
424
|
+
try {
|
|
425
|
+
fs.fsyncSync(handle);
|
|
426
|
+
} finally {
|
|
427
|
+
fs.closeSync(handle);
|
|
428
|
+
}
|
|
429
|
+
fs.renameSync(temporaryPath, filePath);
|
|
430
|
+
securePrivateFile(filePath);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
385
437
|
function isInteractiveUpdateStart({ argv, env, stdin, stdout }) {
|
|
386
438
|
return Array.isArray(argv)
|
|
387
439
|
&& argv.length === 0
|
|
@@ -394,30 +446,68 @@ function installCommandText(version) {
|
|
|
394
446
|
return `npm i -g ${PACKAGE_NAME}@${version}`;
|
|
395
447
|
}
|
|
396
448
|
|
|
449
|
+
function createUpdateProgress(output) {
|
|
450
|
+
const enabled = output?.isTTY === true;
|
|
451
|
+
const phaseCount = 4;
|
|
452
|
+
const width = 24;
|
|
453
|
+
const cyan = '\u001B[36m';
|
|
454
|
+
const dim = '\u001B[2m';
|
|
455
|
+
const reset = '\u001B[0m';
|
|
456
|
+
let active = false;
|
|
457
|
+
const update = (phase, label) => {
|
|
458
|
+
if (!enabled) return;
|
|
459
|
+
const completed = Math.max(0, Math.min(phaseCount, phase));
|
|
460
|
+
const filled = Math.round((completed / phaseCount) * width);
|
|
461
|
+
const bar = `${'━'.repeat(filled)}${'─'.repeat(width - filled)}`;
|
|
462
|
+
output.write(`\r\u001B[2K${cyan}${bar}${reset} ${dim}${completed}/${phaseCount}${reset} ${label}`);
|
|
463
|
+
active = completed < phaseCount;
|
|
464
|
+
if (!active) output.write('\n');
|
|
465
|
+
};
|
|
466
|
+
update.finish = () => {
|
|
467
|
+
if (!enabled || !active) return;
|
|
468
|
+
active = false;
|
|
469
|
+
output.write('\n');
|
|
470
|
+
};
|
|
471
|
+
return update;
|
|
472
|
+
}
|
|
473
|
+
|
|
397
474
|
function promptUpdateAction({ currentVersion, release, input, output, processRef = process }) {
|
|
398
475
|
return new Promise((resolve, reject) => {
|
|
399
|
-
let selected =
|
|
476
|
+
let selected = 0;
|
|
400
477
|
let settled = false;
|
|
401
478
|
let cursorHidden = false;
|
|
402
479
|
let escapeTimer;
|
|
403
480
|
let pendingInput = '';
|
|
404
481
|
const decoder = new StringDecoder('utf8');
|
|
405
482
|
const wasRaw = input.isRaw === true;
|
|
406
|
-
const
|
|
483
|
+
const cyan = '\u001B[36m';
|
|
484
|
+
const dim = '\u001B[2m';
|
|
485
|
+
const underline = '\u001B[4m';
|
|
486
|
+
const reset = '\u001B[0m';
|
|
487
|
+
const actions = [
|
|
488
|
+
{
|
|
489
|
+
choice: 'install',
|
|
490
|
+
label: `Jetzt aktualisieren (führt \`${installCommandText(release.version)}\` aus)`,
|
|
491
|
+
},
|
|
492
|
+
{ choice: 'skip-once', label: 'Überspringen' },
|
|
493
|
+
{ choice: 'skip-version', label: 'Auf den nächsten Release warten' },
|
|
494
|
+
];
|
|
407
495
|
const signalHandlers = new Map();
|
|
408
496
|
|
|
409
497
|
const renderActions = (refresh) => {
|
|
410
|
-
if (refresh) output.write(
|
|
498
|
+
if (refresh) output.write(`\u001B[${actions.length + 1}F`);
|
|
411
499
|
for (let index = 0; index < actions.length; index += 1) {
|
|
412
|
-
|
|
500
|
+
const line = `${selected === index ? '›' : ' '} ${index + 1}. ${actions[index].label}`;
|
|
501
|
+
output.write(`\r\u001B[2K${selected === index ? cyan : ''}${line}${selected === index ? reset : ''}\n`);
|
|
413
502
|
}
|
|
503
|
+
output.write(`\r\u001B[2K${dim}Enter drücken, um fortzufahren${reset}\n`);
|
|
414
504
|
};
|
|
415
505
|
const cleanup = () => {
|
|
416
506
|
if (escapeTimer !== undefined) clearTimeout(escapeTimer);
|
|
417
507
|
input.removeListener('data', onData);
|
|
418
|
-
input.removeListener('end',
|
|
419
|
-
input.removeListener('close',
|
|
420
|
-
input.removeListener('error',
|
|
508
|
+
input.removeListener('end', skipOnce);
|
|
509
|
+
input.removeListener('close', skipOnce);
|
|
510
|
+
input.removeListener('error', skipOnce);
|
|
421
511
|
for (const [signal, handler] of signalHandlers) {
|
|
422
512
|
processRef.removeListener(signal, handler);
|
|
423
513
|
}
|
|
@@ -464,14 +554,14 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
|
|
|
464
554
|
if (typeof processRef.exit === 'function') processRef.exit(exitCode);
|
|
465
555
|
reject(error);
|
|
466
556
|
};
|
|
467
|
-
function
|
|
468
|
-
finish('
|
|
557
|
+
function skipOnce() {
|
|
558
|
+
finish('skip-once');
|
|
469
559
|
}
|
|
470
560
|
const waitForEscapeSuffix = () => {
|
|
471
561
|
if (escapeTimer !== undefined) return;
|
|
472
562
|
escapeTimer = setTimeout(() => {
|
|
473
563
|
escapeTimer = undefined;
|
|
474
|
-
|
|
564
|
+
skipOnce();
|
|
475
565
|
}, UPDATE_KEY_ESCAPE_TIMEOUT_MS);
|
|
476
566
|
};
|
|
477
567
|
const clearEscapeTimer = () => {
|
|
@@ -500,23 +590,30 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
|
|
|
500
590
|
while (pendingInput.length > 0) {
|
|
501
591
|
const first = pendingInput[0];
|
|
502
592
|
if (first === '\u0003') {
|
|
503
|
-
|
|
593
|
+
skipOnce();
|
|
504
594
|
return;
|
|
505
595
|
}
|
|
506
596
|
if (first === '\r' || first === '\n') {
|
|
507
597
|
pendingInput = pendingInput.slice(1);
|
|
508
|
-
finish(selected
|
|
598
|
+
finish(actions[selected].choice);
|
|
509
599
|
return;
|
|
510
600
|
}
|
|
511
601
|
if (first !== '\u001B') {
|
|
602
|
+
const numericSelection = Number(first) - 1;
|
|
512
603
|
pendingInput = pendingInput.slice(1);
|
|
604
|
+
if (Number.isInteger(numericSelection)
|
|
605
|
+
&& numericSelection >= 0
|
|
606
|
+
&& numericSelection < actions.length) {
|
|
607
|
+
selected = numericSelection;
|
|
608
|
+
renderActions(true);
|
|
609
|
+
}
|
|
513
610
|
continue;
|
|
514
611
|
}
|
|
515
612
|
if (pendingInput.length > 1
|
|
516
613
|
&& (pendingInput[1] === '\r'
|
|
517
614
|
|| pendingInput[1] === '\n'
|
|
518
615
|
|| pendingInput[1] === '\u0003')) {
|
|
519
|
-
|
|
616
|
+
skipOnce();
|
|
520
617
|
return;
|
|
521
618
|
}
|
|
522
619
|
const sequence = readEscapeSequence();
|
|
@@ -526,13 +623,13 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
|
|
|
526
623
|
}
|
|
527
624
|
clearEscapeTimer();
|
|
528
625
|
if (sequence === '\u001B[A' || sequence === '\u001B[D') {
|
|
529
|
-
selected =
|
|
626
|
+
selected = (selected - 1 + actions.length) % actions.length;
|
|
530
627
|
pendingInput = pendingInput.slice(sequence.length);
|
|
531
628
|
renderActions(true);
|
|
532
629
|
continue;
|
|
533
630
|
}
|
|
534
631
|
if (sequence === '\u001B[B' || sequence === '\u001B[C') {
|
|
535
|
-
selected = 1;
|
|
632
|
+
selected = (selected + 1) % actions.length;
|
|
536
633
|
pendingInput = pendingInput.slice(sequence.length);
|
|
537
634
|
renderActions(true);
|
|
538
635
|
continue;
|
|
@@ -558,16 +655,18 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
|
|
|
558
655
|
signalHandlers.set(signal, handler);
|
|
559
656
|
processRef.once(signal, handler);
|
|
560
657
|
}
|
|
561
|
-
output.write(`\
|
|
562
|
-
if (release.notesUrl)
|
|
563
|
-
|
|
658
|
+
output.write(`\nBLUN Code-Update verfügbar: ${currentVersion} -> ${release.version}\n`);
|
|
659
|
+
if (release.notesUrl) {
|
|
660
|
+
output.write(`${dim}Versionshinweise:${reset} ${underline}${release.notesUrl}${reset}\n`);
|
|
661
|
+
}
|
|
662
|
+
output.write('\n');
|
|
564
663
|
cursorHidden = true;
|
|
565
664
|
output.write('\u001B[?25l');
|
|
566
665
|
renderActions(false);
|
|
567
666
|
input.on('data', onData);
|
|
568
|
-
input.once('end',
|
|
569
|
-
input.once('close',
|
|
570
|
-
input.once('error',
|
|
667
|
+
input.once('end', skipOnce);
|
|
668
|
+
input.once('close', skipOnce);
|
|
669
|
+
input.once('error', skipOnce);
|
|
571
670
|
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
572
671
|
if (typeof input.resume === 'function') input.resume();
|
|
573
672
|
} catch (error) {
|
|
@@ -1121,6 +1220,9 @@ async function runUpdateFlow(options, explicitUpdate) {
|
|
|
1121
1220
|
? explicitNoTarget(stdout, reason)
|
|
1122
1221
|
: { kind: 'continue', reason };
|
|
1123
1222
|
}
|
|
1223
|
+
if (!explicitUpdate && readSkippedVersion(blunDir) === release.version) {
|
|
1224
|
+
return { kind: 'continue', reason: 'skipped_version' };
|
|
1225
|
+
}
|
|
1124
1226
|
if (explicitUpdate) {
|
|
1125
1227
|
try {
|
|
1126
1228
|
ensurePrivateDirectory(blunDir);
|
|
@@ -1177,6 +1279,17 @@ async function runUpdateFlow(options, explicitUpdate) {
|
|
|
1177
1279
|
}
|
|
1178
1280
|
if (action !== 'install') {
|
|
1179
1281
|
if (installerSession?.ready) await installerSession.cancel();
|
|
1282
|
+
if (action === 'skip-once') {
|
|
1283
|
+
return { kind: 'continue', reason: 'skipped_once' };
|
|
1284
|
+
}
|
|
1285
|
+
if (action === 'skip-version') {
|
|
1286
|
+
try {
|
|
1287
|
+
writeSkippedVersion(blunDir, release.version);
|
|
1288
|
+
} catch {
|
|
1289
|
+
// A failed preference write must not block the existing console.
|
|
1290
|
+
}
|
|
1291
|
+
return { kind: 'continue', reason: 'skipped_version' };
|
|
1292
|
+
}
|
|
1180
1293
|
try {
|
|
1181
1294
|
writeSnoozeState(blunDir, (options.now || Date.now)());
|
|
1182
1295
|
} catch {
|
|
@@ -1185,6 +1298,10 @@ async function runUpdateFlow(options, explicitUpdate) {
|
|
|
1185
1298
|
return { kind: 'continue', reason: 'postponed' };
|
|
1186
1299
|
}
|
|
1187
1300
|
|
|
1301
|
+
const updateProgress = createUpdateProgress(stdout);
|
|
1302
|
+
updateProgress(1, 'Update geprüft');
|
|
1303
|
+
updateProgress(2, 'Installer bereit');
|
|
1304
|
+
|
|
1188
1305
|
try {
|
|
1189
1306
|
lease.assertOwned();
|
|
1190
1307
|
} catch {
|
|
@@ -1193,6 +1310,7 @@ async function runUpdateFlow(options, explicitUpdate) {
|
|
|
1193
1310
|
: { kind: 'update_in_progress' };
|
|
1194
1311
|
}
|
|
1195
1312
|
let installResult;
|
|
1313
|
+
updateProgress(3, 'Paket wird installiert');
|
|
1196
1314
|
try {
|
|
1197
1315
|
installResult = options.installVersion
|
|
1198
1316
|
? await options.installVersion(release.version)
|
|
@@ -1203,22 +1321,26 @@ async function runUpdateFlow(options, explicitUpdate) {
|
|
|
1203
1321
|
if (installResult?.leaseBusy === true
|
|
1204
1322
|
|| installResult?.leaseUnavailable === true
|
|
1205
1323
|
|| installResult?.unknownState === true) {
|
|
1324
|
+
updateProgress.finish();
|
|
1206
1325
|
return explicitUpdate
|
|
1207
1326
|
? explicitFailure(stderr, 'update_in_progress')
|
|
1208
1327
|
: { kind: 'update_in_progress' };
|
|
1209
1328
|
}
|
|
1210
1329
|
if (installResult?.code === 0 && installResult?.verified === true) {
|
|
1330
|
+
updateProgress(4, 'Installation geprüft');
|
|
1211
1331
|
stdout.write('Update erfolgreich installiert. Starte BLUN Code neu, um die neue Version zu verwenden.\n');
|
|
1212
1332
|
return { kind: 'installed', version: release.version };
|
|
1213
1333
|
}
|
|
1214
1334
|
|
|
1215
1335
|
if (Number.isInteger(installResult?.code) && installResult.code !== 0) {
|
|
1336
|
+
updateProgress.finish();
|
|
1216
1337
|
if (explicitUpdate) {
|
|
1217
1338
|
return explicitFailure(stderr, 'install_failed', installResult.code);
|
|
1218
1339
|
}
|
|
1219
1340
|
stderr.write(`Update fehlgeschlagen (Code ${installResult.code}). Die vorhandene Version wird gestartet.\n`);
|
|
1220
1341
|
return { kind: 'continue', reason: 'install_failed', exitCode: installResult.code };
|
|
1221
1342
|
}
|
|
1343
|
+
updateProgress.finish();
|
|
1222
1344
|
if (explicitUpdate) return explicitFailure(stderr, 'verification_failed');
|
|
1223
1345
|
stderr.write('Update konnte nicht verifiziert werden. Starte BLUN Code neu.\n');
|
|
1224
1346
|
return { kind: 'update_in_progress' };
|
|
@@ -1258,10 +1380,12 @@ module.exports = {
|
|
|
1258
1380
|
parseFallbackManifest,
|
|
1259
1381
|
parseRegistryDocument,
|
|
1260
1382
|
promptUpdateAction,
|
|
1383
|
+
readSkippedVersion,
|
|
1261
1384
|
readSnoozeState,
|
|
1262
1385
|
requestTrustedJson,
|
|
1263
1386
|
resolveEffectiveRelease,
|
|
1264
1387
|
runExplicitUpdate,
|
|
1265
1388
|
runUpdateNotice,
|
|
1389
|
+
writeSkippedVersion,
|
|
1266
1390
|
writeSnoozeState,
|
|
1267
1391
|
};
|