Files
wild-cloud/wild-cli/cmd/wild/app/list.go

320 lines
8.2 KiB
Go

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 <app> # Fetch app template to project")
output.Info(" wild app deploy <app> # 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
}