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.
Files changed (90) hide show
  1. package/.env.example +51 -0
  2. package/README.md +399 -0
  3. package/dist/app.js +33 -0
  4. package/dist/cicd/analytics.js +261 -0
  5. package/dist/cicd/auth.js +269 -0
  6. package/dist/cicd/cache-manager.js +172 -0
  7. package/dist/cicd/data-retention.js +305 -0
  8. package/dist/cicd/performance-monitor.js +224 -0
  9. package/dist/cicd/webhook-receiver.js +634 -0
  10. package/dist/cli.js +500 -0
  11. package/dist/commands/api.js +343 -0
  12. package/dist/commands/self.js +318 -0
  13. package/dist/commands/theme.js +257 -0
  14. package/dist/commands/zsh-import.js +240 -0
  15. package/dist/components/App.js +1 -0
  16. package/dist/components/Divider.js +29 -0
  17. package/dist/components/REPL.js +43 -0
  18. package/dist/components/Terminal.js +232 -0
  19. package/dist/components/UserInput.js +30 -0
  20. package/dist/daemon/api-server.js +315 -0
  21. package/dist/daemon/job-registry.js +554 -0
  22. package/dist/daemon/lshd.js +822 -0
  23. package/dist/daemon/monitoring-api.js +220 -0
  24. package/dist/examples/supabase-integration.js +106 -0
  25. package/dist/lib/api-error-handler.js +183 -0
  26. package/dist/lib/associative-arrays.js +285 -0
  27. package/dist/lib/base-api-server.js +290 -0
  28. package/dist/lib/base-command-registrar.js +286 -0
  29. package/dist/lib/base-job-manager.js +293 -0
  30. package/dist/lib/brace-expansion.js +160 -0
  31. package/dist/lib/builtin-commands.js +439 -0
  32. package/dist/lib/cloud-config-manager.js +347 -0
  33. package/dist/lib/command-validator.js +190 -0
  34. package/dist/lib/completion-system.js +344 -0
  35. package/dist/lib/cron-job-manager.js +364 -0
  36. package/dist/lib/daemon-client-helper.js +141 -0
  37. package/dist/lib/daemon-client.js +501 -0
  38. package/dist/lib/database-persistence.js +638 -0
  39. package/dist/lib/database-schema.js +259 -0
  40. package/dist/lib/enhanced-history-system.js +246 -0
  41. package/dist/lib/env-validator.js +265 -0
  42. package/dist/lib/executors/builtin-executor.js +52 -0
  43. package/dist/lib/extended-globbing.js +411 -0
  44. package/dist/lib/extended-parameter-expansion.js +227 -0
  45. package/dist/lib/floating-point-arithmetic.js +256 -0
  46. package/dist/lib/history-system.js +245 -0
  47. package/dist/lib/interactive-shell.js +460 -0
  48. package/dist/lib/job-builtins.js +580 -0
  49. package/dist/lib/job-manager.js +386 -0
  50. package/dist/lib/job-storage-database.js +156 -0
  51. package/dist/lib/job-storage-memory.js +73 -0
  52. package/dist/lib/logger.js +274 -0
  53. package/dist/lib/lshrc-init.js +177 -0
  54. package/dist/lib/pathname-expansion.js +216 -0
  55. package/dist/lib/prompt-system.js +328 -0
  56. package/dist/lib/script-runner.js +226 -0
  57. package/dist/lib/secrets-manager.js +193 -0
  58. package/dist/lib/shell-executor.js +2504 -0
  59. package/dist/lib/shell-parser.js +958 -0
  60. package/dist/lib/shell-types.js +6 -0
  61. package/dist/lib/shell.lib.js +40 -0
  62. package/dist/lib/supabase-client.js +58 -0
  63. package/dist/lib/theme-manager.js +476 -0
  64. package/dist/lib/variable-expansion.js +385 -0
  65. package/dist/lib/zsh-compatibility.js +658 -0
  66. package/dist/lib/zsh-import-manager.js +699 -0
  67. package/dist/lib/zsh-options.js +328 -0
  68. package/dist/pipeline/job-tracker.js +491 -0
  69. package/dist/pipeline/mcli-bridge.js +302 -0
  70. package/dist/pipeline/pipeline-service.js +1116 -0
  71. package/dist/pipeline/workflow-engine.js +867 -0
  72. package/dist/services/api/api.js +58 -0
  73. package/dist/services/api/auth.js +35 -0
  74. package/dist/services/api/config.js +7 -0
  75. package/dist/services/api/file.js +22 -0
  76. package/dist/services/cron/cron-registrar.js +235 -0
  77. package/dist/services/cron/cron.js +9 -0
  78. package/dist/services/daemon/daemon-registrar.js +565 -0
  79. package/dist/services/daemon/daemon.js +9 -0
  80. package/dist/services/lib/lib.js +86 -0
  81. package/dist/services/log-file-extractor.js +170 -0
  82. package/dist/services/secrets/secrets.js +94 -0
  83. package/dist/services/shell/shell.js +28 -0
  84. package/dist/services/supabase/supabase-registrar.js +367 -0
  85. package/dist/services/supabase/supabase.js +9 -0
  86. package/dist/services/zapier.js +16 -0
  87. package/dist/simple-api-server.js +148 -0
  88. package/dist/store/store.js +31 -0
  89. package/dist/util/lib.util.js +11 -0
  90. package/package.json +144 -0
