lsh-framework 1.1.0 ā 1.2.1
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 +70 -4
- 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/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 +154 -30
- 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
|
});
|
|
@@ -6,6 +6,7 @@ import SecretsManager from '../../lib/secrets-manager.js';
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as readline from 'readline';
|
|
9
|
+
import { getGitRepoInfo } from '../../lib/git-utils.js';
|
|
9
10
|
export async function init_secrets(program) {
|
|
10
11
|
// Push secrets to cloud
|
|
11
12
|
program
|
|
@@ -50,6 +51,8 @@ export async function init_secrets(program) {
|
|
|
50
51
|
.description('List secrets in the current local .env file')
|
|
51
52
|
.option('-f, --file <path>', 'Path to .env file', '.env')
|
|
52
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)')
|
|
53
56
|
.action(async (options) => {
|
|
54
57
|
try {
|
|
55
58
|
const envPath = path.resolve(options.file);
|
|
@@ -80,24 +83,38 @@ export async function init_secrets(program) {
|
|
|
80
83
|
console.log('No secrets found in .env file');
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
// Handle keys-only mode
|
|
87
|
+
if (options.keysOnly) {
|
|
88
|
+
console.log(`\nš Keys in ${options.file}:\n`);
|
|
89
|
+
for (const { key } of secrets) {
|
|
86
90
|
console.log(` ${key}`);
|
|
87
91
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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);
|
|
99
117
|
}
|
|
100
|
-
console.log(`\n Total: ${secrets.length} secrets\n`);
|
|
101
118
|
}
|
|
102
119
|
catch (error) {
|
|
103
120
|
const err = error;
|
|
@@ -110,6 +127,7 @@ export async function init_secrets(program) {
|
|
|
110
127
|
.command('env [environment]')
|
|
111
128
|
.description('List all stored environments or show secrets for specific environment')
|
|
112
129
|
.option('--all-files', 'List all tracked .env files across environments')
|
|
130
|
+
.option('--format <type>', 'Output format: env, json, yaml, toml, export', 'env')
|
|
113
131
|
.action(async (environment, options) => {
|
|
114
132
|
try {
|
|
115
133
|
const manager = new SecretsManager();
|
|
@@ -129,7 +147,14 @@ export async function init_secrets(program) {
|
|
|
129
147
|
}
|
|
130
148
|
// If environment specified, show secrets for that environment
|
|
131
149
|
if (environment) {
|
|
132
|
-
|
|
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);
|
|
133
158
|
return;
|
|
134
159
|
}
|
|
135
160
|
// Otherwise, list all environments
|
|
@@ -272,13 +297,113 @@ API_KEY=
|
|
|
272
297
|
process.exit(1);
|
|
273
298
|
}
|
|
274
299
|
});
|
|
300
|
+
// Info command - show relevant context information
|
|
301
|
+
program
|
|
302
|
+
.command('info')
|
|
303
|
+
.description('Show current directory context and tracked environment')
|
|
304
|
+
.option('-f, --file <path>', 'Path to .env file', '.env')
|
|
305
|
+
.option('-e, --env <name>', 'Environment name', 'dev')
|
|
306
|
+
.action(async (options) => {
|
|
307
|
+
try {
|
|
308
|
+
const gitInfo = getGitRepoInfo();
|
|
309
|
+
const manager = new SecretsManager();
|
|
310
|
+
const envPath = path.resolve(options.file);
|
|
311
|
+
console.log('\nš Current Directory Context\n');
|
|
312
|
+
// Git Repository Info
|
|
313
|
+
if (gitInfo.isGitRepo) {
|
|
314
|
+
console.log('š Git Repository:');
|
|
315
|
+
console.log(` Root: ${gitInfo.rootPath || 'unknown'}`);
|
|
316
|
+
console.log(` Name: ${gitInfo.repoName || 'unknown'}`);
|
|
317
|
+
if (gitInfo.currentBranch) {
|
|
318
|
+
console.log(` Branch: ${gitInfo.currentBranch}`);
|
|
319
|
+
}
|
|
320
|
+
if (gitInfo.remoteUrl) {
|
|
321
|
+
console.log(` Remote: ${gitInfo.remoteUrl}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.log('š Not in a git repository');
|
|
326
|
+
}
|
|
327
|
+
console.log('');
|
|
328
|
+
// Environment Tracking
|
|
329
|
+
console.log('š Environment Tracking:');
|
|
330
|
+
// Show the effective environment name used for cloud storage
|
|
331
|
+
const effectiveEnv = gitInfo.repoName
|
|
332
|
+
? `${gitInfo.repoName}_${options.env}`
|
|
333
|
+
: options.env;
|
|
334
|
+
console.log(` Base environment: ${options.env}`);
|
|
335
|
+
console.log(` Cloud storage name: ${effectiveEnv}`);
|
|
336
|
+
if (gitInfo.repoName) {
|
|
337
|
+
console.log(` Namespace: ${gitInfo.repoName}`);
|
|
338
|
+
console.log(' ā¹ļø Repo-based isolation enabled');
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.log(' Namespace: (none - not in git repo)');
|
|
342
|
+
console.log(' ā ļø No isolation - shared environment name');
|
|
343
|
+
}
|
|
344
|
+
console.log('');
|
|
345
|
+
// Local File Status
|
|
346
|
+
console.log('š Local .env File:');
|
|
347
|
+
if (fs.existsSync(envPath)) {
|
|
348
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
349
|
+
const lines = content.split('\n').filter(line => {
|
|
350
|
+
const trimmed = line.trim();
|
|
351
|
+
return trimmed && !trimmed.startsWith('#') && trimmed.includes('=');
|
|
352
|
+
});
|
|
353
|
+
console.log(` Path: ${envPath}`);
|
|
354
|
+
console.log(` Keys: ${lines.length}`);
|
|
355
|
+
console.log(` Size: ${Math.round(content.length / 1024 * 10) / 10} KB`);
|
|
356
|
+
// Check for encryption key
|
|
357
|
+
const hasKey = content.includes('LSH_SECRETS_KEY=');
|
|
358
|
+
console.log(` Has encryption key: ${hasKey ? 'ā
' : 'ā'}`);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.log(` Path: ${envPath}`);
|
|
362
|
+
console.log(' Status: ā Not found');
|
|
363
|
+
}
|
|
364
|
+
console.log('');
|
|
365
|
+
// Cloud Status
|
|
366
|
+
console.log('āļø Cloud Storage:');
|
|
367
|
+
try {
|
|
368
|
+
const status = await manager.status(options.file, options.env);
|
|
369
|
+
if (status.cloudExists) {
|
|
370
|
+
console.log(` Environment: ${effectiveEnv}`);
|
|
371
|
+
console.log(` Keys stored: ${status.cloudKeys}`);
|
|
372
|
+
console.log(` Last updated: ${status.cloudModified ? new Date(status.cloudModified).toLocaleString() : 'unknown'}`);
|
|
373
|
+
if (status.keyMatches !== undefined) {
|
|
374
|
+
console.log(` Key matches: ${status.keyMatches ? 'ā
' : 'ā MISMATCH'}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
console.log(` Environment: ${effectiveEnv}`);
|
|
379
|
+
console.log(' Status: ā Not synced yet');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (_error) {
|
|
383
|
+
console.log(' Status: ā ļø Unable to check (Supabase not configured)');
|
|
384
|
+
}
|
|
385
|
+
console.log('');
|
|
386
|
+
// Quick Actions
|
|
387
|
+
console.log('š” Quick Actions:');
|
|
388
|
+
console.log(` Push: lsh push --env ${options.env}`);
|
|
389
|
+
console.log(` Pull: lsh pull --env ${options.env}`);
|
|
390
|
+
console.log(` Sync: lsh sync --env ${options.env}`);
|
|
391
|
+
console.log('');
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
const err = error;
|
|
395
|
+
console.error('ā Failed to get info:', err.message);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
275
399
|
// Get a specific secret value
|
|
276
400
|
program
|
|
277
401
|
.command('get [key]')
|
|
278
402
|
.description('Get a specific secret value from .env file, or all secrets with --all')
|
|
279
403
|
.option('-f, --file <path>', 'Path to .env file', '.env')
|
|
280
404
|
.option('--all', 'Get all secrets from the file')
|
|
281
|
-
.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')
|
|
282
407
|
.action(async (key, options) => {
|
|
283
408
|
try {
|
|
284
409
|
const envPath = path.resolve(options.file);
|
|
@@ -306,20 +431,19 @@ API_KEY=
|
|
|
306
431
|
secrets.push({ key, value });
|
|
307
432
|
}
|
|
308
433
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
// Output in KEY=VALUE format
|
|
319
|
-
for (const { key, value } of secrets) {
|
|
320
|
-
console.log(`${key}=${value}`);
|
|
321
|
-
}
|
|
434
|
+
// Handle format output
|
|
435
|
+
const format = options.export ? 'export' : options.format.toLowerCase();
|
|
436
|
+
const validFormats = ['env', 'json', 'yaml', 'toml', 'export'];
|
|
437
|
+
if (!validFormats.includes(format)) {
|
|
438
|
+
console.error(`ā Invalid format: ${format}`);
|
|
439
|
+
console.log(`Valid formats: ${validFormats.join(', ')}`);
|
|
440
|
+
process.exit(1);
|
|
322
441
|
}
|
|
442
|
+
// Import format utilities dynamically
|
|
443
|
+
const { formatSecrets } = await import('../../lib/format-utils.js');
|
|
444
|
+
// For get --all, always show full values (no masking)
|
|
445
|
+
const output = formatSecrets(secrets, format, false);
|
|
446
|
+
console.log(output);
|
|
323
447
|
return;
|
|
324
448
|
}
|
|
325
449
|
// Handle single key lookup
|