feat(pkg): add json output for pkg update
All checks were successful
Security Scan / security (push) Successful in 18s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 05:38:12 +00:00
parent e259ce323b
commit 2a9177a30b
3 changed files with 163 additions and 13 deletions

View file

@ -157,6 +157,7 @@ func runPkgList(format string) error {
} }
var updateAll bool var updateAll bool
var updateFormat string
// addPkgUpdateCommand adds the 'pkg update' command. // addPkgUpdateCommand adds the 'pkg update' command.
func addPkgUpdateCommand(parent *cobra.Command) { func addPkgUpdateCommand(parent *cobra.Command) {
@ -165,16 +166,40 @@ func addPkgUpdateCommand(parent *cobra.Command) {
Short: i18n.T("cmd.pkg.update.short"), Short: i18n.T("cmd.pkg.update.short"),
Long: i18n.T("cmd.pkg.update.long"), Long: i18n.T("cmd.pkg.update.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPkgUpdate(args, updateAll) format, err := cmd.Flags().GetString("format")
if err != nil {
return err
}
return runPkgUpdate(args, updateAll, format)
}, },
} }
updateCmd.Flags().BoolVar(&updateAll, "all", false, i18n.T("cmd.pkg.update.flag.all")) updateCmd.Flags().BoolVar(&updateAll, "all", false, i18n.T("cmd.pkg.update.flag.all"))
updateCmd.Flags().StringVar(&updateFormat, "format", "table", "Output format: table or json")
parent.AddCommand(updateCmd) parent.AddCommand(updateCmd)
} }
func runPkgUpdate(packages []string, all bool) error { type pkgUpdateEntry struct {
Name string `json:"name"`
Path string `json:"path"`
Installed bool `json:"installed"`
Status string `json:"status"`
Output string `json:"output,omitempty"`
}
type pkgUpdateReport struct {
Format string `json:"format"`
Total int `json:"total"`
Installed int `json:"installed"`
Missing int `json:"missing"`
Updated int `json:"updated"`
UpToDate int `json:"upToDate"`
Failed int `json:"failed"`
Packages []pkgUpdateEntry `json:"packages"`
}
func runPkgUpdate(packages []string, all bool, 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"))
@ -193,6 +218,7 @@ func runPkgUpdate(packages []string, all bool) error {
basePath = filepath.Join(filepath.Dir(regPath), basePath) basePath = filepath.Join(filepath.Dir(regPath), basePath)
} }
jsonOutput := strings.EqualFold(format, "json")
var toUpdate []string var toUpdate []string
if all || len(packages) == 0 { if all || len(packages) == 0 {
for _, r := range reg.List() { for _, r := range reg.List() {
@ -202,44 +228,117 @@ func runPkgUpdate(packages []string, all bool) error {
toUpdate = packages toUpdate = packages
} }
if !jsonOutput {
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.update.update_label")), i18n.T("cmd.pkg.update.updating", map[string]int{"Count": len(toUpdate)})) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.update.update_label")), i18n.T("cmd.pkg.update.updating", map[string]int{"Count": len(toUpdate)}))
}
var updated, skipped, failed int var updated, upToDate, skipped, failed int
var entries []pkgUpdateEntry
for _, name := range toUpdate { for _, name := range toUpdate {
repoPath := filepath.Join(basePath, name) repoPath := filepath.Join(basePath, name)
if _, err := coreio.Local.List(filepath.Join(repoPath, ".git")); err != nil { if _, err := coreio.Local.List(filepath.Join(repoPath, ".git")); err != nil {
if !jsonOutput {
fmt.Printf(" %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed")) fmt.Printf(" %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed"))
}
if jsonOutput {
entries = append(entries, pkgUpdateEntry{
Name: name,
Path: repoPath,
Installed: false,
Status: "missing",
})
}
skipped++ skipped++
continue continue
} }
if !jsonOutput {
fmt.Printf(" %s %s... ", dimStyle.Render("↓"), name) fmt.Printf(" %s %s... ", dimStyle.Render("↓"), name)
}
cmd := exec.Command("git", "-C", repoPath, "pull", "--ff-only") cmd := exec.Command("git", "-C", repoPath, "pull", "--ff-only")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
if !jsonOutput {
fmt.Printf("%s\n", errorStyle.Render("✗")) fmt.Printf("%s\n", errorStyle.Render("✗"))
fmt.Printf(" %s\n", strings.TrimSpace(string(output))) fmt.Printf(" %s\n", strings.TrimSpace(string(output)))
}
if jsonOutput {
entries = append(entries, pkgUpdateEntry{
Name: name,
Path: repoPath,
Installed: true,
Status: "failed",
Output: strings.TrimSpace(string(output)),
})
}
failed++ failed++
continue continue
} }
if strings.Contains(string(output), "Already up to date") { if strings.Contains(string(output), "Already up to date") {
if !jsonOutput {
fmt.Printf("%s\n", dimStyle.Render(i18n.T("common.status.up_to_date"))) fmt.Printf("%s\n", dimStyle.Render(i18n.T("common.status.up_to_date")))
}
if jsonOutput {
entries = append(entries, pkgUpdateEntry{
Name: name,
Path: repoPath,
Installed: true,
Status: "up_to_date",
Output: strings.TrimSpace(string(output)),
})
}
upToDate++
} else { } else {
if !jsonOutput {
fmt.Printf("%s\n", successStyle.Render("✓")) fmt.Printf("%s\n", successStyle.Render("✓"))
} }
if jsonOutput {
entries = append(entries, pkgUpdateEntry{
Name: name,
Path: repoPath,
Installed: true,
Status: "updated",
Output: strings.TrimSpace(string(output)),
})
}
updated++ updated++
} }
}
if jsonOutput {
report := pkgUpdateReport{
Format: "json",
Total: len(toUpdate),
Installed: updated + upToDate + failed,
Missing: skipped,
Updated: updated,
UpToDate: upToDate,
Failed: failed,
Packages: entries,
}
return printPkgUpdateJSON(report)
}
fmt.Println() fmt.Println()
fmt.Printf("%s %s\n", fmt.Printf("%s %s\n",
dimStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.update.summary", map[string]int{"Updated": updated, "Skipped": skipped, "Failed": failed})) dimStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.update.summary", map[string]int{"Updated": updated + upToDate, "Skipped": skipped, "Failed": failed}))
return nil return nil
} }
func printPkgUpdateJSON(report pkgUpdateReport) error {
out, err := json.MarshalIndent(report, "", " ")
if err != nil {
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.format", "update results"), err)
}
fmt.Println(string(out))
return nil
}
// 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 var format string

