@testsmith/api-spector 0.3.9 → 0.4.1

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.
@@ -13983,6 +13983,16 @@ const createCollectionsSlice = (set2, get2) => ({
13983
13983
  if (folder.headers?.length) inheritedHeaders = [...inheritedHeaders, ...folder.headers];
13984
13984
  }
13985
13985
  return { auth: inheritedAuth, headers: inheritedHeaders };
13986
+ },
13987
+ getInheritedVariables: (requestId) => {
13988
+ const state = get2();
13989
+ const colEntry = Object.values(state.collections).find((c) => c.data.requests[requestId]);
13990
+ if (!colEntry) return {};
13991
+ const merged = {};
13992
+ for (const folder of findFolderPath(colEntry.data.rootFolder, requestId)) {
13993
+ if (folder.variables) Object.assign(merged, folder.variables);
13994
+ }
13995
+ return merged;
13986
13996
  }
13987
13997
  });
13988
13998
  function envSlugRelPath(name2) {
@@ -14133,6 +14143,7 @@ const createUiSlice = (set2) => ({
14133
14143
  pinnedResponse: null,
14134
14144
  activeGitDiff: null,
14135
14145
  quickInsertsOpen: true,
14146
+ sendSignal: 0,
14136
14147
  setShowGeneratorPanel: (v) => set2((s) => {
14137
14148
  s.showGeneratorPanel = v;
14138
14149
  }),
@@ -14180,6 +14191,9 @@ const createUiSlice = (set2) => ({
14180
14191
  }),
14181
14192
  setQuickInsertsOpen: (open) => set2((s) => {
14182
14193
  s.quickInsertsOpen = open;
14194
+ }),
14195
+ requestSend: () => set2((s) => {
14196
+ s.sendSignal += 1;
14183
14197
  })
14184
14198
  });
14185
14199
  const useStore = create()(
@@ -35468,7 +35482,11 @@ function AuthEditor({
35468
35482
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35469
35483
  "button",
35470
35484
  {
35471
- onClick: () => secrets.saveSecret(auth.passwordSecretRef ?? "NTLM_PASSWORD"),
35485
+ onClick: () => {
35486
+ const ref2 = auth.passwordSecretRef ?? "NTLM_PASSWORD";
35487
+ onChange({ passwordSecretRef: ref2 });
35488
+ void secrets.saveSecret(ref2);
35489
+ },
35472
35490
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35473
35491
  children: secrets.saved ? "✓" : "Save"
35474
35492
  }
@@ -35581,7 +35599,11 @@ function AuthEditor({
35581
35599
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35582
35600
  "button",
35583
35601
  {
35584
- onClick: () => secrets.saveSecret(auth.apiKeySecretRef ?? "API_KEY"),
35602
+ onClick: () => {
35603
+ const ref2 = auth.apiKeySecretRef ?? "API_KEY";
35604
+ onChange({ apiKeySecretRef: ref2 });
35605
+ void secrets.saveSecret(ref2);
35606
+ },
35585
35607
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35586
35608
  children: secrets.saved ? "✓" : "Save"
35587
35609
  }
@@ -35640,55 +35662,88 @@ function BasicCredentialsFields({
35640
35662
  label,
35641
35663
  note
35642
35664
  }) {
35665
+ const [keychainOpen, setKeychainOpen] = reactExports.useState(!!auth.passwordSecretRef);
35643
35666
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
35644
35667
  label && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-500 text-[10px] uppercase tracking-wide", children: label }),
35645
35668
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
35646
35669
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
35647
35670
  /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-surface-400", children: "Username" }),
35648
35671
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35649
- "input",
35672
+ VarInput,
35650
35673
  {
35651
35674
  value: auth.username ?? "",
35652
- onChange: (e) => setAuth({ username: e.target.value }),
35675
+ onChange: (v) => setAuth({ username: v }),
35676
+ placeholder: "username",
35653
35677
  className: "mt-1 w-full bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500"
35654
35678
  }
35655
35679
  )
35656
35680
  ] }),
35657
35681
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
35658
- /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-surface-400", children: "Password" }),
35659
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 mt-1", children: [
35660
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35661
- "input",
35662
- {
35663
- type: "password",
35664
- value: secretValue,
35665
- onChange: (e) => setSecretValue(e.target.value),
35666
- placeholder: auth.passwordSecretRef ? `Stored as "${auth.passwordSecretRef}"` : "Password",
35667
- className: "flex-1 bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500 font-mono"
35668
- }
35669
- ),
35670
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35671
- "button",
35672
- {
35673
- onClick: () => saveSecret(auth.passwordSecretRef ?? "API_PASSWORD"),
35674
- className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35675
- children: saved ? "✓" : "Save"
35676
- }
35677
- )
35678
- ] })
35682
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "text-surface-400", children: [
35683
+ "Password",
35684
+ " ",
35685
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-500 text-[10px]", children: [
35686
+ "- supports ",
35687
+ "{{variables}}"
35688
+ ] })
35689
+ ] }),
35690
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35691
+ VarInput,
35692
+ {
35693
+ value: auth.password ?? "",
35694
+ onChange: (v) => setAuth({ password: v }),
35695
+ placeholder: "password or API token",
35696
+ className: "mt-1 w-full bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500 font-mono"
35697
+ }
35698
+ )
35679
35699
  ] })
35680
35700
  ] }),
