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.
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +167 -0
- package/dist/commands/state.d.ts +3 -0
- package/dist/commands/state.js +135 -0
- package/dist/commands/webhook.d.ts +4 -0
- package/dist/commands/webhook.js +111 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.js +29 -0
- package/dist/lib/api.d.ts +25 -0
- package/dist/lib/api.js +8 -0
- package/package.json +1 -1
|
@@ -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,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,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
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const API_BASE: string;
|
|
2
|
-
export declare const VERSION = "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
|
+
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}`;
|