From 576db4212aa877719071653cb7da4d23fa33a42b Mon Sep 17 00:00:00 2001 From: DeoJin Date: Thu, 19 Mar 2026 04:28:47 +0100 Subject: [PATCH] fix: accept Git Bash-rewritten library IDs Fixes upstash/context7#2277 --- .../cli/src/__tests__/docs-command.test.ts | 21 ++++++++++++++ packages/cli/src/commands/docs.ts | 28 +++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 packages/cli/src/__tests__/docs-command.test.ts diff --git a/packages/cli/src/__tests__/docs-command.test.ts b/packages/cli/src/__tests__/docs-command.test.ts new file mode 100644 index 00000000..67834e9c --- /dev/null +++ b/packages/cli/src/__tests__/docs-command.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "vitest"; + +import { normalizeLibraryId } from "../commands/docs.js"; + +describe("normalizeLibraryId", () => { + test("preserves already valid library IDs", () => { + expect(normalizeLibraryId("/facebook/react")).toBe("/facebook/react"); + expect(normalizeLibraryId("/facebook/react/v19.0.0")).toBe("/facebook/react/v19.0.0"); + }); + + test("normalizes Git Bash rewritten Windows paths into library IDs", () => { + expect(normalizeLibraryId("C:/Program Files/Git/facebook/react")).toBe("/facebook/react"); + expect(normalizeLibraryId("C:/Program Files/Git/vercel/next.js/v15.0.0")).toBe( + "/vercel/next.js/v15.0.0" + ); + }); + + test("leaves unrelated filesystem paths unchanged", () => { + expect(normalizeLibraryId("C:/Users/alice/project/docs")).toBe("C:/Users/alice/project/docs"); + }); +}); diff --git a/packages/cli/src/commands/docs.ts b/packages/cli/src/commands/docs.ts index abe070c6..7ca6897c 100644 --- a/packages/cli/src/commands/docs.ts +++ b/packages/cli/src/commands/docs.ts @@ -10,6 +10,26 @@ import type { LibrarySearchResult, ContextResponse } from "../types.js"; const isTTY = process.stdout.isTTY; +export function normalizeLibraryId(input: string): string { + if (/^\/[^/]+\/.+/.test(input)) { + return input; + } + + const normalizedPath = input.replace(/\\/g, "/"); + const gitBashPrefixMatch = normalizedPath.match(/^[A-Za-z]:\/Program Files\/Git\/(.+)$/i); + + if (!gitBashPrefixMatch) { + return input; + } + + const segments = gitBashPrefixMatch[1].split("/").filter(Boolean); + if (segments.length < 2) { + return input; + } + + return `/${segments.slice(0).join("/")}`; +} + function getReputationLabel(score: number | undefined): "High" | "Medium" | "Low" | "Unknown" { if (score === undefined || score < 0) return "Unknown"; if (score >= 7) return "High"; @@ -120,7 +140,9 @@ async function queryCommand( ): Promise { trackEvent("command", { name: "docs" }); - if (!libraryId.startsWith("/") || !/^\/[^/]+\/[^/]/.test(libraryId)) { + const normalizedLibraryId = normalizeLibraryId(libraryId); + + if (!normalizedLibraryId.startsWith("/") || !/^\/[^/]+\/[^/]/.test(normalizedLibraryId)) { log.error(`Invalid library ID: "${libraryId}"`); log.info(`Expected format: /owner/repo or /owner/repo/version (e.g., /facebook/react)`); log.info(`Run "ctx7 library " to find the correct ID`); @@ -128,13 +150,13 @@ async function queryCommand( return; } - const spinner = isTTY ? ora(`Fetching docs for "${libraryId}"...`).start() : null; + const spinner = isTTY ? ora(`Fetching docs for "${normalizedLibraryId}"...`).start() : null; const accessToken = getAccessToken(); const outputType = options.json ? "json" : "txt"; let result; try { - result = await getLibraryContext(libraryId, query, { type: outputType }, accessToken); + result = await getLibraryContext(normalizedLibraryId, query, { type: outputType }, accessToken); } catch (err) { spinner?.fail(`Error: ${err instanceof Error ? err.message : String(err)}`); if (!spinner) log.error(err instanceof Error ? err.message : String(err));