canvaslms-cli 1.4.1 → 1.4.2
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/commands/submit.js +22 -5
- package/lib/file-upload.js +2 -1
- package/lib/interactive.js +416 -0
- package/package.json +1 -1
package/commands/submit.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { makeCanvasRequest } from '../lib/api-client.js';
|
|
8
|
-
import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved, pad } from '../lib/interactive.js';
|
|
8
|
+
import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved, selectFilesInteractive, pad } from '../lib/interactive.js';
|
|
9
9
|
import { uploadSingleFileToCanvas, submitAssignmentWithFiles } from '../lib/file-upload.js';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
|
|
@@ -146,15 +146,32 @@ export async function submitAssignment(options) {
|
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
}
|
|
150
|
-
// Step 3: Always list files in current directory and prompt user to select
|
|
149
|
+
} // Step 3: Choose file selection method and select files
|
|
151
150
|
let filePaths = [];
|
|
152
151
|
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
153
|
-
console.log(chalk.cyan.bold('File Selection'));
|
|
152
|
+
console.log(chalk.cyan.bold('File Selection Method'));
|
|
154
153
|
console.log(chalk.cyan('-'.repeat(60)));
|
|
155
154
|
console.log(chalk.white('Course: ') + chalk.bold(selectedCourse.name));
|
|
156
155
|
console.log(chalk.white('Assignment: ') + chalk.bold(selectedAssignment.name) + '\n');
|
|
157
|
-
|
|
156
|
+
|
|
157
|
+
console.log(chalk.yellow('📁 Choose file selection method:'));
|
|
158
|
+
console.log(chalk.white('1. ') + chalk.cyan('Enhanced Interactive Selector') + chalk.gray(' (Recommended - Visual browser with search, filters, and navigation)'));
|
|
159
|
+
console.log(chalk.white('2. ') + chalk.cyan('Text-based Selector') + chalk.gray(' (Traditional - Type filenames and wildcards)'));
|
|
160
|
+
console.log(chalk.white('3. ') + chalk.cyan('Basic Directory Listing') + chalk.gray(' (Simple - Select from numbered list)'));
|
|
161
|
+
|
|
162
|
+
const selectorChoice = await askQuestion(rl, chalk.bold.cyan('\nChoose method (1-3): '));
|
|
163
|
+
|
|
164
|
+
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
165
|
+
console.log(chalk.cyan.bold('File Selection'));
|
|
166
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
167
|
+
|
|
168
|
+
if (selectorChoice === '1') {
|
|
169
|
+
filePaths = await selectFilesInteractive(rl);
|
|
170
|
+
} else if (selectorChoice === '2') {
|
|
171
|
+
filePaths = await selectFilesImproved(rl);
|
|
172
|
+
} else {
|
|
173
|
+
filePaths = await selectFiles(rl);
|
|
174
|
+
}
|
|
158
175
|
// Validate all selected files exist
|
|
159
176
|
const validFiles = [];
|
|
160
177
|
for (const file of filePaths) {
|
package/lib/file-upload.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
6
7
|
import axios from 'axios';
|
|
7
8
|
import FormData from 'form-data';
|
|
8
9
|
import { makeCanvasRequest } from './api-client.js';
|
|
@@ -17,7 +18,7 @@ export async function uploadSingleFileToCanvas(courseId, assignmentId, filePath)
|
|
|
17
18
|
throw new Error(`File not found: ${filePath}`);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const fileName =
|
|
21
|
+
const fileName = path.basename(filePath);
|
|
21
22
|
const fileContent = fs.readFileSync(filePath);
|
|
22
23
|
|
|
23
24
|
// Step 1: Get upload URL from Canvas
|
package/lib/interactive.js
CHANGED
|
@@ -359,6 +359,421 @@ async function selectFilesImproved(rl, currentDir = process.cwd()) {
|
|
|
359
359
|
return selectedFiles;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Enhanced interactive file selector with real-time preview and navigation
|
|
364
|
+
*/
|
|
365
|
+
async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
366
|
+
const selectedFiles = [];
|
|
367
|
+
let currentPath = currentDir;
|
|
368
|
+
let fileList = [];
|
|
369
|
+
let currentPage = 0;
|
|
370
|
+
const itemsPerPage = 10;
|
|
371
|
+
|
|
372
|
+
// Helper function to refresh file list
|
|
373
|
+
function refreshFileList() {
|
|
374
|
+
try {
|
|
375
|
+
const entries = fs.readdirSync(currentPath);
|
|
376
|
+
fileList = [];
|
|
377
|
+
|
|
378
|
+
// Add parent directory option if not at root
|
|
379
|
+
if (currentPath !== currentDir) {
|
|
380
|
+
fileList.push({
|
|
381
|
+
name: '📁 .. (Parent Directory)',
|
|
382
|
+
path: path.dirname(currentPath),
|
|
383
|
+
type: 'parent',
|
|
384
|
+
size: 0
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Add directories first
|
|
389
|
+
entries.forEach(entry => {
|
|
390
|
+
const fullPath = path.join(currentPath, entry);
|
|
391
|
+
try {
|
|
392
|
+
const stats = fs.statSync(fullPath);
|
|
393
|
+
if (stats.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.vscode'].includes(entry)) {
|
|
394
|
+
fileList.push({
|
|
395
|
+
name: `📁 ${entry}`,
|
|
396
|
+
path: fullPath,
|
|
397
|
+
type: 'directory',
|
|
398
|
+
size: 0
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Add files
|
|
405
|
+
entries.forEach(entry => {
|
|
406
|
+
const fullPath = path.join(currentPath, entry);
|
|
407
|
+
try {
|
|
408
|
+
const stats = fs.statSync(fullPath);
|
|
409
|
+
if (stats.isFile()) {
|
|
410
|
+
const icon = getFileIcon(entry);
|
|
411
|
+
fileList.push({
|
|
412
|
+
name: `${icon} ${entry}`,
|
|
413
|
+
path: fullPath,
|
|
414
|
+
type: 'file',
|
|
415
|
+
size: stats.size
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
} catch (e) {}
|
|
419
|
+
});
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.log(chalk.red('Error reading directory: ' + error.message));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Helper function to get file icon based on extension
|
|
426
|
+
function getFileIcon(filename) {
|
|
427
|
+
const ext = path.extname(filename).toLowerCase();
|
|
428
|
+
const icons = {
|
|
429
|
+
'.pdf': '📄', '.doc': '📄', '.docx': '📄', '.txt': '📄',
|
|
430
|
+
'.js': '📜', '.ts': '📜', '.py': '📜', '.java': '📜', '.cpp': '📜', '.c': '📜',
|
|
431
|
+
'.html': '🌐', '.css': '🎨', '.scss': '🎨', '.less': '🎨',
|
|
432
|
+
'.json': '⚙️', '.xml': '⚙️', '.yml': '⚙️', '.yaml': '⚙️',
|
|
433
|
+
'.zip': '📦', '.rar': '📦', '.7z': '📦', '.tar': '📦',
|
|
434
|
+
'.jpg': '🖼️', '.jpeg': '🖼️', '.png': '🖼️', '.gif': '🖼️', '.svg': '🖼️',
|
|
435
|
+
'.mp4': '🎬', '.avi': '🎬', '.mov': '🎬', '.mkv': '🎬',
|
|
436
|
+
'.mp3': '🎵', '.wav': '🎵', '.flac': '🎵'
|
|
437
|
+
};
|
|
438
|
+
return icons[ext] || '📋';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Helper function to display current page
|
|
442
|
+
function displayPage() {
|
|
443
|
+
console.clear();
|
|
444
|
+
console.log(chalk.cyan.bold('╭' + '─'.repeat(78) + '╮'));
|
|
445
|
+
console.log(chalk.cyan.bold('│') + chalk.white.bold(' Interactive File Selector'.padEnd(76)) + chalk.cyan.bold('│'));
|
|
446
|
+
console.log(chalk.cyan.bold('╰' + '─'.repeat(78) + '╯'));
|
|
447
|
+
|
|
448
|
+
// Display current path
|
|
449
|
+
const relativePath = path.relative(currentDir, currentPath) || '.';
|
|
450
|
+
console.log(chalk.yellow('📂 Current folder: ') + chalk.white.bold(relativePath));
|
|
451
|
+
|
|
452
|
+
// Display selected files count
|
|
453
|
+
if (selectedFiles.length > 0) {
|
|
454
|
+
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
455
|
+
try {
|
|
456
|
+
return sum + fs.statSync(file).size;
|
|
457
|
+
} catch {
|
|
458
|
+
return sum;
|
|
459
|
+
}
|
|
460
|
+
}, 0);
|
|
461
|
+
console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB)`));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log();
|
|
465
|
+
|
|
466
|
+
if (fileList.length === 0) {
|
|
467
|
+
console.log(chalk.yellow('📭 No files found in this directory.'));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const startIdx = currentPage * itemsPerPage;
|
|
472
|
+
const endIdx = Math.min(startIdx + itemsPerPage, fileList.length);
|
|
473
|
+
const pageItems = fileList.slice(startIdx, endIdx);
|
|
474
|
+
|
|
475
|
+
// Display page header
|
|
476
|
+
console.log(chalk.cyan(`📋 Showing ${startIdx + 1}-${endIdx} of ${fileList.length} items (Page ${currentPage + 1}/${Math.ceil(fileList.length / itemsPerPage)})`));
|
|
477
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
478
|
+
|
|
479
|
+
// Display items
|
|
480
|
+
pageItems.forEach((item, index) => {
|
|
481
|
+
const globalIndex = startIdx + index + 1;
|
|
482
|
+
const isSelected = selectedFiles.includes(item.path);
|
|
483
|
+
const prefix = isSelected ? chalk.green('✓') : chalk.white(globalIndex.toString().padStart(2));
|
|
484
|
+
const sizeDisplay = item.type === 'file' ? chalk.gray(`(${(item.size / 1024).toFixed(1)} KB)`) : '';
|
|
485
|
+
|
|
486
|
+
if (isSelected) {
|
|
487
|
+
console.log(`${prefix} ${chalk.green(item.name)} ${sizeDisplay}`);
|
|
488
|
+
} else if (item.type === 'parent') {
|
|
489
|
+
console.log(`${prefix} ${chalk.blue(item.name)}`);
|
|
490
|
+
} else if (item.type === 'directory') {
|
|
491
|
+
console.log(`${prefix} ${chalk.cyan(item.name)}`);
|
|
492
|
+
} else {
|
|
493
|
+
console.log(`${prefix} ${chalk.white(item.name)} ${sizeDisplay}`);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
498
|
+
displayCommands();
|
|
499
|
+
}
|
|
500
|
+
// Helper function to display available commands
|
|
501
|
+
function displayCommands() {
|
|
502
|
+
console.log(chalk.yellow.bold('Commands:'));
|
|
503
|
+
console.log(chalk.white(' 1-9, 10+ ') + chalk.gray('Select/deselect file by number'));
|
|
504
|
+
console.log(chalk.white(' n/next/→ ') + chalk.gray('Next page'));
|
|
505
|
+
console.log(chalk.white(' p/prev/← ') + chalk.gray('Previous page'));
|
|
506
|
+
console.log(chalk.white(' a/all ') + chalk.gray('Select all files on current page'));
|
|
507
|
+
console.log(chalk.white(' c/clear ') + chalk.gray('Clear all selections'));
|
|
508
|
+
console.log(chalk.white(' r/remove ') + chalk.gray('Remove specific file from selection'));
|
|
509
|
+
console.log(chalk.white(' s/search ') + chalk.gray('Search files by name or extension'));
|
|
510
|
+
console.log(chalk.white(' f/filter ') + chalk.gray('Filter by file extension'));
|
|
511
|
+
console.log(chalk.white(' h/help ') + chalk.gray('Show detailed help'));
|
|
512
|
+
console.log(chalk.white(' q/quit ') + chalk.gray('Finish selection'));
|
|
513
|
+
console.log();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Main interaction loop
|
|
517
|
+
while (true) {
|
|
518
|
+
refreshFileList();
|
|
519
|
+
displayPage();
|
|
520
|
+
|
|
521
|
+
const input = await askQuestion(rl, chalk.cyan.bold('Enter command: '));
|
|
522
|
+
const cmd = input.trim().toLowerCase();
|
|
523
|
+
|
|
524
|
+
if (!cmd || cmd === 'q' || cmd === 'quit') {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
// Handle navigation
|
|
528
|
+
if (cmd === 'n' || cmd === 'next' || cmd === '→') {
|
|
529
|
+
if ((currentPage + 1) * itemsPerPage < fileList.length) {
|
|
530
|
+
currentPage++;
|
|
531
|
+
} else {
|
|
532
|
+
console.log(chalk.yellow('Already on last page!'));
|
|
533
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (cmd === 'p' || cmd === 'prev' || cmd === '←') {
|
|
539
|
+
if (currentPage > 0) {
|
|
540
|
+
currentPage--;
|
|
541
|
+
} else {
|
|
542
|
+
console.log(chalk.yellow('Already on first page!'));
|
|
543
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
544
|
+
}
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Handle help
|
|
549
|
+
if (cmd === 'h' || cmd === 'help') {
|
|
550
|
+
console.clear();
|
|
551
|
+
console.log(chalk.cyan.bold('╭' + '─'.repeat(78) + '╮'));
|
|
552
|
+
console.log(chalk.cyan.bold('│') + chalk.white.bold(' Interactive File Selector - Help'.padEnd(76)) + chalk.cyan.bold('│'));
|
|
553
|
+
console.log(chalk.cyan.bold('╰' + '─'.repeat(78) + '╯'));
|
|
554
|
+
console.log();
|
|
555
|
+
console.log(chalk.yellow.bold('📋 File Selection:'));
|
|
556
|
+
console.log(chalk.white(' • Type a number (1-9999) to toggle file selection'));
|
|
557
|
+
console.log(chalk.white(' • Files show with checkmarks (✓) when selected'));
|
|
558
|
+
console.log(chalk.white(' • Directories and parent folders can be navigated into'));
|
|
559
|
+
console.log();
|
|
560
|
+
console.log(chalk.yellow.bold('📁 Navigation:'));
|
|
561
|
+
console.log(chalk.white(' • Use n/next/→ to go to next page'));
|
|
562
|
+
console.log(chalk.white(' • Use p/prev/← to go to previous page'));
|
|
563
|
+
console.log(chalk.white(' • Click on folder numbers to enter directories'));
|
|
564
|
+
console.log(chalk.white(' • Use .. (Parent Directory) to go up one level'));
|
|
565
|
+
console.log();
|
|
566
|
+
console.log(chalk.yellow.bold('🔍 Advanced Features:'));
|
|
567
|
+
console.log(chalk.white(' • s/search: Find files by name or extension'));
|
|
568
|
+
console.log(chalk.white(' • f/filter: Filter and select by file extension'));
|
|
569
|
+
console.log(chalk.white(' • a/all: Select all files on current page'));
|
|
570
|
+
console.log(chalk.white(' • c/clear: Remove all selections'));
|
|
571
|
+
console.log(chalk.white(' • r/remove: Remove specific files from selection'));
|
|
572
|
+
console.log();
|
|
573
|
+
console.log(chalk.yellow.bold('💡 Tips:'));
|
|
574
|
+
console.log(chalk.white(' • File sizes are shown in KB'));
|
|
575
|
+
console.log(chalk.white(' • Your current selection count and size is displayed at top'));
|
|
576
|
+
console.log(chalk.white(' • Use search for large directories to find files quickly'));
|
|
577
|
+
console.log(chalk.white(' • Filter by extension (e.g., .pdf, .docx) for specific file types'));
|
|
578
|
+
console.log();
|
|
579
|
+
console.log(chalk.green('Press Enter to continue...'));
|
|
580
|
+
await askQuestion(rl, '');
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Handle selection commands
|
|
585
|
+
if (cmd === 'a' || cmd === 'all') {
|
|
586
|
+
const startIdx = currentPage * itemsPerPage;
|
|
587
|
+
const endIdx = Math.min(startIdx + itemsPerPage, fileList.length);
|
|
588
|
+
let addedCount = 0;
|
|
589
|
+
|
|
590
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
591
|
+
const item = fileList[i];
|
|
592
|
+
if (item.type === 'file' && !selectedFiles.includes(item.path)) {
|
|
593
|
+
selectedFiles.push(item.path);
|
|
594
|
+
addedCount++;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
console.log(chalk.green(`Added ${addedCount} files to selection!`));
|
|
599
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (cmd === 'c' || cmd === 'clear') {
|
|
604
|
+
const count = selectedFiles.length;
|
|
605
|
+
selectedFiles.length = 0;
|
|
606
|
+
console.log(chalk.yellow(`Cleared ${count} files from selection!`));
|
|
607
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (cmd === 'r' || cmd === 'remove') {
|
|
612
|
+
if (selectedFiles.length === 0) {
|
|
613
|
+
console.log(chalk.red('No files selected to remove!'));
|
|
614
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
console.log(chalk.cyan('\nSelected files:'));
|
|
619
|
+
selectedFiles.forEach((file, index) => {
|
|
620
|
+
console.log(`${index + 1}. ${path.basename(file)}`);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const removeInput = await askQuestion(rl, chalk.cyan('Enter number to remove (or press Enter to cancel): '));
|
|
624
|
+
const removeIndex = parseInt(removeInput) - 1;
|
|
625
|
+
|
|
626
|
+
if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
|
|
627
|
+
const removed = selectedFiles.splice(removeIndex, 1)[0];
|
|
628
|
+
console.log(chalk.green(`Removed: ${path.basename(removed)}`));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (cmd === 's' || cmd === 'search') {
|
|
636
|
+
const searchTerm = await askQuestion(rl, chalk.cyan('Enter search term (filename or extension): '));
|
|
637
|
+
if (searchTerm) {
|
|
638
|
+
const matches = fileList.filter(item =>
|
|
639
|
+
item.type === 'file' &&
|
|
640
|
+
(item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
641
|
+
path.extname(item.path).toLowerCase() === searchTerm.toLowerCase())
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
if (matches.length > 0) {
|
|
645
|
+
console.log(chalk.cyan(`\nFound ${matches.length} matches:`));
|
|
646
|
+
matches.forEach((item, index) => {
|
|
647
|
+
const isSelected = selectedFiles.includes(item.path);
|
|
648
|
+
const prefix = isSelected ? chalk.green('✓') : chalk.white((index + 1).toString());
|
|
649
|
+
console.log(`${prefix} ${item.name} ${chalk.gray(`(${(item.size / 1024).toFixed(1)} KB)`)}`);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const selectAll = await askConfirmation(rl, chalk.cyan('Select all search results?'), false);
|
|
653
|
+
if (selectAll) {
|
|
654
|
+
matches.forEach(item => {
|
|
655
|
+
if (!selectedFiles.includes(item.path)) {
|
|
656
|
+
selectedFiles.push(item.path);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
console.log(chalk.green(`Added ${matches.length} files!`));
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
console.log(chalk.yellow('No matches found.'));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
666
|
+
}
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (cmd === 'f' || cmd === 'filter') {
|
|
671
|
+
const allExtensions = [...new Set(
|
|
672
|
+
fileList
|
|
673
|
+
.filter(item => item.type === 'file')
|
|
674
|
+
.map(item => path.extname(item.path).toLowerCase())
|
|
675
|
+
.filter(ext => ext)
|
|
676
|
+
)].sort();
|
|
677
|
+
|
|
678
|
+
if (allExtensions.length === 0) {
|
|
679
|
+
console.log(chalk.yellow('No file extensions found!'));
|
|
680
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
console.log(chalk.cyan('\nAvailable extensions:'));
|
|
685
|
+
allExtensions.forEach((ext, index) => {
|
|
686
|
+
const count = fileList.filter(item =>
|
|
687
|
+
item.type === 'file' && path.extname(item.path).toLowerCase() === ext
|
|
688
|
+
).length;
|
|
689
|
+
console.log(`${index + 1}. ${ext} (${count} files)`);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const extChoice = await askQuestion(rl, chalk.cyan('Enter extension number (or press Enter to cancel): '));
|
|
693
|
+
const extIndex = parseInt(extChoice) - 1;
|
|
694
|
+
|
|
695
|
+
if (extIndex >= 0 && extIndex < allExtensions.length) {
|
|
696
|
+
const selectedExt = allExtensions[extIndex];
|
|
697
|
+
const matches = fileList.filter(item =>
|
|
698
|
+
item.type === 'file' && path.extname(item.path).toLowerCase() === selectedExt
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
const selectAll = await askConfirmation(rl, chalk.cyan(`Select all ${selectedExt} files?`), false);
|
|
702
|
+
if (selectAll) {
|
|
703
|
+
matches.forEach(item => {
|
|
704
|
+
if (!selectedFiles.includes(item.path)) {
|
|
705
|
+
selectedFiles.push(item.path);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
console.log(chalk.green(`Added ${matches.length} ${selectedExt} files!`));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Handle numeric selection
|
|
717
|
+
const num = parseInt(cmd);
|
|
718
|
+
if (!isNaN(num) && num > 0 && num <= fileList.length) {
|
|
719
|
+
const item = fileList[num - 1];
|
|
720
|
+
|
|
721
|
+
if (item.type === 'parent') {
|
|
722
|
+
currentPath = item.path;
|
|
723
|
+
currentPage = 0;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (item.type === 'directory') {
|
|
728
|
+
currentPath = item.path;
|
|
729
|
+
currentPage = 0;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (item.type === 'file') {
|
|
733
|
+
const index = selectedFiles.indexOf(item.path);
|
|
734
|
+
if (index === -1) {
|
|
735
|
+
selectedFiles.push(item.path);
|
|
736
|
+
const size = (item.size / 1024).toFixed(1);
|
|
737
|
+
console.log(chalk.green(`✓ Added: ${path.basename(item.path)} (${size} KB)`));
|
|
738
|
+
} else {
|
|
739
|
+
selectedFiles.splice(index, 1);
|
|
740
|
+
console.log(chalk.yellow(`✗ Removed: ${path.basename(item.path)}`));
|
|
741
|
+
}
|
|
742
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
console.log(chalk.red('Invalid command!'));
|
|
747
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Display completion summary
|
|
751
|
+
if (selectedFiles.length > 0) {
|
|
752
|
+
console.clear();
|
|
753
|
+
console.log(chalk.green.bold('✅ File Selection Complete!'));
|
|
754
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
755
|
+
|
|
756
|
+
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
757
|
+
try {
|
|
758
|
+
return sum + fs.statSync(file).size;
|
|
759
|
+
} catch {
|
|
760
|
+
return sum;
|
|
761
|
+
}
|
|
762
|
+
}, 0);
|
|
763
|
+
|
|
764
|
+
console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
|
|
765
|
+
selectedFiles.forEach((file, index) => {
|
|
766
|
+
const stats = fs.statSync(file);
|
|
767
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
768
|
+
console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
769
|
+
});
|
|
770
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
771
|
+
} else {
|
|
772
|
+
console.log(chalk.yellow('No files selected.'));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return selectedFiles;
|
|
776
|
+
}
|
|
362
777
|
|
|
363
778
|
export {
|
|
364
779
|
createReadlineInterface,
|
|
@@ -367,6 +782,7 @@ export {
|
|
|
367
782
|
askConfirmation,
|
|
368
783
|
selectFromList,
|
|
369
784
|
selectFilesImproved,
|
|
785
|
+
selectFilesInteractive,
|
|
370
786
|
getFilesMatchingWildcard,
|
|
371
787
|
getSubfoldersRecursive,
|
|
372
788
|
pad
|