From 1dd401fa043d45ca54d447a11a55d10117bbed40 Mon Sep 17 00:00:00 2001 From: Virgil Date: Tue, 31 Mar 2026 19:45:42 +0000 Subject: [PATCH] feat(pkg): add json format for package list Co-Authored-By: Virgil --- cmd/core/pkgcmd/cmd_manage.go | 81 +++++++++++++++++--- cmd/core/pkgcmd/cmd_manage_test.go | 116 +++++++++++++++++++++++++++++ docs/cmd/pkg/index.md | 10 +++ 3 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 cmd/core/pkgcmd/cmd_manage_test.go diff --git a/cmd/core/pkgcmd/cmd_manage.go b/cmd/core/pkgcmd/cmd_manage.go index 2964d3f..7df7f95 100644 --- a/cmd/core/pkgcmd/cmd_manage.go +++ b/cmd/core/pkgcmd/cmd_manage.go @@ -1,6 +1,7 @@ package pkgcmd import ( + "encoding/json" "errors" "fmt" "os/exec" @@ -15,19 +16,40 @@ import ( // addPkgListCommand adds the 'pkg list' command. func addPkgListCommand(parent *cobra.Command) { + var format string listCmd := &cobra.Command{ Use: "list", Short: i18n.T("cmd.pkg.list.short"), Long: i18n.T("cmd.pkg.list.long"), RunE: func(cmd *cobra.Command, args []string) error { - return runPkgList() + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + return runPkgList(format) }, } + listCmd.Flags().StringVar(&format, "format", "table", "Output format: table or json") parent.AddCommand(listCmd) } -func runPkgList() error { +type pkgListEntry struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Installed bool `json:"installed"` + Path string `json:"path"` +} + +type pkgListReport struct { + Format string `json:"format"` + Total int `json:"total"` + Installed int `json:"installed"` + Missing int `json:"missing"` + Packages []pkgListEntry `json:"packages"` +} + +func runPkgList(format string) error { regPath, err := repos.FindRegistry(coreio.Local) if err != nil { return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml_workspace")) @@ -52,8 +74,7 @@ func runPkgList() error { return nil } - fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.pkg.list.title"))) - + var entries []pkgListEntry var installed, missing int for _, r := range allRepos { repoPath := filepath.Join(basePath, r.Name) @@ -64,20 +85,58 @@ func runPkgList() error { missing++ } - status := successStyle.Render("✓") - if !exists { - status = dimStyle.Render("○") - } - desc := r.Description if len(desc) > 40 { desc = desc[:37] + "..." } if desc == "" { - desc = dimStyle.Render(i18n.T("cmd.pkg.no_description")) + desc = i18n.T("cmd.pkg.no_description") } - fmt.Printf(" %s %s\n", status, repoNameStyle.Render(r.Name)) + entries = append(entries, pkgListEntry{ + Name: r.Name, + Description: desc, + Installed: exists, + Path: repoPath, + }) + } + + if format == "json" { + report := pkgListReport{ + Format: "json", + Total: len(entries), + Installed: installed, + Missing: missing, + Packages: entries, + } + + out, err := json.MarshalIndent(report, "", " ") + if err != nil { + return fmt.Errorf("failed to format package list: %w", err) + } + + fmt.Println(string(out)) + return nil + } + + if format != "table" { + return fmt.Errorf("unsupported format %q: expected table or json", format) + } + + fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.pkg.list.title"))) + + for _, entry := range entries { + status := successStyle.Render("✓") + if !entry.Installed { + status = dimStyle.Render("○") + } + + desc := entry.Description + if !entry.Installed { + desc = dimStyle.Render(desc) + } + + fmt.Printf(" %s %s\n", status, repoNameStyle.Render(entry.Name)) fmt.Printf(" %s\n", desc) } diff --git a/cmd/core/pkgcmd/cmd_manage_test.go b/cmd/core/pkgcmd/cmd_manage_test.go new file mode 100644 index 0000000..7908121 --- /dev/null +++ b/cmd/core/pkgcmd/cmd_manage_test.go @@ -0,0 +1,116 @@ +package pkgcmd + +import ( + "bytes" + "encoding/json" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func capturePkgOutput(t *testing.T, fn func()) string { + t.Helper() + + oldStdout := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + + defer func() { + os.Stdout = oldStdout + }() + + fn() + + require.NoError(t, w.Close()) + + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + require.NoError(t, err) + return buf.String() +} + +func withWorkingDir(t *testing.T, dir string) { + t.Helper() + + oldwd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(dir)) + + t.Cleanup(func() { + require.NoError(t, os.Chdir(oldwd)) + }) +} + +func writeTestRegistry(t *testing.T, dir string) { + t.Helper() + + registry := strings.TrimSpace(` +org: host-uk +base_path: . +repos: + core-alpha: + type: foundation + description: Alpha package + core-beta: + type: module + description: Beta package +`) + "\n" + + require.NoError(t, os.WriteFile(filepath.Join(dir, "repos.yaml"), []byte(registry), 0644)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "core-alpha", ".git"), 0755)) +} + +func TestRunPkgList_Good(t *testing.T) { + tmp := t.TempDir() + writeTestRegistry(t, tmp) + withWorkingDir(t, tmp) + + out := capturePkgOutput(t, func() { + err := runPkgList("table") + require.NoError(t, err) + }) + + assert.Contains(t, out, "core-alpha") + assert.Contains(t, out, "core-beta") + assert.Contains(t, out, "core setup") +} + +func TestRunPkgList_JSON(t *testing.T) { + tmp := t.TempDir() + writeTestRegistry(t, tmp) + withWorkingDir(t, tmp) + + out := capturePkgOutput(t, func() { + err := runPkgList("json") + require.NoError(t, err) + }) + + var report pkgListReport + require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(out)), &report)) + assert.Equal(t, "json", report.Format) + assert.Equal(t, 2, report.Total) + assert.Equal(t, 1, report.Installed) + assert.Equal(t, 1, report.Missing) + require.Len(t, report.Packages, 2) + assert.Equal(t, "core-alpha", report.Packages[0].Name) + assert.True(t, report.Packages[0].Installed) + assert.Equal(t, filepath.Join(tmp, "core-alpha"), report.Packages[0].Path) + assert.Equal(t, "core-beta", report.Packages[1].Name) + assert.False(t, report.Packages[1].Installed) +} + +func TestRunPkgList_UnsupportedFormat(t *testing.T) { + tmp := t.TempDir() + writeTestRegistry(t, tmp) + withWorkingDir(t, tmp) + + err := runPkgList("yaml") + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported format") +} diff --git a/docs/cmd/pkg/index.md b/docs/cmd/pkg/index.md index fcc218b..c07dc02 100644 --- a/docs/cmd/pkg/index.md +++ b/docs/cmd/pkg/index.md @@ -98,6 +98,16 @@ core pkg list Shows installed status (✓) and description for each package. +### Flags + +| Flag | Description | +|------|-------------| +| `--format` | Output format (`table` or `json`) | + +### JSON Output + +When `--format json` is set, `core pkg list` emits a structured report with package entries, installed state, and summary counts. + --- ## pkg update -- 2.45.3