From 52d6dde4aa60eec9608270df55f8f285f89219df Mon Sep 17 00:00:00 2001 From: Zayden Jung Date: Fri, 15 May 2026 23:36:12 +0800 Subject: [PATCH] first commit --- cmd/bundle.go | 36 ++++++++++++++++++++++++ cmd/root.go | 18 ++++++++++++ cmd/tree.go | 40 +++++++++++++++++++++++++++ go.mod | 15 ++++++++++ go.sum | 25 +++++++++++++++++ internal/bundle/bundle.go | 52 +++++++++++++++++++++++++++++++++++ internal/config/config.go | 22 +++++++++++++++ internal/gitignore/ignore.go | 18 ++++++++++++ internal/tree/tree.go | 53 ++++++++++++++++++++++++++++++++++++ main.go | 7 +++++ 10 files changed, 286 insertions(+) create mode 100644 cmd/bundle.go create mode 100644 cmd/root.go create mode 100644 cmd/tree.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/bundle/bundle.go create mode 100644 internal/config/config.go create mode 100644 internal/gitignore/ignore.go create mode 100644 internal/tree/tree.go create mode 100644 main.go diff --git a/cmd/bundle.go b/cmd/bundle.go new file mode 100644 index 0000000..b3b3896 --- /dev/null +++ b/cmd/bundle.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "fmt" + + "cliprepo/internal/bundle" + "github.com/spf13/cobra" +) + +var bundleClipboard bool + +var bundleCmd = &cobra.Command{ + Use: "bundle [key]", + Short: "Bundle files by key from config", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + key := args[0] + + result, err := bundle.Build(key) + if err != nil { + return err + } + + if bundleClipboard { + return bundle.Copy(result) + } + + fmt.Println(result) + return nil + }, +} + +func init() { + rootCmd.AddCommand(bundleCmd) + bundleCmd.Flags().BoolVar(&bundleClipboard, "clipboard", false, "Copy to clipboard") +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..d607f9c --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "cliprepo", + Short: "Extract repo structure and bundle code for LLMs", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} \ No newline at end of file diff --git a/cmd/tree.go b/cmd/tree.go new file mode 100644 index 0000000..e793a66 --- /dev/null +++ b/cmd/tree.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "fmt" + + "cliprepo/internal/tree" + "github.com/spf13/cobra" +) + +var treeOutput string +var treeClipboard bool + +var treeCmd = &cobra.Command{ + Use: "tree", + Short: "Print repo tree respecting .gitignore", + RunE: func(cmd *cobra.Command, args []string) error { + result, err := tree.Generate(".") + if err != nil { + return err + } + + if treeClipboard { + return tree.Copy(result) + } + + if treeOutput != "" { + return tree.Write(treeOutput, result) + } + + fmt.Println(result) + return nil + }, +} + +func init() { + rootCmd.AddCommand(treeCmd) + + treeCmd.Flags().StringVarP(&treeOutput, "output", "o", "", "Output file") + treeCmd.Flags().BoolVar(&treeClipboard, "clipboard", false, "Copy to clipboard") +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..129483a --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module cliprepo + +go 1.26.3 + +require ( + github.com/atotto/clipboard v0.1.4 + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 + github.com/spf13/cobra v1.10.2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..61a41f0 --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bundle/bundle.go b/internal/bundle/bundle.go new file mode 100644 index 0000000..59f43d6 --- /dev/null +++ b/internal/bundle/bundle.go @@ -0,0 +1,52 @@ +package bundle + +import ( + "fmt" + "os" + "strings" + + "cliprepo/internal/config" + "github.com/atotto/clipboard" +) + +const maxFileSize = 1 * 1024 * 1024 // 1MB + +func Build(key string) (string, error) { + cfg, err := config.Load() + if err != nil { + return "", err + } + + files, ok := cfg.Bundles[key] + if !ok { + return "", fmt.Errorf("bundle not found: %s", key) + } + + var builder strings.Builder + + for _, file := range files { + info, err := os.Stat(file) + if err != nil { + continue + } + + if info.Size() > maxFileSize { + continue + } + + data, err := os.ReadFile(file) + if err != nil { + continue + } + + builder.WriteString(fmt.Sprintf("===== %s =====\n", file)) + builder.Write(data) + builder.WriteString("\n\n") + } + + return builder.String(), nil +} + +func Copy(content string) error { + return clipboard.WriteAll(content) +} \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..bf70fa4 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Bundles map[string][]string `yaml:"bundles"` +} + +func Load() (*Config, error) { + data, err := os.ReadFile("cliprepo.yaml") + if err != nil { + return nil, err + } + + var cfg Config + err = yaml.Unmarshal(data, &cfg) + return &cfg, err +} \ No newline at end of file diff --git a/internal/gitignore/ignore.go b/internal/gitignore/ignore.go new file mode 100644 index 0000000..25e0313 --- /dev/null +++ b/internal/gitignore/ignore.go @@ -0,0 +1,18 @@ +package gitignore + +import ( + "os" + "path/filepath" + + ignore "github.com/sabhiram/go-gitignore" +) + +func Load(root string) (*ignore.GitIgnore, error) { + path := filepath.Join(root, ".gitignore") + + if _, err := os.Stat(path); os.IsNotExist(err) { + return ignore.CompileIgnoreLines(), nil + } + + return ignore.CompileIgnoreFile(path) +} \ No newline at end of file diff --git a/internal/tree/tree.go b/internal/tree/tree.go new file mode 100644 index 0000000..fe2adc3 --- /dev/null +++ b/internal/tree/tree.go @@ -0,0 +1,53 @@ +package tree + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "cliprepo/internal/gitignore" + "github.com/atotto/clipboard" +) + +func Generate(root string) (string, error) { + ig, _ := gitignore.Load(root) + + var builder strings.Builder + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if path == root { + builder.WriteString(".\n") + return nil + } + + rel, _ := filepath.Rel(root, path) + + if ig.MatchesPath(rel) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + + level := strings.Count(rel, string(os.PathSeparator)) + prefix := strings.Repeat("│ ", level) + + builder.WriteString(fmt.Sprintf("%sā”œā”€ā”€ %s\n", prefix, info.Name())) + return nil + }) + + return builder.String(), err +} + +func Copy(content string) error { + return clipboard.WriteAll(content) +} + +func Write(path, content string) error { + return os.WriteFile(path, []byte(content), 0644) +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..6cef226 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "cliprepo/cmd" + +func main() { + cmd.Execute() +} \ No newline at end of file