canvaslms-cli 1.4.3 → 1.4.5
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/commands/submit.js +483 -245
- package/lib/interactive.js +41 -34
- package/package.json +2 -1
- package/src/index.js +1 -1
package/commands/submit.js
CHANGED
|
@@ -18,195 +18,336 @@ export async function submitAssignment(options) {
|
|
|
18
18
|
let selectedCourse = null;
|
|
19
19
|
let selectedAssignment = null;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
|
|
22
|
+
// Step 1 & 2: Select Course and Assignment (if not provided)
|
|
23
|
+
while (!courseId || !assignmentId) {
|
|
24
|
+
if (!courseId) {
|
|
25
|
+
let courses = null;
|
|
26
|
+
while (true) {
|
|
27
|
+
console.log(chalk.cyan.bold(`\n${'-'.repeat(60)}`));
|
|
28
|
+
console.log(chalk.cyan.bold('Loading your courses, please wait...'));
|
|
29
|
+
try {
|
|
30
|
+
courses = await makeCanvasRequest('get', 'courses', [
|
|
31
|
+
'enrollment_state=active',
|
|
32
|
+
'include[]=favorites'
|
|
33
|
+
]);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(chalk.red('Error: Failed to load courses: ' + error.message));
|
|
36
|
+
const retryCourses = await askConfirmation(rl, chalk.yellow('Try loading courses again?'), true);
|
|
37
|
+
if (retryCourses) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!courses || courses.length === 0) {
|
|
45
|
+
console.log(chalk.red('Error: No courses found.'));
|
|
46
|
+
const retryCourses = await askConfirmation(rl, chalk.yellow('Refresh course list?'), true);
|
|
47
|
+
if (retryCourses) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let selectableCourses = courses;
|
|
57
|
+
if (!options.all) {
|
|
58
|
+
selectableCourses = courses.filter(course => course.is_favorite);
|
|
59
|
+
if (selectableCourses.length === 0) {
|
|
60
|
+
console.log(chalk.yellow('No starred courses found. Showing all enrolled courses...'));
|
|
61
|
+
selectableCourses = courses;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
66
|
+
console.log(chalk.cyan.bold('Select a course:'));
|
|
67
|
+
selectableCourses.forEach((course, index) => {
|
|
68
|
+
console.log(pad(chalk.white((index + 1) + '. '), 5) + chalk.white(course.name));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
while (!courseId) {
|
|
72
|
+
const courseChoice = await askQuestion(rl, chalk.bold.cyan(`
|
|
73
|
+
Enter course number (or ".."/"back" to cancel): `));
|
|
74
|
+
if (courseChoice === '..' || courseChoice.toLowerCase() === 'back') {
|
|
75
|
+
const confirmCancel = await askConfirmation(rl, chalk.yellow('Cancel submission?'), false);
|
|
76
|
+
if (confirmCancel) {
|
|
77
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!courseChoice.trim()) {
|
|
83
|
+
console.log(chalk.yellow('Please enter a course number.'));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const parsedIndex = Number.parseInt(courseChoice, 10);
|
|
87
|
+
if (Number.isNaN(parsedIndex)) {
|
|
88
|
+
console.log(chalk.red('Error: Please enter a valid course number.'));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const courseIndex = parsedIndex - 1;
|
|
92
|
+
if (courseIndex < 0 || courseIndex >= selectableCourses.length) {
|
|
93
|
+
console.log(chalk.red('Error: Invalid course selection.'));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
selectedCourse = selectableCourses[courseIndex];
|
|
97
|
+
courseId = selectedCourse.id;
|
|
98
|
+
console.log(chalk.green(`Success: Selected ${selectedCourse.name}
|
|
99
|
+
`));
|
|
40
100
|
}
|
|
41
101
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
|
|
103
|
+
if (!assignmentId) {
|
|
104
|
+
let assignments = null;
|
|
105
|
+
while (true) {
|
|
106
|
+
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
107
|
+
console.log(chalk.cyan.bold('Loading assignments, please wait...'));
|
|
108
|
+
try {
|
|
109
|
+
assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
|
|
110
|
+
'include[]=submission',
|
|
111
|
+
'order_by=due_at',
|
|
112
|
+
'per_page=100'
|
|
113
|
+
]);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(chalk.red('Error: Failed to load assignments: ' + error.message));
|
|
116
|
+
const retryAssignments = await askConfirmation(rl, chalk.yellow('Try loading assignments again?'), true);
|
|
117
|
+
if (retryAssignments) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const chooseDifferentCourse = await askConfirmation(rl, chalk.yellow('Choose a different course?'), true);
|
|
121
|
+
if (chooseDifferentCourse) {
|
|
122
|
+
courseId = null;
|
|
123
|
+
selectedCourse = null;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!assignments || assignments.length === 0) {
|
|
131
|
+
console.log(chalk.red('Error: No assignments found for this course.'));
|
|
132
|
+
const chooseDifferentCourse = await askConfirmation(rl, chalk.yellow('Select a different course?'), true);
|
|
133
|
+
if (chooseDifferentCourse) {
|
|
134
|
+
courseId = null;
|
|
135
|
+
selectedCourse = null;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const retryAssignments = await askConfirmation(rl, chalk.yellow('Refresh assignments for this course?'), true);
|
|
139
|
+
if (retryAssignments) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!courseId) {
|
|
149
|
+
assignmentId = null;
|
|
150
|
+
selectedAssignment = null;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
155
|
+
console.log(chalk.cyan.bold(`Found ${assignments.length} assignment(s):`));
|
|
156
|
+
const submittedCount = assignments.filter(a => a.submission && a.submission.submitted_at).length;
|
|
157
|
+
const pendingCount = assignments.length - submittedCount;
|
|
158
|
+
const uploadableCount = assignments.filter(a =>
|
|
159
|
+
a.submission_types &&
|
|
160
|
+
a.submission_types.includes('online_upload') &&
|
|
161
|
+
a.workflow_state === 'published'
|
|
162
|
+
).length;
|
|
163
|
+
console.log(chalk.yellow(`Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads`));
|
|
164
|
+
console.log(chalk.cyan('-'.repeat(60)));
|
|
165
|
+
|
|
166
|
+
assignments.forEach((assignment, index) => {
|
|
167
|
+
const dueDate = assignment.due_at ? new Date(assignment.due_at).toLocaleDateString() : 'No due date';
|
|
168
|
+
const submitted = assignment.submission && assignment.submission.submitted_at ? chalk.green('Submitted') : chalk.yellow('Not submitted');
|
|
169
|
+
const canSubmitFiles = assignment.submission_types && assignment.submission_types.includes('online_upload') && assignment.workflow_state === 'published';
|
|
170
|
+
let gradeDisplay = '';
|
|
171
|
+
const submission = assignment.submission;
|
|
172
|
+
if (submission && submission.score !== null && submission.score !== undefined) {
|
|
173
|
+
const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
|
|
174
|
+
const total = assignment.points_possible || 0;
|
|
175
|
+
gradeDisplay = ` | Grade: ${score}/${total}`;
|
|
176
|
+
} else if (submission && submission.excused) {
|
|
177
|
+
gradeDisplay = ' | Grade: Excused';
|
|
178
|
+
} else if (submission && submission.missing) {
|
|
179
|
+
gradeDisplay = ' | Grade: Missing';
|
|
180
|
+
} else if (assignment.points_possible) {
|
|
181
|
+
gradeDisplay = ` | Grade: –/${assignment.points_possible}`;
|
|
182
|
+
}
|
|
183
|
+
let line = pad(chalk.white((index + 1) + '. '), 5) + chalk.white(assignment.name) + chalk.gray(` (${dueDate})`) + ' ' + submitted + gradeDisplay;
|
|
184
|
+
if (!canSubmitFiles) {
|
|
185
|
+
line += chalk.red(' [No file uploads]');
|
|
186
|
+
}
|
|
187
|
+
console.log(line);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
let awaitingAssignment = true;
|
|
191
|
+
while (awaitingAssignment) {
|
|
192
|
+
const assignmentChoice = await askQuestion(rl, chalk.bold.cyan(`
|
|
193
|
+
Enter assignment number (or ".."/"back" to re-select course): `));
|
|
194
|
+
if (assignmentChoice === '..' || assignmentChoice.toLowerCase() === 'back') {
|
|
195
|
+
const chooseCourseAgain = await askConfirmation(rl, chalk.yellow('Go back to course selection?'), true);
|
|
196
|
+
if (chooseCourseAgain) {
|
|
197
|
+
courseId = null;
|
|
198
|
+
selectedCourse = null;
|
|
199
|
+
assignmentId = null;
|
|
200
|
+
selectedAssignment = null;
|
|
201
|
+
awaitingAssignment = false;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!assignmentChoice.trim()) {
|
|
207
|
+
console.log(chalk.yellow('Please enter an assignment number.'));
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const parsedIndex = Number.parseInt(assignmentChoice, 10);
|
|
211
|
+
if (Number.isNaN(parsedIndex)) {
|
|
212
|
+
console.log(chalk.red('Error: Please enter a valid assignment number.'));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const assignmentIndex = parsedIndex - 1;
|
|
216
|
+
if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
|
|
217
|
+
console.log(chalk.red('Error: Invalid assignment selection.'));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const candidate = assignments[assignmentIndex];
|
|
221
|
+
const canSubmitFiles = candidate.submission_types && candidate.submission_types.includes('online_upload') && candidate.workflow_state === 'published';
|
|
222
|
+
if (!canSubmitFiles) {
|
|
223
|
+
console.log(chalk.red('Error: This assignment does not accept file uploads or is not published.'));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
selectedAssignment = candidate;
|
|
227
|
+
assignmentId = candidate.id;
|
|
228
|
+
console.log(chalk.green(`Success: Selected ${selectedAssignment.name}
|
|
229
|
+
`));
|
|
230
|
+
if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
|
|
231
|
+
const resubmit = await askConfirmation(rl, chalk.yellow('This assignment has already been submitted. Do you want to resubmit?'), true);
|
|
232
|
+
if (!resubmit) {
|
|
233
|
+
console.log(chalk.yellow('Select a different assignment.'));
|
|
234
|
+
assignmentId = null;
|
|
235
|
+
selectedAssignment = null;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
awaitingAssignment = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!courseId) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
56
245
|
}
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
continue;
|
|
246
|
+
|
|
247
|
+
if (courseId && assignmentId) {
|
|
248
|
+
break;
|
|
61
249
|
}
|
|
62
|
-
selectedCourse = selectableCourses[courseIndex];
|
|
63
|
-
courseId = selectedCourse.id;
|
|
64
|
-
console.log(chalk.green(`Success: Selected ${selectedCourse.name}\n`));
|
|
65
250
|
}
|
|
66
|
-
|
|
67
|
-
// Step
|
|
68
|
-
|
|
251
|
+
|
|
252
|
+
// Step 3: Choose file selection method and select files
|
|
253
|
+
let filePaths = [];
|
|
254
|
+
while (filePaths.length === 0) {
|
|
69
255
|
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
70
|
-
console.log(chalk.cyan.bold('
|
|
71
|
-
const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
|
|
72
|
-
'include[]=submission',
|
|
73
|
-
'order_by=due_at',
|
|
74
|
-
'per_page=100'
|
|
75
|
-
]);
|
|
76
|
-
if (!assignments || assignments.length === 0) {
|
|
77
|
-
console.log(chalk.red('Error: No assignments found for this course.'));
|
|
78
|
-
rl.close();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
console.log(chalk.cyan('-'.repeat(60)));
|
|
82
|
-
console.log(chalk.cyan.bold(`Found ${assignments.length} assignment(s):`));
|
|
83
|
-
// Show summary of assignment statuses
|
|
84
|
-
const submittedCount = assignments.filter(a => a.submission && a.submission.submitted_at).length;
|
|
85
|
-
const pendingCount = assignments.length - submittedCount;
|
|
86
|
-
const uploadableCount = assignments.filter(a =>
|
|
87
|
-
a.submission_types &&
|
|
88
|
-
a.submission_types.includes('online_upload') &&
|
|
89
|
-
a.workflow_state === 'published'
|
|
90
|
-
).length;
|
|
91
|
-
console.log(chalk.yellow(`Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads`));
|
|
256
|
+
console.log(chalk.cyan.bold('File Selection Method'));
|
|
92
257
|
console.log(chalk.cyan('-'.repeat(60)));
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
258
|
+
const courseLabel = selectedCourse?.name || `Course ${courseId}`;
|
|
259
|
+
const assignmentLabel = selectedAssignment?.name || `Assignment ${assignmentId}`;
|
|
260
|
+
console.log(chalk.white('Course: ') + chalk.bold(courseLabel));
|
|
261
|
+
console.log(chalk.white('Assignment: ') + chalk.bold(assignmentLabel));
|
|
262
|
+
console.log();
|
|
263
|
+
|
|
264
|
+
console.log(chalk.yellow('📁 Choose file selection method:'));
|
|
265
|
+
console.log(chalk.white('1. ') + chalk.cyan('Keyboard Navigator') + chalk.gray(' (NEW! - Use arrow keys and space bar to navigate and select)'));
|
|
266
|
+
console.log(chalk.white('2. ') + chalk.cyan('Text-based Selector') + chalk.gray(' (Traditional - Type filenames and wildcards)'));
|
|
267
|
+
console.log(chalk.white('3. ') + chalk.cyan('Basic Directory Listing') + chalk.gray(' (Simple - Select from numbered list)'));
|
|
268
|
+
|
|
269
|
+
let selectorChoice = '';
|
|
270
|
+
while (true) {
|
|
271
|
+
selectorChoice = await askQuestion(rl, chalk.bold.cyan(`
|
|
272
|
+
Choose method (1-3): `));
|
|
273
|
+
if (!selectorChoice) {
|
|
274
|
+
console.log(chalk.yellow('Please choose option 1, 2, or 3.'));
|
|
275
|
+
continue;
|
|
110
276
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
line += chalk.red(' [No file uploads]');
|
|
277
|
+
if (['1', '2', '3'].includes(selectorChoice)) {
|
|
278
|
+
break;
|
|
114
279
|
}
|
|
115
|
-
console.log(
|
|
116
|
-
});
|
|
117
|
-
const assignmentChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter assignment number (or ".."/"back" to re-select course): '));
|
|
118
|
-
if (assignmentChoice === '..' || assignmentChoice.toLowerCase() === 'back') {
|
|
119
|
-
courseId = null;
|
|
120
|
-
selectedCourse = null;
|
|
121
|
-
break;
|
|
280
|
+
console.log(chalk.red('Invalid option. Please enter 1, 2, or 3.'));
|
|
122
281
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
rl
|
|
126
|
-
|
|
282
|
+
|
|
283
|
+
if (selectorChoice === '1') {
|
|
284
|
+
filePaths = await selectFilesKeyboard(rl);
|
|
285
|
+
} else if (selectorChoice === '2') {
|
|
286
|
+
filePaths = await selectFilesImproved(rl);
|
|
287
|
+
} else {
|
|
288
|
+
filePaths = await selectFiles(rl);
|
|
127
289
|
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
console.log(chalk.
|
|
290
|
+
|
|
291
|
+
if (!filePaths || filePaths.length === 0) {
|
|
292
|
+
console.log(chalk.yellow('No files selected.'));
|
|
293
|
+
const retrySelection = await askConfirmation(rl, chalk.yellow('Do you want to try selecting files again?'), true);
|
|
294
|
+
if (!retrySelection) {
|
|
295
|
+
console.log(chalk.yellow('Submission cancelled.'));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
131
298
|
continue;
|
|
132
299
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
300
|
+
|
|
301
|
+
const validFiles = [];
|
|
302
|
+
for (const file of filePaths) {
|
|
303
|
+
if (fs.existsSync(file)) {
|
|
304
|
+
validFiles.push(file);
|
|
305
|
+
} else {
|
|
306
|
+
console.log(chalk.red('Error: File not found: ' + file));
|
|
307
|
+
}
|
|
138
308
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
}
|
package/lib/interactive.js
CHANGED
|
@@ -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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
+
"version": "1.4.5",
|
|
4
4
|
"description": "A command line tool for interacting with Canvas LMS API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"canvas",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"adm-zip": "^0.5.16",
|
|
51
51
|
"axios": "^1.6.0",
|
|
52
|
+
"canvaslms-cli": "^1.4.4",
|
|
52
53
|
"chalk": "^5.4.1",
|
|
53
54
|
"commander": "^11.1.0",
|
|
54
55
|
"form-data": "^4.0.3"
|