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