@voiden/runner 0.1.0-beta.5 → 2.1.0

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 (41) hide show
  1. package/README.md +117 -95
  2. package/dist/cliElectron.d.ts +1 -1
  3. package/dist/index.js +197 -51
  4. package/dist/index.js.map +1 -1
  5. package/dist/plugins/community.d.ts +4 -3
  6. package/dist/plugins/community.d.ts.map +1 -1
  7. package/dist/plugins/community.js +23 -19
  8. package/dist/plugins/community.js.map +1 -1
  9. package/dist/plugins/loader.d.ts +8 -0
  10. package/dist/plugins/loader.d.ts.map +1 -1
  11. package/dist/plugins/loader.js +78 -7
  12. package/dist/plugins/loader.js.map +1 -1
  13. package/dist/plugins/registry.d.ts +26 -21
  14. package/dist/plugins/registry.d.ts.map +1 -1
  15. package/dist/plugins/registry.js +69 -46
  16. package/dist/plugins/registry.js.map +1 -1
  17. package/dist/plugins/registryCache.d.ts +28 -0
  18. package/dist/plugins/registryCache.d.ts.map +1 -0
  19. package/dist/plugins/registryCache.js +95 -0
  20. package/dist/plugins/registryCache.js.map +1 -0
  21. package/dist/plugins/store.d.ts +6 -1
  22. package/dist/plugins/store.d.ts.map +1 -1
  23. package/dist/plugins/store.js +21 -1
  24. package/dist/plugins/store.js.map +1 -1
  25. package/dist/plugins/updateCheck.d.ts +14 -0
  26. package/dist/plugins/updateCheck.d.ts.map +1 -0
  27. package/dist/plugins/updateCheck.js +46 -0
  28. package/dist/plugins/updateCheck.js.map +1 -0
  29. package/dist/report/mail.d.ts +1 -0
  30. package/dist/report/mail.d.ts.map +1 -1
  31. package/dist/report/mail.js +199 -35
  32. package/dist/report/mail.js.map +1 -1
  33. package/dist/runner.d.ts +1 -1
  34. package/dist/runner.d.ts.map +1 -1
  35. package/dist/runner.js +19 -2
  36. package/dist/runner.js.map +1 -1
  37. package/dist/runtimeVars.d.ts +5 -0
  38. package/dist/runtimeVars.d.ts.map +1 -1
  39. package/dist/runtimeVars.js +36 -2
  40. package/dist/runtimeVars.js.map +1 -1
  41. package/package.json +10 -5
package/dist/index.js CHANGED
@@ -9,9 +9,11 @@ import { runVoidFile } from './runner.js';
9
9
  import { loadEnabledPlugins } from './plugins/loader.js';
10
10
  import { exportToCsv } from './report/csv.js';
11
11
  import { sendMailReport } from './report/mail.js';
12
- import { CORE_PLUGINS, findPlugin } from './plugins/registry.js';
12
+ import { getCorePlugins, findPlugin } from './plugins/registry.js';
13
+ import { downloadCoreRunner } from './plugins/loader.js';
13
14
  import { fetchCommunityPlugins, findCommunityPlugin, hasCommunityRunner, installCommunityRunner, } from './plugins/community.js';
14
- import { installPlugin, uninstallPlugin, setPluginEnabled, getAllInstalledPlugins, readStore, STORE_DIR, } from './plugins/store.js';
15
+ import { installPlugin, uninstallPlugin, setPluginEnabled, setPluginVersion, getAllInstalledPlugins, readStore, STORE_DIR, } from './plugins/store.js';
16
+ import { checkForPluginUpdates } from './plugins/updateCheck.js';
15
17
  import { appendSessionResults, loadSessionResults, clearSession, } from './session.js';
16
18
  // ─────────────────────────────────────────────────────────────────────────────
17
19
  // Helpers
@@ -279,6 +281,29 @@ function printRunSummaryJson(results, totalMs, activePlugins) {
279
281
  console.log(JSON.stringify(output, null, 2));
280
282
  }
281
283
  // ─────────────────────────────────────────────────────────────────────────────
