@hasna/terminal 2.2.0 → 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 (205) hide show
  1. package/dist/cli.js +29 -12
  2. package/package.json +1 -1
  3. package/src/ai.ts +50 -36
  4. package/src/cli.tsx +29 -12
  5. package/src/context-hints.ts +89 -0
  6. package/src/discover.ts +238 -0
  7. package/src/economy.ts +53 -0
  8. package/src/output-store.ts +65 -0
  9. package/src/providers/index.ts +4 -4
  10. package/src/sessions-db.ts +81 -0
  11. package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
  12. package/temp/rtk/.claude/agents/debugger.md +519 -0
  13. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
  14. package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
  15. package/temp/rtk/.claude/agents/technical-writer.md +355 -0
  16. package/temp/rtk/.claude/commands/diagnose.md +352 -0
  17. package/temp/rtk/.claude/commands/test-routing.md +362 -0
  18. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
  19. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
  20. package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
  21. package/temp/rtk/.claude/rules/cli-testing.md +526 -0
  22. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
  23. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
  24. package/temp/rtk/.claude/skills/performance.md +435 -0
  25. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
  26. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
  27. package/temp/rtk/.claude/skills/repo-recap.md +206 -0
  28. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
  29. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
  30. package/temp/rtk/.claude/skills/security-guardian.md +503 -0
  31. package/temp/rtk/.claude/skills/ship.md +404 -0
  32. package/temp/rtk/.github/workflows/benchmark.yml +34 -0
  33. package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
  34. package/temp/rtk/.github/workflows/release-please.yml +51 -0
  35. package/temp/rtk/.github/workflows/release.yml +343 -0
  36. package/temp/rtk/.github/workflows/security-check.yml +135 -0
  37. package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
  38. package/temp/rtk/.release-please-manifest.json +3 -0
  39. package/temp/rtk/ARCHITECTURE.md +1491 -0
  40. package/temp/rtk/CHANGELOG.md +640 -0
  41. package/temp/rtk/CLAUDE.md +605 -0
  42. package/temp/rtk/CONTRIBUTING.md +199 -0
  43. package/temp/rtk/Cargo.lock +1668 -0
  44. package/temp/rtk/Cargo.toml +64 -0
  45. package/temp/rtk/Formula/rtk.rb +43 -0
  46. package/temp/rtk/INSTALL.md +390 -0
  47. package/temp/rtk/LICENSE +21 -0
  48. package/temp/rtk/README.md +386 -0
  49. package/temp/rtk/README_es.md +159 -0
  50. package/temp/rtk/README_fr.md +197 -0
  51. package/temp/rtk/README_ja.md +159 -0
  52. package/temp/rtk/README_ko.md +159 -0
  53. package/temp/rtk/README_zh.md +167 -0
  54. package/temp/rtk/ROADMAP.md +15 -0
  55. package/temp/rtk/SECURITY.md +217 -0
  56. package/temp/rtk/TEST_EXEC_TIME.md +102 -0
  57. package/temp/rtk/build.rs +57 -0
  58. package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
  59. package/temp/rtk/docs/FEATURES.md +1410 -0
  60. package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
  61. package/temp/rtk/docs/filter-workflow.md +102 -0
  62. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  63. package/temp/rtk/docs/tracking.md +583 -0
  64. package/temp/rtk/hooks/opencode-rtk.ts +39 -0
  65. package/temp/rtk/hooks/rtk-awareness.md +29 -0
  66. package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
  67. package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
  68. package/temp/rtk/install.sh +124 -0
  69. package/temp/rtk/release-please-config.json +10 -0
  70. package/temp/rtk/scripts/benchmark.sh +592 -0
  71. package/temp/rtk/scripts/check-installation.sh +162 -0
  72. package/temp/rtk/scripts/install-local.sh +37 -0
  73. package/temp/rtk/scripts/rtk-economics.sh +137 -0
  74. package/temp/rtk/scripts/test-all.sh +561 -0
  75. package/temp/rtk/scripts/test-aristote.sh +227 -0
  76. package/temp/rtk/scripts/test-tracking.sh +79 -0
  77. package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
  78. package/temp/rtk/scripts/validate-docs.sh +73 -0
  79. package/temp/rtk/src/aws_cmd.rs +880 -0
  80. package/temp/rtk/src/binlog.rs +1645 -0
  81. package/temp/rtk/src/cargo_cmd.rs +1727 -0
  82. package/temp/rtk/src/cc_economics.rs +1157 -0
  83. package/temp/rtk/src/ccusage.rs +340 -0
  84. package/temp/rtk/src/config.rs +187 -0
  85. package/temp/rtk/src/container.rs +855 -0
  86. package/temp/rtk/src/curl_cmd.rs +134 -0
  87. package/temp/rtk/src/deps.rs +268 -0
  88. package/temp/rtk/src/diff_cmd.rs +367 -0
  89. package/temp/rtk/src/discover/mod.rs +274 -0
  90. package/temp/rtk/src/discover/provider.rs +388 -0
  91. package/temp/rtk/src/discover/registry.rs +2022 -0
  92. package/temp/rtk/src/discover/report.rs +202 -0
  93. package/temp/rtk/src/discover/rules.rs +667 -0
  94. package/temp/rtk/src/display_helpers.rs +402 -0
  95. package/temp/rtk/src/dotnet_cmd.rs +1771 -0
  96. package/temp/rtk/src/dotnet_format_report.rs +133 -0
  97. package/temp/rtk/src/dotnet_trx.rs +593 -0
  98. package/temp/rtk/src/env_cmd.rs +204 -0
  99. package/temp/rtk/src/filter.rs +462 -0
  100. package/temp/rtk/src/filters/README.md +52 -0
  101. package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
  102. package/temp/rtk/src/filters/basedpyright.toml +47 -0
  103. package/temp/rtk/src/filters/biome.toml +45 -0
  104. package/temp/rtk/src/filters/brew-install.toml +37 -0
  105. package/temp/rtk/src/filters/composer-install.toml +40 -0
  106. package/temp/rtk/src/filters/df.toml +16 -0
  107. package/temp/rtk/src/filters/dotnet-build.toml +64 -0
  108. package/temp/rtk/src/filters/du.toml +16 -0
  109. package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
  110. package/temp/rtk/src/filters/gcc.toml +49 -0
  111. package/temp/rtk/src/filters/gcloud.toml +22 -0
  112. package/temp/rtk/src/filters/hadolint.toml +24 -0
  113. package/temp/rtk/src/filters/helm.toml +29 -0
  114. package/temp/rtk/src/filters/iptables.toml +27 -0
  115. package/temp/rtk/src/filters/jj.toml +28 -0
  116. package/temp/rtk/src/filters/jq.toml +24 -0
  117. package/temp/rtk/src/filters/make.toml +41 -0
  118. package/temp/rtk/src/filters/markdownlint.toml +24 -0
  119. package/temp/rtk/src/filters/mix-compile.toml +27 -0
  120. package/temp/rtk/src/filters/mix-format.toml +15 -0
  121. package/temp/rtk/src/filters/mvn-build.toml +44 -0
  122. package/temp/rtk/src/filters/oxlint.toml +43 -0
  123. package/temp/rtk/src/filters/ping.toml +63 -0
  124. package/temp/rtk/src/filters/pio-run.toml +40 -0
  125. package/temp/rtk/src/filters/poetry-install.toml +50 -0
  126. package/temp/rtk/src/filters/pre-commit.toml +35 -0
  127. package/temp/rtk/src/filters/ps.toml +16 -0
  128. package/temp/rtk/src/filters/quarto-render.toml +41 -0
  129. package/temp/rtk/src/filters/rsync.toml +48 -0
  130. package/temp/rtk/src/filters/shellcheck.toml +27 -0
  131. package/temp/rtk/src/filters/shopify-theme.toml +29 -0
  132. package/temp/rtk/src/filters/skopeo.toml +45 -0
  133. package/temp/rtk/src/filters/sops.toml +16 -0
  134. package/temp/rtk/src/filters/ssh.toml +44 -0
  135. package/temp/rtk/src/filters/stat.toml +34 -0
  136. package/temp/rtk/src/filters/swift-build.toml +41 -0
  137. package/temp/rtk/src/filters/systemctl-status.toml +33 -0
  138. package/temp/rtk/src/filters/terraform-plan.toml +35 -0
  139. package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
  140. package/temp/rtk/src/filters/tofu-init.toml +38 -0
  141. package/temp/rtk/src/filters/tofu-plan.toml +35 -0
  142. package/temp/rtk/src/filters/tofu-validate.toml +17 -0
  143. package/temp/rtk/src/filters/trunk-build.toml +39 -0
  144. package/temp/rtk/src/filters/ty.toml +50 -0
  145. package/temp/rtk/src/filters/uv-sync.toml +37 -0
  146. package/temp/rtk/src/filters/xcodebuild.toml +99 -0
  147. package/temp/rtk/src/filters/yamllint.toml +25 -0
  148. package/temp/rtk/src/find_cmd.rs +598 -0
  149. package/temp/rtk/src/format_cmd.rs +386 -0
  150. package/temp/rtk/src/gain.rs +723 -0
  151. package/temp/rtk/src/gh_cmd.rs +1651 -0
  152. package/temp/rtk/src/git.rs +2012 -0
  153. package/temp/rtk/src/go_cmd.rs +592 -0
  154. package/temp/rtk/src/golangci_cmd.rs +254 -0
  155. package/temp/rtk/src/grep_cmd.rs +288 -0
  156. package/temp/rtk/src/gt_cmd.rs +810 -0
  157. package/temp/rtk/src/hook_audit_cmd.rs +283 -0
  158. package/temp/rtk/src/hook_check.rs +171 -0
  159. package/temp/rtk/src/init.rs +1859 -0
  160. package/temp/rtk/src/integrity.rs +537 -0
  161. package/temp/rtk/src/json_cmd.rs +231 -0
  162. package/temp/rtk/src/learn/detector.rs +628 -0
  163. package/temp/rtk/src/learn/mod.rs +119 -0
  164. package/temp/rtk/src/learn/report.rs +184 -0
  165. package/temp/rtk/src/lint_cmd.rs +694 -0
  166. package/temp/rtk/src/local_llm.rs +316 -0
  167. package/temp/rtk/src/log_cmd.rs +248 -0
  168. package/temp/rtk/src/ls.rs +324 -0
  169. package/temp/rtk/src/main.rs +2482 -0
  170. package/temp/rtk/src/mypy_cmd.rs +389 -0
  171. package/temp/rtk/src/next_cmd.rs +241 -0
  172. package/temp/rtk/src/npm_cmd.rs +236 -0
  173. package/temp/rtk/src/parser/README.md +267 -0
  174. package/temp/rtk/src/parser/error.rs +46 -0
  175. package/temp/rtk/src/parser/formatter.rs +336 -0
  176. package/temp/rtk/src/parser/mod.rs +311 -0
  177. package/temp/rtk/src/parser/types.rs +119 -0
  178. package/temp/rtk/src/pip_cmd.rs +302 -0
  179. package/temp/rtk/src/playwright_cmd.rs +479 -0
  180. package/temp/rtk/src/pnpm_cmd.rs +573 -0
  181. package/temp/rtk/src/prettier_cmd.rs +221 -0
  182. package/temp/rtk/src/prisma_cmd.rs +482 -0
  183. package/temp/rtk/src/psql_cmd.rs +382 -0
  184. package/temp/rtk/src/pytest_cmd.rs +384 -0
  185. package/temp/rtk/src/read.rs +217 -0
  186. package/temp/rtk/src/rewrite_cmd.rs +50 -0
  187. package/temp/rtk/src/ruff_cmd.rs +402 -0
  188. package/temp/rtk/src/runner.rs +271 -0
  189. package/temp/rtk/src/summary.rs +297 -0
  190. package/temp/rtk/src/tee.rs +405 -0
  191. package/temp/rtk/src/telemetry.rs +248 -0
  192. package/temp/rtk/src/toml_filter.rs +1655 -0
  193. package/temp/rtk/src/tracking.rs +1416 -0
  194. package/temp/rtk/src/tree.rs +209 -0
  195. package/temp/rtk/src/tsc_cmd.rs +259 -0
  196. package/temp/rtk/src/utils.rs +432 -0
  197. package/temp/rtk/src/verify_cmd.rs +47 -0
  198. package/temp/rtk/src/vitest_cmd.rs +385 -0
  199. package/temp/rtk/src/wc_cmd.rs +401 -0
  200. package/temp/rtk/src/wget_cmd.rs +260 -0
  201. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
  202. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
  203. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
  204. package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
  205. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
