@skillguard/cli 0.2.1 → 0.5.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 +24 -0
- package/README.md +98 -5
- package/dist/cli.js +113 -15
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +103 -0
- 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.js +20 -9
- 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/config.d.ts +1 -0
- package/dist/lib/config.js +20 -1
- package/dist/lib/help.d.ts +2 -0
- package/dist/lib/help.js +267 -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,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- Added `skillguard auth` command group:
|
|
6
|
+
- `auth login` (store key),
|
|
7
|
+
- `auth status` (show effective auth source),
|
|
8
|
+
- `auth logout` (remove local config key)
|
|
9
|
+
- Added auth help/man coverage in CLI help pages
|
|
10
|
+
|
|
11
|
+
## 0.4.0
|
|
12
|
+
|
|
13
|
+
- Added `skillguard workflow` command to generate ready-to-use GitHub Actions CI gate workflow
|
|
14
|
+
- Added `--auto-gh-push` option for workflow command (`git add/commit/push`)
|
|
15
|
+
- Added `skillguard help <command>` and `skillguard man <command>` detailed help pages
|
|
16
|
+
- Added `--workflow` alias for fast workflow generation
|
|
17
|
+
- Updated CLI/web workflow snippets to default to zero-secret gate flow
|
|
18
|
+
|
|
19
|
+
## 0.3.0
|
|
20
|
+
|
|
21
|
+
- Added `skillguard gate` command for deterministic CI gating (`0` pass, `1` fail)
|
|
22
|
+
- Added `skillguard limits` command with human and `--json` output
|
|
23
|
+
- Added anonymous scan fallback when no API key is configured
|
|
24
|
+
- Added CLI telemetry headers (`X-SkillGuard-Source`, `X-SkillGuard-CI`)
|
|
25
|
+
- Added support for backend anonymous limits endpoint
|
|
26
|
+
|
|
3
27
|
## 0.2.0
|
|
4
28
|
|
|
5
29
|
- Added `skillguard scan` without path argument (defaults to recursive scan from current directory)
|
package/README.md
CHANGED
|
@@ -8,18 +8,38 @@ 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
|
+
|
|
11
13
|
### Scan all skills in current repo
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
16
|
npx @skillguard/cli scan
|
|
15
17
|
```
|
|
16
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
|
+
|
|
17
31
|
## Setup
|
|
18
32
|
|
|
19
33
|
```bash
|
|
20
34
|
npx @skillguard/cli init
|
|
21
35
|
```
|
|
22
36
|
|
|
37
|
+
Or via auth command:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @skillguard/cli auth login
|
|
41
|
+
```
|
|
42
|
+
|
|
23
43
|
You can also provide the key via env:
|
|
24
44
|
|
|
25
45
|
```bash
|
|
@@ -46,6 +66,31 @@ npx @skillguard/cli scan ./skills --fail-on warning
|
|
|
46
66
|
npx @skillguard/cli scan ./skills --json > skillguard-report.json
|
|
47
67
|
```
|
|
48
68
|
|
|
69
|
+
### CI gate (recommended)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx @skillguard/cli gate ./skills
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Check remaining limits
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx @skillguard/cli limits
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Auth status and logout
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx @skillguard/cli auth status
|
|
85
|
+
npx @skillguard/cli auth logout
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Generate and push workflow in one command
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx @skillguard/cli workflow --auto-gh-push
|
|
92
|
+
```
|
|
93
|
+
|
|
49
94
|
### Worst-score CI exit code (recommended)
|
|
50
95
|
|
|
51
96
|
```bash
|
|
@@ -74,6 +119,39 @@ npx @skillguard/cli verify ./SKILL.md
|
|
|
74
119
|
- `--skip-node-modules` (default: enabled)
|
|
75
120
|
- `--scan-all` (disable skip filters, scans everything recursively)
|
|
76
121
|
|
|
122
|
+
### gate
|
|
123
|
+
|
|
124
|
+
- same options as `scan`
|
|
125
|
+
- defaults to CI gate behavior (`--fail-on warning` in threshold mode)
|
|
126
|
+
|
|
127
|
+
### limits
|
|
128
|
+
|
|
129
|
+
- `--json`
|
|
130
|
+
- `--timeout <ms>` (default: `30000`)
|
|
131
|
+
- `--base-url <url>` (default: `https://skillguard.ai`)
|
|
132
|
+
- `--api-key <key>` (optional)
|
|
133
|
+
- `--no-color`
|
|
134
|
+
|
|
135
|
+
### auth
|
|
136
|
+
|
|
137
|
+
- `auth login [--api-key <key>] [--base-url <url>]`
|
|
138
|
+
- `auth status [--json] [--api-key <key>] [--base-url <url>]`
|
|
139
|
+
- `auth logout`
|
|
140
|
+
|
|
141
|
+
### workflow
|
|
142
|
+
|
|
143
|
+
- `--path <file>` (default: `.github/workflows/skillguard.yml`)
|
|
144
|
+
- `--scan-path <path>` (default: `.`)
|
|
145
|
+
- `--fail-on <safe|warning|dangerous>` (default: `warning`)
|
|
146
|
+
- `--print` (print yaml, do not write file)
|
|
147
|
+
- `--force` (overwrite existing workflow file)
|
|
148
|
+
- `--auto-gh-push` (git add/commit/push after write)
|
|
149
|
+
|
|
150
|
+
### help / man
|
|
151
|
+
|
|
152
|
+
- `help [command]`
|
|
153
|
+
- `man [command]`
|
|
154
|
+
|
|
77
155
|
### verify
|
|
78
156
|
|
|
79
157
|
- `--json`
|
|
@@ -92,6 +170,15 @@ npx @skillguard/cli verify ./SKILL.md
|
|
|
92
170
|
- `verify`:
|
|
93
171
|
- `0` signature valid
|
|
94
172
|
- `1` invalid/tampered/expired signature
|
|
173
|
+
- `limits`:
|
|
174
|
+
- `0` success
|
|
175
|
+
- `gate`:
|
|
176
|
+
- `0` below threshold
|
|
177
|
+
- `1` threshold exceeded
|
|
178
|
+
- `workflow`:
|
|
179
|
+
- `0` success
|
|
180
|
+
- `auth`:
|
|
181
|
+
- `0` success
|
|
95
182
|
- all commands:
|
|
96
183
|
- `4` usage/input/config error
|
|
97
184
|
- `5` network/API/runtime external failure
|
|
@@ -99,8 +186,16 @@ npx @skillguard/cli verify ./SKILL.md
|
|
|
99
186
|
## GitHub Actions example
|
|
100
187
|
|
|
101
188
|
```yaml
|
|
102
|
-
name:
|
|
103
|
-
on:
|
|
189
|
+
name: SkillGuard Gate
|
|
190
|
+
on:
|
|
191
|
+
pull_request:
|
|
192
|
+
paths:
|
|
193
|
+
- '**/SKILL.md'
|
|
194
|
+
- '**/skill.md'
|
|
195
|
+
push:
|
|
196
|
+
paths:
|
|
197
|
+
- '**/SKILL.md'
|
|
198
|
+
- '**/skill.md'
|
|
104
199
|
|
|
105
200
|
jobs:
|
|
106
201
|
scan:
|
|
@@ -110,9 +205,7 @@ jobs:
|
|
|
110
205
|
- uses: actions/setup-node@v4
|
|
111
206
|
with:
|
|
112
207
|
node-version: '20'
|
|
113
|
-
- run: npx @skillguard/cli
|
|
114
|
-
env:
|
|
115
|
-
SKILLGUARD_API_KEY: ${{ secrets.SKILLGUARD_API_KEY }}
|
|
208
|
+
- run: npx @skillguard/cli gate . --fail-on warning
|
|
116
209
|
```
|
|
117
210
|
|
|
118
211
|
More info: [skillguard.ai](https://skillguard.ai)
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
+
import { runAuth } from './commands/auth.js';
|
|
3
4
|
import { runInit } from './commands/init.js';
|
|
5
|
+
import { runGate } from './commands/gate.js';
|
|
6
|
+
import { runLimits } from './commands/limits.js';
|
|
4
7
|
import { runScan } from './commands/scan.js';
|
|
5
8
|
import { runVerify } from './commands/verify.js';
|
|
6
|
-
|
|
9
|
+
import { runWorkflow } from './commands/workflow.js';
|
|
10
|
+
import { renderGeneralHelp, renderTopicHelp } from './lib/help.js';
|
|
11
|
+
import { CLI_VERSION } from './lib/version.js';
|
|
12
|
+
const VERSION = CLI_VERSION;
|
|
7
13
|
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] [--skip-node-modules] [--scan-all]
|
|
13
|
-
skillguard verify <path> [--json] [--timeout <ms>] [--base-url <url>]
|
|
14
|
-
|
|
15
|
-
Options:
|
|
16
|
-
--help Show help
|
|
17
|
-
--version Show version
|
|
18
|
-
`);
|
|
14
|
+
console.log(renderGeneralHelp(VERSION));
|
|
19
15
|
}
|
|
20
16
|
function toPositiveInteger(raw, fallback) {
|
|
21
17
|
if (!raw) {
|
|
@@ -53,6 +49,11 @@ function parseShared(args) {
|
|
|
53
49
|
timeout: { type: 'string' },
|
|
54
50
|
'base-url': { type: 'string' },
|
|
55
51
|
'api-key': { type: 'string' },
|
|
52
|
+
path: { type: 'string' },
|
|
53
|
+
'scan-path': { type: 'string' },
|
|
54
|
+
print: { type: 'boolean' },
|
|
55
|
+
force: { type: 'boolean' },
|
|
56
|
+
'auto-gh-push': { type: 'boolean' },
|
|
56
57
|
},
|
|
57
58
|
strict: false,
|
|
58
59
|
});
|
|
@@ -66,7 +67,12 @@ function isFailOnExplicit(args) {
|
|
|
66
67
|
}
|
|
67
68
|
async function run() {
|
|
68
69
|
const argv = process.argv.slice(2);
|
|
69
|
-
|
|
70
|
+
let command = argv[0];
|
|
71
|
+
let commandArgs = argv.slice(1);
|
|
72
|
+
if (command === '--workflow') {
|
|
73
|
+
command = 'workflow';
|
|
74
|
+
commandArgs = argv.slice(1);
|
|
75
|
+
}
|
|
70
76
|
if (!command || command === '--help' || command === '-h') {
|
|
71
77
|
printUsage();
|
|
72
78
|
return 0;
|
|
@@ -75,13 +81,33 @@ async function run() {
|
|
|
75
81
|
console.log(VERSION);
|
|
76
82
|
return 0;
|
|
77
83
|
}
|
|
78
|
-
|
|
84
|
+
if (command === 'help' || command === 'man') {
|
|
85
|
+
const topic = argv[1];
|
|
86
|
+
if (!topic) {
|
|
87
|
+
printUsage();
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
const helpText = renderTopicHelp(topic);
|
|
91
|
+
if (!helpText) {
|
|
92
|
+
console.error(`Unknown help topic: ${topic}`);
|
|
93
|
+
return 4;
|
|
94
|
+
}
|
|
95
|
+
console.log(helpText);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const { options, positionals } = parseShared(commandArgs);
|
|
79
99
|
if (options.version) {
|
|
80
100
|
console.log(VERSION);
|
|
81
101
|
return 0;
|
|
82
102
|
}
|
|
83
103
|
if (options.help) {
|
|
84
|
-
|
|
104
|
+
const topicHelp = renderTopicHelp(command);
|
|
105
|
+
if (topicHelp) {
|
|
106
|
+
console.log(topicHelp);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
printUsage();
|
|
110
|
+
}
|
|
85
111
|
return 0;
|
|
86
112
|
}
|
|
87
113
|
if (command === 'init') {
|
|
@@ -90,6 +116,15 @@ async function run() {
|
|
|
90
116
|
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
91
117
|
});
|
|
92
118
|
}
|
|
119
|
+
if (command === 'auth') {
|
|
120
|
+
const authOptions = {
|
|
121
|
+
subcommand: positionals[0],
|
|
122
|
+
json: options.json === true,
|
|
123
|
+
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
124
|
+
apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
|
|
125
|
+
};
|
|
126
|
+
return await runAuth(authOptions);
|
|
127
|
+
}
|
|
93
128
|
if (command === 'scan') {
|
|
94
129
|
const pathArg = positionals[0];
|
|
95
130
|
let parsedTimeout;
|
|
@@ -119,6 +154,69 @@ async function run() {
|
|
|
119
154
|
};
|
|
120
155
|
return await runScan(pathArg, scanOptions);
|
|
121
156
|
}
|
|
157
|
+
if (command === 'gate') {
|
|
158
|
+
const pathArg = positionals[0];
|
|
159
|
+
let parsedTimeout;
|
|
160
|
+
let failOn;
|
|
161
|
+
try {
|
|
162
|
+
parsedTimeout = toPositiveInteger(typeof options.timeout === 'string' ? options.timeout : undefined, 30000);
|
|
163
|
+
failOn = toFailOn(typeof options['fail-on'] === 'string' ? options['fail-on'] : undefined);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error(error.message);
|
|
167
|
+
return 4;
|
|
168
|
+
}
|
|
169
|
+
const gateOptions = {
|
|
170
|
+
json: options.json === true,
|
|
171
|
+
failOn,
|
|
172
|
+
scanAll: options['scan-all'] === true,
|
|
173
|
+
skipNodeModules: options['scan-all'] === true ? false : options['skip-node-modules'] !== false,
|
|
174
|
+
timeoutMs: parsedTimeout,
|
|
175
|
+
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
176
|
+
apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
|
|
177
|
+
dryRun: options['dry-run'] === true,
|
|
178
|
+
quiet: options.quiet === true,
|
|
179
|
+
noColor: options['no-color'] === true,
|
|
180
|
+
};
|
|
181
|
+
return await runGate(pathArg, gateOptions);
|
|
182
|
+
}
|
|
183
|
+
if (command === 'limits') {
|
|
184
|
+
let parsedTimeout;
|
|
185
|
+
try {
|
|
186
|
+
parsedTimeout = toPositiveInteger(typeof options.timeout === 'string' ? options.timeout : undefined, 30000);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error(error.message);
|
|
190
|
+
return 4;
|
|
191
|
+
}
|
|
192
|
+
const limitsOptions = {
|
|
193
|
+
json: options.json === true,
|
|
194
|
+
timeoutMs: parsedTimeout,
|
|
195
|
+
baseUrl: typeof options['base-url'] === 'string' ? options['base-url'] : undefined,
|
|
196
|
+
apiKey: typeof options['api-key'] === 'string' ? options['api-key'] : undefined,
|
|
197
|
+
noColor: options['no-color'] === true,
|
|
198
|
+
};
|
|
199
|
+
return await runLimits(limitsOptions);
|
|
200
|
+
}
|
|
201
|
+
if (command === 'workflow') {
|
|
202
|
+
let failOn;
|
|
203
|
+
try {
|
|
204
|
+
failOn = toFailOn(typeof options['fail-on'] === 'string' ? options['fail-on'] : undefined);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error(error.message);
|
|
208
|
+
return 4;
|
|
209
|
+
}
|
|
210
|
+
const workflowOptions = {
|
|
211
|
+
outputPath: typeof options.path === 'string' ? options.path : undefined,
|
|
212
|
+
scanPath: typeof options['scan-path'] === 'string' ? options['scan-path'] : '.',
|
|
213
|
+
failOn,
|
|
214
|
+
print: options.print === true,
|
|
215
|
+
force: options.force === true,
|
|
216
|
+
autoGhPush: options['auto-gh-push'] === true,
|
|
217
|
+
};
|
|
218
|
+
return await runWorkflow(workflowOptions);
|
|
219
|
+
}
|
|
122
220
|
if (command === 'verify') {
|
|
123
221
|
const pathArg = positionals[0];
|
|
124
222
|
if (!pathArg) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { clearConfig, DEFAULT_BASE_URL, getConfigPath, normalizeBaseUrl, readConfig, resolveApiKey } from '../lib/config.js';
|
|
2
|
+
import { runInit } from './init.js';
|
|
3
|
+
function toAuthSubcommand(raw) {
|
|
4
|
+
if (!raw) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const value = raw.trim().toLowerCase();
|
|
8
|
+
if (value === 'login')
|
|
9
|
+
return 'login';
|
|
10
|
+
if (value === 'status')
|
|
11
|
+
return 'status';
|
|
12
|
+
if (value === 'logout')
|
|
13
|
+
return 'logout';
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
function renderStatusHuman(input) {
|
|
17
|
+
const lines = [
|
|
18
|
+
`Auth: ${input.authenticated ? 'configured' : 'not configured'}`,
|
|
19
|
+
`Source: ${input.source}`,
|
|
20
|
+
`Base URL: ${input.baseUrl}`,
|
|
21
|
+
`Config file: ${input.configPath}`,
|
|
22
|
+
];
|
|
23
|
+
if (!input.authenticated) {
|
|
24
|
+
lines.push('No API key found. Use "skillguard auth login" or set SKILLGUARD_API_KEY.');
|
|
25
|
+
}
|
|
26
|
+
if (input.hasEnvKey) {
|
|
27
|
+
lines.push('Env key detected: SKILLGUARD_API_KEY');
|
|
28
|
+
}
|
|
29
|
+
if (input.hasConfigKey) {
|
|
30
|
+
lines.push('Config key detected.');
|
|
31
|
+
}
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
async function runStatus(options) {
|
|
35
|
+
const configPath = getConfigPath();
|
|
36
|
+
let config = null;
|
|
37
|
+
try {
|
|
38
|
+
config = await readConfig(configPath);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(error.message);
|
|
42
|
+
return 4;
|
|
43
|
+
}
|
|
44
|
+
const resolvedApiKey = resolveApiKey({
|
|
45
|
+
apiKeyFlag: options.apiKey,
|
|
46
|
+
env: process.env,
|
|
47
|
+
config,
|
|
48
|
+
});
|
|
49
|
+
let baseUrl;
|
|
50
|
+
try {
|
|
51
|
+
baseUrl = normalizeBaseUrl(options.baseUrl || config?.apiUrl || DEFAULT_BASE_URL);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error(error.message);
|
|
55
|
+
return 4;
|
|
56
|
+
}
|
|
57
|
+
const payload = {
|
|
58
|
+
authenticated: Boolean(resolvedApiKey),
|
|
59
|
+
source: (resolvedApiKey?.source || 'none'),
|
|
60
|
+
baseUrl,
|
|
61
|
+
configPath,
|
|
62
|
+
hasConfigKey: typeof config?.apiKey === 'string' && config.apiKey.trim().length > 0,
|
|
63
|
+
hasEnvKey: typeof process.env.SKILLGUARD_API_KEY === 'string' && process.env.SKILLGUARD_API_KEY.trim().length > 0,
|
|
64
|
+
};
|
|
65
|
+
if (options.json) {
|
|
66
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
console.log(renderStatusHuman(payload));
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
async function runLogout() {
|
|
73
|
+
const configPath = getConfigPath();
|
|
74
|
+
try {
|
|
75
|
+
const removed = await clearConfig(configPath);
|
|
76
|
+
console.log(removed ? `Logged out. Removed ${configPath}` : `No config found at ${configPath}`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(error.message);
|
|
80
|
+
return 4;
|
|
81
|
+
}
|
|
82
|
+
if (typeof process.env.SKILLGUARD_API_KEY === 'string' && process.env.SKILLGUARD_API_KEY.trim().length > 0) {
|
|
83
|
+
console.log('Note: SKILLGUARD_API_KEY is set in environment and will still be used by this shell.');
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
export async function runAuth(options) {
|
|
88
|
+
const subcommand = toAuthSubcommand(options.subcommand);
|
|
89
|
+
if (!subcommand) {
|
|
90
|
+
console.error('Auth subcommand is required: login | status | logout');
|
|
91
|
+
return 4;
|
|
92
|
+
}
|
|
93
|
+
if (subcommand === 'login') {
|
|
94
|
+
return runInit({
|
|
95
|
+
apiKey: options.apiKey,
|
|
96
|
+
baseUrl: options.baseUrl,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (subcommand === 'status') {
|
|
100
|
+
return runStatus(options);
|
|
101
|
+
}
|
|
102
|
+
return runLogout();
|
|
103
|
+
}
|
|
@@ -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.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFile, readdir, realpath } from 'node:fs/promises';
|
|
2
2
|
import { basename, resolve, sep } from 'node:path';
|
|
3
|
-
import { normalizeFindings, scanContent, toScanScore } from '../lib/api.js';
|
|
3
|
+
import { normalizeFindings, scanContent, scanContentAnonymous, toScanScore } from '../lib/api.js';
|
|
4
4
|
import { normalizeBaseUrl, readConfig, resolveApiKey } from '../lib/config.js';
|
|
5
5
|
import { renderSingleScan, renderSummary, shouldUseColor, summarizeScores } from '../lib/format.js';
|
|
6
|
+
import { CLI_VERSION } from '../lib/version.js';
|
|
6
7
|
const FAIL_LEVELS = {
|
|
7
8
|
safe: 0,
|
|
8
9
|
warning: 1,
|
|
@@ -164,22 +165,32 @@ export async function runScan(inputPath, options) {
|
|
|
164
165
|
env: process.env,
|
|
165
166
|
config,
|
|
166
167
|
});
|
|
167
|
-
|
|
168
|
+
const useAnonymous = !resolvedApiKey;
|
|
169
|
+
if (!useAnonymous && !resolvedApiKey) {
|
|
168
170
|
console.error("No API key found. Run 'skillguard init' or set SKILLGUARD_API_KEY.");
|
|
169
171
|
return 4;
|
|
170
172
|
}
|
|
171
173
|
const color = shouldUseColor(options.noColor);
|
|
172
174
|
const results = [];
|
|
175
|
+
if (useAnonymous && !options.quiet && !options.json) {
|
|
176
|
+
console.log('Scanning anonymously (limited). Add API key for higher limits.');
|
|
177
|
+
}
|
|
173
178
|
for (const filePath of targets) {
|
|
174
179
|
const content = await readFile(filePath, 'utf8');
|
|
175
180
|
let apiResponse;
|
|
176
181
|
try {
|
|
177
|
-
apiResponse =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
apiResponse = useAnonymous
|
|
183
|
+
? await scanContentAnonymous({
|
|
184
|
+
baseUrl,
|
|
185
|
+
content,
|
|
186
|
+
timeoutMs: options.timeoutMs,
|
|
187
|
+
})
|
|
188
|
+
: await scanContent({
|
|
189
|
+
baseUrl,
|
|
190
|
+
apiKey: resolvedApiKey.apiKey,
|
|
191
|
+
content,
|
|
192
|
+
timeoutMs: options.timeoutMs,
|
|
193
|
+
});
|
|
183
194
|
}
|
|
184
195
|
catch (error) {
|
|
185
196
|
console.error(error.message);
|
|
@@ -208,7 +219,7 @@ export async function runScan(inputPath, options) {
|
|
|
208
219
|
if (options.json) {
|
|
209
220
|
const summary = summarizeScores(results);
|
|
210
221
|
console.log(JSON.stringify({
|
|
211
|
-
cli_version:
|
|
222
|
+
cli_version: CLI_VERSION,
|
|
212
223
|
scanned_at: new Date().toISOString(),
|
|
213
224
|
mode,
|
|
214
225
|
worst_score: worstScore,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ScanScore } from '../lib/api.js';
|
|
2
|
+
export interface WorkflowOptions {
|
|
3
|
+
outputPath?: string;
|
|
4
|
+
scanPath: string;
|
|
5
|
+
failOn: ScanScore;
|
|
6
|
+
print: boolean;
|
|
7
|
+
force: boolean;
|
|
8
|
+
autoGhPush: boolean;
|
|
9
|
+
commitMessage?: string;
|
|
10
|
+
}
|
|
11
|
+
interface GitCommandResult {
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
status: number;
|
|
15
|
+
}
|
|
16
|
+
type GitRunner = (args: string[]) => GitCommandResult;
|
|
17
|
+
export declare function runWorkflow(options: WorkflowOptions, runtime?: {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
gitRunner?: GitRunner;
|
|
20
|
+
}): Promise<number>;
|
|
21
|
+
export {};
|