@lazykedar/lazydocs 1.3.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.
@@ -0,0 +1,219 @@
1
+ # Contributing to LazyDocs
2
+
3
+ Thank you for your interest in contributing to LazyDocs! 🎉
4
+
5
+ ## Getting Started
6
+
7
+ ### Prerequisites
8
+ - Node.js >= 18
9
+ - npm or yarn
10
+ - Git
11
+ - A Groq API key (for testing)
12
+
13
+ ### Development Setup
14
+
15
+ 1. **Fork and clone the repository**
16
+ ```bash
17
+ git clone https://github.com/YOUR_USERNAME/lazydocs.git
18
+ cd lazydocs
19
+ ```
20
+
21
+ 2. **Install dependencies**
22
+ ```bash
23
+ npm install
24
+ ```
25
+
26
+ 3. **Build the project**
27
+ ```bash
28
+ npm run build
29
+ ```
30
+
31
+ 4. **Run tests**
32
+ ```bash
33
+ npm test
34
+ ```
35
+
36
+ 5. **Set up your API key**
37
+ ```bash
38
+ export GROQ_API_KEY=your_api_key_here
39
+ ```
40
+
41
+ ## Development Workflow
42
+
43
+ ### Project Structure
44
+ ```
45
+ src/
46
+ ├── cli.ts # CLI entry point
47
+ ├── ai.ts # AI integration with Groq
48
+ ├── a.ts # Code analyzer
49
+ ├── o/ # Output generators (short names for brevity)
50
+ │ ├── r.ts # README generator
51
+ │ ├── p.ts # PR description generator
52
+ │ └── c.ts # Changelog generator
53
+ └── t/ # Templates
54
+ └── r.hbs # README Handlebars template
55
+ ```
56
+
57
+ ### Making Changes
58
+
59
+ 1. **Create a feature branch**
60
+ ```bash
61
+ git checkout -b feature/your-feature-name
62
+ ```
63
+
64
+ 2. **Make your changes**
65
+ - Write clean, readable code
66
+ - Follow existing code style
67
+ - Add tests for new features
68
+ - Update documentation
69
+
70
+ 3. **Test your changes**
71
+ ```bash
72
+ npm test
73
+ npm run build
74
+ ```
75
+
76
+ 4. **Commit your changes**
77
+ ```bash
78
+ git commit -m "feat: add amazing feature"
79
+ ```
80
+
81
+ Use conventional commit messages:
82
+ - `feat:` - New feature
83
+ - `fix:` - Bug fix
84
+ - `docs:` - Documentation changes
85
+ - `test:` - Test changes
86
+ - `refactor:` - Code refactoring
87
+ - `chore:` - Maintenance tasks
88
+
89
+ 5. **Push and create a PR**
90
+ ```bash
91
+ git push origin feature/your-feature-name
92
+ ```
93
+
94
+ ## Code Style
95
+
96
+ - Use TypeScript
97
+ - Follow existing formatting
98
+ - Use meaningful variable names
99
+ - Add comments for complex logic
100
+ - Keep functions small and focused
101
+
102
+ ## Testing
103
+
104
+ - Write tests for new features
105
+ - Ensure all tests pass before submitting PR
106
+ - Aim for good test coverage
107
+
108
+ ```bash
109
+ # Run tests
110
+ npm test
111
+
112
+ # Run tests in watch mode
113
+ npm run test:watch
114
+
115
+ # Run tests with coverage
116
+ npm run test:coverage
117
+ ```
118
+
119
+ ## Adding New Features
120
+
121
+ ### Adding a New AI Model
122
+ 1. Update `AVAILABLE_MODELS` in `src/ai.ts`
123
+ 2. Test with the new model
124
+ 3. Update documentation
125
+
126
+ ### Adding a New File Type
127
+ 1. Update `SUPPORTED_EXTENSIONS` in `src/a.ts`
128
+ 2. Add appropriate Babel plugins if needed
129
+ 3. Test with sample files
130
+ 4. Update documentation
131
+
132
+ ### Adding a New Output Type
133
+ 1. Create a new generator in `src/o/`
134
+ 2. Add template in `src/t/`
135
+ 3. Update CLI in `src/cli.ts`
136
+ 4. Add tests
137
+ 5. Update documentation
138
+
139
+ ## Documentation
140
+
141
+ - Update README.md for user-facing changes
142
+ - Update CHANGELOG.md following Keep a Changelog format
143
+ - Add JSDoc comments for public APIs
144
+ - Include code examples where helpful
145
+
146
+ ## Pull Request Process
147
+
148
+ 1. Ensure your PR description clearly describes the problem and solution
149
+ 2. Include relevant issue numbers if applicable
150
+ 3. Update documentation as needed
151
+ 4. Ensure all tests pass
152
+ 5. Request review from maintainers
153
+
154
+ ### PR Checklist
155
+ - [ ] Code follows project style
156
+ - [ ] Tests added/updated
157
+ - [ ] Documentation updated
158
+ - [ ] CHANGELOG.md updated
159
+ - [ ] All tests passing
160
+ - [ ] No TypeScript errors
161
+ - [ ] Commit messages follow convention
162
+
163
+ ## Reporting Bugs
164
+
165
+ ### Before Submitting
166
+ - Check existing issues
167
+ - Try the latest version
168
+ - Gather relevant information
169
+
170
+ ### Bug Report Template
171
+ ```markdown
172
+ **Describe the bug**
173
+ A clear description of the bug.
174
+
175
+ **To Reproduce**
176
+ Steps to reproduce:
177
+ 1. Run command '...'
178
+ 2. See error
179
+
180
+ **Expected behavior**
181
+ What you expected to happen.
182
+
183
+ **Environment:**
184
+ - OS: [e.g., macOS 14.0]
185
+ - Node version: [e.g., 18.0.0]
186
+ - LazyDocs version: [e.g., 1.0.0]
187
+
188
+ **Additional context**
189
+ Any other relevant information.
190
+ ```
191
+
192
+ ## Feature Requests
193
+
194
+ We welcome feature requests! Please:
195
+ - Check if it's already requested
196
+ - Describe the use case
197
+ - Explain why it would be useful
198
+ - Provide examples if possible
199
+
200
+ ## Questions?
201
+
202
+ - Open a GitHub issue
203
+ - Check existing documentation
204
+ - Review closed issues for similar questions
205
+
206
+ ## Code of Conduct
207
+
208
+ - Be respectful and inclusive
209
+ - Welcome newcomers
210
+ - Focus on constructive feedback
211
+ - Help others learn and grow
212
+
213
+ ## License
214
+
215
+ By contributing, you agree that your contributions will be licensed under the MIT License.
216
+
217
+ ---
218
+
219
+ Thank you for contributing to LazyDocs! 🚀
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kedar Sathe
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
+ # LazyDocs
2
+
3
+ AI-powered documentation generator using Groq. Generate READMEs, PR descriptions, and changelogs in seconds.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@tfkedar/lazydocs)](https://www.npmjs.com/package/@tfkedar/lazydocs)
6
+ [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g @tfkedar/lazydocs
12
+ ```
13
+
14
+ ## Setup
15
+
16
+ Get a free API key from [console.groq.com](https://console.groq.com):
17
+
18
+ ```bash
19
+ lazydocs config set GROQ_API_KEY=your_key_here
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ # Interactive mode (recommended)
26
+ lazydocs generate --interactive
27
+
28
+ # Generate README
29
+ lazydocs generate --type readme
30
+
31
+ # Generate PR description
32
+ lazydocs generate --type pr
33
+
34
+ # Generate changelog
35
+ lazydocs generate --type changelog
36
+ ```
37
+
38
+ ## Features
39
+
40
+ - **Fast** - Powered by Groq's LLM inference.
41
+ - **Smart Merging** - Automatically updates sections within comment anchors (`<!-- lazydocs:start:section -->`), preserving manual edits.
42
+ - **Custom Templates** - Use your own Handlebars layouts to structure README files.
43
+ - **Branch Comparison** - Compare custom branch ranges for precise PR descriptions.
44
+ - **Reliable** - Automatic retry logic with exponential backoff.
45
+ - **Easy** - Interactive CLI with helpful prompts.
46
+
47
+ ## Configuration
48
+
49
+ ```bash
50
+ lazydocs config set GROQ_API_KEY=your_key
51
+ lazydocs config list
52
+ lazydocs config get GROQ_API_KEY
53
+ ```
54
+
55
+ ## Options
56
+
57
+ ```bash
58
+ lazydocs generate [options]
59
+
60
+ -i, --input <dir> Code directory (default: "./src")
61
+ -o, --output <file> Output file (auto-detected)
62
+ -t, --type <type> readme | pr | changelog
63
+ -m, --model <model> AI model to use
64
+ --temperature <temp> Creativity 0-1 (default: 0.7)
65
+ --max-tokens <tokens> Max response length (default: 2048)
66
+ --interactive Interactive mode
67
+ --verbose Show details
68
+ --template <path> Custom Handlebars template path (README only)
69
+ --base <branch> Base branch for PR diff comparison
70
+ --head <branch> Head branch for PR diff comparison
71
+ ```
72
+
73
+ ## Advanced Usage
74
+
75
+ ### 🔄 Smart Merging (Incremental Updates)
76
+
77
+ To prevent `lazydocs` from overwriting custom edits in your README, wrap dynamic sections in comment anchors. On subsequent runs, `lazydocs` will only update content within these tags:
78
+
79
+ ```markdown
80
+ # My Project
81
+
82
+ This is a manually written description that will never be overwritten.
83
+
84
+ ## 📋 Overview
85
+ <!-- lazydocs:start:overview -->
86
+ Overview content automatically managed by lazydocs.
87
+ <!-- lazydocs:end:overview -->
88
+
89
+ Some other manual developer notes...
90
+ ```
91
+
92
+ Supported default anchors:
93
+ - `<!-- lazydocs:start:overview -->` / `<!-- lazydocs:end:overview -->`
94
+ - `<!-- lazydocs:start:stats -->` / `<!-- lazydocs:end:stats -->`
95
+ - `<!-- lazydocs:start:usage -->` / `<!-- lazydocs:end:usage -->`
96
+ - `<!-- lazydocs:start:api -->` / `<!-- lazydocs:end:api -->`
97
+
98
+ ### 🎨 Custom Templates
99
+
100
+ Create a custom Handlebars file (e.g. `my-readme.hbs`) and specify it with the `--template` option:
101
+
102
+ ```bash
103
+ lazydocs generate --type readme --template ./my-readme.hbs
104
+ ```
105
+
106
+ Available variables in the template:
107
+ - `{{projectName}}` - Name of the project.
108
+ - `{{{overview}}}` - Generated overview section.
109
+ - `{{{usage}}}` - Generated usage guidelines.
110
+ - `{{stats}}` - Object containing `fileCount`, `totalLines`, `functions`, `classes`, and `complexity`.
111
+ - `{{apis}}` - List of parsed functions and classes containing `name` and `desc`.
112
+
113
+ ### 🔀 Git PR Branch Comparisons
114
+
115
+ Generate PR descriptions by comparing custom git branches:
116
+
117
+ ```bash
118
+ # Compare the current branch against main
119
+ lazydocs generate --type pr --base main
120
+
121
+ # Compare a feature branch against production
122
+ lazydocs generate --type pr --base production --head feature-login
123
+ ```
124
+
125
+ ## Models
126
+
127
+ ```bash
128
+ # List available models
129
+ lazydocs models
130
+
131
+ # Fetch latest from API
132
+ lazydocs models --refresh
133
+ ```
134
+
135
+ Popular models:
136
+ - `llama-3.3-70b-versatile` (default) - Best quality
137
+ - `llama-3.1-8b-instant` - Fastest
138
+ - `mixtral-8x7b-32768` - Huge context window
139
+
140
+ ## Requirements
141
+
142
+ - Node.js 18+
143
+ - Free Groq API key
144
+
145
+ ## Links
146
+
147
+ - [NPM Package](https://www.npmjs.com/package/@tfkedar/lazydocs)
148
+ - [GitHub](https://github.com/kedar49/lazydocs)
149
+ - [Issues](https://github.com/kedar49/lazydocs/issues)
150
+ - [Groq Console](https://console.groq.com)
151
+
152
+ ## License
153
+
154
+ MIT © [Kedar Sathe](https://github.com/kedar49)
package/dist/a.js ADDED
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.estimateTokenCount = void 0;
37
+ exports.analyzeCode = analyzeCode;
38
+ const parser = __importStar(require("@babel/parser"));
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const child_process_1 = require("child_process");
42
+ const SUPPORTED_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py'];
43
+ // Estimate token count (1 token approximately equals 4 characters)
44
+ const estimateTokenCount = (text) => {
45
+ return Math.ceil(text.length / 4);
46
+ };
47
+ exports.estimateTokenCount = estimateTokenCount;
48
+ function analyzeCode(dir, maxTokens = 6000) {
49
+ let functions = [];
50
+ let classes = [];
51
+ let fullSnippet = '';
52
+ let fileCount = 0;
53
+ let totalLines = 0;
54
+ let totalSize = 0;
55
+ let complexitySum = 0;
56
+ let complexityCount = 0;
57
+ const fileStats = [];
58
+ // Load project config
59
+ let projectConfig = {};
60
+ const configPath = path.join(process.cwd(), '.lazydocs.json');
61
+ if (fs.existsSync(configPath)) {
62
+ try {
63
+ projectConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
64
+ }
65
+ catch (error) {
66
+ console.warn('Failed to parse .lazydocs.json');
67
+ }
68
+ }
69
+ const allowedFileTypes = projectConfig.fileTypes || SUPPORTED_EXTENSIONS;
70
+ const logError = (message) => {
71
+ const logDir = path.join(process.cwd(), 'logs');
72
+ if (!fs.existsSync(logDir))
73
+ fs.mkdirSync(logDir);
74
+ const logFile = path.join(logDir, 'error.log');
75
+ fs.appendFileSync(logFile, `${new Date().toISOString()} - ${message}\n`);
76
+ };
77
+ function analyzeDirectory(currentDir) {
78
+ if (!fs.existsSync(currentDir)) {
79
+ logError(`Directory not found: ${currentDir}`);
80
+ console.warn(`Directory not found: ${currentDir}`);
81
+ return;
82
+ }
83
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ const fullPath = path.join(currentDir, entry.name);
86
+ if (entry.isDirectory()) {
87
+ if (!['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '__pycache__'].includes(entry.name)) {
88
+ analyzeDirectory(fullPath);
89
+ }
90
+ continue;
91
+ }
92
+ const ext = path.extname(entry.name);
93
+ if (!allowedFileTypes.includes(ext)) {
94
+ continue;
95
+ }
96
+ try {
97
+ const code = fs.readFileSync(fullPath, 'utf-8');
98
+ const stats = fs.statSync(fullPath);
99
+ totalSize += stats.size;
100
+ const lines = code.split('\n').length;
101
+ totalLines += lines;
102
+ fileCount++;
103
+ let fileFunctions = 0;
104
+ let fileClasses = 0;
105
+ // Only add full code if we're under token limit for efficient API usage
106
+ const currentTokens = (0, exports.estimateTokenCount)(fullSnippet);
107
+ if (currentTokens < maxTokens) {
108
+ fullSnippet += `\n// File: ${path.relative(dir, fullPath)}\n${code.slice(0, 2000)}\n`;
109
+ }
110
+ // JavaScript/TypeScript analysis
111
+ if (['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'].includes(ext)) {
112
+ const plugins = ['typescript'];
113
+ if (ext === '.tsx' || ext === '.jsx') {
114
+ plugins.push('jsx');
115
+ }
116
+ const ast = parser.parse(code, {
117
+ sourceType: 'module',
118
+ plugins,
119
+ errorRecovery: true,
120
+ });
121
+ let localComplexity = 1;
122
+ ast.program.body.forEach(node => {
123
+ if (node.type === 'FunctionDeclaration') {
124
+ functions.push(node.id?.name || 'anonymous');
125
+ fileFunctions++;
126
+ localComplexity += 1;
127
+ }
128
+ if (node.type === 'ClassDeclaration') {
129
+ classes.push(node.id?.name || 'anonymous');
130
+ fileClasses++;
131
+ localComplexity += 2;
132
+ }
133
+ if (node.type === 'VariableDeclaration') {
134
+ node.declarations.forEach(decl => {
135
+ if (decl.init && (decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression')) {
136
+ if (decl.id.type === 'Identifier') {
137
+ functions.push(decl.id.name);
138
+ fileFunctions++;
139
+ localComplexity += 1;
140
+ }
141
+ }
142
+ });
143
+ }
144
+ if (node.type === 'IfStatement' || node.type === 'ForStatement' || node.type === 'WhileStatement') {
145
+ localComplexity += 1;
146
+ }
147
+ });
148
+ complexitySum += localComplexity;
149
+ complexityCount += 1;
150
+ // Track detailed file statistics
151
+ fileStats.push({
152
+ file: path.relative(dir, fullPath),
153
+ lines,
154
+ size: stats.size,
155
+ functions: fileFunctions,
156
+ classes: fileClasses,
157
+ });
158
+ }
159
+ // Python analysis
160
+ if (ext === '.py') {
161
+ try {
162
+ const pythonScript = `
163
+ import ast
164
+ import json
165
+ import sys
166
+ try:
167
+ with open('${fullPath.replace(/\\/g, '\\\\')}', 'r', encoding='utf-8') as f:
168
+ tree = ast.parse(f.read())
169
+ functions = [node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)]
170
+ classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
171
+ complexity = sum(1 for node in ast.walk(tree) if isinstance(node, (ast.If, ast.For, ast.While))) + len(functions) + 2 * len(classes)
172
+ print(json.dumps({"functions": functions, "classes": classes, "complexity": complexity}))
173
+ except Exception as e:
174
+ print(json.dumps({"functions": [], "classes": [], "complexity": 1}))
175
+ `;
176
+ const tempFile = path.join(process.cwd(), 'temp_analyze.py');
177
+ fs.writeFileSync(tempFile, pythonScript);
178
+ const result = (0, child_process_1.execSync)('python temp_analyze.py', { encoding: 'utf-8', timeout: 5000 });
179
+ fs.unlinkSync(tempFile);
180
+ const { functions: pyFunctions, classes: pyClasses, complexity: pyComplexity } = JSON.parse(result);
181
+ functions.push(...pyFunctions);
182
+ classes.push(...pyClasses);
183
+ fileFunctions = pyFunctions.length;
184
+ fileClasses = pyClasses.length;
185
+ complexitySum += pyComplexity;
186
+ complexityCount += 1;
187
+ // Track Python file stats
188
+ fileStats.push({
189
+ file: path.relative(dir, fullPath),
190
+ lines,
191
+ size: stats.size,
192
+ functions: fileFunctions,
193
+ classes: fileClasses,
194
+ });
195
+ }
196
+ catch (error) {
197
+ logError(`Python analysis failed for ${fullPath}: ${error.message}`);
198
+ console.warn(`Failed to analyze Python file ${fullPath}`);
199
+ }
200
+ }
201
+ }
202
+ catch (error) {
203
+ logError(`Failed to parse ${fullPath}: ${error.message}`);
204
+ console.warn(`Failed to parse ${fullPath}: ${error.message}`);
205
+ }
206
+ }
207
+ }
208
+ analyzeDirectory(dir);
209
+ // Build compact summary for token-efficient API calls
210
+ const compactSummary = buildCompactSummary(fileStats, fileCount, totalLines, totalSize, functions.length, classes.length);
211
+ return {
212
+ functions: [...new Set(functions)],
213
+ classes: [...new Set(classes)],
214
+ snippets: fullSnippet.slice(0, 8000),
215
+ fileCount,
216
+ totalLines,
217
+ totalSize,
218
+ complexity: complexityCount ? complexitySum / complexityCount : 0,
219
+ fileStats,
220
+ compactSummary,
221
+ };
222
+ }
223
+ // Build compact summary for token efficiency
224
+ function buildCompactSummary(fileStats, fileCount, totalLines, totalSize, totalFunctions, totalClasses) {
225
+ const sorted = [...fileStats].sort((a, b) => b.lines - a.lines);
226
+ const top = sorted.slice(0, 15); // Top 15 files by lines
227
+ const lines = [];
228
+ lines.push(`Project Summary:`);
229
+ lines.push(`- Files: ${fileCount}`);
230
+ lines.push(`- Total lines: ${totalLines.toLocaleString()}`);
231
+ lines.push(`- Total size: ${(totalSize / 1024).toFixed(1)} KB`);
232
+ lines.push(`- Functions: ${totalFunctions}`);
233
+ lines.push(`- Classes: ${totalClasses}`);
234
+ lines.push('');
235
+ lines.push('Key files:');
236
+ for (const f of top) {
237
+ const details = [];
238
+ if (f.functions > 0)
239
+ details.push(`${f.functions} fn`);
240
+ if (f.classes > 0)
241
+ details.push(`${f.classes} cls`);
242
+ const detailStr = details.length > 0 ? ` (${details.join(', ')})` : '';
243
+ lines.push(`- ${f.file}: ${f.lines} lines${detailStr}`);
244
+ }
245
+ if (sorted.length > top.length) {
246
+ lines.push(`...and ${sorted.length - top.length} more files`);
247
+ }
248
+ return lines.join('\n');
249
+ }