canvaslms-cli 1.4.2 → 1.4.3
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/LICENSE +1 -1
- package/README.md +60 -134
- package/commands/submit.js +8 -7
- package/lib/interactive.js +402 -309
- package/package.json +3 -3
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,182 +1,106 @@
|
|
|
1
1
|
# Canvas CLI Tool
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A modern, user-friendly command-line interface for Canvas LMS. Manage courses, assignments, submissions, grades, and more directly from your terminal.
|
|
4
|
+
|
|
5
|
+
---
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- List and filter enrolled/starred courses
|
|
10
|
+
- View assignments, grades, and submission status
|
|
11
|
+
- Interactive file upload for assignments
|
|
12
|
+
- View course announcements
|
|
13
|
+
- Display user profile information
|
|
14
|
+
- Direct access to Canvas API endpoints
|
|
15
|
+
|
|
16
|
+
---
|
|
13
17
|
|
|
14
18
|
## Installation
|
|
15
19
|
|
|
16
|
-
### Global
|
|
20
|
+
### Global (Recommended)
|
|
17
21
|
|
|
18
22
|
```bash
|
|
19
23
|
npm install -g canvas-cli-tool
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
### Local
|
|
26
|
+
### Local (Project)
|
|
23
27
|
|
|
24
28
|
```bash
|
|
25
29
|
npm install canvas-cli-tool
|
|
26
30
|
```
|
|
27
31
|
|
|
32
|
+
---
|
|
33
|
+
|
|
28
34
|
## Setup
|
|
29
35
|
|
|
30
|
-
1. **Get your Canvas API
|
|
31
|
-
- Log
|
|
36
|
+
1. **Get your Canvas API Token**
|
|
37
|
+
- Log in to Canvas
|
|
32
38
|
- Go to Account → Settings
|
|
33
|
-
-
|
|
34
|
-
- Click "+ New Access Token"
|
|
39
|
+
- Under Approved Integrations, click + New Access Token
|
|
35
40
|
- Copy the generated token
|
|
36
41
|
|
|
37
|
-
2. **Configure the CLI
|
|
42
|
+
2. **Configure the CLI**
|
|
43
|
+
|
|
38
44
|
```bash
|
|
39
45
|
canvas config
|
|
40
46
|
```
|
|
41
47
|
|
|
42
|
-
3. **
|
|
48
|
+
3. **Environment Variables**
|
|
49
|
+
|
|
50
|
+
Create a `.env` file in your project root:
|
|
51
|
+
|
|
43
52
|
```env
|
|
44
53
|
CANVAS_DOMAIN=your-canvas-domain.instructure.com
|
|
45
54
|
CANVAS_API_TOKEN=your-api-token
|
|
46
55
|
```
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
### Basic Commands
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
# Show configuration help
|
|
54
|
-
canvas config
|
|
55
|
-
|
|
56
|
-
# List starred courses (default)
|
|
57
|
-
canvas list
|
|
58
|
-
|
|
59
|
-
# List all enrolled courses
|
|
60
|
-
canvas list -a
|
|
61
|
-
|
|
62
|
-
# List courses with detailed information
|
|
63
|
-
canvas list -v
|
|
64
|
-
|
|
65
|
-
# Show user profile
|
|
66
|
-
canvas profile
|
|
67
|
-
|
|
68
|
-
# Show detailed profile information
|
|
69
|
-
canvas profile -v
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Assignment Operations
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
# List assignments for a course
|
|
76
|
-
canvas assignments 12345
|
|
77
|
-
|
|
78
|
-
# Show detailed assignment information
|
|
79
|
-
canvas assignments 12345 -v
|
|
57
|
+
---
|
|
80
58
|
|
|
81
|
-
|
|
82
|
-
canvas assignments 12345 -s
|
|
83
|
-
|
|
84
|
-
# Show only pending assignments
|
|
85
|
-
canvas assignments 12345 -p
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### File Submission
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
# Interactive assignment submission
|
|
92
|
-
canvas submit
|
|
93
|
-
|
|
94
|
-
# Submit with specific course ID
|
|
95
|
-
canvas submit -c 12345
|
|
96
|
-
|
|
97
|
-
# Submit with specific assignment ID
|
|
98
|
-
canvas submit -a 67890
|
|
99
|
-
|
|
100
|
-
# Submit specific file
|
|
101
|
-
canvas submit -f myfile.pdf
|
|
102
|
-
```
|
|
59
|
+
## Usage
|
|
103
60
|
|
|
104
|
-
###
|
|
61
|
+
### Common Commands
|
|
105
62
|
|
|
106
63
|
```bash
|
|
107
|
-
#
|
|
108
|
-
canvas
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
canvas grades
|
|
112
|
-
|
|
113
|
-
# Show
|
|
114
|
-
canvas
|
|
115
|
-
|
|
116
|
-
# Show announcements for specific course
|
|
117
|
-
canvas announcements 12345
|
|
64
|
+
canvas config # Configure domain and API token
|
|
65
|
+
canvas list # List starred courses
|
|
66
|
+
canvas list -a # List all enrolled courses
|
|
67
|
+
canvas assignments <course> # List assignments for a course
|
|
68
|
+
canvas grades # Show grades for all courses
|
|
69
|
+
canvas announcements # Show recent announcements
|
|
70
|
+
canvas profile # Show user profile
|
|
71
|
+
canvas submit # Interactive assignment submission
|
|
118
72
|
```
|
|
119
73
|
|
|
120
|
-
###
|
|
74
|
+
### Assignment Submission
|
|
121
75
|
|
|
122
76
|
```bash
|
|
123
|
-
#
|
|
124
|
-
canvas
|
|
125
|
-
|
|
126
|
-
#
|
|
127
|
-
canvas get courses -q enrollment_state=active
|
|
128
|
-
|
|
129
|
-
# POST request with data
|
|
130
|
-
canvas post courses/123/assignments -d '{"assignment": {"name": "Test"}}'
|
|
131
|
-
|
|
132
|
-
# POST with data from file
|
|
133
|
-
canvas post courses/123/assignments -d @assignment.json
|
|
77
|
+
canvas submit # Interactive mode
|
|
78
|
+
canvas submit -c <courseId> # Specify course
|
|
79
|
+
canvas submit -a <assignmentId> # Specify assignment
|
|
80
|
+
canvas submit -f <file> # Submit specific file
|
|
134
81
|
```
|
|
135
82
|
|
|
136
83
|
## Command Reference
|
|
137
84
|
|
|
138
|
-
| Command
|
|
139
|
-
|
|
140
|
-
| `list`
|
|
141
|
-
| `assignments`
|
|
142
|
-
| `submit`
|
|
143
|
-
| `grades`
|
|
144
|
-
| `announcements` | `announce
|
|
145
|
-
| `profile`
|
|
146
|
-
| `config`
|
|
147
|
-
| `get` | `g` | GET API request |
|
|
148
|
-
| `post` | `p` | POST API request |
|
|
149
|
-
| `put` | - | PUT API request |
|
|
150
|
-
| `delete` | `d` | DELETE API request |
|
|
151
|
-
|
|
152
|
-
## File Structure
|
|
85
|
+
| Command | Alias | Description |
|
|
86
|
+
|-----------------|-----------|-----------------------------------|
|
|
87
|
+
| `list` | `l` | List courses |
|
|
88
|
+
| `assignments` | `assign` | List assignments |
|
|
89
|
+
| `submit` | `sub` | Submit assignment files |
|
|
90
|
+
| `grades` | `grade` | Show grades |
|
|
91
|
+
| `announcements` | `announce`| Show announcements |
|
|
92
|
+
| `profile` | `me` | Show user profile |
|
|
93
|
+
| `config` | - | Show configuration |
|
|
153
94
|
|
|
154
|
-
|
|
155
|
-
canvas-cli-tool/
|
|
156
|
-
├── src/
|
|
157
|
-
│ └── index.js # Main CLI entry point
|
|
158
|
-
├── lib/
|
|
159
|
-
│ ├── api-client.js # Canvas API client
|
|
160
|
-
│ ├── config.js # Configuration management
|
|
161
|
-
│ ├── file-upload.js # File upload utilities
|
|
162
|
-
│ └── interactive.js # Interactive prompt utilities
|
|
163
|
-
├── commands/
|
|
164
|
-
│ ├── list.js # List courses command
|
|
165
|
-
│ ├── assignments.js # Assignments command
|
|
166
|
-
│ ├── submit.js # Submit command
|
|
167
|
-
│ ├── grades.js # Grades command
|
|
168
|
-
│ ├── announcements.js # Announcements command
|
|
169
|
-
│ ├── profile.js # Profile command
|
|
170
|
-
│ ├── config.js # Config command
|
|
171
|
-
│ └── api.js # Raw API commands
|
|
172
|
-
└── package.json
|
|
173
|
-
```
|
|
95
|
+
---
|
|
174
96
|
|
|
175
97
|
## Requirements
|
|
176
98
|
|
|
177
|
-
- Node.js >= 14.
|
|
178
|
-
- npm >= 6.
|
|
179
|
-
- Valid Canvas LMS
|
|
99
|
+
- Node.js >= 14.x
|
|
100
|
+
- npm >= 6.x
|
|
101
|
+
- Valid Canvas LMS API token
|
|
102
|
+
|
|
103
|
+
---
|
|
180
104
|
|
|
181
105
|
## Contributing
|
|
182
106
|
|
|
@@ -184,10 +108,12 @@ canvas-cli-tool/
|
|
|
184
108
|
2. Create a feature branch: `git checkout -b feature-name`
|
|
185
109
|
3. Make your changes
|
|
186
110
|
4. Run tests: `npm test`
|
|
187
|
-
5. Commit
|
|
188
|
-
6. Push
|
|
111
|
+
5. Commit: `git commit -am 'Add feature'`
|
|
112
|
+
6. Push: `git push origin feature-name`
|
|
189
113
|
7. Submit a pull request
|
|
190
114
|
|
|
115
|
+
---
|
|
116
|
+
|
|
191
117
|
## License
|
|
192
118
|
|
|
193
|
-
MIT License - see [LICENSE](LICENSE)
|
|
119
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
package/commands/submit.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { makeCanvasRequest } from '../lib/api-client.js';
|
|
8
|
-
import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved,
|
|
8
|
+
import { createReadlineInterface, askQuestion, askConfirmation, selectFilesImproved, selectFilesKeyboard, pad } from '../lib/interactive.js';
|
|
9
9
|
import { uploadSingleFileToCanvas, submitAssignmentWithFiles } from '../lib/file-upload.js';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
|
|
@@ -137,16 +137,17 @@ export async function submitAssignment(options) {
|
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
assignmentId = selectedAssignment.id;
|
|
140
|
-
console.log(chalk.green(`Success: Selected ${selectedAssignment.name}\n`));
|
|
141
|
-
|
|
142
|
-
const resubmit = await askConfirmation(rl, chalk.yellow('This assignment has already been submitted. Do you want to resubmit?'), false);
|
|
140
|
+
console.log(chalk.green(`Success: Selected ${selectedAssignment.name}\n`)); if (selectedAssignment.submission && selectedAssignment.submission.submitted_at) {
|
|
141
|
+
const resubmit = await askConfirmation(rl, chalk.yellow('This assignment has already been submitted. Do you want to resubmit?'), true);
|
|
143
142
|
if (!resubmit) {
|
|
144
143
|
console.log(chalk.yellow('Submission cancelled.'));
|
|
145
144
|
rl.close();
|
|
146
145
|
return;
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
|
-
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Step 3: Choose file selection method and select files
|
|
150
151
|
let filePaths = [];
|
|
151
152
|
console.log(chalk.cyan.bold('-'.repeat(60)));
|
|
152
153
|
console.log(chalk.cyan.bold('File Selection Method'));
|
|
@@ -155,7 +156,7 @@ export async function submitAssignment(options) {
|
|
|
155
156
|
console.log(chalk.white('Assignment: ') + chalk.bold(selectedAssignment.name) + '\n');
|
|
156
157
|
|
|
157
158
|
console.log(chalk.yellow('📁 Choose file selection method:'));
|
|
158
|
-
console.log(chalk.white('1. ') + chalk.cyan('
|
|
159
|
+
console.log(chalk.white('1. ') + chalk.cyan('Keyboard Navigator') + chalk.gray(' (NEW! - Use arrow keys and space bar to navigate and select)'));
|
|
159
160
|
console.log(chalk.white('2. ') + chalk.cyan('Text-based Selector') + chalk.gray(' (Traditional - Type filenames and wildcards)'));
|
|
160
161
|
console.log(chalk.white('3. ') + chalk.cyan('Basic Directory Listing') + chalk.gray(' (Simple - Select from numbered list)'));
|
|
161
162
|
|
|
@@ -166,7 +167,7 @@ export async function submitAssignment(options) {
|
|
|
166
167
|
console.log(chalk.cyan('-'.repeat(60)));
|
|
167
168
|
|
|
168
169
|
if (selectorChoice === '1') {
|
|
169
|
-
filePaths = await
|
|
170
|
+
filePaths = await selectFilesKeyboard(rl);
|
|
170
171
|
} else if (selectorChoice === '2') {
|
|
171
172
|
filePaths = await selectFilesImproved(rl);
|
|
172
173
|
} else {
|
package/lib/interactive.js
CHANGED
|
@@ -8,6 +8,19 @@ import path from 'path';
|
|
|
8
8
|
import AdmZip from 'adm-zip';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
|
|
11
|
+
// Key codes for raw terminal input
|
|
12
|
+
const KEYS = {
|
|
13
|
+
UP: '\u001b[A',
|
|
14
|
+
DOWN: '\u001b[B',
|
|
15
|
+
LEFT: '\u001b[D',
|
|
16
|
+
RIGHT: '\u001b[C',
|
|
17
|
+
SPACE: ' ',
|
|
18
|
+
ENTER: '\r',
|
|
19
|
+
ESCAPE: '\u001b',
|
|
20
|
+
BACKSPACE: '\u007f',
|
|
21
|
+
TAB: '\t'
|
|
22
|
+
};
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Create readline interface for user input
|
|
13
26
|
*/
|
|
@@ -51,15 +64,26 @@ function askQuestionWithValidation(rl, question, validator, errorMessage) {
|
|
|
51
64
|
* Ask for confirmation (Y/n format)
|
|
52
65
|
*/
|
|
53
66
|
async function askConfirmation(rl, question, defaultYes = true) {
|
|
54
|
-
const suffix = defaultYes ?
|
|
55
|
-
const answer = await askQuestion(rl, question + suffix +
|
|
67
|
+
const suffix = defaultYes ? " (Y/n)" : " (y/N)";
|
|
68
|
+
const answer = await askQuestion(rl, question + suffix + ": ");
|
|
69
|
+
|
|
70
|
+
const lower = answer.toLowerCase();
|
|
56
71
|
|
|
57
|
-
|
|
72
|
+
// If user presses Enter → return defaultYes (true by default)
|
|
73
|
+
if (lower === "") {
|
|
58
74
|
return defaultYes;
|
|
59
75
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
|
|
77
|
+
// Convert input to boolean
|
|
78
|
+
if (lower === "y" || lower === "yes") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (lower === "n" || lower === "no") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If input is something else, fallback to defaultYes
|
|
86
|
+
return defaultYes;
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
/**
|
|
@@ -360,58 +384,72 @@ async function selectFilesImproved(rl, currentDir = process.cwd()) {
|
|
|
360
384
|
}
|
|
361
385
|
|
|
362
386
|
/**
|
|
363
|
-
*
|
|
387
|
+
* Interactive file selector with tree view and keyboard navigation
|
|
364
388
|
*/
|
|
365
|
-
async function
|
|
389
|
+
async function selectFilesKeyboard(rl, currentDir = process.cwd()) {
|
|
366
390
|
const selectedFiles = [];
|
|
367
|
-
let
|
|
391
|
+
let expandedFolders = new Set();
|
|
392
|
+
let fileTree = [];
|
|
368
393
|
let fileList = [];
|
|
369
|
-
let
|
|
370
|
-
|
|
394
|
+
let currentPath = currentDir;
|
|
395
|
+
let currentIndex = 0;
|
|
396
|
+
let isNavigating = true;
|
|
397
|
+
let viewStartIndex = 0;
|
|
398
|
+
const maxVisibleItems = 15;
|
|
371
399
|
|
|
372
|
-
//
|
|
373
|
-
|
|
400
|
+
// Setup raw mode for keyboard input
|
|
401
|
+
process.stdin.setRawMode(true);
|
|
402
|
+
process.stdin.resume();
|
|
403
|
+
process.stdin.setEncoding('utf8');
|
|
404
|
+
|
|
405
|
+
// Helper function to build file tree
|
|
406
|
+
function buildFileTree(basePath = currentDir, level = 0, parentPath = '') {
|
|
407
|
+
const tree = [];
|
|
374
408
|
try {
|
|
375
|
-
const entries = fs.readdirSync(
|
|
376
|
-
fileList = [];
|
|
377
|
-
|
|
378
|
-
// Add parent directory option if not at root
|
|
379
|
-
if (currentPath !== currentDir) {
|
|
380
|
-
fileList.push({
|
|
381
|
-
name: '📁 .. (Parent Directory)',
|
|
382
|
-
path: path.dirname(currentPath),
|
|
383
|
-
type: 'parent',
|
|
384
|
-
size: 0
|
|
385
|
-
});
|
|
386
|
-
}
|
|
409
|
+
const entries = fs.readdirSync(basePath).sort();
|
|
387
410
|
|
|
388
411
|
// Add directories first
|
|
389
412
|
entries.forEach(entry => {
|
|
390
|
-
const fullPath = path.join(
|
|
413
|
+
const fullPath = path.join(basePath, entry);
|
|
414
|
+
const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
|
|
415
|
+
|
|
391
416
|
try {
|
|
392
417
|
const stats = fs.statSync(fullPath);
|
|
393
|
-
if (stats.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.vscode'].includes(entry)) {
|
|
394
|
-
|
|
395
|
-
|
|
418
|
+
if (stats.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.vscode', '.next'].includes(entry)) {
|
|
419
|
+
const isExpanded = expandedFolders.has(fullPath);
|
|
420
|
+
tree.push({
|
|
421
|
+
name: entry,
|
|
396
422
|
path: fullPath,
|
|
423
|
+
relativePath,
|
|
397
424
|
type: 'directory',
|
|
425
|
+
level,
|
|
426
|
+
isExpanded,
|
|
398
427
|
size: 0
|
|
399
428
|
});
|
|
429
|
+
|
|
430
|
+
// If expanded, add children
|
|
431
|
+
if (isExpanded) {
|
|
432
|
+
const children = buildFileTree(fullPath, level + 1, relativePath);
|
|
433
|
+
tree.push(...children);
|
|
434
|
+
}
|
|
400
435
|
}
|
|
401
436
|
} catch (e) {}
|
|
402
437
|
});
|
|
403
438
|
|
|
404
439
|
// Add files
|
|
405
440
|
entries.forEach(entry => {
|
|
406
|
-
const fullPath = path.join(
|
|
441
|
+
const fullPath = path.join(basePath, entry);
|
|
442
|
+
const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
|
|
443
|
+
|
|
407
444
|
try {
|
|
408
445
|
const stats = fs.statSync(fullPath);
|
|
409
446
|
if (stats.isFile()) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
name: `${icon} ${entry}`,
|
|
447
|
+
tree.push({
|
|
448
|
+
name: entry,
|
|
413
449
|
path: fullPath,
|
|
450
|
+
relativePath,
|
|
414
451
|
type: 'file',
|
|
452
|
+
level,
|
|
415
453
|
size: stats.size
|
|
416
454
|
});
|
|
417
455
|
}
|
|
@@ -420,6 +458,8 @@ async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
|
420
458
|
} catch (error) {
|
|
421
459
|
console.log(chalk.red('Error reading directory: ' + error.message));
|
|
422
460
|
}
|
|
461
|
+
|
|
462
|
+
return tree;
|
|
423
463
|
}
|
|
424
464
|
|
|
425
465
|
// Helper function to get file icon based on extension
|
|
@@ -437,19 +477,45 @@ async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
|
437
477
|
};
|
|
438
478
|
return icons[ext] || '📋';
|
|
439
479
|
}
|
|
480
|
+
// Helper function to build breadcrumb path
|
|
481
|
+
function buildBreadcrumb() {
|
|
482
|
+
const relativePath = path.relative(currentDir, currentPath);
|
|
483
|
+
if (!relativePath || relativePath === '.') {
|
|
484
|
+
return ''
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const parts = relativePath.split(path.sep);
|
|
488
|
+
const breadcrumb = parts.map((part, index) => {
|
|
489
|
+
if (index === parts.length - 1) {
|
|
490
|
+
return chalk.white.bold(part);
|
|
491
|
+
}
|
|
492
|
+
return chalk.gray(part);
|
|
493
|
+
}).join(chalk.gray(' › '));
|
|
494
|
+
|
|
495
|
+
return chalk.yellow('📂 ') + breadcrumb;
|
|
496
|
+
}
|
|
440
497
|
|
|
441
|
-
// Helper function to display
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
console.log(chalk.cyan.bold('╭' + '─'.repeat(
|
|
445
|
-
console.log(chalk.cyan.bold('│') + chalk.white.bold('
|
|
446
|
-
|
|
498
|
+
// Helper function to display the file browser
|
|
499
|
+
function displayBrowser() {
|
|
500
|
+
// Header with keyboard controls at the top
|
|
501
|
+
console.log(chalk.cyan.bold('╭' + '─'.repeat(100) + '╮'));
|
|
502
|
+
console.log(chalk.cyan.bold('│') + chalk.white.bold(' Keyboard File Selector'.padEnd(98)) + chalk.cyan.bold('│')); console.log(chalk.cyan.bold('├' + '─'.repeat(100) + '┤'));
|
|
503
|
+
// Keyboard controls - compact format at top
|
|
504
|
+
const controls = [
|
|
505
|
+
'↑↓←→:Navigate', 'Space:Select', 'Enter:Open/Finish', 'Backspace:Up', 'a:All', 'c:Clear', 'Esc:Exit'
|
|
506
|
+
];
|
|
507
|
+
const controlLine = controls.map(c => {
|
|
508
|
+
const [key, desc] = c.split(':');
|
|
509
|
+
return chalk.white(key) + chalk.gray(':') + chalk.gray(desc);
|
|
510
|
+
}).join(chalk.gray(' │ '));
|
|
511
|
+
console.log(chalk.cyan.bold('│ ') + controlLine.padEnd(98) + chalk.cyan.bold(' │'));
|
|
512
|
+
console.log(chalk.cyan.bold('╰' + '─'.repeat(100) + '╯'));
|
|
447
513
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
console.log(
|
|
514
|
+
console.log();
|
|
515
|
+
// Breadcrumb path
|
|
516
|
+
console.log(buildBreadcrumb());
|
|
451
517
|
|
|
452
|
-
//
|
|
518
|
+
// Selected files count
|
|
453
519
|
if (selectedFiles.length > 0) {
|
|
454
520
|
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
455
521
|
try {
|
|
@@ -458,7 +524,9 @@ async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
|
458
524
|
return sum;
|
|
459
525
|
}
|
|
460
526
|
}, 0);
|
|
461
|
-
console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB)`));
|
|
527
|
+
console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB) - Press Enter to finish`));
|
|
528
|
+
} else {
|
|
529
|
+
console.log(chalk.gray('💡 Use Space to select files, then press Enter to finish'));
|
|
462
530
|
}
|
|
463
531
|
|
|
464
532
|
console.log();
|
|
@@ -468,311 +536,336 @@ async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
|
468
536
|
return;
|
|
469
537
|
}
|
|
470
538
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
539
|
+
// Display files with tree-like structure
|
|
540
|
+
displayFileTree();
|
|
541
|
+
}
|
|
542
|
+
// Helper function to display files in a horizontal grid layout
|
|
543
|
+
function displayFileTree() {
|
|
544
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
545
|
+
const maxDisplayItems = 50; // Show more items in grid view
|
|
546
|
+
const startIdx = Math.max(0, currentIndex - Math.floor(maxDisplayItems / 2));
|
|
547
|
+
const endIdx = Math.min(fileList.length, startIdx + maxDisplayItems);
|
|
548
|
+
const visibleItems = fileList.slice(startIdx, endIdx);
|
|
478
549
|
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const prefix = isSelected ? chalk.green('✓') : chalk.white(globalIndex.toString().padStart(2));
|
|
484
|
-
const sizeDisplay = item.type === 'file' ? chalk.gray(`(${(item.size / 1024).toFixed(1)} KB)`) : '';
|
|
485
|
-
|
|
486
|
-
if (isSelected) {
|
|
487
|
-
console.log(`${prefix} ${chalk.green(item.name)} ${sizeDisplay}`);
|
|
488
|
-
} else if (item.type === 'parent') {
|
|
489
|
-
console.log(`${prefix} ${chalk.blue(item.name)}`);
|
|
490
|
-
} else if (item.type === 'directory') {
|
|
491
|
-
console.log(`${prefix} ${chalk.cyan(item.name)}`);
|
|
492
|
-
} else {
|
|
493
|
-
console.log(`${prefix} ${chalk.white(item.name)} ${sizeDisplay}`);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
550
|
+
// Show scroll indicators
|
|
551
|
+
if (startIdx > 0) {
|
|
552
|
+
console.log(chalk.gray(` ⋮ (${startIdx} items above)`));
|
|
553
|
+
}
|
|
496
554
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
console.log(chalk.yellow.bold('Commands:'));
|
|
503
|
-
console.log(chalk.white(' 1-9, 10+ ') + chalk.gray('Select/deselect file by number'));
|
|
504
|
-
console.log(chalk.white(' n/next/→ ') + chalk.gray('Next page'));
|
|
505
|
-
console.log(chalk.white(' p/prev/← ') + chalk.gray('Previous page'));
|
|
506
|
-
console.log(chalk.white(' a/all ') + chalk.gray('Select all files on current page'));
|
|
507
|
-
console.log(chalk.white(' c/clear ') + chalk.gray('Clear all selections'));
|
|
508
|
-
console.log(chalk.white(' r/remove ') + chalk.gray('Remove specific file from selection'));
|
|
509
|
-
console.log(chalk.white(' s/search ') + chalk.gray('Search files by name or extension'));
|
|
510
|
-
console.log(chalk.white(' f/filter ') + chalk.gray('Filter by file extension'));
|
|
511
|
-
console.log(chalk.white(' h/help ') + chalk.gray('Show detailed help'));
|
|
512
|
-
console.log(chalk.white(' q/quit ') + chalk.gray('Finish selection'));
|
|
513
|
-
console.log();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Main interaction loop
|
|
517
|
-
while (true) {
|
|
518
|
-
refreshFileList();
|
|
519
|
-
displayPage();
|
|
555
|
+
// Calculate item width and columns
|
|
556
|
+
const maxItemWidth = Math.max(...visibleItems.map(item => {
|
|
557
|
+
const name = path.basename(item.path);
|
|
558
|
+
return name.length + 4; // 2 for icon + space, 2 for padding
|
|
559
|
+
}));
|
|
520
560
|
|
|
521
|
-
const
|
|
522
|
-
const
|
|
561
|
+
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25); // Min 15, max 25 chars
|
|
562
|
+
const columnsPerRow = Math.floor((terminalWidth - 4) / itemWidth); // Leave 4 chars margin
|
|
563
|
+
const actualColumns = Math.max(1, columnsPerRow);
|
|
523
564
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
// Handle navigation
|
|
528
|
-
if (cmd === 'n' || cmd === 'next' || cmd === '→') {
|
|
529
|
-
if ((currentPage + 1) * itemsPerPage < fileList.length) {
|
|
530
|
-
currentPage++;
|
|
531
|
-
} else {
|
|
532
|
-
console.log(chalk.yellow('Already on last page!'));
|
|
533
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
534
|
-
}
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
565
|
+
// Group items into rows
|
|
566
|
+
let currentRow = '';
|
|
567
|
+
let itemsInCurrentRow = 0;
|
|
537
568
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
569
|
+
visibleItems.forEach((item, index) => {
|
|
570
|
+
const actualIndex = startIdx + index;
|
|
571
|
+
const isSelected = selectedFiles.includes(item.path);
|
|
572
|
+
const isCurrent = actualIndex === currentIndex;
|
|
573
|
+
|
|
574
|
+
// Get icon and name
|
|
575
|
+
let icon = '';
|
|
576
|
+
if (item.type === 'parent' || item.type === 'directory') {
|
|
577
|
+
icon = '📁';
|
|
541
578
|
} else {
|
|
542
|
-
|
|
543
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
579
|
+
icon = getFileIcon(path.basename(item.path));
|
|
544
580
|
}
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Handle help
|
|
549
|
-
if (cmd === 'h' || cmd === 'help') {
|
|
550
|
-
console.clear();
|
|
551
|
-
console.log(chalk.cyan.bold('╭' + '─'.repeat(78) + '╮'));
|
|
552
|
-
console.log(chalk.cyan.bold('│') + chalk.white.bold(' Interactive File Selector - Help'.padEnd(76)) + chalk.cyan.bold('│'));
|
|
553
|
-
console.log(chalk.cyan.bold('╰' + '─'.repeat(78) + '╯'));
|
|
554
|
-
console.log();
|
|
555
|
-
console.log(chalk.yellow.bold('📋 File Selection:'));
|
|
556
|
-
console.log(chalk.white(' • Type a number (1-9999) to toggle file selection'));
|
|
557
|
-
console.log(chalk.white(' • Files show with checkmarks (✓) when selected'));
|
|
558
|
-
console.log(chalk.white(' • Directories and parent folders can be navigated into'));
|
|
559
|
-
console.log();
|
|
560
|
-
console.log(chalk.yellow.bold('📁 Navigation:'));
|
|
561
|
-
console.log(chalk.white(' • Use n/next/→ to go to next page'));
|
|
562
|
-
console.log(chalk.white(' • Use p/prev/← to go to previous page'));
|
|
563
|
-
console.log(chalk.white(' • Click on folder numbers to enter directories'));
|
|
564
|
-
console.log(chalk.white(' • Use .. (Parent Directory) to go up one level'));
|
|
565
|
-
console.log();
|
|
566
|
-
console.log(chalk.yellow.bold('🔍 Advanced Features:'));
|
|
567
|
-
console.log(chalk.white(' • s/search: Find files by name or extension'));
|
|
568
|
-
console.log(chalk.white(' • f/filter: Filter and select by file extension'));
|
|
569
|
-
console.log(chalk.white(' • a/all: Select all files on current page'));
|
|
570
|
-
console.log(chalk.white(' • c/clear: Remove all selections'));
|
|
571
|
-
console.log(chalk.white(' • r/remove: Remove specific files from selection'));
|
|
572
|
-
console.log();
|
|
573
|
-
console.log(chalk.yellow.bold('💡 Tips:'));
|
|
574
|
-
console.log(chalk.white(' • File sizes are shown in KB'));
|
|
575
|
-
console.log(chalk.white(' • Your current selection count and size is displayed at top'));
|
|
576
|
-
console.log(chalk.white(' • Use search for large directories to find files quickly'));
|
|
577
|
-
console.log(chalk.white(' • Filter by extension (e.g., .pdf, .docx) for specific file types'));
|
|
578
|
-
console.log();
|
|
579
|
-
console.log(chalk.green('Press Enter to continue...'));
|
|
580
|
-
await askQuestion(rl, '');
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Handle selection commands
|
|
585
|
-
if (cmd === 'a' || cmd === 'all') {
|
|
586
|
-
const startIdx = currentPage * itemsPerPage;
|
|
587
|
-
const endIdx = Math.min(startIdx + itemsPerPage, fileList.length);
|
|
588
|
-
let addedCount = 0;
|
|
589
581
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
582
|
+
const name = item.name || path.basename(item.path);
|
|
583
|
+
const truncatedName = name.length > itemWidth - 4 ? name.slice(0, itemWidth - 7) + '...' : name;
|
|
584
|
+
|
|
585
|
+
// Build item display string
|
|
586
|
+
let itemDisplay = `${icon} ${truncatedName}`;
|
|
587
|
+
|
|
588
|
+
// Apply styling based on state
|
|
589
|
+
if (isCurrent) {
|
|
590
|
+
if (isSelected) {
|
|
591
|
+
itemDisplay = chalk.black.bgGreen(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
592
|
+
} else if (item.type === 'parent') {
|
|
593
|
+
itemDisplay = chalk.white.bgBlue(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
594
|
+
} else if (item.type === 'directory') {
|
|
595
|
+
itemDisplay = chalk.black.bgCyan(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
596
|
+
} else {
|
|
597
|
+
itemDisplay = chalk.black.bgWhite(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
if (isSelected) {
|
|
601
|
+
itemDisplay = chalk.green(`✓${itemDisplay}`.padEnd(itemWidth));
|
|
602
|
+
} else if (item.type === 'parent') {
|
|
603
|
+
itemDisplay = chalk.blue(` ${itemDisplay}`.padEnd(itemWidth));
|
|
604
|
+
} else if (item.type === 'directory') {
|
|
605
|
+
itemDisplay = chalk.cyan(` ${itemDisplay}`.padEnd(itemWidth));
|
|
606
|
+
} else {
|
|
607
|
+
itemDisplay = chalk.white(` ${itemDisplay}`.padEnd(itemWidth));
|
|
595
608
|
}
|
|
596
609
|
}
|
|
597
610
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
611
|
+
// Add to current row
|
|
612
|
+
currentRow += itemDisplay;
|
|
613
|
+
itemsInCurrentRow++;
|
|
614
|
+
|
|
615
|
+
// Check if we need to start a new row
|
|
616
|
+
if (itemsInCurrentRow >= actualColumns || index === visibleItems.length - 1) {
|
|
617
|
+
console.log(currentRow);
|
|
618
|
+
currentRow = '';
|
|
619
|
+
itemsInCurrentRow = 0;
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Show scroll indicators
|
|
624
|
+
if (endIdx < fileList.length) {
|
|
625
|
+
console.log(chalk.gray(` ⋮ (${fileList.length - endIdx} items below)`));
|
|
601
626
|
}
|
|
602
627
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
628
|
+
console.log();
|
|
629
|
+
|
|
630
|
+
// Show current position and navigation info
|
|
631
|
+
if (fileList.length > maxDisplayItems) {
|
|
632
|
+
console.log(chalk.gray(`Showing ${startIdx + 1}-${endIdx} of ${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
633
|
+
} else {
|
|
634
|
+
console.log(chalk.gray(`${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
609
635
|
}
|
|
610
636
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
637
|
+
// Show grid info
|
|
638
|
+
console.log(chalk.gray(`Grid: ${actualColumns} columns × ${itemWidth} chars | Terminal width: ${terminalWidth}`));
|
|
639
|
+
}
|
|
640
|
+
// Helper function to refresh file list for current directory
|
|
641
|
+
function refreshFileList() {
|
|
642
|
+
fileList = [];
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
// Add parent directory option if not at root
|
|
646
|
+
if (currentPath !== currentDir) {
|
|
647
|
+
fileList.push({
|
|
648
|
+
type: 'parent',
|
|
649
|
+
path: path.dirname(currentPath),
|
|
650
|
+
name: '..'
|
|
651
|
+
});
|
|
616
652
|
}
|
|
617
653
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
654
|
+
// Read current directory
|
|
655
|
+
const entries = fs.readdirSync(currentPath).sort();
|
|
656
|
+
|
|
657
|
+
// Add directories first
|
|
658
|
+
entries.forEach(entry => {
|
|
659
|
+
const fullPath = path.join(currentPath, entry);
|
|
660
|
+
const stat = fs.statSync(fullPath);
|
|
661
|
+
|
|
662
|
+
if (stat.isDirectory()) {
|
|
663
|
+
fileList.push({
|
|
664
|
+
type: 'directory',
|
|
665
|
+
path: fullPath,
|
|
666
|
+
name: entry
|
|
667
|
+
});
|
|
668
|
+
}
|
|
621
669
|
});
|
|
622
670
|
|
|
623
|
-
|
|
624
|
-
|
|
671
|
+
// Add files
|
|
672
|
+
entries.forEach(entry => {
|
|
673
|
+
const fullPath = path.join(currentPath, entry);
|
|
674
|
+
const stat = fs.statSync(fullPath);
|
|
675
|
+
|
|
676
|
+
if (stat.isFile()) {
|
|
677
|
+
fileList.push({
|
|
678
|
+
type: 'file',
|
|
679
|
+
path: fullPath,
|
|
680
|
+
name: entry,
|
|
681
|
+
size: stat.size
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
});
|
|
625
685
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
686
|
+
// Ensure currentIndex is within bounds
|
|
687
|
+
if (currentIndex >= fileList.length) {
|
|
688
|
+
currentIndex = Math.max(0, fileList.length - 1);
|
|
629
689
|
}
|
|
630
690
|
|
|
631
|
-
|
|
632
|
-
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error('Error reading directory:', error.message);
|
|
693
|
+
fileList = [];
|
|
633
694
|
}
|
|
695
|
+
}
|
|
696
|
+
// Main keyboard event handler
|
|
697
|
+
function handleKeyInput(key) {
|
|
698
|
+
// Calculate grid dimensions for navigation
|
|
699
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
700
|
+
const maxItemWidth = Math.max(...fileList.map(item => {
|
|
701
|
+
const name = path.basename(item.path);
|
|
702
|
+
return name.length + 4;
|
|
703
|
+
}));
|
|
704
|
+
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25);
|
|
705
|
+
const columnsPerRow = Math.max(1, Math.floor((terminalWidth - 4) / itemWidth));
|
|
634
706
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
707
|
+
switch (key) {
|
|
708
|
+
case KEYS.UP:
|
|
709
|
+
// Move up by one row (subtract columns)
|
|
710
|
+
const newUpIndex = currentIndex - columnsPerRow;
|
|
711
|
+
if (newUpIndex >= 0) {
|
|
712
|
+
currentIndex = newUpIndex;
|
|
713
|
+
displayBrowser();
|
|
714
|
+
}
|
|
715
|
+
break;
|
|
643
716
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
717
|
+
case KEYS.DOWN:
|
|
718
|
+
// Move down by one row (add columns)
|
|
719
|
+
const newDownIndex = currentIndex + columnsPerRow;
|
|
720
|
+
if (newDownIndex < fileList.length) {
|
|
721
|
+
currentIndex = newDownIndex;
|
|
722
|
+
displayBrowser();
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
|
|
726
|
+
case KEYS.LEFT:
|
|
727
|
+
// Move left by one column
|
|
728
|
+
if (currentIndex > 0) {
|
|
729
|
+
currentIndex--;
|
|
730
|
+
displayBrowser();
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
|
|
734
|
+
case KEYS.RIGHT:
|
|
735
|
+
// Move right by one column
|
|
736
|
+
if (currentIndex < fileList.length - 1) {
|
|
737
|
+
currentIndex++;
|
|
738
|
+
displayBrowser();
|
|
739
|
+
}
|
|
740
|
+
break;
|
|
741
|
+
|
|
742
|
+
case KEYS.SPACE:
|
|
743
|
+
if (fileList.length > 0) {
|
|
744
|
+
const item = fileList[currentIndex];
|
|
745
|
+
if (item.type === 'file') {
|
|
746
|
+
const index = selectedFiles.indexOf(item.path);
|
|
747
|
+
if (index === -1) {
|
|
748
|
+
selectedFiles.push(item.path);
|
|
749
|
+
} else {
|
|
750
|
+
selectedFiles.splice(index, 1);
|
|
751
|
+
}
|
|
752
|
+
displayBrowser();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
break;
|
|
756
|
+
case KEYS.ENTER:
|
|
757
|
+
if (fileList.length > 0) {
|
|
758
|
+
const item = fileList[currentIndex];
|
|
759
|
+
if (item.type === 'parent' || item.type === 'directory') {
|
|
760
|
+
currentPath = item.path;
|
|
761
|
+
currentIndex = 0;
|
|
762
|
+
refreshFileList();
|
|
763
|
+
displayBrowser();
|
|
764
|
+
} else {
|
|
765
|
+
// When Enter is pressed on a file, finish selection if files are selected
|
|
766
|
+
if (selectedFiles.length > 0) {
|
|
767
|
+
isNavigating = false;
|
|
768
|
+
}
|
|
660
769
|
}
|
|
661
770
|
} else {
|
|
662
|
-
|
|
771
|
+
// Finish selection when directory is empty and Enter is pressed (if files selected)
|
|
772
|
+
if (selectedFiles.length > 0) {
|
|
773
|
+
isNavigating = false;
|
|
774
|
+
}
|
|
663
775
|
}
|
|
776
|
+
break;
|
|
664
777
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
778
|
+
case KEYS.BACKSPACE:
|
|
779
|
+
if (currentPath !== currentDir) {
|
|
780
|
+
currentPath = path.dirname(currentPath);
|
|
781
|
+
currentIndex = 0;
|
|
782
|
+
refreshFileList();
|
|
783
|
+
displayBrowser();
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
|
|
787
|
+
case 'a':
|
|
788
|
+
// Select all files in current directory
|
|
789
|
+
let addedCount = 0;
|
|
790
|
+
fileList.forEach(item => {
|
|
791
|
+
if (item.type === 'file' && !selectedFiles.includes(item.path)) {
|
|
792
|
+
selectedFiles.push(item.path);
|
|
793
|
+
addedCount++;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
if (addedCount > 0) {
|
|
797
|
+
displayBrowser();
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
|
|
801
|
+
case 'c':
|
|
802
|
+
// Clear all selections
|
|
803
|
+
selectedFiles.length = 0;
|
|
804
|
+
displayBrowser();
|
|
805
|
+
break;
|
|
806
|
+
|
|
807
|
+
case KEYS.ESCAPE:
|
|
808
|
+
case '\u001b': // ESC key
|
|
809
|
+
isNavigating = false;
|
|
810
|
+
break;
|
|
811
|
+
|
|
812
|
+
default:
|
|
813
|
+
// Ignore other keys
|
|
814
|
+
break;
|
|
668
815
|
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Initialize and start navigation
|
|
819
|
+
return new Promise((resolve) => {
|
|
820
|
+
refreshFileList();
|
|
821
|
+
displayBrowser();
|
|
669
822
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
.filter(item => item.type === 'file')
|
|
674
|
-
.map(item => path.extname(item.path).toLowerCase())
|
|
675
|
-
.filter(ext => ext)
|
|
676
|
-
)].sort();
|
|
677
|
-
|
|
678
|
-
if (allExtensions.length === 0) {
|
|
679
|
-
console.log(chalk.yellow('No file extensions found!'));
|
|
680
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
681
|
-
continue;
|
|
823
|
+
process.stdin.on('data', (key) => {
|
|
824
|
+
if (!isNavigating) {
|
|
825
|
+
return;
|
|
682
826
|
}
|
|
683
827
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const count = fileList.filter(item =>
|
|
687
|
-
item.type === 'file' && path.extname(item.path).toLowerCase() === ext
|
|
688
|
-
).length;
|
|
689
|
-
console.log(`${index + 1}. ${ext} (${count} files)`);
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
const extChoice = await askQuestion(rl, chalk.cyan('Enter extension number (or press Enter to cancel): '));
|
|
693
|
-
const extIndex = parseInt(extChoice) - 1;
|
|
828
|
+
const keyStr = key.toString();
|
|
829
|
+
handleKeyInput(keyStr);
|
|
694
830
|
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
);
|
|
831
|
+
if (!isNavigating) {
|
|
832
|
+
// Cleanup
|
|
833
|
+
process.stdin.removeAllListeners('data');
|
|
834
|
+
process.stdin.setRawMode(false);
|
|
835
|
+
process.stdin.pause();
|
|
700
836
|
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
837
|
+
// Display completion summary
|
|
838
|
+
if (selectedFiles.length > 0) {
|
|
839
|
+
console.log(chalk.green.bold('✅ File Selection Complete!'));
|
|
840
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
841
|
+
|
|
842
|
+
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
843
|
+
try {
|
|
844
|
+
return sum + fs.statSync(file).size;
|
|
845
|
+
} catch {
|
|
846
|
+
return sum;
|
|
847
|
+
}
|
|
848
|
+
}, 0);
|
|
849
|
+
|
|
850
|
+
console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
|
|
851
|
+
selectedFiles.forEach((file, index) => {
|
|
852
|
+
try {
|
|
853
|
+
const stats = fs.statSync(file);
|
|
854
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
855
|
+
console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
856
|
+
} catch (e) {
|
|
857
|
+
console.log(pad(chalk.red(`${index + 1}.`), 5) + chalk.red(path.basename(file) + ' (Error reading file)'));
|
|
706
858
|
}
|
|
707
859
|
});
|
|
708
|
-
console.log(chalk.
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Handle numeric selection
|
|
717
|
-
const num = parseInt(cmd);
|
|
718
|
-
if (!isNaN(num) && num > 0 && num <= fileList.length) {
|
|
719
|
-
const item = fileList[num - 1];
|
|
720
|
-
|
|
721
|
-
if (item.type === 'parent') {
|
|
722
|
-
currentPath = item.path;
|
|
723
|
-
currentPage = 0;
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (item.type === 'directory') {
|
|
728
|
-
currentPath = item.path;
|
|
729
|
-
currentPage = 0;
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
if (item.type === 'file') {
|
|
733
|
-
const index = selectedFiles.indexOf(item.path);
|
|
734
|
-
if (index === -1) {
|
|
735
|
-
selectedFiles.push(item.path);
|
|
736
|
-
const size = (item.size / 1024).toFixed(1);
|
|
737
|
-
console.log(chalk.green(`✓ Added: ${path.basename(item.path)} (${size} KB)`));
|
|
860
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
738
861
|
} else {
|
|
739
|
-
|
|
740
|
-
console.log(chalk.yellow(`✗ Removed: ${path.basename(item.path)}`));
|
|
862
|
+
console.log(chalk.yellow('No files selected.'));
|
|
741
863
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
console.log(chalk.red('Invalid command!'));
|
|
747
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Display completion summary
|
|
751
|
-
if (selectedFiles.length > 0) {
|
|
752
|
-
console.clear();
|
|
753
|
-
console.log(chalk.green.bold('✅ File Selection Complete!'));
|
|
754
|
-
console.log(chalk.cyan('-'.repeat(50)));
|
|
755
|
-
|
|
756
|
-
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
757
|
-
try {
|
|
758
|
-
return sum + fs.statSync(file).size;
|
|
759
|
-
} catch {
|
|
760
|
-
return sum;
|
|
864
|
+
|
|
865
|
+
resolve(selectedFiles);
|
|
761
866
|
}
|
|
762
|
-
}, 0);
|
|
763
|
-
|
|
764
|
-
console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
|
|
765
|
-
selectedFiles.forEach((file, index) => {
|
|
766
|
-
const stats = fs.statSync(file);
|
|
767
|
-
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
768
|
-
console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
769
867
|
});
|
|
770
|
-
|
|
771
|
-
} else {
|
|
772
|
-
console.log(chalk.yellow('No files selected.'));
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
return selectedFiles;
|
|
868
|
+
});
|
|
776
869
|
}
|
|
777
870
|
|
|
778
871
|
export {
|
|
@@ -782,7 +875,7 @@ export {
|
|
|
782
875
|
askConfirmation,
|
|
783
876
|
selectFromList,
|
|
784
877
|
selectFilesImproved,
|
|
785
|
-
|
|
878
|
+
selectFilesKeyboard,
|
|
786
879
|
getFilesMatchingWildcard,
|
|
787
880
|
getSubfoldersRecursive,
|
|
788
881
|
pad
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvaslms-cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "A command line tool for interacting with Canvas LMS API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"canvas",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"CHANGELOG.md"
|
|
39
39
|
],
|
|
40
40
|
"scripts": {
|
|
41
|
-
"start": "
|
|
42
|
-
"dev": "
|
|
41
|
+
"start": "bun src/index.js",
|
|
42
|
+
"dev": "bun src/index.js",
|
|
43
43
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
44
44
|
"lint": "echo \"Add linting here\"",
|
|
45
45
|
"format": "echo \"Add formatting here\"",
|