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,386 @@
1
+ /**
2
+ * Job Management System for LSH Shell
3
+ * Supports CRUD operations on shell jobs and system processes
4
+ *
5
+ * REFACTORED: Now extends BaseJobManager to eliminate duplication
6
+ */
7
+ import { spawn, exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import * as fs from 'fs';
10
+ import { BaseJobManager, } from './base-job-manager.js';
11
+ import MemoryJobStorage from './job-storage-memory.js';
12
+ const execAsync = promisify(exec);
13
+ export class JobManager extends BaseJobManager {
14
+ nextJobId = 1;
15
+ persistenceFile;
16
+ schedulerInterval;
17
+ constructor(persistenceFile = '/tmp/lsh-jobs.json') {
18
+ super(new MemoryJobStorage(), 'JobManager');
19
+ this.persistenceFile = persistenceFile;
20
+ this.loadPersistedJobs();
21
+ this.startScheduler();
22
+ this.setupCleanupHandlers();
23
+ }
24
+ /**
25
+ * Start a job (execute it as a process)
26
+ */
27
+ async startJob(jobId) {
28
+ const baseJob = await this.getJob(jobId);
29
+ if (!baseJob) {
30
+ throw new Error(`Job ${jobId} not found`);
31
+ }
32
+ const job = baseJob;
33
+ if (job.status === 'running') {
34
+ throw new Error(`Job ${jobId} is already running`);
35
+ }
36
+ try {
37
+ // Spawn the process
38
+ if (job.type === 'shell') {
39
+ job.process = spawn('sh', ['-c', job.command], {
40
+ cwd: job.cwd,
41
+ env: job.env,
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ });
44
+ }
45
+ else {
46
+ const [cmd, ...args] = job.command.split(' ');
47
+ job.process = spawn(cmd, args.concat(job.args || []), {
48
+ cwd: job.cwd,
49
+ env: job.env,
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ });
52
+ }
53
+ job.pid = job.process.pid;
54
+ // Handle output
55
+ job.process.stdout?.on('data', (data) => {
56
+ job.stdout = (job.stdout || '') + data.toString();
57
+ if (job.logFile) {
58
+ fs.appendFileSync(job.logFile, data);
59
+ }
60
+ this.emit('jobOutput', job.id, 'stdout', data.toString());
61
+ });
62
+ job.process.stderr?.on('data', (data) => {
63
+ job.stderr = (job.stderr || '') + data.toString();
64
+ if (job.logFile) {
65
+ fs.appendFileSync(job.logFile, data);
66
+ }
67
+ this.emit('jobOutput', job.id, 'stderr', data.toString());
68
+ });
69
+ // Handle completion
70
+ job.process.on('exit', (code, signal) => {
71
+ const status = code === 0 ? 'completed' : (signal === 'SIGKILL' ? 'killed' : 'failed');
72
+ this.updateJobStatus(job.id, status, {
73
+ completedAt: new Date(),
74
+ exitCode: code || undefined,
75
+ });
76
+ this.emit('jobCompleted', job, code, signal);
77
+ this.persistJobs();
78
+ });
79
+ // Set timeout if specified
80
+ if (job.timeout) {
81
+ job.timer = setTimeout(() => {
82
+ this.stopJob(job.id, 'SIGKILL');
83
+ }, job.timeout);
84
+ }
85
+ // Update status to running
86
+ const updatedJob = await this.updateJobStatus(job.id, 'running', {
87
+ startedAt: new Date(),
88
+ pid: job.pid,
89
+ });
90
+ await this.persistJobs();
91
+ return updatedJob;
92
+ }
93
+ catch (error) {
94
+ await this.updateJobStatus(job.id, 'failed', {
95
+ completedAt: new Date(),
96
+ stderr: error.message,
97
+ });
98
+ this.emit('jobFailed', job, error);
99
+ await this.persistJobs();
100
+ throw error;
101
+ }
102
+ }
103
+ /**
104
+ * Stop a running job
105
+ */
106
+ async stopJob(jobId, signal = 'SIGTERM') {
107
+ const baseJob = await this.getJob(jobId);
108
+ if (!baseJob) {
109
+ throw new Error(`Job ${jobId} not found`);
110
+ }
111
+ const job = baseJob;
112
+ if (job.status !== 'running') {
113
+ throw new Error(`Job ${jobId} is not running`);
114
+ }
115
+ if (!job.process || !job.pid) {
116
+ throw new Error(`Job ${jobId} has no associated process`);
117
+ }
118
+ // Clear timeout if exists
119
+ if (job.timer) {
120
+ clearTimeout(job.timer);
121
+ job.timer = undefined;
122
+ }
123
+ // Kill the process
124
+ try {
125
+ job.process.kill(signal);
126
+ }
127
+ catch (error) {
128
+ this.logger.error(`Failed to kill job ${jobId}`, error);
129
+ }
130
+ // Update status
131
+ const updatedJob = await this.updateJobStatus(jobId, 'stopped', {
132
+ completedAt: new Date(),
133
+ });
134
+ await this.persistJobs();
135
+ return updatedJob;
136
+ }
137
+ /**
138
+ * Create and immediately start a job
139
+ */
140
+ async runJob(spec) {
141
+ const job = await this.createJob(spec);
142
+ return await this.startJob(job.id);
143
+ }
144
+ /**
145
+ * Pause a job (stop it but keep for later resumption)
146
+ */
147
+ async pauseJob(jobId) {
148
+ await this.stopJob(jobId, 'SIGSTOP');
149
+ return await this.updateJobStatus(jobId, 'paused');
150
+ }
151
+ /**
152
+ * Resume a paused job
153
+ */
154
+ async resumeJob(jobId) {
155
+ const baseJob = await this.getJob(jobId);
156
+ if (!baseJob) {
157
+ throw new Error(`Job ${jobId} not found`);
158
+ }
159
+ const job = baseJob;
160
+ if (job.status !== 'paused') {
161
+ throw new Error(`Job ${jobId} is not paused`);
162
+ }
163
+ if (!job.process || !job.pid) {
164
+ throw new Error(`Job ${jobId} has no associated process`);
165
+ }
166
+ // Send SIGCONT to resume
167
+ try {
168
+ job.process.kill('SIGCONT');
169
+ return await this.updateJobStatus(jobId, 'running');
170
+ }
171
+ catch (error) {
172
+ throw new Error(`Failed to resume job ${jobId}: ${error}`);
173
+ }
174
+ }
175
+ /**
176
+ * Kill a job forcefully
177
+ */
178
+ async killJob(jobId, signal = 'SIGKILL') {
179
+ return await this.stopJob(jobId, signal);
180
+ }
181
+ /**
182
+ * Monitor a job's resource usage
183
+ */
184
+ async monitorJob(jobId) {
185
+ const baseJob = await this.getJob(jobId);
186
+ if (!baseJob) {
187
+ throw new Error(`Job ${jobId} not found`);
188
+ }
189
+ const job = baseJob;
190
+ if (!job.pid) {
191
+ throw new Error(`Job ${jobId} is not running`);
192
+ }
193
+ try {
194
+ const { stdout } = await execAsync(`ps -p ${job.pid} -o pid,ppid,pcpu,pmem,etime,state`);
195
+ const lines = stdout.split('\n');
196
+ if (lines.length < 2) {
197
+ return null; // Process not found
198
+ }
199
+ const parts = lines[1].trim().split(/\s+/);
200
+ const monitoring = {
201
+ pid: parseInt(parts[0]),
202
+ ppid: parseInt(parts[1]),
203
+ cpu: parseFloat(parts[2]),
204
+ memory: parseFloat(parts[3]),
205
+ elapsed: parts[4],
206
+ state: parts[5],
207
+ timestamp: new Date(),
208
+ };
209
+ // Update job with current resource usage
210
+ job.cpuUsage = monitoring.cpu;
211
+ job.memoryUsage = monitoring.memory;
212
+ this.emit('jobMonitoring', job, monitoring);
213
+ return monitoring;
214
+ }
215
+ catch (_error) {
216
+ return null; // Process likely terminated
217
+ }
218
+ }
219
+ /**
220
+ * Get system processes
221
+ */
222
+ async getSystemProcesses() {
223
+ try {
224
+ const { stdout } = await execAsync('ps -eo pid,ppid,user,pcpu,pmem,lstart,comm,args');
225
+ const lines = stdout.split('\n').slice(1); // Skip header
226
+ return lines
227
+ .filter(line => line.trim())
228
+ .map(line => {
229
+ const parts = line.trim().split(/\s+/);
230
+ return {
231
+ pid: parseInt(parts[0]),
232
+ ppid: parseInt(parts[1]),
233
+ user: parts[2],
234
+ cpu: parseFloat(parts[3]),
235
+ memory: parseFloat(parts[4]),
236
+ startTime: new Date(parts.slice(5, 9).join(' ')),
237
+ name: parts[9],
238
+ command: parts.slice(10).join(' ') || parts[9],
239
+ status: 'running'
240
+ };
241
+ });
242
+ }
243
+ catch (error) {
244
+ this.logger.error('Failed to get system processes', error);
245
+ return [];
246
+ }
247
+ }
248
+ /**
249
+ * Get job statistics
250
+ */
251
+ getJobStats() {
252
+ const jobs = Array.from(this.jobs.values());
253
+ const stats = {
254
+ total: jobs.length,
255
+ byStatus: {},
256
+ byType: {},
257
+ running: jobs.filter(j => j.status === 'running').length,
258
+ completed: jobs.filter(j => j.status === 'completed').length,
259
+ failed: jobs.filter(j => j.status === 'failed').length,
260
+ };
261
+ jobs.forEach(job => {
262
+ stats.byStatus[job.status] = (stats.byStatus[job.status] || 0) + 1;
263
+ stats.byType[job.type] = (stats.byType[job.type] || 0) + 1;
264
+ });
265
+ return stats;
266
+ }
267
+ /**
268
+ * Clean up old jobs
269
+ */
270
+ async cleanupJobs(olderThanHours = 24) {
271
+ const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
272
+ const jobs = await this.listJobs();
273
+ let cleaned = 0;
274
+ for (const job of jobs) {
275
+ if (job.status === 'completed' || job.status === 'failed') {
276
+ if (job.completedAt && job.completedAt < cutoff) {
277
+ await this.removeJob(job.id, true);
278
+ cleaned++;
279
+ }
280
+ }
281
+ }
282
+ this.logger.info(`Cleaned up ${cleaned} old jobs`);
283
+ return cleaned;
284
+ }
285
+ // ================================
286
+ // PRIVATE: Persistence & Scheduling
287
+ // ================================
288
+ async loadPersistedJobs() {
289
+ try {
290
+ if (fs.existsSync(this.persistenceFile)) {
291
+ const data = fs.readFileSync(this.persistenceFile, 'utf8');
292
+ const persistedJobs = JSON.parse(data);
293
+ for (const job of persistedJobs) {
294
+ // Convert date strings back to Date objects
295
+ job.createdAt = new Date(job.createdAt);
296
+ if (job.startedAt)
297
+ job.startedAt = new Date(job.startedAt);
298
+ if (job.completedAt)
299
+ job.completedAt = new Date(job.completedAt);
300
+ // Don't restore running processes - mark them as stopped
301
+ if (job.status === 'running') {
302
+ job.status = 'stopped';
303
+ }
304
+ await this.storage.save(job);
305
+ this.jobs.set(job.id, job);
306
+ }
307
+ this.logger.info(`Loaded ${persistedJobs.length} persisted jobs`);
308
+ }
309
+ }
310
+ catch (error) {
311
+ this.logger.error('Failed to load persisted jobs', error);
312
+ }
313
+ }
314
+ async persistJobs() {
315
+ try {
316
+ const jobs = Array.from(this.jobs.values()).map(job => {
317
+ const { process: _process, timer: _timer, ...serializable } = job;
318
+ return serializable;
319
+ });
320
+ fs.writeFileSync(this.persistenceFile, JSON.stringify(jobs, null, 2));
321
+ }
322
+ catch (error) {
323
+ this.logger.error('Failed to persist jobs', error);
324
+ }
325
+ }
326
+ startScheduler() {
327
+ // Check for scheduled jobs every minute
328
+ this.schedulerInterval = setInterval(() => {
329
+ this.checkScheduledJobs();
330
+ }, 60000);
331
+ // Run immediately on startup
332
+ this.checkScheduledJobs();
333
+ }
334
+ async checkScheduledJobs() {
335
+ const jobs = await this.listJobs({ status: 'created' });
336
+ const now = new Date();
337
+ for (const job of jobs) {
338
+ if (job.schedule?.nextRun && job.schedule.nextRun <= now) {
339
+ this.logger.info(`Starting scheduled job: ${job.id}`);
340
+ try {
341
+ await this.startJob(job.id);
342
+ // Calculate next run time
343
+ if (job.schedule.interval) {
344
+ job.schedule.nextRun = new Date(now.getTime() + job.schedule.interval);
345
+ await this.updateJob(job.id, { schedule: job.schedule });
346
+ }
347
+ }
348
+ catch (error) {
349
+ this.logger.error(`Failed to start scheduled job ${job.id}`, error);
350
+ }
351
+ }
352
+ }
353
+ }
354
+ setupCleanupHandlers() {
355
+ const cleanup = async () => {
356
+ this.logger.info('JobManager shutting down...');
357
+ if (this.schedulerInterval) {
358
+ clearInterval(this.schedulerInterval);
359
+ }
360
+ // Stop all running jobs
361
+ const jobs = await this.listJobs({ status: 'running' });
362
+ for (const job of jobs) {
363
+ try {
364
+ await this.stopJob(job.id);
365
+ }
366
+ catch (error) {
367
+ this.logger.error(`Failed to stop job ${job.id}`, error);
368
+ }
369
+ }
370
+ await this.persistJobs();
371
+ await this.cleanup();
372
+ };
373
+ process.on('SIGTERM', cleanup);
374
+ process.on('SIGINT', cleanup);
375
+ }
376
+ /**
377
+ * Override cleanup to include scheduler
378
+ */
379
+ async cleanup() {
380
+ if (this.schedulerInterval) {
381
+ clearInterval(this.schedulerInterval);
382
+ }
383
+ await super.cleanup();
384
+ }
385
+ }
386
+ export default JobManager;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Database Job Storage
3
+ * Persistent storage for jobs and executions using DatabasePersistence
4
+ * Used by CronJobManager and other database-backed managers
5
+ */
6
+ import DatabasePersistence from './database-persistence.js';
7
+ export class DatabaseJobStorage {
8
+ persistence;
9
+ userId;
10
+ constructor(userId) {
11
+ this.userId = userId;
12
+ this.persistence = new DatabasePersistence(userId);
13
+ }
14
+ async save(job) {
15
+ // Map BaseJobSpec to database format
16
+ const dbJob = {
17
+ job_id: job.id,
18
+ command: job.command,
19
+ started_at: job.startedAt?.toISOString(),
20
+ completed_at: job.completedAt?.toISOString(),
21
+ status: job.status,
22
+ exit_code: job.exitCode,
23
+ output: job.stdout,
24
+ error: job.stderr,
25
+ };
26
+ // Save using available method
27
+ await this.persistence.saveJob(dbJob);
28
+ }
29
+ async get(_jobId) {
30
+ // This would require adding a method to DatabasePersistence
31
+ // For now, return null and rely on list() filtering
32
+ return null;
33
+ }
34
+ async list(_filter) {
35
+ // Get active jobs from database
36
+ const dbJobs = await this.persistence.getActiveJobs();
37
+ // Convert to BaseJobSpec format
38
+ const jobs = dbJobs.map(dbJob => ({
39
+ id: dbJob.job_id,
40
+ name: dbJob.job_id, // Using job_id as name since name field doesn't exist
41
+ command: dbJob.command,
42
+ status: this.mapDbStatusToJobStatus(dbJob.status),
43
+ createdAt: new Date(dbJob.started_at),
44
+ startedAt: new Date(dbJob.started_at),
45
+ completedAt: dbJob.completed_at ? new Date(dbJob.completed_at) : undefined,
46
+ user: this.userId,
47
+ tags: [],
48
+ priority: 5,
49
+ maxRetries: 3,
50
+ retryCount: 0,
51
+ databaseSync: true,
52
+ exitCode: dbJob.exit_code,
53
+ stdout: dbJob.output,
54
+ stderr: dbJob.error,
55
+ }));
56
+ return jobs;
57
+ }
58
+ mapDbStatusToJobStatus(dbStatus) {
59
+ switch (dbStatus) {
60
+ case 'running':
61
+ return 'running';
62
+ case 'completed':
63
+ case 'success':
64
+ return 'completed';
65
+ case 'stopped':
66
+ return 'stopped';
67
+ case 'paused':
68
+ return 'paused';
69
+ case 'failed':
70
+ case 'timeout':
71
+ return 'failed';
72
+ case 'killed':
73
+ return 'killed';
74
+ default:
75
+ return 'created';
76
+ }
77
+ }
78
+ async update(jobId, updates) {
79
+ // Update by saving again (upsert behavior)
80
+ if (updates.command) {
81
+ const dbJob = {
82
+ job_id: jobId,
83
+ command: updates.command,
84
+ started_at: updates.startedAt?.toISOString(),
85
+ completed_at: updates.completedAt?.toISOString(),
86
+ status: updates.status || 'running',
87
+ exit_code: updates.exitCode,
88
+ output: updates.stdout,
89
+ error: updates.stderr,
90
+ };
91
+ await this.persistence.saveJob(dbJob);
92
+ }
93
+ }
94
+ async delete(jobId) {
95
+ // DatabasePersistence doesn't have a delete method yet
96
+ // This would need to be added
97
+ console.warn(`Delete not implemented for job ${jobId}`);
98
+ }
99
+ async saveExecution(execution) {
100
+ // Map to database format and save as job
101
+ const dbJob = {
102
+ job_id: execution.jobId,
103
+ command: execution.command,
104
+ started_at: execution.startTime.toISOString(),
105
+ completed_at: execution.endTime?.toISOString(),
106
+ status: execution.status,
107
+ exit_code: execution.exitCode,
108
+ output: execution.stdout,
109
+ error: execution.stderr || execution.errorMessage,
110
+ };
111
+ await this.persistence.saveJob(dbJob);
112
+ }
113
+ async getExecutions(jobId, limit = 50) {
114
+ // Get active jobs (no specific history method available yet)
115
+ const dbJobs = await this.persistence.getActiveJobs();
116
+ const jobExecutions = dbJobs.filter(job => job.job_id === jobId);
117
+ return jobExecutions.slice(0, limit).map(dbExec => ({
118
+ executionId: `exec_${dbExec.job_id}_${Date.now()}`,
119
+ jobId: dbExec.job_id,
120
+ jobName: dbExec.job_id,
121
+ command: dbExec.command,
122
+ startTime: new Date(dbExec.started_at),
123
+ endTime: dbExec.completed_at ? new Date(dbExec.completed_at) : undefined,
124
+ duration: dbExec.completed_at
125
+ ? new Date(dbExec.completed_at).getTime() - new Date(dbExec.started_at).getTime()
126
+ : undefined,
127
+ status: this.mapDbStatus(dbExec.status),
128
+ exitCode: dbExec.exit_code,
129
+ stdout: dbExec.output,
130
+ stderr: dbExec.error,
131
+ errorMessage: dbExec.error,
132
+ }));
133
+ }
134
+ mapDbStatus(dbStatus) {
135
+ switch (dbStatus) {
136
+ case 'running':
137
+ return 'running';
138
+ case 'completed':
139
+ case 'success':
140
+ return 'completed';
141
+ case 'failed':
142
+ return 'failed';
143
+ case 'killed':
144
+ return 'killed';
145
+ case 'timeout':
146
+ return 'timeout';
147
+ default:
148
+ return 'failed';
149
+ }
150
+ }
151
+ async cleanup() {
152
+ // DatabasePersistence maintains its own connections
153
+ // No cleanup needed here
154
+ }
155
+ }
156
+ export default DatabaseJobStorage;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * In-Memory Job Storage
3
+ * Fast, volatile storage for jobs and executions
4
+ * Used by JobManager for runtime job tracking
5
+ */
6
+ export class MemoryJobStorage {
7
+ jobs = new Map();
8
+ executions = new Map();
9
+ maxExecutionsPerJob;
10
+ constructor(maxExecutionsPerJob = 100) {
11
+ this.maxExecutionsPerJob = maxExecutionsPerJob;
12
+ }
13
+ async save(job) {
14
+ this.jobs.set(job.id, { ...job });
15
+ }
16
+ async get(jobId) {
17
+ const job = this.jobs.get(jobId);
18
+ return job ? { ...job } : null;
19
+ }
20
+ async list(filter) {
21
+ let jobs = Array.from(this.jobs.values()).map(job => ({ ...job }));
22
+ // Basic filtering (additional filters applied by BaseJobManager)
23
+ if (filter?.status) {
24
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
25
+ jobs = jobs.filter(job => statuses.includes(job.status));
26
+ }
27
+ return jobs;
28
+ }
29
+ async update(jobId, updates) {
30
+ const job = this.jobs.get(jobId);
31
+ if (!job) {
32
+ throw new Error(`Job ${jobId} not found`);
33
+ }
34
+ Object.assign(job, updates);
35
+ this.jobs.set(jobId, job);
36
+ }
37
+ async delete(jobId) {
38
+ this.jobs.delete(jobId);
39
+ this.executions.delete(jobId);
40
+ }
41
+ async saveExecution(execution) {
42
+ const jobExecutions = this.executions.get(execution.jobId) || [];
43
+ // Add new execution at the beginning
44
+ jobExecutions.unshift(execution);
45
+ // Limit number of executions stored
46
+ if (jobExecutions.length > this.maxExecutionsPerJob) {
47
+ jobExecutions.length = this.maxExecutionsPerJob;
48
+ }
49
+ this.executions.set(execution.jobId, jobExecutions);
50
+ }
51
+ async getExecutions(jobId, limit) {
52
+ const jobExecutions = this.executions.get(jobId) || [];
53
+ if (limit && limit < jobExecutions.length) {
54
+ return jobExecutions.slice(0, limit);
55
+ }
56
+ return jobExecutions.map(e => ({ ...e }));
57
+ }
58
+ async cleanup() {
59
+ this.jobs.clear();
60
+ this.executions.clear();
61
+ }
62
+ // Additional utility methods
63
+ getJobCount() {
64
+ return this.jobs.size;
65
+ }
66
+ getExecutionCount(jobId) {
67
+ if (jobId) {
68
+ return this.executions.get(jobId)?.length || 0;
69
+ }
70
+ return Array.from(this.executions.values()).reduce((sum, execs) => sum + execs.length, 0);
71
+ }
72
+ }
73
+ export default MemoryJobStorage;