diff --git a/pkg/agentfs/sdk_version_check.go b/pkg/agentfs/sdk_version_check.go index b42a33cc..982da508 100644 --- a/pkg/agentfs/sdk_version_check.go +++ b/pkg/agentfs/sdk_version_check.go @@ -11,6 +11,7 @@ import ( "github.com/BurntSushi/toml" "github.com/Masterminds/semver/v3" + "github.com/livekit/livekit-cli/v2/pkg/util" ) // PackageInfo represents information about a package found in a project @@ -30,6 +31,14 @@ type VersionCheckResult struct { Error error } +// SourceType indicates whether we're checking a lock file or package file +type SourceType int + +const ( + SourceTypePackage SourceType = iota // Package file (e.g., requirements.txt, package.json) + SourceTypeLock // Lock file (e.g., package-lock.json, poetry.lock) +) + // CheckSDKVersion performs a comprehensive check for livekit-agents packages func CheckSDKVersion(dir string, projectType ProjectType, settingsMap map[string]string) error { pythonMinSDKVersion := settingsMap["python-min-sdk-version"] @@ -57,7 +66,7 @@ func CheckSDKVersion(dir string, projectType ProjectType, settingsMap map[string // Find the best result (prefer lock files over source files) bestResult := findBestResult(results) if bestResult == nil { - return fmt.Errorf("package %s not found in any project files", getTargetPackageName(projectType)) + return fmt.Errorf("package %s not found in any project files", projectType.TargetPackageName()) } if !bestResult.Satisfied { @@ -86,7 +95,7 @@ func detectProjectFiles(dir string, projectType ProjectType) []string { "uv.lock", } for _, filename := range pythonFiles { - if path := filepath.Join(dir, filename); fileExists(path) { + if path := filepath.Join(dir, filename); util.FileExists(path) { files = append(files, path) } } @@ -99,7 +108,7 @@ func detectProjectFiles(dir string, projectType ProjectType) []string { "bun.lockb", } for _, filename := range nodeFiles { - if path := filepath.Join(dir, filename); fileExists(path) { + if path := filepath.Join(dir, filename); util.FileExists(path) { files = append(files, path) } } @@ -156,11 +165,16 @@ func parsePythonPackageVersion(line string) (string, bool) { return "latest", true } + // Convert Python operators to semver operators + if operator == "==" { + operator = "=" + } + // clean up the version string if it contains multiple constraints // handle comma-separated version constraints like ">=1.2.5,<2" if strings.Contains(version, ",") { - parts := strings.Split(version, ",") - for _, part := range parts { + parts := strings.SplitSeq(version, ",") + for part := range parts { trimmed := strings.TrimSpace(part) if regexp.MustCompile(`\d`).MatchString(trimmed) { if strings.ContainsAny(trimmed, "=~><") { @@ -205,7 +219,7 @@ func checkRequirementsFile(filePath, minVersion string) VersionCheckResult { version, found := parsePythonPackageVersion(line) if found { - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypePackage) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "livekit-agents", @@ -243,7 +257,7 @@ func checkPyprojectToml(filePath, minVersion string) VersionCheckResult { if line, ok := dep.(string); ok { version, found := parsePythonPackageVersion(line) if found { - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypePackage) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "livekit-agents", @@ -281,7 +295,7 @@ func checkPipfile(filePath, minVersion string) VersionCheckResult { version = "latest" } - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypePackage) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "livekit-agents", @@ -326,7 +340,7 @@ func checkSetupPy(filePath, minVersion string) VersionCheckResult { } version, found := parsePythonPackageVersion(packageLine) if found { - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypePackage) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "livekit-agents", @@ -360,7 +374,7 @@ func checkSetupCfg(filePath, minVersion string) VersionCheckResult { if matches != nil { version := strings.TrimSpace(matches[2]) - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypePackage) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "livekit-agents", @@ -407,7 +421,7 @@ func checkPackageJSON(filePath, minVersion string) VersionCheckResult { for _, deps := range dependencyMaps { if version, ok := deps["@livekit/agents"]; ok { - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypePackage) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "@livekit/agents", @@ -467,7 +481,7 @@ func checkPackageLockJSON(filePath, minVersion string) VersionCheckResult { } if dep, ok := lockJSON.Dependencies["@livekit/agents"]; ok { - satisfied, err := isVersionSatisfied(dep.Version, minVersion) + satisfied, err := isVersionSatisfied(dep.Version, minVersion, SourceTypeLock) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "@livekit/agents", @@ -497,7 +511,7 @@ func checkYarnLock(filePath, minVersion string) VersionCheckResult { matches := pattern.FindStringSubmatch(string(content)) if matches != nil { version := matches[1] - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypeLock) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "@livekit/agents", @@ -527,7 +541,7 @@ func checkPnpmLock(filePath, minVersion string) VersionCheckResult { matches := pattern.FindStringSubmatch(string(content)) if matches != nil { version := strings.TrimSpace(matches[1]) - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypeLock) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "@livekit/agents", @@ -557,7 +571,7 @@ func checkPoetryLock(filePath, minVersion string) VersionCheckResult { matches := pattern.FindStringSubmatch(string(content)) if matches != nil { version := matches[1] - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypeLock) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "livekit-agents", @@ -582,23 +596,37 @@ func checkUvLock(filePath, minVersion string) VersionCheckResult { return VersionCheckResult{Error: err} } - // Look for livekit-agents in the lock file - pattern := regexp.MustCompile(`(?m)^\s*livekit-agents\s*=\s*"([^"]+)"`) - matches := pattern.FindStringSubmatch(string(content)) - if matches != nil { - version := matches[1] - satisfied, err := isVersionSatisfied(version, minVersion) - return VersionCheckResult{ - PackageInfo: PackageInfo{ - Name: "livekit-agents", - Version: version, - FoundInFile: filePath, - ProjectType: ProjectTypePythonUV, - Ecosystem: "pypi", - }, - MinVersion: minVersion, - Satisfied: satisfied, - Error: err, + type uvLockPackage struct { + Name string `toml:"name"` + Version string `toml:"version"` + } + + type uvLockFile struct { + Packages []uvLockPackage `toml:"package"` + } + + var uvLock uvLockFile + if err := toml.Unmarshal(content, &uvLock); err != nil { + return VersionCheckResult{Error: err} + } + + // Check for livekit-agents in the packages + for _, pkg := range uvLock.Packages { + if pkg.Name == "livekit-agents" { + version := pkg.Version + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypeLock) + return VersionCheckResult{ + PackageInfo: PackageInfo{ + Name: "livekit-agents", + Version: version, + FoundInFile: filePath, + ProjectType: ProjectTypePythonUV, + Ecosystem: "pypi", + }, + MinVersion: minVersion, + Satisfied: satisfied, + Error: err, + } } } @@ -617,7 +645,7 @@ func checkPipfileLock(filePath, minVersion string) VersionCheckResult { matches := pattern.FindStringSubmatch(string(content)) if matches != nil { version := matches[1] - satisfied, err := isVersionSatisfied(version, minVersion) + satisfied, err := isVersionSatisfied(version, minVersion, SourceTypeLock) return VersionCheckResult{ PackageInfo: PackageInfo{ Name: "livekit-agents", @@ -636,48 +664,79 @@ func checkPipfileLock(filePath, minVersion string) VersionCheckResult { } // isVersionSatisfied checks if a version satisfies the minimum requirement -func isVersionSatisfied(version, minVersion string) (bool, error) { +func isVersionSatisfied(version, minVersion string, sourceType SourceType) (bool, error) { // Handle special cases if version == "latest" || version == "*" || version == "" { return true, nil // Latest version always satisfies } - // Normalize version strings - normalizedVersion := normalizeVersion(version) - normalizedMin := normalizeVersion(minVersion) + switch sourceType { + case SourceTypeLock: + // For lock files, we have the exact version that was installed + // Check if this exact version is >= the minimum version + normalizedVersion := normalizeVersion(version, sourceType) + v, err := semver.NewVersion(normalizedVersion) + if err != nil { + return false, fmt.Errorf("failed to extract base version for %s: %w", version, err) + } - // Parse versions - v, err := semver.NewVersion(normalizedVersion) - if err != nil { - return false, fmt.Errorf("invalid version format: %s", version) - } + min, err := semver.NewVersion(minVersion) + if err != nil { + return false, fmt.Errorf("invalid minimum version format: %s", minVersion) + } - min, err := semver.NewVersion(normalizedMin) - if err != nil { - return false, fmt.Errorf("invalid minimum version format: %s", minVersion) - } + // Check if the exact version is >= minimum version + return !v.LessThan(min), nil - return !v.LessThan(min), nil -} + case SourceTypePackage: + // For package files, we may have a constraint that will be resolved at install time. -// normalizeVersion normalizes version strings for semver parsing -func normalizeVersion(version string) string { - // Remove common prefixes and suffixes - version = strings.TrimSpace(version) - version = strings.Trim(version, " \"'") + // First, we check if the normalized version is greater than or equal to the minimum version + // This is safe because in < and <= checks, the newest version will always be installed and in + // ^, ~ and >= checks, if the lower bound is greater than the minimum SDK version, we're good. + normalizedVersion := normalizeVersion(version, sourceType) + baseVersion, err := semver.NewVersion(normalizedVersion) + if err != nil { + return false, fmt.Errorf("failed to extract base version for %s: %w", version, err) + } + + min, err := semver.NewVersion(minVersion) + if err != nil { + return false, fmt.Errorf("invalid minimum version format: %s", minVersion) + } + + if baseVersion.GreaterThanEqual(min) { + return true, nil + } - // Remove version specifiers that aren't part of the version itself - version = regexp.MustCompile(`^[=~>= minimum. + + return false, nil - // Handle npm version ranges - if strings.HasPrefix(version, "^") || strings.HasPrefix(version, "~") { - version = version[1:] + default: + return false, fmt.Errorf("unknown source type: %d", sourceType) } +} +// Cleans up version strings for parsing +func normalizeVersion(version string, sourceType SourceType) string { + // Remove whitespace, quotes, and version range specifiers + version = strings.TrimSpace(version) + version = strings.Trim(version, `"'^~><=`) return version } -// findBestResult finds the best result from multiple package checks +// Finds the best possible source for version checks func findBestResult(results []VersionCheckResult) *VersionCheckResult { if len(results) == 0 { return nil @@ -720,21 +779,3 @@ func findBestResult(results []VersionCheckResult) *VersionCheckResult { return bestResult } - -// getTargetPackageName returns the target package name for the project type -func getTargetPackageName(projectType ProjectType) string { - switch projectType { - case ProjectTypePythonPip, ProjectTypePythonUV: - return "livekit-agents" - case ProjectTypeNode: - return "@livekit/agents" - default: - return "" - } -} - -// fileExists checks if a file exists -func fileExists(path string) bool { - _, err := os.Stat(path) - return err == nil -} diff --git a/pkg/agentfs/sdk_version_check_test.go b/pkg/agentfs/sdk_version_check_test.go index 1acb64a1..acd7036f 100644 --- a/pkg/agentfs/sdk_version_check_test.go +++ b/pkg/agentfs/sdk_version_check_test.go @@ -1,6 +1,7 @@ package agentfs import ( + "fmt" "os" "path/filepath" "testing" @@ -110,6 +111,19 @@ livekit-agents = ">=1.0.0"`, expectError: true, errorMsg: "too old", }, + { + name: "Node package.json with good version", + projectType: ProjectTypeNode, + setupFiles: map[string]string{ + "package.json": `{ + "dependencies": { + "@livekit/agents": "^1.1.1" + } +}`, + }, + expectError: false, + }, + { name: "Node package-lock.json with valid version", projectType: ProjectTypeNode, @@ -138,7 +152,15 @@ version = "1.5.0"`, name: "Python uv.lock with valid version", projectType: ProjectTypePythonUV, setupFiles: map[string]string{ - "uv.lock": `livekit-agents = "1.5.0"`, + "uv.lock": `[[package]] +name = "livekit-agents" +version = "1.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "watchfiles" }, +] +`, }, expectError: false, }, @@ -196,26 +218,46 @@ func TestIsVersionSatisfied(t *testing.T) { tests := []struct { version string minVersion string + sourceType SourceType expected bool expectErr bool }{ - {"1.5.0", "1.0.0", true, false}, - {"1.0.0", "1.0.0", true, false}, - {"0.9.0", "1.0.0", false, false}, - {"latest", "1.0.0", true, false}, - {"*", "1.0.0", true, false}, - {"", "1.0.0", true, false}, - {"^1.5.0", "1.0.0", true, false}, - {"~1.5.0", "1.0.0", true, false}, - {">=1.5.0", "1.0.0", true, false}, - {"==1.5.0", "1.0.0", true, false}, - {"invalid", "1.0.0", false, true}, - {"1.5.0", "invalid", false, true}, + // Lock file tests (exact version matching) + {"1.5.0", "1.0.0", SourceTypeLock, true, false}, + {"1.0.0", "1.0.0", SourceTypeLock, true, false}, + {"0.9.0", "1.0.0", SourceTypeLock, false, false}, + {"1.5.0", "2.0.0", SourceTypeLock, false, false}, + {"2.0.0", "2.0.0", SourceTypeLock, true, false}, + + // Package file tests (constraint satisfaction) + {">=1.5.0", "1.0.0", SourceTypePackage, true, false}, + {"<2.0.0", "1.0.0", SourceTypePackage, true, false}, + {">=2.0.0", "1.0.0", SourceTypePackage, true, false}, + {"~1.2.0", "1.0.0", SourceTypePackage, true, false}, + {"^1.0.0", "1.0.0", SourceTypePackage, true, false}, + // Test the specific case that was failing: ^0.7.9 should satisfy minimum 0.0.7 + {"^0.7.9", "0.0.7", SourceTypePackage, true, false}, + // Test other caret scenarios + {"^0.5.0", "0.0.7", SourceTypePackage, true, false}, // ^0.5.0 allows 0.5.0+ which >= 0.0.7 + {"^1.0.0", "0.0.7", SourceTypePackage, true, false}, // 1.0.0+ >= 0.0.7 + + // Special cases + {"latest", "1.0.0", SourceTypeLock, true, false}, + {"*", "1.0.0", SourceTypeLock, true, false}, + {"", "1.0.0", SourceTypeLock, true, false}, + + // Error cases + {"invalid", "1.0.0", SourceTypeLock, false, true}, + {"1.5.0", "invalid", SourceTypeLock, false, true}, } for _, tt := range tests { - t.Run(tt.version+"_vs_"+tt.minVersion, func(t *testing.T) { - result, err := isVersionSatisfied(tt.version, tt.minVersion) + sourceTypeStr := "Package" + if tt.sourceType == SourceTypeLock { + sourceTypeStr = "Lock" + } + t.Run(fmt.Sprintf("%s_vs_%s_%s", tt.version, tt.minVersion, sourceTypeStr), func(t *testing.T) { + result, err := isVersionSatisfied(tt.version, tt.minVersion, tt.sourceType) if tt.expectErr { if err == nil { @@ -235,24 +277,45 @@ func TestIsVersionSatisfied(t *testing.T) { func TestNormalizeVersion(t *testing.T) { tests := []struct { - input string - expected string + input string + sourceType SourceType + expected string + description string }{ - {"1.5.0", "1.5.0"}, - {"^1.5.0", "1.5.0"}, - {"~1.5.0", "1.5.0"}, - {">=1.5.0", "1.5.0"}, - {"==1.5.0", "1.5.0"}, - {" 1.5.0 ", "1.5.0"}, - {`"1.5.0"`, "1.5.0"}, - {`'1.5.0'`, "1.5.0"}, - {"*", "*"}, - {"latest", "latest"}, + // Lock file tests (should remove ^ and ~) + {"1.5.0", SourceTypeLock, "1.5.0", "exact version"}, + {"^1.5.0", SourceTypeLock, "1.5.0", "npm caret removed for lock file"}, + {"~1.5.0", SourceTypeLock, "1.5.0", "npm tilde removed for lock file"}, + {">=1.5.0", SourceTypeLock, "1.5.0", "semver operators removed for lock file"}, + {"<2.0.0", SourceTypeLock, "2.0.0", "semver operators removed for lock file"}, + {"==1.5.0", SourceTypeLock, "1.5.0", "semver operators removed for lock file"}, + {" 1.5.0 ", SourceTypeLock, "1.5.0", "whitespace removed"}, + {`"1.5.0"`, SourceTypeLock, "1.5.0", "quotes removed"}, + {`'1.5.0'`, SourceTypeLock, "1.5.0", "quotes removed"}, + {"*", SourceTypeLock, "*", "wildcard preserved"}, + {"latest", SourceTypeLock, "latest", "latest preserved"}, + + // Package file tests (should preserve ^ and ~) + {"1.5.0", SourceTypePackage, "1.5.0", "exact version"}, + {"^1.5.0", SourceTypePackage, "1.5.0", "npm caret removed for package file"}, + {"~1.5.0", SourceTypePackage, "1.5.0", "npm tilde removed for package file"}, + {">=1.5.0", SourceTypePackage, "1.5.0", "semver operators removed for package file"}, + {"<2.0.0", SourceTypePackage, "2.0.0", "semver operators removed for package file"}, + {"==1.5.0", SourceTypePackage, "1.5.0", "semver operators removed for package file"}, + {" 1.5.0 ", SourceTypePackage, "1.5.0", "whitespace removed"}, + {`"1.5.0"`, SourceTypePackage, "1.5.0", "quotes removed"}, + {`'1.5.0'`, SourceTypePackage, "1.5.0", "quotes removed"}, + {"*", SourceTypePackage, "*", "wildcard preserved"}, + {"latest", SourceTypePackage, "latest", "latest preserved"}, } for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - result := normalizeVersion(tt.input) + sourceTypeStr := "Package" + if tt.sourceType == SourceTypeLock { + sourceTypeStr = "Lock" + } + t.Run(fmt.Sprintf("%s_%s_%s", tt.input, sourceTypeStr, tt.description), func(t *testing.T) { + result := normalizeVersion(tt.input, tt.sourceType) if result != tt.expected { t.Errorf("Expected %s but got %s", tt.expected, result) } @@ -354,7 +417,7 @@ func TestGetTargetPackageName(t *testing.T) { for _, tt := range tests { t.Run(string(tt.projectType), func(t *testing.T) { - result := getTargetPackageName(tt.projectType) + result := tt.projectType.TargetPackageName() if result != tt.expected { t.Errorf("Expected %s but got %s", tt.expected, result) } @@ -378,81 +441,29 @@ func contains(s, substr string) bool { func TestParsePythonPackageVersion(t *testing.T) { tests := []struct { - name string - input string - expectedOutput string - expectedFound bool + input string + expected string }{ - { - name: "Simple version", - input: "livekit-agents==1.5.0", - expectedOutput: "==1.5.0", - expectedFound: true, - }, - { - name: "Version with extras", - input: "livekit-agents[extra]==1.5.0", - expectedOutput: "==1.5.0", - expectedFound: true, - }, - { - name: "Version with no specifier", - input: "livekit-agents", - expectedOutput: "latest", - expectedFound: true, - }, - { - name: "Version with greater than", - input: "livekit-agents>=1.5.0", - expectedOutput: ">=1.5.0", - expectedFound: true, - }, - { - name: "Comma-separated constraints", - input: "livekit-agents>=1.2.5,<2", - expectedOutput: ">=1.2.5", - expectedFound: true, - }, - { - name: "Space-separated constraints", - input: "livekit-agents>=1.2.5 <2", - expectedOutput: ">=1.2.5", - expectedFound: true, - }, - { - name: "Not livekit-agents", - input: "some-other-package==1.0.0", - expectedOutput: "", - expectedFound: false, - }, - { - name: "Git URL format", - input: "livekit-agents[openai,turn-detector,silero,cartesia,deepgram] @ git+https://github.com/livekit/agents.git@load-debug#subdirectory=livekit-agents", - expectedOutput: "latest", - expectedFound: true, - }, - { - name: "Git URL format with extras", - input: "livekit-agents[voice] @ git+https://github.com/livekit/agents.git@main", - expectedOutput: "latest", - expectedFound: true, - }, - { - name: "Git URL format simple", - input: "livekit-agents @ git+https://github.com/livekit/agents.git", - expectedOutput: "latest", - expectedFound: true, - }, + {"livekit-agents==1.5.0", "=1.5.0"}, + {"livekit-agents[extra]==1.5.0", "=1.5.0"}, + {"livekit-agents", "latest"}, + {"livekit-agents>=1.5.0", ">=1.5.0"}, + {"livekit-agents>=1.2.5,<2", ">=1.2.5"}, + {"livekit-agents 1.5.0", "1.5.0"}, + {"requests==2.25.1", ""}, + {"livekit-agents@git+https://github.com/livekit/livekit-agents.git", "latest"}, + {"livekit-agents[extra]@git+https://github.com/livekit/livekit-agents.git", "latest"}, + {"livekit-agents@git+https://github.com/livekit/livekit-agents.git", "latest"}, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.input, func(t *testing.T) { output, found := parsePythonPackageVersion(tt.input) - if found != tt.expectedFound { - t.Errorf("Expected found=%v, got %v", tt.expectedFound, found) + if found != (tt.expected != "") { // Expect found to be true if expected is not empty + t.Errorf("Expected found=%v, got %v", tt.expected != "", found) } - if output != tt.expectedOutput { - t.Errorf("Expected output=%q, got %q", tt.expectedOutput, output) + if output != tt.expected { + t.Errorf("Expected output=%q, got %q", tt.expected, output) } }) } diff --git a/pkg/agentfs/utils.go b/pkg/agentfs/utils.go index 8dc49dcb..14b65f25 100644 --- a/pkg/agentfs/utils.go +++ b/pkg/agentfs/utils.go @@ -65,6 +65,17 @@ func (p ProjectType) FileExt() string { } } +func (p ProjectType) TargetPackageName() string { + switch p { + case ProjectTypePythonPip, ProjectTypePythonUV: + return "livekit-agents" + case ProjectTypeNode: + return "@livekit/agents" + default: + return "" + } +} + func LocateLockfile(dir string, p ProjectType) (bool, string) { pythonFiles := []string{ "requirements.txt", @@ -101,21 +112,21 @@ func LocateLockfile(dir string, p ProjectType) (bool, string) { func DetectProjectType(dir string) (ProjectType, error) { // Node.js detection - if util.FileExists(dir, "package.json") { + if util.FileExistsInDir(dir, "package.json") { return ProjectTypeNode, nil } // Python detection - if util.FileExists(dir, "uv.lock") { + if util.FileExistsInDir(dir, "uv.lock") { return ProjectTypePythonUV, nil } - if util.FileExists(dir, "poetry.lock") || util.FileExists(dir, "Pipfile.lock") { + if util.FileExistsInDir(dir, "poetry.lock") || util.FileExistsInDir(dir, "Pipfile.lock") { return ProjectTypePythonPip, nil // We can treat as pip-compatible } - if util.FileExists(dir, "requirements.txt") { + if util.FileExistsInDir(dir, "requirements.txt") { return ProjectTypePythonPip, nil } - if util.FileExists(dir, "pyproject.toml") { + if util.FileExistsInDir(dir, "pyproject.toml") { tomlPath := filepath.Join(dir, "pyproject.toml") data, err := os.ReadFile(tomlPath) if err == nil { diff --git a/pkg/util/fs.go b/pkg/util/fs.go index 39ce09d1..4d8f9cc8 100644 --- a/pkg/util/fs.go +++ b/pkg/util/fs.go @@ -24,7 +24,12 @@ import ( "github.com/livekit/protocol/utils/guid" ) -func FileExists(dir, filename string) bool { +func FileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func FileExistsInDir(dir, filename string) bool { _, err := os.Stat(filepath.Join(dir, filename)) return err == nil }