Skip to content

Commit 650d172

Browse files
authored
feat: implement burnless toil export command (closes #9)
- export toil events as CSV (default) or JSON - optional --month flag to filter by month - optional --output flag to write to a file instead of stdout - includes cost_usd column calculated from DefaultCostModel - CSV format compatible with Excel, Google Sheets
1 parent 1245c1e commit 650d172

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

internal/cli/toil.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func NewToilCmd() *cobra.Command {
1818

1919
toilCmd.AddCommand(newToilLogCmd())
2020
toilCmd.AddCommand(newToilReportCmd())
21+
toilCmd.AddCommand(newToilExportCmd())
2122

2223
return toilCmd
2324
}

internal/cli/toil_export.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package cli
2+
3+
import (
4+
"encoding/csv"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"os"
9+
"strconv"
10+
11+
"github.com/burnless/burnless/internal/toil"
12+
13+
"github.com/spf13/cobra"
14+
)
15+
16+
// NewToilExportCmd creates the "burnless toil export" command.
17+
func newToilExportCmd() *cobra.Command {
18+
var (
19+
format string
20+
month string
21+
output string
22+
)
23+
24+
cmd := &cobra.Command{
25+
Use: "export",
26+
Short: "Export toil events as CSV or JSON",
27+
SilenceUsage: true,
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
return runToilExport(format, month, output)
30+
},
31+
}
32+
33+
cmd.Flags().StringVar(&format, "format", "csv", "Output format: csv or json")
34+
cmd.Flags().StringVar(&month, "month", "", "Filter by month e.g. 2026-06 (default: all)")
35+
cmd.Flags().StringVar(&output, "output", "", "File path to write to (default: stdout)")
36+
37+
return cmd
38+
}
39+
40+
func runToilExport(format, month, output string) error {
41+
dbPath, err := toil.DefaultDBPath()
42+
if err != nil {
43+
return fmt.Errorf("finding database path: %w", err)
44+
}
45+
46+
store, err := toil.OpenStore(dbPath)
47+
if err != nil {
48+
return fmt.Errorf("opening toil database: %w", err)
49+
}
50+
defer func() { _ = store.Close() }()
51+
52+
events, err := store.All()
53+
if err != nil {
54+
return fmt.Errorf("reading toil events: %w", err)
55+
}
56+
57+
// filter by month if specified
58+
if month != "" {
59+
events, err = toil.FilterByMonth(events, month)
60+
if err != nil {
61+
return err
62+
}
63+
}
64+
65+
if len(events) == 0 {
66+
fmt.Fprintln(os.Stderr, "No toil events found.")
67+
return nil
68+
}
69+
70+
// determine output writer
71+
var w io.Writer = os.Stdout
72+
if output != "" {
73+
f, err := os.Create(output)
74+
if err != nil {
75+
return fmt.Errorf("creating output file: %w", err)
76+
}
77+
defer func() { _ = f.Close() }()
78+
w = f
79+
}
80+
81+
model := toil.DefaultCostModel()
82+
83+
switch format {
84+
case "csv":
85+
return exportCSV(w, events, model)
86+
case "json":
87+
return exportJSON(w, events, model)
88+
default:
89+
return fmt.Errorf("unknown format %q — use csv or json", format)
90+
}
91+
}
92+
93+
func exportCSV(w io.Writer, events []toil.Event, model toil.CostModel) error {
94+
cw := csv.NewWriter(w)
95+
defer cw.Flush()
96+
97+
// header
98+
if err := cw.Write([]string{
99+
"id", "service", "task", "date",
100+
"duration_minutes", "automatable", "trigger", "notes", "cost_usd",
101+
}); err != nil {
102+
return err
103+
}
104+
105+
// rows
106+
for _, e := range events {
107+
cost := model.Cost(e)
108+
if err := cw.Write([]string{
109+
e.ID,
110+
e.Service,
111+
e.Task,
112+
e.Date.Format("2006-01-02"),
113+
strconv.Itoa(e.DurationMins),
114+
strconv.FormatBool(e.Automatable),
115+
e.Trigger,
116+
e.Notes,
117+
fmt.Sprintf("%.2f", cost),
118+
}); err != nil {
119+
return err
120+
}
121+
}
122+
123+
return cw.Error()
124+
}
125+
126+
type exportRow struct {
127+
toil.Event
128+
CostUSD float64 `json:"cost_usd"`
129+
}
130+
131+
func exportJSON(w io.Writer, events []toil.Event, model toil.CostModel) error {
132+
rows := make([]exportRow, len(events))
133+
for i, e := range events {
134+
rows[i] = exportRow{
135+
Event: e,
136+
CostUSD: model.Cost(e),
137+
}
138+
}
139+
enc := json.NewEncoder(w)
140+
enc.SetIndent("", " ")
141+
return enc.Encode(rows)
142+
}

0 commit comments

Comments
 (0)