canvaslms-cli 1.4.2 → 1.4.4
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/LICENSE +1 -1
- package/README.md +60 -134
- package/commands/submit.js +484 -245
- package/lib/interactive.js +416 -316
- package/package.json +3 -3
package/commands/submit.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { makeCanvasRequest } from '../lib/api-client.js';
|
|
8
|
-
import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved,
|
|
8
|
+
import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved, selectFilesKeyboard, pad } from '../lib/interactive.js';
|
|
9
9
|
import { uploadSingleFileToCanvas, submitAssignmentWithFiles } from '../lib/file-upload.js';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
|
|
@@ -18,194 +18,336 @@ export async function submitAssignment(options) {
|
|
|
18
18
|
let selectedCourse = null;
|
|
19
19
|
let selectedAssignment = null;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
|
|
22
|
+
// Step 1 & 2: Select Course and Assignment (if not provided)
|
|
23
|
+
while (!courseId || !assignmentId) {
|
|
24
|
+
if (!courseId) {
|
|
25
|
+
let courses = null;
|
|
26
|
+
while (true) {
|
|
27
|
+
console.log(chalk.cyan.bold(`\n${'-'.repeat(60)}`));
|
|
28
|
+
console.log(chalk.cyan.bold('Loading your courses, please wait...'));
|
|
29
|
+
try {
|
|
30
|
+
courses = await makeCanvasRequest('get', 'courses', [
|
|
31
|
+
'enrollment_state=active',
|
|
32
|
+
'include[]=favorites'
|
|
33
|
+
]);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(chalk.red('Error: Failed to load courses: ' + error.message));
|
|
36
|
+
const retryCourses = await askConfirmation(rl, chalk.yellow('Try loading courses again?'), true);
|
|
37
|
+
if (retryCourses) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!courses || courses.length === 0) {
|
|
45
|
+
console.log(chalk.red('Error: No courses found.'));
|
|
46
|
+
const retryCourses = await askConfirmation(rl, chalk.yellow('Refresh course list?'), true);
|
|
47
|
+
if (retryCourses) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let selectableCourses = courses;
|
|
57
|
+
if (!options.all) {
|
|
58
|
+
selectableCourses = courses.filter(course => course.is_favorite);
|
|
59
|
+
if (selectableCourses.length === 0) {
|
|
60
|
+
console.log(chalk.yellow('No starred courses found. Showing all enrolled courses...'));
|
|
61
|
+
selectableCourses = courses;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
66
|
+
console.log(chalk.cyan.bold('Select a course:'));
|
|
67
|
+
selectableCourses.forEach((course, index) => {
|
|
68
|
+
console.log(pad(chalk.white((index + 1) + '. '), 5) + chalk.white(course.name));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
while (!courseId) {
|
|
72
|
+
const courseChoice = await askQuestion(rl, chalk.bold.cyan(`
|
|
73
|
+
Enter course number (or ".."/"back" to cancel): `));
|
|
74
|
+
if (courseChoice === '..' || courseChoice.toLowerCase() === 'back') {
|
|
75
|
+
const confirmCancel = await askConfirmation(rl, chalk.yellow('Cancel submission?'), false);
|
|
76
|
+
if (confirmCancel) {
|
|
77
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!courseChoice.trim()) {
|
|
83
|
+
console.log(chalk.yellow('Please enter a course number.'));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const parsedIndex = Number.parseInt(courseChoice, 10);
|
|
87
|
+
if (Number.isNaN(parsedIndex)) {
|
|
88
|
+
console.log(chalk.red('Error: Please enter a valid course number.'));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const courseIndex = parsedIndex - 1;
|
|
92
|
+
if (courseIndex < 0 || courseIndex >= selectableCourses.length) {
|
|
93
|
+
console.log(chalk.red('Error: Invalid course selection.'));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
selectedCourse = selectableCourses[courseIndex];
|
|
97
|
+
courseId = selectedCourse.id;
|
|
98
|
+
console.log(chalk.green(`Success: Selected ${selectedCourse.name}
|
|
99
|
+
`));
|
|
40
100
|
}
|
|
41
101
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
|
|
103
|
+
if (!assignmentId) {
|
|
104
|
+
let assignments = null;
|
|
105
|
+
while (true) {
|
|
106
|
+
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
107
|
+
console.log(chalk.cyan.bold('Loading assignments, please wait...'));
|
|
108
|
+
try {
|
|
109
|
+
assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
|
|
110
|
+
'include[]=submission',
|
|
111
|
+
'order_by=due_at',
|
|
112
|
+
'per_page=100'
|
|
113
|
+
]);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(chalk.red('Error: Failed to load assignments: ' + error.message));
|
|
116
|
+
const retryAssignments = await askConfirmation(rl, chalk.yellow('Try loading assignments again?'), true);
|
|
117
|
+
if (retryAssignments) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const chooseDifferentCourse = await askConfirmation(rl, chalk.yellow('Choose a different course?'), true);
|
|
121
|
+
if (chooseDifferentCourse) {
|
|
122
|
+
courseId = null;
|
|
123
|
+
selectedCourse = null;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!assignments || assignments.length === 0) {
|
|
131
|
+
console.log(chalk.red('Error: No assignments found for this course.'));
|
|
132
|
+
const chooseDifferentCourse = await askConfirmation(rl, chalk.yellow('Select a different course?'), true);
|
|
133
|
+
if (chooseDifferentCourse) {
|
|
134
|
+
courseId = null;
|
|
135
|
+
selectedCourse = null;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const retryAssignments = await askConfirmation(rl, chalk.yellow('Refresh assignments for this course?'), true);
|
|
139
|
+
if (retryAssignments) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!courseId) {
|
|
149
|
+
assignmentId = null;
|
|
150
|
+
selectedAssignment = null;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
155
|
+
console.log(chalk.cyan.bold(`Found ${assignments.length} assignment(s):`));
|
|
156
|
+
const submittedCount = assignments.filter(a => a.submission && a.submission.submitted_at).length;
|
|
157
|
+
const pendingCount = assignments.length - submittedCount;
|
|
158
|
+
const uploadableCount = assignments.filter(a =>
|
|
159
|
+
a.submission_types &&
|
|
160
|
+
a.submission_types.includes('online_upload') &&
|
|
161
|
+
a.workflow_state === 'published'
|
|
162
|
+
).length;
|
|
163
|
+
console.log(chalk.yellow(`Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads`));
|
|
164
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
165
|
+
|
|
166
|
+
assignments.forEach((assignment, index) => {
|
|
167
|
+
const dueDate = assignment.due_at ? new Date(assignment.due_at).toLocaleDateString() : 'No due date';
|
|
168
|
+
const submitted = assignment.submission && assignment.submission.submitted_at ? chalk.green('Submitted') : chalk.yellow('Not submitted');
|
|
169
|
+
const canSubmitFiles = assignment.submission_types && assignment.submission_types.includes('online_upload') && assignment.workflow_state === 'published';
|
|
170
|
+
let gradeDisplay = '';
|
|
171
|
+
const submission = assignment.submission;
|
|
172
|
+
if (submission && submission.score !== null && submission.score !== undefined) {
|
|
173
|
+
const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
|
|
174
|
+
const total = assignment.points_possible || 0;
|
|
175
|
+
gradeDisplay = ` | Grade: ${score}/${total}`;
|
|
176
|
+
} else if (submission && submission.excused) {
|
|
177
|
+
gradeDisplay = ' | Grade: Excused';
|
|
178
|
+
} else if (submission && submission.missing) {
|
|
179
|
+
gradeDisplay = ' | Grade: Missing';
|
|
180
|
+
} else if (assignment.points_possible) {
|
|
181
|
+
gradeDisplay = ` | Grade: –/${assignment.points_possible}`;
|
|
182
|
+
}
|
|
183
|
+
let line = pad(chalk.white((index + 1) + '. '), 5) + chalk.white(assignment.name) + chalk.gray(` (${dueDate})`) + ' ' + submitted + gradeDisplay;
|
|
184
|
+
if (!canSubmitFiles) {
|
|
185
|
+
line += chalk.red(' [No file uploads]');
|
|
186
|
+
}
|
|
187
|
+
console.log(line);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
let awaitingAssignment = true;
|
|
191
|
+
while (awaitingAssignment) {
|
|
192
|
+
const assignmentChoice = await askQuestion(rl, chalk.bold.cyan(`
|
|
193
|
+
Enter assignment number (or ".."/"back" to re-select course): `));
|
|
194
|
+
if (assignmentChoice === '..' || assignmentChoice.toLowerCase() === 'back') {
|
|
195
|
+
const chooseCourseAgain = await askConfirmation(rl, chalk.yellow('Go back to course selection?'), true);
|
|
196
|
+
if (chooseCourseAgain) {
|
|
197
|
+
courseId = null;
|
|
198
|
+
selectedCourse = null;
|
|
199
|
+
assignmentId = null;
|
|
200
|
+
selectedAssignment = null;
|
|
201
|
+
awaitingAssignment = false;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!assignmentChoice.trim()) {
|
|
207
|
+
console.log(chalk.yellow('Please enter an assignment number.'));
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const parsedIndex = Number.parseInt(assignmentChoice, 10);
|
|
211
|
+
if (Number.isNaN(parsedIndex)) {
|
|
212
|
+
console.log(chalk.red('Error: Please enter a valid assignment number.'));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const assignmentIndex = parsedIndex - 1;
|
|
216
|
+
if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
|
|
217
|
+
console.log(chalk.red('Error: Invalid assignment selection.'));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const candidate = assignments[assignmentIndex];
|
|
221
|
+
const canSubmitFiles = candidate.submission_types && candidate.submission_types.includes('online_upload') && candidate.workflow_state === 'published';
|
|
222
|
+
if (!canSubmitFiles) {
|
|
223
|
+
console.log(chalk.red('Error: This assignment does not accept file uploads or is not published.'));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
selectedAssignment = candidate;
|
|
227
|
+
assignmentId = candidate.id;
|
|
228
|
+
console.log(chalk.green(`Success: Selected ${selectedAssignment.name}
|
|
229
|
+
`));
|
|
230
|
+
if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
|
|
231
|
+
const resubmit = await askConfirmation(rl, chalk.yellow('This assignment has already been submitted. Do you want to resubmit?'), true);
|
|
232
|
+
if (!resubmit) {
|
|
233
|
+
console.log(chalk.yellow('Select a different assignment.'));
|
|
234
|
+
assignmentId = null;
|
|
235
|
+
selectedAssignment = null;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
awaitingAssignment = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!courseId) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
56
245
|
}
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
continue;
|
|
246
|
+
|
|
247
|
+
if (courseId && assignmentId) {
|
|
248
|
+
break;
|
|
61
249
|
}
|
|
62
|
-
selectedCourse = selectableCourses[courseIndex];
|
|
63
|
-
courseId = selectedCourse.id;
|
|
64
|
-
console.log(chalk.green(`Success: Selected ${selectedCourse.name}\n`));
|
|
65
250
|
}
|
|
66
|
-
|
|
67
|
-
// Step
|
|
68
|
-
|
|
251
|
+
|
|
252
|
+
// Step 3: Choose file selection method and select files
|
|
253
|
+
let filePaths = [];
|
|
254
|
+
while (filePaths.length === 0) {
|
|
69
255
|
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
70
|
-
console.log(chalk.cyan.bold('
|
|
71
|
-
const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
|
|
72
|
-
'include[]=submission',
|
|
73
|
-
'order_by=due_at',
|
|
74
|
-
'per_page=100'
|
|
75
|
-
]);
|
|
76
|
-
if (!assignments || assignments.length === 0) {
|
|
77
|
-
console.log(chalk.red('Error: No assignments found for this course.'));
|
|
78
|
-
rl.close();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
console.log(chalk.cyan('-'.repeat(60)));
|
|
82
|
-
console.log(chalk.cyan.bold(`Found ${assignments.length} assignment(s):`));
|
|
83
|
-
// Show summary of assignment statuses
|
|
84
|
-
const submittedCount = assignments.filter(a => a.submission && a.submission.submitted_at).length;
|
|
85
|
-
const pendingCount = assignments.length - submittedCount;
|
|
86
|
-
const uploadableCount = assignments.filter(a =>
|
|
87
|
-
a.submission_types &&
|
|
88
|
-
a.submission_types.includes('online_upload') &&
|
|
89
|
-
a.workflow_state === 'published'
|
|
90
|
-
).length;
|
|
91
|
-
console.log(chalk.yellow(`Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads`));
|
|
256
|
+
console.log(chalk.cyan.bold('File Selection Method'));
|
|
92
257
|
console.log(chalk.cyan('-'.repeat(60)));
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
258
|
+
const courseLabel = selectedCourse?.name || `Course ${courseId}`;
|
|
259
|
+
const assignmentLabel = selectedAssignment?.name || `Assignment ${assignmentId}`;
|
|
260
|
+
console.log(chalk.white('Course: ') + chalk.bold(courseLabel));
|
|
261
|
+
console.log(chalk.white('Assignment: ') + chalk.bold(assignmentLabel));
|
|
262
|
+
console.log();
|
|
263
|
+
|
|
264
|
+
console.log(chalk.yellow('📁 Choose file selection method:'));
|
|
265
|
+
console.log(chalk.white('1. ') + chalk.cyan('Keyboard Navigator') + chalk.gray(' (NEW! - Use arrow keys and space bar to navigate and select)'));
|
|
266
|
+
console.log(chalk.white('2. ') + chalk.cyan('Text-based Selector') + chalk.gray(' (Traditional - Type filenames and wildcards)'));
|
|
267
|
+
console.log(chalk.white('3. ') + chalk.cyan('Basic Directory Listing') + chalk.gray(' (Simple - Select from numbered list)'));
|
|
268
|
+
|
|
269
|
+
let selectorChoice = '';
|
|
270
|
+
while (true) {
|
|
271
|
+
selectorChoice = await askQuestion(rl, chalk.bold.cyan(`
|
|
272
|
+
Choose method (1-3): `));
|
|
273
|
+
if (!selectorChoice) {
|
|
274
|
+
console.log(chalk.yellow('Please choose option 1, 2, or 3.'));
|
|
275
|
+
continue;
|
|
110
276
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
line += chalk.red(' [No file uploads]');
|
|
277
|
+
if (['1', '2', '3'].includes(selectorChoice)) {
|
|
278
|
+
break;
|
|
114
279
|
}
|
|
115
|
-
console.log(
|
|
116
|
-
});
|
|
117
|
-
const assignmentChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter assignment number (or ".."/"back" to re-select course): '));
|
|
118
|
-
if (assignmentChoice === '..' || assignmentChoice.toLowerCase() === 'back') {
|
|
119
|
-
courseId = null;
|
|
120
|
-
selectedCourse = null;
|
|
121
|
-
break;
|
|
280
|
+
console.log(chalk.red('Invalid option. Please enter 1, 2, or 3.'));
|
|
122
281
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
rl
|
|
126
|
-
|
|
282
|
+
|
|
283
|
+
if (selectorChoice === '1') {
|
|
284
|
+
filePaths = await selectFilesKeyboard(rl);
|
|
285
|
+
} else if (selectorChoice === '2') {
|
|
286
|
+
filePaths = await selectFilesImproved(rl);
|
|
287
|
+
} else {
|
|
288
|
+
filePaths = await selectFiles(rl);
|
|
127
289
|
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
console.log(chalk.
|
|
290
|
+
|
|
291
|
+
if (!filePaths || filePaths.length === 0) {
|
|
292
|
+
console.log(chalk.yellow('No files selected.'));
|
|
293
|
+
const retrySelection = await askConfirmation(rl, chalk.yellow('Do you want to try selecting files again?'), true);
|
|
294
|
+
if (!retrySelection) {
|
|
295
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
131
298
|
continue;
|
|
132
299
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
300
|
+
|
|
301
|
+
const validFiles = [];
|
|
302
|
+
for (const file of filePaths) {
|
|
303
|
+
if (fs.existsSync(file)) {
|
|
304
|
+
validFiles.push(file);
|
|
305
|
+
} else {
|
|
306
|
+
console.log(chalk.red('Error: File not found: ' + file));
|
|
307
|
+
}
|
|
138
308
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
309
|
+
|
|
310
|
+
if (validFiles.length === 0) {
|
|
311
|
+
console.log(chalk.red('Error: No valid files selected.'));
|
|
312
|
+
const retrySelection = await askConfirmation(rl, chalk.yellow('Try selecting files again?'), true);
|
|
313
|
+
if (!retrySelection) {
|
|
144
314
|
console.log(chalk.yellow('Submission cancelled.'));
|
|
145
|
-
rl.close();
|
|
146
315
|
return;
|
|
147
316
|
}
|
|
317
|
+
filePaths = [];
|
|
318
|
+
continue;
|
|
148
319
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
152
|
-
console.log(chalk.cyan.bold('File Selection Method'));
|
|
153
|
-
console.log(chalk.cyan('-'.repeat(60)));
|
|
154
|
-
console.log(chalk.white('Course: ') + chalk.bold(selectedCourse.name));
|
|
155
|
-
console.log(chalk.white('Assignment: ') + chalk.bold(selectedAssignment.name) + '\n');
|
|
156
|
-
|
|
157
|
-
console.log(chalk.yellow('📁 Choose file selection method:'));
|
|
158
|
-
console.log(chalk.white('1. ') + chalk.cyan('Enhanced Interactive Selector') + chalk.gray(' (Recommended - Visual browser with search, filters, and navigation)'));
|
|
159
|
-
console.log(chalk.white('2. ') + chalk.cyan('Text-based Selector') + chalk.gray(' (Traditional - Type filenames and wildcards)'));
|
|
160
|
-
console.log(chalk.white('3. ') + chalk.cyan('Basic Directory Listing') + chalk.gray(' (Simple - Select from numbered list)'));
|
|
161
|
-
|
|
162
|
-
const selectorChoice = await askQuestion(rl, chalk.bold.cyan('\nChoose method (1-3): '));
|
|
163
|
-
|
|
164
|
-
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
165
|
-
console.log(chalk.cyan.bold('File Selection'));
|
|
166
|
-
console.log(chalk.cyan('-'.repeat(60)));
|
|
167
|
-
|
|
168
|
-
if (selectorChoice === '1') {
|
|
169
|
-
filePaths = await selectFilesInteractive(rl);
|
|
170
|
-
} else if (selectorChoice === '2') {
|
|
171
|
-
filePaths = await selectFilesImproved(rl);
|
|
172
|
-
} else {
|
|
173
|
-
filePaths = await selectFiles(rl);
|
|
174
|
-
}
|
|
175
|
-
// Validate all selected files exist
|
|
176
|
-
const validFiles = [];
|
|
177
|
-
for (const file of filePaths) {
|
|
178
|
-
if (fs.existsSync(file)) {
|
|
179
|
-
validFiles.push(file);
|
|
180
|
-
} else {
|
|
181
|
-
console.log(chalk.red('Error: File not found: ' + file));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (validFiles.length === 0) {
|
|
185
|
-
console.log(chalk.red('Error: No valid files selected.'));
|
|
186
|
-
rl.close();
|
|
187
|
-
return;
|
|
320
|
+
|
|
321
|
+
filePaths = validFiles;
|
|
188
322
|
}
|
|
189
|
-
|
|
323
|
+
|
|
190
324
|
// Step 4: Confirm and Submit
|
|
191
325
|
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
192
326
|
console.log(chalk.cyan.bold('Submission Summary:'));
|
|
193
327
|
console.log(chalk.cyan('-'.repeat(60)));
|
|
194
|
-
|
|
195
|
-
|
|
328
|
+
const courseSummary = selectedCourse?.name || `Course ${courseId}`;
|
|
329
|
+
const assignmentSummary = selectedAssignment?.name || `Assignment ${assignmentId}`;
|
|
330
|
+
console.log(chalk.white('Course: ') + chalk.bold(courseSummary));
|
|
331
|
+
console.log(chalk.white('Assignment: ') + chalk.bold(assignmentSummary));
|
|
196
332
|
console.log(chalk.white(`Files (${filePaths.length}):`));
|
|
197
333
|
filePaths.forEach((file, index) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
334
|
+
try {
|
|
335
|
+
const stats = fs.statSync(file);
|
|
336
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
337
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.red(' [unavailable]'));
|
|
340
|
+
}
|
|
201
341
|
});
|
|
202
|
-
const confirm = await askConfirmation(rl, chalk.bold.cyan(
|
|
342
|
+
const confirm = await askConfirmation(rl, chalk.bold.cyan(`
|
|
343
|
+
Proceed with submission?`), true, { requireExplicit: true });
|
|
203
344
|
if (!confirm) {
|
|
204
345
|
console.log(chalk.yellow('Submission cancelled.'));
|
|
205
|
-
rl.close();
|
|
206
346
|
return;
|
|
207
347
|
}
|
|
208
|
-
|
|
348
|
+
|
|
349
|
+
console.log(chalk.cyan.bold(`
|
|
350
|
+
Uploading files, please wait...`));
|
|
209
351
|
// Upload all files
|
|
210
352
|
const uploadedFileIds = [];
|
|
211
353
|
for (let i = 0; i < filePaths.length; i++) {
|
|
@@ -216,11 +358,13 @@ export async function submitAssignment(options) {
|
|
|
216
358
|
uploadedFileIds.push(fileId);
|
|
217
359
|
console.log(chalk.green('Success: Uploaded.'));
|
|
218
360
|
} catch (error) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
361
|
+
if (error && error.message && error.message.includes('filetype not allowed')) {
|
|
362
|
+
console.error(chalk.red(`Error: File type not allowed for ${currentFile}. Please select a permitted file type.`));
|
|
363
|
+
} else {
|
|
364
|
+
console.error(chalk.red(`Error: Failed to upload ${currentFile}: ${error.message}`));
|
|
223
365
|
}
|
|
366
|
+
const continueUpload = await askConfirmation(rl, chalk.yellow('Continue with remaining files?'), true);
|
|
367
|
+
if (!continueUpload) break;
|
|
224
368
|
}
|
|
225
369
|
}
|
|
226
370
|
// Submit assignment with uploaded files
|
|
@@ -235,6 +379,7 @@ export async function submitAssignment(options) {
|
|
|
235
379
|
} else {
|
|
236
380
|
console.log(chalk.red('Error: No files were uploaded. Submission not completed.'));
|
|
237
381
|
}
|
|
382
|
+
|
|
238
383
|
} catch (error) {
|
|
239
384
|
console.error(chalk.red('Error: Submission failed: ' + error.message));
|
|
240
385
|
} finally {
|
|
@@ -246,119 +391,213 @@ async function selectFiles(rl) {
|
|
|
246
391
|
console.log('📁 File selection options:');
|
|
247
392
|
console.log('1. Enter file path(s) manually');
|
|
248
393
|
console.log('2. Select from current directory');
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
394
|
+
|
|
395
|
+
while (true) {
|
|
396
|
+
const fileChoice = await askQuestion(rl, '\nChoose option (1-2): ');
|
|
397
|
+
|
|
398
|
+
if (!fileChoice) {
|
|
399
|
+
console.log('Please choose option 1 or 2.');
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (fileChoice === '1') {
|
|
404
|
+
return await selectFilesManually(rl);
|
|
405
|
+
}
|
|
406
|
+
if (fileChoice === '2') {
|
|
407
|
+
return await selectFilesFromDirectory(rl);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
console.log('Invalid option. Please enter 1 or 2.');
|
|
259
411
|
}
|
|
260
412
|
}
|
|
261
413
|
|
|
262
414
|
async function selectFilesManually(rl) {
|
|
263
|
-
|
|
415
|
+
let mode = '';
|
|
416
|
+
while (true) {
|
|
417
|
+
mode = await askQuestion(rl, 'Submit single file or multiple files? (s/m): ');
|
|
418
|
+
if (!mode) {
|
|
419
|
+
mode = 's';
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
const normalized = mode.toLowerCase();
|
|
423
|
+
if (normalized === 's' || normalized === 'single') {
|
|
424
|
+
mode = 's';
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
if (normalized === 'm' || normalized === 'multiple') {
|
|
428
|
+
mode = 'm';
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
console.log('Please enter "s" for single or "m" for multiple.');
|
|
432
|
+
}
|
|
433
|
+
|
|
264
434
|
const filePaths = [];
|
|
265
|
-
|
|
266
|
-
if (singleOrMultiple.toLowerCase() === 'm' || singleOrMultiple.toLowerCase() === 'multiple') {
|
|
435
|
+
if (mode === 'm') {
|
|
267
436
|
console.log('Enter file paths (one per line). Press Enter on empty line to finish:');
|
|
268
|
-
let fileInput = '';
|
|
269
437
|
while (true) {
|
|
270
|
-
fileInput = await askQuestion(rl, 'File path: ');
|
|
271
|
-
if (fileInput
|
|
438
|
+
const fileInput = await askQuestion(rl, 'File path: ');
|
|
439
|
+
if (!fileInput) {
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
272
442
|
filePaths.push(fileInput);
|
|
273
443
|
}
|
|
274
444
|
} else {
|
|
275
|
-
|
|
276
|
-
|
|
445
|
+
while (filePaths.length === 0) {
|
|
446
|
+
const singleFile = await askQuestion(rl, 'Enter file path: ');
|
|
447
|
+
if (!singleFile) {
|
|
448
|
+
console.log('File path cannot be empty.');
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
filePaths.push(singleFile);
|
|
452
|
+
}
|
|
277
453
|
}
|
|
278
|
-
|
|
454
|
+
|
|
279
455
|
return filePaths;
|
|
280
456
|
}
|
|
281
457
|
|
|
282
458
|
async function selectFilesFromDirectory(rl) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
459
|
+
while (true) {
|
|
460
|
+
let files;
|
|
461
|
+
try {
|
|
462
|
+
files = fs.readdirSync('.').filter(file => {
|
|
463
|
+
try {
|
|
464
|
+
return (
|
|
465
|
+
fs.statSync(file).isFile() &&
|
|
466
|
+
!file.startsWith('.') &&
|
|
467
|
+
file !== 'package.json' &&
|
|
468
|
+
file !== 'README.md'
|
|
469
|
+
);
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
} catch (error) {
|
|
475
|
+
console.log('Error reading directory: ' + error.message);
|
|
476
|
+
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
477
|
+
return manualFile ? [manualFile] : [];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!files || files.length === 0) {
|
|
292
481
|
console.log('No suitable files found in current directory.');
|
|
293
482
|
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
294
|
-
return [manualFile];
|
|
483
|
+
return manualFile ? [manualFile] : [];
|
|
295
484
|
}
|
|
296
|
-
|
|
485
|
+
|
|
297
486
|
console.log('\nFiles in current directory:');
|
|
298
487
|
files.forEach((file, index) => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
488
|
+
try {
|
|
489
|
+
const stats = fs.statSync(file);
|
|
490
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
491
|
+
console.log(`${index + 1}. ${file} (${size})`);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.log(`${index + 1}. ${file} (unavailable)`);
|
|
494
|
+
}
|
|
302
495
|
});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
496
|
+
|
|
497
|
+
while (true) {
|
|
498
|
+
const multipleFiles = await askQuestion(rl, '\nSelect multiple files? (y/N): ');
|
|
499
|
+
if (!multipleFiles) {
|
|
500
|
+
const selection = await selectSingleFileFromList(rl, files);
|
|
501
|
+
if (selection.length > 0) {
|
|
502
|
+
return selection;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
const normalized = multipleFiles.toLowerCase();
|
|
506
|
+
if (normalized === 'y' || normalized === 'yes') {
|
|
507
|
+
const selection = await selectMultipleFilesFromList(rl, files);
|
|
508
|
+
if (selection.length > 0) {
|
|
509
|
+
return selection;
|
|
510
|
+
}
|
|
511
|
+
} else if (normalized === 'n' || normalized === 'no') {
|
|
512
|
+
const selection = await selectSingleFileFromList(rl, files);
|
|
513
|
+
if (selection.length > 0) {
|
|
514
|
+
return selection;
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
console.log('Please answer with y or n.');
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
console.log('No files selected. Returning to file list.');
|
|
523
|
+
break;
|
|
310
524
|
}
|
|
311
|
-
} catch (error) {
|
|
312
|
-
console.log('Error reading directory.');
|
|
313
|
-
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
314
|
-
return [manualFile];
|
|
315
525
|
}
|
|
316
526
|
}
|
|
317
527
|
|
|
318
528
|
async function selectMultipleFilesFromList(rl, files) {
|
|
319
|
-
console.log('Enter file numbers separated by commas (e.g., 1,3,5) or ranges (e.g., 1-3)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
529
|
+
console.log('Enter file numbers separated by commas (e.g., 1,3,5) or ranges (e.g., 1-3). Type ".." or "back" to cancel.');
|
|
530
|
+
while (true) {
|
|
531
|
+
const fileIndices = await askQuestion(rl, 'File numbers: ');
|
|
532
|
+
if (!fileIndices) {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
if (fileIndices === '..' || fileIndices.toLowerCase() === 'back') {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const selectedIndices = [];
|
|
540
|
+
const parts = fileIndices.split(',');
|
|
541
|
+
|
|
542
|
+
for (const part of parts) {
|
|
543
|
+
const trimmed = part.trim();
|
|
544
|
+
if (!trimmed) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (trimmed.includes('-')) {
|
|
548
|
+
const [startStr, endStr] = trimmed.split('-').map(n => n.trim());
|
|
549
|
+
const start = Number.parseInt(startStr, 10);
|
|
550
|
+
const end = Number.parseInt(endStr, 10);
|
|
551
|
+
if (Number.isNaN(start) || Number.isNaN(end)) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const rangeStart = Math.min(start, end);
|
|
555
|
+
const rangeEnd = Math.max(start, end);
|
|
556
|
+
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
557
|
+
selectedIndices.push(i - 1);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
561
|
+
if (!Number.isNaN(parsed)) {
|
|
562
|
+
selectedIndices.push(parsed - 1);
|
|
563
|
+
}
|
|
332
564
|
}
|
|
333
|
-
} else {
|
|
334
|
-
// Handle single number
|
|
335
|
-
selectedIndices.push(parseInt(trimmed) - 1); // Convert to 0-based index
|
|
336
565
|
}
|
|
566
|
+
|
|
567
|
+
const uniqueIndices = [...new Set(selectedIndices)].filter(
|
|
568
|
+
index => index >= 0 && index < files.length
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (uniqueIndices.length === 0) {
|
|
572
|
+
console.log('No valid file selections. Please try again.');
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return uniqueIndices.map(index => files[index]);
|
|
337
577
|
}
|
|
338
|
-
|
|
339
|
-
// Remove duplicates and filter valid indices
|
|
340
|
-
const uniqueIndices = [...new Set(selectedIndices)].filter(
|
|
341
|
-
index => index >= 0 && index < files.length
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
if (uniqueIndices.length === 0) {
|
|
345
|
-
console.log('No valid file selections.');
|
|
346
|
-
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
347
|
-
return [manualFile];
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return uniqueIndices.map(index => files[index]);
|
|
351
578
|
}
|
|
352
579
|
|
|
353
580
|
async function selectSingleFileFromList(rl, files) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
581
|
+
while (true) {
|
|
582
|
+
const fileIndex = await askQuestion(rl, '\nEnter file number (or ".." to cancel): ');
|
|
583
|
+
if (!fileIndex) {
|
|
584
|
+
console.log('Please enter a file number or ".." to cancel.');
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (fileIndex === '..' || fileIndex.toLowerCase() === 'back') {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const selectedFileIndex = Number.parseInt(fileIndex, 10) - 1;
|
|
592
|
+
if (Number.isNaN(selectedFileIndex)) {
|
|
593
|
+
console.log('Please enter a valid number.');
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (selectedFileIndex >= 0 && selectedFileIndex < files.length) {
|
|
598
|
+
return [files[selectedFileIndex]];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
console.log('Invalid file selection. Please try again.');
|
|
363
602
|
}
|
|
364
603
|
}
|