@boolesai/tspec-cli 1.3.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/dist/index.js CHANGED
@@ -3,10 +3,10 @@ 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, getPluginManager, version as version$1, registry as registry$1, executeSuite, parseTestCases, scheduler, PluginManager } 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";
9
8
  import { findConfigFile, findLocalConfigFile, findGlobalConfigFile, PLUGINS_DIR, isPluginInstalled, installPlugin, GLOBAL_CONFIG_PATH } from "@boolesai/tspec/plugin";
9
+ import { glob } from "glob";
10
10
  import process$2 from "node:process";
11
11
  async function discoverTSpecFiles(patterns, cwd) {
12
12
  const workingDir = process.cwd();
@@ -233,7 +233,7 @@ function error(message, ...args) {
233
233
  console.error(chalk.red(`[error] ${message}`), ...args);
234
234
  }
235
235
  function log(message, ...args) {
236
- console.error(message, ...args);
236
+ console.log(message, ...args);
237
237
  }
238
238
  function newline() {
239
239
  if (!globalOptions.quiet) {
@@ -250,8 +250,77 @@ const logger = {
250
250
  newline,
251
251
  setOptions: setLoggerOptions
252
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
+ }
253
311
  async function executeValidate(params) {
254
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
+ }
255
324
  const { files: fileDescriptors } = await discoverTSpecFiles(params.files);
256
325
  if (fileDescriptors.length === 0) {
257
326
  return {
@@ -279,13 +348,16 @@ async function executeValidate(params) {
279
348
  data: { results }
280
349
  };
281
350
  }
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) => {
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) => {
283
352
  setLoggerOptions({ quiet: options.quiet });
284
353
  const spinner = options.quiet ? null : ora("Validating...").start();
285
354
  try {
286
355
  const result = await executeValidate({
287
356
  files,
288
- output: options.output
357
+ output: options.output,
358
+ noProxy: options.noProxy,
359
+ proxyUrl: options.proxyUrl,
360
+ config: options.config
289
361
  });
290
362
  spinner?.stop();
291
363
  logger.log(result.output);
@@ -355,6 +427,23 @@ async function runFileTestCasesInternal(descriptor, env, params, concurrency, fa
355
427
  async function executeRun(params) {
356
428
  clearTemplateCache();
357
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
+ }
358
447
  if (configPath) {
359
448
  const pluginManager = getPluginManager(version$1);
360
449
  await pluginManager.initialize(configPath, {
@@ -474,6 +563,7 @@ ${parseErrors.length} file(s) failed to parse:`);
474
563
  }
475
564
  async function executeSuiteRun(suiteFiles, additionalTspecFiles, options) {
476
565
  const { env, params: paramValues, failFast, output, verbose, quiet } = options;
566
+ const isJsonOutput = output === "json";
477
567
  const allResults = [];
478
568
  const parseErrors = [];
479
569
  let totalTests = 0;
@@ -487,14 +577,15 @@ async function executeSuiteRun(suiteFiles, additionalTspecFiles, options) {
487
577
  const suiteResult = await executeSuite(suiteDescriptor.path, {
488
578
  env,
489
579
  params: paramValues,
490
- onSuiteStart: (name) => {
580
+ silent: isJsonOutput,
581
+ onSuiteStart: isJsonOutput ? void 0 : (name) => {
491
582
  if (!quiet) logger.log(chalk.blue(`
492
583
  Suite: ${name}`));
493
584
  },
494
- onTestStart: (file) => {
585
+ onTestStart: isJsonOutput ? void 0 : (file) => {
495
586
  if (verbose) logger.log(chalk.gray(` Running: ${file}`));
496
587
  },
497
- onTestComplete: (file, result) => {
588
+ onTestComplete: isJsonOutput ? void 0 : (file, result) => {
498
589
  const statusIcon = result.status === "passed" ? chalk.green("✓") : chalk.red("✗");
499
590
  if (!quiet) logger.log(` ${statusIcon} ${result.name} (${result.duration}ms)`);
500
591
  }
@@ -582,9 +673,105 @@ ${parseErrors.length} file(s) failed to parse:`);
582
673
  data: { results: allResults, summary, parseErrors }
583
674
  };
584
675
  }
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) => {
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) => {
586
773
  setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
587
- const spinner = options.quiet ? null : ora("Running tests...").start();
774
+ const spinner = options.quiet || options.output === "json" ? null : ora("Running tests...").start();
588
775
  try {
589
776
  const result = await executeRun({
590
777
  files,
@@ -595,6 +782,8 @@ const runCommand = new Command("run").description("Execute test cases and report
595
782
  failFast: options.failFast,
596
783
  config: options.config,
597
784
  noAutoInstall: options.noAutoInstall,
785
+ noProxy: options.noProxy,
786
+ proxyUrl: options.proxyUrl,
598
787
  env: options.env,
599
788
  params: options.params
600
789
  });
@@ -639,12 +828,108 @@ function parseKeyValue(value, previous = {}) {
639
828
  }
640
829
  return previous;
641
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
+ }
642
916
  async function executeParse(params) {
643
917
  clearTemplateCache();
644
918
  const output = params.output ?? "text";
645
919
  params.verbose ?? false;
646
920
  const env = params.env ?? {};
647
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
+ }
648
933
  const { files: fileDescriptors } = await discoverTSpecFiles(params.files);
649
934
  if (fileDescriptors.length === 0) {
650
935
  return {
@@ -696,7 +981,7 @@ async function executeParse(params) {
696
981
  data: { testCases: allTestCases, parseErrors, summary }
697
982
  };
698
983
  }
699
- 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) => {
700
985
  setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
701
986
  const spinner = options.quiet ? null : ora("Parsing...").start();
702
987
  try {
@@ -705,7 +990,10 @@ const parseCommand = new Command("parse").description("Parse and display test ca
705
990
  output: options.output,
706
991
  verbose: options.verbose,
707
992
  env: options.env,
708
- params: options.params
993
+ params: options.params,
994
+ noProxy: options.noProxy,
995
+ proxyUrl: options.proxyUrl,
996
+ config: options.config
709
997
  });
710
998
  spinner?.stop();
711
999
  logger.log(result.output);