Skip to content

Commit c73d535

Browse files
authored
Merge pull request #4 from Vader-7/feat/json-output
feat: add JSON output format via --format flag
2 parents 4344e94 + 8f7d186 commit c73d535

3 files changed

Lines changed: 570 additions & 18 deletions

File tree

cmd/root.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var (
2727
excludePatterns []string
2828
includePatterns []string
2929
branch string
30+
outputFormat string
3031
)
3132

3233
var rootCmd = &cobra.Command{
@@ -62,6 +63,12 @@ You can specify a local path or a repository URL as the source.`,
6263
return nil
6364
},
6465
Run: func(cmd *cobra.Command, args []string) {
66+
// Validate format flag early
67+
if outputFormat != "text" && outputFormat != "json" {
68+
fmt.Fprintf(os.Stderr, "Error: unsupported format '%s'. Use 'text' or 'json'.\n", outputFormat)
69+
os.Exit(1)
70+
}
71+
6572
source := args[0]
6673

6774
opts := digest.IngestionOptions{
@@ -84,31 +91,41 @@ You can specify a local path or a repository URL as the source.`,
8491
os.Exit(1)
8592
}
8693

87-
ingestResult.FormatOutput(opts)
94+
// Route to the correct formatter — exactly one call
95+
if outputFormat == "json" {
96+
jsonBytes, errJSON := ingestResult.FormatJSON(opts)
97+
if errJSON != nil {
98+
fmt.Fprintf(os.Stderr, "Error formatting JSON output: %v\n", errJSON)
99+
os.Exit(1)
100+
}
88101

89-
if opts.OutputFile != "" && opts.OutputFile != "-" {
90-
outputDir := filepath.Dir(opts.OutputFile)
91-
if outputDir != "." && outputDir != "" {
92-
if err := os.MkdirAll(outputDir, 0755); err != nil {
93-
fmt.Fprintf(os.Stderr, "Error creating output directory %s: %v\n", outputDir, err)
102+
if opts.OutputFile != "" && opts.OutputFile != "-" {
103+
if err := writeOutputFile(opts.OutputFile, jsonBytes); err != nil {
104+
fmt.Fprintf(os.Stderr, "Error writing to output file %s: %v\n", opts.OutputFile, err)
94105
os.Exit(1)
95106
}
107+
fmt.Fprintf(os.Stderr, "Digest written to: %s\n", opts.OutputFile)
108+
} else {
109+
os.Stdout.Write(jsonBytes)
96110
}
111+
} else {
112+
ingestResult.FormatOutput(opts)
97113

98-
fileContentToWrite := ingestResult.TreeStructure + "\n" + ingestResult.FileContents
99-
err = os.WriteFile(opts.OutputFile, []byte(fileContentToWrite), 0644)
100-
if err != nil {
101-
fmt.Fprintf(os.Stderr, "Error writing to output file %s: %v\n", opts.OutputFile, err)
102-
os.Exit(1)
114+
if opts.OutputFile != "" && opts.OutputFile != "-" {
115+
textBytes := []byte(ingestResult.TreeStructure + "\n" + ingestResult.FileContents)
116+
if err := writeOutputFile(opts.OutputFile, textBytes); err != nil {
117+
fmt.Fprintf(os.Stderr, "Error writing to output file %s: %v\n", opts.OutputFile, err)
118+
os.Exit(1)
119+
}
120+
fmt.Fprintf(os.Stderr, "Digest written to: %s\n", opts.OutputFile)
121+
} else {
122+
fmt.Println(ingestResult.TreeStructure)
123+
fmt.Println(ingestResult.FileContents)
103124
}
104-
fmt.Fprintf(os.Stderr, "Digest written to: %s\n", opts.OutputFile)
105-
} else {
106-
fmt.Println(ingestResult.TreeStructure)
107-
fmt.Println(ingestResult.FileContents)
108-
}
109125

110-
fmt.Fprintln(os.Stderr, "\n--- Summary ---")
111-
fmt.Fprint(os.Stderr, ingestResult.Summary)
126+
fmt.Fprintln(os.Stderr, "\n--- Summary ---")
127+
fmt.Fprint(os.Stderr, ingestResult.Summary)
128+
}
112129

113130
},
114131
}
@@ -126,6 +143,16 @@ var versionCmd = &cobra.Command{
126143
},
127144
}
128145

