@boolesai/tspec-cli 0.0.1 → 0.0.4

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,46 +1,49 @@
1
1
  import { Command } from "commander";
2
2
  import ora from "ora";
3
- import { validateTestCase, parseTestCases, scheduler, registry } from "@boolesai/tspec";
3
+ import { getTypeFromFilePath, validateTestCase, clearTemplateCache, parseTestCases, scheduler, registry } from "@boolesai/tspec";
4
4
  import { glob } from "glob";
5
- import { isAbsolute, resolve } from "path";
5
+ import { isAbsolute, resolve, basename, relative } from "path";
6
6
  import { existsSync, statSync } from "fs";
7
7
  import chalk from "chalk";
8
- async function resolveFiles(patterns, cwd) {
8
+ async function discoverTSpecFiles(patterns, cwd) {
9
9
  const workingDir = process.cwd();
10
- const files = [];
10
+ const filePaths = [];
11
11
  const errors = [];
12
12
  for (const pattern of patterns) {
13
13
  const absolutePath = isAbsolute(pattern) ? pattern : resolve(workingDir, pattern);
14
14
  if (existsSync(absolutePath)) {
15
15
  const stat = statSync(absolutePath);
16
16
  if (stat.isFile()) {
17
- files.push(absolutePath);
17
+ if (absolutePath.endsWith(".tspec")) {
18
+ filePaths.push(absolutePath);
19
+ }
18
20
  continue;
19
21
  } else if (stat.isDirectory()) {
20
22
  const dirFiles = await glob("**/*.tspec", { cwd: absolutePath, absolute: true });
21
23
  if (dirFiles.length === 0) {
22
24
  errors.push(`No .tspec files found in directory: ${pattern}`);
23
25
  } else {
24
- files.push(...dirFiles);
26
+ filePaths.push(...dirFiles);
25
27
  }
26
28
  continue;
27
29
  }
28
30
  }
29
31
  const matches = await glob(pattern, { cwd: workingDir, absolute: true });
30
- if (matches.length === 0) {
31
- errors.push(`No files matched pattern: ${pattern}`);
32
+ const tspecMatches = matches.filter((f) => f.endsWith(".tspec"));
33
+ if (tspecMatches.length === 0) {
34
+ errors.push(`No .tspec files matched pattern: ${pattern}`);
32
35
  } else {
33
- files.push(...matches);
36
+ filePaths.push(...tspecMatches);
34
37
  }
35
38
  }
36
- const uniqueFiles = [...new Set(files)];
37
- return { files: uniqueFiles, errors };
38
- }
39
- function filterByExtension(files, extension) {
40
- return files.filter((f) => f.endsWith(extension));
41
- }
42
- function getTspecFiles(files) {
43
- return filterByExtension(files, ".tspec");
39
+ const uniquePaths = [...new Set(filePaths)];
40
+ const files = uniquePaths.map((filePath) => ({
41
+ path: filePath,
42
+ relativePath: relative(workingDir, filePath),
43
+ fileName: basename(filePath),
44
+ protocol: getTypeFromFilePath(filePath)
45
+ }));
46
+ return { files, errors };
44
47
  }
45
48
  function formatJson(data) {
46
49
  return JSON.stringify(data, null, 2);
@@ -175,21 +178,20 @@ const logger = {
175
178
  };
176
179
  const validateCommand = new Command("validate").description("Validate .tspec 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) => {
177
180
  setLoggerOptions({ quiet: options.quiet });
178
- const spinner = options.quiet ? null : ora("Resolving files...").start();
181
+ const spinner = options.quiet ? null : ora("Discovering files...").start();
179
182
  try {
180
- const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);
181
- const tspecFiles = getTspecFiles(resolvedFiles);
183
+ const { files: fileDescriptors, errors: resolveErrors } = await discoverTSpecFiles(files);
182
184
  if (resolveErrors.length > 0 && !options.quiet) {
183
185
  resolveErrors.forEach((err) => logger.warn(err));
184
186
  }
185
- if (tspecFiles.length === 0) {
187
+ if (fileDescriptors.length === 0) {
186
188
  spinner?.fail("No .tspec files found");
187
189
  process.exit(2);
188
190
  }
189
- if (spinner) spinner.text = `Validating ${tspecFiles.length} file(s)...`;
190
- const results = tspecFiles.map((file) => ({
191
- file,
192
- result: validateTestCase(file)
191
+ if (spinner) spinner.text = `Validating ${fileDescriptors.length} file(s)...`;
192
+ const results = fileDescriptors.map((descriptor) => ({
193
+ file: descriptor.path,
194
+ result: validateTestCase(descriptor.path)
193
195
  }));
194
196
  spinner?.stop();
195
197
  const output = formatValidationResults(results, { format: options.output });
@@ -222,81 +224,93 @@ function formatResult(result) {
222
224
  }))
223
225
  };
224
226
  }
227
+ async function runFileTestCases(descriptor, options, concurrency, spinner) {
228
+ const result = {
229
+ file: descriptor.path,
230
+ testCases: 0,
231
+ results: []
232
+ };
233
+ let testCases;
234
+ try {
235
+ testCases = parseTestCases(descriptor.path, {
236
+ env: options.env,
237
+ params: options.params
238
+ });
239
+ result.testCases = testCases.length;
240
+ } catch (err) {
241
+ result.parseError = err instanceof Error ? err.message : String(err);
242
+ return result;
243
+ }
244
+ if (testCases.length === 0) {
245
+ return result;
246
+ }
247
+ if (spinner) spinner.text = `Running: ${descriptor.relativePath} (${testCases.length} test(s))...`;
248
+ if (options.failFast) {
249
+ for (const testCase of testCases) {
250
+ try {
251
+ const scheduleResult = await scheduler.schedule([testCase], { concurrency: 1 });
252
+ result.results.push(...scheduleResult.results);
253
+ if (!scheduleResult.results[0]?.passed) {
254
+ break;
255
+ }
256
+ } catch {
257
+ break;
258
+ }
259
+ }
260
+ } else {
261
+ const scheduleResult = await scheduler.schedule(testCases, { concurrency });
262
+ result.results = scheduleResult.results;
263
+ }
264
+ return result;
265
+ }
225
266
  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) => {
226
267
  setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
268
+ clearTemplateCache();
227
269
  const concurrency = parseInt(options.concurrency || "5", 10);
228
- const spinner = options.quiet ? null : ora("Resolving files...").start();
270
+ const spinner = options.quiet ? null : ora("Discovering files...").start();
229
271
  try {
230
- const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);
231
- const tspecFiles = getTspecFiles(resolvedFiles);
272
+ const { files: fileDescriptors, errors: resolveErrors } = await discoverTSpecFiles(files);
232
273
  if (resolveErrors.length > 0 && !options.quiet) {
233
274
  resolveErrors.forEach((err) => logger.warn(err));
234
275
  }
235
- if (tspecFiles.length === 0) {
276
+ if (fileDescriptors.length === 0) {
236
277
  spinner?.fail("No .tspec files found");
237
278
  process.exit(2);
238
279
  }
239
- if (spinner) spinner.text = `Parsing ${tspecFiles.length} file(s)...`;
240
- const allTestCases = [];
280
+ if (spinner) spinner.text = `Running ${fileDescriptors.length} file(s)...`;
281
+ const allResults = [];
241
282
  const parseErrors = [];
242
- for (const file of tspecFiles) {
243
- try {
244
- const testCases = parseTestCases(file, {
245
- env: options.env,
246
- params: options.params
247
- });
248
- allTestCases.push(...testCases);
249
- } catch (err) {
250
- const message = err instanceof Error ? err.message : String(err);
251
- parseErrors.push({ file, error: message });
252
- }
253
- }
254
- if (allTestCases.length === 0) {
255
- spinner?.fail("No test cases found");
256
- if (parseErrors.length > 0) {
257
- parseErrors.forEach(({ file, error: error2 }) => logger.error(` ${file}: ${error2}`));
283
+ let totalTestCases = 0;
284
+ let stopped = false;
285
+ for (const descriptor of fileDescriptors) {
286
+ if (stopped) break;
287
+ const fileResult = await runFileTestCases(descriptor, options, concurrency, spinner);
288
+ if (fileResult.parseError) {
289
+ parseErrors.push({ file: fileResult.file, error: fileResult.parseError });
290
+ continue;
258
291
  }
259
- process.exit(2);
260
- }
261
- if (spinner) spinner.text = `Running ${allTestCases.length} test(s) with concurrency ${concurrency}...`;
262
- let scheduleResult;
263
- if (options.failFast) {
264
- const results = [];
265
- let stopped = false;
266
- for (const testCase of allTestCases) {
267
- if (stopped) break;
268
- if (spinner) spinner.text = `Running: ${testCase.id}...`;
269
- try {
270
- const result = await scheduler.schedule([testCase], { concurrency: 1 });
271
- results.push(...result.results);
272
- if (!result.results[0]?.passed) {
273
- stopped = true;
274
- }
275
- } catch (err) {
276
- stopped = true;
277
- }
292
+ totalTestCases += fileResult.testCases;
293
+ allResults.push(...fileResult.results);
294
+ if (options.failFast && fileResult.results.some((r) => !r.passed)) {
295
+ stopped = true;
278
296
  }
279
- const passed = results.filter((r) => r.passed).length;
280
- const failed = results.length - passed;
281
- scheduleResult = {
282
- results,
283
- duration: 0,
284
- summary: {
285
- total: results.length,
286
- passed,
287
- failed,
288
- passRate: results.length > 0 ? passed / results.length * 100 : 0
289
- }
290
- };
291
- } else {
292
- scheduleResult = await scheduler.schedule(allTestCases, { concurrency });
293
297
  }
294
298
  spinner?.stop();
295
- const formattedResults = scheduleResult.results.map(formatResult);
299
+ if (totalTestCases === 0 && parseErrors.length === fileDescriptors.length) {
300
+ logger.error("No test cases found - all files failed to parse");
301
+ parseErrors.forEach(({ file, error: error2 }) => logger.error(` ${file}: ${error2}`));
302
+ process.exit(2);
303
+ }
304
+ const passed = allResults.filter((r) => r.passed).length;
305
+ const failed = allResults.length - passed;
296
306
  const summary = {
297
- ...scheduleResult.summary,
298
- duration: scheduleResult.duration
307
+ total: allResults.length,
308
+ passed,
309
+ failed,
310
+ passRate: allResults.length > 0 ? passed / allResults.length * 100 : 0,
311
+ duration: 0
299
312
  };
313
+ const formattedResults = allResults.map(formatResult);
300
314
  if (options.output === "json") {
301
315
  logger.log(formatJson({
302
316
  results: formattedResults,
@@ -322,7 +336,7 @@ const runCommand = new Command("run").description("Execute test cases and report
322
336
  }
323
337
  }
324
338
  }
325
- process.exit(scheduleResult.summary.failed > 0 ? 1 : 0);
339
+ process.exit(failed > 0 ? 1 : 0);
326
340
  } catch (err) {
327
341
  spinner?.fail("Execution failed");
328
342
  const message = err instanceof Error ? err.message : String(err);
@@ -339,30 +353,30 @@ function parseKeyValue(value, previous = {}) {
339
353
  }
340
354
  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) => {
341
355
  setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
342
- const spinner = options.quiet ? null : ora("Resolving files...").start();
356
+ clearTemplateCache();
357
+ const spinner = options.quiet ? null : ora("Discovering files...").start();
343
358
  try {
344
- const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);
345
- const tspecFiles = getTspecFiles(resolvedFiles);
359
+ const { files: fileDescriptors, errors: resolveErrors } = await discoverTSpecFiles(files);
346
360
  if (resolveErrors.length > 0 && !options.quiet) {
347
361
  resolveErrors.forEach((err) => logger.warn(err));
348
362
  }
349
- if (tspecFiles.length === 0) {
363
+ if (fileDescriptors.length === 0) {
350
364
  spinner?.fail("No .tspec files found");
351
365
  process.exit(2);
352
366
  }
353
- if (spinner) spinner.text = `Parsing ${tspecFiles.length} file(s)...`;
367
+ if (spinner) spinner.text = `Parsing ${fileDescriptors.length} file(s)...`;
354
368
  const allTestCases = [];
355
369
  const parseErrors = [];
356
- for (const file of tspecFiles) {
370
+ for (const descriptor of fileDescriptors) {
357
371
  try {
358
- const testCases = parseTestCases(file, {
372
+ const testCases = parseTestCases(descriptor.path, {
359
373
  env: options.env,
360
374
  params: options.params
361
375
  });
362
376
  allTestCases.push(...testCases);
363
377
  } catch (err) {
364
378
  const message = err instanceof Error ? err.message : String(err);
365
- parseErrors.push({ file, error: message });
379
+ parseErrors.push({ file: descriptor.path, error: message });
366
380
  }
367
381
  }
368
382
  spinner?.stop();
@@ -371,13 +385,13 @@ const parseCommand = new Command("parse").description("Parse and display test ca
371
385
  testCases: allTestCases,
372
386
  errors: parseErrors,
373
387
  summary: {
374
- totalFiles: tspecFiles.length,
388
+ totalFiles: fileDescriptors.length,
375
389
  totalTestCases: allTestCases.length,
376
390
  parseErrors: parseErrors.length
377
391
  }
378
392
  }));
379
393
  } else {
380
- logger.info(`Parsed ${allTestCases.length} test case(s) from ${tspecFiles.length} file(s)`);
394
+ logger.info(`Parsed ${allTestCases.length} test case(s) from ${fileDescriptors.length} file(s)`);
381
395
  logger.newline();
382
396
  for (const testCase of allTestCases) {
383
397
  logger.log(formatParsedTestCase(testCase, { format: options.output, verbose: options.verbose }));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/utils/files.ts","../src/utils/formatter.ts","../src/utils/logger.ts","../src/commands/validate.ts","../src/commands/run.ts","../src/commands/parse.ts","../src/commands/list.ts","../src/index.ts"],"sourcesContent":["import { glob } from 'glob';\nimport { resolve, isAbsolute } from 'path';\nimport { existsSync, statSync } from 'fs';\n\nexport interface FileResolutionResult {\n files: string[];\n errors: string[];\n}\n\nexport async function resolveFiles(patterns: string[], cwd?: string): Promise<FileResolutionResult> {\n const workingDir = cwd || process.cwd();\n const files: string[] = [];\n const errors: string[] = [];\n\n for (const pattern of patterns) {\n // Check if it's a direct file path\n const absolutePath = isAbsolute(pattern) ? pattern : resolve(workingDir, pattern);\n \n if (existsSync(absolutePath)) {\n const stat = statSync(absolutePath);\n if (stat.isFile()) {\n files.push(absolutePath);\n continue;\n } else if (stat.isDirectory()) {\n // If it's a directory, glob for .tspec files\n const dirFiles = await glob('**/*.tspec', { cwd: absolutePath, absolute: true });\n if (dirFiles.length === 0) {\n errors.push(`No .tspec files found in directory: ${pattern}`);\n } else {\n files.push(...dirFiles);\n }\n continue;\n }\n }\n\n // Treat as glob pattern\n const matches = await glob(pattern, { cwd: workingDir, absolute: true });\n if (matches.length === 0) {\n errors.push(`No files matched pattern: ${pattern}`);\n } else {\n files.push(...matches);\n }\n }\n\n // Deduplicate files\n const uniqueFiles = [...new Set(files)];\n\n return { files: uniqueFiles, errors };\n}\n\nexport function filterByExtension(files: string[], extension: string): string[] {\n return files.filter(f => f.endsWith(extension));\n}\n\nexport function getTspecFiles(files: string[]): string[] {\n return filterByExtension(files, '.tspec');\n}\n","import chalk from 'chalk';\nimport type { ValidationResult } from '@boolesai/tspec';\n\nexport type OutputFormat = 'json' | 'text' | 'table';\n\nexport interface FormatOptions {\n format?: OutputFormat;\n verbose?: boolean;\n quiet?: boolean;\n}\n\nexport function formatJson(data: unknown): string {\n return JSON.stringify(data, null, 2);\n}\n\nexport function formatValidationResult(result: ValidationResult, filePath: string): string {\n if (result.valid) {\n return chalk.green(`✓ ${filePath}`);\n }\n const errors = result.errors.map(e => chalk.red(` - ${e}`)).join('\\n');\n return `${chalk.red(`✗ ${filePath}`)}\\n${errors}`;\n}\n\nexport function formatValidationResults(\n results: Array<{ file: string; result: ValidationResult }>,\n options: FormatOptions = {}\n): string {\n const { format = 'text' } = options;\n\n if (format === 'json') {\n return formatJson(results.map(r => ({\n file: r.file,\n valid: r.result.valid,\n errors: r.result.errors\n })));\n }\n\n const lines = results.map(r => formatValidationResult(r.result, r.file));\n const passed = results.filter(r => r.result.valid).length;\n const failed = results.length - passed;\n\n lines.push('');\n lines.push(chalk.bold(`Validation Summary: ${passed} passed, ${failed} failed`));\n\n return lines.join('\\n');\n}\n\nexport interface TestResultSummary {\n total: number;\n passed: number;\n failed: number;\n passRate: number;\n duration: number;\n}\n\nexport interface FormattedTestResult {\n testCaseId: string;\n passed: boolean;\n duration: number;\n assertions: Array<{\n passed: boolean;\n type: string;\n message: string;\n }>;\n}\n\nexport function formatTestResult(result: FormattedTestResult, verbose = false): string {\n const status = result.passed\n ? chalk.green('✓ PASS')\n : chalk.red('✗ FAIL');\n \n const duration = chalk.gray(`(${result.duration}ms)`);\n let output = `${status} ${result.testCaseId} ${duration}`;\n\n if (verbose || !result.passed) {\n const assertionLines = result.assertions\n .filter(a => verbose || !a.passed)\n .map(a => {\n const icon = a.passed ? chalk.green(' ✓') : chalk.red(' ✗');\n return `${icon} [${a.type}] ${a.message}`;\n });\n if (assertionLines.length > 0) {\n output += '\\n' + assertionLines.join('\\n');\n }\n }\n\n return output;\n}\n\nexport function formatTestSummary(summary: TestResultSummary): string {\n const passRate = summary.passRate.toFixed(1);\n const statusColor = summary.failed === 0 ? chalk.green : chalk.red;\n \n return [\n '',\n chalk.bold('─'.repeat(50)),\n chalk.bold('Test Summary'),\n ` Total: ${summary.total}`,\n ` ${chalk.green('Passed:')} ${summary.passed}`,\n ` ${chalk.red('Failed:')} ${summary.failed}`,\n ` Pass Rate: ${statusColor(passRate + '%')}`,\n ` Duration: ${summary.duration}ms`,\n chalk.bold('─'.repeat(50))\n ].join('\\n');\n}\n\nexport function formatTestResults(\n results: FormattedTestResult[],\n summary: TestResultSummary,\n options: FormatOptions = {}\n): string {\n const { format = 'text', verbose = false } = options;\n\n if (format === 'json') {\n return formatJson({ results, summary });\n }\n\n const lines = results.map(r => formatTestResult(r, verbose));\n lines.push(formatTestSummary(summary));\n\n return lines.join('\\n');\n}\n\nexport function formatProtocolList(protocols: string[], options: FormatOptions = {}): string {\n const { format = 'text' } = options;\n\n if (format === 'json') {\n return formatJson({ protocols });\n }\n\n return [\n chalk.bold('Supported Protocols:'),\n ...protocols.map(p => ` - ${p}`)\n ].join('\\n');\n}\n\nexport function formatParsedTestCase(testCase: unknown, options: FormatOptions = {}): string {\n const { format = 'text' } = options;\n\n if (format === 'json') {\n return formatJson(testCase);\n }\n\n // For text format, show a simplified view\n const tc = testCase as Record<string, unknown>;\n const lines: string[] = [];\n \n lines.push(chalk.bold(`Test Case: ${tc.id || 'unknown'}`));\n if (tc.description) lines.push(` Description: ${tc.description}`);\n if (tc.type) lines.push(` Protocol: ${tc.type}`);\n \n return lines.join('\\n');\n}\n","import chalk from 'chalk';\n\nexport type LogLevel = 'debug' | 'info' | 'success' | 'warn' | 'error';\n\nexport interface LoggerOptions {\n verbose?: boolean;\n quiet?: boolean;\n}\n\nlet globalOptions: LoggerOptions = {};\n\nexport function setLoggerOptions(options: LoggerOptions): void {\n globalOptions = { ...globalOptions, ...options };\n}\n\nexport function debug(message: string, ...args: unknown[]): void {\n if (globalOptions.verbose && !globalOptions.quiet) {\n console.log(chalk.gray(`[debug] ${message}`), ...args);\n }\n}\n\nexport function info(message: string, ...args: unknown[]): void {\n if (!globalOptions.quiet) {\n console.log(chalk.blue(message), ...args);\n }\n}\n\nexport function success(message: string, ...args: unknown[]): void {\n if (!globalOptions.quiet) {\n console.log(chalk.green(message), ...args);\n }\n}\n\nexport function warn(message: string, ...args: unknown[]): void {\n console.warn(chalk.yellow(`[warn] ${message}`), ...args);\n}\n\nexport function error(message: string, ...args: unknown[]): void {\n console.error(chalk.red(`[error] ${message}`), ...args);\n}\n\nexport function log(message: string, ...args: unknown[]): void {\n console.log(message, ...args);\n}\n\nexport function newline(): void {\n if (!globalOptions.quiet) {\n console.log();\n }\n}\n\nexport const logger = {\n debug,\n info,\n success,\n warn,\n error,\n log,\n newline,\n setOptions: setLoggerOptions\n};\n","import { Command } from 'commander';\nimport ora from 'ora';\nimport { validateTestCase } from '@boolesai/tspec';\nimport { resolveFiles, getTspecFiles } from '../utils/files.js';\nimport { formatValidationResults } from '../utils/formatter.js';\nimport { logger, setLoggerOptions } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface ValidateOptions {\n output?: OutputFormat;\n quiet?: boolean;\n}\n\nexport const validateCommand = new Command('validate')\n .description('Validate .tspec files for schema correctness')\n .argument('<files...>', 'Files or glob patterns to validate')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .option('-q, --quiet', 'Only output errors')\n .action(async (files: string[], options: ValidateOptions) => {\n setLoggerOptions({ quiet: options.quiet });\n \n const spinner = options.quiet ? null : ora('Resolving files...').start();\n \n try {\n const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);\n const tspecFiles = getTspecFiles(resolvedFiles);\n \n if (resolveErrors.length > 0 && !options.quiet) {\n resolveErrors.forEach(err => logger.warn(err));\n }\n \n if (tspecFiles.length === 0) {\n spinner?.fail('No .tspec files found');\n process.exit(2);\n }\n \n if (spinner) spinner.text = `Validating ${tspecFiles.length} file(s)...`;\n \n const results = tspecFiles.map(file => ({\n file,\n result: validateTestCase(file)\n }));\n \n spinner?.stop();\n \n const output = formatValidationResults(results, { format: options.output });\n logger.log(output);\n \n const hasErrors = results.some(r => !r.result.valid);\n process.exit(hasErrors ? 1 : 0);\n } catch (err) {\n spinner?.fail('Validation failed');\n const message = err instanceof Error ? err.message : String(err);\n logger.error(message);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport ora from 'ora';\nimport chalk from 'chalk';\nimport { parseTestCases, scheduler } from '@boolesai/tspec';\nimport type { TestCase, TestResult, ScheduleResult } from '@boolesai/tspec';\nimport { resolveFiles, getTspecFiles } from '../utils/files.js';\nimport { formatTestResults, formatJson, type FormattedTestResult, type TestResultSummary } from '../utils/formatter.js';\nimport { logger, setLoggerOptions } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface RunOptions {\n output?: OutputFormat;\n concurrency?: string;\n verbose?: boolean;\n quiet?: boolean;\n failFast?: boolean;\n env: Record<string, string>;\n params: Record<string, string>;\n}\n\nfunction parseKeyValue(value: string, previous: Record<string, string> = {}): Record<string, string> {\n const [key, val] = value.split('=');\n if (key && val !== undefined) {\n previous[key] = val;\n }\n return previous;\n}\n\nfunction formatResult(result: TestResult): FormattedTestResult {\n return {\n testCaseId: result.testCaseId,\n passed: result.passed,\n duration: result.duration,\n assertions: result.assertions.map(a => ({\n passed: a.passed,\n type: a.type,\n message: a.message\n }))\n };\n}\n\nexport const runCommand = new Command('run')\n .description('Execute test cases and report results')\n .argument('<files...>', 'Files or glob patterns to run')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .option('-c, --concurrency <number>', 'Max concurrent tests', '5')\n .option('-e, --env <key=value>', 'Environment variables', parseKeyValue, {})\n .option('-p, --params <key=value>', 'Parameters', parseKeyValue, {})\n .option('-v, --verbose', 'Verbose output')\n .option('-q, --quiet', 'Only output summary')\n .option('--fail-fast', 'Stop on first failure')\n .action(async (files: string[], options: RunOptions) => {\n setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });\n \n const concurrency = parseInt(options.concurrency || '5', 10);\n const spinner = options.quiet ? null : ora('Resolving files...').start();\n \n try {\n // Resolve files\n const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);\n const tspecFiles = getTspecFiles(resolvedFiles);\n \n if (resolveErrors.length > 0 && !options.quiet) {\n resolveErrors.forEach(err => logger.warn(err));\n }\n \n if (tspecFiles.length === 0) {\n spinner?.fail('No .tspec files found');\n process.exit(2);\n }\n \n // Parse test cases\n if (spinner) spinner.text = `Parsing ${tspecFiles.length} file(s)...`;\n \n const allTestCases: TestCase[] = [];\n const parseErrors: Array<{ file: string; error: string }> = [];\n \n for (const file of tspecFiles) {\n try {\n const testCases = parseTestCases(file, {\n env: options.env,\n params: options.params\n });\n allTestCases.push(...testCases);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n parseErrors.push({ file, error: message });\n }\n }\n \n if (allTestCases.length === 0) {\n spinner?.fail('No test cases found');\n if (parseErrors.length > 0) {\n parseErrors.forEach(({ file, error }) => logger.error(` ${file}: ${error}`));\n }\n process.exit(2);\n }\n \n // Execute tests\n if (spinner) spinner.text = `Running ${allTestCases.length} test(s) with concurrency ${concurrency}...`;\n \n let scheduleResult: ScheduleResult;\n \n if (options.failFast) {\n // For fail-fast, execute sequentially and stop on first failure\n const results: TestResult[] = [];\n let stopped = false;\n \n for (const testCase of allTestCases) {\n if (stopped) break;\n \n if (spinner) spinner.text = `Running: ${testCase.id}...`;\n \n try {\n const result = await scheduler.schedule([testCase], { concurrency: 1 });\n results.push(...result.results);\n \n if (!result.results[0]?.passed) {\n stopped = true;\n }\n } catch (err) {\n stopped = true;\n }\n }\n \n const passed = results.filter(r => r.passed).length;\n const failed = results.length - passed;\n \n scheduleResult = {\n results,\n duration: 0,\n summary: {\n total: results.length,\n passed,\n failed,\n passRate: results.length > 0 ? (passed / results.length) * 100 : 0\n }\n };\n } else {\n scheduleResult = await scheduler.schedule(allTestCases, { concurrency });\n }\n \n spinner?.stop();\n \n // Format and output results\n const formattedResults = scheduleResult.results.map(formatResult);\n const summary: TestResultSummary = {\n ...scheduleResult.summary,\n duration: scheduleResult.duration\n };\n \n if (options.output === 'json') {\n logger.log(formatJson({\n results: formattedResults,\n summary,\n parseErrors\n }));\n } else {\n if (!options.quiet) {\n const output = formatTestResults(formattedResults, summary, {\n format: options.output,\n verbose: options.verbose\n });\n logger.log(output);\n } else {\n // Quiet mode: just show summary line\n const statusColor = summary.failed === 0 ? chalk.green : chalk.red;\n logger.log(statusColor(`${summary.passed}/${summary.total} tests passed (${summary.passRate.toFixed(1)}%)`));\n }\n \n if (parseErrors.length > 0) {\n logger.newline();\n logger.warn(`${parseErrors.length} file(s) failed to parse:`);\n for (const { file, error } of parseErrors) {\n logger.error(` ${file}: ${error}`);\n }\n }\n }\n \n // Exit code: 0 if all passed, 1 if any failed\n process.exit(scheduleResult.summary.failed > 0 ? 1 : 0);\n } catch (err) {\n spinner?.fail('Execution failed');\n const message = err instanceof Error ? err.message : String(err);\n logger.error(message);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport ora from 'ora';\nimport { parseTestCases } from '@boolesai/tspec';\nimport { resolveFiles, getTspecFiles } from '../utils/files.js';\nimport { formatParsedTestCase, formatJson } from '../utils/formatter.js';\nimport { logger, setLoggerOptions } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface ParseOptions {\n output?: OutputFormat;\n verbose?: boolean;\n quiet?: boolean;\n}\n\nfunction parseKeyValue(value: string, previous: Record<string, string> = {}): Record<string, string> {\n const [key, val] = value.split('=');\n if (key && val !== undefined) {\n previous[key] = val;\n }\n return previous;\n}\n\nexport const parseCommand = new Command('parse')\n .description('Parse and display test case information without execution')\n .argument('<files...>', 'Files or glob patterns to parse')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .option('-v, --verbose', 'Show detailed information')\n .option('-q, --quiet', 'Minimal output')\n .option('-e, --env <key=value>', 'Environment variables', parseKeyValue, {})\n .option('-p, --params <key=value>', 'Parameters', parseKeyValue, {})\n .action(async (files: string[], options: ParseOptions & { env: Record<string, string>; params: Record<string, string> }) => {\n setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });\n \n const spinner = options.quiet ? null : ora('Resolving files...').start();\n \n try {\n const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);\n const tspecFiles = getTspecFiles(resolvedFiles);\n \n if (resolveErrors.length > 0 && !options.quiet) {\n resolveErrors.forEach(err => logger.warn(err));\n }\n \n if (tspecFiles.length === 0) {\n spinner?.fail('No .tspec files found');\n process.exit(2);\n }\n \n if (spinner) spinner.text = `Parsing ${tspecFiles.length} file(s)...`;\n \n const allTestCases: unknown[] = [];\n const parseErrors: Array<{ file: string; error: string }> = [];\n \n for (const file of tspecFiles) {\n try {\n const testCases = parseTestCases(file, {\n env: options.env,\n params: options.params\n });\n allTestCases.push(...testCases);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n parseErrors.push({ file, error: message });\n }\n }\n \n spinner?.stop();\n \n if (options.output === 'json') {\n logger.log(formatJson({\n testCases: allTestCases,\n errors: parseErrors,\n summary: {\n totalFiles: tspecFiles.length,\n totalTestCases: allTestCases.length,\n parseErrors: parseErrors.length\n }\n }));\n } else {\n logger.info(`Parsed ${allTestCases.length} test case(s) from ${tspecFiles.length} file(s)`);\n logger.newline();\n \n for (const testCase of allTestCases) {\n logger.log(formatParsedTestCase(testCase, { format: options.output, verbose: options.verbose }));\n logger.newline();\n }\n \n if (parseErrors.length > 0) {\n logger.newline();\n logger.warn(`${parseErrors.length} file(s) failed to parse:`);\n for (const { file, error } of parseErrors) {\n logger.error(` ${file}: ${error}`);\n }\n }\n }\n \n process.exit(parseErrors.length > 0 ? 1 : 0);\n } catch (err) {\n spinner?.fail('Parse failed');\n const message = err instanceof Error ? err.message : String(err);\n logger.error(message);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport { registry } from '@boolesai/tspec';\nimport { formatProtocolList } from '../utils/formatter.js';\nimport { logger } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface ListOptions {\n output?: OutputFormat;\n}\n\nexport const listCommand = new Command('list')\n .description('List supported protocols and configuration')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .action(async (options: ListOptions) => {\n try {\n const protocols = registry.getRegisteredTypes();\n const output = formatProtocolList(protocols, { format: options.output });\n logger.log(output);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`Failed to list protocols: ${message}`);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport { validateCommand } from './commands/validate.js';\nimport { runCommand } from './commands/run.js';\nimport { parseCommand } from './commands/parse.js';\nimport { listCommand } from './commands/list.js';\n\nconst program = new Command();\n\nprogram\n .name('tspec')\n .description('CLI for @boolesai/tspec testing framework')\n .version('1.0.0');\n\nprogram.addCommand(validateCommand);\nprogram.addCommand(runCommand);\nprogram.addCommand(parseCommand);\nprogram.addCommand(listCommand);\n\nprogram.parse();\n"],"names":["parseKeyValue","error"],"mappings":";;;;;;;AASA,eAAsB,aAAa,UAAoB,KAA6C;AAClG,QAAM,aAAoB,QAAQ,IAAA;AAClC,QAAM,QAAkB,CAAA;AACxB,QAAM,SAAmB,CAAA;AAEzB,aAAW,WAAW,UAAU;AAE9B,UAAM,eAAe,WAAW,OAAO,IAAI,UAAU,QAAQ,YAAY,OAAO;AAEhF,QAAI,WAAW,YAAY,GAAG;AAC5B,YAAM,OAAO,SAAS,YAAY;AAClC,UAAI,KAAK,UAAU;AACjB,cAAM,KAAK,YAAY;AACvB;AAAA,MACF,WAAW,KAAK,eAAe;AAE7B,cAAM,WAAW,MAAM,KAAK,cAAc,EAAE,KAAK,cAAc,UAAU,MAAM;AAC/E,YAAI,SAAS,WAAW,GAAG;AACzB,iBAAO,KAAK,uCAAuC,OAAO,EAAE;AAAA,QAC9D,OAAO;AACL,gBAAM,KAAK,GAAG,QAAQ;AAAA,QACxB;AACA;AAAA,MACF;AAAA,IACF;AAGA,UAAM,UAAU,MAAM,KAAK,SAAS,EAAE,KAAK,YAAY,UAAU,MAAM;AACvE,QAAI,QAAQ,WAAW,GAAG;AACxB,aAAO,KAAK,6BAA6B,OAAO,EAAE;AAAA,IACpD,OAAO;AACL,YAAM,KAAK,GAAG,OAAO;AAAA,IACvB;AAAA,EACF;AAGA,QAAM,cAAc,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC;AAEtC,SAAO,EAAE,OAAO,aAAa,OAAA;AAC/B;AAEO,SAAS,kBAAkB,OAAiB,WAA6B;AAC9E,SAAO,MAAM,OAAO,CAAA,MAAK,EAAE,SAAS,SAAS,CAAC;AAChD;AAEO,SAAS,cAAc,OAA2B;AACvD,SAAO,kBAAkB,OAAO,QAAQ;AAC1C;AC7CO,SAAS,WAAW,MAAuB;AAChD,SAAO,KAAK,UAAU,MAAM,MAAM,CAAC;AACrC;AAEO,SAAS,uBAAuB,QAA0B,UAA0B;AACzF,MAAI,OAAO,OAAO;AAChB,WAAO,MAAM,MAAM,KAAK,QAAQ,EAAE;AAAA,EACpC;AACA,QAAM,SAAS,OAAO,OAAO,IAAI,CAAA,MAAK,MAAM,IAAI,OAAO,CAAC,EAAE,CAAC,EAAE,KAAK,IAAI;AACtE,SAAO,GAAG,MAAM,IAAI,KAAK,QAAQ,EAAE,CAAC;AAAA,EAAK,MAAM;AACjD;AAEO,SAAS,wBACd,SACA,UAAyB,IACjB;AACR,QAAM,EAAE,SAAS,OAAA,IAAW;AAE5B,MAAI,WAAW,QAAQ;AACrB,WAAO,WAAW,QAAQ,IAAI,CAAA,OAAM;AAAA,MAClC,MAAM,EAAE;AAAA,MACR,OAAO,EAAE,OAAO;AAAA,MAChB,QAAQ,EAAE,OAAO;AAAA,IAAA,EACjB,CAAC;AAAA,EACL;AAEA,QAAM,QAAQ,QAAQ,IAAI,CAAA,MAAK,uBAAuB,EAAE,QAAQ,EAAE,IAAI,CAAC;AACvE,QAAM,SAAS,QAAQ,OAAO,OAAK,EAAE,OAAO,KAAK,EAAE;AACnD,QAAM,SAAS,QAAQ,SAAS;AAEhC,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,MAAM,KAAK,uBAAuB,MAAM,YAAY,MAAM,SAAS,CAAC;AAE/E,SAAO,MAAM,KAAK,IAAI;AACxB;AAqBO,SAAS,iBAAiB,QAA6B,UAAU,OAAe;AACrF,QAAM,SAAS,OAAO,SAClB,MAAM,MAAM,QAAQ,IACpB,MAAM,IAAI,QAAQ;AAEtB,QAAM,WAAW,MAAM,KAAK,IAAI,OAAO,QAAQ,KAAK;AACpD,MAAI,SAAS,GAAG,MAAM,IAAI,OAAO,UAAU,IAAI,QAAQ;AAEvD,MAAI,WAAW,CAAC,OAAO,QAAQ;AAC7B,UAAM,iBAAiB,OAAO,WAC3B,OAAO,CAAA,MAAK,WAAW,CAAC,EAAE,MAAM,EAChC,IAAI,CAAA,MAAK;AACR,YAAM,OAAO,EAAE,SAAS,MAAM,MAAM,KAAK,IAAI,MAAM,IAAI,KAAK;AAC5D,aAAO,GAAG,IAAI,KAAK,EAAE,IAAI,KAAK,EAAE,OAAO;AAAA,IACzC,CAAC;AACH,QAAI,eAAe,SAAS,GAAG;AAC7B,gBAAU,OAAO,eAAe,KAAK,IAAI;AAAA,IAC3C;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,kBAAkB,SAAoC;AACpE,QAAM,WAAW,QAAQ,SAAS,QAAQ,CAAC;AAC3C,QAAM,cAAc,QAAQ,WAAW,IAAI,MAAM,QAAQ,MAAM;AAE/D,SAAO;AAAA,IACL;AAAA,IACA,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;AAAA,IACzB,MAAM,KAAK,cAAc;AAAA,IACzB,eAAe,QAAQ,KAAK;AAAA,IAC5B,KAAK,MAAM,MAAM,SAAS,CAAC,KAAK,QAAQ,MAAM;AAAA,IAC9C,KAAK,MAAM,IAAI,SAAS,CAAC,KAAK,QAAQ,MAAM;AAAA,IAC5C,gBAAgB,YAAY,WAAW,GAAG,CAAC;AAAA,IAC3C,gBAAgB,QAAQ,QAAQ;AAAA,IAChC,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;AAAA,EAAA,EACzB,KAAK,IAAI;AACb;AAEO,SAAS,kBACd,SACA,SACA,UAAyB,CAAA,GACjB;AACR,QAAM,EAAE,SAAS,QAAQ,UAAU,UAAU;AAE7C,MAAI,WAAW,QAAQ;AACrB,WAAO,WAAW,EAAE,SAAS,SAAS;AAAA,EACxC;AAEA,QAAM,QAAQ,QAAQ,IAAI,OAAK,iBAAiB,GAAG,OAAO,CAAC;AAC3D,QAAM,KAAK,kBAAkB,OAAO,CAAC;AAErC,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,mBAAmB,WAAqB,UAAyB,IAAY;AAC3F,QAAM,EAAE,SAAS,OAAA,IAAW;AAE5B,MAAI,WAAW,QAAQ;AACrB,WAAO,WAAW,EAAE,WAAW;AAAA,EACjC;AAEA,SAAO;AAAA,IACL,MAAM,KAAK,sBAAsB;AAAA,IACjC,GAAG,UAAU,IAAI,CAAA,MAAK,OAAO,CAAC,EAAE;AAAA,EAAA,EAChC,KAAK,IAAI;AACb;AAEO,SAAS,qBAAqB,UAAmB,UAAyB,IAAY;AAC3F,QAAM,EAAE,SAAS,OAAA,IAAW;AAE5B,MAAI,WAAW,QAAQ;AACrB,WAAO,WAAW,QAAQ;AAAA,EAC5B;AAGA,QAAM,KAAK;AACX,QAAM,QAAkB,CAAA;AAExB,QAAM,KAAK,MAAM,KAAK,cAAc,GAAG,MAAM,SAAS,EAAE,CAAC;AACzD,MAAI,GAAG,YAAa,OAAM,KAAK,kBAAkB,GAAG,WAAW,EAAE;AACjE,MAAI,GAAG,KAAM,OAAM,KAAK,eAAe,GAAG,IAAI,EAAE;AAEhD,SAAO,MAAM,KAAK,IAAI;AACxB;AC/IA,IAAI,gBAA+B,CAAA;AAE5B,SAAS,iBAAiB,SAA8B;AAC7D,kBAAgB,EAAE,GAAG,eAAe,GAAG,QAAA;AACzC;AAEO,SAAS,MAAM,YAAoB,MAAuB;AAC/D,MAAI,cAAc,WAAW,CAAC,cAAc,OAAO;AACjD,YAAQ,IAAI,MAAM,KAAK,WAAW,OAAO,EAAE,GAAG,GAAG,IAAI;AAAA,EACvD;AACF;AAEO,SAAS,KAAK,YAAoB,MAAuB;AAC9D,MAAI,CAAC,cAAc,OAAO;AACxB,YAAQ,IAAI,MAAM,KAAK,OAAO,GAAG,GAAG,IAAI;AAAA,EAC1C;AACF;AAEO,SAAS,QAAQ,YAAoB,MAAuB;AACjE,MAAI,CAAC,cAAc,OAAO;AACxB,YAAQ,IAAI,MAAM,MAAM,OAAO,GAAG,GAAG,IAAI;AAAA,EAC3C;AACF;AAEO,SAAS,KAAK,YAAoB,MAAuB;AAC9D,UAAQ,KAAK,MAAM,OAAO,UAAU,OAAO,EAAE,GAAG,GAAG,IAAI;AACzD;AAEO,SAAS,MAAM,YAAoB,MAAuB;AAC/D,UAAQ,MAAM,MAAM,IAAI,WAAW,OAAO,EAAE,GAAG,GAAG,IAAI;AACxD;AAEO,SAAS,IAAI,YAAoB,MAAuB;AAC7D,UAAQ,IAAI,SAAS,GAAG,IAAI;AAC9B;AAEO,SAAS,UAAgB;AAC9B,MAAI,CAAC,cAAc,OAAO;AACxB,YAAQ,IAAA;AAAA,EACV;AACF;AAEO,MAAM,SAAS;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd;AC/CO,MAAM,kBAAkB,IAAI,QAAQ,UAAU,EAClD,YAAY,8CAA8C,EAC1D,SAAS,cAAc,oCAAoC,EAC3D,OAAO,yBAAyB,6BAA6B,MAAM,EACnE,OAAO,eAAe,oBAAoB,EAC1C,OAAO,OAAO,OAAiB,YAA6B;AAC3D,mBAAiB,EAAE,OAAO,QAAQ,MAAA,CAAO;AAEzC,QAAM,UAAU,QAAQ,QAAQ,OAAO,IAAI,oBAAoB,EAAE,MAAA;AAEjE,MAAI;AACF,UAAM,EAAE,OAAO,eAAe,QAAQ,kBAAkB,MAAM,aAAa,KAAK;AAChF,UAAM,aAAa,cAAc,aAAa;AAE9C,QAAI,cAAc,SAAS,KAAK,CAAC,QAAQ,OAAO;AAC9C,oBAAc,QAAQ,CAAA,QAAO,OAAO,KAAK,GAAG,CAAC;AAAA,IAC/C;AAEA,QAAI,WAAW,WAAW,GAAG;AAC3B,eAAS,KAAK,uBAAuB;AACrC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,QAAI,QAAS,SAAQ,OAAO,cAAc,WAAW,MAAM;AAE3D,UAAM,UAAU,WAAW,IAAI,CAAA,UAAS;AAAA,MACtC;AAAA,MACA,QAAQ,iBAAiB,IAAI;AAAA,IAAA,EAC7B;AAEF,aAAS,KAAA;AAET,UAAM,SAAS,wBAAwB,SAAS,EAAE,QAAQ,QAAQ,QAAQ;AAC1E,WAAO,IAAI,MAAM;AAEjB,UAAM,YAAY,QAAQ,KAAK,OAAK,CAAC,EAAE,OAAO,KAAK;AACnD,YAAQ,KAAK,YAAY,IAAI,CAAC;AAAA,EAChC,SAAS,KAAK;AACZ,aAAS,KAAK,mBAAmB;AACjC,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO,MAAM,OAAO;AACpB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;ACpCH,SAASA,gBAAc,OAAe,WAAmC,IAA4B;AACnG,QAAM,CAAC,KAAK,GAAG,IAAI,MAAM,MAAM,GAAG;AAClC,MAAI,OAAO,QAAQ,QAAW;AAC5B,aAAS,GAAG,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,QAAyC;AAC7D,SAAO;AAAA,IACL,YAAY,OAAO;AAAA,IACnB,QAAQ,OAAO;AAAA,IACf,UAAU,OAAO;AAAA,IACjB,YAAY,OAAO,WAAW,IAAI,CAAA,OAAM;AAAA,MACtC,QAAQ,EAAE;AAAA,MACV,MAAM,EAAE;AAAA,MACR,SAAS,EAAE;AAAA,IAAA,EACX;AAAA,EAAA;AAEN;AAEO,MAAM,aAAa,IAAI,QAAQ,KAAK,EACxC,YAAY,uCAAuC,EACnD,SAAS,cAAc,+BAA+B,EACtD,OAAO,yBAAyB,6BAA6B,MAAM,EACnE,OAAO,8BAA8B,wBAAwB,GAAG,EAChE,OAAO,yBAAyB,yBAAyBA,iBAAe,CAAA,CAAE,EAC1E,OAAO,4BAA4B,cAAcA,iBAAe,EAAE,EAClE,OAAO,iBAAiB,gBAAgB,EACxC,OAAO,eAAe,qBAAqB,EAC3C,OAAO,eAAe,uBAAuB,EAC7C,OAAO,OAAO,OAAiB,YAAwB;AACtD,mBAAiB,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,OAAO;AAEnE,QAAM,cAAc,SAAS,QAAQ,eAAe,KAAK,EAAE;AAC3D,QAAM,UAAU,QAAQ,QAAQ,OAAO,IAAI,oBAAoB,EAAE,MAAA;AAEjE,MAAI;AAEF,UAAM,EAAE,OAAO,eAAe,QAAQ,kBAAkB,MAAM,aAAa,KAAK;AAChF,UAAM,aAAa,cAAc,aAAa;AAE9C,QAAI,cAAc,SAAS,KAAK,CAAC,QAAQ,OAAO;AAC9C,oBAAc,QAAQ,CAAA,QAAO,OAAO,KAAK,GAAG,CAAC;AAAA,IAC/C;AAEA,QAAI,WAAW,WAAW,GAAG;AAC3B,eAAS,KAAK,uBAAuB;AACrC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,QAAI,QAAS,SAAQ,OAAO,WAAW,WAAW,MAAM;AAExD,UAAM,eAA2B,CAAA;AACjC,UAAM,cAAsD,CAAA;AAE5D,eAAW,QAAQ,YAAY;AAC7B,UAAI;AACF,cAAM,YAAY,eAAe,MAAM;AAAA,UACrC,KAAK,QAAQ;AAAA,UACb,QAAQ,QAAQ;AAAA,QAAA,CACjB;AACD,qBAAa,KAAK,GAAG,SAAS;AAAA,MAChC,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,oBAAY,KAAK,EAAE,MAAM,OAAO,SAAS;AAAA,MAC3C;AAAA,IACF;AAEA,QAAI,aAAa,WAAW,GAAG;AAC7B,eAAS,KAAK,qBAAqB;AACnC,UAAI,YAAY,SAAS,GAAG;AAC1B,oBAAY,QAAQ,CAAC,EAAE,MAAM,OAAAC,OAAA,MAAY,OAAO,MAAM,KAAK,IAAI,KAAKA,MAAK,EAAE,CAAC;AAAA,MAC9E;AACA,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,QAAI,QAAS,SAAQ,OAAO,WAAW,aAAa,MAAM,6BAA6B,WAAW;AAElG,QAAI;AAEJ,QAAI,QAAQ,UAAU;AAEpB,YAAM,UAAwB,CAAA;AAC9B,UAAI,UAAU;AAEd,iBAAW,YAAY,cAAc;AACnC,YAAI,QAAS;AAEb,YAAI,QAAS,SAAQ,OAAO,YAAY,SAAS,EAAE;AAEnD,YAAI;AACF,gBAAM,SAAS,MAAM,UAAU,SAAS,CAAC,QAAQ,GAAG,EAAE,aAAa,GAAG;AACtE,kBAAQ,KAAK,GAAG,OAAO,OAAO;AAE9B,cAAI,CAAC,OAAO,QAAQ,CAAC,GAAG,QAAQ;AAC9B,sBAAU;AAAA,UACZ;AAAA,QACF,SAAS,KAAK;AACZ,oBAAU;AAAA,QACZ;AAAA,MACF;AAEA,YAAM,SAAS,QAAQ,OAAO,CAAA,MAAK,EAAE,MAAM,EAAE;AAC7C,YAAM,SAAS,QAAQ,SAAS;AAEhC,uBAAiB;AAAA,QACf;AAAA,QACA,UAAU;AAAA,QACV,SAAS;AAAA,UACP,OAAO,QAAQ;AAAA,UACf;AAAA,UACA;AAAA,UACA,UAAU,QAAQ,SAAS,IAAK,SAAS,QAAQ,SAAU,MAAM;AAAA,QAAA;AAAA,MACnE;AAAA,IAEJ,OAAO;AACL,uBAAiB,MAAM,UAAU,SAAS,cAAc,EAAE,aAAa;AAAA,IACzE;AAEA,aAAS,KAAA;AAGT,UAAM,mBAAmB,eAAe,QAAQ,IAAI,YAAY;AAChE,UAAM,UAA6B;AAAA,MACjC,GAAG,eAAe;AAAA,MAClB,UAAU,eAAe;AAAA,IAAA;AAG3B,QAAI,QAAQ,WAAW,QAAQ;AAC7B,aAAO,IAAI,WAAW;AAAA,QACpB,SAAS;AAAA,QACT;AAAA,QACA;AAAA,MAAA,CACD,CAAC;AAAA,IACJ,OAAO;AACL,UAAI,CAAC,QAAQ,OAAO;AAClB,cAAM,SAAS,kBAAkB,kBAAkB,SAAS;AAAA,UAC1D,QAAQ,QAAQ;AAAA,UAChB,SAAS,QAAQ;AAAA,QAAA,CAClB;AACD,eAAO,IAAI,MAAM;AAAA,MACnB,OAAO;AAEL,cAAM,cAAc,QAAQ,WAAW,IAAI,MAAM,QAAQ,MAAM;AAC/D,eAAO,IAAI,YAAY,GAAG,QAAQ,MAAM,IAAI,QAAQ,KAAK,kBAAkB,QAAQ,SAAS,QAAQ,CAAC,CAAC,IAAI,CAAC;AAAA,MAC7G;AAEA,UAAI,YAAY,SAAS,GAAG;AAC1B,eAAO,QAAA;AACP,eAAO,KAAK,GAAG,YAAY,MAAM,2BAA2B;AAC5D,mBAAW,EAAE,MAAM,OAAAA,OAAA,KAAW,aAAa;AACzC,iBAAO,MAAM,KAAK,IAAI,KAAKA,MAAK,EAAE;AAAA,QACpC;AAAA,MACF;AAAA,IACF;AAGA,YAAQ,KAAK,eAAe,QAAQ,SAAS,IAAI,IAAI,CAAC;AAAA,EACxD,SAAS,KAAK;AACZ,aAAS,KAAK,kBAAkB;AAChC,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO,MAAM,OAAO;AACpB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AC7KH,SAAS,cAAc,OAAe,WAAmC,IAA4B;AACnG,QAAM,CAAC,KAAK,GAAG,IAAI,MAAM,MAAM,GAAG;AAClC,MAAI,OAAO,QAAQ,QAAW;AAC5B,aAAS,GAAG,IAAI;AAAA,EAClB;AACA,SAAO;AACT;AAEO,MAAM,eAAe,IAAI,QAAQ,OAAO,EAC5C,YAAY,2DAA2D,EACvE,SAAS,cAAc,iCAAiC,EACxD,OAAO,yBAAyB,6BAA6B,MAAM,EACnE,OAAO,iBAAiB,2BAA2B,EACnD,OAAO,eAAe,gBAAgB,EACtC,OAAO,yBAAyB,yBAAyB,eAAe,EAAE,EAC1E,OAAO,4BAA4B,cAAc,eAAe,CAAA,CAAE,EAClE,OAAO,OAAO,OAAiB,YAA4F;AAC1H,mBAAiB,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,OAAO;AAEnE,QAAM,UAAU,QAAQ,QAAQ,OAAO,IAAI,oBAAoB,EAAE,MAAA;AAEjE,MAAI;AACF,UAAM,EAAE,OAAO,eAAe,QAAQ,kBAAkB,MAAM,aAAa,KAAK;AAChF,UAAM,aAAa,cAAc,aAAa;AAE9C,QAAI,cAAc,SAAS,KAAK,CAAC,QAAQ,OAAO;AAC9C,oBAAc,QAAQ,CAAA,QAAO,OAAO,KAAK,GAAG,CAAC;AAAA,IAC/C;AAEA,QAAI,WAAW,WAAW,GAAG;AAC3B,eAAS,KAAK,uBAAuB;AACrC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,QAAI,QAAS,SAAQ,OAAO,WAAW,WAAW,MAAM;AAExD,UAAM,eAA0B,CAAA;AAChC,UAAM,cAAsD,CAAA;AAE5D,eAAW,QAAQ,YAAY;AAC7B,UAAI;AACF,cAAM,YAAY,eAAe,MAAM;AAAA,UACrC,KAAK,QAAQ;AAAA,UACb,QAAQ,QAAQ;AAAA,QAAA,CACjB;AACD,qBAAa,KAAK,GAAG,SAAS;AAAA,MAChC,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,oBAAY,KAAK,EAAE,MAAM,OAAO,SAAS;AAAA,MAC3C;AAAA,IACF;AAEA,aAAS,KAAA;AAET,QAAI,QAAQ,WAAW,QAAQ;AAC7B,aAAO,IAAI,WAAW;AAAA,QACpB,WAAW;AAAA,QACX,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,YAAY,WAAW;AAAA,UACvB,gBAAgB,aAAa;AAAA,UAC7B,aAAa,YAAY;AAAA,QAAA;AAAA,MAC3B,CACD,CAAC;AAAA,IACJ,OAAO;AACL,aAAO,KAAK,UAAU,aAAa,MAAM,sBAAsB,WAAW,MAAM,UAAU;AAC1F,aAAO,QAAA;AAEP,iBAAW,YAAY,cAAc;AACnC,eAAO,IAAI,qBAAqB,UAAU,EAAE,QAAQ,QAAQ,QAAQ,SAAS,QAAQ,QAAA,CAAS,CAAC;AAC/F,eAAO,QAAA;AAAA,MACT;AAEA,UAAI,YAAY,SAAS,GAAG;AAC1B,eAAO,QAAA;AACP,eAAO,KAAK,GAAG,YAAY,MAAM,2BAA2B;AAC5D,mBAAW,EAAE,MAAM,OAAAA,OAAA,KAAW,aAAa;AACzC,iBAAO,MAAM,KAAK,IAAI,KAAKA,MAAK,EAAE;AAAA,QACpC;AAAA,MACF;AAAA,IACF;AAEA,YAAQ,KAAK,YAAY,SAAS,IAAI,IAAI,CAAC;AAAA,EAC7C,SAAS,KAAK;AACZ,aAAS,KAAK,cAAc;AAC5B,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO,MAAM,OAAO;AACpB,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AC7FI,MAAM,cAAc,IAAI,QAAQ,MAAM,EAC1C,YAAY,4CAA4C,EACxD,OAAO,yBAAyB,6BAA6B,MAAM,EACnE,OAAO,OAAO,YAAyB;AACtC,MAAI;AACF,UAAM,YAAY,SAAS,mBAAA;AAC3B,UAAM,SAAS,mBAAmB,WAAW,EAAE,QAAQ,QAAQ,QAAQ;AACvE,WAAO,IAAI,MAAM;AAAA,EACnB,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO,MAAM,6BAA6B,OAAO,EAAE;AACnD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;ACjBH,MAAM,UAAU,IAAI,QAAA;AAEpB,QACG,KAAK,OAAO,EACZ,YAAY,2CAA2C,EACvD,QAAQ,OAAO;AAElB,QAAQ,WAAW,eAAe;AAClC,QAAQ,WAAW,UAAU;AAC7B,QAAQ,WAAW,YAAY;AAC/B,QAAQ,WAAW,WAAW;AAE9B,QAAQ,MAAA;"}
1
+ {"version":3,"file":"index.js","sources":["../src/utils/files.ts","../src/utils/formatter.ts","../src/utils/logger.ts","../src/commands/validate.ts","../src/commands/run.ts","../src/commands/parse.ts","../src/commands/list.ts","../src/index.ts"],"sourcesContent":["import { glob } from 'glob';\nimport { resolve, isAbsolute, relative, basename } from 'path';\nimport { existsSync, statSync } from 'fs';\nimport { getTypeFromFilePath } from '@boolesai/tspec';\n\nexport type ProtocolType = 'http' | 'grpc' | 'graphql' | 'websocket';\n\nexport interface FileResolutionResult {\n files: string[];\n errors: string[];\n}\n\n/**\n * Represents a discovered .tspec file without loading its content.\n * Used for lazy file loading - files are scanned first, content read on-demand.\n */\nexport interface TSpecFileDescriptor {\n /** Absolute path to the file */\n path: string;\n /** Path relative to working directory */\n relativePath: string;\n /** Base filename (e.g., \"login.http.tspec\") */\n fileName: string;\n /** Protocol type extracted from filename (e.g., \"http\" from \"login.http.tspec\") */\n protocol: ProtocolType | null;\n}\n\nexport interface DiscoveryResult {\n files: TSpecFileDescriptor[];\n errors: string[];\n}\n\nexport async function resolveFiles(patterns: string[], cwd?: string): Promise<FileResolutionResult> {\n const workingDir = cwd || process.cwd();\n const files: string[] = [];\n const errors: string[] = [];\n\n for (const pattern of patterns) {\n // Check if it's a direct file path\n const absolutePath = isAbsolute(pattern) ? pattern : resolve(workingDir, pattern);\n \n if (existsSync(absolutePath)) {\n const stat = statSync(absolutePath);\n if (stat.isFile()) {\n files.push(absolutePath);\n continue;\n } else if (stat.isDirectory()) {\n // If it's a directory, glob for .tspec files\n const dirFiles = await glob('**/*.tspec', { cwd: absolutePath, absolute: true });\n if (dirFiles.length === 0) {\n errors.push(`No .tspec files found in directory: ${pattern}`);\n } else {\n files.push(...dirFiles);\n }\n continue;\n }\n }\n\n // Treat as glob pattern\n const matches = await glob(pattern, { cwd: workingDir, absolute: true });\n if (matches.length === 0) {\n errors.push(`No files matched pattern: ${pattern}`);\n } else {\n files.push(...matches);\n }\n }\n\n // Deduplicate files\n const uniqueFiles = [...new Set(files)];\n\n return { files: uniqueFiles, errors };\n}\n\nexport function filterByExtension(files: string[], extension: string): string[] {\n return files.filter(f => f.endsWith(extension));\n}\n\nexport function getTspecFiles(files: string[]): string[] {\n return filterByExtension(files, '.tspec');\n}\n\n/**\n * Discovers .tspec files without loading their content.\n * Files are scanned and classified, content is read on-demand during validation/parsing/execution.\n * \n * @param patterns - File paths, directory paths, or glob patterns\n * @param cwd - Working directory for resolving relative paths\n * @returns DiscoveryResult with file descriptors and any errors\n */\nexport async function discoverTSpecFiles(patterns: string[], cwd?: string): Promise<DiscoveryResult> {\n const workingDir = cwd || process.cwd();\n const filePaths: string[] = [];\n const errors: string[] = [];\n\n for (const pattern of patterns) {\n // Check if it's a direct file path\n const absolutePath = isAbsolute(pattern) ? pattern : resolve(workingDir, pattern);\n \n if (existsSync(absolutePath)) {\n const stat = statSync(absolutePath);\n if (stat.isFile()) {\n if (absolutePath.endsWith('.tspec')) {\n filePaths.push(absolutePath);\n }\n continue;\n } else if (stat.isDirectory()) {\n // If it's a directory, glob for .tspec files\n const dirFiles = await glob('**/*.tspec', { cwd: absolutePath, absolute: true });\n if (dirFiles.length === 0) {\n errors.push(`No .tspec files found in directory: ${pattern}`);\n } else {\n filePaths.push(...dirFiles);\n }\n continue;\n }\n }\n\n // Treat as glob pattern\n const matches = await glob(pattern, { cwd: workingDir, absolute: true });\n const tspecMatches = matches.filter(f => f.endsWith('.tspec'));\n if (tspecMatches.length === 0) {\n errors.push(`No .tspec files matched pattern: ${pattern}`);\n } else {\n filePaths.push(...tspecMatches);\n }\n }\n\n // Deduplicate and create descriptors with protocol classification\n const uniquePaths = [...new Set(filePaths)];\n const files: TSpecFileDescriptor[] = uniquePaths.map(filePath => ({\n path: filePath,\n relativePath: relative(workingDir, filePath),\n fileName: basename(filePath),\n protocol: getTypeFromFilePath(filePath) as ProtocolType | null,\n }));\n\n return { files, errors };\n}\n","import chalk from 'chalk';\nimport type { ValidationResult } from '@boolesai/tspec';\n\nexport type OutputFormat = 'json' | 'text' | 'table';\n\nexport interface FormatOptions {\n format?: OutputFormat;\n verbose?: boolean;\n quiet?: boolean;\n}\n\nexport function formatJson(data: unknown): string {\n return JSON.stringify(data, null, 2);\n}\n\nexport function formatValidationResult(result: ValidationResult, filePath: string): string {\n if (result.valid) {\n return chalk.green(`✓ ${filePath}`);\n }\n const errors = result.errors.map(e => chalk.red(` - ${e}`)).join('\\n');\n return `${chalk.red(`✗ ${filePath}`)}\\n${errors}`;\n}\n\nexport function formatValidationResults(\n results: Array<{ file: string; result: ValidationResult }>,\n options: FormatOptions = {}\n): string {\n const { format = 'text' } = options;\n\n if (format === 'json') {\n return formatJson(results.map(r => ({\n file: r.file,\n valid: r.result.valid,\n errors: r.result.errors\n })));\n }\n\n const lines = results.map(r => formatValidationResult(r.result, r.file));\n const passed = results.filter(r => r.result.valid).length;\n const failed = results.length - passed;\n\n lines.push('');\n lines.push(chalk.bold(`Validation Summary: ${passed} passed, ${failed} failed`));\n\n return lines.join('\\n');\n}\n\nexport interface TestResultSummary {\n total: number;\n passed: number;\n failed: number;\n passRate: number;\n duration: number;\n}\n\nexport interface FormattedTestResult {\n testCaseId: string;\n passed: boolean;\n duration: number;\n assertions: Array<{\n passed: boolean;\n type: string;\n message: string;\n }>;\n}\n\nexport function formatTestResult(result: FormattedTestResult, verbose = false): string {\n const status = result.passed\n ? chalk.green('✓ PASS')\n : chalk.red('✗ FAIL');\n \n const duration = chalk.gray(`(${result.duration}ms)`);\n let output = `${status} ${result.testCaseId} ${duration}`;\n\n if (verbose || !result.passed) {\n const assertionLines = result.assertions\n .filter(a => verbose || !a.passed)\n .map(a => {\n const icon = a.passed ? chalk.green(' ✓') : chalk.red(' ✗');\n return `${icon} [${a.type}] ${a.message}`;\n });\n if (assertionLines.length > 0) {\n output += '\\n' + assertionLines.join('\\n');\n }\n }\n\n return output;\n}\n\nexport function formatTestSummary(summary: TestResultSummary): string {\n const passRate = summary.passRate.toFixed(1);\n const statusColor = summary.failed === 0 ? chalk.green : chalk.red;\n \n return [\n '',\n chalk.bold('─'.repeat(50)),\n chalk.bold('Test Summary'),\n ` Total: ${summary.total}`,\n ` ${chalk.green('Passed:')} ${summary.passed}`,\n ` ${chalk.red('Failed:')} ${summary.failed}`,\n ` Pass Rate: ${statusColor(passRate + '%')}`,\n ` Duration: ${summary.duration}ms`,\n chalk.bold('─'.repeat(50))\n ].join('\\n');\n}\n\nexport function formatTestResults(\n results: FormattedTestResult[],\n summary: TestResultSummary,\n options: FormatOptions = {}\n): string {\n const { format = 'text', verbose = false } = options;\n\n if (format === 'json') {\n return formatJson({ results, summary });\n }\n\n const lines = results.map(r => formatTestResult(r, verbose));\n lines.push(formatTestSummary(summary));\n\n return lines.join('\\n');\n}\n\nexport function formatProtocolList(protocols: string[], options: FormatOptions = {}): string {\n const { format = 'text' } = options;\n\n if (format === 'json') {\n return formatJson({ protocols });\n }\n\n return [\n chalk.bold('Supported Protocols:'),\n ...protocols.map(p => ` - ${p}`)\n ].join('\\n');\n}\n\nexport function formatParsedTestCase(testCase: unknown, options: FormatOptions = {}): string {\n const { format = 'text' } = options;\n\n if (format === 'json') {\n return formatJson(testCase);\n }\n\n // For text format, show a simplified view\n const tc = testCase as Record<string, unknown>;\n const lines: string[] = [];\n \n lines.push(chalk.bold(`Test Case: ${tc.id || 'unknown'}`));\n if (tc.description) lines.push(` Description: ${tc.description}`);\n if (tc.type) lines.push(` Protocol: ${tc.type}`);\n \n return lines.join('\\n');\n}\n","import chalk from 'chalk';\n\nexport type LogLevel = 'debug' | 'info' | 'success' | 'warn' | 'error';\n\nexport interface LoggerOptions {\n verbose?: boolean;\n quiet?: boolean;\n}\n\nlet globalOptions: LoggerOptions = {};\n\nexport function setLoggerOptions(options: LoggerOptions): void {\n globalOptions = { ...globalOptions, ...options };\n}\n\nexport function debug(message: string, ...args: unknown[]): void {\n if (globalOptions.verbose && !globalOptions.quiet) {\n console.log(chalk.gray(`[debug] ${message}`), ...args);\n }\n}\n\nexport function info(message: string, ...args: unknown[]): void {\n if (!globalOptions.quiet) {\n console.log(chalk.blue(message), ...args);\n }\n}\n\nexport function success(message: string, ...args: unknown[]): void {\n if (!globalOptions.quiet) {\n console.log(chalk.green(message), ...args);\n }\n}\n\nexport function warn(message: string, ...args: unknown[]): void {\n console.warn(chalk.yellow(`[warn] ${message}`), ...args);\n}\n\nexport function error(message: string, ...args: unknown[]): void {\n console.error(chalk.red(`[error] ${message}`), ...args);\n}\n\nexport function log(message: string, ...args: unknown[]): void {\n console.log(message, ...args);\n}\n\nexport function newline(): void {\n if (!globalOptions.quiet) {\n console.log();\n }\n}\n\nexport const logger = {\n debug,\n info,\n success,\n warn,\n error,\n log,\n newline,\n setOptions: setLoggerOptions\n};\n","import { Command } from 'commander';\nimport ora from 'ora';\nimport { validateTestCase } from '@boolesai/tspec';\nimport { discoverTSpecFiles } from '../utils/files.js';\nimport { formatValidationResults } from '../utils/formatter.js';\nimport { logger, setLoggerOptions } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface ValidateOptions {\n output?: OutputFormat;\n quiet?: boolean;\n}\n\nexport const validateCommand = new Command('validate')\n .description('Validate .tspec files for schema correctness')\n .argument('<files...>', 'Files or glob patterns to validate')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .option('-q, --quiet', 'Only output errors')\n .action(async (files: string[], options: ValidateOptions) => {\n setLoggerOptions({ quiet: options.quiet });\n \n const spinner = options.quiet ? null : ora('Discovering files...').start();\n \n try {\n // Discover files without loading content\n const { files: fileDescriptors, errors: resolveErrors } = await discoverTSpecFiles(files);\n \n if (resolveErrors.length > 0 && !options.quiet) {\n resolveErrors.forEach(err => logger.warn(err));\n }\n \n if (fileDescriptors.length === 0) {\n spinner?.fail('No .tspec files found');\n process.exit(2);\n }\n \n if (spinner) spinner.text = `Validating ${fileDescriptors.length} file(s)...`;\n \n // Validate each file individually (lazy loading - content read on-demand)\n const results = fileDescriptors.map(descriptor => ({\n file: descriptor.path,\n result: validateTestCase(descriptor.path)\n }));\n \n spinner?.stop();\n \n const output = formatValidationResults(results, { format: options.output });\n logger.log(output);\n \n const hasErrors = results.some(r => !r.result.valid);\n process.exit(hasErrors ? 1 : 0);\n } catch (err) {\n spinner?.fail('Validation failed');\n const message = err instanceof Error ? err.message : String(err);\n logger.error(message);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport ora from 'ora';\nimport chalk from 'chalk';\nimport { parseTestCases, scheduler, clearTemplateCache } from '@boolesai/tspec';\nimport type { TestCase, TestResult, ScheduleResult } from '@boolesai/tspec';\nimport { discoverTSpecFiles, type TSpecFileDescriptor } from '../utils/files.js';\nimport { formatTestResults, formatJson, type FormattedTestResult, type TestResultSummary } from '../utils/formatter.js';\nimport { logger, setLoggerOptions } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface RunOptions {\n output?: OutputFormat;\n concurrency?: string;\n verbose?: boolean;\n quiet?: boolean;\n failFast?: boolean;\n env: Record<string, string>;\n params: Record<string, string>;\n}\n\ninterface FileRunResult {\n file: string;\n testCases: number;\n results: TestResult[];\n parseError?: string;\n}\n\nfunction parseKeyValue(value: string, previous: Record<string, string> = {}): Record<string, string> {\n const [key, val] = value.split('=');\n if (key && val !== undefined) {\n previous[key] = val;\n }\n return previous;\n}\n\nfunction formatResult(result: TestResult): FormattedTestResult {\n return {\n testCaseId: result.testCaseId,\n passed: result.passed,\n duration: result.duration,\n assertions: result.assertions.map(a => ({\n passed: a.passed,\n type: a.type,\n message: a.message\n }))\n };\n}\n\nasync function runFileTestCases(\n descriptor: TSpecFileDescriptor,\n options: RunOptions,\n concurrency: number,\n spinner: ReturnType<typeof ora> | null\n): Promise<FileRunResult> {\n const result: FileRunResult = {\n file: descriptor.path,\n testCases: 0,\n results: []\n };\n\n // Parse file on-demand\n let testCases: TestCase[];\n try {\n testCases = parseTestCases(descriptor.path, {\n env: options.env,\n params: options.params\n });\n result.testCases = testCases.length;\n } catch (err) {\n result.parseError = err instanceof Error ? err.message : String(err);\n return result;\n }\n\n if (testCases.length === 0) {\n return result;\n }\n\n // Execute test cases for this file\n if (spinner) spinner.text = `Running: ${descriptor.relativePath} (${testCases.length} test(s))...`;\n\n if (options.failFast) {\n // For fail-fast, execute sequentially\n for (const testCase of testCases) {\n try {\n const scheduleResult = await scheduler.schedule([testCase], { concurrency: 1 });\n result.results.push(...scheduleResult.results);\n \n if (!scheduleResult.results[0]?.passed) {\n break; // Stop on first failure within file\n }\n } catch {\n break;\n }\n }\n } else {\n // Execute all test cases for this file with concurrency\n const scheduleResult = await scheduler.schedule(testCases, { concurrency });\n result.results = scheduleResult.results;\n }\n\n return result;\n}\n\nexport const runCommand = new Command('run')\n .description('Execute test cases and report results')\n .argument('<files...>', 'Files or glob patterns to run')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .option('-c, --concurrency <number>', 'Max concurrent tests', '5')\n .option('-e, --env <key=value>', 'Environment variables', parseKeyValue, {})\n .option('-p, --params <key=value>', 'Parameters', parseKeyValue, {})\n .option('-v, --verbose', 'Verbose output')\n .option('-q, --quiet', 'Only output summary')\n .option('--fail-fast', 'Stop on first failure')\n .action(async (files: string[], options: RunOptions) => {\n setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });\n \n // Clear template cache to ensure fresh reads for this run\n clearTemplateCache();\n \n const concurrency = parseInt(options.concurrency || '5', 10);\n const spinner = options.quiet ? null : ora('Discovering files...').start();\n \n try {\n // Discover files without loading content\n const { files: fileDescriptors, errors: resolveErrors } = await discoverTSpecFiles(files);\n \n if (resolveErrors.length > 0 && !options.quiet) {\n resolveErrors.forEach(err => logger.warn(err));\n }\n \n if (fileDescriptors.length === 0) {\n spinner?.fail('No .tspec files found');\n process.exit(2);\n }\n \n if (spinner) spinner.text = `Running ${fileDescriptors.length} file(s)...`;\n \n // Parse and execute one file at a time (lazy loading)\n const allResults: TestResult[] = [];\n const parseErrors: Array<{ file: string; error: string }> = [];\n let totalTestCases = 0;\n let stopped = false;\n \n for (const descriptor of fileDescriptors) {\n if (stopped) break;\n \n const fileResult = await runFileTestCases(descriptor, options, concurrency, spinner);\n \n if (fileResult.parseError) {\n parseErrors.push({ file: fileResult.file, error: fileResult.parseError });\n continue;\n }\n \n totalTestCases += fileResult.testCases;\n allResults.push(...fileResult.results);\n \n // Check fail-fast across files\n if (options.failFast && fileResult.results.some(r => !r.passed)) {\n stopped = true;\n }\n }\n \n spinner?.stop();\n \n if (totalTestCases === 0 && parseErrors.length === fileDescriptors.length) {\n logger.error('No test cases found - all files failed to parse');\n parseErrors.forEach(({ file, error }) => logger.error(` ${file}: ${error}`));\n process.exit(2);\n }\n \n // Calculate summary\n const passed = allResults.filter(r => r.passed).length;\n const failed = allResults.length - passed;\n const summary: TestResultSummary = {\n total: allResults.length,\n passed,\n failed,\n passRate: allResults.length > 0 ? (passed / allResults.length) * 100 : 0,\n duration: 0\n };\n \n // Format and output results\n const formattedResults = allResults.map(formatResult);\n \n if (options.output === 'json') {\n logger.log(formatJson({\n results: formattedResults,\n summary,\n parseErrors\n }));\n } else {\n if (!options.quiet) {\n const output = formatTestResults(formattedResults, summary, {\n format: options.output,\n verbose: options.verbose\n });\n logger.log(output);\n } else {\n // Quiet mode: just show summary line\n const statusColor = summary.failed === 0 ? chalk.green : chalk.red;\n logger.log(statusColor(`${summary.passed}/${summary.total} tests passed (${summary.passRate.toFixed(1)}%)`));\n }\n \n if (parseErrors.length > 0) {\n logger.newline();\n logger.warn(`${parseErrors.length} file(s) failed to parse:`);\n for (const { file, error } of parseErrors) {\n logger.error(` ${file}: ${error}`);\n }\n }\n }\n \n // Exit code: 0 if all passed, 1 if any failed\n process.exit(failed > 0 ? 1 : 0);\n } catch (err) {\n spinner?.fail('Execution failed');\n const message = err instanceof Error ? err.message : String(err);\n logger.error(message);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport ora from 'ora';\nimport { parseTestCases, clearTemplateCache } from '@boolesai/tspec';\nimport { discoverTSpecFiles } from '../utils/files.js';\nimport { formatParsedTestCase, formatJson } from '../utils/formatter.js';\nimport { logger, setLoggerOptions } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface ParseOptions {\n output?: OutputFormat;\n verbose?: boolean;\n quiet?: boolean;\n}\n\nfunction parseKeyValue(value: string, previous: Record<string, string> = {}): Record<string, string> {\n const [key, val] = value.split('=');\n if (key && val !== undefined) {\n previous[key] = val;\n }\n return previous;\n}\n\nexport const parseCommand = new Command('parse')\n .description('Parse and display test case information without execution')\n .argument('<files...>', 'Files or glob patterns to parse')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .option('-v, --verbose', 'Show detailed information')\n .option('-q, --quiet', 'Minimal output')\n .option('-e, --env <key=value>', 'Environment variables', parseKeyValue, {})\n .option('-p, --params <key=value>', 'Parameters', parseKeyValue, {})\n .action(async (files: string[], options: ParseOptions & { env: Record<string, string>; params: Record<string, string> }) => {\n setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });\n \n // Clear template cache to ensure fresh reads for this parse\n clearTemplateCache();\n \n const spinner = options.quiet ? null : ora('Discovering files...').start();\n \n try {\n // Discover files without loading content\n const { files: fileDescriptors, errors: resolveErrors } = await discoverTSpecFiles(files);\n \n if (resolveErrors.length > 0 && !options.quiet) {\n resolveErrors.forEach(err => logger.warn(err));\n }\n \n if (fileDescriptors.length === 0) {\n spinner?.fail('No .tspec files found');\n process.exit(2);\n }\n \n if (spinner) spinner.text = `Parsing ${fileDescriptors.length} file(s)...`;\n \n const allTestCases: unknown[] = [];\n const parseErrors: Array<{ file: string; error: string }> = [];\n \n // Parse each file individually (lazy loading - content read on-demand)\n for (const descriptor of fileDescriptors) {\n try {\n const testCases = parseTestCases(descriptor.path, {\n env: options.env,\n params: options.params\n });\n allTestCases.push(...testCases);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n parseErrors.push({ file: descriptor.path, error: message });\n }\n }\n \n spinner?.stop();\n \n if (options.output === 'json') {\n logger.log(formatJson({\n testCases: allTestCases,\n errors: parseErrors,\n summary: {\n totalFiles: fileDescriptors.length,\n totalTestCases: allTestCases.length,\n parseErrors: parseErrors.length\n }\n }));\n } else {\n logger.info(`Parsed ${allTestCases.length} test case(s) from ${fileDescriptors.length} file(s)`);\n logger.newline();\n \n for (const testCase of allTestCases) {\n logger.log(formatParsedTestCase(testCase, { format: options.output, verbose: options.verbose }));\n logger.newline();\n }\n \n if (parseErrors.length > 0) {\n logger.newline();\n logger.warn(`${parseErrors.length} file(s) failed to parse:`);\n for (const { file, error } of parseErrors) {\n logger.error(` ${file}: ${error}`);\n }\n }\n }\n \n process.exit(parseErrors.length > 0 ? 1 : 0);\n } catch (err) {\n spinner?.fail('Parse failed');\n const message = err instanceof Error ? err.message : String(err);\n logger.error(message);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport { registry } from '@boolesai/tspec';\nimport { formatProtocolList } from '../utils/formatter.js';\nimport { logger } from '../utils/logger.js';\nimport type { OutputFormat } from '../utils/formatter.js';\n\ninterface ListOptions {\n output?: OutputFormat;\n}\n\nexport const listCommand = new Command('list')\n .description('List supported protocols and configuration')\n .option('-o, --output <format>', 'Output format: json, text', 'text')\n .action(async (options: ListOptions) => {\n try {\n const protocols = registry.getRegisteredTypes();\n const output = formatProtocolList(protocols, { format: options.output });\n logger.log(output);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`Failed to list protocols: ${message}`);\n process.exit(2);\n }\n });\n","import { Command } from 'commander';\nimport { validateCommand } from './commands/validate.js';\nimport { runCommand } from './commands/run.js';\nimport { parseCommand } from './commands/parse.js';\nimport { listCommand } from './commands/list.js';\n\nconst program = new Command();\n\nprogram\n .name('tspec')\n .description('CLI for @boolesai/tspec testing framework')\n .version('1.0.0');\n\nprogram.addCommand(validateCommand);\nprogram.addCommand(runCommand);\nprogram.addCommand(parseCommand);\nprogram.addCommand(listCommand);\n\nprogram.parse();\n"],"names":["parseKeyValue","error"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boolesai/tspec-cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "CLI for @boolesai/tspec testing framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,16 +22,16 @@
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@boolesai/tspec": "^0.0.1",
26
- "commander": "^12.0.0",
25
+ "@boolesai/tspec": "^0.0.3",
27
26
  "chalk": "^5.0.0",
27
+ "commander": "^12.0.0",
28
28
  "glob": "^11.0.0",
29
29
  "ora": "^8.0.0"
30
30
  },
31
31
  "devDependencies": {
32
+ "@types/node": "^22.0.0",
32
33
  "typescript": "^5.0.0",
33
- "vite": "^7.0.0",
34
- "@types/node": "^22.0.0"
34
+ "vite": "^7.0.0"
35
35
  },
36
36
  "keywords": [
37
37
  "testing",
@@ -1,7 +1,35 @@
1
+ export type ProtocolType = 'http' | 'grpc' | 'graphql' | 'websocket';
1
2
  export interface FileResolutionResult {
2
3
  files: string[];
3
4
  errors: string[];
4
5
  }
6
+ /**
7
+ * Represents a discovered .tspec file without loading its content.
8
+ * Used for lazy file loading - files are scanned first, content read on-demand.
9
+ */
10
+ export interface TSpecFileDescriptor {
11
+ /** Absolute path to the file */
12
+ path: string;
13
+ /** Path relative to working directory */
14
+ relativePath: string;
15
+ /** Base filename (e.g., "login.http.tspec") */
16
+ fileName: string;
17
+ /** Protocol type extracted from filename (e.g., "http" from "login.http.tspec") */
18
+ protocol: ProtocolType | null;
19
+ }
20
+ export interface DiscoveryResult {
21
+ files: TSpecFileDescriptor[];
22
+ errors: string[];
23
+ }
5
24
  export declare function resolveFiles(patterns: string[], cwd?: string): Promise<FileResolutionResult>;
6
25
  export declare function filterByExtension(files: string[], extension: string): string[];
7
26
  export declare function getTspecFiles(files: string[]): string[];
27
+ /**
28
+ * Discovers .tspec files without loading their content.
29
+ * Files are scanned and classified, content is read on-demand during validation/parsing/execution.
30
+ *
31
+ * @param patterns - File paths, directory paths, or glob patterns
32
+ * @param cwd - Working directory for resolving relative paths
33
+ * @returns DiscoveryResult with file descriptors and any errors
34
+ */
35
+ export declare function discoverTSpecFiles(patterns: string[], cwd?: string): Promise<DiscoveryResult>;