canvaslms-cli 1.4.6 → 1.5.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 (81) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +32 -22
  3. package/dist/commands/announcements.d.ts +3 -0
  4. package/dist/commands/announcements.d.ts.map +1 -0
  5. package/dist/commands/announcements.js +92 -0
  6. package/dist/commands/announcements.js.map +1 -0
  7. package/dist/commands/api.d.ts +3 -0
  8. package/dist/commands/api.d.ts.map +1 -0
  9. package/dist/commands/api.js +8 -0
  10. package/dist/commands/api.js.map +1 -0
  11. package/dist/commands/assignments.d.ts +3 -0
  12. package/dist/commands/assignments.d.ts.map +1 -0
  13. package/dist/commands/assignments.js +100 -0
  14. package/dist/commands/assignments.js.map +1 -0
  15. package/dist/commands/config.d.ts +6 -0
  16. package/dist/commands/config.d.ts.map +1 -0
  17. package/dist/commands/config.js +202 -0
  18. package/dist/commands/config.js.map +1 -0
  19. package/dist/commands/grades.d.ts +3 -0
  20. package/dist/commands/grades.d.ts.map +1 -0
  21. package/dist/commands/grades.js +335 -0
  22. package/dist/commands/grades.js.map +1 -0
  23. package/dist/commands/list.d.ts +3 -0
  24. package/dist/commands/list.d.ts.map +1 -0
  25. package/dist/commands/list.js +61 -0
  26. package/dist/commands/list.js.map +1 -0
  27. package/dist/commands/profile.d.ts +6 -0
  28. package/dist/commands/profile.d.ts.map +1 -0
  29. package/dist/commands/profile.js +30 -0
  30. package/dist/commands/profile.js.map +1 -0
  31. package/dist/commands/submit.d.ts +8 -0
  32. package/dist/commands/submit.d.ts.map +1 -0
  33. package/dist/commands/submit.js +319 -0
  34. package/dist/commands/submit.js.map +1 -0
  35. package/dist/index.d.ts +15 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +15 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/lib/api-client.d.ts +2 -0
  40. package/dist/lib/api-client.d.ts.map +1 -0
  41. package/dist/lib/api-client.js +76 -0
  42. package/dist/lib/api-client.js.map +1 -0
  43. package/dist/lib/config-validator.d.ts +4 -0
  44. package/dist/lib/config-validator.d.ts.map +1 -0
  45. package/dist/lib/config-validator.js +38 -0
  46. package/dist/lib/config-validator.js.map +1 -0
  47. package/dist/lib/config.d.ts +10 -0
  48. package/dist/lib/config.d.ts.map +1 -0
  49. package/dist/lib/config.js +85 -0
  50. package/dist/lib/config.js.map +1 -0
  51. package/dist/lib/file-upload.d.ts +3 -0
  52. package/dist/lib/file-upload.d.ts.map +1 -0
  53. package/dist/lib/file-upload.js +52 -0
  54. package/dist/lib/file-upload.js.map +1 -0
  55. package/dist/lib/interactive.d.ts +16 -0
  56. package/dist/lib/interactive.d.ts.map +1 -0
  57. package/dist/lib/interactive.js +758 -0
  58. package/dist/lib/interactive.js.map +1 -0
  59. package/dist/src/index.d.ts +3 -0
  60. package/dist/src/index.d.ts.map +1 -0
  61. package/dist/src/index.js +90 -0
  62. package/dist/src/index.js.map +1 -0
  63. package/dist/types/index.d.ts +147 -0
  64. package/dist/types/index.d.ts.map +1 -0
  65. package/dist/types/index.js +2 -0
  66. package/dist/types/index.js.map +1 -0
  67. package/package.json +15 -15
  68. package/commands/announcements.js +0 -102
  69. package/commands/api.js +0 -19
  70. package/commands/assignments.js +0 -100
  71. package/commands/config.js +0 -239
  72. package/commands/grades.js +0 -201
  73. package/commands/list.js +0 -68
  74. package/commands/profile.js +0 -35
  75. package/commands/submit.js +0 -603
  76. package/lib/api-client.js +0 -75
  77. package/lib/config-validator.js +0 -60
  78. package/lib/config.js +0 -103
  79. package/lib/file-upload.js +0 -74
  80. package/lib/interactive.js +0 -889
  81. package/src/index.js +0 -120
