@testsmith/api-spector 0.2.3 → 0.2.4

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-DIsjTggj.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
  }
@@ -3567,70 +3759,31 @@ function registerWsHandlers(ipc) {
3567
3759
  }
3568
3760
  });
3569
3761
  }
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
- });
3762
+ function isJsonContentType(ct) {
3763
+ return !!ct && /\bjson\b/i.test(ct);
3585
3764
  }
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
- });
3765
+ function formatBody(body, contentType) {
3766
+ if (!body) return "";
3767
+ const trimmed = body.length > 8e3 ? body.slice(0, 8e3) + "\n… (truncated)" : body;
3768
+ if (isJsonContentType(contentType)) {
3769
+ try {
3770
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
3771
+ } catch {
3772
+ }
3773
+ }
3774
+ return trimmed;
3775
+ }
3776
+ function langForContentType(ct) {
3777
+ if (!ct) return "";
3778
+ if (/\bjson\b/i.test(ct)) return "json";
3779
+ if (/\bxml\b/i.test(ct)) return "xml";
3780
+ if (/\bhtml\b/i.test(ct)) return "html";
3781
+ return "";
3629
3782
  }
3630
3783
  function escMd(s) {
3631
3784
  return s.replace(/[|\\`*_{}[\]()#+\-.!]/g, (c) => `\\${c}`);
3632
3785
  }
3633
- function requestToMarkdown(req) {
3786
+ function requestToMarkdown(req, example) {
3634
3787
  const lines = [];
3635
3788
  const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
3636
3789
  lines.push(`#### ${methodLabel} ${escMd(req.name)}`);
@@ -3697,9 +3850,31 @@ function requestToMarkdown(req) {
3697
3850
  lines.push("```");
3698
3851
  lines.push("");
3699
3852
  }
3853
+ if (example?.sent?.body?.trim()) {
3854
+ const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
3855
+ const lang = langForContentType(ct);
3856
+ lines.push("**Example Request Body**");
3857
+ lines.push("```" + lang);
3858
+ lines.push(formatBody(example.sent.body, ct));
3859
+ lines.push("```");
3860
+ lines.push("");
3861
+ }
3862
+ if (example?.response) {
3863
+ const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
3864
+ const lang = langForContentType(ct);
3865
+ lines.push(`**Example Response** (${example.response.status})`);
3866
+ if (example.response.body?.trim()) {
3867
+ lines.push("```" + lang);
3868
+ lines.push(formatBody(example.response.body, ct));
3869
+ lines.push("```");
3870
+ } else {
3871
+ lines.push("_(empty body)_");
3872
+ }
3873
+ lines.push("");
3874
+ }
3700
3875
  return lines.join("\n");
3701
3876
  }
3702
- function folderToMarkdown(folder, requests, depth) {
3877
+ function folderToMarkdown(folder, requests, depth, examples) {
3703
3878
  const lines = [];
3704
3879
  const heading = "#".repeat(depth);
3705
3880
  if (folder.name !== "root") {
@@ -3713,11 +3888,11 @@ function folderToMarkdown(folder, requests, depth) {
3713
3888
  for (const reqId of folder.requestIds) {
3714
3889
  const req = requests[reqId];
3715
3890
  if (req) {
3716
- lines.push(requestToMarkdown(req));
3891
+ lines.push(requestToMarkdown(req, examples[reqId]));
3717
3892
  }
3718
3893
  }
3719
3894
  for (const sub of folder.folders) {
3720
- lines.push(folderToMarkdown(sub, requests, depth + 1));
3895
+ lines.push(folderToMarkdown(sub, requests, depth + 1, examples));
3721
3896
  }
3722
3897
  return lines.join("\n");
3723
3898
  }
@@ -3725,6 +3900,7 @@ function generateMarkdown(payload) {
3725
3900
  const lines = [];
3726
3901
  lines.push("# API Documentation");
3727
3902
  lines.push("");
3903
+ const examples = payload.examples ?? {};
3728
3904
  for (const { collection, requests } of payload.collections) {
3729
3905
  lines.push(`## ${escMd(collection.name)}`);
3730
3906
  lines.push("");
@@ -3732,7 +3908,7 @@ function generateMarkdown(payload) {
3732
3908
  lines.push(collection.description.trim());
3733
3909
  lines.push("");
3734
3910
  }
3735
- lines.push(folderToMarkdown(collection.rootFolder, requests, 3));
3911
+ lines.push(folderToMarkdown(collection.rootFolder, requests, 3, examples));
3736
3912
  }
3737
3913
  return lines.join("\n");
3738
3914
  }
@@ -3749,7 +3925,7 @@ const METHOD_COLORS = {
3749
3925
  OPTIONS: "#9ca3af",
3750
3926
  WS: "#22d3ee"
3751
3927
  };
3752
- function requestToHtml(req) {
3928
+ function requestToHtml(req, example) {
3753
3929
  const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
3754
3930
  const color = METHOD_COLORS[methodLabel] ?? "#9ca3af";
3755
3931
  let html = `<div class="request">`;
@@ -3787,10 +3963,23 @@ function requestToHtml(req) {
3787
3963
  } else if (mode === "soap" && req.body.soap?.envelope?.trim()) {
3788
3964
  html += `<div class="label">Body (SOAP)</div><pre><code>${escHtml(req.body.soap.envelope.trim())}</code></pre>`;
3789
3965
  }
3966
+ if (example?.sent?.body?.trim()) {
3967
+ const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
3968
+ html += `<div class="label">Example Request Body</div><pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.sent.body, ct))}</code></pre>`;
3969
+ }
3970
+ if (example?.response) {
3971
+ const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
3972
+ html += `<div class="label">Example Response (${example.response.status})</div>`;
3973
+ if (example.response.body?.trim()) {
3974
+ html += `<pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.response.body, ct))}</code></pre>`;
3975
+ } else {
3976
+ html += `<p class="desc"><em>(empty body)</em></p>`;
3977
+ }
3978
+ }
3790
3979
  html += `</div>`;
3791
3980
  return html;
3792
3981
  }
3793
- function folderToHtml(folder, requests, depth) {
3982
+ function folderToHtml(folder, requests, depth, examples) {
3794
3983
  let html = "";
3795
3984
  const tag = `h${Math.min(depth, 6)}`;
3796
3985
  if (folder.name !== "root") {
@@ -3801,21 +3990,22 @@ function folderToHtml(folder, requests, depth) {
3801
3990
  }
3802
3991
  for (const reqId of folder.requestIds) {
3803
3992
  const req = requests[reqId];
3804
- if (req) html += requestToHtml(req);
3993
+ if (req) html += requestToHtml(req, examples[reqId]);
3805
3994
  }
3806
3995
  for (const sub of folder.folders) {
3807
- html += folderToHtml(sub, requests, depth + 1);
3996
+ html += folderToHtml(sub, requests, depth + 1, examples);
3808
3997
  }
3809
3998
  return html;
3810
3999
  }
3811
4000
  function generateHtml(payload) {
3812
4001
  let body = "";
4002
+ const examples = payload.examples ?? {};
3813
4003
  for (const { collection, requests } of payload.collections) {
3814
4004
  body += `<section class="collection"><h2>${escHtml(collection.name)}</h2>`;
3815
4005
  if (collection.description?.trim()) {
3816
4006
  body += `<p class="collection-desc">${escHtml(collection.description.trim())}</p>`;
3817
4007
  }
3818
- body += folderToHtml(collection.rootFolder, requests, 3);
4008
+ body += folderToHtml(collection.rootFolder, requests, 3, examples);
3819
4009
  body += `</section>`;
3820
4010
  }
3821
4011
  return `<!DOCTYPE html>
@@ -3901,6 +4091,7 @@ async function resolveSnapshotSpec(relPath) {
3901
4091
  }
3902
4092
  function registerContractHandlers(ipc) {
3903
4093
  ipc.handle("contract:run", async (_e, payload) => {
4094
+ ipcValidate.validateContractRunPayload(payload);
3904
4095
  const { mode, requests, envVars, collectionVars = {}, requestBaseUrl } = payload;
3905
4096
  let { specUrl, specPath } = payload;
3906
4097
  if (payload.specSnapshotRelPath) {
@@ -3961,6 +4152,8 @@ function registerGitHandlers(ipc) {
3961
4152
  });
3962
4153
  ipc.handle("git:init", async () => {
3963
4154
  await git().init();
4155
+ const dir = getWorkspaceDir();
4156
+ if (dir) await ensureGitignore(dir);
3964
4157
  });
3965
4158
  ipc.handle("git:status", async () => {
3966
4159
  const result = await git().status();
@@ -4020,16 +4213,58 @@ function registerGitHandlers(ipc) {
4020
4213
  }));
4021
4214
  });
4022
4215
  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
- }));
4216
+ const raw = await git().raw([
4217
+ "for-each-ref",
4218
+ "--format=%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)",
4219
+ "refs/heads",
4220
+ "refs/remotes"
4221
+ ]);
4222
+ const current = (await git().branch()).current;
4223
+ const branches = [];
4224
+ for (const line of raw.split("\n")) {
4225
+ if (!line.trim()) continue;
4226
+ const [shortName, head, upstream, track] = line.split("|");
4227
+ if (!shortName || shortName.endsWith("/HEAD")) continue;
4228
+ const isRemote = shortName.startsWith("origin/") || shortName.includes("/");
4229
+ const looksLocal = !isRemote;
4230
+ let ahead;
4231
+ let behind;
4232
+ const aheadM = track?.match(/ahead (\d+)/);
4233
+ const behindM = track?.match(/behind (\d+)/);
4234
+ if (aheadM) ahead = Number(aheadM[1]);
4235
+ if (behindM) behind = Number(behindM[1]);
4236
+ branches.push({
4237
+ name: shortName,
4238
+ current: looksLocal && (head === "*" || shortName === current),
4239
+ remote: isRemote,
4240
+ upstream: upstream || void 0,
4241
+ ahead,
4242
+ behind
4243
+ });
4244
+ }
4245
+ return branches;
4029
4246
  });
4030
4247
  ipc.handle("git:checkout", async (_e, branch, create) => {
4031
- if (create) await git().checkoutLocalBranch(branch);
4032
- else await git().checkout(branch);
4248
+ if (create) {
4249
+ await git().checkoutLocalBranch(branch);
4250
+ return;
4251
+ }
4252
+ const m = /^([^/]+)\/(.+)$/.exec(branch);
4253
+ if (m) {
4254
+ const remote = m[1];
4255
+ const localName = m[2];
4256
+ const localList = await git().branchLocal();
4257
+ if (!localList.all.includes(localName)) {
4258
+ await git().checkoutBranch(localName, `${remote}/${localName}`);
4259
+ return;
4260
+ }
4261
+ await git().checkout(localName);
4262
+ return;
4263
+ }
4264
+ await git().checkout(branch);
4265
+ });
4266
+ ipc.handle("git:deleteBranch", async (_e, name, force = false) => {
4267
+ await git().deleteLocalBranch(name, force);
4033
4268
  });
4034
4269
  ipc.handle("git:pull", async () => {
4035
4270
  await git().pull();
@@ -4189,7 +4424,7 @@ electron.app.whenReady().then(async () => {
4189
4424
  registerMockHandlers(electron.ipcMain);
4190
4425
  registerOAuth2Handlers(electron.ipcMain);
4191
4426
  registerWsHandlers(electron.ipcMain);
4192
- registerSoapHandlers(electron.ipcMain);
4427
+ soapHandler.registerSoapHandlers(electron.ipcMain);
4193
4428
  registerDocsHandlers(electron.ipcMain);
4194
4429
  registerContractHandlers(electron.ipcMain);
4195
4430
  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");
@@ -4,7 +4,7 @@ const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const undici = require("undici");
6
6
  const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
7
- const requestCollection = require("./chunks/request-collection-DMVlm0PA.js");
7
+ const requestCollection = require("./chunks/request-collection-DIsjTggj.js");
8
8
  require("crypto");
9
9
  require("dayjs");
10
10
  require("vm");
@@ -12,6 +12,7 @@ require("tv4");
12
12
  require("jsonpath-plus");
13
13
  require("@xmldom/xmldom");
14
14
  require("ajv");
15
+ require("./chunks/ipc-validate-CscN4HfG.js");
15
16
  function buildJsonReport(results, summary, meta = {}) {
16
17
  return JSON.stringify({
17
18
  timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -267,7 +268,7 @@ function buildHtmlReport(results, summary, meta = {}) {
267
268
  const html = document.documentElement
268
269
  const isLight = html.classList.toggle('light')
269
270
  document.getElementById('themeBtn').textContent = isLight ? '🌙' : '☀️'
270
- try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {}
271
+ try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {} /* private mode / quota — non-fatal */
271
272
  }
272
273
  // Restore saved preference or respect OS preference
273
274
  (function() {
@@ -277,7 +278,7 @@ function buildHtmlReport(results, summary, meta = {}) {
277
278
  document.documentElement.classList.add('light')
278
279
  document.getElementById('themeBtn').textContent = '🌙'
279
280
  }
280
- } catch {}
281
+ } catch {} /* private mode / quota — non-fatal */
281
282
  })()
282
283
  <\/script>
283
284
  </body>