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 +21 -0
- package/README.md +154 -0
- package/bin/tree-fs.js +70 -0
- package/package.json +44 -0
- package/src/generator.js +60 -0
- package/src/index.js +7 -0
- package/src/normaliser.js +39 -0
- package/src/parser.js +56 -0
- package/src/utils.js +25 -0
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
|
+
[](https://www.npmjs.com/package/tree-fs)
|
|
8
|
+
[](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
|
+
}
|
package/src/generator.js
ADDED
|
@@ -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,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 }
|