35681
- /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-surface-400 text-[10px]", children: [
35682
- "Password stored in OS keychain as",
35683
- " ",
35684
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35685
- "input",
35686
- {
35687
- value: auth.passwordSecretRef ?? "API_PASSWORD",
35688
- onChange: (e) => setAuth({ passwordSecretRef: e.target.value }),
35689
- className: "inline bg-transparent border-b border-surface-700 focus:outline-none focus:border-blue-500 w-24"
35690
- }
35691
- )
35701
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
35702
+ "button",
35703
+ {
35704
+ onClick: () => setKeychainOpen((o) => !o),
35705
+ className: "text-[10px] text-surface-500 hover:text-surface-300 text-left transition-colors w-fit",
35706
+ children: [
35707
+ keychainOpen ? "▾" : "",
35708
+ " Store password in OS keychain instead"
35709
+ ]
35710
+ }
35711
+ ),
35712
+ keychainOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 pl-3 border-l border-surface-800", children: [
35713
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-500 text-[10px]", children: "Encrypt the password in your OS keychain rather than saving it with the request. Leave the Password field above empty to use the keychain value." }),
35714
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
35715
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35716
+ "input",
35717
+ {
35718
+ type: "password",
35719
+ value: secretValue,
35720
+ onChange: (e) => setSecretValue(e.target.value),
35721
+ placeholder: auth.passwordSecretRef ? `Stored as "${auth.passwordSecretRef}"` : "Password",
35722
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500 font-mono"
35723
+ }
35724
+ ),
35725
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35726
+ "input",
35727
+ {
35728
+ value: auth.passwordSecretRef ?? "API_PASSWORD",
35729
+ onChange: (e) => setAuth({ passwordSecretRef: e.target.value }),
35730
+ placeholder: "Keychain key",
35731
+ className: "w-32 bg-surface-800 border border-surface-700 rounded px-2 py-1 focus:outline-none focus:border-blue-500"
35732
+ }
35733
+ ),
35734
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35735
+ "button",
35736
+ {
35737
+ onClick: () => {
35738
+ const ref2 = auth.passwordSecretRef ?? "API_PASSWORD";
35739
+ setAuth({ passwordSecretRef: ref2 });
35740
+ void saveSecret(ref2);
35741
+ },
35742
+ className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35743
+ children: saved ? "✓" : "Save"
35744
+ }
35745
+ )
35746
+ ] })
35692
35747
  ] }),
35693
35748
  note && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-600 text-[10px]", children: note })
35694
35749
  ] });
@@ -35758,7 +35813,11 @@ function BearerPanel({
35758
35813
  /* @__PURE__ */ jsxRuntimeExports.jsx(
35759
35814
  "button",
35760
35815
  {
35761
- onClick: () => saveSecret(auth.tokenSecretRef ?? "API_TOKEN"),
35816
+ onClick: () => {
35817
+ const ref2 = auth.tokenSecretRef ?? "API_TOKEN";
35818
+ setAuth({ tokenSecretRef: ref2 });
35819
+ void saveSecret(ref2);
35820
+ },
35762
35821
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
35763
35822
  children: saved ? "✓" : "Save"
35764
35823
  }
@@ -35772,11 +35831,17 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35772
35831
  const [activeTab, setActiveTab] = reactExports.useState("auth");
35773
35832
  const [auth, setAuth] = reactExports.useState(folder.auth ?? { type: "none" });
35774
35833
  const [headers, setHeaders] = reactExports.useState(folder.headers ?? []);
35834
+ const [varRows, setVarRows] = reactExports.useState(
35835
+ Object.entries(folder.variables ?? {}).map(([key, value]) => ({ key, value, enabled: true }))
35836
+ );
35775
35837
  function patchAuth(patch) {
35776
35838
  setAuth((prev) => ({ ...prev, ...patch }));
35777
35839
  }
35778
35840
  function save() {
35779
- updateFolder(collectionId, folder.id, { auth, headers });
35841
+ const variables = Object.fromEntries(
35842
+ varRows.filter((r) => r.key.trim()).map((r) => [r.key.trim(), r.value])
35843
+ );
35844
+ updateFolder(collectionId, folder.id, { auth, headers, variables });
35780
35845
  onClose();
35781
35846
  }
35782
35847
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
@@ -35791,12 +35856,12 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35791
35856
  /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-sm font-semibold", children: "Folder settings" }),
35792
35857
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-surface-600 mt-0.5", children: [
35793
35858
  folder.name,
35794
- " - auth and headers inherited by all requests in this folder"
35859
+ " - auth, headers and variables inherited by all requests in this folder"
35795
35860
  ] })
35796
35861
  ] }),
35797
35862
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "text-surface-400 hover:text-white text-lg leading-none", children: "×" })
35798
35863
  ] }),
35799
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-4 shrink-0", children: ["auth", "headers"].map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
35864
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-4 shrink-0", children: ["auth", "headers", "variables"].map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
35800
35865
  "button",
35801
35866
  {
35802
35867
  onClick: () => setActiveTab(t2),
@@ -35823,7 +35888,23 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35823
35888
  valuePlaceholder: "value",
35824
35889
  headerMode: true
35825
35890
  }
35826
- )
35891
+ ),
35892
+ activeTab === "variables" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
35893
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-surface-600", children: [
35894
+ "Variables scoped to this folder. They override collection variables and are overridden by an inner folder, the active environment, and script-set values. Reference them anywhere with ",
35895
+ "{{name}}",
35896
+ "."
35897
+ ] }),
35898
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35899
+ KVTable,
35900
+ {
35901
+ rows: varRows,
35902
+ onChange: setVarRows,
35903
+ keyPlaceholder: "VARIABLE_NAME",
35904
+ valuePlaceholder: "value"
35905
+ }
35906
+ )
35907
+ ] })
35827
35908
  ] }),
