|
1 | 1 | #!/usr/bin/env bun
|
2 | 2 | import fs from 'node:fs'
|
| 3 | +import { homedir } from 'node:os' |
3 | 4 | import path from 'node:path'
|
4 | 5 | import process from 'node:process'
|
5 | 6 | import { CAC } from 'cac'
|
| 7 | +import { Glob } from 'bun' |
6 | 8 | import { install, install_prefix, list, uninstall } from '../src'
|
7 | 9 | import { config } from '../src/config'
|
8 | 10 | import { dump, integrate, shellcode } from '../src/dev'
|
@@ -381,51 +383,236 @@ async function performSetup(options: {
|
381 | 383 | }
|
382 | 384 | }
|
383 | 385 |
|
| 386 | +/** |
| 387 | + * Check if a path is a directory |
| 388 | + */ |
| 389 | +async function isDirectory(path: string): Promise<boolean> { |
| 390 | + try { |
| 391 | + const stats = await fs.promises.stat(path) |
| 392 | + return stats.isDirectory() |
| 393 | + } |
| 394 | + catch { |
| 395 | + return false |
| 396 | + } |
| 397 | +} |
| 398 | + |
| 399 | +/** |
| 400 | + * Set up development environment for a project directory |
| 401 | + */ |
| 402 | +async function setupDevelopmentEnvironment( |
| 403 | + targetDir: string, |
| 404 | + options: { dryRun?: boolean, quiet?: boolean, shell?: boolean }, |
| 405 | +): Promise<void> { |
| 406 | + // For shell integration, force quiet mode and set environment variable |
| 407 | + const isShellIntegration = options?.shell || false |
| 408 | + if (isShellIntegration) { |
| 409 | + process.env.LAUNCHPAD_SHELL_INTEGRATION = '1' |
| 410 | + } |
| 411 | + |
| 412 | + await dump(targetDir, { |
| 413 | + dryrun: options?.dryRun || false, |
| 414 | + quiet: options?.quiet || isShellIntegration, |
| 415 | + shellOutput: isShellIntegration, |
| 416 | + skipGlobal: process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_SKIP_GLOBAL_AUTO_SCAN === 'true', |
| 417 | + }) |
| 418 | +} |
| 419 | + |
| 420 | +/** |
| 421 | + * Find all dependency files across the machine and install global dependencies |
| 422 | + */ |
| 423 | +async function installGlobalDependencies(options: { |
| 424 | + dryRun?: boolean |
| 425 | + quiet?: boolean |
| 426 | + verbose?: boolean |
| 427 | +}): Promise<void> { |
| 428 | + if (!options.quiet) { |
| 429 | + console.log('🔍 Scanning machine for dependency files...') |
| 430 | + } |
| 431 | + |
| 432 | + const { DEPENDENCY_FILE_NAMES } = await import('../src/env') |
| 433 | + const globalDepLocations = [ |
| 434 | + homedir(), |
| 435 | + path.join(homedir(), '.dotfiles'), |
| 436 | + path.join(homedir(), '.config'), |
| 437 | + path.join(homedir(), 'Desktop'), |
| 438 | + path.join(homedir(), 'Documents'), |
| 439 | + path.join(homedir(), 'Projects'), |
| 440 | + path.join(homedir(), 'Code'), |
| 441 | + path.join(homedir(), 'Development'), |
| 442 | + '/opt', |
| 443 | + '/usr/local', |
| 444 | + ] |
| 445 | + |
| 446 | + const foundFiles: string[] = [] |
| 447 | + const allPackages = new Set<string>() |
| 448 | + |
| 449 | + // Use Bun's glob for efficient file searching |
| 450 | + const dependencyPatterns = DEPENDENCY_FILE_NAMES.map(name => `**/${name}`) |
| 451 | + |
| 452 | + // Search for dependency files in all potential locations using Bun glob |
| 453 | + for (const location of globalDepLocations) { |
| 454 | + if (!fs.existsSync(location)) |
| 455 | + continue |
| 456 | + |
| 457 | + try { |
| 458 | + // Search for all dependency file types in parallel using Bun's glob |
| 459 | + for (const pattern of dependencyPatterns) { |
| 460 | + const glob = new Glob(pattern) |
| 461 | + for await (const file of glob.scan({ |
| 462 | + cwd: location, |
| 463 | + onlyFiles: true, |
| 464 | + followSymlinks: false |
| 465 | + })) { |
| 466 | + const fullPath = path.resolve(location, file) |
| 467 | + foundFiles.push(fullPath) |
| 468 | + } |
| 469 | + } |
| 470 | + } |
| 471 | + catch { |
| 472 | + // Ignore permission errors or other issues |
| 473 | + continue |
| 474 | + } |
| 475 | + } |
| 476 | + |
| 477 | + if (!options.quiet) { |
| 478 | + console.log(`📁 Found ${foundFiles.length} dependency files`) |
| 479 | + } |
| 480 | + |
| 481 | + // Parse all found dependency files and extract packages |
| 482 | + const { default: sniff } = await import('../src/dev/sniff') |
| 483 | + |
| 484 | + for (const file of foundFiles) { |
| 485 | + try { |
| 486 | + const dir = path.dirname(file) |
| 487 | + const sniffResult = await sniff({ string: dir }) |
| 488 | + |
| 489 | + for (const pkg of sniffResult.pkgs) { |
| 490 | + allPackages.add(pkg.project) |
| 491 | + } |
| 492 | + |
| 493 | + if (options.verbose) { |
| 494 | + console.log(` 📄 ${file}: ${sniffResult.pkgs.length} packages`) |
| 495 | + } |
| 496 | + } |
| 497 | + catch (error) { |
| 498 | + if (options.verbose) { |
| 499 | + console.warn(`Failed to parse ${file}: ${error instanceof Error ? error.message : String(error)}`) |
| 500 | + } |
| 501 | + } |
| 502 | + } |
| 503 | + |
| 504 | + const packageList = Array.from(allPackages) |
| 505 | + |
| 506 | + if (!options.quiet) { |
| 507 | + console.log(`📦 Found ${packageList.length} unique global dependencies`) |
| 508 | + if (options.dryRun) { |
| 509 | + console.log('🔍 Packages that would be installed:') |
| 510 | + packageList.forEach(pkg => console.log(` • ${pkg}`)) |
| 511 | + return |
| 512 | + } |
| 513 | + } |
| 514 | + |
| 515 | + if (packageList.length === 0) { |
| 516 | + if (!options.quiet) { |
| 517 | + console.log('ℹ️ No global dependencies found') |
| 518 | + } |
| 519 | + return |
| 520 | + } |
| 521 | + |
| 522 | + // Install all global dependencies |
| 523 | + const globalEnvDir = path.join(homedir(), '.local', 'share', 'launchpad', 'global') |
| 524 | + |
| 525 | + try { |
| 526 | + const results = await install(packageList, globalEnvDir) |
| 527 | + |
| 528 | + if (!options.quiet) { |
| 529 | + if (results.length > 0) { |
| 530 | + console.log(`🎉 Successfully installed ${packageList.length} global dependencies (${results.length} binaries)`) |
| 531 | + } |
| 532 | + else { |
| 533 | + console.log('✅ All global dependencies were already installed') |
| 534 | + } |
| 535 | + } |
| 536 | + } |
| 537 | + catch (error) { |
| 538 | + if (!options.quiet) { |
| 539 | + console.error('❌ Failed to install global dependencies:', error instanceof Error ? error.message : String(error)) |
| 540 | + } |
| 541 | + throw error |
| 542 | + } |
| 543 | +} |
| 544 | + |
384 | 545 | const cli = new CAC('launchpad')
|
385 | 546 |
|
386 | 547 | cli.version(version)
|
387 | 548 | cli.help()
|
388 | 549 |
|
389 | 550 | // Main installation command
|
390 | 551 | cli
|
391 |
| - .command('install [packages...]', 'Install packages') |
| 552 | + .command('install [packages...]', 'Install packages or set up development environment') |
392 | 553 | .alias('i')
|
393 | 554 | .alias('add')
|
394 | 555 | .option('--verbose', 'Enable verbose output')
|
395 | 556 | .option('--path <path>', 'Custom installation path')
|
| 557 | + .option('--global-deps', 'Install all global dependencies found across the machine') |
| 558 | + .option('--dry-run', 'Show packages that would be installed without installing them') |
| 559 | + .option('--quiet', 'Suppress non-error output') |
| 560 | + .option('--shell', 'Output shell code for evaluation (use with eval)') |
396 | 561 | .example('launchpad install node python')
|
397 | 562 | .example('launchpad install --path ~/.local node python')
|
| 563 | + .example('launchpad install') |
| 564 | + .example('launchpad install ./my-project') |
| 565 | + .example('launchpad install --global-deps') |
398 | 566 | .example('launchpad add node python')
|
399 |
| - .action(async (packages: string[], options: { verbose?: boolean, path?: string }) => { |
| 567 | + .action(async (packages: string[], options: { |
| 568 | + verbose?: boolean |
| 569 | + path?: string |
| 570 | + globalDeps?: boolean |
| 571 | + dryRun?: boolean |
| 572 | + quiet?: boolean |
| 573 | + shell?: boolean |
| 574 | + }) => { |
400 | 575 | if (options.verbose) {
|
401 | 576 | config.verbose = true
|
402 | 577 | }
|
403 | 578 |
|
404 | 579 | // Ensure packages is an array
|
405 | 580 | const packageList = Array.isArray(packages) ? packages : [packages].filter(Boolean)
|
406 | 581 |
|
407 |
| - if (packageList.length === 0) { |
408 |
| - console.error('No packages specified') |
409 |
| - process.exit(1) |
410 |
| - } |
411 |
| - |
412 | 582 | try {
|
413 |
| - const installPath = options.path || install_prefix().string |
| 583 | + // Handle global dependencies installation |
| 584 | + if (options.globalDeps) { |
| 585 | + await installGlobalDependencies(options) |
| 586 | + return |
| 587 | + } |
414 | 588 |
|
| 589 | + // Handle development environment setup (no packages specified or directory path given) |
| 590 | + if (packageList.length === 0 || (packageList.length === 1 && await isDirectory(packageList[0]))) { |
| 591 | + const targetDir = packageList.length === 1 ? path.resolve(packageList[0]) : process.cwd() |
| 592 | + await setupDevelopmentEnvironment(targetDir, options) |
| 593 | + return |
| 594 | + } |
| 595 | + |
| 596 | + // Handle regular package installation |
| 597 | + const installPath = options.path || install_prefix().string |
415 | 598 | const results = await install(packageList, installPath)
|
416 | 599 |
|
417 |
| - if (results.length > 0) { |
418 |
| - console.log(`🎉 Successfully installed ${packageList.join(', ')} (${results.length} ${results.length === 1 ? 'binary' : 'binaries'})`) |
419 |
| - results.forEach((file) => { |
420 |
| - console.log(` ${file}`) |
421 |
| - }) |
422 |
| - } |
423 |
| - else { |
424 |
| - console.log('⚠️ No binaries were installed') |
| 600 | + if (!options.quiet) { |
| 601 | + if (results.length > 0) { |
| 602 | + console.log(`🎉 Successfully installed ${packageList.join(', ')} (${results.length} ${results.length === 1 ? 'binary' : 'binaries'})`) |
| 603 | + results.forEach((file) => { |
| 604 | + console.log(` ${file}`) |
| 605 | + }) |
| 606 | + } |
| 607 | + else { |
| 608 | + console.log('⚠️ No binaries were installed') |
| 609 | + } |
425 | 610 | }
|
426 | 611 | }
|
427 | 612 | catch (error) {
|
428 |
| - console.error('Installation failed:', error instanceof Error ? error.message : String(error)) |
| 613 | + if (!options.quiet) { |
| 614 | + console.error('Installation failed:', error instanceof Error ? error.message : String(error)) |
| 615 | + } |
429 | 616 | process.exit(1)
|
430 | 617 | }
|
431 | 618 | })
|
@@ -1164,52 +1351,6 @@ cli
|
1164 | 1351 | console.log(shellcode(testMode))
|
1165 | 1352 | })
|
1166 | 1353 |
|
1167 |
| -cli |
1168 |
| - .command('dev [dir]', 'Set up development environment for project dependencies') |
1169 |
| - .option('--dry-run', 'Show packages that would be installed without installing them') |
1170 |
| - .option('--quiet', 'Suppress non-error output') |
1171 |
| - .option('--shell', 'Output shell code for evaluation (use with eval)') |
1172 |
| - .action(async (dir?: string, options?: { dryRun?: boolean, quiet?: boolean, shell?: boolean }) => { |
1173 |
| - try { |
1174 |
| - 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 |
| - |
1182 |
| - await dump(targetDir, { |
1183 |
| - dryrun: options?.dryRun || false, |
1184 |
| - quiet: options?.quiet || isShellIntegration, // Force quiet for shell integration |
1185 |
| - shellOutput: isShellIntegration, |
1186 |
| - skipGlobal: process.env.NODE_ENV === 'test' || process.env.LAUNCHPAD_SKIP_GLOBAL_AUTO_SCAN === 'true', // Skip global packages only in test mode or when explicitly disabled |
1187 |
| - }) |
1188 |
| - } |
1189 |
| - catch (error) { |
1190 |
| - if (!options?.quiet && !options?.shell) { |
1191 |
| - console.error('Failed to set up dev environment:', error instanceof Error ? error.message : String(error)) |
1192 |
| - } |
1193 |
| - else if (options?.shell) { |
1194 |
| - // For shell mode, output robust fallback that ensures basic system tools are available |
1195 |
| - // This prevents shell integration from hanging or failing |
1196 |
| - console.log('# Environment setup failed, using system fallback') |
1197 |
| - console.log('# Ensure basic system paths are available') |
1198 |
| - console.log('for sys_path in /usr/local/bin /usr/bin /bin /usr/sbin /sbin; do') |
1199 |
| - console.log(' if [[ -d "$sys_path" && ":$PATH:" != *":$sys_path:"* ]]; then') |
1200 |
| - console.log(' export PATH="$PATH:$sys_path"') |
1201 |
| - console.log(' fi') |
1202 |
| - console.log('done') |
1203 |
| - console.log('# Clear command hash to ensure fresh lookups') |
1204 |
| - console.log('hash -r 2>/dev/null || true') |
1205 |
| - return |
1206 |
| - } |
1207 |
| - if (!options?.shell) { |
1208 |
| - process.exit(1) |
1209 |
| - } |
1210 |
| - } |
1211 |
| - }) |
1212 |
| - |
1213 | 1354 | cli
|
1214 | 1355 | .command('dev:integrate', 'Install shell integration hooks')
|
1215 | 1356 | .option('--uninstall', 'Remove shell integration hooks')
|
|
0 commit comments