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 +63 -1
- package/commands/announcements.js +5 -2
- package/commands/assignments.js +9 -5
- package/commands/grades.js +182 -45
- package/commands/submit.js +166 -26
- package/lib/interactive.js +295 -1
- package/package.json +1 -1
- package/src/index.js +1 -1
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.
|
|
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(
|
|
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
|
@@ -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}
|
|
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)
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
149
|
-
if (resubmit
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
186
|
-
console.log(
|
|
187
|
-
console.log(
|
|
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
|
|
195
|
-
if (confirm
|
|
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
|
|
216
|
-
if (continueUpload
|
|
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
|
};
|
package/lib/interactive.js
CHANGED
|
@@ -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