1 package cmd
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "path/filepath"
9 "slices"
10 "text/tabwriter"
11
12 "sigs.k8s.io/yaml"
13
14 "edge-infra.dev/pkg/lib/cli/rags"
15 "edge-infra.dev/pkg/lib/cli/sink"
16 "edge-infra.dev/pkg/tools/dlog"
17 )
18
19
20
21 func New() *sink.Command {
22 var (
23 dir, status, out string
24 tags []string
25 )
26
27 cmd := &sink.Command{
28 Use: "dlog [command] [flags]",
29 Short: "Manage decision logs.",
30 Flags: []*rags.Rag{
31 dirFlag(&dir),
32 {
33 Name: "status",
34 Short: "s",
35 Usage: "Filter by dlog status",
36 Value: &rags.String{Var: &status},
37 },
38 {
39 Name: "tags",
40 Usage: "Comma separated list of tags to filter dlogs by. Can be provided multiple times.",
41 Value: &rags.StringSet{Var: &tags},
42 },
43 {
44 Name: "output",
45 Usage: "Control output format. One of [plain, json, yaml]",
46 Value: &rags.StringEnum{
47 Var: &out, Valid: []string{"plain", "json", "yaml"},
48 },
49 },
50 },
51 Commands: []*sink.Command{
52 newCreateCmd(),
53 newEditCmd(),
54 },
55 Exec: func(_ context.Context, r sink.Run) error {
56 decisions, err := dlog.FromDir(dir)
57 if err != nil {
58 return err
59 }
60
61 decisions = filterDecisions(decisions, status, tags)
62
63 switch out {
64 case "json":
65 data, err := json.Marshal(dlogsToExternalFmt(dir, decisions))
66 if err != nil {
67 return err
68 }
69 out := new(bytes.Buffer)
70 if err := json.Indent(out, data, "", "\t"); err != nil {
71 return err
72 }
73 fmt.Fprintln(r.Out(), out)
74 return nil
75 case "yaml":
76 data, err := yaml.Marshal(dlogsToExternalFmt(dir, decisions))
77 if err != nil {
78 return err
79 }
80 fmt.Fprintln(r.Out(), string(data))
81 return nil
82 case "plain":
83 tw := tabwriter.NewWriter(r.Out(), 2, 0, 1, ' ', 0)
84 defer tw.Flush()
85
86 for _, d := range decisions {
87 fmt.Fprintf(tw, "%s\t%s\n", d.Number(), d.Title())
88 fmt.Fprintf(tw, "\t%s\t%s\t\n", d.Status, d.Date)
89 if len(d.Tags) > 0 {
90 fmt.Fprintf(tw, "\ttags:\t%s\n", d.Tags)
91 }
92 fmt.Fprintf(tw, "\tdeciders:\t%s\n", d.Deciders)
93 if len(d.Consulted) > 0 {
94 fmt.Fprintf(tw, "\tconsulted:\t%s\n", d.Consulted)
95 }
96 if len(d.Informed) > 0 {
97 fmt.Fprintf(tw, "\tinformed:\t%s\n", d.Informed)
98 }
99 fmt.Fprintln(tw)
100 }
101 }
102
103 return nil
104 },
105 }
106
107 return cmd
108 }
109
110 func dirFlag(dir *string) *rags.Rag {
111 return &rags.Rag{
112 Name: "dir",
113 Short: "d",
114 Value: rags.NewValueDefault(dir, "docs/decisions"),
115 Usage: "Root directory for decision logs",
116 }
117 }
118
119 func templateFlag(template *string) *rags.Rag {
120 return &rags.Rag{
121 Name: "template",
122 Short: "t",
123 Value: rags.NewValueDefault(template, "docs/decisions/template/template.md"),
124 Usage: "Path to decision log template",
125 }
126 }
127
128 func filterDecisions(dd []*dlog.Decision, status string, tags []string) []*dlog.Decision {
129 if status == "" && len(tags) == 0 {
130 return dd
131 }
132
133 n := 0
134 for _, d := range dd {
135 if !shouldSkip(d, status, tags) {
136 dd[n] = d
137 n++
138 }
139 }
140 dd = dd[:n]
141
142 return dd
143 }
144
145 func shouldSkip(d *dlog.Decision, status string, tags []string) bool {
146 if status != "" && d.Status != status {
147 return true
148 }
149 if len(tags) > 0 {
150 for _, t := range tags {
151 if !slices.Contains(d.Tags, t) {
152 return true
153 }
154 }
155 }
156 return false
157 }
158
159
160
161
162
163
164
165
166
167
168
169 func dlogsToExternalFmt(dir string, dd []*dlog.Decision) any {
170 type printedDlog struct {
171 *dlog.Decision
172 Number string `json:"number"`
173 File string `json:"file"`
174 Title string `json:"title"`
175 }
176 a := make([]printedDlog, len(dd))
177 for i, d := range dd {
178 a[i] = printedDlog{
179 Decision: d,
180 Number: d.Number(),
181 File: filepath.Join(dir, d.File()),
182 Title: d.Title(),
183 }
184 }
185 return a
186 }
187
View as plain text