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 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
+ }
@@ -0,0 +1,2 @@
1
+ export declare const API_BASE: string;
2
+ export declare const VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ export const API_BASE = process.env.FETCHSANDBOX_API_URL ?? "https://fetchsandbox.com";
2
+ export const VERSION = "0.1.0";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }>;
@@ -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
+ }