@amirdaraee/namewise 0.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.
Files changed (105) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +82 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +61 -0
  3. package/.github/workflows/auto-release.yml +78 -0
  4. package/.github/workflows/ci.yml +78 -0
  5. package/.github/workflows/publish.yml +43 -0
  6. package/.github/workflows/test.yml +37 -0
  7. package/CHANGELOG.md +128 -0
  8. package/LICENSE +21 -0
  9. package/README.md +251 -0
  10. package/dist/cli/commands.d.ts +3 -0
  11. package/dist/cli/commands.d.ts.map +1 -0
  12. package/dist/cli/commands.js +19 -0
  13. package/dist/cli/commands.js.map +1 -0
  14. package/dist/cli/rename.d.ts +2 -0
  15. package/dist/cli/rename.d.ts.map +1 -0
  16. package/dist/cli/rename.js +136 -0
  17. package/dist/cli/rename.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +13 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/parsers/excel-parser.d.ts +6 -0
  23. package/dist/parsers/excel-parser.d.ts.map +1 -0
  24. package/dist/parsers/excel-parser.js +42 -0
  25. package/dist/parsers/excel-parser.js.map +1 -0
  26. package/dist/parsers/factory.d.ts +7 -0
  27. package/dist/parsers/factory.d.ts.map +1 -0
  28. package/dist/parsers/factory.js +29 -0
  29. package/dist/parsers/factory.js.map +1 -0
  30. package/dist/parsers/pdf-parser.d.ts +7 -0
  31. package/dist/parsers/pdf-parser.d.ts.map +1 -0
  32. package/dist/parsers/pdf-parser.js +67 -0
  33. package/dist/parsers/pdf-parser.js.map +1 -0
  34. package/dist/parsers/text-parser.d.ts +6 -0
  35. package/dist/parsers/text-parser.d.ts.map +1 -0
  36. package/dist/parsers/text-parser.js +39 -0
  37. package/dist/parsers/text-parser.js.map +1 -0
  38. package/dist/parsers/word-parser.d.ts +6 -0
  39. package/dist/parsers/word-parser.d.ts.map +1 -0
  40. package/dist/parsers/word-parser.js +44 -0
  41. package/dist/parsers/word-parser.js.map +1 -0
  42. package/dist/services/ai-factory.d.ts +5 -0
  43. package/dist/services/ai-factory.d.ts.map +1 -0
  44. package/dist/services/ai-factory.js +15 -0
  45. package/dist/services/ai-factory.js.map +1 -0
  46. package/dist/services/claude-service.d.ts +9 -0
  47. package/dist/services/claude-service.d.ts.map +1 -0
  48. package/dist/services/claude-service.js +113 -0
  49. package/dist/services/claude-service.js.map +1 -0
  50. package/dist/services/file-renamer.d.ts +12 -0
  51. package/dist/services/file-renamer.d.ts.map +1 -0
  52. package/dist/services/file-renamer.js +99 -0
  53. package/dist/services/file-renamer.js.map +1 -0
  54. package/dist/services/openai-service.d.ts +9 -0
  55. package/dist/services/openai-service.d.ts.map +1 -0
  56. package/dist/services/openai-service.js +112 -0
  57. package/dist/services/openai-service.js.map +1 -0
  58. package/dist/types/index.d.ts +61 -0
  59. package/dist/types/index.d.ts.map +1 -0
  60. package/dist/types/index.js +2 -0
  61. package/dist/types/index.js.map +1 -0
  62. package/dist/utils/file-templates.d.ts +18 -0
  63. package/dist/utils/file-templates.d.ts.map +1 -0
  64. package/dist/utils/file-templates.js +232 -0
  65. package/dist/utils/file-templates.js.map +1 -0
  66. package/dist/utils/naming-conventions.d.ts +4 -0
  67. package/dist/utils/naming-conventions.d.ts.map +1 -0
  68. package/dist/utils/naming-conventions.js +55 -0
  69. package/dist/utils/naming-conventions.js.map +1 -0
  70. package/package.json +75 -0
  71. package/src/cli/commands.ts +20 -0
  72. package/src/cli/rename.ts +157 -0
  73. package/src/index.ts +17 -0
  74. package/src/parsers/excel-parser.ts +49 -0
  75. package/src/parsers/factory.ts +34 -0
  76. package/src/parsers/pdf-parser.ts +78 -0
  77. package/src/parsers/text-parser.ts +43 -0
  78. package/src/parsers/word-parser.ts +50 -0
  79. package/src/services/ai-factory.ts +16 -0
  80. package/src/services/claude-service.ts +114 -0
  81. package/src/services/file-renamer.ts +123 -0
  82. package/src/services/openai-service.ts +113 -0
  83. package/src/types/index.ts +71 -0
  84. package/src/types/pdf-extraction.d.ts +7 -0
  85. package/src/utils/file-templates.ts +275 -0
  86. package/src/utils/naming-conventions.ts +67 -0
  87. package/tests/data/empty-file.txt +0 -0
  88. package/tests/data/sample-markdown.md +9 -0
  89. package/tests/data/sample-pdf.pdf +0 -0
  90. package/tests/data/sample-text.txt +25 -0
  91. package/tests/integration/end-to-end.test.ts +209 -0
  92. package/tests/integration/workflow.test.ts +336 -0
  93. package/tests/mocks/mock-ai-service.ts +58 -0
  94. package/tests/unit/cli/commands.test.ts +163 -0
  95. package/tests/unit/parsers/factory.test.ts +100 -0
  96. package/tests/unit/parsers/pdf-parser.test.ts +63 -0
  97. package/tests/unit/parsers/text-parser.test.ts +85 -0
  98. package/tests/unit/services/ai-factory.test.ts +37 -0
  99. package/tests/unit/services/claude-service.test.ts +188 -0
  100. package/tests/unit/services/file-renamer.test.ts +299 -0
  101. package/tests/unit/services/openai-service.test.ts +196 -0
  102. package/tests/unit/utils/file-templates.test.ts +199 -0
  103. package/tests/unit/utils/naming-conventions.test.ts +88 -0
  104. package/tsconfig.json +20 -0
  105. package/vitest.config.ts +30 -0
