Skip to content

Commit 9d467a4

Browse files
committed
feat: Fix and complete support for 11 package managers in agent creation
This commit adds comprehensive support for creating agents across these python and node.js package managers: Python: - pip - UV - Poetry - Hatch - PDM - Pipenv Node.js: - npm - pnpm - Yarn (v1) - Yarn Berry (v2+) - Bun Key improvements: - Updated and working project detection for all 11 package managers - Fixed SDK version check to handle package-specific project files - Added TypeScript build detection with helpful error messages - Allow checking build output directories (dist/) despite .dockerignore for node projects - Added validation warnings for missing lock files per package manager - Better error handling when entrypoint cannot be detected
1 parent 0171b8e commit 9d467a4

File tree

3 files changed

+280
-81
lines changed

3 files changed

+280
-81
lines changed

pkg/agentfs/docker.go

Lines changed: 121 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,12 @@ func validateYarnBerryProject(dir string, silent bool) {
296296
}
297297

298298
func validateBunProject(dir string, silent bool) {
299-
bunLockPath := filepath.Join(dir, "bun.lockb")
299+
bunLockPath := filepath.Join(dir, "bun.lock")
300300
if _, err := os.Stat(bunLockPath); err != nil {
301301
if !silent {
302-
fmt.Printf("! Warning: Bun project detected but %s file not found\n", util.Accented("bun.lockb"))
303-
fmt.Printf(" Consider running %s to generate %s for reproducible builds\n", util.Accented("bun install"), util.Accented("bun.lockb"))
304-
fmt.Printf(" This ensures consistent dependency versions across environments\n")
305-
fmt.Printf(" Note: bun.lockb is a binary file, ensure it's committed to version control\n\n")
302+
fmt.Printf("! Warning: Bun project detected but %s file not found\n", util.Accented("bun.lock"))
303+
fmt.Printf(" Consider running %s to generate %s for reproducible builds\n", util.Accented("bun install"), util.Accented("bun.lock"))
304+
fmt.Printf(" This ensures consistent dependency versions across environments\n\n")
306305
}
307306
}
308307
}
@@ -320,18 +319,55 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
320319
return "", fmt.Errorf("failed to create pattern matcher: %w", err)
321320
}
322321

323-
// Recursively find all relevant files, respecting dockerignore
322+
// For Node.js projects, we need to check build output directories even if dockerignore excludes them
323+
// because these directories will be created during the Docker build
324+
allowedPaths := make(map[string]bool)
325+
if projectType.IsNode() {
326+
// Try to detect the output directory from tsconfig.json or use defaults per package manager
327+
// Default TypeScript output directories by convention
328+
switch projectType {
329+
case ProjectTypeNodeNPM, ProjectTypeNodePNPM:
330+
// npm and pnpm typically use 'dist' for TypeScript builds
331+
allowedPaths["dist"] = true
332+
allowedPaths["build"] = true
333+
case ProjectTypeNodeYarn, ProjectTypeNodeYarnBerry:
334+
// Yarn projects often use 'dist' or 'lib'
335+
allowedPaths["dist"] = true
336+
allowedPaths["lib"] = true
337+
case ProjectTypeNodeBun:
338+
// Bun can use 'dist' or 'out'
339+
allowedPaths["dist"] = true
340+
allowedPaths["out"] = true
341+
}
342+
343+
// TODO: Parse tsconfig.json "outDir" if it exists to get the actual output directory
344+
// For now, we're using common conventions
345+
}
346+
347+
// Recursively find all relevant files, respecting dockerignore (with exceptions)
324348
fileMap := make(map[string]bool)
325349
if err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
326350
if err != nil {
327351
return err
328352
}
329353

