Implement automatic Cursor download links tracking system

This commit is contained in:
shtse8
2025-02-27 22:53:12 +00:00
parent 403794f38a
commit 7d203b1e75
7 changed files with 393 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
{
"name": "TypeScript + Bun Development",
"image": "mcr.microsoft.com/devcontainers/javascript-node:18",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "18"
}
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"oven.bun-vscode"
]
}
},
"postCreateCommand": "curl -fsSL https://bun.sh/install | bash && npm install",
"remoteUser": "node"
}

View File

@@ -0,0 +1,46 @@
name: Update Cursor Download Links
on:
schedule:
- cron: '0 * * * *' # Run hourly at minute 0
workflow_dispatch: # Allow manual triggering
permissions:
contents: write
jobs:
update-links:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run update script
run: bun src/update-cursor-links.ts
- name: Check for changes
id: git-check
run: |
git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT
- name: Commit and push if changed
if: steps.git-check.outputs.changes == 'true'
run: |
git config --global user.email "github-actions@github.com"
git config --global user.name "GitHub Actions"
git add README.md
git commit -m "Update Cursor download links"
git push

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Build artifacts
dist/
build/
out/
# Bun files
bun.lockb
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea/
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS specific
.DS_Store
Thumbs.db

73
bun.lock Normal file
View File

@@ -0,0 +1,73 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "cursor-downloads-tracker",
"dependencies": {
"axios": "^1.6.0",
},
"devDependencies": {
"@types/node": "^18.18.0",
"bun-types": "latest",
"typescript": "^5.2.2",
},
},
},
"packages": {
"@types/node": ["@types/node@18.19.76", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.8.1", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g=="],
"bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
}
}

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "cursor-downloads-tracker",
"version": "1.0.0",
"description": "Automatically tracks and updates Cursor download links",
"type": "module",
"scripts": {
"build": "tsc",
"start": "bun src/update-cursor-links.ts",
"update": "bun src/update-cursor-links.ts",
"test": "echo \"No tests specified\" && exit 0"
},
"dependencies": {
"axios": "^1.6.0"
},
"devDependencies": {
"@types/node": "^18.18.0",
"typescript": "^5.2.2",
"bun-types": "latest"
}
}

182
src/update-cursor-links.ts Normal file
View File

