Skip to content

Commit 532d8fd

Browse files
committed
fix: correct Cursor CLI download URL resolution and version input
1 parent 3cb0460 commit 532d8fd

6 files changed

Lines changed: 88 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"cursor-action": patch
3+
---
4+
5+
Fix Cursor CLI download URLs: resolve `latest` via the lab endpoint, use `windows` in artifact paths on Win32, and allow pinning lab build ids in `cursor-version` input validation.

__tests__/input.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ describe("getInputs", () => {
5555
expect(inputs.cursorVersion).toBe("v1.2.3");
5656
});
5757

58+
it("accepts a pinned Cursor lab build id", () => {
59+
setupInputs({ "cursor-version": "2026.03.20-44cb435" });
60+
const inputs = getInputs();
61+
expect(inputs.cursorVersion).toBe("2026.03.20-44cb435");
62+
});
63+
64+
it("accepts a pinned lab build id with v prefix", () => {
65+
setupInputs({ "cursor-version": "v2026.03.20-44cb435" });
66+
const inputs = getInputs();
67+
expect(inputs.cursorVersion).toBe("v2026.03.20-44cb435");
68+
});
69+
5870
it("throws on invalid permission value", () => {
5971
setupInputs({ permissions: "superuser" });
6072
expect(() => getInputs()).toThrow(/Invalid 'permissions'/);

__tests__/installer.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { beforeEach, describe, expect, it, mock } from "bun:test";
2+
3+
import * as actionsHttpClient from "@actions/http-client";
4+
5+
const mockHttpGet = mock();
6+
7+
class MockHttpClient {
8+
public get = mockHttpGet;
9+
}
10+
11+
mock.module("@actions/http-client", () => ({
12+
...actionsHttpClient,
13+
HttpClient: MockHttpClient,
14+
}));
15+
16+
const { buildDownloadUrl, resolveVersion } = await import("../src/installer");
17+
18+
describe("resolveVersion", () => {
19+
beforeEach(() => {
20+
mock.clearAllMocks();
21+
});
22+
23+
it("resolves latest from Cursor endpoint and normalizes v prefix", () => {
24+
mockHttpGet.mockResolvedValue({
25+
readBody: mock(() => Promise.resolve("v2026.03.20-44cb435\n")),
26+
});
27+
28+
expect(resolveVersion("latest")).resolves.toBe("2026.03.20-44cb435");
29+
expect(mockHttpGet).toHaveBeenCalledWith(
30+
"https://downloads.cursor.com/lab/latest-version"
31+
);
32+
});
33+
34+
it("returns pinned version without calling network", () => {
35+
expect(resolveVersion("v1.2.3")).resolves.toBe("1.2.3");
36+
expect(mockHttpGet).not.toHaveBeenCalled();
37+
});
38+
});
39+
40+
describe("buildDownloadUrl", () => {
41+
it("uses windows path segment and zip extension for win32", () => {
42+
expect(buildDownloadUrl("1.2.3", "win32", "x64")).toBe(
43+
"https://downloads.cursor.com/lab/1.2.3/windows/x64/agent-cli-package.zip"
44+
);
45+
});
46+
47+
it("uses tar.gz for linux and darwin", () => {
48+
expect(buildDownloadUrl("1.2.3", "linux", "arm64")).toBe(
49+
"https://downloads.cursor.com/lab/1.2.3/linux/arm64/agent-cli-package.tar.gz"
50+
);
51+
expect(buildDownloadUrl("1.2.3", "darwin", "x64")).toBe(
52+
"https://downloads.cursor.com/lab/1.2.3/darwin/x64/agent-cli-package.tar.gz"
53+
);
54+
});
55+
});

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ branding:
88

99
inputs:
1010
cursor-version:
11-
description: "Version of the Cursor CLI to install (e.g. '0.1.0' or 'latest')"
11+
description: "Cursor CLI build to install: 'latest' or exact lab id (e.g. from https://downloads.cursor.com/lab/latest-version, like '2026.03.20-44cb435')"
1212
required: false
1313
default: "latest"
1414

src/input.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type { ActionInputs, Permission } from "./types";
44

55
const VALID_PERMISSIONS: Permission[] = ["read-only", "read-write", "full"];
66

7+
/** Cursor CLI artifacts use paths like `2026.03.20-44cb435`, not npm-style semver only. */
8+
const PINNED_CURSOR_VERSION_RE = /^\d+\.\d+\.\d+(?:-[A-Za-z0-9]+)?$/;
9+
710
/**
811
* Reads, validates, and returns all action inputs.
912
* @returns The validated action inputs.
@@ -43,14 +46,17 @@ export const getInputs = (): ActionInputs => {
4346
);
4447
}
4548

46-
// Validate version format (semver or "latest")
49+
// Validate version: "latest" or a published lab build id (see official install script / latest-version)
50+
const normalizedCursorVersion = cursorVersion.replace(/^v/, "");
4751
if (
4852
cursorVersion !== "latest" &&
49-
!/^\d+\.\d+\.\d+$/.test(cursorVersion.replace(/^v/, ""))
53+
!PINNED_CURSOR_VERSION_RE.test(normalizedCursorVersion)
5054
) {
5155
throw new Error(
5256
`Invalid 'cursor-version' value: '${cursorVersion}'. ` +
53-
`Use 'latest' or a semver string like '1.2.3'.`
57+
`Use 'latest', or pin an exact build id that exists on Cursor's CDN ` +
58+
`(e.g. the value from https://downloads.cursor.com/lab/latest-version, ` +
59+
`like '2026.03.20-44cb435'). Arbitrary values like '1.2.3' are not published and will fail to download.`
5460
);
5561
}
5662

src/installer.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ const getArch = (): Arch => {
4848
* Resolves "latest" to a concrete semver version string by querying
4949
* Cursor's version endpoint.
5050
*/
51-
const resolveVersion = async (version: string): Promise<string> => {
52-
if (version === "latest") {
53-
// Strip leading 'v' if present for consistency
51+
export const resolveVersion = async (version: string): Promise<string> => {
52+
if (version !== "latest") {
53+
// Keep pinned versions local; no network call needed.
5454
return version.replace(/^v/, "");
5555
}
5656

@@ -74,13 +74,14 @@ const resolveVersion = async (version: string): Promise<string> => {
7474
* Builds the download URL for the Cursor CLI tarball.
7575
* Pattern: https://downloads.cursor.com/lab/{version}/{platform}/{arch}/agent-cli-package.tar.gz
7676
*/
77-
const buildDownloadUrl = (
77+
export const buildDownloadUrl = (
7878
version: string,
7979
platform: Platform,
8080
arch: Arch
8181
): string => {
8282
const ext = platform === "win32" ? "zip" : "tar.gz";
83-
return `${CURSOR_DOWNLOAD_BASE}/${version}/${platform}/${arch}/agent-cli-package.${ext}`;
83+
const platformSegment = platform === "win32" ? "windows" : platform;
84+
return `${CURSOR_DOWNLOAD_BASE}/${version}/${platformSegment}/${arch}/agent-cli-package.${ext}`;
8485
};
8586

8687
const buildCacheKey = (

0 commit comments

Comments
 (0)