返回 DeepSeek-Reasonix
inspect.go
根目录 / internal / command / inspect.go
1 package command
2
3 import (
4 "os"
5 "path/filepath"
6 "slices"
7 "sort"
8 "strings"
9 )
10
11 // CandidateStatus is the diagnostic disposition of one command file.
12 type CandidateStatus string
13
14 const (
15 CandidateWinner CandidateStatus = "winner"
16 CandidateShadowed CandidateStatus = "shadowed"
17 CandidateError CandidateStatus = "error"
18 )
19
20 // Candidate is one command file considered during Load, including shadowed
21 // sources that later directories override.
22 type Candidate struct {
23 Name string
24 Description string
25 Path string
26 Root string
27 Status CandidateStatus
28 WinnerPath string
29 Error string
30 }
31
32 // RootInfo is one scanned commands directory.
33 type RootInfo struct {
34 Dir string
35 Status string // ok | missing | not-directory | unreadable
36 }
37
38 // Inspection is a read-only snapshot matching command.Load override semantics
39 // (later dir wins) without changing Load itself.
40 type Inspection struct {
41 Roots []RootInfo
42 Candidates []Candidate
43 Winners []Command
44 }
45
46 // Inspect walks dirs in order (same as Load) and records every candidate.
47 // Missing dirs are listed as missing and produce no warnings.
48 func Inspect(dirs ...string) Inspection {
49 var roots []RootInfo
50 // Per-name list of candidates in scan order; last becomes winner.
51 byName := map[string][]Candidate{}
52 winners := map[string]Command{}
53
54 for _, dir := range dirs {
55 root, err := filepath.Abs(dir)
56 if err != nil {
57 roots = append(roots, RootInfo{Dir: dir, Status: "unreadable"})
58 continue
59 }
60 st := rootStatus(root)
61 roots = append(roots, RootInfo{Dir: root, Status: st})
62 if st != "ok" {
63 continue
64 }
65 visited := map[string]bool{}
66 if real, err := filepath.EvalSymlinks(root); err == nil {
67 visited[real] = true
68 } else {
69 visited[root] = true
70 }
71 walkCommands(root, root, visited, func(path string) {
72 c, perr := parseFile(root, path)
73 if perr != nil {
74 name := guessName(root, path)
75 byName[name] = append(byName[name], Candidate{
76 Name: name, Path: path, Root: root,
77 Status: CandidateError, Error: perr.Error(),
78 })
79 return
80 }
81 byName[c.Name] = append(byName[c.Name], Candidate{
82 Name: c.Name, Description: c.Description, Path: path, Root: root,
83 Status: CandidateWinner, // provisional; finalized below
84 })
85 winners[c.Name] = c
86 })
87 }
88
89 var candidates []Candidate
90 for name, list := range byName {
91 // Find last non-error candidate as winner; errors stay as errors.
92 winIdx := -1
93 for i, v := range slices.Backward(list) {
94 if v.Status != CandidateError {
95 winIdx = i
96 break
97 }
98 }
99 for i, c := range list {
100 if c.Status == CandidateError {
101 candidates = append(candidates, c)
102 continue
103 }
104 if i == winIdx {
105 c.Status = CandidateWinner
106 c.WinnerPath = ""
107 } else if winIdx >= 0 {
108 c.Status = CandidateShadowed
109 c.WinnerPath = list[winIdx].Path
110 }
111 candidates = append(candidates, c)
112 }
113 _ = name
114 }
115
116 cmds := make([]Command, 0, len(winners))
117 for _, c := range winners {
118 cmds = append(cmds, c)
119 }
120 sort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })
121 sort.SliceStable(candidates, func(i, j int) bool {
122 if candidates[i].Name != candidates[j].Name {
123 return candidates[i].Name < candidates[j].Name
124 }
125 order := map[CandidateStatus]int{CandidateWinner: 0, CandidateShadowed: 1, CandidateError: 2}
126 return order[candidates[i].Status] < order[candidates[j].Status]
127 })
128
129 return Inspection{Roots: roots, Candidates: candidates, Winners: cmds}
130 }
131
132 func rootStatus(dir string) string {
133 info, err := os.Stat(dir)
134 if err != nil {
135 if os.IsNotExist(err) {
136 return "missing"
137 }
138 return "unreadable"
139 }
140 if !info.IsDir() {
141 return "not-directory"
142 }
143 f, err := os.Open(dir)
144 if err != nil {
145 return "unreadable"
146 }
147 _ = f.Close()
148 return "ok"
149 }
150
151 func guessName(root, path string) string {
152 rel, err := filepath.Rel(root, path)
153 if err != nil {
154 rel = filepath.Base(path)
155 }
156 return strings.ReplaceAll(strings.TrimSuffix(filepath.ToSlash(rel), ".md"), "/", ":")
157 }
158
158 lines GO