feat(pkg): add JSON output for pkg outdated
All checks were successful
Security Scan / security (push) Successful in 15s
All checks were successful
Security Scan / security (push) Successful in 15s
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
10de071704
commit
9aff00de1e
5 changed files with 216 additions and 7 deletions
|
|
@ -245,19 +245,43 @@ func runPkgUpdate(packages []string, all bool) error {
|
||||||
|
|
||||||
// addPkgOutdatedCommand adds the 'pkg outdated' command.
|
// addPkgOutdatedCommand adds the 'pkg outdated' command.
|
||||||
func addPkgOutdatedCommand(parent *cobra.Command) {
|
func addPkgOutdatedCommand(parent *cobra.Command) {
|
||||||
|
var format string
|
||||||
outdatedCmd := &cobra.Command{
|
outdatedCmd := &cobra.Command{
|
||||||
Use: "outdated",
|
Use: "outdated",
|
||||||
Short: i18n.T("cmd.pkg.outdated.short"),
|
Short: i18n.T("cmd.pkg.outdated.short"),
|
||||||
Long: i18n.T("cmd.pkg.outdated.long"),
|
Long: i18n.T("cmd.pkg.outdated.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPkgOutdated()
|
format, err := cmd.Flags().GetString("format")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return runPkgOutdated(format)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outdatedCmd.Flags().StringVar(&format, "format", "table", i18n.T("cmd.pkg.outdated.flag.format"))
|
||||||
parent.AddCommand(outdatedCmd)
|
parent.AddCommand(outdatedCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPkgOutdated() error {
|
type pkgOutdatedEntry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Behind int `json:"behind"`
|
||||||
|
UpToDate bool `json:"upToDate"`
|
||||||
|
Installed bool `json:"installed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pkgOutdatedReport struct {
|
||||||
|
Format string `json:"format"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Installed int `json:"installed"`
|
||||||
|
Missing int `json:"missing"`
|
||||||
|
Outdated int `json:"outdated"`
|
||||||
|
UpToDate int `json:"upToDate"`
|
||||||
|
Packages []pkgOutdatedEntry `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPkgOutdated(format string) error {
|
||||||
regPath, err := repos.FindRegistry(coreio.Local)
|
regPath, err := repos.FindRegistry(coreio.Local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
|
return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
|
||||||
|
|
@ -276,17 +300,31 @@ func runPkgOutdated() error {
|
||||||
basePath = filepath.Join(filepath.Dir(regPath), basePath)
|
basePath = filepath.Join(filepath.Dir(regPath), basePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.outdated.outdated_label")), i18n.T("common.progress.checking_updates"))
|
jsonOutput := strings.EqualFold(format, "json")
|
||||||
|
if !jsonOutput {
|
||||||
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.outdated.outdated_label")), i18n.T("common.progress.checking_updates"))
|
||||||
|
}
|
||||||
|
|
||||||
var outdated, upToDate, notInstalled int
|
var installed, outdated, upToDate, notInstalled int
|
||||||
|
var entries []pkgOutdatedEntry
|
||||||
|
|
||||||
for _, r := range reg.List() {
|
for _, r := range reg.List() {
|
||||||
repoPath := filepath.Join(basePath, r.Name)
|
repoPath := filepath.Join(basePath, r.Name)
|
||||||
|
|
||||||
if !coreio.Local.Exists(filepath.Join(repoPath, ".git")) {
|
if !coreio.Local.Exists(filepath.Join(repoPath, ".git")) {
|
||||||
notInstalled++
|
notInstalled++
|
||||||
|
if jsonOutput {
|
||||||
|
entries = append(entries, pkgOutdatedEntry{
|
||||||
|
Name: r.Name,
|
||||||
|
Path: repoPath,
|
||||||
|
Behind: 0,
|
||||||
|
UpToDate: false,
|
||||||
|
Installed: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
installed++
|
||||||
|
|
||||||
// Fetch updates
|
// Fetch updates
|
||||||
_ = exec.Command("git", "-C", repoPath, "fetch", "--quiet").Run()
|
_ = exec.Command("git", "-C", repoPath, "fetch", "--quiet").Run()
|
||||||
|
|
@ -299,15 +337,52 @@ func runPkgOutdated() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
count := strings.TrimSpace(string(output))
|
count := strings.TrimSpace(string(output))
|
||||||
|
behind := 0
|
||||||
|
if count != "" {
|
||||||
|
fmt.Sscanf(count, "%d", &behind)
|
||||||
|
}
|
||||||
if count != "0" {
|
if count != "0" {
|
||||||
fmt.Printf(" %s %s (%s)\n",
|
if !jsonOutput {
|
||||||
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), i18n.T("cmd.pkg.outdated.commits_behind", map[string]string{"Count": count}))
|
fmt.Printf(" %s %s (%s)\n",
|
||||||
|
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), i18n.T("cmd.pkg.outdated.commits_behind", map[string]string{"Count": count}))
|
||||||
|
}
|
||||||
outdated++
|
outdated++
|
||||||
|
if jsonOutput {
|
||||||
|
entries = append(entries, pkgOutdatedEntry{
|
||||||
|
Name: r.Name,
|
||||||
|
Path: repoPath,
|
||||||
|
Behind: behind,
|
||||||
|
UpToDate: false,
|
||||||
|
Installed: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
upToDate++
|
upToDate++
|
||||||
|
if jsonOutput {
|
||||||
|
entries = append(entries, pkgOutdatedEntry{
|
||||||
|
Name: r.Name,
|
||||||
|
Path: repoPath,
|
||||||
|
Behind: 0,
|
||||||
|
UpToDate: true,
|
||||||
|
Installed: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
report := pkgOutdatedReport{
|
||||||
|
Format: "json",
|
||||||
|
Total: len(reg.List()),
|
||||||
|
Installed: installed,
|
||||||
|
Missing: notInstalled,
|
||||||
|
Outdated: outdated,
|
||||||
|
UpToDate: upToDate,
|
||||||
|
Packages: entries,
|
||||||
|
}
|
||||||
|
return printPkgOutdatedJSON(report)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if outdated == 0 {
|
if outdated == 0 {
|
||||||
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.outdated.all_up_to_date"))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.outdated.all_up_to_date"))
|
||||||
|
|
@ -319,3 +394,13 @@ func runPkgOutdated() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printPkgOutdatedJSON(report pkgOutdatedReport) error {
|
||||||
|
out, err := json.MarshalIndent(report, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.format", "outdated results"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(out))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -67,6 +68,70 @@ repos:
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "core-alpha", ".git"), 0755))
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "core-alpha", ".git"), 0755))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gitCommand(t *testing.T, dir string, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
require.NoError(t, err, "git %v failed: %s", args, string(out))
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commitGitRepo(t *testing.T, dir, filename, content, message string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644))
|
||||||
|
gitCommand(t, dir, "add", filename)
|
||||||
|
gitCommand(t, dir, "commit", "-m", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupOutdatedRegistry(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tmp := t.TempDir()
|
||||||
|
|
||||||
|
remoteDir := filepath.Join(tmp, "remote.git")
|
||||||
|
gitCommand(t, tmp, "init", "--bare", remoteDir)
|
||||||
|
|
||||||
|
seedDir := filepath.Join(tmp, "seed")
|
||||||
|
require.NoError(t, os.MkdirAll(seedDir, 0755))
|
||||||
|
gitCommand(t, seedDir, "init")
|
||||||
|
gitCommand(t, seedDir, "config", "user.email", "test@test.com")
|
||||||
|
gitCommand(t, seedDir, "config", "user.name", "Test")
|
||||||
|
commitGitRepo(t, seedDir, "repo.txt", "v1\n", "initial")
|
||||||
|
gitCommand(t, seedDir, "remote", "add", "origin", remoteDir)
|
||||||
|
gitCommand(t, seedDir, "push", "-u", "origin", "master")
|
||||||
|
|
||||||
|
freshDir := filepath.Join(tmp, "core-fresh")
|
||||||
|
gitCommand(t, tmp, "clone", remoteDir, freshDir)
|
||||||
|
|
||||||
|
staleDir := filepath.Join(tmp, "core-stale")
|
||||||
|
gitCommand(t, tmp, "clone", remoteDir, staleDir)
|
||||||
|
|
||||||
|
commitGitRepo(t, seedDir, "repo.txt", "v2\n", "second")
|
||||||
|
gitCommand(t, seedDir, "push")
|
||||||
|
gitCommand(t, freshDir, "pull", "--ff-only")
|
||||||
|
|
||||||
|
registry := strings.TrimSpace(`
|
||||||
|
org: host-uk
|
||||||
|
base_path: .
|
||||||
|
repos:
|
||||||
|
core-fresh:
|
||||||
|
type: foundation
|
||||||
|
description: Fresh package
|
||||||
|
core-stale:
|
||||||
|
type: module
|
||||||
|
description: Stale package
|
||||||
|
core-missing:
|
||||||
|
type: module
|
||||||
|
description: Missing package
|
||||||
|
`) + "\n"
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(tmp, "repos.yaml"), []byte(registry), 0644))
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunPkgList_Good(t *testing.T) {
|
func TestRunPkgList_Good(t *testing.T) {
|
||||||
tmp := t.TempDir()
|
tmp := t.TempDir()
|
||||||
writeTestRegistry(t, tmp)
|
writeTestRegistry(t, tmp)
|
||||||
|
|
@ -116,6 +181,51 @@ func TestRunPkgList_UnsupportedFormat(t *testing.T) {
|
||||||
assert.Contains(t, err.Error(), "unsupported format")
|
assert.Contains(t, err.Error(), "unsupported format")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunPkgOutdated_JSON(t *testing.T) {
|
||||||
|
tmp := setupOutdatedRegistry(t)
|
||||||
|
withWorkingDir(t, tmp)
|
||||||
|
|
||||||
|
out := capturePkgOutput(t, func() {
|
||||||
|
err := runPkgOutdated("json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
var report pkgOutdatedReport
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(out)), &report))
|
||||||
|
assert.Equal(t, "json", report.Format)
|
||||||
|
assert.Equal(t, 3, report.Total)
|
||||||
|
assert.Equal(t, 2, report.Installed)
|
||||||
|
assert.Equal(t, 1, report.Missing)
|
||||||
|
assert.Equal(t, 1, report.Outdated)
|
||||||
|
assert.Equal(t, 1, report.UpToDate)
|
||||||
|
require.Len(t, report.Packages, 3)
|
||||||
|
|
||||||
|
var staleFound, freshFound, missingFound bool
|
||||||
|
for _, pkg := range report.Packages {
|
||||||
|
switch pkg.Name {
|
||||||
|
case "core-stale":
|
||||||
|
staleFound = true
|
||||||
|
assert.True(t, pkg.Installed)
|
||||||
|
assert.False(t, pkg.UpToDate)
|
||||||
|
assert.Equal(t, 1, pkg.Behind)
|
||||||
|
case "core-fresh":
|
||||||
|
freshFound = true
|
||||||
|
assert.True(t, pkg.Installed)
|
||||||
|
assert.True(t, pkg.UpToDate)
|
||||||
|
assert.Equal(t, 0, pkg.Behind)
|
||||||
|
case "core-missing":
|
||||||
|
missingFound = true
|
||||||
|
assert.False(t, pkg.Installed)
|
||||||
|
assert.False(t, pkg.UpToDate)
|
||||||
|
assert.Equal(t, 0, pkg.Behind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, staleFound)
|
||||||
|
assert.True(t, freshFound)
|
||||||
|
assert.True(t, missingFound)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderPkgSearchResults_ShowsMetadata(t *testing.T) {
|
func TestRenderPkgSearchResults_ShowsMetadata(t *testing.T) {
|
||||||
out := capturePkgOutput(t, func() {
|
out := capturePkgOutput(t, func() {
|
||||||
renderPkgSearchResults([]ghRepo{
|
renderPkgSearchResults([]ghRepo{
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,5 @@ core pkg update core-api
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
core pkg outdated
|
core pkg outdated
|
||||||
|
core pkg outdated --format json
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,16 @@ core pkg outdated
|
||||||
|
|
||||||
Fetches from remote and shows packages that are behind.
|
Fetches from remote and shows packages that are behind.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--format` | Output format (`table` or `json`) |
|
||||||
|
|
||||||
|
### JSON Output
|
||||||
|
|
||||||
|
When `--format json` is set, `core pkg outdated` emits a structured report with package status, behind counts, and summary totals.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,10 @@
|
||||||
"all_up_to_date": "All packages are up to date",
|
"all_up_to_date": "All packages are up to date",
|
||||||
"commits_behind": "{{.Count}} commits behind",
|
"commits_behind": "{{.Count}} commits behind",
|
||||||
"update_with": "Update with: core pkg update {{.Name}}",
|
"update_with": "Update with: core pkg update {{.Name}}",
|
||||||
"summary": "{{.Outdated}}/{{.Total}} outdated"
|
"summary": "{{.Outdated}}/{{.Total}} outdated",
|
||||||
|
"flag": {
|
||||||
|
"format": "Output format: table or json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue