320 lines
8.2 KiB
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
|
|
}
|