@testsmith/api-spector 0.3.1 → 0.3.3

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
@@ -22,11 +22,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  const electron = require("electron");
25
+ const handle = require("./chunks/handle-C0IQL-Vl.js");
25
26
  const path = require("path");
26
27
  const fs = require("fs");
27
28
  const promises = require("fs/promises");
28
- const requestCollection = require("./chunks/request-collection-d2vgYls1.js");
29
- const authBuilder = require("./chunks/auth-builder-CRQayp8x.js");
29
+ const requestCollection = require("./chunks/request-collection-8TOVNXE0.js");
30
+ const authBuilder = require("./chunks/auth-builder-CUs9yzOF.js");
31
+ const ipcValidate = require("./chunks/ipc-validate-k6KI8adf.js");
30
32
  const uuid = require("uuid");
31
33
  const jsYaml = require("js-yaml");
32
34
  const undici = require("undici");
@@ -34,11 +36,10 @@ const JSZip = require("jszip");
34
36
  const mockServer = require("./chunks/mock-server-DSLR2ulH.js");
35
37
  const http = require("http");
36
38
  const WebSocket = require("ws");
37
- const soapHandler = require("./chunks/soap-handler-Cpj-JwyA.js");
39
+ const soapHandler = require("./chunks/soap-handler-B9x_YCtj.js");
38
40
  const os = require("os");
39
41
  const crypto = require("crypto");
40
- const snapshots = require("./chunks/snapshots-CQliv7WB.js");
41
- const ipcValidate = require("./chunks/ipc-validate-CscN4HfG.js");
42
+ const snapshots = require("./chunks/snapshots-UFd3XgSS.js");
42
43
  const simpleGit = require("simple-git");
43
44
  const recorder = require("./chunks/recorder-DFxJgn9c.js");
44
45
  require("vm");
@@ -211,7 +212,7 @@ function defaultWorkspaceName() {
211
212
  return "my-workspace.spector";
212
213
  }
