canvaslms-cli 1.4.5 → 1.4.7
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/CHANGELOG.md +18 -0
- package/README.md +1 -9
- package/dist/commands/announcements.d.ts +3 -0
- package/dist/commands/announcements.d.ts.map +1 -0
- package/dist/commands/announcements.js +87 -0
- package/dist/commands/announcements.js.map +1 -0
- package/dist/commands/api.d.ts +3 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +8 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/assignments.d.ts +3 -0
- package/dist/commands/assignments.d.ts.map +1 -0
- package/dist/commands/assignments.js +100 -0
- package/dist/commands/assignments.js.map +1 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +202 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/grades.d.ts +3 -0
- package/dist/commands/grades.d.ts.map +1 -0
- package/dist/commands/grades.js +128 -0
- package/dist/commands/grades.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +61 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/profile.d.ts +6 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +30 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/submit.d.ts +8 -0
- package/dist/commands/submit.d.ts.map +1 -0
- package/dist/commands/submit.js +319 -0
- package/dist/commands/submit.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +2 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +69 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/config-validator.d.ts +4 -0
- package/dist/lib/config-validator.d.ts.map +1 -0
- package/dist/lib/config-validator.js +38 -0
- package/dist/lib/config-validator.js.map +1 -0
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +85 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/file-upload.d.ts +3 -0
- package/dist/lib/file-upload.d.ts.map +1 -0
- package/dist/lib/file-upload.js +52 -0
- package/dist/lib/file-upload.js.map +1 -0
- package/dist/lib/interactive.d.ts +16 -0
- package/dist/lib/interactive.d.ts.map +1 -0
- package/dist/lib/interactive.js +756 -0
- package/dist/lib/interactive.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +89 -0
- package/dist/src/index.js.map +1 -0
- package/dist/types/index.d.ts +146 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +76 -71
- package/commands/announcements.js +0 -102
- package/commands/api.js +0 -19
- package/commands/assignments.js +0 -100
- package/commands/config.js +0 -239
- package/commands/grades.js +0 -201
- package/commands/list.js +0 -68
- package/commands/profile.js +0 -35
- package/commands/submit.js +0 -603
- package/lib/api-client.js +0 -75
- package/lib/config-validator.js +0 -60
- package/lib/config.js +0 -103
- package/lib/file-upload.js +0 -74
- package/lib/interactive.js +0 -889
- package/src/index.js +0 -120
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import AdmZip from 'adm-zip';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
const KEYS = {
|
|
7
|
+
UP: '\u001b[A',
|
|
8
|
+
DOWN: '\u001b[B',
|
|
9
|
+
LEFT: '\u001b[D',
|
|
10
|
+
RIGHT: '\u001b[C',
|
|
11
|
+
SPACE: ' ',
|
|
12
|
+
ENTER: '\r',
|
|
13
|
+
ESCAPE: '\u001b',
|
|
14
|
+
BACKSPACE: '\u007f',
|
|
15
|
+
TAB: '\t',
|
|
16
|
+
CTRL_C: '\u0003'
|
|
17
|
+
};
|
|
18
|
+
export function createReadlineInterface() {
|
|
19
|
+
return readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function askQuestion(rl, question) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
rl.question(question, (answer) => {
|
|
27
|
+
resolve(answer.trim());
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function askQuestionWithValidation(rl, question, validator, errorMessage) {
|
|
32
|
+
return new Promise(async (resolve) => {
|
|
33
|
+
let answer;
|
|
34
|
+
do {
|
|
35
|
+
answer = await askQuestion(rl, question);
|
|
36
|
+
if (validator(answer)) {
|
|
37
|
+
resolve(answer.trim());
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(errorMessage || 'Invalid input. Please try again.');
|
|
42
|
+
}
|
|
43
|
+
} while (true);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function askConfirmation(rl, question, defaultYes = true, options = {}) {
|
|
47
|
+
const { requireExplicit = false } = options;
|
|
48
|
+
const suffix = defaultYes ? " (Y/n)" : " (y/N)";
|
|
49
|
+
while (true) {
|
|
50
|
+
const answer = await askQuestion(rl, question + suffix + ": ");
|
|
51
|
+
const lower = answer.toLowerCase();
|
|
52
|
+
if (lower === "") {
|
|
53
|
+
if (!requireExplicit) {
|
|
54
|
+
return defaultYes;
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (lower === "y" || lower === "yes") {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (lower === "n" || lower === "no") {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (!requireExplicit) {
|
|
66
|
+
return defaultYes;
|
|
67
|
+
}
|
|
68
|
+
console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function selectFromList(rl, items, displayProperty = null, allowCancel = true) {
|
|
72
|
+
if (!items || items.length === 0) {
|
|
73
|
+
console.log('No items to select from.');
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
console.log('\nSelect an option:');
|
|
77
|
+
items.forEach((item, index) => {
|
|
78
|
+
const displayText = displayProperty && typeof item === 'object' && item !== null ? item[displayProperty] : item;
|
|
79
|
+
console.log(`${index + 1}. ${displayText}`);
|
|
80
|
+
});
|
|
81
|
+
if (allowCancel) {
|
|
82
|
+
console.log('0. Cancel');
|
|
83
|
+
}
|
|
84
|
+
const validator = (input) => {
|
|
85
|
+
const num = parseInt(input);
|
|
86
|
+
return !isNaN(num) && num >= (allowCancel ? 0 : 1) && num <= items.length;
|
|
87
|
+
};
|
|
88
|
+
const answer = await askQuestionWithValidation(rl, '\nEnter your choice: ', validator, `Please enter a number between ${allowCancel ? '0' : '1'} and ${items.length}.`);
|
|
89
|
+
const choice = parseInt(answer);
|
|
90
|
+
if (choice === 0 && allowCancel) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const selectedItem = items[choice - 1];
|
|
94
|
+
return selectedItem !== undefined ? selectedItem : null;
|
|
95
|
+
}
|
|
96
|
+
export function getSubfoldersRecursive(startDir = process.cwd()) {
|
|
97
|
+
const result = [];
|
|
98
|
+
function walk(dir) {
|
|
99
|
+
const items = fs.readdirSync(dir);
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
const fullPath = path.join(dir, item);
|
|
102
|
+
try {
|
|
103
|
+
const stat = fs.statSync(fullPath);
|
|
104
|
+
if (stat.isDirectory()) {
|
|
105
|
+
const baseName = path.basename(fullPath);
|
|
106
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(baseName))
|
|
107
|
+
continue;
|
|
108
|
+
result.push(fullPath);
|
|
109
|
+
walk(fullPath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
walk(startDir);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
export function getFilesMatchingWildcard(pattern, currentDir = process.cwd()) {
|
|
120
|
+
try {
|
|
121
|
+
const allFolders = [currentDir, ...getSubfoldersRecursive(currentDir)];
|
|
122
|
+
let allFiles = [];
|
|
123
|
+
for (const folder of allFolders) {
|
|
124
|
+
const files = fs.readdirSync(folder).map(f => path.join(folder, f));
|
|
125
|
+
for (const filePath of files) {
|
|
126
|
+
try {
|
|
127
|
+
if (fs.statSync(filePath).isFile()) {
|
|
128
|
+
allFiles.push(filePath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) { }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let regexPattern;
|
|
135
|
+
let matchFullPath = false;
|
|
136
|
+
if (pattern === '*' || (!pattern.includes('.') && !pattern.includes('/'))) {
|
|
137
|
+
regexPattern = new RegExp('.*', 'i');
|
|
138
|
+
matchFullPath = true;
|
|
139
|
+
}
|
|
140
|
+
else if (pattern.startsWith('*.')) {
|
|
141
|
+
const extension = pattern.slice(2);
|
|
142
|
+
regexPattern = new RegExp(`\\.${extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
|
|
143
|
+
}
|
|
144
|
+
else if (pattern.includes('*')) {
|
|
145
|
+
regexPattern = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i');
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
regexPattern = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
|
|
149
|
+
}
|
|
150
|
+
const matchedFiles = allFiles.filter(filePath => {
|
|
151
|
+
if (matchFullPath) {
|
|
152
|
+
const relPath = path.relative(currentDir, filePath);
|
|
153
|
+
return regexPattern.test(relPath);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return regexPattern.test(path.basename(filePath));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return matchedFiles;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
163
|
+
console.error(`Error reading directory: ${errorMessage}`);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function pad(str, len) {
|
|
168
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
169
|
+
}
|
|
170
|
+
export async function selectFilesImproved(rl, currentDir = process.cwd()) {
|
|
171
|
+
const selectedFiles = [];
|
|
172
|
+
console.log(chalk.cyan.bold('\n' + '-'.repeat(50)));
|
|
173
|
+
console.log(chalk.cyan.bold('File Selection'));
|
|
174
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
175
|
+
console.log(chalk.yellow('Tips:'));
|
|
176
|
+
console.log(' • Type filename to add individual files');
|
|
177
|
+
console.log(' • Use wildcards: *.html, *.js, *.pdf, etc.');
|
|
178
|
+
console.log(' • Type "browse" to see available files');
|
|
179
|
+
console.log(' • Type "remove" to remove files from selection');
|
|
180
|
+
console.log(' • Type ".." or "back" to return to previous menu');
|
|
181
|
+
console.log(' • Press Enter with no input to finish selection\n');
|
|
182
|
+
while (true) {
|
|
183
|
+
if (selectedFiles.length > 0) {
|
|
184
|
+
console.log(chalk.cyan('\n' + '-'.repeat(50)));
|
|
185
|
+
console.log(chalk.cyan.bold(`Currently selected (${selectedFiles.length} files):`));
|
|
186
|
+
selectedFiles.forEach((file, index) => {
|
|
187
|
+
const stats = fs.statSync(file);
|
|
188
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
189
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
190
|
+
});
|
|
191
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
192
|
+
}
|
|
193
|
+
const input = await askQuestion(rl, chalk.bold.cyan('\nAdd file (or press Enter to finish): '));
|
|
194
|
+
if (!input.trim())
|
|
195
|
+
break;
|
|
196
|
+
if (input === '..' || input.toLowerCase() === 'back') {
|
|
197
|
+
return selectedFiles;
|
|
198
|
+
}
|
|
199
|
+
if (input.toLowerCase() === 'browse') {
|
|
200
|
+
console.log(chalk.cyan('\n' + '-'.repeat(50)));
|
|
201
|
+
console.log(chalk.cyan.bold('Browsing available files:'));
|
|
202
|
+
try {
|
|
203
|
+
const listedFiles = [];
|
|
204
|
+
function walk(dir) {
|
|
205
|
+
const entries = fs.readdirSync(dir);
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const fullPath = path.join(dir, entry);
|
|
208
|
+
const relPath = path.relative(currentDir, fullPath);
|
|
209
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(entry))
|
|
210
|
+
continue;
|
|
211
|
+
try {
|
|
212
|
+
const stat = fs.statSync(fullPath);
|
|
213
|
+
if (stat.isDirectory()) {
|
|
214
|
+
walk(fullPath);
|
|
215
|
+
}
|
|
216
|
+
else if (stat.isFile()) {
|
|
217
|
+
listedFiles.push({ path: fullPath, rel: relPath, size: stat.size });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
walk(currentDir);
|
|
226
|
+
if (listedFiles.length === 0) {
|
|
227
|
+
console.log(chalk.red(' No suitable files found.'));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
listedFiles.forEach((file, index) => {
|
|
231
|
+
const sizeKB = (file.size / 1024).toFixed(1);
|
|
232
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(file.rel, 35) + chalk.gray(sizeKB + ' KB'));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
238
|
+
console.log(chalk.red(' Error reading directory: ' + errorMessage));
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (input.toLowerCase() === 'remove') {
|
|
243
|
+
if (selectedFiles.length === 0) {
|
|
244
|
+
console.log(chalk.red('No files selected to remove.'));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
console.log(chalk.cyan('\nSelect file to remove:'));
|
|
248
|
+
selectedFiles.forEach((file, index) => {
|
|
249
|
+
console.log(pad(chalk.white((index + 1) + '.'), 5) + path.basename(file));
|
|
250
|
+
});
|
|
251
|
+
const removeChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter number to remove (or press Enter to cancel): '));
|
|
252
|
+
if (removeChoice.trim()) {
|
|
253
|
+
const removeIndex = parseInt(removeChoice) - 1;
|
|
254
|
+
if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
|
|
255
|
+
const removedFile = selectedFiles.splice(removeIndex, 1)[0];
|
|
256
|
+
if (removedFile) {
|
|
257
|
+
console.log(chalk.green(`Removed: ${path.basename(removedFile)}`));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.log(chalk.red('Invalid selection.'));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
let filePath = input;
|
|
267
|
+
let zipRequested = false;
|
|
268
|
+
if (filePath.endsWith(' -zip')) {
|
|
269
|
+
filePath = filePath.slice(0, -5).trim();
|
|
270
|
+
zipRequested = true;
|
|
271
|
+
}
|
|
272
|
+
if (!path.isAbsolute(filePath)) {
|
|
273
|
+
filePath = path.join(currentDir, filePath);
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
if (!fs.existsSync(filePath)) {
|
|
277
|
+
console.log(chalk.red('Error: File not found: ' + input));
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const stats = fs.statSync(filePath);
|
|
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
|
+
}
|
|
290
|
+
else if (stats.isFile()) {
|
|
291
|
+
zip.addLocalFile(filePath);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
console.log(chalk.red('Not a file or folder.'));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
zip.writeZip(zipPath);
|
|
298
|
+
console.log(chalk.green('Done.'));
|
|
299
|
+
console.log(chalk.green(`Created ZIP: ${zipName}`));
|
|
300
|
+
if (selectedFiles.includes(zipPath)) {
|
|
301
|
+
console.log(chalk.yellow(`File already selected: ${zipName}`));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
selectedFiles.push(zipPath);
|
|
305
|
+
const size = (fs.statSync(zipPath).size / 1024).toFixed(1) + ' KB';
|
|
306
|
+
console.log(chalk.green(`Added: ${zipName} (${size})`));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (stats.isDirectory()) {
|
|
310
|
+
const baseName = path.basename(filePath);
|
|
311
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(baseName))
|
|
312
|
+
continue;
|
|
313
|
+
const collectedFiles = [];
|
|
314
|
+
function walk(dir) {
|
|
315
|
+
const entries = fs.readdirSync(dir);
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
const fullPath = path.join(dir, entry);
|
|
318
|
+
const stat = fs.statSync(fullPath);
|
|
319
|
+
if (stat.isDirectory()) {
|
|
320
|
+
const baseName = path.basename(fullPath);
|
|
321
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(baseName))
|
|
322
|
+
continue;
|
|
323
|
+
walk(fullPath);
|
|
324
|
+
}
|
|
325
|
+
else if (stat.isFile()) {
|
|
326
|
+
collectedFiles.push(fullPath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
walk(filePath);
|
|
331
|
+
if (collectedFiles.length === 0) {
|
|
332
|
+
console.log(chalk.yellow(`Folder is empty: ${input}`));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
console.log(chalk.cyan(`\nFound ${collectedFiles.length} file(s) in folder "${path.relative(currentDir, filePath)}":`));
|
|
336
|
+
let totalSize = 0;
|
|
337
|
+
collectedFiles.forEach((f, i) => {
|
|
338
|
+
const stat = fs.statSync(f);
|
|
339
|
+
totalSize += stat.size;
|
|
340
|
+
const relativePath = path.relative(currentDir, f);
|
|
341
|
+
console.log(pad(chalk.white((i + 1) + '.'), 5) + pad(relativePath, 35) + chalk.gray((stat.size / 1024).toFixed(1) + ' KB'));
|
|
342
|
+
});
|
|
343
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
344
|
+
console.log(chalk.cyan(`Total size: ${(totalSize / 1024).toFixed(1)} KB`));
|
|
345
|
+
const confirmFolder = await askConfirmation(rl, chalk.bold.cyan(`Add all ${collectedFiles.length} files from this folder?`), true);
|
|
346
|
+
if (confirmFolder) {
|
|
347
|
+
const newFiles = collectedFiles.filter(f => !selectedFiles.includes(f));
|
|
348
|
+
selectedFiles.push(...newFiles);
|
|
349
|
+
console.log(chalk.green(`Added ${newFiles.length} new files (${collectedFiles.length - newFiles.length} already selected)`));
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (selectedFiles.includes(filePath)) {
|
|
354
|
+
console.log(chalk.yellow(`File already selected: ${path.basename(filePath)}`));
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
selectedFiles.push(filePath);
|
|
358
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
359
|
+
console.log(chalk.green(`Added: ${path.basename(filePath)} (${size})`));
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
363
|
+
console.log(chalk.red('Error accessing file: ' + errorMessage));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return selectedFiles;
|
|
367
|
+
}
|
|
368
|
+
export async function selectFilesKeyboard(_rl, currentDir = process.cwd(), allowedExtensions) {
|
|
369
|
+
const selectedFiles = [];
|
|
370
|
+
let fileList = [];
|
|
371
|
+
let currentPath = currentDir;
|
|
372
|
+
let currentIndex = 0;
|
|
373
|
+
let isNavigating = true;
|
|
374
|
+
const prevDataListeners = process.stdin.listeners('data').slice();
|
|
375
|
+
process.stdin.removeAllListeners('data');
|
|
376
|
+
if (process.stdin.isTTY) {
|
|
377
|
+
process.stdin.setRawMode(true);
|
|
378
|
+
}
|
|
379
|
+
process.stdin.resume();
|
|
380
|
+
process.stdin.setEncoding('utf8');
|
|
381
|
+
function getFileIcon(filename) {
|
|
382
|
+
const ext = path.extname(filename).toLowerCase();
|
|
383
|
+
const icons = {
|
|
384
|
+
'.pdf': '📄', '.doc': '📄', '.docx': '📄', '.txt': '📄',
|
|
385
|
+
'.js': '📜', '.ts': '📜', '.py': '📜', '.java': '📜', '.cpp': '📜', '.c': '📜',
|
|
386
|
+
'.html': '🌐', '.css': '🎨', '.scss': '🎨', '.less': '🎨',
|
|
387
|
+
'.json': '⚙️', '.xml': '⚙️', '.yml': '⚙️', '.yaml': '⚙️',
|
|
388
|
+
'.zip': '📦', '.rar': '📦', '.7z': '📦', '.tar': '📦',
|
|
389
|
+
'.jpg': '🖼️', '.jpeg': '🖼️', '.png': '🖼️', '.gif': '🖼️', '.svg': '🖼️',
|
|
390
|
+
'.mp4': '🎬', '.avi': '🎬', '.mov': '🎬', '.mkv': '🎬',
|
|
391
|
+
'.mp3': '🎵', '.wav': '🎵', '.flac': '🎵'
|
|
392
|
+
};
|
|
393
|
+
return icons[ext] || '📋';
|
|
394
|
+
}
|
|
395
|
+
function buildBreadcrumb() {
|
|
396
|
+
const relativePath = path.relative(currentDir, currentPath);
|
|
397
|
+
if (!relativePath || relativePath === '.') {
|
|
398
|
+
return '';
|
|
399
|
+
}
|
|
400
|
+
const parts = relativePath.split(path.sep);
|
|
401
|
+
const breadcrumb = parts.map((part, index) => {
|
|
402
|
+
if (index === parts.length - 1) {
|
|
403
|
+
return chalk.white.bold(part);
|
|
404
|
+
}
|
|
405
|
+
return chalk.gray(part);
|
|
406
|
+
}).join(chalk.gray(' › '));
|
|
407
|
+
return chalk.yellow('📂 ') + breadcrumb;
|
|
408
|
+
}
|
|
409
|
+
let lastDisplayLines = 0;
|
|
410
|
+
function clearPreviousDisplay() {
|
|
411
|
+
if (!process.stdout.isTTY || lastDisplayLines === 0)
|
|
412
|
+
return;
|
|
413
|
+
for (let i = 0; i < lastDisplayLines; i++) {
|
|
414
|
+
process.stdout.moveCursor(0, -1);
|
|
415
|
+
process.stdout.clearLine(0);
|
|
416
|
+
}
|
|
417
|
+
process.stdout.cursorTo(0);
|
|
418
|
+
}
|
|
419
|
+
function displayBrowser() {
|
|
420
|
+
clearPreviousDisplay();
|
|
421
|
+
lastDisplayLines = 0;
|
|
422
|
+
const breadcrumb = buildBreadcrumb();
|
|
423
|
+
if (breadcrumb) {
|
|
424
|
+
console.log(breadcrumb);
|
|
425
|
+
lastDisplayLines++;
|
|
426
|
+
}
|
|
427
|
+
console.log(chalk.gray('💡 ↑↓←→:Navigate', 'Space:Select', 'Enter:Open/Finish', 'Backspace:Up', 'a:All', 'c:Clear', 'r:Reload', 'Esc/Ctrl+C:Exit'));
|
|
428
|
+
lastDisplayLines++;
|
|
429
|
+
if (Array.isArray(allowedExtensions) && allowedExtensions.length > 0) {
|
|
430
|
+
const exts = allowedExtensions
|
|
431
|
+
.map(e => (e.startsWith('.') ? e.toLowerCase() : '.' + e.toLowerCase()))
|
|
432
|
+
.join(', ');
|
|
433
|
+
console.log(chalk.yellow(`Allowed: ${exts}`));
|
|
434
|
+
lastDisplayLines++;
|
|
435
|
+
}
|
|
436
|
+
if (selectedFiles.length > 0) {
|
|
437
|
+
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
438
|
+
try {
|
|
439
|
+
return sum + fs.statSync(file).size;
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return sum;
|
|
443
|
+
}
|
|
444
|
+
}, 0);
|
|
445
|
+
console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB) - Press Enter to finish`));
|
|
446
|
+
lastDisplayLines++;
|
|
447
|
+
}
|
|
448
|
+
console.log();
|
|
449
|
+
lastDisplayLines++;
|
|
450
|
+
if (fileList.length === 0) {
|
|
451
|
+
console.log(chalk.yellow('📭 No files found in this directory.'));
|
|
452
|
+
lastDisplayLines++;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
displayFileTree();
|
|
456
|
+
}
|
|
457
|
+
function displayFileTree() {
|
|
458
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
459
|
+
const maxDisplayItems = 50;
|
|
460
|
+
const startIdx = Math.max(0, currentIndex - Math.floor(maxDisplayItems / 2));
|
|
461
|
+
const endIdx = Math.min(fileList.length, startIdx + maxDisplayItems);
|
|
462
|
+
const visibleItems = fileList.slice(startIdx, endIdx);
|
|
463
|
+
if (startIdx > 0) {
|
|
464
|
+
console.log(chalk.gray(` ⋮ (${startIdx} items above)`));
|
|
465
|
+
lastDisplayLines++;
|
|
466
|
+
}
|
|
467
|
+
const maxItemWidth = Math.max(...visibleItems.map(item => {
|
|
468
|
+
const name = path.basename(item.path);
|
|
469
|
+
return name.length + 4;
|
|
470
|
+
}));
|
|
471
|
+
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25);
|
|
472
|
+
const columnsPerRow = Math.max(1, Math.floor((terminalWidth - 4) / itemWidth));
|
|
473
|
+
let currentRow = '';
|
|
474
|
+
let itemsInCurrentRow = 0;
|
|
475
|
+
visibleItems.forEach((item, index) => {
|
|
476
|
+
const actualIndex = startIdx + index;
|
|
477
|
+
const isSelected = selectedFiles.includes(item.path);
|
|
478
|
+
const isCurrent = actualIndex === currentIndex;
|
|
479
|
+
let icon = '';
|
|
480
|
+
if (item.type === 'parent' || item.type === 'directory') {
|
|
481
|
+
icon = '📁';
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
icon = getFileIcon(path.basename(item.path));
|
|
485
|
+
}
|
|
486
|
+
const name = item.name || path.basename(item.path);
|
|
487
|
+
const truncatedName = name.length > itemWidth - 4 ? name.slice(0, itemWidth - 7) + '...' : name;
|
|
488
|
+
let itemDisplay = `${icon} ${truncatedName}`;
|
|
489
|
+
if (isCurrent) {
|
|
490
|
+
if (isSelected) {
|
|
491
|
+
itemDisplay = chalk.black.bgGreen(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
492
|
+
}
|
|
493
|
+
else if (item.type === 'parent') {
|
|
494
|
+
itemDisplay = chalk.white.bgBlue(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
495
|
+
}
|
|
496
|
+
else if (item.type === 'directory') {
|
|
497
|
+
itemDisplay = chalk.black.bgCyan(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
itemDisplay = chalk.black.bgWhite(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
if (isSelected) {
|
|
505
|
+
itemDisplay = chalk.green(`✓${itemDisplay}`.padEnd(itemWidth));
|
|
506
|
+
}
|
|
507
|
+
else if (item.type === 'parent') {
|
|
508
|
+
itemDisplay = chalk.blue(` ${itemDisplay}`.padEnd(itemWidth));
|
|
509
|
+
}
|
|
510
|
+
else if (item.type === 'directory') {
|
|
511
|
+
itemDisplay = chalk.cyan(` ${itemDisplay}`.padEnd(itemWidth));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
itemDisplay = chalk.white(` ${itemDisplay}`.padEnd(itemWidth));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
currentRow += itemDisplay;
|
|
518
|
+
itemsInCurrentRow++;
|
|
519
|
+
if (itemsInCurrentRow >= columnsPerRow || index === visibleItems.length - 1) {
|
|
520
|
+
console.log(currentRow);
|
|
521
|
+
lastDisplayLines++;
|
|
522
|
+
currentRow = '';
|
|
523
|
+
itemsInCurrentRow = 0;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
if (endIdx < fileList.length) {
|
|
527
|
+
console.log(chalk.gray(` ⋮ (${fileList.length - endIdx} items below)`));
|
|
528
|
+
lastDisplayLines++;
|
|
529
|
+
}
|
|
530
|
+
console.log();
|
|
531
|
+
lastDisplayLines++;
|
|
532
|
+
if (fileList.length > maxDisplayItems) {
|
|
533
|
+
console.log(chalk.gray(`Showing ${startIdx + 1}-${endIdx} of ${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
console.log(chalk.gray(`${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
537
|
+
}
|
|
538
|
+
lastDisplayLines++;
|
|
539
|
+
const columnsInfo = `Grid: ${columnsPerRow} columns × ${itemWidth} chars | Terminal width: ${terminalWidth}`;
|
|
540
|
+
console.log(chalk.gray(columnsInfo));
|
|
541
|
+
lastDisplayLines++;
|
|
542
|
+
}
|
|
543
|
+
function refreshFileList() {
|
|
544
|
+
fileList = [];
|
|
545
|
+
try {
|
|
546
|
+
if (currentPath !== currentDir) {
|
|
547
|
+
fileList.push({
|
|
548
|
+
type: 'parent',
|
|
549
|
+
path: path.dirname(currentPath),
|
|
550
|
+
name: '..'
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const entries = fs.readdirSync(currentPath).sort();
|
|
554
|
+
entries.forEach(entry => {
|
|
555
|
+
const fullPath = path.join(currentPath, entry);
|
|
556
|
+
const stat = fs.statSync(fullPath);
|
|
557
|
+
if (stat.isDirectory()) {
|
|
558
|
+
fileList.push({
|
|
559
|
+
type: 'directory',
|
|
560
|
+
path: fullPath,
|
|
561
|
+
name: entry
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
entries.forEach(entry => {
|
|
566
|
+
const fullPath = path.join(currentPath, entry);
|
|
567
|
+
const stat = fs.statSync(fullPath);
|
|
568
|
+
if (stat.isFile()) {
|
|
569
|
+
if (Array.isArray(allowedExtensions) && allowedExtensions.length > 0) {
|
|
570
|
+
const lowerExts = allowedExtensions.map(e => (e.startsWith('.') ? e.toLowerCase() : '.' + e.toLowerCase()));
|
|
571
|
+
const ext = path.extname(entry).toLowerCase();
|
|
572
|
+
if (!lowerExts.includes(ext)) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
fileList.push({
|
|
577
|
+
type: 'file',
|
|
578
|
+
path: fullPath,
|
|
579
|
+
name: entry,
|
|
580
|
+
size: stat.size
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
if (currentIndex >= fileList.length) {
|
|
585
|
+
currentIndex = Math.max(0, fileList.length - 1);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
console.error('Error reading directory:', error);
|
|
590
|
+
fileList = [];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function handleKeyInput(key) {
|
|
594
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
595
|
+
const maxItemWidth = Math.max(...fileList.map(item => {
|
|
596
|
+
const name = path.basename(item.path);
|
|
597
|
+
return name.length + 4;
|
|
598
|
+
}));
|
|
599
|
+
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25);
|
|
600
|
+
const columnsPerRow = Math.max(1, Math.floor((terminalWidth - 4) / itemWidth));
|
|
601
|
+
switch (key) {
|
|
602
|
+
case KEYS.UP:
|
|
603
|
+
const newUpIndex = currentIndex - columnsPerRow;
|
|
604
|
+
if (newUpIndex >= 0) {
|
|
605
|
+
currentIndex = newUpIndex;
|
|
606
|
+
displayBrowser();
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
case KEYS.DOWN:
|
|
610
|
+
const newDownIndex = currentIndex + columnsPerRow;
|
|
611
|
+
if (newDownIndex < fileList.length) {
|
|
612
|
+
currentIndex = newDownIndex;
|
|
613
|
+
displayBrowser();
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
case KEYS.LEFT:
|
|
617
|
+
if (currentIndex > 0) {
|
|
618
|
+
currentIndex--;
|
|
619
|
+
displayBrowser();
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
case KEYS.RIGHT:
|
|
623
|
+
if (currentIndex < fileList.length - 1) {
|
|
624
|
+
currentIndex++;
|
|
625
|
+
displayBrowser();
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
case KEYS.SPACE:
|
|
629
|
+
if (fileList.length > 0) {
|
|
630
|
+
const item = fileList[currentIndex];
|
|
631
|
+
if (item && item.type === 'file') {
|
|
632
|
+
const index = selectedFiles.indexOf(item.path);
|
|
633
|
+
if (index === -1) {
|
|
634
|
+
selectedFiles.push(item.path);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
selectedFiles.splice(index, 1);
|
|
638
|
+
}
|
|
639
|
+
displayBrowser();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
case KEYS.ENTER:
|
|
644
|
+
if (fileList.length > 0) {
|
|
645
|
+
const item = fileList[currentIndex];
|
|
646
|
+
if (item && (item.type === 'parent' || item.type === 'directory')) {
|
|
647
|
+
currentPath = item.path;
|
|
648
|
+
currentIndex = 0;
|
|
649
|
+
refreshFileList();
|
|
650
|
+
displayBrowser();
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
if (selectedFiles.length > 0) {
|
|
654
|
+
isNavigating = false;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
if (selectedFiles.length > 0) {
|
|
660
|
+
isNavigating = false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
case KEYS.BACKSPACE:
|
|
665
|
+
if (currentPath !== currentDir) {
|
|
666
|
+
currentPath = path.dirname(currentPath);
|
|
667
|
+
currentIndex = 0;
|
|
668
|
+
refreshFileList();
|
|
669
|
+
displayBrowser();
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
case 'a':
|
|
673
|
+
let addedCount = 0;
|
|
674
|
+
fileList.forEach(item => {
|
|
675
|
+
if (item.type === 'file' && !selectedFiles.includes(item.path)) {
|
|
676
|
+
selectedFiles.push(item.path);
|
|
677
|
+
addedCount++;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
if (addedCount > 0) {
|
|
681
|
+
displayBrowser();
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
case 'c':
|
|
685
|
+
selectedFiles.length = 0;
|
|
686
|
+
displayBrowser();
|
|
687
|
+
break;
|
|
688
|
+
case 'r':
|
|
689
|
+
refreshFileList();
|
|
690
|
+
displayBrowser();
|
|
691
|
+
break;
|
|
692
|
+
case KEYS.CTRL_C:
|
|
693
|
+
selectedFiles.length = 0;
|
|
694
|
+
isNavigating = false;
|
|
695
|
+
break;
|
|
696
|
+
case KEYS.ESCAPE:
|
|
697
|
+
case '\u001b':
|
|
698
|
+
isNavigating = false;
|
|
699
|
+
break;
|
|
700
|
+
default:
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return new Promise((resolve) => {
|
|
705
|
+
refreshFileList();
|
|
706
|
+
displayBrowser();
|
|
707
|
+
const onData = (key) => {
|
|
708
|
+
if (!isNavigating) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const keyStr = key.toString();
|
|
712
|
+
handleKeyInput(keyStr);
|
|
713
|
+
if (!isNavigating) {
|
|
714
|
+
process.stdin.removeListener('data', onData);
|
|
715
|
+
try {
|
|
716
|
+
for (const l of prevDataListeners) {
|
|
717
|
+
process.stdin.on('data', l);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
}
|
|
722
|
+
if (process.stdin.isTTY) {
|
|
723
|
+
process.stdin.setRawMode(false);
|
|
724
|
+
}
|
|
725
|
+
process.stdin.pause();
|
|
726
|
+
if (selectedFiles.length > 0) {
|
|
727
|
+
console.log(chalk.green.bold('✅ File Selection Complete!'));
|
|
728
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
729
|
+
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
730
|
+
try {
|
|
731
|
+
return sum + fs.statSync(file).size;
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return sum;
|
|
735
|
+
}
|
|
736
|
+
}, 0);
|
|
737
|
+
console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
|
|
738
|
+
selectedFiles.forEach((file, index) => {
|
|
739
|
+
try {
|
|
740
|
+
const stats = fs.statSync(file);
|
|
741
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
742
|
+
console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
743
|
+
}
|
|
744
|
+
catch (e) {
|
|
745
|
+
console.log(pad(chalk.red(`${index + 1}.`), 5) + chalk.red(path.basename(file) + ' (Error reading file)'));
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
749
|
+
}
|
|
750
|
+
resolve(selectedFiles);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
process.stdin.on('data', onData);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
//# sourceMappingURL=interactive.js.map
|