| 1 | package capdiag |
| 2 | |
| 3 | import ( |
| 4 | "net/url" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | ) |
| 9 | |
| 10 | // displayPath rewrites absolute paths for safe reports: |
| 11 | // - under workspace → <workspace>/... |
| 12 | // - under Reasonix home → <reasonix-home>/... |
| 13 | // - under OS home → ~/... |
| 14 | // - elsewhere → <external>/basename (no full external path, no username) |
| 15 | func displayPath(p, workspace, home, reasonixHome string) string { |
| 16 | p = strings.TrimSpace(p) |
| 17 | if p == "" { |
| 18 | return "" |
| 19 | } |
| 20 | // Builtin markers stay as-is. |
| 21 | if strings.HasPrefix(p, "(builtin") { |
| 22 | return p |
| 23 | } |
| 24 | clean := filepath.Clean(p) |
| 25 | if abs, err := filepath.Abs(clean); err == nil { |
| 26 | clean = abs |
| 27 | } |
| 28 | ws := workspace |
| 29 | if ws != "" { |
| 30 | if abs, err := filepath.Abs(ws); err == nil { |
| 31 | ws = abs |
| 32 | } |
| 33 | if clean == ws { |
| 34 | return "<workspace>" |
| 35 | } |
| 36 | if rel, err := filepath.Rel(ws, clean); err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { |
| 37 | return "<workspace>/" + filepath.ToSlash(rel) |
| 38 | } |
| 39 | } |
| 40 | if rh := strings.TrimSpace(reasonixHome); rh != "" { |
| 41 | if abs, err := filepath.Abs(rh); err == nil { |
| 42 | rh = abs |
| 43 | } |
| 44 | rh = filepath.Clean(rh) |
| 45 | if clean == rh { |
| 46 | return "<reasonix-home>" |
| 47 | } |
| 48 | if rel, err := filepath.Rel(rh, clean); err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { |
| 49 | return "<reasonix-home>/" + filepath.ToSlash(rel) |
| 50 | } |
| 51 | } |
| 52 | if home == "" { |
| 53 | if h, err := os.UserHomeDir(); err == nil { |
| 54 | home = h |
| 55 | } |
| 56 | } |
| 57 | if home != "" { |
| 58 | home = filepath.Clean(home) |
| 59 | if clean == home { |
| 60 | return "~" |
| 61 | } |
| 62 | if rel, err := filepath.Rel(home, clean); err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { |
| 63 | return "~/" + filepath.ToSlash(rel) |
| 64 | } |
| 65 | } |
| 66 | // External: only show a generic marker + base name so reports stay shareable. |
| 67 | base := filepath.Base(clean) |
| 68 | if base == "" || base == "." || base == string(os.PathSeparator) { |
| 69 | return "<external>" |
| 70 | } |
| 71 | return "<external>/" + base |
| 72 | } |
| 73 | |
| 74 | func sortedKeys(m map[string]string) []string { |
| 75 | if len(m) == 0 { |
| 76 | return nil |
| 77 | } |
| 78 | keys := make([]string, 0, len(m)) |
| 79 | for k := range m { |
| 80 | if strings.TrimSpace(k) != "" { |
| 81 | keys = append(keys, k) |
| 82 | } |
| 83 | } |
| 84 | // Insertion sort for stability without importing sort in hot path of small maps. |
| 85 | for i := 1; i < len(keys); i++ { |
| 86 | j := i |
| 87 | for j > 0 && keys[j-1] > keys[j] { |
| 88 | keys[j-1], keys[j] = keys[j], keys[j-1] |
| 89 | j-- |
| 90 | } |
| 91 | } |
| 92 | return keys |
| 93 | } |
| 94 | |
| 95 | // urlHostOnly returns the host (and port) from a URL without path/query/userinfo. |
| 96 | func urlHostOnly(raw string) string { |
| 97 | raw = strings.TrimSpace(raw) |
| 98 | if raw == "" { |
| 99 | return "" |
| 100 | } |
| 101 | u, err := url.Parse(raw) |
| 102 | if err != nil || u.Host == "" { |
| 103 | return "<url>" |
| 104 | } |
| 105 | return u.Host |
| 106 | } |
| 107 | |
| 108 | func transportOf(typ string) string { |
| 109 | t := strings.ToLower(strings.TrimSpace(typ)) |
| 110 | switch t { |
| 111 | case "", "stdio": |
| 112 | return "stdio" |
| 113 | case "http", "streamable-http", "streamable_http": |
| 114 | return "http" |
| 115 | case "sse": |
| 116 | return "sse" |
| 117 | default: |
| 118 | return t |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func isValidTransport(t string) bool { |
| 123 | switch transportOf(t) { |
| 124 | case "stdio", "http", "sse": |
| 125 | return true |
| 126 | default: |
| 127 | return false |
| 128 | } |
| 129 | } |
| 130 |