canvaslms-cli 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  - Remove files from selection
16
16
  - Show currently selected files
17
17
  - Smart file filtering (excludes hidden files, package files)
18
+ - **Wildcard support**: Use patterns like *.html, *.js, *.pdf to select multiple files
19
+ - File type icons for better visual identification
18
20
 
19
21
  - **Improved Grade Viewing**:
20
22
  - Interactive course selection for grade viewing
@@ -41,6 +43,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
43
  - **Announcements Command**: Show course names instead of IDs
42
44
  - **User Experience**: More consistent and intuitive interfaces across all commands
43
45
 
46
+ ### Fixed
47
+
48
+ - **Assignment Name Display**: Fixed "Unknown Assignment" issue in submission summary
49
+ - **File Selection Flow**: Better error handling and user guidance during file selection
50
+ - **Variable Scope**: Proper assignment variable handling throughout submission process
51
+
44
52
  ### Technical
45
53
 
46
54
  - Enhanced interactive utilities in `lib/interactive.js`
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { makeCanvasRequest } = require('../lib/api-client');
8
- const { createReadlineInterface, askQuestion } = require('../lib/interactive');
8
+ const { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved } = require('../lib/interactive');
9
9
  const { uploadSingleFileToCanvas, submitAssignmentWithFiles } = require('../lib/file-upload');
10
10
 
11
11
  async function submitAssignment(options) {
@@ -130,9 +130,7 @@ async function submitAssignment(options) {
130
130
  console.log(` šŸ“‹ Note: This assignment doesn't accept file uploads`);
131
131
  }
132
132
  });
133
-
134
- const assignmentChoice = await askQuestion(rl, '\nEnter assignment number: ');
135
- const assignmentIndex = parseInt(assignmentChoice) - 1;
133
+ const assignmentIndex = parseInt(assignmentChoice) - 1;
136
134
 
137
135
  if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
138
136
  console.log('Invalid assignment selection.');
@@ -140,7 +138,7 @@ async function submitAssignment(options) {
140
138
  return;
141
139
  }
142
140
 
143
- const selectedAssignment = assignments[assignmentIndex];
141
+ selectedAssignment = assignments[assignmentIndex];
144
142
 
145
143
  // Check if assignment accepts file uploads