@@ -0,0 +1,324 @@
1
+ use crate::tracking;
2
+ use anyhow::{Context, Result};
3
+ use std::process::Command;
4
+
5
+ /// Noise directories commonly excluded from LLM context
6
+ const NOISE_DIRS: &[&str] = &[
7
+ "node_modules",
8
+ ".git",
9
+ "target",
10
+ "__pycache__",
11
+ ".next",
12
+ "dist",
13
+ "build",
14
+ ".cache",
15
+ ".turbo",
16
+ ".vercel",
17
+ ".pytest_cache",
18
+ ".mypy_cache",
19
+ ".tox",
20
+ ".venv",
21
+ "venv",
22
+ "coverage",
23
+ ".nyc_output",
24
+ ".DS_Store",
25
+ "Thumbs.db",
26
+ ".idea",
27
+ ".vscode",
28
+ ".vs",
29
+ "*.egg-info",
30
+ ".eggs",
31
+ ];
32
+
33
+ pub fn run(args: &[String], verbose: u8) -> Result<()> {
34
+ let timer = tracking::TimedExecution::start();
35
+
36
+ // Separate flags from paths
37
+ let show_all = args
38
+ .iter()
39
+ .any(|a| (a.starts_with('-') && !a.starts_with("--") && a.contains('a')) || a == "--all");
40
+
41
+ let flags: Vec<&str> = args
42
+ .iter()
43
+ .filter(|a| a.starts_with('-'))
44
+ .map(|s| s.as_str())
45
+ .collect();
46
+ let paths: Vec<&str> = args
47
+ .iter()
48
+ .filter(|a| !a.starts_with('-'))
49
+ .map(|s| s.as_str())
50
+ .collect();
51
+
52
+ // Build ls -la + any extra flags the user passed (e.g. -R)
53
+ // Strip -l, -a, -h (we handle all of these ourselves)
54
+ let mut cmd = Command::new("ls");
55
+ cmd.arg("-la");
56
+ for flag in &flags {
57
+ if flag.starts_with("--") {
58
+ // Long flags: skip --all (already handled)
59
+ if *flag != "--all" {
60
+ cmd.arg(flag);
61
+ }
62
+ } else {
63
+ let stripped = flag.trim_start_matches('-');
64
+ let extra: String = stripped
65
+ .chars()
66
+ .filter(|c| *c != 'l' && *c != 'a' && *c != 'h')
67
+ .collect();
68
+ if !extra.is_empty() {
69
+ cmd.arg(format!("-{}", extra));
70
+ }
71
+ }
72
+ }
73
+
74
+ // Add paths (default to "." if none)
75
+ if paths.is_empty() {
76
+ cmd.arg(".");
77
+ } else {
78
+ for p in &paths {
79
+ cmd.arg(p);
80
+ }
81
+ }
82
+
83
+ let output = cmd.output().context("Failed to run ls")?;
84
+
85
+ if !output.status.success() {
86
+ let stderr = String::from_utf8_lossy(&output.stderr);
87
+ eprint!("{}", stderr);
88
+ std::process::exit(output.status.code().unwrap_or(1));
89
+ }
90
+
91
+ let raw = String::from_utf8_lossy(&output.stdout).to_string();
92
+ let filtered = compact_ls(&raw, show_all);
93
+
94
+ if verbose > 0 {
95
+ eprintln!(
96
+ "Chars: {} → {} ({}% reduction)",
97
+ raw.len(),
98
+ filtered.len(),
99
+ if !raw.is_empty() {
100
+ 100 - (filtered.len() * 100 / raw.len())
101
+ } else {
102
+ 0
103
+ }
104
+ );
105
+ }
106
+
107
+ let target_display = if paths.is_empty() {
108
+ ".".to_string()
109
+ } else {
110
+ paths.join(" ")
111
+ };
112
+ print!("{}", filtered);
113
+ timer.track(
114
+ &format!("ls -la {}", target_display),
115
+ "rtk ls",
116
+ &raw,
117
+ &filtered,
118
+ );
119
+
120
+ Ok(())
121
+ }
122
+
123
+ /// Format bytes into human-readable size
124
+ fn human_size(bytes: u64) -> String {
125
+ if bytes >= 1_048_576 {
126
+ format!("{:.1}M", bytes as f64 / 1_048_576.0)
127
+ } else if bytes >= 1024 {
128
+ format!("{:.1}K", bytes as f64 / 1024.0)
129
+ } else {
130
+ format!("{}B", bytes)
131
+ }
132
+ }
133
+
134
+ /// Parse ls -la output into compact format:
135
+ /// name/ (dirs)
136
+ /// name size (files)
137
+ fn compact_ls(raw: &str, show_all: bool) -> String {
138
+ use std::collections::HashMap;
139
+
140
+ let mut dirs: Vec<String> = Vec::new();
141
+ let mut files: Vec<(String, String)> = Vec::new(); // (name, size)
142
+ let mut by_ext: HashMap<String, usize> = HashMap::new();
143
+
144
+ for line in raw.lines() {
145
+ // Skip total, empty, . and ..
146
+ if line.starts_with("total ") || line.is_empty() {
147
+ continue;
148
+ }
149
+
150
+ let parts: Vec<&str> = line.split_whitespace().collect();
151
+ if parts.len() < 9 {
152
+ continue;
153
+ }
154
+
155
+ // Filename is everything from column 9 onward (handles spaces)
156
+ let name = parts[8..].join(" ");
157
+
158
+ // Skip . and ..
159
+ if name == "." || name == ".." {
160
+ continue;
161
+ }
162
+
163
+ // Filter noise dirs unless -a
164
+ if !show_all && NOISE_DIRS.iter().any(|noise| name == *noise) {
165
+ continue;
166
+ }
167
+
168
+ let is_dir = parts[0].starts_with('d');
169
+
170
+ if is_dir {
171
+ dirs.push(name);
172
+ } else if parts[0].starts_with('-') || parts[0].starts_with('l') {
173
+ let size: u64 = parts[4].parse().unwrap_or(0);
174
+ let ext = if let Some(pos) = name.rfind('.') {
175
+ name[pos..].to_string()
176
+ } else {
177
+ "no ext".to_string()
178
+ };
179
+ *by_ext.entry(ext).or_insert(0) += 1;
180
+ files.push((name, human_size(size)));
181
+ }
182
+ }
183
+
184
+ if dirs.is_empty() && files.is_empty() {
185
+ return "(empty)\n".to_string();
186
+ }
187
+
188
+ let mut out = String::new();
189
+
190
+ // Dirs first, compact
191
+ for d in &dirs {
192
+ out.push_str(d);
193
+ out.push_str("/\n");
194
+ }
195
+
196
+ // Files with size
197
+ for (name, size) in &files {
198
+ out.push_str(name);
199
+ out.push_str(" ");
200
+ out.push_str(size);
201
+ out.push('\n');
202
+ }
203
+
204
+ // Summary line
205
+ out.push('\n');
206
+ let mut summary = format!("📊 {} files, {} dirs", files.len(), dirs.len());
207
+ if !by_ext.is_empty() {
208
+ let mut ext_counts: Vec<_> = by_ext.iter().collect();
209
+ ext_counts.sort_by(|a, b| b.1.cmp(a.1));
210
+ let ext_parts: Vec<String> = ext_counts
211
+ .iter()
212
+ .take(5)
213
+ .map(|(ext, count)| format!("{} {}", count, ext))
214
+ .collect();
215
+ summary.push_str(" (");
216
+ summary.push_str(&ext_parts.join(", "));
217
+ if ext_counts.len() > 5 {
218
+ summary.push_str(&format!(", +{} more", ext_counts.len() - 5));
219
+ }
220
+ summary.push(')');
221
+ }
222
+ out.push_str(&summary);
223
+ out.push('\n');
224
+
225
+ out
226
+ }
227
+
228
+ #[cfg(test)]
229
+ mod tests {
230
+ use super::*;
231
+
232
+ #[test]
233
+ fn test_compact_basic() {
234
+ let input = "total 48\n\
235
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 .\n\
236
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 ..\n\
237
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 src\n\
238
+ -rw-r--r-- 1 user staff 1234 Jan 1 12:00 Cargo.toml\n\
239
+ -rw-r--r-- 1 user staff 5678 Jan 1 12:00 README.md\n";
240
+ let output = compact_ls(input, false);
241
+ assert!(output.contains("src/"));
242
+ assert!(output.contains("Cargo.toml"));
243
+ assert!(output.contains("README.md"));
244
+ assert!(output.contains("1.2K")); // 1234 bytes
245
+ assert!(output.contains("5.5K")); // 5678 bytes
246
+ assert!(!output.contains("drwx")); // no permissions
247
+ assert!(!output.contains("staff")); // no group
248
+ assert!(!output.contains("total")); // no total
249
+ assert!(!output.contains("\n.\n")); // no . entry
250
+ assert!(!output.contains("\n..\n")); // no .. entry
251
+ }
252
+
253
+ #[test]
254
+ fn test_compact_filters_noise() {
255
+ let input = "total 8\n\
256
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 node_modules\n\
257
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 .git\n\
258
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 target\n\
259
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 src\n\
260
+ -rw-r--r-- 1 user staff 100 Jan 1 12:00 main.rs\n";
261
+ let output = compact_ls(input, false);
262
+ assert!(!output.contains("node_modules"));
263
+ assert!(!output.contains(".git"));
264
+ assert!(!output.contains("target"));
265
+ assert!(output.contains("src/"));
266
+ assert!(output.contains("main.rs"));
267
+ }
268
+
269
+ #[test]
270
+ fn test_compact_show_all() {
271
+ let input = "total 8\n\
272
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 .git\n\
273
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 src\n";
274
+ let output = compact_ls(input, true);
275
+ assert!(output.contains(".git/"));
276
+ assert!(output.contains("src/"));
277
+ }
278
+
279
+ #[test]
280
+ fn test_compact_empty() {
281
+ let input = "total 0\n";
282
+ let output = compact_ls(input, false);
283
+ assert_eq!(output, "(empty)\n");
284
+ }
285
+
286
+ #[test]
287
+ fn test_compact_summary() {
288
+ let input = "total 48\n\
289
+ drwxr-xr-x 2 user staff 64 Jan 1 12:00 src\n\
290
+ -rw-r--r-- 1 user staff 1234 Jan 1 12:00 main.rs\n\
291
+ -rw-r--r-- 1 user staff 5678 Jan 1 12:00 lib.rs\n\
292
+ -rw-r--r-- 1 user staff 100 Jan 1 12:00 Cargo.toml\n";
293
+ let output = compact_ls(input, false);
294
+ assert!(output.contains("📊 3 files, 1 dirs"));
295
+ assert!(output.contains(".rs"));
296
+ assert!(output.contains(".toml"));
297
+ }
298
+
299
+ #[test]
300
+ fn test_human_size() {
301
+ assert_eq!(human_size(0), "0B");
302
+ assert_eq!(human_size(500), "500B");
303
+ assert_eq!(human_size(1024), "1.0K");
304
+ assert_eq!(human_size(1234), "1.2K");
305
+ assert_eq!(human_size(1_048_576), "1.0M");
306
+ assert_eq!(human_size(2_500_000), "2.4M");
307
+ }
308
+
309
+ #[test]
310
+ fn test_compact_handles_filenames_with_spaces() {
311
+ let input = "total 8\n\
312
+ -rw-r--r-- 1 user staff 1234 Jan 1 12:00 my file.txt\n";
313
+ let output = compact_ls(input, false);
314
+ assert!(output.contains("my file.txt"));
315
+ }
316
+
317
+ #[test]
318
+ fn test_compact_symlinks() {
319
+ let input = "total 8\n\
320
+ lrwxr-xr-x 1 user staff 10 Jan 1 12:00 link -> target\n";
321
+ let output = compact_ls(input, false);
322
+ assert!(output.contains("link -> target"));
323
+ }
324
+ }