@testsmith/api-spector 0.4.9 → 0.5.0

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.
@@ -13403,6 +13403,38 @@ const createWsSlice = (set2) => ({
13403
13403
  if (s.wsConnections[requestId]) s.wsConnections[requestId].messages = [];
13404
13404
  })
13405
13405
  });
13406
+ const GRPC_MESSAGE_CAP = 1e3;
13407
+ const blank = () => ({ status: "idle", messages: [], services: [] });
13408
+ const createGrpcSlice = (set2) => ({
13409
+ grpcCalls: {},
13410
+ setGrpcStatus: (requestId, status, extra) => set2((s) => {
13411
+ const c = s.grpcCalls[requestId] ??= blank();
13412
+ c.status = status;
13413
+ c.code = extra?.code;
13414
+ c.codeName = extra?.codeName;
13415
+ c.error = extra?.error;
13416
+ }),
13417
+ addGrpcMessage: (requestId, message) => set2((s) => {
13418
+ const c = s.grpcCalls[requestId] ??= blank();
13419
+ c.messages.push(message);
13420
+ if (c.messages.length > GRPC_MESSAGE_CAP) {
13421
+ c.messages.splice(0, c.messages.length - GRPC_MESSAGE_CAP);
13422
+ }
13423
+ }),
13424
+ clearGrpcMessages: (requestId) => set2((s) => {
13425
+ if (s.grpcCalls[requestId]) s.grpcCalls[requestId].messages = [];
13426
+ }),
13427
+ setGrpcServices: (requestId, services) => set2((s) => {
13428
+ const c = s.grpcCalls[requestId] ??= blank();
13429
+ c.services = services;
13430
+ c.protoError = void 0;
13431
+ }),
13432
+ setGrpcProtoError: (requestId, error2) => set2((s) => {
13433
+ const c = s.grpcCalls[requestId] ??= blank();
13434
+ c.protoError = error2;
13435
+ if (error2) c.services = [];
13436
+ })
13437
+ });
13406
13438
  const LIVE_EVENT_CAP = 5e3;