330-
// Skip files that match .dockerignore patterns
331-
if ignored, err := matcher.MatchesOrParentMatches(path); ignored {
332-
return nil
333-
} else if err != nil {
334-
return err
354+
// Check if this path should be allowed regardless of dockerignore
355+
relPath, _ := filepath.Rel(dir, path)
356+
shouldAllow := false
357+
for allowedDir := range allowedPaths {
358+
if strings.HasPrefix(relPath, allowedDir+string(filepath.Separator)) || relPath == allowedDir {
359+
shouldAllow = true
360+
break
361+
}
362+
}
363+
364+
// Skip files that match .dockerignore patterns (unless explicitly allowed)
365+
if !shouldAllow {
366+
if ignored, err := matcher.MatchesOrParentMatches(path); ignored {
367+
return nil
368+
} else if err != nil {
369+
return err
370+
}
335371
}
336372

337373
// Only include files with the correct extension
@@ -359,31 +395,46 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
359395

360396
if projectType.IsPython() {
361397
// Common Python entry point patterns in order of preference
362-
priorityOrder = []string{
363-
"agent.py", // LiveKit agents
364-
"src/agent.py", // LiveKit agents in src
365-
"main.py", // Most common
366-
"src/main.py", // Modern src layout
367-
"app.py", // Flask/web apps
368-
"src/app.py", // Flask/web apps in src
369-
"__main__.py", // Python module entry
370-
"src/__main__.py", // Python module entry in src
398+
priorityDirs := []string{
399+
"",
400+
"src",
401+
}
402+
priorityFiles := []string{
403+
"agent.py",
404+
"main.py",
405+
"app.py",
406+
}
407+
408+
// search the for the files in priority order in each of the directories in priority order
409+
for _, file := range priorityFiles {
410+
for _, dir := range priorityDirs {
411+
if dir != "" {
412+
dir = dir + "/"
413+
}
414+
priorityOrder = append(priorityOrder, fmt.Sprintf("%s%s", dir, file))
415+
}
371416
}
372417
} else if projectType.IsNode() {
373418
// Common Node.js entry point patterns
374-
priorityOrder = []string{
375-
"dist/agent.js", // Built TypeScript output
376-
"dist/index.js", // Built TypeScript output
377-
"dist/main.js", // Built TypeScript output
378-
"dist/app.js", // Built TypeScript output
419+
priorityDirs := []string{
420+
"dist",
421+
"",
422+
"src",
423+
}
424+
priorityFiles := []string{
379425
"agent.js",
380426
"index.js",
381427
"main.js",
382428
"app.js",
383-
"src/agent.js",
384-
"src/index.js",
385-
"src/main.js",
386-
"src/app.js",
429+
}
430+
// search through each directory in order for .js files per the priority order
431+
for _, dir := range priorityDirs {
432+
for _, file := range priorityFiles {
433+
if dir != "" {
434+
dir = dir + "/"
435+
}
436+
priorityOrder = append(priorityOrder, fmt.Sprintf("%s%s.js", dir, file))
437+
}
387438
}
388439
}
389440

@@ -406,8 +457,40 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
406457
return candidates[0], nil
407458
}
408459

409-
// If no matching files found, return early
460+
// If no matching files found, check for TypeScript files and provide helpful message
410461
if len(candidates) == 0 {
462+
// For Node.js projects, check if there are TypeScript files
463+
if projectType.IsNode() {
464+
hasTSFiles := false
465+
for fileName := range fileMap {
466+
// Check if any .ts files exist (but not .d.ts)
467+
if strings.HasSuffix(fileName, ".ts") && !strings.HasSuffix(fileName, ".d.ts") {
468+
hasTSFiles = true
469+
break
470+
}
471+
}
472+
473+
if hasTSFiles {
474+
// Check specifically for common TypeScript source files
475+
tsFiles := []string{"agent.ts", "index.ts", "main.ts", "app.ts", "src/agent.ts", "src/index.ts", "src/main.ts", "src/app.ts"}
476+
foundTS := []string{}
477+
for _, tsFile := range tsFiles {
478+
if info, err := os.Stat(filepath.Join(dir, tsFile)); err == nil && !info.IsDir() {
479+
foundTS = append(foundTS, tsFile)
480+
}
481+
}
482+
483+
if len(foundTS) > 0 {
484+
displayCount := 3
485+
if len(foundTS) < displayCount {
486+
displayCount = len(foundTS)
487+
}
488+
fmt.Printf("\nTypeScript files detected (%s) but no compiled JavaScript files found.\n", strings.Join(foundTS[:displayCount], ", "))
489+
fmt.Printf("Please build your project first using: %s\n\n", util.Accented("npm run build"))
490+
return "", fmt.Errorf("no compiled JavaScript files found - run build command first")
491+
}
492+
}
493+
}
411494
return "", nil
412495
}
413496

@@ -493,6 +576,14 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
493576
return nil, err
494577
}
495578

579+
// Check if entrypoint is empty and provide helpful error message
580+
if newEntrypoint == "" {
581+
if projectType.IsNode() {
582+
return nil, fmt.Errorf("failed to detect entrypoint script - no JavaScript files found. If using TypeScript, run 'npm run build' (or equivalent) first")
583+
}
584+
return nil, fmt.Errorf("failed to detect entrypoint script - no %s files found in the project", projectType.FileExt())
585+
}
586+
496587
tpl := template.Must(template.New("Dockerfile").Parse(string(dockerfileContent)))
497588
buf := &bytes.Buffer{}
498589
tpl.Execute(buf, map[string]string{

0 commit comments

Comments
 (0)