canvaslms-cli 1.1.0 → 1.2.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,64 @@ 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
+
19
+ - **Improved Grade Viewing**:
20
+ - Interactive course selection for grade viewing
21
+ - Assignment-level grade details with color coding
22
+ - Overall course grade summary
23
+ - Better grade formatting and status indicators
24
+ - Support for letter grades, excused, and missing assignments
25
+
26
+ - **Enhanced Display Names**:
27
+ - Show course names instead of IDs in all commands
28
+ - Display assignment names prominently
29
+ - Better labeling of IDs vs names throughout interface
30
+
31
+ - **Interactive Utilities**:
32
+ - Added validation and retry logic for user input
33
+ - Confirmation helpers with default values
34
+ - List selection utilities with cancel option
35
+
36
+ ### Improved
37
+
38
+ - **Submit Command**: Complete redesign with better file selection workflow
39
+ - **Grades Command**: Interactive course selection and detailed assignment grades
40
+ - **Assignments Command**: Display course names prominently
41
+ - **Announcements Command**: Show course names instead of IDs
42
+ - **User Experience**: More consistent and intuitive interfaces across all commands
43
+
44
+ ### Technical
45
+
46
+ - Enhanced interactive utilities in `lib/interactive.js`
47
+ - Better error handling and user guidance
48
+ - Improved code organization and modularity
49
+
50
+ ## [1.1.1] - 2025-07-03
51
+
52
+ ### Fixed
53
+
54
+ - Removed dotenv dependency that was causing module not found errors
55
+ - Fixed configuration file path to use `.canvaslms-cli-config.json`
56
+ - Resolved package publishing and global installation issues
9
57
 
10
58
  ### Added
11
59
 
60
+ - Dual binary support: both `canvaslms-cli` and `canvas` commands work
61
+
62
+ ## [1.1.0] - 2025-07-03
63
+
64
+ ### Major Changes
65
+
12
66
  - Home directory configuration system (~/.canvaslms-cli-config.json)
13
67
  - Interactive configuration setup wizard (`canvas config setup`)
14
68
  - 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
 
@@ -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)
@@ -129,7 +140,7 @@ async function submitAssignment(options) {
129
140
  return;
130
141
  }
131
142
 
132
- const selectedAssignment = assignments[assignmentIndex];
143
+ const selectedAssignment = assignments[assignmentIndex];
133
144
 
134
145
  // Check if assignment accepts file uploads
135
146
  if (!selectedAssignment.submission_types ||
@@ -141,7 +152,7 @@ async function submitAssignment(options) {
141
152
  }
142
153
 
143
154
  assignmentId = selectedAssignment.id;
144
- console.log(`Selected: ${selectedAssignment.name}\n`);
155
+ console.log(`✅ Selected: ${selectedAssignment.name}\n`);
145
156
 
146
157
  // Check if already submitted
147
158
  if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
@@ -152,14 +163,21 @@ async function submitAssignment(options) {
152
163
  return;
153
164
  }
154
165
  }
166
+ } else {
167
+ // Fetch assignment details if ID was provided
168
+ try {
169
+ selectedAssignment = await makeCanvasRequest('get', `courses/${courseId}/assignments/${assignmentId}`);
170
+ } catch (error) {
171
+ console.log(`⚠️ Could not fetch assignment details for ID ${assignmentId}`);
172
+ selectedAssignment = { id: assignmentId, name: `Assignment ${assignmentId}` };
173
+ }
155
174
  }
156
-
157
- // Step 3: Select Files (if not provided)
175
+ // Step 3: Select Files (if not provided)
158
176
  let filePaths = [];
159
177
  if (filePath) {
160
178
  filePaths = [filePath]; // Single file provided via option
161
179
  } else {
162
- filePaths = await selectFiles(rl);
180
+ filePaths = await selectFilesImproved(rl);
163
181
  }
164
182
 
165
183
  // Validate all selected files exist