35828
35909
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 px-4 py-3 border-t border-surface-800 shrink-0", children: [
35829
35910
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -55915,7 +55996,11 @@ function AuthTab({ request, onChange }) {
55915
55996
  /* @__PURE__ */ jsxRuntimeExports.jsx(
55916
55997
  "button",
55917
55998
  {
55918
- onClick: () => saveSecret(auth.oauth2ClientSecretRef ?? "OAUTH2_CLIENT_SECRET"),
55999
+ onClick: () => {
56000
+ const ref2 = auth.oauth2ClientSecretRef ?? "OAUTH2_CLIENT_SECRET";
56001
+ setAuth({ oauth2ClientSecretRef: ref2 });
56002
+ void saveSecret(ref2);
56003
+ },
55919
56004
  className: "px-2 py-1 bg-blue-700 hover:bg-blue-600 rounded transition-colors",
55920
56005
  children: saved ? "✓" : "Save"
55921
56006
  }
@@ -63825,6 +63910,7 @@ function RequestBuilder({ request }) {
63825
63910
  }
63826
63911
  const [editingName, setEditingName] = reactExports.useState(false);
63827
63912
  const [showFuzz, setShowFuzz] = reactExports.useState(false);
63913
+ const [customVerb, setCustomVerb] = reactExports.useState(false);
63828
63914
  const [runHooks, setRunHooks] = reactExports.useState(() => localStorage.getItem("runHooks") !== "false");
63829
63915
  function toggleRunHooks() {
63830
63916
  setRunHooks((prev) => {
@@ -63836,6 +63922,14 @@ function RequestBuilder({ request }) {
63836
63922
  function update(patch) {
63837
63923
  updateRequest(request.id, patch);
63838
63924
  }
63925
+ const sendSignal = useStore((s) => s.sendSignal);
63926
+ const lastSendSignal = reactExports.useRef(sendSignal);
63927
+ reactExports.useEffect(() => {
63928
+ if (sendSignal !== lastSendSignal.current) {
63929
+ lastSendSignal.current = sendSignal;
63930
+ void sendRequest();
63931
+ }
63932
+ }, [sendSignal]);
63839
63933
  async function sendRequest() {
63840
63934
  if (!activeTabId) return;
63841
63935
  setTabSending(activeTabId, true);
@@ -63854,6 +63948,8 @@ function RequestBuilder({ request }) {
63854
63948
  };
63855
63949
  let collectionVars = {
63856
63950
  ...activeCollectionId ? collections[activeCollectionId]?.data.collectionVariables ?? {} : {},
63951
+ // Folder-chain variables sit above collection vars and below session/local.
63952
+ ...useStore.getState().getInheritedVariables(request.id),
63857
63953
  ...sessionVars
63858
63954
  };
63859
63955
  let liveGlobals = { ...globals };
@@ -64064,15 +64160,39 @@ function RequestBuilder({ request }) {
64064
64160
  }
64065
64161
  )
64066
64162
  ] }),
64067
- !isWs && !isSoap && /* @__PURE__ */ jsxRuntimeExports.jsx(
64068
- "select",
64163
+ !isWs && !isSoap && (customVerb ? /* @__PURE__ */ jsxRuntimeExports.jsx(
64164
+ "input",
64069
64165
  {
64166
+ autoFocus: true,
64070
64167
  value: request.method,
64071
- onChange: (e) => update({ method: e.target.value }),
64072
- className: `bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold focus:outline-none focus:border-blue-500 ${METHOD_COLORS[request.method]}`,
64073
- children: METHODS.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, className: "text-white", children: m }, m))
64168
+ onChange: (e) => update({ method: e.target.value.toUpperCase() }),
64169
+ onBlur: () => setCustomVerb(false),
64170
+ onKeyDown: (e) => {
64171
+ if (e.key === "Enter") {
64172
+ e.preventDefault();
64173
+ setCustomVerb(false);
64174
+ }
64175
+ },
64176
+ placeholder: "VERB",
64177
+ title: "Type any HTTP method",
64178
+ className: "w-24 bg-surface-800 border border-blue-500 rounded px-2 py-1.5 text-xs font-bold uppercase focus:outline-none text-fuchsia-400 placeholder-surface-600"
64074
64179
  }
64075
- ),
64180
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(
64181
+ "select",
64182
+ {
64183
+ value: METHODS.includes(request.method) ? request.method : "__current__",
64184
+ onChange: (e) => {
64185
+ if (e.target.value === "__custom__") setCustomVerb(true);
64186
+ else if (e.target.value !== "__current__") update({ method: e.target.value });
64187
+ },
64188
+ className: `bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold focus:outline-none focus:border-blue-500 ${METHOD_COLORS[request.method] ?? "text-fuchsia-400"}`,
64189
+ children: [
64190
+ METHODS.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, className: "text-white", children: m }, m)),
64191
+ !METHODS.includes(request.method) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "__current__", className: "text-white", children: request.method }),
64192
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "__custom__", className: "text-white", children: "Custom…" })
64193
+ ]
64194
+ }
64195
+ )),
64076
64196
  isSoap && /* @__PURE__ */ jsxRuntimeExports.jsx(
64077
64197
  "span",
64078
64198
  {
@@ -65248,7 +65368,7 @@ function KVBlock({ label, rows }) {
65248
65368
  ] }, k))
65249
65369
  ] });
65250
65370
  }
65251
- function HistoryTabRow({ entry, onLoad }) {
65371
+ function HistoryTabRow({ entry, onLoad, onResend }) {
65252
65372
  const [open, setOpen] = reactExports.useState(false);
65253
65373
  const reqBody = requestBodyText(entry.request.body);
65254
65374
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-b border-surface-800", children: [
@@ -65266,7 +65386,8 @@ function HistoryTabRow({ entry, onLoad }) {
65266
65386
  ] }),
65267
65387
  entry.environmentName && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] bg-surface-800 text-surface-400 px-1.5 py-0.5 rounded shrink-0", children: entry.environmentName }),
65268
65388
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] text-surface-500 ml-auto shrink-0", children: new Date(entry.timestamp).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) }),
65269
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onLoad, title: "Load this response into the viewer", className: "text-[10px] text-blue-400 hover:text-blue-300 shrink-0", children: "load" })
65389
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onLoad, title: "Load this response into the viewer", className: "text-[10px] text-blue-400 hover:text-blue-300 shrink-0", children: "load" }),
65390
+ onResend && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onResend, title: "Send this request again", className: "text-[10px] text-emerald-400 hover:text-emerald-300 shrink-0", children: "resend" })
65270
65391
  ] }),
65271
65392
  open && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 pb-3 pt-1 flex flex-col gap-3 bg-surface-950/40", children: [