View file

@ -297,7 +297,7 @@ func TestRunPkgUpdate_NoArgs_UpdatesAll(t *testing.T) {
withWorkingDir(t, tmp) withWorkingDir(t, tmp)
out := capturePkgOutput(t, func() { out := capturePkgOutput(t, func() {
err := runPkgUpdate(nil, false) err := runPkgUpdate(nil, false, "table")
require.NoError(t, err) require.NoError(t, err)
}) })
@ -305,3 +305,46 @@ func TestRunPkgUpdate_NoArgs_UpdatesAll(t *testing.T) {
assert.Contains(t, out, "core-fresh") assert.Contains(t, out, "core-fresh")
assert.Contains(t, out, "core-stale") assert.Contains(t, out, "core-stale")
} }
func TestRunPkgUpdate_JSON(t *testing.T) {
tmp := setupOutdatedRegistry(t)
withWorkingDir(t, tmp)
out := capturePkgOutput(t, func() {
err := runPkgUpdate(nil, false, "json")
require.NoError(t, err)
})
var report pkgUpdateReport
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.Updated)
assert.Equal(t, 1, report.UpToDate)
assert.Equal(t, 0, report.Failed)
require.Len(t, report.Packages, 3)
var updatedFound, upToDateFound, missingFound bool
for _, pkg := range report.Packages {
switch pkg.Name {
case "core-stale":
updatedFound = true
assert.True(t, pkg.Installed)
assert.Equal(t, "updated", pkg.Status)
case "core-fresh":
upToDateFound = true
assert.True(t, pkg.Installed)
assert.Equal(t, "up_to_date", pkg.Status)
case "core-missing":
missingFound = true
assert.False(t, pkg.Installed)
assert.Equal(t, "missing", pkg.Status)
}
}
assert.True(t, updatedFound)
assert.True(t, upToDateFound)
assert.True(t, missingFound)
}

View file

@ -126,6 +126,7 @@ core pkg update [<name>...] [flags]
| Flag | Description | | Flag | Description |
|------|-------------| |------|-------------|
| `--all` | Update all packages | | `--all` | Update all packages |
| `--format` | Output format (`table` or `json`) |
### Examples ### Examples
@ -135,8 +136,15 @@ core pkg update core-php
# Update all packages # Update all packages
core pkg update --all core pkg update --all
# JSON output for automation
core pkg update --format json
``` ```
### JSON Output
When `--format json` is set, `core pkg update` emits a structured report with per-package update status and summary totals.
--- ---
## pkg outdated ## pkg outdated