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.
@@ -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, selectFilesInteractive, pad } from '../lib/interactive.js';
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
- // Step 1: Select Course (if not provided)
22
- while (!courseId) {
23
- console.log(chalk.cyan.bold('\n' + '-'.repeat(60)));
24
- console.log(chalk.cyan.bold('Loading your courses, please wait...'));
25
- const courses = await makeCanvasRequest('get', 'courses', [
26
- 'enrollment_state=active',
27
- 'include[]=favorites'
28
- ]);
29
- if (!courses || courses.length === 0) {
30
- console.log(chalk.red('Error: No courses found.'));
31
- rl.close();
32
- return;
33
- }
34
- let selectableCourses = courses;
35
- if (!options.all) {
36
- selectableCourses = courses.filter(course => course.is_favorite);
37
- if (selectableCourses.length === 0) {
38
- console.log(chalk.red('Error: No starred courses found. Showing all enrolled courses...'));
39
- selectableCourses = courses;
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
- console.log(chalk.cyan('-'.repeat(60)));
43
- console.log(chalk.cyan.bold('Select a course:'));
44
- selectableCourses.forEach((course, index) => {
45
- console.log(pad(chalk.white((index + 1) + '. '), 5) + chalk.white(course.name));
46
- });
47
- const courseChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter course number (or ".."/"back" to cancel): '));
48
- if (courseChoice === '..' || courseChoice.toLowerCase() === 'back') {
49
- rl.close();
50
- return;
51
- }
52
- if (!courseChoice.trim()) {
53
- console.log(chalk.red('Error: No course selected. Exiting...'));
54
- rl.close();
55
- return;
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
- const courseIndex = parseInt(courseChoice) - 1;
58
- if (courseIndex < 0 || courseIndex >= selectableCourses.length) {
59
- console.log(chalk.red('Error: Invalid course selection.'));
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 2: Select Assignment (if not provided)
68
- while (!assignmentId) {
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('Loading assignments, please wait...'));
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
- // Numbered menu
94
- assignments.forEach((assignment, index) => {
95
- const dueDate = assignment.due_at ? new Date(assignment.due_at).toLocaleDateString() : 'No due date';
96
- const submitted = assignment.submission && assignment.submission.submitted_at ? chalk.green('Submitted') : chalk.yellow('Not submitted');
97
- const canSubmitFiles = assignment.submission_types && assignment.submission_types.includes('online_upload') && assignment.workflow_state === 'published';
98
- let gradeDisplay = '';
99
- const submission = assignment.submission;
100
- if (submission && submission.score !== null && submission.score !== undefined) {
101
- const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
102
- const total = assignment.points_possible || 0;
103
- gradeDisplay = ` | Grade: ${score}/${total}`;
104
- } else if (submission && submission.excused) {
105
- gradeDisplay = ' | Grade: Excused';
106
- } else if (submission && submission.missing) {
107
- gradeDisplay = ' | Grade: Missing';
108
- } else if (assignment.points_possible) {
109
- gradeDisplay = ` | Grade: –/${assignment.points_possible}`;
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
- let line = pad(chalk.white((index + 1) + '. '), 5) + chalk.white(assignment.name) + chalk.gray(` (${dueDate})`) + ' ' + submitted + gradeDisplay;
112
- if (!canSubmitFiles) {
113
- line += chalk.red(' [No file uploads]');
277
+ if (['1', '2', '3'].includes(selectorChoice)) {
278
+ break;
114
279
  }
115
- console.log(line);
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
- if (!assignmentChoice.trim()) {
124
- console.log(chalk.red('Error: No assignment selected. Exiting...'));
125
- rl.close();
126
- return;
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
- const assignmentIndex = parseInt(assignmentChoice) - 1;
129
- if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
130
- console.log(chalk.red('Error: Invalid assignment selection.'));
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
- selectedAssignment = assignments[assignmentIndex];
134
- if (!selectedAssignment.submission_types || !selectedAssignment.submission_types.includes('online_upload') || selectedAssignment.workflow_state !== 'published') {
135
- console.log(chalk.red('Error: This assignment does not accept file uploads or is not published.'));
136
- rl.close();
137
- return;
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
- assignmentId = selectedAssignment.id;
140
- console.log(chalk.green(`Success: Selected ${selectedAssignment.name}\n`));
141
- if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
142
- const resubmit = await askConfirmation(rl, chalk.yellow('This assignment has already been submitted. Do you want to resubmit?'), false);
143
- if (!resubmit) {
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
- } // Step 3: Choose file selection method and select files
150
- let filePaths = [];
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
- filePaths = validFiles;
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
- console.log(chalk.white('Course: ') + chalk.bold(selectedCourse?.name || 'Unknown Course'));
195
- console.log(chalk.white('Assignment: ') + chalk.bold(selectedAssignment?.name || 'Unknown Assignment'));
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
- const stats = fs.statSync(file);
199
- const size = (stats.size / 1024).toFixed(1) + ' KB';
200
- console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.gray(size));
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('\nProceed with submission?'), true);
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
- console.log(chalk.cyan.bold('\nUploading files, please wait...'));
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
- console.error(chalk.red(`Error: Failed to upload ${currentFile}: ${error.message}`));
220
- const continueUpload = await askConfirmation(rl, chalk.yellow('Continue with remaining files?'), true);
221
- if (!continueUpload) {
222
- break;
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
- const fileChoice = await askQuestion(rl, '\nChoose option (1-2): ');
251
-
252
- if (fileChoice === '1') {
253
- return await selectFilesManually(rl);
254
- } else if (fileChoice === '2') {
255
- return await selectFilesFromDirectory(rl);
256
- } else {
257
- console.log('Invalid option.');
258
- return [];
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
- const singleOrMultiple = await askQuestion(rl, 'Submit single file or multiple files? (s/m): ');
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 === '') break;
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
- const singleFile = await askQuestion(rl, 'Enter file path: ');
276
- filePaths.push(singleFile);
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
- try {
284
- const files = fs.readdirSync('.').filter(file =>
285
- fs.statSync(file).isFile() &&
286
- !file.startsWith('.') &&
287
- file !== 'package.json' &&
288
- file !== 'README.md'
289
- );
290
-
291
- if (files.length === 0) {
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
- const stats = fs.statSync(file);
300
- const size = (stats.size / 1024).toFixed(1) + ' KB';
301
- console.log(`${index + 1}. ${file} (${size})`);
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
- const multipleFiles = await askQuestion(rl, '\nSelect multiple files? (y/N): ');
305
-
306
- if (multipleFiles.toLowerCase() === 'y' || multipleFiles.toLowerCase() === 'yes') {
307
- return await selectMultipleFilesFromList(rl, files);
308
- } else {
309
- return await selectSingleFileFromList(rl, files);
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
- const fileIndices = await askQuestion(rl, 'File numbers: ');
321
-
322
- const selectedIndices = [];
323
- const parts = fileIndices.split(',');
324
-
325
- for (const part of parts) {
326
- const trimmed = part.trim();
327
- if (trimmed.includes('-')) {
328
- // Handle range (e.g., 1-3)
329
- const [start, end] = trimmed.split('-').map(n => parseInt(n.trim()));
330
- for (let i = start; i <= end; i++) {
331
- selectedIndices.push(i - 1); // Convert to 0-based index
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
- const fileIndex = await askQuestion(rl, '\nEnter file number: ');
355
- const selectedFileIndex = parseInt(fileIndex) - 1;
356
-
357
- if (selectedFileIndex >= 0 && selectedFileIndex < files.length) {
358
- return [files[selectedFileIndex]];
359
- } else {
360
- console.log('Invalid file selection.');
361
- const manualFile = await askQuestion(rl, 'Enter file path manually: ');
362
- return [manualFile];
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
  }