213
214
  function registerFileHandlers(ipc) {
214
- ipc.handle("file:openWorkspace", async () => {
215
+ handle.handleIpc(ipc, handle.IPC.file.openWorkspace, async () => {
215
216
  const result = await electron.dialog.showOpenDialog({
216
217
  title: "Open Workspace",
217
218
  defaultPath: dialogStartDir(),
@@ -227,7 +228,7 @@ function registerFileHandlers(ipc) {
227
228
  const raw = await promises.readFile(wsPath, "utf8");
228
229
  return { workspace: JSON.parse(raw), workspacePath: wsPath };
229
230
  });
230
- ipc.handle("file:newWorkspace", async () => {
231
+ handle.handleIpc(ipc, handle.IPC.file.newWorkspace, async () => {
231
232
  const startDir = dialogStartDir();
232
233
  const defaultPath = startDir ? path.join(startDir, defaultWorkspaceName()) : defaultWorkspaceName();
233
234
  const result = await electron.dialog.showSaveDialog({
@@ -254,34 +255,34 @@ function registerFileHandlers(ipc) {
254
255
  await saveLastWorkspacePath(result.filePath);
255
256
  return { workspace: ws, workspacePath: result.filePath };
256
257
  });
257
- ipc.handle("file:saveWorkspace", async (_e, ws) => {
258
+ handle.handleIpc(ipc, handle.IPC.file.saveWorkspace, async (_e, ws) => {
258
259
  if (!workspaceFile) return;
259
260
  await atomicWrite(workspaceFile, JSON.stringify(ws, null, 2));
260
261
  if (workspaceDir) await ensureVscodeFileAssociation(workspaceDir);
261
262
  });
262
- ipc.handle("file:loadCollection", async (_e, relPath) => {
263
+ handle.handleIpc(ipc, handle.IPC.file.loadCollection, async (_e, relPath) => {
263
264
  if (!workspaceDir) throw new Error("No workspace open");
264
265
  const raw = await promises.readFile(path.resolve(workspaceDir, relPath), "utf8");
265
266
  return JSON.parse(raw);
266
267
  });
267
- ipc.handle("file:saveCollection", async (_e, relPath, col) => {
268
+ handle.handleIpc(ipc, handle.IPC.file.saveCollection, async (_e, relPath, col) => {
268
269
  if (!workspaceDir) throw new Error("No workspace open");
269
270
  const fullPath = path.resolve(workspaceDir, relPath);
270
271
  await promises.mkdir(path.dirname(fullPath), { recursive: true });
271
272
  await atomicWrite(fullPath, JSON.stringify(col, null, 2));
272
273
  });
273
- ipc.handle("file:loadEnvironment", async (_e, relPath) => {
274
+ handle.handleIpc(ipc, handle.IPC.file.loadEnvironment, async (_e, relPath) => {
274
275
  if (!workspaceDir) throw new Error("No workspace open");
275
276
  const raw = await promises.readFile(path.resolve(workspaceDir, relPath), "utf8");
276
277
  return JSON.parse(raw);
277
278
  });
278
- ipc.handle("file:saveEnvironment", async (_e, relPath, env) => {
279
+ handle.handleIpc(ipc, handle.IPC.file.saveEnvironment, async (_e, relPath, env) => {
279
280
  if (!workspaceDir) throw new Error("No workspace open");
280
281
  const fullPath = path.resolve(workspaceDir, relPath);
281
282
  await promises.mkdir(path.dirname(fullPath), { recursive: true });
282
283
  await atomicWrite(fullPath, JSON.stringify(env, null, 2));
283
284
  });
284
- ipc.handle("file:deleteWorkspaceFile", async (_e, relPath) => {
285
+ handle.handleIpc(ipc, handle.IPC.file.deleteWorkspaceFile, async (_e, relPath) => {
285
286
  if (!workspaceDir) throw new Error("No workspace open");
286
287
  const fullPath = path.resolve(workspaceDir, relPath);
287
288
  if (!fullPath.startsWith(path.resolve(workspaceDir) + (process.platform === "win32" ? "\\" : "/"))) {
@@ -293,14 +294,14 @@ function registerFileHandlers(ipc) {
293
294
  if (err.code !== "ENOENT") throw err;
294
295
  }
295
296
  });
296
- ipc.handle("dialog:pickDir", async () => {
297
+ handle.handleIpc(ipc, handle.IPC.dialog.pickDir, async () => {
297
298
  const result = await electron.dialog.showOpenDialog({
298
299
  title: "Select Output Directory",
299
300
  properties: ["openDirectory", "createDirectory"]
300
301
  });
301
302
  return result.canceled ? null : result.filePaths[0];
302
303
  });
303
- ipc.handle("results:save", async (_e, content, defaultName) => {
304
+ handle.handleIpc(ipc, handle.IPC.results.save, async (_e, content, defaultName) => {
304
305
  const ext = defaultName.endsWith(".xml") ? "xml" : defaultName.endsWith(".html") ? "html" : "json";
305
306
  const allFilters = [
306
307
  { name: "JSON", extensions: ["json"] },
@@ -316,19 +317,19 @@ function registerFileHandlers(ipc) {
316
317
  await promises.writeFile(result.filePath, content, "utf8");
317
318
  return true;
318
319
  });
319
- ipc.handle("globals:get", () => requestCollection.getGlobals());
320
- ipc.handle("globals:set", async (_e, patch) => {
320
+ handle.handleIpc(ipc, handle.IPC.globals.get, () => requestCollection.getGlobals());
321
+ handle.handleIpc(ipc, handle.IPC.globals.set, async (_e, patch) => {
321
322
  requestCollection.setGlobals(patch);
322
323
  await requestCollection.persistGlobals();
323
324
  });
324
- ipc.handle("file:closeWorkspace", async () => {
325
+ handle.handleIpc(ipc, handle.IPC.file.closeWorkspace, async () => {
325
326
  workspaceDir = null;
326
327
  workspaceFile = null;
327
328
  await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch((err) => {
328
329
  console.warn("file-handler: could not clear last-workspace pointer", err);
329
330
  });
330
331
  });
331
- ipc.handle("file:getLastWorkspace", async () => {
332
+ handle.handleIpc(ipc, handle.IPC.file.getLastWorkspace, async () => {
332
333
  const cwd = process.env.API_SPECTOR_LAUNCH_CWD;
333
334
  if (cwd) {
334
335
  const fromCwd = await tryOpenWorkspaceInDir(cwd);
@@ -372,6 +373,255 @@ async function tryOpenWorkspaceInDir(dir) {
372
373
  function getWorkspaceDir() {
373
374
  return workspaceDir;
374
375
  }
376
+ function asObject(value) {
377
+ return value && typeof value === "object" ? value : null;
378
+ }
379
+ function readStringField(obj, key) {
380
+ const value = obj?.[key];
381
+ return typeof value === "string" && value ? value : void 0;
382
+ }
383
+ function safeProxySummary(proxy) {
384
+ if (!proxy?.url?.trim()) return "off";
385
+ try {
386
+ const normalized = requestCollection.buildProxyUri({ url: proxy.url });
387
+ const parsed = new URL(normalized);
388
+ const host = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
389
+ const auth = proxy.auth ? "yes" : "no";
390
+ return `${parsed.protocol}//${host} auth=${auth}`;
391
+ } catch {
392
+ return `invalid input "${proxy.url}"`;
393
+ }
394
+ }
395
+ function safeTlsSummary(tls) {
396
+ if (!tls) return "off";
397
+ const parts = [];
398
+ if (tls.rejectUnauthorized !== void 0) parts.push(`rejectUnauthorized=${String(tls.rejectUnauthorized)}`);
399
+ if (tls.caCertPath) parts.push(`ca=${tls.caCertPath}`);
400
+ if (tls.clientCertPath) parts.push(`cert=${tls.clientCertPath}`);
401
+ if (tls.clientKeyPath) parts.push(`key=${tls.clientKeyPath}`);
402
+ return parts.length ? parts.join(", ") : "on";
403
+ }
404
+ function formatRequestError(err, context) {
405
+ const obj = asObject(err);
406
+ const message = err instanceof Error ? err.message : String(err);
407
+ const code = readStringField(obj, "code");
408
+ const stack = err instanceof Error ? err.stack : void 0;
409
+ const causeObj = obj ? asObject(obj["cause"]) : null;
410
+ const causeMessage = readStringField(causeObj, "message");
411
+ const causeCode = readStringField(causeObj, "code");
412
+ const lines = [
413
+ `[request:send] ${context.method} ${context.resolvedUrl}`,
414
+ `[request:send] requestId=${context.requestId}`,
415
+ `[request:send] proxy=${safeProxySummary(context.proxy)}`,
416
+ `[request:send] tls=${safeTlsSummary(context.tls)}`,
417
+ `[request:send] error=${message}${code ? ` (code=${code})` : ""}`
418
+ ];
419
+ if (causeMessage) {
420
+ lines.push(`[request:send] cause=${causeMessage}${causeCode ? ` (code=${causeCode})` : ""}`);
421
+ }
422
+ if (stack) {
423
+ const preview = stack.split("\n").slice(0, 6).join("\n");
424
+ lines.push("[request:send] stack:");
425
+ lines.push(preview);
426
+ }
427
+ return lines.join("\n");
428
+ }
429
+ function registerRequestHandler(ipc) {
430
+ handle.handleIpc(ipc, handle.IPC.request.send, async (_e, payload) => {
431
+ ipcValidate.validateSendRequestPayload(payload);
432
+ const {
433
+ request: req,
434
+ environment,
435
+ collectionVars,
436
+ globals: payloadGlobals,
437
+ proxy,
438
+ tls,
439
+ piiMaskPatterns = []
440
+ } = payload;
441
+ requestCollection.applyRequestDefaults(req);
442
+ const start = Date.now();
443
+ const liveGlobals = requestCollection.getGlobals();
444
+ const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
445
+ const envVars = await authBuilder.buildEnvVars(environment);
446
+ let localVars = {};
447
+ const decryptionWarnings = [];
448
+ if (environment) {
449
+ const masterKeySet = Boolean(process.env["API_SPECTOR_MASTER_KEY"]);
450
+ for (const v of environment.variables) {
451
+ if (!v.enabled || !v.secret || !v.secretEncrypted) continue;
452
+ if (!masterKeySet) {
453
+ decryptionWarnings.push(`[warn] Secret "${v.key}" was not decrypted: API_SPECTOR_MASTER_KEY is not set. Use the master password modal or export the variable in your shell.`);
454
+ } else if (envVars[v.key] === void 0) {
455
+ decryptionWarnings.push(`[warn] Secret "${v.key}" could not be decrypted: wrong password or corrupted data.`);
456
+ }
457
+ }
458
+ }
459
+ const dynamicVars = await authBuilder.buildDynamicVars();
460
+ let vars = authBuilder.mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars);
461
+ let preScriptMeta = { consoleOutput: [] };
462
+ let updatedCollectionVars = { ...collectionVars };
463
+ let updatedEnvVars = { ...envVars };
464
+ let updatedGlobals = { ...mergedGlobals };
465
+ if (req.preRequestScript?.trim()) {
466
+ const result = await requestCollection.runScript(authBuilder.interpolate(req.preRequestScript, vars), {
467
+ envVars: { ...envVars },
468
+ collectionVars: { ...collectionVars },
469
+ globals: { ...mergedGlobals },
470
+ localVars: {}
471
+ });
472
+ preScriptMeta = { error: result.error, consoleOutput: result.consoleOutput };
473
+ localVars = result.updatedLocalVars;
474
+ updatedEnvVars = result.updatedEnvVars;
475
+ updatedCollectionVars = result.updatedCollectionVars;
476
+ updatedGlobals = result.updatedGlobals;
477
+ requestCollection.patchGlobals(result.updatedGlobals);
478
+ await requestCollection.persistGlobals();
479
+ vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
480
+ }
481
+ let response;
482
+ let scriptResponse;
483
+ let sentRequest = { method: req.method, url: "", headers: {} };
484
+ const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
485
+ const secretValues = /* @__PURE__ */ new Set();
486
+ if (environment) {
487
+ for (const v of environment.variables) {
488
+ if (!v.enabled) continue;
489
+ if ((v.secret || v.envRef) && envVars[v.key]) {
490
+ secretValues.add(envVars[v.key]);
491
+ }
492
+ }
493
+ }
494
+ function redactSecrets(s) {
495
+ if (!secretValues.size) return s;
496
+ let result = s;
497
+ for (const secret of secretValues) {
498
+ if (secret) result = result.split(secret).join("[*****]");
499
+ }
500
+ return result;
501
+ }
502
+ function redactSentRequest(sr) {
503
+ const headers = {};
504
+ for (const [k, v] of Object.entries(sr.headers)) {
505
+ headers[k] = redactSecrets(v);
506
+ }
507
+ return {
508
+ method: sr.method,
509
+ url: redactSecrets(sr.url),
510
+ headers,
511
+ body: sr.body !== void 0 ? redactSecrets(sr.body) : void 0
512
+ };
513
+ }
514
+ try {
515
+ const dispatcher = await requestCollection.buildDispatcher(proxy, tls);
516
+ const exchange = await requestCollection.performHttpExchange({
517
+ req,
518
+ vars,
519
+ resolvedUrl,
520
+ dispatcher,
521
+ proxy,
522
+ tls,
523
+ onSent: (sent) => {
524
+ sentRequest = sent;
525
+ }
526
+ });
527
+ const maskedBody = requestCollection.maskPii(exchange.responseBody, piiMaskPatterns);
528
+ const maskedHeaders = requestCollection.maskHeaders(exchange.rawHeaders, piiMaskPatterns);
529
+ const bodySize = Buffer.byteLength(exchange.responseBody, "utf8");
530
+ response = {
531
+ status: exchange.status,
532
+ statusText: exchange.statusText,
533
+ headers: maskedHeaders,
534
+ body: maskedBody,
535
+ bodySize,
536
+ durationMs: exchange.durationMs
537
+ };
538
+ scriptResponse = {
539
+ status: exchange.status,
540
+ statusText: exchange.statusText,
541
+ headers: exchange.rawHeaders,
542
+ body: exchange.responseBody,
543
+ bodySize,
544
+ durationMs: exchange.durationMs
545
+ };
546
+ } catch (err) {
547
+ const diagnostic = formatRequestError(err, {
548
+ requestId: req.id,
549
+ method: req.method,
550
+ resolvedUrl,
551
+ proxy,
552
+ tls
553
+ });
554
+ console.error(diagnostic);
555
+ response = {
556
+ status: 0,
557
+ statusText: "Error",
558
+ headers: {},
559
+ body: "",
560
+ bodySize: 0,
561
+ durationMs: Date.now() - start,
562
+ error: diagnostic
563
+ };
564
+ scriptResponse = response;
565
+ }
566
+ const schemaTestResults = !response.error ? requestCollection.buildSchemaTestResults(req.schema, scriptResponse.body) : [];
567
+ let postTestResults = [];
568
+ let postConsole = [];
569
+ let postError;
570
+ if (req.postRequestScript?.trim() && !response.error) {
571
+ const result = await requestCollection.runScript(authBuilder.interpolate(req.postRequestScript, vars), {
572
+ envVars: { ...updatedEnvVars },
573
+ collectionVars: { ...updatedCollectionVars },
574
+ globals: { ...updatedGlobals },
575
+ localVars: { ...localVars },
576
+ // Pass the *unmasked* response so the script can extract real values
577
+ // (tokens, ids, …). The displayed `response` keeps the redacted copy.
578
+ response: scriptResponse
579
+ });
580
+ postTestResults = result.testResults;
581
+ postConsole = result.consoleOutput;
582
+ postError = result.error;
583
+ updatedEnvVars = result.updatedEnvVars;
584
+ updatedCollectionVars = result.updatedCollectionVars;
585
+ updatedGlobals = result.updatedGlobals;
586
+ localVars = result.updatedLocalVars;
587
+ requestCollection.patchGlobals(result.updatedGlobals);
588
+ await requestCollection.persistGlobals();
589
+ }
590
+ const combinedTestResults = [...schemaTestResults, ...postTestResults];
591
+ if (!response.error && response.status >= 400 && combinedTestResults.length === 0) {
592
+ combinedTestResults.push({
593
+ name: `HTTP status ${response.status} ${response.statusText}`.trim(),
594
+ passed: false,
595
+ error: `Request returned ${response.status} — no assertion was defined to verify the status code.`
596
+ });
597
+ }
598
+ const scriptResult = {
599
+ testResults: combinedTestResults,
600
+ consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
601
+ updatedEnvVars,
602
+ updatedCollectionVars,
603
+ updatedGlobals,
604
+ updatedLocalVars: localVars,
605
+ resolvedUrl,
606
+ preScriptError: preScriptMeta.error,
607
+ postScriptError: postError
608
+ };
609
+ return { response, scriptResult, sentRequest: redactSentRequest(sentRequest) };
610
+ });
611
+ handle.handleIpc(ipc, handle.IPC.script.runHook, async (_e, payload) => {
612
+ const { script, envVars, collectionVars, globals } = payload;
613
+ const result = await requestCollection.runScript(script, { envVars, collectionVars, globals, localVars: {} });
614
+ requestCollection.patchGlobals(result.updatedGlobals);
615
+ await requestCollection.persistGlobals();
616
+ return {
617
+ updatedEnvVars: result.updatedEnvVars,
618
+ updatedCollectionVars: result.updatedCollectionVars,
619
+ updatedGlobals: result.updatedGlobals,
620
+ consoleOutput: result.consoleOutput,
621
+ error: result.error
622
+ };
623
+ });
624
+ }
375
625
  const POSTMAN_RULES = [
376
626
  // Variables — environment
377
627
  [/\bpm\.environment\.get\(/g, "sp.environment.get("],
@@ -1125,8 +1375,160 @@ async function importBruno(filePath) {
1125
1375
  requests
1126
1376
  };
1127
1377
  }
1378
+ const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1379
+ const HTTP_TO_SPECTOR = {
1380
+ $guid: "$uuid",
1381
+ $randomInt: "$randomInt",
1382
+ $timestamp: "$timestamp",
1383
+ $datetime: "$isoTimestamp",
1384
+ $localDatetime: "$isoTimestamp"
1385
+ };
1386
+ const SPECTOR_TO_HTTP = {
1387
+ $uuid: "$guid",
1388
+ $randomInt: "$randomInt",
1389
+ $timestamp: "$timestamp",
1390
+ $isoTimestamp: "$datetime iso8601"
1391
+ };
1392
+ function mapDynamicVars(s) {
1393
+ return s.replace(/\{\{\s*(\$[A-Za-z]+)[^}]*\}\}/g, (_m, name) => `{{${HTTP_TO_SPECTOR[name] ?? name}}}`);
1394
+ }
1395
+ function contentTypeOf(headers) {
1396
+ return headers.find((h) => h.key.toLowerCase() === "content-type")?.value.toLowerCase() ?? "";
1397
+ }
1398
+ function toRequestBody(bodyText, headers) {
1399
+ if (!bodyText.trim()) return { mode: "none" };
1400
+ const ct = contentTypeOf(headers);
1401
+ if (ct.includes("json") || !ct && /^\s*[[{]/.test(bodyText)) {
1402
+ return { mode: "json", json: bodyText };
1403
+ }
1404
+ if (ct.includes("graphql")) {
1405
+ return { mode: "graphql", graphql: { query: bodyText, variables: "{}" } };
1406
+ }
1407
+ if (ct.includes("x-www-form-urlencoded")) {
1408
+ const form = bodyText.split("&").filter(Boolean).map((pair) => {
1409
+ const [k, v = ""] = pair.split("=");
1410
+ return { key: decodeURIComponent(k.trim()), value: decodeURIComponent(v.trim()), enabled: true };
1411
+ });
1412
+ return { mode: "form", form };
1413
+ }
1414
+ return { mode: "raw", raw: bodyText, rawContentType: ct || "text/plain" };
1415
+ }
1416
+ function extractAuth(headers) {
1417
+ const authHeader = headers.find((h) => h.key.toLowerCase() === "authorization");
1418
+ if (authHeader) {
1419
+ const bearer = /^Bearer\s+(.+)$/i.exec(authHeader.value.trim());
1420
+ if (bearer) {
1421
+ return {
1422
+ headers: headers.filter((h) => h !== authHeader),
1423
+ auth: { type: "bearer", token: bearer[1].trim() }
1424
+ };
1425
+ }
1426
+ }
1427
+ return { headers, auth: { type: "none" } };
1428
+ }
1429
+ function stripScriptBlocks(bodyLines2) {
1430
+ const joined = bodyLines2.join("\n");
1431
+ return joined.replace(/[<>]\s*\{%[\s\S]*?%\}/g, "").replace(/^\s*>\s+\S.*$/gm, "").replace(/^\s*<\s+\S.*$/gm, "").trim();
1432
+ }
1433
+ function splitBlocks(text) {
1434
+ const blocks = [];
1435
+ let current = null;
1436
+ for (const raw of text.split(/\r?\n/)) {
1437
+ const sep = /^###\s*(.*)$/.exec(raw);
1438
+ if (sep) {
1439
+ current = { label: sep[1].trim() || void 0, lines: [] };
1440
+ blocks.push(current);
1441
+ } else if (current) {
1442
+ current.lines.push(raw);
1443
+ } else {
1444
+ current = { lines: [raw] };
1445
+ blocks.push(current);
1446
+ }
1447
+ }
1448
+ return blocks;
1449
+ }
1450
+ function parseBlock(block) {
1451
+ const { lines } = block;
1452
+ let name = block.label;
1453
+ let i = 0;
1454
+ for (; i < lines.length; i++) {
1455
+ const t = lines[i].trim();
1456
+ if (!t) continue;
1457
+ const named = /^(?:#|\/\/)\s*@name\s*=?\s*(.+)$/.exec(t);
1458
+ if (named) {
1459
+ name = named[1].trim();
1460
+ continue;
1461
+ }
1462
+ if (t.startsWith("#") || t.startsWith("//")) continue;
1463
+ if (/^@[A-Za-z0-9_]+\s*=/.test(t)) continue;
1464
+ break;
1465
+ }
1466
+ if (i >= lines.length) return null;
1467
+ const requestLine = lines[i++].trim();
1468
+ const rm = new RegExp(`^(?:(${METHODS.join("|")})\\s+)?(\\S.*?)(?:\\s+HTTP/[\\d.]+)?$`, "i").exec(requestLine);
1469
+ if (!rm) return null;
1470
+ const method = (rm[1] ?? "GET").toUpperCase();
1471
+ let url = rm[2].trim();
1472
+ const rawHeaders = [];
1473
+ for (; i < lines.length; i++) {
1474
+ const line = lines[i];
1475
+ if (!line.trim()) {
1476
+ i++;
1477
+ break;
1478
+ }
1479
+ if (/^\s*[?&]/.test(line)) {
1480
+ url += line.trim();
1481
+ continue;
1482
+ }
1483
+ const h = /^([^:\s][^:]*):\s*(.*)$/.exec(line);
1484
+ if (h) rawHeaders.push({ key: h[1].trim(), value: mapDynamicVars(h[2].trim()), enabled: true });
1485
+ }
1486
+ const bodyText = mapDynamicVars(stripScriptBlocks(lines.slice(i)));
1487
+ const { headers, auth } = extractAuth(rawHeaders);
1488
+ const body = toRequestBody(bodyText, headers);
1489
+ return {
1490
+ id: uuid.v4(),
1491
+ name: name ?? `${method} ${url}`,
1492
+ method,
1493
+ url: mapDynamicVars(url),
1494
+ headers,
1495
+ params: [],
1496
+ auth,
1497
+ body,
1498
+ meta: { tags: ["http-file"] }
1499
+ };
1500
+ }
1501
+ function parseHttpFile(text, name) {
1502
+ const collectionVariables = {};
1503
+ for (const line of text.split(/\r?\n/)) {
1504
+ const m = /^@([A-Za-z0-9_]+)\s*=\s*(.*)$/.exec(line);
1505
+ if (m) collectionVariables[m[1]] = mapDynamicVars(m[2].trim());
1506
+ }
1507
+ const rootFolder = { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: [] };
1508
+ const requests = {};
1509
+ for (const block of splitBlocks(text)) {
1510
+ const req = parseBlock(block);
1511
+ if (!req) continue;
1512
+ requests[req.id] = req;
1513
+ rootFolder.requestIds.push(req.id);
1514
+ }
1515
+ return {
1516
+ version: "1.0",
1517
+ id: uuid.v4(),
1518
+ name,
1519
+ description: "",
1520
+ rootFolder,
1521
+ requests,
1522
+ ...Object.keys(collectionVariables).length ? { collectionVariables } : {}
1523
+ };
1524
+ }
1525
+ async function importHttpFile(filePath) {
1526
+ const raw = await promises.readFile(filePath, "utf8");
1527
+ const name = path.basename(filePath).replace(/\.(http|rest)$/i, "") || "Imported HTTP";
1528
+ return parseHttpFile(raw, name);
1529
+ }
1128
1530
  function registerImportHandlers(ipc) {
1129
- ipc.handle("import:postman", async () => {
1531
+ handle.handleIpc(ipc, handle.IPC.import.postman, async () => {
1130
1532
  const result = await electron.dialog.showOpenDialog({
1131
1533
  title: "Import Postman Collection",
1132
1534
  filters: [{ name: "JSON", extensions: ["json"] }],
@@ -1135,7 +1537,7 @@ function registerImportHandlers(ipc) {
1135
1537
  if (result.canceled || !result.filePaths[0]) return null;
1136
1538
  return importPostman(result.filePaths[0]);
1137
1539
  });
1138
- ipc.handle("import:openapi", async () => {
1540
+ handle.handleIpc(ipc, handle.IPC.import.openapi, async () => {
1139
1541
  const result = await electron.dialog.showOpenDialog({
1140
1542
  title: "Import OpenAPI Definition",
1141
1543
  filters: [{ name: "OpenAPI", extensions: ["json", "yaml", "yml"] }],
@@ -1144,10 +1546,10 @@ function registerImportHandlers(ipc) {
1144
1546
  if (result.canceled || !result.filePaths[0]) return null;
1145
1547
  return importOpenApi(result.filePaths[0]);
1146
1548
  });
1147
- ipc.handle("import:openapi-url", async (_event, url) => {
1549
+ handle.handleIpc(ipc, handle.IPC.import.openapiUrl, async (_event, url) => {
1148
1550
  return importOpenApiFromUrl(url);
1149
1551
  });
1150
- ipc.handle("import:insomnia", async () => {
1552
+ handle.handleIpc(ipc, handle.IPC.import.insomnia, async () => {
1151
1553
  const result = await electron.dialog.showOpenDialog({
1152
1554
  title: "Import Insomnia Collection",
1153
1555
  filters: [{ name: "JSON", extensions: ["json"] }],
@@ -1156,7 +1558,7 @@ function registerImportHandlers(ipc) {
1156
1558
  if (result.canceled || !result.filePaths[0]) return null;
1157
1559
  return importInsomnia(result.filePaths[0]);
1158
1560
  });
1159
- ipc.handle("import:bruno", async () => {
1561
+ handle.handleIpc(ipc, handle.IPC.import.bruno, async () => {
1160
1562
  const result = await electron.dialog.showOpenDialog({
1161
1563
  title: "Import Bruno Collection",
1162
1564
  filters: [{ name: "Bruno Collection", extensions: ["json"] }],
@@ -1165,7 +1567,16 @@ function registerImportHandlers(ipc) {
1165
1567
  if (result.canceled || !result.filePaths[0]) return null;
1166
1568
  return importBruno(result.filePaths[0]);
1167
1569
  });
1168
- ipc.handle("import:openapi-schemas", async () => {
1570
+ handle.handleIpc(ipc, handle.IPC.import.http, async () => {
1571
+ const result = await electron.dialog.showOpenDialog({
1572
+ title: "Import .http / .rest file",
1573
+ filters: [{ name: "HTTP file", extensions: ["http", "rest"] }],
1574
+ properties: ["openFile"]
1575
+ });
1576
+ if (result.canceled || !result.filePaths[0]) return null;
1577
+ return importHttpFile(result.filePaths[0]);
1578
+ });
1579
+ handle.handleIpc(ipc, handle.IPC.import.openapiSchemas, async () => {
1169
1580
  const result = await electron.dialog.showOpenDialog({
1170
1581
  title: "Load OpenAPI spec for schema sync",
1171
1582
  filters: [{ name: "OpenAPI", extensions: ["json", "yaml", "yml"] }],
@@ -1174,7 +1585,7 @@ function registerImportHandlers(ipc) {
1174
1585
  if (result.canceled || !result.filePaths[0]) return null;
1175
1586
  return extractSchemasFromFile(result.filePaths[0]);
1176
1587
  });
1177
- ipc.handle("import:openapi-schemas-url", async (_event, url) => {
1588
+ handle.handleIpc(ipc, handle.IPC.import.openapiSchemasUrl, async (_event, url) => {
1178
1589
  return extractSchemasFromUrl(url);
1179
1590
  });
1180
1591
  }
@@ -1232,6 +1643,97 @@ function parsePostScript(script) {
1232
1643
  function accessorToJsonPath(accessor) {
1233
1644
  return accessor.replace(/^json\.?/, "").replace(/\["([^"]+)"\]/g, "['$1']");
1234
1645
  }
1646
+ function slug(name) {
1647
+ return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
1648
+ }
1649
+ function toEnvVar(key) {
1650
+ return key.replace(/\W+/g, "_").toUpperCase();
1651
+ }
1652
+ function interpolateEnvVars(value, sharedVars = /* @__PURE__ */ new Set()) {
1653
+ return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
1654
+ const envKey = toEnvVar(key.trim());
1655
+ return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
1656
+ });
1657
+ }
1658
+ function renderJsValue(value, indent, sharedVars = /* @__PURE__ */ new Set()) {
1659
+ const next = indent + " ";
1660
+ if (value === null) return "null";
1661
+ if (typeof value === "boolean" || typeof value === "number") return String(value);
1662
+ if (typeof value === "string") {
1663
+ if (value.includes("{{")) {
1664
+ return "`" + interpolateEnvVars(value, sharedVars) + "`";
1665
+ }
1666
+ return JSON.stringify(value);
1667
+ }
1668
+ if (Array.isArray(value)) {
1669
+ if (!value.length) return "[]";
1670
+ return `[
1671
+ ${value.map((v) => next + renderJsValue(v, next, sharedVars)).join(",\n")},
1672
+ ${indent}]`;
1673
+ }
1674
+ if (typeof value === "object") {
1675
+ const entries = Object.entries(value);
1676
+ if (!entries.length) return "{}";
1677
+ return `{
1678
+ ${entries.map(([k, v]) => `${next}${k}: ${renderJsValue(v, next, sharedVars)}`).join(",\n")},
1679
+ ${indent}}`;
1680
+ }
1681
+ return JSON.stringify(value);
1682
+ }
1683
+ function buildNameMap$1(folder, requests) {
1684
+ const map = /* @__PURE__ */ new Map();
1685
+ const used = /* @__PURE__ */ new Set();
1686
+ for (const id of folder.requestIds) {
1687
+ const req = requests[id];
1688
+ if (!req) continue;
1689
+ const base = req.name;
1690
+ let name = base;
1691
+ if (used.has(name)) {
1692
+ let i = 2;
1693
+ while (used.has(`${base} ${i}`)) i++;
1694
+ name = `${base} ${i}`;
1695
+ }
1696
+ used.add(name);
1697
+ map.set(id, name);
1698
+ }
1699
+ return map;
1700
+ }
1701
+ function javaClass(name) {
1702
+ return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1703
+ }
1704
+ function resolveEffectiveAuth(req, inherited) {
1705
+ return req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
1706
+ }
1707
+ function mergeHeaders(req, inherited) {
1708
+ return [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
1709
+ }
1710
+ function hasBody(req) {
1711
+ return req.body.mode !== "none" && !["get", "head"].includes(req.method.toLowerCase());
1712
+ }
1713
+ function getEnvBaseUrl(environment, fallback) {
1714
+ return environment?.variables.find(
1715
+ (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
1716
+ )?.value ?? fallback;
1717
+ }
1718
+ function renderTree(paths) {
1719
+ const root = {};
1720
+ for (const p of [...paths].sort()) {
1721
+ let cur = root;
1722
+ for (const part of p.split("/")) {
1723
+ cur = cur[part] ??= {};
1724
+ }
1725
+ }
1726
+ function render(node, prefix = "") {
1727
+ const entries = Object.entries(node);
1728
+ return entries.flatMap(([name, children], i) => {
1729
+ const last = i === entries.length - 1;
1730
+ const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
1731
+ if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
1732
+ return lines;
1733
+ });
1734
+ }
1735
+ return [".", ...render(root)].join("\n");
1736
+ }
1235
1737
  function safeName(name) {
1236
1738
  return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1237
1739
  }
@@ -1247,7 +1749,7 @@ function interpolate(value, vars) {
1247
1749
  return v?.secret ? envVar(key.trim()) : robotVar(key.trim());
1248
1750
  });
1249
1751
  }
1250
- function buildNameMap$2(root, requests) {
1752
+ function buildNameMap(root, requests) {
1251
1753
  const map = /* @__PURE__ */ new Map();
1252
1754
  const used = /* @__PURE__ */ new Set();
1253
1755
  function visit(folder) {
@@ -1317,8 +1819,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
1317
1819
  lines.push(kwName);
1318
1820
  lines.push(` [Documentation] Hook: ${req.hookType} — ${req.name}`);
1319
1821
  const { body } = req;
1320
- const hasBody = body.mode !== "none" && !["GET", "HEAD"].includes(req.method);
1321
- if (hasBody && body.mode === "json" && body.json) {
1822
+ if (hasBody(req) && body.mode === "json" && body.json) {
1322
1823
  const bodyPairs = jsonToRfDictPairs(body.json, varMap);
1323
1824
  if (bodyPairs !== null) {
1324
1825
  lines.push(` VAR &{body} ${bodyPairs}`);
@@ -1327,7 +1828,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
1327
1828
  }
1328
1829
  }
1329
1830
  const callArgs = [];
1330
- if (hasBody && body.mode === "json") callArgs.push("json=${body}");
1831
+ if (hasBody(req) && body.mode === "json") callArgs.push("json=${body}");
1331
1832
  lines.push(` \${response}= ${method} ${url}`);
1332
1833
  if (callArgs.length) lines.push(` ... ${callArgs.join(" ")}`);
1333
1834
  const parsed = parsePostScript(req.postRequestScript);
@@ -1353,8 +1854,8 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
1353
1854
  lines.push(kwName);
1354
1855
  lines.push(` [Documentation] ${req.description || req.name}`);
1355
1856
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
1356
- const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
1357
- const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
1857
+ const effectiveAuth = resolveEffectiveAuth(req, inherited);
1858
+ const allHeaders = mergeHeaders(req, inherited);
1358
1859
  const headerPairs = [];
1359
1860
  if (effectiveAuth.type === "bearer") {
1360
1861
  const token = effectiveAuth.token ?? "";
@@ -1392,8 +1893,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
1392
1893
  lines.push(` VAR &{params} ${pairs}`);
1393
1894
  }
1394
1895
  const { body } = req;
1395
- const hasBody = body.mode !== "none" && !["GET", "HEAD"].includes(req.method);
1396
- if (hasBody && body.mode === "json" && body.json) {
1896
+ if (hasBody(req) && body.mode === "json" && body.json) {
1397
1897
  const bodyPairs = jsonToRfDictPairs(body.json, varMap);
1398
1898
  if (bodyPairs !== null) {
1399
1899
  lines.push(` VAR &{body} ${bodyPairs}`);
@@ -1405,7 +1905,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
1405
1905
  const callArgs = [];
1406
1906
  if (headerPairs.length) callArgs.push("headers=${headers}");
1407
1907
  if (enabledParams.length) callArgs.push("params=${params}");
1408
- if (hasBody && body.mode === "json") callArgs.push("json=${body}");
1908
+ if (hasBody(req) && body.mode === "json") callArgs.push("json=${body}");
1409
1909
  lines.push(` \${response}= ${method} ${url}`);
1410
1910
  if (callArgs.length) {
1411
1911
  lines.push(` ... ${callArgs.join(" ")}`);
@@ -1494,27 +1994,8 @@ function buildTestSuite(collection, environment, nameMap) {
1494
1994
  processFolder(collection.rootFolder);
1495
1995
  return lines.join("\n");
1496
1996
  }
1497
- function renderTree$6(paths) {
1498
- const root = {};
1499
- for (const p of [...paths].sort()) {
1500
- let cur = root;
1501
- for (const part of p.split("/")) {
1502
- cur = cur[part] ??= {};
1503
- }
1504
- }
1505
- function render(node, prefix = "") {
1506
- const entries = Object.entries(node);
1507
- return entries.flatMap(([name, children], i) => {
1508
- const last = i === entries.length - 1;
1509
- const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
1510
- if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
1511
- return lines;
1512
- });
1513
- }
1514
- return [".", ...render(root)].join("\n");
1515
- }
1516
1997
  function buildReadme$6(collectionName, filePaths) {
1517
- const tree = renderTree$6(filePaths);
1998
+ const tree = renderTree(filePaths);
1518
1999
  return `# ${collectionName} — API Tests (Robot Framework)
1519
2000
 
1520
2001
  ## Project structure
@@ -1543,7 +2024,7 @@ function generateRobotFramework(collection, environment) {
1543
2024
  const varMap = new Map(
1544
2025
  (environment?.variables ?? []).map((v) => [v.key, v])
1545
2026
  );
1546
- const nameMap = buildNameMap$2(collection.rootFolder, collection.requests);
2027
+ const nameMap = buildNameMap(collection.rootFolder, collection.requests);
1547
2028
  const slug2 = collection.name.replace(/\W+/g, "_").toLowerCase();
1548
2029
  const hookExtractedVars = /* @__PURE__ */ new Set();
1549
2030
  const contentFiles = [
@@ -1558,65 +2039,8 @@ function generateRobotFramework(collection, environment) {
1558
2039
  ...contentFiles
1559
2040
  ];
1560
2041
  }
1561
- function slug$3(name) {
1562
- return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
1563
- }
1564
- function toEnvVar$3(key) {
1565
- return key.replace(/\W+/g, "_").toUpperCase();
1566
- }
1567
- function interpolatePath$1(value, sharedVars = /* @__PURE__ */ new Set()) {
1568
- return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
1569
- const envKey = toEnvVar$3(key.trim());
1570
- return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
1571
- });
1572
- }
1573
- function buildNameMap$1(folder, requests) {
1574
- const map = /* @__PURE__ */ new Map();
1575
- const used = /* @__PURE__ */ new Set();
1576
- for (const id of folder.requestIds) {
1577
- const req = requests[id];
1578
- if (!req) continue;
1579
- const base = req.name;
1580
- let name = base;
1581
- if (used.has(name)) {
1582
- let i = 2;
1583
- while (used.has(`${base} ${i}`)) i++;
1584
- name = `${base} ${i}`;
1585
- }
1586
- used.add(name);
1587
- map.set(id, name);
1588
- }
1589
- return map;
1590
- }
1591
- function renderJsValue$1(value, indent, sharedVars = /* @__PURE__ */ new Set()) {
1592
- const next = indent + " ";
1593
- if (value === null) return "null";
1594
- if (typeof value === "boolean" || typeof value === "number") return String(value);
1595
- if (typeof value === "string") {
1596
- if (value.includes("{{")) {
1597
- return "`" + interpolatePath$1(value, sharedVars) + "`";
1598
- }
1599
- return JSON.stringify(value);
1600
- }
1601
- if (Array.isArray(value)) {
1602
- if (!value.length) return "[]";
1603
- return `[
1604
- ${value.map((v) => next + renderJsValue$1(v, next, sharedVars)).join(",\n")},
1605
- ${indent}]`;
1606
- }
1607
- if (typeof value === "object") {
1608
- const entries = Object.entries(value);
1609
- if (!entries.length) return "{}";
1610
- return `{
1611
- ${entries.map(([k, v]) => `${next}${k}: ${renderJsValue$1(v, next, sharedVars)}`).join(",\n")},
1612
- ${indent}}`;
1613
- }
1614
- return JSON.stringify(value);
1615
- }
1616
2042
  function buildPlaywrightConfig$1(environment) {
1617
- const baseUrl = environment?.variables.find(
1618
- (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
1619
- )?.value ?? "http://localhost:3000";
2043
+ const baseUrl = getEnvBaseUrl(environment, "http://localhost:3000");
1620
2044
  return `import { defineConfig } from '@playwright/test'
1621
2045
  import * as dotenv from 'dotenv'
1622
2046
 
@@ -1636,19 +2060,19 @@ export default defineConfig({
1636
2060
  function buildHookLines$1(req, sharedVars) {
1637
2061
  const method = req.method.toLowerCase();
1638
2062
  const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
1639
- const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2) + "`" : `'${path2}'`;
2063
+ const pathExpr = path2.includes("{{") ? "`" + interpolateEnvVars(path2) + "`" : `'${path2}'`;
1640
2064
  const headerEntries = [];
1641
2065
  if (req.auth.type === "bearer") {
1642
2066
  const token = req.auth.token ?? "";
1643
2067
  if (token.includes("{{")) {
1644
- headerEntries.push(`Authorization: \`Bearer ${interpolatePath$1(token)}\``);
2068
+ headerEntries.push(`Authorization: \`Bearer ${interpolateEnvVars(token)}\``);
1645
2069
  } else {
1646
2070
  const ref = req.auth.tokenSecretRef ?? "API_TOKEN";
1647
- headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$3(ref)} ?? ''}\``);
2071
+ headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar(ref)} ?? ''}\``);
1648
2072
  }
1649
2073
  }
1650
2074
  for (const h of req.headers.filter((h2) => h2.enabled && h2.key)) {
1651
- headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value)}\``);
2075
+ headerEntries.push(`'${h.key}': \`${interpolateEnvVars(h.value)}\``);
1652
2076
  }
1653
2077
  const optionParts = [];
1654
2078
  if (headerEntries.length) {
@@ -1656,7 +2080,7 @@ function buildHookLines$1(req, sharedVars) {
1656
2080
  }
1657
2081
  if (req.body.mode === "json" && req.body.json && !["get", "head"].includes(method)) {
1658
2082
  try {
1659
- optionParts.push(`data: ${renderJsValue$1(JSON.parse(req.body.json), " ")}`);
2083
+ optionParts.push(`data: ${renderJsValue(JSON.parse(req.body.json), " ")}`);
1660
2084
  } catch {
1661
2085
  }
1662
2086
  }
@@ -1670,7 +2094,7 @@ function buildHookLines$1(req, sharedVars) {
1670
2094
  for (const e of parsed.extractions) {
1671
2095
  const jsonPath = e.accessor.replace(/^json\.?/, "");
1672
2096
  const expr = jsonPath ? `hookJson.${jsonPath}` : "hookJson";
1673
- const varName = toEnvVar$3(e.varName);
2097
+ const varName = toEnvVar(e.varName);
1674
2098
  sharedVars.add(varName);
1675
2099
  lines.push(` ${varName} = String(${expr});`);
1676
2100
  }
@@ -1728,32 +2152,32 @@ ${lines.join("\n")}
1728
2152
  const testName = nameMap.get(reqId) ?? req.name;
1729
2153
  const method = req.method.toLowerCase();
1730
2154
  const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
1731
- const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2, sharedVars) + "`" : `'${path2}'`;
2155
+ const pathExpr = path2.includes("{{") ? "`" + interpolateEnvVars(path2, sharedVars) + "`" : `'${path2}'`;
1732
2156
  const optionParts = [];
1733
2157
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
1734
- const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
1735
- const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
2158
+ const effectiveAuth = resolveEffectiveAuth(req, inherited);
2159
+ const allHeaders = mergeHeaders(req, inherited);
1736
2160
  const headerEntries = [];
1737
2161
  if (effectiveAuth.type === "bearer") {
1738
2162
  const token = effectiveAuth.token ?? "";
1739
2163
  if (token.includes("{{")) {
1740
- headerEntries.push(`Authorization: \`Bearer ${interpolatePath$1(token, sharedVars)}\``);
2164
+ headerEntries.push(`Authorization: \`Bearer ${interpolateEnvVars(token, sharedVars)}\``);
1741
2165
  } else {
1742
2166
  const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
1743
- headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$3(ref)} ?? ''}\``);
2167
+ headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar(ref)} ?? ''}\``);
1744
2168
  }
1745
2169
  } else if (effectiveAuth.type === "apikey" && effectiveAuth.apiKeyIn === "header") {
1746
2170
  const val = effectiveAuth.apiKeyValue ?? "";
1747
2171
  const name = effectiveAuth.apiKeyName ?? "X-API-Key";
1748
2172
  if (val.includes("{{")) {
1749
- headerEntries.push(`'${name}': \`${interpolatePath$1(val, sharedVars)}\``);
2173
+ headerEntries.push(`'${name}': \`${interpolateEnvVars(val, sharedVars)}\``);
1750
2174
  } else {
1751
2175
  const ref = effectiveAuth.apiKeySecretRef ?? "API_KEY";
1752
- headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$3(ref)} ?? ''}\``);
2176
+ headerEntries.push(`'${name}': \`\${process.env.${toEnvVar(ref)} ?? ''}\``);
1753
2177
  }
1754
2178
  }
1755
2179
  for (const h of allHeaders) {
1756
- headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value, sharedVars)}\``);
2180
+ headerEntries.push(`'${h.key}': \`${interpolateEnvVars(h.value, sharedVars)}\``);
1757
2181
  }
1758
2182
  if (headerEntries.length) {
1759
2183
  optionParts.push(` headers: {
@@ -1763,17 +2187,16 @@ ${lines.join("\n")}
1763
2187
  const enabledParams = req.params.filter((p) => p.enabled && p.key);
1764
2188
  if (enabledParams.length) {
1765
2189
  const pairs = enabledParams.map(
1766
- (p) => p.value.includes("{{") ? `'${p.key}': \`${interpolatePath$1(p.value, sharedVars)}\`` : `'${p.key}': '${p.value}'`
2190
+ (p) => p.value.includes("{{") ? `'${p.key}': \`${interpolateEnvVars(p.value, sharedVars)}\`` : `'${p.key}': '${p.value}'`
1767
2191
  ).join(", ");
1768
2192
  optionParts.push(` params: { ${pairs} }`);
1769
2193
  }
1770
- const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
1771
- if (hasBody && req.body.mode === "json" && req.body.json) {
2194
+ if (hasBody(req) && req.body.mode === "json" && req.body.json) {
1772
2195
  try {
1773
- const rendered = renderJsValue$1(JSON.parse(req.body.json), " ", sharedVars);
2196
+ const rendered = renderJsValue(JSON.parse(req.body.json), " ", sharedVars);
1774
2197
  optionParts.push(` data: ${rendered}`);
1775
2198
  } catch {
1776
- optionParts.push(` data: \`${interpolatePath$1(req.body.json, sharedVars)}\``);
2199
+ optionParts.push(` data: \`${interpolateEnvVars(req.body.json, sharedVars)}\``);
1777
2200
  }
1778
2201
  }
1779
2202
  const optionsStr = optionParts.length ? `, {
@@ -1828,7 +2251,7 @@ ${optionParts.join(",\n")},
1828
2251
  const path22 = e.accessor.replace(/^json\.?/, "");
1829
2252
  const expr = path22 ? `json.${path22}` : "json";
1830
2253
  lines.push(` // Extract: ${e.varName} = ${expr}`);
1831
- lines.push(` process.env.${toEnvVar$3(e.varName)} = String(${expr});`);
2254
+ lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
1832
2255
  }
1833
2256
  lines.push(` });`);
1834
2257
  tests.push(lines.join("\n"));
@@ -1859,27 +2282,8 @@ function buildPackageJson$3(collectionName) {
1859
2282
  }
1860
2283
  }, null, 2) + "\n";
1861
2284
  }
1862
- function renderTree$5(paths) {
1863
- const root = {};
1864
- for (const p of [...paths].sort()) {
1865
- let cur = root;
1866
- for (const part of p.split("/")) {
1867
- cur = cur[part] ??= {};
1868
- }
1869
- }
1870
- function render(node, prefix = "") {
1871
- const entries = Object.entries(node);
1872
- return entries.flatMap(([name, children], i) => {
1873
- const last = i === entries.length - 1;
1874
- const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
1875
- if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
1876
- return lines;
1877
- });
1878
- }
1879
- return [".", ...render(root)].join("\n");
1880
- }
1881
2285
  function buildReadme$5(collectionName, filePaths) {
1882
- const tree = renderTree$5([...filePaths, ".env.local"]);
2286
+ const tree = renderTree([...filePaths, ".env.local"]);
1883
2287
  return `# ${collectionName} — API Tests (Playwright TypeScript)
1884
2288
 
1885
2289
  ## Project structure
@@ -1900,83 +2304,26 @@ npm test
1900
2304
  `;
1901
2305
  }
1902
2306
  function generatePlaywright(collection, environment) {
1903
- const files = [];
1904
- function processFolder(folder, name) {
1905
- if (folder.requestIds.length > 0) {
1906
- const nameMap = buildNameMap$1(folder, collection.requests);
1907
- files.push({ path: `tests/${slug$3(name)}.spec.ts`, content: buildSpec$1(name, folder, collection, nameMap) });
1908
- }
1909
- for (const sub of folder.folders) processFolder(sub, sub.name);
1910
- }
1911
- if (collection.rootFolder.requestIds.length > 0) processFolder(collection.rootFolder, collection.name);
1912
- for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
1913
- const scaffoldPaths = ["package.json", "playwright.config.ts", ...files.map((f) => f.path)];
1914
- files.unshift(
1915
- { path: "package.json", content: buildPackageJson$3(collection.name) },
1916
- { path: "playwright.config.ts", content: buildPlaywrightConfig$1(environment) },
1917
- { path: "README.md", content: buildReadme$5(collection.name, scaffoldPaths) }
1918
- );
1919
- return files;
1920
- }
1921
- function slug$2(name) {
1922
- return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
1923
- }
1924
- function toEnvVar$2(key) {
1925
- return key.replace(/\W+/g, "_").toUpperCase();
1926
- }
1927
- function interpolatePath(value, sharedVars = /* @__PURE__ */ new Set()) {
1928
- return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
1929
- const envKey = toEnvVar$2(key.trim());
1930
- return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
1931
- });
1932
- }
1933
- function buildNameMap(folder, requests) {
1934
- const map = /* @__PURE__ */ new Map();
1935
- const used = /* @__PURE__ */ new Set();
1936
- for (const id of folder.requestIds) {
1937
- const req = requests[id];
1938
- if (!req) continue;
1939
- const base = req.name;
1940
- let name = base;
1941
- if (used.has(name)) {
1942
- let i = 2;
1943
- while (used.has(`${base} ${i}`)) i++;
1944
- name = `${base} ${i}`;
1945
- }
1946
- used.add(name);
1947
- map.set(id, name);
1948
- }
1949
- return map;
1950
- }
1951
- function renderJsValue(value, indent, sharedVars = /* @__PURE__ */ new Set()) {
1952
- const next = indent + " ";
1953
- if (value === null) return "null";
1954
- if (typeof value === "boolean" || typeof value === "number") return String(value);
1955
- if (typeof value === "string") {
1956
- if (value.includes("{{")) {
1957
- return "`" + interpolatePath(value, sharedVars) + "`";
2307
+ const files = [];
2308
+ function processFolder(folder, name) {
2309
+ if (folder.requestIds.length > 0) {
2310
+ const nameMap = buildNameMap$1(folder, collection.requests);
2311
+ files.push({ path: `tests/${slug(name)}.spec.ts`, content: buildSpec$1(name, folder, collection, nameMap) });
1958
2312
  }
1959
- return JSON.stringify(value);
1960
- }
1961
- if (Array.isArray(value)) {
1962
- if (!value.length) return "[]";
1963
- return `[
1964
- ${value.map((v) => next + renderJsValue(v, next, sharedVars)).join(",\n")},
1965
- ${indent}]`;
1966
- }
1967
- if (typeof value === "object") {
1968
- const entries = Object.entries(value);
1969
- if (!entries.length) return "{}";
1970
- return `{
1971
- ${entries.map(([k, v]) => `${next}${k}: ${renderJsValue(v, next, sharedVars)}`).join(",\n")},
1972
- ${indent}}`;
2313
+ for (const sub of folder.folders) processFolder(sub, sub.name);
1973
2314
  }
1974
- return JSON.stringify(value);
2315
+ if (collection.rootFolder.requestIds.length > 0) processFolder(collection.rootFolder, collection.name);
2316
+ for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
2317
+ const scaffoldPaths = ["package.json", "playwright.config.ts", ...files.map((f) => f.path)];
2318
+ files.unshift(
2319
+ { path: "package.json", content: buildPackageJson$3(collection.name) },
2320
+ { path: "playwright.config.ts", content: buildPlaywrightConfig$1(environment) },
2321
+ { path: "README.md", content: buildReadme$5(collection.name, scaffoldPaths) }
2322
+ );
2323
+ return files;
1975
2324
  }
1976
2325
  function buildPlaywrightConfig(environment) {
1977
- const baseUrl = environment?.variables.find(
1978
- (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
1979
- )?.value ?? "http://localhost:3000";
2326
+ const baseUrl = getEnvBaseUrl(environment, "http://localhost:3000");
1980
2327
  return `// @ts-check
1981
2328
  const { defineConfig } = require('@playwright/test');
1982
2329
  require('dotenv').config({ path: '.env.local' });
@@ -1995,19 +2342,19 @@ module.exports = defineConfig({
1995
2342
  function buildHookLines(req, sharedVars) {
1996
2343
  const method = req.method.toLowerCase();
1997
2344
  const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
1998
- const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2) + "`" : `'${path2}'`;
2345
+ const pathExpr = path2.includes("{{") ? "`" + interpolateEnvVars(path2) + "`" : `'${path2}'`;
1999
2346
  const headerEntries = [];
2000
2347
  if (req.auth.type === "bearer") {
2001
2348
  const token = req.auth.token ?? "";
2002
2349
  if (token.includes("{{")) {
2003
- headerEntries.push(`Authorization: \`Bearer ${interpolatePath(token)}\``);
2350
+ headerEntries.push(`Authorization: \`Bearer ${interpolateEnvVars(token)}\``);
2004
2351
  } else {
2005
2352
  const ref = req.auth.tokenSecretRef ?? "API_TOKEN";
2006
- headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$2(ref)} ?? ''}\``);
2353
+ headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar(ref)} ?? ''}\``);
2007
2354
  }
2008
2355
  }
2009
2356
  for (const h of req.headers.filter((h2) => h2.enabled && h2.key)) {
2010
- headerEntries.push(`'${h.key}': \`${interpolatePath(h.value)}\``);
2357
+ headerEntries.push(`'${h.key}': \`${interpolateEnvVars(h.value)}\``);
2011
2358
  }
2012
2359
  const optParts = [];
2013
2360
  if (headerEntries.length) optParts.push(`headers: { ${headerEntries.join(", ")} }`);
@@ -2026,7 +2373,7 @@ function buildHookLines(req, sharedVars) {
2026
2373
  for (const e of parsed.extractions) {
2027
2374
  const jp = e.accessor.replace(/^json\.?/, "");
2028
2375
  const expr = jp ? `hookJson.${jp}` : "hookJson";
2029
- const varName = toEnvVar$2(e.varName);
2376
+ const varName = toEnvVar(e.varName);
2030
2377
  sharedVars.add(varName);
2031
2378
  lines.push(` ${varName} = String(${expr});`);
2032
2379
  }
@@ -2082,32 +2429,32 @@ ${lines.join("\n")}
2082
2429
  const testName = nameMap.get(reqId) ?? req.name;
2083
2430
  const method = req.method.toLowerCase();
2084
2431
  const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
2085
- const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2, sharedVars) + "`" : `'${path2}'`;
2432
+ const pathExpr = path2.includes("{{") ? "`" + interpolateEnvVars(path2, sharedVars) + "`" : `'${path2}'`;
2086
2433
  const optionParts = [];
2087
2434
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
2088
- const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
2089
- const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
2435
+ const effectiveAuth = resolveEffectiveAuth(req, inherited);
2436
+ const allHeaders = mergeHeaders(req, inherited);
2090
2437
  const headerEntries = [];
2091
2438
  if (effectiveAuth.type === "bearer") {
2092
2439
  const token = effectiveAuth.token ?? "";
2093
2440
  if (token.includes("{{")) {
2094
- headerEntries.push(`Authorization: \`Bearer ${interpolatePath(token, sharedVars)}\``);
2441
+ headerEntries.push(`Authorization: \`Bearer ${interpolateEnvVars(token, sharedVars)}\``);
2095
2442
  } else {
2096
2443
  const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
2097
- headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$2(ref)} ?? ''}\``);
2444
+ headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar(ref)} ?? ''}\``);
2098
2445
  }
2099
2446
  } else if (effectiveAuth.type === "apikey" && effectiveAuth.apiKeyIn === "header") {
2100
2447
  const val = effectiveAuth.apiKeyValue ?? "";
2101
2448
  const name = effectiveAuth.apiKeyName ?? "X-API-Key";
2102
2449
  if (val.includes("{{")) {
2103
- headerEntries.push(`'${name}': \`${interpolatePath(val, sharedVars)}\``);
2450
+ headerEntries.push(`'${name}': \`${interpolateEnvVars(val, sharedVars)}\``);
2104
2451
  } else {
2105
2452
  const ref = effectiveAuth.apiKeySecretRef ?? "API_KEY";
2106
- headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$2(ref)} ?? ''}\``);
2453
+ headerEntries.push(`'${name}': \`\${process.env.${toEnvVar(ref)} ?? ''}\``);
2107
2454
  }
2108
2455
  }
2109
2456
  for (const h of allHeaders) {
2110
- headerEntries.push(`'${h.key}': \`${interpolatePath(h.value, sharedVars)}\``);
2457
+ headerEntries.push(`'${h.key}': \`${interpolateEnvVars(h.value, sharedVars)}\``);
2111
2458
  }
2112
2459
  if (headerEntries.length) {
2113
2460
  optionParts.push(` headers: {
@@ -2117,17 +2464,16 @@ ${lines.join("\n")}
2117
2464
  const enabledParams = req.params.filter((p) => p.enabled && p.key);
2118
2465
  if (enabledParams.length) {
2119
2466
  const pairs = enabledParams.map(
2120
- (p) => p.value.includes("{{") ? `'${p.key}': \`${interpolatePath(p.value, sharedVars)}\`` : `'${p.key}': '${p.value}'`
2467
+ (p) => p.value.includes("{{") ? `'${p.key}': \`${interpolateEnvVars(p.value, sharedVars)}\`` : `'${p.key}': '${p.value}'`
2121
2468
  ).join(", ");
2122
2469
  optionParts.push(` params: { ${pairs} }`);
2123
2470
  }
2124
- const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
2125
- if (hasBody && req.body.mode === "json" && req.body.json) {
2471
+ if (hasBody(req) && req.body.mode === "json" && req.body.json) {
2126
2472
  try {
2127
2473
  const rendered = renderJsValue(JSON.parse(req.body.json), " ", sharedVars);
2128
2474
  optionParts.push(` data: ${rendered}`);
2129
2475
  } catch {
2130
- optionParts.push(` data: \`${interpolatePath(req.body.json, sharedVars)}\``);
2476
+ optionParts.push(` data: \`${interpolateEnvVars(req.body.json, sharedVars)}\``);
2131
2477
  }
2132
2478
  }
2133
2479
  const optionsStr = optionParts.length ? `, {
@@ -2173,7 +2519,7 @@ ${optionParts.join(",\n")},
2173
2519
  for (const e of parsed.extractions) {
2174
2520
  const path22 = e.accessor.replace(/^json\.?/, "");
2175
2521
  const expr = path22 ? `json.${path22}` : "json";
2176
- lines.push(` process.env.${toEnvVar$2(e.varName)} = String(${expr});`);
2522
+ lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
2177
2523
  }
2178
2524
  lines.push(` });`);
2179
2525
  tests.push(lines.join("\n"));
@@ -2203,27 +2549,8 @@ function buildPackageJson$2(collectionName) {
2203
2549
  }
2204
2550
  }, null, 2) + "\n";
2205
2551
  }
2206
- function renderTree$4(paths) {
2207
- const root = {};
2208
- for (const p of [...paths].sort()) {
2209
- let cur = root;
2210
- for (const part of p.split("/")) {
2211
- cur = cur[part] ??= {};
2212
- }
2213
- }
2214
- function render(node, prefix = "") {
2215
- const entries = Object.entries(node);
2216
- return entries.flatMap(([name, children], i) => {
2217
- const last = i === entries.length - 1;
2218
- const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
2219
- if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
2220
- return lines;
2221
- });
2222
- }
2223
- return [".", ...render(root)].join("\n");
2224
- }
2225
2552
  function buildReadme$4(collectionName, filePaths) {
2226
- const tree = renderTree$4([...filePaths, ".env.local"]);
2553
+ const tree = renderTree([...filePaths, ".env.local"]);
2227
2554
  return `# ${collectionName} — API Tests (Playwright JavaScript)
2228
2555
 
2229
2556
  ## Project structure
@@ -2247,8 +2574,8 @@ function generatePlaywrightJs(collection, environment) {
2247
2574
  const files = [];
2248
2575
  function processFolder(folder, name) {
2249
2576
  if (folder.requestIds.length > 0) {
2250
- const nameMap = buildNameMap(folder, collection.requests);
2251
- files.push({ path: `tests/${slug$2(name)}.spec.js`, content: buildSpec(name, folder, collection, nameMap) });
2577
+ const nameMap = buildNameMap$1(folder, collection.requests);
2578
+ files.push({ path: `tests/${slug(name)}.spec.js`, content: buildSpec(name, folder, collection, nameMap) });
2252
2579
  }
2253
2580
  for (const sub of folder.folders) processFolder(sub, sub.name);
2254
2581
  }
@@ -2262,18 +2589,6 @@ function generatePlaywrightJs(collection, environment) {
2262
2589
  );
2263
2590
  return files;
2264
2591
  }
2265
- function slug$1(name) {
2266
- return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
2267
- }
2268
- function toEnvVar$1(key) {
2269
- return key.replace(/\W+/g, "_").toUpperCase();
2270
- }
2271
- function interpolateValue$1(value, sharedVars = /* @__PURE__ */ new Set()) {
2272
- return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
2273
- const envKey = toEnvVar$1(key.trim());
2274
- return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
2275
- });
2276
- }
2277
2592
  function buildJestConfig$1() {
2278
2593
  return `import type { Config } from 'jest'
2279
2594
 
@@ -2288,11 +2603,9 @@ export default config
2288
2603
  `;
2289
2604
  }
2290
2605
  function buildClient$1(environment) {
2291
- const baseUrl = environment?.variables.find(
2292
- (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
2293
- )?.value ?? "http://localhost:3000";
2606
+ const baseUrl = getEnvBaseUrl(environment, "http://localhost:3000");
2294
2607
  const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
2295
- const envComments = secretVars.map((v) => `# ${toEnvVar$1(v.key)}=<from keychain>`).join("\n");
2608
+ const envComments = secretVars.map((v) => `# ${toEnvVar(v.key)}=<from keychain>`).join("\n");
2296
2609
  return `import supertest from 'supertest'
2297
2610
  import * as dotenv from 'dotenv'
2298
2611
 
@@ -2337,7 +2650,7 @@ function buildTestFile$1(folderName, folder, collection) {
2337
2650
  for (const e of parsed.extractions) {
2338
2651
  const jp = e.accessor.replace(/^json\.?/, "");
2339
2652
  const expr = jp ? `hookRes.body.${jp}` : "hookRes.body";
2340
- const varName = toEnvVar$1(e.varName);
2653
+ const varName = toEnvVar(e.varName);
2341
2654
  sharedVars.add(varName);
2342
2655
  lines.push(` ${varName} = String(${expr});`);
2343
2656
  }
@@ -2368,12 +2681,11 @@ ${lines.join("\n")}
2368
2681
  const req = requests[reqId];
2369
2682
  if (!req || req.disabled || req.hookType) continue;
2370
2683
  const method = req.method.toLowerCase();
2371
- const path2 = interpolateValue$1(req.url.replace(/^https?:\/\/[^/]+/, "") || "/", sharedVars);
2684
+ const path2 = interpolateEnvVars(req.url.replace(/^https?:\/\/[^/]+/, "") || "/", sharedVars);
2372
2685
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
2373
- const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
2374
- const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
2686
+ const effectiveAuth = resolveEffectiveAuth(req, inherited);
2687
+ const allHeaders = mergeHeaders(req, inherited);
2375
2688
  const enabledParams = req.params.filter((p) => p.enabled && p.key);
2376
- const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
2377
2689
  const lines = [];
2378
2690
  lines.push(` it('${nameMap.get(reqId)}', async () => {`);
2379
2691
  lines.push(` const res = await api`);
@@ -2381,29 +2693,29 @@ ${lines.join("\n")}
2381
2693
  if (effectiveAuth.type === "bearer") {
2382
2694
  const token = effectiveAuth.token ?? "";
2383
2695
  if (token.includes("{{")) {
2384
- lines.push(` .set('Authorization', \`Bearer ${interpolateValue$1(token, sharedVars)}\`)`);
2696
+ lines.push(` .set('Authorization', \`Bearer ${interpolateEnvVars(token, sharedVars)}\`)`);
2385
2697
  } else {
2386
2698
  const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
2387
- lines.push(` .set('Authorization', \`Bearer \${process.env.${toEnvVar$1(ref)} ?? ''}\`)`);
2699
+ lines.push(` .set('Authorization', \`Bearer \${process.env.${toEnvVar(ref)} ?? ''}\`)`);
2388
2700
  }
2389
2701
  }
2390
2702
  for (const h of allHeaders) {
2391
- lines.push(` .set('${h.key}', \`${interpolateValue$1(h.value, sharedVars)}\`)`);
2703
+ lines.push(` .set('${h.key}', \`${interpolateEnvVars(h.value, sharedVars)}\`)`);
2392
2704
  }
2393
2705
  if (enabledParams.length) {
2394
- const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue$1(p.value, sharedVars)}\``).join(", ");
2706
+ const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateEnvVars(p.value, sharedVars)}\``).join(", ");
2395
2707
  lines.push(` .query({ ${pairs} })`);
2396
2708
  }
2397
- if (hasBody) {
2709
+ if (hasBody(req)) {
2398
2710
  if (req.body.mode === "json") {
2399
2711
  const jsonBody = req.body.json ?? "{}";
2400
2712
  if (jsonBody.includes("{{")) {
2401
- lines.push(` .send(JSON.parse(\`${interpolateValue$1(jsonBody, sharedVars)}\`))`);
2713
+ lines.push(` .send(JSON.parse(\`${interpolateEnvVars(jsonBody, sharedVars)}\`))`);
2402
2714
  } else {
2403
2715
  lines.push(` .send(${jsonBody})`);
2404
2716
  }
2405
2717
  } else if (req.body.mode === "form" && req.body.form) {
2406
- const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue$1(p.value, sharedVars)}\``).join(", ");
2718
+ const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateEnvVars(p.value, sharedVars)}\``).join(", ");
2407
2719
  lines.push(` .type('form')`);
2408
2720
  lines.push(` .send({ ${pairs} })`);
2409
2721
  }
@@ -2442,7 +2754,7 @@ ${lines.join("\n")}
2442
2754
  for (const e of parsed.extractions) {
2443
2755
  const path22 = e.accessor.replace(/^json\.?/, "");
2444
2756
  const expr = path22 ? `res.body.${path22}` : "res.body";
2445
- lines.push(` process.env.${toEnvVar$1(e.varName)} = String(${expr});`);
2757
+ lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
2446
2758
  }
2447
2759
  lines.push(` })`);
2448
2760
  tests.push(lines.join("\n"));
@@ -2486,27 +2798,8 @@ function buildTsConfig() {
2486
2798
  exclude: ["node_modules", "dist"]
2487
2799
  }, null, 2) + "\n";
2488
2800
  }
2489
- function renderTree$3(paths) {
2490
- const root = {};
2491
- for (const p of [...paths].sort()) {
2492
- let cur = root;
2493
- for (const part of p.split("/")) {
2494
- cur = cur[part] ??= {};
2495
- }
2496
- }
2497
- function render(node, prefix = "") {
2498
- const entries = Object.entries(node);
2499
- return entries.flatMap(([name, children], i) => {
2500
- const last = i === entries.length - 1;
2501
- const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
2502
- if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
2503
- return lines;
2504
- });
2505
- }
2506
- return [".", ...render(root)].join("\n");
2507
- }
2508
2801
  function buildReadme$3(collectionName, filePaths) {
2509
- const tree = renderTree$3([...filePaths, ".env.local"]);
2802
+ const tree = renderTree([...filePaths, ".env.local"]);
2510
2803
  return `# ${collectionName} — API Tests (Supertest + Jest TypeScript)
2511
2804
 
2512
2805
  ## Project structure
@@ -2533,7 +2826,7 @@ function generateSupertestTs(collection, environment) {
2533
2826
  function processFolder(folder, name) {
2534
2827
  if (folder.requestIds.length > 0) {
2535
2828
  files.push({
2536
- path: `tests/${slug$1(name)}.test.ts`,
2829
+ path: `tests/${slug(name)}.test.ts`,
2537
2830
  content: buildTestFile$1(name, folder, collection)
2538
2831
  });
2539
2832
  }
@@ -2555,18 +2848,6 @@ function generateSupertestTs(collection, environment) {
2555
2848
  );
2556
2849
  return files;
2557
2850
  }
2558
- function slug(name) {
2559
- return name.replace(/\W+/g, "-").toLowerCase().replace(/^-|-$/g, "");
2560
- }
2561
- function toEnvVar(key) {
2562
- return key.replace(/\W+/g, "_").toUpperCase();
2563
- }
2564
- function interpolateValue(value, sharedVars = /* @__PURE__ */ new Set()) {
2565
- return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
2566
- const envKey = toEnvVar(key.trim());
2567
- return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
2568
- });
2569
- }
2570
2851
  function buildJestConfig() {
2571
2852
  return `/** @type {import('jest').Config} */
2572
2853
  module.exports = {
@@ -2577,9 +2858,7 @@ module.exports = {
2577
2858
  `;
2578
2859
  }
2579
2860
  function buildClient(environment) {
2580
- const baseUrl = environment?.variables.find(
2581
- (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
2582
- )?.value ?? "http://localhost:3000";
2861
+ const baseUrl = getEnvBaseUrl(environment, "http://localhost:3000");
2583
2862
  const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
2584
2863
  const envComments = secretVars.map((v) => `# ${toEnvVar(v.key)}=<from keychain>`).join("\n");
2585
2864
  return `const supertest = require('supertest');
@@ -2655,12 +2934,11 @@ ${lines.join("\n")}
2655
2934
  const req = requests[reqId];
2656
2935
  if (!req || req.disabled || req.hookType) continue;
2657
2936
  const method = req.method.toLowerCase();
2658
- const path2 = interpolateValue(req.url.replace(/^https?:\/\/[^/]+/, "") || "/", sharedVars);
2937
+ const path2 = interpolateEnvVars(req.url.replace(/^https?:\/\/[^/]+/, "") || "/", sharedVars);
2659
2938
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
2660
- const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
2661
- const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
2939
+ const effectiveAuth = resolveEffectiveAuth(req, inherited);
2940
+ const allHeaders = mergeHeaders(req, inherited);
2662
2941
  const enabledParams = req.params.filter((p) => p.enabled && p.key);
2663
- const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
2664
2942
  const lines = [];
2665
2943
  lines.push(` it('${nameMap.get(reqId)}', async () => {`);
2666
2944
  lines.push(` const res = await api`);
@@ -2668,29 +2946,29 @@ ${lines.join("\n")}
2668
2946
  if (effectiveAuth.type === "bearer") {
2669
2947
  const token = effectiveAuth.token ?? "";
2670
2948
  if (token.includes("{{")) {
2671
- lines.push(` .set('Authorization', \`Bearer ${interpolateValue(token, sharedVars)}\`)`);
2949
+ lines.push(` .set('Authorization', \`Bearer ${interpolateEnvVars(token, sharedVars)}\`)`);
2672
2950
  } else {
2673
2951
  const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
2674
2952
  lines.push(` .set('Authorization', \`Bearer \${process.env.${toEnvVar(ref)} ?? ''}\`)`);
2675
2953
  }
2676
2954
  }
2677
2955
  for (const h of allHeaders) {
2678
- lines.push(` .set('${h.key}', \`${interpolateValue(h.value, sharedVars)}\`)`);
2956
+ lines.push(` .set('${h.key}', \`${interpolateEnvVars(h.value, sharedVars)}\`)`);
2679
2957
  }
2680
2958
  if (enabledParams.length) {
2681
- const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue(p.value, sharedVars)}\``).join(", ");
2959
+ const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateEnvVars(p.value, sharedVars)}\``).join(", ");
2682
2960
  lines.push(` .query({ ${pairs} })`);
2683
2961
  }
2684
- if (hasBody) {
2962
+ if (hasBody(req)) {
2685
2963
  if (req.body.mode === "json") {
2686
2964
  const jsonBody = req.body.json ?? "{}";
2687
2965
  if (jsonBody.includes("{{")) {
2688
- lines.push(` .send(JSON.parse(\`${interpolateValue(jsonBody, sharedVars)}\`))`);
2966
+ lines.push(` .send(JSON.parse(\`${interpolateEnvVars(jsonBody, sharedVars)}\`))`);
2689
2967
  } else {
2690
2968
  lines.push(` .send(${jsonBody})`);
2691
2969
  }
2692
2970
  } else if (req.body.mode === "form" && req.body.form) {
2693
- const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue(p.value, sharedVars)}\``).join(", ");
2971
+ const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateEnvVars(p.value, sharedVars)}\``).join(", ");
2694
2972
  lines.push(` .type('form')`);
2695
2973
  lines.push(` .send({ ${pairs} })`);
2696
2974
  }
@@ -2755,27 +3033,8 @@ function buildPackageJson(collectionName) {
2755
3033
  }
2756
3034
  }, null, 2) + "\n";
2757
3035
  }
2758
- function renderTree$2(paths) {
2759
- const root = {};
2760
- for (const p of [...paths].sort()) {
2761
- let cur = root;
2762
- for (const part of p.split("/")) {
2763
- cur = cur[part] ??= {};
2764
- }
2765
- }
2766
- function render(node, prefix = "") {
2767
- const entries = Object.entries(node);
2768
- return entries.flatMap(([name, children], i) => {
2769
- const last = i === entries.length - 1;
2770
- const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
2771
- if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
2772
- return lines;
2773
- });
2774
- }
2775
- return [".", ...render(root)].join("\n");
2776
- }
2777
3036
  function buildReadme$2(collectionName, filePaths) {
2778
- const tree = renderTree$2([...filePaths, ".env.local"]);
3037
+ const tree = renderTree([...filePaths, ".env.local"]);
2779
3038
  return `# ${collectionName} — API Tests (Supertest + Jest JavaScript)
2780
3039
 
2781
3040
  ## Project structure
@@ -2823,9 +3082,6 @@ function generateSupertestJs(collection, environment) {
2823
3082
  );
2824
3083
  return files;
2825
3084
  }
2826
- function javaClass$1(name) {
2827
- return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
2828
- }
2829
3085
  function javaMethod(name) {
2830
3086
  const parts = name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean);
2831
3087
  return parts[0].toLowerCase() + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
@@ -2842,12 +3098,9 @@ function javaTypeFor(expected) {
2842
3098
  return "Object.class";
2843
3099
  }
2844
3100
  }
2845
- function toEnvConst(key) {
2846
- return key.replace(/\W+/g, "_").toUpperCase();
2847
- }
2848
3101
  function interpolateJava(value, sharedVars = /* @__PURE__ */ new Set()) {
2849
3102
  return '"' + value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
2850
- const envKey = toEnvConst(key.trim());
3103
+ const envKey = toEnvVar(key.trim());
2851
3104
  if (sharedVars.has(envKey)) {
2852
3105
  return `" + ${envKey} + "`;
2853
3106
  }
@@ -2922,12 +3175,10 @@ function buildPom$1(collectionName) {
2922
3175
  `;
2923
3176
  }
2924
3177
  function buildBaseTest(environment) {
2925
- const baseUrl = environment?.variables.find(
2926
- (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
2927
- )?.value ?? "http://localhost:8080";
3178
+ const baseUrl = getEnvBaseUrl(environment, "http://localhost:8080");
2928
3179
  const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
2929
3180
  const secretComments = secretVars.map(
2930
- (v) => ` * - ${toEnvConst(v.key)}=<value from keychain>`
3181
+ (v) => ` * - ${toEnvVar(v.key)}=<value from keychain>`
2931
3182
  ).join("\n");
2932
3183
  return `package com.example.api;
2933
3184
 
@@ -2966,7 +3217,7 @@ public class BaseTest {
2966
3217
  }
2967
3218
  function buildTestClass(folderName, folder, collection) {
2968
3219
  const requests = collection.requests;
2969
- const className = javaClass$1(folderName) + "Test";
3220
+ const className = javaClass(folderName) + "Test";
2970
3221
  const methods = [];
2971
3222
  const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
2972
3223
  const beforeAllH = hooks.beforeAll;
@@ -2986,7 +3237,7 @@ function buildTestClass(folderName, folder, collection) {
2986
3237
  lines.push(` .when().${method}("${path2}");`);
2987
3238
  for (const e of parsed.extractions) {
2988
3239
  const jp = accessorToJsonPath(e.accessor);
2989
- const varName = toEnvConst(e.varName);
3240
+ const varName = toEnvVar(e.varName);
2990
3241
  sharedVars.add(varName);
2991
3242
  lines.push(` ${varName} = hookResponse.jsonPath().getString("${jp}");`);
2992
3243
  }
@@ -3032,10 +3283,9 @@ ${lines.join("\n")}
3032
3283
  const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
3033
3284
  const javaPath = interpolateJava(path2, sharedVars);
3034
3285
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
3035
- const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
3036
- const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
3286
+ const effectiveAuth = resolveEffectiveAuth(req, inherited);
3287
+ const allHeaders = mergeHeaders(req, inherited);
3037
3288
  const enabledParams = req.params.filter((p) => p.enabled && p.key);
3038
- const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
3039
3289
  const lines = [];
3040
3290
  lines.push(` @Test`);
3041
3291
  lines.push(` public void ${methodName}() {`);
@@ -3045,14 +3295,14 @@ ${lines.join("\n")}
3045
3295
  const token = effectiveAuth.token ?? "";
3046
3296
  if (token.includes("{{")) {
3047
3297
  const varRef = token.match(/\{\{([^}]+)\}\}/)?.[1]?.trim();
3048
- const envKey = varRef ? toEnvConst(varRef) : "";
3298
+ const envKey = varRef ? toEnvVar(varRef) : "";
3049
3299
  if (envKey && sharedVars.has(envKey)) {
3050
3300
  lines.push(` .header("Authorization", "Bearer " + ${envKey})`);
3051
3301
  } else {
3052
3302
  lines.push(` .header("Authorization", "Bearer " + ${interpolateJava(token, sharedVars)})`);
3053
3303
  }
3054
3304
  } else {
3055
- lines.push(` .header("Authorization", "Bearer " + System.getenv("${toEnvConst(effectiveAuth.tokenSecretRef ?? "API_TOKEN")}"))`);
3305
+ lines.push(` .header("Authorization", "Bearer " + System.getenv("${toEnvVar(effectiveAuth.tokenSecretRef ?? "API_TOKEN")}"))`);
3056
3306
  }
3057
3307
  }
3058
3308
  for (const h of allHeaders) {
@@ -3061,7 +3311,7 @@ ${lines.join("\n")}
3061
3311
  for (const p of enabledParams) {
3062
3312
  lines.push(` .queryParam("${p.key}", ${interpolateJava(p.value, sharedVars)})`);
3063
3313
  }
3064
- if (hasBody) {
3314
+ if (hasBody(req)) {
3065
3315
  if (req.body.mode === "json" && req.body.json) {
3066
3316
  const escaped = req.body.json.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
3067
3317
  lines.push(` .body(${interpolateJava(escaped, sharedVars)})`);
@@ -3122,27 +3372,8 @@ ${methods.join("\n\n")}
3122
3372
  }
3123
3373
  `;
3124
3374
  }
3125
- function renderTree$1(paths) {
3126
- const root = {};
3127
- for (const p of [...paths].sort()) {
3128
- let cur = root;
3129
- for (const part of p.split("/")) {
3130
- cur = cur[part] ??= {};
3131
- }
3132
- }
3133
- function render(node, prefix = "") {
3134
- const entries = Object.entries(node);
3135
- return entries.flatMap(([name, children], i) => {
3136
- const last = i === entries.length - 1;
3137
- const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
3138
- if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
3139
- return lines;
3140
- });
3141
- }
3142
- return [".", ...render(root)].join("\n");
3143
- }
3144
3375
  function buildReadme$1(collectionName, filePaths) {
3145
- const tree = renderTree$1(filePaths);
3376
+ const tree = renderTree(filePaths);
3146
3377
  return `# ${collectionName} — API Tests (REST Assured + JUnit 5)
3147
3378
 
3148
3379
  ## Project structure
@@ -3171,7 +3402,7 @@ function generateRestAssured(collection, environment) {
3171
3402
  ];
3172
3403
  function processFolder(folder, name) {
3173
3404
  if (folder.requestIds.length > 0) {
3174
- const className = javaClass$1(name) + "Test";
3405
+ const className = javaClass(name) + "Test";
3175
3406
  files.push({
3176
3407
  path: `src/test/java/com/example/api/${className}.java`,
3177
3408
  content: buildTestClass(name, folder, collection)
@@ -3190,9 +3421,6 @@ function generateRestAssured(collection, environment) {
3190
3421
  files.unshift({ path: "README.md", content: buildReadme$1(collection.name, files.map((f) => f.path)) });
3191
3422
  return files;
3192
3423
  }
3193
- function javaClass(name) {
3194
- return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
3195
- }
3196
3424
  function jsVar(name) {
3197
3425
  const parts = name.replace(/[^a-zA-Z0-9]+/g, " ").split(/\s+/).filter(Boolean).map((p) => p.toLowerCase());
3198
3426
  if (parts.length === 0) return "_";
@@ -3290,9 +3518,7 @@ function buildPom(collectionName) {
3290
3518
  `;
3291
3519
  }
3292
3520
  function buildKarateConfig(environment) {
3293
- const baseUrl = environment?.variables.find(
3294
- (v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
3295
- )?.value ?? "http://localhost:8080";
3521
+ const baseUrl = getEnvBaseUrl(environment, "http://localhost:8080");
3296
3522
  const lines = [];
3297
3523
  lines.push(`function fn() {`);
3298
3524
  lines.push(` var env = karate.env || 'dev';`);
@@ -3364,7 +3590,7 @@ function buildBackground(folderId, collection) {
3364
3590
  for (const x of (h.headers ?? []).filter((x2) => x2.enabled && x2.key)) {
3365
3591
  lines.push(` * header ${x.key} = ${interpolateKarate(x.value)}`);
3366
3592
  }
3367
- if (h.body.mode !== "none" && !["get", "head"].includes(method)) {
3593
+ if (hasBody(h)) {
3368
3594
  for (const block of bodySteps(h.body, has)) {
3369
3595
  lines.push(` * ${block[0]}`);
3370
3596
  for (let i = 1; i < block.length; i++) lines.push(block[i]);
@@ -3453,11 +3679,10 @@ function buildFeature(folderName, folder, collection) {
3453
3679
  }
3454
3680
  usedTags.add(tag);
3455
3681
  const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
3456
- const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
3457
- const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
3682
+ const effectiveAuth = resolveEffectiveAuth(req, inherited);
3683
+ const allHeaders = mergeHeaders(req, inherited);
3458
3684
  const enabledParams = req.params.filter((p) => p.enabled && p.key);
3459
3685
  const method = req.method.toLowerCase();
3460
- const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
3461
3686
  const lines = [];
3462
3687
  lines.push(`@${tag}`);
3463
3688
  lines.push(`Scenario: ${req.name}`);
@@ -3487,7 +3712,7 @@ function buildFeature(folderName, folder, collection) {
3487
3712
  for (const p of enabledParams) {
3488
3713
  setup.push([`param ${p.key} = ${interpolateKarate(p.value)}`]);
3489
3714
  }
3490
- if (hasBody) {
3715
+ if (hasBody(req)) {
3491
3716
  const declared = new Set(allHeaders.map((h) => h.key.toLowerCase()));
3492
3717
  const has = (n) => declared.has(n.toLowerCase());
3493
3718
  for (const block of bodySteps(req.body, has)) setup.push(block);
@@ -3547,25 +3772,6 @@ ${bgBlock}
3547
3772
  ${scenarios.join("\n\n")}
3548
3773
  `;
3549
3774
  }
3550
- function renderTree(paths) {
3551
- const root = {};
3552
- for (const p of [...paths].sort()) {
3553
- let cur = root;
3554
- for (const part of p.split("/")) {
3555
- cur = cur[part] ??= {};
3556
- }
3557
- }
3558
- function render(node, prefix = "") {
3559
- const entries = Object.entries(node);
3560
- return entries.flatMap(([name, children], i) => {
3561
- const last = i === entries.length - 1;
3562
- const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
3563
- if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
3564
- return lines;
3565
- });
3566
- }
3567
- return [".", ...render(root)].join("\n");
3568
- }
3569
3775
  function buildReadme(collectionName, filePaths) {
3570
3776
  const tree = renderTree(filePaths);
3571
3777
  return `# ${collectionName} — API Tests (Karate + JUnit 5)
@@ -3626,8 +3832,106 @@ function generateKarate(collection, environment) {
3626
3832
  files.unshift({ path: "README.md", content: buildReadme(collection.name, files.map((f) => f.path)) });
3627
3833
  return files;
3628
3834
  }
3835
+ function mapOut(s) {
3836
+ return s.replace(/\{\{\s*(\$[A-Za-z]+)\s*\}\}/g, (_m, name) => `{{${SPECTOR_TO_HTTP[name] ?? name}}}`);
3837
+ }
3838
+ function orderedRequests(collection) {
3839
+ const out = [];
3840
+ const walk = (folder) => {
3841
+ for (const id of folder.requestIds) {
3842
+ const req = collection.requests[id];
3843
+ if (req) out.push(req);
3844
+ }
3845
+ for (const sub of folder.folders) walk(sub);
3846
+ };
3847
+ walk(collection.rootFolder);
3848
+ return out;
3849
+ }
3850
+ function authToHeader(auth) {
3851
+ switch (auth.type) {
3852
+ case "bearer":
3853
+ return auth.token ? { key: "Authorization", value: `Bearer ${auth.token}`, enabled: true } : null;
3854
+ case "basic": {
3855
+ const user = auth.username ?? "";
3856
+ const pass = auth.password ?? "";
3857
+ const templated = /\{\{/.test(user) || /\{\{/.test(pass);
3858
+ const value = templated ? `Basic {{base64(${user}:${pass})}}` : `Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}`;
3859
+ return { key: "Authorization", value, enabled: true };
3860
+ }
3861
+ case "apikey":
3862
+ if (auth.apiKeyIn === "query") return null;
3863
+ return auth.apiKeyName ? { key: auth.apiKeyName, value: auth.apiKeyValue ?? "", enabled: true } : null;
3864
+ default:
3865
+ return null;
3866
+ }
3867
+ }
3868
+ function buildUrl(req) {
3869
+ const query = (req.params ?? []).filter((p) => p.enabled && p.key && p.paramType !== "path");
3870
+ if (req.auth.type === "apikey" && req.auth.apiKeyIn === "query" && req.auth.apiKeyName) {
3871
+ query.push({ key: req.auth.apiKeyName, value: req.auth.apiKeyValue ?? "", enabled: true });
3872
+ }
3873
+ if (!query.length) return req.url;
3874
+ const qs = query.map((p) => `${p.key}=${p.value}`).join("&");
3875
+ return req.url + (req.url.includes("?") ? "&" : "?") + qs;
3876
+ }
3877
+ function bodyLines(req) {
3878
+ const b = req.body;
3879
+ switch (b.mode) {
3880
+ case "json":
3881
+ return { contentType: "application/json", text: b.json ?? "" };
3882
+ case "graphql":
3883
+ return { contentType: "application/json", text: b.graphql?.query ?? "" };
3884
+ case "soap":
3885
+ return { contentType: "text/xml", text: b.soap?.envelope ?? "" };
3886
+ case "raw":
3887
+ return { contentType: b.rawContentType, text: b.raw ?? "" };
3888
+ case "form":
3889
+ return {
3890
+ contentType: "application/x-www-form-urlencoded",
3891
+ text: (b.form ?? []).filter((f) => f.enabled && f.key).map((f) => `${encodeURIComponent(f.key)}=${encodeURIComponent(f.value)}`).join("&")
3892
+ };
3893
+ default:
3894
+ return {};
3895
+ }
3896
+ }
3897
+ function generateHttpFile(collection, environment) {
3898
+ const lines = [];
3899
+ const vars = {};
3900
+ for (const v of environment?.variables ?? []) {
3901
+ if (v.enabled && !v.secret) vars[v.key] = v.value;
3902
+ }
3903
+ Object.assign(vars, collection.collectionVariables ?? {});
3904
+ const varKeys = Object.keys(vars);
3905
+ if (varKeys.length) {
3906
+ for (const k of varKeys) lines.push(`@${k} = ${mapOut(vars[k])}`);
3907
+ lines.push("");
3908
+ }
3909
+ orderedRequests(collection).forEach((req, idx) => {
3910
+ if (idx > 0) lines.push("");
3911
+ lines.push(`### ${req.name}`);
3912
+ if (req.description?.trim()) {
3913
+ for (const l of req.description.trim().split(/\r?\n/)) lines.push(`# ${l}`);
3914
+ }
3915
+ lines.push(`${req.method} ${mapOut(buildUrl(req))}`);
3916
+ const headers = [...(req.headers ?? []).filter((h) => h.enabled && h.key)];
3917
+ const authHeader = authToHeader(req.auth);
3918
+ if (authHeader) headers.unshift(authHeader);
3919
+ const body = bodyLines(req);
3920
+ const hasCT = headers.some((h) => h.key.toLowerCase() === "content-type");
3921
+ if (body.text && body.contentType && !hasCT) {
3922
+ headers.push({ key: "Content-Type", value: body.contentType, enabled: true });
3923
+ }
3924
+ for (const h of headers) lines.push(`${h.key}: ${mapOut(h.value)}`);
3925
+ if (body.text) {
3926
+ lines.push("");
3927
+ lines.push(mapOut(body.text));
3928
+ }
3929
+ });
3930
+ const slug2 = collection.name.replace(/\W+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "collection";
3931
+ return [{ path: `${slug2}.http`, content: lines.join("\n") + "\n" }];
3932
+ }
3629
3933
  function registerGenerateHandlers(ipc) {
3630
- ipc.handle("generate:code", (_e, opts) => {
3934
+ handle.handleIpc(ipc, handle.IPC.generate.code, (_e, opts) => {
3631
3935
  const { collection, environment, target } = opts;
3632
3936
  switch (target) {
3633
3937
  case "robot_framework":
@@ -3644,18 +3948,20 @@ function registerGenerateHandlers(ipc) {
3644
3948
  return generateRestAssured(collection, environment);
3645
3949
  case "karate":
3646
3950
  return generateKarate(collection, environment);
3951
+ case "http_file":
3952
+ return generateHttpFile(collection, environment);
3647
3953
  default:
3648
3954
  throw new Error(`Unknown target: ${target}`);
3649
3955
  }
3650
3956
  });
3651
- ipc.handle("generate:save", async (_e, files, outputDir) => {
3957
+ handle.handleIpc(ipc, handle.IPC.generate.save, async (_e, files, outputDir) => {
3652
3958
  for (const file of files) {
3653
3959
  const fullPath = path.join(outputDir, file.path);
3654
3960
  await promises.mkdir(path.dirname(fullPath), { recursive: true });
3655
3961
  await promises.writeFile(fullPath, file.content, "utf8");
3656
3962
  }
3657
3963
  });
3658
- ipc.handle("generate:saveZip", async (_e, files, collectionName, target) => {
3964
+ handle.handleIpc(ipc, handle.IPC.generate.saveZip, async (_e, files, collectionName, target) => {
3659
3965
  const colSlug = collectionName.replace(/\W+/g, "-").toLowerCase();
3660
3966
  const targetSlug = target.replace(/_/g, "-");
3661
3967
  const defaultName = `${colSlug}-${targetSlug}.zip`;
@@ -3672,294 +3978,23 @@ function registerGenerateHandlers(ipc) {
3672
3978
  return true;
3673
3979
  });
3674
3980
  }
3675
- async function buildDispatcher(proxy, tls) {
3676
- const connectOpts = {};
3677
- let hasTls = false;
3678
- if (tls) {
3679
- hasTls = true;
3680
- if (tls.rejectUnauthorized !== void 0) connectOpts["rejectUnauthorized"] = tls.rejectUnauthorized;
3681
- if (tls.caCertPath) {
3682
- try {
3683
- connectOpts["ca"] = await promises.readFile(tls.caCertPath);
3684
- } catch {
3685
- }
3686
- }
3687
- if (tls.clientCertPath) {
3688
- try {
3689
- connectOpts["cert"] = await promises.readFile(tls.clientCertPath);
3690
- } catch {
3691
- }
3692
- }
3693
- if (tls.clientKeyPath) {
3694
- try {
3695
- connectOpts["key"] = await promises.readFile(tls.clientKeyPath);
3696
- } catch {
3697
- }
3698
- }
3699
- }
3700
- if (proxy?.url) {
3701
- return new undici.ProxyAgent({
3702
- uri: proxyUri,
3703
- requestTls: proxyConnect,
3704
- proxyTls: proxyConnect
3705
- });
3706
- }
3707
- if (hasTls) return new undici.Agent({ connect: connectOpts });
3708
- return void 0;
3709
- }
3710
- async function executeOne(req, collectionVars, envVars, globals, localVars, dispatcher, piiMaskPatterns, proxy, tls) {
3711
- if (!req.headers) req.headers = [];
3712
- if (!req.params) req.params = [];
3713
- if (!req.body) req.body = { mode: "none" };
3714
- if (!req.auth) req.auth = { type: "none" };
3715
- const base = {
3716
- requestId: req.id,
3717
- name: req.name,
3718
- method: req.method,
3719
- resolvedUrl: "",
3720
- status: "running"
3721
- };
3722
- const dynamicVars = await authBuilder.buildDynamicVars();
3723
- let vars = authBuilder.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
3724
- let updatedEnvVars = { ...envVars };
3725
- let updatedCollectionVars = { ...collectionVars };
3726
- let updatedGlobals = { ...globals };
3727
- let preScriptError;
3728
- if (req.preRequestScript?.trim()) {
3729
- const r = await requestCollection.runScript(authBuilder.interpolate(req.preRequestScript, vars), {
3730
- envVars: { ...envVars },
3731
- collectionVars: { ...collectionVars },
3732
- globals: { ...globals },
3733
- localVars: {},
3734
- piiMaskPatterns
3735
- });
3736
- preScriptError = r.error;
3737
- localVars = r.updatedLocalVars;
3738
- updatedEnvVars = r.updatedEnvVars;
3739
- updatedCollectionVars = r.updatedCollectionVars;
3740
- updatedGlobals = r.updatedGlobals;
3741
- requestCollection.patchGlobals(r.updatedGlobals);
3742
- await requestCollection.persistGlobals();
3743
- vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
3744
- }
3745
- const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
3746
- base.resolvedUrl = resolvedUrl;
3747
- const start = Date.now();
3748
- try {
3749
- if (req.auth.type === "oauth2") {
3750
- const now = Date.now();
3751
- const tokenMissing = !req.auth.oauth2CachedToken;
3752
- const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
3753
- if (tokenMissing || tokenExpired) {
3754
- const result = await authBuilder.fetchOAuth2Token(req.auth, vars);
3755
- req.auth.oauth2CachedToken = result.accessToken;
3756
- req.auth.oauth2TokenExpiry = result.expiresAt;
3757
- }
3758
- }
3759
- const authHeaders = await authBuilder.buildAuthHeaders(req.auth, vars);
3760
- const headers = new undici.Headers();
3761
- for (const h of req.headers) {
3762
- if (h.enabled && h.key) headers.set(authBuilder.interpolate(h.key, vars), authBuilder.interpolate(h.value, vars));
3763
- }
3764
- for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
3765
- let body;
3766
- if (req.body.mode === "json" && req.body.json) {
3767
- body = authBuilder.interpolate(req.body.json, vars);
3768
- if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
3769
- } else if (req.body.mode === "raw" && req.body.raw) {
3770
- body = authBuilder.interpolate(req.body.raw, vars);
3771
- if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
3772
- } else if (req.body.mode === "graphql" && req.body.graphql) {
3773
- const gql = req.body.graphql;
3774
- const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
3775
- const rawVars = gql.variables?.trim();
3776
- if (rawVars) {
3777
- try {
3778
- gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
3779
- } catch {
3780
- }
3781
- }
3782
- if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
3783
- body = JSON.stringify(gqlBody);
3784
- if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
3785
- } else if (req.body.mode === "soap" && req.body.soap) {
3786
- body = authBuilder.interpolate(req.body.soap.envelope, vars);
3787
- if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
3788
- if (req.body.soap.soapAction && !headers.has("soapaction")) {
3789
- headers.set("SOAPAction", req.body.soap.soapAction);
3790
- }
3791
- }
3792
- const methodHasBody = !["GET", "HEAD"].includes(req.method);
3793
- const doFetch = (h) => undici.fetch(resolvedUrl, {
3794
- method: req.method,
3795
- headers: h,
3796
- body: methodHasBody ? body : void 0,
3797
- dispatcher
3798
- });
3799
- let fetchResp;
3800
- if (req.auth.type === "ntlm") {
3801
- const capturedHeaders = {};
3802
- headers.forEach((v, k) => {
3803
- capturedHeaders[k] = v;
3804
- });
3805
- fetchResp = await authBuilder.performNtlmRequest({
3806
- url: resolvedUrl,
3807
- method: req.method,
3808
- auth: req.auth,
3809
- vars,
3810
- baseHeaders: capturedHeaders,
3811
- body: methodHasBody ? body : void 0,
3812
- tls,
3813
- proxy
3814
- });
3815
- } else if (req.auth.type === "digest") {
3816
- const probeFetch = (url, init) => undici.fetch(url, {
3817
- ...init,
3818
- dispatcher
3819
- });
3820
- const digestHeader = await authBuilder.performDigestAuth(resolvedUrl, req.method, req.auth, vars, probeFetch);
3821
- if (digestHeader) headers.set("Authorization", digestHeader);
3822
- fetchResp = await doFetch(headers);
3823
- } else {
3824
- fetchResp = await doFetch(headers);
3825
- }
3826
- const responseBody = await fetchResp.text();
3827
- const durationMs = Date.now() - start;
3828
- const rawRespHeaders = {};
3829
- fetchResp.headers.forEach((v, k) => {
3830
- rawRespHeaders[k] = v;
3831
- });
3832
- const maskedBody = requestCollection.maskPii(responseBody, piiMaskPatterns);
3833
- const maskedHeaders = requestCollection.maskHeaders(rawRespHeaders, piiMaskPatterns);
3834
- const scriptResponse = {
3835
- status: fetchResp.status,
3836
- statusText: fetchResp.statusText,
3837
- headers: rawRespHeaders,
3838
- body: responseBody,
3839
- bodySize: Buffer.byteLength(responseBody, "utf8"),
3840
- durationMs
3841
- };
3842
- const schemaTestResults = requestCollection.buildSchemaTestResults(req.schema, responseBody);
3843
- const protocolFaultTests = requestCollection.buildProtocolFaultTests(req.body.mode, responseBody);
3844
- let testResults = [...schemaTestResults, ...protocolFaultTests];
3845
- let consoleOutput = [];
3846
- let postScriptError;
3847
- if (req.postRequestScript?.trim()) {
3848
- const r = await requestCollection.runScript(authBuilder.interpolate(req.postRequestScript, vars), {
3849
- envVars: updatedEnvVars,
3850
- collectionVars: updatedCollectionVars,
3851
- globals: updatedGlobals,
3852
- localVars,
3853
- response: scriptResponse,
3854
- piiMaskPatterns
3855
- });
3856
- testResults = [...schemaTestResults, ...protocolFaultTests, ...r.testResults];
3857
- consoleOutput = r.consoleOutput;
3858
- postScriptError = r.error;
3859
- updatedEnvVars = r.updatedEnvVars;
3860
- updatedCollectionVars = r.updatedCollectionVars;
3861
- updatedGlobals = r.updatedGlobals;
3862
- localVars = r.updatedLocalVars;
3863
- requestCollection.patchGlobals(r.updatedGlobals);
3864
- await requestCollection.persistGlobals();
3865
- }
3866
- const allPassed = testResults.every((t) => t.passed);
3867
- const httpFailed = fetchResp.status >= 400;
3868
- const hasTests = testResults.length > 0;
3869
- const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "passed";
3870
- if (httpFailed && testResults.length === 0) {
3871
- testResults = [
3872
- ...testResults,
3873
- {
3874
- name: `HTTP status ${fetchResp.status} ${fetchResp.statusText}`.trim(),
3875
- passed: false,
3876
- error: `Request returned ${fetchResp.status} — no assertion was defined to verify the status code.`
3877
- }
3878
- ];
3879
- }
3880
- const sentHeaders = {};
3881
- headers.forEach((v, k) => {
3882
- sentHeaders[k] = v;
3883
- });
3884
- return {
3885
- result: {
3886
- ...base,
3887
- status,
3888
- httpStatus: fetchResp.status,
3889
- durationMs,
3890
- testResults,
3891
- consoleOutput,
3892
- preScriptError,
3893
- postScriptError,
3894
- sentRequest: { headers: sentHeaders, body: body ?? void 0 },
3895
- receivedResponse: {
3896
- status: fetchResp.status,
3897
- statusText: fetchResp.statusText,
3898
- headers: maskedHeaders,
3899
- body: maskedBody
3900
- }
3901
- },
3902
- updatedEnvVars,
3903
- updatedCollectionVars,
3904
- updatedGlobals,
3905
- updatedLocalVars: localVars
3906
- };
3907
- } catch (err) {
3908
- return {
3909
- result: {
3910
- ...base,
3911
- status: "error",
3912
- durationMs: Date.now() - start,
3913
- error: err instanceof Error ? err.cause instanceof Error ? `${err.message}: ${err.cause.message}` : err.message : String(err),
3914
- preScriptError
3915
- },
3916
- updatedEnvVars,
3917
- updatedCollectionVars,
3918
- updatedGlobals,
3919
- updatedLocalVars: localVars
3920
- };
3921
- }
3922
- }
3923
3981
  const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
3924
3982
  function registerRunnerHandler(ipc) {
3925
- ipc.handle("runner:start", async (event, payload) => {
3983
+ handle.handleIpc(ipc, handle.IPC.runner.start, async (event, payload) => {
3926
3984
  const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
3927
3985
  const envVars = await authBuilder.buildEnvVars(environment);
3928
3986
  const liveGlobals = requestCollection.getGlobals();
3929
3987
  const globals = { ...payloadGlobals, ...liveGlobals };
3930
- const dispatcher = await buildDispatcher(proxy, tls);
3988
+ const dispatcher = await requestCollection.buildDispatcher(proxy, tls);
3931
3989
  const summary = { total: items.length, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
3932
3990
  const totalStart = Date.now();
3933
3991
  let runEnvVars = { ...envVars };
3934
3992
  let runCollectionVars = {};
3935
3993
  let runGlobals = { ...globals };
3936
3994
  let runLocalVars = {};
3937
- const failedScopes = /* @__PURE__ */ new Set();
3938
- const skipRequests = /* @__PURE__ */ new Set();
3995
+ const skipTracker = new requestCollection.HookSkipTracker();
3939
3996
  for (const item of items) {
3940
- const { isHook, hookType, scopeId, scopeAncestors, mainRequestId } = item;
3941
- let skipReason;
3942
- if (isHook) {
3943
- if (hookType === "beforeAll") {
3944
- if ((scopeAncestors ?? []).some((id) => failedScopes.has(id))) {
3945
- skipReason = "Skipped — outer scope hook failed";
3946
- }
3947
- } else if (hookType === "before") {
3948
- const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
3949
- if (allScopes.some((id) => failedScopes.has(id))) {
3950
- skipReason = "Skipped — scope hook failed";
3951
- } else if (mainRequestId && skipRequests.has(mainRequestId)) {
3952
- skipReason = "Skipped — before hook failed";
3953
- }
3954
- }
3955
- } else {
3956
- const allScopes = [...item.scopeAncestors ?? [], item.scopeId].filter(Boolean);
3957
- if (allScopes.some((id) => failedScopes.has(id))) {
3958
- skipReason = "Skipped — beforeAll hook failed";
3959
- } else if (skipRequests.has(item.request.id)) {
3960
- skipReason = "Skipped — before hook failed";
3961
- }
3962
- }
3997
+ const skipReason = skipTracker.shouldSkip(item);
3963
3998
  if (skipReason) {
3964
3999
  const skipped = {
3965
4000
  requestId: item.request.id,
@@ -3975,7 +4010,7 @@ function registerRunnerHandler(ipc) {
3975
4010
  iterationLabel: item.iterationLabel
3976
4011
  };
3977
4012
  summary.failed++;
3978
- event.sender.send("runner:progress", skipped);
4013
+ event.sender.send(handle.IPC.runner.progress, skipped);
3979
4014
  continue;
3980
4015
  }
3981
4016
  const runningUpdate = {
@@ -3986,35 +4021,28 @@ function registerRunnerHandler(ipc) {
3986
4021
  scopeId: item.scopeId,
3987
4022
  scopePath: item.scopePath
3988
4023
  };
3989
- event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
3990
- const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeOne(
3991
- item.request,
3992
- { ...item.collectionVars, ...runCollectionVars },
3993
- runEnvVars,
3994
- runGlobals,
3995
- { ...runLocalVars, ...item.dataRow ?? {} },
4024
+ event.sender.send(handle.IPC.runner.progress, { requestId: item.request.id, ...runningUpdate });
4025
+ const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await requestCollection.executeRunnerRequest({
4026
+ req: item.request,
4027
+ collectionVars: { ...item.collectionVars, ...runCollectionVars },
4028
+ envVars: runEnvVars,
4029
+ globals: runGlobals,
4030
+ localVars: { ...runLocalVars, ...item.dataRow ?? {} },
3996
4031
  dispatcher,
3997
4032
  piiMaskPatterns,
3998
4033
  proxy,
3999
4034
  tls
4000
- );
4035
+ });
4001
4036
  runEnvVars = updatedEnvVars;
4002
4037
  runCollectionVars = updatedCollectionVars;
4003
4038
  runGlobals = updatedGlobals;
4004
4039
  runLocalVars = updatedLocalVars;
4005
- const hookFailed = result.status === "failed" || result.status === "error";
4006
- if (isHook && hookFailed) {
4007
- if (hookType === "beforeAll" && scopeId) {
4008
- failedScopes.add(scopeId);
4009
- } else if (hookType === "before" && mainRequestId) {
4010
- skipRequests.add(mainRequestId);
4011
- }
4012
- }
4040
+ skipTracker.recordResult(item, result.status);
4013
4041
  if (result.status === "passed") summary.passed++;
4014
4042
  else if (result.status === "failed") summary.failed++;
4015
4043
  else if (result.status === "skipped") summary.skipped++;
4016
4044
  else summary.errors++;
4017
- event.sender.send("runner:progress", {
4045
+ event.sender.send(handle.IPC.runner.progress, {
4018
4046
  ...result,
4019
4047
  iterationLabel: item.iterationLabel,
4020
4048
  isHook: item.isHook,
@@ -4031,26 +4059,26 @@ function registerRunnerHandler(ipc) {
4031
4059
  });
4032
4060
  }
4033
4061
  function registerMockHandlers(ipc) {
4034
- ipc.handle("mock:start", async (e, server) => {
4035
- mockServer.setHitCallback((hit) => e.sender.send("mock:hit", hit));
4062
+ handle.handleIpc(ipc, handle.IPC.mock.start, async (e, server) => {
4063
+ mockServer.setHitCallback((hit) => e.sender.send(handle.IPC.mock.hit, hit));
4036
4064
  await mockServer.startMock(server);
4037
4065
  });
4038
- ipc.handle("mock:stop", async (_e, id) => {
4066
+ handle.handleIpc(ipc, handle.IPC.mock.stop, async (_e, id) => {
4039
4067
  await mockServer.stopMock(id);
4040
4068
  });
4041
- ipc.handle("mock:isRunning", (_e, id) => mockServer.isRunning(id));
4042
- ipc.handle("mock:updateRoutes", (_e, id, routes) => {
4069
+ handle.handleIpc(ipc, handle.IPC.mock.isRunning, (_e, id) => mockServer.isRunning(id));
4070
+ handle.handleIpc(ipc, handle.IPC.mock.updateRoutes, (_e, id, routes) => {
4043
4071
  mockServer.updateMockRoutes(id, routes);
4044
4072
  });
4045
- ipc.handle("mock:runningIds", () => mockServer.getRunningIds());
4046
- ipc.handle("file:saveMock", async (_e, relPath, server) => {
4073
+ handle.handleIpc(ipc, handle.IPC.mock.runningIds, () => mockServer.getRunningIds());
4074
+ handle.handleIpc(ipc, handle.IPC.file.saveMock, async (_e, relPath, server) => {
4047
4075
  const wsDir = getWorkspaceDir();
4048
4076
  if (!wsDir) throw new Error("No workspace open");
4049
4077
  const fullPath = path.join(wsDir, relPath);
4050
4078
  await promises.mkdir(path.dirname(fullPath), { recursive: true });
4051
4079
  await promises.writeFile(fullPath, JSON.stringify(server, null, 2), "utf8");
4052
4080
  });
4053
- ipc.handle("file:loadMock", async (_e, relPath) => {
4081
+ handle.handleIpc(ipc, handle.IPC.file.loadMock, async (_e, relPath) => {
4054
4082
  const wsDir = getWorkspaceDir();
4055
4083
  if (!wsDir) throw new Error("No workspace open");
4056
4084
  const raw = await promises.readFile(path.join(wsDir, relPath), "utf8");
@@ -4058,7 +4086,7 @@ function registerMockHandlers(ipc) {
4058
4086
  });
4059
4087
  }
4060
4088
  function registerOAuth2Handlers(ipc) {
4061
- ipc.handle("oauth2:startFlow", async (_e, auth, vars) => {
4089
+ handle.handleIpc(ipc, handle.IPC.oauth2.startFlow, async (_e, auth, vars) => {
4062
4090
  const port = auth.oauth2RedirectPort ?? 9876;
4063
4091
  const redirectUri = `http://localhost:${port}/callback`;
4064
4092
  const authUrl = authBuilder.interpolate(auth.oauth2AuthUrl ?? "", vars);
@@ -4142,7 +4170,7 @@ function registerOAuth2Handlers(ipc) {
4142
4170
  refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
4143
4171
  };
4144
4172
  });
4145
- ipc.handle("oauth2:refreshToken", async (_e, auth, vars, refreshToken) => {
4173
+ handle.handleIpc(ipc, handle.IPC.oauth2.refreshToken, async (_e, auth, vars, refreshToken) => {
4146
4174
  const tokenUrl = authBuilder.interpolate(auth.oauth2TokenUrl ?? "", vars);
4147
4175
  const clientId = authBuilder.interpolate(auth.oauth2ClientId ?? "", vars);
4148
4176
  let clientSecret = auth.oauth2ClientSecret ?? "";
@@ -4188,17 +4216,17 @@ function closeAllWsConnections() {
4188
4216
  connections.clear();
4189
4217
  }
4190
4218
  function registerWsHandlers(ipc) {
4191
- ipc.handle("ws:connect", async (event, requestId, url, headers) => {
4219
+ handle.handleIpc(ipc, handle.IPC.ws.connect, async (event, requestId, url, headers) => {
4192
4220
  const existing = connections.get(requestId);
4193
4221
  if (existing) {
4194
4222
  existing.close();
4195
4223
  connections.delete(requestId);
4196
4224
  }
4197
- event.sender.send("ws:status", { requestId, status: "connecting" });
4225
+ event.sender.send(handle.IPC.ws.status, { requestId, status: "connecting" });
4198
4226
  const ws = new WebSocket(url, { headers });
4199
4227
  connections.set(requestId, ws);
4200
4228
  ws.on("open", () => {
4201
- event.sender.send("ws:status", { requestId, status: "connected" });
4229
+ event.sender.send(handle.IPC.ws.status, { requestId, status: "connected" });
4202
4230
  });
4203
4231
  ws.on("message", (data) => {
4204
4232
  const message = {
@@ -4207,25 +4235,25 @@ function registerWsHandlers(ipc) {
4207
4235
  data: data.toString(),
4208
4236
  timestamp: Date.now()
4209
4237
  };
4210
- event.sender.send("ws:message", { requestId, message });
4238
+ event.sender.send(handle.IPC.ws.message, { requestId, message });
4211
4239
  });
4212
4240
  ws.on("error", (err) => {
4213
- event.sender.send("ws:status", { requestId, status: "error", error: err.message });
4241
+ event.sender.send(handle.IPC.ws.status, { requestId, status: "error", error: err.message });
4214
4242
  connections.delete(requestId);
4215
4243
  });
4216
4244
  ws.on("close", () => {
4217
- event.sender.send("ws:status", { requestId, status: "disconnected" });
4245
+ event.sender.send(handle.IPC.ws.status, { requestId, status: "disconnected" });
4218
4246
  connections.delete(requestId);
4219
4247
  });
4220
4248
  });
4221
- ipc.handle("ws:send", async (_event, requestId, data) => {
4249
+ handle.handleIpc(ipc, handle.IPC.ws.send, async (_event, requestId, data) => {
4222
4250
  const ws = connections.get(requestId);
4223
4251
  if (!ws || ws.readyState !== WebSocket.OPEN) {
4224
4252
  throw new Error("WebSocket is not connected");
4225
4253
  }
4226
4254
  ws.send(data);
4227
4255
  });
4228
- ipc.handle("ws:disconnect", async (_event, requestId) => {
4256
+ handle.handleIpc(ipc, handle.IPC.ws.disconnect, async (_event, requestId) => {
4229
4257
  const ws = connections.get(requestId);
4230
4258
  if (ws) {
4231
4259
  ws.close();
@@ -4516,7 +4544,7 @@ ${body}
4516
4544
  </html>`;
4517
4545
  }
4518
4546
  function registerDocsHandlers(ipc) {
4519
- ipc.handle("docs:generate", async (_event, payload) => {
4547
+ handle.handleIpc(ipc, handle.IPC.docs.generate, async (_event, payload) => {
4520
4548
  if (payload.format === "html") {
4521
4549
  return generateHtml(payload);
4522
4550
  }
@@ -4564,9 +4592,9 @@ async function resolveSnapshotSpec(relPath) {
4564
4592
  return { specPath: tmp };
4565
4593
  }
4566
4594
  function registerContractHandlers(ipc) {
4567
- ipc.handle("contract:run", async (_e, payload) => {
4595
+ handle.handleIpc(ipc, handle.IPC.contract.run, async (_e, payload) => {
4568
4596
  ipcValidate.validateContractRunPayload(payload);
4569
- const { mode, requests, envVars, collectionVars = {}, requestBaseUrl } = payload;
4597
+ const { mode, requests, envVars, collectionVars = {}, requestBaseUrl, providerBaseUrl, stateHandlerUrl } = payload;
4570
4598
  let { specUrl, specPath } = payload;
4571
4599
  if (payload.specSnapshotRelPath) {
4572
4600
  const resolved = await resolveSnapshotSpec(payload.specSnapshotRelPath);
@@ -4578,15 +4606,27 @@ function registerContractHandlers(ipc) {
4578
4606
  return snapshots.runConsumerContracts(requests, envVars, collectionVars);
4579
4607
  case "provider":
4580
4608
  return snapshots.runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl);
4609
+ case "provider-live":
4610
+ return snapshots.runLiveProviderVerification(requests, envVars, collectionVars, providerBaseUrl, stateHandlerUrl);
4581
4611
  case "bidirectional":
4582
4612
  return snapshots.runBidirectional(requests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
4583
4613
  }
4584
4614
  });
4585
- ipc.handle("contract:inferSchema", (_e, jsonBody) => {
4615
+ handle.handleIpc(ipc, handle.IPC.contract.inferSchema, (_e, jsonBody) => {
4586
4616
  const schema = inferSchemaFromJson(jsonBody);
4587
4617
  return schema ? JSON.stringify(schema, null, 2) : null;
4588
4618
  });
4589
- ipc.handle("contract:captureSnapshot", async (_e, opts) => {
4619
+ handle.handleIpc(ipc, handle.IPC.contract.exportReportHtml, async (_e, report, meta = {}) => {
4620
+ const { canceled, filePath } = await electron.dialog.showSaveDialog({
4621
+ title: "Save contract report",
4622
+ defaultPath: `contract-report-${report.mode}.html`,
4623
+ filters: [{ name: "HTML", extensions: ["html"] }]
4624
+ });
4625
+ if (canceled || !filePath) return false;
4626
+ await promises.writeFile(filePath, snapshots.reportToHtml(report, { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...meta }), "utf8");
4627
+ return true;
4628
+ });
4629
+ handle.handleIpc(ipc, handle.IPC.contract.captureSnapshot, async (_e, opts) => {
4590
4630
  const dir = getWorkspaceDir();
4591
4631
  if (!dir) throw new Error("No workspace open — cannot capture snapshot.");
4592
4632
  const snapshot = await snapshots.captureSnapshot(dir, opts);
@@ -4594,17 +4634,17 @@ function registerContractHandlers(ipc) {
4594
4634
  if (!relPath) throw new Error("Snapshot created but relPath was not attached.");
4595
4635
  return { relPath, snapshot };
4596
4636
  });
4597
- ipc.handle("contract:listSnapshots", async (_e, registered = []) => {
4637
+ handle.handleIpc(ipc, handle.IPC.contract.listSnapshots, async (_e, registered = []) => {
4598
4638
  const dir = getWorkspaceDir();
4599
4639
  if (!dir) return [];
4600
4640
  return snapshots.listSnapshots(dir, registered);
4601
4641
  });
4602
- ipc.handle("contract:loadSnapshot", async (_e, relPath) => {
4642
+ handle.handleIpc(ipc, handle.IPC.contract.loadSnapshot, async (_e, relPath) => {
4603
4643
  const dir = getWorkspaceDir();
4604
4644
  if (!dir) throw new Error("No workspace open.");
4605
4645
  return snapshots.loadSnapshot(dir, relPath);
4606
4646
  });
4607
- ipc.handle("contract:deleteSnapshot", async (_e, relPath) => {
4647
+ handle.handleIpc(ipc, handle.IPC.contract.deleteSnapshot, async (_e, relPath) => {
4608
4648
  const dir = getWorkspaceDir();
4609
4649
  if (!dir) return;
4610
4650
  await snapshots.deleteSnapshot(dir, relPath);
@@ -4616,7 +4656,7 @@ function git() {
4616
4656
  return simpleGit.simpleGit(dir);
4617
4657
  }
4618
4658
  function registerGitHandlers(ipc) {
4619
- ipc.handle("git:isRepo", async () => {
4659
+ handle.handleIpc(ipc, handle.IPC.git.isRepo, async () => {
4620
4660
  const dir = getWorkspaceDir();
4621
4661
  if (!dir) return false;
4622
4662
  try {
@@ -4626,12 +4666,12 @@ function registerGitHandlers(ipc) {
4626
4666
  return false;
4627
4667
  }
4628
4668
  });
4629
- ipc.handle("git:init", async () => {
4669
+ handle.handleIpc(ipc, handle.IPC.git.init, async () => {
4630
4670
  await git().init();
4631
4671
  const dir = getWorkspaceDir();
4632
4672
  if (dir) await ensureGitignore(dir);
4633
4673
  });
4634
- ipc.handle("git:status", async () => {
4674
+ handle.handleIpc(ipc, handle.IPC.git.status, async () => {
4635
4675
  const result = await git().status();
4636
4676
  return {
4637
4677
  staged: result.staged.map((f) => ({ path: f, status: resolveStatus(result, f, true) })),
@@ -4644,40 +4684,40 @@ function registerGitHandlers(ipc) {
4644
4684
  remote: result.tracking ?? null
4645
4685
  };
4646
4686
  });
4647
- ipc.handle("git:resolveOurs", async (_e, filePath) => {
4687
+ handle.handleIpc(ipc, handle.IPC.git.resolveOurs, async (_e, filePath) => {
4648
4688
  const g = git();
4649
4689
  await g.checkout(["--ours", "--", filePath]);
4650
4690
  await g.add([filePath]);
4651
4691
  });
4652
- ipc.handle("git:resolveTheirs", async (_e, filePath) => {
4692
+ handle.handleIpc(ipc, handle.IPC.git.resolveTheirs, async (_e, filePath) => {
4653
4693
  const g = git();
4654
4694
  await g.checkout(["--theirs", "--", filePath]);
4655
4695
  await g.add([filePath]);
4656
4696
  });
4657
- ipc.handle("git:markResolved", async (_e, filePath) => {
4697
+ handle.handleIpc(ipc, handle.IPC.git.markResolved, async (_e, filePath) => {
4658
4698
  await git().add([filePath]);
4659
4699
  });
4660
- ipc.handle("git:diff", async (_e, filePath) => {
4700
+ handle.handleIpc(ipc, handle.IPC.git.diff, async (_e, filePath) => {
4661
4701
  if (filePath) return git().diff(["--", filePath]);
4662
4702
  return git().diff();
4663
4703
  });
4664
- ipc.handle("git:diffStaged", async (_e, filePath) => {
4704
+ handle.handleIpc(ipc, handle.IPC.git.diffStaged, async (_e, filePath) => {
4665
4705
  if (filePath) return git().diff(["--cached", "--", filePath]);
4666
4706
  return git().diff(["--cached"]);
4667
4707
  });
4668
- ipc.handle("git:stage", async (_e, paths) => {
4708
+ handle.handleIpc(ipc, handle.IPC.git.stage, async (_e, paths) => {
4669
4709
  await git().add(paths);
4670
4710
  });
4671
- ipc.handle("git:unstage", async (_e, paths) => {
4711
+ handle.handleIpc(ipc, handle.IPC.git.unstage, async (_e, paths) => {
4672
4712
  await git().reset(["HEAD", "--", ...paths]);
4673
4713
  });
4674
- ipc.handle("git:stageAll", async () => {
4714
+ handle.handleIpc(ipc, handle.IPC.git.stageAll, async () => {
4675
4715
  await git().add(["."]);
4676
4716
  });
4677
- ipc.handle("git:commit", async (_e, message) => {
4717
+ handle.handleIpc(ipc, handle.IPC.git.commit, async (_e, message) => {
4678
4718
  await git().commit(message);
4679
4719
  });
4680
- ipc.handle("git:log", async (_e, limit = 50) => {
4720
+ handle.handleIpc(ipc, handle.IPC.git.log, async (_e, limit = 50) => {
4681
4721
  const result = await git().log({ maxCount: limit });
4682
4722
  return result.all.map((c) => ({
4683
4723
  hash: c.hash,
@@ -4688,7 +4728,7 @@ function registerGitHandlers(ipc) {
4688
4728
  date: c.date
4689
4729
  }));
4690
4730
  });
4691
- ipc.handle("git:branches", async () => {
4731
+ handle.handleIpc(ipc, handle.IPC.git.branches, async () => {
4692
4732
  const raw = await git().raw([
4693
4733
  "for-each-ref",
4694
4734
  "--format=%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)",
@@ -4720,7 +4760,7 @@ function registerGitHandlers(ipc) {
4720
4760
  }
4721
4761
  return branches;
4722
4762
  });
4723
- ipc.handle("git:checkout", async (_e, branch, create) => {
4763
+ handle.handleIpc(ipc, handle.IPC.git.checkout, async (_e, branch, create) => {
4724
4764
  if (create) {
4725
4765
  await git().checkoutLocalBranch(branch);
4726
4766
  return;
@@ -4739,13 +4779,13 @@ function registerGitHandlers(ipc) {
4739
4779
  }
4740
4780
  await git().checkout(branch);
4741
4781
  });
4742
- ipc.handle("git:deleteBranch", async (_e, name, force = false) => {
4782
+ handle.handleIpc(ipc, handle.IPC.git.deleteBranch, async (_e, name, force = false) => {
4743
4783
  await git().deleteLocalBranch(name, force);
4744
4784
  });
4745
- ipc.handle("git:pull", async () => {
4785
+ handle.handleIpc(ipc, handle.IPC.git.pull, async () => {
4746
4786
  await git().pull();
4747
4787
  });
4748
- ipc.handle("git:push", async (_e, setUpstream) => {
4788
+ handle.handleIpc(ipc, handle.IPC.git.push, async (_e, setUpstream) => {
4749
4789
  if (setUpstream) {
4750
4790
  const status = await git().status();
4751
4791
  await git().push(["--set-upstream", "origin", status.current ?? "main"]);
@@ -4753,20 +4793,20 @@ function registerGitHandlers(ipc) {
4753
4793
  await git().push();
4754
4794
  }
4755
4795
  });
4756
- ipc.handle("git:remotes", async () => {
4796
+ handle.handleIpc(ipc, handle.IPC.git.remotes, async () => {
4757
4797
  const result = await git().getRemotes(true);
4758
4798
  return result.map((r) => ({ name: r.name, url: r.refs.fetch || r.refs.push || "" }));
4759
4799
  });
4760
- ipc.handle("git:addRemote", async (_e, name, url) => {
4800
+ handle.handleIpc(ipc, handle.IPC.git.addRemote, async (_e, name, url) => {
4761
4801
  await git().addRemote(name, url);
4762
4802
  });
4763
- ipc.handle("git:setRemoteUrl", async (_e, name, url) => {
4803
+ handle.handleIpc(ipc, handle.IPC.git.setRemoteUrl, async (_e, name, url) => {
4764
4804
  await git().remote(["set-url", name, url]);
4765
4805
  });
4766
- ipc.handle("git:removeRemote", async (_e, name) => {
4806
+ handle.handleIpc(ipc, handle.IPC.git.removeRemote, async (_e, name) => {
4767
4807
  await git().removeRemote(name);
4768
4808
  });
4769
- ipc.handle("git:writeCiFile", async (_e, relPath, content) => {
4809
+ handle.handleIpc(ipc, handle.IPC.git.writeCiFile, async (_e, relPath, content) => {
4770
4810
  const wsDir = getWorkspaceDir();
4771
4811
  if (!wsDir) throw new Error("No workspace open");
4772
4812
  const fullPath = path.join(wsDir, relPath);
@@ -4785,20 +4825,20 @@ function resolveStatus(result, filePath, staged) {
4785
4825
  return "modified";
4786
4826
  }
4787
4827
  function registerRecordHandlers(ipc, getWebContents) {
4788
- ipc.handle("record:start", async (_e, config) => {
4828
+ handle.handleIpc(ipc, handle.IPC.record.start, async (_e, config) => {
4789
4829
  await recorder.startRecorder(config);
4790
4830
  recorder.setRecorderHitCallback((entry) => {
4791
- getWebContents()?.send("record:hit", entry);
4831
+ getWebContents()?.send(handle.IPC.record.hit, entry);
4792
4832
  });
4793
4833
  });
4794
- ipc.handle("record:stop", async () => {
4834
+ handle.handleIpc(ipc, handle.IPC.record.stop, async () => {
4795
4835
  const session = recorder.stopRecorder();
4796
4836
  recorder.setRecorderHitCallback(null);
4797
4837
  return session;
4798
4838
  });
4799
- ipc.handle("record:isRunning", () => recorder.isRecorderRunning());
4800
- ipc.handle("record:entries", () => recorder.getRecorderEntries());
4801
- ipc.handle("record:toMock", (_e, entries, upstream, name, port) => {
4839
+ handle.handleIpc(ipc, handle.IPC.record.isRunning, () => recorder.isRecorderRunning());
4840
+ handle.handleIpc(ipc, handle.IPC.record.entries, () => recorder.getRecorderEntries());
4841
+ handle.handleIpc(ipc, handle.IPC.record.toMock, (_e, entries, upstream, name, port) => {
4802
4842
  return recorder.entriesToMockServer(entries, upstream, name, port);
4803
4843
  });
4804
4844
  }
@@ -4892,7 +4932,7 @@ electron.app.whenReady().then(async () => {
4892
4932
  if (process.platform !== "darwin") electron.Menu.setApplicationMenu(null);
4893
4933
  await authBuilder.initSecretStore(electron.app.getPath("userData"));
4894
4934
  registerFileHandlers(electron.ipcMain);
4895
- requestCollection.registerRequestHandler(electron.ipcMain);
4935
+ registerRequestHandler(electron.ipcMain);
4896
4936
  authBuilder.registerSecretHandlers(electron.ipcMain);
4897
4937
  registerImportHandlers(electron.ipcMain);
4898
4938
  registerGenerateHandlers(electron.ipcMain);
@@ -4905,7 +4945,7 @@ electron.app.whenReady().then(async () => {
4905
4945
  registerContractHandlers(electron.ipcMain);
4906
4946
  registerGitHandlers(electron.ipcMain);
4907
4947
  registerRecordHandlers(electron.ipcMain, () => electron.BrowserWindow.getAllWindows()[0]?.webContents ?? null);
4908
- electron.ipcMain.handle("shell:openExternal", (_e, url) => electron.shell.openExternal(url));
4948
+ handle.handleIpc(electron.ipcMain, handle.IPC.shell.openExternal, (_e, url) => electron.shell.openExternal(url));
4909
4949
  createWindow();
4910
4950
  electron.app.on("activate", () => {
4911
4951
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();