treezip 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 ADDED
@@ -0,0 +1,139 @@
1
+ # treezip
2
+
3
+ Compressed directory trees for AI prompts. Zero dependencies.
4
+
5
+ `tree` output is verbose — `treezip` compresses it using brace grouping, directory collapsing, and truncation so you can fit more codebase structure into fewer tokens.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g treezip
11
+ ```
12
+
13
+ Or run directly:
14
+
15
+ ```bash
16
+ npx treezip ./my-project
17
+ ```
18
+
19
+ ## Before / After
20
+
21
+ **`tree` (43 lines):**
22
+
23
+ ```
24
+ my-project/
25
+ ├── docs
26
+ │ ├── api-reference.md
27
+ │ ├── contributing.md
28
+ │ └── getting-started.md
29
+ ├── package.json
30
+ ├── public
31
+ │ └── images
32
+ │ ├── avatar.png
33
+ │ ├── background.png
34
+ │ ├── badge.png
35
+ │ ├── banner.png
36
+ │ ├── feature.png
37
+ │ ├── hero.png
38
+ │ ├── icon.png
39
+ │ ├── logo.png
40
+ │ ├── pattern.png
41
+ │ ├── placeholder.png
42
+ │ └── screenshot.png
43
+ ├── README.md
44
+ ├── src
45
+ │ ├── App.tsx
46
+ │ ├── components
47
+ │ │ ├── Button.tsx
48
+ │ │ ├── Card.tsx
49
+ │ │ ├── Header.tsx
50
+ │ │ ├── Modal.tsx
51
+ │ │ └── Sidebar.tsx
52
+ │ ├── hooks
53
+ │ │ ├── useApi.ts
54
+ │ │ ├── useAuth.ts
55
+ │ │ └── useTheme.ts
56
+ │ ├── index.ts
57
+ │ └── utils
58
+ │ ├── api.ts
59
+ │ ├── auth.ts
60
+ │ ├── format.ts
61
+ │ └── validate.ts
62
+ ├── tests
63
+ │ ├── Button.test.tsx
64
+ │ ├── Card.test.tsx
65
+ │ ├── Header.test.tsx
66
+ │ ├── Modal.test.tsx
67
+ │ ├── setup.ts
68
+ │ └── Sidebar.test.tsx
69
+ └── tsconfig.json
70
+ ```
71
+
72
+ **`treezip` (17 lines):**
73
+
74
+ ```
75
+ my-project/
76
+ ├── {.gitignore,README.md,package.json,tsconfig.json}
77
+ ├── docs/{api-reference,contributing,getting-started}.md
78
+ ├── public/images/{avatar,background,badge,banner,feature,...}.png
79
+ ├── src/
80
+ │ ├── {App.tsx,index.ts}
81
+ │ ├── components/{Button,Card,Header,Modal,Sidebar}.tsx
82
+ │ ├── hooks/{useApi,useAuth,useTheme}.ts
83
+ │ └── utils/{api,auth,format,validate}.ts
84
+ └── tests/
85
+ ├── setup.ts
86
+ └── {Button,Card,Header,Modal,Sidebar}.test.tsx
87
+ ```
88
+
89
+ **60% fewer lines.** Same information. Every brace group decompresses to exactly one file per entry.
90
+
91
+ ## How it works
92
+
93
+ | Pattern | Meaning | Example |
94
+ |---|---|---|
95
+ | `{a,b,c}.ext` | Siblings sharing an extension | `{Button,Card,Modal}.tsx` |
96
+ | `{a.x,b.y}` | Siblings with different extensions | `{README.md,tsconfig.json}` |
97
+ | `dir/{stems}.ext` | All files in dir share an extension | `utils/{api,auth,format}.ts` |
98
+ | `dir/file` | Single-child directory collapsed | `scripts/build.sh` |
99
+ | `{a,b,...}.ext` | Truncated group (more files exist) | `images/{logo,hero,...}.png` |
100
+
101
+ ### Compression rules
102
+
103
+ 1. **Mixed directories** (files + subdirs): all files grouped into one `{...}` set
104
+ 2. **Files-only directories**: grouped by shared extension (compound-aware: `.test.ts`, `.config.js`)
105
+ 3. **Inline collapse**: directories that compress to a single item shown inline
106
+ 4. **Single-child chain**: `a/b/c.txt` when each level has one child
107
+ 5. **Truncation**: groups with >8 items show the first 5 + `...`
108
+
109
+ ### Default ignores
110
+
111
+ `node_modules`, `.git`, `.DS_Store`, `__pycache__`, `.pnpm`
112
+
113
+ ## Output header
114
+
115
+ Every output includes a self-documenting header:
116
+
117
+ ```
118
+ # COMPRESSED FILE TREE — AI READ GUIDE
119
+ # {a,b,c}.ext → siblings sharing extension | scripts/f.js → collapsed single-child dir
120
+ # {...}.ext → truncated list (more exist) | decompress: expand braces × each entry = 1 path
121
+ ```
122
+
123
+ This lets LLMs interpret the format without extra instructions.
124
+
125
+ ## Programmatic API
126
+
127
+ ```ts
128
+ import { scan, compress, render, countNodes } from "treezip";
129
+
130
+ const tree = scan("./my-project");
131
+ const compressed = compress(tree);
132
+ const { dirs, files } = countNodes(tree);
133
+ const output = render("my-project/", compressed);
134
+ console.log(output);
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { scan, countNodes } from "./scanner.js";
4
+ import { compress } from "./compress.js";
5
+ import { render } from "./renderer.js";
6
+ const HEADER = `# COMPRESSED FILE TREE — AI READ GUIDE
7
+ # {a,b,c}.ext → siblings sharing extension | scripts/f.js → collapsed single-child dir
8
+ # {...}.ext → truncated list (more exist) | decompress: expand braces × each entry = 1 path
9
+ #`;
10
+ function main() {
11
+ const args = process.argv.slice(2);
12
+ if (args.includes("--help") || args.includes("-h")) {
13
+ console.log("Usage: treezip [directory]");
14
+ console.log(" Outputs a compressed, AI-optimized directory tree.");
15
+ process.exit(0);
16
+ }
17
+ const target = args.find((a) => !a.startsWith("-")) ?? ".";
18
+ const rootPath = resolve(target);
19
+ const tree = scan(rootPath);
20
+ const compressed = compress(tree);
21
+ const { dirs, files } = countNodes(tree);
22
+ // Use the original argument (or .) as the display root name, with trailing /
23
+ const displayRoot = target.replace(/\/$/, "") + "/";
24
+ const body = render(displayRoot, compressed);
25
+ console.log(HEADER);
26
+ console.log(body);
27
+ console.log(`\n${dirs} directories, ${files} files`);
28
+ }
29
+ main();
@@ -0,0 +1,19 @@
1
+ import type { TreeNode } from "./scanner.js";
2
+ export type CompressedItem = {
3
+ kind: "file";
4
+ name: string;
5
+ } | {
6
+ kind: "brace-group";
7
+ stems: string[];
8
+ ext: string;
9
+ truncated: boolean;
10
+ } | {
11
+ kind: "dir";
12
+ name: string;
13
+ children: CompressedItem[];
14
+ } | {
15
+ kind: "inline-dir";
16
+ prefix: string;
17
+ item: CompressedItem;
18
+ };
19
+ export declare function compress(node: TreeNode): CompressedItem[];
@@ -0,0 +1,134 @@
1
+ const MAX_GROUP_ITEMS = 8;
2
+ const TRUNCATED_SHOW = 5;
3
+ // ── Public API ───────────────────────────────────────────────────────
4
+ export function compress(node) {
5
+ if (!node.isDir)
6
+ return [{ kind: "file", name: node.name }];
7
+ return compressChildren(node);
8
+ }
9
+ // ── Core logic ───────────────────────────────────────────────────────
10
+ function compressChildren(node) {
11
+ const files = node.children.filter((c) => !c.isDir);
12
+ const dirs = node.children.filter((c) => c.isDir);
13
+ const hasFiles = files.length > 0;
14
+ const hasDirs = dirs.length > 0;
15
+ // Recursively compress each subdir first
16
+ const compressedDirs = dirs.map((d) => compressDir(d));
17
+ if (hasFiles && hasDirs) {
18
+ // Rule 1 — Mixed directory: group ALL files into one brace group
19
+ const fileItems = files.length >= 2
20
+ ? [makeBraceGroupFullNames(files.map((f) => f.name))]
21
+ : [{ kind: "file", name: files[0].name }];
22
+ return [...fileItems, ...compressedDirs];
23
+ }
24
+ if (hasFiles && !hasDirs) {
25
+ // Rule 2 — Files-only directory: group by extension
26
+ return groupByExtension(files.map((f) => f.name));
27
+ }
28
+ // Dirs only
29
+ return compressedDirs;
30
+ }
31
+ /** Compress a single directory node, applying inline/single-child collapse */
32
+ function compressDir(node) {
33
+ // Rule 4 — Single-child chain collapse
34
+ const chain = [node.name];
35
+ let current = node;
36
+ while (current.children.length === 1) {
37
+ const only = current.children[0];
38
+ if (only.isDir) {
39
+ chain.push(only.name);
40
+ current = only;
41
+ }
42
+ else {
43
+ // Single file child → full chain collapse
44
+ return {
45
+ kind: "inline-dir",
46
+ prefix: chain.join("/"),
47
+ item: { kind: "file", name: only.name },
48
+ };
49
+ }
50
+ }
51
+ const chainPrefix = chain.join("/");
52
+ const children = compressChildren(current);
53
+ // Rule 3 — Inline collapse: if files-only dir compresses to 1 item
54
+ const onlyFiles = current.children.every((c) => !c.isDir);
55
+ if (onlyFiles && children.length === 1) {
56
+ return { kind: "inline-dir", prefix: chainPrefix, item: children[0] };
57
+ }
58
+ return { kind: "dir", name: chainPrefix, children };
59
+ }
60
+ // ── Extension-based grouping (Rule 2) ────────────────────────────────
61
+ function getExtensionCandidates(filename) {
62
+ const parts = filename.split(".");
63
+ if (parts.length <= 1)
64
+ return []; // no extension (e.g., LICENSE, gitignore)
65
+ const candidates = [];
66
+ // Longest first: .test.ts before .ts
67
+ for (let i = 1; i < parts.length; i++) {
68
+ candidates.push("." + parts.slice(i).join("."));
69
+ }
70
+ candidates.reverse(); // longest first
71
+ return candidates;
72
+ }
73
+ function getStem(filename, ext) {
74
+ return filename.slice(0, filename.length - ext.length);
75
+ }
76
+ function groupByExtension(filenames) {
77
+ // Build extension → files mapping
78
+ const extToFiles = new Map();
79
+ for (const f of filenames) {
80
+ for (const ext of getExtensionCandidates(f)) {
81
+ if (!extToFiles.has(ext))
82
+ extToFiles.set(ext, []);
83
+ extToFiles.get(ext).push(f);
84
+ }
85
+ }
86
+ // Sort by extension length (longest first) for greedy assignment
87
+ const sortedExts = [...extToFiles.entries()]
88
+ .filter(([, files]) => files.length >= 2)
89
+ .sort((a, b) => b[0].length - a[0].length);
90
+ const assigned = new Set();
91
+ const groups = [];
92
+ for (const [ext, extFiles] of sortedExts) {
93
+ const unassigned = extFiles.filter((f) => !assigned.has(f));
94
+ if (unassigned.length >= 2) {
95
+ groups.push({ ext, files: unassigned.sort() });
96
+ for (const f of unassigned)
97
+ assigned.add(f);
98
+ }
99
+ }
100
+ // Remaining files are standalones
101
+ const standalones = filenames.filter((f) => !assigned.has(f)).sort();
102
+ const result = [];
103
+ // Standalones first
104
+ if (standalones.length >= 2) {
105
+ result.push(makeBraceGroupFullNames(standalones));
106
+ }
107
+ else if (standalones.length === 1) {
108
+ result.push({ kind: "file", name: standalones[0] });
109
+ }
110
+ // Then extension groups (sorted by extension alphabetically)
111
+ groups.sort((a, b) => a.ext.localeCompare(b.ext));
112
+ for (const g of groups) {
113
+ const stems = g.files.map((f) => getStem(f, g.ext));
114
+ const truncated = stems.length > MAX_GROUP_ITEMS;
115
+ result.push({
116
+ kind: "brace-group",
117
+ stems: truncated ? stems.slice(0, TRUNCATED_SHOW) : stems,
118
+ ext: g.ext,
119
+ truncated,
120
+ });
121
+ }
122
+ return result;
123
+ }
124
+ // ── Helpers ──────────────────────────────────────────────────────────
125
+ function makeBraceGroupFullNames(names) {
126
+ const sorted = [...names].sort();
127
+ const truncated = sorted.length > MAX_GROUP_ITEMS;
128
+ return {
129
+ kind: "brace-group",
130
+ stems: truncated ? sorted.slice(0, TRUNCATED_SHOW) : sorted,
131
+ ext: "",
132
+ truncated,
133
+ };
134
+ }
@@ -0,0 +1,5 @@
1
+ export { scan, countNodes } from "./scanner.js";
2
+ export type { TreeNode } from "./scanner.js";
3
+ export { compress } from "./compress.js";
4
+ export type { CompressedItem } from "./compress.js";
5
+ export { render } from "./renderer.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { scan, countNodes } from "./scanner.js";
2
+ export { compress } from "./compress.js";
3
+ export { render } from "./renderer.js";
@@ -0,0 +1,2 @@
1
+ import type { CompressedItem } from "./compress.js";
2
+ export declare function render(rootName: string, items: CompressedItem[]): string;
@@ -0,0 +1,46 @@
1
+ const PIPE = "│ ";
2
+ const TEE = "├── ";
3
+ const ELBOW = "└── ";
4
+ const SPACE = " ";
5
+ export function render(rootName, items) {
6
+ const lines = [rootName];
7
+ renderItems(items, "", lines);
8
+ return lines.join("\n");
9
+ }
10
+ function renderItems(items, prefix, lines) {
11
+ for (let i = 0; i < items.length; i++) {
12
+ const isLast = i === items.length - 1;
13
+ const connector = isLast ? ELBOW : TEE;
14
+ const childPrefix = prefix + (isLast ? SPACE : PIPE);
15
+ const item = items[i];
16
+ switch (item.kind) {
17
+ case "file":
18
+ lines.push(`${prefix}${connector}${item.name}`);
19
+ break;
20
+ case "brace-group":
21
+ lines.push(`${prefix}${connector}${formatBraceGroup(item.stems, item.ext, item.truncated)}`);
22
+ break;
23
+ case "inline-dir":
24
+ lines.push(`${prefix}${connector}${item.prefix}/${formatInlineItem(item.item)}`);
25
+ break;
26
+ case "dir":
27
+ lines.push(`${prefix}${connector}${item.name}/`);
28
+ renderItems(item.children, childPrefix, lines);
29
+ break;
30
+ }
31
+ }
32
+ }
33
+ function formatBraceGroup(stems, ext, truncated) {
34
+ const inner = truncated ? [...stems, "..."].join(",") : stems.join(",");
35
+ return `{${inner}}${ext}`;
36
+ }
37
+ function formatInlineItem(item) {
38
+ switch (item.kind) {
39
+ case "file":
40
+ return item.name;
41
+ case "brace-group":
42
+ return formatBraceGroup(item.stems, item.ext, item.truncated);
43
+ default:
44
+ return "?";
45
+ }
46
+ }
@@ -0,0 +1,11 @@
1
+ export interface TreeNode {
2
+ name: string;
3
+ isDir: boolean;
4
+ children: TreeNode[];
5
+ }
6
+ export declare function scan(rootPath: string, ignores?: Set<string>): TreeNode;
7
+ /** Count total files and directories in a TreeNode (excludes root) */
8
+ export declare function countNodes(node: TreeNode): {
9
+ dirs: number;
10
+ files: number;
11
+ };
@@ -0,0 +1,64 @@
1
+ import { readdirSync, statSync, lstatSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const DEFAULT_IGNORES = new Set([
4
+ "node_modules",
5
+ ".git",
6
+ ".DS_Store",
7
+ ".Spotlight-V100",
8
+ ".Trashes",
9
+ "__pycache__",
10
+ ".pnpm",
11
+ ]);
12
+ export function scan(rootPath, ignores = DEFAULT_IGNORES) {
13
+ const stat = statSync(rootPath);
14
+ const name = rootPath.split("/").filter(Boolean).pop() ?? rootPath;
15
+ if (!stat.isDirectory()) {
16
+ return { name, isDir: false, children: [] };
17
+ }
18
+ const entries = readdirSync(rootPath).filter((e) => !ignores.has(e)).sort();
19
+ const files = [];
20
+ const dirs = [];
21
+ for (const entry of entries) {
22
+ const fullPath = join(rootPath, entry);
23
+ let entryStat;
24
+ try {
25
+ entryStat = lstatSync(fullPath);
26
+ }
27
+ catch {
28
+ continue;
29
+ }
30
+ // Skip symlinks
31
+ if (entryStat.isSymbolicLink())
32
+ continue;
33
+ if (entryStat.isDirectory()) {
34
+ dirs.push(scan(fullPath, ignores));
35
+ }
36
+ else if (entryStat.isFile()) {
37
+ files.push({ name: entry, isDir: false, children: [] });
38
+ }
39
+ }
40
+ // Files first (alphabetical), then dirs (alphabetical)
41
+ return {
42
+ name,
43
+ isDir: true,
44
+ children: [...files, ...dirs],
45
+ };
46
+ }
47
+ /** Count total files and directories in a TreeNode (excludes root) */
48
+ export function countNodes(node) {
49
+ let dirs = 0;
50
+ let files = 0;
51
+ function walk(n) {
52
+ for (const child of n.children) {
53
+ if (child.isDir) {
54
+ dirs++;
55
+ walk(child);
56
+ }
57
+ else {
58
+ files++;
59
+ }
60
+ }
61
+ }
62
+ walk(node);
63
+ return { dirs, files };
64
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "treezip",
3
+ "version": "1.0.0",
4
+ "description": "Compressed tree output for AI-optimized codebase summaries",
5
+ "type": "module",
6
+ "bin": {
7
+ "treezip": "dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsx src/cli.ts",
21
+ "test": "vitest run",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/yigitkonur/treezip.git"
30
+ },
31
+ "homepage": "https://github.com/yigitkonur/treezip",
32
+ "bugs": {
33
+ "url": "https://github.com/yigitkonur/treezip/issues"
34
+ },
35
+ "author": "Yigit Konur",
36
+ "devDependencies": {
37
+ "@types/node": "^25.5.0",
38
+ "tsx": "^4.19.0",
39
+ "typescript": "^5.7.0",
40
+ "vitest": "^3.0.0"
41
+ },
42
+ "keywords": [
43
+ "tree",
44
+ "compressed",
45
+ "ai",
46
+ "codebase",
47
+ "cli",
48
+ "directory",
49
+ "structure",
50
+ "llm",
51
+ "context",
52
+ "brace-expansion",
53
+ "npx"
54
+ ],
55
+ "license": "MIT",
56
+ "engines": {
57
+ "node": ">=18"
58
+ }
59
+ }