284
+ // Plugin update notices — surfaced after `run` and `plugin` commands so users
285
+ // learn about newer registry versions without having to run `plugin list`.
286
+ // ─────────────────────────────────────────────────────────────────────────────
287
+ function printUpdateNotice(updates) {
288
+ if (updates.length === 0)
289
+ return;
290
+ console.log();
291
+ console.log(chalk.yellow(` ⬆ ${updates.length} plugin update${updates.length !== 1 ? 's' : ''} available`));
292
+ for (const u of updates) {
293
+ console.log(chalk.gray(` ${chalk.bold(u.id.padEnd(24))} ${u.installedVersion} → ${chalk.green(u.latestVersion)}`));
294
+ }
295
+ console.log(chalk.gray(` Run: voiden-runner plugin update --all`));
296
+ }
297
+ /** Best-effort update check — never throws, never blocks command output on failure. */
298
+ async function notifyPluginUpdates() {
299
+ try {
300
+ printUpdateNotice(await checkForPluginUpdates());
301
+ }
302
+ catch {
303
+ // Informational only — ignore failures (e.g. offline)
304
+ }
305
+ }
306
+ // ─────────────────────────────────────────────────────────────────────────────
282
307
  // CLI
283
308
  // ─────────────────────────────────────────────────────────────────────────────
284
309
  const pkgPath = resolve(join(dirname(fileURLToPath(import.meta.url)), '../package.json'));
@@ -308,7 +333,7 @@ program
308
333
  .option('--fail-on-error', 'Exit with code 1 if any request fails (runs all files first)')
309
334
  .option('--verbose', 'Print plugin and script logs')
310
335
  .option('--json', 'Output results as JSON (suppresses normal output — useful for CI pipelines)')
311
- .option('--no-session', 'Completely stateless run (do not load/save results or runtime variables)')
336
+ .option('--no-session', 'Completely stateless run no variables are loaded from disk, shared between files, or saved')
312
337
  .option('--output-json <file>', 'Write the full result object to a JSON file — pass to the next CLI, script, or tool')
313
338
  .option('--csv <path>', 'Export full report (request + response headers, bodies, assertions) to a CSV file')
314
339
  .option('--mail', 'Send HTML report to address specified in VOIDEN_MAIL_TO env')
@@ -383,10 +408,6 @@ program
383
408
  console.error(chalk.red(' ✗ Mail error: no recipient found. Please provide --mail-to or set VOIDEN_MAIL_TO.'));
384
409
  process.exit(1);
385
410
  }
