canvaslms-cli 1.4.6 → 1.5.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 +18 -0
- package/README.md +32 -22
- package/dist/commands/announcements.d.ts +3 -0
- package/dist/commands/announcements.d.ts.map +1 -0
- package/dist/commands/announcements.js +92 -0
- package/dist/commands/announcements.js.map +1 -0
- package/dist/commands/api.d.ts +3 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +8 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/assignments.d.ts +3 -0
- package/dist/commands/assignments.d.ts.map +1 -0
- package/dist/commands/assignments.js +100 -0
- package/dist/commands/assignments.js.map +1 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +202 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/grades.d.ts +3 -0
- package/dist/commands/grades.d.ts.map +1 -0
- package/dist/commands/grades.js +335 -0
- package/dist/commands/grades.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +61 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/profile.d.ts +6 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +30 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/submit.d.ts +8 -0
- package/dist/commands/submit.d.ts.map +1 -0
- package/dist/commands/submit.js +319 -0
- package/dist/commands/submit.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +2 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +76 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/config-validator.d.ts +4 -0
- package/dist/lib/config-validator.d.ts.map +1 -0
- package/dist/lib/config-validator.js +38 -0
- package/dist/lib/config-validator.js.map +1 -0
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +85 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/file-upload.d.ts +3 -0
- package/dist/lib/file-upload.d.ts.map +1 -0
- package/dist/lib/file-upload.js +52 -0
- package/dist/lib/file-upload.js.map +1 -0
- package/dist/lib/interactive.d.ts +16 -0
- package/dist/lib/interactive.d.ts.map +1 -0
- package/dist/lib/interactive.js +758 -0
- package/dist/lib/interactive.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +90 -0
- package/dist/src/index.js.map +1 -0
- package/dist/types/index.d.ts +147 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +15 -15
- package/commands/announcements.js +0 -102
- package/commands/api.js +0 -19
- package/commands/assignments.js +0 -100
- package/commands/config.js +0 -239
- package/commands/grades.js +0 -201
- package/commands/list.js +0 -68
- package/commands/profile.js +0 -35
- package/commands/submit.js +0 -603
- package/lib/api-client.js +0 -75
- package/lib/config-validator.js +0 -60
- package/lib/config.js +0 -103
- package/lib/file-upload.js +0 -74
- package/lib/interactive.js +0 -889
- package/src/index.js +0 -120
package/commands/submit.js
DELETED
|
@@ -1,603 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Submit command for interactive assignment submission
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { makeCanvasRequest } from '../lib/api-client.js';
|
|
8
|
-
import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved, selectFilesKeyboard, pad } from '../lib/interactive.js';
|
|
9
|
-
import { uploadSingleFileToCanvas, submitAssignmentWithFiles } from '../lib/file-upload.js';
|
|
10
|
-
import chalk from 'chalk';
|
|
11
|
-
|
|
12
|
-
export async function submitAssignment(options) {
|
|
13
|
-
const rl = createReadlineInterface();
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
let courseId = options.course;
|
|
17
|
-
let assignmentId = options.assignment;
|
|
18
|
-
let selectedCourse = null;
|
|
19
|
-
let selectedAssignment = null;
|
|
20
|
-
|
|
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
|
-
`));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
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
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (courseId && assignmentId) {
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Step 3: Choose file selection method and select files
|
|
253
|
-
let filePaths = [];
|
|
254
|
-
while (filePaths.length === 0) {
|
|
255
|
-
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
256
|
-
console.log(chalk.cyan.bold('File Selection Method'));
|
|
257
|
-
console.log(chalk.cyan('-'.repeat(60)));
|
|
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;
|
|
276
|
-
}
|
|
277
|
-
if (['1', '2', '3'].includes(selectorChoice)) {
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
console.log(chalk.red('Invalid option. Please enter 1, 2, or 3.'));
|
|
281
|
-
}
|
|
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);
|
|
289
|
-
}
|
|
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
|
-
}
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
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
|
-
}
|
|
308
|
-
}
|
|
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) {
|
|
314
|
-
console.log(chalk.yellow('Submission cancelled.'));
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
filePaths = [];
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
filePaths = validFiles;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Step 4: Confirm and Submit
|
|
325
|
-
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
326
|
-
console.log(chalk.cyan.bold('Submission Summary:'));
|
|
327
|
-
console.log(chalk.cyan('-'.repeat(60)));
|
|
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));
|
|
332
|
-
console.log(chalk.white(`Files (${filePaths.length}):`));
|
|
333
|
-
filePaths.forEach((file, index) => {
|
|
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
|
-
}
|
|
341
|
-
});
|
|
342
|
-
const confirm = await askConfirmation(rl, chalk.bold.cyan(`
|
|
343
|
-
Proceed with submission?`), true, { requireExplicit: true });
|
|
344
|
-
if (!confirm) {
|
|
345
|
-
console.log(chalk.yellow('Submission cancelled.'));
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
console.log(chalk.cyan.bold(`
|
|
350
|
-
Uploading files, please wait...`));
|
|
351
|
-
// Upload all files
|
|
352
|
-
const uploadedFileIds = [];
|
|
353
|
-
for (let i = 0; i < filePaths.length; i++) {
|
|
354
|
-
const currentFile = filePaths[i];
|
|
355
|
-
process.stdout.write(chalk.yellow(`Uploading ${i + 1}/${filePaths.length}: ${path.basename(currentFile)} ... `));
|
|
356
|
-
try {
|
|
357
|
-
const fileId = await uploadSingleFileToCanvas(courseId, assignmentId, currentFile);
|
|
358
|
-
uploadedFileIds.push(fileId);
|
|
359
|
-
console.log(chalk.green('Success: Uploaded.'));
|
|
360
|
-
} catch (error) {
|
|
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}`));
|
|
365
|
-
}
|
|
366
|
-
const continueUpload = await askConfirmation(rl, chalk.yellow('Continue with remaining files?'), true);
|
|
367
|
-
if (!continueUpload) break;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
// Submit assignment with uploaded files
|
|
371
|
-
if (uploadedFileIds.length > 0) {
|
|
372
|
-
try {
|
|
373
|
-
console.log(chalk.cyan.bold('Submitting assignment, please wait...'));
|
|
374
|
-
await submitAssignmentWithFiles(courseId, assignmentId, uploadedFileIds);
|
|
375
|
-
console.log(chalk.green('Success: Assignment submitted successfully!'));
|
|
376
|
-
} catch (error) {
|
|
377
|
-
console.error(chalk.red('Error: Failed to submit assignment: ' + error.message));
|
|
378
|
-
}
|
|
379
|
-
} else {
|
|
380
|
-
console.log(chalk.red('Error: No files were uploaded. Submission not completed.'));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
} catch (error) {
|
|
384
|
-
console.error(chalk.red('Error: Submission failed: ' + error.message));
|
|
385
|
-
} finally {
|
|
386
|
-
rl.close();
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async function selectFiles(rl) {
|
|
391
|
-
console.log('📁 File selection options:');
|
|
392
|
-
console.log('1. Enter file path(s) manually');
|
|
393
|
-
console.log('2. Select from current directory');
|
|
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.');
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
async function selectFilesManually(rl) {
|
|
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
|
-
|
|
434
|
-
const filePaths = [];
|
|
435
|
-
if (mode === 'm') {
|
|
436
|
-
console.log('Enter file paths (one per line). Press Enter on empty line to finish:');
|
|
437
|
-
while (true) {
|
|
438
|
-
const fileInput = await askQuestion(rl, 'File path: ');
|
|
439
|
-
if (!fileInput) {
|
|
440
|
-
break;
|
|
441
|
-
}
|
|
442
|
-
filePaths.push(fileInput);
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
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
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return filePaths;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async function selectFilesFromDirectory(rl) {
|
|
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) {
|
|
481
|
-
console.log('No suitable files found in current directory.');
|
|
482
|
-
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
483
|
-
return manualFile ? [manualFile] : [];
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
console.log('\nFiles in current directory:');
|
|
487
|
-
files.forEach((file, index) => {
|
|
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
|
-
}
|
|
495
|
-
});
|
|
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;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async function selectMultipleFilesFromList(rl, files) {
|
|
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
|
-
}
|
|
564
|
-
}
|
|
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]);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
async function selectSingleFileFromList(rl, files) {
|
|
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.');
|
|
602
|
-
}
|
|
603
|
-
}
|
package/lib/api-client.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canvas API client
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import axios from 'axios';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import { getInstanceConfig } from './config.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Make Canvas API request
|
|
11
|
-
*/
|
|
12
|
-
export async function makeCanvasRequest(method, endpoint, queryParams = [], requestBody = null) {
|
|
13
|
-
const instanceConfig = getInstanceConfig();
|
|
14
|
-
|
|
15
|
-
// Construct the full URL
|
|
16
|
-
const baseUrl = `https://${instanceConfig.domain}/api/v1`;
|
|
17
|
-
const url = `${baseUrl}/${endpoint.replace(/^\//, '')}`;
|
|
18
|
-
|
|
19
|
-
// Setup request configuration
|
|
20
|
-
const config = {
|
|
21
|
-
method: method.toLowerCase(),
|
|
22
|
-
url: url,
|
|
23
|
-
headers: {
|
|
24
|
-
'Authorization': `Bearer ${instanceConfig.token}`,
|
|
25
|
-
'Content-Type': 'application/json'
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Add query parameters
|
|
30
|
-
if (queryParams.length > 0) {
|
|
31
|
-
const params = new URLSearchParams();
|
|
32
|
-
queryParams.forEach(param => {
|
|
33
|
-
const [key, value] = param.split('=', 2);
|
|
34
|
-
params.append(key, value || '');
|
|
35
|
-
});
|
|
36
|
-
config.params = params;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Add request body for POST/PUT requests
|
|
40
|
-
if (requestBody && (method.toLowerCase() === 'post' || method.toLowerCase() === 'put')) {
|
|
41
|
-
if (requestBody.startsWith('@')) {
|
|
42
|
-
// Read from file
|
|
43
|
-
const filename = requestBody.substring(1);
|
|
44
|
-
try {
|
|
45
|
-
config.data = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.error(`Error reading file ${filename}: ${error.message}`);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
// Parse JSON string
|
|
52
|
-
try {
|
|
53
|
-
config.data = JSON.parse(requestBody);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
console.error(`Error parsing JSON: ${error.message}`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const response = await axios(config);
|
|
63
|
-
return response.data;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (error.response) {
|
|
66
|
-
console.error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
|
67
|
-
if (error.response.data) {
|
|
68
|
-
console.error(JSON.stringify(error.response.data, null, 2));
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
console.error(`Request failed: ${error.message}`);
|
|
72
|
-
}
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
}
|