@crashbytes/dendro 1.0.2 → 1.1.1
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/.github/dependabot.yml +50 -0
- package/.github/workflows/release.yml +48 -0
- package/.github/workflows/test.yml +39 -0
- package/CHANGELOG.md +16 -0
- package/README.md +3 -3
- package/bin/cli.js +5 -5
- package/deploy.sh +95 -0
- package/docs/.nojekyll +0 -0
- package/docs/index.html +360 -0
- package/index.js +3 -3
- package/package.json +14 -5
- package/test/index.test.js +387 -0
- package/test.js +2 -2
- package/vitest.config.js +26 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "npm"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
day: "monday"
|
|
8
|
+
time: "09:00"
|
|
9
|
+
open-pull-requests-limit: 10
|
|
10
|
+
groups:
|
|
11
|
+
# Group all production dependencies
|
|
12
|
+
production-dependencies:
|
|
13
|
+
applies-to: version-updates
|
|
14
|
+
dependency-type: "production"
|
|
15
|
+
update-types:
|
|
16
|
+
- "minor"
|
|
17
|
+
- "patch"
|
|
18
|
+
|
|
19
|
+
# Group development dependencies
|
|
20
|
+
development-dependencies:
|
|
21
|
+
applies-to: version-updates
|
|
22
|
+
dependency-type: "development"
|
|
23
|
+
update-types:
|
|
24
|
+
- "minor"
|
|
25
|
+
- "patch"
|
|
26
|
+
|
|
27
|
+
# Auto-assign PRs
|
|
28
|
+
assignees:
|
|
29
|
+
- "MichaelEakins"
|
|
30
|
+
|
|
31
|
+
# Labels for PRs
|
|
32
|
+
labels:
|
|
33
|
+
- "dependencies"
|
|
34
|
+
- "automated"
|
|
35
|
+
|
|
36
|
+
commit-message:
|
|
37
|
+
prefix: "chore(deps)"
|
|
38
|
+
include: "scope"
|
|
39
|
+
|
|
40
|
+
- package-ecosystem: "github-actions"
|
|
41
|
+
directory: "/"
|
|
42
|
+
schedule:
|
|
43
|
+
interval: "weekly"
|
|
44
|
+
day: "monday"
|
|
45
|
+
time: "09:00"
|
|
46
|
+
labels:
|
|
47
|
+
- "dependencies"
|
|
48
|
+
- "github-actions"
|
|
49
|
+
commit-message:
|
|
50
|
+
prefix: "chore(actions)"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
# CRITICAL: Required for npm Trusted Publishing (OIDC authentication)
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write # Needed to create GitHub releases
|
|
11
|
+
id-token: write # Needed for npm provenance/trusted publishing
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js
|
|
22
|
+
uses: actions/setup-node@v6
|
|
23
|
+
with:
|
|
24
|
+
node-version: '20'
|
|
25
|
+
# NO registry-url - prevents .npmrc conflicts with OIDC
|
|
26
|
+
|
|
27
|
+
- name: Upgrade npm for Trusted Publishing
|
|
28
|
+
run: npm install -g npm@latest # Requires npm >= 11.5.1
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: npm ci
|
|
32
|
+
|
|
33
|
+
- name: Test
|
|
34
|
+
run: npm test
|
|
35
|
+
|
|
36
|
+
- name: Publish to npm with Provenance
|
|
37
|
+
run: npm publish --provenance --access public
|
|
38
|
+
# OIDC authentication via GitHub Actions - no tokens needed!
|
|
39
|
+
|
|
40
|
+
- name: Create GitHub Release
|
|
41
|
+
uses: softprops/action-gh-release@v2
|
|
42
|
+
with:
|
|
43
|
+
tag_name: ${{ github.ref }}
|
|
44
|
+
name: Release ${{ github.ref_name }}
|
|
45
|
+
body: |
|
|
46
|
+
See [CHANGELOG.md](https://github.com/CrashBytes/dendro/blob/main/CHANGELOG.md) for details.
|
|
47
|
+
draft: false
|
|
48
|
+
prerelease: false
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [20.x, 22.x]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: ${{ matrix.node-version }}
|
|
24
|
+
cache: 'npm'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Run tests with coverage
|
|
30
|
+
run: npm run test:coverage
|
|
31
|
+
|
|
32
|
+
- name: Upload coverage to Codecov
|
|
33
|
+
if: matrix.node-version == '20.x'
|
|
34
|
+
uses: codecov/codecov-action@v4
|
|
35
|
+
with:
|
|
36
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
37
|
+
files: ./coverage/lcov.info
|
|
38
|
+
flags: unittests
|
|
39
|
+
fail_ci_if_error: false
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to dendro will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2025-01-14
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- 🔄 **BREAKING**: Converted project from CommonJS to ESM (ES Modules)
|
|
12
|
+
- ⬆️ Updated chalk from v4.1.2 to v5.6.2 (major version upgrade)
|
|
13
|
+
- 📦 Updated commander to v14.0.2 (already latest)
|
|
14
|
+
- 🔧 Added `"type": "module"` to package.json
|
|
15
|
+
- 📝 Updated all imports to use ESM syntax (`import`/`export` instead of `require`/`module.exports`)
|
|
16
|
+
- 🔗 Updated relative imports to include `.js` extensions for ESM compatibility
|
|
17
|
+
|
|
18
|
+
### Migration Notes
|
|
19
|
+
- Node.js 20+ required (already specified in engines)
|
|
20
|
+
- If you were importing this package, update your code to use ESM imports
|
|
21
|
+
- All functionality remains the same, only the module system changed
|
|
22
|
+
|
|
8
23
|
## [1.0.1] - 2025-10-13
|
|
9
24
|
|
|
10
25
|
### Fixed
|
|
@@ -36,5 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
36
51
|
- 📊 Built-in statistics
|
|
37
52
|
- 🔧 Both CLI and API
|
|
38
53
|
|
|
54
|
+
[1.1.0]: https://github.com/CrashBytes/dendro/releases/tag/v1.1.0
|
|
39
55
|
[1.0.1]: https://github.com/CrashBytes/dendro/releases/tag/v1.0.1
|
|
40
56
|
[1.0.0]: https://github.com/CrashBytes/dendro/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
A beautiful, fast directory tree visualization CLI with intuitive file type icons.
|
|
13
13
|
|
|
14
|
-
🔗 **[GitHub Repository](https://github.com/CrashBytes/dendro)** | 📦 **[npm Package](https://www.npmjs.com/package/@crashbytes/dendro)**
|
|
14
|
+
📚 **[Documentation](https://crashbytes.github.io/dendro/)** | 🔗 **[GitHub Repository](https://github.com/CrashBytes/dendro)** | 📦 **[npm Package](https://www.npmjs.com/package/@crashbytes/dendro)**
|
|
15
|
+
|
|
16
|
+
> **Note:** Requires Node.js 20.0.0 or higher
|
|
15
17
|
|
|
16
18
|
---
|
|
17
19
|
|
|
@@ -368,8 +370,6 @@ MIT License - see [LICENSE](https://github.com/CrashBytes/dendro/blob/main/LICEN
|
|
|
368
370
|
|
|
369
371
|
## Acknowledgments
|
|
370
372
|
|
|
371
|
-
Built with ❤️ for developers who love beautiful CLIs.
|
|
372
|
-
|
|
373
373
|
Special thanks to all [contributors](https://github.com/CrashBytes/dendro/graphs/contributors) who help make dendro better!
|
|
374
374
|
|
|
375
375
|
---
|
package/bin/cli.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { buildTree, renderTree, getTreeStats } from '../index.js';
|
|
7
7
|
|
|
8
8
|
const program = new Command();
|
|
9
9
|
|
|
10
10
|
program
|
|
11
11
|
.name('dendro')
|
|
12
12
|
.description('Display directory tree structure with beautiful icons')
|
|
13
|
-
.version('1.
|
|
13
|
+
.version('1.1.0')
|
|
14
14
|
.argument('[path]', 'Directory path to display', '.')
|
|
15
15
|
.option('-d, --max-depth <number>', 'Maximum depth to traverse', parseInt)
|
|
16
16
|
.option('-a, --all', 'Show hidden files and directories', false)
|
package/deploy.sh
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e # Exit on any error
|
|
3
|
+
|
|
4
|
+
# Configuration
|
|
5
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
6
|
+
|
|
7
|
+
echo "🚀 Dendro v${VERSION} Production Deployment"
|
|
8
|
+
echo "========================================"
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
# Phase 1: Pre-flight Validation
|
|
12
|
+
echo "📋 Phase 1: Pre-flight Validation"
|
|
13
|
+
echo "-----------------------------------"
|
|
14
|
+
|
|
15
|
+
# Check Node.js version
|
|
16
|
+
NODE_VERSION=$(node -v)
|
|
17
|
+
echo "✓ Node.js: $NODE_VERSION"
|
|
18
|
+
|
|
19
|
+
# Check npm version
|
|
20
|
+
NPM_VERSION=$(npm -v)
|
|
21
|
+
echo "✓ npm: $NPM_VERSION"
|
|
22
|
+
|
|
23
|
+
# Verify authentication
|
|
24
|
+
echo -n "✓ npm authentication: "
|
|
25
|
+
npm whoami
|
|
26
|
+
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
# Phase 2: Test Suite Execution
|
|
30
|
+
echo "🧪 Phase 2: Test Suite Execution"
|
|
31
|
+
echo "-----------------------------------"
|
|
32
|
+
npm test
|
|
33
|
+
|
|
34
|
+
echo ""
|
|
35
|
+
|
|
36
|
+
# Phase 3: Package Validation
|
|
37
|
+
echo "📦 Phase 3: Package Validation"
|
|
38
|
+
echo "-----------------------------------"
|
|
39
|
+
echo "Package contents preview:"
|
|
40
|
+
npm pack --dry-run
|
|
41
|
+
|
|
42
|
+
echo ""
|
|
43
|
+
|
|
44
|
+
# Phase 4: Git Operations
|
|
45
|
+
echo "📝 Phase 4: Version Control"
|
|
46
|
+
echo "-----------------------------------"
|
|
47
|
+
|
|
48
|
+
# Check git status
|
|
49
|
+
if [[ -n $(git status -s) ]]; then
|
|
50
|
+
echo "Committing changes..."
|
|
51
|
+
git add package.json .gitignore
|
|
52
|
+
git commit -m "chore: bump version to ${VERSION} - update repository links and secure credentials"
|
|
53
|
+
echo "✓ Changes committed"
|
|
54
|
+
else
|
|
55
|
+
echo "✓ Working directory clean"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Create and push tag
|
|
59
|
+
echo "Creating version tag v${VERSION}..."
|
|
60
|
+
git tag -f "v${VERSION}"
|
|
61
|
+
echo "✓ Tag created"
|
|
62
|
+
|
|
63
|
+
echo "Pushing to remote..."
|
|
64
|
+
git push origin main
|
|
65
|
+
git push origin "v${VERSION}" --force
|
|
66
|
+
echo "✓ Pushed to GitHub"
|
|
67
|
+
|
|
68
|
+
echo ""
|
|
69
|
+
|
|
70
|
+
# Phase 5: NPM Publication
|
|
71
|
+
echo "🚀 Phase 5: NPM Publication"
|
|
72
|
+
echo "-----------------------------------"
|
|
73
|
+
echo "Publishing @crashbytes/dendro@${VERSION}..."
|
|
74
|
+
npm publish --access public
|
|
75
|
+
|
|
76
|
+
echo ""
|
|
77
|
+
|
|
78
|
+
# Phase 6: Post-Deployment Verification
|
|
79
|
+
echo "✅ Phase 6: Post-Deployment Verification"
|
|
80
|
+
echo "-----------------------------------"
|
|
81
|
+
echo "Package details:"
|
|
82
|
+
npm view @crashbytes/dendro
|
|
83
|
+
|
|
84
|
+
echo ""
|
|
85
|
+
echo "=========================================="
|
|
86
|
+
echo "✨ Deployment Complete!"
|
|
87
|
+
echo "=========================================="
|
|
88
|
+
echo ""
|
|
89
|
+
echo "Verification commands:"
|
|
90
|
+
echo " npm install -g @crashbytes/dendro@${VERSION}"
|
|
91
|
+
echo " dendro --version"
|
|
92
|
+
echo ""
|
|
93
|
+
echo "Package URL:"
|
|
94
|
+
echo " https://www.npmjs.com/package/@crashbytes/dendro"
|
|
95
|
+
echo ""
|
package/docs/.nojekyll
ADDED
|
File without changes
|
package/docs/index.html
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Dendro Documentation - Directory Tree Visualization</title>
|
|
7
|
+
<meta name="description" content="A beautiful directory tree visualization CLI with file type icons - dendro (δένδρο) means 'tree' in Greek">
|
|
8
|
+
<style>
|
|
9
|
+
* {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
17
|
+
line-height: 1.6;
|
|
18
|
+
color: #333;
|
|
19
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
padding: 2rem 1rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.container {
|
|
25
|
+
max-width: 900px;
|
|
26
|
+
margin: 0 auto;
|
|
27
|
+
background: white;
|
|
28
|
+
border-radius: 12px;
|
|
29
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.header {
|
|
34
|
+
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
|
35
|
+
color: white;
|
|
36
|
+
padding: 3rem 2rem;
|
|
37
|
+
text-align: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.header h1 {
|
|
41
|
+
font-size: 2.5rem;
|
|
42
|
+
margin-bottom: 0.5rem;
|
|
43
|
+
font-weight: 700;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.header .subtitle {
|
|
47
|
+
font-size: 1.1rem;
|
|
48
|
+
opacity: 0.9;
|
|
49
|
+
font-style: italic;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.content {
|
|
53
|
+
padding: 2rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.badge {
|
|
57
|
+
display: inline-block;
|
|
58
|
+
background: #27ae60;
|
|
59
|
+
color: white;
|
|
60
|
+
padding: 0.25rem 0.75rem;
|
|
61
|
+
border-radius: 12px;
|
|
62
|
+
font-size: 0.85rem;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
margin: 0.5rem 0.25rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.badge.warning {
|
|
68
|
+
background: #e67e22;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.requirements {
|
|
72
|
+
background: #fff3cd;
|
|
73
|
+
border-left: 4px solid #ffc107;
|
|
74
|
+
padding: 1rem;
|
|
75
|
+
margin: 1.5rem 0;
|
|
76
|
+
border-radius: 4px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.requirements strong {
|
|
80
|
+
color: #856404;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.code-block-container {
|
|
84
|
+
position: relative;
|
|
85
|
+
margin: 1.5rem 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.copy-button {
|
|
89
|
+
position: absolute;
|
|
90
|
+
top: 0.5rem;
|
|
91
|
+
right: 0.5rem;
|
|
92
|
+
padding: 0.5rem 1rem;
|
|
93
|
+
background: #667eea;
|
|
94
|
+
color: white;
|
|
95
|
+
border: none;
|
|
96
|
+
border-radius: 6px;
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
font-size: 0.875rem;
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
transition: all 0.2s;
|
|
101
|
+
z-index: 10;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.copy-button:hover {
|
|
105
|
+
background: #764ba2;
|
|
106
|
+
transform: translateY(-1px);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.copy-button.copied {
|
|
110
|
+
background: #27ae60;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pre {
|
|
114
|
+
background: #2c3e50;
|
|
115
|
+
color: #ecf0f1;
|
|
116
|
+
border-radius: 8px;
|
|
117
|
+
padding: 1.5rem;
|
|
118
|
+
overflow-x: auto;
|
|
119
|
+
font-size: 0.9rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
code {
|
|
123
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
h2 {
|
|
127
|
+
color: #2c3e50;
|
|
128
|
+
font-size: 1.8rem;
|
|
129
|
+
margin: 2rem 0 1rem;
|
|
130
|
+
padding-bottom: 0.5rem;
|
|
131
|
+
border-bottom: 2px solid #667eea;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
h3 {
|
|
135
|
+
color: #34495e;
|
|
136
|
+
font-size: 1.3rem;
|
|
137
|
+
margin: 1.5rem 0 0.75rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.section {
|
|
141
|
+
margin-bottom: 2rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.features {
|
|
145
|
+
display: grid;
|
|
146
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
147
|
+
gap: 1rem;
|
|
148
|
+
margin: 1.5rem 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.feature-card {
|
|
152
|
+
background: #f8f9fa;
|
|
153
|
+
padding: 1.5rem;
|
|
154
|
+
border-radius: 8px;
|
|
155
|
+
border-left: 4px solid #667eea;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.feature-card h4 {
|
|
159
|
+
color: #2c3e50;
|
|
160
|
+
margin-bottom: 0.5rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.footer {
|
|
164
|
+
background: #2c3e50;
|
|
165
|
+
color: white;
|
|
166
|
+
padding: 2rem;
|
|
167
|
+
text-align: center;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.footer a {
|
|
171
|
+
color: #667eea;
|
|
172
|
+
text-decoration: none;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.footer a:hover {
|
|
177
|
+
color: #764ba2;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (max-width: 768px) {
|
|
181
|
+
.header h1 {
|
|
182
|
+
font-size: 2rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.content {
|
|
186
|
+
padding: 1.5rem;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
</style>
|
|
190
|
+
</head>
|
|
191
|
+
<body>
|
|
192
|
+
<div class="container">
|
|
193
|
+
<div class="header">
|
|
194
|
+
<h1>🌳 Dendro</h1>
|
|
195
|
+
<p class="subtitle">δένδρο (dendro) means "tree" in Greek</p>
|
|
196
|
+
<div style="margin-top: 1rem;">
|
|
197
|
+
<span class="badge">v1.0.2</span>
|
|
198
|
+
<span class="badge">Node.js 20+</span>
|
|
199
|
+
<span class="badge">MIT License</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="content">
|
|
204
|
+
<section class="section">
|
|
205
|
+
<p style="font-size: 1.1rem; color: #555; margin-bottom: 1.5rem;">
|
|
206
|
+
A beautiful directory tree visualization CLI with file type icons.
|
|
207
|
+
Perfect for exploring project structures, creating documentation, and understanding codebases.
|
|
208
|
+
</p>
|
|
209
|
+
|
|
210
|
+
<div class="requirements">
|
|
211
|
+
<strong>⚠️ Requirements:</strong> Node.js version 20.0.0 or higher is required.
|
|
212
|
+
</div>
|
|
213
|
+
</section>
|
|
214
|
+
|
|
215
|
+
<section class="section">
|
|
216
|
+
<h2>✨ Features</h2>
|
|
217
|
+
<div class="features">
|
|
218
|
+
<div class="feature-card">
|
|
219
|
+
<h4>🎨 Beautiful Icons</h4>
|
|
220
|
+
<p>File type-specific icons for instant recognition</p>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="feature-card">
|
|
223
|
+
<h4>⚡ Lightning Fast</h4>
|
|
224
|
+
<p>Optimized performance for large directories</p>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="feature-card">
|
|
227
|
+
<h4>🎯 Configurable Depth</h4>
|
|
228
|
+
<p>Control how deep to traverse directories</p>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="feature-card">
|
|
231
|
+
<h4>👁️ Hidden Files</h4>
|
|
232
|
+
<p>Toggle visibility of hidden files and folders</p>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</section>
|
|
236
|
+
|
|
237
|
+
<section class="section">
|
|
238
|
+
<h2>📦 Installation</h2>
|
|
239
|
+
|
|
240
|
+
<h3>Via npm (Recommended)</h3>
|
|
241
|
+
<div class="code-block-container">
|
|
242
|
+
<button class="copy-button" onclick="copyCode(this, 'install-npm')">Copy</button>
|
|
243
|
+
<pre><code id="install-npm">npm install -g @crashbytes/dendro</code></pre>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<h3>Via npx (No Installation)</h3>
|
|
247
|
+
<div class="code-block-container">
|
|
248
|
+
<button class="copy-button" onclick="copyCode(this, 'install-npx')">Copy</button>
|
|
249
|
+
<pre><code id="install-npx">npx @crashbytes/dendro</code></pre>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<h3>From Source (GitHub)</h3>
|
|
253
|
+
<div class="code-block-container">
|
|
254
|
+
<button class="copy-button" onclick="copyCode(this, 'install-source')">Copy</button>
|
|
255
|
+
<pre><code id="install-source">git clone https://github.com/CrashBytes/dendro.git
|
|
256
|
+
cd dendro
|
|
257
|
+
npm install
|
|
258
|
+
npm link</code></pre>
|
|
259
|
+
</div>
|
|
260
|
+
</section>
|
|
261
|
+
|
|
262
|
+
<section class="section">
|
|
263
|
+
<h2>🚀 Quick Start</h2>
|
|
264
|
+
|
|
265
|
+
<h3>Basic Usage</h3>
|
|
266
|
+
<div class="code-block-container">
|
|
267
|
+
<button class="copy-button" onclick="copyCode(this, 'quickstart-1')">Copy</button>
|
|
268
|
+
<pre><code id="quickstart-1"># Visualize current directory
|
|
269
|
+
dendro</code></pre>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="code-block-container">
|
|
273
|
+
<button class="copy-button" onclick="copyCode(this, 'quickstart-2')">Copy</button>
|
|
274
|
+
<pre><code id="quickstart-2"># Visualize specific directory
|
|
275
|
+
dendro /path/to/project</code></pre>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<h3>Advanced Options</h3>
|
|
279
|
+
<div class="code-block-container">
|
|
280
|
+
<button class="copy-button" onclick="copyCode(this, 'quickstart-3')">Copy</button>
|
|
281
|
+
<pre><code id="quickstart-3"># Limit depth to 3 levels
|
|
282
|
+
dendro ~/projects -d 3</code></pre>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div class="code-block-container">
|
|
286
|
+
<button class="copy-button" onclick="copyCode(this, 'quickstart-4')">Copy</button>
|
|
287
|
+
<pre><code id="quickstart-4"># Show all files including hidden
|
|
288
|
+
dendro -a</code></pre>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="code-block-container">
|
|
292
|
+
<button class="copy-button" onclick="copyCode(this, 'quickstart-5')">Copy</button>
|
|
293
|
+
<pre><code id="quickstart-5"># Combine options
|
|
294
|
+
dendro ~/my-project -d 2 -a</code></pre>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<h3>Get Help</h3>
|
|
298
|
+
<div class="code-block-container">
|
|
299
|
+
<button class="copy-button" onclick="copyCode(this, 'quickstart-6')">Copy</button>
|
|
300
|
+
<pre><code id="quickstart-6"># Show help
|
|
301
|
+
dendro --help</code></pre>
|
|
302
|
+
</div>
|
|
303
|
+
</section>
|
|
304
|
+
|
|
305
|
+
<section class="section">
|
|
306
|
+
<h2>📖 Command Options</h2>
|
|
307
|
+
<pre style="background: #f8f9fa; color: #2c3e50; border: 1px solid #e1e4e8;">
|
|
308
|
+
Usage: dendro [options] [directory]
|
|
309
|
+
|
|
310
|
+
Arguments:
|
|
311
|
+
directory Directory to visualize (default: current directory)
|
|
312
|
+
|
|
313
|
+
Options:
|
|
314
|
+
-V, --version Output the version number
|
|
315
|
+
-d, --depth <n> Maximum depth to traverse (default: unlimited)
|
|
316
|
+
-a, --all Show hidden files and directories
|
|
317
|
+
-h, --help Display help for command
|
|
318
|
+
</pre>
|
|
319
|
+
</section>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div class="footer">
|
|
323
|
+
<p><a href="https://github.com/CrashBytes" target="_blank">CrashBytes</a></p>
|
|
324
|
+
<p style="margin-top: 0.5rem;">
|
|
325
|
+
<a href="https://github.com/CrashBytes/dendro" target="_blank">GitHub</a> •
|
|
326
|
+
<a href="https://www.npmjs.com/package/@crashbytes/dendro" target="_blank">npm</a> •
|
|
327
|
+
<a href="https://github.com/CrashBytes/dendro/issues" target="_blank">Report Issues</a>
|
|
328
|
+
</p>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<script>
|
|
333
|
+
async function copyCode(button, codeId) {
|
|
334
|
+
const codeElement = document.getElementById(codeId);
|
|
335
|
+
const textToCopy = codeElement.textContent;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
await navigator.clipboard.writeText(textToCopy);
|
|
339
|
+
|
|
340
|
+
// Visual feedback
|
|
341
|
+
const originalText = button.textContent;
|
|
342
|
+
button.textContent = '✓ Copied!';
|
|
343
|
+
button.classList.add('copied');
|
|
344
|
+
|
|
345
|
+
// Reset after 2 seconds
|
|
346
|
+
setTimeout(() => {
|
|
347
|
+
button.textContent = originalText;
|
|
348
|
+
button.classList.remove('copied');
|
|
349
|
+
}, 2000);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.error('Failed to copy text:', err);
|
|
352
|
+
button.textContent = 'Failed';
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
button.textContent = 'Copy';
|
|
355
|
+
}, 2000);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
</script>
|
|
359
|
+
</body>
|
|
360
|
+
</html>
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
3
|
|
|
4
4
|
// Icon mappings for different file types and directories
|
|
5
5
|
const icons = {
|
|
@@ -287,7 +287,7 @@ function getTreeStats(tree) {
|
|
|
287
287
|
return { files, directories };
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
export {
|
|
291
291
|
buildTree,
|
|
292
292
|
renderTree,
|
|
293
293
|
getTreeStats,
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crashbytes/dendro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A beautiful directory tree visualization CLI with file type icons - dendro (δένδρο) means 'tree' in Greek",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "index.js",
|
|
6
7
|
"bin": {
|
|
7
8
|
"dendro": "bin/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"test": "
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"test:coverage": "vitest run --coverage",
|
|
14
|
+
"test:ui": "vitest --ui"
|
|
11
15
|
},
|
|
12
16
|
"keywords": [
|
|
13
17
|
"directory",
|
|
@@ -28,10 +32,15 @@
|
|
|
28
32
|
},
|
|
29
33
|
"homepage": "https://github.com/CrashBytes/dendro#readme",
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"
|
|
35
|
+
"chalk": "^5.6.2",
|
|
36
|
+
"commander": "^14.0.3"
|
|
33
37
|
},
|
|
34
38
|
"engines": {
|
|
35
|
-
"node": ">=
|
|
39
|
+
"node": ">=20.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
43
|
+
"@vitest/ui": "^4.0.18",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
36
45
|
}
|
|
37
46
|
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { buildTree, renderTree, getTreeStats, getIcon, icons } from '../index.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
describe('getIcon', () => {
|
|
8
|
+
it('should return directory icon for directories', () => {
|
|
9
|
+
expect(getIcon('my-folder', true)).toBe(icons.directory);
|
|
10
|
+
expect(getIcon('src', true)).toBe(icons.directory);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return correct icons for JavaScript files', () => {
|
|
14
|
+
expect(getIcon('app.js', false)).toBe(icons.javascript);
|
|
15
|
+
expect(getIcon('component.jsx', false)).toBe(icons.javascript);
|
|
16
|
+
expect(getIcon('module.mjs', false)).toBe(icons.javascript);
|
|
17
|
+
expect(getIcon('common.cjs', false)).toBe(icons.javascript);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return correct icons for TypeScript files', () => {
|
|
21
|
+
expect(getIcon('app.ts', false)).toBe(icons.typescript);
|
|
22
|
+
expect(getIcon('component.tsx', false)).toBe(icons.typescript);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return correct icons for config files', () => {
|
|
26
|
+
expect(getIcon('config.yaml', false)).toBe(icons.config);
|
|
27
|
+
expect(getIcon('config.yml', false)).toBe(icons.config);
|
|
28
|
+
expect(getIcon('config.toml', false)).toBe(icons.config);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
it('should return correct icons for markdown files', () => {
|
|
33
|
+
expect(getIcon('README.md', false)).toBe(icons.markdown);
|
|
34
|
+
expect(getIcon('CHANGELOG.mdx', false)).toBe(icons.markdown);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return correct icons for data files', () => {
|
|
38
|
+
expect(getIcon('data.json', false)).toBe(icons.json);
|
|
39
|
+
expect(getIcon('package.json', false)).toBe(icons.json);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return correct icons for image files', () => {
|
|
43
|
+
expect(getIcon('photo.png', false)).toBe(icons.image);
|
|
44
|
+
expect(getIcon('logo.svg', false)).toBe(icons.image);
|
|
45
|
+
expect(getIcon('image.jpg', false)).toBe(icons.image);
|
|
46
|
+
expect(getIcon('pic.jpeg', false)).toBe(icons.image);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return correct icons for lock files', () => {
|
|
50
|
+
expect(getIcon('package-lock.json', false)).toBe(icons.lock);
|
|
51
|
+
expect(getIcon('yarn.lock', false)).toBe(icons.lock);
|
|
52
|
+
expect(getIcon('pnpm-lock.yaml', false)).toBe(icons.lock);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return correct icons for git files', () => {
|
|
56
|
+
expect(getIcon('.gitignore', false)).toBe(icons.git);
|
|
57
|
+
expect(getIcon('.gitattributes', false)).toBe(icons.git);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return default icon for unknown file types', () => {
|
|
61
|
+
expect(getIcon('unknown.xyz', false)).toBe(icons.default);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should be case-insensitive for extensions', () => {
|
|
65
|
+
expect(getIcon('FILE.JS', false)).toBe(icons.javascript);
|
|
66
|
+
expect(getIcon('FILE.PNG', false)).toBe(icons.image);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
describe('buildTree', () => {
|
|
72
|
+
let tempDir;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// Create a temporary test directory structure
|
|
76
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dendro-test-'));
|
|
77
|
+
|
|
78
|
+
// Create test structure
|
|
79
|
+
fs.mkdirSync(path.join(tempDir, 'src'));
|
|
80
|
+
fs.mkdirSync(path.join(tempDir, 'test'));
|
|
81
|
+
fs.mkdirSync(path.join(tempDir, '.git'));
|
|
82
|
+
fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test');
|
|
83
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), '{}');
|
|
84
|
+
fs.writeFileSync(path.join(tempDir, 'src', 'index.js'), 'console.log("test")');
|
|
85
|
+
fs.writeFileSync(path.join(tempDir, 'test', 'test.js'), 'test');
|
|
86
|
+
fs.writeFileSync(path.join(tempDir, '.git', 'config'), '');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
// Clean up temp directory
|
|
91
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should build a tree structure', () => {
|
|
95
|
+
const tree = buildTree(tempDir);
|
|
96
|
+
|
|
97
|
+
expect(tree).toBeDefined();
|
|
98
|
+
expect(tree.type).toBe('directory');
|
|
99
|
+
expect(tree.name).toBe(path.basename(tempDir));
|
|
100
|
+
expect(tree.children).toBeDefined();
|
|
101
|
+
expect(Array.isArray(tree.children)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should include files and directories', () => {
|
|
105
|
+
const tree = buildTree(tempDir);
|
|
106
|
+
|
|
107
|
+
expect(tree.children.length).toBeGreaterThan(0);
|
|
108
|
+
|
|
109
|
+
const hasFiles = tree.children.some(child => child.type === 'file');
|
|
110
|
+
const hasDirs = tree.children.some(child => child.type === 'directory');
|
|
111
|
+
|
|
112
|
+
expect(hasFiles).toBe(true);
|
|
113
|
+
expect(hasDirs).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
it('should respect maxDepth option', () => {
|
|
118
|
+
// maxDepth: 1 means we go to depth 0 only (root), children at depth 1 return null
|
|
119
|
+
const tree1 = buildTree(tempDir, { maxDepth: 1 });
|
|
120
|
+
expect(tree1.children.every(child => !child.children || child.children.length === 0)).toBe(true);
|
|
121
|
+
|
|
122
|
+
// maxDepth: 2 means we go to depths 0 and 1, children at depth 2 return null
|
|
123
|
+
const tree2 = buildTree(tempDir, { maxDepth: 2 });
|
|
124
|
+
const srcDir = tree2.children.find(child => child.name === 'src' && child.type === 'directory');
|
|
125
|
+
// srcDir exists at depth 1, but its children (at depth 2) won't be built
|
|
126
|
+
if (srcDir) {
|
|
127
|
+
expect(srcDir.children).toBeDefined();
|
|
128
|
+
// Children array exists but should be empty since they're at maxDepth
|
|
129
|
+
expect(srcDir.children.length).toBe(0);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should hide hidden files by default', () => {
|
|
134
|
+
const tree = buildTree(tempDir, { showHidden: false });
|
|
135
|
+
|
|
136
|
+
const hasHidden = tree.children.some(child => child.name.startsWith('.'));
|
|
137
|
+
expect(hasHidden).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should show hidden files when showHidden is true', () => {
|
|
141
|
+
const tree = buildTree(tempDir, { showHidden: true });
|
|
142
|
+
|
|
143
|
+
const gitDir = tree.children.find(child => child.name === '.git');
|
|
144
|
+
expect(gitDir).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should exclude patterns', () => {
|
|
148
|
+
const tree = buildTree(tempDir, {
|
|
149
|
+
excludePatterns: [/^test$/]
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const testDir = tree.children.find(child => child.name === 'test');
|
|
153
|
+
expect(testDir).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should sort directories before files', () => {
|
|
157
|
+
const tree = buildTree(tempDir);
|
|
158
|
+
|
|
159
|
+
let lastWasDir = true;
|
|
160
|
+
for (const child of tree.children) {
|
|
161
|
+
if (child.type === 'file' && lastWasDir) {
|
|
162
|
+
lastWasDir = false;
|
|
163
|
+
} else if (child.type === 'directory' && !lastWasDir) {
|
|
164
|
+
// Found a directory after a file - sorting is wrong
|
|
165
|
+
expect(true).toBe(false);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
expect(true).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
it('should sort items alphabetically within type', () => {
|
|
173
|
+
const tree = buildTree(tempDir);
|
|
174
|
+
|
|
175
|
+
const dirs = tree.children.filter(c => c.type === 'directory');
|
|
176
|
+
const files = tree.children.filter(c => c.type === 'file');
|
|
177
|
+
|
|
178
|
+
// Check directories are sorted
|
|
179
|
+
for (let i = 0; i < dirs.length - 1; i++) {
|
|
180
|
+
expect(dirs[i].name.localeCompare(dirs[i + 1].name)).toBeLessThanOrEqual(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check files are sorted
|
|
184
|
+
for (let i = 0; i < files.length - 1; i++) {
|
|
185
|
+
expect(files[i].name.localeCompare(files[i + 1].name)).toBeLessThanOrEqual(0);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return null for non-existent paths', () => {
|
|
190
|
+
const tree = buildTree('/non/existent/path');
|
|
191
|
+
expect(tree).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle empty directories', () => {
|
|
195
|
+
const emptyDir = path.join(tempDir, 'empty');
|
|
196
|
+
fs.mkdirSync(emptyDir);
|
|
197
|
+
|
|
198
|
+
const tree = buildTree(emptyDir);
|
|
199
|
+
expect(tree).toBeDefined();
|
|
200
|
+
expect(tree.children).toBeDefined();
|
|
201
|
+
expect(tree.children.length).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
describe('renderTree', () => {
|
|
207
|
+
it('should render a tree structure as text', () => {
|
|
208
|
+
const tree = {
|
|
209
|
+
name: 'root',
|
|
210
|
+
type: 'directory',
|
|
211
|
+
icon: '📁',
|
|
212
|
+
children: [
|
|
213
|
+
{ name: 'file1.js', type: 'file', icon: '📜', path: '/root/file1.js' },
|
|
214
|
+
{ name: 'file2.md', type: 'file', icon: '📝', path: '/root/file2.md' }
|
|
215
|
+
]
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const output = renderTree(tree);
|
|
219
|
+
expect(output).toContain('root');
|
|
220
|
+
expect(output).toContain('file1.js');
|
|
221
|
+
expect(output).toContain('file2.md');
|
|
222
|
+
expect(output).toContain('📁');
|
|
223
|
+
expect(output).toContain('📜');
|
|
224
|
+
expect(output).toContain('📝');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should use tree connectors', () => {
|
|
228
|
+
const tree = {
|
|
229
|
+
name: 'root',
|
|
230
|
+
type: 'directory',
|
|
231
|
+
icon: '📁',
|
|
232
|
+
children: [
|
|
233
|
+
{ name: 'file1.js', type: 'file', icon: '📜', path: '/root/file1.js' }
|
|
234
|
+
]
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const output = renderTree(tree);
|
|
238
|
+
expect(output).toMatch(/[└├]/); // Should contain tree connectors
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should hide icons when showIcons is false', () => {
|
|
242
|
+
const tree = {
|
|
243
|
+
name: 'root',
|
|
244
|
+
type: 'directory',
|
|
245
|
+
icon: '📁',
|
|
246
|
+
children: [
|
|
247
|
+
{ name: 'file.js', type: 'file', icon: '📜', path: '/root/file.js' }
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const output = renderTree(tree, { showIcons: false });
|
|
252
|
+
expect(output).not.toContain('📁');
|
|
253
|
+
expect(output).not.toContain('📜');
|
|
254
|
+
expect(output).toContain('root');
|
|
255
|
+
expect(output).toContain('file.js');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
it('should show paths when showPaths is true', () => {
|
|
260
|
+
const tree = {
|
|
261
|
+
name: 'root',
|
|
262
|
+
type: 'directory',
|
|
263
|
+
icon: '📁',
|
|
264
|
+
path: '/test/root',
|
|
265
|
+
children: [
|
|
266
|
+
{ name: 'file.js', type: 'file', icon: '📜', path: '/test/root/file.js' }
|
|
267
|
+
]
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const output = renderTree(tree, { showPaths: true });
|
|
271
|
+
expect(output).toContain('/test/root');
|
|
272
|
+
expect(output).toContain('/test/root/file.js');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should return empty string for null tree', () => {
|
|
276
|
+
const output = renderTree(null);
|
|
277
|
+
expect(output).toBe('');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle nested structures', () => {
|
|
281
|
+
const tree = {
|
|
282
|
+
name: 'root',
|
|
283
|
+
type: 'directory',
|
|
284
|
+
icon: '📁',
|
|
285
|
+
children: [
|
|
286
|
+
{
|
|
287
|
+
name: 'src',
|
|
288
|
+
type: 'directory',
|
|
289
|
+
icon: '📁',
|
|
290
|
+
children: [
|
|
291
|
+
{ name: 'index.js', type: 'file', icon: '📜', path: '/root/src/index.js' },
|
|
292
|
+
{ name: 'utils.js', type: 'file', icon: '📜', path: '/root/src/utils.js' }
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const output = renderTree(tree);
|
|
299
|
+
expect(output).toContain('root');
|
|
300
|
+
expect(output).toContain('src');
|
|
301
|
+
expect(output).toContain('index.js');
|
|
302
|
+
// With siblings in src, we should get vertical lines
|
|
303
|
+
expect(output).toMatch(/[│├]/);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
describe('getTreeStats', () => {
|
|
309
|
+
it('should count files and directories', () => {
|
|
310
|
+
const tree = {
|
|
311
|
+
name: 'root',
|
|
312
|
+
type: 'directory',
|
|
313
|
+
children: [
|
|
314
|
+
{ name: 'file1.js', type: 'file' },
|
|
315
|
+
{ name: 'file2.js', type: 'file' },
|
|
316
|
+
{
|
|
317
|
+
name: 'subdir',
|
|
318
|
+
type: 'directory',
|
|
319
|
+
children: [
|
|
320
|
+
{ name: 'file3.js', type: 'file' }
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const stats = getTreeStats(tree);
|
|
327
|
+
expect(stats.files).toBe(3);
|
|
328
|
+
expect(stats.directories).toBe(2); // root + subdir
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should return zero counts for null tree', () => {
|
|
332
|
+
const stats = getTreeStats(null);
|
|
333
|
+
expect(stats.files).toBe(0);
|
|
334
|
+
expect(stats.directories).toBe(0);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should handle empty directories', () => {
|
|
338
|
+
const tree = {
|
|
339
|
+
name: 'empty',
|
|
340
|
+
type: 'directory',
|
|
341
|
+
children: []
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const stats = getTreeStats(tree);
|
|
345
|
+
expect(stats.files).toBe(0);
|
|
346
|
+
expect(stats.directories).toBe(1);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should handle single file', () => {
|
|
350
|
+
const tree = {
|
|
351
|
+
name: 'file.js',
|
|
352
|
+
type: 'file'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const stats = getTreeStats(tree);
|
|
356
|
+
expect(stats.files).toBe(1);
|
|
357
|
+
expect(stats.directories).toBe(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should handle deeply nested structures', () => {
|
|
361
|
+
const tree = {
|
|
362
|
+
name: 'root',
|
|
363
|
+
type: 'directory',
|
|
364
|
+
children: [
|
|
365
|
+
{
|
|
366
|
+
name: 'level1',
|
|
367
|
+
type: 'directory',
|
|
368
|
+
children: [
|
|
369
|
+
{
|
|
370
|
+
name: 'level2',
|
|
371
|
+
type: 'directory',
|
|
372
|
+
children: [
|
|
373
|
+
{ name: 'deep.js', type: 'file' }
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
{ name: 'file1.js', type: 'file' }
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
{ name: 'file2.js', type: 'file' }
|
|
380
|
+
]
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const stats = getTreeStats(tree);
|
|
384
|
+
expect(stats.files).toBe(3);
|
|
385
|
+
expect(stats.directories).toBe(3); // root, level1, level2
|
|
386
|
+
});
|
|
387
|
+
});
|
package/test.js
CHANGED
package/vitest.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
reporter: ['text', 'html', 'lcov'],
|
|
10
|
+
exclude: [
|
|
11
|
+
'node_modules/**',
|
|
12
|
+
'test/**',
|
|
13
|
+
'bin/cli.js', // CLI is harder to test, focus on core logic
|
|
14
|
+
'*.config.js',
|
|
15
|
+
'coverage/**',
|
|
16
|
+
'dist/**'
|
|
17
|
+
],
|
|
18
|
+
thresholds: {
|
|
19
|
+
lines: 80,
|
|
20
|
+
functions: 80,
|
|
21
|
+
branches: 70,
|
|
22
|
+
statements: 80
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|