@@ -179,16 +197,15 @@ async function submitAssignment(options) {
179
197
  }
180
198
 
181
199
  filePaths = validFiles;
182
-
183
- // Step 4: Confirm and Submit
200
+ // Step 4: Confirm and Submit
184
201
  console.log('\n📋 Submission Summary:');
185
- console.log(`Course ID: ${courseId}`);
186
- console.log(`Assignment ID: ${assignmentId}`);
187
- console.log(`Files (${filePaths.length}):`);
202
+ console.log(`📚 Course: ${selectedCourse?.name || 'Unknown Course'}`);
203
+ console.log(`📝 Assignment: ${selectedAssignment?.name || 'Unknown Assignment'}`);
204
+ console.log(`📁 Files (${filePaths.length}):`);
188
205
  filePaths.forEach((file, index) => {
189
206
  const stats = fs.statSync(file);
190
207
  const size = (stats.size / 1024).toFixed(1) + ' KB';
191
- console.log(` ${index + 1}. ${file} (${size})`);
208
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
192
209
  });
193
210
 
194
211
  const confirm = await askQuestion(rl, '\nProceed with submission? (Y/n): ');
@@ -361,6 +378,127 @@ async function selectSingleFileFromList(rl, files) {
361
378
  }
362
379
  }
363
380
 
381
+ async function selectFilesImproved(rl) {
382
+ console.log('📁 Enhanced File Selection');
383
+ console.log('Choose files by entering their paths or browse current directory');
384
+ console.log('Press Enter with no input when done selecting files.\n');
385
+
386
+ const allFiles = [];
387
+ let fileIndex = 1;
388
+
389
+ while (true) {
390
+ console.log(`\n📎 File ${fileIndex} selection:`);
391
+ console.log('1. Enter file path directly');
392
+ console.log('2. Browse current directory');
393
+ console.log('3. Show currently selected files');
394
+ console.log('(Press Enter with no input to finish selection)');
395
+
396
+ const choice = await askQuestion(rl, '\nChoose option (1-3 or Enter to finish): ');
397
+
398
+ // Empty input means we're done selecting files
399
+ if (choice.trim() === '') {
400
+ if (allFiles.length === 0) {
401
+ console.log('⚠️ No files selected. Please select at least one file.');
402
+ continue;
403
+ }
404
+ break;
405
+ }
406
+
407
+ if (choice === '1') {
408
+ // Direct file path entry
409
+ const filePath = await askQuestion(rl, 'Enter file path: ');
410
+ if (filePath.trim() !== '') {
411
+ if (fs.existsSync(filePath.trim())) {
412
+ if (!allFiles.includes(filePath.trim())) {
413
+ allFiles.push(filePath.trim());
414
+ const stats = fs.statSync(filePath.trim());
415
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
416
+ console.log(`✅ Added: ${path.basename(filePath.trim())} (${size})`);
417
+ fileIndex++;
418
+ } else {
419
+ console.log('⚠️ File already selected.');
420
+ }
421
+ } else {
422
+ console.log('❌ File not found. Please check the path.');
423
+ }
424
+ }
425
+ } else if (choice === '2') {
426
+ // Browse current directory
427
+ try {
428
+ const files = fs.readdirSync('.').filter(file => {
429
+ const stats = fs.statSync(file);
430
+ return stats.isFile() &&
431
+ !file.startsWith('.') &&
432
+ !['package.json', 'package-lock.json', 'node_modules'].includes(file);
433
+ });
434
+
435
+ if (files.length === 0) {
436
+ console.log('No suitable files found in current directory.');
437
+ continue;
438
+ }
439
+
440
+ console.log('\n📂 Files in current directory:');
441
+ files.forEach((file, index) => {
442
+ const stats = fs.statSync(file);
443
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
444
+ const alreadySelected = allFiles.includes(file) ? ' ✅' : '';
445
+ console.log(`${index + 1}. ${file} (${size})${alreadySelected}`);
446
+ });
447
+
448
+ const fileChoice = await askQuestion(rl, '\nEnter file number (or Enter to go back): ');
449
+ if (fileChoice.trim() !== '') {
450
+ const fileIdx = parseInt(fileChoice) - 1;
451
+ if (fileIdx >= 0 && fileIdx < files.length) {
452
+ const selectedFile = files[fileIdx];
453
+ if (!allFiles.includes(selectedFile)) {
454
+ allFiles.push(selectedFile);
455
+ const stats = fs.statSync(selectedFile);
456
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
457
+ console.log(`✅ Added: ${selectedFile} (${size})`);
458
+ fileIndex++;
459
+ } else {
460
+ console.log('⚠️ File already selected.');
461
+ }
462
+ } else {
463
+ console.log('❌ Invalid file number.');
464
+ }
465
+ }
466
+ } catch (error) {
467
+ console.log('❌ Error reading directory:', error.message);
468
+ }
469
+ } else if (choice === '3') {
470
+ // Show currently selected files
471
+ if (allFiles.length === 0) {
472
+ console.log('📋 No files selected yet.');
473
+ } else {
474
+ console.log(`\n📋 Currently selected files (${allFiles.length}):`);
475
+ allFiles.forEach((file, index) => {
476
+ const stats = fs.existsSync(file) ? fs.statSync(file) : null;
477
+ const size = stats ? (stats.size / 1024).toFixed(1) + ' KB' : 'File not found';
478
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
479
+ });
480
+
481
+ const removeFile = await askQuestion(rl, '\nRemove a file? Enter number or press Enter to continue: ');
482
+ if (removeFile.trim() !== '') {
483
+ const removeIdx = parseInt(removeFile) - 1;
484
+ if (removeIdx >= 0 && removeIdx < allFiles.length) {
485
+ const removedFile = allFiles.splice(removeIdx, 1)[0];
486
+ console.log(`🗑️ Removed: ${path.basename(removedFile)}`);
487
+ fileIndex--;
488
+ } else {
489
+ console.log('❌ Invalid file number.');
490
+ }
491
+ }
492
+ }
493
+ } else {
494
+ console.log('❌ Invalid option. Please choose 1, 2, 3, or press Enter to finish.');
495
+ }
496
+ }
497
+
498
+ console.log(`\n✅ File selection complete! Selected ${allFiles.length} file(s).`);
499
+ return allFiles;
500
+ }
501
+
364
502
  module.exports = {
365
503
  submitAssignment
366
504
  };
