@hasna/terminal 2.0.5 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/dist/cli.js +52 -21
  2. package/package.json +1 -1
  3. package/src/ai.ts +77 -130
  4. package/src/cli.tsx +51 -21
  5. package/src/command-validator.ts +11 -0
  6. package/src/context-hints.ts +291 -0
  7. package/src/discover.ts +238 -0
  8. package/src/economy.ts +53 -0
  9. package/src/output-processor.ts +7 -18
  10. package/src/output-store.ts +65 -0
  11. package/src/providers/base.ts +3 -1
  12. package/src/providers/groq.ts +108 -0
  13. package/src/providers/index.ts +26 -2
  14. package/src/providers/providers.test.ts +4 -2
  15. package/src/providers/xai.ts +108 -0
  16. package/src/sessions-db.ts +81 -0
  17. package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
  18. package/temp/rtk/.claude/agents/debugger.md +519 -0
  19. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
  20. package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
  21. package/temp/rtk/.claude/agents/technical-writer.md +355 -0
  22. package/temp/rtk/.claude/commands/diagnose.md +352 -0
  23. package/temp/rtk/.claude/commands/test-routing.md +362 -0
  24. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
  25. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
  26. package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
  27. package/temp/rtk/.claude/rules/cli-testing.md +526 -0
  28. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
  29. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
  30. package/temp/rtk/.claude/skills/performance.md +435 -0
  31. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
  32. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
  33. package/temp/rtk/.claude/skills/repo-recap.md +206 -0
  34. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
  35. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
  36. package/temp/rtk/.claude/skills/security-guardian.md +503 -0
  37. package/temp/rtk/.claude/skills/ship.md +404 -0
  38. package/temp/rtk/.github/workflows/benchmark.yml +34 -0
  39. package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
  40. package/temp/rtk/.github/workflows/release-please.yml +51 -0
  41. package/temp/rtk/.github/workflows/release.yml +343 -0
  42. package/temp/rtk/.github/workflows/security-check.yml +135 -0
  43. package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
  44. package/temp/rtk/.release-please-manifest.json +3 -0
  45. package/temp/rtk/ARCHITECTURE.md +1491 -0
  46. package/temp/rtk/CHANGELOG.md +640 -0
  47. package/temp/rtk/CLAUDE.md +605 -0
  48. package/temp/rtk/CONTRIBUTING.md +199 -0
  49. package/temp/rtk/Cargo.lock +1668 -0
  50. package/temp/rtk/Cargo.toml +64 -0
  51. package/temp/rtk/Formula/rtk.rb +43 -0
  52. package/temp/rtk/INSTALL.md +390 -0
  53. package/temp/rtk/LICENSE +21 -0
  54. package/temp/rtk/README.md +386 -0
  55. package/temp/rtk/README_es.md +159 -0
  56. package/temp/rtk/README_fr.md +197 -0
  57. package/temp/rtk/README_ja.md +159 -0
  58. package/temp/rtk/README_ko.md +159 -0
  59. package/temp/rtk/README_zh.md +167 -0
  60. package/temp/rtk/ROADMAP.md +15 -0
  61. package/temp/rtk/SECURITY.md +217 -0
  62. package/temp/rtk/TEST_EXEC_TIME.md +102 -0
  63. package/temp/rtk/build.rs +57 -0
  64. package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
  65. package/temp/rtk/docs/FEATURES.md +1410 -0
  66. package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
  67. package/temp/rtk/docs/filter-workflow.md +102 -0
  68. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  69. package/temp/rtk/docs/tracking.md +583 -0
  70. package/temp/rtk/hooks/opencode-rtk.ts +39 -0
  71. package/temp/rtk/hooks/rtk-awareness.md +29 -0
  72. package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
  73. package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
  74. package/temp/rtk/install.sh +124 -0
  75. package/temp/rtk/release-please-config.json +10 -0
  76. package/temp/rtk/scripts/benchmark.sh +592 -0
  77. package/temp/rtk/scripts/check-installation.sh +162 -0
  78. package/temp/rtk/scripts/install-local.sh +37 -0
  79. package/temp/rtk/scripts/rtk-economics.sh +137 -0
  80. package/temp/rtk/scripts/test-all.sh +561 -0
  81. package/temp/rtk/scripts/test-aristote.sh +227 -0
  82. package/temp/rtk/scripts/test-tracking.sh +79 -0
  83. package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
  84. package/temp/rtk/scripts/validate-docs.sh +73 -0
  85. package/temp/rtk/src/aws_cmd.rs +880 -0
  86. package/temp/rtk/src/binlog.rs +1645 -0
  87. package/temp/rtk/src/cargo_cmd.rs +1727 -0
  88. package/temp/rtk/src/cc_economics.rs +1157 -0
  89. package/temp/rtk/src/ccusage.rs +340 -0
  90. package/temp/rtk/src/config.rs +187 -0
  91. package/temp/rtk/src/container.rs +855 -0
  92. package/temp/rtk/src/curl_cmd.rs +134 -0
  93. package/temp/rtk/src/deps.rs +268 -0
  94. package/temp/rtk/src/diff_cmd.rs +367 -0
  95. package/temp/rtk/src/discover/mod.rs +274 -0
  96. package/temp/rtk/src/discover/provider.rs +388 -0
  97. package/temp/rtk/src/discover/registry.rs +2022 -0
  98. package/temp/rtk/src/discover/report.rs +202 -0
  99. package/temp/rtk/src/discover/rules.rs +667 -0
  100. package/temp/rtk/src/display_helpers.rs +402 -0
  101. package/temp/rtk/src/dotnet_cmd.rs +1771 -0
  102. package/temp/rtk/src/dotnet_format_report.rs +133 -0
  103. package/temp/rtk/src/dotnet_trx.rs +593 -0
  104. package/temp/rtk/src/env_cmd.rs +204 -0
  105. package/temp/rtk/src/filter.rs +462 -0
  106. package/temp/rtk/src/filters/README.md +52 -0
  107. package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
  108. package/temp/rtk/src/filters/basedpyright.toml +47 -0
  109. package/temp/rtk/src/filters/biome.toml +45 -0
  110. package/temp/rtk/src/filters/brew-install.toml +37 -0
  111. package/temp/rtk/src/filters/composer-install.toml +40 -0
  112. package/temp/rtk/src/filters/df.toml +16 -0
  113. package/temp/rtk/src/filters/dotnet-build.toml +64 -0
  114. package/temp/rtk/src/filters/du.toml +16 -0
  115. package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
  116. package/temp/rtk/src/filters/gcc.toml +49 -0
  117. package/temp/rtk/src/filters/gcloud.toml +22 -0
  118. package/temp/rtk/src/filters/hadolint.toml +24 -0
  119. package/temp/rtk/src/filters/helm.toml +29 -0
  120. package/temp/rtk/src/filters/iptables.toml +27 -0
  121. package/temp/rtk/src/filters/jj.toml +28 -0
  122. package/temp/rtk/src/filters/jq.toml +24 -0
  123. package/temp/rtk/src/filters/make.toml +41 -0
  124. package/temp/rtk/src/filters/markdownlint.toml +24 -0
  125. package/temp/rtk/src/filters/mix-compile.toml +27 -0
  126. package/temp/rtk/src/filters/mix-format.toml +15 -0
  127. package/temp/rtk/src/filters/mvn-build.toml +44 -0
  128. package/temp/rtk/src/filters/oxlint.toml +43 -0
  129. package/temp/rtk/src/filters/ping.toml +63 -0
  130. package/temp/rtk/src/filters/pio-run.toml +40 -0
  131. package/temp/rtk/src/filters/poetry-install.toml +50 -0
  132. package/temp/rtk/src/filters/pre-commit.toml +35 -0
  133. package/temp/rtk/src/filters/ps.toml +16 -0
  134. package/temp/rtk/src/filters/quarto-render.toml +41 -0
  135. package/temp/rtk/src/filters/rsync.toml +48 -0
  136. package/temp/rtk/src/filters/shellcheck.toml +27 -0
  137. package/temp/rtk/src/filters/shopify-theme.toml +29 -0
  138. package/temp/rtk/src/filters/skopeo.toml +45 -0
  139. package/temp/rtk/src/filters/sops.toml +16 -0
  140. package/temp/rtk/src/filters/ssh.toml +44 -0
  141. package/temp/rtk/src/filters/stat.toml +34 -0
  142. package/temp/rtk/src/filters/swift-build.toml +41 -0
  143. package/temp/rtk/src/filters/systemctl-status.toml +33 -0
  144. package/temp/rtk/src/filters/terraform-plan.toml +35 -0
  145. package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
  146. package/temp/rtk/src/filters/tofu-init.toml +38 -0
  147. package/temp/rtk/src/filters/tofu-plan.toml +35 -0
  148. package/temp/rtk/src/filters/tofu-validate.toml +17 -0
  149. package/temp/rtk/src/filters/trunk-build.toml +39 -0
  150. package/temp/rtk/src/filters/ty.toml +50 -0
  151. package/temp/rtk/src/filters/uv-sync.toml +37 -0
  152. package/temp/rtk/src/filters/xcodebuild.toml +99 -0
  153. package/temp/rtk/src/filters/yamllint.toml +25 -0
  154. package/temp/rtk/src/find_cmd.rs +598 -0
  155. package/temp/rtk/src/format_cmd.rs +386 -0
  156. package/temp/rtk/src/gain.rs +723 -0
  157. package/temp/rtk/src/gh_cmd.rs +1651 -0
  158. package/temp/rtk/src/git.rs +2012 -0
  159. package/temp/rtk/src/go_cmd.rs +592 -0
  160. package/temp/rtk/src/golangci_cmd.rs +254 -0
  161. package/temp/rtk/src/grep_cmd.rs +288 -0
  162. package/temp/rtk/src/gt_cmd.rs +810 -0
  163. package/temp/rtk/src/hook_audit_cmd.rs +283 -0
  164. package/temp/rtk/src/hook_check.rs +171 -0
  165. package/temp/rtk/src/init.rs +1859 -0
  166. package/temp/rtk/src/integrity.rs +537 -0
  167. package/temp/rtk/src/json_cmd.rs +231 -0
  168. package/temp/rtk/src/learn/detector.rs +628 -0
  169. package/temp/rtk/src/learn/mod.rs +119 -0
  170. package/temp/rtk/src/learn/report.rs +184 -0
  171. package/temp/rtk/src/lint_cmd.rs +694 -0
  172. package/temp/rtk/src/local_llm.rs +316 -0
  173. package/temp/rtk/src/log_cmd.rs +248 -0
  174. package/temp/rtk/src/ls.rs +324 -0
  175. package/temp/rtk/src/main.rs +2482 -0
  176. package/temp/rtk/src/mypy_cmd.rs +389 -0
  177. package/temp/rtk/src/next_cmd.rs +241 -0
  178. package/temp/rtk/src/npm_cmd.rs +236 -0
  179. package/temp/rtk/src/parser/README.md +267 -0
  180. package/temp/rtk/src/parser/error.rs +46 -0
  181. package/temp/rtk/src/parser/formatter.rs +336 -0
  182. package/temp/rtk/src/parser/mod.rs +311 -0
  183. package/temp/rtk/src/parser/types.rs +119 -0
  184. package/temp/rtk/src/pip_cmd.rs +302 -0
  185. package/temp/rtk/src/playwright_cmd.rs +479 -0
  186. package/temp/rtk/src/pnpm_cmd.rs +573 -0
  187. package/temp/rtk/src/prettier_cmd.rs +221 -0
  188. package/temp/rtk/src/prisma_cmd.rs +482 -0
  189. package/temp/rtk/src/psql_cmd.rs +382 -0
  190. package/temp/rtk/src/pytest_cmd.rs +384 -0
  191. package/temp/rtk/src/read.rs +217 -0
  192. package/temp/rtk/src/rewrite_cmd.rs +50 -0
  193. package/temp/rtk/src/ruff_cmd.rs +402 -0
  194. package/temp/rtk/src/runner.rs +271 -0
  195. package/temp/rtk/src/summary.rs +297 -0
  196. package/temp/rtk/src/tee.rs +405 -0
  197. package/temp/rtk/src/telemetry.rs +248 -0
  198. package/temp/rtk/src/toml_filter.rs +1655 -0
  199. package/temp/rtk/src/tracking.rs +1416 -0
  200. package/temp/rtk/src/tree.rs +209 -0
  201. package/temp/rtk/src/tsc_cmd.rs +259 -0
  202. package/temp/rtk/src/utils.rs +432 -0
  203. package/temp/rtk/src/verify_cmd.rs +47 -0
  204. package/temp/rtk/src/vitest_cmd.rs +385 -0
  205. package/temp/rtk/src/wc_cmd.rs +401 -0
  206. package/temp/rtk/src/wget_cmd.rs +260 -0
  207. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
  208. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
  209. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
  210. package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
  211. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
  212. package/dist/App.js +0 -404
  213. package/dist/Browse.js +0 -79
  214. package/dist/FuzzyPicker.js +0 -47
  215. package/dist/Onboarding.js +0 -51
  216. package/dist/Spinner.js +0 -12
  217. package/dist/StatusBar.js +0 -49
  218. package/dist/ai.js +0 -368
  219. package/dist/cache.js +0 -41
  220. package/dist/command-rewriter.js +0 -64
  221. package/dist/command-validator.js +0 -77
  222. package/dist/compression.js +0 -107
  223. package/dist/diff-cache.js +0 -107
  224. package/dist/economy.js +0 -79
  225. package/dist/expand-store.js +0 -38
  226. package/dist/file-cache.js +0 -72
  227. package/dist/file-index.js +0 -62
  228. package/dist/history.js +0 -62
  229. package/dist/lazy-executor.js +0 -54
  230. package/dist/line-dedup.js +0 -59
  231. package/dist/loop-detector.js +0 -75
  232. package/dist/mcp/install.js +0 -98
  233. package/dist/mcp/server.js +0 -569
  234. package/dist/noise-filter.js +0 -86
  235. package/dist/output-processor.js +0 -136
  236. package/dist/output-router.js +0 -41
  237. package/dist/parsers/base.js +0 -2
  238. package/dist/parsers/build.js +0 -64
  239. package/dist/parsers/errors.js +0 -101
  240. package/dist/parsers/files.js +0 -78
  241. package/dist/parsers/git.js +0 -99
  242. package/dist/parsers/index.js +0 -48
  243. package/dist/parsers/tests.js +0 -89
  244. package/dist/providers/anthropic.js +0 -39
  245. package/dist/providers/base.js +0 -4
  246. package/dist/providers/cerebras.js +0 -95
  247. package/dist/providers/index.js +0 -49
  248. package/dist/recipes/model.js +0 -20
  249. package/dist/recipes/storage.js +0 -136
  250. package/dist/search/content-search.js +0 -68
  251. package/dist/search/file-search.js +0 -61
  252. package/dist/search/filters.js +0 -34
  253. package/dist/search/index.js +0 -5
  254. package/dist/search/semantic.js +0 -320
  255. package/dist/session-boot.js +0 -59
  256. package/dist/session-context.js +0 -55
  257. package/dist/sessions-db.js +0 -120
  258. package/dist/smart-display.js +0 -286
  259. package/dist/snapshots.js +0 -51
  260. package/dist/supervisor.js +0 -112
  261. package/dist/test-watchlist.js +0 -131
  262. package/dist/tree.js +0 -94
  263. package/dist/usage-cache.js +0 -65
