snap-scaffold 1.0.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/README.md +103 -0
- package/bin/snap.js +112 -0
- package/lib/index.js +347 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# snap-scaffold
|
|
2
|
+
|
|
3
|
+
A CLI to **snapshot** your directory structure into a plain-text file and **restore** it anywhere — including from pasted text.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g snap-scaffold
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### `snap export`
|
|
18
|
+
|
|
19
|
+
Scans current directory and **appends** a snapshot block to `.snapshot`.
|
|
20
|
+
Never truncates or deletes previous snapshots.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
snap export
|
|
24
|
+
snap export -o my-project.snapshot
|
|
25
|
+
snap export --ignore dist,build,coverage
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### `snap restore`
|
|
29
|
+
|
|
30
|
+
Recreates the directory/file structure. **Never overwrites** existing files.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
snap restore
|
|
34
|
+
snap restore -i my-project.snapshot -d ./new-folder
|
|
35
|
+
snap restore --dry-run # preview only
|
|
36
|
+
snap restore --text "d src f src/index.js" # restore from pasted text
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `snap tree`
|
|
40
|
+
|
|
41
|
+
Prints a pretty visual tree of your current directory (or a snapshot).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
snap tree
|
|
45
|
+
|
|
46
|
+
# Output:
|
|
47
|
+
my-project/
|
|
48
|
+
├── .env
|
|
49
|
+
├── apps/
|
|
50
|
+
│ └── web/
|
|
51
|
+
├── infra/
|
|
52
|
+
│ └── docker-compose.yml
|
|
53
|
+
└── package.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
snap tree -i my-project.snapshot # tree from a saved snapshot
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Snapshot format
|
|
63
|
+
|
|
64
|
+
The `.snapshot` file is plain text:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
# snap snapshot
|
|
68
|
+
# created: 2025-04-01T10:00:00.000Z
|
|
69
|
+
# root: /Users/you/my-project
|
|
70
|
+
#
|
|
71
|
+
# d = directory f = file
|
|
72
|
+
#
|
|
73
|
+
d src
|
|
74
|
+
d src/components
|
|
75
|
+
f src/components/Button.js
|
|
76
|
+
f src/index.js
|
|
77
|
+
f package.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each `export` appends a new timestamped block — you keep a full history.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Safety
|
|
85
|
+
|
|
86
|
+
| Situation | Behaviour |
|
|
87
|
+
| ------------------------------ | ---------------------------- |
|
|
88
|
+
| Export on existing `.snapshot` | Appends — old data untouched |
|
|
89
|
+
| Restore on existing file/dir | Skips it — never overwrites |
|
|
90
|
+
| `node_modules`, `.git` | Always excluded from export |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
|
|
96
|
+
| Flag | Command | Description |
|
|
97
|
+
| --------------------- | ------------ | ---------------------------------- |
|
|
98
|
+
| `-o, --output <file>` | export | Output file (default: `.snapshot`) |
|
|
99
|
+
| `--ignore <a,b>` | export/tree | Comma-separated names to skip |
|
|
100
|
+
| `-i, --input <file>` | restore/tree | Snapshot file to read |
|
|
101
|
+
| `-d, --dest <dir>` | restore | Where to restore (default: cwd) |
|
|
102
|
+
| `-n, --dry-run` | restore | Preview without writing |
|
|
103
|
+
| `--text "<text>"` | restore | Restore from raw pasted text |
|
package/bin/snap.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const minimist = require("minimist");
|
|
5
|
+
const { exportSnap, restoreSnap, printTree } = require("../lib/index");
|
|
6
|
+
|
|
7
|
+
const argv = minimist(process.argv.slice(2), {
|
|
8
|
+
string: ["output", "input", "dest", "ignore", "text"],
|
|
9
|
+
boolean: ["dry-run", "help", "version"],
|
|
10
|
+
alias: {
|
|
11
|
+
o: "output",
|
|
12
|
+
i: "input",
|
|
13
|
+
d: "dest",
|
|
14
|
+
n: "dry-run",
|
|
15
|
+
h: "help",
|
|
16
|
+
v: "version",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const command = argv._[0];
|
|
21
|
+
|
|
22
|
+
// ── Version ──────────────────────────────────────────────────────────────────
|
|
23
|
+
if (argv.version) {
|
|
24
|
+
const pkg = require("../package.json");
|
|
25
|
+
console.log(`snap-scaffold v${pkg.version}`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Help ─────────────────────────────────────────────────────────────────────
|
|
30
|
+
const HELP = `
|
|
31
|
+
snap — snapshot and scaffold directory structures
|
|
32
|
+
|
|
33
|
+
COMMANDS
|
|
34
|
+
snap export Scan current dir → write .snapshot file
|
|
35
|
+
snap restore Read .snapshot → recreate files & folders
|
|
36
|
+
snap tree Print a visual ├── tree of current dir
|
|
37
|
+
|
|
38
|
+
EXPORT
|
|
39
|
+
-o, --output <file> Output file (default: .snapshot)
|
|
40
|
+
--ignore <a,b,c> Extra names to skip (comma-separated)
|
|
41
|
+
|
|
42
|
+
RESTORE
|
|
43
|
+
-i, --input <file> Snapshot file (default: .snapshot)
|
|
44
|
+
-d, --dest <dir> Where to restore (default: cwd)
|
|
45
|
+
-n, --dry-run Preview without writing anything
|
|
46
|
+
--text "<d f ...>" Restore directly from pasted snapshot text
|
|
47
|
+
|
|
48
|
+
TREE
|
|
49
|
+
-i, --input <file> Show tree from a snapshot file instead of live fs
|
|
50
|
+
--ignore <a,b,c> Extra names to skip
|
|
51
|
+
|
|
52
|
+
NOTES
|
|
53
|
+
• export NEVER deletes or truncates — it always appends a new block
|
|
54
|
+
• restore NEVER overwrites — only creates what is missing
|
|
55
|
+
• node_modules, .git, .snapshot are always excluded from export
|
|
56
|
+
|
|
57
|
+
EXAMPLES
|
|
58
|
+
snap export
|
|
59
|
+
snap export -o project.snapshot --ignore dist,build
|
|
60
|
+
snap restore
|
|
61
|
+
snap restore -i project.snapshot -d ./new-project --dry-run
|
|
62
|
+
snap restore --text "d src f src/index.js f README.md"
|
|
63
|
+
snap tree
|
|
64
|
+
snap tree -i project.snapshot
|
|
65
|
+
`.trimStart();
|
|
66
|
+
|
|
67
|
+
if (argv.help || !command) {
|
|
68
|
+
process.stdout.write(HELP);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (command === "export") {
|
|
76
|
+
const extra = argv.ignore
|
|
77
|
+
? String(argv.ignore)
|
|
78
|
+
.split(",")
|
|
79
|
+
.map((s) => s.trim())
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
: [];
|
|
82
|
+
exportSnap({ rootDir: process.cwd(), output: argv.output, ignore: extra });
|
|
83
|
+
} else if (command === "restore") {
|
|
84
|
+
if (argv.text) {
|
|
85
|
+
restoreSnap({
|
|
86
|
+
rawText: String(argv.text),
|
|
87
|
+
destDir: argv.dest,
|
|
88
|
+
dryRun: argv["dry-run"],
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
restoreSnap({
|
|
92
|
+
input: argv.input,
|
|
93
|
+
destDir: argv.dest,
|
|
94
|
+
dryRun: argv["dry-run"],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} else if (command === "tree") {
|
|
98
|
+
const extra = argv.ignore
|
|
99
|
+
? String(argv.ignore)
|
|
100
|
+
.split(",")
|
|
101
|
+
.map((s) => s.trim())
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
: [];
|
|
104
|
+
printTree({ rootDir: process.cwd(), input: argv.input, ignore: extra });
|
|
105
|
+
} else {
|
|
106
|
+
console.error(`❌ Unknown command: "${command}"\nRun: snap --help`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`❌ ${err.message}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
// ── Always-excluded names ────────────────────────────────────────────────────
|
|
7
|
+
const DEFAULT_IGNORE = new Set([
|
|
8
|
+
"node_modules",
|
|
9
|
+
".git",
|
|
10
|
+
".DS_Store",
|
|
11
|
+
"Thumbs.db",
|
|
12
|
+
".snapshot",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function readDir(dir) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sorted(entries) {
|
|
26
|
+
return [...entries].sort((a, b) => {
|
|
27
|
+
if (a.isDirectory() === b.isDirectory())
|
|
28
|
+
return a.name.localeCompare(b.name);
|
|
29
|
+
return a.isDirectory() ? -1 : 1; // dirs first
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── WALK (shared by export + tree) ──────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns flat list: [{ type:'dir'|'file', rel:string, depth:number }]
|
|
37
|
+
*/
|
|
38
|
+
function walk(rootDir, ignoreSet) {
|
|
39
|
+
const results = [];
|
|
40
|
+
|
|
41
|
+
function recurse(current, depth) {
|
|
42
|
+
for (const entry of sorted(readDir(current))) {
|
|
43
|
+
if (ignoreSet.has(entry.name)) continue;
|
|
44
|
+
|
|
45
|
+
const abs = path.join(current, entry.name);
|
|
46
|
+
const rel = path.relative(rootDir, abs).replace(/\\/g, "/"); // normalise to forward slashes
|
|
47
|
+
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
results.push({ type: "dir", rel, depth });
|
|
50
|
+
recurse(abs, depth + 1);
|
|
51
|
+
} else {
|
|
52
|
+
results.push({ type: "file", rel, depth });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
recurse(rootDir, 0);
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
62
|
+
// EXPORT
|
|
63
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Snapshot the directory into a .snapshot file.
|
|
67
|
+
* ALWAYS appends — never truncates.
|
|
68
|
+
*
|
|
69
|
+
* File format:
|
|
70
|
+
* # snap snapshot
|
|
71
|
+
* # created: <ISO>
|
|
72
|
+
* # root: <abs path>
|
|
73
|
+
* #
|
|
74
|
+
* d src/components
|
|
75
|
+
* f src/components/Button.js
|
|
76
|
+
*/
|
|
77
|
+
function exportSnap(opts = {}) {
|
|
78
|
+
const rootDir = path.resolve(opts.rootDir || process.cwd());
|
|
79
|
+
const output = path.resolve(opts.output || path.join(rootDir, ".snapshot"));
|
|
80
|
+
const ignore = new Set([...DEFAULT_IGNORE, ...(opts.ignore || [])]);
|
|
81
|
+
|
|
82
|
+
console.log(`📂 Scanning ${rootDir}`);
|
|
83
|
+
const entries = walk(rootDir, ignore);
|
|
84
|
+
|
|
85
|
+
const lines = [
|
|
86
|
+
"# snap snapshot",
|
|
87
|
+
`# created: ${new Date().toISOString()}`,
|
|
88
|
+
`# root: ${rootDir}`,
|
|
89
|
+
"#",
|
|
90
|
+
"# d = directory f = file",
|
|
91
|
+
"#",
|
|
92
|
+
...entries.map((e) => `${e.type === "dir" ? "d" : "f"} ${e.rel}`),
|
|
93
|
+
"",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const block = lines.join("\n");
|
|
97
|
+
const sep = "\n# ──────────────────────────────────────────────\n\n";
|
|
98
|
+
const prev = fs.existsSync(output) ? fs.readFileSync(output, "utf8") : "";
|
|
99
|
+
|
|
100
|
+
// Flag "w" re-writes the file but we prepend ALL previous content — safe.
|
|
101
|
+
fs.writeFileSync(output, prev ? prev + sep + block : block, {
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const dirs = entries.filter((e) => e.type === "dir").length;
|
|
106
|
+
const files = entries.filter((e) => e.type === "file").length;
|
|
107
|
+
console.log(`✅ Snapshot → ${output}`);
|
|
108
|
+
console.log(` ${dirs} dirs, ${files} files`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
112
|
+
// RESTORE
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse the LAST snapshot block in a file.
|
|
117
|
+
*/
|
|
118
|
+
function parseSnapshot(filePath) {
|
|
119
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
120
|
+
const blocks = raw.split(/\n#\s─{10,}.*\n/);
|
|
121
|
+
const last = blocks[blocks.length - 1];
|
|
122
|
+
|
|
123
|
+
let snapshotRoot = null;
|
|
124
|
+
const entries = [];
|
|
125
|
+
|
|
126
|
+
for (const line of last.split("\n")) {
|
|
127
|
+
const t = line.trim();
|
|
128
|
+
if (!t || t.startsWith("#")) {
|
|
129
|
+
const m = t.match(/^#\s*root:\s*(.+)$/);
|
|
130
|
+
if (m) snapshotRoot = m[1].trim();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const m = t.match(/^([df])\s+(.+)$/);
|
|
134
|
+
if (m) entries.push({ type: m[1] === "d" ? "dir" : "file", rel: m[2] });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { entries, snapshotRoot };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parse a RAW text (not a file) — used when user pastes the content directly.
|
|
142
|
+
* Handles both forward and back slashes.
|
|
143
|
+
*/
|
|
144
|
+
function parseRawText(text) {
|
|
145
|
+
const entries = [];
|
|
146
|
+
|
|
147
|
+
// Tokenise: split on whitespace but keep "d"/"f" markers + the path that follows.
|
|
148
|
+
// This handles both:
|
|
149
|
+
// multi-line: "d bin\nf bin/snap.js"
|
|
150
|
+
// single-line: "d bin f bin/snap.js d lib f lib/index.js"
|
|
151
|
+
const tokens = text.trim().split(/\s+/);
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
154
|
+
const tok = tokens[i];
|
|
155
|
+
if (tok === "d" || tok === "f") {
|
|
156
|
+
const relPath = tokens[i + 1];
|
|
157
|
+
if (relPath && relPath !== "d" && relPath !== "f") {
|
|
158
|
+
entries.push({
|
|
159
|
+
type: tok === "d" ? "dir" : "file",
|
|
160
|
+
rel: relPath.replace(/\\/g, "/"), // normalise backslashes
|
|
161
|
+
});
|
|
162
|
+
i++; // consume the path token
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { entries, snapshotRoot: null };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Restore from snapshot.
|
|
172
|
+
* NEVER overwrites existing files or directories.
|
|
173
|
+
*/
|
|
174
|
+
function restoreSnap(opts = {}) {
|
|
175
|
+
const destDir = path.resolve(opts.destDir || process.cwd());
|
|
176
|
+
const dryRun = !!opts.dryRun;
|
|
177
|
+
|
|
178
|
+
let entries, snapshotRoot;
|
|
179
|
+
|
|
180
|
+
if (opts.rawText) {
|
|
181
|
+
// ── restore from pasted text ──────────────────────────────────────────
|
|
182
|
+
({ entries, snapshotRoot } = parseRawText(opts.rawText));
|
|
183
|
+
console.log("📋 Reading from provided text");
|
|
184
|
+
} else {
|
|
185
|
+
const input = path.resolve(
|
|
186
|
+
opts.input || path.join(process.cwd(), ".snapshot"),
|
|
187
|
+
);
|
|
188
|
+
if (!fs.existsSync(input))
|
|
189
|
+
throw new Error(`Snapshot file not found: ${input}`);
|
|
190
|
+
({ entries, snapshotRoot } = parseSnapshot(input));
|
|
191
|
+
console.log(`📄 Reading ${input}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (snapshotRoot) console.log(` Original root ${snapshotRoot}`);
|
|
195
|
+
console.log(` Restoring into ${destDir}`);
|
|
196
|
+
if (dryRun) console.log(" (dry-run — nothing written)\n");
|
|
197
|
+
|
|
198
|
+
let created = 0,
|
|
199
|
+
skipped = 0;
|
|
200
|
+
|
|
201
|
+
for (const e of entries) {
|
|
202
|
+
const target = path.join(destDir, e.rel);
|
|
203
|
+
|
|
204
|
+
if (fs.existsSync(target)) {
|
|
205
|
+
console.log(` ⏭ exists ${e.rel}${e.type === "dir" ? "/" : ""}`);
|
|
206
|
+
skipped++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (e.type === "dir") {
|
|
211
|
+
console.log(` 📁 mkdir ${e.rel}/`);
|
|
212
|
+
if (!dryRun) fs.mkdirSync(target, { recursive: true });
|
|
213
|
+
} else {
|
|
214
|
+
console.log(` 📄 touch ${e.rel}`);
|
|
215
|
+
if (!dryRun) {
|
|
216
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
217
|
+
fs.writeFileSync(target, "", { encoding: "utf8", flag: "wx" });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
created++;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log(
|
|
225
|
+
dryRun
|
|
226
|
+
? `✅ Dry-run done. Would create ${created}, skip ${skipped}.`
|
|
227
|
+
: `✅ Restore done. Created ${created}, skipped ${skipped} (already existed).`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
232
|
+
// TREE (pretty ├── display)
|
|
233
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Print a visual tree, either from the live filesystem or from a snapshot file.
|
|
237
|
+
*
|
|
238
|
+
* jarvis/
|
|
239
|
+
* ├── .env
|
|
240
|
+
* ├── infra/
|
|
241
|
+
* │ └── docker-compose.yml
|
|
242
|
+
* └── apps/
|
|
243
|
+
*/
|
|
244
|
+
function printTree(opts = {}) {
|
|
245
|
+
let entries;
|
|
246
|
+
let label;
|
|
247
|
+
|
|
248
|
+
if (opts.input) {
|
|
249
|
+
// ── from snapshot file ────────────────────────────────────────────────
|
|
250
|
+
const input = path.resolve(opts.input);
|
|
251
|
+
if (!fs.existsSync(input))
|
|
252
|
+
throw new Error(`Snapshot file not found: ${input}`);
|
|
253
|
+
const parsed = parseSnapshot(input);
|
|
254
|
+
entries = parsed.entries.map((e) => ({
|
|
255
|
+
...e,
|
|
256
|
+
depth: e.rel.split("/").length - 1,
|
|
257
|
+
}));
|
|
258
|
+
label = parsed.snapshotRoot
|
|
259
|
+
? path.basename(parsed.snapshotRoot)
|
|
260
|
+
: "(snapshot)";
|
|
261
|
+
} else {
|
|
262
|
+
// ── from live filesystem ──────────────────────────────────────────────
|
|
263
|
+
const rootDir = path.resolve(opts.rootDir || process.cwd());
|
|
264
|
+
const ignore = new Set([...DEFAULT_IGNORE, ...(opts.ignore || [])]);
|
|
265
|
+
entries = walk(rootDir, ignore);
|
|
266
|
+
label = path.basename(rootDir);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Build tree lines
|
|
270
|
+
// We need to know, for each depth, whether there are more siblings coming.
|
|
271
|
+
// Strategy: track the last-child status per depth level.
|
|
272
|
+
|
|
273
|
+
const lines = [`${label}/`];
|
|
274
|
+
|
|
275
|
+
// Convert flat list to a nested structure for proper └── vs ├── detection
|
|
276
|
+
// We'll do it by scanning ahead.
|
|
277
|
+
for (let i = 0; i < entries.length; i++) {
|
|
278
|
+
const e = entries[i];
|
|
279
|
+
const depth = e.depth;
|
|
280
|
+
const name = path.basename(e.rel);
|
|
281
|
+
|
|
282
|
+
// Is this the last entry at this depth under the same parent?
|
|
283
|
+
const parentRel = e.rel.includes("/")
|
|
284
|
+
? e.rel.slice(0, e.rel.lastIndexOf("/"))
|
|
285
|
+
: "";
|
|
286
|
+
let isLast = true;
|
|
287
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
288
|
+
const next = entries[j];
|
|
289
|
+
const nextParent = next.rel.includes("/")
|
|
290
|
+
? next.rel.slice(0, next.rel.lastIndexOf("/"))
|
|
291
|
+
: "";
|
|
292
|
+
if (next.depth < depth) break; // went up — we were last
|
|
293
|
+
if (next.depth === depth && nextParent === parentRel) {
|
|
294
|
+
isLast = false;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Build the prefix: for each ancestor depth, check if that ancestor was last
|
|
300
|
+
let prefix = "";
|
|
301
|
+
for (let d = 0; d < depth; d++) {
|
|
302
|
+
// Find the ancestor at depth d that contains this entry
|
|
303
|
+
let ancestorIsLast = true;
|
|
304
|
+
// Look at what comes after the ancestor
|
|
305
|
+
// Find ancestor rel
|
|
306
|
+
const parts = e.rel.split("/");
|
|
307
|
+
const ancestorRel = parts.slice(0, d + 1).join("/");
|
|
308
|
+
const ancestorParent = d > 0 ? parts.slice(0, d).join("/") : "";
|
|
309
|
+
|
|
310
|
+
for (let j = 0; j < entries.length; j++) {
|
|
311
|
+
const candidate = entries[j];
|
|
312
|
+
const candidateParent = candidate.rel.includes("/")
|
|
313
|
+
? candidate.rel.slice(0, candidate.rel.lastIndexOf("/"))
|
|
314
|
+
: "";
|
|
315
|
+
if (candidate.depth === d && candidateParent === ancestorParent) {
|
|
316
|
+
if (candidate.rel === ancestorRel) {
|
|
317
|
+
// Now scan forward to see if there are more siblings after this ancestor
|
|
318
|
+
ancestorIsLast = true;
|
|
319
|
+
for (let k = j + 1; k < entries.length; k++) {
|
|
320
|
+
const sib = entries[k];
|
|
321
|
+
const sibParent = sib.rel.includes("/")
|
|
322
|
+
? sib.rel.slice(0, sib.rel.lastIndexOf("/"))
|
|
323
|
+
: "";
|
|
324
|
+
if (sib.depth < d) break;
|
|
325
|
+
if (sib.depth === d && sibParent === ancestorParent) {
|
|
326
|
+
ancestorIsLast = false;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
prefix += ancestorIsLast ? " " : "│ ";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const branch = isLast ? "└── " : "├── ";
|
|
338
|
+
const suffix = e.type === "dir" ? "/" : "";
|
|
339
|
+
lines.push(`${prefix}${branch}${name}${suffix}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const output = lines.join("\n");
|
|
343
|
+
console.log(output);
|
|
344
|
+
return output;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = { exportSnap, restoreSnap, printTree, parseRawText };
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "snap-scaffold",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Snapshot your directory structure and restore it anywhere",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"snap": "bin/snap.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"cli",
|
|
11
|
+
"scaffold",
|
|
12
|
+
"snapshot",
|
|
13
|
+
"directory",
|
|
14
|
+
"file-structure",
|
|
15
|
+
"tree"
|
|
16
|
+
],
|
|
17
|
+
"author": "mrxgroot",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"minimist": "^1.2.8"
|
|
21
|
+
}
|
|
22
|
+
}
|