@voiden/runner 0.1.0-beta.6 → 2.1.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 (46) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bundled-runners/package.json +3 -0
  3. package/bundled-runners/simple-assertions-runner.js +1 -0
  4. package/bundled-runners/versions.json +9 -0
  5. package/bundled-runners/voiden-advanced-auth-runner.js +1 -0
  6. package/bundled-runners/voiden-faker-runner.js +16 -0
  7. package/bundled-runners/voiden-graphql-runner.js +1 -0
  8. package/bundled-runners/voiden-rest-api-runner.js +1 -0
  9. package/bundled-runners/voiden-scripting-runner.js +397 -0
  10. package/bundled-runners/voiden-sockets-grpcs-runner.js +1 -0
  11. package/dist/cliElectron.d.ts +1 -1
  12. package/dist/index.js +267 -26
  13. package/dist/index.js.map +1 -1
  14. package/dist/plugins/community.d.ts +4 -3
  15. package/dist/plugins/community.d.ts.map +1 -1
  16. package/dist/plugins/community.js +32 -21
  17. package/dist/plugins/community.js.map +1 -1
  18. package/dist/plugins/loader.d.ts +9 -0
  19. package/dist/plugins/loader.d.ts.map +1 -1
  20. package/dist/plugins/loader.js +93 -8
  21. package/dist/plugins/loader.js.map +1 -1
  22. package/dist/plugins/registry.d.ts +30 -21
  23. package/dist/plugins/registry.d.ts.map +1 -1
  24. package/dist/plugins/registry.js +111 -46
  25. package/dist/plugins/registry.js.map +1 -1
  26. package/dist/plugins/registryCache.d.ts +30 -0
  27. package/dist/plugins/registryCache.d.ts.map +1 -0
  28. package/dist/plugins/registryCache.js +95 -0
  29. package/dist/plugins/registryCache.js.map +1 -0
  30. package/dist/plugins/store.d.ts +6 -1
  31. package/dist/plugins/store.d.ts.map +1 -1
  32. package/dist/plugins/store.js +21 -1
  33. package/dist/plugins/store.js.map +1 -1
  34. package/dist/plugins/updateCheck.d.ts +19 -0
  35. package/dist/plugins/updateCheck.d.ts.map +1 -0
  36. package/dist/plugins/updateCheck.js +60 -0
  37. package/dist/plugins/updateCheck.js.map +1 -0
  38. package/dist/runner.d.ts +1 -1
  39. package/dist/runner.d.ts.map +1 -1
  40. package/dist/runner.js +19 -2
  41. package/dist/runner.js.map +1 -1
  42. package/dist/runtimeVars.d.ts +5 -0
  43. package/dist/runtimeVars.d.ts.map +1 -1
  44. package/dist/runtimeVars.js +36 -2
  45. package/dist/runtimeVars.js.map +1 -1
  46. package/package.json +13 -6
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, hasCoreRunner } 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
@@ -278,6 +280,72 @@ function printRunSummaryJson(results, totalMs, activePlugins) {
278
280
  };
279
281
  console.log(JSON.stringify(output, null, 2));
280
282
  }
