@testsmith/api-spector 0.8.7 → 0.8.9

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-170x6Jx9.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,21 +783,24 @@ 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 },
803
+ folderVars: { ...folderVars },
795
804
  // Seed the script's local scope with the already-resolved local vars
796
805
  // (request-scoped variables + any data-row) so they survive the script
797
806
  // run — otherwise updatedLocalVars below would overwrite them with {} and
@@ -803,9 +812,8 @@ function registerRequestHandler(ipc) {
803
812
  updatedEnvVars = result.updatedEnvVars;
804
813
  updatedCollectionVars = result.updatedCollectionVars;
805
814
  updatedGlobals = result.updatedGlobals;
806
- requestExec.patchGlobals(result.updatedGlobals);
807
- await requestExec.persistGlobals();
808
- vars = requestExec.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
815
+ updatedFolderVars = result.updatedFolderVars;
816
+ vars = requestExec.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars, updatedFolderVars);
809
817
  }
810
818
  let response;
811
819
  let scriptResponse;
@@ -917,6 +925,7 @@ function registerRequestHandler(ipc) {
917
925
  envVars: { ...updatedEnvVars },
918
926
  collectionVars: { ...updatedCollectionVars },
919
927
  globals: { ...updatedGlobals },
928
+ folderVars: { ...updatedFolderVars },
920
929
  localVars: { ...localVars },
921
930
  // Pass the *unmasked* response so the script can extract real values
922
931
  // (tokens, ids, …). The displayed `response` keeps the redacted copy.
@@ -928,9 +937,8 @@ function registerRequestHandler(ipc) {
928
937
  updatedEnvVars = result.updatedEnvVars;
929
938
  updatedCollectionVars = result.updatedCollectionVars;
930
939
  updatedGlobals = result.updatedGlobals;
940
+ updatedFolderVars = result.updatedFolderVars;
931
941
  localVars = result.updatedLocalVars;
932
- requestExec.patchGlobals(result.updatedGlobals);
933
- await requestExec.persistGlobals();
934
942
  }
935
943
  const combinedTestResults = [...schemaTestResults, ...postTestResults];
936
944
  if (!response.error && response.status >= 400 && combinedTestResults.length === 0) {
@@ -943,10 +951,14 @@ function registerRequestHandler(ipc) {
943
951
  const scriptResult = {
944
952
  testResults: combinedTestResults,
945
953
  consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
946
- updatedEnvVars,
947
- updatedCollectionVars,
948
- updatedGlobals,
949
- 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),
950
962
  resolvedUrl,
951
963
  preScriptError: preScriptMeta.error,
952
964
  postScriptError: postError
@@ -956,8 +968,6 @@ function registerRequestHandler(ipc) {
956
968
  handle.handleIpc(ipc, handle.IPC.script.runHook, async (_e, payload) => {
957
969
  const { script, envVars, collectionVars, globals } = payload;
958
970
  const result = await requestExec.runScript(script, { envVars, collectionVars, globals, localVars: {} });
959
- requestExec.patchGlobals(result.updatedGlobals);
960
- await requestExec.persistGlobals();
961
971
  return {
962
972
  updatedEnvVars: result.updatedEnvVars,
963
973
  updatedCollectionVars: result.updatedCollectionVars,
@@ -1128,6 +1138,18 @@ function parseHeaders$1(raw) {
1128
1138
  description: h.description ?? ""
1129
1139
  }));
1130
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
+ }
1131
1153
  function parseParams$1(urlObj) {
1132
1154
  if (!urlObj || typeof urlObj === "string") return [];
1133
1155
  return (urlObj.query ?? []).map((p) => ({
@@ -1309,13 +1331,16 @@ async function importPostman(filePath) {
1309
1331
  rootFolder.requestIds.push(req.id);
1310
1332
  }
1311
1333
  }
1334
+ const { variables: collectionVariables, disabled: collectionVariablesDisabled } = parseCollectionVariables(data.variable);
1312
1335
  return {
1313
1336
  version: "1.0",
1314
1337
  id: uuid.v4(),
1315
1338
  name: info.name ?? "Imported Collection",
1316
1339
  description: info.description ?? "",
1317
1340
  rootFolder,
1318
- requests
1341
+ requests,
1342
+ ...Object.keys(collectionVariables).length ? { collectionVariables } : {},
1343
+ ...Object.keys(collectionVariablesDisabled).length ? { collectionVariablesDisabled } : {}
1319
1344
  };
1320
1345
  }
1321
1346
  async function importPostmanEnvironment(filePath) {
@@ -1745,6 +1770,15 @@ function parseAuth(auth) {
1745
1770
  }
1746
1771
  return { type: "none" };
1747
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
+ }
1748
1782
  async function importInsomnia(filePath) {
1749
1783
  const raw = await promises.readFile(filePath, "utf8");
1750
1784
  const data = JSON.parse(raw);
@@ -1756,12 +1790,14 @@ async function importInsomnia(filePath) {
1756
1790
  const rootFolder = { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: [] };
1757
1791
  for (const r of resources) {
1758
1792
  if (r._type !== "request_group") continue;
1793
+ const variables = toVarMap(r.environment);
1759
1794
  folderById[r._id] = {
1760
1795
  id: uuid.v4(),
1761
1796
  name: r.name ?? "Folder",
1762
1797
  description: r.description ?? "",
1763
1798
  folders: [],
1764
- requestIds: []
1799
+ requestIds: [],
1800
+ ...Object.keys(variables).length ? { variables } : {}
1765
1801
  };
1766
1802
  }
1767
1803
  for (const r of resources) {
@@ -1798,13 +1834,16 @@ async function importInsomnia(filePath) {
1798
1834
  folderById[r.parentId].folders.push(folder);
1799
1835
  }
1800
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);
1801
1839
  return {
1802
1840
  version: "1.0",
1803
1841
  id: uuid.v4(),
1804
1842
  name: workspace.name ?? "Imported Collection",
1805
1843
  description: workspace.description ?? "",
1806
1844
  rootFolder,
1807
- requests
1845
+ requests,
1846
+ ...Object.keys(collectionVariables).length ? { collectionVariables } : {}
1808
1847
  };
1809
1848
  }
1810
1849
  function extractBlock(content, blockName) {
@@ -1844,6 +1883,20 @@ function parseKv(blockContent) {
1844
1883
  }
1845
1884
  return result;
1846
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"]);
1847
1900
  const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "query"];
1848
1901
  function parseBruFile(content, fileName) {
1849
1902
  const meta = parseKv(extractBlock(content, "meta"));
@@ -1908,6 +1961,7 @@ function parseBruFile(content, fileName) {
1908
1961
  apiKeyIn: apiKeyAttrs["in"] === "query" ? "query" : "header"
1909
1962
  };
1910
1963
  }
1964
+ const { variables, disabled: variablesDisabled } = parseVarsBlock(extractBlock(content, "vars:pre-request"));
1911
1965
  return {
1912
1966
  id: uuid.v4(),
1913
1967
  name: meta["name"] ?? path.basename(fileName, ".bru"),
@@ -1921,7 +1975,9 @@ function parseBruFile(content, fileName) {
1921
1975
  description: extractBlock(content, "docs")?.trim() ?? "",
1922
1976
  preRequestScript: extractBlock(content, "script:pre-request") ? translateScript(extractBlock(content, "script:pre-request").trim(), "bruno") || void 0 : void 0,
1923
1977
  postRequestScript: extractBlock(content, "script:post-response") ? translateScript(extractBlock(content, "script:post-response").trim(), "bruno") || void 0 : void 0,
1924
- 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 } : {}
1925
1981
  };
1926
1982
  }
1927
1983
  async function scanDir(dirPath, name, requests, ignore) {
@@ -1935,7 +1991,16 @@ async function scanDir(dirPath, name, requests, ignore) {
1935
1991
  if (s.isDirectory()) {
1936
1992
  const sub = await scanDir(fullPath, entry, requests, ignore);
1937
1993
  if (sub.folders.length > 0 || sub.requestIds.length > 0) folder.folders.push(sub);
1938
- } 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)) {
1939
2004
  const content = await promises.readFile(fullPath, "utf8");
1940
2005
  const req = parseBruFile(content, entry);
1941
2006
  requests[req.id] = req;
@@ -1953,13 +2018,24 @@ async function importBruno(filePath) {
1953
2018
  const ignore = /* @__PURE__ */ new Set(["node_modules", ".git", ...config.ignore ?? []]);
1954
2019
  const requests = {};
1955
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
+ }
1956
2030
  return {
1957
2031
  version: "1.0",
1958
2032
  id: uuid.v4(),
1959
2033
  name: config.name ?? "Imported Collection",
1960
2034
  description: "",
1961
2035
  rootFolder,
1962
- requests
2036
+ requests,
2037
+ ...Object.keys(collectionVariables).length ? { collectionVariables } : {},
2038
+ ...Object.keys(collectionVariablesDisabled).length ? { collectionVariablesDisabled } : {}
1963
2039
  };
1964
2040
  }
1965
2041
  const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "QUERY"];
@@ -5359,7 +5435,7 @@ function registerRunnerHandler(ipc) {
5359
5435
  const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
5360
5436
  const concurrency = Math.max(1, Math.min(20, Math.floor(payload.concurrency ?? 1)));
5361
5437
  const envVars = await requestExec.buildEnvVars(environment);
5362
- const liveGlobals = requestExec.getGlobals();
5438
+ const liveGlobals = requestCollection.getGlobals();
5363
5439
  const globals = { ...payloadGlobals, ...liveGlobals };
5364
5440
  const dispatcher = await requestExec.buildDispatcher(proxy, tls);
5365
5441
  const summary = { total: items.length, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
@@ -5484,7 +5560,7 @@ function registerFlowHandler(ipc) {
5484
5560
  const flow = flowEngine.normalizeFlow(payload.flow);
5485
5561
  const flows = (payload.flows ?? []).map(flowEngine.normalizeFlow);
5486
5562
  const envVars = await requestExec.buildEnvVars(environment);
5487
- const globals = { ...payloadGlobals, ...requestExec.getGlobals() };
5563
+ const globals = { ...payloadGlobals, ...requestCollection.getGlobals() };
5488
5564
  const dispatcher = await requestExec.buildDispatcher(proxy, tls);
5489
5565
  const colById = new Map(collections.map((c) => [c.id, c]));
5490
5566
  const deps = {
@@ -7339,6 +7415,31 @@ function registerContractHandlers(ipc) {
7339
7415
  return { file };
7340
7416
  });
7341
7417
  }
7418
+ const BRANCH_REF_FORMAT = "%(refname)|%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)";
7419
+ function parseBranchRefs(raw, current) {
7420
+ const branches = [];
7421
+ for (const line of raw.split("\n")) {
7422
+ if (!line.trim()) continue;
7423
+ const [fullRef, shortName, head, upstream, track] = line.split("|");
7424
+ if (!shortName || shortName.endsWith("/HEAD")) continue;
7425
+ const isRemote = fullRef.startsWith("refs/remotes/");
7426
+ let ahead;
7427
+ let behind;
7428
+ const aheadM = /ahead (\d+)/.exec(track ?? "");
7429
+ const behindM = /behind (\d+)/.exec(track ?? "");
7430
+ if (aheadM) ahead = Number(aheadM[1]);
7431
+ if (behindM) behind = Number(behindM[1]);
7432
+ branches.push({
7433
+ name: shortName,
7434
+ current: !isRemote && (head === "*" || shortName === current),
7435
+ remote: isRemote,
7436
+ upstream: upstream || void 0,
7437
+ ahead,
7438
+ behind
7439
+ });
7440
+ }
7441
+ return branches;
7442
+ }
7342
7443
  function git() {
7343
7444
  const dir = getWorkspaceDir();
7344
7445
  if (!dir) throw new Error("No workspace open");
@@ -7431,34 +7532,12 @@ function registerGitHandlers(ipc) {
7431
7532
  handle.handleIpc(ipc, handle.IPC.git.branches, async () => {
7432
7533
  const raw = await git().raw([
7433
7534
  "for-each-ref",
7434
- "--format=%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)",
7535
+ `--format=${BRANCH_REF_FORMAT}`,
7435
7536
  "refs/heads",
7436
7537
  "refs/remotes"
7437
7538
  ]);
7438
7539
  const current = (await git().branch()).current;
7439
- const branches = [];
7440
- for (const line of raw.split("\n")) {
7441
- if (!line.trim()) continue;
7442
- const [shortName, head, upstream, track] = line.split("|");
7443
- if (!shortName || shortName.endsWith("/HEAD")) continue;
7444
- const isRemote = shortName.startsWith("origin/") || shortName.includes("/");
7445
- const looksLocal = !isRemote;
7446
- let ahead;
7447
- let behind;
7448
- const aheadM = /ahead (\d+)/.exec(track);
7449
- const behindM = /behind (\d+)/.exec(track);
7450
- if (aheadM) ahead = Number(aheadM[1]);
7451
- if (behindM) behind = Number(behindM[1]);
7452
- branches.push({
7453
- name: shortName,
7454
- current: looksLocal && (head === "*" || shortName === current),
7455
- remote: isRemote,
7456
- upstream: upstream || void 0,
7457
- ahead,
7458
- behind
7459
- });
7460
- }
7461
- return branches;
7540
+ return parseBranchRefs(raw, current);
7462
7541
  });
7463
7542
  handle.handleIpc(ipc, handle.IPC.git.checkout, async (_e, branch, create) => {
7464
7543
  if (create) {
@@ -7833,7 +7912,7 @@ function registerCloudHandlers(ipc) {
7833
7912
  handle.handleIpc(ipc, handle.IPC.cloud.pushMonitor, async (_e, input) => {
7834
7913
  const { request, environment, collectionVars, globals } = input;
7835
7914
  const envVars = await requestExec.buildEnvVars(environment);
7836
- const liveGlobals = { ...globals, ...requestExec.getGlobals() };
7915
+ const liveGlobals = { ...globals, ...requestCollection.getGlobals() };
7837
7916
  const staticVars = requestExec.mergeVars(envVars, collectionVars ?? {}, liveGlobals, {}, {});
7838
7917
  const setup = input.setup ?? [];
7839
7918
  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.7"}`;
554
+ const version = `v${"0.8.9"}`;
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-CLKJD4rg.js";
2
+ import { u as useT, a as useStore, b as useActiveEnvironment, i as isSubscriptionQuery } from "./index-D_-cF6J0.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-CLKJD4rg.js";
2
+ import { u as useT, c as btnPrimaryCls, d as commentKeymap } from "./index-D_-cF6J0.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";