@rafter-security/cli 0.1.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.
Files changed (3) hide show
  1. package/README.md +108 -0
  2. package/dist/index.js +281 -0
  3. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @rafter-security/cli
2
+
3
+ A Node.js CLI for Rafter Security that supports npm, pnpm, and yarn package managers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Using npm
9
+ npm install -g @rafter-security/cli
10
+
11
+ # Using pnpm
12
+ pnpm add -g @rafter-security/cli
13
+
14
+ # Using yarn
15
+ yarn global add @rafter-security/cli
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ # Set your API key
22
+ export RAFTER_API_KEY="your-api-key-here"
23
+
24
+ # Run a security scan
25
+ rafter run
26
+
27
+ # Get scan results
28
+ rafter get <scan-id>
29
+
30
+ # Check API usage
31
+ rafter usage
32
+ ```
33
+
34
+ ## Commands
35
+
36
+ ### `rafter run [options]`
37
+
38
+ Trigger a new security scan for your repository.
39
+
40
+ **Options:**
41
+ - `-r, --repo <repo>` - Repository in format `org/repo` (default: auto-detected)
42
+ - `-b, --branch <branch>` - Branch name (default: auto-detected)
43
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
44
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
45
+ - `--skip-interactive` - Don't wait for scan completion
46
+ - `--quiet` - Suppress status messages
47
+
48
+ **Examples:**
49
+ ```bash
50
+ # Basic scan with auto-detection
51
+ rafter run
52
+
53
+ # Scan specific repo/branch
54
+ rafter run --repo myorg/myrepo --branch feature-branch
55
+
56
+ # Non-interactive scan
57
+ rafter run --skip-interactive
58
+ ```
59
+
60
+ ### `rafter get <scan-id> [options]`
61
+
62
+ Retrieve results from a completed scan.
63
+
64
+ **Options:**
65
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
66
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
67
+ - `--interactive` - Poll until scan completes
68
+ - `--quiet` - Suppress status messages
69
+
70
+ **Examples:**
71
+ ```bash
72
+ # Get scan results
73
+ rafter get <scan-id>
74
+
75
+ # Wait for scan completion
76
+ rafter get <scan-id> --interactive
77
+ ```
78
+
79
+ ### `rafter usage [options]`
80
+
81
+ Check your API quota and usage.
82
+
83
+ **Options:**
84
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
85
+
86
+ **Example:**
87
+ ```bash
88
+ rafter usage
89
+ ```
90
+
91
+ ## Configuration
92
+
93
+ ### Environment Variables
94
+
95
+ - `RAFTER_API_KEY` - Your Rafter API key (alternative to `--api-key` flag)
96
+
97
+ ### Git Auto-Detection
98
+
99
+ The CLI automatically detects your repository and branch from the current Git repository:
100
+
101
+ 1. **Repository**: Extracted from Git remote URL
102
+ 2. **Branch**: Current branch name, or `main` if on detached HEAD
103
+
104
+ **Note**: The CLI only scans remote repositories, not your current local branch.
105
+
106
+ ## Documentation
107
+
108
+ For comprehensive documentation, API reference, and examples, see [https://docs.rafter.so](https://docs.rafter.so).
package/dist/index.js ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import axios from "axios";
4
+ import ora from "ora";
5
+ import * as dotenv from "dotenv";
6
+ import { execSync } from "child_process";
7
+ dotenv.config();
8
+ const program = new Command()
9
+ .name("rafter")
10
+ .description("Rafter CLI")
11
+ .version("0.1.0");
12
+ const API = "https://rafter.so/api/";
13
+ // Exit codes
14
+ const EXIT_SUCCESS = 0;
15
+ const EXIT_GENERAL_ERROR = 1;
16
+ const EXIT_SCAN_NOT_FOUND = 2;
17
+ const EXIT_QUOTA_EXHAUSTED = 3;
18
+ function resolveKey(cliKey) {
19
+ if (cliKey)
20
+ return cliKey;
21
+ if (process.env.RAFTER_API_KEY)
22
+ return process.env.RAFTER_API_KEY;
23
+ console.error("No API key provided. Use --api-key or set RAFTER_API_KEY");
24
+ process.exit(EXIT_GENERAL_ERROR);
25
+ }
26
+ function writePayload(data, fmt, quiet) {
27
+ const payload = fmt === "md" && data.markdown ? data.markdown : JSON.stringify(data, null, quiet ? 0 : 2);
28
+ // Stream to stdout for pipelines
29
+ process.stdout.write(payload);
30
+ return EXIT_SUCCESS;
31
+ }
32
+ function git(cmd) {
33
+ return execSync(`git ${cmd}`, { stdio: ["ignore", "pipe", "ignore"] })
34
+ .toString()
35
+ .trim();
36
+ }
37
+ function safeBranch(gitFn) {
38
+ try {
39
+ return gitFn("symbolic-ref --quiet --short HEAD");
40
+ }
41
+ catch {
42
+ return gitFn("rev-parse --short HEAD");
43
+ }
44
+ }
45
+ function parseRemote(url) {
46
+ url = url.replace(/^(https?:\/\/|git@)/, "").replace(":", "/");
47
+ if (url.endsWith(".git"))
48
+ url = url.slice(0, -4);
49
+ const parts = url.split("/");
50
+ return parts.slice(-2).join("/"); // owner/repo
51
+ }
52
+ function detectRepo(opts) {
53
+ if (opts.repo && opts.branch)
54
+ return opts;
55
+ const repoEnv = process.env.GITHUB_REPOSITORY || process.env.CI_REPOSITORY;
56
+ const branchEnv = process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH || process.env.CI_BRANCH;
57
+ let repoSlug = opts.repo || repoEnv;
58
+ let branch = opts.branch || branchEnv;
59
+ try {
60
+ if (!repoSlug || !branch) {
61
+ if (git("rev-parse --is-inside-work-tree") !== "true")
62
+ throw new Error("not a repo");
63
+ if (!repoSlug)
64
+ repoSlug = parseRemote(git("remote get-url origin"));
65
+ if (!branch) {
66
+ try {
67
+ branch = safeBranch(git);
68
+ }
69
+ catch {
70
+ branch = "main";
71
+ }
72
+ }
73
+ }
74
+ if ((!opts.repo || !opts.branch) && !opts.quiet) {
75
+ console.error(`Repo auto-detected: ${repoSlug} @ ${branch} (note: scanning remote)`);
76
+ }
77
+ return { repo: repoSlug, branch };
78
+ }
79
+ catch {
80
+ throw new Error("Could not auto-detect Git repository. Please pass --repo and --branch explicitly.");
81
+ }
82
+ }
83
+ async function handleScanStatus(scan_id, headers, fmt, quiet) {
84
+ // First poll
85
+ let poll;
86
+ try {
87
+ poll = await axios.get(`${API}/static/scan`, { params: { scan_id, format: fmt }, headers });
88
+ }
89
+ catch (e) {
90
+ if (e.response?.status === 404) {
91
+ console.error(`Scan '${scan_id}' not found`);
92
+ return EXIT_SCAN_NOT_FOUND;
93
+ }
94
+ console.error(`Error: ${e.response?.data || e.message}`);
95
+ return EXIT_GENERAL_ERROR;
96
+ }
97
+ let status = poll.data.status;
98
+ if (["queued", "pending", "processing"].includes(status)) {
99
+ if (!quiet) {
100
+ const spinner = ora("Waiting for scan to complete... (this could take several minutes)").start();
101
+ while (["queued", "pending", "processing"].includes(status)) {
102
+ await new Promise((r) => setTimeout(r, 10000));
103
+ poll = await axios.get(`${API}/static/scan`, { params: { scan_id, format: fmt }, headers });
104
+ status = poll.data.status;
105
+ if (status === "completed") {
106
+ spinner.succeed("Scan completed");
107
+ return writePayload(poll.data, fmt, quiet);
108
+ }
109
+ else if (status === "failed") {
110
+ spinner.fail("Scan failed");
111
+ return EXIT_GENERAL_ERROR;
112
+ }
113
+ }
114
+ console.error(`Scan status: ${status}`);
115
+ }
116
+ else {
117
+ while (["queued", "pending", "processing"].includes(status)) {
118
+ await new Promise((r) => setTimeout(r, 10000));
119
+ poll = await axios.get(`${API}/static/scan`, { params: { scan_id, format: fmt }, headers });
120
+ status = poll.data.status;
121
+ if (status === "completed") {
122
+ return writePayload(poll.data, fmt, quiet);
123
+ }
124
+ else if (status === "failed") {
125
+ return EXIT_GENERAL_ERROR;
126
+ }
127
+ }
128
+ }
129
+ }
130
+ else if (status === "completed") {
131
+ if (!quiet) {
132
+ console.error("Scan completed");
133
+ }
134
+ return writePayload(poll.data, fmt, quiet);
135
+ }
136
+ else if (status === "failed") {
137
+ console.error("Scan failed");
138
+ return EXIT_GENERAL_ERROR;
139
+ }
140
+ else {
141
+ if (!quiet) {
142
+ console.error(`Scan status: ${status}`);
143
+ }
144
+ }
145
+ return writePayload(poll.data, fmt, quiet);
146
+ }
147
+ program
148
+ .name("rafter")
149
+ .description("Rafter CLI");
150
+ program
151
+ .command("run")
152
+ .option("-r, --repo <repo>", "org/repo (default: current)")
153
+ .option("-b, --branch <branch>", "branch (default: current else main)")
154
+ .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
155
+ .option("-f, --format <format>", "json | md", "json")
156
+ .option("--skip-interactive", "do not wait for scan to complete")
157
+ .option("--quiet", "suppress status messages")
158
+ .action(async (opts) => {
159
+ const key = resolveKey(opts.apiKey);
160
+ let repo, branch;
161
+ try {
162
+ ({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
163
+ }
164
+ catch (e) {
165
+ if (e instanceof Error) {
166
+ console.error(e.message);
167
+ }
168
+ else {
169
+ console.error(e);
170
+ }
171
+ process.exit(EXIT_GENERAL_ERROR);
172
+ }
173
+ if (!opts.quiet) {
174
+ const spinner = ora("Submitting scan").start();
175
+ try {
176
+ const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
177
+ spinner.succeed(`Scan ID: ${data.scan_id}`);
178
+ if (opts.skipInteractive)
179
+ return;
180
+ const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format, opts.quiet);
181
+ process.exit(exitCode);
182
+ }
183
+ catch (e) {
184
+ spinner.fail("Request failed");
185
+ if (e.response?.status === 429) {
186
+ console.error("Quota exhausted");
187
+ process.exit(EXIT_QUOTA_EXHAUSTED);
188
+ }
189
+ else if (e.response?.data) {
190
+ console.error(e.response.data);
191
+ }
192
+ else if (e instanceof Error) {
193
+ console.error(e.message);
194
+ }
195
+ else {
196
+ console.error(e);
197
+ }
198
+ process.exit(EXIT_GENERAL_ERROR);
199
+ }
200
+ }
201
+ else {
202
+ try {
203
+ const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
204
+ if (opts.skipInteractive)
205
+ return;
206
+ const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format, opts.quiet);
207
+ process.exit(exitCode);
208
+ }
209
+ catch (e) {
210
+ if (e.response?.status === 429) {
211
+ process.exit(EXIT_QUOTA_EXHAUSTED);
212
+ }
213
+ else if (e.response?.data) {
214
+ console.error(e.response.data);
215
+ }
216
+ else if (e instanceof Error) {
217
+ console.error(e.message);
218
+ }
219
+ else {
220
+ console.error(e);
221
+ }
222
+ process.exit(EXIT_GENERAL_ERROR);
223
+ }
224
+ }
225
+ });
226
+ program
227
+ .command("get")
228
+ .argument("<scan_id>")
229
+ .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
230
+ .option("-f, --format <format>", "json | md", "json")
231
+ .option("--interactive", "poll until done")
232
+ .option("--quiet", "suppress status messages")
233
+ .action(async (scan_id, opts) => {
234
+ const key = resolveKey(opts.apiKey);
235
+ if (!opts.interactive) {
236
+ try {
237
+ const { data } = await axios.get(`${API}/static/scan`, { params: { scan_id, format: opts.format }, headers: { "x-api-key": key } });
238
+ const exitCode = writePayload(data, opts.format, opts.quiet);
239
+ process.exit(exitCode);
240
+ }
241
+ catch (e) {
242
+ if (e.response?.status === 404) {
243
+ console.error(`Scan '${scan_id}' not found`);
244
+ process.exit(EXIT_SCAN_NOT_FOUND);
245
+ }
246
+ else if (e.response?.data) {
247
+ console.error(e.response.data);
248
+ }
249
+ else if (e instanceof Error) {
250
+ console.error(e.message);
251
+ }
252
+ else {
253
+ console.error(e);
254
+ }
255
+ process.exit(EXIT_GENERAL_ERROR);
256
+ }
257
+ return;
258
+ }
259
+ const exitCode = await handleScanStatus(scan_id, { "x-api-key": key }, opts.format, opts.quiet);
260
+ process.exit(exitCode);
261
+ });
262
+ program
263
+ .command("usage")
264
+ .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
265
+ .action(async (opts) => {
266
+ const key = resolveKey(opts.apiKey);
267
+ try {
268
+ const { data } = await axios.get(`${API}/static/usage`, { headers: { "x-api-key": key } });
269
+ console.log(JSON.stringify(data, null, 2));
270
+ }
271
+ catch (e) {
272
+ if (e.response?.data) {
273
+ console.error(e.response.data);
274
+ }
275
+ else {
276
+ console.error(e.message);
277
+ }
278
+ process.exit(EXIT_GENERAL_ERROR);
279
+ }
280
+ });
281
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@rafter-security/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "rafter": "./dist/index.js"
7
+ },
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "prepublishOnly": "pnpm run build",
12
+ "test": "vitest"
13
+ },
14
+ "engines": { "node": ">=18" },
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "commander": "^11.1.0",
18
+ "axios": "^1.6.8",
19
+ "dotenv": "^16.4.5",
20
+ "chalk": "^5.3.0",
21
+ "ora": "^7.0.1"
22
+ },
23
+ "devDependencies": {
24
+ "tsx": "^4.7.0",
25
+ "typescript": "^5.4.5",
26
+ "@types/node": "^20.11.30",
27
+ "vitest": "^1.5.0"
28
+ }
29
+ }