canvaslms-cli 1.4.3 → 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.
@@ -18,195 +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`)); if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
141
- const resubmit = await askConfirmation(rl, chalk.yellow('This assignment has already been submitted. Do you want to resubmit?'), true);
142
- 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) {
143
314
  console.log(chalk.yellow('Submission cancelled.'));
144
- rl.close();
145
315
  return;
146
316
  }
317
+ filePaths = [];
318
+ continue;
147
319
  }
320
+
321
+ filePaths = validFiles;
148
322
  }
149
-
150
- // Step 3: Choose file selection method and select files
151
- let filePaths = [];
152
- console.log(chalk.cyan.bold('-'.repeat(60)));
153
- console.log(chalk.cyan.bold('File Selection Method'));
154
- console.log(chalk.cyan('-'.repeat(60)));
155
- console.log(chalk.white('Course: ') + chalk.bold(selectedCourse.name));
156
- console.log(chalk.white('Assignment: ') + chalk.bold(selectedAssignment.name) + '\n');
157
-
158
- console.log(chalk.yellow('📁 Choose file selection method:'));
159
- console.log(chalk.white('1. ') + chalk.cyan('Keyboard Navigator') + chalk.gray(' (NEW! - Use arrow keys and space bar to navigate and select)'));
160
- console.log(chalk.white('2. ') + chalk.cyan('Text-based Selector') + chalk.gray(' (Traditional - Type filenames and wildcards)'));
161
- console.log(chalk.white('3. ') + chalk.cyan('Basic Directory Listing') + chalk.gray(' (Simple - Select from numbered list)'));
162
-
163
- const selectorChoice = await askQuestion(rl, chalk.bold.cyan('\nChoose method (1-3): '));
164
-
165
- console.log(chalk.cyan.bold('-'.repeat(60)));
166
- console.log(chalk.cyan.bold('File Selection'));
167
- console.log(chalk.cyan('-'.repeat(60)));
168
-
169
- if (selectorChoice === '1') {
170
- filePaths = await selectFilesKeyboard(rl);
171
- } else if (selectorChoice === '2') {
172
- filePaths = await selectFilesImproved(rl);
173
- } else {
174
- filePaths = await selectFiles(rl);
175
- }
176
- // Validate all selected files exist
177
- const validFiles = [];
178
- for (const file of filePaths) {
179
- if (fs.existsSync(file)) {
180
- validFiles.push(file);
181
- } else {
182
- console.log(chalk.red('Error: File not found: ' + file));
183
- }
184
- }
185
- if (validFiles.length === 0) {
186
- console.log(chalk.red('Error: No valid files selected.'));
187
- rl.close();
188
- return;
189
- }
190
- filePaths = validFiles;
323
+
191
324
  // Step 4: Confirm and Submit
192
325
  console.log(chalk.cyan.bold('-'.repeat(60)));
193
326
  console.log(chalk.cyan.bold('Submission Summary:'));
194
327
  console.log(chalk.cyan('-'.repeat(60)));
195
- console.log(chalk.white('Course: ') + chalk.bold(selectedCourse?.name || 'Unknown Course'));
196
- 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));
197
332
  console.log(chalk.white(`Files (${filePaths.length}):`));
198
333
  filePaths.forEach((file, index) => {
199
- const stats = fs.statSync(file);
200
- const size = (stats.size / 1024).toFixed(1) + ' KB';
201
- 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
+ }
202
341
  });
203
- 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 });
204
344
  if (!confirm) {
205
345
  console.log(chalk.yellow('Submission cancelled.'));
206
- rl.close();
207
346
  return;
208
347
  }
209
- console.log(chalk.cyan.bold('\nUploading files, please wait...'));
348
+
349
+ console.log(chalk.cyan.bold(`
350
+ Uploading files, please wait...`));
210
351
  // Upload all files
211
352
  const uploadedFileIds = [];
212
353
  for (let i = 0; i < filePaths.length; i++) {
@@ -217,11 +358,13 @@ export async function submitAssignment(options) {
217
358
  uploadedFileIds.push(fileId);
218
359
  console.log(chalk.green('Success: Uploaded.'));
219
360
  } catch (error) {
220
- console.error(chalk.red(`Error: Failed to upload ${currentFile}: ${error.message}`));
221
- const continueUpload = await askConfirmation(rl, chalk.yellow('Continue with remaining files?'), true);
222
- if (!continueUpload) {
223
- 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}`));
224
365
  }
366
+ const continueUpload = await askConfirmation(rl, chalk.yellow('Continue with remaining files?'), true);
367
+ if (!continueUpload) break;
225
368
  }
226
369
  }
227
370
  // Submit assignment with uploaded files
@@ -236,6 +379,7 @@ export async function submitAssignment(options) {
236
379
  } else {
237
380
  console.log(chalk.red('Error: No files were uploaded. Submission not completed.'));
238
381
  }
