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, db, 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, db *registry.DB, registryRoot, absPath string, args []string) (*exec.Cmd, error) { switch fn.Lang { case "go": return buildGoCommand(fn, registryRoot, absPath, args) case "py": return buildPyRunnerCommand(fn, db, registryRoot, args) case "bash": return buildBashCommand(absPath, args) case "ts": return buildTsCommand(registryRoot, absPath, args) case "cpp": return buildCppCommand(fn, 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 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 }