|
| 1 | +// apmyml.go provides a minimal apm.yml parser for the Go CLI rewrite. |
| 2 | +// Only the fields needed for read-only CLI commands are parsed. |
| 3 | +package main |
| 4 | + |
| 5 | +import ( |
| 6 | + "bufio" |
| 7 | + "fmt" |
| 8 | + "os" |
| 9 | + "path/filepath" |
| 10 | + "strings" |
| 11 | +) |
| 12 | + |
| 13 | +// ApmProject holds the parsed apm.yml structure. |
| 14 | +type ApmProject struct { |
| 15 | + Name string |
| 16 | + Version string |
| 17 | + Description string |
| 18 | + Author string |
| 19 | + Targets []string |
| 20 | + Scripts map[string]string |
| 21 | + Deps []ApmDep |
| 22 | + MCPDeps []ApmDep |
| 23 | + Marketplaces []ApmMarketplace |
| 24 | +} |
| 25 | + |
| 26 | +// ApmDep is a single dependency entry (owner/repo or owner/repo@ref). |
| 27 | +type ApmDep struct { |
| 28 | + Package string |
| 29 | + Ref string |
| 30 | +} |
| 31 | + |
| 32 | +// ApmMarketplace is a registered marketplace source. |
| 33 | +type ApmMarketplace struct { |
| 34 | + Name string |
| 35 | + URL string |
| 36 | +} |
| 37 | + |
| 38 | +// findApmYML walks up from dir looking for apm.yml. |
| 39 | +func findApmYML(dir string) (string, error) { |
| 40 | + current := dir |
| 41 | + for { |
| 42 | + candidate := filepath.Join(current, "apm.yml") |
| 43 | + if _, err := os.Stat(candidate); err == nil { |
| 44 | + return candidate, nil |
| 45 | + } |
| 46 | + parent := filepath.Dir(current) |
| 47 | + if parent == current { |
| 48 | + break |
| 49 | + } |
| 50 | + current = parent |
| 51 | + } |
| 52 | + return "", fmt.Errorf("apm.yml not found (searched from %s)", dir) |
| 53 | +} |
| 54 | + |
| 55 | +// parseApmYML does a line-by-line best-effort parse of apm.yml. |
| 56 | +// It handles simple YAML scalars and list entries only. |
| 57 | +func parseApmYML(path string) (*ApmProject, error) { |
| 58 | + f, err := os.Open(path) |
| 59 | + if err != nil { |
| 60 | + return nil, err |
| 61 | + } |
| 62 | + defer f.Close() |
| 63 | + |
| 64 | + p := &ApmProject{Scripts: map[string]string{}} |
| 65 | + scanner := bufio.NewScanner(f) |
| 66 | + |
| 67 | + var section string |
| 68 | + var depSection string // "apm" or "mcp" |
| 69 | + |
| 70 | + for scanner.Scan() { |
| 71 | + line := scanner.Text() |
| 72 | + trimmed := strings.TrimSpace(line) |
| 73 | + if trimmed == "" || strings.HasPrefix(trimmed, "#") { |
| 74 | + continue |
| 75 | + } |
| 76 | + |
| 77 | + // Top-level key detection. |
| 78 | + if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, "-") { |
| 79 | + if idx := strings.Index(trimmed, ":"); idx >= 0 { |
| 80 | + key := strings.TrimSpace(trimmed[:idx]) |
| 81 | + val := strings.TrimSpace(trimmed[idx+1:]) |
| 82 | + switch key { |
| 83 | + case "name": |
| 84 | + p.Name = unquote(val) |
| 85 | + section = "" |
| 86 | + case "version": |
| 87 | + p.Version = unquote(val) |
| 88 | + section = "" |
| 89 | + case "description": |
| 90 | + p.Description = unquote(val) |
| 91 | + section = "" |
| 92 | + case "author": |
| 93 | + p.Author = unquote(val) |
| 94 | + section = "" |
| 95 | + case "targets": |
| 96 | + section = "targets" |
| 97 | + if val != "" && val != "[]" { |
| 98 | + p.Targets = parseInlineList(val) |
| 99 | + } |
| 100 | + case "scripts": |
| 101 | + section = "scripts" |
| 102 | + case "dependencies": |
| 103 | + section = "dependencies" |
| 104 | + depSection = "" |
| 105 | + case "marketplace": |
| 106 | + section = "marketplace" |
| 107 | + default: |
| 108 | + section = key |
| 109 | + } |
| 110 | + continue |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + // Section-specific parsing. |
| 115 | + indent := len(line) - len(strings.TrimLeft(line, " \t")) |
| 116 | + |
| 117 | + switch section { |
| 118 | + case "targets": |
| 119 | + if strings.HasPrefix(trimmed, "-") { |
| 120 | + val := strings.TrimSpace(trimmed[1:]) |
| 121 | + if val != "" { |
| 122 | + p.Targets = append(p.Targets, unquote(val)) |
| 123 | + } |
| 124 | + } |
| 125 | + case "scripts": |
| 126 | + if idx := strings.Index(trimmed, ":"); idx >= 0 { |
| 127 | + key := strings.TrimSpace(trimmed[:idx]) |
| 128 | + val := strings.TrimSpace(trimmed[idx+1:]) |
| 129 | + if key != "" && !strings.HasPrefix(key, "-") { |
| 130 | + p.Scripts[key] = unquote(val) |
| 131 | + } |
| 132 | + } |
| 133 | + case "dependencies": |
| 134 | + if indent == 2 || indent == 0 { |
| 135 | + if strings.HasSuffix(trimmed, ":") { |
| 136 | + depSection = strings.TrimSuffix(trimmed, ":") |
| 137 | + } |
| 138 | + } |
| 139 | + if strings.HasPrefix(trimmed, "-") { |
| 140 | + val := strings.TrimSpace(trimmed[1:]) |
| 141 | + if val != "" { |
| 142 | + dep := parseDep(unquote(val)) |
| 143 | + switch depSection { |
| 144 | + case "apm": |
| 145 | + p.Deps = append(p.Deps, dep) |
| 146 | + case "mcp": |
| 147 | + p.MCPDeps = append(p.MCPDeps, dep) |
| 148 | + } |
| 149 | + } |
| 150 | + } |
| 151 | + case "marketplace": |
| 152 | + // Parse marketplace entries (name: URL or - name: url) |
| 153 | + if idx := strings.Index(trimmed, ":"); idx >= 0 { |
| 154 | + key := strings.TrimSpace(trimmed[:idx]) |
| 155 | + val := strings.TrimSpace(trimmed[idx+1:]) |
| 156 | + if key != "" && val != "" && !strings.HasPrefix(key, "-") { |
| 157 | + p.Marketplaces = append(p.Marketplaces, ApmMarketplace{Name: key, URL: unquote(val)}) |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + return p, scanner.Err() |
| 163 | +} |
| 164 | + |
| 165 | +func unquote(s string) string { |
| 166 | + s = strings.TrimSpace(s) |
| 167 | + if len(s) >= 2 && ((s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'')) { |
| 168 | + return s[1 : len(s)-1] |
| 169 | + } |
| 170 | + return s |
| 171 | +} |
| 172 | + |
| 173 | +func parseInlineList(s string) []string { |
| 174 | + s = strings.TrimPrefix(strings.TrimSuffix(strings.TrimSpace(s), "]"), "[") |
| 175 | + var out []string |
| 176 | + for _, part := range strings.Split(s, ",") { |
| 177 | + v := strings.TrimSpace(part) |
| 178 | + if v != "" { |
| 179 | + out = append(out, unquote(v)) |
| 180 | + } |
| 181 | + } |
| 182 | + return out |
| 183 | +} |
| 184 | + |
| 185 | +func parseDep(s string) ApmDep { |
| 186 | + parts := strings.SplitN(s, "@", 2) |
| 187 | + if len(parts) == 2 { |
| 188 | + return ApmDep{Package: parts[0], Ref: parts[1]} |
| 189 | + } |
| 190 | + return ApmDep{Package: s} |
| 191 | +} |
0 commit comments