@testsmith/api-spector 0.8.6 → 0.8.8

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.
@@ -29,13 +29,13 @@ const path = require("node:path");
29
29
  const crypto = require("node:crypto");
30
30
  const JSZip = require("jszip");
31
31
  const dataFiles = require("./chunks/data-files-BYqhsoPe.js");
32
- const requestExec = require("./chunks/request-exec-BI4sXJop.js");
32
+ const requestCollection = require("./chunks/request-collection-zRRUfg0X.js");
33
+ const requestExec = require("./chunks/request-exec-s7k-oRi2.js");
33
34
  const ipcValidate = require("./chunks/ipc-validate-sqUFqRmE.js");
34
35
  const uuid = require("uuid");
35
36
  const jsYaml = require("js-yaml");
36
37
  const undici = require("undici");
37
38
  const soapHandler = require("./chunks/soap-handler-BF5ii0UV.js");
38
- const requestCollection = require("./chunks/request-collection-gkOYlWml.js");
39
39
  const runConcurrent = require("./chunks/run-concurrent-468tH0AX.js");
40
40
  const flowEngine = require("./chunks/flow-engine-BxwjEy37.js");
41
41
  const mockServer = require("./chunks/mock-server-C4kTFkod.js");
@@ -49,7 +49,7 @@ const os = require("node:os");
49
49
  const grpc = require("@grpc/grpc-js");
50
50
  const protoLoader = require("@grpc/proto-loader");
51
51
  const escape = require("./chunks/escape-DDVAlOvZ.js");
52
- const brokerClient = require("./chunks/broker-client-Bp9YId3a.js");
52
+ const brokerClient = require("./chunks/broker-client-Bmt4seb8.js");
53
53
  const simpleGit = require("simple-git");
54
54
  const recorder = require("./chunks/recorder-BkO8G_mB.js");
55
55
  require("fs/promises");
@@ -297,7 +297,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
297
297
  }
298
298
  workspaceDir = path.dirname(wsPath);
299
299
  workspaceFile = wsPath;
300
- await requestExec.loadGlobals(workspaceDir);
300
+ await requestCollection.loadGlobals(workspaceDir);
301
301
  await saveLastWorkspacePath(wsPath);
302
302
  requestExec.setSecretsConfig(parsed.settings?.secrets);
303
303
  return { workspace: parsed, workspacePath: wsPath };
@@ -313,7 +313,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
313
313
  if (result.canceled || !result.filePath) return null;
314
314
  workspaceDir = path.dirname(result.filePath);
315
315
  workspaceFile = result.filePath;
316
- await requestExec.loadGlobals(workspaceDir);
316
+ await requestCollection.loadGlobals(workspaceDir);
317
317
  await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
318
318
  await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
319
319
  await promises.mkdir(path.join(workspaceDir, "flows"), { recursive: true });
@@ -420,10 +420,10 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
420
420
  await promises.writeFile(result.filePath, content, "utf8");
421
421
  return true;
422
422
  });
423
- handle.handleIpc(ipc, handle.IPC.globals.get, () => requestExec.getGlobals());
423
+ handle.handleIpc(ipc, handle.IPC.globals.get, () => requestCollection.getGlobals());
424
424
  handle.handleIpc(ipc, handle.IPC.globals.set, async (_e, patch) => {
425
- requestExec.setGlobals(patch);
426
- await requestExec.persistGlobals();
425
+ requestCollection.setGlobals(patch);
426
+ await requestCollection.persistGlobals();
427
427
  });
