Skip to content

Commit a9f5623

Browse files
committed
feat: add ux for installation process
1 parent 97f7960 commit a9f5623

File tree

4 files changed

+1083
-3
lines changed

4 files changed

+1083
-3
lines changed

packages/launchpad/src/install.ts

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-console */
12
import { Buffer } from 'node:buffer'
23
import fs from 'node:fs'
34
import { arch, platform } from 'node:os'
@@ -6,6 +7,7 @@ import process from 'node:process'
67
import { aliases, packages } from 'ts-pkgx'
78
import { config } from './config'
89
import { Path } from './path'
10+
import { ProgressBar, Spinner } from './progress'
911

1012
// Extract all package alias names from ts-pkgx
1113
export type PackageAlias = keyof typeof aliases
@@ -319,8 +321,61 @@ async function downloadPackage(
319321

320322
const response = await fetch(url)
321323
if (response.ok) {
322-
const buffer = await response.arrayBuffer()
323-
await fs.promises.writeFile(file, Buffer.from(buffer))
324+
const contentLength = response.headers.get('content-length')
325+
const totalBytes = contentLength ? Number.parseInt(contentLength, 10) : 0
326+
327+
if (!config.verbose && totalBytes > 0) {
328+
// Show progress bar for downloads
329+
const progressBar = new ProgressBar(totalBytes, {
330+
showBytes: true,
331+
showSpeed: true,
332+
showETA: true,
333+
})
334+
335+
console.log(`📦 Downloading ${domain} v${version}...`)
336+
337+
const reader = response.body?.getReader()
338+
if (reader) {
339+
const chunks: Uint8Array[] = []
340+
let receivedBytes = 0
341+
342+
while (true) {
343+
const { done, value } = await reader.read()
344+
if (done)
345+
break
346+
347+
if (value) {
348+
chunks.push(value)
349+
receivedBytes += value.length
350+
progressBar.update(receivedBytes, totalBytes)
351+
}
352+
}
353+
354+
progressBar.complete()
355+
356+
// Combine all chunks into a single buffer
357+
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0)
358+
const buffer = new Uint8Array(totalLength)
359+
let offset = 0
360+
for (const chunk of chunks) {
361+
buffer.set(chunk, offset)
362+
offset += chunk.length
363+
}
364+
365+
await fs.promises.writeFile(file, buffer)
366+
}
367+
else {
368+
// Fallback for when reader is not available
369+
const buffer = await response.arrayBuffer()
370+
await fs.promises.writeFile(file, Buffer.from(buffer))
371+
}
372+
}
373+
else {
374+
// Fallback for when content-length is not available or verbose mode
375+
const buffer = await response.arrayBuffer()
376+
await fs.promises.writeFile(file, Buffer.from(buffer))
377+
}
378+
324379
downloadUrl = url
325380
archiveFile = file
326381
break
@@ -347,6 +402,12 @@ async function downloadPackage(
347402

348403
const isXz = archiveFile.endsWith('.tar.xz')
349404

405+
// Show spinner during extraction
406+
const extractSpinner = new Spinner()
407+
if (!config.verbose) {
408+
extractSpinner.start(`🔧 Extracting ${domain} v${version}...`)
409+
}
410+
350411
// Use Bun's spawn directly to avoid shell dependency issues
351412
const tarPath = process.platform === 'win32' ? 'tar' : '/usr/bin/tar'
352413
const tarArgs = isXz
@@ -359,6 +420,11 @@ async function downloadPackage(
359420
})
360421

361422
const result = await proc.exited
423+
424+
if (!config.verbose) {
425+
extractSpinner.stop()
426+
}
427+
362428
if (result !== 0) {
363429
const stderr = await new Response(proc.stderr).text()
364430
throw new Error(`Failed to extract archive: ${stderr}`)
@@ -373,6 +439,12 @@ async function downloadPackage(
373439
const binDir = path.join(installPath, 'bin')
374440
await fs.promises.mkdir(binDir, { recursive: true })
375441

442+
// Show spinner during installation
443+
const installSpinner = new Spinner()
444+
if (!config.verbose) {
445+
installSpinner.start(`⚡ Installing ${domain} v${version}...`)
446+
}
447+
376448
// Look for executables in common locations
377449
const searchDirs = [
378450
extractDir,
@@ -414,6 +486,10 @@ async function downloadPackage(
414486
}
415487
}
416488

489+
if (!config.verbose) {
490+
installSpinner.stop(`✅ Successfully installed ${domain} v${version}`)
491+
}
492+
417493
// Clean up temp directory
418494
await fs.promises.rm(tempDir, { recursive: true, force: true })
419495

@@ -450,6 +526,9 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
450526
console.warn(`Installing packages: ${packageList.join(', ')}`)
451527
console.warn(`Install path: ${installPath}`)
452528
}
529+
else if (packageList.length > 1) {
530+
console.log(`🚀 Installing ${packageList.length} packages...`)
531+
}
453532

454533
const os = getPlatform()
455534
const architecture = getArchitecture()
@@ -460,11 +539,15 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
460539

461540
const allInstalledFiles: string[] = []
462541

463-
for (const pkg of packageList) {
542+
for (let i = 0; i < packageList.length; i++) {
543+
const pkg = packageList[i]
464544
try {
465545
if (config.verbose) {
466546
console.warn(`Processing package: ${pkg}`)
467547
}
548+
else if (packageList.length > 1) {
549+
console.log(`📦 [${i + 1}/${packageList.length}] ${pkg}`)
550+
}
468551

469552
// Parse package name and version
470553
const { name: packageName, version: requestedVersion } = parsePackageSpec(pkg)
@@ -505,6 +588,16 @@ export async function install(packages: PackageSpec | PackageSpec[], basePath?:
505588
if (config.verbose) {
506589
console.warn(`Installation complete. Installed ${allInstalledFiles.length} files.`)
507590
}
591+
else if (packageList.length > 0) {
592+
const packageCount = packageList.length
593+
const fileCount = allInstalledFiles.length
594+
if (packageCount === 1) {
595+
console.log(`🎉 Installation complete!`)
596+
}
597+
else {
598+
console.log(`🎉 Successfully installed ${packageCount} packages (${fileCount} files)`)
599+
}
600+
}
508601

509602
return allInstalledFiles
510603
}

0 commit comments

Comments
 (0)