lsh-framework 1.2.1 → 1.3.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.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Fuzzy matching utilities for secret keys
3
+ */
4
+ /**
5
+ * Normalize a string for fuzzy matching by removing spaces and special chars
6
+ */
7
+ function normalizeForMatching(str) {
8
+ // Remove spaces, hyphens, and convert to lowercase
9
+ return str.toLowerCase().replace(/[\s-]/g, '');
10
+ }
11
+ /**
12
+ * Calculate fuzzy match score between search string and key
13
+ * Returns a score where higher is better, or -1 if no match
14
+ *
15
+ * Supports space-separated searches:
16
+ * - "stripe api" matches "STRIPE_API_KEY"
17
+ * - "stripe secret" matches "STRIPE_SECRET_KEY"
18
+ */
19
+ export function calculateMatchScore(searchTerm, key) {
20
+ // Empty search term matches nothing
21
+ if (!searchTerm || searchTerm.trim() === '') {
22
+ return -1;
23
+ }
24
+ const searchLower = searchTerm.toLowerCase();
25
+ const keyLower = key.toLowerCase();
26
+ // Normalize both for space-insensitive matching
27
+ const searchNormalized = normalizeForMatching(searchTerm);
28
+ const keyNormalized = normalizeForMatching(key);
29
+ // Exact match (case sensitive) - highest priority
30
+ if (searchTerm === key) {
31
+ return 1100;
32
+ }
33
+ // Exact match (case insensitive)
34
+ if (searchLower === keyLower || searchNormalized === keyNormalized) {
35
+ return 1000;
36
+ }
37
+ // Key starts with search term (case sensitive)
38
+ if (key.startsWith(searchTerm)) {
39
+ return 950;
40
+ }
41
+ // Key starts with search term (case insensitive, normalized)
42
+ if (keyLower.startsWith(searchLower) || keyNormalized.startsWith(searchNormalized)) {
43
+ return 900;
44
+ }
45
+ // Check if search with spaces can match multiple words in key
46
+ // e.g., "stripe api" should match "STRIPE_API_KEY"
47
+ // Only do multi-word matching if user provided spaces (not underscores)
48
+ const hasSpaces = /\s/.test(searchTerm);
49
+ if (hasSpaces) {
50
+ const searchWords = searchTerm.toLowerCase().split(/[\s_-]+/).filter(w => w.length > 0);
51
+ const keyWords = key.toLowerCase().split(/[_-]/).filter(w => w.length > 0);
52
+ if (searchWords.length > 1) {
53
+ // Try to match all search words in order within key words
54
+ let matchCount = 0;
55
+ let lastMatchIndex = -1;
56
+ for (const searchWord of searchWords) {
57
+ let found = false;
58
+ for (let i = lastMatchIndex + 1; i < keyWords.length; i++) {
59
+ if (keyWords[i].startsWith(searchWord) || keyWords[i].includes(searchWord)) {
60
+ matchCount++;
61
+ lastMatchIndex = i;
62
+ found = true;
63
+ break;
64
+ }
65
+ }
66
+ if (!found) {
67
+ break; // If any word doesn't match, stop
68
+ }
69
+ }
70
+ // If all search words matched, score based on how many matched
71
+ if (matchCount === searchWords.length) {
72
+ // Perfect multi-word match - higher than substring matches
73
+ return 850 - (lastMatchIndex * 10); // Earlier matches score higher
74
+ }
75
+ else if (matchCount > 0) {
76
+ // Partial multi-word match
77
+ return 300 + (matchCount * 50);
78
+ }
79
+ }
80
+ }
81
+ // Key contains search term (case insensitive, normalized)
82
+ // Only apply this for single-word searches (no spaces)
83
+ if (!hasSpaces && (keyLower.includes(searchLower) || keyNormalized.includes(searchNormalized))) {
84
+ // Score based on position (earlier is better)
85
+ const position = keyNormalized.indexOf(searchNormalized);
86
+ const relativePosition = position / keyNormalized.length;
87
+ return 500 - (relativePosition * 100);
88
+ }
89
+ // Substring match with underscores/boundaries
90
+ // e.g., "stripe" matches "STRIPE_API_KEY" or "MY_STRIPE_SECRET"
91
+ const words = keyLower.split('_');
92
+ for (let i = 0; i < words.length; i++) {
93
+ if (words[i].startsWith(searchLower)) {
94
+ // Earlier words get higher scores
95
+ return 700 - (i * 50);
96
+ }
97
+ if (words[i].includes(searchLower)) {
98
+ return 400 - (i * 50);
99
+ }
100
+ }
101
+ // No match
102
+ return -1;
103
+ }
104
+ /**
105
+ * Find fuzzy matches for a search term in a list of key-value pairs
106
+ * Returns matches sorted by relevance (best match first)
107
+ */
108
+ export function findFuzzyMatches(searchTerm, secrets) {
109
+ const results = [];
110
+ for (const secret of secrets) {
111
+ const score = calculateMatchScore(searchTerm, secret.key);
112
+ if (score >= 0) {
113
+ results.push({
114
+ key: secret.key,
115
+ value: secret.value,
116
+ score,
117
+ });
118
+ }
119
+ }
120
+ // Sort by score (descending - best match first)
121
+ results.sort((a, b) => b.score - a.score);
122
+ return results;
123
+ }
@@ -404,6 +404,7 @@ API_KEY=
404
404
  .option('--all', 'Get all secrets from the file')
