@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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +82 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +61 -0
- package/.github/workflows/auto-release.yml +78 -0
- package/.github/workflows/ci.yml +78 -0
- package/.github/workflows/publish.yml +43 -0
- package/.github/workflows/test.yml +37 -0
- package/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +19 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/rename.d.ts +2 -0
- package/dist/cli/rename.d.ts.map +1 -0
- package/dist/cli/rename.js +136 -0
- package/dist/cli/rename.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/excel-parser.d.ts +6 -0
- package/dist/parsers/excel-parser.d.ts.map +1 -0
- package/dist/parsers/excel-parser.js +42 -0
- package/dist/parsers/excel-parser.js.map +1 -0
- package/dist/parsers/factory.d.ts +7 -0
- package/dist/parsers/factory.d.ts.map +1 -0
- package/dist/parsers/factory.js +29 -0
- package/dist/parsers/factory.js.map +1 -0
- package/dist/parsers/pdf-parser.d.ts +7 -0
- package/dist/parsers/pdf-parser.d.ts.map +1 -0
- package/dist/parsers/pdf-parser.js +67 -0
- package/dist/parsers/pdf-parser.js.map +1 -0
- package/dist/parsers/text-parser.d.ts +6 -0
- package/dist/parsers/text-parser.d.ts.map +1 -0
- package/dist/parsers/text-parser.js +39 -0
- package/dist/parsers/text-parser.js.map +1 -0
- package/dist/parsers/word-parser.d.ts +6 -0
- package/dist/parsers/word-parser.d.ts.map +1 -0
- package/dist/parsers/word-parser.js +44 -0
- package/dist/parsers/word-parser.js.map +1 -0
- package/dist/services/ai-factory.d.ts +5 -0
- package/dist/services/ai-factory.d.ts.map +1 -0
- package/dist/services/ai-factory.js +15 -0
- package/dist/services/ai-factory.js.map +1 -0
- package/dist/services/claude-service.d.ts +9 -0
- package/dist/services/claude-service.d.ts.map +1 -0
- package/dist/services/claude-service.js +113 -0
- package/dist/services/claude-service.js.map +1 -0
- package/dist/services/file-renamer.d.ts +12 -0
- package/dist/services/file-renamer.d.ts.map +1 -0
- package/dist/services/file-renamer.js +99 -0
- package/dist/services/file-renamer.js.map +1 -0
- package/dist/services/openai-service.d.ts +9 -0
- package/dist/services/openai-service.d.ts.map +1 -0
- package/dist/services/openai-service.js +112 -0
- package/dist/services/openai-service.js.map +1 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-templates.d.ts +18 -0
- package/dist/utils/file-templates.d.ts.map +1 -0
- package/dist/utils/file-templates.js +232 -0
- package/dist/utils/file-templates.js.map +1 -0
- package/dist/utils/naming-conventions.d.ts +4 -0
- package/dist/utils/naming-conventions.d.ts.map +1 -0
- package/dist/utils/naming-conventions.js +55 -0
- package/dist/utils/naming-conventions.js.map +1 -0
- package/package.json +75 -0
- package/src/cli/commands.ts +20 -0
- package/src/cli/rename.ts +157 -0
- package/src/index.ts +17 -0
- package/src/parsers/excel-parser.ts +49 -0
- package/src/parsers/factory.ts +34 -0
- package/src/parsers/pdf-parser.ts +78 -0
- package/src/parsers/text-parser.ts +43 -0
- package/src/parsers/word-parser.ts +50 -0
- package/src/services/ai-factory.ts +16 -0
- package/src/services/claude-service.ts +114 -0
- package/src/services/file-renamer.ts +123 -0
- package/src/services/openai-service.ts +113 -0
- package/src/types/index.ts +71 -0
- package/src/types/pdf-extraction.d.ts +7 -0
- package/src/utils/file-templates.ts +275 -0
- package/src/utils/naming-conventions.ts +67 -0
- package/tests/data/empty-file.txt +0 -0
- package/tests/data/sample-markdown.md +9 -0
- package/tests/data/sample-pdf.pdf +0 -0
- package/tests/data/sample-text.txt +25 -0
- package/tests/integration/end-to-end.test.ts +209 -0
- package/tests/integration/workflow.test.ts +336 -0
- package/tests/mocks/mock-ai-service.ts +58 -0
- package/tests/unit/cli/commands.test.ts +163 -0
- package/tests/unit/parsers/factory.test.ts +100 -0
- package/tests/unit/parsers/pdf-parser.test.ts +63 -0
- package/tests/unit/parsers/text-parser.test.ts +85 -0
- package/tests/unit/services/ai-factory.test.ts +37 -0
- package/tests/unit/services/claude-service.test.ts +188 -0
- package/tests/unit/services/file-renamer.test.ts +299 -0
- package/tests/unit/services/openai-service.test.ts +196 -0
- package/tests/unit/utils/file-templates.test.ts +199 -0
- package/tests/unit/utils/naming-conventions.test.ts +88 -0
- package/tsconfig.json +20 -0
- 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
|
|
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
|
+
});
|