@testsmith/api-spector 0.1.0 → 0.1.2

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.
@@ -208,6 +208,13 @@ async function buildEnvVars(environment) {
208
208
  } catch {
209
209
  }
210
210
  }
211
+ if (vars[v.key] === void 0 && process.env[v.key] !== void 0) {
212
+ vars[v.key] = process.env[v.key];
213
+ }
214
+ } else if (v.secret) {
215
+ if (process.env[v.key] !== void 0) {
216
+ vars[v.key] = process.env[v.key];
217
+ }
211
218
  } else {
212
219
  vars[v.key] = v.value;
213
220
  }
package/out/main/index.js CHANGED
@@ -25,7 +25,7 @@ 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 requestHandler = require("./chunks/request-handler-9MdHOWVf.js");
28
+ const requestHandler = require("./chunks/request-handler-CvqESn11.js");
29
29
  const uuid = require("uuid");
30
30
  const jsYaml = require("js-yaml");
31
31
  const undici = require("undici");
@@ -35,6 +35,7 @@ const http = require("http");
35
35
  const WebSocket = require("ws");
36
36
  const https = require("https");
37
37
  const Ajv = require("ajv");
38
+ const simpleGit = require("simple-git");
38
39
  require("crypto");
39
40
  require("dayjs");
40
41
  require("vm");
@@ -86,7 +87,7 @@ function registerFileHandlers(ipc) {
86
87
  await requestHandler.loadGlobals(workspaceDir);
87
88
  await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
88
89
  await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
89
- const gitignore = "# api Spector — never commit secrets\n*.secrets\n.env.local\n";
90
+ const gitignore = "# api Spector — never commit secrets\n*.secrets\n.env.local\n\n# Dependencies\nnode_modules/\n";
90
91
  await atomicWrite(path.join(workspaceDir, ".gitignore"), gitignore);
91
92
  const ws = {
92
93
  version: "1.0",
@@ -3436,6 +3437,120 @@ function registerContractHandlers(ipc) {
3436
3437
  return schema ? JSON.stringify(schema, null, 2) : null;
3437
3438
  });
3438
3439
  }
