fetchsandbox 0.1.0 → 0.2.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.
@@ -0,0 +1 @@
1
+ export declare function check(sandboxId: string): Promise<void>;
@@ -0,0 +1,102 @@
1
+ import pc from "picocolors";
2
+ import ora from "ora";
3
+ import { getSandbox, getSandboxLogs, getWorkflows, getWebhooks, validateSandbox, } from "../lib/api.js";
4
+ import { fail, blank, heading, friendlyError } from "../lib/output.js";
5
+ export async function check(sandboxId) {
6
+ blank();
7
+ const spinner = ora({ text: "Running integration readiness check...", indent: 2 }).start();
8
+ try {
9
+ const sb = await getSandbox(sandboxId);
10
+ // Fetch all data in parallel
11
+ const [validation, logs, workflowData, webhooks] = await Promise.all([
12
+ validateSandbox(sandboxId).catch(() => null),
13
+ getSandboxLogs(sandboxId, 200).catch(() => []),
14
+ getWorkflows(sb.spec_id).catch(() => ({ workflows: [] })),
15
+ getWebhooks(sandboxId).catch(() => []),
16
+ ]);
17
+ spinner.stop();
18
+ heading(`Integration Readiness: ${sb.spec_name || sb.name}`);
19
+ blank();
20
+ // ── 1. Endpoint Health (30%) ──────────────────────────────────
21
+ const healthScore = validation ? validation.pass_rate / 100 : 0;
22
+ const healthPct = Math.round(healthScore * 100);
23
+ const healthIcon = healthPct >= 90 ? pc.green("✓") : healthPct >= 60 ? pc.yellow("⚠") : pc.red("✗");
24
+ console.log(` ${healthIcon} Endpoint health: ${healthPct >= 90 ? pc.green(healthPct + "%") : healthPct >= 60 ? pc.yellow(healthPct + "%") : pc.red(healthPct + "%")}${validation ? pc.dim(` (${validation.passed}/${validation.total} passed)`) : pc.dim(" (unable to validate)")}`);
25
+ // ── 2. Endpoint Coverage (30%) ────────────────────────────────
26
+ const uniqueEndpointsCalled = new Set(logs.map((l) => `${l.method} ${l.matched_path_template || l.path}`));
27
+ const totalEndpoints = sb.endpoints_count || 1;
28
+ const coverageRatio = Math.min(uniqueEndpointsCalled.size / totalEndpoints, 1);
29
+ const coveragePct = Math.round(coverageRatio * 100);
30
+ const coverageIcon = coveragePct >= 25 ? pc.green("✓") : coveragePct >= 10 ? pc.yellow("⚠") : pc.red("✗");
31
+ console.log(` ${coverageIcon} Endpoints exercised: ${coveragePct >= 25 ? pc.green(coveragePct + "%") : coveragePct >= 10 ? pc.yellow(coveragePct + "%") : pc.red(coveragePct + "%")}${pc.dim(` (${uniqueEndpointsCalled.size}/${totalEndpoints} unique endpoints called)`)}`);
32
+ // ── 3. Workflow Completion (25%) ──────────────────────────────
33
+ const wfs = workflowData.workflows || [];
34
+ let workflowScore = 0;
35
+ let completedWorkflows = 0;
36
+ const incompleteWorkflows = [];
37
+ if (wfs.length > 0) {
38
+ for (const wf of wfs) {
39
+ let stepsMatched = 0;
40
+ for (const step of wf.steps) {
41
+ const found = logs.some((l) => l.method === step.method && (l.matched_path_template === step.path || l.path.includes(step.path.replace(/\{[^}]+\}/g, ""))));
42
+ if (found)
43
+ stepsMatched++;
44
+ }
45
+ if (stepsMatched === wf.steps.length)
46
+ completedWorkflows++;
47
+ else if (stepsMatched > 0)
48
+ incompleteWorkflows.push({ name: wf.name, completed: stepsMatched, total: wf.steps.length });
49
+ }
50
+ workflowScore = completedWorkflows / wfs.length;
51
+ }
52
+ const wfPct = Math.round(workflowScore * 100);
53
+ const wfIcon = completedWorkflows > 0 ? pc.green("✓") : wfs.length > 0 ? pc.red("✗") : pc.dim("—");
54
+ console.log(` ${wfIcon} Workflows completed: ${completedWorkflows > 0 ? pc.green(`${completedWorkflows}/${wfs.length}`) : wfs.length > 0 ? pc.red(`${completedWorkflows}/${wfs.length}`) : pc.dim("none discovered")}`);
55
+ for (const inc of incompleteWorkflows.slice(0, 3)) {
56
+ console.log(` ${pc.dim("⤷")} ${inc.name}: ${pc.yellow(`${inc.completed}/${inc.total} steps`)}`);
57
+ }
58
+ // ── 4. Webhook Readiness (15%) ────────────────────────────────
59
+ const webhookScore = webhooks.length > 0 ? 1 : 0;
60
+ const whIcon = webhookScore > 0 ? pc.green("✓") : pc.yellow("⚠");
61
+ console.log(` ${whIcon} Webhook handler: ${webhookScore > 0 ? pc.green(`${webhooks.length} registered`) : pc.yellow("not registered")}`);
62
+ // ── Overall Score ─────────────────────────────────────────────
63
+ const overall = Math.round(healthScore * 30 +
64
+ coverageRatio * 30 +
65
+ workflowScore * 25 +
66
+ webhookScore * 15);
67
+ blank();
68
+ const overallColor = overall >= 80 ? pc.green : overall >= 50 ? pc.yellow : pc.red;
69
+ console.log(` ${pc.bold("Overall:")} ${overallColor(pc.bold(`${overall}%`))}`);
70
+ // ── Next Steps ────────────────────────────────────────────────
71
+ const nextSteps = [];
72
+ if (coveragePct < 10) {
73
+ nextSteps.push(`Try an API call: ${pc.cyan(`fetchsandbox request ${sandboxId} GET /v1`)}`);
74
+ }
75
+ if (wfs.length > 0 && completedWorkflows === 0) {
76
+ nextSteps.push(`Run a workflow: ${pc.cyan(`fetchsandbox workflows ${sandboxId}`)}`);
77
+ }
78
+ if (webhookScore === 0) {
79
+ nextSteps.push(`Register a webhook in the sandbox dashboard`);
80
+ }
81
+ if (healthPct < 90 && healthPct > 0) {
82
+ nextSteps.push(`Check failing endpoints: ${pc.cyan(`fetchsandbox endpoints ${sandboxId}`)}`);
83
+ }
84
+ if (sb.active_scenario === "default") {
85
+ nextSteps.push(`Test error scenarios: ${pc.cyan(`fetchsandbox scenario ${sandboxId} auth-failure`)}`);
86
+ }
87
+ if (nextSteps.length > 0) {
88
+ blank();
89
+ heading("Next steps:");
90
+ for (let i = 0; i < nextSteps.length; i++) {
91
+ console.log(` ${pc.dim(`${i + 1}.`)} ${nextSteps[i]}`);
92
+ }
93
+ }
94
+ blank();
95
+ }
96
+ catch (error) {
97
+ spinner.stop();
98
+ fail(friendlyError(error));
99
+ blank();
100
+ process.exit(1);
101
+ }
102
+ }
@@ -0,0 +1,3 @@
1
+ export declare function deleteCmd(sandboxId: string, options: {
2
+ force?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import pc from "picocolors";
2
+ import { getSandbox, deleteSandbox } from "../lib/api.js";
3
+ import { ok, fail, blank, friendlyError } from "../lib/output.js";
4
+ import { createInterface } from "node:readline";
5
+ export async function deleteCmd(sandboxId, options) {
6
+ blank();
7
+ try {
8
+ const sb = await getSandbox(sandboxId);
9
+ if (!options.force) {
10
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
11
+ const answer = await new Promise((resolve) => {
12
+ rl.question(` ${pc.yellow("?")} Delete "${sb.spec_name || sb.name}" (${sb.id})? [y/N] `, resolve);
13
+ });
14
+ rl.close();
15
+ if (answer.toLowerCase() !== "y") {
16
+ console.log(` ${pc.dim("Cancelled.")}`);
17
+ blank();
18
+ return;
19
+ }
20
+ }
21
+ await deleteSandbox(sandboxId);
22
+ ok(`Deleted sandbox "${sb.spec_name || sb.name}" (${sb.id})`);
23
+ blank();
24
+ }
25
+ catch (error) {
26
+ fail(friendlyError(error));
27
+ blank();
28
+ process.exit(1);
29
+ }
30
+ }
@@ -0,0 +1 @@
1
+ export declare function docs(sandboxId: string): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { getSandbox } from "../lib/api.js";
2
+ import { ok, fail, blank, friendlyError } from "../lib/output.js";
3
+ import { API_BASE } from "../constants.js";
4
+ import { exec } from "node:child_process";
5
+ export async function docs(sandboxId) {
6
+ blank();
7
+ try {
8
+ const sb = await getSandbox(sandboxId);
9
+ const slug = sb.slug || sb.spec_id;
10
+ const url = `${API_BASE}/docs/${slug}`;
11
+ // Open in default browser
12
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
13
+ exec(`${cmd} ${url}`);
14
+ ok(`Opening ${url}`);
15
+ blank();
16
+ }
17
+ catch (error) {
18
+ fail(friendlyError(error));
19
+ blank();
20
+ process.exit(1);
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ export declare function endpoints(sandboxId: string, options: {
2
+ search?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import pc from "picocolors";
2
+ import { getEndpoints } from "../lib/api.js";
3
+ import { fail, blank, heading, method as methodColor, tableHeader, row, friendlyError } from "../lib/output.js";
4
+ export async function endpoints(sandboxId, options) {
5
+ blank();
6
+ try {
7
+ let eps = await getEndpoints(sandboxId);
8
+ if (options.search) {
9
+ const term = options.search.toLowerCase();
10
+ eps = eps.filter((e) => e.path.toLowerCase().includes(term) ||
11
+ e.summary?.toLowerCase().includes(term) ||
12
+ e.operation_id?.toLowerCase().includes(term) ||
13
+ e.tags?.some((t) => t.toLowerCase().includes(term)));
14
+ }
15
+ if (eps.length === 0) {
16
+ console.log(` ${pc.dim(options.search ? `No endpoints matching "${options.search}".` : "No endpoints found.")}`);
17
+ blank();
18
+ return;
19
+ }
20
+ heading(`${eps.length} endpoints${options.search ? ` matching "${options.search}"` : ""}:`);
21
+ blank();
22
+ const widths = [8, 42, 40];
23
+ tableHeader(["Method", "Path", "Summary"], widths);
24
+ for (const ep of eps) {
25
+ const m = methodColor(ep.method);
26
+ const summary = (ep.summary || "").slice(0, 38);
27
+ row([m, ep.path.slice(0, 40), pc.dim(summary)], widths);
28
+ }
29
+ blank();
30
+ console.log(` ${pc.dim("Try one:")} ${pc.cyan(`fetchsandbox request ${sandboxId} GET ${eps[0]?.path || "/v1"}`)}`);
31
+ blank();
32
+ }
33
+ catch (error) {
34
+ fail(friendlyError(error));
35
+ blank();
36
+ process.exit(1);
37
+ }
38
+ }
@@ -0,0 +1,5 @@
1
+ export declare function logs(sandboxId: string, options: {
2
+ follow?: boolean;
3
+ status?: string;
4
+ limit?: string;
5
+ }): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import pc from "picocolors";
2
+ import { getSandboxLogs } from "../lib/api.js";
3
+ import { fail, blank, heading, method as methodColor, status as statusColor, timeAgo, friendlyError } from "../lib/output.js";
4
+ export async function logs(sandboxId, options) {
5
+ blank();
6
+ try {
7
+ const limit = options.limit ? parseInt(options.limit, 10) : 30;
8
+ const statusFilter = options.status ? parseInt(options.status, 10) : 0;
9
+ const printLogs = (entries) => {
10
+ for (const log of entries) {
11
+ if (statusFilter) {
12
+ // Support range filters: 4xx matches 400-499, 5xx matches 500-599
13
+ if (String(statusFilter).includes("x")) {
14
+ const base = parseInt(String(statusFilter).replace(/x/gi, "0"), 10);
15
+ if (log.response_status < base || log.response_status >= base + 100)
16
+ continue;
17
+ }
18
+ else if (log.response_status !== statusFilter) {
19
+ continue;
20
+ }
21
+ }
22
+ const time = timeAgo(log.timestamp).padEnd(8);
23
+ const m = methodColor(log.method);
24
+ const path = log.path.length > 40 ? log.path.slice(0, 37) + "..." : log.path;
25
+ console.log(` ${pc.dim(time)}${m} ${path.padEnd(41)} ${statusColor(log.response_status)} ${pc.dim(log.duration_ms + "ms")}`);
26
+ }
27
+ };
28
+ if (options.follow) {
29
+ heading("Tailing logs (Ctrl+C to stop):");
30
+ blank();
31
+ let lastSeen = "";
32
+ const poll = async () => {
33
+ const entries = await getSandboxLogs(sandboxId, 20);
34
+ const newEntries = lastSeen
35
+ ? entries.filter((e) => e.timestamp > lastSeen)
36
+ : entries;
37
+ if (newEntries.length > 0) {
38
+ printLogs(newEntries);
39
+ lastSeen = newEntries[0].timestamp; // logs are newest-first
40
+ }
41
+ };
42
+ await poll();
43
+ const interval = setInterval(poll, 2000);
44
+ process.on("SIGINT", () => { clearInterval(interval); blank(); process.exit(0); });
45
+ // Keep alive
46
+ await new Promise(() => { });
47
+ }
48
+ else {
49
+ const entries = await getSandboxLogs(sandboxId, limit);
50
+ if (entries.length === 0) {
51
+ console.log(` ${pc.dim("No request logs yet. Make an API call to see logs.")}`);
52
+ blank();
53
+ return;
54
+ }
55
+ heading(`Last ${entries.length} requests:`);
56
+ blank();
57
+ printLogs(entries);
58
+ blank();
59
+ }
60
+ }
61
+ catch (error) {
62
+ fail(friendlyError(error));
63
+ blank();
64
+ process.exit(1);
65
+ }
66
+ }
@@ -0,0 +1,3 @@
1
+ export declare function sendRequest(sandboxId: string, httpMethod: string, path: string, options: {
2
+ data?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import pc from "picocolors";
2
+ import { getSandbox, sandboxRequest } from "../lib/api.js";
3
+ import { fail, blank, friendlyError, method as methodColor, status as statusColor } from "../lib/output.js";
4
+ export async function sendRequest(sandboxId, httpMethod, path, options) {
5
+ blank();
6
+ try {
7
+ const sb = await getSandbox(sandboxId);
8
+ const apiKey = sb.credentials?.[0]?.api_key;
9
+ const m = httpMethod.toUpperCase();
10
+ console.log(` ${methodColor(m)} ${pc.cyan(path)}`);
11
+ if (options.data) {
12
+ console.log(` ${pc.dim("Body:")} ${options.data.length > 80 ? options.data.slice(0, 77) + "..." : options.data}`);
13
+ }
14
+ blank();
15
+ const res = await sandboxRequest(sandboxId, m, path, options.data, apiKey);
16
+ // Status line
17
+ const statusStr = statusColor(res.status);
18
+ const statusText = res.status < 300 ? "OK" : res.status < 500 ? "Client Error" : "Server Error";
19
+ console.log(` ${pc.dim("←")} ${statusStr} ${statusText}`);
20
+ blank();
21
+ // Response body
22
+ if (res.body !== undefined && res.body !== null && res.body !== "") {
23
+ const formatted = typeof res.body === "string"
24
+ ? res.body
25
+ : JSON.stringify(res.body, null, 2);
26
+ // Truncate very large responses for terminal readability
27
+ const lines = formatted.split("\n");
28
+ const maxLines = 60;
29
+ for (const line of lines.slice(0, maxLines)) {
30
+ console.log(` ${pc.dim(line)}`);
31
+ }
32
+ if (lines.length > maxLines) {
33
+ blank();
34
+ console.log(` ${pc.dim(`... ${lines.length - maxLines} more lines (${formatted.length} bytes total)`)}`);
35
+ }
36
+ }
37
+ blank();
38
+ }
39
+ catch (error) {
40
+ fail(friendlyError(error));
41
+ blank();
42
+ process.exit(1);
43
+ }
44
+ }
@@ -0,0 +1,3 @@
1
+ export declare function scenario(sandboxId: string, options: {
2
+ name?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import pc from "picocolors";
2
+ import { getSandbox, getScenarios, setScenario } from "../lib/api.js";
3
+ import { ok, fail, blank, heading, friendlyError } from "../lib/output.js";
4
+ export async function scenario(sandboxId, options) {
5
+ blank();
6
+ try {
7
+ const sb = await getSandbox(sandboxId);
8
+ if (options.name) {
9
+ // Set scenario
10
+ const result = await setScenario(sandboxId, options.name);
11
+ ok(`Scenario set to "${result.active_scenario}"`);
12
+ blank();
13
+ return;
14
+ }
15
+ // List scenarios
16
+ const scenarios = await getScenarios();
17
+ heading(`Scenarios for ${sb.spec_name || sb.name}:`);
18
+ console.log(` ${pc.dim("Active:")} ${pc.bold(sb.active_scenario)}`);
19
+ blank();
20
+ if (scenarios.length === 0) {
21
+ console.log(` ${pc.dim("No custom scenarios available.")}`);
22
+ blank();
23
+ return;
24
+ }
25
+ for (const s of scenarios) {
26
+ const active = s.name === sb.active_scenario ? pc.green(" ● active") : "";
27
+ console.log(` ${pc.bold(s.name)}${active}`);
28
+ console.log(` ${pc.dim(s.description)}`);
29
+ console.log(` ${pc.dim(`${s.overrides_count} overrides, auth ${s.auth_enabled ? "enabled" : "disabled"}`)}`);
30
+ blank();
31
+ }
32
+ console.log(` ${pc.dim("Switch:")} ${pc.cyan(`fetchsandbox scenario ${sandboxId} --name <scenario>`)}`);
33
+ blank();
34
+ }
35
+ catch (error) {
36
+ fail(friendlyError(error));
37
+ blank();
38
+ process.exit(1);
39
+ }
40
+ }
@@ -0,0 +1,3 @@
1
+ export declare function workflows(sandboxId: string, options: {
2
+ name?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,68 @@
1
+ import pc from "picocolors";
2
+ import { getSandbox, getWorkflows } from "../lib/api.js";
3
+ import { fail, blank, heading, method as methodColor, friendlyError } from "../lib/output.js";
4
+ export async function workflows(sandboxId, options) {
5
+ blank();
6
+ try {
7
+ const sb = await getSandbox(sandboxId);
8
+ const data = await getWorkflows(sb.spec_id);
9
+ const wfs = data.workflows || [];
10
+ if (wfs.length === 0) {
11
+ console.log(` ${pc.dim("No workflows discovered for this sandbox.")}`);
12
+ blank();
13
+ return;
14
+ }
15
+ // If a specific workflow name is given, show detail view
16
+ if (options.name) {
17
+ const match = wfs.find((w) => w.id === options.name ||
18
+ w.name.toLowerCase().includes(options.name.toLowerCase()));
19
+ if (!match) {
20
+ fail(`No workflow matching "${options.name}". Run ${pc.cyan("fetchsandbox workflows " + sandboxId)} to see all.`);
21
+ blank();
22
+ return;
23
+ }
24
+ heading(`${match.name}`);
25
+ console.log(` ${pc.dim(match.description)}`);
26
+ if (match.category)
27
+ console.log(` ${pc.dim("Category:")} ${match.category}`);
28
+ blank();
29
+ for (const step of match.steps) {
30
+ const m = methodColor(step.method);
31
+ console.log(` ${pc.bold(String(step.step) + ".")} ${step.name}`);
32
+ console.log(` ${m} ${pc.cyan(step.path)}`);
33
+ if (step.description)
34
+ console.log(` ${pc.dim(step.description)}`);
35
+ blank();
36
+ }
37
+ if (match.webhook_events && match.webhook_events.length > 0) {
38
+ heading("Webhook events:");
39
+ for (const evt of match.webhook_events) {
40
+ console.log(` ${pc.dim("→")} ${evt}`);
41
+ }
42
+ blank();
43
+ }
44
+ // Show helpful next step
45
+ console.log(` ${pc.dim("Try it:")} ${pc.cyan(`fetchsandbox request ${sandboxId} ${match.steps[0].method} ${match.steps[0].path}`)}`);
46
+ blank();
47
+ return;
48
+ }
49
+ // List all workflows
50
+ heading(`${wfs.length} workflows for ${sb.spec_name || sb.name}:`);
51
+ blank();
52
+ for (const wf of wfs) {
53
+ const stepCount = `${wf.steps.length} steps`;
54
+ console.log(` ${pc.bold(wf.name)} ${pc.dim(stepCount)}`);
55
+ console.log(` ${pc.dim(wf.description)}`);
56
+ const methods = wf.steps.map((s) => `${s.method} ${s.path}`).join(" → ");
57
+ console.log(` ${pc.dim(methods.length > 80 ? methods.slice(0, 77) + "..." : methods)}`);
58
+ blank();
59
+ }
60
+ console.log(` ${pc.dim("Show detail:")} ${pc.cyan(`fetchsandbox workflows ${sandboxId} --name "<workflow name>"`)}`);
61
+ blank();
62
+ }
63
+ catch (error) {
64
+ fail(friendlyError(error));
65
+ blank();
66
+ process.exit(1);
67
+ }
68
+ }
@@ -1,2 +1,2 @@
1
1
  export declare const API_BASE: string;
2
- export declare const VERSION = "0.1.0";
2
+ export declare const VERSION = "0.2.0";
package/dist/constants.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const API_BASE = process.env.FETCHSANDBOX_API_URL ?? "https://fetchsandbox.com";
2
- export const VERSION = "0.1.0";
2
+ export const VERSION = "0.2.0";
package/dist/index.js CHANGED
@@ -6,42 +6,113 @@ import { generate } from "./commands/generate.js";
6
6
  import { showStatus } from "./commands/status.js";
7
7
  import { reset } from "./commands/reset.js";
8
8
  import { list } from "./commands/list.js";
9
+ import { workflows } from "./commands/workflows.js";
10
+ import { sendRequest } from "./commands/request.js";
11
+ import { check } from "./commands/check.js";
12
+ import { endpoints } from "./commands/endpoints.js";
13
+ import { logs } from "./commands/logs.js";
14
+ import { scenario } from "./commands/scenario.js";
15
+ import { deleteCmd } from "./commands/delete.js";
16
+ import { docs } from "./commands/docs.js";
9
17
  const program = new Command();
10
18
  program
11
19
  .name("fetchsandbox")
12
- .description(pc.dim("Turn any OpenAPI spec into a live developer portal"))
20
+ .description(pc.dim("Ship your API integration in minutes, not months"))
13
21
  .version(VERSION, "-v, --version");
22
+ // ── Setup ─────────────────────────────────────────────────────────────
14
23
  program
15
24
  .command("generate <spec>")
16
- .description("Create a portal from an OpenAPI spec file or URL")
25
+ .description("Create a sandbox from an OpenAPI spec file or URL")
17
26
  .action(generate);
27
+ program
28
+ .command("list")
29
+ .description("List all sandboxes")
30
+ .action(list);
31
+ program
32
+ .command("delete <sandbox-id>")
33
+ .description("Delete a sandbox")
34
+ .option("-f, --force", "Skip confirmation prompt")
35
+ .action((id, opts) => deleteCmd(id, opts));
36
+ // ── Understand ────────────────────────────────────────────────────────
37
+ program
38
+ .command("endpoints <sandbox-id>")
39
+ .description("List all endpoints (use --search to filter)")
40
+ .option("-s, --search <term>", "Filter by path, summary, or tag")
41
+ .action((id, opts) => endpoints(id, opts));
42
+ program
43
+ .command("docs <sandbox-id>")
44
+ .description("Open the docs portal in your browser")
45
+ .action(docs);
46
+ // ── Plan ──────────────────────────────────────────────────────────────
47
+ program
48
+ .command("workflows <sandbox-id>")
49
+ .description("List integration workflows (use --name for detail)")
50
+ .option("-n, --name <name>", "Show detail for a specific workflow")
51
+ .action((id, opts) => workflows(id, opts));
52
+ // ── Implement ─────────────────────────────────────────────────────────
53
+ program
54
+ .command("request <sandbox-id> <method> <path>")
55
+ .description("Make an API call to the sandbox")
56
+ .option("-d, --data <json>", "Request body (JSON string)")
57
+ .action((id, method, path, opts) => sendRequest(id, method, path, opts));
58
+ // ── Test ──────────────────────────────────────────────────────────────
18
59
  program
19
60
  .command("status <sandbox-id>")
20
61
  .description("Show sandbox state, resources, and recent activity")
21
62
  .action(showStatus);
63
+ program
64
+ .command("logs <sandbox-id>")
65
+ .description("Show request logs (use --follow for live tail)")
66
+ .option("-f, --follow", "Tail logs in real-time")
67
+ .option("--status <code>", "Filter by status code (e.g. 200, 4xx, 5xx)")
68
+ .option("-l, --limit <n>", "Number of log entries (default: 30)")
69
+ .action((id, opts) => logs(id, opts));
70
+ program
71
+ .command("scenario <sandbox-id>")
72
+ .description("List or switch test scenarios")
73
+ .option("-n, --name <name>", "Set active scenario")
74
+ .action((id, opts) => scenario(id, opts));
22
75
  program
23
76
  .command("reset <sandbox-id>")
24
77
  .description("Reset sandbox to its original seed data")
25
78
  .action(reset);
79
+ // ── Go Live ───────────────────────────────────────────────────────────
26
80
  program
27
- .command("list")
28
- .description("List all sandboxes")
29
- .action(list);
81
+ .command("check <sandbox-id>")
82
+ .description("Run integration readiness check")
83
+ .action(check);
30
84
  // If no command given, show help with a friendly message
31
85
  if (process.argv.length <= 2) {
32
86
  console.log();
33
87
  console.log(` ${pc.bold("fetchsandbox")} ${pc.dim(`v${VERSION}`)}`);
34
- console.log(` ${pc.dim("Turn any OpenAPI spec into a live developer portal")}`);
88
+ console.log(` ${pc.dim("Ship your API integration in minutes, not months")}`);
35
89
  console.log();
36
90
  console.log(` ${pc.dim("Quick start:")}`);
37
91
  console.log(` ${pc.cyan("fetchsandbox generate ./openapi.yaml")}`);
38
- console.log(` ${pc.cyan("fetchsandbox generate https://api.example.com/openapi.yaml")}`);
39
92
  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`);
93
+ console.log(` ${pc.dim("Understand:")}`);
94
+ console.log(` ${pc.white("endpoints <id>")} List all endpoints (--search to filter)`);
95
+ console.log(` ${pc.white("docs <id>")} Open docs portal in browser`);
96
+ console.log();
97
+ console.log(` ${pc.dim("Plan:")}`);
98
+ console.log(` ${pc.white("workflows <id>")} List integration workflows`);
99
+ console.log();
100
+ console.log(` ${pc.dim("Implement:")}`);
101
+ console.log(` ${pc.white("request <id> <method> <path>")} Make an API call`);
102
+ console.log();
103
+ console.log(` ${pc.dim("Test:")}`);
104
+ console.log(` ${pc.white("status <id>")} Show sandbox state and activity`);
105
+ console.log(` ${pc.white("logs <id>")} Show request logs (--follow for live)`);
106
+ console.log(` ${pc.white("scenario <id>")} List or switch test scenarios`);
43
107
  console.log(` ${pc.white("reset <id>")} Reset sandbox to seed data`);
108
+ console.log();
109
+ console.log(` ${pc.dim("Go Live:")}`);
110
+ console.log(` ${pc.white("check <id>")} Integration readiness score`);
111
+ console.log();
112
+ console.log(` ${pc.dim("Manage:")}`);
113
+ console.log(` ${pc.white("generate <spec>")} Create sandbox from spec file or URL`);
44
114
  console.log(` ${pc.white("list")} List all sandboxes`);
115
+ console.log(` ${pc.white("delete <id>")} Delete a sandbox`);
45
116
  console.log();
46
117
  console.log(` ${pc.dim("Learn more:")} ${pc.cyan("https://fetchsandbox.com")}`);
47
118
  console.log();
package/dist/lib/api.d.ts CHANGED
@@ -28,6 +28,64 @@ export interface LogEntry {
28
28
  response_status: number;
29
29
  duration_ms: number;
30
30
  timestamp: string;
31
+ matched_operation?: string;
32
+ matched_path_template?: string;
33
+ request_body?: unknown;
34
+ response_body?: unknown;
35
+ }
36
+ export interface EndpointInfo {
37
+ method: string;
38
+ path: string;
39
+ operation_id: string;
40
+ summary: string;
41
+ tags: string[];
42
+ sandbox_url: string;
43
+ parameters: Array<{
44
+ name: string;
45
+ in: string;
46
+ required: boolean;
47
+ schema?: unknown;
48
+ }>;
49
+ has_request_body: boolean;
50
+ request_body_schema?: unknown;
51
+ sample_values?: Record<string, string>;
52
+ sample_body?: unknown;
53
+ }
54
+ export interface Workflow {
55
+ id: string;
56
+ name: string;
57
+ description: string;
58
+ category: string;
59
+ steps: Array<{
60
+ step: number;
61
+ name: string;
62
+ method: string;
63
+ path: string;
64
+ operation_id: string;
65
+ description: string;
66
+ request_body?: Record<string, unknown>;
67
+ expected_status?: number;
68
+ }>;
69
+ webhook_events?: string[];
70
+ }
71
+ export interface ScenarioInfo {
72
+ name: string;
73
+ description: string;
74
+ overrides_count: number;
75
+ auth_enabled: boolean;
76
+ }
77
+ export interface WebhookRegistration {
78
+ id: string;
79
+ sandbox_id: string;
80
+ url: string;
81
+ events: string[];
82
+ enabled: boolean;
83
+ created_at: string;
84
+ }
85
+ export interface SandboxProxyResponse {
86
+ status: number;
87
+ headers: Record<string, string>;
88
+ body: unknown;
31
89
  }
32
90
  export declare function uploadSpec(input: string): Promise<SpecResult>;
33
91
  export declare function createSandbox(specId: string, name?: string): Promise<SandboxResult>;
@@ -44,4 +102,22 @@ export declare function validateSandbox(id: string): Promise<{
44
102
  passed: number;
45
103
  failed: number;
46
104
  pass_rate: number;
105
+ results?: Array<{
106
+ method: string;
107
+ path: string;
108
+ status: number;
109
+ ok: boolean;
110
+ }>;
111
+ }>;
112
+ export declare function getEndpoints(id: string): Promise<EndpointInfo[]>;
113
+ export declare function getWorkflows(specId: string): Promise<{
114
+ workflows: Workflow[];
115
+ }>;
116
+ export declare function getScenarios(): Promise<ScenarioInfo[]>;
117
+ export declare function setScenario(sandboxId: string, scenario: string): Promise<{
118
+ status: string;
119
+ active_scenario: string;
47
120
  }>;
121
+ export declare function deleteSandbox(id: string): Promise<void>;
122
+ export declare function getWebhooks(sandboxId: string): Promise<WebhookRegistration[]>;
123
+ export declare function sandboxRequest(sandboxId: string, method: string, path: string, body?: string, apiKey?: string): Promise<SandboxProxyResponse>;
package/dist/lib/api.js CHANGED
@@ -71,3 +71,51 @@ export async function resetSandbox(id) {
71
71
  export async function validateSandbox(id) {
72
72
  return request(`/api/sandboxes/${id}/validate`);
73
73
  }
74
+ export async function getEndpoints(id) {
75
+ return request(`/api/sandboxes/${id}/endpoints`);
76
+ }
77
+ export async function getWorkflows(specId) {
78
+ return request(`/api/specs/${specId}/workflows`);
79
+ }
80
+ export async function getScenarios() {
81
+ return request("/api/scenarios");
82
+ }
83
+ export async function setScenario(sandboxId, scenario) {
84
+ return request(`/api/sandboxes/${sandboxId}/scenario`, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({ scenario }),
88
+ });
89
+ }
90
+ export async function deleteSandbox(id) {
91
+ return request(`/api/sandboxes/${id}`, { method: "DELETE" });
92
+ }
93
+ export async function getWebhooks(sandboxId) {
94
+ return request(`/api/sandboxes/${sandboxId}/webhooks`);
95
+ }
96
+ export async function sandboxRequest(sandboxId, method, path, body, apiKey) {
97
+ // Use the path-based proxy: /sandbox/{id}/{path}
98
+ const sandboxPath = path.startsWith("/") ? path : `/${path}`;
99
+ const url = `${API_BASE}/sandbox/${sandboxId}${sandboxPath}`;
100
+ const headers = {};
101
+ if (apiKey)
102
+ headers["api-key"] = apiKey;
103
+ if (body)
104
+ headers["Content-Type"] = "application/json";
105
+ const res = await fetch(url, {
106
+ method: method.toUpperCase(),
107
+ headers,
108
+ body: body || undefined,
109
+ });
110
+ const resBody = await res.text();
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(resBody);
114
+ }
115
+ catch {
116
+ parsed = resBody;
117
+ }
118
+ const resHeaders = {};
119
+ res.headers.forEach((v, k) => { resHeaders[k] = v; });
120
+ return { status: res.status, headers: resHeaders, body: parsed };
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetchsandbox",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Turn any OpenAPI spec into a live developer portal with a stateful sandbox",
5
5
  "type": "module",
6
6
  "bin": {