13407
13439
  const createStreamSlice = (set2) => ({
13408
13440
  liveStream: null,
@@ -13706,6 +13738,19 @@ const createCollectionsSlice = (set2, get2) => ({
13706
13738
  s.activeCollectionId = col.id;
13707
13739
  if (s.workspace) s.workspace.collections.push(relPath);
13708
13740
  }),
13741
+ addCollectionObject: (data) => {
13742
+ let id2 = data.id;
13743
+ set2((s) => {
13744
+ const existingNames = Object.values(s.collections).map((c) => c.data.name);
13745
+ data.name = uniqueName(data.name, existingNames);
13746
+ const relPath = colRelPath(data.name, data.id);
13747
+ s.collections[data.id] = { relPath, data, dirty: true };
13748
+ s.activeCollectionId = data.id;
13749
+ if (s.workspace) s.workspace.collections.push(relPath);
13750
+ id2 = data.id;
13751
+ });
13752
+ return id2;
13753
+ },
13709
13754
  renameCollection: (id2, name2) => set2((s) => {
13710
13755
  if (!s.collections[id2]) return;
13711
13756
  const oldRelPath = s.collections[id2].relPath;
@@ -14278,6 +14323,8 @@ const createUiSlice = (set2) => ({
14278
14323
  quickInsertsOpen: true,
14279
14324
  sendSignal: 0,
14280
14325
  collectionPanelOpen: false,
14326
+ coverageOpen: false,
14327
+ compareOpen: false,
14281
14328
  setShowGeneratorPanel: (v) => set2((s) => {
14282
14329
  s.showGeneratorPanel = v;
14283
14330
  }),
@@ -14331,12 +14378,27 @@ const createUiSlice = (set2) => ({
14331
14378
  }),
14332
14379
  setCollectionPanelOpen: (open) => set2((s) => {
14333
14380
  s.collectionPanelOpen = open;
14381
+ }),
14382
+ setCoverageOpen: (open) => set2((s) => {
14383
+ s.coverageOpen = open;
14384
+ }),
14385
+ setCompareOpen: (open) => set2((s) => {
14386
+ s.compareOpen = open;
14387
+ }),
14388
+ // The OpenAPI spec used for coverage travels with the workspace (persisted on
14389
+ // save), mirroring how theme/zoom are stored.
14390
+ setCoverageSpec: (spec) => set2((s) => {
14391
+ if (s.workspace) {
14392
+ if (!s.workspace.settings) s.workspace.settings = {};
14393
+ s.workspace.settings.coverageSpec = spec;
14394
+ }
14334
14395
  })
14335
14396
  });
14336
14397
  const useStore = create()(
14337
14398
  immer((set2, get2, api) => ({
14338
14399
  // ── Slice composition ─────────────────────────────────────────────────────
14339
14400
  ...createWsSlice(set2),
14401
+ ...createGrpcSlice(set2),
14340
14402
  ...createStreamSlice(set2),
14341
14403
  ...createHistorySlice(set2, get2),
14342
14404
  ...createRunnerSlice(set2),
@@ -14430,7 +14492,7 @@ const useStore = create()(
14430
14492
  })
14431
14493
  }))
14432
14494
  );
14433
- const { electron: electron$v } = window;
14495
+ const { electron: electron$y } = window;
14434
14496
  function useAutoSave() {
14435
14497
  const collections = useStore((s) => s.collections);
14436
14498
  useStore((s) => s.environments);
@@ -14446,7 +14508,7 @@ function useAutoSave() {
14446
14508
  for (const { relPath, data, dirty } of dirtyCollections) {
14447
14509
  if (!dirty) continue;
14448
14510
  try {
14449
- await electron$v.saveCollection(relPath, data);
14511
+ await electron$y.saveCollection(relPath, data);
14450
14512
  markCollectionClean(data.id);
14451
14513
  } catch (e) {
14452
14514
  console.error("Auto-save failed for", relPath, e);
@@ -14462,7 +14524,7 @@ function useAutoSave() {
14462
14524
  if (wsTimerRef.current) clearTimeout(wsTimerRef.current);
14463
14525
  wsTimerRef.current = setTimeout(async () => {
14464
14526
  try {
14465
- await electron$v.saveWorkspace(workspace);
14527
+ await electron$y.saveWorkspace(workspace);
14466
14528
  } catch {
14467
14529
  }
14468
14530
  }, 300);
@@ -14471,7 +14533,7 @@ function useAutoSave() {
14471
14533
  };
14472
14534
  }, [workspace]);
14473
14535
  }
14474
- const { electron: electron$u } = window;
14536
+ const { electron: electron$x } = window;
14475
14537
  function useWorkspaceLoader() {
14476
14538
  const loadCollection = useStore((s) => s.loadCollection);
14477
14539
  const loadEnvironment = useStore((s) => s.loadEnvironment);
@@ -14495,7 +14557,7 @@ function useWorkspaceLoader() {
14495
14557
  if (typeof ws2.settings?.zoom === "number") setZoom(ws2.settings.zoom);
14496
14558
  if (ws2.settings?.persistHistory) {
14497
14559
  try {
14498
- const entries = await electron$u.loadHistory();
14560
+ const entries = await electron$x.loadHistory();
14499
14561
  useStore.getState().setHistory(entries);
14500
14562
  } catch {
14501
14563
  }
@@ -14504,14 +14566,14 @@ function useWorkspaceLoader() {
14504
14566
  }
14505
14567
  for (const colPath of ws2.collections ?? []) {
14506
14568
  try {
14507
- const col = await electron$u.loadCollection(colPath);
14569
+ const col = await electron$x.loadCollection(colPath);
14508
14570
  loadCollection(colPath, col);
14509
14571
  } catch {
14510
14572
  }
14511
14573
  }
14512
14574
  for (const envPath of ws2.environments ?? []) {
14513
14575
  try {
14514
- const env = await electron$u.loadEnvironment(envPath);
14576
+ const env = await electron$x.loadEnvironment(envPath);
14515
14577
  loadEnvironment(envPath, env);
14516
14578
  } catch {
14517
14579
  }
@@ -14530,19 +14592,19 @@ function useWorkspaceLoader() {
14530
14592
  }
14531
14593
  for (const relPath of ws2.mocks ?? []) {
14532
14594
  try {
14533
- const mockData = await electron$u.loadMock(relPath);
14595
+ const mockData = await electron$x.loadMock(relPath);
14534
14596
  loadMock(relPath, mockData);
14535
14597
  } catch {
14536
14598
  }
14537
14599
  }
14538
14600
  try {
14539
- const snapshots = await electron$u.listContractSnapshots(ws2.contracts ?? []);
14601
+ const snapshots = await electron$x.listContractSnapshots(ws2.contracts ?? []);
14540
14602
  for (const { relPath, snapshot } of snapshots) loadContractSnapshot(relPath, snapshot);
14541
14603
  } catch {
14542
14604
  }
14543
14605
  if ((ws2.collections ?? []).length > 0) {
14544
14606
  try {
14545
- const firstCol = await electron$u.loadCollection(ws2.collections[0]);
14607
+ const firstCol = await electron$x.loadCollection(ws2.collections[0]);
14546
14608
  setActiveCollection(firstCol.id);
14547
14609
  } catch {
14548
14610
  }
@@ -36428,7 +36490,7 @@ function CollectionSettingsModal({ collection, onClose }) {
36428
36490
  }
36429
36491
  );
36430
36492
  }
36431
- const { electron: electron$t } = window;
36493
+ const { electron: electron$w } = window;
36432
36494
  function normalisePath(url) {
36433
36495
  let path = url.replace(/^\{\{[^}]+\}\}/, "").replace(/^https?:\/\/[^/]+/, "");
36434
36496
  if (!path.startsWith("/")) path = "/" + path;
@@ -36515,7 +36577,7 @@ function SchemaSyncModal({
36515
36577
  setLoading(true);
36516
36578
  setError(null);
36517
36579
  try {
36518
- const entries = await electron$t.extractOpenApiSchemas();
36580
+ const entries = await electron$w.extractOpenApiSchemas();
36519
36581
  if (!entries) {
36520
36582
  setLoading(false);
36521
36583
  return;
@@ -36534,7 +36596,7 @@ function SchemaSyncModal({
36534
36596
  setLoading(true);
36535
36597
  setError(null);
36536
36598
  try {
36537
- const entries = await electron$t.extractOpenApiSchemasFromUrl(trimmed);
36599
+ const entries = await electron$w.extractOpenApiSchemasFromUrl(trimmed);
36538
36600
  setSpecEntries(entries);
36539
36601
  autoSelectChanged(entries);
36540
36602
  } catch (err) {
@@ -36567,7 +36629,7 @@ function SchemaSyncModal({
36567
36629
  }
36568
36630
  const entry = useStore.getState().collections[collectionId];
36569
36631
  if (entry) {
36570
- await electron$t.saveCollection(entry.relPath, entry.data);
36632
+ await electron$w.saveCollection(entry.relPath, entry.data);
36571
36633
  markCollectionClean(collectionId);
36572
36634
  }
36573
36635
  onClose();
@@ -36852,14 +36914,14 @@ function collectTagged(folder, requests, collectionVars, filterTags, parentPath
36852
36914
  }
36853
36915
  function collectAllTags(folder, requests) {
36854
36916
  const tags2 = /* @__PURE__ */ new Set();
36855
- function walk(f) {
36917
+ function walk2(f) {
36856
36918
  (f.tags ?? []).forEach((t2) => tags2.add(t2));
36857
36919
  for (const reqId of f.requestIds) {
36858
36920
  (requests[reqId]?.meta?.tags ?? []).forEach((t2) => tags2.add(t2));
36859
36921
  }
36860
- f.folders.forEach(walk);
36922
+ f.folders.forEach(walk2);
36861
36923
  }
36862
- walk(folder);
36924
+ walk2(folder);
36863
36925
  return Array.from(tags2).sort();
36864
36926
  }
36865
36927
  function folderPathTo(root2, requestId) {
@@ -36982,7 +37044,7 @@ function hasContract(r) {
36982
37044
  const c = r.contract;
36983
37045
  return !!c && (c.statusCode != null || !!c.bodySchema || !!c.bodyMatcher || (c.headers?.length ?? 0) > 0 || (c.providerStates?.length ?? 0) > 0);
36984
37046
  }
36985
- const { electron: electron$s } = window;
37047
+ const { electron: electron$v } = window;
36986
37048
  function cloudEnabled() {
36987
37049
  return Boolean(useStore.getState().workspace?.settings?.cloud?.enabled);
36988
37050
  }
@@ -36991,7 +37053,7 @@ function assertEnabled() {
36991
37053
  }
36992
37054
  async function pushContractToCloud(requests, opts) {
36993
37055
  assertEnabled();
36994
- return electron$s.cloudPushPact({
37056
+ return electron$v.cloudPushPact({
36995
37057
  consumer: opts.consumer,
36996
37058
  provider: opts.provider,
36997
37059
  consumerVersion: opts.version,
@@ -37000,15 +37062,15 @@ async function pushContractToCloud(requests, opts) {
37000
37062
  }
37001
37063
  async function pushProviderSpecToCloud(opts) {
37002
37064
  assertEnabled();
37003
- return electron$s.cloudPushSpec(opts);
37065
+ return electron$v.cloudPushSpec(opts);
37004
37066
  }
37005
37067
  function openCloudMatrix() {
37006
- void electron$s.cloudOpenMatrix();
37068
+ void electron$v.cloudOpenMatrix();
37007
37069
  }
37008
37070
  async function getCloudMockRoutes(name2) {
37009
37071
  if (!cloudEnabled()) return null;
37010
37072
  try {
37011
- const res = await electron$s.cloudGetMock(name2);
37073
+ const res = await electron$v.cloudGetMock(name2);
37012
37074
  return res.exists ? res.routes ?? [] : null;
37013
37075
  } catch {
37014
37076
  return null;
@@ -37017,7 +37079,7 @@ async function getCloudMockRoutes(name2) {
37017
37079
  async function pushMockToCloud(server, routeIds) {
37018
37080
  assertEnabled();
37019
37081
  const routes = routeIds ? (server.routes ?? []).filter((r) => routeIds.includes(r.id)) : server.routes ?? [];
37020
- return electron$s.cloudPushMock({ ...server, routes });
37082
+ return electron$v.cloudPushMock({ ...server, routes });
37021
37083
  }
37022
37084
  async function pushRequestAsMonitor(request, opts) {
37023
37085
  assertEnabled();
@@ -37038,7 +37100,7 @@ async function pushRequestAsMonitor(request, opts) {
37038
37100
  ...request.headers
37039
37101
  ];
37040
37102
  const mergedRequest = { ...request, auth: mergedAuth, headers: mergedHeaders };
37041
- return electron$s.cloudPushMonitor(
37103
+ return electron$v.cloudPushMonitor(
37042
37104
  {
37043
37105
  request: mergedRequest,
37044
37106
  setup,
@@ -37796,16 +37858,16 @@ function methodColor$2(method) {
37796
37858
  }
37797
37859
  function collectMatches(col, q) {
37798
37860
  const out = [];
37799
- const walk = (folder, path) => {
37861
+ const walk2 = (folder, path) => {
37800
37862
  for (const reqId of folder.requestIds) {
37801
37863
  const req = col.requests[reqId];
37802
37864
  if (!req) continue;
37803
37865
  const hay = [req.name, req.url, req.method, ...req.meta?.tags ?? []].join(" ").toLowerCase();
37804
37866
  if (hay.includes(q)) out.push({ collectionId: col.id, collectionName: col.name, req, path });
37805
37867
  }
37806
- for (const sub of folder.folders) walk(sub, [...path, sub.name]);
37868
+ for (const sub of folder.folders) walk2(sub, [...path, sub.name]);
37807
37869
  };
37808
- walk(col.rootFolder, []);
37870
+ walk2(col.rootFolder, []);
37809
37871
  return out;
37810
37872
  }
37811
37873
  function CollectionTree() {
@@ -56448,7 +56510,7 @@ function withContentType(headers, value) {
56448
56510
  if (idx === -1) return [...headers, next];
56449
56511
  return headers.map((h, i) => i === idx ? { ...h, value, enabled: true } : h);
56450
56512
  }
56451
- const { electron: electron$r } = window;
56513
+ const { electron: electron$u } = window;
56452
56514
  function ParamTree({ params, depth = 0 }) {
56453
56515
  if (params.length === 0) {
56454
56516
  return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-600 italic", children: "No parameters declared in WSDL." });
@@ -56462,7 +56524,7 @@ function ParamTree({ params, depth = 0 }) {
56462
56524
  }
56463
56525
  function SoapEditor({ request, onChange }) {
56464
56526
  const soap = request.body.soap ?? { wsdlUrl: "", envelope: "" };
56465
- const [operations, setOperations] = reactExports.useState([]);
56527
+ const [operations2, setOperations] = reactExports.useState([]);
56466
56528
  const [endpoints, setEndpoints] = reactExports.useState([]);
56467
56529
  const [targetNs, setTargetNs] = reactExports.useState("");
56468
56530
  const [fetchError, setFetchError] = reactExports.useState(null);
@@ -56474,7 +56536,7 @@ function SoapEditor({ request, onChange }) {
56474
56536
  let cancelled = false;
56475
56537
  (async () => {
56476
56538
  try {
56477
- const result = await electron$r.wsdlFetch(url);
56539
+ const result = await electron$u.wsdlFetch(url);
56478
56540
  if (cancelled) return;
56479
56541
  setOperations(result.operations);
56480
56542
  setEndpoints(result.endpoints);
@@ -56511,7 +56573,7 @@ function SoapEditor({ request, onChange }) {
56511
56573
  setFetching(true);
56512
56574
  setFetchError(null);
56513
56575
  try {
56514
- const result = await electron$r.wsdlFetch(soap.wsdlUrl.trim());
56576
+ const result = await electron$u.wsdlFetch(soap.wsdlUrl.trim());
56515
56577
  setOperations(result.operations);
56516
56578
  setEndpoints(result.endpoints);
56517
56579
  setTargetNs(result.targetNamespace);
@@ -56527,11 +56589,11 @@ function SoapEditor({ request, onChange }) {
56527
56589
  function selectOperation(op) {
56528
56590
  applyOperation(op);
56529
56591
  }
56530
- const selected = operations.find((o) => o.name === soap.operationName) ?? operations[0];
56592
+ const selected = operations2.find((o) => o.name === soap.operationName) ?? operations2[0];
56531
56593
  const primaryEndpoint = endpoints[0]?.address;
56532
- const versions = Array.from(new Set(operations.map((o) => o.soapVersion))).sort();
56594
+ const versions = Array.from(new Set(operations2.map((o) => o.soapVersion))).sort();
56533
56595
  const hasSavedSoap = Boolean(soap.envelope?.trim() || soap.operationName || soap.wsdlUrl?.trim());
56534
- const showFullUi = operations.length > 0;
56596
+ const showFullUi = operations2.length > 0;
56535
56597
  const showFallback = !showFullUi && hasSavedSoap;
56536
56598
  const showEmpty = !showFullUi && !showFallback;
56537
56599
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 h-full min-h-0", children: [
@@ -56555,12 +56617,12 @@ function SoapEditor({ request, onChange }) {
56555
56617
  onClick: fetchWsdl,
56556
56618
  disabled: fetching || !soap.wsdlUrl.trim(),
56557
56619
  className: "px-3 py-1.5 text-xs bg-blue-700 hover:bg-blue-600 disabled:bg-surface-800 disabled:text-surface-600 rounded transition-colors whitespace-nowrap",
56558
- children: fetching ? "Fetching…" : operations.length > 0 ? "Refresh" : "Fetch WSDL"
56620
+ children: fetching ? "Fetching…" : operations2.length > 0 ? "Refresh" : "Fetch WSDL"
56559
56621
  }
56560
56622
  )
56561
56623
  ] }),
56562
56624
  fetchError && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-red-400 bg-red-950/30 border border-red-900 rounded px-2.5 py-1.5", children: fetchError }),
56563
- operations.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-surface-900/40 border border-surface-800 rounded-md px-3 py-2 text-[11px] flex flex-col gap-1", children: [
56625
+ operations2.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-surface-900/40 border border-surface-800 rounded-md px-3 py-2 text-[11px] flex flex-col gap-1", children: [
56564
56626
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-surface-500", children: [
56565
56627
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "uppercase tracking-wider text-[9px] font-semibold text-surface-600 w-20 shrink-0", children: "Endpoint" }),
56566
56628
  /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-200 font-mono truncate", children: primaryEndpoint ?? "(not declared)" })
@@ -56572,7 +56634,7 @@ function SoapEditor({ request, onChange }) {
56572
56634
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-surface-500", children: [
56573
56635
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "uppercase tracking-wider text-[9px] font-semibold text-surface-600 w-20 shrink-0", children: "Operations" }),
56574
56636
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-300", children: [
56575
- operations.length,
56637
+ operations2.length,
56576
56638
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-600", children: [
56577
56639
  " · SOAP ",
56578
56640
  versions.join(", ")
@@ -56583,7 +56645,7 @@ function SoapEditor({ request, onChange }) {
56583
56645
  showFullUi && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 min-h-0 flex-1", children: [
56584
56646
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-44 shrink-0 flex flex-col bg-surface-900/40 border border-surface-800 rounded-md overflow-hidden", children: [
56585
56647
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2.5 py-1.5 text-[9px] uppercase tracking-wider font-semibold text-surface-600 border-b border-surface-800", children: "Operations" }),
56586
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-y-auto flex-1", children: operations.map((op) => {
56648
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-y-auto flex-1", children: operations2.map((op) => {
56587
56649
  const active = selected && op.name === selected.name && op.soapVersion === selected.soapVersion;
56588
56650
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
56589
56651
  "button",
@@ -56781,7 +56843,7 @@ function BodyTab({ request, onChange }) {
56781
56843
  mode === "soap" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SoapEditor, { request, onChange }) })
56782
56844
  ] });
56783
56845
  }
56784
- const { electron: electron$q } = window;
56846
+ const { electron: electron$t } = window;
56785
56847
  const AUTH_TYPES = ["none", "bearer", "basic", "digest", "ntlm", "apikey", "oauth2"];
56786
56848
  function AuthTab({ request, onChange }) {
56787
56849
  const auth = request.auth;
@@ -56795,7 +56857,7 @@ function AuthTab({ request, onChange }) {
56795
56857
  }
56796
56858
  async function saveSecret(ref2) {
56797
56859
  if (!secretValue || !ref2) return;
56798
- await electron$q.setSecret(ref2, secretValue);
56860
+ await electron$t.setSecret(ref2, secretValue);
56799
56861
  setSaved(true);
56800
56862
  setSecretValue("");
56801
56863
  setTimeout(() => setSaved(false), 2e3);
@@ -56807,7 +56869,7 @@ function AuthTab({ request, onChange }) {
56807
56869
  setOauth2Error("");
56808
56870
  try {
56809
56871
  const vars = {};
56810
- const result = await electron$q.oauth2StartFlow(oauth2Auth, vars);
56872
+ const result = await electron$t.oauth2StartFlow(oauth2Auth, vars);
56811
56873
  setAuth({
56812
56874
  oauth2CachedToken: result.accessToken,
56813
56875
  oauth2TokenExpiry: result.expiresAt
@@ -56825,7 +56887,7 @@ function AuthTab({ request, onChange }) {
56825
56887
  setOauth2Status("fetching");
56826
56888
  setOauth2Error("");
56827
56889
  try {
56828
- const result = await electron$q.oauth2RefreshToken(oauth2Auth, {}, oauth2RefreshToken);
56890
+ const result = await electron$t.oauth2RefreshToken(oauth2Auth, {}, oauth2RefreshToken);
56829
56891
  setAuth({
56830
56892
  oauth2CachedToken: result.accessToken,
56831
56893
  oauth2TokenExpiry: result.expiresAt
@@ -60546,8 +60608,8 @@ function getData($data, { dataLevel, dataNames, dataPathArr }) {
60546
60608
  return data;
60547
60609
  }
60548
60610
  let expr = data;
60549
- const segments = jsonPointer.split("/");
60550
- for (const segment of segments) {
60611
+ const segments2 = jsonPointer.split("/");
60612
+ for (const segment of segments2) {
60551
60613
  if (segment) {
60552
60614
  data = (0, codegen_1$n._)`${data}${(0, codegen_1$n.getProperty)((0, util_1$m.unescapeJsonPointer)(segment))}`;
60553
60615
  expr = (0, codegen_1$n._)`${expr} && ${data}`;
@@ -60695,7 +60757,7 @@ function compileSchema(sch) {
60695
60757
  }
60696
60758
  }
60697
60759
  compile.compileSchema = compileSchema;
60698
- function resolveRef(root2, baseId, ref2) {
60760
+ function resolveRef$3(root2, baseId, ref2) {
60699
60761
  var _a2;
60700
60762
  ref2 = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref2);
60701
60763
  const schOrFunc = root2.refs[ref2];
@@ -60712,7 +60774,7 @@ function resolveRef(root2, baseId, ref2) {
60712
60774
  return;
60713
60775
  return root2.refs[ref2] = inlineOrCompile.call(this, _sch);
60714
60776
  }
60715
- compile.resolveRef = resolveRef;
60777
+ compile.resolveRef = resolveRef$3;
60716
60778
  function inlineOrCompile(sch) {
60717
60779
  if ((0, resolve_1.inlineRef)(sch.schema, this.opts.inlineRefs))
60718
60780
  return sch.schema;
@@ -61909,9 +61971,9 @@ uri$1.default = uri;
61909
61971
  const rules2 = this.RULES.all;
61910
61972
  metaSchema = JSON.parse(JSON.stringify(metaSchema));
61911
61973
  for (const jsonPointer of keywordsJsonPointers) {
61912
- const segments = jsonPointer.split("/").slice(1);
61974
+ const segments2 = jsonPointer.split("/").slice(1);
61913
61975
  let keywords2 = metaSchema;
61914
- for (const seg of segments)
61976
+ for (const seg of segments2)
61915
61977
  keywords2 = keywords2[seg];
61916
61978
  for (const key in rules2) {
61917
61979
  const rule = rules2[key];
@@ -64148,7 +64210,7 @@ function SchemaTab({ request, onChange }) {
64148
64210
  ] }) })
64149
64211
  ] });
64150
64212
  }
64151
- const { electron: electron$p } = window;
64213
+ const { electron: electron$s } = window;
64152
64214
  const EMPTY = { statusCode: 200, headers: [], bodySchema: "" };
64153
64215
  function ContractTab({ request, onChange }) {
64154
64216
  const activeTab = useStore((s) => s.tabs.find((t2) => t2.id === s.activeTabId));
@@ -64162,7 +64224,7 @@ function ContractTab({ request, onChange }) {
64162
64224
  if (!lastResponse?.body) return;
64163
64225
  setInferring(true);
64164
64226
  try {
64165
- const schema = await electron$p.inferContractSchema(lastResponse.body);
64227
+ const schema = await electron$s.inferContractSchema(lastResponse.body);
64166
64228
  if (schema) update({ bodySchema: schema });
64167
64229
  } finally {
64168
64230
  setInferring(false);
@@ -64360,8 +64422,8 @@ function StreamTab({ request, onChange }) {
64360
64422
  ] })
64361
64423
  ] });
64362
64424
  }
64363
- const { electron: electron$o } = window;
64364
- function formatTime$1(ts) {
64425
+ const { electron: electron$r } = window;
64426
+ function formatTime$2(ts) {
64365
64427
  const d = new Date(ts);
64366
64428
  const hh = String(d.getHours()).padStart(2, "0");
64367
64429
  const mm = String(d.getMinutes()).padStart(2, "0");
@@ -64380,14 +64442,14 @@ function WebSocketPanel({ request }) {
64380
64442
  const [sendText, setSendText] = reactExports.useState("");
64381
64443
  const logEndRef = reactExports.useRef(null);
64382
64444
  reactExports.useEffect(() => {
64383
- electron$o.onWsMessage(({ requestId, message }) => {
64445
+ electron$r.onWsMessage(({ requestId, message }) => {
64384
64446
  addWsMessage(requestId, message);
64385
64447
  });
64386
- electron$o.onWsStatus(({ requestId, status, error: error2 }) => {
64448
+ electron$r.onWsStatus(({ requestId, status, error: error2 }) => {
64387
64449
  setWsStatus(requestId, status, error2);
64388
64450
  });
64389
64451
  return () => {
64390
- electron$o.offWsEvents();
64452
+ electron$r.offWsEvents();
64391
64453
  };
64392
64454
  }, [addWsMessage, setWsStatus]);
64393
64455
  reactExports.useEffect(() => {
@@ -64400,19 +64462,19 @@ function WebSocketPanel({ request }) {
64400
64462
  if (h.enabled && h.key) headers[h.key] = h.value;
64401
64463
  }
64402
64464
  try {
64403
- await electron$o.wsConnect(request.id, request.url, headers);
64465
+ await electron$r.wsConnect(request.id, request.url, headers);
64404
64466
  } catch (err) {
64405
64467
  setWsStatus(request.id, "error", err instanceof Error ? err.message : String(err));
64406
64468
  }
64407
64469
  }
64408
64470
  async function disconnect() {
64409
- await electron$o.wsDisconnect(request.id);
64471
+ await electron$r.wsDisconnect(request.id);
64410
64472
  }
64411
64473
  async function sendMessage() {
64412
64474
  const text = sendText.trim();
64413
64475
  if (!text || !isConnected) return;
64414
64476
  try {
64415
- await electron$o.wsSend(request.id, text);
64477
+ await electron$r.wsSend(request.id, text);
64416
64478
  const msg = {
64417
64479
  id: crypto.randomUUID(),
64418
64480
  direction: "sent",
@@ -64475,7 +64537,7 @@ function WebSocketPanel({ request }) {
64475
64537
  children: [
64476
64538
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `flex-shrink-0 font-mono font-bold ${msg.direction === "sent" ? "text-blue-400" : "text-emerald-400"}`, children: msg.direction === "sent" ? "→" : "←" }),
64477
64539
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 font-mono text-surface-300 whitespace-pre-wrap break-all", children: msg.data }),
64478
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-shrink-0 text-surface-400 font-mono text-[10px] mt-px", children: formatTime$1(msg.timestamp) })
64540
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-shrink-0 text-surface-400 font-mono text-[10px] mt-px", children: formatTime$2(msg.timestamp) })
64479
64541
  ]
64480
64542
  },
64481
64543
  msg.id
@@ -64515,6 +64577,209 @@ function WebSocketPanel({ request }) {
64515
64577
  ] })
64516
64578
  ] });
64517
64579
  }
64580
+ const { electron: electron$q } = window;
64581
+ function formatTime$1(ts) {
64582
+ const d = new Date(ts);
64583
+ const p2 = (n, w = 2) => String(n).padStart(w, "0");
64584
+ return `${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}.${p2(d.getMilliseconds(), 3)}`;
64585
+ }
64586
+ const DEFAULT_GRPC = { message: "{}", metadata: [], plaintext: false };
64587
+ function metaToText(meta2) {
64588
+ return (meta2 ?? []).map((m) => `${m.key}: ${m.value}`).join("\n");
64589
+ }
64590
+ function textToMeta(text) {
64591
+ return text.split("\n").map((line) => {
64592
+ const i = line.indexOf(":");
64593
+ if (i === -1) return null;
64594
+ const key = line.slice(0, i).trim();
64595
+ if (!key) return null;
64596
+ return { key, value: line.slice(i + 1).trim(), enabled: true };
64597
+ }).filter((x) => x !== null);
64598
+ }
64599
+ function GrpcPanel({ request, onChange }) {
64600
+ const grpc = { ...DEFAULT_GRPC, ...request.body.grpc ?? {} };
64601
+ const grpcCalls = useStore((s) => s.grpcCalls);
64602
+ const setGrpcStatus = useStore((s) => s.setGrpcStatus);
64603
+ const addGrpcMessage = useStore((s) => s.addGrpcMessage);
64604
+ const clearGrpcMessages = useStore((s) => s.clearGrpcMessages);
64605
+ const setGrpcServices = useStore((s) => s.setGrpcServices);
64606
+ const setGrpcProtoError = useStore((s) => s.setGrpcProtoError);
64607
+ const call = grpcCalls[request.id] ?? { status: "idle", messages: [], services: [] };
64608
+ const isRunning = call.status === "running";
64609
+ const [loadingProto, setLoadingProto] = reactExports.useState(false);
64610
+ const logEndRef = reactExports.useRef(null);
64611
+ function patchGrpc(changes) {
64612
+ onChange({ body: { ...request.body, mode: "grpc", grpc: { ...grpc, ...changes } } });
64613
+ }
64614
+ reactExports.useEffect(() => {
64615
+ electron$q.onGrpcMessage(({ requestId, message }) => {
64616
+ addGrpcMessage(requestId, message);
64617
+ });
64618
+ electron$q.onGrpcStatus(({ requestId, status, code: code2, codeName, error: error2 }) => {
64619
+ setGrpcStatus(requestId, status, { code: code2, codeName, error: error2 });
64620
+ });
64621
+ return () => electron$q.offGrpcEvents();
64622
+ }, [addGrpcMessage, setGrpcStatus]);
64623
+ reactExports.useEffect(() => {
64624
+ logEndRef.current?.scrollIntoView({ behavior: "smooth" });
64625
+ }, [call.messages.length]);
64626
+ const selectedService = reactExports.useMemo(
64627
+ () => call.services.find((s) => s.name === grpc.serviceName),
64628
+ [call.services, grpc.serviceName]
64629
+ );
64630
+ async function loadProto() {
64631
+ if (!grpc.protoSource?.trim() && !grpc.protoPath?.trim()) return;
64632
+ setLoadingProto(true);
64633
+ try {
64634
+ const result = await electron$q.grpcLoadProto({ protoSource: grpc.protoSource, protoPath: grpc.protoPath });
64635
+ const services = result.services;
64636
+ setGrpcServices(request.id, services);
64637
+ if (services.length && !services.some((s) => s.name === grpc.serviceName)) {
64638
+ patchGrpc({ serviceName: services[0].name, methodName: services[0].methods[0]?.name });
64639
+ }
64640
+ } catch (err) {
64641
+ setGrpcProtoError(request.id, err instanceof Error ? err.message : String(err));
64642
+ } finally {
64643
+ setLoadingProto(false);
64644
+ }
64645
+ }
64646
+ async function invoke() {
64647
+ if (!request.url || !grpc.serviceName || !grpc.methodName) return;
64648
+ clearGrpcMessages(request.id);
64649
+ addGrpcMessage(request.id, { id: crypto.randomUUID(), direction: "sent", data: grpc.message || "{}", timestamp: Date.now() });
64650
+ const metadata2 = {};
64651
+ for (const m of grpc.metadata ?? []) if (m.enabled && m.key) metadata2[m.key] = m.value;
64652
+ try {
64653
+ await electron$q.grpcInvoke(request.id, {
64654
+ target: request.url,
64655
+ serviceName: grpc.serviceName,
64656
+ methodName: grpc.methodName,
64657
+ message: grpc.message || "{}",
64658
+ metadata: metadata2,
64659
+ plaintext: grpc.plaintext,
64660
+ protoSource: grpc.protoSource,
64661
+ protoPath: grpc.protoPath
64662
+ });
64663
+ } catch (err) {
64664
+ setGrpcStatus(request.id, "error", { error: err instanceof Error ? err.message : String(err) });
64665
+ }
64666
+ }
64667
+ function cancel() {
64668
+ electron$q.grpcCancel(request.id);
64669
+ }
64670
+ const statusColors = {
64671
+ running: "bg-amber-400",
64672
+ completed: "bg-emerald-500",
64673
+ error: "bg-red-500",
64674
+ idle: "bg-surface-600"
64675
+ };
64676
+ const inputCls = "bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs focus:outline-none focus:border-blue-500";
64677
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
64678
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 pt-2 pb-1 flex-shrink-0 flex items-center gap-2", children: [
64679
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-violet-400 bg-surface-800 border border-surface-700 rounded px-2 py-1", children: "gRPC" }),
64680
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-600", children: "Set the target (host:port) in the URL field above." }),
64681
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `ml-auto w-2 h-2 rounded-full ${statusColors[call.status] ?? "bg-surface-600"}`, title: call.status }),
64682
+ call.codeName && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono text-surface-400", children: call.codeName })
64683
+ ] }),
64684
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto px-4 pb-2 flex flex-col gap-3", children: [
64685
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64686
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
64687
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-surface-400", children: "Proto (paste .proto source, or a file path below)" }),
64688
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: loadProto, disabled: loadingProto, className: "text-[10px] px-2 py-0.5 rounded border border-surface-700 text-surface-300 hover:border-violet-500 hover:text-violet-400 disabled:opacity-40", children: loadingProto ? "Loading…" : "Load proto" })
64689
+ ] }),
64690
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64691
+ "textarea",
64692
+ {
64693
+ value: grpc.protoSource ?? "",
64694
+ onChange: (e) => patchGrpc({ protoSource: e.target.value }),
64695
+ rows: 4,
64696
+ spellCheck: false,
64697
+ placeholder: 'syntax = "proto3";\npackage helloworld;\nservice Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }',
64698
+ className: "w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-violet-500 placeholder-surface-700"
64699
+ }
64700
+ ),
64701
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64702
+ "input",
64703
+ {
64704
+ value: grpc.protoPath ?? "",
64705
+ onChange: (e) => patchGrpc({ protoPath: e.target.value }),
64706
+ placeholder: "/path/to/service.proto (optional; used if source is empty)",
64707
+ className: `${inputCls} w-full font-mono mt-1`
64708
+ }
64709
+ ),
64710
+ call.protoError && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-400 mt-1", children: call.protoError })
64711
+ ] }),
64712
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-[1fr_1fr_auto] gap-2 items-end", children: [
64713
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64714
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[11px] text-surface-400 mb-1", children: "Service" }),
64715
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("select", { value: grpc.serviceName ?? "", onChange: (e) => patchGrpc({ serviceName: e.target.value, methodName: call.services.find((s) => s.name === e.target.value)?.methods[0]?.name }), className: `${inputCls} w-full`, children: [
64716
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: call.services.length ? "Select…" : "Load a proto first" }),
64717
+ call.services.map((s) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: s.name, children: s.name }, s.name))
64718
+ ] })
64719
+ ] }),
64720
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64721
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[11px] text-surface-400 mb-1", children: "Method" }),
64722
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("select", { value: grpc.methodName ?? "", onChange: (e) => patchGrpc({ methodName: e.target.value }), className: `${inputCls} w-full`, children: [
64723
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "Select…" }),
64724
+ selectedService?.methods.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: m.name, children: [
64725
+ m.name,
64726
+ m.responseStream ? " (stream)" : ""
64727
+ ] }, m.name))
64728
+ ] })
64729
+ ] }),
64730
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-1.5 text-xs text-surface-300 pb-1.5", children: [
64731
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: !!grpc.plaintext, onChange: (e) => patchGrpc({ plaintext: e.target.checked }) }),
64732
+ "Plaintext"
64733
+ ] })
64734
+ ] }),
64735
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64736
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[11px] text-surface-400 mb-1", children: "Request message (JSON)" }),
64737
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64738
+ "textarea",
64739
+ {
64740
+ value: grpc.message,
64741
+ onChange: (e) => patchGrpc({ message: e.target.value }),
64742
+ rows: 4,
64743
+ spellCheck: false,
64744
+ placeholder: '{ "name": "world" }',
64745
+ className: "w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-violet-500 placeholder-surface-700"
64746
+ }
64747
+ )
64748
+ ] }),
64749
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64750
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "block text-[11px] text-surface-400 mb-1", children: [
64751
+ "Metadata (one ",
64752
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono", children: "key: value" }),
64753
+ " per line)"
64754
+ ] }),
64755
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64756
+ "textarea",
64757
+ {
64758
+ value: metaToText(grpc.metadata),
64759
+ onChange: (e) => patchGrpc({ metadata: textToMeta(e.target.value) }),
64760
+ rows: 2,
64761
+ spellCheck: false,
64762
+ placeholder: "authorization: Bearer {{TOKEN}}",
64763
+ className: "w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-violet-500 placeholder-surface-700"
64764
+ }
64765
+ )
64766
+ ] }),
64767
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
64768
+ isRunning ? /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: cancel, className: "px-4 py-1.5 bg-red-700 hover:bg-red-600 rounded text-sm font-medium", children: "Cancel" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: invoke, disabled: !request.url || !grpc.serviceName || !grpc.methodName, className: "px-4 py-1.5 bg-violet-600 hover:bg-violet-500 disabled:bg-surface-800 disabled:text-surface-500 rounded text-sm font-medium", children: "Invoke" }),
64769
+ call.messages.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => clearGrpcMessages(request.id), className: "text-[10px] text-surface-600 hover:text-surface-400 ml-auto", children: "Clear" })
64770
+ ] }),
64771
+ call.status === "error" && call.error && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-1.5 bg-red-900/40 border border-red-700/50 rounded text-xs text-red-400", children: call.error }),
64772
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border border-surface-800 rounded bg-surface-950 min-h-[120px]", children: call.messages.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-center text-xs text-surface-500", children: "No messages yet" }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 flex flex-col gap-1", children: [
64773
+ call.messages.map((msg) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex gap-2 items-start text-xs rounded px-2 py-1.5 ${msg.direction === "sent" ? "bg-blue-900/30 border border-blue-800/40" : "bg-emerald-900/20 border border-emerald-800/30"}`, children: [
64774
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `flex-shrink-0 font-mono font-bold ${msg.direction === "sent" ? "text-blue-400" : "text-emerald-400"}`, children: msg.direction === "sent" ? "→" : "←" }),
64775
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 font-mono text-surface-300 whitespace-pre-wrap break-all", children: msg.data }),
64776
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-shrink-0 text-surface-400 font-mono text-[10px] mt-px", children: formatTime$1(msg.timestamp) })
64777
+ ] }, msg.id)),
64778
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: logEndRef })
64779
+ ] }) })
64780
+ ] })
64781
+ ] });
64782
+ }
64518
64783
  function statusColor$3(code2) {
64519
64784
  const d = String(code2)[0];
64520
64785
  return d === "2" ? "text-emerald-400" : d === "3" ? "text-amber-400" : "text-red-400";
@@ -64770,7 +65035,7 @@ function FuzzResultsPanel({ report, onClear }) {
64770
65035
  ] }) })
64771
65036
  ] });
64772
65037
  }
64773
- const { electron: electron$n } = window;
65038
+ const { electron: electron$p } = window;
64774
65039
  const WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
64775
65040
  function FuzzModal({ request, onClose }) {
64776
65041
  const environments = useStore((s) => s.environments);
@@ -64796,7 +65061,7 @@ function FuzzModal({ request, onClose }) {
64796
65061
  const env = resolveEnvironmentById(environments, activeEnvironmentId);
64797
65062
  const envVars = env ? Object.fromEntries(env.variables.filter((v) => v.enabled).map((v) => [v.key, v.value])) : {};
64798
65063
  const collectionVars = activeCollectionId ? collections[activeCollectionId]?.data.collectionVariables ?? {} : {};
64799
- const result = await electron$n.fuzzContracts({
65064
+ const result = await electron$p.fuzzContracts({
64800
65065
  requests: [request],
64801
65066
  envVars,
64802
65067
  collectionVars,
@@ -64888,7 +65153,7 @@ function FuzzModal({ request, onClose }) {
64888
65153
  }
64889
65154
  );
64890
65155
  }
64891
- const { electron: electron$m } = window;
65156
+ const { electron: electron$o } = window;
64892
65157
  const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "QUERY"];
64893
65158
  const METHOD_COLORS = {
64894
65159
  GET: "text-emerald-400",
@@ -65013,7 +65278,7 @@ function RequestBuilder({ request }) {
65013
65278
  activeEnvironmentId
65014
65279
  );
65015
65280
  const hookSessionVars = useStore.getState().sessionVars;
65016
- const r = await electron$m.sendRequest({
65281
+ const r = await electron$o.sendRequest({
65017
65282
  ...basePayload,
65018
65283
  environment: hookEnv,
65019
65284
  request: hook,
@@ -65061,7 +65326,7 @@ function RequestBuilder({ request }) {
65061
65326
  startLiveStream(activeTabId, streamId);
65062
65327
  let result;
65063
65328
  try {
65064
- result = await electron$m.sendRequest({
65329
+ result = await electron$o.sendRequest({
65065
65330
  ...basePayload,
65066
65331
  environment: freshEnv,
65067
65332
  request: mergedRequest,
@@ -65093,7 +65358,7 @@ function RequestBuilder({ request }) {
65093
65358
  activeEnvironmentId
65094
65359
  );
65095
65360
  const hookSessionVars = useStore.getState().sessionVars;
65096
- const r = await electron$m.sendRequest({
65361
+ const r = await electron$o.sendRequest({
65097
65362
  ...basePayload,
65098
65363
  environment: hookEnv,
65099
65364
  request: hook,
@@ -65143,6 +65408,7 @@ function RequestBuilder({ request }) {
65143
65408
  const hasPostScript = Boolean(request.postRequestScript?.trim());
65144
65409
  const isWs = request.protocol === "websocket";
65145
65410
  const isSoap = request.protocol === "soap";
65411
+ const isGrpc = request.protocol === "grpc";
65146
65412
  const isExample = !!activeAppTab?.exampleId;
65147
65413
  const tabs = [
65148
65414
  // SOAP collapses Params + Body into a single "SOAP" tab — the WSDL drives both.
@@ -65190,7 +65456,7 @@ function RequestBuilder({ request }) {
65190
65456
  "button",
65191
65457
  {
65192
65458
  onClick: () => update({ protocol: "http" }),
65193
- className: `px-2 py-1.5 transition-colors ${request.protocol !== "websocket" && request.protocol !== "soap" ? "bg-blue-600 text-white" : "text-surface-500 hover:text-white"}`,
65459
+ className: `px-2 py-1.5 transition-colors ${!isWs && !isSoap && !isGrpc ? "bg-blue-600 text-white" : "text-surface-500 hover:text-white"}`,
65194
65460
  title: "HTTP request",
65195
65461
  children: "HTTP"
65196
65462
  }
@@ -65219,9 +65485,21 @@ function RequestBuilder({ request }) {
65219
65485
  title: "SOAP - endpoint and method are derived from the WSDL",
65220
65486
  children: "SOAP"
65221
65487
  }
65488
+ ),
65489
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
65490
+ "button",
65491
+ {
65492
+ onClick: () => update({
65493
+ protocol: "grpc",
65494
+ body: request.body.mode === "grpc" ? request.body : { ...request.body, mode: "grpc", grpc: request.body.grpc ?? { message: "{}", metadata: [], plaintext: false } }
65495
+ }),
65496
+ className: `px-2 py-1.5 transition-colors ${isGrpc ? "bg-violet-700 text-violet-100" : "text-surface-500 hover:text-white"}`,
65497
+ title: "gRPC - proto-defined services over HTTP/2",
65498
+ children: "gRPC"
65499
+ }
65222
65500
  )
65223
65501
  ] }),
65224
- !isWs && !isSoap && (customVerb ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65502
+ !isWs && !isSoap && !isGrpc && (customVerb ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65225
65503
  "input",
65226
65504
  {
65227
65505
  autoFocus: true,
@@ -65262,18 +65540,26 @@ function RequestBuilder({ request }) {
65262
65540
  children: "POST"
65263
65541
  }
65264
65542
  ),
65543
+ isGrpc && /* @__PURE__ */ jsxRuntimeExports.jsx(
65544
+ "span",
65545
+ {
65546
+ className: "bg-surface-900 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold text-violet-400 select-none",
65547
+ title: "gRPC target is host:port",
65548
+ children: "gRPC"
65549
+ }
65550
+ ),
65265
65551
  /* @__PURE__ */ jsxRuntimeExports.jsx(
65266
65552
  VarInput,
65267
65553
  {
65268
65554
  value: request.url,
65269
65555
  onChange: (url) => update({ url }),
65270
65556
  onPaste: handleUrlPaste,
65271
- placeholder: isWs ? "ws://example.com/socket" : isSoap ? "Endpoint (auto-filled from WSDL <soap:address>)" : "https://api.example.com/endpoint",
65557
+ placeholder: isWs ? "ws://example.com/socket" : isSoap ? "Endpoint (auto-filled from WSDL <soap:address>)" : isGrpc ? "localhost:50051" : "https://api.example.com/endpoint",
65272
65558
  wrapperClassName: "flex-1",
65273
65559
  className: `border rounded px-3 py-1.5 text-sm focus:outline-none focus:border-blue-500 font-mono placeholder-surface-700 ${isSoap ? "bg-surface-900 border-amber-900/50 text-surface-300" : "bg-surface-800 border-surface-700"}`
65274
65560
  }
65275
65561
  ),
65276
- !isWs && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65562
+ !isWs && !isGrpc && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65277
65563
  /* @__PURE__ */ jsxRuntimeExports.jsx(
65278
65564
  "button",
65279
65565
  {
@@ -65295,7 +65581,7 @@ function RequestBuilder({ request }) {
65295
65581
  ] })
65296
65582
  ] }),
65297
65583
  showFuzz && /* @__PURE__ */ jsxRuntimeExports.jsx(FuzzModal, { request, onClose: () => setShowFuzz(false) }),
65298
- isWs ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(WebSocketPanel, { request }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65584
+ isWs ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(WebSocketPanel, { request }) }) : isGrpc ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(GrpcPanel, { request, onChange: update }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65299
65585
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex border-b border-surface-800 px-4 gap-0 flex-shrink-0", children: [
65300
65586
  tabs.map((tab) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
65301
65587
  "button",
@@ -65828,7 +66114,7 @@ function InteractiveBody({ body, contentType, onAssert }) {
65828
66114
  treeContent
65829
66115
  ] });
65830
66116
  }
65831
- const { electron: electron$l } = window;
66117
+ const { electron: electron$n } = window;
65832
66118
  const RENDER_TAIL = 500;
65833
66119
  const CLOSE_LABEL = {
65834
66120
  complete: "closed",
@@ -65869,7 +66155,7 @@ function StreamView({ events, streaming, streamId, streamClose, firstEventMs })
65869
66155
  if (!streamId) return;
65870
66156
  setStopping(true);
65871
66157
  try {
65872
- await electron$l.stopStream(streamId);
66158
+ await electron$n.stopStream(streamId);
65873
66159
  } catch {
65874
66160
  }
65875
66161
  }
@@ -66031,7 +66317,7 @@ function HookResultsPanel({ results }) {
66031
66317
  }) })
66032
66318
  ] });
66033
66319
  }
66034
- const { electron: electron$k } = window;
66320
+ const { electron: electron$m } = window;
66035
66321
  function extractPath(url) {
66036
66322
  try {
66037
66323
  return new URL(url).pathname || "/";
@@ -66085,14 +66371,14 @@ function SaveAsMockModal({ onClose }) {
66085
66371
  const entry = state.mocks[serverId];
66086
66372
  const updated = { ...entry.data, name: newServerName, port: Number(newServerPort), routes: [route] };
66087
66373
  updateMock(serverId, updated);
66088
- await electron$k.saveMock(entry.relPath, updated);
66374
+ await electron$m.saveMock(entry.relPath, updated);
66089
66375
  const ws2 = useStore.getState().workspace;
66090
- if (ws2) await electron$k.saveWorkspace(ws2);
66376
+ if (ws2) await electron$m.saveWorkspace(ws2);
66091
66377
  } else {
66092
66378
  const entry = useStore.getState().mocks[serverId];
66093
66379
  const updated = { ...entry.data, routes: [...entry.data.routes, route] };
66094
66380
  updateMock(serverId, updated);
66095
- await electron$k.saveMock(entry.relPath, updated);
66381
+ await electron$m.saveMock(entry.relPath, updated);
66096
66382
  }
66097
66383
  onClose();
66098
66384
  } finally {
@@ -66519,7 +66805,7 @@ function validateHttpSemantics(res, opts = {}) {
66519
66805
  const order = { error: 0, warning: 1, hint: 2 };
66520
66806
  return f.sort((a, b) => order[a.severity] - order[b.severity]);
66521
66807
  }
66522
- const { electron: electron$j } = window;
66808
+ const { electron: electron$l } = window;
66523
66809
  function xmlWellFormed(body) {
66524
66810
  try {
66525
66811
  const doc2 = new DOMParser().parseFromString(body, "application/xml");
@@ -66538,6 +66824,8 @@ function requestBodyText(body) {
66538
66824
  return body.graphql?.query ?? "";
66539
66825
  case "soap":
66540
66826
  return body.soap?.envelope ?? "";
66827
+ case "grpc":
66828
+ return body.grpc?.message ?? "";
66541
66829
  case "form":
66542
66830
  return (body.form ?? []).filter((p2) => p2.enabled && p2.key).map((p2) => `${p2.key}=${p2.value}`).join("\n");
66543
66831
  default:
@@ -66657,7 +66945,7 @@ function ResponseViewer() {
66657
66945
  const contractToast = useToast(2500);
66658
66946
  async function saveAsContract() {
66659
66947
  if (!response || !requestId || !activeTabId) return;
66660
- const schema = response.body ? await electron$j.inferContractSchema(response.body) : null;
66948
+ const schema = response.body ? await electron$l.inferContractSchema(response.body) : null;
66661
66949
  const contentType2 = response.headers["content-type"];
66662
66950
  const headers = contentType2 ? [{ key: "content-type", value: contentType2, required: true }] : [];
66663
66951
  updateRequest(requestId, {
@@ -66965,7 +67253,7 @@ function ResponseViewer() {
66965
67253
  ] }) })
66966
67254
  ] });
66967
67255
  }
66968
- const { electron: electron$i } = window;
67256
+ const { electron: electron$k } = window;
66969
67257
  const TARGETS = [
66970
67258
  { id: "robot_framework", label: "Robot Framework", description: "Python RequestsLibrary keywords + test suite" },
66971
67259
  { id: "playwright_ts", label: "Playwright TS", description: "TypeScript page-object API classes + spec files" },
@@ -67006,7 +67294,7 @@ function GeneratorPanel() {
67006
67294
  try {
67007
67295
  const col = collections[selectedCollectionId]?.data;
67008
67296
  const env = resolveEnvironmentById(environments, activeEnvironmentId);
67009
- const generated = await electron$i.generateCode({ collection: col, environment: env, target });
67297
+ const generated = await electron$k.generateCode({ collection: col, environment: env, target });
67010
67298
  setFiles(generated);
67011
67299
  setSelectedFile(generated[0]?.path ?? null);
67012
67300
  } catch (e) {
@@ -67018,7 +67306,7 @@ function GeneratorPanel() {
67018
67306
  async function saveZip() {
67019
67307
  if (files.length === 0) return;
67020
67308
  const col = collections[selectedCollectionId]?.data;
67021
- await electron$i.saveGeneratedFilesAsZip(files, col?.name ?? "api-tests", target);
67309
+ await electron$k.saveGeneratedFilesAsZip(files, col?.name ?? "api-tests", target);
67022
67310
  }
67023
67311
  const selectedContent = files.find((f) => f.path === selectedFile)?.content ?? "";
67024
67312
  const activeTarget = TARGETS.find((t2) => t2.id === target);
@@ -67202,7 +67490,7 @@ function historyToHar(entries, creatorVersion = "1.0") {
67202
67490
  }
67203
67491
  }, null, 2);
67204
67492
  }
67205
- const { electron: electron$h } = window;
67493
+ const { electron: electron$j } = window;
67206
67494
  const STATUS_COLOR = {
67207
67495
  "2": "text-emerald-400",
67208
67496
  "3": "text-amber-400",
@@ -67250,7 +67538,7 @@ function HistoryPanel() {
67250
67538
  }
67251
67539
  async function downloadHar() {
67252
67540
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:T]/g, "-");
67253
- await electron$h.saveResults(historyToHar(history2), `api-spector-history-${stamp}.har`);
67541
+ await electron$j.saveResults(historyToHar(history2), `api-spector-history-${stamp}.har`);
67254
67542
  }
67255
67543
  function open(entry) {
67256
67544
  setSelected(entry);
@@ -67381,30 +67669,30 @@ function HistoryRow({
67381
67669
  }
67382
67670
  );
67383
67671
  }
67384
- const { electron: electron$g } = window;
67672
+ const { electron: electron$i } = window;
67385
67673
  function WelcomeScreen() {
67386
67674
  const { applyWorkspace } = useWorkspaceLoader();
67387
67675
  const [recents, setRecents] = reactExports.useState([]);
67388
67676
  const [update, setUpdate] = reactExports.useState(null);
67389
67677
  reactExports.useEffect(() => {
67390
- electron$g.getRecentWorkspaces().then(setRecents).catch(() => setRecents([]));
67391
- electron$g.checkForUpdate().then((info) => {
67678
+ electron$i.getRecentWorkspaces().then(setRecents).catch(() => setRecents([]));
67679
+ electron$i.checkForUpdate().then((info) => {
67392
67680
  if (info?.updateAvailable) setUpdate(info);
67393
67681
  }).catch(() => {
67394
67682
  });
67395
67683
  }, []);
67396
67684
  async function openWorkspace() {
67397
- const result = await electron$g.openWorkspace();
67685
+ const result = await electron$i.openWorkspace();
67398
67686
  if (!result) return;
67399
67687
  await applyWorkspace(result.workspace, result.workspacePath);
67400
67688
  }
67401
67689
  async function newWorkspace() {
67402
- const result = await electron$g.newWorkspace();
67690
+ const result = await electron$i.newWorkspace();
67403
67691
  if (!result) return;
67404
67692
  await applyWorkspace(result.workspace, result.workspacePath);
67405
67693
  }
67406
67694
  async function openRecent(path) {
67407
- const result = await electron$g.openWorkspacePath(path);
67695
+ const result = await electron$i.openWorkspacePath(path);
67408
67696
  if (!result) {
67409
67697
  setRecents((prev) => prev.filter((r) => r.path !== path));
67410
67698
  return;
@@ -67444,7 +67732,7 @@ function WelcomeScreen() {
67444
67732
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-400 text-sm max-w-sm", children: "Local-first API testing with Robot Framework & Playwright code generation. Secrets stay on your machine." }),
67445
67733
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[11px] mt-3", style: { color: "var(--text-muted)" }, children: [
67446
67734
  "version ",
67447
- "0.4.9"
67735
+ "0.5.0"
67448
67736
  ] })
67449
67737
  ] }),
67450
67738
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 w-64", children: [
@@ -67488,7 +67776,7 @@ function WelcomeScreen() {
67488
67776
  ] })
67489
67777
  ] });
67490
67778
  }
67491
- const { electron: electron$f } = window;
67779
+ const { electron: electron$h } = window;
67492
67780
  const EXAMPLES = [
67493
67781
  {
67494
67782
  label: "macOS / Linux (~/.zshrc or ~/.bashrc)",
@@ -67512,7 +67800,7 @@ function MasterKeyModal({ onSuccess, onCancel }) {
67512
67800
  setError("Password cannot be empty.");
67513
67801
  return;
67514
67802
  }
67515
- await electron$f.setMasterKey(password);
67803
+ await electron$h.setMasterKey(password);
67516
67804
  onSuccess(password);
67517
67805
  }
67518
67806
  function copy(idx, text) {
@@ -67596,7 +67884,7 @@ function MasterKeyModal({ onSuccess, onCancel }) {
67596
67884
  }
67597
67885
  );
67598
67886
  }
67599
- const { electron: electron$e } = window;
67887
+ const { electron: electron$g } = window;
67600
67888
  async function shortHash(value) {
67601
67889
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
67602
67890
  return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
@@ -67723,7 +68011,7 @@ function EnvironmentEditor({ onClose }) {
67723
68011
  async function saveEncrypted(idx) {
67724
68012
  const plaintext = secretInputs[idx] ?? "";
67725
68013
  if (!plaintext) return;
67726
- const { set: set2 } = await electron$e.checkMasterKey();
68014
+ const { set: set2 } = await electron$g.checkMasterKey();
67727
68015
  if (!set2) {
67728
68016
  setPendingEncryptIdx(idx);
67729
68017
  return;
@@ -67760,9 +68048,9 @@ function EnvironmentEditor({ onClose }) {
67760
68048
  } : state.workspace
67761
68049
  }));
67762
68050
  const ws2 = useStore.getState().workspace;
67763
- if (ws2) await electron$e.saveWorkspace(ws2);
68051
+ if (ws2) await electron$g.saveWorkspace(ws2);
67764
68052
  }
67765
- await electron$e.saveEnvironment(newRelPath, env);
68053
+ await electron$g.saveEnvironment(newRelPath, env);
67766
68054
  }
67767
68055
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
67768
68056
  pendingEncryptIdx !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -68072,7 +68360,7 @@ function EnvironmentEditor({ onClose }) {
68072
68360
  )
68073
68361
  ] });
68074
68362
  }
68075
- const { electron: electron$d } = window;
68363
+ const { electron: electron$f } = window;
68076
68364
  function EnvironmentBar({ inline = false }) {
68077
68365
  const environments = useStore((s) => s.environments);
68078
68366
  const activeEnvironmentId = useStore((s) => s.activeEnvironmentId);
@@ -68087,7 +68375,7 @@ function EnvironmentBar({ inline = false }) {
68087
68375
  if (id2) {
68088
68376
  const hasSecrets = environments[id2]?.data.variables.some((v) => v.enabled && v.secret);
68089
68377
  if (hasSecrets) {
68090
- const { set: set2 } = await electron$d.checkMasterKey();
68378
+ const { set: set2 } = await electron$f.checkMasterKey();
68091
68379
  if (!set2) {
68092
68380
  setPendingEnvId(id2);
68093
68381
  return;
@@ -68141,7 +68429,7 @@ function EnvironmentBar({ inline = false }) {
68141
68429
  controls
68142
68430
  ] });
68143
68431
  }
68144
- const { electron: electron$c } = window;
68432
+ const { electron: electron$e } = window;
68145
68433
  const DEFAULT_PII_PATTERNS = ["authorization", "password", "token", "secret", "api-key", "x-api-key"];
68146
68434
  const ZOOM_STEPS = [0.75, 0.9, 1, 1.1, 1.25, 1.5];
68147
68435
  function WorkspaceSettingsModal({ onClose }) {
@@ -68173,8 +68461,8 @@ function WorkspaceSettingsModal({ onClose }) {
68173
68461
  async function testCloud() {
68174
68462
  setCloudTest({ status: "testing" });
68175
68463
  try {
68176
- if (cloudToken.trim()) await electron$c.setSecret("cloud:token", cloudToken.trim());
68177
- const me = await electron$c.cloudTest();
68464
+ if (cloudToken.trim()) await electron$e.setSecret("cloud:token", cloudToken.trim());
68465
+ const me = await electron$e.cloudTest();
68178
68466
  setCloudTest({ status: "ok", msg: `Connected as ${me.email} · ${me.organization} (${me.plan})` });
68179
68467
  } catch (e) {
68180
68468
  setCloudTest({ status: "err", msg: e.message });
@@ -68198,7 +68486,7 @@ function WorkspaceSettingsModal({ onClose }) {
68198
68486
  async function vaultOidcLogin() {
68199
68487
  setOidc({ status: "busy", msg: "Complete the sign-in in your browser…" });
68200
68488
  try {
68201
- const r = await electron$c.vaultOidcLogin({
68489
+ const r = await electron$e.vaultOidcLogin({
68202
68490
  address: vAddress.trim(),
68203
68491
  mount: vLoginMount.trim() || void 0,
68204
68492
  role: vJwtRole.trim() || void 0,
@@ -68250,7 +68538,7 @@ function WorkspaceSettingsModal({ onClose }) {
68250
68538
  } else {
68251
68539
  delete settings.cloud;
68252
68540
  }
68253
- if (cloudToken.trim()) await electron$c.setSecret("cloud:token", cloudToken.trim());
68541
+ if (cloudToken.trim()) await electron$e.setSecret("cloud:token", cloudToken.trim());
68254
68542
  if (defaultEnvironment) settings.defaultEnvironment = defaultEnvironment;
68255
68543
  else delete settings.defaultEnvironment;
68256
68544
  if (persistHistory) settings.persistHistory = true;
@@ -68277,9 +68565,9 @@ function WorkspaceSettingsModal({ onClose }) {
68277
68565
  else delete settings.secrets;
68278
68566
  updateWorkspaceSettings(settings);
68279
68567
  const updated = useStore.getState().workspace;
68280
- if (updated) await electron$c.saveWorkspace(updated);
68568
+ if (updated) await electron$e.saveWorkspace(updated);
68281
68569
  if (persistHistory) {
68282
- await electron$c.saveHistory(useStore.getState().history).catch(() => {
68570
+ await electron$e.saveHistory(useStore.getState().history).catch(() => {
68283
68571
  });
68284
68572
  }
68285
68573
  onClose();
@@ -68800,7 +69088,7 @@ function WorkspaceSettingsModal({ onClose }) {
68800
69088
  }
68801
69089
  );
68802
69090
  }
68803
- const { electron: electron$b } = window;
69091
+ const { electron: electron$d } = window;
68804
69092
  function DocsGeneratorModal({ onClose }) {
68805
69093
  const collections = useStore((s) => s.collections);
68806
69094
  const collectionList = Object.values(collections);
@@ -68840,9 +69128,9 @@ function DocsGeneratorModal({ onClose }) {
68840
69128
  setGenerating(true);
68841
69129
  setError(null);
68842
69130
  try {
68843
- const content2 = await electron$b.generateDocs(buildPayload());
69131
+ const content2 = await electron$d.generateDocs(buildPayload());
68844
69132
  const filename = format2 === "html" ? "api-docs.html" : "api-docs.md";
68845
- await electron$b.saveResults(content2, filename);
69133
+ await electron$d.saveResults(content2, filename);
68846
69134
  } catch (err) {
68847
69135
  setError(err instanceof Error ? err.message : String(err));
68848
69136
  } finally {
@@ -68853,7 +69141,7 @@ function DocsGeneratorModal({ onClose }) {
68853
69141
  setGenerating(true);
68854
69142
  setError(null);
68855
69143
  try {
68856
- const content2 = await electron$b.generateDocs(buildPayload());
69144
+ const content2 = await electron$d.generateDocs(buildPayload());
68857
69145
  setPreview(content2);
68858
69146
  } catch (err) {
68859
69147
  setError(err instanceof Error ? err.message : String(err));
@@ -69202,7 +69490,7 @@ function parseCurl(command2) {
69202
69490
  }
69203
69491
  return { name: name2, method: resolvedMethod, url, headers, params: [], auth, body };
69204
69492
  }
69205
- const { electron: electron$a } = window;
69493
+ const { electron: electron$c } = window;
69206
69494
  const OPTIONS = [
69207
69495
  { id: "postman", label: "Postman", description: "Collection v2.1 JSON" },
69208
69496
  { id: "openapi", label: "OpenAPI", description: "JSON or YAML (v3.x)", supportsUrl: true },
@@ -69214,16 +69502,16 @@ const OPTIONS = [
69214
69502
  ];
69215
69503
  function listEndpoints(col) {
69216
69504
  const result = [];
69217
- function walk(folder, path) {
69505
+ function walk2(folder, path) {
69218
69506
  for (const id2 of folder.requestIds) {
69219
69507
  const req = col.requests[id2];
69220
69508
  if (req) result.push({ request: req, folderPath: path });
69221
69509
  }
69222
69510
  for (const sub of folder.folders) {
69223
- walk(sub, [...path, sub.name]);
69511
+ walk2(sub, [...path, sub.name]);
69224
69512
  }
69225
69513
  }
69226
- walk(col.rootFolder, []);
69514
+ walk2(col.rootFolder, []);
69227
69515
  return result;
69228
69516
  }
69229
69517
  function ImportModal({ onImport, onClose }) {
@@ -69277,12 +69565,12 @@ function ImportModal({ onImport, onClose }) {
69277
69565
  setError(null);
69278
69566
  try {
69279
69567
  let col = null;
69280
- if (opt2.id === "postman") col = await electron$a.importPostman();
69281
- if (opt2.id === "openapi") col = await electron$a.importOpenApi();
69282
- if (opt2.id === "insomnia") col = await electron$a.importInsomnia();
69283
- if (opt2.id === "bruno") col = await electron$a.importBruno();
69284
- if (opt2.id === "http") col = await electron$a.importHttpFile();
69285
- if (opt2.id === "spector") col = await electron$a.importSpectorCollection();
69568
+ if (opt2.id === "postman") col = await electron$c.importPostman();
69569
+ if (opt2.id === "openapi") col = await electron$c.importOpenApi();
69570
+ if (opt2.id === "insomnia") col = await electron$c.importInsomnia();
69571
+ if (opt2.id === "bruno") col = await electron$c.importBruno();
69572
+ if (opt2.id === "http") col = await electron$c.importHttpFile();
69573
+ if (opt2.id === "spector") col = await electron$c.importSpectorCollection();
69286
69574
  if (!col) {
69287
69575
  setLoading(false);
69288
69576
  return;
@@ -69305,7 +69593,7 @@ function ImportModal({ onImport, onClose }) {
69305
69593
  setLoading(true);
69306
69594
  setError(null);
69307
69595
  try {
69308
- const col = await electron$a.importOpenApiFromUrl(trimmed);
69596
+ const col = await electron$c.importOpenApiFromUrl(trimmed);
69309
69597
  if (col) enterPreview(col);
69310
69598
  } catch (err) {
69311
69599
  setError(err instanceof Error ? err.message : String(err));
@@ -69412,7 +69700,7 @@ function ImportModal({ onImport, onClose }) {
69412
69700
  mergeIntoCollection(target, prunedRoot, prunedRequests);
69413
69701
  const entry = useStore.getState().collections[target];
69414
69702
  if (entry) {
69415
- await electron$a.saveCollection(entry.relPath, entry.data);
69703
+ await electron$c.saveCollection(entry.relPath, entry.data);
69416
69704
  markCollectionClean(target);
69417
69705
  }
69418
69706
  setActiveCollection(target);
@@ -69439,7 +69727,7 @@ function ImportModal({ onImport, onClose }) {
69439
69727
  variables: [{ key: name2, value, enabled: true }]
69440
69728
  };
69441
69729
  const relPath = envRelPath(finalName, envId);
69442
- await electron$a.saveEnvironment(relPath, env);
69730
+ await electron$c.saveEnvironment(relPath, env);
69443
69731
  useStore.setState((s) => {
69444
69732
  s.environments[envId] = { relPath, data: env };
69445
69733
  if (!s.activeEnvironmentId) s.activeEnvironmentId = envId;
@@ -69449,7 +69737,7 @@ function ImportModal({ onImport, onClose }) {
69449
69737
  return s;
69450
69738
  });
69451
69739
  const ws2 = useStore.getState().workspace;
69452
- if (ws2) await electron$a.saveWorkspace(ws2);
69740
+ if (ws2) await electron$c.saveWorkspace(ws2);
69453
69741
  } else {
69454
69742
  const entry = state.environments[envTarget];
69455
69743
  if (!entry) throw new Error("Target environment not found");
@@ -69464,7 +69752,7 @@ function ImportModal({ onImport, onClose }) {
69464
69752
  } else {
69465
69753
  updated.variables = [...updated.variables, { key: name2, value, enabled: true }];
69466
69754
  }
69467
- await electron$a.saveEnvironment(entry.relPath, updated);
69755
+ await electron$c.saveEnvironment(entry.relPath, updated);
69468
69756
  useStore.getState().updateEnvironment(envTarget, updated);
69469
69757
  }
69470
69758
  }
@@ -69831,16 +70119,16 @@ function pruneFolder(src, _col, keep, _out) {
69831
70119
  }
69832
70120
  function collectRequestsByFolder(folder, src) {
69833
70121
  const out = {};
69834
- function walk(f) {
70122
+ function walk2(f) {
69835
70123
  for (const id2 of f.requestIds) {
69836
70124
  if (src.requests[id2]) out[id2] = src.requests[id2];
69837
70125
  }
69838
- for (const sub of f.folders) walk(sub);
70126
+ for (const sub of f.folders) walk2(sub);
69839
70127
  }
69840
- walk(folder);
70128
+ walk2(folder);
69841
70129
  return out;
69842
70130
  }
69843
- const { electron: electron$9 } = window;
70131
+ const { electron: electron$b } = window;
69844
70132
  function Toolbar({ onOpenDocs: _onOpenDocs }) {
69845
70133
  const { applyWorkspace } = useWorkspaceLoader();
69846
70134
  const workspace = useStore((s) => s.workspace);
@@ -69863,13 +70151,13 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69863
70151
  try {
69864
70152
  for (const { relPath, data, dirty } of Object.values(collections)) {
69865
70153
  if (!dirty) continue;
69866
- await electron$9.saveCollection(relPath, data);
70154
+ await electron$b.saveCollection(relPath, data);
69867
70155
  markCollectionClean(data.id);
69868
70156
  }
69869
70157
  for (const { relPath, data } of Object.values(environments)) {
69870
- await electron$9.saveEnvironment(relPath, data);
70158
+ await electron$b.saveEnvironment(relPath, data);
69871
70159
  }
69872
- if (workspace) await electron$9.saveWorkspace(workspace);
70160
+ if (workspace) await electron$b.saveWorkspace(workspace);
69873
70161
  } finally {
69874
70162
  setSaving(false);
69875
70163
  }
@@ -69877,14 +70165,14 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69877
70165
  async function afterImport(col) {
69878
70166
  if (!col) return;
69879
70167
  const relPath = colRelPath(col.name, col.id);
69880
- await electron$9.saveCollection(relPath, col);
70168
+ await electron$b.saveCollection(relPath, col);
69881
70169
  loadCollection(relPath, col);
69882
70170
  setActiveCollection(col.id);
69883
70171
  const ws2 = useStore.getState().workspace;
69884
70172
  if (ws2 && !ws2.collections.includes(relPath)) {
69885
70173
  const updated = { ...ws2, collections: [...ws2.collections, relPath] };
69886
70174
  useStore.setState({ workspace: updated });
69887
- await electron$9.saveWorkspace(updated);
70175
+ await electron$b.saveWorkspace(updated);
69888
70176
  }
69889
70177
  }
69890
70178
  if (!workspace) return null;
@@ -69973,7 +70261,7 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69973
70261
  "button",
69974
70262
  {
69975
70263
  onClick: async () => {
69976
- const result = await electron$9.openWorkspace();
70264
+ const result = await electron$b.openWorkspace();
69977
70265
  if (result) await applyWorkspace(result.workspace, result.workspacePath);
69978
70266
  },
69979
70267
  className: "px-2.5 py-1 text-xs bg-surface-800 hover:bg-surface-700 rounded transition-colors",
@@ -69985,7 +70273,7 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69985
70273
  "button",
69986
70274
  {
69987
70275
  onClick: async () => {
69988
- const result = await electron$9.newWorkspace();
70276
+ const result = await electron$b.newWorkspace();
69989
70277
  if (result) await applyWorkspace(result.workspace, result.workspacePath);
69990
70278
  },
69991
70279
  className: "px-2.5 py-1 text-xs bg-surface-800 hover:bg-surface-700 rounded transition-colors",
@@ -69997,7 +70285,7 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69997
70285
  "button",
69998
70286
  {
69999
70287
  onClick: async () => {
70000
- await electron$9.closeWorkspace();
70288
+ await electron$b.closeWorkspace();
70001
70289
  closeWorkspace();
70002
70290
  },
70003
70291
  className: "px-2.5 py-1 text-xs bg-surface-800 hover:bg-surface-700 rounded transition-colors",
@@ -70448,7 +70736,7 @@ api-tests:
70448
70736
  function EmptyState({ message }) {
70449
70737
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-24 text-surface-400 text-xs", children: message });
70450
70738
  }
70451
- const { electron: electron$8 } = window;
70739
+ const { electron: electron$a } = window;
70452
70740
  const HOOK_BADGE = {
70453
70741
  beforeAll: { label: "BEFORE ALL", cls: "bg-violet-700 text-white" },
70454
70742
  before: { label: "BEFORE", cls: "bg-violet-600 text-white" },
@@ -70568,13 +70856,13 @@ function RunnerModal() {
70568
70856
  setSummary(null);
70569
70857
  setRunnerRunning(true);
70570
70858
  progressIdxRef.current = 0;
70571
- electron$8.onRunProgress((result) => {
70859
+ electron$a.onRunProgress((result) => {
70572
70860
  const idx = progressIdxRef.current;
70573
70861
  patchRunnerResult(idx, result);
70574
70862
  if (result.status !== "running") progressIdxRef.current++;
70575
70863
  });
70576
70864
  try {
70577
- const s = await electron$8.runCollection({
70865
+ const s = await electron$a.runCollection({
70578
70866
  items: items2,
70579
70867
  environment: env,
70580
70868
  globals,
@@ -70585,7 +70873,7 @@ function RunnerModal() {
70585
70873
  });
70586
70874
  setSummary(s);
70587
70875
  } finally {
70588
- electron$8.offRunProgress();
70876
+ electron$a.offRunProgress();
70589
70877
  setRunnerRunning(false);
70590
70878
  }
70591
70879
  }, [collectionId, folderId, filterTags, selectedEnvId, environments, globals, colEntry, requestDelay, workspaceSettings, setRunnerResults, patchRunnerResult, setRunnerRunning]);
@@ -70811,7 +71099,7 @@ function RunnerModal() {
70811
71099
  };
70812
71100
  const content2 = exportFormat === "junit" ? buildJUnitReport(runnerResults, summary, meta2) : exportFormat === "html" ? buildHtmlReport(runnerResults, summary, meta2) : buildJsonReport(runnerResults, summary, meta2);
70813
71101
  const ext = exportFormat === "junit" ? "xml" : exportFormat === "html" ? "html" : "json";
70814
- electron$8.saveResults(content2, `spector-results.${ext}`);
71102
+ electron$a.saveResults(content2, `spector-results.${ext}`);
70815
71103
  },
70816
71104
  className: "px-2.5 py-0.5 bg-surface-800 hover:bg-surface-700 rounded transition-colors text-[11px] whitespace-nowrap",
70817
71105
  children: "Export results"
@@ -70823,11 +71111,843 @@ function RunnerModal() {
70823
71111
  }
70824
71112
  );
70825
71113
  }
71114
+ const HTTP_METHODS$2 = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
71115
+ function normalizePath(url) {
71116
+ let u = (url || "").trim();
71117
+ u = u.split("#")[0].split("?")[0];
71118
+ u = u.replace(/\{\{[^}]*\}\}/g, "");
71119
+ u = u.replace(/^[a-z0-9+.-]+:\/\/[^/]*/i, "");
71120
+ u = u.replace(/\/{2,}/g, "/");
71121
+ if (!u.startsWith("/")) u = "/" + u;
71122
+ u = u.replace(/\/+$/, "");
71123
+ return u === "" ? "/" : u;
71124
+ }
71125
+ function segments(path) {
71126
+ return path.split("/").filter(Boolean);
71127
+ }
71128
+ function isParam(seg) {
71129
+ return seg.startsWith("{") && seg.endsWith("}");
71130
+ }
71131
+ function pathMatches(requestUrl, template) {
71132
+ const reqSegs = segments(normalizePath(requestUrl));
71133
+ const tplSegs = segments(template);
71134
+ if (reqSegs.length < tplSegs.length) return false;
71135
+ const tail = reqSegs.slice(reqSegs.length - tplSegs.length);
71136
+ return tplSegs.every((seg, i) => isParam(seg) ? tail[i] !== void 0 && tail[i] !== "" : tail[i] === seg);
71137
+ }
71138
+ function resolveRef$2(spec, ref2) {
71139
+ const parts = ref2.replace(/^#\//, "").split("/");
71140
+ return parts.reduce((o, k) => o?.[decodeURIComponent(k.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
71141
+ }
71142
+ function deref$2(spec, node, depth = 0, seen = /* @__PURE__ */ new Set()) {
71143
+ if (!node || typeof node !== "object" || depth > 8) return node;
71144
+ if (Array.isArray(node)) return node.map((n) => deref$2(spec, n, depth + 1, seen));
71145
+ if ("$ref" in node) {
71146
+ const target = resolveRef$2(spec, node.$ref);
71147
+ if (!target || seen.has(target)) return {};
71148
+ return deref$2(spec, target, depth + 1, /* @__PURE__ */ new Set([...seen, target]));
71149
+ }
71150
+ const out = {};
71151
+ for (const [k, v] of Object.entries(node)) out[k] = deref$2(spec, v, depth + 1, seen);
71152
+ return out;
71153
+ }
71154
+ function flattenSchemaPaths(schema, prefix2 = "", depth = 0) {
71155
+ if (!schema || typeof schema !== "object" || depth > 8) return [];
71156
+ const type2 = Array.isArray(schema.type) ? schema.type[0] : schema.type;
71157
+ const out = [];
71158
+ if (type2 === "object" || schema.properties) {
71159
+ for (const [name2, sub] of Object.entries(schema.properties ?? {})) {
71160
+ const path = prefix2 ? `${prefix2}.${name2}` : name2;
71161
+ out.push(path);
71162
+ out.push(...flattenSchemaPaths(sub, path, depth + 1));
71163
+ }
71164
+ } else if (type2 === "array" && schema.items) {
71165
+ out.push(...flattenSchemaPaths(schema.items, `${prefix2}[]`, depth + 1));
71166
+ }
71167
+ return out;
71168
+ }
71169
+ function flattenValuePaths(value, prefix2 = "", depth = 0) {
71170
+ if (value == null || typeof value !== "object" || depth > 8) return [];
71171
+ const out = [];
71172
+ if (Array.isArray(value)) {
71173
+ for (const el of value) out.push(...flattenValuePaths(el, `${prefix2}[]`, depth + 1));
71174
+ } else {
71175
+ for (const [k, v] of Object.entries(value)) {
71176
+ const path = prefix2 ? `${prefix2}.${k}` : k;
71177
+ out.push(path);
71178
+ out.push(...flattenValuePaths(v, path, depth + 1));
71179
+ }
71180
+ }
71181
+ return [...new Set(out)];
71182
+ }
71183
+ function successResponseSchema$1(spec, responses) {
71184
+ const code2 = Object.keys(responses ?? {}).filter((c) => /^2\d\d$/.test(c)).sort()[0];
71185
+ if (!code2) return void 0;
71186
+ const schema = responses[code2]?.content?.["application/json"]?.schema ?? responses[code2]?.content?.["application/json;charset=utf-8"]?.schema;
71187
+ return schema ? deref$2(spec, schema) : void 0;
71188
+ }
71189
+ function enumerateOperations(spec) {
71190
+ const doc2 = spec ?? {};
71191
+ const ops2 = [];
71192
+ for (const [path, item] of Object.entries(doc2.paths ?? {})) {
71193
+ if (!item || typeof item !== "object") continue;
71194
+ for (const [method, op] of Object.entries(item)) {
71195
+ if (!HTTP_METHODS$2.includes(method.toLowerCase())) continue;
71196
+ if (!op || typeof op !== "object") continue;
71197
+ const declaredStatuses = Object.keys(op.responses ?? {}).filter((c) => /^\d{3}$/.test(c));
71198
+ ops2.push({ method: method.toUpperCase(), path, operationId: op.operationId, declaredStatuses, responses: op.responses });
71199
+ }
71200
+ }
71201
+ return ops2;
71202
+ }
71203
+ function round(n) {
71204
+ return Math.round(n * 10) / 10;
71205
+ }
71206
+ function computeCoverage(spec, requests, observations = []) {
71207
+ const doc2 = spec ?? {};
71208
+ const operations2 = enumerateOperations(spec).map((op) => {
71209
+ const mapped = requests.filter((r) => r.method.toUpperCase() === op.method && pathMatches(r.url, op.path));
71210
+ const obs = observations.filter((o) => o.method.toUpperCase() === op.method && pathMatches(o.url, op.path));
71211
+ const asserted = mapped.map((r) => r.expectedStatus).filter((s) => typeof s === "number");
71212
+ const seen = obs.map((o) => o.status);
71213
+ const covered = /* @__PURE__ */ new Set([...asserted, ...seen]);
71214
+ const coveredStatuses2 = op.declaredStatuses.filter((code2) => covered.has(Number(code2)));
71215
+ const hasNegativeTest = [...covered].some((s) => s >= 400);
71216
+ const declaredProperties2 = flattenSchemaPaths(successResponseSchema$1(doc2, op.responses ?? {}));
71217
+ const observedPaths = new Set(obs.flatMap((o) => o.responsePaths ?? []));
71218
+ const coveredProperties2 = declaredProperties2.filter((p2) => observedPaths.has(p2));
71219
+ return {
71220
+ method: op.method,
71221
+ path: op.path,
71222
+ operationId: op.operationId,
71223
+ tested: mapped.length > 0 || obs.length > 0,
71224
+ requests: mapped.map((r) => r.name),
71225
+ declaredStatuses: op.declaredStatuses,
71226
+ coveredStatuses: coveredStatuses2,
71227
+ hasNegativeTest,
71228
+ declaredProperties: declaredProperties2,
71229
+ coveredProperties: coveredProperties2
71230
+ };
71231
+ });
71232
+ const tested = operations2.filter((o) => o.tested).length;
71233
+ const declaredStatuses = operations2.reduce((n, o) => n + o.declaredStatuses.length, 0);
71234
+ const coveredStatuses = operations2.reduce((n, o) => n + o.coveredStatuses.length, 0);
71235
+ const declaredProperties = operations2.reduce((n, o) => n + o.declaredProperties.length, 0);
71236
+ const coveredProperties = operations2.reduce((n, o) => n + o.coveredProperties.length, 0);
71237
+ const withoutNegativeTest = operations2.filter((o) => o.tested && !o.hasNegativeTest).length;
71238
+ return {
71239
+ spec: { title: doc2.info?.title, version: doc2.info?.version },
71240
+ totals: {
71241
+ operations: operations2.length,
71242
+ tested,
71243
+ untested: operations2.length - tested,
71244
+ operationPct: operations2.length ? round(tested / operations2.length * 100) : 0,
71245
+ declaredStatuses,
71246
+ coveredStatuses,
71247
+ statusPct: declaredStatuses ? round(coveredStatuses / declaredStatuses * 100) : 0,
71248
+ withoutNegativeTest,
71249
+ declaredProperties,
71250
+ coveredProperties,
71251
+ propertyPct: declaredProperties ? round(coveredProperties / declaredProperties * 100) : 0
71252
+ },
71253
+ operations: operations2
71254
+ };
71255
+ }
71256
+ const HTTP_METHODS$1 = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
71257
+ function resolveRef$1(spec, ref2) {
71258
+ const parts = ref2.replace(/^#\//, "").split("/");
71259
+ return parts.reduce((o, k) => o?.[decodeURIComponent(k.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
71260
+ }
71261
+ function deref$1(spec, node, depth = 0, seen = /* @__PURE__ */ new Set()) {
71262
+ if (!node || typeof node !== "object" || depth > 8) return node;
71263
+ if (Array.isArray(node)) return node.map((n) => deref$1(spec, n, depth + 1, seen));
71264
+ if ("$ref" in node) {
71265
+ const target = resolveRef$1(spec, node.$ref);
71266
+ if (!target || seen.has(target)) return {};
71267
+ return deref$1(spec, target, depth + 1, /* @__PURE__ */ new Set([...seen, target]));
71268
+ }
71269
+ const out = {};
71270
+ for (const [k, v] of Object.entries(node)) out[k] = deref$1(spec, v, depth + 1, seen);
71271
+ return out;
71272
+ }
71273
+ function jsonSchemaFor(spec, responses, code2) {
71274
+ const resp = responses?.[code2];
71275
+ const schema = resp?.content?.["application/json"]?.schema ?? resp?.content?.["application/json;charset=utf-8"]?.schema;
71276
+ return schema ? deref$1(spec, schema) : void 0;
71277
+ }
71278
+ function sampleValue(schema) {
71279
+ if (!schema || typeof schema !== "object") return "string";
71280
+ if (schema.example !== void 0) return schema.example;
71281
+ if (schema.default !== void 0) return schema.default;
71282
+ if (Array.isArray(schema.enum) && schema.enum.length) return schema.enum[0];
71283
+ const type2 = Array.isArray(schema.type) ? schema.type[0] : schema.type;
71284
+ switch (type2) {
71285
+ case "integer":
71286
+ case "number": {
71287
+ const min = schema.minimum ?? (schema.exclusiveMinimum != null ? schema.exclusiveMinimum + 1 : void 0);
71288
+ const max = schema.maximum ?? (schema.exclusiveMaximum != null ? schema.exclusiveMaximum - 1 : void 0);
71289
+ if (min != null) return min;
71290
+ if (max != null) return max;
71291
+ return type2 === "integer" ? 1 : 1.5;
71292
+ }
71293
+ case "boolean":
71294
+ return true;
71295
+ case "array":
71296
+ return [sampleValue(schema.items ?? {})];
71297
+ case "object": {
71298
+ const out = {};
71299
+ const props = schema.properties ?? {};
71300
+ const required2 = schema.required ?? [];
71301
+ for (const [name2, propSchema] of Object.entries(props)) out[name2] = sampleValue(propSchema);
71302
+ for (const name2 of required2) if (!(name2 in out)) out[name2] = "string";
71303
+ return out;
71304
+ }
71305
+ case "string":
71306
+ default:
71307
+ return sampleString(schema);
71308
+ }
71309
+ }
71310
+ function sampleString(schema) {
71311
+ switch (schema.format) {
71312
+ case "email":
71313
+ return "user@example.com";
71314
+ case "uuid":
71315
+ return "00000000-0000-0000-0000-000000000000";
71316
+ case "date":
71317
+ return "2020-01-01";
71318
+ case "date-time":
71319
+ return "2020-01-01T00:00:00Z";
71320
+ case "uri":
71321
+ case "url":
71322
+ return "https://example.com";
71323
+ case "hostname":
71324
+ return "example.com";
71325
+ case "ipv4":
71326
+ return "127.0.0.1";
71327
+ default: {
71328
+ const min = schema.minLength ?? 0;
71329
+ let s = "string";
71330
+ if (min > s.length) s = s.padEnd(min, "x");
71331
+ if (schema.maxLength != null && s.length > schema.maxLength) s = s.slice(0, schema.maxLength);
71332
+ return s;
71333
+ }
71334
+ }
71335
+ }
71336
+ function collectParams(spec, pathItem, op) {
71337
+ const raw = [...pathItem?.parameters ?? [], ...op?.parameters ?? []].map((p2) => deref$1(spec, p2));
71338
+ const byKey = /* @__PURE__ */ new Map();
71339
+ for (const p2 of raw) if (p2?.name && p2?.in) byKey.set(`${p2.in}:${p2.name}`, p2);
71340
+ return [...byKey.values()];
71341
+ }
71342
+ function successCode(responses) {
71343
+ const codes = Object.keys(responses ?? {}).filter((c) => /^2\d\d$/.test(c)).map(Number).sort((a, b) => a - b);
71344
+ return codes[0] ?? 200;
71345
+ }
71346
+ function negativeCode(responses) {
71347
+ const codes = Object.keys(responses ?? {}).filter((c) => /^4\d\d$/.test(c)).map(Number).sort((a, b) => a - b);
71348
+ return codes[0] ?? 400;
71349
+ }
71350
+ function baseTest(method, path, op, params) {
71351
+ const pathParams = {};
71352
+ for (const p2 of params.filter((p22) => p22.in === "path")) pathParams[p2.name] = sampleValue(p2.schema ?? {});
71353
+ const query = params.filter((p2) => p2.in === "query" && p2.required).map((p2) => ({ key: p2.name, value: String(sampleValue(p2.schema ?? {})) }));
71354
+ const headers = params.filter((p2) => p2.in === "header" && p2.required).map((p2) => ({ key: p2.name, value: String(sampleValue(p2.schema ?? {})) }));
71355
+ return { operationId: op?.operationId, method, path, pathParams, query, headers };
71356
+ }
71357
+ function requestBodySchema(spec, op) {
71358
+ const rb = deref$1(spec, op?.requestBody);
71359
+ const schema = rb?.content?.["application/json"]?.schema;
71360
+ return schema;
71361
+ }
71362
+ function generateForOperation(spec, method, path, pathItem, op, opts) {
71363
+ const tests = [];
71364
+ const params = collectParams(spec, pathItem, op);
71365
+ const responses = op?.responses ?? {};
71366
+ const okCode = successCode(responses);
71367
+ const badCode = negativeCode(responses);
71368
+ const bodySchema = requestBodySchema(spec, op);
71369
+ const validBody = bodySchema ? sampleValue(bodySchema) : void 0;
71370
+ const base2 = baseTest(method, path, op, params);
71371
+ const label = `${method} ${path}`;
71372
+ tests.push({
71373
+ ...base2,
71374
+ name: `${label} - happy path`,
71375
+ category: "happy",
71376
+ body: validBody !== void 0 ? JSON.stringify(validBody, null, 2) : void 0,
71377
+ expectedStatus: okCode,
71378
+ responseSchema: jsonSchemaFor(spec, responses, String(okCode)) ? JSON.stringify(jsonSchemaFor(spec, responses, String(okCode)), null, 2) : void 0
71379
+ });
71380
+ const props = bodySchema?.properties ?? {};
71381
+ const required2 = bodySchema?.required ?? [];
71382
+ if (opts.includeNegative && validBody && typeof validBody === "object") {
71383
+ let n = 0;
71384
+ for (const field of required2) {
71385
+ if (n >= opts.maxNegativePerOp) break;
71386
+ const mutated = { ...validBody };
71387
+ delete mutated[field];
71388
+ tests.push({ ...base2, name: `${label} - missing ${field}`, category: "negative", body: JSON.stringify(mutated, null, 2), expectedStatus: badCode });
71389
+ n++;
71390
+ }
71391
+ for (const [field, ps] of Object.entries(props)) {
71392
+ if (n >= opts.maxNegativePerOp) break;
71393
+ const t2 = Array.isArray(ps.type) ? ps.type[0] : ps.type;
71394
+ if (t2 !== "string" && t2 !== "integer" && t2 !== "number" && t2 !== "boolean") continue;
71395
+ const wrong = t2 === "string" ? 12345 : "not-a-valid-value";
71396
+ tests.push({ ...base2, name: `${label} - ${field} wrong type`, category: "negative", body: JSON.stringify({ ...validBody, [field]: wrong }, null, 2), expectedStatus: badCode });
71397
+ n++;
71398
+ }
71399
+ }
71400
+ if (opts.includeBoundary && validBody && typeof validBody === "object") {
71401
+ let n = 0;
71402
+ for (const [field, ps] of Object.entries(props)) {
71403
+ if (n >= opts.maxBoundaryPerOp) break;
71404
+ const t2 = Array.isArray(ps.type) ? ps.type[0] : ps.type;
71405
+ if ((t2 === "integer" || t2 === "number") && ps.minimum != null) {
71406
+ tests.push({ ...base2, name: `${label} - ${field} below minimum`, category: "boundary", body: JSON.stringify({ ...validBody, [field]: ps.minimum - 1 }, null, 2), expectedStatus: badCode });
71407
+ n++;
71408
+ } else if ((t2 === "integer" || t2 === "number") && ps.maximum != null) {
71409
+ tests.push({ ...base2, name: `${label} - ${field} above maximum`, category: "boundary", body: JSON.stringify({ ...validBody, [field]: ps.maximum + 1 }, null, 2), expectedStatus: badCode });
71410
+ n++;
71411
+ } else if (t2 === "string" && ps.maxLength != null) {
71412
+ tests.push({ ...base2, name: `${label} - ${field} too long`, category: "boundary", body: JSON.stringify({ ...validBody, [field]: "x".repeat(ps.maxLength + 1) }, null, 2), expectedStatus: badCode });
71413
+ n++;
71414
+ }
71415
+ }
71416
+ }
71417
+ return tests;
71418
+ }
71419
+ function generateTests(spec, options = {}) {
71420
+ const opts = {
71421
+ only: options.only ?? /* @__PURE__ */ new Set(),
71422
+ includeNegative: options.includeNegative ?? true,
71423
+ includeBoundary: options.includeBoundary ?? true,
71424
+ maxNegativePerOp: options.maxNegativePerOp ?? 4,
71425
+ maxBoundaryPerOp: options.maxBoundaryPerOp ?? 4
71426
+ };
71427
+ const doc2 = spec ?? {};
71428
+ const out = [];
71429
+ for (const [path, item] of Object.entries(doc2.paths ?? {})) {
71430
+ if (!item || typeof item !== "object") continue;
71431
+ for (const [method, op] of Object.entries(item)) {
71432
+ if (!HTTP_METHODS$1.includes(method.toLowerCase())) continue;
71433
+ if (!op || typeof op !== "object") continue;
71434
+ if (opts.only.size && !opts.only.has(`${method.toUpperCase()} ${path}`)) continue;
71435
+ out.push(...generateForOperation(doc2, method.toUpperCase(), path, item, op, opts));
71436
+ }
71437
+ }
71438
+ return out;
71439
+ }
71440
+ function testUrl(test, baseVar = "{{baseUrl}}") {
71441
+ let path = test.path;
71442
+ for (const [name2, value] of Object.entries(test.pathParams)) {
71443
+ path = path.replace(new RegExp(`\\{${name2}\\}`, "g"), encodeURIComponent(String(value)));
71444
+ }
71445
+ const qs = test.query.filter((q) => q.key).map((q) => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value)}`).join("&");
71446
+ return `${baseVar}${path}${qs ? `?${qs}` : ""}`;
71447
+ }
71448
+ function toApiRequest(test, id2) {
71449
+ const headers = test.headers.map((h) => ({ key: h.key, value: h.value, enabled: true }));
71450
+ return {
71451
+ id: id2,
71452
+ name: test.name,
71453
+ method: test.method,
71454
+ url: testUrl(test),
71455
+ headers,
71456
+ params: [],
71457
+ auth: { type: "none" },
71458
+ body: test.body !== void 0 ? { mode: "json", json: test.body } : { mode: "none" },
71459
+ contract: {
71460
+ statusCode: test.expectedStatus,
71461
+ ...test.responseSchema ? { bodySchema: test.responseSchema } : {}
71462
+ },
71463
+ meta: { tags: [test.category] }
71464
+ };
71465
+ }
71466
+ const { electron: electron$9 } = window;
71467
+ function Bar({ pct }) {
71468
+ const color = pct >= 80 ? "bg-emerald-500" : pct >= 50 ? "bg-amber-500" : "bg-red-500";
71469
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-2 w-full rounded bg-surface-800 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `h-full ${color}`, style: { width: `${pct}%` } }) });
71470
+ }
71471
+ function Stat({ value, label }) {
71472
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
71473
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-2xl font-bold text-white", children: value }),
71474
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] text-surface-400", children: label })
71475
+ ] });
71476
+ }
71477
+ function CoverageModal() {
71478
+ const open = useStore((s) => s.coverageOpen);
71479
+ const setOpen = useStore((s) => s.setCoverageOpen);
71480
+ const collections = useStore((s) => s.collections);
71481
+ const history2 = useStore((s) => s.history);
71482
+ const savedSpec = useStore((s) => s.workspace?.settings?.coverageSpec);
71483
+ const setCoverageSpec = useStore((s) => s.setCoverageSpec);
71484
+ const addCollectionObject = useStore((s) => s.addCollectionObject);
71485
+ const [source, setSource] = reactExports.useState(savedSpec ?? "");
71486
+ const [pasted, setPasted] = reactExports.useState("");
71487
+ const [report, setReport] = reactExports.useState(null);
71488
+ const [spec, setSpec] = reactExports.useState(null);
71489
+ const [error2, setError] = reactExports.useState(null);
71490
+ const [busy, setBusy] = reactExports.useState(false);
71491
+ const [onlyGaps, setOnlyGaps] = reactExports.useState(false);
71492
+ const [generated, setGenerated] = reactExports.useState(null);
71493
+ const requests = reactExports.useMemo(() => {
71494
+ const out = [];
71495
+ for (const entry of Object.values(collections)) {
71496
+ for (const req of Object.values(entry.data.requests)) {
71497
+ if (req.disabled) continue;
71498
+ out.push({ name: `${entry.data.name} / ${req.name}`, method: req.method, url: req.url, expectedStatus: req.contract?.statusCode });
71499
+ }
71500
+ }
71501
+ return out;
71502
+ }, [collections]);
71503
+ const observations = reactExports.useMemo(() => {
71504
+ return history2.map((h) => {
71505
+ let responsePaths;
71506
+ try {
71507
+ if (h.response?.body) responsePaths = flattenValuePaths(JSON.parse(h.response.body));
71508
+ } catch {
71509
+ }
71510
+ return { method: h.request.method, url: h.resolvedUrl, status: h.response?.status ?? 0, responsePaths };
71511
+ });
71512
+ }, [history2]);
71513
+ async function compute() {
71514
+ setBusy(true);
71515
+ setError(null);
71516
+ try {
71517
+ const isUrl = /^https?:\/\//i.test(source.trim());
71518
+ const spec2 = await electron$9.coverageLoadSpec(
71519
+ pasted.trim() ? { text: pasted } : isUrl ? { url: source.trim() } : { path: source.trim() }
71520
+ );
71521
+ if (source.trim() && !pasted.trim()) setCoverageSpec(source.trim());
71522
+ setSpec(spec2);
71523
+ setReport(computeCoverage(spec2, requests, observations));
71524
+ setGenerated(null);
71525
+ } catch (err) {
71526
+ setError(err instanceof Error ? err.message : String(err));
71527
+ setReport(null);
71528
+ } finally {
71529
+ setBusy(false);
71530
+ }
71531
+ }
71532
+ function generateForGaps() {
71533
+ if (!spec || !report) return;
71534
+ const only = new Set(report.operations.filter((o) => !o.tested).map((o) => `${o.method} ${o.path}`));
71535
+ if (only.size === 0) return;
71536
+ const tests = generateTests(spec, { only });
71537
+ const requestIds = [];
71538
+ const requestMap = {};
71539
+ for (const t22 of tests) {
71540
+ const id2 = crypto.randomUUID();
71541
+ requestMap[id2] = toApiRequest(t22, id2);
71542
+ requestIds.push(id2);
71543
+ }
71544
+ const name2 = `${report.spec.title ?? "API"} tests (generated)`;
71545
+ const collection = {
71546
+ version: "1.0",
71547
+ id: crypto.randomUUID(),
71548
+ name: name2,
71549
+ description: "Generated for untested operations from the OpenAPI spec.",
71550
+ rootFolder: { id: crypto.randomUUID(), name: "root", description: "", folders: [], requestIds },
71551
+ requests: requestMap
71552
+ };
71553
+ addCollectionObject(collection);
71554
+ setGenerated(`Added ${tests.length} tests for ${only.size} untested operations as "${collection.name}".`);
71555
+ }
71556
+ if (!open) return null;
71557
+ const t2 = report?.totals;
71558
+ const shownOps = report?.operations.filter((o) => !onlyGaps || !o.tested || !o.hasNegativeTest) ?? [];
71559
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
71560
+ Modal,
71561
+ {
71562
+ onClose: () => setOpen(false),
71563
+ overlayClassName: "bg-black/50 z-50 flex items-start justify-center pt-16",
71564
+ panelClassName: "bg-surface-900 border border-surface-800 rounded-lg shadow-2xl flex flex-col w-[760px] max-h-[82vh]",
71565
+ title: "API test coverage",
71566
+ subtitle: "How much of an OpenAPI contract this workspace tests",
71567
+ children: [
71568
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 border-b border-surface-800 flex flex-col gap-2 flex-shrink-0", children: [
71569
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-surface-400", children: "OpenAPI spec (file path or URL)" }),
71570
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
71571
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71572
+ "input",
71573
+ {
71574
+ value: source,
71575
+ onChange: (e) => setSource(e.target.value),
71576
+ placeholder: "./openapi.yaml or https://api.example.com/openapi.json",
71577
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:border-blue-500"
71578
+ }
71579
+ ),
71580
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71581
+ "button",
71582
+ {
71583
+ onClick: compute,
71584
+ disabled: busy || !source.trim() && !pasted.trim(),
71585
+ className: "px-4 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-surface-800 disabled:text-surface-500 rounded text-sm font-medium",
71586
+ children: busy ? "…" : "Measure"
71587
+ }
71588
+ )
71589
+ ] }),
71590
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("details", { className: "text-[11px] text-surface-500", children: [
71591
+ /* @__PURE__ */ jsxRuntimeExports.jsx("summary", { className: "cursor-pointer hover:text-surface-300", children: "or paste spec" }),
71592
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71593
+ "textarea",
71594
+ {
71595
+ value: pasted,
71596
+ onChange: (e) => setPasted(e.target.value),
71597
+ rows: 4,
71598
+ placeholder: "Paste OpenAPI JSON or YAML here",
71599
+ className: "mt-1 w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-blue-500"
71600
+ }
71601
+ )
71602
+ ] }),
71603
+ error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-red-400", children: error2 })
71604
+ ] }),
71605
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3 flex-1 overflow-y-auto min-h-0", children: !report ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm text-surface-500 text-center py-10", children: [
71606
+ "Point at your OpenAPI spec and choose ",
71607
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-300", children: "Measure" }),
71608
+ " to see which operations are tested."
71609
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
71610
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3", children: [
71611
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [
71612
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-300", children: [
71613
+ report.spec.title ?? "API",
71614
+ report.spec.version ? ` v${report.spec.version}` : ""
71615
+ ] }),
71616
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-400", children: [
71617
+ t2.operationPct,
71618
+ "% operations tested"
71619
+ ] })
71620
+ ] }),
71621
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Bar, { pct: t2.operationPct })
71622
+ ] }),
71623
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4 mb-4 border-b border-surface-800 pb-3", children: [
71624
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: `${t2.tested}/${t2.operations}`, label: "operations tested" }),
71625
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: `${t2.coveredStatuses}/${t2.declaredStatuses}`, label: "response codes covered" }),
71626
+ t2.declaredProperties > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: `${t2.propertyPct}%`, label: "response shape seen in runs" }),
71627
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: t2.untested ? "text-amber-400" : "text-emerald-400", children: t2.untested }), label: "never tested" }),
71628
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: t2.withoutNegativeTest ? "text-amber-400" : "text-emerald-400", children: t2.withoutNegativeTest }), label: "no negative test" })
71629
+ ] }),
71630
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-2 gap-3", children: [
71631
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 text-xs text-surface-400", children: [
71632
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: onlyGaps, onChange: (e) => setOnlyGaps(e.target.checked) }),
71633
+ "Show only gaps (untested or missing a negative test)"
71634
+ ] }),
71635
+ t2.untested > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
71636
+ "button",
71637
+ {
71638
+ onClick: generateForGaps,
71639
+ title: "Generate happy-path, negative, and boundary tests for the untested operations",
71640
+ className: "px-3 py-1 text-xs bg-violet-600 hover:bg-violet-500 rounded font-medium shrink-0",
71641
+ children: [
71642
+ "Generate tests for ",
71643
+ t2.untested,
71644
+ " gap",
71645
+ t2.untested !== 1 ? "s" : ""
71646
+ ]
71647
+ }
71648
+ )
71649
+ ] }),
71650
+ generated && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-2 px-3 py-1.5 rounded bg-emerald-900/20 border border-emerald-800/40 text-xs text-emerald-300", children: [
71651
+ generated,
71652
+ " Re-measure to see the coverage rise."
71653
+ ] }),
71654
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-0.5", children: shownOps.map((op, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-xs py-1 border-b border-surface-800/50", children: [
71655
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: op.tested ? "text-emerald-400" : "text-red-400", children: op.tested ? "✓" : "✗" }),
71656
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-surface-300 w-14 shrink-0", children: op.method }),
71657
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `font-mono flex-1 ${op.tested ? "text-white" : "text-surface-500"}`, children: op.path }),
71658
+ op.declaredStatuses.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-500 shrink-0", children: [
71659
+ op.coveredStatuses.length,
71660
+ "/",
71661
+ op.declaredStatuses.length,
71662
+ " codes"
71663
+ ] }),
71664
+ op.tested && !op.hasNegativeTest && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-amber-400 shrink-0", children: "no negative" })
71665
+ ] }, `${op.method}-${op.path}-${i}`)) })
71666
+ ] }) })
71667
+ ]
71668
+ }
71669
+ );
71670
+ }
71671
+ const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
71672
+ function resolveRef(spec, ref2) {
71673
+ const parts = ref2.replace(/^#\//, "").split("/");
71674
+ return parts.reduce((o, k) => o?.[decodeURIComponent(k.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
71675
+ }
71676
+ function deref(spec, node, depth = 0, seen = /* @__PURE__ */ new Set()) {
71677
+ if (!node || typeof node !== "object" || depth > 8) return node;
71678
+ if (Array.isArray(node)) return node.map((n) => deref(spec, n, depth + 1, seen));
71679
+ if ("$ref" in node) {
71680
+ const target = resolveRef(spec, node.$ref);
71681
+ if (!target || seen.has(target)) return {};
71682
+ return deref(spec, target, depth + 1, /* @__PURE__ */ new Set([...seen, target]));
71683
+ }
71684
+ const out = {};
71685
+ for (const [k, v] of Object.entries(node)) out[k] = deref(spec, v, depth + 1, seen);
71686
+ return out;
71687
+ }
71688
+ function walk(schema, prefix2, depth, types2, required2) {
71689
+ if (!schema || typeof schema !== "object" || depth > 8) return;
71690
+ const type2 = Array.isArray(schema.type) ? schema.type[0] : schema.type;
71691
+ if (type2 === "object" || schema.properties) {
71692
+ const req = schema.required ?? [];
71693
+ for (const [name2, sub] of Object.entries(schema.properties ?? {})) {
71694
+ const p2 = prefix2 ? `${prefix2}.${name2}` : name2;
71695
+ const subType = (Array.isArray(sub?.type) ? sub.type[0] : sub?.type) ?? (sub?.properties ? "object" : "any");
71696
+ types2.set(p2, subType);
71697
+ if (req.includes(name2)) required2.add(p2);
71698
+ walk(sub, p2, depth + 1, types2, required2);
71699
+ }
71700
+ } else if (type2 === "array" && schema.items) {
71701
+ walk(schema.items, `${prefix2}[]`, depth + 1, types2, required2);
71702
+ }
71703
+ }
71704
+ function schemaInfo(spec, schema) {
71705
+ const types2 = /* @__PURE__ */ new Map();
71706
+ const required2 = /* @__PURE__ */ new Set();
71707
+ if (schema) walk(deref(spec, schema), "", 0, types2, required2);
71708
+ return { types: types2, required: required2 };
71709
+ }
71710
+ function requestSchema(spec, op) {
71711
+ return deref(spec, op?.requestBody)?.content?.["application/json"]?.schema;
71712
+ }
71713
+ function successResponseSchema(spec, op) {
71714
+ const responses = op?.responses ?? {};
71715
+ const code2 = Object.keys(responses).filter((c) => /^2\d\d$/.test(c)).sort()[0];
71716
+ return code2 ? deref(spec, responses[code2])?.content?.["application/json"]?.schema : void 0;
71717
+ }
71718
+ function paramRequired(spec, pathItem, op) {
71719
+ const raw = [...pathItem?.parameters ?? [], ...op?.parameters ?? []].map((p2) => deref(spec, p2));
71720
+ const m = /* @__PURE__ */ new Map();
71721
+ for (const p2 of raw) if (p2?.name && p2?.in) m.set(`${p2.in}:${p2.name}`, !!p2.required);
71722
+ return m;
71723
+ }
71724
+ function operations(spec) {
71725
+ const map = /* @__PURE__ */ new Map();
71726
+ for (const [path, item] of Object.entries(spec?.paths ?? {})) {
71727
+ if (!item || typeof item !== "object") continue;
71728
+ for (const [method, op] of Object.entries(item)) {
71729
+ if (!HTTP_METHODS.includes(method.toLowerCase())) continue;
71730
+ map.set(`${method.toUpperCase()} ${path}`, { method: method.toUpperCase(), path, pathItem: item, op });
71731
+ }
71732
+ }
71733
+ return map;
71734
+ }
71735
+ function diffSpecs(oldSpec, newSpec) {
71736
+ const oldOps = operations(oldSpec);
71737
+ const newOps = operations(newSpec);
71738
+ const changes = [];
71739
+ for (const [key, o] of oldOps) {
71740
+ if (!newOps.has(key)) {
71741
+ changes.push({ kind: "operation-removed", breaking: true, method: o.method, path: o.path, detail: `Operation ${key} was removed` });
71742
+ }
71743
+ }
71744
+ for (const [key, n] of newOps) {
71745
+ if (!oldOps.has(key)) {
71746
+ changes.push({ kind: "operation-added", breaking: false, method: n.method, path: n.path, detail: `Operation ${key} was added` });
71747
+ continue;
71748
+ }
71749
+ const o = oldOps.get(key);
71750
+ const label = `${n.method} ${n.path}`;
71751
+ const oReq = schemaInfo(oldSpec, requestSchema(oldSpec, o.op));
71752
+ const nReq = schemaInfo(newSpec, requestSchema(newSpec, n.op));
71753
+ for (const p2 of nReq.required) {
71754
+ if (!oReq.required.has(p2)) {
71755
+ changes.push({ kind: "request-required-added", breaking: true, method: n.method, path: n.path, detail: `${label}: request field "${p2}" is now required` });
71756
+ }
71757
+ }
71758
+ for (const [p2, t2] of nReq.types) {
71759
+ const ot = oReq.types.get(p2);
71760
+ if (ot && ot !== t2) {
71761
+ changes.push({ kind: "request-type-changed", breaking: true, method: n.method, path: n.path, detail: `${label}: request field "${p2}" type ${ot} -> ${t2}` });
71762
+ }
71763
+ }
71764
+ const oRes = schemaInfo(oldSpec, successResponseSchema(oldSpec, o.op));
71765
+ const nRes = schemaInfo(newSpec, successResponseSchema(newSpec, n.op));
71766
+ for (const [p2, t2] of oRes.types) {
71767
+ if (!nRes.types.has(p2)) {
71768
+ changes.push({ kind: "response-removed", breaking: true, method: n.method, path: n.path, detail: `${label}: response field "${p2}" was removed` });
71769
+ } else if (nRes.types.get(p2) !== t2) {
71770
+ changes.push({ kind: "response-type-changed", breaking: true, method: n.method, path: n.path, detail: `${label}: response field "${p2}" type ${t2} -> ${nRes.types.get(p2)}` });
71771
+ }
71772
+ }
71773
+ const oParams = paramRequired(oldSpec, o.pathItem, o.op);
71774
+ const nParams = paramRequired(newSpec, n.pathItem, n.op);
71775
+ for (const [k, req] of nParams) {
71776
+ if (req && !oParams.get(k)) {
71777
+ changes.push({ kind: "param-required-added", breaking: true, method: n.method, path: n.path, detail: `${label}: parameter "${k}" is now required` });
71778
+ }
71779
+ }
71780
+ const oCodes = Object.keys(o.op?.responses ?? {}).filter((c) => /^2\d\d$/.test(c));
71781
+ const nCodes = new Set(Object.keys(n.op?.responses ?? {}));
71782
+ for (const c of oCodes) {
71783
+ if (!nCodes.has(c)) {
71784
+ changes.push({ kind: "success-code-removed", breaking: true, method: n.method, path: n.path, detail: `${label}: success response ${c} was removed` });
71785
+ }
71786
+ }
71787
+ }
71788
+ return changes;
71789
+ }
71790
+ function summarizeDiff(changes) {
71791
+ return {
71792
+ breaking: changes.filter((c) => c.breaking).length,
71793
+ nonBreaking: changes.filter((c) => !c.breaking).length
71794
+ };
71795
+ }
71796
+ const { electron: electron$8 } = window;
71797
+ function SpecField({ label, value, onSource, onText }) {
71798
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
71799
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-surface-400", children: label }),
71800
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71801
+ "input",
71802
+ {
71803
+ value: value.source,
71804
+ onChange: (e) => onSource(e.target.value),
71805
+ placeholder: "./openapi.yaml or URL",
71806
+ className: "mt-1 w-full bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-blue-500"
71807
+ }
71808
+ ),
71809
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("details", { className: "text-[10px] text-surface-500 mt-1", children: [
71810
+ /* @__PURE__ */ jsxRuntimeExports.jsx("summary", { className: "cursor-pointer hover:text-surface-300", children: "or paste" }),
71811
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71812
+ "textarea",
71813
+ {
71814
+ value: value.text,
71815
+ onChange: (e) => onText(e.target.value),
71816
+ rows: 3,
71817
+ className: "mt-1 w-full resize-y bg-surface-950 border border-surface-800 rounded px-2 py-1.5 text-[11px] font-mono focus:outline-none focus:border-blue-500"
71818
+ }
71819
+ )
71820
+ ] })
71821
+ ] });
71822
+ }
71823
+ function CompareModal() {
71824
+ const open = useStore((s) => s.compareOpen);
71825
+ const setOpen = useStore((s) => s.setCompareOpen);
71826
+ const collections = useStore((s) => s.collections);
71827
+ const savedSpec = useStore((s) => s.workspace?.settings?.coverageSpec);
71828
+ const [oldIn, setOldIn] = reactExports.useState({ source: "", text: "" });
71829
+ const [newIn, setNewIn] = reactExports.useState({ source: savedSpec ?? "", text: "" });
71830
+ const [changes, setChanges] = reactExports.useState(null);
71831
+ const [error2, setError] = reactExports.useState(null);
71832
+ const [busy, setBusy] = reactExports.useState(false);
71833
+ const requests = reactExports.useMemo(() => {
71834
+ const out = [];
71835
+ for (const entry of Object.values(collections)) {
71836
+ for (const req of Object.values(entry.data.requests)) {
71837
+ if (req.disabled) continue;
71838
+ out.push({ name: `${entry.data.name} / ${req.name}`, method: req.method, url: req.url });
71839
+ }
71840
+ }
71841
+ return out;
71842
+ }, [collections]);
71843
+ const load = (i) => electron$8.coverageLoadSpec(i.text.trim() ? { text: i.text } : /^https?:\/\//i.test(i.source.trim()) ? { url: i.source.trim() } : { path: i.source.trim() });
71844
+ async function run() {
71845
+ setBusy(true);
71846
+ setError(null);
71847
+ try {
71848
+ const [oldSpec, newSpec] = await Promise.all([load(oldIn), load(newIn)]);
71849
+ setChanges(diffSpecs(oldSpec, newSpec));
71850
+ } catch (err) {
71851
+ setError(err instanceof Error ? err.message : String(err));
71852
+ setChanges(null);
71853
+ } finally {
71854
+ setBusy(false);
71855
+ }
71856
+ }
71857
+ const impact = reactExports.useMemo(() => {
71858
+ if (!changes) return null;
71859
+ const affected = changes.filter((c) => c.breaking && c.method).map((change) => ({
71860
+ change,
71861
+ tests: requests.filter((r) => r.method.toUpperCase() === change.method && pathMatches(r.url, change.path)).map((r) => r.name)
71862
+ }));
71863
+ const hitTests = new Set(affected.flatMap((a) => a.tests));
71864
+ return { affected, hitTests: hitTests.size };
71865
+ }, [changes, requests]);
71866
+ if (!open) return null;
71867
+ const sum = changes ? summarizeDiff(changes) : null;
71868
+ const canRun = (oldIn.source.trim() || oldIn.text.trim()) && (newIn.source.trim() || newIn.text.trim());
71869
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
71870
+ Modal,
71871
+ {
71872
+ onClose: () => setOpen(false),
71873
+ overlayClassName: "bg-black/50 z-50 flex items-start justify-center pt-16",
71874
+ panelClassName: "bg-surface-900 border border-surface-800 rounded-lg shadow-2xl flex flex-col w-[760px] max-h-[82vh]",
71875
+ title: "Compare API specs",
71876
+ subtitle: "Breaking-change detection and impact on your tests",
71877
+ children: [
71878
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 border-b border-surface-800 flex-shrink-0", children: [
71879
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 items-start", children: [
71880
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SpecField, { label: "Baseline (old)", value: oldIn, onSource: (s) => setOldIn({ ...oldIn, source: s }), onText: (t2) => setOldIn({ ...oldIn, text: t2 }) }),
71881
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-surface-500 pt-6", children: "->" }),
71882
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SpecField, { label: "Candidate (new)", value: newIn, onSource: (s) => setNewIn({ ...newIn, source: s }), onText: (t2) => setNewIn({ ...newIn, text: t2 }) })
71883
+ ] }),
71884
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end mt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
71885
+ "button",
71886
+ {
71887
+ onClick: run,
71888
+ disabled: busy || !canRun,
71889
+ className: "px-4 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-surface-800 disabled:text-surface-500 rounded text-sm font-medium",
71890
+ children: busy ? "…" : "Compare"
71891
+ }
71892
+ ) }),
71893
+ error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-red-400 mt-2", children: error2 })
71894
+ ] }),
71895
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3 flex-1 overflow-y-auto min-h-0 text-xs", children: !changes ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-surface-500 text-center py-10", children: [
71896
+ "Point at two spec versions and choose ",
71897
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-300", children: "Compare" }),
71898
+ "."
71899
+ ] }) : changes.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 text-center py-10", children: "No differences between the specs." }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
71900
+ sum.breaking > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3", children: [
71901
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 font-bold uppercase tracking-wide text-[11px] mb-1", children: "Breaking changes" }),
71902
+ changes.filter((c) => c.breaking).map((c, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 text-surface-300 py-0.5", children: [
71903
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-400", children: "✗" }),
71904
+ c.detail
71905
+ ] }, i))
71906
+ ] }),
71907
+ sum.nonBreaking > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3", children: [
71908
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 font-bold uppercase tracking-wide text-[11px] mb-1", children: "Non-breaking" }),
71909
+ changes.filter((c) => !c.breaking).map((c, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 text-surface-400 py-0.5", children: [
71910
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-emerald-400", children: "✓" }),
71911
+ c.detail
71912
+ ] }, i))
71913
+ ] }),
71914
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 pt-3 border-t border-surface-800", children: sum.breaking === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 font-medium", children: "No breaking changes. Safe to deploy." }) : impact.hitTests === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-amber-400", children: [
71915
+ sum.breaking,
71916
+ " breaking change",
71917
+ sum.breaking !== 1 ? "s" : "",
71918
+ ", but no test in this workspace exercises the affected operations. Add tests, then re-check."
71919
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
71920
+ impact.affected.filter((a) => a.tests.length).map((a, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-1", children: [
71921
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-amber-300", children: a.change.detail }),
71922
+ a.tests.map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-surface-500 pl-4", children: [
71923
+ "- ",
71924
+ t2
71925
+ ] }, t2))
71926
+ ] }, i)),
71927
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 text-red-400 font-bold", children: [
71928
+ "Block deployment: ",
71929
+ sum.breaking,
71930
+ " breaking change",
71931
+ sum.breaking !== 1 ? "s" : "",
71932
+ " affect ",
71933
+ impact.hitTests,
71934
+ " test",
71935
+ impact.hitTests !== 1 ? "s" : "",
71936
+ "."
71937
+ ] })
71938
+ ] }) })
71939
+ ] }) })
71940
+ ]
71941
+ }
71942
+ );
71943
+ }
70826
71944
  function CollectionPanel() {
70827
71945
  const activeCollectionId = useStore((s) => s.activeCollectionId);
70828
71946
  const collections = useStore((s) => s.collections);
70829
71947
  const updateCollectionDataSet = useStore((s) => s.updateCollectionDataSet);
70830
71948
  const openRunner = useStore((s) => s.openRunner);
71949
+ const setCoverageOpen = useStore((s) => s.setCoverageOpen);
71950
+ const setCompareOpen = useStore((s) => s.setCompareOpen);
70831
71951
  const [activeTab, setActiveTab] = reactExports.useState("data");
70832
71952
  if (!activeCollectionId) {
70833
71953
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-surface-400 text-sm", children: "Select a request from the sidebar" });
@@ -70849,17 +71969,37 @@ function CollectionPanel() {
70849
71969
  iterCount > 0 ? ` · ${iterCount} data row${iterCount !== 1 ? "s" : ""}` : ""
70850
71970
  ] })
