specshield 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specshield",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "CLI to compare OpenAPI/Swagger specs and detect breaking changes for CI/CD pipelines and local developer workflows.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3,6 +3,8 @@
3
3
  const { Command } = require('commander');
4
4
  const { version } = require('../package.json');
5
5
  const compareCommand = require('./commands/compare');
6
+ const loginCommand = require('./commands/login');
7
+ const logoutCommand = require('./commands/logout');
6
8
 
7
9
  const program = new Command();
8
10
 
@@ -12,6 +14,8 @@ program
12
14
  .version(version);
13
15
 
14
16
  program.addCommand(compareCommand);
17
+ program.addCommand(loginCommand);
18
+ program.addCommand(logoutCommand);
15
19
 
16
20
  program.parseAsync(process.argv).catch((err) => {
17
21
  const logger = require('./utils/logger');
@@ -14,6 +14,9 @@ const { loadConfig } = require('../core/configLoader');
14
14
  const { resolveExitCode } = require('../core/exitCode');
15
15
  const logger = require('../utils/logger');
16
16
  const fsExtra = require('fs-extra');
17
+ const { getStoredApiKey } = require('../config/localConfig');
18
+
19
+ const HOSTED_API_URL = 'https://api.specshield.io';
17
20
 
18
21
  const compare = new Command('compare');
19
22
 
@@ -28,7 +31,9 @@ compare
28
31
  .option('--config <path>', 'Path to .specshield.yml config file')
29
32
  .option('--ignore <change>', 'Ignore a specific change string (repeatable)', collect, [])
30
33
  .option('--severity <level>', 'Minimum severity level: info | warning | error', 'error')
31
- .option('--remote-url <url>', 'Remote API endpoint for comparison')
34
+ .option('--remote', 'Use the SpecShield hosted compare API')
35
+ .option('--api-key <key>', 'API key for hosted mode (overrides env and stored config)')
36
+ .option('--remote-url <url>', 'Override the hosted API base URL')
32
37
  .option('--timeout <ms>', 'Request timeout for remote mode (ms)', '10000')
33
38
  .action(async (base, target, opts) => {
34
39
  try {
@@ -38,11 +43,25 @@ compare
38
43
  // Merge config with CLI options (CLI wins)
39
44
  const options = mergeOptions(config, opts);
40
45
 
46
+ // Resolve API key for remote mode (precedence: --api-key > env > stored config > .specshield.yml)
47
+ if (options.remote || options.remoteUrl || (config.remote && config.remote.enabled)) {
48
+ options.resolvedApiKey = opts.apiKey
49
+ || process.env.SPECSHIELD_API_KEY
50
+ || await getStoredApiKey()
51
+ || (config.remote && config.remote.apiKey)
52
+ || null;
53
+
54
+ if (!options.resolvedApiKey) {
55
+ logger.error('No API key found. Run: specshield login --api-key <KEY>');
56
+ process.exit(2);
57
+ }
58
+ }
59
+
41
60
  const spinner = options.json ? null : ora('Loading specs...').start();
42
61
 
43
62
  let result;
44
63
 
45
- if (options.remoteUrl || (config.remote && config.remote.enabled)) {
64
+ if (options.remote || options.remoteUrl || (config.remote && config.remote.enabled)) {
46
65
  result = await runRemoteComparison(base, target, options, spinner);
47
66
  } else {
48
67
  result = await runLocalComparison(base, target, options, spinner);
@@ -107,22 +126,29 @@ async function runLocalComparison(base, target, options, spinner) {
107
126
 
108
127
  async function runRemoteComparison(base, target, options, spinner) {
109
128
  const axios = require('axios');
110
- const { loadSpec } = require('../core/loadSpec');
129
+ const { version } = require('../../package.json');
111
130
 
112
131
  if (spinner) spinner.text = 'Loading specs for remote comparison...';
113
132
  const baseRaw = await loadSpec(base);
114
133
  const targetRaw = await loadSpec(target);
115
134
 
116
- const url = options.remoteUrl || (options.remote && options.remote.url);
135
+ const url = options.remoteUrl || HOSTED_API_URL + '/compare';
117
136
  const timeout = parseInt(options.timeout, 10) || 10000;
118
137
 
119
- if (spinner) spinner.text = `Sending to remote: ${url}`;
138
+ if (spinner) spinner.text = `Sending to hosted API...`;
139
+
140
+ const headers = {
141
+ 'Content-Type': 'application/json',
142
+ 'X-Api-Key': options.resolvedApiKey,
143
+ 'X-SpecShield-Client': 'cli',
144
+ 'X-SpecShield-Version': version,
145
+ };
120
146
 
121
147
  try {
122
148
  const response = await axios.post(
123
149
  url,
124
150
  { baseSpec: baseRaw, targetSpec: targetRaw },
125
- { timeout, headers: { 'Content-Type': 'application/json' } }
151
+ { timeout, headers }
126
152
  );
127
153
  return response.data;
128
154
  } catch (err) {
@@ -167,7 +193,8 @@ function mergeOptions(config, cliOpts) {
167
193
  ...(config.ignore || []),
168
194
  ],
169
195
  severity: cliOpts.severity || config.severity || 'error',
170
- remoteUrl: cliOpts.remoteUrl || (config.remote && config.remote.enabled ? config.remote.url : null),
196
+ remote: cliOpts.remote || (config.remote && config.remote.enabled) || false,
197
+ remoteUrl: cliOpts.remoteUrl || null,
171
198
  timeout: cliOpts.timeout || (config.remote && config.remote.timeout) || 10000,
172
199
  };
173
200
  }
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const axios = require('axios');
7
+ const { setStoredApiKey, CONFIG_PATH } = require('../config/localConfig');
8
+
9
+ const HOSTED_API_URL = 'https://api.specshield.io';
10
+
11
+ const login = new Command('login');
12
+
13
+ login
14
+ .description('Authenticate with the SpecShield hosted API using your API key')
15
+ .requiredOption('--api-key <key>', 'Your SpecShield API key (starts with ss_)')
16
+ .option('--api-url <url>', 'Override the hosted API base URL', HOSTED_API_URL)
17
+ .action(async (opts) => {
18
+ const spinner = ora('Validating API key...').start();
19
+
20
+ try {
21
+ const response = await axios.post(
22
+ `${opts.apiUrl}/auth/validate-api-key`,
23
+ {},
24
+ {
25
+ headers: { 'X-Api-Key': opts.apiKey },
26
+ timeout: 10000,
27
+ }
28
+ );
29
+
30
+ if (!response.data.valid) {
31
+ spinner.fail(chalk.red('API key is invalid.'));
32
+ process.exit(1);
33
+ }
34
+
35
+ await setStoredApiKey(opts.apiKey);
36
+
37
+ spinner.succeed(chalk.green('Logged in successfully.'));
38
+ console.log('');
39
+ console.log(` ${chalk.bold('Customer:')} ${response.data.name}`);
40
+ console.log(` ${chalk.bold('Plan:')} ${response.data.plan}`);
41
+ console.log(` ${chalk.gray('Config:')} ${CONFIG_PATH}`);
42
+ console.log('');
43
+ console.log(chalk.gray(' Run: specshield compare base.yaml target.yaml --remote'));
44
+
45
+ } catch (err) {
46
+ const msg = err.response
47
+ ? `Validation failed (${err.response.status}): ${JSON.stringify(err.response.data)}`
48
+ : `Connection error: ${err.message}`;
49
+ spinner.fail(chalk.red(msg));
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ module.exports = login;
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const { clearStoredApiKey } = require('../config/localConfig');
6
+
7
+ const logout = new Command('logout');
8
+
9
+ logout
10
+ .description('Remove your stored SpecShield API key')
11
+ .action(async () => {
12
+ await clearStoredApiKey();
13
+ console.log(chalk.green('Logged out. API key removed from local config.'));
14
+ });
15
+
16
+ module.exports = logout;
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const fsExtra = require('fs-extra');
6
+
7
+ const CONFIG_PATH = path.join(os.homedir(), '.specshield', 'config.json');
8
+
9
+ async function loadLocalConfig() {
10
+ try {
11
+ return await fsExtra.readJson(CONFIG_PATH);
12
+ } catch {
13
+ return {};
14
+ }
15
+ }
16
+
17
+ async function saveLocalConfig(data) {
18
+ await fsExtra.outputJson(CONFIG_PATH, data, { spaces: 2 });
19
+ }
20
+
21
+ async function getStoredApiKey() {
22
+ const config = await loadLocalConfig();
23
+ return config.apiKey || null;
24
+ }
25
+
26
+ async function setStoredApiKey(apiKey) {
27
+ const config = await loadLocalConfig();
28
+ config.apiKey = apiKey;
29
+ await saveLocalConfig(config);
30
+ }
31
+
32
+ async function clearStoredApiKey() {
33
+ const config = await loadLocalConfig();
34
+ delete config.apiKey;
35
+ await saveLocalConfig(config);
36
+ }
37
+
38
+ module.exports = { loadLocalConfig, saveLocalConfig, getStoredApiKey, setStoredApiKey, clearStoredApiKey, CONFIG_PATH };