treefs 2.0.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.
treefs-2.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 marcelodevops
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.
treefs-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: treefs
3
+ Version: 2.0.0
4
+ Summary: Filesystem generator/importer using tree, YAML, JSON, TOML and tree format. Deluxe edition.
5
+ Author: Marcelo
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/marcelodevops/treefspy
8
+ Project-URL: Repository, https://github.com/marcelodevops/treefspy
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Classifier: Topic :: Utilities
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: PyYAML>=6.0
18
+ Requires-Dist: toml>=0.10.2
19
+ Dynamic: license-file
20
+
21
+ # ๐Ÿงฐ TreeFS
22
+
23
+ A powerful CLI toolkit to build and export directory structures from **tree text**, **YAML**, **JSON**, or **TOML** configs โ€” with support for templates, Git initialization, and cross-platform release binaries.
24
+
25
+ Originally inspired by a tiny Bash script, TreeFS has grown into a full-featured Python project that helps automate project scaffolding and structure generation.
26
+
27
+ ---
28
+
29
+ ## Features
30
+
31
+ - **Build directory trees** from:
32
+ - Tree-formatted text files
33
+ - YAML / JSON / TOML config files
34
+ - Support for **file templates** and content templates
35
+ - **Export** file systems into tree text or structured configs
36
+ - **Git integration**
37
+ - `git init` with optional `.gitignore` and hooks
38
+ - **Cross-platform binaries**
39
+ - Build single-file executables via PyInstaller
40
+ - GitHub Actions workflow included to auto-build release binaries
41
+
42
+ ---
43
+
44
+ ## ๐Ÿงพ Installation
45
+
46
+ Install locally using `pip`:
47
+
48
+ ```bash
49
+ pip install .
50
+ ```
51
+
52
+ ## Usage
53
+ - Build from a tree file
54
+ ```bash
55
+ treefs build tree.txt myproject/
56
+
57
+ ```
58
+ - Add optional templates and git setup
59
+ ```bash
60
+ treefs build project.yaml myproj/ \
61
+ --templates ./templates \
62
+ --init-git \
63
+ --gitignore ./common_gitignore \
64
+ --git-hooks ./my-hooks
65
+ ```
66
+ - Preview what would be created without writing to disk:
67
+ ```bash
68
+ treefs build project.yaml myproj/ --dry-run
69
+ ```
70
+ ### Export project structure
71
+ - Export a folder to a tree layout text:
72
+ ```bash
73
+ treefs export-tree myproj/ tree.txt
74
+ ```
75
+ - Or export to structured configs:
76
+ ```bash
77
+ treefs export-config myproj/ project.yaml
78
+ treefs export-config myproj/ project.json
79
+ treefs export-config myproj/ project.toml
80
+ ```
81
+ ### Bundling a binary
82
+ - If PyInstaller is installed, you can auto-bundle TreeFS:
83
+ ```bash
84
+ treefs bundle treefs.py --name treefs
85
+ ```
treefs-2.0.0/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # ๐Ÿงฐ TreeFS
2
+
3
+ A powerful CLI toolkit to build and export directory structures from **tree text**, **YAML**, **JSON**, or **TOML** configs โ€” with support for templates, Git initialization, and cross-platform release binaries.
4
+
5
+ Originally inspired by a tiny Bash script, TreeFS has grown into a full-featured Python project that helps automate project scaffolding and structure generation.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Build directory trees** from:
12
+ - Tree-formatted text files
13
+ - YAML / JSON / TOML config files
14
+ - Support for **file templates** and content templates
15
+ - **Export** file systems into tree text or structured configs
16
+ - **Git integration**
17
+ - `git init` with optional `.gitignore` and hooks
18
+ - **Cross-platform binaries**
19
+ - Build single-file executables via PyInstaller
20
+ - GitHub Actions workflow included to auto-build release binaries
21
+
22
+ ---
23
+
24
+ ## ๐Ÿงพ Installation
25
+
26
+ Install locally using `pip`:
27
+
28
+ ```bash
29
+ pip install .
30
+ ```
31
+
32
+ ## Usage
33
+ - Build from a tree file
34
+ ```bash
35
+ treefs build tree.txt myproject/
36
+
37
+ ```
38
+ - Add optional templates and git setup
39
+ ```bash
40
+ treefs build project.yaml myproj/ \
41
+ --templates ./templates \
42
+ --init-git \
43
+ --gitignore ./common_gitignore \
44
+ --git-hooks ./my-hooks
45
+ ```
46
+ - Preview what would be created without writing to disk:
47
+ ```bash
48
+ treefs build project.yaml myproj/ --dry-run
49
+ ```
50
+ ### Export project structure
51
+ - Export a folder to a tree layout text:
52
+ ```bash
53
+ treefs export-tree myproj/ tree.txt
54
+ ```
55
+ - Or export to structured configs:
56
+ ```bash
57
+ treefs export-config myproj/ project.yaml
58
+ treefs export-config myproj/ project.json
59
+ treefs export-config myproj/ project.toml
60
+ ```
61
+ ### Bundling a binary
62
+ - If PyInstaller is installed, you can auto-bundle TreeFS:
63
+ ```bash
64
+ treefs bundle treefs.py --name treefs
65
+ ```
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "treefs"
7
+ version = "2.0.0"
8
+ description = "Filesystem generator/importer using tree, YAML, JSON, TOML and tree format. Deluxe edition."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{name = "Marcelo"}]
13
+ dependencies = ["PyYAML>=6.0", "toml>=0.10.2"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ "Environment :: Console",
18
+ "Topic :: Software Development :: Build Tools",
19
+ "Topic :: Utilities",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/marcelodevops/treefspy"
24
+ Repository = "https://github.com/marcelodevops/treefspy"
25
+
26
+ [project.scripts]
27
+ treefs = "treefs:main"
28
+
29
+ [tool.setuptools]
30
+ py-modules = ["treefs"]
treefs-2.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,116 @@
1
+ """Unit tests for treefs.py's tree-file parser and build logic.
2
+
3
+ Run with: python -m unittest discover -s tests
4
+ (stdlib unittest is used so no extra test-runner dependency is required.)
5
+ """
6
+ import sys
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+
13
+ import treefs # noqa: E402
14
+
15
+
16
+ class TreeToDictTests(unittest.TestCase):
17
+ def _parse(self, text: str) -> dict:
18
+ with tempfile.NamedTemporaryFile("w", suffix=".tree", delete=False) as f:
19
+ f.write(text)
20
+ path = Path(f.name)
21
+ try:
22
+ return treefs.tree_to_dict(path)
23
+ finally:
24
+ path.unlink()
25
+
26
+ def test_directories_without_trailing_slash(self):
27
+ # This is what the stock `tree` command actually outputs: no
28
+ # trailing "/" on directory names.
29
+ text = (
30
+ "myproj\n"
31
+ "โ”œโ”€โ”€ src\n"
32
+ "โ”‚ โ”œโ”€โ”€ main.py\n"
33
+ "โ”‚ โ””โ”€โ”€ utils.py\n"
34
+ "โ””โ”€โ”€ README.md\n"
35
+ )
36
+ structure = self._parse(text)
37
+ self.assertEqual(
38
+ structure,
39
+ {
40
+ "myproj": {
41
+ "src": {"main.py": "", "utils.py": ""},
42
+ "README.md": "",
43
+ }
44
+ },
45
+ )
46
+
47
+ def test_directories_with_trailing_slash(self):
48
+ text = (
49
+ "myproj/\n"
50
+ "โ”œโ”€โ”€ src/\n"
51
+ "โ”‚ โ””โ”€โ”€ main.py\n"
52
+ "โ””โ”€โ”€ README.md\n"
53
+ )
54
+ structure = self._parse(text)
55
+ self.assertEqual(
56
+ structure,
57
+ {"myproj": {"src": {"main.py": ""}, "README.md": ""}},
58
+ )
59
+
60
+ def test_deep_nesting_with_spacer_indent(self):
61
+ # Last child of a directory uses plain spaces instead of "โ”‚ ".
62
+ text = (
63
+ "myproj\n"
64
+ "โ”œโ”€โ”€ src\n"
65
+ "โ”‚ โ”œโ”€โ”€ pkg\n"
66
+ "โ”‚ โ”‚ โ””โ”€โ”€ deep.py\n"
67
+ "โ”‚ โ””โ”€โ”€ utils.py\n"
68
+ "โ””โ”€โ”€ docs\n"
69
+ " โ””โ”€โ”€ guide.md\n"
70
+ )
71
+ structure = self._parse(text)
72
+ self.assertEqual(
73
+ structure,
74
+ {
75
+ "myproj": {
76
+ "src": {"pkg": {"deep.py": ""}, "utils.py": ""},
77
+ "docs": {"guide.md": ""},
78
+ }
79
+ },
80
+ )
81
+
82
+ def test_empty_directory_needs_trailing_slash(self):
83
+ # An empty directory has no children to infer depth from, so it must
84
+ # be marked explicitly with a trailing "/".
85
+ text = "myproj\nโ”œโ”€โ”€ empty_dir/\nโ””โ”€โ”€ file.txt\n"
86
+ structure = self._parse(text)
87
+ self.assertEqual(
88
+ structure,
89
+ {"myproj": {"empty_dir": {}, "file.txt": ""}},
90
+ )
91
+
92
+
93
+ class BuildFromTreeTests(unittest.TestCase):
94
+ def test_build_creates_expected_files_on_disk(self):
95
+ text = (
96
+ "myproj\n"
97
+ "โ”œโ”€โ”€ src\n"
98
+ "โ”‚ โ””โ”€โ”€ main.py\n"
99
+ "โ””โ”€โ”€ README.md\n"
100
+ )
101
+ with tempfile.TemporaryDirectory() as tmp:
102
+ tmp_path = Path(tmp)
103
+ tree_file = tmp_path / "sample.tree"
104
+ tree_file.write_text(text, encoding="utf-8")
105
+
106
+ root = tmp_path / "out"
107
+ root.mkdir()
108
+ treefs.build_from_tree(tree_file, root, force=False, dry_run=False)
109
+
110
+ self.assertTrue((root / "src").is_dir())
111
+ self.assertTrue((root / "src" / "main.py").is_file())
112
+ self.assertTrue((root / "README.md").is_file())
113
+
114
+
115
+ if __name__ == "__main__":
116
+ unittest.main()
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: treefs
3
+ Version: 2.0.0
4
+ Summary: Filesystem generator/importer using tree, YAML, JSON, TOML and tree format. Deluxe edition.
5
+ Author: Marcelo
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/marcelodevops/treefspy
8
+ Project-URL: Repository, https://github.com/marcelodevops/treefspy
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Classifier: Topic :: Utilities
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: PyYAML>=6.0
18
+ Requires-Dist: toml>=0.10.2
19
+ Dynamic: license-file
20
+
21
+ # ๐Ÿงฐ TreeFS
22
+
23
+ A powerful CLI toolkit to build and export directory structures from **tree text**, **YAML**, **JSON**, or **TOML** configs โ€” with support for templates, Git initialization, and cross-platform release binaries.
24
+
25
+ Originally inspired by a tiny Bash script, TreeFS has grown into a full-featured Python project that helps automate project scaffolding and structure generation.
26
+
27
+ ---
28
+
29
+ ## Features
30
+
31
+ - **Build directory trees** from:
32
+ - Tree-formatted text files
33
+ - YAML / JSON / TOML config files
34
+ - Support for **file templates** and content templates
35
+ - **Export** file systems into tree text or structured configs
36
+ - **Git integration**
37
+ - `git init` with optional `.gitignore` and hooks
38
+ - **Cross-platform binaries**
39
+ - Build single-file executables via PyInstaller
40
+ - GitHub Actions workflow included to auto-build release binaries
41
+
42
+ ---
43
+
44
+ ## ๐Ÿงพ Installation
45
+
46
+ Install locally using `pip`:
47
+
48
+ ```bash
49
+ pip install .
50
+ ```
51
+
52
+ ## Usage
53
+ - Build from a tree file
54
+ ```bash
55
+ treefs build tree.txt myproject/
56
+
57
+ ```
58
+ - Add optional templates and git setup
59
+ ```bash
60
+ treefs build project.yaml myproj/ \
61
+ --templates ./templates \
62
+ --init-git \
63
+ --gitignore ./common_gitignore \
64
+ --git-hooks ./my-hooks
65
+ ```
66
+ - Preview what would be created without writing to disk:
67
+ ```bash
68
+ treefs build project.yaml myproj/ --dry-run
69
+ ```
70
+ ### Export project structure
71
+ - Export a folder to a tree layout text:
72
+ ```bash
73
+ treefs export-tree myproj/ tree.txt
74
+ ```
75
+ - Or export to structured configs:
76
+ ```bash
77
+ treefs export-config myproj/ project.yaml
78
+ treefs export-config myproj/ project.json
79
+ treefs export-config myproj/ project.toml
80
+ ```
81
+ ### Bundling a binary
82
+ - If PyInstaller is installed, you can auto-bundle TreeFS:
83
+ ```bash
84
+ treefs bundle treefs.py --name treefs
85
+ ```
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ treefs.py
5
+ tests/test_treefs.py
6
+ treefs.egg-info/PKG-INFO
7
+ treefs.egg-info/SOURCES.txt
8
+ treefs.egg-info/dependency_links.txt
9
+ treefs.egg-info/entry_points.txt
10
+ treefs.egg-info/requires.txt
11
+ treefs.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ treefs = treefs:main
@@ -0,0 +1,2 @@
1
+ PyYAML>=6.0
2
+ toml>=0.10.2
@@ -0,0 +1 @@
1
+ treefs
treefs-2.0.0/treefs.py ADDED
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ TreeFS โ€” deluxe edition (fixed parser version)
4
+
5
+ Changes:
6
+ - Replaced fragile ASCII-tree stack parser
7
+ - Introduced AST-based tree parser (tree โ†’ dict)
8
+ - build_from_tree now reuses build_from_dict safely
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any, Dict, Optional
19
+
20
+ # optional deps: pyyaml, toml
21
+ try:
22
+ import yaml
23
+ except Exception:
24
+ yaml = None
25
+
26
+ try:
27
+ import toml
28
+ except Exception:
29
+ toml = None
30
+
31
+
32
+ # ---------- Utilities ----------
33
+
34
+ # 4-character indentation units a tree line's prefix is built from (one per
35
+ # ancestor depth level that is *not* the immediate parent connector).
36
+ _INDENT_UNITS = ("โ”‚ ", " ", "| ")
37
+
38
+ # 4-character connectors that introduce an entry's own name.
39
+ _CONNECTORS = ("โ”œโ”€โ”€ ", "โ””โ”€โ”€ ", "|-- ", "`-- ")
40
+
41
+
42
+ def _parse_tree_line(line: str) -> tuple[int, str]:
43
+ """Split a single tree line into (depth, name).
44
+
45
+ This walks the line manually instead of relying on trailing slashes or
46
+ fragile character-replacement math, so it works whether or not the tree
47
+ file marks directories with a trailing "/" (the stock `tree` command does
48
+ not do so unless run with `-F`).
49
+
50
+ A bare root line (no leading indent units and no connector, e.g. the
51
+ "myproj" line at the very top of `tree` output) is one level *above* its
52
+ connector-prefixed children, even though they share the same count of
53
+ indent units, so it gets depth -1 relative to them.
54
+ """
55
+ i = 0
56
+ depth = 0
57
+ while line[i:i + 4] in _INDENT_UNITS:
58
+ depth += 1
59
+ i += 4
60
+
61
+ has_connector = line[i:i + 4] in _CONNECTORS
62
+ if has_connector:
63
+ i += 4
64
+ else:
65
+ depth -= 1
66
+
67
+ name = line[i:].strip()
68
+ return depth, name
69
+
70
+
71
+ def is_tty() -> bool:
72
+ return sys.stdout.isatty()
73
+
74
+
75
+ def colorize(text: str, color: str) -> str:
76
+ if not is_tty():
77
+ return text
78
+ codes = {
79
+ "green": "\033[92m",
80
+ "yellow": "\033[93m",
81
+ "blue": "\033[94m",
82
+ "red": "\033[91m",
83
+ "reset": "\033[0m",
84
+ }
85
+ return f"{codes.get(color, '')}{text}{codes['reset']}"
86
+
87
+
88
+ def safe_print(msg: str = "", c: str = "green"):
89
+ print(colorize(msg, c))
90
+
91
+
92
+ def ensure_parent(path: Path):
93
+ path.parent.mkdir(parents=True, exist_ok=True)
94
+
95
+
96
+ # ---------- FIXED: TREE PARSER (AST BUILDER) ----------
97
+
98
+
99
+ def tree_to_dict(tree_file: Path) -> dict:
100
+ """
101
+ Converts ASCII tree file into nested dictionary (AST).
102
+
103
+ Directories are detected either by an explicit trailing "/" on the name,
104
+ or (since the stock `tree` command doesn't add one) by the presence of a
105
+ following line indented one level deeper.
106
+ """
107
+
108
+ entries = [] # (depth, name, has_trailing_slash)
109
+
110
+ with tree_file.open("r", encoding="utf-8") as f:
111
+ for raw in f:
112
+ line = raw.rstrip()
113
+
114
+ if not line or line.strip() == "โ”‚":
115
+ continue
116
+
117
+ if line.lower().startswith("project"):
118
+ continue
119
+
120
+ depth, name = _parse_tree_line(line)
121
+ if not name:
122
+ continue
123
+
124
+ has_slash = name.endswith("/")
125
+ entries.append((depth, name.rstrip("/"), has_slash))
126
+
127
+ root: dict = {}
128
+ stack = [(-2, root)] # (depth, node); -2 is below any real/root depth
129
+
130
+ for idx, (depth, name, has_slash) in enumerate(entries):
131
+ is_dir = has_slash
132
+ if not is_dir and idx + 1 < len(entries):
133
+ is_dir = entries[idx + 1][0] > depth
134
+
135
+ # move to correct parent
136
+ while stack and stack[-1][0] >= depth:
137
+ stack.pop()
138
+
139
+ parent = stack[-1][1]
140
+
141
+ if is_dir:
142
+ node: dict = {}
143
+ parent[name] = node
144
+ stack.append((depth, node))
145
+ else:
146
+ parent[name] = ""
147
+
148
+ return root
149
+
150
+
151
+ # ---------- FIXED: BUILD FROM TREE ----------
152
+
153
+
154
+ def build_from_tree(
155
+ tree_file: Path,
156
+ root: Path,
157
+ force: bool,
158
+ dry_run: bool,
159
+ templates_dir: Optional[Path] = None,
160
+ ):
161
+ structure = tree_to_dict(tree_file)
162
+
163
+ # unwrap single-root tree (platform/ etc.)
164
+ if len(structure) == 1:
165
+ structure = list(structure.values())[0]
166
+
167
+ return build_from_dict(
168
+ root=root,
169
+ structure=structure,
170
+ force=force,
171
+ dry_run=dry_run,
172
+ templates_dir=templates_dir,
173
+ )
174
+
175
+
176
+ # ---------- BUILD FROM DICT (UNCHANGED CORE) ----------
177
+
178
+
179
+ def build_from_dict(
180
+ root: Path,
181
+ structure: Dict[str, Any],
182
+ force: bool,
183
+ dry_run: bool,
184
+ templates_dir: Optional[Path] = None,
185
+ ):
186
+ created = []
187
+
188
+ def recurse(base: Path, node: Dict[str, Any]):
189
+ for name, val in node.items():
190
+ path = base / name
191
+
192
+ if isinstance(val, dict):
193
+ if dry_run:
194
+ created.append(str(path) + "/ (would create)")
195
+ else:
196
+ path.mkdir(parents=True, exist_ok=True)
197
+ created.append(str(path) + "/")
198
+
199
+ recurse(path, val)
200
+
201
+ else:
202
+ ensure_parent(path)
203
+
204
+ if path.exists() and not force:
205
+ created.append(f"{path} (exists, kept)")
206
+ else:
207
+ if dry_run:
208
+ created.append(f"{path} (would create file)")
209
+ else:
210
+ path.write_text(val or "", encoding="utf-8")
211
+ created.append(str(path))
212
+
213
+ recurse(root, structure)
214
+ return created
215
+
216
+
217
+ # ---------- EXPORT TREE ----------
218
+
219
+
220
+ def export_tree(root: Path, output: Path):
221
+ lines = [root.name + "/"]
222
+
223
+ def walk(p: Path, prefix: str = ""):
224
+ entries = sorted(p.iterdir(), key=lambda x: x.name)
225
+ for i, entry in enumerate(entries):
226
+ connector = "โ””โ”€โ”€ " if i == len(entries) - 1 else "โ”œโ”€โ”€ "
227
+ lines.append(prefix + connector + entry.name)
228
+
229
+ if entry.is_dir():
230
+ ext = " " if i == len(entries) - 1 else "โ”‚ "
231
+ walk(entry, prefix + ext)
232
+
233
+ walk(root)
234
+ output.write_text("\n".join(lines), encoding="utf-8")
235
+ return str(output)
236
+
237
+
238
+ # ---------- EXPORT CONFIG ----------
239
+
240
+
241
+ def export_dict(root: Path) -> Dict[str, Any]:
242
+ def recurse(p: Path):
243
+ data = {}
244
+ for child in sorted(p.iterdir(), key=lambda x: x.name):
245
+ if child.is_dir():
246
+ data[child.name] = recurse(child)
247
+ else:
248
+ data[child.name] = child.read_text(encoding="utf-8")
249
+ return data
250
+
251
+ return {root.name: recurse(root)}
252
+
253
+
254
+ # ---------- CLI ----------
255
+
256
+
257
+ def main(argv=None):
258
+ parser = argparse.ArgumentParser("treefs")
259
+ sub = parser.add_subparsers(dest="cmd")
260
+
261
+ b = sub.add_parser("build")
262
+ b.add_argument("input")
263
+ b.add_argument("root")
264
+ b.add_argument("--force", action="store_true")
265
+ b.add_argument("--dry-run", action="store_true")
266
+
267
+ t = sub.add_parser("export-tree")
268
+ t.add_argument("root")
269
+ t.add_argument("output")
270
+
271
+ c = sub.add_parser("export-config")
272
+ c.add_argument("root")
273
+ c.add_argument("output")
274
+
275
+ args = parser.parse_args(argv)
276
+
277
+ if args.cmd == "build":
278
+ inp = Path(args.input)
279
+ root = Path(args.root)
280
+ root.mkdir(parents=True, exist_ok=True)
281
+
282
+ created = build_from_tree(
283
+ inp,
284
+ root,
285
+ force=args.force,
286
+ dry_run=args.dry_run,
287
+ )
288
+
289
+ safe_print("\nCreated / Verified:", "yellow")
290
+ for cstr in created:
291
+ safe_print(" - " + cstr)
292
+
293
+ elif args.cmd == "export-tree":
294
+ out = Path(args.output)
295
+ safe_print("Exporting tree...", "blue")
296
+ export_tree(Path(args.root), out)
297
+ safe_print("Saved", "yellow")
298
+
299
+ elif args.cmd == "export-config":
300
+ root = Path(args.root)
301
+ out = Path(args.output)
302
+ struct = export_dict(root)
303
+
304
+ fmt = out.suffix.lower().lstrip(".")
305
+ if fmt == "json":
306
+ out.write_text(json.dumps(struct, indent=2))
307
+ elif fmt == "yaml" and yaml:
308
+ out.write_text(yaml.safe_dump(struct))
309
+ elif fmt == "toml" and toml:
310
+ out.write_text(toml.dumps(struct))
311
+ else:
312
+ raise RuntimeError("Unsupported format")
313
+
314
+ safe_print("Saved", "yellow")
315
+
316
+ else:
317
+ parser.print_help()
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()