382
+
239
383
  } catch (error) {
240
384
  console.error(chalk.red('Error: Submission failed: ' + error.message));
241
385
  } finally {
@@ -247,119 +391,213 @@ async function selectFiles(rl) {
247
391
  console.log('📁 File selection options:');
248
392
  console.log('1. Enter file path(s) manually');
249
393
  console.log('2. Select from current directory');
250
-
251
- const fileChoice = await askQuestion(rl, '\nChoose option (1-2): ');
252
-
253
- if (fileChoice === '1') {
254
- return await selectFilesManually(rl);
255
- } else if (fileChoice === '2') {
256
- return await selectFilesFromDirectory(rl);
257
- } else {
258
- console.log('Invalid option.');
259
- 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.');
260
411
  }
261
412
  }
262
413
 
263
414
  async function selectFilesManually(rl) {
264
- 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
+
265
434
  const filePaths = [];
266
-
267
- if (singleOrMultiple.toLowerCase() === 'm' || singleOrMultiple.toLowerCase() === 'multiple') {
435
+ if (mode === 'm') {
268
436
  console.log('Enter file paths (one per line). Press Enter on empty line to finish:');
269
- let fileInput = '';
270
437
  while (true) {
271
- fileInput = await askQuestion(rl, 'File path: ');
272
- if (fileInput === '') break;
438
+ const fileInput = await askQuestion(rl, 'File path: ');
439
+ if (!fileInput) {
440
+ break;
441
+ }
273
442
  filePaths.push(fileInput);
274
443
  }
275
444
  } else {
276
- const singleFile = await askQuestion(rl, 'Enter file path: ');
277
- 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
+ }
278
453
  }
279
-
454
+
280
455
  return filePaths;
281
456
  }
282
457
 
283
458
  async function selectFilesFromDirectory(rl) {
284
- try {
285
- const files = fs.readdirSync('.').filter(file =>
286
- fs.statSync(file).isFile() &&
287
- !file.startsWith('.') &&
288
- file !== 'package.json' &&
289
- file !== 'README.md'
290
- );
291
-
292
- 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) {
293
481
  console.log('No suitable files found in current directory.');
294
482
  const manualFile = await askQuestion(rl, 'Enter file path manually: ');
295
- return [manualFile];
483
+ return manualFile ? [manualFile] : [];
296
484
  }
297
-
485
+
298
486
  console.log('\nFiles in current directory:');
299
487
  files.forEach((file, index) => {
300
- const stats = fs.statSync(file);
301
- const size = (stats.size / 1024).toFixed(1) + ' KB';
302
- 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
+ }
303
495
  });
304
-
305
- const multipleFiles = await askQuestion(rl, '\nSelect multiple files? (y/N): ');
306
-
307
- if (multipleFiles.toLowerCase() === 'y' || multipleFiles.toLowerCase() === 'yes') {
308
- return await selectMultipleFilesFromList(rl, files);
309
- } else {
310
- 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;
311
524
  }
312
- } catch (error) {
313
- console.log('Error reading directory.');
314
- const manualFile = await askQuestion(rl, 'Enter file path manually: ');
315
- return [manualFile];
316
525
  }
317
526
  }
318
527
 
319
528
  async function selectMultipleFilesFromList(rl, files) {
320
- console.log('Enter file numbers separated by commas (e.g., 1,3,5) or ranges (e.g., 1-3):');
321
- const fileIndices = await askQuestion(rl, 'File numbers: ');
322
-
323
- const selectedIndices = [];
324
- const parts = fileIndices.split(',');
325
-
326
- for (const part of parts) {
327
- const trimmed = part.trim();
328
- if (trimmed.includes('-')) {
329
- // Handle range (e.g., 1-3)
330
- const [start, end] = trimmed.split('-').map(n => parseInt(n.trim()));
331
- for (let i = start; i <= end; i++) {
332
- 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
+ }
333
564
  }
334
- } else {
335
- // Handle single number
336
- selectedIndices.push(parseInt(trimmed) - 1); // Convert to 0-based index
337
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]);
338
577
  }
339
-
340
- // Remove duplicates and filter valid indices
341
- const uniqueIndices = [...new Set(selectedIndices)].filter(
342
- index => index >= 0 && index < files.length
343
- );
344
-
345
- if (uniqueIndices.length === 0) {
346
- console.log('No valid file selections.');
347
- const manualFile = await askQuestion(rl, 'Enter file path manually: ');
348
- return [manualFile];
349
- }
350
-
351
- return uniqueIndices.map(index => files[index]);
352
578
  }
353
579
 
354
580
  async function selectSingleFileFromList(rl, files) {
355
- const fileIndex = await askQuestion(rl, '\nEnter file number: ');
356
- const selectedFileIndex = parseInt(fileIndex) - 1;
357
-
358
- if (selectedFileIndex >= 0 && selectedFileIndex < files.length) {
359
- return [files[selectedFileIndex]];
360
- } else {
361
- console.log('Invalid file selection.');
362
- const manualFile = await askQuestion(rl, 'Enter file path manually: ');
363
- 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.');
364
602
  }
365
603
  }
