|
| 1 | +package detector |
| 2 | + |
| 3 | +import ( |
| 4 | + "regexp" |
| 5 | + "sort" |
| 6 | + "strings" |
| 7 | + |
| 8 | + "github.com/Priyans-hu/argus/pkg/types" |
| 9 | +) |
| 10 | + |
| 11 | +// CommandCategory represents a category of commands with priority |
| 12 | +type CommandCategory int |
| 13 | + |
| 14 | +const ( |
| 15 | + CategoryBuild CommandCategory = iota |
| 16 | + CategoryTest |
| 17 | + CategoryLint |
| 18 | + CategoryFormat |
| 19 | + CategoryRun |
| 20 | + CategoryInstall |
| 21 | + CategoryClean |
| 22 | + CategoryGenerate |
| 23 | + CategoryDeploy |
| 24 | + CategoryDocker |
| 25 | + CategoryDatabase |
| 26 | + CategoryOther |
| 27 | +) |
| 28 | + |
| 29 | +// categoryPriority maps categories to their priority (lower = higher priority) |
| 30 | +var categoryPriority = map[CommandCategory]int{ |
| 31 | + CategoryBuild: 1, |
| 32 | + CategoryTest: 2, |
| 33 | + CategoryLint: 3, |
| 34 | + CategoryFormat: 4, |
| 35 | + CategoryRun: 5, |
| 36 | + CategoryInstall: 6, |
| 37 | + CategoryClean: 7, |
| 38 | + CategoryGenerate: 8, |
| 39 | + CategoryDeploy: 9, |
| 40 | + CategoryDocker: 10, |
| 41 | + CategoryDatabase: 11, |
| 42 | + CategoryOther: 99, |
| 43 | +} |
| 44 | + |
| 45 | +// categoryPatterns maps regex patterns to categories |
| 46 | +// These work across all languages |
| 47 | +var categoryPatterns = []struct { |
| 48 | + pattern *regexp.Regexp |
| 49 | + category CommandCategory |
| 50 | +}{ |
| 51 | + // Build commands |
| 52 | + {regexp.MustCompile(`(?i)^(make\s+)?(build|compile|dist|release|bundle)$`), CategoryBuild}, |
| 53 | + {regexp.MustCompile(`(?i)^go\s+build`), CategoryBuild}, |
| 54 | + {regexp.MustCompile(`(?i)^cargo\s+build`), CategoryBuild}, |
| 55 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?build`), CategoryBuild}, |
| 56 | + {regexp.MustCompile(`(?i)^python\s+setup\.py\s+build`), CategoryBuild}, |
| 57 | + {regexp.MustCompile(`(?i)^poetry\s+build`), CategoryBuild}, |
| 58 | + {regexp.MustCompile(`(?i)^gradle\s+build`), CategoryBuild}, |
| 59 | + {regexp.MustCompile(`(?i)^mvn\s+(compile|package)`), CategoryBuild}, |
| 60 | + {regexp.MustCompile(`(?i)^dotnet\s+build`), CategoryBuild}, |
| 61 | + |
| 62 | + // Test commands |
| 63 | + {regexp.MustCompile(`(?i)^(make\s+)?test`), CategoryTest}, |
| 64 | + {regexp.MustCompile(`(?i)^go\s+test`), CategoryTest}, |
| 65 | + {regexp.MustCompile(`(?i)^cargo\s+test`), CategoryTest}, |
| 66 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?test`), CategoryTest}, |
| 67 | + {regexp.MustCompile(`(?i)^pytest`), CategoryTest}, |
| 68 | + {regexp.MustCompile(`(?i)^python\s+-m\s+(pytest|unittest)`), CategoryTest}, |
| 69 | + {regexp.MustCompile(`(?i)^poetry\s+run\s+(pytest|python\s+-m\s+pytest)`), CategoryTest}, |
| 70 | + {regexp.MustCompile(`(?i)^jest`), CategoryTest}, |
| 71 | + {regexp.MustCompile(`(?i)^vitest`), CategoryTest}, |
| 72 | + {regexp.MustCompile(`(?i)^gradle\s+test`), CategoryTest}, |
| 73 | + {regexp.MustCompile(`(?i)^mvn\s+test`), CategoryTest}, |
| 74 | + {regexp.MustCompile(`(?i)^dotnet\s+test`), CategoryTest}, |
| 75 | + {regexp.MustCompile(`(?i)^rspec`), CategoryTest}, |
| 76 | + {regexp.MustCompile(`(?i)^bundle\s+exec\s+rspec`), CategoryTest}, |
| 77 | + {regexp.MustCompile(`(?i)coverage`), CategoryTest}, |
| 78 | + |
| 79 | + // Lint commands |
| 80 | + {regexp.MustCompile(`(?i)^(make\s+)?lint`), CategoryLint}, |
| 81 | + {regexp.MustCompile(`(?i)^golangci-lint`), CategoryLint}, |
| 82 | + {regexp.MustCompile(`(?i)^cargo\s+clippy`), CategoryLint}, |
| 83 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?lint`), CategoryLint}, |
| 84 | + {regexp.MustCompile(`(?i)^eslint`), CategoryLint}, |
| 85 | + {regexp.MustCompile(`(?i)^(ruff|flake8|pylint|mypy)\s+check`), CategoryLint}, |
| 86 | + {regexp.MustCompile(`(?i)^poetry\s+run\s+(ruff|flake8|pylint|mypy)`), CategoryLint}, |
| 87 | + {regexp.MustCompile(`(?i)^rubocop`), CategoryLint}, |
| 88 | + {regexp.MustCompile(`(?i)^check`), CategoryLint}, |
| 89 | + |
| 90 | + // Format commands |
| 91 | + {regexp.MustCompile(`(?i)^(make\s+)?(format|fmt)$`), CategoryFormat}, |
| 92 | + {regexp.MustCompile(`(?i)^go\s+fmt`), CategoryFormat}, |
| 93 | + {regexp.MustCompile(`(?i)^gofmt`), CategoryFormat}, |
| 94 | + {regexp.MustCompile(`(?i)^goimports`), CategoryFormat}, |
| 95 | + {regexp.MustCompile(`(?i)^cargo\s+fmt`), CategoryFormat}, |
| 96 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?format`), CategoryFormat}, |
| 97 | + {regexp.MustCompile(`(?i)^prettier`), CategoryFormat}, |
| 98 | + {regexp.MustCompile(`(?i)^(black|ruff\s+format|autopep8|yapf)`), CategoryFormat}, |
| 99 | + {regexp.MustCompile(`(?i)^poetry\s+run\s+(black|ruff\s+format)`), CategoryFormat}, |
| 100 | + |
| 101 | + // Run/Dev commands |
| 102 | + {regexp.MustCompile(`(?i)^(make\s+)?(run|start|serve|dev)$`), CategoryRun}, |
| 103 | + {regexp.MustCompile(`(?i)^go\s+run`), CategoryRun}, |
| 104 | + {regexp.MustCompile(`(?i)^cargo\s+run`), CategoryRun}, |
| 105 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?(start|dev|serve)`), CategoryRun}, |
| 106 | + {regexp.MustCompile(`(?i)^python\s+(app|main|run|manage)\.py`), CategoryRun}, |
| 107 | + {regexp.MustCompile(`(?i)^(flask|uvicorn|gunicorn|django)`), CategoryRun}, |
| 108 | + {regexp.MustCompile(`(?i)^poetry\s+run\s+(python|flask|uvicorn)`), CategoryRun}, |
| 109 | + {regexp.MustCompile(`(?i)^rails\s+s`), CategoryRun}, |
| 110 | + {regexp.MustCompile(`(?i)^bundle\s+exec\s+rails`), CategoryRun}, |
| 111 | + {regexp.MustCompile(`(?i)runserver`), CategoryRun}, |
| 112 | + |
| 113 | + // Install commands |
| 114 | + {regexp.MustCompile(`(?i)^(make\s+)?install$`), CategoryInstall}, |
| 115 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+install`), CategoryInstall}, |
| 116 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+ci`), CategoryInstall}, |
| 117 | + {regexp.MustCompile(`(?i)^pip\s+install`), CategoryInstall}, |
| 118 | + {regexp.MustCompile(`(?i)^poetry\s+install`), CategoryInstall}, |
| 119 | + {regexp.MustCompile(`(?i)^cargo\s+install`), CategoryInstall}, |
| 120 | + {regexp.MustCompile(`(?i)^bundle\s+install`), CategoryInstall}, |
| 121 | + {regexp.MustCompile(`(?i)^go\s+mod\s+(download|tidy)`), CategoryInstall}, |
| 122 | + {regexp.MustCompile(`(?i)^setup`), CategoryInstall}, |
| 123 | + |
| 124 | + // Clean commands |
| 125 | + {regexp.MustCompile(`(?i)^(make\s+)?clean`), CategoryClean}, |
| 126 | + {regexp.MustCompile(`(?i)^cargo\s+clean`), CategoryClean}, |
| 127 | + {regexp.MustCompile(`(?i)^go\s+clean`), CategoryClean}, |
| 128 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?clean`), CategoryClean}, |
| 129 | + |
| 130 | + // Generate commands |
| 131 | + {regexp.MustCompile(`(?i)^(make\s+)?(generate|gen|codegen|proto)`), CategoryGenerate}, |
| 132 | + {regexp.MustCompile(`(?i)^go\s+generate`), CategoryGenerate}, |
| 133 | + {regexp.MustCompile(`(?i)^protoc`), CategoryGenerate}, |
| 134 | + |
| 135 | + // Deploy commands |
| 136 | + {regexp.MustCompile(`(?i)^(make\s+)?deploy`), CategoryDeploy}, |
| 137 | + {regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?deploy`), CategoryDeploy}, |
| 138 | + {regexp.MustCompile(`(?i)^kubectl`), CategoryDeploy}, |
| 139 | + {regexp.MustCompile(`(?i)^helm`), CategoryDeploy}, |
| 140 | + {regexp.MustCompile(`(?i)^terraform`), CategoryDeploy}, |
| 141 | + |
| 142 | + // Docker commands |
| 143 | + {regexp.MustCompile(`(?i)^(make\s+)?docker`), CategoryDocker}, |
| 144 | + {regexp.MustCompile(`(?i)^docker\s+(build|compose|run)`), CategoryDocker}, |
| 145 | + |
| 146 | + // Database commands |
| 147 | + {regexp.MustCompile(`(?i)^(make\s+)?migrate`), CategoryDatabase}, |
| 148 | + {regexp.MustCompile(`(?i)^(make\s+)?seed`), CategoryDatabase}, |
| 149 | + {regexp.MustCompile(`(?i)migrations`), CategoryDatabase}, |
| 150 | + {regexp.MustCompile(`(?i)^(npx\s+)?prisma`), CategoryDatabase}, |
| 151 | + {regexp.MustCompile(`(?i)^alembic`), CategoryDatabase}, |
| 152 | + {regexp.MustCompile(`(?i)^rails\s+db:`), CategoryDatabase}, |
| 153 | +} |
| 154 | + |
| 155 | +// PrioritizedCommand extends Command with priority info |
| 156 | +type PrioritizedCommand struct { |
| 157 | + types.Command |
| 158 | + Category CommandCategory |
| 159 | + Priority int |
| 160 | +} |
| 161 | + |
| 162 | +// categorizeCommand determines the category of a command |
| 163 | +func categorizeCommand(cmd types.Command) CommandCategory { |
| 164 | + // Check command name against patterns |
| 165 | + cmdName := strings.TrimSpace(cmd.Name) |
| 166 | + |
| 167 | + for _, cp := range categoryPatterns { |
| 168 | + if cp.pattern.MatchString(cmdName) { |
| 169 | + return cp.category |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + // Check description for hints |
| 174 | + desc := strings.ToLower(cmd.Description) |
| 175 | + if strings.Contains(desc, "build") || strings.Contains(desc, "compile") { |
| 176 | + return CategoryBuild |
| 177 | + } |
| 178 | + if strings.Contains(desc, "test") { |
| 179 | + return CategoryTest |
| 180 | + } |
| 181 | + if strings.Contains(desc, "lint") || strings.Contains(desc, "check") { |
| 182 | + return CategoryLint |
| 183 | + } |
| 184 | + if strings.Contains(desc, "format") { |
| 185 | + return CategoryFormat |
| 186 | + } |
| 187 | + if strings.Contains(desc, "install") || strings.Contains(desc, "dependencies") { |
| 188 | + return CategoryInstall |
| 189 | + } |
| 190 | + if strings.Contains(desc, "clean") { |
| 191 | + return CategoryClean |
| 192 | + } |
| 193 | + if strings.Contains(desc, "run") || strings.Contains(desc, "start") || strings.Contains(desc, "dev") { |
| 194 | + return CategoryRun |
| 195 | + } |
| 196 | + |
| 197 | + return CategoryOther |
| 198 | +} |
| 199 | + |
| 200 | +// PrioritizeCommands sorts commands by category priority and removes duplicates |
| 201 | +func PrioritizeCommands(commands []types.Command) []types.Command { |
| 202 | + if len(commands) == 0 { |
| 203 | + return commands |
| 204 | + } |
| 205 | + |
| 206 | + // Categorize all commands |
| 207 | + prioritized := make([]PrioritizedCommand, 0, len(commands)) |
| 208 | + for _, cmd := range commands { |
| 209 | + cat := categorizeCommand(cmd) |
| 210 | + prioritized = append(prioritized, PrioritizedCommand{ |
| 211 | + Command: cmd, |
| 212 | + Category: cat, |
| 213 | + Priority: categoryPriority[cat], |
| 214 | + }) |
| 215 | + } |
| 216 | + |
| 217 | + // Sort by priority, then alphabetically within same priority |
| 218 | + sort.SliceStable(prioritized, func(i, j int) bool { |
| 219 | + if prioritized[i].Priority != prioritized[j].Priority { |
| 220 | + return prioritized[i].Priority < prioritized[j].Priority |
| 221 | + } |
| 222 | + return prioritized[i].Name < prioritized[j].Name |
| 223 | + }) |
| 224 | + |
| 225 | + // Remove duplicates (keep first occurrence which has higher priority) |
| 226 | + seen := make(map[string]bool) |
| 227 | + result := make([]types.Command, 0, len(prioritized)) |
| 228 | + |
| 229 | + for _, pc := range prioritized { |
| 230 | + // Normalize command for dedup |
| 231 | + normalized := normalizeCommand(pc.Name) |
| 232 | + if seen[normalized] { |
| 233 | + continue |
| 234 | + } |
| 235 | + seen[normalized] = true |
| 236 | + result = append(result, pc.Command) |
| 237 | + } |
| 238 | + |
| 239 | + return result |
| 240 | +} |
| 241 | + |
| 242 | +// GetQuickReferenceCommands returns the top N most important commands |
| 243 | +func GetQuickReferenceCommands(commands []types.Command, maxCommands int) []types.Command { |
| 244 | + prioritized := PrioritizeCommands(commands) |
| 245 | + |
| 246 | + // Take top N commands, ensuring we get diversity across categories |
| 247 | + if len(prioritized) <= maxCommands { |
| 248 | + return prioritized |
| 249 | + } |
| 250 | + |
| 251 | + // Ensure at least one from each important category if available |
| 252 | + result := make([]types.Command, 0, maxCommands) |
| 253 | + categoryCount := make(map[CommandCategory]int) |
| 254 | + importantCategories := []CommandCategory{ |
| 255 | + CategoryBuild, CategoryTest, CategoryLint, CategoryFormat, CategoryRun, CategoryInstall, |
| 256 | + } |
| 257 | + |
| 258 | + // First pass: get one from each important category |
| 259 | + for _, cmd := range prioritized { |
| 260 | + cat := categorizeCommand(cmd) |
| 261 | + for _, ic := range importantCategories { |
| 262 | + if cat == ic && categoryCount[cat] == 0 { |
| 263 | + result = append(result, cmd) |
| 264 | + categoryCount[cat]++ |
| 265 | + break |
| 266 | + } |
| 267 | + } |
| 268 | + if len(result) >= maxCommands { |
| 269 | + break |
| 270 | + } |
| 271 | + } |
| 272 | + |
| 273 | + // Second pass: fill remaining slots with highest priority commands not yet added |
| 274 | + added := make(map[string]bool) |
| 275 | + for _, cmd := range result { |
| 276 | + added[normalizeCommand(cmd.Name)] = true |
| 277 | + } |
| 278 | + |
| 279 | + for _, cmd := range prioritized { |
| 280 | + if len(result) >= maxCommands { |
| 281 | + break |
| 282 | + } |
| 283 | + normalized := normalizeCommand(cmd.Name) |
| 284 | + if !added[normalized] { |
| 285 | + result = append(result, cmd) |
| 286 | + added[normalized] = true |
| 287 | + } |
| 288 | + } |
| 289 | + |
| 290 | + return result |
| 291 | +} |
| 292 | + |
| 293 | +// normalizeCommand normalizes a command string for deduplication |
| 294 | +func normalizeCommand(cmd string) string { |
| 295 | + // Remove common variations |
| 296 | + cmd = strings.ToLower(cmd) |
| 297 | + cmd = strings.TrimSpace(cmd) |
| 298 | + |
| 299 | + // Remove "(in subdir)" suffixes |
| 300 | + if idx := strings.Index(cmd, " (in "); idx > 0 { |
| 301 | + cmd = cmd[:idx] |
| 302 | + } |
| 303 | + |
| 304 | + // Normalize package managers |
| 305 | + cmd = strings.ReplaceAll(cmd, "yarn ", "npm ") |
| 306 | + cmd = strings.ReplaceAll(cmd, "pnpm ", "npm ") |
| 307 | + cmd = strings.ReplaceAll(cmd, "bun ", "npm ") |
| 308 | + |
| 309 | + // Remove "run" from npm run commands |
| 310 | + cmd = strings.ReplaceAll(cmd, "npm run ", "npm ") |
| 311 | + |
| 312 | + return cmd |
| 313 | +} |
| 314 | + |
| 315 | +// GetCategoryName returns a human-readable category name |
| 316 | +func GetCategoryName(cat CommandCategory) string { |
| 317 | + names := map[CommandCategory]string{ |
| 318 | + CategoryBuild: "Build", |
| 319 | + CategoryTest: "Test", |
| 320 | + CategoryLint: "Lint", |
| 321 | + CategoryFormat: "Format", |
| 322 | + CategoryRun: "Run", |
| 323 | + CategoryInstall: "Setup", |
| 324 | + CategoryClean: "Clean", |
| 325 | + CategoryGenerate: "Generate", |
| 326 | + CategoryDeploy: "Deploy", |
| 327 | + CategoryDocker: "Docker", |
| 328 | + CategoryDatabase: "Database", |
| 329 | + CategoryOther: "Other", |
| 330 | + } |
| 331 | + if name, ok := names[cat]; ok { |
| 332 | + return name |
| 333 | + } |
| 334 | + return "Other" |
| 335 | +} |
| 336 | + |
| 337 | +// GroupCommandsByCategory groups commands by their category |
| 338 | +func GroupCommandsByCategory(commands []types.Command) map[string][]types.Command { |
| 339 | + groups := make(map[string][]types.Command) |
| 340 | + |
| 341 | + for _, cmd := range commands { |
| 342 | + cat := categorizeCommand(cmd) |
| 343 | + catName := GetCategoryName(cat) |
| 344 | + groups[catName] = append(groups[catName], cmd) |
| 345 | + } |
| 346 | + |
| 347 | + return groups |
| 348 | +} |
0 commit comments