283
+ /** Split CHANGELOG.md into per-version entries on "## " headers (newest first, matching file order). */
284
+ function parseChangelog(content) {
285
+ const lines = content.split('\n');
286
+ const entries = [];
287
+ let current = null;
288
+ for (const line of lines) {
289
+ const headerMatch = line.match(/^##\s+(\S+)(?:\s+-\s+(.+))?$/);
290
+ if (headerMatch) {
291
+ if (current)
292
+ entries.push(current);
293
+ current = { version: headerMatch[1], date: headerMatch[2]?.trim(), body: [] };
294
+ continue;
295
+ }
296
+ if (current)
297
+ current.body.push(line);
298
+ }
299
+ if (current)
300
+ entries.push(current);
301
+ return entries;
302
+ }
303
+ function renderChangelogEntry(entry) {
304
+ const header = entry.date ? `${entry.version} ${chalk.gray(entry.date)}` : entry.version;
305
+ console.log(chalk.bold.white(` ${header}`));
306
+ console.log(DIVIDER);
307
+ for (const line of entry.body) {
308
+ const trimmed = line.trim();
309
+ if (!trimmed)
310
+ continue;
311
+ const sectionMatch = trimmed.match(/^###\s+(.+)$/);
312
+ if (sectionMatch) {
313
+ console.log();
314
+ console.log(chalk.bold.cyan(` ${sectionMatch[1]}`));
315
+ continue;
316
+ }
317
+ const bulletMatch = trimmed.match(/^-\s+(.+)$/);
318
+ if (bulletMatch) {
319
+ console.log(chalk.gray(` · `) + bulletMatch[1]);
320
+ continue;
321
+ }
322
+ console.log(` ${trimmed}`);
323
+ }
324
+ console.log();
325
+ }
326
+ // ─────────────────────────────────────────────────────────────────────────────
327
+ // Plugin update notices — surfaced after `run` and `plugin` commands so users
328
+ // learn about newer registry versions without having to run `plugin list`.
329
+ // ─────────────────────────────────────────────────────────────────────────────
330
+ function printUpdateNotice(updates) {
331
+ if (updates.length === 0)
332
+ return;
333
+ console.log();
334
+ console.log(chalk.yellow(` ⬆ ${updates.length} plugin update${updates.length !== 1 ? 's' : ''} available`));
335
+ for (const u of updates) {
336
+ console.log(chalk.gray(` ${chalk.bold(u.id.padEnd(24))} ${u.installedVersion} → ${chalk.green(u.latestVersion)}`));
337
+ }
338
+ console.log(chalk.gray(` Run: voiden-runner plugin update --all`));
339
+ }
340
+ /** Best-effort update check — never throws, never blocks command output on failure. */
341
+ async function notifyPluginUpdates() {
342
+ try {
343
+ printUpdateNotice(await checkForPluginUpdates());
344
+ }
345
+ catch {
346
+ // Informational only — ignore failures (e.g. offline)
347
+ }
348
+ }
281
349
  // ─────────────────────────────────────────────────────────────────────────────
282
350
  // CLI
283
351
  // ─────────────────────────────────────────────────────────────────────────────
@@ -287,6 +355,47 @@ program
287
355
  .name('voiden-runner')
288
356
  .description('Run .void files headlessly — REST, WebSocket, and gRPC')
289
357
  .version(pkg.version);
358
+ // ── voiden-runner changelog ───────────────────────────────────────────────────
359
+ program
360
+ .command('changelog [version]')
361
+ .description('Show release notes for voiden-runner — versioned independently of the desktop app\n\n' +
362
+ ' Examples:\n' +
363
+ ' voiden-runner changelog # full history\n' +
364
+ ' voiden-runner changelog --latest # most recent release only\n' +
365
+ ' voiden-runner changelog 2.1.0 # a specific version\n')
366
+ .option('--latest', 'Show only the most recent release')
367
+ .action((version, opts) => {
368
+ const changelogPath = resolve(join(dirname(fileURLToPath(import.meta.url)), '../CHANGELOG.md'));
369
+ if (!existsSync(changelogPath)) {
370
+ console.error(chalk.red(' ✗ No CHANGELOG.md found for this install.'));
371
+ process.exit(1);
372
+ }
373
+ const entries = parseChangelog(readFileSync(changelogPath, 'utf-8'));
374
+ if (entries.length === 0) {
375
+ console.log(chalk.gray(' Changelog is empty.'));
376
+ return;
377
+ }
378
+ let toShow = entries;
379
+ if (version) {
380
+ const normalized = version.replace(/^v/, '');
381
+ const match = entries.find(e => e.version.replace(/^v/, '') === normalized);
382
+ if (!match) {
383
+ console.error(chalk.red(` ✗ No changelog entry found for version "${version}".`));
384
+ console.log(chalk.gray(` Available: ${entries.map(e => e.version).join(', ')}`));
385
+ process.exit(1);
386
+ }
387
+ toShow = [match];
388
+ }
389
+ else if (opts.latest) {
390
+ toShow = [entries[0]];
391
+ }
392
+ console.log();
393
+ console.log(chalk.bold.white(' voiden-runner changelog'));
394
+ console.log();
395
+ for (const entry of toShow) {
396
+ renderChangelogEntry(entry);
397
+ }
398
+ });
290
399
  // ── voiden-runner run ─────────────────────────────────────────────────────────
291
400
  program
292
401
  .command('run <paths...>')
@@ -541,6 +650,9 @@ program
541
650
  console.log(chalk.gray(' (use this exit code in your shell script to abort on failure)'));
542
651
  console.log();
543
652
  }
653
+ // Surface plugin update notices — skipped in --json mode so output stays machine-readable
654
+ if (!opts.json)
655
+ await notifyPluginUpdates();
544
656
  process.exit(shouldFail ? 1 : 0);
545
657
  });