386
- if (!opts.csv) {
387
- console.error(chalk.red(' ✗ Mail error: no CSV generated. Mail requires --csv flag.'));
388
- process.exit(1);
389
- }
390
411
  if (!smtpHost) {
391
412
  console.error(chalk.red(' ✗ Mail keys are missing. Please provide SMTP configuration (VOIDEN_SMTP_HOST).'));
392
413
  process.exit(1);
@@ -401,7 +422,7 @@ program
401
422
  const runtimeVars = {};
402
423
  // Load persisted runtime variables if not disabled
403
424
  const VARS_PATH = join(STORE_DIR, '.process.env.json');
404
- if (!opts.noSession && existsSync(VARS_PATH)) {
425
+ if (opts.session && existsSync(VARS_PATH)) {
405
426
  try {
406
427
  const data = JSON.parse(readFileSync(VARS_PATH, 'utf-8'));
407
428
  Object.assign(runtimeVars, data);
@@ -419,7 +440,10 @@ program
419
440
  const file = resolvedFiles[i];
420
441
  const stopSpinner = opts.json ? () => { } : startSpinner(`[${i + 1}/${resolvedFiles.length}] ${basename(file)}`);
421
442
  try {
422
- const { results } = await runVoidFile(file, { env, verbose: opts.verbose, runtimeVars, activePlugins });
443
+ // --no-session: each file is fully isolated no vars flow from one file to another.
444
+ // Session mode: all files share runtimeVars so captured vars chain across files.
445
+ const fileVars = opts.session ? runtimeVars : {};
446
+ const { results } = await runVoidFile(file, { env, verbose: opts.verbose, runtimeVars: fileVars, activePlugins });
423
447
  stopSpinner();
424
448
  for (const { result } of results) {
425
449
  if (!result.success)
@@ -450,12 +474,12 @@ program
450
474
  }
451
475
  }
452
476
  // Save session results if not disabled
453
- if (!opts.noSession) {
477
+ if (opts.session) {
454
478
  appendSessionResults(allResults);
455
479
  }
456
480
  const totalMs = Date.now() - runStart;
457
481
  // Save runtime variables if not disabled
458
- if (!opts.noSession && Object.keys(runtimeVars).length > 0) {
482
+ if (opts.session && Object.keys(runtimeVars).length > 0) {
459
483
  try {
460
484
  mkdirSync(STORE_DIR, { recursive: true });
461
485
  writeFileSync(VARS_PATH, JSON.stringify(runtimeVars, null, 2), 'utf-8');
@@ -489,6 +513,30 @@ program
489
513
  console.error(chalk.red(` ✗ Failed to write CSV: ${err?.message ?? String(err)}`));
490
514
  }
491
515
  }
516
+ // ── Output JSON to file (before mail so it can be attached) ──────────────
517
+ let savedJsonPath;
518
+ if (opts.outputJson) {
519
+ const jsonData = {
520
+ summary: {
521
+ total: allResults.length,
522
+ passed: allResults.filter(r => r.result.success).length,
523
+ failed: allResults.filter(r => !r.result.success).length,
524
+ totalDurationMs: totalMs,
525
+ activePlugins,
526
+ },
527
+ requests: allResults.map(r => ({ file: r.file, ...r.result })),
528
+ };
529
+ try {
530
+ mkdirSync(dirname(opts.outputJson), { recursive: true });
531
+ writeFileSync(opts.outputJson, JSON.stringify(jsonData, null, 2) + '\n', 'utf-8');
532
+ savedJsonPath = opts.outputJson;
533
+ if (!opts.json)
534
+ console.log(chalk.gray(` ↳ Results written to ${opts.outputJson}`));
535
+ }
536
+ catch (err) {
537
+ console.error(chalk.red(` ✗ Failed to write JSON: ${err?.message ?? String(err)}`));
538
+ }
539
+ }
492
540
  // ── Email report ──────────────────────────────────────────────────────────
493
541
  if (mailTo) {
494
542
  console.log(chalk.gray(` ↑ Sending report to ${mailTo} …`));
@@ -503,6 +551,7 @@ program
503
551
  smtpUser: smtpUser,
504
552
  smtpPass: smtpPass,
505
553
  csvPath: savedCsvPath,
554
+ jsonPath: savedJsonPath,
506
555
  });
507
556
  console.log(chalk.green(` ✓ Report sent to ${mailTo}`));
508
557
  }
@@ -510,23 +559,6 @@ program
510
559
  console.error(chalk.red(` ✗ Failed to send email: ${err?.message ?? String(err)}`));
511
560
  }
512
561
  }
513
- // ── Output JSON to file ───────────────────────────────────────────────────
514
- if (opts.outputJson) {
515
- const jsonData = {
516
- summary: {
517
- total: allResults.length,
518
- passed: allResults.filter(r => r.result.success).length,
519
- failed: allResults.filter(r => !r.result.success).length,
520
- totalDurationMs: Date.now() - runStart,
521
- activePlugins,
522
- },
523
- requests: allResults.map(r => ({ file: r.file, ...r.result })),
524
- };
525
- mkdirSync(dirname(opts.outputJson), { recursive: true });
526
- writeFileSync(opts.outputJson, JSON.stringify(jsonData, null, 2) + '\n', 'utf-8');
527
- if (!opts.json)
528
- console.log(chalk.gray(` ↳ Results written to ${opts.outputJson}`));
529
- }
530
562
  const shouldFail = (opts.failOnError || stopOnFailure) && anyFailed;
531
563
  if (shouldFail && !opts.json) {
532
564
  const failedCount = allResults.filter(r => !r.result.success).length;
@@ -534,6 +566,9 @@ program
534
566
  console.log(chalk.gray(' (use this exit code in your shell script to abort on failure)'));
535
567
  console.log();
536
568
  }
569
+ // Surface plugin update notices — skipped in --json mode so output stays machine-readable
570
+ if (!opts.json)
571
+ await notifyPluginUpdates();
537
572
  process.exit(shouldFail ? 1 : 0);
538
573
  });
