tree-fs 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ghazi
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.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # tree-fs
2
+
3
+ **tree-fs** is a tiny, zero-dependency Node.js utility that turns text-based directory trees into physical files and folders.
4
+
5
+ It is designed to be the **standard "Paste & Go" receiver for AI-generated code**.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/tree-fs.svg)](https://www.npmjs.com/package/tree-fs)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ---
11
+
12
+ ## ⚡ Why tree-fs?
13
+
14
+ LLMs (ChatGPT, Claude, DeepSeek) are great at planning architectures but bad at executing them.
15
+ They often output this:
16
+
17
+ ```text
18
+ my-app
19
+ ├── src
20
+ │ ├── index.js
21
+ │ └── utils.js
22
+ └── README.md
23
+ ```
24
+
25
+ Copying that structure manually is tedious. **tree-fs** makes it instant.
26
+
27
+ ### Features
28
+ * **AI Compatible:** Strips comments (`# entry point`) and handles weird Markdown formatting.
29
+ * **Deterministic:** Same text input = same file structure. Always.
30
+ * **Safe:** Never overwrites existing files by default.
31
+ * **Smart:** Distinguishes `v1.0` (folder) from `v1.0.js` (file) automatically.
32
+ * **Zero Dependencies:** Installs in seconds.
33
+
34
+ ---
35
+
36
+ ## 🚀 Usage
37
+
38
+ ### 1. Interactive Mode (The "Paste" Workflow)
39
+
40
+ Perfect for when ChatGPT gives you a project structure.
41
+
42
+ ```bash
43
+ npx tree-fs
44
+ ```
45
+ *(You don't even need to install it!)*
46
+
47
+ 1. Paste your tree.
48
+ 2. Press **Enter twice**.
49
+ 3. Done.
50
+
51
+ ### 2. CLI with File Input
52
+
53
+ Generate structure from a text file saved in your repo.
54
+
55
+ ```bash
56
+ tree-fs structure.txt
57
+ ```
58
+
59
+ ### 3. Programmatic API (For Tool Builders)
60
+
61
+ Embed `tree-fs` into your own CLIs, generators, or scripts.
62
+
63
+ ```bash
64
+ npm install tree-fs
65
+ ```
66
+
67
+ ```javascript
68
+ const { parseTree, generateFS } = require("tree-fs")
69
+ const path = require("path")
70
+
71
+ const treeInput = `
72
+ backend-api
73
+ ├── src
74
+ │ ├── controllers/
75
+ │ └── models/
76
+ └── .env
77
+ `
78
+
79
+ // 1. Parse text into JSON AST
80
+ const tree = parseTree(treeInput)
81
+
82
+ // 2. Generate files at the target directory
83
+ generateFS(tree, path.resolve(__dirname, "./output"))
84
+
85
+ console.log("Structure created!")
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 💡 Syntax Guide & Robustness
91
+
92
+ tree-fs is built to handle the "messy reality" of text inputs.
93
+
94
+ ### 1. Comments are ignored
95
+ Great for annotated AI outputs.
96
+ ```text
97
+ src
98
+ ├── auth.js # Handles JWT tokens
99
+ └── db.js # Connection logic
100
+ ```
101
+ *Result: Creates `auth.js` and `db.js`. Comments are stripped.*
102
+
103
+ ### 2. Explicit Folders
104
+ If a name looks like a file but is actually a folder (e.g., version numbers), end it with a slash `/`.
105
+ ```text
106
+ api
107
+ ├── v1.5/ <-- Created as a folder
108
+ └── v2.0/ <-- Created as a folder
109
+ ```
110
+
111
+ ### 3. Smart Nesting
112
+ If an item has children indented below it, it is **automatically treated as a folder**, even if it has a dot.
113
+ ```text
114
+ app
115
+ └── v2.5 <-- Treated as folder because it has a child
116
+ └── migrator.js
117
+ ```
118
+
119
+ ### 4. Markdown & Symbols
120
+ We handle standard tree characters, ASCII art, and bullets.
121
+ ```text
122
+ project
123
+ - src
124
+ + components
125
+ > Header.jsx
126
+ * public
127
+ - logo.png
128
+ ```
129
+
130
+ ### 5. Config Files
131
+ Known files without extensions are correctly identified as files.
132
+ * `Dockerfile`, `Makefile`, `LICENSE`, `Procfile`, `.gitignore`, `Jenkinsfile`
133
+
134
+ ---
135
+
136
+ ## 📦 CI/CD Integration
137
+
138
+ You can use `tree-fs` to scaffold environments in GitHub Actions or pipelines.
139
+
140
+ ```yaml
141
+ - name: Scaffold Directory
142
+ run: npx tree-fs structure.txt
143
+ ```
144
+
145
+ To test without writing files (Dry Run):
146
+ ```bash
147
+ tree-fs structure.txt --dry-run
148
+ ```
149
+
150
+ ---
151
+
152
+ ## License
153
+
154
+ MIT
package/bin/tree-fs.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs")
4
+ const readline = require("readline")
5
+ const { parseTree, generateFS } = require("../src")
6
+
7
+ const args = process.argv.slice(2)
8
+ const dryRun = args.includes("--dry-run")
9
+
10
+ async function readInteractiveInput() {
11
+ console.log("Paste directory tree. Press Enter on an empty line to finish.\n")
12
+
13
+ const rl = readline.createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout,
16
+ terminal: false // This helps handle raw paste streams better
17
+ })
18
+
19
+ const lines = []
20
+
21
+ for await (const line of rl) {
22
+ // If the line is empty (user hit Enter on a new line), we are done.
23
+ if (line.trim() === "") {
24
+ rl.close()
25
+ break
26
+ }
27
+ lines.push(line)
28
+ }
29
+
30
+ return lines.join("\n")
31
+ }
32
+
33
+ async function main() {
34
+ let input = ""
35
+
36
+ // Programmatic / CI / file input
37
+ if (args[0] && !args[0].startsWith("--")) {
38
+ try {
39
+ input = fs.readFileSync(args[0], "utf8")
40
+ } catch (err) {
41
+ console.error(`Error reading file: ${args[0]}`)
42
+ process.exit(1)
43
+ }
44
+ }
45
+ // Interactive mode
46
+ else {
47
+ input = await readInteractiveInput()
48
+ }
49
+
50
+ if (!input.trim()) {
51
+ console.error("No tree input provided.")
52
+ process.exit(1)
53
+ }
54
+
55
+ try {
56
+ const tree = parseTree(input)
57
+ generateFS(tree, process.cwd(), { dryRun })
58
+
59
+ if (dryRun) {
60
+ console.log("Dry run complete. No files written.")
61
+ } else {
62
+ console.log("✅ Structure created successfully.")
63
+ }
64
+ } catch (error) {
65
+ console.error("Failed to generate structure:", error.message)
66
+ process.exit(1)
67
+ }
68
+ }
69
+
70
+ main()
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "tree-fs",
3
+ "version": "0.1.0",
4
+ "description": "Generate file system structures from text-based directory trees. The standard receiver for AI-generated project scaffolding.",
5
+ "bin": {
6
+ "tree-fs": "bin/tree-fs.js"
7
+ },
8
+ "main": "src/index.js",
9
+ "type": "commonjs",
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=14.0.0"
18
+ },
19
+ "scripts": {
20
+ "test": "echo \"Error: no test specified\" && exit 1",
21
+ "start": "node bin/tree-fs.js"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/mgks/tree-fs.git"
26
+ },
27
+ "keywords": [
28
+ "filesystem",
29
+ "scaffold",
30
+ "tree",
31
+ "directory-structure",
32
+ "generator",
33
+ "llm",
34
+ "ai",
35
+ "automation",
36
+ "cli"
37
+ ],
38
+ "author": "Ghazi <hello@mgks.dev> (https://mgks.dev)",
39
+ "license": "MIT",
40
+ "bugs": {
41
+ "url": "https://github.com/mgks/tree-fs/issues"
42
+ },
43
+ "homepage": "https://github.com/mgks/tree-fs"
44
+ }
@@ -0,0 +1,60 @@
1
+ // src/generator.js
2
+ const fs = require("fs")
3
+ const path = require("path")
4
+
5
+ function generateFS(tree, baseDir = process.cwd(), options = {}) {
6
+ const {
7
+ dryRun = false,
8
+ overwrite = false
9
+ } = options
10
+
11
+ if (!tree || tree.length === 0) return
12
+
13
+ let nodes = tree
14
+ let rootPath = baseDir
15
+
16
+ // Intelligent Root Detection:
17
+ // If there is exactly one top-level folder, we treat it as the project root.
18
+ if (nodes.length === 1 && nodes[0].type === "folder") {
19
+ rootPath = path.join(baseDir, nodes[0].name)
20
+ nodes = nodes[0].children
21
+
22
+ // Create the root directory physically
23
+ if (!dryRun && !fs.existsSync(rootPath)) {
24
+ fs.mkdirSync(rootPath, { recursive: true })
25
+ }
26
+ }
27
+
28
+ function walk(nodes, currentPath) {
29
+ for (const node of nodes) {
30
+ // Robustness: normalize path separators for cross-platform safety
31
+ const target = path.join(currentPath, node.name)
32
+
33
+ if (node.type === "folder") {
34
+ if (!fs.existsSync(target)) {
35
+ if (!dryRun) fs.mkdirSync(target, { recursive: true })
36
+ }
37
+ if (node.children && node.children.length > 0) {
38
+ walk(node.children, target)
39
+ }
40
+ } else {
41
+ // File handling
42
+ if (fs.existsSync(target) && !overwrite) {
43
+ continue
44
+ }
45
+
46
+ // Ensure parent directory exists (handles cases like "src/utils/helpers.js" as a single line)
47
+ const parentDir = path.dirname(target)
48
+ if (!dryRun && !fs.existsSync(parentDir)) {
49
+ fs.mkdirSync(parentDir, { recursive: true })
50
+ }
51
+
52
+ if (!dryRun) fs.writeFileSync(target, "")
53
+ }
54
+ }
55
+ }
56
+
57
+ walk(nodes, rootPath)
58
+ }
59
+
60
+ module.exports = { generateFS }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ const { parseTree } = require("./parser")
2
+ const { generateFS } = require("./generator")
3
+
4
+ module.exports = {
5
+ parseTree,
6
+ generateFS
7
+ }
@@ -0,0 +1,39 @@
1
+ // src/normaliser.js
2
+
3
+ // 1. Expanded Regex to catch +, >, and markdown bullets
4
+ const STRIP_REGEX = /^[\s│├└─•*|\-+>]+/
5
+
6
+ function normaliseLines(input) {
7
+ return input
8
+ .split("\n")
9
+ .map(line => line.replace(/\r/g, ""))
10
+ .filter(Boolean)
11
+ .map(raw => {
12
+ // 2. Detect indentation based on prefix length
13
+ const match = raw.match(STRIP_REGEX)
14
+ const prefixLength = match ? match[0].length : 0
15
+
16
+ // 3. Strip comments (anything after ' #')
17
+ // We explicitly look for " #" so we don't break "page#1.js"
18
+ const commentIndex = raw.indexOf(" #")
19
+ let cleaned = commentIndex !== -1 ? raw.substring(0, commentIndex) : raw
20
+
21
+ // 4. Check for explicit trailing slash (User saying "This is a folder/")
22
+ const endsWithSlash = cleaned.trim().endsWith("/")
23
+
24
+ cleaned = cleaned
25
+ .replace(STRIP_REGEX, "") // Remove tree characters
26
+ .replace(/\/$/, "") // Remove that trailing slash for the name
27
+ .trim()
28
+
29
+ return {
30
+ raw,
31
+ indent: prefixLength,
32
+ name: cleaned,
33
+ explicitFolder: endsWithSlash // Pass this signal to the parser
34
+ }
35
+ })
36
+ .filter(line => line.name.length > 0)
37
+ }
38
+
39
+ module.exports = { normaliseLines }
package/src/parser.js ADDED
@@ -0,0 +1,56 @@
1
+ const { normaliseLines } = require("./normaliser")
2
+ const { isFile } = require("./utils")
3
+
4
+ function parseTree(input) {
5
+ const lines = normaliseLines(input)
6
+ const root = { name: "__root__", type: "folder", children: [] }
7
+
8
+ // Stack keeps track of [ { node, indent } ]
9
+ const stack = [{ node: root, indent: -1 }]
10
+
11
+ for (let i = 0; i < lines.length; i++) {
12
+ const line = lines[i]
13
+ const nextLine = lines[i + 1]
14
+
15
+ // It is a folder IF:
16
+ // 1. It ends with '/' (Explicit)
17
+ // 2. The NEXT line exists AND has a deeper indent (Implicit Parent)
18
+ // 3. It does NOT pass the isFile check (Standard folder)
19
+
20
+ let isFolder = false
21
+
22
+ if (line.explicitFolder) {
23
+ isFolder = true
24
+ } else if (nextLine && nextLine.indent > line.indent) {
25
+ // If the next item is indented deeper, THIS item must be a folder to hold it.
26
+ isFolder = true
27
+ } else {
28
+ // Fallback to name-based checking
29
+ isFolder = !isFile(line.name)
30
+ }
31
+
32
+ const node = {
33
+ name: line.name,
34
+ type: isFolder ? "folder" : "file",
35
+ children: []
36
+ }
37
+
38
+ // Standard stack unwinding
39
+ while (stack.length && line.indent <= stack[stack.length - 1].indent) {
40
+ stack.pop()
41
+ }
42
+
43
+ // Add to parent
44
+ const parent = stack[stack.length - 1].node
45
+ parent.children.push(node)
46
+
47
+ // Push to stack if folder
48
+ if (isFolder) {
49
+ stack.push({ node, indent: line.indent })
50
+ }
51
+ }
52
+
53
+ return root.children
54
+ }
55
+
56
+ module.exports = { parseTree }
package/src/utils.js ADDED
@@ -0,0 +1,25 @@
1
+ // src/utils.js
2
+
3
+ // Common files that have no extension but should be treated as files
4
+ const KNOWN_FILES = new Set([
5
+ "LICENSE", "licence",
6
+ "README", "readme",
7
+ "Dockerfile", "dockerfile",
8
+ "Makefile", "makefile",
9
+ "Jenkinsfile",
10
+ "Procfile",
11
+ "CNAME",
12
+ ".gitignore", ".env", ".npmrc", ".dockerignore", ".editorconfig"
13
+ ])
14
+
15
+ function isFile(name) {
16
+ // Check exact matches (case-insensitive for safety if needed, but Sets are exact)
17
+ if (KNOWN_FILES.has(name)) return true
18
+
19
+ // Standard extension check: dot followed by 1-5 chars (e.g., .js, .json, .java)
20
+ // We avoid detecting "v1.2" as a file by ensuring the extension is letters only
21
+ // OR if it ends in specific known formats like .tar.gz
22
+ return /\.[a-zA-Z0-9]{1,10}$/.test(name)
23
+ }
24
+
25
+ module.exports = { isFile }