middlebrick 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.
- package/README.md +74 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +33 -0
- package/dist/commands/results.d.ts +2 -0
- package/dist/commands/results.js +47 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +103 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +41 -0
- package/dist/formatters/json.d.ts +2 -0
- package/dist/formatters/json.js +3 -0
- package/dist/formatters/summary.d.ts +2 -0
- package/dist/formatters/summary.js +97 -0
- package/dist/formatters/table.d.ts +2 -0
- package/dist/formatters/table.js +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +20 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# middlebrick
|
|
2
|
+
|
|
3
|
+
CLI for [middleBrick](https://middlebrick.com) API security scanning. Scan APIs from your terminal, get risk scores, and gate CI/CD pipelines.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g middlebrick
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Set up your API key
|
|
15
|
+
middlebrick configure
|
|
16
|
+
|
|
17
|
+
# Scan an API
|
|
18
|
+
middlebrick scan https://api.example.com/v1/users
|
|
19
|
+
|
|
20
|
+
# JSON output for CI
|
|
21
|
+
middlebrick scan https://api.example.com/v1/users --format json
|
|
22
|
+
|
|
23
|
+
# Fail if score < 80
|
|
24
|
+
middlebrick scan https://api.example.com/v1/users --threshold 80
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
### `middlebrick scan <url>`
|
|
30
|
+
|
|
31
|
+
Scan an API endpoint and display security results.
|
|
32
|
+
|
|
33
|
+
| Flag | Description | Default |
|
|
34
|
+
|------|-------------|---------|
|
|
35
|
+
| `--method <METHOD>` | HTTP method | `GET` |
|
|
36
|
+
| `--format <FORMAT>` | Output: `summary`, `json`, `table` | `summary` |
|
|
37
|
+
| `--threshold <score>` | Exit code 1 if score below threshold | — |
|
|
38
|
+
| `--no-wait` | Return scanId immediately | — |
|
|
39
|
+
|
|
40
|
+
### `middlebrick results <scanId>`
|
|
41
|
+
|
|
42
|
+
Fetch results of a previous scan.
|
|
43
|
+
|
|
44
|
+
### `middlebrick configure`
|
|
45
|
+
|
|
46
|
+
Interactively set up your API key. Saves to `~/.middlebrick/config.json`.
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
| Priority | Source |
|
|
51
|
+
|----------|--------|
|
|
52
|
+
| 1 | `--api-key` flag |
|
|
53
|
+
| 2 | `MIDDLEBRICK_API_KEY` env var |
|
|
54
|
+
| 3 | `~/.middlebrick/config.json` |
|
|
55
|
+
|
|
56
|
+
## Exit Codes
|
|
57
|
+
|
|
58
|
+
| Code | Meaning |
|
|
59
|
+
|------|---------|
|
|
60
|
+
| 0 | Success |
|
|
61
|
+
| 1 | Scan failed or score below threshold |
|
|
62
|
+
| 2 | Auth/config error |
|
|
63
|
+
|
|
64
|
+
## CI/CD
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# GitHub Actions
|
|
68
|
+
- name: API Security Gate
|
|
69
|
+
run: npx middlebrick scan ${{ env.API_URL }} --threshold 75 --api-key ${{ secrets.MIDDLEBRICK_API_KEY }}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT — [Zevlat Intelligence](https://zev.lat)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { saveConfig, loadConfig } from "../config.js";
|
|
4
|
+
export const configureCommand = new Command("configure")
|
|
5
|
+
.description("Set up your middleBrick API key")
|
|
6
|
+
.action(async () => {
|
|
7
|
+
const existing = loadConfig();
|
|
8
|
+
const rl = createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
});
|
|
12
|
+
const prompt = existing.apiKey
|
|
13
|
+
? `API key [${existing.apiKey.slice(0, 6)}...]: `
|
|
14
|
+
: "API key: ";
|
|
15
|
+
const apiKey = await new Promise((resolve) => {
|
|
16
|
+
rl.question(prompt, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(answer.trim());
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
if (existing.apiKey) {
|
|
23
|
+
console.log("Keeping existing key.");
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.error("No key provided. Get one at https://middlebrick.com/dashboard");
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
saveConfig({ apiKey });
|
|
32
|
+
console.log("API key saved to ~/.middlebrick/config.json");
|
|
33
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { MiddleBrickClient, MiddleBrickError } from "@middlebrick/api-client";
|
|
3
|
+
import { resolveApiKey } from "../config.js";
|
|
4
|
+
import { formatSummary } from "../formatters/summary.js";
|
|
5
|
+
import { formatJson } from "../formatters/json.js";
|
|
6
|
+
import { formatTable } from "../formatters/table.js";
|
|
7
|
+
export const resultsCommand = new Command("results")
|
|
8
|
+
.description("Fetch results of a previous scan")
|
|
9
|
+
.argument("<scanId>", "Scan ID to retrieve")
|
|
10
|
+
.option("--format <FORMAT>", "Output format: summary | json | table", "summary")
|
|
11
|
+
.action(async (scanId, opts, cmd) => {
|
|
12
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
13
|
+
let apiKey;
|
|
14
|
+
try {
|
|
15
|
+
apiKey = resolveApiKey(globalOpts);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
console.error(err.message);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
const client = new MiddleBrickClient({
|
|
22
|
+
apiKey,
|
|
23
|
+
baseUrl: globalOpts.baseUrl,
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const result = await client.getScan(scanId);
|
|
27
|
+
switch (opts.format) {
|
|
28
|
+
case "json":
|
|
29
|
+
console.log(formatJson(result));
|
|
30
|
+
break;
|
|
31
|
+
case "table":
|
|
32
|
+
console.log(formatTable(result));
|
|
33
|
+
break;
|
|
34
|
+
case "summary":
|
|
35
|
+
default:
|
|
36
|
+
console.log(formatSummary(result));
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err instanceof MiddleBrickError) {
|
|
42
|
+
console.error(`Error: ${err.message}`);
|
|
43
|
+
process.exit(err.statusCode === 401 || err.statusCode === 403 ? 2 : 1);
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { MiddleBrickClient, MiddleBrickError } from "@middlebrick/api-client";
|
|
3
|
+
import { resolveApiKey } from "../config.js";
|
|
4
|
+
import { formatSummary } from "../formatters/summary.js";
|
|
5
|
+
import { formatJson } from "../formatters/json.js";
|
|
6
|
+
import { formatTable } from "../formatters/table.js";
|
|
7
|
+
export const scanCommand = new Command("scan")
|
|
8
|
+
.description("Scan an API endpoint for security risks")
|
|
9
|
+
.argument("<url>", "API endpoint URL to scan")
|
|
10
|
+
.option("--method <METHOD>", "HTTP method", "GET")
|
|
11
|
+
.option("--format <FORMAT>", "Output format: summary | json | table", "summary")
|
|
12
|
+
.option("--threshold <score>", "Exit code 1 if overall score < threshold", parseFloat)
|
|
13
|
+
.option("--no-wait", "Return scanId immediately, don't poll")
|
|
14
|
+
.action(async (url, opts, cmd) => {
|
|
15
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
16
|
+
let apiKey;
|
|
17
|
+
try {
|
|
18
|
+
apiKey = resolveApiKey(globalOpts);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
console.error(err.message);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const client = new MiddleBrickClient({
|
|
25
|
+
apiKey,
|
|
26
|
+
baseUrl: globalOpts.baseUrl,
|
|
27
|
+
});
|
|
28
|
+
let result;
|
|
29
|
+
try {
|
|
30
|
+
if (opts.wait === false) {
|
|
31
|
+
if (opts.threshold !== undefined) {
|
|
32
|
+
console.error("Warning: --threshold is ignored with --no-wait");
|
|
33
|
+
}
|
|
34
|
+
const { scanId } = await client.scanApi({
|
|
35
|
+
apiUrl: url,
|
|
36
|
+
method: opts.method,
|
|
37
|
+
});
|
|
38
|
+
console.log(scanId);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Show spinner while waiting
|
|
42
|
+
const spinner = createSpinner("Scanning...");
|
|
43
|
+
spinner.start();
|
|
44
|
+
try {
|
|
45
|
+
result = await client.scanAndWait({
|
|
46
|
+
apiUrl: url,
|
|
47
|
+
method: opts.method,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
spinner.stop();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err instanceof MiddleBrickError) {
|
|
56
|
+
console.error(`Error: ${err.message}`);
|
|
57
|
+
process.exit(err.statusCode === 401 || err.statusCode === 403 ? 2 : 1);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
// Format output
|
|
62
|
+
const output = formatOutput(result, opts.format, opts.threshold);
|
|
63
|
+
console.log(output);
|
|
64
|
+
// Threshold check
|
|
65
|
+
if (opts.threshold !== undefined &&
|
|
66
|
+
result.overall !== undefined &&
|
|
67
|
+
result.overall < opts.threshold) {
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
if (result.status === "failed") {
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
function formatOutput(result, format, threshold) {
|
|
75
|
+
switch (format) {
|
|
76
|
+
case "json":
|
|
77
|
+
return formatJson(result);
|
|
78
|
+
case "table":
|
|
79
|
+
return formatTable(result);
|
|
80
|
+
case "summary":
|
|
81
|
+
default:
|
|
82
|
+
return formatSummary(result, threshold);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function createSpinner(message) {
|
|
86
|
+
let interval = null;
|
|
87
|
+
let dots = 0;
|
|
88
|
+
return {
|
|
89
|
+
start() {
|
|
90
|
+
process.stdout.write(message);
|
|
91
|
+
interval = setInterval(() => {
|
|
92
|
+
dots = (dots + 1) % 4;
|
|
93
|
+
process.stdout.write(`\r${message}${".".repeat(dots)}${" ".slice(dots)}`);
|
|
94
|
+
}, 300);
|
|
95
|
+
},
|
|
96
|
+
stop() {
|
|
97
|
+
if (interval) {
|
|
98
|
+
clearInterval(interval);
|
|
99
|
+
process.stdout.write("\r" + " ".repeat(message.length + 4) + "\r");
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
}
|
|
4
|
+
declare const CONFIG_DIR: string;
|
|
5
|
+
declare const CONFIG_FILE: string;
|
|
6
|
+
export declare function resolveApiKey(opts: {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function loadConfig(): Config;
|
|
10
|
+
export declare function saveConfig(config: Config): void;
|
|
11
|
+
export { CONFIG_DIR, CONFIG_FILE };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".middlebrick");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function resolveApiKey(opts) {
|
|
7
|
+
// 1. CLI flag
|
|
8
|
+
if (opts.apiKey)
|
|
9
|
+
return opts.apiKey;
|
|
10
|
+
// 2. Environment variable
|
|
11
|
+
const envKey = process.env.MIDDLEBRICK_API_KEY;
|
|
12
|
+
if (envKey)
|
|
13
|
+
return envKey;
|
|
14
|
+
// 3. Config file
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
if (config.apiKey)
|
|
17
|
+
return config.apiKey;
|
|
18
|
+
throw new Error([
|
|
19
|
+
"No API key found. Set one with:",
|
|
20
|
+
"",
|
|
21
|
+
" middlebrick configure",
|
|
22
|
+
" --api-key <key>",
|
|
23
|
+
" MIDDLEBRICK_API_KEY=mb_... middlebrick scan <url>",
|
|
24
|
+
"",
|
|
25
|
+
"Get your API key at https://middlebrick.com/dashboard",
|
|
26
|
+
].join("\n"));
|
|
27
|
+
}
|
|
28
|
+
export function loadConfig() {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function saveConfig(config) {
|
|
38
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
40
|
+
}
|
|
41
|
+
export { CONFIG_DIR, CONFIG_FILE };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
function scoreBar(score) {
|
|
3
|
+
const filled = Math.round(score / 10);
|
|
4
|
+
const empty = 10 - filled;
|
|
5
|
+
return chalk.green("█".repeat(filled)) + chalk.gray("░".repeat(empty));
|
|
6
|
+
}
|
|
7
|
+
function gradeColor(grade) {
|
|
8
|
+
switch (grade) {
|
|
9
|
+
case "A":
|
|
10
|
+
return chalk.green(grade);
|
|
11
|
+
case "B":
|
|
12
|
+
return chalk.cyan(grade);
|
|
13
|
+
case "C":
|
|
14
|
+
return chalk.yellow(grade);
|
|
15
|
+
case "D":
|
|
16
|
+
return chalk.hex("#FF8800")(grade);
|
|
17
|
+
case "F":
|
|
18
|
+
return chalk.red(grade);
|
|
19
|
+
default:
|
|
20
|
+
return grade;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function severityIcon(severity) {
|
|
24
|
+
switch (severity) {
|
|
25
|
+
case "critical":
|
|
26
|
+
case "high":
|
|
27
|
+
case "medium":
|
|
28
|
+
return chalk.red("✗");
|
|
29
|
+
case "low":
|
|
30
|
+
return chalk.yellow("!");
|
|
31
|
+
case "info":
|
|
32
|
+
return chalk.green("✓");
|
|
33
|
+
default:
|
|
34
|
+
return " ";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function severityLabel(severity) {
|
|
38
|
+
const upper = severity.toUpperCase();
|
|
39
|
+
switch (severity) {
|
|
40
|
+
case "critical":
|
|
41
|
+
return chalk.bgRed.white(` ${upper} `);
|
|
42
|
+
case "high":
|
|
43
|
+
return chalk.red(`[${upper}]`);
|
|
44
|
+
case "medium":
|
|
45
|
+
return chalk.yellow(`[${upper}]`);
|
|
46
|
+
case "low":
|
|
47
|
+
return chalk.cyan(`[${upper}]`);
|
|
48
|
+
case "info":
|
|
49
|
+
return chalk.gray(`[${upper}]`);
|
|
50
|
+
default:
|
|
51
|
+
return `[${upper}]`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function formatSummary(result, threshold) {
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push(chalk.bold(" middleBrick API Security Scan"));
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push(` URL: ${result.apiUrl}`);
|
|
60
|
+
lines.push(` Method: ${result.method}`);
|
|
61
|
+
if (result.status === "failed") {
|
|
62
|
+
lines.push("");
|
|
63
|
+
lines.push(chalk.red(` Scan failed: ${result.errorMessage || "Unknown error"}`));
|
|
64
|
+
lines.push("");
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
if (result.overall !== undefined && result.letterGrade) {
|
|
68
|
+
lines.push(` Score: ${result.overall}/100 (${gradeColor(result.letterGrade)})`);
|
|
69
|
+
}
|
|
70
|
+
if (result.categories && Object.keys(result.categories).length > 0) {
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push(chalk.bold(" Categories:"));
|
|
73
|
+
for (const [name, score] of Object.entries(result.categories)) {
|
|
74
|
+
const label = name.padEnd(20);
|
|
75
|
+
lines.push(` ${label} ${scoreBar(score)} ${score}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const findings = result.findings || [];
|
|
79
|
+
if (findings.length > 0) {
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push(chalk.bold(` Findings (${findings.length}):`));
|
|
82
|
+
for (const f of findings) {
|
|
83
|
+
lines.push(` ${severityIcon(f.severity)} ${severityLabel(f.severity)} ${f.title}`);
|
|
84
|
+
lines.push(` ${chalk.gray(f.description)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (threshold !== undefined && result.overall !== undefined) {
|
|
88
|
+
lines.push("");
|
|
89
|
+
const passed = result.overall >= threshold;
|
|
90
|
+
const status = passed
|
|
91
|
+
? chalk.green("PASS ✓")
|
|
92
|
+
: chalk.red("FAIL ✗");
|
|
93
|
+
lines.push(` Threshold: ${threshold} → ${status}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push("");
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function formatTable(result) {
|
|
2
|
+
const findings = result.findings || [];
|
|
3
|
+
if (findings.length === 0) {
|
|
4
|
+
return "No findings.";
|
|
5
|
+
}
|
|
6
|
+
const header = "SEVERITY\tCATEGORY\tTITLE";
|
|
7
|
+
const rows = findings.map((f) => `${f.severity.toUpperCase()}\t${f.category}\t${f.title}`);
|
|
8
|
+
return [header, ...rows].join("\n");
|
|
9
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { scanCommand } from "./commands/scan.js";
|
|
7
|
+
import { resultsCommand } from "./commands/results.js";
|
|
8
|
+
import { configureCommand } from "./commands/configure.js";
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
11
|
+
const program = new Command()
|
|
12
|
+
.name("middlebrick")
|
|
13
|
+
.description("middleBrick — API security scanning from your terminal")
|
|
14
|
+
.version(pkg.version)
|
|
15
|
+
.option("--api-key <key>", "API key (overrides env/config)")
|
|
16
|
+
.option("--base-url <url>", "API base URL override");
|
|
17
|
+
program.addCommand(scanCommand);
|
|
18
|
+
program.addCommand(resultsCommand);
|
|
19
|
+
program.addCommand(configureCommand);
|
|
20
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "middlebrick",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "middleBrick CLI — API security scanning from your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"middlebrick": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": ["dist"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@middlebrick/api-client": "^0.1.0",
|
|
17
|
+
"chalk": "^5.4.1",
|
|
18
|
+
"commander": "^13.1.0"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
}
|
|
24
|
+
}
|