428
428
  handle.handleIpc(ipc, handle.IPC.file.closeWorkspace, async () => {
429
429
  workspaceDir = null;
@@ -445,7 +445,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
445
445
  const workspace = JSON.parse(raw);
446
446
  workspaceDir = path.dirname(wsPath);
447
447
  workspaceFile = wsPath;
448
- await requestExec.loadGlobals(workspaceDir);
448
+ await requestCollection.loadGlobals(workspaceDir);
449
449
  requestExec.setSecretsConfig(workspace.settings?.secrets);
450
450
  return { workspace, workspacePath: wsPath };
451
451
  } catch {
@@ -475,7 +475,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
475
475
  if (looksLikeCollection(parsed)) return null;
476
476
  workspaceDir = path.dirname(wsPath);
477
477
  workspaceFile = wsPath;
478
- await requestExec.loadGlobals(workspaceDir);
478
+ await requestCollection.loadGlobals(workspaceDir);
479
479
  await saveLastWorkspacePath(wsPath);
480
480
  requestExec.setSecretsConfig(parsed.settings?.secrets);
481
481
  return { workspace: parsed, workspacePath: wsPath };
@@ -509,7 +509,7 @@ Create a workspace in this folder and add "${path.basename(wsPath)}" to it? The
509
509
  const parsed = JSON.parse(await promises.readFile(wsPath, "utf8"));
510
510
  workspaceDir = path.dirname(wsPath);
511
511
  workspaceFile = wsPath;
512
- await requestExec.loadGlobals(workspaceDir);
512
+ await requestCollection.loadGlobals(workspaceDir);
513
513
  await saveLastWorkspacePath(wsPath);
514
514
  requestExec.setSecretsConfig(parsed.settings?.secrets);
515
515
  return { workspace: parsed, workspacePath: wsPath };
@@ -530,7 +530,7 @@ async function tryOpenWorkspaceInDir(dir) {
530
530
  const workspace = JSON.parse(raw);
531
531
  workspaceDir = dir;
532
532
  workspaceFile = wsPath;
533
- await requestExec.loadGlobals(workspaceDir);
533
+ await requestCollection.loadGlobals(workspaceDir);
534
534
  requestExec.setSecretsConfig(workspace.settings?.secrets);
535
535
  return { workspace, workspacePath: wsPath };
536
536
  } catch {
@@ -657,10 +657,15 @@ async function wrapCollectionAsWorkspace(collFilePath, collection) {
657
657
  await atomicWrite(collFilePath, JSON.stringify(ws, null, 2));
658
658
  workspaceDir = dir;
659
659
  workspaceFile = collFilePath;
660
- await requestExec.loadGlobals(dir);
660
+ await requestCollection.loadGlobals(dir);
661
661
  await saveLastWorkspacePath(collFilePath);
662
662
  return { workspace: ws, workspacePath: collFilePath };
663
663
  }
664
+ function scopeDelta(base, updated) {
665
+ const out = {};
666
+ for (const [k, v] of Object.entries(updated)) if (base[k] !== v) out[k] = v;
667
+ return out;
668
+ }
664
669
  function asObject(value) {
665
670
  return value && typeof value === "object" ? value : null;
666
671
  }
@@ -737,6 +742,7 @@ function registerRequestHandler(ipc) {
737
742
  request: req,
738
743
  environment,
739
744
  collectionVars,
745
+ folderVars = {},
740
746
  globals: payloadGlobals,
741
747
  proxy,
742
748
  tls,
@@ -759,7 +765,7 @@ function registerRequestHandler(ipc) {
759
765
  flushTimer = setInterval(flushStream, 40);
760
766
  }
761
767
  const start = Date.now();
762
- const liveGlobals = requestExec.getGlobals();
768
+ const liveGlobals = requestCollection.getGlobals();
763
769
  const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
764
770
  const envVars = await requestExec.buildEnvVars(environment);
765
771
  let localVars = {};
@@ -777,31 +783,37 @@ function registerRequestHandler(ipc) {
777
783
  }
778
784
  const dynamicVars = await requestExec.buildDynamicVars();
779
785
  if (dataRow && Object.keys(dataRow).length > 0) {
780
- localVars = requestExec.resolveDataRow(dataRow, requestExec.mergeVars(envVars, collectionVars, mergedGlobals, {}, dynamicVars));
786
+ localVars = requestExec.resolveDataRow(dataRow, requestExec.mergeVars(envVars, collectionVars, mergedGlobals, {}, dynamicVars, folderVars));
781
787
  }
782
788
  if (req.variables && Object.keys(req.variables).length > 0) {
783
789
  localVars = { ...req.variables, ...localVars };
784
790
  }
785
- let vars = requestExec.mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars);
791
+ const seededLocalVars = { ...localVars };
792
+ let vars = requestExec.mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars, folderVars);
786
793
  let preScriptMeta = { consoleOutput: [] };
787
794
  let updatedCollectionVars = { ...collectionVars };
788
795
  let updatedEnvVars = { ...envVars };
789
796
  let updatedGlobals = { ...mergedGlobals };
797
+ let updatedFolderVars = { ...folderVars };
790
798
  if (req.preRequestScript?.trim()) {
791
799
  const result = await requestExec.runScript(requestExec.interpolate(req.preRequestScript, vars), {
792
800
  envVars: { ...envVars },
793
801
  collectionVars: { ...collectionVars },
794
802
  globals: { ...mergedGlobals },
795
- localVars: {}
803
+ folderVars: { ...folderVars },
804
+ // Seed the script's local scope with the already-resolved local vars
805
+ // (request-scoped variables + any data-row) so they survive the script
806
+ // run — otherwise updatedLocalVars below would overwrite them with {} and
807
+ // {{request-level vars}} would go un-interpolated at send time.
808
+ localVars: { ...localVars }
796
809
  });
797
810
  preScriptMeta = { error: result.error, consoleOutput: result.consoleOutput };
798
811
  localVars = result.updatedLocalVars;
799
812
  updatedEnvVars = result.updatedEnvVars;
800
813
  updatedCollectionVars = result.updatedCollectionVars;
801
814
  updatedGlobals = result.updatedGlobals;
802
- requestExec.patchGlobals(result.updatedGlobals);
803
- await requestExec.persistGlobals();
804
- vars = requestExec.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
815
+ updatedFolderVars = result.updatedFolderVars;
816
+ vars = requestExec.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars, updatedFolderVars);
805
817
  }
806
818
  let response;
807
819
  let scriptResponse;
@@ -913,6 +925,7 @@ function registerRequestHandler(ipc) {
913
925
  envVars: { ...updatedEnvVars },
914
926
  collectionVars: { ...updatedCollectionVars },
915
927
  globals: { ...updatedGlobals },
928
+ folderVars: { ...updatedFolderVars },
916
929
  localVars: { ...localVars },
917
930
  // Pass the *unmasked* response so the script can extract real values
918
931
  // (tokens, ids, …). The displayed `response` keeps the redacted copy.
@@ -924,9 +937,8 @@ function registerRequestHandler(ipc) {
924
937
  updatedEnvVars = result.updatedEnvVars;
925
938
  updatedCollectionVars = result.updatedCollectionVars;
926
939
  updatedGlobals = result.updatedGlobals;
940
+ updatedFolderVars = result.updatedFolderVars;
927
941
  localVars = result.updatedLocalVars;
928
- requestExec.patchGlobals(result.updatedGlobals);
929
- await requestExec.persistGlobals();
930
942
  }
931
943
  const combinedTestResults = [...schemaTestResults, ...postTestResults];
932
944
  if (!response.error && response.status >= 400 && combinedTestResults.length === 0) {
@@ -939,10 +951,14 @@ function registerRequestHandler(ipc) {
939
951
  const scriptResult = {
940
952
  testResults: combinedTestResults,
941
953
  consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
942
- updatedEnvVars,
943
- updatedCollectionVars,
944
- updatedGlobals,
945
- updatedLocalVars: localVars,
954
+ // Persist only what the scripts actually changed, so echoed inputs (the
955
+ // sent session/collection vars, the seeded request/data vars) don't get
956
+ // written back and overwrite stored values.
957
+ updatedEnvVars: scopeDelta(envVars, updatedEnvVars),
958
+ updatedCollectionVars: scopeDelta(collectionVars, updatedCollectionVars),
959
+ updatedGlobals: scopeDelta(mergedGlobals, updatedGlobals),
960
+ updatedLocalVars: scopeDelta(seededLocalVars, localVars),
961
+ updatedFolderVars: scopeDelta(folderVars, updatedFolderVars),
946
962
  resolvedUrl,
947
963
  preScriptError: preScriptMeta.error,
948
964
  postScriptError: postError
@@ -952,8 +968,6 @@ function registerRequestHandler(ipc) {
952
968
  handle.handleIpc(ipc, handle.IPC.script.runHook, async (_e, payload) => {
953
969
  const { script, envVars, collectionVars, globals } = payload;
954
970
  const result = await requestExec.runScript(script, { envVars, collectionVars, globals, localVars: {} });
955
- requestExec.patchGlobals(result.updatedGlobals);
956
- await requestExec.persistGlobals();
957
971
  return {
958
972
  updatedEnvVars: result.updatedEnvVars,
959
973
  updatedCollectionVars: result.updatedCollectionVars,
@@ -1124,6 +1138,18 @@ function parseHeaders$1(raw) {
1124
1138
  description: h.description ?? ""
1125
1139
  }));
1126
1140
  }
1141
+ function parseCollectionVariables(raw) {
1142
+ const variables = {};
1143
+ const disabled = {};
1144
+ for (const v of raw ?? []) {
1145
+ const key = typeof v?.key === "string" ? v.key.trim() : "";
1146
+ if (!key) continue;
1147
+ const value = v.value == null ? "" : String(v.value);
1148
+ if (v.disabled) disabled[key] = value;
1149
+ else variables[key] = value;
1150
+ }
1151
+ return { variables, disabled };
1152
+ }
1127
1153
  function parseParams$1(urlObj) {
1128
1154
  if (!urlObj || typeof urlObj === "string") return [];
1129
1155
  return (urlObj.query ?? []).map((p) => ({
@@ -1305,13 +1331,16 @@ async function importPostman(filePath) {
1305
1331
  rootFolder.requestIds.push(req.id);
1306
1332
  }
1307
1333
  }
1334
+ const { variables: collectionVariables, disabled: collectionVariablesDisabled } = parseCollectionVariables(data.variable);
1308
1335
  return {
1309
1336
  version: "1.0",
1310
1337
  id: uuid.v4(),
1311
1338
  name: info.name ?? "Imported Collection",
1312
1339
  description: info.description ?? "",
1313
1340
  rootFolder,
1314
- requests
1341
+ requests,
1342
+ ...Object.keys(collectionVariables).length ? { collectionVariables } : {},
1343
+ ...Object.keys(collectionVariablesDisabled).length ? { collectionVariablesDisabled } : {}
1315
1344
  };
1316
1345
  }
1317
1346
  async function importPostmanEnvironment(filePath) {
@@ -1741,6 +1770,15 @@ function parseAuth(auth) {
1741
1770
  }
1742
1771
  return { type: "none" };
1743
1772
  }
1773
+ function toVarMap(data) {
1774
+ const out = {};
1775
+ if (!data || typeof data !== "object") return out;
1776
+ for (const [key, value] of Object.entries(data)) {
1777
+ if (!key || value == null) continue;
1778
+ out[key] = typeof value === "object" ? JSON.stringify(value) : String(value);
1779
+ }
1780
+ return out;
1781
+ }
1744
1782
  async function importInsomnia(filePath) {
1745
1783
  const raw = await promises.readFile(filePath, "utf8");
1746
1784
  const data = JSON.parse(raw);
@@ -1752,12 +1790,14 @@ async function importInsomnia(filePath) {
1752
1790
  const rootFolder = { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: [] };
1753
1791
  for (const r of resources) {
1754
1792
  if (r._type !== "request_group") continue;
1793
+ const variables = toVarMap(r.environment);
1755
1794
  folderById[r._id] = {
1756
1795
  id: uuid.v4(),
1757
1796
  name: r.name ?? "Folder",
1758
1797
  description: r.description ?? "",
1759
1798
  folders: [],
1760
- requestIds: []
1799
+ requestIds: [],
1800
+ ...Object.keys(variables).length ? { variables } : {}
1761
1801
  };
1762
1802
  }
1763
1803
  for (const r of resources) {
@@ -1794,13 +1834,16 @@ async function importInsomnia(filePath) {
1794
1834
  folderById[r.parentId].folders.push(folder);
1795
1835
  }
1796
1836
  }
1837
+ const baseEnv = resources.find((r) => r._type === "environment" && r.parentId === workspaceId) ?? resources.find((r) => r._type === "environment");
1838
+ const collectionVariables = toVarMap(baseEnv?.data);
1797
1839
  return {
1798
1840
  version: "1.0",
1799
1841
  id: uuid.v4(),
1800
1842
  name: workspace.name ?? "Imported Collection",
1801
1843
  description: workspace.description ?? "",
1802
1844
  rootFolder,
1803
- requests
1845
+ requests,
1846
+ ...Object.keys(collectionVariables).length ? { collectionVariables } : {}
1804
1847
  };
1805
1848
  }
1806
1849
  function extractBlock(content, blockName) {
@@ -1840,6 +1883,20 @@ function parseKv(blockContent) {
1840
1883
  }
1841
1884
  return result;
1842
1885
  }
1886
+ function parseVarsBlock(blockContent) {
1887
+ const variables = {};
1888
+ const disabled = {};
1889
+ for (const [rawKey, value] of Object.entries(parseKv(blockContent))) {
1890
+ if (rawKey.startsWith("~")) {
1891
+ const key = rawKey.slice(1).trim();
1892
+ if (key) disabled[key] = value;
1893
+ } else {
1894
+ variables[rawKey] = value;
1895
+ }
1896
+ }
1897
+ return { variables, disabled };
1898
+ }
1899
+ const BRU_CONFIG_FILES = /* @__PURE__ */ new Set(["folder.bru", "collection.bru"]);
1843
1900
  const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "query"];
1844
1901
  function parseBruFile(content, fileName) {
1845
1902
  const meta = parseKv(extractBlock(content, "meta"));
@@ -1904,6 +1961,7 @@ function parseBruFile(content, fileName) {
1904
1961
  apiKeyIn: apiKeyAttrs["in"] === "query" ? "query" : "header"
1905
1962
  };
1906
1963
  }
1964
+ const { variables, disabled: variablesDisabled } = parseVarsBlock(extractBlock(content, "vars:pre-request"));
1907
1965
  return {
1908
1966
  id: uuid.v4(),
1909
1967
  name: meta["name"] ?? path.basename(fileName, ".bru"),
@@ -1917,7 +1975,9 @@ function parseBruFile(content, fileName) {
1917
1975
  description: extractBlock(content, "docs")?.trim() ?? "",
1918
1976
  preRequestScript: extractBlock(content, "script:pre-request") ? translateScript(extractBlock(content, "script:pre-request").trim(), "bruno") || void 0 : void 0,
1919
1977
  postRequestScript: extractBlock(content, "script:post-response") ? translateScript(extractBlock(content, "script:post-response").trim(), "bruno") || void 0 : void 0,
1920
- meta: { seq: meta["seq"] !== void 0 ? Number(meta["seq"]) : 0 }
1978
+ meta: { seq: meta["seq"] !== void 0 ? Number(meta["seq"]) : 0 },
1979
+ ...Object.keys(variables).length ? { variables } : {},
1980
+ ...Object.keys(variablesDisabled).length ? { variablesDisabled } : {}
1921
1981
  };
1922
1982
  }
1923
1983
  async function scanDir(dirPath, name, requests, ignore) {
@@ -1931,7 +1991,16 @@ async function scanDir(dirPath, name, requests, ignore) {
1931
1991
  if (s.isDirectory()) {
1932
1992
  const sub = await scanDir(fullPath, entry, requests, ignore);
1933
1993
  if (sub.folders.length > 0 || sub.requestIds.length > 0) folder.folders.push(sub);
1934
- } else if (entry.endsWith(".bru")) {
1994
+ } else if (entry === "folder.bru") {
1995
+ const content = await promises.readFile(fullPath, "utf8");
1996
+ const meta = parseKv(extractBlock(content, "meta"));
1997
+ if (meta["name"]) folder.name = meta["name"];
1998
+ const docs = extractBlock(content, "docs")?.trim();
1999
+ if (docs) folder.description = docs;
2000
+ const { variables, disabled } = parseVarsBlock(extractBlock(content, "vars:pre-request"));
2001
+ if (Object.keys(variables).length) folder.variables = variables;
2002
+ if (Object.keys(disabled).length) folder.variablesDisabled = disabled;
2003
+ } else if (entry.endsWith(".bru") && !BRU_CONFIG_FILES.has(entry)) {
1935
2004
  const content = await promises.readFile(fullPath, "utf8");
1936
2005
  const req = parseBruFile(content, entry);
1937
2006
  requests[req.id] = req;
@@ -1949,13 +2018,24 @@ async function importBruno(filePath) {
1949
2018
  const ignore = /* @__PURE__ */ new Set(["node_modules", ".git", ...config.ignore ?? []]);
1950
2019
  const requests = {};
1951
2020
  const rootFolder = await scanDir(dirPath, "root", requests, ignore);
2021
+ let collectionVariables = {};
2022
+ let collectionVariablesDisabled = {};
2023
+ try {
2024
+ const colBru = await promises.readFile(path.join(dirPath, "collection.bru"), "utf8");
2025
+ const parsed = parseVarsBlock(extractBlock(colBru, "vars:pre-request"));
2026
+ collectionVariables = parsed.variables;
2027
+ collectionVariablesDisabled = parsed.disabled;
2028
+ } catch {
2029
+ }
1952
2030
  return {
1953
2031
  version: "1.0",
1954
2032
  id: uuid.v4(),
1955
2033
  name: config.name ?? "Imported Collection",
1956
2034
  description: "",
1957
2035
  rootFolder,
1958
- requests
2036
+ requests,
2037
+ ...Object.keys(collectionVariables).length ? { collectionVariables } : {},
2038
+ ...Object.keys(collectionVariablesDisabled).length ? { collectionVariablesDisabled } : {}
1959
2039
  };
1960
2040
  }
1961
2041
  const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "QUERY"];
@@ -5355,7 +5435,7 @@ function registerRunnerHandler(ipc) {
5355
5435
  const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
5356
5436
  const concurrency = Math.max(1, Math.min(20, Math.floor(payload.concurrency ?? 1)));
5357
5437
  const envVars = await requestExec.buildEnvVars(environment);
5358
- const liveGlobals = requestExec.getGlobals();
5438
+ const liveGlobals = requestCollection.getGlobals();
5359
5439
  const globals = { ...payloadGlobals, ...liveGlobals };
5360
5440
  const dispatcher = await requestExec.buildDispatcher(proxy, tls);
5361
5441
  const summary = { total: items.length, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
@@ -5480,7 +5560,7 @@ function registerFlowHandler(ipc) {
5480
5560
  const flow = flowEngine.normalizeFlow(payload.flow);
5481
5561
  const flows = (payload.flows ?? []).map(flowEngine.normalizeFlow);
5482
5562
  const envVars = await requestExec.buildEnvVars(environment);
5483
- const globals = { ...payloadGlobals, ...requestExec.getGlobals() };
5563
+ const globals = { ...payloadGlobals, ...requestCollection.getGlobals() };
5484
5564
  const dispatcher = await requestExec.buildDispatcher(proxy, tls);
5485
5565
  const colById = new Map(collections.map((c) => [c.id, c]));
5486
5566
  const deps = {
@@ -7829,7 +7909,7 @@ function registerCloudHandlers(ipc) {
7829
7909
  handle.handleIpc(ipc, handle.IPC.cloud.pushMonitor, async (_e, input) => {
7830
7910
  const { request, environment, collectionVars, globals } = input;
7831
7911
  const envVars = await requestExec.buildEnvVars(environment);
7832
- const liveGlobals = { ...globals, ...requestExec.getGlobals() };
7912
+ const liveGlobals = { ...globals, ...requestCollection.getGlobals() };
7833
7913
  const staticVars = requestExec.mergeVars(envVars, collectionVars ?? {}, liveGlobals, {}, {});
7834
7914
  const setup = input.setup ?? [];
7835
7915
  const resolveVars2 = omitKeys(staticVars, setVarNames(setup));
@@ -2,9 +2,9 @@
2
2
  "use strict";
3
3
  const promises = require("node:fs/promises");
4
4
  const path = require("node:path");
5
- const requestExec = require("./chunks/request-exec-BI4sXJop.js");
5
+ const requestExec = require("./chunks/request-exec-s7k-oRi2.js");
6
+ const requestCollection = require("./chunks/request-collection-zRRUfg0X.js");
6
7
  const escape = require("./chunks/escape-DDVAlOvZ.js");
7
- const requestCollection = require("./chunks/request-collection-gkOYlWml.js");
8
8
  const runConcurrent = require("./chunks/run-concurrent-468tH0AX.js");
9
9
  const environments = require("./chunks/environments-iM3SUM-4.js");
10
10
  const cliCommon = require("./chunks/cli-common-GnihVSjU.js");
@@ -510,10 +510,10 @@ function printResult(r, verbose) {
510
510
  if (r.error) console.log(cliCommon.color(` Error: ${r.error}`, cliCommon.C.red));
511
511
  }
512
512
  async function main() {
513
- const args = cliCommon.parseArgs(process.argv.slice(2));
513
+ const args = cliCommon.parseArgs(process.argv.slice(2), ["ask", "var", "env-var", "global-var"]);
514
514
  if (args.help) {
515
515
  console.log(
516
- '\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--output <path>] [--format json|junit|html|sarif]\n [--ask "Message=value"] [--concurrency <n>] [--verbose] [--bail]\n\n --ask "Message=value" Supply an ask variable ({{@Message}}) for this run\n (repeatable). Requests whose ask variables are not\n supplied are skipped.\n --concurrency <n> Run n requests at a time (default 1 = sequential).\n Hooks run as barriers; best for independent requests.\n'
516
+ '\nUsage:\n api-spector run --workspace <path> [--environment <name>] [--tags <a,b>]\n [--collection <name>] [--var "key=value"]\n [--env-var "key=value"] [--global-var "key=value"]\n [--output <path>] [--format json|junit|html|sarif]\n [--ask "Message=value"] [--concurrency <n>] [--verbose] [--bail]\n\n --var "key=value" Override a variable for this run (repeatable). Sits at\n the CLI tier: above environment/collection/folder/\n request values, below data-row and script-set values.\n --env-var "key=value" Set an environment variable for this run (repeatable).\n Seeds the environment scope; a script may override it.\n --global-var "key=value" Set a global variable for this run (repeatable).\n Seeds the globals scope; a script may override it.\n --ask "Message=value" Supply an ask variable ({{@Message}}) for this run\n (repeatable). Requests whose ask variables are not\n supplied are skipped.\n --concurrency <n> Run n requests at a time (default 1 = sequential).\n Hooks run as barriers; best for independent requests.\n'
517
517
  );
518
518
  process.exit(0);
519
519
  }
@@ -539,7 +539,7 @@ async function main() {
539
539
  console.error(cliCommon.color(`Error: could not read workspace file: ${wsPath}`, cliCommon.C.red));
540
540
  process.exit(1);
541
541
  }
542
- await requestExec.loadGlobals(wsDir);
542
+ await requestCollection.loadGlobals(wsDir);
543
543
  requestExec.setSecretsConfig(workspace.settings?.secrets);
544
544
  const collections = await cliCommon.loadCollections(workspace, wsDir, {
545
545
  onError: (relPath) => console.error(cliCommon.color(` [warn] Could not load collection: ${relPath}`, cliCommon.C.yellow))
@@ -551,7 +551,7 @@ async function main() {
551
551
  } else if (!envName && workspace.settings?.defaultEnvironment && !env) {
552
552
  console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
553
553
  }
554
- const version = `v${"0.8.6"}`;
554
+ const version = `v${"0.8.8"}`;
555
555
  console.log("");
556
556
  console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
557
557
  console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
@@ -565,6 +565,18 @@ async function main() {
565
565
  const eq = entry.indexOf("=");
566
566
  if (eq > 0) askAnswers[entry.slice(0, eq).trim()] = entry.slice(eq + 1);
567
567
  }
568
+ const parseKeyVals = (raw) => {
569
+ const out = {};
570
+ for (const entry of Array.isArray(raw) ? raw : raw ? [raw] : []) {
571
+ if (typeof entry !== "string") continue;
572
+ const eq = entry.indexOf("=");
573
+ if (eq > 0) out[entry.slice(0, eq).trim()] = entry.slice(eq + 1);
574
+ }
575
+ return out;
576
+ };
577
+ const cliVars = parseKeyVals(args["var"]);
578
+ const envVarOverrides = parseKeyVals(args["env-var"]);
579
+ const globalVarOverrides = parseKeyVals(args["global-var"]);
568
580
  const concurrency = Math.max(1, Math.min(20, Math.floor(Number(args["concurrency"] ?? 1) || 1)));
569
581
  const envVarsSnapshot = await requestExec.buildEnvVars(env);
570
582
  const secretValuesToMask = (env?.variables ?? []).filter((v) => (v.secret || v.secretRef) && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
@@ -616,11 +628,26 @@ async function main() {
616
628
  let items = requestCollection.buildRunPlan(col, null, filterTags);
617
629
  if (items.length === 0) continue;
618
630
  if (!firstColName) firstColName = col.name;
619
- let runEnvVars = await requestExec.buildEnvVars(env);
620
- let runGlobals = requestExec.getGlobals();
631
+ let runEnvVars = { ...await requestExec.buildEnvVars(env), ...envVarOverrides };
632
+ let runGlobals = { ...requestCollection.getGlobals(), ...globalVarOverrides };
621
633
  let runCollectionVars = { ...col.collectionVariables };
622
634
  let runLocalVars = {};
623
635
  const runFolderVars = {};
636
+ const cliOverridden = /* @__PURE__ */ new Set();
637
+ const cliSeed = () => Object.fromEntries(Object.entries(cliVars).filter(([k]) => !cliOverridden.has(k)));
638
+ const stripCliSeed = (m) => {
639
+ const copy = { ...m };
640
+ for (const k of Object.keys(cliVars)) delete copy[k];
641
+ return copy;
642
+ };
643
+ const trackCliOverrides = (out, inputs) => {
644
+ for (const k of Object.keys(cliVars)) {
645
+ if (cliOverridden.has(k)) continue;
646
+ if (out.updatedCollectionVars[k] !== inputs.col[k] || out.updatedEnvVars[k] !== inputs.env[k] || out.updatedGlobals[k] !== inputs.globals[k] || out.updatedFolderVars[k] !== inputs.folder[k] || out.updatedLocalVars[k] !== inputs.local[k]) {
647
+ cliOverridden.add(k);
648
+ }
649
+ }
650
+ };
624
651
  console.log(cliCommon.color(` ┌ ${col.name}`, cliCommon.C.bold, cliCommon.C.white));
625
652
  const workspaceTls = workspace.settings?.tls;
626
653
  const effectiveTls = col.tls ? { ...workspaceTls, ...col.tls } : workspaceTls;
@@ -682,16 +709,16 @@ async function main() {
682
709
  iterationLabel: item.iterationLabel
683
710
  };
684
711
  } else {
712
+ const colInput = { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) };
713
+ const folderInput = { ...item.folderVars ?? {}, ...item.folderId ? runFolderVars[item.folderId] ?? {} : {} };
714
+ const localInput = { ...cliSeed(), ...runLocalVars, ...item.dataRow };
685
715
  const out = await requestExec.executeRunnerRequest({
686
716
  req: item.request,
687
- collectionVars: { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) },
717
+ collectionVars: colInput,
688
718
  envVars: runEnvVars,
689
719
  globals: runGlobals,
690
- localVars: { ...runLocalVars, ...item.dataRow },
691
- folderVars: {
692
- ...item.folderVars ?? {},
693
- ...item.folderId ? runFolderVars[item.folderId] ?? {} : {}
694
- },
720
+ localVars: localInput,
721
+ folderVars: folderInput,
695
722
  dispatcher,
696
723
  piiMaskPatterns: piiPatterns,
697
724
  tls: effectiveTls,
@@ -699,11 +726,12 @@ async function main() {
699
726
  expectStatus: item.expectStatus,
700
727
  owaspTag: item.owaspTag
701
728
  });
729
+ trackCliOverrides(out, { col: colInput, env: runEnvVars, globals: runGlobals, folder: folderInput, local: localInput });
702
730
  result = out.result;
703
731
  runEnvVars = out.updatedEnvVars;
704
732
  runCollectionVars = out.updatedCollectionVars;
705
733
  runGlobals = out.updatedGlobals;
706
- runLocalVars = out.updatedLocalVars;
734
+ runLocalVars = stripCliSeed(out.updatedLocalVars);
707
735
  if (item.folderId) {
708
736
  runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
709
737
  }
@@ -762,23 +790,27 @@ async function main() {
762
790
  return;
763
791
  }
764
792
  try {
793
+ const colInput = { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) };
794
+ const folderInput = { ...item.folderVars ?? {}, ...item.folderId ? runFolderVars[item.folderId] ?? {} : {} };
795
+ const localInput = { ...cliSeed(), ...runLocalVars, ...item.dataRow };
765
796
  const out = await requestExec.executeRunnerRequest({
766
797
  req: item.request,
767
- collectionVars: { ...item.collectionVars, ...runCollectionVars, ...resolveAskValues(itemAsks, askAnswers) },
798
+ collectionVars: colInput,
768
799
  envVars: runEnvVars,
769
800
  globals: runGlobals,
770
- localVars: { ...runLocalVars, ...item.dataRow },
771
- folderVars: { ...item.folderVars ?? {}, ...item.folderId ? runFolderVars[item.folderId] ?? {} : {} },
801
+ localVars: localInput,
802
+ folderVars: folderInput,
772
803
  dispatcher,
773
804
  piiMaskPatterns: piiPatterns,
774
805
  tls: effectiveTls,
775
806
  expectStatus: item.expectStatus,
776
807
  owaspTag: item.owaspTag
777
808
  });
809
+ trackCliOverrides(out, { col: colInput, env: runEnvVars, globals: runGlobals, folder: folderInput, local: localInput });
778
810
  runEnvVars = { ...runEnvVars, ...out.updatedEnvVars };
779
811
  runCollectionVars = { ...runCollectionVars, ...out.updatedCollectionVars };
780
812
  runGlobals = { ...runGlobals, ...out.updatedGlobals };
781
- runLocalVars = { ...runLocalVars, ...out.updatedLocalVars };
813
+ runLocalVars = stripCliSeed({ ...runLocalVars, ...out.updatedLocalVars });
782
814
  if (item.folderId) runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
783
815
  const result = out.result;
784
816
  result.isHook = item.isHook;
@@ -1,5 +1,5 @@
1
1
  import { l as linter, E as EditorView, L as LanguageSupport, i as LRLanguage, s as styleTags, m as indentNodeProp, n as delimitedIndent, p as foldNodeProp, q as foldInside, S as StateField, u as LRParser, v as StateEffect, w as tags, a as reactExports, j as jsxRuntimeExports, e as ReactCodeMirror, o as oneDark, d as json } from "./codemirror-BKmwMUlV.js";
2
- import { u as useT, a as useStore, b as useActiveEnvironment, i as isSubscriptionQuery } from "./index-DTzdyIHp.js";
2
+ import { u as useT, a as useStore, b as useActiveEnvironment, i as isSubscriptionQuery } from "./index-fV0So_bF.js";
3
3
  import "./ajv-CJPwFjiP.js";
4
4
  function devAssert(condition, message) {
5
5
  const booleanCondition = Boolean(condition);
@@ -1,5 +1,5 @@
1
1
  import { a as reactExports, j as jsxRuntimeExports, e as ReactCodeMirror, o as oneDark, x as xml } from "./codemirror-BKmwMUlV.js";
2
- import { u as useT, c as btnPrimaryCls, d as commentKeymap } from "./index-DTzdyIHp.js";
2
+ import { u as useT, c as btnPrimaryCls, d as commentKeymap } from "./index-fV0So_bF.js";
3
3
  import "./ajv-CJPwFjiP.js";
4
4
  const SOAP_11_CONTENT_TYPE = "text/xml; charset=utf-8";
5
5
  const SOAP_12_CONTENT_TYPE = "application/soap+xml; charset=utf-8";