3440
+ function git() {
3441
+ const dir = getWorkspaceDir();
3442
+ if (!dir) throw new Error("No workspace open");
3443
+ return simpleGit.simpleGit(dir);
3444
+ }
3445
+ function registerGitHandlers(ipc) {
3446
+ ipc.handle("git:isRepo", async () => {
3447
+ try {
3448
+ await git().revparse(["--git-dir"]);
3449
+ return true;
3450
+ } catch {
3451
+ return false;
3452
+ }
3453
+ });
3454
+ ipc.handle("git:init", async () => {
3455
+ await git().init();
3456
+ });
3457
+ ipc.handle("git:status", async () => {
3458
+ const result = await git().status();
3459
+ return {
3460
+ staged: result.staged.map((f) => ({ path: f, status: resolveStatus(result, f, true) })),
3461
+ unstaged: result.modified.filter((f) => !result.staged.includes(f)).concat(result.deleted.filter((f) => !result.staged.includes(f))).map((f) => ({ path: f, status: resolveStatus(result, f, false) })),
3462
+ untracked: result.not_added.map((f) => ({ path: f, status: "untracked" })),
3463
+ branch: result.current ?? "",
3464
+ ahead: result.ahead,
3465
+ behind: result.behind,
3466
+ remote: result.tracking ?? null
3467
+ };
3468
+ });
3469
+ ipc.handle("git:diff", async (_e, filePath) => {
3470
+ if (filePath) return git().diff(["--", filePath]);
3471
+ return git().diff();
3472
+ });
3473
+ ipc.handle("git:diffStaged", async (_e, filePath) => {
3474
+ if (filePath) return git().diff(["--cached", "--", filePath]);
3475
+ return git().diff(["--cached"]);
3476
+ });
3477
+ ipc.handle("git:stage", async (_e, paths) => {
3478
+ await git().add(paths);
3479
+ });
3480
+ ipc.handle("git:unstage", async (_e, paths) => {
3481
+ await git().reset(["HEAD", "--", ...paths]);
3482
+ });
3483
+ ipc.handle("git:stageAll", async () => {
3484
+ await git().add(["."]);
3485
+ });
3486
+ ipc.handle("git:commit", async (_e, message) => {
3487
+ await git().commit(message);
3488
+ });
3489
+ ipc.handle("git:log", async (_e, limit = 50) => {
3490
+ const result = await git().log({ maxCount: limit });
3491
+ return result.all.map((c) => ({
3492
+ hash: c.hash,
3493
+ short: c.hash.slice(0, 7),
3494
+ message: c.message,
3495
+ author: c.author_name,
3496
+ email: c.author_email,
3497
+ date: c.date
3498
+ }));
3499
+ });
3500
+ ipc.handle("git:branches", async () => {
3501
+ const result = await git().branch(["-a", "--format=%(refname:short)|%(objectname:short)|%(upstream:short)|%(upstream:track)"]);
3502
+ return result.all.filter((name) => !name.includes("HEAD")).map((name) => ({
3503
+ name: name.replace(/^remotes\//, ""),
3504
+ current: name === result.current,
3505
+ remote: name.startsWith("remotes/")
3506
+ }));
3507
+ });
3508
+ ipc.handle("git:checkout", async (_e, branch, create) => {
3509
+ if (create) await git().checkoutLocalBranch(branch);
3510
+ else await git().checkout(branch);
3511
+ });
3512
+ ipc.handle("git:pull", async () => {
3513
+ await git().pull();
3514
+ });
3515
+ ipc.handle("git:push", async (_e, setUpstream) => {
3516
+ if (setUpstream) {
3517
+ const status = await git().status();
3518
+ await git().push(["--set-upstream", "origin", status.current ?? "main"]);
3519
+ } else {
3520
+ await git().push();
3521
+ }
3522
+ });
3523
+ ipc.handle("git:remotes", async () => {
3524
+ const result = await git().getRemotes(true);
3525
+ return result.map((r) => ({ name: r.name, url: r.refs.fetch || r.refs.push || "" }));
3526
+ });
3527
+ ipc.handle("git:addRemote", async (_e, name, url) => {
3528
+ await git().addRemote(name, url);
3529
+ });
3530
+ ipc.handle("git:setRemoteUrl", async (_e, name, url) => {
3531
+ await git().remote(["set-url", name, url]);
3532
+ });
3533
+ ipc.handle("git:removeRemote", async (_e, name) => {
3534
+ await git().removeRemote(name);
3535
+ });
3536
+ ipc.handle("git:writeCiFile", async (_e, relPath, content) => {
3537
+ const wsDir = getWorkspaceDir();
3538
+ if (!wsDir) throw new Error("No workspace open");
3539
+ const fullPath = path.join(wsDir, relPath);
3540
+ await promises.mkdir(path.dirname(fullPath), { recursive: true });
3541
+ await promises.writeFile(fullPath, content, "utf8");
3542
+ });
3543
+ }
3544
+ function resolveStatus(result, filePath, staged) {
3545
+ if (staged) {
3546
+ if (result.created.includes(filePath)) return "added";
3547
+ if (result.deleted.includes(filePath)) return "deleted";
3548
+ if (result.renamed.find((r) => r.to === filePath || r.from === filePath)) return "renamed";
3549
+ return "modified";
3550
+ }
3551
+ if (result.deleted.includes(filePath)) return "deleted";
3552
+ return "modified";
3553
+ }
3439
3554
  if (process.env.ELECTRON_NO_SANDBOX === "1") {
3440
3555
  electron.app.commandLine.appendSwitch("no-sandbox");
3441
3556
  electron.app.commandLine.appendSwitch("disable-features", "RendererCodeIntegrity");
@@ -3536,6 +3651,7 @@ electron.app.whenReady().then(async () => {
3536
3651
  registerSoapHandlers(electron.ipcMain);
3537
3652
  registerDocsHandlers(electron.ipcMain);
3538
3653
  registerContractHandlers(electron.ipcMain);
3654
+ registerGitHandlers(electron.ipcMain);
3539
3655
  createWindow();
3540
3656
  electron.app.on("activate", () => {
3541
3657
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
@@ -3,7 +3,7 @@
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const undici = require("undici");
6
- const requestHandler = require("./chunks/request-handler-9MdHOWVf.js");
6
+ const requestHandler = require("./chunks/request-handler-CvqESn11.js");
7
7
  require("crypto");
8
8
  require("dayjs");
9
9
  require("vm");
@@ -35,6 +35,177 @@ function buildJsonReport(results, summary, meta = {}) {
35
35
  }))
36
36
  }, null, 2);
37
37
  }
38
+ function buildHtmlReport(results, summary, meta = {}) {
39
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
40
+ function prettyJson(s) {
41
+ try {
42
+ return esc(JSON.stringify(JSON.parse(s), null, 2));
43
+ } catch {
44
+ return esc(s);
45
+ }
46
+ }
47
+ function headersTable(h) {
48
+ const rows = Object.entries(h).map(
49
+ ([k, v]) => `<tr><td class="hk">${esc(k)}</td><td class="hv">${esc(v)}</td></tr>`
50
+ ).join("");
51
+ return rows ? `<table class="htable"><tbody>${rows}</tbody></table>` : '<span class="muted">none</span>';
52
+ }
53
+ const ts = meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
54
+ const collection = meta.collection ?? "API Tests";
55
+ const env = meta.environment ?? "—";
56
+ const passRate = summary.total > 0 ? Math.round(summary.passed / summary.total * 100) : 0;
57
+ const cards = results.map((r, idx) => {
58
+ const statusCls = r.status === "passed" ? "badge-pass" : r.status === "failed" ? "badge-fail" : "badge-err";
59
+ const httpCls = r.httpStatus && r.httpStatus < 300 ? "http-ok" : r.httpStatus && r.httpStatus < 400 ? "http-redir" : "http-err";
60
+ const dur = r.durationMs != null ? `${r.durationMs} ms` : "—";
61
+ const label = r.iterationLabel ? ` <span class="muted">#${esc(r.iterationLabel)}</span>` : "";
62
+ const testRows = (r.testResults ?? []).map(
63
+ (t) => `<div class="test-row ${t.passed ? "test-pass" : "test-fail"}">
64
+ <span class="dot">${t.passed ? "✓" : "✗"}</span> ${esc(t.name)}
65
+ ${!t.passed ? `<div class="test-err">${esc(t.error ?? "")}</div>` : ""}
66
+ </div>`
67
+ ).join("");
68
+ const errRows = [
69
+ r.error ? `<div class="err-row">&#x26a0; ${esc(r.error)}</div>` : "",
70
+ r.preScriptError ? `<div class="err-row">&#x26a0; Pre-script: ${esc(r.preScriptError)}</div>` : "",
71
+ r.postScriptError ? `<div class="err-row">&#x26a0; Post-script: ${esc(r.postScriptError)}</div>` : ""
72
+ ].filter(Boolean).join("");
73
+ const consoleHtml = (r.consoleOutput ?? []).length ? `<div class="section-label">Console</div>
74
+ <div class="code-block">${(r.consoleOutput ?? []).map((l) => `<div>${esc(l)}</div>`).join("")}</div>` : "";
75
+ const reqHeaders = r.sentRequest?.headers ?? {};
76
+ const reqBody = r.sentRequest?.body;
77
+ const reqHtml = `
78
+ <div class="panel-label">Request</div>
79
+ <div class="panel req-panel">
80
+ <div class="req-line"><span class="method-badge">${esc(r.method)}</span> <span class="mono">${esc(r.resolvedUrl ?? "")}</span></div>
81
+ <div class="section-label">Headers</div>
82
+ ${headersTable(reqHeaders)}
83
+ ${reqBody ? `<div class="section-label">Body</div><pre class="code-block">${prettyJson(reqBody)}</pre>` : ""}
84
+ </div>`;
85
+ const resp = r.receivedResponse;
86
+ const respHtml = resp ? `
87
+ <div class="panel-label">Response</div>
88
+ <div class="panel resp-panel">
89
+ <div class="resp-status ${httpCls}">${resp.status} ${esc(resp.statusText)}</div>
90
+ <div class="section-label">Headers</div>
91
+ ${headersTable(resp.headers)}
92
+ ${resp.body ? `<div class="section-label">Body</div><pre class="code-block">${prettyJson(resp.body)}</pre>` : ""}
93
+ </div>` : "";
94
+ return `
95
+ <div class="card" id="r${idx}">
96
+ <div class="card-header" onclick="toggle(${idx})">
97
+ <span class="chevron" id="ch${idx}">▶</span>
98
+ <span class="badge ${statusCls}">${r.status}</span>
99
+ <span class="method mono">${esc(r.method)}</span>
100
+ <span class="card-name">${esc(r.name)}${label}</span>
101
+ <span class="card-url muted">${esc(r.resolvedUrl ?? "")}</span>
102
+ <span class="dur muted">${dur}</span>
103
+ ${r.httpStatus ? `<span class="http-badge ${httpCls}">${r.httpStatus}</span>` : ""}
104
+ </div>
105
+ <div class="card-body" id="cb${idx}" style="display:none">
106
+ ${errRows}
107
+ ${testRows ? `<div class="section-label">Tests</div><div class="tests-wrap">${testRows}</div>` : ""}
108
+ ${consoleHtml}
109
+ <div class="req-resp-grid">
110
+ ${reqHtml}
111
+ ${respHtml}
112
+ </div>
113
+ </div>
114
+ </div>`;
115
+ }).join("\n");
116
+ return `<!DOCTYPE html>
117
+ <html lang="en">
118
+ <head>
119
+ <meta charset="UTF-8">
120
+ <meta name="viewport" content="width=device-width, initial-scale=1">
121
+ <title>${esc(collection)} — Test Results</title>
122
+ <style>
123
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
124
+ body { font-family: system-ui, sans-serif; background: #0f1117; color: #c9d1d9; font-size: 13px; line-height: 1.5; }
125
+ .wrap { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
126
+ h1 { font-size: 20px; font-weight: 600; color: #e6edf3; margin-bottom: 4px; }
127
+ .meta-line { color: #8b949e; font-size: 11px; margin-bottom: 24px; }
128
+ .summary { display: flex; gap: 12px; margin-bottom: 28px; flex-wrap: wrap; }
129
+ .stat { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 20px; min-width: 90px; }
130
+ .stat-val { font-size: 22px; font-weight: 700; color: #e6edf3; }
131
+ .stat-lbl { font-size: 11px; color: #8b949e; margin-top: 2px; }
132
+ .stat-pass .stat-val { color: #3fb950; }
133
+ .stat-fail .stat-val { color: #f85149; }
134
+ .stat-err .stat-val { color: #d29922; }
135
+ /* Cards */
136
+ .card { border: 1px solid #21262d; border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
137
+ .card-header { display: flex; align-items: baseline; gap: 8px; padding: 10px 14px; cursor: pointer; user-select: none; }
138
+ .card-header:hover { background: #161b22; }
139
+ .chevron { font-size: 10px; color: #8b949e; min-width: 10px; transition: transform .15s; }
140
+ .card-name { font-weight: 500; color: #e6edf3; white-space: nowrap; }
141
+ .card-url { font-family: monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
142
+ .card-body { padding: 12px 14px; border-top: 1px solid #21262d; display: flex; flex-direction: column; gap: 10px; }
143
+ /* Badges */
144
+ .badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap; }
145
+ .badge-pass { background: #0d3a1e; color: #3fb950; }
146
+ .badge-fail { background: #3d1014; color: #f85149; }
147
+ .badge-err { background: #3d2a00; color: #d29922; }
148
+ .method { font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; white-space: nowrap; }
149
+ .method-badge { display: inline-block; font-family: monospace; font-size: 11px; font-weight: 700; color: #79c0ff; min-width: 52px; }
150
+ .http-badge { font-family: monospace; font-size: 11px; font-weight: 600; white-space: nowrap; }
151
+ .http-ok { color: #3fb950; }
152
+ .http-redir { color: #79c0ff; }
153
+ .http-err { color: #f85149; }
154
+ .dur { font-family: monospace; font-size: 11px; white-space: nowrap; }
155
+ .muted { color: #8b949e; }
156
+ .mono { font-family: monospace; font-size: 12px; }
157
+ /* Request / Response */
158
+ .req-resp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
159
+ @media (max-width: 700px) { .req-resp-grid { grid-template-columns: 1fr; } }
160
+ .panel-label { font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
161
+ .panel { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
162
+ .req-line { font-family: monospace; font-size: 12px; color: #c9d1d9; word-break: break-all; }
163
+ .resp-status { font-family: monospace; font-size: 13px; font-weight: 700; }
164
+ .section-label { font-size: 10px; font-weight: 600; color: #8b949e; text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
165
+ .htable { width: 100%; border-collapse: collapse; }
166
+ .htable td { font-family: monospace; font-size: 11px; padding: 1px 0; vertical-align: top; }
167
+ .hk { color: #79c0ff; padding-right: 12px; white-space: nowrap; }
168
+ .hv { color: #c9d1d9; word-break: break-all; }
169
+ .code-block { font-family: monospace; font-size: 11px; color: #c9d1d9; background: #0d1117; border: 1px solid #21262d; border-radius: 4px; padding: 8px; white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; }
170
+ /* Tests */
171
+ .section-label { font-size: 11px; font-weight: 600; color: #8b949e; }
172
+ .tests-wrap { display: flex; flex-direction: column; gap: 2px; }
173
+ .test-row { font-size: 12px; display: flex; flex-wrap: wrap; gap: 4px; }
174
+ .test-pass { color: #3fb950; }
175
+ .test-fail { color: #f85149; }
176
+ .test-err { color: #8b949e; padding-left: 16px; width: 100%; font-family: monospace; font-size: 11px; }
177
+ .dot { font-weight: 700; }
178
+ /* Console */
179
+ .err-row { color: #f85149; font-size: 12px; }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <div class="wrap">
184
+ <h1>${esc(collection)}</h1>
185
+ <div class="meta-line">Environment: ${esc(env)} &nbsp;·&nbsp; ${esc(ts)}</div>
186
+ <div class="summary">
187
+ <div class="stat"><div class="stat-val">${summary.total}</div><div class="stat-lbl">Total</div></div>
188
+ <div class="stat stat-pass"><div class="stat-val">${summary.passed}</div><div class="stat-lbl">Passed</div></div>
189
+ <div class="stat stat-fail"><div class="stat-val">${summary.failed}</div><div class="stat-lbl">Failed</div></div>
190
+ <div class="stat stat-err"><div class="stat-val">${summary.errors}</div><div class="stat-lbl">Errors</div></div>
191
+ <div class="stat"><div class="stat-val">${passRate}%</div><div class="stat-lbl">Pass rate</div></div>
192
+ <div class="stat"><div class="stat-val">${summary.durationMs} ms</div><div class="stat-lbl">Duration</div></div>
193
+ </div>
194
+ <div class="cards">${cards}</div>
195
+ </div>
196
+ <script>
197
+ function toggle(i) {
198
+ const body = document.getElementById('cb' + i)
199
+ const ch = document.getElementById('ch' + i)
200
+ const open = body.style.display !== 'none'
201
+ body.style.display = open ? 'none' : 'block'
202
+ ch.style.transform = open ? '' : 'rotate(90deg)'
203
+ }
204
+ <\/script>
205
+ </body>
206
+ </html>
207
+ `;
208
+ }
38
209
  function buildJUnitReport(results, summary, meta = {}) {
39
210
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
40
211
  const suiteName = esc(meta.collection ?? "API Tests");
@@ -123,9 +294,18 @@ function parseArgs(argv) {
123
294
  }
124
295
  return args;
125
296
  }
297
+ async function resolveWorkspacePath(wsPath) {
298
+ const s = await promises.stat(wsPath);
299
+ if (!s.isDirectory()) return wsPath;
300
+ const entries = await promises.readdir(wsPath);
301
+ const spector = entries.find((e) => e.endsWith(".spector"));
302
+ if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
303
+ return path.join(wsPath, spector);
304
+ }
126
305
  async function loadWorkspace(wsPath) {
127
- const raw = await promises.readFile(wsPath, "utf8");
128
- return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(wsPath)) };
306
+ const resolved = await resolveWorkspacePath(wsPath);
307
+ const raw = await promises.readFile(resolved, "utf8");
308
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)) };
129
309
  }