546
658
  // ── voiden-runner session ─────────────────────────────────────────────────────
@@ -765,13 +877,14 @@ pluginCmd
765
877
  ' voiden-runner plugin install apyhub-explorer\n')
766
878
  .option('--all', 'Install all core plugins (community plugins must be installed by name)')
767
879
  .action(async (names, opts) => {
880
+ const corePlugins = await getCorePlugins();
768
881
  const communityPlugins = await fetchCommunityPlugins();
769
882
  const targets = opts.all
770
- ? CORE_PLUGINS.map(p => p.name)
883
+ ? corePlugins.map(p => p.name)
771
884
  : names;
772
885
  if (targets.length === 0) {
773
886
  console.error(chalk.red('Specify plugin name(s) or use --all'));
774
- console.log(chalk.gray(' Core: ' + CORE_PLUGINS.map(p => p.name).join(', ')));
887
+ console.log(chalk.gray(' Core: ' + corePlugins.map(p => p.name).join(', ')));
775
888
  if (communityPlugins.length > 0) {
776
889
  console.log(chalk.gray(' Community (install by name): ' + communityPlugins.map(p => p.id).join(', ')));
777
890
  }
@@ -779,12 +892,29 @@ pluginCmd
779
892
  }
780
893
  let installedCount = 0;
781
894
  for (const name of targets) {
782
- const coreDef = findPlugin(name);
895
+ const coreDef = await findPlugin(name);
783
896
  const commDef = !coreDef ? findCommunityPlugin(name, communityPlugins) : undefined;
784
897
  if (!coreDef && !commDef) {
785
898
  console.log(chalk.yellow(` ⚠ Unknown plugin "${name}" — skipped`));
786
899
  continue;
787
900
  }
901
+ // Core plugins: only download if not already bundled in the package or cached
902
+ // from a previous install — `bundled: true` plugins should need no network call.
903
+ if (coreDef && !hasCoreRunner(name)) {
904
+ process.stdout.write(` ↓ Downloading runner for ${chalk.bold(name)} …`);
905
+ try {
906
+ const ok = await downloadCoreRunner(coreDef.name, coreDef.repo, coreDef.runnerAsset, coreDef.version, false);
907
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
908
+ if (!ok) {
909
+ console.log(chalk.red(` ✗ No "${coreDef.runnerAsset}" asset in release v${coreDef.version} for "${name}"`));
910
+ continue;
911
+ }
912
+ }
913
+ catch (err) {
914
+ process.stdout.write('\r' + chalk.red(` ✗ Failed to download runner for "${name}": ${err?.message ?? String(err)}\n`));
915
+ continue;
916
+ }
917
+ }
788
918
  // Community plugins: download runner.js from the GitHub release first
789
919
  if (commDef) {
790
920
  process.stdout.write(` ↓ Downloading runner for ${chalk.bold(name)} …`);
@@ -794,6 +924,7 @@ pluginCmd
794
924
  process.stdout.write('\r' + chalk.yellow(` ⚠ No runner.js in release for "${name}" — skipped\n`));
795
925
  continue;
796
926
  }
927
+ setPluginVersion(name, commDef.version);
797
928
  process.stdout.write('\r' + ' '.repeat(60) + '\r'); // clear the line
798
929
  }
799
930
  catch (err) {
@@ -802,7 +933,7 @@ pluginCmd
802
933
  }
803
934
  }
804
935
  const description = coreDef ? coreDef.description : commDef.description;
805
- const fresh = installPlugin(name);
936
+ const fresh = installPlugin(name, coreDef?.version ?? commDef?.version);
806
937
  if (fresh) {
807
938
  console.log(chalk.green(` ✓ Installed`) + chalk.bold(` ${name}`) + chalk.gray(` — ${description}`));
808
939
  installedCount++;
@@ -815,18 +946,111 @@ pluginCmd
815
946
  console.log();
816
947
  console.log(chalk.gray(` ${installedCount} plugin(s) installed. State saved to ~/.voiden/plugins.json`));
817
948
  }
949
+ await notifyPluginUpdates();
818
950
  });
