@voiden/runner 2.1.1 → 2.2.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.
- package/CHANGELOG.md +6 -0
- package/bundled-runners/versions.json +6 -6
- package/bundled-runners/voiden-faker-runner.js +6 -13
- package/dist/discovery.d.ts +11 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +54 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.js +263 -111
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +19 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +19 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcpInstall.d.ts +48 -0
- package/dist/mcpInstall.d.ts.map +1 -0
- package/dist/mcpInstall.js +194 -0
- package/dist/mcpInstall.js.map +1 -0
- package/dist/plugins/registry.d.ts +8 -1
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js +13 -2
- package/dist/plugins/registry.js.map +1 -1
- package/dist/plugins/store.d.ts +28 -1
- package/dist/plugins/store.d.ts.map +1 -1
- package/dist/plugins/store.js +46 -9
- package/dist/plugins/store.js.map +1 -1
- package/dist/plugins/versionInfo.d.ts +15 -0
- package/dist/plugins/versionInfo.d.ts.map +1 -0
- package/dist/plugins/versionInfo.js +18 -0
- package/dist/plugins/versionInfo.js.map +1 -0
- package/dist/resultBlock.d.ts +31 -0
- package/dist/resultBlock.d.ts.map +1 -0
- package/dist/resultBlock.js +121 -0
- package/dist/resultBlock.js.map +1 -0
- package/dist/runner.d.ts +11 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +61 -2
- package/dist/runner.js.map +1 -1
- package/dist/runtimeVars.js +3 -3
- package/dist/runtimeVars.js.map +1 -1
- package/dist/skillContent.d.ts +8 -0
- package/dist/skillContent.d.ts.map +1 -0
- package/dist/skillContent.js +42 -0
- package/dist/skillContent.js.map +1 -0
- package/package.json +10 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
|
-
import { readFileSync, existsSync,
|
|
4
|
-
import { resolve, basename, join, dirname } from 'path';
|
|
3
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
4
|
+
import { resolve, relative, basename, join, dirname } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { readdir } from 'fs/promises';
|
|
7
6
|
import chalk from 'chalk';
|
|
8
7
|
import { runVoidFile } from './runner.js';
|
|
8
|
+
import { resolveFiles } from './discovery.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';
|
|
@@ -14,8 +14,31 @@ import { downloadCoreRunner } from './plugins/loader.js';
|
|
|
14
14
|
import { fetchCommunityPlugins, findCommunityPlugin, hasCommunityRunner, installCommunityRunner, } from './plugins/community.js';
|
|
15
15
|
import { installPlugin, uninstallPlugin, setPluginEnabled, setPluginVersion, getAllInstalledPlugins, readStore, STORE_DIR, } from './plugins/store.js';
|
|
16
16
|
import { checkForPluginUpdates } from './plugins/updateCheck.js';
|
|
17
|
+
import { getInstalledPluginInfo } from './plugins/versionInfo.js';
|
|
18
|
+
import { parseVoidFile } from './parser.js';
|
|
19
|
+
import { classifyBlockVersion } from '@voiden/executors';
|
|
20
|
+
import { installMcpIntegration, uninstallMcpIntegration, getMcpStatus } from './mcpInstall.js';
|
|
21
|
+
import { RUNNER_SKILL_MARKDOWN } from './skillContent.js';
|
|
17
22
|
import { appendSessionResults, loadSessionResults, clearSession, } from './session.js';
|
|
18
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Exit codes — a stable, documented contract CI pipelines can branch on.
|
|
25
|
+
//
|
|
26
|
+
// 0 success — all requests passed
|
|
27
|
+
// 1 one or more requests failed (assertions/errors), or --bail /
|
|
28
|
+
// --fail-on-error triggered — unchanged from prior releases
|
|
29
|
+
// 2 the runner could not execute the run at all: bad CLI args/flags,
|
|
30
|
+
// missing files, missing plugins, invalid env — a pipeline/config
|
|
31
|
+
// problem, not an API failure
|
|
32
|
+
//
|
|
33
|
+
// See CHANGELOG.md and docs.voiden.md/docs/developer-tools/voiden-runner/ci-cd
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
const EXIT_SUCCESS = 0;
|
|
36
|
+
const EXIT_RUN_FAILURE = 1;
|
|
37
|
+
const EXIT_USAGE_ERROR = 2;
|
|
38
|
+
/** JSON output schema version — bump whenever a field is renamed, removed, or
|
|
39
|
+
* reinterpreted (adding a field is not a breaking change and does not need a bump). */
|
|
40
|
+
const JSON_SCHEMA_VERSION = '1';
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
42
|
// Helpers
|
|
20
43
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
44
|
function loadEnvFile(envPath) {
|
|
@@ -49,50 +72,6 @@ function formatDuration(ms) {
|
|
|
49
72
|
return `${ms}ms`;
|
|
50
73
|
return `${(ms / 1000).toFixed(2)}s`;
|
|
51
74
|
}
|
|
52
|
-
/** Recursively collect all .void files under a directory. */
|
|
53
|
-
async function collectVoidFiles(inputPath) {
|
|
54
|
-
const abs = resolve(inputPath);
|
|
55
|
-
if (!existsSync(abs))
|
|
56
|
-
return [];
|
|
57
|
-
const stat = statSync(abs);
|
|
58
|
-
if (stat.isFile()) {
|
|
59
|
-
return abs.endsWith('.void') ? [abs] : [];
|
|
60
|
-
}
|
|
61
|
-
if (stat.isDirectory()) {
|
|
62
|
-
const entries = await readdir(abs, { withFileTypes: true });
|
|
63
|
-
const results = [];
|
|
64
|
-
for (const entry of entries) {
|
|
65
|
-
const full = resolve(abs, entry.name);
|
|
66
|
-
if (entry.isDirectory()) {
|
|
67
|
-
results.push(...(await collectVoidFiles(full)));
|
|
68
|
-
}
|
|
69
|
-
else if (entry.isFile() && entry.name.endsWith('.void')) {
|
|
70
|
-
results.push(full);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return results;
|
|
74
|
-
}
|
|
75
|
-
return [];
|
|
76
|
-
}
|
|
77
|
-
/** Expand a list of paths/globs into resolved .void file paths. */
|
|
78
|
-
async function resolveFiles(patterns) {
|
|
79
|
-
const resolved = [];
|
|
80
|
-
for (const pattern of patterns) {
|
|
81
|
-
if (pattern.includes('*')) {
|
|
82
|
-
const dir = resolve(pattern.replace(/\/?\*.*$/, '') || '.');
|
|
83
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
84
|
-
for (const entry of entries) {
|
|
85
|
-
if (entry.isFile() && entry.name.endsWith('.void')) {
|
|
86
|
-
resolved.push(resolve(dir, entry.name));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
resolved.push(...(await collectVoidFiles(pattern)));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return resolved;
|
|
95
|
-
}
|
|
96
75
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
76
|
// Spinner
|
|
98
77
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -266,18 +245,26 @@ function printRunSummary(results, totalMs) {
|
|
|
266
245
|
console.log(DIVIDER);
|
|
267
246
|
console.log();
|
|
268
247
|
}
|
|
269
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Builds the `--json` / `--output-json` payload shape shared by `run` and
|
|
250
|
+
* `report generate`. `schemaVersion` is the stable contract external tooling
|
|
251
|
+
* codes against — see the exit-codes comment above for the versioning rule.
|
|
252
|
+
*/
|
|
253
|
+
function buildJsonReport(results, extra = {}) {
|
|
270
254
|
const passed = results.filter(r => r.result.success).length;
|
|
271
|
-
|
|
255
|
+
return {
|
|
256
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
272
257
|
summary: {
|
|
273
258
|
total: results.length,
|
|
274
259
|
passed,
|
|
275
260
|
failed: results.length - passed,
|
|
276
|
-
|
|
277
|
-
activePlugins,
|
|
261
|
+
...extra,
|
|
278
262
|
},
|
|
279
263
|
requests: results.map(r => ({ file: r.file, ...r.result })),
|
|
280
264
|
};
|
|
265
|
+
}
|
|
266
|
+
function printRunSummaryJson(results, totalMs, activePlugins) {
|
|
267
|
+
const output = buildJsonReport(results, { totalDurationMs: totalMs, activePlugins });
|
|
281
268
|
console.log(JSON.stringify(output, null, 2));
|
|
282
269
|
}
|
|
283
270
|
/** Split CHANGELOG.md into per-version entries on "## " headers (newest first, matching file order). */
|
|
@@ -346,6 +333,65 @@ async function notifyPluginUpdates() {
|
|
|
346
333
|
// Informational only — ignore failures (e.g. offline)
|
|
347
334
|
}
|
|
348
335
|
}
|
|
336
|
+
function scanProjectRequirements(files, cwd) {
|
|
337
|
+
// pluginId → version → files that declared it
|
|
338
|
+
const usages = new Map();
|
|
339
|
+
for (const file of files) {
|
|
340
|
+
let content;
|
|
341
|
+
try {
|
|
342
|
+
content = readFileSync(file, 'utf-8');
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
for (const block of parseVoidFile(content)) {
|
|
348
|
+
const pluginId = block.attrs?.pluginId;
|
|
349
|
+
const pluginVersion = block.attrs?.pluginVersion;
|
|
350
|
+
if (!pluginId || !pluginVersion)
|
|
351
|
+
continue;
|
|
352
|
+
if (!usages.has(pluginId))
|
|
353
|
+
usages.set(pluginId, new Map());
|
|
354
|
+
const versions = usages.get(pluginId);
|
|
355
|
+
if (!versions.has(pluginVersion))
|
|
356
|
+
versions.set(pluginVersion, new Set());
|
|
357
|
+
versions.get(pluginVersion).add(relative(cwd, file));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const issues = [];
|
|
361
|
+
for (const [pluginId, versions] of usages) {
|
|
362
|
+
for (const [version, fileSet] of versions) {
|
|
363
|
+
const installed = getInstalledPluginInfo(pluginId);
|
|
364
|
+
const status = classifyBlockVersion({ pluginId, pluginVersion: version, blockType: '' }, installed);
|
|
365
|
+
if (status === 'ok')
|
|
366
|
+
continue;
|
|
367
|
+
issues.push({ pluginId, requiredVersion: version, installedVersion: installed?.version, status, files: [...fileSet] });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return issues;
|
|
371
|
+
}
|
|
372
|
+
function printProjectStatus(issues) {
|
|
373
|
+
if (issues.length === 0)
|
|
374
|
+
return;
|
|
375
|
+
console.log();
|
|
376
|
+
console.log(chalk.yellow(` ⚠ ${issues.length} plugin${issues.length !== 1 ? 's' : ''} ${issues.length !== 1 ? "don't" : "doesn't"} match what this project needs`));
|
|
377
|
+
for (const issue of issues) {
|
|
378
|
+
const have = issue.status === 'not-installed' ? 'not installed'
|
|
379
|
+
: issue.status === 'disabled' ? 'installed but disabled'
|
|
380
|
+
: `v${issue.installedVersion} installed`;
|
|
381
|
+
console.log(chalk.gray(` ${chalk.bold(issue.pluginId.padEnd(24))} requires v${issue.requiredVersion} — ${have}`));
|
|
382
|
+
console.log(chalk.gray(` used in: ${issue.files.join(', ')}`));
|
|
383
|
+
}
|
|
384
|
+
console.log(chalk.gray(` Run: voiden-runner plugin install <name>@<version>`));
|
|
385
|
+
}
|
|
386
|
+
/** Best-effort — never throws, never blocks command output on failure. */
|
|
387
|
+
function notifyProjectStatus(files, cwd) {
|
|
388
|
+
try {
|
|
389
|
+
printProjectStatus(scanProjectRequirements(files, cwd));
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// Informational only
|
|
393
|
+
}
|
|
394
|
+
}
|
|
349
395
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
350
396
|
// CLI
|
|
351
397
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -368,7 +414,7 @@ program
|
|
|
368
414
|
const changelogPath = resolve(join(dirname(fileURLToPath(import.meta.url)), '../CHANGELOG.md'));
|
|
369
415
|
if (!existsSync(changelogPath)) {
|
|
370
416
|
console.error(chalk.red(' ✗ No CHANGELOG.md found for this install.'));
|
|
371
|
-
process.exit(
|
|
417
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
372
418
|
}
|
|
373
419
|
const entries = parseChangelog(readFileSync(changelogPath, 'utf-8'));
|
|
374
420
|
if (entries.length === 0) {
|
|
@@ -382,7 +428,7 @@ program
|
|
|
382
428
|
if (!match) {
|
|
383
429
|
console.error(chalk.red(` ✗ No changelog entry found for version "${version}".`));
|
|
384
430
|
console.log(chalk.gray(` Available: ${entries.map(e => e.version).join(', ')}`));
|
|
385
|
-
process.exit(
|
|
431
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
386
432
|
}
|
|
387
433
|
toShow = [match];
|
|
388
434
|
}
|
|
@@ -442,14 +488,14 @@ program
|
|
|
442
488
|
const envPath = resolve(opts.env);
|
|
443
489
|
if (!existsSync(envPath)) {
|
|
444
490
|
console.error(chalk.red(`Env file not found: ${envPath}`));
|
|
445
|
-
process.exit(
|
|
491
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
446
492
|
}
|
|
447
493
|
try {
|
|
448
494
|
Object.assign(env, loadEnvFile(envPath));
|
|
449
495
|
}
|
|
450
496
|
catch (err) {
|
|
451
497
|
console.error(chalk.red(` ✗ ${err.message}`));
|
|
452
|
-
process.exit(
|
|
498
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
453
499
|
}
|
|
454
500
|
}
|
|
455
501
|
// 2. Individual --env-var overrides
|
|
@@ -458,13 +504,13 @@ program
|
|
|
458
504
|
const eq = pair.indexOf('=');
|
|
459
505
|
if (eq === -1) {
|
|
460
506
|
console.error(chalk.red(` ✗ Invalid --env-var format: "${pair}" (expected key=value)`));
|
|
461
|
-
process.exit(
|
|
507
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
462
508
|
}
|
|
463
509
|
const key = pair.slice(0, eq).trim();
|
|
464
510
|
const val = pair.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
465
511
|
if (!key) {
|
|
466
512
|
console.error(chalk.red(` ✗ Invalid --env-var format: "${pair}" (key cannot be empty)`));
|
|
467
|
-
process.exit(
|
|
513
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
468
514
|
}
|
|
469
515
|
env[key] = val;
|
|
470
516
|
}
|
|
@@ -472,7 +518,7 @@ program
|
|
|
472
518
|
const resolvedFiles = await resolveFiles(paths);
|
|
473
519
|
if (resolvedFiles.length === 0) {
|
|
474
520
|
console.error(chalk.red('No .void files found at the given path(s)'));
|
|
475
|
-
process.exit(
|
|
521
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
476
522
|
}
|
|
477
523
|
// --stop-on-failure is a CI-friendly alias for --bail
|
|
478
524
|
const stopOnFailure = opts.bail || opts.stopOnFailure;
|
|
@@ -490,11 +536,11 @@ program
|
|
|
490
536
|
if (opts.mail || opts.mailTo) {
|
|
491
537
|
if (!mailTo) {
|
|
492
538
|
console.error(chalk.red(' ✗ Mail error: no recipient found. Please provide --mail-to or set VOIDEN_MAIL_TO.'));
|
|
493
|
-
process.exit(
|
|
539
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
494
540
|
}
|
|
495
541
|
if (!smtpHost) {
|
|
496
542
|
console.error(chalk.red(' ✗ Mail keys are missing. Please provide SMTP configuration (VOIDEN_SMTP_HOST).'));
|
|
497
|
-
process.exit(
|
|
543
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
498
544
|
}
|
|
499
545
|
}
|
|
500
546
|
const runStart = Date.now();
|
|
@@ -600,16 +646,7 @@ program
|
|
|
600
646
|
// ── Output JSON to file (before mail so it can be attached) ──────────────
|
|
601
647
|
let savedJsonPath;
|
|
602
648
|
if (opts.outputJson) {
|
|
603
|
-
const jsonData = {
|
|
604
|
-
summary: {
|
|
605
|
-
total: allResults.length,
|
|
606
|
-
passed: allResults.filter(r => r.result.success).length,
|
|
607
|
-
failed: allResults.filter(r => !r.result.success).length,
|
|
608
|
-
totalDurationMs: totalMs,
|
|
609
|
-
activePlugins,
|
|
610
|
-
},
|
|
611
|
-
requests: allResults.map(r => ({ file: r.file, ...r.result })),
|
|
612
|
-
};
|
|
649
|
+
const jsonData = buildJsonReport(allResults, { totalDurationMs: totalMs, activePlugins });
|
|
613
650
|
try {
|
|
614
651
|
mkdirSync(dirname(opts.outputJson), { recursive: true });
|
|
615
652
|
writeFileSync(opts.outputJson, JSON.stringify(jsonData, null, 2) + '\n', 'utf-8');
|
|
@@ -650,10 +687,12 @@ program
|
|
|
650
687
|
console.log(chalk.gray(' (use this exit code in your shell script to abort on failure)'));
|
|
651
688
|
console.log();
|
|
652
689
|
}
|
|
653
|
-
// Surface plugin update notices — skipped in --json mode so output stays machine-readable
|
|
654
|
-
if (!opts.json)
|
|
690
|
+
// Surface plugin update / project-requirements notices — skipped in --json mode so output stays machine-readable
|
|
691
|
+
if (!opts.json) {
|
|
655
692
|
await notifyPluginUpdates();
|
|
656
|
-
|
|
693
|
+
notifyProjectStatus(resolvedFiles, process.cwd());
|
|
694
|
+
}
|
|
695
|
+
process.exit(shouldFail ? EXIT_RUN_FAILURE : EXIT_SUCCESS);
|
|
657
696
|
});
|
|
658
697
|
// ── voiden-runner session ─────────────────────────────────────────────────────
|
|
659
698
|
const sessionCmd = program
|
|
@@ -768,7 +807,7 @@ reportCmd
|
|
|
768
807
|
const results = loadSessionResults();
|
|
769
808
|
if (results.length === 0) {
|
|
770
809
|
console.error(chalk.red(' ✗ No results found in session. Run some .void files first.'));
|
|
771
|
-
process.exit(
|
|
810
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
772
811
|
}
|
|
773
812
|
// Load optional .env for report SMTP settings
|
|
774
813
|
const env = { ...process.env };
|
|
@@ -787,12 +826,12 @@ reportCmd
|
|
|
787
826
|
if (opts.mail || opts.mailTo) {
|
|
788
827
|
if (!mailTo) {
|
|
789
828
|
console.error(chalk.red(' ✗ Mail error: no recipient found. Please provide --mail-to or set VOIDEN_MAIL_TO.'));
|
|
790
|
-
process.exit(
|
|
829
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
791
830
|
}
|
|
792
831
|
const smtpHost = opts.smtpHost || env.VOIDEN_SMTP_HOST;
|
|
793
832
|
if (!smtpHost) {
|
|
794
833
|
console.error(chalk.red(' ✗ Mail keys are missing. Please provide SMTP configuration (VOIDEN_SMTP_HOST).'));
|
|
795
|
-
process.exit(
|
|
834
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
796
835
|
}
|
|
797
836
|
}
|
|
798
837
|
if (!opts.csv && !opts.outputJson && !mailTo) {
|
|
@@ -811,14 +850,7 @@ reportCmd
|
|
|
811
850
|
}
|
|
812
851
|
let savedJsonPath;
|
|
813
852
|
if (opts.outputJson) {
|
|
814
|
-
const jsonData =
|
|
815
|
-
summary: {
|
|
816
|
-
total: results.length,
|
|
817
|
-
passed: results.filter(r => r.result.success).length,
|
|
818
|
-
failed: results.filter(r => !r.result.success).length,
|
|
819
|
-
},
|
|
820
|
-
requests: results.map(r => ({ file: r.file, ...r.result })),
|
|
821
|
-
};
|
|
853
|
+
const jsonData = buildJsonReport(results);
|
|
822
854
|
try {
|
|
823
855
|
mkdirSync(dirname(opts.outputJson), { recursive: true });
|
|
824
856
|
writeFileSync(opts.outputJson, JSON.stringify(jsonData, null, 2) + '\n', 'utf-8');
|
|
@@ -839,7 +871,7 @@ reportCmd
|
|
|
839
871
|
if (!smtpHost) {
|
|
840
872
|
console.error(chalk.red(' ✗ SMTP configuration required for email reports.'));
|
|
841
873
|
console.log(chalk.gray(' Set VOIDEN_SMTP_HOST in your environment or use --smtp-host.'));
|
|
842
|
-
process.exit(
|
|
874
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
843
875
|
}
|
|
844
876
|
console.log(chalk.gray(` ↑ Sending session report to ${mailTo} …`));
|
|
845
877
|
try {
|
|
@@ -862,6 +894,14 @@ reportCmd
|
|
|
862
894
|
}
|
|
863
895
|
}
|
|
864
896
|
});
|
|
897
|
+
// Splits a `name` or `name@version` CLI arg. Plugin ids are plain slugs (no
|
|
898
|
+
// leading `@`), so splitting on the first `@` is unambiguous.
|
|
899
|
+
function parsePluginTarget(raw) {
|
|
900
|
+
const at = raw.indexOf('@');
|
|
901
|
+
if (at <= 0)
|
|
902
|
+
return { name: raw };
|
|
903
|
+
return { name: raw.slice(0, at), pinVersion: raw.slice(at + 1) };
|
|
904
|
+
}
|
|
865
905
|
// ── voiden-runner plugin ──────────────────────────────────────────────────────
|
|
866
906
|
const pluginCmd = program
|
|
867
907
|
.command('plugin')
|
|
@@ -870,37 +910,44 @@ const pluginCmd = program
|
|
|
870
910
|
pluginCmd
|
|
871
911
|
.command('install [names...]')
|
|
872
912
|
.description('Install one or more plugins, or all core plugins\n\n' +
|
|
873
|
-
' --all installs all core plugins only. Community plugins must be installed by name.\n
|
|
913
|
+
' --all installs all core plugins only. Community plugins must be installed by name.\n' +
|
|
914
|
+
' Pin an exact version with name@version (e.g. after `voiden-runner lock`, or to\n' +
|
|
915
|
+
' match a "Block ... requires plugin X vY" error).\n\n' +
|
|
874
916
|
' Examples:\n' +
|
|
875
917
|
' voiden-runner plugin install --all\n' +
|
|
876
918
|
' voiden-runner plugin install voiden-scripting\n' +
|
|
877
|
-
' voiden-runner plugin install
|
|
919
|
+
' voiden-runner plugin install voiden-rest-api@1.4.7\n')
|
|
878
920
|
.option('--all', 'Install all core plugins (community plugins must be installed by name)')
|
|
879
|
-
.action(async (
|
|
921
|
+
.action(async (rawNames, opts) => {
|
|
880
922
|
const corePlugins = await getCorePlugins();
|
|
881
923
|
const communityPlugins = await fetchCommunityPlugins();
|
|
882
924
|
const targets = opts.all
|
|
883
|
-
? corePlugins.map(p => p.name)
|
|
884
|
-
:
|
|
925
|
+
? corePlugins.map(p => ({ name: p.name }))
|
|
926
|
+
: rawNames.map(parsePluginTarget);
|
|
885
927
|
if (targets.length === 0) {
|
|
886
928
|
console.error(chalk.red('Specify plugin name(s) or use --all'));
|
|
887
929
|
console.log(chalk.gray(' Core: ' + corePlugins.map(p => p.name).join(', ')));
|
|
888
930
|
if (communityPlugins.length > 0) {
|
|
889
931
|
console.log(chalk.gray(' Community (install by name): ' + communityPlugins.map(p => p.id).join(', ')));
|
|
890
932
|
}
|
|
891
|
-
process.exit(
|
|
933
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
892
934
|
}
|
|
893
935
|
let installedCount = 0;
|
|
894
|
-
for (const name of targets) {
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
if (!
|
|
936
|
+
for (const { name, pinVersion } of targets) {
|
|
937
|
+
const foundCoreDef = await findPlugin(name);
|
|
938
|
+
const foundCommDef = !foundCoreDef ? findCommunityPlugin(name, communityPlugins) : undefined;
|
|
939
|
+
if (!foundCoreDef && !foundCommDef) {
|
|
898
940
|
console.log(chalk.yellow(` ⚠ Unknown plugin "${name}" — skipped`));
|
|
899
941
|
continue;
|
|
900
942
|
}
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
|
|
943
|
+
// A pinned version overrides the registry's "latest" default — this is what
|
|
944
|
+
// makes the fix-it command in version-mismatch errors ("plugin install x@y")
|
|
945
|
+
// actually able to install the exact version a file declares.
|
|
946
|
+
const coreDef = foundCoreDef && pinVersion ? { ...foundCoreDef, version: pinVersion } : foundCoreDef;
|
|
947
|
+
const commDef = foundCommDef && pinVersion ? { ...foundCommDef, version: pinVersion } : foundCommDef;
|
|
948
|
+
// Core plugins: only download if not already bundled/cached — unless a
|
|
949
|
+
// specific version was pinned, in which case always fetch that version.
|
|
950
|
+
if (coreDef && (pinVersion || !hasCoreRunner(name))) {
|
|
904
951
|
process.stdout.write(` ↓ Downloading runner for ${chalk.bold(name)} …`);
|
|
905
952
|
try {
|
|
906
953
|
const ok = await downloadCoreRunner(coreDef.name, coreDef.repo, coreDef.runnerAsset, coreDef.version, false);
|
|
@@ -938,6 +985,13 @@ pluginCmd
|
|
|
938
985
|
console.log(chalk.green(` ✓ Installed`) + chalk.bold(` ${name}`) + chalk.gray(` — ${description}`));
|
|
939
986
|
installedCount++;
|
|
940
987
|
}
|
|
988
|
+
else if (pinVersion) {
|
|
989
|
+
// Re-running install with an explicit pin (e.g. to fix a version-mismatch)
|
|
990
|
+
// should still record the newly-downloaded version even if already "installed".
|
|
991
|
+
setPluginVersion(name, coreDef?.version ?? commDef?.version ?? pinVersion);
|
|
992
|
+
console.log(chalk.green(` ✓ Installed`) + chalk.bold(` ${name}@${pinVersion}`));
|
|
993
|
+
installedCount++;
|
|
994
|
+
}
|
|
941
995
|
else {
|
|
942
996
|
console.log(chalk.gray(` · Already installed`) + ` ${name}`);
|
|
943
997
|
}
|
|
@@ -1030,16 +1084,24 @@ pluginCmd
|
|
|
1030
1084
|
' voiden-runner plugin uninstall apyhub-explorer voiden-scripting\n' +
|
|
1031
1085
|
' voiden-runner plugin uninstall --all\n')
|
|
1032
1086
|
.option('--all', 'Uninstall all installed plugins (core and community)')
|
|
1033
|
-
.action((names, opts) => {
|
|
1034
|
-
|
|
1087
|
+
.action(async (names, opts) => {
|
|
1088
|
+
// Core plugins are bundled and enabled by default — they may never have
|
|
1089
|
+
// an explicit store record even though they're clearly active, so --all
|
|
1090
|
+
// (and named uninstalls of a bundled plugin) must include the full core
|
|
1091
|
+
// registry, not just names that already happen to have a store record.
|
|
1092
|
+
const corePlugins = await getCorePlugins();
|
|
1093
|
+
const coreNames = new Set(corePlugins.map(p => p.name));
|
|
1094
|
+
const targets = opts.all
|
|
1095
|
+
? [...new Set([...coreNames, ...getAllInstalledPlugins().map(p => p.name)])]
|
|
1096
|
+
: names;
|
|
1035
1097
|
if (targets.length === 0) {
|
|
1036
1098
|
console.error(chalk.red(' Specify plugin name(s) or use --all'));
|
|
1037
|
-
process.exit(
|
|
1099
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1038
1100
|
return;
|
|
1039
1101
|
}
|
|
1040
1102
|
let removedCount = 0;
|
|
1041
1103
|
for (const name of targets) {
|
|
1042
|
-
const removed = uninstallPlugin(name);
|
|
1104
|
+
const removed = uninstallPlugin(name, coreNames.has(name));
|
|
1043
1105
|
if (removed) {
|
|
1044
1106
|
console.log(chalk.green(` ✓ Uninstalled`) + ` ${name}`);
|
|
1045
1107
|
removedCount++;
|
|
@@ -1064,9 +1126,11 @@ pluginCmd
|
|
|
1064
1126
|
.action(async (name, opts) => {
|
|
1065
1127
|
if (opts.all) {
|
|
1066
1128
|
const store = readStore();
|
|
1067
|
-
// Re-enable all explicitly disabled plugins (core + community)
|
|
1129
|
+
// Re-enable all explicitly disabled plugins (core + community) — but not
|
|
1130
|
+
// uninstalled ones; bringing those back requires an explicit `plugin
|
|
1131
|
+
// install`, not a blanket --all enable.
|
|
1068
1132
|
const disabled = Object.entries(store.installedPlugins)
|
|
1069
|
-
.filter(([, r]) => !r.enabled)
|
|
1133
|
+
.filter(([, r]) => !r.enabled && !r.uninstalled)
|
|
1070
1134
|
.map(([n]) => n);
|
|
1071
1135
|
// Also ensure all core plugins that were never in the store are treated as enabled (default)
|
|
1072
1136
|
const disabledCoreNotInStore = [];
|
|
@@ -1083,14 +1147,14 @@ pluginCmd
|
|
|
1083
1147
|
}
|
|
1084
1148
|
if (!name) {
|
|
1085
1149
|
console.error(chalk.red(' Specify a plugin name or use --all'));
|
|
1086
|
-
process.exit(
|
|
1150
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1087
1151
|
}
|
|
1088
1152
|
const communityPlugins = await fetchCommunityPlugins();
|
|
1089
1153
|
const commDef = findCommunityPlugin(name, communityPlugins);
|
|
1090
1154
|
if (commDef && !hasCommunityRunner(name)) {
|
|
1091
1155
|
console.log(chalk.red(` ✗ Cannot enable "${name}" — runner not installed`));
|
|
1092
1156
|
console.log(chalk.gray(` Run: voiden-runner plugin install ${name}`));
|
|
1093
|
-
process.exit(
|
|
1157
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1094
1158
|
}
|
|
1095
1159
|
setPluginEnabled(name, true);
|
|
1096
1160
|
console.log(chalk.green(` ✓ Enabled`) + ` ${name}`);
|
|
@@ -1125,7 +1189,7 @@ pluginCmd
|
|
|
1125
1189
|
}
|
|
1126
1190
|
if (!name) {
|
|
1127
1191
|
console.error(chalk.red(' Specify a plugin name or use --all'));
|
|
1128
|
-
process.exit(
|
|
1192
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1129
1193
|
return;
|
|
1130
1194
|
}
|
|
1131
1195
|
setPluginEnabled(name, false);
|
|
@@ -1155,10 +1219,16 @@ pluginCmd
|
|
|
1155
1219
|
console.log(DIVIDER);
|
|
1156
1220
|
for (const def of corePlugins) {
|
|
1157
1221
|
const record = store.installedPlugins[def.name];
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1222
|
+
let statusBadge;
|
|
1223
|
+
if (record?.uninstalled) {
|
|
1224
|
+
statusBadge = chalk.gray(' not installed');
|
|
1225
|
+
}
|
|
1226
|
+
else if (record !== undefined && !record.enabled) {
|
|
1227
|
+
statusBadge = chalk.yellow(' · disabled');
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
statusBadge = chalk.green(' ✓ enabled');
|
|
1231
|
+
}
|
|
1162
1232
|
console.log(` ${chalk.bold(def.name.padEnd(24))}${statusBadge}${updateBadge(def.name, def.version)}`);
|
|
1163
1233
|
console.log(chalk.gray(` ${def.description}`));
|
|
1164
1234
|
}
|
|
@@ -1211,5 +1281,87 @@ pluginCmd
|
|
|
1211
1281
|
}
|
|
1212
1282
|
console.log();
|
|
1213
1283
|
});
|
|
1284
|
+
// ── voiden-runner mcp ─────────────────────────────────────────────────────────
|
|
1285
|
+
//
|
|
1286
|
+
// Enables the AI-agent loop for CLI-only users (no Voiden app installed):
|
|
1287
|
+
// registers @voiden/mcp-server with Claude Code / Codex, and installs a
|
|
1288
|
+
// standalone skill teaching the run/verify/write-back workflow. The Voiden
|
|
1289
|
+
// app's own Settings toggle does the equivalent for desktop users, reusing
|
|
1290
|
+
// the same registration helpers from mcpInstall.ts.
|
|
1291
|
+
function resolveMcpTargets(opts) {
|
|
1292
|
+
// Default to both when neither flag is given — a single command should be
|
|
1293
|
+
// enough to "just enable this".
|
|
1294
|
+
if (!opts.claude && !opts.codex)
|
|
1295
|
+
return { claude: true, codex: true };
|
|
1296
|
+
return { claude: Boolean(opts.claude), codex: Boolean(opts.codex) };
|
|
1297
|
+
}
|
|
1298
|
+
const mcpCmd = program
|
|
1299
|
+
.command('mcp')
|
|
1300
|
+
.description('Enable AI-agent integration — registers @voiden/mcp-server and installs a run/verify skill');
|
|
1301
|
+
mcpCmd
|
|
1302
|
+
.command('install')
|
|
1303
|
+
.description('Register @voiden/mcp-server with Claude Code and/or Codex, and install a skill teaching the run/verify/write-back loop.\n\n' +
|
|
1304
|
+
' Examples:\n' +
|
|
1305
|
+
' voiden-runner mcp install # both Claude Code and Codex\n' +
|
|
1306
|
+
' voiden-runner mcp install --claude # Claude Code only\n' +
|
|
1307
|
+
' voiden-runner mcp install -p ./my-project # register against a specific project dir (default: cwd)\n' +
|
|
1308
|
+
' voiden-runner mcp install --local-server ./dist/index.js # before publishing: point at a local build instead of npx\n')
|
|
1309
|
+
.option('--claude', 'Install for Claude Code only')
|
|
1310
|
+
.option('--codex', 'Install for Codex only')
|
|
1311
|
+
.option('-p, --project <path>', 'Project directory to register the MCP server against', '.')
|
|
1312
|
+
.option('--local-server <path>', 'Use `node <path>` instead of `npx -y @voiden/mcp-server` — for testing against a local build before it\'s published')
|
|
1313
|
+
.action((opts) => {
|
|
1314
|
+
const targets = resolveMcpTargets(opts);
|
|
1315
|
+
const serverCommand = opts.localServer
|
|
1316
|
+
? { command: 'node', args: [resolve(opts.localServer), resolve(opts.project)] }
|
|
1317
|
+
: undefined;
|
|
1318
|
+
const installed = installMcpIntegration(opts.project, targets, RUNNER_SKILL_MARKDOWN, serverCommand);
|
|
1319
|
+
if (installed.length === 0) {
|
|
1320
|
+
console.log(chalk.yellow(' Nothing to install.'));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
console.log();
|
|
1324
|
+
for (const target of installed) {
|
|
1325
|
+
console.log(chalk.green(` ✓ ${target === 'claude' ? 'Claude Code' : 'Codex'}`) + chalk.gray(` — skill installed, @voiden/mcp-server registered for ${resolve(opts.project)}`));
|
|
1326
|
+
}
|
|
1327
|
+
if (serverCommand) {
|
|
1328
|
+
console.log(chalk.gray(` Using local build: node ${serverCommand.args[0]}`));
|
|
1329
|
+
}
|
|
1330
|
+
console.log();
|
|
1331
|
+
console.log(chalk.gray(' Restart Claude Code / Codex (or run /mcp) to pick up the new server.'));
|
|
1332
|
+
});
|
|
1333
|
+
mcpCmd
|
|
1334
|
+
.command('uninstall')
|
|
1335
|
+
.description('Remove the MCP server registration and skill installed by `mcp install`')
|
|
1336
|
+
.option('--claude', 'Remove Claude Code integration only')
|
|
1337
|
+
.option('--codex', 'Remove Codex integration only')
|
|
1338
|
+
.option('-p, --project <path>', 'Project directory to unregister the MCP server from', '.')
|
|
1339
|
+
.action((opts) => {
|
|
1340
|
+
const targets = resolveMcpTargets(opts);
|
|
1341
|
+
const removed = uninstallMcpIntegration(opts.project, targets);
|
|
1342
|
+
if (removed.length === 0) {
|
|
1343
|
+
console.log(chalk.yellow(' Nothing to remove.'));
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
for (const target of removed) {
|
|
1347
|
+
console.log(chalk.green(` ✓ Removed`) + chalk.gray(` ${target === 'claude' ? 'Claude Code' : 'Codex'} integration`));
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
mcpCmd
|
|
1351
|
+
.command('status')
|
|
1352
|
+
.description('Show whether the MCP server + skill are installed for this project')
|
|
1353
|
+
.option('-p, --project <path>', 'Project directory to check', '.')
|
|
1354
|
+
.action((opts) => {
|
|
1355
|
+
const status = getMcpStatus(opts.project);
|
|
1356
|
+
console.log();
|
|
1357
|
+
console.log(chalk.bold(' Claude Code'));
|
|
1358
|
+
console.log(` skill installed: ${status.claude.skillInstalled ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1359
|
+
console.log(` server registered: ${status.claude.serverRegistered ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1360
|
+
console.log();
|
|
1361
|
+
console.log(chalk.bold(' Codex'));
|
|
1362
|
+
console.log(` skill installed: ${status.codex.skillInstalled ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1363
|
+
console.log(` server registered: ${status.codex.serverRegistered ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1364
|
+
console.log();
|
|
1365
|
+
});
|
|
1214
1366
|
program.parse();
|
|
1215
1367
|
//# sourceMappingURL=index.js.map
|