lsh-framework 3.2.5 → 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 +49 -38
  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,556 +0,0 @@
1
- /**
2
- * Job Registry - Comprehensive job execution history and analytics
3
- * Tracks all job runs with detailed pass/failure history, output logs, and performance metrics
4
- *
5
- * REFACTORED: Now extends BaseJobManager for unified interface
6
- * Note: This is a read-only tracker - startJob/stopJob only record events
7
- */
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import * as os from 'os';
11
- import { exec } from 'child_process';
12
- import { BaseJobManager } from '../lib/base-job-manager.js';
13
- import MemoryJobStorage from '../lib/job-storage-memory.js';
14
- import { ENV_VARS, DEFAULTS, PATHS } from '../constants/index.js';
15
- export class JobRegistry extends BaseJobManager {
16
- config;
17
- records = new Map(); // jobId -> execution records
18
- index = new Map(); // tag -> jobIds
19
- statistics = new Map(); // jobId -> stats
20
- constructor(config) {
21
- super(new MemoryJobStorage(), 'JobRegistry');
22
- this.config = {
23
- registryFile: PATHS.JOB_REGISTRY_FILE,
24
- maxRecordsPerJob: DEFAULTS.MAX_RECORDS_PER_JOB,
25
- maxTotalRecords: DEFAULTS.MAX_TOTAL_RECORDS,
26
- compressionEnabled: true,
27
- metricsRetentionDays: DEFAULTS.METRICS_RETENTION_DAYS,
28
- outputLogDir: PATHS.JOB_LOGS_DIR,
29
- indexingEnabled: true,
30
- ...config
31
- };
32
- this.ensureLogDirectory();
33
- this.loadRegistry();
34
- }
35
- /**
36
- * Record the start of a job execution
37
- */
38
- recordJobStart(job, executionId) {
39
- const record = {
40
- executionId: executionId || this.generateExecutionId(),
41
- jobId: job.id,
42
- jobName: job.name,
43
- command: job.command,
44
- startTime: new Date(),
45
- status: 'running',
46
- stdout: '',
47
- stderr: '',
48
- outputSize: 0,
49
- environment: { ...(job.env || {}) },
50
- workingDirectory: job.cwd || process.cwd(),
51
- user: job.user || process.env[ENV_VARS.USER] || 'unknown',
52
- hostname: os.hostname(),
53
- tags: [...(job.tags || [])],
54
- priority: job.priority || 5,
55
- scheduled: job.type === 'scheduled',
56
- retryCount: 0,
57
- pid: job.pid,
58
- ppid: job.ppid
59
- };
60
- // Store output logs in separate files for large outputs
61
- if (this.config.outputLogDir) {
62
- record.logFile = path.join(this.config.outputLogDir, `${record.executionId}.log`);
63
- }
64
- this.addRecord(record);
65
- this.emit('executionStarted', record);
66
- return record;
67
- }
68
- /**
69
- * Record job output (stdout/stderr)
70
- */
71
- recordJobOutput(executionId, type, data) {
72
- const record = this.findRecordByExecutionId(executionId);
73
- if (!record)
74
- return;
75
- if (type === 'stdout') {
76
- record.stdout += data;
77
- }
78
- else {
79
- record.stderr += data;
80
- }
81
- record.outputSize += data.length;
82
- // Write to log file if configured
83
- if (record.logFile) {
84
- const logEntry = `[${new Date().toISOString()}] ${type.toUpperCase()}: ${data}`;
85
- fs.appendFileSync(record.logFile, logEntry);
86
- }
87
- this.emit('outputRecorded', executionId, type, data);
88
- }
89
- /**
90
- * Record job completion
91
- */
92
- recordJobCompletion(executionId, status, exitCode, signal, error) {
93
- const record = this.findRecordByExecutionId(executionId);
94
- if (!record)
95
- return;
96
- record.endTime = new Date();
97
- record.duration = record.endTime.getTime() - record.startTime.getTime();
98
- record.status = status;
99
- record.exitCode = exitCode;
100
- record.signal = signal;
101
- if (error) {
102
- record.errorType = error.constructor.name;
103
- record.errorMessage = error.message;
104
- record.stackTrace = error.stack;
105
- }
106
- // Record resource usage
107
- this.recordResourceUsage(record);
108
- this.updateJobStatistics(record);
109
- this.saveRegistry();
110
- this.emit('executionCompleted', record);
111
- }
112
- /**
113
- * Get execution history for a job - overrides BaseJobManager
114
- * Returns JobExecutionRecord[] which is compatible with BaseJobExecution[]
115
- */
116
- async getJobHistory(jobId, limit = 50) {
117
- const records = this.records.get(jobId) || [];
118
- return limit ? records.slice(0, limit) : records;
119
- }
120
- /**
121
- * Get job statistics - overrides BaseJobManager
122
- * Returns JobStatistics which is compatible with BaseJobStatistics
123
- */
124
- async getJobStatistics(jobId) {
125
- const stats = this.statistics.get(jobId);
126
- if (!stats) {
127
- throw new Error(`No statistics found for job ${jobId}`);
128
- }
129
- return stats;
130
- }
131
- /**
132
- * Get all job statistics
133
- */
134
- getAllStatistics() {
135
- return Array.from(this.statistics.values());
136
- }
137
- /**
138
- * Search job executions
139
- */
140
- searchExecutions(criteria) {
141
- let results = [];
142
- // Collect all records
143
- for (const records of this.records.values()) {
144
- results.push(...records);
145
- }
146
- // Apply filters
147
- if (criteria.jobId) {
148
- results = results.filter(r => r.jobId === criteria.jobId);
149
- }
150
- if (criteria.status) {
151
- results = results.filter(r => criteria.status.includes(r.status));
152
- }
153
- if (criteria.startTime) {
154
- if (criteria.startTime.from) {
155
- results = results.filter(r => r.startTime >= criteria.startTime.from);
156
- }
157
- if (criteria.startTime.to) {
158
- results = results.filter(r => r.startTime <= criteria.startTime.to);
159
- }
160
- }
161
- if (criteria.duration) {
162
- if (criteria.duration.min && criteria.duration.max) {
163
- results = results.filter(r => r.duration &&
164
- r.duration >= criteria.duration.min &&
165
- r.duration <= criteria.duration.max);
166
- }
167
- }
168
- if (criteria.tags && criteria.tags.length > 0) {
169
- results = results.filter(r => criteria.tags.some(tag => r.tags.includes(tag)));
170
- }
171
- if (criteria.user) {
172
- results = results.filter(r => r.user === criteria.user);
173
- }
174
- if (criteria.command) {
175
- results = results.filter(r => criteria.command.test(r.command));
176
- }
177
- if (criteria.exitCode) {
178
- results = results.filter(r => r.exitCode !== undefined &&
179
- criteria.exitCode.includes(r.exitCode));
180
- }
181
- // Sort by start time (newest first)
182
- results.sort((a, b) => b.startTime.getTime() - a.startTime.getTime());
183
- // Apply limit
184
- if (criteria.limit) {
185
- results = results.slice(0, criteria.limit);
186
- }
187
- return results;
188
- }
189
- /**
190
- * Generate execution report
191
- */
192
- async generateReport(options = {}) {
193
- const { jobId, timeRange, format = 'text' } = options;
194
- let records = [];
195
- if (jobId) {
196
- records = await this.getJobHistory(jobId);
197
- }
198
- else {
199
- for (const jobRecords of this.records.values()) {
200
- records.push(...jobRecords);
201
- }
202
- }
203
- if (timeRange) {
204
- records = records.filter(r => r.startTime >= timeRange.from && r.startTime <= timeRange.to);
205
- }
206
- switch (format) {
207
- case 'json':
208
- return JSON.stringify(records, null, 2);
209
- case 'csv':
210
- return this.generateCSVReport(records);
211
- case 'text':
212
- default:
213
- return this.generateTextReport(records);
214
- }
215
- }
216
- /**
217
- * Clean old records - overrides BaseJobManager
218
- */
219
- async cleanup() {
220
- const cutoffDate = new Date();
221
- cutoffDate.setDate(cutoffDate.getDate() - this.config.metricsRetentionDays);
222
- let removedCount = 0;
223
- for (const [jobId, records] of this.records) {
224
- const filteredRecords = records.filter(r => r.startTime >= cutoffDate);
225
- const removed = records.length - filteredRecords.length;
226
- if (removed > 0) {
227
- this.records.set(jobId, filteredRecords);
228
- removedCount += removed;
229
- // Clean up log files for removed records
230
- records
231
- .filter(r => r.startTime < cutoffDate)
232
- .forEach(r => {
233
- if (r.logFile && fs.existsSync(r.logFile)) {
234
- fs.unlinkSync(r.logFile);
235
- }
236
- });
237
- }
238
- // Limit records per job
239
- if (filteredRecords.length > this.config.maxRecordsPerJob) {
240
- const limited = filteredRecords.slice(0, this.config.maxRecordsPerJob);
241
- this.records.set(jobId, limited);
242
- removedCount += filteredRecords.length - limited.length;
243
- }
244
- }
245
- this.saveRegistry();
246
- this.logger.info(`Cleaned up ${removedCount} old records`);
247
- // Call base cleanup
248
- await super.cleanup();
249
- }
250
- /**
251
- * Export registry data
252
- */
253
- export(filePath, format = 'json') {
254
- const allRecords = Array.from(this.records.values()).flat();
255
- let content;
256
- if (format === 'csv') {
257
- content = this.generateCSVReport(allRecords);
258
- }
259
- else {
260
- content = JSON.stringify({
261
- records: allRecords,
262
- statistics: Array.from(this.statistics.values()),
263
- exportedAt: new Date().toISOString()
264
- }, null, 2);
265
- }
266
- fs.writeFileSync(filePath, content);
267
- }
268
- addRecord(record) {
269
- const jobRecords = this.records.get(record.jobId) || [];
270
- jobRecords.unshift(record); // Add to beginning (newest first)
271
- // Limit records per job
272
- if (jobRecords.length > this.config.maxRecordsPerJob) {
273
- const removed = jobRecords.splice(this.config.maxRecordsPerJob);
274
- // Clean up log files for removed records
275
- removed.forEach(r => {
276
- if (r.logFile && fs.existsSync(r.logFile)) {
277
- fs.unlinkSync(r.logFile);
278
- }
279
- });
280
- }
281
- this.records.set(record.jobId, jobRecords);
282
- // Update index
283
- if (this.config.indexingEnabled) {
284
- record.tags.forEach(tag => {
285
- const jobIds = this.index.get(tag) || new Set();
286
- jobIds.add(record.jobId);
287
- this.index.set(tag, jobIds);
288
- });
289
- }
290
- }
291
- findRecordByExecutionId(executionId) {
292
- for (const records of this.records.values()) {
293
- const record = records.find(r => r.executionId === executionId);
294
- if (record)
295
- return record;
296
- }
297
- return undefined;
298
- }
299
- updateJobStatistics(record) {
300
- const stats = this.statistics.get(record.jobId) || this.createInitialStatistics(record);
301
- stats.totalExecutions++;
302
- switch (record.status) {
303
- case 'completed':
304
- stats.successfulExecutions++;
305
- stats.lastSuccess = record.endTime;
306
- break;
307
- case 'failed':
308
- stats.failedExecutions++;
309
- stats.lastFailure = record.endTime;
310
- this.updateFailureAnalysis(stats, record);
311
- break;
312
- case 'killed':
313
- stats.killedExecutions++;
314
- break;
315
- }
316
- stats.successRate = (stats.successfulExecutions / stats.totalExecutions) * 100;
317
- stats.lastExecution = record.endTime;
318
- // Update timing statistics
319
- if (record.duration) {
320
- stats.totalRuntime += record.duration;
321
- stats.averageDuration = stats.totalRuntime / stats.totalExecutions;
322
- if (stats.minDuration === 0 || record.duration < stats.minDuration) {
323
- stats.minDuration = record.duration;
324
- }
325
- if (record.duration > stats.maxDuration) {
326
- stats.maxDuration = record.duration;
327
- }
328
- }
329
- // Update resource statistics
330
- if (record.maxMemory) {
331
- stats.averageMemory = (stats.averageMemory * (stats.totalExecutions - 1) + record.maxMemory) / stats.totalExecutions;
332
- if (record.maxMemory > stats.maxMemoryUsed) {
333
- stats.maxMemoryUsed = record.maxMemory;
334
- }
335
- }
336
- if (record.avgCpu) {
337
- stats.averageCpuUsage = (stats.averageCpuUsage * (stats.totalExecutions - 1) + record.avgCpu) / stats.totalExecutions;
338
- }
339
- // Determine trend
340
- stats.recentTrend = this.calculateTrend(record.jobId);
341
- this.statistics.set(record.jobId, stats);
342
- }
343
- createInitialStatistics(record) {
344
- return {
345
- jobId: record.jobId,
346
- jobName: record.jobName,
347
- totalExecutions: 0,
348
- successfulExecutions: 0,
349
- failedExecutions: 0,
350
- killedExecutions: 0,
351
- successRate: 0,
352
- averageDuration: 0,
353
- minDuration: 0,
354
- maxDuration: 0,
355
- totalRuntime: 0,
356
- averageMemory: 0,
357
- maxMemoryUsed: 0,
358
- averageCpuUsage: 0,
359
- recentTrend: 'stable',
360
- commonFailures: []
361
- };
362
- }
363
- updateFailureAnalysis(stats, record) {
364
- if (!record.errorMessage)
365
- return;
366
- const existing = stats.commonFailures.find(f => f.error === record.errorMessage);
367
- if (existing) {
368
- existing.count++;
369
- }
370
- else {
371
- stats.commonFailures.push({
372
- error: record.errorMessage,
373
- count: 1,
374
- percentage: 0
375
- });
376
- }
377
- // Update percentages
378
- const totalFailures = stats.commonFailures.reduce((sum, f) => sum + f.count, 0);
379
- stats.commonFailures.forEach(f => {
380
- f.percentage = (f.count / totalFailures) * 100;
381
- });
382
- // Sort by frequency
383
- stats.commonFailures.sort((a, b) => b.count - a.count);
384
- // Keep only top 10 failure types
385
- stats.commonFailures = stats.commonFailures.slice(0, 10);
386
- }
387
- calculateTrend(jobId) {
388
- const records = this.records.get(jobId) || [];
389
- if (records.length < 5)
390
- return 'stable';
391
- const recent = records.slice(0, 5);
392
- const successCount = recent.filter(r => r.status === 'completed').length;
393
- const _failureCount = recent.filter(r => r.status === 'failed').length;
394
- const recentSuccessRate = successCount / recent.length;
395
- const overallStats = this.statistics.get(jobId);
396
- if (!overallStats)
397
- return 'stable';
398
- const overallSuccessRate = overallStats.successRate / 100;
399
- if (recentSuccessRate > overallSuccessRate + 0.1) {
400
- return 'improving';
401
- }
402
- else if (recentSuccessRate < overallSuccessRate - 0.1) {
403
- return 'degrading';
404
- }
405
- else {
406
- return 'stable';
407
- }
408
- }
409
- recordResourceUsage(record) {
410
- if (!record.pid)
411
- return;
412
- try {
413
- exec(`ps -p ${record.pid} -o %mem,%cpu`, (error, stdout) => {
414
- if (!error && stdout) {
415
- const lines = stdout.trim().split('\n');
416
- if (lines.length > 1) {
417
- const [mem, cpu] = lines[1].trim().split(/\s+/).map(parseFloat);
418
- record.maxMemory = mem;
419
- record.avgCpu = cpu;
420
- }
421
- }
422
- });
423
- }
424
- catch (_error) {
425
- // Ignore resource monitoring errors
426
- }
427
- }
428
- generateExecutionId() {
429
- return `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
430
- }
431
- generateTextReport(records) {
432
- let report = `Job Execution Report\n`;
433
- report += `Generated: ${new Date().toISOString()}\n`;
434
- report += `Total Executions: ${records.length}\n\n`;
435
- const byStatus = records.reduce((acc, r) => {
436
- acc[r.status] = (acc[r.status] || 0) + 1;
437
- return acc;
438
- }, {});
439
- report += `Status Summary:\n`;
440
- Object.entries(byStatus).forEach(([status, count]) => {
441
- report += ` ${status}: ${count}\n`;
442
- });
443
- report += `\nRecent Executions:\n`;
444
- records.slice(0, 20).forEach(r => {
445
- report += `${r.startTime.toISOString()} | ${r.jobName} | ${r.status} | ${r.duration || 0}ms\n`;
446
- });
447
- return report;
448
- }
449
- generateCSVReport(records) {
450
- const headers = [
451
- 'executionId', 'jobId', 'jobName', 'command', 'startTime', 'endTime',
452
- 'duration', 'status', 'exitCode', 'user', 'hostname', 'outputSize'
453
- ];
454
- let csv = headers.join(',') + '\n';
455
- records.forEach(r => {
456
- const values = [
457
- r.executionId,
458
- r.jobId,
459
- r.jobName,
460
- `"${r.command}"`,
461
- r.startTime.toISOString(),
462
- r.endTime?.toISOString() || '',
463
- r.duration || '',
464
- r.status,
465
- r.exitCode || '',
466
- r.user,
467
- r.hostname,
468
- r.outputSize
469
- ];
470
- csv += values.join(',') + '\n';
471
- });
472
- return csv;
473
- }
474
- ensureLogDirectory() {
475
- if (!fs.existsSync(this.config.outputLogDir)) {
476
- fs.mkdirSync(this.config.outputLogDir, { recursive: true });
477
- }
478
- }
479
- loadRegistry() {
480
- try {
481
- if (fs.existsSync(this.config.registryFile)) {
482
- const data = JSON.parse(fs.readFileSync(this.config.registryFile, 'utf8'));
483
- // Restore records
484
- if (data.records) {
485
- for (const [jobId, records] of Object.entries(data.records)) {
486
- const serializedRecords = records;
487
- this.records.set(jobId, serializedRecords.map(r => ({
488
- ...r,
489
- startTime: new Date(r.startTime),
490
- endTime: r.endTime ? new Date(r.endTime) : undefined
491
- })));
492
- }
493
- }
494
- // Restore statistics
495
- if (data.statistics) {
496
- for (const [jobId, stats] of Object.entries(data.statistics)) {
497
- const s = stats;
498
- this.statistics.set(jobId, {
499
- ...s,
500
- lastExecution: s.lastExecution ? new Date(s.lastExecution) : undefined,
501
- lastSuccess: s.lastSuccess ? new Date(s.lastSuccess) : undefined,
502
- lastFailure: s.lastFailure ? new Date(s.lastFailure) : undefined
503
- });
504
- }
505
- }
506
- }
507
- }
508
- catch (error) {
509
- this.logger.error('Failed to load job registry', error);
510
- }
511
- }
512
- saveRegistry() {
513
- try {
514
- const data = {
515
- records: Object.fromEntries(this.records),
516
- statistics: Object.fromEntries(this.statistics),
517
- savedAt: new Date().toISOString()
518
- };
519
- fs.writeFileSync(this.config.registryFile, JSON.stringify(data, null, 2));
520
- }
521
- catch (error) {
522
- this.logger.error('Failed to save job registry', error);
523
- }
524
- }
525
- /**
526
- * Start job - implements BaseJobManager abstract method
527
- * JobRegistry is read-only, so this records the start event
528
- */
529
- async startJob(jobId) {
530
- const job = await this.getJob(jobId);
531
- if (!job) {
532
- throw new Error(`Job ${jobId} not found in registry`);
533
- }
534
- // Record execution start
535
- this.recordJobStart(job);
536
- // Update job status
537
- return await this.updateJobStatus(jobId, 'running', {
538
- startedAt: new Date(),
539
- });
540
- }
541
- /**
542
- * Stop job - implements BaseJobManager abstract method
543
- * JobRegistry is read-only, so this records the stop event
544
- */
545
- async stopJob(jobId, _signal) {
546
- const job = await this.getJob(jobId);
547
- if (!job) {
548
- throw new Error(`Job ${jobId} not found in registry`);
549
- }
550
- // Update job status
551
- return await this.updateJobStatus(jobId, 'stopped', {
552
- completedAt: new Date(),
553
- });
554
- }
555
- }
556
- export default JobRegistry;