819
- // voiden-runner plugin uninstall <name>
951
+ // voiden-runner plugin update [names...] --all
820
952
  pluginCmd
821
- .command('uninstall <name>')
822
- .description('Remove an installed plugin\n\n Example:\n voiden-runner plugin uninstall voiden-scripting\n')
823
- .action((name) => {
824
- const removed = uninstallPlugin(name);
825
- if (removed) {
826
- console.log(chalk.green(` ✓ Uninstalled`) + ` ${name}`);
953
+ .command('update [names...]')
954
+ .description('Update installed plugins to the latest registry version\n\n' +
955
+ ' --all updates every installed plugin that has a newer version available.\n\n' +
956
+ ' Examples:\n' +
957
+ ' voiden-runner plugin update --all\n' +
958
+ ' voiden-runner plugin update voiden-scripting\n' +
959
+ ' voiden-runner plugin update apyhub-explorer voiden-scripting\n')
960
+ .option('--all', 'Update every installed plugin that has a newer version available')
961
+ .action(async (names, opts) => {
962
+ const updates = await checkForPluginUpdates();
963
+ if (updates.length === 0) {
964
+ console.log(chalk.gray(' All installed plugins are up to date.'));
965
+ return;
827
966
  }
828
- else {
829
- console.log(chalk.yellow(` ⚠ Plugin "${name}" is not installed`));
967
+ const targets = opts.all ? updates.map(u => u.id) : names;
968
+ if (targets.length === 0) {
969
+ console.log(chalk.yellow(` ${updates.length} update${updates.length !== 1 ? 's' : ''} available — specify plugin name(s) or use --all:`));
970
+ printUpdateNotice(updates);
971
+ return;
972
+ }
973
+ const corePlugins = await getCorePlugins();
974
+ const communityPlugins = await fetchCommunityPlugins();
975
+ let updatedCount = 0;
976
+ for (const name of targets) {
977
+ const update = updates.find(u => u.id === name);
978
+ if (!update) {
979
+ const known = readStore().installedPlugins[name];
980
+ console.log(known
981
+ ? chalk.gray(` · ${name} is already up to date — skipped`)
982
+ : chalk.yellow(` ⚠ Plugin "${name}" is not installed — skipped`));
983
+ continue;
984
+ }
985
+ process.stdout.write(` ↓ Updating ${chalk.bold(name)} to v${update.latestVersion} …`);
986
+ try {
987
+ if (update.type === 'core') {
988
+ const def = corePlugins.find(p => p.name === name);
989
+ if (!def)
990
+ throw new Error('plugin no longer in core registry');
991
+ const ok = await downloadCoreRunner(def.name, def.repo, def.runnerAsset, def.version, false);
992
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
993
+ if (!ok) {
994
+ console.log(chalk.red(` ✗ No "${def.runnerAsset}" asset in release v${update.latestVersion} for "${name}"`));
995
+ continue;
996
+ }
997
+ }
998
+ else {
999
+ const def = communityPlugins.find(p => p.id === name);
1000
+ if (!def)
1001
+ throw new Error('plugin no longer in community registry');
1002
+ const result = await installCommunityRunner(def);
1003
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
1004
+ if (result === 'no-runner') {
1005
+ console.log(chalk.red(` ✗ No runner.js in release v${update.latestVersion} for "${name}"`));
1006
+ continue;
1007
+ }
1008
+ setPluginVersion(name, update.latestVersion);
1009
+ }
1010
+ console.log(chalk.green(` ✓ Updated`) + chalk.bold(` ${name}`) +
1011
+ chalk.gray(` ${update.installedVersion} → ${update.latestVersion}`));
1012
+ updatedCount++;
1013
+ }
1014
+ catch (err) {
1015
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
1016
+ console.log(chalk.red(` ✗ Failed to update "${name}": ${err?.message ?? String(err)}`));
1017
+ }
1018
+ }
1019
+ if (updatedCount > 0) {
1020
+ console.log();
1021
+ console.log(chalk.gray(` ${updatedCount} plugin(s) updated.`));
1022
+ }
1023
+ });
1024
+ // voiden-runner plugin uninstall [names...] --all
1025
+ pluginCmd
1026
+ .command('uninstall [names...]')
1027
+ .description('Remove one or more installed plugins, or all installed plugins\n\n' +
1028
+ ' Examples:\n' +
1029
+ ' voiden-runner plugin uninstall voiden-scripting\n' +
1030
+ ' voiden-runner plugin uninstall apyhub-explorer voiden-scripting\n' +
1031
+ ' voiden-runner plugin uninstall --all\n')
1032
+ .option('--all', 'Uninstall all installed plugins (core and community)')
1033
+ .action((names, opts) => {
1034
+ const targets = opts.all ? Object.keys(readStore().installedPlugins) : names;
1035
+ if (targets.length === 0) {
1036
+ console.error(chalk.red(' Specify plugin name(s) or use --all'));
1037
+ process.exit(1);
1038
+ return;
1039
+ }
1040
+ let removedCount = 0;
1041
+ for (const name of targets) {
1042
+ const removed = uninstallPlugin(name);
1043
+ if (removed) {
1044
+ console.log(chalk.green(` ✓ Uninstalled`) + ` ${name}`);
1045
+ removedCount++;
1046
+ }
1047
+ else {
1048
+ console.log(chalk.yellow(` ⚠ Plugin "${name}" is not installed`));
1049
+ }
1050
+ }
1051
+ if (removedCount > 1) {
1052
+ console.log();
1053
+ console.log(chalk.gray(` ${removedCount} plugin(s) uninstalled.`));
830
1054
  }
831
1055
  });
832
1056
  // voiden-runner plugin enable [name] --all
@@ -879,31 +1103,34 @@ pluginCmd
879
1103
  ' voiden-runner plugin disable voiden-scripting\n' +
880
1104
  ' voiden-runner plugin disable --all\n')
881
1105
  .option('--all', 'Disable all plugins (core and community)')
882
- .action((name, opts) => {
1106
+ .action(async (name, opts) => {
1107
+ const corePlugins = await getCorePlugins();
883
1108
  if (opts.all) {
884
1109
  // Disable all core plugins
885
- for (const def of CORE_PLUGINS) {
1110
+ for (const def of corePlugins) {
886
1111
  setPluginEnabled(def.name, false);
887
1112
  console.log(chalk.yellow(` · Disabled`) + ` ${def.name}`);
888
1113
  }
889
1114
  // Disable all installed community plugins
890
1115
  const store = readStore();
891
- const communityNames = Object.keys(store.installedPlugins).filter(n => !findPlugin(n));
1116
+ const coreNames = new Set(corePlugins.map(p => p.name));
1117
+ const communityNames = Object.keys(store.installedPlugins).filter(n => !coreNames.has(n));
892
1118
  for (const n of communityNames) {
893
1119
  setPluginEnabled(n, false);
894
1120
  console.log(chalk.yellow(` · Disabled`) + ` ${n}`);
895
1121
  }
896
- const total = CORE_PLUGINS.length + communityNames.length;
1122
+ const total = corePlugins.length + communityNames.length;
897
1123
  console.log(chalk.gray(` ${total} plugin(s) disabled.`));
898
1124
  return;
899
1125
  }
900
1126
  if (!name) {
901
1127
  console.error(chalk.red(' Specify a plugin name or use --all'));
902
1128
  process.exit(1);
1129
+ return;
903
1130
  }
904
1131
  setPluginEnabled(name, false);
905
1132
  console.log(chalk.yellow(` · Disabled`) + ` ${name}`);
906
- if (findPlugin(name)) {
1133
+ if (await findPlugin(name)) {
907
1134
  console.log(chalk.gray(` Core plugin disabled. Re-enable with: voiden-runner plugin enable ${name}`));
908
1135
  }
909
1136
  });
@@ -913,17 +1140,26 @@ pluginCmd
913
1140
  .description('List all available and installed plugins')
914
1141
  .action(async () => {
915
1142
  const store = readStore();
1143
+ const corePlugins = await getCorePlugins();
916
1144
  const communityPlugins = await fetchCommunityPlugins();
1145
+ // Per-plugin "update available" badge — compares the version recorded at
1146
+ // install/download time against the registry's current version.
1147
+ const updateBadge = (id, latestVersion) => {
1148
+ const installedVersion = store.installedPlugins[id]?.version;
1149
+ if (!installedVersion || installedVersion === latestVersion)
1150
+ return '';
1151
+ return chalk.yellow(` ⬆ v${latestVersion} available`);
1152
+ };
917
1153
  console.log();
918
- console.log(chalk.bold(' Core plugins') + chalk.gray(' (@voiden/core-extensions)'));
1154
+ console.log(chalk.bold(' Core plugins') + chalk.gray(' (from VoidenHQ/plugin-registry)'));
919
1155
  console.log(DIVIDER);
920
- for (const def of CORE_PLUGINS) {
1156
+ for (const def of corePlugins) {
921
1157
  const record = store.installedPlugins[def.name];
922
1158
  const isDisabled = record !== undefined && !record.enabled;
923
1159
  const statusBadge = isDisabled
924
1160
  ? chalk.yellow(' · disabled')
925
1161
  : chalk.green(' ✓ enabled');
926
- console.log(` ${chalk.bold(def.name.padEnd(24))}${statusBadge}`);
1162
+ console.log(` ${chalk.bold(def.name.padEnd(24))}${statusBadge}${updateBadge(def.name, def.version)}`);
927
1163
  console.log(chalk.gray(` ${def.description}`));
928
1164
  }
929
1165
  // ── Community plugins ───────────────────────────────────────────────────
@@ -933,7 +1169,7 @@ pluginCmd
933
1169
  console.log(DIVIDER);
934
1170
  }
935
1171
  else {
936
- console.log(chalk.bold(' Community plugins') + chalk.gray(' (github.com/VoidenHQ/plugins)'));
1172
+ console.log(chalk.bold(' Community plugins') + chalk.gray(' (from VoidenHQ/plugin-registry)'));
937
1173
  console.log(DIVIDER);
938
1174
  for (const def of communityPlugins) {
939
1175
  const installed = store.installedPlugins[def.id];
@@ -948,14 +1184,14 @@ pluginCmd
948
1184
  statusBadge = chalk.yellow(' · disabled');
949
1185
  }
950
1186
  const runnerBadge = hasCommunityRunner(def.id) ? '' : chalk.gray(' [no runner]');
951
- console.log(` ${chalk.bold(def.id.padEnd(24))}${statusBadge}${runnerBadge}` +
1187
+ console.log(` ${chalk.bold(def.id.padEnd(24))}${statusBadge}${runnerBadge}${updateBadge(def.id, def.version)}` +
952
1188
  chalk.gray(` v${def.version}`) +
953
1189
  chalk.gray(` by ${def.author}`));
954
1190
  console.log(chalk.gray(` ${def.description}`));
955
1191
  }
956
1192
  }
957
1193
  const knownIds = new Set([
958
- ...CORE_PLUGINS.map(p => p.name),
1194
+ ...corePlugins.map(p => p.name),
959
1195
  ...communityPlugins.map(p => p.id),
960
1196
  ]);
961
1197
  const extras = getAllInstalledPlugins().filter(p => !knownIds.has(p.name));
@@ -968,6 +1204,11 @@ pluginCmd
968
1204
  console.log(` ${chalk.bold(p.name.padEnd(24))}${badge}`);
969
1205
  }
970
1206
  }
1207
+ const updates = await checkForPluginUpdates();
1208
+ if (updates.length > 0) {
1209
+ console.log();
1210
+ console.log(chalk.gray(` Run: voiden-runner plugin update --all (${updates.length} update${updates.length !== 1 ? 's' : ''} available)`));
1211
+ }
971
1212
  console.log();
972
1213
  });
973
1214
  program.parse();