canvaslms-cli 1.1.1 → 1.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/CHANGELOG.md CHANGED
@@ -5,10 +5,72 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [1.1.0] - 2025-07-03
8
+ ## [1.2.0] - 2025-07-05
9
+
10
+ ### Added
11
+
12
+ - **Enhanced File Selection UX**: Implemented continuous file selection until empty input
13
+ - Browse current directory with file size display
14
+ - Add multiple files one by one
15
+ - Remove files from selection
16
+ - Show currently selected files
17
+ - Smart file filtering (excludes hidden files, package files)
18
+ - **Wildcard support**: Use patterns like *.html, *.js, *.pdf to select multiple files
19
+ - File type icons for better visual identification
20
+
21
+ - **Improved Grade Viewing**:
22
+ - Interactive course selection for grade viewing
23
+ - Assignment-level grade details with color coding
24
+ - Overall course grade summary
25
+ - Better grade formatting and status indicators
26
+ - Support for letter grades, excused, and missing assignments
27
+
28
+ - **Enhanced Display Names**:
29
+ - Show course names instead of IDs in all commands
30
+ - Display assignment names prominently
31
+ - Better labeling of IDs vs names throughout interface
32
+
33
+ - **Interactive Utilities**:
34
+ - Added validation and retry logic for user input
35
+ - Confirmation helpers with default values
36
+ - List selection utilities with cancel option
37
+
38
+ ### Improved
39
+
40
+ - **Submit Command**: Complete redesign with better file selection workflow
41
+ - **Grades Command**: Interactive course selection and detailed assignment grades
42
+ - **Assignments Command**: Display course names prominently
43
+ - **Announcements Command**: Show course names instead of IDs
44
+ - **User Experience**: More consistent and intuitive interfaces across all commands
45
+
46
+ ### Fixed
47
+
48
+ - **Assignment Name Display**: Fixed "Unknown Assignment" issue in submission summary
49
+ - **File Selection Flow**: Better error handling and user guidance during file selection
50
+ - **Variable Scope**: Proper assignment variable handling throughout submission process
51
+
52
+ ### Technical
53
+
54
+ - Enhanced interactive utilities in `lib/interactive.js`
55
+ - Better error handling and user guidance
56
+ - Improved code organization and modularity
57
+
58
+ ## [1.1.1] - 2025-07-03
59
+
60
+ ### Fixed
61
+
62
+ - Removed dotenv dependency that was causing module not found errors
63
+ - Fixed configuration file path to use `.canvaslms-cli-config.json`
64
+ - Resolved package publishing and global installation issues
9
65
 
10
66
  ### Added
11
67
 
68
+ - Dual binary support: both `canvaslms-cli` and `canvas` commands work
69
+
70
+ ## [1.1.0] - 2025-07-03
71
+
72
+ ### Major Changes
73
+
12
74
  - Home directory configuration system (~/.canvaslms-cli-config.json)
13
75
  - Interactive configuration setup wizard (`canvas config setup`)
14
76
  - Configuration management subcommands:
@@ -7,6 +7,9 @@ const { makeCanvasRequest } = require('../lib/api-client');
7
7
  async function showAnnouncements(courseId, options) {
8
8
  try {
9
9
  if (courseId) {
10
+ // Get course information first
11
+ const course = await makeCanvasRequest('get', `courses/${courseId}`);
12
+
10
13
  // Get announcements for specific course
11
14
  const announcements = await makeCanvasRequest('get', `courses/${courseId}/discussion_topics`, [
12
15
  'only_announcements=true',
@@ -14,11 +17,11 @@ async function showAnnouncements(courseId, options) {
14
17
  ]);
15
18
 
16
19
  if (!announcements || announcements.length === 0) {
17
- console.log('No announcements found for this course.');
20
+ console.log(`No announcements found for course: ${course.name}`);
18
21
  return;
19
22
  }
20
23
 
21
- console.log(`Recent ${announcements.length} announcement(s) for course ${courseId}:\n`);
24
+ console.log(`šŸ“¢ Recent ${announcements.length} announcement(s) for: ${course.name}\n`);
22
25
 
23
26
  announcements.forEach((announcement, index) => {
24
27
  console.log(`${index + 1}. ${announcement.title}`);
@@ -6,12 +6,15 @@ const { makeCanvasRequest } = require('../lib/api-client');
6
6
 
7
7
  async function listAssignments(courseId, options) {
8
8
  try {
9
+ // First get course information to display course name
10
+ const course = await makeCanvasRequest('get', `courses/${courseId}`);
11
+
9
12
  const queryParams = ['include[]=submission', 'include[]=score_statistics', 'per_page=100'];
10
13
 
11
14
  const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, queryParams);
12
15
 
13
16
  if (!assignments || assignments.length === 0) {
14
- console.log('No assignments found for this course.');
17
+ console.log(`No assignments found for course: ${course.name}`);
15
18
  return;
16
19
  }
17
20
 
@@ -23,7 +26,9 @@ async function listAssignments(courseId, options) {
23
26
  filteredAssignments = assignments.filter(a => !a.submission || !a.submission.submitted_at);
24
27
  }
25
28
 
26
- console.log(`Found ${filteredAssignments.length} assignment(s):\n`);
29
+ // Display course information prominently
30
+ console.log(`šŸ“š Course: ${course.name}`);
31
+ console.log(`šŸ“ Found ${filteredAssignments.length} assignment(s):\n`);
27
32
 
28
33
  filteredAssignments.forEach((assignment, index) => {
29
34
  const submission = assignment.submission;
@@ -67,9 +72,8 @@ async function listAssignments(courseId, options) {
67
72
  if (submission && submission.grade && isNaN(submission.grade)) {
68
73
  gradeDisplay = submission.grade + (assignment.points_possible ? ` (${gradeDisplay})` : '');
69
74
  }
70
-
71
- console.log(`${index + 1}. ${submissionStatus} ${assignment.name}`);
72
- console.log(` ID: ${assignment.id}`);
75
+ console.log(`${index + 1}. ${submissionStatus} ${assignment.name}`);
76
+ console.log(` Assignment ID: ${assignment.id}`);
73
77
  console.log(` Grade: ${gradeColor}${gradeDisplay} pts\x1b[0m`);
74
78
  console.log(` Due: ${assignment.due_at ? new Date(assignment.due_at).toLocaleString() : 'No due date'}`);
75
79
 
@@ -3,63 +3,200 @@
3
3
  */
4
4
 
5
5
  const { makeCanvasRequest } = require('../lib/api-client');
6
+ const { createReadlineInterface, askQuestion } = require('../lib/interactive');
6
7
 
7
8
  async function showGrades(courseId, options) {
9
+ const rl = createReadlineInterface();
10
+
8
11
  try {
9
12
  if (courseId) {
10
- // Get grades for specific course
11
- const enrollments = await makeCanvasRequest('get', `courses/${courseId}/enrollments`, ['user_id=self', 'include[]=grades']);
12
-
13
- if (!enrollments || enrollments.length === 0) {
14
- console.log('No enrollment found for this course.');
15
- return;
16
- }
17
-
18
- const enrollment = enrollments[0];
19
- console.log(`Grades for course: ${enrollment.course_id}`);
20
- console.log(`Current Score: ${enrollment.grades?.current_score || 'N/A'}%`);
21
- console.log(`Final Score: ${enrollment.grades?.final_score || 'N/A'}%`);
22
- console.log(`Current Grade: ${enrollment.grades?.current_grade || 'N/A'}`);
23
- console.log(`Final Grade: ${enrollment.grades?.final_grade || 'N/A'}`);
13
+ // Get grades for specific course with assignment details
14
+ await showCourseGrades(courseId, options, rl);
24
15
  } else {
25
- // Get grades for all courses
26
- const courses = await makeCanvasRequest('get', 'courses', [
27
- 'enrollment_state=active',
28
- 'include[]=enrollments',
29
- 'include[]=total_scores'
30
- ]);
16
+ // Interactive course selection for grades
17
+ await showInteractiveGrades(options, rl);
18
+ }
19
+
20
+ } catch (error) {
21
+ console.error('Error fetching grades:', error.message);
22
+ process.exit(1);
23
+ } finally {
24
+ rl.close();
25
+ }
26
+ }
27
+
28
+ async function showCourseGrades(courseId, options, rl) {
29
+ // Get course details
30
+ const course = await makeCanvasRequest('get', `courses/${courseId}`);
31
+
32
+ // Get overall enrollment grades
33
+ const enrollments = await makeCanvasRequest('get', `courses/${courseId}/enrollments`, [
34
+ 'user_id=self',
35
+ 'include[]=grades'
36
+ ]);
37
+
38
+ if (!enrollments || enrollments.length === 0) {
39
+ console.log('No enrollment found for this course.');
40
+ return;
41
+ }
42
+
43
+ const enrollment = enrollments[0];
44
+
45
+ console.log(`\nšŸ“š Grades for: ${course.name}`);
46
+ console.log(`šŸ“Š Course Overview:`);
47
+ console.log(` Current Score: ${enrollment.grades?.current_score || 'N/A'}%`);
48
+ console.log(` Final Score: ${enrollment.grades?.final_score || 'N/A'}%`);
49
+ console.log(` Current Grade: ${enrollment.grades?.current_grade || 'N/A'}`);
50
+ console.log(` Final Grade: ${enrollment.grades?.final_grade || 'N/A'}`);
51
+
52
+ // Get assignment grades
53
+ console.log('\nšŸ“ Loading assignment grades...');
54
+ const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
55
+ 'include[]=submission',
56
+ 'order_by=due_at',
57
+ 'per_page=100'
58
+ ]);
59
+
60
+ if (!assignments || assignments.length === 0) {
61
+ console.log('No assignments found for this course.');
62
+ return;
63
+ }
64
+
65
+ // Filter assignments with grades
66
+ const gradedAssignments = assignments.filter(assignment => {
67
+ const submission = assignment.submission;
68
+ return submission && (
69
+ submission.score !== null ||
70
+ submission.excused ||
71
+ submission.missing ||
72
+ submission.grade
73
+ );
74
+ });
75
+
76
+ if (gradedAssignments.length === 0) {
77
+ console.log('No graded assignments found.');
78
+ return;
79
+ }
80
+
81
+ console.log(`\nšŸ“‹ Assignment Grades (${gradedAssignments.length} graded):`);
82
+ gradedAssignments.forEach((assignment, index) => {
83
+ const submission = assignment.submission;
84
+ let gradeDisplay = '';
85
+ let gradeColor = '';
86
+
87
+ if (submission.score !== null && submission.score !== undefined) {
88
+ const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
89
+ const total = assignment.points_possible || 0;
90
+ gradeDisplay = `${score}/${total} pts`;
31
91
 
32
- if (!courses || courses.length === 0) {
33
- console.log('No courses found.');
34
- return;
92
+ // Color coding based on percentage
93
+ if (total > 0) {
94
+ const percentage = (submission.score / total) * 100;
95
+ if (percentage >= 90) gradeColor = '\x1b[32m'; // Green
96
+ else if (percentage >= 80) gradeColor = '\x1b[36m'; // Cyan
97
+ else if (percentage >= 70) gradeColor = '\x1b[33m'; // Yellow
98
+ else if (percentage >= 60) gradeColor = '\x1b[35m'; // Magenta
99
+ else gradeColor = '\x1b[31m'; // Red
35
100
  }
36
101
 
37
- console.log('Grades Summary:\n');
38
-
39
- courses.forEach((course, index) => {
40
- console.log(`${index + 1}. ${course.name}`);
41
- console.log(` ID: ${course.id}`);
102
+ // Add letter grade if available
103
+ if (submission.grade && isNaN(submission.grade)) {
104
+ gradeDisplay = `${submission.grade} (${gradeDisplay})`;
105
+ }
106
+ } else if (submission.excused) {
107
+ gradeDisplay = 'Excused';
108
+ gradeColor = '\x1b[34m'; // Blue
109
+ } else if (submission.missing) {
110
+ gradeDisplay = 'Missing';
111
+ gradeColor = '\x1b[31m'; // Red
112
+ }
113
+
114
+ console.log(`${index + 1}. ${assignment.name}`);
115
+ console.log(` Grade: ${gradeColor}${gradeDisplay}\x1b[0m`);
116
+
117
+ if (submission.submitted_at) {
118
+ console.log(` Submitted: ${new Date(submission.submitted_at).toLocaleDateString()}`);
119
+ }
120
+
121
+ if (assignment.due_at) {
122
+ console.log(` Due: ${new Date(assignment.due_at).toLocaleDateString()}`);
123
+ }
124
+
125
+ if (options.verbose && submission.grader_comments) {
126
+ console.log(` Comments: ${submission.grader_comments}`);
127
+ }
128
+
129
+ console.log('');
130
+ });
131
+ }
132
+
133
+ async function showInteractiveGrades(options, rl) {
134
+ // Get all courses with enrollment information
135
+ const courses = await makeCanvasRequest('get', 'courses', [
136
+ 'enrollment_state=active',
137
+ 'include[]=enrollments',
138
+ 'include[]=favorites',
139
+ 'include[]=total_scores'
140
+ ]);
141
+
142
+ if (!courses || courses.length === 0) {
143
+ console.log('No courses found.');
144
+ return;
145
+ }
146
+
147
+ // Show courses overview first
148
+ console.log('šŸ“Š Grades Overview:\n');
149
+
150
+ const coursesWithGrades = courses.filter(course =>
151
+ course.enrollments && course.enrollments.length > 0
152
+ );
153
+
154
+ console.log(`Found ${coursesWithGrades.length} enrolled course(s):\n`);
155
+
156
+ coursesWithGrades.forEach((course, index) => {
157
+ const starIcon = course.is_favorite ? '⭐ ' : '';
158
+ console.log(`${index + 1}. ${starIcon}${course.name}`);
159
+
160
+ if (course.enrollments && course.enrollments.length > 0) {
161
+ const enrollment = course.enrollments[0];
162
+ if (enrollment.grades) {
163
+ const currentScore = enrollment.grades.current_score;
164
+ const currentGrade = enrollment.grades.current_grade;
42
165
 
43
- if (course.enrollments && course.enrollments.length > 0) {
44
- const enrollment = course.enrollments[0];
45
- if (enrollment.grades) {
46
- console.log(` Current Score: ${enrollment.grades.current_score || 'N/A'}%`);
47
- console.log(` Final Score: ${enrollment.grades.final_score || 'N/A'}%`);
48
- console.log(` Current Grade: ${enrollment.grades.current_grade || 'N/A'}`);
49
- } else {
50
- console.log(` Grades: Not available`);
51
- }
52
- } else {
53
- console.log(` Enrollment: Not found`);
166
+ // Color code the grade
167
+ let gradeColor = '\x1b[90m'; // Gray default
168
+ if (currentScore !== null && currentScore !== undefined) {
169
+ if (currentScore >= 90) gradeColor = '\x1b[32m'; // Green
170
+ else if (currentScore >= 80) gradeColor = '\x1b[36m'; // Cyan
171
+ else if (currentScore >= 70) gradeColor = '\x1b[33m'; // Yellow
172
+ else if (currentScore >= 60) gradeColor = '\x1b[35m'; // Magenta
173
+ else gradeColor = '\x1b[31m'; // Red
54
174
  }
55
175
 
56
- console.log('');
57
- });
176
+ console.log(` Current: ${gradeColor}${currentScore || 'N/A'}% (${currentGrade || 'N/A'})\x1b[0m`);
177
+ } else {
178
+ console.log(` Current: \x1b[90mGrades not available\x1b[0m`);
179
+ }
58
180
  }
59
-
60
- } catch (error) {
61
- console.error('Error fetching grades:', error.message);
62
- process.exit(1);
181
+ console.log('');
182
+ });
183
+
184
+ // Ask if user wants to see detailed grades for a specific course
185
+ const viewDetailed = await askQuestion(rl, 'View detailed grades for a specific course? (Y/n): ');
186
+
187
+ if (viewDetailed.toLowerCase() === 'n' || viewDetailed.toLowerCase() === 'no') {
188
+ return;
189
+ }
190
+
191
+ const courseChoice = await askQuestion(rl, '\nEnter course number for detailed grades: ');
192
+ const courseIndex = parseInt(courseChoice) - 1;
193
+
194
+ if (courseIndex >= 0 && courseIndex < coursesWithGrades.length) {
195
+ const selectedCourse = coursesWithGrades[courseIndex];
196
+ console.log(`\nāœ… Selected: ${selectedCourse.name}`);
197
+ await showCourseGrades(selectedCourse.id, options, rl);
198
+ } else {
199
+ console.log('Invalid course selection.');
63
200
  }
64
201
  }
65
202
 
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { makeCanvasRequest } = require('../lib/api-client');
8
- const { createReadlineInterface, askQuestion } = require('../lib/interactive');
8
+ const { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved } = require('../lib/interactive');
9
9
  const { uploadSingleFileToCanvas, submitAssignmentWithFiles } = require('../lib/file-upload');
10
10
 
11
11
  async function submitAssignment(options) {
@@ -15,6 +15,8 @@ async function submitAssignment(options) {
15
15
  let courseId = options.course;
16
16
  let assignmentId = options.assignment;
17
17
  let filePath = options.file;
18
+ let selectedCourse = null;
19
+ let selectedAssignment = null;
18
20
 
19
21
  // Step 1: Select Course (if not provided)
20
22
  if (!courseId) {
@@ -42,7 +44,7 @@ async function submitAssignment(options) {
42
44
  console.log('Select a course:');
43
45
  starredCourses.forEach((course, index) => {
44
46
  const starIcon = course.is_favorite ? '⭐ ' : '';
45
- console.log(`${index + 1}. ${starIcon}${course.name} (ID: ${course.id})`);
47
+ console.log(`${index + 1}. ${starIcon}${course.name}`);
46
48
  });
47
49
 
48
50
  const courseChoice = await askQuestion(rl, '\nEnter course number: ');
@@ -54,8 +56,17 @@ async function submitAssignment(options) {
54
56
  return;
55
57
  }
56
58
 
57
- courseId = starredCourses[courseIndex].id;
58
- console.log(`Selected: ${starredCourses[courseIndex].name}\n`);
59
+ selectedCourse = starredCourses[courseIndex];
60
+ courseId = selectedCourse.id;
61
+ console.log(`āœ… Selected: ${selectedCourse.name}\n`);
62
+ } else {
63
+ // Fetch course details if ID was provided
64
+ try {
65
+ selectedCourse = await makeCanvasRequest('get', `courses/${courseId}`);
66
+ } catch (error) {
67
+ console.log(`āš ļø Could not fetch course details for ID ${courseId}`);
68
+ selectedCourse = { id: courseId, name: `Course ${courseId}` };
69
+ }
59
70
  }
60
71
 
61
72
  // Step 2: Select Assignment (if not provided)
@@ -119,9 +130,7 @@ async function submitAssignment(options) {
119
130
  console.log(` šŸ“‹ Note: This assignment doesn't accept file uploads`);
120
131
  }
121
132
  });
122
-
123
- const assignmentChoice = await askQuestion(rl, '\nEnter assignment number: ');
124
- const assignmentIndex = parseInt(assignmentChoice) - 1;
133
+ const assignmentIndex = parseInt(assignmentChoice) - 1;
125
134
 
126
135
  if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
127
136
  console.log('Invalid assignment selection.');
@@ -129,7 +138,7 @@ async function submitAssignment(options) {
129
138
  return;
130
139
  }
131
140
 
132
- const selectedAssignment = assignments[assignmentIndex];
141
+ selectedAssignment = assignments[assignmentIndex];
133
142
 
134
143
  // Check if assignment accepts file uploads
135
144
  if (!selectedAssignment.submission_types ||
@@ -141,25 +150,36 @@ async function submitAssignment(options) {
141
150
  }
142
151
 
143
152
  assignmentId = selectedAssignment.id;
144
- console.log(`Selected: ${selectedAssignment.name}\n`);
153
+ console.log(`āœ… Selected: ${selectedAssignment.name}\n`);
145
154
 
146
155
  // Check if already submitted
147
156
  if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
148
- const resubmit = await askQuestion(rl, 'This assignment has already been submitted. Do you want to resubmit? (y/N): ');
149
- if (resubmit.toLowerCase() !== 'y' && resubmit.toLowerCase() !== 'yes') {
157
+ const resubmit = await askConfirmation(rl, 'This assignment has already been submitted. Do you want to resubmit?', false);
158
+ if (!resubmit) {
150
159
  console.log('Submission cancelled.');
151
160
  rl.close();
152
161
  return;
153
162
  }
154
163
  }
155
- }
156
-
157
- // Step 3: Select Files (if not provided)
164
+ } else {
165
+ // Fetch assignment details if ID was provided
166
+ try {
167
+ selectedAssignment = await makeCanvasRequest('get', `courses/${courseId}/assignments/${assignmentId}`);
168
+ console.log(`āœ… Using assignment: ${selectedAssignment.name}\n`);
169
+ } catch (error) {
170
+ console.log(`āš ļø Could not fetch assignment details for ID ${assignmentId}`);
171
+ selectedAssignment = { id: assignmentId, name: `Assignment ${assignmentId}` };
172
+ }
173
+ } // Step 3: Select Files (if not provided)
158
174
  let filePaths = [];
159
175
  if (filePath) {
160
176
  filePaths = [filePath]; // Single file provided via option
161
177
  } else {
162
- filePaths = await selectFiles(rl);
178
+ console.log('\nšŸ“ File Selection');
179
+ console.log(`šŸ“š Course: ${selectedCourse.name}`);
180
+ console.log(`šŸ“ Assignment: ${selectedAssignment.name}\n`);
181
+
182
+ filePaths = await selectFilesImproved(rl);
163
183
  }
164
184
 
165
185
  // Validate all selected files exist
@@ -179,20 +199,20 @@ async function submitAssignment(options) {
179
199
  }
180
200
 
181
201
  filePaths = validFiles;
182
-
202
+
183
203
  // Step 4: Confirm and Submit
184
204
  console.log('\nšŸ“‹ Submission Summary:');
185
- console.log(`Course ID: ${courseId}`);
186
- console.log(`Assignment ID: ${assignmentId}`);
187
- console.log(`Files (${filePaths.length}):`);
205
+ console.log(`šŸ“š Course: ${selectedCourse?.name || 'Unknown Course'}`);
206
+ console.log(`šŸ“ Assignment: ${selectedAssignment?.name || 'Unknown Assignment'}`);
207
+ console.log(`šŸ“ Files (${filePaths.length}):`);
188
208
  filePaths.forEach((file, index) => {
189
209
  const stats = fs.statSync(file);
190
210
  const size = (stats.size / 1024).toFixed(1) + ' KB';
191
- console.log(` ${index + 1}. ${file} (${size})`);
211
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
192
212
  });
193
213
 
194
- const confirm = await askQuestion(rl, '\nProceed with submission? (Y/n): ');
195
- if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
214
+ const confirm = await askConfirmation(rl, '\nProceed with submission?', true);
215
+ if (!confirm) {
196
216
  console.log('Submission cancelled.');
197
217
  rl.close();
198
218
  return;
@@ -209,11 +229,10 @@ async function submitAssignment(options) {
209
229
  try {
210
230
  const fileId = await uploadSingleFileToCanvas(courseId, assignmentId, currentFile);
211
231
  uploadedFileIds.push(fileId);
212
- console.log(`āœ… ${path.basename(currentFile)} uploaded successfully`);
213
- } catch (error) {
232
+ console.log(`āœ… ${path.basename(currentFile)} uploaded successfully`); } catch (error) {
214
233
  console.error(`āŒ Failed to upload ${currentFile}: ${error.message}`);
215
- const continueUpload = await askQuestion(rl, 'Continue with remaining files? (Y/n): ');
216
- if (continueUpload.toLowerCase() === 'n' || continueUpload.toLowerCase() === 'no') {
234
+ const continueUpload = await askConfirmation(rl, 'Continue with remaining files?', true);
235
+ if (!continueUpload) {
217
236
  break;
218
237
  }
219
238
  }
@@ -361,6 +380,127 @@ async function selectSingleFileFromList(rl, files) {
361
380
  }
362
381
  }
363
382
 
383
+ async function selectFilesImproved(rl) {
384
+ console.log('šŸ“ Enhanced File Selection');
385
+ console.log('Choose files by entering their paths or browse current directory');
386
+ console.log('Press Enter with no input when done selecting files.\n');
387
+
388
+ const allFiles = [];
389
+ let fileIndex = 1;
390
+
391
+ while (true) {
392
+ console.log(`\nšŸ“Ž File ${fileIndex} selection:`);
393
+ console.log('1. Enter file path directly');
394
+ console.log('2. Browse current directory');
395
+ console.log('3. Show currently selected files');
396
+ console.log('(Press Enter with no input to finish selection)');
397
+
398
+ const choice = await askQuestion(rl, '\nChoose option (1-3 or Enter to finish): ');
399
+
400
+ // Empty input means we're done selecting files
401
+ if (choice.trim() === '') {
402
+ if (allFiles.length === 0) {
403
+ console.log('āš ļø No files selected. Please select at least one file.');
404
+ continue;
405
+ }
406
+ break;
407
+ }
408
+
409
+ if (choice === '1') {
410
+ // Direct file path entry
411
+ const filePath = await askQuestion(rl, 'Enter file path: ');
412
+ if (filePath.trim() !== '') {
413
+ if (fs.existsSync(filePath.trim())) {
414
+ if (!allFiles.includes(filePath.trim())) {
415
+ allFiles.push(filePath.trim());
416
+ const stats = fs.statSync(filePath.trim());
417
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
418
+ console.log(`āœ… Added: ${path.basename(filePath.trim())} (${size})`);
419
+ fileIndex++;
420
+ } else {
421
+ console.log('āš ļø File already selected.');
422
+ }
423
+ } else {
424
+ console.log('āŒ File not found. Please check the path.');
425
+ }
426
+ }
427
+ } else if (choice === '2') {
428
+ // Browse current directory
429
+ try {
430
+ const files = fs.readdirSync('.').filter(file => {
431
+ const stats = fs.statSync(file);
432
+ return stats.isFile() &&
433
+ !file.startsWith('.') &&
434
+ !['package.json', 'package-lock.json', 'node_modules'].includes(file);
435
+ });
436
+
437
+ if (files.length === 0) {
438
+ console.log('No suitable files found in current directory.');
439
+ continue;
440
+ }
441
+
442
+ console.log('\nšŸ“‚ Files in current directory:');
443
+ files.forEach((file, index) => {
444
+ const stats = fs.statSync(file);
445
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
446
+ const alreadySelected = allFiles.includes(file) ? ' āœ…' : '';
447
+ console.log(`${index + 1}. ${file} (${size})${alreadySelected}`);
448
+ });
449
+
450
+ const fileChoice = await askQuestion(rl, '\nEnter file number (or Enter to go back): ');
451
+ if (fileChoice.trim() !== '') {
452
+ const fileIdx = parseInt(fileChoice) - 1;
453
+ if (fileIdx >= 0 && fileIdx < files.length) {
454
+ const selectedFile = files[fileIdx];
455
+ if (!allFiles.includes(selectedFile)) {
456
+ allFiles.push(selectedFile);
457
+ const stats = fs.statSync(selectedFile);
458
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
459
+ console.log(`āœ… Added: ${selectedFile} (${size})`);
460
+ fileIndex++;
461
+ } else {
462
+ console.log('āš ļø File already selected.');
463
+ }
464
+ } else {
465
+ console.log('āŒ Invalid file number.');
466
+ }
467
+ }
468
+ } catch (error) {
469
+ console.log('āŒ Error reading directory:', error.message);
470
+ }
471
+ } else if (choice === '3') {
472
+ // Show currently selected files
473
+ if (allFiles.length === 0) {
474
+ console.log('šŸ“‹ No files selected yet.');
475
+ } else {
476
+ console.log(`\nšŸ“‹ Currently selected files (${allFiles.length}):`);
477
+ allFiles.forEach((file, index) => {
478
+ const stats = fs.existsSync(file) ? fs.statSync(file) : null;
479
+ const size = stats ? (stats.size / 1024).toFixed(1) + ' KB' : 'File not found';
480
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
481
+ });
482
+
483
+ const removeFile = await askQuestion(rl, '\nRemove a file? Enter number or press Enter to continue: ');
484
+ if (removeFile.trim() !== '') {
485
+ const removeIdx = parseInt(removeFile) - 1;
486
+ if (removeIdx >= 0 && removeIdx < allFiles.length) {
487
+ const removedFile = allFiles.splice(removeIdx, 1)[0];
488
+ console.log(`šŸ—‘ļø Removed: ${path.basename(removedFile)}`);
489
+ fileIndex--;
490
+ } else {
491
+ console.log('āŒ Invalid file number.');
492
+ }
493
+ }
494
+ }
495
+ } else {
496
+ console.log('āŒ Invalid option. Please choose 1, 2, 3, or press Enter to finish.');
497
+ }
498
+ }
499
+
500
+ console.log(`\nāœ… File selection complete! Selected ${allFiles.length} file(s).`);
501
+ return allFiles;
502
+ }
503
+
364
504
  module.exports = {
365
505
  submitAssignment
366
506
  };
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  const readline = require('readline');
6
+ const fs = require('fs');
7
+ const path = require('path');
6
8
 
7
9
  /**
8
10
  * Create readline interface for user input
@@ -25,7 +27,299 @@ function askQuestion(rl, question) {
25
27
  });
26
28
  }
27
29
 
30
+ /**
31
+ * Ask a question with validation and retry logic
32
+ */
33
+ function askQuestionWithValidation(rl, question, validator, errorMessage) {
34
+ return new Promise(async (resolve) => {
35
+ let answer;
36
+ do {
37
+ answer = await askQuestion(rl, question);
38
+ if (validator(answer)) {
39
+ resolve(answer.trim());
40
+ return;
41
+ } else {
42
+ console.log(errorMessage || 'Invalid input. Please try again.');
43
+ }
44
+ } while (true);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Ask for confirmation (Y/n format)
50
+ */
51
+ async function askConfirmation(rl, question, defaultYes = true) {
52
+ const suffix = defaultYes ? ' (Y/n)' : ' (y/N)';
53
+ const answer = await askQuestion(rl, question + suffix + ': ');
54
+
55
+ if (answer.trim() === '') {
56
+ return defaultYes;
57
+ }
58
+
59
+ const lower = answer.toLowerCase();
60
+ return lower === 'y' || lower === 'yes';
61
+ }
62
+
63
+ /**
64
+ * Select from a list of options
65
+ */
66
+ async function selectFromList(rl, items, displayProperty = null, allowCancel = true) {
67
+ if (!items || items.length === 0) {
68
+ console.log('No items to select from.');
69
+ return null;
70
+ }
71
+
72
+ console.log('\nSelect an option:');
73
+ items.forEach((item, index) => {
74
+ const displayText = displayProperty ? item[displayProperty] : item;
75
+ console.log(`${index + 1}. ${displayText}`);
76
+ });
77
+
78
+ if (allowCancel) {
79
+ console.log('0. Cancel');
80
+ }
81
+
82
+ const validator = (input) => {
83
+ const num = parseInt(input);
84
+ return !isNaN(num) && num >= (allowCancel ? 0 : 1) && num <= items.length;
85
+ };
86
+
87
+ const answer = await askQuestionWithValidation(
88
+ rl,
89
+ '\nEnter your choice: ',
90
+ validator,
91
+ `Please enter a number between ${allowCancel ? '0' : '1'} and ${items.length}.`
92
+ );
93
+
94
+ const choice = parseInt(answer);
95
+
96
+ if (choice === 0 && allowCancel) {
97
+ return null;
98
+ }
99
+
100
+ return items[choice - 1];
101
+ }
102
+
103
+ /**
104
+ * Get files matching a wildcard pattern
105
+ */
106
+ function getFilesMatchingWildcard(pattern, currentDir = process.cwd()) {
107
+ try {
108
+ const files = fs.readdirSync(currentDir);
109
+ const matchedFiles = [];
110
+
111
+ // Convert wildcard pattern to regex
112
+ let regexPattern;
113
+ if (pattern.startsWith('*.')) {
114
+ // Handle *.extension patterns
115
+ const extension = pattern.slice(2);
116
+ regexPattern = new RegExp(`\\.${extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
117
+ } else if (pattern.includes('*')) {
118
+ // Handle other wildcard patterns
119
+ regexPattern = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i');
120
+ } else {
121
+ // Exact match
122
+ regexPattern = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
123
+ }
124
+
125
+ files.forEach(file => {
126
+ const filePath = path.join(currentDir, file);
127
+ const stats = fs.statSync(filePath);
128
+
129
+ if (stats.isFile() && regexPattern.test(file)) {
130
+ matchedFiles.push(filePath);
131
+ }
132
+ });
133
+
134
+ return matchedFiles;
135
+ } catch (error) {
136
+ console.error(`Error reading directory: ${error.message}`);
137
+ return [];
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Enhanced file selection with wildcard support
143
+ */
144
+ async function selectFilesImproved(rl, currentDir = process.cwd()) {
145
+ const selectedFiles = [];
146
+
147
+ console.log('\nšŸ“ Enhanced File Selection');
148
+ console.log('šŸ’” Tips:');
149
+ console.log(' • Type filename to add individual files');
150
+ console.log(' • Use wildcards: *.html, *.js, *.pdf, etc.');
151
+ console.log(' • Type "browse" to see available files');
152
+ console.log(' • Type "remove" to remove files from selection');
153
+ console.log(' • Press Enter with no input to finish selection\n');
154
+
155
+ while (true) {
156
+ // Show current selection
157
+ if (selectedFiles.length > 0) {
158
+ console.log(`\nšŸ“‹ Currently selected (${selectedFiles.length} files):`);
159
+ selectedFiles.forEach((file, index) => {
160
+ const stats = fs.statSync(file);
161
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
162
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
163
+ });
164
+ }
165
+
166
+ const input = await askQuestion(rl, '\nšŸ“Ž Add file (or press Enter to finish): ');
167
+
168
+ if (!input.trim()) {
169
+ // Empty input - finish selection
170
+ break;
171
+ }
172
+
173
+ if (input.toLowerCase() === 'browse') {
174
+ // Show available files
175
+ console.log('\nšŸ“‚ Available files in current directory:');
176
+ try {
177
+ const files = fs.readdirSync(currentDir);
178
+ const filteredFiles = files.filter(file => {
179
+ const filePath = path.join(currentDir, file);
180
+ const stats = fs.statSync(filePath);
181
+ return stats.isFile() &&
182
+ !file.startsWith('.') &&
183
+ !['package.json', 'package-lock.json', 'node_modules'].includes(file);
184
+ });
185
+
186
+ if (filteredFiles.length === 0) {
187
+ console.log(' No suitable files found.');
188
+ } else {
189
+ filteredFiles.forEach((file, index) => {
190
+ const filePath = path.join(currentDir, file);
191
+ const stats = fs.statSync(filePath);
192
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
193
+ const ext = path.extname(file);
194
+ const icon = getFileIcon(ext);
195
+ console.log(` ${index + 1}. ${icon} ${file} (${size})`);
196
+ });
197
+ }
198
+ } catch (error) {
199
+ console.log(` Error reading directory: ${error.message}`);
200
+ }
201
+ continue;
202
+ }
203
+
204
+ if (input.toLowerCase() === 'remove') {
205
+ // Remove files from selection
206
+ if (selectedFiles.length === 0) {
207
+ console.log('āŒ No files selected to remove.');
208
+ continue;
209
+ }
210
+
211
+ console.log('\nSelect file to remove:');
212
+ selectedFiles.forEach((file, index) => {
213
+ console.log(`${index + 1}. ${path.basename(file)}`);
214
+ });
215
+
216
+ const removeChoice = await askQuestion(rl, '\nEnter number to remove (or press Enter to cancel): ');
217
+ if (removeChoice.trim()) {
218
+ const removeIndex = parseInt(removeChoice) - 1;
219
+ if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
220
+ const removedFile = selectedFiles.splice(removeIndex, 1)[0];
221
+ console.log(`āœ… Removed: ${path.basename(removedFile)}`);
222
+ } else {
223
+ console.log('āŒ Invalid selection.');
224
+ }
225
+ }
226
+ continue;
227
+ }
228
+
229
+ // Check if input contains wildcards
230
+ if (input.includes('*') || input.includes('?')) {
231
+ const matchedFiles = getFilesMatchingWildcard(input, currentDir);
232
+
233
+ if (matchedFiles.length === 0) {
234
+ console.log(`āŒ No files found matching pattern: ${input}`);
235
+ continue;
236
+ }
237
+
238
+ console.log(`\nšŸŽÆ Found ${matchedFiles.length} files matching "${input}":`);
239
+ matchedFiles.forEach((file, index) => {
240
+ const stats = fs.statSync(file);
241
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
242
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
243
+ });
244
+
245
+ const confirmAll = await askConfirmation(rl, `Add all ${matchedFiles.length} files?`, true);
246
+
247
+ if (confirmAll) {
248
+ const newFiles = matchedFiles.filter(file => !selectedFiles.includes(file));
249
+ selectedFiles.push(...newFiles);
250
+ console.log(`āœ… Added ${newFiles.length} new files (${matchedFiles.length - newFiles.length} were already selected)`);
251
+ }
252
+ continue;
253
+ }
254
+
255
+ // Handle individual file selection
256
+ let filePath = input;
257
+
258
+ // If not absolute path, make it relative to current directory
259
+ if (!path.isAbsolute(filePath)) {
260
+ filePath = path.join(currentDir, filePath);
261
+ }
262
+
263
+ try {
264
+ if (!fs.existsSync(filePath)) {
265
+ console.log(`āŒ File not found: ${input}`);
266
+ continue;
267
+ }
268
+
269
+ const stats = fs.statSync(filePath);
270
+ if (!stats.isFile()) {
271
+ console.log(`āŒ Not a file: ${input}`);
272
+ continue;
273
+ }
274
+
275
+ if (selectedFiles.includes(filePath)) {
276
+ console.log(`āš ļø File already selected: ${path.basename(filePath)}`);
277
+ continue;
278
+ }
279
+
280
+ selectedFiles.push(filePath);
281
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
282
+ console.log(`āœ… Added: ${path.basename(filePath)} (${size})`);
283
+
284
+ } catch (error) {
285
+ console.log(`āŒ Error accessing file: ${error.message}`);
286
+ }
287
+ }
288
+
289
+ return selectedFiles;
290
+ }
291
+
292
+ /**
293
+ * Get file icon based on extension
294
+ */
295
+ function getFileIcon(extension) {
296
+ const iconMap = {
297
+ '.js': 'šŸ“œ',
298
+ '.html': '🌐',
299
+ '.css': 'šŸŽØ',
300
+ '.pdf': 'šŸ“„',
301
+ '.doc': 'šŸ“',
302
+ '.docx': 'šŸ“',
303
+ '.txt': 'šŸ“„',
304
+ '.md': 'šŸ“–',
305
+ '.json': 'āš™ļø',
306
+ '.xml': 'šŸ“‹',
307
+ '.zip': 'šŸ“¦',
308
+ '.png': 'šŸ–¼ļø',
309
+ '.jpg': 'šŸ–¼ļø',
310
+ '.jpeg': 'šŸ–¼ļø',
311
+ '.gif': 'šŸ–¼ļø'
312
+ };
313
+
314
+ return iconMap[extension.toLowerCase()] || 'šŸ“Ž';
315
+ }
316
+
28
317
  module.exports = {
29
318
  createReadlineInterface,
30
- askQuestion
319
+ askQuestion,
320
+ askQuestionWithValidation,
321
+ askConfirmation,
322
+ selectFromList,
323
+ selectFilesImproved,
324
+ getFilesMatchingWildcard
31
325
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvaslms-cli",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "A command line tool for interacting with Canvas LMS API",
5
5
  "keywords": [
6
6
  "canvas",
package/src/index.js CHANGED
@@ -26,7 +26,7 @@ const program = new Command();
26
26
  program
27
27
  .name('canvas')
28
28
  .description('Canvas API Command Line Tool')
29
- .version('1.1.1');
29
+ .version('1.2.0');
30
30
 
31
31
  // Raw API commands
32
32
  function createQueryCommand(method) {