package/lib/config.js CHANGED
@@ -5,10 +5,9 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
- require('dotenv').config();
9
8
 
10
9
  // Configuration file path in user's home directory
11
- const CONFIG_FILE = path.join(os.homedir(), '.canvas-cli-config.json');
10
+ const CONFIG_FILE = path.join(os.homedir(), '.canvaslms-cli-config.json');
12
11
 
13
12
  /**
14
13
  * Get default configuration structure
@@ -23,11 +22,11 @@ function getDefaultConfig() {
23
22
  }
24
23
 
25
24
  /**
26
- * Load configuration from file or environment variables
25
+ * Load configuration from file
27
26
  */
28
27
  function loadConfig() {
29
28
  try {
30
- // First, try to load from config file
29
+ // Load from config file
31
30
  if (fs.existsSync(CONFIG_FILE)) {
32
31
  const configData = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
33
32
  if (configData.domain && configData.token) {
@@ -37,24 +36,10 @@ function loadConfig() {
37
36
  }
38
37
  }
39
38
 
40
- // Fallback to environment variables
41
- let domain = process.env.CANVAS_DOMAIN;
42
- const token = process.env.CANVAS_API_TOKEN;
43
-
44
- if (!domain || !token) {
45
- console.error('❌ No Canvas configuration found!');
46
- console.error('\nPlease run "canvas config setup" to configure your Canvas credentials.');
47
- console.error('Or set environment variables:');
48
- console.error(' CANVAS_DOMAIN=your-canvas-domain.instructure.com');
49
- console.error(' CANVAS_API_TOKEN=your-api-token');
50
- process.exit(1);
51
- }
52
-
53
- // Clean up domain - remove https:// and trailing slashes
54
- domain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
55
-
56
- return { domain, token };
57
- } catch (error) {
39
+ // No configuration found
40
+ console.error('❌ No Canvas configuration found!');
41
+ console.error('\nPlease run "canvas config setup" to configure your Canvas credentials.');
42
+ process.exit(1); } catch (error) {
58
43
  console.error(`❌ Error loading configuration: ${error.message}`);
59
44
  process.exit(1);
60
45
  }
@@ -25,7 +25,83 @@ function askQuestion(rl, question) {
25
25
  });
26
26
  }