@@ -1,889 +0,0 @@
1
- /**
2
- * Interactive prompt utilities
3
- */
4
-
5
- import readline from 'readline';
6
- import fs from 'fs';
7
- import path from 'path';
8
- import AdmZip from 'adm-zip';
9
- import chalk from 'chalk';
10
-
11
- // Key codes for raw terminal input
12
- const KEYS = {
13
- UP: '\u001b[A',
14
- DOWN: '\u001b[B',
15
- LEFT: '\u001b[D',
16
- RIGHT: '\u001b[C',
17
- SPACE: ' ',
18
- ENTER: '\r',
19
- ESCAPE: '\u001b',
20
- BACKSPACE: '\u007f',
21
- TAB: '\t'
22
- };
23
-
24
- /**
25
- * Create readline interface for user input
26
- */
27
- function createReadlineInterface() {
28
- return readline.createInterface({
29
- input: process.stdin,
30
- output: process.stdout
31
- });
32
- }
33
-
34
- /**
35
- * Prompt user for input
36
- */
37
- function askQuestion(rl, question) {
38
- return new Promise((resolve) => {
39
- rl.question(question, (answer) => {
40
- resolve(answer.trim());
41
- });
42
- });
43
- }
44
-
45
- /**
46
- * Ask a question with validation and retry logic
47
- */
48
- function askQuestionWithValidation(rl, question, validator, errorMessage) {
49
- return new Promise(async (resolve) => {
50
- let answer;
51
- do {
52
- answer = await askQuestion(rl, question);
53
- if (validator(answer)) {
54
- resolve(answer.trim());
55
- return;
56
- } else {
57
- console.log(errorMessage || 'Invalid input. Please try again.');
58
- }
59
- } while (true);
60
- });
61
- }
62
-
63
- /**
64
- * Ask for confirmation (Y/n format)
65
- */
66
- async function askConfirmation(rl, question, defaultYes = true, options = {}) {
67
- const { requireExplicit = false } = options;
68
- const suffix = defaultYes ? " (Y/n)" : " (y/N)";
69
-
70
- while (true) {
71
- const answer = await askQuestion(rl, question + suffix + ": ");
72
- const lower = answer.toLowerCase();
73
-
74
- // If user presses Enter → return defaultYes (true by default)
75
- if (lower === "") {
76
- if (!requireExplicit) {
77
- return defaultYes;
78
- }
79
- console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
80
- continue;
81
- }
82
-
83
- // Convert input to boolean
84
- if (lower === "y" || lower === "yes") {
85
- return true;
86
- }
87
- if (lower === "n" || lower === "no") {
88
- return false;
89
- }
90
-
91
- if (!requireExplicit) {
92
- // If input is something else, fallback to defaultYes
93
- return defaultYes;
94
- }
95
-
96
- console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
97
- }
98
- }
99
-
100
- /**
101
- * Select from a list of options
102
- */
103
- async function selectFromList(rl, items, displayProperty = null, allowCancel = true) {
104
- if (!items || items.length === 0) {
105
- console.log('No items to select from.');
106
- return null;
107
- }
108
-
109
- console.log('\nSelect an option:');
110
- items.forEach((item, index) => {
111
- const displayText = displayProperty ? item[displayProperty] : item;
112
- console.log(`${index + 1}. ${displayText}`);
113
- });
114
-
115
- if (allowCancel) {
116
- console.log('0. Cancel');
117
- }
118
-
119
- const validator = (input) => {
120
- const num = parseInt(input);
121
- return !isNaN(num) && num >= (allowCancel ? 0 : 1) && num <= items.length;
122
- };
123
-
124
- const answer = await askQuestionWithValidation(
125
- rl,
126
- '\nEnter your choice: ',
127
- validator,
128
- `Please enter a number between ${allowCancel ? '0' : '1'} and ${items.length}.`
129
- );
130
-
131
- const choice = parseInt(answer);
132
-
133
- if (choice === 0 && allowCancel) {
134
- return null;
135
- }
136
-
137
- return items[choice - 1];
138
- }
139
-
140
- function getSubfoldersRecursive(startDir = process.cwd()) {
141
- const result = [];
142
- function walk(dir) {
143
- const items = fs.readdirSync(dir);
144
- for (const item of items) {
145
- const fullPath = path.join(dir, item);
146
- try {
147
- const stat = fs.statSync(fullPath);
148
- if (stat.isDirectory()) {
149
- const baseName = path.basename(fullPath);
150
- if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
151
- result.push(fullPath);
152
- walk(fullPath); // recurse into subdirectory
153
- }
154
- } catch (err) {
155
- // Optionally log: console.warn(`Skipped unreadable folder: ${fullPath}`);
156
- }
157
- }
158
- }
159
- walk(startDir);
160
- return result;
161
- }
162
-
163
- /**
164
- * Get files matching a wildcard pattern
165
- */
166
- function getFilesMatchingWildcard(pattern, currentDir = process.cwd()) {
167
- try {
168
- // Gather all subfolders
169
- const allFolders = [currentDir, ...getSubfoldersRecursive(currentDir)];
170
- let allFiles = [];
171
- for (const folder of allFolders) {
172
- const files = fs.readdirSync(folder).map(f => path.join(folder, f));
173
- for (const filePath of files) {
174
- try {
175
- if (fs.statSync(filePath).isFile()) {
176
- allFiles.push(filePath);
177
- }
178
- } catch (e) {}
179
- }
180
- }
181
- // Convert wildcard pattern to regex
182
- let regexPattern;
183
- let matchFullPath = false;
184
- if (pattern === '*' || (!pattern.includes('.') && !pattern.includes('/'))) {
185
- regexPattern = new RegExp('.*', 'i');
186
- matchFullPath = true;
187
- } else if (pattern.startsWith('*.')) {
188
- const extension = pattern.slice(2);
189
- regexPattern = new RegExp(`\\.${extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
190
- } else if (pattern.includes('*')) {
191
- regexPattern = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i');
192
- } else {
193
- regexPattern = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
194
- }
195
- const matchedFiles = allFiles.filter(filePath => {
196
- if (matchFullPath) {
197
- const relPath = path.relative(currentDir, filePath);
198
- return regexPattern.test(relPath);
199
- } else {
200
- return regexPattern.test(path.basename(filePath));
201
- }
202
- });
203
- return matchedFiles;
204
- } catch (error) {
205
- console.error(`Error reading directory: ${error.message}`);
206
- return [];
207
- }
208
- }
209
-
210
- function pad(str, len) {
211
- return str + ' '.repeat(Math.max(0, len - str.length));
212
- }
213
-
214
- /**
215
- * Enhanced file selection with wildcard support
216
- */
217
- async function selectFilesImproved(rl, currentDir = process.cwd()) {
218
- const selectedFiles = [];
219
- console.log(chalk.cyan.bold('\n' + '-'.repeat(50)));
220
- console.log(chalk.cyan.bold('File Selection'));
221
- console.log(chalk.cyan('-'.repeat(50)));
222
- console.log(chalk.yellow('Tips:'));
223
- console.log(' • Type filename to add individual files');
224
- console.log(' • Use wildcards: *.html, *.js, *.pdf, etc.');
225
- console.log(' • Type "browse" to see available files');
226
- console.log(' • Type "remove" to remove files from selection');
227
- console.log(' • Type ".." or "back" to return to previous menu');
228
- console.log(' • Press Enter with no input to finish selection\n');
229
- while (true) {
230
- if (selectedFiles.length > 0) {
231
- console.log(chalk.cyan('\n' + '-'.repeat(50)));
232
- console.log(chalk.cyan.bold(`Currently selected (${selectedFiles.length} files):`));
233
- selectedFiles.forEach((file, index) => {
234
- const stats = fs.statSync(file);
235
- const size = (stats.size / 1024).toFixed(1) + ' KB';
236
- console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.gray(size));
237
- });
238
- console.log(chalk.cyan('-'.repeat(50)));
239
- }
240
- const input = await askQuestion(rl, chalk.bold.cyan('\nAdd file (or press Enter to finish): '));
241
- if (!input.trim()) break;
242
- if (input === '..' || input.toLowerCase() === 'back') {
243
- return selectedFiles;
244
- }
245
- if (input.toLowerCase() === 'browse') {
246
- console.log(chalk.cyan('\n' + '-'.repeat(50)));
247
- console.log(chalk.cyan.bold('Browsing available files:'));
248
- try {
249
- const listedFiles = [];
250
- function walk(dir) {
251
- const entries = fs.readdirSync(dir);
252
- for (const entry of entries) {
253
- const fullPath = path.join(dir, entry);
254
- const relPath = path.relative(currentDir, fullPath);
255
- if (['node_modules', '.git', 'dist', 'build'].includes(entry)) continue;
256
- try {
257
- const stat = fs.statSync(fullPath);
258
- if (stat.isDirectory()) {
259
- walk(fullPath);
260
- } else if (stat.isFile()) {
261
- listedFiles.push({ path: fullPath, rel: relPath, size: stat.size });
262
- }
263
- } catch (e) { continue; }
264
- }
265
- }
266
- walk(currentDir);
267
- if (listedFiles.length === 0) {
268
- console.log(chalk.red(' No suitable files found.'));
269
- } else {
270
- listedFiles.forEach((file, index) => {
271
- const sizeKB = (file.size / 1024).toFixed(1);
272
- console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(file.rel, 35) + chalk.gray(sizeKB + ' KB'));
273
- });
274
- }
275
- } catch (error) {
276
- console.log(chalk.red(' Error reading directory: ' + error.message));
277
- }
278
- continue;
279
- }
280
- if (input.toLowerCase() === 'remove') {
281
- if (selectedFiles.length === 0) {
282
- console.log(chalk.red('No files selected to remove.'));
283
- continue;
284
- }
285
- console.log(chalk.cyan('\nSelect file to remove:'));
286
- selectedFiles.forEach((file, index) => {
287
- console.log(pad(chalk.white((index + 1) + '.'), 5) + path.basename(file));
288
- });
289
- const removeChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter number to remove (or press Enter to cancel): '));
290
- if (removeChoice.trim()) {
291
- const removeIndex = parseInt(removeChoice) - 1;
292
- if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
293
- const removedFile = selectedFiles.splice(removeIndex, 1)[0];
294
- console.log(chalk.green(`Removed: ${path.basename(removedFile)}`));
295
- } else {
296
- console.log(chalk.red('Invalid selection.'));
297
- }
298
- }
299
- continue;
300
- }
301
- let filePath = input;
302
- let zipRequested = false;
303
- if (filePath.endsWith(' -zip')) {
304
- filePath = filePath.slice(0, -5).trim();
305
- zipRequested = true;
306
- }
307
- if (!path.isAbsolute(filePath)) {
308
- filePath = path.join(currentDir, filePath);
309
- }
310
- try {
311
- if (!fs.existsSync(filePath)) {
312
- console.log(chalk.red('Error: File not found: ' + input));
313
- continue;
314
- }
315
- const stats = fs.statSync(filePath);
316
- if (zipRequested) {
317
- const baseName = path.basename(filePath);
318
- const zipName = baseName.replace(/\.[^/.]+$/, '') + '.zip';
319
- const zipPath = path.join(currentDir, zipName);
320
- const zip = new AdmZip();
321
- process.stdout.write(chalk.yellow('Zipping, please wait... '));
322
- if (stats.isDirectory()) {
323
- zip.addLocalFolder(filePath);
324
- } else if (stats.isFile()) {
325
- zip.addLocalFile(filePath);
326
- } else {
327
- console.log(chalk.red('Not a file or folder.'));
328
- continue;
329
- }
330
- zip.writeZip(zipPath);
331
- console.log(chalk.green('Done.'));
332
- console.log(chalk.green(`Created ZIP: ${zipName}`));
333
- if (selectedFiles.includes(zipPath)) {
334
- console.log(chalk.yellow(`File already selected: ${zipName}`));
335
- continue;
336
- }
337
- selectedFiles.push(zipPath);
338
- const size = (fs.statSync(zipPath).size / 1024).toFixed(1) + ' KB';
339
- console.log(chalk.green(`Added: ${zipName} (${size})`));
340
- continue;
341
- }
342
- if (stats.isDirectory()) {
343
- const baseName = path.basename(filePath);
344
- if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
345
- const collectedFiles = [];
346
- function walk(dir) {
347
- const entries = fs.readdirSync(dir);
348
- for (const entry of entries) {
349
- const fullPath = path.join(dir, entry);
350
- const stat = fs.statSync(fullPath);
351
- if (stat.isDirectory()) {
352
- const baseName = path.basename(fullPath);
353
- if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
354
- walk(fullPath);
355
- } else if (stat.isFile()) {
356
- collectedFiles.push(fullPath);
357
- }
358
- }
359
- }
360
- walk(filePath);
361
- if (collectedFiles.length === 0) {
362
- console.log(chalk.yellow(`Folder is empty: ${input}`));
363
- continue;
364
- }
365
- console.log(chalk.cyan(`\nFound ${collectedFiles.length} file(s) in folder "${path.relative(currentDir, filePath)}":`));
366
- let totalSize = 0;
367
- collectedFiles.forEach((f, i) => {
368
- const stat = fs.statSync(f);
369
- totalSize += stat.size;
370
- const relativePath = path.relative(currentDir, f);
371
- console.log(pad(chalk.white((i + 1) + '.'), 5) + pad(relativePath, 35) + chalk.gray((stat.size / 1024).toFixed(1) + ' KB'));
372
- });
373
- console.log(chalk.cyan('-'.repeat(50)));
374
- console.log(chalk.cyan(`Total size: ${(totalSize / 1024).toFixed(1)} KB`));
375
- const confirmFolder = await askConfirmation(rl, chalk.bold.cyan(`Add all ${collectedFiles.length} files from this folder?`), true);
376
- if (confirmFolder) {
377
- const newFiles = collectedFiles.filter(f => !selectedFiles.includes(f));
378
- selectedFiles.push(...newFiles);
379
- console.log(chalk.green(`Added ${newFiles.length} new files (${collectedFiles.length - newFiles.length} already selected)`));
380
- }
381
- continue;
382
- }
383
- if (selectedFiles.includes(filePath)) {
384
- console.log(chalk.yellow(`File already selected: ${path.basename(filePath)}`));
385
- continue;
386
- }
387
- selectedFiles.push(filePath);
388
- const size = (stats.size / 1024).toFixed(1) + ' KB';
389
- console.log(chalk.green(`Added: ${path.basename(filePath)} (${size})`));
390
- } catch (error) {
391
- console.log(chalk.red('Error accessing file: ' + error.message));
392
- }
393
- }
394
- return selectedFiles;
395
- }
396
-
397
- /**
398
- * Interactive file selector with tree view and keyboard navigation
399
- */
400
- async function selectFilesKeyboard(rl, currentDir = process.cwd()) {
401
- const selectedFiles = [];
402
- let expandedFolders = new Set();
403
- let fileTree = [];
404
- let fileList = [];
405
- let currentPath = currentDir;
406
- let currentIndex = 0;
407
- let isNavigating = true;
408
- let viewStartIndex = 0;
409
- const maxVisibleItems = 15;
410
-
411
- // Setup raw mode for keyboard input
412
- process.stdin.setRawMode(true);
413
- process.stdin.resume();
414
- process.stdin.setEncoding('utf8');
415
-
416
- // Helper function to build file tree
417
- function buildFileTree(basePath = currentDir, level = 0, parentPath = '') {
418
- const tree = [];
419
- try {
420
- const entries = fs.readdirSync(basePath).sort();
421
-
422
- // Add directories first
423
- entries.forEach(entry => {
424
- const fullPath = path.join(basePath, entry);
425
- const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
426
-
427
- try {
428
- const stats = fs.statSync(fullPath);
429
- if (stats.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.vscode', '.next'].includes(entry)) {
430
- const isExpanded = expandedFolders.has(fullPath);
431
- tree.push({
432
- name: entry,
433
- path: fullPath,
434
- relativePath,
435
- type: 'directory',
436
- level,
437
- isExpanded,
438
- size: 0
439
- });
440
-
441
- // If expanded, add children
442
- if (isExpanded) {
443
- const children = buildFileTree(fullPath, level + 1, relativePath);
444
- tree.push(...children);
445
- }
446
- }
447
- } catch (e) {}
448
- });
449
-
450
- // Add files
451
- entries.forEach(entry => {
452
- const fullPath = path.join(basePath, entry);
453
- const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
454
-
455
- try {
456
- const stats = fs.statSync(fullPath);
457
- if (stats.isFile()) {
458
- tree.push({
459
- name: entry,
460
- path: fullPath,
461
- relativePath,
462
- type: 'file',
463
- level,
464
- size: stats.size
465
- });
466
- }
467
- } catch (e) {}
468
- });
469
- } catch (error) {
470
- console.log(chalk.red('Error reading directory: ' + error.message));
471
- }
472
-
473
- return tree;
474
- }
475
-
476
- // Helper function to get file icon based on extension
477
- function getFileIcon(filename) {
478
- const ext = path.extname(filename).toLowerCase();
479
- const icons = {
480
- '.pdf': '📄', '.doc': '📄', '.docx': '📄', '.txt': '📄',
481
- '.js': '📜', '.ts': '📜', '.py': '📜', '.java': '📜', '.cpp': '📜', '.c': '📜',
482
- '.html': '🌐', '.css': '🎨', '.scss': '🎨', '.less': '🎨',
483
- '.json': '⚙️', '.xml': '⚙️', '.yml': '⚙️', '.yaml': '⚙️',
484
- '.zip': '📦', '.rar': '📦', '.7z': '📦', '.tar': '📦',
485
- '.jpg': '🖼️', '.jpeg': '🖼️', '.png': '🖼️', '.gif': '🖼️', '.svg': '🖼️',
486
- '.mp4': '🎬', '.avi': '🎬', '.mov': '🎬', '.mkv': '🎬',
487
- '.mp3': '🎵', '.wav': '🎵', '.flac': '🎵'
488
- };
489
- return icons[ext] || '📋';
490
- }
491
- // Helper function to build breadcrumb path
492
- function buildBreadcrumb() {
493
- const relativePath = path.relative(currentDir, currentPath);
494
- if (!relativePath || relativePath === '.') {
495
- return ''
496
- }
497
-
498
- const parts = relativePath.split(path.sep);
499
- const breadcrumb = parts.map((part, index) => {
500
- if (index === parts.length - 1) {
501
- return chalk.white.bold(part);
502
- }
503
- return chalk.gray(part);
504
- }).join(chalk.gray(' › '));
505
-
506
- return chalk.yellow('📂 ') + breadcrumb;
507
- }
508
-
509
- // Helper to clear the terminal without dropping scrollback history
510
- function safeClearScreen() {
511
- if (!process.stdout.isTTY) return;
512
-
513
- process.stdout.write('\x1b[H\x1b[J'); // Move cursor to top and clear below, not full console.clear()
514
-
515
- }
516
-
517
- // Helper function to display the file browser
518
- function displayBrowser() {
519
- safeClearScreen();
520
- // Keyboard controls - compact format at top
521
- const controls = [
522
- ];
523
- // Breadcrumb path
524
- console.log(buildBreadcrumb());
525
- console.log(chalk.gray('💡 ↑↓←→:Navigate', 'Space:Select', 'Enter:Open/Finish', 'Backspace:Up', 'a:All', 'c:Clear', 'Esc:Exit'));
526
-
527
- // Selected files count
528
- if (selectedFiles.length > 0) {
529
- const totalSize = selectedFiles.reduce((sum, file) => {
530
- try {
531
- return sum + fs.statSync(file).size;
532
- } catch {
533
- return sum;
534
- }
535
- }, 0);
536
- console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB) - Press Enter to finish`));
537
- }
538
-
539
- console.log();
540
-
541
- if (fileList.length === 0) {
542
- console.log(chalk.yellow('📭 No files found in this directory.'));
543
- return;
544
- }
545
-
546
- // Display files with tree-like structure
547
- displayFileTree();
548
- }
549
- // Helper function to display files in a horizontal grid layout
550
- function displayFileTree() {
551
- const terminalWidth = process.stdout.columns || 80;
552
- const maxDisplayItems = 50; // Show more items in grid view
553
- const startIdx = Math.max(0, currentIndex - Math.floor(maxDisplayItems / 2));
554
- const endIdx = Math.min(fileList.length, startIdx + maxDisplayItems);
555
- const visibleItems = fileList.slice(startIdx, endIdx);
556
-
557
- // Show scroll indicators
558
- if (startIdx > 0) {
559
- console.log(chalk.gray(` ⋮ (${startIdx} items above)`));
560
- }
561
-
562
- // Calculate item width and columns
563
- const maxItemWidth = Math.max(...visibleItems.map(item => {
564
- const name = path.basename(item.path);
565
- return name.length + 4; // 2 for icon + space, 2 for padding
566
- }));
567
-
568
- const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25); // Min 15, max 25 chars
569
- const columnsPerRow = Math.floor((terminalWidth - 4) / itemWidth); // Leave 4 chars margin
570
- const actualColumns = Math.max(1, columnsPerRow);
571
-
572
- // Group items into rows
573
- let currentRow = '';
574
- let itemsInCurrentRow = 0;
575
-
576
- visibleItems.forEach((item, index) => {
577
- const actualIndex = startIdx + index;
578
- const isSelected = selectedFiles.includes(item.path);
579
- const isCurrent = actualIndex === currentIndex;
580
-
581
- // Get icon and name
582
- let icon = '';
583
- if (item.type === 'parent' || item.type === 'directory') {
584
- icon = '📁';
585
- } else {
586
- icon = getFileIcon(path.basename(item.path));
587
- }
588
-
589
- const name = item.name || path.basename(item.path);
590
- const truncatedName = name.length > itemWidth - 4 ? name.slice(0, itemWidth - 7) + '...' : name;
591
-
592
- // Build item display string
593
- let itemDisplay = `${icon} ${truncatedName}`;
594
-
595
- // Apply styling based on state
596
- if (isCurrent) {
597
- if (isSelected) {
598
- itemDisplay = chalk.black.bgGreen(` ${itemDisplay}`.padEnd(itemWidth - 1));
599
- } else if (item.type === 'parent') {
600
- itemDisplay = chalk.white.bgBlue(` ${itemDisplay}`.padEnd(itemWidth - 1));
601
- } else if (item.type === 'directory') {
602
- itemDisplay = chalk.black.bgCyan(` ${itemDisplay}`.padEnd(itemWidth - 1));
603
- } else {
604
- itemDisplay = chalk.black.bgWhite(` ${itemDisplay}`.padEnd(itemWidth - 1));
605
- }
606
- } else {
607
- if (isSelected) {
608
- itemDisplay = chalk.green(`✓${itemDisplay}`.padEnd(itemWidth));
609
- } else if (item.type === 'parent') {
610
- itemDisplay = chalk.blue(` ${itemDisplay}`.padEnd(itemWidth));
611
- } else if (item.type === 'directory') {
612
- itemDisplay = chalk.cyan(` ${itemDisplay}`.padEnd(itemWidth));
613
- } else {
614
- itemDisplay = chalk.white(` ${itemDisplay}`.padEnd(itemWidth));
615
- }
616
- }
617
-
618
- // Add to current row
619
- currentRow += itemDisplay;
620
- itemsInCurrentRow++;
621
-
622
- // Check if we need to start a new row
623
- if (itemsInCurrentRow >= actualColumns || index === visibleItems.length - 1) {
624
- console.log(currentRow);
625
- currentRow = '';
626
- itemsInCurrentRow = 0;
627
- }
628
- });
629
-
630
- // Show scroll indicators
631
- if (endIdx < fileList.length) {
632
- console.log(chalk.gray(` ⋮ (${fileList.length - endIdx} items below)`));
633
- }
634
-
635
- console.log();
636
-
637
- // Show current position and navigation info
638
- if (fileList.length > maxDisplayItems) {
639
- console.log(chalk.gray(`Showing ${startIdx + 1}-${endIdx} of ${fileList.length} items | Current: ${currentIndex + 1}`));
640
- } else {
641
- console.log(chalk.gray(`${fileList.length} items | Current: ${currentIndex + 1}`));
642
- }
643
-
644
- // Show grid info
645
- console.log(chalk.gray(`Grid: ${actualColumns} columns × ${itemWidth} chars | Terminal width: ${terminalWidth}`));
646
- }
647
- // Helper function to refresh file list for current directory
648
- function refreshFileList() {
649
- fileList = [];
650
-
651
- try {
652
- // Add parent directory option if not at root
653
- if (currentPath !== currentDir) {
654
- fileList.push({
655
- type: 'parent',
656
- path: path.dirname(currentPath),
657
- name: '..'
658
- });
659
- }
660
-
661
- // Read current directory
662
- const entries = fs.readdirSync(currentPath).sort();
663
-
664
- // Add directories first
665
- entries.forEach(entry => {
666
- const fullPath = path.join(currentPath, entry);
667
- const stat = fs.statSync(fullPath);
668
-
669
- if (stat.isDirectory()) {
670
- fileList.push({
671
- type: 'directory',
672
- path: fullPath,
673
- name: entry
674
- });
675
- }
676
- });
677
-
678
- // Add files
679
- entries.forEach(entry => {
680
- const fullPath = path.join(currentPath, entry);
681
- const stat = fs.statSync(fullPath);
682
-
683
- if (stat.isFile()) {
684
- fileList.push({
685
- type: 'file',
686
- path: fullPath,
687
- name: entry,
688
- size: stat.size
689
- });
690
- }
691
- });
692
-
693
- // Ensure currentIndex is within bounds
694
- if (currentIndex >= fileList.length) {
695
- currentIndex = Math.max(0, fileList.length - 1);
696
- }
697
-
698
- } catch (error) {
699
- console.error('Error reading directory:', error.message);
700
- fileList = [];
701
- }
702
- }
703
- // Main keyboard event handler
704
- function handleKeyInput(key) {
705
- // Calculate grid dimensions for navigation
706
- const terminalWidth = process.stdout.columns || 80;
707
- const maxItemWidth = Math.max(...fileList.map(item => {
708
- const name = path.basename(item.path);
709
- return name.length + 4;
710
- }));
711
- const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25);
712
- const columnsPerRow = Math.max(1, Math.floor((terminalWidth - 4) / itemWidth));
713
-
714
- switch (key) {
715
- case KEYS.UP:
716
- // Move up by one row (subtract columns)
717
- const newUpIndex = currentIndex - columnsPerRow;
718
- if (newUpIndex >= 0) {
719
- currentIndex = newUpIndex;
720
- displayBrowser();
721
- }
722
- break;
723
-
724
- case KEYS.DOWN:
725
- // Move down by one row (add columns)
726
- const newDownIndex = currentIndex + columnsPerRow;
727
- if (newDownIndex < fileList.length) {
728
- currentIndex = newDownIndex;
729
- displayBrowser();
730
- }
731
- break;
732
-
733
- case KEYS.LEFT:
734
- // Move left by one column
735
- if (currentIndex > 0) {
736
- currentIndex--;
737
- displayBrowser();
738
- }
739
- break;
740
-
741
- case KEYS.RIGHT:
742
- // Move right by one column
743
- if (currentIndex < fileList.length - 1) {
744
- currentIndex++;
745
- displayBrowser();
746
- }
747
- break;
748
-
749
- case KEYS.SPACE:
750
- if (fileList.length > 0) {
751
- const item = fileList[currentIndex];
752
- if (item.type === 'file') {
753
- const index = selectedFiles.indexOf(item.path);
754
- if (index === -1) {
755
- selectedFiles.push(item.path);
756
- } else {
757
- selectedFiles.splice(index, 1);
758
- }
759
- displayBrowser();
760
- }
761
- }
762
- break;
763
- case KEYS.ENTER:
764
- if (fileList.length > 0) {
765
- const item = fileList[currentIndex];
766
- if (item.type === 'parent' || item.type === 'directory') {
767
- currentPath = item.path;
768
- currentIndex = 0;
769
- refreshFileList();
770
- displayBrowser();
771
- } else {
772
- // When Enter is pressed on a file, finish selection if files are selected
773
- if (selectedFiles.length > 0) {
774
- isNavigating = false;
775
- }
776
- }
777
- } else {
778
- // Finish selection when directory is empty and Enter is pressed (if files selected)
779
- if (selectedFiles.length > 0) {
780
- isNavigating = false;
781
- }
782
- }
783
- break;
784
-
785
- case KEYS.BACKSPACE:
786
- if (currentPath !== currentDir) {
787
- currentPath = path.dirname(currentPath);
788
- currentIndex = 0;
789
- refreshFileList();
790
- displayBrowser();
791
- }
792
- break;
793
-
794
- case 'a':
795
- // Select all files in current directory
796
- let addedCount = 0;
797
- fileList.forEach(item => {
798
- if (item.type === 'file' && !selectedFiles.includes(item.path)) {
799
- selectedFiles.push(item.path);
800
- addedCount++;
801
- }
802
- });
803
- if (addedCount > 0) {
804
- displayBrowser();
805
- }
806
- break;
807
-
808
- case 'c':
809
- // Clear all selections
810
- selectedFiles.length = 0;
811
- displayBrowser();
812
- break;
813
-
814
- case KEYS.ESCAPE:
815
- case '\u001b': // ESC key
816
- isNavigating = false;
817
- break;
818
-
819
- default:
820
- // Ignore other keys
821
- break;
822
- }
823
- }
824
-
825
- // Initialize and start navigation
826
- return new Promise((resolve) => {
827
- refreshFileList();
828
- displayBrowser();
829
-
830
- process.stdin.on('data', (key) => {
831
- if (!isNavigating) {
832
- return;
833
- }
834
-
835
- const keyStr = key.toString();
836
- handleKeyInput(keyStr);
837
-
838
- if (!isNavigating) {
839
- // Cleanup
840
- process.stdin.removeAllListeners('data');
841
- process.stdin.setRawMode(false);
842
- process.stdin.pause();
843
-
844
- // Display completion summary
845
- if (selectedFiles.length > 0) {
846
- console.log(chalk.green.bold('✅ File Selection Complete!'));
847
- console.log(chalk.cyan('-'.repeat(50)));
848
-
849
- const totalSize = selectedFiles.reduce((sum, file) => {
850
- try {
851
- return sum + fs.statSync(file).size;
852
- } catch {
853
- return sum;
854
- }
855
- }, 0);
856
-
857
- console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
858
- selectedFiles.forEach((file, index) => {
859
- try {
860
- const stats = fs.statSync(file);
861
- const size = (stats.size / 1024).toFixed(1) + ' KB';
862
- console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
863
- } catch (e) {
864
- console.log(pad(chalk.red(`${index + 1}.`), 5) + chalk.red(path.basename(file) + ' (Error reading file)'));
865
- }
866
- });
867
- console.log(chalk.cyan('-'.repeat(50)));
868
- } else {
869
- console.log(chalk.yellow('No files selected.'));
870
- }
871
-
872
- resolve(selectedFiles);
873
- }
874
- });
875
- });
876
- }
877
-
878
- export {
879
- createReadlineInterface,
880
- askQuestion,
881
- askQuestionWithValidation,
882
- askConfirmation,
883
- selectFromList,
884
- selectFilesImproved,
885
- selectFilesKeyboard,
886
- getFilesMatchingWildcard,
887
- getSubfoldersRecursive,
888
- pad
889
- };