@boolesai/tspec-cli 1.2.0 → 1.3.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/README.md +270 -4
- package/dist/index.js +583 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/types/commands/parse.d.ts +3 -0
- package/types/commands/plugin-install.d.ts +24 -0
- package/types/commands/plugin-list.d.ts +39 -0
- package/types/commands/run.d.ts +4 -0
- package/types/commands/validate.d.ts +3 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { existsSync, statSync, readFileSync } from "fs";
|
|
2
|
+
import { existsSync, statSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { isAbsolute, resolve as resolve$1, basename, relative, dirname, join } from "path";
|
|
5
5
|
import ora from "ora";
|
|
6
|
-
import { getTypeFromFilePath, isSuiteFile, getSuiteProtocolType, validateTestCase, clearTemplateCache, executeSuite, parseTestCases, scheduler, registry as registry$1 } from "@boolesai/tspec";
|
|
7
|
-
import { glob } from "glob";
|
|
8
6
|
import chalk from "chalk";
|
|
7
|
+
import { getTypeFromFilePath, isSuiteFile, getSuiteProtocolType, loadConfig, isProxyEnabled, getProxyConfig, validateTestCase, readTestFiles, ProxyClient, clearTemplateCache, getPluginManager, version as version$1, registry as registry$1, executeSuite, parseTestCases, scheduler, PluginManager } from "@boolesai/tspec";
|
|
8
|
+
import { findConfigFile, findLocalConfigFile, findGlobalConfigFile, PLUGINS_DIR, isPluginInstalled, installPlugin, GLOBAL_CONFIG_PATH } from "@boolesai/tspec/plugin";
|
|
9
|
+
import { glob } from "glob";
|
|
9
10
|
import process$2 from "node:process";
|
|
10
11
|
async function discoverTSpecFiles(patterns, cwd) {
|
|
11
12
|
const workingDir = process.cwd();
|
|
@@ -232,7 +233,7 @@ function error(message, ...args) {
|
|
|
232
233
|
console.error(chalk.red(`[error] ${message}`), ...args);
|
|
233
234
|
}
|
|
234
235
|
function log(message, ...args) {
|
|
235
|
-
console.
|
|
236
|
+
console.log(message, ...args);
|
|
236
237
|
}
|
|
237
238
|
function newline() {
|
|
238
239
|
if (!globalOptions.quiet) {
|
|
@@ -249,8 +250,77 @@ const logger = {
|
|
|
249
250
|
newline,
|
|
250
251
|
setOptions: setLoggerOptions
|
|
251
252
|
};
|
|
253
|
+
async function executeValidateViaProxy(params, proxyConfig) {
|
|
254
|
+
const output = params.output ?? "text";
|
|
255
|
+
const { files: fileDescriptors } = await discoverTSpecFiles(params.files);
|
|
256
|
+
const allFiles = fileDescriptors.map((f) => f.path);
|
|
257
|
+
if (allFiles.length === 0) {
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
output: "No .tcase files found",
|
|
261
|
+
data: { results: [] }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const fileResult = readTestFiles(allFiles);
|
|
265
|
+
if (fileResult.errors.length > 0 && Object.keys(fileResult.fileContents).length === 0) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
output: `Failed to read test files: ${fileResult.errors.map((e) => e.error).join(", ")}`,
|
|
269
|
+
data: { results: [] }
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const client = new ProxyClient({
|
|
273
|
+
url: proxyConfig.url,
|
|
274
|
+
timeout: proxyConfig.timeout,
|
|
275
|
+
headers: proxyConfig.headers
|
|
276
|
+
});
|
|
277
|
+
const result = await client.executeValidate(allFiles, fileResult.fileContents);
|
|
278
|
+
if (!result.success || !result.data) {
|
|
279
|
+
const errorMsg = result.error ? `${result.error.code}: ${result.error.message}${result.error.details ? ` (${result.error.details})` : ""}` : "Unknown proxy error";
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
output: output === "json" ? formatJson({ error: errorMsg, results: [] }) : `Proxy error: ${errorMsg}`,
|
|
283
|
+
data: { results: [] }
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const proxyResponse = result.data;
|
|
287
|
+
const results = (proxyResponse.results || []).map((r) => ({
|
|
288
|
+
file: r.file,
|
|
289
|
+
valid: r.valid,
|
|
290
|
+
errors: r.errors
|
|
291
|
+
}));
|
|
292
|
+
const hasErrors = results.some((r) => !r.valid);
|
|
293
|
+
let outputStr;
|
|
294
|
+
if (output === "json") {
|
|
295
|
+
outputStr = formatJson({ results });
|
|
296
|
+
} else {
|
|
297
|
+
const parts = [];
|
|
298
|
+
parts.push(chalk.cyan(`[Proxy: ${proxyConfig.url}]`));
|
|
299
|
+
parts.push(formatValidationResults(
|
|
300
|
+
results.map((r) => ({ file: r.file, result: { valid: r.valid, errors: r.errors } })),
|
|
301
|
+
{ format: output }
|
|
302
|
+
));
|
|
303
|
+
outputStr = parts.join("\n");
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
success: !hasErrors,
|
|
307
|
+
output: outputStr,
|
|
308
|
+
data: { results }
|
|
309
|
+
};
|
|
310
|
+
}
|
|
252
311
|
async function executeValidate(params) {
|
|
253
312
|
const output = params.output ?? "text";
|
|
313
|
+
if (!params.noProxy) {
|
|
314
|
+
const configPath = params.config || findConfigFile() || void 0;
|
|
315
|
+
const config2 = await loadConfig(configPath);
|
|
316
|
+
const proxyUrl = params.proxyUrl;
|
|
317
|
+
if (proxyUrl || isProxyEnabled(config2, "validate")) {
|
|
318
|
+
const proxyConfig = proxyUrl ? { url: proxyUrl, timeout: 3e4, headers: {} } : getProxyConfig(config2);
|
|
319
|
+
if (proxyConfig) {
|
|
320
|
+
return executeValidateViaProxy(params, proxyConfig);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
254
324
|
const { files: fileDescriptors } = await discoverTSpecFiles(params.files);
|
|
255
325
|
if (fileDescriptors.length === 0) {
|
|
256
326
|
return {
|
|
@@ -278,13 +348,16 @@ async function executeValidate(params) {
|
|
|
278
348
|
data: { results }
|
|
279
349
|
};
|
|
280
350
|
}
|
|
281
|
-
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) => {
|
|
351
|
+
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").option("--config <path>", "Path to tspec.config.json").option("--no-proxy", "Disable proxy for this execution").option("--proxy-url <url>", "Override proxy URL for this execution").action(async (files, options) => {
|
|
282
352
|
setLoggerOptions({ quiet: options.quiet });
|
|
283
353
|
const spinner = options.quiet ? null : ora("Validating...").start();
|
|
284
354
|
try {
|
|
285
355
|
const result = await executeValidate({
|
|
286
356
|
files,
|
|
287
|
-
output: options.output
|
|
357
|
+
output: options.output,
|
|
358
|
+
noProxy: options.noProxy,
|
|
359
|
+
proxyUrl: options.proxyUrl,
|
|
360
|
+
config: options.config
|
|
288
361
|
});
|
|
289
362
|
spinner?.stop();
|
|
290
363
|
logger.log(result.output);
|
|
@@ -353,6 +426,31 @@ async function runFileTestCasesInternal(descriptor, env, params, concurrency, fa
|
|
|
353
426
|
}
|
|
354
427
|
async function executeRun(params) {
|
|
355
428
|
clearTemplateCache();
|
|
429
|
+
const configPath = params.config || findConfigFile();
|
|
430
|
+
if (!params.noProxy) {
|
|
431
|
+
const config2 = await loadConfig(configPath || void 0);
|
|
432
|
+
const proxyUrl = params.proxyUrl;
|
|
433
|
+
if (proxyUrl || isProxyEnabled(config2, "run")) {
|
|
434
|
+
const proxyConfig = proxyUrl ? { url: proxyUrl, timeout: 3e4, headers: {} } : getProxyConfig(config2);
|
|
435
|
+
if (proxyConfig) {
|
|
436
|
+
return executeRunViaProxy(params, proxyConfig);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (configPath) {
|
|
441
|
+
const pluginManager = getPluginManager(version$1);
|
|
442
|
+
await pluginManager.initialize(configPath, {
|
|
443
|
+
skipAutoInstall: params.noAutoInstall
|
|
444
|
+
});
|
|
445
|
+
registry$1.enablePluginManager();
|
|
446
|
+
}
|
|
447
|
+
if (configPath) {
|
|
448
|
+
const pluginManager = getPluginManager(version$1);
|
|
449
|
+
await pluginManager.initialize(configPath, {
|
|
450
|
+
skipAutoInstall: params.noAutoInstall
|
|
451
|
+
});
|
|
452
|
+
registry$1.enablePluginManager();
|
|
453
|
+
}
|
|
356
454
|
const concurrency = params.concurrency ?? 5;
|
|
357
455
|
const env = params.env ?? {};
|
|
358
456
|
const paramValues = params.params ?? {};
|
|
@@ -465,6 +563,7 @@ ${parseErrors.length} file(s) failed to parse:`);
|
|
|
465
563
|
}
|
|
466
564
|
async function executeSuiteRun(suiteFiles, additionalTspecFiles, options) {
|
|
467
565
|
const { env, params: paramValues, failFast, output, verbose, quiet } = options;
|
|
566
|
+
const isJsonOutput = output === "json";
|
|
468
567
|
const allResults = [];
|
|
469
568
|
const parseErrors = [];
|
|
470
569
|
let totalTests = 0;
|
|
@@ -478,14 +577,15 @@ async function executeSuiteRun(suiteFiles, additionalTspecFiles, options) {
|
|
|
478
577
|
const suiteResult = await executeSuite(suiteDescriptor.path, {
|
|
479
578
|
env,
|
|
480
579
|
params: paramValues,
|
|
481
|
-
|
|
580
|
+
silent: isJsonOutput,
|
|
581
|
+
onSuiteStart: isJsonOutput ? void 0 : (name) => {
|
|
482
582
|
if (!quiet) logger.log(chalk.blue(`
|
|
483
583
|
Suite: ${name}`));
|
|
484
584
|
},
|
|
485
|
-
onTestStart: (file) => {
|
|
585
|
+
onTestStart: isJsonOutput ? void 0 : (file) => {
|
|
486
586
|
if (verbose) logger.log(chalk.gray(` Running: ${file}`));
|
|
487
587
|
},
|
|
488
|
-
onTestComplete: (file, result) => {
|
|
588
|
+
onTestComplete: isJsonOutput ? void 0 : (file, result) => {
|
|
489
589
|
const statusIcon = result.status === "passed" ? chalk.green("✓") : chalk.red("✗");
|
|
490
590
|
if (!quiet) logger.log(` ${statusIcon} ${result.name} (${result.duration}ms)`);
|
|
491
591
|
}
|
|
@@ -573,9 +673,105 @@ ${parseErrors.length} file(s) failed to parse:`);
|
|
|
573
673
|
data: { results: allResults, summary, parseErrors }
|
|
574
674
|
};
|
|
575
675
|
}
|
|
576
|
-
|
|
676
|
+
async function executeRunViaProxy(params, proxyConfig) {
|
|
677
|
+
const output = params.output ?? "text";
|
|
678
|
+
const { tspecFiles, suiteFiles } = await discoverAllTestFiles(params.files);
|
|
679
|
+
const allFiles = [...tspecFiles.map((f) => f.path), ...suiteFiles.map((f) => f.path)];
|
|
680
|
+
if (allFiles.length === 0) {
|
|
681
|
+
return {
|
|
682
|
+
success: false,
|
|
683
|
+
output: "No .tcase or .tsuite files found",
|
|
684
|
+
data: {
|
|
685
|
+
results: [],
|
|
686
|
+
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
687
|
+
parseErrors: []
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const fileResult = readTestFiles(allFiles);
|
|
692
|
+
if (fileResult.errors.length > 0 && Object.keys(fileResult.fileContents).length === 0) {
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
output: `Failed to read test files: ${fileResult.errors.map((e) => e.error).join(", ")}`,
|
|
696
|
+
data: {
|
|
697
|
+
results: [],
|
|
698
|
+
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
699
|
+
parseErrors: fileResult.errors.map((e) => ({ file: e.file, error: e.error }))
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const client = new ProxyClient({
|
|
704
|
+
url: proxyConfig.url,
|
|
705
|
+
timeout: proxyConfig.timeout,
|
|
706
|
+
headers: proxyConfig.headers
|
|
707
|
+
});
|
|
708
|
+
const result = await client.executeRun(
|
|
709
|
+
allFiles,
|
|
710
|
+
fileResult.fileContents,
|
|
711
|
+
{
|
|
712
|
+
concurrency: params.concurrency,
|
|
713
|
+
failFast: params.failFast,
|
|
714
|
+
env: params.env,
|
|
715
|
+
params: params.params
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
if (!result.success || !result.data) {
|
|
719
|
+
const errorMsg = result.error ? `${result.error.code}: ${result.error.message}${result.error.details ? ` (${result.error.details})` : ""}` : "Unknown proxy error";
|
|
720
|
+
return {
|
|
721
|
+
success: false,
|
|
722
|
+
output: output === "json" ? formatJson({ error: errorMsg, results: [], summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 }, parseErrors: [] }) : `Proxy error: ${errorMsg}`,
|
|
723
|
+
data: {
|
|
724
|
+
results: [],
|
|
725
|
+
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
726
|
+
parseErrors: []
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const proxyResponse = result.data;
|
|
731
|
+
const formattedResults = (proxyResponse.results || []).map((r) => ({
|
|
732
|
+
testCaseId: r.testCaseId,
|
|
733
|
+
passed: r.passed,
|
|
734
|
+
duration: r.duration,
|
|
735
|
+
assertions: (r.assertions || []).map((a) => ({
|
|
736
|
+
passed: a.passed,
|
|
737
|
+
type: a.type,
|
|
738
|
+
message: a.message || ""
|
|
739
|
+
}))
|
|
740
|
+
}));
|
|
741
|
+
const summary = proxyResponse.summary ? {
|
|
742
|
+
total: proxyResponse.summary.total,
|
|
743
|
+
passed: proxyResponse.summary.passed,
|
|
744
|
+
failed: proxyResponse.summary.failed,
|
|
745
|
+
passRate: proxyResponse.summary.passRate,
|
|
746
|
+
duration: proxyResponse.summary.duration
|
|
747
|
+
} : { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 };
|
|
748
|
+
const parseErrors = (proxyResponse.parseErrors || []).map((e) => ({
|
|
749
|
+
file: e.file,
|
|
750
|
+
error: e.message
|
|
751
|
+
}));
|
|
752
|
+
let outputStr;
|
|
753
|
+
if (output === "json") {
|
|
754
|
+
outputStr = formatJson({ results: formattedResults, summary, parseErrors });
|
|
755
|
+
} else {
|
|
756
|
+
const parts = [];
|
|
757
|
+
parts.push(chalk.cyan(`[Proxy: ${proxyConfig.url}]`));
|
|
758
|
+
parts.push(formatTestResults(formattedResults, summary, { format: output, verbose: params.verbose }));
|
|
759
|
+
if (parseErrors.length > 0) {
|
|
760
|
+
parts.push(`
|
|
761
|
+
${parseErrors.length} file(s) failed to parse:`);
|
|
762
|
+
parseErrors.forEach(({ file, error: error2 }) => parts.push(` ${file}: ${error2}`));
|
|
763
|
+
}
|
|
764
|
+
outputStr = parts.join("\n");
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
success: summary.failed === 0 && parseErrors.length === 0,
|
|
768
|
+
output: outputStr,
|
|
769
|
+
data: { results: formattedResults, summary, parseErrors }
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
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").option("--no-proxy", "Disable proxy for this execution").option("--proxy-url <url>", "Override proxy URL for this execution").action(async (files, options) => {
|
|
577
773
|
setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
|
|
578
|
-
const spinner = options.quiet ? null : ora("Running tests...").start();
|
|
774
|
+
const spinner = options.quiet || options.output === "json" ? null : ora("Running tests...").start();
|
|
579
775
|
try {
|
|
580
776
|
const result = await executeRun({
|
|
581
777
|
files,
|
|
@@ -584,6 +780,10 @@ const runCommand = new Command("run").description("Execute test cases and report
|
|
|
584
780
|
verbose: options.verbose,
|
|
585
781
|
quiet: options.quiet,
|
|
586
782
|
failFast: options.failFast,
|
|
783
|
+
config: options.config,
|
|
784
|
+
noAutoInstall: options.noAutoInstall,
|
|
785
|
+
noProxy: options.noProxy,
|
|
786
|
+
proxyUrl: options.proxyUrl,
|
|
587
787
|
env: options.env,
|
|
588
788
|
params: options.params
|
|
589
789
|
});
|
|
@@ -604,9 +804,20 @@ const runCommand = new Command("run").description("Execute test cases and report
|
|
|
604
804
|
}
|
|
605
805
|
process.exit(result.success ? 0 : 1);
|
|
606
806
|
} catch (err) {
|
|
607
|
-
spinner?.
|
|
807
|
+
spinner?.stop();
|
|
608
808
|
const message = err instanceof Error ? err.message : String(err);
|
|
609
|
-
|
|
809
|
+
if (options.output === "json") {
|
|
810
|
+
const errorOutput = formatJson({
|
|
811
|
+
results: [],
|
|
812
|
+
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
813
|
+
parseErrors: [],
|
|
814
|
+
error: message
|
|
815
|
+
});
|
|
816
|
+
logger.log(errorOutput);
|
|
817
|
+
} else {
|
|
818
|
+
spinner?.fail("Execution failed");
|
|
819
|
+
logger.error(message);
|
|
820
|
+
}
|
|
610
821
|
process.exit(2);
|
|
611
822
|
}
|
|
612
823
|
});
|
|
@@ -617,12 +828,108 @@ function parseKeyValue(value, previous = {}) {
|
|
|
617
828
|
}
|
|
618
829
|
return previous;
|
|
619
830
|
}
|
|
831
|
+
async function executeParseViaProxy(params, proxyConfig) {
|
|
832
|
+
const output = params.output ?? "text";
|
|
833
|
+
params.verbose ?? false;
|
|
834
|
+
const { files: fileDescriptors } = await discoverTSpecFiles(params.files);
|
|
835
|
+
const allFiles = fileDescriptors.map((f) => f.path);
|
|
836
|
+
if (allFiles.length === 0) {
|
|
837
|
+
return {
|
|
838
|
+
success: false,
|
|
839
|
+
output: "No .tcase files found",
|
|
840
|
+
data: {
|
|
841
|
+
testCases: [],
|
|
842
|
+
parseErrors: [],
|
|
843
|
+
summary: { totalFiles: 0, totalTestCases: 0, parseErrors: 0 }
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
const fileResult = readTestFiles(allFiles);
|
|
848
|
+
if (fileResult.errors.length > 0 && Object.keys(fileResult.fileContents).length === 0) {
|
|
849
|
+
return {
|
|
850
|
+
success: false,
|
|
851
|
+
output: `Failed to read test files: ${fileResult.errors.map((e) => e.error).join(", ")}`,
|
|
852
|
+
data: {
|
|
853
|
+
testCases: [],
|
|
854
|
+
parseErrors: fileResult.errors.map((e) => ({ file: e.file, error: e.error })),
|
|
855
|
+
summary: { totalFiles: 0, totalTestCases: 0, parseErrors: fileResult.errors.length }
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const client = new ProxyClient({
|
|
860
|
+
url: proxyConfig.url,
|
|
861
|
+
timeout: proxyConfig.timeout,
|
|
862
|
+
headers: proxyConfig.headers
|
|
863
|
+
});
|
|
864
|
+
const result = await client.executeParse(
|
|
865
|
+
allFiles,
|
|
866
|
+
fileResult.fileContents,
|
|
867
|
+
{ env: params.env, params: params.params }
|
|
868
|
+
);
|
|
869
|
+
if (!result.success || !result.data) {
|
|
870
|
+
const errorMsg = result.error ? `${result.error.code}: ${result.error.message}${result.error.details ? ` (${result.error.details})` : ""}` : "Unknown proxy error";
|
|
871
|
+
return {
|
|
872
|
+
success: false,
|
|
873
|
+
output: output === "json" ? formatJson({ error: errorMsg, testCases: [], errors: [], summary: { totalFiles: 0, totalTestCases: 0, parseErrors: 0 } }) : `Proxy error: ${errorMsg}`,
|
|
874
|
+
data: {
|
|
875
|
+
testCases: [],
|
|
876
|
+
parseErrors: [],
|
|
877
|
+
summary: { totalFiles: 0, totalTestCases: 0, parseErrors: 0 }
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const proxyResponse = result.data;
|
|
882
|
+
const testCases = proxyResponse.testCases || [];
|
|
883
|
+
const parseErrors = (proxyResponse.parseErrors || []).map((e) => ({
|
|
884
|
+
file: e.file,
|
|
885
|
+
error: e.message
|
|
886
|
+
}));
|
|
887
|
+
const summary = proxyResponse.summary || {
|
|
888
|
+
totalFiles: allFiles.length,
|
|
889
|
+
totalTestCases: testCases.length,
|
|
890
|
+
parseErrors: parseErrors.length
|
|
891
|
+
};
|
|
892
|
+
let outputStr;
|
|
893
|
+
if (output === "json") {
|
|
894
|
+
outputStr = formatJson({ testCases, errors: parseErrors, summary });
|
|
895
|
+
} else {
|
|
896
|
+
const parts = [];
|
|
897
|
+
parts.push(chalk.cyan(`[Proxy: ${proxyConfig.url}]`));
|
|
898
|
+
parts.push(`Parsed ${testCases.length} test case(s) from ${summary.totalFiles} file(s)`);
|
|
899
|
+
parts.push("");
|
|
900
|
+
for (const testCase of testCases) {
|
|
901
|
+
parts.push(formatParsedTestCase(testCase, { format: output }));
|
|
902
|
+
parts.push("");
|
|
903
|
+
}
|
|
904
|
+
if (parseErrors.length > 0) {
|
|
905
|
+
parts.push(`${parseErrors.length} file(s) failed to parse:`);
|
|
906
|
+
parseErrors.forEach(({ file, error: error2 }) => parts.push(` ${file}: ${error2}`));
|
|
907
|
+
}
|
|
908
|
+
outputStr = parts.join("\n");
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
success: parseErrors.length === 0,
|
|
912
|
+
output: outputStr,
|
|
913
|
+
data: { testCases, parseErrors, summary }
|
|
914
|
+
};
|
|
915
|
+
}
|
|
620
916
|
async function executeParse(params) {
|
|
621
917
|
clearTemplateCache();
|
|
622
918
|
const output = params.output ?? "text";
|
|
623
919
|
params.verbose ?? false;
|
|
624
920
|
const env = params.env ?? {};
|
|
625
921
|
const paramValues = params.params ?? {};
|
|
922
|
+
if (!params.noProxy) {
|
|
923
|
+
const configPath = params.config || findConfigFile() || void 0;
|
|
924
|
+
const config2 = await loadConfig(configPath);
|
|
925
|
+
const proxyUrl = params.proxyUrl;
|
|
926
|
+
if (proxyUrl || isProxyEnabled(config2, "parse")) {
|
|
927
|
+
const proxyConfig = proxyUrl ? { url: proxyUrl, timeout: 3e4, headers: {} } : getProxyConfig(config2);
|
|
928
|
+
if (proxyConfig) {
|
|
929
|
+
return executeParseViaProxy(params, proxyConfig);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
626
933
|
const { files: fileDescriptors } = await discoverTSpecFiles(params.files);
|
|
627
934
|
if (fileDescriptors.length === 0) {
|
|
628
935
|
return {
|
|
@@ -674,7 +981,7 @@ async function executeParse(params) {
|
|
|
674
981
|
data: { testCases: allTestCases, parseErrors, summary }
|
|
675
982
|
};
|
|
676
983
|
}
|
|
677
|
-
const parseCommand = new Command("parse").description("Parse and display test case information without execution").argument("<files...>", "Files or glob patterns to parse").option("-o, --output <format>", "Output format: json, text", "text").option("-v, --verbose", "Show detailed information").option("-q, --quiet", "Minimal output").option("-e, --env <key=value>", "Environment variables", parseKeyValue, {}).option("-p, --params <key=value>", "Parameters", parseKeyValue, {}).action(async (files, options) => {
|
|
984
|
+
const parseCommand = new Command("parse").description("Parse and display test case information without execution").argument("<files...>", "Files or glob patterns to parse").option("-o, --output <format>", "Output format: json, text", "text").option("-v, --verbose", "Show detailed information").option("-q, --quiet", "Minimal output").option("-e, --env <key=value>", "Environment variables", parseKeyValue, {}).option("-p, --params <key=value>", "Parameters", parseKeyValue, {}).option("--config <path>", "Path to tspec.config.json").option("--no-proxy", "Disable proxy for this execution").option("--proxy-url <url>", "Override proxy URL for this execution").action(async (files, options) => {
|
|
678
985
|
setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
|
|
679
986
|
const spinner = options.quiet ? null : ora("Parsing...").start();
|
|
680
987
|
try {
|
|
@@ -683,7 +990,10 @@ const parseCommand = new Command("parse").description("Parse and display test ca
|
|
|
683
990
|
output: options.output,
|
|
684
991
|
verbose: options.verbose,
|
|
685
992
|
env: options.env,
|
|
686
|
-
params: options.params
|
|
993
|
+
params: options.params,
|
|
994
|
+
noProxy: options.noProxy,
|
|
995
|
+
proxyUrl: options.proxyUrl,
|
|
996
|
+
config: options.config
|
|
687
997
|
});
|
|
688
998
|
spinner?.stop();
|
|
689
999
|
logger.log(result.output);
|
|
@@ -15138,6 +15448,262 @@ const mcpCommand = new Command("mcp").description("Start MCP server for tool int
|
|
|
15138
15448
|
setLoggerOptions({ quiet: true });
|
|
15139
15449
|
await startMcpServer();
|
|
15140
15450
|
});
|
|
15451
|
+
async function executePluginList(params) {
|
|
15452
|
+
const output = params.output ?? "text";
|
|
15453
|
+
const pluginManager = new PluginManager(version$1);
|
|
15454
|
+
const localConfigPath = findLocalConfigFile();
|
|
15455
|
+
const globalConfigPath = findGlobalConfigFile();
|
|
15456
|
+
const configPath = params.config || localConfigPath || globalConfigPath;
|
|
15457
|
+
let loadSummary;
|
|
15458
|
+
if (configPath) {
|
|
15459
|
+
loadSummary = await pluginManager.initialize(configPath);
|
|
15460
|
+
}
|
|
15461
|
+
const plugins = pluginManager.list();
|
|
15462
|
+
const protocols = pluginManager.listProtocols();
|
|
15463
|
+
let healthReports;
|
|
15464
|
+
if (params.health) {
|
|
15465
|
+
healthReports = await pluginManager.healthCheck();
|
|
15466
|
+
}
|
|
15467
|
+
const data = {
|
|
15468
|
+
plugins: plugins.map((p) => ({
|
|
15469
|
+
name: p.name,
|
|
15470
|
+
version: p.version,
|
|
15471
|
+
description: p.description,
|
|
15472
|
+
protocols: p.protocols,
|
|
15473
|
+
author: p.author,
|
|
15474
|
+
homepage: p.homepage
|
|
15475
|
+
})),
|
|
15476
|
+
protocols,
|
|
15477
|
+
configPath: configPath || void 0,
|
|
15478
|
+
configSources: {
|
|
15479
|
+
local: localConfigPath || void 0,
|
|
15480
|
+
global: globalConfigPath || void 0
|
|
15481
|
+
},
|
|
15482
|
+
pluginsDir: PLUGINS_DIR,
|
|
15483
|
+
health: healthReports
|
|
15484
|
+
};
|
|
15485
|
+
let outputStr;
|
|
15486
|
+
if (output === "json") {
|
|
15487
|
+
outputStr = JSON.stringify(data, null, 2);
|
|
15488
|
+
} else {
|
|
15489
|
+
outputStr = formatPluginListText(data, params.verbose ?? false, loadSummary);
|
|
15490
|
+
}
|
|
15491
|
+
return {
|
|
15492
|
+
success: true,
|
|
15493
|
+
output: outputStr,
|
|
15494
|
+
data
|
|
15495
|
+
};
|
|
15496
|
+
}
|
|
15497
|
+
function formatPluginListText(data, verbose, loadSummary) {
|
|
15498
|
+
const lines = [];
|
|
15499
|
+
lines.push(chalk.bold("\nTSpec Plugins\n"));
|
|
15500
|
+
lines.push(chalk.bold("Config:"));
|
|
15501
|
+
if (data.configSources?.local) {
|
|
15502
|
+
lines.push(chalk.gray(` Local: ${data.configSources.local}`));
|
|
15503
|
+
} else {
|
|
15504
|
+
lines.push(chalk.gray(" Local: (none)"));
|
|
15505
|
+
}
|
|
15506
|
+
if (data.configSources?.global) {
|
|
15507
|
+
lines.push(chalk.gray(` Global: ${data.configSources.global}`));
|
|
15508
|
+
} else {
|
|
15509
|
+
lines.push(chalk.gray(" Global: (none)"));
|
|
15510
|
+
}
|
|
15511
|
+
lines.push(chalk.gray(` Plugins dir: ${data.pluginsDir}`));
|
|
15512
|
+
if (loadSummary) {
|
|
15513
|
+
lines.push("");
|
|
15514
|
+
lines.push(chalk.gray(`Discovered: ${loadSummary.total}, Loaded: ${loadSummary.loaded}`));
|
|
15515
|
+
if (loadSummary.installed && loadSummary.installed > 0) {
|
|
15516
|
+
lines.push(chalk.green(`Installed: ${loadSummary.installed} plugin(s)`));
|
|
15517
|
+
}
|
|
15518
|
+
if (loadSummary.failed > 0) {
|
|
15519
|
+
lines.push(chalk.red(`Failed: ${loadSummary.failed}`));
|
|
15520
|
+
for (const error2 of loadSummary.errors) {
|
|
15521
|
+
lines.push(chalk.red(` ${error2.plugin}: ${error2.error}`));
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15524
|
+
if (loadSummary.installErrors && loadSummary.installErrors.length > 0) {
|
|
15525
|
+
lines.push(chalk.red(`Install failures:`));
|
|
15526
|
+
for (const error2 of loadSummary.installErrors) {
|
|
15527
|
+
lines.push(chalk.red(` ${error2.plugin}: ${error2.error}`));
|
|
15528
|
+
}
|
|
15529
|
+
}
|
|
15530
|
+
}
|
|
15531
|
+
lines.push("");
|
|
15532
|
+
if (data.plugins.length === 0) {
|
|
15533
|
+
lines.push(chalk.yellow("No plugins loaded."));
|
|
15534
|
+
lines.push(chalk.gray("Add plugins to your tspec.config.json:"));
|
|
15535
|
+
lines.push(chalk.gray(" {"));
|
|
15536
|
+
lines.push(chalk.gray(' "plugins": ["@tspec/http", "@tspec/web"]'));
|
|
15537
|
+
lines.push(chalk.gray(" }"));
|
|
15538
|
+
} else {
|
|
15539
|
+
for (const plugin of data.plugins) {
|
|
15540
|
+
lines.push(`${chalk.cyan(plugin.name)} ${chalk.gray(`v${plugin.version}`)}`);
|
|
15541
|
+
if (verbose && plugin.description) {
|
|
15542
|
+
lines.push(` ${plugin.description}`);
|
|
15543
|
+
}
|
|
15544
|
+
lines.push(` Protocols: ${plugin.protocols.join(", ")}`);
|
|
15545
|
+
if (verbose) {
|
|
15546
|
+
if (plugin.author) {
|
|
15547
|
+
lines.push(` Author: ${plugin.author}`);
|
|
15548
|
+
}
|
|
15549
|
+
if (plugin.homepage) {
|
|
15550
|
+
lines.push(` Homepage: ${plugin.homepage}`);
|
|
15551
|
+
}
|
|
15552
|
+
}
|
|
15553
|
+
lines.push("");
|
|
15554
|
+
}
|
|
15555
|
+
}
|
|
15556
|
+
if (data.health) {
|
|
15557
|
+
lines.push(chalk.bold("Health Check\n"));
|
|
15558
|
+
for (const report of data.health) {
|
|
15559
|
+
const status = report.healthy ? chalk.green("✓ Healthy") : chalk.red("✗ Unhealthy");
|
|
15560
|
+
lines.push(`${chalk.cyan(report.plugin)}: ${status}`);
|
|
15561
|
+
if (report.message) {
|
|
15562
|
+
lines.push(` ${report.message}`);
|
|
15563
|
+
}
|
|
15564
|
+
}
|
|
15565
|
+
lines.push("");
|
|
15566
|
+
}
|
|
15567
|
+
if (data.protocols.length > 0) {
|
|
15568
|
+
lines.push(chalk.bold("Supported Protocols: ") + data.protocols.join(", "));
|
|
15569
|
+
}
|
|
15570
|
+
return lines.join("\n");
|
|
15571
|
+
}
|
|
15572
|
+
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) => {
|
|
15573
|
+
try {
|
|
15574
|
+
const result = await executePluginList({
|
|
15575
|
+
output: options.output,
|
|
15576
|
+
verbose: options.verbose,
|
|
15577
|
+
health: options.health,
|
|
15578
|
+
config: options.config
|
|
15579
|
+
});
|
|
15580
|
+
logger.log(result.output);
|
|
15581
|
+
} catch (err) {
|
|
15582
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15583
|
+
logger.error(`Failed to list plugins: ${message}`);
|
|
15584
|
+
process.exit(2);
|
|
15585
|
+
}
|
|
15586
|
+
});
|
|
15587
|
+
function loadConfigFile(configPath) {
|
|
15588
|
+
if (!existsSync(configPath)) {
|
|
15589
|
+
return { plugins: [], pluginOptions: {} };
|
|
15590
|
+
}
|
|
15591
|
+
try {
|
|
15592
|
+
const content = readFileSync(configPath, "utf-8");
|
|
15593
|
+
return JSON.parse(content);
|
|
15594
|
+
} catch {
|
|
15595
|
+
return { plugins: [], pluginOptions: {} };
|
|
15596
|
+
}
|
|
15597
|
+
}
|
|
15598
|
+
function saveConfigFile(configPath, config2) {
|
|
15599
|
+
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
15600
|
+
if (!existsSync(dir)) {
|
|
15601
|
+
mkdirSync(dir, { recursive: true });
|
|
15602
|
+
}
|
|
15603
|
+
writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
15604
|
+
}
|
|
15605
|
+
function addPluginToConfig(config2, pluginName) {
|
|
15606
|
+
if (!config2.plugins) {
|
|
15607
|
+
config2.plugins = [];
|
|
15608
|
+
}
|
|
15609
|
+
if (config2.plugins.includes(pluginName)) {
|
|
15610
|
+
return false;
|
|
15611
|
+
}
|
|
15612
|
+
config2.plugins.push(pluginName);
|
|
15613
|
+
return true;
|
|
15614
|
+
}
|
|
15615
|
+
async function executePluginInstall(params) {
|
|
15616
|
+
const { pluginName, output = "text", global: useGlobal = false, config: customConfig } = params;
|
|
15617
|
+
let configPath;
|
|
15618
|
+
if (customConfig) {
|
|
15619
|
+
configPath = customConfig;
|
|
15620
|
+
} else if (useGlobal) {
|
|
15621
|
+
configPath = GLOBAL_CONFIG_PATH;
|
|
15622
|
+
} else {
|
|
15623
|
+
const localConfig = findLocalConfigFile();
|
|
15624
|
+
configPath = localConfig || GLOBAL_CONFIG_PATH;
|
|
15625
|
+
}
|
|
15626
|
+
const alreadyInstalled = isPluginInstalled(pluginName);
|
|
15627
|
+
let installed = false;
|
|
15628
|
+
let installError;
|
|
15629
|
+
if (!alreadyInstalled) {
|
|
15630
|
+
const result = await installPlugin(pluginName);
|
|
15631
|
+
installed = result.success;
|
|
15632
|
+
if (!result.success) {
|
|
15633
|
+
installError = result.error;
|
|
15634
|
+
}
|
|
15635
|
+
} else {
|
|
15636
|
+
installed = true;
|
|
15637
|
+
}
|
|
15638
|
+
let configUpdated = false;
|
|
15639
|
+
if (installed) {
|
|
15640
|
+
const config2 = loadConfigFile(configPath);
|
|
15641
|
+
configUpdated = addPluginToConfig(config2, pluginName);
|
|
15642
|
+
if (configUpdated) {
|
|
15643
|
+
saveConfigFile(configPath, config2);
|
|
15644
|
+
}
|
|
15645
|
+
}
|
|
15646
|
+
const data = {
|
|
15647
|
+
plugin: pluginName,
|
|
15648
|
+
installed,
|
|
15649
|
+
configUpdated,
|
|
15650
|
+
configPath: installed ? configPath : void 0,
|
|
15651
|
+
error: installError
|
|
15652
|
+
};
|
|
15653
|
+
let outputStr;
|
|
15654
|
+
if (output === "json") {
|
|
15655
|
+
outputStr = JSON.stringify(data, null, 2);
|
|
15656
|
+
} else {
|
|
15657
|
+
if (!installed) {
|
|
15658
|
+
outputStr = chalk.red(`Failed to install ${pluginName}: ${installError || "Unknown error"}`);
|
|
15659
|
+
} else if (alreadyInstalled && !configUpdated) {
|
|
15660
|
+
outputStr = chalk.yellow(`Plugin ${pluginName} is already installed and configured.`);
|
|
15661
|
+
} else if (alreadyInstalled && configUpdated) {
|
|
15662
|
+
outputStr = [
|
|
15663
|
+
chalk.green(`Plugin ${pluginName} is already installed.`),
|
|
15664
|
+
chalk.green(`Added to config: ${configPath}`)
|
|
15665
|
+
].join("\n");
|
|
15666
|
+
} else if (configUpdated) {
|
|
15667
|
+
outputStr = [
|
|
15668
|
+
chalk.green(`Successfully installed ${pluginName}`),
|
|
15669
|
+
chalk.green(`Added to config: ${configPath}`)
|
|
15670
|
+
].join("\n");
|
|
15671
|
+
} else {
|
|
15672
|
+
outputStr = [
|
|
15673
|
+
chalk.green(`Successfully installed ${pluginName}`),
|
|
15674
|
+
chalk.yellow(`Plugin already in config: ${configPath}`)
|
|
15675
|
+
].join("\n");
|
|
15676
|
+
}
|
|
15677
|
+
}
|
|
15678
|
+
return {
|
|
15679
|
+
success: installed,
|
|
15680
|
+
output: outputStr,
|
|
15681
|
+
data
|
|
15682
|
+
};
|
|
15683
|
+
}
|
|
15684
|
+
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) => {
|
|
15685
|
+
const spinner = ora(`Installing ${plugin}...`).start();
|
|
15686
|
+
try {
|
|
15687
|
+
const result = await executePluginInstall({
|
|
15688
|
+
pluginName: plugin,
|
|
15689
|
+
output: options.output,
|
|
15690
|
+
global: options.global,
|
|
15691
|
+
config: options.config
|
|
15692
|
+
});
|
|
15693
|
+
spinner.stop();
|
|
15694
|
+
logger.log(result.output);
|
|
15695
|
+
process.exit(result.success ? 0 : 1);
|
|
15696
|
+
} catch (err) {
|
|
15697
|
+
spinner.stop();
|
|
15698
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15699
|
+
if (options.output === "json") {
|
|
15700
|
+
logger.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
15701
|
+
} else {
|
|
15702
|
+
logger.error(`Failed to install plugin: ${message}`);
|
|
15703
|
+
}
|
|
15704
|
+
process.exit(2);
|
|
15705
|
+
}
|
|
15706
|
+
});
|
|
15141
15707
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
15142
15708
|
const __dirname$1 = dirname(__filename$1);
|
|
15143
15709
|
const packageJson = JSON.parse(readFileSync(join(__dirname$1, "../package.json"), "utf-8"));
|
|
@@ -15148,5 +15714,7 @@ program.addCommand(runCommand);
|
|
|
15148
15714
|
program.addCommand(parseCommand);
|
|
15149
15715
|
program.addCommand(listCommand);
|
|
15150
15716
|
program.addCommand(mcpCommand);
|
|
15717
|
+
program.addCommand(pluginListCommand);
|
|
15718
|
+
program.addCommand(pluginInstallCommand);
|
|
15151
15719
|
await program.parseAsync();
|
|
15152
15720
|
//# sourceMappingURL=index.js.map
|