lsh-framework 1.2.0 → 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.
Files changed (74) hide show
  1. package/README.md +40 -3
  2. package/dist/cli.js +104 -486
  3. package/dist/commands/doctor.js +427 -0
  4. package/dist/commands/init.js +371 -0
  5. package/dist/constants/api.js +94 -0
  6. package/dist/constants/commands.js +64 -0
  7. package/dist/constants/config.js +56 -0
  8. package/dist/constants/database.js +21 -0
  9. package/dist/constants/errors.js +79 -0
  10. package/dist/constants/index.js +28 -0
  11. package/dist/constants/paths.js +28 -0
  12. package/dist/constants/ui.js +73 -0
  13. package/dist/constants/validation.js +124 -0
  14. package/dist/daemon/lshd.js +11 -32
  15. package/dist/lib/daemon-client-helper.js +7 -4
  16. package/dist/lib/daemon-client.js +9 -2
  17. package/dist/lib/format-utils.js +163 -0
  18. package/dist/lib/fuzzy-match.js +123 -0
  19. package/dist/lib/job-manager.js +2 -1
  20. package/dist/lib/platform-utils.js +211 -0
  21. package/dist/lib/secrets-manager.js +11 -1
  22. package/dist/lib/string-utils.js +128 -0
  23. package/dist/services/daemon/daemon-registrar.js +3 -2
  24. package/dist/services/secrets/secrets.js +119 -59
  25. package/package.json +10 -74
  26. package/dist/app.js +0 -33
  27. package/dist/cicd/analytics.js +0 -261
  28. package/dist/cicd/auth.js +0 -269
  29. package/dist/cicd/cache-manager.js +0 -172
  30. package/dist/cicd/data-retention.js +0 -305
  31. package/dist/cicd/performance-monitor.js +0 -224
  32. package/dist/cicd/webhook-receiver.js +0 -640
  33. package/dist/commands/api.js +0 -346
  34. package/dist/commands/theme.js +0 -261
  35. package/dist/commands/zsh-import.js +0 -240
  36. package/dist/components/App.js +0 -1
  37. package/dist/components/Divider.js +0 -29
  38. package/dist/components/REPL.js +0 -43
  39. package/dist/components/Terminal.js +0 -232
  40. package/dist/components/UserInput.js +0 -30
  41. package/dist/daemon/api-server.js +0 -316
  42. package/dist/daemon/monitoring-api.js +0 -220
  43. package/dist/lib/api-error-handler.js +0 -185
  44. package/dist/lib/associative-arrays.js +0 -285
  45. package/dist/lib/base-api-server.js +0 -290
  46. package/dist/lib/brace-expansion.js +0 -160
  47. package/dist/lib/builtin-commands.js +0 -439
  48. package/dist/lib/executors/builtin-executor.js +0 -52
  49. package/dist/lib/extended-globbing.js +0 -411
  50. package/dist/lib/extended-parameter-expansion.js +0 -227
  51. package/dist/lib/interactive-shell.js +0 -460
  52. package/dist/lib/job-builtins.js +0 -582
  53. package/dist/lib/pathname-expansion.js +0 -216
  54. package/dist/lib/script-runner.js +0 -226
  55. package/dist/lib/shell-executor.js +0 -2504
  56. package/dist/lib/shell-parser.js +0 -958
  57. package/dist/lib/shell-types.js +0 -6
  58. package/dist/lib/shell.lib.js +0 -40
  59. package/dist/lib/theme-manager.js +0 -476
  60. package/dist/lib/variable-expansion.js +0 -385
  61. package/dist/lib/zsh-compatibility.js +0 -659
  62. package/dist/lib/zsh-import-manager.js +0 -707
  63. package/dist/lib/zsh-options.js +0 -328
  64. package/dist/pipeline/job-tracker.js +0 -491
  65. package/dist/pipeline/mcli-bridge.js +0 -309
  66. package/dist/pipeline/pipeline-service.js +0 -1119
  67. package/dist/pipeline/workflow-engine.js +0 -870
  68. package/dist/services/api/api.js +0 -58
  69. package/dist/services/api/auth.js +0 -35
  70. package/dist/services/api/config.js +0 -7
  71. package/dist/services/api/file.js +0 -22
  72. package/dist/services/shell/shell.js +0 -28
  73. package/dist/services/zapier.js +0 -16
  74. package/dist/simple-api-server.js +0 -148
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Platform Utilities
3
+ * Cross-platform path and environment handling for Windows, macOS, and Linux
4
+ */
5
+ import * as path from 'path';
6
+ import * as os from 'os';
7
+ /**
8
+ * Get platform-specific paths
9
+ * Handles differences between Windows, macOS, and Linux
10
+ */
11
+ export function getPlatformPaths(appName = 'lsh') {
12
+ const isWindows = process.platform === 'win32';
13
+ const isMac = process.platform === 'darwin';
14
+ // Temporary directory
15
+ const tmpDir = os.tmpdir();
16
+ // Home directory
17
+ const homeDir = os.homedir();
18
+ // Username
19
+ const user = os.userInfo().username;
20
+ // Application-specific temporary file paths
21
+ const pidFile = path.join(tmpDir, `${appName}-daemon-${user}.pid`);
22
+ const logFile = path.join(tmpDir, `${appName}-daemon-${user}.log`);
23
+ // Socket/IPC path (platform-specific)
24
+ // Windows uses Named Pipes, Unix systems use Unix Domain Sockets
25
+ const socketPath = isWindows
26
+ ? `\\\\.\\pipe\\${appName}-daemon-${user}`
27
+ : path.join(tmpDir, `${appName}-daemon-${user}.sock`);
28
+ // Configuration directory
29
+ // Windows: %APPDATA%\lsh
30
+ // macOS: ~/Library/Application Support/lsh
31
+ // Linux: ~/.config/lsh
32
+ let configDir;
33
+ if (isWindows) {
34
+ configDir = path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), appName);
35
+ }
36
+ else if (isMac) {
37
+ configDir = path.join(homeDir, 'Library', 'Application Support', appName);
38
+ }
39
+ else {
40
+ configDir = path.join(homeDir, '.config', appName);
41
+ }
42
+ // Data directory
43
+ // Windows: %LOCALAPPDATA%\lsh
44
+ // macOS: ~/Library/Application Support/lsh
45
+ // Linux: ~/.local/share/lsh
46
+ let dataDir;
47
+ if (isWindows) {
48
+ dataDir = path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), appName);
49
+ }
50
+ else if (isMac) {
51
+ dataDir = path.join(homeDir, 'Library', 'Application Support', appName);
52
+ }
53
+ else {
54
+ dataDir = path.join(homeDir, '.local', 'share', appName);
55
+ }
56
+ return {
57
+ tmpDir,
58
+ homeDir,
59
+ user,
60
+ pidFile,
61
+ logFile,
62
+ socketPath,
63
+ configDir,
64
+ dataDir,
65
+ };
66
+ }
67
+ /**
68
+ * Normalize path for current platform
69
+ */
70
+ export function normalizePath(inputPath) {
71
+ return path.normalize(inputPath);
72
+ }
73
+ /**
74
+ * Check if running on Windows
75
+ */
76
+ export function isWindows() {
77
+ return process.platform === 'win32';
78
+ }
79
+ /**
80
+ * Check if running on macOS
81
+ */
82
+ export function isMacOS() {
83
+ return process.platform === 'darwin';
84
+ }
85
+ /**
86
+ * Check if running on Linux
87
+ */
88
+ export function isLinux() {
89
+ return process.platform === 'linux';
90
+ }
91
+ /**
92
+ * Get platform name
93
+ */
94
+ export function getPlatformName() {
95
+ const platform = process.platform;
96
+ switch (platform) {
97
+ case 'win32':
98
+ return 'Windows';
99
+ case 'darwin':
100
+ return 'macOS';
101
+ case 'linux':
102
+ return 'Linux';
103
+ default:
104
+ return platform;
105
+ }
106
+ }
107
+ /**
108
+ * Get environment variable with fallback
109
+ * Handles Windows vs Unix differences (e.g., HOME vs USERPROFILE)
110
+ */
111
+ export function getEnvVar(unixVar, windowsVar) {
112
+ if (isWindows() && windowsVar) {
113
+ return process.env[windowsVar] || process.env[unixVar];
114
+ }
115
+ return process.env[unixVar];
116
+ }
117
+ /**
118
+ * Ensure directory exists, create if needed (cross-platform)
119
+ */
120
+ export async function ensureDir(dirPath) {
121
+ const fs = await import('fs/promises');
122
+ try {
123
+ await fs.mkdir(dirPath, { recursive: true });
124
+ }
125
+ catch (error) {
126
+ // Ignore if directory already exists
127
+ if (error.code !== 'EEXIST') {
128
+ throw error;
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * Get shell executable path for current platform
134
+ */
135
+ export function getDefaultShell() {
136
+ if (isWindows()) {
137
+ return process.env.COMSPEC || 'cmd.exe';
138
+ }
139
+ return process.env.SHELL || '/bin/sh';
140
+ }
141
+ /**
142
+ * Get path separator for current platform
143
+ */
144
+ export function getPathSeparator() {
145
+ return path.delimiter; // : on Unix, ; on Windows
146
+ }
147
+ /**
148
+ * Join paths with platform-appropriate separator
149
+ */
150
+ export function joinPaths(...paths) {
151
+ return path.join(...paths);
152
+ }
153
+ /**
154
+ * Convert Unix-style path to platform path
155
+ */
156
+ export function toPlatformPath(unixPath) {
157
+ if (isWindows()) {
158
+ return unixPath.replace(/\//g, '\\');
159
+ }
160
+ return unixPath;
161
+ }
162
+ /**
163
+ * Convert platform path to Unix-style path
164
+ */
165
+ export function toUnixPath(platformPath) {
166
+ return platformPath.replace(/\\/g, '/');
167
+ }
168
+ /**
169
+ * Get executable extension for current platform
170
+ */
171
+ export function getExecutableExtension() {
172
+ return isWindows() ? '.exe' : '';
173
+ }
174
+ /**
175
+ * Check if a path is absolute (cross-platform)
176
+ */
177
+ export function isAbsolutePath(inputPath) {
178
+ return path.isAbsolute(inputPath);
179
+ }
180
+ /**
181
+ * Resolve path relative to home directory
182
+ */
183
+ export function resolveHomePath(relativePath) {
184
+ if (relativePath.startsWith('~')) {
185
+ const homeDir = os.homedir();
186
+ return path.join(homeDir, relativePath.slice(1));
187
+ }
188
+ return relativePath;
189
+ }
190
+ /**
191
+ * Get platform-specific line ending
192
+ */
193
+ export function getLineEnding() {
194
+ return isWindows() ? '\r\n' : '\n';
195
+ }
196
+ /**
197
+ * Get comprehensive platform information
198
+ */
199
+ export function getPlatformInfo() {
200
+ const paths = getPlatformPaths();
201
+ return {
202
+ platform: process.platform,
203
+ platformName: getPlatformName(),
204
+ arch: process.arch,
205
+ release: os.release(),
206
+ nodeVersion: process.version,
207
+ homeDir: paths.homeDir,
208
+ tmpDir: paths.tmpDir,
209
+ user: paths.user,
210
+ };
211
+ }
@@ -340,7 +340,7 @@ export class SecretsManager {
340
340
  /**
341
341
  * Show secrets (masked)
342
342
  */
343
- async show(environment = 'dev') {
343
+ async show(environment = 'dev', format = 'env') {
344
344
  const jobs = await this.persistence.getActiveJobs();
345
345
  const secretsJobs = jobs
346
346
  .filter(j => j.command === 'secrets_sync' && j.job_id.includes(environment))
@@ -355,6 +355,16 @@ export class SecretsManager {
355
355
  }
356
356
  const decrypted = this.decrypt(latestSecret.output);
357
357
  const env = this.parseEnvFile(decrypted);
358
+ // Convert to array format for formatSecrets
359
+ const secrets = Object.entries(env).map(([key, value]) => ({ key, value }));
360
+ // Use format utilities if not default env format
361
+ if (format !== 'env') {
362
+ const { formatSecrets } = await import('./format-utils.js');
363
+ const output = formatSecrets(secrets, format, false); // No masking for structured formats
364
+ console.log(output);
365
+ return;
366
+ }
367
+ // Default env format with masking (legacy behavior)
358
368
  console.log(`\n📦 Secrets for ${environment} (${Object.keys(env).length} total):\n`);
359
369
  for (const [key, value] of Object.entries(env)) {
360
370
  const masked = value.length > 4
@@ -0,0 +1,128 @@
1
+ /**
2
+ * String Utilities
3
+ *
4
+ * Helper functions for working with strings, especially for formatting
5
+ * constant template strings with dynamic values.
6
+ */
7
+ /**
8
+ * Format a template string by replacing ${varName} placeholders with values
9
+ *
10
+ * @param template - Template string with ${varName} placeholders
11
+ * @param vars - Object mapping variable names to values
12
+ * @returns Formatted string
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { ERRORS } from '../constants/index.js';
17
+ * import { formatMessage } from './string-utils.js';
18
+ *
19
+ * const error = formatMessage(ERRORS.JOB_NOT_FOUND, { jobId: '12345' });
20
+ * // Returns: "Job 12345 not found"
21
+ * ```
22
+ */
23
+ export function formatMessage(template, vars) {
24
+ let result = template;
25
+ // Sort keys by length (longest first) to prevent overlapping variable name issues
26
+ // Example: if we have both 'id' and 'jobId', we want to replace 'jobId' first
27
+ const sortedEntries = Object.entries(vars).sort((a, b) => b[0].length - a[0].length);
28
+ for (const [key, value] of sortedEntries) {
29
+ const placeholder = `\${${key}}`;
30
+ // Use replaceAll with literal string to avoid regex escaping complexity
31
+ result = result.replaceAll(placeholder, String(value));
32
+ }
33
+ return result;
34
+ }
35
+ /**
36
+ * Format a path template by replacing ${VAR} placeholders with environment variables
37
+ *
38
+ * @param pathTemplate - Path template with ${VAR} placeholders
39
+ * @param fallbacks - Optional fallback values for variables
40
+ * @returns Formatted path
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * import { PATHS } from '../constants/index.js';
45
+ * import { formatPath } from './string-utils.js';
46
+ *
47
+ * const socketPath = formatPath(PATHS.DAEMON_SOCKET_TEMPLATE, { USER: 'default' });
48
+ * // Returns: "/tmp/lsh-job-daemon-johndoe.sock" (if USER env var is johndoe)
49
+ * // Or: "/tmp/lsh-job-daemon-default.sock" (if USER env var is not set)
50
+ * ```
51
+ */
52
+ export function formatPath(pathTemplate, fallbacks = {}) {
53
+ let result = pathTemplate;
54
+ const pattern = /\$\{([^}]+)\}/g;
55
+ const matches = Array.from(pathTemplate.matchAll(pattern));
56
+ // Process matches to collect unique variable names and their values
57
+ const replacements = new Map();
58
+ for (const match of matches) {
59
+ const varName = match[1];
60
+ if (!replacements.has(match[0])) {
61
+ const value = process.env[varName] || fallbacks[varName] || '';
62
+ replacements.set(match[0], value);
63
+ }
64
+ }
65
+ // Replace all occurrences of each placeholder
66
+ for (const [placeholder, value] of replacements) {
67
+ result = result.replaceAll(placeholder, value);
68
+ }
69
+ return result;
70
+ }
71
+ /**
72
+ * Truncate a string to a maximum length, adding ellipsis if needed
73
+ *
74
+ * @param str - String to truncate
75
+ * @param maxLength - Maximum length (default: 50)
76
+ * @param ellipsis - Ellipsis to append (default: '...')
77
+ * @returns Truncated string
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * truncate('This is a very long error message', 20);
82
+ * // Returns: "This is a very lo..."
83
+ * ```
84
+ */
85
+ export function truncate(str, maxLength = 50, ellipsis = '...') {
86
+ // Validate that maxLength is greater than ellipsis length
87
+ if (maxLength < ellipsis.length) {
88
+ throw new Error(`maxLength (${maxLength}) must be greater than or equal to ellipsis length (${ellipsis.length})`);
89
+ }
90
+ if (str.length <= maxLength) {
91
+ return str;
92
+ }
93
+ return str.substring(0, maxLength - ellipsis.length) + ellipsis;
94
+ }
95
+ /**
96
+ * Escape special characters in a string for use in a regular expression
97
+ *
98
+ * @param str - String to escape
99
+ * @returns Escaped string
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * escapeRegex('test.file');
104
+ * // Returns: "test\\.file"
105
+ * ```
106
+ */
107
+ export function escapeRegex(str) {
108
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
109
+ }
110
+ /**
111
+ * Pluralize a word based on count
112
+ *
113
+ * @param count - Count to check
114
+ * @param singular - Singular form
115
+ * @param plural - Plural form (default: singular + 's')
116
+ * @returns Pluralized string with count
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * pluralize(1, 'job'); // "1 job"
121
+ * pluralize(5, 'job'); // "5 jobs"
122
+ * pluralize(2, 'query', 'queries'); // "2 queries"
123
+ * ```
124
+ */
125
+ export function pluralize(count, singular, plural) {
126
+ const pluralForm = plural || `${singular}s`;
127
+ return `${count} ${count === 1 ? singular : pluralForm}`;
128
+ }
@@ -6,6 +6,7 @@ import { BaseCommandRegistrar } from '../../lib/base-command-registrar.js';
6
6
  import * as fs from 'fs';
7
7
  import { exec } from 'child_process';
8
8
  import { promisify } from 'util';
9
+ import { getPlatformPaths } from '../../lib/platform-utils.js';
9
10
  const execAsync = promisify(exec);
10
11
  export class DaemonCommandRegistrar extends BaseCommandRegistrar {
11
12
  constructor() {
@@ -43,8 +44,8 @@ export class DaemonCommandRegistrar extends BaseCommandRegistrar {
43
44
  description: 'Start the daemon',
44
45
  action: async () => {
45
46
  const { spawn } = await import('child_process');
46
- const socketPath = `/tmp/lsh-job-daemon-${process.env.USER || 'user'}.sock`;
47
- const daemonProcess = spawn('node', ['dist/daemon/lshd.js', 'start', socketPath], {
47
+ const platformPaths = getPlatformPaths('lsh');
48
+ const daemonProcess = spawn('node', ['dist/daemon/lshd.js', 'start', platformPaths.socketPath], {
48
49
  detached: true,
49
50
  stdio: 'ignore'
50
51
  });
@@ -51,6 +51,8 @@ export async function init_secrets(program) {
51
51
  .description('List secrets in the current local .env file')
52
52
  .option('-f, --file <path>', 'Path to .env file', '.env')
53
53
  .option('--keys-only', 'Show only keys, not values')
54
+ .option('--format <type>', 'Output format: env, json, yaml, toml, export', 'env')
55
+ .option('--no-mask', 'Show full values (default: auto based on format)')
54
56
  .action(async (options) => {
55
57
  try {
56
58
  const envPath = path.resolve(options.file);
@@ -81,24 +83,38 @@ export async function init_secrets(program) {
81
83
  console.log('No secrets found in .env file');
82
84
  return;
83
85
  }
84
- console.log(`\n📋 Secrets in ${options.file}:\n`);
85
- for (const { key, value } of secrets) {
86
- if (options.keysOnly) {
86
+ // Handle keys-only mode
87
+ if (options.keysOnly) {
88
+ console.log(`\n📋 Keys in ${options.file}:\n`);
89
+ for (const { key } of secrets) {
87
90
  console.log(` ${key}`);
88
91
  }
89
- else {
90
- // Mask the value but show first/last 3 chars if long enough
91
- let maskedValue = value;
92
- if (value.length > 8) {
93
- maskedValue = `${value.substring(0, 3)}${'*'.repeat(value.length - 6)}${value.substring(value.length - 3)}`;
94
- }
95
- else if (value.length > 0) {
96
- maskedValue = '*'.repeat(value.length);
97
- }
98
- console.log(` ${key}=${maskedValue}`);
99
- }
92
+ console.log(`\n Total: ${secrets.length} keys\n`);
93
+ return;
94
+ }
95
+ // Handle format output
96
+ const format = options.format.toLowerCase();
97
+ const validFormats = ['env', 'json', 'yaml', 'toml', 'export'];
98
+ if (!validFormats.includes(format)) {
99
+ console.error(`❌ Invalid format: ${format}`);
100
+ console.log(`Valid formats: ${validFormats.join(', ')}`);
101
+ process.exit(1);
102
+ }
103
+ // Import format utilities dynamically
104
+ const { formatSecrets } = await import('../../lib/format-utils.js');
105
+ // Determine masking behavior
106
+ const shouldMask = options.mask !== false ? undefined : false;
107
+ const output = formatSecrets(secrets, format, shouldMask);
108
+ // Only show header for default env format
109
+ if (format === 'env') {
110
+ console.log(`\n📋 Secrets in ${options.file}:\n`);
111
+ console.log(output);
112
+ console.log(`\n Total: ${secrets.length} secrets\n`);
113
+ }
114
+ else {
115
+ // For structured formats, output directly (no decoration)
116
+ console.log(output);
100
117
  }
101
- console.log(`\n Total: ${secrets.length} secrets\n`);
102
118
  }
103
119
  catch (error) {
104
120
  const err = error;
@@ -111,6 +127,7 @@ export async function init_secrets(program) {
111
127
  .command('env [environment]')
112
128
  .description('List all stored environments or show secrets for specific environment')
113
129
  .option('--all-files', 'List all tracked .env files across environments')
130
+ .option('--format <type>', 'Output format: env, json, yaml, toml, export', 'env')
114
131
  .action(async (environment, options) => {
115
132
  try {
116
133
  const manager = new SecretsManager();
@@ -130,7 +147,14 @@ export async function init_secrets(program) {
130
147
  }
131
148
  // If environment specified, show secrets for that environment
132
149
  if (environment) {
133
- await manager.show(environment);
150
+ const format = options.format.toLowerCase();
151
+ const validFormats = ['env', 'json', 'yaml', 'toml', 'export'];
152
+ if (!validFormats.includes(format)) {
153
+ console.error(`❌ Invalid format: ${format}`);
154
+ console.log(`Valid formats: ${validFormats.join(', ')}`);
155
+ process.exit(1);
156
+ }
157
+ await manager.show(environment, format);
134
158
  return;
135
159
  }
136
160
  // Otherwise, list all environments
@@ -378,7 +402,9 @@ API_KEY=
378
402
  .description('Get a specific secret value from .env file, or all secrets with --all')
379
403
  .option('-f, --file <path>', 'Path to .env file', '.env')
380
404
  .option('--all', 'Get all secrets from the file')
381
- .option('--export', 'Output in export format for shell evaluation')
405
+ .option('--export', 'Output in export format for shell evaluation (alias for --format export)')
406
+ .option('--format <type>', 'Output format: env, json, yaml, toml, export', 'env')
407
+ .option('--exact', 'Require exact key match (disable fuzzy matching)')
382
408
  .action(async (key, options) => {
383
409
  try {
384
410
  const envPath = path.resolve(options.file);
@@ -388,61 +414,95 @@ API_KEY=
388
414
  }
389
415
  const content = fs.readFileSync(envPath, 'utf8');
390
416
  const lines = content.split('\n');
391
- // Handle --all flag
392
- if (options.all) {
393
- const secrets = [];
394
- for (const line of lines) {
395
- if (line.trim().startsWith('#') || !line.trim())
396
- continue;
397
- const match = line.match(/^([^=]+)=(.*)$/);
398
- if (match) {
399
- const key = match[1].trim();
400
- let value = match[2].trim();
401
- // Remove quotes if present
402
- if ((value.startsWith('"') && value.endsWith('"')) ||
403
- (value.startsWith("'") && value.endsWith("'"))) {
404
- value = value.slice(1, -1);
405
- }
406
- secrets.push({ key, value });
407
- }
408
- }
409
- if (options.export) {
410
- // Output in export format for shell evaluation
411
- for (const { key, value } of secrets) {
412
- // Escape single quotes in value and wrap in single quotes
413
- const escapedValue = value.replace(/'/g, "'\\''");
414
- console.log(`export ${key}='${escapedValue}'`);
415
- }
416
- }
417
- else {
418
- // Output in KEY=VALUE format
419
- for (const { key, value } of secrets) {
420
- console.log(`${key}=${value}`);
421
- }
422
- }
423
- return;
424
- }
425
- // Handle single key lookup
426
- if (!key) {
427
- console.error('❌ Please provide a key or use --all flag');
428
- process.exit(1);
429
- }
417
+ // Parse all secrets from file
418
+ const secrets = [];
430
419
  for (const line of lines) {
431
420
  if (line.trim().startsWith('#') || !line.trim())
432
421
  continue;
433
422
  const match = line.match(/^([^=]+)=(.*)$/);
434
- if (match && match[1].trim() === key) {
423
+ if (match) {
424
+ const key = match[1].trim();
435
425
  let value = match[2].trim();
436
426
  // Remove quotes if present
437
427
  if ((value.startsWith('"') && value.endsWith('"')) ||
438
428
  (value.startsWith("'") && value.endsWith("'"))) {
439
429
  value = value.slice(1, -1);
440
430
  }
441
- console.log(value);
431
+ secrets.push({ key, value });
432
+ }
433
+ }
434
+ // Handle --all flag
435
+ if (options.all) {
436
+ // Handle format output
437
+ const format = options.export ? 'export' : options.format.toLowerCase();
438
+ const validFormats = ['env', 'json', 'yaml', 'toml', 'export'];
439
+ if (!validFormats.includes(format)) {
440
+ console.error(`❌ Invalid format: ${format}`);
441
+ console.log(`Valid formats: ${validFormats.join(', ')}`);
442
+ process.exit(1);
443
+ }
444
+ // Import format utilities dynamically
445
+ const { formatSecrets } = await import('../../lib/format-utils.js');
446
+ // For get --all, always show full values (no masking)
447
+ const output = formatSecrets(secrets, format, false);
448
+ console.log(output);
449
+ return;
450
+ }
451
+ // Handle single key lookup
452
+ if (!key) {
453
+ console.error('❌ Please provide a key or use --all flag');
454
+ process.exit(1);
455
+ }
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);
442
489
  return;
443
490
  }
444
491
  }
445
- 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
+ }
446
506
  process.exit(1);
447
507
  }
448
508
  catch (error) {