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,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
configExists,
|
|
7
|
+
readConfig,
|
|
8
|
+
saveConfig,
|
|
9
|
+
deleteConfig,
|
|
10
|
+
getConfigPath
|
|
11
|
+
} = require('../lib/config');
|
|
12
|
+
const { createReadlineInterface, askQuestion } = require('../lib/interactive');
|
|
13
|
+
|
|
14
|
+
async function showConfig() {
|
|
15
|
+
console.log('š Canvas CLI Configuration\n');
|
|
16
|
+
|
|
17
|
+
const configPath = getConfigPath();
|
|
18
|
+
const hasConfig = configExists();
|
|
19
|
+
|
|
20
|
+
console.log(`Configuration file: ${configPath}`);
|
|
21
|
+
console.log(`Status: ${hasConfig ? 'ā
Found' : 'ā Not found'}\n`);
|
|
22
|
+
|
|
23
|
+
if (hasConfig) {
|
|
24
|
+
const config = readConfig();
|
|
25
|
+
if (config) {
|
|
26
|
+
console.log('Current configuration:');
|
|
27
|
+
console.log(` š Canvas Domain: ${config.domain || 'Not set'}`);
|
|
28
|
+
console.log(` š API Token: ${config.token ? config.token.substring(0, 10) + '...' : 'Not set'}`);
|
|
29
|
+
console.log(` š
Created: ${config.createdAt ? new Date(config.createdAt).toLocaleString() : 'Unknown'}`);
|
|
30
|
+
console.log(` š Last Updated: ${config.lastUpdated ? new Date(config.lastUpdated).toLocaleString() : 'Unknown'}`);
|
|
31
|
+
} } else {
|
|
32
|
+
console.log('ā No configuration found.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('\nš Available commands:');
|
|
36
|
+
console.log(' canvas config setup # Interactive setup wizard');
|
|
37
|
+
console.log(' canvas config edit # Edit existing configuration');
|
|
38
|
+
console.log(' canvas config show # Show current configuration');
|
|
39
|
+
console.log(' canvas config delete # Delete configuration file');
|
|
40
|
+
console.log(' canvas config path # Show configuration file path');
|
|
41
|
+
|
|
42
|
+
console.log('\nš§ Manual setup:');
|
|
43
|
+
console.log('1. Get your Canvas API token:');
|
|
44
|
+
console.log(' - Log into your Canvas instance');
|
|
45
|
+
console.log(' - Go to Account ā Settings');
|
|
46
|
+
console.log(' - Scroll down to "Approved Integrations"');
|
|
47
|
+
console.log(' - Click "+ New Access Token"');
|
|
48
|
+
console.log(' - Copy the generated token');
|
|
49
|
+
console.log('2. Run "canvas config setup" to configure');
|
|
50
|
+
|
|
51
|
+
console.log('\nš Example usage after setup:');
|
|
52
|
+
console.log(' canvas list # List starred courses');
|
|
53
|
+
console.log(' canvas list -a # List all enrolled courses');
|
|
54
|
+
console.log(' canvas submit # Interactive assignment submission');
|
|
55
|
+
console.log(' canvas profile # Show user profile');
|
|
56
|
+
console.log(' canvas assignments 12345 # Show assignments for course');
|
|
57
|
+
console.log(' canvas grades # Show grades for all courses');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function setupConfig() {
|
|
61
|
+
const rl = createReadlineInterface();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
console.log('š Canvas CLI Configuration Setup\n');
|
|
65
|
+
|
|
66
|
+
// Check if config already exists
|
|
67
|
+
if (configExists()) {
|
|
68
|
+
const config = readConfig();
|
|
69
|
+
console.log('š Existing configuration found:');
|
|
70
|
+
console.log(` Domain: ${config.domain || 'Not set'}`);
|
|
71
|
+
console.log(` Token: ${config.token ? 'Set (hidden)' : 'Not set'}\n`);
|
|
72
|
+
|
|
73
|
+
const overwrite = await askQuestion(rl, 'Do you want to overwrite the existing configuration? (y/N): ');
|
|
74
|
+
if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
|
|
75
|
+
console.log('Setup cancelled.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
// Get Canvas domain
|
|
81
|
+
const currentConfig = readConfig();
|
|
82
|
+
let domain = await askQuestion(rl, `Enter your Canvas domain${currentConfig?.domain ? ` (${currentConfig.domain})` : ''}: `);
|
|
83
|
+
if (!domain && currentConfig?.domain) {
|
|
84
|
+
domain = currentConfig.domain;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!domain) {
|
|
88
|
+
console.log('ā Canvas domain is required.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Validate and clean domain
|
|
93
|
+
domain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
94
|
+
if (!domain.includes('.')) {
|
|
95
|
+
console.log('ā Invalid domain format. Please enter a valid domain (e.g., school.instructure.com)');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`ā
Domain set to: ${domain}\n`);
|
|
100
|
+
// Get API token
|
|
101
|
+
const defaultToken = currentConfig?.token || '';
|
|
102
|
+
let token = await askQuestion(rl, `Enter your Canvas API token${defaultToken ? ' (press Enter to keep current)' : ''}: `);
|
|
103
|
+
if (!token && defaultToken) {
|
|
104
|
+
token = defaultToken;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!token) {
|
|
108
|
+
console.log('ā Canvas API token is required.');
|
|
109
|
+
console.log('\nš To get your API token:');
|
|
110
|
+
console.log('1. Log into your Canvas instance');
|
|
111
|
+
console.log('2. Go to Account ā Settings');
|
|
112
|
+
console.log('3. Scroll down to "Approved Integrations"');
|
|
113
|
+
console.log('4. Click "+ New Access Token"');
|
|
114
|
+
console.log('5. Copy the generated token');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Validate token format (basic check)
|
|
119
|
+
if (token.length < 10) {
|
|
120
|
+
console.log('ā API token seems too short. Please check your token.');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('ā
Token received\n');
|
|
125
|
+
|
|
126
|
+
// Save configuration
|
|
127
|
+
const saved = saveConfig(domain, token);
|
|
128
|
+
if (saved) {
|
|
129
|
+
console.log('š Configuration setup completed successfully!');
|
|
130
|
+
console.log('\nš Next steps:');
|
|
131
|
+
console.log(' canvas list # Test your setup by listing courses');
|
|
132
|
+
console.log(' canvas profile # View your profile');
|
|
133
|
+
console.log(' canvas config show # View your configuration');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`ā Setup failed: ${error.message}`);
|
|
138
|
+
} finally {
|
|
139
|
+
rl.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function editConfig() {
|
|
144
|
+
const rl = createReadlineInterface();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
if (!configExists()) {
|
|
148
|
+
console.log('ā No configuration file found. Run "canvas config setup" first.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const config = readConfig();
|
|
153
|
+
console.log('āļø Edit Canvas CLI Configuration\n');
|
|
154
|
+
console.log('Current values:');
|
|
155
|
+
console.log(` Domain: ${config.domain}`);
|
|
156
|
+
console.log(` Token: ${config.token ? config.token.substring(0, 10) + '...' : 'Not set'}\n`);
|
|
157
|
+
|
|
158
|
+
// Edit domain
|
|
159
|
+
const newDomain = await askQuestion(rl, `New Canvas domain (${config.domain}): `);
|
|
160
|
+
const domain = newDomain.trim() || config.domain;
|
|
161
|
+
|
|
162
|
+
// Edit token
|
|
163
|
+
const changeToken = await askQuestion(rl, 'Change API token? (y/N): ');
|
|
164
|
+
let token = config.token;
|
|
165
|
+
|
|
166
|
+
if (changeToken.toLowerCase() === 'y' || changeToken.toLowerCase() === 'yes') {
|
|
167
|
+
const newToken = await askQuestion(rl, 'New API token: ');
|
|
168
|
+
if (newToken.trim()) {
|
|
169
|
+
token = newToken.trim();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Confirm changes
|
|
174
|
+
console.log('\nš New configuration:');
|
|
175
|
+
console.log(` Domain: ${domain}`);
|
|
176
|
+
console.log(` Token: ${token ? token.substring(0, 10) + '...' : 'Not set'}`);
|
|
177
|
+
|
|
178
|
+
const confirm = await askQuestion(rl, '\nSave changes? (Y/n): ');
|
|
179
|
+
if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
|
|
180
|
+
console.log('Changes cancelled.');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const saved = saveConfig(domain, token);
|
|
185
|
+
if (saved) {
|
|
186
|
+
console.log('ā
Configuration updated successfully!');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`ā Edit failed: ${error.message}`);
|
|
191
|
+
} finally {
|
|
192
|
+
rl.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function showConfigPath() {
|
|
197
|
+
console.log(`š Configuration file location: ${getConfigPath()}`);
|
|
198
|
+
console.log(`š Exists: ${configExists() ? 'Yes' : 'No'}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function deleteConfigFile() {
|
|
202
|
+
console.log('šļø Delete Configuration\n');
|
|
203
|
+
|
|
204
|
+
if (!configExists()) {
|
|
205
|
+
console.log('ā No configuration file found.');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const config = readConfig();
|
|
210
|
+
console.log('Current configuration:');
|
|
211
|
+
console.log(` Domain: ${config.domain}`);
|
|
212
|
+
console.log(` Token: ${config.token ? 'Set (hidden)' : 'Not set'}`);
|
|
213
|
+
console.log(` File: ${getConfigPath()}\n`);
|
|
214
|
+
|
|
215
|
+
// For safety, require explicit confirmation
|
|
216
|
+
console.log('ā ļø This will permanently delete your Canvas CLI configuration.');
|
|
217
|
+
console.log('You will need to run "canvas config setup" again to use the CLI.');
|
|
218
|
+
console.log('\nTo confirm deletion, type: DELETE');
|
|
219
|
+
|
|
220
|
+
const readline = require('readline');
|
|
221
|
+
const rl = readline.createInterface({
|
|
222
|
+
input: process.stdin,
|
|
223
|
+
output: process.stdout
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
rl.question('Confirmation: ', (answer) => {
|
|
227
|
+
if (answer === 'DELETE') {
|
|
228
|
+
deleteConfig();
|
|
229
|
+
} else {
|
|
230
|
+
console.log('Deletion cancelled.');
|
|
231
|
+
}
|
|
232
|
+
rl.close();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
showConfig,
|
|
238
|
+
setupConfig,
|
|
239
|
+
editConfig,
|
|
240
|
+
showConfigPath,
|
|
241
|
+
deleteConfigFile
|
|
242
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grades command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { makeCanvasRequest } = require('../lib/api-client');
|
|
6
|
+
|
|
7
|
+
async function showGrades(courseId, options) {
|
|
8
|
+
try {
|
|
9
|
+
if (courseId) {
|
|
10
|
+
// Get grades for specific course
|
|
11
|
+
const enrollments = await makeCanvasRequest('get', `courses/${courseId}/enrollments`, ['user_id=self', 'include[]=grades']);
|
|
12
|
+
|
|
13
|
+
if (!enrollments || enrollments.length === 0) {
|
|
14
|
+
console.log('No enrollment found for this course.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const enrollment = enrollments[0];
|
|
19
|
+
console.log(`Grades for course: ${enrollment.course_id}`);
|
|
20
|
+
console.log(`Current Score: ${enrollment.grades?.current_score || 'N/A'}%`);
|
|
21
|
+
console.log(`Final Score: ${enrollment.grades?.final_score || 'N/A'}%`);
|
|
22
|
+
console.log(`Current Grade: ${enrollment.grades?.current_grade || 'N/A'}`);
|
|
23
|
+
console.log(`Final Grade: ${enrollment.grades?.final_grade || 'N/A'}`);
|
|
24
|
+
} else {
|
|
25
|
+
// Get grades for all courses
|
|
26
|
+
const courses = await makeCanvasRequest('get', 'courses', [
|
|
27
|
+
'enrollment_state=active',
|
|
28
|
+
'include[]=enrollments',
|
|
29
|
+
'include[]=total_scores'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
if (!courses || courses.length === 0) {
|
|
33
|
+
console.log('No courses found.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('Grades Summary:\n');
|
|
38
|
+
|
|
39
|
+
courses.forEach((course, index) => {
|
|
40
|
+
console.log(`${index + 1}. ${course.name}`);
|
|
41
|
+
console.log(` ID: ${course.id}`);
|
|
42
|
+
|
|
43
|
+
if (course.enrollments && course.enrollments.length > 0) {
|
|
44
|
+
const enrollment = course.enrollments[0];
|
|
45
|
+
if (enrollment.grades) {
|
|
46
|
+
console.log(` Current Score: ${enrollment.grades.current_score || 'N/A'}%`);
|
|
47
|
+
console.log(` Final Score: ${enrollment.grades.final_score || 'N/A'}%`);
|
|
48
|
+
console.log(` Current Grade: ${enrollment.grades.current_grade || 'N/A'}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(` Grades: Not available`);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
console.log(` Enrollment: Not found`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Error fetching grades:', error.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
showGrades
|
|
68
|
+
};
|
package/commands/list.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List courses command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { makeCanvasRequest } = require('../lib/api-client');
|
|
6
|
+
|
|
7
|
+
async function listCourses(options) {
|
|
8
|
+
try {
|
|
9
|
+
const queryParams = [];
|
|
10
|
+
|
|
11
|
+
// Always show only active courses
|
|
12
|
+
queryParams.push('enrollment_state=active');
|
|
13
|
+
|
|
14
|
+
// Include additional course information
|
|
15
|
+
queryParams.push('include[]=term');
|
|
16
|
+
queryParams.push('include[]=course_progress');
|
|
17
|
+
queryParams.push('include[]=total_students');
|
|
18
|
+
queryParams.push('include[]=favorites'); // Include favorite status
|
|
19
|
+
|
|
20
|
+
const courses = await makeCanvasRequest('get', 'courses', queryParams);
|
|
21
|
+
|
|
22
|
+
if (!courses || courses.length === 0) {
|
|
23
|
+
console.log('No courses found.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// By default, show only starred courses unless -a flag is used
|
|
28
|
+
let filteredCourses = courses;
|
|
29
|
+
if (!options.all) {
|
|
30
|
+
filteredCourses = courses.filter(course => course.is_favorite);
|
|
31
|
+
|
|
32
|
+
if (filteredCourses.length === 0) {
|
|
33
|
+
console.log('No starred courses found. Use -a to see all courses.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const courseLabel = options.all ? 'enrolled course(s)' : 'starred course(s)';
|
|
39
|
+
console.log(`Found ${filteredCourses.length} ${courseLabel}:\n`);
|
|
40
|
+
|
|
41
|
+
filteredCourses.forEach((course, index) => {
|
|
42
|
+
const starIcon = course.is_favorite ? 'ā ' : '';
|
|
43
|
+
console.log(`${index + 1}. ${starIcon}${course.name}`);
|
|
44
|
+
console.log(` ID: ${course.id}`);
|
|
45
|
+
console.log(` Code: ${course.course_code || 'N/A'}`);
|
|
46
|
+
|
|
47
|
+
if (options.verbose) {
|
|
48
|
+
console.log(` Term: ${course.term?.name || 'N/A'}`);
|
|
49
|
+
console.log(` Students: ${course.total_students || 'N/A'}`);
|
|
50
|
+
console.log(` Start Date: ${course.start_at ? new Date(course.start_at).toLocaleDateString() : 'N/A'}`);
|
|
51
|
+
console.log(` End Date: ${course.end_at ? new Date(course.end_at).toLocaleDateString() : 'N/A'}`);
|
|
52
|
+
console.log(` Workflow State: ${course.workflow_state}`);
|
|
53
|
+
|
|
54
|
+
if (course.course_progress) {
|
|
55
|
+
console.log(` Progress: ${course.course_progress.requirement_completed_count || 0}/${course.course_progress.requirement_count || 0} requirements`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(''); // Empty line between courses
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error fetching courses:', error.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
listCourses
|
|
70
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { makeCanvasRequest } = require('../lib/api-client');
|
|
6
|
+
|
|
7
|
+
async function showProfile(options) {
|
|
8
|
+
try {
|
|
9
|
+
const user = await makeCanvasRequest('get', 'users/self', ['include[]=email', 'include[]=locale']);
|
|
10
|
+
|
|
11
|
+
console.log('User Profile:\n');
|
|
12
|
+
console.log(`Name: ${user.name}`);
|
|
13
|
+
console.log(`ID: ${user.id}`);
|
|
14
|
+
console.log(`Email: ${user.email || 'N/A'}`);
|
|
15
|
+
console.log(`Login ID: ${user.login_id || 'N/A'}`);
|
|
16
|
+
|
|
17
|
+
if (options.verbose) {
|
|
18
|
+
console.log(`Short Name: ${user.short_name || 'N/A'}`);
|
|
19
|
+
console.log(`Sortable Name: ${user.sortable_name || 'N/A'}`);
|
|
20
|
+
console.log(`Locale: ${user.locale || 'N/A'}`);
|
|
21
|
+
console.log(`Time Zone: ${user.time_zone || 'N/A'}`);
|
|
22
|
+
console.log(`Avatar URL: ${user.avatar_url || 'N/A'}`);
|
|
23
|
+
console.log(`Created: ${user.created_at ? new Date(user.created_at).toLocaleString() : 'N/A'}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Error fetching profile:', error.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
showProfile
|
|
34
|
+
};
|