diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..670f81a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} \ No newline at end of file diff --git a/.github/workflows/update-cursor-links.yml b/.github/workflows/update-cursor-links.yml new file mode 100644 index 0000000..faf0682 --- /dev/null +++ b/.github/workflows/update-cursor-links.yml @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df3fcda --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..8ede55c --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0717aa2 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/update-cursor-links.ts b/src/update-cursor-links.ts new file mode 100644 index 0000000..1e9524f --- /dev/null +++ b/src/update-cursor-links.ts @@ -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 { + 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 { + 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('
'); + } + + // 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('
'); + } + + // 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); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d3a5c95 --- /dev/null +++ b/tsconfig.json @@ -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"] +} \ No newline at end of file