package app import ( "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "github.com/wild-cloud/wild-cli/internal/environment" "github.com/wild-cloud/wild-cli/internal/output" ) // AppManifest represents the structure of manifest.yaml files type AppManifest struct { Name string `yaml:"name"` Version string `yaml:"version"` Description string `yaml:"description"` Install bool `yaml:"install"` Icon string `yaml:"icon"` Requires []struct { Name string `yaml:"name"` } `yaml:"requires"` } // AppInfo represents an installable app with its status type AppInfo struct { Name string Version string Description string Icon string Requires []string Installed bool InstalledVersion string } var ( searchQuery string verbose bool outputFormat string ) func newListCommand() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List available applications", Long: `List all available Wild Cloud apps with their metadata. This command shows applications from the Wild Cloud installation directory. Apps are read from WC_ROOT/apps and filtered to show only installable ones. Examples: wild app list wild app list --search database wild app list --verbose`, RunE: runList, } cmd.Flags().StringVar(&searchQuery, "search", "", "search applications by name or description") cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "show additional metadata (icon, requires)") cmd.Flags().StringVar(&outputFormat, "format", "table", "output format: table, json, yaml") return cmd } func runList(cmd *cobra.Command, args []string) error { // Initialize environment env := environment.New() if err := env.RequiresInstallation(); err != nil { return err } // Get apps directory from WC_ROOT appsDir := filepath.Join(env.WCRoot(), "apps") if _, err := os.Stat(appsDir); os.IsNotExist(err) { return fmt.Errorf("apps directory not found at %s", appsDir) } // Get project apps directory if available var projectAppsDir string if env.WCHome() != "" { projectAppsDir = filepath.Join(env.WCHome(), "apps") } // Read all installable apps apps, err := getInstallableApps(appsDir, projectAppsDir) if err != nil { return fmt.Errorf("failed to read apps: %w", err) } // Filter by search query if searchQuery != "" { apps = filterApps(apps, searchQuery) } if len(apps) == 0 { output.Warning("No applications found matching criteria") return nil } // Display results based on format switch outputFormat { case "json": return outputJSON(apps) case "yaml": return outputYAML(apps) default: return outputTable(apps, verbose) } } // getInstallableApps reads apps from WC_ROOT/apps directory and checks installation status func getInstallableApps(appsDir, projectAppsDir string) ([]AppInfo, error) { var apps []AppInfo entries, err := os.ReadDir(appsDir) if err != nil { return nil, fmt.Errorf("reading apps directory: %w", err) } for _, entry := range entries { if !entry.IsDir() { continue } appName := entry.Name() appDir := filepath.Join(appsDir, appName) manifestPath := filepath.Join(appDir, "manifest.yaml") // Skip if no manifest.yaml if _, err := os.Stat(manifestPath); os.IsNotExist(err) { continue } // Parse manifest manifestData, err := os.ReadFile(manifestPath) if err != nil { continue } var manifest AppManifest if err := yaml.Unmarshal(manifestData, &manifest); err != nil { continue } // Skip if not installable if !manifest.Install { continue } // Extract requires list var requires []string for _, req := range manifest.Requires { requires = append(requires, req.Name) } // Check installation status installed := false installedVersion := "" if projectAppsDir != "" { projectManifestPath := filepath.Join(projectAppsDir, appName, "manifest.yaml") if projectManifestData, err := os.ReadFile(projectManifestPath); err == nil { var projectManifest AppManifest if err := yaml.Unmarshal(projectManifestData, &projectManifest); err == nil { installed = true installedVersion = projectManifest.Version } } } app := AppInfo{ Name: manifest.Name, Version: manifest.Version, Description: manifest.Description, Icon: manifest.Icon, Requires: requires, Installed: installed, InstalledVersion: installedVersion, } apps = append(apps, app) } return apps, nil } // filterApps filters apps by search query (name or description) func filterApps(apps []AppInfo, query string) []AppInfo { query = strings.ToLower(query) var filtered []AppInfo for _, app := range apps { if strings.Contains(strings.ToLower(app.Name), query) || strings.Contains(strings.ToLower(app.Description), query) { filtered = append(filtered, app) } } return filtered } // outputTable displays apps in table format func outputTable(apps []AppInfo, verbose bool) error { if verbose { output.Header("Available Wild Cloud Apps (verbose)") output.Printf("%-15s %-10s %-12s %-40s %-15s %s\n", "NAME", "VERSION", "INSTALLED", "DESCRIPTION", "REQUIRES", "ICON") output.Printf("%-15s %-10s %-12s %-40s %-15s %s\n", "----", "-------", "---------", "-----------", "--------", "----") } else { output.Header("Available Wild Cloud Apps") output.Printf("%-15s %-10s %-12s %s\n", "NAME", "VERSION", "INSTALLED", "DESCRIPTION") output.Printf("%-15s %-10s %-12s %s\n", "----", "-------", "---------", "-----------") } for _, app := range apps { installedStatus := "NO" if app.Installed { installedStatus = app.InstalledVersion } description := app.Description if len(description) > 40 && !verbose { description = description[:37] + "..." } if verbose { requiresList := strings.Join(app.Requires, ",") if len(requiresList) > 15 { requiresList = requiresList[:12] + "..." } icon := app.Icon if len(icon) > 30 { icon = icon[:27] + "..." } output.Printf("%-15s %-10s %-12s %-40s %-15s %s\n", app.Name, app.Version, installedStatus, description, requiresList, icon) } else { output.Printf("%-15s %-10s %-12s %s\n", app.Name, app.Version, installedStatus, description) } } output.Info("") output.Info(fmt.Sprintf("Total installable apps: %d", len(apps))) output.Info("") output.Info("Usage:") output.Info(" wild app fetch # Fetch app template to project") output.Info(" wild app deploy # Deploy app to Kubernetes") return nil } // outputJSON displays apps in JSON format func outputJSON(apps []AppInfo) error { output.Printf("{\n") output.Printf(" \"apps\": [\n") for i, app := range apps { output.Printf(" {\n") output.Printf(" \"name\": \"%s\",\n", app.Name) output.Printf(" \"version\": \"%s\",\n", app.Version) output.Printf(" \"description\": \"%s\",\n", app.Description) output.Printf(" \"icon\": \"%s\",\n", app.Icon) output.Printf(" \"requires\": [") for j, req := range app.Requires { output.Printf("\"%s\"", req) if j < len(app.Requires)-1 { output.Printf(", ") } } output.Printf("],\n") if app.Installed { output.Printf(" \"installed\": \"%s\",\n", app.InstalledVersion) } else { output.Printf(" \"installed\": \"NO\",\n") } output.Printf(" \"installed_version\": \"%s\"\n", app.InstalledVersion) output.Printf(" }") if i < len(apps)-1 { output.Printf(",") } output.Printf("\n") } output.Printf(" ],\n") output.Printf(" \"total\": %d\n", len(apps)) output.Printf("}\n") return nil } // outputYAML displays apps in YAML format func outputYAML(apps []AppInfo) error { output.Printf("apps:\n") for _, app := range apps { output.Printf("- name: %s\n", app.Name) output.Printf(" version: %s\n", app.Version) output.Printf(" description: %s\n", app.Description) if app.Installed { output.Printf(" installed: %s\n", app.InstalledVersion) } else { output.Printf(" installed: NO\n") } if app.InstalledVersion != "" { output.Printf(" installed_version: %s\n", app.InstalledVersion) } if app.Icon != "" { output.Printf(" icon: %s\n", app.Icon) } if len(app.Requires) > 0 { output.Printf(" requires:\n") for _, req := range app.Requires { output.Printf(" - %s\n", req) } } } return nil }