65272
65393
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
@@ -65307,6 +65428,7 @@ function ResponseViewer() {
65307
65428
  const hookResults = activeTab?.lastHookResults ?? null;
65308
65429
  const requestId = activeTab?.requestId ?? null;
65309
65430
  const setTabResponse = useStore((s) => s.setTabResponse);
65431
+ const requestSend = useStore((s) => s.requestSend);
65310
65432
  const history2 = useStore((s) => s.history);
65311
65433
  const activeEnvironmentId = useStore((s) => s.activeEnvironmentId);
65312
65434
  const environments = useStore((s) => s.environments);
@@ -65330,6 +65452,13 @@ function ResponseViewer() {
65330
65452
  setTab("console");
65331
65453
  }
65332
65454
  }, [scriptResult?.preScriptError, scriptResult?.postScriptError]);
65455
+ reactExports.useEffect(() => {
65456
+ if (response?.error) {
65457
+ if (tab !== "request" && tab !== "history") setTab("error");
65458
+ } else if (tab === "error") {
65459
+ setTab("body");
65460
+ }
65461
+ }, [response]);
65333
65462
  const [diffMode, setDiffMode] = reactExports.useState(false);
65334
65463
  const [showMockModal, setShowMockModal] = reactExports.useState(false);
65335
65464
  const [bodyView, setBodyView] = reactExports.useState("raw");
@@ -65370,12 +65499,6 @@ function ResponseViewer() {
65370
65499
  if (!response) {
65371
65500
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full flex items-center justify-center text-surface-400 text-sm", children: "Hit Send to see the response" });
65372
65501
  }
65373
- if (response.error) {
65374
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col p-4 gap-2", children: [
65375
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 text-sm font-medium", children: "Request failed" }),
65376
- /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-xs text-red-300 whitespace-pre-wrap", children: response.error })
65377
- ] });
65378
- }
65379
65502
  const contentType = response.headers["content-type"] ?? "";
65380
65503
  const isJson = contentType.includes("json");
65381
65504
  const isXml = !isJson && (contentType.includes("xml") || contentType.includes("html"));
@@ -65393,15 +65516,31 @@ function ResponseViewer() {
65393
65516
  const totalCount = scriptResult?.testResults.length ?? 0;
65394
65517
  const consoleCount = scriptResult?.consoleOutput.length ?? 0;
65395
65518
  const hasScriptError = !!(scriptResult?.preScriptError || scriptResult?.postScriptError);
65396
- const tabList = [
65519
+ const historyBadge = requestHistory.length > 0 ? requestHistory.length : void 0;
65520
+ const tabList = response.error ? [
65521
+ { id: "error", label: "Error", error: true },
65522
+ { id: "request", label: "Request" },
65523
+ { id: "history", label: "History", badge: historyBadge }
65524
+ ] : [
65397
65525
  { id: "request", label: "Request" },
65398
65526
  { id: "body", label: "Body", badge: bodyParseError ? "!" : void 0, error: bodyParseError },
65399
65527
  { id: "headers", label: "Headers" },
65400
65528
  { id: "tests", label: "Tests", badge: totalCount > 0 ? `${passedCount}/${totalCount}` : void 0 },
65401
65529
  { id: "console", label: "Console", badge: hasScriptError ? "!" : consoleCount > 0 ? consoleCount : void 0, error: hasScriptError },
65402
- { id: "history", label: "History", badge: requestHistory.length > 0 ? requestHistory.length : void 0 },
65530
+ { id: "history", label: "History", badge: historyBadge },
65403
65531
  { id: "http", label: "HTTP", badge: httpFindings.length > 0 ? httpErrors > 0 ? "!" : httpFindings.length : void 0, error: httpErrors > 0 }
65404
65532
  ];
65533
+ const historyContent = requestHistory.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-500 text-center p-8", children: "No past responses for this request yet. Each send is recorded here." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col", children: requestHistory.map((entry) => /* @__PURE__ */ jsxRuntimeExports.jsx(
65534
+ HistoryTabRow,
65535
+ {
65536
+ entry,
65537
+ onLoad: () => {
65538
+ if (activeTabId) setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65539
+ },
65540
+ onResend: requestSend
65541
+ },
65542
+ entry.id
65543
+ )) });
65405
65544
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col", children: [
65406
65545
  hookResults && hookResults.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(HookResultsPanel, { results: hookResults }),
65407
65546
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 px-4 py-1.5 border-b border-surface-800 flex-shrink-0 overflow-x-auto", children: [
@@ -65430,7 +65569,7 @@ function ResponseViewer() {
65430
65569
  },
65431
65570
  t2.id
65432
65571
  )) }),
65433
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ml-auto flex items-center gap-1 shrink-0", children: [
65572
+ !response.error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ml-auto flex items-center gap-1 shrink-0", children: [
65434
65573
  assertToast.toast && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-emerald-400 font-medium px-1", children: assertToast.toast.msg }),
65435
65574
  contractToast.toast && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-blue-400 font-medium px-1", children: contractToast.toast.msg }),
65436
65575
  tab === "body" && supportsTree && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex rounded overflow-hidden border border-surface-800 mr-1", children: [
@@ -65492,7 +65631,10 @@ function ResponseViewer() {
65492
65631
  ] })
65493
65632
  ] }),
65494
65633
  showMockModal && /* @__PURE__ */ jsxRuntimeExports.jsx(SaveAsMockModal, { onClose: () => setShowMockModal(false) }),
65495
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 flex flex-col overflow-y-auto", children: diffMode && pinnedResponse ? /* @__PURE__ */ jsxRuntimeExports.jsx(DiffView, { pinned: pinnedResponse, current: response }) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65634
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 flex flex-col overflow-y-auto", children: response.error ? tab === "request" ? /* @__PURE__ */ jsxRuntimeExports.jsx(RequestPanel, { sentRequest }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: historyContent }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col p-4 gap-2", children: [
65635
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 text-sm font-medium", children: "Request failed" }),
65636
+ /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-xs text-red-300 whitespace-pre-wrap", children: response.error })
65637
+ ] }) : diffMode && pinnedResponse ? /* @__PURE__ */ jsxRuntimeExports.jsx(DiffView, { pinned: pinnedResponse, current: response }) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65496
65638
  InteractiveBody,
