package main import ( "fmt" "os" "os/exec" "path/filepath" "strings" "fn-registry/registry" ) func cmdRun(args []string) { if len(args) == 0 { fmt.Fprintln(os.Stderr, "usage: fn run [args...]") os.Exit(1) } idOrName := args[0] passArgs := args[1:] registryRoot := root() dbPath := filepath.Join(registryRoot, dbName) db, err := registry.Open(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "error: cannot open registry: %v\n", err) os.Exit(1) } defer db.Close() fn, err := resolveFunction(db, idOrName) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if fn.FilePath == "" { fmt.Fprintf(os.Stderr, "error: %s has no file_path in registry\n", fn.ID) os.Exit(1) } absPath := filepath.Join(registryRoot, fn.FilePath) if _, err := os.Stat(absPath); os.IsNotExist(err) { fmt.Fprintf(os.Stderr, "error: file not found: %s\n", absPath) os.Exit(1) } cmd, err := buildCommand(fn, registryRoot, absPath, passArgs) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin fmt.Fprintf(os.Stderr, "[fn run] %s (%s/%s) %s\n", fn.ID, fn.Lang, fn.Kind, strings.Join(passArgs, " ")) if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { os.Exit(exitErr.ExitCode()) } fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func resolveFunction(db *registry.DB, idOrName string) (*registry.Function, error) { fn, err := db.GetFunction(idOrName) if err == nil { return fn, nil } fns, err := db.GetFunctionsByName(idOrName) if err != nil { return nil, fmt.Errorf("lookup failed: %w", err) } if len(fns) == 0 { return nil, fmt.Errorf("function %q not found (tried as ID and name)", idOrName) } if len(fns) == 1 { return &fns[0], nil } var b strings.Builder fmt.Fprintf(&b, "ambiguous name %q — found %d matches:\n", idOrName, len(fns)) for _, f := range fns { fmt.Fprintf(&b, " %s (%s/%s)\n", f.ID, f.Lang, f.Kind) } fmt.Fprintf(&b, "use the full ID to disambiguate") return nil, fmt.Errorf("%s", b.String()) } func buildCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) { switch fn.Lang { case "go": return buildGoCommand(fn, registryRoot, absPath, args) case "py": return buildPyCommand(registryRoot, absPath, args) case "bash": return buildBashCommand(absPath, args) case "ts": return buildTsCommand(registryRoot, absPath, args) default: return nil, fmt.Errorf("unsupported lang %q for execution", fn.Lang) } } func buildGoCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) { dir := filepath.Dir(absPath) env := append(os.Environ(), "CGO_ENABLED=1") // If directory has main.go → go run . (pipelines and standalone executables) mainGo := filepath.Join(dir, "main.go") if _, err := os.Stat(mainGo); err == nil { cmdArgs := append([]string{"run", "."}, args...) cmd := exec.Command("go", cmdArgs...) cmd.Dir = dir cmd.Env = env return cmd, nil } // Library code: if it has tests → go test if fn.Tested && fn.TestFilePath != "" { testAbs := filepath.Join(registryRoot, fn.TestFilePath) if _, err := os.Stat(testAbs); err == nil { relPkg, _ := filepath.Rel(registryRoot, dir) pkgPath := "./" + filepath.ToSlash(relPkg) cmdArgs := append([]string{"test", "-v", "-count=1", "-tags", "fts5", pkgPath}, args...) cmd := exec.Command("go", cmdArgs...) cmd.Dir = registryRoot cmd.Env = env return cmd, nil } } // No tests: go vet (compilation check) relPkg, _ := filepath.Rel(registryRoot, dir) pkgPath := "./" + filepath.ToSlash(relPkg) cmdArgs := []string{"vet", "-tags", "fts5", pkgPath} cmd := exec.Command("go", cmdArgs...) cmd.Dir = registryRoot cmd.Env = env fmt.Fprintf(os.Stderr, "[fn run] %s is library code without tests — running go vet\n", fn.ID) return cmd, nil } func buildPyCommand(registryRoot, absPath string, args []string) (*exec.Cmd, error) { venvPython := filepath.Join(registryRoot, "python", ".venv", "bin", "python3") pythonBin := "python3" if _, err := os.Stat(venvPython); err == nil { pythonBin = venvPython } dir := filepath.Dir(absPath) // If the file is inside a package (has __init__.py), use python -m // so relative imports work. PYTHONPATH points to python/functions/ or // the equivalent parent that contains the domain packages. initPy := filepath.Join(dir, "__init__.py") if _, err := os.Stat(initPy); err == nil { // The pythonPath is the well-known python/functions/ directory // which contains domain packages (metabase/, etc.) pythonPath := filepath.Join(registryRoot, "python", "functions") if _, err := os.Stat(pythonPath); os.IsNotExist(err) { // Fallback: walk up from dir to find the parent of the top package pythonPath = filepath.Dir(dir) } // Build module path: metabase/databases.py → metabase.databases relToRoot, _ := filepath.Rel(pythonPath, absPath) modPath := strings.TrimSuffix(relToRoot, ".py") modPath = strings.ReplaceAll(filepath.ToSlash(modPath), "/", ".") cmdArgs := append([]string{"-m", modPath}, args...) cmd := exec.Command(pythonBin, cmdArgs...) cmd.Dir = pythonPath cmd.Env = append(os.Environ(), "PYTHONPATH="+pythonPath) return cmd, nil } // Standalone script (no __init__.py) cmdArgs := append([]string{absPath}, args...) cmd := exec.Command(pythonBin, cmdArgs...) cmd.Dir = dir return cmd, nil } func buildBashCommand(absPath string, args []string) (*exec.Cmd, error) { cmdArgs := append([]string{absPath}, args...) cmd := exec.Command("bash", cmdArgs...) cmd.Dir = filepath.Dir(absPath) return cmd, nil } func buildTsCommand(registryRoot, absPath string, args []string) (*exec.Cmd, error) { tsxBin := filepath.Join(registryRoot, "frontend", "node_modules", ".bin", "tsx") if _, err := os.Stat(tsxBin); os.IsNotExist(err) { return nil, fmt.Errorf("tsx not found — run: cd frontend && pnpm add -D tsx") } cmdArgs := append([]string{absPath}, args...) cmd := exec.Command(tsxBin, cmdArgs...) cmd.Dir = filepath.Dir(absPath) return cmd, nil }