@boolesai/tspec-cli 1.1.0 → 1.3.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/README.md +291 -25
- package/dist/index.js +492 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/types/commands/plugin-install.d.ts +24 -0
- package/types/commands/plugin-list.d.ts +39 -0
- package/types/commands/run.d.ts +2 -0
- package/types/utils/files.d.ts +43 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { existsSync, statSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { isAbsolute, resolve as resolve$1, basename, relative, dirname, join } from "path";
|
|
2
5
|
import ora from "ora";
|
|
3
|
-
import { getTypeFromFilePath, validateTestCase, clearTemplateCache,
|
|
6
|
+
import { getTypeFromFilePath, isSuiteFile, getSuiteProtocolType, validateTestCase, clearTemplateCache, getPluginManager, version as version$1, registry as registry$1, executeSuite, parseTestCases, scheduler, PluginManager } from "@boolesai/tspec";
|
|
4
7
|
import { glob } from "glob";
|
|
5
|
-
import { isAbsolute, resolve as resolve$1, basename, relative } from "path";
|
|
6
|
-
import { existsSync, statSync } from "fs";
|
|
7
8
|
import chalk from "chalk";
|
|
9
|
+
import { findConfigFile, findLocalConfigFile, findGlobalConfigFile, PLUGINS_DIR, isPluginInstalled, installPlugin, GLOBAL_CONFIG_PATH } from "@boolesai/tspec/plugin";
|
|
8
10
|
import process$2 from "node:process";
|
|
9
11
|
async function discoverTSpecFiles(patterns, cwd) {
|
|
10
12
|
const workingDir = process.cwd();
|
|
@@ -15,14 +17,14 @@ async function discoverTSpecFiles(patterns, cwd) {
|
|
|
15
17
|
if (existsSync(absolutePath)) {
|
|
16
18
|
const stat = statSync(absolutePath);
|
|
17
19
|
if (stat.isFile()) {
|
|
18
|
-
if (absolutePath.endsWith(".
|
|
20
|
+
if (absolutePath.endsWith(".tcase")) {
|
|
19
21
|
filePaths.push(absolutePath);
|
|
20
22
|
}
|
|
21
23
|
continue;
|
|
22
24
|
} else if (stat.isDirectory()) {
|
|
23
|
-
const dirFiles = await glob("**/*.
|
|
25
|
+
const dirFiles = await glob("**/*.tcase", { cwd: absolutePath, absolute: true });
|
|
24
26
|
if (dirFiles.length === 0) {
|
|
25
|
-
errors2.push(`No .
|
|
27
|
+
errors2.push(`No .tcase files found in directory: ${pattern2}`);
|
|
26
28
|
} else {
|
|
27
29
|
filePaths.push(...dirFiles);
|
|
28
30
|
}
|
|
@@ -30,9 +32,9 @@ async function discoverTSpecFiles(patterns, cwd) {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
const matches = await glob(pattern2, { cwd: workingDir, absolute: true });
|
|
33
|
-
const tspecMatches = matches.filter((f) => f.endsWith(".
|
|
35
|
+
const tspecMatches = matches.filter((f) => f.endsWith(".tcase"));
|
|
34
36
|
if (tspecMatches.length === 0) {
|
|
35
|
-
errors2.push(`No .
|
|
37
|
+
errors2.push(`No .tcase files matched pattern: ${pattern2}`);
|
|
36
38
|
} else {
|
|
37
39
|
filePaths.push(...tspecMatches);
|
|
38
40
|
}
|
|
@@ -46,6 +48,61 @@ async function discoverTSpecFiles(patterns, cwd) {
|
|
|
46
48
|
}));
|
|
47
49
|
return { files, errors: errors2 };
|
|
48
50
|
}
|
|
51
|
+
async function discoverAllTestFiles(patterns, cwd) {
|
|
52
|
+
const workingDir = process.cwd();
|
|
53
|
+
const tspecPaths = [];
|
|
54
|
+
const suitePaths = [];
|
|
55
|
+
const errors2 = [];
|
|
56
|
+
for (const pattern2 of patterns) {
|
|
57
|
+
const absolutePath = isAbsolute(pattern2) ? pattern2 : resolve$1(workingDir, pattern2);
|
|
58
|
+
if (existsSync(absolutePath)) {
|
|
59
|
+
const stat = statSync(absolutePath);
|
|
60
|
+
if (stat.isFile()) {
|
|
61
|
+
if (absolutePath.endsWith(".tcase")) {
|
|
62
|
+
tspecPaths.push(absolutePath);
|
|
63
|
+
} else if (isSuiteFile(absolutePath)) {
|
|
64
|
+
suitePaths.push(absolutePath);
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
} else if (stat.isDirectory()) {
|
|
68
|
+
const tspecFiles2 = await glob("**/*.tcase", { cwd: absolutePath, absolute: true });
|
|
69
|
+
const suiteFiles2 = await glob("**/*.tsuite", { cwd: absolutePath, absolute: true });
|
|
70
|
+
if (tspecFiles2.length === 0 && suiteFiles2.length === 0) {
|
|
71
|
+
errors2.push(`No .tcase or .tsuite files found in directory: ${pattern2}`);
|
|
72
|
+
} else {
|
|
73
|
+
tspecPaths.push(...tspecFiles2);
|
|
74
|
+
suitePaths.push(...suiteFiles2);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const matches = await glob(pattern2, { cwd: workingDir, absolute: true });
|
|
80
|
+
const tspecMatches = matches.filter((f) => f.endsWith(".tcase"));
|
|
81
|
+
const suiteMatches = matches.filter((f) => isSuiteFile(f));
|
|
82
|
+
if (tspecMatches.length === 0 && suiteMatches.length === 0) {
|
|
83
|
+
errors2.push(`No .tcase or .tsuite files matched pattern: ${pattern2}`);
|
|
84
|
+
} else {
|
|
85
|
+
tspecPaths.push(...tspecMatches);
|
|
86
|
+
suitePaths.push(...suiteMatches);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const uniqueTspecPaths = [...new Set(tspecPaths)];
|
|
90
|
+
const uniqueSuitePaths = [...new Set(suitePaths)];
|
|
91
|
+
const tspecFiles = uniqueTspecPaths.map((filePath) => ({
|
|
92
|
+
path: filePath,
|
|
93
|
+
relativePath: relative(workingDir, filePath),
|
|
94
|
+
fileName: basename(filePath),
|
|
95
|
+
protocol: getTypeFromFilePath(filePath)
|
|
96
|
+
}));
|
|
97
|
+
const suiteFiles = uniqueSuitePaths.map((filePath) => ({
|
|
98
|
+
path: filePath,
|
|
99
|
+
relativePath: relative(workingDir, filePath),
|
|
100
|
+
fileName: basename(filePath),
|
|
101
|
+
protocol: getSuiteProtocolType(filePath),
|
|
102
|
+
isTemplate: filePath.endsWith(".tsuite.yaml")
|
|
103
|
+
}));
|
|
104
|
+
return { tspecFiles, suiteFiles, errors: errors2 };
|
|
105
|
+
}
|
|
49
106
|
function formatJson(data) {
|
|
50
107
|
return JSON.stringify(data, null, 2);
|
|
51
108
|
}
|
|
@@ -199,7 +256,7 @@ async function executeValidate(params) {
|
|
|
199
256
|
if (fileDescriptors.length === 0) {
|
|
200
257
|
return {
|
|
201
258
|
success: false,
|
|
202
|
-
output: "No .
|
|
259
|
+
output: "No .tcase files found",
|
|
203
260
|
data: { results: [] }
|
|
204
261
|
};
|
|
205
262
|
}
|
|
@@ -222,7 +279,7 @@ async function executeValidate(params) {
|
|
|
222
279
|
data: { results }
|
|
223
280
|
};
|
|
224
281
|
}
|
|
225
|
-
const validateCommand = new Command("validate").description("Validate .
|
|
282
|
+
const validateCommand = new Command("validate").description("Validate .tcase files for schema correctness").argument("<files...>", "Files or glob patterns to validate").option("-o, --output <format>", "Output format: json, text", "text").option("-q, --quiet", "Only output errors").action(async (files, options) => {
|
|
226
283
|
setLoggerOptions({ quiet: options.quiet });
|
|
227
284
|
const spinner = options.quiet ? null : ora("Validating...").start();
|
|
228
285
|
try {
|
|
@@ -297,6 +354,14 @@ async function runFileTestCasesInternal(descriptor, env, params, concurrency, fa
|
|
|
297
354
|
}
|
|
298
355
|
async function executeRun(params) {
|
|
299
356
|
clearTemplateCache();
|
|
357
|
+
const configPath = params.config || findConfigFile();
|
|
358
|
+
if (configPath) {
|
|
359
|
+
const pluginManager = getPluginManager(version$1);
|
|
360
|
+
await pluginManager.initialize(configPath, {
|
|
361
|
+
skipAutoInstall: params.noAutoInstall
|
|
362
|
+
});
|
|
363
|
+
registry$1.enablePluginManager();
|
|
364
|
+
}
|
|
300
365
|
const concurrency = params.concurrency ?? 5;
|
|
301
366
|
const env = params.env ?? {};
|
|
302
367
|
const paramValues = params.params ?? {};
|
|
@@ -304,11 +369,11 @@ async function executeRun(params) {
|
|
|
304
369
|
const output = params.output ?? "text";
|
|
305
370
|
const verbose = params.verbose ?? false;
|
|
306
371
|
const quiet = params.quiet ?? false;
|
|
307
|
-
const {
|
|
308
|
-
if (
|
|
372
|
+
const { tspecFiles, suiteFiles, errors: resolveErrors } = await discoverAllTestFiles(params.files);
|
|
373
|
+
if (tspecFiles.length === 0 && suiteFiles.length === 0) {
|
|
309
374
|
return {
|
|
310
375
|
success: false,
|
|
311
|
-
output: "No .
|
|
376
|
+
output: "No .tcase or .tsuite files found",
|
|
312
377
|
data: {
|
|
313
378
|
results: [],
|
|
314
379
|
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
@@ -316,6 +381,29 @@ async function executeRun(params) {
|
|
|
316
381
|
}
|
|
317
382
|
};
|
|
318
383
|
}
|
|
384
|
+
if (suiteFiles.length > 0) {
|
|
385
|
+
return executeSuiteRun(suiteFiles, tspecFiles, {
|
|
386
|
+
env,
|
|
387
|
+
params: paramValues,
|
|
388
|
+
concurrency,
|
|
389
|
+
failFast,
|
|
390
|
+
output,
|
|
391
|
+
verbose,
|
|
392
|
+
quiet
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return executeTspecRun(tspecFiles, {
|
|
396
|
+
env,
|
|
397
|
+
params: paramValues,
|
|
398
|
+
concurrency,
|
|
399
|
+
failFast,
|
|
400
|
+
output,
|
|
401
|
+
verbose,
|
|
402
|
+
quiet
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async function executeTspecRun(fileDescriptors, options) {
|
|
406
|
+
const { env, params: paramValues, concurrency, failFast, output, verbose, quiet } = options;
|
|
319
407
|
const allResults = [];
|
|
320
408
|
const parseErrors = [];
|
|
321
409
|
let totalTestCases = 0;
|
|
@@ -384,7 +472,117 @@ ${parseErrors.length} file(s) failed to parse:`);
|
|
|
384
472
|
data: { results: formattedResults, summary, parseErrors }
|
|
385
473
|
};
|
|
386
474
|
}
|
|
387
|
-
|
|
475
|
+
async function executeSuiteRun(suiteFiles, additionalTspecFiles, options) {
|
|
476
|
+
const { env, params: paramValues, failFast, output, verbose, quiet } = options;
|
|
477
|
+
const allResults = [];
|
|
478
|
+
const parseErrors = [];
|
|
479
|
+
let totalTests = 0;
|
|
480
|
+
let totalPassed = 0;
|
|
481
|
+
let totalFailed = 0;
|
|
482
|
+
let stopped = false;
|
|
483
|
+
for (const suiteDescriptor of suiteFiles) {
|
|
484
|
+
if (stopped) break;
|
|
485
|
+
if (suiteDescriptor.isTemplate) continue;
|
|
486
|
+
try {
|
|
487
|
+
const suiteResult = await executeSuite(suiteDescriptor.path, {
|
|
488
|
+
env,
|
|
489
|
+
params: paramValues,
|
|
490
|
+
onSuiteStart: (name) => {
|
|
491
|
+
if (!quiet) logger.log(chalk.blue(`
|
|
492
|
+
Suite: ${name}`));
|
|
493
|
+
},
|
|
494
|
+
onTestStart: (file) => {
|
|
495
|
+
if (verbose) logger.log(chalk.gray(` Running: ${file}`));
|
|
496
|
+
},
|
|
497
|
+
onTestComplete: (file, result) => {
|
|
498
|
+
const statusIcon = result.status === "passed" ? chalk.green("✓") : chalk.red("✗");
|
|
499
|
+
if (!quiet) logger.log(` ${statusIcon} ${result.name} (${result.duration}ms)`);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
totalTests += suiteResult.stats.total;
|
|
503
|
+
totalPassed += suiteResult.stats.passed;
|
|
504
|
+
totalFailed += suiteResult.stats.failed + suiteResult.stats.error;
|
|
505
|
+
for (const testResult of suiteResult.tests) {
|
|
506
|
+
allResults.push({
|
|
507
|
+
testCaseId: testResult.name,
|
|
508
|
+
passed: testResult.status === "passed",
|
|
509
|
+
duration: testResult.duration,
|
|
510
|
+
assertions: (testResult.assertions || []).map((a) => ({
|
|
511
|
+
passed: a.passed,
|
|
512
|
+
type: a.type,
|
|
513
|
+
message: a.message || ""
|
|
514
|
+
}))
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (suiteResult.suites) {
|
|
518
|
+
for (const nestedSuite of suiteResult.suites) {
|
|
519
|
+
totalTests += nestedSuite.stats.total;
|
|
520
|
+
totalPassed += nestedSuite.stats.passed;
|
|
521
|
+
totalFailed += nestedSuite.stats.failed + nestedSuite.stats.error;
|
|
522
|
+
for (const testResult of nestedSuite.tests) {
|
|
523
|
+
allResults.push({
|
|
524
|
+
testCaseId: `${nestedSuite.name}/${testResult.name}`,
|
|
525
|
+
passed: testResult.status === "passed",
|
|
526
|
+
duration: testResult.duration,
|
|
527
|
+
assertions: (testResult.assertions || []).map((a) => ({
|
|
528
|
+
passed: a.passed,
|
|
529
|
+
type: a.type,
|
|
530
|
+
message: a.message || ""
|
|
531
|
+
}))
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (failFast && (suiteResult.status === "failed" || suiteResult.status === "error")) {
|
|
537
|
+
stopped = true;
|
|
538
|
+
}
|
|
539
|
+
} catch (err) {
|
|
540
|
+
parseErrors.push({
|
|
541
|
+
file: suiteDescriptor.path,
|
|
542
|
+
error: err instanceof Error ? err.message : String(err)
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (additionalTspecFiles.length > 0 && !stopped) {
|
|
547
|
+
const tspecResult = await executeTspecRun(additionalTspecFiles, options);
|
|
548
|
+
allResults.push(...tspecResult.data.results);
|
|
549
|
+
parseErrors.push(...tspecResult.data.parseErrors);
|
|
550
|
+
totalTests += tspecResult.data.summary.total;
|
|
551
|
+
totalPassed += tspecResult.data.summary.passed;
|
|
552
|
+
totalFailed += tspecResult.data.summary.failed;
|
|
553
|
+
}
|
|
554
|
+
const summary = {
|
|
555
|
+
total: totalTests,
|
|
556
|
+
passed: totalPassed,
|
|
557
|
+
failed: totalFailed,
|
|
558
|
+
passRate: totalTests > 0 ? totalPassed / totalTests * 100 : 0,
|
|
559
|
+
duration: 0
|
|
560
|
+
};
|
|
561
|
+
let outputStr;
|
|
562
|
+
if (output === "json") {
|
|
563
|
+
outputStr = formatJson({ results: allResults, summary, parseErrors });
|
|
564
|
+
} else {
|
|
565
|
+
const parts = [];
|
|
566
|
+
if (!quiet) {
|
|
567
|
+
parts.push("\n" + chalk.bold("Results:"));
|
|
568
|
+
parts.push(formatTestResults(allResults, summary, { format: output, verbose }));
|
|
569
|
+
} else {
|
|
570
|
+
parts.push(`${summary.passed}/${summary.total} tests passed (${summary.passRate.toFixed(1)}%)`);
|
|
571
|
+
}
|
|
572
|
+
if (parseErrors.length > 0) {
|
|
573
|
+
parts.push(`
|
|
574
|
+
${parseErrors.length} file(s) failed to parse:`);
|
|
575
|
+
parseErrors.forEach(({ file, error: error2 }) => parts.push(` ${file}: ${error2}`));
|
|
576
|
+
}
|
|
577
|
+
outputStr = parts.join("\n");
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
success: totalFailed === 0 && parseErrors.length === 0,
|
|
581
|
+
output: outputStr,
|
|
582
|
+
data: { results: allResults, summary, parseErrors }
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const runCommand = new Command("run").description("Execute test cases and report results").argument("<files...>", "Files or glob patterns to run").option("-o, --output <format>", "Output format: json, text", "text").option("-c, --concurrency <number>", "Max concurrent tests", "5").option("-e, --env <key=value>", "Environment variables", parseKeyValue$1, {}).option("-p, --params <key=value>", "Parameters", parseKeyValue$1, {}).option("-v, --verbose", "Verbose output").option("-q, --quiet", "Only output summary").option("--fail-fast", "Stop on first failure").option("--config <path>", "Path to tspec.config.json for plugin loading").option("--no-auto-install", "Skip automatic plugin installation").action(async (files, options) => {
|
|
388
586
|
setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
|
|
389
587
|
const spinner = options.quiet ? null : ora("Running tests...").start();
|
|
390
588
|
try {
|
|
@@ -395,6 +593,8 @@ const runCommand = new Command("run").description("Execute test cases and report
|
|
|
395
593
|
verbose: options.verbose,
|
|
396
594
|
quiet: options.quiet,
|
|
397
595
|
failFast: options.failFast,
|
|
596
|
+
config: options.config,
|
|
597
|
+
noAutoInstall: options.noAutoInstall,
|
|
398
598
|
env: options.env,
|
|
399
599
|
params: options.params
|
|
400
600
|
});
|
|
@@ -415,9 +615,20 @@ const runCommand = new Command("run").description("Execute test cases and report
|
|
|
415
615
|
}
|
|
416
616
|
process.exit(result.success ? 0 : 1);
|
|
417
617
|
} catch (err) {
|
|
418
|
-
spinner?.
|
|
618
|
+
spinner?.stop();
|
|
419
619
|
const message = err instanceof Error ? err.message : String(err);
|
|
420
|
-
|
|
620
|
+
if (options.output === "json") {
|
|
621
|
+
const errorOutput = formatJson({
|
|
622
|
+
results: [],
|
|
623
|
+
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
624
|
+
parseErrors: [],
|
|
625
|
+
error: message
|
|
626
|
+
});
|
|
627
|
+
logger.log(errorOutput);
|
|
628
|
+
} else {
|
|
629
|
+
spinner?.fail("Execution failed");
|
|
630
|
+
logger.error(message);
|
|
631
|
+
}
|
|
421
632
|
process.exit(2);
|
|
422
633
|
}
|
|
423
634
|
});
|
|
@@ -438,7 +649,7 @@ async function executeParse(params) {
|
|
|
438
649
|
if (fileDescriptors.length === 0) {
|
|
439
650
|
return {
|
|
440
651
|
success: false,
|
|
441
|
-
output: "No .
|
|
652
|
+
output: "No .tcase files found",
|
|
442
653
|
data: {
|
|
443
654
|
testCases: [],
|
|
444
655
|
parseErrors: [],
|
|
@@ -14756,7 +14967,7 @@ const TOOL_DEFINITIONS = [
|
|
|
14756
14967
|
files: {
|
|
14757
14968
|
type: "array",
|
|
14758
14969
|
items: { type: "string" },
|
|
14759
|
-
description: 'Files or glob patterns to run (e.g., ["tests/*.
|
|
14970
|
+
description: 'Files or glob patterns to run (e.g., ["tests/*.tcase"])'
|
|
14760
14971
|
},
|
|
14761
14972
|
concurrency: {
|
|
14762
14973
|
type: "number",
|
|
@@ -14787,7 +14998,7 @@ const TOOL_DEFINITIONS = [
|
|
|
14787
14998
|
},
|
|
14788
14999
|
{
|
|
14789
15000
|
name: "tspec_validate",
|
|
14790
|
-
description: "Validate .
|
|
15001
|
+
description: "Validate .tcase files for schema correctness",
|
|
14791
15002
|
inputSchema: {
|
|
14792
15003
|
type: "object",
|
|
14793
15004
|
properties: {
|
|
@@ -14949,12 +15160,273 @@ const mcpCommand = new Command("mcp").description("Start MCP server for tool int
|
|
|
14949
15160
|
setLoggerOptions({ quiet: true });
|
|
14950
15161
|
await startMcpServer();
|
|
14951
15162
|
});
|
|
15163
|
+
async function executePluginList(params) {
|
|
15164
|
+
const output = params.output ?? "text";
|
|
15165
|
+
const pluginManager = new PluginManager(version$1);
|
|
15166
|
+
const localConfigPath = findLocalConfigFile();
|
|
15167
|
+
const globalConfigPath = findGlobalConfigFile();
|
|
15168
|
+
const configPath = params.config || localConfigPath || globalConfigPath;
|
|
15169
|
+
let loadSummary;
|
|
15170
|
+
if (configPath) {
|
|
15171
|
+
loadSummary = await pluginManager.initialize(configPath);
|
|
15172
|
+
}
|
|
15173
|
+
const plugins = pluginManager.list();
|
|
15174
|
+
const protocols = pluginManager.listProtocols();
|
|
15175
|
+
let healthReports;
|
|
15176
|
+
if (params.health) {
|
|
15177
|
+
healthReports = await pluginManager.healthCheck();
|
|
15178
|
+
}
|
|
15179
|
+
const data = {
|
|
15180
|
+
plugins: plugins.map((p) => ({
|
|
15181
|
+
name: p.name,
|
|
15182
|
+
version: p.version,
|
|
15183
|
+
description: p.description,
|
|
15184
|
+
protocols: p.protocols,
|
|
15185
|
+
author: p.author,
|
|
15186
|
+
homepage: p.homepage
|
|
15187
|
+
})),
|
|
15188
|
+
protocols,
|
|
15189
|
+
configPath: configPath || void 0,
|
|
15190
|
+
configSources: {
|
|
15191
|
+
local: localConfigPath || void 0,
|
|
15192
|
+
global: globalConfigPath || void 0
|
|
15193
|
+
},
|
|
15194
|
+
pluginsDir: PLUGINS_DIR,
|
|
15195
|
+
health: healthReports
|
|
15196
|
+
};
|
|
15197
|
+
let outputStr;
|
|
15198
|
+
if (output === "json") {
|
|
15199
|
+
outputStr = JSON.stringify(data, null, 2);
|
|
15200
|
+
} else {
|
|
15201
|
+
outputStr = formatPluginListText(data, params.verbose ?? false, loadSummary);
|
|
15202
|
+
}
|
|
15203
|
+
return {
|
|
15204
|
+
success: true,
|
|
15205
|
+
output: outputStr,
|
|
15206
|
+
data
|
|
15207
|
+
};
|
|
15208
|
+
}
|
|
15209
|
+
function formatPluginListText(data, verbose, loadSummary) {
|
|
15210
|
+
const lines = [];
|
|
15211
|
+
lines.push(chalk.bold("\nTSpec Plugins\n"));
|
|
15212
|
+
lines.push(chalk.bold("Config:"));
|
|
15213
|
+
if (data.configSources?.local) {
|
|
15214
|
+
lines.push(chalk.gray(` Local: ${data.configSources.local}`));
|
|
15215
|
+
} else {
|
|
15216
|
+
lines.push(chalk.gray(" Local: (none)"));
|
|
15217
|
+
}
|
|
15218
|
+
if (data.configSources?.global) {
|
|
15219
|
+
lines.push(chalk.gray(` Global: ${data.configSources.global}`));
|
|
15220
|
+
} else {
|
|
15221
|
+
lines.push(chalk.gray(" Global: (none)"));
|
|
15222
|
+
}
|
|
15223
|
+
lines.push(chalk.gray(` Plugins dir: ${data.pluginsDir}`));
|
|
15224
|
+
if (loadSummary) {
|
|
15225
|
+
lines.push("");
|
|
15226
|
+
lines.push(chalk.gray(`Discovered: ${loadSummary.total}, Loaded: ${loadSummary.loaded}`));
|
|
15227
|
+
if (loadSummary.installed && loadSummary.installed > 0) {
|
|
15228
|
+
lines.push(chalk.green(`Installed: ${loadSummary.installed} plugin(s)`));
|
|
15229
|
+
}
|
|
15230
|
+
if (loadSummary.failed > 0) {
|
|
15231
|
+
lines.push(chalk.red(`Failed: ${loadSummary.failed}`));
|
|
15232
|
+
for (const error2 of loadSummary.errors) {
|
|
15233
|
+
lines.push(chalk.red(` ${error2.plugin}: ${error2.error}`));
|
|
15234
|
+
}
|
|
15235
|
+
}
|
|
15236
|
+
if (loadSummary.installErrors && loadSummary.installErrors.length > 0) {
|
|
15237
|
+
lines.push(chalk.red(`Install failures:`));
|
|
15238
|
+
for (const error2 of loadSummary.installErrors) {
|
|
15239
|
+
lines.push(chalk.red(` ${error2.plugin}: ${error2.error}`));
|
|
15240
|
+
}
|
|
15241
|
+
}
|
|
15242
|
+
}
|
|
15243
|
+
lines.push("");
|
|
15244
|
+
if (data.plugins.length === 0) {
|
|
15245
|
+
lines.push(chalk.yellow("No plugins loaded."));
|
|
15246
|
+
lines.push(chalk.gray("Add plugins to your tspec.config.json:"));
|
|
15247
|
+
lines.push(chalk.gray(" {"));
|
|
15248
|
+
lines.push(chalk.gray(' "plugins": ["@tspec/http", "@tspec/web"]'));
|
|
15249
|
+
lines.push(chalk.gray(" }"));
|
|
15250
|
+
} else {
|
|
15251
|
+
for (const plugin of data.plugins) {
|
|
15252
|
+
lines.push(`${chalk.cyan(plugin.name)} ${chalk.gray(`v${plugin.version}`)}`);
|
|
15253
|
+
if (verbose && plugin.description) {
|
|
15254
|
+
lines.push(` ${plugin.description}`);
|
|
15255
|
+
}
|
|
15256
|
+
lines.push(` Protocols: ${plugin.protocols.join(", ")}`);
|
|
15257
|
+
if (verbose) {
|
|
15258
|
+
if (plugin.author) {
|
|
15259
|
+
lines.push(` Author: ${plugin.author}`);
|
|
15260
|
+
}
|
|
15261
|
+
if (plugin.homepage) {
|
|
15262
|
+
lines.push(` Homepage: ${plugin.homepage}`);
|
|
15263
|
+
}
|
|
15264
|
+
}
|
|
15265
|
+
lines.push("");
|
|
15266
|
+
}
|
|
15267
|
+
}
|
|
15268
|
+
if (data.health) {
|
|
15269
|
+
lines.push(chalk.bold("Health Check\n"));
|
|
15270
|
+
for (const report of data.health) {
|
|
15271
|
+
const status = report.healthy ? chalk.green("✓ Healthy") : chalk.red("✗ Unhealthy");
|
|
15272
|
+
lines.push(`${chalk.cyan(report.plugin)}: ${status}`);
|
|
15273
|
+
if (report.message) {
|
|
15274
|
+
lines.push(` ${report.message}`);
|
|
15275
|
+
}
|
|
15276
|
+
}
|
|
15277
|
+
lines.push("");
|
|
15278
|
+
}
|
|
15279
|
+
if (data.protocols.length > 0) {
|
|
15280
|
+
lines.push(chalk.bold("Supported Protocols: ") + data.protocols.join(", "));
|
|
15281
|
+
}
|
|
15282
|
+
return lines.join("\n");
|
|
15283
|
+
}
|
|
15284
|
+
const pluginListCommand = new Command("plugin:list").alias("plugins").description("List all installed TSpec plugins").option("-o, --output <format>", "Output format: json, text", "text").option("-v, --verbose", "Show detailed plugin information").option("--health", "Run health checks on all plugins").option("-c, --config <path>", "Path to tspec.config.json").action(async (options) => {
|
|
15285
|
+
try {
|
|
15286
|
+
const result = await executePluginList({
|
|
15287
|
+
output: options.output,
|
|
15288
|
+
verbose: options.verbose,
|
|
15289
|
+
health: options.health,
|
|
15290
|
+
config: options.config
|
|
15291
|
+
});
|
|
15292
|
+
logger.log(result.output);
|
|
15293
|
+
} catch (err) {
|
|
15294
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15295
|
+
logger.error(`Failed to list plugins: ${message}`);
|
|
15296
|
+
process.exit(2);
|
|
15297
|
+
}
|
|
15298
|
+
});
|
|
15299
|
+
function loadConfigFile(configPath) {
|
|
15300
|
+
if (!existsSync(configPath)) {
|
|
15301
|
+
return { plugins: [], pluginOptions: {} };
|
|
15302
|
+
}
|
|
15303
|
+
try {
|
|
15304
|
+
const content = readFileSync(configPath, "utf-8");
|
|
15305
|
+
return JSON.parse(content);
|
|
15306
|
+
} catch {
|
|
15307
|
+
return { plugins: [], pluginOptions: {} };
|
|
15308
|
+
}
|
|
15309
|
+
}
|
|
15310
|
+
function saveConfigFile(configPath, config2) {
|
|
15311
|
+
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
15312
|
+
if (!existsSync(dir)) {
|
|
15313
|
+
mkdirSync(dir, { recursive: true });
|
|
15314
|
+
}
|
|
15315
|
+
writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
15316
|
+
}
|
|
15317
|
+
function addPluginToConfig(config2, pluginName) {
|
|
15318
|
+
if (!config2.plugins) {
|
|
15319
|
+
config2.plugins = [];
|
|
15320
|
+
}
|
|
15321
|
+
if (config2.plugins.includes(pluginName)) {
|
|
15322
|
+
return false;
|
|
15323
|
+
}
|
|
15324
|
+
config2.plugins.push(pluginName);
|
|
15325
|
+
return true;
|
|
15326
|
+
}
|
|
15327
|
+
async function executePluginInstall(params) {
|
|
15328
|
+
const { pluginName, output = "text", global: useGlobal = false, config: customConfig } = params;
|
|
15329
|
+
let configPath;
|
|
15330
|
+
if (customConfig) {
|
|
15331
|
+
configPath = customConfig;
|
|
15332
|
+
} else if (useGlobal) {
|
|
15333
|
+
configPath = GLOBAL_CONFIG_PATH;
|
|
15334
|
+
} else {
|
|
15335
|
+
const localConfig = findLocalConfigFile();
|
|
15336
|
+
configPath = localConfig || GLOBAL_CONFIG_PATH;
|
|
15337
|
+
}
|
|
15338
|
+
const alreadyInstalled = isPluginInstalled(pluginName);
|
|
15339
|
+
let installed = false;
|
|
15340
|
+
let installError;
|
|
15341
|
+
if (!alreadyInstalled) {
|
|
15342
|
+
const result = await installPlugin(pluginName);
|
|
15343
|
+
installed = result.success;
|
|
15344
|
+
if (!result.success) {
|
|
15345
|
+
installError = result.error;
|
|
15346
|
+
}
|
|
15347
|
+
} else {
|
|
15348
|
+
installed = true;
|
|
15349
|
+
}
|
|
15350
|
+
let configUpdated = false;
|
|
15351
|
+
if (installed) {
|
|
15352
|
+
const config2 = loadConfigFile(configPath);
|
|
15353
|
+
configUpdated = addPluginToConfig(config2, pluginName);
|
|
15354
|
+
if (configUpdated) {
|
|
15355
|
+
saveConfigFile(configPath, config2);
|
|
15356
|
+
}
|
|
15357
|
+
}
|
|
15358
|
+
const data = {
|
|
15359
|
+
plugin: pluginName,
|
|
15360
|
+
installed,
|
|
15361
|
+
configUpdated,
|
|
15362
|
+
configPath: installed ? configPath : void 0,
|
|
15363
|
+
error: installError
|
|
15364
|
+
};
|
|
15365
|
+
let outputStr;
|
|
15366
|
+
if (output === "json") {
|
|
15367
|
+
outputStr = JSON.stringify(data, null, 2);
|
|
15368
|
+
} else {
|
|
15369
|
+
if (!installed) {
|
|
15370
|
+
outputStr = chalk.red(`Failed to install ${pluginName}: ${installError || "Unknown error"}`);
|
|
15371
|
+
} else if (alreadyInstalled && !configUpdated) {
|
|
15372
|
+
outputStr = chalk.yellow(`Plugin ${pluginName} is already installed and configured.`);
|
|
15373
|
+
} else if (alreadyInstalled && configUpdated) {
|
|
15374
|
+
outputStr = [
|
|
15375
|
+
chalk.green(`Plugin ${pluginName} is already installed.`),
|
|
15376
|
+
chalk.green(`Added to config: ${configPath}`)
|
|
15377
|
+
].join("\n");
|
|
15378
|
+
} else if (configUpdated) {
|
|
15379
|
+
outputStr = [
|
|
15380
|
+
chalk.green(`Successfully installed ${pluginName}`),
|
|
15381
|
+
chalk.green(`Added to config: ${configPath}`)
|
|
15382
|
+
].join("\n");
|
|
15383
|
+
} else {
|
|
15384
|
+
outputStr = [
|
|
15385
|
+
chalk.green(`Successfully installed ${pluginName}`),
|
|
15386
|
+
chalk.yellow(`Plugin already in config: ${configPath}`)
|
|
15387
|
+
].join("\n");
|
|
15388
|
+
}
|
|
15389
|
+
}
|
|
15390
|
+
return {
|
|
15391
|
+
success: installed,
|
|
15392
|
+
output: outputStr,
|
|
15393
|
+
data
|
|
15394
|
+
};
|
|
15395
|
+
}
|
|
15396
|
+
const pluginInstallCommand = new Command("plugin:install").alias("install").description("Install a TSpec plugin and add it to config").argument("<plugin>", "Plugin name (npm package name, e.g., @tspec/http)").option("-o, --output <format>", "Output format: json, text", "text").option("-g, --global", "Add plugin to global config (~/.tspec/tspec.config.json)").option("-c, --config <path>", "Path to config file to update").action(async (plugin, options) => {
|
|
15397
|
+
const spinner = ora(`Installing ${plugin}...`).start();
|
|
15398
|
+
try {
|
|
15399
|
+
const result = await executePluginInstall({
|
|
15400
|
+
pluginName: plugin,
|
|
15401
|
+
output: options.output,
|
|
15402
|
+
global: options.global,
|
|
15403
|
+
config: options.config
|
|
15404
|
+
});
|
|
15405
|
+
spinner.stop();
|
|
15406
|
+
logger.log(result.output);
|
|
15407
|
+
process.exit(result.success ? 0 : 1);
|
|
15408
|
+
} catch (err) {
|
|
15409
|
+
spinner.stop();
|
|
15410
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15411
|
+
if (options.output === "json") {
|
|
15412
|
+
logger.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
15413
|
+
} else {
|
|
15414
|
+
logger.error(`Failed to install plugin: ${message}`);
|
|
15415
|
+
}
|
|
15416
|
+
process.exit(2);
|
|
15417
|
+
}
|
|
15418
|
+
});
|
|
15419
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
15420
|
+
const __dirname$1 = dirname(__filename$1);
|
|
15421
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname$1, "../package.json"), "utf-8"));
|
|
14952
15422
|
const program = new Command();
|
|
14953
|
-
program.name("tspec").description("CLI for @boolesai/tspec testing framework").version(
|
|
15423
|
+
program.name("tspec").description("CLI for @boolesai/tspec testing framework").version(packageJson.version);
|
|
14954
15424
|
program.addCommand(validateCommand);
|
|
14955
15425
|
program.addCommand(runCommand);
|
|
14956
15426
|
program.addCommand(parseCommand);
|
|
14957
15427
|
program.addCommand(listCommand);
|
|
14958
15428
|
program.addCommand(mcpCommand);
|
|
15429
|
+
program.addCommand(pluginListCommand);
|
|
15430
|
+
program.addCommand(pluginInstallCommand);
|
|
14959
15431
|
await program.parseAsync();
|
|
14960
15432
|
//# sourceMappingURL=index.js.map
|