@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/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.error(message, ...args);
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
- onSuiteStart: (name) => {
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
- 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").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) => {
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?.fail("Execution failed");
807
+ spinner?.stop();
608
808
  const message = err instanceof Error ? err.message : String(err);
609
- logger.error(message);
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