canvaslms-cli 1.1.1 ā 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 +55 -1
- package/commands/announcements.js +5 -2
- package/commands/assignments.js +9 -5
- package/commands/grades.js +182 -45
- package/commands/submit.js +152 -14
- package/lib/interactive.js +77 -1
- package/package.json +1 -1
- package/src/index.js +1 -1
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.
|
|
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(
|
|
20
|
+
console.log(`No announcements found for course: ${course.name}`);
|
|
18
21
|
return;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
console.log(
|
|
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}`);
|
package/commands/assignments.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
package/commands/grades.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
package/commands/submit.js
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
186
|
-
console.log(
|
|
187
|
-
console.log(
|
|
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/interactive.js
CHANGED
|
@@ -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