@@ -0,0 +1,501 @@
1
+ /**
2
+ * LSH Daemon Client
3
+ * Provides communication interface between LSH and the job daemon
4
+ */
5
+ import * as net from 'net';
6
+ import * as fs from 'fs';
7
+ import { EventEmitter } from 'events';
8
+ import DatabasePersistence from './database-persistence.js';
9
+ import { createLogger } from './logger.js';
10
+ export class DaemonClient extends EventEmitter {
11
+ socketPath;
12
+ socket;
13
+ connected = false;
14
+ messageId = 0;
15
+ pendingMessages = new Map();
16
+ databasePersistence;
17
+ userId;
18
+ sessionId;
19
+ logger = createLogger('DaemonClient');
20
+ constructor(socketPath, userId) {
21
+ super();
22
+ // Use user-specific socket path if not provided
23
+ this.socketPath = socketPath || `/tmp/lsh-job-daemon-${process.env.USER || 'default'}.sock`;
24
+ this.userId = userId;
25
+ this.sessionId = `lsh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
26
+ if (userId) {
27
+ this.databasePersistence = new DatabasePersistence(userId);
28
+ }
29
+ }
30
+ /**
31
+ * Connect to the daemon
32
+ */
33
+ async connect() {
34
+ return new Promise((resolve, reject) => {
35
+ if (this.connected) {
36
+ resolve(true);
37
+ return;
38
+ }
39
+ // Check if socket file exists
40
+ if (!fs.existsSync(this.socketPath)) {
41
+ reject(new Error(`Daemon socket not found at ${this.socketPath}. Is the daemon running?`));
42
+ return;
43
+ }
44
+ // Check socket permissions
45
+ try {
46
+ fs.accessSync(this.socketPath, fs.constants.R_OK | fs.constants.W_OK);
47
+ }
48
+ catch (_err) {
49
+ const stats = fs.statSync(this.socketPath);
50
+ const currentUid = process.getuid?.();
51
+ const owner = currentUid !== undefined && stats.uid === currentUid ? 'you' : 'another user';
52
+ reject(new Error(`Permission denied to access socket at ${this.socketPath}. Socket is owned by ${owner}. You may need to start your own daemon with: lsh daemon start`));
53
+ return;
54
+ }
55
+ this.socket = new net.Socket();
56
+ this.socket.on('connect', () => {
57
+ this.connected = true;
58
+ this.emit('connected');
59
+ resolve(true);
60
+ });
61
+ let buffer = '';
62
+ const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB limit
63
+ this.socket.on('data', (data) => {
64
+ try {
65
+ buffer += data.toString();
66
+ // Prevent buffer from growing too large
67
+ if (buffer.length > MAX_BUFFER_SIZE) {
68
+ this.logger.error('Daemon response too large, truncating buffer');
69
+ buffer = buffer.substring(buffer.length - MAX_BUFFER_SIZE / 2);
70
+ }
71
+ // Try to parse complete JSON messages
72
+ // Messages might be split across multiple data events
73
+ while (buffer.length > 0) {
74
+ try {
75
+ // Try to find complete JSON message boundaries
76
+ let jsonStart = -1;
77
+ let braceCount = 0;
78
+ let inString = false;
79
+ let escaped = false;
80
+ for (let i = 0; i < buffer.length; i++) {
81
+ const char = buffer[i];
82
+ if (escaped) {
83
+ escaped = false;
84
+ continue;
85
+ }
86
+ if (char === '\\') {
87
+ escaped = true;
88
+ continue;
89
+ }
90
+ if (char === '"' && !escaped) {
91
+ inString = !inString;
92
+ continue;
93
+ }
94
+ if (inString)
95
+ continue;
96
+ if (char === '{') {
97
+ if (jsonStart === -1)
98
+ jsonStart = i;
99
+ braceCount++;
100
+ }
101
+ else if (char === '}') {
102
+ braceCount--;
103
+ if (braceCount === 0 && jsonStart !== -1) {
104
+ // Found complete JSON object
105
+ const jsonStr = buffer.substring(jsonStart, i + 1);
106
+ try {
107
+ const response = JSON.parse(jsonStr);
108
+ this.handleResponse(response);
109
+ buffer = buffer.substring(i + 1); // Remove processed part
110
+ break;
111
+ }
112
+ catch (parseError) {
113
+ this.logger.error('Invalid JSON in daemon response', parseError, { jsonContent: jsonStr.substring(0, 200) });
114
+ buffer = buffer.substring(i + 1); // Skip this invalid JSON
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ // If we didn't find a complete JSON object, wait for more data
121
+ if (braceCount > 0 || jsonStart === -1) {
122
+ break;
123
+ }
124
+ }
125
+ catch (parseError) {
126
+ this.logger.error('JSON parsing error', parseError);
127
+ // Try to find the start of the next JSON object
128
+ const nextStart = buffer.indexOf('{', 1);
129
+ if (nextStart > 0) {
130
+ buffer = buffer.substring(nextStart);
131
+ }
132
+ else {
133
+ buffer = '';
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ catch (error) {
140
+ this.logger.error('Failed to process daemon response', error);
141
+ buffer = ''; // Reset buffer on error
142
+ }
143
+ });
144
+ this.socket.on('error', (error) => {
145
+ this.connected = false;
146
+ // Enhance error messages for common issues
147
+ if (error.code === 'EACCES') {
148
+ error.message = `Permission denied to access daemon socket at ${this.socketPath}. The socket may be owned by another user. Try starting your own daemon with: lsh daemon start`;
149
+ }
150
+ else if (error.code === 'ECONNREFUSED') {
151
+ error.message = `Daemon is not responding at ${this.socketPath}. The daemon may have crashed. Try restarting with: lsh daemon restart`;
152
+ }
153
+ else if (error.code === 'ENOENT') {
154
+ error.message = `Daemon socket not found at ${this.socketPath}. Start the daemon with: lsh daemon start`;
155
+ }
156
+ this.emit('error', error);
157
+ reject(error);
158
+ });
159
+ this.socket.on('close', () => {
160
+ this.connected = false;
161
+ this.emit('disconnected');
162
+ });
163
+ this.socket.connect(this.socketPath);
164
+ });
165
+ }
166
+ /**
167
+ * Disconnect from the daemon
168
+ */
169
+ disconnect() {
170
+ if (this.socket) {
171
+ this.socket.destroy();
172
+ this.socket = undefined;
173
+ }
174
+ this.connected = false;
175
+ }
176
+ /**
177
+ * Send a message to the daemon
178
+ */
179
+ async sendMessage(message) {
180
+ if (!this.connected || !this.socket) {
181
+ throw new Error('Not connected to daemon');
182
+ }
183
+ const id = (++this.messageId).toString();
184
+ message.id = id;
185
+ return new Promise((resolve, reject) => {
186
+ // Set timeout for response (reduced for faster failure detection)
187
+ const timeoutId = setTimeout(() => {
188
+ if (this.pendingMessages.has(id)) {
189
+ this.pendingMessages.delete(id);
190
+ reject(new Error(`Request timeout after 10 seconds for command: ${message.command}`));
191
+ }
192
+ }, 10000); // 10 second timeout
193
+ // Store timeout ID for cleanup
194
+ this.pendingMessages.set(id, {
195
+ resolve: (data) => {
196
+ clearTimeout(timeoutId);
197
+ resolve(data);
198
+ },
199
+ reject: (error) => {
200
+ clearTimeout(timeoutId);
201
+ reject(error);
202
+ }
203
+ });
204
+ this.socket.write(JSON.stringify(message));
205
+ });
206
+ }
207
+ /**
208
+ * Handle response from daemon
209
+ */
210
+ handleResponse(response) {
211
+ if (response.id && this.pendingMessages.has(response.id)) {
212
+ const { resolve, reject } = this.pendingMessages.get(response.id);
213
+ this.pendingMessages.delete(response.id);
214
+ if (response.success) {
215
+ resolve(response.data);
216
+ }
217
+ else {
218
+ reject(new Error(response.error || 'Unknown error'));
219
+ }
220
+ }
221
+ }
222
+ /**
223
+ * Get daemon status
224
+ */
225
+ async getStatus() {
226
+ return await this.sendMessage({ command: 'status' });
227
+ }
228
+ /**
229
+ * Add a simple job to the daemon
230
+ */
231
+ async addJob(jobSpec) {
232
+ return await this.sendMessage({
233
+ command: 'addJob',
234
+ args: { jobSpec }
235
+ });
236
+ }
237
+ /**
238
+ * Create a cron job
239
+ */
240
+ async createCronJob(jobSpec) {
241
+ const daemonJobSpec = {
242
+ id: jobSpec.id,
243
+ name: jobSpec.name,
244
+ command: jobSpec.command,
245
+ type: 'scheduled',
246
+ schedule: jobSpec.schedule,
247
+ env: jobSpec.environment || {},
248
+ cwd: jobSpec.workingDirectory || process.cwd(),
249
+ user: jobSpec.user || process.env.USER,
250
+ priority: jobSpec.priority || 0,
251
+ tags: jobSpec.tags || [],
252
+ enabled: jobSpec.enabled !== false,
253
+ maxRetries: jobSpec.maxRetries || 3,
254
+ timeout: jobSpec.timeout || 0,
255
+ };
256
+ const result = await this.sendMessage({
257
+ command: 'addJob',
258
+ args: { jobSpec: daemonJobSpec }
259
+ });
260
+ // Sync to database if enabled
261
+ if (jobSpec.databaseSync && this.databasePersistence) {
262
+ await this.syncJobToDatabase(jobSpec, 'created');
263
+ }
264
+ return result;
265
+ }
266
+ /**
267
+ * Start a job
268
+ */
269
+ async startJob(jobId) {
270
+ const result = await this.sendMessage({
271
+ command: 'startJob',
272
+ args: { jobId }
273
+ });
274
+ // Sync to database
275
+ if (this.databasePersistence) {
276
+ await this.syncJobToDatabase({ id: jobId }, 'running');
277
+ }
278
+ return result;
279
+ }
280
+ /**
281
+ * Trigger a job to run immediately (bypass schedule)
282
+ */
283
+ async triggerJob(jobId) {
284
+ const result = await this.sendMessage({
285
+ command: 'triggerJob',
286
+ args: { jobId }
287
+ });
288
+ // Record job execution in database
289
+ if (this.databasePersistence) {
290
+ try {
291
+ await this.databasePersistence.saveJob({
292
+ user_id: this.userId,
293
+ session_id: this.sessionId,
294
+ job_id: jobId,
295
+ command: `Triggered execution of ${jobId}`,
296
+ status: result.success ? 'completed' : 'failed',
297
+ working_directory: process.cwd(),
298
+ started_at: new Date().toISOString(),
299
+ completed_at: new Date().toISOString(),
300
+ exit_code: result.success ? 0 : 1,
301
+ output: result.output,
302
+ error: result.error
303
+ });
304
+ }
305
+ catch (error) {
306
+ // Don't fail the trigger if database save fails
307
+ this.logger.warn(`Failed to save job execution to database: ${error.message}`);
308
+ }
309
+ }
310
+ return result;
311
+ }
312
+ /**
313
+ * Stop a job
314
+ */
315
+ async stopJob(jobId, signal = 'SIGTERM') {
316
+ const result = await this.sendMessage({
317
+ command: 'stopJob',
318
+ args: { jobId, signal }
319
+ });
320
+ // Sync to database
321
+ if (this.databasePersistence) {
322
+ await this.syncJobToDatabase({ id: jobId }, 'stopped');
323
+ }
324
+ return result;
325
+ }
326
+ /**
327
+ * List all jobs
328
+ */
329
+ async listJobs(filter) {
330
+ try {
331
+ const result = await this.sendMessage({
332
+ command: 'listJobs',
333
+ args: {
334
+ filter,
335
+ limit: 50 // Limit results to prevent oversized responses
336
+ }
337
+ });
338
+ // Ensure we return an array
339
+ if (Array.isArray(result)) {
340
+ return result;
341
+ }
342
+ else if (result && typeof result === 'object' && Array.isArray(result.jobs)) {
343
+ return result.jobs;
344
+ }
345
+ else {
346
+ this.logger.warn('Unexpected job list format', { resultType: typeof result });
347
+ return [];
348
+ }
349
+ }
350
+ catch (error) {
351
+ this.logger.error('Failed to list jobs', error);
352
+ // Return empty array instead of throwing to prevent crashes
353
+ return [];
354
+ }
355
+ }
356
+ /**
357
+ * Get job details
358
+ */
359
+ async getJob(jobId) {
360
+ return await this.sendMessage({
361
+ command: 'getJob',
362
+ args: { jobId }
363
+ });
364
+ }
365
+ /**
366
+ * Remove a job
367
+ */
368
+ async removeJob(jobId, force = false) {
369
+ const result = await this.sendMessage({
370
+ command: 'removeJob',
371
+ args: { jobId, force }
372
+ });
373
+ // Remove from database
374
+ if (this.databasePersistence) {
375
+ // Note: DatabasePersistence doesn't have a removeJob method yet
376
+ // This would need to be implemented
377
+ }
378
+ return result;
379
+ }
380
+ /**
381
+ * Restart the daemon
382
+ */
383
+ async restartDaemon() {
384
+ await this.sendMessage({ command: 'restart' });
385
+ }
386
+ /**
387
+ * Stop the daemon
388
+ */
389
+ async stopDaemon() {
390
+ await this.sendMessage({ command: 'stop' });
391
+ }
392
+ /**
393
+ * Sync job status to Supabase database
394
+ */
395
+ async syncJobToDatabase(jobSpec, status) {
396
+ if (!this.databasePersistence)
397
+ return;
398
+ try {
399
+ await this.databasePersistence.saveJob({
400
+ session_id: this.databasePersistence.getSessionId(),
401
+ job_id: jobSpec.id,
402
+ command: jobSpec.command,
403
+ status: status,
404
+ working_directory: jobSpec.workingDirectory || process.cwd(),
405
+ started_at: new Date().toISOString(),
406
+ });
407
+ }
408
+ catch (error) {
409
+ this.logger.error('Failed to sync job to database', error);
410
+ }
411
+ }
412
+ /**
413
+ * Create a database-backed cron job
414
+ */
415
+ async createDatabaseCronJob(jobSpec) {
416
+ // Create job in daemon
417
+ const daemonResult = await this.createCronJob({
418
+ ...jobSpec,
419
+ databaseSync: true
420
+ });
421
+ // Create initial database record
422
+ if (this.databasePersistence) {
423
+ await this.databasePersistence.saveJob({
424
+ session_id: this.databasePersistence.getSessionId(),
425
+ job_id: jobSpec.id,
426
+ command: jobSpec.command,
427
+ status: 'running',
428
+ working_directory: jobSpec.workingDirectory || process.cwd(),
429
+ started_at: new Date().toISOString(),
430
+ });
431
+ }
432
+ return daemonResult;
433
+ }
434
+ /**
435
+ * Get job execution history from database
436
+ */
437
+ async getJobHistory(jobId, limit = 100) {
438
+ if (!this.databasePersistence) {
439
+ throw new Error('Database persistence not configured');
440
+ }
441
+ const jobs = await this.databasePersistence.getActiveJobs();
442
+ if (jobId) {
443
+ return jobs.filter(job => job.job_id === jobId);
444
+ }
445
+ return jobs.slice(0, limit);
446
+ }
447
+ /**
448
+ * Get job statistics from database
449
+ */
450
+ async getJobStatistics(jobId) {
451
+ if (!this.databasePersistence) {
452
+ throw new Error('Database persistence not configured');
453
+ }
454
+ const jobs = await this.databasePersistence.getActiveJobs();
455
+ if (jobId) {
456
+ const jobJobs = jobs.filter(job => job.job_id === jobId);
457
+ return this.calculateJobStatistics(jobJobs);
458
+ }
459
+ return this.calculateJobStatistics(jobs);
460
+ }
461
+ /**
462
+ * Calculate job statistics
463
+ */
464
+ calculateJobStatistics(jobs) {
465
+ const total = jobs.length;
466
+ const byStatus = jobs.reduce((acc, job) => {
467
+ acc[job.status] = (acc[job.status] || 0) + 1;
468
+ return acc;
469
+ }, {});
470
+ const successRate = byStatus.completed ? (byStatus.completed / total) * 100 : 0;
471
+ return {
472
+ totalJobs: total,
473
+ byStatus,
474
+ successRate,
475
+ lastExecution: jobs.length > 0 ? jobs[0].started_at : null,
476
+ };
477
+ }
478
+ /**
479
+ * Check if daemon is running
480
+ */
481
+ isDaemonRunning() {
482
+ if (!fs.existsSync(this.socketPath)) {
483
+ return false;
484
+ }
485
+ try {
486
+ // Try to access the socket to verify it's working
487
+ fs.accessSync(this.socketPath, fs.constants.R_OK | fs.constants.W_OK);
488
+ return true;
489
+ }
490
+ catch (_error) {
491
+ return false;
492
+ }
493
+ }
494
+ /**
495
+ * Get connection status
496
+ */
497
+ isConnected() {
498
+ return this.connected;
499
+ }
500
+ }
501
+ export default DaemonClient;