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