codesummary 1.1.1 → 1.2.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/src/index.js CHANGED
@@ -1,26 +1,26 @@
1
- #!/usr/bin/env node
2
-
3
- import CLI from './cli.js';
4
- import ErrorHandler from './errorHandler.js';
5
-
6
- /**
7
- * CodeSummary - Main Entry Point
8
- * A cross-platform CLI tool for generating PDF documentation from source code
9
- */
10
-
11
- async function main() {
12
- try {
13
- // Setup global error handlers and validate environment
14
- ErrorHandler.setupGlobalHandlers();
15
- await ErrorHandler.validateEnvironment();
16
-
17
- const cli = new CLI();
18
- const args = process.argv.slice(2);
19
- await cli.run(args);
20
- } catch (error) {
21
- ErrorHandler.handleError(error, 'Main Application');
22
- }
23
- }
24
-
25
- // Execute main function
1
+ #!/usr/bin/env node
2
+
3
+ import CLI from './cli.js';
4
+ import ErrorHandler from './errorHandler.js';
5
+
6
+ /**
7
+ * CodeSummary - Main Entry Point
8
+ * A cross-platform CLI tool for generating PDF documentation from source code
9
+ */
10
+
11
+ async function main() {
12
+ try {
13
+ // Setup global error handlers and validate environment
14
+ ErrorHandler.setupGlobalHandlers();
15
+ await ErrorHandler.validateEnvironment();
16
+
17
+ const cli = new CLI();
18
+ const args = process.argv.slice(2);
19
+ await cli.run(args);
20
+ } catch (error) {
21
+ ErrorHandler.handleError(error, 'Main Application');
22
+ }
23
+ }
24
+
25
+ // Execute main function
26
26
  main();