@@ -0,0 +1,598 @@
1
+ use crate::tracking;
2
+ use anyhow::{Context, Result};
3
+ use ignore::WalkBuilder;
4
+ use std::collections::HashMap;
5
+ use std::path::Path;
6
+
7
+ /// Match a filename against a glob pattern (supports `*` and `?`).
8
+ fn glob_match(pattern: &str, name: &str) -> bool {
9
+ glob_match_inner(pattern.as_bytes(), name.as_bytes())
10
+ }
11
+
12
+ fn glob_match_inner(pat: &[u8], name: &[u8]) -> bool {
13
+ match (pat.first(), name.first()) {
14
+ (None, None) => true,
15
+ (Some(b'*'), _) => {
16
+ // '*' matches zero or more characters
17
+ glob_match_inner(&pat[1..], name)
18
+ || (!name.is_empty() && glob_match_inner(pat, &name[1..]))
19
+ }
20
+ (Some(b'?'), Some(_)) => glob_match_inner(&pat[1..], &name[1..]),
21
+ (Some(&p), Some(&n)) if p == n => glob_match_inner(&pat[1..], &name[1..]),
22
+ _ => false,
23
+ }
24
+ }
25
+
26
+ /// Parsed arguments from either native find or RTK find syntax.
27
+ #[derive(Debug)]
28
+ struct FindArgs {
29
+ pattern: String,
30
+ path: String,
31
+ max_results: usize,
32
+ max_depth: Option<usize>,
33
+ file_type: String,
34
+ case_insensitive: bool,
35
+ }
36
+
37
+ impl Default for FindArgs {
38
+ fn default() -> Self {
39
+ Self {
40
+ pattern: "*".to_string(),
41
+ path: ".".to_string(),
42
+ max_results: 50,
43
+ max_depth: None,
44
+ file_type: "f".to_string(),
45
+ case_insensitive: false,
46
+ }
47
+ }
48
+ }
49
+
50
+ /// Consume the next argument from `args` at position `i`, advancing the index.
51
+ /// Returns `None` if `i` is past the end of `args`.
52
+ fn next_arg(args: &[String], i: &mut usize) -> Option<String> {
53
+ *i += 1;
54
+ args.get(*i).cloned()
55
+ }
56
+
57
+ /// Check if args contain native find flags (-name, -type, -maxdepth, etc.)
58
+ fn has_native_find_flags(args: &[String]) -> bool {
59
+ args.iter()
60
+ .any(|a| a == "-name" || a == "-type" || a == "-maxdepth" || a == "-iname")
61
+ }
62
+
63
+ /// Native find flags that RTK cannot handle correctly.
64
+ /// These involve compound predicates, actions, or semantics we don't support.
65
+ const UNSUPPORTED_FIND_FLAGS: &[&str] = &[
66
+ "-not", "!", "-or", "-o", "-and", "-a", "-exec", "-execdir", "-delete", "-print0", "-newer",
67
+ "-perm", "-size", "-mtime", "-mmin", "-atime", "-amin", "-ctime", "-cmin", "-empty", "-link",
68
+ "-regex", "-iregex",
69
+ ];
70
+
71
+ fn has_unsupported_find_flags(args: &[String]) -> bool {
72
+ args.iter()
73
+ .any(|a| UNSUPPORTED_FIND_FLAGS.contains(&a.as_str()))
74
+ }
75
+
76
+ /// Parse arguments from raw args vec, supporting both native find and RTK syntax.
77
+ ///
78
+ /// Native find syntax: `find . -name "*.rs" -type f -maxdepth 3`
79
+ /// RTK syntax: `find *.rs [path] [-m max] [-t type]`
80
+ fn parse_find_args(args: &[String]) -> Result<FindArgs> {
81
+ if args.is_empty() {
82
+ return Ok(FindArgs::default());
83
+ }
84
+
85
+ if has_unsupported_find_flags(args) {
86
+ anyhow::bail!(
87
+ "rtk find does not support compound predicates or actions (e.g. -not, -exec). Use `find` directly."
88
+ );
89
+ }
90
+
91
+ if has_native_find_flags(args) {
92
+ parse_native_find_args(args)
93
+ } else {
94
+ parse_rtk_find_args(args)
95
+ }
96
+ }
97
+
98
+ /// Parse native find syntax: `find [path] -name "*.rs" -type f -maxdepth 3`
99
+ fn parse_native_find_args(args: &[String]) -> Result<FindArgs> {
100
+ let mut parsed = FindArgs::default();
101
+ let mut i = 0;
102
+
103
+ // First non-flag argument is the path (standard find behavior)
104
+ if !args[0].starts_with('-') {
105
+ parsed.path = args[0].clone();
106
+ i = 1;
107
+ }
108
+
109
+ while i < args.len() {
110
+ match args[i].as_str() {
111
+ "-name" => {
112
+ if let Some(val) = next_arg(args, &mut i) {
113
+ parsed.pattern = val;
114
+ }
115
+ }
116
+ "-iname" => {
117
+ if let Some(val) = next_arg(args, &mut i) {
118
+ parsed.pattern = val;
119
+ parsed.case_insensitive = true;
120
+ }
121
+ }
122
+ "-type" => {
123
+ if let Some(val) = next_arg(args, &mut i) {
124
+ parsed.file_type = val;
125
+ }
126
+ }
127
+ "-maxdepth" => {
128
+ if let Some(val) = next_arg(args, &mut i) {
129
+ parsed.max_depth = Some(val.parse().context("invalid -maxdepth value")?);
130
+ }
131
+ }
132
+ flag if flag.starts_with('-') => {
133
+ eprintln!("rtk find: unknown flag '{}', ignored", flag);
134
+ }
135
+ _ => {}
136
+ }
137
+ i += 1;
138
+ }
139
+
140
+ Ok(parsed)
141
+ }
142
+
143
+ /// Parse RTK syntax: `find <pattern> [path] [-m max] [-t type]`
144
+ fn parse_rtk_find_args(args: &[String]) -> Result<FindArgs> {
145
+ let mut parsed = FindArgs {
146
+ pattern: args[0].clone(),
147
+ ..FindArgs::default()
148
+ };
149
+ let mut i = 1;
150
+
151
+ // Second positional arg (if not a flag) is the path
152
+ if i < args.len() && !args[i].starts_with('-') {
153
+ parsed.path = args[i].clone();
154
+ i += 1;
155
+ }
156
+
157
+ while i < args.len() {
158
+ match args[i].as_str() {
159
+ "-m" | "--max" => {
160
+ if let Some(val) = next_arg(args, &mut i) {
161
+ parsed.max_results = val.parse().context("invalid --max value")?;
162
+ }
163
+ }
164
+ "-t" | "--file-type" => {
165
+ if let Some(val) = next_arg(args, &mut i) {
166
+ parsed.file_type = val;
167
+ }
168
+ }
169
+ _ => {}
170
+ }
171
+ i += 1;
172
+ }
173
+
174
+ Ok(parsed)
175
+ }
176
+
177
+ /// Entry point from main.rs — parses raw args then delegates to run().
178
+ pub fn run_from_args(args: &[String], verbose: u8) -> Result<()> {
179
+ let parsed = parse_find_args(args)?;
180
+ run(
181
+ &parsed.pattern,
182
+ &parsed.path,
183
+ parsed.max_results,
184
+ parsed.max_depth,
185
+ &parsed.file_type,
186
+ parsed.case_insensitive,
187
+ verbose,
188
+ )
189
+ }
190
+
191
+ pub fn run(
192
+ pattern: &str,
193
+ path: &str,
194
+ max_results: usize,
195
+ max_depth: Option<usize>,
196
+ file_type: &str,
197
+ case_insensitive: bool,
198
+ verbose: u8,
199
+ ) -> Result<()> {
200
+ let timer = tracking::TimedExecution::start();
201
+
202
+ // Treat "." as match-all
203
+ let effective_pattern = if pattern == "." { "*" } else { pattern };
204
+
205
+ if verbose > 0 {
206
+ eprintln!("find: {} in {}", effective_pattern, path);
207
+ }
208
+
209
+ let want_dirs = file_type == "d";
210
+
211
+ let mut builder = WalkBuilder::new(path);
212
+ builder
213
+ .hidden(true) // skip hidden files/dirs
214
+ .git_ignore(true) // respect .gitignore
215
+ .git_global(true)
216
+ .git_exclude(true);
217
+ if let Some(depth) = max_depth {
218
+ builder.max_depth(Some(depth));
219
+ }
220
+ let walker = builder.build();
221
+
222
+ let mut files: Vec<String> = Vec::new();
223
+
224
+ for entry in walker {
225
+ let entry = match entry {
226
+ Ok(e) => e,
227
+ Err(_) => continue,
228
+ };
229
+
230
+ let ft = entry.file_type();
231
+ let is_dir = ft.as_ref().is_some_and(|t| t.is_dir());
232
+
233
+ // Filter by type
234
+ if want_dirs && !is_dir {
235
+ continue;
236
+ }
237
+ if !want_dirs && is_dir {
238
+ continue;
239
+ }
240
+
241
+ let entry_path = entry.path();
242
+
243
+ // Get filename for glob matching
244
+ let name = match entry_path.file_name() {
245
+ Some(n) => n.to_string_lossy(),
246
+ None => continue,
247
+ };
248
+
249
+ let matches = if case_insensitive {
250
+ glob_match(&effective_pattern.to_lowercase(), &name.to_lowercase())
251
+ } else {
252
+ glob_match(effective_pattern, &name)
253
+ };
254
+ if !matches {
255
+ continue;
256
+ }
257
+
258
+ // Store path relative to search root
259
+ let display_path = entry_path
260
+ .strip_prefix(path)
261
+ .unwrap_or(entry_path)
262
+ .to_string_lossy()
263
+ .to_string();
264
+
265
+ if !display_path.is_empty() {
266
+ files.push(display_path);
267
+ }
268
+ }
269
+
270
+ files.sort();
271
+
272
+ let raw_output = files.join("\n");
273
+
274
+ if files.is_empty() {
275
+ let msg = format!("0 for '{}'", effective_pattern);
276
+ println!("{}", msg);
277
+ timer.track(
278
+ &format!("find {} -name '{}'", path, effective_pattern),
279
+ "rtk find",
280
+ &raw_output,
281
+ &msg,
282
+ );
283
+ return Ok(());
284
+ }
285
+
286
+ // Group by directory
287
+ let mut by_dir: HashMap<String, Vec<String>> = HashMap::new();
288
+
289
+ for file in &files {
290
+ let p = Path::new(file);
291
+ let dir = p
292
+ .parent()
293
+ .map(|d| d.to_string_lossy().to_string())
294
+ .unwrap_or_else(|| ".".to_string());
295
+ let dir = if dir.is_empty() { ".".to_string() } else { dir };
296
+ let filename = p
297
+ .file_name()
298
+ .map(|f| f.to_string_lossy().to_string())
299
+ .unwrap_or_default();
300
+ by_dir.entry(dir).or_default().push(filename);
301
+ }
302
+
303
+ let mut dirs: Vec<_> = by_dir.keys().cloned().collect();
304
+ dirs.sort();
305
+ let dirs_count = dirs.len();
306
+ let total_files = files.len();
307
+
308
+ println!("📁 {}F {}D:", total_files, dirs_count);
309
+ println!();
310
+
311
+ // Display with proper --max limiting (count individual files)
312
+ let mut shown = 0;
313
+ for dir in &dirs {
314
+ if shown >= max_results {
315
+ break;
316
+ }
317
+
318
+ let files_in_dir = &by_dir[dir];
319
+ let dir_display = if dir.len() > 50 {
320
+ format!("...{}", &dir[dir.len() - 47..])
321
+ } else {
322
+ dir.clone()
323
+ };
324
+
325
+ let remaining_budget = max_results - shown;
326
+ if files_in_dir.len() <= remaining_budget {
327
+ println!("{}/ {}", dir_display, files_in_dir.join(" "));
328
+ shown += files_in_dir.len();
329
+ } else {
330
+ // Partial display: show only what fits in budget
331
+ let partial: Vec<_> = files_in_dir
332
+ .iter()
333
+ .take(remaining_budget)
334
+ .cloned()
335
+ .collect();
336
+ println!("{}/ {}", dir_display, partial.join(" "));
337
+ shown += partial.len();
338
+ break;
339
+ }
340
+ }
341
+
342
+ if shown < total_files {
343
+ println!("+{} more", total_files - shown);
344
+ }
345
+
346
+ // Extension summary
347
+ let mut by_ext: HashMap<String, usize> = HashMap::new();
348
+ for file in &files {
349
+ let ext = Path::new(file)
350
+ .extension()
351
+ .map(|e| e.to_string_lossy().to_string())
352
+ .unwrap_or_else(|| "none".to_string());
353
+ *by_ext.entry(ext).or_default() += 1;
354
+ }
355
+
356
+ let mut ext_line = String::new();
357
+ if by_ext.len() > 1 {
358
+ println!();
359
+ let mut exts: Vec<_> = by_ext.iter().collect();
360
+ exts.sort_by(|a, b| b.1.cmp(a.1));
361
+ let ext_str: Vec<String> = exts
362
+ .iter()
363
+ .take(5)
364
+ .map(|(e, c)| format!(".{}({})", e, c))
365
+ .collect();
366
+ ext_line = format!("ext: {}", ext_str.join(" "));
367
+ println!("{}", ext_line);
368
+ }
369
+
370
+ let rtk_output = format!("{}F {}D + {}", total_files, dirs_count, ext_line);
371
+ timer.track(
372
+ &format!("find {} -name '{}'", path, effective_pattern),
373
+ "rtk find",
374
+ &raw_output,
375
+ &rtk_output,
376
+ );
377
+
378
+ Ok(())
379
+ }
380
+
381
+ #[cfg(test)]
382
+ mod tests {
383
+ use super::*;
384
+
385
+ /// Convert string slices to Vec<String> for test convenience.
386
+ fn args(values: &[&str]) -> Vec<String> {
387
+ values.iter().map(|s| s.to_string()).collect()
388
+ }
389
+
390
+ // --- glob_match unit tests ---
391
+
392
+ #[test]
393
+ fn glob_match_star_rs() {
394
+ assert!(glob_match("*.rs", "main.rs"));
395
+ assert!(glob_match("*.rs", "find_cmd.rs"));
396
+ assert!(!glob_match("*.rs", "main.py"));
397
+ assert!(!glob_match("*.rs", "rs"));
398
+ }
399
+
400
+ #[test]
401
+ fn glob_match_star_all() {
402
+ assert!(glob_match("*", "anything.txt"));
403
+ assert!(glob_match("*", "a"));
404
+ assert!(glob_match("*", ".hidden"));
405
+ }
406
+
407
+ #[test]
408
+ fn glob_match_question_mark() {
409
+ assert!(glob_match("?.rs", "a.rs"));
410
+ assert!(!glob_match("?.rs", "ab.rs"));
411
+ }
412
+
413
+ #[test]
414
+ fn glob_match_exact() {
415
+ assert!(glob_match("Cargo.toml", "Cargo.toml"));
416
+ assert!(!glob_match("Cargo.toml", "cargo.toml"));
417
+ }
418
+
419
+ #[test]
420
+ fn glob_match_complex() {
421
+ assert!(glob_match("test_*", "test_foo"));
422
+ assert!(glob_match("test_*", "test_"));
423
+ assert!(!glob_match("test_*", "test"));
424
+ }
425
+
426
+ // --- dot pattern treated as star ---
427
+
428
+ #[test]
429
+ fn dot_becomes_star() {
430
+ // run() converts "." to "*" internally, test the logic
431
+ let effective = if "." == "." { "*" } else { "." };
432
+ assert_eq!(effective, "*");
433
+ }
434
+
435
+ // --- parse_find_args: native find syntax ---
436
+
437
+ #[test]
438
+ fn parse_native_find_name() {
439
+ let parsed = parse_find_args(&args(&[".", "-name", "*.rs"])).unwrap();
440
+ assert_eq!(parsed.pattern, "*.rs");
441
+ assert_eq!(parsed.path, ".");
442
+ assert_eq!(parsed.file_type, "f");
443
+ assert_eq!(parsed.max_results, 50);
444
+ }
445
+
446
+ #[test]
447
+ fn parse_native_find_name_and_type() {
448
+ let parsed = parse_find_args(&args(&["src", "-name", "*.rs", "-type", "f"])).unwrap();
449
+ assert_eq!(parsed.pattern, "*.rs");
450
+ assert_eq!(parsed.path, "src");
451
+ assert_eq!(parsed.file_type, "f");
452
+ }
453
+
454
+ #[test]
455
+ fn parse_native_find_type_d() {
456
+ let parsed = parse_find_args(&args(&[".", "-type", "d"])).unwrap();
457
+ assert_eq!(parsed.pattern, "*");
458
+ assert_eq!(parsed.file_type, "d");
459
+ }
460
+
461
+ #[test]
462
+ fn parse_native_find_maxdepth() {
463
+ let parsed = parse_find_args(&args(&[".", "-name", "*.toml", "-maxdepth", "2"])).unwrap();
464
+ assert_eq!(parsed.pattern, "*.toml");
465
+ assert_eq!(parsed.max_depth, Some(2));
466
+ assert_eq!(parsed.max_results, 50); // max_results unchanged by -maxdepth
467
+ }
468
+
469
+ #[test]
470
+ fn parse_native_find_iname() {
471
+ let parsed = parse_find_args(&args(&[".", "-iname", "Makefile"])).unwrap();
472
+ assert_eq!(parsed.pattern, "Makefile");
473
+ assert!(parsed.case_insensitive);
474
+ }
475
+
476
+ #[test]
477
+ fn parse_native_find_name_is_case_sensitive() {
478
+ let parsed = parse_find_args(&args(&[".", "-name", "*.rs"])).unwrap();
479
+ assert!(!parsed.case_insensitive);
480
+ }
481
+
482
+ #[test]
483
+ fn parse_native_find_no_path() {
484
+ // `find -name "*.rs"` without explicit path defaults to "."
485
+ let parsed = parse_find_args(&args(&["-name", "*.rs"])).unwrap();
486
+ assert_eq!(parsed.pattern, "*.rs");
487
+ assert_eq!(parsed.path, ".");
488
+ }
489
+
490
+ // --- parse_find_args: unsupported flags ---
491
+
492
+ #[test]
493
+ fn parse_native_find_rejects_not() {
494
+ let result = parse_find_args(&args(&[".", "-name", "*.rs", "-not", "-name", "*_test.rs"]));
495
+ assert!(result.is_err());
496
+ let msg = result.unwrap_err().to_string();
497
+ assert!(msg.contains("compound predicates"));
498
+ }
499
+
500
+ #[test]
501
+ fn parse_native_find_rejects_exec() {
502
+ let result = parse_find_args(&args(&[".", "-name", "*.tmp", "-exec", "rm", "{}", ";"]));
503
+ assert!(result.is_err());
504
+ }
505
+
506
+ // --- parse_find_args: RTK syntax ---
507
+
508
+ #[test]
509
+ fn parse_rtk_syntax_pattern_only() {
510
+ let parsed = parse_find_args(&args(&["*.rs"])).unwrap();
511
+ assert_eq!(parsed.pattern, "*.rs");
512
+ assert_eq!(parsed.path, ".");
513
+ }
514
+
515
+ #[test]
516
+ fn parse_rtk_syntax_pattern_and_path() {
517
+ let parsed = parse_find_args(&args(&["*.rs", "src"])).unwrap();
518
+ assert_eq!(parsed.pattern, "*.rs");
519
+ assert_eq!(parsed.path, "src");
520
+ }
521
+
522
+ #[test]
523
+ fn parse_rtk_syntax_with_flags() {
524
+ let parsed = parse_find_args(&args(&["*.rs", "src", "-m", "10", "-t", "d"])).unwrap();
525
+ assert_eq!(parsed.pattern, "*.rs");
526
+ assert_eq!(parsed.path, "src");
527
+ assert_eq!(parsed.max_results, 10);
528
+ assert_eq!(parsed.file_type, "d");
529
+ }
530
+
531
+ #[test]
532
+ fn parse_empty_args() {
533
+ let parsed = parse_find_args(&args(&[])).unwrap();
534
+ assert_eq!(parsed.pattern, "*");
535
+ assert_eq!(parsed.path, ".");
536
+ }
537
+
538
+ // --- run_from_args integration tests ---
539
+
540
+ #[test]
541
+ fn run_from_args_native_find_syntax() {
542
+ // Simulates: find . -name "*.rs" -type f
543
+ let result = run_from_args(&args(&[".", "-name", "*.rs", "-type", "f"]), 0);
544
+ assert!(result.is_ok());
545
+ }
546
+
547
+ #[test]
548
+ fn run_from_args_rtk_syntax() {
549
+ // Simulates: rtk find *.rs src
550
+ let result = run_from_args(&args(&["*.rs", "src"]), 0);
551
+ assert!(result.is_ok());
552
+ }
553
+
554
+ #[test]
555
+ fn run_from_args_iname_case_insensitive() {
556
+ // -iname should match case-insensitively
557
+ let result = run_from_args(&args(&[".", "-iname", "cargo.toml"]), 0);
558
+ assert!(result.is_ok());
559
+ }
560
+
561
+ // --- integration: run on this repo ---
562
+
563
+ #[test]
564
+ fn find_rs_files_in_src() {
565
+ // Should find .rs files without error
566
+ let result = run("*.rs", "src", 100, None, "f", false, 0);
567
+ assert!(result.is_ok());
568
+ }
569
+
570
+ #[test]
571
+ fn find_dot_pattern_works() {
572
+ // "." pattern should not error (was broken before)
573
+ let result = run(".", "src", 10, None, "f", false, 0);
574
+ assert!(result.is_ok());
575
+ }
576
+
577
+ #[test]
578
+ fn find_no_matches() {
579
+ let result = run("*.xyz_nonexistent", "src", 50, None, "f", false, 0);
580
+ assert!(result.is_ok());
581
+ }
582
+
583
+ #[test]
584
+ fn find_respects_max() {
585
+ // With max=2, should not error
586
+ let result = run("*.rs", "src", 2, None, "f", false, 0);
587
+ assert!(result.is_ok());
588
+ }
589
+
590
+ #[test]
591
+ fn find_gitignored_excluded() {
592
+ // target/ is in .gitignore — files inside should not appear
593
+ let result = run("*", ".", 1000, None, "f", false, 0);
594
+ assert!(result.is_ok());
595
+ // We can't easily capture stdout in unit tests, but at least
596
+ // verify it runs without error. The smoke tests verify content.
597
+ }
598
+ }