@cryptotaxi247 / kubo / commits / 5288946fd

feat(cli): improve ipfs dag stat output UX (#11097)

- add TTY auto-detection for progress display (matching `dag export`) - use single-line progress with carriage return instead of flooding - show human-readable sizes alongside raw bytes in summary - update --progress flag to be auto-detected by default progress format: `Fetched/Processed N blocks, M bytes (X MB)` summary format: `Total Size: 99 (99 B)`

Marcin Rataj committed Jan 9, 2026 at 18:41 UTC 5288946fd16f698f15f5bca029f61a59ed7fdf40
4 files changed +67 -11
core/commands/dag/dag.go
+7 -2
@@ -7,6 +7,7 @@ import (
7 "io"
8 "path"
9
10 + "github.com/dustin/go-humanize"
11 "github.com/ipfs/kubo/core/commands/cmdenv"
12 "github.com/ipfs/kubo/core/commands/cmdutils"
13
@@ -349,7 +350,11 @@ type DagStatSummary struct {
350 }
351
352 func (s *DagStatSummary) String() string {
352 - return fmt.Sprintf("Total Size: %d\nUnique Blocks: %d\nShared Size: %d\nRatio: %f", s.TotalSize, s.UniqueBlocks, s.SharedSize, s.Ratio)
353 + return fmt.Sprintf("Total Size: %d (%s)\nUnique Blocks: %d\nShared Size: %d (%s)\nRatio: %f",
354 + s.TotalSize, humanize.Bytes(s.TotalSize),
355 + s.UniqueBlocks,
356 + s.SharedSize, humanize.Bytes(s.SharedSize),
357 + s.Ratio)
358 }
359
360 func (s *DagStatSummary) incrementTotalSize(size uint64) {
@@ -384,7 +389,7 @@ Note: This command skips duplicate blocks in reporting both size and the number
389 cmds.StringArg("root", true, true, "CID of a DAG root to get statistics for").EnableStdin(),
390 },
391 Options: []cmds.Option{
387 - cmds.BoolOption(progressOptionName, "p", "Return progressive data while reading through the DAG").WithDefault(true),
392 + cmds.BoolOption(progressOptionName, "p", "Show progress on stderr. Auto-detected if stderr is a terminal."),
393 },
394 Run: dagStat,
395 Type: DagStatSummary{},
core/commands/dag/stat.go
+33 -7
@@ -5,6 +5,7 @@ import (
5 "io"
6 "os"
7
8 + "github.com/dustin/go-humanize"
9 mdag "github.com/ipfs/boxo/ipld/merkledag"
10 "github.com/ipfs/boxo/ipld/merkledag/traverse"
11 cid "github.com/ipfs/go-cid"
@@ -19,7 +20,11 @@ import (
20 // to compute the new state
21
22 func dagStat(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
22 - progressive := req.Options[progressOptionName].(bool)
23 + // Default to true (emit intermediate states) for HTTP/RPC clients that want progress
24 + progressive := true
25 + if val, specified := req.Options[progressOptionName].(bool); specified {
26 + progressive = val
27 + }
28 api, err := cmdenv.GetApi(env, req)
29 if err != nil {
30 return err
@@ -84,6 +89,18 @@ func dagStat(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment)
89 }
90
91 func finishCLIStat(res cmds.Response, re cmds.ResponseEmitter) error {
92 + // Determine whether to show progress based on TTY detection or explicit flag
93 + var showProgress bool
94 + val, specified := res.Request().Options[progressOptionName]
95 + if !specified {
96 + // Auto-detect: show progress only if stderr is a TTY
97 + if errStat, err := os.Stderr.Stat(); err == nil {
98 + showProgress = (errStat.Mode() & os.ModeCharDevice) != 0
99 + }
100 + } else {
101 + showProgress = val.(bool)
102 + }
103 +
104 var dagStats *DagStatSummary
105 for {
106 v, err := res.Next()
@@ -96,17 +113,26 @@ func finishCLIStat(res cmds.Response, re cmds.ResponseEmitter) error {
113 switch out := v.(type) {
114 case *DagStatSummary:
115 dagStats = out
99 - if dagStats.Ratio == 0 {
100 - length := len(dagStats.DagStatsArray)
101 - if length > 0 {
102 - currentStat := dagStats.DagStatsArray[length-1]
103 - fmt.Fprintf(os.Stderr, "CID: %s, Size: %d, NumBlocks: %d\n", currentStat.Cid, currentStat.Size, currentStat.NumBlocks)
116 + // Ratio == 0 means this is a progress update (not final result)
117 + if showProgress && dagStats.Ratio == 0 {
118 + // Sum up total progress across all DAGs being scanned
119 + var totalBlocks int64
120 + var totalSize uint64
121 + for _, stat := range dagStats.DagStatsArray {
122 + totalBlocks += stat.NumBlocks
123 + totalSize += stat.Size
124 }
125 + fmt.Fprintf(os.Stderr, "Fetched/Processed %d blocks, %d bytes (%s)\r", totalBlocks, totalSize, humanize.Bytes(totalSize))
126 }
127 default:
128 return e.TypeErr(out, v)
108 -
129 }
130 }
131 +
132 + // Clear the progress line before final output
133 + if showProgress {
134 + fmt.Fprint(os.Stderr, "\033[2K\r")
135 + }
136 +
137 return re.Emit(dagStats)
138 }
docs/changelogs/v0.40.md
+25
@@ -12,6 +12,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
12 - [🔦 Highlights](#-highlights)
13 - [Routing V1 HTTP API now exposed by default](#routing-v1-http-api-now-exposed-by-default)
14 - [Track total size when adding pins](#track-total-size-when-adding-pins)
15 + - [Improved `ipfs dag stat` output](#improved-ipfs-dag-stat-output)
16 - [Skip bad keys when listing](#skip_bad_keys_when_listing)
17 - [📦️ Dependency updates](#-dependency-updates)
18 - [📝 Changelog](#-changelog)
@@ -34,6 +35,30 @@ Example output:
35 Fetched/Processed 336 nodes (83 MB)
36 ```
37
38 +#### Improved `ipfs dag stat` output
39 +
40 +The `ipfs dag stat` command has been improved for better terminal UX:
41 +
42 +- Progress output now uses a single line with carriage return, avoiding terminal flooding
43 +- Progress is auto-detected: shown only in interactive terminals by default
44 +- Human-readable sizes are now displayed alongside raw byte counts
45 +
46 +Example progress (interactive terminal):
47 +```
48 +Fetched/Processed 84 blocks, 2097152 bytes (2.1 MB)
49 +```
50 +
51 +Example summary output:
52 +```
53 +Summary
54 +Total Size: 2097152 (2.1 MB)
55 +Unique Blocks: 42
56 +Shared Size: 1048576 (1.0 MB)
57 +Ratio: 1.500000
58 +```
59 +
60 +Use `--progress=true` to force progress even when piped, or `--progress=false` to disable it.
61 +
62 #### Skip bad keys when listing
63
64 Change the `ipfs key list` behavior to log an error and continue listing keys when a key cannot be read from the keystore or decoded.
test/cli/fixtures/TestDagStatExpectedOutput.txt
+2 -2
@@ -4,9 +4,9 @@ bafyreibmdfd7c5db4kls4ty57zljfhqv36gi43l6txl44pi423wwmeskwy 2 53
4 bafyreie3njilzdi4ixumru4nzgecsnjtu7fzfcwhg7e6s4s5i7cnbslvn4 2 53
5
6 Summary
7 -Total Size: 99
7 +Total Size: 99 (99 B)
8 Unique Blocks: 3
9 -Shared Size: 7
9 +Shared Size: 7 (7 B)
10 Ratio: 1.070707
11
12