65497
65639
  {
65498
65640
  body: response.body,
@@ -65538,16 +65680,7 @@ function ResponseViewer() {
65538
65680
  ] }),
65539
65681
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-200 mt-1", children: find2.message })
65540
65682
  ] }, i);
65541
- }) }) }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: requestHistory.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-500 text-center p-8", children: "No past responses for this request yet. Each send is recorded here." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col", children: requestHistory.map((entry) => /* @__PURE__ */ jsxRuntimeExports.jsx(
65542
- HistoryTabRow,
65543
- {
65544
- entry,
65545
- onLoad: () => {
65546
- if (activeTabId) setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65547
- }
65548
- },
65549
- entry.id
65550
- )) }) }) : null }),
65683
+ }) }) }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: historyContent }) : null }),
65551
65684
  headerMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(
65552
65685
  ContextMenu,
65553
65686
  {
@@ -65632,7 +65765,8 @@ const TARGETS = [
65632
65765
  { id: "supertest_js", label: "Supertest JS", description: "Jest + Supertest JavaScript tests" },
65633
65766
  { id: "rest_assured", label: "REST Assured", description: "Java + JUnit 5 + Maven pom.xml" },
65634
65767
  { id: "karate", label: "Karate", description: "Karate feature files + JUnit 5 runner + Maven" },
65635
- { id: "http_file", label: "HTTP file", description: ".http / .rest file (VSCode REST Client / IntelliJ)" }
65768
+ { id: "http_file", label: "HTTP file", description: ".http / .rest file (VSCode REST Client / IntelliJ)" },
65769
+ { id: "curl", label: "cURL", description: "Runnable shell script, one curl command per request" }
65636
65770
  ];
65637
65771
  function GeneratorPanel() {
65638
65772
  const setShowGeneratorPanel = useStore((s) => s.setShowGeneratorPanel);
@@ -65888,6 +66022,7 @@ function HistoryPanel() {
65888
66022
  const activeTabId = useStore((s) => s.activeTabId);
65889
66023
  const setTabResponse = useStore((s) => s.setTabResponse);
65890
66024
  const setActiveRequest = useStore((s) => s.setActiveRequest);
66025
+ const requestSend = useStore((s) => s.requestSend);
65891
66026
  const collections = useStore((s) => s.collections);
65892
66027
  const [selected, setSelected] = reactExports.useState(null);
65893
66028
  const [search, setSearch] = reactExports.useState("");
@@ -65910,8 +66045,8 @@ function HistoryPanel() {
65910
66045
  }
65911
66046
  function open(entry) {
65912
66047
  setSelected(entry);
65913
- const stillExists = Object.values(collections).some((c) => entry.request.id in c.data.requests);
65914
- if (stillExists) {
66048
+ const stillExists2 = Object.values(collections).some((c) => entry.request.id in c.data.requests);
66049
+ if (stillExists2) {
65915
66050
  setActiveRequest(entry.request.id);
65916
66051
  const tabId = useStore.getState().activeTabId;
65917
66052
  if (tabId) setTabResponse(tabId, entry.response, entry.scriptResult ?? null);
@@ -65919,6 +66054,14 @@ function HistoryPanel() {
65919
66054
  setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65920
66055
  }
65921
66056
  }
66057
+ function replay(entry) {
66058
+ setSelected(entry);
66059
+ setActiveRequest(entry.request.id);
66060
+ requestSend();
66061
+ }
66062
+ function stillExists(entry) {
66063
+ return Object.values(collections).some((c) => entry.request.id in c.data.requests);
66064
+ }
65922
66065
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [
65923
66066
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 py-2 border-b border-surface-800 flex gap-1.5 flex-shrink-0", children: [
65924
66067
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -65963,7 +66106,8 @@ function HistoryPanel() {
65963
66106
  {
65964
66107
  entry,
65965
66108
  isSelected: selected?.id === entry.id,
65966
- onSelect: () => open(entry)
66109
+ onSelect: () => open(entry),
66110
+ onResend: stillExists(entry) ? () => replay(entry) : void 0
65967
66111
  },
65968
66112
  entry.id
65969
66113
  ))
@@ -65974,19 +66118,40 @@ function HistoryPanel() {
65974
66118
  function HistoryRow({
65975
66119
  entry,
65976
66120
  isSelected,
65977
- onSelect
66121
+ onSelect,
66122
+ onResend
65978
66123
  }) {
65979
66124
  const status = entry.response.status;
65980
66125
  const hasError = !!entry.response.error;
65981
66126
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
65982
- "button",
66127
+ "div",
65983
66128
  {
66129
+ role: "button",
66130
+ tabIndex: 0,
65984
66131
  onClick: onSelect,
65985
- className: `w-full text-left px-3 py-2 border-b border-surface-800/50 transition-colors ${isSelected ? "bg-surface-800" : "hover:bg-surface-800/50"}`,
66132
+ onKeyDown: (e) => {
66133
+ if (e.key === "Enter" || e.key === " ") {
66134
+ e.preventDefault();
66135
+ onSelect();
66136
+ }
66137
+ },
66138
+ className: `group w-full text-left px-3 py-2 border-b border-surface-800/50 transition-colors cursor-pointer ${isSelected ? "bg-surface-800" : "hover:bg-surface-800/50"}`,
65986
66139
  children: [
65987
66140
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
65988
66141
  /* @__PURE__ */ jsxRuntimeExports.jsx(MethodBadge, { method: entry.request.method, size: "xs" }),
65989
66142
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 text-xs truncate text-white", children: entry.request.name }),
66143
+ onResend && /* @__PURE__ */ jsxRuntimeExports.jsx(
66144
+ "button",
66145
+ {
66146
+ onClick: (e) => {
66147
+ e.stopPropagation();
66148
+ onResend();
66149
+ },
66150
+ title: "Send this request again",
66151
+ className: "text-[10px] text-emerald-400 hover:text-emerald-300 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity",
66152
+ children: "resend"
66153
+ }
66154
+ ),
65990
66155
  hasError ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-400 text-[10px] font-medium shrink-0", children: "ERR" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] font-bold shrink-0 ${statusColor$2(status)}`, children: status })
65991
66156
  ] }),
65992
66157
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mt-0.5 min-w-0", children: [
@@ -66010,6 +66175,15 @@ function HistoryRow({
66010
66175
  const { electron: electron$f } = window;
66011
66176
  function WelcomeScreen() {
66012
66177
  const { applyWorkspace } = useWorkspaceLoader();
66178
+ const [recents, setRecents] = reactExports.useState([]);
66179
+ const [update, setUpdate] = reactExports.useState(null);
66180
+ reactExports.useEffect(() => {
66181
+ electron$f.getRecentWorkspaces().then(setRecents).catch(() => setRecents([]));
66182
+ electron$f.checkForUpdate().then((info) => {
66183
+ if (info?.updateAvailable) setUpdate(info);
66184
+ }).catch(() => {
66185
+ });
66186
+ }, []);
66013
66187
  async function openWorkspace() {
66014
66188
  const result = await electron$f.openWorkspace();
66015
66189
  if (!result) return;
@@ -66020,7 +66194,25 @@ function WelcomeScreen() {
66020
66194
  if (!result) return;
66021
66195
  await applyWorkspace(result.workspace, result.workspacePath);
66022
66196
  }
66197
+ async function openRecent(path) {
66198
+ const result = await electron$f.openWorkspacePath(path);
66199
+ if (!result) {
66200
+ setRecents((prev) => prev.filter((r) => r.path !== path));
66201
+ return;
66202
+ }
66203
+ await applyWorkspace(result.workspace, result.workspacePath);
66204
+ }
66023
66205
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-6 text-center p-8", children: [
66206
+ update && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-1 px-4 py-2 rounded-lg border border-blue-800 bg-blue-950/40 text-xs", children: [
66207
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-200", children: [
66208
+ "New version available (v",
66209
+ update.latest,
66210
+ ", you have v",
66211
+ update.current,
66212
+ ")"
66213
+ ] }),
66214
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-[11px] text-blue-300 bg-surface-900 px-2 py-0.5 rounded select-all", children: update.command })
66215
+ ] }),
66024
66216
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
66025
66217
  /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "text-2xl font-semibold mb-1", children: [
66026
66218
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "var(--wordmark-muted)" }, children: "API" }),
@@ -66040,7 +66232,11 @@ function WelcomeScreen() {
66040
66232
  }
66041
66233
  )
66042
66234
  ] }),
