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.
@@ -2,91 +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
-
50
- const courseChoice = await askQuestion(rl, '\nEnter course number: ');
51
- const courseIndex = parseInt(courseChoice) - 1;
52
-
53
- if (courseIndex < 0 || courseIndex >= starredCourses.length) {
54
- console.log('Invalid course selection.');
47
+ const courseChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter course number (or ".."/"back" to cancel): '));
48
+ if (courseChoice === '..' || courseChoice.toLowerCase() === 'back') {
55
49
  rl.close();
56
50
  return;
57
51
  }
58
-
59
- selectedCourse = starredCourses[courseIndex];
60
- courseId = selectedCourse.id;
61
- console.log(`āœ… Selected: ${selectedCourse.name}\n`);
62
- } else {
63
- // Fetch course details if ID was provided
64
- try {
65
- selectedCourse = await makeCanvasRequest('get', `courses/${courseId}`);
66
- } catch (error) {
67
- console.log(`āš ļø Could not fetch course details for ID ${courseId}`);
68
- selectedCourse = { id: courseId, name: `Course ${courseId}` };
52
+ if (!courseChoice.trim()) {
53
+ console.log(chalk.red('Error: No course selected. Exiting...'));
54
+ rl.close();
55
+ return;
69
56
  }
57
+ const courseIndex = parseInt(courseChoice) - 1;
58
+ if (courseIndex < 0 || courseIndex >= selectableCourses.length) {
59
+ console.log(chalk.red('Error: Invalid course selection.'));
60
+ continue;
61
+ }
62
+ selectedCourse = selectableCourses[courseIndex];
63
+ courseId = selectedCourse.id;
64
+ console.log(chalk.green(`Success: Selected ${selectedCourse.name}\n`));
70
65
  }
71
66
 
72
67
  // Step 2: Select Assignment (if not provided)
73
- if (!assignmentId) {
74
- console.log('šŸ“ Loading assignments...\n');
75
-
68
+ while (!assignmentId) {
69
+ console.log(chalk.cyan.bold('-'.repeat(60)));
70
+ console.log(chalk.cyan.bold('Loading assignments, please wait...'));
76
71
  const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, [
77
72
  'include[]=submission',
78
73
  'order_by=due_at',
79
74
  'per_page=100'
80
75
  ]);
81
-
82
76
  if (!assignments || assignments.length === 0) {
83
- console.log('No assignments found for this course.');
77
+ console.log(chalk.red('Error: No assignments found for this course.'));
84
78
  rl.close();
85
79
  return;
86
80
  }
87
-
88
- console.log(`Found ${assignments.length} assignment(s):\n`);
89
-
81
+ console.log(chalk.cyan('-'.repeat(60)));
82
+ console.log(chalk.cyan.bold(`Found ${assignments.length} assignment(s):`));
90
83
  // Show summary of assignment statuses
91
84
  const submittedCount = assignments.filter(a => a.submission && a.submission.submitted_at).length;
92
85
  const pendingCount = assignments.length - submittedCount;
@@ -95,23 +88,15 @@ async function submitAssignment(options) {
95
88
  a.submission_types.includes('online_upload') &&
96
89
  a.workflow_state === 'published'
97
90
  ).length;
98
-
99
- console.log(`šŸ“Š Summary: ${submittedCount} submitted, ${pendingCount} pending, ${uploadableCount} accept file uploads\n`);
100
- 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
101
94
  assignments.forEach((assignment, index) => {
102
95
  const dueDate = assignment.due_at ? new Date(assignment.due_at).toLocaleDateString() : 'No due date';
103
- const submitted = assignment.submission && assignment.submission.submitted_at ? 'āœ…' : 'āŒ';
104
-
105
- // Check if assignment accepts file uploads
106
- const canSubmitFiles = assignment.submission_types &&
107
- assignment.submission_types.includes('online_upload') &&
108
- assignment.workflow_state === 'published';
109
- const submissionIcon = canSubmitFiles ? 'šŸ“¤' : 'šŸ“‹';
110
-
111
- // 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';
112
98
  let gradeDisplay = '';
113
99
  const submission = assignment.submission;
114
-
115
100
  if (submission && submission.score !== null && submission.score !== undefined) {
116
101
  const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
117
102
  const total = assignment.points_possible || 0;
@@ -123,137 +108,118 @@ async function submitAssignment(options) {
123
108
  } else if (assignment.points_possible) {
124
109
  gradeDisplay = ` | Grade: –/${assignment.points_possible}`;
125
110
  }
126
-
127
- console.log(`${index + 1}. ${submissionIcon} ${assignment.name} ${submitted}`);
128
- 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;
129
112
  if (!canSubmitFiles) {
130
- console.log(` šŸ“‹ Note: This assignment doesn't accept file uploads`);
113
+ line += chalk.red(' [No file uploads]');
131
114
  }
115
+ console.log(line);
132
116
  });
133
- const assignmentIndex = parseInt(assignmentChoice) - 1;
134
-
135
- if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
136
- console.log('Invalid assignment selection.');
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
+ }
123
+ if (!assignmentChoice.trim()) {
124
+ console.log(chalk.red('Error: No assignment selected. Exiting...'));
137
125
  rl.close();
138
126
  return;
139
127
  }
140
-
128
+ const assignmentIndex = parseInt(assignmentChoice) - 1;
129
+ if (assignmentIndex < 0 || assignmentIndex >= assignments.length) {
130
+ console.log(chalk.red('Error: Invalid assignment selection.'));
131
+ continue;
132
+ }
141
133
  selectedAssignment = assignments[assignmentIndex];
142
-
143
- // Check if assignment accepts file uploads
144
- if (!selectedAssignment.submission_types ||
145
- !selectedAssignment.submission_types.includes('online_upload') ||
146
- selectedAssignment.workflow_state !== 'published') {
147
- 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.'));
148
136
  rl.close();
149
137
  return;
150
138
  }
151
-
152
139
  assignmentId = selectedAssignment.id;
153
- console.log(`āœ… Selected: ${selectedAssignment.name}\n`);
154
-
155
- // Check if already submitted
140
+ console.log(chalk.green(`Success: Selected ${selectedAssignment.name}\n`));
156
141
  if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
157
- 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);
158
143
  if (!resubmit) {
159
- console.log('Submission cancelled.');
144
+ console.log(chalk.yellow('Submission cancelled.'));
160
145
  rl.close();
161
146
  return;
162
147
  }
163
148
  }
