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.
- package/README.md +40 -3
- package/dist/cli.js +104 -486
- package/dist/commands/doctor.js +427 -0
- package/dist/commands/init.js +371 -0
- package/dist/constants/api.js +94 -0
- package/dist/constants/commands.js +64 -0
- package/dist/constants/config.js +56 -0
- package/dist/constants/database.js +21 -0
- package/dist/constants/errors.js +79 -0
- package/dist/constants/index.js +28 -0
- package/dist/constants/paths.js +28 -0
- package/dist/constants/ui.js +73 -0
- package/dist/constants/validation.js +124 -0
- package/dist/daemon/lshd.js +11 -32
- package/dist/lib/daemon-client-helper.js +7 -4
- package/dist/lib/daemon-client.js +9 -2
- package/dist/lib/format-utils.js +163 -0
- package/dist/lib/fuzzy-match.js +123 -0
- package/dist/lib/job-manager.js +2 -1
- package/dist/lib/platform-utils.js +211 -0
- package/dist/lib/secrets-manager.js +11 -1
- package/dist/lib/string-utils.js +128 -0
- package/dist/services/daemon/daemon-registrar.js +3 -2
- package/dist/services/secrets/secrets.js +119 -59
- package/package.json +10 -74
- package/dist/app.js +0 -33
- package/dist/cicd/analytics.js +0 -261
- package/dist/cicd/auth.js +0 -269
- package/dist/cicd/cache-manager.js +0 -172
- package/dist/cicd/data-retention.js +0 -305
- package/dist/cicd/performance-monitor.js +0 -224
- package/dist/cicd/webhook-receiver.js +0 -640
- package/dist/commands/api.js +0 -346
- package/dist/commands/theme.js +0 -261
- package/dist/commands/zsh-import.js +0 -240
- package/dist/components/App.js +0 -1
- package/dist/components/Divider.js +0 -29
- package/dist/components/REPL.js +0 -43
- package/dist/components/Terminal.js +0 -232
- package/dist/components/UserInput.js +0 -30
- package/dist/daemon/api-server.js +0 -316
- package/dist/daemon/monitoring-api.js +0 -220
- package/dist/lib/api-error-handler.js +0 -185
- package/dist/lib/associative-arrays.js +0 -285
- package/dist/lib/base-api-server.js +0 -290
- package/dist/lib/brace-expansion.js +0 -160
- package/dist/lib/builtin-commands.js +0 -439
- package/dist/lib/executors/builtin-executor.js +0 -52
- package/dist/lib/extended-globbing.js +0 -411
- package/dist/lib/extended-parameter-expansion.js +0 -227
- package/dist/lib/interactive-shell.js +0 -460
- package/dist/lib/job-builtins.js +0 -582
- package/dist/lib/pathname-expansion.js +0 -216
- package/dist/lib/script-runner.js +0 -226
- package/dist/lib/shell-executor.js +0 -2504
- package/dist/lib/shell-parser.js +0 -958
- package/dist/lib/shell-types.js +0 -6
- package/dist/lib/shell.lib.js +0 -40
- package/dist/lib/theme-manager.js +0 -476
- package/dist/lib/variable-expansion.js +0 -385
- package/dist/lib/zsh-compatibility.js +0 -659
- package/dist/lib/zsh-import-manager.js +0 -707
- package/dist/lib/zsh-options.js +0 -328
- package/dist/pipeline/job-tracker.js +0 -491
- package/dist/pipeline/mcli-bridge.js +0 -309
- package/dist/pipeline/pipeline-service.js +0 -1119
- package/dist/pipeline/workflow-engine.js +0 -870
- package/dist/services/api/api.js +0 -58
- package/dist/services/api/auth.js +0 -35
- package/dist/services/api/config.js +0 -7
- package/dist/services/api/file.js +0 -22
- package/dist/services/shell/shell.js +0 -28
- package/dist/services/zapier.js +0 -16
- 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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
392
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|