@testsmith/api-spector 0.0.1

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,133 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const promises = require("fs/promises");
4
+ const path = require("path");
5
+ const mockServer = require("./chunks/mock-server-ZB7zpXh9.js");
6
+ require("http");
7
+ require("crypto");
8
+ const C = {
9
+ reset: "\x1B[0m",
10
+ bold: "\x1B[1m",
11
+ green: "\x1B[32m",
12
+ red: "\x1B[31m",
13
+ yellow: "\x1B[33m",
14
+ cyan: "\x1B[36m",
15
+ gray: "\x1B[90m",
16
+ white: "\x1B[97m"
17
+ };
18
+ function color(str, ...codes) {
19
+ return process.stdout.isTTY ? codes.join("") + str + C.reset : str;
20
+ }
21
+ function parseArgs(argv) {
22
+ const args = {};
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const arg = argv[i];
25
+ if (!arg.startsWith("--")) continue;
26
+ const key = arg.slice(2);
27
+ const next = argv[i + 1];
28
+ if (!next || next.startsWith("--")) {
29
+ args[key] = true;
30
+ } else {
31
+ if (key === "name") {
32
+ const prev = args[key];
33
+ args[key] = Array.isArray(prev) ? [...prev, next] : [next];
34
+ } else {
35
+ args[key] = next;
36
+ }
37
+ i++;
38
+ }
39
+ }
40
+ return args;
41
+ }
42
+ async function loadWorkspace(wsPath) {
43
+ const raw = await promises.readFile(wsPath, "utf8");
44
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(wsPath)) };
45
+ }
46
+ async function loadMocks(workspace, dir) {
47
+ const mocks = [];
48
+ for (const relPath of workspace.mocks ?? []) {
49
+ try {
50
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
51
+ mocks.push(JSON.parse(raw));
52
+ } catch {
53
+ console.warn(color(` [warn] Could not load mock: ${relPath}`, C.yellow));
54
+ }
55
+ }
56
+ return mocks;
57
+ }
58
+ async function main() {
59
+ const args = parseArgs(process.argv.slice(2));
60
+ if (args.help) {
61
+ console.log(
62
+ "\nUsage:\n api-spector mock --workspace <path> [--name <server-name>]\n\nOptions:\n --workspace <path> Path to workspace.json (required)\n --name <name> Start only the named server (repeat for multiple)\n --help Show this message\n"
63
+ );
64
+ process.exit(0);
65
+ }
66
+ const wsPath = args.workspace;
67
+ if (!wsPath) {
68
+ console.error(color("Error: --workspace is required", C.red));
69
+ process.exit(1);
70
+ }
71
+ let workspace, wsDir;
72
+ try {
73
+ ;
74
+ ({ workspace, dir: wsDir } = await loadWorkspace(wsPath));
75
+ } catch {
76
+ console.error(color(`Error: could not read workspace: ${wsPath}`, C.red));
77
+ process.exit(1);
78
+ }
79
+ const allMocks = await loadMocks(workspace, wsDir);
80
+ if (allMocks.length === 0) {
81
+ console.error(color(" No mock servers defined in this workspace.", C.yellow));
82
+ process.exit(0);
83
+ }
84
+ const nameFilter = args.name ? (Array.isArray(args.name) ? args.name : [args.name]).map((n) => n.toLowerCase()) : null;
85
+ const toStart = nameFilter ? allMocks.filter((m) => nameFilter.includes(m.name.toLowerCase())) : allMocks;
86
+ if (toStart.length === 0) {
87
+ console.error(color(` No mock servers matched the given --name filter.`, C.yellow));
88
+ console.log(color(` Available: ${allMocks.map((m) => `"${m.name}"`).join(", ")}`, C.gray));
89
+ process.exit(1);
90
+ }
91
+ console.log("");
92
+ console.log(color(" Mock Servers", C.bold, C.white));
93
+ console.log(color(` Workspace: ${wsPath}`, C.gray));
94
+ console.log("");
95
+ let started = 0;
96
+ for (const mock of toStart) {
97
+ try {
98
+ await mockServer.startMock(mock);
99
+ console.log(
100
+ color(" ✓", C.green, C.bold) + ` ${color(mock.name, C.white)} ` + color(`http://127.0.0.1:${mock.port}`, C.cyan) + color(` (${mock.routes.length} route${mock.routes.length !== 1 ? "s" : ""})`, C.gray)
101
+ );
102
+ for (const route of mock.routes) {
103
+ const delay = route.delay ? color(` ${route.delay}ms`, C.gray) : "";
104
+ console.log(
105
+ color(` ${route.method.padEnd(7)} ${route.path}`, C.gray) + color(` → ${route.statusCode}`, route.statusCode < 400 ? C.green : C.red) + delay
106
+ );
107
+ }
108
+ started++;
109
+ } catch (e) {
110
+ console.error(
111
+ color(" ✗", C.red, C.bold) + ` ${mock.name} ` + color(e.message, C.red)
112
+ );
113
+ }
114
+ }
115
+ if (started === 0) {
116
+ process.exit(1);
117
+ }
118
+ console.log("");
119
+ console.log(color(" Press Ctrl+C to stop all servers.\n", C.gray));
120
+ async function shutdown() {
121
+ console.log(color("\n Stopping mock servers…", C.gray));
122
+ await mockServer.stopAll();
123
+ process.exit(0);
124
+ }
125
+ process.on("SIGINT", shutdown);
126
+ process.on("SIGTERM", shutdown);
127
+ setInterval(() => {
128
+ }, 1 << 30);
129
+ }
130
+ main().catch((err) => {
131
+ console.error(color(`Fatal: ${err.message}`, C.red));
132
+ process.exit(2);
133
+ });
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const promises = require("fs/promises");
4
+ const path = require("path");
5
+ const undici = require("undici");
6
+ const scriptRunner = require("./chunks/script-runner-Ci5t2-bo.js");
7
+ require("crypto");
8
+ require("vm");
9
+ require("dayjs");
10
+ require("tv4");
11
+ function buildJsonReport(results, summary, meta = {}) {
12
+ return JSON.stringify({
13
+ timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
14
+ workspace: meta.workspace ?? null,
15
+ environment: meta.environment ?? null,
16
+ collection: meta.collection ?? null,
17
+ summary,
18
+ results: results.map((r) => ({
19
+ name: r.name,
20
+ method: r.method,
21
+ url: r.resolvedUrl,
22
+ status: r.status,
23
+ httpStatus: r.httpStatus ?? null,
24
+ durationMs: r.durationMs ?? null,
25
+ iterationLabel: r.iterationLabel ?? null,
26
+ error: r.error ?? null,
27
+ preScriptError: r.preScriptError ?? null,
28
+ postScriptError: r.postScriptError ?? null,
29
+ tests: r.testResults ?? [],
30
+ consoleOutput: r.consoleOutput ?? [],
31
+ request: r.sentRequest ?? null,
32
+ response: r.receivedResponse ?? null
33
+ }))
34
+ }, null, 2);
35
+ }
36
+ function buildJUnitReport(results, summary, meta = {}) {
37
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38
+ const suiteName = esc(meta.collection ?? "API Tests");
39
+ const totalSec = (summary.durationMs / 1e3).toFixed(3);
40
+ const ts = meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
41
+ const cases = results.map((r) => {
42
+ const label = r.iterationLabel ? ` #${r.iterationLabel}` : "";
43
+ const name = esc(r.name + label);
44
+ const classname = esc(`${r.method} ${r.resolvedUrl}`);
45
+ const timeSec = ((r.durationMs ?? 0) / 1e3).toFixed(3);
46
+ const failures = [];
47
+ if (r.status === "error") {
48
+ failures.push(` <error message="${esc(r.error ?? "Network error")}" type="NetworkError" />`);
49
+ } else if (r.preScriptError) {
50
+ failures.push(` <error message="${esc(r.preScriptError)}" type="PreScriptError" />`);
51
+ } else if (r.postScriptError) {
52
+ failures.push(` <error message="${esc(r.postScriptError)}" type="PostScriptError" />`);
53
+ } else if (r.testResults?.length) {
54
+ for (const t of r.testResults) {
55
+ if (!t.passed) {
56
+ failures.push(
57
+ ` <failure message="${esc(t.name)}" type="AssertionError">${esc(t.error ?? "Assertion failed")}</failure>`
58
+ );
59
+ }
60
+ }
61
+ }
62
+ const inner = failures.length ? `
63
+ ${failures.join("\n")}
64
+ ` : "";
65
+ return ` <testcase name="${name}" classname="${classname}" time="${timeSec}">${inner}</testcase>`;
66
+ });
67
+ const lines = [
68
+ '<?xml version="1.0" encoding="UTF-8"?>',
69
+ `<testsuites name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" time="${totalSec}">`,
70
+ ` <testsuite name="${suiteName}" tests="${summary.total}" failures="${summary.failed}" errors="${summary.errors}" time="${totalSec}" timestamp="${ts}">`,
71
+ ...cases,
72
+ " </testsuite>",
73
+ "</testsuites>"
74
+ ];
75
+ return lines.join("\n") + "\n";
76
+ }
77
+ function collectTagged(folder, requests, collectionVars, filterTags) {
78
+ const results = [];
79
+ for (const reqId of folder.requestIds) {
80
+ const req = requests[reqId];
81
+ if (!req) continue;
82
+ const tags = req.meta?.tags ?? [];
83
+ if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
84
+ results.push({ request: req, collectionVars });
85
+ }
86
+ for (const sub of folder.folders) {
87
+ const folderTags = sub.tags ?? [];
88
+ const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
89
+ results.push(...collectTagged(sub, requests, collectionVars, effectiveTags));
90
+ }
91
+ return results;
92
+ }
93
+ const C = {
94
+ reset: "\x1B[0m",
95
+ bold: "\x1B[1m",
96
+ dim: "\x1B[2m",
97
+ green: "\x1B[32m",
98
+ red: "\x1B[31m",
99
+ yellow: "\x1B[33m",
100
+ cyan: "\x1B[36m",
101
+ gray: "\x1B[90m",
102
+ white: "\x1B[97m"
103
+ };
104
+ function color(str, ...codes) {
105
+ return process.stdout.isTTY ? codes.join("") + str + C.reset : str;
106
+ }
107
+ function parseArgs(argv) {
108
+ const args = {};
109
+ for (let i = 0; i < argv.length; i++) {
110
+ const arg = argv[i];
111
+ if (arg.startsWith("--")) {
112
+ const key = arg.slice(2);
113
+ const next = argv[i + 1];
114
+ if (!next || next.startsWith("--")) {
115
+ args[key] = true;
116
+ } else {
117
+ args[key] = next;
118
+ i++;
119
+ }
120
+ }
121
+ }
122
+ return args;
123
+ }
124
+ async function loadWorkspace(wsPath) {
125
+ const raw = await promises.readFile(wsPath, "utf8");
126
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(wsPath)) };
127
+ }
128
+ async function loadCollections(workspace, dir) {
129
+ const cols = [];
130
+ for (const relPath of workspace.collections) {
131
+ try {
132
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
133
+ cols.push(JSON.parse(raw));
134
+ } catch (_e) {
135
+ console.error(color(` [warn] Could not load collection: ${relPath}`, C.yellow));
136
+ }
137
+ }
138
+ return cols;
139
+ }
140
+ async function loadEnvironments(workspace, dir) {
141
+ const envs = [];
142
+ for (const relPath of workspace.environments) {
143
+ try {
144
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
145
+ envs.push(JSON.parse(raw));
146
+ } catch (_e) {
147
+ }
148
+ }
149
+ return envs;
150
+ }
151
+ async function executeRequest(req, collectionVars, envVars, globals, verbose) {
152
+ const base = {
153
+ requestId: req.id,
154
+ name: req.name,
155
+ method: req.method,
156
+ resolvedUrl: req.url,
157
+ status: "running"
158
+ };
159
+ let localVars = {};
160
+ let updatedEnvVars = { ...envVars };
161
+ let updatedCollectionVars = { ...collectionVars };
162
+ let updatedGlobals = { ...globals };
163
+ let preScriptError;
164
+ if (req.preRequestScript?.trim()) {
165
+ const r = await scriptRunner.runScript(req.preRequestScript, {
166
+ envVars: { ...envVars },
167
+ collectionVars: { ...collectionVars },
168
+ globals: { ...globals },
169
+ localVars: {}
170
+ });
171
+ preScriptError = r.error;
172
+ localVars = r.updatedLocalVars;
173
+ updatedEnvVars = r.updatedEnvVars;
174
+ updatedCollectionVars = r.updatedCollectionVars;
175
+ updatedGlobals = r.updatedGlobals;
176
+ scriptRunner.patchGlobals(r.updatedGlobals);
177
+ await scriptRunner.persistGlobals();
178
+ if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [pre] ${l}`, C.gray)));
179
+ if (r.error) console.error(color(` [pre-script error] ${r.error}`, C.red));
180
+ }
181
+ const vars = scriptRunner.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars);
182
+ const resolvedUrl = scriptRunner.buildUrl(req.url, req.params, vars);
183
+ base.resolvedUrl = resolvedUrl;
184
+ const start = Date.now();
185
+ try {
186
+ const authH = {};
187
+ if (req.auth.type === "bearer") {
188
+ let token = req.auth.token ?? "";
189
+ if (!token && req.auth.tokenSecretRef) token = await scriptRunner.getSecret(req.auth.tokenSecretRef) ?? "";
190
+ if (token) authH["Authorization"] = `Bearer ${scriptRunner.interpolate(token, vars)}`;
191
+ }
192
+ const headers = new undici.Headers();
193
+ for (const h of req.headers) {
194
+ if (h.enabled && h.key) headers.set(scriptRunner.interpolate(h.key, vars), scriptRunner.interpolate(h.value, vars));
195
+ }
196
+ for (const [k, v] of Object.entries(authH)) headers.set(k, v);
197
+ let body;
198
+ if (req.body.mode === "json" && req.body.json) {
199
+ body = scriptRunner.interpolate(req.body.json, vars);
200
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
201
+ } else if (req.body.mode === "raw" && req.body.raw) {
202
+ body = scriptRunner.interpolate(req.body.raw, vars);
203
+ if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
204
+ } else if (req.body.mode === "graphql" && req.body.graphql) {
205
+ const gql = req.body.graphql;
206
+ const gqlBody = { query: scriptRunner.interpolate(gql.query, vars) };
207
+ const rawVars = gql.variables?.trim();
208
+ if (rawVars) {
209
+ try {
210
+ gqlBody.variables = JSON.parse(scriptRunner.interpolate(rawVars, vars));
211
+ } catch {
212
+ }
213
+ }
214
+ if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
215
+ body = JSON.stringify(gqlBody);
216
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
217
+ }
218
+ const fetchResp = await undici.fetch(resolvedUrl, {
219
+ method: req.method,
220
+ headers,
221
+ body: ["GET", "HEAD"].includes(req.method) ? void 0 : body
222
+ });
223
+ const responseBody = await fetchResp.text();
224
+ const durationMs = Date.now() - start;
225
+ const respHeaders = {};
226
+ fetchResp.headers.forEach((v, k) => {
227
+ respHeaders[k] = v;
228
+ });
229
+ const response = {
230
+ status: fetchResp.status,
231
+ statusText: fetchResp.statusText,
232
+ headers: respHeaders,
233
+ body: responseBody,
234
+ bodySize: Buffer.byteLength(responseBody, "utf8"),
235
+ durationMs
236
+ };
237
+ let testResults = [];
238
+ let consoleOutput = [];
239
+ let postScriptError;
240
+ if (req.postRequestScript?.trim()) {
241
+ const r = await scriptRunner.runScript(req.postRequestScript, {
242
+ envVars: updatedEnvVars,
243
+ collectionVars: updatedCollectionVars,
244
+ globals: updatedGlobals,
245
+ localVars,
246
+ response
247
+ });
248
+ testResults = r.testResults;
249
+ consoleOutput = r.consoleOutput;
250
+ postScriptError = r.error;
251
+ scriptRunner.patchGlobals(r.updatedGlobals);
252
+ await scriptRunner.persistGlobals();
253
+ if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [post] ${l}`, C.gray)));
254
+ }
255
+ const allPassed = testResults.every((t) => t.passed);
256
+ const status = postScriptError ? "error" : testResults.length > 0 ? allPassed ? "passed" : "failed" : "passed";
257
+ return { ...base, status, httpStatus: fetchResp.status, durationMs, testResults, consoleOutput, preScriptError, postScriptError };
258
+ } catch (err) {
259
+ return {
260
+ ...base,
261
+ status: "error",
262
+ durationMs: Date.now() - start,
263
+ error: err instanceof Error ? err.message : String(err),
264
+ preScriptError
265
+ };
266
+ }
267
+ }
268
+ function printResult(r, verbose) {
269
+ const icon = r.status === "passed" ? color("✓", C.green, C.bold) : r.status === "failed" ? color("✗", C.red, C.bold) : color("⚠", C.yellow, C.bold);
270
+ const http = r.httpStatus ? color(` ${r.httpStatus}`, r.httpStatus < 400 ? C.green : C.red) : "";
271
+ const dur = r.durationMs !== void 0 ? color(` ${r.durationMs}ms`, C.gray) : "";
272
+ const method = color(r.method.padEnd(7), C.cyan);
273
+ console.log(` ${icon} ${method} ${r.name}${http}${dur}`);
274
+ if (verbose) console.log(color(` ${r.resolvedUrl}`, C.gray));
275
+ if (r.testResults?.length) {
276
+ for (const t of r.testResults) {
277
+ const ti = t.passed ? color(" ✓", C.green) : color(" ✗", C.red);
278
+ console.log(`${ti} ${t.name}${t.error ? color(` — ${t.error}`, C.red) : ""}`);
279
+ }
280
+ }
281
+ if (r.error) console.log(color(` Error: ${r.error}`, C.red));
282
+ }
283
+ async function main() {
284
+ const args = parseArgs(process.argv.slice(2));
285
+ if (args.help) {
286
+ console.log(
287
+ "\nUsage:\n api-spector run --workspace <path> [--env <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit]\n [--verbose] [--bail]\n"
288
+ );
289
+ process.exit(0);
290
+ }
291
+ const wsPath = args.workspace;
292
+ if (!wsPath) {
293
+ console.error(color("Error: --workspace is required", C.red));
294
+ process.exit(1);
295
+ }
296
+ const filterTags = args.tags ? args.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
297
+ const envName = args.env;
298
+ const colName = args.collection;
299
+ const verbose = Boolean(args.verbose);
300
+ const bail = Boolean(args.bail);
301
+ const outputPath = args.output;
302
+ const inferredFormat = outputPath && path.extname(outputPath).toLowerCase() === ".xml" ? "junit" : "json";
303
+ const outputFormat = args.format?.toLowerCase() === "junit" ? "junit" : inferredFormat;
304
+ let workspace, wsDir;
305
+ try {
306
+ ;
307
+ ({ workspace, dir: wsDir } = await loadWorkspace(wsPath));
308
+ } catch {
309
+ console.error(color(`Error: could not read workspace file: ${wsPath}`, C.red));
310
+ process.exit(1);
311
+ }
312
+ await scriptRunner.loadGlobals(wsDir);
313
+ const collections = await loadCollections(workspace, wsDir);
314
+ const environments = await loadEnvironments(workspace, wsDir);
315
+ const env = envName ? environments.find((e) => e.name.toLowerCase() === envName.toLowerCase()) ?? null : null;
316
+ if (envName && !env) {
317
+ console.warn(color(`Warning: environment "${envName}" not found. Running without environment.`, C.yellow));
318
+ }
319
+ console.log("");
320
+ console.log(color(" API Test Runner", C.bold, C.white));
321
+ console.log(color(` Workspace: ${wsPath}`, C.gray));
322
+ console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
323
+ if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
324
+ console.log("");
325
+ const summary = { total: 0, passed: 0, failed: 0, errors: 0, durationMs: 0 };
326
+ const allResults = [];
327
+ const totalStart = Date.now();
328
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
329
+ let firstColName;
330
+ for (const col of collections) {
331
+ if (colName && col.name.toLowerCase() !== colName.toLowerCase()) continue;
332
+ const items = collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
333
+ if (items.length === 0) continue;
334
+ if (!firstColName) firstColName = col.name;
335
+ const envVars = await scriptRunner.buildEnvVars(env);
336
+ const globals = scriptRunner.getGlobals();
337
+ console.log(color(` ┌ ${col.name}`, C.bold, C.white));
338
+ let bailed = false;
339
+ for (const item of items) {
340
+ const result = await executeRequest(item.request, item.collectionVars, envVars, globals, verbose);
341
+ printResult(result, verbose);
342
+ allResults.push(result);
343
+ summary.total++;
344
+ if (result.status === "passed") summary.passed++;
345
+ else if (result.status === "failed") summary.failed++;
346
+ else summary.errors++;
347
+ if (bail && (result.status === "failed" || result.status === "error")) {
348
+ console.log(color("\n Bailing after first failure.", C.yellow));
349
+ bailed = true;
350
+ break;
351
+ }
352
+ }
353
+ console.log("");
354
+ if (bailed) break;
355
+ }
356
+ summary.durationMs = Date.now() - totalStart;
357
+ const passStr = color(`${summary.passed} passed`, C.green, C.bold);
358
+ const failStr = summary.failed > 0 ? color(` · ${summary.failed} failed`, C.red, C.bold) : "";
359
+ const errStr = summary.errors > 0 ? color(` · ${summary.errors} errors`, C.yellow, C.bold) : "";
360
+ const totalStr = color(` · ${summary.total} total · ${summary.durationMs}ms`, C.gray);
361
+ console.log(` ${passStr}${failStr}${errStr}${totalStr}
362
+ `);
363
+ if (outputPath) {
364
+ const meta = { workspace: wsPath, environment: env?.name ?? null, collection: firstColName, timestamp };
365
+ const report = outputFormat === "junit" ? buildJUnitReport(allResults, summary, meta) : buildJsonReport(allResults, summary, meta);
366
+ await promises.writeFile(path.resolve(outputPath), report, "utf8");
367
+ console.log(color(` Report written: ${outputPath} (${outputFormat})
368
+ `, C.gray));
369
+ }
370
+ process.exit(summary.failed + summary.errors > 0 ? 1 : 0);
371
+ }
372
+ main().catch((err) => {
373
+ console.error(color(`Fatal: ${err.message}`, C.red));
374
+ process.exit(2);
375
+ });
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ const electron = require("electron");
3
+ const api = {
4
+ // ─── Workspace / File ──────────────────────────────────────────────────────
5
+ openWorkspace: () => electron.ipcRenderer.invoke("file:openWorkspace"),
6
+ getLastWorkspace: () => electron.ipcRenderer.invoke("file:getLastWorkspace"),
7
+ saveWorkspace: (ws) => electron.ipcRenderer.invoke("file:saveWorkspace", ws),
8
+ newWorkspace: () => electron.ipcRenderer.invoke("file:newWorkspace"),
9
+ closeWorkspace: () => electron.ipcRenderer.invoke("file:closeWorkspace"),
10
+ loadCollection: (relPath) => electron.ipcRenderer.invoke("file:loadCollection", relPath),
11
+ saveCollection: (relPath, col) => electron.ipcRenderer.invoke("file:saveCollection", relPath, col),
12
+ loadEnvironment: (relPath) => electron.ipcRenderer.invoke("file:loadEnvironment", relPath),
13
+ saveEnvironment: (relPath, env) => electron.ipcRenderer.invoke("file:saveEnvironment", relPath, env),
14
+ // ─── HTTP execution ────────────────────────────────────────────────────────
15
+ sendRequest: (payload) => electron.ipcRenderer.invoke("request:send", payload),
16
+ // ─── Secrets (encrypted, master-key-based) ───────────────────────────────
17
+ checkMasterKey: () => electron.ipcRenderer.invoke("secret:checkMasterKey"),
18
+ setMasterKey: (value) => electron.ipcRenderer.invoke("secret:setMasterKey", value),
19
+ // ─── Globals ──────────────────────────────────────────────────────────────
20
+ getGlobals: () => electron.ipcRenderer.invoke("globals:get"),
21
+ setGlobals: (globals) => electron.ipcRenderer.invoke("globals:set", globals),
22
+ // ─── Runner ───────────────────────────────────────────────────────────────
23
+ runCollection: (payload) => electron.ipcRenderer.invoke("runner:start", payload),
24
+ onRunProgress: (cb) => {
25
+ electron.ipcRenderer.on("runner:progress", (_e, result) => cb(result));
26
+ },
27
+ offRunProgress: () => {
28
+ electron.ipcRenderer.removeAllListeners("runner:progress");
29
+ },
30
+ saveResults: (content, defaultName) => electron.ipcRenderer.invoke("results:save", content, defaultName),
31
+ // ─── Import ────────────────────────────────────────────────────────────────
32
+ importPostman: () => electron.ipcRenderer.invoke("import:postman"),
33
+ importOpenApi: () => electron.ipcRenderer.invoke("import:openapi"),
34
+ importOpenApiFromUrl: (url) => electron.ipcRenderer.invoke("import:openapi-url", url),
35
+ importInsomnia: () => electron.ipcRenderer.invoke("import:insomnia"),
36
+ importBruno: () => electron.ipcRenderer.invoke("import:bruno"),
37
+ // ─── Code generation ──────────────────────────────────────────────────────
38
+ generateCode: (opts) => electron.ipcRenderer.invoke("generate:code", opts),
39
+ pickOutputDir: () => electron.ipcRenderer.invoke("dialog:pickDir"),
40
+ saveGeneratedFiles: (files, outputDir) => electron.ipcRenderer.invoke("generate:save", files, outputDir),
41
+ saveGeneratedFilesAsZip: (files, collectionName, target) => electron.ipcRenderer.invoke("generate:saveZip", files, collectionName, target),
42
+ // ─── OAuth 2.0 ────────────────────────────────────────────────────────────
43
+ oauth2StartFlow: (auth, vars) => electron.ipcRenderer.invoke("oauth2:startFlow", auth, vars),
44
+ oauth2RefreshToken: (auth, vars, refreshToken) => electron.ipcRenderer.invoke("oauth2:refreshToken", auth, vars, refreshToken),
45
+ // ─── Mock servers ─────────────────────────────────────────────────────────────
46
+ mockStart: (server) => electron.ipcRenderer.invoke("mock:start", server),
47
+ mockStop: (id) => electron.ipcRenderer.invoke("mock:stop", id),
48
+ mockIsRunning: (id) => electron.ipcRenderer.invoke("mock:isRunning", id),
49
+ mockRunningIds: () => electron.ipcRenderer.invoke("mock:runningIds"),
50
+ saveMock: (relPath, server) => electron.ipcRenderer.invoke("file:saveMock", relPath, server),
51
+ loadMock: (relPath) => electron.ipcRenderer.invoke("file:loadMock", relPath),
52
+ mockUpdateRoutes: (id, routes) => electron.ipcRenderer.invoke("mock:updateRoutes", id, routes),
53
+ onMockHit: (cb) => {
54
+ electron.ipcRenderer.on("mock:hit", (_e, hit) => cb(hit));
55
+ },
56
+ offMockHit: () => {
57
+ electron.ipcRenderer.removeAllListeners("mock:hit");
58
+ },
59
+ // ─── WebSocket ────────────────────────────────────────────────────────────
60
+ wsConnect: (requestId, url, headers) => electron.ipcRenderer.invoke("ws:connect", requestId, url, headers),
61
+ wsSend: (requestId, data) => electron.ipcRenderer.invoke("ws:send", requestId, data),
62
+ wsDisconnect: (requestId) => electron.ipcRenderer.invoke("ws:disconnect", requestId),
63
+ onWsMessage: (cb) => {
64
+ electron.ipcRenderer.on("ws:message", (_e, payload) => cb(payload));
65
+ },
66
+ onWsStatus: (cb) => {
67
+ electron.ipcRenderer.on("ws:status", (_e, payload) => cb(payload));
68
+ },
69
+ offWsEvents: () => {
70
+ electron.ipcRenderer.removeAllListeners("ws:message");
71
+ electron.ipcRenderer.removeAllListeners("ws:status");
72
+ },
73
+ // ─── SOAP / WSDL ──────────────────────────────────────────────────────────
74
+ wsdlFetch: (url, extraHeaders) => electron.ipcRenderer.invoke("wsdl:fetch", url, extraHeaders ?? {}),
75
+ // ─── Docs generation ──────────────────────────────────────────────────────
76
+ generateDocs: (payload) => electron.ipcRenderer.invoke("docs:generate", payload),
77
+ // ─── Contract testing ─────────────────────────────────────────────────────
78
+ runContracts: (payload) => electron.ipcRenderer.invoke("contract:run", payload),
79
+ inferContractSchema: (jsonBody) => electron.ipcRenderer.invoke("contract:inferSchema", jsonBody)
80
+ };
81
+ electron.contextBridge.exposeInMainWorld("electron", api);