66043
- /* @__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." })
66235
+ /* @__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." }),
66236
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[11px] mt-3", style: { color: "var(--text-muted)" }, children: [
66237
+ "version ",
66238
+ "0.4.1"
66239
+ ] })
66044
66240
  ] }),
66045
66241
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 w-64", children: [
66046
66242
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -66060,6 +66256,22 @@ function WelcomeScreen() {
66060
66256
  }
66061
66257
  )
66062
66258
  ] }),
66259
+ recents.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 w-72 text-left", children: [
66260
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] uppercase tracking-wider font-semibold px-1", style: { color: "var(--text-muted)" }, children: "Recent workspaces" }),
66261
+ recents.map((r) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
66262
+ "button",
66263
+ {
66264
+ onClick: () => openRecent(r.path),
66265
+ title: r.path,
66266
+ className: "flex flex-col items-start px-2 py-1.5 rounded hover:bg-surface-800 transition-colors text-left group",
66267
+ children: [
66268
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-surface-200 group-hover:text-white truncate max-w-full", children: r.name }),
66269
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-500 truncate max-w-full", children: r.path })
66270
+ ]
66271
+ },
66272
+ r.path
66273
+ ))
66274
+ ] }),
66063
66275
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-surface-400 text-xs max-w-xs", children: [
66064
66276
  "A workspace is a ",
66065
66277
  /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-500", children: ".spector" }),
@@ -67256,6 +67468,230 @@ function DocsGeneratorModal({ onClose }) {
67256
67468
  }
67257
67469
  );
67258
67470
  }
67471
+ const SKIP_WITH_ARG = /* @__PURE__ */ new Set([
67472
+ "--connect-timeout",
67473
+ "--max-time",
67474
+ "-m",
67475
+ "--retry",
67476
+ "--resolve",
67477
+ "--cacert",
67478
+ "--cert",
67479
+ "--key",
67480
+ "--proxy",
67481
+ "-x",
67482
+ "--output",
67483
+ "-o",
67484
+ "--write-out",
67485
+ "-w"
67486
+ ]);
67487
+ function tokenize(input) {
67488
+ const tokens = [];
67489
+ let cur2 = "";
67490
+ let inTok = false;
67491
+ let i = 0;
67492
+ const n = input.length;
67493
+ while (i < n) {
67494
+ const c = input[i];
67495
+ if (c === "\\" && (input[i + 1] === "\n" || input[i + 1] === "\r")) {
67496
+ i += input[i + 1] === "\r" && input[i + 2] === "\n" ? 3 : 2;
67497
+ continue;
67498
+ }
67499
+ if (c === "'") {
67500
+ inTok = true;
67501
+ i++;
67502
+ while (i < n && input[i] !== "'") cur2 += input[i++];
67503
+ i++;
67504
+ continue;
67505
+ }
67506
+ if (c === '"') {
67507
+ inTok = true;
67508
+ i++;
67509
+ while (i < n && input[i] !== '"') {
67510
+ if (input[i] === "\\" && i + 1 < n && `"\\$\``.includes(input[i + 1])) {
67511
+ cur2 += input[i + 1];
67512
+ i += 2;
67513
+ } else cur2 += input[i++];
67514
+ }
67515
+ i++;
67516
+ continue;
67517
+ }
67518
+ if (/\s/.test(c)) {
67519
+ if (inTok) {
67520
+ tokens.push(cur2);
67521
+ cur2 = "";
67522
+ inTok = false;
67523
+ }
67524
+ i++;
67525
+ continue;
67526
+ }
67527
+ if (c === "\\" && i + 1 < n) {
67528
+ cur2 += input[i + 1];
67529
+ i += 2;
67530
+ inTok = true;
67531
+ continue;
67532
+ }
67533
+ cur2 += c;
67534
+ inTok = true;
67535
+ i++;
67536
+ }
67537
+ if (inTok) tokens.push(cur2);
67538
+ return tokens;
67539
+ }
67540
+ function looksJson(s) {
67541
+ const t2 = s.trim();
67542
+ if (!(t2.startsWith("{") || t2.startsWith("["))) return false;
67543
+ try {
67544
+ JSON.parse(t2);
67545
+ return true;
67546
+ } catch {
67547
+ return false;
67548
+ }
67549
+ }
67550
+ function decodeBasic(b642) {
67551
+ try {
67552
+ const decoded = typeof atob === "function" ? atob(b642) : Buffer.from(b642, "base64").toString("utf8");
67553
+ const idx = decoded.indexOf(":");
67554
+ if (idx === -1) return null;
67555
+ return { username: decoded.slice(0, idx), password: decoded.slice(idx + 1) };
67556
+ } catch {
67557
+ return null;
67558
+ }
67559
+ }
67560
+ function parseCurl(command2) {
67561
+ const tokens = tokenize(command2.trim());
67562
+ if (tokens[0] === "curl") tokens.shift();
67563
+ let method = null;
67564
+ let url = "";
67565
+ let user = null;
67566
+ let forceGet = false;
67567
+ const headers = [];
67568
+ const dataParts = [];
67569
+ const formParts = [];
67570
+ const next = (i) => tokens[i + 1] ?? "";
67571
+ for (let i = 0; i < tokens.length; i++) {
67572
+ const t2 = tokens[i];
67573
+ switch (t2) {
67574
+ case "-X":
67575
+ case "--request":
67576
+ method = next(i);
67577
+ i++;
67578
+ break;
67579
+ case "-H":
67580
+ case "--header": {
67581
+ const raw = next(i);
67582
+ i++;
67583
+ const idx = raw.indexOf(":");
67584
+ if (idx > 0) headers.push({ key: raw.slice(0, idx).trim(), value: raw.slice(idx + 1).trim(), enabled: true });
67585
+ break;
67586
+ }
67587
+ case "-u":
67588
+ case "--user":
67589
+ user = next(i);
67590
+ i++;
67591
+ break;
67592
+ case "-d":
67593
+ case "--data":
67594
+ case "--data-raw":
67595
+ case "--data-ascii":
67596
+ case "--data-binary":
67597
+ dataParts.push(next(i));
67598
+ i++;
67599
+ break;
67600
+ case "--data-urlencode":
67601
+ dataParts.push(next(i));
67602
+ i++;
67603
+ break;
67604
+ case "-F":
67605
+ case "--form":
67606
+ formParts.push(next(i));
67607
+ i++;
67608
+ break;
67609
+ case "-b":
67610
+ case "--cookie":
67611
+ headers.push({ key: "Cookie", value: next(i), enabled: true });
67612
+ i++;
67613
+ break;
67614
+ case "-A":
67615
+ case "--user-agent":
67616
+ headers.push({ key: "User-Agent", value: next(i), enabled: true });
67617
+ i++;
67618
+ break;
67619
+ case "-e":
67620
+ case "--referer":
67621
+ headers.push({ key: "Referer", value: next(i), enabled: true });
67622
+ i++;
67623
+ break;
67624
+ case "--url":
67625
+ url = next(i);
67626
+ i++;
67627
+ break;
67628
+ case "-G":
67629
+ case "--get":
67630
+ forceGet = true;
67631
+ break;
67632
+ case "-I":
67633
+ case "--head":
67634
+ method = method ?? "HEAD";
67635
+ break;
67636
+ default:
67637
+ if (SKIP_WITH_ARG.has(t2)) {
67638
+ i++;
67639
+ break;
67640
+ }
67641
+ if (t2.startsWith("-")) break;
67642
+ if (!url) url = t2;
67643
+ break;
67644
+ }
67645
+ }
67646
+ if (!url) throw new Error("No URL found in the curl command.");
67647
+ const hasData = dataParts.length > 0;
67648
+ const isForm = formParts.length > 0;
67649
+ const resolvedMethod = (forceGet ? "GET" : method ?? (hasData || isForm ? "POST" : "GET")).toUpperCase();
67650
+ let auth = { type: "none" };
67651
+ if (user) {
67652
+ const idx = user.indexOf(":");
67653
+ auth = { type: "basic", username: idx === -1 ? user : user.slice(0, idx), password: idx === -1 ? "" : user.slice(idx + 1) };
67654
+ } else {
67655
+ const authIdx = headers.findIndex((h) => h.key.toLowerCase() === "authorization");
67656
+ if (authIdx !== -1) {
67657
+ const v = headers[authIdx].value;
67658
+ if (/^Bearer\s+/i.test(v)) {
67659
+ auth = { type: "bearer", token: v.replace(/^Bearer\s+/i, "") };
67660
+ headers.splice(authIdx, 1);
67661
+ } else if (/^Basic\s+/i.test(v)) {
67662
+ const creds = decodeBasic(v.replace(/^Basic\s+/i, "").trim());
67663
+ if (creds) {
67664
+ auth = { type: "basic", ...creds };
67665
+ headers.splice(authIdx, 1);
67666
+ }
67667
+ }
67668
+ }
67669
+ }
67670
+ let body = { mode: "none" };
67671
+ if (isForm) {
67672
+ body = {
67673
+ mode: "form",
67674
+ form: formParts.map((kv) => {
67675
+ const idx = kv.indexOf("=");
67676
+ return { key: idx === -1 ? kv : kv.slice(0, idx), value: idx === -1 ? "" : kv.slice(idx + 1), enabled: true };
67677
+ })
67678
+ };
67679
+ } else if (hasData) {
67680
+ const data = dataParts.join("&");
67681
+ const ct = headers.find((h) => h.key.toLowerCase() === "content-type")?.value;
67682
+ if (ct && ct.includes("json") || looksJson(data)) body = { mode: "json", json: data };
67683
+ else body = { mode: "raw", raw: data, rawContentType: ct ?? "application/x-www-form-urlencoded" };
67684
+ }
67685
+ let name2;
67686
+ try {
67687
+ const u = new URL(url.replace(/\{\{[^}]+\}\}/g, "x"));
67688
+ name2 = `${resolvedMethod} ${u.pathname === "/" ? u.hostname : u.pathname}`;
67689
+ } catch {
67690
+ const cleaned = url.replace(/^[a-z]+:\/\//i, "").replace(/\?.*$/, "");
67691
+ name2 = `${resolvedMethod} ${cleaned}`.trim();
67692
+ }
67693
+ return { name: name2, method: resolvedMethod, url, headers, params: [], auth, body };
67694
+ }
67259
67695
  const { electron: electron$9 } = window;
