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 +1 -1
- package/src/cli.js +4 -0
- package/src/commands/compare.js +34 -7
- package/src/commands/login.js +54 -0
- package/src/commands/logout.js +16 -0
- package/src/config/localConfig.js +38 -0
package/package.json
CHANGED
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');
|
package/src/commands/compare.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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 ||
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|