@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 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
- ### Claude Desktop Configuration
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
- Or if installed globally:
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, executeSuite, parseTestCases, scheduler, registry as registry$1 } from "@boolesai/tspec";
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?.fail("Execution failed");
618
+ spinner?.stop();
608
619
  const message = err instanceof Error ? err.message : String(err);
609
- logger.error(message);
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