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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File upload utilities for Canvas
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const FormData = require('form-data');
|
|
8
|
+
const { makeCanvasRequest } = require('./api-client');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Upload single file to Canvas and return the file ID
|
|
12
|
+
*/
|
|
13
|
+
async function uploadSingleFileToCanvas(courseId, assignmentId, filePath) {
|
|
14
|
+
try {
|
|
15
|
+
// Check if file exists
|
|
16
|
+
if (!fs.existsSync(filePath)) {
|
|
17
|
+
throw new Error(`File not found: ${filePath}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fileName = require('path').basename(filePath);
|
|
21
|
+
const fileContent = fs.readFileSync(filePath);
|
|
22
|
+
|
|
23
|
+
// Step 1: Get upload URL from Canvas
|
|
24
|
+
const uploadParams = [
|
|
25
|
+
`name=${encodeURIComponent(fileName)}`,
|
|
26
|
+
`size=${fileContent.length}`,
|
|
27
|
+
'parent_folder_path=/assignments'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const uploadData = await makeCanvasRequest('post', `courses/${courseId}/assignments/${assignmentId}/submissions/self/files`, uploadParams);
|
|
31
|
+
|
|
32
|
+
if (!uploadData.upload_url) {
|
|
33
|
+
throw new Error('Failed to get upload URL from Canvas');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Step 2: Upload file to the provided URL
|
|
37
|
+
const form = new FormData();
|
|
38
|
+
|
|
39
|
+
// Add all the required fields from Canvas response
|
|
40
|
+
Object.keys(uploadData.upload_params).forEach(key => {
|
|
41
|
+
form.append(key, uploadData.upload_params[key]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Add the file
|
|
45
|
+
form.append('file', fileContent, fileName);
|
|
46
|
+
|
|
47
|
+
const uploadResponse = await axios.post(uploadData.upload_url, form, {
|
|
48
|
+
headers: form.getHeaders(),
|
|
49
|
+
maxRedirects: 0,
|
|
50
|
+
validateStatus: (status) => status < 400
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Return the file ID for later submission
|
|
54
|
+
return uploadData.id || uploadResponse.data.id;
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Failed to upload file ${filePath}: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Submit assignment with uploaded files
|
|
63
|
+
*/
|
|
64
|
+
async function submitAssignmentWithFiles(courseId, assignmentId, fileIds) {
|
|
65
|
+
const submissionData = {
|
|
66
|
+
submission: {
|
|
67
|
+
submission_type: 'online_upload',
|
|
68
|
+
file_ids: fileIds
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return await makeCanvasRequest('post', `courses/${courseId}/assignments/${assignmentId}/submissions`, [], JSON.stringify(submissionData));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
uploadSingleFileToCanvas,
|
|
77
|
+
submitAssignmentWithFiles
|
|
78
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompt utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create readline interface for user input
|
|
9
|
+
*/
|
|
10
|
+
function createReadlineInterface() {
|
|
11
|
+
return readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prompt user for input
|
|
19
|
+
*/
|
|
20
|
+
function askQuestion(rl, question) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(question, (answer) => {
|
|
23
|
+
resolve(answer.trim());
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
createReadlineInterface,
|
|
30
|
+
askQuestion
|
|
31
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "canvaslms-cli",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A command line tool for interacting with Canvas LMS API",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"canvas",
|
|
7
|
+
"cli",
|
|
8
|
+
"api",
|
|
9
|
+
"instructure",
|
|
10
|
+
"lms",
|
|
11
|
+
"education",
|
|
12
|
+
"canvas-lms"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/caphefalumi/Canvas-CLI#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/caphefalumi/Canvas-CLI/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/caphefalumi/Canvas-CLI.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "Your Name",
|
|
25
|
+
"email": "your.email@example.com"
|
|
26
|
+
},
|
|
27
|
+
"type": "commonjs",
|
|
28
|
+
"main": "src/index.js",
|
|
29
|
+
"bin": {
|
|
30
|
+
"canvas": "src/index.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src/",
|
|
34
|
+
"lib/",
|
|
35
|
+
"commands/",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"start": "node src/index.js",
|
|
41
|
+
"dev": "node src/index.js",
|
|
42
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
43
|
+
"lint": "echo \"Add linting here\"",
|
|
44
|
+
"format": "echo \"Add formatting here\"",
|
|
45
|
+
"setup": "powershell -ExecutionPolicy Bypass -File setup.ps1",
|
|
46
|
+
"postinstall": "echo \"Canvas CLI installed successfully! Run 'canvas config' to get started.\""
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"axios": "^1.6.0",
|
|
50
|
+
"commander": "^11.1.0",
|
|
51
|
+
"form-data": "^4.0.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"eslint": "^8.0.0",
|
|
55
|
+
"prettier": "^3.0.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=14.0.0",
|
|
59
|
+
"npm": ">=6.0.0"
|
|
60
|
+
},
|
|
61
|
+
"preferGlobal": true,
|
|
62
|
+
"os": [
|
|
63
|
+
"win32",
|
|
64
|
+
"darwin",
|
|
65
|
+
"linux"
|
|
66
|
+
]
|
|
67
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canvas CLI - A command line tool for interacting with Canvas API
|
|
5
|
+
*
|
|
6
|
+
* @author Canvas CLI Team
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { Command } = require('commander');
|
|
11
|
+
|
|
12
|
+
// Import command handlers
|
|
13
|
+
const { listCourses } = require('../commands/list');
|
|
14
|
+
const { showConfig, setupConfig, editConfig, showConfigPath, deleteConfigFile } = require('../commands/config');
|
|
15
|
+
const { listAssignments } = require('../commands/assignments');
|
|
16
|
+
const { showGrades } = require('../commands/grades');
|
|
17
|
+
const { showAnnouncements } = require('../commands/announcements');
|
|
18
|
+
const { showProfile } = require('../commands/profile');
|
|
19
|
+
const { submitAssignment } = require('../commands/submit');
|
|
20
|
+
const { createQueryHandler } = require('../commands/api');
|
|
21
|
+
const { requireConfig } = require('../lib/config-validator');
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
|
|
25
|
+
// Setup CLI program
|
|
26
|
+
program
|
|
27
|
+
.name('canvas')
|
|
28
|
+
.description('Canvas API Command Line Tool')
|
|
29
|
+
.version('1.1.0');
|
|
30
|
+
|
|
31
|
+
// Raw API commands
|
|
32
|
+
function createQueryCommand(method) {
|
|
33
|
+
return program
|
|
34
|
+
.command(method)
|
|
35
|
+
.alias(method === 'query' ? 'q' : method.charAt(0))
|
|
36
|
+
.argument('<endpoint>', 'Canvas API endpoint to query')
|
|
37
|
+
.option('-q, --query <param>', 'Query parameter (can be used multiple times)', [])
|
|
38
|
+
.option('-d, --data <data>', 'Request body (JSON string or @filename)')
|
|
39
|
+
.description(`${method.toUpperCase()} request to Canvas API`)
|
|
40
|
+
.action(requireConfig(createQueryHandler(method)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create raw API commands
|
|
44
|
+
createQueryCommand('get');
|
|
45
|
+
createQueryCommand('post');
|
|
46
|
+
createQueryCommand('put');
|
|
47
|
+
createQueryCommand('delete');
|
|
48
|
+
createQueryCommand('query');
|
|
49
|
+
|
|
50
|
+
// List command to show enrolled courses
|
|
51
|
+
program
|
|
52
|
+
.command('list')
|
|
53
|
+
.alias('l')
|
|
54
|
+
.description('List starred courses (default) or all courses with -a')
|
|
55
|
+
.option('-a, --all', 'Show all enrolled courses instead of just starred ones')
|
|
56
|
+
.option('-v, --verbose', 'Show detailed course information')
|
|
57
|
+
.action(requireConfig(listCourses));
|
|
58
|
+
|
|
59
|
+
// Config command with subcommands
|
|
60
|
+
const configCommand = program
|
|
61
|
+
.command('config')
|
|
62
|
+
.description('Manage Canvas CLI configuration')
|
|
63
|
+
.action(showConfig); // Default action when no subcommand is provided
|
|
64
|
+
|
|
65
|
+
configCommand
|
|
66
|
+
.command('show')
|
|
67
|
+
.alias('status')
|
|
68
|
+
.description('Show current configuration')
|
|
69
|
+
.action(showConfig);
|
|
70
|
+
|
|
71
|
+
configCommand
|
|
72
|
+
.command('setup')
|
|
73
|
+
.alias('init')
|
|
74
|
+
.description('Interactive configuration setup')
|
|
75
|
+
.action(setupConfig);
|
|
76
|
+
|
|
77
|
+
configCommand
|
|
78
|
+
.command('edit')
|
|
79
|
+
.alias('update')
|
|
80
|
+
.description('Edit existing configuration')
|
|
81
|
+
.action(editConfig);
|
|
82
|
+
|
|
83
|
+
configCommand
|
|
84
|
+
.command('path')
|
|
85
|
+
.description('Show configuration file path')
|
|
86
|
+
.action(showConfigPath);
|
|
87
|
+
|
|
88
|
+
configCommand
|
|
89
|
+
.command('delete')
|
|
90
|
+
.alias('remove')
|
|
91
|
+
.description('Delete configuration file')
|
|
92
|
+
.action(deleteConfigFile);
|
|
93
|
+
|
|
94
|
+
// Assignments command to show assignments for a course
|
|
95
|
+
program
|
|
96
|
+
.command('assignments')
|
|
97
|
+
.alias('assign')
|
|
98
|
+
.description('List assignments for a specific course')
|
|
99
|
+
.argument('<course-id>', 'Course ID to get assignments from')
|
|
100
|
+
.option('-v, --verbose', 'Show detailed assignment information')
|
|
101
|
+
.option('-s, --submitted', 'Only show submitted assignments')
|
|
102
|
+
.option('-p, --pending', 'Only show pending assignments')
|
|
103
|
+
.action(requireConfig(listAssignments));
|
|
104
|
+
|
|
105
|
+
// Grades command to show grades
|
|
106
|
+
program
|
|
107
|
+
.command('grades')
|
|
108
|
+
.alias('grade')
|
|
109
|
+
.description('Show grades for all courses or a specific course')
|
|
110
|
+
.argument('[course-id]', 'Optional course ID to get grades for specific course')
|
|
111
|
+
.option('-v, --verbose', 'Show detailed grade information')
|
|
112
|
+
.action(requireConfig(showGrades));
|
|
113
|
+
|
|
114
|
+
// Announcements command
|
|
115
|
+
program
|
|
116
|
+
.command('announcements')
|
|
117
|
+
.alias('announce')
|
|
118
|
+
.description('Show recent announcements')
|
|
119
|
+
.argument('[course-id]', 'Optional course ID to get announcements for specific course')
|
|
120
|
+
.option('-l, --limit <number>', 'Number of announcements to show', '5')
|
|
121
|
+
.action(requireConfig(showAnnouncements));
|
|
122
|
+
|
|
123
|
+
// Profile command
|
|
124
|
+
program
|
|
125
|
+
.command('profile')
|
|
126
|
+
.alias('me')
|
|
127
|
+
.description('Show current user profile information')
|
|
128
|
+
.option('-v, --verbose', 'Show detailed profile information')
|
|
129
|
+
.action(requireConfig(showProfile));
|
|
130
|
+
|
|
131
|
+
// Submit command for interactive assignment submission
|
|
132
|
+
program
|
|
133
|
+
.command('submit')
|
|
134
|
+
.alias('sub')
|
|
135
|
+
.description('Interactively submit one or multiple files to an assignment')
|
|
136
|
+
.option('-c, --course <course-id>', 'Skip course selection and use specific course ID')
|
|
137
|
+
.option('-a, --assignment <assignment-id>', 'Skip assignment selection and use specific assignment ID')
|
|
138
|
+
.option('-f, --file <file-path>', 'Skip file selection and use specific file path')
|
|
139
|
+
.action(requireConfig(submitAssignment));
|
|
140
|
+
|
|
141
|
+
// Parse command line arguments
|
|
142
|
+
program.parse();
|