539
574
  // ── voiden-runner session ─────────────────────────────────────────────────────
@@ -635,8 +670,9 @@ reportCmd
635
670
  .alias('gen')
636
671
  .option('-e, --env <path>', 'Path to .env or .yaml file for SMTP configuration')
637
672
  .option('--csv <path>', 'Export session results to a CSV file')
638
- .option('--mail', 'Send HTML summary + attached CSV using VOIDEN_MAIL_TO env (requires --csv)')
639
- .option('--mail-to <address>', 'Send HTML summary + attached CSV to this email address (requires --csv)')
673
+ .option('--output-json <file>', 'Write full result object to a JSON file (also attached to email if --mail is used)')
674
+ .option('--mail', 'Send HTML report to VOIDEN_MAIL_TO (attaches --csv and/or --output-json if provided)')
675
+ .option('--mail-to <address>', 'Send HTML report to this email address (attaches --csv and/or --output-json if provided)')
640
676
  .option('--mail-from <address>', 'Sender address for the report email')
641
677
  .option('--mail-subject <subject>', 'Email subject line')
642
678
  .option('--smtp-host <host>', 'SMTP server host')
@@ -669,18 +705,14 @@ reportCmd
669
705
  console.error(chalk.red(' ✗ Mail error: no recipient found. Please provide --mail-to or set VOIDEN_MAIL_TO.'));
670
706
  process.exit(1);
671
707
  }
672
- if (!opts.csv) {
673
- console.error(chalk.red(' ✗ Mail error: no CSV generated. Mail requires --csv flag.'));
674
- process.exit(1);
675
- }
676
708
  const smtpHost = opts.smtpHost || env.VOIDEN_SMTP_HOST;
677
709
  if (!smtpHost) {
678
710
  console.error(chalk.red(' ✗ Mail keys are missing. Please provide SMTP configuration (VOIDEN_SMTP_HOST).'));
679
711
  process.exit(1);
680
712
  }
681
713
  }
682
- if (!opts.csv && !mailTo) {
683
- console.log(chalk.gray(` Session has ${results.length} accumulated results. Specify --csv to generate a report.`));
714
+ if (!opts.csv && !opts.outputJson && !mailTo) {
715
+ console.log(chalk.gray(` Session has ${results.length} accumulated results. Specify --csv or --output-json to generate a report.`));
684
716
  return;
685
717
  }
686
718
  let savedCsvPath;
@@ -693,6 +725,26 @@ reportCmd
693
725
  console.error(chalk.red(` ✗ Failed to write CSV: ${err?.message ?? String(err)}`));
694
726
  }
695
727
  }
728
+ let savedJsonPath;
729
+ if (opts.outputJson) {
730
+ const jsonData = {
731
+ summary: {
732
+ total: results.length,
733
+ passed: results.filter(r => r.result.success).length,
734
+ failed: results.filter(r => !r.result.success).length,
735
+ },
736
+ requests: results.map(r => ({ file: r.file, ...r.result })),
737
+ };
738
+ try {
739
+ mkdirSync(dirname(opts.outputJson), { recursive: true });
740
+ writeFileSync(opts.outputJson, JSON.stringify(jsonData, null, 2) + '\n', 'utf-8');
741
+ savedJsonPath = opts.outputJson;
742
+ console.log(chalk.gray(` ↳ Results written to ${opts.outputJson}`));
743
+ }
744
+ catch (err) {
745
+ console.error(chalk.red(` ✗ Failed to write JSON: ${err?.message ?? String(err)}`));
746
+ }
747
+ }
696
748
  if (mailTo) {
697
749
  // SMTP settings — read from CLI options or environment
698
750
  const smtpHost = opts.smtpHost || env.VOIDEN_SMTP_HOST || process.env.VOIDEN_SMTP_HOST;
@@ -717,6 +769,7 @@ reportCmd
717
769
  smtpUser: smtpUser,
718
770
  smtpPass: smtpPass,
719
771
  csvPath: savedCsvPath,
772
+ jsonPath: savedJsonPath,
720
773
  });
721
774
  console.log(chalk.green(` ✓ Report sent to ${mailTo}`));
722
775
  }
