blun-king-cli 9.1.18 → 9.1.20

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 (3) hide show
  1. package/bin/update-notice.js +106 -7
  2. package/blun.mjs +2265 -410
  3. package/package.json +1 -1
@@ -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,6 +446,19 @@ 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 = 20;
453
+ return (phase, label) => {
454
+ if (!enabled) return;
455
+ const completed = Math.max(0, Math.min(phaseCount, phase));
456
+ const filled = Math.round((completed / phaseCount) * width);
457
+ const bar = `${'#'.repeat(filled)}${'-'.repeat(width - filled)}`;
458
+ output.write(`Update [${bar}] ${completed}/${phaseCount} ${label}\n`);
459
+ };
460
+ }
461
+
397
462
  function promptUpdateAction({ currentVersion, release, input, output, processRef = process }) {
398
463
  return new Promise((resolve, reject) => {
399
464
  let selected = 1;
@@ -403,14 +468,22 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
403
468
  let pendingInput = '';
404
469
  const decoder = new StringDecoder('utf8');
405
470
  const wasRaw = input.isRaw === true;
406
- const actions = ['Herunterladen und installieren', 'Aussetzen'];
471
+ const actions = [
472
+ {
473
+ choice: 'install',
474
+ label: `Jetzt aktualisieren (führt \`${installCommandText(release.version)}\` aus)`,
475
+ },
476
+ { choice: 'postpone', label: 'In sieben Tagen erneut fragen' },
477
+ { choice: 'skip-version', label: 'Diese Version überspringen' },
478
+ ];
407
479
  const signalHandlers = new Map();
408
480
 
409
481
  const renderActions = (refresh) => {
410
- if (refresh) output.write('\u001B[2F');
482
+ if (refresh) output.write(`\u001B[${actions.length + 1}F`);
411
483
  for (let index = 0; index < actions.length; index += 1) {
412
- output.write(`\r\u001B[2K ${selected === index ? '>' : ' '} ${actions[index]}\n`);
484
+ output.write(`\r\u001B[2K ${selected === index ? '>' : ' '} ${index + 1}. ${actions[index].label}\n`);
413
485
  }
486
+ output.write('\r\u001B[2KEnter drücken, um fortzufahren\n');
414
487
  };
415
488
  const cleanup = () => {
416
489
  if (escapeTimer !== undefined) clearTimeout(escapeTimer);
@@ -505,11 +578,18 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
505
578
  }
506
579
  if (first === '\r' || first === '\n') {
507
580
  pendingInput = pendingInput.slice(1);
508
- finish(selected === 0 ? 'install' : 'postpone');
581
+ finish(actions[selected].choice);
509
582
  return;
510
583
  }
511
584
  if (first !== '\u001B') {
585
+ const numericSelection = Number(first) - 1;
512
586
  pendingInput = pendingInput.slice(1);
587
+ if (Number.isInteger(numericSelection)
588
+ && numericSelection >= 0
589
+ && numericSelection < actions.length) {
590
+ selected = numericSelection;
591
+ renderActions(true);
592
+ }
513
593
  continue;
514
594
  }
515
595
  if (pendingInput.length > 1
@@ -526,13 +606,13 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
526
606
  }
527
607
  clearEscapeTimer();
528
608
  if (sequence === '\u001B[A' || sequence === '\u001B[D') {
529
- selected = 0;
609
+ selected = (selected - 1 + actions.length) % actions.length;
530
610
  pendingInput = pendingInput.slice(sequence.length);
531
611
  renderActions(true);
532
612
  continue;
533
613
  }
534
614
  if (sequence === '\u001B[B' || sequence === '\u001B[C') {
535
- selected = 1;
615
+ selected = (selected + 1) % actions.length;
536
616
  pendingInput = pendingInput.slice(sequence.length);
537
617
  renderActions(true);
538
618
  continue;
@@ -560,7 +640,7 @@ function promptUpdateAction({ currentVersion, release, input, output, processRef
560
640
  }
561
641
  output.write(`\nNeues Update verfügbar (${currentVersion} -> ${release.version})\n`);
562
642
  if (release.notesUrl) output.write(`Versionshinweise: ${release.notesUrl}\n`);
563
- output.write(`Installationsbefehl: ${installCommandText(release.version)}\n\n`);
643
+ output.write('\n');
564
644
  cursorHidden = true;
565
645
  output.write('\u001B[?25l');
566
646
  renderActions(false);
@@ -1121,6 +1201,9 @@ async function runUpdateFlow(options, explicitUpdate) {
1121
1201
  ? explicitNoTarget(stdout, reason)
1122
1202
  : { kind: 'continue', reason };
1123
1203
  }
1204
+ if (!explicitUpdate && readSkippedVersion(blunDir) === release.version) {
1205
+ return { kind: 'continue', reason: 'skipped_version' };
1206
+ }
1124
1207
  if (explicitUpdate) {
1125
1208
  try {
1126
1209
  ensurePrivateDirectory(blunDir);
@@ -1177,6 +1260,14 @@ async function runUpdateFlow(options, explicitUpdate) {
1177
1260
  }
1178
1261
  if (action !== 'install') {
1179
1262
  if (installerSession?.ready) await installerSession.cancel();
1263
+ if (action === 'skip-version') {
1264
+ try {
1265
+ writeSkippedVersion(blunDir, release.version);
1266
+ } catch {
1267
+ // A failed preference write must not block the existing console.
1268
+ }
1269
+ return { kind: 'continue', reason: 'skipped_version' };
1270
+ }
1180
1271
  try {
1181
1272
  writeSnoozeState(blunDir, (options.now || Date.now)());
1182
1273
  } catch {
@@ -1185,6 +1276,10 @@ async function runUpdateFlow(options, explicitUpdate) {
1185
1276
  return { kind: 'continue', reason: 'postponed' };
1186
1277
  }
1187
1278
 
1279
+ const updateProgress = createUpdateProgress(stdout);
1280
+ updateProgress(1, 'Update geprüft');
1281
+ updateProgress(2, 'Installer bereit');
1282
+
1188
1283
  try {
1189
1284
  lease.assertOwned();
1190
1285
  } catch {
@@ -1193,6 +1288,7 @@ async function runUpdateFlow(options, explicitUpdate) {
1193
1288
  : { kind: 'update_in_progress' };
1194
1289
  }
1195
1290
  let installResult;
1291
+ updateProgress(3, 'Paket wird installiert');
1196
1292
  try {
1197
1293
  installResult = options.installVersion
1198
1294
  ? await options.installVersion(release.version)
@@ -1208,6 +1304,7 @@ async function runUpdateFlow(options, explicitUpdate) {
1208
1304
  : { kind: 'update_in_progress' };
1209
1305
  }
1210
1306
  if (installResult?.code === 0 && installResult?.verified === true) {
1307
+ updateProgress(4, 'Installation geprüft');
1211
1308
  stdout.write('Update erfolgreich installiert. Starte BLUN Code neu, um die neue Version zu verwenden.\n');
1212
1309
  return { kind: 'installed', version: release.version };
1213
1310
  }
@@ -1258,10 +1355,12 @@ module.exports = {
1258
1355
  parseFallbackManifest,
1259
1356
  parseRegistryDocument,
1260
1357
  promptUpdateAction,
1358
+ readSkippedVersion,
1261
1359
  readSnoozeState,
1262
1360
  requestTrustedJson,
1263
1361
  resolveEffectiveRelease,
1264
1362
  runExplicitUpdate,
1265
1363
  runUpdateNotice,
1364
+ writeSkippedVersion,
1266
1365
  writeSnoozeState,
1267
1366
  };