package main import ( "flag" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "fn-registry/functions/infra" ) func main() { // Flags scriptPath := flag.String("script", "", "Ruta al archivo YAML con el script de navegacion (obligatorio)") port := flag.Int("port", 9222, "Puerto CDP de Chrome") launch := flag.Bool("launch", false, "Lanzar Chrome nuevo en vez de conectarse a uno existente") headless := flag.Bool("headless", false, "Lanzar Chrome en modo headless (requiere --launch)") chromePath := flag.String("chrome-path", "", "Ruta al ejecutable de Chrome (ej: '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe')") userDataDir := flag.String("user-data-dir", "", "Directorio de perfil de Chrome (path WSL, se convierte a Windows automaticamente)") keepOpen := flag.Bool("keep-open", false, "No cerrar Chrome al terminar") abortOnError := flag.Bool("abort-on-error", false, "Abortar el script al primer error en cualquier paso") flag.Parse() if *scriptPath == "" { fmt.Fprintln(os.Stderr, "error: --script es obligatorio") flag.Usage() os.Exit(1) } if err := run(*scriptPath, *port, *launch, *headless, *abortOnError, *userDataDir, *keepOpen, *chromePath); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func run(scriptPath string, port int, launch, headless, abortOnError bool, userDataDir string, keepOpen bool, chromePath string) error { // 1. Cargar y validar el script YAML script, err := LoadScript(scriptPath) if err != nil { return fmt.Errorf("cargar script: %w", err) } fmt.Printf("[script_navegador] script: %q (%d pasos)\n", script.Name, len(script.Steps)) // 2. Inicializar operations.db appDir, err := filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { // Fallback al directorio de trabajo appDir, _ = os.Getwd() } // Si estamos corriendo con `go run .`, os.Args[0] es un tmp, usar cwd if cwd, e := os.Getwd(); e == nil { if _, e2 := os.Stat(filepath.Join(cwd, "app.md")); e2 == nil { appDir = cwd } } db, err := initOpsDB(appDir) if err != nil { // No es fatal: seguir sin operations.db, solo logear fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudo inicializar operations.db: %v\n", err) } if db != nil { defer db.Close() } // 3. Lanzar Chrome o conectarse al existente var pid int if launch { // Convertir path WSL a Windows para chrome.exe // Si empieza con / es un path Linux (WSL), convertir. Si empieza con letra:\ ya es Windows. winDataDir := userDataDir if winDataDir != "" && strings.HasPrefix(winDataDir, "/") { out, err := exec.Command("wslpath", "-w", winDataDir).Output() if err == nil { winDataDir = strings.TrimSpace(string(out)) } } fmt.Printf("[chrome] lanzando Chrome en puerto %d (headless=%v, user-data-dir=%q)...\n", port, headless, winDataDir) pid, err = infra.ChromeLaunch(infra.ChromeLaunchOpts{ Port: port, Headless: headless, UserDataDir: winDataDir, ChromePath: chromePath, }) if err != nil { return fmt.Errorf("lanzar Chrome: %w", err) } fmt.Printf("[chrome] Chrome lanzado (pid=%d)\n", pid) } else { fmt.Printf("[chrome] conectando a Chrome en localhost:%d...\n", port) } // 4. Conectar CDP (con mirrored networking, localhost es compartido WSL<->Windows) fmt.Printf("[cdp] conectando a localhost:%d...\n", port) conn, err := infra.CdpConnect(port) if err != nil { // Si lanzamos Chrome, matar el proceso antes de salir if pid > 0 { _ = infra.CdpClose(nil, pid) } return fmt.Errorf("conectar CDP en localhost:%d: %w", port, err) } fmt.Printf("[cdp] conexion establecida\n") // Asegurar cierre al salir (respetar --keep-open) defer func() { if keepOpen { fmt.Printf("[cdp] cerrando conexion CDP (Chrome sigue abierto, pid=%d, puerto=%d)\n", pid, port) // Solo cerrar la conexion WebSocket, no matar Chrome if err := infra.CdpClose(conn, 0); err != nil { fmt.Fprintf(os.Stderr, "[cdp] aviso al cerrar conexion: %v\n", err) } } else { fmt.Printf("[cdp] cerrando conexion y limpiando recursos...\n") if err := infra.CdpClose(conn, pid); err != nil { fmt.Fprintf(os.Stderr, "[cdp] aviso al cerrar: %v\n", err) } } }() // 5. Registrar entities y relations en operations.db var relationID string if db != nil { _, _, _, err := EnsureEntities(db, port, chromePath, userDataDir, script.Name, scriptPath) if err != nil { fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudieron crear entities: %v\n", err) } else { fmt.Printf("[ops] entities registradas\n") } relationID, err = EnsureRelations(db, "chrome_instance", "cdp_session", fmt.Sprintf("script_%s", script.Name)) if err != nil { fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudieron crear relations: %v\n", err) } else { fmt.Printf("[ops] relations registradas\n") } } // 6. Ejecutar el script runner := NewRunner(conn, RunnerOpts{AbortOnError: abortOnError}) startedAt := time.Now() fmt.Printf("[run] iniciando ejecucion: %s\n", startedAt.Format(time.RFC3339)) results, runErr := runner.Run(script) endedAt := time.Now() // 7. Imprimir resumen de pasos printSummary(script, results, runErr, startedAt, endedAt) // 8. Registrar execution y actualizar relation en operations.db if db != nil { execID, err := RecordRun(db, script, relationID, results, runErr, startedAt, endedAt) if err != nil { fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudo registrar ejecucion: %v\n", err) } else { fmt.Printf("[ops] ejecucion registrada en operations.db (id=%s)\n", execID[:8]) } // Registrar cada paso como log for _, r := range results { if logErr := LogStep(db, execID, r); logErr != nil { fmt.Fprintf(os.Stderr, "[ops] aviso: no se pudo registrar log step[%d]: %v\n", r.Index, logErr) } } // Actualizar relation status if relationID != "" { UpdateRelationAfterRun(db, relationID, runErr) } } if runErr != nil { return runErr } return nil } // printSummary imprime un resumen legible de la ejecucion. func printSummary(script *Script, results []StepResult, runErr error, startedAt, endedAt time.Time) { duration := endedAt.Sub(startedAt) success := 0 for _, r := range results { if r.Err == nil { success++ } } fmt.Printf("\n--- Resumen: %q ---\n", script.Name) fmt.Printf("Duracion: %v\n", duration.Round(time.Millisecond)) fmt.Printf("Pasos: %d/%d exitosos\n", success, len(results)) fmt.Println() for _, r := range results { status := "ok" detail := "" if r.Err != nil { status = "ERROR" detail = fmt.Sprintf(" -> %v", r.Err) } else if r.Output != "" { detail = fmt.Sprintf(" -> %q", r.Output) } fmt.Printf(" [%d] %-12s %s (%dms)%s\n", r.Index, r.Action, status, r.Elapsed.Milliseconds(), detail) } if runErr != nil { fmt.Printf("\nAbortado: %v\n", runErr) } else { fmt.Printf("\nScript completado.\n") } }