@@ -0,0 +1,189 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { formatFileSize } from './utils.js';
4
+
5
+ /**
6
+ * LLM Generator for CodeSummary
7
+ * Generates a single Markdown file optimised for direct consumption by LLMs.
8
+ * Applies lossless content optimisations to reduce token count.
9
+ */
10
+ export class LlmGenerator {
11
+ constructor() {
12
+ this.stats = {
13
+ filesProcessed: 0,
14
+ filesSkipped: 0,
15
+ startTime: null,
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Generate an LLM-optimised Markdown file
21
+ * @param {object} filesByExtension - Files grouped by extension
22
+ * @param {Array} selectedExtensions - Extensions selected by user
23
+ * @param {string} outputPath - Output .md file path
24
+ * @param {string} projectName - Project name
25
+ * @returns {Promise<object>} Result with outputPath and stats
26
+ */
27
+ async generateLlmOutput(filesByExtension, selectedExtensions, outputPath, projectName) {
28
+ this.stats.startTime = Date.now();
29
+
30
+ // Collect and sort all selected files
31
+ const allFiles = [];
32
+ for (const ext of selectedExtensions) {
33
+ for (const file of (filesByExtension[ext] || [])) {
34
+ allFiles.push(file);
35
+ }
36
+ }
37
+ allFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
38
+
39
+ const stream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
40
+
41
+ await this.writeLine(stream, this.buildHeader(projectName, allFiles));
42
+ await this.writeLine(stream, this.buildFileTree(allFiles));
43
+
44
+ for (const file of allFiles) {
45
+ const block = await this.buildFileBlock(file);
46
+ await this.writeLine(stream, block);
47
+ }
48
+
49
+ await new Promise((resolve, reject) => {
50
+ stream.end();
51
+ stream.on('finish', resolve);
52
+ stream.on('error', reject);
53
+ });
54
+
55
+ const duration = (Date.now() - this.stats.startTime) / 1000;
56
+
57
+ return {
58
+ outputPath,
59
+ totalFiles: this.stats.filesProcessed,
60
+ skippedFiles: this.stats.filesSkipped,
61
+ duration,
62
+ };
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Private helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ writeLine(stream, text) {
70
+ return new Promise((resolve, reject) => {
71
+ const ok = stream.write(text);
72
+ if (!ok) {
73
+ stream.once('drain', resolve);
74
+ } else {
75
+ resolve();
76
+ }
77
+ });
78
+ }
79
+
80
+ buildHeader(projectName, allFiles) {
81
+ const totalSize = allFiles.reduce((sum, f) => sum + (f.size || 0), 0);
82
+ const date = new Date().toISOString().split('T')[0];
83
+
84
+ return (
85
+ `# ${projectName} — Code Summary\n\n` +
86
+ `**Generated:** ${date} | ` +
87
+ `**Files:** ${allFiles.length} | ` +
88
+ `**Total size:** ${formatFileSize(totalSize)}\n\n` +
89
+ `---\n\n`
90
+ );
91
+ }
92
+
93
+ buildFileTree(allFiles) {
94
+ const lines = allFiles.map(f => ` ${f.relativePath}`).join('\n');
95
+ return `## File Tree\n\n\`\`\`\n${lines}\n\`\`\`\n\n---\n\n`;
96
+ }
97
+
98
+ async buildFileBlock(file) {
99
+ try {
100
+ const raw = await fs.readFile(file.absolutePath, 'utf8');
101
+ const ext = path.extname(file.relativePath).toLowerCase();
102
+ const optimized = this.optimizeContent(raw, ext);
103
+ const lang = this.fenceLang(ext);
104
+ const fence = this.fence(optimized);
105
+
106
+ this.stats.filesProcessed++;
107
+ return `## ${file.relativePath}\n\n${fence}${lang}\n${optimized}\n${fence}\n\n`;
108
+ } catch (error) {
109
+ this.stats.filesSkipped++;
110
+ return `## ${file.relativePath}\n\n> Error: ${error.message}\n\n`;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Apply lossless content optimisations
116
+ * @param {string} content - Raw file content
117
+ * @param {string} ext - File extension (e.g. ".js")
118
+ * @returns {string} Optimised content
119
+ */
120
+ optimizeContent(content, ext) {
121
+ // 1. Normalize line endings
122
+ let result = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
123
+
124
+ // 2. Trim trailing whitespace on every line (safe for all languages)
125
+ result = result.split('\n').map(line => line.trimEnd()).join('\n');
126
+
127
+ // 3. Compact JSON (parse → re-serialise without indentation)
128
+ if (ext === '.json') {
129
+ try {
130
+ result = JSON.stringify(JSON.parse(result));
131
+ return result; // Already single-line, no further blank-line processing needed
132
+ } catch {
133
+ // Invalid / JSON5 / JSONC — fall through to generic processing
134
+ }
135
+ }
136
+
137
+ // 4. Remove leading and trailing blank lines from the file
138
+ result = result.trim();
139
+
140
+ // 5. Collapse consecutive blank lines
141
+ // Markdown: max 2 consecutive blank lines (paragraph semantics preserved)
142
+ // Everything else: max 1 consecutive blank line
143
+ if (ext === '.md' || ext === '.mdx') {
144
+ result = result.replace(/\n{4,}/g, '\n\n\n');
145
+ } else {
146
+ result = result.replace(/\n{3,}/g, '\n\n');
147
+ }
148
+
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Map a file extension to a Markdown fence language identifier
154
+ */
155
+ fenceLang(ext) {
156
+ const map = {
157
+ '.js': 'js', '.jsx': 'jsx', '.ts': 'ts', '.tsx': 'tsx',
158
+ '.py': 'python', '.java': 'java', '.cs': 'csharp', '.cpp': 'cpp',
159
+ '.c': 'c', '.h': 'c', '.html': 'html', '.css': 'css',
160
+ '.scss': 'scss', '.sass': 'sass', '.json': 'json', '.yaml': 'yaml',
161
+ '.yml': 'yaml', '.xml': 'xml', '.md': 'markdown', '.mdx': 'mdx',
162
+ '.txt': '', '.sh': 'bash', '.bat': 'bat', '.ps1': 'powershell',
163
+ '.sql': 'sql', '.graphql': 'graphql', '.gql': 'graphql', '.proto': 'protobuf',
164
+ '.toml': 'toml', '.ini': 'ini', '.properties': 'properties',
165
+ '.tf': 'hcl', '.tfvars': 'hcl', '.prisma': 'prisma',
166
+ '.dart': 'dart', '.lua': 'lua', '.r': 'r',
167
+ '.ex': 'elixir', '.exs': 'elixir', '.pl': 'perl',
168
+ '.rb': 'ruby', '.go': 'go', '.rs': 'rust',
169
+ '.swift': 'swift', '.kt': 'kotlin', '.php': 'php',
170
+ '.scala': 'scala', '.vue': 'vue', '.svelte': 'svelte',
171
+ '.astro': 'astro', '.env': 'bash', '.cfg': 'ini',
172
+ '.conf': 'nginx', '.cmake': 'cmake', '.ino': 'cpp',
173
+ '.mk': 'makefile', '.csv': 'csv', '.tsv': 'tsv',
174
+ '.j2': 'jinja', '.lua': 'lua',
175
+ };
176
+ return map[ext] ?? '';
177
+ }
178
+
179
+ /**
180
+ * Return a fence string with enough backticks to safely wrap the content.
181
+ * Ensures the opening fence cannot appear inside the content block.
182
+ */
183
+ fence(content) {
184
+ const max = (content.match(/`+/g) ?? []).reduce((m, s) => Math.max(m, s.length), 2);
185
+ return '`'.repeat(Math.max(3, max + 1));
186
+ }
187
+ }
188
+
189
+ export default LlmGenerator;