this repo has no description

my first pass code review

+155 -403
+11 -26
README.md
··· 2 2 3 3 **Simple GitHub release installer.** 4 4 5 - A Node.js CLI tool that automatically downloads and installs GitHub release binaries for your platform without requiring manual CLI installation. 5 + A Node.js CLI to automatically download and install GitHub release binaries for your platform. 6 + Helpful for installing non-npm tools. 6 7 7 8 ## Features 8 9 9 - - 🎯 **Automatic platform detection** - Detects your OS and architecture 10 - - 🔍 **Smart asset matching** - Finds the right binary for your platform 11 - - 📥 **Download & extract** - Handles `.tar.gz` and `.zip` archives 12 - - ⚡ **Zero dependencies** - Uses built-in Node.js modules 13 - - 🔧 **Configurable** - CLI options and package.json configuration 14 - - 🌍 **Cross-platform** - Works on macOS, Linux, and Windows 10 + - **Automatically detect** OS and architecture 11 + - **Download & extract** `.tar.gz` and `.zip` archives 12 + - **Configurable** platform mappings, binary name, output dir 13 + - **Zero dependencies** (requires Node.js 22+) 14 + - **Cross-platform** 15 15 16 16 ## Installation 17 17 18 18 ```bash 19 - npm install -g release-installer 19 + npm i release-installer 20 20 ``` 21 21 22 22 ## Usage ··· 48 48 -h, --help Show this help message 49 49 ``` 50 50 51 + <!-- 51 52 ### Package.json Configuration 52 53 53 54 You can also configure releases in your `package.json`: ··· 63 64 } 64 65 } 65 66 ``` 67 + --> 66 68 67 69 ## Platform Detection 68 70 69 - The tool automatically detects your platform and architecture, then matches against common naming patterns: 71 + The tool tries to detect your platform and architecture, then matches against common naming patterns: 70 72 71 73 - **macOS**: `apple-darwin`, `macos`, `darwin` 72 74 - **Linux**: `linux-gnu`, `linux`, `unknown-linux` ··· 92 94 release-installer owner/repo v1.0.0 --platform-map='{"darwin-x64":"app-macos.tar.gz"}' 93 95 ``` 94 96 95 - ## Error Handling 96 - 97 - The tool provides clear error messages for common issues: 98 - 99 - - Repository or release not found 100 - - No matching asset for your platform 101 - - Download or extraction failures 102 - - Permission errors 103 - 104 97 ## Development 105 98 106 99 ```bash 107 - # Clone and install 108 - git clone https://github.com/tbeseda/release-installer.git 109 - cd release-installer 110 100 npm install 111 101 112 - # Build 113 102 npm run build 114 103 115 - # Run tests 116 104 npm test 117 - 118 - # Lint 119 - npm run lint 120 105 ```
+2 -1
biome.json
··· 11 11 "formatter": { 12 12 "enabled": true, 13 13 "indentStyle": "space", 14 - "indentWidth": 2 14 + "indentWidth": 2, 15 + "lineWidth": 120 15 16 }, 16 17 "linter": { 17 18 "enabled": true,
+1
package.json
··· 20 20 "scripts": { 21 21 "lint": "biome check --write .", 22 22 "build": "rm -rf dist && tsc", 23 + "postbuild": "chmod +x dist/cli.js", 23 24 "test": "tsx --test", 24 25 "posttest": "npm run lint", 25 26 "prepublishOnly": "npm run build"
+10 -7
src/cli.ts
··· 17 17 }) 18 18 19 19 if (values.help) { 20 - console.log(` 20 + console.log( 21 + ` 21 22 Usage: release-installer <owner/repo> <version> [options] 22 23 23 24 Options: ··· 31 32 Examples: 32 33 release-installer getzola/zola v0.20.0 33 34 release-installer getzola/zola v0.20.0 --bin-name=zola --output=./bin 34 - `) 35 + `.trim(), 36 + ) 35 37 process.exit(0) 36 38 } 37 39 38 40 const [repo, version] = positionals 41 + const { 'bin-name': binName, output: outputDir, force, verbose } = values 39 42 40 43 if (!repo || !version) { 41 44 console.error('Error: Both repository and version are required') ··· 49 52 if (values['platform-map']) { 50 53 try { 51 54 platformMap = JSON.parse(values['platform-map']) 52 - } catch (_parseError) { 55 + } catch { 53 56 console.error('Error: Invalid JSON in platform-map parameter') 54 57 console.error('Expected format: {"platform-arch": "asset-name"}') 55 58 process.exit(1) ··· 57 60 } 58 61 59 62 await installRelease(repo, version, { 60 - binName: values['bin-name'], 61 - outputDir: values.output, 63 + binName, 64 + outputDir, 62 65 platformMap, 63 - force: values.force, 64 - verbose: values.verbose, 66 + force, 67 + verbose, 65 68 }) 66 69 } catch (error) { 67 70 console.error('Error:', error instanceof Error ? error.message : error)
+15 -42
src/extract.ts
··· 1 1 import { spawn } from 'node:child_process' 2 2 import { createReadStream, createWriteStream } from 'node:fs' 3 - import { access } from 'node:fs/promises' 3 + import { access, mkdir } from 'node:fs/promises' 4 4 import { dirname, join, resolve } from 'node:path' 5 5 import { pipeline } from 'node:stream/promises' 6 6 import { createGunzip } from 'node:zlib' ··· 22 22 // Extract filename (first 100 bytes, null-terminated) 23 23 const nameBytes = buffer.subarray(0, 100) 24 24 const nameEnd = nameBytes.indexOf(0) 25 - const name = nameBytes 26 - .subarray(0, nameEnd > 0 ? nameEnd : 100) 27 - .toString('utf8') 25 + const name = nameBytes.subarray(0, nameEnd > 0 ? nameEnd : 100).toString('utf8') 28 26 29 27 // Extract file size (12 bytes at offset 124, octal format) 30 - const sizeStr = buffer 31 - .subarray(124, 136) 32 - .toString('utf8') 33 - .trim() 34 - .replace(/\0/g, '') 28 + const sizeStr = buffer.subarray(124, 136).toString('utf8').trim().replace(/\0/g, '') 35 29 const size = sizeStr ? parseInt(sizeStr, 8) : 0 36 30 37 31 // Extract file type (1 byte at offset 156) ··· 41 35 return { name, size, type } 42 36 } 43 37 44 - export async function extractTarGz( 45 - archivePath: string, 46 - outputDir: string, 47 - ): Promise<void> { 38 + export async function extractTarGz(archivePath: string, outputDir: string): Promise<void> { 48 39 const resolvedArchivePath = resolve(archivePath) 49 40 const resolvedOutputDir = resolve(outputDir) 50 41 ··· 54 45 throw new Error(`Archive file does not exist: ${resolvedArchivePath}`) 55 46 } 56 47 57 - const fs = await import('node:fs/promises') 58 - await fs.mkdir(resolvedOutputDir, { recursive: true }) 48 + await mkdir(resolvedOutputDir, { recursive: true }) 59 49 60 50 // Create streams for decompression 61 51 const readStream = createReadStream(resolvedArchivePath) ··· 84 74 // Process any queued chunks 85 75 while (chunks.length > 0) { 86 76 const nextChunk = chunks.shift() 87 - if (nextChunk) { 88 - buffer = Buffer.concat([buffer, nextChunk]) 89 - } 77 + if (nextChunk) buffer = Buffer.concat([buffer, nextChunk]) 90 78 } 91 79 92 80 try { ··· 105 93 const outputPath = join(resolvedOutputDir, header.name) 106 94 107 95 // Ensure directory exists 108 - await fs.mkdir(dirname(outputPath), { recursive: true }) 96 + await mkdir(dirname(outputPath), { recursive: true }) 109 97 currentFile.stream = createWriteStream(outputPath) 110 98 } 111 99 ··· 182 170 }) 183 171 } 184 172 185 - export async function extractZip( 186 - archivePath: string, 187 - outputDir: string, 188 - ): Promise<void> { 173 + export async function extractZip(archivePath: string, outputDir: string): Promise<void> { 189 174 // Validate paths to prevent command injection 190 175 const resolvedArchivePath = resolve(archivePath) 191 176 const resolvedOutputDir = resolve(outputDir) ··· 196 181 throw new Error(`Archive file does not exist: ${resolvedArchivePath}`) 197 182 } 198 183 199 - const fs = await import('node:fs/promises') 200 - await fs.mkdir(resolvedOutputDir, { recursive: true }) 184 + await mkdir(resolvedOutputDir, { recursive: true }) 201 185 202 186 // Try multiple zip extraction tools for better compatibility 203 187 const zipTools = [ ··· 221 205 for (const tool of zipTools) { 222 206 try { 223 207 await new Promise<void>((resolve, reject) => { 224 - const proc = spawn(tool.command, tool.args, { 225 - stdio: ['ignore', 'pipe', 'pipe'], 226 - }) 227 - 208 + const proc = spawn(tool.command, tool.args, { stdio: ['ignore', 'pipe', 'pipe'] }) 228 209 let stderr = '' 229 210 230 211 proc.stderr?.on('data', (data) => { ··· 235 216 if (code === 0) { 236 217 resolve() 237 218 } else { 238 - reject( 239 - new Error(`${tool.command} failed with code ${code}: ${stderr}`), 240 - ) 219 + reject(new Error(`${tool.command} failed with code ${code}: ${stderr}`)) 241 220 } 242 221 }) 243 222 ··· 248 227 249 228 // If we get here, extraction succeeded 250 229 return 251 - } catch (_error) {} 230 + } catch {} 252 231 } 253 232 254 233 // If all tools failed 255 - throw new Error( 256 - 'Failed to extract zip file. Please ensure unzip, PowerShell, or 7z is available on your system.', 257 - ) 234 + throw new Error('Failed to extract zip file. Please ensure unzip, PowerShell, or 7z is available on your system.') 258 235 } 259 236 260 - export async function extractArchive( 261 - archivePath: string, 262 - outputDir: string, 263 - ): Promise<void> { 264 - const fs = await import('node:fs/promises') 265 - await fs.mkdir(outputDir, { recursive: true }) 237 + export async function extractArchive(archivePath: string, outputDir: string): Promise<void> { 238 + await mkdir(outputDir, { recursive: true }) 266 239 267 240 if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) { 268 241 await extractTarGz(archivePath, outputDir)
+20 -52
src/github.ts
··· 1 + import { createWriteStream } from 'node:fs' 2 + import { unlink } from 'node:fs/promises' 3 + import { Readable } from 'node:stream' 4 + import { pipeline } from 'node:stream/promises' 5 + import type { ReadableStream } from 'node:stream/web' 1 6 import type { ReleaseInfo } from './index.js' 2 7 3 - export async function fetchReleaseInfo( 4 - repo: string, 5 - version: string, 6 - ): Promise<ReleaseInfo> { 8 + export async function fetchReleaseInfo(repo: string, version: string): Promise<ReleaseInfo> { 7 9 const url = `https://api.github.com/repos/${repo}/releases/tags/${version}` 8 10 9 11 const response = await fetch(url, { 10 - headers: { 11 - 'User-Agent': 'release-installer', 12 - Accept: 'application/vnd.github.v3+json', 13 - }, 12 + headers: { Accept: 'application/vnd.github.v3+json' }, 14 13 }) 15 14 16 15 if (!response.ok) { 17 - if (response.status === 404) { 18 - throw new Error(`Release ${version} not found for repository ${repo}`) 19 - } 20 - throw new Error( 21 - `Failed to fetch release info: ${response.status} ${response.statusText}`, 22 - ) 16 + if (response.status === 404) throw new Error(`Release ${version} not found for repository ${repo}`) 17 + throw new Error(`Failed to fetch release info: ${response.status} ${response.statusText}`) 23 18 } 24 19 25 20 const data = await response.json() 26 21 27 22 return { 28 23 tag_name: data.tag_name, 29 - assets: data.assets.map( 30 - (asset: { 31 - name: string 32 - browser_download_url: string 33 - size: number 34 - }) => ({ 35 - name: asset.name, 36 - download_url: asset.browser_download_url, 37 - size: asset.size, 38 - }), 39 - ), 24 + assets: data.assets.map((asset: { name: string; browser_download_url: string; size: number }) => ({ 25 + name: asset.name, 26 + download_url: asset.browser_download_url, 27 + size: asset.size, 28 + })), 40 29 } 41 30 } 42 31 43 - export async function downloadAsset( 44 - url: string, 45 - outputPath: string, 46 - ): Promise<void> { 47 - const response = await fetch(url, { 48 - headers: { 49 - 'User-Agent': 'release-installer', 50 - }, 51 - }) 52 - 53 - if (!response.ok) { 54 - throw new Error( 55 - `Failed to download asset: ${response.status} ${response.statusText}`, 56 - ) 57 - } 32 + export async function downloadAsset(url: string, outputPath: string): Promise<void> { 33 + const response = await fetch(url) 58 34 59 - if (!response.body) { 60 - throw new Error('Response body is empty') 61 - } 35 + if (!response.ok) throw new Error(`Failed to download asset: ${response.status} ${response.statusText}`) 36 + if (!response.body) throw new Error('GitHub API response body is empty') 62 37 63 38 // Use streaming for better memory efficiency 64 - const { createWriteStream } = await import('node:fs') 65 - const { pipeline } = await import('node:stream/promises') 66 - const { Readable } = await import('node:stream') 67 - 68 39 const fileStream = createWriteStream(outputPath) 69 - const readableStream = Readable.fromWeb(response.body) 40 + const readableStream = Readable.fromWeb(response.body as ReadableStream) 70 41 71 42 try { 72 43 await pipeline(readableStream, fileStream) 73 44 } catch (error) { 74 45 // Clean up partial file on error 75 46 try { 76 - const { unlink } = await import('node:fs/promises') 77 47 await unlink(outputPath) 78 48 } catch { 79 49 // Ignore cleanup errors 80 50 } 81 - throw new Error( 82 - `Failed to download asset: ${error instanceof Error ? error.message : 'Unknown error'}`, 83 - ) 51 + throw new Error(`Failed to download asset: ${error instanceof Error ? error.message : 'Unknown error'}`) 84 52 } 85 53 }
+50 -99
src/index.ts
··· 1 + import { access, chmod, mkdir, readdir, stat, unlink } from 'node:fs/promises' 2 + import { basename, extname, join, resolve } from 'node:path' 3 + import { extractArchive } from './extract.js' 4 + import { downloadAsset, fetchReleaseInfo } from './github.js' 5 + import { findBestAsset, getPlatformInfo } from './platform.js' 6 + 1 7 export interface InstallOptions { 2 8 binName?: string 3 9 outputDir?: string ··· 17 23 assets: ReleaseAsset[] 18 24 } 19 25 20 - import { access, chmod, readdir, stat } from 'node:fs/promises' 21 - import { basename, extname, join, resolve } from 'node:path' 22 - import { extractArchive } from './extract.js' 23 - import { downloadAsset, fetchReleaseInfo } from './github.js' 24 - import { findBestAsset, getPlatformInfo } from './platform.js' 25 - 26 - export async function installRelease( 27 - repo: string, 28 - version: string, 29 - options: InstallOptions = {}, 30 - ): Promise<void> { 31 - const { 32 - binName, 33 - outputDir = './bin', 34 - platformMap, 35 - force = false, 36 - verbose = false, 37 - } = options 26 + export async function installRelease(repo: string, version: string, options: InstallOptions = {}): Promise<void> { 27 + const { binName, outputDir = './bin', platformMap, force = false, verbose: v = false } = options 38 28 39 - if (verbose) { 40 - console.log(`Installing ${repo} ${version}...`) 41 - } 29 + if (v) console.log(`Installing ${repo} ${version}...`) 42 30 43 31 // Fetch release info 44 32 const releaseInfo = await fetchReleaseInfo(repo, version) 45 33 46 - if (verbose) { 47 - console.log( 48 - `Found ${releaseInfo.assets.length} assets for ${releaseInfo.tag_name}`, 49 - ) 50 - } 34 + if (v) console.log(`Found ${releaseInfo.assets.length} assets for ${releaseInfo.tag_name}`) 51 35 52 36 // Get platform info 53 37 const platformInfo = getPlatformInfo() 54 38 55 - if (verbose) { 56 - console.log(`Platform: ${platformInfo.combined}`) 57 - } 39 + if (v) console.log(`Platform: ${platformInfo.combined}`) 58 40 59 41 // Find matching asset 60 - const assetNames = releaseInfo.assets.map((a) => a.name) 61 - let selectedAsset: string | null = null 42 + const releaseAssetNames = releaseInfo.assets.map((a) => a.name) 43 + let selectedReleaseAssetName: string | null = null 62 44 63 45 if (platformMap?.[platformInfo.combined]) { 64 46 // Use custom platform mapping 65 47 const templateName = platformMap[platformInfo.combined] 66 - selectedAsset = templateName.replace('{version}', version) 48 + selectedReleaseAssetName = templateName.replace('{version}', version) 67 49 } else { 68 50 // Use automatic detection 69 - selectedAsset = findBestAsset(assetNames, platformInfo) 51 + selectedReleaseAssetName = findBestAsset(releaseAssetNames, platformInfo) 70 52 } 71 53 72 - if (!selectedAsset) { 73 - console.error( 74 - `No matching asset found for platform ${platformInfo.combined}`, 75 - ) 76 - console.error('Available assets:') 77 - assetNames.forEach((name) => console.error(` - ${name}`)) 78 - throw new Error('No matching asset found') 54 + if (!selectedReleaseAssetName) { 55 + console.error(`No matching release asset found for platform ${platformInfo.combined}`) 56 + console.error('Available release assets:') 57 + releaseAssetNames.forEach((name) => console.error(` - ${name}`)) 58 + throw new Error('No matching release asset found') 79 59 } 80 60 81 - const asset = releaseInfo.assets.find((a) => a.name === selectedAsset) 82 - if (!asset) { 83 - throw new Error(`Asset ${selectedAsset} not found in release`) 84 - } 61 + const releaseAsset = releaseInfo.assets.find((a) => a.name === selectedReleaseAssetName) 62 + if (!releaseAsset) throw new Error(`Release asset ${selectedReleaseAssetName} not found in release`) 85 63 86 - if (verbose) { 87 - console.log(`Selected asset: ${selectedAsset}`) 88 - } 64 + if (v) console.log(`Selected release asset: ${selectedReleaseAssetName}`) 89 65 90 66 // Create output directory 91 67 const outputPath = resolve(outputDir) 92 - const { mkdir } = await import('node:fs/promises') 93 68 await mkdir(outputPath, { recursive: true }) 94 69 95 70 // Download asset - sanitize filename to prevent path traversal 96 - const sanitizedFilename = basename(selectedAsset) 71 + const sanitizedFilename = basename(selectedReleaseAssetName) 97 72 const tempPath = join(outputPath, sanitizedFilename) 98 73 99 74 // Check if binary already exists (unless force is enabled) ··· 108 83 for (const binaryPath of potentialBinaryPaths) { 109 84 try { 110 85 await access(binaryPath) 111 - throw new Error( 112 - `Binary ${binaryPath} already exists. Use --force to overwrite.`, 113 - ) 86 + throw new Error(`Binary ${binaryPath} already exists. Use --force to overwrite.`) 114 87 } catch (error) { 115 88 // If access throws, file doesn't exist, continue checking 116 - if ( 117 - error instanceof Error && 118 - error.message.includes('already exists') 119 - ) { 89 + if (error instanceof Error && error.message.includes('already exists')) { 120 90 throw error 121 91 } 122 92 } 123 93 } 124 94 } 125 95 126 - if (verbose) { 127 - console.log(`Downloading ${selectedAsset}...`) 128 - } 96 + if (v) console.log(`Downloading ${selectedReleaseAssetName}...`) 129 97 130 - await downloadAsset(asset.download_url, tempPath) 98 + await downloadAsset(releaseAsset.download_url, tempPath) 131 99 132 100 let extractionSuccessful = false 133 101 try { 134 102 // Extract archive 135 - if (verbose) { 136 - console.log(`Extracting ${selectedAsset}...`) 137 - } 103 + if (v) console.log(`Extracting ${selectedReleaseAssetName}...`) 138 104 139 105 await extractArchive(tempPath, outputPath) 140 106 extractionSuccessful = true ··· 143 109 const finalBinName = binName || repo.split('/')[1] 144 110 145 111 // Optimized binary search - try common locations first, then fallback to recursive 146 - const potentialBinaryNames = [ 147 - finalBinName, 148 - `${finalBinName}.exe`, 149 - `${finalBinName}.bin`, 150 - ] 112 + const potentialBinaryNames = [finalBinName, `${finalBinName}.exe`, `${finalBinName}.bin`] 151 113 152 114 let binaryFound = false 153 115 ··· 158 120 const fileStat = await stat(directPath) 159 121 if (fileStat.isFile()) { 160 122 await chmod(directPath, 0o755) 161 - if (verbose) { 162 - console.log(`Made ${directPath} executable`) 163 - } 123 + if (v) console.log(`Made ${directPath} executable`) 164 124 binaryFound = true 165 125 break 166 126 } 167 - } catch (_error) { 127 + } catch { 168 128 // File doesn't exist at direct path, continue 169 129 } 170 130 } ··· 184 144 if (!fileStat.isFile()) continue 185 145 186 146 const isExactMatch = fileName === finalBinName 187 - const isExecutableWithExt = 188 - fileName === `${finalBinName}.exe` || 189 - fileName === `${finalBinName}.bin` 190 - const hasSuspiciousExt = [ 191 - '.txt', 192 - '.md', 193 - '.json', 194 - '.xml', 195 - '.html', 196 - '.log', 197 - ].includes(fileExtension) 147 + const isExecutableWithExt = fileName === `${finalBinName}.exe` || fileName === `${finalBinName}.bin` 148 + const hasSuspiciousExt = ['.txt', '.md', '.json', '.xml', '.html', '.log'].includes(fileExtension) 198 149 199 150 if ((isExactMatch || isExecutableWithExt) && !hasSuspiciousExt) { 200 151 await chmod(filePath, 0o755) 201 - if (verbose) { 152 + if (v) { 202 153 console.log(`Made ${filePath} executable`) 203 154 } 204 155 binaryFound = true 205 156 break // Early termination once we find the binary 206 157 } 207 - } catch (_error) { 158 + } catch { 208 159 // Ignore chmod errors on Windows or if file doesn't exist 209 160 } 210 161 } 211 162 } 212 163 213 - if (!binaryFound && verbose) { 214 - console.log( 215 - `Warning: Binary '${finalBinName}' not found in extracted files`, 216 - ) 164 + if (!binaryFound && v) console.log(`Warning: Binary '${finalBinName}' not found in extracted files`) 165 + 166 + // Throw if binName is specified and not found 167 + if (binName && !binaryFound) { 168 + const files = await readdir(outputPath, { recursive: true }) 169 + console.error(`Error: Specified binName '${binName}' not found in extracted files.`) 170 + console.error('Files found:') 171 + for (const file of files) { 172 + console.error(` - ${file}`) 173 + } 174 + throw new Error(`Specified binName '${binName}' not found after extraction`) 217 175 } 218 176 219 - if (verbose) { 220 - console.log( 221 - `✓ Successfully installed ${repo} ${version} to ${outputPath}`, 222 - ) 223 - } 177 + if (v) console.log(`✓ Successfully installed ${repo} ${version} to ${outputPath}`) 224 178 } finally { 225 179 // Always clean up downloaded archive, regardless of extraction success 226 - const { unlink } = await import('node:fs/promises') 227 180 try { 228 181 await unlink(tempPath) 229 - if (verbose && !extractionSuccessful) { 230 - console.log('Cleaned up downloaded archive after failed extraction') 231 - } 232 - } catch (_error) { 182 + if (v && !extractionSuccessful) console.log('Cleaned up downloaded archive after failed extraction') 183 + } catch { 233 184 // Ignore cleanup errors 234 185 } 235 186 }
+4 -15
src/platform.ts
··· 17 17 } 18 18 } 19 19 20 - export function matchAssetName( 21 - assetName: string, 22 - platformInfo: PlatformInfo, 23 - ): boolean { 20 + export function matchAssetName(assetName: string, platformInfo: PlatformInfo): boolean { 24 21 const name = assetName.toLowerCase() 25 22 const { platform: p, arch: a } = platformInfo 26 23 ··· 38 35 arm: ['armv7', 'arm'], 39 36 } 40 37 41 - const platformMatch = platformPatterns[ 42 - p as keyof typeof platformPatterns 43 - ]?.some((pattern) => name.includes(pattern)) 44 - 45 - const archMatch = archPatterns[a as keyof typeof archPatterns]?.some( 46 - (pattern) => name.includes(pattern), 47 - ) 38 + const platformMatch = platformPatterns[p as keyof typeof platformPatterns]?.some((pattern) => name.includes(pattern)) 39 + const archMatch = archPatterns[a as keyof typeof archPatterns]?.some((pattern) => name.includes(pattern)) 48 40 49 41 return Boolean(platformMatch && archMatch) 50 42 } 51 43 52 - export function findBestAsset( 53 - assets: string[], 54 - platformInfo: PlatformInfo, 55 - ): string | null { 44 + export function findBestAsset(assets: string[], platformInfo: PlatformInfo): string | null { 56 45 const matches = assets.filter((asset) => matchAssetName(asset, platformInfo)) 57 46 58 47 if (matches.length === 0) return null
+2 -9
test/binary-search.test.ts
··· 251 251 const warningMessages = consoleMessages.filter( 252 252 (msg) => msg.includes('Warning: Binary') && msg.includes('not found'), 253 253 ) 254 - assert.equal( 255 - warningMessages.length, 256 - 1, 257 - 'Should log warning when binary not found', 258 - ) 254 + assert.equal(warningMessages.length, 1, 'Should log warning when binary not found') 259 255 } finally { 260 256 console.log = originalConsoleLog 261 257 await rm(testDir, { recursive: true }).catch(() => {}) ··· 320 316 // If zip extraction fails (expected on non-Windows), that's still a valid test 321 317 // since we're mainly testing the asset download and binary naming logic 322 318 if (error.message.includes('Failed to extract zip file')) { 323 - assert.ok( 324 - true, 325 - 'Test completed - zip extraction not available on this platform', 326 - ) 319 + assert.ok(true, 'Test completed - zip extraction not available on this platform') 327 320 } else { 328 321 throw error // Re-throw unexpected errors 329 322 }
+1 -7
test/cli.test.ts
··· 36 36 37 37 test('parseArgs handles long flags', () => { 38 38 const { values } = parseArgs({ 39 - args: [ 40 - '--bin-name', 41 - 'custom-bin', 42 - '--output', 43 - './custom-output', 44 - '--verbose', 45 - ], 39 + args: ['--bin-name', 'custom-bin', '--output', './custom-output', '--verbose'], 46 40 options: { 47 41 'bin-name': { type: 'string', short: 'b' }, 48 42 output: { type: 'string', short: 'o' },
+5 -22
test/download.test.ts
··· 16 16 17 17 try { 18 18 await assert.rejects( 19 - async () => 20 - await downloadAsset( 21 - 'https://example.com/file.tar.gz', 22 - './test-null-body.tar.gz', 23 - ), 19 + async () => await downloadAsset('https://example.com/file.tar.gz', './test-null-body.tar.gz'), 24 20 /Response body is empty/, 25 21 ) 26 22 } finally { ··· 41 37 42 38 try { 43 39 await assert.rejects( 44 - async () => 45 - await downloadAsset( 46 - 'https://example.com/file.tar.gz', 47 - './test-undefined-body.tar.gz', 48 - ), 40 + async () => await downloadAsset('https://example.com/file.tar.gz', './test-undefined-body.tar.gz'), 49 41 /Response body is empty/, 50 42 ) 51 43 } finally { ··· 74 66 75 67 try { 76 68 await assert.rejects( 77 - async () => 78 - await downloadAsset('https://example.com/file.tar.gz', testFile), 69 + async () => await downloadAsset('https://example.com/file.tar.gz', testFile), 79 70 /Failed to download asset: Stream interrupted/, 80 71 ) 81 72 ··· 104 95 105 96 try { 106 97 await assert.rejects( 107 - async () => 108 - await downloadAsset( 109 - 'https://example.com/nonexistent.tar.gz', 110 - './test-404.tar.gz', 111 - ), 98 + async () => await downloadAsset('https://example.com/nonexistent.tar.gz', './test-404.tar.gz'), 112 99 /Failed to download asset: 404 Not Found/, 113 100 ) 114 101 } finally { ··· 164 151 165 152 try { 166 153 await assert.rejects( 167 - async () => 168 - await downloadAsset( 169 - 'https://example.com/file.tar.gz', 170 - './test-network-error.tar.gz', 171 - ), 154 + async () => await downloadAsset('https://example.com/file.tar.gz', './test-network-error.tar.gz'), 172 155 /ECONNRESET: Connection reset by peer/, 173 156 ) 174 157 } finally {
+5 -10
test/error.test.ts
··· 65 65 [currentPlatform.combined]: 'nonexistent-asset.tar.gz', 66 66 }, 67 67 }), 68 - /Asset nonexistent-asset.tar.gz not found in release/, 68 + /Release asset nonexistent-asset.tar.gz not found in release/, 69 69 ) 70 70 }) 71 71 ··· 114 114 [currentPlatform.combined]: 'nonexistent-file.tar.gz', 115 115 }, 116 116 }), 117 - /Asset nonexistent-file.tar.gz not found in release/, 117 + /Release asset nonexistent-file.tar.gz not found in release/, 118 118 ) 119 119 }) 120 120 ··· 156 156 await installRelease('test/app', 'v0.20.0', { 157 157 outputDir: testDir, 158 158 platformMap: { 159 - [currentPlatform.combined]: 160 - 'test-app-v0.20.0-x86_64-apple-darwin.tar.gz', 159 + [currentPlatform.combined]: 'test-app-v0.20.0-x86_64-apple-darwin.tar.gz', 161 160 }, 162 161 }), 163 162 /Binary .* already exists. Use --force to overwrite./, ··· 214 213 await installRelease('test/app', 'v0.20.0', { 215 214 outputDir: testDir, 216 215 platformMap: { 217 - [currentPlatform.combined]: 218 - 'test-app-v0.20.0-x86_64-apple-darwin.tar.gz', 216 + [currentPlatform.combined]: 'test-app-v0.20.0-x86_64-apple-darwin.tar.gz', 219 217 }, 220 218 }), 221 219 // Should fail during extraction but still clean up ··· 227 225 // Verify archive was cleaned up (temp file shouldn't exist) 228 226 const { access } = await import('node:fs/promises') 229 227 const { join } = await import('node:path') 230 - const tempPath = join( 231 - testDir, 232 - 'test-app-v0.20.0-x86_64-apple-darwin.tar.gz', 233 - ) 228 + const tempPath = join(testDir, 'test-app-v0.20.0-x86_64-apple-darwin.tar.gz') 234 229 235 230 await assert.rejects( 236 231 async () => await access(tempPath),
+5 -14
test/extract.test.ts
··· 19 19 await extractArchive(archivePath, testDir) 20 20 } catch (error) { 21 21 // Expected to fail with gzip/tar error from our native implementation 22 - assert.match( 23 - (error as Error).message, 24 - /tar command failed|Failed to extract|incorrect header check/, 25 - ) 22 + assert.match((error as Error).message, /tar command failed|Failed to extract|incorrect header check/) 26 23 } 27 24 28 25 // Clean up ··· 43 40 await extractArchive(archivePath, testDir) 44 41 } catch (error) { 45 42 // Expected to fail with unzip command error 46 - assert.match( 47 - (error as Error).message, 48 - /unzip command failed|Failed to extract/, 49 - ) 43 + assert.match((error as Error).message, /unzip command failed|Failed to extract/) 50 44 } 51 45 52 46 // Clean up ··· 60 54 await mkdir(testDir, { recursive: true }) 61 55 await writeFile(archivePath, Buffer.from('mock-content')) 62 56 63 - await assert.rejects( 64 - async () => await extractArchive(archivePath, testDir), 65 - /Unsupported archive format/, 66 - ) 57 + await assert.rejects(async () => await extractArchive(archivePath, testDir), /Unsupported archive format/) 67 58 68 59 // Clean up 69 60 await rm(testDir, { recursive: true }) ··· 78 69 79 70 try { 80 71 await extractArchive(archivePath, testDir) 81 - } catch (_error) { 72 + } catch { 82 73 // Expected to fail, but directory should be created 83 74 try { 84 75 await access(testDir) 85 76 // Directory exists, test passed 86 - } catch (_accessError) { 77 + } catch { 87 78 assert.fail('Output directory was not created') 88 79 } 89 80 }
+1 -4
test/github.test.ts
··· 23 23 describe('GitHub API Integration', () => { 24 24 test('fetchReleaseInfo returns release data for valid repo/version', async (t) => { 25 25 t.mock.method(global, 'fetch', async (url: string) => { 26 - if ( 27 - url === 28 - 'https://api.github.com/repos/getzola/zola/releases/tags/v0.20.0' 29 - ) { 26 + if (url === 'https://api.github.com/repos/getzola/zola/releases/tags/v0.20.0') { 30 27 return { 31 28 ok: true, 32 29 json: async () => mockReleaseData,
+17 -69
test/platform.test.ts
··· 1 1 import assert from 'node:assert/strict' 2 2 import { describe, test } from 'node:test' 3 - import { 4 - findBestAsset, 5 - getPlatformInfo, 6 - matchAssetName, 7 - } from '../src/platform.js' 3 + import { findBestAsset, getPlatformInfo, matchAssetName } from '../src/platform.js' 8 4 9 5 describe('Platform Detection', () => { 10 6 test('getPlatformInfo returns valid platform info', () => { ··· 22 18 combined: 'darwin-x64', 23 19 } 24 20 25 - assert.equal( 26 - matchAssetName('app-v1.0.0-x86_64-apple-darwin.tar.gz', platform), 27 - true, 28 - ) 21 + assert.equal(matchAssetName('app-v1.0.0-x86_64-apple-darwin.tar.gz', platform), true) 29 22 assert.equal(matchAssetName('app-v1.0.0-amd64-macos.zip', platform), true) 30 23 assert.equal(matchAssetName('app-v1.0.0-x64-darwin.tar.gz', platform), true) 31 - assert.equal( 32 - matchAssetName('app-v1.0.0-x86_64-apple.tar.gz', platform), 33 - true, 34 - ) 24 + assert.equal(matchAssetName('app-v1.0.0-x86_64-apple.tar.gz', platform), true) 35 25 }) 36 26 37 27 test('matchAssetName detects Linux assets correctly', () => { 38 28 const platform = { platform: 'linux', arch: 'x64', combined: 'linux-x64' } 39 29 40 - assert.equal( 41 - matchAssetName('app-v1.0.0-x86_64-unknown-linux-gnu.tar.gz', platform), 42 - true, 43 - ) 44 - assert.equal( 45 - matchAssetName('app-v1.0.0-amd64-linux.tar.gz', platform), 46 - true, 47 - ) 48 - assert.equal( 49 - matchAssetName('app-v1.0.0-x64-linux-gnu.tar.gz', platform), 50 - true, 51 - ) 30 + assert.equal(matchAssetName('app-v1.0.0-x86_64-unknown-linux-gnu.tar.gz', platform), true) 31 + assert.equal(matchAssetName('app-v1.0.0-amd64-linux.tar.gz', platform), true) 32 + assert.equal(matchAssetName('app-v1.0.0-x64-linux-gnu.tar.gz', platform), true) 52 33 }) 53 34 54 35 test('matchAssetName detects Windows assets correctly', () => { 55 36 const platform = { platform: 'win32', arch: 'x64', combined: 'win32-x64' } 56 37 57 - assert.equal( 58 - matchAssetName('app-v1.0.0-x86_64-pc-windows-msvc.zip', platform), 59 - true, 60 - ) 38 + assert.equal(matchAssetName('app-v1.0.0-x86_64-pc-windows-msvc.zip', platform), true) 61 39 assert.equal(matchAssetName('app-v1.0.0-amd64-windows.zip', platform), true) 62 40 assert.equal(matchAssetName('app-v1.0.0-x64-win64.zip', platform), true) 63 41 assert.equal(matchAssetName('app-v1.0.0-x86_64-win32.zip', platform), true) ··· 70 48 combined: 'darwin-arm64', 71 49 } 72 50 73 - assert.equal( 74 - matchAssetName('app-v1.0.0-aarch64-apple-darwin.tar.gz', platform), 75 - true, 76 - ) 77 - assert.equal( 78 - matchAssetName('app-v1.0.0-arm64-macos.tar.gz', platform), 79 - true, 80 - ) 51 + assert.equal(matchAssetName('app-v1.0.0-aarch64-apple-darwin.tar.gz', platform), true) 52 + assert.equal(matchAssetName('app-v1.0.0-arm64-macos.tar.gz', platform), true) 81 53 }) 82 54 83 55 test('matchAssetName rejects non-matching assets', () => { ··· 87 59 combined: 'darwin-x64', 88 60 } 89 61 90 - assert.equal( 91 - matchAssetName('app-v1.0.0-x86_64-unknown-linux-gnu.tar.gz', platform), 92 - false, 93 - ) 94 - assert.equal( 95 - matchAssetName('app-v1.0.0-aarch64-apple-darwin.tar.gz', platform), 96 - false, 97 - ) 98 - assert.equal( 99 - matchAssetName('app-v1.0.0-x86_64-pc-windows-msvc.zip', platform), 100 - false, 101 - ) 62 + assert.equal(matchAssetName('app-v1.0.0-x86_64-unknown-linux-gnu.tar.gz', platform), false) 63 + assert.equal(matchAssetName('app-v1.0.0-aarch64-apple-darwin.tar.gz', platform), false) 64 + assert.equal(matchAssetName('app-v1.0.0-x86_64-pc-windows-msvc.zip', platform), false) 102 65 assert.equal(matchAssetName('app-v1.0.0-source.tar.gz', platform), false) 103 66 }) 104 67 ··· 129 92 'app-v1.0.0-source.tar.gz', 130 93 ] 131 94 132 - assert.equal( 133 - findBestAsset(assets, platform), 134 - 'app-v1.0.0-x86_64-apple-darwin.tar.gz', 135 - ) 95 + assert.equal(findBestAsset(assets, platform), 'app-v1.0.0-x86_64-apple-darwin.tar.gz') 136 96 }) 137 97 138 98 test('findBestAsset prefers tar.gz over zip on non-Windows', () => { ··· 141 101 arch: 'x64', 142 102 combined: 'darwin-x64', 143 103 } 144 - const assets = [ 145 - 'app-v1.0.0-x86_64-apple-darwin.zip', 146 - 'app-v1.0.0-x86_64-apple-darwin.tar.gz', 147 - ] 104 + const assets = ['app-v1.0.0-x86_64-apple-darwin.zip', 'app-v1.0.0-x86_64-apple-darwin.tar.gz'] 148 105 149 - assert.equal( 150 - findBestAsset(assets, platform), 151 - 'app-v1.0.0-x86_64-apple-darwin.tar.gz', 152 - ) 106 + assert.equal(findBestAsset(assets, platform), 'app-v1.0.0-x86_64-apple-darwin.tar.gz') 153 107 }) 154 108 155 109 test('findBestAsset returns first match when no tar.gz preference', () => { 156 110 const platform = { platform: 'win32', arch: 'x64', combined: 'win32-x64' } 157 - const assets = [ 158 - 'app-v1.0.0-x86_64-pc-windows-msvc.zip', 159 - 'app-v1.0.0-x64-windows.zip', 160 - ] 111 + const assets = ['app-v1.0.0-x86_64-pc-windows-msvc.zip', 'app-v1.0.0-x64-windows.zip'] 161 112 162 - assert.equal( 163 - findBestAsset(assets, platform), 164 - 'app-v1.0.0-x86_64-pc-windows-msvc.zip', 165 - ) 113 + assert.equal(findBestAsset(assets, platform), 'app-v1.0.0-x86_64-pc-windows-msvc.zip') 166 114 }) 167 115 })
+3 -14
test/tar-extraction.test.ts
··· 78 78 const nonExistentPath = './nonexistent/file.tar.gz' 79 79 const outputDir = './test-nonexistent' 80 80 81 - await assert.rejects( 82 - async () => await extractTarGz(nonExistentPath, outputDir), 83 - /Archive file does not exist/, 84 - ) 81 + await assert.rejects(async () => await extractTarGz(nonExistentPath, outputDir), /Archive file does not exist/) 85 82 }) 86 83 87 84 test('handles tar with directories and files', async () => { ··· 127 124 // End of archive (two zero blocks) 128 125 const endOfArchive = Buffer.alloc(1024) 129 126 130 - const tarData = Buffer.concat([ 131 - tarHeader, 132 - fileContent, 133 - filePadding, 134 - endOfArchive, 135 - ]) 127 + const tarData = Buffer.concat([tarHeader, fileContent, filePadding, endOfArchive]) 136 128 137 129 const readableStream = Readable.from([tarData]) 138 130 const gzipStream = createGzip() ··· 145 137 146 138 // Verify file was extracted 147 139 const { readFile } = await import('node:fs/promises') 148 - const extractedContent = await readFile( 149 - join(outputDir, 'test.txt'), 150 - 'utf8', 151 - ) 140 + const extractedContent = await readFile(join(outputDir, 'test.txt'), 'utf8') 152 141 assert.equal(extractedContent, 'Hello, world!') 153 142 } finally { 154 143 await rm(testDir, { recursive: true }).catch(() => {})
+2 -8
test/types.test.ts
··· 16 16 combined: 'darwin-x64', 17 17 } 18 18 19 - assert.equal( 20 - matchAssetName('app-v1.0.0-x86_64-apple-darwin.tar.gz', platformInfo), 21 - true, 22 - ) 23 - assert.equal( 24 - matchAssetName('app-v1.0.0-x86_64-unknown-linux-gnu.tar.gz', platformInfo), 25 - false, 26 - ) 19 + assert.equal(matchAssetName('app-v1.0.0-x86_64-apple-darwin.tar.gz', platformInfo), true) 20 + assert.equal(matchAssetName('app-v1.0.0-x86_64-unknown-linux-gnu.tar.gz', platformInfo), false) 27 21 })
+1 -4
test/zip-extraction.test.ts
··· 26 26 const nonExistentPath = './nonexistent/file.zip' 27 27 const outputDir = './test-zip-nonexistent' 28 28 29 - await assert.rejects( 30 - async () => await extractZip(nonExistentPath, outputDir), 31 - /Archive file does not exist/, 32 - ) 29 + await assert.rejects(async () => await extractZip(nonExistentPath, outputDir), /Archive file does not exist/) 33 30 }) 34 31 35 32 test('creates output directory if it does not exist', async () => {