@@ -0,0 +1,275 @@
1
+ import { NamingConvention, FileInfo } from '../types/index.js';
2
+ import { applyNamingConvention } from './naming-conventions.js';
3
+
4
+ export type FileCategory = 'document' | 'movie' | 'music' | 'series' | 'photo' | 'book' | 'general' | 'auto';
5
+
6
+ export interface TemplateOptions {
7
+ personalName?: string;
8
+ dateFormat?: 'YYYY-MM-DD' | 'YYYY' | 'YYYYMMDD' | 'none';
9
+ category?: FileCategory;
10
+ }
11
+
12
+ export interface FileTemplate {
13
+ category: FileCategory;
14
+ pattern: string; // e.g., "{content}-{personalName}-{date}"
15
+ description: string;
16
+ examples: string[];
17
+ }
18
+
19
+ export const FILE_TEMPLATES: Record<Exclude<FileCategory, 'auto'>, FileTemplate> = {
20
+ document: {
21
+ category: 'document',
22
+ pattern: '{content}-{personalName}-{date}',
23
+ description: 'Personal documents with name and date',
24
+ examples: [
25
+ 'driving-license-amirhossein-20250213.pdf',
26
+ 'dennemeyer-working-contract-amirhossein-20240314.pdf',
27
+ 'university-diploma-sarah-20220615.pdf'
28
+ ]
29
+ },
30
+ movie: {
31
+ category: 'movie',
32
+ pattern: '{content}-{year}',
33
+ description: 'Movies with release year',
34
+ examples: [
35
+ 'the-dark-knight-2008.mkv',
36
+ 'inception-2010.mp4',
37
+ 'pulp-fiction-1994.avi'
38
+ ]
39
+ },
40
+ music: {
41
+ category: 'music',
42
+ pattern: '{artist}-{content}',
43
+ description: 'Music files with artist name',
44
+ examples: [
45
+ 'the-beatles-hey-jude.mp3',
46
+ 'queen-bohemian-rhapsody.flac',
47
+ 'pink-floyd-wish-you-were-here.wav'
48
+ ]
49
+ },
50
+ series: {
51
+ category: 'series',
52
+ pattern: '{content}-s{season}e{episode}',
53
+ description: 'TV series with season and episode',
54
+ examples: [
55
+ 'breaking-bad-s01e01.mkv',
56
+ 'game-of-thrones-s04e09.mp4',
57
+ 'the-office-s02e01.avi'
58
+ ]
59
+ },
60
+ photo: {
61
+ category: 'photo',
62
+ pattern: '{content}-{personalName}-{date}',
63
+ description: 'Photos with personal name and date',
64
+ examples: [
65
+ 'vacation-paris-john-20240715.jpg',
66
+ 'wedding-ceremony-maria-20231009.png',
67
+ 'birthday-party-alex-20240320.heic'
68
+ ]
69
+ },
70
+ book: {
71
+ category: 'book',
72
+ pattern: '{author}-{content}',
73
+ description: 'Books with author name',
74
+ examples: [
75
+ 'george-orwell-1984.pdf',
76
+ 'j-k-rowling-harry-potter-philosophers-stone.epub',
77
+ 'stephen-king-the-shining.mobi'
78
+ ]
79
+ },
80
+ general: {
81
+ category: 'general',
82
+ pattern: '{content}',
83
+ description: 'General files without special formatting',
84
+ examples: [
85
+ 'meeting-notes-q4-2024.txt',
86
+ 'project-requirements.docx',
87
+ 'financial-report.xlsx'
88
+ ]
89
+ }
90
+ };
91
+
92
+ export function categorizeFile(filePath: string, content?: string, fileInfo?: FileInfo): FileCategory {
93
+ const extension = getFileExtension(filePath).toLowerCase();
94
+ const fileName = getFileName(filePath).toLowerCase();
95
+ const contentLower = content?.toLowerCase() || '';
96
+
97
+ // Use metadata for enhanced categorization
98
+ let metadataHints: string[] = [];
99
+ if (fileInfo?.documentMetadata) {
100
+ const meta = fileInfo.documentMetadata;
101
+ if (meta.title) metadataHints.push(meta.title.toLowerCase());
102
+ if (meta.author) metadataHints.push(meta.author.toLowerCase());
103
+ if (meta.creator) metadataHints.push(meta.creator.toLowerCase());
104
+ if (meta.subject) metadataHints.push(meta.subject.toLowerCase());
105
+ if (meta.keywords) metadataHints.push(...meta.keywords.map(k => k.toLowerCase()));
106
+ }
107
+
108
+ // Use folder context for better categorization
109
+ let folderHints: string[] = [];
110
+ if (fileInfo?.folderPath) {
111
+ folderHints = fileInfo.folderPath.map(f => f.toLowerCase());
112
+ }
113
+ if (fileInfo?.parentFolder) {
114
+ folderHints.push(fileInfo.parentFolder.toLowerCase());
115
+ }
116
+
117
+ const allHints = [...metadataHints, ...folderHints, contentLower, fileName].join(' ');
118
+
119
+ // Document types
120
+ const documentExtensions = ['.pdf', '.docx', '.doc', '.txt', '.rtf'];
121
+ const documentKeywords = ['contract', 'agreement', 'license', 'certificate', 'diploma', 'invoice', 'receipt', 'report', 'application', 'form', 'resume', 'cv', 'letter'];
122
+
123
+ // Media types
124
+ const movieExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'];
125
+ const musicExtensions = ['.mp3', '.flac', '.wav', '.aac', '.ogg', '.m4a'];
126
+ const photoExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.heic', '.webp'];
127
+ const bookExtensions = ['.epub', '.mobi', '.azw', '.azw3'];
128
+
129
+ // Enhanced series detection
130
+ const seriesKeywords = ['s01', 's02', 's03', 's04', 's05', 'season', 'episode', 'e01', 'e02', 'e03', 'series', 'show', 'tv'];
131
+
132
+ // Enhanced movie keywords
133
+ const movieKeywords = ['movie', 'film', 'cinema', '1080p', '720p', '4k', 'bluray', 'dvdrip', 'webrip'];
134
+
135
+ // Book keywords
136
+ const bookKeywords = ['chapter', 'author', 'book', 'novel', 'ebook', 'isbn', 'publisher', 'edition'];
137
+
138
+ // Music keywords
139
+ const musicKeywords = ['album', 'track', 'artist', 'band', 'singer', 'song', 'music'];
140
+
141
+ // Photo keywords
142
+ const photoKeywords = ['photo', 'image', 'picture', 'vacation', 'wedding', 'birthday', 'selfie', 'portrait'];
143
+
144
+ // Folder-based hints
145
+ const folderMovieHints = ['movies', 'films', 'cinema', 'video'];
146
+ const folderSeriesHints = ['series', 'shows', 'tv', 'television'];
147
+ const folderMusicHints = ['music', 'audio', 'songs', 'albums'];
148
+ const folderPhotoHints = ['photos', 'images', 'pictures', 'gallery'];
149
+ const folderBookHints = ['books', 'ebooks', 'library', 'reading'];
150
+ const folderDocumentHints = ['documents', 'docs', 'papers', 'files'];
151
+
152
+ // Check folder context first for strong hints
153
+ if (folderHints.some(hint => folderSeriesHints.includes(hint))) return 'series';
154
+ if (folderHints.some(hint => folderMovieHints.includes(hint))) return 'movie';
155
+ if (folderHints.some(hint => folderMusicHints.includes(hint))) return 'music';
156
+ if (folderHints.some(hint => folderPhotoHints.includes(hint))) return 'photo';
157
+ if (folderHints.some(hint => folderBookHints.includes(hint))) return 'book';
158
+ if (folderHints.some(hint => folderDocumentHints.includes(hint))) return 'document';
159
+
160
+ // Check for series first (before movies)
161
+ if (movieExtensions.includes(extension) && (
162
+ seriesKeywords.some(keyword => allHints.includes(keyword))
163
+ )) {
164
+ return 'series';
165
+ }
166
+
167
+ // Check by extension with enhanced keyword matching
168
+ if (documentExtensions.includes(extension)) {
169
+ // Check if it's a book
170
+ if (bookExtensions.includes(extension) || bookKeywords.some(keyword => allHints.includes(keyword))) {
171
+ return 'book';
172
+ }
173
+ // Check if it's likely a personal document
174
+ if (documentKeywords.some(keyword => allHints.includes(keyword))) {
175
+ return 'document';
176
+ }
177
+ return 'document'; // Default for document extensions
178
+ }
179
+
180
+ // Enhanced media type detection
181
+ if (movieExtensions.includes(extension)) {
182
+ if (movieKeywords.some(keyword => allHints.includes(keyword))) {
183
+ return 'movie';
184
+ }
185
+ return 'movie'; // Default for movie extensions
186
+ }
187
+
188
+ if (musicExtensions.includes(extension)) {
189
+ if (musicKeywords.some(keyword => allHints.includes(keyword))) {
190
+ return 'music';
191
+ }
192
+ return 'music';
193
+ }
194
+
195
+ if (photoExtensions.includes(extension)) {
196
+ if (photoKeywords.some(keyword => allHints.includes(keyword))) {
197
+ return 'photo';
198
+ }
199
+ return 'photo';
200
+ }
201
+
202
+ if (bookExtensions.includes(extension)) return 'book';
203
+
204
+ return 'general';
205
+ }
206
+
207
+ export function applyTemplate(
208
+ aiGeneratedName: string,
209
+ category: FileCategory,
210
+ templateOptions: TemplateOptions,
211
+ namingConvention: NamingConvention
212
+ ): string {
213
+ if (category === 'auto') {
214
+ throw new Error('Cannot apply template for "auto" category. Category should be resolved before calling applyTemplate.');
215
+ }
216
+ const template = FILE_TEMPLATES[category as Exclude<FileCategory, 'auto'>];
217
+ let result = template.pattern;
218
+
219
+ // Replace template variables
220
+ result = result.replace('{content}', aiGeneratedName);
221
+
222
+ if (templateOptions.personalName) {
223
+ result = result.replace('{personalName}', templateOptions.personalName);
224
+ }
225
+
226
+ if (templateOptions.dateFormat && templateOptions.dateFormat !== 'none') {
227
+ const date = formatDate(new Date(), templateOptions.dateFormat);
228
+ result = result.replace('{date}', date);
229
+ }
230
+
231
+ // Clean up any remaining unreplaced variables
232
+ result = result.replace(/\{[^}]+\}/g, '');
233
+
234
+ // Clean up multiple hyphens or other separators
235
+ result = result.replace(/-+/g, '-').replace(/^-|-$/g, '');
236
+
237
+ // Apply naming convention
238
+ return applyNamingConvention(result, namingConvention);
239
+ }
240
+
241
+ function formatDate(date: Date, format: 'YYYY-MM-DD' | 'YYYY' | 'YYYYMMDD'): string {
242
+ const year = date.getFullYear();
243
+ const month = String(date.getMonth() + 1).padStart(2, '0');
244
+ const day = String(date.getDate()).padStart(2, '0');
245
+
246
+ switch (format) {
247
+ case 'YYYY-MM-DD':
248
+ return `${year}-${month}-${day}`;
249
+ case 'YYYY':
250
+ return `${year}`;
251
+ case 'YYYYMMDD':
252
+ return `${year}${month}${day}`;
253
+ default:
254
+ return `${year}${month}${day}`;
255
+ }
256
+ }
257
+
258
+ function getFileExtension(filePath: string): string {
259
+ const parts = filePath.split('.');
260
+ return parts.length > 1 ? '.' + parts[parts.length - 1] : '';
261
+ }
262
+
263
+ function getFileName(filePath: string): string {
264
+ const pathParts = filePath.split(/[/\\]/);
265
+ const fileName = pathParts[pathParts.length - 1];
266
+ return fileName.replace(/\.[^.]*$/, ''); // Remove extension
267
+ }
268
+
269
+ export function getTemplateInstructions(category: FileCategory): string {
270
+ if (category === 'auto') {
271
+ return 'Generate appropriate filename based on detected file type and content.';
272
+ }
273
+ const template = FILE_TEMPLATES[category as Exclude<FileCategory, 'auto'>];
274
+ return `Generate filename for ${category} type files. ${template.description}. Examples: ${template.examples.join(', ')}`;
275
+ }
@@ -0,0 +1,67 @@
1
+ export type NamingConvention = 'kebab-case' | 'snake_case' | 'camelCase' | 'PascalCase' | 'lowercase' | 'UPPERCASE';
2
+
3
+ export function applyNamingConvention(text: string, convention: NamingConvention): string {
4
+ // First, normalize the text by removing special characters and extra spaces
5
+ const normalized = text
6
+ .replace(/[^\w\s-]/g, '') // Remove special characters except hyphens
7
+ .replace(/\s+/g, ' ') // Normalize spaces
8
+ .trim();
9
+
10
+ switch (convention) {
11
+ case 'kebab-case':
12
+ return normalized
13
+ .toLowerCase()
14
+ .replace(/\s+/g, '-')
15
+ .replace(/[_]/g, '-');
16
+
17
+ case 'snake_case':
18
+ return normalized
19
+ .toLowerCase()
20
+ .replace(/\s+/g, '_')
21
+ .replace(/[-]/g, '_');
22
+
23
+ case 'camelCase':
24
+ return normalized
25
+ .split(/[\s\-_]+/)
26
+ .map((word, index) =>
27
+ index === 0
28
+ ? word.toLowerCase()
29
+ : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
30
+ )
31
+ .join('');
32
+
33
+ case 'PascalCase':
34
+ return normalized
35
+ .split(/[\s\-_]+/)
36
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
37
+ .join('');
38
+
39
+ case 'lowercase':
40
+ return normalized
41
+ .toLowerCase()
42
+ .replace(/\s+/g, '')
43
+ .replace(/[-_]/g, '');
44
+
45
+ case 'UPPERCASE':
46
+ return normalized
47
+ .toUpperCase()
48
+ .replace(/\s+/g, '')
49
+ .replace(/[-_]/g, '');
50
+
51
+ default:
52
+ return normalized.replace(/\s+/g, '-').toLowerCase(); // Default to kebab-case
53
+ }
54
+ }
55
+
56
+ export function getNamingInstructions(convention: NamingConvention): string {
57
+ const instructions = {
58
+ 'kebab-case': 'Use lowercase with hyphens between words (e.g., "meeting-notes-2024")',
59
+ 'snake_case': 'Use lowercase with underscores between words (e.g., "meeting_notes_2024")',
60
+ 'camelCase': 'Use camelCase format starting with lowercase (e.g., "meetingNotes2024")',
61
+ 'PascalCase': 'Use PascalCase format starting with uppercase (e.g., "MeetingNotes2024")',
62
+ 'lowercase': 'Use single lowercase word with no separators (e.g., "meetingnotes2024")',
63
+ 'UPPERCASE': 'Use single uppercase word with no separators (e.g., "MEETINGNOTES2024")'
64
+ };
65
+
66
+ return instructions[convention];
67
+ }
File without changes
@@ -0,0 +1,9 @@
1
+ # Meeting Notes
2
+
3
+ Date: 2024-03-15
4
+ Attendees: John, Sarah, Mike
5
+
6
+ ## Action Items
7
+ - Review budget proposal
8
+ - Update project timeline
9
+ - Schedule follow-up meeting
Binary file
@@ -0,0 +1,25 @@
1
+ Project Requirements Document
2
+ =========================
3
+
4
+ Overview
5
+ --------
6
+ This document outlines the requirements for the new customer management system.
7
+
8
+ Features
9
+ --------
10
+ 1. User Authentication
11
+ 2. Customer Database
12
+ 3. Reporting Dashboard
13
+ 4. Data Export Functionality
14
+
15
+ Technical Stack
16
+ --------------
17
+ - Frontend: React.js
18
+ - Backend: Node.js
19
+ - Database: PostgreSQL
20
+
21
+ Timeline
22
+ --------
23
+ Development: 3 months
24
+ Testing: 1 month
25
+ Deployment: 2 weeks
@@ -0,0 +1,209 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ // Mock fs operations for integration tests
10
+ vi.mock('fs', async () => {
11
+ const actual = await vi.importActual('fs');
12
+ return {
13
+ ...actual,
14
+ promises: {
15
+ ...actual.promises,
16
+ rename: vi.fn(),
17
+ access: vi.fn()
18
+ }
19
+ };
20
+ });
21
+
22
+ describe('End-to-End Integration Tests', () => {
23
+ const testDataDir = path.join(process.cwd(), 'tests/data');
24
+ const cliPath = path.join(process.cwd(), 'dist/index.js');
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ describe('CLI Integration', () => {
35
+ it('should show help message', async () => {
36
+ const { stdout } = await execAsync(`node ${cliPath} --help`);
37
+
38
+ expect(stdout).toContain('AI-powered tool to intelligently rename files based on their content');
39
+ expect(stdout).toContain('rename [options] <directory>');
40
+ expect(stdout).toContain('Commands:');
41
+ });
42
+
43
+ it('should show rename command help', async () => {
44
+ const { stdout } = await execAsync(`node ${cliPath} rename --help`);
45
+
46
+ expect(stdout).toContain('Rename files in a directory based on their content');
47
+ expect(stdout).toContain('Arguments:');
48
+ expect(stdout).toContain('directory');
49
+ expect(stdout).toContain('Options:');
50
+ expect(stdout).toContain('-p, --provider');
51
+ expect(stdout).toContain('-k, --api-key');
52
+ expect(stdout).toContain('-c, --case');
53
+ expect(stdout).toContain('-t, --template');
54
+ expect(stdout).toContain('-n, --name');
55
+ expect(stdout).toContain('-d, --date');
56
+ expect(stdout).toContain('--dry-run');
57
+ expect(stdout).toContain('--max-size');
58
+ });
59
+
60
+ it('should show naming convention options in help', async () => {
61
+ const { stdout } = await execAsync(`node ${cliPath} rename --help`);
62
+
63
+ expect(stdout).toContain('kebab-case');
64
+ expect(stdout).toContain('snake_case');
65
+ expect(stdout).toContain('camelCase');
66
+ expect(stdout).toContain('PascalCase');
67
+ expect(stdout).toContain('lowercase');
68
+ expect(stdout).toContain('UPPERCASE');
69
+ });
70
+
71
+ it('should show template category options in help', async () => {
72
+ const { stdout } = await execAsync(`node ${cliPath} rename --help`);
73
+
74
+ expect(stdout).toContain('document');
75
+ expect(stdout).toContain('movie');
76
+ expect(stdout).toContain('music');
77
+ expect(stdout).toContain('series');
78
+ expect(stdout).toContain('photo');
79
+ expect(stdout).toContain('book');
80
+ expect(stdout).toContain('general');
81
+ });
82
+
83
+ it('should show date format options in help', async () => {
84
+ const { stdout } = await execAsync(`node ${cliPath} rename --help`);
85
+
86
+ expect(stdout).toContain('YYYY-MM-DD');
87
+ expect(stdout).toContain('YYYY');
88
+ expect(stdout).toContain('YYYYMMDD');
89
+ expect(stdout).toContain('none');
90
+ });
91
+
92
+ it('should show version', async () => {
93
+ const { stdout } = await execAsync(`node ${cliPath} --version`);
94
+
95
+ expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
96
+ });
97
+
98
+ it('should require directory argument', async () => {
99
+ try {
100
+ await execAsync(`node ${cliPath} rename`);
101
+ expect.fail('Should have thrown an error');
102
+ } catch (error: any) {
103
+ expect(error.stderr || error.stdout).toContain('error: missing required argument');
104
+ }
105
+ });
106
+
107
+ it('should handle non-existent directory', async () => {
108
+ try {
109
+ await execAsync(`node ${cliPath} rename /non/existent/directory --dry-run`, {
110
+ input: 'test-key\n'
111
+ });
112
+ expect.fail('Should have thrown an error');
113
+ } catch (error: any) {
114
+ expect(error.stderr || error.stdout).toContain('Error:');
115
+ }
116
+ });
117
+ });
118
+
119
+ describe('Full Workflow Integration', () => {
120
+ it('should process files with mock AI service (dry run)', async () => {
121
+ // This test would need actual AI service mocking at the CLI level
122
+ // For now, we'll test the structure
123
+
124
+ const testDir = path.join(testDataDir);
125
+
126
+ // Mock the AI service response by setting environment variables or config
127
+ process.env.MOCK_AI_RESPONSE = 'test-document-name';
128
+
129
+ try {
130
+ // This would fail without real API key, but tests the flow
131
+ const command = `echo "test-key" | node ${cliPath} rename ${testDir} --dry-run --provider claude`;
132
+
133
+ // For a real test, we'd need to mock the AI service at a higher level
134
+ // This is a placeholder for the integration test structure
135
+ expect(true).toBe(true); // Placeholder assertion
136
+ } catch (error) {
137
+ // Expected in test environment without real API key
138
+ expect(true).toBe(true);
139
+ } finally {
140
+ delete process.env.MOCK_AI_RESPONSE;
141
+ }
142
+ });
143
+ });
144
+
145
+ describe('File Processing Integration', () => {
146
+ it('should detect supported files correctly', async () => {
147
+ // Create a temporary test directory structure
148
+ const tempDir = path.join(process.cwd(), 'temp-test');
149
+
150
+ try {
151
+ await fs.mkdir(tempDir, { recursive: true });
152
+ await fs.writeFile(path.join(tempDir, 'test.txt'), 'Test content');
153
+ await fs.writeFile(path.join(tempDir, 'test.md'), '# Test markdown');
154
+ await fs.writeFile(path.join(tempDir, 'unsupported.xyz'), 'Unsupported file');
155
+
156
+ // The actual CLI would process only supported files
157
+ // This test validates the file detection logic
158
+
159
+ const files = await fs.readdir(tempDir);
160
+ const supportedExtensions = ['.txt', '.md', '.pdf', '.docx', '.xlsx'];
161
+ const supportedFiles = files.filter(file =>
162
+ supportedExtensions.some(ext => file.endsWith(ext))
163
+ );
164
+
165
+ expect(supportedFiles).toHaveLength(2);
166
+ expect(supportedFiles).toContain('test.txt');
167
+ expect(supportedFiles).toContain('test.md');
168
+ expect(supportedFiles).not.toContain('unsupported.xyz');
169
+
170
+ } finally {
171
+ // Clean up
172
+ try {
173
+ await fs.rm(tempDir, { recursive: true, force: true });
174
+ } catch (error) {
175
+ // Ignore cleanup errors
176
+ }
177
+ }
178
+ });
179
+ });
180
+
181
+ describe('Error Handling Integration', () => {
182
+ it('should handle parser errors gracefully', async () => {
183
+ // Test that the application handles various error conditions
184
+ // without crashing and provides meaningful error messages
185
+
186
+ const invalidFiles = [
187
+ 'non-existent.pdf',
188
+ 'empty.txt',
189
+ 'corrupted.docx'
190
+ ];
191
+
192
+ // Each of these should be handled gracefully by the application
193
+ // without causing the entire process to fail
194
+
195
+ expect(invalidFiles.length).toBeGreaterThan(0); // Placeholder assertion
196
+ });
197
+
198
+ it('should validate configuration parameters', async () => {
199
+ const invalidConfigs = [
200
+ { maxSize: -1 },
201
+ { maxSize: 'invalid' },
202
+ { provider: 'invalid-provider' }
203
+ ];
204
+
205
+ // Each invalid config should be caught and handled appropriately
206
+ expect(invalidConfigs.length).toBeGreaterThan(0); // Placeholder assertion
207
+ });
208
+ });
209
+ });