package main import ( "fmt" "os" "os/exec" "path/filepath" "strings" "text/tabwriter" ) func cmdApp(args []string) { if len(args) < 1 { printAppUsage() os.Exit(1) } switch args[0] { case "list": appList() case "clone": if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: fn app clone ") os.Exit(1) } appClone(args[1]) case "pull": if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: fn app pull ") os.Exit(1) } appPull(args[1]) case "help", "-h": printAppUsage() default: fmt.Fprintf(os.Stderr, "unknown app command: %s\n", args[0]) printAppUsage() os.Exit(1) } } func printAppUsage() { fmt.Println(`fn app — manage registry apps Usage: fn app list List all apps (local + remote) fn app clone Clone app repo to apps// fn app pull Git pull in existing clone`) } func appList() { db := openDB() defer db.Close() apps, err := db.ListAllApps() if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if len(apps) == 0 { fmt.Println("No apps registered.") return } r := root() w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tLANG\tDOMAIN\tSTATUS\tREPO_URL") for _, a := range apps { status := "local" if a.RepoURL != "" { dirPath := filepath.Join(r, "apps", a.Name) if _, err := os.Stat(dirPath); os.IsNotExist(err) { status = "remote" } else { status = "cloned" } } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain, status, a.RepoURL) } w.Flush() } func appClone(id string) { db := openDB() defer db.Close() a, err := db.GetApp(id) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if a.RepoURL == "" { fmt.Fprintf(os.Stderr, "app %s has no repo_url set\n", id) os.Exit(1) } r := root() target := filepath.Join(r, "apps", a.Name) if _, err := os.Stat(target); err == nil { fmt.Fprintf(os.Stderr, "directory already exists: %s\n", target) os.Exit(1) } fmt.Printf("Cloning %s → %s\n", a.RepoURL, target) cmd := exec.Command("git", "clone", a.RepoURL, target) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "git clone failed: %v\n", err) os.Exit(1) } fmt.Println("Done. Run 'fn index' to re-index.") } func appPull(id string) { db := openDB() defer db.Close() a, err := db.GetApp(id) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } r := root() dir := filepath.Join(r, "apps", a.Name) if _, err := os.Stat(dir); os.IsNotExist(err) { fmt.Fprintf(os.Stderr, "app not cloned locally: %s\n", dir) fmt.Fprintln(os.Stderr, "Run 'fn app clone "+id+"' first.") os.Exit(1) } fmt.Printf("Pulling %s\n", dir) cmd := exec.Command("git", "-C", dir, "pull") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "git pull failed: %v\n", err) os.Exit(1) } fmt.Println("Done. Run 'fn index' to re-index.") } // formatRepoURL ensures the URL does not contain inline credentials for display. func formatRepoURL(url string) string { if strings.Contains(url, "@") && strings.Contains(url, "://") { // Mask credentials in URLs like https://user:pass@host/... parts := strings.SplitN(url, "://", 2) if len(parts) == 2 { atIdx := strings.LastIndex(parts[1], "@") if atIdx >= 0 { return parts[0] + "://" + parts[1][atIdx+1:] } } } return url }