diff --git a/cmd/root.go b/cmd/root.go index 26145a5..06ccb59 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,6 +27,7 @@ var ( excludePatterns []string includePatterns []string branch string + outputFormat string ) var rootCmd = &cobra.Command{ @@ -84,7 +85,19 @@ You can specify a local path or a repository URL as the source.`, os.Exit(1) } - ingestResult.FormatOutput(opts) + var outputContent string + + if outputFormat == "json" { + jsonBytes, errJSON := ingestResult.FormatJSON(opts) + if errJSON != nil { + fmt.Fprintf(os.Stderr, "Error formatting JSON output: %v\n", errJSON) + os.Exit(1) + } + outputContent = string(jsonBytes) + } else { + ingestResult.FormatOutput(opts) + outputContent = ingestResult.TreeStructure + "\n" + ingestResult.FileContents + } if opts.OutputFile != "" && opts.OutputFile != "-" { outputDir := filepath.Dir(opts.OutputFile) @@ -95,20 +108,21 @@ You can specify a local path or a repository URL as the source.`, } } - fileContentToWrite := ingestResult.TreeStructure + "\n" + ingestResult.FileContents - err = os.WriteFile(opts.OutputFile, []byte(fileContentToWrite), 0644) + err = os.WriteFile(opts.OutputFile, []byte(outputContent), 0644) if err != nil { fmt.Fprintf(os.Stderr, "Error writing to output file %s: %v\n", opts.OutputFile, err) os.Exit(1) } fmt.Fprintf(os.Stderr, "Digest written to: %s\n", opts.OutputFile) } else { - fmt.Println(ingestResult.TreeStructure) - fmt.Println(ingestResult.FileContents) + fmt.Println(outputContent) } - fmt.Fprintln(os.Stderr, "\n--- Summary ---") - fmt.Fprint(os.Stderr, ingestResult.Summary) + if outputFormat != "json" { + ingestResult.FormatOutput(opts) + fmt.Fprintln(os.Stderr, "\n--- Summary ---") + fmt.Fprint(os.Stderr, ingestResult.Summary) + } }, } @@ -142,4 +156,5 @@ func init() { rootCmd.Flags().StringSliceP("exclude-pattern", "e", []string{}, "Comma-separated glob patterns to exclude (adds to defaults)") rootCmd.Flags().StringSliceVarP(&includePatterns, "include-pattern", "i", []string{}, "Comma-separated glob patterns to include (overrides excludes)") rootCmd.Flags().StringVarP(&branch, "branch", "b", "", "Branch to clone and ingest (if source is a Git URL)") + rootCmd.Flags().StringVarP(&outputFormat, "format", "f", "text", "Output format: text or json") } diff --git a/internal/digest/json.go b/internal/digest/json.go new file mode 100644 index 0000000..06cff74 --- /dev/null +++ b/internal/digest/json.go @@ -0,0 +1,140 @@ +package digest + +import ( + "encoding/json" + "path/filepath" +) + +type JSONOutput struct { + Summary JSONSummary `json:"summary"` + Tree []*JSONNode `json:"tree"` + Files []JSONFile `json:"files"` + GitInfo *JSONGitInfo `json:"git_info,omitempty"` +} + +type JSONSummary struct { + Source string `json:"source"` + TotalFiles int `json:"total_files"` + TotalSize int64 `json:"total_size"` + TotalSizeHuman string `json:"total_size_human"` + ExcludePatterns []string `json:"exclude_patterns,omitempty"` + IncludePatterns []string `json:"include_patterns,omitempty"` + MaxFileSize int64 `json:"max_file_size,omitempty"` +} + +type JSONNode struct { + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Size int64 `json:"size,omitempty"` + Children []*JSONNode `json:"children,omitempty"` +} + +type JSONFile struct { + Path string `json:"path"` + Size int64 `json:"size"` + Type string `json:"type"` + Content string `json:"content,omitempty"` +} + +type JSONGitInfo struct { + RepoURL string `json:"repo_url,omitempty"` + Branch string `json:"branch,omitempty"` + Commit string `json:"commit,omitempty"` + User string `json:"user,omitempty"` + RepoName string `json:"repo_name,omitempty"` +} + +func (r *Result) FormatJSON(opts IngestionOptions) ([]byte, error) { + output := JSONOutput{ + Summary: JSONSummary{ + Source: opts.Source, + TotalFiles: r.TotalFiles, + TotalSize: r.TotalSize, + TotalSizeHuman: formatBytes(r.TotalSize), + ExcludePatterns: opts.ExcludePatterns, + IncludePatterns: opts.IncludePatterns, + MaxFileSize: opts.MaxFileSize, + }, + Tree: buildJSONTree(r.RootNode), + Files: gatherJSONFiles(r.RootNode), + } + + if r.GitInfo != nil { + output.GitInfo = &JSONGitInfo{ + RepoURL: r.GitInfo.RepoURL, + Branch: r.GitInfo.Branch, + Commit: r.GitInfo.Commit, + User: r.GitInfo.User, + RepoName: r.GitInfo.RepoName, + } + } + + return json.MarshalIndent(output, "", " ") +} + +func buildJSONTree(node *FileNode) []*JSONNode { + if node == nil { + return nil + } + + if node.Type == NodeTypeDir && node.Children != nil { + result := make([]*JSONNode, 0, len(node.Children)) + for _, child := range node.Children { + result = append(result, fileNodeToJSON(child)) + } + return result + } + + return []*JSONNode{fileNodeToJSON(node)} +} + +func fileNodeToJSON(node *FileNode) *JSONNode { + jn := &JSONNode{ + Name: node.Name, + Path: filepath.ToSlash(node.Path), + Type: string(node.Type), + Size: node.Size, + } + + if node.Type == NodeTypeDir && node.Children != nil { + jn.Children = make([]*JSONNode, 0, len(node.Children)) + for _, child := range node.Children { + jn.Children = append(jn.Children, fileNodeToJSON(child)) + } + } + + return jn +} + +func gatherJSONFiles(node *FileNode) []JSONFile { + var files []JSONFile + gatherJSONFilesRecursive(node, &files) + return files +} + +func gatherJSONFilesRecursive(node *FileNode, files *[]JSONFile) { + if node.Type == NodeTypeFile { + f := JSONFile{ + Path: filepath.ToSlash(node.Path), + Size: node.Size, + Type: string(node.Type), + } + if node.Content != "" { + f.Content = node.Content + } + *files = append(*files, f) + } else if node.Type == NodeTypeNotText || node.Type == NodeTypeTooLarge { + *files = append(*files, JSONFile{ + Path: filepath.ToSlash(node.Path), + Size: node.Size, + Type: string(node.Type), + }) + } + + if node.Type == NodeTypeDir { + for _, child := range node.Children { + gatherJSONFilesRecursive(child, files) + } + } +}