@@ -63,27 +63,38 @@ function askQuestionWithValidation(rl, question, validator, errorMessage) {
63
63
  /**
64
64
  * Ask for confirmation (Y/n format)
65
65
  */
66
- async function askConfirmation(rl, question, defaultYes = true) {
66
+ async function askConfirmation(rl, question, defaultYes = true, options = {}) {
67
+ const { requireExplicit = false } = options;
67
68
  const suffix = defaultYes ? " (Y/n)" : " (y/N)";
68
- const answer = await askQuestion(rl, question + suffix + ": ");
69
69
 
70
- const lower = answer.toLowerCase();
71
-
72
- // If user presses Enter → return defaultYes (true by default)
73
- if (lower === "") {
74
- return defaultYes;
75
- }
70
+ while (true) {
71
+ const answer = await askQuestion(rl, question + suffix + ": ");
72
+ const lower = answer.toLowerCase();
76
73
 
77
- // Convert input to boolean
78
- if (lower === "y" || lower === "yes") {
79
- return true;
80
- }
81
- if (lower === "n" || lower === "no") {
82
- return false;
83
- }
74
+ // If user presses Enter → return defaultYes (true by default)
75
+ if (lower === "") {
76
+ if (!requireExplicit) {
77
+ return defaultYes;
78
+ }
79
+ console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
80
+ continue;
81
+ }
82
+
83
+ // Convert input to boolean
84
+ if (lower === "y" || lower === "yes") {
85
+ return true;
86
+ }
87
+ if (lower === "n" || lower === "no") {
88
+ return false;
89
+ }
90
+
91
+ if (!requireExplicit) {
92
+ // If input is something else, fallback to defaultYes
93
+ return defaultYes;
94
+ }
84
95
 
85
- // If input is something else, fallback to defaultYes
86
- return defaultYes;
96
+ console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
97
+ }
87
98
  }
88
99
 
89
100
  /**
@@ -495,25 +506,23 @@ async function selectFilesKeyboard(rl, currentDir = process.cwd()) {
495
506
  return chalk.yellow('📂 ') + breadcrumb;
496
507
  }
497
508
 
509
+ // Helper to clear the terminal without dropping scrollback history
510
+ function safeClearScreen() {
511
+ if (!process.stdout.isTTY) return;
512
+
513
+ process.stdout.write('\x1b[H\x1b[J'); // Move cursor to top and clear below, not full console.clear()
514
+
515
+ }
516
+
498
517
  // Helper function to display the file browser
499
518
  function displayBrowser() {
500
- // Header with keyboard controls at the top
501
- console.log(chalk.cyan.bold('╭' + '─'.repeat(100) + '╮'));
502
- console.log(chalk.cyan.bold('│') + chalk.white.bold(' Keyboard File Selector'.padEnd(98)) + chalk.cyan.bold('│')); console.log(chalk.cyan.bold('├' + '─'.repeat(100) + '┤'));
519
+ safeClearScreen();
503
520
  // Keyboard controls - compact format at top
504
521
  const controls = [
505
- '↑↓←→:Navigate', 'Space:Select', 'Enter:Open/Finish', 'Backspace:Up', 'a:All', 'c:Clear', 'Esc:Exit'
506
522
  ];
507
- const controlLine = controls.map(c => {
508
- const [key, desc] = c.split(':');
509
- return chalk.white(key) + chalk.gray(':') + chalk.gray(desc);
510
- }).join(chalk.gray(' │ '));
511
- console.log(chalk.cyan.bold('│ ') + controlLine.padEnd(98) + chalk.cyan.bold(' │'));
512
- console.log(chalk.cyan.bold('╰' + '─'.repeat(100) + '╯'));
513
-
514
- console.log();
515
523
  // Breadcrumb path
516
524
  console.log(buildBreadcrumb());
525
+ console.log(chalk.gray('💡 ↑↓←→:Navigate', 'Space:Select', 'Enter:Open/Finish', 'Backspace:Up', 'a:All', 'c:Clear', 'Esc:Exit'));
517
526
 
518
527
  // Selected files count
519
528
  if (selectedFiles.length > 0) {
@@ -525,12 +534,10 @@ async function selectFilesKeyboard(rl, currentDir = process.cwd()) {
525
534
  }
526
535
  }, 0);
527
536
  console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB) - Press Enter to finish`));
528
- } else {
529
- console.log(chalk.gray('💡 Use Space to select files, then press Enter to finish'));
530
537
  }
531
-
538
+
532
539
  console.log();
533
-
540
+
534
541
  if (fileList.length === 0) {
535
542
  console.log(chalk.yellow('📭 No files found in this directory.'));
536
543
  return;
@@ -879,4 +886,4 @@ export {
879
886
  getFilesMatchingWildcard,
880
887
  getSubfoldersRecursive,
881
888
  pad
882
- };
889
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvaslms-cli",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "A command line tool for interacting with Canvas LMS API",
5
5
  "keywords": [
6
6
  "canvas",