From 131b7e5ab1413a2262fdf51f9c173a9cee62e275 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Viet <123613986+NgoQuocViet2001@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:09:04 +0700 Subject: [PATCH] fix(importer-curl): read combined short flags as separate options (#577) --- plugins/importer-curl/src/index.ts | 53 +++++++++++++++++++---- plugins/importer-curl/tests/index.test.ts | 30 +++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/plugins/importer-curl/src/index.ts b/plugins/importer-curl/src/index.ts index d03e31a2..dec52c65 100644 --- a/plugins/importer-curl/src/index.ts +++ b/plugins/importer-curl/src/index.ts @@ -40,6 +40,44 @@ const SUPPORTED_FLAGS = [ const BOOLEAN_FLAGS = ["G", "get", "digest"]; +// Short flags that consume a value, derived so this stays in step with the +// tables above. +const VALUE_SHORT_FLAGS = SUPPORTED_FLAGS.flat().filter( + (name) => name.length === 1 && !BOOLEAN_FLAGS.includes(name), +); + +/** + * Expand a short-flag token into separate arguments. + * + * curl reads a short cluster left to right, one option per character, until an + * option that takes a value — that one swallows the rest of the cluster. So + * `-XPOST` is `-X POST`, but `-fsSL` is four boolean flags rather than `-f` + * plus a value. Splitting unconditionally after the first character left + * `sSL` as a positional argument, and the first positional is read as the URL: + * `curl -fsSL https://example.com` imported with a URL of `sSL`. + */ +function expandShortFlags(token: string): string[] { + if (!token.startsWith("-") || token.startsWith("--") || token.length <= 2) { + return [token]; + } + + const expanded: string[] = []; + for (let i = 1; i < token.length; i++) { + const name = token[i] ?? ""; + expanded.push(`-${name}`); + + if (VALUE_SHORT_FLAGS.includes(name)) { + const value = token.slice(i + 1); + if (value) { + expanded.push(value); + } + break; + } + } + + return expanded; +} + type FlagValue = string | boolean; type FlagsByName = Record; @@ -154,14 +192,7 @@ export function convertCurl(rawData: string) { const commands: string[][] = splitCommands(rawData).map((cmd) => { const tokens = split(cmd); - - // Break up squished arguments like `-XPOST` into `-X POST` - return tokens.flatMap((token) => { - if (token.startsWith("-") && !token.startsWith("--") && token.length > 2) { - return [token.slice(0, 2), token.slice(2)]; - } - return token; - }); + return tokens.flatMap(expandShortFlags); }); const workspace: ExportResources["workspaces"][0] = { @@ -502,7 +533,11 @@ function importCommand(parseEntries: string[], workspaceId: string) { if (graphqlBody != null) { bodyType = "graphql"; body = graphqlBody; - } else if (mimeType === "application/json" || mimeType === "text/xml" || mimeType === "text/plain") { + } else if ( + mimeType === "application/json" || + mimeType === "text/xml" || + mimeType === "text/plain" + ) { bodyType = mimeType; body = { text }; } else { diff --git a/plugins/importer-curl/tests/index.test.ts b/plugins/importer-curl/tests/index.test.ts index 6852ab09..3c2d6dc0 100644 --- a/plugins/importer-curl/tests/index.test.ts +++ b/plugins/importer-curl/tests/index.test.ts @@ -16,6 +16,36 @@ describe("importer-curl", () => { }); }); + // A short cluster is one option per character until one that takes a value. + // `-fsSL` is four boolean flags, so nothing in it is a positional argument -- + // the URL used to come out as "sSL". + test("Imports combined short flags", () => { + expect(convertCurl("curl -fsSL https://yaak.app")).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: "https://yaak.app", + }), + ], + }, + }); + }); + + test("Imports a combined short cluster ending in a value flag", () => { + expect(convertCurl("curl -sSXPOST https://yaak.app")).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: "https://yaak.app", + method: "POST", + }), + ], + }, + }); + }); + test("Explicit URL", () => { expect(convertCurl("curl --url https://yaak.app")).toEqual({ resources: {