import "dart:io"; import "base_tool.dart"; class GlobTool extends BaseTool { @override final String name = "Glob"; @override final String description = "Fast file pattern matching. Supports glob patterns like \"**/*.js\". " "Returns matching file paths sorted by modification time."; @override Future execute(Map input) async { final pattern = requireString(input, "pattern"); final pathArg = optionalString(input, "path"); final searchDir = pathArg != null ? Directory(pathArg) : Directory.current; if (!await searchDir.exists()) { return "Error: Directory does not exist: ${searchDir.path}"; } final start = DateTime.now(); final matched = []; await for (final entity in searchDir.list(recursive: true, followLinks: false)) { if (entity is File) { final rel = entity.path.substring(searchDir.path.length); // strip leading slash final relClean = rel.startsWith("/") ? rel.substring(1) : rel; if (_matchGlob(pattern, relClean)) { matched.add(entity); } } } const maxResults = 100; final truncated = matched.length > maxResults; final limited = truncated ? matched.sublist(0, maxResults) : matched; // sort by mtime desc (same as original) final withStat = >[]; for (final f in limited) { try { final st = await f.stat(); withStat.add(MapEntry(f, st.modified)); } catch (_) { withStat.add(MapEntry(f, DateTime.fromMillisecondsSinceEpoch(0))); } } withStat.sort((a, b) => b.value.compareTo(a.value)); final filenames = withStat.map((e) { final rel = e.key.path.substring(searchDir.path.length); return rel.startsWith("/") ? rel.substring(1) : rel; }).toList(); final durationMs = DateTime.now().difference(start).inMilliseconds; if (filenames.isEmpty) { return "No files found"; } final buf = StringBuffer(); for (final f in filenames) { buf.writeln(f); } if (truncated) { buf.writeln("(Results are truncated. Consider using a more specific path or pattern.)"); } // small summary footer buf.write("Found ${filenames.length} file${filenames.length == 1 ? "" : "s"} in ${durationMs}ms"); return buf.toString(); } // basic glob matching - handles ** and * bool _matchGlob(String pattern, String path) { return _globMatch(pattern, path); } bool _globMatch(String pattern, String text) { // convert glob to regex-ish check final regexStr = _globToRegex(pattern); final regex = RegExp(regexStr); return regex.hasMatch(text); } String _globToRegex(String pattern) { final buf = StringBuffer("^"); int i = 0; while (i < pattern.length) { final c = pattern[i]; if (c == "*") { if (i + 1 < pattern.length && pattern[i + 1] == "*") { // ** matches anything including slashes buf.write(".*"); i += 2; // skip optional trailing slash after ** if (i < pattern.length && pattern[i] == "/") i++; } else { // * matches anything except slash buf.write("[^/]*"); i++; } } else if (c == "?") { buf.write("[^/]"); i++; } else if (c == ".") { buf.write("\\."); i++; } else if (c == "{") { // {a,b,c} style alternation final close = pattern.indexOf("}", i); if (close == -1) { buf.write("\\{"); i++; } else { final inner = pattern.substring(i + 1, close); final parts = inner.split(",").map(_globToRegex).join("|"); buf.write("(?:$parts)"); i = close + 1; } } else if ("[]()|^".contains(c)) { buf.write("\\$c"); i++; } else { buf.write(c); i++; } } buf.write(r"$"); return buf.toString(); } }