@voiden/runner 2.1.1 → 2.3.0-beta.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.
- package/CHANGELOG.md +6 -0
- package/bundled-runners/versions.json +6 -6
- package/bundled-runners/voiden-advanced-auth-runner.js +1 -1
- package/bundled-runners/voiden-graphql-runner.js +1 -1
- package/bundled-runners/voiden-rest-api-runner.js +1 -1
- package/bundled-runners/voiden-sockets-grpcs-runner.js +1 -1
- 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/envFile.d.ts +2 -0
- package/dist/envFile.d.ts.map +1 -0
- package/dist/envFile.js +59 -0
- package/dist/envFile.js.map +1 -0
- package/dist/headlessContext.d.ts.map +1 -1
- package/dist/headlessContext.js +9 -0
- package/dist/headlessContext.js.map +1 -1
- package/dist/index.js +261 -130
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +16 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +18 -0
- package/dist/lib.js.map +1 -0
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +2 -0
- package/dist/plugins/loader.js.map +1 -1
- 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/requestContainerRegistry.d.ts +36 -0
- package/dist/requestContainerRegistry.d.ts.map +1 -0
- package/dist/requestContainerRegistry.js +42 -0
- package/dist/requestContainerRegistry.js.map +1 -0
- package/dist/resultBlock.d.ts +37 -0
- package/dist/resultBlock.d.ts.map +1 -0
- package/dist/resultBlock.js +143 -0
- package/dist/resultBlock.js.map +1 -0
- package/dist/runner.d.ts +20 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +85 -10
- package/dist/runner.js.map +1 -1
- package/dist/runtimeVars.js +3 -3
- package/dist/runtimeVars.js.map +1 -1
- package/dist/types.d.ts +1 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
- package/dist/parser.d.ts +0 -24
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -87
- package/dist/parser.js.map +0 -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,29 +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 { classifyBlockVersion, parseVoidFile, installMcpIntegration, uninstallMcpIntegration, getMcpStatus, MCP_SKILL_MARKDOWN, } from '@voiden/executors';
|
|
19
|
+
import { loadEnvFile } from './envFile.js';
|
|
17
20
|
import { appendSessionResults, loadSessionResults, clearSession, } from './session.js';
|
|
18
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Exit codes — a stable, documented contract CI pipelines can branch on.
|
|
23
|
+
//
|
|
24
|
+
// 0 success — all requests passed
|
|
25
|
+
// 1 one or more requests failed (assertions/errors), or --bail /
|
|
26
|
+
// --fail-on-error triggered — unchanged from prior releases
|
|
27
|
+
// 2 the runner could not execute the run at all: bad CLI args/flags,
|
|
28
|
+
// missing files, missing plugins, invalid env — a pipeline/config
|
|
29
|
+
// problem, not an API failure
|
|
30
|
+
//
|
|
31
|
+
// See CHANGELOG.md and docs.voiden.md/docs/developer-tools/voiden-runner/ci-cd
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
const EXIT_SUCCESS = 0;
|
|
34
|
+
const EXIT_RUN_FAILURE = 1;
|
|
35
|
+
const EXIT_USAGE_ERROR = 2;
|
|
36
|
+
/** JSON output schema version — bump whenever a field is renamed, removed, or
|
|
37
|
+
* reinterpreted (adding a field is not a breaking change and does not need a bump). */
|
|
38
|
+
const JSON_SCHEMA_VERSION = '1';
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
40
|
// Helpers
|
|
20
41
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
function loadEnvFile(envPath) {
|
|
22
|
-
const content = readFileSync(envPath, 'utf-8');
|
|
23
|
-
const env = {};
|
|
24
|
-
const lines = content.split('\n');
|
|
25
|
-
for (let i = 0; i < lines.length; i++) {
|
|
26
|
-
const line = lines[i].trim();
|
|
27
|
-
if (!line || line.startsWith('#'))
|
|
28
|
-
continue;
|
|
29
|
-
const eq = line.indexOf('=');
|
|
30
|
-
if (eq === -1)
|
|
31
|
-
throw new Error(`Malformed line ${i + 1} in .env file: missing "="`);
|
|
32
|
-
const key = line.slice(0, eq).trim();
|
|
33
|
-
const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
34
|
-
if (!key)
|
|
35
|
-
throw new Error(`Malformed line ${i + 1} in .env file: empty key`);
|
|
36
|
-
env[key] = val;
|
|
37
|
-
}
|
|
38
|
-
return env;
|
|
39
|
-
}
|
|
40
42
|
function formatBytes(bytes) {
|
|
41
43
|
if (bytes < 1024)
|
|
42
44
|
return `${bytes}B`;
|
|
@@ -49,50 +51,6 @@ function formatDuration(ms) {
|
|
|
49
51
|
return `${ms}ms`;
|
|
50
52
|
return `${(ms / 1000).toFixed(2)}s`;
|
|
51
53
|
}
|
|
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
54
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
55
|
// Spinner
|
|
98
56
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -266,18 +224,26 @@ function printRunSummary(results, totalMs) {
|
|
|
266
224
|
console.log(DIVIDER);
|
|
267
225
|
console.log();
|
|
268
226
|
}
|
|
269
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Builds the `--json` / `--output-json` payload shape shared by `run` and
|
|
229
|
+
* `report generate`. `schemaVersion` is the stable contract external tooling
|
|
230
|
+
* codes against — see the exit-codes comment above for the versioning rule.
|
|
231
|
+
*/
|
|
232
|
+
function buildJsonReport(results, extra = {}) {
|
|
270
233
|
const passed = results.filter(r => r.result.success).length;
|
|
271
|
-
|
|
234
|
+
return {
|
|
235
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
272
236
|
summary: {
|
|
273
237
|
total: results.length,
|
|
274
238
|
passed,
|
|
275
239
|
failed: results.length - passed,
|
|
276
|
-
|
|
277
|
-
activePlugins,
|
|
240
|
+
...extra,
|
|
278
241
|
},
|
|
279
242
|
requests: results.map(r => ({ file: r.file, ...r.result })),
|
|
280
243
|
};
|
|
244
|
+
}
|
|
245
|
+
function printRunSummaryJson(results, totalMs, activePlugins) {
|
|
246
|
+
const output = buildJsonReport(results, { totalDurationMs: totalMs, activePlugins });
|
|
281
247
|
console.log(JSON.stringify(output, null, 2));
|
|
282
248
|
}
|
|
283
249
|
/** Split CHANGELOG.md into per-version entries on "## " headers (newest first, matching file order). */
|
|
@@ -346,6 +312,65 @@ async function notifyPluginUpdates() {
|
|
|
346
312
|
// Informational only — ignore failures (e.g. offline)
|
|
347
313
|
}
|
|
348
314
|
}
|
|
315
|
+
function scanProjectRequirements(files, cwd) {
|
|
316
|
+
// pluginId → version → files that declared it
|
|
317
|
+
const usages = new Map();
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
let content;
|
|
320
|
+
try {
|
|
321
|
+
content = readFileSync(file, 'utf-8');
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
for (const block of parseVoidFile(content)) {
|
|
327
|
+
const pluginId = block.attrs?.pluginId;
|
|
328
|
+
const pluginVersion = block.attrs?.pluginVersion;
|
|
329
|
+
if (!pluginId || !pluginVersion)
|
|
330
|
+
continue;
|
|
331
|
+
if (!usages.has(pluginId))
|
|
332
|
+
usages.set(pluginId, new Map());
|
|
333
|
+
const versions = usages.get(pluginId);
|
|
334
|
+
if (!versions.has(pluginVersion))
|
|
335
|
+
versions.set(pluginVersion, new Set());
|
|
336
|
+
versions.get(pluginVersion).add(relative(cwd, file));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const issues = [];
|
|
340
|
+
for (const [pluginId, versions] of usages) {
|
|
341
|
+
for (const [version, fileSet] of versions) {
|
|
342
|
+
const installed = getInstalledPluginInfo(pluginId);
|
|
343
|
+
const status = classifyBlockVersion({ pluginId, pluginVersion: version, blockType: '' }, installed);
|
|
344
|
+
if (status === 'ok')
|
|
345
|
+
continue;
|
|
346
|
+
issues.push({ pluginId, requiredVersion: version, installedVersion: installed?.version, status, files: [...fileSet] });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return issues;
|
|
350
|
+
}
|
|
351
|
+
function printProjectStatus(issues) {
|
|
352
|
+
if (issues.length === 0)
|
|
353
|
+
return;
|
|
354
|
+
console.log();
|
|
355
|
+
console.log(chalk.yellow(` ⚠ ${issues.length} plugin${issues.length !== 1 ? 's' : ''} ${issues.length !== 1 ? "don't" : "doesn't"} match what this project needs`));
|
|
356
|
+
for (const issue of issues) {
|
|
357
|
+
const have = issue.status === 'not-installed' ? 'not installed'
|
|
358
|
+
: issue.status === 'disabled' ? 'installed but disabled'
|
|
359
|
+
: `v${issue.installedVersion} installed`;
|
|
360
|
+
console.log(chalk.gray(` ${chalk.bold(issue.pluginId.padEnd(24))} requires v${issue.requiredVersion} — ${have}`));
|
|
361
|
+
console.log(chalk.gray(` used in: ${issue.files.join(', ')}`));
|
|
362
|
+
}
|
|
363
|
+
console.log(chalk.gray(` Run: voiden-runner plugin install <name>@<version>`));
|
|
364
|
+
}
|
|
365
|
+
/** Best-effort — never throws, never blocks command output on failure. */
|
|
366
|
+
function notifyProjectStatus(files, cwd) {
|
|
367
|
+
try {
|
|
368
|
+
printProjectStatus(scanProjectRequirements(files, cwd));
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// Informational only
|
|
372
|
+
}
|
|
373
|
+
}
|
|
349
374
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
350
375
|
// CLI
|
|
351
376
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -368,7 +393,7 @@ program
|
|
|
368
393
|
const changelogPath = resolve(join(dirname(fileURLToPath(import.meta.url)), '../CHANGELOG.md'));
|
|
369
394
|
if (!existsSync(changelogPath)) {
|
|
370
395
|
console.error(chalk.red(' ✗ No CHANGELOG.md found for this install.'));
|
|
371
|
-
process.exit(
|
|
396
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
372
397
|
}
|
|
373
398
|
const entries = parseChangelog(readFileSync(changelogPath, 'utf-8'));
|
|
374
399
|
if (entries.length === 0) {
|
|
@@ -382,7 +407,7 @@ program
|
|
|
382
407
|
if (!match) {
|
|
383
408
|
console.error(chalk.red(` ✗ No changelog entry found for version "${version}".`));
|
|
384
409
|
console.log(chalk.gray(` Available: ${entries.map(e => e.version).join(', ')}`));
|
|
385
|
-
process.exit(
|
|
410
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
386
411
|
}
|
|
387
412
|
toShow = [match];
|
|
388
413
|
}
|
|
@@ -442,14 +467,14 @@ program
|
|
|
442
467
|
const envPath = resolve(opts.env);
|
|
443
468
|
if (!existsSync(envPath)) {
|
|
444
469
|
console.error(chalk.red(`Env file not found: ${envPath}`));
|
|
445
|
-
process.exit(
|
|
470
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
446
471
|
}
|
|
447
472
|
try {
|
|
448
473
|
Object.assign(env, loadEnvFile(envPath));
|
|
449
474
|
}
|
|
450
475
|
catch (err) {
|
|
451
476
|
console.error(chalk.red(` ✗ ${err.message}`));
|
|
452
|
-
process.exit(
|
|
477
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
453
478
|
}
|
|
454
479
|
}
|
|
455
480
|
// 2. Individual --env-var overrides
|
|
@@ -458,13 +483,13 @@ program
|
|
|
458
483
|
const eq = pair.indexOf('=');
|
|
459
484
|
if (eq === -1) {
|
|
460
485
|
console.error(chalk.red(` ✗ Invalid --env-var format: "${pair}" (expected key=value)`));
|
|
461
|
-
process.exit(
|
|
486
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
462
487
|
}
|
|
463
488
|
const key = pair.slice(0, eq).trim();
|
|
464
489
|
const val = pair.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
465
490
|
if (!key) {
|
|
466
491
|
console.error(chalk.red(` ✗ Invalid --env-var format: "${pair}" (key cannot be empty)`));
|
|
467
|
-
process.exit(
|
|
492
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
468
493
|
}
|
|
469
494
|
env[key] = val;
|
|
470
495
|
}
|
|
@@ -472,7 +497,7 @@ program
|
|
|
472
497
|
const resolvedFiles = await resolveFiles(paths);
|
|
473
498
|
if (resolvedFiles.length === 0) {
|
|
474
499
|
console.error(chalk.red('No .void files found at the given path(s)'));
|
|
475
|
-
process.exit(
|
|
500
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
476
501
|
}
|
|
477
502
|
// --stop-on-failure is a CI-friendly alias for --bail
|
|
478
503
|
const stopOnFailure = opts.bail || opts.stopOnFailure;
|
|
@@ -490,11 +515,11 @@ program
|
|
|
490
515
|
if (opts.mail || opts.mailTo) {
|
|
491
516
|
if (!mailTo) {
|
|
492
517
|
console.error(chalk.red(' ✗ Mail error: no recipient found. Please provide --mail-to or set VOIDEN_MAIL_TO.'));
|
|
493
|
-
process.exit(
|
|
518
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
494
519
|
}
|
|
495
520
|
if (!smtpHost) {
|
|
496
521
|
console.error(chalk.red(' ✗ Mail keys are missing. Please provide SMTP configuration (VOIDEN_SMTP_HOST).'));
|
|
497
|
-
process.exit(
|
|
522
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
498
523
|
}
|
|
499
524
|
}
|
|
500
525
|
const runStart = Date.now();
|
|
@@ -600,16 +625,7 @@ program
|
|
|
600
625
|
// ── Output JSON to file (before mail so it can be attached) ──────────────
|
|
601
626
|
let savedJsonPath;
|
|
602
627
|
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
|
-
};
|
|
628
|
+
const jsonData = buildJsonReport(allResults, { totalDurationMs: totalMs, activePlugins });
|
|
613
629
|
try {
|
|
614
630
|
mkdirSync(dirname(opts.outputJson), { recursive: true });
|
|
615
631
|
writeFileSync(opts.outputJson, JSON.stringify(jsonData, null, 2) + '\n', 'utf-8');
|
|
@@ -650,10 +666,12 @@ program
|
|
|
650
666
|
console.log(chalk.gray(' (use this exit code in your shell script to abort on failure)'));
|
|
651
667
|
console.log();
|
|
652
668
|
}
|
|
653
|
-
// Surface plugin update notices — skipped in --json mode so output stays machine-readable
|
|
654
|
-
if (!opts.json)
|
|
669
|
+
// Surface plugin update / project-requirements notices — skipped in --json mode so output stays machine-readable
|
|
670
|
+
if (!opts.json) {
|
|
655
671
|
await notifyPluginUpdates();
|
|
656
|
-
|
|
672
|
+
notifyProjectStatus(resolvedFiles, process.cwd());
|
|
673
|
+
}
|
|
674
|
+
process.exit(shouldFail ? EXIT_RUN_FAILURE : EXIT_SUCCESS);
|
|
657
675
|
});
|
|
658
676
|
// ── voiden-runner session ─────────────────────────────────────────────────────
|
|
659
677
|
const sessionCmd = program
|
|
@@ -768,7 +786,7 @@ reportCmd
|
|
|
768
786
|
const results = loadSessionResults();
|
|
769
787
|
if (results.length === 0) {
|
|
770
788
|
console.error(chalk.red(' ✗ No results found in session. Run some .void files first.'));
|
|
771
|
-
process.exit(
|
|
789
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
772
790
|
}
|
|
773
791
|
// Load optional .env for report SMTP settings
|
|
774
792
|
const env = { ...process.env };
|
|
@@ -787,12 +805,12 @@ reportCmd
|
|
|
787
805
|
if (opts.mail || opts.mailTo) {
|
|
788
806
|
if (!mailTo) {
|
|
789
807
|
console.error(chalk.red(' ✗ Mail error: no recipient found. Please provide --mail-to or set VOIDEN_MAIL_TO.'));
|
|
790
|
-
process.exit(
|
|
808
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
791
809
|
}
|
|
792
810
|
const smtpHost = opts.smtpHost || env.VOIDEN_SMTP_HOST;
|
|
793
811
|
if (!smtpHost) {
|
|
794
812
|
console.error(chalk.red(' ✗ Mail keys are missing. Please provide SMTP configuration (VOIDEN_SMTP_HOST).'));
|
|
795
|
-
process.exit(
|
|
813
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
796
814
|
}
|
|
797
815
|
}
|
|
798
816
|
if (!opts.csv && !opts.outputJson && !mailTo) {
|
|
@@ -811,14 +829,7 @@ reportCmd
|
|
|
811
829
|
}
|
|
812
830
|
let savedJsonPath;
|
|
813
831
|
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
|
-
};
|
|
832
|
+
const jsonData = buildJsonReport(results);
|
|
822
833
|
try {
|
|
823
834
|
mkdirSync(dirname(opts.outputJson), { recursive: true });
|
|
824
835
|
writeFileSync(opts.outputJson, JSON.stringify(jsonData, null, 2) + '\n', 'utf-8');
|
|
@@ -839,7 +850,7 @@ reportCmd
|
|
|
839
850
|
if (!smtpHost) {
|
|
840
851
|
console.error(chalk.red(' ✗ SMTP configuration required for email reports.'));
|
|
841
852
|
console.log(chalk.gray(' Set VOIDEN_SMTP_HOST in your environment or use --smtp-host.'));
|
|
842
|
-
process.exit(
|
|
853
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
843
854
|
}
|
|
844
855
|
console.log(chalk.gray(` ↑ Sending session report to ${mailTo} …`));
|
|
845
856
|
try {
|
|
@@ -862,6 +873,14 @@ reportCmd
|
|
|
862
873
|
}
|
|
863
874
|
}
|
|
864
875
|
});
|
|
876
|
+
// Splits a `name` or `name@version` CLI arg. Plugin ids are plain slugs (no
|
|
877
|
+
// leading `@`), so splitting on the first `@` is unambiguous.
|
|
878
|
+
function parsePluginTarget(raw) {
|
|
879
|
+
const at = raw.indexOf('@');
|
|
880
|
+
if (at <= 0)
|
|
881
|
+
return { name: raw };
|
|
882
|
+
return { name: raw.slice(0, at), pinVersion: raw.slice(at + 1) };
|
|
883
|
+
}
|
|
865
884
|
// ── voiden-runner plugin ──────────────────────────────────────────────────────
|
|
866
885
|
const pluginCmd = program
|
|
867
886
|
.command('plugin')
|
|
@@ -870,37 +889,44 @@ const pluginCmd = program
|
|
|
870
889
|
pluginCmd
|
|
871
890
|
.command('install [names...]')
|
|
872
891
|
.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
|
|
892
|
+
' --all installs all core plugins only. Community plugins must be installed by name.\n' +
|
|
893
|
+
' Pin an exact version with name@version (e.g. after `voiden-runner lock`, or to\n' +
|
|
894
|
+
' match a "Block ... requires plugin X vY" error).\n\n' +
|
|
874
895
|
' Examples:\n' +
|
|
875
896
|
' voiden-runner plugin install --all\n' +
|
|
876
897
|
' voiden-runner plugin install voiden-scripting\n' +
|
|
877
|
-
' voiden-runner plugin install
|
|
898
|
+
' voiden-runner plugin install voiden-rest-api@1.4.7\n')
|
|
878
899
|
.option('--all', 'Install all core plugins (community plugins must be installed by name)')
|
|
879
|
-
.action(async (
|
|
900
|
+
.action(async (rawNames, opts) => {
|
|
880
901
|
const corePlugins = await getCorePlugins();
|
|
881
902
|
const communityPlugins = await fetchCommunityPlugins();
|
|
882
903
|
const targets = opts.all
|
|
883
|
-
? corePlugins.map(p => p.name)
|
|
884
|
-
:
|
|
904
|
+
? corePlugins.map(p => ({ name: p.name }))
|
|
905
|
+
: rawNames.map(parsePluginTarget);
|
|
885
906
|
if (targets.length === 0) {
|
|
886
907
|
console.error(chalk.red('Specify plugin name(s) or use --all'));
|
|
887
908
|
console.log(chalk.gray(' Core: ' + corePlugins.map(p => p.name).join(', ')));
|
|
888
909
|
if (communityPlugins.length > 0) {
|
|
889
910
|
console.log(chalk.gray(' Community (install by name): ' + communityPlugins.map(p => p.id).join(', ')));
|
|
890
911
|
}
|
|
891
|
-
process.exit(
|
|
912
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
892
913
|
}
|
|
893
914
|
let installedCount = 0;
|
|
894
|
-
for (const name of targets) {
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
if (!
|
|
915
|
+
for (const { name, pinVersion } of targets) {
|
|
916
|
+
const foundCoreDef = await findPlugin(name);
|
|
917
|
+
const foundCommDef = !foundCoreDef ? findCommunityPlugin(name, communityPlugins) : undefined;
|
|
918
|
+
if (!foundCoreDef && !foundCommDef) {
|
|
898
919
|
console.log(chalk.yellow(` ⚠ Unknown plugin "${name}" — skipped`));
|
|
899
920
|
continue;
|
|
900
921
|
}
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
|
|
922
|
+
// A pinned version overrides the registry's "latest" default — this is what
|
|
923
|
+
// makes the fix-it command in version-mismatch errors ("plugin install x@y")
|
|
924
|
+
// actually able to install the exact version a file declares.
|
|
925
|
+
const coreDef = foundCoreDef && pinVersion ? { ...foundCoreDef, version: pinVersion } : foundCoreDef;
|
|
926
|
+
const commDef = foundCommDef && pinVersion ? { ...foundCommDef, version: pinVersion } : foundCommDef;
|
|
927
|
+
// Core plugins: only download if not already bundled/cached — unless a
|
|
928
|
+
// specific version was pinned, in which case always fetch that version.
|
|
929
|
+
if (coreDef && (pinVersion || !hasCoreRunner(name))) {
|
|
904
930
|
process.stdout.write(` ↓ Downloading runner for ${chalk.bold(name)} …`);
|
|
905
931
|
try {
|
|
906
932
|
const ok = await downloadCoreRunner(coreDef.name, coreDef.repo, coreDef.runnerAsset, coreDef.version, false);
|
|
@@ -938,6 +964,13 @@ pluginCmd
|
|
|
938
964
|
console.log(chalk.green(` ✓ Installed`) + chalk.bold(` ${name}`) + chalk.gray(` — ${description}`));
|
|
939
965
|
installedCount++;
|
|
940
966
|
}
|
|
967
|
+
else if (pinVersion) {
|
|
968
|
+
// Re-running install with an explicit pin (e.g. to fix a version-mismatch)
|
|
969
|
+
// should still record the newly-downloaded version even if already "installed".
|
|
970
|
+
setPluginVersion(name, coreDef?.version ?? commDef?.version ?? pinVersion);
|
|
971
|
+
console.log(chalk.green(` ✓ Installed`) + chalk.bold(` ${name}@${pinVersion}`));
|
|
972
|
+
installedCount++;
|
|
973
|
+
}
|
|
941
974
|
else {
|
|
942
975
|
console.log(chalk.gray(` · Already installed`) + ` ${name}`);
|
|
943
976
|
}
|
|
@@ -1030,16 +1063,24 @@ pluginCmd
|
|
|
1030
1063
|
' voiden-runner plugin uninstall apyhub-explorer voiden-scripting\n' +
|
|
1031
1064
|
' voiden-runner plugin uninstall --all\n')
|
|
1032
1065
|
.option('--all', 'Uninstall all installed plugins (core and community)')
|
|
1033
|
-
.action((names, opts) => {
|
|
1034
|
-
|
|
1066
|
+
.action(async (names, opts) => {
|
|
1067
|
+
// Core plugins are bundled and enabled by default — they may never have
|
|
1068
|
+
// an explicit store record even though they're clearly active, so --all
|
|
1069
|
+
// (and named uninstalls of a bundled plugin) must include the full core
|
|
1070
|
+
// registry, not just names that already happen to have a store record.
|
|
1071
|
+
const corePlugins = await getCorePlugins();
|
|
1072
|
+
const coreNames = new Set(corePlugins.map(p => p.name));
|
|
1073
|
+
const targets = opts.all
|
|
1074
|
+
? [...new Set([...coreNames, ...getAllInstalledPlugins().map(p => p.name)])]
|
|
1075
|
+
: names;
|
|
1035
1076
|
if (targets.length === 0) {
|
|
1036
1077
|
console.error(chalk.red(' Specify plugin name(s) or use --all'));
|
|
1037
|
-
process.exit(
|
|
1078
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1038
1079
|
return;
|
|
1039
1080
|
}
|
|
1040
1081
|
let removedCount = 0;
|
|
1041
1082
|
for (const name of targets) {
|
|
1042
|
-
const removed = uninstallPlugin(name);
|
|
1083
|
+
const removed = uninstallPlugin(name, coreNames.has(name));
|
|
1043
1084
|
if (removed) {
|
|
1044
1085
|
console.log(chalk.green(` ✓ Uninstalled`) + ` ${name}`);
|
|
1045
1086
|
removedCount++;
|
|
@@ -1064,9 +1105,11 @@ pluginCmd
|
|
|
1064
1105
|
.action(async (name, opts) => {
|
|
1065
1106
|
if (opts.all) {
|
|
1066
1107
|
const store = readStore();
|
|
1067
|
-
// Re-enable all explicitly disabled plugins (core + community)
|
|
1108
|
+
// Re-enable all explicitly disabled plugins (core + community) — but not
|
|
1109
|
+
// uninstalled ones; bringing those back requires an explicit `plugin
|
|
1110
|
+
// install`, not a blanket --all enable.
|
|
1068
1111
|
const disabled = Object.entries(store.installedPlugins)
|
|
1069
|
-
.filter(([, r]) => !r.enabled)
|
|
1112
|
+
.filter(([, r]) => !r.enabled && !r.uninstalled)
|
|
1070
1113
|
.map(([n]) => n);
|
|
1071
1114
|
// Also ensure all core plugins that were never in the store are treated as enabled (default)
|
|
1072
1115
|
const disabledCoreNotInStore = [];
|
|
@@ -1083,14 +1126,14 @@ pluginCmd
|
|
|
1083
1126
|
}
|
|
1084
1127
|
if (!name) {
|
|
1085
1128
|
console.error(chalk.red(' Specify a plugin name or use --all'));
|
|
1086
|
-
process.exit(
|
|
1129
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1087
1130
|
}
|
|
1088
1131
|
const communityPlugins = await fetchCommunityPlugins();
|
|
1089
1132
|
const commDef = findCommunityPlugin(name, communityPlugins);
|
|
1090
1133
|
if (commDef && !hasCommunityRunner(name)) {
|
|
1091
1134
|
console.log(chalk.red(` ✗ Cannot enable "${name}" — runner not installed`));
|
|
1092
1135
|
console.log(chalk.gray(` Run: voiden-runner plugin install ${name}`));
|
|
1093
|
-
process.exit(
|
|
1136
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1094
1137
|
}
|
|
1095
1138
|
setPluginEnabled(name, true);
|
|
1096
1139
|
console.log(chalk.green(` ✓ Enabled`) + ` ${name}`);
|
|
@@ -1125,7 +1168,7 @@ pluginCmd
|
|
|
1125
1168
|
}
|
|
1126
1169
|
if (!name) {
|
|
1127
1170
|
console.error(chalk.red(' Specify a plugin name or use --all'));
|
|
1128
|
-
process.exit(
|
|
1171
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
1129
1172
|
return;
|
|
1130
1173
|
}
|
|
1131
1174
|
setPluginEnabled(name, false);
|
|
@@ -1155,10 +1198,16 @@ pluginCmd
|
|
|
1155
1198
|
console.log(DIVIDER);
|
|
1156
1199
|
for (const def of corePlugins) {
|
|
1157
1200
|
const record = store.installedPlugins[def.name];
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1201
|
+
let statusBadge;
|
|
1202
|
+
if (record?.uninstalled) {
|
|
1203
|
+
statusBadge = chalk.gray(' not installed');
|
|
1204
|
+
}
|
|
1205
|
+
else if (record !== undefined && !record.enabled) {
|
|
1206
|
+
statusBadge = chalk.yellow(' · disabled');
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
statusBadge = chalk.green(' ✓ enabled');
|
|
1210
|
+
}
|
|
1162
1211
|
console.log(` ${chalk.bold(def.name.padEnd(24))}${statusBadge}${updateBadge(def.name, def.version)}`);
|
|
1163
1212
|
console.log(chalk.gray(` ${def.description}`));
|
|
1164
1213
|
}
|
|
@@ -1211,5 +1260,87 @@ pluginCmd
|
|
|
1211
1260
|
}
|
|
1212
1261
|
console.log();
|
|
1213
1262
|
});
|
|
1263
|
+
// ── voiden-runner mcp ─────────────────────────────────────────────────────────
|
|
1264
|
+
//
|
|
1265
|
+
// Enables the AI-agent loop for CLI-only users (no Voiden app installed):
|
|
1266
|
+
// registers @voiden/mcp-server with Claude Code / Codex, and installs a
|
|
1267
|
+
// standalone skill teaching the run/verify/write-back workflow. The Voiden
|
|
1268
|
+
// app's own Settings toggle does the equivalent for desktop users, reusing
|
|
1269
|
+
// the same registration helpers from mcpInstall.ts.
|
|
1270
|
+
function resolveMcpTargets(opts) {
|
|
1271
|
+
// Default to both when neither flag is given — a single command should be
|
|
1272
|
+
// enough to "just enable this".
|
|
1273
|
+
if (!opts.claude && !opts.codex)
|
|
1274
|
+
return { claude: true, codex: true };
|
|
1275
|
+
return { claude: Boolean(opts.claude), codex: Boolean(opts.codex) };
|
|
1276
|
+
}
|
|
1277
|
+
const mcpCmd = program
|
|
1278
|
+
.command('mcp')
|
|
1279
|
+
.description('Enable AI-agent integration — registers @voiden/mcp-server and installs a run/verify skill');
|
|
1280
|
+
mcpCmd
|
|
1281
|
+
.command('install')
|
|
1282
|
+
.description('Register @voiden/mcp-server with Claude Code and/or Codex, and install a skill teaching the run/verify/write-back loop.\n\n' +
|
|
1283
|
+
' Examples:\n' +
|
|
1284
|
+
' voiden-runner mcp install # both Claude Code and Codex\n' +
|
|
1285
|
+
' voiden-runner mcp install --claude # Claude Code only\n' +
|
|
1286
|
+
' voiden-runner mcp install -p ./my-project # register against a specific project dir (default: cwd)\n' +
|
|
1287
|
+
' voiden-runner mcp install --local-server ./dist/index.js # before publishing: point at a local build instead of npx\n')
|
|
1288
|
+
.option('--claude', 'Install for Claude Code only')
|
|
1289
|
+
.option('--codex', 'Install for Codex only')
|
|
1290
|
+
.option('-p, --project <path>', 'Project directory to register the MCP server against', '.')
|
|
1291
|
+
.option('--local-server <path>', 'Use `node <path>` instead of `npx -y @voiden/mcp-server` — for testing against a local build before it\'s published')
|
|
1292
|
+
.action((opts) => {
|
|
1293
|
+
const targets = resolveMcpTargets(opts);
|
|
1294
|
+
const serverCommand = opts.localServer
|
|
1295
|
+
? { command: 'node', args: [resolve(opts.localServer), resolve(opts.project)] }
|
|
1296
|
+
: undefined;
|
|
1297
|
+
const installed = installMcpIntegration(opts.project, targets, MCP_SKILL_MARKDOWN, serverCommand);
|
|
1298
|
+
if (installed.length === 0) {
|
|
1299
|
+
console.log(chalk.yellow(' Nothing to install.'));
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
console.log();
|
|
1303
|
+
for (const target of installed) {
|
|
1304
|
+
console.log(chalk.green(` ✓ ${target === 'claude' ? 'Claude Code' : 'Codex'}`) + chalk.gray(` — skill installed, @voiden/mcp-server registered for ${resolve(opts.project)}`));
|
|
1305
|
+
}
|
|
1306
|
+
if (serverCommand) {
|
|
1307
|
+
console.log(chalk.gray(` Using local build: node ${serverCommand.args[0]}`));
|
|
1308
|
+
}
|
|
1309
|
+
console.log();
|
|
1310
|
+
console.log(chalk.gray(' Restart Claude Code / Codex (or run /mcp) to pick up the new server.'));
|
|
1311
|
+
});
|
|
1312
|
+
mcpCmd
|
|
1313
|
+
.command('uninstall')
|
|
1314
|
+
.description('Remove the MCP server registration and skill installed by `mcp install`')
|
|
1315
|
+
.option('--claude', 'Remove Claude Code integration only')
|
|
1316
|
+
.option('--codex', 'Remove Codex integration only')
|
|
1317
|
+
.option('-p, --project <path>', 'Project directory to unregister the MCP server from', '.')
|
|
1318
|
+
.action((opts) => {
|
|
1319
|
+
const targets = resolveMcpTargets(opts);
|
|
1320
|
+
const removed = uninstallMcpIntegration(opts.project, targets);
|
|
1321
|
+
if (removed.length === 0) {
|
|
1322
|
+
console.log(chalk.yellow(' Nothing to remove.'));
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
for (const target of removed) {
|
|
1326
|
+
console.log(chalk.green(` ✓ Removed`) + chalk.gray(` ${target === 'claude' ? 'Claude Code' : 'Codex'} integration`));
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
mcpCmd
|
|
1330
|
+
.command('status')
|
|
1331
|
+
.description('Show whether the MCP server + skill are installed for this project')
|
|
1332
|
+
.option('-p, --project <path>', 'Project directory to check', '.')
|
|
1333
|
+
.action((opts) => {
|
|
1334
|
+
const status = getMcpStatus(opts.project);
|
|
1335
|
+
console.log();
|
|
1336
|
+
console.log(chalk.bold(' Claude Code'));
|
|
1337
|
+
console.log(` skill installed: ${status.claude.skillInstalled ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1338
|
+
console.log(` server registered: ${status.claude.serverRegistered ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1339
|
+
console.log();
|
|
1340
|
+
console.log(chalk.bold(' Codex'));
|
|
1341
|
+
console.log(` skill installed: ${status.codex.skillInstalled ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1342
|
+
console.log(` server registered: ${status.codex.serverRegistered ? chalk.green('yes') : chalk.gray('no')}`);
|
|
1343
|
+
console.log();
|
|
1344
|
+
});
|
|
1214
1345
|
program.parse();
|
|
1215
1346
|
//# sourceMappingURL=index.js.map
|