@@ -0,0 +1,182 @@
import axios from 'axios';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
// Get dirname in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface PlatformInfo {
platforms: string[];
readableNames: string[];
section: string;
}
interface PlatformMap {
[key: string]: PlatformInfo;
}
interface VersionInfo {
url: string;
version: string;
}
interface ResultMap {
[os: string]: {
[platform: string]: VersionInfo;
};
}
const PLATFORMS: PlatformMap = {
windows: {
platforms: ['win32-x64', 'win32-arm64'],
readableNames: ['win32-x64', 'win32-arm64'],
section: 'Windows Installer'
},
mac: {
platforms: ['darwin-universal', 'darwin-x64', 'darwin-arm64'],
readableNames: ['darwin-universal', 'darwin-x64', 'darwin-arm64'],
section: 'Mac Installer'
},
linux: {
platforms: ['linux-x64'],
readableNames: ['linux-x64'],
section: 'Linux Installer'
}
};
/**
* Extract version from URL or filename
*/
function extractVersion(url: string): string {
// For Windows
const winMatch = url.match(/CursorUserSetup-[^-]+-([0-9.]+)\.exe/);
if (winMatch && winMatch[1]) return winMatch[1];
// For other URLs, try to find version pattern
const versionMatch = url.match(/[0-9]+\.[0-9]+\.[0-9]+/);
return versionMatch ? versionMatch[0] : 'Unknown';
}
/**
* Format date as YYYY-MM-DD
*/
function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Fetch latest download URL for a platform
*/
async function fetchLatestDownloadUrl(platform: string): Promise<string | null> {
try {
const response = await axios.get(`https://www.cursor.com/api/download?platform=${platform}&releaseTrack=latest`);
return response.data.downloadUrl;
} catch (error) {
console.error(`Error fetching download URL for platform ${platform}:`, error instanceof Error ? error.message : 'Unknown error');
return null;
}
}
/**
* Update the README.md file with latest Cursor links
*/
async function updateReadme(): Promise<void> {
const readmePath = path.join(process.cwd(), 'README.md');
let readmeContent = fs.readFileSync(readmePath, 'utf8');
// Collect all URLs and versions
const results: ResultMap = {};
let latestVersion = '0.0.0';
const currentDate = formatDate(new Date());
// Fetch all platform download URLs
for (const [osKey, osData] of Object.entries(PLATFORMS)) {
results[osKey] = {};
for (let i = 0; i < osData.platforms.length; i++) {
const platform = osData.platforms[i];
const url = await fetchLatestDownloadUrl(platform);
if (url) {
const version = extractVersion(url);
results[osKey][platform] = { url, version };
// Track the highest version number
if (version !== 'Unknown' && version > latestVersion) {
latestVersion = version;
}
}
}
}
// Update the date in the Cursor AI IDE section
const ideUpdateRegex = /(Official Download Link for The latest version from `\[Cursor AI IDE\]'s \[Check for Updates\.\.\.\]` \(on `)([^`]+)(`\) is:)/;
readmeContent = readmeContent.replace(ideUpdateRegex, `$1${currentDate}$3`);
// Also update the date in the website section
const websiteUpdateRegex = /(Official Download Link for The latest version from \[Cursor AI's Website\]\(https:\/\/www\.cursor\.com\/downloads\) \(on `)([^`]+)(`\) is:)/;
readmeContent = readmeContent.replace(websiteUpdateRegex, `$1${currentDate}$3`);
// Check if the latest version already exists in the table
const versionRowRegex = new RegExp(`\\| ${latestVersion} \\|`);
if (!versionRowRegex.test(readmeContent)) {
// Add new row to the table for the latest version
const tableStartRegex = /\| Version \| Date \| Mac Installer \| Windows Installer \| Linux Installer \|\n\| --- \| --- \| --- \| --- \| --- \|/;
// Generate Mac links section
let macLinks = '';
if (results.mac) {
const macPlatforms = ['darwin-universal', 'darwin-x64', 'darwin-arm64'];
const macUrls = macPlatforms.map(platform => {
if (results.mac[platform] && results.mac[platform].url) {
return `[${platform}](${results.mac[platform].url})`;
}
return null;
}).filter(Boolean);
macLinks = macUrls.join(' <br>');
}
// Generate Windows links section
let windowsLinks = '';
if (results.windows) {
const winPlatforms = ['win32-x64', 'win32-arm64'];
const winUrls = winPlatforms.map(platform => {
if (results.windows[platform] && results.windows[platform].url) {
return `[${platform}](${results.windows[platform].url})`;
}
return null;
}).filter(Boolean);
windowsLinks = winUrls.join('<br>');
}
// Generate Linux link
let linuxLinks = 'Not Ready';
if (results.linux && results.linux['linux-x64'] && results.linux['linux-x64'].url) {
linuxLinks = `[linux-x64](${results.linux['linux-x64'].url})`;
}
// New table row
const newRow = `\n| ${latestVersion} | ${currentDate} | ${macLinks} | ${windowsLinks} | ${linuxLinks} |`;
// Insert the new row after the table header
readmeContent = readmeContent.replace(tableStartRegex, `$&${newRow}`);
}
// Save the updated README
fs.writeFileSync(readmePath, readmeContent);
console.log(`README.md updated with Cursor version ${latestVersion}`);
}
// Run the update
updateReadme().catch(error => {
console.error('Error updating README:', error instanceof Error ? error.message : 'Unknown error');
process.exit(1);
});

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"moduleResolution": "node16",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"outDir": "dist",
"forceConsistentCasingInFileNames": true,
"lib": ["esnext"],
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}