@testsmith/api-spector 0.2.3 → 0.2.5

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/out/main/index.js CHANGED
@@ -25,19 +25,20 @@ const electron = require("electron");
25
25
  const path = require("path");
26
26
  const fs = require("fs");
27
27
  const promises = require("fs/promises");
28
- const requestCollection = require("./chunks/request-collection-DMVlm0PA.js");
28
+ const requestCollection = require("./chunks/request-collection-CElFJzre.js");
29
29
  const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
30
30
  const uuid = require("uuid");
31
31
  const jsYaml = require("js-yaml");
32
32
  const undici = require("undici");
33
33
  const JSZip = require("jszip");
34
- const mockServer = require("./chunks/mock-server-Cx-xG4pJ.js");
34
+ const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
35
35
  const http = require("http");
36
36
  const WebSocket = require("ws");
37
- const https = require("https");
37
+ const soapHandler = require("./chunks/soap-handler-Cpj-JwyA.js");
38
38
  const os = require("os");
39
39
  const crypto = require("crypto");
40
40
  const snapshots = require("./chunks/snapshots-C7YbGHM7.js");
41
+ const ipcValidate = require("./chunks/ipc-validate-CscN4HfG.js");
41
42
  const simpleGit = require("simple-git");
42
43
  const recorder = require("./chunks/recorder-DFxJgn9c.js");
43
44
  require("vm");
@@ -46,6 +47,7 @@ require("tv4");
46
47
  require("jsonpath-plus");
47
48
  require("@xmldom/xmldom");
48
49
  require("ajv");
50
+ require("https");
49
51
  const LAST_WS_FILE = path.join(electron.app.getPath("userData"), "last-workspace.json");
50
52
  async function saveLastWorkspacePath(wsPath) {
51
53
  await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: wsPath }), "utf8");
@@ -63,10 +65,156 @@ let workspaceFile = null;
63
65
  function atomicWrite(path2, data) {
64
66
  return promises.writeFile(path2, data, "utf8");
65
67
  }
