feat: fn run — ejecución multi-lenguaje de funciones y pipelines desde CLI
Nuevo comando que despacha automáticamente según lenguaje: Go pipelines con go run, Go functions con go test/vet, Python con venv y -m para imports relativos, Bash directo, TypeScript con tsx del frontend. Resolución por nombre con desambiguación. Añadido GetFunctionsByName al store y tsx al frontend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,8 @@ func main() {
|
||||
cmdOps(os.Args[2:])
|
||||
case "proposal":
|
||||
cmdProposal(os.Args[2:])
|
||||
case "run":
|
||||
cmdRun(os.Args[2:])
|
||||
case "help", "-h", "--help":
|
||||
printUsage()
|
||||
default:
|
||||
@@ -51,6 +53,7 @@ Usage:
|
||||
fn list [-k kind] [-d domain] [-l lang]
|
||||
fn show <id> Muestra entrada completa
|
||||
fn add [-k kind] Abre $EDITOR con template
|
||||
fn run <id_or_name> [args...] Ejecuta funcion/pipeline (go/py/bash)
|
||||
fn ops <subcommand> Gestiona operations.db (fn ops help)
|
||||
fn proposal <add|list|show|update> Gestiona proposals`)
|
||||
}
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
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 <id_or_name> [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
|
||||
}
|
||||
Reference in New Issue
Block a user