Skip to main content
Glama
package_json.go5.98 kB
package parser import ( "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "time" ) type PackageJSON struct { Name string `json:"name"` Version string `json:"version"` Description string `json:"description"` Scripts map[string]string `json:"scripts"` Dependencies map[string]string `json:"dependencies"` DevDependencies map[string]string `json:"devdependencies"` Engines map[string]string `json:"engines"` PackageManager string `json:"packageManager"` Private bool `json:"private"` Workspaces interface{} `json:"workspaces"` // Can be array or object } type PackageManager string const ( NPM PackageManager = "npm" Yarn PackageManager = "yarn" PNPM PackageManager = "pnpm" Bun PackageManager = "bun" ) func ParsePackageJSON(path string) (*PackageJSON, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read package.json: %w", err) } var pkg PackageJSON if err := json.Unmarshal(data, &pkg); err != nil { return nil, fmt.Errorf("failed to parse package.json: %w", err) } return &pkg, nil } func DetectPackageManager(projectPath string) PackageManager { if _, err := os.Stat(filepath.Join(projectPath, "bun.lockb")); err == nil { return Bun } if _, err := os.Stat(filepath.Join(projectPath, "pnpm-lock.yaml")); err == nil { return PNPM } if _, err := os.Stat(filepath.Join(projectPath, "yarn.lock")); err == nil { return Yarn } if _, err := os.Stat(filepath.Join(projectPath, "package-lock.json")); err == nil { return NPM } return NPM } func (pm PackageManager) RunCommand() string { switch pm { case Yarn: return "yarn" case PNPM: return "pnpm" case Bun: return "bun" default: return "npm" } } func (pm PackageManager) RunScriptCommand(script string) []string { switch pm { case Yarn: return []string{"yarn", "run", script} case PNPM: return []string{"pnpm", "run", script} case Bun: return []string{"bun", "run", script} default: return []string{"npm", "run", script} } } type InstalledPackageManager struct { Manager PackageManager Version string Path string } var ( cachedInstalledManagers []InstalledPackageManager cacheOnce sync.Once ) // DetectInstalledPackageManagers checks which package managers are installed on the system func DetectInstalledPackageManagers() []InstalledPackageManager { cacheOnce.Do(func() { cachedInstalledManagers = detectInstalledPackageManagersUncached() }) return cachedInstalledManagers } // detectInstalledPackageManagersUncached performs the actual detection func detectInstalledPackageManagersUncached() []InstalledPackageManager { var installed []InstalledPackageManager managers := []struct { name PackageManager command string versionArgs []string }{ {NPM, "npm", []string{"--version"}}, {Yarn, "yarn", []string{"--version"}}, {PNPM, "pnpm", []string{"--version"}}, {Bun, "bun", []string{"--version"}}, } for _, mgr := range managers { path, err := findExecutable(mgr.command) if err != nil { continue } // Get version with timeout ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd := exec.CommandContext(ctx, mgr.command, mgr.versionArgs...) output, err := cmd.Output() if err != nil { continue } version := strings.TrimSpace(string(output)) installed = append(installed, InstalledPackageManager{ Manager: mgr.name, Version: version, Path: path, }) } return installed } // findExecutable finds the full path to an executable, cross-platform func findExecutable(name string) (string, error) { // Use a channel to implement timeout for LookPath type result struct { path string err error } resultChan := make(chan result, 1) go func() { if runtime.GOOS == "windows" { // On Windows, try with common extensions extensions := []string{"", ".exe", ".cmd", ".bat"} for _, ext := range extensions { path, err := exec.LookPath(name + ext) if err == nil { resultChan <- result{path: path, err: nil} return } } resultChan <- result{path: "", err: fmt.Errorf("executable not found: %s", name)} } else { path, err := exec.LookPath(name) resultChan <- result{path: path, err: err} } }() // Wait for result with timeout select { case res := <-resultChan: return res.path, res.err case <-time.After(1 * time.Second): return "", fmt.Errorf("timeout finding executable: %s", name) } } // GetPreferredPackageManager determines the preferred package manager based on: // 1. User preference (if set) // 2. packageManager field in package.json // 3. engines field in package.json // 4. Lock file detection // 5. First installed manager func GetPreferredPackageManager(pkg *PackageJSON, projectPath string, userPreference *PackageManager) PackageManager { // 1. User preference takes precedence if userPreference != nil { return *userPreference } // Handle nil package.json (fallback mode) if pkg == nil { return DetectPackageManager(projectPath) } // 2. Check packageManager field (e.g., "yarn@3.2.0") if pkg.PackageManager != "" { parts := strings.Split(pkg.PackageManager, "@") if len(parts) > 0 { switch strings.ToLower(parts[0]) { case "npm": return NPM case "yarn": return Yarn case "pnpm": return PNPM case "bun": return Bun } } } // 3. Check engines field if pkg.Engines != nil { // Check in order of preference if _, hasYarn := pkg.Engines["yarn"]; hasYarn { return Yarn } if _, hasPnpm := pkg.Engines["pnpm"]; hasPnpm { return PNPM } if _, hasBun := pkg.Engines["bun"]; hasBun { return Bun } if _, hasNpm := pkg.Engines["npm"]; hasNpm { return NPM } } // 4. Lock file detection (existing logic) return DetectPackageManager(projectPath) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server