canvaslms-cli 1.1.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 +83 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/commands/announcements.js +77 -0
- package/commands/api.js +19 -0
- package/commands/assignments.js +126 -0
- package/commands/config.js +242 -0
- package/commands/grades.js +68 -0
- package/commands/list.js +70 -0
- package/commands/profile.js +34 -0
- package/commands/submit.js +366 -0
- package/lib/api-client.js +79 -0
- package/lib/config-validator.js +63 -0
- package/lib/config.js +150 -0
- package/lib/file-upload.js +78 -0
- package/lib/interactive.js +31 -0
- package/package.json +67 -0
- package/src/index.js +142 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Submit command for interactive assignment submission
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { makeCanvasRequest } = require('../lib/api-client');
|
|
8
|
+
const { createReadlineInterface, askQuestion } = require('../lib/interactive');
|
|
9
|
+
const { uploadSingleFileToCanvas, submitAssignmentWithFiles } = require('../lib/file-upload');
|
|
10
|
+
|
|
11
|
+
async function submitAssignment(options) {
|
|
12
|
+
const rl = createReadlineInterface();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
let courseId = options.course;
|
|
16
|
+
let assignmentId = options.assignment;
|
|
17
|
+
let filePath = options.file;
|
|
18
|
+
|
|
19
|
+
// Step 1: Select Course (if not provided)
|
|
20
|
+
if (!courseId) {
|
|
21
|
+
console.log('đ Loading your starred courses...\n');
|
|
22
|
+
|
|
23
|
+
const courses = await makeCanvasRequest('get', 'courses', [
|
|
24
|
+
'enrollment_state=active',
|
|
25
|
+
'include[]=favorites'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
if (!courses || courses.length === 0) {
|
|
29
|
+
console.log('No courses found.');
|
|
30
|
+
rl.close();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Filter for starred courses by default
|
|
35
|
+
let starredCourses = courses.filter(course => course.is_favorite);
|
|
36
|
+
|
|
37
|
+
if (starredCourses.length === 0) {
|
|
38
|
+
console.log('No starred courses found. Showing all enrolled courses...\n');
|
|
39
|
+
starredCourses = courses;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('Select a course:');
|
|
43
|
+
starredCourses.forEach((course, index) => {
|
|
44
|
+
const starIcon = course.is_favorite ? 'â ' : '';
|
|
45
|
+
console.log(`${index + 1}. ${starIcon}${course.name} (ID: ${course.id})`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const courseChoice = await askQuestion(rl, '\nEnter course number: ');
|
|
49
|
+
const courseIndex = parseInt(courseChoice) - 1;
|
|
50
|
+
|
|
51
|
+
if (courseIndex < 0 || courseIndex >= starredCourses.length) {
|
|
52
|
+
console.log('Invalid course selection.');
|
|
53
|
+
rl.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
courseId = starredCourses[courseIndex].id;
|
|
58
|
+
console.log(`Selected: ${starredCourses[courseIndex].name}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 2: Select Assignment (if not provided)
|
|
62
|
+
if (!assignmentId) {
|
|
63
|
+
console.log('đ Loading assignments...\n');
|
|
64
|
+
|
|
65
|
+
const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
|
|
66
|
+
'include[]=submission',
|
|
67
|
+
'order_by=due_at',
|
|
68
|
+
'per_page=100'
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (!assignments || assignments.length === 0) {
|
|
72
|
+
console.log('No assignments found for this course.');
|
|
73
|
+
rl.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`Found ${assignments.length} assignment(s):\n`);
|
|
78
|
+
|
|
79
|
+
// Show summary of assignment statuses
|
|
80
|
+
const submittedCount = assignments.filter(a => a.submission && a.submission.submitted_at).length;
|
|
81
|
+
const pendingCount = assignments.length - submittedCount;
|
|
82
|
+
const uploadableCount = assignments.filter(a =>
|
|
83
|
+
a.submission_types &&
|
|
84
|
+
a.submission_types.includes('online_upload') &&
|
|
85
|
+
a.workflow_state === 'published'
|
|
86
|
+
).length;
|
|
87
|
+
|
|
88
|
+
console.log(`đ Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads\n`);
|
|
89
|
+
console.log('Select an assignment:');
|
|
90
|
+
assignments.forEach((assignment, index) => {
|
|
91
|
+
const dueDate = assignment.due_at ? new Date(assignment.due_at).toLocaleDateString() : 'No due date';
|
|
92
|
+
const submitted = assignment.submission && assignment.submission.submitted_at ? 'â
' : 'â';
|
|
93
|
+
|
|
94
|
+
// Check if assignment accepts file uploads
|
|
95
|
+
const canSubmitFiles = assignment.submission_types &&
|
|
96
|
+
assignment.submission_types.includes('online_upload') &&
|
|
97
|
+
assignment.workflow_state === 'published';
|
|
98
|
+
const submissionIcon = canSubmitFiles ? 'đ¤' : 'đ';
|
|
99
|
+
|
|
100
|
+
// Format grade like Canvas web interface
|
|
101
|
+
let gradeDisplay = '';
|
|
102
|
+
const submission = assignment.submission;
|
|
103
|
+
|
|
104
|
+
if (submission && submission.score !== null && submission.score !== undefined) {
|
|
105
|
+
const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
|
|
106
|
+
const total = assignment.points_possible || 0;
|
|
107
|
+
gradeDisplay = ` | Grade: ${score}/${total}`;
|
|
108
|
+
} else if (submission && submission.excused) {
|
|
109
|
+
gradeDisplay = ' | Grade: Excused';
|
|
110
|
+
} else if (submission && submission.missing) {
|
|
111
|
+
gradeDisplay = ' | Grade: Missing';
|
|
112
|
+
} else if (assignment.points_possible) {
|
|
113
|
+
gradeDisplay = ` | Grade: â/${assignment.points_possible}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`${index + 1}. ${submissionIcon} ${assignment.name} ${submitted}`);
|
|
117
|
+
console.log(` Due: ${dueDate} | Points: ${assignment.points_possible || 'N/A'}${gradeDisplay}`);
|
|
118
|
+
if (!canSubmitFiles) {
|
|
119
|
+
console.log(` đ Note: This assignment doesn't accept file uploads`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const assignmentChoice = await askQuestion(rl, '\nEnter assignment number: ');
|
|
124
|
+
const assignmentIndex = parseInt(assignmentChoice) - 1;
|
|
125
|
+
|
|
126
|
+
if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
|
|
127
|
+
console.log('Invalid assignment selection.');
|
|
128
|
+
rl.close();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const selectedAssignment = assignments[assignmentIndex];
|
|
133
|
+
|
|
134
|
+
// Check if assignment accepts file uploads
|
|
135
|
+
if (!selectedAssignment.submission_types ||
|
|
136
|
+
!selectedAssignment.submission_types.includes('online_upload') ||
|
|
137
|
+
selectedAssignment.workflow_state !== 'published') {
|
|
138
|
+
console.log('â This assignment does not accept file uploads or is not published.');
|
|
139
|
+
rl.close();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
assignmentId = selectedAssignment.id;
|
|
144
|
+
console.log(`Selected: ${selectedAssignment.name}\n`);
|
|
145
|
+
|
|
146
|
+
// Check if already submitted
|
|
147
|
+
if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
|
|
148
|
+
const resubmit = await askQuestion(rl, 'This assignment has already been submitted. Do you want to resubmit? (y/N): ');
|
|
149
|
+
if (resubmit.toLowerCase() !== 'y' && resubmit.toLowerCase() !== 'yes') {
|
|
150
|
+
console.log('Submission cancelled.');
|
|
151
|
+
rl.close();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Step 3: Select Files (if not provided)
|
|
158
|
+
let filePaths = [];
|
|
159
|
+
if (filePath) {
|
|
160
|
+
filePaths = [filePath]; // Single file provided via option
|
|
161
|
+
} else {
|
|
162
|
+
filePaths = await selectFiles(rl);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Validate all selected files exist
|
|
166
|
+
const validFiles = [];
|
|
167
|
+
for (const file of filePaths) {
|
|
168
|
+
if (fs.existsSync(file)) {
|
|
169
|
+
validFiles.push(file);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`â ī¸ File not found: ${file}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (validFiles.length === 0) {
|
|
176
|
+
console.log('No valid files selected.');
|
|
177
|
+
rl.close();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
filePaths = validFiles;
|
|
182
|
+
|
|
183
|
+
// Step 4: Confirm and Submit
|
|
184
|
+
console.log('\nđ Submission Summary:');
|
|
185
|
+
console.log(`Course ID: ${courseId}`);
|
|
186
|
+
console.log(`Assignment ID: ${assignmentId}`);
|
|
187
|
+
console.log(`Files (${filePaths.length}):`);
|
|
188
|
+
filePaths.forEach((file, index) => {
|
|
189
|
+
const stats = fs.statSync(file);
|
|
190
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
191
|
+
console.log(` ${index + 1}. ${file} (${size})`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const confirm = await askQuestion(rl, '\nProceed with submission? (Y/n): ');
|
|
195
|
+
if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
|
|
196
|
+
console.log('Submission cancelled.');
|
|
197
|
+
rl.close();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('\nđ Uploading files...');
|
|
202
|
+
|
|
203
|
+
// Upload all files
|
|
204
|
+
const uploadedFileIds = [];
|
|
205
|
+
for (let i = 0; i < filePaths.length; i++) {
|
|
206
|
+
const currentFile = filePaths[i];
|
|
207
|
+
console.log(`đ¤ Uploading ${i + 1}/${filePaths.length}: ${path.basename(currentFile)}`);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const fileId = await uploadSingleFileToCanvas(courseId, assignmentId, currentFile);
|
|
211
|
+
uploadedFileIds.push(fileId);
|
|
212
|
+
console.log(`â
${path.basename(currentFile)} uploaded successfully`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(`â Failed to upload ${currentFile}: ${error.message}`);
|
|
215
|
+
const continueUpload = await askQuestion(rl, 'Continue with remaining files? (Y/n): ');
|
|
216
|
+
if (continueUpload.toLowerCase() === 'n' || continueUpload.toLowerCase() === 'no') {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (uploadedFileIds.length === 0) {
|
|
223
|
+
console.log('â No files were uploaded successfully.');
|
|
224
|
+
rl.close();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Submit the assignment with all uploaded files
|
|
229
|
+
console.log('\nđ Submitting assignment...');
|
|
230
|
+
const submission = await submitAssignmentWithFiles(courseId, assignmentId, uploadedFileIds);
|
|
231
|
+
|
|
232
|
+
console.log(`â
Assignment submitted successfully with ${uploadedFileIds.length} file(s)!`);
|
|
233
|
+
console.log(`Submission ID: ${submission.id}`);
|
|
234
|
+
console.log(`Submitted at: ${new Date(submission.submitted_at).toLocaleString()}`);
|
|
235
|
+
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error('â Submission failed:', error.message);
|
|
238
|
+
} finally {
|
|
239
|
+
rl.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function selectFiles(rl) {
|
|
244
|
+
console.log('đ File selection options:');
|
|
245
|
+
console.log('1. Enter file path(s) manually');
|
|
246
|
+
console.log('2. Select from current directory');
|
|
247
|
+
|
|
248
|
+
const fileChoice = await askQuestion(rl, '\nChoose option (1-2): ');
|
|
249
|
+
|
|
250
|
+
if (fileChoice === '1') {
|
|
251
|
+
return await selectFilesManually(rl);
|
|
252
|
+
} else if (fileChoice === '2') {
|
|
253
|
+
return await selectFilesFromDirectory(rl);
|
|
254
|
+
} else {
|
|
255
|
+
console.log('Invalid option.');
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function selectFilesManually(rl) {
|
|
261
|
+
const singleOrMultiple = await askQuestion(rl, 'Submit single file or multiple files? (s/m): ');
|
|
262
|
+
const filePaths = [];
|
|
263
|
+
|
|
264
|
+
if (singleOrMultiple.toLowerCase() === 'm' || singleOrMultiple.toLowerCase() === 'multiple') {
|
|
265
|
+
console.log('Enter file paths (one per line). Press Enter on empty line to finish:');
|
|
266
|
+
let fileInput = '';
|
|
267
|
+
while (true) {
|
|
268
|
+
fileInput = await askQuestion(rl, 'File path: ');
|
|
269
|
+
if (fileInput === '') break;
|
|
270
|
+
filePaths.push(fileInput);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
const singleFile = await askQuestion(rl, 'Enter file path: ');
|
|
274
|
+
filePaths.push(singleFile);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return filePaths;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function selectFilesFromDirectory(rl) {
|
|
281
|
+
try {
|
|
282
|
+
const files = fs.readdirSync('.').filter(file =>
|
|
283
|
+
fs.statSync(file).isFile() &&
|
|
284
|
+
!file.startsWith('.') &&
|
|
285
|
+
file !== 'package.json' &&
|
|
286
|
+
file !== 'README.md'
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (files.length === 0) {
|
|
290
|
+
console.log('No suitable files found in current directory.');
|
|
291
|
+
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
292
|
+
return [manualFile];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log('\nFiles in current directory:');
|
|
296
|
+
files.forEach((file, index) => {
|
|
297
|
+
const stats = fs.statSync(file);
|
|
298
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
299
|
+
console.log(`${index + 1}. ${file} (${size})`);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const multipleFiles = await askQuestion(rl, '\nSelect multiple files? (y/N): ');
|
|
303
|
+
|
|
304
|
+
if (multipleFiles.toLowerCase() === 'y' || multipleFiles.toLowerCase() === 'yes') {
|
|
305
|
+
return await selectMultipleFilesFromList(rl, files);
|
|
306
|
+
} else {
|
|
307
|
+
return await selectSingleFileFromList(rl, files);
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.log('Error reading directory.');
|
|
311
|
+
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
312
|
+
return [manualFile];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function selectMultipleFilesFromList(rl, files) {
|
|
317
|
+
console.log('Enter file numbers separated by commas (e.g., 1,3,5) or ranges (e.g., 1-3):');
|
|
318
|
+
const fileIndices = await askQuestion(rl, 'File numbers: ');
|
|
319
|
+
|
|
320
|
+
const selectedIndices = [];
|
|
321
|
+
const parts = fileIndices.split(',');
|
|
322
|
+
|
|
323
|
+
for (const part of parts) {
|
|
324
|
+
const trimmed = part.trim();
|
|
325
|
+
if (trimmed.includes('-')) {
|
|
326
|
+
// Handle range (e.g., 1-3)
|
|
327
|
+
const [start, end] = trimmed.split('-').map(n => parseInt(n.trim()));
|
|
328
|
+
for (let i = start; i <= end; i++) {
|
|
329
|
+
selectedIndices.push(i - 1); // Convert to 0-based index
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// Handle single number
|
|
333
|
+
selectedIndices.push(parseInt(trimmed) - 1); // Convert to 0-based index
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Remove duplicates and filter valid indices
|
|
338
|
+
const uniqueIndices = [...new Set(selectedIndices)].filter(
|
|
339
|
+
index => index >= 0 && index < files.length
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (uniqueIndices.length === 0) {
|
|
343
|
+
console.log('No valid file selections.');
|
|
344
|
+
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
345
|
+
return [manualFile];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return uniqueIndices.map(index => files[index]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function selectSingleFileFromList(rl, files) {
|
|
352
|
+
const fileIndex = await askQuestion(rl, '\nEnter file number: ');
|
|
353
|
+
const selectedFileIndex = parseInt(fileIndex) - 1;
|
|
354
|
+
|
|
355
|
+
if (selectedFileIndex >= 0 && selectedFileIndex < files.length) {
|
|
356
|
+
return [files[selectedFileIndex]];
|
|
357
|
+
} else {
|
|
358
|
+
console.log('Invalid file selection.');
|
|
359
|
+
const manualFile = await askQuestion(rl, 'Enter file path manually: ');
|
|
360
|
+
return [manualFile];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
submitAssignment
|
|
366
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas API client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { getInstanceConfig } = require('./config');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Make Canvas API request
|
|
11
|
+
*/
|
|
12
|
+
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
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
makeCanvasRequest
|
|
79
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation and setup helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { configExists, readConfig } = require('./config');
|
|
6
|
+
const { createReadlineInterface, askQuestion } = require('./interactive');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if configuration is valid and prompt setup if needed
|
|
10
|
+
*/
|
|
11
|
+
async function ensureConfig() {
|
|
12
|
+
if (!configExists()) {
|
|
13
|
+
console.log('â No Canvas configuration found!');
|
|
14
|
+
console.log('\nđ Let\'s set up your Canvas CLI configuration...\n');
|
|
15
|
+
|
|
16
|
+
const rl = createReadlineInterface();
|
|
17
|
+
const setup = await askQuestion(rl, 'Would you like to set up your configuration now? (Y/n): ');
|
|
18
|
+
rl.close();
|
|
19
|
+
|
|
20
|
+
if (setup.toLowerCase() === 'n' || setup.toLowerCase() === 'no') {
|
|
21
|
+
console.log('\nđ To set up later, run: canvas config setup');
|
|
22
|
+
console.log('đĄ Or set environment variables: CANVAS_DOMAIN and CANVAS_API_TOKEN');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Import and run setup (dynamic import to avoid circular dependency)
|
|
27
|
+
const { setupConfig } = require('../commands/config');
|
|
28
|
+
await setupConfig();
|
|
29
|
+
|
|
30
|
+
// Check if setup was successful
|
|
31
|
+
if (!configExists()) {
|
|
32
|
+
console.log('\nâ Configuration setup was not completed. Please run "canvas config setup" to try again.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log('\nâ
Configuration complete! You can now use Canvas CLI commands.');
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate existing config
|
|
41
|
+
const config = readConfig();
|
|
42
|
+
if (!config || !config.domain || !config.token) {
|
|
43
|
+
console.log('â Invalid configuration found. Please run "canvas config setup" to reconfigure.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wrapper function to ensure config exists before running a command
|
|
52
|
+
*/
|
|
53
|
+
function requireConfig(commandFunction) {
|
|
54
|
+
return async function(...args) {
|
|
55
|
+
await ensureConfig();
|
|
56
|
+
return commandFunction(...args);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
ensureConfig,
|
|
62
|
+
requireConfig
|
|
63
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Canvas CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
require('dotenv').config();
|
|
9
|
+
|
|
10
|
+
// Configuration file path in user's home directory
|
|
11
|
+
const CONFIG_FILE = path.join(os.homedir(), '.canvas-cli-config.json');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get default configuration structure
|
|
15
|
+
*/
|
|
16
|
+
function getDefaultConfig() {
|
|
17
|
+
return {
|
|
18
|
+
domain: '',
|
|
19
|
+
token: '',
|
|
20
|
+
createdAt: new Date().toISOString(),
|
|
21
|
+
lastUpdated: new Date().toISOString()
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load configuration from file or environment variables
|
|
27
|
+
*/
|
|
28
|
+
function loadConfig() {
|
|
29
|
+
try {
|
|
30
|
+
// First, try to load from config file
|
|
31
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
32
|
+
const configData = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
33
|
+
if (configData.domain && configData.token) {
|
|
34
|
+
// Clean up domain - remove https:// and trailing slashes
|
|
35
|
+
const domain = configData.domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
36
|
+
return { domain, token: configData.token };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback to environment variables
|
|
41
|
+
let domain = process.env.CANVAS_DOMAIN;
|
|
42
|
+
const token = process.env.CANVAS_API_TOKEN;
|
|
43
|
+
|
|
44
|
+
if (!domain || !token) {
|
|
45
|
+
console.error('â No Canvas configuration found!');
|
|
46
|
+
console.error('\nPlease run "canvas config setup" to configure your Canvas credentials.');
|
|
47
|
+
console.error('Or set environment variables:');
|
|
48
|
+
console.error(' CANVAS_DOMAIN=your-canvas-domain.instructure.com');
|
|
49
|
+
console.error(' CANVAS_API_TOKEN=your-api-token');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Clean up domain - remove https:// and trailing slashes
|
|
54
|
+
domain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
55
|
+
|
|
56
|
+
return { domain, token };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`â Error loading configuration: ${error.message}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Save configuration to file
|
|
65
|
+
*/
|
|
66
|
+
function saveConfig(domain, token) {
|
|
67
|
+
try {
|
|
68
|
+
const config = {
|
|
69
|
+
domain: domain.replace(/^https?:\/\//, '').replace(/\/$/, ''),
|
|
70
|
+
token,
|
|
71
|
+
createdAt: fs.existsSync(CONFIG_FILE) ?
|
|
72
|
+
JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')).createdAt :
|
|
73
|
+
new Date().toISOString(),
|
|
74
|
+
lastUpdated: new Date().toISOString()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
78
|
+
console.log(`â
Configuration saved to ${CONFIG_FILE}`);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`â Error saving configuration: ${error.message}`);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get configuration file path
|
|
88
|
+
*/
|
|
89
|
+
function getConfigPath() {
|
|
90
|
+
return CONFIG_FILE;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if configuration file exists
|
|
95
|
+
*/
|
|
96
|
+
function configExists() {
|
|
97
|
+
return fs.existsSync(CONFIG_FILE);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read current configuration (if exists)
|
|
102
|
+
*/
|
|
103
|
+
function readConfig() {
|
|
104
|
+
try {
|
|
105
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
106
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`â Error reading configuration: ${error.message}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Delete configuration file
|
|
117
|
+
*/
|
|
118
|
+
function deleteConfig() {
|
|
119
|
+
try {
|
|
120
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
121
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
122
|
+
console.log('â
Configuration file deleted successfully.');
|
|
123
|
+
return true;
|
|
124
|
+
} else {
|
|
125
|
+
console.log('âšī¸ No configuration file found.');
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(`â Error deleting configuration: ${error.message}`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the Canvas instance configuration
|
|
136
|
+
*/
|
|
137
|
+
function getInstanceConfig() {
|
|
138
|
+
return loadConfig();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
loadConfig,
|
|
143
|
+
getInstanceConfig,
|
|
144
|
+
saveConfig,
|
|
145
|
+
getConfigPath,
|
|
146
|
+
configExists,
|
|
147
|
+
readConfig,
|
|
148
|
+
deleteConfig,
|
|
149
|
+
getDefaultConfig
|
|
150
|
+
};
|