@@ -740,13 +793,14 @@ pluginCmd
740
793
  ' voiden-runner plugin install apyhub-explorer\n')
741
794
  .option('--all', 'Install all core plugins (community plugins must be installed by name)')
742
795
  .action(async (names, opts) => {
796
+ const corePlugins = await getCorePlugins();
743
797
  const communityPlugins = await fetchCommunityPlugins();
744
798
  const targets = opts.all
745
- ? CORE_PLUGINS.map(p => p.name)
799
+ ? corePlugins.map(p => p.name)
746
800
  : names;
747
801
  if (targets.length === 0) {
748
802
  console.error(chalk.red('Specify plugin name(s) or use --all'));
749
- console.log(chalk.gray(' Core: ' + CORE_PLUGINS.map(p => p.name).join(', ')));
803
+ console.log(chalk.gray(' Core: ' + corePlugins.map(p => p.name).join(', ')));
750
804
  if (communityPlugins.length > 0) {
751
805
  console.log(chalk.gray(' Community (install by name): ' + communityPlugins.map(p => p.id).join(', ')));
752
806
  }
@@ -754,7 +808,7 @@ pluginCmd
754
808
  }
755
809
  let installedCount = 0;
756
810
  for (const name of targets) {
757
- const coreDef = findPlugin(name);
811
+ const coreDef = await findPlugin(name);
758
812
  const commDef = !coreDef ? findCommunityPlugin(name, communityPlugins) : undefined;
759
813
  if (!coreDef && !commDef) {
760
814
  console.log(chalk.yellow(` ⚠ Unknown plugin "${name}" — skipped`));
@@ -769,6 +823,7 @@ pluginCmd
769
823
  process.stdout.write('\r' + chalk.yellow(` ⚠ No runner.js in release for "${name}" — skipped\n`));
770
824
  continue;
771
825
  }
826
+ setPluginVersion(name, commDef.version);
772
827
  process.stdout.write('\r' + ' '.repeat(60) + '\r'); // clear the line
773
828
  }
774
829
  catch (err) {
@@ -777,7 +832,7 @@ pluginCmd
777
832
  }
778
833
  }
779
834
  const description = coreDef ? coreDef.description : commDef.description;
780
- const fresh = installPlugin(name);
835
+ const fresh = installPlugin(name, coreDef?.version ?? commDef?.version);
781
836
  if (fresh) {
782
837
  console.log(chalk.green(` ✓ Installed`) + chalk.bold(` ${name}`) + chalk.gray(` — ${description}`));
783
838
  installedCount++;
@@ -790,6 +845,80 @@ pluginCmd
790
845
  console.log();
791
846
  console.log(chalk.gray(` ${installedCount} plugin(s) installed. State saved to ~/.voiden/plugins.json`));
792
847
  }
848
+ await notifyPluginUpdates();
849
+ });
850
+ // voiden-runner plugin update [names...] --all
851
+ pluginCmd
852
+ .command('update [names...]')
853
+ .description('Update installed plugins to the latest registry version\n\n' +
854
+ ' --all updates every installed plugin that has a newer version available.\n\n' +
855
+ ' Examples:\n' +
856
+ ' voiden-runner plugin update --all\n' +
857
+ ' voiden-runner plugin update voiden-scripting\n' +
858
+ ' voiden-runner plugin update apyhub-explorer voiden-scripting\n')
859
+ .option('--all', 'Update every installed plugin that has a newer version available')
860
+ .action(async (names, opts) => {
861
+ const updates = await checkForPluginUpdates();
862
+ if (updates.length === 0) {
863
+ console.log(chalk.gray(' All installed plugins are up to date.'));
864
+ return;
865
+ }
866
+ const targets = opts.all ? updates.map(u => u.id) : names;
867
+ if (targets.length === 0) {
868
+ console.log(chalk.yellow(` ${updates.length} update${updates.length !== 1 ? 's' : ''} available — specify plugin name(s) or use --all:`));
869
+ printUpdateNotice(updates);
870
+ return;
871
+ }
872
+ const corePlugins = await getCorePlugins();
873
+ const communityPlugins = await fetchCommunityPlugins();
874
+ let updatedCount = 0;
875
+ for (const name of targets) {
876
+ const update = updates.find(u => u.id === name);
877
+ if (!update) {
878
+ const known = readStore().installedPlugins[name];
879
+ console.log(known
880
+ ? chalk.gray(` · ${name} is already up to date — skipped`)
881
+ : chalk.yellow(` ⚠ Plugin "${name}" is not installed — skipped`));
882
+ continue;
883
+ }
884
+ process.stdout.write(` ↓ Updating ${chalk.bold(name)} to v${update.latestVersion} …`);
885
+ try {
886
+ if (update.type === 'core') {
887
+ const def = corePlugins.find(p => p.name === name);
888
+ if (!def)
889
+ throw new Error('plugin no longer in core registry');
890
+ const ok = await downloadCoreRunner(def.name, def.repo, def.runnerAsset, def.version, false);
891
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
892
+ if (!ok) {
893
+ console.log(chalk.red(` ✗ No "${def.runnerAsset}" asset in release v${update.latestVersion} for "${name}"`));
894
+ continue;
895
+ }
896
+ }
897
+ else {
898
+ const def = communityPlugins.find(p => p.id === name);
899
+ if (!def)
900
+ throw new Error('plugin no longer in community registry');
901
+ const result = await installCommunityRunner(def);
902
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
903
+ if (result === 'no-runner') {
904
+ console.log(chalk.red(` ✗ No runner.js in release v${update.latestVersion} for "${name}"`));
905
+ continue;
906
+ }
907
+ setPluginVersion(name, update.latestVersion);
908
+ }
909
+ console.log(chalk.green(` ✓ Updated`) + chalk.bold(` ${name}`) +
910
+ chalk.gray(` ${update.installedVersion} → ${update.latestVersion}`));
911
+ updatedCount++;
912
+ }
913
+ catch (err) {
914
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
915
+ console.log(chalk.red(` ✗ Failed to update "${name}": ${err?.message ?? String(err)}`));
916
+ }
917
+ }
918
+ if (updatedCount > 0) {
919
+ console.log();
920
+ console.log(chalk.gray(` ${updatedCount} plugin(s) updated.`));
921
+ }
793
922
  });
