lsh-framework 3.2.4 → 3.5.0

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