diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 000000000..fd23c2745 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"65da1927-3851-45d0-bb3b-43c54d4332ca","pid":1201948,"procStart":"94713473","acquiredAt":1779638740909} diff --git a/cmd/features/info.go b/cmd/features/info.go index fe37e2f0e..e1555d03e 100644 --- a/cmd/features/info.go +++ b/cmd/features/info.go @@ -3,7 +3,6 @@ package features import ( "fmt" "os" - "strings" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" @@ -149,16 +148,18 @@ func listTags(ref name.Reference) ([]string, error) { func (cmd *InfoCmd) printText(info *featureInfo) error { w := os.Stdout - _, _ = fmt.Fprintf(w, "Feature: %s\n", info.Name) - printField(w, "ID", info.ID) - printField(w, "Version", info.Version) - printField(w, "Description", info.Description) - printField(w, "Authors", info.Authors) - printField(w, "Source", info.Source) - printField(w, "Documentation", info.DocumentationURL) + + rows := [][]string{{headerFeature, info.Name}} + rows = appendField(rows, "ID", info.ID) + rows = appendField(rows, "Version", info.Version) + rows = appendField(rows, "Description", info.Description) + rows = appendField(rows, "Authors", info.Authors) + rows = appendField(rows, "Source", info.Source) + rows = appendField(rows, "Documentation", info.DocumentationURL) if info.Deprecated { - _, _ = fmt.Fprintln(w, "Status: DEPRECATED") + rows = append(rows, []string{"Status", "DEPRECATED"}) } + table.Print([]string{"Property", headerValue}, rows) cmd.printDependencies(w, info) cmd.printTags(w, info) @@ -168,10 +169,11 @@ func (cmd *InfoCmd) printText(info *featureInfo) error { return nil } -func printField(w *os.File, label, value string) { +func appendField(rows [][]string, label, value string) [][]string { if value != "" { - _, _ = fmt.Fprintf(w, "%s: %s\n", label, value) + return append(rows, []string{label, value}) } + return rows } func (cmd *InfoCmd) printDependencies(w *os.File, info *featureInfo) { @@ -192,7 +194,11 @@ func (cmd *InfoCmd) printTags(w *os.File, info *featureInfo) { return } _, _ = fmt.Fprintln(w, "\nAvailable Tags:") - _, _ = fmt.Fprintf(w, " %s\n", strings.Join(info.Tags, ", ")) + rows := make([][]string, 0, len(info.Tags)) + for _, tag := range info.Tags { + rows = append(rows, []string{tag}) + } + table.Print([]string{"Tag"}, rows) } func (cmd *InfoCmd) printOptions(w *os.File, info *featureInfo) { @@ -213,7 +219,7 @@ func (cmd *InfoCmd) printAnnotations(w *os.File, info *featureInfo) { return } _, _ = fmt.Fprintln(w, "\nOCI Annotations:") - headers := []string{"Key", "Value"} + headers := []string{"Key", headerValue} var rows [][]string for k, v := range info.Annotations { rows = append(rows, []string{k, v}) diff --git a/cmd/features/info_manifest.go b/cmd/features/info_manifest.go index 195c8066c..1a59c12df 100644 --- a/cmd/features/info_manifest.go +++ b/cmd/features/info_manifest.go @@ -3,9 +3,11 @@ package features import ( "fmt" "os" + "strconv" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/feature" + "github.com/devsy-org/devsy/pkg/table" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/spf13/cobra" @@ -62,28 +64,36 @@ func (cmd *InfoManifestCmd) Run(featureID string) error { func (cmd *InfoManifestCmd) printText(manifest *v1.Manifest) error { w := os.Stdout - _, _ = fmt.Fprintf(w, "Schema Version: %d\n", manifest.SchemaVersion) - _, _ = fmt.Fprintf(w, "Media Type: %s\n", manifest.MediaType) - _, _ = fmt.Fprintf(w, "\nConfig:\n") - _, _ = fmt.Fprintf(w, " Media Type: %s\n", manifest.Config.MediaType) - _, _ = fmt.Fprintf(w, " Digest: %s\n", manifest.Config.Digest) - _, _ = fmt.Fprintf(w, " Size: %d\n", manifest.Config.Size) + table.Print([]string{"Property", headerValue}, [][]string{ + {"Schema Version", strconv.FormatInt(manifest.SchemaVersion, 10)}, + {"Media Type", string(manifest.MediaType)}, + {"Config Media Type", string(manifest.Config.MediaType)}, + {"Config Digest", manifest.Config.Digest.String()}, + {"Config Size", strconv.FormatInt(manifest.Config.Size, 10)}, + }) if len(manifest.Layers) > 0 { - _, _ = fmt.Fprintf(w, "\nLayers:\n") + _, _ = fmt.Fprintln(w, "\nLayers:") + layerRows := make([][]string, 0, len(manifest.Layers)) for i, layer := range manifest.Layers { - _, _ = fmt.Fprintf(w, " [%d] Media Type: %s\n", i, layer.MediaType) - _, _ = fmt.Fprintf(w, " Digest: %s\n", layer.Digest) - _, _ = fmt.Fprintf(w, " Size: %d\n", layer.Size) + layerRows = append(layerRows, []string{ + strconv.Itoa(i), + string(layer.MediaType), + layer.Digest.String(), + strconv.FormatInt(layer.Size, 10), + }) } + table.Print([]string{"#", "Media Type", "Digest", "Size"}, layerRows) } if len(manifest.Annotations) > 0 { - _, _ = fmt.Fprintf(w, "\nAnnotations:\n") + _, _ = fmt.Fprintln(w, "\nAnnotations:") + annoRows := make([][]string, 0, len(manifest.Annotations)) for k, v := range manifest.Annotations { - _, _ = fmt.Fprintf(w, " %s: %s\n", k, v) + annoRows = append(annoRows, []string{k, v}) } + table.Print([]string{"Key", headerValue}, annoRows) } return nil diff --git a/cmd/features/info_tags.go b/cmd/features/info_tags.go index d79bd7788..f89ae5392 100644 --- a/cmd/features/info_tags.go +++ b/cmd/features/info_tags.go @@ -3,9 +3,9 @@ package features import ( "fmt" "os" - "strings" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/table" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" ) @@ -61,13 +61,16 @@ func (cmd *InfoTagsCmd) Run(featureID string) error { return writeJSON(os.Stdout, &tagsOutput{Tags: tags}) } - w := os.Stdout if len(tags) == 0 { - _, _ = fmt.Fprintln(w, "No tags found.") + _, _ = fmt.Fprintln(os.Stdout, "No tags found.") return nil } - _, _ = fmt.Fprintln(w, "Available Tags:") - _, _ = fmt.Fprintf(w, " %s\n", strings.Join(tags, ", ")) + _, _ = fmt.Fprintln(os.Stdout, "Available Tags:") + rows := make([][]string, 0, len(tags)) + for _, tag := range tags { + rows = append(rows, []string{tag}) + } + table.Print([]string{"Tag"}, rows) return nil } diff --git a/cmd/features/output.go b/cmd/features/output.go index 0d205afdb..19d5dce20 100644 --- a/cmd/features/output.go +++ b/cmd/features/output.go @@ -10,6 +10,9 @@ const ( outputJSON = "json" outputText = "text" outputYAML = "yaml" + + headerFeature = "Feature" + headerValue = "Value" ) func validateOutputFormat(format string) error { diff --git a/cmd/features/package.go b/cmd/features/package.go index 8dbaa651e..ae9a7b1fe 100644 --- a/cmd/features/package.go +++ b/cmd/features/package.go @@ -10,6 +10,7 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/extract" "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -238,13 +239,15 @@ func (cmd *PackageCmd) packageFeature( func (cmd *PackageCmd) printResults(results []packageResult) error { w := os.Stdout _, _ = fmt.Fprintln(w, "Packaged dev container features:") + rows := make([][]string, 0, len(results)) for _, r := range results { version := r.Version if version == "" { version = "(no version)" } - _, _ = fmt.Fprintf(w, " %s (%s) -> %s\n", r.FeatureID, version, r.OutputPath) + rows = append(rows, []string{r.FeatureID, version, r.OutputPath}) } + table.Print([]string{"Feature ID", "Version", "Output Path"}, rows) _, _ = fmt.Fprintf(w, "\nTotal: %d feature(s) packaged\n", len(results)) return nil } diff --git a/cmd/features/resolvedeps.go b/cmd/features/resolvedeps.go index ff3336599..622de36d8 100644 --- a/cmd/features/resolvedeps.go +++ b/cmd/features/resolvedeps.go @@ -137,7 +137,7 @@ func (cmd *ResolveDepsCmd) loadConfig() (*config.DevContainerConfig, error) { func (cmd *ResolveDepsCmd) printText(resolved []resolvedFeature) error { _, _ = fmt.Fprintf(os.Stdout, "Feature install order (%d features):\n\n", len(resolved)) - headers := []string{"#", "Feature", "Depends On", "Installs After"} + headers := []string{"#", headerFeature, "Depends On", "Installs After"} var rows [][]string for i, rf := range resolved { version := rf.ID diff --git a/cmd/features/test.go b/cmd/features/test.go index 508af84b4..7c975c8b0 100644 --- a/cmd/features/test.go +++ b/cmd/features/test.go @@ -10,6 +10,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -349,24 +350,24 @@ func (cmd *TestCmd) dockerRemove(containerName string) { func (cmd *TestCmd) printResults(results []testResult) { w := os.Stdout - _, _ = fmt.Fprintln(w, "\n=== Feature Test Results ===") + _, _ = fmt.Fprintln(w, "\nFeature Test Results") + passed := 0 failed := 0 - + rows := make([][]string, 0, len(results)) for _, r := range results { - label := r.FeatureID - if r.Scenario != "" { - label = fmt.Sprintf("%s/%s", r.FeatureID, r.Scenario) - } - - if r.Passed { - _, _ = fmt.Fprintf(w, " PASS: %s\n", label) - passed++ - } else { - _, _ = fmt.Fprintf(w, " FAIL: %s — %s\n", label, r.Error) + status := "PASS" + if !r.Passed { + status = "FAIL" failed++ + } else { + passed++ } + + rows = append(rows, []string{status, r.FeatureID, r.Scenario, r.Error}) } + table.Print([]string{"Status", headerFeature, "Scenario", "Error"}, rows) + _, _ = fmt.Fprintf(w, "\nTotal: %d passed, %d failed\n", passed, failed) } diff --git a/cmd/pro/daemon/netcheck.go b/cmd/pro/daemon/netcheck.go index b2fa1f267..66c677d30 100644 --- a/cmd/pro/daemon/netcheck.go +++ b/cmd/pro/daemon/netcheck.go @@ -11,6 +11,7 @@ import ( "github.com/devsy-org/devsy/pkg/config" daemon "github.com/devsy-org/devsy/pkg/daemon/platform" providerpkg "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" "tailscale.com/client/local" ) @@ -91,29 +92,28 @@ func (cmd *NetcheckCmd) Run( return err } + rows := [][]string{} for _, region := range dm.Regions { report, err := tsClient.DebugDERPRegion(ctx, strconv.Itoa(region.RegionID)) if err != nil { return err } - msg := fmt.Sprintf("DERP %d (%s)\n", region.RegionID, region.RegionCode) - if len(report.Errors) > 0 { - for _, error := range report.Errors { - msg += fmt.Sprintf(" Error: %s\n", error) - } + regionLabel := fmt.Sprintf("DERP %d (%s)", region.RegionID, region.RegionCode) + for _, e := range report.Errors { + rows = append(rows, []string{regionLabel, "Error", e}) } - if len(report.Warnings) > 0 { - for _, warning := range report.Warnings { - msg += fmt.Sprintf(" Warning: %s\n", warning) - } + for _, w := range report.Warnings { + rows = append(rows, []string{regionLabel, "Warning", w}) } - if len(report.Info) > 0 { - for _, info := range report.Info { - msg += fmt.Sprintf(" Info: %s\n", info) - } + for _, i := range report.Info { + rows = append(rows, []string{regionLabel, "Info", i}) + } + if len(report.Errors) == 0 && len(report.Warnings) == 0 && len(report.Info) == 0 { + rows = append(rows, []string{regionLabel, "", ""}) } - fmt.Println(msg) } + table.Print([]string{"Region", "Level", "Message"}, rows) + return nil } diff --git a/cmd/pro/list_clusters.go b/cmd/pro/list_clusters.go index 2c2aea43f..a1c4cebe4 100644 --- a/cmd/pro/list_clusters.go +++ b/cmd/pro/list_clusters.go @@ -3,13 +3,16 @@ package pro import ( "bytes" "context" + "encoding/json" "fmt" + managementv1 "github.com/devsy-org/api/pkg/apis/management/v1" "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -77,7 +80,26 @@ func (cmd *ListClustersCmd) Run( return fmt.Errorf("list clusters with provider \"%s\": %w", provider.Name, err) } - fmt.Println(buf.String()) + headers := []string{headerName, headerDisplayName, "Online"} + if buf.Len() == 0 { + table.Print(headers, nil) + return nil + } + + clusters := managementv1.ProjectClusters{} + if err := json.Unmarshal(buf.Bytes(), &clusters); err != nil { + return fmt.Errorf("parse clusters output: %w", err) + } + + rows := make([][]string, 0, len(clusters.Clusters)) + for _, c := range clusters.Clusters { + rows = append(rows, []string{ + c.GetName(), + c.Spec.DisplayName, + fmt.Sprintf("%t", c.Status.Online), + }) + } + table.Print(headers, rows) return nil } diff --git a/cmd/pro/list_projects.go b/cmd/pro/list_projects.go index aeb4c723b..3effaff4c 100644 --- a/cmd/pro/list_projects.go +++ b/cmd/pro/list_projects.go @@ -3,12 +3,15 @@ package pro import ( "bytes" "context" + "encoding/json" "fmt" + managementv1 "github.com/devsy-org/api/pkg/apis/management/v1" "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -70,7 +73,26 @@ func (cmd *ListProjectsCmd) Run( return fmt.Errorf("watch workspaces with provider \"%s\": %w", provider.Name, err) } - fmt.Println(buf.String()) + headers := []string{headerName, headerDisplayName, "Description"} + if buf.Len() == 0 { + table.Print(headers, nil) + return nil + } + + projects := []managementv1.Project{} + if err := json.Unmarshal(buf.Bytes(), &projects); err != nil { + return fmt.Errorf("parse projects output: %w", err) + } + + rows := make([][]string, 0, len(projects)) + for _, p := range projects { + rows = append(rows, []string{ + p.GetName(), + p.Spec.DisplayName, + p.Spec.Description, + }) + } + table.Print(headers, rows) return nil } diff --git a/cmd/pro/list_templates.go b/cmd/pro/list_templates.go index 35b13c848..e2360ccd4 100644 --- a/cmd/pro/list_templates.go +++ b/cmd/pro/list_templates.go @@ -3,13 +3,16 @@ package pro import ( "bytes" "context" + "encoding/json" "fmt" + managementv1 "github.com/devsy-org/api/pkg/apis/management/v1" "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -77,7 +80,26 @@ func (cmd *ListTemplatesCmd) Run( return fmt.Errorf("list templates with provider \"%s\": %w", provider.Name, err) } - fmt.Println(buf.String()) + headers := []string{headerName, headerDisplayName, "Description"} + if buf.Len() == 0 { + table.Print(headers, nil) + return nil + } + + templates := managementv1.ProjectTemplates{} + if err := json.Unmarshal(buf.Bytes(), &templates); err != nil { + return fmt.Errorf("parse templates output: %w", err) + } + + rows := make([][]string, 0, len(templates.DevsyWorkspaceTemplates)) + for _, t := range templates.DevsyWorkspaceTemplates { + rows = append(rows, []string{ + t.GetName(), + t.Spec.DisplayName, + t.Spec.Description, + }) + } + table.Print(headers, rows) return nil } diff --git a/cmd/pro/list_workspaces.go b/cmd/pro/list_workspaces.go index e19443b73..b279c6a2a 100644 --- a/cmd/pro/list_workspaces.go +++ b/cmd/pro/list_workspaces.go @@ -3,12 +3,16 @@ package pro import ( "bytes" "context" + "encoding/json" "fmt" + "time" + managementv1 "github.com/devsy-org/api/pkg/apis/management/v1" "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -70,7 +74,35 @@ func (cmd *ListWorkspacesCmd) Run( return fmt.Errorf("list workspaces: %w", err) } - fmt.Println(buf.String()) + headers := []string{headerName, headerDisplayName, "Project", "Age"} + if buf.Len() == 0 { + table.Print(headers, nil) + return nil + } + + instances := []managementv1.DevsyWorkspaceInstance{} + if err := json.Unmarshal(buf.Bytes(), &instances); err != nil { + return fmt.Errorf("parse workspaces output: %w", err) + } + + rows := make([][]string, 0, len(instances)) + for _, inst := range instances { + project := "" + if inst.GetLabels() != nil { + project = inst.GetLabels()["devsy.sh/project"] + } + age := "" + if !inst.CreationTimestamp.IsZero() { + age = time.Since(inst.CreationTimestamp.Time).Round(time.Second).String() + } + rows = append(rows, []string{ + inst.GetName(), + inst.Spec.DisplayName, + project, + age, + }) + } + table.Print(headers, rows) return nil } diff --git a/cmd/pro/pro.go b/cmd/pro/pro.go index 964be85c5..b2d88e935 100644 --- a/cmd/pro/pro.go +++ b/cmd/pro/pro.go @@ -16,6 +16,11 @@ import ( "github.com/spf13/cobra" ) +const ( + headerName = "Name" + headerDisplayName = "Display Name" +) + // NewProCmd returns a new command. func NewProCmd(flags *flags.GlobalFlags) *cobra.Command { globalFlags := &proflags.GlobalFlags{GlobalFlags: flags} diff --git a/cmd/provider/list-default-providers.go b/cmd/provider/list_available.go similarity index 93% rename from cmd/provider/list-default-providers.go rename to cmd/provider/list_available.go index f3653d1dc..c6e1ace18 100644 --- a/cmd/provider/list-default-providers.go +++ b/cmd/provider/list_available.go @@ -12,6 +12,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" devsyhttp "github.com/devsy-org/devsy/pkg/http" + "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -45,15 +46,17 @@ func (cmd *ListAvailableCmd) Run(ctx context.Context) error { } _, _ = fmt.Fprintln(os.Stdout, "List of available providers from "+config.RepoOwner+":") + var rows [][]string for _, v := range jsonResult { name, ok := v["name"].(string) if !ok || name == "" { continue } if after, ok0 := strings.CutPrefix(name, config.ProviderPrefix); ok0 { - _, _ = fmt.Fprintln(os.Stdout, "\t", after) + rows = append(rows, []string{after}) } } + table.Print([]string{"Provider"}, rows) return nil } diff --git a/e2e/tests/features/features.go b/e2e/tests/features/features.go index f9ffdcb0b..5ca3665ad 100644 --- a/e2e/tests/features/features.go +++ b/e2e/tests/features/features.go @@ -349,9 +349,9 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { }) framework.ExpectNoError(err) - gomega.Expect(stdout).To(gomega.ContainSubstring("Schema Version:")) - gomega.Expect(stdout).To(gomega.ContainSubstring("Media Type:")) - gomega.Expect(stdout).To(gomega.ContainSubstring("Config:")) + gomega.Expect(stdout).To(gomega.ContainSubstring("Schema Version")) + gomega.Expect(stdout).To(gomega.ContainSubstring("Media Type")) + gomega.Expect(stdout).To(gomega.ContainSubstring("Config Media Type")) gomega.Expect(stdout).To(gomega.ContainSubstring("Layers:")) }, ginkgo.SpecTimeout(framework.TimeoutShort()))