@boolesai/tspec-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/bin/tspec.js +2 -0
- package/dist/index.js +420 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/types/commands/list.d.ts +2 -0
- package/types/commands/parse.d.ts +2 -0
- package/types/commands/run.d.ts +2 -0
- package/types/commands/validate.d.ts +2 -0
- package/types/index.d.ts +1 -0
- package/types/utils/files.d.ts +7 -0
- package/types/utils/formatter.d.ts +35 -0
- package/types/utils/logger.d.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# @boolesai/tspec-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for TSpec - a multi-protocol testing DSL designed for Developer + AI collaboration.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@boolesai/tspec-cli)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @boolesai/tspec-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or run directly with npx:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @boolesai/tspec-cli <command>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### `tspec validate`
|
|
23
|
+
|
|
24
|
+
Validate `.tspec` files for schema correctness.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
tspec validate <files...> [options]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Options:**
|
|
31
|
+
- `-o, --output <format>` - Output format: `json`, `text` (default: `text`)
|
|
32
|
+
- `-q, --quiet` - Only output errors
|
|
33
|
+
|
|
34
|
+
**Examples:**
|
|
35
|
+
```bash
|
|
36
|
+
# Validate a single file
|
|
37
|
+
tspec validate tests/login.http.tspec
|
|
38
|
+
|
|
39
|
+
# Validate multiple files with glob pattern
|
|
40
|
+
tspec validate "tests/**/*.tspec"
|
|
41
|
+
|
|
42
|
+
# JSON output for CI/CD
|
|
43
|
+
tspec validate tests/*.tspec --output json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `tspec run`
|
|
47
|
+
|
|
48
|
+
Execute test cases and report results.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
tspec run <files...> [options]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Options:**
|
|
55
|
+
- `-o, --output <format>` - Output format: `json`, `text` (default: `text`)
|
|
56
|
+
- `-c, --concurrency <n>` - Max concurrent tests (default: `5`)
|
|
57
|
+
- `-e, --env <key=value>` - Environment variables (repeatable)
|
|
58
|
+
- `-p, --params <key=value>` - Parameters (repeatable)
|
|
59
|
+
- `-v, --verbose` - Verbose output
|
|
60
|
+
- `-q, --quiet` - Only output summary
|
|
61
|
+
- `--fail-fast` - Stop on first failure
|
|
62
|
+
|
|
63
|
+
**Examples:**
|
|
64
|
+
```bash
|
|
65
|
+
# Run tests with default settings
|
|
66
|
+
tspec run tests/*.http.tspec
|
|
67
|
+
|
|
68
|
+
# Run with environment variables
|
|
69
|
+
tspec run tests/*.tspec -e API_HOST=api.example.com -e API_KEY=secret
|
|
70
|
+
|
|
71
|
+
# Run with parameters
|
|
72
|
+
tspec run tests/*.tspec -p username=testuser -p timeout=5000
|
|
73
|
+
|
|
74
|
+
# Run with higher concurrency
|
|
75
|
+
tspec run tests/*.tspec -c 10
|
|
76
|
+
|
|
77
|
+
# Verbose output for debugging
|
|
78
|
+
tspec run tests/*.tspec -v
|
|
79
|
+
|
|
80
|
+
# JSON output for CI/CD
|
|
81
|
+
tspec run tests/*.tspec --output json
|
|
82
|
+
|
|
83
|
+
# Stop on first failure
|
|
84
|
+
tspec run tests/*.tspec --fail-fast
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `tspec parse`
|
|
88
|
+
|
|
89
|
+
Parse and display test case information without execution.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
tspec parse <files...> [options]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Options:**
|
|
96
|
+
- `-o, --output <format>` - Output format: `json`, `text` (default: `text`)
|
|
97
|
+
- `-v, --verbose` - Show detailed information
|
|
98
|
+
- `-q, --quiet` - Minimal output
|
|
99
|
+
- `-e, --env <key=value>` - Environment variables
|
|
100
|
+
- `-p, --params <key=value>` - Parameters
|
|
101
|
+
|
|
102
|
+
**Examples:**
|
|
103
|
+
```bash
|
|
104
|
+
# Parse and display test cases
|
|
105
|
+
tspec parse tests/login.http.tspec
|
|
106
|
+
|
|
107
|
+
# JSON output for inspection
|
|
108
|
+
tspec parse tests/*.tspec --output json
|
|
109
|
+
|
|
110
|
+
# With variable substitution
|
|
111
|
+
tspec parse tests/*.tspec -e API_HOST=localhost
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `tspec list`
|
|
115
|
+
|
|
116
|
+
List supported protocols and configuration.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
tspec list [options]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Options:**
|
|
123
|
+
- `-o, --output <format>` - Output format: `json`, `text` (default: `text`)
|
|
124
|
+
|
|
125
|
+
**Examples:**
|
|
126
|
+
```bash
|
|
127
|
+
# List supported protocols
|
|
128
|
+
tspec list
|
|
129
|
+
|
|
130
|
+
# JSON output
|
|
131
|
+
tspec list --output json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Exit Codes
|
|
135
|
+
|
|
136
|
+
| Code | Description |
|
|
137
|
+
|------|-------------|
|
|
138
|
+
| `0` | Success (all tests passed / validation passed) |
|
|
139
|
+
| `1` | Failure (tests failed / validation errors) |
|
|
140
|
+
| `2` | Error (invalid input / configuration error) |
|
|
141
|
+
|
|
142
|
+
## CI/CD Integration
|
|
143
|
+
|
|
144
|
+
### GitHub Actions
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
- name: Run TSpec tests
|
|
148
|
+
run: |
|
|
149
|
+
npx @boolesai/tspec-cli run tests/*.tspec --output json > results.json
|
|
150
|
+
|
|
151
|
+
- name: Validate TSpec files
|
|
152
|
+
run: npx @boolesai/tspec-cli validate tests/*.tspec
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### GitLab CI
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
test:
|
|
159
|
+
script:
|
|
160
|
+
- npx @boolesai/tspec-cli run tests/*.tspec --output json
|
|
161
|
+
artifacts:
|
|
162
|
+
reports:
|
|
163
|
+
junit: results.json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Build from Source
|
|
167
|
+
|
|
168
|
+
Prerequisites:
|
|
169
|
+
- Node.js >= 18.0.0
|
|
170
|
+
- npm >= 9.0.0
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Clone the repository
|
|
174
|
+
git clone https://github.com/boolesai/testing-spec.git
|
|
175
|
+
cd testing-spec
|
|
176
|
+
|
|
177
|
+
# Build core package first (required dependency)
|
|
178
|
+
cd core
|
|
179
|
+
npm install
|
|
180
|
+
npm run package
|
|
181
|
+
npm link
|
|
182
|
+
|
|
183
|
+
# Build CLI package
|
|
184
|
+
cd ../cli
|
|
185
|
+
npm install
|
|
186
|
+
npm link @boolesai/tspec
|
|
187
|
+
npm run build
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Build Output
|
|
191
|
+
|
|
192
|
+
- `dist/` - Compiled JavaScript
|
|
193
|
+
- `types/` - TypeScript type definitions
|
|
194
|
+
- `bin/` - Executable entry point
|
|
195
|
+
|
|
196
|
+
### Install Built CLI Globally
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# From the cli directory
|
|
200
|
+
npm install -g .
|
|
201
|
+
|
|
202
|
+
# Or link for development
|
|
203
|
+
npm link
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Create Distribution Package
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Create a tarball for distribution
|
|
210
|
+
npm run package
|
|
211
|
+
|
|
212
|
+
# This generates @boolesai-tspec-cli-0.0.1.tgz
|
|
213
|
+
# Install from tarball:
|
|
214
|
+
npm install -g ./boolesai-tspec-cli-0.0.1.tgz
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Development Mode
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Watch mode for development
|
|
221
|
+
npm run dev
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Documentation
|
|
225
|
+
|
|
226
|
+
For complete TSpec DSL documentation, see the [docs](../doc) directory.
|
|
227
|
+
|
|
228
|
+
## Related
|
|
229
|
+
|
|
230
|
+
- [@boolesai/tspec](https://www.npmjs.com/package/@boolesai/tspec) - Core library for TSpec
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
package/bin/tspec.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { validateTestCase, parseTestCases, scheduler, registry } from "@boolesai/tspec";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
import { isAbsolute, resolve } from "path";
|
|
6
|
+
import { existsSync, statSync } from "fs";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
async function resolveFiles(patterns, cwd) {
|
|
9
|
+
const workingDir = process.cwd();
|
|
10
|
+
const files = [];
|
|
11
|
+
const errors = [];
|
|
12
|
+
for (const pattern of patterns) {
|
|
13
|
+
const absolutePath = isAbsolute(pattern) ? pattern : resolve(workingDir, pattern);
|
|
14
|
+
if (existsSync(absolutePath)) {
|
|
15
|
+
const stat = statSync(absolutePath);
|
|
16
|
+
if (stat.isFile()) {
|
|
17
|
+
files.push(absolutePath);
|
|
18
|
+
continue;
|
|
19
|
+
} else if (stat.isDirectory()) {
|
|
20
|
+
const dirFiles = await glob("**/*.tspec", { cwd: absolutePath, absolute: true });
|
|
21
|
+
if (dirFiles.length === 0) {
|
|
22
|
+
errors.push(`No .tspec files found in directory: ${pattern}`);
|
|
23
|
+
} else {
|
|
24
|
+
files.push(...dirFiles);
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const matches = await glob(pattern, { cwd: workingDir, absolute: true });
|
|
30
|
+
if (matches.length === 0) {
|
|
31
|
+
errors.push(`No files matched pattern: ${pattern}`);
|
|
32
|
+
} else {
|
|
33
|
+
files.push(...matches);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
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");
|
|
44
|
+
}
|
|
45
|
+
function formatJson(data) {
|
|
46
|
+
return JSON.stringify(data, null, 2);
|
|
47
|
+
}
|
|
48
|
+
function formatValidationResult(result, filePath) {
|
|
49
|
+
if (result.valid) {
|
|
50
|
+
return chalk.green(`✓ ${filePath}`);
|
|
51
|
+
}
|
|
52
|
+
const errors = result.errors.map((e) => chalk.red(` - ${e}`)).join("\n");
|
|
53
|
+
return `${chalk.red(`✗ ${filePath}`)}
|
|
54
|
+
${errors}`;
|
|
55
|
+
}
|
|
56
|
+
function formatValidationResults(results, options = {}) {
|
|
57
|
+
const { format = "text" } = options;
|
|
58
|
+
if (format === "json") {
|
|
59
|
+
return formatJson(results.map((r) => ({
|
|
60
|
+
file: r.file,
|
|
61
|
+
valid: r.result.valid,
|
|
62
|
+
errors: r.result.errors
|
|
63
|
+
})));
|
|
64
|
+
}
|
|
65
|
+
const lines = results.map((r) => formatValidationResult(r.result, r.file));
|
|
66
|
+
const passed = results.filter((r) => r.result.valid).length;
|
|
67
|
+
const failed = results.length - passed;
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push(chalk.bold(`Validation Summary: ${passed} passed, ${failed} failed`));
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
function formatTestResult(result, verbose = false) {
|
|
73
|
+
const status = result.passed ? chalk.green("✓ PASS") : chalk.red("✗ FAIL");
|
|
74
|
+
const duration = chalk.gray(`(${result.duration}ms)`);
|
|
75
|
+
let output = `${status} ${result.testCaseId} ${duration}`;
|
|
76
|
+
if (verbose || !result.passed) {
|
|
77
|
+
const assertionLines = result.assertions.filter((a) => verbose || !a.passed).map((a) => {
|
|
78
|
+
const icon = a.passed ? chalk.green(" ✓") : chalk.red(" ✗");
|
|
79
|
+
return `${icon} [${a.type}] ${a.message}`;
|
|
80
|
+
});
|
|
81
|
+
if (assertionLines.length > 0) {
|
|
82
|
+
output += "\n" + assertionLines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return output;
|
|
86
|
+
}
|
|
87
|
+
function formatTestSummary(summary) {
|
|
88
|
+
const passRate = summary.passRate.toFixed(1);
|
|
89
|
+
const statusColor = summary.failed === 0 ? chalk.green : chalk.red;
|
|
90
|
+
return [
|
|
91
|
+
"",
|
|
92
|
+
chalk.bold("─".repeat(50)),
|
|
93
|
+
chalk.bold("Test Summary"),
|
|
94
|
+
` Total: ${summary.total}`,
|
|
95
|
+
` ${chalk.green("Passed:")} ${summary.passed}`,
|
|
96
|
+
` ${chalk.red("Failed:")} ${summary.failed}`,
|
|
97
|
+
` Pass Rate: ${statusColor(passRate + "%")}`,
|
|
98
|
+
` Duration: ${summary.duration}ms`,
|
|
99
|
+
chalk.bold("─".repeat(50))
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
function formatTestResults(results, summary, options = {}) {
|
|
103
|
+
const { format = "text", verbose = false } = options;
|
|
104
|
+
if (format === "json") {
|
|
105
|
+
return formatJson({ results, summary });
|
|
106
|
+
}
|
|
107
|
+
const lines = results.map((r) => formatTestResult(r, verbose));
|
|
108
|
+
lines.push(formatTestSummary(summary));
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
function formatProtocolList(protocols, options = {}) {
|
|
112
|
+
const { format = "text" } = options;
|
|
113
|
+
if (format === "json") {
|
|
114
|
+
return formatJson({ protocols });
|
|
115
|
+
}
|
|
116
|
+
return [
|
|
117
|
+
chalk.bold("Supported Protocols:"),
|
|
118
|
+
...protocols.map((p) => ` - ${p}`)
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
function formatParsedTestCase(testCase, options = {}) {
|
|
122
|
+
const { format = "text" } = options;
|
|
123
|
+
if (format === "json") {
|
|
124
|
+
return formatJson(testCase);
|
|
125
|
+
}
|
|
126
|
+
const tc = testCase;
|
|
127
|
+
const lines = [];
|
|
128
|
+
lines.push(chalk.bold(`Test Case: ${tc.id || "unknown"}`));
|
|
129
|
+
if (tc.description) lines.push(` Description: ${tc.description}`);
|
|
130
|
+
if (tc.type) lines.push(` Protocol: ${tc.type}`);
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
let globalOptions = {};
|
|
134
|
+
function setLoggerOptions(options) {
|
|
135
|
+
globalOptions = { ...globalOptions, ...options };
|
|
136
|
+
}
|
|
137
|
+
function debug(message, ...args) {
|
|
138
|
+
if (globalOptions.verbose && !globalOptions.quiet) {
|
|
139
|
+
console.log(chalk.gray(`[debug] ${message}`), ...args);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function info(message, ...args) {
|
|
143
|
+
if (!globalOptions.quiet) {
|
|
144
|
+
console.log(chalk.blue(message), ...args);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function success(message, ...args) {
|
|
148
|
+
if (!globalOptions.quiet) {
|
|
149
|
+
console.log(chalk.green(message), ...args);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function warn(message, ...args) {
|
|
153
|
+
console.warn(chalk.yellow(`[warn] ${message}`), ...args);
|
|
154
|
+
}
|
|
155
|
+
function error(message, ...args) {
|
|
156
|
+
console.error(chalk.red(`[error] ${message}`), ...args);
|
|
157
|
+
}
|
|
158
|
+
function log(message, ...args) {
|
|
159
|
+
console.log(message, ...args);
|
|
160
|
+
}
|
|
161
|
+
function newline() {
|
|
162
|
+
if (!globalOptions.quiet) {
|
|
163
|
+
console.log();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const logger = {
|
|
167
|
+
debug,
|
|
168
|
+
info,
|
|
169
|
+
success,
|
|
170
|
+
warn,
|
|
171
|
+
error,
|
|
172
|
+
log,
|
|
173
|
+
newline,
|
|
174
|
+
setOptions: setLoggerOptions
|
|
175
|
+
};
|
|
176
|
+
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
|
+
setLoggerOptions({ quiet: options.quiet });
|
|
178
|
+
const spinner = options.quiet ? null : ora("Resolving files...").start();
|
|
179
|
+
try {
|
|
180
|
+
const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);
|
|
181
|
+
const tspecFiles = getTspecFiles(resolvedFiles);
|
|
182
|
+
if (resolveErrors.length > 0 && !options.quiet) {
|
|
183
|
+
resolveErrors.forEach((err) => logger.warn(err));
|
|
184
|
+
}
|
|
185
|
+
if (tspecFiles.length === 0) {
|
|
186
|
+
spinner?.fail("No .tspec files found");
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
if (spinner) spinner.text = `Validating ${tspecFiles.length} file(s)...`;
|
|
190
|
+
const results = tspecFiles.map((file) => ({
|
|
191
|
+
file,
|
|
192
|
+
result: validateTestCase(file)
|
|
193
|
+
}));
|
|
194
|
+
spinner?.stop();
|
|
195
|
+
const output = formatValidationResults(results, { format: options.output });
|
|
196
|
+
logger.log(output);
|
|
197
|
+
const hasErrors = results.some((r) => !r.result.valid);
|
|
198
|
+
process.exit(hasErrors ? 1 : 0);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
spinner?.fail("Validation failed");
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
202
|
+
logger.error(message);
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
function parseKeyValue$1(value, previous = {}) {
|
|
207
|
+
const [key, val] = value.split("=");
|
|
208
|
+
if (key && val !== void 0) {
|
|
209
|
+
previous[key] = val;
|
|
210
|
+
}
|
|
211
|
+
return previous;
|
|
212
|
+
}
|
|
213
|
+
function formatResult(result) {
|
|
214
|
+
return {
|
|
215
|
+
testCaseId: result.testCaseId,
|
|
216
|
+
passed: result.passed,
|
|
217
|
+
duration: result.duration,
|
|
218
|
+
assertions: result.assertions.map((a) => ({
|
|
219
|
+
passed: a.passed,
|
|
220
|
+
type: a.type,
|
|
221
|
+
message: a.message
|
|
222
|
+
}))
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
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
|
+
setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
|
|
227
|
+
const concurrency = parseInt(options.concurrency || "5", 10);
|
|
228
|
+
const spinner = options.quiet ? null : ora("Resolving files...").start();
|
|
229
|
+
try {
|
|
230
|
+
const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);
|
|
231
|
+
const tspecFiles = getTspecFiles(resolvedFiles);
|
|
232
|
+
if (resolveErrors.length > 0 && !options.quiet) {
|
|
233
|
+
resolveErrors.forEach((err) => logger.warn(err));
|
|
234
|
+
}
|
|
235
|
+
if (tspecFiles.length === 0) {
|
|
236
|
+
spinner?.fail("No .tspec files found");
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
if (spinner) spinner.text = `Parsing ${tspecFiles.length} file(s)...`;
|
|
240
|
+
const allTestCases = [];
|
|
241
|
+
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}`));
|
|
258
|
+
}
|
|
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
|
+
}
|
|
278
|
+
}
|
|
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
|
+
}
|
|
294
|
+
spinner?.stop();
|
|
295
|
+
const formattedResults = scheduleResult.results.map(formatResult);
|
|
296
|
+
const summary = {
|
|
297
|
+
...scheduleResult.summary,
|
|
298
|
+
duration: scheduleResult.duration
|
|
299
|
+
};
|
|
300
|
+
if (options.output === "json") {
|
|
301
|
+
logger.log(formatJson({
|
|
302
|
+
results: formattedResults,
|
|
303
|
+
summary,
|
|
304
|
+
parseErrors
|
|
305
|
+
}));
|
|
306
|
+
} else {
|
|
307
|
+
if (!options.quiet) {
|
|
308
|
+
const output = formatTestResults(formattedResults, summary, {
|
|
309
|
+
format: options.output,
|
|
310
|
+
verbose: options.verbose
|
|
311
|
+
});
|
|
312
|
+
logger.log(output);
|
|
313
|
+
} else {
|
|
314
|
+
const statusColor = summary.failed === 0 ? chalk.green : chalk.red;
|
|
315
|
+
logger.log(statusColor(`${summary.passed}/${summary.total} tests passed (${summary.passRate.toFixed(1)}%)`));
|
|
316
|
+
}
|
|
317
|
+
if (parseErrors.length > 0) {
|
|
318
|
+
logger.newline();
|
|
319
|
+
logger.warn(`${parseErrors.length} file(s) failed to parse:`);
|
|
320
|
+
for (const { file, error: error2 } of parseErrors) {
|
|
321
|
+
logger.error(` ${file}: ${error2}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
process.exit(scheduleResult.summary.failed > 0 ? 1 : 0);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
spinner?.fail("Execution failed");
|
|
328
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
329
|
+
logger.error(message);
|
|
330
|
+
process.exit(2);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
function parseKeyValue(value, previous = {}) {
|
|
334
|
+
const [key, val] = value.split("=");
|
|
335
|
+
if (key && val !== void 0) {
|
|
336
|
+
previous[key] = val;
|
|
337
|
+
}
|
|
338
|
+
return previous;
|
|
339
|
+
}
|
|
340
|
+
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
|
+
setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
|
|
342
|
+
const spinner = options.quiet ? null : ora("Resolving files...").start();
|
|
343
|
+
try {
|
|
344
|
+
const { files: resolvedFiles, errors: resolveErrors } = await resolveFiles(files);
|
|
345
|
+
const tspecFiles = getTspecFiles(resolvedFiles);
|
|
346
|
+
if (resolveErrors.length > 0 && !options.quiet) {
|
|
347
|
+
resolveErrors.forEach((err) => logger.warn(err));
|
|
348
|
+
}
|
|
349
|
+
if (tspecFiles.length === 0) {
|
|
350
|
+
spinner?.fail("No .tspec files found");
|
|
351
|
+
process.exit(2);
|
|
352
|
+
}
|
|
353
|
+
if (spinner) spinner.text = `Parsing ${tspecFiles.length} file(s)...`;
|
|
354
|
+
const allTestCases = [];
|
|
355
|
+
const parseErrors = [];
|
|
356
|
+
for (const file of tspecFiles) {
|
|
357
|
+
try {
|
|
358
|
+
const testCases = parseTestCases(file, {
|
|
359
|
+
env: options.env,
|
|
360
|
+
params: options.params
|
|
361
|
+
});
|
|
362
|
+
allTestCases.push(...testCases);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
365
|
+
parseErrors.push({ file, error: message });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
spinner?.stop();
|
|
369
|
+
if (options.output === "json") {
|
|
370
|
+
logger.log(formatJson({
|
|
371
|
+
testCases: allTestCases,
|
|
372
|
+
errors: parseErrors,
|
|
373
|
+
summary: {
|
|
374
|
+
totalFiles: tspecFiles.length,
|
|
375
|
+
totalTestCases: allTestCases.length,
|
|
376
|
+
parseErrors: parseErrors.length
|
|
377
|
+
}
|
|
378
|
+
}));
|
|
379
|
+
} else {
|
|
380
|
+
logger.info(`Parsed ${allTestCases.length} test case(s) from ${tspecFiles.length} file(s)`);
|
|
381
|
+
logger.newline();
|
|
382
|
+
for (const testCase of allTestCases) {
|
|
383
|
+
logger.log(formatParsedTestCase(testCase, { format: options.output, verbose: options.verbose }));
|
|
384
|
+
logger.newline();
|
|
385
|
+
}
|
|
386
|
+
if (parseErrors.length > 0) {
|
|
387
|
+
logger.newline();
|
|
388
|
+
logger.warn(`${parseErrors.length} file(s) failed to parse:`);
|
|
389
|
+
for (const { file, error: error2 } of parseErrors) {
|
|
390
|
+
logger.error(` ${file}: ${error2}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
process.exit(parseErrors.length > 0 ? 1 : 0);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
spinner?.fail("Parse failed");
|
|
397
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
398
|
+
logger.error(message);
|
|
399
|
+
process.exit(2);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
const listCommand = new Command("list").description("List supported protocols and configuration").option("-o, --output <format>", "Output format: json, text", "text").action(async (options) => {
|
|
403
|
+
try {
|
|
404
|
+
const protocols = registry.getRegisteredTypes();
|
|
405
|
+
const output = formatProtocolList(protocols, { format: options.output });
|
|
406
|
+
logger.log(output);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
409
|
+
logger.error(`Failed to list protocols: ${message}`);
|
|
410
|
+
process.exit(2);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
const program = new Command();
|
|
414
|
+
program.name("tspec").description("CLI for @boolesai/tspec testing framework").version("1.0.0");
|
|
415
|
+
program.addCommand(validateCommand);
|
|
416
|
+
program.addCommand(runCommand);
|
|
417
|
+
program.addCommand(parseCommand);
|
|
418
|
+
program.addCommand(listCommand);
|
|
419
|
+
program.parse();
|
|
420
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boolesai/tspec-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for @boolesai/tspec testing framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tspec": "./bin/tspec.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"types",
|
|
14
|
+
"bin"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "vite build && npm run types",
|
|
18
|
+
"types": "tsc --emitDeclarationOnly",
|
|
19
|
+
"dev": "vite build --watch",
|
|
20
|
+
"test": "node --test test/**/*.js",
|
|
21
|
+
"package": "npm run build && npm pack",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@boolesai/tspec": "^0.0.1",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"chalk": "^5.0.0",
|
|
28
|
+
"glob": "^11.0.0",
|
|
29
|
+
"ora": "^8.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^5.0.0",
|
|
33
|
+
"vite": "^7.0.0",
|
|
34
|
+
"@types/node": "^22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"testing",
|
|
38
|
+
"cli",
|
|
39
|
+
"tspec",
|
|
40
|
+
"specification",
|
|
41
|
+
"api-testing"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/boolesai/testing-spec.git",
|
|
47
|
+
"directory": "cli"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"author": "Booles AI",
|
|
53
|
+
"homepage": "https://github.com/boolesai/testing-spec#readme",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/boolesai/testing-spec/issues"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface FileResolutionResult {
|
|
2
|
+
files: string[];
|
|
3
|
+
errors: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function resolveFiles(patterns: string[], cwd?: string): Promise<FileResolutionResult>;
|
|
6
|
+
export declare function filterByExtension(files: string[], extension: string): string[];
|
|
7
|
+
export declare function getTspecFiles(files: string[]): string[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ValidationResult } from '@boolesai/tspec';
|
|
2
|
+
export type OutputFormat = 'json' | 'text' | 'table';
|
|
3
|
+
export interface FormatOptions {
|
|
4
|
+
format?: OutputFormat;
|
|
5
|
+
verbose?: boolean;
|
|
6
|
+
quiet?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function formatJson(data: unknown): string;
|
|
9
|
+
export declare function formatValidationResult(result: ValidationResult, filePath: string): string;
|
|
10
|
+
export declare function formatValidationResults(results: Array<{
|
|
11
|
+
file: string;
|
|
12
|
+
result: ValidationResult;
|
|
13
|
+
}>, options?: FormatOptions): string;
|
|
14
|
+
export interface TestResultSummary {
|
|
15
|
+
total: number;
|
|
16
|
+
passed: number;
|
|
17
|
+
failed: number;
|
|
18
|
+
passRate: number;
|
|
19
|
+
duration: number;
|
|
20
|
+
}
|
|
21
|
+
export interface FormattedTestResult {
|
|
22
|
+
testCaseId: string;
|
|
23
|
+
passed: boolean;
|
|
24
|
+
duration: number;
|
|
25
|
+
assertions: Array<{
|
|
26
|
+
passed: boolean;
|
|
27
|
+
type: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export declare function formatTestResult(result: FormattedTestResult, verbose?: boolean): string;
|
|
32
|
+
export declare function formatTestSummary(summary: TestResultSummary): string;
|
|
33
|
+
export declare function formatTestResults(results: FormattedTestResult[], summary: TestResultSummary, options?: FormatOptions): string;
|
|
34
|
+
export declare function formatProtocolList(protocols: string[], options?: FormatOptions): string;
|
|
35
|
+
export declare function formatParsedTestCase(testCase: unknown, options?: FormatOptions): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'success' | 'warn' | 'error';
|
|
2
|
+
export interface LoggerOptions {
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
quiet?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function setLoggerOptions(options: LoggerOptions): void;
|
|
7
|
+
export declare function debug(message: string, ...args: unknown[]): void;
|
|
8
|
+
export declare function info(message: string, ...args: unknown[]): void;
|
|
9
|
+
export declare function success(message: string, ...args: unknown[]): void;
|
|
10
|
+
export declare function warn(message: string, ...args: unknown[]): void;
|
|
11
|
+
export declare function error(message: string, ...args: unknown[]): void;
|
|
12
|
+
export declare function log(message: string, ...args: unknown[]): void;
|
|
13
|
+
export declare function newline(): void;
|
|
14
|
+
export declare const logger: {
|
|
15
|
+
debug: typeof debug;
|
|
16
|
+
info: typeof info;
|
|
17
|
+
success: typeof success;
|
|
18
|
+
warn: typeof warn;
|
|
19
|
+
error: typeof error;
|
|
20
|
+
log: typeof log;
|
|
21
|
+
newline: typeof newline;
|
|
22
|
+
setOptions: typeof setLoggerOptions;
|
|
23
|
+
};
|