canvaslms-cli 1.3.3 → 1.4.1

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,9 +2,11 @@
2
2
  * Interactive prompt utilities
3
3
  */
4
4
 
5
- const readline = require('readline');
6
- const fs = require('fs');
7
- const path = require('path');
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
- const files = fs.readdirSync(currentDir);
109
- const matchedFiles = [];
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
- if (pattern.startsWith('*.')) {
114
- // Handle *.extension patterns
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
- files.forEach(file => {
126
- const filePath = path.join(currentDir, file);
127
- const stats = fs.statSync(filePath);
128
-
129
- if (stats.isFile() && regexPattern.test(file)) {
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('\nšŸ“ Enhanced File Selection');
148
- console.log('šŸ’” Tips:');
184
+ console.log(chalk.cyan.bold('\n' + '-'.repeat(50)));
185
+ console.log(chalk.cyan.bold('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(`\nšŸ“‹ Currently selected (${selectedFiles.length} files):`);
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(` ${index + 1}. ${path.basename(file)} (${size})`);
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
- const input = await askQuestion(rl, '\nšŸ“Ž Add file (or press Enter to finish): ');
167
-
168
- if (!input.trim()) {
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
- // Show available files
175
- console.log('\nšŸ“‚ Available files in current directory:');
211
+ console.log(chalk.cyan('\n' + '-'.repeat(50)));
212
+ console.log(chalk.cyan.bold('Browsing available files:'));
176
213
  try {
177
- const files = fs.readdirSync(currentDir);
178
- const filteredFiles = files.filter(file => {
179
- const filePath = path.join(currentDir, file);
180
- const stats = fs.statSync(filePath);
181
- return stats.isFile() &&
182
- !file.startsWith('.') &&
183
- !['package.json', 'package-lock.json', 'node_modules'].includes(file);
184
- });
185
-
186
- if (filteredFiles.length === 0) {
187
- console.log(' No suitable files found.');
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
- filteredFiles.forEach((file, index) => {
190
- const filePath = path.join(currentDir, file);
191
- const stats = fs.statSync(filePath);
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(` Error reading directory: ${error.message}`);
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('āŒ No files selected to remove.');
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(`${index + 1}. ${path.basename(file)}`);
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(`āœ… Removed: ${path.basename(removedFile)}`);
259
+ console.log(chalk.green(`Removed: ${path.basename(removedFile)}`));
222
260
  } else {
223
- console.log('āŒ Invalid selection.');
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
- // If not absolute path, make it relative to current directory
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(`āŒ File not found: ${input}`);
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 (!stats.isFile()) {
271
- console.log(`āŒ Not a file: ${input}`);
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(`āš ļø File already selected: ${path.basename(filePath)}`);
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(`āœ… Added: ${path.basename(filePath)} (${size})`);
283
-
354
+ console.log(chalk.green(`Added: ${path.basename(filePath)} (${size})`));
284
355
  } catch (error) {
285
- console.log(`āŒ Error accessing file: ${error.message}`);
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
- module.exports = {
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.3",
3
+ "version": "1.4.1",
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": "commonjs",
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
- 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');
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
 
@@ -28,25 +25,6 @@ program
28
25
  .description('Canvas API Command Line Tool')
29
26
  .version('1.3.3');
30
27
 
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
28
  // List command to show enrolled courses
51
29
  program
52
30
  .command('list')
@@ -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('announce')
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
- .action(requireConfig(submitAssignment));
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();