67260
67696
  const OPTIONS = [
67261
67697
  { id: "postman", label: "Postman", description: "Collection v2.1 JSON" },
@@ -67263,7 +67699,8 @@ const OPTIONS = [
67263
67699
  { id: "insomnia", label: "Insomnia", description: "Export v4 JSON" },
67264
67700
  { id: "bruno", label: "Bruno", description: "bruno.json collection file" },
67265
67701
  { id: "http", label: "HTTP file", description: ".http / .rest (REST Client)" },
67266
- { id: "spector", label: "API Spector", description: "An existing .spector / .json collection" }
67702
+ { id: "spector", label: "API Spector", description: "An existing .spector / .json collection" },
67703
+ { id: "curl", label: "cURL", description: "Paste a curl command" }
67267
67704
  ];
67268
67705
  function listEndpoints(col) {
67269
67706
  const result = [];
@@ -67287,6 +67724,7 @@ function ImportModal({ onImport, onClose }) {
67287
67724
  const markCollectionClean = useStore((s) => s.markCollectionClean);
67288
67725
  const [selected, setSelected] = reactExports.useState(null);
67289
67726
  const [url, setUrl] = reactExports.useState("");
67727
+ const [curlText, setCurlText] = reactExports.useState("");
67290
67728
  const [loading, setLoading] = reactExports.useState(false);
67291
67729
  const [error2, setError] = reactExports.useState(null);
67292
67730
  const urlInputRef = reactExports.useRef(null);
@@ -67365,6 +67803,37 @@ function ImportModal({ onImport, onClose }) {
67365
67803
  setLoading(false);
67366
67804
  }
67367
67805
  }
67806
+ function importCurl() {
67807
+ const text = curlText.trim();
67808
+ if (!text) return;
67809
+ setError(null);
67810
+ try {
67811
+ const parsed = parseCurl(text);
67812
+ const reqId = v4();
67813
+ const col = {
67814
+ version: "1.0",
67815
+ id: v4(),
67816
+ name: parsed.name || "Imported request",
67817
+ description: "",
67818
+ rootFolder: { id: v4(), name: "root", description: "", folders: [], requestIds: [reqId] },
67819
+ requests: {
67820
+ [reqId]: {
67821
+ id: reqId,
67822
+ name: parsed.name,
67823
+ method: parsed.method,
67824
+ url: parsed.url,
67825
+ headers: parsed.headers,
67826
+ params: parsed.params,
67827
+ auth: parsed.auth,
67828
+ body: parsed.body
67829
+ }
67830
+ }
67831
+ };
67832
+ enterPreview(col);
67833
+ } catch (err) {
67834
+ setError(err instanceof Error ? err.message : String(err));
67835
+ }
67836
+ }
67368
67837
  function toggleId(id2) {
67369
67838
  setChosenIds((prev) => {
67370
67839
  const next = new Set(prev);
@@ -67499,7 +67968,7 @@ function ImportModal({ onImport, onClose }) {
67499
67968
  children: [
67500
67969
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [
67501
67970
  /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-sm font-semibold text-surface-100", children: [
67502
- "Import OpenAPI - ",
67971
+ "Import - ",
67503
67972
  previewCol.name
67504
67973
  ] }),
67505
67974
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -67751,6 +68220,24 @@ function ImportModal({ onImport, onClose }) {
67751
68220
  )
67752
68221
  ] })
67753
68222
  ] }),
