fetchsandbox 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 +87 -0
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.js +55 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +31 -0
- package/dist/commands/reset.d.ts +1 -0
- package/dist/commands/reset.js +21 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +54 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +50 -0
- package/dist/lib/api.d.ts +47 -0
- package/dist/lib/api.js +73 -0
- package/dist/lib/output.d.ts +14 -0
- package/dist/lib/output.js +78 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# fetchsandbox
|
|
2
|
+
|
|
3
|
+
Turn any OpenAPI spec into a live developer portal with a stateful sandbox.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g fetchsandbox
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx fetchsandbox generate ./openapi.yaml
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `generate <spec>`
|
|
20
|
+
|
|
21
|
+
Create a portal from an OpenAPI spec file or URL.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# From a local file
|
|
25
|
+
fetchsandbox generate ./stripe-openapi.yaml
|
|
26
|
+
|
|
27
|
+
# From a URL
|
|
28
|
+
fetchsandbox generate https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yaml
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Output:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Spec loaded: Stripe API v2024-06-20 (327 endpoints)
|
|
35
|
+
Sandbox created: beed86d499
|
|
36
|
+
Seed data: 63 resources across 21 types
|
|
37
|
+
|
|
38
|
+
Your sandbox is ready:
|
|
39
|
+
|
|
40
|
+
API Key sandbox_3a4f93a4ea3ea857abb88deea90c3fcb
|
|
41
|
+
Base URL https://stripe.fetchsandbox.com
|
|
42
|
+
Portal https://fetchsandbox.com/docs/stripe
|
|
43
|
+
|
|
44
|
+
Try it:
|
|
45
|
+
curl https://stripe.fetchsandbox.com/v1/customers \
|
|
46
|
+
-H "api-key: sandbox_3a4f93a4ea3ea857abb88deea90c3fcb"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `status <sandbox-id>`
|
|
50
|
+
|
|
51
|
+
Show sandbox state, resources, and recent activity.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
fetchsandbox status stripe
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `reset <sandbox-id>`
|
|
58
|
+
|
|
59
|
+
Reset sandbox to its original seed data.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
fetchsandbox reset stripe
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `list`
|
|
66
|
+
|
|
67
|
+
List all sandboxes.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
fetchsandbox list
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Environment
|
|
74
|
+
|
|
75
|
+
| Variable | Default | Description |
|
|
76
|
+
|----------|---------|-------------|
|
|
77
|
+
| `FETCHSANDBOX_API_URL` | `https://fetchsandbox.com` | API base URL |
|
|
78
|
+
|
|
79
|
+
## Learn more
|
|
80
|
+
|
|
81
|
+
- [fetchsandbox.com](https://fetchsandbox.com) - Generate a portal from any OpenAPI spec
|
|
82
|
+
- [Documentation](https://fetchsandbox.com/docs/stripe) - Example: Stripe API portal
|
|
83
|
+
- [GitHub](https://github.com/fetchsandbox) - Source and showcases
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generate(specInput: string): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { uploadSpec, createSandbox } from "../lib/api.js";
|
|
3
|
+
import { ok, fail, label, blank, heading, code, friendlyError } from "../lib/output.js";
|
|
4
|
+
import { API_BASE } from "../constants.js";
|
|
5
|
+
export async function generate(specInput) {
|
|
6
|
+
blank();
|
|
7
|
+
const spinner = ora({ text: "Loading spec...", indent: 2 }).start();
|
|
8
|
+
try {
|
|
9
|
+
// Step 1: Upload spec
|
|
10
|
+
const spec = await uploadSpec(specInput);
|
|
11
|
+
spinner.stop();
|
|
12
|
+
ok(`Spec loaded: ${spec.name} v${spec.version} (${spec.endpoints_count} endpoints)`);
|
|
13
|
+
// Step 2: Create sandbox
|
|
14
|
+
const spinner2 = ora({ text: "Creating sandbox...", indent: 2 }).start();
|
|
15
|
+
const sb = await createSandbox(spec.id);
|
|
16
|
+
spinner2.stop();
|
|
17
|
+
ok(`Sandbox created: ${sb.id}`);
|
|
18
|
+
// Step 3: Show resource stats
|
|
19
|
+
const totalResources = Object.values(sb.resource_stats).reduce((s, c) => s + c, 0);
|
|
20
|
+
const typeCount = Object.values(sb.resource_stats).filter(c => c > 0).length;
|
|
21
|
+
if (totalResources > 0) {
|
|
22
|
+
ok(`Seed data: ${totalResources} resources across ${typeCount} types`);
|
|
23
|
+
}
|
|
24
|
+
// Step 4: Show results
|
|
25
|
+
blank();
|
|
26
|
+
heading("Your sandbox is ready:");
|
|
27
|
+
blank();
|
|
28
|
+
const apiKey = sb.credentials?.[0]?.api_key || "N/A";
|
|
29
|
+
const slug = sb.slug || sb.spec_id;
|
|
30
|
+
const portalUrl = `${API_BASE}/docs/${slug}`;
|
|
31
|
+
const subdomainBase = sb.slug ? `https://${sb.slug}.fetchsandbox.com` : `${API_BASE}/sandbox/${sb.id}`;
|
|
32
|
+
label("API Key", apiKey);
|
|
33
|
+
label("Base URL", subdomainBase);
|
|
34
|
+
label("Portal", portalUrl);
|
|
35
|
+
// Step 5: Show curl example
|
|
36
|
+
blank();
|
|
37
|
+
heading("Try it:");
|
|
38
|
+
code([
|
|
39
|
+
`curl ${subdomainBase}/v1 \\`,
|
|
40
|
+
` -H "api-key: ${apiKey}"`,
|
|
41
|
+
]);
|
|
42
|
+
blank();
|
|
43
|
+
heading("Next steps:");
|
|
44
|
+
label("Status", `fetchsandbox status ${slug}`);
|
|
45
|
+
label("Reset", `fetchsandbox reset ${slug}`);
|
|
46
|
+
label("Portal", portalUrl);
|
|
47
|
+
blank();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
spinner.stop();
|
|
51
|
+
fail(friendlyError(error));
|
|
52
|
+
blank();
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function list(): Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { listSandboxes } from "../lib/api.js";
|
|
3
|
+
import { fail, blank, tableHeader, row, friendlyError } from "../lib/output.js";
|
|
4
|
+
export async function list() {
|
|
5
|
+
blank();
|
|
6
|
+
try {
|
|
7
|
+
const sandboxes = await listSandboxes();
|
|
8
|
+
if (sandboxes.length === 0) {
|
|
9
|
+
console.log(` ${pc.dim("No sandboxes found. Run")} fetchsandbox generate <spec> ${pc.dim("to create one.")}`);
|
|
10
|
+
blank();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const widths = [13, 30, 10, 10];
|
|
14
|
+
tableHeader(["ID", "Name", "Status", "Endpoints"], widths);
|
|
15
|
+
for (const sb of sandboxes) {
|
|
16
|
+
const dot = sb.status === "running" ? pc.green("●") : pc.yellow("●");
|
|
17
|
+
row([
|
|
18
|
+
sb.id,
|
|
19
|
+
(sb.spec_name || sb.name).slice(0, 28),
|
|
20
|
+
`${dot} ${sb.status}`,
|
|
21
|
+
String(sb.endpoints_count),
|
|
22
|
+
], widths);
|
|
23
|
+
}
|
|
24
|
+
blank();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
fail(friendlyError(error));
|
|
28
|
+
blank();
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reset(sandboxId: string): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { resetSandbox } from "../lib/api.js";
|
|
3
|
+
import { ok, fail, blank, friendlyError } from "../lib/output.js";
|
|
4
|
+
export async function reset(sandboxId) {
|
|
5
|
+
blank();
|
|
6
|
+
const spinner = ora({ text: "Resetting sandbox...", indent: 2 }).start();
|
|
7
|
+
try {
|
|
8
|
+
const result = await resetSandbox(sandboxId);
|
|
9
|
+
spinner.stop();
|
|
10
|
+
const total = Object.values(result.resource_stats).reduce((s, c) => s + c, 0);
|
|
11
|
+
const types = Object.values(result.resource_stats).filter(c => c > 0).length;
|
|
12
|
+
ok(`Sandbox reset: ${total} resources restored across ${types} types`);
|
|
13
|
+
blank();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
spinner.stop();
|
|
17
|
+
fail(friendlyError(error));
|
|
18
|
+
blank();
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function showStatus(sandboxId: string): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { getSandbox, getSandboxState, getSandboxLogs, validateSandbox } from "../lib/api.js";
|
|
3
|
+
import { fail, blank, heading, label, method, status, timeAgo, friendlyError } from "../lib/output.js";
|
|
4
|
+
export async function showStatus(sandboxId) {
|
|
5
|
+
blank();
|
|
6
|
+
try {
|
|
7
|
+
const [sb, state, logs, val] = await Promise.all([
|
|
8
|
+
getSandbox(sandboxId),
|
|
9
|
+
getSandboxState(sandboxId).catch(() => null),
|
|
10
|
+
getSandboxLogs(sandboxId, 10).catch(() => []),
|
|
11
|
+
validateSandbox(sandboxId).catch(() => null),
|
|
12
|
+
]);
|
|
13
|
+
// Header
|
|
14
|
+
const dot = sb.status === "running" ? pc.green("●") : pc.yellow("●");
|
|
15
|
+
const health = val ? `${Math.round(val.pass_rate)}% healthy` : "";
|
|
16
|
+
console.log(` ${pc.bold(sb.spec_name || sb.name)} ${dot} ${sb.status} ${pc.dim(health)}`);
|
|
17
|
+
blank();
|
|
18
|
+
// Resources
|
|
19
|
+
const resources = state
|
|
20
|
+
? Object.entries(state).filter(([, v]) => Array.isArray(v) && v.length > 0)
|
|
21
|
+
: Object.entries(sb.resource_stats).filter(([, c]) => c > 0).map(([k, c]) => [k, Array(c).fill(null)]);
|
|
22
|
+
if (resources.length > 0) {
|
|
23
|
+
heading("Resources:");
|
|
24
|
+
for (const [type, items] of resources) {
|
|
25
|
+
const count = Array.isArray(items) ? items.length : 0;
|
|
26
|
+
console.log(` ${pc.dim(String(type).padEnd(25))}${pc.bold(String(count))}`);
|
|
27
|
+
}
|
|
28
|
+
blank();
|
|
29
|
+
}
|
|
30
|
+
// Recent activity
|
|
31
|
+
if (logs.length > 0) {
|
|
32
|
+
heading("Recent activity:");
|
|
33
|
+
for (const log of logs.slice(0, 8)) {
|
|
34
|
+
const time = timeAgo(log.timestamp).padEnd(8);
|
|
35
|
+
const m = method(log.method);
|
|
36
|
+
const path = log.path.length > 35 ? log.path.slice(0, 35) + "…" : log.path;
|
|
37
|
+
console.log(` ${pc.dim(time)}${m} ${path.padEnd(36)} ${status(log.response_status)} ${pc.dim(log.duration_ms + "ms")}`);
|
|
38
|
+
}
|
|
39
|
+
blank();
|
|
40
|
+
}
|
|
41
|
+
// Credentials
|
|
42
|
+
if (sb.credentials?.[0]) {
|
|
43
|
+
heading("Credentials:");
|
|
44
|
+
label("API Key", sb.credentials[0].api_key);
|
|
45
|
+
label("Sandbox ID", sb.id);
|
|
46
|
+
blank();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
fail(friendlyError(error));
|
|
51
|
+
blank();
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { VERSION } from "./constants.js";
|
|
5
|
+
import { generate } from "./commands/generate.js";
|
|
6
|
+
import { showStatus } from "./commands/status.js";
|
|
7
|
+
import { reset } from "./commands/reset.js";
|
|
8
|
+
import { list } from "./commands/list.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("fetchsandbox")
|
|
12
|
+
.description(pc.dim("Turn any OpenAPI spec into a live developer portal"))
|
|
13
|
+
.version(VERSION, "-v, --version");
|
|
14
|
+
program
|
|
15
|
+
.command("generate <spec>")
|
|
16
|
+
.description("Create a portal from an OpenAPI spec file or URL")
|
|
17
|
+
.action(generate);
|
|
18
|
+
program
|
|
19
|
+
.command("status <sandbox-id>")
|
|
20
|
+
.description("Show sandbox state, resources, and recent activity")
|
|
21
|
+
.action(showStatus);
|
|
22
|
+
program
|
|
23
|
+
.command("reset <sandbox-id>")
|
|
24
|
+
.description("Reset sandbox to its original seed data")
|
|
25
|
+
.action(reset);
|
|
26
|
+
program
|
|
27
|
+
.command("list")
|
|
28
|
+
.description("List all sandboxes")
|
|
29
|
+
.action(list);
|
|
30
|
+
// If no command given, show help with a friendly message
|
|
31
|
+
if (process.argv.length <= 2) {
|
|
32
|
+
console.log();
|
|
33
|
+
console.log(` ${pc.bold("fetchsandbox")} ${pc.dim(`v${VERSION}`)}`);
|
|
34
|
+
console.log(` ${pc.dim("Turn any OpenAPI spec into a live developer portal")}`);
|
|
35
|
+
console.log();
|
|
36
|
+
console.log(` ${pc.dim("Quick start:")}`);
|
|
37
|
+
console.log(` ${pc.cyan("fetchsandbox generate ./openapi.yaml")}`);
|
|
38
|
+
console.log(` ${pc.cyan("fetchsandbox generate https://api.example.com/openapi.yaml")}`);
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(` ${pc.dim("Commands:")}`);
|
|
41
|
+
console.log(` ${pc.white("generate <spec>")} Create a portal from a spec file or URL`);
|
|
42
|
+
console.log(` ${pc.white("status <id>")} Show sandbox state and recent activity`);
|
|
43
|
+
console.log(` ${pc.white("reset <id>")} Reset sandbox to seed data`);
|
|
44
|
+
console.log(` ${pc.white("list")} List all sandboxes`);
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(` ${pc.dim("Learn more:")} ${pc.cyan("https://fetchsandbox.com")}`);
|
|
47
|
+
console.log();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
program.parse();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface SpecResult {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
endpoints_count: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SandboxResult {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
status: string;
|
|
11
|
+
spec_id: string;
|
|
12
|
+
spec_name: string;
|
|
13
|
+
slug?: string;
|
|
14
|
+
base_url: string;
|
|
15
|
+
active_scenario: string;
|
|
16
|
+
endpoints_count: number;
|
|
17
|
+
resource_stats: Record<string, number>;
|
|
18
|
+
credentials: Array<{
|
|
19
|
+
api_key: string;
|
|
20
|
+
api_secret: string;
|
|
21
|
+
}>;
|
|
22
|
+
created_at: string;
|
|
23
|
+
}
|
|
24
|
+
export interface LogEntry {
|
|
25
|
+
id: string;
|
|
26
|
+
method: string;
|
|
27
|
+
path: string;
|
|
28
|
+
response_status: number;
|
|
29
|
+
duration_ms: number;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function uploadSpec(input: string): Promise<SpecResult>;
|
|
33
|
+
export declare function createSandbox(specId: string, name?: string): Promise<SandboxResult>;
|
|
34
|
+
export declare function getSandbox(id: string): Promise<SandboxResult>;
|
|
35
|
+
export declare function listSandboxes(): Promise<SandboxResult[]>;
|
|
36
|
+
export declare function getSandboxState(id: string): Promise<Record<string, unknown[]>>;
|
|
37
|
+
export declare function getSandboxLogs(id: string, limit?: number): Promise<LogEntry[]>;
|
|
38
|
+
export declare function resetSandbox(id: string): Promise<{
|
|
39
|
+
status: string;
|
|
40
|
+
resource_stats: Record<string, number>;
|
|
41
|
+
}>;
|
|
42
|
+
export declare function validateSandbox(id: string): Promise<{
|
|
43
|
+
total: number;
|
|
44
|
+
passed: number;
|
|
45
|
+
failed: number;
|
|
46
|
+
pass_rate: number;
|
|
47
|
+
}>;
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { API_BASE } from "../constants.js";
|
|
4
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
5
|
+
async function request(path, options) {
|
|
6
|
+
const url = `${API_BASE}${path}`;
|
|
7
|
+
const res = await fetch(url, options);
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
const body = await res.text().catch(() => "");
|
|
10
|
+
throw new Error(`${res.status}: ${body.slice(0, 200)}`);
|
|
11
|
+
}
|
|
12
|
+
if (res.status === 204)
|
|
13
|
+
return undefined;
|
|
14
|
+
return res.json();
|
|
15
|
+
}
|
|
16
|
+
function isUrl(input) {
|
|
17
|
+
return input.startsWith("http://") || input.startsWith("https://");
|
|
18
|
+
}
|
|
19
|
+
function githubBlobToRaw(url) {
|
|
20
|
+
// Convert GitHub blob URLs to raw content URLs
|
|
21
|
+
return url
|
|
22
|
+
.replace("github.com", "raw.githubusercontent.com")
|
|
23
|
+
.replace("/blob/", "/");
|
|
24
|
+
}
|
|
25
|
+
// ── API Functions ──────────────────────────────────────────────────────
|
|
26
|
+
export async function uploadSpec(input) {
|
|
27
|
+
let content;
|
|
28
|
+
let filename;
|
|
29
|
+
if (isUrl(input)) {
|
|
30
|
+
const url = input.includes("github.com") && input.includes("/blob/")
|
|
31
|
+
? githubBlobToRaw(input)
|
|
32
|
+
: input;
|
|
33
|
+
const res = await fetch(url);
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Error(`Failed to fetch spec from URL: ${res.status}`);
|
|
36
|
+
content = await res.text();
|
|
37
|
+
filename = basename(new URL(url).pathname) || "openapi.yaml";
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
content = readFileSync(input, "utf-8");
|
|
41
|
+
filename = basename(input);
|
|
42
|
+
}
|
|
43
|
+
const name = filename.replace(/\.(yaml|yml|json)$/i, "");
|
|
44
|
+
const form = new FormData();
|
|
45
|
+
form.append("name", name);
|
|
46
|
+
form.append("spec_content", new Blob([content], { type: "application/x-yaml" }), filename);
|
|
47
|
+
return request("/api/specs", { method: "POST", body: form });
|
|
48
|
+
}
|
|
49
|
+
export async function createSandbox(specId, name) {
|
|
50
|
+
return request("/api/sandboxes", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ spec_id: specId, name: name || "", scenario: "default" }),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function getSandbox(id) {
|
|
57
|
+
return request(`/api/sandboxes/${id}`);
|
|
58
|
+
}
|
|
59
|
+
export async function listSandboxes() {
|
|
60
|
+
return request("/api/sandboxes");
|
|
61
|
+
}
|
|
62
|
+
export async function getSandboxState(id) {
|
|
63
|
+
return request(`/api/sandboxes/${id}/state`);
|
|
64
|
+
}
|
|
65
|
+
export async function getSandboxLogs(id, limit = 10) {
|
|
66
|
+
return request(`/api/sandboxes/${id}/logs?limit=${limit}`);
|
|
67
|
+
}
|
|
68
|
+
export async function resetSandbox(id) {
|
|
69
|
+
return request(`/api/sandboxes/${id}/reset`, { method: "POST" });
|
|
70
|
+
}
|
|
71
|
+
export async function validateSandbox(id) {
|
|
72
|
+
return request(`/api/sandboxes/${id}/validate`);
|
|
73
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const ok: (msg: string) => void;
|
|
2
|
+
export declare const fail: (msg: string) => void;
|
|
3
|
+
export declare const warn: (msg: string) => void;
|
|
4
|
+
export declare const info: (msg: string) => void;
|
|
5
|
+
export declare const blank: () => void;
|
|
6
|
+
export declare const label: (key: string, val: string) => void;
|
|
7
|
+
export declare const heading: (msg: string) => void;
|
|
8
|
+
export declare const code: (lines: string[]) => void;
|
|
9
|
+
export declare const row: (cols: string[], widths: number[]) => void;
|
|
10
|
+
export declare const tableHeader: (cols: string[], widths: number[]) => void;
|
|
11
|
+
export declare const method: (m: string) => string;
|
|
12
|
+
export declare const status: (code: number) => string;
|
|
13
|
+
export declare const timeAgo: (ts: string) => string;
|
|
14
|
+
export declare const friendlyError: (error: unknown) => string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
// ── Core output primitives ─────────────────────────────────────────────
|
|
3
|
+
// Every line is indented 2 spaces for visual breathing room.
|
|
4
|
+
// Colors aid comprehension, never decoration.
|
|
5
|
+
export const ok = (msg) => console.log(` ${pc.green("✓")} ${msg}`);
|
|
6
|
+
export const fail = (msg) => console.log(` ${pc.red("✗")} ${msg}`);
|
|
7
|
+
export const warn = (msg) => console.log(` ${pc.yellow("!")} ${msg}`);
|
|
8
|
+
export const info = (msg) => console.log(` ${pc.dim(msg)}`);
|
|
9
|
+
export const blank = () => console.log();
|
|
10
|
+
// Key-value pair with aligned labels
|
|
11
|
+
export const label = (key, val) => console.log(` ${pc.dim(key.padEnd(11))}${val}`);
|
|
12
|
+
// Section header
|
|
13
|
+
export const heading = (msg) => console.log(` ${pc.bold(msg)}`);
|
|
14
|
+
// Code block (for curl examples etc.)
|
|
15
|
+
export const code = (lines) => {
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
console.log(` ${pc.cyan(line)}`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// Table row
|
|
21
|
+
export const row = (cols, widths) => {
|
|
22
|
+
const formatted = cols.map((c, i) => c.padEnd(widths[i] || 15)).join("");
|
|
23
|
+
console.log(` ${formatted}`);
|
|
24
|
+
};
|
|
25
|
+
// Table header
|
|
26
|
+
export const tableHeader = (cols, widths) => {
|
|
27
|
+
const formatted = cols.map((c, i) => pc.dim(c.padEnd(widths[i] || 15))).join("");
|
|
28
|
+
console.log(` ${formatted}`);
|
|
29
|
+
};
|
|
30
|
+
// Method pill (colored like the web UI)
|
|
31
|
+
export const method = (m) => {
|
|
32
|
+
const colors = {
|
|
33
|
+
GET: pc.blue,
|
|
34
|
+
POST: pc.green,
|
|
35
|
+
PUT: pc.yellow,
|
|
36
|
+
PATCH: pc.magenta,
|
|
37
|
+
DELETE: pc.red,
|
|
38
|
+
};
|
|
39
|
+
return (colors[m] || pc.dim)(m.padEnd(6));
|
|
40
|
+
};
|
|
41
|
+
// Status code (colored)
|
|
42
|
+
export const status = (code) => {
|
|
43
|
+
if (code < 300)
|
|
44
|
+
return pc.green(String(code));
|
|
45
|
+
if (code < 500)
|
|
46
|
+
return pc.yellow(String(code));
|
|
47
|
+
return pc.red(String(code));
|
|
48
|
+
};
|
|
49
|
+
// Relative time
|
|
50
|
+
export const timeAgo = (ts) => {
|
|
51
|
+
const diff = Date.now() - new Date(ts).getTime();
|
|
52
|
+
if (diff < 10000)
|
|
53
|
+
return "now";
|
|
54
|
+
if (diff < 60000)
|
|
55
|
+
return `${Math.floor(diff / 1000)}s ago`;
|
|
56
|
+
if (diff < 3600000)
|
|
57
|
+
return `${Math.floor(diff / 60000)}m ago`;
|
|
58
|
+
if (diff < 86400000)
|
|
59
|
+
return `${Math.floor(diff / 3600000)}h ago`;
|
|
60
|
+
return new Date(ts).toLocaleDateString();
|
|
61
|
+
};
|
|
62
|
+
// Friendly error — never show stack traces to users
|
|
63
|
+
export const friendlyError = (error) => {
|
|
64
|
+
if (error instanceof Error) {
|
|
65
|
+
if (error.message.includes("ECONNREFUSED"))
|
|
66
|
+
return "Cannot connect to FetchSandbox API. Is the server running?";
|
|
67
|
+
if (error.message.includes("ENOTFOUND"))
|
|
68
|
+
return "Cannot resolve FetchSandbox API hostname. Check your internet connection.";
|
|
69
|
+
if (error.message.includes("401"))
|
|
70
|
+
return "Authentication failed. Check your API key.";
|
|
71
|
+
if (error.message.includes("404"))
|
|
72
|
+
return "Not found. Check the sandbox ID.";
|
|
73
|
+
if (error.message.includes("413"))
|
|
74
|
+
return "Spec file too large. Maximum size is 5 MB.";
|
|
75
|
+
return error.message;
|
|
76
|
+
}
|
|
77
|
+
return String(error);
|
|
78
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fetchsandbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn any OpenAPI spec into a live developer portal with a stateful sandbox",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fetchsandbox": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/fetchsandbox/cli"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://fetchsandbox.com",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["openapi", "sandbox", "api", "developer-portal", "mock", "testing", "api-testing", "mock-server", "openapi-tools"],
|
|
24
|
+
"author": "FetchSandbox",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^12.0.0",
|
|
31
|
+
"ora": "^8.0.0",
|
|
32
|
+
"picocolors": "^1.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"typescript": "^5.6.0"
|
|
37
|
+
}
|
|
38
|
+
}
|