Skip to content

Commit f16a0cd

Browse files
committed
fix: install full Cursor CLI package and bust incomplete caches
1 parent 277c9f5 commit f16a0cd

2 files changed

Lines changed: 59 additions & 17 deletions

File tree

.changeset/fix-cursor-cli-download-urls.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"cursor-action": patch
33
---
44

5-
Fix Cursor CLI download URLs: resolve `latest` via the lab endpoint (validate HTTP 200 and lab id shape), fall back to parsing `https://cursor.com/install` when the lab `latest-version` URL returns 403, use `windows` in artifact paths on Win32, and allow pinning lab build ids in `cursor-version` input validation.
5+
Fix Cursor CLI download URLs: resolve `latest` via the lab endpoint (validate HTTP 200 and lab id shape), fall back to parsing `https://cursor.com/install` when the lab `latest-version` URL returns 403, use `windows` in artifact paths on Win32, and allow pinning lab build ids in `cursor-version` input validation. Install the full extracted agent package (launcher + bundled `node`, etc.) instead of only the `cursor-agent` file, and bump cache keys so old incomplete installs are not reused.

src/installer.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { execSync } from "node:child_process";
2+
import { promises as fs } from "node:fs";
13
import { homedir } from "node:os";
24
import path from "node:path";
35

46
import { restoreCache, saveCache } from "@actions/cache";
57
import { addPath, debug, info, warning } from "@actions/core";
68
import { HttpClient } from "@actions/http-client";
7-
import { mkdirP, mv } from "@actions/io";
9+
import { mkdirP } from "@actions/io";
810
import {
911
cacheDir,
1012
downloadTool,
@@ -131,18 +133,24 @@ export const buildDownloadUrl = (
131133
return `${CURSOR_DOWNLOAD_BASE}/${version}/${platformSegment}/${arch}/agent-cli-package.${ext}`;
132134
};
133135

136+
/** Bumped when install layout changes (e.g. copy full package vs single binary). */
137+
const CURSOR_CLI_CACHE_LAYOUT = "bundle-v1";
138+
134139
const buildCacheKey = (
135140
version: string,
136141
platform: Platform,
137142
arch: Arch
138-
): string => `cursor-cli-${platform}-${arch}-${version}`;
143+
): string =>
144+
`cursor-cli-${platform}-${arch}-${version}-${CURSOR_CLI_CACHE_LAYOUT}`;
145+
146+
/** Tool-cache version must match find() — suffix invalidates old single-file caches. */
147+
const toolCacheVersion = (labVersion: string): string =>
148+
`${labVersion}-${CURSOR_CLI_CACHE_LAYOUT}`;
139149

140150
const getBinaryName = (): string =>
141151
process.platform === "win32" ? "cursor-agent.exe" : "cursor-agent";
142152

143153
const findBinary = async (dir: string, binaryName: string): Promise<string> => {
144-
const { promises: fs } = await import("node:fs");
145-
146154
const search = async (currentDir: string): Promise<string | null> => {
147155
const entries = await fs.readdir(currentDir, { withFileTypes: true });
148156
for (const entry of entries) {
@@ -169,6 +177,47 @@ const findBinary = async (dir: string, binaryName: string): Promise<string> => {
169177
return result;
170178
};
171179

180+
/**
181+
* Copies the extracted agent package (cursor-agent launcher + bundled node, etc.) into installDir.
182+
*/
183+
const installAgentPackage = async (
184+
extractedPath: string,
185+
installDir: string,
186+
binaryName: string
187+
): Promise<void> => {
188+
const binarySource = await findBinary(extractedPath, binaryName);
189+
const packageRoot = path.dirname(binarySource);
190+
191+
await mkdirP(installDir);
192+
const entries = await fs.readdir(packageRoot, { withFileTypes: true });
193+
for (const entry of entries) {
194+
const from = path.join(packageRoot, entry.name);
195+
const to = path.join(installDir, entry.name);
196+
await fs.cp(from, to, { recursive: true });
197+
}
198+
199+
const mainBin = path.join(installDir, binaryName);
200+
try {
201+
await fs.access(mainBin);
202+
if (process.platform !== "win32") {
203+
execSync(`chmod +x "${mainBin}"`);
204+
}
205+
} catch {
206+
// ignore
207+
}
208+
209+
const nodeName = process.platform === "win32" ? "node.exe" : "node";
210+
const nodePath = path.join(installDir, nodeName);
211+
try {
212+
const st = await fs.stat(nodePath);
213+
if (st.isFile() && process.platform !== "win32") {
214+
execSync(`chmod +x "${nodePath}"`);
215+
}
216+
} catch {
217+
// bundled node may be absent on some platforms
218+
}
219+
};
220+
172221
/**
173222
* Installer entry point
174223
* @returns Returns { binPath, cacheHit } where binPath is the directory added to PATH
@@ -186,8 +235,10 @@ export const installCursorCLI = async (
186235

187236
info(`Installing Cursor CLI v${resolvedVersion} (${platform}/${arch})`);
188237

238+
const tcVersion = toolCacheVersion(resolvedVersion);
239+
189240
// Check @actions/tool-cache first (within-job cache)
190-
const cachedPath = find("cursor-agent", resolvedVersion, arch);
241+
const cachedPath = find("cursor-agent", tcVersion, arch);
191242
if (cachedPath) {
192243
info(`Found Cursor CLI in tool cache: ${cachedPath}`);
193244
addPath(cachedPath);
@@ -224,17 +275,8 @@ export const installCursorCLI = async (
224275
);
225276
}
226277

227-
// Move binary to install dir
228-
await mkdirP(installDir);
229278
const binaryName = getBinaryName();
230-
231-
const binarySource = await findBinary(extractedPath, binaryName);
232-
await mv(binarySource, path.join(installDir, binaryName));
233-
234-
if (platform !== "win32") {
235-
const { execSync } = await import("node:child_process");
236-
execSync(`chmod +x "${path.join(installDir, binaryName)}"`);
237-
}
279+
await installAgentPackage(extractedPath, installDir, binaryName);
238280

239281
// Cache the install dir for future jobs
240282
try {
@@ -246,7 +288,7 @@ export const installCursorCLI = async (
246288
}
247289

248290
// Also register in tool-cache for within-job reuse
249-
await cacheDir(installDir, "cursor-agent", resolvedVersion, arch);
291+
await cacheDir(installDir, "cursor-agent", tcVersion, arch);
250292

251293
addPath(installDir);
252294
info(`Cursor CLI v${resolvedVersion} installed to ${installDir}`);

0 commit comments

Comments
 (0)