trls-cli 0.4.0__tar.gz → 0.5.0__tar.gz
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.
- {trls_cli-0.4.0 → trls_cli-0.5.0}/PKG-INFO +44 -3
- {trls_cli-0.4.0 → trls_cli-0.5.0}/README.md +43 -2
- {trls_cli-0.4.0 → trls_cli-0.5.0}/RELEASING.md +4 -4
- {trls_cli-0.4.0 → trls_cli-0.5.0}/pyproject.toml +1 -1
- {trls_cli-0.4.0 → trls_cli-0.5.0}/src/trls/__init__.py +1 -1
- {trls_cli-0.4.0 → trls_cli-0.5.0}/src/trls/cli.py +18 -2
- {trls_cli-0.4.0 → trls_cli-0.5.0}/src/trls/clipboard.py +98 -108
- trls_cli-0.5.0/src/trls/paths.py +114 -0
- trls_cli-0.5.0/src/trls/skill.py +41 -0
- {trls_cli-0.4.0 → trls_cli-0.5.0}/src/trls/snapshot.py +60 -60
- {trls_cli-0.4.0 → trls_cli-0.5.0}/src/trls/tree.py +3 -1
- {trls_cli-0.4.0 → trls_cli-0.5.0}/tests/test_cli.py +110 -2
- {trls_cli-0.4.0 → trls_cli-0.5.0}/tests/test_tree.py +87 -1
- {trls_cli-0.4.0 → trls_cli-0.5.0}/.gitignore +0 -0
- {trls_cli-0.4.0 → trls_cli-0.5.0}/LICENSE +0 -0
- {trls_cli-0.4.0 → trls_cli-0.5.0}/src/trls/__main__.py +0 -0
- {trls_cli-0.4.0 → trls_cli-0.5.0}/src/trls/renderers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trls-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A modern tree-style CLI for humans and AI prompts.
|
|
5
5
|
Author: Yonglin and Yuanben
|
|
6
6
|
License: MIT
|
|
@@ -41,6 +41,7 @@ Classic `tree` output is useful, but `trls` focuses on modern developer workflow
|
|
|
41
41
|
- `prompt` output for AI-friendly, copy-pasteable directory snapshots
|
|
42
42
|
- snapshot diff by default, so each run shows what changed since last time
|
|
43
43
|
- `-c` clipboard copy for a compact, paste-ready LLM format
|
|
44
|
+
- `--skill` output for agent workflows, with automatic clipboard copy
|
|
44
45
|
- always-on progress feedback during scans in interactive terminals
|
|
45
46
|
- richer colors for common media, office, and data files
|
|
46
47
|
- `text` output for universal shell compatibility
|
|
@@ -77,6 +78,12 @@ Copy a compact LLM-friendly version to the clipboard while keeping the normal te
|
|
|
77
78
|
trls . -c
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
Print the built-in agent skill text and copy it to the clipboard:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
trls --skill
|
|
85
|
+
```
|
|
86
|
+
|
|
80
87
|
In interactive terminals, `trls` also shows live scan progress so large trees do not feel stuck.
|
|
81
88
|
|
|
82
89
|
Snapshots are saved automatically after each run. If you want to refresh the baseline without showing any diff markers, force-save it explicitly:
|
|
@@ -206,7 +213,7 @@ Diff markers:
|
|
|
206
213
|
- `-` removed file or directory
|
|
207
214
|
- `~` modified file or directory
|
|
208
215
|
|
|
209
|
-
Current behavior in `v0.
|
|
216
|
+
Current behavior in `v0.5.0`:
|
|
210
217
|
|
|
211
218
|
- every successful run updates the latest snapshot for that path
|
|
212
219
|
- default `trls` output compares against the previous run for that path
|
|
@@ -228,6 +235,38 @@ Clipboard behavior:
|
|
|
228
235
|
- the clipboard payload is intentionally different from the terminal render to save tokens
|
|
229
236
|
- WSL-aware clipboard fallbacks are attempted before reporting an error
|
|
230
237
|
|
|
238
|
+
## Agent Skill Output
|
|
239
|
+
|
|
240
|
+
Use `trls -skill` or `trls --skill` to print a built-in English skill block for Cursor-style agents and copy it to the clipboard automatically.
|
|
241
|
+
|
|
242
|
+
Skill behavior:
|
|
243
|
+
|
|
244
|
+
- prints a ready-to-paste agent instruction block to stdout
|
|
245
|
+
- copies the same text to the clipboard automatically
|
|
246
|
+
- tells agents to prefer `trls` for file structure analysis before broad manual exploration
|
|
247
|
+
- suggests `--format prompt`, `--depth`, `--ignore`, and `-diff` as the primary workflow
|
|
248
|
+
|
|
249
|
+
Recommended agent command order:
|
|
250
|
+
|
|
251
|
+
1. Run `trls <path> --format prompt` for the first structure pass.
|
|
252
|
+
2. Add `--depth <n>` if the repository is too large.
|
|
253
|
+
3. Add repeated `--ignore <pattern>` options to remove noisy folders such as `.git`, `node_modules`, `dist`, or build output.
|
|
254
|
+
4. Add `--hidden` only when hidden files are likely relevant.
|
|
255
|
+
5. Use `-diff` when you want to compare against the previous snapshot.
|
|
256
|
+
6. Use `-c` when you want a compact clipboard-oriented summary.
|
|
257
|
+
|
|
258
|
+
Common agent examples:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
trls . --format prompt
|
|
262
|
+
trls . --format prompt --depth 2
|
|
263
|
+
trls . --format prompt --ignore .git --ignore node_modules --ignore dist
|
|
264
|
+
trls src --format prompt --depth 3
|
|
265
|
+
trls . -diff --format prompt
|
|
266
|
+
trls . -c
|
|
267
|
+
trls --skill
|
|
268
|
+
```
|
|
269
|
+
|
|
231
270
|
## Progress And Color UX
|
|
232
271
|
|
|
233
272
|
- interactive runs show a transient progress indicator while scanning
|
|
@@ -236,7 +275,7 @@ Clipboard behavior:
|
|
|
236
275
|
- spreadsheet and office files such as `.xlsx`, `.csv`, `.docx`, and `.pptx` have their own color family
|
|
237
276
|
- data-oriented files such as `.parquet` and `.ipynb` are also colorized
|
|
238
277
|
|
|
239
|
-
## CLI contract for `v0.
|
|
278
|
+
## CLI contract for `v0.5.0`
|
|
240
279
|
|
|
241
280
|
The first public release guarantees:
|
|
242
281
|
|
|
@@ -249,6 +288,7 @@ The first public release guarantees:
|
|
|
249
288
|
- snapshots are automatically updated after successful runs
|
|
250
289
|
- snapshot diffing is available via `-save`/`--save-snapshot`, `-diff`/`--diff-last`, `-compare`/`--compare-with`, and `-update`/`--update-snapshot`
|
|
251
290
|
- clipboard copy is available via `-c`/`--copy`
|
|
291
|
+
- agent skill output is available via `-skill`/`--skill`
|
|
252
292
|
- interactive scans show live progress feedback
|
|
253
293
|
- directories are listed before files and names are sorted case-insensitively
|
|
254
294
|
- unreadable directories are reported in the output instead of crashing the renderers
|
|
@@ -290,6 +330,7 @@ Recommended flow:
|
|
|
290
330
|
2. Verify `pip install`, `trls --version`, and one real CLI example.
|
|
291
331
|
3. Publish the tagged release to PyPI with Trusted Publishing.
|
|
292
332
|
|
|
333
|
+
.venv\Scripts\Activate.ps1
|
|
293
334
|
Remove-Item -Recurse -Force .\dist
|
|
294
335
|
python -m pytest
|
|
295
336
|
python -m build
|
|
@@ -15,6 +15,7 @@ Classic `tree` output is useful, but `trls` focuses on modern developer workflow
|
|
|
15
15
|
- `prompt` output for AI-friendly, copy-pasteable directory snapshots
|
|
16
16
|
- snapshot diff by default, so each run shows what changed since last time
|
|
17
17
|
- `-c` clipboard copy for a compact, paste-ready LLM format
|
|
18
|
+
- `--skill` output for agent workflows, with automatic clipboard copy
|
|
18
19
|
- always-on progress feedback during scans in interactive terminals
|
|
19
20
|
- richer colors for common media, office, and data files
|
|
20
21
|
- `text` output for universal shell compatibility
|
|
@@ -51,6 +52,12 @@ Copy a compact LLM-friendly version to the clipboard while keeping the normal te
|
|
|
51
52
|
trls . -c
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+
Print the built-in agent skill text and copy it to the clipboard:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
trls --skill
|
|
59
|
+
```
|
|
60
|
+
|
|
54
61
|
In interactive terminals, `trls` also shows live scan progress so large trees do not feel stuck.
|
|
55
62
|
|
|
56
63
|
Snapshots are saved automatically after each run. If you want to refresh the baseline without showing any diff markers, force-save it explicitly:
|
|
@@ -180,7 +187,7 @@ Diff markers:
|
|
|
180
187
|
- `-` removed file or directory
|
|
181
188
|
- `~` modified file or directory
|
|
182
189
|
|
|
183
|
-
Current behavior in `v0.
|
|
190
|
+
Current behavior in `v0.5.0`:
|
|
184
191
|
|
|
185
192
|
- every successful run updates the latest snapshot for that path
|
|
186
193
|
- default `trls` output compares against the previous run for that path
|
|
@@ -202,6 +209,38 @@ Clipboard behavior:
|
|
|
202
209
|
- the clipboard payload is intentionally different from the terminal render to save tokens
|
|
203
210
|
- WSL-aware clipboard fallbacks are attempted before reporting an error
|
|
204
211
|
|
|
212
|
+
## Agent Skill Output
|
|
213
|
+
|
|
214
|
+
Use `trls -skill` or `trls --skill` to print a built-in English skill block for Cursor-style agents and copy it to the clipboard automatically.
|
|
215
|
+
|
|
216
|
+
Skill behavior:
|
|
217
|
+
|
|
218
|
+
- prints a ready-to-paste agent instruction block to stdout
|
|
219
|
+
- copies the same text to the clipboard automatically
|
|
220
|
+
- tells agents to prefer `trls` for file structure analysis before broad manual exploration
|
|
221
|
+
- suggests `--format prompt`, `--depth`, `--ignore`, and `-diff` as the primary workflow
|
|
222
|
+
|
|
223
|
+
Recommended agent command order:
|
|
224
|
+
|
|
225
|
+
1. Run `trls <path> --format prompt` for the first structure pass.
|
|
226
|
+
2. Add `--depth <n>` if the repository is too large.
|
|
227
|
+
3. Add repeated `--ignore <pattern>` options to remove noisy folders such as `.git`, `node_modules`, `dist`, or build output.
|
|
228
|
+
4. Add `--hidden` only when hidden files are likely relevant.
|
|
229
|
+
5. Use `-diff` when you want to compare against the previous snapshot.
|
|
230
|
+
6. Use `-c` when you want a compact clipboard-oriented summary.
|
|
231
|
+
|
|
232
|
+
Common agent examples:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
trls . --format prompt
|
|
236
|
+
trls . --format prompt --depth 2
|
|
237
|
+
trls . --format prompt --ignore .git --ignore node_modules --ignore dist
|
|
238
|
+
trls src --format prompt --depth 3
|
|
239
|
+
trls . -diff --format prompt
|
|
240
|
+
trls . -c
|
|
241
|
+
trls --skill
|
|
242
|
+
```
|
|
243
|
+
|
|
205
244
|
## Progress And Color UX
|
|
206
245
|
|
|
207
246
|
- interactive runs show a transient progress indicator while scanning
|
|
@@ -210,7 +249,7 @@ Clipboard behavior:
|
|
|
210
249
|
- spreadsheet and office files such as `.xlsx`, `.csv`, `.docx`, and `.pptx` have their own color family
|
|
211
250
|
- data-oriented files such as `.parquet` and `.ipynb` are also colorized
|
|
212
251
|
|
|
213
|
-
## CLI contract for `v0.
|
|
252
|
+
## CLI contract for `v0.5.0`
|
|
214
253
|
|
|
215
254
|
The first public release guarantees:
|
|
216
255
|
|
|
@@ -223,6 +262,7 @@ The first public release guarantees:
|
|
|
223
262
|
- snapshots are automatically updated after successful runs
|
|
224
263
|
- snapshot diffing is available via `-save`/`--save-snapshot`, `-diff`/`--diff-last`, `-compare`/`--compare-with`, and `-update`/`--update-snapshot`
|
|
225
264
|
- clipboard copy is available via `-c`/`--copy`
|
|
265
|
+
- agent skill output is available via `-skill`/`--skill`
|
|
226
266
|
- interactive scans show live progress feedback
|
|
227
267
|
- directories are listed before files and names are sorted case-insensitively
|
|
228
268
|
- unreadable directories are reported in the output instead of crashing the renderers
|
|
@@ -264,6 +304,7 @@ Recommended flow:
|
|
|
264
304
|
2. Verify `pip install`, `trls --version`, and one real CLI example.
|
|
265
305
|
3. Publish the tagged release to PyPI with Trusted Publishing.
|
|
266
306
|
|
|
307
|
+
.venv\Scripts\Activate.ps1
|
|
267
308
|
Remove-Item -Recurse -Force .\dist
|
|
268
309
|
python -m pytest
|
|
269
310
|
python -m build
|
|
@@ -6,7 +6,7 @@ This repository is prepared for a CLI-first PyPI release.
|
|
|
6
6
|
|
|
7
7
|
- distribution name: `trls-cli`
|
|
8
8
|
- import package: `trls`
|
|
9
|
-
- version: `0.
|
|
9
|
+
- version: `0.5.0`
|
|
10
10
|
- release channel: TestPyPI first, then PyPI
|
|
11
11
|
|
|
12
12
|
At the time of preparation, `https://pypi.org/project/trls-cli/` should be re-checked right before uploading.
|
|
@@ -79,8 +79,8 @@ The workflow already expects the `pypi` environment in GitHub Actions.
|
|
|
79
79
|
After Trusted Publishing is configured:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
git tag v0.
|
|
83
|
-
git push origin v0.
|
|
82
|
+
git tag v0.5.0
|
|
83
|
+
git push origin v0.5.0
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
The GitHub Actions workflow will:
|
|
@@ -97,4 +97,4 @@ The GitHub Actions workflow will:
|
|
|
97
97
|
3. Add the real repository URLs to `pyproject.toml`.
|
|
98
98
|
4. Ensure README examples still match current CLI behavior.
|
|
99
99
|
5. Upload to TestPyPI and validate installation.
|
|
100
|
-
6. Push the `v0.
|
|
100
|
+
6. Push the `v0.5.0` tag to trigger the production release.
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
from typing import Sequence
|
|
7
6
|
|
|
8
7
|
from rich.console import Console
|
|
@@ -10,6 +9,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
|
10
9
|
|
|
11
10
|
from trls import __version__
|
|
12
11
|
from trls.clipboard import copy_text
|
|
12
|
+
from trls.paths import normalize_input_path
|
|
13
13
|
from trls.renderers import (
|
|
14
14
|
build_rich_tree,
|
|
15
15
|
render_compact_copy,
|
|
@@ -18,6 +18,7 @@ from trls.renderers import (
|
|
|
18
18
|
render_prompt,
|
|
19
19
|
render_text,
|
|
20
20
|
)
|
|
21
|
+
from trls.skill import AGENT_SKILL_TEXT
|
|
21
22
|
from trls.snapshot import load_last_snapshot, load_snapshot, save_last_snapshot
|
|
22
23
|
from trls.tree import DEFAULT_IGNORE_PATTERNS, diff_trees, scan_tree
|
|
23
24
|
|
|
@@ -90,6 +91,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
90
91
|
action="store_true",
|
|
91
92
|
help="Copy a compact LLM-friendly version to the clipboard after rendering.",
|
|
92
93
|
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-skill",
|
|
96
|
+
"--skill",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help="Print the built-in agent skill text and copy it to the clipboard.",
|
|
99
|
+
)
|
|
93
100
|
parser.add_argument(
|
|
94
101
|
"--version",
|
|
95
102
|
action="version",
|
|
@@ -102,6 +109,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
102
109
|
parser = build_parser()
|
|
103
110
|
args = parser.parse_args(argv)
|
|
104
111
|
|
|
112
|
+
if args.skill:
|
|
113
|
+
print(AGENT_SKILL_TEXT)
|
|
114
|
+
try:
|
|
115
|
+
copy_text(AGENT_SKILL_TEXT)
|
|
116
|
+
except RuntimeError as exc:
|
|
117
|
+
print(f"Failed to copy to clipboard: {exc}", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
return 0
|
|
120
|
+
|
|
105
121
|
if args.depth is not None and args.depth < 0:
|
|
106
122
|
parser.error("--depth must be greater than or equal to 0")
|
|
107
123
|
if args.diff_last and args.compare_with:
|
|
@@ -114,7 +130,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
114
130
|
if args.update_snapshot and not (args.diff_last or args.compare_with):
|
|
115
131
|
parser.error("--update-snapshot requires -diff/--diff-last or -compare/--compare-with")
|
|
116
132
|
|
|
117
|
-
root_path =
|
|
133
|
+
root_path = normalize_input_path(args.path)
|
|
118
134
|
needs_fingerprints = True
|
|
119
135
|
|
|
120
136
|
current_tree = _scan_tree_with_progress(
|
|
@@ -1,108 +1,98 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
input=text,
|
|
100
|
-
text=True,
|
|
101
|
-
check=True,
|
|
102
|
-
capture_output=True,
|
|
103
|
-
)
|
|
104
|
-
except FileNotFoundError as exc:
|
|
105
|
-
raise RuntimeError(f"Clipboard command not found: {command[0]}") from exc
|
|
106
|
-
except subprocess.CalledProcessError as exc:
|
|
107
|
-
stderr = exc.stderr.strip() if exc.stderr else "unknown clipboard error"
|
|
108
|
-
raise RuntimeError(f"Clipboard command failed: {stderr}") from exc
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from trls.paths import is_wsl
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def copy_text(text: str) -> None:
|
|
12
|
+
backends = _clipboard_backends()
|
|
13
|
+
if not backends:
|
|
14
|
+
raise RuntimeError(
|
|
15
|
+
"No supported clipboard command was found. "
|
|
16
|
+
"On Linux/WSL, install wl-clipboard, xclip, or xsel."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
errors: list[str] = []
|
|
20
|
+
for backend_name, command in backends:
|
|
21
|
+
try:
|
|
22
|
+
_run_clipboard_command(command, text)
|
|
23
|
+
return
|
|
24
|
+
except RuntimeError as exc:
|
|
25
|
+
errors.append(f"{backend_name}: {exc}")
|
|
26
|
+
|
|
27
|
+
attempted_names = ", ".join(name for name, _ in backends)
|
|
28
|
+
error_details = "; ".join(errors)
|
|
29
|
+
raise RuntimeError(
|
|
30
|
+
f"No clipboard backend succeeded. Tried: {attempted_names}. {error_details}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _clipboard_backends() -> list[tuple[str, list[str]]]:
|
|
35
|
+
system = platform.system()
|
|
36
|
+
backends: list[tuple[str, list[str]]] = []
|
|
37
|
+
|
|
38
|
+
if system == "Windows":
|
|
39
|
+
command = _resolve_command("clip")
|
|
40
|
+
if command:
|
|
41
|
+
backends.append(("clip", [command]))
|
|
42
|
+
return backends
|
|
43
|
+
|
|
44
|
+
if system == "Darwin":
|
|
45
|
+
command = _resolve_command("pbcopy")
|
|
46
|
+
if command:
|
|
47
|
+
backends.append(("pbcopy", [command]))
|
|
48
|
+
return backends
|
|
49
|
+
|
|
50
|
+
if is_wsl():
|
|
51
|
+
clip_exe = _resolve_command("clip.exe") or _resolve_windows_clip_path()
|
|
52
|
+
if clip_exe:
|
|
53
|
+
backends.append(("clip.exe", [clip_exe]))
|
|
54
|
+
powershell_exe = _resolve_command("powershell.exe")
|
|
55
|
+
if powershell_exe:
|
|
56
|
+
backends.append(
|
|
57
|
+
(
|
|
58
|
+
"powershell.exe Set-Clipboard",
|
|
59
|
+
[powershell_exe, "-NoProfile", "-Command", "Set-Clipboard"],
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
wl_copy = _resolve_command("wl-copy")
|
|
64
|
+
if wl_copy:
|
|
65
|
+
backends.append(("wl-copy", [wl_copy]))
|
|
66
|
+
|
|
67
|
+
xclip = _resolve_command("xclip")
|
|
68
|
+
if xclip:
|
|
69
|
+
backends.append(("xclip", [xclip, "-selection", "clipboard"]))
|
|
70
|
+
|
|
71
|
+
xsel = _resolve_command("xsel")
|
|
72
|
+
if xsel:
|
|
73
|
+
backends.append(("xsel", [xsel, "--clipboard", "--input"]))
|
|
74
|
+
|
|
75
|
+
return backends
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_command(name: str) -> str | None:
|
|
79
|
+
return shutil.which(name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _resolve_windows_clip_path() -> str | None:
|
|
83
|
+
candidate = Path("/mnt/c/Windows/System32/clip.exe")
|
|
84
|
+
return str(candidate) if candidate.exists() else None
|
|
85
|
+
def _run_clipboard_command(command: list[str], text: str) -> None:
|
|
86
|
+
try:
|
|
87
|
+
subprocess.run(
|
|
88
|
+
command,
|
|
89
|
+
input=text,
|
|
90
|
+
text=True,
|
|
91
|
+
check=True,
|
|
92
|
+
capture_output=True,
|
|
93
|
+
)
|
|
94
|
+
except FileNotFoundError as exc:
|
|
95
|
+
raise RuntimeError(f"Clipboard command not found: {command[0]}") from exc
|
|
96
|
+
except subprocess.CalledProcessError as exc:
|
|
97
|
+
stderr = exc.stderr.strip() if exc.stderr else "unknown clipboard error"
|
|
98
|
+
raise RuntimeError(f"Clipboard command failed: {stderr}") from exc
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path, PureWindowsPath
|
|
7
|
+
|
|
8
|
+
_WINDOWS_DRIVE_PATH_RE = re.compile(r"^([a-zA-Z]):(?:[\\/](.*))?$")
|
|
9
|
+
_WSL_MOUNT_PATH_RE = re.compile(r"^/mnt(?:/host)?/([a-zA-Z])(?:/(.*))?$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_wsl() -> bool:
|
|
13
|
+
if os.environ.get("WSL_DISTRO_NAME"):
|
|
14
|
+
return True
|
|
15
|
+
try:
|
|
16
|
+
return "microsoft" in Path("/proc/version").read_text(encoding="utf-8").lower()
|
|
17
|
+
except OSError:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_input_path(path_value: str | Path) -> Path:
|
|
22
|
+
translated = translate_input_path(path_value)
|
|
23
|
+
return Path(translated).expanduser().resolve()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def translate_input_path(path_value: str | Path) -> str:
|
|
27
|
+
raw_path = os.fspath(path_value)
|
|
28
|
+
mounted_windows_path = _parse_wsl_mounted_windows_path(raw_path)
|
|
29
|
+
|
|
30
|
+
if platform.system() == "Windows":
|
|
31
|
+
if mounted_windows_path is not None:
|
|
32
|
+
drive, parts = mounted_windows_path
|
|
33
|
+
return _build_windows_path(drive, parts)
|
|
34
|
+
return raw_path
|
|
35
|
+
|
|
36
|
+
if mounted_windows_path is not None:
|
|
37
|
+
drive, parts = mounted_windows_path
|
|
38
|
+
translated_path = _build_wsl_mount_path(drive, parts)
|
|
39
|
+
mount_root = Path("/mnt") / drive.lower()
|
|
40
|
+
if is_wsl() or mount_root.exists():
|
|
41
|
+
return translated_path
|
|
42
|
+
return raw_path
|
|
43
|
+
|
|
44
|
+
windows_drive_path = _parse_windows_drive_path(raw_path)
|
|
45
|
+
if windows_drive_path is None:
|
|
46
|
+
return raw_path
|
|
47
|
+
|
|
48
|
+
drive, parts = windows_drive_path
|
|
49
|
+
translated_path = _build_wsl_mount_path(drive, parts)
|
|
50
|
+
mount_root = Path("/mnt") / drive.lower()
|
|
51
|
+
if is_wsl() or mount_root.exists():
|
|
52
|
+
return translated_path
|
|
53
|
+
return raw_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def canonicalize_path_identity(path_value: str | Path) -> str:
|
|
57
|
+
normalized_path = normalize_input_path(path_value)
|
|
58
|
+
windows_identity = _windows_identity(normalized_path)
|
|
59
|
+
if windows_identity is not None:
|
|
60
|
+
return f"win:{windows_identity}"
|
|
61
|
+
return f"posix:{normalized_path.as_posix()}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_windows_drive_path(raw_path: str) -> tuple[str, list[str]] | None:
|
|
65
|
+
match = _WINDOWS_DRIVE_PATH_RE.match(raw_path)
|
|
66
|
+
if match is None:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
drive = match.group(1)
|
|
70
|
+
remainder = match.group(2) or ""
|
|
71
|
+
parts = [part for part in re.split(r"[\\/]+", remainder) if part]
|
|
72
|
+
return drive, parts
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse_wsl_mounted_windows_path(raw_path: str) -> tuple[str, list[str]] | None:
|
|
76
|
+
match = _WSL_MOUNT_PATH_RE.match(raw_path)
|
|
77
|
+
if match is None:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
drive = match.group(1)
|
|
81
|
+
remainder = match.group(2) or ""
|
|
82
|
+
parts = [part for part in remainder.split("/") if part]
|
|
83
|
+
return drive, parts
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _build_windows_path(drive: str, parts: list[str]) -> str:
|
|
87
|
+
return str(PureWindowsPath(f"{drive.upper()}:\\", *parts))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _build_wsl_mount_path(drive: str, parts: list[str]) -> str:
|
|
91
|
+
mounted_path = Path("/mnt") / drive.lower()
|
|
92
|
+
for part in parts:
|
|
93
|
+
mounted_path /= part
|
|
94
|
+
return mounted_path.as_posix()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _windows_identity(path: Path) -> str | None:
|
|
98
|
+
windows_drive_path = _parse_windows_drive_path(str(path))
|
|
99
|
+
if windows_drive_path is not None:
|
|
100
|
+
drive, parts = windows_drive_path
|
|
101
|
+
return _build_windows_identity(drive, parts)
|
|
102
|
+
|
|
103
|
+
mounted_windows_path = _parse_wsl_mounted_windows_path(path.as_posix())
|
|
104
|
+
if mounted_windows_path is not None:
|
|
105
|
+
drive, parts = mounted_windows_path
|
|
106
|
+
return _build_windows_identity(drive, parts)
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _build_windows_identity(drive: str, parts: list[str]) -> str:
|
|
112
|
+
if not parts:
|
|
113
|
+
return drive.lower()
|
|
114
|
+
return f"{drive.lower()}/{'/'.join(parts)}"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
AGENT_SKILL_TEXT = """Use `trls` first when you need to understand a project's file structure.
|
|
4
|
+
|
|
5
|
+
Priority rules:
|
|
6
|
+
- Prefer `trls` over ad hoc `ls`, `tree`, or broad recursive file listing when your goal is repository structure analysis.
|
|
7
|
+
- Start with `trls <path> --format prompt` for an AI-friendly directory snapshot.
|
|
8
|
+
- If the tree is too large, rerun with `--depth <n>` before opening files.
|
|
9
|
+
- If the tree is noisy, rerun with repeated `--ignore <pattern>` arguments.
|
|
10
|
+
- Use `--hidden` only when hidden files are likely relevant.
|
|
11
|
+
- Use `-diff` when you need to inspect structure changes between runs.
|
|
12
|
+
- Use `-c` when you want a compact clipboard-friendly summary for prompts.
|
|
13
|
+
- Use `--format json` only when a downstream tool needs machine-readable output.
|
|
14
|
+
|
|
15
|
+
Recommended workflow:
|
|
16
|
+
1. Run `trls <target> --format prompt`.
|
|
17
|
+
2. If needed, narrow the scope with `trls <target> --format prompt --depth <n>`.
|
|
18
|
+
3. If needed, remove noise with repeated `--ignore` patterns such as `.git`, `node_modules`, `dist`, `build`, or caches.
|
|
19
|
+
4. Review the tree and identify the smallest relevant subdirectory.
|
|
20
|
+
5. Run `trls` again on that narrower path before opening many files.
|
|
21
|
+
6. Only then open specific files or run targeted code searches.
|
|
22
|
+
|
|
23
|
+
Argument guide:
|
|
24
|
+
- `--format prompt`: default choice for agent-oriented structure inspection
|
|
25
|
+
- `--depth <n>`: limit traversal when the repository is large
|
|
26
|
+
- `--ignore <pattern>`: suppress noisy paths; can be repeated
|
|
27
|
+
- `--hidden`: include hidden files and directories when required
|
|
28
|
+
- `-diff`: compare the current tree with the previous snapshot
|
|
29
|
+
- `-c`: copy a compact prompt-oriented summary to the clipboard
|
|
30
|
+
- `--format json`: use for automation, not for first-pass exploration
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
- `trls . --format prompt`
|
|
34
|
+
- `trls . --format prompt --depth 2`
|
|
35
|
+
- `trls . --format prompt --ignore .git --ignore node_modules --ignore dist`
|
|
36
|
+
- `trls src --format prompt --depth 3 -diff`
|
|
37
|
+
- `trls . -c`
|
|
38
|
+
|
|
39
|
+
Goal:
|
|
40
|
+
Use `trls` to reduce noisy exploration, keep structural analysis consistent, and produce prompt-ready project snapshots before deeper file inspection.
|
|
41
|
+
"""
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from trls.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
normalized_root =
|
|
18
|
-
key = hashlib.sha256(normalized_root.encode("utf-8")).hexdigest()[:16]
|
|
19
|
-
return snapshot_storage_dir() / f"{key}.json"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def snapshot_storage_dir() -> Path:
|
|
23
|
-
configured_dir = os.environ.get(SNAPSHOT_DIR_ENV)
|
|
24
|
-
if configured_dir:
|
|
25
|
-
return
|
|
26
|
-
return Path.home() / ".trls" / "snapshots"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def save_snapshot(snapshot_path: str | Path, root: str | Path, tree: TreeNode) -> Path:
|
|
30
|
-
destination =
|
|
31
|
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
-
root_path =
|
|
33
|
-
payload = {
|
|
34
|
-
"snapshot_version": SNAPSHOT_VERSION,
|
|
35
|
-
"root_path": str(root_path),
|
|
36
|
-
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
37
|
-
"tree": tree.to_dict(include_fingerprint=True),
|
|
38
|
-
}
|
|
39
|
-
destination.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
40
|
-
return destination
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def save_last_snapshot(root: str | Path, tree: TreeNode) -> Path:
|
|
44
|
-
return save_snapshot(default_snapshot_path(root), root, tree)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def load_snapshot(snapshot_path: str | Path) -> TreeNode:
|
|
48
|
-
path =
|
|
49
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
-
return TreeNode.from_dict(payload["tree"])
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def load_last_snapshot(root: str | Path) -> TreeNode:
|
|
54
|
-
snapshot_path = default_snapshot_path(root)
|
|
55
|
-
if not snapshot_path.exists():
|
|
56
|
-
raise FileNotFoundError(
|
|
57
|
-
f"No saved snapshot for {
|
|
58
|
-
"Run trls with --save-snapshot first."
|
|
59
|
-
)
|
|
60
|
-
return load_snapshot(snapshot_path)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from trls.paths import canonicalize_path_identity, normalize_input_path
|
|
10
|
+
from trls.tree import TreeNode
|
|
11
|
+
|
|
12
|
+
SNAPSHOT_VERSION = 1
|
|
13
|
+
SNAPSHOT_DIR_ENV = "TRLS_SNAPSHOT_DIR"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def default_snapshot_path(root: str | Path) -> Path:
|
|
17
|
+
normalized_root = canonicalize_path_identity(root)
|
|
18
|
+
key = hashlib.sha256(normalized_root.encode("utf-8")).hexdigest()[:16]
|
|
19
|
+
return snapshot_storage_dir() / f"{key}.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def snapshot_storage_dir() -> Path:
|
|
23
|
+
configured_dir = os.environ.get(SNAPSHOT_DIR_ENV)
|
|
24
|
+
if configured_dir:
|
|
25
|
+
return normalize_input_path(configured_dir)
|
|
26
|
+
return Path.home() / ".trls" / "snapshots"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def save_snapshot(snapshot_path: str | Path, root: str | Path, tree: TreeNode) -> Path:
|
|
30
|
+
destination = normalize_input_path(snapshot_path)
|
|
31
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
root_path = normalize_input_path(root)
|
|
33
|
+
payload = {
|
|
34
|
+
"snapshot_version": SNAPSHOT_VERSION,
|
|
35
|
+
"root_path": str(root_path),
|
|
36
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
37
|
+
"tree": tree.to_dict(include_fingerprint=True),
|
|
38
|
+
}
|
|
39
|
+
destination.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
40
|
+
return destination
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_last_snapshot(root: str | Path, tree: TreeNode) -> Path:
|
|
44
|
+
return save_snapshot(default_snapshot_path(root), root, tree)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_snapshot(snapshot_path: str | Path) -> TreeNode:
|
|
48
|
+
path = normalize_input_path(snapshot_path)
|
|
49
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
return TreeNode.from_dict(payload["tree"])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_last_snapshot(root: str | Path) -> TreeNode:
|
|
54
|
+
snapshot_path = default_snapshot_path(root)
|
|
55
|
+
if not snapshot_path.exists():
|
|
56
|
+
raise FileNotFoundError(
|
|
57
|
+
f"No saved snapshot for {normalize_input_path(root)}. "
|
|
58
|
+
"Run trls with --save-snapshot first."
|
|
59
|
+
)
|
|
60
|
+
return load_snapshot(snapshot_path)
|
|
@@ -6,6 +6,8 @@ from fnmatch import fnmatch
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Callable, Literal
|
|
8
8
|
|
|
9
|
+
from trls.paths import normalize_input_path
|
|
10
|
+
|
|
9
11
|
DEFAULT_IGNORE_PATTERNS = (".venv", "dist", "__pycache__")
|
|
10
12
|
DiffStatus = Literal["added", "removed", "modified", "unchanged"]
|
|
11
13
|
|
|
@@ -74,7 +76,7 @@ def scan_tree(
|
|
|
74
76
|
include_fingerprints: bool = False,
|
|
75
77
|
on_visit: Callable[[Path], None] | None = None,
|
|
76
78
|
) -> TreeNode:
|
|
77
|
-
root_path =
|
|
79
|
+
root_path = normalize_input_path(root)
|
|
78
80
|
if not root_path.exists():
|
|
79
81
|
raise FileNotFoundError(f"Path does not exist: {root}")
|
|
80
82
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
4
5
|
|
|
5
6
|
from trls import __version__
|
|
6
7
|
import trls.clipboard as clipboard_module
|
|
7
8
|
import trls.cli as cli_module
|
|
9
|
+
import trls.snapshot as snapshot_module
|
|
8
10
|
from trls.cli import main
|
|
11
|
+
from trls.skill import AGENT_SKILL_TEXT
|
|
9
12
|
from trls.snapshot import save_snapshot
|
|
10
13
|
from trls.tree import scan_tree
|
|
11
14
|
|
|
@@ -97,6 +100,111 @@ def test_cli_json_output(capsys, tmp_path):
|
|
|
97
100
|
assert any(child["name"] == "src" for child in payload["children"])
|
|
98
101
|
|
|
99
102
|
|
|
103
|
+
def test_cli_skill_prints_and_copies_without_scanning(capsys, monkeypatch):
|
|
104
|
+
copied = {}
|
|
105
|
+
|
|
106
|
+
def fake_copy_text(text):
|
|
107
|
+
copied["text"] = text
|
|
108
|
+
|
|
109
|
+
monkeypatch.setattr(cli_module, "copy_text", fake_copy_text)
|
|
110
|
+
monkeypatch.setattr(
|
|
111
|
+
cli_module,
|
|
112
|
+
"normalize_input_path",
|
|
113
|
+
lambda raw_path: (_ for _ in ()).throw(AssertionError("skill mode should not scan")),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
exit_code = main(["--skill"])
|
|
117
|
+
captured = capsys.readouterr()
|
|
118
|
+
|
|
119
|
+
assert exit_code == 0
|
|
120
|
+
assert captured.out == AGENT_SKILL_TEXT + "\n"
|
|
121
|
+
assert copied["text"] == AGENT_SKILL_TEXT
|
|
122
|
+
|
|
123
|
+
@pytest.mark.parametrize("flag", ["-skill", "--skill"])
|
|
124
|
+
def test_cli_skill_aliases_are_supported(capsys, monkeypatch, flag):
|
|
125
|
+
monkeypatch.setattr(cli_module, "copy_text", lambda text: None)
|
|
126
|
+
|
|
127
|
+
exit_code = main([flag])
|
|
128
|
+
captured = capsys.readouterr()
|
|
129
|
+
|
|
130
|
+
assert exit_code == 0
|
|
131
|
+
assert "Use `trls` first" in captured.out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_cli_short_s_is_not_skill_alias(capsys):
|
|
135
|
+
with pytest.raises(SystemExit):
|
|
136
|
+
main(["-s"])
|
|
137
|
+
|
|
138
|
+
captured = capsys.readouterr()
|
|
139
|
+
|
|
140
|
+
assert "ambiguous option: -s" in captured.err
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_cli_skill_reports_clipboard_failure(capsys, monkeypatch):
|
|
144
|
+
def failing_copy_text(text):
|
|
145
|
+
raise RuntimeError("clipboard unavailable")
|
|
146
|
+
|
|
147
|
+
monkeypatch.setattr(cli_module, "copy_text", failing_copy_text)
|
|
148
|
+
|
|
149
|
+
exit_code = main(["--skill"])
|
|
150
|
+
captured = capsys.readouterr()
|
|
151
|
+
|
|
152
|
+
assert exit_code == 1
|
|
153
|
+
assert captured.out == AGENT_SKILL_TEXT + "\n"
|
|
154
|
+
assert "Failed to copy to clipboard: clipboard unavailable" in captured.err
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_cli_normalizes_foreign_input_path_before_scan(capsys, monkeypatch, tmp_path):
|
|
158
|
+
root = create_project(tmp_path)
|
|
159
|
+
normalized_inputs = []
|
|
160
|
+
|
|
161
|
+
def fake_normalize_input_path(raw_path):
|
|
162
|
+
normalized_inputs.append(raw_path)
|
|
163
|
+
return root
|
|
164
|
+
|
|
165
|
+
monkeypatch.setattr(cli_module, "normalize_input_path", fake_normalize_input_path)
|
|
166
|
+
|
|
167
|
+
exit_code = main(["/mnt/host/c/Users/user/Desktop/ZYB/genewa/trls", "--format", "text"])
|
|
168
|
+
captured = capsys.readouterr()
|
|
169
|
+
|
|
170
|
+
assert exit_code == 0
|
|
171
|
+
assert normalized_inputs == ["/mnt/host/c/Users/user/Desktop/ZYB/genewa/trls"]
|
|
172
|
+
assert "demo/" in captured.out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_cli_snapshot_dir_env_uses_normalized_path(capsys, monkeypatch, tmp_path):
|
|
176
|
+
root = create_project(tmp_path)
|
|
177
|
+
root_input = "/mnt/c/Users/user/Desktop/ZYB/genewa/demo"
|
|
178
|
+
snapshot_env = "/mnt/host/c/Users/user/.trls/snapshots"
|
|
179
|
+
normalized_snapshot_dir = tmp_path / "snapshots"
|
|
180
|
+
normalized_calls = []
|
|
181
|
+
|
|
182
|
+
def fake_cli_normalize_input_path(raw_path):
|
|
183
|
+
normalized_calls.append(raw_path)
|
|
184
|
+
if raw_path == root_input:
|
|
185
|
+
return root
|
|
186
|
+
return Path(raw_path).resolve()
|
|
187
|
+
|
|
188
|
+
def fake_snapshot_normalize_input_path(raw_path):
|
|
189
|
+
normalized_calls.append(raw_path)
|
|
190
|
+
if raw_path == snapshot_env:
|
|
191
|
+
return normalized_snapshot_dir
|
|
192
|
+
if raw_path == root_input:
|
|
193
|
+
return root
|
|
194
|
+
return Path(raw_path).resolve()
|
|
195
|
+
|
|
196
|
+
monkeypatch.setenv("TRLS_SNAPSHOT_DIR", snapshot_env)
|
|
197
|
+
monkeypatch.setattr(cli_module, "normalize_input_path", fake_cli_normalize_input_path)
|
|
198
|
+
monkeypatch.setattr(snapshot_module, "normalize_input_path", fake_snapshot_normalize_input_path)
|
|
199
|
+
|
|
200
|
+
exit_code = main([root_input, "--format", "prompt"])
|
|
201
|
+
capsys.readouterr()
|
|
202
|
+
|
|
203
|
+
assert exit_code == 0
|
|
204
|
+
assert snapshot_env in normalized_calls
|
|
205
|
+
assert any(normalized_snapshot_dir.iterdir())
|
|
206
|
+
|
|
207
|
+
|
|
100
208
|
def test_cli_copy_writes_compact_clipboard_output(capsys, monkeypatch, tmp_path):
|
|
101
209
|
root = create_project(tmp_path)
|
|
102
210
|
copied = {}
|
|
@@ -319,7 +427,7 @@ def test_cli_diff_last_first_run_creates_baseline_without_error(capsys, monkeypa
|
|
|
319
427
|
def test_copy_text_uses_clip_exe_in_wsl(monkeypatch):
|
|
320
428
|
calls = []
|
|
321
429
|
|
|
322
|
-
monkeypatch.setattr(clipboard_module, "
|
|
430
|
+
monkeypatch.setattr(clipboard_module, "is_wsl", lambda: True)
|
|
323
431
|
monkeypatch.setattr(clipboard_module.platform, "system", lambda: "Linux")
|
|
324
432
|
monkeypatch.setattr(
|
|
325
433
|
clipboard_module,
|
|
@@ -342,7 +450,7 @@ def test_copy_text_uses_clip_exe_in_wsl(monkeypatch):
|
|
|
342
450
|
def test_copy_text_reports_install_hint_when_no_backend_exists(monkeypatch):
|
|
343
451
|
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)
|
|
344
452
|
monkeypatch.setattr(clipboard_module.platform, "system", lambda: "Linux")
|
|
345
|
-
monkeypatch.setattr(clipboard_module, "
|
|
453
|
+
monkeypatch.setattr(clipboard_module, "is_wsl", lambda: False)
|
|
346
454
|
monkeypatch.setattr(clipboard_module, "_resolve_command", lambda name: None)
|
|
347
455
|
monkeypatch.setattr(clipboard_module, "_resolve_windows_clip_path", lambda: None)
|
|
348
456
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
|
|
4
|
+
import trls.paths as paths_module
|
|
5
|
+
import trls.snapshot as snapshot_module
|
|
3
6
|
from trls import (
|
|
4
7
|
build_rich_tree,
|
|
5
8
|
render_compact_copy,
|
|
@@ -9,7 +12,8 @@ from trls import (
|
|
|
9
12
|
render_text,
|
|
10
13
|
scan_tree,
|
|
11
14
|
)
|
|
12
|
-
from trls.
|
|
15
|
+
from trls.paths import canonicalize_path_identity
|
|
16
|
+
from trls.snapshot import default_snapshot_path, load_snapshot, save_snapshot, snapshot_storage_dir
|
|
13
17
|
from trls.tree import diff_trees
|
|
14
18
|
|
|
15
19
|
|
|
@@ -95,6 +99,88 @@ def test_rich_renderer_applies_styles_by_node_type(tmp_path):
|
|
|
95
99
|
assert labels["sheet.xlsx"] == "bright_green"
|
|
96
100
|
|
|
97
101
|
|
|
102
|
+
def test_translate_input_path_converts_wsl_mount_paths_for_windows_runtime(monkeypatch):
|
|
103
|
+
monkeypatch.setattr(paths_module.platform, "system", lambda: "Windows")
|
|
104
|
+
|
|
105
|
+
assert (
|
|
106
|
+
paths_module.translate_input_path("/mnt/c/Users/test/project")
|
|
107
|
+
== r"C:\Users\test\project"
|
|
108
|
+
)
|
|
109
|
+
assert (
|
|
110
|
+
paths_module.translate_input_path("/mnt/host/d/Work/demo")
|
|
111
|
+
== r"D:\Work\demo"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_translate_input_path_converts_windows_paths_for_wsl_runtime(monkeypatch):
|
|
116
|
+
monkeypatch.setattr(paths_module.platform, "system", lambda: "Linux")
|
|
117
|
+
monkeypatch.setattr(paths_module, "is_wsl", lambda: True)
|
|
118
|
+
|
|
119
|
+
assert (
|
|
120
|
+
paths_module.translate_input_path(r"C:\Users\test\project")
|
|
121
|
+
== "/mnt/c/Users/test/project"
|
|
122
|
+
)
|
|
123
|
+
assert paths_module.translate_input_path("D:/Work/demo") == "/mnt/d/Work/demo"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_translate_input_path_normalizes_host_mount_paths_for_wsl_runtime(monkeypatch):
|
|
127
|
+
monkeypatch.setattr(paths_module.platform, "system", lambda: "Linux")
|
|
128
|
+
monkeypatch.setattr(paths_module, "is_wsl", lambda: True)
|
|
129
|
+
|
|
130
|
+
assert (
|
|
131
|
+
paths_module.translate_input_path("/mnt/host/c/Users/test/project")
|
|
132
|
+
== "/mnt/c/Users/test/project"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_canonicalize_path_identity_matches_equivalent_aliases(monkeypatch, tmp_path):
|
|
137
|
+
project = create_sample_tree(tmp_path)
|
|
138
|
+
alias_map = {
|
|
139
|
+
os.fspath(project): project,
|
|
140
|
+
"/mnt/c/Users/test/project": project,
|
|
141
|
+
r"C:\Users\test\project": project,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
monkeypatch.setattr(
|
|
145
|
+
paths_module,
|
|
146
|
+
"normalize_input_path",
|
|
147
|
+
lambda raw_path: alias_map[os.fspath(raw_path)],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
mounted_identity = canonicalize_path_identity("/mnt/c/Users/test/project")
|
|
151
|
+
windows_identity = canonicalize_path_identity(r"C:\Users\test\project")
|
|
152
|
+
|
|
153
|
+
assert mounted_identity == windows_identity
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_snapshot_storage_dir_normalizes_foreign_env_path(monkeypatch, tmp_path):
|
|
157
|
+
normalized_snapshot_dir = tmp_path / "snapshots"
|
|
158
|
+
foreign_snapshot_dir = "/mnt/host/c/Users/user/.trls/snapshots"
|
|
159
|
+
|
|
160
|
+
monkeypatch.setenv(snapshot_module.SNAPSHOT_DIR_ENV, foreign_snapshot_dir)
|
|
161
|
+
monkeypatch.setattr(
|
|
162
|
+
snapshot_module,
|
|
163
|
+
"normalize_input_path",
|
|
164
|
+
lambda raw_path: normalized_snapshot_dir,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
assert snapshot_storage_dir() == normalized_snapshot_dir
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_default_snapshot_path_matches_equivalent_aliases(monkeypatch, tmp_path):
|
|
171
|
+
monkeypatch.setattr(snapshot_module, "snapshot_storage_dir", lambda: tmp_path)
|
|
172
|
+
monkeypatch.setattr(
|
|
173
|
+
snapshot_module,
|
|
174
|
+
"canonicalize_path_identity",
|
|
175
|
+
lambda raw_path: "win:c/Users/test/project",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
mounted_path = default_snapshot_path("/mnt/c/Users/test/project")
|
|
179
|
+
windows_path = default_snapshot_path(r"C:\Users\test\project")
|
|
180
|
+
|
|
181
|
+
assert mounted_path == windows_path
|
|
182
|
+
|
|
183
|
+
|
|
98
184
|
def test_snapshot_round_trip_and_diff_statuses(tmp_path):
|
|
99
185
|
project = create_sample_tree(tmp_path)
|
|
100
186
|
snapshot_path = tmp_path / "snapshot.json"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|