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