27
27
 
28
+ /**
29
+ * Ask a question with validation and retry logic
30
+ */
31
+ function askQuestionWithValidation(rl, question, validator, errorMessage) {
32
+ return new Promise(async (resolve) => {
33
+ let answer;
34
+ do {
35
+ answer = await askQuestion(rl, question);
36
+ if (validator(answer)) {
37
+ resolve(answer.trim());
38
+ return;
39
+ } else {
40
+ console.log(errorMessage || 'Invalid input. Please try again.');
41
+ }
42
+ } while (true);
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Ask for confirmation (Y/n format)
48
+ */
49
+ async function askConfirmation(rl, question, defaultYes = true) {
50
+ const suffix = defaultYes ? ' (Y/n)' : ' (y/N)';
51
+ const answer = await askQuestion(rl, question + suffix + ': ');
52
+
53
+ if (answer.trim() === '') {
54
+ return defaultYes;
55
+ }
56
+
57
+ const lower = answer.toLowerCase();
58
+ return lower === 'y' || lower === 'yes';
59
+ }
60
+
61
+ /**
62
+ * Select from a list of options
63
+ */
64
+ async function selectFromList(rl, items, displayProperty = null, allowCancel = true) {
65
+ if (!items || items.length === 0) {
66
+ console.log('No items to select from.');
67
+ return null;
68
+ }
69
+
70
+ console.log('\nSelect an option:');
71
+ items.forEach((item, index) => {
72
+ const displayText = displayProperty ? item[displayProperty] : item;
73
+ console.log(`${index + 1}. ${displayText}`);
74
+ });
75
+
76
+ if (allowCancel) {
77
+ console.log('0. Cancel');
78
+ }
79
+
80
+ const validator = (input) => {
81
+ const num = parseInt(input);
82
+ return !isNaN(num) && num >= (allowCancel ? 0 : 1) && num <= items.length;
83
+ };
84
+
85
+ const answer = await askQuestionWithValidation(
86
+ rl,
87
+ '\nEnter your choice: ',
88
+ validator,
89
+ `Please enter a number between ${allowCancel ? '0' : '1'} and ${items.length}.`
90
+ );
91
+
92
+ const choice = parseInt(answer);
93
+
94
+ if (choice === 0 && allowCancel) {
95
+ return null;
96
+ }
97
+
98
+ return items[choice - 1];
99
+ }
100
+
28
101
  module.exports = {
29
102
  createReadlineInterface,
30
- askQuestion
103
+ askQuestion,
104
+ askQuestionWithValidation,
105
+ askConfirmation,
106
+ selectFromList
31
107
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvaslms-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A command line tool for interacting with Canvas LMS API",
5
5
  "keywords": [
6
6
  "canvas",
@@ -27,6 +27,7 @@
27
27
  "type": "commonjs",
28
28
  "main": "src/index.js",
29
29
  "bin": {
30
+ "canvaslms-cli": "src/index.js",
30
31
  "canvas": "src/index.js"
31
32
  },
32
33
  "files": [
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.0');
29
+ .version('1.2.0');
30
30
 
31
31
  // Raw API commands
32
32
  function createQueryCommand(method) {