130
310
  async function loadCollections(workspace, dir) {
131
311
  const cols = [];
@@ -258,8 +438,24 @@ async function executeRequest(req, collectionVars, envVars, globals, verbose, tl
258
438
  if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [post] ${l}`, C.gray)));
259
439
  }
260
440
  const allPassed = testResults.every((t) => t.passed);
261
- const status = postScriptError ? "error" : testResults.length > 0 ? allPassed ? "passed" : "failed" : "passed";
262
- return { ...base, status, httpStatus: fetchResp.status, durationMs, testResults, consoleOutput, preScriptError, postScriptError };
441
+ const httpOk = fetchResp.status < 400;
442
+ const status = postScriptError ? "error" : testResults.length > 0 ? allPassed ? "passed" : "failed" : httpOk ? "passed" : "failed";
443
+ const reqHeaders = {};
444
+ headers.forEach((v, k) => {
445
+ reqHeaders[k] = v;
446
+ });
447
+ return {
448
+ ...base,
449
+ status,
450
+ httpStatus: fetchResp.status,
451
+ durationMs,
452
+ testResults,
453
+ consoleOutput,
454
+ preScriptError,
455
+ postScriptError,
456
+ sentRequest: { headers: reqHeaders, body },
457
+ receivedResponse: response
458
+ };
263
459
  } catch (err) {
264
460
  return {
265
461
  ...base,
@@ -289,7 +485,7 @@ async function main() {
289
485
  const args = parseArgs(process.argv.slice(2));
290
486
  if (args.help) {
291
487
  console.log(
292
- "\nUsage:\n api-spector run --workspace <path> [--env <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit]\n [--verbose] [--bail]\n"
488
+ "\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit]\n [--verbose] [--bail]\n"
293
489
  );
294
490
  process.exit(0);
295
491
  }
@@ -299,13 +495,14 @@ async function main() {
299
495
  process.exit(1);
300
496
  }
301
497
  const filterTags = args.tags ? args.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
302
- const envName = args.env;
498
+ const envName = args.environment ?? args.env;
303
499
  const colName = args.collection;
304
500
  const verbose = Boolean(args.verbose);
305
501
  const bail = Boolean(args.bail);
306
502
  const outputPath = args.output;
307
- const inferredFormat = outputPath && path.extname(outputPath).toLowerCase() === ".xml" ? "junit" : "json";
308
- const outputFormat = args.format?.toLowerCase() === "junit" ? "junit" : inferredFormat;
503
+ const inferredFormat = outputPath ? path.extname(outputPath).toLowerCase() === ".xml" ? "junit" : path.extname(outputPath).toLowerCase() === ".html" ? "html" : "json" : "json";
504
+ const explicitFormat = args.format?.toLowerCase();
505
+ const outputFormat = explicitFormat === "junit" || explicitFormat === "html" ? explicitFormat : inferredFormat;
309
506
  let workspace, wsDir;
310
507
  try {
311
508
  ;
@@ -327,6 +524,26 @@ async function main() {
327
524
  console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
328
525
  if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
329
526
  console.log("");
527
+ const envVarsSnapshot = await requestHandler.buildEnvVars(env);
528
+ const secretValuesToMask = (env?.variables ?? []).filter((v) => v.secret && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
529
+ function redact(s) {
530
+ let out = s;
531
+ for (const secret of secretValuesToMask) out = out.split(secret).join("***");
532
+ return out;
533
+ }
534
+ function maskResult(r) {
535
+ return {
536
+ ...r,
537
+ sentRequest: r.sentRequest ? {
538
+ headers: Object.fromEntries(Object.entries(r.sentRequest.headers).map(([k, v]) => [k, redact(v)])),
539
+ body: r.sentRequest.body != null ? redact(r.sentRequest.body) : void 0
540
+ } : void 0,
541
+ receivedResponse: r.receivedResponse ? {
542
+ ...r.receivedResponse,
543
+ body: redact(r.receivedResponse.body)
544
+ } : void 0
545
+ };
546
+ }
330
547
  const summary = { total: 0, passed: 0, failed: 0, errors: 0, durationMs: 0 };
331
548
  const allResults = [];
332
549
  const totalStart = Date.now();
@@ -369,7 +586,8 @@ async function main() {
369
586
  `);
370
587
  if (outputPath) {
371
588
  const meta = { workspace: wsPath, environment: env?.name ?? null, collection: firstColName, timestamp };
372
- const report = outputFormat === "junit" ? buildJUnitReport(allResults, summary, meta) : buildJsonReport(allResults, summary, meta);
589
+ const maskedResults = allResults.map(maskResult);
590
+ const report = outputFormat === "junit" ? buildJUnitReport(maskedResults, summary, meta) : outputFormat === "html" ? buildHtmlReport(maskedResults, summary, meta) : buildJsonReport(maskedResults, summary, meta);
373
591
  await promises.writeFile(path.resolve(outputPath), report, "utf8");
374
592
  console.log(color(` Report written: ${outputPath} (${outputFormat})
375
593
  `, C.gray));
@@ -81,6 +81,26 @@ const api = {
81
81
  inferContractSchema: (jsonBody) => electron.ipcRenderer.invoke("contract:inferSchema", jsonBody),
82
82
  // ─── Script hooks ─────────────────────────────────────────────────────────
83
83
  runScriptHook: (payload) => electron.ipcRenderer.invoke("script:run-hook", payload),
84
+ // ─── Git ──────────────────────────────────────────────────────────────────
85
+ gitIsRepo: () => electron.ipcRenderer.invoke("git:isRepo"),
86
+ gitInit: () => electron.ipcRenderer.invoke("git:init"),
87
+ gitStatus: () => electron.ipcRenderer.invoke("git:status"),
88
+ gitDiff: (filePath) => electron.ipcRenderer.invoke("git:diff", filePath),
89
+ gitDiffStaged: (filePath) => electron.ipcRenderer.invoke("git:diffStaged", filePath),
90
+ gitStage: (paths) => electron.ipcRenderer.invoke("git:stage", paths),
91
+ gitUnstage: (paths) => electron.ipcRenderer.invoke("git:unstage", paths),
92
+ gitStageAll: () => electron.ipcRenderer.invoke("git:stageAll"),
93
+ gitCommit: (message) => electron.ipcRenderer.invoke("git:commit", message),
94
+ gitLog: (limit) => electron.ipcRenderer.invoke("git:log", limit),
95
+ gitBranches: () => electron.ipcRenderer.invoke("git:branches"),
96
+ gitCheckout: (branch, create) => electron.ipcRenderer.invoke("git:checkout", branch, create),
97
+ gitPull: () => electron.ipcRenderer.invoke("git:pull"),
98
+ gitPush: (setUpstream) => electron.ipcRenderer.invoke("git:push", setUpstream),
99
+ gitRemotes: () => electron.ipcRenderer.invoke("git:remotes"),
100
+ gitAddRemote: (name, url) => electron.ipcRenderer.invoke("git:addRemote", name, url),
101
+ gitSetRemoteUrl: (name, url) => electron.ipcRenderer.invoke("git:setRemoteUrl", name, url),
102
+ gitRemoveRemote: (name) => electron.ipcRenderer.invoke("git:removeRemote", name),
103
+ gitWriteCiFile: (relPath, content) => electron.ipcRenderer.invoke("git:writeCiFile", relPath, content),
84
104
  // ─── Zoom ─────────────────────────────────────────────────────────────────
85
105
  setZoomFactor: (factor) => electron.webFrame.setZoomFactor(factor),
86
106
  // ─── Platform ─────────────────────────────────────────────────────────────