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.
Files changed (73) hide show
  1. package/README.md +70 -4
  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/job-manager.js +2 -1
  19. package/dist/lib/platform-utils.js +211 -0
  20. package/dist/lib/secrets-manager.js +11 -1
  21. package/dist/lib/string-utils.js +128 -0
  22. package/dist/services/daemon/daemon-registrar.js +3 -2
  23. package/dist/services/secrets/secrets.js +154 -30
  24. package/package.json +10 -74
  25. package/dist/app.js +0 -33
  26. package/dist/cicd/analytics.js +0 -261
  27. package/dist/cicd/auth.js +0 -269
  28. package/dist/cicd/cache-manager.js +0 -172
  29. package/dist/cicd/data-retention.js +0 -305
  30. package/dist/cicd/performance-monitor.js +0 -224
  31. package/dist/cicd/webhook-receiver.js +0 -640
  32. package/dist/commands/api.js +0 -346
  33. package/dist/commands/theme.js +0 -261
  34. package/dist/commands/zsh-import.js +0 -240
  35. package/dist/components/App.js +0 -1
  36. package/dist/components/Divider.js +0 -29
  37. package/dist/components/REPL.js +0 -43
  38. package/dist/components/Terminal.js +0 -232
  39. package/dist/components/UserInput.js +0 -30
  40. package/dist/daemon/api-server.js +0 -316
  41. package/dist/daemon/monitoring-api.js +0 -220
  42. package/dist/lib/api-error-handler.js +0 -185
  43. package/dist/lib/associative-arrays.js +0 -285
  44. package/dist/lib/base-api-server.js +0 -290
  45. package/dist/lib/brace-expansion.js +0 -160
  46. package/dist/lib/builtin-commands.js +0 -439
  47. package/dist/lib/executors/builtin-executor.js +0 -52
  48. package/dist/lib/extended-globbing.js +0 -411
  49. package/dist/lib/extended-parameter-expansion.js +0 -227
  50. package/dist/lib/interactive-shell.js +0 -460
  51. package/dist/lib/job-builtins.js +0 -582
  52. package/dist/lib/pathname-expansion.js +0 -216
  53. package/dist/lib/script-runner.js +0 -226
  54. package/dist/lib/shell-executor.js +0 -2504
  55. package/dist/lib/shell-parser.js +0 -958
  56. package/dist/lib/shell-types.js +0 -6
  57. package/dist/lib/shell.lib.js +0 -40
  58. package/dist/lib/theme-manager.js +0 -476
  59. package/dist/lib/variable-expansion.js +0 -385
  60. package/dist/lib/zsh-compatibility.js +0 -659
  61. package/dist/lib/zsh-import-manager.js +0 -707
  62. package/dist/lib/zsh-options.js +0 -328
  63. package/dist/pipeline/job-tracker.js +0 -491
  64. package/dist/pipeline/mcli-bridge.js +0 -309
  65. package/dist/pipeline/pipeline-service.js +0 -1119
  66. package/dist/pipeline/workflow-engine.js +0 -870
  67. package/dist/services/api/api.js +0 -58
  68. package/dist/services/api/auth.js +0 -35
  69. package/dist/services/api/config.js +0 -7
  70. package/dist/services/api/file.js +0 -22
  71. package/dist/services/shell/shell.js +0 -28
  72. package/dist/services/zapier.js +0 -16
  73. 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
  });
@@ -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
- console.log(`\nšŸ“‹ Secrets in ${options.file}:\n`);
84
- for (const { key, value } of secrets) {
85
- 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) {
86
90
  console.log(` ${key}`);
87
91
  }
88
- else {
89
- // Mask the value but show first/last 3 chars if long enough
90
- let maskedValue = value;
91
- if (value.length > 8) {
92
- maskedValue = `${value.substring(0, 3)}${'*'.repeat(value.length - 6)}${value.substring(value.length - 3)}`;
93
- }
94
- else if (value.length > 0) {
95
- maskedValue = '*'.repeat(value.length);
96
- }
97
- console.log(` ${key}=${maskedValue}`);
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
- 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);
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
- if (options.export) {
310
- // Output in export format for shell evaluation
311
- for (const { key, value } of secrets) {
312
- // Escape single quotes in value and wrap in single quotes
313
- const escapedValue = value.replace(/'/g, "'\\''");
314
- console.log(`export ${key}='${escapedValue}'`);
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