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.
@@ -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
- filePaths = await selectFilesImproved(rl);
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) {
@@ -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 = require('path').basename(filePath);
21
+ const fileName = path.basename(filePath);
21
22
  const fileContent = fs.readFileSync(filePath);
22
23
 
23
24
  // Step 1: Get upload URL from Canvas
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvaslms-cli",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "A command line tool for interacting with Canvas LMS API",
5
5
  "keywords": [
6
6
  "canvas",