canvaslms-cli 1.2.0 ā 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/commands/submit.js +18 -16
- package/lib/interactive.js +219 -1
- package/package.json +1 -1
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`
|
package/commands/submit.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { makeCanvasRequest } = require('../lib/api-client');
|
|
8
|
-
const { createReadlineInterface, askQuestion } = require('../lib/interactive');
|
|
8
|
+
const { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved } = require('../lib/interactive');
|
|
9
9
|
const { uploadSingleFileToCanvas, submitAssignmentWithFiles } = require('../lib/file-upload');
|
|
10
10
|
|
|
11
11
|
async function submitAssignment(options) {
|
|
@@ -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
|
-
|
|
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
|
|
160
|
-
if (resubmit
|
|
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
|
-
|
|
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
|
|
212
|
-
if (confirm
|
|
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
|
|
233
|
-
if (continueUpload
|
|
234
|
+
const continueUpload = await askConfirmation(rl, 'Continue with remaining files?', true);
|
|
235
|
+
if (!continueUpload) {
|
|
234
236
|
break;
|
|
235
237
|
}
|
|
236
238
|
}
|
package/lib/interactive.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Create readline interface for user input
|
|
@@ -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
|
};
|