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.
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +102 -0
- package/dist/commands/delete.d.ts +3 -0
- package/dist/commands/delete.js +30 -0
- package/dist/commands/docs.d.ts +1 -0
- package/dist/commands/docs.js +22 -0
- package/dist/commands/endpoints.d.ts +3 -0
- package/dist/commands/endpoints.js +38 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +66 -0
- package/dist/commands/request.d.ts +3 -0
- package/dist/commands/request.js +44 -0
- package/dist/commands/scenario.d.ts +3 -0
- package/dist/commands/scenario.js +40 -0
- package/dist/commands/workflows.d.ts +3 -0
- package/dist/commands/workflows.js +68 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/index.js +81 -10
- package/dist/lib/api.d.ts +76 -0
- package/dist/lib/api.js +48 -0
- package/package.json +1 -1
|
@@ -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,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,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,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,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,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,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
|
+
}
|
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.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.
|
|
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("
|
|
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
|
|
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("
|
|
28
|
-
.description("
|
|
29
|
-
.action(
|
|
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("
|
|
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("
|
|
41
|
-
console.log(` ${pc.white("
|
|
42
|
-
console.log(` ${pc.white("
|
|
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
|
+
}
|