@supalytics/cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/package.json +3 -2
- package/src/commands/completions.ts +305 -0
- package/src/commands/events.ts +47 -9
- package/src/commands/query.ts +47 -24
- package/src/commands/stats.ts +94 -9
- package/src/commands/trend.ts +13 -1
- package/src/index.ts +6 -1
- package/src/ui.ts +111 -0
package/README.md
CHANGED
|
@@ -46,6 +46,14 @@ supalytics realtime # Current visitors
|
|
|
46
46
|
supalytics realtime --watch # Auto-refresh every 30s
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
### Trend
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
supalytics trend # Daily visitor trend with bar chart
|
|
53
|
+
supalytics trend --period 7d # Last 7 days
|
|
54
|
+
supalytics trend --compact # Compact sparkline only
|
|
55
|
+
```
|
|
56
|
+
|
|
49
57
|
### Breakdowns
|
|
50
58
|
|
|
51
59
|
```bash
|
|
@@ -105,6 +113,21 @@ supalytics default <domain> # Set default site
|
|
|
105
113
|
supalytics stats -s other.com # Query specific site
|
|
106
114
|
```
|
|
107
115
|
|
|
116
|
+
## Shell Completions
|
|
117
|
+
|
|
118
|
+
Enable tab completion for your shell:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Bash (add to ~/.bashrc)
|
|
122
|
+
eval "$(supalytics completions bash)"
|
|
123
|
+
|
|
124
|
+
# Zsh (add to ~/.zshrc)
|
|
125
|
+
eval "$(supalytics completions zsh)"
|
|
126
|
+
|
|
127
|
+
# Fish
|
|
128
|
+
supalytics completions fish > ~/.config/fish/completions/supalytics.fish
|
|
129
|
+
```
|
|
130
|
+
|
|
108
131
|
## License
|
|
109
132
|
|
|
110
133
|
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supalytics/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for Supalytics web analytics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
|
11
|
-
"bin"
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
15
|
"dev": "bun run src/index.ts"
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
const BASH_COMPLETION = `# Supalytics CLI Bash Completion
|
|
4
|
+
# Add this to ~/.bashrc or ~/.bash_profile:
|
|
5
|
+
# eval "$(supalytics completions bash)"
|
|
6
|
+
|
|
7
|
+
_supalytics_completions() {
|
|
8
|
+
local cur prev commands opts
|
|
9
|
+
COMPREPLY=()
|
|
10
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
11
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
12
|
+
|
|
13
|
+
commands="login logout sites default remove stats pages referrers countries trend query events realtime completions help"
|
|
14
|
+
|
|
15
|
+
# Main command completion
|
|
16
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
17
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
|
|
18
|
+
return 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Subcommand options
|
|
22
|
+
case "\${COMP_WORDS[1]}" in
|
|
23
|
+
stats)
|
|
24
|
+
opts="today yesterday week month year 7d 14d 30d 90d 12mo all --site --start --end --filter --all --no-revenue --json"
|
|
25
|
+
;;
|
|
26
|
+
pages|referrers|countries)
|
|
27
|
+
opts="--site --period --start --end --limit --filter --no-revenue --json"
|
|
28
|
+
;;
|
|
29
|
+
trend)
|
|
30
|
+
opts="--site --period --start --end --filter --no-revenue --compact --json"
|
|
31
|
+
;;
|
|
32
|
+
query)
|
|
33
|
+
opts="--site --metrics --dimensions --filter --sort --timezone --period --start --end --limit --offset --no-revenue --json"
|
|
34
|
+
;;
|
|
35
|
+
events)
|
|
36
|
+
opts="--site --period --property --limit --no-revenue --json"
|
|
37
|
+
;;
|
|
38
|
+
realtime)
|
|
39
|
+
opts="--site --json --watch"
|
|
40
|
+
;;
|
|
41
|
+
login)
|
|
42
|
+
opts="--domain"
|
|
43
|
+
;;
|
|
44
|
+
completions)
|
|
45
|
+
opts="bash zsh fish"
|
|
46
|
+
;;
|
|
47
|
+
*)
|
|
48
|
+
opts=""
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
complete -F _supalytics_completions supalytics
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const ZSH_COMPLETION = `#compdef supalytics
|
|
60
|
+
# Supalytics CLI Zsh Completion
|
|
61
|
+
# Add this to ~/.zshrc:
|
|
62
|
+
# eval "$(supalytics completions zsh)"
|
|
63
|
+
|
|
64
|
+
_supalytics() {
|
|
65
|
+
local -a commands
|
|
66
|
+
commands=(
|
|
67
|
+
'login:Add a site by verifying its API key'
|
|
68
|
+
'logout:Remove stored credentials'
|
|
69
|
+
'sites:List all configured sites'
|
|
70
|
+
'default:Set the default site'
|
|
71
|
+
'remove:Remove a site'
|
|
72
|
+
'stats:Overview stats (pageviews, visitors, bounce rate, revenue)'
|
|
73
|
+
'pages:Top pages by visitors'
|
|
74
|
+
'referrers:Top referrers'
|
|
75
|
+
'countries:Traffic by country'
|
|
76
|
+
'trend:Daily visitor trend'
|
|
77
|
+
'query:Flexible query with custom metrics and dimensions'
|
|
78
|
+
'events:List and explore custom events'
|
|
79
|
+
'realtime:Live visitors on your site right now'
|
|
80
|
+
'completions:Generate shell completions'
|
|
81
|
+
'help:Display help for command'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
local -a period_opts
|
|
85
|
+
period_opts=(
|
|
86
|
+
'today:Today only'
|
|
87
|
+
'yesterday:Yesterday only'
|
|
88
|
+
'week:This week'
|
|
89
|
+
'month:This month'
|
|
90
|
+
'year:This year'
|
|
91
|
+
'7d:Last 7 days'
|
|
92
|
+
'14d:Last 14 days'
|
|
93
|
+
'30d:Last 30 days'
|
|
94
|
+
'90d:Last 90 days'
|
|
95
|
+
'12mo:Last 12 months'
|
|
96
|
+
'all:All time'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
_arguments -C \\
|
|
100
|
+
'1: :->command' \\
|
|
101
|
+
'*:: :->args'
|
|
102
|
+
|
|
103
|
+
case \$state in
|
|
104
|
+
command)
|
|
105
|
+
_describe 'command' commands
|
|
106
|
+
;;
|
|
107
|
+
args)
|
|
108
|
+
case \$words[1] in
|
|
109
|
+
stats)
|
|
110
|
+
_arguments \\
|
|
111
|
+
'1: :->period' \\
|
|
112
|
+
'--site[Site to query]:domain:' \\
|
|
113
|
+
'--start[Start date]:date:' \\
|
|
114
|
+
'--end[End date]:date:' \\
|
|
115
|
+
'*--filter[Filter]:filter:' \\
|
|
116
|
+
'--all[Show detailed breakdown]' \\
|
|
117
|
+
'--no-revenue[Exclude revenue metrics]' \\
|
|
118
|
+
'--json[Output as JSON]'
|
|
119
|
+
case \$state in
|
|
120
|
+
period)
|
|
121
|
+
_describe 'period' period_opts
|
|
122
|
+
;;
|
|
123
|
+
esac
|
|
124
|
+
;;
|
|
125
|
+
pages|referrers|countries)
|
|
126
|
+
_arguments \\
|
|
127
|
+
'--site[Site to query]:domain:' \\
|
|
128
|
+
'--period[Time period]:period:(7d 14d 30d 90d 12mo all)' \\
|
|
129
|
+
'--start[Start date]:date:' \\
|
|
130
|
+
'--end[End date]:date:' \\
|
|
131
|
+
'--limit[Number of results]:limit:' \\
|
|
132
|
+
'*--filter[Filter]:filter:' \\
|
|
133
|
+
'--no-revenue[Exclude revenue metrics]' \\
|
|
134
|
+
'--json[Output as JSON]'
|
|
135
|
+
;;
|
|
136
|
+
trend)
|
|
137
|
+
_arguments \\
|
|
138
|
+
'--site[Site to query]:domain:' \\
|
|
139
|
+
'--period[Time period]:period:(7d 14d 30d 90d 12mo all)' \\
|
|
140
|
+
'--start[Start date]:date:' \\
|
|
141
|
+
'--end[End date]:date:' \\
|
|
142
|
+
'*--filter[Filter]:filter:' \\
|
|
143
|
+
'--no-revenue[Exclude revenue metrics]' \\
|
|
144
|
+
'--compact[Show compact sparkline only]' \\
|
|
145
|
+
'--json[Output as JSON]'
|
|
146
|
+
;;
|
|
147
|
+
query)
|
|
148
|
+
_arguments \\
|
|
149
|
+
'--site[Site to query]:domain:' \\
|
|
150
|
+
'--metrics[Metrics]:metrics:(visitors bounce_rate avg_session_duration revenue conversions)' \\
|
|
151
|
+
'--dimensions[Dimensions]:dimensions:(page referrer country region city browser os device date hour event)' \\
|
|
152
|
+
'*--filter[Filter]:filter:' \\
|
|
153
|
+
'--sort[Sort by field]:sort:' \\
|
|
154
|
+
'--timezone[Timezone]:timezone:' \\
|
|
155
|
+
'--period[Time period]:period:(7d 14d 30d 90d 12mo all)' \\
|
|
156
|
+
'--start[Start date]:date:' \\
|
|
157
|
+
'--end[End date]:date:' \\
|
|
158
|
+
'--limit[Number of results]:limit:' \\
|
|
159
|
+
'--offset[Skip results]:offset:' \\
|
|
160
|
+
'--no-revenue[Exclude revenue metrics]' \\
|
|
161
|
+
'--json[Output as JSON]'
|
|
162
|
+
;;
|
|
163
|
+
events)
|
|
164
|
+
_arguments \\
|
|
165
|
+
'1:event name:' \\
|
|
166
|
+
'--site[Site to query]:domain:' \\
|
|
167
|
+
'--period[Time period]:period:(7d 14d 30d 90d 12mo all)' \\
|
|
168
|
+
'--property[Property key]:property:' \\
|
|
169
|
+
'--limit[Number of results]:limit:' \\
|
|
170
|
+
'--no-revenue[Exclude revenue]' \\
|
|
171
|
+
'--json[Output as JSON]'
|
|
172
|
+
;;
|
|
173
|
+
realtime)
|
|
174
|
+
_arguments \\
|
|
175
|
+
'--site[Site to query]:domain:' \\
|
|
176
|
+
'--json[Output as JSON]' \\
|
|
177
|
+
'--watch[Auto-refresh every 30 seconds]'
|
|
178
|
+
;;
|
|
179
|
+
login)
|
|
180
|
+
_arguments \\
|
|
181
|
+
'1:API key:' \\
|
|
182
|
+
'--domain[Override detected domain]:domain:'
|
|
183
|
+
;;
|
|
184
|
+
default|remove)
|
|
185
|
+
_arguments '1:domain:'
|
|
186
|
+
;;
|
|
187
|
+
completions)
|
|
188
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
189
|
+
;;
|
|
190
|
+
esac
|
|
191
|
+
;;
|
|
192
|
+
esac
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_supalytics
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const FISH_COMPLETION = `# Supalytics CLI Fish Completion
|
|
199
|
+
# Add this to ~/.config/fish/completions/supalytics.fish:
|
|
200
|
+
# supalytics completions fish > ~/.config/fish/completions/supalytics.fish
|
|
201
|
+
|
|
202
|
+
# Disable file completion by default
|
|
203
|
+
complete -c supalytics -f
|
|
204
|
+
|
|
205
|
+
# Commands
|
|
206
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "login" -d "Add a site by verifying its API key"
|
|
207
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "logout" -d "Remove stored credentials"
|
|
208
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "sites" -d "List all configured sites"
|
|
209
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "default" -d "Set the default site"
|
|
210
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "remove" -d "Remove a site"
|
|
211
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "stats" -d "Overview stats"
|
|
212
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "pages" -d "Top pages by visitors"
|
|
213
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "referrers" -d "Top referrers"
|
|
214
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "countries" -d "Traffic by country"
|
|
215
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "trend" -d "Daily visitor trend"
|
|
216
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "query" -d "Flexible query with custom metrics"
|
|
217
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "events" -d "List and explore custom events"
|
|
218
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "realtime" -d "Live visitors right now"
|
|
219
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "completions" -d "Generate shell completions"
|
|
220
|
+
complete -c supalytics -n "__fish_use_subcommand" -a "help" -d "Display help for command"
|
|
221
|
+
|
|
222
|
+
# Stats options
|
|
223
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -a "today yesterday week month year 7d 14d 30d 90d 12mo all"
|
|
224
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -l site -s s -d "Site to query"
|
|
225
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -l start -d "Start date"
|
|
226
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -l end -d "End date"
|
|
227
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -l filter -s f -d "Filter"
|
|
228
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -l all -s a -d "Show detailed breakdown"
|
|
229
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -l no-revenue -d "Exclude revenue"
|
|
230
|
+
complete -c supalytics -n "__fish_seen_subcommand_from stats" -l json -d "Output as JSON"
|
|
231
|
+
|
|
232
|
+
# Pages/Referrers/Countries options
|
|
233
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l site -s s -d "Site to query"
|
|
234
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l period -s p -d "Time period" -a "7d 14d 30d 90d 12mo all"
|
|
235
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l start -d "Start date"
|
|
236
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l end -d "End date"
|
|
237
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l limit -s l -d "Number of results"
|
|
238
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l filter -s f -d "Filter"
|
|
239
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l no-revenue -d "Exclude revenue"
|
|
240
|
+
complete -c supalytics -n "__fish_seen_subcommand_from pages referrers countries" -l json -d "Output as JSON"
|
|
241
|
+
|
|
242
|
+
# Trend options
|
|
243
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l site -s s -d "Site to query"
|
|
244
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l period -s p -d "Time period" -a "7d 14d 30d 90d 12mo all"
|
|
245
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l start -d "Start date"
|
|
246
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l end -d "End date"
|
|
247
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l filter -s f -d "Filter"
|
|
248
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l no-revenue -d "Exclude revenue"
|
|
249
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l compact -d "Show compact sparkline"
|
|
250
|
+
complete -c supalytics -n "__fish_seen_subcommand_from trend" -l json -d "Output as JSON"
|
|
251
|
+
|
|
252
|
+
# Query options
|
|
253
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l site -s s -d "Site to query"
|
|
254
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l metrics -s m -d "Metrics" -a "visitors bounce_rate avg_session_duration revenue conversions"
|
|
255
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l dimensions -s d -d "Dimensions" -a "page referrer country region city browser os device date hour event"
|
|
256
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l filter -s f -d "Filter"
|
|
257
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l sort -d "Sort by field"
|
|
258
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l timezone -d "Timezone"
|
|
259
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l period -s p -d "Time period" -a "7d 14d 30d 90d 12mo all"
|
|
260
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l start -d "Start date"
|
|
261
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l end -d "End date"
|
|
262
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l limit -s l -d "Number of results"
|
|
263
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l offset -d "Skip results"
|
|
264
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l no-revenue -d "Exclude revenue"
|
|
265
|
+
complete -c supalytics -n "__fish_seen_subcommand_from query" -l json -d "Output as JSON"
|
|
266
|
+
|
|
267
|
+
# Events options
|
|
268
|
+
complete -c supalytics -n "__fish_seen_subcommand_from events" -l site -s s -d "Site to query"
|
|
269
|
+
complete -c supalytics -n "__fish_seen_subcommand_from events" -l period -s p -d "Time period" -a "7d 14d 30d 90d 12mo all"
|
|
270
|
+
complete -c supalytics -n "__fish_seen_subcommand_from events" -l property -d "Property key"
|
|
271
|
+
complete -c supalytics -n "__fish_seen_subcommand_from events" -l limit -s l -d "Number of results"
|
|
272
|
+
complete -c supalytics -n "__fish_seen_subcommand_from events" -l no-revenue -d "Exclude revenue"
|
|
273
|
+
complete -c supalytics -n "__fish_seen_subcommand_from events" -l json -d "Output as JSON"
|
|
274
|
+
|
|
275
|
+
# Realtime options
|
|
276
|
+
complete -c supalytics -n "__fish_seen_subcommand_from realtime" -l site -s s -d "Site to query"
|
|
277
|
+
complete -c supalytics -n "__fish_seen_subcommand_from realtime" -l json -d "Output as JSON"
|
|
278
|
+
complete -c supalytics -n "__fish_seen_subcommand_from realtime" -l watch -s w -d "Auto-refresh"
|
|
279
|
+
|
|
280
|
+
# Login options
|
|
281
|
+
complete -c supalytics -n "__fish_seen_subcommand_from login" -l domain -d "Override detected domain"
|
|
282
|
+
|
|
283
|
+
# Completions command
|
|
284
|
+
complete -c supalytics -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"
|
|
285
|
+
`;
|
|
286
|
+
|
|
287
|
+
export const completionsCommand = new Command("completions")
|
|
288
|
+
.description("Generate shell completions")
|
|
289
|
+
.argument("<shell>", "Shell type: bash, zsh, or fish")
|
|
290
|
+
.action((shell: string) => {
|
|
291
|
+
switch (shell.toLowerCase()) {
|
|
292
|
+
case "bash":
|
|
293
|
+
console.log(BASH_COMPLETION);
|
|
294
|
+
break;
|
|
295
|
+
case "zsh":
|
|
296
|
+
console.log(ZSH_COMPLETION);
|
|
297
|
+
break;
|
|
298
|
+
case "fish":
|
|
299
|
+
console.log(FISH_COMPLETION);
|
|
300
|
+
break;
|
|
301
|
+
default:
|
|
302
|
+
console.error(`Unknown shell: ${shell}. Use bash, zsh, or fish.`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
});
|
package/src/commands/events.ts
CHANGED
|
@@ -9,14 +9,14 @@ Examples:
|
|
|
9
9
|
# List all events
|
|
10
10
|
supalytics events
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Show event stats and properties
|
|
13
13
|
supalytics events signup
|
|
14
14
|
|
|
15
15
|
# Get breakdown of a property
|
|
16
16
|
supalytics events signup --property plan
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
supalytics events signup --property plan --revenue`;
|
|
18
|
+
# Without revenue
|
|
19
|
+
supalytics events signup --property plan --no-revenue`;
|
|
20
20
|
|
|
21
21
|
function displayPropertyKeys(response: PropertyKeysResponse, event: string, json: boolean) {
|
|
22
22
|
if (json) {
|
|
@@ -48,7 +48,7 @@ export const eventsCommand = new Command("events")
|
|
|
48
48
|
.option("-p, --period <period>", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
|
|
49
49
|
.option("--property <key>", "Get breakdown for a specific property")
|
|
50
50
|
.option("-l, --limit <number>", "Number of results", "20")
|
|
51
|
-
.option("--revenue", "
|
|
51
|
+
.option("--no-revenue", "Exclude revenue metrics")
|
|
52
52
|
.option("--json", "Output as JSON")
|
|
53
53
|
.action(async (event, options) => {
|
|
54
54
|
const site = options.site || (await getDefaultSite());
|
|
@@ -94,7 +94,7 @@ export const eventsCommand = new Command("events")
|
|
|
94
94
|
options.property,
|
|
95
95
|
options.period,
|
|
96
96
|
parseInt(options.limit),
|
|
97
|
-
options.revenue
|
|
97
|
+
options.revenue !== false
|
|
98
98
|
);
|
|
99
99
|
|
|
100
100
|
if (options.json) {
|
|
@@ -113,7 +113,7 @@ export const eventsCommand = new Command("events")
|
|
|
113
113
|
|
|
114
114
|
for (const v of response.data) {
|
|
115
115
|
let line = ` ${chalk.cyan(v.value)} ${formatNumber(v.visitors)} visitors ${formatNumber(v.count)} events`;
|
|
116
|
-
if (options.revenue && v.revenue !== null) {
|
|
116
|
+
if (options.revenue !== false && v.revenue !== null) {
|
|
117
117
|
line += ` ${chalk.green("$" + (v.revenue / 100).toFixed(2))}`;
|
|
118
118
|
}
|
|
119
119
|
console.log(line);
|
|
@@ -122,9 +122,47 @@ export const eventsCommand = new Command("events")
|
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// If just event name, show properties
|
|
126
|
-
const
|
|
127
|
-
|
|
125
|
+
// If just event name, show event stats + properties
|
|
126
|
+
const [eventsResponse, propsResponse] = await Promise.all([
|
|
127
|
+
listEvents(site, options.period, 100), // Get all events to find this one
|
|
128
|
+
getEventProperties(site, event, options.period),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
// Find the specific event stats
|
|
132
|
+
const eventData = eventsResponse.data.find((e) => e.name === event);
|
|
133
|
+
|
|
134
|
+
if (options.json) {
|
|
135
|
+
console.log(JSON.stringify({
|
|
136
|
+
event: eventData || { name: event, count: 0, visitors: 0, has_properties: false },
|
|
137
|
+
properties: propsResponse.data,
|
|
138
|
+
meta: propsResponse.meta,
|
|
139
|
+
}, null, 2));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const [startDate, endDate] = propsResponse.meta.date_range;
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(chalk.bold(event), chalk.dim(`${startDate} → ${endDate}`));
|
|
146
|
+
console.log();
|
|
147
|
+
|
|
148
|
+
// Show event stats
|
|
149
|
+
if (eventData) {
|
|
150
|
+
console.log(` ${formatNumber(eventData.visitors)} visitors ${formatNumber(eventData.count)} events`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(chalk.dim(" No data for this event"));
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
|
|
156
|
+
// Show properties
|
|
157
|
+
if (propsResponse.data.length > 0) {
|
|
158
|
+
console.log(chalk.dim(" PROPERTIES"));
|
|
159
|
+
for (const key of propsResponse.data) {
|
|
160
|
+
console.log(` ${chalk.cyan(key)}`);
|
|
161
|
+
}
|
|
162
|
+
console.log();
|
|
163
|
+
console.log(chalk.dim(` Use: supalytics events ${event} --property <key>`));
|
|
164
|
+
}
|
|
165
|
+
console.log();
|
|
128
166
|
|
|
129
167
|
} catch (error) {
|
|
130
168
|
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
package/src/commands/query.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { query, formatNumber, formatPercent, formatDuration } from "../api";
|
|
4
4
|
import { getDefaultSite } from "../config";
|
|
5
|
+
import { table, type TableColumn } from "../ui";
|
|
5
6
|
|
|
6
7
|
const queryDescription = `Flexible query with custom metrics and dimensions.
|
|
7
8
|
|
|
@@ -36,7 +37,7 @@ Examples:
|
|
|
36
37
|
export const queryCommand = new Command("query")
|
|
37
38
|
.description(queryDescription)
|
|
38
39
|
.option("-s, --site <site>", "Site to query")
|
|
39
|
-
.option("-m, --metrics <metrics>", "Metrics: visitors, bounce_rate, avg_session_duration, revenue, conversions", "visitors")
|
|
40
|
+
.option("-m, --metrics <metrics>", "Metrics: visitors, bounce_rate, avg_session_duration, revenue, conversions", "visitors,revenue")
|
|
40
41
|
.option("-d, --dimensions <dimensions>", "Dimensions: page, referrer, country, region, city, browser, os, device, date, hour, event, utm_* (max 2, event cannot combine)")
|
|
41
42
|
.option("-f, --filter <filters...>", "Filters: field:operator:value (e.g., 'page:contains:/blog', 'event:is:signup', 'event_property:is:plan:premium')")
|
|
42
43
|
.option("--sort <sort>", "Sort by field:order (e.g., 'revenue:desc', 'visitors:asc')")
|
|
@@ -126,34 +127,56 @@ export const queryCommand = new Command("query")
|
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
// Build columns from data
|
|
131
|
+
const columns: TableColumn[] = [];
|
|
132
|
+
|
|
133
|
+
// Add dimension columns
|
|
134
|
+
if (response.data[0]?.dimensions) {
|
|
135
|
+
for (const key of Object.keys(response.data[0].dimensions)) {
|
|
136
|
+
columns.push({
|
|
137
|
+
key: `dim_${key}`,
|
|
138
|
+
label: key.toUpperCase(),
|
|
139
|
+
align: "left",
|
|
140
|
+
});
|
|
138
141
|
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add metric columns
|
|
145
|
+
for (const key of Object.keys(response.data[0]?.metrics || {})) {
|
|
146
|
+
columns.push({
|
|
147
|
+
key: `met_${key}`,
|
|
148
|
+
label: key.toUpperCase().replaceAll("_", " "),
|
|
149
|
+
align: "right",
|
|
150
|
+
format: (val) => {
|
|
151
|
+
if (val === null || val === undefined) return "-";
|
|
152
|
+
const v = val as number;
|
|
153
|
+
if (key === "bounce_rate" || key === "conversion_rate") {
|
|
154
|
+
return formatPercent(v);
|
|
155
|
+
} else if (key === "avg_session_duration") {
|
|
156
|
+
return formatDuration(v);
|
|
157
|
+
} else if (key === "revenue") {
|
|
158
|
+
return chalk.green("$" + (v / 100).toFixed(2));
|
|
159
|
+
}
|
|
160
|
+
return formatNumber(v);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
formatted = formatDuration(value);
|
|
147
|
-
} else if (key === "revenue") {
|
|
148
|
-
formatted = chalk.green("$" + (value / 100).toFixed(2));
|
|
149
|
-
} else {
|
|
150
|
-
formatted = formatNumber(value);
|
|
165
|
+
// Transform data for table
|
|
166
|
+
const tableData = response.data.map((r) => {
|
|
167
|
+
const row: Record<string, unknown> = {};
|
|
168
|
+
if (r.dimensions) {
|
|
169
|
+
for (const [k, v] of Object.entries(r.dimensions)) {
|
|
170
|
+
row[`dim_${k}`] = v || "(none)";
|
|
151
171
|
}
|
|
152
|
-
parts.push(`${chalk.dim(key)}=${formatted}`);
|
|
153
172
|
}
|
|
173
|
+
for (const [k, v] of Object.entries(r.metrics)) {
|
|
174
|
+
row[`met_${k}`] = v;
|
|
175
|
+
}
|
|
176
|
+
return row;
|
|
177
|
+
});
|
|
154
178
|
|
|
155
|
-
|
|
156
|
-
}
|
|
179
|
+
console.log(table(tableData, columns));
|
|
157
180
|
console.log();
|
|
158
181
|
} catch (error) {
|
|
159
182
|
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
package/src/commands/stats.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { query, formatNumber, formatPercent, formatDuration } from "../api";
|
|
3
|
+
import { query, formatNumber, formatPercent, formatDuration, listEvents } from "../api";
|
|
4
4
|
import { getDefaultSite } from "../config";
|
|
5
|
-
import { logo, parsePeriod } from "../ui";
|
|
5
|
+
import { logo, parsePeriod, truncate } from "../ui";
|
|
6
6
|
|
|
7
7
|
export const statsCommand = new Command("stats")
|
|
8
8
|
.description("Overview stats: pageviews, visitors, bounce rate, revenue")
|
|
@@ -11,6 +11,7 @@ export const statsCommand = new Command("stats")
|
|
|
11
11
|
.option("--start <date>", "Start date (YYYY-MM-DD)")
|
|
12
12
|
.option("--end <date>", "End date (YYYY-MM-DD)")
|
|
13
13
|
.option("-f, --filter <filters...>", "Filters in format 'field:operator:value'")
|
|
14
|
+
.option("-a, --all", "Show detailed breakdown (pages, referrers, countries, etc.)")
|
|
14
15
|
.option("--no-revenue", "Exclude revenue metrics")
|
|
15
16
|
.option("--json", "Output as JSON")
|
|
16
17
|
.action(async (period, options) => {
|
|
@@ -43,15 +44,46 @@ export const statsCommand = new Command("stats")
|
|
|
43
44
|
metrics.push("revenue", "conversions", "conversion_rate");
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// Build API calls
|
|
48
|
+
const apiCalls: Promise<unknown>[] = [
|
|
49
|
+
query(site, {
|
|
50
|
+
metrics,
|
|
51
|
+
filters,
|
|
52
|
+
date_range: dateRange,
|
|
53
|
+
include_revenue: options.revenue !== false,
|
|
54
|
+
}),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Add breakdown calls if --all flag
|
|
58
|
+
if (options.all) {
|
|
59
|
+
const breakdownOpts = { filters, date_range: dateRange, limit: 10 };
|
|
60
|
+
apiCalls.push(
|
|
61
|
+
query(site, { metrics: ["visitors"], dimensions: ["page"], ...breakdownOpts }),
|
|
62
|
+
query(site, { metrics: ["visitors"], dimensions: ["referrer"], ...breakdownOpts }),
|
|
63
|
+
query(site, { metrics: ["visitors"], dimensions: ["country"], ...breakdownOpts }),
|
|
64
|
+
query(site, { metrics: ["visitors"], dimensions: ["browser"], ...breakdownOpts }),
|
|
65
|
+
query(site, { metrics: ["visitors"], dimensions: ["utm_source"], ...breakdownOpts }),
|
|
66
|
+
listEvents(site, typeof dateRange === "string" ? dateRange : "30d", 10)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const results = await Promise.all(apiCalls);
|
|
71
|
+
const response = results[0] as Awaited<ReturnType<typeof query>>;
|
|
52
72
|
|
|
53
73
|
if (options.json) {
|
|
54
|
-
|
|
74
|
+
if (options.all) {
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
stats: results[0],
|
|
77
|
+
pages: results[1],
|
|
78
|
+
referrers: results[2],
|
|
79
|
+
countries: results[3],
|
|
80
|
+
browsers: results[4],
|
|
81
|
+
utm_sources: results[5],
|
|
82
|
+
events: results[6],
|
|
83
|
+
}, null, 2));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(JSON.stringify(response, null, 2));
|
|
86
|
+
}
|
|
55
87
|
return;
|
|
56
88
|
}
|
|
57
89
|
|
|
@@ -96,6 +128,59 @@ export const statsCommand = new Command("stats")
|
|
|
96
128
|
console.log(` ${chalk.bold(convRate)}`);
|
|
97
129
|
}
|
|
98
130
|
|
|
131
|
+
// Detailed breakdowns if --all
|
|
132
|
+
if (options.all) {
|
|
133
|
+
const pagesRes = results[1] as Awaited<ReturnType<typeof query>>;
|
|
134
|
+
const referrersRes = results[2] as Awaited<ReturnType<typeof query>>;
|
|
135
|
+
const countriesRes = results[3] as Awaited<ReturnType<typeof query>>;
|
|
136
|
+
const browsersRes = results[4] as Awaited<ReturnType<typeof query>>;
|
|
137
|
+
const utmRes = results[5] as Awaited<ReturnType<typeof query>>;
|
|
138
|
+
const eventsRes = results[6] as Awaited<ReturnType<typeof listEvents>>;
|
|
139
|
+
|
|
140
|
+
const renderBreakdown = (
|
|
141
|
+
title: string,
|
|
142
|
+
data: { dimensions?: Record<string, string>; metrics: Record<string, number> }[],
|
|
143
|
+
dimKey: string,
|
|
144
|
+
metricKey: string = "visitors",
|
|
145
|
+
metricLabel: string = "VISITORS"
|
|
146
|
+
) => {
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(chalk.dim(" ────────────────────────────────────────"));
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(` ${chalk.dim(title.padEnd(36))}${chalk.dim(metricLabel)}`);
|
|
151
|
+
if (data.length === 0) {
|
|
152
|
+
console.log(chalk.dim(" (no data)"));
|
|
153
|
+
} else {
|
|
154
|
+
for (const row of data) {
|
|
155
|
+
const dim = truncate(row.dimensions?.[dimKey] || "(direct)", 34);
|
|
156
|
+
const val = formatNumber(row.metrics[metricKey] || 0);
|
|
157
|
+
console.log(` ${dim.padEnd(36)}${val.padStart(8)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
renderBreakdown("PAGES", pagesRes.data, "page");
|
|
163
|
+
renderBreakdown("REFERRERS", referrersRes.data, "referrer");
|
|
164
|
+
renderBreakdown("COUNTRIES", countriesRes.data, "country");
|
|
165
|
+
renderBreakdown("BROWSERS", browsersRes.data, "browser");
|
|
166
|
+
renderBreakdown("UTM SOURCES", utmRes.data, "utm_source");
|
|
167
|
+
|
|
168
|
+
// Events have different structure
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(chalk.dim(" ────────────────────────────────────────"));
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(` ${chalk.dim("EVENTS".padEnd(36))}${chalk.dim("COUNT")}`);
|
|
173
|
+
if (eventsRes.data.length === 0) {
|
|
174
|
+
console.log(chalk.dim(" (no events)"));
|
|
175
|
+
} else {
|
|
176
|
+
for (const event of eventsRes.data) {
|
|
177
|
+
const name = truncate(event.name, 34);
|
|
178
|
+
const count = formatNumber(event.count);
|
|
179
|
+
console.log(` ${name.padEnd(36)}${count.padStart(8)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
99
184
|
console.log();
|
|
100
185
|
} catch (error) {
|
|
101
186
|
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
package/src/commands/trend.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { query, formatNumber } from "../api";
|
|
4
4
|
import { getDefaultSite } from "../config";
|
|
5
|
+
import { coloredSparkline } from "../ui";
|
|
5
6
|
|
|
6
7
|
export const trendCommand = new Command("trend")
|
|
7
8
|
.description("Daily visitor trend")
|
|
@@ -11,6 +12,7 @@ export const trendCommand = new Command("trend")
|
|
|
11
12
|
.option("--end <date>", "End date (YYYY-MM-DD)")
|
|
12
13
|
.option("-f, --filter <filters...>", "Filters in format 'field:operator:value' (e.g., 'is:country:US')")
|
|
13
14
|
.option("--no-revenue", "Exclude revenue metrics")
|
|
15
|
+
.option("--compact", "Show compact sparkline only")
|
|
14
16
|
.option("--json", "Output as JSON")
|
|
15
17
|
.action(async (options) => {
|
|
16
18
|
const site = options.site || (await getDefaultSite());
|
|
@@ -68,8 +70,18 @@ export const trendCommand = new Command("trend")
|
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
const visitors = response.data.map((r) => r.metrics.visitors || 0);
|
|
74
|
+
const total = visitors.reduce((a, b) => a + b, 0);
|
|
75
|
+
|
|
76
|
+
// Compact mode - just sparkline
|
|
77
|
+
if (options.compact) {
|
|
78
|
+
console.log(` ${coloredSparkline(visitors)} ${formatNumber(total)} visitors`);
|
|
79
|
+
console.log();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
// Find max for sparkline scaling
|
|
72
|
-
const maxVisitors = Math.max(...
|
|
84
|
+
const maxVisitors = Math.max(...visitors);
|
|
73
85
|
|
|
74
86
|
for (const result of response.data) {
|
|
75
87
|
const date = result.dimensions?.date || "";
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { trendCommand } from "./commands/trend";
|
|
|
11
11
|
import { queryCommand } from "./commands/query";
|
|
12
12
|
import { eventsCommand } from "./commands/events";
|
|
13
13
|
import { realtimeCommand } from "./commands/realtime";
|
|
14
|
+
import { completionsCommand } from "./commands/completions";
|
|
14
15
|
|
|
15
16
|
const description = `CLI for Supalytics web analytics.
|
|
16
17
|
|
|
@@ -54,10 +55,13 @@ Output:
|
|
|
54
55
|
--json Raw JSON output (useful for piping to other tools or AI)
|
|
55
56
|
--no-revenue Exclude revenue metrics from output`;
|
|
56
57
|
|
|
58
|
+
// Read version from package.json
|
|
59
|
+
const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
60
|
+
|
|
57
61
|
program
|
|
58
62
|
.name("supalytics")
|
|
59
63
|
.description(description)
|
|
60
|
-
.version(
|
|
64
|
+
.version(pkg.version);
|
|
61
65
|
|
|
62
66
|
// Auth & site management commands
|
|
63
67
|
program.addCommand(loginCommand);
|
|
@@ -75,5 +79,6 @@ program.addCommand(trendCommand);
|
|
|
75
79
|
program.addCommand(queryCommand);
|
|
76
80
|
program.addCommand(eventsCommand);
|
|
77
81
|
program.addCommand(realtimeCommand);
|
|
82
|
+
program.addCommand(completionsCommand);
|
|
78
83
|
|
|
79
84
|
program.parse();
|
package/src/ui.ts
CHANGED
|
@@ -77,3 +77,114 @@ export function formatRevenue(cents: number): string {
|
|
|
77
77
|
}
|
|
78
78
|
return chalk.green("$" + (cents / 100).toFixed(0));
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
// Sparkline characters from lowest to highest
|
|
82
|
+
const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate a sparkline from an array of numbers
|
|
86
|
+
*/
|
|
87
|
+
export function sparkline(values: number[]): string {
|
|
88
|
+
if (values.length === 0) return "";
|
|
89
|
+
|
|
90
|
+
const min = Math.min(...values);
|
|
91
|
+
const max = Math.max(...values);
|
|
92
|
+
const range = max - min || 1;
|
|
93
|
+
|
|
94
|
+
return values
|
|
95
|
+
.map((v) => {
|
|
96
|
+
const normalized = (v - min) / range;
|
|
97
|
+
const index = Math.min(Math.floor(normalized * SPARK_CHARS.length), SPARK_CHARS.length - 1);
|
|
98
|
+
return SPARK_CHARS[index];
|
|
99
|
+
})
|
|
100
|
+
.join("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate a colored sparkline (green for up, red for down trend)
|
|
105
|
+
*/
|
|
106
|
+
export function coloredSparkline(values: number[]): string {
|
|
107
|
+
if (values.length === 0) return "";
|
|
108
|
+
|
|
109
|
+
const spark = sparkline(values);
|
|
110
|
+
const first = values[0];
|
|
111
|
+
const last = values[values.length - 1];
|
|
112
|
+
|
|
113
|
+
if (last > first) {
|
|
114
|
+
return chalk.green(spark);
|
|
115
|
+
} else if (last < first) {
|
|
116
|
+
return chalk.red(spark);
|
|
117
|
+
}
|
|
118
|
+
return chalk.dim(spark);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface TableColumn {
|
|
122
|
+
key: string;
|
|
123
|
+
label?: string;
|
|
124
|
+
align?: "left" | "right";
|
|
125
|
+
width?: number;
|
|
126
|
+
format?: (value: unknown) => string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TableOptions {
|
|
130
|
+
indent?: number;
|
|
131
|
+
showHeader?: boolean;
|
|
132
|
+
headerColor?: (s: string) => string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Render a table with proper column alignment
|
|
137
|
+
*/
|
|
138
|
+
export function table(
|
|
139
|
+
data: Record<string, unknown>[],
|
|
140
|
+
columns: TableColumn[],
|
|
141
|
+
options: TableOptions = {}
|
|
142
|
+
): string {
|
|
143
|
+
const { indent = 2, showHeader = true, headerColor = chalk.dim } = options;
|
|
144
|
+
const prefix = " ".repeat(indent);
|
|
145
|
+
const lines: string[] = [];
|
|
146
|
+
|
|
147
|
+
// Calculate column widths
|
|
148
|
+
const widths = columns.map((col) => {
|
|
149
|
+
const label = col.label || col.key;
|
|
150
|
+
const dataWidth = Math.max(
|
|
151
|
+
...data.map((row) => {
|
|
152
|
+
const val = row[col.key];
|
|
153
|
+
const formatted = col.format ? col.format(val) : String(val ?? "");
|
|
154
|
+
// Strip ANSI codes for width calculation
|
|
155
|
+
return formatted.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
return col.width || Math.max(label.length, dataWidth);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Header row
|
|
162
|
+
if (showHeader) {
|
|
163
|
+
const headerParts = columns.map((col, i) => {
|
|
164
|
+
const label = col.label || col.key.toUpperCase();
|
|
165
|
+
const width = widths[i];
|
|
166
|
+
return col.align === "right" ? label.padStart(width) : label.padEnd(width);
|
|
167
|
+
});
|
|
168
|
+
lines.push(prefix + headerColor(headerParts.join(" ")));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Data rows
|
|
172
|
+
for (const row of data) {
|
|
173
|
+
const parts = columns.map((col, i) => {
|
|
174
|
+
const val = row[col.key];
|
|
175
|
+
const formatted = col.format ? col.format(val) : String(val ?? "");
|
|
176
|
+
const width = widths[i];
|
|
177
|
+
// Get display length (without ANSI codes)
|
|
178
|
+
const displayLen = formatted.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
179
|
+
const padding = Math.max(0, width - displayLen);
|
|
180
|
+
|
|
181
|
+
if (col.align === "right") {
|
|
182
|
+
return " ".repeat(padding) + formatted;
|
|
183
|
+
}
|
|
184
|
+
return formatted + " ".repeat(padding);
|
|
185
|
+
});
|
|
186
|
+
lines.push(prefix + parts.join(" "));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|