68
+ const SPECTOR_GITIGNORE = [
69
+ "# API Spector — never commit secrets",
70
+ "*.secrets",
71
+ ".env",
72
+ ".env.local",
73
+ ".env.*.local",
74
+ "",
75
+ "# Dependencies",
76
+ "node_modules/",
77
+ "",
78
+ "# Generated API docs",
79
+ "api-docs.html",
80
+ "api-docs.md",
81
+ "docs/",
82
+ "",
83
+ "# Run reports (api-spector run --output)",
84
+ "reports/",
85
+ "*-report.json",
86
+ "*-report.xml",
87
+ "*-report.html",
88
+ "results.json",
89
+ "results.xml",
90
+ "results.html",
91
+ "coverage/",
92
+ "",
93
+ "# OS / editor",
94
+ ".DS_Store",
95
+ "Thumbs.db",
96
+ ".idea/",
97
+ ""
98
+ ].join("\n");
99
+ function readmeContents(workspaceFileName) {
100
+ return [
101
+ `# API tests`,
102
+ ``,
103
+ `This folder is an [API Spector](https://github.com/testsmith-io/api-spector) workspace.`,
104
+ `Everything here is plain JSON — diff it, commit it, review it like any other code.`,
105
+ ``,
106
+ `## Layout`,
107
+ ``,
108
+ "```",
109
+ `${workspaceFileName} ← workspace manifest (this is what you "open")`,
110
+ `collections/ ← your request collections`,
111
+ `environments/ ← per-env variable files (dev, staging, prod, …)`,
112
+ `mocks/ ← saved mock servers (optional)`,
113
+ `contracts/ ← pinned OpenAPI snapshots (optional)`,
114
+ `.gitignore ← excludes secrets, generated docs, run reports`,
115
+ `.vscode/settings.json ← maps *.spector to JSON for editor highlighting`,
116
+ "```",
117
+ ``,
118
+ `## Open the workspace`,
119
+ ``,
120
+ "```bash",
121
+ `# launches the GUI in this folder`,
122
+ `npx -y @testsmith/api-spector ui`,
123
+ "```",
124
+ ``,
125
+ `## Run the tests from CI`,
126
+ ``,
127
+ "```bash",
128
+ `npx -y @testsmith/api-spector run \\`,
129
+ ` --workspace ./${workspaceFileName} \\`,
130
+ ` --environment ci \\`,
131
+ ` --output reports/results.xml --format junit`,
132
+ "```",
133
+ ``,
134
+ `Other useful commands:`,
135
+ ``,
136
+ `| Command | What it does |`,
137
+ `|---|---|`,
138
+ `| \`api-spector run\` | Execute requests / assertions |`,
139
+ `| \`api-spector mock\` | Start mock servers from this workspace |`,
140
+ `| \`api-spector contract\` | Manage and run pinned contract snapshots |`,
141
+ `| \`api-spector wsdl\` | Inspect a WSDL or import as collection / mock |`,
142
+ ``,
143
+ `## A note on secrets`,
144
+ ``,
145
+ `Secret values (passwords, OAuth client secrets, API keys) are stored in your`,
146
+ `OS keychain — **not** in this folder. Environment files only reference the`,
147
+ `keychain entry by name, so it's safe to commit them.`,
148
+ ``
149
+ ].join("\n");
150
+ }
151
+ async function ensureReadme(workspaceDir2, workspaceFileName) {
152
+ const path$1 = path.join(workspaceDir2, "README.md");
153
+ try {
154
+ await promises.readFile(path$1, "utf8");
155
+ return;
156
+ } catch (err) {
157
+ if (err.code !== "ENOENT") {
158
+ console.warn("file-handler: could not check README.md", err);
159
+ return;
160
+ }
161
+ await atomicWrite(path$1, readmeContents(workspaceFileName));
162
+ }
163
+ }
164
+ async function ensureGitignore(workspaceDir2) {
165
+ const path$1 = path.join(workspaceDir2, ".gitignore");
166
+ try {
167
+ await promises.readFile(path$1, "utf8");
168
+ return;
169
+ } catch (err) {
170
+ if (err.code !== "ENOENT") {
171
+ console.warn("file-handler: could not check .gitignore", err);
172
+ return;
173
+ }
174
+ await atomicWrite(path$1, SPECTOR_GITIGNORE);
175
+ }
176
+ }
177
+ async function ensureVscodeFileAssociation(workspaceDir2) {
178
+ const dir = path.join(workspaceDir2, ".vscode");
179
+ const file = path.join(dir, "settings.json");
180
+ try {
181
+ const raw = await promises.readFile(file, "utf8");
182
+ let parsed;
183
+ try {
184
+ parsed = JSON.parse(raw);
185
+ } catch {
186
+ return;
187
+ }
188
+ const associations = parsed["files.associations"] ?? {};
189
+ if (associations["*.spector"] === "json") return;
190
+ parsed["files.associations"] = { ...associations, "*.spector": "json" };
191
+ await atomicWrite(file, JSON.stringify(parsed, null, 2) + "\n");
192
+ } catch (err) {
193
+ if (err.code !== "ENOENT") {
194
+ console.warn("file-handler: could not read existing .vscode/settings.json", err);
195
+ return;
196
+ }
197
+ await promises.mkdir(dir, { recursive: true });
198
+ const fresh = { "files.associations": { "*.spector": "json" } };
199
+ await atomicWrite(file, JSON.stringify(fresh, null, 2) + "\n");
200
+ }
201
+ }
202
+ function dialogStartDir() {
203
+ return process.env.API_SPECTOR_LAUNCH_CWD || workspaceDir || void 0;
204
+ }
205
+ function defaultWorkspaceName() {
206
+ const cwd = process.env.API_SPECTOR_LAUNCH_CWD;
207
+ if (cwd) {
208
+ const base = cwd.split(/[\\/]/).filter(Boolean).pop();
209
+ if (base && /^[a-zA-Z0-9._-]+$/.test(base)) return `${base}.spector`;
210
+ }
211
+ return "my-workspace.spector";
212
+ }
66
213
  function registerFileHandlers(ipc) {
67
214
  ipc.handle("file:openWorkspace", async () => {
68
215
  const result = await electron.dialog.showOpenDialog({
69
216
  title: "Open Workspace",
217
+ defaultPath: dialogStartDir(),
70
218
  filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }],
71
219
  properties: ["openFile"]
72
220
  });
@@ -80,9 +228,11 @@ function registerFileHandlers(ipc) {
80
228
  return { workspace: JSON.parse(raw), workspacePath: wsPath };
81
229
  });
