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.
@@ -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 = 1;
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 actions = ['Herunterladen und installieren', 'Aussetzen'];
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('\u001B[2F');
498
+ if (refresh) output.write(`\u001B[${actions.length + 1}F`);
411
499
  for (let index = 0; index < actions.length; index += 1) {
412
- output.write(`\r\u001B[2K ${selected === index ? '>' : ' '} ${actions[index]}\n`);
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', postpone);
419
- input.removeListener('close', postpone);
420
- input.removeListener('error', postpone);
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 postpone() {
468
- finish('postpone');
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
- postpone();
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
- postpone();
593
+ skipOnce();
504
594
  return;
505
595
  }
506
596
  if (first === '\r' || first === '\n') {
507
597
  pendingInput = pendingInput.slice(1);
508
- finish(selected === 0 ? 'install' : 'postpone');
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
- postpone();
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 = 0;
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(`\nNeues Update verfügbar (${currentVersion} -> ${release.version})\n`);
562
- if (release.notesUrl) output.write(`Versionshinweise: ${release.notesUrl}\n`);
563
- output.write(`Installationsbefehl: ${installCommandText(release.version)}\n\n`);
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', postpone);
569
- input.once('close', postpone);
570
- input.once('error', postpone);
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
  };