@boolesai/tspec-cli 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -4
- package/dist/index.js +285 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/types/commands/plugin-install.d.ts +24 -0
- package/types/commands/plugin-list.d.ts +39 -0
- package/types/commands/run.d.ts +2 -0
package/README.md
CHANGED
|
@@ -17,6 +17,120 @@ Or run directly with npx:
|
|
|
17
17
|
npx @boolesai/tspec-cli <command>
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
## Plugin Installation
|
|
21
|
+
|
|
22
|
+
TSpec uses a plugin architecture to support different protocols. Plugins can be installed automatically or manually.
|
|
23
|
+
|
|
24
|
+
### Installing Plugins via CLI
|
|
25
|
+
|
|
26
|
+
The easiest way to install plugins is using the `plugin:install` command:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install HTTP/HTTPS protocol plugin
|
|
30
|
+
tspec plugin:install @tspec/http
|
|
31
|
+
|
|
32
|
+
# Install Web browser UI testing plugin
|
|
33
|
+
tspec plugin:install @tspec/web
|
|
34
|
+
|
|
35
|
+
# Install and add to global config
|
|
36
|
+
tspec plugin:install @tspec/http --global
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Manual Installation
|
|
40
|
+
|
|
41
|
+
You can also install plugins manually as npm packages:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Install HTTP/HTTPS protocol plugin
|
|
45
|
+
npm install -D @tspec/http
|
|
46
|
+
|
|
47
|
+
# Install Web browser UI testing plugin
|
|
48
|
+
npm install -D @tspec/web
|
|
49
|
+
|
|
50
|
+
# Install multiple plugins at once
|
|
51
|
+
npm install -D @tspec/http @tspec/web
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Plugin Configuration
|
|
55
|
+
|
|
56
|
+
TSpec uses JSON configuration files. Create a `tspec.config.json` file in your project root:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"plugins": [
|
|
61
|
+
"@tspec/http",
|
|
62
|
+
"@tspec/web"
|
|
63
|
+
],
|
|
64
|
+
"pluginOptions": {
|
|
65
|
+
"@tspec/http": {
|
|
66
|
+
"timeout": 30000,
|
|
67
|
+
"followRedirects": true,
|
|
68
|
+
"maxRedirects": 5
|
|
69
|
+
},
|
|
70
|
+
"@tspec/web": {
|
|
71
|
+
"headless": true,
|
|
72
|
+
"timeout": 30000,
|
|
73
|
+
"slowMo": 0
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Configuration Locations
|
|
80
|
+
|
|
81
|
+
TSpec supports dual configuration with local taking precedence:
|
|
82
|
+
|
|
83
|
+
| Location | Path | Priority |
|
|
84
|
+
|----------|------|----------|
|
|
85
|
+
| Local | `./tspec.config.json` (searched upward) | Higher |
|
|
86
|
+
| Global | `~/.tspec/tspec.config.json` | Lower |
|
|
87
|
+
|
|
88
|
+
When both configs exist, they are merged with local values overriding global ones.
|
|
89
|
+
|
|
90
|
+
#### Auto-Installation
|
|
91
|
+
|
|
92
|
+
When running `tspec run`, missing plugins in your config are automatically installed to `~/.tspec/plugins/`. Use `--no-auto-install` to disable this behavior.
|
|
93
|
+
|
|
94
|
+
### Available Official Plugins
|
|
95
|
+
|
|
96
|
+
| Plugin | Protocol | Description | Package |
|
|
97
|
+
|--------|----------|-------------|----------|
|
|
98
|
+
| HTTP/HTTPS | `http`, `https` | REST API testing with axios | `@tspec/http` |
|
|
99
|
+
| Web UI | `web` | Browser testing with Puppeteer | `@tspec/web` |
|
|
100
|
+
|
|
101
|
+
### Using Plugins
|
|
102
|
+
|
|
103
|
+
Once installed and configured, plugins are automatically loaded when running tests:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Run HTTP tests
|
|
107
|
+
tspec run tests/**/*.http.tcase
|
|
108
|
+
|
|
109
|
+
# Run Web UI tests
|
|
110
|
+
tspec run tests/**/*.web.tcase
|
|
111
|
+
|
|
112
|
+
# List loaded plugins and supported protocols
|
|
113
|
+
tspec list
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Custom Plugins
|
|
117
|
+
|
|
118
|
+
You can also install custom third-party plugins or create your own:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Install custom plugin from npm
|
|
122
|
+
tspec plugin:install my-custom-tspec-plugin
|
|
123
|
+
|
|
124
|
+
# Or use a local plugin path in tspec.config.json:
|
|
125
|
+
{
|
|
126
|
+
"plugins": [
|
|
127
|
+
"./plugins/my-custom-protocol"
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For plugin development details, see the [Plugin Development Guide](../plugins/DEVELOPMENT.md).
|
|
133
|
+
|
|
20
134
|
## Commands
|
|
21
135
|
|
|
22
136
|
### `tspec validate`
|
|
@@ -59,6 +173,8 @@ tspec run <files...> [options]
|
|
|
59
173
|
- `-v, --verbose` - Verbose output
|
|
60
174
|
- `-q, --quiet` - Only output summary
|
|
61
175
|
- `--fail-fast` - Stop on first failure
|
|
176
|
+
- `--config <path>` - Path to tspec.config.json
|
|
177
|
+
- `--no-auto-install` - Skip automatic plugin installation
|
|
62
178
|
|
|
63
179
|
**Examples:**
|
|
64
180
|
```bash
|
|
@@ -131,6 +247,62 @@ tspec list
|
|
|
131
247
|
tspec list --output json
|
|
132
248
|
```
|
|
133
249
|
|
|
250
|
+
### `tspec plugin:install`
|
|
251
|
+
|
|
252
|
+
Install a TSpec plugin and add it to configuration.
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
tspec plugin:install <plugin> [options]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Options:**
|
|
259
|
+
- `-o, --output <format>` - Output format: `json`, `text` (default: `text`)
|
|
260
|
+
- `-g, --global` - Add plugin to global config (`~/.tspec/tspec.config.json`)
|
|
261
|
+
- `-c, --config <path>` - Path to specific config file to update
|
|
262
|
+
|
|
263
|
+
**Examples:**
|
|
264
|
+
```bash
|
|
265
|
+
# Install plugin (adds to local config if exists, otherwise global)
|
|
266
|
+
tspec plugin:install @tspec/http
|
|
267
|
+
|
|
268
|
+
# Install and add to global config
|
|
269
|
+
tspec plugin:install @tspec/web --global
|
|
270
|
+
|
|
271
|
+
# Install and add to specific config file
|
|
272
|
+
tspec plugin:install @tspec/http --config ./tspec.config.json
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### `tspec plugin:list`
|
|
276
|
+
|
|
277
|
+
List all installed TSpec plugins and configuration sources.
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
tspec plugin:list [options]
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Alias:** `tspec plugins`
|
|
284
|
+
|
|
285
|
+
**Options:**
|
|
286
|
+
- `-o, --output <format>` - Output format: `json`, `text` (default: `text`)
|
|
287
|
+
- `-v, --verbose` - Show detailed plugin information
|
|
288
|
+
- `--health` - Run health checks on all plugins
|
|
289
|
+
- `-c, --config <path>` - Path to tspec.config.json
|
|
290
|
+
|
|
291
|
+
**Examples:**
|
|
292
|
+
```bash
|
|
293
|
+
# List installed plugins
|
|
294
|
+
tspec plugin:list
|
|
295
|
+
|
|
296
|
+
# Show detailed information
|
|
297
|
+
tspec plugin:list --verbose
|
|
298
|
+
|
|
299
|
+
# Check plugin health status
|
|
300
|
+
tspec plugin:list --health
|
|
301
|
+
|
|
302
|
+
# JSON output
|
|
303
|
+
tspec plugin:list --output json
|
|
304
|
+
```
|
|
305
|
+
|
|
134
306
|
### `tspec mcp`
|
|
135
307
|
|
|
136
308
|
Start MCP (Model Context Protocol) server for AI tool integration.
|
|
@@ -143,7 +315,15 @@ This starts an MCP server over stdio that exposes TSpec commands as tools for AI
|
|
|
143
315
|
|
|
144
316
|
## MCP Integration
|
|
145
317
|
|
|
146
|
-
TSpec CLI can run as an MCP server, exposing all commands as tools for AI assistants.
|
|
318
|
+
TSpec CLI can run as an MCP (Model Context Protocol) server, exposing all commands as tools for AI assistants. This enables AI assistants like Claude to execute TSpec commands directly through the MCP protocol.
|
|
319
|
+
|
|
320
|
+
### Overview
|
|
321
|
+
|
|
322
|
+
The MCP server runs over stdio, providing a standardized interface for AI tools to:
|
|
323
|
+
- Execute test cases with customizable parameters
|
|
324
|
+
- Validate test case files for schema correctness
|
|
325
|
+
- Parse test specifications without execution
|
|
326
|
+
- Query supported protocols and configurations
|
|
147
327
|
|
|
148
328
|
### Available Tools
|
|
149
329
|
|
|
@@ -154,12 +334,17 @@ TSpec CLI can run as an MCP server, exposing all commands as tools for AI assist
|
|
|
154
334
|
| `tspec_parse` | Parse and display test case information |
|
|
155
335
|
| `tspec_list` | List supported protocols |
|
|
156
336
|
|
|
157
|
-
###
|
|
337
|
+
### Configuration
|
|
338
|
+
|
|
339
|
+
#### Claude Desktop
|
|
158
340
|
|
|
159
341
|
Add the following to your Claude Desktop configuration file:
|
|
160
342
|
|
|
161
343
|
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
162
344
|
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
345
|
+
**Linux:** `~/.config/Claude/claude_desktop_config.json`
|
|
346
|
+
|
|
347
|
+
**Option 1: Using npx (recommended for always getting the latest version):**
|
|
163
348
|
|
|
164
349
|
```json
|
|
165
350
|
{
|
|
@@ -172,7 +357,7 @@ Add the following to your Claude Desktop configuration file:
|
|
|
172
357
|
}
|
|
173
358
|
```
|
|
174
359
|
|
|
175
|
-
|
|
360
|
+
**Option 2: Using global installation:**
|
|
176
361
|
|
|
177
362
|
```json
|
|
178
363
|
{
|
|
@@ -185,15 +370,56 @@ Or if installed globally:
|
|
|
185
370
|
}
|
|
186
371
|
```
|
|
187
372
|
|
|
373
|
+
**Option 3: Using absolute path (for development or specific versions):**
|
|
374
|
+
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"mcpServers": {
|
|
378
|
+
"tspec": {
|
|
379
|
+
"command": "/path/to/tspec/cli/bin/tspec.js",
|
|
380
|
+
"args": ["mcp"]
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### Other MCP Clients
|
|
387
|
+
|
|
388
|
+
For other MCP-compatible clients, start the server with:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
tspec mcp
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The server will communicate via stdio, waiting for JSON-RPC 2.0 formatted requests.
|
|
395
|
+
|
|
396
|
+
### Server Behavior
|
|
397
|
+
|
|
398
|
+
- **Transport:** stdio (reads from stdin, writes to stdout)
|
|
399
|
+
- **Protocol:** JSON-RPC 2.0 over MCP
|
|
400
|
+
- **Lifecycle:** Runs indefinitely until explicitly terminated (Ctrl+C or SIGTERM)
|
|
401
|
+
- **Logging:** Error logs are written to stderr to avoid polluting stdio transport
|
|
402
|
+
|
|
188
403
|
### Tool Parameters
|
|
189
404
|
|
|
190
405
|
#### tspec_run
|
|
191
406
|
|
|
407
|
+
Execute test cases with optional configuration.
|
|
408
|
+
|
|
409
|
+
**Parameters:**
|
|
410
|
+
- `files` (required): Array of file paths or glob patterns
|
|
411
|
+
- `concurrency` (optional): Maximum concurrent test execution (default: 5)
|
|
412
|
+
- `env` (optional): Environment variables as key-value object
|
|
413
|
+
- `params` (optional): Test parameters as key-value object
|
|
414
|
+
- `failFast` (optional): Stop on first failure (default: false)
|
|
415
|
+
- `output` (optional): Output format - "json" or "text" (default: "text")
|
|
416
|
+
|
|
417
|
+
**Example:**
|
|
192
418
|
```json
|
|
193
419
|
{
|
|
194
420
|
"files": ["tests/*.tcase"],
|
|
195
421
|
"concurrency": 5,
|
|
196
|
-
"env": { "API_HOST": "localhost" },
|
|
422
|
+
"env": { "API_HOST": "localhost", "API_PORT": "8080" },
|
|
197
423
|
"params": { "timeout": "5000" },
|
|
198
424
|
"failFast": false,
|
|
199
425
|
"output": "text"
|
|
@@ -202,6 +428,13 @@ Or if installed globally:
|
|
|
202
428
|
|
|
203
429
|
#### tspec_validate
|
|
204
430
|
|
|
431
|
+
Validate test case files for schema correctness.
|
|
432
|
+
|
|
433
|
+
**Parameters:**
|
|
434
|
+
- `files` (required): Array of file paths or glob patterns
|
|
435
|
+
- `output` (optional): Output format - "json" or "text" (default: "text")
|
|
436
|
+
|
|
437
|
+
**Example:**
|
|
205
438
|
```json
|
|
206
439
|
{
|
|
207
440
|
"files": ["tests/*.tcase"],
|
|
@@ -211,6 +444,16 @@ Or if installed globally:
|
|
|
211
444
|
|
|
212
445
|
#### tspec_parse
|
|
213
446
|
|
|
447
|
+
Parse test case files without execution.
|
|
448
|
+
|
|
449
|
+
**Parameters:**
|
|
450
|
+
- `files` (required): Array of file paths or glob patterns
|
|
451
|
+
- `env` (optional): Environment variables for variable substitution
|
|
452
|
+
- `params` (optional): Parameters for variable substitution
|
|
453
|
+
- `verbose` (optional): Show detailed information (default: false)
|
|
454
|
+
- `output` (optional): Output format - "json" or "text" (default: "text")
|
|
455
|
+
|
|
456
|
+
**Example:**
|
|
214
457
|
```json
|
|
215
458
|
{
|
|
216
459
|
"files": ["tests/*.tcase"],
|
|
@@ -223,12 +466,35 @@ Or if installed globally:
|
|
|
223
466
|
|
|
224
467
|
#### tspec_list
|
|
225
468
|
|
|
469
|
+
List supported protocols and configuration.
|
|
470
|
+
|
|
471
|
+
**Parameters:**
|
|
472
|
+
- `output` (optional): Output format - "json" or "text" (default: "text")
|
|
473
|
+
|
|
474
|
+
**Example:**
|
|
226
475
|
```json
|
|
227
476
|
{
|
|
228
477
|
"output": "text"
|
|
229
478
|
}
|
|
230
479
|
```
|
|
231
480
|
|
|
481
|
+
### Troubleshooting
|
|
482
|
+
|
|
483
|
+
**Server doesn't appear in Claude Desktop:**
|
|
484
|
+
- Verify the configuration file path is correct for your OS
|
|
485
|
+
- Check JSON syntax is valid (use a JSON validator)
|
|
486
|
+
- Restart Claude Desktop after configuration changes
|
|
487
|
+
- Check Claude Desktop logs for connection errors
|
|
488
|
+
|
|
489
|
+
**Server hangs or doesn't respond:**
|
|
490
|
+
- Ensure Node.js >= 18.0.0 is installed
|
|
491
|
+
- Verify `@boolesai/tspec-cli` is accessible (try running `tspec --version`)
|
|
492
|
+
- Check stderr output for error messages
|
|
493
|
+
|
|
494
|
+
**Permission errors:**
|
|
495
|
+
- Ensure the tspec executable has proper permissions
|
|
496
|
+
- For global installation, verify npm global bin directory is in PATH
|
|
497
|
+
|
|
232
498
|
## Exit Codes
|
|
233
499
|
|
|
234
500
|
| Code | Description |
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { existsSync, statSync, readFileSync } from "fs";
|
|
2
|
+
import { existsSync, statSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { isAbsolute, resolve as resolve$1, basename, relative, dirname, join } from "path";
|
|
5
5
|
import ora from "ora";
|
|
6
|
-
import { getTypeFromFilePath, isSuiteFile, getSuiteProtocolType, validateTestCase, clearTemplateCache,
|
|
6
|
+
import { getTypeFromFilePath, isSuiteFile, getSuiteProtocolType, validateTestCase, clearTemplateCache, getPluginManager, version as version$1, registry as registry$1, executeSuite, parseTestCases, scheduler, PluginManager } from "@boolesai/tspec";
|
|
7
7
|
import { glob } from "glob";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
+
import { findConfigFile, findLocalConfigFile, findGlobalConfigFile, PLUGINS_DIR, isPluginInstalled, installPlugin, GLOBAL_CONFIG_PATH } from "@boolesai/tspec/plugin";
|
|
9
10
|
import process$2 from "node:process";
|
|
10
11
|
async function discoverTSpecFiles(patterns, cwd) {
|
|
11
12
|
const workingDir = process.cwd();
|
|
@@ -353,6 +354,14 @@ async function runFileTestCasesInternal(descriptor, env, params, concurrency, fa
|
|
|
353
354
|
}
|
|
354
355
|
async function executeRun(params) {
|
|
355
356
|
clearTemplateCache();
|
|
357
|
+
const configPath = params.config || findConfigFile();
|
|
358
|
+
if (configPath) {
|
|
359
|
+
const pluginManager = getPluginManager(version$1);
|
|
360
|
+
await pluginManager.initialize(configPath, {
|
|
361
|
+
skipAutoInstall: params.noAutoInstall
|
|
362
|
+
});
|
|
363
|
+
registry$1.enablePluginManager();
|
|
364
|
+
}
|
|
356
365
|
const concurrency = params.concurrency ?? 5;
|
|
357
366
|
const env = params.env ?? {};
|
|
358
367
|
const paramValues = params.params ?? {};
|
|
@@ -573,7 +582,7 @@ ${parseErrors.length} file(s) failed to parse:`);
|
|
|
573
582
|
data: { results: allResults, summary, parseErrors }
|
|
574
583
|
};
|
|
575
584
|
}
|
|
576
|
-
const runCommand = new Command("run").description("Execute test cases and report results").argument("<files...>", "Files or glob patterns to run").option("-o, --output <format>", "Output format: json, text", "text").option("-c, --concurrency <number>", "Max concurrent tests", "5").option("-e, --env <key=value>", "Environment variables", parseKeyValue$1, {}).option("-p, --params <key=value>", "Parameters", parseKeyValue$1, {}).option("-v, --verbose", "Verbose output").option("-q, --quiet", "Only output summary").option("--fail-fast", "Stop on first failure").action(async (files, options) => {
|
|
585
|
+
const runCommand = new Command("run").description("Execute test cases and report results").argument("<files...>", "Files or glob patterns to run").option("-o, --output <format>", "Output format: json, text", "text").option("-c, --concurrency <number>", "Max concurrent tests", "5").option("-e, --env <key=value>", "Environment variables", parseKeyValue$1, {}).option("-p, --params <key=value>", "Parameters", parseKeyValue$1, {}).option("-v, --verbose", "Verbose output").option("-q, --quiet", "Only output summary").option("--fail-fast", "Stop on first failure").option("--config <path>", "Path to tspec.config.json for plugin loading").option("--no-auto-install", "Skip automatic plugin installation").action(async (files, options) => {
|
|
577
586
|
setLoggerOptions({ verbose: options.verbose, quiet: options.quiet });
|
|
578
587
|
const spinner = options.quiet ? null : ora("Running tests...").start();
|
|
579
588
|
try {
|
|
@@ -584,6 +593,8 @@ const runCommand = new Command("run").description("Execute test cases and report
|
|
|
584
593
|
verbose: options.verbose,
|
|
585
594
|
quiet: options.quiet,
|
|
586
595
|
failFast: options.failFast,
|
|
596
|
+
config: options.config,
|
|
597
|
+
noAutoInstall: options.noAutoInstall,
|
|
587
598
|
env: options.env,
|
|
588
599
|
params: options.params
|
|
589
600
|
});
|
|
@@ -604,9 +615,20 @@ const runCommand = new Command("run").description("Execute test cases and report
|
|
|
604
615
|
}
|
|
605
616
|
process.exit(result.success ? 0 : 1);
|
|
606
617
|
} catch (err) {
|
|
607
|
-
spinner?.
|
|
618
|
+
spinner?.stop();
|
|
608
619
|
const message = err instanceof Error ? err.message : String(err);
|
|
609
|
-
|
|
620
|
+
if (options.output === "json") {
|
|
621
|
+
const errorOutput = formatJson({
|
|
622
|
+
results: [],
|
|
623
|
+
summary: { total: 0, passed: 0, failed: 0, passRate: 0, duration: 0 },
|
|
624
|
+
parseErrors: [],
|
|
625
|
+
error: message
|
|
626
|
+
});
|
|
627
|
+
logger.log(errorOutput);
|
|
628
|
+
} else {
|
|
629
|
+
spinner?.fail("Execution failed");
|
|
630
|
+
logger.error(message);
|
|
631
|
+
}
|
|
610
632
|
process.exit(2);
|
|
611
633
|
}
|
|
612
634
|
});
|
|
@@ -15138,6 +15160,262 @@ const mcpCommand = new Command("mcp").description("Start MCP server for tool int
|
|
|
15138
15160
|
setLoggerOptions({ quiet: true });
|
|
15139
15161
|
await startMcpServer();
|
|
15140
15162
|
});
|
|
15163
|
+
async function executePluginList(params) {
|
|
15164
|
+
const output = params.output ?? "text";
|
|
15165
|
+
const pluginManager = new PluginManager(version$1);
|
|
15166
|
+
const localConfigPath = findLocalConfigFile();
|
|
15167
|
+
const globalConfigPath = findGlobalConfigFile();
|
|
15168
|
+
const configPath = params.config || localConfigPath || globalConfigPath;
|
|
15169
|
+
let loadSummary;
|
|
15170
|
+
if (configPath) {
|
|
15171
|
+
loadSummary = await pluginManager.initialize(configPath);
|
|
15172
|
+
}
|
|
15173
|
+
const plugins = pluginManager.list();
|
|
15174
|
+
const protocols = pluginManager.listProtocols();
|
|
15175
|
+
let healthReports;
|
|
15176
|
+
if (params.health) {
|
|
15177
|
+
healthReports = await pluginManager.healthCheck();
|
|
15178
|
+
}
|
|
15179
|
+
const data = {
|
|
15180
|
+
plugins: plugins.map((p) => ({
|
|
15181
|
+
name: p.name,
|
|
15182
|
+
version: p.version,
|
|
15183
|
+
description: p.description,
|
|
15184
|
+
protocols: p.protocols,
|
|
15185
|
+
author: p.author,
|
|
15186
|
+
homepage: p.homepage
|
|
15187
|
+
})),
|
|
15188
|
+
protocols,
|
|
15189
|
+
configPath: configPath || void 0,
|
|
15190
|
+
configSources: {
|
|
15191
|
+
local: localConfigPath || void 0,
|
|
15192
|
+
global: globalConfigPath || void 0
|
|
15193
|
+
},
|
|
15194
|
+
pluginsDir: PLUGINS_DIR,
|
|
15195
|
+
health: healthReports
|
|
15196
|
+
};
|
|
15197
|
+
let outputStr;
|
|
15198
|
+
if (output === "json") {
|
|
15199
|
+
outputStr = JSON.stringify(data, null, 2);
|
|
15200
|
+
} else {
|
|
15201
|
+
outputStr = formatPluginListText(data, params.verbose ?? false, loadSummary);
|
|
15202
|
+
}
|
|
15203
|
+
return {
|
|
15204
|
+
success: true,
|
|
15205
|
+
output: outputStr,
|
|
15206
|
+
data
|
|
15207
|
+
};
|
|
15208
|
+
}
|
|
15209
|
+
function formatPluginListText(data, verbose, loadSummary) {
|
|
15210
|
+
const lines = [];
|
|
15211
|
+
lines.push(chalk.bold("\nTSpec Plugins\n"));
|
|
15212
|
+
lines.push(chalk.bold("Config:"));
|
|
15213
|
+
if (data.configSources?.local) {
|
|
15214
|
+
lines.push(chalk.gray(` Local: ${data.configSources.local}`));
|
|
15215
|
+
} else {
|
|
15216
|
+
lines.push(chalk.gray(" Local: (none)"));
|
|
15217
|
+
}
|
|
15218
|
+
if (data.configSources?.global) {
|
|
15219
|
+
lines.push(chalk.gray(` Global: ${data.configSources.global}`));
|
|
15220
|
+
} else {
|
|
15221
|
+
lines.push(chalk.gray(" Global: (none)"));
|
|
15222
|
+
}
|
|
15223
|
+
lines.push(chalk.gray(` Plugins dir: ${data.pluginsDir}`));
|
|
15224
|
+
if (loadSummary) {
|
|
15225
|
+
lines.push("");
|
|
15226
|
+
lines.push(chalk.gray(`Discovered: ${loadSummary.total}, Loaded: ${loadSummary.loaded}`));
|
|
15227
|
+
if (loadSummary.installed && loadSummary.installed > 0) {
|
|
15228
|
+
lines.push(chalk.green(`Installed: ${loadSummary.installed} plugin(s)`));
|
|
15229
|
+
}
|
|
15230
|
+
if (loadSummary.failed > 0) {
|
|
15231
|
+
lines.push(chalk.red(`Failed: ${loadSummary.failed}`));
|
|
15232
|
+
for (const error2 of loadSummary.errors) {
|
|
15233
|
+
lines.push(chalk.red(` ${error2.plugin}: ${error2.error}`));
|
|
15234
|
+
}
|
|
15235
|
+
}
|
|
15236
|
+
if (loadSummary.installErrors && loadSummary.installErrors.length > 0) {
|
|
15237
|
+
lines.push(chalk.red(`Install failures:`));
|
|
15238
|
+
for (const error2 of loadSummary.installErrors) {
|
|
15239
|
+
lines.push(chalk.red(` ${error2.plugin}: ${error2.error}`));
|
|
15240
|
+
}
|
|
15241
|
+
}
|
|
15242
|
+
}
|
|
15243
|
+
lines.push("");
|
|
15244
|
+
if (data.plugins.length === 0) {
|
|
15245
|
+
lines.push(chalk.yellow("No plugins loaded."));
|
|
15246
|
+
lines.push(chalk.gray("Add plugins to your tspec.config.json:"));
|
|
15247
|
+
lines.push(chalk.gray(" {"));
|
|
15248
|
+
lines.push(chalk.gray(' "plugins": ["@tspec/http", "@tspec/web"]'));
|
|
15249
|
+
lines.push(chalk.gray(" }"));
|
|
15250
|
+
} else {
|
|
15251
|
+
for (const plugin of data.plugins) {
|
|
15252
|
+
lines.push(`${chalk.cyan(plugin.name)} ${chalk.gray(`v${plugin.version}`)}`);
|
|
15253
|
+
if (verbose && plugin.description) {
|
|
15254
|
+
lines.push(` ${plugin.description}`);
|
|
15255
|
+
}
|
|
15256
|
+
lines.push(` Protocols: ${plugin.protocols.join(", ")}`);
|
|
15257
|
+
if (verbose) {
|
|
15258
|
+
if (plugin.author) {
|
|
15259
|
+
lines.push(` Author: ${plugin.author}`);
|
|
15260
|
+
}
|
|
15261
|
+
if (plugin.homepage) {
|
|
15262
|
+
lines.push(` Homepage: ${plugin.homepage}`);
|
|
15263
|
+
}
|
|
15264
|
+
}
|
|
15265
|
+
lines.push("");
|
|
15266
|
+
}
|
|
15267
|
+
}
|
|
15268
|
+
if (data.health) {
|
|
15269
|
+
lines.push(chalk.bold("Health Check\n"));
|
|
15270
|
+
for (const report of data.health) {
|
|
15271
|
+
const status = report.healthy ? chalk.green("✓ Healthy") : chalk.red("✗ Unhealthy");
|
|
15272
|
+
lines.push(`${chalk.cyan(report.plugin)}: ${status}`);
|
|
15273
|
+
if (report.message) {
|
|
15274
|
+
lines.push(` ${report.message}`);
|
|
15275
|
+
}
|
|
15276
|
+
}
|
|
15277
|
+
lines.push("");
|
|
15278
|
+
}
|
|
15279
|
+
if (data.protocols.length > 0) {
|
|
15280
|
+
lines.push(chalk.bold("Supported Protocols: ") + data.protocols.join(", "));
|
|
15281
|
+
}
|
|
15282
|
+
return lines.join("\n");
|
|
15283
|
+
}
|
|
15284
|
+
const pluginListCommand = new Command("plugin:list").alias("plugins").description("List all installed TSpec plugins").option("-o, --output <format>", "Output format: json, text", "text").option("-v, --verbose", "Show detailed plugin information").option("--health", "Run health checks on all plugins").option("-c, --config <path>", "Path to tspec.config.json").action(async (options) => {
|
|
15285
|
+
try {
|
|
15286
|
+
const result = await executePluginList({
|
|
15287
|
+
output: options.output,
|
|
15288
|
+
verbose: options.verbose,
|
|
15289
|
+
health: options.health,
|
|
15290
|
+
config: options.config
|
|
15291
|
+
});
|
|
15292
|
+
logger.log(result.output);
|
|
15293
|
+
} catch (err) {
|
|
15294
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15295
|
+
logger.error(`Failed to list plugins: ${message}`);
|
|
15296
|
+
process.exit(2);
|
|
15297
|
+
}
|
|
15298
|
+
});
|
|
15299
|
+
function loadConfigFile(configPath) {
|
|
15300
|
+
if (!existsSync(configPath)) {
|
|
15301
|
+
return { plugins: [], pluginOptions: {} };
|
|
15302
|
+
}
|
|
15303
|
+
try {
|
|
15304
|
+
const content = readFileSync(configPath, "utf-8");
|
|
15305
|
+
return JSON.parse(content);
|
|
15306
|
+
} catch {
|
|
15307
|
+
return { plugins: [], pluginOptions: {} };
|
|
15308
|
+
}
|
|
15309
|
+
}
|
|
15310
|
+
function saveConfigFile(configPath, config2) {
|
|
15311
|
+
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
15312
|
+
if (!existsSync(dir)) {
|
|
15313
|
+
mkdirSync(dir, { recursive: true });
|
|
15314
|
+
}
|
|
15315
|
+
writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
15316
|
+
}
|
|
15317
|
+
function addPluginToConfig(config2, pluginName) {
|
|
15318
|
+
if (!config2.plugins) {
|
|
15319
|
+
config2.plugins = [];
|
|
15320
|
+
}
|
|
15321
|
+
if (config2.plugins.includes(pluginName)) {
|
|
15322
|
+
return false;
|
|
15323
|
+
}
|
|
15324
|
+
config2.plugins.push(pluginName);
|
|
15325
|
+
return true;
|
|
15326
|
+
}
|
|
15327
|
+
async function executePluginInstall(params) {
|
|
15328
|
+
const { pluginName, output = "text", global: useGlobal = false, config: customConfig } = params;
|
|
15329
|
+
let configPath;
|
|
15330
|
+
if (customConfig) {
|
|
15331
|
+
configPath = customConfig;
|
|
15332
|
+
} else if (useGlobal) {
|
|
15333
|
+
configPath = GLOBAL_CONFIG_PATH;
|
|
15334
|
+
} else {
|
|
15335
|
+
const localConfig = findLocalConfigFile();
|
|
15336
|
+
configPath = localConfig || GLOBAL_CONFIG_PATH;
|
|
15337
|
+
}
|
|
15338
|
+
const alreadyInstalled = isPluginInstalled(pluginName);
|
|
15339
|
+
let installed = false;
|
|
15340
|
+
let installError;
|
|
15341
|
+
if (!alreadyInstalled) {
|
|
15342
|
+
const result = await installPlugin(pluginName);
|
|
15343
|
+
installed = result.success;
|
|
15344
|
+
if (!result.success) {
|
|
15345
|
+
installError = result.error;
|
|
15346
|
+
}
|
|
15347
|
+
} else {
|
|
15348
|
+
installed = true;
|
|
15349
|
+
}
|
|
15350
|
+
let configUpdated = false;
|
|
15351
|
+
if (installed) {
|
|
15352
|
+
const config2 = loadConfigFile(configPath);
|
|
15353
|
+
configUpdated = addPluginToConfig(config2, pluginName);
|
|
15354
|
+
if (configUpdated) {
|
|
15355
|
+
saveConfigFile(configPath, config2);
|
|
15356
|
+
}
|
|
15357
|
+
}
|
|
15358
|
+
const data = {
|
|
15359
|
+
plugin: pluginName,
|
|
15360
|
+
installed,
|
|
15361
|
+
configUpdated,
|
|
15362
|
+
configPath: installed ? configPath : void 0,
|
|
15363
|
+
error: installError
|
|
15364
|
+
};
|
|
15365
|
+
let outputStr;
|
|
15366
|
+
if (output === "json") {
|
|
15367
|
+
outputStr = JSON.stringify(data, null, 2);
|
|
15368
|
+
} else {
|
|
15369
|
+
if (!installed) {
|
|
15370
|
+
outputStr = chalk.red(`Failed to install ${pluginName}: ${installError || "Unknown error"}`);
|
|
15371
|
+
} else if (alreadyInstalled && !configUpdated) {
|
|
15372
|
+
outputStr = chalk.yellow(`Plugin ${pluginName} is already installed and configured.`);
|
|
15373
|
+
} else if (alreadyInstalled && configUpdated) {
|
|
15374
|
+
outputStr = [
|
|
15375
|
+
chalk.green(`Plugin ${pluginName} is already installed.`),
|
|
15376
|
+
chalk.green(`Added to config: ${configPath}`)
|
|
15377
|
+
].join("\n");
|
|
15378
|
+
} else if (configUpdated) {
|
|
15379
|
+
outputStr = [
|
|
15380
|
+
chalk.green(`Successfully installed ${pluginName}`),
|
|
15381
|
+
chalk.green(`Added to config: ${configPath}`)
|
|
15382
|
+
].join("\n");
|
|
15383
|
+
} else {
|
|
15384
|
+
outputStr = [
|
|
15385
|
+
chalk.green(`Successfully installed ${pluginName}`),
|
|
15386
|
+
chalk.yellow(`Plugin already in config: ${configPath}`)
|
|
15387
|
+
].join("\n");
|
|
15388
|
+
}
|
|
15389
|
+
}
|
|
15390
|
+
return {
|
|
15391
|
+
success: installed,
|
|
15392
|
+
output: outputStr,
|
|
15393
|
+
data
|
|
15394
|
+
};
|
|
15395
|
+
}
|
|
15396
|
+
const pluginInstallCommand = new Command("plugin:install").alias("install").description("Install a TSpec plugin and add it to config").argument("<plugin>", "Plugin name (npm package name, e.g., @tspec/http)").option("-o, --output <format>", "Output format: json, text", "text").option("-g, --global", "Add plugin to global config (~/.tspec/tspec.config.json)").option("-c, --config <path>", "Path to config file to update").action(async (plugin, options) => {
|
|
15397
|
+
const spinner = ora(`Installing ${plugin}...`).start();
|
|
15398
|
+
try {
|
|
15399
|
+
const result = await executePluginInstall({
|
|
15400
|
+
pluginName: plugin,
|
|
15401
|
+
output: options.output,
|
|
15402
|
+
global: options.global,
|
|
15403
|
+
config: options.config
|
|
15404
|
+
});
|
|
15405
|
+
spinner.stop();
|
|
15406
|
+
logger.log(result.output);
|
|
15407
|
+
process.exit(result.success ? 0 : 1);
|
|
15408
|
+
} catch (err) {
|
|
15409
|
+
spinner.stop();
|
|
15410
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15411
|
+
if (options.output === "json") {
|
|
15412
|
+
logger.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
15413
|
+
} else {
|
|
15414
|
+
logger.error(`Failed to install plugin: ${message}`);
|
|
15415
|
+
}
|
|
15416
|
+
process.exit(2);
|
|
15417
|
+
}
|
|
15418
|
+
});
|
|
15141
15419
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
15142
15420
|
const __dirname$1 = dirname(__filename$1);
|
|
15143
15421
|
const packageJson = JSON.parse(readFileSync(join(__dirname$1, "../package.json"), "utf-8"));
|
|
@@ -15148,5 +15426,7 @@ program.addCommand(runCommand);
|
|
|
15148
15426
|
program.addCommand(parseCommand);
|
|
15149
15427
|
program.addCommand(listCommand);
|
|
15150
15428
|
program.addCommand(mcpCommand);
|
|
15429
|
+
program.addCommand(pluginListCommand);
|
|
15430
|
+
program.addCommand(pluginInstallCommand);
|
|
15151
15431
|
await program.parseAsync();
|
|
15152
15432
|
//# sourceMappingURL=index.js.map
|