aenvs/internal/cli/list.go

174 lines
3.7 KiB
Go

package cli
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"time"
"github.com/spf13/cobra"
"github.com/user/aevs/internal/config"
"github.com/user/aevs/internal/storage"
"github.com/user/aevs/internal/types"
)
var (
listJSON bool
)
var listCmd = &cobra.Command{
Use: "list",
Short: "List projects in storage",
Long: `Show all projects stored in cloud storage.`,
RunE: runList,
}
func init() {
listCmd.Flags().BoolVar(&listJSON, "json", false, "output in JSON format")
}
func runList(cmd *cobra.Command, args []string) error {
// Load global config
globalCfg, err := config.LoadGlobalConfig()
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("no storage configured; run 'aevs config' first")
}
return fmt.Errorf("failed to load global config: %w", err)
}
// Create storage client
s3Storage, err := storage.NewS3Storage(&globalCfg.Storage)
if err != nil {
return fmt.Errorf("failed to create storage client: %w", err)
}
ctx := context.Background()
// List projects
projects, err := s3Storage.ListProjects(ctx)
if err != nil {
return fmt.Errorf("failed to list projects: %w", err)
}
if len(projects) == 0 {
fmt.Println("No projects found in storage.")
return nil
}
// Load metadata for each project
var projectInfos []types.ProjectInfo
for _, project := range projects {
metadataKey := fmt.Sprintf("%s/%s", project, config.MetadataFileName)
metadataReader, err := s3Storage.Download(ctx, metadataKey)
if err != nil {
// Skip projects without metadata
continue
}
metadataBytes, err := io.ReadAll(metadataReader)
metadataReader.Close()
if err != nil {
continue
}
var metadata types.Metadata
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
continue
}
projectInfos = append(projectInfos, types.ProjectInfo{
Name: project,
FileCount: len(metadata.Files),
UpdatedAt: metadata.UpdatedAt,
SizeBytes: metadata.SizeBytes,
})
}
if listJSON {
// JSON output
output, err := json.MarshalIndent(projectInfos, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
fmt.Println(string(output))
return nil
}
// Table output
fmt.Println()
fmt.Printf("Projects in s3://%s:\n", globalCfg.Storage.Bucket)
fmt.Println()
fmt.Printf(" %-25s %-8s %-20s %s\n", "PROJECT", "FILES", "UPDATED", "SIZE")
for _, info := range projectInfos {
updatedStr := formatTime(info.UpdatedAt)
sizeStr := formatSize(info.SizeBytes)
fmt.Printf(" %-25s %-8d %-20s %s\n", info.Name, info.FileCount, updatedStr, sizeStr)
}
fmt.Println()
fmt.Printf("Total: %d project(s)\n", len(projectInfos))
return nil
}
// formatTime formats time in a human-readable way
func formatTime(t time.Time) string {
now := time.Now()
diff := now.Sub(t)
if diff < time.Minute {
return "just now"
}
if diff < time.Hour {
mins := int(diff.Minutes())
if mins == 1 {
return "1 minute ago"
}
return fmt.Sprintf("%d minutes ago", mins)
}
if diff < 24*time.Hour {
hours := int(diff.Hours())
if hours == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", hours)
}
if diff < 7*24*time.Hour {
days := int(diff.Hours() / 24)
if days == 1 {
return "1 day ago"
}
return fmt.Sprintf("%d days ago", days)
}
if diff < 30*24*time.Hour {
weeks := int(diff.Hours() / 24 / 7)
if weeks == 1 {
return "1 week ago"
}
return fmt.Sprintf("%d weeks ago", weeks)
}
return t.Format("2006-01-02")
}
// formatSize formats bytes in a human-readable way
func formatSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}