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.
- 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/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 +54 -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,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;
|