canvaslms-cli 1.3.2 ā 1.4.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/commands/announcements.js +102 -80
- package/commands/assignments.js +100 -130
- package/commands/config.js +239 -242
- package/commands/grades.js +201 -205
- package/commands/list.js +68 -70
- package/commands/profile.js +35 -34
- package/commands/submit.js +105 -143
- package/lib/api-client.js +4 -8
- package/lib/config-validator.js +60 -63
- package/lib/config.js +103 -135
- package/lib/file-upload.js +73 -78
- package/lib/interactive.js +180 -132
- package/package.json +4 -2
- package/src/index.js +26 -48
package/lib/interactive.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Interactive prompt utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import AdmZip from 'adm-zip';
|
|
9
|
+
import chalk from 'chalk';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Create readline interface for user input
|
|
@@ -100,37 +102,69 @@ async function selectFromList(rl, items, displayProperty = null, allowCancel = t
|
|
|
100
102
|
return items[choice - 1];
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
function getSubfoldersRecursive(startDir = process.cwd()) {
|
|
106
|
+
const result = [];
|
|
107
|
+
function walk(dir) {
|
|
108
|
+
const items = fs.readdirSync(dir);
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
const fullPath = path.join(dir, item);
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.statSync(fullPath);
|
|
113
|
+
if (stat.isDirectory()) {
|
|
114
|
+
const baseName = path.basename(fullPath);
|
|
115
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
|
|
116
|
+
result.push(fullPath);
|
|
117
|
+
walk(fullPath); // recurse into subdirectory
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// Optionally log: console.warn(`Skipped unreadable folder: ${fullPath}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
walk(startDir);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
/**
|
|
104
129
|
* Get files matching a wildcard pattern
|
|
105
130
|
*/
|
|
106
131
|
function getFilesMatchingWildcard(pattern, currentDir = process.cwd()) {
|
|
107
132
|
try {
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
133
|
+
// Gather all subfolders
|
|
134
|
+
const allFolders = [currentDir, ...getSubfoldersRecursive(currentDir)];
|
|
135
|
+
let allFiles = [];
|
|
136
|
+
for (const folder of allFolders) {
|
|
137
|
+
const files = fs.readdirSync(folder).map(f => path.join(folder, f));
|
|
138
|
+
for (const filePath of files) {
|
|
139
|
+
try {
|
|
140
|
+
if (fs.statSync(filePath).isFile()) {
|
|
141
|
+
allFiles.push(filePath);
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
111
146
|
// Convert wildcard pattern to regex
|
|
112
147
|
let regexPattern;
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
let matchFullPath = false;
|
|
149
|
+
if (pattern === '*' || (!pattern.includes('.') && !pattern.includes('/'))) {
|
|
150
|
+
regexPattern = new RegExp('.*', 'i');
|
|
151
|
+
matchFullPath = true;
|
|
152
|
+
} else if (pattern.startsWith('*.')) {
|
|
115
153
|
const extension = pattern.slice(2);
|
|
116
154
|
regexPattern = new RegExp(`\\.${extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
|
|
117
155
|
} else if (pattern.includes('*')) {
|
|
118
|
-
// Handle other wildcard patterns
|
|
119
156
|
regexPattern = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i');
|
|
120
157
|
} else {
|
|
121
|
-
// Exact match
|
|
122
158
|
regexPattern = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
|
|
123
159
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
matchedFiles.push(filePath);
|
|
160
|
+
const matchedFiles = allFiles.filter(filePath => {
|
|
161
|
+
if (matchFullPath) {
|
|
162
|
+
const relPath = path.relative(currentDir, filePath);
|
|
163
|
+
return regexPattern.test(relPath);
|
|
164
|
+
} else {
|
|
165
|
+
return regexPattern.test(path.basename(filePath));
|
|
131
166
|
}
|
|
132
167
|
});
|
|
133
|
-
|
|
134
168
|
return matchedFiles;
|
|
135
169
|
} catch (error) {
|
|
136
170
|
console.error(`Error reading directory: ${error.message}`);
|
|
@@ -138,188 +172,202 @@ function getFilesMatchingWildcard(pattern, currentDir = process.cwd()) {
|
|
|
138
172
|
}
|
|
139
173
|
}
|
|
140
174
|
|
|
175
|
+
function pad(str, len) {
|
|
176
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
177
|
+
}
|
|
178
|
+
|
|
141
179
|
/**
|
|
142
180
|
* Enhanced file selection with wildcard support
|
|
143
181
|
*/
|
|
144
182
|
async function selectFilesImproved(rl, currentDir = process.cwd()) {
|
|
145
183
|
const selectedFiles = [];
|
|
146
|
-
|
|
147
|
-
console.log('
|
|
148
|
-
console.log('
|
|
184
|
+
console.log(chalk.cyan.bold('\n' + '-'.repeat(50)));
|
|
185
|
+
console.log(chalk.cyan.bold('Enhanced File Selection'));
|
|
186
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
187
|
+
console.log(chalk.yellow('Tips:'));
|
|
149
188
|
console.log(' ⢠Type filename to add individual files');
|
|
150
189
|
console.log(' ⢠Use wildcards: *.html, *.js, *.pdf, etc.');
|
|
151
190
|
console.log(' ⢠Type "browse" to see available files');
|
|
152
191
|
console.log(' ⢠Type "remove" to remove files from selection');
|
|
192
|
+
console.log(' ⢠Type ".." or "back" to return to previous menu');
|
|
153
193
|
console.log(' ⢠Press Enter with no input to finish selection\n');
|
|
154
|
-
|
|
155
194
|
while (true) {
|
|
156
|
-
// Show current selection
|
|
157
195
|
if (selectedFiles.length > 0) {
|
|
158
|
-
console.log(
|
|
196
|
+
console.log(chalk.cyan('\n' + '-'.repeat(50)));
|
|
197
|
+
console.log(chalk.cyan.bold(`Currently selected (${selectedFiles.length} files):`));
|
|
159
198
|
selectedFiles.forEach((file, index) => {
|
|
160
199
|
const stats = fs.statSync(file);
|
|
161
200
|
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
162
|
-
console.log(
|
|
201
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
163
202
|
});
|
|
203
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
164
204
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Empty input - finish selection
|
|
170
|
-
break;
|
|
205
|
+
const input = await askQuestion(rl, chalk.bold.cyan('\nAdd file (or press Enter to finish): '));
|
|
206
|
+
if (!input.trim()) break;
|
|
207
|
+
if (input === '..' || input.toLowerCase() === 'back') {
|
|
208
|
+
return selectedFiles;
|
|
171
209
|
}
|
|
172
|
-
|
|
173
210
|
if (input.toLowerCase() === 'browse') {
|
|
174
|
-
|
|
175
|
-
console.log('
|
|
211
|
+
console.log(chalk.cyan('\n' + '-'.repeat(50)));
|
|
212
|
+
console.log(chalk.cyan.bold('Browsing available files:'));
|
|
176
213
|
try {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
const listedFiles = [];
|
|
215
|
+
function walk(dir) {
|
|
216
|
+
const entries = fs.readdirSync(dir);
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const fullPath = path.join(dir, entry);
|
|
219
|
+
const relPath = path.relative(currentDir, fullPath);
|
|
220
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(entry)) continue;
|
|
221
|
+
try {
|
|
222
|
+
const stat = fs.statSync(fullPath);
|
|
223
|
+
if (stat.isDirectory()) {
|
|
224
|
+
walk(fullPath);
|
|
225
|
+
} else if (stat.isFile()) {
|
|
226
|
+
listedFiles.push({ path: fullPath, rel: relPath, size: stat.size });
|
|
227
|
+
}
|
|
228
|
+
} catch (e) { continue; }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
walk(currentDir);
|
|
232
|
+
if (listedFiles.length === 0) {
|
|
233
|
+
console.log(chalk.red(' No suitable files found.'));
|
|
188
234
|
} else {
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
193
|
-
const ext = path.extname(file);
|
|
194
|
-
const icon = getFileIcon(ext);
|
|
195
|
-
console.log(` ${index + 1}. ${icon} ${file} (${size})`);
|
|
235
|
+
listedFiles.forEach((file, index) => {
|
|
236
|
+
const sizeKB = (file.size / 1024).toFixed(1);
|
|
237
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(file.rel, 35) + chalk.gray(sizeKB + ' KB'));
|
|
196
238
|
});
|
|
197
239
|
}
|
|
198
240
|
} catch (error) {
|
|
199
|
-
console.log(
|
|
241
|
+
console.log(chalk.red(' Error reading directory: ' + error.message));
|
|
200
242
|
}
|
|
201
243
|
continue;
|
|
202
244
|
}
|
|
203
|
-
|
|
204
245
|
if (input.toLowerCase() === 'remove') {
|
|
205
|
-
// Remove files from selection
|
|
206
246
|
if (selectedFiles.length === 0) {
|
|
207
|
-
console.log('
|
|
247
|
+
console.log(chalk.red('No files selected to remove.'));
|
|
208
248
|
continue;
|
|
209
249
|
}
|
|
210
|
-
|
|
211
|
-
console.log('\nSelect file to remove:');
|
|
250
|
+
console.log(chalk.cyan('\nSelect file to remove:'));
|
|
212
251
|
selectedFiles.forEach((file, index) => {
|
|
213
|
-
console.log(
|
|
252
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + path.basename(file));
|
|
214
253
|
});
|
|
215
|
-
|
|
216
|
-
const removeChoice = await askQuestion(rl, '\nEnter number to remove (or press Enter to cancel): ');
|
|
254
|
+
const removeChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter number to remove (or press Enter to cancel): '));
|
|
217
255
|
if (removeChoice.trim()) {
|
|
218
256
|
const removeIndex = parseInt(removeChoice) - 1;
|
|
219
257
|
if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
|
|
220
258
|
const removedFile = selectedFiles.splice(removeIndex, 1)[0];
|
|
221
|
-
console.log(
|
|
259
|
+
console.log(chalk.green(`Removed: ${path.basename(removedFile)}`));
|
|
222
260
|
} else {
|
|
223
|
-
console.log('
|
|
261
|
+
console.log(chalk.red('Invalid selection.'));
|
|
224
262
|
}
|
|
225
263
|
}
|
|
226
264
|
continue;
|
|
227
265
|
}
|
|
228
|
-
|
|
229
|
-
// Check if input contains wildcards
|
|
230
|
-
if (input.includes('*') || input.includes('?')) {
|
|
231
|
-
const matchedFiles = getFilesMatchingWildcard(input, currentDir);
|
|
232
|
-
|
|
233
|
-
if (matchedFiles.length === 0) {
|
|
234
|
-
console.log(`ā No files found matching pattern: ${input}`);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
console.log(`\nšÆ Found ${matchedFiles.length} files matching "${input}":`);
|
|
239
|
-
matchedFiles.forEach((file, index) => {
|
|
240
|
-
const stats = fs.statSync(file);
|
|
241
|
-
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
242
|
-
console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
const confirmAll = await askConfirmation(rl, `Add all ${matchedFiles.length} files?`, true);
|
|
246
|
-
|
|
247
|
-
if (confirmAll) {
|
|
248
|
-
const newFiles = matchedFiles.filter(file => !selectedFiles.includes(file));
|
|
249
|
-
selectedFiles.push(...newFiles);
|
|
250
|
-
console.log(`ā
Added ${newFiles.length} new files (${matchedFiles.length - newFiles.length} were already selected)`);
|
|
251
|
-
}
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Handle individual file selection
|
|
256
266
|
let filePath = input;
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
let zipRequested = false;
|
|
268
|
+
if (filePath.endsWith(' -zip')) {
|
|
269
|
+
filePath = filePath.slice(0, -5).trim();
|
|
270
|
+
zipRequested = true;
|
|
271
|
+
}
|
|
259
272
|
if (!path.isAbsolute(filePath)) {
|
|
260
273
|
filePath = path.join(currentDir, filePath);
|
|
261
274
|
}
|
|
262
|
-
|
|
263
275
|
try {
|
|
264
276
|
if (!fs.existsSync(filePath)) {
|
|
265
|
-
console.log(
|
|
277
|
+
console.log(chalk.red('Error: File not found: ' + input));
|
|
266
278
|
continue;
|
|
267
279
|
}
|
|
268
|
-
|
|
269
280
|
const stats = fs.statSync(filePath);
|
|
270
|
-
if (
|
|
271
|
-
|
|
281
|
+
if (zipRequested) {
|
|
282
|
+
const baseName = path.basename(filePath);
|
|
283
|
+
const zipName = baseName.replace(/\.[^/.]+$/, '') + '.zip';
|
|
284
|
+
const zipPath = path.join(currentDir, zipName);
|
|
285
|
+
const zip = new AdmZip();
|
|
286
|
+
process.stdout.write(chalk.yellow('Zipping, please wait... '));
|
|
287
|
+
if (stats.isDirectory()) {
|
|
288
|
+
zip.addLocalFolder(filePath);
|
|
289
|
+
} else if (stats.isFile()) {
|
|
290
|
+
zip.addLocalFile(filePath);
|
|
291
|
+
} else {
|
|
292
|
+
console.log(chalk.red('Not a file or folder.'));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
zip.writeZip(zipPath);
|
|
296
|
+
console.log(chalk.green('Done.'));
|
|
297
|
+
console.log(chalk.green(`Created ZIP: ${zipName}`));
|
|
298
|
+
if (selectedFiles.includes(zipPath)) {
|
|
299
|
+
console.log(chalk.yellow(`File already selected: ${zipName}`));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
selectedFiles.push(zipPath);
|
|
303
|
+
const size = (fs.statSync(zipPath).size / 1024).toFixed(1) + ' KB';
|
|
304
|
+
console.log(chalk.green(`Added: ${zipName} (${size})`));
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (stats.isDirectory()) {
|
|
308
|
+
const baseName = path.basename(filePath);
|
|
309
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
|
|
310
|
+
const collectedFiles = [];
|
|
311
|
+
function walk(dir) {
|
|
312
|
+
const entries = fs.readdirSync(dir);
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const fullPath = path.join(dir, entry);
|
|
315
|
+
const stat = fs.statSync(fullPath);
|
|
316
|
+
if (stat.isDirectory()) {
|
|
317
|
+
const baseName = path.basename(fullPath);
|
|
318
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
|
|
319
|
+
walk(fullPath);
|
|
320
|
+
} else if (stat.isFile()) {
|
|
321
|
+
collectedFiles.push(fullPath);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
walk(filePath);
|
|
326
|
+
if (collectedFiles.length === 0) {
|
|
327
|
+
console.log(chalk.yellow(`Folder is empty: ${input}`));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
console.log(chalk.cyan(`\nFound ${collectedFiles.length} file(s) in folder "${path.relative(currentDir, filePath)}":`));
|
|
331
|
+
let totalSize = 0;
|
|
332
|
+
collectedFiles.forEach((f, i) => {
|
|
333
|
+
const stat = fs.statSync(f);
|
|
334
|
+
totalSize += stat.size;
|
|
335
|
+
const relativePath = path.relative(currentDir, f);
|
|
336
|
+
console.log(pad(chalk.white((i + 1) + '.'), 5) + pad(relativePath, 35) + chalk.gray((stat.size / 1024).toFixed(1) + ' KB'));
|
|
337
|
+
});
|
|
338
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
339
|
+
console.log(chalk.cyan(`Total size: ${(totalSize / 1024).toFixed(1)} KB`));
|
|
340
|
+
const confirmFolder = await askConfirmation(rl, chalk.bold.cyan(`Add all ${collectedFiles.length} files from this folder?`), true);
|
|
341
|
+
if (confirmFolder) {
|
|
342
|
+
const newFiles = collectedFiles.filter(f => !selectedFiles.includes(f));
|
|
343
|
+
selectedFiles.push(...newFiles);
|
|
344
|
+
console.log(chalk.green(`Added ${newFiles.length} new files (${collectedFiles.length - newFiles.length} already selected)`));
|
|
345
|
+
}
|
|
272
346
|
continue;
|
|
273
347
|
}
|
|
274
|
-
|
|
275
348
|
if (selectedFiles.includes(filePath)) {
|
|
276
|
-
console.log(
|
|
349
|
+
console.log(chalk.yellow(`File already selected: ${path.basename(filePath)}`));
|
|
277
350
|
continue;
|
|
278
351
|
}
|
|
279
|
-
|
|
280
352
|
selectedFiles.push(filePath);
|
|
281
353
|
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
282
|
-
console.log(
|
|
283
|
-
|
|
354
|
+
console.log(chalk.green(`Added: ${path.basename(filePath)} (${size})`));
|
|
284
355
|
} catch (error) {
|
|
285
|
-
console.log(
|
|
356
|
+
console.log(chalk.red('Error accessing file: ' + error.message));
|
|
286
357
|
}
|
|
287
358
|
}
|
|
288
|
-
|
|
289
359
|
return selectedFiles;
|
|
290
360
|
}
|
|
291
361
|
|
|
292
|
-
/**
|
|
293
|
-
* Get file icon based on extension
|
|
294
|
-
*/
|
|
295
|
-
function getFileIcon(extension) {
|
|
296
|
-
const iconMap = {
|
|
297
|
-
'.js': 'š',
|
|
298
|
-
'.html': 'š',
|
|
299
|
-
'.css': 'šØ',
|
|
300
|
-
'.pdf': 'š',
|
|
301
|
-
'.doc': 'š',
|
|
302
|
-
'.docx': 'š',
|
|
303
|
-
'.txt': 'š',
|
|
304
|
-
'.md': 'š',
|
|
305
|
-
'.json': 'āļø',
|
|
306
|
-
'.xml': 'š',
|
|
307
|
-
'.zip': 'š¦',
|
|
308
|
-
'.png': 'š¼ļø',
|
|
309
|
-
'.jpg': 'š¼ļø',
|
|
310
|
-
'.jpeg': 'š¼ļø',
|
|
311
|
-
'.gif': 'š¼ļø'
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
return iconMap[extension.toLowerCase()] || 'š';
|
|
315
|
-
}
|
|
316
362
|
|
|
317
|
-
|
|
363
|
+
export {
|
|
318
364
|
createReadlineInterface,
|
|
319
365
|
askQuestion,
|
|
320
366
|
askQuestionWithValidation,
|
|
321
367
|
askConfirmation,
|
|
322
368
|
selectFromList,
|
|
323
369
|
selectFilesImproved,
|
|
324
|
-
getFilesMatchingWildcard
|
|
325
|
-
|
|
370
|
+
getFilesMatchingWildcard,
|
|
371
|
+
getSubfoldersRecursive,
|
|
372
|
+
pad
|
|
373
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvaslms-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A command line tool for interacting with Canvas LMS API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"canvas",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"name": "Your Name",
|
|
25
25
|
"email": "your.email@example.com"
|
|
26
26
|
},
|
|
27
|
-
"type": "
|
|
27
|
+
"type": "module",
|
|
28
28
|
"main": "src/index.js",
|
|
29
29
|
"bin": {
|
|
30
30
|
"canvaslms-cli": "src/index.js",
|
|
@@ -47,7 +47,9 @@
|
|
|
47
47
|
"postinstall": "echo \"Canvas CLI installed successfully! Run 'canvas config' to get started.\""
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"adm-zip": "^0.5.16",
|
|
50
51
|
"axios": "^1.6.0",
|
|
52
|
+
"chalk": "^5.4.1",
|
|
51
53
|
"commander": "^11.1.0",
|
|
52
54
|
"form-data": "^4.0.3"
|
|
53
55
|
},
|
package/src/index.js
CHANGED
|
@@ -7,18 +7,15 @@
|
|
|
7
7
|
* @version 1.0.0
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const { submitAssignment } = require('../commands/submit');
|
|
20
|
-
const { createQueryHandler } = require('../commands/api');
|
|
21
|
-
const { requireConfig } = require('../lib/config-validator');
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { listCourses } from '../commands/list.js';
|
|
12
|
+
import { showConfig, setupConfig, editConfig, showConfigPath, deleteConfigFile } from '../commands/config.js';
|
|
13
|
+
import { listAssignments } from '../commands/assignments.js';
|
|
14
|
+
import { showGrades } from '../commands/grades.js';
|
|
15
|
+
import { showAnnouncements } from '../commands/announcements.js';
|
|
16
|
+
import { showProfile } from '../commands/profile.js';
|
|
17
|
+
import { submitAssignment } from '../commands/submit.js';
|
|
18
|
+
import { requireConfig } from '../lib/config-validator.js';
|
|
22
19
|
|
|
23
20
|
const program = new Command();
|
|
24
21
|
|
|
@@ -26,26 +23,7 @@ const program = new Command();
|
|
|
26
23
|
program
|
|
27
24
|
.name('canvas')
|
|
28
25
|
.description('Canvas API Command Line Tool')
|
|
29
|
-
.version('1.3.
|
|
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');
|
|
26
|
+
.version('1.3.3');
|
|
49
27
|
|
|
50
28
|
// List command to show enrolled courses
|
|
51
29
|
program
|
|
@@ -54,42 +32,42 @@ program
|
|
|
54
32
|
.description('List starred courses (default) or all courses with -a')
|
|
55
33
|
.option('-a, --all', 'Show all enrolled courses instead of just starred ones')
|
|
56
34
|
.option('-v, --verbose', 'Show detailed course information')
|
|
57
|
-
.action(requireConfig(listCourses));
|
|
35
|
+
.action((...args) => requireConfig(listCourses)(...args));
|
|
58
36
|
|
|
59
37
|
// Config command with subcommands
|
|
60
38
|
const configCommand = program
|
|
61
39
|
.command('config')
|
|
62
40
|
.description('Manage Canvas CLI configuration')
|
|
63
|
-
.action(showConfig); // Default action when no subcommand is provided
|
|
41
|
+
.action((...args) => showConfig(...args)); // Default action when no subcommand is provided
|
|
64
42
|
|
|
65
43
|
configCommand
|
|
66
44
|
.command('show')
|
|
67
45
|
.alias('status')
|
|
68
46
|
.description('Show current configuration')
|
|
69
|
-
.action(showConfig);
|
|
47
|
+
.action((...args) => showConfig(...args));
|
|
70
48
|
|
|
71
49
|
configCommand
|
|
72
50
|
.command('setup')
|
|
73
51
|
.alias('init')
|
|
74
52
|
.description('Interactive configuration setup')
|
|
75
|
-
.action(setupConfig);
|
|
53
|
+
.action((...args) => setupConfig(...args));
|
|
76
54
|
|
|
77
55
|
configCommand
|
|
78
56
|
.command('edit')
|
|
79
57
|
.alias('update')
|
|
80
58
|
.description('Edit existing configuration')
|
|
81
|
-
.action(editConfig);
|
|
59
|
+
.action((...args) => editConfig(...args));
|
|
82
60
|
|
|
83
61
|
configCommand
|
|
84
62
|
.command('path')
|
|
85
63
|
.description('Show configuration file path')
|
|
86
|
-
.action(showConfigPath);
|
|
64
|
+
.action((...args) => showConfigPath(...args));
|
|
87
65
|
|
|
88
66
|
configCommand
|
|
89
67
|
.command('delete')
|
|
90
68
|
.alias('remove')
|
|
91
69
|
.description('Delete configuration file')
|
|
92
|
-
.action(deleteConfigFile);
|
|
70
|
+
.action((...args) => deleteConfigFile(...args));
|
|
93
71
|
|
|
94
72
|
// Assignments command to show assignments for a course
|
|
95
73
|
program
|
|
@@ -100,7 +78,7 @@ program
|
|
|
100
78
|
.option('-v, --verbose', 'Show detailed assignment information')
|
|
101
79
|
.option('-s, --submitted', 'Only show submitted assignments')
|
|
102
80
|
.option('-p, --pending', 'Only show pending assignments')
|
|
103
|
-
.action(requireConfig(listAssignments));
|
|
81
|
+
.action((...args) => requireConfig(listAssignments)(...args));
|
|
104
82
|
|
|
105
83
|
// Grades command to show grades
|
|
106
84
|
program
|
|
@@ -109,16 +87,16 @@ program
|
|
|
109
87
|
.description('Show grades for all courses or a specific course')
|
|
110
88
|
.argument('[course-id]', 'Optional course ID to get grades for specific course')
|
|
111
89
|
.option('-v, --verbose', 'Show detailed grade information')
|
|
112
|
-
.action(requireConfig(showGrades));
|
|
90
|
+
.action((...args) => requireConfig(showGrades)(...args));
|
|
113
91
|
|
|
114
92
|
// Announcements command
|
|
115
93
|
program
|
|
116
94
|
.command('announcements')
|
|
117
|
-
.alias('
|
|
118
|
-
.description('Show recent announcements')
|
|
95
|
+
.alias('an')
|
|
96
|
+
.description('Show recent announcements (interactive if no course-id)')
|
|
119
97
|
.argument('[course-id]', 'Optional course ID to get announcements for specific course')
|
|
120
98
|
.option('-l, --limit <number>', 'Number of announcements to show', '5')
|
|
121
|
-
.action(requireConfig(showAnnouncements));
|
|
99
|
+
.action((...args) => requireConfig(showAnnouncements)(...args));
|
|
122
100
|
|
|
123
101
|
// Profile command
|
|
124
102
|
program
|
|
@@ -126,17 +104,17 @@ program
|
|
|
126
104
|
.alias('me')
|
|
127
105
|
.description('Show current user profile information')
|
|
128
106
|
.option('-v, --verbose', 'Show detailed profile information')
|
|
129
|
-
.action(requireConfig(showProfile));
|
|
107
|
+
.action((...args) => requireConfig(showProfile)(...args));
|
|
130
108
|
|
|
131
|
-
// Submit command for interactive assignment submission
|
|
109
|
+
// Submit command for interactive assignment submission (always list files in current directory)
|
|
132
110
|
program
|
|
133
111
|
.command('submit')
|
|
134
112
|
.alias('sub')
|
|
135
113
|
.description('Interactively submit one or multiple files to an assignment')
|
|
136
114
|
.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
115
|
.option('-f, --file <file-path>', 'Skip file selection and use specific file path')
|
|
139
|
-
.
|
|
116
|
+
.option('-a, --all', 'Show all enrolled courses instead of just starred ones')
|
|
117
|
+
.action((...args) => requireConfig(submitAssignment)(...args));
|
|
140
118
|
|
|
141
119
|
// Parse command line arguments
|
|
142
120
|
program.parse();
|