lsh-framework 0.5.4
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/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- package/package.json +144 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Server Commands for LSH
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import DaemonClient from '../lib/daemon-client.js';
|
|
7
|
+
export function registerApiCommands(program) {
|
|
8
|
+
const api = program
|
|
9
|
+
.command('api')
|
|
10
|
+
.description('API server management and configuration');
|
|
11
|
+
// Start daemon with API server enabled
|
|
12
|
+
api
|
|
13
|
+
.command('start')
|
|
14
|
+
.description('Start daemon with API server enabled')
|
|
15
|
+
.option('-p, --port <port>', 'API port', '3030')
|
|
16
|
+
.option('-k, --api-key <key>', 'API key (generated if not provided)')
|
|
17
|
+
.option('--webhooks', 'Enable webhook support', false)
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const apiKey = options.apiKey || crypto.randomBytes(32).toString('hex');
|
|
21
|
+
const daemonClient = new DaemonClient();
|
|
22
|
+
// Check if daemon is already running
|
|
23
|
+
if (daemonClient.isDaemonRunning()) {
|
|
24
|
+
console.log(chalk.yellow('⚠️ Daemon is already running. Restarting with API enabled...'));
|
|
25
|
+
// Stop existing daemon
|
|
26
|
+
await daemonClient.connect();
|
|
27
|
+
await daemonClient.stopDaemon();
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
29
|
+
}
|
|
30
|
+
// Write API configuration to environment or a config file
|
|
31
|
+
process.env.LSH_API_ENABLED = 'true';
|
|
32
|
+
process.env.LSH_API_PORT = options.port;
|
|
33
|
+
process.env.LSH_API_KEY = apiKey;
|
|
34
|
+
process.env.LSH_ENABLE_WEBHOOKS = options.webhooks ? 'true' : 'false';
|
|
35
|
+
// Restart the daemon which will pick up the environment variables
|
|
36
|
+
await daemonClient.restartDaemon();
|
|
37
|
+
console.log(chalk.green('✅ Daemon started with API server'));
|
|
38
|
+
console.log(chalk.blue(`\n📡 API Server: http://localhost:${options.port}`));
|
|
39
|
+
console.log(chalk.yellow(`🔑 API Key: ${apiKey}`));
|
|
40
|
+
console.log(chalk.gray('\nStore this API key securely. You will need it to authenticate API requests.'));
|
|
41
|
+
console.log(chalk.gray(`\nExample usage:`));
|
|
42
|
+
console.log(chalk.gray(` curl -H "X-API-Key: ${apiKey}" http://localhost:${options.port}/api/status`));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(chalk.red(`❌ Failed to start API server: ${error.message}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// Generate API key
|
|
50
|
+
api
|
|
51
|
+
.command('key')
|
|
52
|
+
.description('Generate a new API key')
|
|
53
|
+
.action(() => {
|
|
54
|
+
const apiKey = crypto.randomBytes(32).toString('hex');
|
|
55
|
+
console.log(chalk.green('🔑 Generated API Key:'));
|
|
56
|
+
console.log(apiKey);
|
|
57
|
+
console.log(chalk.gray('\nSet this as LSH_API_KEY environment variable:'));
|
|
58
|
+
console.log(chalk.gray(` export LSH_API_KEY="${apiKey}"`));
|
|
59
|
+
});
|
|
60
|
+
// Test API connection
|
|
61
|
+
api
|
|
62
|
+
.command('test')
|
|
63
|
+
.description('Test API server connection')
|
|
64
|
+
.option('-p, --port <port>', 'API port', '3030')
|
|
65
|
+
.option('-k, --api-key <key>', 'API key')
|
|
66
|
+
.action(async (options) => {
|
|
67
|
+
try {
|
|
68
|
+
const apiKey = options.apiKey || process.env.LSH_API_KEY;
|
|
69
|
+
if (!apiKey) {
|
|
70
|
+
console.error(chalk.red('❌ API key required. Use --api-key or set LSH_API_KEY environment variable'));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const response = await fetch(`http://localhost:${options.port}/api/status`, {
|
|
74
|
+
headers: {
|
|
75
|
+
'X-API-Key': apiKey
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
if (response.ok) {
|
|
79
|
+
const status = await response.json();
|
|
80
|
+
console.log(chalk.green('✅ API server is running'));
|
|
81
|
+
console.log('Status:', JSON.stringify(status, null, 2));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.error(chalk.red(`❌ API server returned ${response.status}: ${response.statusText}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error(chalk.red(`❌ Failed to connect to API server: ${error.message}`));
|
|
89
|
+
console.log(chalk.yellow('Make sure the daemon is running with API enabled:'));
|
|
90
|
+
console.log(chalk.gray(' lsh api start'));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Configure webhooks
|
|
94
|
+
api
|
|
95
|
+
.command('webhook')
|
|
96
|
+
.description('Configure webhook endpoints')
|
|
97
|
+
.argument('<action>', 'Action: add, list, remove')
|
|
98
|
+
.argument('[endpoint]', 'Webhook endpoint URL')
|
|
99
|
+
.option('-p, --port <port>', 'API port', '3030')
|
|
100
|
+
.option('-k, --api-key <key>', 'API key')
|
|
101
|
+
.action(async (action, endpoint, options) => {
|
|
102
|
+
try {
|
|
103
|
+
const apiKey = options.apiKey || process.env.LSH_API_KEY;
|
|
104
|
+
if (!apiKey) {
|
|
105
|
+
console.error(chalk.red('❌ API key required'));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const baseUrl = `http://localhost:${options.port}`;
|
|
109
|
+
switch (action) {
|
|
110
|
+
case 'add': {
|
|
111
|
+
if (!endpoint) {
|
|
112
|
+
console.error(chalk.red('❌ Endpoint URL required'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const addResponse = await fetch(`${baseUrl}/api/webhooks`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'X-API-Key': apiKey,
|
|
119
|
+
'Content-Type': 'application/json'
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ endpoint })
|
|
122
|
+
});
|
|
123
|
+
if (addResponse.ok) {
|
|
124
|
+
const result = await addResponse.json();
|
|
125
|
+
console.log(chalk.green('✅ Webhook added successfully'));
|
|
126
|
+
console.log('Endpoints:', result.endpoints);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.error(chalk.red('❌ Failed to add webhook'));
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'list': {
|
|
134
|
+
const listResponse = await fetch(`${baseUrl}/api/webhooks`, {
|
|
135
|
+
headers: { 'X-API-Key': apiKey }
|
|
136
|
+
});
|
|
137
|
+
if (listResponse.ok) {
|
|
138
|
+
const webhooks = await listResponse.json();
|
|
139
|
+
console.log(chalk.blue('📮 Webhook Configuration:'));
|
|
140
|
+
console.log(`Enabled: ${webhooks.enabled}`);
|
|
141
|
+
console.log('Endpoints:', webhooks.endpoints);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
default:
|
|
146
|
+
console.error(chalk.red(`❌ Unknown action: ${action}`));
|
|
147
|
+
console.log('Valid actions: add, list, remove');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error(chalk.red(`❌ Failed: ${error.message}`));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// Example client code generator
|
|
155
|
+
api
|
|
156
|
+
.command('example')
|
|
157
|
+
.description('Generate example client code')
|
|
158
|
+
.option('-l, --language <lang>', 'Language (js, python, curl)', 'js')
|
|
159
|
+
.option('-p, --port <port>', 'API port', '3030')
|
|
160
|
+
.action((options) => {
|
|
161
|
+
const apiKey = process.env.LSH_API_KEY || 'YOUR_API_KEY';
|
|
162
|
+
switch (options.language) {
|
|
163
|
+
case 'js':
|
|
164
|
+
console.log(chalk.blue('// JavaScript Example Client\n'));
|
|
165
|
+
console.log(`const LSHClient = {
|
|
166
|
+
baseURL: 'http://localhost:${options.port}',
|
|
167
|
+
apiKey: '${apiKey}',
|
|
168
|
+
|
|
169
|
+
async request(path, options = {}) {
|
|
170
|
+
const response = await fetch(\`\${this.baseURL}\${path}\`, {
|
|
171
|
+
...options,
|
|
172
|
+
headers: {
|
|
173
|
+
'X-API-Key': this.apiKey,
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
...options.headers
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(\`API error: \${response.statusText}\`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return response.json();
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Get daemon status
|
|
187
|
+
async getStatus() {
|
|
188
|
+
return this.request('/api/status');
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// List all jobs
|
|
192
|
+
async listJobs() {
|
|
193
|
+
return this.request('/api/jobs');
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Create a new job
|
|
197
|
+
async createJob(jobSpec) {
|
|
198
|
+
return this.request('/api/jobs', {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
body: JSON.stringify(jobSpec)
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Trigger job execution
|
|
205
|
+
async triggerJob(jobId) {
|
|
206
|
+
return this.request(\`/api/jobs/\${jobId}/trigger\`, {
|
|
207
|
+
method: 'POST'
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// Stream events using EventSource
|
|
212
|
+
streamEvents() {
|
|
213
|
+
const eventSource = new EventSource(\`\${this.baseURL}/api/events\`);
|
|
214
|
+
|
|
215
|
+
eventSource.onmessage = (event) => {
|
|
216
|
+
const data = JSON.parse(event.data);
|
|
217
|
+
console.log('Event:', data);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
eventSource.onerror = (error) => {
|
|
221
|
+
console.error('EventSource error:', error);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return eventSource;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Example usage
|
|
229
|
+
(async () => {
|
|
230
|
+
try {
|
|
231
|
+
const status = await LSHClient.getStatus();
|
|
232
|
+
console.log('Daemon status:', status);
|
|
233
|
+
|
|
234
|
+
// Create a job
|
|
235
|
+
const job = await LSHClient.createJob({
|
|
236
|
+
name: 'Example Job',
|
|
237
|
+
command: 'echo "Hello from API"',
|
|
238
|
+
type: 'shell'
|
|
239
|
+
});
|
|
240
|
+
console.log('Created job:', job);
|
|
241
|
+
|
|
242
|
+
// Trigger the job
|
|
243
|
+
const result = await LSHClient.triggerJob(job.id);
|
|
244
|
+
console.log('Job result:', result);
|
|
245
|
+
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('Error:', error);
|
|
248
|
+
}
|
|
249
|
+
})();`);
|
|
250
|
+
break;
|
|
251
|
+
case 'python':
|
|
252
|
+
console.log(chalk.blue('# Python Example Client\n'));
|
|
253
|
+
console.log(`import requests
|
|
254
|
+
import json
|
|
255
|
+
from typing import Dict, Any, Optional
|
|
256
|
+
|
|
257
|
+
class LSHClient:
|
|
258
|
+
def __init__(self, base_url: str = "http://localhost:${options.port}", api_key: str = "${apiKey}"):
|
|
259
|
+
self.base_url = base_url
|
|
260
|
+
self.api_key = api_key
|
|
261
|
+
self.headers = {
|
|
262
|
+
"X-API-Key": api_key,
|
|
263
|
+
"Content-Type": "application/json"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
def request(self, method: str, path: str, data: Optional[Dict] = None) -> Dict[str, Any]:
|
|
267
|
+
"""Make an API request"""
|
|
268
|
+
url = f"{self.base_url}{path}"
|
|
269
|
+
response = requests.request(method, url, headers=self.headers, json=data)
|
|
270
|
+
response.raise_for_status()
|
|
271
|
+
return response.json()
|
|
272
|
+
|
|
273
|
+
def get_status(self) -> Dict[str, Any]:
|
|
274
|
+
"""Get daemon status"""
|
|
275
|
+
return self.request("GET", "/api/status")
|
|
276
|
+
|
|
277
|
+
def list_jobs(self) -> list:
|
|
278
|
+
"""List all jobs"""
|
|
279
|
+
return self.request("GET", "/api/jobs")
|
|
280
|
+
|
|
281
|
+
def create_job(self, job_spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
282
|
+
"""Create a new job"""
|
|
283
|
+
return self.request("POST", "/api/jobs", job_spec)
|
|
284
|
+
|
|
285
|
+
def trigger_job(self, job_id: str) -> Dict[str, Any]:
|
|
286
|
+
"""Trigger job execution"""
|
|
287
|
+
return self.request("POST", f"/api/jobs/{job_id}/trigger")
|
|
288
|
+
|
|
289
|
+
# Example usage
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
client = LSHClient()
|
|
292
|
+
|
|
293
|
+
# Get status
|
|
294
|
+
status = client.get_status()
|
|
295
|
+
print(f"Daemon status: {json.dumps(status, indent=2)}")
|
|
296
|
+
|
|
297
|
+
# Create a job
|
|
298
|
+
job = client.create_job({
|
|
299
|
+
"name": "Example Job",
|
|
300
|
+
"command": "echo 'Hello from Python'",
|
|
301
|
+
"type": "shell"
|
|
302
|
+
})
|
|
303
|
+
print(f"Created job: {job}")
|
|
304
|
+
|
|
305
|
+
# Trigger the job
|
|
306
|
+
result = client.trigger_job(job["id"])
|
|
307
|
+
print(f"Job result: {result}")`);
|
|
308
|
+
break;
|
|
309
|
+
case 'curl':
|
|
310
|
+
console.log(chalk.blue('# cURL Examples\n'));
|
|
311
|
+
console.log(`# Get daemon status
|
|
312
|
+
curl -H "X-API-Key: ${apiKey}" \\
|
|
313
|
+
http://localhost:${options.port}/api/status
|
|
314
|
+
|
|
315
|
+
# List all jobs
|
|
316
|
+
curl -H "X-API-Key: ${apiKey}" \\
|
|
317
|
+
http://localhost:${options.port}/api/jobs
|
|
318
|
+
|
|
319
|
+
# Create a new job
|
|
320
|
+
curl -X POST \\
|
|
321
|
+
-H "X-API-Key: ${apiKey}" \\
|
|
322
|
+
-H "Content-Type: application/json" \\
|
|
323
|
+
-d '{
|
|
324
|
+
"name": "Example Job",
|
|
325
|
+
"command": "echo Hello World",
|
|
326
|
+
"type": "shell"
|
|
327
|
+
}' \\
|
|
328
|
+
http://localhost:${options.port}/api/jobs
|
|
329
|
+
|
|
330
|
+
# Trigger a job
|
|
331
|
+
curl -X POST \\
|
|
332
|
+
-H "X-API-Key: ${apiKey}" \\
|
|
333
|
+
http://localhost:${options.port}/api/jobs/JOB_ID/trigger
|
|
334
|
+
|
|
335
|
+
# Stream events
|
|
336
|
+
curl -H "X-API-Key: ${apiKey}" \\
|
|
337
|
+
-H "Accept: text/event-stream" \\
|
|
338
|
+
http://localhost:${options.port}/api/events`);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
export default registerApiCommands;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-management commands for LSH
|
|
3
|
+
* Provides utilities for updating and maintaining the CLI
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import * as https from 'https';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const selfCommand = new Command('self');
|
|
15
|
+
selfCommand.description('Manage and update the LSH application');
|
|
16
|
+
/**
|
|
17
|
+
* Parse version string to tuple for comparison
|
|
18
|
+
*/
|
|
19
|
+
function parseVersion(version) {
|
|
20
|
+
return version
|
|
21
|
+
.replace(/^v/, '')
|
|
22
|
+
.split('.')
|
|
23
|
+
.map(x => parseInt(x) || 0);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compare two version strings
|
|
27
|
+
* Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
28
|
+
*/
|
|
29
|
+
function compareVersions(v1, v2) {
|
|
30
|
+
const parts1 = parseVersion(v1);
|
|
31
|
+
const parts2 = parseVersion(v2);
|
|
32
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
33
|
+
const p1 = parts1[i] || 0;
|
|
34
|
+
const p2 = parts2[i] || 0;
|
|
35
|
+
if (p1 < p2)
|
|
36
|
+
return -1;
|
|
37
|
+
if (p1 > p2)
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get current version from package.json
|
|
44
|
+
*/
|
|
45
|
+
function getCurrentVersion() {
|
|
46
|
+
try {
|
|
47
|
+
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
48
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
49
|
+
return packageJson.version || 'unknown';
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return 'unknown';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fetch latest version from npm registry
|
|
57
|
+
*/
|
|
58
|
+
async function fetchLatestVersion() {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const options = {
|
|
61
|
+
hostname: 'registry.npmjs.org',
|
|
62
|
+
port: 443,
|
|
63
|
+
path: '/gwicho38-lsh',
|
|
64
|
+
method: 'GET',
|
|
65
|
+
headers: {
|
|
66
|
+
'User-Agent': 'lsh-cli',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
https.get(options, (res) => {
|
|
70
|
+
let data = '';
|
|
71
|
+
res.on('data', (chunk) => {
|
|
72
|
+
data += chunk;
|
|
73
|
+
});
|
|
74
|
+
res.on('end', () => {
|
|
75
|
+
try {
|
|
76
|
+
if (res.statusCode === 200) {
|
|
77
|
+
const npmData = JSON.parse(data);
|
|
78
|
+
const latestVersion = npmData['dist-tags']?.latest;
|
|
79
|
+
if (latestVersion) {
|
|
80
|
+
const publishedAt = npmData.time?.[latestVersion];
|
|
81
|
+
resolve({
|
|
82
|
+
version: latestVersion,
|
|
83
|
+
publishedAt: publishedAt || undefined,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
resolve(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(chalk.red(`✗ npm registry returned status ${res.statusCode}`));
|
|
92
|
+
resolve(null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
reject(error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}).on('error', (error) => {
|
|
100
|
+
reject(error);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check GitHub Actions CI status for a specific version tag
|
|
106
|
+
*/
|
|
107
|
+
async function checkCIStatus(_version) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const options = {
|
|
110
|
+
hostname: 'api.github.com',
|
|
111
|
+
port: 443,
|
|
112
|
+
path: `/repos/gwicho38/lsh/actions/runs?per_page=5`,
|
|
113
|
+
method: 'GET',
|
|
114
|
+
headers: {
|
|
115
|
+
'User-Agent': 'lsh-cli',
|
|
116
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
https.get(options, (res) => {
|
|
120
|
+
let data = '';
|
|
121
|
+
res.on('data', (chunk) => {
|
|
122
|
+
data += chunk;
|
|
123
|
+
});
|
|
124
|
+
res.on('end', () => {
|
|
125
|
+
try {
|
|
126
|
+
if (res.statusCode === 200) {
|
|
127
|
+
const ghData = JSON.parse(data);
|
|
128
|
+
const runs = ghData.workflow_runs || [];
|
|
129
|
+
// Find the most recent workflow run for main branch
|
|
130
|
+
const mainRuns = runs.filter((run) => run.head_branch === 'main' && run.status === 'completed');
|
|
131
|
+
if (mainRuns.length > 0) {
|
|
132
|
+
const latestRun = mainRuns[0];
|
|
133
|
+
const passing = latestRun.conclusion === 'success';
|
|
134
|
+
resolve({
|
|
135
|
+
passing,
|
|
136
|
+
url: latestRun.html_url,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// No completed runs found, assume passing
|
|
141
|
+
resolve({ passing: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// If we can't check CI, don't block the update
|
|
146
|
+
resolve({ passing: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (_error) {
|
|
150
|
+
// On error, don't block the update
|
|
151
|
+
resolve({ passing: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}).on('error', () => {
|
|
155
|
+
// On network error, don't block the update
|
|
156
|
+
resolve({ passing: true });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Update command - check for and install updates from npm
|
|
162
|
+
*/
|
|
163
|
+
selfCommand
|
|
164
|
+
.command('update')
|
|
165
|
+
.description('Check for and install LSH updates from npm')
|
|
166
|
+
.option('--check', 'Only check for updates, don\'t install')
|
|
167
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
168
|
+
.option('--skip-ci-check', 'Skip CI status check and install anyway')
|
|
169
|
+
.action(async (options) => {
|
|
170
|
+
try {
|
|
171
|
+
const currentVersion = getCurrentVersion();
|
|
172
|
+
console.log(chalk.cyan('Current version:'), currentVersion);
|
|
173
|
+
console.log(chalk.cyan('Checking npm for updates...'));
|
|
174
|
+
// Fetch latest version from npm
|
|
175
|
+
const latestInfo = await fetchLatestVersion();
|
|
176
|
+
if (!latestInfo) {
|
|
177
|
+
console.log(chalk.red('✗ Failed to fetch version information from npm'));
|
|
178
|
+
console.log(chalk.yellow('⚠ Make sure you have internet connectivity'));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const { version: latestVersion, publishedAt } = latestInfo;
|
|
182
|
+
console.log(chalk.cyan('Latest version:'), latestVersion);
|
|
183
|
+
if (publishedAt) {
|
|
184
|
+
const date = new Date(publishedAt);
|
|
185
|
+
console.log(chalk.dim(` Published: ${date.toLocaleDateString()}`));
|
|
186
|
+
}
|
|
187
|
+
// Compare versions
|
|
188
|
+
const comparison = compareVersions(currentVersion, latestVersion);
|
|
189
|
+
if (comparison === 0) {
|
|
190
|
+
console.log(chalk.green('✓ You\'re already on the latest version!'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (comparison > 0) {
|
|
194
|
+
console.log(chalk.green(`✓ Your version (${currentVersion}) is newer than npm`));
|
|
195
|
+
console.log(chalk.dim(' You may be using a development version'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Update available
|
|
199
|
+
console.log(chalk.yellow(`⬆ Update available: ${currentVersion} → ${latestVersion}`));
|
|
200
|
+
if (options.check) {
|
|
201
|
+
console.log(chalk.cyan('ℹ Run \'lsh self update\' to install the update'));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Ask for confirmation unless --yes flag is used
|
|
205
|
+
if (!options.yes) {
|
|
206
|
+
const readline = await import('readline');
|
|
207
|
+
const rl = readline.createInterface({
|
|
208
|
+
input: process.stdin,
|
|
209
|
+
output: process.stdout,
|
|
210
|
+
});
|
|
211
|
+
const answer = await new Promise((resolve) => {
|
|
212
|
+
rl.question(chalk.yellow(`Install lsh ${latestVersion}? (y/N) `), (ans) => {
|
|
213
|
+
rl.close();
|
|
214
|
+
resolve(ans);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
218
|
+
console.log(chalk.yellow('Update cancelled'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Check CI status before installing (unless skipped)
|
|
223
|
+
if (!options.skipCiCheck) {
|
|
224
|
+
console.log(chalk.cyan('🔍 Checking CI status...'));
|
|
225
|
+
const ciStatus = await checkCIStatus(latestVersion);
|
|
226
|
+
if (!ciStatus.passing) {
|
|
227
|
+
console.log(chalk.red('✗ CI build is failing for the latest version'));
|
|
228
|
+
if (ciStatus.url) {
|
|
229
|
+
console.log(chalk.yellow(` View CI status: ${ciStatus.url}`));
|
|
230
|
+
}
|
|
231
|
+
console.log(chalk.yellow('⚠ Update blocked to prevent installing a broken version'));
|
|
232
|
+
console.log(chalk.dim(' Use --skip-ci-check to install anyway (not recommended)'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log(chalk.green('✓ CI build is passing'));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Install update
|
|
240
|
+
console.log(chalk.cyan(`📦 Installing lsh ${latestVersion}...`));
|
|
241
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
242
|
+
const updateProcess = spawn(npmCmd, ['install', '-g', 'gwicho38-lsh@latest'], {
|
|
243
|
+
stdio: 'inherit',
|
|
244
|
+
});
|
|
245
|
+
updateProcess.on('close', (code) => {
|
|
246
|
+
if (code === 0) {
|
|
247
|
+
console.log(chalk.green(`✓ Successfully updated to lsh ${latestVersion}!`));
|
|
248
|
+
console.log(chalk.yellow('ℹ Restart your terminal or run \'hash -r\' to use the new version'));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
console.log(chalk.red('✗ Update failed'));
|
|
252
|
+
console.log(chalk.yellow('ℹ Try running with sudo: sudo npm install -g gwicho38-lsh@latest'));
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
console.error(chalk.red('✗ Error during update:'), error);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
/**
|
|
261
|
+
* Version command - show detailed version information
|
|
262
|
+
*/
|
|
263
|
+
selfCommand
|
|
264
|
+
.command('version')
|
|
265
|
+
.description('Show detailed version information')
|
|
266
|
+
.action(() => {
|
|
267
|
+
const currentVersion = getCurrentVersion();
|
|
268
|
+
console.log(chalk.cyan('╔════════════════════════════════════╗'));
|
|
269
|
+
console.log(chalk.cyan('║ LSH Version Info ║'));
|
|
270
|
+
console.log(chalk.cyan('╚════════════════════════════════════╝'));
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(chalk.cyan('Version:'), currentVersion);
|
|
273
|
+
console.log(chalk.cyan('Node:'), process.version);
|
|
274
|
+
console.log(chalk.cyan('Platform:'), `${process.platform} (${process.arch})`);
|
|
275
|
+
console.log();
|
|
276
|
+
console.log(chalk.dim('Run \'lsh self update --check\' to check for updates'));
|
|
277
|
+
});
|
|
278
|
+
/**
|
|
279
|
+
* Info command - show installation and configuration info
|
|
280
|
+
*/
|
|
281
|
+
selfCommand
|
|
282
|
+
.command('info')
|
|
283
|
+
.description('Show installation and configuration information')
|
|
284
|
+
.action(() => {
|
|
285
|
+
const currentVersion = getCurrentVersion();
|
|
286
|
+
console.log(chalk.cyan('╔════════════════════════════════════╗'));
|
|
287
|
+
console.log(chalk.cyan('║ LSH Installation Info ║'));
|
|
288
|
+
console.log(chalk.cyan('╚════════════════════════════════════╝'));
|
|
289
|
+
console.log();
|
|
290
|
+
// Version info
|
|
291
|
+
console.log(chalk.yellow('Version Information:'));
|
|
292
|
+
console.log(' LSH Version:', currentVersion);
|
|
293
|
+
console.log(' Node.js:', process.version);
|
|
294
|
+
console.log(' Platform:', `${process.platform} (${process.arch})`);
|
|
295
|
+
console.log();
|
|
296
|
+
// Installation paths
|
|
297
|
+
console.log(chalk.yellow('Installation:'));
|
|
298
|
+
console.log(' Executable:', process.execPath);
|
|
299
|
+
console.log(' Working Dir:', process.cwd());
|
|
300
|
+
console.log();
|
|
301
|
+
// Environment
|
|
302
|
+
console.log(chalk.yellow('Environment:'));
|
|
303
|
+
console.log(' NODE_ENV:', process.env.NODE_ENV || 'not set');
|
|
304
|
+
console.log(' HOME:', process.env.HOME || 'not set');
|
|
305
|
+
console.log(' USER:', process.env.USER || 'not set');
|
|
306
|
+
console.log();
|
|
307
|
+
// Configuration
|
|
308
|
+
const envFile = path.join(process.cwd(), '.env');
|
|
309
|
+
const envExists = fs.existsSync(envFile);
|
|
310
|
+
console.log(chalk.yellow('Configuration:'));
|
|
311
|
+
console.log(' .env file:', envExists ? chalk.green('Found') : chalk.red('Not found'));
|
|
312
|
+
if (!envExists) {
|
|
313
|
+
console.log(chalk.dim(' Copy .env.example to .env to configure'));
|
|
314
|
+
}
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(chalk.dim('For more info, visit: https://github.com/gwicho38/lsh'));
|
|
317
|
+
});
|
|
318
|
+
export default selfCommand;
|