first mvp
This commit is contained in:
+10
-20
@@ -1,23 +1,13 @@
|
|||||||
ignore:
|
config:
|
||||||
- node_modules
|
path_mode: relative
|
||||||
- dist
|
|
||||||
- "*.log"
|
|
||||||
|
|
||||||
include:
|
prompts:
|
||||||
- "*.go"
|
preset: "下面的代码来自文件 {{path}}:\n---\n{{content}}"
|
||||||
- "*.py"
|
|
||||||
- "*.ts"
|
|
||||||
|
|
||||||
max_file_size_kb: 200
|
preset:
|
||||||
|
internal:
|
||||||
|
- "internal/tree/*"
|
||||||
|
- "internal/utils/glob.go"
|
||||||
|
|
||||||
bundle:
|
cmd:
|
||||||
max_tokens: 20000
|
- "cmd/*"
|
||||||
add_file_header: true
|
|
||||||
format: "markdown" # or plain
|
|
||||||
|
|
||||||
clipboard:
|
|
||||||
auto_copy: true
|
|
||||||
|
|
||||||
tree:
|
|
||||||
show_size: true
|
|
||||||
max_depth: 5
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/atotto/clipboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
func copyToClipboard(content string) error {
|
||||||
|
return clipboard.WriteAll(content)
|
||||||
|
}
|
||||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "cliprepo",
|
Use: "cliprepo",
|
||||||
Short: "Extract repo structure and bundle code for LLMs",
|
Short: "LLM context builder from repo",
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
|||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"cliprepo/internal/config"
|
||||||
|
"cliprepo/internal/tree"
|
||||||
|
"cliprepo/internal/utils"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/atotto/clipboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
var delineate string
|
||||||
|
var useClipboard bool
|
||||||
|
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Generate LLM context from repo",
|
||||||
|
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. tree
|
||||||
|
treeStr, err := tree.Generate(".")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("===== PROJECT TREE =====\n")
|
||||||
|
builder.WriteString(treeStr)
|
||||||
|
builder.WriteString("\n\n")
|
||||||
|
|
||||||
|
// 2. preset files
|
||||||
|
rawFiles, ok := cfg.Preset[delineate]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("preset not found: %s", delineate)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := utils.ExpandGlob(rawFiles)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. template
|
||||||
|
template := cfg.Prompts.Preset
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
|
||||||
|
content, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path := file
|
||||||
|
if cfg.Config.PathMode == "absolute" {
|
||||||
|
abs, _ := filepath.Abs(file)
|
||||||
|
path = abs
|
||||||
|
}
|
||||||
|
|
||||||
|
block := strings.ReplaceAll(template, "{{path}}", path)
|
||||||
|
block = strings.ReplaceAll(block, "{{content}}", string(content))
|
||||||
|
|
||||||
|
builder.WriteString(block)
|
||||||
|
builder.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := builder.String()
|
||||||
|
|
||||||
|
if useClipboard {
|
||||||
|
return clipboard.WriteAll(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(runCmd)
|
||||||
|
|
||||||
|
runCmd.Flags().StringVar(&delineate, "delineate", "", "preset key")
|
||||||
|
runCmd.Flags().BoolVar(&useClipboard, "clipboard", false, "copy output to clipboard")
|
||||||
|
}
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package bundle
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"cliprepo/internal/config"
|
|
||||||
"github.com/atotto/clipboard"
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxFileSize = 1 * 1024 * 1024 // 1MB
|
|
||||||
|
|
||||||
// Build 根据 config 中定义的 bundle key,将多个文件内容聚合成一个字符串
|
|
||||||
//
|
|
||||||
// 设计用途:
|
|
||||||
// 用于 CLI 工具中快速“打包”一组文件内容,例如:
|
|
||||||
// cliprepo bundle api
|
|
||||||
// => 输出 api 相关的一组代码文件内容,方便复制/分享/提交 prompt
|
|
||||||
//
|
|
||||||
// 行为特点:
|
|
||||||
// 1. 从 cliprepo.yaml 中读取 bundles 配置
|
|
||||||
// 2. 根据 key 找到对应文件列表
|
|
||||||
// 3. 逐个读取文件内容并拼接
|
|
||||||
// 4. 自动跳过异常文件(不存在 / 读取失败)
|
|
||||||
// 5. 自动过滤过大的文件(> 1MB)
|
|
||||||
//
|
|
||||||
// 输出格式:
|
|
||||||
//
|
|
||||||
// ===== path/to/file =====
|
|
||||||
// <file content>
|
|
||||||
//
|
|
||||||
// ===== path/to/another =====
|
|
||||||
// <file content>
|
|
||||||
func Build(key string) (string, error) {
|
|
||||||
// 读取配置文件(cliprepo.yaml)
|
|
||||||
cfg, err := config.Load()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取对应 bundle 的文件列表
|
|
||||||
files, ok := cfg.Bundles[key]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("bundle not found: %s", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
|
|
||||||
// 遍历 bundle 中的所有文件路径
|
|
||||||
for _, file := range files {
|
|
||||||
// 获取文件信息(用于判断大小 / 是否存在)
|
|
||||||
info, err := os.Stat(file)
|
|
||||||
if err != nil {
|
|
||||||
// 文件不存在或无法访问时跳过
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跳过过大的文件,避免 CLI 输出过载或内存占用过高
|
|
||||||
if info.Size() > maxFileSize {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取文件内容
|
|
||||||
data, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
// 读取失败直接跳过,不中断整个 bundle 构建
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入文件分隔标记,便于阅读结构
|
|
||||||
builder.WriteString(fmt.Sprintf("===== %s =====\n", file))
|
|
||||||
|
|
||||||
// 写入文件内容
|
|
||||||
builder.Write(data)
|
|
||||||
builder.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy 将 bundle 内容写入系统剪贴板
|
|
||||||
//
|
|
||||||
// 常见使用场景:
|
|
||||||
// CLI 一键复制 bundle 输出,用于:
|
|
||||||
// - prompt 输入
|
|
||||||
// - 代码审查
|
|
||||||
// - 远程分享
|
|
||||||
// - AI 上下文输入
|
|
||||||
func Copy(content string) error {
|
|
||||||
return clipboard.WriteAll(content)
|
|
||||||
}
|
|
||||||
+10
-18
@@ -6,33 +6,25 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config 定义 cliprepo 的配置结构
|
|
||||||
//
|
|
||||||
// 当前支持的配置:
|
|
||||||
// Bundles:用于定义一组文件路径集合
|
|
||||||
// key -> bundle 名称
|
|
||||||
// value -> 对应的文件路径列表
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Bundles map[string][]string `yaml:"bundles"`
|
Config struct {
|
||||||
|
PathMode string `yaml:"path_mode"`
|
||||||
|
} `yaml:"config"`
|
||||||
|
|
||||||
|
Prompts struct {
|
||||||
|
Preset string `yaml:"preset"`
|
||||||
|
} `yaml:"prompts"`
|
||||||
|
|
||||||
|
Preset map[string][]string `yaml:"preset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 从项目根目录读取 cliprepo.yaml 配置文件,并解析为 Config 结构
|
|
||||||
//
|
|
||||||
// 行为说明:
|
|
||||||
// 1. 默认读取当前工作目录下的 cliprepo.yaml
|
|
||||||
// 2. 如果文件不存在或读取失败,直接返回 error
|
|
||||||
// 3. 使用 YAML 解析配置内容到 Config 结构体
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
// 读取配置文件内容
|
data, err := os.ReadFile(".cliprepo.yaml")
|
||||||
data, err := os.ReadFile("cliprepo.yaml")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg Config
|
var cfg Config
|
||||||
|
|
||||||
// 解析 YAML 到结构体
|
|
||||||
err = yaml.Unmarshal(data, &cfg)
|
err = yaml.Unmarshal(data, &cfg)
|
||||||
|
|
||||||
return &cfg, err
|
return &cfg, err
|
||||||
}
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package gitignore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
ignore "github.com/sabhiram/go-gitignore"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Load 加载指定 root 目录下的 .gitignore 规则
|
|
||||||
//
|
|
||||||
// 行为说明:
|
|
||||||
// 1. 如果 root/.gitignore 存在,则解析该文件
|
|
||||||
// 2. 如果不存在,则返回一个“空规则集”(不会忽略任何文件)
|
|
||||||
// 3. 返回 go-gitignore 的 GitIgnore 对象,用于路径匹配
|
|
||||||
func Load(root string) (*ignore.GitIgnore, error) {
|
|
||||||
// 拼接 .gitignore 文件路径
|
|
||||||
path := filepath.Join(root, ".gitignore")
|
|
||||||
|
|
||||||
// 如果 .gitignore 不存在,则返回一个空的 ignore 规则
|
|
||||||
// 这样后续 MatchesPath 不会 panic,也不会过滤任何文件
|
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
||||||
return ignore.CompileIgnoreLines(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果存在 .gitignore,则解析文件内容生成规则
|
|
||||||
return ignore.CompileIgnoreFile(path)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package tree
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
ignore "github.com/sabhiram/go-gitignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load 加载 .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)
|
||||||
|
}
|
||||||
+4
-37
@@ -5,73 +5,40 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"cliprepo/internal/gitignore"
|
|
||||||
"github.com/atotto/clipboard"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate 从 root 目录开始遍历文件树,并生成类似 `tree` 命令的目录结构字符串
|
|
||||||
//
|
|
||||||
// 功能特点:
|
|
||||||
// 1. 递归遍历目录
|
|
||||||
// 2. 支持 .gitignore 规则过滤
|
|
||||||
// 3. 输出树形结构字符串
|
|
||||||
func Generate(root string) (string, error) {
|
func Generate(root string) (string, error) {
|
||||||
// 加载 gitignore 规则(如果失败则忽略错误,默认不过滤)
|
|
||||||
ig, _ := gitignore.Load(root)
|
ig, _ := Load(root)
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
|
|
||||||
// 递归遍历目录结构
|
|
||||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根目录特殊处理,统一显示为 "."
|
|
||||||
if path == root {
|
if path == root {
|
||||||
builder.WriteString(".\n")
|
builder.WriteString(".\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为相对路径,便于匹配 gitignore 和计算层级
|
|
||||||
rel, _ := filepath.Rel(root, path)
|
rel, _ := filepath.Rel(root, path)
|
||||||
|
|
||||||
// 如果路径匹配 gitignore 规则,则跳过
|
// gitignore filter
|
||||||
// 如果是目录,则直接跳过整个子树
|
if ig != nil && ig.MatchesPath(rel) {
|
||||||
if ig.MatchesPath(rel) {
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算当前节点在目录树中的层级
|
|
||||||
// 通过路径分隔符数量判断深度
|
|
||||||
level := strings.Count(rel, string(os.PathSeparator))
|
level := strings.Count(rel, string(os.PathSeparator))
|
||||||
|
|
||||||
// 根据层级生成缩进
|
|
||||||
prefix := strings.Repeat("│ ", level)
|
prefix := strings.Repeat("│ ", level)
|
||||||
|
|
||||||
// 写入当前节点(文件或目录)
|
|
||||||
builder.WriteString(fmt.Sprintf("%s├── %s\n", prefix, info.Name()))
|
builder.WriteString(fmt.Sprintf("%s├── %s\n", prefix, info.Name()))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return builder.String(), err
|
return builder.String(), err
|
||||||
}
|
|
||||||
|
|
||||||
// Copy 将生成的字符串写入系统剪贴板
|
|
||||||
//
|
|
||||||
// 常用于 CLI 工具:直接复制 tree 输出结果
|
|
||||||
func Copy(content string) error {
|
|
||||||
return clipboard.WriteAll(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write 将内容写入指定文件路径
|
|
||||||
//
|
|
||||||
// 如果文件存在则覆盖
|
|
||||||
// 权限默认设置为 0644(rw-r--r--)
|
|
||||||
func Write(path, content string) error {
|
|
||||||
return os.WriteFile(path, []byte(content), 0644)
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExpandPaths(patterns []string) ([]string, error) {
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range matches {
|
||||||
|
info, err := os.Stat(m)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExpandGlob(patterns []string) ([]string, error) {
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
for _, p := range patterns {
|
||||||
|
matches, err := filepath.Glob(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range matches {
|
||||||
|
info, err := os.Stat(m)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user