146+
func writeOutputFile(path string, data []byte) error {
147+
outputDir := filepath.Dir(path)
148+
if outputDir != "." && outputDir != "" {
149+
if err := os.MkdirAll(outputDir, 0755); err != nil {
150+
return err
151+
}
152+
}
153+
return os.WriteFile(path, data, 0644)
154+
}
155+
129156
func Execute() {
130157
err := rootCmd.Execute()
131158
if err != nil {
@@ -142,4 +169,5 @@ func init() {
142169
rootCmd.Flags().StringSliceP("exclude-pattern", "e", []string{}, "Comma-separated glob patterns to exclude (adds to defaults)")
143170
rootCmd.Flags().StringSliceVarP(&includePatterns, "include-pattern", "i", []string{}, "Comma-separated glob patterns to include (overrides excludes)")
144171
rootCmd.Flags().StringVarP(&branch, "branch", "b", "", "Branch to clone and ingest (if source is a Git URL)")
172+
rootCmd.Flags().StringVarP(&outputFormat, "format", "f", "text", "Output format: text or json")
145173
}

internal/digest/json.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package digest
2+
3+
import (
4+
"encoding/json"
5+
"path/filepath"
6+
)
7+
8+
type JSONOutput struct {
9+
Summary JSONSummary `json:"summary"`
10+
Tree []*JSONNode `json:"tree"`
11+
Files []JSONFile `json:"files"`
12+
GitInfo *JSONGitInfo `json:"git_info,omitempty"`
13+
}
14+
15+
type JSONSummary struct {
16+
Source string `json:"source"`
17+
TotalFiles int `json:"total_files"`
18+
TotalSize int64 `json:"total_size"`
19+
TotalSizeHuman string `json:"total_size_human"`
20+
ExcludePatterns []string `json:"exclude_patterns"`
21+
IncludePatterns []string `json:"include_patterns"`
22+
MaxFileSize int64 `json:"max_file_size"`
23+
}
24+
25+
type JSONNode struct {
26+
Name string `json:"name"`
27+
Path string `json:"path"`
28+
Type string `json:"type"`
29+
Size int64 `json:"size,omitempty"`
30+
Children []*JSONNode `json:"children,omitempty"`
31+
}
32+
33+
type JSONFile struct {
34+
Path string `json:"path"`
35+
Size int64 `json:"size"`
36+
Type string `json:"type"`
37+
Content string `json:"content"`
38+
}
39+
40+
type JSONGitInfo struct {
41+
RepoURL string `json:"repo_url,omitempty"`
42+
Branch string `json:"branch,omitempty"`
43+
Commit string `json:"commit,omitempty"`
44+
User string `json:"user,omitempty"`
45+
RepoName string `json:"repo_name,omitempty"`
46+
}
47+
48+
func (r *Result) FormatJSON(opts IngestionOptions) ([]byte, error) {
49+
output := JSONOutput{
50+
Summary: JSONSummary{
51+
Source: opts.Source,
52+
TotalFiles: r.TotalFiles,
53+
TotalSize: r.TotalSize,
54+
TotalSizeHuman: formatBytes(r.TotalSize),
55+
ExcludePatterns: opts.ExcludePatterns,
56+
IncludePatterns: opts.IncludePatterns,
57+
MaxFileSize: opts.MaxFileSize,
58+
},
59+
Tree: buildJSONTree(r.RootNode),
60+
Files: gatherJSONFiles(r.RootNode),
61+
}
62+
63+
if r.GitInfo != nil {
64+
output.GitInfo = &JSONGitInfo{
65+
RepoURL: r.GitInfo.RepoURL,
66+
Branch: r.GitInfo.Branch,
67+
Commit: r.GitInfo.Commit,
68+
User: r.GitInfo.User,
69+
RepoName: r.GitInfo.RepoName,
70+
}
71+
}
72+
73+
return json.MarshalIndent(output, "", " ")
74+
}
75+
76+
func buildJSONTree(node *FileNode) []*JSONNode {
77+
if node == nil {
78+
return nil
79+
}
80+
81+
if node.Type == NodeTypeDir && node.Children != nil {
82+
result := make([]*JSONNode, 0, len(node.Children))
83+
for _, child := range node.Children {
84+
result = append(result, fileNodeToJSON(child))
85+
}
86+
return result
87+
}
88+
89+
return []*JSONNode{fileNodeToJSON(node)}
90+
}
91+
92+
func fileNodeToJSON(node *FileNode) *JSONNode {
93+
jn := &JSONNode{
94+
Name: node.Name,
95+
Path: filepath.ToSlash(node.Path),
96+
Type: string(node.Type),
97+
Size: node.Size,
98+
}
99+
100+
if node.Type == NodeTypeDir && node.Children != nil {
101+
jn.Children = make([]*JSONNode, 0, len(node.Children))
102+
for _, child := range node.Children {
103+
jn.Children = append(jn.Children, fileNodeToJSON(child))
104+
}
105+
}
106+
107+
return jn
108+
}
109+
110+
func gatherJSONFiles(node *FileNode) []JSONFile {
111+
var files []JSONFile
112+
gatherJSONFilesRecursive(node, &files)
113+
return files
114+
}
115+
116+
func gatherJSONFilesRecursive(node *FileNode, files *[]JSONFile) {
117+
if node.Type == NodeTypeFile {
118+
f := JSONFile{
119+
Path: filepath.ToSlash(node.Path),
120+
Size: node.Size,
121+
Type: string(node.Type),
122+
Content: node.Content, // always include, even if empty
123+
}
124+
*files = append(*files, f)
125+
} else if node.Type == NodeTypeNotText || node.Type == NodeTypeTooLarge {
126+
*files = append(*files, JSONFile{
127+
Path: filepath.ToSlash(node.Path),
128+
Size: node.Size,
129+
Type: string(node.Type),
130+
})
131+
}
132+
133+
if node.Type == NodeTypeDir {
134+
for _, child := range node.Children {
135+
gatherJSONFilesRecursive(child, files)
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)