@skillguard/cli 0.1.1 → 0.4.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/CHANGELOG.md +25 -0
- package/README.md +106 -10
- package/dist/cli.js +119 -25
- package/dist/commands/gate.d.ts +14 -0
- package/dist/commands/gate.js +17 -0
- package/dist/commands/limits.d.ts +8 -0
- package/dist/commands/limits.js +98 -0
- package/dist/commands/scan.d.ts +5 -2
- package/dist/commands/scan.js +72 -22
- package/dist/commands/verify.js +3 -3
- package/dist/commands/workflow.d.ts +21 -0
- package/dist/commands/workflow.js +143 -0
- package/dist/lib/api.d.ts +40 -0
- package/dist/lib/api.js +127 -0
- package/dist/lib/format.js +2 -0
- package/dist/lib/help.d.ts +2 -0
- package/dist/lib/help.js +240 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Added `skillguard workflow` command to generate ready-to-use GitHub Actions CI gate workflow
|
|
6
|
+
- Added `--auto-gh-push` option for workflow command (`git add/commit/push`)
|
|
7
|
+
- Added `skillguard help <command>` and `skillguard man <command>` detailed help pages
|
|
8
|
+
- Added `--workflow` alias for fast workflow generation
|
|
9
|
+
- Updated CLI/web workflow snippets to default to zero-secret gate flow
|
|
10
|
+
|
|
11
|
+
## 0.3.0
|
|
12
|
+
|
|
13
|
+
- Added `skillguard gate` command for deterministic CI gating (`0` pass, `1` fail)
|
|
14
|
+
- Added `skillguard limits` command with human and `--json` output
|
|
15
|
+
- Added anonymous scan fallback when no API key is configured
|
|
16
|
+
- Added CLI telemetry headers (`X-SkillGuard-Source`, `X-SkillGuard-CI`)
|
|
17
|
+
- Added support for backend anonymous limits endpoint
|
|
18
|
+
|
|
19
|
+
## 0.2.0
|
|
20
|
+
|
|
21
|
+
- Added `skillguard scan` without path argument (defaults to recursive scan from current directory)
|
|
22
|
+
- Added recursive skip controls: `--skip-node-modules` and `--scan-all`
|
|
23
|
+
- Added worst-score default exit mode for scan: `safe=0`, `warning=1`, `dangerous=2`
|
|
24
|
+
- Preserved legacy threshold mode when `--fail-on` is explicitly provided
|
|
25
|
+
- Added JSON scan output fields: `worst_score`, `exit_code`, `mode`
|
|
26
|
+
- Updated non-scan failure exits across commands: usage/input/config -> `4`, network/API/runtime -> `5`
|
|
27
|
+
|
|
3
28
|
## 0.1.0
|
|
4
29
|
|
|
5
30
|
- Initial release of `@skillguard/cli`
|
package/README.md
CHANGED
|
@@ -8,6 +8,26 @@ Security scanner CLI for AI agent `SKILL.md` files.
|
|
|
8
8
|
npx @skillguard/cli scan SKILL.md
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
No API key configured? CLI falls back to anonymous scanning (rate-limited).
|
|
12
|
+
|
|
13
|
+
### Scan all skills in current repo
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @skillguard/cli scan
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Generate CI workflow (no secrets required)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @skillguard/cli workflow
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Command help (man-like)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @skillguard/cli help scan
|
|
29
|
+
```
|
|
30
|
+
|
|
11
31
|
## Setup
|
|
12
32
|
|
|
13
33
|
```bash
|
|
@@ -40,6 +60,31 @@ npx @skillguard/cli scan ./skills --fail-on warning
|
|
|
40
60
|
npx @skillguard/cli scan ./skills --json > skillguard-report.json
|
|
41
61
|
```
|
|
42
62
|
|
|
63
|
+
### CI gate (recommended)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx @skillguard/cli gate ./skills
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Check remaining limits
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx @skillguard/cli limits
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Generate and push workflow in one command
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx @skillguard/cli workflow --auto-gh-push
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Worst-score CI exit code (recommended)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx @skillguard/cli scan
|
|
85
|
+
# exit: safe=0, warning=1, dangerous=2
|
|
86
|
+
```
|
|
87
|
+
|
|
43
88
|
### Verify signature
|
|
44
89
|
|
|
45
90
|
```bash
|
|
@@ -51,13 +96,42 @@ npx @skillguard/cli verify ./SKILL.md
|
|
|
51
96
|
### scan
|
|
52
97
|
|
|
53
98
|
- `--json`
|
|
54
|
-
- `--fail-on <safe|warning|dangerous>` (
|
|
99
|
+
- `--fail-on <safe|warning|dangerous>` (optional, enables legacy threshold mode)
|
|
55
100
|
- `--timeout <ms>` (default: `30000`)
|
|
56
101
|
- `--base-url <url>` (default: `https://skillguard.ai`)
|
|
57
102
|
- `--api-key <key>`
|
|
58
103
|
- `--dry-run`
|
|
59
104
|
- `--quiet`
|
|
60
105
|
- `--no-color`
|
|
106
|
+
- `--skip-node-modules` (default: enabled)
|
|
107
|
+
- `--scan-all` (disable skip filters, scans everything recursively)
|
|
108
|
+
|
|
109
|
+
### gate
|
|
110
|
+
|
|
111
|
+
- same options as `scan`
|
|
112
|
+
- defaults to CI gate behavior (`--fail-on warning` in threshold mode)
|
|
113
|
+
|
|
114
|
+
### limits
|
|
115
|
+
|
|
116
|
+
- `--json`
|
|
117
|
+
- `--timeout <ms>` (default: `30000`)
|
|
118
|
+
- `--base-url <url>` (default: `https://skillguard.ai`)
|
|
119
|
+
- `--api-key <key>` (optional)
|
|
120
|
+
- `--no-color`
|
|
121
|
+
|
|
122
|
+
### workflow
|
|
123
|
+
|
|
124
|
+
- `--path <file>` (default: `.github/workflows/skillguard.yml`)
|
|
125
|
+
- `--scan-path <path>` (default: `.`)
|
|
126
|
+
- `--fail-on <safe|warning|dangerous>` (default: `warning`)
|
|
127
|
+
- `--print` (print yaml, do not write file)
|
|
128
|
+
- `--force` (overwrite existing workflow file)
|
|
129
|
+
- `--auto-gh-push` (git add/commit/push after write)
|
|
130
|
+
|
|
131
|
+
### help / man
|
|
132
|
+
|
|
133
|
+
- `help [command]`
|
|
134
|
+
- `man [command]`
|
|
61
135
|
|
|
62
136
|
### verify
|
|
63
137
|
|
|
@@ -67,16 +141,40 @@ npx @skillguard/cli verify ./SKILL.md
|
|
|
67
141
|
|
|
68
142
|
## Exit codes
|
|
69
143
|
|
|
70
|
-
- `
|
|
71
|
-
- `
|
|
72
|
-
- `
|
|
73
|
-
- `
|
|
144
|
+
- `scan` default mode (no `--fail-on`):
|
|
145
|
+
- `0` worst score is `safe`
|
|
146
|
+
- `1` worst score is `warning`
|
|
147
|
+
- `2` worst score is `dangerous`
|
|
148
|
+
- `scan` threshold mode (`--fail-on` provided):
|
|
149
|
+
- `0` below threshold
|
|
150
|
+
- `1` threshold exceeded
|
|
151
|
+
- `verify`:
|
|
152
|
+
- `0` signature valid
|
|
153
|
+
- `1` invalid/tampered/expired signature
|
|
154
|
+
- `limits`:
|
|
155
|
+
- `0` success
|
|
156
|
+
- `gate`:
|
|
157
|
+
- `0` below threshold
|
|
158
|
+
- `1` threshold exceeded
|
|
159
|
+
- `workflow`:
|
|
160
|
+
- `0` success
|
|
161
|
+
- all commands:
|
|
162
|
+
- `4` usage/input/config error
|
|
163
|
+
- `5` network/API/runtime external failure
|
|
74
164
|
|
|
75
165
|
## GitHub Actions example
|
|
76
166
|
|
|
77
167
|
```yaml
|
|
78
|
-
name:
|
|
79
|
-
on:
|
|
168
|
+
name: SkillGuard Gate
|
|
169
|
+
on:
|
|
170
|
+
pull_request:
|
|
171
|
+
paths:
|
|
172
|
+
- '**/SKILL.md'
|
|
173
|
+
- '**/skill.md'
|
|
174
|
+
push:
|
|
175
|
+
paths:
|
|
176
|
+
- '**/SKILL.md'
|
|
177
|
+
- '**/skill.md'
|
|
80
178
|
|
|
81
179
|
jobs:
|
|
82
180
|
scan:
|
|
@@ -86,9 +184,7 @@ jobs:
|
|
|
86
184
|
- uses: actions/setup-node@v4
|
|
87
185
|
with:
|
|
88
186
|
node-version: '20'
|
|
89
|
-
- run: npx @skillguard/cli
|
|
90
|
-
env:
|
|
91
|
-
SKILLGUARD_API_KEY: ${{ secrets.SKILLGUARD_API_KEY }}
|
|
187
|
+
- run: npx @skillguard/cli gate . --fail-on warning
|
|
92
188
|
```
|
|
93
189
|
|
|
94
190
|
More info: [skillguard.ai](https://skillguard.ai)
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { runInit } from './commands/init.js';
|
|
4
|
+
import { runGate } from './commands/gate.js';
|
|
5
|
+
import { runLimits } from './commands/limits.js';
|
|
4
6
|
import { runScan } from './commands/scan.js';
|
|
5
7
|
import { runVerify } from './commands/verify.js';
|
|
6
|
-
|
|
8
|
+
import { runWorkflow } from './commands/workflow.js';
|
|
9
|
+
import { renderGeneralHelp, renderTopicHelp } from './lib/help.js';
|
|
10
|
+
import { CLI_VERSION } from './lib/version.js';
|
|
11
|
+
const VERSION = CLI_VERSION;
|
|
7
12
|
function printUsage() {
|
|
8
|
-
console.log(
|
|
9
|
-
|
|
10
|
-
Usage:
|
|
11
|
-
skillguard init [--api-key <key>] [--base-url <url>]
|
|
12
|
-
skillguard scan <path> [--json] [--fail-on <safe|warning|dangerous>] [--timeout <ms>] [--base-url <url>] [--api-key <key>] [--dry-run] [--quiet] [--no-color]
|
|
13
|
-
skillguard verify <path> [--json] [--timeout <ms>] [--base-url <url>]
|
|
14
|
-
|
|
15
|
-
Options:
|
|
16
|
-
--help Show help
|
|
17
|
-
--version Show version
|
|
18
|
-
`);
|
|
13
|
+
console.log(renderGeneralHelp(VERSION));
|
|
19
14
|
}
|
|
20
15
|
function toPositiveInteger(raw, fallback) {
|
|
21
16
|
if (!raw) {
|
|
@@ -48,9 +43,16 @@ function parseShared(args) {
|
|
|
48
43
|
quiet: { type: 'boolean' },
|
|
49
44
|
'no-color': { type: 'boolean' },
|
|
50
45
|
'fail-on': { type: 'string' },
|
|
46
|
+
'scan-all': { type: 'boolean' },
|
|
47
|
+
'skip-node-modules': { type: 'boolean' },
|
|
51
48
|
timeout: { type: 'string' },
|
|
52
49
|
'base-url': { type: 'string' },
|
|
53
50
|
'api-key': { type: 'string' },
|
|
51
|
+
path: { type: 'string' },
|
|
52
|
+
'scan-path': { type: 'string' },
|
|
53
|
+
print: { type: 'boolean' },
|
|
54
|
+
force: { type: 'boolean' },
|
|
55
|
+
'auto-gh-push': { type: 'boolean' },
|
|
54
56
|
},
|
|
55
57
|
strict: false,
|
|
56
58
|
});
|
|
@@ -59,9 +61,17 @@ function parseShared(args) {
|
|
|
59
61
|
positionals: parsed.positionals,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
64
|
+
function isFailOnExplicit(args) {
|
|
65
|
+
return args.some((arg) => arg === '--fail-on' || arg.startsWith('--fail-on='));
|
|
66
|
+
}
|
|
62
67
|
async function run() {
|
|
63
68
|
const argv = process.argv.slice(2);
|
|
64
|
-
|
|
69
|
+
let command = argv[0];
|
|
70
|
+
let commandArgs = argv.slice(1);
|
|
71
|
+
if (command === '--workflow') {
|
|
72
|
+
command = 'workflow';
|
|
73
|
+
commandArgs = argv.slice(1);
|
|
74
|
+
}
|
|
65
75
|
if (!command || command === '--help' || command === '-h') {
|
|
66
76
|
printUsage();
|
|
67
77
|
return 0;
|
|
@@ -70,13 +80,33 @@ async function run() {
|
|
|
70
80
|
console.log(VERSION);
|
|
71
81
|
return 0;
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
if (command === 'help' || command === 'man') {
|
|
84
|
+
const topic = argv[1];
|
|
85
|
+
if (!topic) {
|
|
86
|
+
printUsage();
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const helpText = renderTopicHelp(topic);
|
|
90
|
+
if (!helpText) {
|
|
91
|
+
console.error(`Unknown help topic: ${topic}`);
|
|
92
|
+
return 4;
|
|
93
|
+
}
|
|
94
|
+
console.log(helpText);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
const { options, positionals } = parseShared(commandArgs);
|
|
74
98
|
if (options.version) {
|
|
75
99
|
console.log(VERSION);
|
|
76
100
|
return 0;
|
|
77
101
|
}
|
|
78
102
|
if (options.help) {
|
|
79
|
-
|
|
103
|
+
const topicHelp = renderTopicHelp(command);
|
|
104
|
+
if (topicHelp) {
|
|
105
|
+
console.log(topicHelp);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
printUsage();
|
|
109
|
+
}
|
|
80
110
|
return 0;
|
|
81
111
|
}
|
|
82
112
|
if (command === 'init') {
|
|
@@ -87,23 +117,24 @@ async function run() {
|
|
|
87
117
|
}
|
|
88
118
|
if (command === 'scan') {
|
|
89
119
|
const pathArg = positionals[0];
|
|
90
|
-
if (!pathArg) {
|
|
91
|
-
console.error('Missing required <path> argument.');
|
|
92
|
-
return 2;
|
|
93
|
-
}
|
|
94
120
|
let parsedTimeout;
|
|
95
121
|
let failOn;
|
|
96
122
|
try {
|
|
97
123
|
parsedTimeout = toPositiveInteger(typeof options.timeout === 'string' ? options.timeout : undefined, 30000);
|
|
98
|
-
|
|
124
|
+
if (isFailOnExplicit(argv.slice(1))) {
|
|
125
|
+
failOn = toFailOn(typeof options['fail-on'] === 'string' ? options['fail-on'] : undefined);
|
|
126
|
+
}
|
|
99
127
|
}
|
|
100
128
|
catch (error) {
|
|
101
129
|
console.error(error.message);
|
|
102
|
-
return
|
|
130
|
+
return 4;
|
|
103
131
|
}
|
|
104
132
|
const scanOptions = {
|
|
105
133
|
json: options.json === true,
|
|
106
134
|
failOn,
|
|
135
|
+
failOnExplicit: isFailOnExplicit(argv.slice(1)),
|
|
136
|
+
scanAll: options['scan-all'] === true,
|
|
137
|
+
skipNodeModules: options['scan-all'] === true ? false : options['skip-node-modules'] !== false,
|
|
107
138
|
timeoutMs: parsedTimeout,
|
|
108
139
|
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
109
140
|
apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
|
|
@@ -113,11 +144,74 @@ async function run() {
|
|
|
113
144
|
};
|
|
114
145
|
return await runScan(pathArg, scanOptions);
|
|
115
146
|
}
|
|
147
|
+
if (command === 'gate') {
|
|
148
|
+
const pathArg = positionals[0];
|
|
149
|
+
let parsedTimeout;
|
|
150
|
+
let failOn;
|
|
151
|
+
try {
|
|
152
|
+
parsedTimeout = toPositiveInteger(typeof options.timeout === 'string' ? options.timeout : undefined, 30000);
|
|
153
|
+
failOn = toFailOn(typeof options['fail-on'] === 'string' ? options['fail-on'] : undefined);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error(error.message);
|
|
157
|
+
return 4;
|
|
158
|
+
}
|
|
159
|
+
const gateOptions = {
|
|
160
|
+
json: options.json === true,
|
|
161
|
+
failOn,
|
|
162
|
+
scanAll: options['scan-all'] === true,
|
|
163
|
+
skipNodeModules: options['scan-all'] === true ? false : options['skip-node-modules'] !== false,
|
|
164
|
+
timeoutMs: parsedTimeout,
|
|
165
|
+
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
166
|
+
apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
|
|
167
|
+
dryRun: options['dry-run'] === true,
|
|
168
|
+
quiet: options.quiet === true,
|
|
169
|
+
noColor: options['no-color'] === true,
|
|
170
|
+
};
|
|
171
|
+
return await runGate(pathArg, gateOptions);
|
|
172
|
+
}
|
|
173
|
+
if (command === 'limits') {
|
|
174
|
+
let parsedTimeout;
|
|
175
|
+
try {
|
|
176
|
+
parsedTimeout = toPositiveInteger(typeof options.timeout === 'string' ? options.timeout : undefined, 30000);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error(error.message);
|
|
180
|
+
return 4;
|
|
181
|
+
}
|
|
182
|
+
const limitsOptions = {
|
|
183
|
+
json: options.json === true,
|
|
184
|
+
timeoutMs: parsedTimeout,
|
|
185
|
+
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
186
|
+
apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
|
|
187
|
+
noColor: options['no-color'] === true,
|
|
188
|
+
};
|
|
189
|
+
return await runLimits(limitsOptions);
|
|
190
|
+
}
|
|
191
|
+
if (command === 'workflow') {
|
|
192
|
+
let failOn;
|
|
193
|
+
try {
|
|
194
|
+
failOn = toFailOn(typeof options['fail-on'] === 'string' ? options['fail-on'] : undefined);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error(error.message);
|
|
198
|
+
return 4;
|
|
199
|
+
}
|
|
200
|
+
const workflowOptions = {
|
|
201
|
+
outputPath: typeof options.path === 'string' ? options.path : undefined,
|
|
202
|
+
scanPath: typeof options['scan-path'] === 'string' ? options['scan-path'] : '.',
|
|
203
|
+
failOn,
|
|
204
|
+
print: options.print === true,
|
|
205
|
+
force: options.force === true,
|
|
206
|
+
autoGhPush: options['auto-gh-push'] === true,
|
|
207
|
+
};
|
|
208
|
+
return await runWorkflow(workflowOptions);
|
|
209
|
+
}
|
|
116
210
|
if (command === 'verify') {
|
|
117
211
|
const pathArg = positionals[0];
|
|
118
212
|
if (!pathArg) {
|
|
119
213
|
console.error('Missing required <path> argument.');
|
|
120
|
-
return
|
|
214
|
+
return 4;
|
|
121
215
|
}
|
|
122
216
|
let parsedTimeout;
|
|
123
217
|
try {
|
|
@@ -125,7 +219,7 @@ async function run() {
|
|
|
125
219
|
}
|
|
126
220
|
catch (error) {
|
|
127
221
|
console.error(error.message);
|
|
128
|
-
return
|
|
222
|
+
return 4;
|
|
129
223
|
}
|
|
130
224
|
const verifyOptions = {
|
|
131
225
|
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
@@ -136,7 +230,7 @@ async function run() {
|
|
|
136
230
|
}
|
|
137
231
|
console.error(`Unknown command: ${command}`);
|
|
138
232
|
printUsage();
|
|
139
|
-
return
|
|
233
|
+
return 4;
|
|
140
234
|
}
|
|
141
235
|
run()
|
|
142
236
|
.then((code) => {
|
|
@@ -144,5 +238,5 @@ run()
|
|
|
144
238
|
})
|
|
145
239
|
.catch(() => {
|
|
146
240
|
console.error('Unexpected CLI error.');
|
|
147
|
-
process.exitCode =
|
|
241
|
+
process.exitCode = 5;
|
|
148
242
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ScanScore } from '../lib/api.js';
|
|
2
|
+
export interface GateOptions {
|
|
3
|
+
json: boolean;
|
|
4
|
+
failOn?: ScanScore;
|
|
5
|
+
scanAll: boolean;
|
|
6
|
+
skipNodeModules: boolean;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
dryRun: boolean;
|
|
11
|
+
quiet: boolean;
|
|
12
|
+
noColor: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function runGate(inputPath: string | undefined, options: GateOptions): Promise<number>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runScan } from './scan.js';
|
|
2
|
+
export async function runGate(inputPath, options) {
|
|
3
|
+
const scanOptions = {
|
|
4
|
+
json: options.json,
|
|
5
|
+
failOn: options.failOn || 'warning',
|
|
6
|
+
failOnExplicit: true,
|
|
7
|
+
scanAll: options.scanAll,
|
|
8
|
+
skipNodeModules: options.skipNodeModules,
|
|
9
|
+
timeoutMs: options.timeoutMs,
|
|
10
|
+
baseUrl: options.baseUrl,
|
|
11
|
+
apiKey: options.apiKey,
|
|
12
|
+
dryRun: options.dryRun,
|
|
13
|
+
quiet: options.quiet,
|
|
14
|
+
noColor: options.noColor,
|
|
15
|
+
};
|
|
16
|
+
return runScan(inputPath, scanOptions);
|
|
17
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { fetchLimits } from '../lib/api.js';
|
|
2
|
+
import { normalizeBaseUrl, readConfig, resolveApiKey } from '../lib/config.js';
|
|
3
|
+
import { CLI_VERSION } from '../lib/version.js';
|
|
4
|
+
function renderHuman(response) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
const mode = typeof response.mode === 'string' ? response.mode : 'unknown';
|
|
7
|
+
lines.push(`Mode: ${mode}`);
|
|
8
|
+
if (typeof response.tokenBalance === 'number') {
|
|
9
|
+
lines.push(`Token balance: ${response.tokenBalance}`);
|
|
10
|
+
}
|
|
11
|
+
if (response.ownerTrial && typeof response.ownerTrial === 'object') {
|
|
12
|
+
const trial = response.ownerTrial;
|
|
13
|
+
lines.push('Owner trial:');
|
|
14
|
+
lines.push(` scans: ${Number(trial.scansUsed || 0)} / ${Number(trial.scansLimit || 0)}`);
|
|
15
|
+
lines.push(` remaining: ${Number(trial.remainingScans || 0)}`);
|
|
16
|
+
lines.push(` active: ${trial.active === true ? 'yes' : 'no'}`);
|
|
17
|
+
if (typeof trial.expiresAt === 'string' && trial.expiresAt) {
|
|
18
|
+
lines.push(` expires: ${trial.expiresAt}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (response.apiKeyQuota && typeof response.apiKeyQuota === 'object') {
|
|
22
|
+
const quota = response.apiKeyQuota;
|
|
23
|
+
lines.push('API key quota:');
|
|
24
|
+
lines.push(` key type: ${String(quota.keyType || 'standard')}`);
|
|
25
|
+
lines.push(` unlimited: ${quota.unlimited === true ? 'yes' : 'no'}`);
|
|
26
|
+
lines.push(` scans used: ${Number(quota.scansUsed || 0)}`);
|
|
27
|
+
if (typeof quota.maxScans === 'number') {
|
|
28
|
+
lines.push(` max scans: ${quota.maxScans}`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof quota.remainingScans === 'number') {
|
|
31
|
+
lines.push(` remaining: ${quota.remainingScans}`);
|
|
32
|
+
}
|
|
33
|
+
lines.push(` active: ${quota.active === true ? 'yes' : 'no'}`);
|
|
34
|
+
if (typeof quota.expiresAt === 'string' && quota.expiresAt) {
|
|
35
|
+
lines.push(` expires: ${quota.expiresAt}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (response.anonymousQuota && typeof response.anonymousQuota === 'object') {
|
|
39
|
+
const quota = response.anonymousQuota;
|
|
40
|
+
lines.push('Anonymous quota:');
|
|
41
|
+
lines.push(` scans: ${Number(quota.scansUsed || 0)} / ${Number(quota.maxScans || 0)}`);
|
|
42
|
+
lines.push(` remaining: ${Number(quota.remainingScans || 0)}`);
|
|
43
|
+
if (typeof quota.resetAt === 'string' && quota.resetAt) {
|
|
44
|
+
lines.push(` reset at: ${quota.resetAt}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
export async function runLimits(options) {
|
|
50
|
+
let baseUrl;
|
|
51
|
+
try {
|
|
52
|
+
baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(error.message);
|
|
56
|
+
return 4;
|
|
57
|
+
}
|
|
58
|
+
const hasApiKeyFlag = typeof options.apiKey === 'string' && options.apiKey.trim().length > 0;
|
|
59
|
+
const hasApiKeyEnv = typeof process.env.SKILLGUARD_API_KEY === 'string' && process.env.SKILLGUARD_API_KEY.trim().length > 0;
|
|
60
|
+
let config = null;
|
|
61
|
+
if (!hasApiKeyFlag && !hasApiKeyEnv) {
|
|
62
|
+
try {
|
|
63
|
+
config = await readConfig();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(error.message);
|
|
67
|
+
return 4;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const resolvedApiKey = resolveApiKey({
|
|
71
|
+
apiKeyFlag: options.apiKey,
|
|
72
|
+
env: process.env,
|
|
73
|
+
config,
|
|
74
|
+
});
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = await fetchLimits({
|
|
78
|
+
baseUrl,
|
|
79
|
+
timeoutMs: options.timeoutMs,
|
|
80
|
+
apiKey: resolvedApiKey?.apiKey,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(error.message);
|
|
85
|
+
return 5;
|
|
86
|
+
}
|
|
87
|
+
if (options.json) {
|
|
88
|
+
console.log(JSON.stringify({
|
|
89
|
+
cli_version: CLI_VERSION,
|
|
90
|
+
fetched_at: new Date().toISOString(),
|
|
91
|
+
...response,
|
|
92
|
+
}, null, 2));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(renderHuman(response));
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type ScanFinding, type ScanScore } from '../lib/api.js';
|
|
2
2
|
export interface ScanOptions {
|
|
3
3
|
json: boolean;
|
|
4
|
-
failOn
|
|
4
|
+
failOn?: ScanScore;
|
|
5
|
+
failOnExplicit: boolean;
|
|
6
|
+
scanAll: boolean;
|
|
7
|
+
skipNodeModules: boolean;
|
|
5
8
|
timeoutMs: number;
|
|
6
9
|
baseUrl?: string;
|
|
7
10
|
apiKey?: string;
|
|
@@ -15,4 +18,4 @@ export interface ScanResult {
|
|
|
15
18
|
findings: ScanFinding[];
|
|
16
19
|
signatureStatus: string;
|
|
17
20
|
}
|
|
18
|
-
export declare function runScan(inputPath: string, options: ScanOptions): Promise<number>;
|
|
21
|
+
export declare function runScan(inputPath: string | undefined, options: ScanOptions): Promise<number>;
|