canvaslms-cli 1.3.3 → 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.
@@ -2,98 +2,84 @@
2
2
  * Submit command for interactive assignment submission
3
3
  */
4
4
 
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { makeCanvasRequest } = require('../lib/api-client');
8
- const { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved } = require('../lib/interactive');
9
- const { uploadSingleFileToCanvas, submitAssignmentWithFiles } = require('../lib/file-upload');
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { makeCanvasRequest } from '../lib/api-client.js';
8
+ import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved, pad } from '../lib/interactive.js';
9
+ import { uploadSingleFileToCanvas, submitAssignmentWithFiles } from '../lib/file-upload.js';
10
+ import chalk from 'chalk';
10
11
 
11
- async function submitAssignment(options) {
12
+ export async function submitAssignment(options) {
12
13
  const rl = createReadlineInterface();
13
14
 
14
15
  try {
15
16
  let courseId = options.course;
16
17
  let assignmentId = options.assignment;
17
- let filePath = options.file;
18
18
  let selectedCourse = null;
19
19
  let selectedAssignment = null;
20
20
 
21
21
  // Step 1: Select Course (if not provided)
22
- if (!courseId) {
23
- console.log('šŸ“š Loading your starred courses...\n');
24
-
22
+ while (!courseId) {
23
+ console.log(chalk.cyan.bold('\n' + '-'.repeat(60)));
24
+ console.log(chalk.cyan.bold('Loading your courses, please wait...'));
25
25
  const courses = await makeCanvasRequest('get', 'courses', [
26
26
  'enrollment_state=active',
27
27
  'include[]=favorites'
28
28
  ]);
29
-
30
29
  if (!courses || courses.length === 0) {
31
- console.log('No courses found.');
30
+ console.log(chalk.red('Error: No courses found.'));
32
31
  rl.close();
33
32
  return;
34
33
  }
35
-
36
- // Filter for starred courses by default
37
- let starredCourses = courses.filter(course => course.is_favorite);
38
-
39
- if (starredCourses.length === 0) {
40
- console.log('No starred courses found. Showing all enrolled courses...\n');
41
- starredCourses = courses;
34
+ let selectableCourses = courses;
35
+ if (!options.all) {
36
+ selectableCourses = courses.filter(course => course.is_favorite);
37
+ if (selectableCourses.length === 0) {
38
+ console.log(chalk.red('Error: No starred courses found. Showing all enrolled courses...'));
39
+ selectableCourses = courses;
40
+ }
42
41
  }
43
-
44
- console.log('Select a course:');
45
- starredCourses.forEach((course, index) => {
46
- const starIcon = course.is_favorite ? '⭐ ' : '';
47
- console.log(`${index + 1}. ${starIcon}${course.name}`);
42
+ console.log(chalk.cyan('-'.repeat(60)));
43
+ console.log(chalk.cyan.bold('Select a course:'));
44
+ selectableCourses.forEach((course, index) => {
45
+ console.log(pad(chalk.white((index + 1) + '. '), 5) + chalk.white(course.name));
48
46
  });
49
- const courseChoice = await askQuestion(rl, '\nEnter course number: ');
50
-
51
- // Handle empty input
52
- if (!courseChoice.trim()) {
53
- console.log('No course selected. Exiting...');
47
+ const courseChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter course number (or ".."/"back" to cancel): '));
48
+ if (courseChoice === '..' || courseChoice.toLowerCase() === 'back') {
54
49
  rl.close();
55
50
  return;
56
51
  }
57
-
58
- const courseIndex = parseInt(courseChoice) - 1;
59
-
60
- if (courseIndex < 0 || courseIndex >= starredCourses.length) {
61
- console.log('Invalid course selection.');
52
+ if (!courseChoice.trim()) {
53
+ console.log(chalk.red('Error: No course selected. Exiting...'));
62
54
  rl.close();
63
55
  return;
64
56
  }
65
-
66
- selectedCourse = starredCourses[courseIndex];
67
- courseId = selectedCourse.id;
68
- console.log(`āœ… Selected: ${selectedCourse.name}\n`);
69
- } else {
70
- // Fetch course details if ID was provided
71
- try {
72
- selectedCourse = await makeCanvasRequest('get', `courses/${courseId}`);
73
- } catch (error) {
74
- console.log(`āš ļø Could not fetch course details for ID ${courseId}`);
75
- selectedCourse = { id: courseId, name: `Course ${courseId}` };
57
+ const courseIndex = parseInt(courseChoice) - 1;
58
+ if (courseIndex < 0 || courseIndex >= selectableCourses.length) {
59
+ console.log(chalk.red('Error: Invalid course selection.'));
60
+ continue;
76
61
  }
62
+ selectedCourse = selectableCourses[courseIndex];
63
+ courseId = selectedCourse.id;
64
+ console.log(chalk.green(`Success: Selected ${selectedCourse.name}\n`));
77
65
  }
78
66
 
79
67
  // Step 2: Select Assignment (if not provided)
80
- if (!assignmentId) {
81
- console.log('šŸ“ Loading assignments...\n');
82
-
68
+ while (!assignmentId) {
69
+ console.log(chalk.cyan.bold('-'.repeat(60)));
70
+ console.log(chalk.cyan.bold('Loading assignments, please wait...'));
83
71
  const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
84
72
  'include[]=submission',
85
73
  'order_by=due_at',
86
74
  'per_page=100'
87
75
  ]);
88
-
89
76
  if (!assignments || assignments.length === 0) {
90
- console.log('No assignments found for this course.');
77
+ console.log(chalk.red('Error: No assignments found for this course.'));
91
78
  rl.close();
92
79
  return;
93
80
  }
94
-
95
- console.log(`Found ${assignments.length} assignment(s):\n`);
96
-
81
+ console.log(chalk.cyan('-'.repeat(60)));
82
+ console.log(chalk.cyan.bold(`Found ${assignments.length} assignment(s):`));
97
83
  // Show summary of assignment statuses
98
84
  const submittedCount = assignments.filter(a => a.submission && a.submission.submitted_at).length;
99
85
  const pendingCount = assignments.length - submittedCount;
@@ -102,23 +88,15 @@ async function submitAssignment(options) {
102
88
  a.submission_types.includes('online_upload') &&
103
89
  a.workflow_state === 'published'
104
90
  ).length;
105
-
106
- console.log(`šŸ“Š Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads\n`);
107
- console.log('Select an assignment:');
91
+ console.log(chalk.yellow(`Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads`));
92
+ console.log(chalk.cyan('-'.repeat(60)));
93
+ // Numbered menu
108
94
  assignments.forEach((assignment, index) => {
109
95
  const dueDate = assignment.due_at ? new Date(assignment.due_at).toLocaleDateString() : 'No due date';
110
- const submitted = assignment.submission && assignment.submission.submitted_at ? 'āœ…' : 'āŒ';
111
-
112
- // Check if assignment accepts file uploads
113
- const canSubmitFiles = assignment.submission_types &&
114
- assignment.submission_types.includes('online_upload') &&
115
- assignment.workflow_state === 'published';
116
- const submissionIcon = canSubmitFiles ? 'šŸ“¤' : 'šŸ“‹';
117
-
118
- // Format grade like Canvas web interface
96
+ const submitted = assignment.submission && assignment.submission.submitted_at ? chalk.green('Submitted') : chalk.yellow('Not submitted');
97
+ const canSubmitFiles = assignment.submission_types && assignment.submission_types.includes('online_upload') && assignment.workflow_state === 'published';
119
98
  let gradeDisplay = '';
120
99
  const submission = assignment.submission;
121
-
122
100
  if (submission && submission.score !== null && submission.score !== undefined) {
123
101
  const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
124
102
  const total = assignment.points_possible || 0;
@@ -130,145 +108,118 @@ async function submitAssignment(options) {
130
108
  } else if (assignment.points_possible) {
131
109
  gradeDisplay = ` | Grade: –/${assignment.points_possible}`;
132
110
  }
133
- console.log(`${index + 1}. ${submissionIcon} ${assignment.name} ${submitted}`);
134
- console.log(` Due: ${dueDate} | Points: ${assignment.points_possible || 'N/A'}${gradeDisplay}`);
111
+ let line = pad(chalk.white((index + 1) + '. '), 5) + chalk.white(assignment.name) + chalk.gray(` (${dueDate})`) + ' ' + submitted + gradeDisplay;
135
112
  if (!canSubmitFiles) {
136
- console.log(` šŸ“‹ Note: This assignment doesn't accept file uploads`);
113
+ line += chalk.red(' [No file uploads]');
137
114
  }
115
+ console.log(line);
138
116
  });
139
- const assignmentChoice = await askQuestion(rl, '\nEnter assignment number: ');
140
-
141
- // Handle empty input
117
+ const assignmentChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter assignment number (or ".."/"back" to re-select course): '));
118
+ if (assignmentChoice === '..' || assignmentChoice.toLowerCase() === 'back') {
119
+ courseId = null;
120
+ selectedCourse = null;
121
+ break;
122
+ }
142
123
  if (!assignmentChoice.trim()) {
143
- console.log('No assignment selected. Exiting...');
124
+ console.log(chalk.red('Error: No assignment selected. Exiting...'));
144
125
  rl.close();
145
126
  return;
146
127
  }
147
-
148
128
  const assignmentIndex = parseInt(assignmentChoice) - 1;
149
-
150
129
  if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
151
- console.log('Invalid assignment selection.');
152
- rl.close();
153
- return;
130
+ console.log(chalk.red('Error: Invalid assignment selection.'));
131
+ continue;
154
132
  }
155
-
156
133
  selectedAssignment = assignments[assignmentIndex];
157
-
158
- // Check if assignment accepts file uploads
159
- if (!selectedAssignment.submission_types ||
160
- !selectedAssignment.submission_types.includes('online_upload') ||
161
- selectedAssignment.workflow_state !== 'published') {
162
- console.log('āŒ This assignment does not accept file uploads or is not published.');
134
+ if (!selectedAssignment.submission_types || !selectedAssignment.submission_types.includes('online_upload') || selectedAssignment.workflow_state !== 'published') {
135
+ console.log(chalk.red('Error: This assignment does not accept file uploads or is not published.'));
163
136
  rl.close();
164
137
  return;
165
138
  }
166
-
167
139
  assignmentId = selectedAssignment.id;
168
- console.log(`āœ… Selected: ${selectedAssignment.name}\n`);
169
-
170
- // Check if already submitted
140
+ console.log(chalk.green(`Success: Selected ${selectedAssignment.name}\n`));
171
141
  if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
172
- const resubmit = await askConfirmation(rl, 'This assignment has already been submitted. Do you want to resubmit?', false);
142
+ const resubmit = await askConfirmation(rl, chalk.yellow('This assignment has already been submitted. Do you want to resubmit?'), false);
173
143
  if (!resubmit) {
174
- console.log('Submission cancelled.');
144
+ console.log(chalk.yellow('Submission cancelled.'));
175
145
  rl.close();
176
146
  return;
177
147
  }
178
148
  }
179
- } else {
180
- // Fetch assignment details if ID was provided
181
- try {
182
- selectedAssignment = await makeCanvasRequest('get', `courses/${courseId}/assignments/${assignmentId}`);
183
- console.log(`āœ… Using assignment: ${selectedAssignment.name}\n`);
184
- } catch (error) {
185
- console.log(`āš ļø Could not fetch assignment details for ID ${assignmentId}`);
186
- selectedAssignment = { id: assignmentId, name: `Assignment ${assignmentId}` };
187
- }
188
- } // Step 3: Select Files (if not provided)
189
- let filePaths = [];
190
- if (filePath) {
191
- filePaths = [filePath]; // Single file provided via option
192
- } else {
193
- console.log('\nšŸ“ File Selection');
194
- console.log(`šŸ“š Course: ${selectedCourse.name}`);
195
- console.log(`šŸ“ Assignment: ${selectedAssignment.name}\n`);
196
-
197
- filePaths = await selectFilesImproved(rl);
198
149
  }
199
-
150
+ // Step 3: Always list files in current directory and prompt user to select
151
+ let filePaths = [];
152
+ console.log(chalk.cyan.bold('-'.repeat(60)));
153
+ console.log(chalk.cyan.bold('File Selection'));
154
+ console.log(chalk.cyan('-'.repeat(60)));
155
+ console.log(chalk.white('Course: ') + chalk.bold(selectedCourse.name));
156
+ console.log(chalk.white('Assignment: ') + chalk.bold(selectedAssignment.name) + '\n');
157
+ filePaths = await selectFilesImproved(rl);
200
158
  // Validate all selected files exist
201
159
  const validFiles = [];
202
160
  for (const file of filePaths) {
203
161
  if (fs.existsSync(file)) {
204
162
  validFiles.push(file);
205
163
  } else {
206
- console.log(`āš ļø File not found: ${file}`);
164
+ console.log(chalk.red('Error: File not found: ' + file));
207
165
  }
208
166
  }
209
-
210
167
  if (validFiles.length === 0) {
211
- console.log('No valid files selected.');
168
+ console.log(chalk.red('Error: No valid files selected.'));
212
169
  rl.close();
213
170
  return;
214
171
  }
215
-
216
172
  filePaths = validFiles;
217
-
218
173
  // Step 4: Confirm and Submit
219
- console.log('\nšŸ“‹ Submission Summary:');
220
- console.log(`šŸ“š Course: ${selectedCourse?.name || 'Unknown Course'}`);
221
- console.log(`šŸ“ Assignment: ${selectedAssignment?.name || 'Unknown Assignment'}`);
222
- console.log(`šŸ“ Files (${filePaths.length}):`);
174
+ console.log(chalk.cyan.bold('-'.repeat(60)));
175
+ console.log(chalk.cyan.bold('Submission Summary:'));
176
+ console.log(chalk.cyan('-'.repeat(60)));
177
+ console.log(chalk.white('Course: ') + chalk.bold(selectedCourse?.name || 'Unknown Course'));
178
+ console.log(chalk.white('Assignment: ') + chalk.bold(selectedAssignment?.name || 'Unknown Assignment'));
179
+ console.log(chalk.white(`Files (${filePaths.length}):`));
223
180
  filePaths.forEach((file, index) => {
224
181
  const stats = fs.statSync(file);
225
182
  const size = (stats.size / 1024).toFixed(1) + ' KB';
226
- console.log(` ${index + 1}. ${path.basename(file)} (${size})`);
183
+ console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.gray(size));
227
184
  });
228
-
229
- const confirm = await askConfirmation(rl, '\nProceed with submission?', true);
185
+ const confirm = await askConfirmation(rl, chalk.bold.cyan('\nProceed with submission?'), true);
230
186
  if (!confirm) {
231
- console.log('Submission cancelled.');
187
+ console.log(chalk.yellow('Submission cancelled.'));
232
188
  rl.close();
233
189
  return;
234
190
  }
235
-
236
- console.log('\nšŸš€ Uploading files...');
237
-
191
+ console.log(chalk.cyan.bold('\nUploading files, please wait...'));
238
192
  // Upload all files
239
193
  const uploadedFileIds = [];
240
194
  for (let i = 0; i < filePaths.length; i++) {
241
195
  const currentFile = filePaths[i];
242
- console.log(`šŸ“¤ Uploading ${i + 1}/${filePaths.length}: ${path.basename(currentFile)}`);
243
-
196
+ process.stdout.write(chalk.yellow(`Uploading ${i + 1}/${filePaths.length}: ${path.basename(currentFile)} ... `));
244
197
  try {
245
198
  const fileId = await uploadSingleFileToCanvas(courseId, assignmentId, currentFile);
246
199
  uploadedFileIds.push(fileId);
247
- console.log(`āœ… ${path.basename(currentFile)} uploaded successfully`); } catch (error) {
248
- console.error(`āŒ Failed to upload ${currentFile}: ${error.message}`);
249
- const continueUpload = await askConfirmation(rl, 'Continue with remaining files?', true);
200
+ console.log(chalk.green('Success: Uploaded.'));
201
+ } catch (error) {
202
+ console.error(chalk.red(`Error: Failed to upload ${currentFile}: ${error.message}`));
203
+ const continueUpload = await askConfirmation(rl, chalk.yellow('Continue with remaining files?'), true);
250
204
  if (!continueUpload) {
251
205
  break;
252
206
  }
253
207
  }
254
208
  }
255
-
256
- if (uploadedFileIds.length === 0) {
257
- console.log('āŒ No files were uploaded successfully.');
258
- rl.close();
259
- return;
209
+ // Submit assignment with uploaded files
210
+ if (uploadedFileIds.length > 0) {
211
+ try {
212
+ console.log(chalk.cyan.bold('Submitting assignment, please wait...'));
213
+ await submitAssignmentWithFiles(courseId, assignmentId, uploadedFileIds);
214
+ console.log(chalk.green('Success: Assignment submitted successfully!'));
215
+ } catch (error) {
216
+ console.error(chalk.red('Error: Failed to submit assignment: ' + error.message));
217
+ }
218
+ } else {
219
+ console.log(chalk.red('Error: No files were uploaded. Submission not completed.'));
260
220
  }
261
-
262
- // Submit the assignment with all uploaded files
263
- console.log('\nšŸ“ Submitting assignment...');
264
- const submission = await submitAssignmentWithFiles(courseId, assignmentId, uploadedFileIds);
265
-
266
- console.log(`āœ… Assignment submitted successfully with ${uploadedFileIds.length} file(s)!`);
267
- console.log(`Submission ID: ${submission.id}`);
268
- console.log(`Submitted at: ${new Date(submission.submitted_at).toLocaleString()}`);
269
-
270
221
  } catch (error) {
271
- console.error('āŒ Submission failed:', error.message);
222
+ console.error(chalk.red('Error: Submission failed: ' + error.message));
272
223
  } finally {
273
224
  rl.close();
274
225
  }
@@ -394,7 +345,3 @@ async function selectSingleFileFromList(rl, files) {
394
345
  return [manualFile];
395
346
  }
396
347
  }
397
-
398
- module.exports = {
399
- submitAssignment
400
- };
package/lib/api-client.js CHANGED
@@ -2,14 +2,14 @@
2
2
  * Canvas API client
3
3
  */
4
4
 
5
- const axios = require('axios');
6
- const fs = require('fs');
7
- const { getInstanceConfig } = require('./config');
5
+ import axios from 'axios';
6
+ import fs from 'fs';
7
+ import { getInstanceConfig } from './config.js';
8
8
 
9
9
  /**
10
10
  * Make Canvas API request
11
11
  */
12
- async function makeCanvasRequest(method, endpoint, queryParams = [], requestBody = null) {
12
+ export async function makeCanvasRequest(method, endpoint, queryParams = [], requestBody = null) {
13
13
  const instanceConfig = getInstanceConfig();
14
14
 
15
15
  // Construct the full URL
@@ -73,7 +73,3 @@ async function makeCanvasRequest(method, endpoint, queryParams = [], requestBody
73
73
  process.exit(1);
74
74
  }
75
75
  }
76
-
77
- module.exports = {
78
- makeCanvasRequest
79
- };
@@ -1,63 +1,60 @@
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
- };
1
+ /**
2
+ * Configuration validation and setup helpers
3
+ */
4
+
5
+ import { configExists, readConfig } from './config.js';
6
+ import { createReadlineInterface, askQuestion } from './interactive.js';
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('\nLet\'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('\nTo 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 } = await import('../commands/config.js');
28
+ await setupConfig();
29
+
30
+ // Check if setup was successful
31
+ if (!configExists()) {
32
+ console.log('\nConfiguration setup was not completed. Please run "canvas config setup" to try again.');
33
+ process.exit(1);
34
+ }
35
+
36
+ console.log('\nConfiguration 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
+ export function requireConfig(fn) {
54
+ return async function(...args) {
55
+ await ensureConfig();
56
+ return fn(...args);
57
+ };
58
+ }
59
+
60
+ export { ensureConfig };