Skip to content

Commit 5137c37

Browse files
committed
chore: minor updates
chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip chore: wip
1 parent e273fda commit 5137c37

21 files changed

+1899
-1717
lines changed

packages/launchpad/bin/cli.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,8 +1159,9 @@ cli
11591159
// Dev commands for shell integration
11601160
cli
11611161
.command('dev:shellcode', 'Generate shell integration code')
1162-
.action(() => {
1163-
console.log(shellcode())
1162+
.option('--test-mode', 'Generate shellcode for testing (bypasses test environment checks)')
1163+
.action(({ testMode }) => {
1164+
console.log(shellcode(testMode))
11641165
})
11651166

11661167
cli
@@ -1171,10 +1172,17 @@ cli
11711172
.action(async (dir?: string, options?: { dryRun?: boolean, quiet?: boolean, shell?: boolean }) => {
11721173
try {
11731174
const targetDir = dir ? path.resolve(dir) : process.cwd()
1175+
1176+
// For shell integration, force quiet mode and set environment variable
1177+
const isShellIntegration = options?.shell || false
1178+
if (isShellIntegration) {
1179+
process.env.LAUNCHPAD_SHELL_INTEGRATION = '1'
1180+
}
1181+
11741182
await dump(targetDir, {
11751183
dryrun: options?.dryRun || false,
1176-
quiet: options?.quiet || false,
1177-
shellOutput: options?.shell || false,
1184+
quiet: options?.quiet || isShellIntegration, // Force quiet for shell integration
1185+
shellOutput: isShellIntegration,
11781186
skipGlobal: process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_SKIP_GLOBAL_AUTO_SCAN === 'true' || process.env.LAUNCHPAD_ENABLE_GLOBAL_AUTO_SCAN !== 'true', // Skip global packages by default unless explicitly enabled
11791187
})
11801188
}

packages/launchpad/src/bun.ts

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable no-console */
2+
import { Buffer } from 'node:buffer'
23
import fs from 'node:fs'
34
import { arch, platform } from 'node:os'
45
import path from 'node:path'
@@ -32,19 +33,84 @@ function validatePath(installPath: string): boolean {
3233
}
3334
}
3435

36+
/**
37+
* Validate if a file is a proper zip archive
38+
*/
39+
function validateZipFile(filePath: string): boolean {
40+
try {
41+
if (!fs.existsSync(filePath)) {
42+
return false
43+
}
44+
45+
const stats = fs.statSync(filePath)
46+
47+
// Check if file size is reasonable (bun should be at least 1MB, less than 200MB)
48+
if (stats.size < 1024 * 1024 || stats.size > 200 * 1024 * 1024) {
49+
if (config.verbose) {
50+
console.warn(`Invalid zip file size: ${stats.size} bytes (expected 1MB-200MB)`)
51+
}
52+
return false
53+
}
54+
55+
// Read the first few bytes to check zip signature
56+
const buffer = Buffer.alloc(4)
57+
const fd = fs.openSync(filePath, 'r')
58+
try {
59+
fs.readSync(fd, buffer, 0, 4, 0)
60+
61+
// Check for ZIP signature (PK\x03\x04 or PK\x05\x06 or PK\x07\x08)
62+
const signature = buffer.readUInt32LE(0)
63+
// eslint-disable-next-line unicorn/number-literal-case
64+
const isValidZip = signature === 0x04034b50 // Local file header
65+
// eslint-disable-next-line unicorn/number-literal-case
66+
|| signature === 0x06054b50 // End of central directory
67+
// eslint-disable-next-line unicorn/number-literal-case
68+
|| signature === 0x08074b50 // Data descriptor
69+
70+
if (!isValidZip && config.verbose) {
71+
console.warn(`Invalid zip signature: 0x${signature.toString(16).padStart(8, '0')}`)
72+
}
73+
74+
return isValidZip
75+
}
76+
finally {
77+
fs.closeSync(fd)
78+
}
79+
}
80+
catch (error) {
81+
if (config.verbose) {
82+
console.warn(`Error validating zip file: ${error}`)
83+
}
84+
return false
85+
}
86+
}
87+
3588
/**
3689
* Get cached binary path for a specific version
3790
*/
3891
function getCachedBinaryPath(version: string, filename: string): string | null {
3992
const cachedArchivePath = path.join(BINARY_CACHE_DIR, version, filename)
4093

41-
if (fs.existsSync(cachedArchivePath)) {
94+
if (fs.existsSync(cachedArchivePath) && validateZipFile(cachedArchivePath)) {
4295
if (config.verbose) {
4396
console.warn(`Found cached binary: ${cachedArchivePath}`)
4497
}
4598
return cachedArchivePath
4699
}
47100

101+
// Remove corrupted cache if it exists
102+
if (fs.existsSync(cachedArchivePath) && !validateZipFile(cachedArchivePath)) {
103+
try {
104+
fs.unlinkSync(cachedArchivePath)
105+
if (config.verbose) {
106+
console.warn(`Cached file is corrupted, removing: ${cachedArchivePath}`)
107+
}
108+
}
109+
catch {
110+
// Ignore errors removing cached file
111+
}
112+
}
113+
48114
return null
49115
}
50116

@@ -290,7 +356,7 @@ export async function install_bun(installPath: string, version?: string): Promis
290356
let zipPath: string
291357

292358
try {
293-
if (cachedArchivePath) {
359+
if (cachedArchivePath && validateZipFile(cachedArchivePath)) {
294360
// Use cached version - show success message directly without intermediate loading message
295361
if (config.verbose) {
296362
console.warn(`Using cached Bun v${bunVersion} from: ${cachedArchivePath}`)
@@ -309,6 +375,18 @@ export async function install_bun(installPath: string, version?: string): Promis
309375
}
310376
}
311377
else {
378+
// Remove corrupted cache if it exists
379+
if (cachedArchivePath && !validateZipFile(cachedArchivePath)) {
380+
if (config.verbose) {
381+
console.warn(`Cached file is corrupted, removing: ${cachedArchivePath}`)
382+
}
383+
try {
384+
fs.unlinkSync(cachedArchivePath)
385+
}
386+
catch {
387+
// Ignore errors removing corrupted cache
388+
}
389+
}
312390
// Download new version
313391
if (config.verbose) {
314392
console.warn(`Downloading from: ${url}`)
@@ -419,6 +497,33 @@ export async function install_bun(installPath: string, version?: string): Promis
419497
if (config.verbose)
420498
console.warn(`Downloaded to ${zipPath}`)
421499

500+
// Validate the downloaded zip file before caching and extraction
501+
if (!validateZipFile(zipPath)) {
502+
// Remove corrupted file
503+
try {
504+
fs.unlinkSync(zipPath)
505+
}
506+
catch {
507+
// Ignore errors removing corrupted file
508+
}
509+
510+
// Try to clear any cached corrupted versions
511+
const cachedPath = getCachedBinaryPath(bunVersion, filename)
512+
if (cachedPath && fs.existsSync(cachedPath)) {
513+
try {
514+
fs.unlinkSync(cachedPath)
515+
if (config.verbose) {
516+
console.warn(`Removed corrupted cached file: ${cachedPath}`)
517+
}
518+
}
519+
catch {
520+
// Ignore errors removing cached file
521+
}
522+
}
523+
524+
throw new Error(`Downloaded bun archive is corrupted. Try clearing cache with: launchpad cache:clear --force`)
525+
}
526+
422527
// Cache the downloaded file for future use
423528
saveBinaryToCache(bunVersion, filename, zipPath)
424529
}
@@ -445,7 +550,40 @@ export async function install_bun(installPath: string, version?: string): Promis
445550
const { promisify } = await import('node:util')
446551
const execAsync = promisify(exec)
447552

448-
await execAsync(`unzip -o "${zipPath}" -d "${tempDir}"`)
553+
try {
554+
await execAsync(`unzip -o "${zipPath}" -d "${tempDir}"`)
555+
}
556+
catch (error) {
557+
// Remove the corrupted file and any cached versions
558+
try {
559+
fs.unlinkSync(zipPath)
560+
}
561+
catch {
562+
// Ignore cleanup errors
563+
}
564+
565+
const cachedPath = getCachedBinaryPath(bunVersion, filename)
566+
if (cachedPath && fs.existsSync(cachedPath)) {
567+
try {
568+
fs.unlinkSync(cachedPath)
569+
if (config.verbose) {
570+
console.warn(`Removed corrupted cached file: ${cachedPath}`)
571+
}
572+
}
573+
catch {
574+
// Ignore cleanup errors
575+
}
576+
}
577+
578+
const errorMessage = error instanceof Error ? error.message : String(error)
579+
if (errorMessage.includes('End-of-central-directory signature not found')
580+
|| errorMessage.includes('zipfile')
581+
|| errorMessage.includes('not a zipfile')) {
582+
throw new Error(`Downloaded bun archive is corrupted. The download may have been interrupted or the file is damaged. Try running: launchpad cache:clear --force`)
583+
}
584+
585+
throw new Error(`Failed to extract bun archive: ${errorMessage}`)
586+
}
449587

450588
// Move the bun executable to the bin directory
451589
const bunExeName = platform() === 'win32' ? 'bun.exe' : 'bun'
@@ -467,6 +605,25 @@ export async function install_bun(installPath: string, version?: string): Promis
467605
}
468606
}
469607

608+
// Show individual package success message to match other packages
609+
if (!config.verbose) {
610+
const message = `✅ bun.sh \x1B[2m\x1B[3m(v${bunVersion})\x1B[0m`
611+
if (process.env.LAUNCHPAD_SHELL_INTEGRATION === '1') {
612+
process.stderr.write(`${message}\n`)
613+
}
614+
else {
615+
console.log(message)
616+
}
617+
}
618+
619+
// Create environment readiness marker file for fast shell integration
620+
try {
621+
fs.writeFileSync(path.join(installPath, '.launchpad_ready'), new Date().toISOString())
622+
}
623+
catch {
624+
// Ignore errors creating marker file
625+
}
626+
470627
// Clean up
471628
fs.rmSync(tempDir, { recursive: true, force: true })
472629

packages/launchpad/src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const defaultConfig: LaunchpadConfig = {
4848
autoAddToPath: process.env.LAUNCHPAD_AUTO_ADD_PATH !== 'false',
4949
showShellMessages: process.env.LAUNCHPAD_SHOW_ENV_MESSAGES !== 'false',
5050
shellActivationMessage: process.env.LAUNCHPAD_SHELL_ACTIVATION_MESSAGE || '✅ Environment activated for \x1B[3m{path}\x1B[0m',
51-
shellDeactivationMessage: process.env.LAUNCHPAD_SHELL_ACTIVATION_MESSAGE || 'Environment deactivated',
51+
shellDeactivationMessage: process.env.LAUNCHPAD_SHELL_DEACTIVATION_MESSAGE || 'Environment deactivated',
5252
useRegistry: true,
5353
installMethod: 'curl',
5454
installPath: getDefaultInstallPath(),

0 commit comments

Comments
 (0)