70851
71971
  ] }),
70852
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
70853
- "button",
70854
- {
70855
- onClick: () => openRunner(activeCollectionId),
70856
- className: "px-3 py-1.5 text-xs bg-emerald-700 hover:bg-emerald-600 rounded font-medium transition-colors flex items-center gap-1.5",
70857
- children: [
70858
- /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) }),
70859
- "Run collection"
70860
- ]
70861
- }
70862
- )
71972
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
71973
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71974
+ "button",
71975
+ {
71976
+ onClick: () => setCoverageOpen(true),
71977
+ title: "Measure how much of an OpenAPI spec this workspace tests",
71978
+ className: "px-3 py-1.5 text-xs border border-surface-700 text-surface-300 hover:text-white hover:border-surface-500 rounded font-medium transition-colors",
71979
+ children: "Coverage"
71980
+ }
71981
+ ),
71982
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71983
+ "button",
71984
+ {
71985
+ onClick: () => setCompareOpen(true),
71986
+ title: "Diff two OpenAPI versions: breaking changes and which tests they affect",
71987
+ className: "px-3 py-1.5 text-xs border border-surface-700 text-surface-300 hover:text-white hover:border-surface-500 rounded font-medium transition-colors",
71988
+ children: "Compare"
71989
+ }
71990
+ ),
71991
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
71992
+ "button",
71993
+ {
71994
+ onClick: () => openRunner(activeCollectionId),
71995
+ className: "px-3 py-1.5 text-xs bg-emerald-700 hover:bg-emerald-600 rounded font-medium transition-colors flex items-center gap-1.5",
71996
+ children: [
71997
+ /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) }),
71998
+ "Run collection"
71999
+ ]
72000
+ }
72001
+ )
72002
+ ] })
70863
72003
  ] }),
