sapper-iq 1.1.29 → 1.1.30

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/sapper.mjs CHANGED
@@ -348,7 +348,7 @@ function getFilesForPicker(dir = '.', prefix = '', maxFiles = 50) {
348
348
  return files.slice(0, maxFiles);
349
349
  }
350
350
 
351
- // Interactive file picker
351
+ // Interactive file picker with arrow keys
352
352
  async function pickFiles() {
353
353
  const files = getFilesForPicker('.', '', 50).filter(f => !f.isDir);
354
354
 
@@ -357,56 +357,117 @@ async function pickFiles() {
357
357
  return [];
358
358
  }
359
359
 
360
- console.log();
361
- console.log(box(
362
- `Select files by number (e.g., ${chalk.cyan('1 3 5')} or ${chalk.cyan('1-5')} or ${chalk.cyan('all')})\n` +
363
- `Press ${chalk.cyan('Enter')} with no input to cancel`,
364
- 'šŸ“Ž File Picker', 'cyan'
365
- ));
366
- console.log();
367
-
368
- // Display files in columns
369
- files.forEach((file, i) => {
370
- const num = chalk.cyan.bold(`[${(i + 1).toString().padStart(2)}]`);
371
- const size = file.size ? chalk.gray(`(${Math.round(file.size/1024)}KB)`) : '';
372
- console.log(` ${num} ${chalk.white(file.path)} ${size}`);
373
- });
360
+ const selected = new Set();
361
+ let cursor = 0;
362
+ const pageSize = Math.min(15, process.stdout.rows - 10 || 15);
374
363
 
375
- console.log();
376
- const selection = await safeQuestion(chalk.cyan('Select files: '));
377
-
378
- if (!selection.trim()) {
379
- console.log(chalk.gray('Cancelled.'));
380
- return [];
364
+ // Enable raw mode for key capture
365
+ if (process.stdin.isTTY) {
366
+ process.stdin.setRawMode(true);
381
367
  }
368
+ process.stdin.resume();
382
369
 
383
- // Parse selection
384
- const selectedFiles = [];
385
- const parts = selection.toLowerCase().split(/[\s,]+/);
386
-
387
- for (const part of parts) {
388
- if (part === 'all') {
389
- return files.map(f => f.path);
370
+ const renderList = () => {
371
+ // Clear screen and move cursor to top
372
+ console.clear();
373
+ console.log(box(
374
+ `${chalk.cyan('↑↓')} Navigate ${chalk.cyan('Space')} Toggle ${chalk.cyan('a')} All ${chalk.cyan('Enter')} Confirm ${chalk.cyan('q/Esc')} Cancel`,
375
+ 'šŸ“Ž Select Files', 'cyan'
376
+ ));
377
+ console.log();
378
+
379
+ // Calculate visible range (pagination)
380
+ const startIdx = Math.max(0, Math.min(cursor - Math.floor(pageSize / 2), files.length - pageSize));
381
+ const endIdx = Math.min(startIdx + pageSize, files.length);
382
+
383
+ // Show scroll indicator if needed
384
+ if (startIdx > 0) {
385
+ console.log(chalk.gray(' ↑ more files above...'));
390
386
  }
391
387
 
392
- // Handle ranges like "1-5"
393
- const rangeMatch = part.match(/^(\d+)-(\d+)$/);
394
- if (rangeMatch) {
395
- const start = parseInt(rangeMatch[1]);
396
- const end = parseInt(rangeMatch[2]);
397
- for (let i = start; i <= end && i <= files.length; i++) {
398
- if (i >= 1) selectedFiles.push(files[i - 1].path);
399
- }
400
- } else {
401
- // Single number
402
- const num = parseInt(part);
403
- if (num >= 1 && num <= files.length) {
404
- selectedFiles.push(files[num - 1].path);
405
- }
388
+ for (let i = startIdx; i < endIdx; i++) {
389
+ const file = files[i];
390
+ const isSelected = selected.has(i);
391
+ const isCursor = i === cursor;
392
+
393
+ const checkbox = isSelected ? chalk.green('ā—‰') : chalk.gray('ā—‹');
394
+ const prefix = isCursor ? chalk.cyan('ā–ø ') : ' ';
395
+ const name = isCursor ? chalk.cyan.bold(file.path) : chalk.white(file.path);
396
+ const size = file.size ? chalk.gray(` (${Math.round(file.size/1024)}KB)`) : '';
397
+
398
+ console.log(`${prefix}${checkbox} ${name}${size}`);
406
399
  }
407
- }
400
+
401
+ if (endIdx < files.length) {
402
+ console.log(chalk.gray(' ↓ more files below...'));
403
+ }
404
+
405
+ console.log();
406
+ console.log(chalk.gray(` Selected: ${selected.size} file${selected.size !== 1 ? 's' : ''}`));
407
+ };
408
408
 
409
- return [...new Set(selectedFiles)]; // Remove duplicates
409
+ return new Promise((resolve) => {
410
+ renderList();
411
+
412
+ const onKeypress = (chunk, key) => {
413
+ if (!key) {
414
+ // Handle raw chunk for arrow keys
415
+ const str = chunk.toString();
416
+ if (str === '\x1b[A') key = { name: 'up' };
417
+ else if (str === '\x1b[B') key = { name: 'down' };
418
+ else if (str === '\x1b[C') key = { name: 'right' };
419
+ else if (str === '\x1b[D') key = { name: 'left' };
420
+ else if (str === ' ') key = { name: 'space' };
421
+ else if (str === '\r' || str === '\n') key = { name: 'return' };
422
+ else if (str === '\x1b' || str === 'q') key = { name: 'escape' };
423
+ else if (str === 'a' || str === 'A') key = { name: 'a' };
424
+ else if (str === '\x03') key = { name: 'c', ctrl: true }; // Ctrl+C
425
+ }
426
+
427
+ if (!key) return;
428
+
429
+ if (key.name === 'up' || key.name === 'k') {
430
+ cursor = cursor > 0 ? cursor - 1 : files.length - 1;
431
+ renderList();
432
+ } else if (key.name === 'down' || key.name === 'j') {
433
+ cursor = cursor < files.length - 1 ? cursor + 1 : 0;
434
+ renderList();
435
+ } else if (key.name === 'space' || key.name === 'right') {
436
+ if (selected.has(cursor)) {
437
+ selected.delete(cursor);
438
+ } else {
439
+ selected.add(cursor);
440
+ }
441
+ renderList();
442
+ } else if (key.name === 'a') {
443
+ // Toggle all
444
+ if (selected.size === files.length) {
445
+ selected.clear();
446
+ } else {
447
+ for (let i = 0; i < files.length; i++) selected.add(i);
448
+ }
449
+ renderList();
450
+ } else if (key.name === 'return') {
451
+ cleanup();
452
+ const selectedFiles = Array.from(selected).map(i => files[i].path);
453
+ console.log(chalk.green(`\nāœ“ Selected ${selectedFiles.length} file${selectedFiles.length !== 1 ? 's' : ''}`));
454
+ resolve(selectedFiles);
455
+ } else if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
456
+ cleanup();
457
+ console.log(chalk.gray('\nCancelled.'));
458
+ resolve([]);
459
+ }
460
+ };
461
+
462
+ const cleanup = () => {
463
+ process.stdin.removeListener('data', onKeypress);
464
+ if (process.stdin.isTTY) {
465
+ process.stdin.setRawMode(false);
466
+ }
467
+ };
468
+
469
+ process.stdin.on('data', onKeypress);
470
+ });
410
471
  }
411
472
 
412
473
  // Format scan results for AI context