flatten-tool 1.3.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/index.ts +73 -2
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -16,7 +16,7 @@ npm install -g flatten-tool
16
16
 
17
17
  ## Usage
18
18
 
19
- By default, the tool merges all file contents into a single Markdown file, with each file's content placed under a header with its relative path, followed by a code block with appropriate language highlighting based on the file extension. Ignores and filters are applied as usual.
19
+ By default, the tool merges all file contents into a single Markdown file, starting with a project file tree for navigation, followed by each file's content under a header with its relative path, in a code block with appropriate language highlighting based on the file extension. Ignores and filters are applied as usual.
20
20
 
21
21
  The `<source>` argument is optional and defaults to the current directory (`.`). The `<target>` argument is also optional and defaults to `flattened.md` (or `flattened/` when using `--directory`).
22
22
 
@@ -113,6 +113,12 @@ This project uses Bun for runtime, TypeScript for type safety, and follows the g
113
113
 
114
114
  ## Changelog
115
115
 
116
+ ### v1.4.0
117
+ - Added project file tree to the beginning of merged Markdown output for better navigation.
118
+
119
+ ### v1.3.1
120
+ - Fixed GIF exclusion pattern to work recursively in subdirectories.
121
+
116
122
  ### v1.3.0
117
123
  - Excluded GIF files by default when merging to Markdown to prevent binary content corruption.
118
124
 
package/index.ts CHANGED
@@ -7,11 +7,65 @@ import yargs from 'yargs';
7
7
  import { hideBin } from 'yargs/helpers';
8
8
  import { globby } from 'globby';
9
9
  import pkg from './package.json' assert { type: 'json' };
10
+ import treeify from 'treeify';
10
11
 
11
12
  function escapePathComponent(component: string): string {
12
13
  return component.replace(/_/g, '__');
13
14
  }
14
15
 
16
+ function buildTreeObject(relPaths: string[]): any {
17
+ const tree: any = {};
18
+
19
+ relPaths.forEach((path) => {
20
+ // Normalize paths
21
+ path = path.replace(/\\/g, '/');
22
+ if (path.startsWith('./')) path = path.slice(2);
23
+
24
+ const parts = path.split('/');
25
+ let node = tree;
26
+
27
+ parts.forEach((part, index) => {
28
+ const isDir = index < parts.length - 1;
29
+ const key = isDir ? `${part}/` : part;
30
+
31
+ if (!node[key]) {
32
+ node[key] = isDir ? {} : null;
33
+ }
34
+
35
+ if (isDir) {
36
+ node = node[key];
37
+ }
38
+ });
39
+ });
40
+
41
+ // Recursively sort: directories first, then files, case-insensitive
42
+ function sortNode(node: any): void {
43
+ if (node === null || typeof node !== 'object') return;
44
+
45
+ const entries = Object.entries(node);
46
+ entries.sort(([a], [b]) => {
47
+ const aIsDir = a.endsWith('/');
48
+ const bIsDir = b.endsWith('/');
49
+ if (aIsDir !== bIsDir) return aIsDir ? -1 : 1; // dirs before files
50
+ return a.toLowerCase().localeCompare(b.toLowerCase());
51
+ });
52
+
53
+ // Rebuild node in sorted order (preserves insertion order for rendering)
54
+ const sorted: any = {};
55
+ entries.forEach(([key, value]) => {
56
+ sorted[key] = value;
57
+ sortNode(value);
58
+ });
59
+
60
+ Object.keys(node).forEach((k) => delete node[k]);
61
+ Object.assign(node, sorted);
62
+ }
63
+
64
+ sortNode(tree);
65
+
66
+ return tree;
67
+ }
68
+
15
69
  async function removeEmptyDirs(dir: string, root?: string): Promise<void> {
16
70
  const entries = await readdir(dir, { withFileTypes: true });
17
71
  for (const entry of entries) {
@@ -49,10 +103,10 @@ export async function flattenDirectory(
49
103
 
50
104
  const ignoreFiles = ['.git'];
51
105
  if (!flattenToDirectory) {
52
- ignoreFiles.push('*.gif');
106
+ ignoreFiles.push('**/*.gif');
53
107
  }
54
108
 
55
- const files = await globby(['**', ...negativeIgnores], {
109
+ const files = await globby(['**', ...negativeIgnores], {
56
110
  cwd: absSource,
57
111
  gitignore: respectGitignore,
58
112
  absolute: true,
@@ -73,9 +127,26 @@ export async function flattenDirectory(
73
127
  if (err.code !== 'ENOENT') throw err;
74
128
  }
75
129
 
130
+ const relPaths: string[] = files.map((srcPath) =>
131
+ relative(absSource, srcPath).replace(/\\/g, '/')
132
+ );
133
+
134
+ // Sort paths for consistent insertion
135
+ relPaths.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
136
+
137
+ const treeObj = buildTreeObject(relPaths);
138
+
139
+ // Wrap with root for nice "." header (empty dirs show just ".")
140
+ const rootTree = { '.': treeObj };
141
+
76
142
  const writeStream = createWriteStream(absTarget);
77
143
  writeStream.setMaxListeners(0);
78
144
 
145
+ writeStream.write("# Project File Tree\n\n");
146
+ writeStream.write("```\n");
147
+ writeStream.write(treeify.asTree(rootTree));
148
+ writeStream.write("```\n\n");
149
+
79
150
  for (const srcPath of files) {
80
151
  const relPath = relative(absSource, srcPath).replace(/\\/g, '/');
81
152
  let ext = extname(srcPath).slice(1) || 'text';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flatten-tool",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI tool to flatten directory structures: merge files into a single Markdown file (default) or copy/move to a flat directory with escaped filenames. Respects .gitignore, supports move/overwrite, and more.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -49,6 +49,7 @@
49
49
  "globby": "^16.1.0",
50
50
  "ignore": "^7.0.5",
51
51
  "minimatch": "^10.1.2",
52
+ "treeify": "^1.1.0",
52
53
  "yargs": "^18.0.0"
53
54
  }
54
55
  }