68223
+ selected === "curl" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
68224
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-500 uppercase tracking-wider font-medium", children: "Paste a curl command" }),
68225
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
68226
+ "textarea",
68227
+ {
68228
+ value: curlText,
68229
+ onChange: (e) => {
68230
+ setCurlText(e.target.value);
68231
+ setError(null);
68232
+ },
68233
+ placeholder: `curl -X POST https://api.example.com/users \\
68234
+ -H 'Content-Type: application/json' \\
68235
+ -d '{"name":"Ada"}'`,
68236
+ rows: 6,
68237
+ className: "w-full text-xs bg-surface-800 border border-surface-700 rounded px-2.5 py-1.5 focus:outline-none focus:border-blue-500 placeholder-surface-600 font-mono resize-y"
68238
+ }
68239
+ )
68240
+ ] }),
67754
68241
  error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-red-400", children: error2 }),
67755
68242
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 pt-1", children: [
67756
68243
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -67761,7 +68248,15 @@ function ImportModal({ onImport, onClose }) {
67761
68248
  children: "Cancel"
67762
68249
  }
67763
68250
  ),
67764
- /* @__PURE__ */ jsxRuntimeExports.jsx(
68251
+ selected === "curl" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
68252
+ "button",
68253
+ {
68254
+ disabled: !curlText.trim() || loading,
68255
+ onClick: importCurl,
68256
+ 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",
68257
+ children: "Parse"
68258
+ }
68259
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
67765
68260
  "button",
67766
68261
  {
67767
68262
  disabled: !selected || loading,
@@ -72085,7 +72580,7 @@ function App() {
72085
72580
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
72086
72581
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
72087
72582
  "v",
72088
- "0.3.9"
72583
+ "0.4.1"
72089
72584
  ] }),
72090
72585
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
72091
72586
  /* @__PURE__ */ jsxRuntimeExports.jsx(