164
- } else {
165
- // Fetch assignment details if ID was provided
166
- try {
167
- selectedAssignment = await makeCanvasRequest('get', `courses/${courseId}/assignments/${assignmentId}`);
168
- console.log(`āœ… Using assignment: ${selectedAssignment.name}\n`);
169
- } catch (error) {
170
- console.log(`āš ļø Could not fetch assignment details for ID ${assignmentId}`);
171
- selectedAssignment = { id: assignmentId, name: `Assignment ${assignmentId}` };
172
- }
173
- } // Step 3: Select Files (if not provided)
174
- let filePaths = [];
175
- if (filePath) {
176
- filePaths = [filePath]; // Single file provided via option
177
- } else {
178
- console.log('\nšŸ“ File Selection');
179
- console.log(`šŸ“š Course: ${selectedCourse.name}`);
180
- console.log(`šŸ“ Assignment: ${selectedAssignment.name}\n`);
181
-
182
- filePaths = await selectFilesImproved(rl);
183
149
  }
184
-
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);
185
158
  // Validate all selected files exist
186
159
  const validFiles = [];
187
160
  for (const file of filePaths) {
188
161
  if (fs.existsSync(file)) {
189
162
  validFiles.push(file);
190
163
  } else {
191
- console.log(`āš ļø File not found: ${file}`);
164
+ console.log(chalk.red('Error: File not found: ' + file));
192
165
  }
193
166
  }
194
-
195
167
  if (validFiles.length === 0) {
196
- console.log('No valid files selected.');
168
+ console.log(chalk.red('Error: No valid files selected.'));
197
169
  rl.close();
198
170
  return;
199
171
  }
200
-
201
172
  filePaths = validFiles;
202
-
203
173
  // Step 4: Confirm and Submit
204
- console.log('\nšŸ“‹ Submission Summary:');
205
- console.log(`šŸ“š Course: ${selectedCourse?.name || 'Unknown Course'}`);
206
- console.log(`šŸ“ Assignment: ${selectedAssignment?.name || 'Unknown Assignment'}`);
207
- 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}):`));
208
180
  filePaths.forEach((file, index) => {
209
181
  const stats = fs.statSync(file);
210
182
  const size = (stats.size / 1024).toFixed(1) + ' KB';
211
- 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));
212
184
  });
213
-
214
- const confirm = await askConfirmation(rl, '\nProceed with submission?', true);
185
+ const confirm = await askConfirmation(rl, chalk.bold.cyan('\nProceed with submission?'), true);
215
186
  if (!confirm) {
216
- console.log('Submission cancelled.');
187
+ console.log(chalk.yellow('Submission cancelled.'));
217
188
  rl.close();
218
189
  return;
219
190
  }
220
-
221
- console.log('\nšŸš€ Uploading files...');
222
-
191
+ console.log(chalk.cyan.bold('\nUploading files, please wait...'));
223
192
  // Upload all files
224
193
  const uploadedFileIds = [];
225
194
  for (let i = 0; i < filePaths.length; i++) {
226
195
  const currentFile = filePaths[i];
227
- console.log(`šŸ“¤ Uploading ${i + 1}/${filePaths.length}: ${path.basename(currentFile)}`);
228
-
196
+ process.stdout.write(chalk.yellow(`Uploading ${i + 1}/${filePaths.length}: ${path.basename(currentFile)} ... `));
229
197
  try {
230
198
  const fileId = await uploadSingleFileToCanvas(courseId, assignmentId, currentFile);
231
199
  uploadedFileIds.push(fileId);
232
- console.log(`āœ… ${path.basename(currentFile)} uploaded successfully`); } catch (error) {
233
- console.error(`āŒ Failed to upload ${currentFile}: ${error.message}`);
234
- 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);
235
204
  if (!continueUpload) {
236
205
  break;
237
206
  }
238
207
  }
239
208
  }
240
-
241
- if (uploadedFileIds.length === 0) {
242
- console.log('āŒ No files were uploaded successfully.');
243
- rl.close();
244
- 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.'));
245
220
  }
246
-
247
- // Submit the assignment with all uploaded files
248
- console.log('\nšŸ“ Submitting assignment...');
249
- const submission = await submitAssignmentWithFiles(courseId, assignmentId, uploadedFileIds);
250
-
251
- console.log(`āœ… Assignment submitted successfully with ${uploadedFileIds.length} file(s)!`);
252
- console.log(`Submission ID: ${submission.id}`);
253
- console.log(`Submitted at: ${new Date(submission.submitted_at).toLocaleString()}`);
254
-
255
221
  } catch (error) {
256
- console.error('āŒ Submission failed:', error.message);
222
+ console.error(chalk.red('Error: Submission failed: ' + error.message));
257
223
  } finally {
258
224
  rl.close();
259
225
  }
@@ -379,7 +345,3 @@ async function selectSingleFileFromList(rl, files) {
379
345
  return [manualFile];
380
346
  }
381
347
  }
382
-
383
- module.exports = {
384
- submitAssignment
385
- };
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 };