794
923
  // voiden-runner plugin uninstall <name>
795
924
  pluginCmd
@@ -854,31 +983,34 @@ pluginCmd
854
983
  ' voiden-runner plugin disable voiden-scripting\n' +
855
984
  ' voiden-runner plugin disable --all\n')
856
985
  .option('--all', 'Disable all plugins (core and community)')
857
- .action((name, opts) => {
986
+ .action(async (name, opts) => {
987
+ const corePlugins = await getCorePlugins();
858
988
  if (opts.all) {
859
989
  // Disable all core plugins
860
- for (const def of CORE_PLUGINS) {
990
+ for (const def of corePlugins) {
861
991
  setPluginEnabled(def.name, false);
862
992
  console.log(chalk.yellow(` · Disabled`) + ` ${def.name}`);
863
993
  }
864
994
  // Disable all installed community plugins
865
995
  const store = readStore();
866
- const communityNames = Object.keys(store.installedPlugins).filter(n => !findPlugin(n));
996
+ const coreNames = new Set(corePlugins.map(p => p.name));
997
+ const communityNames = Object.keys(store.installedPlugins).filter(n => !coreNames.has(n));
867
998
  for (const n of communityNames) {
868
999
  setPluginEnabled(n, false);
869
1000
  console.log(chalk.yellow(` · Disabled`) + ` ${n}`);
870
1001
  }
871
- const total = CORE_PLUGINS.length + communityNames.length;
1002
+ const total = corePlugins.length + communityNames.length;
872
1003
  console.log(chalk.gray(` ${total} plugin(s) disabled.`));
873
1004
  return;
874
1005
  }
875
1006
  if (!name) {
876
1007
  console.error(chalk.red(' Specify a plugin name or use --all'));
877
1008
  process.exit(1);
1009
+ return;
878
1010
  }
879
1011
  setPluginEnabled(name, false);
880
1012
  console.log(chalk.yellow(` · Disabled`) + ` ${name}`);
881
- if (findPlugin(name)) {
1013
+ if (await findPlugin(name)) {
882
1014
  console.log(chalk.gray(` Core plugin disabled. Re-enable with: voiden-runner plugin enable ${name}`));
883
1015
  }
884
1016
  });
@@ -888,17 +1020,26 @@ pluginCmd
888
1020
  .description('List all available and installed plugins')
889
1021
  .action(async () => {
890
1022
  const store = readStore();
1023
+ const corePlugins = await getCorePlugins();
891
1024
  const communityPlugins = await fetchCommunityPlugins();
1025
+ // Per-plugin "update available" badge — compares the version recorded at
1026
+ // install/download time against the registry's current version.
1027
+ const updateBadge = (id, latestVersion) => {
1028
+ const installedVersion = store.installedPlugins[id]?.version;
1029
+ if (!installedVersion || installedVersion === latestVersion)
1030
+ return '';
1031
+ return chalk.yellow(` ⬆ v${latestVersion} available`);
1032
+ };
892
1033
  console.log();
893
- console.log(chalk.bold(' Core plugins') + chalk.gray(' (@voiden/core-extensions)'));
1034
+ console.log(chalk.bold(' Core plugins') + chalk.gray(' (from VoidenHQ/plugin-registry)'));
894
1035
  console.log(DIVIDER);
895
- for (const def of CORE_PLUGINS) {
1036
+ for (const def of corePlugins) {
896
1037
  const record = store.installedPlugins[def.name];
897
1038
  const isDisabled = record !== undefined && !record.enabled;
898
1039
  const statusBadge = isDisabled
899
1040
  ? chalk.yellow(' · disabled')
900
1041
  : chalk.green(' ✓ enabled');
901
- console.log(` ${chalk.bold(def.name.padEnd(24))}${statusBadge}`);
1042
+ console.log(` ${chalk.bold(def.name.padEnd(24))}${statusBadge}${updateBadge(def.name, def.version)}`);
902
1043
  console.log(chalk.gray(` ${def.description}`));
903
1044
  }
904
1045
  // ── Community plugins ───────────────────────────────────────────────────
@@ -908,7 +1049,7 @@ pluginCmd
908
1049
  console.log(DIVIDER);
909
1050
  }
910
1051
  else {
911
- console.log(chalk.bold(' Community plugins') + chalk.gray(' (github.com/VoidenHQ/plugins)'));
1052
+ console.log(chalk.bold(' Community plugins') + chalk.gray(' (from VoidenHQ/plugin-registry)'));
912
1053
  console.log(DIVIDER);
913
1054
  for (const def of communityPlugins) {
914
1055
  const installed = store.installedPlugins[def.id];
@@ -923,14 +1064,14 @@ pluginCmd
923
1064
  statusBadge = chalk.yellow(' · disabled');
924
1065
  }
925
1066
  const runnerBadge = hasCommunityRunner(def.id) ? '' : chalk.gray(' [no runner]');
926
- console.log(` ${chalk.bold(def.id.padEnd(24))}${statusBadge}${runnerBadge}` +
1067
+ console.log(` ${chalk.bold(def.id.padEnd(24))}${statusBadge}${runnerBadge}${updateBadge(def.id, def.version)}` +
927
1068
  chalk.gray(` v${def.version}`) +
928
1069
  chalk.gray(` by ${def.author}`));
929
1070
  console.log(chalk.gray(` ${def.description}`));
930
1071
  }
931
1072
  }
932
1073
  const knownIds = new Set([
933
- ...CORE_PLUGINS.map(p => p.name),
1074
+ ...corePlugins.map(p => p.name),
934
1075
  ...communityPlugins.map(p => p.id),
935
1076
  ]);
936
1077
  const extras = getAllInstalledPlugins().filter(p => !knownIds.has(p.name));
@@ -943,6 +1084,11 @@ pluginCmd
943
1084
  console.log(` ${chalk.bold(p.name.padEnd(24))}${badge}`);
944
1085
  }
945
1086
  }
1087
+ const updates = await checkForPluginUpdates();
1088
+ if (updates.length > 0) {
1089
+ console.log();
1090
+ console.log(chalk.gray(` Run: voiden-runner plugin update --all (${updates.length} update${updates.length !== 1 ? 's' : ''} available)`));
1091
+ }
946
1092
  console.log();
947
1093
  });
948
1094
  program.parse();