| package bundle |
|
|
| import ( |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "strings" |
|
|
| "huggingface.co/turenlabs/Vigil/source/pkg/parser" |
| ) |
|
|
| const ( |
| |
| |
| |
| |
| maxSniffBytes = 1048576 |
|
|
| |
| |
| |
| maxBundleFiles = 4096 |
| ) |
|
|
| |
| const skillMdName = "skill.md" |
|
|
| |
| |
| |
| |
| func IsBundlePath(path string) bool { |
| info, err := os.Stat(path) |
| if err != nil { |
| return false |
| } |
| if info.IsDir() { |
| return true |
| } |
| return strings.EqualFold(filepath.Base(path), skillMdName) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func ScanPath(path string) (*Bundle, error) { |
| info, err := os.Stat(path) |
| if err != nil { |
| return nil, fmt.Errorf("stat %s: %w", path, err) |
| } |
|
|
| if info.IsDir() { |
| return Scan(path) |
| } |
|
|
| if strings.EqualFold(filepath.Base(path), skillMdName) { |
| return Scan(filepath.Dir(path)) |
| } |
|
|
| |
| return scanSingleFile(path) |
| } |
|
|
| |
| |
| func scanSingleFile(path string) (*Bundle, error) { |
| abs, err := filepath.Abs(path) |
| if err != nil { |
| abs = path |
| } |
| b := &Bundle{ |
| Dir: filepath.Dir(abs), |
| SkillMdPath: abs, |
| Files: nil, |
| } |
| skill, perr := parser.Parse(path) |
| if perr != nil { |
| b.ParseErr = perr |
| } else { |
| b.Skill = skill |
| b.AllowedTools = parseAllowedTools(skill.RawContent) |
| } |
| if info, serr := os.Stat(path); serr == nil { |
| b.SkillMdBytes = info.Size() |
| } |
| return b, nil |
| } |
|
|
| |
| |
| |
| |
| |
| func Scan(dir string) (*Bundle, error) { |
| absDir, err := filepath.Abs(dir) |
| if err != nil { |
| absDir = dir |
| } |
|
|
| b := &Bundle{Dir: absDir} |
|
|
| |
| skillMdAbs := findSkillMd(absDir) |
| b.SkillMdPath = skillMdAbs |
|
|
| visited := make(map[uint64]bool) |
| fileCount := 0 |
|
|
| walkErr := filepath.WalkDir(absDir, func(path string, d os.DirEntry, walkErr error) error { |
| if walkErr != nil { |
| |
| b.Notes = append(b.Notes, fmt.Sprintf("walk error at %s: %v", relOrPath(absDir, path), walkErr)) |
| if d != nil && d.IsDir() { |
| return filepath.SkipDir |
| } |
| return nil |
| } |
|
|
| if path == absDir { |
| return nil |
| } |
|
|
| if fileCount >= maxBundleFiles { |
| b.Notes = append(b.Notes, fmt.Sprintf("file cap %d reached; remaining entries skipped", maxBundleFiles)) |
| return filepath.SkipAll |
| } |
|
|
| rel := relOrPath(absDir, path) |
|
|
| if d.IsDir() { |
| |
| |
| return nil |
| } |
|
|
| |
| if skillMdAbs != "" && sameAbs(path, skillMdAbs) { |
| return nil |
| } |
|
|
| f := &File{ |
| RelPath: rel, |
| AbsPath: path, |
| Hidden: pathHasHiddenSegment(rel), |
| } |
|
|
| |
| |
| |
| isSymlink := d.Type()&os.ModeSymlink != 0 |
| f.IsSymlink = isSymlink |
|
|
| |
| |
| |
| readPath := path |
|
|
| if isSymlink { |
| target, escapes, isLoop := resolveSymlink(path, absDir) |
| f.SymlinkTarget = target |
| f.SymlinkEscapes = escapes |
| if isLoop { |
| b.Notes = append(b.Notes, fmt.Sprintf("symlink loop broken at %s", rel)) |
| |
| |
| b.Files = append(b.Files, f) |
| fileCount++ |
| return nil |
| } |
| if target != "" { |
| readPath = target |
| } |
| |
| |
| |
| if ino, ok := inodeOf(readPath); ok { |
| if visited[ino] { |
| b.Notes = append(b.Notes, fmt.Sprintf("symlink target already visited at %s", rel)) |
| } else { |
| visited[ino] = true |
| } |
| } |
| } else if ino, ok := inodeOf(path); ok { |
| visited[ino] = true |
| } |
|
|
| |
| |
| if info, serr := os.Stat(readPath); serr == nil { |
| f.SizeBytes = info.Size() |
| } |
|
|
| sniff, _, truncated, newlineRatio, rerr := readSniff(readPath) |
| if rerr != nil { |
| b.Notes = append(b.Notes, fmt.Sprintf("read error at %s: %v", rel, rerr)) |
| } |
| f.Sniff = sniff |
| f.Truncated = truncated |
| f.NewlineRatio = newlineRatio |
|
|
| f.Kind = classifyKind(filepath.Base(path), sniff) |
| f.ScriptLang = scriptLangFor(filepath.Base(path), sniff) |
|
|
| b.Files = append(b.Files, f) |
| fileCount++ |
| b.PayloadBytes += f.SizeBytes |
| return nil |
| }) |
| if walkErr != nil { |
| b.Notes = append(b.Notes, fmt.Sprintf("walk terminated: %v", walkErr)) |
| } |
|
|
| |
| if skillMdAbs != "" { |
| skill, perr := parser.Parse(skillMdAbs) |
| if perr != nil { |
| b.ParseErr = perr |
| } else { |
| b.Skill = skill |
| b.AllowedTools = parseAllowedTools(skill.RawContent) |
| } |
| if info, serr := os.Stat(skillMdAbs); serr == nil { |
| b.SkillMdBytes = info.Size() |
| } |
| } else { |
| b.Notes = append(b.Notes, "no SKILL.md found in bundle directory") |
| } |
|
|
| resolveReferences(b) |
| return b, nil |
| } |
|
|
| |
| |
| |
| |
| func findSkillMd(dir string) string { |
| entries, err := os.ReadDir(dir) |
| if err != nil { |
| return "" |
| } |
| for _, e := range entries { |
| if e.IsDir() { |
| continue |
| } |
| if strings.EqualFold(e.Name(), skillMdName) { |
| return filepath.Join(dir, e.Name()) |
| } |
| } |
| return "" |
| } |
|
|
| |
| |
| |
| |
| |
| func readSniff(absPath string) (data []byte, size int64, truncated bool, newlineRatio float64, err error) { |
| f, oerr := os.Open(absPath) |
| if oerr != nil { |
| return nil, 0, false, 0, oerr |
| } |
| defer func() { _ = f.Close() }() |
|
|
| if info, serr := f.Stat(); serr == nil { |
| size = info.Size() |
| } |
|
|
| |
| |
| buf := make([]byte, maxSniffBytes+1) |
| n, rerr := io.ReadFull(f, buf) |
| if rerr != nil && rerr != io.EOF && rerr != io.ErrUnexpectedEOF { |
| return nil, size, false, 0, rerr |
| } |
|
|
| if n > maxSniffBytes { |
| truncated = true |
| n = maxSniffBytes |
| } |
| data = buf[:n] |
|
|
| if n > 0 { |
| newlines := 0 |
| for _, b := range data { |
| if b == '\n' { |
| newlines++ |
| } |
| } |
| newlineRatio = float64(newlines) / float64(n) |
| } |
|
|
| return data, size, truncated, newlineRatio, nil |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func resolveSymlink(absPath, bundleRoot string) (target string, escapes bool, isLoop bool) { |
| resolved, err := filepath.EvalSymlinks(absPath) |
| if err != nil { |
| |
| |
| |
| if link, lerr := os.Readlink(absPath); lerr == nil { |
| t := link |
| if !filepath.IsAbs(t) { |
| t = filepath.Join(filepath.Dir(absPath), t) |
| } |
| return filepath.Clean(t), !withinRoot(filepath.Clean(t), bundleRoot), true |
| } |
| return "", false, true |
| } |
| return resolved, !withinRoot(resolved, bundleRoot), false |
| } |
|
|
| |
| func withinRoot(target, root string) bool { |
| rel, err := filepath.Rel(root, target) |
| if err != nil { |
| return false |
| } |
| return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) |
| } |
|
|
| |
| |
| func relOrPath(base, path string) string { |
| if rel, err := filepath.Rel(base, path); err == nil { |
| return rel |
| } |
| return path |
| } |
|
|
| |
| func sameAbs(a, b string) bool { |
| ca, _ := filepath.Abs(a) |
| cb, _ := filepath.Abs(b) |
| return filepath.Clean(ca) == filepath.Clean(cb) |
| } |
|
|