70864
72004
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-6 flex-shrink-0", children: [
70865
72005
  { id: "data", label: "Data", badge: iterCount > 0 ? iterCount : 0 },
@@ -73276,18 +74416,70 @@ function detectPlatform(remotes) {
73276
74416
  return "unknown";
73277
74417
  }
73278
74418
  const NODE_LTS = "lts/*";
73279
- function generateCiContent(platform, envName, tags2, secretVars) {
74419
+ const PROVIDER_ENV = {
74420
+ vault: ["VAULT_ADDR", "VAULT_ROLE_ID", "VAULT_SECRET_ID"],
74421
+ aws: ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
74422
+ azure: ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"],
74423
+ op: ["OP_CONNECT_HOST", "OP_CONNECT_TOKEN"]
74424
+ };
74425
+ function secretManagerOf(ref2) {
74426
+ if (ref2.startsWith("op://")) return "op";
74427
+ const scheme = ref2.split(":", 1)[0];
74428
+ if (scheme === "vault" || scheme === "aws" || scheme === "azure") return scheme;
74429
+ return null;
74430
+ }
74431
+ const INLINE_REF_RE = /\{\{\s*(vault:|aws:|azure:|op:\/\/)/g;
74432
+ function inlineSecretManagers(text) {
74433
+ if (!text) return [];
74434
+ const kinds = /* @__PURE__ */ new Set();
74435
+ for (const m of text.matchAll(INLINE_REF_RE)) {
74436
+ kinds.add(m[1] === "op://" ? "op" : m[1].slice(0, -1));
74437
+ }
74438
+ return [...kinds];
74439
+ }
74440
+ function requestSecretManagers(req) {
74441
+ return inlineSecretManagers(JSON.stringify(req));
74442
+ }
74443
+ function providerEnvVars(managers) {
74444
+ return [...new Set(managers.flatMap((k) => PROVIDER_ENV[k]))];
74445
+ }
74446
+ function generateCiContent(platform, envName, tags2, secretVars, secretManagers = []) {
73280
74447
  const runCmd = [
73281
74448
  "api-spector run --workspace .",
73282
74449
  envName ? `--environment "${envName}"` : "",
73283
74450
  tags2 ? `--tags "${tags2}"` : "",
73284
74451
  "--output results.html"
73285
74452
  ].filter(Boolean).join(" ");
73286
- const allSecretVars = secretVars.length ? ["API_SPECTOR_MASTER_KEY", ...secretVars] : [];
74453
+ const managers = [...new Set(secretManagers)];
74454
+ const usesVault = managers.includes("vault");
74455
+ const encryptedVars = secretVars.length ? ["API_SPECTOR_MASTER_KEY", ...secretVars] : [];
73287
74456
  if (platform === "github") {
73288
- const secretHint = allSecretVars.length ? ` # Add these secrets in: Settings → Secrets and variables → Actions
73289
- ` + allSecretVars.map((v) => ` # ${v}`).join("\n") + "\n" : "";
73290
- const envBlock = allSecretVars.length ? "\n env:\n" + allSecretVars.map((v) => ` ${v}: \${{ secrets.${v} }}`).join("\n") : "";
74457
+ const ghProviderEnv = providerEnvVars(managers).filter((v) => v !== "VAULT_ROLE_ID" && v !== "VAULT_SECRET_ID");
74458
+ const allSecretVars = [...encryptedVars, ...ghProviderEnv.filter((v) => v !== "VAULT_ADDR")];
74459
+ const hintLines = [
74460
+ ...allSecretVars.map((v) => ` # ${v}`),
74461
+ ...usesVault ? [" # VAULT_ADDR (as a repository variable, not a secret)"] : []
74462
+ ];
74463
+ const secretHint = hintLines.length ? ` # ⚠ Add these in: Settings → Secrets and variables → Actions
74464
+ ${hintLines.join("\n")}
74465
+ ` : "";
74466
+ const vaultStep = usesVault ? ` - name: Authenticate to Vault (OIDC)
74467
+ uses: hashicorp/vault-action@v3
74468
+ with:
74469
+ url: \${{ vars.VAULT_ADDR }}
74470
+ method: jwt
74471
+ role: apispector-ci # your Vault JWT/OIDC role
74472
+ exportToken: true # sets VAULT_TOKEN for later steps
74473
+ ` : "";
74474
+ const permissions = usesVault ? ` permissions:
74475
+ contents: read
74476
+ id-token: write # required for Vault OIDC
74477
+ ` : "";
74478
+ const envEntries = [
74479
+ ...allSecretVars.map((v) => ` ${v}: \${{ secrets.${v} }}`),
74480
+ ...usesVault ? [" VAULT_ADDR: ${{ vars.VAULT_ADDR }}"] : []
74481
+ ];
74482
+ const envBlock = envEntries.length ? "\n env:\n" + envEntries.join("\n") : "";
73291
74483
  return `name: API Tests
73292
74484
 
73293
74485
  on:
@@ -73298,13 +74490,13 @@ on:
73298
74490
  jobs:
73299
74491
  api-tests:
73300
74492
  runs-on: ubuntu-latest
73301
- steps:
74493
+ ${permissions} steps:
73302
74494
  - uses: actions/checkout@v4
73303
74495
  - uses: actions/setup-node@v4
73304
74496
  with:
73305
74497
  node-version: '${NODE_LTS}'
73306
74498
  - run: npm install -g @testsmith/api-spector
73307
- ${secretHint} - name: Run API tests
74499
+ ${vaultStep}${secretHint} - name: Run API tests
73308
74500
  run: ${runCmd}${envBlock}
73309
74501
  - name: Upload test results
73310
74502
  if: always()
@@ -73315,6 +74507,7 @@ ${secretHint} - name: Run API tests
73315
74507
  `;
73316
74508
  }
73317
74509
  if (platform === "gitlab") {
74510
+ const allSecretVars = [...encryptedVars, ...providerEnvVars(managers)];
73318
74511
  const secretHint = allSecretVars.length ? ` # ⚠ Add these in: Settings → CI/CD → Variables
73319
74512
  ` + allSecretVars.map((v) => ` # ${v}`).join("\n") + "\n" : "";
73320
74513
  const envBlock = allSecretVars.length ? "\n variables:\n" + allSecretVars.map((v) => ` ${v}: $${v}`).join("\n") : "";
@@ -73333,6 +74526,7 @@ ${secretHint} script:
73333
74526
  `;
73334
74527
  }
73335
74528
  if (platform === "azure") {
74529
+ const allSecretVars = [...encryptedVars, ...providerEnvVars(managers)];
73336
74530
  const secretHint = allSecretVars.length ? ` # ⚠ Add these in: Pipelines → Library → Variable groups (mark as secret)
73337
74531
  ` + allSecretVars.map((v) => ` # ${v}`).join("\n") + "\n" : "";
73338
74532
  const envBlock = allSecretVars.length ? "\n env:\n" + allSecretVars.map((v) => ` ${v}: $(${v})`).join("\n") : "";
@@ -74026,6 +75220,7 @@ function BranchesTab({ onRefresh }) {
74026
75220
  function CiTab() {
74027
75221
  const environments = useStore((s) => s.environments);
74028
75222
  const envList = Object.values(environments);
75223
+ const collections = useStore((s) => s.collections);
74029
75224
  const [remotes, setRemotes] = reactExports.useState([]);
74030
75225
  const [platform, setPlatform] = reactExports.useState("unknown");
74031
75226
  const [envId, setEnvId] = reactExports.useState("");
@@ -74045,10 +75240,13 @@ function CiTab() {
74045
75240
  reactExports.useEffect(() => {
74046
75241
  const env = envList.find((e) => e.data.id === envId);
74047
75242
  const secretVars = env ? env.data.variables.filter((v) => v.secret && v.enabled).map((v) => v.key) : [];
75243
+ const envRefManagers = env ? env.data.variables.filter((v) => v.enabled && v.secretRef).map((v) => secretManagerOf(v.secretRef)).filter((k) => k !== null) : [];
75244
+ const inlineManagers = Object.values(collections).flatMap((c) => Object.values(c.data.requests).flatMap(requestSecretManagers));
75245
+ const secretManagers = [.../* @__PURE__ */ new Set([...envRefManagers, ...inlineManagers])];
74048
75246
  const envName = env?.data.name ?? "";
74049
- setPreview(generateCiContent(platform, envName, tags2, secretVars));
75247
+ setPreview(generateCiContent(platform, envName, tags2, secretVars, secretManagers));
74050
75248
  setWritten(false);
74051
- }, [platform, envId, tags2, environments]);
75249
+ }, [platform, envId, tags2, environments, collections]);
74052
75250
  async function write() {
74053
75251
  try {
74054
75252
  setError(null);
@@ -74633,6 +75831,8 @@ function App() {
74633
75831
  }
74634
75832
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-screen bg-surface-950", style: { color: "var(--text-primary)" }, children: [
74635
75833
  /* @__PURE__ */ jsxRuntimeExports.jsx(RunnerModal, {}),
75834
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CoverageModal, {}),
75835
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CompareModal, {}),
74636
75836
  /* @__PURE__ */ jsxRuntimeExports.jsx(CommandPalette, {}),
74637
75837
  docsModalOpen && /* @__PURE__ */ jsxRuntimeExports.jsx(DocsGeneratorModal, { onClose: () => setDocsModalOpen(false) }),
74638
75838
  window.electron.platform !== "win32" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "drag-region flex-shrink-0 bg-surface-950 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "no-drag text-[11px] font-medium tracking-widest select-none", style: { color: "var(--text-muted)" }, children: [
@@ -74641,7 +75841,7 @@ function App() {
74641
75841
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
74642
75842
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
74643
75843
  "v",
74644
- "0.4.9"
75844
+ "0.5.0"
74645
75845
  ] }),
74646
75846
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
74647
75847
  /* @__PURE__ */ jsxRuntimeExports.jsx(