canvaslms-cli 1.4.6 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +32 -22
- package/dist/commands/announcements.d.ts +3 -0
- package/dist/commands/announcements.d.ts.map +1 -0
- package/dist/commands/announcements.js +92 -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 +335 -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 +76 -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 +758 -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 +90 -0
- package/dist/src/index.js.map +1 -0
- package/dist/types/index.d.ts +147 -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 +15 -15
- 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
package/lib/interactive.js
DELETED
|
@@ -1,889 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive prompt utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
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';
|
|
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
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Create readline interface for user input
|
|
26
|
-
*/
|
|
27
|
-
function createReadlineInterface() {
|
|
28
|
-
return readline.createInterface({
|
|
29
|
-
input: process.stdin,
|
|
30
|
-
output: process.stdout
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Prompt user for input
|
|
36
|
-
*/
|
|
37
|
-
function askQuestion(rl, question) {
|
|
38
|
-
return new Promise((resolve) => {
|
|
39
|
-
rl.question(question, (answer) => {
|
|
40
|
-
resolve(answer.trim());
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Ask a question with validation and retry logic
|
|
47
|
-
*/
|
|
48
|
-
function askQuestionWithValidation(rl, question, validator, errorMessage) {
|
|
49
|
-
return new Promise(async (resolve) => {
|
|
50
|
-
let answer;
|
|
51
|
-
do {
|
|
52
|
-
answer = await askQuestion(rl, question);
|
|
53
|
-
if (validator(answer)) {
|
|
54
|
-
resolve(answer.trim());
|
|
55
|
-
return;
|
|
56
|
-
} else {
|
|
57
|
-
console.log(errorMessage || 'Invalid input. Please try again.');
|
|
58
|
-
}
|
|
59
|
-
} while (true);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Ask for confirmation (Y/n format)
|
|
65
|
-
*/
|
|
66
|
-
async function askConfirmation(rl, question, defaultYes = true, options = {}) {
|
|
67
|
-
const { requireExplicit = false } = options;
|
|
68
|
-
const suffix = defaultYes ? " (Y/n)" : " (y/N)";
|
|
69
|
-
|
|
70
|
-
while (true) {
|
|
71
|
-
const answer = await askQuestion(rl, question + suffix + ": ");
|
|
72
|
-
const lower = answer.toLowerCase();
|
|
73
|
-
|
|
74
|
-
// If user presses Enter → return defaultYes (true by default)
|
|
75
|
-
if (lower === "") {
|
|
76
|
-
if (!requireExplicit) {
|
|
77
|
-
return defaultYes;
|
|
78
|
-
}
|
|
79
|
-
console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Convert input to boolean
|
|
84
|
-
if (lower === "y" || lower === "yes") {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
if (lower === "n" || lower === "no") {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!requireExplicit) {
|
|
92
|
-
// If input is something else, fallback to defaultYes
|
|
93
|
-
return defaultYes;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Select from a list of options
|
|
102
|
-
*/
|
|
103
|
-
async function selectFromList(rl, items, displayProperty = null, allowCancel = true) {
|
|
104
|
-
if (!items || items.length === 0) {
|
|
105
|
-
console.log('No items to select from.');
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
console.log('\nSelect an option:');
|
|
110
|
-
items.forEach((item, index) => {
|
|
111
|
-
const displayText = displayProperty ? item[displayProperty] : item;
|
|
112
|
-
console.log(`${index + 1}. ${displayText}`);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (allowCancel) {
|
|
116
|
-
console.log('0. Cancel');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const validator = (input) => {
|
|
120
|
-
const num = parseInt(input);
|
|
121
|
-
return !isNaN(num) && num >= (allowCancel ? 0 : 1) && num <= items.length;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const answer = await askQuestionWithValidation(
|
|
125
|
-
rl,
|
|
126
|
-
'\nEnter your choice: ',
|
|
127
|
-
validator,
|
|
128
|
-
`Please enter a number between ${allowCancel ? '0' : '1'} and ${items.length}.`
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const choice = parseInt(answer);
|
|
132
|
-
|
|
133
|
-
if (choice === 0 && allowCancel) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return items[choice - 1];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function getSubfoldersRecursive(startDir = process.cwd()) {
|
|
141
|
-
const result = [];
|
|
142
|
-
function walk(dir) {
|
|
143
|
-
const items = fs.readdirSync(dir);
|
|
144
|
-
for (const item of items) {
|
|
145
|
-
const fullPath = path.join(dir, item);
|
|
146
|
-
try {
|
|
147
|
-
const stat = fs.statSync(fullPath);
|
|
148
|
-
if (stat.isDirectory()) {
|
|
149
|
-
const baseName = path.basename(fullPath);
|
|
150
|
-
if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
|
|
151
|
-
result.push(fullPath);
|
|
152
|
-
walk(fullPath); // recurse into subdirectory
|
|
153
|
-
}
|
|
154
|
-
} catch (err) {
|
|
155
|
-
// Optionally log: console.warn(`Skipped unreadable folder: ${fullPath}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
walk(startDir);
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Get files matching a wildcard pattern
|
|
165
|
-
*/
|
|
166
|
-
function getFilesMatchingWildcard(pattern, currentDir = process.cwd()) {
|
|
167
|
-
try {
|
|
168
|
-
// Gather all subfolders
|
|
169
|
-
const allFolders = [currentDir, ...getSubfoldersRecursive(currentDir)];
|
|
170
|
-
let allFiles = [];
|
|
171
|
-
for (const folder of allFolders) {
|
|
172
|
-
const files = fs.readdirSync(folder).map(f => path.join(folder, f));
|
|
173
|
-
for (const filePath of files) {
|
|
174
|
-
try {
|
|
175
|
-
if (fs.statSync(filePath).isFile()) {
|
|
176
|
-
allFiles.push(filePath);
|
|
177
|
-
}
|
|
178
|
-
} catch (e) {}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
// Convert wildcard pattern to regex
|
|
182
|
-
let regexPattern;
|
|
183
|
-
let matchFullPath = false;
|
|
184
|
-
if (pattern === '*' || (!pattern.includes('.') && !pattern.includes('/'))) {
|
|
185
|
-
regexPattern = new RegExp('.*', 'i');
|
|
186
|
-
matchFullPath = true;
|
|
187
|
-
} else if (pattern.startsWith('*.')) {
|
|
188
|
-
const extension = pattern.slice(2);
|
|
189
|
-
regexPattern = new RegExp(`\\.${extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
|
|
190
|
-
} else if (pattern.includes('*')) {
|
|
191
|
-
regexPattern = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'), 'i');
|
|
192
|
-
} else {
|
|
193
|
-
regexPattern = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i');
|
|
194
|
-
}
|
|
195
|
-
const matchedFiles = allFiles.filter(filePath => {
|
|
196
|
-
if (matchFullPath) {
|
|
197
|
-
const relPath = path.relative(currentDir, filePath);
|
|
198
|
-
return regexPattern.test(relPath);
|
|
199
|
-
} else {
|
|
200
|
-
return regexPattern.test(path.basename(filePath));
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
return matchedFiles;
|
|
204
|
-
} catch (error) {
|
|
205
|
-
console.error(`Error reading directory: ${error.message}`);
|
|
206
|
-
return [];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function pad(str, len) {
|
|
211
|
-
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Enhanced file selection with wildcard support
|
|
216
|
-
*/
|
|
217
|
-
async function selectFilesImproved(rl, currentDir = process.cwd()) {
|
|
218
|
-
const selectedFiles = [];
|
|
219
|
-
console.log(chalk.cyan.bold('\n' + '-'.repeat(50)));
|
|
220
|
-
console.log(chalk.cyan.bold('File Selection'));
|
|
221
|
-
console.log(chalk.cyan('-'.repeat(50)));
|
|
222
|
-
console.log(chalk.yellow('Tips:'));
|
|
223
|
-
console.log(' • Type filename to add individual files');
|
|
224
|
-
console.log(' • Use wildcards: *.html, *.js, *.pdf, etc.');
|
|
225
|
-
console.log(' • Type "browse" to see available files');
|
|
226
|
-
console.log(' • Type "remove" to remove files from selection');
|
|
227
|
-
console.log(' • Type ".." or "back" to return to previous menu');
|
|
228
|
-
console.log(' • Press Enter with no input to finish selection\n');
|
|
229
|
-
while (true) {
|
|
230
|
-
if (selectedFiles.length > 0) {
|
|
231
|
-
console.log(chalk.cyan('\n' + '-'.repeat(50)));
|
|
232
|
-
console.log(chalk.cyan.bold(`Currently selected (${selectedFiles.length} files):`));
|
|
233
|
-
selectedFiles.forEach((file, index) => {
|
|
234
|
-
const stats = fs.statSync(file);
|
|
235
|
-
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
236
|
-
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
237
|
-
});
|
|
238
|
-
console.log(chalk.cyan('-'.repeat(50)));
|
|
239
|
-
}
|
|
240
|
-
const input = await askQuestion(rl, chalk.bold.cyan('\nAdd file (or press Enter to finish): '));
|
|
241
|
-
if (!input.trim()) break;
|
|
242
|
-
if (input === '..' || input.toLowerCase() === 'back') {
|
|
243
|
-
return selectedFiles;
|
|
244
|
-
}
|
|
245
|
-
if (input.toLowerCase() === 'browse') {
|
|
246
|
-
console.log(chalk.cyan('\n' + '-'.repeat(50)));
|
|
247
|
-
console.log(chalk.cyan.bold('Browsing available files:'));
|
|
248
|
-
try {
|
|
249
|
-
const listedFiles = [];
|
|
250
|
-
function walk(dir) {
|
|
251
|
-
const entries = fs.readdirSync(dir);
|
|
252
|
-
for (const entry of entries) {
|
|
253
|
-
const fullPath = path.join(dir, entry);
|
|
254
|
-
const relPath = path.relative(currentDir, fullPath);
|
|
255
|
-
if (['node_modules', '.git', 'dist', 'build'].includes(entry)) continue;
|
|
256
|
-
try {
|
|
257
|
-
const stat = fs.statSync(fullPath);
|
|
258
|
-
if (stat.isDirectory()) {
|
|
259
|
-
walk(fullPath);
|
|
260
|
-
} else if (stat.isFile()) {
|
|
261
|
-
listedFiles.push({ path: fullPath, rel: relPath, size: stat.size });
|
|
262
|
-
}
|
|
263
|
-
} catch (e) { continue; }
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
walk(currentDir);
|
|
267
|
-
if (listedFiles.length === 0) {
|
|
268
|
-
console.log(chalk.red(' No suitable files found.'));
|
|
269
|
-
} else {
|
|
270
|
-
listedFiles.forEach((file, index) => {
|
|
271
|
-
const sizeKB = (file.size / 1024).toFixed(1);
|
|
272
|
-
console.log(pad(chalk.white((index + 1) + '.'), 5) + pad(file.rel, 35) + chalk.gray(sizeKB + ' KB'));
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
} catch (error) {
|
|
276
|
-
console.log(chalk.red(' Error reading directory: ' + error.message));
|
|
277
|
-
}
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
if (input.toLowerCase() === 'remove') {
|
|
281
|
-
if (selectedFiles.length === 0) {
|
|
282
|
-
console.log(chalk.red('No files selected to remove.'));
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
console.log(chalk.cyan('\nSelect file to remove:'));
|
|
286
|
-
selectedFiles.forEach((file, index) => {
|
|
287
|
-
console.log(pad(chalk.white((index + 1) + '.'), 5) + path.basename(file));
|
|
288
|
-
});
|
|
289
|
-
const removeChoice = await askQuestion(rl, chalk.bold.cyan('\nEnter number to remove (or press Enter to cancel): '));
|
|
290
|
-
if (removeChoice.trim()) {
|
|
291
|
-
const removeIndex = parseInt(removeChoice) - 1;
|
|
292
|
-
if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
|
|
293
|
-
const removedFile = selectedFiles.splice(removeIndex, 1)[0];
|
|
294
|
-
console.log(chalk.green(`Removed: ${path.basename(removedFile)}`));
|
|
295
|
-
} else {
|
|
296
|
-
console.log(chalk.red('Invalid selection.'));
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
let filePath = input;
|
|
302
|
-
let zipRequested = false;
|
|
303
|
-
if (filePath.endsWith(' -zip')) {
|
|
304
|
-
filePath = filePath.slice(0, -5).trim();
|
|
305
|
-
zipRequested = true;
|
|
306
|
-
}
|
|
307
|
-
if (!path.isAbsolute(filePath)) {
|
|
308
|
-
filePath = path.join(currentDir, filePath);
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
311
|
-
if (!fs.existsSync(filePath)) {
|
|
312
|
-
console.log(chalk.red('Error: File not found: ' + input));
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
const stats = fs.statSync(filePath);
|
|
316
|
-
if (zipRequested) {
|
|
317
|
-
const baseName = path.basename(filePath);
|
|
318
|
-
const zipName = baseName.replace(/\.[^/.]+$/, '') + '.zip';
|
|
319
|
-
const zipPath = path.join(currentDir, zipName);
|
|
320
|
-
const zip = new AdmZip();
|
|
321
|
-
process.stdout.write(chalk.yellow('Zipping, please wait... '));
|
|
322
|
-
if (stats.isDirectory()) {
|
|
323
|
-
zip.addLocalFolder(filePath);
|
|
324
|
-
} else if (stats.isFile()) {
|
|
325
|
-
zip.addLocalFile(filePath);
|
|
326
|
-
} else {
|
|
327
|
-
console.log(chalk.red('Not a file or folder.'));
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
zip.writeZip(zipPath);
|
|
331
|
-
console.log(chalk.green('Done.'));
|
|
332
|
-
console.log(chalk.green(`Created ZIP: ${zipName}`));
|
|
333
|
-
if (selectedFiles.includes(zipPath)) {
|
|
334
|
-
console.log(chalk.yellow(`File already selected: ${zipName}`));
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
selectedFiles.push(zipPath);
|
|
338
|
-
const size = (fs.statSync(zipPath).size / 1024).toFixed(1) + ' KB';
|
|
339
|
-
console.log(chalk.green(`Added: ${zipName} (${size})`));
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
if (stats.isDirectory()) {
|
|
343
|
-
const baseName = path.basename(filePath);
|
|
344
|
-
if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
|
|
345
|
-
const collectedFiles = [];
|
|
346
|
-
function walk(dir) {
|
|
347
|
-
const entries = fs.readdirSync(dir);
|
|
348
|
-
for (const entry of entries) {
|
|
349
|
-
const fullPath = path.join(dir, entry);
|
|
350
|
-
const stat = fs.statSync(fullPath);
|
|
351
|
-
if (stat.isDirectory()) {
|
|
352
|
-
const baseName = path.basename(fullPath);
|
|
353
|
-
if (['node_modules', '.git', 'dist', 'build'].includes(baseName)) continue;
|
|
354
|
-
walk(fullPath);
|
|
355
|
-
} else if (stat.isFile()) {
|
|
356
|
-
collectedFiles.push(fullPath);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
walk(filePath);
|
|
361
|
-
if (collectedFiles.length === 0) {
|
|
362
|
-
console.log(chalk.yellow(`Folder is empty: ${input}`));
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
console.log(chalk.cyan(`\nFound ${collectedFiles.length} file(s) in folder "${path.relative(currentDir, filePath)}":`));
|
|
366
|
-
let totalSize = 0;
|
|
367
|
-
collectedFiles.forEach((f, i) => {
|
|
368
|
-
const stat = fs.statSync(f);
|
|
369
|
-
totalSize += stat.size;
|
|
370
|
-
const relativePath = path.relative(currentDir, f);
|
|
371
|
-
console.log(pad(chalk.white((i + 1) + '.'), 5) + pad(relativePath, 35) + chalk.gray((stat.size / 1024).toFixed(1) + ' KB'));
|
|
372
|
-
});
|
|
373
|
-
console.log(chalk.cyan('-'.repeat(50)));
|
|
374
|
-
console.log(chalk.cyan(`Total size: ${(totalSize / 1024).toFixed(1)} KB`));
|
|
375
|
-
const confirmFolder = await askConfirmation(rl, chalk.bold.cyan(`Add all ${collectedFiles.length} files from this folder?`), true);
|
|
376
|
-
if (confirmFolder) {
|
|
377
|
-
const newFiles = collectedFiles.filter(f => !selectedFiles.includes(f));
|
|
378
|
-
selectedFiles.push(...newFiles);
|
|
379
|
-
console.log(chalk.green(`Added ${newFiles.length} new files (${collectedFiles.length - newFiles.length} already selected)`));
|
|
380
|
-
}
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
if (selectedFiles.includes(filePath)) {
|
|
384
|
-
console.log(chalk.yellow(`File already selected: ${path.basename(filePath)}`));
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
selectedFiles.push(filePath);
|
|
388
|
-
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
389
|
-
console.log(chalk.green(`Added: ${path.basename(filePath)} (${size})`));
|
|
390
|
-
} catch (error) {
|
|
391
|
-
console.log(chalk.red('Error accessing file: ' + error.message));
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return selectedFiles;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Interactive file selector with tree view and keyboard navigation
|
|
399
|
-
*/
|
|
400
|
-
async function selectFilesKeyboard(rl, currentDir = process.cwd()) {
|
|
401
|
-
const selectedFiles = [];
|
|
402
|
-
let expandedFolders = new Set();
|
|
403
|
-
let fileTree = [];
|
|
404
|
-
let fileList = [];
|
|
405
|
-
let currentPath = currentDir;
|
|
406
|
-
let currentIndex = 0;
|
|
407
|
-
let isNavigating = true;
|
|
408
|
-
let viewStartIndex = 0;
|
|
409
|
-
const maxVisibleItems = 15;
|
|
410
|
-
|
|
411
|
-
// Setup raw mode for keyboard input
|
|
412
|
-
process.stdin.setRawMode(true);
|
|
413
|
-
process.stdin.resume();
|
|
414
|
-
process.stdin.setEncoding('utf8');
|
|
415
|
-
|
|
416
|
-
// Helper function to build file tree
|
|
417
|
-
function buildFileTree(basePath = currentDir, level = 0, parentPath = '') {
|
|
418
|
-
const tree = [];
|
|
419
|
-
try {
|
|
420
|
-
const entries = fs.readdirSync(basePath).sort();
|
|
421
|
-
|
|
422
|
-
// Add directories first
|
|
423
|
-
entries.forEach(entry => {
|
|
424
|
-
const fullPath = path.join(basePath, entry);
|
|
425
|
-
const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
const stats = fs.statSync(fullPath);
|
|
429
|
-
if (stats.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.vscode', '.next'].includes(entry)) {
|
|
430
|
-
const isExpanded = expandedFolders.has(fullPath);
|
|
431
|
-
tree.push({
|
|
432
|
-
name: entry,
|
|
433
|
-
path: fullPath,
|
|
434
|
-
relativePath,
|
|
435
|
-
type: 'directory',
|
|
436
|
-
level,
|
|
437
|
-
isExpanded,
|
|
438
|
-
size: 0
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
// If expanded, add children
|
|
442
|
-
if (isExpanded) {
|
|
443
|
-
const children = buildFileTree(fullPath, level + 1, relativePath);
|
|
444
|
-
tree.push(...children);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
} catch (e) {}
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
// Add files
|
|
451
|
-
entries.forEach(entry => {
|
|
452
|
-
const fullPath = path.join(basePath, entry);
|
|
453
|
-
const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
|
|
454
|
-
|
|
455
|
-
try {
|
|
456
|
-
const stats = fs.statSync(fullPath);
|
|
457
|
-
if (stats.isFile()) {
|
|
458
|
-
tree.push({
|
|
459
|
-
name: entry,
|
|
460
|
-
path: fullPath,
|
|
461
|
-
relativePath,
|
|
462
|
-
type: 'file',
|
|
463
|
-
level,
|
|
464
|
-
size: stats.size
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
} catch (e) {}
|
|
468
|
-
});
|
|
469
|
-
} catch (error) {
|
|
470
|
-
console.log(chalk.red('Error reading directory: ' + error.message));
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return tree;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Helper function to get file icon based on extension
|
|
477
|
-
function getFileIcon(filename) {
|
|
478
|
-
const ext = path.extname(filename).toLowerCase();
|
|
479
|
-
const icons = {
|
|
480
|
-
'.pdf': '📄', '.doc': '📄', '.docx': '📄', '.txt': '📄',
|
|
481
|
-
'.js': '📜', '.ts': '📜', '.py': '📜', '.java': '📜', '.cpp': '📜', '.c': '📜',
|
|
482
|
-
'.html': '🌐', '.css': '🎨', '.scss': '🎨', '.less': '🎨',
|
|
483
|
-
'.json': '⚙️', '.xml': '⚙️', '.yml': '⚙️', '.yaml': '⚙️',
|
|
484
|
-
'.zip': '📦', '.rar': '📦', '.7z': '📦', '.tar': '📦',
|
|
485
|
-
'.jpg': '🖼️', '.jpeg': '🖼️', '.png': '🖼️', '.gif': '🖼️', '.svg': '🖼️',
|
|
486
|
-
'.mp4': '🎬', '.avi': '🎬', '.mov': '🎬', '.mkv': '🎬',
|
|
487
|
-
'.mp3': '🎵', '.wav': '🎵', '.flac': '🎵'
|
|
488
|
-
};
|
|
489
|
-
return icons[ext] || '📋';
|
|
490
|
-
}
|
|
491
|
-
// Helper function to build breadcrumb path
|
|
492
|
-
function buildBreadcrumb() {
|
|
493
|
-
const relativePath = path.relative(currentDir, currentPath);
|
|
494
|
-
if (!relativePath || relativePath === '.') {
|
|
495
|
-
return ''
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const parts = relativePath.split(path.sep);
|
|
499
|
-
const breadcrumb = parts.map((part, index) => {
|
|
500
|
-
if (index === parts.length - 1) {
|
|
501
|
-
return chalk.white.bold(part);
|
|
502
|
-
}
|
|
503
|
-
return chalk.gray(part);
|
|
504
|
-
}).join(chalk.gray(' › '));
|
|
505
|
-
|
|
506
|
-
return chalk.yellow('📂 ') + breadcrumb;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Helper to clear the terminal without dropping scrollback history
|
|
510
|
-
function safeClearScreen() {
|
|
511
|
-
if (!process.stdout.isTTY) return;
|
|
512
|
-
|
|
513
|
-
process.stdout.write('\x1b[H\x1b[J'); // Move cursor to top and clear below, not full console.clear()
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Helper function to display the file browser
|
|
518
|
-
function displayBrowser() {
|
|
519
|
-
safeClearScreen();
|
|
520
|
-
// Keyboard controls - compact format at top
|
|
521
|
-
const controls = [
|
|
522
|
-
];
|
|
523
|
-
// Breadcrumb path
|
|
524
|
-
console.log(buildBreadcrumb());
|
|
525
|
-
console.log(chalk.gray('💡 ↑↓←→:Navigate', 'Space:Select', 'Enter:Open/Finish', 'Backspace:Up', 'a:All', 'c:Clear', 'Esc:Exit'));
|
|
526
|
-
|
|
527
|
-
// Selected files count
|
|
528
|
-
if (selectedFiles.length > 0) {
|
|
529
|
-
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
530
|
-
try {
|
|
531
|
-
return sum + fs.statSync(file).size;
|
|
532
|
-
} catch {
|
|
533
|
-
return sum;
|
|
534
|
-
}
|
|
535
|
-
}, 0);
|
|
536
|
-
console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB) - Press Enter to finish`));
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
console.log();
|
|
540
|
-
|
|
541
|
-
if (fileList.length === 0) {
|
|
542
|
-
console.log(chalk.yellow('📭 No files found in this directory.'));
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Display files with tree-like structure
|
|
547
|
-
displayFileTree();
|
|
548
|
-
}
|
|
549
|
-
// Helper function to display files in a horizontal grid layout
|
|
550
|
-
function displayFileTree() {
|
|
551
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
552
|
-
const maxDisplayItems = 50; // Show more items in grid view
|
|
553
|
-
const startIdx = Math.max(0, currentIndex - Math.floor(maxDisplayItems / 2));
|
|
554
|
-
const endIdx = Math.min(fileList.length, startIdx + maxDisplayItems);
|
|
555
|
-
const visibleItems = fileList.slice(startIdx, endIdx);
|
|
556
|
-
|
|
557
|
-
// Show scroll indicators
|
|
558
|
-
if (startIdx > 0) {
|
|
559
|
-
console.log(chalk.gray(` ⋮ (${startIdx} items above)`));
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Calculate item width and columns
|
|
563
|
-
const maxItemWidth = Math.max(...visibleItems.map(item => {
|
|
564
|
-
const name = path.basename(item.path);
|
|
565
|
-
return name.length + 4; // 2 for icon + space, 2 for padding
|
|
566
|
-
}));
|
|
567
|
-
|
|
568
|
-
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25); // Min 15, max 25 chars
|
|
569
|
-
const columnsPerRow = Math.floor((terminalWidth - 4) / itemWidth); // Leave 4 chars margin
|
|
570
|
-
const actualColumns = Math.max(1, columnsPerRow);
|
|
571
|
-
|
|
572
|
-
// Group items into rows
|
|
573
|
-
let currentRow = '';
|
|
574
|
-
let itemsInCurrentRow = 0;
|
|
575
|
-
|
|
576
|
-
visibleItems.forEach((item, index) => {
|
|
577
|
-
const actualIndex = startIdx + index;
|
|
578
|
-
const isSelected = selectedFiles.includes(item.path);
|
|
579
|
-
const isCurrent = actualIndex === currentIndex;
|
|
580
|
-
|
|
581
|
-
// Get icon and name
|
|
582
|
-
let icon = '';
|
|
583
|
-
if (item.type === 'parent' || item.type === 'directory') {
|
|
584
|
-
icon = '📁';
|
|
585
|
-
} else {
|
|
586
|
-
icon = getFileIcon(path.basename(item.path));
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const name = item.name || path.basename(item.path);
|
|
590
|
-
const truncatedName = name.length > itemWidth - 4 ? name.slice(0, itemWidth - 7) + '...' : name;
|
|
591
|
-
|
|
592
|
-
// Build item display string
|
|
593
|
-
let itemDisplay = `${icon} ${truncatedName}`;
|
|
594
|
-
|
|
595
|
-
// Apply styling based on state
|
|
596
|
-
if (isCurrent) {
|
|
597
|
-
if (isSelected) {
|
|
598
|
-
itemDisplay = chalk.black.bgGreen(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
599
|
-
} else if (item.type === 'parent') {
|
|
600
|
-
itemDisplay = chalk.white.bgBlue(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
601
|
-
} else if (item.type === 'directory') {
|
|
602
|
-
itemDisplay = chalk.black.bgCyan(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
603
|
-
} else {
|
|
604
|
-
itemDisplay = chalk.black.bgWhite(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
605
|
-
}
|
|
606
|
-
} else {
|
|
607
|
-
if (isSelected) {
|
|
608
|
-
itemDisplay = chalk.green(`✓${itemDisplay}`.padEnd(itemWidth));
|
|
609
|
-
} else if (item.type === 'parent') {
|
|
610
|
-
itemDisplay = chalk.blue(` ${itemDisplay}`.padEnd(itemWidth));
|
|
611
|
-
} else if (item.type === 'directory') {
|
|
612
|
-
itemDisplay = chalk.cyan(` ${itemDisplay}`.padEnd(itemWidth));
|
|
613
|
-
} else {
|
|
614
|
-
itemDisplay = chalk.white(` ${itemDisplay}`.padEnd(itemWidth));
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Add to current row
|
|
619
|
-
currentRow += itemDisplay;
|
|
620
|
-
itemsInCurrentRow++;
|
|
621
|
-
|
|
622
|
-
// Check if we need to start a new row
|
|
623
|
-
if (itemsInCurrentRow >= actualColumns || index === visibleItems.length - 1) {
|
|
624
|
-
console.log(currentRow);
|
|
625
|
-
currentRow = '';
|
|
626
|
-
itemsInCurrentRow = 0;
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
// Show scroll indicators
|
|
631
|
-
if (endIdx < fileList.length) {
|
|
632
|
-
console.log(chalk.gray(` ⋮ (${fileList.length - endIdx} items below)`));
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
console.log();
|
|
636
|
-
|
|
637
|
-
// Show current position and navigation info
|
|
638
|
-
if (fileList.length > maxDisplayItems) {
|
|
639
|
-
console.log(chalk.gray(`Showing ${startIdx + 1}-${endIdx} of ${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
640
|
-
} else {
|
|
641
|
-
console.log(chalk.gray(`${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Show grid info
|
|
645
|
-
console.log(chalk.gray(`Grid: ${actualColumns} columns × ${itemWidth} chars | Terminal width: ${terminalWidth}`));
|
|
646
|
-
}
|
|
647
|
-
// Helper function to refresh file list for current directory
|
|
648
|
-
function refreshFileList() {
|
|
649
|
-
fileList = [];
|
|
650
|
-
|
|
651
|
-
try {
|
|
652
|
-
// Add parent directory option if not at root
|
|
653
|
-
if (currentPath !== currentDir) {
|
|
654
|
-
fileList.push({
|
|
655
|
-
type: 'parent',
|
|
656
|
-
path: path.dirname(currentPath),
|
|
657
|
-
name: '..'
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Read current directory
|
|
662
|
-
const entries = fs.readdirSync(currentPath).sort();
|
|
663
|
-
|
|
664
|
-
// Add directories first
|
|
665
|
-
entries.forEach(entry => {
|
|
666
|
-
const fullPath = path.join(currentPath, entry);
|
|
667
|
-
const stat = fs.statSync(fullPath);
|
|
668
|
-
|
|
669
|
-
if (stat.isDirectory()) {
|
|
670
|
-
fileList.push({
|
|
671
|
-
type: 'directory',
|
|
672
|
-
path: fullPath,
|
|
673
|
-
name: entry
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// Add files
|
|
679
|
-
entries.forEach(entry => {
|
|
680
|
-
const fullPath = path.join(currentPath, entry);
|
|
681
|
-
const stat = fs.statSync(fullPath);
|
|
682
|
-
|
|
683
|
-
if (stat.isFile()) {
|
|
684
|
-
fileList.push({
|
|
685
|
-
type: 'file',
|
|
686
|
-
path: fullPath,
|
|
687
|
-
name: entry,
|
|
688
|
-
size: stat.size
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
// Ensure currentIndex is within bounds
|
|
694
|
-
if (currentIndex >= fileList.length) {
|
|
695
|
-
currentIndex = Math.max(0, fileList.length - 1);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
} catch (error) {
|
|
699
|
-
console.error('Error reading directory:', error.message);
|
|
700
|
-
fileList = [];
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
// Main keyboard event handler
|
|
704
|
-
function handleKeyInput(key) {
|
|
705
|
-
// Calculate grid dimensions for navigation
|
|
706
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
707
|
-
const maxItemWidth = Math.max(...fileList.map(item => {
|
|
708
|
-
const name = path.basename(item.path);
|
|
709
|
-
return name.length + 4;
|
|
710
|
-
}));
|
|
711
|
-
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25);
|
|
712
|
-
const columnsPerRow = Math.max(1, Math.floor((terminalWidth - 4) / itemWidth));
|
|
713
|
-
|
|
714
|
-
switch (key) {
|
|
715
|
-
case KEYS.UP:
|
|
716
|
-
// Move up by one row (subtract columns)
|
|
717
|
-
const newUpIndex = currentIndex - columnsPerRow;
|
|
718
|
-
if (newUpIndex >= 0) {
|
|
719
|
-
currentIndex = newUpIndex;
|
|
720
|
-
displayBrowser();
|
|
721
|
-
}
|
|
722
|
-
break;
|
|
723
|
-
|
|
724
|
-
case KEYS.DOWN:
|
|
725
|
-
// Move down by one row (add columns)
|
|
726
|
-
const newDownIndex = currentIndex + columnsPerRow;
|
|
727
|
-
if (newDownIndex < fileList.length) {
|
|
728
|
-
currentIndex = newDownIndex;
|
|
729
|
-
displayBrowser();
|
|
730
|
-
}
|
|
731
|
-
break;
|
|
732
|
-
|
|
733
|
-
case KEYS.LEFT:
|
|
734
|
-
// Move left by one column
|
|
735
|
-
if (currentIndex > 0) {
|
|
736
|
-
currentIndex--;
|
|
737
|
-
displayBrowser();
|
|
738
|
-
}
|
|
739
|
-
break;
|
|
740
|
-
|
|
741
|
-
case KEYS.RIGHT:
|
|
742
|
-
// Move right by one column
|
|
743
|
-
if (currentIndex < fileList.length - 1) {
|
|
744
|
-
currentIndex++;
|
|
745
|
-
displayBrowser();
|
|
746
|
-
}
|
|
747
|
-
break;
|
|
748
|
-
|
|
749
|
-
case KEYS.SPACE:
|
|
750
|
-
if (fileList.length > 0) {
|
|
751
|
-
const item = fileList[currentIndex];
|
|
752
|
-
if (item.type === 'file') {
|
|
753
|
-
const index = selectedFiles.indexOf(item.path);
|
|
754
|
-
if (index === -1) {
|
|
755
|
-
selectedFiles.push(item.path);
|
|
756
|
-
} else {
|
|
757
|
-
selectedFiles.splice(index, 1);
|
|
758
|
-
}
|
|
759
|
-
displayBrowser();
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
break;
|
|
763
|
-
case KEYS.ENTER:
|
|
764
|
-
if (fileList.length > 0) {
|
|
765
|
-
const item = fileList[currentIndex];
|
|
766
|
-
if (item.type === 'parent' || item.type === 'directory') {
|
|
767
|
-
currentPath = item.path;
|
|
768
|
-
currentIndex = 0;
|
|
769
|
-
refreshFileList();
|
|
770
|
-
displayBrowser();
|
|
771
|
-
} else {
|
|
772
|
-
// When Enter is pressed on a file, finish selection if files are selected
|
|
773
|
-
if (selectedFiles.length > 0) {
|
|
774
|
-
isNavigating = false;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
// Finish selection when directory is empty and Enter is pressed (if files selected)
|
|
779
|
-
if (selectedFiles.length > 0) {
|
|
780
|
-
isNavigating = false;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
break;
|
|
784
|
-
|
|
785
|
-
case KEYS.BACKSPACE:
|
|
786
|
-
if (currentPath !== currentDir) {
|
|
787
|
-
currentPath = path.dirname(currentPath);
|
|
788
|
-
currentIndex = 0;
|
|
789
|
-
refreshFileList();
|
|
790
|
-
displayBrowser();
|
|
791
|
-
}
|
|
792
|
-
break;
|
|
793
|
-
|
|
794
|
-
case 'a':
|
|
795
|
-
// Select all files in current directory
|
|
796
|
-
let addedCount = 0;
|
|
797
|
-
fileList.forEach(item => {
|
|
798
|
-
if (item.type === 'file' && !selectedFiles.includes(item.path)) {
|
|
799
|
-
selectedFiles.push(item.path);
|
|
800
|
-
addedCount++;
|
|
801
|
-
}
|
|
802
|
-
});
|
|
803
|
-
if (addedCount > 0) {
|
|
804
|
-
displayBrowser();
|
|
805
|
-
}
|
|
806
|
-
break;
|
|
807
|
-
|
|
808
|
-
case 'c':
|
|
809
|
-
// Clear all selections
|
|
810
|
-
selectedFiles.length = 0;
|
|
811
|
-
displayBrowser();
|
|
812
|
-
break;
|
|
813
|
-
|
|
814
|
-
case KEYS.ESCAPE:
|
|
815
|
-
case '\u001b': // ESC key
|
|
816
|
-
isNavigating = false;
|
|
817
|
-
break;
|
|
818
|
-
|
|
819
|
-
default:
|
|
820
|
-
// Ignore other keys
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Initialize and start navigation
|
|
826
|
-
return new Promise((resolve) => {
|
|
827
|
-
refreshFileList();
|
|
828
|
-
displayBrowser();
|
|
829
|
-
|
|
830
|
-
process.stdin.on('data', (key) => {
|
|
831
|
-
if (!isNavigating) {
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const keyStr = key.toString();
|
|
836
|
-
handleKeyInput(keyStr);
|
|
837
|
-
|
|
838
|
-
if (!isNavigating) {
|
|
839
|
-
// Cleanup
|
|
840
|
-
process.stdin.removeAllListeners('data');
|
|
841
|
-
process.stdin.setRawMode(false);
|
|
842
|
-
process.stdin.pause();
|
|
843
|
-
|
|
844
|
-
// Display completion summary
|
|
845
|
-
if (selectedFiles.length > 0) {
|
|
846
|
-
console.log(chalk.green.bold('✅ File Selection Complete!'));
|
|
847
|
-
console.log(chalk.cyan('-'.repeat(50)));
|
|
848
|
-
|
|
849
|
-
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
850
|
-
try {
|
|
851
|
-
return sum + fs.statSync(file).size;
|
|
852
|
-
} catch {
|
|
853
|
-
return sum;
|
|
854
|
-
}
|
|
855
|
-
}, 0);
|
|
856
|
-
|
|
857
|
-
console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
|
|
858
|
-
selectedFiles.forEach((file, index) => {
|
|
859
|
-
try {
|
|
860
|
-
const stats = fs.statSync(file);
|
|
861
|
-
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
862
|
-
console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
863
|
-
} catch (e) {
|
|
864
|
-
console.log(pad(chalk.red(`${index + 1}.`), 5) + chalk.red(path.basename(file) + ' (Error reading file)'));
|
|
865
|
-
}
|
|
866
|
-
});
|
|
867
|
-
console.log(chalk.cyan('-'.repeat(50)));
|
|
868
|
-
} else {
|
|
869
|
-
console.log(chalk.yellow('No files selected.'));
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
resolve(selectedFiles);
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
export {
|
|
879
|
-
createReadlineInterface,
|
|
880
|
-
askQuestion,
|
|
881
|
-
askQuestionWithValidation,
|
|
882
|
-
askConfirmation,
|
|
883
|
-
selectFromList,
|
|
884
|
-
selectFilesImproved,
|
|
885
|
-
selectFilesKeyboard,
|
|
886
|
-
getFilesMatchingWildcard,
|
|
887
|
-
getSubfoldersRecursive,
|
|
888
|
-
pad
|
|
889
|
-
};
|