From d55e9e9413882a08c9a51a1f28ca4c3ed9af9ca5 Mon Sep 17 00:00:00 2001 From: Gubarz <1037896+Gubarz@users.noreply.github.com> Date: Mon, 18 May 2026 11:24:15 -0600 Subject: [PATCH] feat: dump - exports cheats into json/csv --- cmd/cheatmd/main.go | 161 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/cmd/cheatmd/main.go b/cmd/cheatmd/main.go index 9c5977b..a712b80 100644 --- a/cmd/cheatmd/main.go +++ b/cmd/cheatmd/main.go @@ -1,10 +1,13 @@ package main import ( + "encoding/csv" + "encoding/json" "fmt" "os" "path/filepath" "runtime" + "strconv" "strings" "time" @@ -52,6 +55,7 @@ func init() { rootCmd.AddCommand(widgetCmd) rootCmd.AddCommand(chainCmd) + rootCmd.AddCommand(dumpCmd) chainCmd.AddCommand(chainResetCmd) rootCmd.PersistentFlags().StringP("output", "o", "", "Output mode: print, copy, exec") @@ -67,6 +71,9 @@ func init() { rootCmd.PersistentFlags().Bool("strict", false, "Treat lint warnings as errors") viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output")) + + dumpCmd.Flags().Bool("csv", false, "Dump cheats as CSV") + dumpCmd.Flags().Bool("json", false, "Dump cheats as JSON") } var chainCmd = &cobra.Command{ @@ -81,6 +88,160 @@ var chainResetCmd = &cobra.Command{ RunE: runChainReset, } +var dumpCmd = &cobra.Command{ + Use: "dump [path]", + Short: "Dump parsed cheat metadata", + Args: cobra.MaximumNArgs(1), + RunE: runDump, +} + +type dumpEntry struct { + Filename string `json:"filename"` + Tags []string `json:"tags"` + Title string `json:"title"` + Description string `json:"description"` + Command string `json:"command"` + ChainName string `json:"chain_name,omitempty"` + ChainStep int `json:"chain_step,omitempty"` + Variables []dumpVar `json:"variables"` +} + +type dumpVar struct { + Name string `json:"name"` + Kind string `json:"kind"` + Value string `json:"value,omitempty"` + Args string `json:"args,omitempty"` + Condition string `json:"condition,omitempty"` +} + +func runDump(cmd *cobra.Command, args []string) error { + useCSV, _ := cmd.Flags().GetBool("csv") + useJSON, _ := cmd.Flags().GetBool("json") + if useCSV && useJSON { + return fmt.Errorf("choose only one dump format: --csv or --json") + } + if !useCSV && !useJSON { + useJSON = true + } + + index, err := parseDumpIndex(args) + if err != nil { + return err + } + + entries := make([]dumpEntry, 0, len(index.Cheats)) + for _, cheat := range index.Cheats { + entries = append(entries, dumpEntry{ + Filename: cheat.File, + Tags: cheat.Tags, + Title: cheat.Header, + Description: cheat.Description, + Command: cheat.Command, + ChainName: cheat.ChainName, + ChainStep: cheat.ChainStep, + Variables: dumpVars(cheat.Vars), + }) + } + + if useCSV { + return writeDumpCSV(cmd, entries) + } + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetIndent("", " ") + enc.SetEscapeHTML(false) + return enc.Encode(entries) +} + +func dumpVars(vars []parser.VarDef) []dumpVar { + out := make([]dumpVar, 0, len(vars)) + for _, v := range vars { + dv := dumpVar{ + Name: v.Name, + Args: v.Args, + Condition: v.Condition, + } + switch { + case v.Literal != "": + dv.Kind = "literal" + dv.Value = v.Literal + case v.Shell != "": + dv.Kind = "shell" + dv.Value = v.Shell + default: + dv.Kind = "prompt" + } + out = append(out, dv) + } + return out +} + +func parseDumpIndex(args []string) (*parser.CheatIndex, error) { + path := "." + if len(args) > 0 { + path = args[0] + } else if config.GetPath() != "." { + path = config.GetPath() + } + absPath, err := filepath.Abs(path) + if err != nil { + return nil, fmt.Errorf("error resolving path: %w", err) + } + info, err := os.Stat(absPath) + if err != nil { + return nil, fmt.Errorf("path error: %w", err) + } + p := parser.NewParser() + if info.IsDir() { + return p.ParseDirectory(absPath) + } + return p.ParseSingleFile(absPath) +} + +func writeDumpCSV(cmd *cobra.Command, entries []dumpEntry) error { + w := csv.NewWriter(cmd.OutOrStdout()) + if err := w.Write([]string{"filename", "tags", "title", "description", "command", "chain_name", "chain_step", "variables"}); err != nil { + return err + } + for _, entry := range entries { + stepStr := "" + if entry.ChainStep > 0 { + stepStr = strconv.Itoa(entry.ChainStep) + } + if err := w.Write([]string{ + entry.Filename, + strings.Join(entry.Tags, "|"), + entry.Title, + entry.Description, + entry.Command, + entry.ChainName, + stepStr, + formatDumpVars(entry.Variables), + }); err != nil { + return err + } + } + w.Flush() + return w.Error() +} + +func formatDumpVars(vars []dumpVar) string { + parts := make([]string, 0, len(vars)) + for _, v := range vars { + part := v.Name + ":" + v.Kind + if v.Value != "" { + part += "=" + v.Value + } + if v.Args != "" { + part += " --- " + v.Args + } + if v.Condition != "" { + part += " if " + v.Condition + } + parts = append(parts, part) + } + return strings.Join(parts, "|") +} + func runChainReset(cmd *cobra.Command, args []string) error { root := "." if config.GetPath() != "." {