filetree-cli 0.1.0__py3-none-any.whl

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.
filetree/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """filetree - Generate ASCII or Markdown directory trees."""
2
+
3
+ __version__ = "0.1.0"
filetree/cli.py ADDED
@@ -0,0 +1,271 @@
1
+ """CLI for filetree - directory tree generator."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from fnmatch import fnmatch
7
+ from pathlib import Path
8
+ from typing import List, Optional, Set, Tuple
9
+
10
+ import click
11
+
12
+ from . import __version__
13
+
14
+ # Default patterns to ignore
15
+ DEFAULT_IGNORES = [
16
+ ".git",
17
+ ".svn",
18
+ ".hg",
19
+ "__pycache__",
20
+ "*.pyc",
21
+ ".DS_Store",
22
+ "node_modules",
23
+ ".venv",
24
+ "venv",
25
+ ".env",
26
+ "*.egg-info",
27
+ ".pytest_cache",
28
+ ".mypy_cache",
29
+ ".ruff_cache",
30
+ ".tox",
31
+ "dist",
32
+ "build",
33
+ ".idea",
34
+ ".vscode",
35
+ ]
36
+
37
+ # Tree characters
38
+ TREE_BRANCH = "├── "
39
+ TREE_LAST = "└── "
40
+ TREE_PIPE = "│ "
41
+ TREE_SPACE = " "
42
+
43
+
44
+ def should_ignore(name: str, path: Path, patterns: List[str]) -> bool:
45
+ """Check if a file/dir should be ignored based on patterns."""
46
+ for pattern in patterns:
47
+ if fnmatch(name, pattern):
48
+ return True
49
+ # Also check full path for patterns with /
50
+ if "/" in pattern and fnmatch(str(path), pattern):
51
+ return True
52
+ return True if name.startswith(".") and ".*" in patterns else False
53
+
54
+
55
+ def get_size(path: Path) -> int:
56
+ """Get file size, 0 for directories."""
57
+ try:
58
+ if path.is_file():
59
+ return path.stat().st_size
60
+ return 0
61
+ except (OSError, PermissionError):
62
+ return 0
63
+
64
+
65
+ def format_size(size: int) -> str:
66
+ """Format bytes to human readable."""
67
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
68
+ if size < 1024:
69
+ return f"{size:.1f}{unit}" if unit != "B" else f"{size}{unit}"
70
+ size /= 1024
71
+ return f"{size:.1f}PB"
72
+
73
+
74
+ def generate_tree(
75
+ root: Path,
76
+ prefix: str = "",
77
+ depth: int = -1,
78
+ current_depth: int = 0,
79
+ ignore_patterns: List[str] = None,
80
+ dirs_only: bool = False,
81
+ show_hidden: bool = False,
82
+ show_size: bool = False,
83
+ stats: dict = None,
84
+ ) -> List[str]:
85
+ """Generate tree lines for a directory."""
86
+ if ignore_patterns is None:
87
+ ignore_patterns = []
88
+
89
+ if stats is None:
90
+ stats = {"dirs": 0, "files": 0, "size": 0}
91
+
92
+ lines = []
93
+
94
+ # Check depth limit
95
+ if depth >= 0 and current_depth >= depth:
96
+ return lines
97
+
98
+ try:
99
+ entries = sorted(root.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
100
+ except PermissionError:
101
+ return lines
102
+
103
+ # Filter entries
104
+ filtered = []
105
+ for entry in entries:
106
+ name = entry.name
107
+
108
+ # Skip hidden unless requested
109
+ if not show_hidden and name.startswith("."):
110
+ continue
111
+
112
+ # Skip ignored patterns
113
+ skip = False
114
+ for pattern in ignore_patterns:
115
+ if fnmatch(name, pattern):
116
+ skip = True
117
+ break
118
+ if skip:
119
+ continue
120
+
121
+ # Skip files if dirs_only
122
+ if dirs_only and entry.is_file():
123
+ continue
124
+
125
+ filtered.append(entry)
126
+
127
+ # Generate tree lines
128
+ for i, entry in enumerate(filtered):
129
+ is_last = i == len(filtered) - 1
130
+ connector = TREE_LAST if is_last else TREE_BRANCH
131
+
132
+ # Build the line
133
+ line = prefix + connector + entry.name
134
+
135
+ if entry.is_dir():
136
+ line += "/"
137
+ stats["dirs"] += 1
138
+ else:
139
+ stats["files"] += 1
140
+ size = get_size(entry)
141
+ stats["size"] += size
142
+ if show_size:
143
+ line += f" ({format_size(size)})"
144
+
145
+ lines.append(line)
146
+
147
+ # Recurse into directories
148
+ if entry.is_dir():
149
+ extension = TREE_SPACE if is_last else TREE_PIPE
150
+ lines.extend(
151
+ generate_tree(
152
+ entry,
153
+ prefix=prefix + extension,
154
+ depth=depth,
155
+ current_depth=current_depth + 1,
156
+ ignore_patterns=ignore_patterns,
157
+ dirs_only=dirs_only,
158
+ show_hidden=show_hidden,
159
+ show_size=show_size,
160
+ stats=stats,
161
+ )
162
+ )
163
+
164
+ return lines
165
+
166
+
167
+ def tree_to_markdown(root_name: str, lines: List[str]) -> str:
168
+ """Convert tree lines to markdown code block."""
169
+ output = f"```\n{root_name}/\n"
170
+ output += "\n".join(lines)
171
+ output += "\n```"
172
+ return output
173
+
174
+
175
+ def tree_to_json(root: Path, lines: List[str], stats: dict) -> dict:
176
+ """Convert tree to JSON structure."""
177
+ return {
178
+ "root": str(root.absolute()),
179
+ "tree": [root.name + "/"] + lines,
180
+ "stats": {
181
+ "directories": stats["dirs"],
182
+ "files": stats["files"],
183
+ "total_size": stats["size"],
184
+ "total_size_human": format_size(stats["size"]),
185
+ },
186
+ }
187
+
188
+
189
+ @click.command()
190
+ @click.argument("path", default=".", type=click.Path(exists=True))
191
+ @click.option("-d", "--depth", default=-1, type=int, help="Maximum depth (-1 for unlimited)")
192
+ @click.option("-I", "--ignore", "ignores", multiple=True, help="Patterns to ignore (can repeat)")
193
+ @click.option("--no-default-ignore", is_flag=True, help="Don't use default ignore patterns")
194
+ @click.option("-a", "--all", "show_hidden", is_flag=True, help="Show hidden files/dirs")
195
+ @click.option("-D", "--dirs-only", is_flag=True, help="Show only directories")
196
+ @click.option("-s", "--size", "show_size", is_flag=True, help="Show file sizes")
197
+ @click.option("--stats", "show_stats", is_flag=True, help="Show summary statistics")
198
+ @click.option("--md", "--markdown", "markdown", is_flag=True, help="Output as markdown code block")
199
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
200
+ @click.option("--version", is_flag=True, help="Show version")
201
+ def main(
202
+ path: str,
203
+ depth: int,
204
+ ignores: Tuple[str, ...],
205
+ no_default_ignore: bool,
206
+ show_hidden: bool,
207
+ dirs_only: bool,
208
+ show_size: bool,
209
+ show_stats: bool,
210
+ markdown: bool,
211
+ as_json: bool,
212
+ version: bool,
213
+ ):
214
+ """Generate directory tree for PATH.
215
+
216
+ Examples:
217
+
218
+ filetree # Current directory
219
+ filetree ~/projects -d 2 # Max depth 2
220
+ filetree . --md # Markdown output
221
+ filetree . -I "*.log" -I tmp # Ignore patterns
222
+ filetree . -D # Directories only
223
+ filetree . -a --size --stats # Show all with sizes and stats
224
+ """
225
+ if version:
226
+ click.echo(f"filetree {__version__}")
227
+ return
228
+
229
+ root = Path(path).resolve()
230
+
231
+ if not root.is_dir():
232
+ click.echo(f"Error: {path} is not a directory", err=True)
233
+ sys.exit(1)
234
+
235
+ # Build ignore patterns
236
+ ignore_patterns = []
237
+ if not no_default_ignore:
238
+ ignore_patterns.extend(DEFAULT_IGNORES)
239
+ ignore_patterns.extend(ignores)
240
+
241
+ # Generate tree
242
+ stats = {"dirs": 0, "files": 0, "size": 0}
243
+ lines = generate_tree(
244
+ root,
245
+ depth=depth,
246
+ ignore_patterns=ignore_patterns,
247
+ dirs_only=dirs_only,
248
+ show_hidden=show_hidden,
249
+ show_size=show_size,
250
+ stats=stats,
251
+ )
252
+
253
+ # Output
254
+ if as_json:
255
+ output = tree_to_json(root, lines, stats)
256
+ click.echo(json.dumps(output, indent=2))
257
+ elif markdown:
258
+ click.echo(tree_to_markdown(root.name, lines))
259
+ else:
260
+ click.echo(f"{root.name}/")
261
+ click.echo("\n".join(lines))
262
+
263
+ if show_stats:
264
+ click.echo()
265
+ click.echo(f"{stats['dirs']} directories, {stats['files']} files")
266
+ if stats["size"] > 0:
267
+ click.echo(f"Total size: {format_size(stats['size'])}")
268
+
269
+
270
+ if __name__ == "__main__":
271
+ main()
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: filetree-cli
3
+ Version: 0.1.0
4
+ Summary: Generate ASCII or Markdown directory trees with smart filtering
5
+ Project-URL: Homepage, https://github.com/marcusbuildsthings-droid/filetree
6
+ Project-URL: Repository, https://github.com/marcusbuildsthings-droid/filetree
7
+ Project-URL: Issues, https://github.com/marcusbuildsthings-droid/filetree/issues
8
+ Author-email: Marcus <marcus.builds.things@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ascii,cli,directory,file,markdown,structure,tree
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Software Development :: Documentation
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.8
26
+ Requires-Dist: click>=8.0.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # filetree
30
+
31
+ Generate ASCII or Markdown directory trees with smart filtering.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install filetree-cli
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ # Basic usage - current directory
43
+ filetree
44
+
45
+ # Specific path with depth limit
46
+ filetree ~/projects -d 2
47
+
48
+ # Markdown output for documentation
49
+ filetree . --md
50
+
51
+ # Ignore additional patterns
52
+ filetree . -I "*.log" -I "tmp"
53
+
54
+ # Show only directories
55
+ filetree . -D
56
+
57
+ # Show hidden files with sizes and stats
58
+ filetree . -a --size --stats
59
+
60
+ # JSON output for programmatic use
61
+ filetree . --json
62
+ ```
63
+
64
+ ## Features
65
+
66
+ - **Smart defaults**: Ignores common cruft (.git, node_modules, __pycache__, etc.)
67
+ - **Depth limiting**: Control how deep the tree goes
68
+ - **Pattern ignoring**: Glob patterns to exclude files/directories
69
+ - **Multiple formats**: ASCII (default), Markdown, or JSON
70
+ - **Size display**: Optional file size annotations
71
+ - **Statistics**: File/directory counts and total size
72
+ - **Hidden files**: Toggle visibility of dotfiles
73
+
74
+ ## Default Ignored Patterns
75
+
76
+ These patterns are ignored by default (use `--no-default-ignore` to include them):
77
+
78
+ - `.git`, `.svn`, `.hg`
79
+ - `__pycache__`, `*.pyc`
80
+ - `node_modules`
81
+ - `.venv`, `venv`, `.env`
82
+ - `*.egg-info`, `.pytest_cache`, `.mypy_cache`
83
+ - `dist`, `build`
84
+ - `.idea`, `.vscode`
85
+ - `.DS_Store`
86
+
87
+ ## Examples
88
+
89
+ ### Basic Tree
90
+ ```
91
+ $ filetree
92
+ myproject/
93
+ ├── README.md
94
+ ├── pyproject.toml
95
+ └── src/
96
+ ├── __init__.py
97
+ └── main.py
98
+ ```
99
+
100
+ ### Markdown Output
101
+ ```
102
+ $ filetree --md
103
+ ```
104
+ ```
105
+ myproject/
106
+ ├── README.md
107
+ ├── pyproject.toml
108
+ └── src/
109
+ ├── __init__.py
110
+ └── main.py
111
+ ```
112
+
113
+ ### With Stats
114
+ ```
115
+ $ filetree --stats --size
116
+ myproject/
117
+ ├── README.md (1.2KB)
118
+ ├── pyproject.toml (856B)
119
+ └── src/
120
+ ├── __init__.py (45B)
121
+ └── main.py (2.3KB)
122
+
123
+ 1 directories, 4 files
124
+ Total size: 4.4KB
125
+ ```
126
+
127
+ ### JSON Output
128
+ ```
129
+ $ filetree --json
130
+ {
131
+ "root": "/path/to/myproject",
132
+ "tree": [
133
+ "myproject/",
134
+ "├── README.md",
135
+ "├── pyproject.toml",
136
+ "└── src/",
137
+ " ├── __init__.py",
138
+ " └── main.py"
139
+ ],
140
+ "stats": {
141
+ "directories": 1,
142
+ "files": 4,
143
+ "total_size": 4501,
144
+ "total_size_human": "4.4KB"
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## Options
150
+
151
+ | Option | Description |
152
+ |--------|-------------|
153
+ | `-d, --depth N` | Maximum depth (-1 for unlimited) |
154
+ | `-I, --ignore PATTERN` | Patterns to ignore (repeatable) |
155
+ | `--no-default-ignore` | Don't use default ignore patterns |
156
+ | `-a, --all` | Show hidden files/directories |
157
+ | `-D, --dirs-only` | Show only directories |
158
+ | `-s, --size` | Show file sizes |
159
+ | `--stats` | Show summary statistics |
160
+ | `--md, --markdown` | Output as markdown code block |
161
+ | `--json` | Output as JSON |
162
+ | `--version` | Show version |
163
+
164
+ ## For AI Agents
165
+
166
+ See [SKILL.md](SKILL.md) for agent-optimized documentation.
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,7 @@
1
+ filetree/__init__.py,sha256=BrzVqYmuqQ7ORoHdTsy1Xw-ZEW-slGWuwkbWsvzdC9w,84
2
+ filetree/cli.py,sha256=40_Kls1ayS5qTFeOV-w-1xm4nov2f_jtNHcgWmbBti4,7628
3
+ filetree_cli-0.1.0.dist-info/METADATA,sha256=cgSyCmgmgaTt13EGlmi-UrTwHUXzuvLzPh4DFX3gS00,4067
4
+ filetree_cli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ filetree_cli-0.1.0.dist-info/entry_points.txt,sha256=yCNjUTIxyI3skWIeFTYQTeDyCL9eZPvPnN5mnlFTNxk,47
6
+ filetree_cli-0.1.0.dist-info/licenses/LICENSE,sha256=9tNBpWq8KGbuJqmeComp40OiNnbvpvsKn1YP26PUtck,1063
7
+ filetree_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ filetree = filetree.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.