82
230
  ipc.handle("file:newWorkspace", async () => {
231
+ const startDir = dialogStartDir();
232
+ const defaultPath = startDir ? path.join(startDir, defaultWorkspaceName()) : defaultWorkspaceName();
83
233
  const result = await electron.dialog.showSaveDialog({
84
234
  title: "Create Workspace",
85
- defaultPath: "my-workspace.spector",
235
+ defaultPath,
86
236
  filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }]
87
237
  });
88
238
  if (result.canceled || !result.filePath) return null;
@@ -91,8 +241,9 @@ function registerFileHandlers(ipc) {
91
241
  await requestCollection.loadGlobals(workspaceDir);
92
242
  await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
93
243
  await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
94
- const gitignore = "# API Spector — never commit secrets\n*.secrets\n.env.local\n\n# Dependencies\nnode_modules/\n";
95
- await atomicWrite(path.join(workspaceDir, ".gitignore"), gitignore);
244
+ await ensureGitignore(workspaceDir);
245
+ await ensureVscodeFileAssociation(workspaceDir);
246
+ await ensureReadme(workspaceDir, path.basename(result.filePath));
96
247
  const ws = {
97
248
  version: "1.0",
98
249
  collections: [],
@@ -106,6 +257,7 @@ function registerFileHandlers(ipc) {
106
257
  ipc.handle("file:saveWorkspace", async (_e, ws) => {
107
258
  if (!workspaceFile) return;
108
259
  await atomicWrite(workspaceFile, JSON.stringify(ws, null, 2));
260
+ if (workspaceDir) await ensureVscodeFileAssociation(workspaceDir);
109
261
  });
110
262
  ipc.handle("file:loadCollection", async (_e, relPath) => {
111
263
  if (!workspaceDir) throw new Error("No workspace open");
@@ -129,6 +281,18 @@ function registerFileHandlers(ipc) {
129
281
  await promises.mkdir(path.dirname(fullPath), { recursive: true });
130
282
  await atomicWrite(fullPath, JSON.stringify(env, null, 2));
131
283
  });
284
+ ipc.handle("file:deleteWorkspaceFile", async (_e, relPath) => {
285
+ if (!workspaceDir) throw new Error("No workspace open");
286
+ const fullPath = path.resolve(workspaceDir, relPath);
287
+ if (!fullPath.startsWith(path.resolve(workspaceDir) + (process.platform === "win32" ? "\\" : "/"))) {
288
+ throw new Error(`Refusing to delete file outside the workspace: ${relPath}`);
289
+ }
290
+ try {
291
+ await promises.unlink(fullPath);
292
+ } catch (err) {
293
+ if (err.code !== "ENOENT") throw err;
294
+ }
295
+ });
132
296
  ipc.handle("dialog:pickDir", async () => {
133
297
  const result = await electron.dialog.showOpenDialog({
134
298
  title: "Select Output Directory",
@@ -160,23 +324,51 @@ function registerFileHandlers(ipc) {
160
324
  ipc.handle("file:closeWorkspace", async () => {
161
325
  workspaceDir = null;
162
326
  workspaceFile = null;
163
- await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch(() => {
327
+ await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch((err) => {
328
+ console.warn("file-handler: could not clear last-workspace pointer", err);
164
329
  });
165
330
  });
166
331
  ipc.handle("file:getLastWorkspace", async () => {
332
+ const cwd = process.env.API_SPECTOR_LAUNCH_CWD;
333
+ if (cwd) {
334
+ const fromCwd = await tryOpenWorkspaceInDir(cwd);
335
+ return fromCwd;
336
+ }
167
337
  const wsPath = await readLastWorkspacePath();
168
338
  if (!wsPath) return null;
169
339
  try {
340
+ const raw = await promises.readFile(wsPath, "utf8");
341
+ const workspace = JSON.parse(raw);
170
342
  workspaceDir = path.dirname(wsPath);
171
343
  workspaceFile = wsPath;
172
344
  await requestCollection.loadGlobals(workspaceDir);
173
- const raw = await promises.readFile(wsPath, "utf8");
174
- return { workspace: JSON.parse(raw), workspacePath: wsPath };
345
+ return { workspace, workspacePath: wsPath };
175
346
  } catch {
176
347
  return null;
177
348
  }
178
349
  });
179
350
  }
351
+ async function tryOpenWorkspaceInDir(dir) {
352
+ let entries;
353
+ try {
354
+ entries = await promises.readdir(dir);
355
+ } catch {
356
+ return null;
357
+ }
358
+ const candidates = entries.filter((f) => f.endsWith(".spector"));
359
+ if (candidates.length !== 1) return null;
360
+ const wsPath = path.join(dir, candidates[0]);
361
+ try {
362
+ const raw = await promises.readFile(wsPath, "utf8");
363
+ const workspace = JSON.parse(raw);
364
+ workspaceDir = dir;
365
+ workspaceFile = wsPath;
366
+ await requestCollection.loadGlobals(workspaceDir);
367
+ return { workspace, workspacePath: wsPath };
368
+ } catch {
369
+ return null;
370
+ }
371
+ }
180
372
  function getWorkspaceDir() {
181
373
  return workspaceDir;
182
374
  }
@@ -3100,7 +3292,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
3100
3292
  envVars: { ...envVars },
3101
3293
  collectionVars: { ...collectionVars },
3102
3294
  globals: { ...globals },
3103
- localVars: {}
3295
+ localVars: {},
3296
+ piiMaskPatterns
3104
3297
  });
3105
3298
  preScriptError = r.error;
3106
3299
  localVars = r.updatedLocalVars;
@@ -3138,6 +3331,25 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
3138
3331
  } else if (req.body.mode === "raw" && req.body.raw) {
3139
3332
  body = authBuilder.interpolate(req.body.raw, vars);
3140
3333
  if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
3334
+ } else if (req.body.mode === "graphql" && req.body.graphql) {
3335
+ const gql = req.body.graphql;
3336
+ const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
3337
+ const rawVars = gql.variables?.trim();
3338
+ if (rawVars) {
3339
+ try {
3340
+ gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
3341
+ } catch {
3342
+ }
3343
+ }
3344
+ if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
3345
+ body = JSON.stringify(gqlBody);
3346
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
3347
+ } else if (req.body.mode === "soap" && req.body.soap) {
3348
+ body = authBuilder.interpolate(req.body.soap.envelope, vars);
3349
+ if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
3350
+ if (req.body.soap.soapAction && !headers.has("soapaction")) {
3351
+ headers.set("SOAPAction", req.body.soap.soapAction);
3352
+ }
3141
3353
  }
3142
3354
  const methodHasBody = !["GET", "HEAD"].includes(req.method);
3143
3355
  const doFetch = (h) => undici.fetch(resolvedUrl, {
@@ -3169,16 +3381,17 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
3169
3381
  });
3170
3382
  const maskedBody = requestCollection.maskPii(responseBody, piiMaskPatterns);
3171
3383
  const maskedHeaders = requestCollection.maskHeaders(rawRespHeaders, piiMaskPatterns);
3172
- const response = {
3384
+ const scriptResponse = {
3173
3385
  status: fetchResp.status,
3174
3386
  statusText: fetchResp.statusText,
3175
- headers: maskedHeaders,
3176
- body: maskedBody,
3387
+ headers: rawRespHeaders,
3388
+ body: responseBody,
3177
3389
  bodySize: Buffer.byteLength(responseBody, "utf8"),
3178
3390
  durationMs
3179
3391
  };
3180
3392
  const schemaTestResults = requestCollection.buildSchemaTestResults(req.schema, responseBody);
3181
- let testResults = [...schemaTestResults];
3393
+ const protocolFaultTests = requestCollection.buildProtocolFaultTests(req.body.mode, responseBody);
3394
+ let testResults = [...schemaTestResults, ...protocolFaultTests];
3182
3395
  let consoleOutput = [];
3183
3396
  let postScriptError;
3184
3397
  if (req.postRequestScript?.trim()) {
@@ -3187,9 +3400,10 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
3187
3400
  collectionVars: updatedCollectionVars,
3188
3401
  globals: updatedGlobals,
3189
3402
  localVars,
3190
- response
3403
+ response: scriptResponse,
3404
+ piiMaskPatterns
3191
3405
  });
3192
- testResults = [...schemaTestResults, ...r.testResults];
3406
+ testResults = [...schemaTestResults, ...protocolFaultTests, ...r.testResults];
3193
3407
  consoleOutput = r.consoleOutput;
3194
3408
  postScriptError = r.error;
3195
3409
  updatedEnvVars = r.updatedEnvVars;
@@ -3202,7 +3416,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
3202
3416
  const allPassed = testResults.every((t) => t.passed);
3203
3417
  const httpFailed = fetchResp.status >= 400;
3204
3418
  const hasTests = testResults.length > 0;
3205
- const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "skipped";
3419
+ const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "passed";
3206
3420
  if (httpFailed && testResults.length === 0) {
3207
3421
  testResults = [
3208
3422
  ...testResults,
@@ -3567,70 +3781,31 @@ function registerWsHandlers(ipc) {
3567
3781
  }
3568
3782
  });
3569
3783
  }
3570
- function fetchUrl(url, headers = {}) {
3571
- return new Promise((resolve2, reject) => {
3572
- const lib = url.startsWith("https") ? https : http;
3573
- const req = lib.get(url, { headers }, (res) => {
3574
- const chunks = [];
3575
- res.on("data", (chunk) => chunks.push(chunk));
3576
- res.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
3577
- res.on("error", reject);
3578
- });
3579
- req.on("error", reject);
3580
- req.setTimeout(15e3, () => {
3581
- req.destroy();
3582
- reject(new Error("WSDL fetch timed out"));
3583
- });
3584
- });
3784
+ function isJsonContentType(ct) {
3785
+ return !!ct && /\bjson\b/i.test(ct);
3585
3786
  }
3586
- function parseWsdl(wsdlText) {
3587
- const nsMatch = wsdlText.match(/targetNamespace\s*=\s*["']([^"']+)["']/);
3588
- const targetNamespace = nsMatch ? nsMatch[1] : "";
3589
- const operationRegex = /<(?:wsdl:)?operation\s+name\s*=\s*["']([^"']+)["']/g;
3590
- const operationNames = /* @__PURE__ */ new Set();
3591
- let m;
3592
- while ((m = operationRegex.exec(wsdlText)) !== null) {
3593
- operationNames.add(m[1]);
3594
- }
3595
- const soapActionMap = {};
3596
- const bindingOpRegex = /<(?:wsdl:)?operation\s+name\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/(?:wsdl:)?operation>/g;
3597
- while ((m = bindingOpRegex.exec(wsdlText)) !== null) {
3598
- const opName = m[1];
3599
- const block = m[2];
3600
- const saMatch = block.match(/soapAction\s*=\s*["']([^"']*)["']/);
3601
- if (saMatch) soapActionMap[opName] = saMatch[1];
3602
- }
3603
- const operations = [];
3604
- for (const name of operationNames) {
3605
- const soapAction = soapActionMap[name];
3606
- const inputTemplate = buildEnvelopeTemplate(name, targetNamespace);
3607
- operations.push({ name, soapAction, inputTemplate });
3608
- }
3609
- return { operations, targetNamespace };
3610
- }
3611
- function buildEnvelopeTemplate(operationName, namespace) {
3612
- return `<?xml version="1.0" encoding="utf-8"?>
3613
- <soap:Envelope
3614
- xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
3615
- xmlns:tns="${namespace}">
3616
- <soap:Header/>
3617
- <soap:Body>
3618
- <tns:${operationName}>
3619
- <!-- Add parameters here -->
3620
- </tns:${operationName}>
3621
- </soap:Body>
3622
- </soap:Envelope>`;
3623
- }
3624
- function registerSoapHandlers(ipc) {
3625
- ipc.handle("wsdl:fetch", async (_event, url, extraHeaders = {}) => {
3626
- const wsdlText = await fetchUrl(url, extraHeaders);
3627
- return parseWsdl(wsdlText);
3628
- });
3787
+ function formatBody(body, contentType) {
3788
+ if (!body) return "";
3789
+ const trimmed = body.length > 8e3 ? body.slice(0, 8e3) + "\n… (truncated)" : body;
3790
+ if (isJsonContentType(contentType)) {
3791
+ try {
3792
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
3793
+ } catch {
3794
+ }
3795
+ }
3796
+ return trimmed;
3797
+ }
3798
+ function langForContentType(ct) {
3799
+ if (!ct) return "";
3800
+ if (/\bjson\b/i.test(ct)) return "json";
3801
+ if (/\bxml\b/i.test(ct)) return "xml";
3802
+ if (/\bhtml\b/i.test(ct)) return "html";
3803
+ return "";
3629
3804
  }
3630
3805
  function escMd(s) {
3631
3806
  return s.replace(/[|\\`*_{}[\]()#+\-.!]/g, (c) => `\\${c}`);
3632
3807
  }
3633
- function requestToMarkdown(req) {
3808
+ function requestToMarkdown(req, example) {
3634
3809
  const lines = [];
3635
3810
  const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
3636
3811
  lines.push(`#### ${methodLabel} ${escMd(req.name)}`);
@@ -3697,9 +3872,31 @@ function requestToMarkdown(req) {
3697
3872
  lines.push("```");
3698
3873
  lines.push("");
3699
3874
  }
3875
+ if (example?.sent?.body?.trim()) {
3876
+ const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
3877
+ const lang = langForContentType(ct);
3878
+ lines.push("**Example Request Body**");
3879
+ lines.push("```" + lang);
3880
+ lines.push(formatBody(example.sent.body, ct));
3881
+ lines.push("```");
3882
+ lines.push("");
3883
+ }
3884
+ if (example?.response) {
3885
+ const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
3886
+ const lang = langForContentType(ct);
3887
+ lines.push(`**Example Response** (${example.response.status})`);
3888
+ if (example.response.body?.trim()) {
3889
+ lines.push("```" + lang);
3890
+ lines.push(formatBody(example.response.body, ct));
3891
+ lines.push("```");
3892
+ } else {
3893
+ lines.push("_(empty body)_");
3894
+ }
3895
+ lines.push("");
3896
+ }
3700
3897
  return lines.join("\n");
3701
3898
  }
3702
- function folderToMarkdown(folder, requests, depth) {
3899
+ function folderToMarkdown(folder, requests, depth, examples) {
3703
3900
  const lines = [];
3704
3901
  const heading = "#".repeat(depth);
3705
3902
  if (folder.name !== "root") {
@@ -3713,11 +3910,11 @@ function folderToMarkdown(folder, requests, depth) {
3713
3910
  for (const reqId of folder.requestIds) {
3714
3911
  const req = requests[reqId];
3715
3912
  if (req) {
3716
- lines.push(requestToMarkdown(req));
3913
+ lines.push(requestToMarkdown(req, examples[reqId]));
3717
3914
  }
3718
3915
  }
3719
3916
  for (const sub of folder.folders) {
3720
- lines.push(folderToMarkdown(sub, requests, depth + 1));
3917
+ lines.push(folderToMarkdown(sub, requests, depth + 1, examples));
3721
3918
  }
3722
3919
  return lines.join("\n");
3723
3920
  }
@@ -3725,6 +3922,7 @@ function generateMarkdown(payload) {
3725
3922
  const lines = [];
3726
3923
  lines.push("# API Documentation");
3727
3924
  lines.push("");
3925
+ const examples = payload.examples ?? {};
3728
3926
  for (const { collection, requests } of payload.collections) {
3729
3927
  lines.push(`## ${escMd(collection.name)}`);
3730
3928
  lines.push("");
@@ -3732,7 +3930,7 @@ function generateMarkdown(payload) {
3732
3930
  lines.push(collection.description.trim());
3733
3931
  lines.push("");
3734
3932
  }
3735
- lines.push(folderToMarkdown(collection.rootFolder, requests, 3));
3933
+ lines.push(folderToMarkdown(collection.rootFolder, requests, 3, examples));
3736
3934
  }
3737
3935
  return lines.join("\n");
3738
3936
  }
@@ -3749,7 +3947,7 @@ const METHOD_COLORS = {
3749
3947
  OPTIONS: "#9ca3af",
3750
3948
  WS: "#22d3ee"
3751
3949
  };
3752
- function requestToHtml(req) {
3950
+ function requestToHtml(req, example) {
3753
3951
  const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
3754
3952
  const color = METHOD_COLORS[methodLabel] ?? "#9ca3af";
3755
3953
  let html = `<div class="request">`;
@@ -3787,10 +3985,23 @@ function requestToHtml(req) {
3787
3985
  } else if (mode === "soap" && req.body.soap?.envelope?.trim()) {
3788
3986
  html += `<div class="label">Body (SOAP)</div><pre><code>${escHtml(req.body.soap.envelope.trim())}</code></pre>`;
3789
3987
  }
3988
+ if (example?.sent?.body?.trim()) {
3989
+ const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
3990
+ html += `<div class="label">Example Request Body</div><pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.sent.body, ct))}</code></pre>`;
3991
+ }
3992
+ if (example?.response) {
3993
+ const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
3994
+ html += `<div class="label">Example Response (${example.response.status})</div>`;
3995
+ if (example.response.body?.trim()) {
3996
+ html += `<pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.response.body, ct))}</code></pre>`;
3997
+ } else {
3998
+ html += `<p class="desc"><em>(empty body)</em></p>`;
3999
+ }
4000
+ }
3790
4001
  html += `</div>`;
3791
4002
  return html;
3792
4003
  }
3793
- function folderToHtml(folder, requests, depth) {
4004
+ function folderToHtml(folder, requests, depth, examples) {
3794
4005
  let html = "";
3795
4006
  const tag = `h${Math.min(depth, 6)}`;
3796
4007
  if (folder.name !== "root") {
@@ -3801,21 +4012,22 @@ function folderToHtml(folder, requests, depth) {
3801
4012
  }
3802
4013
  for (const reqId of folder.requestIds) {
3803
4014
  const req = requests[reqId];
3804
- if (req) html += requestToHtml(req);
4015
+ if (req) html += requestToHtml(req, examples[reqId]);
3805
4016
  }
3806
4017
  for (const sub of folder.folders) {
3807
- html += folderToHtml(sub, requests, depth + 1);
4018
+ html += folderToHtml(sub, requests, depth + 1, examples);
3808
4019
  }
3809
4020
  return html;
3810
4021
  }
3811
4022
  function generateHtml(payload) {
3812
4023
  let body = "";
4024
+ const examples = payload.examples ?? {};
3813
4025
  for (const { collection, requests } of payload.collections) {
3814
4026
  body += `<section class="collection"><h2>${escHtml(collection.name)}</h2>`;
3815
4027
  if (collection.description?.trim()) {
3816
4028
  body += `<p class="collection-desc">${escHtml(collection.description.trim())}</p>`;
3817
4029
  }
3818
- body += folderToHtml(collection.rootFolder, requests, 3);
4030
+ body += folderToHtml(collection.rootFolder, requests, 3, examples);
3819
4031
  body += `</section>`;
3820
4032
  }
3821
4033
  return `<!DOCTYPE html>
@@ -3901,6 +4113,7 @@ async function resolveSnapshotSpec(relPath) {
3901
4113
  }
3902
4114
  function registerContractHandlers(ipc) {
3903
4115
  ipc.handle("contract:run", async (_e, payload) => {
4116
+ ipcValidate.validateContractRunPayload(payload);
3904
4117
  const { mode, requests, envVars, collectionVars = {}, requestBaseUrl } = payload;
3905
4118
  let { specUrl, specPath } = payload;
3906
4119
  if (payload.specSnapshotRelPath) {
@@ -3961,6 +4174,8 @@ function registerGitHandlers(ipc) {
3961
4174
  });
3962
4175
  ipc.handle("git:init", async () => {
3963
4176
  await git().init();
4177
+ const dir = getWorkspaceDir();
4178
+ if (dir) await ensureGitignore(dir);
3964
4179
  });
3965
4180
  ipc.handle("git:status", async () => {
3966
4181
  const result = await git().status();
@@ -4020,16 +4235,58 @@ function registerGitHandlers(ipc) {
4020
4235
  }));
4021
4236
  });
4022
4237
  ipc.handle("git:branches", async () => {
4023
- const result = await git().branch(["-a", "--format=%(refname:short)|%(objectname:short)|%(upstream:short)|%(upstream:track)"]);
4024
- return result.all.filter((name) => !name.includes("HEAD")).map((name) => ({
4025
- name: name.replace(/^remotes\//, ""),
4026
- current: name === result.current,
4027
- remote: name.startsWith("remotes/")
4028
- }));
4238
+ const raw = await git().raw([
4239
+ "for-each-ref",
4240
+ "--format=%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)",
4241
+ "refs/heads",
4242
+ "refs/remotes"
4243
+ ]);
4244
+ const current = (await git().branch()).current;
4245
+ const branches = [];
4246
+ for (const line of raw.split("\n")) {
4247
+ if (!line.trim()) continue;
4248
+ const [shortName, head, upstream, track] = line.split("|");
4249
+ if (!shortName || shortName.endsWith("/HEAD")) continue;
4250
+ const isRemote = shortName.startsWith("origin/") || shortName.includes("/");
4251
+ const looksLocal = !isRemote;
4252
+ let ahead;
4253
+ let behind;
4254
+ const aheadM = track?.match(/ahead (\d+)/);
4255
+ const behindM = track?.match(/behind (\d+)/);
4256
+ if (aheadM) ahead = Number(aheadM[1]);
4257
+ if (behindM) behind = Number(behindM[1]);
4258
+ branches.push({
4259
+ name: shortName,
4260
+ current: looksLocal && (head === "*" || shortName === current),
4261
+ remote: isRemote,
4262
+ upstream: upstream || void 0,
4263
+ ahead,
4264
+ behind
4265
+ });
4266
+ }
4267
+ return branches;
4029
4268
  });
4030
4269
  ipc.handle("git:checkout", async (_e, branch, create) => {
4031
- if (create) await git().checkoutLocalBranch(branch);
4032
- else await git().checkout(branch);
4270
+ if (create) {
4271
+ await git().checkoutLocalBranch(branch);
4272
+ return;
4273
+ }
4274
+ const m = /^([^/]+)\/(.+)$/.exec(branch);
4275
+ if (m) {
4276
+ const remote = m[1];
4277
+ const localName = m[2];
4278
+ const localList = await git().branchLocal();
4279
+ if (!localList.all.includes(localName)) {
4280
+ await git().checkoutBranch(localName, `${remote}/${localName}`);
4281
+ return;
4282
+ }
4283
+ await git().checkout(localName);
4284
+ return;
4285
+ }
4286
+ await git().checkout(branch);
4287
+ });
4288
+ ipc.handle("git:deleteBranch", async (_e, name, force = false) => {
4289
+ await git().deleteLocalBranch(name, force);
4033
4290
  });
4034
4291
  ipc.handle("git:pull", async () => {
4035
4292
  await git().pull();
@@ -4189,7 +4446,7 @@ electron.app.whenReady().then(async () => {
4189
4446
  registerMockHandlers(electron.ipcMain);
4190
4447
  registerOAuth2Handlers(electron.ipcMain);
4191
4448
  registerWsHandlers(electron.ipcMain);
4192
- registerSoapHandlers(electron.ipcMain);
4449
+ soapHandler.registerSoapHandlers(electron.ipcMain);
4193
4450
  registerDocsHandlers(electron.ipcMain);
4194
4451
  registerContractHandlers(electron.ipcMain);
4195
4452
  registerGitHandlers(electron.ipcMain);
package/out/main/mock.js CHANGED
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const mockServer = require("./chunks/mock-server-Cx-xG4pJ.js");
5
+ const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
6
6
  require("http");
7
7
  require("crypto");
8
8
  require("vm");