fetchsandbox 0.2.0 → 0.3.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,4 @@
1
+ export declare function run(sandboxId: string, workflowName: string, options?: {
2
+ scenario?: string;
3
+ verbose?: boolean;
4
+ }): Promise<void>;
@@ -0,0 +1,167 @@
1
+ import pc from "picocolors";
2
+ import ora from "ora";
3
+ import { runWorkflow, getWorkflows, getSandbox, setScenario } from "../lib/api.js";
4
+ import { fail, blank, heading, info, method as methodColor, friendlyError } from "../lib/output.js";
5
+ export async function run(sandboxId, workflowName, options = {}) {
6
+ blank();
7
+ try {
8
+ // If a scenario was requested, switch to it first
9
+ if (options.scenario) {
10
+ const spinner = ora({ text: ` Switching to scenario: ${options.scenario}`, indent: 0 }).start();
11
+ try {
12
+ await setScenario(sandboxId, options.scenario);
13
+ spinner.succeed(` Scenario: ${options.scenario}`);
14
+ }
15
+ catch (err) {
16
+ spinner.fail(` Failed to set scenario: ${friendlyError(err)}`);
17
+ blank();
18
+ process.exit(1);
19
+ }
20
+ }
21
+ // Run the workflow
22
+ const spinner = ora({ text: ` Running workflow...`, indent: 0 }).start();
23
+ let result;
24
+ try {
25
+ result = await runWorkflow(sandboxId, workflowName);
26
+ spinner.stop();
27
+ }
28
+ catch (err) {
29
+ spinner.fail(` Failed to run workflow`);
30
+ blank();
31
+ // If 404, try to list available workflows and show helpful error
32
+ const errMsg = friendlyError(err);
33
+ if (errMsg.includes("404") || errMsg.includes("not found") || errMsg.includes("Not found")) {
34
+ await showAvailableWorkflows(sandboxId, workflowName);
35
+ }
36
+ else {
37
+ fail(errMsg);
38
+ }
39
+ blank();
40
+ process.exit(1);
41
+ }
42
+ // Display results
43
+ const totalSteps = result.steps.length;
44
+ const passedSteps = result.steps.filter((s) => s.status === "passed").length;
45
+ const workflowLabel = result.flow_description || result.flow_name;
46
+ heading(`${workflowLabel} (${totalSteps} step${totalSteps === 1 ? "" : "s"})`);
47
+ blank();
48
+ for (let i = 0; i < result.steps.length; i++) {
49
+ const step = result.steps[i];
50
+ const stepNum = `Step ${i + 1}/${totalSteps}`;
51
+ const icon = step.status === "passed" ? pc.green("✓") : pc.red("✗");
52
+ const duration = pc.dim(`${step.duration_ms}ms`);
53
+ // Parse method and path from detail or step name
54
+ const { method: httpMethod, path } = parseMethodPath(step.detail);
55
+ // Step header
56
+ if (httpMethod && path) {
57
+ console.log(` ${icon} ${pc.dim(stepNum)} ${methodColor(httpMethod)} ${path} ${duration}`);
58
+ }
59
+ else {
60
+ console.log(` ${icon} ${pc.dim(stepNum)} ${step.name} ${duration}`);
61
+ }
62
+ // Step detail
63
+ const detail = formatDetail(step);
64
+ if (detail) {
65
+ console.log(` ${detail}`);
66
+ }
67
+ // Verbose: show full data
68
+ if (options.verbose && Object.keys(step.data).length > 0) {
69
+ const json = JSON.stringify(step.data, null, 2);
70
+ for (const line of json.split("\n")) {
71
+ console.log(` ${pc.dim(line)}`);
72
+ }
73
+ }
74
+ // Show webhook events in step
75
+ if (step.name === "webhook_verification") {
76
+ const matched = (step.data.matched || []);
77
+ for (const evt of matched) {
78
+ console.log(` ${pc.yellow("⚡")} ${evt}`);
79
+ }
80
+ }
81
+ }
82
+ blank();
83
+ // Summary
84
+ const totalTime = `${result.total_duration_ms}ms`;
85
+ if (result.passed) {
86
+ console.log(` ${pc.green("✓")} Workflow complete — ${passedSteps}/${totalSteps} steps passed ${pc.dim(`(${totalTime})`)}`);
87
+ }
88
+ else {
89
+ console.log(` ${pc.red("✗")} Workflow failed at step ${passedSteps + 1}/${totalSteps} ${pc.dim(`(${totalTime})`)}`);
90
+ }
91
+ // If using a scenario and it failed as expected, that's actually a good thing
92
+ if (options.scenario && !result.passed) {
93
+ blank();
94
+ info(`Scenario "${options.scenario}" correctly caused failure at step ${passedSteps + 1}.`);
95
+ info("This confirms your error handling works.");
96
+ }
97
+ blank();
98
+ // Reset scenario back to default if we changed it
99
+ if (options.scenario) {
100
+ try {
101
+ await setScenario(sandboxId, "default");
102
+ info("Scenario reset to default.");
103
+ blank();
104
+ }
105
+ catch {
106
+ // Silent — best effort reset
107
+ }
108
+ }
109
+ // Exit with appropriate code
110
+ if (!result.passed && !options.scenario) {
111
+ process.exit(1);
112
+ }
113
+ }
114
+ catch (error) {
115
+ fail(friendlyError(error));
116
+ blank();
117
+ process.exit(1);
118
+ }
119
+ }
120
+ // ── Helpers ──────────────────────────────────────────────────────────────
121
+ /** Parse HTTP method and path from step detail string. */
122
+ function parseMethodPath(detail) {
123
+ const match = detail.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD)\s+(\S+)/);
124
+ if (match) {
125
+ return { method: match[1], path: match[2] };
126
+ }
127
+ return { method: "", path: "" };
128
+ }
129
+ /** Format a human-readable detail line from step data. */
130
+ function formatDetail(step) {
131
+ if (step.status === "failed") {
132
+ return pc.red(step.detail);
133
+ }
134
+ // Extract meaningful info from data
135
+ const id = step.data.id || step.data.resource_id || "";
136
+ const status = step.data.status || "";
137
+ const responseStatus = step.data.response_status;
138
+ const parts = [];
139
+ if (responseStatus)
140
+ parts.push(`→ ${pc.green(String(responseStatus))}`);
141
+ if (id)
142
+ parts.push(pc.white(String(id)));
143
+ if (status)
144
+ parts.push(pc.dim(`status: ${status}`));
145
+ return parts.length > 0 ? parts.join(" ") : "";
146
+ }
147
+ /** Show available workflows when one isn't found. */
148
+ async function showAvailableWorkflows(sandboxId, attempted) {
149
+ fail(`Workflow "${attempted}" not found.`);
150
+ try {
151
+ const sb = await getSandbox(sandboxId);
152
+ const { workflows } = await getWorkflows(sb.spec_id);
153
+ if (workflows.length > 0) {
154
+ blank();
155
+ info("Available workflows:");
156
+ for (const wf of workflows) {
157
+ const id = wf.id || wf.name;
158
+ console.log(` ${pc.dim("•")} ${id}`);
159
+ }
160
+ blank();
161
+ info(`Try: fetchsandbox run ${sandboxId} "${workflows[0].id || workflows[0].name}"`);
162
+ }
163
+ }
164
+ catch {
165
+ // Silent — best effort
166
+ }
167
+ }
@@ -0,0 +1,3 @@
1
+ export declare function state(sandboxId: string, resource?: string, options?: {
2
+ id?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,135 @@
1
+ import pc from "picocolors";
2
+ import { getSandboxState } from "../lib/api.js";
3
+ import { fail, blank, heading, info, row, tableHeader, friendlyError } from "../lib/output.js";
4
+ export async function state(sandboxId, resource, options = {}) {
5
+ blank();
6
+ try {
7
+ const snapshot = await getSandboxState(sandboxId);
8
+ const types = Object.entries(snapshot).filter(([, records]) => Array.isArray(records) && records.length > 0);
9
+ if (types.length === 0) {
10
+ info("No resources in this sandbox. Run some requests first.");
11
+ blank();
12
+ return;
13
+ }
14
+ // ── Single resource type ──────────────────────────────────────────
15
+ if (resource) {
16
+ const records = snapshot[resource];
17
+ if (!records || !Array.isArray(records) || records.length === 0) {
18
+ const available = types.map(([t]) => t).join(", ");
19
+ fail(`No resource type "${resource}" found.`);
20
+ info(`Available: ${available}`);
21
+ blank();
22
+ return;
23
+ }
24
+ // Single record by --id
25
+ if (options.id) {
26
+ const typedRecords = records;
27
+ const record = typedRecords.find((r) => r.id === options.id || r.ID === options.id);
28
+ if (!record) {
29
+ fail(`No record with id "${options.id}" in ${resource}.`);
30
+ info(`${records.length} records available. Try without --id to list all.`);
31
+ blank();
32
+ return;
33
+ }
34
+ heading(`${resource} / ${options.id}`);
35
+ blank();
36
+ console.log(formatJson(record));
37
+ blank();
38
+ return;
39
+ }
40
+ // Table view for all records of this type
41
+ heading(`${resource} — ${records.length} record${records.length === 1 ? "" : "s"}`);
42
+ blank();
43
+ const cols = pickColumns(records);
44
+ const widths = cols.map((c) => Math.max(c.length + 2, Math.min(30, maxWidth(records, c) + 2)));
45
+ tableHeader(cols, widths);
46
+ for (const record of records) {
47
+ const values = cols.map((c) => truncate(String(record[c] ?? ""), widths[cols.indexOf(c)] - 2));
48
+ row(values, widths);
49
+ }
50
+ blank();
51
+ return;
52
+ }
53
+ // ── Summary of all resource types ─────────────────────────────────
54
+ heading(`State snapshot — ${types.length} resource type${types.length === 1 ? "" : "s"}, ${types.reduce((sum, [, r]) => sum + r.length, 0)} records`);
55
+ blank();
56
+ for (const [type, records] of types) {
57
+ const items = records;
58
+ const dist = stateDistribution(items);
59
+ const countStr = pc.bold(String(items.length));
60
+ const distStr = dist ? pc.dim(` (${dist})`) : "";
61
+ console.log(` ${type.padEnd(25)} ${countStr}${distStr}`);
62
+ }
63
+ blank();
64
+ info("Tip: fetchsandbox state <sandbox> <resource> to see records");
65
+ blank();
66
+ }
67
+ catch (error) {
68
+ fail(friendlyError(error));
69
+ blank();
70
+ process.exit(1);
71
+ }
72
+ }
73
+ // ── Helpers ──────────────────────────────────────────────────────────────
74
+ /** Pick the most useful columns to display in a table. */
75
+ function pickColumns(records) {
76
+ if (records.length === 0)
77
+ return [];
78
+ const allKeys = Object.keys(records[0]);
79
+ // Priority: id first, then status, then name/email/type, then rest
80
+ const priority = ["id", "ID", "status", "state", "name", "email", "type", "amount", "currency", "description"];
81
+ const picked = [];
82
+ for (const key of priority) {
83
+ if (allKeys.includes(key) && !picked.includes(key)) {
84
+ picked.push(key);
85
+ }
86
+ }
87
+ // Fill up to 5 columns with remaining keys (skip large objects/arrays)
88
+ for (const key of allKeys) {
89
+ if (picked.length >= 5)
90
+ break;
91
+ if (picked.includes(key))
92
+ continue;
93
+ const sample = records[0][key];
94
+ if (typeof sample === "object" && sample !== null)
95
+ continue; // skip nested
96
+ picked.push(key);
97
+ }
98
+ return picked;
99
+ }
100
+ /** Get the max display width of a column across all records. */
101
+ function maxWidth(records, key) {
102
+ let max = key.length;
103
+ for (const r of records) {
104
+ const len = String(r[key] ?? "").length;
105
+ if (len > max)
106
+ max = len;
107
+ }
108
+ return Math.min(max, 28);
109
+ }
110
+ /** Truncate a string to fit a column width. */
111
+ function truncate(s, max) {
112
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
113
+ }
114
+ /** Compute state/status distribution string, e.g. "2 active, 1 suspended". */
115
+ function stateDistribution(records) {
116
+ const field = records[0]?.status !== undefined ? "status" : records[0]?.state !== undefined ? "state" : null;
117
+ if (!field)
118
+ return "";
119
+ const counts = {};
120
+ for (const r of records) {
121
+ const val = String(r[field] ?? "unknown");
122
+ counts[val] = (counts[val] || 0) + 1;
123
+ }
124
+ return Object.entries(counts)
125
+ .sort(([, a], [, b]) => b - a)
126
+ .map(([val, count]) => `${count} ${val}`)
127
+ .join(", ");
128
+ }
129
+ /** Pretty-print a JSON object with 2-space indent, indented for CLI output. */
130
+ function formatJson(obj) {
131
+ return JSON.stringify(obj, null, 2)
132
+ .split("\n")
133
+ .map((line) => ` ${pc.white(line)}`)
134
+ .join("\n");
135
+ }
@@ -0,0 +1,4 @@
1
+ export declare function webhookListen(sandboxId: string, options?: {
2
+ verbose?: boolean;
3
+ eventType?: string;
4
+ }): Promise<void>;
@@ -0,0 +1,111 @@
1
+ import pc from "picocolors";
2
+ import { getWebhookEvents } from "../lib/api.js";
3
+ import { fail, blank, heading, info, friendlyError } from "../lib/output.js";
4
+ export async function webhookListen(sandboxId, options = {}) {
5
+ blank();
6
+ try {
7
+ heading("Listening for webhook events... (Ctrl+C to stop)");
8
+ blank();
9
+ let lastSeen = "";
10
+ let totalShown = 0;
11
+ const poll = async () => {
12
+ const events = await getWebhookEvents(sandboxId, 30);
13
+ // Filter to only new events (same dedup pattern as logs --follow)
14
+ const newEvents = lastSeen
15
+ ? events.filter((e) => e.timestamp > lastSeen)
16
+ : events;
17
+ if (newEvents.length === 0)
18
+ return;
19
+ // Update watermark — events are newest-first from API
20
+ lastSeen = newEvents[0].timestamp;
21
+ // Display oldest-first for natural reading order
22
+ const ordered = [...newEvents].reverse();
23
+ for (const event of ordered) {
24
+ // Optional event type filter
25
+ if (options.eventType && !event.event_type.includes(options.eventType)) {
26
+ continue;
27
+ }
28
+ const time = formatTime(event.timestamp);
29
+ const type = colorEventType(event.event_type);
30
+ const resourceId = extractResourceId(event.payload);
31
+ const statusField = extractStatus(event.payload);
32
+ console.log(` ${pc.dim(time)} ${type} ${pc.white(resourceId)}${statusField}`);
33
+ if (options.verbose && event.payload) {
34
+ const json = JSON.stringify(event.payload, null, 2);
35
+ for (const line of json.split("\n")) {
36
+ console.log(` ${pc.dim(" ")}${pc.dim(line)}`);
37
+ }
38
+ }
39
+ totalShown++;
40
+ }
41
+ };
42
+ // Initial poll
43
+ await poll();
44
+ if (totalShown === 0) {
45
+ info("No events yet. Make API calls in another terminal to see webhooks fire.");
46
+ }
47
+ // Poll every 2 seconds
48
+ const interval = setInterval(async () => {
49
+ try {
50
+ await poll();
51
+ }
52
+ catch {
53
+ // Silently retry on transient errors during polling
54
+ }
55
+ }, 2000);
56
+ process.on("SIGINT", () => {
57
+ clearInterval(interval);
58
+ blank();
59
+ if (totalShown > 0) {
60
+ console.log(` ${pc.dim(`${totalShown} event${totalShown === 1 ? "" : "s"} received.`)}`);
61
+ }
62
+ blank();
63
+ process.exit(0);
64
+ });
65
+ // Keep alive
66
+ await new Promise(() => { });
67
+ }
68
+ catch (error) {
69
+ fail(friendlyError(error));
70
+ blank();
71
+ process.exit(1);
72
+ }
73
+ }
74
+ // ── Helpers ──────────────────────────────────────────────────────────────
75
+ /** Format ISO timestamp to HH:MM:SS. */
76
+ function formatTime(ts) {
77
+ try {
78
+ const d = new Date(ts);
79
+ return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
80
+ }
81
+ catch {
82
+ return ts.slice(11, 19);
83
+ }
84
+ }
85
+ /** Color event type based on action. */
86
+ function colorEventType(eventType) {
87
+ const padded = eventType.padEnd(30);
88
+ if (eventType.includes("created"))
89
+ return pc.green(padded);
90
+ if (eventType.includes("succeeded") || eventType.includes("completed"))
91
+ return pc.green(padded);
92
+ if (eventType.includes("failed") || eventType.includes("declined"))
93
+ return pc.red(padded);
94
+ if (eventType.includes("deleted") || eventType.includes("canceled"))
95
+ return pc.red(padded);
96
+ if (eventType.includes("updated") || eventType.includes("captured"))
97
+ return pc.yellow(padded);
98
+ return pc.white(padded);
99
+ }
100
+ /** Extract the resource ID from a webhook payload. */
101
+ function extractResourceId(payload) {
102
+ const id = payload?.id ?? payload?.ID ?? "";
103
+ return id ? String(id) : "";
104
+ }
105
+ /** Extract a status-like field from payload for compact display. */
106
+ function extractStatus(payload) {
107
+ const status = payload?.status ?? payload?.state ?? null;
108
+ if (!status)
109
+ return "";
110
+ return pc.dim(` → ${status}`);
111
+ }
@@ -1,2 +1,2 @@
1
1
  export declare const API_BASE: string;
2
- export declare const VERSION = "0.2.0";
2
+ export declare const VERSION = "0.3.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.2.0";
2
+ export const VERSION = "0.3.0";
package/dist/index.js CHANGED
@@ -14,6 +14,9 @@ import { logs } from "./commands/logs.js";
14
14
  import { scenario } from "./commands/scenario.js";
15
15
  import { deleteCmd } from "./commands/delete.js";
16
16
  import { docs } from "./commands/docs.js";
17
+ import { state } from "./commands/state.js";
18
+ import { webhookListen } from "./commands/webhook.js";
19
+ import { run } from "./commands/run.js";
17
20
  const program = new Command();
18
21
  program
19
22
  .name("fetchsandbox")
@@ -55,6 +58,13 @@ program
55
58
  .description("Make an API call to the sandbox")
56
59
  .option("-d, --data <json>", "Request body (JSON string)")
57
60
  .action((id, method, path, opts) => sendRequest(id, method, path, opts));
61
+ // ── Prove ─────────────────────────────────────────────────────────────
62
+ program
63
+ .command("run <sandbox-id> <workflow>")
64
+ .description("Execute a workflow end-to-end and verify each step")
65
+ .option("--scenario <name>", "Run under an error scenario (resets after)")
66
+ .option("--verbose", "Show full response data for each step")
67
+ .action((id, workflow, opts) => run(id, workflow, opts));
58
68
  // ── Test ──────────────────────────────────────────────────────────────
59
69
  program
60
70
  .command("status <sandbox-id>")
@@ -72,10 +82,22 @@ program
72
82
  .description("List or switch test scenarios")
73
83
  .option("-n, --name <name>", "Set active scenario")
74
84
  .action((id, opts) => scenario(id, opts));
85
+ program
86
+ .command("state <sandbox-id> [resource]")
87
+ .description("Inspect resource state (use --id for single record)")
88
+ .option("--id <resource-id>", "Show a single record by ID")
89
+ .action((id, resource, opts) => state(id, resource, opts));
75
90
  program
76
91
  .command("reset <sandbox-id>")
77
92
  .description("Reset sandbox to its original seed data")
78
93
  .action(reset);
94
+ // ── Observe ──────────────────────────────────────────────────────────
95
+ program
96
+ .command("webhook-listen <sandbox-id>")
97
+ .description("Live-tail webhook events (Ctrl+C to stop)")
98
+ .option("--verbose", "Show full event payload")
99
+ .option("--event-type <type>", "Filter by event type (e.g. charge.created)")
100
+ .action((id, opts) => webhookListen(id, opts));
79
101
  // ── Go Live ───────────────────────────────────────────────────────────
80
102
  program
81
103
  .command("check <sandbox-id>")
@@ -100,12 +122,19 @@ if (process.argv.length <= 2) {
100
122
  console.log(` ${pc.dim("Implement:")}`);
101
123
  console.log(` ${pc.white("request <id> <method> <path>")} Make an API call`);
102
124
  console.log();
125
+ console.log(` ${pc.dim("Prove:")}`);
126
+ console.log(` ${pc.white("run <id> <workflow>")} Run a workflow end-to-end`);
127
+ console.log();
103
128
  console.log(` ${pc.dim("Test:")}`);
104
129
  console.log(` ${pc.white("status <id>")} Show sandbox state and activity`);
130
+ console.log(` ${pc.white("state <id> [type]")} Inspect resource data (--id for single)`);
105
131
  console.log(` ${pc.white("logs <id>")} Show request logs (--follow for live)`);
106
132
  console.log(` ${pc.white("scenario <id>")} List or switch test scenarios`);
107
133
  console.log(` ${pc.white("reset <id>")} Reset sandbox to seed data`);
108
134
  console.log();
135
+ console.log(` ${pc.dim("Observe:")}`);
136
+ console.log(` ${pc.white("webhook-listen <id>")} Live-tail webhook events`);
137
+ console.log();
109
138
  console.log(` ${pc.dim("Go Live:")}`);
110
139
  console.log(` ${pc.white("check <id>")} Integration readiness score`);
111
140
  console.log();
package/dist/lib/api.d.ts CHANGED
@@ -82,6 +82,29 @@ export interface WebhookRegistration {
82
82
  enabled: boolean;
83
83
  created_at: string;
84
84
  }
85
+ export interface WebhookEvent {
86
+ id: string;
87
+ event_type: string;
88
+ timestamp: string;
89
+ payload: Record<string, unknown>;
90
+ delivered: boolean;
91
+ delivery_status: number;
92
+ }
93
+ export interface WorkflowRunStep {
94
+ name: string;
95
+ description: string;
96
+ status: "pending" | "passed" | "failed";
97
+ detail: string;
98
+ duration_ms: number;
99
+ data: Record<string, unknown>;
100
+ }
101
+ export interface WorkflowRunResult {
102
+ flow_name: string;
103
+ flow_description: string;
104
+ passed: boolean;
105
+ total_duration_ms: number;
106
+ steps: WorkflowRunStep[];
107
+ }
85
108
  export interface SandboxProxyResponse {
86
109
  status: number;
87
110
  headers: Record<string, string>;
@@ -120,4 +143,6 @@ export declare function setScenario(sandboxId: string, scenario: string): Promis
120
143
  }>;
121
144
  export declare function deleteSandbox(id: string): Promise<void>;
122
145
  export declare function getWebhooks(sandboxId: string): Promise<WebhookRegistration[]>;
146
+ export declare function getWebhookEvents(sandboxId: string, limit?: number): Promise<WebhookEvent[]>;
147
+ export declare function runWorkflow(sandboxId: string, workflowName: string): Promise<WorkflowRunResult>;
123
148
  export declare function sandboxRequest(sandboxId: string, method: string, path: string, body?: string, apiKey?: string): Promise<SandboxProxyResponse>;
package/dist/lib/api.js CHANGED
@@ -93,6 +93,14 @@ export async function deleteSandbox(id) {
93
93
  export async function getWebhooks(sandboxId) {
94
94
  return request(`/api/sandboxes/${sandboxId}/webhooks`);
95
95
  }
96
+ export async function getWebhookEvents(sandboxId, limit = 50) {
97
+ return request(`/api/sandboxes/${sandboxId}/webhooks/events?limit=${limit}`);
98
+ }
99
+ export async function runWorkflow(sandboxId, workflowName) {
100
+ return request(`/api/sandboxes/${sandboxId}/workflows/${encodeURIComponent(workflowName)}/run`, {
101
+ method: "POST",
102
+ });
103
+ }
96
104
  export async function sandboxRequest(sandboxId, method, path, body, apiKey) {
97
105
  // Use the path-based proxy: /sandbox/{id}/{path}
98
106
  const sandboxPath = path.startsWith("/") ? path : `/${path}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetchsandbox",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Turn any OpenAPI spec into a live developer portal with a stateful sandbox",
5
5
  "type": "module",
6
6
  "bin": {