146
144
  if (!selectedAssignment.submission_types ||
@@ -156,8 +154,8 @@ async function submitAssignment(options) {
156
154
 
157
155
  // Check if already submitted
158
156
  if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
159
- const resubmit = await askQuestion(rl, 'This assignment has already been submitted. Do you want to resubmit? (y/N): ');
160
- if (resubmit.toLowerCase() !== 'y' && resubmit.toLowerCase() !== 'yes') {
157
+ const resubmit = await askConfirmation(rl, 'This assignment has already been submitted. Do you want to resubmit?', false);
158
+ if (!resubmit) {
161
159
  console.log('Submission cancelled.');
162
160
  rl.close();
163
161
  return;
@@ -167,16 +165,20 @@ async function submitAssignment(options) {
167
165
  // Fetch assignment details if ID was provided
168
166
  try {
169
167
  selectedAssignment = await makeCanvasRequest('get', `courses/${courseId}/assignments/${assignmentId}`);
168
+ console.log(`āœ… Using assignment: ${selectedAssignment.name}\n`);
170
169
  } catch (error) {
171
170
  console.log(`āš ļø Could not fetch assignment details for ID ${assignmentId}`);
172
171
  selectedAssignment = { id: assignmentId, name: `Assignment ${assignmentId}` };
173
172
  }
174
- }
175
- // Step 3: Select Files (if not provided)
173
+ } // Step 3: Select Files (if not provided)
176
174
  let filePaths = [];
177
175
  if (filePath) {
178
176
  filePaths = [filePath]; // Single file provided via option
179
177
  } else {
178
+ console.log('\nšŸ“ File Selection');
179
+ console.log(`šŸ“š Course: ${selectedCourse.name}`);
180
+ console.log(`šŸ“ Assignment: ${selectedAssignment.name}\n`);
181
+
180
182
  filePaths = await selectFilesImproved(rl);
181
183
  }
182
184
 
@@ -197,7 +199,8 @@ async function submitAssignment(options) {
197
199
  }
198
200
 
199
201
  filePaths = validFiles;
200
- // Step 4: Confirm and Submit
202
+
203
+ // Step 4: Confirm and Submit
201
204
  console.log('\nšŸ“‹ Submission Summary:');
202
205
  console.log(`šŸ“š Course: ${selectedCourse?.name || 'Unknown Course'}`);
203
206
  console.log(`šŸ“ Assignment: ${selectedAssignment?.name || 'Unknown Assignment'}`);
@@ -208,8 +211,8 @@ async function submitAssignment(options) {
208
211
  console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
209
212
  });
210
213
 
211
- const confirm = await askQuestion(rl, '\nProceed with submission? (Y/n): ');
212
- if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
214
+ const confirm = await askConfirmation(rl, '\nProceed with submission?', true);
215
+ if (!confirm) {
213
216
  console.log('Submission cancelled.');
214
217
  rl.close();
215
218
  return;
@@ -226,11 +229,10 @@ async function submitAssignment(options) {
226
229
  try {
227
230
  const fileId = await uploadSingleFileToCanvas(courseId, assignmentId, currentFile);
228
231
  uploadedFileIds.push(fileId);
229
- console.log(`āœ… ${path.basename(currentFile)} uploaded successfully`);
230
- } catch (error) {
232
+ console.log(`āœ… ${path.basename(currentFile)} uploaded successfully`); } catch (error) {
231
233
  console.error(`āŒ Failed to upload ${currentFile}: ${error.message}`);
232
- const continueUpload = await askQuestion(rl, 'Continue with remaining files? (Y/n): ');
233
- if (continueUpload.toLowerCase() === 'n' || continueUpload.toLowerCase() === 'no') {
234
+ const continueUpload = await askConfirmation(rl, 'Continue with remaining files?', true);
235
+ if (!continueUpload) {
234
236
  break;
235
237
  }
236
238
  }
@@ -378,127 +380,6 @@ async function selectSingleFileFromList(rl, files) {
378
380
  }
379
381
  }
380
382
 
381
- async function selectFilesImproved(rl) {
382
- console.log('šŸ“ Enhanced File Selection');
383
- console.log('Choose files by entering their paths or browse current directory');
384
- console.log('Press Enter with no input when done selecting files.\n');
385
-
386
- const allFiles = [];
387
- let fileIndex = 1;
388
-
389
- while (true) {
390
- console.log(`\nšŸ“Ž File ${fileIndex} selection:`);
391
- console.log('1. Enter file path directly');
392
- console.log('2. Browse current directory');
393
- console.log('3. Show currently selected files');
394
- console.log('(Press Enter with no input to finish selection)');
395
-
396
- const choice = await askQuestion(rl, '\nChoose option (1-3 or Enter to finish): ');
397
-
398
- // Empty input means we're done selecting files
399
- if (choice.trim() === '') {
400
- if (allFiles.length === 0) {
401
- console.log('āš ļø No files selected. Please select at least one file.');
402
- continue;
403
- }
404
- break;
405
- }
406
-
407
- if (choice === '1') {
408
- // Direct file path entry
409
- const filePath = await askQuestion(rl, 'Enter file path: ');
410
- if (filePath.trim() !== '') {
411
- if (fs.existsSync(filePath.trim())) {
412
- if (!allFiles.includes(filePath.trim())) {
413
- allFiles.push(filePath.trim());
414
- const stats = fs.statSync(filePath.trim());
415
- const size = (stats.size / 1024).toFixed(1) + ' KB';
416
- console.log(`āœ… Added: ${path.basename(filePath.trim())} (${size})`);
417
- fileIndex++;
418
- } else {
419
- console.log('āš ļø File already selected.');
420
- }
421
- } else {
422
- console.log('āŒ File not found. Please check the path.');
423
- }
424
- }
425
- } else if (choice === '2') {
426
- // Browse current directory
427
- try {
428
- const files = fs.readdirSync('.').filter(file => {
429
- const stats = fs.statSync(file);
430
- return stats.isFile() &&
431
- !file.startsWith('.') &&
432
- !['package.json', 'package-lock.json', 'node_modules'].includes(file);
433
- });
434
-
435
- if (files.length === 0) {
436
- console.log('No suitable files found in current directory.');
437
- continue;
438
- }
439
-
440
- console.log('\nšŸ“‚ Files in current directory:');
441
- files.forEach((file, index) => {
442
- const stats = fs.statSync(file);
443
- const size = (stats.size / 1024).toFixed(1) + ' KB';
444
- const alreadySelected = allFiles.includes(file) ? ' āœ…' : '';
445
- console.log(`${index + 1}. ${file} (${size})${alreadySelected}`);
446
- });
447
-
448
- const fileChoice = await askQuestion(rl, '\nEnter file number (or Enter to go back): ');
449
- if (fileChoice.trim() !== '') {
450
- const fileIdx = parseInt(fileChoice) - 1;
451
- if (fileIdx >= 0 && fileIdx < files.length) {
452
- const selectedFile = files[fileIdx];
453
- if (!allFiles.includes(selectedFile)) {
454
- allFiles.push(selectedFile);
455
- const stats = fs.statSync(selectedFile);
456
- const size = (stats.size / 1024).toFixed(1) + ' KB';
457
- console.log(`āœ… Added: ${selectedFile} (${size})`);
458
- fileIndex++;
459
- } else {
460
- console.log('āš ļø File already selected.');
461
- }
462
- } else {
463
- console.log('āŒ Invalid file number.');
464
- }
465
- }
466
- } catch (error) {
467
- console.log('āŒ Error reading directory:', error.message);
468
- }
469
- } else if (choice === '3') {
470
- // Show currently selected files
471
- if (allFiles.length === 0) {
472
- console.log('šŸ“‹ No files selected yet.');
473
- } else {
474
- console.log(`\nšŸ“‹ Currently selected files (${allFiles.length}):`);
475
- allFiles.forEach((file, index) => {
476
- const stats = fs.existsSync(file) ? fs.statSync(file) : null;
477
- const size = stats ? (stats.size / 1024).toFixed(1) + ' KB' : 'File not found';
478
- console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
479
- });
480
-
481
- const removeFile = await askQuestion(rl, '\nRemove a file? Enter number or press Enter to continue: ');
482
- if (removeFile.trim() !== '') {
483
- const removeIdx = parseInt(removeFile) - 1;
484
- if (removeIdx >= 0 && removeIdx < allFiles.length) {
485
- const removedFile = allFiles.splice(removeIdx, 1)[0];
486
- console.log(`šŸ—‘ļø Removed: ${path.basename(removedFile)}`);
487
- fileIndex--;
488
- } else {
489
- console.log('āŒ Invalid file number.');
490
- }
491
- }
492
- }
493
- } else {
494
- console.log('āŒ Invalid option. Please choose 1, 2, 3, or press Enter to finish.');
495
- }
496
- }
497
-
498
- console.log(`\nāœ… File selection complete! Selected ${allFiles.length} file(s).`);
499
- return allFiles;
500
- }
501
-
502
383
  module.exports = {
503
384
  submitAssignment
504
385
  };
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  const readline = require('readline');
6
+ const fs = require('fs');
7
+ const path = require('path');
6
8
 
7
9
  /**
8
10
  * Create readline interface for user input
@@ -98,10 +100,226 @@ async function selectFromList(rl, items, displayProperty = null, allowCancel = t
98
100
  return items[choice - 1];
99
101
  }
100
102
 
103
+ /**
104
+ * Get files matching a wildcard pattern
105
+ */
106
+ function getFilesMatchingWildcard(pattern, currentDir = process.cwd()) {
107
+ try {
108
+ const files = fs.readdirSync(currentDir);
109
+ const matchedFiles = [];
110
+
111
+ // Convert wildcard pattern to regex
112
+ let regexPattern;
113
+ if (pattern.startsWith('*.')) {
114
+ // Handle *.extension patterns
115
+ const extension = pattern.slice(2);
116
+ regexPattern = new RegExp(`\\.${extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
117
+ } else if (pattern.includes('*')) {
118
+ // Handle other wildcard patterns
119
+ regexPattern = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i');
120
+ } else {
121
+ // Exact match
122
+ regexPattern = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
123
+ }
124
+
125
+ files.forEach(file => {
126
+ const filePath = path.join(currentDir, file);
127
+ const stats = fs.statSync(filePath);
128
+
129
+ if (stats.isFile() && regexPattern.test(file)) {
130
+ matchedFiles.push(filePath);
131
+ }
132
+ });
133
+
134
+ return matchedFiles;
135
+ } catch (error) {
136
+ console.error(`Error reading directory: ${error.message}`);
137
+ return [];
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Enhanced file selection with wildcard support
143
+ */
144
+ async function selectFilesImproved(rl, currentDir = process.cwd()) {
145
+ const selectedFiles = [];
146
+
147
+ console.log('\nšŸ“ Enhanced File Selection');
148
+ console.log('šŸ’” Tips:');
149
+ console.log(' • Type filename to add individual files');
150
+ console.log(' • Use wildcards: *.html, *.js, *.pdf, etc.');
151
+ console.log(' • Type "browse" to see available files');
152
+ console.log(' • Type "remove" to remove files from selection');
153
+ console.log(' • Press Enter with no input to finish selection\n');
154
+
155
+ while (true) {
156
+ // Show current selection
157
+ if (selectedFiles.length > 0) {
158
+ console.log(`\nšŸ“‹ Currently selected (${selectedFiles.length} files):`);
159
+ selectedFiles.forEach((file, index) => {
160
+ const stats = fs.statSync(file);
161
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
162
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
163
+ });
164
+ }
165
+
166
+ const input = await askQuestion(rl, '\nšŸ“Ž Add file (or press Enter to finish): ');
167
+
168
+ if (!input.trim()) {
169
+ // Empty input - finish selection
170
+ break;
171
+ }
172
+
173
+ if (input.toLowerCase() === 'browse') {
174
+ // Show available files
175
+ console.log('\nšŸ“‚ Available files in current directory:');
176
+ try {
177
+ const files = fs.readdirSync(currentDir);
178
+ const filteredFiles = files.filter(file => {
179
+ const filePath = path.join(currentDir, file);
180
+ const stats = fs.statSync(filePath);
181
+ return stats.isFile() &&
182
+ !file.startsWith('.') &&
183
+ !['package.json', 'package-lock.json', 'node_modules'].includes(file);
184
+ });
185
+
186
+ if (filteredFiles.length === 0) {
187
+ console.log(' No suitable files found.');
188
+ } else {
189
+ filteredFiles.forEach((file, index) => {
190
+ const filePath = path.join(currentDir, file);
191
+ const stats = fs.statSync(filePath);
192
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
193
+ const ext = path.extname(file);
194
+ const icon = getFileIcon(ext);
195
+ console.log(` ${index + 1}. ${icon} ${file} (${size})`);
196
+ });
197
+ }
198
+ } catch (error) {
199
+ console.log(` Error reading directory: ${error.message}`);
200
+ }
201
+ continue;
202
+ }
203
+
204
+ if (input.toLowerCase() === 'remove') {
205
+ // Remove files from selection
206
+ if (selectedFiles.length === 0) {
207
+ console.log('āŒ No files selected to remove.');
208
+ continue;
209
+ }
210
+
211
+ console.log('\nSelect file to remove:');
212
+ selectedFiles.forEach((file, index) => {
213
+ console.log(`${index + 1}. ${path.basename(file)}`);
214
+ });
215
+
216
+ const removeChoice = await askQuestion(rl, '\nEnter number to remove (or press Enter to cancel): ');
217
+ if (removeChoice.trim()) {
218
+ const removeIndex = parseInt(removeChoice) - 1;
219
+ if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
220
+ const removedFile = selectedFiles.splice(removeIndex, 1)[0];
221
+ console.log(`āœ… Removed: ${path.basename(removedFile)}`);
222
+ } else {
223
+ console.log('āŒ Invalid selection.');
224
+ }
225
+ }
226
+ continue;
227
+ }
228
+
229
+ // Check if input contains wildcards
230
+ if (input.includes('*') || input.includes('?')) {
231
+ const matchedFiles = getFilesMatchingWildcard(input, currentDir);
232
+
233
+ if (matchedFiles.length === 0) {
234
+ console.log(`āŒ No files found matching pattern: ${input}`);
235
+ continue;
236
+ }
237
+
238
+ console.log(`\nšŸŽÆ Found ${matchedFiles.length} files matching "${input}":`);
239
+ matchedFiles.forEach((file, index) => {
240
+ const stats = fs.statSync(file);
241
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
242
+ console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
243
+ });
244
+
245
+ const confirmAll = await askConfirmation(rl, `Add all ${matchedFiles.length} files?`, true);
246
+
247
+ if (confirmAll) {
248
+ const newFiles = matchedFiles.filter(file => !selectedFiles.includes(file));
249
+ selectedFiles.push(...newFiles);
250
+ console.log(`āœ… Added ${newFiles.length} new files (${matchedFiles.length - newFiles.length} were already selected)`);
251
+ }
252
+ continue;
253
+ }
254
+
255
+ // Handle individual file selection
256
+ let filePath = input;
257
+
258
+ // If not absolute path, make it relative to current directory
259
+ if (!path.isAbsolute(filePath)) {
260
+ filePath = path.join(currentDir, filePath);
261
+ }
262
+
263
+ try {
264
+ if (!fs.existsSync(filePath)) {
265
+ console.log(`āŒ File not found: ${input}`);
266
+ continue;
267
+ }
268
+
269
+ const stats = fs.statSync(filePath);
270
+ if (!stats.isFile()) {
271
+ console.log(`āŒ Not a file: ${input}`);
272
+ continue;
273
+ }
274
+
275
+ if (selectedFiles.includes(filePath)) {
276
+ console.log(`āš ļø File already selected: ${path.basename(filePath)}`);
277
+ continue;
278
+ }
279
+
280
+ selectedFiles.push(filePath);
281
+ const size = (stats.size / 1024).toFixed(1) + ' KB';
282
+ console.log(`āœ… Added: ${path.basename(filePath)} (${size})`);
283
+
284
+ } catch (error) {
285
+ console.log(`āŒ Error accessing file: ${error.message}`);
286
+ }
287
+ }
288
+
289
+ return selectedFiles;
290
+ }
291
+
292
+ /**
293
+ * Get file icon based on extension
294
+ */
295
+ function getFileIcon(extension) {
296
+ const iconMap = {
297
+ '.js': 'šŸ“œ',
298
+ '.html': '🌐',
299
+ '.css': 'šŸŽØ',
300
+ '.pdf': 'šŸ“„',
301
+ '.doc': 'šŸ“',
302
+ '.docx': 'šŸ“',
303
+ '.txt': 'šŸ“„',
304
+ '.md': 'šŸ“–',
305
+ '.json': 'āš™ļø',
306
+ '.xml': 'šŸ“‹',
307
+ '.zip': 'šŸ“¦',
308
+ '.png': 'šŸ–¼ļø',
309
+ '.jpg': 'šŸ–¼ļø',
310
+ '.jpeg': 'šŸ–¼ļø',
311
+ '.gif': 'šŸ–¼ļø'
312
+ };
313
+
314
+ return iconMap[extension.toLowerCase()] || 'šŸ“Ž';
315
+ }
316
+
101
317
  module.exports = {
102
318
  createReadlineInterface,
103
319
  askQuestion,
104
320
  askQuestionWithValidation,
105
321
  askConfirmation,
106
- selectFromList
322
+ selectFromList,
323
+ selectFilesImproved,
324
+ getFilesMatchingWildcard
107
325
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvaslms-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "A command line tool for interacting with Canvas LMS API",
5
5
  "keywords": [
6
6
  "canvas",