405
405
  .option('--export', 'Output in export format for shell evaluation (alias for --format export)')
406
406
  .option('--format <type>', 'Output format: env, json, yaml, toml, export', 'env')
407
+ .option('--exact', 'Require exact key match (disable fuzzy matching)')
407
408
  .action(async (key, options) => {
408
409
  try {
409
410
  const envPath = path.resolve(options.file);
@@ -413,24 +414,25 @@ API_KEY=
413
414
  }
414
415
  const content = fs.readFileSync(envPath, 'utf8');
415
416
  const lines = content.split('\n');
416
- // Handle --all flag
417
- if (options.all) {
418
- const secrets = [];
419
- for (const line of lines) {
420
- if (line.trim().startsWith('#') || !line.trim())
421
- continue;
422
- const match = line.match(/^([^=]+)=(.*)$/);
423
- if (match) {
424
- const key = match[1].trim();
425
- let value = match[2].trim();
426
- // Remove quotes if present
427
- if ((value.startsWith('"') && value.endsWith('"')) ||
428
- (value.startsWith("'") && value.endsWith("'"))) {
429
- value = value.slice(1, -1);
430
- }
431
- secrets.push({ key, value });
417
+ // Parse all secrets from file
418
+ const secrets = [];
419
+ for (const line of lines) {
420
+ if (line.trim().startsWith('#') || !line.trim())
421
+ continue;
422
+ const match = line.match(/^([^=]+)=(.*)$/);
423
+ if (match) {
424
+ const key = match[1].trim();
425
+ let value = match[2].trim();
426
+ // Remove quotes if present
427
+ if ((value.startsWith('"') && value.endsWith('"')) ||
428
+ (value.startsWith("'") && value.endsWith("'"))) {
429
+ value = value.slice(1, -1);
432
430
  }
431
+ secrets.push({ key, value });
433
432
  }
433
+ }
434
+ // Handle --all flag
435
+ if (options.all) {
434
436
  // Handle format output
435
437
  const format = options.export ? 'export' : options.format.toLowerCase();
436
438
  const validFormats = ['env', 'json', 'yaml', 'toml', 'export'];
@@ -451,22 +453,56 @@ API_KEY=
451
453
  console.error('❌ Please provide a key or use --all flag');
452
454
  process.exit(1);
453
455
  }
454
- for (const line of lines) {
455
- if (line.trim().startsWith('#') || !line.trim())
456
- continue;
457
- const match = line.match(/^([^=]+)=(.*)$/);
458
- if (match && match[1].trim() === key) {
459
- let value = match[2].trim();
460
- // Remove quotes if present
461
- if ((value.startsWith('"') && value.endsWith('"')) ||
462
- (value.startsWith("'") && value.endsWith("'"))) {
463
- value = value.slice(1, -1);
464
- }
465
- console.log(value);
456
+ // Try exact match first
457
+ const exactMatch = secrets.find(s => s.key === key);
458
+ if (exactMatch) {
459
+ console.log(exactMatch.value);
460
+ return;
461
+ }
462
+ // If exact match enabled, don't do fuzzy matching
463
+ if (options.exact) {
464
+ console.error(`❌ Key '${key}' not found in ${options.file}`);
465
+ process.exit(1);
466
+ }
467
+ // Use fuzzy matching
468
+ const { findFuzzyMatches } = await import('../../lib/fuzzy-match.js');
469
+ const matches = findFuzzyMatches(key, secrets);
470
+ if (matches.length === 0) {
471
+ console.error(`❌ No matches found for '${key}' in ${options.file}`);
472
+ console.error('💡 Tip: Use --exact flag for exact matching only');
473
+ process.exit(1);
474
+ }
475
+ // If single match, return it
476
+ if (matches.length === 1) {
477
+ console.log(matches[0].value);
478
+ return;
479
+ }
480
+ // If best match score is significantly higher than second best (clear winner)
481
+ // then auto-select it
482
+ if (matches.length > 1) {
483
+ const bestScore = matches[0].score;
484
+ const secondBestScore = matches[1].score;
485
+ // If best match scores 700+ and is at least 2x better than second best,
486
+ // consider it a clear match
487
+ if (bestScore >= 700 && bestScore >= secondBestScore * 2) {
488
+ console.log(matches[0].value);
466
489
  return;
467
490
  }
468
491
  }
469
- console.error(`❌ Key '${key}' not found in ${options.file}`);
492
+ // Multiple matches - show all matches for user to choose
493
+ console.error(`🔍 Found ${matches.length} matches for '${key}':\n`);
494
+ for (const match of matches) {
495
+ // Mask value for display
496
+ const maskedValue = match.value.length > 4
497
+ ? match.value.substring(0, 4) + '*'.repeat(Math.min(match.value.length - 4, 10))
498
+ : '****';
499
+ console.error(` ${match.key}=${maskedValue}`);
500
+ }
501
+ console.error('');
502
+ console.error('💡 Please specify the exact key name or use one of:');
503
+ for (const match of matches) {
504
+ console.error(` lsh get ${match.key}`);
505
+ }
470
506
  process.exit(1);
471
507
  }
472
508
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Simple, cross-platform encrypted secrets manager with automatic sync and multi-environment support. Just run lsh sync and start managing your secrets.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {