lsh-framework 1.2.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 +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/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 +54 -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,427 @@
1
+ /**
2
+ * LSH Doctor Command
3
+ * Health check and troubleshooting utility
4
+ */
5
+ import chalk from 'chalk';
6
+ import * as fs from 'fs/promises';
7
+ import * as path from 'path';
8
+ import { createClient } from '@supabase/supabase-js';
9
+ import { getPlatformPaths, getPlatformInfo } from '../lib/platform-utils.js';
10
+ /**
11
+ * Register doctor commands
12
+ */
13
+ export function registerDoctorCommands(program) {
14
+ program
15
+ .command('doctor')
16
+ .description('Health check and troubleshooting')
17
+ .option('-v, --verbose', 'Show detailed information')
18
+ .option('--json', 'Output results as JSON')
19
+ .action(async (options) => {
20
+ try {
21
+ await runHealthCheck(options);
22
+ }
23
+ catch (error) {
24
+ const err = error;
25
+ console.error(chalk.red('\n❌ Health check failed:'), err.message);
26
+ process.exit(1);
27
+ }
28
+ });
29
+ }
30
+ /**
31
+ * Run comprehensive health check
32
+ */
33
+ async function runHealthCheck(options) {
34
+ if (!options.json) {
35
+ console.log(chalk.bold.cyan('\n🏥 LSH Health Check'));
36
+ console.log(chalk.gray('━'.repeat(50)));
37
+ console.log('');
38
+ }
39
+ const checks = [];
40
+ // Platform check
41
+ checks.push(await checkPlatform(options.verbose));
42
+ // .env file check
43
+ checks.push(await checkEnvFile(options.verbose));
44
+ // Encryption key check
45
+ checks.push(await checkEncryptionKey(options.verbose));
46
+ // Storage backend check
47
+ const storageChecks = await checkStorageBackend(options.verbose);
48
+ checks.push(...storageChecks);
49
+ // Git repository check
50
+ checks.push(await checkGitRepository(options.verbose));
51
+ // Permissions check
52
+ checks.push(await checkPermissions(options.verbose));
53
+ // Display results
54
+ if (options.json) {
55
+ console.log(JSON.stringify({ checks, summary: getSummary(checks) }, null, 2));
56
+ }
57
+ else {
58
+ displayResults(checks);
59
+ displayRecommendations(checks);
60
+ }
61
+ // Exit code based on results
62
+ const hasFailed = checks.some(c => c.status === 'fail');
63
+ if (hasFailed) {
64
+ process.exit(1);
65
+ }
66
+ }
67
+ /**
68
+ * Check platform compatibility
69
+ */
70
+ async function checkPlatform(verbose) {
71
+ const info = getPlatformInfo();
72
+ const supportedPlatforms = ['win32', 'darwin', 'linux'];
73
+ const isSupported = supportedPlatforms.includes(info.platform);
74
+ return {
75
+ name: 'Platform Compatibility',
76
+ status: isSupported ? 'pass' : 'warn',
77
+ message: isSupported
78
+ ? `${info.platformName} ${info.arch} (${info.release})`
79
+ : `${info.platformName} may not be fully supported`,
80
+ details: verbose ? `Node ${info.nodeVersion}` : undefined,
81
+ };
82
+ }
83
+ /**
84
+ * Check .env file
85
+ */
86
+ async function checkEnvFile(verbose) {
87
+ try {
88
+ const envPath = path.join(process.cwd(), '.env');
89
+ // Read file directly without access check to avoid TOCTOU race condition
90
+ const content = await fs.readFile(envPath, 'utf-8');
91
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
92
+ return {
93
+ name: '.env File',
94
+ status: 'pass',
95
+ message: 'Found and readable',
96
+ details: verbose ? `${lines.length} variables configured` : undefined,
97
+ };
98
+ }
99
+ catch {
100
+ return {
101
+ name: '.env File',
102
+ status: 'fail',
103
+ message: 'Not found',
104
+ details: 'Run "lsh init" to create configuration',
105
+ };
106
+ }
107
+ }
108
+ /**
109
+ * Check encryption key
110
+ */
111
+ async function checkEncryptionKey(verbose) {
112
+ try {
113
+ const envPath = path.join(process.cwd(), '.env');
114
+ const content = await fs.readFile(envPath, 'utf-8');
115
+ const match = content.match(/^LSH_SECRETS_KEY=(.+)$/m);
116
+ if (!match) {
117
+ return {
118
+ name: 'Encryption Key',
119
+ status: 'fail',
120
+ message: 'LSH_SECRETS_KEY not found in .env',
121
+ details: 'Run "lsh key" to generate a key',
122
+ };
123
+ }
124
+ const key = match[1].trim();
125
+ if (key.length < 32) {
126
+ return {
127
+ name: 'Encryption Key',
128
+ status: 'warn',
129
+ message: 'Key is too short (< 32 characters)',
130
+ details: 'Generate a stronger key with "lsh key"',
131
+ };
132
+ }
133
+ // Check if it's a valid hex string
134
+ const isHex = /^[0-9a-fA-F]+$/.test(key);
135
+ const expectedLength = 64; // 32 bytes in hex
136
+ if (isHex && key.length === expectedLength) {
137
+ return {
138
+ name: 'Encryption Key',
139
+ status: 'pass',
140
+ message: 'Valid (AES-256 compatible)',
141
+ details: verbose ? `${key.length} characters (hex)` : undefined,
142
+ };
143
+ }
144
+ return {
145
+ name: 'Encryption Key',
146
+ status: 'pass',
147
+ message: 'Present',
148
+ details: verbose ? `${key.length} characters` : undefined,
149
+ };
150
+ }
151
+ catch {
152
+ return {
153
+ name: 'Encryption Key',
154
+ status: 'fail',
155
+ message: 'Could not verify',
156
+ details: 'Ensure .env file exists and is readable',
157
+ };
158
+ }
159
+ }
160
+ /**
161
+ * Check storage backend configuration
162
+ */
163
+ async function checkStorageBackend(verbose) {
164
+ const checks = [];
165
+ try {
166
+ const envPath = path.join(process.cwd(), '.env');
167
+ const content = await fs.readFile(envPath, 'utf-8');
168
+ const supabaseUrl = content.match(/^SUPABASE_URL=(.+)$/m)?.[1]?.trim();
169
+ const supabaseKey = content.match(/^SUPABASE_ANON_KEY=(.+)$/m)?.[1]?.trim();
170
+ const databaseUrl = content.match(/^DATABASE_URL=(.+)$/m)?.[1]?.trim();
171
+ const storageMode = content.match(/^LSH_STORAGE_MODE=(.+)$/m)?.[1]?.trim();
172
+ // Determine storage type
173
+ if (storageMode === 'local') {
174
+ checks.push({
175
+ name: 'Storage Backend',
176
+ status: 'pass',
177
+ message: 'Local encryption mode',
178
+ details: 'No cloud sync available',
179
+ });
180
+ }
181
+ else if (supabaseUrl && supabaseKey) {
182
+ checks.push({
183
+ name: 'Storage Backend',
184
+ status: 'pass',
185
+ message: 'Supabase configured',
186
+ });
187
+ // Test connection
188
+ const connectionCheck = await testSupabaseConnection(supabaseUrl, supabaseKey, verbose);
189
+ checks.push(connectionCheck);
190
+ }
191
+ else if (databaseUrl) {
192
+ checks.push({
193
+ name: 'Storage Backend',
194
+ status: 'pass',
195
+ message: 'PostgreSQL configured',
196
+ details: verbose ? maskConnectionString(databaseUrl) : undefined,
197
+ });
198
+ }
199
+ else {
200
+ checks.push({
201
+ name: 'Storage Backend',
202
+ status: 'warn',
203
+ message: 'No storage backend configured',
204
+ details: 'Secrets will only be stored locally',
205
+ });
206
+ }
207
+ }
208
+ catch {
209
+ checks.push({
210
+ name: 'Storage Backend',
211
+ status: 'fail',
212
+ message: 'Could not verify configuration',
213
+ });
214
+ }
215
+ return checks;
216
+ }
217
+ /**
218
+ * Test Supabase connection
219
+ */
220
+ async function testSupabaseConnection(url, key, verbose) {
221
+ try {
222
+ const supabase = createClient(url, key);
223
+ // Try to query (404 for missing table is fine - means connection works)
224
+ const { error } = await supabase.from('lsh_secrets').select('count').limit(0);
225
+ if (!error || error.code === 'PGRST116' || error.message.includes('relation')) {
226
+ return {
227
+ name: 'Supabase Connection',
228
+ status: 'pass',
229
+ message: 'Connected successfully',
230
+ details: verbose ? url : undefined,
231
+ };
232
+ }
233
+ return {
234
+ name: 'Supabase Connection',
235
+ status: 'warn',
236
+ message: `Connection warning: ${error.message}`,
237
+ };
238
+ }
239
+ catch (error) {
240
+ const err = error;
241
+ return {
242
+ name: 'Supabase Connection',
243
+ status: 'fail',
244
+ message: 'Cannot connect',
245
+ details: err.message,
246
+ };
247
+ }
248
+ }
249
+ /**
250
+ * Check if in git repository
251
+ */
252
+ async function checkGitRepository(verbose) {
253
+ try {
254
+ const gitPath = path.join(process.cwd(), '.git');
255
+ // Use stat instead of access to avoid TOCTOU race condition
256
+ await fs.stat(gitPath);
257
+ // Check .gitignore
258
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
259
+ try {
260
+ const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
261
+ const ignoresEnv = gitignoreContent.includes('.env');
262
+ return {
263
+ name: 'Git Repository',
264
+ status: ignoresEnv ? 'pass' : 'warn',
265
+ message: ignoresEnv ? 'Git repository with .env in .gitignore' : 'Git repository found',
266
+ details: !ignoresEnv ? '.env not in .gitignore - secrets may be committed!' : undefined,
267
+ };
268
+ }
269
+ catch {
270
+ return {
271
+ name: 'Git Repository',
272
+ status: 'warn',
273
+ message: 'Git repository found, no .gitignore',
274
+ details: 'Add .env to .gitignore to prevent committing secrets',
275
+ };
276
+ }
277
+ }
278
+ catch {
279
+ return {
280
+ name: 'Git Repository',
281
+ status: 'skip',
282
+ message: 'Not in a git repository',
283
+ };
284
+ }
285
+ }
286
+ /**
287
+ * Check file permissions
288
+ */
289
+ async function checkPermissions(verbose) {
290
+ try {
291
+ const paths = getPlatformPaths();
292
+ // Check if we can write to temp directory with secure permissions
293
+ // Use crypto.randomBytes for secure random filename
294
+ const crypto = await import('crypto');
295
+ const randomSuffix = crypto.randomBytes(8).toString('hex');
296
+ const testFile = path.join(paths.tmpDir, `lsh-test-${randomSuffix}`);
297
+ // Create file with secure permissions (mode 0o600 = rw-------)
298
+ await fs.writeFile(testFile, 'test', { mode: 0o600 });
299
+ await fs.unlink(testFile);
300
+ // Check if we can create config directory
301
+ await fs.mkdir(paths.configDir, { recursive: true });
302
+ return {
303
+ name: 'File Permissions',
304
+ status: 'pass',
305
+ message: 'Can read/write required directories',
306
+ details: verbose ? `tmp: ${paths.tmpDir}, config: ${paths.configDir}` : undefined,
307
+ };
308
+ }
309
+ catch (error) {
310
+ const err = error;
311
+ return {
312
+ name: 'File Permissions',
313
+ status: 'fail',
314
+ message: 'Permission denied',
315
+ details: err.message,
316
+ };
317
+ }
318
+ }
319
+ /**
320
+ * Mask sensitive parts of connection string
321
+ */
322
+ function maskConnectionString(url) {
323
+ try {
324
+ const parsed = new URL(url);
325
+ if (parsed.password) {
326
+ parsed.password = '***';
327
+ }
328
+ return parsed.toString();
329
+ }
330
+ catch {
331
+ return url.replace(/:[^:@]+@/, ':***@');
332
+ }
333
+ }
334
+ /**
335
+ * Get summary statistics
336
+ */
337
+ function getSummary(checks) {
338
+ return {
339
+ total: checks.length,
340
+ passed: checks.filter(c => c.status === 'pass').length,
341
+ warned: checks.filter(c => c.status === 'warn').length,
342
+ failed: checks.filter(c => c.status === 'fail').length,
343
+ skipped: checks.filter(c => c.status === 'skip').length,
344
+ };
345
+ }
346
+ /**
347
+ * Display results in terminal
348
+ */
349
+ function displayResults(checks) {
350
+ for (const check of checks) {
351
+ let icon;
352
+ let color;
353
+ switch (check.status) {
354
+ case 'pass':
355
+ icon = '✅';
356
+ color = chalk.green;
357
+ break;
358
+ case 'warn':
359
+ icon = '⚠️ ';
360
+ color = chalk.yellow;
361
+ break;
362
+ case 'fail':
363
+ icon = '❌';
364
+ color = chalk.red;
365
+ break;
366
+ case 'skip':
367
+ icon = '⊝ ';
368
+ color = chalk.gray;
369
+ break;
370
+ }
371
+ console.log(icon, color(check.name), '-', check.message);
372
+ if (check.details) {
373
+ console.log(chalk.gray(` ${check.details}`));
374
+ }
375
+ }
376
+ console.log('');
377
+ }
378
+ /**
379
+ * Display recommendations
380
+ */
381
+ function displayRecommendations(checks) {
382
+ const summary = getSummary(checks);
383
+ const hasIssues = summary.failed > 0 || summary.warned > 0;
384
+ console.log(chalk.gray('━'.repeat(50)));
385
+ console.log('');
386
+ if (!hasIssues) {
387
+ console.log(chalk.bold.green('🎉 All checks passed!'));
388
+ console.log('');
389
+ console.log(chalk.gray('Your LSH installation is healthy and ready to use.'));
390
+ }
391
+ else {
392
+ console.log(chalk.bold.yellow('💡 Recommendations:'));
393
+ console.log('');
394
+ const failedChecks = checks.filter(c => c.status === 'fail');
395
+ const warnedChecks = checks.filter(c => c.status === 'warn');
396
+ if (failedChecks.length > 0) {
397
+ console.log(chalk.red(`❌ ${failedChecks.length} critical issue(s):`));
398
+ failedChecks.forEach(c => {
399
+ console.log(chalk.gray(` • ${c.name}: ${c.details || c.message}`));
400
+ });
401
+ console.log('');
402
+ }
403
+ if (warnedChecks.length > 0) {
404
+ console.log(chalk.yellow(`⚠️ ${warnedChecks.length} warning(s):`));
405
+ warnedChecks.forEach(c => {
406
+ console.log(chalk.gray(` • ${c.name}: ${c.details || c.message}`));
407
+ });
408
+ console.log('');
409
+ }
410
+ // Specific recommendations
411
+ if (checks.some(c => c.name === '.env File' && c.status === 'fail')) {
412
+ console.log(chalk.cyan('👉 Run: lsh init'));
413
+ }
414
+ if (checks.some(c => c.name === 'Supabase Connection' && c.status === 'fail')) {
415
+ console.log(chalk.cyan('👉 Verify Supabase credentials in .env'));
416
+ console.log(chalk.gray(' Check SUPABASE_URL and SUPABASE_ANON_KEY'));
417
+ }
418
+ if (checks.some(c => c.name === 'Git Repository' && c.status === 'warn')) {
419
+ console.log(chalk.cyan('👉 Add .env to .gitignore:'));
420
+ console.log(chalk.gray(' echo ".env" >> .gitignore'));
421
+ }
422
+ }
423
+ console.log('');
424
+ console.log(chalk.gray('Need help? Visit https://github.com/gwicho38/lsh'));
425
+ console.log('');
426
+ }
427
+ export default registerDoctorCommands;